Skip to content

Commit c965066

Browse files
author
Travis Whidden
committed
zzzprojects#764 - Move ConstantExpressionHelper to Instance. Refactor feedback from Review
1 parent 72a6906 commit c965066

13 files changed

+417
-374
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,60 @@
1-
using System.Linq.Dynamic.Core.Util;
1+
using System.Linq.Dynamic.Core.Util.Cache;
22
using System.Linq.Expressions;
33

4-
namespace System.Linq.Dynamic.Core.Parser
4+
namespace System.Linq.Dynamic.Core.Parser;
5+
6+
internal class ConstantExpressionHelper
57
{
6-
internal static class ConstantExpressionHelper
8+
private readonly ParsingConfig _config;
9+
10+
// Static shared instance to prevent duplications of the same objects
11+
private static ThreadSafeSlidingCache<object, Expression>? _expressions;
12+
private static ThreadSafeSlidingCache<Expression, string>? _literals;
13+
14+
public ConstantExpressionHelper(ParsingConfig config)
15+
{
16+
_config = config;
17+
18+
}
19+
20+
private ThreadSafeSlidingCache<Expression, string> GetLiterals()
21+
{
22+
_literals ??= new ThreadSafeSlidingCache<Expression, string>(
23+
_config.ConstantExpressionSlidingCacheTimeToLive,
24+
_config.ConstantExpressionSlidingCacheCleanupFrequency,
25+
_config.ConstantExpressionSlidingCacheMinItemsTrigger
26+
);
27+
return _literals;
28+
}
29+
30+
private ThreadSafeSlidingCache<object, Expression> GetExpression()
731
{
8-
private static readonly TimeSpan TimeToLivePeriod = TimeSpan.FromMinutes(10);
32+
_expressions ??= new ThreadSafeSlidingCache<object, Expression>(
33+
_config.ConstantExpressionSlidingCacheTimeToLive,
34+
_config.ConstantExpressionSlidingCacheCleanupFrequency,
35+
_config.ConstantExpressionSlidingCacheMinItemsTrigger
36+
);
37+
return _expressions;
38+
}
939

10-
public static readonly ThreadSafeSlidingCache<object, Expression> Expressions = new(TimeToLivePeriod);
11-
private static readonly ThreadSafeSlidingCache<Expression, string> Literals = new(TimeToLivePeriod);
1240

41+
public bool TryGetText(Expression expression, out string? text)
42+
{
43+
return GetLiterals().TryGetValue(expression, out text);
44+
}
1345

14-
public static bool TryGetText(Expression expression, out string? text)
46+
public Expression CreateLiteral(object value, string text)
47+
{
48+
if (GetExpression().TryGetValue(value, out var outputValue))
1549
{
16-
return Literals.TryGetValue(expression, out text);
50+
return outputValue;
1751
}
1852

19-
public static Expression CreateLiteral(object value, string text)
20-
{
21-
if (Expressions.TryGetValue(value, out var outputValue))
22-
{
23-
return outputValue;
24-
}
25-
26-
ConstantExpression constantExpression = Expression.Constant(value);
53+
var constantExpression = Expression.Constant(value);
2754

28-
Expressions.AddOrUpdate(value, constantExpression);
29-
Literals.AddOrUpdate(constantExpression, text);
55+
GetExpression().AddOrUpdate(value, constantExpression);
56+
GetLiterals().AddOrUpdate(constantExpression, text);
3057

31-
return constantExpression;
32-
}
58+
return constantExpression;
3359
}
3460
}

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 = new ConstantExpressionHelper(_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 = new ConstantExpressionHelper(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 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 = new ConstantExpressionHelper(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

+23
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,27 @@ public IQueryableAnalyzer QueryableAnalyzer
234234
/// Default value is <c>false</c>.
235235
/// </summary>
236236
public bool DisallowNewKeyword { get; set; } = false;
237+
238+
/// <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.
242+
/// </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+
237260
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace System.Linq.Dynamic.Core.Util.Cache;
2+
3+
internal struct CacheContainer<TValue> where TValue : notnull
4+
{
5+
public CacheContainer(TValue value, DateTime expirationTime)
6+
{
7+
Value = value;
8+
ExpirationTime = expirationTime;
9+
}
10+
11+
public TValue Value { get; }
12+
13+
public DateTime ExpirationTime { get; }
14+
}

0 commit comments

Comments
 (0)