DataAnnotations allows us to add to our entities some Meta data including simple rules of validation (as well as hiding column, defining display name etc.).
By simple validation rules I mean we can define:
- Required fields
- String Length
- Range
Adding DataAnnotations is quite simple, after adding a reference to System.ComponentModel.DataAnnotations, we can add a partial class extending the relevant entity from our Entity Framework edmx and defining a metadatatype class with the rules.
For example:
[MetadataType(typeof(TrackMetaData))]
public partial class Track
{
public class TrackMetaData
{
[ScaffoldColumn(false)]
[DisplayName("Track Id")]
public int Id { get; set; }
[StringLength(50)]
public string Name{ get; set; }
[Range(typeof(DateTime), "1/1/1753", "31/12/9999",
ErrorMessage = "Value for {0} must be between {1} and {2}")]
[DisplayName("Play Date")]
public Nullable PlayDate { get; set; }
}
}
}
In this example we can see the use of ScaffoldColumn, StringLegth, DisplayName & Range...for those who work on multi language system or just prefer - you can retrieve the error message from a resource file as well.
Adding this class will work for MVC, MVC's plumbing will use this class to show error messages when relevant...but what if we have another type of client and we want to make sure data is validated before saving it to database?
Add this simple helper class to your project (or even better to your framework/core/library):
public class ValidationHelper
{
///
/// Check if specified object is valid
///
/// The object to validate ///
public static bool IsValid(object obj)
{
return (!Validate(obj).Any());
}
///
/// Validate an object against Data Annotations meta data defined against the object
///
/// The object to validate /// A List of
public static List Validate(object obj)
{
Type instanceType = obj.GetType();
Type metaData = null;
MetadataTypeAttribute[] metaAttr = (MetadataTypeAttribute[])instanceType.GetCustomAttributes(typeof(MetadataTypeAttribute), true);
if (metaAttr.Count() > 0)
{
metaData = metaAttr[0].MetadataClassType;
}
else
{
throw new InvalidOperationException("Cannot validate object, no metadata assoicated with the specified type");
}
TypeDescriptor.AddProviderTransparent(
new AssociatedMetadataTypeTypeDescriptionProvider(instanceType, metaData), instanceType);
List results = new List();
ValidationContext ctx = new ValidationContext(obj, null, null);
bool valid = Validator.TryValidateObject(obj, ctx, results, true);
return results;
}
///
/// Get Validation errors as string
///
/// The object to validate ///
public static string GetValidationErrors(object obj)
{
List errors = Validate(obj);
var errorText = new StringBuilder();
foreach (var error in errors)
{
errorText.Append(error.ErrorMessage + Environment.NewLine);
}
return errorText.ToString();
}
}
Than..add a few lines of code to your UnitOfWork (see my previous post for details: EF4 Self Tracking Entities & Repository design pattern)
public class UnitOfWork:IUnitOfWork, IDisposable
{
public void ApplyChanges(string entityName, object entity)
{
if (entity == null)
return;
if (entity is IObjectWithChangeTracker)
{
bool ok = ValidationHelper.IsValid(entity);
if (!ok)
{
string message = string.Format("Can not apply changes to the '{0}' entity due to validation errors", entity.ToString());
LogUtil.LogInfo(string.Format("{0} ({1})", message, ValidationHelper.GetValidationErrors(entity)));
throw new ValidationException(message);
}
_context.ApplyChanges(entityName, (IObjectWithChangeTracker)entity);
}
else
{
throw new ArgumentException("entity must implement IObjectWithChangeTracker to use applyChanges");
}
}
}
That's it..before the save of every entity (you implemented a MetadataType class for), it will validate the object and refuse to applyChanges or any other policy you decide is suitable to your project.
Happy validation,
Diego
