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:
[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?
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.