In this recipe, we will be creating complex business rule validators that allow us to evaluate more than one property at a time, and the contents of a property.
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.org.
We will also be using a database for connecting to the data and updating it.
Open the Improving Complex 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 properly separated: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 ShouldErrorOnTitleLongerThanDescription() { //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" }; //Act context.Set<Blog>().Add(blog); context.SaveChanges(); //Assert Assert.Fail("Didn't Error"); } [TestMethod] [ExpectedException(typeof(DbEntityValidationException))] public void ShouldErrorOnTitleContainsAt() { //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 = "That" }; //Act context.Set<Blog>().Add(blog); context.SaveChanges(); //Assert Assert.Fail("Didn't Error"); } [TestMethod] [ExpectedException(typeof(DbEntityValidationException))] public void ShouldErrorOnTitleAndDescriptionLengthIs15() { //Arrange var init = new Initializer(); var context = new BlogContext(Settings.Default.BlogConnection); init.InitializeDatabase(context); var blog = new Blog() { Creationdate = DateTime.Now, ShortDescription = "Testing", Title = "Somethin" }; //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" }); context.SaveChanges(); } } }
BusinessLogic
project, add a new C# class named Blog
with the following code:using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; namespace BusinessLogic { public class Blog : IValidatableObject { 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; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { List<ValidationResult> results = new List<ValidationResult>(); var target = ((Blog)validationContext.ObjectInstance); if (target.Title.Length > target.ShortDescription.Length) results.Add(new ValidationResult("Description cannot be shorter than the title")); if(target.Title.Contains("at"))results.Add(new ValidationResult("WE hate the word at!")); if(target.Title.Length + target.ShortDescription.Length == 15) results.Add(new ValidationResult("No adding to 15!!!")); return results; } } }
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"); } } }
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()); 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 creating some tests that explain our intent, and will serve to validate that our solutions work. These tests specify that the title and the description, when combined, cannot be exactly 15 characters. The title cannot be longer than the short description, and the title must not contain the letters at
in order. These are a couple scenarios that will be evaluated at the same time.
The Blog
object is a normal object, but it implements the IValidatableObject
interface which will allow us to evaluate the entire object. This interface forces us to implement the Validate
method. The validation context allows us to look at the properties by casting the object instance to the type for the validator.
The mappings that we have set up have no knowledge of the previously enforced validation rules, but will still be enforced of their own right, once wired into the Blog
context. These are a standard that we will see often, when dealing with validation.
As we proceed, we will observe that the more complex the business rules get, the more we will have to tend toward using this solution. However,, there are several options that will allow us to build reusable validation.
When dealing with the shared validation logic across many related types, you can put the validation interface implementation on the base class. The validation context allows us to access the values by using the key value pairs by the property name. The preferred method is to deal with the object directly so as to avoid magic strings. The only issue is, if we want to define a generic and non-type specific validation, then we would have to use the key value pairs.