From 81cdc845d4000e80ec948f46e28fdc0b98e1805e Mon Sep 17 00:00:00 2001 From: Renan Carlos Pereira Date: Sun, 25 May 2025 10:56:21 +0100 Subject: [PATCH 1/2] Fix: Add Fallback in ExpressionPromoter to Handle Cache Cleanup in ConstantExpressionHelper (#905) * include ParseRealLiteral tests * Change ParseRealLiteral to remove literals before parsing * Merge master * NumberParserTests to split the tests NumberParser_Parse[Type]Literal * Fixing race condition * fix enum tests * Update to set instance via reflection * Update ExpressionPromoterTests.cs * using IDynamicLinqCustomTypeProvider --------- Co-authored-by: Renan Pereira --- .../Parser/ExpressionPromoter.cs | 9 +- .../Parser/ExpressionPromoterTests.cs | 148 +++++++++++++----- 2 files changed, 119 insertions(+), 38 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs index 43f7acbf..088b755e 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs @@ -50,7 +50,12 @@ public ExpressionPromoter(ParsingConfig config) } else { - if (_constantExpressionHelper.TryGetText(ce, out var text)) + if (!_constantExpressionHelper.TryGetText(ce, out var text)) + { + text = ce.Value?.ToString(); + } + + if (text != null) { Type target = TypeHelper.GetNonNullableType(type); object? value = null; @@ -67,7 +72,7 @@ public ExpressionPromoter(ParsingConfig config) // Make sure an enum value stays an enum value if (target.IsEnum) { - value = Enum.ToObject(target, value!); + TypeHelper.TryParseEnum(text, target, out value); } break; diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionPromoterTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionPromoterTests.cs index 267ea999..bcbc370b 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionPromoterTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionPromoterTests.cs @@ -1,56 +1,132 @@ -using Moq; +using FluentAssertions; +using Moq; using System.Collections.Generic; using System.Linq.Dynamic.Core.CustomTypeProviders; using System.Linq.Dynamic.Core.Parser; using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Xunit; -namespace System.Linq.Dynamic.Core.Tests.Parser; - -public class ExpressionPromoterTests +namespace System.Linq.Dynamic.Core.Tests.Parser { - public class SampleDto + public class ExpressionPromoterTests { - public Guid Data { get; set; } - } + public class SampleDto + { + public Guid data { get; set; } + } - private readonly Mock _expressionPromoterMock; - private readonly Mock _dynamicLinkCustomTypeProviderMock; + private readonly Mock _expressionPromoterMock; + private readonly Mock _dynamicLinqCustomTypeProviderMock; - public ExpressionPromoterTests() - { - _dynamicLinkCustomTypeProviderMock = new Mock(); - _dynamicLinkCustomTypeProviderMock.Setup(d => d.GetCustomTypes()).Returns(new HashSet()); - _dynamicLinkCustomTypeProviderMock.Setup(d => d.ResolveType(It.IsAny())).Returns(typeof(SampleDto)); + public ExpressionPromoterTests() + { + _dynamicLinqCustomTypeProviderMock = new Mock(); + _dynamicLinqCustomTypeProviderMock.Setup(d => d.GetCustomTypes()).Returns(new HashSet()); + _dynamicLinqCustomTypeProviderMock.Setup(d => d.ResolveType(It.IsAny())).Returns(typeof(SampleDto)); + + _expressionPromoterMock = new Mock(); + _expressionPromoterMock.Setup(e => e.Promote(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Expression.Constant(Guid.NewGuid())); + } + + [Fact] + public void DynamicExpressionParser_ParseLambda_WithCustomExpressionPromoter() + { + // Assign + var parsingConfig = new ParsingConfig() + { + AllowNewToEvaluateAnyType = true, + CustomTypeProvider = _dynamicLinqCustomTypeProviderMock.Object, + ExpressionPromoter = _expressionPromoterMock.Object + }; + + // Act + string query = $"new {typeof(SampleDto).FullName}(@0 as data)"; + LambdaExpression expression = DynamicExpressionParser.ParseLambda(parsingConfig, null, query, new object[] { Guid.NewGuid().ToString() }); + Delegate del = expression.Compile(); + SampleDto result = (SampleDto)del.DynamicInvoke(); + + // Assert + Assert.NotNull(result); + + // Verify + _dynamicLinqCustomTypeProviderMock.Verify(d => d.GetCustomTypes(), Times.Once); + _dynamicLinqCustomTypeProviderMock.Verify(d => d.ResolveType($"{typeof(SampleDto).FullName}"), Times.Once); + + _expressionPromoterMock.Verify(e => e.Promote(It.IsAny(), typeof(Guid), true, true), Times.Once); + } - _expressionPromoterMock = new Mock(); - _expressionPromoterMock.Setup(e => e.Promote(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Expression.Constant(Guid.NewGuid())); + [Fact] + public async Task Promote_Should_Succeed_Even_When_LiteralsCache_Is_Cleaned() + { + // Arrange + var parsingConfig = new ParsingConfig() + { + ConstantExpressionCacheConfig = new Core.Util.Cache.CacheConfig + { + CleanupFrequency = TimeSpan.FromMilliseconds(500), // Run cleanup more often + TimeToLive = TimeSpan.FromMilliseconds(500), // Shorten TTL to force expiration + ReturnExpiredItems = false + } + }; + + // because the field is static only one process is setting the field, + // we need a way to set up because the instance is private we are not able to overwrite the configuration. + ConstantExpressionHelperReflection.Initiate(parsingConfig); + + var constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(parsingConfig); + var expressionPromoter = new ExpressionPromoter(parsingConfig); + + double value = 0.40; + string text = "0.40"; + Type targetType = typeof(decimal); + + // Step 1: Add constant to cache + var literalExpression = constantExpressionHelper.CreateLiteral(value, text); + Assert.NotNull(literalExpression); // Ensure it was added + + // Step 2: Manually trigger cleanup + var cts = new CancellationTokenSource(500); + await Task.Run(async () => + { + while (!cts.IsCancellationRequested) + { + constantExpressionHelper.TryGetText(literalExpression, out _); + await Task.Delay(50); // Give some time for cleanup to be triggered + } + }); + + // Ensure some cleanup cycles have passed + await Task.Delay(500); // Allow cache cleanup to happen + + // Step 3: Attempt to promote the expression after cleanup + var promotedExpression = expressionPromoter.Promote(literalExpression, targetType, exact: false, true); + + // Assert: Promotion should still work even if the cache was cleaned + promotedExpression.Should().NotBeNull(); // Ensure `Promote()` still returns a valid expression + } } - [Fact] - public void DynamicExpressionParser_ParseLambda_WithCustomExpressionPromoter() + public static class ConstantExpressionHelperReflection { - // Assign - var parsingConfig = new ParsingConfig() + private static readonly Type _constantExpressionHelperFactoryType; + + static ConstantExpressionHelperReflection() { - AllowNewToEvaluateAnyType = true, - CustomTypeProvider = _dynamicLinkCustomTypeProviderMock.Object, - ExpressionPromoter = _expressionPromoterMock.Object - }; + var assembly = Assembly.GetAssembly(typeof(DynamicClass))!; - // Act - string query = $"new {typeof(SampleDto).FullName}(@0 as Data)"; - LambdaExpression expression = DynamicExpressionParser.ParseLambda(parsingConfig, null, query, Guid.NewGuid().ToString()); - Delegate del = expression.Compile(); - SampleDto result = (SampleDto)del.DynamicInvoke(); + _constantExpressionHelperFactoryType = assembly.GetType("System.Linq.Dynamic.Core.Parser.ConstantExpressionHelperFactory")!; + } - // Assert - Assert.NotNull(result); + public static void Initiate(ParsingConfig parsingConfig) + { + var instance = new ConstantExpressionHelper(parsingConfig); - // Verify - _dynamicLinkCustomTypeProviderMock.Verify(d => d.GetCustomTypes(), Times.Once); - _dynamicLinkCustomTypeProviderMock.Verify(d => d.ResolveType($"{typeof(SampleDto).FullName}"), Times.Once); + var field = _constantExpressionHelperFactoryType.GetField("_instance", BindingFlags.NonPublic | BindingFlags.Static); - _expressionPromoterMock.Verify(e => e.Promote(It.IsAny(), typeof(Guid), true, true), Times.Once); + field?.SetValue(field, instance); + } } -} \ No newline at end of file +} From 9ea3bbb28a1f2e108b4f90e95e77bfc455f1a861 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 28 May 2025 15:08:46 +0200 Subject: [PATCH 2/2] v1.6.5 --- CHANGELOG.md | 4 ++++ Generate-ReleaseNotes.bat | 2 +- version.xml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cf8a02..a9d9c232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# v1.6.5 (28 May 2025) +- [#905](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/905) - Fix: Add Fallback in ExpressionPromoter to Handle Cache Cleanup in ConstantExpressionHelper [bug] contributed by [RenanCarlosPereira](https://github.com/RenanCarlosPereira) +- [#904](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/904) - Race Condition in ConstantExpressionHelper Causing Parsing Failures [bug] + # v1.6.4 (19 May 2025) - [#915](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/915) - Add support for "not in" and "not_in" [feature] contributed by [StefH](https://github.com/StefH) - [#923](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/923) - Fix MethodFinder TryFindAggregateMethod to support array [bug] contributed by [StefH](https://github.com/StefH) diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index d81555a9..cf49f246 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.6.4 +SET version=v1.6.5 GitHubReleaseNotes --output CHANGELOG.md --exclude-labels known_issue out_of_scope not_planned invalid question documentation wontfix environment duplicate --language en --version %version% --token %GH_TOKEN% diff --git a/version.xml b/version.xml index e265d039..1e9ea2a1 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 4 + 5 \ No newline at end of file