⚡ 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 HybridCache from Microsoft. But not just that: in a strange turn of events, since at the time of this writing (Jan 2025) Microsoft has not yet released their default implementation, FusionCache is the FIRST production-ready implementation of HybridCache AT ALL, including the one by Microsoft itself. Quite bonkers 😬
With .NET 9 Microsoft introduced their own hybrid cache, called HybridCache.
This of course sparked a lot of questions about what I thought about it, and the future of FusionCache.
If you like you can read my thoughts 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:
- be available ALSO as an implementation of HybridCache
- 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 (which as of this writing, Jan 2025, has not been released yet) but also a shared abstraction that anyone can implement.
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:
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):
public class SomeService(HybridCache cache)
{
private HybridCache _cache = cache;
public async Task<string> 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<string> 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:
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 theFusionCache
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 the need.
Also, when using the adapter based on FusionCache, we'll have more features anyway.
Yep, more features: read on.
Important
Again, to be clear, this does NOT mean that FusionCache will now be based ON HybridCache
from Microsoft, but that it will ALSO be available AS an implementation of it, via an adapter class.
Let's see which features are on the table.
For the Microsoft implementation, the features will be:
- cache stampede protection (also in FusionCache)
- usable as L1 only (memory) or L1+L2 (memory + distributed) (also in FusionCache)
- multi-node notifications (also in FusionCache)
- tagging (also in FusionCache)
- serialization compression (not there yet, but already working on it)
FusionCache on the other hand has more, like:
- fail-safe
- soft/hard timeouts
- adaptive caching
- conditional refresh
- eager refresh
- auto-recovery
- multiple named caches
- clear
- advanced logging
- events
- background distributed operations
- full OpenTelemetry support
- the API is both sync+async (HybridCache is async-only)
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, the speed of soft/hard timeouts and eager-refresh, the automatic synchronization of the backplane, the self-healing power of auto-recovery, the full observability of the native OpenTelemetry support 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 😬
Ok, here's something crazy to think about.
The HybridCache
implementation from Microsoft currently available (it's in preview, the GA is not out yet) has 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
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 beyound 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), 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 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:
services.AddFusionCache()
.AsHybridCache();
and then request it via serviceProvider.GetRequiredService<HybridCache>()
or via a param injection like this:
public class SomeService(HybridCache cache) {
...
}
we can register it like this:
services.AddFusionCache()
.AsKeyedHybridCache("Foo");
and then request it via serviceProvider.GetRequiredKeyedService<HybridCache>("Foo")
or via a param injection like this:
public class SomeService([FromKeyedServices("Foo")] HybridCache cache) {
...
}
And is it possible to do both at the same time? Of course, simply register one FusionCache with .AsHybridCache()
and or more with .AsKeyedHybridCache(...)
, that's it.
Boom!
The HybridCache API surface area is more limited: for example for each GetOrCreateAsync()
call we can 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.
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, the back and forth and the time spent reading my mega-comments.
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.