Click here to Skip to main content
Click here to Skip to main content

Entity Framework - Second Level Caching with DbContext

, 6 Aug 2012
Rate this:
Please Sign up or sign in to vote.
How to enable 2nd level caching in the Entity Framework when using DbContext.

Introduction  

The Entity Framework doesn't support 2nd level caching straight out of the box. The EFCachingProvider project by Jarek Kowalski provides a means to create a wrapper around the provider to support caching, but most of the examples available are using ObjectContext rather than DbContext to create the wrapper. This expects you to have an edmx file for your model

The purpose of this article is to show how you can create a wrapper around DbContext with EntityConfiguration mappings that will support 2nd level caching in your EF applications. 

Background  

At it's most basic, 2nd level caching is a query cache. The results of SQL commands are stored in the cache, so that the same SQL commands retrieve their data from the Cache rather than executing the query again against the underlying provider. This can have a performance boost for your application and results in less activity against your database, at the cost of increased memory.

There are some arguments around where caching should occurr when using an ORM. Is it the job of your ORM to cache data, or should that only concern itself with data tasks and caching should be entirely your responsibility within your application domain? Personally, I like to be given the choice - something NHibernate and other ORM's allow you to do by specificying your own cache implementation.

Another problem with Entity Framework caching is that you cannot cache the results of your queries directly since they will be associated with a Context and objects cannot be associated with more than one Context at a time, you will see exceptions such as "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key" 

This normally results in the developer creating 'cachable DTOs' and then translating entity POCO objects into these so they can cache the data. (Further information on this is available here). You then have to implement cache invalidation to make sure your cached objects are kept in sync with any data updates. This certainly adds a layer of complexity within an application that could be taken care of within the ORM. 

For more information on second level caching, a very good article by Ayende Rahien can be found here

Prerequisites 

Binaries

To keep the download size small, I've only included the very minimum requirements to get the project running. I suggest you use NuGet to refresh the references yourself.

  • I have included slightly modified binaries of the EFCachingProvider project, but you can download these yourself. I will describe the changes I have made further in the article.
  • The demo application is using NuGet Package EntityFramework.5.0.0-rc 
  • The application also uses Castle Windsor for some basic dependency injection 

Database 

The demo application is using SQL Server and everyones favourite database Northwind Smile | <img src= " /> If you don't have a copy of Northwind, you can download the SQL to create the database from Codeplex

Northwind Database Download

Using the code     

First things first, amend the Web.Config file and change the connection string to a valid Northwind connection for your environment.

When you are in the config file, notice that we need to register our custom data providers in the config, 

  <system.data>
    <DbProviderFactories>
      <add name="EF Caching Data Provider" invariant="EFCachingProvider" description="Caching Provider Wrapper" type="EFCachingProvider.EFCachingProviderFactory, EFCachingProvider" />
      <add name="EF Tracing Data Provider" invariant="EFTracingProvider" description="Tracing Provider Wrapper" type="EFTracingProvider.EFTracingProviderFactory, EFTracingProvider" />
      <add name="EF Generic Provider Wrapper" invariant="EFProviderWrapper" description="Generic Provider Wrapper" type="EFProviderWrapperToolkit.EFProviderWrapperFactory, EFProviderWrapperToolkit" />
    </DbProviderFactories>
  </system.data>  

When the Entity Framework attempts to create a DbContext, it uses a factory pattern to generate a DbConnection object. The factory can be set programmatically by using Database.DefaultConnectionFactory or you can specify in web.config. We just need to tell the framework to use our custom factory when creating instances of our context.  

  <entityFramework>
    <defaultConnectionFactory type="SecondLevelCaching.Data.CachedContextConnectionFactory, SecondLevelCaching.Data">
    </defaultConnectionFactory>
    <contexts>
      <context type="SecondLevelCaching.Data.Models.NorthwindContext, SecondLevelCaching.Data">
      </context>
    </contexts>
  </entityFramework>

Connection factories must implement System.Data.Entity.Infrastructure.IDbConnectionFactory. Our custom implementation is simple for this example, we simply want to create and return a DbConnection using the EFCachingConnection object.

public class CachedContextConnectionFactory : System.Data.Entity.Infrastructure.IDbConnectionFactory
{
    public DbConnection CreateConnection(string nameOrConnectionString)
    {
        var providerInvariantName = "System.Data.SqlClient";
 
        var wrappedConnectionString = "wrappedProvider=" +
            providerInvariantName + ";" +
            nameOrConnectionString;
 
        return new EFCachingConnection
        {
            ConnectionString = wrappedConnectionString,
            CachingPolicy = CachingPolicy.CacheAll,
            Cache = EntityCache.Instance
        };
    }
}
 

The Cache Object

Data contexts are short lived, within web applications they will be created for the lifetime of each HttpRequest. However, we need our caching mechanism to live beyond that so that subsequent HTTP requests can access previously cached data. I'm using a simple Singleton object that is implemented in the EntityCache class. 

