Validating simple properties

In this recipe, we are going to specify simple validation rules that can be applied to a property value to enforce business rules.

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.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.

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 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");
    
        }
    
      }
    
    }
  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"
          });
          context.SaveChanges();
        }
      }
    }
  3. In the 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; }
      }
    }
  4. 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");
        }
    
      }
    }
  5. Modify the 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();
        }
      }
    }
  6. Run our test, and see how it works.

How it works...

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.

There's more...

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.

Deciding between configuration and attributes

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.

Sharing validation

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.

Delaying validation

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.

Displaying error messages

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.

See also

In this chapter:

  • Validating complex properties
  • Improving MVC UI with entity framework validation
..................Content has been hidden....................

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