Short answer: Don't use ICacheEntry
(directly) at all.
Instead, use only the Microsoft.Extensions.Caching.Memory.CacheExtensions
methods: Set<TItem>
and TryGetValue<TItem>
- specifically, the Set
extension method handles ICacheEntry
for you.
Note there's a slight gotcha with this approach: you have to be careful you're using consistent TItem
type-parameters and Object key
keys - e.g. if you cache a List<String>
using Set< List<String> >(...)
but later use TryGetValue< String[] >
then your code will silently fail and return null
- instead of complaining that a cached List<String>
cannot be retrieved as a String[]
. (Of course, you probably shouldn't be caching mutable types like List<T>
, instead you should cache something like ImmutableList<String>
and only retrieve items via a compatible interface like IReadOnlyList<String>
(which means you can also store String[]
and List<String>
in addition to ImmutableList<String>
).
Longer answer: Don't retain ICacheEntry
and call .Dispose()
immediately after setting .Value
There's a huge (still undocumented) gotcha with ICacheEntry
: while the design of the API surface of the entire Microsoft.Extensions.Caching.Memory
library implies that the ICacheEntry
object represents a named/keyed slot in the IMemoryCache
that you can keep in a long-life'd field or static
field - and can manipulate any time you like - this is not the case: the ICacheEntry
returned actually represents an uncomitted cache entry - so simply setting .Value
won't do anything until you call .Dispose()
on the ICacheEntry
(or use a using(){}
block).
The CacheExtensions.Set
method does this for you: observe the using ICacheEntry entry = cache.CreateEntry(key);
line in Set
.
So, by example, if you are going to use ICacheEntry
directly, do not use it like this as it will not work:
public class ThingDoer_Bad
{
// DO NOT USE THIS CODE - IT IS AN EXAMPLE OF HOW IMEMORYCACHE IS MISUNDERSTOOD:
private readonly IMemoryCache memCache; // From DI
private const String CACHE_KEY = "TheThing123";
private ICacheEntry? entry;
public void AddThingToCacheOrOverwriteExistingThing( Thing thing )
{
this.entry = this.memCache.CreateEntry( CACHE_KEY );
this.entry.Value = thing;
this.entry.Expiration = whatever;
}
public void UseCachedThing()
{
if( this.entry != null )
{
Console.WriteLine( this.entry.Value ); // <-- This will appear to work.
}
}
}
...in the above code, the ThingDoer_Bad.AddThingToCacheOrOverwriteExistingThing
method doesn't actually do anything useful - people might think it works because if you retrieve this.entry.Value
it will return the stored Thing
object, but the Thing
object won't be in the cache - you'll just be working with an normal stored object-reference just with extra steps.
This is how you're meant to use it, by not retaining ICacheEntry
at all and disposing it immediately:
public class ThingDoer
{
private readonly IMemoryCache memCache; // From DI
private const String CACHE_KEY = "TheThing123";
public void AddThingToCacheOrOverwriteExistingThing( Thing thing )
{
using( ICacheEntry e = this.memCache.CreateEntry( CACHE_KEY ) )
{
e.Value = thing;
e.Expiration = whatever;
} // <-- The implicit .Dispose() call at the end of the `using` block is what actually triggers persistence.
}
public void UseCachedThing()
{
if( this.memCache.TryGetValue( CACHE_KEY, out Thing? maybeThing ) )
{
Console.WriteLine( cachedThing );
}
}
}
Better answer: Use this strongly-typed struct
to handle get/set of single items
public readonly struct TypedMemoryCacheEntry<T> : IEquatable< TypedMemoryCacheEntry<T> >
where T : class
{
private readonly IMemoryCache cache;
private readonly String key;
public TypedMemoryCacheEntry( IMemoryCache cache, String key )
{
this.cache = cache ?? throw new ArgumentNullException( nameof(cache) );
this.key = key ?? throw new ArgumentNullException( nameof(key) );
}
public String Key => this.key;
public void Set( T value, DateTimeOffset absoluteExpiration )
{
if( value is null ) throw new ArgumentNullException( nameof(value) );
_ = this.cache.Set<T>( key: this.key, value: value, absoluteExpiration: absoluteExpiration );
}
public Boolean TryGet( [NotNullWhen(true)] out T? value )
{
return this.cache.TryGetValue<T>( key: this.key, out value );
}
#region Tedium
public override Boolean Equals( Object? obj ) => obj is TypedMemoryCacheEntry<T> other && this.Equals( other: other );
public Boolean Equals( TypedMemoryCacheEntry<T> other ) => StringComparer.Ordinal.Equals( this.key, other.key ) && Object.ReferenceEquals( this.cache, other.cache );
public override Int32 GetHashCode() => HashCode.Combine( this.cache.GetHashCode(), this.key.GetHashCode() );
public static Boolean operator==( TypedMemoryCacheEntry<T> left, TypedMemoryCacheEntry<T> right ) => left.Equals( other: right );
public static Boolean operator!=( TypedMemoryCacheEntry<T> left, TypedMemoryCacheEntry<T> right ) => !left.Equals( other: right );
#endregion
}
...then by then storing TypedMemoryCacheEntry<T>
as a field you don't need to worry about keeping track of consistent TItem
parameters in every call-site:
public class ThingDoer
{
private readonly IMemoryCache memCache;
private readonly TypedMemoryCacheEntry<Thing> cacheEntry;
public ThingDoer( IMemoryCache memCache )
{
this.memCache = memCache ?? throw new ArgumentNullException(nameof(memCache));
thus.cacheEntry = new TypedMemoryCacheEntry<Thing>( this.memCache, key: "TheThing123" );
}
public void AddThingToCacheOrOverwriteExistingThing( Thing thing )
{
this.cacheEntry.Set( thing, expiration: etc );
}
public void UseCachedThing()
{
if( this.cacheEntry.TryGetValue( out Thing? maybeThing ) )
{
Console.WriteLine( cachedThing );
}
}
}
Much simpler, imo - I don't know why something useful and straightforward to use like this wasn't included in the Microsoft.Extensions.Caching.Abstractions
library.