You are cleaning a cache wrong.

Serhii Korol - Oct 22 '23 - - Dev Community

Today, I would like to talk about cache. No about how to create or as important caching. I want to talk about how to manage the cache correctly. The most famous cache provider is Memory Cache from Microsoft. I'm sure you know the Microsoft.Extensions.Caching.Memory NuGet package. The typical implementation is something like this:

using Microsoft.Extensions.Caching.Memory;

var options = new MemoryCacheOptions
{
    ExpirationScanFrequency = TimeSpan.FromMinutes(15)
};
IMemoryCache cache = new MemoryCache(options);
cache.Set("item1", "value1", TimeSpan.FromMinutes(1));
cache.Remove("item1");
Enter fullscreen mode Exit fullscreen mode

So why am I writing this article if everything works fine and is very simple? The problem is in removing data from the memory. The typical setting is data lifecycle, after which it'll be deleted. The issue is in the probable blocking of the main thread, which can lead to a deadlock.

Let's consider another NuGet package as CacheManager.Microsoft.Extensions.Caching.Memory. This package allows you to manage data more effectively and safely.
Let's implement a cache manager:

var cacheManager = new BaseCacheManager<string>(
            new ConfigurationBuilder()
                .WithMicrosoftMemoryCacheHandle()
                .EnablePerformanceCounters()
                .EnableStatistics()
                .WithExpiration(ExpirationMode.Sliding, TimeSpan.FromMinutes(15))
                .Build()
        );
Enter fullscreen mode Exit fullscreen mode

The cache manager allows you to work with different providers, even Redis. Also, you can choose different time expiration modes, performance counters, and statistics.

Further, let me show you how to add items:

// Add items to the cache
        cacheManager.Add("item1",
            DateTime.Now.AddMinutes(1).ToString(CultureInfo.InvariantCulture),
            "region1");
        cacheManager.Add("item2",
            DateTime.Now.AddMinutes(2).ToString(CultureInfo.InvariantCulture),
            "region1");
        cacheManager.Add("item1",
            DateTime.Now.AddMinutes(3).ToString(CultureInfo.InvariantCulture), "region2");
        var item = new CacheItem<string>(
            "item2",
            "region2",
            DateTime.Now.AddMinutes(4)
                .ToString(CultureInfo.InvariantCulture),
            ExpirationMode.Absolute,
            TimeSpan.FromSeconds(6));
        cacheManager.Add(item);
Enter fullscreen mode Exit fullscreen mode

Here, you can see the difference between Memory Cache. The Cache Manager allows you to add regions for each item. For what purposes is it needed? First of all, you can add equal keys to storage. Second, you can remove the batch of things. The Cache Manager also knows how to set expiration with different modes for each item. Below, I showed how to batch-delete items:

cacheManager.ClearRegion("region1");
Enter fullscreen mode Exit fullscreen mode

Most interesting, we'll clear data in a different thread.

// Start the cache cleanup task (runs in the background)
        await Task.Run(async () =>
        {
            while (true)
            {
                await Task.Delay(TimeSpan.FromSeconds(5));
                DisplayValue(cacheManager, "item1", "region1");
                DisplayValue(cacheManager, "item2", "region1");
                DisplayValue(cacheManager, "item1", "region2");
                DisplayValue(cacheManager, "item2", "region2");
                await Task.Delay(TimeSpan.FromSeconds(10));
                cacheManager.Clear();
                DisplayValue(cacheManager, "item1", "region1");
                DisplayValue(cacheManager, "item2", "region1");
                DisplayValue(cacheManager, "item1", "region2");
                DisplayValue(cacheManager, "item2", "region2");
            }
        });
Enter fullscreen mode Exit fullscreen mode

For reusing of code, I used this method:

static void DisplayValue(ICacheManager<string> cache, string key, string region)
    {
        var value = cache.Get(key, region);
        Console.WriteLine(value != null
            ? $"Retrieved value for {key} in {region}: {value}"
            : $"Item {key} not found in {region}.");
    }
Enter fullscreen mode Exit fullscreen mode

If we run it, we'll see that the first two rows are empty. The deleted items by region caused it.

Item item1 not found in region1.
Item item2 not found in region1.
Retrieved value for item1 in region2: 10/22/2023 16:36:37
Retrieved value for item2 in region2: 10/22/2023 16:37:37
Enter fullscreen mode Exit fullscreen mode

And in the end, the storage was entirely cleared.

Item item1 not found in region1.
Item item2 not found in region1.
Item item1 not found in region2.
Item item2 not found in region2.
Enter fullscreen mode Exit fullscreen mode

For more information about this library's opportunities and features, I recommend visiting the official documentation. I hope this information was helpful to you. See you next week, and happy coding.
The source code is, as always, on my GitHub.

Buy Me A Beer

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .