Caching is one of the simplest ways to improve application performance with minimal effort.
It enhances application performance by temporarily storing data for faster access without repeatedly querying the original source.
Caching not only boosts performance but also enhances scalability and ensures data availability, even when the original data source becomes temporarily unavailable.
Until now, we’ve relied on IMemoryCache for in-memory caching and IDistributedCache for distributed caching.
With the release of .NET 9, HybridCache has been introduced, seemingly aiming to replace the older interfaces and offer functionality similar to FusionCache.
Hybrid Cache
HybridCache is a caching mechanism designed to efficiently combine memory-based caching and distributed caching, providing a unified caching solution.
It implements a two-level caching strategy to optimize data storage and retrieval.
- The first level, L1 (In-Memory), stores data locally in the application's memory, providing rapid access to frequently requested data with minimal overhead.
- The second level, L2 (Distributed), supports larger, scalable storage solutions, such as Redis. This layer ensures that data can be available across multiple instances and environments.
Although introduced alongside .NET 9, HybridCache supports multiple platforms, including:
- .NET 9
- .NET 8
- .NET Framework (4.7, 4.7.1, 4.7.2 and 4.8)
- .NET Standard 2.0
HybridCache bridges the gap between previous caching mechanisms and provides several enhancements:
- Stamped Protection: Prevents redundant parallel requests for the same data, improving efficiency under load.
- Customizable Serialization: Supports flexible serialization options to adapt your application needs.
- Tags: Enables grouping of cache entries for easier management, such as bulk invalidation by tags.
By addressing the limitations of IMemoryCache and IDistributedCache, HybridCache provides an amazing solution for modern development needs.
Getting Started with Hybrid Caching
To get started with HybridCache, you need to install the NuGet package. You can do this via the NuGet Package Manager or by running the following command in the Package Manager Console:
Install-Package Microsoft.Extensions.Caching.Hybrid
NOTE: At the time of writing this blog, the HybridCache library is still in preview mode. If you're using NuGet package manager, make sure to check the "Include prerelease" box.
To start using HybridCache, you’ll need to add it to your dependency injection setup. This can be done simply using the AddHybridCache method:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHybridCache();
However, in practice, the cache registration will typically look more like this, as it’s important to configure caching to suit your specific scenario:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHybridCache(options =>
{
options.MaximumPayloadBytes = 1024 * 1024;
options.MaximumKeyLength = 1024;
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5),
LocalCacheExpiration = TimeSpan.FromMinutes(5)
};
});
By default, the library supports string and byte[] types internally. However, as mentioned earlier, you can configure it to use other serializers such as Protobuf or XML.
Here’s an example:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromSeconds(10),
LocalCacheExpiration = TimeSpan.FromSeconds(5)
};
}).AddSerializer<SomeProtobufMessage, GoogleProtobufSerializer<SomeProtobufMessage>>();
Basic Usage
It offers a set of methods to manage cache entries and perform various operations.
Here’s an overview of its methods:
SetAsync
When you want to set a new or update an exisisting entry you can use SetAsync method.
public async Task<Guid> Handle(Command request, CancellationToken cancellationToken)
{
var product = new Product(
Guid.NewGuid(),
DateTime.UtcNow,
request.Name,
request.Description,
request.Price);
_dbContext.Products.Add(product);
await _dbContext.SaveChangesAsync(cancellationToken);
await _cache.SetAsync(
$"products-{product.Id}",
product,
cancellationToken: cancellationToken);
return product.Id;
}
It's a useful method when you want to store an object without retrieving it first.
GetOrCreateAsync
When you want to retrieve data and add data to cache if it's not found you can use GetOrCreateAsync method.
public async Task<ProductResponse?> Handle(Query request, CancellationToken cancellationToken)
{
var key = $"products-{request.Id}";
var response = await _cache.GetOrCreateAsync(
key,
async _ =>
{
return await _dbContext.Products
.FirstOrDefaultAsync(
p => p.Id == request.Id,
cancellationToken);
},
cancellationToken: cancellationToken);
return response?.Adapt<ProductResponse>();
}
I really appreciate that we now have the GetOrCreateAsync method, it's so much more convenient than before!
Before, I would usually write my own GetOrCreate method since it's quite a common use case.
RemoveAsync
When the underlying data for a cache entry changes before it expires, you can remove the entry explicitly by calling RemoveAsync with the key to the entry.
An overload lets you specify a collection of key values.
public async Task<Result> Handle(Command request, CancellationToken cancellationToken)
{
var product = await _dbContext.Products
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);
if (product is null)
{
return Result.Fail(Errors.ProductNotFound);
}
_dbContext.Products.Remove(product);
await _dbContext.SaveChangesAsync(cancellationToken);
await _cache.RemoveAsync($"products-{request.Id}", cancellationToken);
return Result.Ok();
}
By default, all methods will update both L1 and L2 caches unless specified otherwise. You can configure the cache to control the behavior of each cache level independently, allowing for more granular control over the caching strategy.
Cache Tags
One of the features that standout is definitely cache tags.
Tags can be used to group cache entries and invalidate them together.
For example you could set tags when calling GetOrCreateAsync method:
public async Task<ProductResponse?> Handle(Query request, CancellationToken cancellationToken)
{
string[] tags = ["products"];
var key = $"products-{request.Id}";
var response = await _cache.GetOrCreateAsync(
key,
async _ =>
{
return await _dbContext.Products
.FirstOrDefaultAsync(
p => p.Id == request.Id,
cancellationToken);
},
tags: tags,
cancellationToken: cancellationToken);
return response?.Adapt<ProductResponse>();
}
And later on you can remove cache entries by a tag or multiple tags using RemoveByTagAsync method:
public async Task Handle(Command request, CancellationToken cancellationToken)
{
await _dbContext.Products.ExecuteDeleteAsync(cancellationToken);
string[] tags = ["products"];
await _cache.RemoveByTagAsync(tags, cancellationToken);
}
Getting Started with Redis
To use Redis all you need to do is to add StackExchangeRedis package and configure it alongside with HybridCache like this:
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration =
builder.Configuration.GetConnectionString("RedisConnectionString");
});
builder.Services.AddHybridCache();
Once Redis IDistributedCache implementation is available from the DI container, HybridCache will use it as the secondary cache and use the serializer configured for it.
Conclusion
Incorporating caching into your application can be essential for delivering optimal performance, reducing latency and improving scalability.
With the introduction of HybridCache, Microsoft offers a powerful tool that combines the best features of in-memory and distributed caching.
Its advanced capabilities such as stamped protection, customizable serialization and tagging, make it a versatile choice for applications that demand high efficiency and scalability.
P.S. If you need caching with advanced resiliency features I highly recommend checking out FusionCache library.
If you want to check out examples I created, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!