diff --git a/.gitignore b/.gitignore index 26cb01a0..93b7d410 100644 --- a/.gitignore +++ b/.gitignore @@ -238,4 +238,5 @@ _Pvt_Extensions /coverage.xml /dynamic-coverage-*.xml /test/**/coverage.net8.0.opencover.xml +/test/**/coverage.opencover.xml .nuget/ diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 0ae3e61d..dcf6a329 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -680,18 +680,7 @@ private Expression ParseComparisonOperator() private static bool HasImplicitConversion(Type baseType, Type targetType) { - var baseTypeHasConversion = baseType.GetMethods(BindingFlags.Public | BindingFlags.Static) - .Where(mi => mi.Name == "op_Implicit" && mi.ReturnType == targetType) - .Any(mi => mi.GetParameters().FirstOrDefault()?.ParameterType == baseType); - - if (baseTypeHasConversion) - { - return true; - } - - return targetType.GetMethods(BindingFlags.Public | BindingFlags.Static) - .Where(mi => mi.Name == "op_Implicit" && mi.ReturnType == targetType) - .Any(mi => mi.GetParameters().FirstOrDefault()?.ParameterType == baseType); + return TypeHelper.TryGetImplicitConversionOperatorMethod(baseType, targetType, out _); } private static ConstantExpression ParseEnumToConstantExpression(int pos, Type leftType, ConstantExpression constantExpr) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs index 49731b24..c82c13f9 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs @@ -135,6 +135,11 @@ public ExpressionPromoter(ParsingConfig config) return sourceExpression; } + if (TypeHelper.TryGetImplicitConversionOperatorMethod(returnType, type, out _)) + { + return Expression.Convert(sourceExpression, type); + } + return null; } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs b/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs index f4401b63..1cfd533a 100644 --- a/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs @@ -533,4 +533,31 @@ public static bool IsDictionary(Type? type) TryFindGenericType(typeof(IReadOnlyDictionary<,>), type, out _); #endif } + + /// + /// Check for implicit conversion operators (op_Implicit) from returnType to type. + /// Look for op_Implicit on the source type or the target type. + /// + public static bool TryGetImplicitConversionOperatorMethod(Type returnType, Type type, [NotNullWhen(true)] out MethodBase? implicitOperator) + { + const string methodName = "op_Implicit"; + + implicitOperator = Find(returnType) ?? Find(type); + return implicitOperator != null; + + MethodBase? Find(Type searchType) + { + return searchType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(m => + { + if (m.Name != methodName || m.ReturnType != type) + { + return false; + } + + var parameters = m.GetParameters(); + return parameters.Length == 1 && parameters[0].ParameterType == returnType; + }); + } + } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs index e301394c..b24a2bd2 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -3,6 +3,7 @@ using System.Linq.Dynamic.Core.Config; using System.Linq.Dynamic.Core.CustomTypeProviders; using System.Linq.Dynamic.Core.Exceptions; +using System.Linq.Dynamic.Core.Parser; using System.Linq.Dynamic.Core.Tests.Helpers; using System.Linq.Dynamic.Core.Tests.Helpers.Models; using System.Linq.Dynamic.Core.Tests.TestHelpers; @@ -270,6 +271,37 @@ public override string ToString() } } + [DynamicLinqType] + public static class MyMethodsWithImplicitOperatorSupport + { + public static string UsesMyStructWithImplicitOperator(MyStructWithImplicitOperator myStruct) + { + return myStruct.Value; + } + } + + public readonly struct MyStructWithImplicitOperator + { + private readonly string _value; + + public MyStructWithImplicitOperator(string value) + { + _value = value; + } + + public static implicit operator MyStructWithImplicitOperator(string value) + { + return new MyStructWithImplicitOperator(value); + } + + public static implicit operator string(MyStructWithImplicitOperator myStruct) + { + return myStruct._value; + } + + public string Value => _value; + } + internal class TestClass794 { public byte ByteValue { get; set; } @@ -1517,6 +1549,23 @@ public void DynamicExpressionParser_ParseLambda_With_One_Way_Implicit_Conversion Assert.NotNull(lambda); } + [Fact] + public void DynamicExpressionParser_ParseLambda_With_Implicit_Operator_In_Method_Argument() + { + // Arrange - Method takes a MyStructWithImplicitOperator but we pass a string literal + var expression = $"{nameof(MyMethodsWithImplicitOperatorSupport)}.{nameof(MyMethodsWithImplicitOperatorSupport.UsesMyStructWithImplicitOperator)}(\"Foo\")"; + + // Act + var parser = new ExpressionParser(parameters: [], expression, values: [], ParsingConfig.Default); + var parsedExpression = parser.Parse(typeof(string)); + var lambda = Expression.Lambda>(parsedExpression); + var method = lambda.Compile(); + var result = method(); + + // Assert + Assert.Equal("Foo", result); + } + [Fact] public void DynamicExpressionParser_ParseLambda_StaticClassWithStaticPropertyWithSameNameAsNormalProperty() {