Tuesday, May 01, 2012

Adding Data Validation to Entity Framework Entities

Validating data entered into EF entites is a cross-cutting concern that you can implement by creating partial class entities, supplementing the designer-generated, partial class entities with validation logic. Personally, I prefer to place any validation logic in the business layer of an application. However, certain validation rules are not necessarily derived via business requirements or user cases, so it's often difficult to ascertain whether a particular validation indeed belongs to the business layer or elsewhere. One example of a validation rule that is not a result of a business requirement or user case, but that should be a given (i.e., assumed to be the case), is that an end date should always occur after a start date. Another example is that a person's name should not contain special characters, such as !, #, etc. And yet another example is that a phone number should not contain letters or other unnecessary characters.


Follow the steps below to learn how to extend existing designer-generated EF entities to implement validation checks.
  1. Add a reference to the System.ComponentModel.DataAnnotations.dll assembly in the project containing the EF EDMX file. This assembly contains the validation features we will use for implementing and returning validation results (IValidatableObject and ValidationResult).
  2. Create a new class file that will hold the new partial class responsible for validating the single EF entity class. In my example, I have an EF entity named NewHire, so my class file is named NewHireValidation.cs.
  3. In the validation class file, add a using reference to the using System.ComponentModel.DataAnnotations namespace.
    using System.ComponentModel.DataAnnotations;
  4. Add a partial class that implements the IValidatableObject interface and matches the name of the EF entity class being extended/supplemented. In my example, I've created a partial classed named NewHireValidation. The namespace the class belongs to must be the same as the namespace the EF entity classes belong to. Here's my example:
    namespace DotNetFun.NewHires.Data
    {
     public partial class NewHire : IValidatableObject
     {
     }
    }
    
  5. Add a private variable that will hold a list of validation errors. I named mine validationErrors:
    private IList<validationresult> validationErrors = new List<validationresult>();
  6. Implement the Validate(ValidationContext validationContext) method, and add validation logic to it. Here's my example:
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
      {
       // We don't want to hold duplicate/redundant error messages, so clear them out
       // every time this method is called, prior to adding any.
       this.validationErrors.Clear();
    
       // Ensure the end date is NOT equal to or less than the start date if an 
       // end date has been provided.
       if (this.EndDate.HasValue)
       {
        if (this.EndDate.Value <= this.StartDate)
        {
         ValidationResult endDateValidationError = new ValidationResult(
          String.Format("The new hire end date ({0}) must be a date/time greater than the start date ({1}).",
          this.EndDate.Value,
          this.StartDate),
          new[] { "EndDate" });
    
         this.validationErrors.Add(endDateValidationError);
        }
       }
    
       return this.validationErrors;
      }
    
  7. To validate the entity, simply call the Validate method. It will return a list of any validation errors. In my example, I created a unit test with the following method:
    public void ValidateTest()
      {
       var newHire = new NewHire();
       newHire.StartDate = DateTime.Now;
       newHire.EndDate = newHire.StartDate;
       var validationErrors = newHire.Validate(null);
    
       foreach (var validationError in validationErrors)
       {
        TestContext.WriteLine(validationError.ErrorMessage);
    
        var fieldsInError = validationError.MemberNames.ToList();
        if (fieldsInError.Count > 0)
        {
         TestContext.WriteLine("Field(s) in error:");
         foreach (var fieldInError in fieldsInError)
          TestContext.WriteLine(fieldInError);
        }
       }
      }
    
My entire example validation class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;

namespace DotNetFun.NewHires.Data
{
 public partial class NewHire : IValidatableObject
 {
  private IList<validationresult> validationErrors = new List<validationresult>();

  partial void OnEndDateChanging(DateTime? value)
  {
   // Optionally, you can add validation checks whenever a property/field changes.
   // But I'd prefer to check on-demand, as opposed to potentially duplicating 
   // errors or creating more errors than is necessary.
  }

  public IEnumerable<validationresult> Validate(ValidationContext validationContext)
  {
   // We don't want to hold duplicate/redundant error messages, so clear them out
   // every time this method is called, prior to adding any.
   this.validationErrors.Clear();

   // Ensure the end date is NOT equal to or less than the start date if an 
   // end date has been provided.
   if (this.EndDate.HasValue)
   {
    if (this.EndDate.Value <= this.StartDate)
    {
     ValidationResult endDateValidationError = new ValidationResult(
      String.Format("The new hire end date ({0}) must be a date/time greater than the start date ({1}).",
      this.EndDate.Value,
      this.StartDate),
      new[] { "EndDate" });

     this.validationErrors.Add(endDateValidationError);
    }
   }

   return this.validationErrors;
  }
 }
}

No comments: