Friday, August 30, 2024

ASP.NET Core 9.0: HybridCache in ASP.NET Core

With ASP.NET Core 9.0, we have access to a new Caching API: HybridCache, and it's designed to replace both IDistributedCache and IMemoryCache.

Let's go through with an example code.

I have the following code:

public interface IDataService
{
    Task<ConfigurationData> GetConfigurationData(CancellationToken cancellationToken = default);
}

public abstract class DataServiceBase : IDataService
{
    protected const string CacheKey = "configuration-cache-key";

    public abstract Task<ConfigurationData> GetConfigurationData(CancellationToken cancellationToken = default);

    protected async Task<ConfigurationData> GetConfigurationFromSource(CancellationToken cancellationToken = default)
    {
        return await Task.FromResult(new ConfigurationData
        {
            SomeConfig1 = "Some Config1",
            SomeConfig2 = "Some Config2"
        });
    }
}
First, let's see how IDistributedCache works and then let's see how HybridCache can simplify it.
public class DataServiceWithIDistributedCache(IDistributedCache distributedCache)
    : DataServiceBase
{
    public async override Task<ConfigurationData> GetConfigurationData(CancellationToken cancellationToken = default)
    {
        byte[]bytes = await distributedCache.GetAsync(CacheKey, cancellationToken)// Try to get from cache.

        // Cache hit; return the deserialized data.
        if (bytes is not null)
        {
            return JsonSerializer.Deserialize<ConfigurationData>(bytes)!;
        }

        // Cache miss; get the data from the real source and cache it.
        ConfigurationData configurationData = await GetConfigurationFromSource(cancellationToken);

        bytes = JsonSerializer.SerializeToUtf8Bytes(configurationData);

        await distributedCache.SetAsync(CacheKey, bytescancellationToken);

        return configurationData;
    }
}
I have IDistributedCache configured with Redis.
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "<ConnectionString>";
});
builder.Services.AddScoped<IDataServiceDataServiceWithIDistributedCache>();
Now here in DataServiceWithIDistributedCache, we are first checking the cache to see whether the item exists, if it is we return the item from the cache, if not we retrieve the item from the original source, cache it, and then return the item.

There are potential problems here. Say the item does not exist in the cache and more than one thread attempts to read Configuration simultaneously. In that case, multiple threads are going to cache the item.

On top of that, we had to first check whether the item exists in the cache, and if not, we need to retrieve the item from original source, and cache it. When retrieving an item from a cache, almost all the time, that's something we will have to do.

Now let's see how HybridCache works. First, we need to install a new package: Microsoft.Extensions.Caching.Hybrid (it's still pre-release as of today)
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0-preview.7.24406.2" />
public class DataServiceWithHybridCache(HybridCache hybridCache)
    : DataServiceBase
{
    public async override Task<ConfigurationData> GetConfigurationData(CancellationToken cancellationToken = default)
    {
        return await hybridCache.GetOrCreateAsync(
            CacheKey, factory: async token => await GetConfigurationFromSource(token),
            cancellationTokencancellationToken
        );
    }
}
Now we need to register HybridCache.
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "<ConnectionString>";
});

builder.Services.AddScoped<IDataServiceDataServiceWithHybridCache>();
builder.Services.AddHybridCache(options =>
{
    // TODO: customize options if required
});
So here HybridCache is created with a primary cache and a secondary cache. 
HybridCache
HybridCache by default uses MemoryCache for its primary cache, and for secondary cache, it uses any IDistributedCache implementation that is configured. Since I have Redis configured, Redis is registered as the secondary cache here.

HybridCache exposes GetOrCreateAsync with two overloads, taking a key and: 
  1. A factory method.
  2. State, and a factory method.
The method uses the key to retrieve the item from the primary cache. If it's not there (cache miss), it then checks the secondary cache (if it's configured). If it doesn't find the item there (another cache miss), it calls the factory method to get the item from the original data source. It then caches the item in both primary and secondary caches. The factory method is never called if the item is found in the primary or secondary cache (a cache hit).

The HybridCache service ensures that only one concurrent caller for a given key calls the factory method, and all other callers wait for the result of that call. The CancellationToken passed to GetOrCreateAsync represents the combined cancellation of all concurrent callers.

I love this.

Hope this helps.

More read:

Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment