Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Javascript

Common Validation rules FrontEnd(JavaScript) and BackEnd(c#) using JavaScript interpreter(Jint)

3.67/5 (4 votes)
9 Jul 2016CPOL3 min read 10.6K  
A Sample way of combining the rules at client and server seamlessly at a common store.

Howdy,

I had many times in my career, came across the situation where we had to implement business rules/validations. These needed to be at the browser side as well as the server side. The implementations I’ve seen before had these issues

  • duplication of code
  • difficult management of rules
  • too complex rule definitions.
  • too much code to evaluate a rule
  • difficulty to select rules based on preconditions
  • if else ladders
  • difficult to extend

I got the idea from the community that maybe we can define the rules in simple JavaScript. On the client side, eval the scripts and on the server, use a JavaScript interpreter to run the same rules. The rules itself can be kept in simple Json style files which is the de-facto data structure for JavaScript.

I’ll go over how we implemented this for our requirements, of course sky is the limit and you can extend this concept as your wishes :).

lets say I have a Business model, with properties, Country and DateOfBirth. I also have another class which selects which rule set to apply. here is a simplest example.

C#
public class Model
{
   public string Country { get; set; }
   public DateTime DataOfBirth { get; set; }
   public int Age { get { return DateTime.Now.Year - DataOfBirth.Year; } }
}

public class RuleSelector
{
   public int RuleSet { get; set; }
}

Country and Date of birth are being input by the end user at the browser side. Age is just a property defined for simplicity.

 

The rules are,

DateOfBirth is required,

If ruleSelector.RuleSet== 1, then following…

  • if country==’USA’, Age should be >= 16

  • if country ==’IND’, Age should be >= 18

if ruleSelector.RuleSet == 2, then following

  • if country==’USA’, Age should be >= 18

  • if country ==’IND’, Age should be >= 16

if ruleSelector.RuleSet <> 1 or 2, then following

  • if country==’USA’, Age should be >= 15

 

lets look at the rules itself.

JavaScript
{
    "true": {
        "DataOfBirth": {
            "IsVisible": "true",
            "IsEditable": "true",
            "DependsOn": null,
            "Required": {
                "Javascript": "true",
                "CSharp": "true",
                "ErrorKey": null
            },
            "InputRegex": null,
            "DefaultValue": null,
            "Validations": [
                {
                    "Javascript": "new Date(jQuery.now()).getFullYear() - new Date($('#DataOfBirth').val()).getFullYear() >= 15",
                    "CSharp": "m.Age >= 15",
                    "ErrorKey": null
                }
            ]
        }

    },
    "m.RuleSet === 1": {
        "DataOfBirth": {
            "IsVisible": "true",
            "IsEditable": "true",
            "DependsOn": null,
            "Required": {
                "Javascript": "true",
                "CSharp": "true",
                "ErrorKey": null
            },
            "InputRegex": null,
            "DefaultValue": null,
            "Validations": [
                {
                    "Javascript": "if($('#Country').val() === 'USA'){ new Date(jQuery.now()).getFullYear() - new Date($('#DataOfBirth').val()).getFullYear() >= 16 } else if($('#Country').val() === 'IND'){ new Date(jQuery.now()).getFullYear() - new Date($('#DataOfBirth').val()).getFullYear() >= 18 } else { true }",
                    "CSharp": "if(m.Country === 'USA') { m.Age >= 16 } else if(m.Country === 'IND') { m.Age >= 18 } else { true }",
                    "ErrorKey": null
                }
            ]
        }

    },
    "m.RuleSet === 2": {
        "DataOfBirth": {
            "IsVisible": "true",
            "IsEditable": "true",
            "DependsOn": null,
            "Required": {
                "Javascript": "true",
                "CSharp": "true",
                "ErrorKey": null
            },
            "InputRegex": null,
            "DefaultValue": null,
            "Validations": [
                {
                    "Javascript": "if($('#Country').val() === 'USA'){ new Date(jQuery.now()).getFullYear() - new Date($('#DataOfBirth').val()).getFullYear() >= 18 } else if($('#Country').val() === 'IND'){ new Date(jQuery.now()).getFullYear() - new Date($('#DataOfBirth').val()).getFullYear() >= 16 } else { true }",
                    "CSharp": "if(m.Country === 'USA') { m.Age >= 18 } else if(m.Country === 'IND') { m.Age >= 16 } else { true }",
                    "ErrorKey": null
                }
            ]
        }

    }
}

 

This Json file is a collection of rules. Every rule set has a key and a value. the key is the applicability of the rule, and the value is the rule itself. for e.g. “true”  means its the Default rule. The rules are written in the order of priority, so if a later rule is applicable, I’ve kept it simple to explain the concept.

