In this recipe, we are going to specify simple validation rules that can be applied to a property value to enforce business rules.
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 Single 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 ShouldErrorOnTitleToLong() { //Arrange var init = new Initializer(); var context = new BlogContext(Settings.Default.BlogConnection); init.InitializeDatabase(context); StringBuilder builder = new StringBuilder(); for (int i = 0; i < 20; i++) { builder.Append("This is going to be repeated"); } var blog = new Blog() { Creationdate = DateTime.Now, ShortDescription = "Test", Title = builder.ToString() }; //Act context.Set<Blog>().Add(blog); context.SaveChanges(); //Assert Assert.Fail("Didn't Error"); } [TestMethod] [ExpectedException(typeof(DbEntityValidationException))] public void ShouldErrorOnDescriptionRequired() { //Arrange var init = new Initializer(); var context = new BlogContext(Settings.Default.BlogConnection); init.InitializeDatabase(context); StringBuilder builder = new StringBuilder(); var blog = new Blog() { Creationdate = DateTime.Now, ShortDescription = null, Title = "Test" }; //Act context.Set<Blog>().Add(blog); context.SaveChanges(); //Assert Assert.Fail("Didn't Error"); } [TestMethod] [ExpectedException(typeof(DbEntityValidationException))] public void ShouldErrorOnDateOutsideAcceptableRange() { //Arrange var init = new Initializer(); var context = new BlogContext(Settings.Default.BlogConnection); init.InitializeDatabase(context); StringBuilder builder = new StringBuilder(); var blog = new Blog() { Creationdate = new DateTime(1890,1,1), ShortDescription = "Test", Title = "Test" }; //Act context.Set<Blog>().Add(blog); context.SaveChanges(); //Assert Assert.Fail("Didn't Error"); } [TestMethod] [ExpectedException(typeof(DbEntityValidationException))] public void ShouldErrorOnRatingOutOfRange() { var init = new Initializer(); var context = new BlogContext(Settings.Default.BlogConnection); init.InitializeDatabase(context); var blog = new Blog() { Creationdate = DateTime.Now, ShortDescription = "Test", Title = "Test", Rating = 6.0 }; //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.ComponentModel.DataAnnotations; 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; } [RegularExpression(pattern: DateBetween1900And2100Pattern,ErrorMessage = "Date is not valid between 1900 and 2100")] public DateTime Creationdate { get; set; } [Required] public string ShortDescription { get; set; } [Required] [StringLength(120,ErrorMessage = "Title is To Long")] public string Title { get; set; } [Range(0.0,5.0, ErrorMessage = "Invalid Range, must be between 0 and 5")] public double Rating { get; set; } } }
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 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, as always, with a few tests that specify our intent, and validate that the work we will do meets the requirements of the customer. We have specified that the creation date must be between January 1st, 1900 and December 31st, 2099. The title must be less than 120 characters, the short description must not be null, and rating must be between 0
and 5
.
We move to initializing our database with a piece of test data just to set up the structure, and give a not null
result set for any get
statement. This will give us a sample set of data.
The next piece is the magic. The Blog
object looks similar to the objects that we have used throughout the book, but it varies in one important way. We have added attributes that specify the restrictions on our object. These range from simple required attributes to more advanced regular expressions. These attributes are specified directly on the object, so that they can be used to send this data to a database across a service connection, or into a user interface. These validation restrictions are not specific to the Entity Framework, but the Entity Framework does leverage them in a fashion that allows us to keep the database clean.
The database mappings that we notice are contradictory to the specified restrictions. In this case, it operates both sets. The object in its current state must be able to pass both the sets of validation. Structure-based and content-based validation, both, have a place in our code base and exist together, not in a vacuum.
Simple properties can hide very complex business logic that restricts what values can be in them, or that means more than they seem. If there are business rules such as this, around an object, then they should be codified in the code and enforced before an object that violates them is created.
Often, there are questions about when the configuration for structure restriction should be used, or when the attributes for content restriction should be used. The answer to this is fairly simple - when it is a database storage concern, use configuration, and when it has to deal with the values in the object without concern for how it is stored, then use data annotations. There are some content rules, such as discriminator columns, which are storage concerns, and should be specified in configuration. If it is a business rule, then most likely it will be an attribute. Think about the object; if we can say When X has a Y it means Z, then we have a good candidate for attribute enforcement.
One thing for us to be aware of is that if we put the attribute validation into the object, then every place where we use this object, it will be a subject to that validation. This can gain us some great benefits from the standpoint that other code bases, and even user interfaces will be able to handle these rules for us, but it will also mean that we do not want to put anything in an attribute validation that might be different, based on how you use the object.
This validation is not real-time when used to instantiate the objects. It must be invoked, depending on when you use it, and the framework you are using it in will determine when it is called. The entity framework invokes the validation before saving an object into the context.
Each of the validation attributes that we are using allows for the setting of a hardcoded error message, or the setting of a resource type and a resource name that it can look up from the value. This will allow us to develop centralized validation logic and off-load the message delivery to a culture-specific resource file.