ASP.NET 4.0: Writing custom output cache providers

Until now we can use ASP.NET default output cache. We don’t have any option to write our own output cache mechanisms. ASP.NET 4.0 introduces extensible output cache – programmers can write their own output cache providers. In this posting I will show you how to write your own output cache provider and give you my sample solution to download.

Preparing

Before we begin with caching we need Visual Studio 2010 solution that has two projects – one web application and one class library. Class library is for output cache provider and web application is for testing purposes. My solution uses following names:

  • Solution: CacheProviderDemo
  • Web application: CacheProviderApp
  • Class library: MyCacheProviders

Before moving on let’s modify web application and add project reference to class library. In this case we don’t have to copy our cache provider to GAC after each build.

Configuring output caching

Now let’s change our web.config and add there the following configuration block under system.web section.


<caching>
<
outputCache defaultProvider="AspNetInternalProvider">
  <providers>
   <add name="FileCache"
    type="MyCacheProviders.FileCacheProvider, MyCacheProviders"/>
  </providers
>
</
outputCache
>
</
caching
>

This configuration block tells ASP.NET that there is output cache provider called FileCache available but we want to use default provider calles AspNetInternalProvider. We will change this block later.

Writing output cache provider

Now let’s start with output cache provider. From output cache configuration block you can see that Class1.cs in our class library must be renamed to FileCacheProvider.cs. Don’t forget also change the class name. Add reference to System.Web to class library and open FileCacheProvider.cs. In the beginning of this file add using line for System.Web.Cachingnamespace.

Our output cache provider class must extend OutputCacheProvider base class. This base class defines all methods that output cache providers must implement. Our output cache provider uses file system for cache storage. Output cache entries are serialized to binary format and back when writing and reading objects from files.

Classes for our custom output cache provider are here.


public class FileCacheProvider : OutputCacheProvider
{
   
private string
_cachePath;

   
private string
CachePath
    {
       
get
        {
           
if (!string
.IsNullOrEmpty(_cachePath))
               
return
_cachePath;

            _cachePath =
ConfigurationManager.AppSettings["OutputCachePath"
];
           
var context = HttpContext
.Current;

           
if (context != null
)
            {
                _cachePath = context.Server.MapPath(_cachePath);
               
if (!_cachePath.EndsWith("\\"
))
                    _cachePath +=
"\\"
;
            }

           
return
_cachePath;
        }
    }

   
public override object Add(string key, object
entry, DateTime utcExpiry)
    {
       
Debug.WriteLine("Cache.Add(" + key + ", " + entry + ", " + utcExpiry + ")"
);

       
var
path = GetPathFromKey(key);

       
if (File
.Exists(path))
           
return
entry;

       
using (var file = File
.OpenWrite(path))
        {
           
var item = new CacheItem
{ Expires = utcExpiry, Item = entry };
           
var formatter = new BinaryFormatter
();
            formatter.Serialize(file, item);
        }

       
return
entry;
    }

   
public override object Get(string
key)
    {
       
Debug.WriteLine("Cache.Get(" + key + ")"
);

       
var
path = GetPathFromKey(key);

       
if (!File
.Exists(path))
           
return null
;

       
CacheItem item = null
;

       
using (var file = File
.OpenRead(path))
        {
           
var formatter = new BinaryFormatter
();
            item = (
CacheItem
)formatter.Deserialize(file);
        }

       
if (item == null
|| item.Expires <= DateTime.Now.ToUniversalTime())
        {
            Remove(key);
           
return null
;
        }

       
return
item.Item;
    }

   
public override void Remove(string
key)
    {
       
Debug.WriteLine("Cache.Remove(" + key + ")"
);

       
var
path = GetPathFromKey(key);

       
if (File
.Exists(path))
           
File
.Delete(path);
    }

   
public override void Set(string key, object
entry, DateTime utcExpiry)
    {
       
Debug.WriteLine("Cache.Set(" + key + ", " + entry + ", " + utcExpiry + ")"
);

       
var item = new CacheItem
{ Expires = utcExpiry, Item = entry };
       
var
path = GetPathFromKey(key);

       
using (var file = File
.OpenWrite(path))
        {
           
var formatter = new BinaryFormatter
();
            formatter.Serialize(file, item);
        }
    }

   
private string GetPathFromKey(string
key)
    {
       
return CachePath + MD5(key) + ".txt"
;
    }

   
private string MD5(string
s)
    {
       
var provider = new MD5CryptoServiceProvider
();
       
var bytes = Encoding
.UTF8.GetBytes(s);
       
var builder = new StringBuilder
();

        bytes = provider.ComputeHash(bytes);

       
foreach (var b in
bytes)
            builder.Append(b.ToString(
"x2"
).ToLower());

       
return builder.ToString();
    }
}

With cached item we have to keep date and time that say us when cached object expires. I write simple CacheItem class that is marked as serializable (otherwise it is not possible to serialize it and therefore we cannot use binary formatter). CacheItem class is simple.


