Sunday, May 8, 2011

Using DataAnnotations to validate entities

In every sample on the internet regarding MVC and Entity Framework 4.0, we can find the use of DataAnnotations.

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

5 comments:

  1. Hi Diego,
    Need your help as this does not seem to work for me. I am using MVC 3 with EF in a seperate assembly / project called Domain. I added the partial class in the same namespace, but MVC project does not seem to validate. Also I am using ajax to call the action. any ideas or help will be great..

    Regards
    Parvez

    ReplyDelete
  2. Well..like I mentioned in the post - in MVC DataAnnotations are quite simple.
    The only thing I can think of without your source code or additional details is something I often forget which is to add the MetadataTypeAttribute, without it MVC won't know that does Annotations belong to the specific entity.

    Good Luck,
    Diego

    ReplyDelete
  3. Thanks Diego,
    I do have the MetaDataTypeAttribute added. might be ajax unable to do model validation not sure. Or do I need to do anything specific on the MVC application project as the domain sits as a seperate class library. will dig further and come back to you.

    Regard
    Parvez

    ReplyDelete
  4. Hi Diego,
    the problem turned out to be Ninject (DI). I installed ninject through nuget and did manual config in the global.asax file based on some blog site which turned out to be issue. the system was working fine and DI hooking services but validation never happened. with ninject for mvc3 loaded with nuget, a file sits in the App_Start folder called NinjectMVC3.cs where all services need to be registered and not in Global.asax. once I did that the validations are working :) hope this helps someone.
    Regards
    Parvez

    ReplyDelete
  5. This post saved my life. Thank you so much.

    ReplyDelete