47

To be succinct. Is possible list all register keys from Memory Cache in the .Net Core Web Application?

I didn't find anything in IMemoryCache interface.

Carlos
  • 664
  • 1
  • 7
  • 12

10 Answers10

53

As other answers point out, current implementation of Microsoft.Extensions.Caching.Memory.MemoryCache does not expose any members allowing to retrieve all cache keys, although there is a way around the problem if we use reflection.

This answer is partially based upon the one by MarkM, adds some speed to the solution by reducing reflection usage to a minimum, adds support for Microsoft.Extensions.Caching.Memory version 7 and packs everything into a single extension class.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using Microsoft.Extensions.Caching.Memory;

public static class MemoryCacheExtensions
{
#region Microsoft.Extensions.Caching.Memory_6_OR_OLDER

    private static readonly Lazy<Func<MemoryCache, object>> GetEntries6 =
        new Lazy<Func<MemoryCache, object>>(() => (Func<MemoryCache, object>)Delegate.CreateDelegate(
            typeof(Func<MemoryCache, object>),
            typeof(MemoryCache).GetProperty("EntriesCollection", BindingFlags.NonPublic | BindingFlags.Instance).GetGetMethod(true),
            throwOnBindFailure: true));

#endregion

#region Microsoft.Extensions.Caching.Memory_7_OR_NEWER

    private static readonly Lazy<Func<MemoryCache, object>> GetCoherentState =
        new Lazy<Func<MemoryCache, object>>(() =>
            CreateGetter<MemoryCache, object>(typeof(MemoryCache)
                .GetField("_coherentState", BindingFlags.NonPublic | BindingFlags.Instance)));

    private static readonly Lazy<Func<object, IDictionary>> GetEntries7 =
        new Lazy<Func<object, IDictionary>>(() =>
            CreateGetter<object, IDictionary>(typeof(MemoryCache)
                .GetNestedType("CoherentState", BindingFlags.NonPublic)
                .GetField("_entries", BindingFlags.NonPublic | BindingFlags.Instance)));

    private static Func<TParam, TReturn> CreateGetter<TParam, TReturn>(FieldInfo field)
    {
        var methodName = $"{field.ReflectedType.FullName}.get_{field.Name}";
        var method = new DynamicMethod(methodName, typeof(TReturn), new[] { typeof(TParam) }, typeof(TParam), true);
        var ilGen = method.GetILGenerator();
        ilGen.Emit(OpCodes.Ldarg_0);
        ilGen.Emit(OpCodes.Ldfld, field);
        ilGen.Emit(OpCodes.Ret);
        return (Func<TParam, TReturn>)method.CreateDelegate(typeof(Func<TParam, TReturn>));
    }

#endregion

    private static readonly Func<MemoryCache, IDictionary> GetEntries =
        Assembly.GetAssembly(typeof(MemoryCache)).GetName().Version.Major < 7
            ? (Func<MemoryCache, IDictionary>)(cache => (IDictionary)GetEntries6.Value(cache))
            : cache => GetEntries7.Value(GetCoherentState.Value(cache));

    public static ICollection GetKeys(this IMemoryCache memoryCache) =>
        GetEntries((MemoryCache)memoryCache).Keys;

    public static IEnumerable<T> GetKeys<T>(this IMemoryCache memoryCache) =>
        memoryCache.GetKeys().OfType<T>();
}

Usage:

var cache = new MemoryCache(new MemoryCacheOptions());
cache.GetOrCreate(1, ce => "one");
cache.GetOrCreate("two", ce => "two");

foreach (var key in cache.GetKeys())
    Console.WriteLine($"Key: '{key}', Key type: '{key.GetType()}'");

foreach (var key in cache.GetKeys<string>())
    Console.WriteLine($"Key: '{key}', Key type: '{key.GetType()}'");

Output:

Key: '1', Key type: 'System.Int32'
Key: 'two', Key type: 'System.String'
Key: 'two', Key type: 'System.String'

