NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
NuGet · #1794 most downloaded on NuGet
FusionCache serializer based on Newtonsoft Json.NET
Last release 16 days ago
22 Sep 2026
Release timing varies
gaps range from 8 days to 5 months
Some releases are documented
notes for 12 of 47 stable releases
Nothing withdrawn
no release was ever pulled
6 years old
66 releases · first in 2020
- Update: package dependencies
Community member @sfhb24 noticed that during an Eager Refresh the L2 was not being checked.
Now, this is admittedly not a huge thing per se, because of 2 reasons:
In both these cases, the extra L2 check during an eager refresh would not be necessary.
Having said that, an L1+L2 setup without a backplane or a distributed locker may also be common, and in that case the new L2 check in the eager refresh window can in fact be helpful in reducing the amount of factory executions even more.
Long story short: I just implemented it!
See here for the issue.
If a factory fails when executed in the background during an eager refresh, FusionCache already takes care of everything and correctly releases the potentially acquired locks (memory and/or distributed), and this is good.
But community member @joaopbnogueira noticed a peculiar edge case: if the factory is not one marked with the async keyword AND it throws an exception, the memory lock is not being released properly.
Or, to better say, "was not". Because now this has been fixed.
Thanks João for spotting this.
See here for the issue.
Community member @joaopbnogueira also noticed another peculiar scenario, specifically when a MemoryCache write fails (yep, it can happen, true story).
Here's an example of it:
MemroyCache is being used as the L1SizeLimitSize specifiedSizeLimit it's mandatory for every entry to specify a Size)In this scenario a distributed lock may not have been released.
But fear no more: this does not hapen anymore!
See here for the issue.
Clear(false) + Fail-SafeCommunity member @joaopbnogueira (damn, he is on a roll!) noticed something else, that somehow went unnoticed until now.
In some cases, after a Clear(false) call, an expired entry saved with Fail-Safe enabled may still be returned by FusionCache.
This too has now been fixed.
See here for the issue.
Community member @GordeySt sent a PR with some really nice optimizations, particularly around reducing resource allocations and CPU consumption when working with metrics.
Now, even with metrics enabled, the hot path is allocation free.
That's great, thanks Gordey!
See here for the PR.
The test suite just crossed the 1600 mark:
Nice 🙂
One column per quarter.
- Update: package dependencies
Community member @joaopbnogueira spotted a problem with an edge case: when using SkipDistributedCacheWrite the distributed locker was not being properly disposed, which was unfortunate.
Now this has been fixed.
See here for the issue.
Community member @igor-henriques noticed an issues because of which when the tag marker (the special cache entry containing the RemoveByTag() timestamp) expired, it was being re-materialized with a newer timestamp: this could have led to the same results as a new RemoveByTag() call.
That was unfortunate, but it has now been fixed.
See here for the issue.
RemoveByTagBehavior.RemoveCommunity member @gpetrou asked for some help regarding a certain scenario, and during the explanation/investigation it emerged that FusionCache could have handled RemoveByTagBehavior.Remove in a slightly better way.
It was mostly an edge case, but still: now the way it is internally handled is even better than before.
See here for the issue.
Community member @dzmitry-tsarevich highlighted that user-initiated cancellations were being handled in a little-too-aggressive way from the oint of view of observability: too much background noise was being generated, which could lead to bloat.
Now this has been made better, slimmer.
See here for the issue.
Community member @Stepami noticed the lack of a specific ext method on the builder to register the distributed locker based on Redis, basically WithRedisDistributedLocker().
After accepting his PR, I noticed a couple of extra ones were also missing, and so I added them.
See here for the issue.
Also, community member @petriceko asked for a new overload of the WithOptions() ext method with better DI support: promptly, community member @vrbyjimmy made a PR to add that, which I merged.
Talk about community collaboration 🙂
See here for the issue.
- Update: package dependencies
Community member @erikatsg noticed what seemed like a strange behavior, but after a preliminary check discovered that the problem was on their side, because they were adding the same FusionCache registration to the DI container more than once.
Something like this:
services.AddFusionCache()
.WithDefaultEntryOptions(options =>
{
options.Duration = TimeSpan.FromSeconds(111);
});
// LATER...
services.AddFusionCache()
.WithDefaultEntryOptions(options =>
{
options.Duration = TimeSpan.FromSeconds(222);
});This is generally not suggested nor supported.
Luckily, FusionCache already had a series of checks for that and more strange situations, and those checks emit log records in those situations.
Unluckily though, those checks were previously performed only when asking to the DI container for an IFusionCacheProvider (to work with named caches).
Well, not anymore: now it works in any setup/scenarion, all automatically.
The current list of checks performed is:
IFusionCache registrations (e.g.: multiple services.AddFusionCache() calls)IFusionCache registrations (e.g.: multiple services.AddFusionCache(name) calls with the same name)IFusionCache registrations (e.g.: multiple services.AddFusionCache().AsKeyedService(key) calls with the same key, for either named caches or the default cache)See here for the issue.
A new check has been added in the Advisor that detecs a potentially incorrect configuration for a System.Text.Json based serializer related to value tuples.
This is not about a FusionCache bug, but about the System.Text.Json serializer's default configuration deviating from the commonly expected one (like, historically, Json.NET for example), see here for more.
Instead of forcing a specific json configuration for FusionCache, the Advisor now checks if the serializer in use is not supporting value tuples correctly and, if so, emits a warning (also configurable) in the logs to warn users about that.
This allows them to make the best decision for their specific use case.
Thanks to @sychare and @bassepeder for the hints.
Also, the check for a missing CacheKeyPrefix is now better, more limited in scope.
See here and here for the issues.
SerializationConfigIssuesLogLevel optionSee the previous item: with this new option is possible to change the desired log level, or suppress it.
DistributedLockerErrorsLogLevel optionIt's now possible to granularly configure the log level to use for distributed lockers errors, which is nice.
Sometimes the handling of OTEL parent traces was not the best, particularly around highly multithreaded scenarios where a native Activity is not being passed around correctly in the context.
This has now been fixed.
RemoveByTag(tag) operation now include the tag (duh)Normally, every operation in the cache has a cache key as the main argument.
In the case of a RemoveByTag(tag) operation though, that is not the case since the main argument is not the cache key, but the tag: strangely enough, FusionCache previously was not logging it front and center, but now it does.
This should help with tagging-related investigations and troubleshootings.
Thanks to community member @tvardero for spotting this.
See here for the issue.
Thanks to community member @DanielStout5 for spotting this: it's quite rare, but sometimes an entry in L2 may be older than the corresponding entry in L1, and before this fix that assumption did not hold sometimes.
Now an extra check is performed, to make sure that this scenario doesnot produce unwanted results.
See here for the issue.
Every single operation in FusionCache has a uniquely identifying operation id (OperationId): this becomes very useful with observability, mainly with logs.
Previously, when no logger was present, the operation id was not generated, at all: this was an optimization to avoid useless work when not actually needed.
But in a case of traces being used without a logger, the traces did not include the operation id for the reason explain above.
With this fix, the optimization is still there, but it also takes into account trace listeners, so that when either a logger or a trace listener is present the operation id is generated, otherwise the optimization is still there.
Community member @vrbyjimmy discovered a nasty issue with user-initiated cancellations when used with the distributed locker, see the issue for more details.
Than, community member @qjustfeelitp sent a PR to fix exactly this: teamwork doing its thing, amirite?
Long story short: now that specific OperationCanceledException is being specifically handled to avoid creating issues.
Thanks both!
See here for the issue.
Thanks to community member @SirPustekuchen for noticing that the MessagePack package version being normally used was since marked as vulnerable: now the reference has been updated.
And thanks to communiy membe @aki-matleb for doing the same, but for the OpenTelemetry.Api package.
See here and here for the issues.
As always, I took some time to updated the docs with the latest stuff and make them overall better.
- Update: package dependencies
Note
You should update to v2.7.2, see here: there's a small issue in v2.7.0, nothing critical really, but better to use the new version.
A new check has been added in the Advisor that detecs a potentially incorrect configuration for a System.Text.Json based serializer related to value tuples.
This is not about a FusionCache bug, but about the System.Text.Json serializer's default behavior deviating from the commonly expected one (like, historically, Json.NET for example), see here for more.
Instead of forcing different json options specifically for FusionCache, the Advisor now checks if the serializer in use is not supporting value tuples correctly and, if so, emits a warning (also configurable) in the logs to warn users about that.
This allows them to make the best decision for their specific use case.
Thanks to @sychare and @bassepeder for the hints.
Also, the check for a missing CacheKeyPrefix is now better, more limited in scope.
See here and here for the issues.
SerializationIssuesLogLevel optionSee the previous item: with this new option is possible to change the desired log level, or suppress it.
DistributedLockerErrorsLogLevel optionIt's now possible to granularly configure the log level to use for distributed lockers errors, which is nice.
Sometimes the handling of OTEL parent traces was not the best, particularly around highly multithreaded scenarios where a native Activity is not being passed around correctly in the context.
This has now been fixed.
RemoveByTag(tag) operation now include the tag (duh)Normally, every operation in the cache has a cache key as the main argument.
In the case of a RemoveByTag(tag) operation though, that is not the case since the main argument is not the cache key, but the tag: strangely enough, FusionCache previously was not logging it front and center, but now it does.
This should help with tagging-related investigations and troubleshootings.
Thanks to community member @tvardero for spotting this.
See here for the issue.
Thanks to community member @DanielStout5 for spotting this: it's quite rare, but sometimes an entry in L2 may be older than the corresponding entry in L1, and before this fix that assumption did not hold sometimes.
Now an extra check is performed, to make sure that this scenario doesnot produce unwanted results.
See here for the issue.
Every single operation in FusionCache has a uniquely identifying operation id (OperationId): this becomes very useful with observability, mainly with logs.
Previously, when no logger was present, the operation id was not generated, at all: this was an optimization to avoid useless work when not actually needed.
But in a case of traces being used without a logger, the traces did not include the operation id for the reason explain above.
With this fix, the optimization is still there, but it also takes into account trace listeners, so that when either a logger or a trace listener is present the operation id is generated, otherwise the optimization is still there.
Community member @vrbyjimmy discovered a nasty issue with user-initiated cancellations when used with the distributed locker, see the issue for more details.
Than, community member @qjustfeelitp sent a PR to fix exactly this: teamwork doing its thing, amirite?
Long story short: now that specific OperationCanceledException is being specifically handled to avoid creating issues.
Thanks both!
See here for the issue.
Thanks to community member @SirPustekuchen for noticing that the MessagePack package version being normally used was since marked as vulnerable: now the reference has been updated.
And thanks to communiy membe @aki-matleb for doing the same, but for the OpenTelemetry.Api package.
See here and here for the issues.
As always, I took some time to updated the docs with the latest stuff and make them overall better.
- Update: package dependencies
RemoveByTag()Normally, when calling RemoveByTag("my-tag"), the entries with such a tag will be gradually expired on a subsequent access.
Community member @charlesvigneault asked for the ability to instead properly remove them.
So I added a new option to allow configuring this behavior:
services.AddFusionCache()
.WithOptions(options =>
{
options.RemoveByTagBehavior = RemoveByTagBehavior.Remove;
});See here for the original issue.
RemoveByTag("*") in HybridCache adapterAfter the initial release of HybridCache in 2025, the team added support for a special case: using RemoveByTag("*") to clear the entire cache.
I didn't notice untile recently, and thanks to community user @vrbyjimmy I did that.
Or, to better say it, he did that!
He acted so quickly that a PR immediately landed with the implementation, so thanks Jakub for that!
What happens underneath is that a RemoveByTag("*") call on the adapter is detected and re-routed to a Clear() call on the underlying FusionCache instance: very simple and elegant, and I like that a lot.
See here for the original issue.
Community user @jgshowpad noticed that when using the new distributed stampede protection introduced in v2.5.0 with Eager Refresh some errors were being logged.
That was caused by the Redis-based distributed locker not handling correctly a timeout of zero (which btw is a pretty common approach to basically check for a lock already being acquired by someone else, without having to wait).
This has now been fixed.
See here for the original issue.
GenerateOperationId()Community user @Inok contributed with a nice set of low-level perf optimizations for the GenerateOperationId() internal method, which may be called quite a lot when doing observability (logging, OTEL, etc).
That's a very nice and welcome contribution, thanks Pavel!
See here for the original issue.
Community member @mumin-khan noticed that, after releasing distributed stampede support in v2.5.0, I missed the related ext method for the the DI registration.
So now I added it, and it's now possible to do this:
builder.Services.AddFusionCacheRedisDistributedLocker(options =>
{
options.Configuration = ... ;
});This has now been added, thanks Mumin!
See here for the original issue.
ConfigureAwait(false)Community user @JerinMathewJose01 noticed that on the old .NET Framework 4.7.2 and 4.8, sometimes the factory may remain stuck without completing correctly.
That was caused by a couple of missing ConfigureAwait(false) when awaiting the factory execution.
This has now been fixed.
See here for the original issue.
As always, I took some time to updated the docs with the latest stuff and make them overall better.
- Update: package dependencies
Since the very beginning FusionCache offered a solid Cache Stampede protection, as explained in the docs where it is clearly illustrated:
Such protection worked not just in the normal flow (miss -> factory -> return) but also with other more advanced features like:
With time the stampede protection got even better, and even extensible: this allowed 3rd party implementations of the core mechanism, called memory locker (IFusionCacheMemoryLocker).
All of this without removing the normal "it just works" experience since, by default, a StandardMemoryLocker is used without needing any user setup or intervention.
Cool.
But here's the thing: this protection had always been a local thing, meaning it did not span multiple nodes, in a distributed way: this meant that, if we were "unlucky", multiple factories could have run at the same time for the same cache key on different nodes.
Meaning, this:
But that was true until now: enter Distributed Cache Stampede Protection 🎉
Thanks to the introduction of the new IFusionCacheDistributedLocker (see the next point) it's now possible to coordinate factory execution accross multiple nodes, so that only one factory would run at the same time for the same cache key even on different nodes.
Meaning, this:
By providing an IFusionCacheDistributedLocker implementation during setup, FusionCache will take care of everything, we don't have to do anything else.
The setup looks like this:
services.AddFusionCache()
// SERIALIZER
.WithSerializer(
new FusionCacheSystemTextJsonSerializer()
)
// DISTRIBUTED CACHE
.WithDistributedCache(
new RedisCache(new RedisCacheOptions
{
Configuration = "localhost:6379",
})
)
// BACKPLANE
.WithBackplane(
new RedisBackplane(new RedisBackplaneOptions
{
Configuration = "localhost:6379",
})
)
// DISTRIBUTED LOCKER <-- HERE IT IS!
.WithDistributedLocker(
new RedisDistributedLocker(new RedisDistributedLockerOptions
{
Configuration = "localhost:6379",
})
);Or, even better, if we want to re-use the same connection multiplexer for better performanceand use of resources, we can do this:
var muxer = ConnectionMultiplexer.Connect("localhost:6379");
services.AddFusionCache()
// SERIALIZER
.WithSerializer(
new FusionCacheSystemTextJsonSerializer()
)
// DISTRIBUTED CACHE
.WithDistributedCache(
new RedisCache(new RedisCacheOptions
{
ConnectionMultiplexerFactory = async () => muxer,
})
)
// BACKPLANE
.WithBackplane(
new RedisBackplane(new RedisBackplaneOptions
{
ConnectionMultiplexerFactory = async () => muxer,
})
)
// DISTRIBUTED LOCKER <-- HERE IT IS!
.WithDistributedLocker(
new RedisDistributedLocker(new RedisDistributedLockerOptions
{
ConnectionMultiplexerFactory = async () => muxer,
})
);As always, the idea is that "it just works".
See here for the original issue.
As mentioned above, this is the new distributed component responsible for coordinating multiple factory executions on different nodes, all automatically.
As of now I'm providing 2 main implementations:
Of course the Redis one is the only real deal for now, meant for production use.
Other implementations in the future will be possible, by simply implementing the new IFusionCacheDistributedLocker abstraction, just like it was possible before with the IFusionCacheMemoryLocker abstraction.
So to recap:
I would say it's all pretty nice 🙂
See here for the original issue.
MemoryCacheDuration entry optionThis is seemingly small, but really important.
In a multi-node scenario with an L1+L2 setup it's important to keep the cache, as a whole, coherent.
When using a Backplane there's no need to do anything: all is taken care of, and the cache as a whole is always coherent.
But wha if we cannot or don't want to use a backplane, for... reasons?
Well, every change in the cache will leave the other L1s out-of-sync for the remaining time before their expiration, and this is not good.
This problem is known as Cache Coherence, and the backplane is what is used to SOLVE it.
But if we can't use a backplane, we should at least MITIGATE it: and we can do that by reducing the incoherency window.
And how?
Well, by simply specifying 2 different durations: one for the L1 and one for the L2.
Now, with FusionCache it has always been possible to specify a different Duration for the distributed cache, thanks to the DistributedCacheDuration option.
The problem was that, when in the scenario above (L1+L2 and no backplane), it would have been nice to be able to simply say "keep all the durations as alrady specified, and just refresh the data in the L1 from L2 every few seconds".
But with only the DistributedCacheDuration option available, the way to achieve this was counterintuitive: instead of somehow override the L1 duration, we needed to lower the normal Duration to a few seconds and specify the intended logical duration as the DistributedCacheDuration.
Not terrible, but not great.
But now, not anymore: enter MemoryCacheDuration.
We can of course go granular on a call-by-call basis, but there's something better: we can simply specify a value in the DefaultEntryOptions, and all the existing call sites will inherit this new value which will automatically override the duration only for the L1.
Done.
And, if we use Tagging we can simply do the same thing for the TagsDefaultEntryOptions, and we're done.
Something like this:
services.AddFusionCache()
.WithOptions(options =>
{
options.DefaultEntryOptions.MemoryCacheDuration = TimeSpan.FromSeconds(5);
options.TagsDefaultEntryOptions.MemoryCacheDuration = TimeSpan.FromSeconds(5);
});Oh, and the new Best Practices Advisor (see next point) can already give this advice when it detects such a scenario.
Nice 🙂
Important
It's important to say that, if you can, you should always use the backplane, as that is THE way to solve cache coherence for good without out-of-sync windows or other issues.
See here for the original issue.
Sometimes we may inadvertently fall into a scenario with:
With time FusionCache got more and more new components (like #575 ) and options (like #571 ) and this, along with the naturally dynamic nature of a flexible setup and configuration, may lead to inadvertently make the wrong decisions and fall into some gotchas.
FusionCache already had a couple of internal checks, like looking for a missing CacheKeyPrefix when using a shared L1 (which may lead to cache key collisions), and warns about them in the logs.
Now this practice has been unified & expanded, and it has a name: Best Practices Advisor.
Long story short, FusionCache now checks for common pitfalls and can give warnings and suggestions, all automatically and based on the current runtime state: no need to scrape the docs to see if the current config may lead to surprises thanks to a bad incantation of options.
I'd like to highlight that I've been careful about not trying to make it too smart for its own good: that is an easy to miss cliff that would lead to exaggerate in the implemented heuristics and checks, leading to bad results.
The checks initially implemented are:
More checks will be added in the future, but for now these are already quite useful.
Oh, one final thing: if you are thinking "great, a new piece of AI crap that will waste resources" then... nah, it's just a bunch of ifs done automatically in the background during startup. And if you want you can disable the Advisor by simply setting the new EnableBestPracticesAdvisor option to false (default is true).
See here for the original issue.
IgnoreTimeoutsWhenDebugging optionCommunity user @tvardero asked if it was possible to automatically ignore all timeouts when debugging.
That was in fact an interesting feature request, and after some investigations I decided to proceed.
Now, when setting the new IgnoreTimeoutsWhenDebugging option to true, all timeouts will be ignored, but ONLY when there is a debugger attached (via Debugger.IsAttached).
All in all this will help when debugging issues locally, without nasty timeouts hitting simply because we are inspecting a variable after a breapoint hit, which is... the whole point of debugging, right?
Thanks @tvardero for the input!
See here for the original issue.
See here for the feature design issue.
Thanks to community user @vit-svoboda I changed the logic that gets the timestamp for a new entry, generated from a factory.
Before, the timestamp was about the moment the factory ended, now it's when it started.
No big change really, but it should help in a couple of edge cases with high concurrency.
See here for the original issue.
Nothing big really, as the perf were already great: just a bunch of extra tuning in a couple of edge cases, nothing big really.
I did not have time to update the docs related to all this new stuff, but I'll do it in the next few days, pinky promise.
For now, this massive release note should be good enough.
As always, with new features come new tests to make sure that all work as intended, now and in the future (regressions, am I right?).
Now we're up to 1534 total running tests, including params combinations & friends.
I can always do more, but still: not bad.
- Update: package dependencies
StaleTags to factory execution contextCommunity 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.
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.
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.
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.
InvalidOperationException when using AlwaysOffSampler with OTELCommunity 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.
Community user @marcominerva discovered a wrong description for the WithDistributedCache() method on the builder: fixed.
Thanks @marcominerva !
See here for the original issue.
FusionCache reached 1500 tests:
That's it, that's the tweet 🙂
- Update: package dependencies
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.
InternalStrings optionsFusionCache 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:
TagCacheKeyPrefixClearRemoveTagDistributedCacheWireFormatSeparatorBackplaneWireFormatSeparatorIt 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.
FusionCacheEntryOptionsProviderCommunity 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.
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.
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:
Almost 1500, not bad.
And finally the docs, which I care a lot about.
I have updated some, like:
Clear(true) and Clear(false)Expire() worksSome of these came from always welcome questions by community members like @GeddesJ , @bebo-dot-dev , @martinkoslof and @jundayin : thanks!
- Update: package dependencies
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.
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.
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.
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.
NullFusionCacheCommunity 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.
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.
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.
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.
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.
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.
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.
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.
- Update: package dependencies
- Update: package dependencies
This version references only .NET 8 core packages, so it can be used in scenarios where only LTS packages can be referenced.
| 📣 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. |
This version is exactly the same as v2.2.0, except for 2 things:
v8 core packages (LTS)v9 core packagesThis 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.
This version references only .NET 8 core packages, so it can be used in scenarios where only LTS packages can be referenced.
| 📣 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. |
This version is exactly the same as v2.1.0, except for 2 things:
v8 core packages (LTS)v9 core packagesThis 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.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →