diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs index cf9f3dad..3074a326 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs @@ -296,11 +296,20 @@ public bool ExpressionQualifiesForNullPropagation(Expression expression) { return expression is MemberExpression || - expression is ParameterExpression || + expression is ParameterExpression || expression is MethodCallExpression || expression is UnaryExpression; } + public Expression GenerateDefaultExpression(Type type) + { +#if NET35 + return Expression.Constant(Activator.CreateInstance(type)); +#else + return Expression.Default(type); +#endif + } + private Expression GetMemberExpression(Expression expression) { if (ExpressionQualifiesForNullPropagation(expression)) @@ -330,11 +339,16 @@ private List CollectExpressions(bool addSelf, Expression sourceExpre var list = new List(); - if (addSelf && expression is MemberExpression memberExpressionFirst) + if (addSelf) { - if (TypeHelper.IsNullableType(memberExpressionFirst.Type) || !memberExpressionFirst.Type.GetTypeInfo().IsValueType) + switch (expression) { - list.Add(sourceExpression); + case MemberExpression _: + list.Add(sourceExpression); + break; + + default: + break; } } diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index c871568e..e53d00ad 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -219,7 +219,7 @@ Expression ParseConditionalOperator() _textParser.ValidateToken(TokenId.Colon, Res.ColonExpected); _textParser.NextToken(); Expression expr2 = ParseConditionalOperator(); - expr = GenerateConditional(expr, expr1, expr2, errorPos); + expr = GenerateConditional(expr, expr1, expr2, false, errorPos); } return expr; } @@ -1145,7 +1145,7 @@ Expression ParseFunctionIif() throw ParseError(errorPos, Res.IifRequiresThreeArgs); } - return GenerateConditional(args[0], args[1], args[2], errorPos); + return GenerateConditional(args[0], args[1], args[2], false, errorPos); } // np(...) function @@ -1166,9 +1166,9 @@ Expression ParseFunctionNullPropagation() bool hasDefaultParameter = args.Length == 2; Expression expressionIfFalse = hasDefaultParameter ? args[1] : Expression.Constant(null); - if (_expressionHelper.TryGenerateAndAlsoNotNullExpression(args[0], hasDefaultParameter, out Expression generatedExpression)) + if (_expressionHelper.TryGenerateAndAlsoNotNullExpression(args[0], true, out Expression generatedExpression)) { - return GenerateConditional(generatedExpression, args[0], expressionIfFalse, errorPos); + return GenerateConditional(generatedExpression, args[0], expressionIfFalse, true, errorPos); } return args[0]; @@ -1234,7 +1234,7 @@ Expression ParseFunctionCast() return Expression.ConvertChecked(_it, resolvedType); } - Expression GenerateConditional(Expression test, Expression expressionIfTrue, Expression expressionIfFalse, int errorPos) + Expression GenerateConditional(Expression test, Expression expressionIfTrue, Expression expressionIfFalse, bool nullPropagating, int errorPos) { if (test.Type != typeof(bool)) { @@ -1244,30 +1244,71 @@ Expression GenerateConditional(Expression test, Expression expressionIfTrue, Exp if (expressionIfTrue.Type != expressionIfFalse.Type) { // If expressionIfTrue is a null constant and expressionIfFalse is ValueType: - // - create nullable constant from expressionIfTrue with type from expressionIfFalse - // - convert expressionIfFalse to nullable (unless it's already nullable) if (Constants.IsNull(expressionIfTrue) && expressionIfFalse.Type.GetTypeInfo().IsValueType) { - Type nullableType = TypeHelper.ToNullableType(expressionIfFalse.Type); - expressionIfTrue = Expression.Constant(null, nullableType); - if (!TypeHelper.IsNullableType(expressionIfFalse.Type)) + if (nullPropagating && _parsingConfig.NullPropagatingUseDefaultValueForNonNullableValueTypes) { - expressionIfFalse = Expression.Convert(expressionIfFalse, nullableType); + // If expressionIfFalse is a non-nullable type: + // generate default expression from the expressionIfFalse-type for expressionIfTrue + // Else + // create nullable constant from expressionIfTrue with type from expressionIfFalse + + if (!TypeHelper.IsNullableType(expressionIfFalse.Type)) + { + expressionIfTrue = _expressionHelper.GenerateDefaultExpression(expressionIfFalse.Type); + } + else + { + expressionIfTrue = Expression.Constant(null, expressionIfFalse.Type); + } + } + else + { + // - create nullable constant from expressionIfTrue with type from expressionIfFalse + // - convert expressionIfFalse to nullable (unless it's already nullable) + + Type nullableType = TypeHelper.ToNullableType(expressionIfFalse.Type); + expressionIfTrue = Expression.Constant(null, nullableType); + + if (!TypeHelper.IsNullableType(expressionIfFalse.Type)) + { + expressionIfFalse = Expression.Convert(expressionIfFalse, nullableType); + } } return Expression.Condition(test, expressionIfTrue, expressionIfFalse); } // If expressionIfFalse is a null constant and expressionIfTrue is a ValueType: - // - create nullable constant from expressionIfFalse with type from expressionIfTrue - // - convert expressionIfTrue to nullable (unless it's already nullable) if (Constants.IsNull(expressionIfFalse) && expressionIfTrue.Type.GetTypeInfo().IsValueType) { - Type nullableType = TypeHelper.ToNullableType(expressionIfTrue.Type); - expressionIfFalse = Expression.Constant(null, nullableType); - if (!TypeHelper.IsNullableType(expressionIfTrue.Type)) + if (nullPropagating && _parsingConfig.NullPropagatingUseDefaultValueForNonNullableValueTypes) { - expressionIfTrue = Expression.Convert(expressionIfTrue, nullableType); + // If expressionIfTrue is a non-nullable type: + // generate default expression from the expressionIfFalse-type for expressionIfFalse + // Else + // create nullable constant from expressionIfFalse with type from expressionIfTrue + + if (!TypeHelper.IsNullableType(expressionIfTrue.Type)) + { + expressionIfFalse = _expressionHelper.GenerateDefaultExpression(expressionIfTrue.Type); + } + else + { + expressionIfFalse = Expression.Constant(null, expressionIfTrue.Type); + } + } + else + { + // - create nullable constant from expressionIfFalse with type from expressionIfTrue + // - convert expressionIfTrue to nullable (unless it's already nullable) + + Type nullableType = TypeHelper.ToNullableType(expressionIfTrue.Type); + expressionIfFalse = Expression.Constant(null, nullableType); + if (!TypeHelper.IsNullableType(expressionIfTrue.Type)) + { + expressionIfTrue = Expression.Convert(expressionIfTrue, nullableType); + } } return Expression.Condition(test, expressionIfTrue, expressionIfFalse); diff --git a/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs index 64ba483a..cabae88c 100644 --- a/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs @@ -37,5 +37,7 @@ internal interface IExpressionHelper bool MemberExpressionIsDynamic(Expression expression); Expression ConvertToExpandoObjectAndCreateDynamicExpression(Expression expression, Type type, string propertyName); + + Expression GenerateDefaultExpression(Type type); } } diff --git a/src/System.Linq.Dynamic.Core/ParsingConfig.cs b/src/System.Linq.Dynamic.Core/ParsingConfig.cs index 8df9fdd8..856c67b4 100644 --- a/src/System.Linq.Dynamic.Core/ParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core/ParsingConfig.cs @@ -191,5 +191,12 @@ public IQueryableAnalyzer QueryableAnalyzer /// Additional TypeConverters /// public IDictionary TypeConverters { get; set; } + + /// + /// When using the NullPropagating function np(...), use a "default value" for non-nullable value types instead of "null value". + /// + /// Default value is false. + /// + public bool NullPropagatingUseDefaultValueForNonNullableValueTypes { get; set; } = false; } } diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs index 2b00d294..f8c80622 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; using System.Linq.Dynamic.Core.CustomTypeProviders; using System.Linq.Dynamic.Core.Exceptions; using System.Linq.Dynamic.Core.Tests.Helpers.Models; @@ -327,9 +328,11 @@ public void DynamicExpressionParser_ParseLambda_UseParameterizedNamesInDynamicQu public void DynamicExpressionParser_ParseLambda_UseParameterizedNamesInDynamicQuery_ForNullableProperty_true(string propName, string valueString) { // Assign + var culture = CultureInfo.CreateSpecificCulture("en-US"); var config = new ParsingConfig { - UseParameterizedNamesInDynamicQuery = true + UseParameterizedNamesInDynamicQuery = true, + NumberParseCulture = culture }; // Act @@ -348,7 +351,7 @@ public void DynamicExpressionParser_ParseLambda_UseParameterizedNamesInDynamicQu var propertyInfo = wrapperObj.GetType().GetProperty("Value", BindingFlags.Instance | BindingFlags.Public); object value = propertyInfo.GetValue(wrapperObj); - Check.That(value).IsEqualTo(Convert.ChangeType(valueString, Nullable.GetUnderlyingType(queriedPropType) ?? queriedPropType)); + value.Should().Be(Convert.ChangeType(valueString, Nullable.GetUnderlyingType(queriedPropType) ?? queriedPropType, culture)); } [Theory] @@ -1070,7 +1073,7 @@ public void DynamicExpressionParser_ParseLambda_Operator_Less_Greater_With_Guids // Assert Assert.Equal(anotherId, result); - } + } [Theory] [InlineData("c => c.Age == 8", "c => (c.Age == 8)")] @@ -1239,7 +1242,7 @@ public void DynamicExpressionParser_ParseLambda_String_TrimEnd_0_Parameters() var @delegate = expression.Compile(); - var result = (bool) @delegate.DynamicInvoke("This is a test "); + var result = (bool)@delegate.DynamicInvoke("This is a test "); // Assert result.Should().BeTrue(); diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs index ecbd1089..8c2ba32c 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs @@ -1478,19 +1478,21 @@ public void ExpressionTests_NullCoalescing() } [Theory] - [InlineData("np(str)", "Select(Param_0 => Param_0.str)")] - [InlineData("np(strNull)", "Select(Param_0 => Param_0.strNull)")] + [InlineData("np(str)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.str != null)), Param_0.str, null))")] [InlineData("np(str, \"x\")", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.str != null)), Param_0.str, \"x\"))")] - [InlineData("np(g)", "Select(Param_0 => Param_0.g)")] - [InlineData("np(gnullable)", "Select(Param_0 => Param_0.gnullable)")] - [InlineData("np(dt)", "Select(Param_0 => Param_0.dt)")] - [InlineData("np(dtnullable)", "Select(Param_0 => Param_0.dtnullable)")] - [InlineData("np(number)", "Select(Param_0 => Param_0.number)")] - [InlineData("np(nullablenumber)", "Select(Param_0 => Param_0.nullablenumber)")] - [InlineData("np(_enum)", "Select(Param_0 => Param_0._enum)")] - [InlineData("np(_enumnullable)", "Select(Param_0 => Param_0._enumnullable)")] + [InlineData("np(strNull)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.strNull != null)), Param_0.strNull, null))")] + [InlineData("np(strNull, \"x\")", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.strNull != null)), Param_0.strNull, \"x\"))")] + [InlineData("np(gnullable)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.gnullable != null)), Param_0.gnullable, null))")] + [InlineData("np(dtnullable)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.dtnullable != null)), Param_0.dtnullable, null))")] + [InlineData("np(number, 42)", "Select(Param_0 => IIF((Param_0 != null), Param_0.number, 42))")] + [InlineData("np(nullablenumber)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nullablenumber != null)), Param_0.nullablenumber, null))")] + [InlineData("np(_enumnullable)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0._enumnullable != null)), Param_0._enumnullable, null))")] #if NET452 + [InlineData("np(dt)", "Select(Param_0 => IIF((Param_0 != null), Convert(Param_0.dt), null))")] + [InlineData("np(_enum)", "Select(Param_0 => IIF((Param_0 != null), Convert(Param_0._enum), null))")] + [InlineData("np(g)", "Select(Param_0 => IIF((Param_0 != null), Convert(Param_0.g), null))")] + [InlineData("np(number)", "Select(Param_0 => IIF((Param_0 != null), Convert(Param_0.number), null))")] [InlineData("np(nested.g)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Convert(Param_0.nested.g), null))")] [InlineData("np(nested.dt)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Convert(Param_0.nested.dt), null))")] [InlineData("np(nested.number)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Convert(Param_0.nested.number), null))")] @@ -1499,23 +1501,27 @@ public void ExpressionTests_NullCoalescing() [InlineData("np(item.GuidNormal)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.item != null)), Convert(Param_0.item.GuidNormal), null))")] [InlineData("np(nested.dtnullable.Value.Year)", "Select(Param_0 => IIF((((Param_0 != null) AndAlso (Param_0.nested != null)) AndAlso (Param_0.nested.dtnullable != null)), Convert(Param_0.nested.dtnullable.Value.Year), null))")] #else + [InlineData("np(dt)", "Select(Param_0 => IIF((Param_0 != null), Convert(Param_0.dt, Nullable`1), null))")] + [InlineData("np(_enum)", "Select(Param_0 => IIF((Param_0 != null), Convert(Param_0._enum, Nullable`1), null))")] + [InlineData("np(g)", "Select(Param_0 => IIF((Param_0 != null), Convert(Param_0.g, Nullable`1), null))")] + [InlineData("np(number)", "Select(Param_0 => IIF((Param_0 != null), Convert(Param_0.number, Nullable`1), null))")] [InlineData("np(nested.g)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Convert(Param_0.nested.g, Nullable`1), null))")] [InlineData("np(nested.dt)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Convert(Param_0.nested.dt, Nullable`1), null))")] [InlineData("np(nested.number)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Convert(Param_0.nested.number, Nullable`1), null))")] + [InlineData("np(nested.number, 42)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Param_0.nested.number, 42))")] [InlineData("np(nested._enum)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Convert(Param_0.nested._enum, Nullable`1), null))")] [InlineData("np(item.Id)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.item != null)), Convert(Param_0.item.Id, Nullable`1), null))")] [InlineData("np(item.GuidNormal)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.item != null)), Convert(Param_0.item.GuidNormal, Nullable`1), null))")] [InlineData("np(nested.dtnullable.Value.Year)", "Select(Param_0 => IIF((((Param_0 != null) AndAlso (Param_0.nested != null)) AndAlso (Param_0.nested.dtnullable != null)), Convert(Param_0.nested.dtnullable.Value.Year, Nullable`1), null))")] #endif - - [InlineData("np(nested.strNull)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Param_0.nested.strNull, null))")] + [InlineData("np(nested.strNull)", "Select(Param_0 => IIF((((Param_0 != null) AndAlso (Param_0.nested != null)) AndAlso (Param_0.nested.strNull != null)), Param_0.nested.strNull, null))")] [InlineData("np(nested.strNull, \"x\")", "Select(Param_0 => IIF((((Param_0 != null) AndAlso (Param_0.nested != null)) AndAlso (Param_0.nested.strNull != null)), Param_0.nested.strNull, \"x\"))")] - [InlineData("np(nested.gnullable)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Param_0.nested.gnullable, null))")] - [InlineData("np(nested.dtnullable)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Param_0.nested.dtnullable, null))")] - [InlineData("np(nested.nullablenumber)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Param_0.nested.nullablenumber, null))")] + [InlineData("np(nested.gnullable)", "Select(Param_0 => IIF((((Param_0 != null) AndAlso (Param_0.nested != null)) AndAlso (Param_0.nested.gnullable != null)), Param_0.nested.gnullable, null))")] + [InlineData("np(nested.dtnullable)", "Select(Param_0 => IIF((((Param_0 != null) AndAlso (Param_0.nested != null)) AndAlso (Param_0.nested.dtnullable != null)), Param_0.nested.dtnullable, null))")] + [InlineData("np(nested.nullablenumber)", "Select(Param_0 => IIF((((Param_0 != null) AndAlso (Param_0.nested != null)) AndAlso (Param_0.nested.nullablenumber != null)), Param_0.nested.nullablenumber, null))")] [InlineData("np(nested.nullablenumber, 42)", "Select(Param_0 => IIF((((Param_0 != null) AndAlso (Param_0.nested != null)) AndAlso (Param_0.nested.nullablenumber != null)), Param_0.nested.nullablenumber, 42))")] - [InlineData("np(nested._enumnullable)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Param_0.nested._enumnullable, null))")] - [InlineData("np(item.GuidNull)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.item != null)), Param_0.item.GuidNull, null))")] + [InlineData("np(nested._enumnullable)", "Select(Param_0 => IIF((((Param_0 != null) AndAlso (Param_0.nested != null)) AndAlso (Param_0.nested._enumnullable != null)), Param_0.nested._enumnullable, null))")] + [InlineData("np(item.GuidNull)", "Select(Param_0 => IIF((((Param_0 != null) AndAlso (Param_0.item != null)) AndAlso (Param_0.item.GuidNull != null)), Param_0.item.GuidNull, null))")] [InlineData("np(items.FirstOrDefault())", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.items != null)), Param_0.items.FirstOrDefault(), null))")] [InlineData("np(items.FirstOrDefault(it != \"x\"))", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.items != null)), Param_0.items.FirstOrDefault(Param_1 => (Param_1 != \"x\")), null))")] public void ExpressionTests_NullPropagating(string test, string query) @@ -1567,6 +1573,66 @@ public void ExpressionTests_NullPropagating(string test, string query) Check.That(queryAsString).Equals(query); } + [Theory] + [InlineData("np(number)", "Select(Param_0 => IIF((Param_0 != null), Param_0.number, default(Int32)))")] + [InlineData("np(number, 42)", "Select(Param_0 => IIF((Param_0 != null), Param_0.number, 42))")] + [InlineData("np(nested.dtnullable.Value.Year)", "Select(Param_0 => IIF((((Param_0 != null) AndAlso (Param_0.nested != null)) AndAlso (Param_0.nested.dtnullable != null)), Param_0.nested.dtnullable.Value.Year, default(Int32)))")] + [InlineData("np(nested.number)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Param_0.nested.number, default(Int32)))")] + public void ExpressionTests_NullPropagating_Config_Has_UseDefault(string test, string query) + { + // Arrange + var config = new ParsingConfig + { + NullPropagatingUseDefaultValueForNonNullableValueTypes = true + }; + + var q = new[] + { + new + { + g = Guid.NewGuid(), + gnullable = (Guid?) Guid.NewGuid(), + dt = DateTime.Now, + dtnullable = (DateTime?) DateTime.Now, + _enum = TestEnum2.Var1, + _enumnullable = (TestEnum2?) TestEnum2.Var2, + number = 1, + nullablenumber = (int?) 2, + str = "str", + strNull = (string) null, + nested = new + { + g = Guid.NewGuid(), + gnullable = (Guid?) Guid.NewGuid(), + dt = DateTime.Now, + dtnullable = (DateTime?) DateTime.Now, + _enum = TestEnum2.Var1, + _enumnullable = (TestEnum2?) TestEnum2.Var2, + number = 1, + nullablenumber = (int?) 2, + str = "str", + strNull = (string) null + }, + item = new TestGuidNullClass + { + Id = 100, + GuidNormal = Guid.NewGuid(), + GuidNull = Guid.NewGuid() + }, + items = new [] { "a", "b" } + } + }.AsQueryable(); + + // Act + var resultDynamic = q.Select(config, test); + + // Assert + string queryAsString = resultDynamic.ToString(); + queryAsString = queryAsString.Substring(queryAsString.IndexOf(".Select") + 1).TrimEnd(']'); + Check.That(queryAsString).Equals(query); + } + + [Fact] public void ExpressionTests_NullPropagating_InstanceMethod_Zero_Arguments() { @@ -1671,7 +1737,23 @@ public void ExpressionTests_NullPropagation_NullableDateTime() } [Fact] - public void ExpressionTests_NullPropagating_NestedInteger_WithoutDefaultValue() + public void ExpressionTests_NullPropagating_Nested1Integer_WithoutDefaultValue() + { + // Arrange + var testModels = User.GenerateSampleModels(2, true).ToList(); + testModels.Add(null); // Add null User + testModels[0].NullableInt = null; // Set the NullableInt to null for first User + + // Act + var result = testModels.AsQueryable().Select(t => t != null && t.NullableInt != null ? t.NullableInt : null).ToArray(); + var resultDynamic = testModels.AsQueryable().Select("np(it.NullableInt)").ToDynamicArray(); + + // Assert + Check.That(resultDynamic).ContainsExactly(result); + } + + [Fact] + public void ExpressionTests_NullPropagating_Nested3Integer_WithoutDefaultValue() { // Arrange var testModels = User.GenerateSampleModels(2, true).ToList(); @@ -1686,6 +1768,27 @@ public void ExpressionTests_NullPropagating_NestedInteger_WithoutDefaultValue() Check.That(resultDynamic).ContainsExactly(result); } + [Fact] + public void ExpressionTests_NullPropagating_Nested3Integer_WithoutDefaultValue_Config_Has_UseDefault() + { + // Arrange + var config = new ParsingConfig + { + NullPropagatingUseDefaultValueForNonNullableValueTypes = true + }; + + var testModels = User.GenerateSampleModels(2, true).ToList(); + testModels.Add(null); // Add null User + testModels[0].Profile = null; // Set the Profile to null for first User + + // Act + var result = testModels.AsQueryable().Select(t => t != null && t.Profile != null && t.Profile.UserProfileDetails != null ? t.Profile.UserProfileDetails.Id : default(long)).ToArray(); + var resultDynamic = testModels.AsQueryable().Select(config, "np(it.Profile.UserProfileDetails.Id)").ToDynamicArray(); + + // Assert + Check.That(resultDynamic).ContainsExactly(result); + } + [Fact] public void ExpressionTests_NullPropagating_NullableInteger_WithDefaultValue() { @@ -1715,7 +1818,23 @@ public void ExpressionTests_NullPropagating_String_WithDefaultValue() } [Fact] - public void ExpressionTests_NullPropagatingNested_WithDefaultValue() + public void ExpressionTests_NullPropagatingNested1_WithDefaultValue() + { + // Arrange + var testModels = User.GenerateSampleModels(2, true).ToList(); + testModels.Add(null); // Add null User + testModels[0].UserName = null; // Set the UserName to null for first User + + // Act + var result = testModels.AsQueryable().Select(t => t != null && t.UserName != null ? t.UserName : "x").ToArray(); + var resultDynamic = testModels.AsQueryable().Select("np(it.UserName, \"x\")").ToDynamicArray(); + + // Assert + Check.That(resultDynamic).ContainsExactly(result); + } + + [Fact] + public void ExpressionTests_NullPropagatingNested3_WithDefaultValue() { // Arrange var testModels = User.GenerateSampleModels(2, true).ToList(); diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionHelperTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionHelperTests.cs index 77f489d1..212df610 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionHelperTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionHelperTests.cs @@ -129,7 +129,7 @@ public void ExpressionHelper_OptimizeStringForEqualityIfPossible_Guid_Invalid() } [Fact] - public void ExpressionHelper_TryGenerateAndAlsoNotNullExpression_NestedNonNullable() + public void ExpressionHelper_TryGenerateAndAlsoNotNullExpression_Nested3NonNullable() { // Assign Expression> expression = x => x.Relation1.Relation2.Id; @@ -139,11 +139,59 @@ public void ExpressionHelper_TryGenerateAndAlsoNotNullExpression_NestedNonNullab // Assert Check.That(result).IsTrue(); - Check.That(generatedExpression.ToString()).IsEqualTo("(((x != null) AndAlso (x.Relation1 != null)) AndAlso (x.Relation1.Relation2 != null))"); + Check.That(generatedExpression.ToString()).IsEqualTo("((((x != null) AndAlso (x.Relation1 != null)) AndAlso (x.Relation1.Relation2 != null)) AndAlso (x => x.Relation1.Relation2.Id != null))"); + } + + [Fact] + public void ExpressionHelper_TryGenerateAndAlsoNotNullExpression_Nested3NonNullable_Config_Has_UseDefault() + { + // Assign + var config = new ParsingConfig + { + NullPropagatingUseDefaultValueForNonNullableValueTypes = true + }; + var expressionHelper = new ExpressionHelper(config); + + Expression> expression = x => x.Relation1.Relation2.Id; + + // Act + bool result = expressionHelper.TryGenerateAndAlsoNotNullExpression(expression, true, out Expression generatedExpression); + + // Assert + Check.That(result).IsTrue(); + Check.That(generatedExpression.ToString()).IsEqualTo("((((x != null) AndAlso (x.Relation1 != null)) AndAlso (x.Relation1.Relation2 != null)) AndAlso (x => x.Relation1.Relation2.Id != null))"); + } + + [Fact] + public void ExpressionHelper_TryGenerateAndAlsoNotNullExpression_Nested1NullableInt() + { + // Assign + Expression> expression = x => x.IdNullable; + + // Act + bool result = _expressionHelper.TryGenerateAndAlsoNotNullExpression(expression, true, out Expression generatedExpression); + + // Assert + Check.That(result).IsTrue(); + Check.That(generatedExpression.ToString()).IsEqualTo("((x != null) AndAlso (x => x.IdNullable != null))"); + } + + [Fact] + public void ExpressionHelper_TryGenerateAndAlsoNotNullExpression_Nested1NullableString() + { + // Assign + Expression> expression = x => x.S; + + // Act + bool result = _expressionHelper.TryGenerateAndAlsoNotNullExpression(expression, true, out Expression generatedExpression); + + // Assert + Check.That(result).IsTrue(); + Check.That(generatedExpression.ToString()).IsEqualTo("((x != null) AndAlso (x => x.S != null))"); } [Fact] - public void ExpressionHelper_TryGenerateAndAlsoNotNullExpression_NestedNullable_AddSelfFalse() + public void ExpressionHelper_TryGenerateAndAlsoNotNullExpression_Nested3Nullable_AddSelfFalse() { // Assign Expression> expression = x => x.Relation1.Relation2.IdNullable; @@ -157,7 +205,7 @@ public void ExpressionHelper_TryGenerateAndAlsoNotNullExpression_NestedNullable_ } [Fact] - public void ExpressionHelper_TryGenerateAndAlsoNotNullExpression_NestedNullable_AddSelfTrue() + public void ExpressionHelper_TryGenerateAndAlsoNotNullExpression_Nested3Nullable_AddSelfTrue() { // Assign Expression> expression = x => x.Relation1.Relation2.IdNullable; @@ -171,17 +219,17 @@ public void ExpressionHelper_TryGenerateAndAlsoNotNullExpression_NestedNullable_ } [Fact] - public void ExpressionHelper_TryGenerateAndAlsoNotNullExpression_NonNullable() + public void ExpressionHelper_TryGenerateAndAlsoNotNullExpression_Nullable() { // Assign - Expression> expression = x => x.Id; + Expression> expression = x => x.Id; // Act bool result = _expressionHelper.TryGenerateAndAlsoNotNullExpression(expression, true, out Expression generatedExpression); // Assert - Check.That(result).IsFalse(); - Check.That(generatedExpression.ToString()).IsEqualTo("x => x.Id"); + result.Should().BeTrue(); + generatedExpression.ToString().Should().StartWith("((x != null) AndAlso (x =>").And.EndWith("!= null))"); } class Item @@ -201,6 +249,8 @@ class Relation2 public int Id { get; set; } public int? IdNullable { get; set; } + + public string S { get; set; } } } }