Skip to content

Commit cc8482a

Browse files
author
Travis Whidden
committed
zzzprojects#764 - ConstantExpressionHelper Singleton Factory; Code Review Resolutions
1 parent c965066 commit cc8482a

10 files changed

+88
-65
lines changed

src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs

+17-27
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,49 @@
11
using System.Linq.Dynamic.Core.Util.Cache;
2+
using System.Linq.Dynamic.Core.Validation;
23
using System.Linq.Expressions;
34

45
namespace System.Linq.Dynamic.Core.Parser;
56

67
internal class ConstantExpressionHelper
78
{
8-
private readonly ParsingConfig _config;
9-
109
// Static shared instance to prevent duplications of the same objects
11-
private static ThreadSafeSlidingCache<object, Expression>? _expressions;
12-
private static ThreadSafeSlidingCache<Expression, string>? _literals;
10+
private readonly ThreadSafeSlidingCache<object, Expression> _expressions;
11+
private readonly ThreadSafeSlidingCache<Expression, string> _literals;
1312

1413
public ConstantExpressionHelper(ParsingConfig config)
1514
{
16-
_config = config;
17-
18-
}
15+
var parsingConfig = Check.NotNull(config);
16+
var cacheConfig = Check.NotNull(parsingConfig.ConstantExpressionCacheConfig);
1917

20-
private ThreadSafeSlidingCache<Expression, string> GetLiterals()
21-
{
22-
_literals ??= new ThreadSafeSlidingCache<Expression, string>(
23-
_config.ConstantExpressionSlidingCacheTimeToLive,
24-
_config.ConstantExpressionSlidingCacheCleanupFrequency,
25-
_config.ConstantExpressionSlidingCacheMinItemsTrigger
18+
_literals = new ThreadSafeSlidingCache<Expression, string>(
19+
cacheConfig.TimeToLive,
20+
cacheConfig.CleanupFrequency,
21+
cacheConfig.MinItemsTrigger
2622
);
27-
return _literals;
28-
}
2923

30-
private ThreadSafeSlidingCache<object, Expression> GetExpression()
31-
{
32-
_expressions ??= new ThreadSafeSlidingCache<object, Expression>(
33-
_config.ConstantExpressionSlidingCacheTimeToLive,
34-
_config.ConstantExpressionSlidingCacheCleanupFrequency,
35-
_config.ConstantExpressionSlidingCacheMinItemsTrigger
24+
_expressions = new ThreadSafeSlidingCache<object, Expression>(
25+
cacheConfig.TimeToLive,
26+
cacheConfig.CleanupFrequency,
27+
cacheConfig.MinItemsTrigger
3628
);
37-
return _expressions;
3829
}
3930

40-
4131
public bool TryGetText(Expression expression, out string? text)
4232
{
43-
return GetLiterals().TryGetValue(expression, out text);
33+
return _literals.TryGetValue(expression, out text);
4434
}
4535

4636
public Expression CreateLiteral(object value, string text)
4737
{
48-
if (GetExpression().TryGetValue(value, out var outputValue))
38+
if (_expressions.TryGetValue(value, out var outputValue))
4939
{
5040
return outputValue;
5141
}
5242

5343
var constantExpression = Expression.Constant(value);
5444

55-
GetExpression().AddOrUpdate(value, constantExpression);
56-
GetLiterals().AddOrUpdate(constantExpression, text);
45+
_expressions.AddOrUpdate(value, constantExpression);
46+
_literals.AddOrUpdate(constantExpression, text);
5747

5848
return constantExpression;
5949
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace System.Linq.Dynamic.Core.Parser;
2+
3+
internal static class ConstantExpressionHelperFactory
4+
{
5+
private static readonly object Lock = new();
6+
private static ConstantExpressionHelper? _instance;
7+
8+
public static ConstantExpressionHelper GetInstance(ParsingConfig config)
9+
{
10+
if (_instance == null)
11+
{
12+
lock (Lock)
13+
{
14+
_instance ??= new ConstantExpressionHelper(config);
15+
}
16+
}
17+
18+
return _instance;
19+
}
20+
}

src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public ExpressionParser(ParameterExpression[]? parameters, string expression, ob
8282
_methodFinder = new MethodFinder(_parsingConfig, _expressionHelper);
8383
_typeFinder = new TypeFinder(_parsingConfig, _keywordsHelper);
8484
_typeConverterFactory = new TypeConverterFactory(_parsingConfig);
85-
_constantExpressionHelper = new ConstantExpressionHelper(_parsingConfig);
85+
_constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(_parsingConfig);
8686

8787
if (parameters != null)
8888
{

src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class ExpressionPromoter : IExpressionPromoter
1818
public ExpressionPromoter(ParsingConfig config)
1919
{
2020
_numberParser = new NumberParser(config);
21-
_constantExpressionHelper = new ConstantExpressionHelper(config);
21+
_constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(config);
2222
}
2323

2424
/// <inheritdoc />

src/System.Linq.Dynamic.Core/Parser/NumberParser.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class NumberParser
1616
private static readonly char[] Qualifiers = { 'U', 'u', 'L', 'l', 'F', 'f', 'D', 'd', 'M', 'm' };
1717
private static readonly char[] QualifiersHex = { 'U', 'u', 'L', 'l' };
1818
private static readonly string[] QualifiersReal = { "F", "f", "D", "d", "M", "m" };
19-
private ConstantExpressionHelper _constantExpressionHelper;
19+
private readonly ConstantExpressionHelper _constantExpressionHelper;
2020

2121
private readonly CultureInfo _culture;
2222

@@ -27,7 +27,7 @@ public class NumberParser
2727
public NumberParser(ParsingConfig? config)
2828
{
2929
_culture = config?.NumberParseCulture ?? CultureInfo.InvariantCulture;
30-
_constantExpressionHelper = new ConstantExpressionHelper(config ?? ParsingConfig.Default);
30+
_constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(config ?? ParsingConfig.Default);
3131
}
3232

3333
/// <summary>

src/System.Linq.Dynamic.Core/ParsingConfig.cs

+3-20
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Globalization;
44
using System.Linq.Dynamic.Core.CustomTypeProviders;
55
using System.Linq.Dynamic.Core.Parser;
6+
using System.Linq.Dynamic.Core.Util.Cache;
67

78
namespace System.Linq.Dynamic.Core;
89

@@ -236,25 +237,7 @@ public IQueryableAnalyzer QueryableAnalyzer
236237
public bool DisallowNewKeyword { get; set; } = false;
237238

238239
/// <summary>
239-
/// Sets a Time-To-Live (TTL) for items in the constant expression cache to prevent uncontrolled growth.
240-
/// Items not accessed within this TTL will be expired, allowing garbage collection to reclaim the memory.
241-
/// Default is 10 minutes.
240+
/// Caches constant expressions to enhance performance. Periodic cleanup is performed to manage cache size, governed by this configuration.
242241
/// </summary>
243-
public TimeSpan ConstantExpressionSlidingCacheTimeToLive { get; set; } = TimeSpan.FromMinutes(10);
244-
245-
246-
/// <summary>
247-
/// Configures the minimum number of items required in the constant expression cache before triggering cleanup.
248-
/// This prevents frequent cleanups, especially in caches with few items.
249-
/// A default value of null implies that cleanup is always allowed to run, helping in timely removal of unused cache items.
250-
/// </summary>
251-
public int? ConstantExpressionSlidingCacheMinItemsTrigger { get; set; } = null;
252-
253-
254-
/// <summary>
255-
/// Sets the frequency for running the cleanup process in the Constant Expression cache.
256-
/// By default, cleanup occurs every 10 minutes.
257-
/// </summary>
258-
public TimeSpan ConstantExpressionSlidingCacheCleanupFrequency { get; set; } = TimeSpan.FromMinutes(10);
259-
242+
public CacheConfig ConstantExpressionCacheConfig { get; set; } = new CacheConfig();
260243
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace System.Linq.Dynamic.Core.Util.Cache;
2+
3+
/// <summary>
4+
/// Cache Configuration Options
5+
/// </summary>
6+
public class CacheConfig
7+
{
8+
/// <summary>
9+
/// Sets a Time-To-Live (TTL) for items in the constant expression cache to prevent uncontrolled growth.
10+
/// Items not accessed within this TTL will be expired, allowing garbage collection to reclaim the memory.
11+
/// Default is 10 minutes.
12+
/// </summary>
13+
public TimeSpan TimeToLive { get; set; } = TimeSpan.FromMinutes(10);
14+
15+
16+
/// <summary>
17+
/// Configures the minimum number of items required in the constant expression cache before triggering cleanup.
18+
/// This prevents frequent cleanups, especially in caches with few items.
19+
/// A default value of null implies that cleanup is always allowed to run, helping in timely removal of unused cache items.
20+
/// </summary>
21+
public int? MinItemsTrigger { get; set; } = null;
22+
23+
24+
/// <summary>
25+
/// Sets the frequency for running the cleanup process in the Constant Expression cache.
26+
/// By default, cleanup occurs every 10 minutes.
27+
/// </summary>
28+
public TimeSpan CleanupFrequency { get; set; } = TimeSpan.FromMinutes(10);
29+
}

src/System.Linq.Dynamic.Core/Util/Cache/CacheContainer.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
internal struct CacheContainer<TValue> where TValue : notnull
44
{
5+
public TValue Value { get; }
6+
7+
public DateTime ExpirationTime { get; }
8+
59
public CacheContainer(TValue value, DateTime expirationTime)
610
{
711
Value = value;
812
ExpirationTime = expirationTime;
913
}
10-
11-
public TValue Value { get; }
12-
13-
public DateTime ExpirationTime { get; }
1414
}

src/System.Linq.Dynamic.Core/Util/TaskUtils.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
namespace System.Linq.Dynamic.Core.Util;
1+
using System.Linq.Dynamic.Core.Validation;
2+
3+
namespace System.Linq.Dynamic.Core.Util;
24

35
internal static class TaskUtils
46
{
57
public static void Run(Action action)
68
{
9+
Check.NotNull(action);
710
#if NET35 || NET40
8-
System.Threading.ThreadPool.QueueUserWorkItem(_ => {
9-
action?.Invoke();
10-
});
11+
System.Threading.ThreadPool.QueueUserWorkItem(_ => action.Invoke());
1112
#else
1213
System.Threading.Tasks.Task.Run(action);
1314
#endif

test/System.Linq.Dynamic.Core.Tests/Util/Cache/ThreadSafeSlidingCacheTests.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ public void ThreadSafeSlidingCache_TestExpire()
5454
// Act
5555
cache.AddOrUpdate(1, "one");
5656

57-
var r = dateTimeUtilsMock.Object.UtcNow.AddMinutes(11);
58-
dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(r);
57+
var newDateTime = dateTimeUtilsMock.Object.UtcNow.AddMinutes(11);
58+
dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(newDateTime);
5959

6060
if (cache.TryGetValue(1, out var value))
6161
{
@@ -81,8 +81,8 @@ public void ThreadSafeSlidingCache_TestAutoExpire()
8181
cache.Count.Should().Be(1, $"Expected 1 items in the cache, only had {cache.Count}");
8282

8383
// move the time forward
84-
var r = dateTimeUtilsMock.Object.UtcNow.AddMinutes(11);
85-
dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(r);
84+
var newDateTime = dateTimeUtilsMock.Object.UtcNow.AddMinutes(11);
85+
dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(newDateTime);
8686

8787
// Trigger the cleanup, asking for non-existing key
8888
cache.TryGetValue(10, out var _);
@@ -124,8 +124,8 @@ public void ThreadSafeSlidingCache_TestMinNumberBeforeTests()
124124
cache.Count.Should().Be(1, $"Expected 1 items in the cache, only had {cache.Count}");
125125

126126
// move the time forward
127-
var r = dateTimeUtilsMock.Object.UtcNow.AddMinutes(11);
128-
dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(r);
127+
var newDateTime = dateTimeUtilsMock.Object.UtcNow.AddMinutes(11);
128+
dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(newDateTime);
129129

130130
// Trigger the cleanup, asking for non-existing key
131131
cache.TryGetValue(10, out var _);

0 commit comments

Comments
 (0)