I want to create an In Memory Cache in .Net. The primary usage for this cache would be to store addresses... I want to keep around 1000 addresses in memory and once the cache is full, remove the Least Recently Used address from the cache.
Microsoft provide 2 versions of MemoryCache
, one is implemented in System.Runtime.Caching and the other is implemented in Microsoft.Extensions.Caching.Memory.
The second implementation seems to be more flexible, you can define Sliding Expiration (which updates expiration time every time an entry is used) and it also allows to set a size limit for the entries in cache. For example I can set the cache size to 1000 entries... however the problem is that once the cache is full (reached 1000 entries), it does not remove the Least Recently Used entry... instead it ignores additional inserts until some of the existing elements are expired.
So I ended up writing my own Most Recently Used Cache:
MruCache.cs
// Most Recently Used Cache
public class MruCache<T>
{
private readonly object _lock;
private readonly int _size;
private Dictionary<string, MruCacheEntry<T>> _enteries;
public MruCache(int size = 1024)
{
_lock = new object();
_size = size;
_enteries = new Dictionary<string, MruCacheEntry<T>>();
}
private bool IsFull
{
get
{
return _enteries.Count >= _size;
}
}
private int OnePercent
{
get
{
int number = _size / 100;
return number < 1 ? 1 : number;
}
}
public bool TryGetValue(string key, out T value)
{
if (_enteries.TryGetValue(key, out MruCacheEntry<T> entry))
{
value = _enteries[key].Value;
return true;
}
else
{
value = default(T);
return false;
}
}
public void AddOrUpdate(string key, T value)
{
lock (_lock)
{
if (_enteries.ContainsKey(key))
{
_enteries[key].Update(value);
}
else
{
MakeRoomIfFull();
_enteries.Add(key, new MruCacheEntry<T>(value));
}
}
}
private void MakeRoomIfFull()
{
if (IsFull)
{
// get 1% of entries which were Least Recently Used
var keysToRemove = _enteries.OrderBy(e => e.Value.LastUsageTime).Select(e => e.Key).Take(OnePercent);
foreach (var k in keysToRemove)
{
_enteries.Remove(k);
}
}
}
}
MruCacheEntry.cs
// Most Recently Used Cache Entry
public class MruCacheEntry<T>
{
private T _value;
public MruCacheEntry(T value)
{
Value = value;
}
public T Value
{
get
{
Touch();
return _value;
}
set
{
Touch();
_value = value;
}
}
public DateTime LastUsageTime { get; set; }
public void Update(T value)
{
Value = value;
}
private void Touch()
{
LastUsageTime = DateTime.Now;
}
}
UnitTests
[TestMethod]
public void Test_cache_size_4()
{
var _mruCache = new MruCache<string>(4);
_mruCache.AddOrUpdate("item1", "1");
_mruCache.AddOrUpdate("item2", "2");
_mruCache.AddOrUpdate("item3", "3");
_mruCache.AddOrUpdate("item1", "new1");
_mruCache.AddOrUpdate("item4", "4");
_mruCache.AddOrUpdate("item5", "5");
_mruCache.TryGetValue("item1", out string valueOfItem1);
_mruCache.TryGetValue("item2", out string valueOfItem2);
_mruCache.TryGetValue("item3", out string valueOfItem3);
_mruCache.TryGetValue("noSuchKey", out string valueOfNoSuchKey);
valueOfItem1.Should().Be("new1");
valueOfItem2.Should().Be(null);
valueOfItem3.Should().Be("3");
valueOfNoSuchKey.Should().Be(null);
}
string
? I'm thinking if you were using a timestamp key, then you can just compare the last usage time and the timestamp (which is the time created). also, whyDateTime.Now
? are you planning to cache address for more than one day ? \$\endgroup\$Key
is normalized StreetAddress (by normalized I mean lowercase and alphanumric, removing evening else)... this way I know if an address is already added to cache or not. At the same time uppercase, lower case, commas and dashes won't make any difference is the addresskey
. I want to keep the frequently used addressed in the cache for much more than a day... for example the city of Auckland (which has Lat/Long) remains in the cache for months as it is frequently used. \$\endgroup\$Dictionary
, which would reduce some efforts_enteries = new Dictionary<string, MruCacheEntry<T>>(StringComparer.InvariantCultureIgnoreCase);
\$\endgroup\$ConcurrentDictionary
instead oflock
? \$\endgroup\$