HybridCache in .NET is a caching mechanism introduced in .NET 9 that combines the benefits of both in-memory caching and distributed caching.It aims to provide a unified and efficient caching solution forASP.NETCore applications and other .NET projects.

1-Installation & Setup

Install from nuget:

  dotnet add package Microsoft.Extensions.Caching.Hybrid


Register the service:

builder.Services.AddHybridCache();

 

Inject and use it :

public class SomeService(HybridCache cache)
{
    private HybridCache _cache = cache;

    public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken ct = default)
    {
        return await _cache.GetOrCreateAsync(
            $"{name}-{id}", // Unique key to the cache entry
            async cancel => await GetDataFromTheSourceAsync(name, id, cancel),
            cancellationToken: ct
        );
    }

    public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken ct)
    {
        string someInfo = $"someinfo-{name}-{id}";
        return someInfo;
    }
}

 

2- How It Works (L1/L2 logic):

If an IDistributedCache implementation is registered, HybridCache will use it. Otherwise, it falls back to MemoryCache.
   ❑it automatically registers IMemoryCache by calling AddMemoryCache() internally
   ❑HybridCache automatically switches between in-memory and distributed caching based on availability and configuration.
   ❑Here's how it works:Switching Logic:

1)Cache Hit in L1 → Returns immediately.
2)Cache Miss in L1 → Checks L2 (if configured).
3)Cache Miss in Both → Calls your factory method to get the data.
4)Stores Result → Saves to both memory and distributed cache.

 

3- Thread Safety
GetOrCreateAsync , SetAsync , RemoveAsync , RefreshAsync are thread-safe by design
   ◼Only one concurrent caller per key
   ◼All other concurrent callers for the same key will wait for the result
   ◼This prevents race conditions and redundant data fetches
   ◼No need for lock or SemaphoreSlim

 

4- Key Configuration & Flags
you can force specific keys to use only local memory in HybridCache, even if a distributed cache like Redis or SQL Server is configured .and vice versa.
HybridCacheEntryOptions.Flags
   →DisableLocalCacheRead //Disables reading from the local in-process cache.
   →DisableLocalCacheWrite //Disables writing to the local in-process cache.
   →DisableLocalCache //Disables both reading from and writing to the local in-process cache.
   →DisableDistributedCacheRead //Disables reading from the secondary distributed cache.
   →DisableDistributedCacheWrite //Disables writing to the secondary distributed cache.
   →DisableDistributedCache //Disables both reading from and writing to the secondary distributed cache.
   →DisableUnderlyingData //Only fetches the value from cache; does not attempt to access the underlying data store.
   →DisableCompression //Disables compression for this payload.

 

 

5- Common Pitfalls: that HybridCache doesn't store type metadata in the key, so keys must be unique across all types.
if you have this

var cacheData=await _cache.GetOrCreateAsync<BlogPost>("MyDoc", async (ct) =>{  return await db.BlogPosts.FirstOrDefaultAsync(x => x.Id == 1);});

and in another service accidentally fetch from the cache with the same key but another type (like this code)

var cacheData = await _cache.GetOrCreateAsync("MyDoc", async (ct) =>
{
    return new MyDoc
    {
        Id = 10
    };
});

it would override the data

 

6- Common Pitfalls (2)
HybridCache stores NULL, too
if you have this

var cacheData=await _cache.GetOrCreateAsync<BlogPost>("MyDoc", async 
                               (ct) =>{  
                                      return await db.BlogPosts.FirstOrDefaultAsync(x => x.Id == 1);
                               });

but factory Methode returns null, the null value will also be cached for this key
⚠️If you don’t want null to be stored. you must check and remove key from cache Manually

 

7-Custom Serialization
It offers configurable serialization options.This means you can choose and implement different serialization methods based on your application's needs.
Default Serializers:
HybridCache provides default serializers for common data types (e.g.,string,byte[]).For other object types, a default serializer likeSystem.Text.Jsonmight be used.
Custom Serializers:
You can implement and configure custom serializers (e.g., usingProtobuf,XML, or other custom formats) to optimize performance, reduce cache size, or handle specific data structures.

builder.Services.AddHybridCache()
                   .AddSerializerFactory<MyCustomSerializerFactory>();
public class MyCustomSerializerFactory: IHybridCacheSerializerFactory
{
 public bool TryCreateSerializer<T>([NotNullWhen(true)] out IHybridCacheSerializer<T>? serializer)
 {
     try
     {
         if (typeof(IMessage).IsAssignableFrom(typeof(T))
             && typeof(IMessage<>).MakeGenericType(typeof(T)).IsAssignableFrom(typeof(T)))
         {
             serializer = (IHybridCacheSerializer<T>)Activator.CreateInstance(typeof(GoogleProtobufSerializer<>).MakeGenericType(typeof(T)))!;
             return true;
         }
     }
     catch (Exception ex)
     {
         // Unexpected; maybe manually implemented and missing .Parser property?
         // Log it and ignore the type.
         Debug.WriteLine(ex.Message);
     }
     serializer = null;
     return false;
 }
}
public class GoogleProtobufSerializer<T> : IHybridCacheSerializer<T> where T : IMessage<T>
{
	private static readonly MessageParser<T> _parser = typeof(T)
     .GetProperty("Parser", BindingFlags.Public | BindingFlags.Static)?.GetValue(null)
         as MessageParser<T> ?? throw new InvalidOperationException("Message parser not found; type may not be Google.Protobuf");

 T IHybridCacheSerializer<T>.Deserialize(ReadOnlySequence<byte> source)
     => _parser.ParseFrom(source);

 void IHybridCacheSerializer<T>.Serialize(T value, IBufferWriter<byte> target)
       => value.WriteTo(target);
}