In this recipe, we will be creating a library which ensures that the queries which will be executed in the database are limited to the data access layer. It limits the queries by creating a clear boundary beyond which IQueryable<T>
is not exposed, only allowing access to the business layer to an IEnumerable<T>
. This ensures that the business layer cannot modify the queries in such a way as to make them non-performant, while still giving them complete access to the data.
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 Performance with Code Access solution in the included source code examples.
Carry out the following steps in order to accomplish this recipe.
using System.Linq; using BusinessLogic.Queries; using DataAccess; using DataAccess.Database; using Microsoft.VisualStudio.TestTools.UnitTesting; using Test.Properties; namespace Test { [TestClass] public class QueryTests { [TestMethod] public void ShouldReturnRecordsFromTheDatabaseByName() { //Arrange var init = new Initializer(); var context = new BlogContext(Settings.Default.BlogConnection); init.InitializeDatabase(context); IBlogRepository repo = new BlogRepository(context); //Act var items = repo.GetBlogsByName("Test"); //Assert Assert.AreEqual(2, items.Count()); } } }
DataAccess project Database
folder with the following code to set up 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 = "not this one", Title = "Test Blog" }); context.Set<Blog>().Add(new Blog() { Creationdate = DateTime.Now, ShortDescription = "Testing", Title = "Test Blog 2" }); context.Set<Blog>().Add(new Blog() { Creationdate = DateTime.Now, ShortDescription = "Testing", Title = "not 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 { 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"); } } }
BusinessLogic
project named IBlogRepository
with the following code:namespace DataAccess { public interface IBlogRepository { void RollbackChanges(); void SaveChanges(); } }
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; 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(); } } }
Queries
to the BusinessLogic
project and add a new C# class to it named BlogQueries
with the following code:using System.Collections.Generic; using System.Linq; using DataAccess; namespace BusinessLogic.Queries { public static class BlogQueries { public static IEnumerable<Blog> GetBlogsByName(this IBlogRepository repo, string name) { return ((InternalBlogRepository)repo) .Set<Blog>().FilterByBlogName(name).ToArray(); } public static IQueryable<Blog> FilterByBlogName(this IQueryable<Blog> items, string name ) { return items.Where(x => x.Title.Contains(name)); } } }
Queries
folder in BusinessLogic
named InternalBlogContext
with the following code:using System.Linq; namespace BusinessLogic.Queries { public abstract class InternalBlogRepository { public abstract IQueryable<T> Set<T>() where T : class; } }
BlogRepository
class with the following code:using System; using System.Data.Entity; using System.Linq; using BusinessLogic; using BusinessLogic.Queries; namespace DataAccess { public class BlogRepository : InternalBlogRepository, IBlogRepository { private readonly IUnitOfWork _context; public BlogRepository(IUnitOfWork context) { _context = context; } public override IQueryable<T> Set<T>() { return _context.Find<T>(); } public void RollbackChanges() { _context.Refresh(); } public void SaveChanges() { try { _context.Commit(); } catch (Exception) { RollbackChanges(); throw; } } } }
We start our solution by setting up a test which allows us to communicate with a database and return objects without having the ability to query directly. This test seeds the database with an initializer and then invokes a query that returns a set of data without surfacing how it does that.
We set up our blog entity, the database schema mapping, and the blog context as a basis for the database communication. These pieces allow us to translate our language integrated queries into SQL statements.
We then create a query that accepts an IBlogRepository
and casts it to an InternalBlogRepository
to get a set of items and return it. This cast allows us to grab a set of entities but not surface that into the interface that is widely used. We also limit the return type of the query to an IEnumerable<>
instead of an IQueryable<>
, so the returned enumerable cannot be used to modify the query in the other layers. This simple convention will indicate that data access is meant to be performed in the query libraries. If we need more control and want to tie this control more closely, then we can move our InternalBlogRepository
and the queries to the data access project, and mark the InternalBlogRepository
as internal. This will ensure that it cannot be used elsewhere.
When limiting access to code and trying to force certain programming behaviors, we should be sure of some things.
As professional developers, we need to have a certain amount of discipline. There are ways around almost all code access restrictions with some being more complex than others. These pathways are there but we must avoid using them and try to adhere to the prescribed architecture. If we disagree with it, then there are ways to address that, but do not just start subverting it as that will only hurt you and the code base in the end.
Likewise, when we are serving as architects, we should always be mindful that we want our developers to "fall into the pit of success." That is to say we want the easiest way to do something to be the right way to do something. If it is not the easiest way, then we are subverting our own architecture from the outset because of how it is designed. One of the benefits brought to us by test driven development is that we always start by using the component that we will be writing, even before we write it. This keeps us focused on the consumer of our libraries. If the test is difficult to write, then the code will also be difficult.
When we start down the road of restricting the architecture, we need to be aware of the cost that it will incur. Making sure that all queries are defined in a testable and reusable fashion is a goal to strive for. However, do not think that it comes without cost. Productivity and learning curve are going to suffer slightly due to this, so be aware and do not make these choices lightly.