通用验证规则前端(JavaScript)和后端(c#)(使用 Jint JavaScript 解释器)
一种在客户端和服务器端无缝地组合规则的示例方法,使用一个公共存储。
大家好,
在我的职业生涯中,我多次遇到需要实现业务规则/验证的情况。这些需要在浏览器端和服务器端都实现。我以前见过的实现存在以下问题:
- 代码重复
- 规则管理困难
- 规则定义过于复杂。
- 评估规则的代码过多
- 难以根据先决条件选择规则
- 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
}
}
}