ZiggyCreatures/FusionCache (ZiggyCreatures.FusionCache)
🏷️ AddStaleTags 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 newStaleTags property to the factory execution context, so that now it's possible to access both the tags that are being passed to theGetOrSet() 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 ANYctx.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 newStaleTags property, this is now solved for good.
Thanks@ted-mundy !
Seehere for the original issue.
Ⓜ️ Better entry options mapping with HybridCache adapter
Community user@TheSpookyElectric noticed that, when working with theHybridCache adapter, theLocalCacheExpiration 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 !
Seehere for the original issue.
🐞 Fix forWithRegisteredSerializer()
Community user@Inok noticed something... strange.
Thebuilder, when callingWithRegisteredSerializer() 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 !
Seehere for the original issue.
🐞 Fix forInvalidOperationException when usingAlwaysOffSampler with OTEL
Community user@DFSko noticed anInvalidOperationException being thrown when working with OTEL and using theAlwaysOffSampler: this had to do with setting the currentActivity 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 !
Seehere for the original issue.
📕 Fix for an inline doc issue
Community user@marcominerva discovered a wrong description for theWithDistributedCache() method on the builder: fixed.
Thanks@marcominerva !
Seehere for the original issue.
✅ 1500 tests, huzza!
FusionCache reached 1500 tests:

