Thank you to our sponsors who keep this newsletter free to the reader:
🎉 JetBrains Rider is now free for non-commercial use. Great news for all hobbyists, students, content creators, and open source contributors! Now you can use Rider, a cross-platform .NET and game dev IDE, for non-commercial development for free. Download and start today!
Remember when APIs were an afterthought? We're not going back. 74% of devs surveyed in the 2024 State of the API Report are API-first, up from 66% in 2023. What else? We're getting faster. 63% of devs can produce an API within a week, up from 47% in 2023. Collaboration on APIs is still a challenge. APIs are increasingly revenue generators. Read the rest of the report for fresh insights.
Caching is essential for building fast, scalable applications.
ASP.NET Core has traditionally offered two caching options: in-memory caching and distributed caching.
Each has its trade-offs.
In-memory caching using IMemoryCache
is fast but limited to a single server.
Distributed caching with IDistributedCache
works across multiple servers using a backplane.
.NET 9 introduces HybridCache
, a new library that combines the best of both approaches.
It prevents common caching problems like cache stampede.
It also adds useful features like tag-based invalidation and better performance monitoring.
In this week's issue, I'll show you how to use HybridCache
in your applications.
What is HybridCache?
The traditional caching options in ASP.NET Core have limitations. In-memory caching is fast but limited to one server. Distributed caching works across servers but is slower.
HybridCache combines both approaches and adds important features:
- Two-level caching (L1/L2)
- L1: Fast in-memory cache
- L2: Distributed cache (Redis, SQL Server, etc.)
- Protection against cache stampede (when many requests hit an empty cache at once)
- Tag-based cache invalidation
- Configurable serialization
- Metrics and monitoring
The L1 cache runs in your application's memory. The L2 cache can be Redis, SQL Server, or any other distributed cache. You can use HybridCache with just the L1 cache if you don't need distributed caching.
Installing HybridCache
Install the Microsoft.Extensions.Caching.Hybrid
NuGet package:
Install-Package Microsoft.Extensions.Caching.Hybrid
Add HybridCache
to your services:
builder.Services.AddHybridCache(options =>
{
// Maximum size of cached items
options.MaximumPayloadBytes = 1024 * 1024 * 10; // 10MB
options.MaximumKeyLength = 512;
// Default timeouts
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(30),
LocalCacheExpiration = TimeSpan.FromMinutes(30)
};
});
For custom types, you can add your own serializer:
builder.Services.AddHybridCache()
.AddSerializer<CustomType, CustomSerializer>();
Using HybridCache
HybridCache
provides several methods to work with cached data.
The most important ones are GetOrCreateAsync
, SetAsync
, and various remove methods.
Let's see how to use each one in real-world scenarios.
Getting or Creating Cache Entries
The GetOrCreateAsync
method is your main tool for working with cached data.
It handles both cache hits and misses automatically.
If the data isn't in the cache, it calls your factory method to get the data, caches it, and returns it.
Here's an endpoint that gets product details:
app.MapGet("/products/{id}", async (
int id,
HybridCache cache,
ProductDbContext db,
CancellationToken ct) =>
{
var product = await cache.GetOrCreateAsync(
$"product-{id}",
async token =>
{
return await db.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id, token);
},
cancellationToken: ct
);
return product is null ? Results.NotFound() : Results.Ok(product);
});
In this example:
- The cache key is unique per product
- If the product is in the cache, it's returned immediately
- If not, the factory method runs to get the data
- Other concurrent requests for the same product wait for the first one to finish
Setting Cache Entries Directly
Sometimes you need to update the cache directly, like after modifying data.
The SetAsync
method handles this:
app.MapPut("/products/{id}", async (int id, Product product, HybridCache cache) =>
{
// First update the database
await UpdateProductInDatabase(product);
// Then update the cache with custom expiration
var options = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromHours(1),
LocalCacheExpiration = TimeSpan.FromMinutes(30)
};
await cache.SetAsync(
$"product-{id}",
product,
options
);
return Results.NoContent();
});
Key points about SetAsync
:
- It updates both L1 and L2 cache
- You can specify different timeouts for L1 and L2
- It overwrites any existing value for the same key
Using Cache Tags
Tags are powerful for managing groups of related cache entries. You can invalidate multiple entries at once using tags:
app.MapGet("/categories/{id}/products", async (
int id,
HybridCache cache,
ProductDbContext db,
CancellationToken ct) =>
{
var tags = [$"category-{id}", "products"];
var products = await cache.GetOrCreateAsync(
$"products-by-category-{id}",
async token =>
{
return await db.Products
.Where(p => p.CategoryId == id)
.Include(p => p.Category)
.ToListAsync(token);
},
tags: tags,
cancellationToken: ct
);
return Results.Ok(products);
});
// Endpoint to invalidate all products in a category
app.MapPost("/categories/{id}/invalidate", async (
int id,
HybridCache cache,
CancellationToken ct) =>
{
await cache.RemoveByTagAsync($"category-{id}", ct);
return Results.NoContent();
});
Tags are useful for:
- Invalidating all products in a category
- Clearing all cached data for a specific user
- Refreshing all related data when something changes
Removing Single Entries
For direct cache invalidation of specific items, use RemoveAsync
:
app.MapDelete("/products/{id}", async (int id, HybridCache cache) =>
{
// First delete from database
await DeleteProductFromDatabase(id);
// Then remove from cache
await cache.RemoveAsync($"product-{id}");
return Results.NoContent();
});
RemoveAsync
:
- Removes the item from both L1 and L2 cache
- Works immediately, no delay
- Does nothing if the key doesn't exist
- Is safe to call multiple times
Remember that HybridCache
handles all the complexity of distributed caching, serialization, and stampede protection for you.
You just need to focus on your cache keys and when to invalidate the cache.
Adding Redis as L2 Cache
To use Redis as your distributed cache:
- Install the
Microsoft.Extensions.Caching.StackExchangeRedis
NuGet package:
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis
- Configure Redis and
HybridCache
:
// Add Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "your-redis-connection-string";
});
// Add HybridCache - it will automatically use Redis as L2
builder.Services.AddHybridCache();
HybridCache
will automatically detect and use Redis as the L2 cache.
Summary
HybridCache
simplifies caching in .NET applications.
It combines fast in-memory caching with distributed caching, prevents common problems like cache stampede,
and works well in both single-server and distributed systems.
Start with the default settings and basic usage patterns - the library is designed to be simple to use while solving complex caching problems.
Thanks for reading.
And stay awesome!