In this recipe, we will leverage the data annotations to provide real-time feedback to the user, while those same annotations will validate objects before allowing them to be saved to the database.
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:
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 contain the new mappings, and a DbSet
property for Blog
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(); } } }
BlogController
with the following code:using System; using System.Data.Entity.Validation; using System.Linq; using System.Web.Mvc; using BusinessLogic; using DataAccess; using UI.Properties; namespace UI.Controllers { public class BlogController : Controller { private IBlogRepository _blogRepository; public BlogController() : this(new BlogRepository(new BlogContext(Settings.Default.BlogConnection))) { } public BlogController(IBlogRepository blogRepository) { _blogRepository = blogRepository; } // GET: /Blog/ public ActionResult Display() { Blog blog = _blogRepository.Set<Blog>().First(); return View(blog); } public ActionResult Create() { return View(new Blog()); } [AcceptVerbs(HttpVerbs.Post)] public ActionResult Create([Bind(Exclude = "Id")]Blog blog) { if (!ModelState.IsValid) return View(); return RedirectToAction("Display"); } } }
We start off by specifying a test that will validate the data annotations on the database side. In this example, we will also have to validate the client-side feedback manually to ensure that we have met our intent.
The Blog
object is restricted with several simple validations that will give some error messages to the MVC View when the user changes inputs and the validation runs. The details of these restrictions, for our example, are not drastically important, but they give us a framework to test against.
We have added a couple Create
methods to the BlogController
, so we can add a Create view
method that is strongly tied to Blog
, and use the Create template
method. This will present the UI, and set up the validation message handlers.
The MVC framework will use the validation
attributes to put messages on the screen as an instant feedback to the user.
Validations like these are generated amazingly simply in an MVC user experience, but we need to understand how that is accomplished.
The Html helper that enables our validation message to be displayed for a property, must be used to display the message from our attribute. The editor for the helper will not display on its own. This also requires sending the business objects to the UI in strongly-typed views. One of the key features of Code First is that we can use the objects throughout our code base, because they are not tied to our database structure.