Net Core MemoryCache not working properly in Azure

skysurfer :

I noticed a strange behavior related to the use of MemoryCache in my API developed in Net Core 2.2

After several tests I created this simple controller to demonstrate the behavior.

The API is published to Azure. In my local environment all works fine.

The controller has three methods:

  • Create: write four items in memory cache
  • List: lists the elements present in memory cache
  • Clear: delete the items from memory cache
    [Route("api/[controller]")]
    public class CacheController : ControllerBase
    {
        private readonly IMemoryCache memoryCache;

        public CacheController(IMemoryCache memoryCache)
        {
            this.memoryCache = memoryCache;
        }

        [HttpGet("Create")]
        public IActionResult CreateEntries()
        {

            memoryCache.Set("Key1", "Value1", TimeSpan.FromHours(1));
            memoryCache.Set("Key2", "Value2", TimeSpan.FromHours(1));
            memoryCache.Set("Key3", "Value3", TimeSpan.FromHours(1));
            memoryCache.Set("Key4", "Value4", TimeSpan.FromHours(1));

            return Ok();
        }


        [HttpGet()]
        public IActionResult List()
        {
            // Get the empty definition for the EntriesCollection
            var cacheEntriesCollectionDefinition = typeof(MemoryCache).GetProperty("EntriesCollection", BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.NonPublic | BindingFlags.Public);

            // Populate the definition with your IMemoryCache instance.  
            // It needs to be cast as a dynamic, otherwise you can't
            // loop through it due to it being a collection of objects.
            var cacheEntriesCollection = cacheEntriesCollectionDefinition.GetValue(memoryCache) as dynamic;

            // Define a new list we'll be adding the cache entries too
            var cacheCollectionValues = new List<KeyValuePair<string, string>>();

            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);

                if (!cacheItemValue.Key.ToString().StartsWith("Microsoft.EntityFrameworkCore"))
                    // Add the cache entry to the list
                    cacheCollectionValues.Add(new KeyValuePair<string, string>(cacheItemValue.Key.ToString(), cacheItemValue.Value.ToString()));
            }
            return Ok(cacheCollectionValues);
        }

        [HttpGet("Clear")]
        public IActionResult Clear()
        {
            var removedKeys = new List<string>();
            // Get the empty definition for the EntriesCollection
            var cacheEntriesCollectionDefinition = typeof(MemoryCache).GetProperty("EntriesCollection", BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.NonPublic | BindingFlags.Public);

            // Populate the definition with your IMemoryCache instance.  
            // It needs to be cast as a dynamic, otherwise you can't
            // loop through it due to it being a collection of objects.
            var cacheEntriesCollection = cacheEntriesCollectionDefinition.GetValue(memoryCache) as dynamic;

            // Define a new list we'll be adding the cache entries too
            var cacheCollectionValues = new List<KeyValuePair<string, string>>();

            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);

                if (!cacheItemValue.Key.ToString().StartsWith("Microsoft.EntityFrameworkCore"))
                {
                    // Remove the cache entry from the list
                    memoryCache.Remove(cacheItemValue.Key.ToString());
                    removedKeys.Add(cacheItemValue.Key.ToString());
                }
            }
            return Ok(removedKeys);
        }
    }

1) Execute List action. Expected result: no element - Result obtained: no element - OK

2) Execute Create action.

3) Execute List action. Expected result: 4 elements - Result obtained: 4 elements

4) Execute List action again. Expected result: 4 elements - Result obtained: 0 items - Wrong!!!

5) Execute Clear action. Expected result: list of 4 items deleted - Result obtained: list of 4 items deleted - OK

6) Execute List action. Expected result: 0 items - Result obtained: 0 elements - OK

7) Execute List action. Expected result: 0 items - Result obtained: 4 elements - Wrong!!!

Can anyone explain this strange behavior to me?

Chris Pratt :

You haven't given any information on the actual hosting setup, but it sounds like you've got multiple instances running, i.e. a web farm in IIS, container replicas, etc.

Memory cache is process-bound, and each running instance of your application is a different process (each process has its own pool of memory). As such, it becomes a luck of the draw as to which process the request hits, and therefore, what data actually exists in memory when that request executes.

If you need consistency in your cache from request to request, you should be using IDistributedCache instead, and a backing store like Redis or SQL Server. There's a MemoryDistributedCache implementation, but this is just a sensible default for development; it's not actually distributed and suffers the same process-bound issues as MemoryCache does.

Otherwise, you'd need to ensure that there is only ever one instance of your app running, which may not even be possible depending on how you're deploying and offers no potential to scale, or you must accept that cache will sometimes exist or not depending on what's happened in that process. There's times when this is okay. For example, if you're just caching the results of some API call, you can use GetOrCreateAsync and the worst case scenario is that sometimes the API will be queried again, but it still overall decreases the load on the API/keeps you under any rate limits.

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=383209&siteId=1