In this recipe, we will work through a full MVC 3 application, using patterns and the Entity Framework to give a solid basis for any enterprise application.
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.
Open the Improving MVC 3 Applications solution in the included source code examples.
ControllerTests
with the following code:using System.Collections.Generic; using System.Web.Mvc; using BusinessLogic; using BusinessLogic.Interfaces; using DataAccess.Queries; using Microsoft.VisualStudio.TestTools.UnitTesting; using Rhino.Mocks; using UI.Controllers; namespace Test { [TestClass] public class ControllerTests { [TestMethod] public void ShouldQueryRepository() { //Arrange var repo = MockRepository.GenerateStrictMock<IRepository>(); repo .Stub(x => x.Find(Arg<BlogByTitleQuery>.Is.Anything)) .Return(new Blog() { Title = "Test", ShortDescription = "This is a test description", Posts = new List<Post>() { new Post() { Title = "Test Post 1", Content = "Test Content 1" }, new Post() { Title = "Test Post 1", Content = "Test Content 1" } } }); var controller = new HomeController(repo); //Act var result = controller.Index(); //Assert Assert.IsInstanceOfType(result, typeof(ViewResult)); var viewResult = result as ViewResult; Assert.IsNotNull(viewResult.Model); Assert.IsInstanceOfType(viewResult.Model, typeof(Blog)); Assert.AreEqual("Test",((Blog)viewResult.Model).Title); } } }
RepositoryTests
with the following code, which will define our problem scope:using System.Collections.Generic; using System.Linq; using BusinessLogic; using BusinessLogic.Interfaces; using DataAccess; using DataAccess.Queries; using Microsoft.VisualStudio.TestTools.UnitTesting; using Rhino.Mocks; namespace Test { [TestClass] public class RepositoryTests { [TestMethod] public void ShouldQueryBlogs() { //Arrange var context = MockRepository.GenerateStrictMock<IDbContext>(); context .Stub(x => x.AsQueryable<Blog>()) .Return(new List<Blog> { new Blog() { Title = "Test", ShortDescription = "This is a test description", Posts = new List<Post>() { new Post() { Title = "Test Post 1", Content = "Test Content 1" }, new Post() { Title = "Test Post 1", Content = "Test Content 1" } } }, new Blog(){Title = "Not Test"} }.AsQueryable()); var repository = new BlogRepository(context); //Act var blog = repository.Find(new BlogByTitleQuery("Test")); //Assert Assert.IsNotNull(blog); Assert.IsNotNull(blog.Posts); Assert.AreEqual(2, blog.Posts.Count()); } } }
Blog
and Post
objects as new C# classes to the BusinessLogic
project, and the audit
objects that will back it with the following code:using System; using System.Collections; using System.Collections.Generic; namespace BusinessLogic { public class Blog : AuditableEntity { public Blog() { Posts = new List<Post>(); } public string ShortDescription { get; set; } public string Title { get; set; } public double Rating { get; set; } public ICollection<Post> Posts { get; set; } } public class AuditableEntity : Entity { public DateTime Created { get; set; } public string CreatedBy { get; set; } public DateTime Modified { get; set; } public string ModifiedBy { get; set; } } public class Entity { public int Id { get; set; } } public class Post : Entity { public string Title { get; set; } public string Content { get; set; } } }
Blog
and Post
to the DataAccess
project as new C# classes 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.Created).HasColumnName("CreationDate").IsRequired(); this.Property(x => x.ShortDescription).HasColumnType("Text").IsMaxLength().IsOptional().HasColumnName("Description"); this.HasMany(x => x.Posts).WithRequired(); } } public class PostMapping : EntityTypeConfiguration<Post> { public PostMapping() { this.ToTable("Posts"); this.HasKey(x => x.Id); this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity).HasColumnName("PostId"); this.Property(x => x.Title).IsRequired().HasMaxLength(250); this.Property(x => x.Content).IsRequired().HasMaxLength(5000); } } }
BusinessLogic
project named IDbContext
with the following code:using System; using System.Linq; namespace BusinessLogic.Interfaces { public interface IDbContext : IDisposable { IQueryable<T> AsQueryable<T>() where T : class; T Add<T>(T item) where T : class; T Remove<T>(T item) where T : class; T Update<T>(T item) where T : class; T Attach<T>(T item) where T : class; T Detach<T>(T item) where T : class; int SaveChanges(); } }
BlogContext
to use the mappings, and implement the IDbContext
interface with the following code:public class BlogContext : BaseDbContext, IDbContext { static BlogContext() { System.Data.Entity.Database.SetInitializer(new Initializer()); } public BlogContext(string connection) : base(connection) { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new BlogMapping()); modelBuilder.Configurations.Add(new PostMapping()); base.OnModelCreating(modelBuilder); } } public class BaseDbContext : DbContext { protected BaseDbContext(string connection) : base(connection) { } public override int SaveChanges() { var auditableCreates =this.ChangeTracker.Entries().Where(x => x.State == EntityState.Added && x.Entity is AuditableEntity); foreach (var item in auditableCreates.Select(auditableCreate => auditableCreate.Entity as AuditableEntity)) { item.Created = item.Modified = DateTime.Now; item.CreatedBy = item.ModifiedBy = "UserName"; } var auditableModifies = this.ChangeTracker.Entries().Where(x => x.State == EntityState.Modified && x.Entity is AuditableEntity); foreach (var item in auditableModifies.Select(auditableModify => auditableModify.Entity as AuditableEntity)) { item.Modified = DateTime.Now; item.ModifiedBy = "UserName"; } return base.SaveChanges(); } public IQueryable<T> AsQueryable<T>() where T : class { return this.Set<T>(); } public T Add<T>(T item) where T : class { this.Set<T>().Add(item); return item; } public T Remove<T>(T item) where T : class { this.Set<T>().Remove(item); return item; } public T Update<T>(T item) where T : class { var entry = this.Entry(item); if (entry != null) { entry.CurrentValues.SetValues(item); } else { this.Attach(item); } return item; } public T Attach<T>(T item) where T : class { this.Set<T>().Attach(item); return item; } public T Detach<T>(T item) where T : class { this.Entry(item).State = EntityState.Detached; return item; } } }
using System.Collections.Generic; namespace BusinessLogic.Interfaces { public interface IQueryObject { int Execute(IDbContext context); } public interface ICommandObject { void Execute(IDbContext context); } public interface IScalarObject<out T> { T Execute(IDbContext context); } public interface IQueryObject<out T> { IEnumerable<T> Execute(IDbContext context); } }
BusinessLogic
project with the following code:using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Linq.Expressions; using System.Reflection; using BusinessLogic.Interfaces; namespace BusinessLogic.Domain { public class QueryObject : IQueryObject { public Func<IDbContext, int> ContextQuery { get; set; } protected void CheckContextAndQuery(IDbContext context) { if (context == null) throw new ArgumentNullException("context"); if (this.ContextQuery == null) throw new InvalidOperationException("Null Query cannot be executed."); } #region IQueryObject<T> Members public virtual int Execute(IDbContext context) { CheckContextAndQuery(context); return this.ContextQuery(context); } #endregion } public abstract class QueryObjectBase<T> : IQueryObject<T> { //if this func returns IQueryable then we can add //functionaltly, such as Where, OrderBy, Take, and so on, to //the QueryOjbect and inject that into the expression before //is it is executed protected Func<IDbContext, IQueryable<T>> ContextQuery { get; set; } protected IDbContext Context { get; set; } protected void CheckContextAndQuery() { if (Context == null) throw new InvalidOperationException("Context cannot be null."); if (this.ContextQuery == null) throw new InvalidOperationException("Null Query cannot be executed."); } protected virtual IQueryable<T> ExtendQuery() { return this.ContextQuery(Context); } #region IQueryObject<T> Members public virtual IEnumerable<T> Execute(IDbContext context) { Context = context; CheckContextAndQuery(); var query = this.ExtendQuery(); return query.AsEnumerable() ?? Enumerable.Empty<T>(); } #endregion } public class QueryObject<T> : QueryObjectBase<T> { protected override IQueryable<T> ExtendQuery() { var source = base.ExtendQuery(); source = this.AppendExpressions(source); return source; } public IQueryObject<T> Take(int count) { var generics = new Type[] { typeof(T) }; var parameters = new Expression[] { Expression.Constant(count) }; this.AddMethodExpression("Take", generics, parameters); return this; } public IQueryObject<T> Skip(int count) { var generics = new Type[] { typeof(T) }; var parameters = new Expression[] { Expression.Constant(count) }; this.AddMethodExpression("Skip", generics, parameters); return this; } #region Helper methods static ReadOnlyCollection<MethodInfo> QueryableMethods; static QueryObject() { QueryableMethods = new ReadOnlyCollection<MethodInfo>(typeof(System.Linq.Queryable) .GetMethods(BindingFlags.Public | BindingFlags.Static).ToList()); } List<Tuple<MethodInfo, Expression[]>> _expressionList = new List<Tuple<MethodInfo, Expression[]>>(); private void AddMethodExpression(string methodName, Type[] generics, Expression[] parameters) { MethodInfo orderMethodInfo = QueryableMethods.Where(m => m.Name == methodName && m.GetParameters().Length == parameters.Length + 1).First(); orderMethodInfo = orderMethodInfo.MakeGenericMethod(generics); _expressionList.Add(new Tuple<MethodInfo, Expression[]>(orderMethodInfo, parameters)); } private IQueryable<T> AppendExpressions(IQueryable<T> query) { var source = query; foreach (var exp in _expressionList) { var newParams = exp.Item2.ToList(); newParams.Insert(0, source.Expression); source = source.Provider.CreateQuery<T>(Expression.Call(null, exp.Item1, newParams)); } return source; } #endregion } public class ScalarObject<T> : IScalarObject<T> { public Func<IDbContext, T> ContextQuery { get; set; } protected void CheckContextAndQuery(IDbContext context) { if (context == null) throw new ArgumentNullException("context"); if (this.ContextQuery == null) throw new InvalidOperationException("Null Query cannot be executed."); } #region IQueryObject<T> Members public virtual T Execute(IDbContext context) { CheckContextAndQuery(context); return this.ContextQuery(context); } #endregion } }
IRepository
interface to accept the query objects on Find
with the following code:using System.Collections.Generic; namespace BusinessLogic.Interfaces { public interface IRepository { T Find<T>(IScalarObject<T> query); IEnumerable<T> Find<T>(IQueryObject<T> query); T Add<T>(T item) where T: class; T Remove<T>(T item) where T : class; T Update<T>(T item) where T : class; T Attach<T>(T item) where T : class; T Detach<T>(T item) where T : class; void SaveChanges(); } }
BlogRepository
to implement this interface with the following code:using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using BusinessLogic; using BusinessLogic.Interfaces; namespace DataAccess { public class BlogRepository : IRepository, IDisposable { private readonly IDbContext _context; public BlogRepository(IDbContext context) { _context = context; } public T Detach<T>(T item) where T : class { return _context.Detach(item); } public void SaveChanges() { _context.SaveChanges(); } public void Dispose() { } public T Find<T>(IScalarObject<T> query) { return query.Execute(_context); } public IEnumerable<T> Find<T>(IQueryObject<T> query) { return query.Execute(_context); } public T Add<T>(T entity) where T : class { return _context.Add(entity); } public T Remove<T>(T entity) where T : class { return _context.Remove(entity); } public T Update<T>(T item) where T : class { throw new NotImplementedException(); } public T Attach<T>(T item) where T : class { throw new NotImplementedException(); } } }
HomeController
to supply the correct data to the view with the following code:using System.Web.Mvc; using BusinessLogic.Interfaces; using DataAccess.Queries; namespace UI.Controllers { public class HomeController : Controller { private readonly IRepository _repository; public HomeController(IRepository repo) { _repository = repo; } public ActionResult Index() { var blog = _repository.Find(new BlogByTitleQuery("Test")); return View(blog); } public ActionResult About() { return View(); } } }
We start with a set of tests, one to define the behavior we want from our controller, and the other to define the behavior we want from the repository. The controller should send a query to the repository but have no control over how the predefined query is executed, and the repository should execute the query with no knowledge of what is in it. This gives us clear lines of separation, and allows us to plug in new queries at will.
We then need to define our object graph, in this case, a Blog
with many Post
objects. These are mapped conventionally to focus our attention on the problem domain.
We then add our interface to the DbContext
that will allow interaction with the database in an abstract fashion. We implement this interface on the DbContext
, and abstract the re-usable chunks into a base repository that will allow us to reuse this behavior at will.
We then define the IQueryObject
, ICommandObject
, IScalarObject
, IQueryObject<out T>
interfaces to our Query
objects. This will give us a contract definition that can be implemented for new queries. The objects that implement this will store the query and a function on IDbContext
to give us a deferred execution and compose-ability. These interfaces will also give some extension points, through the Extend
query, which will allow us to implement behaviors such as paging or ordering.
We define the repository interface to accept the query objects as parameters for the Find
methods. The implementation of these Find
methods invoke the execute
method of the query, and pass in the DbContext
. This gives the repository a layer of separation from the implementation of queries, and keeps the repository interface simple and incredibly powerful.
This leverages the specification pattern, expression trees, and strategy pattern that are well worth understanding and we are using them extensively.
The specification pattern is a pattern that allows the business rules, or, in our case, queries to be chained together and reused by simply adhering to a standard contract. We specified that all Query
objects had to have an execute
method that is overrideable and is a ContextQuery
. This allows us to chain them together and define ever more complex query paths without sacrificing the elegance and simplicity of the solution.
Expression trees are a data representation of code, and are traversable as such. They allow us to build large and complex code chunks, store them until needed, and supply the parameters required for them to be compiled and executed. This gives us a fairly flexible framework for defining queries and database interactions without needing to focus on when the call will be made.
The Strategy pattern simply allows the functionality being executed to vary without the code exercising it being changed. This is to say that we pass in new derived types with new ContextQueries
, without the code that uses it ever having to know that is is dealing with a different object. No if
statements, no switch
statements, just exercising the behavior we have pre-loaded.
Chapter 5, Improving Entity Framework with Query Libraries: