Skip to content

Conversation

@renovate
Copy link
Contributor

@renovate renovate bot commented Nov 24, 2025

This PR contains the following updates:

Package Change Age Confidence
ZiggyCreatures.FusionCache 2.0.2 -> 2.4.0 age confidence
ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis 2.0.2 -> 2.4.0 age confidence
ZiggyCreatures.FusionCache.Serialization.SystemTextJson 2.0.2 -> 2.4.0 age confidence

Release Notes

ZiggyCreatures/FusionCache (ZiggyCreatures.FusionCache)

v2.4.0

🏷️ 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

🔑 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's an 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

🎯 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.1.0

🙋‍♂️ 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.


Configuration

📅 Schedule: Branch creation - "every 2nd week starting on the 2 week of the year before 4am on Monday" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about these updates again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@bitwarden-bot bitwarden-bot changed the title [deps]: Update fusioncache monorepo to 2.4.0 [PM-28720] [deps]: Update fusioncache monorepo to 2.4.0 Nov 24, 2025
@bitwarden-bot
Copy link

Internal tracking:

@renovate renovate bot changed the title [PM-28720] [deps]: Update fusioncache monorepo to 2.4.0 [deps]: Update fusioncache monorepo to 2.4.0 Nov 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants