We’ve recently implemented Redis caching in an API which took a little bit of time to figure out.

Redis caching is available through the Microsoft.Extensions.Caching.Distributed package and this also supports other provides such as NCache.

In our case we only wanted to enable Redis caching for the stg and prd environments and to use an in memory cache in dev and local to keep costs down and make testing easier. This was all specified in the project Startup.cs.

// Add distributed cache
var redisOptions = _configuration.GetOptions<RedisConnectionOptions>("Redis");
        
if (_hostingEnv.EnvironmentName == EnvironmentType.Staging.ToString() || _hostingEnv.EnvironmentName == EnvironmentType.Production.ToString())
{
    services.AddStackExchangeRedisCache(options =>
    {
        options.ConfigurationOptions = new ConfigurationOptions
        {
            EndPoints = { $"{redisOptions.Endpoint}:{redisOptions.Port}" },
            Password = redisOptions.Password,
            Ssl = true,
            AllowAdmin = true,
        };
        options.InstanceName = $"MyProject-{_hostingEnv.EnvironmentName}";
    });
}
else
{
    services.AddDistributedMemoryCache();
}

We had some issues with directly providing a formatted connection string initially but providing the parameters and letting the constructor deal with it fixed that.

By default IDistributedCache stores data as a byte array and not as a JSON object so if your API is returning JSON as ours is you might want to create some additional extension methods as mentioned here.

public static class DistributedCacheExtensions
{
    public static Task SetAsync<T>(this IDistributedCache cache, string key, T value)
    {
        return SetAsync(cache, key, value, new DistributedCacheEntryOptions());
    }
        
    public static Task SetAsync<T>(this IDistributedCache cache, string key, T value, DistributedCacheEntryOptions options)
    {
            
        var bytes = Encoding.UTF8.GetBytes(JsonHelper.Serialize(value)!);
        return cache.SetAsync(key, bytes, options);
    }

    public static bool TryGetValue<T>(this IDistributedCache cache, string key, out T? value)
    {
        var val = cache.Get(key);
        value = default;
        if (val == null) return false;
        value = JsonHelper.Deserialize<T>(val);
        return true;
    }
}

We’re using caching as part of a MediatR pipeline and adding it as a behaviour so all we need to do is have our request inherit from ICacheable for the response to be cached.

public interface ICacheable
{
   string CacheKey { get; }
}
public class CacheBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
    private readonly ILogger<CacheBehaviour<TRequest, TResponse>> _logger;
    private readonly IDistributedCache _cache;
    private readonly IDateTimeService _dateTimeService;

    public CacheBehaviour(ILogger<CacheBehaviour<TRequest, TResponse>> logger, IDistributedCache cache, IDateTimeService dateTimeService)
    {
        _logger = logger;
        _cache = cache;
        _dateTimeService = dateTimeService;
    }

    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        if (!typeof(TRequest).IsAssignableTo(typeof(ICacheable)))
        {
            return await next();
        }

        var cacheableRequest = (ICacheable)request;
        var cacheKey = cacheableRequest.CacheKey;
        if(string.IsNullOrEmpty(cacheKey))
            return await next();

        if (_cache.TryGetValue(cacheKey, out TResponse? result))
        {
            _logger.LogInformation("Returned data from distributed cache.");
            if (result != null) return result;
        }
        else
        {
            _logger.LogInformation("No data in distributed cache.");
        }

        result = await next();

        var cacheExpiryOptions = new DistributedCacheEntryOptions
        {
            AbsoluteExpiration = _dateTimeService.UtcNow.AddMinutes(5),
            SlidingExpiration = TimeSpan.FromMinutes(2)
        };

        await _cache.SetAsync(cacheKey, result, cacheExpiryOptions);

        return result;
    }
}
public class GetRolesRequest : IRequest<List<RoleDto>>, ICacheable
{
    public string CacheKey => $"{CacheConstants.Roles}";
}

If you want to mock the cache for testing you can do so as below.

public static IDistributedCache GetDistributedMemoryCache(object expectedValue)
{
    var mockMemoryCache = new Mock<IDistributedCache>();
    var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedValue, JsonHelper.GetAllOptions()));
            
    mockMemoryCache
        .Setup(x => x.GetAsync(It.IsAny<string>(), It.IsAny<System.Threading.CancellationToken>()))
        .ReturnsAsync(bytes);
                
    return mockMemoryCache.Object;
}

0 Comments

Leave a Reply

Avatar placeholder

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