In this recipe, we will define our own attribute for validation, so we can validate more advanced scenarios.
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.
Let's get connected to the database using the following steps:
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"); } } }
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(); } } }
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; } } }
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; } } }
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"); } } }
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); } } }
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(); } } }
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.
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.
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.
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.