ASP.NET MVC: How to execute Data Annotation validations in the service layer

asp.net-mvcasp.net-mvc-2data-annotations

In a recent question posed here:
ASP.NET MVC: Is Data Annotation Validation Enough?

…it was concluded that relying on data annotation validation (triggered by the model binder) was not enough to ensure that validation was always executed. We still need to add the same validation logic in the services layer (or somewhere else after ModelBinding happens). The unfortunately thing about this is that we will be duplicating our validation code (once with Data Annotations and again in the services layer). Is there an easy way for the services layer to trigger validation based on what's been defined in Data Annotations? If this can be possible, then we will get the best of both worlds…we won't need to repeat the validation code, but we'll still ensure that the validation always gets executed.

Best Answer

With the help of this blog: http://goneale.com/2009/03/04/using-metadatatype-attribute-with-aspnet-mvc-xval-validation-framework/ I was able to create a method that will test my object based on validations defined by data annotations. It will execute any validation attribute that derives from ValidateAttribute. I can now pass my object to this method from my service layer (or DomainModel) and my service layer is no longer dependent on the controller. This will ensure that validation will always be executed prior to persisting data into the database. I couldn't use the code on the blog as is, as I don't seem to have access to some of the extension methods that Graham was using, so here's my version of it:

    public static IList<KeyValuePair<string, string>> GetErrors(object obj)
    {
        // get the name of the buddy class for obj
        MetadataTypeAttribute metadataAttrib = obj.GetType().GetCustomAttributes(typeof(MetadataTypeAttribute), true).FirstOrDefault() as MetadataTypeAttribute;

        // if metadataAttrib is null, then obj doesn't have a buddy class, and in such a case, we'll work with the model class
        Type buddyClassOrModelClass = metadataAttrib != null ? metadataAttrib.MetadataClassType : obj.GetType();

        var buddyClassProperties = TypeDescriptor.GetProperties(buddyClassOrModelClass).Cast<PropertyDescriptor>();
        var modelClassProperties = TypeDescriptor.GetProperties(obj.GetType()).Cast<PropertyDescriptor>();

        var errors = from buddyProp in buddyClassProperties
                           join modelProp in modelClassProperties on buddyProp.Name equals modelProp.Name // as this is an inner join, it will return only the properties that are in both the buddy and model classes
                           from attribute in buddyProp.Attributes.OfType<ValidationAttribute>() // get only the attributes of type ValidationAttribute
                           where !attribute.IsValid(modelProp.GetValue(obj))
                           select new KeyValuePair<string, string>(buddyProp.Name, attribute.FormatErrorMessage(string.Empty));

        return errors.ToList();
    }

This code works with both classes that do and don't have buddy classes, although if you don't use buddy classes, this code can be simplified a bit. I hope you find this useful.