Thread-Safe, Strongly-Typed Memory Caching in C#

Quick and simple. This extension adds a strongly-typed version of AddOrGetExisting that utilizes Lazy<T> to ensure thread-safety.

public static class ObjectCacheExtensions
{
    public static T AddOrGetExisting<T>(this ObjectCache cache,
                                        string key,
                                        Func<T> valueFactory,
                                        CacheItemPolicy policy)
    {
        var newValue = new Lazy<T>(valueFactory);
        var oldValue = cache.AddOrGetExisting(key, newValue, policy) as Lazy<T>;

        try
        {
            return (oldValue ?? newValue).Value;
        }
        catch
        {
            cache.Remove(key);
            throw;
        }
    }
}

Usage

ObjectCache cache = MemoryCache.Default;
var foo = cache.AddOrGetExisting<int>("foo",
    () =>
    {
        return 1;
    },
    new CacheItemPolicy
    {
        AbsoluteExpiration = DateTimeOffset.Now.AddHours(1),
    });

Let's break it down. The first param, "foo", is our key in the cache. The second is a lambda that returns the value that we want to cache. This could be literally anything: a database query, call to a web API, etc. Thanks to Lazy<T>, the lambda is only evaluated if there's nothing in the cache yet, meaning we avoid the expensive work, if it's already been done before. The final param is an instance of CacheItemPolicy, which allows you define the rules for how the item will be cached. Here, we're just telling it to hold the value in cache for one hour.

That's all there is to it. Let me know if this helped you out in the comments, and I'll try to add more quickies like it in the future.

UPDATE (07/07/2016)

This is good time to point out that we developers often just claim to know what the bloody hell we're doing. This little bit of code satisfied my needs at the time, so I put a cherry on top and called it day. Then, I came across a situation where I wanted to cache the result of an async operation, and Visual Studio started screaming in red underscores. Oh, yeah, huh. This code only works with synchronous operations, not async, so let's do something about that.

I made a few futile first attempts to add some async goodness, only to realize that Lazy<T> wrapped async lambdas aren't going to work. Notably, Lazy<T> doesn't support async, so I took to the friendly Interwebs to look for a solution. Credit here goes to Stephen Toub for his post on the MSDN blog. It's well worth a read, if you have the time, to understand how this all works, but suffice to say, I stole some of his code:

public class AsyncLazy<T> : Lazy<Task<T>> 
{ 
    public AsyncLazy(Func<T> valueFactory) : 
        base(() => Task.Factory.StartNew(valueFactory)) { }
    public AsyncLazy(Func<Task<T>> taskFactory) : 
        base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap()) { } 
}

With that, we can now make an async version of our previous AddOrGetExisting<T>:

public static async Task<T> AddOrGetExistingAsync<T>(
    this ObjectCache cache,
    string key,
    Func<Task<T>> valueFactory,
    CacheItemPolicy policy)
{
    var newValue = new AsyncLazy<T>(valueFactory);
    var oldValue = cache.AddOrGetExisting(key, newValue, policy) as Lazy<T>;

    try
    {
        return oldValue != null ? oldValue.Value : await newValue.Value;
    }
    catch
    {
        cache.Remove(key);
        throw;
    }
}

For newValue, we're now using AsyncLazy<T>. Other than that, I just changed the contents of the try block to a ternary rather than a null coalesce, as we're now dealing with both Lazy<T> and AsyncLazy<T>, rather than two Lazy<T>s. You call it exactly the same as with the other method, just passing a lambda that returns Task instead.

var foo = cache.AddOrGetExistingAsync<Foo>("foo",
    () => db.Foos.FindAsync(id),
    new CacheItemPolicy { AbsoluteExpiration = DateTimeOffset.Now.AddHours(1) }
);

Bear one thing in mind, though. Here, we can return the Task<T> directly from the lambda. However, if you needed to operate on the Foo instance in the lambda, you would need to use something like:

async () => {
    var foo = await db.Foos.FindAsync(id);
    // do something with `foo`
    return foo;
}
comments powered by Disqus