In this recipe, we will decouple the query and entity definitions from the database communication to illustrate how they can be leveraged separately.
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 Entity and Library Reuse solution in the included source code examples.
Carry out the following steps in order to accomplish this recipe.
QueryTests
to the test project. We will make a test that connects to the database and retrieves a couple of records with a reusable query using the following code:using System; using System.Linq; using BusinessLogic; 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 ShouldReturnRecordsFromTheDatabase() { //Arrange var init = new Initializer(); var context = new BlogContext(Settings.Default.BlogConnection); init.InitializeDatabase(context); IBlogRepository repo = new BlogRepository(context); //Act var items = repo.Set<Blog>().FilterByBlogName("Test"); //Assert Assert.AreEqual(2, items.Count()); } [TestMethod] public void ShouldReturnRecordsFromAnotherSourceWithTheSameQuery() { //Arrange var anotherDatasource = new SomeOtherDataSource(); //Act var items = anotherDatasource.Blogs.FilterByBlogName("Test"); //Assert Assert.AreEqual(1,items.Count()); } } }
SomeOtherDataSource
to the test project with the following code:using System; using System.Collections.Generic; using System.Linq; using BusinessLogic; namespace Test { public class SomeOtherDataSource { public IQueryable<Blog> Blogs { get { return new List<Blog>() { new Blog() { Creationdate = DateTime.Now, Rating = 1, Id = 1, ShortDescription = "Test", Title = "Not This one" }, new Blog() { Creationdate = DateTime.Now, Rating = 1, Id = 1, ShortDescription = "Test", Title = "Test" } }.AsQueryable(); } } } }
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 = "Testing", 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"); } } }
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.Linq; namespace BusinessLogic.Queries { public static class BlogQueries { public static IQueryable<Blog> FilterByBlogName (this IQueryable<Blog> items, string name) { return items.Where(x => x.Title.Contains(name)); } } }
Program.cs
of the project ConsoleApp
with the following code:using System; using BusinessLogic.Queries; namespace ConsoleApp { class Program { static void Main(string[] args) { var repo = new InMemoryRepository(); var items = repo.Blogs.FilterByBlogName("Test"); foreach (var blog in items) { Console.WriteLine(blog.Title); } Console.ReadLine(); } } }
ConsoleApp
project named InMemoryRepository
with the following code:using System; using System.Collections.Generic; using System.Linq; using BusinessLogic; namespace ConsoleApp { internal class InMemoryRepository { internal IQueryable<Blog> Blogs { get { return new List<Blog>() { new Blog() { Creationdate = DateTime.Now, Rating = 1, Id = 1, ShortDescription = "Test", Title = "Not This one" }, new Blog() { Creationdate = DateTime.Now, Rating = 1, Id = 1, ShortDescription = "Test", Title = "Test" } }.AsQueryable(); } } } }
We will start our solution by defining a test which encapsulates the functionality we are trying to complete. This will serve as our marker that we have accomplished the goal. The test, in this case, is that we can not only execute the defined queries on the database, but also use them against other collections of the proper type.
We initialized the database to seed records to filter, and we set up another data source that provides in memory lists of data to filter. We defined the blog entity, the mapping, and the DbContext
so our test can function against the database. This will ensure that we have not broken the database communication by adding this level of reuse to the application.
The next piece that we move to is a console application that references the BusinessLogic
project without referencing the DataAccess
. This demonstrates that we have separated our functions correctly and that we can leverage it from multiple applications without any problems.
This level of reuse should extend to applications that hit the same database but with different delivery mechanisms. There are several things to keep in mind here.
This solution should only be used when we cannot define a service or we have tight control over the usage in both applications. The preferred way to share a database is to define a service that does the data access. We would then be sharing schema and contract details instead of sharing types and assemblies.
If we need to share the assemblies between solutions directly, then there are some tools which will make this easier. One of those options is to define a NuGet package and host it internally in your company on a private NuGet repository. This will ensure updates are distributed and that everyone goes to the same place to get the reference instead of pulling and compiling their own updates, or even worse, changing and compiling them.