Tag Archives: Outputcache

OutputCacheProvider for Redis

Following this series of posts about caching and OutputCache in particular, I will now show you how to use Redis to store the output cache items in your ASP.NET MVC application. You can use the same concepts to store in any other data store, such as AppFabric, MongoDb, File System, etc.

First, we will need a Redis client. In this implementation i’m using StackExchange.Redis, so you will need to run this in the package manager console (or via nuget package manager UI):

Install-Package StackExchange.Redis

Next, let’s implement the provider. The first thing you should do is to create a class that inherits from System.Web.Caching.OutputCacheProvider, and then provide implementations for Add, Set, Get and Remove methods. The stubs look like this:

public class RedisOutputCacheProvider : OutputCacheProvider
{
    public override object Add(string key, object entry, DateTime utcExpiry)
    {
        throw new NotImplementedException();
    }

    public override object Get(string key)
    {
        throw new NotImplementedException();
    }

    public override void Remove(string key)
    {
        throw new NotImplementedException();
    }

    public override void Set(string key, object entry, DateTime utcExpiry)
    {
        throw new NotImplementedException();
    }
}

Next, we will connect to the Redis cluster. Since i’m only using my local development machine for testing, i’ll create a ConnectionMultiplexer like so:

private static ConnectionMultiplexer Redis = ConnectionMultiplexer.Connect("localhost:6379");

Also, since this Redis client needs to receive a byte[] when storing the entries, we will need to handle the serialization as well. I created an abstraction for this, also because I wanted to test with different serializers:

public interface IItemSerializer
{
    byte[] Serialize(object item);
    object Deserialize(byte[] itemBytes);
}

Now, for the provider implementation:

public class RedisOutputCacheProvider : OutputCacheProvider
{
    private static ConnectionMultiplexer Redis = ConnectionMultiplexer.Connect("localhost:6379");
    private static IItemSerializer Serializer = new BinaryFormatterItemSerializer();

    public override object Add(string key, object entry, DateTime utcExpiry)
    {
        // return the existing value if any
        var existingValue = Get(key);
        if (existingValue != null)
        {
            return existingValue;
        }

        var expiration = utcExpiry - DateTime.UtcNow;
        var entryBytes = Serializer.Serialize(entry);

        var db = Redis.GetDatabase();

        // set the cache item with the defined expiration, only if an item does not exist
        db.StringSet(key, entryBytes, expiration, When.NotExists);

        return entry;
    }

    public override void Set(string key, object entry, DateTime utcExpiry)
    {
        var expiration = utcExpiry - DateTime.UtcNow;
        var entryBytes = Serializer.Serialize(entry);

        var db = Redis.GetDatabase();

        // set the cache item with the defined expiration, overriding existing items
        db.StringSet(key, entryBytes, expiration, When.Always);
    }

    public override object Get(string key)
    {
        var db = Redis.GetDatabase();

        var valueBytes = db.StringGet(key);

        if (!valueBytes.HasValue)
        {
            return null;
        }

        var value = Serializer.Deserialize(valueBytes);

        return value;
    }

    public override void Remove(string key)
    {
        var db = Redis.GetDatabase();

        // signal key removal but don't wait for the result
        db.KeyDelete(key);
    }
}

The provider implementation must follow the documentation guidelines in the MSDN page.

In particular, these are the main design rules for the Add method:

  • If there is already a value in the cache for the specified key, the provider must return that value.
  • The Add method stores the data if it is not already in the cache.
  • If the data is in the cache, the Add method returns it.

In the example above, i’m using a implementation of the IItemSerializer interface that uses the BinaryFormatter for the serialization:

public class BinaryFormatterItemSerializer : IItemSerializer
{
    public byte[] Serialize(object item)
    {
        var formatter = new BinaryFormatter();

        byte[] itemBytes;

        using (var ms = new MemoryStream())
        {
            formatter.Serialize(ms, item);
            itemBytes = ms.ToArray();
        }

        return itemBytes;
    }

    public object Deserialize(byte[] bytes)
    {
        var formatter = new BinaryFormatter();

        object item;
        using (var ms = new MemoryStream(bytes))
        {
            item = formatter.Deserialize(ms);
        }

        return item;
    }
}

Unit testing

I wrote some unit tests so that we can check that everything is working correctly before using it in our application. The first test checks all the basic functionality, and the second one checks the ability of the cache provider to remove expired items:

[TestClass]
public class RedisOutputCacheProviderTests
{
    [TestMethod]
    public void CanAddAndGetAndSetAndRemoveItem()
    {
        var key = "mykey" + Guid.NewGuid();
        var provider = new RedisOutputCacheProvider();

        // test add and get

        provider.Add(key, "myvalue", DateTime.UtcNow.Add(TimeSpan.FromMinutes(1)));

        var value = (string)provider.Get(key);

        Assert.AreEqual("myvalue", value);

        // test set and get

        provider.Set(key, "anothervalue", DateTime.UtcNow.Add(TimeSpan.FromMinutes(1)));

        var valueAfterSet = (string)provider.Get(key);

        Assert.AreEqual("anothervalue", valueAfterSet);

        // test remove and get

        provider.Remove(key);

        var valueAfterRemove = (string)provider.Get(key);
        Assert.Is Null(valueAfterRemove);
    }

    [TestMethod]
    public async Task EntryExpiresAfterAbsoluteTime()
    {
        var key = "mykey" + Guid.NewGuid();
        var provider = new RedisOutputCacheProvider();

        // add the value and check that it is in cache

        provider.Add(key, "myvalue", DateTime.UtcNow.Add(TimeSpan.FromSeconds(3)));
        var value = (string)provider.Get(key);
        Assert.AreEqual("myvalue", value);

        await Task.Delay(3000);

        var valueAfterExpiration = (string)provider.Get(key);
        Assert.Is Null(valueAfterExpiration);
    }
}

Configure your web application

After checking that the basic behavior is fine, we can integrate the provider into our web application. Assuming that you are already using the OutputCacheAttribute, you can do this by adding the following xml to your web.config:

<system.web>
  (...)
  <caching>
    <outputCache defaultProvider="RedisOutputCache">
      <providers>
        <add name="RedisOutputCache" type="YourNamespace.RedisOutputCacheProvider, YourAssemblyName" />
      </providers>
    </outputCache>
  </caching>
  (...)
</system.web>

Then, if you go back to your web application, your OutputCache attributes should all be using this new provider!

Further work

I was also experimenting with a faster binary serializer, protobuf-net. It works for unit tests, but since the type that the OutputCache is trying to store is internal, we cannot use this implementation at the moment. I will try to figure out a way to use protobuf-net for this, and if you have any suggestions please drop a comment below:

    ...
    using ProtoBuf;
    ...
    public class ProtoBufItemSerializer : IItemSerializer
    {
        public byte[] Serialize(object entry)
        {
            var item = new CacheItem { Value = entry };

            byte[] itemBytes;

            using (var ms = new MemoryStream())
            {
                Serializer.Serialize<CacheItem>(ms, item);
                itemBytes = ms.ToArray();
            }

            return itemBytes;
        }

        public object Deserialize(byte[] bytes)
        {
            CacheItem item;
            using (var ms = new MemoryStream(bytes))
            {
                item = Serializer.Deserialize<CacheItem>(ms);
            }

            return item.Value;
        }

        [ProtoContract]
        public class CacheItem
        {
            [ProtoMember(1, DynamicType = true)]
            public object Value { get; set; }
        }
    }

Finally, since I didn’t want my web application to fail when temporary connection issues to the Redis cluster arise, I wrapped all the implementations in a try-catch, so that if the cache fails, MVC will ignore the cache and execute the action.

Example for the Get method:

public override object Get(string key)
{
    try
    {
        // implementation goes here
    }
    catch (RedisConnectionException e)
    {
        Trace.TraceError(e.ToString());
        return null;
    }
}

You can test this by loading up your web application, making a request to a OutputCached action, and then shutting down redis and reloading the page. The page should still show up, but the content is being regenerated in every request.

Invalidating cache when data changes

You can invalidate a cache item over the response context. As an example, I created one more action in my controller to invalidate the Index item cache:

public string InvalidateCache()
{
    Response.RemoveOutputCacheItem(Url.Action("Index", "Home"));
    return "Done";
}

Code on GitHub

I created a repository on github to host these and other OutputCache-related extensions. You can find the full code at https://github.com/danielbcorreia/OutputCacheExtensions.

If you have any comments or suggestions about this article, go ahead and write in the comments.

 

ASP.NET MVC OutputCache Vary by Current User

Caching user-specific data is a common requirement in web applications. Most times the first thing that you do when you sign in a user in your website is redirect them to a dashboard that is unique for that user. When the user spends a lot of time on his dashboard, you need to make sure that you can improve his experience without compromising your ability to fetch all the relevant data from all the different parts of your system.

In ASP.NET MVC, the easier way to cache the full rendered page is to use the OutputCacheAttribute, like so:

[Authorize]
public class DashboardController : Controller 
{
    [OutputCache(Duration = 3600)]
    public ActionResult Index() 
    {
       // Your awesome code goes here
    }
}

The previous code would make the Index action stay in cache for one hour. This works great for public content, but for user-specific content you must complement the cache key, so that different users don’t see each-other’s cache.

To do this, we use the VaryByCustom property of the OutputCacheAttribute, like this:

[Authorize]
public class DashboardController : Controller 
{
    [OutputCache(Duration = 3600, VaryByCustom = "User")]
    public ActionResult Index() 
    {
       // Your awesome code goes here
    }
}

And you also need to handle that “User” value in a method that is called over the HttpApplication: the GetVaryByCustomString method.
So, you need to go to your Global.asax.cs and override that method:

public override string GetVaryByCustomString(HttpContext context, string arg) 
{ 
    if (arg.Equals("User", StringComparison.InvariantCultureIgnoreCase)) 
    {
        var user = context.User.Identity.Name; // TODO here you have to pick an unique identifier from the current user identity
        return string.Format("{0}@{1}", userIdentifier.Name, userIdentifier.Container); 
    }

    return base.GetVaryByCustomString(context, arg); 
}

Or, if you don’t set a custom principal in your pipeline, you can look for the session id like so:

private static SessionStateSection SessionStateSection = (System.Web.Configuration.SessionStateSection)ConfigurationManager.GetSection("system.web/sessionState");

public override string GetVaryByCustomString(HttpContext context, string arg) 
{ 
    if (arg.Equals("User", StringComparison.InvariantCultureIgnoreCase)) 
    {
        var cookieName =  SessionStateSection.CookieName;
        var cookie = context.Request.Cookies[cookieName];
        return cookie.Value;
    }

    return base.GetVaryByCustomString(context, arg); 
}

I’m using the session state configuration section to get the session cookie name, so that if you changed the default “ASP.NET_SessionId” it will still work.

I hope you will find this handy. If you have more cache subjects you would like me to write about, please write in the comments.
In the next post I expect to demonstrate how to implement an OutputCacheAttribute that that enables you to choose another cache provider instead of the memory cache, so that you can use a distributed cache system like AppFabric or a Redis Cluster to store your rendered pages.