4

i found this interface and i want to use it. But i dont understand how to use the Create function...

namespace Microsoft.Extensions.Caching.Memory
{
    public interface IMemoryCache : IDisposable
    {
        ICacheEntry CreateEntry(object key);
        void Remove(object key);
        bool TryGetValue(object key, out object value);
    }
}

How to store something in CreateEntry when there is only the key not the value in the function call? How to store something in the key?

So i have this:

class RedisObjectTestCache : IMemoryCache
    {
        public ICacheEntry CreateEntry(object key)
        {
            Console.WriteLine("Created key: " + key);
            return new CacheEntryTest() { };
        }

        public void Dispose()
        {
            Console.WriteLine("Dispose");
            return;
        }

        public void Remove(object key)
        {
            Console.WriteLine("Removed key: " + key);
            return;
        }

        public bool TryGetValue(object key, out object value)
        {
            Console.WriteLine("Requested key: " + key);
            value = "";
            return false;
        }
    }

and then i call it with the framework:

QueryCacheManager.Cache = new RedisObjectTestCache();

Can i somehow get the value?

Dai
  • 141,631
  • 28
  • 261
  • 374
Florian
  • 41
  • 3
  • 2
    Without looking at the docs, I'd guess that `ICacheEntry` gives you access to the contents of that entry. – phuzi Dec 10 '21 at 10:48
  • The docs specify some [extension methods](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache?view=dotnet-plat-ext-6.0#extension-methods), seems like you can use the [Set](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.set?view=dotnet-plat-ext-6.0#Microsoft_Extensions_Caching_Memory_CacheExtensions_Set__1_Microsoft_Extensions_Caching_Memory_IMemoryCache_System_Object___0_) extension method – MindSwipe Dec 10 '21 at 10:50
  • A few examples are available here: https://stackoverflow.com/q/45751948/706456 – oleksii Dec 10 '21 at 10:53

2 Answers2

5

The ICacheEntry instance returned from the CreateEntry method has a Value property which you can set to the value you want to cache, along with several other properties you can use to control the caching.

There are also several extension methods for the IMemoryCache interface which provide shorthand ways of setting an item in the cache.

Richard Deeming
  • 29,830
  • 10
  • 79
  • 151
  • .NET does it the same way, look at the [implementation](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/MemoryCacheExtensions.cs#L44-L50) of the `Set` method – MindSwipe Dec 10 '21 at 10:52
  • 1
    There's a huge gotcha with `ICacheEntry`: the value won't actually be cached until you call `.Dispose()`! So make sure you do `using( ICacheEntry e = memoryCache.CreateEntry(...) ) { e.Value = value; }`. Kinda ironic that you can't _cache_ the `ICacheEntry` object as a proxy into the `IMemoryCache`. – Dai Feb 07 '23 at 23:11
0

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.

Dai
  • 141,631
  • 28
  • 261
  • 374