As I was playing around with Entity Framework code first and ASP.NET MVC, it became pretty obvious that caching of some of the EF query results would be pretty handy. For example, if you look at my post that includes a simple ASP.NET MVC blogging application, you could see that I have to keep fetching the list of categories every time I render the blog entry screen. This is because I want to keep my application stateless. This is not very efficient.
As a result, I looked around for some caching options. There is a second-level cache project that was published on Microsoft code. Here is the link to it. This is very involved and handy project, but currently does not support EF 4.1 (official release name for a number of features, including Code First). You can download release candidate here or get it from NuGet inside Visual Studio 2010.
Also, I wanted something more explicit, such as extension method to specify that I want to cache results of a specific query. Here is how I wrote a simple extension method:
public static IEnumerable<T> AsCacheable<T>(this IQueryable<T> query)
{
if (cacheProvider == null)
{
throw new InvalidOperationException("Please set cache provider (call SetCacheProvider) before using caching");
}
return cacheProvider.GetOrCreateCache<T>(query);
}
I envision my final usage of new functionality will look a following:
EFCacheExtensions.SetCacheProvider(MemoryCacheProvider.GetInstance());
using (ProductContext context = new ProductContext())
{
var query = context.Products.OrderBy(one => one.ProductNumber).
Where(one => one.IsActive).AsCacheable();
}
In the example above, I am using in-memory cache provider I wrote. This provider is using static memory variable to cache data. This is poor man’s caching solution every day. To elaborate, this approach works in any environment. If you are using think client, such as WPF or Windows forms, the cache will stay in memory as long as the application is running. This is because of the rule of garbage collector. Static variables are never garbage collected. In case of web application, such as WCF, the static variables will leave as long as application pool lives. They will be lost when application pool is recycled. Default time for app pool recycling in IIS 7 is 20 minutes. This means that your cache will not live very long. So, I am also going to implement a provider that is using AppFabric Caching (formerly known as project Velocity).
Since I would like to have an abstraction over both, so that I do not have to change the code going from Velocity to In-Memory provider, I will create an interface to deal with that.
public interface IEFCacheProvider
{
IEnumerable<T> GetOrCreateCache<T>(IQueryable<T> query);
IEnumerable<T> GetOrCreateCache<T>(IQueryable<T> query, TimeSpan cacheDuration);
bool RemoveFromCache<T>(IQueryable<T> query);
}
Now, the key code is to implement both providers. I am going to explain memory provider first. Here is the class
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Concurrent;
namespace EFCodeFirstCacheExtensions
{
public class MemoryCacheProvider : IEFCacheProvider
{
private MemoryCacheProvider() { }
public static MemoryCacheProvider GetInstance()
{
lock (locker)
{
if (dictionary == null)
{
dictionary = new ConcurrentDictionary<string, CacheItem>();
}
if (instance == null)
{
instance = new MemoryCacheProvider();
}
}
return instance;
}
private static ConcurrentDictionary<string, CacheItem> dictionary;
private static MemoryCacheProvider instance;
private static object locker = new object();
public IEnumerable<T> GetOrCreateCache<T>(IQueryable<T> query, TimeSpan cacheDuration)
{
string key = GetKey<T>(query);
CacheItem item = dictionary.GetOrAdd(
key,
(keyToFind) => { return new CacheItem()
{ Item = query.ToList(), AdditionTime = DateTime.Now }; });
if (DateTime.Now.Subtract(item.AdditionTime) > cacheDuration)
{
item = dictionary.AddOrUpdate(
key,
new CacheItem() { Item = item.Item, AdditionTime = DateTime.Now },
(keyToFind, oldItem) => { return new CacheItem()
{ Item = query.ToList(), AdditionTime = DateTime.Now }; });
}
foreach (var oneItem in ((List<T>)item.Item))
{
yield return oneItem;
}
}
public IEnumerable<T> GetOrCreateCache<T>(IQueryable<T> query)
{
string key = GetKey<T>(query);
CacheItem item = dictionary.GetOrAdd(
key,
(keyToFind) => { return new CacheItem()
{ Item = query.ToList(), AdditionTime = DateTime.Now }; });
foreach (var oneItem in ((List<T>)item.Item))
{
yield return oneItem;
}
}
public bool RemoveFromCache<T>(IQueryable<T> query)
{
string key = GetKey<T>(query);
CacheItem item = null;
return dictionary.TryRemove(key, out item);
}
private static string GetKey<T>(IQueryable<T> query)
{
string key = string.Concat(query.ToString(), "nr",
typeof(T).AssemblyQualifiedName);
return key;
}
}
}
As you can see, I am implementing the original provider interface. I am supporting self-expiring as well as non-expiring cache. I am using a neat feature of EF Code Frist. If I issue to .ToString() of IQueryable, I will get the actual T-SQL query that will be executed. I am using it as a key for caching in conjunction with class name (T is the type that is returned by they query). I have to issue ToList prior to caching because of deferred execution in Entity Framework – query is not executed on the back end until I access at least one result. So, am caching the results of the query as List<T>, but returning IEnumerable<T> to be more generic.
Next, comes AppFabric provider. The only difference is that I am using AppFabric caching features. To write and to test, I have to install AppFabric locally and add references to client DLLs to my project. Those DLLs are:
Microsoft.ApplicationServer.Caching.Client
Microsoft.ApplicationServer.Caching.Core
Once those are done, I just need to replace any chunks of code that refer to memory to refer to cache object from AppFabric. Here is the full class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EFCodeFirstCacheExtensions;
using Microsoft.ApplicationServer.Caching;
namespace AppFabricCacheProvider
{
public class AppFabricCacheProvider : IEFCacheProvider
{
private AppFabricCacheProvider() { }
private static object locker = new object();
private static AppFabricCacheProvider instance;
private static DataCache cache;
public static AppFabricCacheProvider GetInstance()
{
lock (locker)
{
if (instance == null)
{
instance = new AppFabricCacheProvider();
DataCacheFactory factory = new DataCacheFactory();
cache = factory.GetCache("Default");
}
}
return instance;
}
public IEnumerable<T> GetOrCreateCache<T>(IQueryable<T> query, TimeSpan cacheDuration)
{
string key = GetKey<T>(query);
var cacheItem = cache.Get(key);
if (cacheItem == null)
{
cache.Put(key, query.ToList(), cacheDuration);
foreach (var oneItem in query)
{
yield return oneItem;
}
}
else
{
foreach (var oneItem in ((List<T>)cacheItem))
{
yield return oneItem;
}
}
}
public IEnumerable<T> GetOrCreateCache<T>(IQueryable<T> query)
{
string key = GetKey<T>(query);
var cacheItem = cache.Get(key);
if (cacheItem == null)
{
cache.Put(key, query.ToList());
foreach (var oneItem in query)
{
yield return oneItem;
}
}
else
{
foreach (var oneItem in ((List<T>)cacheItem))
{
yield return oneItem;
}
}
}
public bool RemoveFromCache<T>(IQueryable<T> query)
{
string key = GetKey<T>(query);
CacheItem item = null;
return cache.Remove(key);
}
private static string GetKey<T>(IQueryable<T> query)
{
string key = string.Concat(query.ToString(), "nr",
typeof(T).AssemblyQualifiedName);
return key;
}
}
}
II am using built-in expiration as well instead of computing the expiration myself. I am using DataCacheFactory to create an instance of cache named Default. Here is my app.config from my unit test project that supports this code:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!–configSections must be the FIRST element –>
<configSections>
<section name="dataCacheClient"
type="Microsoft.ApplicationServer.Caching.DataCacheClientSection,
Microsoft.ApplicationServer.Caching.Core, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35"
allowLocation="true"
allowDefinition="Everywhere"/>
</configSections>
<dataCacheClient>
<hosts>
<host
name="SERGEYB-PC1"
cachePort="22233"/>
</hosts>
<localCache
isEnabled="true"
sync="TimeoutBased"
objectCount="100000"
ttlValue="300" />
</dataCacheClient>
<connectionStrings>
<add name="ProductConnection"
connectionString="Server=(local);Database=Products;Trusted_Connection=True;"
providerName="System.Data.SqlClient"/>
</connectionStrings>
</configuration>
Also, I used AppFabric console to create cache named Default.
Here is my unit test code that test usage of my new classes:
[TestMethod]
public void MemoryCacheProviderGetOrCreateCacheUsageTest()
{
EFCacheExtensions.SetCacheProvider(MemoryCacheProvider.GetInstance());
using (ProductContext context = new ProductContext())
{
var query = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive).AsCacheable();
Assert.AreEqual(2, query.Count(), "Should have 2 rows");
SQLCommandHelper.ExecuteNonQuery("Update Products Set IsActive = 0");
query = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive).AsCacheable();
Assert.AreEqual(2, query.Count(), "Should have 2 rows");
IQueryable<Product> cleanupQuery = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive);
EFCacheExtensions.RemoveFromCache<Product>(cleanupQuery);
query = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive).AsCacheable();
Assert.AreEqual(0, query.Count(), "Should have 0 rows");
EFCacheExtensions.RemoveFromCache<Product>(cleanupQuery);
}
}
This demonstrates my intended use of caching in c – simply add AsCacheable to explicitly cache results of a query. You can also supply timespan for which the cache will live.
You can download entire solution with unit tests and In-Memory and AppFabric implementations here.
Thanks.
EF 4.1 IQueryable ToString() returns the sql statement for the query. My god. It’s almost as if there were new people behind this 4.1 stuff it’s so good.
Great stuff. This caching pattern looks freakishly similar to a grid hosting project I am working on with EF4.1 integrated. I will use the extension idea you posted. I added a third option to store results in the ASP.Net Cache. It will be great when AppFabric caching implements dependencies (for now just home-growing dependency management via tags).
Am I missing something here but ToString() does not evaluate the actual sql parameters,
Now I am confused. In my unit tests it does have parameters, such as IsActive=1, etc… Could you provide mode details please?
Thanks.
Sergey.
It is a good solution, very similar to the caching facade I wrote before in one of my employers. There is one enhancement/feedback I can recommend, the caching interface will be a lot more useful if it is decoupled from IQueryable interface as parameter data for caching. That layer should be very clean and supporting basic data types (object).
The extension is where the code should have coupling with IQueryable interface. This seems to me is a better code design practice.
But that is just me. Overall, I find the article fantastic, especially it is aiding me in my research/comparison of nHibernate 3.x vs. EF 4.1.
Marco is right. When you use ToString you don’t get parameters. In test try
var isActive = true;
var query = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive == isActive).AsCacheable();
I see. Yes, this is indeed the case because the variable will not be evaluated until the query is executed. To get around this you would have to intercept the query at provider level. As a result, the solution I blogged about does have this limitation.
Thanks for pointing it out.
Hi!Congratulations for your post! It’s very useful.
I just have one question: Does it possible to include SqlDependency to invalidate the cache when one or more tables are changing?
I’m trying to find any documentation about this, but I just found some articles that not work properly.
I configured the SQL Server to enable Service Broker and I tried to use SqlCommand to subscription the Query Notification Service. The code run ok, but when the data are changing the cache isn’t destroyed.
Have you ever implement this funcionality?
I’m using EF41 and SQL2008.
Thank’s in advanced
Thanks Sergey for sharing you cache extensions for EF Code First. The extension is elegant in it’s simplicity and very ergonomic for the developer. I wanted to share my attempt at overcoming the limitation when generating cache keys. Essentially, I extended the ExpressionVisitor class and traversed the MemberExpression nodes to resolve the query parameters and include them with the generated cache key. You can find the details here: http://stackoverflow.com/questions/8275881/generating-cache-keys-from-iqueryable-for-caching-results-of-ef-code-first-queri
As I mentioned in the SO question, I haven’t gotten a chance to profile the performance/overhead of using this method to generate the cache key, but I’m cautiously optimistic 🙂 Stay tuned for performance profiling results.
Hi Sergey, I tried your version of generic repository however I encountered an error that says “{“The ObjectContext instance has been disposed and can no longer be used for operations that require a connection.”}. I got this because my POCO class has a POCO class property. Like Employee class has Address property. When I access Employee.Address.State that raised the error. To avoid that issue I just removed IDisposable and replaced it with destructor method that calls the disposed method.
@AJ
This is likely because you use lazy loading of associated properties and you access them once you already retrived the data. You should opt to explicitly load related data if you need to, possibly by using .Include.
Custom Select methods in an inherited repository class maybe something that would help you. You should try to predictidly dispose of the repository and concequently DbContext. You could do what you did and dispose it in Dispose method of a Controller (if you are using MVC) or other places in other technologies. In any case you should give some more throught to you data access strategies.
Thanks.
Using your cache might be dangerous because the entitiie cache might get detached.
@Carsten
Could you elaborate please? Cache is there for read only access to frequently used data, so I do not see any dangers in the face that entities are detached. They should be detached anyway, and, really, should be queried as NoTracking.,
Interesting read