[Serializable]
internal class CacheItem
{
   
public
DateTime Expires;
   
public object Item;
}

I made this class internal because we have no reason to show it to the world. We only use this class to store and restore cache entries with expiry date.

Some notes for FileCacheProvider class.

  • You must have appSetting called OutputCachePath that contains path to cache files folder. This path may contain ~ because in the case of web application this path goes through Server.MapPath() method.
  • File names are made of MD5 hashed cache entry key and .txt extension. You can use whatever file extension you like.
  • I am using binary formatter because it workes better for me than XmlSerializer (Add() method is used with internal classes that have no public constructor with empty argument list).
  • My code uses calls to Debug.WriteLine() method. When you run web application with Visual Studio then you can see output cache messages in output window. I found that it is very easy and powerful way to debug provider because these messages show you what is going on.

Preparing web application for testing

Let’s prepare now web application for testing. As a first thing we must make some changes to web.config. We have to add appSetting for cache folder and we have to make our file cache provider as default output cache provider. Here are XML-blocks for configuration.


<appSettings>
  <add key="OutputCachePath" value="~/Cache/" 
/>
</
appSettings>
 
<caching>
  <outputCache defaultProvider="FileCache">
    <providers>
      <add name="FileCache" type="MyCacheProvider.FileCacheProvider, MyCacheProvider"/>
    </providers>
  </outputCache
>
</
caching
>

I expect you know where to put these blocks in web.config file. We have one more change to make before running our web application. Let’s turn output caching on for it. Although there should be providerName attribute available it doesn’t work yet. At least for my installation of Visual Studio 2010 and ASP.NET 4.0. Add the following line to Default.aspx.


<%@ OutputCache VaryByParam="ID" Duration="300" %>


I added VaryByParam for one reason: to test caching with different ID-s. Cache duration is 300 seconds (5 minutes). You can change to attributes if you like.

Testing output cache provider

Now let’s test our output cache provider. Run web application.

Empty ASP.NET application

When you see default page in your browser check out output window in Visual Studio 2010. You should see there something like this.


Cache.Get(a2/default.aspx)

Cache.Add(a2/default.aspx, System.Web.Caching.CachedVary, 31.12.9999 23:59:59)

Cache.Set(a2/default.aspxHQNidV+n+FCDE, System.Web.Caching.OutputCacheEntry, 19.11.2009 12:20:56)


After refresh you should see something like this in output window.


Cache.Get(a2/default.aspx)

Cache.Get(a2/default.aspxHQNidV+n+FCDE)

Cache.Get(a2/styles/site.css)

Cache.Get(a2/webresource.axd)

Cache.Set(a2/styles/site.css, System.Web.Caching.OutputCacheEntry, 20.11.2009 12:17:09)

Cache.Add(a2/webresource.axd, System.Web.Caching.CachedVary, 31.12.9999 23:59:59)

Cache.Set(a2/webresource.axdHQNdVjnaEMMY-tPD2-oCbZFp7vHhVoQiB8SS98KHQMNKjUfs1FCDE, System.Web.Caching.OutputCacheEntry, 19.11.2010 12:17:09)

Cache.Get(a2/webresource.axd)

Cache.Get(a2/webresource.axdHQNdVqx_c6oOlTqymCAsGZh_8JA2FCDE)

Cache.Add(a2/webresource.axd, System.Web.Caching.CachedVary, 31.12.9999 23:59:59)

Cache.Set(a2/webresource.axdHQNdVqx_c6oOlTqymCAsGZh_8JA2FCDE, System.Web.Caching.OutputCacheEntry, 19.11.2010 12:17:10)


You see that second request caches some more data. There requests to FileCacheProvider are made by ASP.NET. During additional refresh some resources get cached and the other resources are removed from output cache.

Here is one simple screenshot fragment of file cache folder.

File cache provider output

You can open these files to see what is inside them. You should see there cache items in binary format as binary formatter serializes them.

Conclusion

Although there are not much informative and guiding documentation about custom output cache providers I got mine working seemlessly well. I am not sure about some aspects of my provider and I will consult with Microsoft about my doubts. Of course, I will let you know if I get some valuable feedback from Microsoft.

The point of this posting was to show you how to write custom output cache provider. As we saw it is not something crucial or complex – we have to extend OutputCacheProvider class and implement some methods that store, edit and clear cached items. You can write your own implementations of output cache provider and keep your output cache wherever you like.

Download sample solution


One thought on “ASP.NET 4.0: Writing custom output cache providers

  • Howie says:

    Sorry to dig up the dead, but we’ve been working with OutputCacheProviders recently and have noticed that the utcExpiry for Add is always 12/31/9999 23:59:59… Set respects the value we have configured in the OutputCache duration attribute in the annotation.

Leave a Reply

Your email address will not be published. Required fields are marked *