Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs
Original file line number Diff line number Diff line change
@@ -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<object, Expression> Expressions = new();
private static readonly ConcurrentDictionary<Expression, string> Literals = new();
#if DEBUG
private static readonly TimeSpan TimeToLivePeriod = TimeSpan.FromSeconds(10);
Comment thread
StefH marked this conversation as resolved.
Outdated
#else
private static readonly TimeSpan TimeToLivePeriod = TimeSpan.FromMinutes(10);
#endif

Comment thread
TWhidden marked this conversation as resolved.
Outdated
public static readonly ThreadSafeSlidingCache<object, Expression> Expressions = new(TimeToLivePeriod);
private static readonly ThreadSafeSlidingCache<Expression, string> Literals = new(TimeToLivePeriod);


public static bool TryGetText(Expression expression, out string? text)
{
Expand All @@ -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;
}
}
}
80 changes: 80 additions & 0 deletions src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Collections.Concurrent;

namespace System.Linq.Dynamic.Core.Parser
Comment thread
StefH marked this conversation as resolved.
Outdated
{
internal class ThreadSafeSlidingCache<T1, T2> where T1 : notnull where T2 : notnull
Comment thread
TWhidden marked this conversation as resolved.
Outdated
{
private readonly ConcurrentDictionary<T1, (T2 Value, DateTime ExpirationTime)> _cache;
private readonly TimeSpan _timeToLive;
private readonly TimeSpan _cleanupFrequency;
private DateTime _lastCleanupTime = DateTime.MinValue;

public ThreadSafeSlidingCache(TimeSpan timeToLive, TimeSpan? cleanupFrequency = null)
Comment thread
StefH marked this conversation as resolved.
Outdated
Comment thread
StefH marked this conversation as resolved.
Outdated
{
_cache = new ConcurrentDictionary<T1, (T2, DateTime)>();
_timeToLive = timeToLive;
_cleanupFrequency = cleanupFrequency ?? TimeSpan.FromSeconds(10);
Comment thread
StefH marked this conversation as resolved.
Outdated
}

public TimeSpan TimeToLive => _timeToLive;

public void AddOrUpdate(T1 key, T2 value)
{
if (key == null) throw new ArgumentNullException(nameof(key));
Comment thread
StefH marked this conversation as resolved.
Outdated
if (value == null) throw new ArgumentNullException(nameof(value));
Comment thread
StefH marked this conversation as resolved.
Outdated

var expirationTime = DateTime.UtcNow.Add(_timeToLive);
_cache[key] = (value, expirationTime);

CleanupIfNeeded();
Comment thread
StefH marked this conversation as resolved.
Outdated
}

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;
_cache[key] = (value, DateTime.UtcNow.Add(_timeToLive));
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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Linq.Dynamic.Core.Parser;
Comment thread
StefH marked this conversation as resolved.
Outdated
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");
}
}
}
}