Implement Memory Cache with Sliding Expiration in .NET

Introduction

Memory caching is a technique that reduces the requirement to acquire data from slower storage systems by storing frequently used data in memory. Expiration is a typical caching option that ensures the cache stays fresh and doesn't use too much memory by automatically removing cached items after a set amount of time. A variation on this is sliding expiry, in which an item's expiration time is increased each time it is accessed.

Implementation Overview

This guide explains how to use the ConcurrentDictionary class for thread-safe access to create a memory cache with sliding expiration in C#.

Components

  • MemoryCache<TKey, TValue>: The primary cache class, MemoryCache<TKey, TValue>, is in charge of keeping track of and organizing cached data.
  • CacheItem<T>: A helper class that stores the value and expiration time of an item in the cache and represents it.
  • Timer: A background timer that's used to find and delete expired cache items on a regular basis.

Initialization

Make an instance of MemoryCache<TKey, TValue> with a default expiration time in order to use the memory cache.

var cache = new MemoryCache<string, string>(TimeSpan.FromSeconds(30));

Adding Items to the Cache

To add or change objects in the cache, use the Set method. You can also give each item a sliding expiration time.

const string infoKey = "Info";
const string jaiminKey = "Jaimin";

cache.Set(infoKey, DateTime.Now);
cache.Set(jaiminKey, DateTime.Now.AddMinutes(3));

Retrieving Items from the Cache

To get objects out of the cache, use the TryGet function. An item's sliding expiration window will be extended if it is accessed during that period.

DateTime value;
if (cache.TryGet(jaiminKey, out value))
{
    Console.WriteLine($"Value for {nameof(jaiminKey)}: {value}");
}
else
{
    Console.WriteLine($"{nameof(jaiminKey)} not found in cache");
}

Cache Expiration

The background timer automatically deletes expired items from the cache.

Example

var cache = new CacheHelper<string, DateTime>(TimeSpan.FromSeconds(5));

const string infoKey = "Info";
const string jaiminKey = "Jaimin";

cache.Set(infoKey, DateTime.Now);
cache.Set(jaiminKey, DateTime.Now.AddMinutes(3));

Console.WriteLine();
Console.WriteLine();

DateTime value;
if (cache.TryGet(infoKey, out value))
{
    Console.WriteLine($"Value for {nameof(infoKey)}: {value}");
}
else
{
    Console.WriteLine($"{nameof(infoKey)} not found in cache");
}

if (cache.TryGet(jaiminKey, out value))
{
    Console.WriteLine($"Value for {nameof(jaiminKey)}: {value}");
}
else
{
    Console.WriteLine($"{nameof(jaiminKey)} not found in cache");
}

Console.WriteLine();

// Wait for some time and get the data from the cache
Thread.Sleep(4000);

if (cache.TryGet(infoKey, out value))
{
    Console.WriteLine($"Value for {nameof(infoKey)}: {value}");
}
else
{
    Console.WriteLine($"{nameof(infoKey)} not found in cache");
}

if (cache.TryGet(jaiminKey, out value))
{
    Console.WriteLine($"Value for {nameof(jaiminKey)}: {value}");
}
else
{
    Console.WriteLine($"{nameof(jaiminKey)} not found in cache");
}

Console.WriteLine();

// Wait for cache expiration
Thread.Sleep(8000);

if (cache.TryGet(jaiminKey, out value))
{
    Console.WriteLine($"Value for {nameof(jaiminKey)} after expiration: {value}");
}
else
{
    Console.WriteLine($"{nameof(jaiminKey)} not found in cache after expiration");
}

Conclusion

You may effectively manage cached data and make sure it stays relevant and fresh for as long as it is viewed inside the designated expiration window by designing a memory cache with sliding expiration.

We learned the new technique and evolved together.

Happy coding! 😊