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