mirror of https://github.com/Squidex/squidex.git
3 changed files with 194 additions and 0 deletions
@ -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<object, LinkedListNode<LRUCacheItem>> cacheMap = new Dictionary<object, LinkedListNode<LRUCacheItem>>(); |
||||
|
private readonly LinkedList<LRUCacheItem> cacheHistory = new LinkedList<LRUCacheItem>(); |
||||
|
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<LRUCacheItem>(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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue