Development With A Dot

Blog on development in general, and specifically on .NET

Sponsors

News

My Friends

My Links

Permanent Posts

Portuguese Communities

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!

Comments

No Comments