Validierungsannotation für einfache Ausdrücke

Mit der Datenannotation [ExpressionValidation] kann man Vergleiche zwischen zwei Properties in einem Datenobjekt definieren.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 5 Min.
Von
  • Dr. Holger Schwichtenberg

Microsoft unterstützt inzwischen an mehreren Stellen im .NET Framework (z. B. ASP.NET Dynamic Data, WCF RIA Services, Model Binding in ASP.NET MVC und ASP.NET Webforms) die sogenannten Datenannotationen (Namensraum System.ComponentModel.DataAnnotations in der gleichnamigen Assembly), insbesondere die dort hinterlegten Validierungsannotationen wie [Required], [Range], [RegularExpression] und [StringLength].

Man kann selbst eigene Validierungsannotationen schreiben, indem man von der Klasse ValidationAttribute erbt. Ricardo Peres zeigt in einem Blog-Eintrag, wie man eine Validierungsannotationen erzeugt, mit der man Properties in einem Datenobjekt vergleichen kann, z. B.

[ExpressionValidation("FreiePlaetze != null && FreiePlaetze >= 
0 && Plaetze >= 10 && Plaetze <= 250 && FreiePlaetze <= Plaetze",
ErrorMessage = "Platzkapaität ist zwischen 10 und 250 Plätzen.
Freie Plätze müssen > 0 und kleiner als Plätze sein.")]
public partial class Flug
{
  public int Plaetze { get; set; }
public int FreiePlaetze { get; set; }
...
}

Allerdings musste ich feststellen, dass seine Validierungsannotation nicht funktioniert, wenn eines der beteiligten Properties ein Nullable Value Type (Nullable<Typ> z. B. Nullable<int> ist). Nullable Value Types konnte man nicht in den Ausdrücken verwenden. Daher habe ich seinen Programmcode ergänzt um diese Option.

Die Herausforderung besteht dabei darin, dass die Data Table, die Ricardo Peres für die Auswertung der Ausdrücke verwendet, keine Null-Werte unterstützt, sondern nur DBNull. Eine noch flexiblere Lösung mit logischen Operatoren wäre natürlich möglich auf Basis von Expression Trees, aber ich komme in meinem aktuellen Projekt vorerst mit den gegebenen Möglichkeiten zurecht.

Hier der modifizierte Programmcode, die Modifikationen habe ich rot hervorgehoben:

using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Data;
using System.Linq;
namespace de.ITVisions.Validation
{

/// <summary>
/// Validierungsannotation für einfache Ausdrücke
/// Quelle: http://weblogs.asp.net/ricardoperes/archive/2012/02/20/
/// general-purpose-data-annotations-validation-attribute.aspx
/// Modifiziert durch Holger Schwichtenberg zur Unterstützung für
/// Nullable Value Types
/// </summary>
[Serializable]
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class
| AttributeTargets.Struct, AllowMultiple = true, Inherited = true)]
public sealed class ExpressionValidationAttribute : ValidationAttribute
{
public ExpressionValidationAttribute(String expression)
{
this.Expression = expression;
}
  /// <summary>
/// The expression to evaluate. May not be null.
/// Supported values
/// PropertyName
/// null
/// {0}
/// Supported operators
/// &gt;, &lt;, &gt;=, &lt;=, ==, !=
/// </summary>
/// <example>
/// PropertyA != null
/// PropertyA > PropertyB
/// </example>
public String Expression
{
get;
private set;
}
  public override Boolean IsDefaultAttribute()
{
return (this.Expression == null);
}
  public override Boolean Equals(Object obj)
{
if (base.Equals(obj) == false)
{
return (false);
}
   if (Object.ReferenceEquals(this, obj) == true)
{
return (true);
}
   ExpressionValidationAttribute other = obj as 
ExpressionValidationAttribute;
   if (other == null)
{
return (false);
}
   return (other.Expression == this.Expression);
}
  public override Int32 GetHashCode()
{
Int32 hashCode = 1;
   hashCode = (hashCode * 397) ^ (this.Expression != null ? 
this.Expression.GetHashCode() : 0);
   return (hashCode);
}
  public String Replace(String originalString, String oldValue, 
String newValue, StringComparison comparisonType)
{
Int32 startIndex = 0;
   while (true)
{
startIndex = originalString.IndexOf(oldValue, startIndex,
comparisonType);
    if (startIndex < 0)
{
break;
}
    originalString = String.Concat(originalString.Substring(0, 
startIndex), newValue, originalString.Substring(startIndex
+ oldValue.Length));
    startIndex += newValue.Length;
}
   return (originalString);
}
  protected override ValidationResult IsValid(Object value, 
ValidationContext validationContext)
{
if (String.IsNullOrWhiteSpace(this.Expression) == true)
{
return (ValidationResult.Success);
}
   Object instance = validationContext.ObjectInstance;
DataTable temp = new DataTable();
String expression = this.Expression;
   while (expression.IndexOf("  ") >= 0)
{
expression = expression.Replace(" ", " ");
}
   //translate .NET language operators into SQL ones
expression = expression.Replace("!=", "<>");
expression = expression.Replace("==", "=");
expression = expression.Replace("!", " NOT ");
expression = expression.Replace("&&", " AND ");
expression = expression.Replace("||", " OR ");
expression = Replace(expression, "= NULL", " IS NULL ",
StringComparison.OrdinalIgnoreCase);
expression = Replace(expression, "<> NULL", " IS NOT NULL ",
StringComparison.OrdinalIgnoreCase);
expression = Replace(expression, "null", "NULL",
StringComparison.OrdinalIgnoreCase);
expression = expression.Replace("{0}", validationContext.MemberName);
   PropertyDescriptor[] props = TypeDescriptor
.GetProperties(instance)
.OfType<PropertyDescriptor>()
.Where(x => x.PropertyType.IsPrimitive || x.PropertyType == typeof(String)
|| (x.PropertyType.IsGenericType &&
x.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
&& x.PropertyType.GetGenericArguments()[0].IsPrimitive)

.ToArray();
   foreach (PropertyDescriptor prop in props)
{
    if (prop.PropertyType.IsGenericType && 
prop.PropertyType.GetGenericTypeDefinition() ==
typeof(Nullable<>))
{

temp.Columns.Add(prop.Name,
prop.PropertyType.GetGenericArguments()[0]);

}
else
{

temp.Columns.Add(prop.Name, prop.PropertyType);
}

}
   temp.BeginLoadData();
   DataRow row = temp.NewRow();
   temp.Rows.Add(row);
   foreach (PropertyDescriptor prop in props)
{
object val = prop.GetValue(instance);
if (val == null) val = DBNull.Value;
row[prop.Name] =val;

}
   DataColumn isValidColumn = new DataColumn();
isValidColumn.ColumnName = "_is_valid";
isValidColumn.Expression = expression;
   temp.Columns.Add(isValidColumn);
   temp.EndLoadData();
   Boolean isValid = Convert.ToBoolean(row[isValidColumn]);
   if (isValid == true)
{
return (ValidationResult.Success);
}
else
{
String errorMessage = this.FormatErrorMessage(validationContext.
MemberName != null ? validationContext.MemberName : validationContext.ObjectInstance.GetType().Name);
return (new ValidationResult(errorMessage, ((validationContext.
MemberName != null) ? new String[] { validationContext.MemberName }
: Enumerable.Empty<String>())));
}
}
}
} ()