As a responsible developer, in addition to being able to write clean code, it is also important to ensure that our application runs efficiently. As we know, we must return results as quickly as possible to provide a better experience to customers. Well, one of the most basic performance improvements we can implement quickly is a cache implementation for repeated access.

Although it seems like a simple process, it gives our applications a significant performance increase in many aspects. In addition, it enables us to use our resources more effectively.

In this article, I will try to show how we can easily implement the Cache-Aside pattern in .NET.

Well,

As we can see from the diagram above, our approach is to always read data from the cache first.

If the data we want to get is not in cache, we need to get the data from the relevant data store and then return the response by adding it to the cache. Thus, by minimizing repeated access, we can provide better experiences to customers and use our resources much more effectively.

Consistency

The cache-aside pattern doesn’t provide any consistency guarantee. In other words, data we want to get may have been changed by a different process.

If we are working in a distributed environment, we can try to keep the data up-to-date as possible with pub/sub approach. Of course, in this case, we must also accept eventual consistency.

Expiration Policy

Another important issue is that the data we want to cache has an expiration policy. Otherwise, the data, which will be in the cache, may become invalid after a while.

We need to set the expiration policy carefully to provide consistency. If we set this expiration time too short, we will not get much benefit from this approach since the repeated access will occur again.

In short, we need to adjust this time carefully according to the domain we have.

Implementation

I will create a simple ASP.NET 5 Web API project for implementation. Our goal here is to be able to create a flexible and reusable cache structure with minimum effort while implementing the cache-aside pattern.

For this, let’s first create an interface called “ICacheService” as follows.

using System;
using System.Threading.Tasks;

namespace MyTodoAPI.Services
{
    public interface ICacheService
    {
        Task<T> GetOrSetAsync<T>(string cacheKey, int cacheDurationInMin, Func<Task<T>> func);
        Task Remove(string cacheKey);
    }
}

We will implement the cache-aside pattern in the “GetOrSetAsync” method. Also in the “Remove” method, we will add the required logic to invalidate the cache of any data we want.

As the cache service, we will implement Redis. For this, I will use Azure Redis Cache service. You can follow the steps here to have Azure Redis Cache service.

Before starting to the implementation, let’s add the “Microsoft.Extensions.Caching.StackExchangeRedis” package to the project via NuGet as follows.

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis --version 5.0.1

Then, let’s implement the “ICacheService” interface by creating a class called “RedisCacheService“.

using System;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;

namespace MyTodoAPI.Services
{
    public class RedisCacheService : ICacheService
    {
        private readonly IDistributedCache _distributedCache;

        public RedisCacheService(IDistributedCache distributedCache)
        {
            _distributedCache = distributedCache;
        }

        public async Task<T> GetOrSetAsync<T>(string cacheKey, int cacheDurationInMin, Func<Task<T>> func)
        {
            string cachedItem = await _distributedCache.GetStringAsync(cacheKey);

            if(!string.IsNullOrEmpty(cachedItem))
            {
                return JsonSerializer.Deserialize<T>(cachedItem);
            }

            var item = await func();

            if (item != null)
            {
                var cacheEntryOptions = new DistributedCacheEntryOptions()
                                        .SetSlidingExpiration(TimeSpan.FromMinutes(cacheDurationInMin));

                string serializedItem = JsonSerializer.Serialize(item);

                await _distributedCache.SetStringAsync(cacheKey, serializedItem, cacheEntryOptions);
            }

            return item;
        }

        public async Task Remove(string cacheKey)
        {
            await _distributedCache.RemoveAsync(cacheKey);
        }
    }
}

If we look at the code above, we can see that we use the “IDistributedCache” interface that comes as build-in for caching operations. We will also inject the redis as an adapter in the next step.

In the “GetOrSetAsync” method, we have used the func delegate to get a reusable structure. We can also see that the method flow is similar to the diagram I have shared above.

In addition, we have implemented the “Remove” method to invalidate a cache.

Let’s make an example

