From 4ce4385e8d987bb92aa09d881634d9146ce34dec Mon Sep 17 00:00:00 2001 From: Travis Whidden Date: Fri, 19 Jan 2024 12:46:06 -0800 Subject: [PATCH 01/11] #764 - Introduce Sliding Cache to Constant Expression Helper --- .../Parser/ConstantExpressionHelper.cs | 28 ++++--- .../Parser/ThreadSafeSlidingCache.cs | 79 +++++++++++++++++++ .../Util/ConstantExpressionHelperTests.cs | 33 ++++++++ 3 files changed, 130 insertions(+), 10 deletions(-) create mode 100644 src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs create mode 100644 test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs diff --git a/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs index 5b651651a..8e0670948 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs @@ -1,12 +1,18 @@ -using System.Collections.Concurrent; -using System.Linq.Expressions; +using System.Linq.Expressions; namespace System.Linq.Dynamic.Core.Parser { internal static class ConstantExpressionHelper { - private static readonly ConcurrentDictionary Expressions = new(); - private static readonly ConcurrentDictionary Literals = new(); +#if DEBUG + private static readonly TimeSpan TimeToLivePeriod = TimeSpan.FromSeconds(10); +#else + private static readonly TimeSpan TimeToLivePeriod = TimeSpan.FromMinutes(10); +#endif + + public static readonly ThreadSafeSlidingCache Expressions = new(TimeToLivePeriod); + private static readonly ThreadSafeSlidingCache Literals = new(TimeToLivePeriod); + public static bool TryGetText(Expression expression, out string? text) { @@ -15,15 +21,17 @@ public static bool TryGetText(Expression expression, out string? text) public static Expression CreateLiteral(object value, string text) { - if (!Expressions.ContainsKey(value)) + if (Expressions.TryGetValue(value, out var outputValue)) { - ConstantExpression constantExpression = Expression.Constant(value); - - Expressions.TryAdd(value, constantExpression); - Literals.TryAdd(constantExpression, text); + return outputValue; } - return Expressions[value]; + ConstantExpression constantExpression = Expression.Constant(value); + + Expressions.AddOrUpdate(value, constantExpression); + Literals.AddOrUpdate(constantExpression, text); + + return constantExpression; } } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs b/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs new file mode 100644 index 000000000..52d2f8de7 --- /dev/null +++ b/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs @@ -0,0 +1,79 @@ +using System.Collections.Concurrent; + +namespace System.Linq.Dynamic.Core.Parser +{ + internal class ThreadSafeSlidingCache where T1 : notnull where T2 : notnull + { + private readonly ConcurrentDictionary _cache; + private readonly TimeSpan _timeToLive; + private readonly TimeSpan _cleanupFrequency; + private DateTime _lastCleanupTime = DateTime.MinValue; + + public ThreadSafeSlidingCache(TimeSpan timeToLive, TimeSpan? cleanupFrequency = null) + { + _cache = new ConcurrentDictionary(); + _timeToLive = timeToLive; + _cleanupFrequency = cleanupFrequency ?? TimeSpan.FromSeconds(10); + } + + public TimeSpan TimeToLive => _timeToLive; + + public void AddOrUpdate(T1 key, T2 value) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + if (value == null) throw new ArgumentNullException(nameof(value)); + + var expirationTime = DateTime.UtcNow.Add(_timeToLive); + _cache[key] = (value, expirationTime); + + CleanupIfNeeded(); + } + + public bool TryGetValue(T1 key, out T2 value) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + + CleanupIfNeeded(); + + if (_cache.TryGetValue(key, out var valueAndExpiration)) + { + if (DateTime.UtcNow <= valueAndExpiration.ExpirationTime) + { + value = valueAndExpiration.Value; + return true; + } + else + { + // Remove expired item + _cache.TryRemove(key, out _); + } + } + + value = default!; + return false; + } + + public bool Remove(T1 key) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + var removed = _cache.TryRemove(key, out _); + CleanupIfNeeded(); + return removed; + } + + private void CleanupIfNeeded() + { + if (DateTime.UtcNow - _lastCleanupTime > _cleanupFrequency) + { + foreach (var key in _cache.Keys) + { + if (DateTime.UtcNow > _cache[key].ExpirationTime) + { + _cache.TryRemove(key, out _); + } + } + _lastCleanupTime = DateTime.UtcNow; + } + } + } +} diff --git a/test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs b/test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs new file mode 100644 index 000000000..108bc2421 --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs @@ -0,0 +1,33 @@ +using System.Linq.Dynamic.Core.Parser; +using System.Threading.Tasks; +using Xunit; + +namespace System.Linq.Dynamic.Core.Tests +{ + public partial class EntitiesTests + { + [Fact] + public async Task TestConstantExpressionLeak() + { + //Arrange + PopulateTestData(1, 0); + + var populateExpression = _context.Blogs.All("BlogId > 2000"); + + var expressions = ConstantExpressionHelper.Expressions; + + if (!expressions.TryGetValue(2000, out _)) + { + Assert.Fail("Cache was missing constant expression for 2000"); + } + + // Wait for the slide cache to expire, check on second later + await Task.Delay(ConstantExpressionHelper.Expressions.TimeToLive.Add(TimeSpan.FromSeconds(1))); + + if (expressions.TryGetValue(2000, out _)) + { + Assert.Fail("Expected constant to be expired 2000"); + } + } + } +} From 1d3bf0aa49ae5e47ba6f729a912db4b2f72fd0d8 Mon Sep 17 00:00:00 2001 From: Travis Whidden Date: Fri, 19 Jan 2024 13:09:52 -0800 Subject: [PATCH 02/11] #764 Additional Tests, Missing TTL Update --- .../Parser/ThreadSafeSlidingCache.cs | 1 + .../Util/ConstantExpressionHelperTests.cs | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs b/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs index 52d2f8de7..64249d66a 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs @@ -40,6 +40,7 @@ public bool TryGetValue(T1 key, out T2 value) if (DateTime.UtcNow <= valueAndExpiration.ExpirationTime) { value = valueAndExpiration.Value; + _cache[key] = (value, DateTime.UtcNow.Add(_timeToLive)); return true; } else diff --git a/test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs b/test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs index 108bc2421..cae023d60 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs @@ -16,11 +16,26 @@ public async Task TestConstantExpressionLeak() var expressions = ConstantExpressionHelper.Expressions; + // Should contain if (!expressions.TryGetValue(2000, out _)) { Assert.Fail("Cache was missing constant expression for 2000"); } - + + // wait half the expiry time + await Task.Delay(TimeSpan.FromSeconds(ConstantExpressionHelper.Expressions.TimeToLive.TotalSeconds/2)); + if (!expressions.TryGetValue(2000, out _)) + { + Assert.Fail("Cache was missing constant expression for 2000 (1)"); + } + + // wait another half the expiry time, plus one second + await Task.Delay(TimeSpan.FromSeconds((ConstantExpressionHelper.Expressions.TimeToLive.TotalSeconds / 2)+1)); + if (!expressions.TryGetValue(2000, out _)) + { + Assert.Fail("Cache was missing constant expression for 2000 (2)"); + } + // Wait for the slide cache to expire, check on second later await Task.Delay(ConstantExpressionHelper.Expressions.TimeToLive.Add(TimeSpan.FromSeconds(1))); From 3d09d491887e3acbd877be7dde6c1e04dd83d137 Mon Sep 17 00:00:00 2001 From: Travis Whidden Date: Fri, 19 Jan 2024 13:41:09 -0800 Subject: [PATCH 03/11] #764 - Rename T1,T2 to TKey, TValue --- .../Parser/ThreadSafeSlidingCache.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs b/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs index 64249d66a..4a72a6022 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs @@ -2,23 +2,23 @@ namespace System.Linq.Dynamic.Core.Parser { - internal class ThreadSafeSlidingCache where T1 : notnull where T2 : notnull + internal class ThreadSafeSlidingCache where TKey : notnull where TValue : notnull { - private readonly ConcurrentDictionary _cache; + private readonly ConcurrentDictionary _cache; private readonly TimeSpan _timeToLive; private readonly TimeSpan _cleanupFrequency; private DateTime _lastCleanupTime = DateTime.MinValue; public ThreadSafeSlidingCache(TimeSpan timeToLive, TimeSpan? cleanupFrequency = null) { - _cache = new ConcurrentDictionary(); + _cache = new ConcurrentDictionary(); _timeToLive = timeToLive; _cleanupFrequency = cleanupFrequency ?? TimeSpan.FromSeconds(10); } public TimeSpan TimeToLive => _timeToLive; - public void AddOrUpdate(T1 key, T2 value) + public void AddOrUpdate(TKey key, TValue value) { if (key == null) throw new ArgumentNullException(nameof(key)); if (value == null) throw new ArgumentNullException(nameof(value)); @@ -29,7 +29,7 @@ public void AddOrUpdate(T1 key, T2 value) CleanupIfNeeded(); } - public bool TryGetValue(T1 key, out T2 value) + public bool TryGetValue(TKey key, out TValue value) { if (key == null) throw new ArgumentNullException(nameof(key)); @@ -54,7 +54,7 @@ public bool TryGetValue(T1 key, out T2 value) return false; } - public bool Remove(T1 key) + public bool Remove(TKey key) { if (key == null) throw new ArgumentNullException(nameof(key)); var removed = _cache.TryRemove(key, out _); From 579085e78f6ec40989ac0ac5621bee3757db4bd6 Mon Sep 17 00:00:00 2001 From: Travis Whidden Date: Fri, 19 Jan 2024 13:48:55 -0800 Subject: [PATCH 04/11] #764 - Set cleanup time prior to enumeration to prevent multiple cleanup hits. --- .../Parser/ThreadSafeSlidingCache.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs b/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs index 4a72a6022..602c02e92 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs @@ -66,6 +66,9 @@ private void CleanupIfNeeded() { if (DateTime.UtcNow - _lastCleanupTime > _cleanupFrequency) { + // Set here, so we don't have re-entry due to large collection enumeration. + _lastCleanupTime = DateTime.UtcNow; + foreach (var key in _cache.Keys) { if (DateTime.UtcNow > _cache[key].ExpirationTime) @@ -73,7 +76,7 @@ private void CleanupIfNeeded() _cache.TryRemove(key, out _); } } - _lastCleanupTime = DateTime.UtcNow; + } } } From a3aede1e0a9b17fa290177e39849a001b95cf365 Mon Sep 17 00:00:00 2001 From: Travis Whidden Date: Sun, 21 Jan 2024 10:08:38 -0800 Subject: [PATCH 05/11] #764 - Code Review Changes implemented; ThreadSafeSlidingCache Tests Added --- .../Parser/ConstantExpressionHelper.cs | 3 +- .../Parser/ThreadSafeSlidingCache.cs | 83 ---------- .../Util/DateTimeUtils.cs | 12 ++ .../Util/ThreadSafeSlidingCache.cs | 131 +++++++++++++++ .../Util/ConstantExpressionHelperTests.cs | 76 ++++----- .../Util/ThreadSafeSlidingCacheTests.cs | 152 ++++++++++++++++++ 6 files changed, 335 insertions(+), 122 deletions(-) delete mode 100644 src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs create mode 100644 src/System.Linq.Dynamic.Core/Util/DateTimeUtils.cs create mode 100644 src/System.Linq.Dynamic.Core/Util/ThreadSafeSlidingCache.cs create mode 100644 test/System.Linq.Dynamic.Core.Tests/Util/ThreadSafeSlidingCacheTests.cs diff --git a/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs index 8e0670948..9c02918fc 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs @@ -1,4 +1,5 @@ -using System.Linq.Expressions; +using System.Linq.Dynamic.Core.Util; +using System.Linq.Expressions; namespace System.Linq.Dynamic.Core.Parser { diff --git a/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs b/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs deleted file mode 100644 index 602c02e92..000000000 --- a/src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Collections.Concurrent; - -namespace System.Linq.Dynamic.Core.Parser -{ - internal class ThreadSafeSlidingCache where TKey : notnull where TValue : notnull - { - private readonly ConcurrentDictionary _cache; - private readonly TimeSpan _timeToLive; - private readonly TimeSpan _cleanupFrequency; - private DateTime _lastCleanupTime = DateTime.MinValue; - - public ThreadSafeSlidingCache(TimeSpan timeToLive, TimeSpan? cleanupFrequency = null) - { - _cache = new ConcurrentDictionary(); - _timeToLive = timeToLive; - _cleanupFrequency = cleanupFrequency ?? TimeSpan.FromSeconds(10); - } - - public TimeSpan TimeToLive => _timeToLive; - - public void AddOrUpdate(TKey key, TValue value) - { - if (key == null) throw new ArgumentNullException(nameof(key)); - if (value == null) throw new ArgumentNullException(nameof(value)); - - var expirationTime = DateTime.UtcNow.Add(_timeToLive); - _cache[key] = (value, expirationTime); - - CleanupIfNeeded(); - } - - public bool TryGetValue(TKey key, out TValue value) - { - if (key == null) throw new ArgumentNullException(nameof(key)); - - CleanupIfNeeded(); - - if (_cache.TryGetValue(key, out var valueAndExpiration)) - { - if (DateTime.UtcNow <= valueAndExpiration.ExpirationTime) - { - value = valueAndExpiration.Value; - _cache[key] = (value, DateTime.UtcNow.Add(_timeToLive)); - return true; - } - else - { - // Remove expired item - _cache.TryRemove(key, out _); - } - } - - value = default!; - return false; - } - - public bool Remove(TKey key) - { - if (key == null) throw new ArgumentNullException(nameof(key)); - var removed = _cache.TryRemove(key, out _); - CleanupIfNeeded(); - return removed; - } - - private void CleanupIfNeeded() - { - if (DateTime.UtcNow - _lastCleanupTime > _cleanupFrequency) - { - // Set here, so we don't have re-entry due to large collection enumeration. - _lastCleanupTime = DateTime.UtcNow; - - foreach (var key in _cache.Keys) - { - if (DateTime.UtcNow > _cache[key].ExpirationTime) - { - _cache.TryRemove(key, out _); - } - } - - } - } - } -} diff --git a/src/System.Linq.Dynamic.Core/Util/DateTimeUtils.cs b/src/System.Linq.Dynamic.Core/Util/DateTimeUtils.cs new file mode 100644 index 000000000..6a61abf1f --- /dev/null +++ b/src/System.Linq.Dynamic.Core/Util/DateTimeUtils.cs @@ -0,0 +1,12 @@ +namespace System.Linq.Dynamic.Core.Util +{ + internal interface IDateTimeUtils + { + DateTime UtcNow { get; } + } + + internal class DateTimeUtils : IDateTimeUtils + { + public DateTime UtcNow => DateTime.UtcNow; + } +} diff --git a/src/System.Linq.Dynamic.Core/Util/ThreadSafeSlidingCache.cs b/src/System.Linq.Dynamic.Core/Util/ThreadSafeSlidingCache.cs new file mode 100644 index 000000000..ba429de31 --- /dev/null +++ b/src/System.Linq.Dynamic.Core/Util/ThreadSafeSlidingCache.cs @@ -0,0 +1,131 @@ +using System.Collections.Concurrent; +using System.Linq.Dynamic.Core.Validation; +using System.Threading.Tasks; + +namespace System.Linq.Dynamic.Core.Util +{ + internal class ThreadSafeSlidingCache where TKey : notnull where TValue : notnull + { + // ReSharper disable once StaticMemberInGenericType + private static readonly TimeSpan _defaultCleanupFrequency = TimeSpan.FromMinutes(10); + private readonly ConcurrentDictionary _cache; + private readonly TimeSpan _cleanupFrequency; + private readonly IDateTimeUtils _dateTimeProvider; + private readonly Func _deleteExpiredCachedItemsDelegate; + private readonly long? _minCacheItemsBeforeCleanup; + private DateTime _lastCleanupTime = DateTime.MinValue; + + /// + /// Sliding Thread Safe Cache + /// + /// The length of time any object would survive before being removed + /// Only look for expired objects over specific periods + /// + /// If defined, only allow the cleanup process after x number of cached items have + /// been stored + /// + /// + /// Provides the Time for the Caching object. Default will be created if not supplied. Used + /// for Testing classes + /// + public ThreadSafeSlidingCache( + TimeSpan timeToLive, + TimeSpan? cleanupFrequency = null, + long? minCacheItemsBeforeCleanup = null, + IDateTimeUtils? dateTimeProvider = null) + { + _cache = new ConcurrentDictionary(); + TimeToLive = timeToLive; + _minCacheItemsBeforeCleanup = minCacheItemsBeforeCleanup; + _cleanupFrequency = cleanupFrequency ?? _defaultCleanupFrequency; + _deleteExpiredCachedItemsDelegate = Cleanup; + _dateTimeProvider = dateTimeProvider ?? new DateTimeUtils(); + } + + public TimeSpan TimeToLive { get; } + + /// + /// Provide the number of items in the cache + /// + public int Count => _cache.Count; + + public void AddOrUpdate(TKey key, TValue value) + { + Check.NotNull(key); + Check.NotNull(value); + + var expirationTime = _dateTimeProvider.UtcNow.Add(TimeToLive); + _cache[key] = (value, expirationTime); + + CleanupIfNeeded(); + } + + public bool TryGetValue(TKey key, out TValue value) + { + Check.NotNull(key); + + CleanupIfNeeded(); + + if (_cache.TryGetValue(key, out var valueAndExpiration)) + { + if (_dateTimeProvider.UtcNow <= valueAndExpiration.ExpirationTime) + { + value = valueAndExpiration.Value; + _cache[key] = (value, _dateTimeProvider.UtcNow.Add(TimeToLive)); + return true; + } + + // Remove expired item + _cache.TryRemove(key, out _); + } + + value = default!; + return false; + } + + public bool Remove(TKey key) + { + Check.NotNull(key); + + var removed = _cache.TryRemove(key, out _); + CleanupIfNeeded(); + return removed; + } + + /// + /// Check if cache needs to be cleaned up. + /// If it does, span the cleanup as a Task to prevent from blocking + /// + private void CleanupIfNeeded() + { + if (_dateTimeProvider.UtcNow - _lastCleanupTime > _cleanupFrequency + && (_minCacheItemsBeforeCleanup == null || + _cache.Count >= + _minCacheItemsBeforeCleanup) // Only cleanup if we have a minimum number of items in the cache. + ) + { + // Set here, so we don't have re-entry due to large collection enumeration. + _lastCleanupTime = _dateTimeProvider.UtcNow; + + Task.Run(_deleteExpiredCachedItemsDelegate); + } + } + + /// + /// Cleanup the Cache + /// + /// + private Task Cleanup() + { + foreach (var key in _cache.Keys) + { + if (_dateTimeProvider.UtcNow > _cache[key].ExpirationTime) + { + _cache.TryRemove(key, out _); + } + } + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs b/test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs index cae023d60..d6b342b78 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs @@ -6,43 +6,43 @@ namespace System.Linq.Dynamic.Core.Tests { public partial class EntitiesTests { - [Fact] - public async Task TestConstantExpressionLeak() - { - //Arrange - PopulateTestData(1, 0); - - var populateExpression = _context.Blogs.All("BlogId > 2000"); - - var expressions = ConstantExpressionHelper.Expressions; - - // Should contain - if (!expressions.TryGetValue(2000, out _)) - { - Assert.Fail("Cache was missing constant expression for 2000"); - } - - // wait half the expiry time - await Task.Delay(TimeSpan.FromSeconds(ConstantExpressionHelper.Expressions.TimeToLive.TotalSeconds/2)); - if (!expressions.TryGetValue(2000, out _)) - { - Assert.Fail("Cache was missing constant expression for 2000 (1)"); - } - - // wait another half the expiry time, plus one second - await Task.Delay(TimeSpan.FromSeconds((ConstantExpressionHelper.Expressions.TimeToLive.TotalSeconds / 2)+1)); - if (!expressions.TryGetValue(2000, out _)) - { - Assert.Fail("Cache was missing constant expression for 2000 (2)"); - } - - // Wait for the slide cache to expire, check on second later - await Task.Delay(ConstantExpressionHelper.Expressions.TimeToLive.Add(TimeSpan.FromSeconds(1))); - - if (expressions.TryGetValue(2000, out _)) - { - Assert.Fail("Expected constant to be expired 2000"); - } - } + //[Fact] + //public async Task TestConstantExpressionLeak() + //{ + // //Arrange + // PopulateTestData(1, 0); + + // var populateExpression = _context.Blogs.All("BlogId > 2000"); + + // var expressions = ConstantExpressionHelper.Expressions; + + // // Should contain + // if (!expressions.TryGetValue(2000, out _)) + // { + // Assert.Fail("Cache was missing constant expression for 2000"); + // } + + // // wait half the expiry time + // await Task.Delay(TimeSpan.FromSeconds(ConstantExpressionHelper.Expressions.TimeToLive.TotalSeconds/2)); + // if (!expressions.TryGetValue(2000, out _)) + // { + // Assert.Fail("Cache was missing constant expression for 2000 (1)"); + // } + + // // wait another half the expiry time, plus one second + // await Task.Delay(TimeSpan.FromSeconds((ConstantExpressionHelper.Expressions.TimeToLive.TotalSeconds / 2)+1)); + // if (!expressions.TryGetValue(2000, out _)) + // { + // Assert.Fail("Cache was missing constant expression for 2000 (2)"); + // } + + // // Wait for the slide cache to expire, check on second later + // await Task.Delay(ConstantExpressionHelper.Expressions.TimeToLive.Add(TimeSpan.FromSeconds(1))); + + // if (expressions.TryGetValue(2000, out _)) + // { + // Assert.Fail("Expected constant to be expired 2000"); + // } + //} } } diff --git a/test/System.Linq.Dynamic.Core.Tests/Util/ThreadSafeSlidingCacheTests.cs b/test/System.Linq.Dynamic.Core.Tests/Util/ThreadSafeSlidingCacheTests.cs new file mode 100644 index 000000000..ca610bd82 --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests/Util/ThreadSafeSlidingCacheTests.cs @@ -0,0 +1,152 @@ +using System.Linq.Dynamic.Core.Util; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Xunit; + +namespace System.Linq.Dynamic.Core.Tests.Util +{ + public class ThreadSafeSlidingCacheTests + { + [Fact] + public void ThreadSafeSlidingCache_CacheOperations() + { + var mockDateTime = new MockDateTimeProvider(); + + // Arrange + var cache = new ThreadSafeSlidingCache( + TimeSpan.FromSeconds(1), + dateTimeProvider: mockDateTime); + + // Add + cache.AddOrUpdate(1, "one"); + cache.AddOrUpdate(2, "two"); + cache.AddOrUpdate(3, "three"); + + // Replace + cache.AddOrUpdate(1, "oneone"); + + Assert.True(cache.Count == 3, $"Expected 3 items in the cache, only had {cache.Count}"); + + + // Test retrieval + Assert.True(cache.TryGetValue(1, out var value1), $"Expected to find the value, but did not"); + Assert.True(cache.TryGetValue(2, out var value2), $"Expected to find the value, but did not"); + Assert.True(cache.TryGetValue(3, out var value3), $"Expected to find the value, but did not"); + + // Test Removal + cache.Remove(1); + Assert.True(cache.Count == 2, $"Expected 2 items in the cache, only had {cache.Count}"); + + } + + [Fact] + public void ThreadSafeSlidingCache_TestExpire() + { + var mockDateTime = new MockDateTimeProvider(); + + // Arrange + var cache = new ThreadSafeSlidingCache(TimeSpan.FromMinutes(10), + dateTimeProvider: mockDateTime); + + // Act + cache.AddOrUpdate(1, "one"); + mockDateTime.UtcNow = mockDateTime.UtcNow.AddMinutes(11); + if (cache.TryGetValue(1, out var value)) + { + Assert.True(false, $"Expected to not find the value, but found {value}"); + } + + } + + [Fact] + public async Task ThreadSafeSlidingCache_TestAutoExpire() + { + var mockDateTime = new MockDateTimeProvider(); + + // Arrange + var cache = new ThreadSafeSlidingCache(TimeSpan.FromMinutes(10), + dateTimeProvider: mockDateTime); + + // Act + cache.AddOrUpdate(1, "one"); + + // Ensure one item is in the cache + Assert.True(cache.Count == 1, $"Expected 1 items in the cache, only had {cache.Count}"); + + // move the time forward + mockDateTime.UtcNow = mockDateTime.UtcNow.AddMinutes(11); + + // Trigger the cleanup, asking for non-existing key + cache.TryGetValue(10, out var _); + + // Since the cache cleanup is triggered by a Task and not on the same thread, + // give it a moment for the cleanup to happen + await Task.Delay(10); + + // Ensure one item is in the cache + Assert.True(cache.Count == 0, $"Expected 0 items in the cache, only had {cache.Count}"); + + } + + [Fact] + public async Task ThreadSafeSlidingCache_TestNull() + { + // Arrange + var cache = new ThreadSafeSlidingCache(TimeSpan.FromMinutes(10)); + + // Expect an ArgumentNullException + var exception = Assert.Throws(() => { + cache.AddOrUpdate(null, "one"); + }); + + } + + [Fact] + public async Task ThreadSafeSlidingCache_TestMinNumberBeforeTests() + { + // Arrange + var mockDateTime = new MockDateTimeProvider(); + + // Arrange + var cache = new ThreadSafeSlidingCache( + TimeSpan.FromMinutes(10), + minCacheItemsBeforeCleanup: 2, + dateTimeProvider: mockDateTime); + + // Act + cache.AddOrUpdate(1, "one"); + + // Ensure one item is in the cache + Assert.True(cache.Count == 1, $"Expected 1 items in the cache, only had {cache.Count}"); + + // move the time forward + mockDateTime.UtcNow = mockDateTime.UtcNow.AddMinutes(11); + + // Trigger the cleanup, asking for non-existing key + cache.TryGetValue(10, out var _); + + // Since the cache cleanup is triggered by a Task and not on the same thread, + // give it a moment for the cleanup to happen + await Task.Delay(10); + + // Ensure one item is in the cache + Assert.True(cache.Count == 1, $"Expected 1 items in the cache, only had {cache.Count}"); + + // Act + cache.AddOrUpdate(2, "two"); + + // Since the cache cleanup is triggered by a Task and not on the same thread, + // give it a moment for the cleanup to happen + await Task.Delay(10); + + // Ensure one item is in the cache + Assert.True(cache.Count == 1, $"Expected 1 items in the cache, had {cache.Count}"); + } + + private class MockDateTimeProvider : IDateTimeUtils + { + public DateTime UtcNow { get; set; } = DateTime.UtcNow; + + } + } +} From bda76228d4fa0834950a555887fdc108a04b0ea0 Mon Sep 17 00:00:00 2001 From: Travis Whidden Date: Sun, 21 Jan 2024 10:14:44 -0800 Subject: [PATCH 06/11] #764 - Move DefaultCleanupFrequency to internal static non generic class --- .../Util/ThreadSafeSlidingCache.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Util/ThreadSafeSlidingCache.cs b/src/System.Linq.Dynamic.Core/Util/ThreadSafeSlidingCache.cs index ba429de31..4391b1a0d 100644 --- a/src/System.Linq.Dynamic.Core/Util/ThreadSafeSlidingCache.cs +++ b/src/System.Linq.Dynamic.Core/Util/ThreadSafeSlidingCache.cs @@ -4,10 +4,14 @@ namespace System.Linq.Dynamic.Core.Util { + internal static class ThreadSafeSlidingCacheConstants + { + // Default cleanup frequency + public static readonly TimeSpan DefaultCleanupFrequency = TimeSpan.FromMinutes(10); + } + internal class ThreadSafeSlidingCache where TKey : notnull where TValue : notnull { - // ReSharper disable once StaticMemberInGenericType - private static readonly TimeSpan _defaultCleanupFrequency = TimeSpan.FromMinutes(10); private readonly ConcurrentDictionary _cache; private readonly TimeSpan _cleanupFrequency; private readonly IDateTimeUtils _dateTimeProvider; @@ -37,7 +41,7 @@ public ThreadSafeSlidingCache( _cache = new ConcurrentDictionary(); TimeToLive = timeToLive; _minCacheItemsBeforeCleanup = minCacheItemsBeforeCleanup; - _cleanupFrequency = cleanupFrequency ?? _defaultCleanupFrequency; + _cleanupFrequency = cleanupFrequency ?? ThreadSafeSlidingCacheConstants.DefaultCleanupFrequency; _deleteExpiredCachedItemsDelegate = Cleanup; _dateTimeProvider = dateTimeProvider ?? new DateTimeUtils(); } From 72a6906cf6b6a9e252fb3d6e848a5b6722b6c30e Mon Sep 17 00:00:00 2001 From: Travis Whidden Date: Sun, 21 Jan 2024 10:18:53 -0800 Subject: [PATCH 07/11] #764 - Dropped Preprocessor Directive for TTL in ConstantExpressionHelper --- .../Parser/ConstantExpressionHelper.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs index 9c02918fc..3c9c88c45 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs @@ -5,11 +5,7 @@ namespace System.Linq.Dynamic.Core.Parser { internal static class ConstantExpressionHelper { -#if DEBUG - private static readonly TimeSpan TimeToLivePeriod = TimeSpan.FromSeconds(10); -#else private static readonly TimeSpan TimeToLivePeriod = TimeSpan.FromMinutes(10); -#endif public static readonly ThreadSafeSlidingCache Expressions = new(TimeToLivePeriod); private static readonly ThreadSafeSlidingCache Literals = new(TimeToLivePeriod); From c965066cfb0769a5ae6b38b1de70dd45390b7a3c Mon Sep 17 00:00:00 2001 From: Travis Whidden Date: Sun, 21 Jan 2024 20:40:57 -0800 Subject: [PATCH 08/11] #764 - Move ConstantExpressionHelper to Instance. Refactor feedback from Review --- .../Parser/ConstantExpressionHelper.cs | 66 +++++--- .../Parser/ExpressionParser.cs | 6 +- .../Parser/ExpressionPromoter.cs | 4 +- .../Parser/NumberParser.cs | 34 ++-- src/System.Linq.Dynamic.Core/ParsingConfig.cs | 23 +++ .../Util/Cache/CacheContainer.cs | 14 ++ .../Util/Cache/ThreadSafeSlidingCache.cs | 135 ++++++++++++++++ .../Cache/ThreadSafeSlidingCacheConstants.cs | 7 + .../Util/TaskUtils.cs | 16 ++ .../Util/ThreadSafeSlidingCache.cs | 135 ---------------- .../Util/Cache/ThreadSafeSlidingCacheTests.cs | 151 +++++++++++++++++ .../Util/ConstantExpressionHelperTests.cs | 48 ------ .../Util/ThreadSafeSlidingCacheTests.cs | 152 ------------------ 13 files changed, 417 insertions(+), 374 deletions(-) create mode 100644 src/System.Linq.Dynamic.Core/Util/Cache/CacheContainer.cs create mode 100644 src/System.Linq.Dynamic.Core/Util/Cache/ThreadSafeSlidingCache.cs create mode 100644 src/System.Linq.Dynamic.Core/Util/Cache/ThreadSafeSlidingCacheConstants.cs create mode 100644 src/System.Linq.Dynamic.Core/Util/TaskUtils.cs delete mode 100644 src/System.Linq.Dynamic.Core/Util/ThreadSafeSlidingCache.cs create mode 100644 test/System.Linq.Dynamic.Core.Tests/Util/Cache/ThreadSafeSlidingCacheTests.cs delete mode 100644 test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs delete mode 100644 test/System.Linq.Dynamic.Core.Tests/Util/ThreadSafeSlidingCacheTests.cs diff --git a/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs index 3c9c88c45..66be39913 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs @@ -1,34 +1,60 @@ -using System.Linq.Dynamic.Core.Util; +using System.Linq.Dynamic.Core.Util.Cache; using System.Linq.Expressions; -namespace System.Linq.Dynamic.Core.Parser +namespace System.Linq.Dynamic.Core.Parser; + +internal class ConstantExpressionHelper { - internal static class ConstantExpressionHelper + private readonly ParsingConfig _config; + + // Static shared instance to prevent duplications of the same objects + private static ThreadSafeSlidingCache? _expressions; + private static ThreadSafeSlidingCache? _literals; + + public ConstantExpressionHelper(ParsingConfig config) + { + _config = config; + + } + + private ThreadSafeSlidingCache GetLiterals() + { + _literals ??= new ThreadSafeSlidingCache( + _config.ConstantExpressionSlidingCacheTimeToLive, + _config.ConstantExpressionSlidingCacheCleanupFrequency, + _config.ConstantExpressionSlidingCacheMinItemsTrigger + ); + return _literals; + } + + private ThreadSafeSlidingCache GetExpression() { - private static readonly TimeSpan TimeToLivePeriod = TimeSpan.FromMinutes(10); + _expressions ??= new ThreadSafeSlidingCache( + _config.ConstantExpressionSlidingCacheTimeToLive, + _config.ConstantExpressionSlidingCacheCleanupFrequency, + _config.ConstantExpressionSlidingCacheMinItemsTrigger + ); + return _expressions; + } - public static readonly ThreadSafeSlidingCache Expressions = new(TimeToLivePeriod); - private static readonly ThreadSafeSlidingCache Literals = new(TimeToLivePeriod); + public bool TryGetText(Expression expression, out string? text) + { + return GetLiterals().TryGetValue(expression, out text); + } - public static bool TryGetText(Expression expression, out string? text) + public Expression CreateLiteral(object value, string text) + { + if (GetExpression().TryGetValue(value, out var outputValue)) { - return Literals.TryGetValue(expression, out text); + return outputValue; } - public static Expression CreateLiteral(object value, string text) - { - if (Expressions.TryGetValue(value, out var outputValue)) - { - return outputValue; - } - - ConstantExpression constantExpression = Expression.Constant(value); + var constantExpression = Expression.Constant(value); - Expressions.AddOrUpdate(value, constantExpression); - Literals.AddOrUpdate(constantExpression, text); + GetExpression().AddOrUpdate(value, constantExpression); + GetLiterals().AddOrUpdate(constantExpression, text); - return constantExpression; - } + return constantExpression; } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 7e81a3eab..788ea9362 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -45,6 +45,7 @@ public class ExpressionParser private ParameterExpression? _root; private Type? _resultType; private bool _createParameterCtor; + private ConstantExpressionHelper _constantExpressionHelper; /// /// Gets name for the `it` field. By default this is set to the KeyWord value "it". @@ -81,6 +82,7 @@ public ExpressionParser(ParameterExpression[]? parameters, string expression, ob _methodFinder = new MethodFinder(_parsingConfig, _expressionHelper); _typeFinder = new TypeFinder(_parsingConfig, _keywordsHelper); _typeConverterFactory = new TypeConverterFactory(_parsingConfig); + _constantExpressionHelper = new ConstantExpressionHelper(_parsingConfig); if (parameters != null) { @@ -900,7 +902,7 @@ private AnyOf ParseStringLiteral(bool forceParseAsString) } _textParser.NextToken(); - return ConstantExpressionHelper.CreateLiteral(parsedStringValue[0], parsedStringValue); + return _constantExpressionHelper.CreateLiteral(parsedStringValue[0], parsedStringValue); } _textParser.NextToken(); @@ -924,7 +926,7 @@ private AnyOf ParseStringLiteral(bool forceParseAsString) parsedStringValue = StringParser.ParseStringAndReplaceDoubleQuotes(text, _textParser.CurrentToken.Pos); - return ConstantExpressionHelper.CreateLiteral(parsedStringValue, parsedStringValue); + return _constantExpressionHelper.CreateLiteral(parsedStringValue, parsedStringValue); } private Expression ParseIntegerLiteral() diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs index 67df36325..9e33b2c70 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs @@ -9,6 +9,7 @@ namespace System.Linq.Dynamic.Core.Parser public class ExpressionPromoter : IExpressionPromoter { private readonly NumberParser _numberParser; + private readonly ConstantExpressionHelper _constantExpressionHelper; /// /// Initializes a new instance of the class. @@ -17,6 +18,7 @@ public class ExpressionPromoter : IExpressionPromoter public ExpressionPromoter(ParsingConfig config) { _numberParser = new NumberParser(config); + _constantExpressionHelper = new ConstantExpressionHelper(config); } /// @@ -48,7 +50,7 @@ public ExpressionPromoter(ParsingConfig config) } else { - if (ConstantExpressionHelper.TryGetText(ce, out var text)) + if (_constantExpressionHelper.TryGetText(ce, out var text)) { Type target = TypeHelper.GetNonNullableType(type); object? value = null; diff --git a/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs b/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs index 791e506c3..d4cc40c45 100644 --- a/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs @@ -16,6 +16,7 @@ public class NumberParser private static readonly char[] Qualifiers = { 'U', 'u', 'L', 'l', 'F', 'f', 'D', 'd', 'M', 'm' }; private static readonly char[] QualifiersHex = { 'U', 'u', 'L', 'l' }; private static readonly string[] QualifiersReal = { "F", "f", "D", "d", "M", "m" }; + private ConstantExpressionHelper _constantExpressionHelper; private readonly CultureInfo _culture; @@ -26,6 +27,7 @@ public class NumberParser public NumberParser(ParsingConfig? config) { _culture = config?.NumberParseCulture ?? CultureInfo.InvariantCulture; + _constantExpressionHelper = new ConstantExpressionHelper(config ?? ParsingConfig.Default); } /// @@ -77,12 +79,12 @@ public Expression ParseIntegerLiteral(int tokenPosition, string text) { if (qualifier == "U" || qualifier == "u") { - return ConstantExpressionHelper.CreateLiteral((uint)unsignedValue, text); + return _constantExpressionHelper.CreateLiteral((uint)unsignedValue, text); } if (qualifier == "L" || qualifier == "l") { - return ConstantExpressionHelper.CreateLiteral((long)unsignedValue, text); + return _constantExpressionHelper.CreateLiteral((long)unsignedValue, text); } if (QualifiersReal.Contains(qualifier)) @@ -90,25 +92,25 @@ public Expression ParseIntegerLiteral(int tokenPosition, string text) return ParseRealLiteral(text, qualifier[0], false); } - return ConstantExpressionHelper.CreateLiteral(unsignedValue, text); + return _constantExpressionHelper.CreateLiteral(unsignedValue, text); } if (unsignedValue <= int.MaxValue) { - return ConstantExpressionHelper.CreateLiteral((int)unsignedValue, text); + return _constantExpressionHelper.CreateLiteral((int)unsignedValue, text); } if (unsignedValue <= uint.MaxValue) { - return ConstantExpressionHelper.CreateLiteral((uint)unsignedValue, text); + return _constantExpressionHelper.CreateLiteral((uint)unsignedValue, text); } if (unsignedValue <= long.MaxValue) { - return ConstantExpressionHelper.CreateLiteral((long)unsignedValue, text); + return _constantExpressionHelper.CreateLiteral((long)unsignedValue, text); } - return ConstantExpressionHelper.CreateLiteral(unsignedValue, text); + return _constantExpressionHelper.CreateLiteral(unsignedValue, text); } if (isHexadecimal || isBinary) @@ -135,7 +137,7 @@ public Expression ParseIntegerLiteral(int tokenPosition, string text) { if (qualifier == "L" || qualifier == "l") { - return ConstantExpressionHelper.CreateLiteral(value, text); + return _constantExpressionHelper.CreateLiteral(value, text); } if (QualifiersReal.Contains(qualifier)) @@ -148,10 +150,10 @@ public Expression ParseIntegerLiteral(int tokenPosition, string text) if (value <= int.MaxValue) { - return ConstantExpressionHelper.CreateLiteral((int)value, text); + return _constantExpressionHelper.CreateLiteral((int)value, text); } - return ConstantExpressionHelper.CreateLiteral(value, text); + return _constantExpressionHelper.CreateLiteral(value, text); } /// @@ -163,18 +165,18 @@ public Expression ParseRealLiteral(string text, char qualifier, bool stripQualif { case 'f': case 'F': - return ConstantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(float))!, text); + return _constantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(float))!, text); case 'm': case 'M': - return ConstantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(decimal))!, text); + return _constantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(decimal))!, text); case 'd': case 'D': - return ConstantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(double))!, text); + return _constantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(double))!, text); default: - return ConstantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(double))!, text); + return _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(double))!, text); } } @@ -285,12 +287,12 @@ private Expression ParseAsBinary(int tokenPosition, string text, bool isNegative { if (RegexBinary32.IsMatch(text)) { - return ConstantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt32(text, 2), text); + return _constantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt32(text, 2), text); } if (RegexBinary64.IsMatch(text)) { - return ConstantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt64(text, 2), text); + return _constantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt64(text, 2), text); } throw new ParseException(string.Format(_culture, Res.InvalidBinaryIntegerLiteral, text), tokenPosition); diff --git a/src/System.Linq.Dynamic.Core/ParsingConfig.cs b/src/System.Linq.Dynamic.Core/ParsingConfig.cs index 157aadd02..c1091ea4e 100644 --- a/src/System.Linq.Dynamic.Core/ParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core/ParsingConfig.cs @@ -234,4 +234,27 @@ public IQueryableAnalyzer QueryableAnalyzer /// Default value is false. /// public bool DisallowNewKeyword { get; set; } = false; + + /// + /// Sets a Time-To-Live (TTL) for items in the constant expression cache to prevent uncontrolled growth. + /// Items not accessed within this TTL will be expired, allowing garbage collection to reclaim the memory. + /// Default is 10 minutes. + /// + public TimeSpan ConstantExpressionSlidingCacheTimeToLive { get; set; } = TimeSpan.FromMinutes(10); + + + /// + /// Configures the minimum number of items required in the constant expression cache before triggering cleanup. + /// This prevents frequent cleanups, especially in caches with few items. + /// A default value of null implies that cleanup is always allowed to run, helping in timely removal of unused cache items. + /// + public int? ConstantExpressionSlidingCacheMinItemsTrigger { get; set; } = null; + + + /// + /// Sets the frequency for running the cleanup process in the Constant Expression cache. + /// By default, cleanup occurs every 10 minutes. + /// + public TimeSpan ConstantExpressionSlidingCacheCleanupFrequency { get; set; } = TimeSpan.FromMinutes(10); + } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Util/Cache/CacheContainer.cs b/src/System.Linq.Dynamic.Core/Util/Cache/CacheContainer.cs new file mode 100644 index 000000000..93e1fe2d7 --- /dev/null +++ b/src/System.Linq.Dynamic.Core/Util/Cache/CacheContainer.cs @@ -0,0 +1,14 @@ +namespace System.Linq.Dynamic.Core.Util.Cache; + +internal struct CacheContainer where TValue : notnull +{ + public CacheContainer(TValue value, DateTime expirationTime) + { + Value = value; + ExpirationTime = expirationTime; + } + + public TValue Value { get; } + + public DateTime ExpirationTime { get; } +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Util/Cache/ThreadSafeSlidingCache.cs b/src/System.Linq.Dynamic.Core/Util/Cache/ThreadSafeSlidingCache.cs new file mode 100644 index 000000000..74064e7e2 --- /dev/null +++ b/src/System.Linq.Dynamic.Core/Util/Cache/ThreadSafeSlidingCache.cs @@ -0,0 +1,135 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Dynamic.Core.Validation; + +namespace System.Linq.Dynamic.Core.Util.Cache; + +internal class ThreadSafeSlidingCache where TKey : notnull where TValue : notnull +{ + private readonly ConcurrentDictionary> _cache; + private readonly TimeSpan _cleanupFrequency; + private readonly IDateTimeUtils _dateTimeProvider; + private readonly Action _deleteExpiredCachedItemsDelegate; + private readonly long? _minCacheItemsBeforeCleanup; + private DateTime _lastCleanupTime; + + /// + /// Sliding Thread Safe Cache + /// + /// The length of time any object would survive before being removed + /// Only look for expired objects over specific periods + /// + /// If defined, only allow the cleanup process after x number of cached items have + /// been stored + /// + /// + /// Provides the Time for the Caching object. Default will be created if not supplied. Used + /// for Testing classes + /// + public ThreadSafeSlidingCache( + TimeSpan timeToLive, + TimeSpan? cleanupFrequency = null, + long? minCacheItemsBeforeCleanup = null, + IDateTimeUtils? dateTimeProvider = null) + { + _cache = new ConcurrentDictionary>(); + TimeToLive = timeToLive; + _minCacheItemsBeforeCleanup = minCacheItemsBeforeCleanup; + _cleanupFrequency = cleanupFrequency ?? ThreadSafeSlidingCacheConstants.DefaultCleanupFrequency; + _deleteExpiredCachedItemsDelegate = Cleanup; + _dateTimeProvider = dateTimeProvider ?? new DateTimeUtils(); + // To prevent a scan on first call, set the last Cleanup to the current Provider time + _lastCleanupTime = _dateTimeProvider.UtcNow; + } + + /// + /// Cache TTL value + /// + public TimeSpan TimeToLive { get; } + + /// + /// Provide the number of items in the cache + /// + public int Count => _cache.Count; + + /// + /// Add or update the item in the cache, at the same time update the expiration time + /// + /// + /// + public void AddOrUpdate(TKey key, TValue value) + { + Check.NotNull(key); + Check.NotNull(value); + + var expirationTime = _dateTimeProvider.UtcNow.Add(TimeToLive); + _cache[key] = new CacheContainer(value, expirationTime); + + CleanupIfNeeded(); + } + + /// + /// Attempt to get the value from the cache. This will extend the cache expiration time if the item is found + /// + /// Key + /// Value + /// + public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value) + { + Check.NotNull(key); + + CleanupIfNeeded(); + + if (_cache.TryGetValue(key, out var valueAndExpiration)) + { + if (_dateTimeProvider.UtcNow <= valueAndExpiration.ExpirationTime) + { + value = valueAndExpiration.Value; + var newExpire = _dateTimeProvider.UtcNow.Add(TimeToLive); + _cache[key] = new CacheContainer(value, newExpire); + return true; + } + + // Remove expired item + _cache.TryRemove(key, out _); + } + + value = default; + return false; + } + + public bool Remove(TKey key) + { + Check.NotNull(key); + + var removed = _cache.TryRemove(key, out _); + CleanupIfNeeded(); + return removed; + } + + private void CleanupIfNeeded() + { + if (_dateTimeProvider.UtcNow - _lastCleanupTime > _cleanupFrequency + && (_minCacheItemsBeforeCleanup == null || + _cache.Count >= + _minCacheItemsBeforeCleanup) // Only cleanup if we have a minimum number of items in the cache. + ) + { + // Set here, so we don't have re-entry due to large collection enumeration. + _lastCleanupTime = _dateTimeProvider.UtcNow; + + TaskUtils.Run(_deleteExpiredCachedItemsDelegate); + } + } + + private void Cleanup() + { + foreach (var key in _cache.Keys) + { + if (_dateTimeProvider.UtcNow > _cache[key].ExpirationTime) + { + _cache.TryRemove(key, out _); + } + } + } +} diff --git a/src/System.Linq.Dynamic.Core/Util/Cache/ThreadSafeSlidingCacheConstants.cs b/src/System.Linq.Dynamic.Core/Util/Cache/ThreadSafeSlidingCacheConstants.cs new file mode 100644 index 000000000..517c4548b --- /dev/null +++ b/src/System.Linq.Dynamic.Core/Util/Cache/ThreadSafeSlidingCacheConstants.cs @@ -0,0 +1,7 @@ +namespace System.Linq.Dynamic.Core.Util.Cache; + +internal static class ThreadSafeSlidingCacheConstants +{ + // Default cleanup frequency + public static readonly TimeSpan DefaultCleanupFrequency = TimeSpan.FromMinutes(10); +} diff --git a/src/System.Linq.Dynamic.Core/Util/TaskUtils.cs b/src/System.Linq.Dynamic.Core/Util/TaskUtils.cs new file mode 100644 index 000000000..48a734539 --- /dev/null +++ b/src/System.Linq.Dynamic.Core/Util/TaskUtils.cs @@ -0,0 +1,16 @@ +namespace System.Linq.Dynamic.Core.Util; + +internal static class TaskUtils +{ + public static void Run(Action action) + { +#if NET35 || NET40 + System.Threading.ThreadPool.QueueUserWorkItem(_ => { + action?.Invoke(); + }); +#else + System.Threading.Tasks.Task.Run(action); +#endif + } +} + diff --git a/src/System.Linq.Dynamic.Core/Util/ThreadSafeSlidingCache.cs b/src/System.Linq.Dynamic.Core/Util/ThreadSafeSlidingCache.cs deleted file mode 100644 index 4391b1a0d..000000000 --- a/src/System.Linq.Dynamic.Core/Util/ThreadSafeSlidingCache.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System.Collections.Concurrent; -using System.Linq.Dynamic.Core.Validation; -using System.Threading.Tasks; - -namespace System.Linq.Dynamic.Core.Util -{ - internal static class ThreadSafeSlidingCacheConstants - { - // Default cleanup frequency - public static readonly TimeSpan DefaultCleanupFrequency = TimeSpan.FromMinutes(10); - } - - internal class ThreadSafeSlidingCache where TKey : notnull where TValue : notnull - { - private readonly ConcurrentDictionary _cache; - private readonly TimeSpan _cleanupFrequency; - private readonly IDateTimeUtils _dateTimeProvider; - private readonly Func _deleteExpiredCachedItemsDelegate; - private readonly long? _minCacheItemsBeforeCleanup; - private DateTime _lastCleanupTime = DateTime.MinValue; - - /// - /// Sliding Thread Safe Cache - /// - /// The length of time any object would survive before being removed - /// Only look for expired objects over specific periods - /// - /// If defined, only allow the cleanup process after x number of cached items have - /// been stored - /// - /// - /// Provides the Time for the Caching object. Default will be created if not supplied. Used - /// for Testing classes - /// - public ThreadSafeSlidingCache( - TimeSpan timeToLive, - TimeSpan? cleanupFrequency = null, - long? minCacheItemsBeforeCleanup = null, - IDateTimeUtils? dateTimeProvider = null) - { - _cache = new ConcurrentDictionary(); - TimeToLive = timeToLive; - _minCacheItemsBeforeCleanup = minCacheItemsBeforeCleanup; - _cleanupFrequency = cleanupFrequency ?? ThreadSafeSlidingCacheConstants.DefaultCleanupFrequency; - _deleteExpiredCachedItemsDelegate = Cleanup; - _dateTimeProvider = dateTimeProvider ?? new DateTimeUtils(); - } - - public TimeSpan TimeToLive { get; } - - /// - /// Provide the number of items in the cache - /// - public int Count => _cache.Count; - - public void AddOrUpdate(TKey key, TValue value) - { - Check.NotNull(key); - Check.NotNull(value); - - var expirationTime = _dateTimeProvider.UtcNow.Add(TimeToLive); - _cache[key] = (value, expirationTime); - - CleanupIfNeeded(); - } - - public bool TryGetValue(TKey key, out TValue value) - { - Check.NotNull(key); - - CleanupIfNeeded(); - - if (_cache.TryGetValue(key, out var valueAndExpiration)) - { - if (_dateTimeProvider.UtcNow <= valueAndExpiration.ExpirationTime) - { - value = valueAndExpiration.Value; - _cache[key] = (value, _dateTimeProvider.UtcNow.Add(TimeToLive)); - return true; - } - - // Remove expired item - _cache.TryRemove(key, out _); - } - - value = default!; - return false; - } - - public bool Remove(TKey key) - { - Check.NotNull(key); - - var removed = _cache.TryRemove(key, out _); - CleanupIfNeeded(); - return removed; - } - - /// - /// Check if cache needs to be cleaned up. - /// If it does, span the cleanup as a Task to prevent from blocking - /// - private void CleanupIfNeeded() - { - if (_dateTimeProvider.UtcNow - _lastCleanupTime > _cleanupFrequency - && (_minCacheItemsBeforeCleanup == null || - _cache.Count >= - _minCacheItemsBeforeCleanup) // Only cleanup if we have a minimum number of items in the cache. - ) - { - // Set here, so we don't have re-entry due to large collection enumeration. - _lastCleanupTime = _dateTimeProvider.UtcNow; - - Task.Run(_deleteExpiredCachedItemsDelegate); - } - } - - /// - /// Cleanup the Cache - /// - /// - private Task Cleanup() - { - foreach (var key in _cache.Keys) - { - if (_dateTimeProvider.UtcNow > _cache[key].ExpirationTime) - { - _cache.TryRemove(key, out _); - } - } - - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/Util/Cache/ThreadSafeSlidingCacheTests.cs b/test/System.Linq.Dynamic.Core.Tests/Util/Cache/ThreadSafeSlidingCacheTests.cs new file mode 100644 index 000000000..525363419 --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests/Util/Cache/ThreadSafeSlidingCacheTests.cs @@ -0,0 +1,151 @@ +using Moq; +using System.Linq.Dynamic.Core.Util; +using System.Linq.Dynamic.Core.Util.Cache; +using System.Linq.Expressions; +using FluentAssertions; +using Xunit; + +namespace System.Linq.Dynamic.Core.Tests.Util.Cache; + +public class ThreadSafeSlidingCacheTests +{ + private static readonly DateTime UtcNow = new(2024, 1, 1, 0, 0, 0); + + [Fact] + public void ThreadSafeSlidingCache_CacheOperations() + { + var dateTimeUtilsMock = new Mock(); + dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(UtcNow); + + // Arrange + var cache = new ThreadSafeSlidingCache( + TimeSpan.FromSeconds(1), + dateTimeProvider: dateTimeUtilsMock.Object); + + // Add + cache.AddOrUpdate(1, "one"); + cache.AddOrUpdate(2, "two"); + cache.AddOrUpdate(3, "three"); + + // Replace + cache.AddOrUpdate(1, "oneone"); + + cache.Count.Should().Be(3, $"Expected 3 items in the cache, only had {cache.Count}"); + + // Test retrieval + Assert.True(cache.TryGetValue(1, out var value1), "Expected to find the value, but did not"); + Assert.True(cache.TryGetValue(2, out var value2), "Expected to find the value, but did not"); + Assert.True(cache.TryGetValue(3, out var value3), "Expected to find the value, but did not"); + + // Test Removal + cache.Remove(1); + cache.Count.Should().Be(2, $"Expected 2 items in the cache, only had {cache.Count}"); + } + + [Fact] + public void ThreadSafeSlidingCache_TestExpire() + { + var dateTimeUtilsMock = new Mock(); + dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(UtcNow); + + var cache = new ThreadSafeSlidingCache(TimeSpan.FromMinutes(10), + dateTimeProvider: dateTimeUtilsMock.Object); + + // Act + cache.AddOrUpdate(1, "one"); + + var r = dateTimeUtilsMock.Object.UtcNow.AddMinutes(11); + dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(r); + + if (cache.TryGetValue(1, out var value)) + { + Assert.True(false, $"Expected to not find the value, but found {value}"); + } + } + + [Fact] + public void ThreadSafeSlidingCache_TestAutoExpire() + { + var dateTimeUtilsMock = new Mock(); + dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(UtcNow); + + // Arrange + var cache = new ThreadSafeSlidingCache( + TimeSpan.FromMinutes(10), + dateTimeProvider: dateTimeUtilsMock.Object); + + // Act + cache.AddOrUpdate(1, "one"); + + // Ensure one item is in the cache + cache.Count.Should().Be(1, $"Expected 1 items in the cache, only had {cache.Count}"); + + // move the time forward + var r = dateTimeUtilsMock.Object.UtcNow.AddMinutes(11); + dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(r); + + // Trigger the cleanup, asking for non-existing key + cache.TryGetValue(10, out var _); + + // Since the cache cleanup is triggered by a Task and not on the same thread, + // give it a moment for the cleanup to happen + System.Threading.Thread.Sleep(10); + + // Ensure one item is in the cache + cache.Count.Should().Be(0, $"Expected 0 items in the cache, only had {cache.Count}"); + } + + [Fact] + public void ThreadSafeSlidingCache_TestNull() + { + // Arrange + var cache = new ThreadSafeSlidingCache(TimeSpan.FromMinutes(10)); + + // Expect an ArgumentNullException + var exception = Assert.Throws(() => { cache.AddOrUpdate(null, "one"); }); + } + + [Fact] + public void ThreadSafeSlidingCache_TestMinNumberBeforeTests() + { + var dateTimeUtilsMock = new Mock(); + dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(UtcNow); + + // Arrange + var cache = new ThreadSafeSlidingCache( + TimeSpan.FromMinutes(10), + minCacheItemsBeforeCleanup: 2, + dateTimeProvider: dateTimeUtilsMock.Object); + + // Act + cache.AddOrUpdate(1, "one"); + + // Ensure one item is in the cache + cache.Count.Should().Be(1, $"Expected 1 items in the cache, only had {cache.Count}"); + + // move the time forward + var r = dateTimeUtilsMock.Object.UtcNow.AddMinutes(11); + dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(r); + + // Trigger the cleanup, asking for non-existing key + cache.TryGetValue(10, out var _); + + // Since the cache cleanup is triggered by a Task and not on the same thread, + // give it a moment for the cleanup to happen + System.Threading.Thread.Sleep(10); + + // Ensure one item is in the cache + cache.Count.Should().Be(1, $"Expected 1 items in the cache, only had {cache.Count}"); + + // Act + cache.AddOrUpdate(2, "two"); + + // Since the cache cleanup is triggered by a Task and not on the same thread, + // give it a moment for the cleanup to happen + System.Threading.Thread.Sleep(10); + + // Ensure one item is in the cache + cache.Count.Should().Be(1, $"Expected 1 items in the cache, had {cache.Count}"); + } + +} diff --git a/test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs b/test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs deleted file mode 100644 index d6b342b78..000000000 --- a/test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Linq.Dynamic.Core.Parser; -using System.Threading.Tasks; -using Xunit; - -namespace System.Linq.Dynamic.Core.Tests -{ - public partial class EntitiesTests - { - //[Fact] - //public async Task TestConstantExpressionLeak() - //{ - // //Arrange - // PopulateTestData(1, 0); - - // var populateExpression = _context.Blogs.All("BlogId > 2000"); - - // var expressions = ConstantExpressionHelper.Expressions; - - // // Should contain - // if (!expressions.TryGetValue(2000, out _)) - // { - // Assert.Fail("Cache was missing constant expression for 2000"); - // } - - // // wait half the expiry time - // await Task.Delay(TimeSpan.FromSeconds(ConstantExpressionHelper.Expressions.TimeToLive.TotalSeconds/2)); - // if (!expressions.TryGetValue(2000, out _)) - // { - // Assert.Fail("Cache was missing constant expression for 2000 (1)"); - // } - - // // wait another half the expiry time, plus one second - // await Task.Delay(TimeSpan.FromSeconds((ConstantExpressionHelper.Expressions.TimeToLive.TotalSeconds / 2)+1)); - // if (!expressions.TryGetValue(2000, out _)) - // { - // Assert.Fail("Cache was missing constant expression for 2000 (2)"); - // } - - // // Wait for the slide cache to expire, check on second later - // await Task.Delay(ConstantExpressionHelper.Expressions.TimeToLive.Add(TimeSpan.FromSeconds(1))); - - // if (expressions.TryGetValue(2000, out _)) - // { - // Assert.Fail("Expected constant to be expired 2000"); - // } - //} - } -} diff --git a/test/System.Linq.Dynamic.Core.Tests/Util/ThreadSafeSlidingCacheTests.cs b/test/System.Linq.Dynamic.Core.Tests/Util/ThreadSafeSlidingCacheTests.cs deleted file mode 100644 index ca610bd82..000000000 --- a/test/System.Linq.Dynamic.Core.Tests/Util/ThreadSafeSlidingCacheTests.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System.Linq.Dynamic.Core.Util; -using System.Linq.Expressions; -using System.Threading.Tasks; -using Xunit; - -namespace System.Linq.Dynamic.Core.Tests.Util -{ - public class ThreadSafeSlidingCacheTests - { - [Fact] - public void ThreadSafeSlidingCache_CacheOperations() - { - var mockDateTime = new MockDateTimeProvider(); - - // Arrange - var cache = new ThreadSafeSlidingCache( - TimeSpan.FromSeconds(1), - dateTimeProvider: mockDateTime); - - // Add - cache.AddOrUpdate(1, "one"); - cache.AddOrUpdate(2, "two"); - cache.AddOrUpdate(3, "three"); - - // Replace - cache.AddOrUpdate(1, "oneone"); - - Assert.True(cache.Count == 3, $"Expected 3 items in the cache, only had {cache.Count}"); - - - // Test retrieval - Assert.True(cache.TryGetValue(1, out var value1), $"Expected to find the value, but did not"); - Assert.True(cache.TryGetValue(2, out var value2), $"Expected to find the value, but did not"); - Assert.True(cache.TryGetValue(3, out var value3), $"Expected to find the value, but did not"); - - // Test Removal - cache.Remove(1); - Assert.True(cache.Count == 2, $"Expected 2 items in the cache, only had {cache.Count}"); - - } - - [Fact] - public void ThreadSafeSlidingCache_TestExpire() - { - var mockDateTime = new MockDateTimeProvider(); - - // Arrange - var cache = new ThreadSafeSlidingCache(TimeSpan.FromMinutes(10), - dateTimeProvider: mockDateTime); - - // Act - cache.AddOrUpdate(1, "one"); - mockDateTime.UtcNow = mockDateTime.UtcNow.AddMinutes(11); - if (cache.TryGetValue(1, out var value)) - { - Assert.True(false, $"Expected to not find the value, but found {value}"); - } - - } - - [Fact] - public async Task ThreadSafeSlidingCache_TestAutoExpire() - { - var mockDateTime = new MockDateTimeProvider(); - - // Arrange - var cache = new ThreadSafeSlidingCache(TimeSpan.FromMinutes(10), - dateTimeProvider: mockDateTime); - - // Act - cache.AddOrUpdate(1, "one"); - - // Ensure one item is in the cache - Assert.True(cache.Count == 1, $"Expected 1 items in the cache, only had {cache.Count}"); - - // move the time forward - mockDateTime.UtcNow = mockDateTime.UtcNow.AddMinutes(11); - - // Trigger the cleanup, asking for non-existing key - cache.TryGetValue(10, out var _); - - // Since the cache cleanup is triggered by a Task and not on the same thread, - // give it a moment for the cleanup to happen - await Task.Delay(10); - - // Ensure one item is in the cache - Assert.True(cache.Count == 0, $"Expected 0 items in the cache, only had {cache.Count}"); - - } - - [Fact] - public async Task ThreadSafeSlidingCache_TestNull() - { - // Arrange - var cache = new ThreadSafeSlidingCache(TimeSpan.FromMinutes(10)); - - // Expect an ArgumentNullException - var exception = Assert.Throws(() => { - cache.AddOrUpdate(null, "one"); - }); - - } - - [Fact] - public async Task ThreadSafeSlidingCache_TestMinNumberBeforeTests() - { - // Arrange - var mockDateTime = new MockDateTimeProvider(); - - // Arrange - var cache = new ThreadSafeSlidingCache( - TimeSpan.FromMinutes(10), - minCacheItemsBeforeCleanup: 2, - dateTimeProvider: mockDateTime); - - // Act - cache.AddOrUpdate(1, "one"); - - // Ensure one item is in the cache - Assert.True(cache.Count == 1, $"Expected 1 items in the cache, only had {cache.Count}"); - - // move the time forward - mockDateTime.UtcNow = mockDateTime.UtcNow.AddMinutes(11); - - // Trigger the cleanup, asking for non-existing key - cache.TryGetValue(10, out var _); - - // Since the cache cleanup is triggered by a Task and not on the same thread, - // give it a moment for the cleanup to happen - await Task.Delay(10); - - // Ensure one item is in the cache - Assert.True(cache.Count == 1, $"Expected 1 items in the cache, only had {cache.Count}"); - - // Act - cache.AddOrUpdate(2, "two"); - - // Since the cache cleanup is triggered by a Task and not on the same thread, - // give it a moment for the cleanup to happen - await Task.Delay(10); - - // Ensure one item is in the cache - Assert.True(cache.Count == 1, $"Expected 1 items in the cache, had {cache.Count}"); - } - - private class MockDateTimeProvider : IDateTimeUtils - { - public DateTime UtcNow { get; set; } = DateTime.UtcNow; - - } - } -} From cc8482ad6a371599de8f79935dc2ba7b3513e85a Mon Sep 17 00:00:00 2001 From: Travis Whidden Date: Mon, 22 Jan 2024 06:43:51 -0800 Subject: [PATCH 09/11] #764 - ConstantExpressionHelper Singleton Factory; Code Review Resolutions --- .../Parser/ConstantExpressionHelper.cs | 44 +++++++------------ .../Parser/ConstantExpressionHelperFactory.cs | 20 +++++++++ .../Parser/ExpressionParser.cs | 2 +- .../Parser/ExpressionPromoter.cs | 2 +- .../Parser/NumberParser.cs | 4 +- src/System.Linq.Dynamic.Core/ParsingConfig.cs | 23 ++-------- .../Util/Cache/CacheConfig.cs | 29 ++++++++++++ .../Util/Cache/CacheContainer.cs | 8 ++-- .../Util/TaskUtils.cs | 9 ++-- .../Util/Cache/ThreadSafeSlidingCacheTests.cs | 12 ++--- 10 files changed, 88 insertions(+), 65 deletions(-) create mode 100644 src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelperFactory.cs create mode 100644 src/System.Linq.Dynamic.Core/Util/Cache/CacheConfig.cs diff --git a/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs index 66be39913..a76c3bf10 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs @@ -1,59 +1,49 @@ using System.Linq.Dynamic.Core.Util.Cache; +using System.Linq.Dynamic.Core.Validation; using System.Linq.Expressions; namespace System.Linq.Dynamic.Core.Parser; internal class ConstantExpressionHelper { - private readonly ParsingConfig _config; - // Static shared instance to prevent duplications of the same objects - private static ThreadSafeSlidingCache? _expressions; - private static ThreadSafeSlidingCache? _literals; + private readonly ThreadSafeSlidingCache _expressions; + private readonly ThreadSafeSlidingCache _literals; public ConstantExpressionHelper(ParsingConfig config) { - _config = config; - - } + var parsingConfig = Check.NotNull(config); + var cacheConfig = Check.NotNull(parsingConfig.ConstantExpressionCacheConfig); - private ThreadSafeSlidingCache GetLiterals() - { - _literals ??= new ThreadSafeSlidingCache( - _config.ConstantExpressionSlidingCacheTimeToLive, - _config.ConstantExpressionSlidingCacheCleanupFrequency, - _config.ConstantExpressionSlidingCacheMinItemsTrigger + _literals = new ThreadSafeSlidingCache( + cacheConfig.TimeToLive, + cacheConfig.CleanupFrequency, + cacheConfig.MinItemsTrigger ); - return _literals; - } - private ThreadSafeSlidingCache GetExpression() - { - _expressions ??= new ThreadSafeSlidingCache( - _config.ConstantExpressionSlidingCacheTimeToLive, - _config.ConstantExpressionSlidingCacheCleanupFrequency, - _config.ConstantExpressionSlidingCacheMinItemsTrigger + _expressions = new ThreadSafeSlidingCache( + cacheConfig.TimeToLive, + cacheConfig.CleanupFrequency, + cacheConfig.MinItemsTrigger ); - return _expressions; } - public bool TryGetText(Expression expression, out string? text) { - return GetLiterals().TryGetValue(expression, out text); + return _literals.TryGetValue(expression, out text); } public Expression CreateLiteral(object value, string text) { - if (GetExpression().TryGetValue(value, out var outputValue)) + if (_expressions.TryGetValue(value, out var outputValue)) { return outputValue; } var constantExpression = Expression.Constant(value); - GetExpression().AddOrUpdate(value, constantExpression); - GetLiterals().AddOrUpdate(constantExpression, text); + _expressions.AddOrUpdate(value, constantExpression); + _literals.AddOrUpdate(constantExpression, text); return constantExpression; } diff --git a/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelperFactory.cs b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelperFactory.cs new file mode 100644 index 000000000..4a3b7ec31 --- /dev/null +++ b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelperFactory.cs @@ -0,0 +1,20 @@ +namespace System.Linq.Dynamic.Core.Parser; + +internal static class ConstantExpressionHelperFactory +{ + private static readonly object Lock = new(); + private static ConstantExpressionHelper? _instance; + + public static ConstantExpressionHelper GetInstance(ParsingConfig config) + { + if (_instance == null) + { + lock (Lock) + { + _instance ??= new ConstantExpressionHelper(config); + } + } + + return _instance; + } +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 788ea9362..74240e615 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -82,7 +82,7 @@ public ExpressionParser(ParameterExpression[]? parameters, string expression, ob _methodFinder = new MethodFinder(_parsingConfig, _expressionHelper); _typeFinder = new TypeFinder(_parsingConfig, _keywordsHelper); _typeConverterFactory = new TypeConverterFactory(_parsingConfig); - _constantExpressionHelper = new ConstantExpressionHelper(_parsingConfig); + _constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(_parsingConfig); if (parameters != null) { diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs index 9e33b2c70..6c12ad7fa 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs @@ -18,7 +18,7 @@ public class ExpressionPromoter : IExpressionPromoter public ExpressionPromoter(ParsingConfig config) { _numberParser = new NumberParser(config); - _constantExpressionHelper = new ConstantExpressionHelper(config); + _constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(config); } /// diff --git a/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs b/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs index d4cc40c45..c8d7c5d1a 100644 --- a/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs @@ -16,7 +16,7 @@ public class NumberParser private static readonly char[] Qualifiers = { 'U', 'u', 'L', 'l', 'F', 'f', 'D', 'd', 'M', 'm' }; private static readonly char[] QualifiersHex = { 'U', 'u', 'L', 'l' }; private static readonly string[] QualifiersReal = { "F", "f", "D", "d", "M", "m" }; - private ConstantExpressionHelper _constantExpressionHelper; + private readonly ConstantExpressionHelper _constantExpressionHelper; private readonly CultureInfo _culture; @@ -27,7 +27,7 @@ public class NumberParser public NumberParser(ParsingConfig? config) { _culture = config?.NumberParseCulture ?? CultureInfo.InvariantCulture; - _constantExpressionHelper = new ConstantExpressionHelper(config ?? ParsingConfig.Default); + _constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(config ?? ParsingConfig.Default); } /// diff --git a/src/System.Linq.Dynamic.Core/ParsingConfig.cs b/src/System.Linq.Dynamic.Core/ParsingConfig.cs index c1091ea4e..0e1a51229 100644 --- a/src/System.Linq.Dynamic.Core/ParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core/ParsingConfig.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Linq.Dynamic.Core.CustomTypeProviders; using System.Linq.Dynamic.Core.Parser; +using System.Linq.Dynamic.Core.Util.Cache; namespace System.Linq.Dynamic.Core; @@ -236,25 +237,7 @@ public IQueryableAnalyzer QueryableAnalyzer public bool DisallowNewKeyword { get; set; } = false; /// - /// Sets a Time-To-Live (TTL) for items in the constant expression cache to prevent uncontrolled growth. - /// Items not accessed within this TTL will be expired, allowing garbage collection to reclaim the memory. - /// Default is 10 minutes. + /// Caches constant expressions to enhance performance. Periodic cleanup is performed to manage cache size, governed by this configuration. /// - public TimeSpan ConstantExpressionSlidingCacheTimeToLive { get; set; } = TimeSpan.FromMinutes(10); - - - /// - /// Configures the minimum number of items required in the constant expression cache before triggering cleanup. - /// This prevents frequent cleanups, especially in caches with few items. - /// A default value of null implies that cleanup is always allowed to run, helping in timely removal of unused cache items. - /// - public int? ConstantExpressionSlidingCacheMinItemsTrigger { get; set; } = null; - - - /// - /// Sets the frequency for running the cleanup process in the Constant Expression cache. - /// By default, cleanup occurs every 10 minutes. - /// - public TimeSpan ConstantExpressionSlidingCacheCleanupFrequency { get; set; } = TimeSpan.FromMinutes(10); - + public CacheConfig ConstantExpressionCacheConfig { get; set; } = new CacheConfig(); } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Util/Cache/CacheConfig.cs b/src/System.Linq.Dynamic.Core/Util/Cache/CacheConfig.cs new file mode 100644 index 000000000..ee9c8817e --- /dev/null +++ b/src/System.Linq.Dynamic.Core/Util/Cache/CacheConfig.cs @@ -0,0 +1,29 @@ +namespace System.Linq.Dynamic.Core.Util.Cache; + +/// +/// Cache Configuration Options +/// +public class CacheConfig +{ + /// + /// Sets a Time-To-Live (TTL) for items in the constant expression cache to prevent uncontrolled growth. + /// Items not accessed within this TTL will be expired, allowing garbage collection to reclaim the memory. + /// Default is 10 minutes. + /// + public TimeSpan TimeToLive { get; set; } = TimeSpan.FromMinutes(10); + + + /// + /// Configures the minimum number of items required in the constant expression cache before triggering cleanup. + /// This prevents frequent cleanups, especially in caches with few items. + /// A default value of null implies that cleanup is always allowed to run, helping in timely removal of unused cache items. + /// + public int? MinItemsTrigger { get; set; } = null; + + + /// + /// Sets the frequency for running the cleanup process in the Constant Expression cache. + /// By default, cleanup occurs every 10 minutes. + /// + public TimeSpan CleanupFrequency { get; set; } = TimeSpan.FromMinutes(10); +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Util/Cache/CacheContainer.cs b/src/System.Linq.Dynamic.Core/Util/Cache/CacheContainer.cs index 93e1fe2d7..6807681c3 100644 --- a/src/System.Linq.Dynamic.Core/Util/Cache/CacheContainer.cs +++ b/src/System.Linq.Dynamic.Core/Util/Cache/CacheContainer.cs @@ -2,13 +2,13 @@ internal struct CacheContainer where TValue : notnull { + public TValue Value { get; } + + public DateTime ExpirationTime { get; } + public CacheContainer(TValue value, DateTime expirationTime) { Value = value; ExpirationTime = expirationTime; } - - public TValue Value { get; } - - public DateTime ExpirationTime { get; } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Util/TaskUtils.cs b/src/System.Linq.Dynamic.Core/Util/TaskUtils.cs index 48a734539..0654424dc 100644 --- a/src/System.Linq.Dynamic.Core/Util/TaskUtils.cs +++ b/src/System.Linq.Dynamic.Core/Util/TaskUtils.cs @@ -1,13 +1,14 @@ -namespace System.Linq.Dynamic.Core.Util; +using System.Linq.Dynamic.Core.Validation; + +namespace System.Linq.Dynamic.Core.Util; internal static class TaskUtils { public static void Run(Action action) { + Check.NotNull(action); #if NET35 || NET40 - System.Threading.ThreadPool.QueueUserWorkItem(_ => { - action?.Invoke(); - }); + System.Threading.ThreadPool.QueueUserWorkItem(_ => action.Invoke()); #else System.Threading.Tasks.Task.Run(action); #endif diff --git a/test/System.Linq.Dynamic.Core.Tests/Util/Cache/ThreadSafeSlidingCacheTests.cs b/test/System.Linq.Dynamic.Core.Tests/Util/Cache/ThreadSafeSlidingCacheTests.cs index 525363419..14bac8069 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Util/Cache/ThreadSafeSlidingCacheTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Util/Cache/ThreadSafeSlidingCacheTests.cs @@ -54,8 +54,8 @@ public void ThreadSafeSlidingCache_TestExpire() // Act cache.AddOrUpdate(1, "one"); - var r = dateTimeUtilsMock.Object.UtcNow.AddMinutes(11); - dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(r); + var newDateTime = dateTimeUtilsMock.Object.UtcNow.AddMinutes(11); + dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(newDateTime); if (cache.TryGetValue(1, out var value)) { @@ -81,8 +81,8 @@ public void ThreadSafeSlidingCache_TestAutoExpire() cache.Count.Should().Be(1, $"Expected 1 items in the cache, only had {cache.Count}"); // move the time forward - var r = dateTimeUtilsMock.Object.UtcNow.AddMinutes(11); - dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(r); + var newDateTime = dateTimeUtilsMock.Object.UtcNow.AddMinutes(11); + dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(newDateTime); // Trigger the cleanup, asking for non-existing key cache.TryGetValue(10, out var _); @@ -124,8 +124,8 @@ public void ThreadSafeSlidingCache_TestMinNumberBeforeTests() cache.Count.Should().Be(1, $"Expected 1 items in the cache, only had {cache.Count}"); // move the time forward - var r = dateTimeUtilsMock.Object.UtcNow.AddMinutes(11); - dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(r); + var newDateTime = dateTimeUtilsMock.Object.UtcNow.AddMinutes(11); + dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(newDateTime); // Trigger the cleanup, asking for non-existing key cache.TryGetValue(10, out var _); From 8d9b6bfc8471a1237df6c66f4595fa224c9eb411 Mon Sep 17 00:00:00 2001 From: Travis Whidden Date: Mon, 22 Jan 2024 10:24:04 -0800 Subject: [PATCH 10/11] #764 - Refactor Naming for SlidingCache; --- .../Parser/ConstantExpressionHelper.cs | 17 +++---------- ...eadSafeSlidingCache.cs => SlidingCache.cs} | 25 ++++++++++++++++--- ...eConstants.cs => SlidingCacheConstants.cs} | 2 +- ...dingCacheTests.cs => SlidingCacheTests.cs} | 12 ++++----- 4 files changed, 33 insertions(+), 23 deletions(-) rename src/System.Linq.Dynamic.Core/Util/Cache/{ThreadSafeSlidingCache.cs => SlidingCache.cs} (81%) rename src/System.Linq.Dynamic.Core/Util/Cache/{ThreadSafeSlidingCacheConstants.cs => SlidingCacheConstants.cs} (76%) rename test/System.Linq.Dynamic.Core.Tests/Util/Cache/{ThreadSafeSlidingCacheTests.cs => SlidingCacheTests.cs} (92%) diff --git a/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs index a76c3bf10..13d4ecf09 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs @@ -7,25 +7,16 @@ namespace System.Linq.Dynamic.Core.Parser; internal class ConstantExpressionHelper { // Static shared instance to prevent duplications of the same objects - private readonly ThreadSafeSlidingCache _expressions; - private readonly ThreadSafeSlidingCache _literals; + private readonly SlidingCache _expressions; + private readonly SlidingCache _literals; public ConstantExpressionHelper(ParsingConfig config) { var parsingConfig = Check.NotNull(config); var cacheConfig = Check.NotNull(parsingConfig.ConstantExpressionCacheConfig); - _literals = new ThreadSafeSlidingCache( - cacheConfig.TimeToLive, - cacheConfig.CleanupFrequency, - cacheConfig.MinItemsTrigger - ); - - _expressions = new ThreadSafeSlidingCache( - cacheConfig.TimeToLive, - cacheConfig.CleanupFrequency, - cacheConfig.MinItemsTrigger - ); + _literals = new SlidingCache(cacheConfig); + _expressions = new SlidingCache(cacheConfig); } public bool TryGetText(Expression expression, out string? text) diff --git a/src/System.Linq.Dynamic.Core/Util/Cache/ThreadSafeSlidingCache.cs b/src/System.Linq.Dynamic.Core/Util/Cache/SlidingCache.cs similarity index 81% rename from src/System.Linq.Dynamic.Core/Util/Cache/ThreadSafeSlidingCache.cs rename to src/System.Linq.Dynamic.Core/Util/Cache/SlidingCache.cs index 74064e7e2..42f8311da 100644 --- a/src/System.Linq.Dynamic.Core/Util/Cache/ThreadSafeSlidingCache.cs +++ b/src/System.Linq.Dynamic.Core/Util/Cache/SlidingCache.cs @@ -4,7 +4,7 @@ namespace System.Linq.Dynamic.Core.Util.Cache; -internal class ThreadSafeSlidingCache where TKey : notnull where TValue : notnull +internal class SlidingCache where TKey : notnull where TValue : notnull { private readonly ConcurrentDictionary> _cache; private readonly TimeSpan _cleanupFrequency; @@ -26,7 +26,7 @@ internal class ThreadSafeSlidingCache where TKey : notnull where T /// Provides the Time for the Caching object. Default will be created if not supplied. Used /// for Testing classes /// - public ThreadSafeSlidingCache( + public SlidingCache( TimeSpan timeToLive, TimeSpan? cleanupFrequency = null, long? minCacheItemsBeforeCleanup = null, @@ -35,7 +35,26 @@ public ThreadSafeSlidingCache( _cache = new ConcurrentDictionary>(); TimeToLive = timeToLive; _minCacheItemsBeforeCleanup = minCacheItemsBeforeCleanup; - _cleanupFrequency = cleanupFrequency ?? ThreadSafeSlidingCacheConstants.DefaultCleanupFrequency; + _cleanupFrequency = cleanupFrequency ?? SlidingCacheConstants.DefaultCleanupFrequency; + _deleteExpiredCachedItemsDelegate = Cleanup; + _dateTimeProvider = dateTimeProvider ?? new DateTimeUtils(); + // To prevent a scan on first call, set the last Cleanup to the current Provider time + _lastCleanupTime = _dateTimeProvider.UtcNow; + } + + /// + /// Sliding Thread Safe Cache + /// + /// + /// + public SlidingCache( + CacheConfig cashConfig, + IDateTimeUtils? dateTimeProvider = null) + { + _cache = new ConcurrentDictionary>(); + TimeToLive = cashConfig.TimeToLive; + _minCacheItemsBeforeCleanup = cashConfig.MinItemsTrigger; + _cleanupFrequency = cashConfig.CleanupFrequency; _deleteExpiredCachedItemsDelegate = Cleanup; _dateTimeProvider = dateTimeProvider ?? new DateTimeUtils(); // To prevent a scan on first call, set the last Cleanup to the current Provider time diff --git a/src/System.Linq.Dynamic.Core/Util/Cache/ThreadSafeSlidingCacheConstants.cs b/src/System.Linq.Dynamic.Core/Util/Cache/SlidingCacheConstants.cs similarity index 76% rename from src/System.Linq.Dynamic.Core/Util/Cache/ThreadSafeSlidingCacheConstants.cs rename to src/System.Linq.Dynamic.Core/Util/Cache/SlidingCacheConstants.cs index 517c4548b..3dd8ee8bd 100644 --- a/src/System.Linq.Dynamic.Core/Util/Cache/ThreadSafeSlidingCacheConstants.cs +++ b/src/System.Linq.Dynamic.Core/Util/Cache/SlidingCacheConstants.cs @@ -1,6 +1,6 @@ namespace System.Linq.Dynamic.Core.Util.Cache; -internal static class ThreadSafeSlidingCacheConstants +internal static class SlidingCacheConstants { // Default cleanup frequency public static readonly TimeSpan DefaultCleanupFrequency = TimeSpan.FromMinutes(10); diff --git a/test/System.Linq.Dynamic.Core.Tests/Util/Cache/ThreadSafeSlidingCacheTests.cs b/test/System.Linq.Dynamic.Core.Tests/Util/Cache/SlidingCacheTests.cs similarity index 92% rename from test/System.Linq.Dynamic.Core.Tests/Util/Cache/ThreadSafeSlidingCacheTests.cs rename to test/System.Linq.Dynamic.Core.Tests/Util/Cache/SlidingCacheTests.cs index 14bac8069..c5a10cb7c 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Util/Cache/ThreadSafeSlidingCacheTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Util/Cache/SlidingCacheTests.cs @@ -7,7 +7,7 @@ namespace System.Linq.Dynamic.Core.Tests.Util.Cache; -public class ThreadSafeSlidingCacheTests +public class SlidingCacheTests { private static readonly DateTime UtcNow = new(2024, 1, 1, 0, 0, 0); @@ -18,7 +18,7 @@ public void ThreadSafeSlidingCache_CacheOperations() dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(UtcNow); // Arrange - var cache = new ThreadSafeSlidingCache( + var cache = new SlidingCache( TimeSpan.FromSeconds(1), dateTimeProvider: dateTimeUtilsMock.Object); @@ -48,7 +48,7 @@ public void ThreadSafeSlidingCache_TestExpire() var dateTimeUtilsMock = new Mock(); dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(UtcNow); - var cache = new ThreadSafeSlidingCache(TimeSpan.FromMinutes(10), + var cache = new SlidingCache(TimeSpan.FromMinutes(10), dateTimeProvider: dateTimeUtilsMock.Object); // Act @@ -70,7 +70,7 @@ public void ThreadSafeSlidingCache_TestAutoExpire() dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(UtcNow); // Arrange - var cache = new ThreadSafeSlidingCache( + var cache = new SlidingCache( TimeSpan.FromMinutes(10), dateTimeProvider: dateTimeUtilsMock.Object); @@ -99,7 +99,7 @@ public void ThreadSafeSlidingCache_TestAutoExpire() public void ThreadSafeSlidingCache_TestNull() { // Arrange - var cache = new ThreadSafeSlidingCache(TimeSpan.FromMinutes(10)); + var cache = new SlidingCache(TimeSpan.FromMinutes(10)); // Expect an ArgumentNullException var exception = Assert.Throws(() => { cache.AddOrUpdate(null, "one"); }); @@ -112,7 +112,7 @@ public void ThreadSafeSlidingCache_TestMinNumberBeforeTests() dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(UtcNow); // Arrange - var cache = new ThreadSafeSlidingCache( + var cache = new SlidingCache( TimeSpan.FromMinutes(10), minCacheItemsBeforeCleanup: 2, dateTimeProvider: dateTimeUtilsMock.Object); From c8653b108ddb4cf462d5c02849192ae0d259debe Mon Sep 17 00:00:00 2001 From: Travis Whidden Date: Mon, 22 Jan 2024 10:38:13 -0800 Subject: [PATCH 11/11] #764 - PR Refactor Feedback --- .../Parser/ConstantExpressionHelper.cs | 7 +++---- src/System.Linq.Dynamic.Core/ParsingConfig.cs | 2 +- src/System.Linq.Dynamic.Core/Util/Cache/CacheConfig.cs | 4 +--- .../Util/Cache/{CacheContainer.cs => CacheEntry.cs} | 4 ++-- .../Util/Cache/SlidingCache.cs | 10 +++++----- .../Util/Cache/SlidingCacheTests.cs | 3 +-- 6 files changed, 13 insertions(+), 17 deletions(-) rename src/System.Linq.Dynamic.Core/Util/Cache/{CacheContainer.cs => CacheEntry.cs} (62%) diff --git a/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs index 13d4ecf09..bae556f8e 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs @@ -6,17 +6,16 @@ namespace System.Linq.Dynamic.Core.Parser; internal class ConstantExpressionHelper { - // Static shared instance to prevent duplications of the same objects private readonly SlidingCache _expressions; private readonly SlidingCache _literals; public ConstantExpressionHelper(ParsingConfig config) { var parsingConfig = Check.NotNull(config); - var cacheConfig = Check.NotNull(parsingConfig.ConstantExpressionCacheConfig); + var useConfig = parsingConfig.ConstantExpressionCacheConfig ?? new CacheConfig(); - _literals = new SlidingCache(cacheConfig); - _expressions = new SlidingCache(cacheConfig); + _literals = new SlidingCache(useConfig); + _expressions = new SlidingCache(useConfig); } public bool TryGetText(Expression expression, out string? text) diff --git a/src/System.Linq.Dynamic.Core/ParsingConfig.cs b/src/System.Linq.Dynamic.Core/ParsingConfig.cs index 0e1a51229..ec3a164a2 100644 --- a/src/System.Linq.Dynamic.Core/ParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core/ParsingConfig.cs @@ -239,5 +239,5 @@ public IQueryableAnalyzer QueryableAnalyzer /// /// Caches constant expressions to enhance performance. Periodic cleanup is performed to manage cache size, governed by this configuration. /// - public CacheConfig ConstantExpressionCacheConfig { get; set; } = new CacheConfig(); + public CacheConfig? ConstantExpressionCacheConfig { get; set; } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Util/Cache/CacheConfig.cs b/src/System.Linq.Dynamic.Core/Util/Cache/CacheConfig.cs index ee9c8817e..d1a63a35c 100644 --- a/src/System.Linq.Dynamic.Core/Util/Cache/CacheConfig.cs +++ b/src/System.Linq.Dynamic.Core/Util/Cache/CacheConfig.cs @@ -12,14 +12,12 @@ public class CacheConfig /// public TimeSpan TimeToLive { get; set; } = TimeSpan.FromMinutes(10); - /// /// Configures the minimum number of items required in the constant expression cache before triggering cleanup. /// This prevents frequent cleanups, especially in caches with few items. /// A default value of null implies that cleanup is always allowed to run, helping in timely removal of unused cache items. /// - public int? MinItemsTrigger { get; set; } = null; - + public int? MinItemsTrigger { get; set; } /// /// Sets the frequency for running the cleanup process in the Constant Expression cache. diff --git a/src/System.Linq.Dynamic.Core/Util/Cache/CacheContainer.cs b/src/System.Linq.Dynamic.Core/Util/Cache/CacheEntry.cs similarity index 62% rename from src/System.Linq.Dynamic.Core/Util/Cache/CacheContainer.cs rename to src/System.Linq.Dynamic.Core/Util/Cache/CacheEntry.cs index 6807681c3..363546c1a 100644 --- a/src/System.Linq.Dynamic.Core/Util/Cache/CacheContainer.cs +++ b/src/System.Linq.Dynamic.Core/Util/Cache/CacheEntry.cs @@ -1,12 +1,12 @@ namespace System.Linq.Dynamic.Core.Util.Cache; -internal struct CacheContainer where TValue : notnull +internal struct CacheEntry where TValue : notnull { public TValue Value { get; } public DateTime ExpirationTime { get; } - public CacheContainer(TValue value, DateTime expirationTime) + public CacheEntry(TValue value, DateTime expirationTime) { Value = value; ExpirationTime = expirationTime; diff --git a/src/System.Linq.Dynamic.Core/Util/Cache/SlidingCache.cs b/src/System.Linq.Dynamic.Core/Util/Cache/SlidingCache.cs index 42f8311da..5697bc34b 100644 --- a/src/System.Linq.Dynamic.Core/Util/Cache/SlidingCache.cs +++ b/src/System.Linq.Dynamic.Core/Util/Cache/SlidingCache.cs @@ -6,7 +6,7 @@ namespace System.Linq.Dynamic.Core.Util.Cache; internal class SlidingCache where TKey : notnull where TValue : notnull { - private readonly ConcurrentDictionary> _cache; + private readonly ConcurrentDictionary> _cache; private readonly TimeSpan _cleanupFrequency; private readonly IDateTimeUtils _dateTimeProvider; private readonly Action _deleteExpiredCachedItemsDelegate; @@ -32,7 +32,7 @@ public SlidingCache( long? minCacheItemsBeforeCleanup = null, IDateTimeUtils? dateTimeProvider = null) { - _cache = new ConcurrentDictionary>(); + _cache = new ConcurrentDictionary>(); TimeToLive = timeToLive; _minCacheItemsBeforeCleanup = minCacheItemsBeforeCleanup; _cleanupFrequency = cleanupFrequency ?? SlidingCacheConstants.DefaultCleanupFrequency; @@ -51,7 +51,7 @@ public SlidingCache( CacheConfig cashConfig, IDateTimeUtils? dateTimeProvider = null) { - _cache = new ConcurrentDictionary>(); + _cache = new ConcurrentDictionary>(); TimeToLive = cashConfig.TimeToLive; _minCacheItemsBeforeCleanup = cashConfig.MinItemsTrigger; _cleanupFrequency = cashConfig.CleanupFrequency; @@ -82,7 +82,7 @@ public void AddOrUpdate(TKey key, TValue value) Check.NotNull(value); var expirationTime = _dateTimeProvider.UtcNow.Add(TimeToLive); - _cache[key] = new CacheContainer(value, expirationTime); + _cache[key] = new CacheEntry(value, expirationTime); CleanupIfNeeded(); } @@ -105,7 +105,7 @@ public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value) { value = valueAndExpiration.Value; var newExpire = _dateTimeProvider.UtcNow.Add(TimeToLive); - _cache[key] = new CacheContainer(value, newExpire); + _cache[key] = new CacheEntry(value, newExpire); return true; } diff --git a/test/System.Linq.Dynamic.Core.Tests/Util/Cache/SlidingCacheTests.cs b/test/System.Linq.Dynamic.Core.Tests/Util/Cache/SlidingCacheTests.cs index c5a10cb7c..31402e29f 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Util/Cache/SlidingCacheTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Util/Cache/SlidingCacheTests.cs @@ -147,5 +147,4 @@ public void ThreadSafeSlidingCache_TestMinNumberBeforeTests() // Ensure one item is in the cache cache.Count.Should().Be(1, $"Expected 1 items in the cache, had {cache.Count}"); } - -} +} \ No newline at end of file