To implement a generic thread-safe cache class in C#, the goal is to create a class that allows you to store, retrieve, and remove key-value pairs in a thread-safe manner. Additionally, the cache should include an expiration mechanism so that items are automatically removed after a certain period.
Here's a step-by-step guide on how to accomplish this:
1. Understanding the Requirements
- Generic Class: The cache should be able to work with any type of key (
TKey
) and value (TValue
). Hence, we need a generic class. - Thread-Safe: The class must handle concurrent access by multiple threads without causing data corruption or unexpected behavior.
- Expiration Mechanism: Items in the cache should have an expiration time after which they are automatically considered invalid and should not be returned from the cache.
2. Choosing the Right Data Structure
- Thread-Safety: C# provides the
ConcurrentDictionary<TKey, TValue>
class in theSystem.Collections.Concurrent
namespace, which is ideal for this purpose. It is designed to handle concurrent operations and provides thread-safe methods for adding, retrieving, and removing items. - CacheItem: Since we need to store both the value and the expiration time for each entry, we should define a
CacheItem
class that contains these two pieces of information.
3. Designing the CacheItem Class
-
The
CacheItem
class will have two properties:- Value: The cached value of type
TValue
. - ExpiryTime: A
DateTime
indicating when the item should expire.
- Value: The cached value of type
-
The class will also have a method
IsExpired()
to check if the current time is past the expiry time.
4. Implementing the Cache Class
- Data Storage: Use a
ConcurrentDictionary<TKey, CacheItem>
to store the cache items. - Add Method: The
Add
method should accept a key, a value, and an expiration duration. It will create aCacheItem
and store it in the dictionary. - Retrieve Method: The
TryGetValue
method will retrieve the item if it exists and is not expired. If the item is expired, it should be removed from the cache. - Remove Method: The
Remove
method will allow manual removal of items from the cache. - Clear Method (Optional): A method to clear all items from the cache.
5. Handling Expiration
- Every time an item is accessed (via
TryGetValue
), its expiration status is checked. If the item has expired, it is removed from the cache, and the method returnsfalse
, indicating the item is no longer valid.
6. Putting It All Together: The Code
Here’s how the code would look:
using System;
using System.Collections.Concurrent;
public class ThreadSafeCache<TKey, TValue>
{
// The CacheItem class holds the value and its expiration time
private class CacheItem
{
public TValue Value { get; }
public DateTime ExpiryTime { get; }
public CacheItem(TValue value, TimeSpan duration)
{
Value = value;
ExpiryTime = DateTime.UtcNow.Add(duration);
}
public bool IsExpired() => DateTime.UtcNow > ExpiryTime;
}
// ConcurrentDictionary to hold the cache items
private readonly ConcurrentDictionary<TKey, CacheItem> _cache = new ConcurrentDictionary<TKey, CacheItem>();
// Method to add an item to the cache
public void Add(TKey key, TValue value, TimeSpan duration)
{
var cacheItem = new CacheItem(value, duration);
_cache[key] = cacheItem; // Adds or updates the value in the cache
}
// Method to retrieve an item from the cache
public bool TryGetValue(TKey key, out TValue value)
{
if (_cache.TryGetValue(key, out CacheItem cacheItem))
{
if (!cacheItem.IsExpired())
{
value = cacheItem.Value;
return true;
}
else
{
// If the item is expired, remove it from the cache
_cache.TryRemove(key, out _);
}
}
value = default;
return false;
}
// Method to remove an item from the cache
public bool Remove(TKey key)
{
return _cache.TryRemove(key, out _);
}
// Optional: Method to clear the cache
public void Clear()
{
_cache.Clear();
}
}
7. Key Points in the Implementation
- Thread-Safety: The
ConcurrentDictionary
handles all thread-safety concerns for adding, retrieving, and removing items. There’s no need for explicit locks or synchronization primitives, which simplifies the implementation. - Expiration Check: The expiration logic is handled within the
TryGetValue
method. If an item is expired, it’s removed from the cache immediately, ensuring that the cache only returns valid items. - Flexible Expiration: The cache allows you to specify different expiration durations for different items, offering flexibility in how long each item should remain valid.