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:
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);
}