使用 C# 3.0 Lambda 表达式进行验证的库






4.77/5 (9投票s)
一个使用 C# 3.0 Lambda 表达式的非常易于使用的业务对象验证库
引言
在 Enterprise Library - Validation Application Block 中,我们有属性来定义复杂的验证表达式。但这太复杂且速度慢,因为它会在后台使用大量类型转换和装箱代码。在 C# 3.0 中,我们有了强类型 Lambda 表达式,为什么不用它们来进行验证逻辑呢?
背景
设想一下这个业务类
public class Foo
{
public string Name { get; set; }
public int Index { get; set; }
}
假设 Name
属性不能为空或为空字符串,而 Index
应该在 0
到 100
的范围内。现在我们可以使用 Lambda 表达式在代码中编写这些条件了。
[Validatable]
public class Foo
{
Func< Foo, bool > NameRule = f => !string.IsNullOrEmpty(f.Name);
Func< Foo, bool > IndexRule = f => f.Index >= 0 && f.Index <= 100;
public string Name { get; set; }
public int Index { get; set; }
}
我们只需要一个解决方案来执行验证检查,这就是我今天来的原因。 ;)
验证库
属性
ValidatableAttribute
:指示类或结构可以进行验证。没有任何属性,并由派生类继承。RuleAttribute
:描述一个规则。
属性(均为 get/set)
string Name
:规则的名称。默认是规则表达式字段或属性的名称。string Message
:规则失败时将收到的消息。默认值:null
。bool Enabled
:初始化规则的启用状态。默认值:true
。string[] AssociatedProperties
:通过名称将规则关联到属性。默认值:空。bool UseNamingConvention
:通过规则名称自动将规则关联到属性。例如:NameRule
将链接到Name
属性,DateRule
将链接到Date
属性,依此类推。默认值:true
。
示例
[Validatable]
public class Foo
{
[Rule(Name = "NameRule")]
Func< Foo, bool > RName = f => !string.IsNullOrEmpty(f.Name);
[Rule(Name = "IndexRule", Enabled = false)
Func< Foo, bool > RIndex = f => f.Index >= 0 && f.Index <= 100;
public string Name { get; set; }
public int Index { get; set; }
}
RName
规则的名称将是“NameRule
”,并通过命名约定链接到 Name
属性,RIndex
规则的名称将是“IndexRule
”,并通过命名约定链接到 Index
属性,并且将被禁用。
[Validatable]
public class Foo
{
[Rule(AssociatedProperies = new string[] { "Name", "Index" })]
Func< Foo, bool > FooRule = f => !string.IsNullOrEmpty(f.Name) &&
f.Index >=0 && f.Index<=100;
public string Name { get; set; }
public int Index { get; set; }
}
规则名称将是“FooRule
”,它将链接到 Name
和 Index
属性。稍后我将解释规则到属性的链接意味着什么。
class Validator
该库验证任何对象,并使用定义的规则,规则由 Func< T, bool >
表达式定义。
属性
public IEnumerable< RuleKey > EnabledRules { get; }
:获取启用的规则(请参阅下面的 RuleKey 结构)。public IEnumerable< RuleKey > DisabledRules { get; }
:获取禁用的规则。
方法
ValidateResults Validate(object instance)
和ValidateResults Validate(string id, object instance)
:验证对象实例。如果未指定验证上下文标识符(id
参数),则为instance.GetType().FullName
。void ValidateToSOAPFault(object instance)
和void ValidateToSOAPFault(string id, object instance)
:与上面相同,只是此方法将抛出FaultException< ValidateResults >
而不是返回结果对象。适用于 WCF。public bool IsRuleEnabled(RuleKey ruleKey)
:如果指定ruleKey
的规则已启用,则返回true
(见下文)。public void EnableRule(RuleKey ruleKey)
:启用由ruleKey
指定的规则。public void DisableRule(RuleKey ruleKey)
:禁用由ruleKey
指定的规则。public void EnableRule(RuleKey ruleKey)
:启用由ruleKey
指定的规则。public void NewEnabledRuleContext()
:将所有规则的启用状态设置为声明值(在RuleAttribute.Enabled
属性上)。public static object GetInstanceForPath(object validatedInstance, string path)
:返回由路径指定的经过验证对象上的对象实例(简单的反射,路径可以指向任何成员)。有关路径详情,请参阅ValidateResults
类说明。public static T GetInstanceForPath< T >(object validatedInstance, string path) where T : class
:与上面相同,但这是类型化的。
注意:启用状态上下文仅适用于调用这些方法的 Validator 实例。为了提高性能,Validator 不会检查指定规则是否存在。它只将这些键注册到一个内部哈希表中(有关实现详情,请参阅Validator.cs)。
事件
EventHandler< ValidationFailedEventArgs > ValidationFailed
:验证失败时发生。event EventHandler< ValidationFailedEventArgs > ValidationFailedGlobal
:在任何 Validator 实例上验证失败时发生。
ValidationFailedEventArgs
只有一个 ValidateResults Results
只读属性。
[DataContract] public class ValidateResults : IEnumerable< ValidateResult >
此类描述一个验证结果。
属性
public string ID { get; }
:传递给Validator.Validate()
方法的验证上下文标识符。public bool IsValid { get; }
:获取一个标志,指示实例是否有效。public ValidateResult[] Results { get; }
:验证结果描述(稍后见)。
方法
public bool IsRuleFailed(RuleKey ruleKey)
:指示指定的规则是否因ruleKey
而失败(稍后见)。public ValidateResult GetResultForRule(RuleKey ruleKey)
:返回指定规则的验证描述。bool IsRuleFailed(string rulePath)
:获取一个标志,指示规则是否通过规则路径指定的有效。该路径是 WPF 风格的访问,通过经过验证实例中的属性/字段名称访问规则成员,例如:Company.Employees[0].NameRule
。ValidateResult GetResultForRule(string rulePath)
:返回由规则路径指定的规则的验证描述。public bool IsPropertyFailed(string propertyPath)
:获取一个标志,指示属性是否通过属性路径指定的有效。该路径是 WPF 风格的访问,通过经过验证实例中的属性/字段名称访问规则成员,例如:Company.Employees[0].Name
。public ValidateResult[] GetResultsForProperty(string propertyPath)
:返回由属性路径指定的属性的验证描述(一个属性可以与RuleAttribute
关联多个规则)。
[DataContract] public sealed class ValidateResult : IComparable< ValidateResult >
此类描述一个验证失败。
属性
public RuleKey RuleKey { get; }
:标识关联的规则定义(见下文)。public string[] RulePaths { get; }
:在对象实例中,这些验证表达式失败的规则路径。public string[] PropertyPaths { get; }
:在对象实例中,这些验证表达式失败的关联属性路径。public string Message { get; }
:在RuleAttribute.Message
属性上定义的规则消息。
public struct RuleKey : IEquatable< RuleKey >, IComparable< RuleKey >
这是一个封闭结构。重写 ==
、!=
运算符和显式 string
转换运算符。
构造函数
public RuleKey(Type type, string ruleName)
:type
是包含的type
(类或结构),ruleName
是规则的名称。RuleKey
结构使用此信息的内部string
表示形式,因此它可以跨服务边界无问题地传输。
示例
[Validatable]
public class Foo
{
Func< Foo, bool > NameRule = f => !string.IsNullOrEmpty(f.Name);
[Rule(Name = "IndexRule")
Func< Foo, bool > RIndex = f => f.Index >=0 && f.Index<=100;
public string Name { get; set; }
public int Index { get; set; }
}
// ...
RuleKey keyForNameRule = new RuleKey(typeof(Foo), "NameRule");
RuleKey keyIndexRule = new RuleKey(typeof(Foo), "IndexRule");
属性
public string RuleName { get; } /* - */ public Type Type { get; }
:规则的名称和包含类型。仅当包含类型的程序集已加载时才可访问(不为null
)。
Using the Code
这是我们高度规范化的合作伙伴注册服务中的一个示例业务类。
[Validatable]
[DataContract]
[Serializable]
public class PublicPlaceModel : EntityModel
{
#region Rules
public static Func< PublicPlaceModel, bool > PublicPlaceUIDRule =
m => m.PublicPlaceUID != Guid.Empty;
public static Func< PublicPlaceModel, bool > SettlementUIDRule =
m => m.SettlementUID != Guid.Empty;
public static Func< PublicPlaceModel, bool > PublicPlaceNameIDRule =
m => m.PublicPlaceNameID > 0;
public static Func< PublicPlaceModel, bool > PublicPlaceTypeIDRule =
m => m.PublicPlaceTypeID > 0;
public static Func< PublicPlaceModel, bool > PostalCodeRule =
m => GeoRules.IsValidPostalCode(m.PostalCode);
//Complex business rule from two properties
[Rule(AssociatedProperties = new string[] { "PostalCode", "SettlementPart" })]
public static Func< PublicPlaceModel, bool > PublicPlaceStateRule =
m => GeoRules.CheckPublicPlaceState(m.PostalCode, m.SettlementPart);
#endregion
//Validated on base class
public Guid? PublicPlaceUID
{
get { return base.EntityUID; }
set { base.EntityUID = value; }
}
[DataMember]
public Guid SettlementUID;
[DataMember]
public int PublicPlaceNameID;
[DataMember]
public int PublicPlaceTypeID;
[DataMember]
public int? PostalCode;
[DataMember]
public string SettlementPart;
}
// Somewhere on service facade implementation:
// We have a request WCF message (message contract)
// which has a public property of type PublicPlaceModel:
// SomeResult, SomeRequest are WCF message contracts
public SomeResult DoSomething(SomeRequest request)
{
Validator v = new Validator();
v.ValidateToSOAPFault(request);
// If SOAP request is invalid a FaultException< ValidateResults >
// will be thrown and returned to consumer.
// Ok. Request is valid.
// Do stuff.
return result; // SomeResult message
}
验证复杂的对象图
这是可能的。每个对象实例都会被验证,但属性和规则路径只会指示失败验证的第一次出现。规则将按对象实例的顺序检查,首先按名称,然后是类型为 [Validatable]
的实例的属性和字段,然后是类型为 Array[T]
或 IEnumerable< T >
且 T
为 [Validatable]
的属性和字段。这是一个描述此功能的单元测试。希望这足够易于理解。
要验证的类
[Validatable]
public class AB
{
public static Func< AB, bool > aRule = abc => abc.a != null;
public static Func< AB, bool > a2Rule = abc => abc.a2 != null;
public static Func< AB, bool > fooRule = abc => !string.IsNullOrEmpty(abc.foo);
public B[] bs;
public A a;
public A a2;
public string foo;
}
[Validatable]
public class A
{
public static Func< A, bool > bRule = ac => ac.b != null;
public B b;
}
[Validatable]
public class B
{
public static Func< B, bool > nameRule = cb => !string.IsNullOrEmpty(cb.name);
public string name;
public AB ab;
}
[Validatable]
public class C
{
public B b;
public A a;
}
单元测试
[TestMethod()]
public void ComplexObjectGraphTest()
{
// Make a complicated graph of object instances:
A aTest = new A { b = new B() };
A aTest2 = new A { b = new B() };
AB abTest = new AB { a = aTest, a2 = aTest2 };
C cTest = new C { b = aTest.b, a = aTest };
aTest.b.ab = abTest;
abTest.bs = new B[] { new B { name = "helo" }, new B { ab = abTest } };
// Create a validator instance:
Validator v = new Validator();
// Test 'em!
ValidateResults abResults = v.Validate(abTest);
Assert.IsFalse(abResults.IsValid);
// Check property paths. This will be same as rule paths so it will be enough.
// Two rule failed.
Assert.AreEqual(2, abResults.Results.Length);
// First instance occurrences using the search rule above:
// fooRule on foo field of AB class instance abTest.
Assert.AreEqual(1, abResults.Results[0].PropertyPaths.Length);
// 3 B class instance nameRule on name field.
Assert.AreEqual(3, abResults.Results[1].PropertyPaths.Length);
Assert.IsTrue(abResults.IsPropertyFailed("foo"));
Assert.IsTrue(abResults.IsPropertyFailed("a.b.name"));
Assert.IsTrue(abResults.IsPropertyFailed("a2.b.name"));
Assert.IsTrue(abResults.IsPropertyFailed("bs[1].name"));
// And so on with this logic:
//A Test
ValidateResults aResult = v.Validate(aTest);
Assert.IsFalse(aResult.IsValid);
Assert.AreEqual(2, abResults.Results.Length);
Assert.AreEqual(1, abResults.Results[0].PropertyPaths.Length);
Assert.AreEqual(3, abResults.Results[1].PropertyPaths.Length);
Assert.IsTrue(aResult.IsPropertyFailed("b.ab.foo"));
Assert.IsTrue(aResult.IsPropertyFailed("b.ab.a2.b.name"));
Assert.IsTrue(aResult.IsPropertyFailed("b.ab.bs[1].name"));
Assert.IsTrue(aResult.IsPropertyFailed("b.name"));
//C Test
ValidateResults cResult = v.Validate(cTest);
Assert.IsFalse(cResult.IsValid);
Assert.AreEqual(2, abResults.Results.Length);
Assert.AreEqual(1, abResults.Results[0].PropertyPaths.Length);
Assert.IsTrue(aResult.IsPropertyFailed("b.ab.foo"));
Assert.IsTrue(aResult.IsPropertyFailed("b.ab.a2.b.name"));
Assert.IsTrue(aResult.IsPropertyFailed("b.ab.bs[1].name"));
Assert.IsTrue(aResult.IsPropertyFailed("b.name"));
}
如果对象实例图是树,其中每个实例都相同(例如 WCF 消息契约),那么路径信息在这种情况下会非常有帮助。
验证 IEnumerables
这是可以做到的。如果您将 IEnumerable< T >
的实例传递给 [Validatable]
类型的 Validator,那么集合中的每个项目都将被验证。规则/属性路径信息将如下所示。
// Path starts with an indexer:
Assert.AreEqual("[0].name", results.Results[0].PropertyPath[0]);
Assert.AreEqual("[2].name", results.Results[0].PropertyPath[1]);
关注点
实现严重依赖 LINQ,欢迎您进行分析。例如,这里有一个使用反射和 LINQ(ValidationRegistry.cs)查找规则表达式的代码片段。
private static RuleMetadata[] GetRulesOf(Type type)
{
// Get rule expression properties query :
var piQuery = from pi in type.GetProperties(BindingFlags.Public |
BindingFlags.DeclaredOnly |
BindingFlags.GetProperty |
BindingFlags.Instance |
BindingFlags.Static) // Reflect them
// Only looking for delegates
where pi.PropertyType.IsSubclassOf(typeof(Delegate))
// Getting built-in Invoke method
let invoke = pi.PropertyType.GetMethod("Invoke")
let pars = invoke.GetParameters() // Getting Invoke parameters
where invoke.ReturnType == typeof(bool) &&
pars.Length == 1 &&
pars[0].ParameterType == type // Only selecting Func< T, bool> ones
select new RuleMetadata(pi); // Generating metadata from property info
// Same query for fields :
var miQuery = from mi in type.GetFields(BindingFlags.Public |
BindingFlags.DeclaredOnly |
BindingFlags.GetField |
BindingFlags.Instance |
BindingFlags.Static)
where mi.FieldType.IsSubclassOf(typeof(Delegate))
let invoke = mi.FieldType.GetMethod("Invoke")
let pars = invoke.GetParameters()
where invoke.ReturnType == typeof(bool) &&
pars.Length == 1 &&
pars[0].ParameterType == type
select new RuleMetadata(mi);
// Run queries, concat the results, sort them and
// return an array from the result set.
return piQuery.Concat(miQuery).OrderBy(meta => meta.ruleKey).ToArray();
}
结论
首先,抱歉我的英语不好(更像是“Hungrish”)。:) 我读了很多英文文档,但没有很多机会说英文。希望这足够易于理解。
使用这段代码再简单不过了。只需在业务类的 public
字段或属性(可以是实例或静态的)上定义 Func< T, bool >
表达式规则,创建 Validator 实例,如果适用,设置规则的启用状态,然后调用 Validate
方法。我一直在开发这个库的可配置版本,您可以在 app.config 或 web.config 部分中定义规则表达式,基于可下载的 Visual Studio 2008 LINQ 示例 - DynamicQuery/Dynamic.cs。
我在项目中包含了一些常见的规则定义(电子邮件、URL、字符串长度)。有关详细信息,请参阅 Rules.cs。例如:
[Validatable]
public class SomeClass
{
public static Func< SomeClass, bool > EMailRule = sc => Rules.EmailRule(sc.EMail);
public string EMail { get; set; }
}
参考文献
历史
- 2007 年 12 月 19 日 - 首次发布:概念验证版本。