Notes:

  • Reflection usage is reduced to a single call that builds the GetEntries delegate. When we're working with the retrieved MemoryCache keys, reflection is not used. In contrast to a raw reflection approach, this decreases execution time and saves machine resources during traversal of long collections of MemoryCache keys.
  • In the solution we are casting MemoryCache's internal dictionary to IDictionary instead of the native ConcurrentDictionary<object, CacheEntry> because CacheEntry type is internal.
  • The code was verified using a matrix of console and webapi apps based on .NET Framework 4.8, .NET 6 and 7 with Microsoft.Extensions.Caching.Memory versions 6 and 7. LangVersion is 7.3.
roxton
  • 1,446
  • 10
  • 9
  • Do you have any workaround for NET7? -- For me the performance is not a big issue, as I use such list when debug, to see what actually got stored in the cache. – Asons Nov 10 '22 at 00:11
  • 2
    @Asons, the answer now supports Microsoft.Extensions.Caching.Memory 7. See verification details in the notes. – roxton Nov 15 '22 at 08:52
  • @Asons note that "members accessed through one of the interfaces the ConcurrentDictionary implements, including extension methods, are not guaranteed to be thread safe". I believe it's actually safe to use IDictionary.Keys here but still not guaranteed by the docs. – Evk Nov 15 '22 at 16:37
  • @Evk -- Thanks, and am aware of that. I'm more interested in how the code itself is set up (being a web developer and want to learn some more), and the way I want to check for a key is neither thread safe dependent, nor from a performance aspect. – Asons Nov 15 '22 at 19:12
41

There is no such thing in .Net Core yet. Here is my workaround:

 var field = typeof(MemoryCache).GetProperty("EntriesCollection", BindingFlags.NonPublic | BindingFlags.Instance);
 var collection = field.GetValue(_memoryCache) as ICollection;
 var items = new List<string>();
 if (collection != null)
 foreach (var item in collection)
 {
      var methodInfo = item.GetType().GetProperty("Key");
      var val = methodInfo.GetValue(item);
      items.Add(val.ToString());
 }
wonea
  • 4,783
  • 17
  • 86
  • 139
MarkM
  • 442
  • 5
  • 7
  • It looks like entity framework also uses the MemoryCache, so if you want keys just for your application, you probably want to filter out all keys that begin with "Microsoft". – Rocklan Jul 21 '20 at 02:01
  • 1
    Brilliant, thank you. Gotta love **System.Reflection**, but very odd that Microsoft didn't include additional extension methods for working with the keys, etc. – Nexus Oct 25 '20 at 14:18
  • 5
    This doesn't work in NET7. Do you have a solution for that? – Asons Nov 10 '22 at 00:07
  • Yes, there is a solution. At least for .NET 6 it is changed to "_entries" – Ton Snoei May 31 '23 at 08:46
26

MarkM's answer didn't quite work for me, it wouldn't cast the results to an ICollection, but I took the idea and came up with this that works quite well for me. Hopefully it helps someone else out there too:

// 2022-12-06
// Updated to work with both .Net7 and previous versions.  Code can handle either version as-is.  
// Remove code as needed for version you are not using if desired.

// Define the collection object for scoping.  It is created as a dynamic object since the collection
// method returns as an object array which cannot be used in a foreach loop to generate the list.
dynamic cacheEntriesCollection = null;

