Creating custom property validation

In this recipe, we will define our own attribute for validation, so we can validate more advanced scenarios.

Getting ready

We will be using the NuGet Package Manager to install the Entity Framework 4.1 assemblies.

The package installer can be found at http://nuget.codeplex.com/.

We will also be using a database for connecting to the data and updating it.

Open the Improving Custom Property Validation solution in the included source code examples.

How to do it...

Let's get connected to the database using the following steps:

  1. We start by adding a new unit test named ValidationTests to the test project. We make a test that connects to the database, and adds an object. This will test whether the configuration and our validation code are separate concerns, by using the following code:
    using System;
    using System.Collections.Generic;
    using System.Data.Entity.Validation;
    using System.Linq;
    using System.Text;
    using System.Text.RegularExpressions;
    using BusinessLogic;
    using DataAccess;
    using DataAccess.Database;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using Test.Properties;
    using System.Data.Entity;
    
    namespace Test
    {
      [TestClass]
      public class ValidationTest
      {
    
        [TestMethod]
        [ExpectedException(typeof (DbEntityValidationException))]
        public void ShouldErrorOnEmailNotHavingAnAtSign()
        {
          //Arrange
          var init = new Initializer();
          var context = new BlogContext(Settings.Default.BlogConnection);
          init.InitializeDatabase(context);
    
          var blog = new Blog()
          {
            Creationdate = DateTime.Now,
            ShortDescription = "Test",
            Title = "This is a lot longer",
            AuthorDetail = new AuthorDetail(){Email = "Test",Name = "Test"}
          };
    
          //Act
          context.Set<Blog>().Add(blog);
          context.SaveChanges();
    
          //Assert
          Assert.Fail("Didn't Error");
        }
    
      }
    }
  2. Add an initializer to the DataAccess project in the Database folder with the following code to set up the data:
    using System;
    using System.Data.Entity;
    using BusinessLogic;
    
    namespace DataAccess.Database
    {
    
      public class Initializer : DropCreateDatabaseAlways<BlogContext>
      {
        public Initializer()
        {
    
        }
        protected override void Seed(BlogContext context)
        {
          context.Set<Blog>().Add(new Blog()
          {
            Creationdate = DateTime.Now,
            ShortDescription = "Testing",
            Title = "Test Blog",
            AuthorDetail = new AuthorDetail(){Email = "[email protected]", Name = "Test"}
          });
          context.SaveChanges();
        }
      }
    }
  3. In the BusinessLogic project, add a new C# class named Blog with the following code:
    using System;
    using System.Collections.Generic;
    using System.Text.RegularExpressions;
    
    namespace BusinessLogic
    {
      public class Blog
      {
        private const string DateBetween1900And2100Pattern = @"^(19|20)dd[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$";
    
        public int Id { get; set; }
    
        public DateTime Creationdate { get; set; }
    
        public string ShortDescription { get; set; }
    
        public string Title { get; set; }
    
        public double Rating { get; set; }
    
        [AuthorDetailValidator]
        public AuthorDetail AuthorDetail { get; set; }
      }
    }
  4. Add another C# class named AuthorDetailValidatorAttribute with the following code:
    using System.ComponentModel.DataAnnotations;
    
    namespace BusinessLogic
    {
      public class AuthorDetailValidatorAttribute : ValidationAttribute
      {
        public override string FormatErrorMessage(string name)
        {
          return string.Format(ErrorMessageString, name);
        }
    
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
          var detail = value as AuthorDetail;
    
          if (detail != null)
          {
            if (!detail.Email.Contains("@"))
            {
              return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
            }
          }
    
          return ValidationResult.Success;
        }
      }
    }
  5. Add a Mapping folder to the DataAccess project, and add a BlogMapping class to the folder with the following code:
    using System.ComponentModel.DataAnnotations;
    using System.Data.Entity.ModelConfiguration;
    using BusinessLogic;
    
    namespace DataAccess.Mappings
    {
      public class BlogMapping : EntityTypeConfiguration<Blog>
      {
        public BlogMapping()
        {
          this.ToTable("Blogs");
          this.HasKey(x => x.Id);
    
          this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity) .HasColumnName("BlogId");
    
          this.Property(x => x.Title).IsRequired().HasMaxLength(250);
    
          this.Property(x => x.Creationdate).HasColumnName("CreationDate").IsRequired();
    
          this.Property(x => x.ShortDescription).HasColumnType("Text").IsMaxLength().IsOptional().HasColumnName("Description");
        }
    
      }
    }
  6. Add another mapping C# class named AuthorDetailMapping to the folder with the following code:
    using System.ComponentModel.DataAnnotations;
    using System.Data.Entity.ModelConfiguration;
    using BusinessLogic;
    
    namespace DataAccess.Mappings
    {
      public class AuthorDetailMapping : EntityTypeConfiguration<AuthorDetail>
      {
        public AuthorDetailMapping()
        {
          this.HasKey(x => x.Id);
    
          this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
        }
      }
    }
  7. Modify the BlogContext class to contain the new mappings with the following code:
    using System;
    using System.Data.Entity;
    using System.Linq;
    using BusinessLogic;
    using DataAccess.Mappings;
    
    namespace DataAccess
    {
      public class BlogContext : DbContext, IUnitOfWork
      {
        public BlogContext(string connectionString) : base(connectionString)
        {
    
        }
    
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
          modelBuilder.Configurations.Add(new BlogMapping());
          modelBuilder.Configurations.Add(new AuthorDetailMapping());
          base.OnModelCreating(modelBuilder);
        }
    
        public IQueryable<T> Find<T>() where T : class
        {
          return this.Set<T>();
        }
    
        public void Refresh()
        {
          this.ChangeTracker.Entries().ToList().ForEach(x=>x.Reload());
        }
    
        public void Commit()
        {
          this.SaveChanges();
        }
      }
    }
  8. Run our test, and see how it works.

How it works...

We start off by defining a test that will validate our solution, and verify that we have done just enough to accomplish the goals. This step is critical to succeeding with the entity framework, as it will ensure that we do not get hit by a mapping error, or an object graph issue at runtime.

We define an attribute that inherits from ValidationAttribute, so that we have overrides for the validate method. This override gives us the object (property) that we attributed, and the validation context that allows us to evaluate complex scenarios, by pulling other property values out of the key-value pair collection. This implementation does not only gives us the property-specific validation ability, but also allows for more complex validation.

The mappings defined further restrict the structure of the objects that are tied to the context. This structure exists fully-independent from the business-level validation that we have defined in our new attribute, but will still be enforced before any save operation on the database.

There's more...

We have the power through this kind of customization to define even the most complex scenarios, but there are some things that we need to keep in mind.

Coupling

We have the ability to evaluate many properties and values to make our determination, but the more things that the validation looks at, the more tightly coupled it gets to the class that it is validating. This can increase your support load, if that type has to change drastically.

Avoiding complexity

Beware of adding too much complexity and traversing to many collections that could be lazy loaded or you will drastically affect performance and the memory footprint. If you have to have those levels of complexity in the validator make sure that you eager load the properties that you need and keep performance in mind.

See also

Chapter 1, Improving Entity Framework in the Real World:

  • Testing queries for performance
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset