In large-scale web applications, caching is a fundamental technique alongside asynchronous and parallel processnig. For externally exposed APIs, implementing caching is crucial to prevent repeated database queries that can lead to server instability.
Below is a generic cache manager interface that defines the core operations for a caching system.
public interface ICacheProvider
{
/// <summary>
/// Retrieves a cached item by its key.
/// </summary>
T Retrieve<T>(string cacheKey);
/// <summary>
/// Stores an item in the cache with a specified expiration time.
/// </summary>
void Store(string cacheKey, object item, int expirationMinutes);
/// <summary>
/// Checks if an item exists in the cache.
/// </summary>
bool Exists(string cacheKey);
/// <summary>
/// Removes an item from the cache by its key.
/// </summary>
void Evict(string cacheKey);
/// <summary>
/// Removes all items whose keys match a given pattern.
/// </summary>
void EvictByPattern(string keyPattern);
/// <summary>
/// Clears all items from the cache.
/// </summary>
void Purge();
}
A concrete implementation using the .NET MemoryCache is provided below.
public class MemoryCacheProvider : ICacheProvider
{
private ObjectCache CacheStore => MemoryCache.Default;
public T Retrieve<T>(string cacheKey)
{
return (T)CacheStore[cacheKey];
}
public void Store(string cacheKey, object item, int expirationMinutes)
{
if (item == null) return;
var policy = new CacheItemPolicy
{
AbsoluteExpiration = DateTime.Now.AddMinutes(expirationMinutes)
};
CacheStore.Add(new CacheItem(cacheKey, item), policy);
}
public bool Exists(string cacheKey)
{
return CacheStore.Contains(cacheKey);
}
public void Evict(string cacheKey)
{
CacheStore.Remove(cacheKey);
}
public void EvictByPattern(string keyPattern)
{
var regex = new Regex(keyPattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
var keysToRemove = CacheStore
.Where(entry => regex.IsMatch(entry.Key))
.Select(entry => entry.Key)
.ToList();
foreach (var key in keysToRemove)
{
Evict(key);
}
}
public void Purge()
{
foreach (var entry in CacheStore)
{
Evict(entry.Key);
}
}
}
To enhance functionality, extension methods can be added for conveinent cache access with automatic loading.
public static class CacheExtensions
{
public static T GetOrCreate<T>(this ICacheProvider cache, string key, Func<T> factory)
{
return GetOrCreate(cache, key, 60, factory);
}
public static T GetOrCreate<T>(this ICacheProvider cache, string key, int cacheMinutes, Func<T> factory)
{
if (cache.Exists(key))
{
return cache.Retrieve<T>(key);
}
var result = factory();
cache.Store(key, result, cacheMinutes);
return result;
}
}
This pattern can be used in data access methods to automatical cache results.
private List<ProjectModel> FetchProjectList()
{
return _cacheProvider.GetOrCreate("PROJECT_LIST", 5, () =>
{
return _repository.GetProjects().ToList();
});
}