Skip to content

v2.0.0

Latest
Compare
Choose a tag to compare
@jodydonetti jodydonetti released this 19 Jan 23:27
· 7 commits to main since this release
c8132e5

Important

This is a world's first!
FusionCache is the FIRST production-ready implementation of Microsoft HybridCache: not just the first 3rd party implementation, which it is, but the very first implementation AT ALL, including Microsoft's own implementation which is not out yet.

Read below for more.

🙋‍♂️ Updating to v2 ? Please read here.

❤️ FusionCache V2: a small personal note

For me (Jody), this feels like a monumental personal achievement. The amount of work poured into it, the sheer size of the release, all the new features like Tagging, Clear(), Microsoft HybridCache support and everything else: I honestly couldn't be prouder of it.

I hope you will all like using it, as much as I liked creating it.

Ok, end of the personal note: let's get the features rolling!

🏷️ Tagging (docs)

FusionCache now has full support for tagging!

This means we can now associate one or more tags to any cache entry and, later on, simply call RemoveByTag("my-tag") to evict all the entries that have the "my-tag" associated to them.

And yes, it works with all the other features of FusionCache like L1+L2, backplane, fail-safe, soft timeouts, eager refresh, adaptive caching and everything else.

Honestly, the end result is a thing of beauty.

Here's an example:

cache.Set("risotto_milanese", 123, tags: ["food", "yellow"]);
cache.Set("kimchi", 123, tags: ["food", "red"]);
cache.Set("trippa", 123, tags: ["food", "red"]);
cache.Set("sunflowers", 123, tags: ["painting", "yellow"]);

// REMOVE ENTRIES WITH TAG "red"
cache.RemoveByTag("red");

// NOW ONLY "risotto_milanese" and "sunflowers" ARE IN THE CACHE

// REMOVE ENTRIES WITH TAG "food"
cache.RemoveByTag("food");

// NOW ONLY "sunflowers" IS IN THE CACHE

It's really that simple.

Well, using it is simple: the behind the scenes on how to make it work, to make it work well, in a scalable and flexible way including support for all the resiliency features of FusionCache (eg: fail-safe, auto-recovery, etc), that is a completely different story. The design is a particular one, and I suggest anyone to take a look at the docs to understand how it all works.

There are also a lot performance optimizations, plus a lot of big and small tweaks.

See here for the official docs.
See here for the original issue.

🧼 Clear() (docs)

Thanks to the new Tagging support, it is now also possible for FusionCache to support a proper Clear() method, something that the community has been asking for a long time.

And this, too, works with everything else like cache key prefix, backplane notifications, auto-recovery and so on.

Here's an example:

cache.Set("foo", 1);
cache.Set("bar", 2);
cache.Set("baz", 3);

// CLEAR
cache.Clear();

// NOW THE CACHE IS EMPTY

Easy peasy.

In reality there's more: an additional bool allowFailSafe param (with a default value of true) allows us to choose between a full Clear (eg: "remove all") and a soft Clear (eg: "expire all"): in the second case all entries will be logically deleted, but still available as a fallback in case fail-safe is needed, and there's full support for both at the same time.

There are also a lot performance optimizations, so everything is incredibly fast, even with a massive amount of data.

See here for the official docs.
See here for the original issue.

Ⓜ️ Microsoft HybridCache support (docs)

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.

Now, 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.

So, as I already announced when I shared my thoughts about it, I wanted to allow FusionCache to be ALSO usable as a 3rd party HybridCache implementation.

To be clear: this does NOT mean that FusionCache is now based on the HybridCache implementation from Microsoft, but that is ALSO usable AS an implementation of the abstraction, via an adapter class.

Currently the HybridCache abstraction has been released, but the Microsoft implementation is not out yet: today, FusionCache becomes the world's first production-ready implementation of it.

So, how can we use it?

Easy, when setting up FusionCache in our Startup.cs file, we simply add .AsHybridCache():

services.AddFusionCache()
  .AsHybridCache(); // MAGIC

Now, every time we'll ask for HybridCache via DI (taken as-is from the official 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.

And this also means we'll have the power of FusionCache itself, including 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 thanks to native OpenTelemetry support and more.

Oh, and we'll still be able to get IFusionCache too all 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.

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, this also means that Cache Stampede protection (and every other feature, of course) 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)

Damn, that's good.

Of course, since the API surface area is more limited (eg: HybridCacheEntryOptions VS FusionCacheEntryOptions) we can enable and configure all of this goodness only at startup and not on a per-call basis: but still, it is a lot of power to have available for when you need/want to depend on the Microsoft abstraction.

Actually, to be more precise: the features available in both HybridCacheEntryOptions and FusionCacheEntryOptions (although with different names) have been automatically mapped and will work flawlessly: an example is using HybridCacheEntryFlags.DisableLocalCacheRead in the HybridCacheEntryOptions which becomes SkipMemoryCacheRead in FusionCacheEntryOptions, all automatically.

Want more? Ok, here's something crazy.

The Microsoft implementation (now in preview) currently have some limitations, like:

  • NO L2 OPT-OUT: if IDistributedCache is not registered in the DI container HybridCache will not have an L2, but if it's registered it will be used, forcibly. This means that if another component registers an IDistributedCache service, we'll be forced to use it in HybridCache too, even if we don't want it
  • 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 limitations are for the (current) HybridCache implementation, not for the abstraction: so can FusionCache 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), so we are not forced to use an L2 just because there's an IDistributedCache registered in the DI container or vice versa.

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 others with .AsKeyedHybridCache(...), that's it.

Boom!

See here for the official docs.
See here for the issue.

❤️ Microsoft (and Marc) and OSS
To me, this is a wonderful example of what it may look like when Microsoft and the OSS community have a constructive dialog. First and foremost many thanks to @mgravell himself for the openness, the back and forth and the time spent reading my mega-comments. Read here for more.

⚡ Better serializers

Thanks to a big effort by community user @stebet , serializers are now natively better and allocate less, thanks to some array pools/buffers magic.
Because of this, support for the external RecyclableMemoryStreamManager has been removed, since it's not necessary anymore.
Less dependencies here, too!

See here for the original PR.

🆕 New AllowStaleOnReadOnly entry option

Historically fail-safe has been used even with read-only methods like TryGet and GetOrDefault, where a "fail" cannot actually happen.

This sometimes created some confusion, and lead to people enabling fail-safe globally and sometimes getting back stale values even with read-only operations.

To allow for better and more granular control over this aspect, a new AllowStaleOnReadOnly entry option has been added, which controls the return of stale values in read-only operations.

🆕 New options to precisely skip read/write on memory/distributed

By taking an inspiration from the upcoming HybridCache from Microsoft (see above), new entry options have been added to precisely skip reading and/or writing for both the memory (L1) and the distributed (L2) levels.

Now we have 4 options with more granular control:

  • SkipMemoryCacheRead
  • SkipMemoryCacheWrite
  • SkipDistributedCacheRead
  • SkipDistributedCacheWrite

Previously we had 2 options:

  • SkipMemoryCache
  • SkipDistributedCache

Now these 2 old ones now act in this way:

  • the setter changes both the corresponding read/write ones
  • the getter returns true if both the read and the write options are set to true (eg: return SkipMemoryCacheRead && SkipMemoryCacheWrite)

Even if they work, to avoid future confusion the 2 old ones have been marked as [Obsolete].

Of course the handy ext methods like SetSkipMemoryCache() still work.

📆 More predictable Expire

Community user @waynebrantley (thanks!) made me think again about the difference between Remove() and Expire() and how to better handle the different expectations between the two.

Now the behaviour is clearer, and hopefully there will be less surprises down the road.

See here for the original issue.

🔭 Better Observability

It is now possible to include the tags of the new Tagging feature when doing observability.

There are 3 new options in FusionCacheOptions:

  • IncludeTagsInLogs
  • IncludeTagsInTraces
  • IncludeTagsInMetrics

They are pretty self-explanatory, but keep in mind that when in OTEL world we should avoid high-cardinality attributes (eg: attributes with a lot of different values) so, be careful.

Also, there are also new traces and meters for Tagging/Clear operations.

Finally, FusionCache now records more events and exceptions than before, when they happen, giving more insights to work on when doing detective work.

ℹ️ Better metadata

Community user @jarodriguez-itsoft noticed that sometimes the payload in the distributed cache was not as small as it could've been.
Because of this the internal shape of the metadata class has been changed, to better reflect the common usage patterns and save memory whenever possible.

This set of changes made it possible to get a metadata-less scenario, which in a lot of cases will lead to smaller payloads in the distributed cache, less network usage and so on.

On top of this, some fields has been changed to a different type to allocate less and consume less cpu, while at the same time better support has been added for cross-nodes entry priority when scaling horizontally.

See here for the original issue.

💥 ReThrowSerializationExceptions does not affect serialization, only deserialization

Community user @angularsen (thanks!) had an issue with serialization, and long story short the ReThrowSerializationExceptions option can now be used to ignore only deserialization exceptions, not serialization ones (since basically an error while serializing means that something must be fixed).

See here for the original issue.

♊ Auto-Clone optimized for immutable objects

By taking an inspiration from the upcoming HybridCache from Microsoft (see above), FusionCache now handles immutable objects better when using Auto-Clone: if an object is found to be immutable, it will now skip the cloning part, to get even better perfs.

🐞 Fix for soft fails in a background factory

It has been noticed by community user @Coelho04 (thanks!) that fail-safe did not activate correctly when the factory failed with ctx.Fail("Oops") and the factory was running in the background (because of a soft timeout or eager refresh).

This is now fixed.

See here for the original issue.

💀 All [Obsolete] members has been marked as errors

All the members previously marked as [Obsolete("Message")] are now marked as [Obsolete("Message", true)], meaning if you are still using them your project will not compile.

They have been obsolete for a long time now, and this should not be a problem for anyone (and, btw, the needed changes are minuscule).

📞 New events for Tagging/Clear

New events have been added for the Tagging and Clear features.

📜 Shorter auto-generated InstanceIds (saves space in logs and traces)

A minor change, but the auto-generated InstanceId for each FusionCache instance are now shorter, saving some space on logs/network.

✅ More tests.Better tests. Faster tests

Thanks to the new features, the HybridCache support and more, the tests just crossed the 1K mark (1.1K to be precise).
While at it, I made some of them faster without the need to wait so long to test complex temporal behaviours (but, still, they'll take some time).

📕 Docs

The docs have been updated with the new features, including Tagging, Clear and Microsoft HybridCache support.

⚠️ Breaking Changes

Since this is a MAJOR version change (v1 -> v2) there have been multiple binary breaking changes, and a single compile-time one.

Having said that, unless someone somehow decided to re-implement FusionCache itself, a simple package update + full recompile should do the trick, since thanks to overloads and whatnot all the existing code should work as before (and, in case that is not the case, please let me know!).

Also, as said at the top, the wire format version has changed: read more here and here.

For the full v2 update guide, please read here.