That's it, that's the tweet 🙂
🔑 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 likeCacheKeyPrefix usually in conjunction with things like Named Caches, so it would be nice to access both theoriginal one and theprocessed 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 KEYctx.OriginalKey;// THE (ORIGINAL) CACHE KEY});Seehere for the original issue, andhere for the design issue.
⚙️ NewInternalStrings 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 theCacheName to automatically separate data in a shared cache when used in conjunction with other FusionCache instances, or the set of special cache keys used withTagging or the waywire 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 somereserved 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 newInternalStrings option inside ofFusionCacheOptions where we can set strings like:
TagCacheKeyPrefixClearRemoveTagDistributedCacheWireFormatSeparatorBackplaneWireFormatSeparator- and more
It can be used like this:
varoptions=newFusionCacheOptions(){// ...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.
Seehere for the original issue.
⚙️ NewFusionCacheEntryOptionsProvider
Community user@gleb-osokin was looking for a way to haveDefaultEntryOptions 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 newFusionCacheEntryOptionsProvider abstract class which anyone can implement with their own custom logic, and that we can set in theFusionCacheOptions 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>publicabstractclassFusionCacheEntryOptionsProvider{/// <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>publicabstractFusionCacheEntryOptions?GetEntryOptions(FusionCacheEntryOptionsProviderContextctx,stringkey,outboolcanMutate);}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 thecanMutate 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 toIFusionCache: historically we had theCreateEntryOptions(...), and now we also have theCreateEntryOptions(key, ...) variant to include the key in the logic.
Here is theoriginal PR, andhere the design issue.
🐞 Fix for skipped check in read-only methods
Community user@permagne noticed that read-only methods (eg:TryGet andGetOrDefault) were not considering theSkipDistributedCacheRead 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.
Seehere 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:

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) andClear(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!
🎯 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.
Seehere andhere 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.
Seehere andhere for the original issues.
🔀 Expose the current distributed cache, if any
Currently, given aFusionCache instance, it's only possible to know if there is a distributed cache level (L2) via thebool 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 newIDistributedCache? DistributedCache { get; } property that expose theIDistributedCache instance being used, if any.
Seehere andhere for the original issues.
[!WARNING]
This istechnically a breaking change, but since nobody has customIFusionCache implementations and I updated both (FusionCache andNullFusionCache), 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 newIFusionCacheBackplane? Backplane { get; } property that expose theIFusionCacheBackplane instance being used, if any.
Seehere andhere for the original issues.
[!WARNING]
This istechnically a breaking change, but since nobody has customIFusionCache implementations and I updated both (FusionCache andNullFusionCache), I think this is fine.
😶 Add DI support forNullFusionCache
Community user@eskild-th noted it was not possible to specify to use aNullFusionCache implementation via DI.
And now it is.
Seehere andhere for the original issues.
🧼 Better perf forClear(true)
Since I addedClear() support in v2, it has become one of the favourite features by the community (including, of course, Tagging).
Recently community user@ctyar noted something seeminglystrange, which I then clarified, so all was good.
But this sparked an optimization idea so, even though the implementation forClear() has been highly optimized since day 1, now it is even more in cases whenRaw Clear is possible (eg: L1 only, no shared).
This, in reality, translates to better Clear checks,which means better perf forany method call in the mentioned scenario (eg:GetOrSet,TryGet, etc), not just when callingClear() itself: yeah 🥳
Seehere 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.
Seehere 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 gettingtoo 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.
Seehere andhere 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 adirect dependency to something external for something so core, but sincev0.26.0 there's a newIFusionCacheMemoryLocker 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.
Seehere 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.
Seehere 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.
Seehere 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.
Seehere for the original issue.
🔌 Integrate All The Things!
Now that v2 isfinally here and with fullTagging 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 isOutput 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 ofIOutputCacheStore.
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@Fabman08asked 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 likefail-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 CACHEservices.AddFusionCache();// FUSION OUTPUT CACHEservices.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 thedefault FusionCache instance like in the example above, it's usually better to have a separatenamed 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(newFusionCacheProtoBufNetSerializer()).WithDistributedCache(newRedisCache(newRedisCacheOptions{Configuration="..."})).WithBackplane(newRedisBackplane(newRedisBackplaneOptions{Configuration="..."}));// FUSION OUTPUT CACHEservices.AddFusionOutputCache(options=>{// WHICH NAMED CACHE TO USEoptions.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 adifferent 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 withbyte[] (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 anatively binary serializer likeprotobuf-net,MessagePack orMemoryPack (the available ones can be foundhere): 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 calledEFCoreSecondLevelCacheInterceptor, 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 leastv5.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.
Seehere for the original issue.
📜 Better Logging
Community user@sebbarg noticed (thanks!) that when filtering log messages for theDebug 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 theTrace log level 🤦.
Anyway now this has been fixed, and log messages are more consistent.
Seehere 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!
✅ 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.
Uh oh!
There was an error while loading.Please reload this page.
This PR contains the following updates:
2.0.2->2.4.02.0.2->2.4.02.0.2->2.4.0Release Notes
ZiggyCreatures/FusionCache (ZiggyCreatures.FusionCache)
v2.4.0🏷️ Add
StaleTagsto 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
StaleTagsproperty to the factory execution context, so that now it's possible to access both the tags that are being passed to theGetOrSet()call and the existing tags of the stale entry in the cache (if any), like this: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
StaleTagsproperty, this is now solved for good.Thanks@ted-mundy !
Seehere for the original issue.
Community user@TheSpookyElectric noticed that, when working with theHybridCache adapter, the
LocalCacheExpirationwas 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 !
Seehere for the original issue.
🐞 Fix for
WithRegisteredSerializer()Community user@Inok noticed something... strange.
Thebuilder, 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 !
Seehere for the original issue.
🐞 Fix for
InvalidOperationExceptionwhen usingAlwaysOffSamplerwith OTELCommunity user@DFSko noticed an
InvalidOperationExceptionbeing thrown when working with OTEL and using theAlwaysOffSampler: this had to do with setting the currentActivityafter 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 !
Seehere 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 !
Seehere for the original issue.
✅ 1500 tests, huzza!
FusionCache reached 1500 tests:
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
CacheKeyPrefixusually in conjunction with things like Named Caches, so it would be nice to access both theoriginal one and theprocessed one.Therefore I added both of them in the factory context, and it can be used like this:
Seehere for the original issue, andhere for the design issue.
⚙️ New
InternalStringsoptionsFusionCache 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
CacheNameto automatically separate data in a shared cache when used in conjunction with other FusionCache instances, or the set of special cache keys used withTagging or the waywire 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 somereserved 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
InternalStringsoption inside ofFusionCacheOptionswhere we can set strings like:TagCacheKeyPrefixClearRemoveTagDistributedCacheWireFormatSeparatorBackplaneWireFormatSeparatorIt can be used like this:
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:
or, if we want to customize the couple of special chars, like this:
In this way it will be possible to keep every little detail under control and always be future proof.
Seehere for the original issue.
⚙️ New
FusionCacheEntryOptionsProviderCommunity user@gleb-osokin was looking for a way to have
DefaultEntryOptionsspecific 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
FusionCacheEntryOptionsProviderabstract class which anyone can implement with their own custom logic, and that we can set in theFusionCacheOptionsobject 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:
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
canMutateparam, 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 theCreateEntryOptions(...), and now we also have theCreateEntryOptions(key, ...)variant to include the key in the logic.Here is theoriginal PR, andhere the design issue.
🐞 Fix for skipped check in read-only methods
Community user@permagne noticed that read-only methods (eg:
TryGetandGetOrDefault) were not considering theSkipDistributedCacheReadentry 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.
Seehere 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:
Almost 1500, not bad.
📕 Docs
And finally the docs, which I care a lot about.
I have updated some, like:
Clear(true)andClear(false)Expire()worksSome 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.
Seehere andhere 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.
Seehere andhere for the original issues.
🔀 Expose the current distributed cache, if any
Currently, given a
FusionCacheinstance, it's only possible to know if there is a distributed cache level (L2) via thebool 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 theIDistributedCacheinstance being used, if any.Seehere andhere for the original issues.
📢 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 theIFusionCacheBackplaneinstance being used, if any.Seehere andhere for the original issues.
😶 Add DI support for
NullFusionCacheCommunity user@eskild-th noted it was not possible to specify to use a
NullFusionCacheimplementation via DI.And now it is.
Seehere andhere 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 seeminglystrange, 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 whenRaw Clear is possible (eg: L1 only, no shared).This, in reality, translates to better Clear checks,which means better perf forany method call in the mentioned scenario (eg:
GetOrSet,TryGet, etc), not just when callingClear()itself: yeah 🥳Seehere 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.
Seehere 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 gettingtoo 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.
Seehere andhere 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 adirect dependency to something external for something so core, but since
v0.26.0there's a newIFusionCacheMemoryLockerabstraction 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.
Seehere 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.
Seehere 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.
Seehere 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.
Seehere for the original issue.
v2.1.0v2? Pleaseread here.🔌 Integrate All The Things!
Now that v2 isfinally here and with fullTagging support, it's time to integrate all the things 🥳
The first 2 can be found below:
🚀 Output Cache, FusionCache style
The first one on my list isOutput 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@Fabman08asked 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 likefail-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:
When using the normal OutputCache (with a memory-only cache store) we need to:
With the FusionCache-based version we just need 2 extra steps, before the common one:
One thing to note is that, even though it's possible to use thedefault FusionCache instance like in the example above, it's usually better to have a separatenamed 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:
Another important aspect is to be able to use adifferent 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 anatively binary serializer like
protobuf-net,MessagePackorMemoryPack(the available ones can be foundhere): 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 calledEFCoreSecondLevelCacheInterceptor, 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.1since 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.
Seehere for the original issue.
📜 Better Logging
Community user@sebbarg noticed (thanks!) that when filtering log messages for the
Debuglevel, 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
Tracelog level 🤦.Anyway now this has been fixed, and log messages are more consistent.
Seehere 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!
✅ 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.
This PR was generated byMend Renovate. View therepository job log.