NB: The EFCachingConnection requires you set an object that implements ICache, in a real application you would certainly use ICache rather than the concrete implementation here. I am only using InMemoryCache for demonstration purposes to access Cache statistics within the UI. 

public class EntityCache
{
    private static InMemoryCache cacheInstance;
    private static object lockObject = new object();
 
    public static InMemoryCache Instance
    {
        get
        {
            if (cacheInstance == null)
            {
                lock (lockObject)
                {
                    if (cacheInstance == null)
                        cacheInstance = new InMemoryCache();
                }
            }
 
            return cacheInstance;
        }
    }
}
 
	   

Simplify the DbContext

It's always a good idea to create an interface for our DbContext, which we can then use for dependency injection. The interface and implementation look like this. 

    public interface IDbContext
    {
        IQueryable<T> Table<T>() where T : class;
 
        int SaveChanges();
    }
public class NorthwindContext : DbContext, IDbContext
{
    static NorthwindContext()
    {
        Database.SetInitializer<NorthwindContext>(null);
    }
 
	public NorthwindContext(string nameOrConnectionString)
        : base(nameOrConnectionString)
	{
	}
 
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        System.Type configType = typeof(CategoryMap);   
        var typesToRegister = Assembly.GetAssembly(configType).GetTypes()
        .Where(type => !String.IsNullOrEmpty(type.Namespace))
        .Where(type => type.BaseType != null && type.BaseType.IsGenericType && type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>));
        foreach (var type in typesToRegister)
        {
            dynamic configurationInstance = Activator.CreateInstance(type);
            modelBuilder.Configurations.Add(configurationInstance);
        }
    }
 
    public IQueryable<T> Table<T>() where T : class
    {
        return this.Set<T>();
    }
}

The above simply adds all of the Mapping classes in the OnModelCreating event and provides access to a Queryable source via the Table method.

Wiring it all together

Now we're ready to wire up our dependencies and see it in action. This example is using Windsor Castle but it will work with whatever your DI library of choice is. We only have one interface to tell the container about - IDbContext

var connectionString = System.Configuration.ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
if (string.IsNullOrEmpty(connectionString))
    throw new Exception("The connection string for Northwind could not be found in the configuration, please make sure you have set this");
 
container.Register(
    Component.For<SecondLevelCaching.Data.IDbContext>()
        .ImplementedBy<SecondLevelCaching.Data.Models.NorthwindContext>()
        .LifeStyle.PerWebRequest
        .Named("NorthwindCachedContext")
        .DependsOn(
            Parameter.ForKey("nameOrConnectionString").Eq(connectionString))
    ); 

Our HomeController can now resolve this dependency and use it to query the database.

public class HomeController : Controller
{
    private readonly IDbContext dataContext;
 
    public HomeController(IDbContext context)
    {
        this.dataContext = context;
    }   

The Home View

There's only a single view in this demo, which is a list of customer data from Northwind. Cache statistics are also displayed on the view, will will show you whenever an item has been retrieved from the cache (CacheHit) or retrieved from the database (CacheMiss, CacheAdd). A search box allows you to filter the data by customer name.

Try entering some different search terms. You will notice that you cause the CacheMiss and CacheAdd numbers to increase. Now try entering a search term you have previously used, you should see the CacheHit number increase.  

When you see this, you have retrieved your query data from EFCachingProvider and no SQL statement has been run against your database. That's the 2nd level cache in action!

Points of Interest   

Some amendments were required to EFCachingProvider to work with Code First. In this project, I used the 'Reverse Engineer Code First' power tools to create my model from the existing database, but if you wanted to use this in an application where the domain Model exists first, then you would receive an exception when the provider tries to create a Command object. This is because in class DbConnectionWrapper, the method CreateDbCommand throws a 'Not Supported' exception. A simple fix to this is to implement the method... 

protected override DbCommand CreateDbCommand()
{
    return wrappedConnection.CreateCommand();
}
 
 

Summary

This demo project shows how you can use the Database Connection Factory to provide a custom factory that injects a caching connection into the DbContext pipeline.

For more information on the Caching solution, visit the Codeplex pages linked in the opening paragraph.

History    

06/08/2012: Initial version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Dylan Morley
Technical Lead
United Kingdom United Kingdom
No Biography provided

Comments and Discussions

 
QuestionAsNoTracking() PinmemberDejan St. Blue14-Jun-13 1:40 
QuestionSqlQuery ? Pinmemberakron_norka13-Mar-13 6:25 
QuestionSql View PinmemberRB@Emphasys15-Jan-13 7:56 
QuestionProblems with EF 4.1 Pinmemberseccomiro7-Aug-12 10:31 
AnswerRe: Problems with EF 4.1 PinmemberDylan Morley7-Aug-12 20:54 
GeneralMy vote of 5 PinmemberPankaj Nikam7-Aug-12 3:55 
GeneralMy vote of 5 PinmemberChristian Amado6-Aug-12 8:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 6 Aug 2012
Article Copyright 2012 by Dylan Morley
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid