Skip to content

Releases: ZiggyCreatures/FusionCache

v2.4.0

17 Aug 09:14
Compare
Choose a tag to compare

🏷️ Add StaleTags to factory execution context

Community user @ted-mundy noticed a tricky behavior when using Tagging with stale entries (see next point).

To solve it, I added a new StaleTags property to the factory execution context, so that now it's possible to access both the tags that are being passed to the GetOrSet() call and the existing tags of the stale entry in the cache (if any), like this:

cache.GetOrSet<string>(
  "foo",
  (ctx, token) => {
    // THE TAGS PASSED BELOW ("tag1", "tag2" and "tag3")
    ctx.Tags;
    // THE TAGS OF THE STALE ENTRY ALREADY IN THE CACHE, IF ANY
    ctx.StaleTags;
    
    return "Combo";
  },
  tags: ["tag1", "tag2", "tag3"]
);

This can be useful even in other scenarios, like applying some custom logic about what to do based on the tags already in the cache.

Nice.

🐞 Fix for tags with stale entries

As mentioned above, community user @ted-mundy noticed a tricky behavior when using Tagging with stale entries.

Thanks to the addition of the new StaleTags property, this is now solved for good.

Thanks @ted-mundy !

See here for the original issue.

Ⓜ️ Better entry options mapping with HybridCache adapter

Community user @TheSpookyElectric noticed that, when working with the HybridCache adapter, the LocalCacheExpiration was not being handled correctly in all cases.

The mapping logic has been updated to account for that, and it now works as expected.

Thanks @TheSpookyElectric !

See here for the original issue.

🐞 Fix for WithRegisteredSerializer()

Community user @Inok noticed something... strange.
The builder, when calling WithRegisteredSerializer() on it, was doing something else: acting on the distributed cache.

That was, well... dumb, that's just what it was: my bad.

Now this has been solved.

Thanks @Inok !

See here for the original issue.

🐞 Fix for InvalidOperationException when using AlwaysOffSampler with OTEL

Community user @DFSko noticed an InvalidOperationException being thrown when working with OTEL and using the AlwaysOffSampler: this had to do with setting the current Activity after certain operations, in conjunction with activities with a state not compatible for being set as the current one.

This has now been solved.

Thanks @DFSko !

See here for the original issue.

📕 Fix for an inline doc issue

Community user @marcominerva discovered a wrong description for the WithDistributedCache() method on the builder: fixed.

Thanks @marcominerva !

See here for the original issue.

✅ 1500 tests, huzza!

FusionCache reached 1500 tests:

image

That's it, that's the tweet 🙂

v2.3.0

09 Jun 08:42
Compare
Choose a tag to compare

🔑 Access the cache key in the factory context

Community user @posledam and others asked for the ability to access the cache key in the factory context, to be able to work with it while going to the database or similar, without creating a closure with the lambda.

So that's what I added, but there's a catch here: FusionCache provides automatic cache key manipulation with things like CacheKeyPrefix usually in conjunction with things like Named Caches, so it would be nice to access both the original one and the processed one.

Therefore I added both of them in the factory context, and it can be used like this:

cache.GetOrSet<string>(
  "foo",
  (ctx, token) => {
    ctx.Key; // THE (PROCESSED) CACHE KEY
    ctx.OriginalKey; // THE (ORIGINAL) CACHE KEY
  }
);

See here for the original issue, and here for the design issue.

⚙️ New InternalStrings options

FusionCache automatically handles a lot of things for us, and to do that it may need to manipulate some strings used internally like the cache key or the backplane channel name.

For example to use the CacheName to automatically separate data in a shared cache when used in conjunction with other FusionCache instances, or the set of special cache keys used with Tagging or the way wire format versioning is handled to automatically avoid errors when evolving the internal data structures.

In these cases some special characters are used as separators.

