Skip to content

Commit fadb250

Browse files
TWhiddenTravis Whidden
and
Travis Whidden
authored
#764 - Introduce Sliding Cache to Constant Expression Helper (#765)
* #764 - Introduce Sliding Cache to Constant Expression Helper * #764 Additional Tests, Missing TTL Update * #764 - Rename T1,T2 to TKey, TValue * #764 - Set cleanup time prior to enumeration to prevent multiple cleanup hits. * #764 - Code Review Changes implemented; ThreadSafeSlidingCache Tests Added * #764 - Move DefaultCleanupFrequency to internal static non generic class * #764 - Dropped Preprocessor Directive for TTL in ConstantExpressionHelper * #764 - Move ConstantExpressionHelper to Instance. Refactor feedback from Review * #764 - ConstantExpressionHelper Singleton Factory; Code Review Resolutions * #764 - Refactor Naming for SlidingCache; * #764 - PR Refactor Feedback --------- Co-authored-by: Travis Whidden <[email protected]>
1 parent 6a70951 commit fadb250

13 files changed

+460
-36
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,40 @@
1-
using System.Collections.Concurrent;
1+
using System.Linq.Dynamic.Core.Util.Cache;
2+
using System.Linq.Dynamic.Core.Validation;
23
using System.Linq.Expressions;
34

4-
namespace System.Linq.Dynamic.Core.Parser
5+
namespace System.Linq.Dynamic.Core.Parser;
6+
7+
internal class ConstantExpressionHelper
58
{
6-
internal static class ConstantExpressionHelper
9+
private readonly SlidingCache<object, Expression> _expressions;
10+
private readonly SlidingCache<Expression, string> _literals;
11+
12+
public ConstantExpressionHelper(ParsingConfig config)
13+
{
14+
var parsingConfig = Check.NotNull(config);
15+
var useConfig = parsingConfig.ConstantExpressionCacheConfig ?? new CacheConfig();
16+
17+
_literals = new SlidingCache<Expression, string>(useConfig);
18+
_expressions = new SlidingCache<object, Expression>(useConfig);
19+
}
20+
21+
public bool TryGetText(Expression expression, out string? text)
722
{
8-
private static readonly ConcurrentDictionary<object, Expression> Expressions = new();
9-
private static readonly ConcurrentDictionary<Expression, string> Literals = new();
23+
return _literals.TryGetValue(expression, out text);
24+
}
1025

11-
public static bool TryGetText(Expression expression, out string? text)
26+
public Expression CreateLiteral(object value, string text)
27+
{
28+
if (_expressions.TryGetValue(value, out var outputValue))
1229
{
13-
return Literals.TryGetValue(expression, out text);
30+
return outputValue;
1431
}
1532

16-
public static Expression CreateLiteral(object value, string text)
17-
{
18-
if (!Expressions.ContainsKey(value))
19-
{
20-
ConstantExpression constantExpression = Expression.Constant(value);
33+
var constantExpression = Expression.Constant(value);
2134

22-
Expressions.TryAdd(value, constantExpression);
23-
Literals.TryAdd(constantExpression, text);
24-
}
35+
_expressions.AddOrUpdate(value, constantExpression);
36+
_literals.AddOrUpdate(constantExpression, text);
2537

26-
return Expressions[value];
27-
}
38+
return constantExpression;
2839
}
2940
}
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

+4-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public class ExpressionParser
4545
private ParameterExpression? _root;
4646
private Type? _resultType;
4747
private bool _createParameterCtor;
48+
private ConstantExpressionHelper _constantExpressionHelper;
4849

4950
/// <summary>
5051
/// 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
8182
_methodFinder = new MethodFinder(_parsingConfig, _expressionHelper);
8283
_typeFinder = new TypeFinder(_parsingConfig, _keywordsHelper);
8384
_typeConverterFactory = new TypeConverterFactory(_parsingConfig);
85+
_constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(_parsingConfig);
8486

8587
if (parameters != null)
8688
{
@@ -900,7 +902,7 @@ private AnyOf<Expression, Type> ParseStringLiteral(bool forceParseAsString)
900902
}
901903

