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.