HybridCache in ASP.NET Core - New Caching Library

HybridCache in ASP.NET Core - New Caching Library

5 min read ·

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:

  1. Install the Microsoft.Extensions.Caching.StackExchangeRedis NuGet package:
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis
  1. 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!


Whenever you're ready, there are 4 ways I can help you:

  1. (COMING SOON) REST APIs in ASP.NET Core: You will learn how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API. Join the waitlist!
  2. Pragmatic Clean Architecture: Join 3,600+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  3. Modular Monolith Architecture: Join 1,600+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  4. Patreon Community: Join a community of 1,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.
  5. Promote yourself to 60,000+ subscribers by sponsoring this newsletter.

Become a Better .NET Software Engineer

Join 60,000+ engineers who are improving their skills every Saturday morning.