diff --git a/src/System.Linq.Dynamic.Core/DynamicClass.cs b/src/System.Linq.Dynamic.Core/DynamicClass.cs index 33f06aee..64685f5e 100644 --- a/src/System.Linq.Dynamic.Core/DynamicClass.cs +++ b/src/System.Linq.Dynamic.Core/DynamicClass.cs @@ -123,6 +123,16 @@ public object? this[string name] } } + /// + /// Determines whether a property with the specified name exists in the collection. + /// + /// The name of the property to locate. Cannot be null. + /// true if a property with the specified name exists; otherwise, false. + public bool ContainsProperty(string name) + { + return Properties.ContainsKey(name); + } + /// /// Returns the enumeration of all dynamic member names. /// diff --git a/src/System.Linq.Dynamic.Core/DynamicClass.uap.cs b/src/System.Linq.Dynamic.Core/DynamicClass.uap.cs index 8778de98..cf28afd3 100644 --- a/src/System.Linq.Dynamic.Core/DynamicClass.uap.cs +++ b/src/System.Linq.Dynamic.Core/DynamicClass.uap.cs @@ -12,7 +12,7 @@ public class DynamicClass : DynamicObject { internal const string IndexerName = "System_Linq_Dynamic_Core_DynamicClass_Indexer"; - private readonly Dictionary _properties = new(); + private readonly Dictionary _properties = new(); /// /// Initializes a new instance of the class. @@ -35,7 +35,7 @@ public DynamicClass(params KeyValuePair[] propertylist) /// The name. /// Value from the property. [IndexerName(IndexerName)] - public object this[string name] + public object? this[string name] { get { @@ -59,6 +59,16 @@ public object this[string name] } } + /// + /// Determines whether a property with the specified name exists in the collection. + /// + /// The name of the property to locate. Cannot be null. + /// true if a property with the specified name exists; otherwise, false. + public bool ContainsProperty(string name) + { + return _properties.ContainsKey(name); + } + /// /// Returns the enumeration of all dynamic member names. /// diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs index fefd5b66..8222d4d7 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs @@ -11,6 +11,7 @@ namespace System.Linq.Dynamic.Core.Parser; internal class ExpressionHelper : IExpressionHelper { + private static readonly Expression _nullExpression = Expression.Constant(null); private readonly IConstantExpressionWrapper _constantExpressionWrapper = new ConstantExpressionWrapper(); private readonly ParsingConfig _parsingConfig; @@ -340,7 +341,7 @@ public bool TryGenerateAndAlsoNotNullExpression(Expression sourceExpression, boo // Convert all expressions into '!= null' expressions (only if the type can be null) var binaryExpressions = expressions .Where(expression => TypeHelper.TypeCanBeNull(expression.Type)) - .Select(expression => Expression.NotEqual(expression, Expression.Constant(null))) + .Select(expression => Expression.NotEqual(expression, _nullExpression)) .ToArray(); // Convert all binary expressions into `AndAlso(...)` @@ -393,16 +394,46 @@ public bool TryConvertTypes(ref Expression left, ref Expression right) if (left.Type == typeof(object)) { + if (TryGetAsIndexerExpression(left, out var ce)) + { + var rightTypeAsNullableType = TypeHelper.GetNullableType(right.Type); + + right = Expression.Convert(right, rightTypeAsNullableType); + + left = Expression.Condition( + ce.Test, + Expression.Convert(ce.IfTrue, rightTypeAsNullableType), + Expression.Convert(_nullExpression, rightTypeAsNullableType) + ); + + return true; + } + left = Expression.Condition( - Expression.Equal(left, Expression.Constant(null, typeof(object))), + Expression.Equal(left, _nullExpression), GenerateDefaultExpression(right.Type), Expression.Convert(left, right.Type) ); } else if (right.Type == typeof(object)) { + if (TryGetAsIndexerExpression(right, out var ce)) + { + var leftTypeAsNullableType = TypeHelper.GetNullableType(left.Type); + + left = Expression.Convert(left, leftTypeAsNullableType); + + right = Expression.Condition( + ce.Test, + Expression.Convert(ce.IfTrue, leftTypeAsNullableType), + Expression.Convert(_nullExpression, leftTypeAsNullableType) + ); + + return true; + } + right = Expression.Condition( - Expression.Equal(right, Expression.Constant(null, typeof(object))), + Expression.Equal(right, _nullExpression), GenerateDefaultExpression(left.Type), Expression.Convert(right, left.Type) ); @@ -546,4 +577,17 @@ private static object[] ConvertIfIEnumerableHasValues(IEnumerable? input) return []; } + + private static bool TryGetAsIndexerExpression(Expression expression, [NotNullWhen(true)] out ConditionalExpression? indexerExpresion) + { + indexerExpresion = expression as ConditionalExpression; + if (indexerExpresion == null) + { + return false; + } + + return + indexerExpresion.IfTrue.ToString().Contains(DynamicClass.IndexerName) && + indexerExpresion.Test.ToString().Contains("ContainsProperty"); + } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index b0fb8157..b8091f55 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -1921,11 +1921,35 @@ private Expression ParseMemberAccess(Type? type, Expression? expression, string? if (!_parsingConfig.DisableMemberAccessToIndexAccessorFallback && extraCheck) { - var indexerName = TypeHelper.IsDynamicClass(type!) ? DynamicClass.IndexerName : "Item"; + var isDynamicClass = TypeHelper.IsDynamicClass(type!); + var indexerName = isDynamicClass ? DynamicClass.IndexerName : "Item"; + + // Try to get the indexer property "Item" or "DynamicClass_Indexer" which takes a string as parameter var indexerMethod = expression?.Type.GetMethod($"get_{indexerName}", [typeof(string)]); if (indexerMethod != null) { - return Expression.Call(expression, indexerMethod, Expression.Constant(id)); + if (!isDynamicClass) + { + return Expression.Call(expression, indexerMethod, Expression.Constant(id)); + } + + var containsPropertyMethod = typeof(DynamicClass).GetMethod("ContainsProperty"); + if (containsPropertyMethod == null) + { + return Expression.Call(expression, indexerMethod, Expression.Constant(id)); + } + + var callContainsPropertyExpression = Expression.Call( + expression!, + containsPropertyMethod, + Expression.Constant(id) + ); + + return Expression.Condition( + Expression.Equal(callContainsPropertyExpression, Expression.Constant(true)), + Expression.Call(expression, indexerMethod, Expression.Constant(id)), + Expression.Constant(null) + ); } } diff --git a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs index d07d8052..759f156f 100644 --- a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs @@ -517,12 +517,14 @@ public void Where_With_Select() [InlineData("notExisting == \"1\"")] [InlineData("notExisting == \"something\"")] [InlineData("notExisting > 1")] + [InlineData("notExisting < 1")] [InlineData("true == notExisting")] [InlineData("\"true\" == notExisting")] [InlineData("1 == notExisting")] [InlineData("\"1\" == notExisting")] [InlineData("\"something\" == notExisting")] [InlineData("1 < notExisting")] + [InlineData("1 > notExisting")] public void Where_NonExistingMember_EmptyResult(string predicate) { // Arrange diff --git a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs index dd5e62ee..9a81f76a 100644 --- a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs @@ -546,12 +546,14 @@ public void Where_With_Select() [InlineData("notExisting == \"1\"")] [InlineData("notExisting == \"something\"")] [InlineData("notExisting > 1")] + [InlineData("notExisting < 1")] [InlineData("true == notExisting")] [InlineData("\"true\" == notExisting")] [InlineData("1 == notExisting")] [InlineData("\"1\" == notExisting")] [InlineData("\"something\" == notExisting")] [InlineData("1 < notExisting")] + [InlineData("1 > notExisting")] public void Where_NonExistingMember_EmptyResult(string predicate) { // Act