In general is never a good idea to mutate data retrieved from the cache, it should always be considered immutable/readonly.
To see why, the typical example of such behaviour is an update flow: get something, change it, and then save it back to the database, seemingly easy peasy.
The problem in this case is that a cache is a cache, meaning the value may not be the very last version from the database, even by just 1s or less: since we are updating it, we want to have the very last version before changing it and saving it back, otherwise we would loose some changes made after the last time cached it.
Although optimistic concurrency, with a variation of etags/last modified, surely help and helps avoid critical issues like loosing some changes, by enlarging the temporal window where something may be stale we are increasing the chance of optimistic concurrency errors.
Therefore in these scenarios (like the update flow mentioned above) the best practice is to bypass the cache entirely and just get it from the database while using the cache for the remaining reads which, in the vast majority of real-world use cases, comprise 99%+ of the overall reads (typically systems are more read-heavy than write-heavy), so we can keep the incredible perf boost with caching.
Having said that, keep reading...
Problem
It came up multipletimes a desire to be able to get something from the cache and safely change it, since this may not necessarily be related to an update flow or because, simply, users may have a particular scenario in mind, and ideally they should be abe to just do that in an easy (and optimized!) way.
Currently a common solution is to create a wrapper implementation of IMemoryCache which is passed to FusionCache and would act on an underlying IMemoryCache instance by serialize + deserialize an entry at each get operation.
This technically works, but it has a couple of issues:
it does not get the same "it just works" vibe I always pushed for in FusionCache
it requires some extra coding (the wrapper class)
it requries some extra setup
since it's done at the entire cache level, it lacks granular control over which cache entries that is needed, it's an all-or-nothing approach
it deserializes for every cache get, which is the whole point, but it also serialize every time and from performance perspective this is not so great: deserializing is what guarantees us cloning, while the serialization to byte[] can happen only once
Solution
Since I collected enough evidence that this is something needed by the community (even though probably not by a huge amount of people), I decided to finally tackle this with a new option called Auto-Cloning that:
just works, out of the box
is easy to use
does not require extra coding/setup (it's just a new option)
Well FusionCache always had the ability to specify a serializer (type IFusionCacheSerializer) to be able to work with the distributed cache: it will use the same serializer to (de)serialize values, easy peasy.
To avoid being forced to also specify A new method SetupSerializer(serializer) is being created, while also being able to do the same via dependency injection, as always.
The option ReThrowOriginalExceptions will also be respected.
There will be a new entry option, namely bool EnableAutoClone that will trigger the mechanism (default: false).
Regarding performance: instead of serializing + deserializing at every get call (which would be a waste), FusionCache will keep track of an internal buffer on each memory entry and, only if and when it will be requried, it will serialize it (once), so that the binary payload will be available to deserialize at every get call.
Extra care will be put into avoiding any synchronization/double-serialization issue related to multithreading/high-load scenario.
Finally, since the feature create the clear expectation to be able to get something from the cache and freely modify it without repercussions, an exception will be thrown in these cases:
if the feature is enabled and there's no serializer, it will be thrown an InvalidOperationException
if the serializer fails to (de)serialize, it will be thrown either the specific exception (by the serializer being used) or a FusionCacheSerializationException, depending on the ReThrowOriginalExceptions option
Of course DefaultEntryOptions are always at our disposal to do the usual default + granular change flow, so we can both enable it granulary per-call or just once in the DefaultEntryOptions and forget about it (but remember: auto-cloning has a cost, so use it with care).
Alternatives
The wrapper class mentioned above, with all the issues it does have.
Additional context
The upcoming HybridCache from Microsoft will seemingly have a similar feature, meaning it will clone cached values before returning them, so feature-wise this would be aligned with that.
Preface
In general is never a good idea to mutate data retrieved from the cache, it should always be considered immutable/readonly.
To see why, the typical example of such behaviour is an update flow: get something, change it, and then save it back to the database, seemingly easy peasy. The problem in this case is that a cache is a cache, meaning the value may not be the very last version from the database, even by just
1s
or less: since we are updating it, we want to have the very last version before changing it and saving it back, otherwise we would loose some changes made after the last time cached it.Although optimistic concurrency, with a variation of etags/last modified, surely help and helps avoid critical issues like loosing some changes, by enlarging the temporal window where something may be stale we are increasing the chance of optimistic concurrency errors.
Therefore in these scenarios (like the update flow mentioned above) the best practice is to bypass the cache entirely and just get it from the database while using the cache for the remaining reads which, in the vast majority of real-world use cases, comprise 99%+ of the overall reads (typically systems are more read-heavy than write-heavy), so we can keep the incredible perf boost with caching.
Having said that, keep reading...
Problem
It came up multiple times a desire to be able to get something from the cache and safely change it, since this may not necessarily be related to an update flow or because, simply, users may have a particular scenario in mind, and ideally they should be abe to just do that in an easy (and optimized!) way.
Currently a common solution is to create a wrapper implementation of
IMemoryCache
which is passed to FusionCache and would act on an underlyingIMemoryCache
instance by serialize + deserialize an entry at each get operation.This technically works, but it has a couple of issues:
byte[]
can happen only onceSolution
Since I collected enough evidence that this is something needed by the community (even though probably not by a huge amount of people), I decided to finally tackle this with a new option called Auto-Cloning that:
IFusionCacheSerializer
)That's cool, but how?
Well FusionCache always had the ability to specify a serializer (type
IFusionCacheSerializer
) to be able to work with the distributed cache: it will use the same serializer to (de)serialize values, easy peasy.To avoid being forced to also specify A new method
SetupSerializer(serializer)
is being created, while also being able to do the same via dependency injection, as always. The optionReThrowOriginalExceptions
will also be respected.There will be a new entry option, namely
bool EnableAutoClone
that will trigger the mechanism (default:false
). Regarding performance: instead of serializing + deserializing at every get call (which would be a waste), FusionCache will keep track of an internal buffer on each memory entry and, only if and when it will be requried, it will serialize it (once), so that the binary payload will be available to deserialize at every get call.Extra care will be put into avoiding any synchronization/double-serialization issue related to multithreading/high-load scenario.
Finally, since the feature create the clear expectation to be able to get something from the cache and freely modify it without repercussions, an exception will be thrown in these cases:
InvalidOperationException
FusionCacheSerializationException
, depending on theReThrowOriginalExceptions
optionOf course
DefaultEntryOptions
are always at our disposal to do the usual default + granular change flow, so we can both enable it granulary per-call or just once in theDefaultEntryOptions
and forget about it (but remember: auto-cloning has a cost, so use it with care).Alternatives
The wrapper class mentioned above, with all the issues it does have.
Additional context
The upcoming HybridCache from Microsoft will seemingly have a similar feature, meaning it will clone cached values before returning them, so feature-wise this would be aligned with that.