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: /// >, <, >=, <=, ==, !=
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…
Have fun!