// This action creates an empty definitions container as defined by the class type.  
// Pull the _coherentState field for .Net version 7 or higher.  Pull the EntriesCollection 
// property for .Net version 6 or lower.    Both of these objects are defined as private, 
// so we need to use Reflection to gain access to the non-public entities.  
var cacheEntriesFieldCollectionDefinition = typeof(MemoryCache).GetField("_coherentState", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var cacheEntriesPropertyCollectionDefinition = typeof(MemoryCache).GetProperty("EntriesCollection", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);


// .Net 6 or lower.  
// In these versions of .Net, the EntriesCollection is a direct property of the MemoryCache type
// definition, so we can populate our cacheEntriesCollection with the definition from Relection
// and the values from our MemoryCache instance.
if (cacheEntriesPropertyCollectionDefinition != null)
{
    cacheEntriesCollection = cacheEntriesPropertyCollectionDefinition.GetValue(instanceIMemoryCache);
}

// .Net 7 or higher.
// Starting with .Net 7.0, the EntriesCollection object was moved to being a child object of
// the _coherentState field under the MemoryCache type.  Same process as before with an extra step.
// Populate the coherentState field variable with the definition from above using the data in
// our MemoryCache instance.  Then use Reflection to gain access to the private property EntriesCollection.
if (cacheEntriesFieldCollectionDefinition != null)
{
    var coherentStateValueCollection = cacheEntriesFieldCollectionDefinition.GetValue(instanceIMemoryCache);
    var entriesCollectionValueCollection = coherentStateValueCollection.GetType().GetProperty("EntriesCollection", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
    cacheEntriesCollection = entriesCollectionValueCollection.GetValue(coherentStateValueCollection);
}

// Define a new list we'll be adding the cache entries to
List<Microsoft.Extensions.Caching.Memory.ICacheEntry> cacheCollectionValues = new List<Microsoft.Extensions.Caching.Memory.ICacheEntry>();

foreach (var cacheItem in cacheEntriesCollection)
{
    // Get the "Value" from the key/value pair which contains the cache entry   
    Microsoft.Extensions.Caching.Memory.ICacheEntry cacheItemValue = cacheItem.GetType().GetProperty("Value").GetValue(cacheItem, null);

    // Add the cache entry to the list
    cacheCollectionValues.Add(cacheItemValue);
}

// You can now loop through the cacheCollectionValues list created above however you like.
dislexic
  • 270
  • 3
  • 5
  • 3
    this solution is the best here so far (6/2020) – Matěj Štágl Feb 06 '21 at 14:39
  • 1
    Updated my answer to work with both .Net 7 and previous versions. The code has been tested and works with either version as-is by utilizing the if statements. Feel free to remove the code for whichever version you are not using. – dislexic Dec 06 '22 at 15:56
10

As of .NET 7, they have changed the internals of the object and there is the new CoherentState private class which is a private field inside the MemoryCache instance and within the CoherentState field (_coherentState) you can access the EntriesCollection collection that you guys have been referencing. So, in order to get the list of keys you can do the following:

var coherentState = typeof(MemoryCache).GetField("_coherentState", BindingFlags.NonPublic | BindingFlags.Instance);

        var coherentStateValue = coherentState.GetValue(_memoryCache);

        var entriesCollection = coherentStateValue.GetType().GetProperty("EntriesCollection", BindingFlags.NonPublic | BindingFlags.Instance);

        var entriesCollectionValue = entriesCollection.GetValue(coherentStateValue) as ICollection;

        var keys = new List<string>();

        if (entriesCollectionValue != null)
        {
            foreach (var item in entriesCollectionValue)
            {
                var methodInfo = item.GetType().GetProperty("Key");

                var val = methodInfo.GetValue(item);

                keys.Add(val.ToString());
            }
        }

I have tested this locally and it works!

Carlos Torrecillas
  • 4,965
  • 7
  • 38
  • 69
9

Currently there is no such method in the IMemoryCache interface to return all the cache keys. As per this github issue comments, i do not think that would be added in the future.

Quoting Eilons comment

I think it's doubtful this would be available because part of the idea with caching is that mere moments after you ask it a question, the answer could have changed. That is, suppose you have the answer to which keys are there - a moment later the cache is purged and the list of keys you have is invalid.

If you need the keys, you should maintain the list of keys in your app while you set items to the cache and use that as needed.

Here is another useful github issue

Will there be GetEnumerator() for MemoryCache ?

Shyju
  • 214,206
  • 104
  • 411
  • 497
  • Assuming that I need to exclude cache registers in specific business situations, a list of keys in the app could be useful. Thanks Shyju – Carlos Aug 10 '17 at 17:30
  • 1
    @Carlos You should keep a data structure which you need to keep in sync when your code writes/removes entry from IMemoryCache. Keeping it sync is not an easy problem to solve IMHO. – Shyju Mar 07 '20 at 19:38
  • `If you need the keys, you should maintain the list of keys in your app while you set items to the cache and use that as needed.` Isn't this like saying "write your own memory cache that exposes keys too"? How can you otherwise ensure parity of keys and contents of cache? Feel like this suggestion contradicts the quote "suppose you get the keys, a moment later the cache is purged and the list of keys you have is invalid". I feel like a better suggestion would be to write a cache that returns the collection of all keys only alongside the collections of all values. – alelom Feb 20 '23 at 08:10
2

I was using MemoryCacheExtensions, suggested in roxton’s answer to show keys list for lazyCache, that wraps MemoryCache as MemoryCacheProvider

public static IEnumerable<string> GetKeys(this IAppCache appCache)  
{
    var cacheProvider = appCache.CacheProvider as MemoryCacheProvider;
    if (cacheProvider != null) //may be MockCacheProvider in tests 
    {
    var field = typeof(MemoryCacheProvider).GetField("cache", BindingFlags.NonPublic | BindingFlags.Instance);
    var memoryCache = field.GetValue(cacheProvider) as MemoryCache;
    return memoryCache.GetKeys<string>();
    }
    return new List<string>();
}
Michael Freidgeim
  • 26,542
  • 16
  • 152
  • 170
1

We implemented this concept to enable removing by regex pattern.

The full implementation is may be found in Saturn72 github repository. We are shifting to AspNet Core these days so the location may moved. Search for MemoryCacheManager in the repository This is the current location

wonea
  • 4,783
  • 17
  • 86
  • 139
Roi Shabtai
  • 2,981
  • 2
  • 31
  • 47
0

For .NET6/7+ see @roxton's excellent answer.
In case you are looking for a drop-in replacement of @MarkM's solution for .NET7:

private static readonly FieldInfo? cacheEntriesStateDefinition = typeof(MemoryCache).GetField("_coherentState", BindingFlags.NonPublic | BindingFlags.Instance);
private static readonly PropertyInfo? cacheEntriesCollectionDefinition = cacheEntriesStateDefinition?.FieldType.GetProperty("EntriesCollection", BindingFlags.NonPublic | BindingFlags.Instance);
        
public static IEnumerable<ICacheEntry>? GetKeysAsICacheEntries(this IMemoryCache cache)
{
    if (cacheEntriesStateDefinition == null || cacheEntriesCollectionDefinition == null)
    {
        return null;
    }

    dynamic? cacheEntriesCollection = cacheEntriesCollectionDefinition.GetValue(cacheEntriesStateDefinition.GetValue(cache));

    if (cacheEntriesCollection == null)
    {
        return null;
    }

    List<ICacheEntry> cacheCollectionValues = new();
    foreach (dynamic cacheItem in cacheEntriesCollection)
    {
        ICacheEntry cacheItemValue = cacheItem.GetType().GetProperty("Value").GetValue(cacheItem, null);
        cacheCollectionValues.Add(cacheItemValue);
    }

    return cacheCollectionValues;
}
Asons
  • 84,923
  • 12
  • 110
  • 165
Matěj Štágl
  • 870
  • 1
  • 9
  • 27
0

An alternative to reflection and relying on internal implementation of MemoryCache approach, you can create a wrapper over IMemoryCache let's say CacheManager that will handle putting/getting things to cache and track keys records like this:

private readonly List<string> _keys = new List<string>();

private void OnCacheEntryAdded(string key)
{
        _keys.Add(key);
}
private void OnCacheEntryRemoved(string key)
{
        _keys.Remove(key);
}

public IEnumerable<string> GetKeys()
{
        foreach (var key in _keys.ToArray())
        {
            if (!IsSet(key))
            {
                _keys.Remove(key);
                continue;
            }

            yield return key;
         }
 }
Artem Vertiy
  • 1,002
  • 15
  • 31
-1

I know it's not the answer, but... There is another approach to this, you could cache a list. Something like this:

public async Task<List<User>> GetUsers()
{
    var cacheKey = "getAllUserCacheKey";

    if (_usersCache != null && ((MemoryCache)_usersCache).Count > 0)
    {
        return _usersCache.Get<List<User>>(cacheKey);
    }

    var users = await _userRepository.GetAll();


    // Set cache options.
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        .SetSlidingExpiration(TimeSpan.FromMinutes(5));

    _usersCache.Set(cacheKey, users);

    return users;

}
gorhal
  • 459
  • 7
  • 13