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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (9投票s)

2007 年 12 月 19 日

CPOL

6分钟阅读

viewsIcon

68654

downloadIcon

386

一个使用 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 应该在 0100 的范围内。现在我们可以使用 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”,它将链接到 NameIndex 属性。稍后我将解释规则到属性的链接意味着什么。 

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.configweb.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 日 - 首次发布:概念验证版本。
© . All rights reserved.