diff --git a/src/Squidex.Infrastructure/Caching/LRUCache.cs b/src/Squidex.Infrastructure/Caching/LRUCache.cs new file mode 100644 index 000000000..95065c214 --- /dev/null +++ b/src/Squidex.Infrastructure/Caching/LRUCache.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// LRUCache.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Caching +{ + public sealed class LRUCache + { + private readonly Dictionary> cacheMap = new Dictionary>(); + private readonly LinkedList cacheHistory = new LinkedList(); + private readonly int capacity; + + public LRUCache(int capacity) + { + Guard.GreaterThan(capacity, 0, nameof(capacity)); + + this.capacity = capacity; + } + + public bool Set(object key, object value) + { + Guard.NotNull(key, nameof(key)); + + if (cacheMap.TryGetValue(key, out var node)) + { + node.Value.Value = value; + + cacheHistory.Remove(node); + cacheHistory.AddLast(node); + + cacheMap[key] = node; + + return true; + } + else + { + if (cacheMap.Count >= capacity) + { + RemoveFirst(); + } + + var cacheItem = new LRUCacheItem { Key = key, Value = value }; + + node = new LinkedListNode(cacheItem); + + cacheMap.Add(key, node); + cacheHistory.AddLast(node); + + return false; + } + } + + public bool Remove(object key) + { + Guard.NotNull(key, nameof(key)); + + if (cacheMap.TryGetValue(key, out var node)) + { + cacheMap.Remove(key); + cacheHistory.Remove(node); + + return true; + } + + return false; + } + + public bool TryGetValue(object key, out object value) + { + Guard.NotNull(key, nameof(key)); + + value = null; + + if (cacheMap.TryGetValue(key, out var node)) + { + value = node.Value.Value; + + cacheHistory.Remove(node); + cacheHistory.AddLast(node); + + return true; + } + + return false; + } + + public bool Contains(object key) + { + Guard.NotNull(key, nameof(key)); + + return cacheMap.ContainsKey(key); + } + + private void RemoveFirst() + { + var node = cacheHistory.First; + + cacheMap.Remove(node.Value.Key); + cacheHistory.RemoveFirst(); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs b/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs new file mode 100644 index 000000000..942fdd15d --- /dev/null +++ b/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// LRUCacheItem.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Infrastructure.Caching +{ + internal class LRUCacheItem + { + public object Key; + public object Value; + } +} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs b/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs new file mode 100644 index 000000000..23401e4a5 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// LRUCacheTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Xunit; + +namespace Squidex.Infrastructure.Caching +{ + public class LRUCacheTests + { + private readonly LRUCache sut = new LRUCache(10); + private readonly string key = "Key"; + + [Fact] + public void Should_always_override_when_setting_value() + { + sut.Set(key, 1); + sut.Set(key, 2); + + Assert.True(sut.TryGetValue(key, out var value)); + Assert.True(sut.Contains(key)); + + Assert.Equal(2, value); + } + + [Fact] + public void Should_remove_old_items_when_capacity_reached() + { + for (int i = 0; i < 15; i++) + { + sut.Set(i.ToString(), i); + } + + for (int i = 0; i < 5; i++) + { + Assert.False(sut.TryGetValue(i.ToString(), out var value)); + Assert.Null(value); + } + + for (int i = 5; i < 15; i++) + { + Assert.True(sut.TryGetValue(i.ToString(), out var value)); + Assert.NotNull(value); + } + } + + [Fact] + public void Should_return_false_when_item_to_remove_does_not_exist() + { + Assert.False(sut.Remove(key)); + } + + [Fact] + public void Should_remove_inserted_item() + { + sut.Set(key, 2); + + Assert.True(sut.Remove(key)); + Assert.False(sut.Contains(key)); + Assert.False(sut.TryGetValue(key, out var value)); + Assert.Null(value); + } + } +} \ No newline at end of file