This is all good and well, but recently community user @stebet started working on a NATS version of the backplane (and that's awesome!) and he noticed that some of these special characters create issues on NATS, which has some reserved characters that have special meaning or cannot be used anyway.

Because of this I'm adding a new set of options specifically for changing the set of internal strings used by FusionCache, so that it's possible to work with systems like NATS and avoid issues.

So now we have a new InternalStrings option inside of FusionCacheOptions where we can set strings like:

  • TagCacheKeyPrefix
  • ClearRemoveTag
  • DistributedCacheWireFormatSeparator
  • BackplaneWireFormatSeparator
  • and more

It can be used like this:

var options = new FusionCacheOptions()
{
  // ...
  InternalStrings = {
    TagCacheKeyPrefix = "tag__",
    ClearRemoveTag = "clear_remove"
    // ...
  }
};

But there issue here that I can already foresee: in the future I may need to add new internal strings, and anyone who had carefully set them to something "safe" for them will start having issues after updating to the new version with the new internal strings.

Therefore I also added a new method on the internal strings class, a method that I will keep updating in case new strings will be needed of course, and that simply set them to values that uses a commonly accepted safe set of characters, meaning only alphanumeric characters + some common separators.

It can be used like this:

options.InternalStrings.SetToSafeStrings();

or, if we want to customize the couple of special chars, like this:

options.InternalStrings.SetToSafeStrings(
  separator: '-',
  specialChar: '_'
);

In this way it will be possible to keep every little detail under control and always be future proof.

See here for the original issue.

⚙️ New FusionCacheEntryOptionsProvider

Community user @gleb-osokin was looking for a way to have DefaultEntryOptions specific for cache keys, to avoid having only one global entry options and have a way to automatically use the right one based on some custom logic instead of having to specify them at every call site.

The design took some time and some back and forth since the default entry options existed since the beginning, and to get things in the right shape has been a particularly delicate effort.

But now we have a new FusionCacheEntryOptionsProvider abstract class which anyone can implement with their own custom logic, and that we can set in the FusionCacheOptions object that we pass to create a FusionCache instance.

Nothing else needs to change, and the per-key provider, if any, is now automatically considered and used.

Particular care has been put into allowing users to have their custom logic without having a ton of new allocations.

Here's the thing:

/// <summary>
/// A provider to get <see cref="FusionCacheEntryOptions"/> based on a key.
/// <br/><br/>
/// ⚠️ <strong>IMPORTANT:</strong> in your GetEntryOptions() implementation carefully set the canMutate out param to indicate if the returned object can be mutated or not.
/// </summary>
public abstract class FusionCacheEntryOptionsProvider
{
	/// <summary>
	/// Provide entry options based on a key, by either returning a new instance or a reference to an existing one (for improved performance).
	/// <br/><br/>
	/// ⚠️ <strong>IMPORTANT:</strong> carefully set the <paramref name="canMutate"/> out param to indicate if the returned object can be mutated or not.
	/// </summary>
	/// <param name="ctx">The context, containing supporting features.</param>
	/// <param name="key">The cache key.</param>
	/// <param name="canMutate">An out parameter that indicate if the returned object can be mutated.</param>
	/// <returns>The entry options.</returns>
	public abstract FusionCacheEntryOptions? GetEntryOptions(FusionCacheEntryOptionsProviderContext ctx, string key, out bool canMutate);
}

As we can see extra care has been put into the xml comments, to warn implementers about the fact that they have to pay attention to the canMutate param, which is fundamental to signal that the returned entry options can bu mutated or not (and FusionCache then will take care of the rest, eventually duplicating it if needed).

A new method has been also added to IFusionCache: historically we had the CreateEntryOptions(...), and now we also have the CreateEntryOptions(key, ...) variant to include the key in the logic.

Here is the original PR, and here the design issue.

🐞 Fix for skipped check in read-only methods

Community user @permagne noticed that read-only methods (eg: TryGet and GetOrDefault) were not considering the SkipDistributedCacheRead entry option: this, in case of a cache miss, means that every call would go to the L2, slowing things down.

This has now been solved.

See here for the original issue.

✅ Update to xUnit v3

I finally took some time to update all the tests to xUnit v3, which has been out for some time now.

On top of this I also added some more tests to cover some missing scenarios, getting the size of the test suite to:

image

Almost 1500, not bad.

📕 Docs

And finally the docs, which I care a lot about.

I have updated some, like:

  • Clear, mostly about the difference between Clear(true) and Clear(false)
  • Core Methods, mostly updating how Expire() works
  • Cache Levels, specifically adding a part about the envelope used with L2 and specify better the wire format versioning logic
  • Tagging, mostly related to some common questions about other massive operations, like searching

Some of these came from always welcome questions by community members like @GeddesJ , @bebo-dot-dev , @martinkoslof and @jundayin : thanks!

v2.2.0

18 Apr 22:44
Compare
Choose a tag to compare

🎯 Changes in multi-targeting

Some time ago I started enabling multi-targeting on FusionCache: I didn't do that because I needed to special case some parts of the code, but just because I wanted to reduce the amount of explicit dependencies for certain TFMs after a request from the community.

But recently community user @nick-randal noticed in #416 some potential issues: long story short, from now on FusionCache will have explicit targeting only for currently supported TFMs (which today means no more .NET 3.1, .NET 6 or .NET 7) and for them it will have the minimum set of explicit dependencies.

But wait: does this mean that those older versions of .NET wil not be able to use FusionCache anymore?

Absolutely not: since FusionCache targets .NET Standard 2.0, this means that ANY version of .NET compatible with .NET Standard 2.0 (meaning: all versions) will still be able to use FusionCache, just without an explicit "support statement", since those versions are anyway not supported anymore, not even by Microsoft itself.

See here and here for the original issues.

🚀 Make the AOT support official

FusionCache has been AOT compatible for a long time, which is already good.

I just need to make that more "official" by declaring it in the csproj, enabling analyzers, create a test console app and, in general, do everything that's needed. And that's what I did.

Thanks to community user @digital88 for pointing that out.

See here and here for the original issues.

🔀 Expose the current distributed cache, if any

Currently, given a FusionCache instance, it's only possible to know if there is a distributed cache level (L2) via the bool HasDistributedCache { get; } property, not which one it is.

Community user @angularsen asked to expose it in #443 .

Historically I've been hesitant to expose internals, but at this point I think I can let this one go.

So now there's a new IDistributedCache? DistributedCache { get; } property that expose the IDistributedCache instance being used, if any.

See here and here for the original issues.

Warning

This is technically a breaking change, but since nobody has custom IFusionCache implementations and I updated both (FusionCache and NullFusionCache), I think this is fine.

📢 Expose the current backplane, if any

Same as above, but for the backplane.

So now there's a new a new IFusionCacheBackplane? Backplane { get; } property that expose the IFusionCacheBackplane instance being used, if any.

See here and here for the original issues.

Warning

This is technically a breaking change, but since nobody has custom IFusionCache implementations and I updated both (FusionCache and NullFusionCache), I think this is fine.

😶 Add DI support for NullFusionCache

Community user @eskild-th noted it was not possible to specify to use a NullFusionCache implementation via DI.
And now it is.

See here and here for the original issues.

🧼 Better perf for Clear(true)

Since I added Clear() support in v2, it has become one of the favourite features by the community (including, of course, Tagging).

Recently community user @ctyar noted something seemingly strange, which I then clarified, so all was good.

But this sparked an optimization idea so, even though the implementation for Clear() has been highly optimized since day 1, now it is even more in cases when Raw Clear is possible (eg: L1 only, no shared).

This, in reality, translates to better Clear checks,which means better perf for any method call in the mentioned scenario (eg: GetOrSet, TryGet, etc), not just when calling Clear() itself: yeah 🥳

See here for the issue that sparked the idea for the optimization.

📢 More async backplane

Community user @pil0t suggested to look into some backplane code, and proposed some changes, which I merged and then added some others on top of them.
The result is that now the backplane should work better, in a more async way, anytime possible: this allows for even less thread blocking than before.

See here for the original issue.

📜 Better logging

Community user @gmij pointed out that there was not much use of the INFO logging level, and that most log entries were at the DEBUG level: that is a good thing to point out, so now all the "entry/exit points" are marked at INFO level.
This means there's more differentiation between the different levels used, which is better to get more visibility into FusionCache internals but without immediately getting too much visibility.

On a different note, but still about logging, distributed cache errors related to internal FusionCache operations now log with a WARNING level instead of an ERROR level: this should avoid triggering some alarms in observability scenarios where that is configured.

See here and here for the original issues.

🔓 New memory locker based on the 3rd party AsyncKeyedLock library

Community user @MarkCiliaVincenti asked in #134 (yup, quite some time ago 😅) to switch FusionCache internal locking mechanism to his own library, AsyncKeyedLock.

Now, I don't want to have a direct dependency to something external for something so core, but since v0.26.0 there's a new IFusionCacheMemoryLocker abstraction which allows the creation of 3rd party implementations, so I decided to give it a try and added support for it.

How good is it? How does it compare to the standard one included in FusionCache? It depends on your own scenario, so the best thing is to try it out, measure everything, and see for yourself.

See here for the original issue.

🐞Fix for Eager Refresh in high concurrency scenarios

Community user @HannaHromenko noted that sometimes, in highly concurrent scenarios, a null reference exception was being thrown, who knows why.

Well damn, I knew why, and that is now fixed.

See here for the original issue.

🐞 Small fix when jittering + fail-safe

It has been noted by @fabianmenges that, when using both jittering and fail-safe, something unexpected could happen: now this has been fixed.

See here for the original issue.

🐞 Small fix when setting a value with AutoClone

Community user @nlconor noted that when using AutoClone the serialization was being done lazily, and this fact may create problems when putting something in the the cache, keeping a reference to it, then change it.
Now this has been fixed.

See here for the original issue.

v2.0.2

18 Apr 23:48
Compare
Choose a tag to compare
📣 LTS-ONLY RELEASE
This version references only .NET 8 core packages, so it can be used in scenarios where only LTS packages can be referenced.

A Special Version

This version is exactly the same as v2.2.0, except for 2 things:

  • it ONLY references v8 core packages (LTS)
  • it does NOT contain the HybridCache integration, since the needed HybridCache abstractions are part of v9 core packages

This has been done to support users that cannot reference STS core packages.

See here for the original issue and more details, like, a lot more details.

v2.2.0-preview-1

06 Apr 17:08
Compare
Choose a tag to compare
v2.2.0-preview-1 Pre-release
Pre-release

🎯 Changes in multi-targeting

Some time ago I started enabling multi-targeting on FusionCache: I didn't do that because I needed to special case some parts of the code, but just because I wanted to reduce the amount of explicit dependencies for certain TFMs fater after a request from the community.

But recently community user @nick-randal noticed in #416 some potential issues: long story short, from now on FusionCache will have explicit targeting only for currently supported TFMs (which today means no more .NET 3.1, .NET 6 or .NET 7) and for them it will have the minimum set of explicit dependencies.

But wait: does this mean that those older versions of .NET wil not be able to use FusionCache anymore?

Absolutely not: since FusionCache targets .NET Standard 2.0, this means that ANY version of .NET compatible with .NET Standard 2.0 (meaning: all versions) will still be able to use FusionCache, just without an explicit "support statement", since those versions are anyway not supported anymore, not even by Microsoft itself.

See here and here for the original issues.

🚀 Make the AOT support official

FusionCache has been AOT compatible for a long time, which is already good.

I just need to make that more "official" by declaring it in the csproj, enabling analyzers, create a test console app and, in general, do everything that's needed. And that's what I did.

Thanks to community user @digital88 for pointing that out.

See here and here for the original issues.

🔀 Expose the current distributed cache, if any

Currently, given a FusionCache instance, it's only possible to know if there is a distributed cache level (L2) via the bool HasDistributedCache { get; } property, not which one it is.

Community user @angularsen asked to expose it in #443 .

Historically I've been hesitant to expose internals, but at this point I think I can let this one go.

So now there's a new IDistributedCache? DistributedCache { get; } property that expose the IDistributedCache instance being used, if any.

See here and here for the original issues.

Warning

This is technically a breaking change, but since nobody has custom IFusionCache implementations and I updated both (FusionCache and NullFusionCache), I think this is fine.

📢 Expose the current backplane, if any

Same as above, but for the backplane.

So now there's a new a new IFusionCacheBackplane? Backplane { get; } property that expose the IFusionCacheBackplane instance being used, if any.

See here and here for the original issues.

Warning

This is technically a breaking change, but since nobody has custom IFusionCache implementations and I updated both (FusionCache and NullFusionCache), I think this is fine.

See here for the original issue.

😶 Add DI support for NullFusionCache

Community user @eskild-th noted it was not possible to specify to use a NullFusionCache implementation via DI.
And now it is.

See here and here for the original issues.

🧼 Better perf for Clear(true)

Since I added Clear() support in v2, it has become one of the favourite features by the community (including, of course, Tagging).

Recently community user @ctyar noted something seemingly strange, which I then clarified, so all was good.

But this sparked an optimization idea so, even though the implementation for Clear() has been highly optimized since day 1, now it is even more in cases when Raw Clear is possible (eg: L1 only, no shared).

This, in reality, translates to better Clear checks,which means better perf for any method call in the mentioned scenario (eg: GetOrSet, TryGet, etc), not just when calling Clear() itself: yeah 🥳

See here for the issue that sparked the idea for the optimization.

📢 More async backplane

Community user @pil0t suggested to look into some backplane code, and proposed some changes, which I merged and then added some others on top of them.
The result is that now the backplane should work better, in a more async way, anytime possible: this allows for even less thread blocking than before.

See here for the original issue.

📜 Better logging

Community user @gmij pointed out that there was not much use of the INFO logging level, and that most log entries were at the DEBUG level: that is a good thing to point out, so now all the "entry/exit points" are marked at INFO level.
This means there's more differentiation between the different levels used, which is better to get more visibility into FusionCache internals but without immediately getting too much visibility.

On a different note, but still about logging, distributed cache errors related to internal FusionCache operations now log with a WARNING level instead of an ERROR level: this should avoid triggering some alarms in observability scenarios where that is configured.

See here and here for the original issues.

🔓 New memory locker based on the 3rd party AsyncKeyedLock library

Community user @MarkCiliaVincenti asked in #134 (yup, quite some time ago 😅) to switch FusionCache internal locking mechanism to his own library, AsyncKeyedLock.

Now, I don't want to have a direct dependency to something external for something so core, but since v0.26.0 there's a new IFusionCacheMemoryLocker abstraction which allows the creation of 3rd party implementations, so I decided to give it a try and added support for it.

How good is it? How does it compare to the standard one included in FusionCache? It depends on your own scenario, so the best thing is to try it out, measure everything, and see for yourself.

See here for the original issue.

🐞Fix for Eager Refresh in high concurrency scenarios

Community user @HannaHromenko noted that sometimes, in highly concurrent scenarios, a null reference exception was being thrown, who knows why.

Well damn, I knew why, and that is now fixed.

See here for the original issue.

🐞 Small fix when jittering + fail-safe

It has been noted by @fabianmenges that, when using both jittering and fail-safe, something unexpected could happen: now this has been fixed.

See here for the original issue.

v2.0.1

23 Feb 20:25
Compare
Choose a tag to compare
📣 LTS-ONLY RELEASE
This version references only .NET 8 core packages, so it can be used in scenarios where only LTS packages can be referenced.

A Special Version

This version is exactly the same as v2.1.0, except for 2 things:

  • it ONLY references v8 core packages (LTS)
  • it does NOT contain the HybridCache integration, since the needed HybridCache abstractions are part of v9 core packages

This has been done to support users that cannot reference STS core packages.

See here for the original issue and more details, like, a lot more details.

v2.1.0

03 Feb 01:09
Compare
Choose a tag to compare
🙋‍♂️ Updating to v2 ? Please read here.

🔌 Integrate All The Things!

Now that v2 isfinally here and with full Tagging support, it's time to integrate all the things 🥳

The first 2 can be found below:

  • OutputCache
  • EF 2nd Level Cache

🚀 Output Cache, FusionCache style

The first one on my list is Output Cache, and the nice thing about the way it has been designed in ASP.NET is that the only thing that is needed to make a custom version is an implementation of IOutputCacheStore.

And so I did, and thanks to native Tagging in FusionCache the whole implementation is a thing of beauty with just 1 line per method: behold.

Btw while I was working on this, community user @Fabman08 asked for the same thing, talk about good timing!

Anyway, why is all of this useful?

Because now, when using OutputCache, we'll not be limited by a simple memory cache anymore, and can instead have the power of all the features of FusionCache like fail-safe, L1+L2, backplane support and more: imagine having the performance of a memory cache (L1) but with the availability and database savings of a distributed cache (L2) including instant synchronization of the backplane.

If you ask me, it's awesome.

Ok so, how can we set it up?

Easy:

// FUSION CACHE
services.AddFusionCache();

// FUSION OUTPUT CACHE
services.AddFusionOutputCache();

// OUTPUT CACHE (STANDARD SETUP)
services.AddOutputCache(options =>
{
  options.AddPolicy("Expire2", builder =>
    builder.Expire(TimeSpan.FromSeconds(2))
  );
  options.AddPolicy("Expire5", builder =>
    builder.Expire(TimeSpan.FromSeconds(5))
  );
});

When using the normal OutputCache (with a memory-only cache store) we need to:

  • setup OutputCache (settings, profiles, etc)

With the FusionCache-based version we just need 2 extra steps, before the common one:

  • 🆕 setup a FusionCache instance
  • 🆕 setup the FusionCache-based OutputCache
  • setup OutputCache normally (settings, profiles, etc)

One thing to note is that, even though it's possible to use the default FusionCache instance like in the example above, it's usually better to have a separate named cache with a specific configuration for OutputCache: this can be useful both to avoid cache key collisions (even though it is already quite hard to have them because of the standard key structure in OutputCache itself) and to have different L1/L2/backplane configurations.

How? Easy:

// FUSION CACHE (WITH CUSTOM NAME, L2, BACKPLANE, DEFAULT ENTRY OPTIONS)
services.AddFusionCache("MyOutputCache")
  .WithDefaultEntryOptions(options =>
  {
    options.IsFailSafeEnabled = true;
  })
  .WithSerializer(new FusionCacheProtoBufNetSerializer())
  .WithDistributedCache(new RedisCache(new RedisCacheOptions
  {
    Configuration = "..."
  }))
  .WithBackplane(new RedisBackplane(new RedisBackplaneOptions
  {
    Configuration = "..."
  }));

// FUSION OUTPUT CACHE
services.AddFusionOutputCache(options =>
{
  // WHICH NAMED CACHE TO USE
  options.CacheName = "MyOutputCache";
});

// OUTPUT CACHE (STANDARD SETUP)
services.AddOutputCache(options =>
{
  options.AddPolicy("Expire2", builder =>
    builder.Expire(TimeSpan.FromSeconds(2))
  );
  options.AddPolicy("Expire5", builder =>
    builder.Expire(TimeSpan.FromSeconds(5))
  );
});

Another important aspect is to be able to use a different serializers.

Wait, but why a different serializer?

Frequently it's common to use text-based serialziers (eg: JSON-based) for our entities and objects in the cache, and that is totally fine.

But OutputCache deals with byte[] (containing the entire http response with headers, body, etc) and by using a text-based serializer we are not getting the best performance for our bucks.

So, my suggestion is to pick a natively binary serializer like protobuf-net, MessagePack or MemoryPack (the available ones can be found here): in this way the payload in L2 will be as small as possible, and performance will be top notch.

Awesome.

🚀 EF 2nd Level Cache

The other one is an interesting project by @VahidN called EFCoreSecondLevelCacheInterceptor, which proposes itself as a transparent 2nd level cache for EFCore.

The nice thing for this is that I did... nothing at all 😅

One community user @kooshan asked for it in their repo some time ago, more recently another user @bbehrens let me know about it and, before I was able to do anything, the maintainer worked on it, released the new v5 version with pluggable multi-provider support and... tada 🎉

If interested, I suggest using at least v5.1 since it's the first that depends on the final FusionCache v2 bits.

📢 More async-y Backplane (docs)

The backplane has always been async at its core, meaning the messages sent and received.

A couple of smaller parts though were not as async-aware as ideally wanted, and now this is fixed: both the initial subscription and the ending unsubscription are now available in a fully async variant.

🐞 Fix edge case bug with parallel init (Protobuf-net)

Comunity user @ilioan noticed (thanks!) a small regression in FusionCache v2 related to a particular edge case: highly parallel initializations.
In that case there was a missing check in a classic double-checked-lock, such that in some cases it resulted in a missing model registration.
Now this has been fixed.

See here for the original issue.

📜 Better Logging

Community user @sebbarg noticed (thanks!) that when filtering log messages for the Debug level, the output was not totally consistent: the general method was being logged (eg: calling RemoveAsync), and the L1 operation was being logged too, but the L2 operation was nowhere to be found.
The issue? L2 operations were using the Trace log level 🤦.

Anyway now this has been fixed, and log messages are more consistent.

See here for the original issue.

🧬 Diagrams (docs)

Sometimes it's nice to be able to visualize the internal flow of a system, even more so for such a complex beast as an hybrid cache like FusionCache.

So, diagrams!

FusionCache Diagrams

✅ Better Tests

As with any new release I made the tests better, this time adding a couple of additional scenarios covered.

I also reorganized all the tests in a better way, by splitting the sync and async ones in separated files thanks to partial classes, so that they are now nicer to work with and easier to keep aligned.

📕 Better Docs (and diagrams!)

I've added some more docs for the latest stuff, and fixed some typos, no big deal.

v2.0.0

19 Jan 23:27
c8132e5
Compare
Choose a tag to compare

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 limit...

Read more

v2.0.0-preview-4

01 Jan 20:30
Compare
Choose a tag to compare
v2.0.0-preview-4 Pre-release
Pre-release

Important

This is most probably the LAST PREVIEW of FusionCache V2, before going GA.
All help is more than welcome since the main feature, Tagging, is an uber complex beast.

Warning

Because of the MAJOR version change, for now I decided to bump the wire format identifier: read more here and here.

Ⓜ️ Support for Microsoft's new HybridCache, without the extra package

Thanks to a suggestion by community member @pwelter34 the ZiggyCreatures.FusionCache.MicrosoftHybridCache extra package was not actually needed.

Because of this, 2 things happened:

  1. the adapter class is now directly in the main FusionCache package
  2. the extra package has been updated, made empty, and will be marked as deprecated

Less code, less packages and less dependencies to deal with, yeah 🥳

See here for the issue.

🆕 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.

ℹ️ 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 scenario 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.

⚡ 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.


⬇️ Stuff from preview-3 ⬇️


Ⓜ️ Native support for Microsoft's new HybridCache

As already announced when I shared my thoughts on the new Microsoft HybridCache some time ago, I wanted to allow FusionCache to be also usable as a 3rd party HybridCache implementation.

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 included in a new Nuget package.

So, how can we use it?

Easy peasy, we just add the new package:

dotnet add package ZiggyCreatures.FusionCache.MicrosoftHybridCache --version "2.0.0-preview-3"

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

services.AddFusionCache()
  .WithDefaultEntryOptions(options =>
  {
    options.Duration = TimeSpan.FromSeconds(10);
    options.IsFailSafeEnabled = true;
  })
  .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
  • fusionCache.GetOrSet("foo", ...) // SYNC

Damn if it feels 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.

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.

🏷️ Better Tagging

The tagging feature is now even better, way more optimized, and working in all scenarios.
Some bugs have been fixed too, the main being that this (specifying tags in the factory):

var foo = await cache.GetOrSetAsync<int>("foo", async (ctx, _) =>
{
	ctx.Tags = ["x", "y", "z"];
	return 123;
});

worked as intended, while this (specifying tags directly in the call):

var foo = await cache.GetOrSetAsync<int>("foo", async _ => 123, tags: ["x", "y", "z"]);

was not, but now this is fixed.

Tagging now works automatically better even without a backplane (even though it is adviced to use one): of course without it a little out-of-sync delay is to be expected.

See here for the original issue.

🧼 Better Clear

The new Clear feature has been improved too, and it now allows us to choose between a full Clear (eg: "remove all") and a soft Clear (eg: "expire all"), with full support for both via a simple bool param with sensible defaults.

The performance has been drastically improved, too, making it incredibly faster.

See here for the original issue.

🐞 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 i...

Read more

v2.0.0-preview-3

09 Dec 23:43
Compare
Choose a tag to compare
v2.0.0-preview-3 Pre-release
Pre-release

Important

This is a PREVIEW of a very big and important milestone for FusionCache.
Although all is already in a very good shape, this is still a PREVIEW version.
All help is more than welcome since the main feature, Tagging, is an uber complex beast.

Warning

Because of the MAJOR version change, for now I decided to bump the wire format identifier: read more here and here.

Ⓜ️ Native support for Microsoft's new HybridCache

As already announced when I shared my thoughts on the new Microsoft HybridCache some time ago, I wanted to allow FusionCache to be also usable as a 3rd party HybridCache implementation.

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 included in a new Nuget package.

So, how can we use it?

Easy peasy, we just add the new package:

dotnet add package ZiggyCreatures.FusionCache.MicrosoftHybridCache --version "2.0.0-preview-3"

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

services.AddFusionCache()
  .WithDefaultEntryOptions(options =>
  {
    options.Duration = TimeSpan.FromSeconds(10);
    options.IsFailSafeEnabled = true;
  })
  .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
  • fusionCache.GetOrSet("foo", ...) // SYNC

Damn if it feels 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.

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.

🏷️ Better Tagging

The tagging feature is now even better, way more optimized, and working in all scenarios.
Some bugs have been fixed too, the main being that this (specifying tags in the factory):

var foo = await cache.GetOrSetAsync<int>("foo", async (ctx, _) =>
{
	ctx.Tags = ["x", "y", "z"];
	return 123;
});

worked as intended, while this (specifying tags directly in the call):

var foo = await cache.GetOrSetAsync<int>("foo", async _ => 123, tags: ["x", "y", "z"]);

was not, but now this is fixed.

Tagging now works automatically better even without a backplane (even though it is adviced to use one): of course without it a little out-of-sync delay is to be expected.

See here for the original issue.

🧼 Better Clear

The new Clear feature has been improved too, and it now allows us to choose between a full Clear (eg: "remove all") and a soft Clear (eg: "expire all"), with full support for both via a simple bool param with sensible defaults.

The performance has been drastically improved, too, making it incredibly faster.

See here for the original issue.

🐞 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.

📆 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.

💥 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.

📞 New events for Tagging/Clear

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

🔭 Better Observability for Tagging/Clear

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

There are 3 new 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.

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

📜 Shorter auto-generated InstanceId for each cache is now shorter (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.

💀 All [Obsolete] members has been marked as errors

All the members previously marked as `[O...

Read more