Monday, November 29, 2010

Entity Framework 4 Model first development with POCO – Implementing Entity Validations

In my previous post, I have demonstrated a sample with adding logic in the SavingChanges event on the ObjectContext class. The same event can be used for validation rules checking on the entities before the changes are applied to the data store. I have used the FluentValidation library from Jeremy Skinner for implementing the Validators on the entities. For e.g. validation rules for the Customer entity is implemented as.
public class CustomerValidator : AbstractValidator<Customer>
{
    public CustomerValidator()
    {
        RuleFor<string>(c => c.Email)
            .EmailAddress().WithMessage("Invalid Email Address")
            .WithPropertyName("Email");

        RuleFor<string>(c => c.FirstName)
            .NotNull().WithMessage("Firstname cannot be null")
            .NotEmpty().WithMessage("Firstname cannot be empty")
            .Length(5, 50).WithMessage("Firstname should be in the range 5 - 50")
            .WithPropertyName("FirstName");

        RuleFor<string>(c => c.LastName)
            .NotNull().WithMessage("LastName cannot be null")
            .NotEmpty().WithMessage("LastName cannot be empty")
            .NotEqual(c => c.FirstName).WithMessage("Firstname and lastname cannot be equal")
            .Length(5, 50).WithMessage("LastName should be in the range 5 - 50")
            .WithPropertyName("LastName");
    }
}

The BaseEntity and other entities in the POCO implementation are now changed to have a new Validate method which calls the appropriate Validator and self validates them.
public abstract class BaseEntity
{
    public Guid Id { get; set; }
    public DateTime CreatedDate { get; set; }
    public DateTime ChangedDate { get; set; }
    public byte[] Timestamp { get; set; }

    public abstract void Validate();
}

public class Customer : BaseEntity
{
    public Customer()
    {
        Orders = new HashSet<Order>();
    }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }

    public virtual ICollection<Order> Orders { get; set; }

    public override void Validate()
    {
        ValidationHelper.Validate<CustomerValidator, Customer>(this);
    }
}

And finally the ValidateEntities method to be called from the SavingChanges event
private void ValidateEntities()
{
    var validatableEntities = this.ObjectStateManager.GetObjectStateEntries
        (System.Data.EntityState.Added | System.Data.EntityState.Deleted | System.Data.EntityState.Modified)
        .Where(entity => entity.Entity is BaseEntity)
        .Select(entity => entity.Entity as BaseEntity);

    validatableEntities.ToList().ForEach(entity => entity.Validate());
}

Test Cases:
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void ObjectContext_save_should_throw_exception_if_invalid_email_is_created_for_customer()
{
    var customer = new Customer
    {
        Email = "invalidEmail",
        FirstName = "Prajeesh",
        LastName = "Prathap"
    };

    using (var context = new ShoppingCartContext())
    {
        context.Customers.AddObject(customer);
        context.SaveChanges();
    }
}

The ValidationHelper class implementation for the sample
public class ValidationHelper
{
    public static void Validate(K entity)
        where T : IValidator, new()
        where K : BaseEntity
    {
        IValidator validator = new T();
        var validationResult = validator.Validate(entity);

        if (!validationResult.IsValid)
        {
            string validationError = GetError(validationResult);
            throw new InvalidOperationException(validationError);
        }
    }

    static string GetError(ValidationResult result)
    {
        var validationErrors = new StringBuilder();
        foreach (var validationFailure in result.Errors)
        {
            validationErrors.Append(validationFailure.ErrorMessage);
            validationErrors.Append(Environment.NewLine);
        }
        return validationErrors.ToString();
    }
}

1 comment:

Jeetendra Sharma said...

Hello,

Thanks for the such a nice post. Where I can get the source code for this post