Click here to Skip to main content
15,881,797 members
Articles / Programming Languages / C#
Article

Validation Library using C# 3.0 Lambda Expressions

Rate me:
Please Sign up or sign in to vote.
4.77/5 (10 votes)
18 Dec 2007CPOL6 min read 67.1K   367   40   12
A very easy to use business object validation library using C# 3.0 lambda expressions

Introduction

In Enterprise Library - Validation Application Block we have attributes to define complex validation expressions. But it is too complicated and slow, because it will use a lot of casting and boxing code under the hood. In C# 3.0, we have strongly typed lambda expressions so why not use them for validation logic?

Background

Imagine this business class:

C#
public class Foo
{
  public string Name { get; set; }

  public int Index { get; set; } 
}

Let's say that the Name property can't be null or empty and the Index should be in 0 and 100 range. Now we can write these conditions to our code with lamda expressions:

C#
[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; } 
}

And we only need a solution to do the validation checks, this is why I am here for today. ;)

The Validation Library

Attributes

  • ValidatableAttribute: Indicates that a class or structure can be validated. Hasn't got any properties and inherited by derived classes.
  • RuleAttribute: Describes a rule.

Properties (all get/set)

  • string Name: Name of the rule. Default is the name of the field or property of the rule expression.
  • string Message: The message will be received when the rule fails. Default: null.
  • bool Enabled: Initializes rule enabled state. Default: true.
  • string[] AssociatedProperties: Connects rule to properties by names. Default: empty.
  • bool UseNamingConvention: Automatically connects rule to a property by name of rule. E. g.: NameRule will be linked to Name property, DateRule will be linked to Date property, etc. Default: true.

Examples

C#
[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 rule will be "NameRule" and linked to Name property by naming convention and RIndex rule will be "IndexRule" and linked to Index property by naming convention and will be disabled.

C#
[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; } 
}

Rule name will be "FooRule" and it will linked to Name and Index properties. I will explain what rule to property link means later. 

C#
class Validator

This class validates any objects and using rules defined on them by Func< T, bool > expressions.

Properties

  • public IEnumerable< RuleKey > EnabledRules { get; }: Get enabled rules (see RuleKey structure below)
  • public IEnumerable< RuleKey > DisabledRules { get; }: Get disabled rules

Methods

  • ValidateResults Validate(object instance) and ValidateResults Validate(string id, object instance): Validates an object instance. If validation context identifier (id argument) is not specified, it will be instance.GetType().FullName.
  • void ValidateToSOAPFault(object instance) and void ValidateToSOAPFault(string id, object instance): Same as above except this method will throw FaultException< ValidateResults > instead of return a result object. Useful for WCF.
  • public bool IsRuleEnabled(RuleKey ruleKey): Returns true if the rule is enabled specified by the ruleKey (see below).
  • public void EnableRule(RuleKey ruleKey): Enables rule specified by the ruleKey.
  • public void DisableRule(RuleKey ruleKey): Disables rule specified by the ruleKey.
  • public void EnableRule(RuleKey ruleKey): Enables rule specified by the ruleKey.
  • public void NewEnabledRuleContext(): Set all rules enabled state to declared value (on RuleAttribute.Enabled property).
  • public static object GetInstanceForPath(object validatedInstance, string path): Returns the object instance on validated object specified by the path (simple reflection, path can point to any member). See ValidateResults class description for path details.
  • public static T GetInstanceForPath< T >(object validatedInstance, string path) where T : class: Same as above but this one is typed.

Note: Enabled state context only applies to the Validator instance which these methods called on. For performance reasons, the Validator will not check whether the specified rule exists. It only registers these keys to an internal hashtable (see Validator.cs for implementation details).

Events

  • EventHandler< ValidationFailedEventArgs > ValidationFailed: Occurs when the validation failed
  • event EventHandler< ValidationFailedEventArgs > ValidationFailedGlobal: Occurs when the validation failed on any Validator instance

ValidationFailedEventArgs has only ValidateResults Results read-only property.

C#
[DataContract] public class ValidateResults : IEnumerable< ValidateResult >

This class describes a validation result

Properties

  • public string ID { get; }: Validation context identifier passed to Validator.Validate() method
  • public bool IsValid { get; }: Gets a flag that indicates whether the instance was valid
  • public ValidateResult[] Results { get; }: Validation result descriptions (see later)

Methods

  • public bool IsRuleFailed(RuleKey ruleKey): Indicates whether the specified rule failed identified by ruleKey (see later)
  • public ValidateResult GetResultForRule(RuleKey ruleKey): Returns validation description for specified rule
  • bool IsRuleFailed(string rulePath): Gets a flag that indicates whether the rule was valid specified by rule path. The path is WPF like access to a rule member through property/field names in validated instance, e.g.: Company.Employees[0].NameRule.
  • ValidateResult GetResultForRule(string rulePath): Returns validation description for the rule specified by rule path.
  • public bool IsPropertyFailed(string propertyPath): Gets a flag that indicates whether the property was valid specified by property path. The path is WPF like access to a rule member through property/field names in validated instance, e.g.: Company.Employees[0].Name.
  • public ValidateResult[] GetResultsForProperty(string propertyPath): Returns validation descriptions for the property specified by property path (a property can be linked to many rules with RuleAttribute).
