General Purpose Data Annotations Validation Attribute

The Data Annotations framework, introduced in .NET 3.5, and enhanced in .NET 4.0, is likely becoming the basis for attribute-based validation in .NET. I like about it the fact that it is extensible and that it can be (and indeed it is) used in a lot of scenarios, from Silverlight to ASP.NET MVC and Dynamic Data.

Here’s an attribute I wrote for class and property validation. I wanted to be able to specify a C# validation expression, so that I could reuse the same attribute on different classes, just changing the expression, for example, “A > B”, “A != null”, and so. I chose C# as the expression language, but it can be easily made to support VB.

I used a less known feature of the DataTable class, calculated columns, that is, columns that can be produced from other columns or from plain expressions. Since calculated columns use a subset of SQL as its native language, I have to replace the C# expression for SQL, which is not that difficult, for this simple scenario.

Here’s the code:

   1: [Serializable]
   2: [AttributeUsage(AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = true)]
   3: public sealed class ExpressionValidationAttribute : ValidationAttribute
   4: {        
   5:     public ExpressionValidationAttribute(String expression)
   6:     {
   7:         this.Expression = expression;
   8:     }
   9:  
  10:     /// <summary>
  11:     /// The expression to evaluate. May not be null.
  12:     /// Supported values:
  13:     /// PropertyName
  14:     /// null
  15:     /// {0}
  16:     /// Supported operators:
  17:     /// &gt;, &lt;, &gt;=, &lt;=, ==, !=
  18:     /// </summary>
  19:     /// <example>
  20:     /// PropertyA != null
  21:     /// PropertyA > PropertyB
  22:     /// </example>
  23:     public String Expression
  24:     {
  25:         get;
  26:         private set;
  27:     }
  28:  
  29:     public override Boolean IsDefaultAttribute()
  30:     {
  31:         return (this.Expression == null);
  32:     }
  33:  
  34:     public override Boolean Equals(Object obj)
  35:     {
  36:         if (base.Equals(obj) == false)
  37:         {
  38:             return (false);
  39:         }
  40:  
  41:         if (Object.ReferenceEquals(this, obj) == true)
  42:         {
  43:             return (true);
  44:         }
  45:  
  46:         ExpressionValidationAttribute other = obj as ExpressionValidationAttribute;
  47:  
  48:         if (other == null)
  49:         {
  50:             return (false);
  51:         }
  52:  
  53:         return (other.Expression == this.Expression);
  54:     }
  55:  
  56:     public override Int32 GetHashCode()
  57:     {
  58:         Int32 hashCode = 1;
  59:  
  60:         hashCode = (hashCode * 397) ^ (this.Expression != null ? this.Expression.GetHashCode() : 0);
  61:  
  62:         return (hashCode);
  63:     }
  64:  
  65:     public static String Replace(this String originalString, String oldValue, String newValue, StringComparison comparisonType)
  66:     {
  67:         Int32 startIndex = 0;
  68:  
  69:         while (true)
  70:         {
  71:             startIndex = originalString.IndexOf(oldValue, startIndex, comparisonType);
  72:             
  73:             if (startIndex < 0)
  74:             {
  75:                 break;
  76:             }
  77:  
  78:             originalString = String.Concat(originalString.Substring(0, startIndex), newValue, originalString.Substring(startIndex + oldValue.Length));
  79:  
  80:             startIndex += newValue.Length;
  81:         }
  82:  
  83:         return (originalString);
  84:     }
  85:  
  86:     protected override ValidationResult IsValid(Object value, ValidationContext validationContext)
  87:     {
  88:         if (String.IsNullOrWhiteSpace(this.Expression) == true)
  89:         {
  90:             return (ValidationResult.Success);
  91:         }
  92:  
  93:         Object instance = validationContext.ObjectInstance;
  94:         DataTable temp = new DataTable();
  95:         String expression = this.Expression;
  96:  
  97:         while (expression.IndexOf("  ") >= 0)
  98:         {
  99:             expression = expression.Replace("  ", " ");
 100:         }
 101:  
 102:         //translate .NET language operators into SQL ones
 103:         expression = expression.Replace("!=", "<>");
 104:         expression = expression.Replace("==", "=");
 105:         expression = expression.Replace("!", " NOT ");
 106:         expression = expression.Replace("&&", " AND ");
 107:         expression = expression.Replace("||", " OR ");
 108:         expression = Replace(expression, "= NULL", " IS NULL ", StringComparison.OrdinalIgnoreCase);
 109:         expression = Replace(expression, "<> NULL", " IS NOT NULL ", StringComparison.OrdinalIgnoreCase);
 110:         expression = Replace(expression, "null", "NULL", StringComparison.OrdinalIgnoreCase);
 111:         expression = expression.Replace("{0}", validationContext.MemberName);
 112:  
 113:         PropertyDescriptor[] props = TypeDescriptor
 114:             .GetProperties(instance)
 115:             .OfType<PropertyDescriptor>()
 116:             .Where(x => x.IsReadOnly == false)
 117:             .Where(x => x.PropertyType.IsPrimitive || x.PropertyType == typeof(String))
 118:             .ToArray();
 119:  
 120:         foreach (PropertyDescriptor prop in props)
 121:         {
 122:             temp.Columns.Add(prop.Name, prop.PropertyType);
 123:         }
 124:  
 125:         temp.BeginLoadData();
 126:  
 127:         DataRow row = temp.NewRow();
 128:  
 129:         temp.Rows.Add(row);
 130:  
 131:         foreach (PropertyDescriptor prop in props)
 132:         {
 133:             row[prop.Name] = prop.GetValue(instance);
 134:         }
 135:  
 136:         DataColumn isValidColumn = new DataColumn();
 137:         isValidColumn.ColumnName = "_is_valid";
 138:         isValidColumn.Expression = expression;
 139:  
 140:         temp.Columns.Add(isValidColumn);
 141:  
 142:         temp.EndLoadData();
 143:  
 144:         Boolean isValid = Convert.ToBoolean(row[isValidColumn]);
 145:  
 146:         if (isValid == true)
 147:         {
 148:             return (ValidationResult.Success);
 149:         }
 150:         else
 151:         {
 152:             String errorMessage = this.FormatErrorMessage(validationContext.MemberName != null ? validationContext.MemberName : validationContext.ObjectInstance.GetType().Name);
 153:             return (new ValidationResult(errorMessage, ((validationContext.MemberName != null) ? new String[] { validationContext.MemberName } : Enumerable.Empty<String>())));
 154:         }
 155:     }
 156: }

And a sample usage:

   1: [ExpressionValidation("A > B")]
   2: public class Test
   3: {
   4:     public Int32 A { get; set; }
   5:     public Int32 B { get; set; }
   6:     [ExpressionValidation("{0} != null")]
   7:     public String C { get; set; }
   8: }

As you can see, this attribute can be applied to either the whole class or a single property. If it is applied to a property, the {0} placeholder can be used instead of the property name; in any case, properties are referred by their names. All properties of primitive or string types that have both a getter and a setter can be used. No complex logic is allowed, such as calling String.Length for example. Maybe next time… Winking smile

Have fun!

                             

No Comments