ASP.NET MVC Output Caching with Windows AppFabric Cache
Enterprise level web applications are typically hosted in web farms where in-process caching is less useful than a distributed caching approach such as Windows Server AppFabric or Memcached. Thanks to the extensibility features introduced with ASP.NET 4.0, modifying an application to use a distributed cache is a very simple process. This post explains how to create an AppFabric OutputCacheProvider and how to configure ASP.NET to use the new provider. We will also discuss a big limitation with the MVC3 OutputCacheAttribute and explain how this is addressed by the MvcDonutCaching NuGet package.
Why use Windows Server AppFabric Caching?
Instead of caching items in memory on the web server, AppFabric allows you to distribute your items across multiple cache hosts. This allows all web servers in your web farm to share a single (distributed) cache. A few advantages are:
Persistency
Unlike in-process caching, AppFabric caches are not lost when your IIS app pool recycles.
Scalability
You are not restricted to the memory on each web server. You cache cluster can grow and grow.
Accessibility
When an item can be removed from the cache, it is (effectively) removed for all web servers.
Coherence
No stale data. All web servers use the same cache data so all clients see the same data too.
Installing Windows Server AppFabric
I am not going to cover the installation of Windows Server AppFabric because 1) it is outside the scope of this article, 2) many people have already written about it and 3) somebody from your infrastructure team has probably already installed a dev instance for you and given you the details :-). Scott Hanselman has a very good article covering the installation process should you need it.
Having said that, in order to implement the provider we do need to reference some AppFabric DLLs.
- Microsoft.ApplicationServer.Caching.Core.dll
- Microsoft.ApplicationServer.Caching.Client.dll
I have included these in the code download later in the article but to get them yourself, download the AppFabric 1.0 installer and just install the "Cache Client". After installation, the DLLs can be found in C:\Windows\System32\AppFabric.
We will discuss AppFabric 1.1 CTP later in the article but should you choose to download this preview version, you will find the DLLs in C:\Program Files\Windows Server AppFabric - 1.1 CTP.
Writing our initial AppFabricOutputCacheProvider
Replacing the built-in in-process output cache mechanism in ASP.NET is as simple as subclassing the abstract OutputCacheProvider class found in System.Web.Caching. While AppFabric 1.0 comes with a DataCacheSessionStoreProvider for moving session state to AppFabric, an OutputCacheProvider implementation is not included so we will need to write our own. There are four abstract methods that we need to implement: Get, Add, Set and Remove.
In order for our provider to read and write to a Windows AppFabric Cache, we need to use the Microsoft.ApplicationServer.Caching.DataCache class which exposes all the methods we require. This class does not have a public constructor. Instead, we use the GetCache method of the Microsoft.ApplicationServer.Caching.DataCacheFactory class. The GetCache method requires a cache name, so we need a mechanism to retreive this value from config.
In a typical domain class, I would look to inject this configuration via the constructor, but because we are using the ASP.NET provider model, the constructor must be empty. Typically, providers read configuration straight from the web.config by overriding the Initialize method of ProviderBase. MSDN has some useful information on writing your own providers. The full Initialize method is shown below:
public override void Initialize(string name, NameValueCollection config)
{
if (config == null)
{
throw new ArgumentNullException("config");
}
var cacheName = config["cacheName"];
if (cacheName == null)
{
throw new ProviderException("Empty or missing cacheName");
}
config.Remove("cacheName");
if (config.Count > 0)
{
throw new ProviderException("Unrecognized attribute: " + config.GetKey(0));
}
base.Initialize(name, config);
_cache = new DataCacheFactory().GetCache(cacheName);
}
Once we have access to an instantiated DataCache, implementing the Get, Add, Set and Remove methods is trivial:
public override object Add(string key, object entry, DateTime utcExpiry)
{
var existingEntry = Get(key);
if (existingEntry != null)
{
return existingEntry;
}
Set(key, entry, utcExpiry);
return entry;
}
public override object Get(string key)
{
return _cache.Get(key);
}
public override void Remove(string key)
{
_cache.Remove(key);
}
public override void Set(string key, object entry, DateTime utcExpiry)
{
_cache.Put(key, entry, utcExpiry.Subtract(DateTime.UtcNow));
}
Integrating the AppFabricOutputCacheProvider into an MVC application
Now that we have our intial OutputCacheProvider implementation, it is very simple to plug it in to an ASP.NET application. All that is needed is a change to the web.config.
<system.web>
<caching>
<outputCache defaultProvider="AppFabricOutputCacheProvider">
<providers>
<add name="AppFabricOutputCacheProvider"
type="MvcApplication1.AppFabricOutputCacheProvider, MvcApplication1"
cacheName="Something" />
</providers>
</outputCache>
</caching>
</system.web>
If we were to try and run our application at this point, we would get the following error:
ErrorCode
:SubStatus :Server collection cannot be empty.
We see this error because we have not specified which AppFabric cache servers we want to use. The DataCacheFactory class that we used in the Initialize method has two constructors, allowing you to specify a DataCacheFactoryConfiguration or use the default which initialises a new DataCacheFactoryConfiguration internally. Either way, the constructor of the DataCacheFactoryConfiguration class calls a private Initialize method which looks for a custom configuration section in the web.config. Let's add the necessary configuration:
<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="devappfabric01" cachePort="23456"/>
</hosts>
</dataCacheClient>
Make sure you change the host name and port to match your AppFabric environment. If you have multiple cache hosts, you can add them all here and the client will take care of load balancing for you. If you try out the provider with some controller actions decorated with the built-in OutputCache, you will find that your actions are cached as expected. If you were to recycle your IIS app pool or close the ASP.NET development server and restart the app, you should find that your cached page is still being returned. You can also query the AppFabric Cache using the Caching Administration Windows PowerShell to prove that your cached page is coming from AppFabric.
If you were to try and output cache a child action, you may be surprised to find that the partial page is not present in AppFabric and not persisted across application pool recycles. Read on to find out why.
Limitations of OutputCache in MVC
As we mentioned in the previous post on MvcDonutCaching, full and partial page caching is very different in MVC3. Whilst full page caching uses the output cache provider configured in the web.config, partial pages from child actions are always stored in an in process ObjectCache.
The good news is that if you add the MvcDonutCaching NuGet package to your project and change [OutputCache] to [DonutOuputCache], you will find that both full and partial pages are cached within AppFabric. There are lots more benefits too. Read the last post for full details.
Adding support for MvcDonutCaching's RemoveItems functionality.
One of the nice features of MvcDonutCaching is the MVC-centric cached item removal API. This includes the ability to remove multiple cache entries at once, typically by specifying a controller name or a controller and action name. This can be very useful for actions that have parameters such as page number which can result in numerous cache entries for a single action. Without this, we would need to remove the cache entry for each page value individually which is pretty unpleasant.
This functionality requires that we have access to a list of all the keys in the cache. Out of the box, this is not available with the current AppFabric client API, but there is an easy workaround. It IS possible to enumerate over all cache keys within an AppFabric named region. Therefore, we just need to create a new DonutCaching region and put all cached pages in that region. The modified provider is displayed below:
public class AppFabricOutputCacheProvider : OutputCacheProvider, IEnumerable<KeyValuePair<string, object>>
{
private const string CacheRegionName = "DonutCache";
private static DataCache _cache;
public override void Initialize(string name, NameValueCollection config)
{
if (config == null)
{
throw new ArgumentNullException("config");
}
var cacheName = config["cacheName"];
if (cacheName == null)
{
throw new ProviderException("Empty or missing cacheName");
}
config.Remove("cacheName");
if (config.Count > 0)
{
throw new ProviderException("Unrecognized attribute: " + config.GetKey(0));
}
base.Initialize(name, config);
_cache = new DataCacheFactory().GetCache(cacheName);
_cache.CreateRegion(CacheRegionName);
}
public override object Add(string key, object entry, DateTime utcExpiry)
{
var existingEntry = Get(key);
if (existingEntry != null)
{
return existingEntry;
}
Set(key, entry, utcExpiry);
return entry;
}
public override object Get(string key)
{
return _cache.Get(key, CacheRegionName);
}
public override void Remove(string key)
{
_cache.Remove(key, CacheRegionName);
}
public override void Set(string key, object entry, DateTime utcExpiry)
{
_cache.Put(key, entry, utcExpiry.Subtract(DateTime.UtcNow), CacheRegionName);
}
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
return _cache.GetObjectsInRegion(CacheRegionName).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
This appears to work perfectly but there is a problem. Cache items that are NOT within a named region are distributed across all cache hosts in your AppFabric cache cluster. Whilst entries are not duplicated across cache hosts (unless you are using High Availablity), this distributed nature does mean that your cache cluster is far more scalable and can take advantage of the load balancing capabilities of AppFabric. Unfortunately, when using named regions, objects in the region are limited to a single cache host. If you have a smaller site with a single cache host then this is not an issue but for high traffic sites with a number of cache hosts, this is more of a concern.
As the MSDN documentation states, "Regions offer searching capabilities, but by limiting cached objects to a single cache host, the use of regions presents a trade-off between functionality and scalability".
Where is the official AppFabric OutputCacheProvider?
Appfabric v1.1 is currently in development and a CTP release has been available for a few months. One of the new features is the addition of a DistributedCacheOutputCacheProvider. You can integrate this provider in the same way that we integrated the AppFabricOutputCacheProvider - purely via the web.config. The DistributedCacheOutputCacheProvider even uses the same custom config section. Bear in mind that this is a CTP release so the code is probably not ready for production use quite yet. Obviously, you don't get the RemoveItems functionality that is available via the MvcDonutCaching NuGet package.
Code Download
You can download all the code from this article here
Conclusion
Modifying an ASP.NET MVC application to use Windows Server AppFabric is very straightforward. Whilst an official provider has not been released yet, it is easy to create a simple implementation that can be used today.
The built-in OutputCache attribute in MVC3 allows caching of full and partial views, but the code for each is quite different. Whilst full page caching can be moved to AppFabric very easily, partial pages are always cached in process. The open source MvcDonutCaching NuGet package addresses this issue, allowing you to move all output cached content to AppFabric.
Using the RemoveItems API from MvcDonutCaching requires the use of named regions in AppFabric. Unfortunately, these regions limit the cached items to a single cache host which may effect scalability on high traffic sites. This trade-off is well known (see MSDN) so in future releases, I hope that something can be done to allow extended functionality whilst retaining scalability.
thanks so much for this article!
question: how would you then configure a cache profile in your web.config using the appfabric plus donut caching?
i'd like to control the settings in the web.config, rather than hard-coded them in the controller DonutOutputCache attribute.