Loading...
Skip to Content

Blog

Post: Cache-Aside Pattern in .NET Core

Cache-Aside Pattern in .NET Core

Introduction

Often the time comes when we need to focus on optimizing the performance of our application. There are many ways to do this and one way is cache some data. In this post I will describe briefly the Cache-Aside Pattern and its simple implementation in .NET Core.

Cache-Aside Pattern

This pattern is very simple and straightforward. When we need specific data, we first try to get it from the cache. If the data is not in the cache, we get it from the source, add it to the cache and return it. Thanks to this, in the next query the data will be get from the cache. When adding to the cache, we need to determine how long the data should be stored in the cache. Below is an algorithm diagram:

Cache-Aside pattern

First implementation

Implementation of this pattern in .NET Core is just as easy as its theoretical part. Firstly, we need register IMemoryCache interface:

// Register IMemoryCache
public void ConfigureServices(IServiceCollection services)
{
    services.AddMemoryCache();
}

Afterwards, we need add Microsoft.Extensions.Caching.Memory NuGet package.

And that’s all. Assuming that we want to cache basic information about our users, the implementation of the pattern looks as follows:

// First implementation of Cache-Aside Pattern
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
    private readonly IMemoryCache _memoryCache;

    public UsersController(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    [HttpGet]
    [Route("{userId:int}")]
    public ActionResult<UserInfo> Get(int userId)
    {
        UserInfo userInfo;

        if (!this._memoryCache.TryGetValue($"UserInfo_{userId}", out userInfo)) // check whether the data is in the cache
        {
            userInfo = this.GetFromDatabase(userId);

            this._memoryCache.Set($"UserInfo_{userId}", userInfo, new TimeSpan(0, 5, 0));
        }

        return userInfo;
    }
}

First of all, we are injecting .NET Core framework IMemoryCache interface implementation. Then in line 18 we check whether the data is in the cache. If it is not in the cache, we get it from the source (i.e. database), add to cache and return.

Code smells

This way of implementation you can find on MSDN site. I could finish this post at this point, but I must admit that there are a few things that I do not like about this code.

First of all, I think the interface IMemoryCache is not abstract enough. It suggests that the data is kept in application memory but the client code should not care where it is stored. Moreover, if we want to keep the cache in the database in the future, the name of this interface will not be correct.

Secondly, client code should not be responsible for logic of the naming cache key. It is Single Responsibility Principle violation. It should only provide data to create this key name.

Lastly, client code should not care about cache expiration. It should be configured in other place - application configuration.

In next section I will show how we can eliminate these 3 code smells.

Improved implementation

The first and most important step is to define a new, more abstract interface: ICacheStore:

// ICacheStore interface
public interface ICacheStore
{
    void Add<TItem>(TItem item, ICacheKey<TItem> key);

    TItem Get<TItem>(ICacheKey<TItem> key) where TItem : class;
}

Then we need to define interface for our cache key classes:

// ICacheKey interface
public interface ICacheKey<TItem>
{
    string CacheKey { get; }
}

This interface has CacheKey string property which is used during resolving cache key in our MemoryCacheStore implementation:

// MemoryCacheStore implementation
public class MemoryCacheStore : ICacheStore
{
    private readonly IMemoryCache _memoryCache;
    private readonly Dictionary<string, TimeSpan> _expirationConfiguration;

    public MemoryCacheStore(
        IMemoryCache memoryCache,
        Dictionary<string, TimeSpan> expirationConfiguration)
    {
        _memoryCache = memoryCache;
        this._expirationConfiguration = expirationConfiguration;
    }

    public void Add<TItem>(TItem item, ICacheKey<TItem> key)
    {
        var cachedObjectName = item.GetType().Name;
        var timespan = _expirationConfiguration[cachedObjectName];

        this._memoryCache.Set(key.CacheKey, item, timespan);
    }

    public TItem Get<TItem>(ICacheKey<TItem> key) where TItem : class
    {
        if (this._memoryCache.TryGetValue(key.CacheKey, out TItem value))
        {
            return value;
        }

        return null;
    }
}

Finally, we need to configure IoC container to resolve MemoryCacheStore instance as ICacheStore together with expiration configuration taken from application configuration:

// Configuration of IoC container"
var children = this.Configuration.GetSection("Caching").GetChildren();
Dictionary<string, TimeSpan> configuration = 
children.ToDictionary(child => child.Key, child => TimeSpan.Parse(child.Value));

services.AddSingleton<ICacheStore>(x => new MemoryCacheStore(x.GetService<IMemoryCache>(), configuration));

This is how new implementation looks like:

Implementation

After this set up we can finally use this implementation in our client code. For each new object that we want to store in cache we need:

  1. Add expiration configuration
/* Caching expiration configuration */
"Caching": {
  "UserInfo": "00:05:00"
}
  1. Class that defines the cache key
// UserInfoCacheKey class
public class UserInfoCacheKey : ICacheKey<UserInfo>
{
   private readonly int _userId;
   public UserInfoCacheKey(int userId)
   {
       _userId = userId;
   }

   public string CacheKey => $"UserId_{this._userId}";
}

Finally, the new client code looks like this:

// Improved implementation usage
[Route("api/[controller]")]
[ApiController]
public class Users2Controller : ControllerBase
{
    private readonly ICacheStore _cacheStore;

    public Users2Controller(ICacheStore cacheStore)
    {
        _cacheStore = cacheStore;
    }

    [HttpGet]
    [Route("{userId:int}")]
    public ActionResult<UserInfo> Get(int userId)
    {
        var userInfoCacheKey = new UserInfoCacheKey(userId);
        UserInfo userInfo = this._cacheStore.Get(userInfoCacheKey);

        if (userInfo == null)
        {
            userInfo = this.GetFromDatabase(userId);

            this._cacheStore.Add(userInfo, userInfoCacheKey);
        }

        return userInfo;
    }
}

In the code above we use more abstract ICacheStore interface, don’t care about creation of cache key and expiration configuration. It is more elegant solution and less error-prone.

Summary

In this post I described Cache-Aside Pattern and its primary implementation in .NET Core. I proposed also augmented design to achieve more elegant solution with a small amount of work. Happy caching! :)

UPDATE 2019-26-02: I updated my sample codebase so if you would like to see full, working example – check my GitHub repository.

Image credits: rawpixel.com on Freepik.

Comments

Related posts See all blog posts