In this recipe, we will be implementing a specification pattern on top of Entity Framework and Repository Pattern to leverage maximum reuse without surfacing queryable objects to the consuming developers.
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 and updating data.
Open the Improving Complex Query Testing solution in the included source code examples.
Carry out the following steps in order to accomplish this recipe.
using System; using System.Collections.Generic; using System.Linq; using BusinessLogic; using DataAccess.Queries; using Microsoft.VisualStudio.TestTools.UnitTesting; using Rhino.Mocks; using DataAccess; using BusinessLogic.QueryObjects; using Test.Properties; namespace Test { [TestClass] public class QueryTests { [TestMethod] public void ShouldFilterTestData() { //Arrange IQueryable<Blog> items = new List<Blog> { new Blog() { Creationdate = DateTime.Now, ShortDescription = "Test", Title = "Test" }, new Blog() { Creationdate = DateTime.Now, ShortDescription = "not this one", Title = "Blog" }, new Blog() { Creationdate = DateTime.Now, ShortDescription = "not this", Title = "TeBlog" }, new Blog() { Creationdate = DateTime.Now, ShortDescription = "not this one", Title = "TestBlog" } }.AsQueryable(); var context = MockRepository .GenerateStrictMock<IDbContext>(); context.Expect(x => x.AsQueryable<Blog>()) .Return(items.AsQueryable()); var repository = new BlogRepository(context); //Act var spec = new TitleNameQuery("Test"); var returnedValues = repository.Find(spec); //Assert Assert.AreEqual(1, returnedValues.Count()); } [TestMethod] public void ShouldConnectToTheDatabase() { var repository = new BlogRepository (new BlogContext(Settings.Default.BlogConnection)); var results = repository.Find(new TitleNameQuery("Test")); } } }
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 { 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; } } }
Mapping
folder to the DataAccess
project and then 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 for Blogs
with the following code:using System; using System.Data.Entity; using System.Linq; using BusinessLogic; using DataAccess.Mappings; using DataAccess.QueryObjects; using BusinessLogic.QueryObjects; namespace DataAccess { public class BlogContext : DbContext, IDbContext { public BlogContext(string connectionString) : base(connectionString) { } protected override void OnModelCreating (DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new BlogMapping()); base.OnModelCreating(modelBuilder); } public void Refresh() { this.ChangeTracker.Entries().ToList( .ForEach(x=>x.Reload()); } public void Commit() { this.SaveChanges(); } public IQueryable<T> AsQueryable<T>() where T : class { return this.Set<T>(); } } }
BusinessLogic
project named QueryObject
with the following code:using System; using System.Collections.Generic; using System.Linq; using BusinessLogic.QueryObjects; namespace DataAccess.QueryObjects { 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 } }
DataAccess
project named QuerObjectOfT
:using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Collections.ObjectModel; using System.Reflection; using System.Linq.Expressions; namespace DataAccess.QueryObjects { 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 } }
DataAccess
project named QueryObjectBase
with the following code:using System; using System.Collections.Generic; using System.Linq; using System.Text; using BusinessLogic.QueryObjects; namespace DataAccess.QueryObjects { public abstract class QueryObjectBase<T> : IQueryObject<T> { 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() { try { return this.ContextQuery(Context); } catch (Exception) { throw; //just here to catch while debugging } } #region IQueryObject<T> Members public virtual IEnumerable<T> Execute(IDbContext context) { Context = context; CheckContextAndQuery(); var query = this.ExtendQuery(); return query.ToList(); } #endregion } }
BusinessLogic
project named IQueryObject
with the following code:using System; using System.Collections.Generic; using System.Linq; using System.Text; using BusinessLogic.QueryObjects; namespace DataAccess.QueryObjects { public interface IQueryObject { int Execute(IDbContext context); } public interface IQueryObject<out T> { IEnumerable<T> Execute(IDbContext context); } }
IRepository
interface in the BusinessLogic
project with the following code:using System.Collections.Generic; using DataAccess.QueryObjects; namespace BusinessLogic { public interface IRepository { IEnumerable<T> Find<T>(IQueryObject<T> spec) where T : class; } }
BlogRepository
class in Data Access with the following code:using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using BusinessLogic; using DataAccess.QueryObjects; using BusinessLogic.QueryObjects; namespace DataAccess { public class BlogRepository : IRepository, IDisposable { private readonly IDbContext _context; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) if (_context != null) _context.Dispose(); } ~BlogRepository() { Dispose(false); } public BlogRepository(IDbContext context) { _context = context; } public IEnumerable<T> Find<T>(IQueryObject<T> spec) where T : class { return spec.Execute(_context); } } }
Queries
folder to the DataAccess
project and a new C# class named BlogQueries
with the following code:using System.Linq; using DataAccess.QueryObjects; using BusinessLogic; namespace DataAccess.Queries { public class TitleNameQuery : QueryObject<Blog> { public TitleNameQuery(string title) { ContextQuery = (c) => c.AsQueryable<Blog>() .Where(x => x.Title == title); } } }
As always, we will start with a test that defines our scope. We want to be able to take a specification and limit a set with it. This will allow us to know when we have achieved the goal. We will then set up the blog object, the mapping, and the context.
Once those are in place, we will move to adding our abstract specification. This will serve as the base for all specifications we will use when moving forward. We will also add some extension methods and some generic specifications which will help us leverage the maximum amount of reuse from our efforts.
We will then need to modify the repository interface to accept in a specification of a certain type and return an enumerable collection of that type. These modifications will allow us to use the same repository interface no matter what we are querying against.
We will then need to modify our BlogRepository
implementation to execute the specification chain. This could be as simple as our example of one specification, or many times it can be more complex. The beauty in a generic and simple implementation is its power and scalability.
Once this is accomplished, we then add our query library and will give prebuilt specifications back to anyone who needs to use it. We can then run our tests.
There is one very important pattern that we should now discuss.
Specification pattern is a pattern used which the developer outlines a business rule that is combinable with other rules. This allows for a highly customizable system of business rules that are at the same time incredibly testable. This fits perfectly for data access because there are certain reusable joins and filters that can be added to, combined, and used with almost infinite combinations.