C#
[DataContract] public sealed class ValidateResult : IComparable< ValidateResult >

This class describes a validation fail.

Properties

  • public RuleKey RuleKey { get; }: Identifies the associated rule definition (see below)
  • public string[] RulePaths { get; }: Path to rules on object instances where these validation expressions failed
  • public string[] PropertyPaths { get; }: Path to linked properties on object instances where these validation expressions failed
  • public string Message { get; }: Message defined on rule with RuleAttribute.Message property 
C#
public struct RuleKey : IEquatable< RuleKey >, IComparable< RuleKey >

It is a closed structure. Overrides the ==, != operators and the explicit string casting operator.

Constructor

  • public RuleKey(Type type, string ruleName): The type is the containing type (class or struct) and the ruleName is the name of the rule. The RuleKey struct uses an internal string representation of this information so it can travel across service boundaries with no problems.

Example

C#
[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");

Properties

  • public string RuleName { get; } /* - */ public Type Type { get; }: Name and containing type of the rule. Only accessible (not null) if the containing type's assembly is loaded.

Using the Code

Here is a sample business class from our highly normalized partner registry service:

C#
[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
}

Validating Complex Object Graphs

It is possible. Every object instance will be validated, but the property and rule paths will only indicate the first occurrence of failed validation. Rules will be checked in an object instance order by names followed by instance's properties and fields which type is [Validatable] followed by properties and fields which type is Array[T] or IEnumerable< T > where T is [Validatable]. Here is a unit test that describes this functionality. I hope this will be understandable enough.

Classes to be validated:

C#
[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;
}

Unit Test

C#
[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"));
}

If the object instance graph is a tree where every instance is identical (like WCF message contracts), this path information will be very useful of course.

Validating IEnumerables

This can be done. If you pass instances of IEnumerable< T > to a Validator where T is [Validatable] every item in the collection will be validated. The rule/property path information is going to be something like this:

C#
// Path starts with an indexer:
Assert.AreEqual("[0].name", results.Results[0].PropertyPath[0]);
Assert.AreEqual("[2].name", results.Results[0].PropertyPath[1]);

Points of Interest

The implementation relies heavily on LINQ, so feel welcome to analyze it. For example, here is a code snippet of looking for rule expressions using reflection along with LINQ (ValidationRegistry.cs):

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

Conclusion

First of all, sorry about my funny English (which is rather Hungrish). :) I've been reading tons of English documentation but I have not got enough opportunity to speak in it. I hope this will be understandable enough.

There is nothing easier than using this code. Simply define Func< T, bool > expression rules on public fields or properties (can be instance or static) on your business classes, create an instance of Validator, set enabled states of rules if they apply, and call the Validate method. I've been working on a configurable version of this library where you can define rule expressions in app.config or web.config sections based on downloadable Visual Studio 2008 LINQ Samples - DynamicQuery/Dynamic.cs.

I've included some common rule definitions to my project (email, URL, string length). See Rules.cs for details. E.g.:

C#
[Validatable]
public class SomeClass
{
  public static Func< SomeClass, bool > EMailRule = sc => Rules.EmailRule(sc.EMail);
  
  public string EMail { get; set; }
}

References

History

  • Dec. 19. 2007 - First post: proof-of-concept release

License

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


Written By
Architect 3GM Ltd.
Hungary Hungary
ex CAD MVP, MCPD 4.0

Comments and Discussions

 
QuestionExamples? Pin
kbanas26-Jul-12 12:06
kbanas26-Jul-12 12:06 
QuestionCheck properties against general rules?? Pin
GerhardKreuzer13-May-09 0:32
GerhardKreuzer13-May-09 0:32 
GeneralConfigurable Version Pin
GerhardKreuzer12-May-09 19:18
GerhardKreuzer12-May-09 19:18 
QuestionVery Nice - Serialization? Pin
Kavan Shaban5-Jan-08 22:02
Kavan Shaban5-Jan-08 22:02 
AnswerRe: Very Nice - Serialization? Pin
Gabor Mezo7-Jan-08 6:43
Gabor Mezo7-Jan-08 6:43 
GeneralRe: Very Nice - Serialization? Pin
Kavan Shaban7-Jan-08 6:53
Kavan Shaban7-Jan-08 6:53 
GeneralRe: Very Nice - Serialization? Pin
Gabor Mezo17-Jan-08 19:49
Gabor Mezo17-Jan-08 19:49 
General[Message Deleted] Pin
Jaroslav Klima19-Dec-07 9:11
Jaroslav Klima19-Dec-07 9:11 
GeneralRe: why fields? Pin
Gabor Mezo19-Dec-07 10:27
Gabor Mezo19-Dec-07 10:27 
AnswerRe: why fields? Pin
Jaroslav Klima19-Dec-07 11:40
Jaroslav Klima19-Dec-07 11:40 
GeneralRe: why fields? Pin
Gabor Mezo19-Dec-07 13:32
Gabor Mezo19-Dec-07 13:32 
Generalexcellent Pin
christoph brändle19-Dec-07 0:09
christoph brändle19-Dec-07 0:09 
i like this, very cool Big Grin | :-D

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.