902904
_textParser.NextToken();
903-
return ConstantExpressionHelper.CreateLiteral(parsedStringValue[0], parsedStringValue);
905+
return _constantExpressionHelper.CreateLiteral(parsedStringValue[0], parsedStringValue);
904906
}
905907

906908
_textParser.NextToken();
@@ -924,7 +926,7 @@ private AnyOf<Expression, Type> ParseStringLiteral(bool forceParseAsString)
924926

925927
parsedStringValue = StringParser.ParseStringAndReplaceDoubleQuotes(text, _textParser.CurrentToken.Pos);
926928

927-
return ConstantExpressionHelper.CreateLiteral(parsedStringValue, parsedStringValue);
929+
return _constantExpressionHelper.CreateLiteral(parsedStringValue, parsedStringValue);
928930
}
929931

930932
private Expression ParseIntegerLiteral()

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace System.Linq.Dynamic.Core.Parser
99
public class ExpressionPromoter : IExpressionPromoter
1010
{
1111
private readonly NumberParser _numberParser;
12+
private readonly ConstantExpressionHelper _constantExpressionHelper;
1213

1314
/// <summary>
1415
/// Initializes a new instance of the <see cref="ExpressionPromoter"/> class.
@@ -17,6 +18,7 @@ public class ExpressionPromoter : IExpressionPromoter
1718
public ExpressionPromoter(ParsingConfig config)
1819
{
1920
_numberParser = new NumberParser(config);
21+
_constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(config);
2022
}
2123

2224
/// <inheritdoc />
@@ -48,7 +50,7 @@ public ExpressionPromoter(ParsingConfig config)
4850
}
4951
else
5052
{
51-
if (ConstantExpressionHelper.TryGetText(ce, out var text))
53+
if (_constantExpressionHelper.TryGetText(ce, out var text))
5254
{
5355
Type target = TypeHelper.GetNonNullableType(type);
5456
object? value = null;

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

+18-16
Original file line numberDiff line numberDiff line change
@@ -16,6 +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 readonly ConstantExpressionHelper _constantExpressionHelper;
1920

2021
private readonly CultureInfo _culture;
2122

@@ -26,6 +27,7 @@ public class NumberParser
2627
public NumberParser(ParsingConfig? config)
2728
{
2829
_culture = config?.NumberParseCulture ?? CultureInfo.InvariantCulture;
30+
_constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(config ?? ParsingConfig.Default);
2931
}
3032

3133
/// <summary>
@@ -77,38 +79,38 @@ public Expression ParseIntegerLiteral(int tokenPosition, string text)
7779
{
7880
if (qualifier == "U" || qualifier == "u")
7981
{
80-
return ConstantExpressionHelper.CreateLiteral((uint)unsignedValue, text);
82+
return _constantExpressionHelper.CreateLiteral((uint)unsignedValue, text);
8183
}
8284

8385
if (qualifier == "L" || qualifier == "l")
8486
{
85-
return ConstantExpressionHelper.CreateLiteral((long)unsignedValue, text);
87+
return _constantExpressionHelper.CreateLiteral((long)unsignedValue, text);
8688
}
8789

8890
if (QualifiersReal.Contains(qualifier))
8991
{
9092
return ParseRealLiteral(text, qualifier[0], false);
9193
}
9294

93-
return ConstantExpressionHelper.CreateLiteral(unsignedValue, text);
95+
return _constantExpressionHelper.CreateLiteral(unsignedValue, text);
9496
}
9597

9698
if (unsignedValue <= int.MaxValue)
9799
{
98-
return ConstantExpressionHelper.CreateLiteral((int)unsignedValue, text);
100+
return _constantExpressionHelper.CreateLiteral((int)unsignedValue, text);
99101
}
100102

101103
if (unsignedValue <= uint.MaxValue)
102104
{
103-
return ConstantExpressionHelper.CreateLiteral((uint)unsignedValue, text);
105+
return _constantExpressionHelper.CreateLiteral((uint)unsignedValue, text);
104106
}
105107

106108
if (unsignedValue <= long.MaxValue)
107109
{
108-
return ConstantExpressionHelper.CreateLiteral((long)unsignedValue, text);
110+
return _constantExpressionHelper.CreateLiteral((long)unsignedValue, text);
109111
}
110112

111-
return ConstantExpressionHelper.CreateLiteral(unsignedValue, text);
113+
return _constantExpressionHelper.CreateLiteral(unsignedValue, text);
112114
}
113115

114116
if (isHexadecimal || isBinary)
@@ -135,7 +137,7 @@ public Expression ParseIntegerLiteral(int tokenPosition, string text)
135137
{
136138
if (qualifier == "L" || qualifier == "l")
137139
{
138-
return ConstantExpressionHelper.CreateLiteral(value, text);
140+
return _constantExpressionHelper.CreateLiteral(value, text);
139141
}
140142

141143
if (QualifiersReal.Contains(qualifier))
@@ -148,10 +150,10 @@ public Expression ParseIntegerLiteral(int tokenPosition, string text)
148150

149151
if (value <= int.MaxValue)
150152
{
151-
return ConstantExpressionHelper.CreateLiteral((int)value, text);
153+
return _constantExpressionHelper.CreateLiteral((int)value, text);
152154
}
153155

154-
return ConstantExpressionHelper.CreateLiteral(value, text);
156+
return _constantExpressionHelper.CreateLiteral(value, text);
155157
}
156158

157159
/// <summary>
@@ -163,18 +165,18 @@ public Expression ParseRealLiteral(string text, char qualifier, bool stripQualif
163165
{
164166
case 'f':
165167
case 'F':
166-
return ConstantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(float))!, text);
168+
return _constantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(float))!, text);
167169

168170
case 'm':
169171
case 'M':
170-
return ConstantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(decimal))!, text);
172+
return _constantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(decimal))!, text);
171173

172174
case 'd':
173175
case 'D':
174-
return ConstantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(double))!, text);
176+
return _constantExpressionHelper.CreateLiteral(ParseNumber(stripQualifier ? text.Substring(0, text.Length - 1) : text, typeof(double))!, text);
175177

176178
default:
177-
return ConstantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(double))!, text);
179+
return _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(double))!, text);
178180
}
179181
}
180182

@@ -285,12 +287,12 @@ private Expression ParseAsBinary(int tokenPosition, string text, bool isNegative
285287
{
286288
if (RegexBinary32.IsMatch(text))
287289
{
288-
return ConstantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt32(text, 2), text);
290+
return _constantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt32(text, 2), text);
289291
}
290292

291293
if (RegexBinary64.IsMatch(text))
292294
{
293-
return ConstantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt64(text, 2), text);
295+
return _constantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt64(text, 2), text);
294296
}
295297

296298
throw new ParseException(string.Format(_culture, Res.InvalidBinaryIntegerLiteral, text), tokenPosition);

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

+6
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

@@ -234,4 +235,9 @@ public IQueryableAnalyzer QueryableAnalyzer
234235
/// Default value is <c>false</c>.
235236
/// </summary>
236237
public bool DisallowNewKeyword { get; set; } = false;
238+
239+
/// <summary>
240+
/// Caches constant expressions to enhance performance. Periodic cleanup is performed to manage cache size, governed by this configuration.
241+
/// </summary>
242+
public CacheConfig? ConstantExpressionCacheConfig { get; set; }
237243
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
/// <summary>
16+
/// Configures the minimum number of items required in the constant expression cache before triggering cleanup.
17+
/// This prevents frequent cleanups, especially in caches with few items.
18+
/// A default value of null implies that cleanup is always allowed to run, helping in timely removal of unused cache items.
19+
/// </summary>
20+
public int? MinItemsTrigger { get; set; }
21+
22+
/// <summary>
23+
/// Sets the frequency for running the cleanup process in the Constant Expression cache.
24+
/// By default, cleanup occurs every 10 minutes.
25+
/// </summary>
26+
public TimeSpan CleanupFrequency { get; set; } = TimeSpan.FromMinutes(10);
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace System.Linq.Dynamic.Core.Util.Cache;
2+
3+
internal struct CacheEntry<TValue> where TValue : notnull
4+
{
5+
public TValue Value { get; }
6+
7+
public DateTime ExpirationTime { get; }
8+
9+
public CacheEntry(TValue value, DateTime expirationTime)
10+
{
11+
Value = value;
12+
ExpirationTime = expirationTime;
13+
}
14+
}

0 commit comments

Comments
 (0)