The rules are defined in JavaScript style syntax. “true” is evaluated true, and something like “2 > 4” is evaluated false. We just pass the model object to the JavaScript interpreter, (in my case, I have used jInt)  which can evaluate the JavaScript rule on that object.
 
In the rule, every property is given a set of rules. the property name is the key. In our example, “DataOfBirth” is the field where rules are defined.

Note that the Json in the example contains more properties like IsVisible, IsEditable etc. which can also be used in your scenarios. Another thing that i’ve kept is that two versions of javascript, one at client side(with key javascript) and for server side(with key CSharp). You can even combine these two and write something of top which can generate these scripts, but  for now, again for simplicity we have two versions.

 

The rule engine would simply look like this. Here, new Engine() is the Jint javascript engine.

C#
using Jint;
public class RuleEngine : IRuleEngine
{
    private Engine _engine = new Engine();

    public bool EvaluateRule(string rule, object model)
     {
        _engine.SetValue("m", model).Execute(rule);
        return (bool)_engine.GetCompletionValue().ToObject();
     }
}

The RuleSelector Class would look like this

C#
public class RuleSelector
{
    private JObject _rules = (JObject)JsonConvert.DeserializeObject("{}");
    private IRuleEngine engine = new RuleEngine();

    public int RuleSet { get; set; }

    public JObject GetMatchingRules()
    {
        if (File.Exists("rules.json"))
        {
            var overrideRules = (JObject)JsonConvert.DeserializeObject(File.ReadAllText("rules.json"));

            if (overrideRules != null)
                foreach (var ruleGroup in overrideRules.Properties())
                {
                    if (engine.EvaluateRule(ruleGroup.Name, this))
                        _rules.Merge(ruleGroup.Value);
                }
        }
        return _rules;
    }
}

Once you have the rules in the server side, applying them could be done by your Validator classes like this.

C#
public class Validator
{
    public class ErrorDetails
    {
        public string ErrorCode { get; set; }
    }

    public static List<ErrorDetails> ValidateModel(dynamic requirements, object model)
    {
        var ret = new List<ErrorDetails>();

        foreach (var member in requirements.Properties())
        {
            var value = model.GetType().GetProperty(member.Name).GetValue(model);

            if (PropertyExists(member.Value, "Validations"))
            {
                foreach (dynamic item in member.Value.Validations)
                {
                    if (PropertyExists(item, "CSharp"))
                    {
                        string cSharp = item.CSharp;
                        bool validated;
                        var engine = new Jint.Engine().SetValue("m", model).Execute(cSharp);
                        validated = (bool)engine.GetCompletionValue().ToObject();

                        if (!validated)
                            ret.Add(new ErrorDetails
                            {
                                ErrorCode = "ValidationFailed.",
                            });
                    }
                }
            }
            if (PropertyExists(member.Value, "Required"))
                if (PropertyExists(member.Value.Required, "CSharp"))
                {
                    string cSharp = member.Value.Required.CSharp;
                    bool required;
                    var engine = new Jint.Engine().SetValue("m", model).Execute(cSharp);
                    required = (bool)engine.GetCompletionValue().ToObject();

                    if (required && (value == null || String.IsNullOrEmpty(value.ToString())))
                        ret.Add(new ErrorDetails
                        {
                            ErrorCode = "RequiredFieldNotProvided.",
                        });
                }
        }
        return ret;
    }

    private static bool PropertyExists(object target, string name)
    {
        var site = System.Runtime.CompilerServices.CallSite<Func<System.Runtime.CompilerServices.CallSite, object, object>>.Create(Microsoft.CSharp.RuntimeBinder.Binder.GetMember(0, name, target.GetType(), new[] { Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create(0, null) }));
        return site.Target(site, target) != null;
    }
}

and from the top level code, you can all this by

C#
var errorList = ValidateModel(new RuleSelector().GetMatchingRules(), model);

All this magic happens at the server side. Now before posting it to the server, same rules, via  RuleResolutor::GetMatchingRules() will be returned to the UI. The browser JavaScript engine would parse the Json and bind the rules to the controls.

JavaScript
//JavaScript
//fieldName = "DataOfBirth"
function validateControls(fieldName) {

    //JsonValidation is the json which comes from RuleResolutor::GetMatchingRules(),
    var controlRules = JsonValidation.Items[fieldName];
    var control = $('#' + fieldName);

    if (controlRules.Validations) {
        //here is the actual validation being evaluated.
        if (eval(controlRules.Validations[0].Javascript)) {
            //validation is fine
        } 
        else {
            //validation failed
        }
    }

}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)