To perform an example, I will go with “WeatherForecast” which comes with the default template.

For this, let’s create an interface called “IWeatherForecastService” and a class called “WeatherForecastService” as follows.

using System.Collections.Generic;
using System.Threading.Tasks;

namespace MyTodoAPI.Services
{
    public interface IWeatherForecastService
    {
        Task<List<WeatherForecast>> GetWeatherForecasts();
        Task AddNewWeatherSummary(string summary);
    }
}
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace MyTodoAPI.Services
{
    public class WeatherForecastService : IWeatherForecastService
    {
        private readonly ICacheService _cacheService;

        private static List<string> Summaries = new()
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private const string GetWeatherForecastsCacheKey = "GetWeatherForecasts";
        private const string GetWeatherForecastsCacheDurationInMin = 30;

        public WeatherForecastService(ICacheService cacheService)
        {
            _cacheService = cacheService;
        }

        public async Task<List<WeatherForecast>> GetWeatherForecasts()
        {
            List<WeatherForecast> weatherForecasts = await _cacheService.GetOrSetAsync(cacheKey: GetWeatherForecastsCacheKey,
                                                                                       cacheDurationInMin: GetWeatherForecastsCacheDurationInMin,
                                                                                       func: () =>
            {
                var rng = new Random();
                var weatherForecasts = new List<WeatherForecast>();

                foreach (var item in Summaries)
                {
                    weatherForecasts.Add(new WeatherForecast
                    {
                        Date = DateTime.Now,
                        TemperatureC = rng.Next(-20, 55),
                        Summary = item
                    });
                }

                return Task.FromResult(weatherForecasts);
            });

            return weatherForecasts;
        }

        public Task AddNewWeatherSummary(string summary)
        {
            Summaries.Add(summary);

            _cacheService.Remove(GetWeatherForecastsCacheKey);

            return Task.CompletedTask;
        }
    }
}

We have implemented a simple logic using the “ICacheService” in the “GetWeatherForecasts” method. If the data we want has been added to the cache before, we will be able to get this data quickly through the cache.

Also, if we want to add a new “summary”, we will invalidate the corresponding cache with the “Remove” method.

At last let’s perform the injection operations in the “Startup” as follows.

services.AddStackExchangeRedisCache(opt =>
{
    opt.Configuration = Configuration.GetConnectionString("redis:connection_string");
});

services.AddSingleton<ICacheService, RedisCacheService>();
services.AddScoped<IWeatherForecastService, WeatherForecastService>();

Conclusion

As we can see, the cache-aside pattern is very easy to implement. It allows us to add the desired data to the cache at the demand of the applications and get the cached data. Thus, we can benefit from caching operations as much as possible even in case of unforeseen situations.

References

Cache-Aside pattern – Cloud Design Patterns | Microsoft Docs

Gökhan Gökalp

View Comments

Recent Posts

Overcoming Event Size Limits with the Conditional Claim-Check Pattern in Event-Driven Architectures

{:en}In today’s technological age, we typically build our application solutions on event-driven architecture in order…

2 months ago

Securing the Supply Chain of Containerized Applications to Reduce Security Risks (Policy Enforcement-Automated Governance with OPA Gatekeeper and Ratify) – Part 2

{:tr} Makalenin ilk bölümünde, Software Supply Chain güvenliğinin öneminden ve containerized uygulamaların güvenlik risklerini azaltabilmek…

7 months ago

Securing the Supply Chain of Containerized Applications to Reduce Security Risks (Security Scanning, SBOMs, Signing&Verifying Artifacts) – Part 1

{:tr}Bildiğimiz gibi modern yazılım geliştirme ortamında containerization'ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi.…

9 months ago

Delegating Identity & Access Management to Azure AD B2C and Integrating with .NET

{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…

1 year ago

How to Order Events in Microservices by Using Azure Service Bus (FIFO Consumers)

{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…

2 years ago

Providing Atomicity for Eventual Consistency with Outbox Pattern in .NET Microservices

{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…

2 years ago