![FusionCache logo](logo-128x128.png)
# Ⓜ️ Microsoft HybridCache Support | ⚡ TL;DR (quick version) | | -------- | | FusionCache can ALSO be used as an implementation of the new `HybridCache` abstraction from Microsoft, with the added extra features of FusionCache. Oh, and it's the first production-ready implementation of HybridCache (see below). | > [!NOTE] > FusionCache is the FIRST 3rd party implementation of Microsoft HybridCache. But not just that: since Microsoft released their default implementation later, in a strange turn of events FusionCache became the world's first production-ready implementation of HybridCache AT ALL, including Microsoft's own. Quite bonkers 🤯 With .NET 9 Microsoft [introduced](https://www.youtube.com/watch?v=rjMfDUP4-eQ) their own hybrid cache, called [HybridCache](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid?view=aspnetcore-9.0). This of course sparked a lot of [questions](https://x.com/markjerz/status/1785654100375281954) about what I thought about it, and the future of FusionCache. If you like you can read [my thoughts](https://github.com/ZiggyCreatures/FusionCache/discussions/266#discussioncomment-9915972) about it, but the main take away is: > FusionCache will NOT leverage the new HybridCache, in the sense that it will not be built "on top of it", BUT it will do 2 main things: > > 1. be available ALSO as an implementation of HybridCache > 2. it may take advantage of some of the new underlying bits being created FOR it You see, the nice thing is that Microsoft introduced not just a (default) _implementation_ (released later in 2025) but also a shared _abstraction_ (released in November 2024, with .NET 9) that anyone can implement. ## 🖼️ Abstractions Ok so `HybridCache` is first and foremost an abstraction, and? This may turn the HybridCache `abstract class` into some sort of "lingua franca" for a basic set of common features for all hybrid caches in .NET. So FusionCache is available ALSO as an implementation of HybridCache, via an adapter class. Ok cool, but how? Easy peasy: ```csharp services.AddFusionCache() .AsHybridCache(); // MAGIC ``` When setting up FusionCache in our `Startup.cs` file, we simply add `.AsHybridCache()`, that's it. Now, every time we'll ask for HybridCache via DI (taken as-is from the [official Microsoft docs](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid?view=aspnetcore-9.0#the-main-getorcreateasync-overload)): ```csharp public class SomeService(HybridCache cache) { private HybridCache _cache = cache; public async Task GetSomeInfoAsync(string name, int id, CancellationToken token = default) { return await _cache.GetOrCreateAsync( $"{name}-{id}", // Unique key to the cache entry async cancel => await GetDataFromTheSourceAsync(name, id, cancel), cancellationToken: token ); } public async Task GetDataFromTheSourceAsync(string name, int id, CancellationToken token) { string someInfo = $"someinfo-{name}-{id}"; return someInfo; } } ``` we'll be using in reality FusionCache underneath _acting as_ HybridCache, all transparently. Oh, and we'll still be able to get `IFusionCache` too at the same time, so another `SomeService2` in the same app, similarly as the above example, can do this: ```csharp public class SomeService2(IFusionCache cache) { private IFusionCache _cache = cache; // ... ``` and the SAME FusionCache instance will be used for both, directly as well as via the HybridCache adapter. As FusionCache users this means we'll have 2 options available: - use `FusionCache` directly, as we did up until today - depend on the `HybridCache` shared _abstraction_ by Microsoft, but use the `FusionCache` _implementation_ (the adapter) Actually, as said, we can do them both at the same time, in the same app: if there are components that depend on the HybridCache abstraction we can use the adapter for them, and if we want more power and more control in our own code we can use FusionCache directly, all while sharing the same underlying data. Basically, we register FusionCache (eg: `.AddFusionCache()`), make it also available as HybridCache (eg: `.AsHybridCache()`), and use what we want based on our needs. Also, when using the adapter based on FusionCache, we'll have more features anyway. Yep, more features: read on. ## 🆎 Feature Comparison Let's see which features are on the table. For the Microsoft implementation, the features are: - cache stampede protection (also [in FusionCache](CacheStampede.md)) - usable as L1 only (memory) or L1+L2 (memory + distributed) (also [in FusionCache](CacheLevels.md)) - tagging (also [in FusionCache](Tagging.md)) - serialization compression (not there yet in FusionCache, but already working on it) FusionCache on the other hand has more, like: - [fail-safe](FailSafe.md) - [backplane](Backplane.md) for multi-node invalidations (⚠️ this is important, see below for more) - [soft/hard timeouts](Timeouts.md) - [adaptive caching](AdaptiveCaching.md) - [conditional refresh](ConditionalRefresh.md) - [eager refresh](EagerRefresh.md) - [auto-recovery](AutoRecovery.md) - [multiple named caches](NamedCaches.md) - [clear](Clear.md) - [advanced logging](Logging.md) - [events](Events.md) - [background distributed operations](BackgroundDistributedOperations.md) - [full OpenTelemetry support](OpenTelemetry.md) - the API is both [sync+async](CoreMethods.md) (HybridCache is async-only) So FusionCache has more features, and that's ok, but one feature currently missing from the Microsoft implementation is pretty important: > [!WARNING] > Although initially planned, the current Microsoft implementation lacks multi-node invalidations (see [here](https://github.com/dotnet/extensions/issues/5517)). This means that when we update a value in the cache in a multi-node scenario, our nodes will be out-of-sync and our cache, as a whole, becomes incoherent! Want to find out how to fix this? Keep reading. ## 📢 Multi-node invalidations As noted above, the current Microsoft implementation of HybridCache lacks support for multi-node invalidations. This can be really problematic when we need horizontal scalability (eg: a multi-nodes scenario) because it means that when we update a value on one node, all the other nodes will be out-of-sync. But wait, FusionCache has the [Backplane](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/Backplane.md) since ages to handle exactly this, so can this solve the problem? Yes, totally 🥳 By simply setting up a backplane, the HybridCache adapter will now auto-magically handle multi-nodes invalidations too, all without the need for use to do anything more. A quick example: ```c# services.AddFusionCache() // SPECIFY A SERIALIZER .WithSerializer( new FusionCacheNewtonsoftJsonSerializer() ) // SPECIFY A DISTRIBUTED CACHE .WithDistributedCache( new RedisCache(new RedisCacheOptions { Configuration = "CONNECTION STRING" }) ) // SPECIFY A BACKPLANE .WithBackplane( new RedisBackplane(new RedisBackplaneOptions { Configuration = "CONNECTION STRING" }) ) // ENABLE THE HYBRIDCACHE ADAPTER .AsHybridCache() ; ``` And voilà: without changing your existing code all will work flawlessly, even when scaling horizontally 🎉 ## 🚀 I Want Moar Being able to use FusionCache "as" HybridCache means we'll also have the power of FusionCache itself, even when using it via the `HybridCache` abstraction. This includes the resiliency of [fail-safe](FailSafe.md), the speed of [soft/hard timeouts](Timeouts.md) and [eager-refresh](EagerRefresh.md), the automatic synchronization of the [backplane](Backplane.md), the self-healing power of [auto-recovery](AutoRecovery.md), the full observability of the native [OpenTelemetry support](OpenTelemetry.md) and more. Oh (x2), and we'll be even able to read and write from **BOTH** at the **SAME TIME**, fully protected from Cache Stampede! Yup, this means that when doing `hybridCache.GetOrCreateAsync("foo", ...)` at the same time as `fusionCache.GetOrSetAsync("foo", ...)`, they both will do only ONE database call, at all, among the 2 of them. Oh (x3), and since FusionCache supports both the sync and async programming model in a unified way (while HybridCache only supports the async one), this also means that Cache Stampede protection and every other feature will work perfectly well even when calling at the same time: - `hybridCache.GetOrCreateAsync("foo", ...)` (async call from the HybridCache adapter) - `fusionCache.GetOrSet("foo", ...)` (sync call from FusionCache directly) They'll be both protected from Cache Stampede automatically, but not just separately, but also between themselves: this means that accross both the HybridCache adapter instance and the FusionCache instance, only 1 database call will be executed, total. Nice 😊 ## 🚀 I Said Moar! Ok, here's something crazy to think about. The default `HybridCache` implementation from Microsoft has currently some limitations, in particular: - **NO L2 OPT-OUT**: there's no way to control the use of `IDistributedCache` or not. If it's registered in the DI container it will be used, otherwise it will not, meaning if another component needs it, you'll be then forced to use it in HybridCache too - **SINGLE INSTANCE:** it does not support multiple named caches, there can be only one - **NO KEYED SERVICES**: since it does not support multiple caches, it means it cannot support Microsoft's own [Keyed Services](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-9.0#keyed-services) Now these are the limitation of the (current) HybridCache _implementation_, not the _abstraction_: does this mean that when using FusionCache via the HybridCache adapter, we can go above and beyond those limits? Yup 🎉 First: since the registration is the one for FusionCache, this means we have total control over what components to use (see the builder [here](DependencyInjection.md)), so we are not forced to use an L2 just because there's an `IDistributedCache` registered in the DI container. Second: thanks to the [Named Caches](NamedCaches.md) feature of FusionCache, we can register more than one, and each of them can be exposed via the adapter. But how can we _access_ them separately in our code? Easy, via Keyed Services! Instead of registering it like this: ```csharp services.AddFusionCache() .AsHybridCache(); ``` and then request it via `serviceProvider.GetRequiredService()` or via a param injection like this: ```csharp public class SomeService(HybridCache cache) { ... } ``` we can register it like this: ```csharp services.AddFusionCache() .AsKeyedHybridCache("Foo"); ``` and then request it via `serviceProvider.GetRequiredKeyedService("Foo")` or via a param injection like this: ```csharp public class SomeService([FromKeyedServices("Foo")] HybridCache cache) { ... } ``` Is it possible to do both at the same time? Of course, simply register one FusionCache with `.AsHybridCache()`, then add any additional caches with `.AsKeyedHybridCache(...)`. That's it! Boom! ## 🚳 Limitations The HybridCache API surface area is more limited: for example for each `GetOrCreateAsync()` call we can only pass a `HybridCacheEntryOptions` object instead of a `FusionCacheEntryOptions` object. Because of this, when using FusionCache via the HybridCache adapter we can configure all of this goodness only at startup, and not on a per-call basis: still, it's a lot of power to have available for when we need or want to depend on the Microsoft abstraction. But there's more: the `HybridCacheEntryOptions` type already allows for some level of flexibility, like controlling Memory/Distributed Read/Write per-call. Therefore the FusionCache adapter automatically maps all of the options to the corresponding ones in FusionCache, and will work flawlessly. As an example, using `HybridCacheEntryFlags.DisableLocalCacheRead` in the `HybridCacheEntryOptions` becomes `SkipMemoryCacheRead` in `FusionCacheEntryOptions`, again all automatically. ## 🙏 Microsoft (and Marc) and OSS To me, this can be a good example of what it may look like when Microsoft and the OSS community have a constructive dialog. First and foremost many thanks to the HybridCache lead @mgravell for the [openness](https://github.com/dotnet/aspnetcore/issues/53255#issuecomment-1941156200), the [back](https://github.com/dotnet/aspnetcore/issues/53255#issuecomment-1945153484) and [forth](https://github.com/microsoft/garnet/issues/85#issuecomment-2014683897) and the time spent reading my [mega-comments](https://github.com/dotnet/aspnetcore/issues/53255#issuecomment-1944576582). I think this can be a really good starting point, and future endeavours by Microsoft to come up with new core components already existing in the OSS space should go even beyond, and be even more collaborative: frequent meetings between the maintainers of the main OSS packages in that space, to be all aligned and have a shared vision while respecting each other's work. With that, Microsoft can provide - if it makes sense in each case - a basic default implementation but even more importantly a shared abstraction, which btw **must** be designed to allow augmentation by the OSS alternatives: in doing so Microsoft must inevitably accept strong inputs from the OSS community to do this well, and yes I know it takes time and resources, but imho it's the only way to make it work. Then it should also give visibility to the OSS alternatives (in the main docs, samples, videos, etc), and encourage all .NET users to discover and try the alternatives: in doing so they will not lose anything, and instead in turn the .NET ecosystem as a whole will thrive. In the past this has not always been the case, but the future _may_ be different. Just my 2 cents.