65.9K
CodeProject 正在变化。 阅读更多。
Home

通用验证规则前端(JavaScript)和后端(c#)(使用 Jint JavaScript 解释器)

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.67/5 (4投票s)

2016 年 7 月 9 日

CPOL

3分钟阅读

viewsIcon

11066

一种在客户端和服务器端无缝地组合规则的示例方法,使用一个公共存储。

大家好,

在我的职业生涯中,我多次遇到需要实现业务规则/验证的情况。这些需要在浏览器端和服务器端都实现。我以前见过的实现存在以下问题:

  • 代码重复
  • 规则管理困难
  • 规则定义过于复杂。
  • 评估规则的代码过多
  • 难以根据先决条件选择规则
  • if else 梯队
  • 难以扩展

我从社区获得了一个想法,也许我们可以用简单的 JavaScript 定义规则。在客户端,评估脚本,在服务器端,使用 JavaScript 解释器运行相同的规则。规则本身可以保存在简单的 Json 样式文件中,Json 是 JavaScript 事实上的数据结构。

我将介绍我们如何为我们的需求实现这一点,当然天空是极限,您可以根据自己的意愿扩展这个概念:)。

假设我有一个业务模型,具有属性 Country 和 DateOfBirth。我还有另一个类,它选择要应用哪个规则集。这是一个最简单的例子。

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 和 Date of birth 由最终用户在浏览器端输入。为了简单起见,Age 只是一个定义的属性。

 

规则是:

DateOfBirth 是必需的。

如果 ruleSelector.RuleSet == 1,则执行以下操作…

  • 如果 country == 'USA',则 Age 应 >= 16

  • 如果 country == 'IND',则 Age 应 >= 18

如果 ruleSelector.RuleSet == 2,则执行以下操作

  • 如果 country == 'USA',则 Age 应 >= 18

  • 如果 country == 'IND',则 Age 应 >= 16

如果 ruleSelector.RuleSet <> 1 或 2,则执行以下操作

  • 如果 country == 'USA',则 Age 应 >= 15

 

让我们看一下规则本身。

{
    "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
                }
            ]
        }

    }
}

 

这个 Json 文件是一个规则集合。每个规则集都有一个键和一个值。键是规则的适用性,值是规则本身。例如,“true” 表示它是默认规则。规则按优先级顺序编写,因此如果后面的规则适用,我为了简单起见,解释这个概念。

规则是用 JavaScript 风格的语法定义的。“true” 被评估为 true,而像 “2 > 4” 这样的东西被评估为 false。我们只需将模型对象传递给 JavaScript 解释器(在我的例子中,我使用了 jInt),它可以在该对象上评估 JavaScript 规则。
 
在规则中,每个属性都给定一组规则。属性名称是键。在我们的例子中,“DataOfBirth” 是定义规则的字段。

请注意,示例中的 Json 包含更多属性,例如 IsVisible、IsEditable 等,这些属性也可以在您的场景中使用。我保留的另一件事是两个版本的 javascript,一个在客户端(带有键 javascript),另一个在服务器端(带有键 CSharp)。您甚至可以将这两个版本组合起来,编写一些可以生成这些脚本的东西,但现在,为了简单起见,我们有两个版本。

 

规则引擎看起来像这样。 这里,new Engine() 是 Jint javascript 引擎。

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();
     }
}

RuleSelector 类看起来像这样

    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;
        }
    }

一旦您在服务器端拥有这些规则,就可以通过您的 Validator 类应用它们,如下所示。

    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;
        }
    }

从顶级代码中,您可以这样调用它

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

所有这些魔法都发生在服务器端。现在,在将其发布到服务器之前,相同的规则将通过 RuleResolutor::GetMatchingRules() 返回到 UI。浏览器 JavaScript 引擎将解析 Json 并将规则绑定到控件。

//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
        }
    }

}
© . All rights reserved.