From 8874662ff18b8f73140e6cc289415b265868b584 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sun, 2 Nov 2025 14:30:05 +0100 Subject: [PATCH 01/21] Fix ExpressionHelper.TryConvertTypes to generate correct Convert in case left or right is null (#954) * Fix ExpressionHelper.TryConvertTypes to generate correct Convert in case left or right is null * opt * Update test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * . --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../NewtonsoftJsonExtensions.cs | 5 +- .../Parser/ExpressionHelper.cs | 19 +++++- .../Parser/IExpressionHelper.cs | 2 +- .../NewtonsoftJsonTests.cs | 61 ++++++++++++++++++- .../SystemTextJsonTests.cs | 31 +++++++++- .../DynamicClassTest.cs | 18 +++--- 6 files changed, 121 insertions(+), 15 deletions(-) diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs index 7ca9d6e3..8aefa397 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs @@ -821,7 +821,7 @@ public static JArray Where(this JArray source, NewtonsoftJsonParsingConfig confi if (source.Count == 0) { - return new JArray(); + return []; } var queryable = ToQueryable(source, config); @@ -848,7 +848,8 @@ public static JArray Where(this JArray source, NewtonsoftJsonParsingConfig confi private static JArray ToJArray(Func func) { var array = new JArray(); - foreach (var dynamicElement in func()) + var funcResult = func(); + foreach (var dynamicElement in funcResult) { var element = dynamicElement switch { diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs index 7d72ad61..fefd5b66 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs @@ -361,7 +361,12 @@ public bool ExpressionQualifiesForNullPropagation(Expression? expression) public Expression GenerateDefaultExpression(Type type) { #if NET35 - return Expression.Constant(Activator.CreateInstance(type)); + if (type.IsValueType) + { + return Expression.Constant(Activator.CreateInstance(type), type); + } + + return Expression.Constant(null, type); #else return Expression.Default(type); #endif @@ -388,11 +393,19 @@ public bool TryConvertTypes(ref Expression left, ref Expression right) if (left.Type == typeof(object)) { - left = Expression.Convert(left, right.Type); + left = Expression.Condition( + Expression.Equal(left, Expression.Constant(null, typeof(object))), + GenerateDefaultExpression(right.Type), + Expression.Convert(left, right.Type) + ); } else if (right.Type == typeof(object)) { - right = Expression.Convert(right, left.Type); + right = Expression.Condition( + Expression.Equal(right, Expression.Constant(null, typeof(object))), + GenerateDefaultExpression(left.Type), + Expression.Convert(right, left.Type) + ); } return true; diff --git a/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs index bbc691cd..4e52949b 100644 --- a/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs @@ -52,5 +52,5 @@ internal interface IExpressionHelper /// /// If the types are different (and not null), try to convert the object type to other type. /// - public bool TryConvertTypes(ref Expression left, ref Expression right); + bool TryConvertTypes(ref Expression left, ref Expression right); } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs index 2e94a212..40185d45 100644 --- a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs @@ -1,4 +1,5 @@ -using FluentAssertions; +using System.Linq.Dynamic.Core.NewtonsoftJson.Config; +using FluentAssertions; using Newtonsoft.Json.Linq; using Xunit; @@ -506,4 +507,62 @@ public void Where_With_Select() var first = result.First(); first.Value().Should().Be("Doe"); } + + //[Fact] + //public void Where_OptionalProperty() + //{ + // // Arrange + // var config = new NewtonsoftJsonParsingConfig + // { + // ConvertObjectToSupportComparison = true + // }; + // var array = + // """ + // [ + // { + // "Name": "John", + // "Age": 30 + // }, + // { + // "Name": "Doe" + // } + // ] + // """; + + // // Act + // var result = JArray.Parse(array).Where(config, "Age > 30").Select("Name"); + + // // Assert + // result.Should().HaveCount(1); + // var first = result.First(); + // first.Value().Should().Be("John"); + //} + + [Theory] + [InlineData("notExisting == true")] + [InlineData("notExisting == \"true\"")] + [InlineData("notExisting == 1")] + [InlineData("notExisting == \"1\"")] + [InlineData("notExisting == \"something\"")] + [InlineData("notExisting > 1")] + [InlineData("true == notExisting")] + [InlineData("\"true\" == notExisting")] + [InlineData("1 == notExisting")] + [InlineData("\"1\" == notExisting")] + [InlineData("\"something\" == notExisting")] + [InlineData("1 < notExisting")] + public void Where_NonExistingMember_EmptyResult(string predicate) + { + // Arrange + var config = new NewtonsoftJsonParsingConfig + { + ConvertObjectToSupportComparison = true + }; + + // Act + var result = _source.Where(config, predicate); + + // Assert + result.Should().BeEmpty(); + } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs index f5dee221..1e817664 100644 --- a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Linq.Dynamic.Core.SystemTextJson.Config; +using System.Text.Json; using FluentAssertions; using Xunit; @@ -535,4 +536,32 @@ public void Where_With_Select() array.Should().HaveCount(1); array.First().GetString().Should().Be("Doe"); } + + [Theory] + [InlineData("notExisting == true")] + [InlineData("notExisting == \"true\"")] + [InlineData("notExisting == 1")] + [InlineData("notExisting == \"1\"")] + [InlineData("notExisting == \"something\"")] + [InlineData("notExisting > 1")] + [InlineData("true == notExisting")] + [InlineData("\"true\" == notExisting")] + [InlineData("1 == notExisting")] + [InlineData("\"1\" == notExisting")] + [InlineData("\"something\" == notExisting")] + [InlineData("1 < notExisting")] + public void Where_NonExistingMember_EmptyResult(string predicate) + { + // Arrange + var config = new SystemTextJsonParsingConfig + { + ConvertObjectToSupportComparison = true + }; + + // Act + var result = _source.Where(config, predicate).RootElement.EnumerateArray(); + + // Assert + result.Should().BeEmpty(); + } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs index 32dd3002..6d33cae8 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs @@ -131,20 +131,24 @@ public void DynamicClass_GettingValue_ByIndex_Should_Work() public void DynamicClass_SettingExistingPropertyValue_ByIndex_Should_Work() { // Arrange - var test = "Test"; - var newTest = "abc"; - var range = new List + var originalValue = "Test"; + var newValue = "abc"; + var array = new object[] { - new { FieldName = test, Value = 3.14159 } + new + { + FieldName = originalValue, + Value = 3.14159 + } }; // Act - var rangeResult = range.AsQueryable().Select("new(FieldName as FieldName)").ToDynamicList(); + var rangeResult = array.AsQueryable().Select("new(FieldName as FieldName)").ToDynamicList(); var item = rangeResult.First(); - item["FieldName"] = newTest; + item["FieldName"] = newValue; var value = item["FieldName"] as string; - value.Should().Be(newTest); + value.Should().Be(newValue); } [Fact] From 3aa76d998278d14dc795be625d25df8db7f2f928 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 8 Nov 2025 07:49:09 +0100 Subject: [PATCH 02/21] v1.6.10 --- CHANGELOG.md | 8 +++++++- Generate-ReleaseNotes.bat | 2 +- version.xml | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 210e2f20..cbaa99da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ -# v1.6.9 (10 October 2025) +# v1.6.10 (08 November 2025) +- [#953](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/953) - Fixed adding Enum and integer [bug] contributed by [StefH](https://github.com/StefH) +- [#954](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/954) - Fix ExpressionHelper.TryConvertTypes to generate correct Convert in case left or right is null [bug] contributed by [StefH](https://github.com/StefH) +- [#951](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/951) - Parsing error adding numeric constant to enum value [bug] +- [#952](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/952) - Json: How to handle not existing member [bug] + +# v1.6.9 (11 October 2025) - [#950](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/950) - DynamicExpressionParser - Handle indexed properties with any number of indices in expression [bug] contributed by [thibault-reigner](https://github.com/thibault-reigner) # v1.6.8 (28 September 2025) diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index aa50344d..3196cbc2 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.6.9 +SET version=v1.6.10 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 61507fe2..79743c43 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 9 + 10 \ No newline at end of file From 3f88b9776348e6d5a0e22687cc13a20108273868 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sun, 9 Nov 2025 13:42:49 +0100 Subject: [PATCH 03/21] Fix parsing Hex and Binary (#956) * Fix parsing Hex and Binary * fix --- .../Parser/ExpressionParser.cs | 5 +- .../Parser/NumberParser.cs | 389 +++++++++--------- .../DynamicExpressionParserTests.cs | 43 +- .../Parser/NumberParserTests.cs | 6 +- 4 files changed, 235 insertions(+), 208 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 4cc77a00..b0fb8157 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -984,11 +984,12 @@ private Expression ParseRealLiteral() { _textParser.ValidateToken(TokenId.RealLiteral); - string text = _textParser.CurrentToken.Text; + var text = _textParser.CurrentToken.Text; + var textOriginal = text; _textParser.NextToken(); - return _numberParser.ParseRealLiteral(text, text[text.Length - 1], true); + return _numberParser.ParseRealLiteral(text, textOriginal, text[text.Length - 1], true); } private Expression ParseParenExpression() diff --git a/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs b/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs index 7fdfdaad..798ff097 100644 --- a/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs @@ -4,244 +4,234 @@ using System.Linq.Expressions; using System.Text.RegularExpressions; -namespace System.Linq.Dynamic.Core.Parser +namespace System.Linq.Dynamic.Core.Parser; + +/// +/// NumberParser +/// +public class NumberParser { + private static readonly Regex RegexBinary32 = new("^[01]{1,32}$", RegexOptions.Compiled); + private static readonly Regex RegexBinary64 = new("^[01]{1,64}$", RegexOptions.Compiled); + private static readonly char[] Qualifiers = { 'U', 'u', 'L', 'l', 'F', 'f', 'D', 'd', 'M', 'm' }; + private static readonly char[] QualifiersHex = { 'U', 'u', 'L', 'l' }; + private static readonly string[] QualifiersReal = { "F", "f", "D", "d", "M", "m" }; + private readonly ConstantExpressionHelper _constantExpressionHelper; + + private readonly CultureInfo _culture; + /// - /// NumberParser + /// Initializes a new instance of the class. /// - public class NumberParser + /// The ParsingConfig. + public NumberParser(ParsingConfig? config) { - private static readonly Regex RegexBinary32 = new("^[01]{1,32}$", RegexOptions.Compiled); - private static readonly Regex RegexBinary64 = new("^[01]{1,64}$", RegexOptions.Compiled); - private static readonly char[] Qualifiers = { 'U', 'u', 'L', 'l', 'F', 'f', 'D', 'd', 'M', 'm' }; - private static readonly char[] QualifiersHex = { 'U', 'u', 'L', 'l' }; - private static readonly string[] QualifiersReal = { "F", "f", "D", "d", "M", "m" }; - private readonly ConstantExpressionHelper _constantExpressionHelper; - - private readonly CultureInfo _culture; - - /// - /// Initializes a new instance of the class. - /// - /// The ParsingConfig. - public NumberParser(ParsingConfig? config) - { - _culture = config?.NumberParseCulture ?? CultureInfo.InvariantCulture; - _constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(config ?? ParsingConfig.Default); - } + _culture = config?.NumberParseCulture ?? CultureInfo.InvariantCulture; + _constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(config ?? ParsingConfig.Default); + } - /// - /// Tries to parse the text into a IntegerLiteral ConstantExpression. - /// - /// The current token position (needed for error reporting). - /// The text. - public Expression ParseIntegerLiteral(int tokenPosition, string text) - { - Check.NotEmpty(text, nameof(text)); + /// + /// Tries to parse the text into a IntegerLiteral ConstantExpression. + /// + /// The current token position (needed for error reporting). + /// The text. + public Expression ParseIntegerLiteral(int tokenPosition, string text) + { + Check.NotEmpty(text); - var last = text[text.Length - 1]; - var isNegative = text[0] == '-'; - var isHexadecimal = text.StartsWith(isNegative ? "-0x" : "0x", StringComparison.OrdinalIgnoreCase); - var isBinary = text.StartsWith(isNegative ? "-0b" : "0b", StringComparison.OrdinalIgnoreCase); - var qualifiers = isHexadecimal ? QualifiersHex : Qualifiers; + var textOriginal = text; + var last = text[text.Length - 1]; + var isNegative = text[0] == '-'; + var isHexadecimal = text.StartsWith(isNegative ? "-0x" : "0x", StringComparison.OrdinalIgnoreCase); + var isBinary = text.StartsWith(isNegative ? "-0b" : "0b", StringComparison.OrdinalIgnoreCase); + var qualifiers = isHexadecimal ? QualifiersHex : Qualifiers; - string? qualifier = null; - if (qualifiers.Contains(last)) + string? qualifier = null; + if (qualifiers.Contains(last)) + { + int pos = text.Length - 1, count = 0; + while (qualifiers.Contains(text[pos])) { - int pos = text.Length - 1, count = 0; - while (qualifiers.Contains(text[pos])) - { - ++count; - --pos; - } - qualifier = text.Substring(text.Length - count, count); - text = text.Substring(0, text.Length - count); + ++count; + --pos; } + qualifier = text.Substring(text.Length - count, count); + text = text.Substring(0, text.Length - count); + } - if (!isNegative) + if (!isNegative) + { + if (isHexadecimal || isBinary) { - if (isHexadecimal || isBinary) - { - text = text.Substring(2); - } - - if (isBinary) - { - return ParseAsBinary(tokenPosition, text, isNegative); - } + text = text.Substring(2); + } - if (!ulong.TryParse(text, isHexadecimal ? NumberStyles.HexNumber : NumberStyles.Integer, _culture, out ulong unsignedValue)) - { - throw new ParseException(string.Format(_culture, Res.InvalidIntegerLiteral, text), tokenPosition); - } + if (isBinary) + { + return ParseAsBinary(tokenPosition, text, textOriginal, isNegative); + } - if (!string.IsNullOrEmpty(qualifier) && qualifier!.Length > 0) - { - if (qualifier == "U" || qualifier == "u") - { - return _constantExpressionHelper.CreateLiteral((uint)unsignedValue, text); - } - - if (qualifier == "L" || qualifier == "l") - { - return _constantExpressionHelper.CreateLiteral((long)unsignedValue, text); - } - - if (QualifiersReal.Contains(qualifier)) - { - return ParseRealLiteral(text, qualifier[0], false); - } - - return _constantExpressionHelper.CreateLiteral(unsignedValue, text); - } + if (!ulong.TryParse(text, isHexadecimal ? NumberStyles.HexNumber : NumberStyles.Integer, _culture, out ulong unsignedValue)) + { + throw new ParseException(string.Format(_culture, Res.InvalidIntegerLiteral, text), tokenPosition); + } - if (unsignedValue <= int.MaxValue) + if (!string.IsNullOrEmpty(qualifier) && qualifier!.Length > 0) + { + if (qualifier == "U" || qualifier == "u") { - return _constantExpressionHelper.CreateLiteral((int)unsignedValue, text); + return _constantExpressionHelper.CreateLiteral((uint)unsignedValue, textOriginal); } - if (unsignedValue <= uint.MaxValue) + if (qualifier == "L" || qualifier == "l") { - return _constantExpressionHelper.CreateLiteral((uint)unsignedValue, text); + return _constantExpressionHelper.CreateLiteral((long)unsignedValue, textOriginal); } - if (unsignedValue <= long.MaxValue) + if (QualifiersReal.Contains(qualifier)) { - return _constantExpressionHelper.CreateLiteral((long)unsignedValue, text); + return ParseRealLiteral(text, textOriginal, qualifier[0], false); } - return _constantExpressionHelper.CreateLiteral(unsignedValue, text); + return _constantExpressionHelper.CreateLiteral(unsignedValue, textOriginal); } - if (isHexadecimal || isBinary) + if (unsignedValue <= int.MaxValue) { - text = text.Substring(3); + return _constantExpressionHelper.CreateLiteral((int)unsignedValue, textOriginal); } - if (isBinary) + if (unsignedValue <= uint.MaxValue) { - return ParseAsBinary(tokenPosition, text, isNegative); + return _constantExpressionHelper.CreateLiteral((uint)unsignedValue, textOriginal); } - if (!long.TryParse(text, isHexadecimal ? NumberStyles.HexNumber : NumberStyles.Integer, _culture, out long value)) + if (unsignedValue <= long.MaxValue) { - throw new ParseException(string.Format(_culture, Res.InvalidIntegerLiteral, text), tokenPosition); + return _constantExpressionHelper.CreateLiteral((long)unsignedValue, textOriginal); } - if (isHexadecimal) - { - value = -value; - } + return _constantExpressionHelper.CreateLiteral(unsignedValue, textOriginal); + } - if (!string.IsNullOrEmpty(qualifier) && qualifier!.Length > 0) - { - if (qualifier == "L" || qualifier == "l") - { - return _constantExpressionHelper.CreateLiteral(value, text); - } + if (isHexadecimal || isBinary) + { + text = text.Substring(3); + } - if (QualifiersReal.Contains(qualifier)) - { - return ParseRealLiteral(text, qualifier[0], false); - } + if (isBinary) + { + return ParseAsBinary(tokenPosition, text, textOriginal, isNegative); + } - throw new ParseException(Res.MinusCannotBeAppliedToUnsignedInteger, tokenPosition); + if (!long.TryParse(text, isHexadecimal ? NumberStyles.HexNumber : NumberStyles.Integer, _culture, out long value)) + { + throw new ParseException(string.Format(_culture, Res.InvalidIntegerLiteral, text), tokenPosition); + } + + if (isHexadecimal) + { + value = -value; + } + + if (!string.IsNullOrEmpty(qualifier) && qualifier!.Length > 0) + { + if (qualifier == "L" || qualifier == "l") + { + return _constantExpressionHelper.CreateLiteral(value, textOriginal); } - if (value <= int.MaxValue) + if (QualifiersReal.Contains(qualifier)) { - return _constantExpressionHelper.CreateLiteral((int)value, text); + return ParseRealLiteral(text, textOriginal, qualifier[0], false); } - return _constantExpressionHelper.CreateLiteral(value, text); + throw new ParseException(Res.MinusCannotBeAppliedToUnsignedInteger, tokenPosition); } - /// - /// Parse the text into a Real ConstantExpression. - /// - public Expression ParseRealLiteral(string text, char qualifier, bool stripQualifier) + if (value <= int.MaxValue) { - if (stripQualifier) - { - var pos = text.Length - 1; - while (pos >= 0 && Qualifiers.Contains(text[pos])) - { - pos--; - } + return _constantExpressionHelper.CreateLiteral((int)value, textOriginal); + } - if (pos < text.Length - 1) - { - qualifier = text[pos + 1]; - text = text.Substring(0, pos + 1); - } - } + return _constantExpressionHelper.CreateLiteral(value, textOriginal); + } - switch (qualifier) + /// + /// Parse the text into a Real ConstantExpression. + /// + public Expression ParseRealLiteral(string text, string textOriginal, char qualifier, bool stripQualifier) + { + if (stripQualifier) + { + var pos = text.Length - 1; + while (pos >= 0 && Qualifiers.Contains(text[pos])) { - case 'f': - case 'F': - return _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(float))!, text); - - case 'm': - case 'M': - return _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(decimal))!, text); - - case 'd': - case 'D': - return _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(double))!, text); + pos--; + } - default: - return _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(double))!, text); + if (pos < text.Length - 1) + { + qualifier = text[pos + 1]; + text = text.Substring(0, pos + 1); } } - /// - /// Tries to parse the number (text) into the specified type. - /// - /// The text. - /// The type. - /// The result. - public bool TryParseNumber(string text, Type type, out object? result) + return qualifier switch { - result = ParseNumber(text, type); - return result != null; - } + 'f' or 'F' => _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(float))!, textOriginal), + 'm' or 'M' => _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(decimal))!, textOriginal), + _ => _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(double))!, textOriginal) + }; + } - /// - /// Parses the number (text) into the specified type. - /// - /// The text. - /// The type. - public object? ParseNumber(string text, Type type) + /// + /// Tries to parse the number (text) into the specified type. + /// + /// The text. + /// The type. + /// The result. + public bool TryParseNumber(string text, Type type, out object? result) + { + result = ParseNumber(text, type); + return result != null; + } + + /// + /// Parses the number (text) into the specified type. + /// + /// The text. + /// The type. + public object? ParseNumber(string text, Type type) + { + try { - try - { #if !(UAP10_0 || NETSTANDARD) - switch (Type.GetTypeCode(TypeHelper.GetNonNullableType(type))) - { - case TypeCode.SByte: - return sbyte.Parse(text, _culture); - case TypeCode.Byte: - return byte.Parse(text, _culture); - case TypeCode.Int16: - return short.Parse(text, _culture); - case TypeCode.UInt16: - return ushort.Parse(text, _culture); - case TypeCode.Int32: - return int.Parse(text, _culture); - case TypeCode.UInt32: - return uint.Parse(text, _culture); - case TypeCode.Int64: - return long.Parse(text, _culture); - case TypeCode.UInt64: - return ulong.Parse(text, _culture); - case TypeCode.Single: - return float.Parse(text, _culture); - case TypeCode.Double: - return double.Parse(text, _culture); - case TypeCode.Decimal: - return decimal.Parse(text, _culture); - } + switch (Type.GetTypeCode(TypeHelper.GetNonNullableType(type))) + { + case TypeCode.SByte: + return sbyte.Parse(text, _culture); + case TypeCode.Byte: + return byte.Parse(text, _culture); + case TypeCode.Int16: + return short.Parse(text, _culture); + case TypeCode.UInt16: + return ushort.Parse(text, _culture); + case TypeCode.Int32: + return int.Parse(text, _culture); + case TypeCode.UInt32: + return uint.Parse(text, _culture); + case TypeCode.Int64: + return long.Parse(text, _culture); + case TypeCode.UInt64: + return ulong.Parse(text, _culture); + case TypeCode.Single: + return float.Parse(text, _culture); + case TypeCode.Double: + return double.Parse(text, _culture); + case TypeCode.Decimal: + return decimal.Parse(text, _culture); + } #else var tp = TypeHelper.GetNonNullableType(type); if (tp == typeof(sbyte)) @@ -289,28 +279,27 @@ public bool TryParseNumber(string text, Type type, out object? result) return decimal.Parse(text, _culture); } #endif - } - catch - { - return null; - } - + } + catch + { return null; } - private Expression ParseAsBinary(int tokenPosition, string text, bool isNegative) - { - if (RegexBinary32.IsMatch(text)) - { - return _constantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt32(text, 2), text); - } + return null; + } - if (RegexBinary64.IsMatch(text)) - { - return _constantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt64(text, 2), text); - } + private Expression ParseAsBinary(int tokenPosition, string text, string textOriginal, bool isNegative) + { + if (RegexBinary32.IsMatch(text)) + { + return _constantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt32(text, 2), textOriginal); + } - throw new ParseException(string.Format(_culture, Res.InvalidBinaryIntegerLiteral, text), tokenPosition); + if (RegexBinary64.IsMatch(text)) + { + return _constantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt64(text, 2), textOriginal); } + + throw new ParseException(string.Format(_culture, Res.InvalidBinaryIntegerLiteral, text), tokenPosition); } -} +} \ 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 4d5d19b5..70d5ce68 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -924,6 +924,7 @@ public void DynamicExpressionParser_ParseLambda_Float(string? culture, string ex [null, "1.2345E4", 12345d] ]; } + [Theory] [MemberData(nameof(Doubles))] public void DynamicExpressionParser_ParseLambda_Double(string? culture, string expression, double expected) @@ -945,6 +946,42 @@ public void DynamicExpressionParser_ParseLambda_Double(string? culture, string e result.Should().Be(expected); } + [Theory] + [InlineData("0x0", 0)] + [InlineData("0xa", 10)] + [InlineData("0xA", 10)] + [InlineData("0x10", 16)] + public void DynamicExpressionParser_ParseLambda_HexToLong(string expression, long expected) + { + // Arrange + var parameters = Array.Empty(); + + // Act + var lambda = DynamicExpressionParser.ParseLambda( parameters, typeof(long), expression); + var result = lambda.Compile().DynamicInvoke(); + + // Assert + result.Should().Be(expected); + } + + [Theory] + [InlineData("0b0", 0)] + [InlineData("0B0", 0)] + [InlineData("0b1000", 8)] + [InlineData("0b1001", 9)] + public void DynamicExpressionParser_ParseLambda_BinaryToLong(string expression, long expected) + { + // Arrange + var parameters = Array.Empty(); + + // Act + var lambda = DynamicExpressionParser.ParseLambda(parameters, typeof(long), expression); + var result = lambda.Compile().DynamicInvoke(); + + // Assert + result.Should().Be(expected); + } + public class EntityDbo { public string Name { get; set; } = string.Empty; @@ -1711,9 +1748,9 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexEx } [Theory] - [InlineData(true, "c => c.Age == 8", "c => (c.Age == 8)")] + [InlineData(true, "c => c.Age == 8", "c => (c.Age == Convert(8, Nullable`1))")] [InlineData(true, "c => c.Name == \"test\"", "c => (c.Name == \"test\")")] - [InlineData(false, "c => c.Age == 8", "Param_0 => (Param_0.Age == 8)")] + [InlineData(false, "c => c.Age == 8", "Param_0 => (Param_0.Age == Convert(8, Nullable`1))")] [InlineData(false, "c => c.Name == \"test\"", "Param_0 => (Param_0.Name == \"test\")")] public void DynamicExpressionParser_ParseLambda_RenameParameterExpression(bool renameParameterExpression, string expressionAsString, string expected) { @@ -1732,7 +1769,7 @@ public void DynamicExpressionParser_ParseLambda_RenameParameterExpression(bool r } [Theory] - [InlineData("c => c.Age == 8", "([a-z]{16}) =\\> \\(\\1\\.Age == 8\\)")] + [InlineData("c => c.Age == 8", "([a-z]{16}) =\\> \\(\\1\\.Age == .+")] [InlineData("c => c.Name == \"test\"", "([a-z]{16}) =\\> \\(\\1\\.Name == \"test\"\\)")] public void DynamicExpressionParser_ParseLambda_RenameEmptyParameterExpressionNames(string expressionAsString, string expected) { diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/NumberParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/NumberParserTests.cs index e291c34e..f8ccc496 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/NumberParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/NumberParserTests.cs @@ -159,7 +159,7 @@ public void NumberParser_ParseIntegerLiteral(string text, double expected) public void NumberParser_ParseDecimalLiteral(string text, char qualifier, decimal expected) { // Act - var result = new NumberParser(_parsingConfig).ParseRealLiteral(text, qualifier, true) as ConstantExpression; + var result = new NumberParser(_parsingConfig).ParseRealLiteral(text, text, qualifier, true) as ConstantExpression; // Assert result?.Value.Should().Be(expected); @@ -175,7 +175,7 @@ public void NumberParser_ParseDoubleLiteral(string text, char qualifier, double // Arrange // Act - var result = new NumberParser(_parsingConfig).ParseRealLiteral(text, qualifier, true) as ConstantExpression; + var result = new NumberParser(_parsingConfig).ParseRealLiteral(text, text, qualifier, true) as ConstantExpression; // Assert result?.Value.Should().Be(expected); @@ -191,7 +191,7 @@ public void NumberParser_ParseFloatLiteral(string text, char qualifier, float ex // Arrange // Act - var result = new NumberParser(_parsingConfig).ParseRealLiteral(text, qualifier, true) as ConstantExpression; + var result = new NumberParser(_parsingConfig).ParseRealLiteral(text, text, qualifier, true) as ConstantExpression; // Assert result?.Value.Should().Be(expected); From 49a69cc7c0e45d739fc2240e8bf8b4bb6f6f685d Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 15 Nov 2025 10:59:32 +0100 Subject: [PATCH 04/21] Support normalization of objects for Z.DynamicLinq.Json (#958) * Json : Normalize * . * tst * ns * sc * any * Update src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NormalizationNonExistingPropertyBehavior.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/System.Linq.Dynamic.Core.SystemTextJson/Config/SystemTextJsonParsingConfig.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NewtonsoftJsonParsingConfig.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * . --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Config/NewtonsoftJsonParsingConfig.cs | 17 +- ...ormalizationNonExistingPropertyBehavior.cs | 17 ++ .../JsonValueInfo.cs | 10 ++ .../NewtonsoftJsonExtensions.cs | 9 +- .../Utils/NormalizeUtils.cs | 131 +++++++++++++++ ...ormalizationNonExistingPropertyBehavior.cs | 17 ++ .../Config/SystemTextJsonParsingConfig.cs | 20 ++- .../Extensions/JsonValueExtensions.cs | 21 +++ .../JsonValueInfo.cs | 10 ++ .../SystemTextJsonExtensions.cs | 9 +- .../Utils/NormalizeUtils.cs | 156 ++++++++++++++++++ .../NewtonsoftJsonTests.cs | 66 ++++---- .../SystemTextJsonTests.cs | 37 ++++- 13 files changed, 482 insertions(+), 38 deletions(-) create mode 100644 src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NormalizationNonExistingPropertyBehavior.cs create mode 100644 src/System.Linq.Dynamic.Core.NewtonsoftJson/JsonValueInfo.cs create mode 100644 src/System.Linq.Dynamic.Core.NewtonsoftJson/Utils/NormalizeUtils.cs create mode 100644 src/System.Linq.Dynamic.Core.SystemTextJson/Config/NormalizationNonExistingPropertyBehavior.cs create mode 100644 src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonValueExtensions.cs create mode 100644 src/System.Linq.Dynamic.Core.SystemTextJson/JsonValueInfo.cs create mode 100644 src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NewtonsoftJsonParsingConfig.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NewtonsoftJsonParsingConfig.cs index fad7f9f1..fbccbf64 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NewtonsoftJsonParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NewtonsoftJsonParsingConfig.cs @@ -15,5 +15,20 @@ public class NewtonsoftJsonParsingConfig : ParsingConfig /// /// The default to use. /// - public DynamicJsonClassOptions? DynamicJsonClassOptions { get; set; } + public DynamicJsonClassOptions? DynamicJsonClassOptions { get; set; } + + /// + /// Gets or sets a value indicating whether the objects in an array should be normalized before processing. + /// + public bool Normalize { get; set; } = true; + + /// + /// Gets or sets the behavior to apply when a property value does not exist during normalization. + /// + /// + /// Use this property to control how the normalization process handles properties that are missing or undefined. + /// The selected behavior may affect the output or error handling of normalization operations. + /// The default value is . + /// + public NormalizationNonExistingPropertyBehavior NormalizationNonExistingPropertyValueBehavior { get; set; } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NormalizationNonExistingPropertyBehavior.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NormalizationNonExistingPropertyBehavior.cs new file mode 100644 index 00000000..bb68277b --- /dev/null +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NormalizationNonExistingPropertyBehavior.cs @@ -0,0 +1,17 @@ +namespace System.Linq.Dynamic.Core.NewtonsoftJson.Config; + +/// +/// Specifies the behavior to use when setting a property value that does not exist or is missing during normalization. +/// +public enum NormalizationNonExistingPropertyBehavior +{ + /// + /// Specifies that the default value should be used. + /// + UseDefaultValue = 0, + + /// + /// Specifies that null values should be used. + /// + UseNull = 1 +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/JsonValueInfo.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/JsonValueInfo.cs new file mode 100644 index 00000000..963ff37d --- /dev/null +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/JsonValueInfo.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json.Linq; + +namespace System.Linq.Dynamic.Core.NewtonsoftJson; + +internal readonly struct JsonValueInfo(JTokenType type, object? value) +{ + public JTokenType Type { get; } = type; + + public object? Value { get; } = value; +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs index 8aefa397..943df3dd 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Linq.Dynamic.Core.NewtonsoftJson.Config; using System.Linq.Dynamic.Core.NewtonsoftJson.Extensions; +using System.Linq.Dynamic.Core.NewtonsoftJson.Utils; using System.Linq.Dynamic.Core.Validation; using JetBrains.Annotations; using Newtonsoft.Json.Linq; @@ -870,7 +871,13 @@ private static JArray ToJArray(Func func) private static IQueryable ToQueryable(JArray source, NewtonsoftJsonParsingConfig? config = null) { - return source.ToDynamicJsonClassArray(config?.DynamicJsonClassOptions).AsQueryable(); + var normalized = config?.Normalize == true ? + NormalizeUtils.NormalizeArray(source, config.NormalizationNonExistingPropertyValueBehavior): + source; + + return normalized + .ToDynamicJsonClassArray(config?.DynamicJsonClassOptions) + .AsQueryable(); } #endregion } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Utils/NormalizeUtils.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Utils/NormalizeUtils.cs new file mode 100644 index 00000000..5669915c --- /dev/null +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Utils/NormalizeUtils.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using System.Linq.Dynamic.Core.NewtonsoftJson.Config; +using Newtonsoft.Json.Linq; + +namespace System.Linq.Dynamic.Core.NewtonsoftJson.Utils; + +internal static class NormalizeUtils +{ + /// + /// Normalizes an array of JSON objects so that each object contains all properties found in the array, + /// including nested objects. Missing properties will have null values. + /// + internal static JArray NormalizeArray(JArray jsonArray, NormalizationNonExistingPropertyBehavior normalizationBehavior) + { + if (jsonArray.Any(item => item is not JObject)) + { + return jsonArray; + } + + var schema = BuildSchema(jsonArray); + var normalizedArray = new JArray(); + + foreach (var jo in jsonArray.OfType()) + { + var normalizedObj = NormalizeObject(jo, schema, normalizationBehavior); + normalizedArray.Add(normalizedObj); + } + + return normalizedArray; + } + + private static Dictionary BuildSchema(JArray array) + { + var schema = new Dictionary(); + + foreach (var item in array) + { + if (item is JObject obj) + { + MergeSchema(schema, obj); + } + } + + return schema; + } + + private static void MergeSchema(Dictionary schema, JObject obj) + { + foreach (var prop in obj.Properties()) + { + if (prop.Value is JObject nested) + { + if (!schema.TryGetValue(prop.Name, out var jsonValueInfo)) + { + jsonValueInfo = new JsonValueInfo(JTokenType.Object, new Dictionary()); + schema[prop.Name] = jsonValueInfo; + } + + MergeSchema((Dictionary)jsonValueInfo.Value!, nested); + } + else + { + if (!schema.ContainsKey(prop.Name)) + { + schema[prop.Name] = new JsonValueInfo(prop.Value.Type, null); + } + } + } + } + + private static JObject NormalizeObject(JObject source, Dictionary schema, NormalizationNonExistingPropertyBehavior normalizationBehavior) + { + var result = new JObject(); + + foreach (var key in schema.Keys) + { + if (schema[key].Value is Dictionary nestedSchema) + { + result[key] = source.ContainsKey(key) && source[key] is JObject jo ? NormalizeObject(jo, nestedSchema, normalizationBehavior) : CreateEmptyObject(nestedSchema, normalizationBehavior); + } + else + { + if (source.ContainsKey(key)) + { + result[key] = source[key]; + } + else + { + result[key] = normalizationBehavior == NormalizationNonExistingPropertyBehavior.UseDefaultValue ? GetDefaultValue(schema[key]) : JValue.CreateNull(); + } + } + } + + return result; + } + + private static JObject CreateEmptyObject(Dictionary schema, NormalizationNonExistingPropertyBehavior normalizationBehavior) + { + var obj = new JObject(); + foreach (var key in schema.Keys) + { + if (schema[key].Value is Dictionary nestedSchema) + { + obj[key] = CreateEmptyObject(nestedSchema, normalizationBehavior); + } + else + { + obj[key] = normalizationBehavior == NormalizationNonExistingPropertyBehavior.UseDefaultValue ? GetDefaultValue(schema[key]) : JValue.CreateNull(); + } + } + + return obj; + } + + private static JToken GetDefaultValue(JsonValueInfo jType) + { + return jType.Type switch + { + JTokenType.Array => new JArray(), + JTokenType.Boolean => default(bool), + JTokenType.Bytes => new byte[0], + JTokenType.Date => DateTime.MinValue, + JTokenType.Float => default(float), + JTokenType.Guid => Guid.Empty, + JTokenType.Integer => default(int), + JTokenType.String => string.Empty, + JTokenType.TimeSpan => TimeSpan.MinValue, + _ => JValue.CreateNull(), + }; + } +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/Config/NormalizationNonExistingPropertyBehavior.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/Config/NormalizationNonExistingPropertyBehavior.cs new file mode 100644 index 00000000..daafaa4b --- /dev/null +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/Config/NormalizationNonExistingPropertyBehavior.cs @@ -0,0 +1,17 @@ +namespace System.Linq.Dynamic.Core.SystemTextJson.Config; + +/// +/// Specifies the behavior to use when setting a property vlue that does not exist or is missing during normalization. +/// +public enum NormalizationNonExistingPropertyBehavior +{ + /// + /// Specifies that the default value should be used. + /// + UseDefaultValue = 0, + + /// + /// Specifies that null values should be used. + /// + UseNull = 1 +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/Config/SystemTextJsonParsingConfig.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/Config/SystemTextJsonParsingConfig.cs index eb42ff6d..a4c1e76a 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/Config/SystemTextJsonParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/Config/SystemTextJsonParsingConfig.cs @@ -8,5 +8,23 @@ public class SystemTextJsonParsingConfig : ParsingConfig /// /// The default ParsingConfig for . /// - public new static SystemTextJsonParsingConfig Default { get; } = new(); + public new static SystemTextJsonParsingConfig Default { get; } = new SystemTextJsonParsingConfig + { + ConvertObjectToSupportComparison = true + }; + + /// + /// Gets or sets a value indicating whether the objects in an array should be normalized before processing. + /// + public bool Normalize { get; set; } = true; + + /// + /// Gets or sets the behavior to apply when a property value does not exist during normalization. + /// + /// + /// Use this property to control how the normalization process handles properties that are missing or undefined. + /// The selected behavior may affect the output or error handling of normalization operations. + /// The default value is . + /// + public NormalizationNonExistingPropertyBehavior NormalizationNonExistingPropertyValueBehavior { get; set; } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonValueExtensions.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonValueExtensions.cs new file mode 100644 index 00000000..907f7bf3 --- /dev/null +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonValueExtensions.cs @@ -0,0 +1,21 @@ +#if !NET8_0_OR_GREATER +namespace System.Text.Json.Nodes; + +internal static class JsonValueExtensions +{ + internal static JsonValueKind? GetValueKind(this JsonNode node) + { + if (node is JsonObject) + { + return JsonValueKind.Object; + } + + if (node is JsonArray) + { + return JsonValueKind.Array; + } + + return node.GetValue() is JsonElement je ? je.ValueKind : null; + } +} +#endif \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/JsonValueInfo.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/JsonValueInfo.cs new file mode 100644 index 00000000..da4f2b5d --- /dev/null +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/JsonValueInfo.cs @@ -0,0 +1,10 @@ +using System.Text.Json; + +namespace System.Linq.Dynamic.Core.SystemTextJson; + +internal readonly struct JsonValueInfo(JsonValueKind type, object? value) +{ + public JsonValueKind Type { get; } = type; + + public object? Value { get; } = value; +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs index 8d1671a0..6d8a5aa0 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs @@ -1093,13 +1093,20 @@ private static JsonDocument ToJsonDocumentArray(Func func) // ReSharper disable once UnusedParameter.Local private static IQueryable ToQueryable(JsonDocument source, SystemTextJsonParsingConfig? config = null) { + config = config ?? SystemTextJsonParsingConfig.Default; + config.ConvertObjectToSupportComparison = true; + var array = source.RootElement; if (array.ValueKind != JsonValueKind.Array) { throw new NotSupportedException("The source is not a JSON array."); } - return JsonDocumentExtensions.ToDynamicJsonClassArray(array).AsQueryable(); + var normalized = config.Normalize ? + NormalizeUtils.NormalizeJsonDocument(source, config.NormalizationNonExistingPropertyValueBehavior) : + source; + + return JsonDocumentExtensions.ToDynamicJsonClassArray(normalized.RootElement).AsQueryable(); } #endregion } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs new file mode 100644 index 00000000..fa5836d2 --- /dev/null +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs @@ -0,0 +1,156 @@ +using System.Collections.Generic; +using System.Linq.Dynamic.Core.SystemTextJson.Config; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace System.Linq.Dynamic.Core.SystemTextJson.Utils; + +internal static class NormalizeUtils +{ + /// + /// Normalizes a document so that each object contains all properties found in the array, including nested objects. + /// + internal static JsonDocument NormalizeJsonDocument(JsonDocument jsonDocument, NormalizationNonExistingPropertyBehavior normalizationBehavior) + { + if (jsonDocument.RootElement.ValueKind != JsonValueKind.Array) + { + throw new NotSupportedException("The source is not a JSON array."); + } + + var jsonArray = JsonNode.Parse(jsonDocument.RootElement.GetRawText())!.AsArray(); + var normalizedArray = NormalizeJsonArray(jsonArray, normalizationBehavior); + + return JsonDocument.Parse(normalizedArray.ToJsonString()); + } + + /// + /// Normalizes an array of JSON objects so that each object contains all properties found in the array, including nested objects. + /// + internal static JsonArray NormalizeJsonArray(JsonArray jsonArray, NormalizationNonExistingPropertyBehavior normalizationBehavior) + { + if (jsonArray.Any(item => item != null && item.GetValueKind() != JsonValueKind.Object)) + { + return jsonArray; + } + + var schema = BuildSchema(jsonArray); + var normalizedArray = new JsonArray(); + + foreach (var item in jsonArray) + { + if (item is JsonObject obj) + { + var normalizedObj = NormalizeObject(obj, schema, normalizationBehavior); + normalizedArray.Add(normalizedObj); + } + } + + return normalizedArray; + } + + private static Dictionary BuildSchema(JsonArray array) + { + var schema = new Dictionary(); + + foreach (var item in array) + { + if (item is JsonObject obj) + { + MergeSchema(schema, obj); + } + } + + return schema; + } + + private static void MergeSchema(Dictionary schema, JsonObject obj) + { + foreach (var prop in obj) + { + if (prop.Value is JsonObject nested) + { + if (!schema.TryGetValue(prop.Key, out var jsonValueInfo)) + { + jsonValueInfo = new JsonValueInfo(JsonValueKind.Object, new Dictionary()); + schema[prop.Key] = jsonValueInfo; + } + + MergeSchema((Dictionary)jsonValueInfo.Value!, nested); + } + else + { + if (!schema.ContainsKey(prop.Key)) + { + schema[prop.Key] = new JsonValueInfo(prop.Value?.GetValueKind() ?? JsonValueKind.Null, null); + } + } + } + } + + private static JsonObject NormalizeObject(JsonObject source, Dictionary schema, NormalizationNonExistingPropertyBehavior normalizationBehavior) + { + var result = new JsonObject(); + + foreach (var kvp in schema) + { + var key = kvp.Key; + var jType = kvp.Value; + + if (jType.Value is Dictionary nestedSchema) + { + result[key] = source.ContainsKey(key) && source[key] is JsonObject jo ? NormalizeObject(jo, nestedSchema, normalizationBehavior) : CreateEmptyObject(nestedSchema, normalizationBehavior); + } + else + { + if (source.ContainsKey(key)) + { +#if NET8_0_OR_GREATER + result[key] = source[key]!.DeepClone(); +#else + result[key] = JsonNode.Parse(source[key]!.ToJsonString()); +#endif + } + else + { + result[key] = normalizationBehavior == NormalizationNonExistingPropertyBehavior.UseDefaultValue ? GetDefaultValue(jType) : null; + } + } + } + + return result; + } + + private static JsonObject CreateEmptyObject(Dictionary schema, NormalizationNonExistingPropertyBehavior normalizationBehavior) + { + var obj = new JsonObject(); + foreach (var kvp in schema) + { + var key = kvp.Key; + var jType = kvp.Value; + + if (jType.Value is Dictionary nestedSchema) + { + obj[key] = CreateEmptyObject(nestedSchema, normalizationBehavior); + } + else + { + obj[key] = normalizationBehavior == NormalizationNonExistingPropertyBehavior.UseDefaultValue ? GetDefaultValue(jType) : null; + } + } + + return obj; + } + + private static JsonNode? GetDefaultValue(JsonValueInfo jType) + { + return jType.Type switch + { + JsonValueKind.Array => new JsonArray(), + JsonValueKind.False => false, + JsonValueKind.Number => default(int), + JsonValueKind.String => string.Empty, + JsonValueKind.True => false, + _ => null, + }; + } +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs index 40185d45..e391ed29 100644 --- a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs @@ -1,4 +1,5 @@ -using System.Linq.Dynamic.Core.NewtonsoftJson.Config; +using System.Linq.Dynamic.Core.Exceptions; +using System.Linq.Dynamic.Core.NewtonsoftJson.Config; using FluentAssertions; using Newtonsoft.Json.Linq; using Xunit; @@ -508,36 +509,6 @@ public void Where_With_Select() first.Value().Should().Be("Doe"); } - //[Fact] - //public void Where_OptionalProperty() - //{ - // // Arrange - // var config = new NewtonsoftJsonParsingConfig - // { - // ConvertObjectToSupportComparison = true - // }; - // var array = - // """ - // [ - // { - // "Name": "John", - // "Age": 30 - // }, - // { - // "Name": "Doe" - // } - // ] - // """; - - // // Act - // var result = JArray.Parse(array).Where(config, "Age > 30").Select("Name"); - - // // Assert - // result.Should().HaveCount(1); - // var first = result.First(); - // first.Value().Should().Be("John"); - //} - [Theory] [InlineData("notExisting == true")] [InlineData("notExisting == \"true\"")] @@ -565,4 +536,37 @@ public void Where_NonExistingMember_EmptyResult(string predicate) // Assert result.Should().BeEmpty(); } + + [Theory] + [InlineData("""[ { "Name": "John", "Age": 30 }, { "Name": "Doe" }, { } ]""")] + [InlineData("""[ { "Name": "Doe" }, { "Name": "John", "Age": 30 }, { } ]""")] + public void NormalizeArray(string array) + { + // Act + var result = JArray.Parse(array) + .Where("Age >= 30") + .Select("Name"); + + // Assert + result.Should().HaveCount(1); + var first = result.First(); + first.Value().Should().Be("John"); + } + + [Fact] + public void NormalizeArray_When_NormalizeIsFalse_ShouldThrow() + { + // Arrange + var config = new NewtonsoftJsonParsingConfig + { + Normalize = false + }; + var array = """[ { "Name": "Doe" }, { "Name": "John", "Age": 30 }, { } ]"""; + + // Act + Action act = () => JArray.Parse(array).Where(config, "Age >= 30"); + + // Assert + act.Should().Throw().WithMessage("The binary operator GreaterThanOrEqual is not defined for the types 'System.Object' and 'System.Int32'."); + } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs index 1e817664..7b27ec5c 100644 --- a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs @@ -551,17 +551,48 @@ public void Where_With_Select() [InlineData("\"something\" == notExisting")] [InlineData("1 < notExisting")] public void Where_NonExistingMember_EmptyResult(string predicate) + { + // Act + var result = _source.Where(predicate).RootElement.EnumerateArray(); + + // Assert + result.Should().BeEmpty(); + } + + [Theory] + [InlineData("""[ { "Name": "John", "Age": 30 }, { "Name": "Doe" }, { } ]""")] + [InlineData("""[ { "Name": "Doe" }, { "Name": "John", "Age": 30 }, { } ]""")] + public void NormalizeArray(string data) + { + // Arrange + var source = JsonDocument.Parse(data); + + // Act + var result = source + .Where("Age >= 30") + .Select("Name"); + + // Assert + var array = result.RootElement.EnumerateArray(); + array.Should().HaveCount(1); + var first = result.First(); + array.First().GetString().Should().Be("John"); + } + + [Fact] + public void NormalizeArray_When_NormalizeIsFalse_ShouldThrow() { // Arrange var config = new SystemTextJsonParsingConfig { - ConvertObjectToSupportComparison = true + Normalize = false }; + var data = """[ { "Name": "Doe" }, { "Name": "John", "Age": 30 }, { } ]"""; // Act - var result = _source.Where(config, predicate).RootElement.EnumerateArray(); + Action act = () => JsonDocument.Parse(data).Where(config, "Age >= 30"); // Assert - result.Should().BeEmpty(); + act.Should().Throw().WithMessage("Unable to find property 'Age' on type '<>f__AnonymousType*"); } } \ No newline at end of file From e7cb1a167fe9bd4fb1a59bbe6f6d300883e9d1bd Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 15 Nov 2025 12:44:00 +0100 Subject: [PATCH 05/21] .NET 10 (#957) * Add examples for .NET 10 * net10 * Update src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10.csproj Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * . * Run Tests .NET 10 (with Coverage) * ... * , * [Fact(Skip = "Fails sometimes in GitHub CI build")] --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 39 +-- System.Linq.Dynamic.Core.sln | 61 +++- ...soleApp_netcore2.0_EF2.0.2_InMemory.csproj | 2 +- .../ConsoleApp_netcore2.0_EF2.0.1.csproj | 2 +- .../ConsoleApp_netcore2.1_EF2.1.1.csproj | 2 +- .../ConsoleApp_netcore2.0_EF2.1.csproj | 2 +- .../ConsoleApp_netcore3.1_EF3.1.csproj | 2 +- .../ConsoleApp_net5.0_EF5.csproj | 2 +- .../ConsoleApp_net5.0_EF5_InMemory.csproj | 2 +- .../ConsoleApp_net6.0_EF6_InMemory.csproj | 2 +- .../ConsoleApp_net6.0_EF6_Sqlite.csproj | 2 +- .../ConsoleApp_net10/ConsoleApp_net10.csproj | 22 ++ .../DataColumnOrdinalIgnoreCaseComparer.cs | 32 ++ src-console/ConsoleApp_net10/Program.cs | 299 ++++++++++++++++++ ...yFrameworkCore.DynamicLinq.EFCore10.csproj | 47 +++ .../Properties/AssemblyInfo.cs | 4 + ...em.Linq.Dynamic.Core.NewtonsoftJson.csproj | 2 +- ...em.Linq.Dynamic.Core.SystemTextJson.csproj | 2 +- .../System.Linq.Dynamic.Core.csproj | 4 +- .../EntityFramework.DynamicLinq.Tests.csproj | 17 +- ...q.Dynamic.Core.NewtonsoftJson.Tests.csproj | 2 +- .../DynamicExpressionParserTests.cs | 2 +- .../Extensions/JsonDocumentExtensionsTests.cs | 2 +- ...q.Dynamic.Core.SystemTextJson.Tests.csproj | 4 +- .../SystemTextJsonTests.cs | 2 +- ...System.Linq.Dynamic.Core.Tests.Net5.csproj | 2 +- ...System.Linq.Dynamic.Core.Tests.Net6.csproj | 2 +- ...System.Linq.Dynamic.Core.Tests.Net7.csproj | 2 +- ...System.Linq.Dynamic.Core.Tests.Net8.csproj | 4 +- ...System.Linq.Dynamic.Core.Tests.Net9.csproj | 48 +++ ...inq.Dynamic.Core.Tests.NetCoreApp31.csproj | 2 +- .../DynamicClassTest.cs | 12 + .../DynamicExpressionParserTests.cs | 6 +- .../QueryableTests.Select.cs | 2 +- .../QueryableTests.Where.cs | 5 +- .../System.Linq.Dynamic.Core.Tests.csproj | 4 +- 36 files changed, 580 insertions(+), 68 deletions(-) create mode 100644 src-console/ConsoleApp_net10/ConsoleApp_net10.csproj create mode 100644 src-console/ConsoleApp_net10/DataColumnOrdinalIgnoreCaseComparer.cs create mode 100644 src-console/ConsoleApp_net10/Program.cs create mode 100644 src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10.csproj create mode 100644 src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Properties/AssemblyInfo.cs create mode 100644 test/System.Linq.Dynamic.Core.Tests.Net9/System.Linq.Dynamic.Core.Tests.Net9.csproj diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f998851e..79f0fba2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,13 +22,13 @@ jobs: - uses: actions/setup-dotnet@v4 with: - dotnet-version: '9.0.x' + dotnet-version: '10.0.x' - name: Build run: | dotnet build ./src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj -c Release -p:buildType=azure-pipelines-ci - - name: Run Tests EFCore net9.0 + - name: Run Tests EFCore net10.0 run: | dotnet test ./test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj -c Release -p:buildType=azure-pipelines-ci @@ -76,39 +76,24 @@ jobs: - name: Build run: | dotnet build ./src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj -c Debug -p:buildType=azure-pipelines-ci - - - name: Run Tests EF net8.0 (with Coverage) + + - name: Run Tests EFCore .NET 10 (with Coverage) run: | - dotnet-coverage collect 'dotnet test ./test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj --configuration Debug --framework net8.0 -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-ef.xml + dotnet-coverage collect 'dotnet test ./test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj --configuration Debug -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-efcore.xml - - name: Run Tests EFCore net8.0 (with Coverage) + - name: Run Tests EF .NET 10 (with Coverage) run: | - dotnet-coverage collect 'dotnet test ./test/System.Linq.Dynamic.Core.Tests.Net8/System.Linq.Dynamic.Core.Tests.Net8.csproj --configuration Debug --framework net8.0 -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-efcore.xml + dotnet-coverage collect 'dotnet test ./test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj --configuration Debug --framework net10.0 -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-ef.xml - - name: Run Tests Newtonsoft.Json .NET 8 (with Coverage) + - name: Run Tests Newtonsoft.Json .NET 10 (with Coverage) run: | - dotnet-coverage collect 'dotnet test ./test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/System.Linq.Dynamic.Core.NewtonsoftJson.Tests.csproj --configuration Debug --framework net8.0 -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-newtonsoftjson.xml + dotnet-coverage collect 'dotnet test ./test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/System.Linq.Dynamic.Core.NewtonsoftJson.Tests.csproj --configuration Debug --framework net10.0 -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-newtonsoftjson.xml - - name: Run Tests System.Text.Json .NET 8 (with Coverage) + - name: Run Tests System.Text.Json .NET 10 (with Coverage) run: | - dotnet-coverage collect 'dotnet test ./test/System.Linq.Dynamic.Core.SystemTextJson.Tests/System.Linq.Dynamic.Core.SystemTextJson.Tests.csproj --configuration Debug --framework net8.0 -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-systemtextjson.xml + dotnet-coverage collect 'dotnet test ./test/System.Linq.Dynamic.Core.SystemTextJson.Tests/System.Linq.Dynamic.Core.SystemTextJson.Tests.csproj --configuration Debug --framework net10.0 -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-systemtextjson.xml - name: End analysis on SonarCloud if: ${{ steps.secret-check.outputs.run_analysis == 'true' }} run: | - dotnet sonarscanner end /d:sonar.token=${{ secrets.SONAR_TOKEN }} - - # - name: Run Tests EFCore net8.0 - # run: | - # dotnet test ./test/System.Linq.Dynamic.Core.Tests.Net7/System.Linq.Dynamic.Core.Tests.Net8.csproj -c Release -p:buildType=azure-pipelines-ci - # continue-on-error: true - - # - name: Run Tests EFCore net7.0 - # run: | - # dotnet test ./test/System.Linq.Dynamic.Core.Tests.Net7/System.Linq.Dynamic.Core.Tests.Net7.csproj -c Release -p:buildType=azure-pipelines-ci - # continue-on-error: true - - # - name: Run Tests EFCore net6.0 - # run: | - # dotnet test ./test/System.Linq.Dynamic.Core.Tests.Net6/System.Linq.Dynamic.Core.Tests.Net6.csproj -c Release -p:buildType=azure-pipelines-ci - # continue-on-error: true \ No newline at end of file + dotnet sonarscanner end /d:sonar.token=${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/System.Linq.Dynamic.Core.sln b/System.Linq.Dynamic.Core.sln index 4bfd1ffa..7aaa3982 100644 --- a/System.Linq.Dynamic.Core.sln +++ b/System.Linq.Dynamic.Core.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31606.5 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11205.157 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8463ED7E-69FB-49AE-85CF-0791AFD98E38}" ProjectSection(SolutionItems) = preProject @@ -161,6 +161,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Linq.Dynamic.Core.Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppPerformanceTest", "src-console\ConsoleAppPerformanceTest\ConsoleAppPerformanceTest.csproj", "{067C00CF-29FA-4643-814D-3A3C3C84634F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp_net10", "src-console\ConsoleApp_net10\ConsoleApp_net10.csproj", "{34C58129-07DB-287E-A29B-FA8EE8BAEB05}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10", "src\Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10\Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10.csproj", "{1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Linq.Dynamic.Core.Tests.Net9", "test\System.Linq.Dynamic.Core.Tests.Net9\System.Linq.Dynamic.Core.Tests.Net9.csproj", "{00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1057,6 +1063,54 @@ Global {067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|x64.Build.0 = Release|Any CPU {067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|x86.ActiveCfg = Release|Any CPU {067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|x86.Build.0 = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|ARM.ActiveCfg = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|ARM.Build.0 = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|x64.ActiveCfg = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|x64.Build.0 = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|x86.ActiveCfg = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|x86.Build.0 = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|Any CPU.Build.0 = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|ARM.ActiveCfg = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|ARM.Build.0 = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|x64.ActiveCfg = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|x64.Build.0 = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|x86.ActiveCfg = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|x86.Build.0 = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|ARM.ActiveCfg = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|ARM.Build.0 = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|x64.ActiveCfg = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|x64.Build.0 = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|x86.ActiveCfg = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|x86.Build.0 = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|Any CPU.Build.0 = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|ARM.ActiveCfg = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|ARM.Build.0 = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|x64.ActiveCfg = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|x64.Build.0 = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|x86.ActiveCfg = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|x86.Build.0 = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|ARM.ActiveCfg = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|ARM.Build.0 = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|x64.ActiveCfg = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|x64.Build.0 = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|x86.ActiveCfg = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|x86.Build.0 = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|Any CPU.Build.0 = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|ARM.ActiveCfg = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|ARM.Build.0 = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|x64.ActiveCfg = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|x64.Build.0 = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|x86.ActiveCfg = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1117,6 +1171,9 @@ Global {C774DAE7-54A0-4FCD-A3B7-3CB63D7E112D} = {DBD7D9B6-FCC7-4650-91AF-E6457573A68F} {CEBE3A33-4814-42A4-BD8E-F7F2308A4C8C} = {8463ED7E-69FB-49AE-85CF-0791AFD98E38} {067C00CF-29FA-4643-814D-3A3C3C84634F} = {7971CAEB-B9F2-416B-966D-2D697C4C1E62} + {34C58129-07DB-287E-A29B-FA8EE8BAEB05} = {7971CAEB-B9F2-416B-966D-2D697C4C1E62} + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520} = {DBD7D9B6-FCC7-4650-91AF-E6457573A68F} + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2} = {8463ED7E-69FB-49AE-85CF-0791AFD98E38} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {94C56722-194E-4B8B-BC23-B3F754E89A20} diff --git a/src-console/ConsoleAppEF2.0.2_InMemory/ConsoleApp_netcore2.0_EF2.0.2_InMemory.csproj b/src-console/ConsoleAppEF2.0.2_InMemory/ConsoleApp_netcore2.0_EF2.0.2_InMemory.csproj index 3a5d7189..9fd2818d 100644 --- a/src-console/ConsoleAppEF2.0.2_InMemory/ConsoleApp_netcore2.0_EF2.0.2_InMemory.csproj +++ b/src-console/ConsoleAppEF2.0.2_InMemory/ConsoleApp_netcore2.0_EF2.0.2_InMemory.csproj @@ -19,7 +19,7 @@ - + diff --git a/src-console/ConsoleAppEF2.0/ConsoleApp_netcore2.0_EF2.0.1.csproj b/src-console/ConsoleAppEF2.0/ConsoleApp_netcore2.0_EF2.0.1.csproj index c019880d..ac0fd8be 100644 --- a/src-console/ConsoleAppEF2.0/ConsoleApp_netcore2.0_EF2.0.1.csproj +++ b/src-console/ConsoleAppEF2.0/ConsoleApp_netcore2.0_EF2.0.1.csproj @@ -12,7 +12,7 @@ - + diff --git a/src-console/ConsoleAppEF2.1.1/ConsoleApp_netcore2.1_EF2.1.1.csproj b/src-console/ConsoleAppEF2.1.1/ConsoleApp_netcore2.1_EF2.1.1.csproj index f31529c9..71257ddb 100644 --- a/src-console/ConsoleAppEF2.1.1/ConsoleApp_netcore2.1_EF2.1.1.csproj +++ b/src-console/ConsoleAppEF2.1.1/ConsoleApp_netcore2.1_EF2.1.1.csproj @@ -18,7 +18,7 @@ - + diff --git a/src-console/ConsoleAppEF2.1/ConsoleApp_netcore2.0_EF2.1.csproj b/src-console/ConsoleAppEF2.1/ConsoleApp_netcore2.0_EF2.1.csproj index 137df235..c30d8bda 100644 --- a/src-console/ConsoleAppEF2.1/ConsoleApp_netcore2.0_EF2.1.csproj +++ b/src-console/ConsoleAppEF2.1/ConsoleApp_netcore2.0_EF2.1.csproj @@ -16,7 +16,7 @@ - + diff --git a/src-console/ConsoleAppEF3.1/ConsoleApp_netcore3.1_EF3.1.csproj b/src-console/ConsoleAppEF3.1/ConsoleApp_netcore3.1_EF3.1.csproj index 0f71601c..1d297cd3 100644 --- a/src-console/ConsoleAppEF3.1/ConsoleApp_netcore3.1_EF3.1.csproj +++ b/src-console/ConsoleAppEF3.1/ConsoleApp_netcore3.1_EF3.1.csproj @@ -20,7 +20,7 @@ - + diff --git a/src-console/ConsoleAppEF5/ConsoleApp_net5.0_EF5.csproj b/src-console/ConsoleAppEF5/ConsoleApp_net5.0_EF5.csproj index 1fa72fbb..bc4122af 100644 --- a/src-console/ConsoleAppEF5/ConsoleApp_net5.0_EF5.csproj +++ b/src-console/ConsoleAppEF5/ConsoleApp_net5.0_EF5.csproj @@ -19,7 +19,7 @@ - + diff --git a/src-console/ConsoleAppEF5_InMemory/ConsoleApp_net5.0_EF5_InMemory.csproj b/src-console/ConsoleAppEF5_InMemory/ConsoleApp_net5.0_EF5_InMemory.csproj index 0d37e480..69742229 100644 --- a/src-console/ConsoleAppEF5_InMemory/ConsoleApp_net5.0_EF5_InMemory.csproj +++ b/src-console/ConsoleAppEF5_InMemory/ConsoleApp_net5.0_EF5_InMemory.csproj @@ -14,7 +14,7 @@ - + diff --git a/src-console/ConsoleAppEF6_InMemory/ConsoleApp_net6.0_EF6_InMemory.csproj b/src-console/ConsoleAppEF6_InMemory/ConsoleApp_net6.0_EF6_InMemory.csproj index e7a8b0ee..5db0eb28 100644 --- a/src-console/ConsoleAppEF6_InMemory/ConsoleApp_net6.0_EF6_InMemory.csproj +++ b/src-console/ConsoleAppEF6_InMemory/ConsoleApp_net6.0_EF6_InMemory.csproj @@ -14,7 +14,7 @@ - + diff --git a/src-console/ConsoleAppEF6_Sqlite/ConsoleApp_net6.0_EF6_Sqlite.csproj b/src-console/ConsoleAppEF6_Sqlite/ConsoleApp_net6.0_EF6_Sqlite.csproj index 9f54be59..a4203e3d 100644 --- a/src-console/ConsoleAppEF6_Sqlite/ConsoleApp_net6.0_EF6_Sqlite.csproj +++ b/src-console/ConsoleAppEF6_Sqlite/ConsoleApp_net6.0_EF6_Sqlite.csproj @@ -14,7 +14,7 @@ - + diff --git a/src-console/ConsoleApp_net10/ConsoleApp_net10.csproj b/src-console/ConsoleApp_net10/ConsoleApp_net10.csproj new file mode 100644 index 00000000..8c348bf9 --- /dev/null +++ b/src-console/ConsoleApp_net10/ConsoleApp_net10.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + ConsoleApp + enable + latest + + + + + + + + + + + + + \ No newline at end of file diff --git a/src-console/ConsoleApp_net10/DataColumnOrdinalIgnoreCaseComparer.cs b/src-console/ConsoleApp_net10/DataColumnOrdinalIgnoreCaseComparer.cs new file mode 100644 index 00000000..e1774a82 --- /dev/null +++ b/src-console/ConsoleApp_net10/DataColumnOrdinalIgnoreCaseComparer.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections; + +namespace ConsoleApp_net6._0; + +public class DataColumnOrdinalIgnoreCaseComparer : IComparer +{ + public int Compare(object? x, object? y) + { + if (x == null && y == null) + { + return 0; + } + + if (x == null) + { + return -1; + } + + if (y == null) + { + return 1; + } + + if (x is string xAsString && y is string yAsString) + { + return StringComparer.OrdinalIgnoreCase.Compare(xAsString, yAsString); + } + + return Comparer.Default.Compare(x, y); + } +} \ No newline at end of file diff --git a/src-console/ConsoleApp_net10/Program.cs b/src-console/ConsoleApp_net10/Program.cs new file mode 100644 index 00000000..6108ceb0 --- /dev/null +++ b/src-console/ConsoleApp_net10/Program.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Linq.Dynamic.Core.NewtonsoftJson; +using System.Linq.Dynamic.Core.SystemTextJson; +using System.Linq.Expressions; +using System.Text.Json; +using ConsoleApp_net6._0; +using Newtonsoft.Json.Linq; + +namespace ConsoleApp; + +public class X +{ + public string Key { get; set; } = null!; + + public List? Contestants { get; set; } +} + +public class Y +{ +} + +public class SalesData +{ + public string Region { get; set; } + public string Product { get; set; } + public string Sales { get; set; } +} + +public class GroupedSalesData +{ + public string Region { get; set; } + public string? Product { get; set; } + public int TotalSales { get; set; } + public int GroupLevel { get; set; } +} + +class Program +{ + static void Main(string[] args) + { + Issue918(); + return; + + Issue912a(); + Issue912b(); + return; + + Json(); + NewtonsoftJson(); + + return; + + Issue389DoesNotWork(); + return; + Issue389_Works(); + return; + + var q = new[] + { + new X { Key = "x" }, + new X { Key = "a" }, + new X { Key = "a", Contestants = new List { new() } } + }.AsQueryable(); + var groupByKey = q.GroupBy("Key"); + var selectQry = groupByKey.Select("new (Key, Sum(np(Contestants.Count, 0)) As TotalCount)").ToDynamicList(); + + Normal(); + Dynamic(); + } + + private static void Issue918() + { + var persons = new DataTable(); + persons.Columns.Add("FirstName", typeof(string)); + persons.Columns.Add("Nickname", typeof(string)); + persons.Columns.Add("Income", typeof(decimal)).AllowDBNull = true; + + // Adding sample data to the first DataTable + persons.Rows.Add("alex", DBNull.Value, 5000.50m); + persons.Rows.Add("MAGNUS", "Mag", 5000.50m); + persons.Rows.Add("Terry", "Ter", 4000.20m); + persons.Rows.Add("Charlotte", "Charl", DBNull.Value); + + var linqQuery = + from personsRow in persons.AsEnumerable() + select personsRow; + + var queryableRows = linqQuery.AsQueryable(); + + // Sorted at the top of the list + var comparer = new DataColumnOrdinalIgnoreCaseComparer(); + var sortedRows = queryableRows.OrderBy("FirstName", comparer).ToList(); + + int xxx = 0; + } + + private static void Issue912a() + { + var extractedRows = new List + { + new() { Region = "North", Product = "Widget", Sales = "100" }, + new() { Region = "North", Product = "Gadget", Sales = "150" }, + new() { Region = "South", Product = "Widget", Sales = "200" }, + new() { Region = "South", Product = "Gadget", Sales = "100" }, + new() { Region = "North", Product = "Widget", Sales = "50" } + }; + + var rows = extractedRows.AsQueryable(); + + // GROUPING SET 1: (Region, Product) + var detailed = rows + .GroupBy("new (Region, Product)") + .Select("new (Key.Region as Region, Key.Product as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 0 as GroupLevel)"); + + // GROUPING SET 2: (Region) + var regionSubtotal = rows + .GroupBy("Region") + .Select("new (Key as Region, null as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 1 as GroupLevel)"); + + var combined = detailed.Concat(regionSubtotal).AsQueryable(); + var ordered = combined.OrderBy("Product").ToDynamicList(); + + int x = 9; + } + + private static void Issue912b() + { + var eInfoJoinTable = new DataTable(); + eInfoJoinTable.Columns.Add("Region", typeof(string)); + eInfoJoinTable.Columns.Add("Product", typeof(string)); + eInfoJoinTable.Columns.Add("Sales", typeof(int)); + + eInfoJoinTable.Rows.Add("North", "Apples", 100); + eInfoJoinTable.Rows.Add("North", "Oranges", 150); + eInfoJoinTable.Rows.Add("South", "Apples", 200); + eInfoJoinTable.Rows.Add("South", "Oranges", 250); + + var extractedRows = + from row in eInfoJoinTable.AsEnumerable() + select row; + + var rows = extractedRows.AsQueryable(); + + // GROUPING SET 1: (Region, Product) + var detailed = rows + .GroupBy("new (Region, Product)") + .Select("new (Key.Region as Region, Key.Product as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 0 as GroupLevel)"); + + // GROUPING SET 2: (Region) + var regionSubtotal = rows + .GroupBy("Region") + .Select("new (Key as Region, null as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 1 as GroupLevel)"); + + var combined = detailed.ToDynamicArray().Concat(regionSubtotal.ToDynamicArray()).AsQueryable(); + var ordered = combined.OrderBy("Product").ToDynamicList(); + + int x = 9; + } + + private static void NewtonsoftJson() + { + var array = JArray.Parse(@"[ + { + ""first"": 1, + ""City"": ""Paris"", + ""third"": ""test"" + }, + { + ""first"": 2, + ""City"": ""New York"", + ""third"": ""abc"" + }]"); + + var where = array.Where("City == @0", "Paris"); + foreach (var result in where) + { + Console.WriteLine(result["first"]); + } + + var select = array.Select("City"); + foreach (var result in select) + { + Console.WriteLine(result); + } + + var whereWithSelect = array.Where("City == @0", "Paris").Select("first"); + foreach (var result in whereWithSelect) + { + Console.WriteLine(result); + } + } + + private static void Json() + { + var doc = JsonDocument.Parse(@"[ + { + ""first"": 1, + ""City"": ""Paris"", + ""third"": ""test"" + }, + { + ""first"": 2, + ""City"": ""New York"", + ""third"": ""abc"" + }]"); + + var where = doc.Where("City == @0", "Paris"); + foreach (var result in where.RootElement.EnumerateArray()) + { + Console.WriteLine(result.GetProperty("first")); + } + + var select = doc.Select("City"); + foreach (var result in select.RootElement.EnumerateArray()) + { + Console.WriteLine(result); + } + + var whereWithSelect = doc.Where("City == @0", "Paris").Select("first"); + foreach (var result in whereWithSelect.RootElement.EnumerateArray()) + { + Console.WriteLine(result); + } + } + + private static void Issue389_Works() + { + var strArray = new[] { "1", "2", "3", "4" }; + var x = new List(); + x.Add(Expression.Parameter(strArray.GetType(), "strArray")); + + string query = "string.Join(\",\", strArray)"; + + var e = DynamicExpressionParser.ParseLambda(x.ToArray(), null, query); + Delegate del = e.Compile(); + var result1 = del.DynamicInvoke(new object?[] { strArray }); + Console.WriteLine(result1); + } + + private static void Issue389WorksWithInts() + { + var intArray = new object[] { 1, 2, 3, 4 }; + var x = new List(); + x.Add(Expression.Parameter(intArray.GetType(), "intArray")); + + string query = "string.Join(\",\", intArray)"; + + var e = DynamicExpressionParser.ParseLambda(x.ToArray(), null, query); + Delegate del = e.Compile(); + var result = del.DynamicInvoke(new object?[] { intArray }); + + Console.WriteLine(result); + } + + private static void Issue389DoesNotWork() + { + var intArray = new[] { 1, 2, 3, 4 }; + var x = new List(); + x.Add(Expression.Parameter(intArray.GetType(), "intArray")); + + string query = "string.Join(\",\", intArray)"; + + var e = DynamicExpressionParser.ParseLambda(x.ToArray(), null, query); + Delegate del = e.Compile(); + var result = del.DynamicInvoke(new object?[] { intArray }); + + Console.WriteLine(result); + } + + private static void Normal() + { + var e = new int[0].AsQueryable(); + var q = new[] { 1 }.AsQueryable(); + + var a = q.FirstOrDefault(); + var b = e.FirstOrDefault(44); + + var c = q.FirstOrDefault(i => i == 0); + var d = q.FirstOrDefault(i => i == 0, 42); + + var t = q.Take(1); + } + + private static void Dynamic() + { + var e = new int[0].AsQueryable() as IQueryable; + var q = new[] { 1 }.AsQueryable() as IQueryable; + + var a = q.FirstOrDefault(); + //var b = e.FirstOrDefault(44); + + var c = q.FirstOrDefault("it == 0"); + //var d = q.FirstOrDefault(i => i == 0, 42); + } +} \ No newline at end of file diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10.csproj new file mode 100644 index 00000000..0c490a96 --- /dev/null +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10.csproj @@ -0,0 +1,47 @@ + + + + + Microsoft.EntityFrameworkCore.DynamicLinq + ../Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2/Microsoft.EntityFrameworkCore.DynamicLinq.snk + Microsoft.EntityFrameworkCore.DynamicLinq + $(DefineConstants);EFCORE;EFCORE_3X;EFDYNAMICFUNCTIONS;ASYNCENUMERABLE + Dynamic Linq extensions for Microsoft.EntityFrameworkCore which adds Async support + system;linq;dynamic;entityframework;core;async + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520} + net10.0 + 10.6.$(PatchVersion) + + + + full + + + + + portable + true + + + + net10.0 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Properties/AssemblyInfo.cs b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..69c2a3cd --- /dev/null +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.InteropServices; + +[assembly: ComVisible(false)] +[assembly: Guid("b467c675-c014-4b55-85b9-9578941d2ef8")] \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/System.Linq.Dynamic.Core.NewtonsoftJson.csproj b/src/System.Linq.Dynamic.Core.NewtonsoftJson/System.Linq.Dynamic.Core.NewtonsoftJson.csproj index 4cbf4851..802a0341 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/System.Linq.Dynamic.Core.NewtonsoftJson.csproj +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/System.Linq.Dynamic.Core.NewtonsoftJson.csproj @@ -8,7 +8,7 @@ Contains some extensions for System.Linq.Dynamic.Core to dynamically query a Newtonsoft.Json.JArray system;linq;dynamic;core;dotnet;json {8C5851B8-5C47-4229-AB55-D4252703598E} - net45;net452;net46;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0 + net45;net452;net46;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0;net10.0 1.6.$(PatchVersion) diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/System.Linq.Dynamic.Core.SystemTextJson.csproj b/src/System.Linq.Dynamic.Core.SystemTextJson/System.Linq.Dynamic.Core.SystemTextJson.csproj index f0c4ec8e..b91247dd 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/System.Linq.Dynamic.Core.SystemTextJson.csproj +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/System.Linq.Dynamic.Core.SystemTextJson.csproj @@ -8,7 +8,7 @@ Contains some extensions for System.Linq.Dynamic.Core to dynamically query a System.Text.Json.JsonDocument system;linq;dynamic;core;dotnet;json {FA01CE15-315A-499E-AFC2-955CA7EB45FF} - netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0 + netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0;net10.0 1.6.$(PatchVersion) diff --git a/src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj b/src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj index bcc32ad0..52443a69 100644 --- a/src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj +++ b/src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj @@ -9,7 +9,7 @@ This is a .NETStandard / .NET Core port of the the Microsoft assembly for the .Net 4.0 Dynamic language functionality. system;linq;dynamic;core;dotnet;NETCoreApp;NETStandard {D3804228-91F4-4502-9595-39584E510002} - net35;net40;net45;net452;net46;netstandard1.3;netstandard2.0;netstandard2.1;uap10.0;netcoreapp2.1;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0 + net35;net40;net45;net452;net46;netstandard1.3;netstandard2.0;netstandard2.1;uap10.0;netcoreapp2.1;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0 1.6.$(PatchVersion) @@ -34,7 +34,7 @@ $(DefineConstants);NETSTANDARD - + $(DefineConstants);ASYNCENUMERABLE diff --git a/test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj b/test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj index bb2d9ae6..88c88664 100644 --- a/test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj +++ b/test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj @@ -2,7 +2,7 @@ Stef Heyenrath - net461;net8.0;net9.0 + net461;net8.0;net9.0;net10.0 full EF;NET461 EntityFramework.DynamicLinq.Tests @@ -40,7 +40,7 @@ - + @@ -58,20 +58,25 @@ - + - + - + - + + + + + + $(DefineConstants);AspNetCoreIdentity diff --git a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/System.Linq.Dynamic.Core.NewtonsoftJson.Tests.csproj b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/System.Linq.Dynamic.Core.NewtonsoftJson.Tests.csproj index 4882ff18..57de2628 100644 --- a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/System.Linq.Dynamic.Core.NewtonsoftJson.Tests.csproj +++ b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/System.Linq.Dynamic.Core.NewtonsoftJson.Tests.csproj @@ -1,7 +1,7 @@  Stef Heyenrath - net452;netcoreapp3.1;net8.0 + net452;netcoreapp3.1;net8.0;net10.0 full True latest diff --git a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/DynamicExpressionParserTests.cs index 6e174735..ed3d4f95 100644 --- a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/DynamicExpressionParserTests.cs @@ -2,7 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Text.Json; -using FluentAssertions; +using AwesomeAssertions; namespace System.Linq.Dynamic.Core.SystemTextJson.Tests; diff --git a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/Extensions/JsonDocumentExtensionsTests.cs b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/Extensions/JsonDocumentExtensionsTests.cs index 048f7f3a..5c3698dc 100644 --- a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/Extensions/JsonDocumentExtensionsTests.cs +++ b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/Extensions/JsonDocumentExtensionsTests.cs @@ -1,7 +1,7 @@ using System.Linq.Dynamic.Core.SystemTextJson.Extensions; using System.Text; using System.Text.Json; -using FluentAssertions; +using AwesomeAssertions; using Xunit; namespace System.Linq.Dynamic.Core.SystemTextJson.Tests.Extensions; diff --git a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/System.Linq.Dynamic.Core.SystemTextJson.Tests.csproj b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/System.Linq.Dynamic.Core.SystemTextJson.Tests.csproj index dd619154..bfb5547e 100644 --- a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/System.Linq.Dynamic.Core.SystemTextJson.Tests.csproj +++ b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/System.Linq.Dynamic.Core.SystemTextJson.Tests.csproj @@ -1,7 +1,7 @@  Stef Heyenrath - net6.0;net8.0 + net6.0;net8.0;net10.0 full True latest @@ -11,7 +11,7 @@ - + all diff --git a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs index 7b27ec5c..e699e086 100644 --- a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs @@ -1,6 +1,6 @@ using System.Linq.Dynamic.Core.SystemTextJson.Config; using System.Text.Json; -using FluentAssertions; +using AwesomeAssertions; using Xunit; namespace System.Linq.Dynamic.Core.SystemTextJson.Tests; diff --git a/test/System.Linq.Dynamic.Core.Tests.Net5/System.Linq.Dynamic.Core.Tests.Net5.csproj b/test/System.Linq.Dynamic.Core.Tests.Net5/System.Linq.Dynamic.Core.Tests.Net5.csproj index 7e8ccc72..47b4bb3d 100644 --- a/test/System.Linq.Dynamic.Core.Tests.Net5/System.Linq.Dynamic.Core.Tests.Net5.csproj +++ b/test/System.Linq.Dynamic.Core.Tests.Net5/System.Linq.Dynamic.Core.Tests.Net5.csproj @@ -17,7 +17,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/test/System.Linq.Dynamic.Core.Tests.Net6/System.Linq.Dynamic.Core.Tests.Net6.csproj b/test/System.Linq.Dynamic.Core.Tests.Net6/System.Linq.Dynamic.Core.Tests.Net6.csproj index c20f741c..0550f818 100644 --- a/test/System.Linq.Dynamic.Core.Tests.Net6/System.Linq.Dynamic.Core.Tests.Net6.csproj +++ b/test/System.Linq.Dynamic.Core.Tests.Net6/System.Linq.Dynamic.Core.Tests.Net6.csproj @@ -16,7 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/test/System.Linq.Dynamic.Core.Tests.Net7/System.Linq.Dynamic.Core.Tests.Net7.csproj b/test/System.Linq.Dynamic.Core.Tests.Net7/System.Linq.Dynamic.Core.Tests.Net7.csproj index 6d415bb0..90fd9b01 100644 --- a/test/System.Linq.Dynamic.Core.Tests.Net7/System.Linq.Dynamic.Core.Tests.Net7.csproj +++ b/test/System.Linq.Dynamic.Core.Tests.Net7/System.Linq.Dynamic.Core.Tests.Net7.csproj @@ -16,7 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/test/System.Linq.Dynamic.Core.Tests.Net8/System.Linq.Dynamic.Core.Tests.Net8.csproj b/test/System.Linq.Dynamic.Core.Tests.Net8/System.Linq.Dynamic.Core.Tests.Net8.csproj index 978abb1f..a6dda0af 100644 --- a/test/System.Linq.Dynamic.Core.Tests.Net8/System.Linq.Dynamic.Core.Tests.Net8.csproj +++ b/test/System.Linq.Dynamic.Core.Tests.Net8/System.Linq.Dynamic.Core.Tests.Net8.csproj @@ -16,7 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + @@ -31,7 +31,7 @@ - + diff --git a/test/System.Linq.Dynamic.Core.Tests.Net9/System.Linq.Dynamic.Core.Tests.Net9.csproj b/test/System.Linq.Dynamic.Core.Tests.Net9/System.Linq.Dynamic.Core.Tests.Net9.csproj new file mode 100644 index 00000000..f7cb2fa7 --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests.Net9/System.Linq.Dynamic.Core.Tests.Net9.csproj @@ -0,0 +1,48 @@ + + + + net9.0 + System.Linq.Dynamic.Core.Tests + full + True + ../../src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.snk + false + $(DefineConstants);NETCOREAPP;EFCORE;EFCORE_3X;NETCOREAPP3_1;AspNetCoreIdentity + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/System.Linq.Dynamic.Core.Tests.NetCoreApp31/System.Linq.Dynamic.Core.Tests.NetCoreApp31.csproj b/test/System.Linq.Dynamic.Core.Tests.NetCoreApp31/System.Linq.Dynamic.Core.Tests.NetCoreApp31.csproj index 5b908293..76de3f36 100644 --- a/test/System.Linq.Dynamic.Core.Tests.NetCoreApp31/System.Linq.Dynamic.Core.Tests.NetCoreApp31.csproj +++ b/test/System.Linq.Dynamic.Core.Tests.NetCoreApp31/System.Linq.Dynamic.Core.Tests.NetCoreApp31.csproj @@ -22,7 +22,7 @@ all runtime; build; native; contentfiles; analyzers - + diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs index 6d33cae8..066d5ce4 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs @@ -109,7 +109,11 @@ public void DynamicClass_GetPropertyValue_Should_Work() value.Should().Be(test); } +#if NET461 + [Fact(Skip = "Does not work for .NET 4.6.1")] +#else [Fact] +#endif public void DynamicClass_GettingValue_ByIndex_Should_Work() { // Arrange @@ -127,7 +131,11 @@ public void DynamicClass_GettingValue_ByIndex_Should_Work() value.Should().Be(test); } +#if NET461 + [Fact(Skip = "Does not work for .NET 4.6.1")] +#else [Fact] +#endif public void DynamicClass_SettingExistingPropertyValue_ByIndex_Should_Work() { // Arrange @@ -151,7 +159,11 @@ public void DynamicClass_SettingExistingPropertyValue_ByIndex_Should_Work() value.Should().Be(newValue); } +#if NET461 + [Fact(Skip = "Does not work for .NET 4.6.1")] +#else [Fact] +#endif public void DynamicClass_SettingNewProperty_ByIndex_Should_Work() { // Arrange diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs index 70d5ce68..e301394c 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -1748,9 +1748,9 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexEx } [Theory] - [InlineData(true, "c => c.Age == 8", "c => (c.Age == Convert(8, Nullable`1))")] + [InlineData(true, "c => c.Age == 8", "c => (c.Age == ")] [InlineData(true, "c => c.Name == \"test\"", "c => (c.Name == \"test\")")] - [InlineData(false, "c => c.Age == 8", "Param_0 => (Param_0.Age == Convert(8, Nullable`1))")] + [InlineData(false, "c => c.Age == 8", "Param_0 => (Param_0.Age == ")] [InlineData(false, "c => c.Name == \"test\"", "Param_0 => (Param_0.Name == \"test\")")] public void DynamicExpressionParser_ParseLambda_RenameParameterExpression(bool renameParameterExpression, string expressionAsString, string expected) { @@ -1765,7 +1765,7 @@ public void DynamicExpressionParser_ParseLambda_RenameParameterExpression(bool r var result = expression.ToString(); // Assert - Check.That(result).IsEqualTo(expected); + Check.That(result).Contains(expected); } [Theory] diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs index 83d17e9c..ddd32a83 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs @@ -471,7 +471,7 @@ public void Select_Dynamic_RenameParameterExpression_Is_True() Check.That(result).Equals("System.Int32[].Select(it => (it * it))"); } -#if NET461 || NET5_0 || NET6_0 || NET7_0 || NET8_0 || NET9_0 +#if NET461 || NET5_0 || NET6_0 || NET7_0 || NET8_0 || NET9_0 || NET10_0 [Fact(Skip = "Fails sometimes in GitHub CI build")] #else [Fact] diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs index 23f6a963..f5300898 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs @@ -157,8 +157,9 @@ public void Where_Dynamic_Exceptions() Assert.Throws(() => qry.Where((string?)null)); Assert.Throws(() => qry.Where("")); Assert.Throws(() => qry.Where(" ")); - var parsingConfigException = Assert.Throws(() => qry.Where("UserName == \"x\"", ParsingConfig.Default)); - Assert.Equal("The ParsingConfig should be provided as first argument to this method. (Parameter 'args')", parsingConfigException.Message); + + Action act = () => qry.Where("UserName == \"x\"", ParsingConfig.Default); + act.Should().Throw().WithMessage("The ParsingConfig should be provided as first argument to this method.*"); } [Fact] diff --git a/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj b/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj index 749ea06c..b76c8eb6 100644 --- a/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj +++ b/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj @@ -1,6 +1,6 @@  - net9.0 + net10.0 System.Linq.Dynamic.Core.Tests full True @@ -15,7 +15,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + From aa47b50f1da56ddb2eb4dc7b24320684a53ab18e Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 15 Nov 2025 12:56:27 +0100 Subject: [PATCH 06/21] v1.7.0 --- CHANGELOG.md | 6 ++++++ Generate-ReleaseNotes.bat | 2 +- .../EntityFramework.DynamicLinq.csproj | 2 +- ...icrosoft.EntityFrameworkCore.DynamicLinq.EFCore10.csproj | 2 +- ...Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2.csproj | 2 +- ...Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3.csproj | 2 +- ...Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5.csproj | 2 +- ...Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6.csproj | 2 +- ...Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7.csproj | 2 +- ...Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8.csproj | 2 +- ...Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9.csproj | 2 +- .../System.Linq.Dynamic.Core.NewtonsoftJson.csproj | 2 +- .../System.Linq.Dynamic.Core.SystemTextJson.csproj | 2 +- .../System.Linq.Dynamic.Core.csproj | 2 +- .../Z.EntityFramework.Classic.DynamicLinq.csproj | 2 +- version.xml | 2 +- 16 files changed, 21 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbaa99da..0f7ce7ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v1.7.0 (15 November 2025) +- [#956](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/956) - Fix parsing Hex and Binary [bug] contributed by [StefH](https://github.com/StefH) +- [#957](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/957) - .NET 10 [feature] contributed by [StefH](https://github.com/StefH) +- [#958](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/958) - Support normalization of objects for Z.DynamicLinq.Json [feature] contributed by [StefH](https://github.com/StefH) +- [#955](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/955) - Hexadecimal und binary literals sometimes are interpreted as decimal [bug] + # v1.6.10 (08 November 2025) - [#953](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/953) - Fixed adding Enum and integer [bug] contributed by [StefH](https://github.com/StefH) - [#954](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/954) - Fix ExpressionHelper.TryConvertTypes to generate correct Convert in case left or right is null [bug] contributed by [StefH](https://github.com/StefH) diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index 3196cbc2..07a52300 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.6.10 +SET version=v1.7.0 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/src/EntityFramework.DynamicLinq/EntityFramework.DynamicLinq.csproj b/src/EntityFramework.DynamicLinq/EntityFramework.DynamicLinq.csproj index a0b95e83..518f2653 100644 --- a/src/EntityFramework.DynamicLinq/EntityFramework.DynamicLinq.csproj +++ b/src/EntityFramework.DynamicLinq/EntityFramework.DynamicLinq.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {D3804228-91F4-4502-9595-39584E510000} net45;net452;net46;netstandard2.1 - 1.6.$(PatchVersion) + 1.7.$(PatchVersion) diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10.csproj index 0c490a96..54817a4c 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10.csproj +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520} net10.0 - 10.6.$(PatchVersion) + 10.7.$(PatchVersion) diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2.csproj index 859ac63e..2e9a5892 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2.csproj +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {D3804228-91F4-4502-9595-39584E510001} netstandard2.0 - 2.6.$(PatchVersion) + 2.7.$(PatchVersion) diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3.csproj index f5dafeee..0254e9a8 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3.csproj +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {7994FECC-965C-4A5D-8B0E-1A6EA769D4BE} netstandard2.0 - 3.6.$(PatchVersion) + 3.7.$(PatchVersion) diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5.csproj index 46c2f5f9..8393f083 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5.csproj +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {D3804228-91F4-4502-9595-39584E519901} netstandard2.1;net5.0 - 5.6.$(PatchVersion) + 5.7.$(PatchVersion) diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6.csproj index 058a5abb..d6d07c0b 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6.csproj +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {D28F6393-B56B-40A2-AF67-E8D669F42546} net6.0 - 6.6.$(PatchVersion) + 6.7.$(PatchVersion) diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7.csproj index 61eec0d8..0d590e5e 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7.csproj +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {FB2F4C99-EC34-4D29-87E2-944B25D90ef7} net6.0;net7.0 - 7.6.$(PatchVersion) + 7.7.$(PatchVersion) diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8.csproj index 2e95aabf..b95d7eeb 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8.csproj +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {9000129D-322D-4FE6-9C47-75464577C374} net8.0 - 8.6.$(PatchVersion) + 8.7.$(PatchVersion) diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9.csproj index ea16a63e..129a511b 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9.csproj +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {C774DAE7-54A0-4FCD-A3B7-3CB63D7E112D} net9.0 - 9.6.$(PatchVersion) + 9.7.$(PatchVersion) diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/System.Linq.Dynamic.Core.NewtonsoftJson.csproj b/src/System.Linq.Dynamic.Core.NewtonsoftJson/System.Linq.Dynamic.Core.NewtonsoftJson.csproj index 802a0341..4d399df7 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/System.Linq.Dynamic.Core.NewtonsoftJson.csproj +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/System.Linq.Dynamic.Core.NewtonsoftJson.csproj @@ -9,7 +9,7 @@ system;linq;dynamic;core;dotnet;json {8C5851B8-5C47-4229-AB55-D4252703598E} net45;net452;net46;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0;net10.0 - 1.6.$(PatchVersion) + 1.7.$(PatchVersion) diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/System.Linq.Dynamic.Core.SystemTextJson.csproj b/src/System.Linq.Dynamic.Core.SystemTextJson/System.Linq.Dynamic.Core.SystemTextJson.csproj index b91247dd..41c3691b 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/System.Linq.Dynamic.Core.SystemTextJson.csproj +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/System.Linq.Dynamic.Core.SystemTextJson.csproj @@ -9,7 +9,7 @@ system;linq;dynamic;core;dotnet;json {FA01CE15-315A-499E-AFC2-955CA7EB45FF} netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0;net10.0 - 1.6.$(PatchVersion) + 1.7.$(PatchVersion) diff --git a/src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj b/src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj index 52443a69..1627393c 100644 --- a/src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj +++ b/src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;core;dotnet;NETCoreApp;NETStandard {D3804228-91F4-4502-9595-39584E510002} net35;net40;net45;net452;net46;netstandard1.3;netstandard2.0;netstandard2.1;uap10.0;netcoreapp2.1;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0 - 1.6.$(PatchVersion) + 1.7.$(PatchVersion) diff --git a/src/Z.EntityFramework.Classic.DynamicLinq/Z.EntityFramework.Classic.DynamicLinq.csproj b/src/Z.EntityFramework.Classic.DynamicLinq/Z.EntityFramework.Classic.DynamicLinq.csproj index 8cf07a98..4edc3a0e 100644 --- a/src/Z.EntityFramework.Classic.DynamicLinq/Z.EntityFramework.Classic.DynamicLinq.csproj +++ b/src/Z.EntityFramework.Classic.DynamicLinq/Z.EntityFramework.Classic.DynamicLinq.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;Z.EntityFramework;core;async;classic {D3804228-91F4-4502-9595-39584Ea20000} net45;netstandard2.0 - 1.6.$(PatchVersion) + 1.7.$(PatchVersion) diff --git a/version.xml b/version.xml index 79743c43..c8f45c09 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 10 + 0 \ No newline at end of file From 1816c3b23db762fab0602737553fd681c920c543 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 22 Nov 2025 10:31:36 +0100 Subject: [PATCH 07/21] Fix Json when property value is null (#961) * Fix Z.DynamicLinq.SystemTextJson when property is null * fix part 2 --- .../Config/NewtonsoftJsonParsingConfig.cs | 7 +++-- ...ormalizationNonExistingPropertyBehavior.cs | 8 +++--- .../Extensions/JObjectExtensions.cs | 5 +--- .../NewtonsoftJsonExtensions.cs | 7 +++-- .../Utils/NormalizeUtils.cs | 25 +++++++++++++++-- ...ormalizationNonExistingPropertyBehavior.cs | 10 +++---- .../Config/SystemTextJsonParsingConfig.cs | 2 +- .../Extensions/JsonDocumentExtensions.cs | 7 ++--- .../Utils/NormalizeUtils.cs | 27 ++++++++++++++++--- .../NewtonsoftJsonTests.cs | 11 ++++---- .../SystemTextJsonTests.cs | 24 +++++++++-------- 11 files changed, 88 insertions(+), 45 deletions(-) diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NewtonsoftJsonParsingConfig.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NewtonsoftJsonParsingConfig.cs index fbccbf64..3cb24306 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NewtonsoftJsonParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NewtonsoftJsonParsingConfig.cs @@ -10,7 +10,10 @@ public class NewtonsoftJsonParsingConfig : ParsingConfig /// /// The default ParsingConfig for . /// - public new static NewtonsoftJsonParsingConfig Default { get; } = new(); + public new static NewtonsoftJsonParsingConfig Default { get; } = new NewtonsoftJsonParsingConfig + { + ConvertObjectToSupportComparison = true + }; /// /// The default to use. @@ -28,7 +31,7 @@ public class NewtonsoftJsonParsingConfig : ParsingConfig /// /// Use this property to control how the normalization process handles properties that are missing or undefined. /// The selected behavior may affect the output or error handling of normalization operations. - /// The default value is . + /// The default value is . /// public NormalizationNonExistingPropertyBehavior NormalizationNonExistingPropertyValueBehavior { get; set; } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NormalizationNonExistingPropertyBehavior.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NormalizationNonExistingPropertyBehavior.cs index bb68277b..44608e83 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NormalizationNonExistingPropertyBehavior.cs +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NormalizationNonExistingPropertyBehavior.cs @@ -6,12 +6,12 @@ public enum NormalizationNonExistingPropertyBehavior { /// - /// Specifies that the default value should be used. + /// Specifies that a null value should be used. /// - UseDefaultValue = 0, + UseNull = 0, /// - /// Specifies that null values should be used. + /// Specifies that the default value should be used. /// - UseNull = 1 + UseDefaultValue = 1 } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Extensions/JObjectExtensions.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Extensions/JObjectExtensions.cs index 1a7be2f4..2f79840b 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Extensions/JObjectExtensions.cs +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Extensions/JObjectExtensions.cs @@ -44,10 +44,7 @@ private class JTokenResolvers : Dictionary func) private static IQueryable ToQueryable(JArray source, NewtonsoftJsonParsingConfig? config = null) { - var normalized = config?.Normalize == true ? - NormalizeUtils.NormalizeArray(source, config.NormalizationNonExistingPropertyValueBehavior): + config = config ?? NewtonsoftJsonParsingConfig.Default; + config.ConvertObjectToSupportComparison = true; + + var normalized = config.Normalize == true ? + NormalizeUtils.NormalizeArray(source, config.NormalizationNonExistingPropertyValueBehavior) : source; return normalized diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Utils/NormalizeUtils.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Utils/NormalizeUtils.cs index 5669915c..fe980319 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Utils/NormalizeUtils.cs +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Utils/NormalizeUtils.cs @@ -86,7 +86,7 @@ private static JObject NormalizeObject(JObject source, Dictionary schem } else { - obj[key] = normalizationBehavior == NormalizationNonExistingPropertyBehavior.UseDefaultValue ? GetDefaultValue(schema[key]) : JValue.CreateNull(); + obj[key] = GetDefaultOrNullValue(normalizationBehavior, schema[key]); } } @@ -125,7 +125,28 @@ private static JToken GetDefaultValue(JsonValueInfo jType) JTokenType.Integer => default(int), JTokenType.String => string.Empty, JTokenType.TimeSpan => TimeSpan.MinValue, + _ => GetNullValue(jType), + }; + } + + private static JValue GetNullValue(JsonValueInfo jType) + { + return jType.Type switch + { + JTokenType.Boolean => new JValue((bool?)null), + JTokenType.Bytes => new JValue((byte[]?)null), + JTokenType.Date => new JValue((DateTime?)null), + JTokenType.Float => new JValue((float?)null), + JTokenType.Guid => new JValue((Guid?)null), + JTokenType.Integer => new JValue((int?)null), + JTokenType.String => new JValue((string?)null), + JTokenType.TimeSpan => new JValue((TimeSpan?)null), _ => JValue.CreateNull(), }; } + + private static JToken GetDefaultOrNullValue(NormalizationNonExistingPropertyBehavior behavior, JsonValueInfo jType) + { + return behavior == NormalizationNonExistingPropertyBehavior.UseDefaultValue ? GetDefaultValue(jType) : GetNullValue(jType); + } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/Config/NormalizationNonExistingPropertyBehavior.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/Config/NormalizationNonExistingPropertyBehavior.cs index daafaa4b..381f0408 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/Config/NormalizationNonExistingPropertyBehavior.cs +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/Config/NormalizationNonExistingPropertyBehavior.cs @@ -1,17 +1,17 @@ namespace System.Linq.Dynamic.Core.SystemTextJson.Config; /// -/// Specifies the behavior to use when setting a property vlue that does not exist or is missing during normalization. +/// Specifies the behavior to use when setting a property value that does not exist or is missing during normalization. /// public enum NormalizationNonExistingPropertyBehavior { /// - /// Specifies that the default value should be used. + /// Specifies that a null value should be used. /// - UseDefaultValue = 0, + UseNull = 0, /// - /// Specifies that null values should be used. + /// Specifies that the default value should be used. /// - UseNull = 1 + UseDefaultValue = 1 } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/Config/SystemTextJsonParsingConfig.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/Config/SystemTextJsonParsingConfig.cs index a4c1e76a..4892bcf2 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/Config/SystemTextJsonParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/Config/SystemTextJsonParsingConfig.cs @@ -24,7 +24,7 @@ public class SystemTextJsonParsingConfig : ParsingConfig /// /// Use this property to control how the normalization process handles properties that are missing or undefined. /// The selected behavior may affect the output or error handling of normalization operations. - /// The default value is . + /// The default value is . /// public NormalizationNonExistingPropertyBehavior NormalizationNonExistingPropertyValueBehavior { get; set; } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonDocumentExtensions.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonDocumentExtensions.cs index 7daf15a5..3437d3c9 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonDocumentExtensions.cs +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonDocumentExtensions.cs @@ -34,10 +34,7 @@ private class JTokenResolvers : Dictionary src, Type newType) { var method = ConvertToTypedArrayGenericMethod.MakeGenericMethod(newType); - return (IEnumerable)method.Invoke(null, new object[] { src })!; + return (IEnumerable)method.Invoke(null, [src])!; } private static readonly MethodInfo ConvertToTypedArrayGenericMethod = typeof(JsonDocumentExtensions).GetMethod(nameof(ConvertToTypedArrayGeneric), BindingFlags.NonPublic | BindingFlags.Static)!; diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs index fa5836d2..7f6e4abf 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs @@ -104,15 +104,16 @@ private static JsonObject NormalizeObject(JsonObject source, Dictionary sc } else { - obj[key] = normalizationBehavior == NormalizationNonExistingPropertyBehavior.UseDefaultValue ? GetDefaultValue(jType) : null; + obj[key] = GetDefaultOrNullValue(normalizationBehavior, jType); } } @@ -150,7 +151,25 @@ private static JsonObject CreateEmptyObject(Dictionary sc JsonValueKind.Number => default(int), JsonValueKind.String => string.Empty, JsonValueKind.True => false, + _ => GetNullValue(jType), + }; + } + + private static JsonNode? GetNullValue(JsonValueInfo jType) + { + return jType.Type switch + { + JsonValueKind.Array => null, + JsonValueKind.False => JsonValue.Create(false), + JsonValueKind.Number => JsonValue.Create(null), + JsonValueKind.String => JsonValue.Create(null), + JsonValueKind.True => JsonValue.Create(true), _ => null, }; } + + private static JsonNode? GetDefaultOrNullValue(NormalizationNonExistingPropertyBehavior behavior, JsonValueInfo jType) + { + return behavior == NormalizationNonExistingPropertyBehavior.UseDefaultValue ? GetDefaultValue(jType) : GetNullValue(jType); + } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs index e391ed29..d07d8052 100644 --- a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs @@ -1,5 +1,4 @@ -using System.Linq.Dynamic.Core.Exceptions; -using System.Linq.Dynamic.Core.NewtonsoftJson.Config; +using System.Linq.Dynamic.Core.NewtonsoftJson.Config; using FluentAssertions; using Newtonsoft.Json.Linq; using Xunit; @@ -13,11 +12,13 @@ public class NewtonsoftJsonTests [ { "Name": "John", - "Age": 30 + "Age": 30, + "IsNull": null }, { "Name": "Doe", - "Age": 40 + "Age": 40, + "AlsoNull": null } ] """; @@ -567,6 +568,6 @@ public void NormalizeArray_When_NormalizeIsFalse_ShouldThrow() Action act = () => JArray.Parse(array).Where(config, "Age >= 30"); // Assert - act.Should().Throw().WithMessage("The binary operator GreaterThanOrEqual is not defined for the types 'System.Object' and 'System.Int32'."); + act.Should().Throw().WithMessage("Unable to find property 'Age' on type '<>*"); } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs index e699e086..dd5e62ee 100644 --- a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs @@ -17,11 +17,13 @@ public class SystemTextJsonTests [ { "Name": "John", - "Age": 30 + "Age": 30, + "IsNull": null }, { "Name": "Doe", - "Age": 40 + "Age": 40, + "AlsoNull": null } ] """; @@ -161,20 +163,20 @@ public void Distinct() public void First() { // Act + Assert 1 - _source.First().GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""John"",""Age"":30}").RootElement.GetRawText()); + _source.First().GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""John"",""Age"":30,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); // Act + Assert 2 - _source.First("Age > 30").GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40}").RootElement.GetRawText()); + _source.First("Age > 30").GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); } [Fact] public void FirstOrDefault() { // Act + Assert 1 - _source.FirstOrDefault()!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""John"",""Age"":30}").RootElement.GetRawText()); + _source.FirstOrDefault()!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""John"",""Age"":30,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); // Act + Assert 2 - _source.FirstOrDefault("Age > 30")!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40}").RootElement.GetRawText()); + _source.FirstOrDefault("Age > 30")!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); // Act + Assert 3 _source.FirstOrDefault("Age > 999").Should().BeNull(); @@ -267,20 +269,20 @@ public void GroupBySimpleKeySelector() public void Last() { // Act + Assert 1 - _source.Last().GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40}").RootElement.GetRawText()); + _source.Last().GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); // Act + Assert 2 - _source.Last("Age > 0").GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40}").RootElement.GetRawText()); + _source.Last("Age > 0").GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); } [Fact] public void LastOrDefault() { // Act + Assert 1 - _source.LastOrDefault()!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40}").RootElement.GetRawText()); + _source.LastOrDefault()!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); // Act + Assert 2 - _source.LastOrDefault("Age > 0")!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40}").RootElement.GetRawText()); + _source.LastOrDefault("Age > 0")!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); // Act + Assert 3 _source.LastOrDefault("Age > 999").Should().BeNull(); @@ -444,7 +446,7 @@ public void SelectMany() public void Single() { // Act + Assert - _source.Single("Age > 30").GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40}").RootElement.GetRawText()); + _source.Single("Age > 30").GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); } [Fact] From 479ab6dd7801b9502dcf0a20905fb70e2561ac7a Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sun, 23 Nov 2025 09:42:38 +0100 Subject: [PATCH 08/21] json: fix logic when property is not found (#962) * json: fix logic when property is not found * refactor --- src/System.Linq.Dynamic.Core/DynamicClass.cs | 10 ++++ .../DynamicClass.uap.cs | 14 +++++- .../Parser/ExpressionHelper.cs | 50 +++++++++++++++++-- .../Parser/ExpressionParser.cs | 28 ++++++++++- .../NewtonsoftJsonTests.cs | 2 + .../SystemTextJsonTests.cs | 2 + 6 files changed, 99 insertions(+), 7 deletions(-) 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 From ed0a3ddf6f7d39c3becff1faf2f6c8cae7515b0a Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Thu, 27 Nov 2025 17:09:37 +0100 Subject: [PATCH 09/21] Fix NumberParser for integer < int.MinValue (#965) --- src/System.Linq.Dynamic.Core/Parser/NumberParser.cs | 2 +- .../Parser/NumberParserTests.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs b/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs index 798ff097..f4bbc731 100644 --- a/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs @@ -149,7 +149,7 @@ public Expression ParseIntegerLiteral(int tokenPosition, string text) throw new ParseException(Res.MinusCannotBeAppliedToUnsignedInteger, tokenPosition); } - if (value <= int.MaxValue) + if (value >= int.MinValue && value <= int.MaxValue) { return _constantExpressionHelper.CreateLiteral((int)value, textOriginal); } diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/NumberParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/NumberParserTests.cs index f8ccc496..2f8a8ed8 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/NumberParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/NumberParserTests.cs @@ -1,8 +1,8 @@ -using FluentAssertions; using System.Collections.Generic; using System.Globalization; using System.Linq.Dynamic.Core.Parser; using System.Linq.Expressions; +using FluentAssertions; using Xunit; namespace System.Linq.Dynamic.Core.Tests.Parser; @@ -129,6 +129,8 @@ public void NumberParser_ParseNumber_Double(string? culture, string text, double [Theory] [InlineData("42", 42)] [InlineData("-42", -42)] + [InlineData("3000000000", 3000000000)] + [InlineData("-3000000000", -3000000000)] [InlineData("77u", 77)] [InlineData("77l", 77)] [InlineData("77ul", 77)] From c0e417c014bcffad2326201c5c562431f72d9026 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 29 Nov 2025 07:28:50 +0100 Subject: [PATCH 10/21] v1.7.1 --- CHANGELOG.md | 7 +++++++ Generate-ReleaseNotes.bat | 2 +- version.xml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f7ce7ac..4a4ae9bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# v1.7.1 (29 November 2025) +- [#961](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/961) - Fix Json when property value is null [bug] contributed by [StefH](https://github.com/StefH) +- [#962](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/962) - json: fix logic when property is not found [bug] contributed by [StefH](https://github.com/StefH) +- [#965](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/965) - Fix NumberParser for integer < int.MinValue [bug] contributed by [StefH](https://github.com/StefH) +- [#960](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/960) - json: follow up for not existing members [bug] +- [#964](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/964) - Integer numbers smaller than int.MinValue are not parsed correctly [bug] + # v1.7.0 (15 November 2025) - [#956](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/956) - Fix parsing Hex and Binary [bug] contributed by [StefH](https://github.com/StefH) - [#957](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/957) - .NET 10 [feature] contributed by [StefH](https://github.com/StefH) diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index 07a52300..809d22d8 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.7.0 +SET version=v1.7.1 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 c8f45c09..d0d14392 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 0 + 1 \ No newline at end of file From b1a64fff418ac772eea114e1ea47367266f77c01 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 28 Mar 2026 09:53:40 +0100 Subject: [PATCH 11/21] Fix some sonarcloud issues (#971) * Fix some sonarcloud issues * . * array --- .../Extensions/JObjectExtensions.cs | 4 ++-- .../NewtonsoftJsonExtensions.cs | 4 ++-- .../Extensions/JsonDocumentExtensions.cs | 2 +- .../Utils/NormalizeUtils.cs | 2 +- .../Compatibility/EmptyArray.cs | 11 +++++++++++ src/System.Linq.Dynamic.Core/DynamicClassFactory.cs | 2 +- .../Extensions/ListExtensions.cs | 1 + .../Parser/ExpressionHelper.cs | 2 +- .../Parser/ExpressionParser.cs | 6 +++--- .../Parser/SupportedMethods/MethodFinder.cs | 2 +- 10 files changed, 24 insertions(+), 12 deletions(-) create mode 100644 src/System.Linq.Dynamic.Core/Compatibility/EmptyArray.cs diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Extensions/JObjectExtensions.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Extensions/JObjectExtensions.cs index 2f79840b..5b1161fa 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Extensions/JObjectExtensions.cs +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Extensions/JObjectExtensions.cs @@ -11,7 +11,7 @@ namespace System.Linq.Dynamic.Core.NewtonsoftJson.Extensions; /// internal static class JObjectExtensions { - private class JTokenResolvers : Dictionary>; + private sealed class JTokenResolvers : Dictionary>; private static readonly JTokenResolvers Resolvers = new() { @@ -57,7 +57,7 @@ private class JTokenResolvers : Dictionary.Value : ConvertJTokenArray(src, options); } private static object? ConvertJObject(JToken arg, DynamicJsonClassOptions? options = null) diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs index ff580c52..d44b3dbf 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs @@ -874,12 +874,12 @@ private static IQueryable ToQueryable(JArray source, NewtonsoftJsonParsingConfig config = config ?? NewtonsoftJsonParsingConfig.Default; config.ConvertObjectToSupportComparison = true; - var normalized = config.Normalize == true ? + var normalized = config.Normalize ? NormalizeUtils.NormalizeArray(source, config.NormalizationNonExistingPropertyValueBehavior) : source; return normalized - .ToDynamicJsonClassArray(config?.DynamicJsonClassOptions) + .ToDynamicJsonClassArray(config.DynamicJsonClassOptions) .AsQueryable(); } #endregion diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonDocumentExtensions.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonDocumentExtensions.cs index 3437d3c9..af2501f5 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonDocumentExtensions.cs +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonDocumentExtensions.cs @@ -8,7 +8,7 @@ namespace System.Linq.Dynamic.Core.SystemTextJson.Extensions; internal static class JsonDocumentExtensions { - private class JTokenResolvers : Dictionary>; + private sealed class JTokenResolvers : Dictionary>; private static readonly JTokenResolvers Resolvers = new() { diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs index 7f6e4abf..e748650d 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs @@ -155,7 +155,7 @@ private static JsonObject CreateEmptyObject(Dictionary sc }; } - private static JsonNode? GetNullValue(JsonValueInfo jType) + private static JsonValue? GetNullValue(JsonValueInfo jType) { return jType.Type switch { diff --git a/src/System.Linq.Dynamic.Core/Compatibility/EmptyArray.cs b/src/System.Linq.Dynamic.Core/Compatibility/EmptyArray.cs new file mode 100644 index 00000000..4ba83704 --- /dev/null +++ b/src/System.Linq.Dynamic.Core/Compatibility/EmptyArray.cs @@ -0,0 +1,11 @@ +// ReSharper disable once CheckNamespace +namespace System; + +internal static class EmptyArray +{ +#if NET35 || NET40 || NET45 || NET452 + public static readonly T[] Value = []; +#else + public static readonly T[] Value = Array.Empty(); +#endif +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs b/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs index 75264a4e..1b4a1eef 100644 --- a/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs +++ b/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs @@ -462,7 +462,7 @@ private static void EmitEqualityOperators(TypeBuilder typeBuilder, MethodBuilder ILGenerator ilNeq = inequalityOperator.GetILGenerator(); - // return !(left == right); + // Define return !(left == right); ilNeq.Emit(OpCodes.Ldarg_0); ilNeq.Emit(OpCodes.Ldarg_1); ilNeq.Emit(OpCodes.Call, equalityOperator); diff --git a/src/System.Linq.Dynamic.Core/Extensions/ListExtensions.cs b/src/System.Linq.Dynamic.Core/Extensions/ListExtensions.cs index 8885fc4e..7cb20bbc 100644 --- a/src/System.Linq.Dynamic.Core/Extensions/ListExtensions.cs +++ b/src/System.Linq.Dynamic.Core/Extensions/ListExtensions.cs @@ -5,6 +5,7 @@ namespace System.Linq.Dynamic.Core.Extensions; internal static class ListExtensions { internal static void AddIfNotNull(this IList list, T? value) + where T : class { if (value != null) { diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs index 8222d4d7..a9b31876 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs @@ -540,7 +540,7 @@ private static Expression GenerateStaticMethodCall(string methodName, Expression right = Expression.Convert(right, parameterTypeRight); } - return Expression.Call(null, methodInfo, [left, right]); + return Expression.Call(null, methodInfo, left, right); } private static bool TryGetStaticMethod(string methodName, Expression left, Expression right, [NotNullWhen(true)] out MethodInfo? methodInfo) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index b8091f55..20524d36 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -1980,7 +1980,7 @@ private bool TryFindPropertyOrField(Type type, string id, Expression? expression switch (member) { case PropertyInfo property: - var propertyIsStatic = property?.GetGetMethod().IsStatic ?? property?.GetSetMethod().IsStatic ?? false; + var propertyIsStatic = property.GetGetMethod()?.IsStatic ?? property.GetSetMethod()?.IsStatic ?? false; propertyOrFieldExpression = propertyIsStatic ? Expression.Property(null, property) : Expression.Property(expression, property); return true; @@ -2545,7 +2545,7 @@ private static Exception IncompatibleOperandsError(string opName, Expression lef var bindingFlags = BindingFlags.Public | BindingFlags.DeclaredOnly | extraBindingFlag; foreach (Type t in TypeHelper.GetSelfAndBaseTypes(type)) { - var findMembersType = _parsingConfig?.IsCaseSensitive == true ? Type.FilterName : Type.FilterNameIgnoreCase; + var findMembersType = _parsingConfig.IsCaseSensitive ? Type.FilterName : Type.FilterNameIgnoreCase; var members = t.FindMembers(MemberTypes.Property | MemberTypes.Field, bindingFlags, findMembersType, memberName); if (members.Length != 0) @@ -2555,7 +2555,7 @@ private static Exception IncompatibleOperandsError(string opName, Expression lef } return null; #else - var isCaseSensitive = _parsingConfig.IsCaseSensitive == true; + var isCaseSensitive = _parsingConfig.IsCaseSensitive; foreach (Type t in TypeHelper.GetSelfAndBaseTypes(type)) { // Try to find a property with the specified memberName diff --git a/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs b/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs index 30877e29..17214eda 100644 --- a/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs +++ b/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs @@ -10,7 +10,7 @@ internal class MethodFinder { private readonly ParsingConfig _parsingConfig; private readonly IExpressionHelper _expressionHelper; - private readonly IDictionary _cachedMethods; + private readonly Dictionary _cachedMethods; /// /// #794 From 50133dd85ed11fcd4ea198b9219bd71a9fe981d0 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 11:29:51 +0100 Subject: [PATCH 12/21] Fix unhandled exceptions from malformed expression strings (Issue #973) (#974) * Initial plan * Fix 5 unhandled exceptions from malformed expression strings (Issue #973) Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/f8cf5eb5-b143-4a52-b59a-624b4cf8a47f Co-authored-by: StefH <249938+StefH@users.noreply.github.com> * Address code review: fix generic type accessibility check and use HashSet for duplicate detection Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/f8cf5eb5-b143-4a52-b59a-624b4cf8a47f Co-authored-by: StefH <249938+StefH@users.noreply.github.com> * Remove accidentally committed .nuget/nuget.exe binary Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/f8cf5eb5-b143-4a52-b59a-624b4cf8a47f Co-authored-by: StefH <249938+StefH@users.noreply.github.com> * Change IsTypePubliclyAccessible parameter from Type to TypeInfo Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/675543ad-d73e-4782-91c3-a63a7a0bab58 Co-authored-by: StefH <249938+StefH@users.noreply.github.com> * fix --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: StefH <249938+StefH@users.noreply.github.com> Co-authored-by: Stef Heyenrath --- .gitignore | 1 + .../DynamicClassFactory.cs | 42 ++++++- .../Parser/ExpressionHelper.cs | 7 ++ .../Parser/ExpressionParser.cs | 38 +++++- .../Issues/Issue973.cs | 115 ++++++++++++++++++ 5 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 test/System.Linq.Dynamic.Core.Tests/Issues/Issue973.cs diff --git a/.gitignore b/.gitignore index 27896dcc..26cb01a0 100644 --- a/.gitignore +++ b/.gitignore @@ -238,3 +238,4 @@ _Pvt_Extensions /coverage.xml /dynamic-coverage-*.xml /test/**/coverage.net8.0.opencover.xml +.nuget/ diff --git a/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs b/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs index 1b4a1eef..e84d508d 100644 --- a/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs +++ b/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs @@ -289,11 +289,16 @@ private static Type EmitType(IList properties, bool createParam { var fieldName = properties[i].Name; var fieldType = properties[i].Type; - var equalityComparerT = EqualityComparer.MakeGenericType(fieldType); + + // Use object-based equality comparer for types that are not publicly accessible from the dynamic assembly (e.g., compiler-generated anonymous types). + // Calling EqualityComparer.get_Default() for a non-public T will throw MethodAccessException. + var fieldTypeIsAccessible = IsTypePubliclyAccessible(fieldType); + var equalityType = fieldTypeIsAccessible ? fieldType : typeof(object); + var equalityComparerT = EqualityComparer.MakeGenericType(equalityType); // Equals() MethodInfo equalityComparerTDefault = equalityComparerT.GetMethod("get_Default", BindingFlags.Static | BindingFlags.Public)!; - MethodInfo equalityComparerTEquals = equalityComparerT.GetMethod(nameof(EqualityComparer.Equals), BindingFlags.Instance | BindingFlags.Public, null, [fieldType, fieldType], null)!; + MethodInfo equalityComparerTEquals = equalityComparerT.GetMethod(nameof(EqualityComparer.Equals), BindingFlags.Instance | BindingFlags.Public, null, [equalityType, equalityType], null)!; // Illegal one-byte branch at position: 9. Requested branch was: 143. // So replace OpCodes.Brfalse_S to OpCodes.Brfalse @@ -301,12 +306,14 @@ private static Type EmitType(IList properties, bool createParam ilgeneratorEquals.Emit(OpCodes.Call, equalityComparerTDefault); ilgeneratorEquals.Emit(OpCodes.Ldarg_0); ilgeneratorEquals.Emit(OpCodes.Ldfld, fieldBuilders[i]); + if (!fieldTypeIsAccessible) ilgeneratorEquals.Emit(OpCodes.Box, fieldType); ilgeneratorEquals.Emit(OpCodes.Ldloc_0); ilgeneratorEquals.Emit(OpCodes.Ldfld, fieldBuilders[i]); + if (!fieldTypeIsAccessible) ilgeneratorEquals.Emit(OpCodes.Box, fieldType); ilgeneratorEquals.Emit(OpCodes.Callvirt, equalityComparerTEquals); // GetHashCode(); - MethodInfo equalityComparerTGetHashCode = equalityComparerT.GetMethod(nameof(EqualityComparer.GetHashCode), BindingFlags.Instance | BindingFlags.Public, null, [fieldType], null)!; + MethodInfo equalityComparerTGetHashCode = equalityComparerT.GetMethod(nameof(EqualityComparer.GetHashCode), BindingFlags.Instance | BindingFlags.Public, null, [equalityType], null)!; ilgeneratorGetHashCode.Emit(OpCodes.Stloc_0); ilgeneratorGetHashCode.Emit(OpCodes.Ldc_I4, -1521134295); ilgeneratorGetHashCode.Emit(OpCodes.Ldloc_0); @@ -314,6 +321,7 @@ private static Type EmitType(IList properties, bool createParam ilgeneratorGetHashCode.Emit(OpCodes.Call, equalityComparerTDefault); ilgeneratorGetHashCode.Emit(OpCodes.Ldarg_0); ilgeneratorGetHashCode.Emit(OpCodes.Ldfld, fieldBuilders[i]); + if (!fieldTypeIsAccessible) ilgeneratorGetHashCode.Emit(OpCodes.Box, fieldType); ilgeneratorGetHashCode.Emit(OpCodes.Callvirt, equalityComparerTGetHashCode); ilgeneratorGetHashCode.Emit(OpCodes.Add); @@ -419,7 +427,33 @@ private static Type EmitType(IList properties, bool createParam EmitEqualityOperators(typeBuilder, equals); - return typeBuilder.CreateType(); + return typeBuilder.CreateType()!; + } + + /// + /// Determines whether a type is publicly accessible from an external (dynamic) assembly. + /// Non-public types (e.g., compiler-generated anonymous types) cannot be used as generic + /// type arguments in EqualityComparer<T> from a dynamic assembly without causing + /// a at runtime. + /// + private static bool IsTypePubliclyAccessible(Type typeInfo) + { + // Check if the type itself is public + if (!typeInfo.GetTypeInfo().IsPublic && !typeInfo.GetTypeInfo().IsNestedPublic) + { + return false; + } + + // For constructed generic types (e.g., List), + // all type arguments must also be publicly accessible. + // Generic type definitions (e.g., List<>) have unbound type parameters + // which are not concrete types and should not be checked. + if (typeInfo.GetTypeInfo().IsGenericType && !typeInfo.GetTypeInfo().IsGenericTypeDefinition) + { + return typeInfo.GetGenericArguments().All(IsTypePubliclyAccessible); + } + + return true; } private static void EmitEqualityOperators(TypeBuilder typeBuilder, MethodBuilder equals) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs index a9b31876..2b830a1e 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs @@ -344,6 +344,13 @@ public bool TryGenerateAndAlsoNotNullExpression(Expression sourceExpression, boo .Select(expression => Expression.NotEqual(expression, _nullExpression)) .ToArray(); + // If no nullable expressions were found, return false (nothing to null-check) + if (binaryExpressions.Length == 0) + { + generatedExpression = sourceExpression; + return false; + } + // Convert all binary expressions into `AndAlso(...)` generatedExpression = binaryExpressions[0]; for (int i = 1; i < binaryExpressions.Length; i++) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 20524d36..f9179323 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -928,7 +928,7 @@ private AnyOf ParseStringLiteral(bool forceParseAsString) if (_textParser.CurrentToken.Text[0] == '\'') { - if (parsedStringValue.Length > 1) + if (parsedStringValue.Length != 1) { throw ParseError(Res.InvalidCharacterLiteral); } @@ -1458,12 +1458,18 @@ private Expression ParseNew() arrayInitializer = true; } + // Track the opening token to enforce matching close token + var openTokenId = _textParser.CurrentToken.Id; _textParser.NextToken(); + // Determine the expected closing token based on the opening token + var closeTokenId = openTokenId == TokenId.OpenParen ? TokenId.CloseParen : TokenId.CloseCurlyParen; + var properties = new List(); var expressions = new List(); + var propertyNames = new HashSet(StringComparer.Ordinal); - while (_textParser.CurrentToken.Id != TokenId.CloseParen && _textParser.CurrentToken.Id != TokenId.CloseCurlyParen) + while (_textParser.CurrentToken.Id != closeTokenId) { int exprPos = _textParser.CurrentToken.Pos; Expression expr = ParseConditionalOperator(); @@ -1483,7 +1489,7 @@ private Expression ParseNew() && methodCallExpression.Arguments.Count == 1 && methodCallExpression.Arguments[0] is ConstantExpression methodCallExpressionArgument && methodCallExpressionArgument.Type == typeof(string) - && properties.All(x => x.Name != (string?)methodCallExpressionArgument.Value)) + && !propertyNames.Contains((string?)methodCallExpressionArgument.Value ?? string.Empty)) { propName = (string?)methodCallExpressionArgument.Value; } @@ -1496,6 +1502,11 @@ private Expression ParseNew() if (!string.IsNullOrEmpty(propName)) { + if (!propertyNames.Add(propName!)) + { + throw ParseError(exprPos, Res.DuplicateIdentifier, propName); + } + properties.Add(new DynamicProperty(propName!, expr.Type)); } } @@ -1510,7 +1521,7 @@ private Expression ParseNew() _textParser.NextToken(); } - if (_textParser.CurrentToken.Id != TokenId.CloseParen && _textParser.CurrentToken.Id != TokenId.CloseCurlyParen) + if (_textParser.CurrentToken.Id != closeTokenId) { throw ParseError(Res.CloseParenOrCommaExpected); } @@ -2393,7 +2404,24 @@ private Expression ParseElementAccess(Expression expr) : args[i]; } - return Expression.Call(expr, indexMethod, indexArgumentExpressions); + var callExpr = Expression.Call(expr, indexMethod, indexArgumentExpressions); + + // For constant expressions with constant arguments, evaluate at parse time to + // produce a ParseException instead of a runtime exception (e.g., index out of bounds). + if (expr is ConstantExpression && args.All(a => a is ConstantExpression)) + { + try + { + var value = Expression.Lambda(callExpr).Compile().DynamicInvoke(); + return Expression.Constant(value, callExpr.Type); + } + catch (Exception ex) + { + throw ParseError(errorPos, (ex.InnerException ?? ex).Message); + } + } + + return callExpr; default: throw ParseError(errorPos, Res.AmbiguousIndexerInvocation, TypeHelper.GetTypeName(expr.Type)); diff --git a/test/System.Linq.Dynamic.Core.Tests/Issues/Issue973.cs b/test/System.Linq.Dynamic.Core.Tests/Issues/Issue973.cs new file mode 100644 index 00000000..87721100 --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests/Issues/Issue973.cs @@ -0,0 +1,115 @@ +using System.Linq.Dynamic.Core.Exceptions; +using FluentAssertions; +using Xunit; + +namespace System.Linq.Dynamic.Core.Tests; + +/// +/// Tests for Issue #973: Multiple unhandled exceptions from malformed expression strings (5 crash sites found via fuzzing). +/// All 5 bugs should throw instead of unhandled runtime exceptions. +/// +public partial class QueryableTests +{ + + /// + /// Bug 1: ParseStringLiteral IndexOutOfRangeException for empty single-quoted string literal ''. + /// + [Fact] + public void Issue973_Bug1_EmptyCharLiteral_ShouldThrowParseException() + { + // Arrange + var items = new[] { new { Id = 1, Name = "Alice" } }.AsQueryable(); + + // Act + Action act = () => items.Where("''"); + + // Assert + act.Should().Throw(); + } + + /// + /// Bug 2: TryGenerateAndAlsoNotNullExpression IndexOutOfRangeException for malformed np() call. + /// + [Fact] + public void Issue973_Bug2_MalformedNpCall_ShouldThrowParseException() + { + // Arrange + var items = new[] { new { Id = 1, Name = "Alice" } }.AsQueryable(); + + // Act + Action act = () => items.Where("-np(--9999999)9999T--99999999999)99999"); + + // Assert + act.Should().Throw(); + } + + /// + /// Bug 3: Compiled expression IndexOutOfRangeException for string indexer with out-of-bounds constant index. + /// + [Fact] + public void Issue973_Bug3_StringIndexerOutOfBounds_ShouldThrowParseException() + { + // Arrange + var items = new[] { new { Id = 1, Name = "Alice" } }.AsQueryable(); + + // Act + Action act = () => items.OrderBy("\"ab\"[3]").ToList(); + + // Assert + act.Should().Throw(); + } + + /// + /// Bug 3 (valid case): String indexer with in-bounds index should work correctly. + /// + [Fact] + public void Issue973_Bug3_StringIndexerInBounds_ShouldWork() + { + // Arrange + var items = new[] { new { Id = 1, Name = "Alice" } }.AsQueryable(); + + // Act - "ab"[0] = 'a', "ab"[1] = 'b' are valid + var result0 = items.OrderBy("\"ab\"[0]").ToList(); + var result1 = items.OrderBy("\"ab\"[1]").ToList(); + + // Assert + result0.Should().HaveCount(1); + result1.Should().HaveCount(1); + } + + /// + /// Bug 4: NullReferenceException in compiled Select projection with mismatched brace/paren in new(). + /// + [Fact] + public void Issue973_Bug4_MismatchedBraceInNewExpression_ShouldThrowParseException() + { + // Arrange + var items = new[] { new { Id = 1, Name = "Alice", Age = 30, Score = 85.5, Active = true } }.AsQueryable(); + + // Act + Action act = () => items.Select("new(Name }. AgAkQ & 1111555555+55555-5555555+555555+55555-55555").ToDynamicList(); + + // Assert + act.Should().Throw(); + } + + /// + /// Bug 5: MethodAccessException from $ identifier in GroupBy due to duplicate property names. + /// + [Fact] + public void Issue973_Bug5_DollarIdentifierWithDuplicatePropertyNames_ShouldThrowParseException() + { + // Arrange + var items = new[] + { + new { Id = 1, Name = "Alice", Age = 30, Score = 85.5, Active = true }, + new { Id = 2, Name = "Bob", Age = 25, Score = 92.0, Active = false }, + }.AsQueryable(); + + // Act + Action act = () => items.GroupBy("new($ as DoubleAge, Age + 2 as DoubleAge)").ToDynamicList(); + + // Assert - duplicate property name 'DoubleAge' should cause a ParseException + act.Should().Throw(); + } +} From 4ff9105933ca4934967990fb84877f2ab1168da4 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:01:16 +0100 Subject: [PATCH 13/21] Fix relational operators failing for nullable IComparable types (e.g., Instant?) (#975) * Initial plan * Fix nullable IComparable types not working with relational operators (>, >=, <, <=) When comparing two values of the same nullable type (e.g., Instant?) using relational operators, the check for IComparable<> interface was done on the nullable type itself (Nullable), which doesn't directly implement IComparable<>. The fix uses TypeHelper.GetNonNullableType() to get the underlying type first, so that if T implements IComparable, nullable T? also works correctly with relational operators. Fixes: Operator '>' incompatible with operand types 'Instant?' and 'Instant?' Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/b497145d-ba3d-430a-b608-eda596efffdd Co-authored-by: StefH <249938+StefH@users.noreply.github.com> * Improve test assertions: verify expected result counts for Instant comparisons Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/b497145d-ba3d-430a-b608-eda596efffdd Co-authored-by: StefH <249938+StefH@users.noreply.github.com> * Add == and != test cases for Instant and Instant? comparison tests Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/4e543367-6cb4-4164-bd33-fbf0fecca256 Co-authored-by: StefH <249938+StefH@users.noreply.github.com> * LocalDateConverter --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: StefH <249938+StefH@users.noreply.github.com> Co-authored-by: Stef Heyenrath --- .../Parser/ExpressionParser.cs | 3 +- .../TypeConvertors/NodaTimeConverterTests.cs | 81 ++++++++++++++++--- 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index f9179323..71828940 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -607,7 +607,8 @@ private Expression ParseComparisonOperator() bool typesAreSameAndImplementCorrectInterface = false; if (left.Type == right.Type) { - var interfaces = left.Type.GetInterfaces().Where(x => x.GetTypeInfo().IsGenericType); + var typeToCheck = TypeHelper.GetNonNullableType(left.Type); + var interfaces = typeToCheck.GetInterfaces().Where(x => x.GetTypeInfo().IsGenericType); if (isEquality) { typesAreSameAndImplementCorrectInterface = interfaces.Any(x => x.GetGenericTypeDefinition() == typeof(IEquatable<>)); diff --git a/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs b/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs index fdfdaa0f..4cff6311 100644 --- a/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/TypeConvertors/NodaTimeConverterTests.cs @@ -1,10 +1,7 @@ #if !NET452 using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq.Dynamic.Core.CustomTypeProviders; -using System.Reflection; using FluentAssertions; using NodaTime; using NodaTime.Text; @@ -158,20 +155,82 @@ public void FilterByNullableLocalDate_WithDynamicExpressionParser_CompareWithNul result.Should().HaveCount(numberOfEntities); } - public class LocalDateConverter : TypeConverter + private class EntityWithInstant + { + public Instant Timestamp { get; set; } + public Instant? TimestampNullable { get; set; } + } + + [Theory] + [InlineData(">", 1)] + [InlineData(">=", 2)] + [InlineData("<", 1)] + [InlineData("<=", 2)] + [InlineData("==", 1)] + [InlineData("!=", 2)] + public void FilterByInstant_WithRelationalOperator(string op, int expectedCount) { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) => sourceType == typeof(string); + // Arrange + var now = SystemClock.Instance.GetCurrentInstant(); + var data = new List + { + new EntityWithInstant { Timestamp = now - Duration.FromHours(1) }, + new EntityWithInstant { Timestamp = now }, + new EntityWithInstant { Timestamp = now + Duration.FromHours(1) } + }.AsQueryable(); - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + // Act + var result = data.Where($"Timestamp {op} @0", now).ToList(); + + // Assert + result.Should().HaveCount(expectedCount); + } + + [Theory] + [InlineData(">", 1)] + [InlineData(">=", 2)] + [InlineData("<", 1)] + [InlineData("<=", 2)] + [InlineData("==", 1)] + [InlineData("!=", 3)] // null != now evaluates to true in C# nullable semantics + public void FilterByNullableInstant_WithRelationalOperator(string op, int expectedCount) + { + // Arrange + var now = SystemClock.Instance.GetCurrentInstant(); + var data = new List { - var result = LocalDatePattern.Iso.Parse(value as string); + new EntityWithInstant { TimestampNullable = now - Duration.FromHours(1) }, + new EntityWithInstant { TimestampNullable = now }, + new EntityWithInstant { TimestampNullable = now + Duration.FromHours(1) }, + new EntityWithInstant { TimestampNullable = null } + }.AsQueryable(); + + // Act + var result = data.Where($"TimestampNullable {op} @0", now).ToList(); - return result.Success - ? result.Value - : throw new FormatException(value?.ToString()); + // Assert - null values are excluded from comparison results + result.Should().HaveCount(expectedCount); + } + + public class LocalDateConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string); + + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + var result = Convert(value); + return result.Success ? result.Value : throw new FormatException(value?.ToString()); } - protected ParseResult Convert(object value) => LocalDatePattern.Iso.Parse(value as string); + private static ParseResult Convert(object value) + { + if (value is string stringValue) + { + return LocalDatePattern.Iso.Parse(stringValue); + } + + return ParseResult.ForException(() => new FormatException(value?.ToString())); + } } } } From b734bfb76984b9e83d5ca12da80143bb95b6c7ae Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:45:12 +0100 Subject: [PATCH 14/21] Fix enum type preservation in additive arithmetic operations (#976) * Initial plan * Fix: enum + int and int + enum now return enum type (C# semantics); enum - int returns enum type Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/0cb2e89e-56b5-4db3-8850-f5013136f847 Co-authored-by: StefH <249938+StefH@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: StefH <249938+StefH@users.noreply.github.com> --- .../Parser/ExpressionParser.cs | 24 +++++++++++++-- .../QueryableTests.Select.cs | 30 +++++++++++++++++-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 71828940..0ae3e61d 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -766,14 +766,34 @@ private Expression ParseAdditive() } else { + var leftTypeForAdd = left.Type; + var rightTypeForAdd = right.Type; CheckAndPromoteOperands(typeof(IAddSignatures), op.Id, op.Text, ref left, ref right, op.Pos); left = _expressionHelper.GenerateAdd(left, right); + // C# semantics: enum + int = enum, int + enum = enum + if (TypeHelper.IsEnumType(leftTypeForAdd) && TypeHelper.IsNumericType(rightTypeForAdd)) + { + left = Expression.Convert(left, leftTypeForAdd); + } + else if (TypeHelper.IsNumericType(leftTypeForAdd) && TypeHelper.IsEnumType(rightTypeForAdd)) + { + left = Expression.Convert(left, rightTypeForAdd); + } } break; case TokenId.Minus: - CheckAndPromoteOperands(typeof(ISubtractSignatures), op.Id, op.Text, ref left, ref right, op.Pos); - left = _expressionHelper.GenerateSubtract(left, right); + { + var leftTypeForSubtract = left.Type; + var rightTypeForSubtract = right.Type; + CheckAndPromoteOperands(typeof(ISubtractSignatures), op.Id, op.Text, ref left, ref right, op.Pos); + left = _expressionHelper.GenerateSubtract(left, right); + // C# semantics: enum - int = enum (but enum - enum = underlying int type) + if (TypeHelper.IsEnumType(leftTypeForSubtract) && TypeHelper.IsNumericType(rightTypeForSubtract)) + { + left = Expression.Convert(left, leftTypeForSubtract); + } + } break; } } diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs index ddd32a83..2055492d 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs @@ -224,8 +224,34 @@ public void Select_Dynamic_Add_Integer_And_DayOfWeekEnum() // Act var rangeResult = range.AsQueryable().Select("it + DayOfWeek.Monday"); - // Assert - Assert.Equal(range.Select(x => x + DayOfWeek.Monday).Cast().ToArray(), rangeResult.Cast().ToArray()); + // Assert - C# semantics: int + enum = enum + Assert.Equal(range.Select(x => x + DayOfWeek.Monday).ToArray(), rangeResult.Cast().ToArray()); + } + + [Fact] + public void Select_Dynamic_Subtract_DayOfWeekEnum_And_Integer() + { + // Arrange + var range = new DayOfWeek[] { DayOfWeek.Tuesday }; + + // Act + var rangeResult = range.AsQueryable().Select("it - 1"); + + // Assert - C# semantics: enum - int = enum + Assert.Equal(range.Select(x => x - 1).ToArray(), rangeResult.Cast().ToArray()); + } + + [Fact] + public void Select_Dynamic_Subtract_DayOfWeekEnum_And_DayOfWeekEnum() + { + // Arrange + var range = new DayOfWeek[] { DayOfWeek.Tuesday }; + + // Act + var rangeResult = range.AsQueryable().Select("it - DayOfWeek.Monday"); + + // Assert - C# semantics: enum - enum = underlying int type + Assert.Equal(range.Select(x => x - DayOfWeek.Monday).ToArray(), rangeResult.Cast().ToArray()); } [Fact] From b9396263a7b70fe1fd328b5d11be886ca6f12035 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 09:01:28 +0200 Subject: [PATCH 15/21] Support implicit operators in method argument matching (#977) * Initial plan * Add implicit operator support in ExpressionPromoter and tests Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/82ed2f4e-0c76-407c-a330-d4043e96162f Co-authored-by: StefH <249938+StefH@users.noreply.github.com> * Remove coverage file from tracking, add to gitignore Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/82ed2f4e-0c76-407c-a330-d4043e96162f Co-authored-by: StefH <249938+StefH@users.noreply.github.com> * Optimize implicit operator lookup in ExpressionPromoter Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/82ed2f4e-0c76-407c-a330-d4043e96162f Co-authored-by: StefH <249938+StefH@users.noreply.github.com> * Refactor: move implicit operator lookup to TypeHelper.TryFindImplicitConversionOperator Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/3d221836-ae46-4e82-b79f-a67b544ee3af Co-authored-by: StefH <249938+StefH@users.noreply.github.com> * refactor code --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: StefH <249938+StefH@users.noreply.github.com> Co-authored-by: Stef Heyenrath --- .gitignore | 1 + .../Parser/ExpressionParser.cs | 13 +---- .../Parser/ExpressionPromoter.cs | 5 ++ .../Parser/TypeHelper.cs | 27 ++++++++++ .../DynamicExpressionParserTests.cs | 49 +++++++++++++++++++ 5 files changed, 83 insertions(+), 12 deletions(-) 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() { From e33dc6c16338e9538c27116d4a8a89868dc75836 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:20:01 +0200 Subject: [PATCH 16/21] Fix NotSupportedException when parsing nested object initialization (#979) * Initial plan * Fix error when parsing nested object initialization (issue #701) Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/4683789c-0227-43d7-9432-4863adea1d4c Co-authored-by: StefH <249938+StefH@users.noreply.github.com> * Refactor TryRebuildMemberInitExpression to use out parameter pattern Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/537f61ea-9dbe-4ae4-9fa8-0358f826f195 Co-authored-by: StefH <249938+StefH@users.noreply.github.com> * Add three-level-deep nested new test to exercise recursive TryRebuildMemberInitExpression Agent-Logs-Url: https://github.com/zzzprojects/System.Linq.Dynamic.Core/sessions/42b1ec4e-90f4-484b-ae3a-73f3bee9c50b Co-authored-by: StefH <249938+StefH@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: StefH <249938+StefH@users.noreply.github.com> --- .../Parser/ExpressionParser.cs | 62 ++++++++++++++++++- .../DynamicExpressionParserTests.cs | 62 +++++++++++++++++++ 2 files changed, 121 insertions(+), 3 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index dcf6a329..4a6319aa 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -1678,14 +1678,70 @@ private Expression CreateNewExpression(List properties, List(new ParsingConfig(), false, "new[]{1,2,3}.Any(z => z > 0)"); } + // https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/701 + [Fact] + public void DynamicExpressionParser_ParseLambda_NestedObjectInitialization() + { + // Arrange + var srcType = typeof(CustomerForNestedNewTest); + + // Act + var lambda = DynamicExpressionParser.ParseLambda(ParsingConfig.DefaultEFCore21, srcType, srcType, "new (new (3 as Id) as CurrentDepartment)"); + var @delegate = lambda.Compile(); + var result = (CustomerForNestedNewTest)@delegate.DynamicInvoke(new CustomerForNestedNewTest())!; + + // Assert + result.Should().NotBeNull(); + result.CurrentDepartment.Should().NotBeNull(); + result.CurrentDepartment!.Id.Should().Be(3); + } + + // https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/701 + [Fact] + public void DynamicExpressionParser_ParseLambda_NestedObjectInitialization_ThreeLevelsDeep() + { + // Arrange — exercises the recursive TryRebuildMemberInitExpression path. + // The parser propagates _resultType (CustomerForNestedNewTest) into all nested new + // expressions. The middle "new (new (3 as Id) as Sub)" therefore builds a + // MIE{ Sub = MIE{Id=3} }. When the outer new binds that to its own + // "Sub" property (type DepartmentForNestedNewTest), TryRebuildMemberInitExpression is + // called and encounters the inner MIE{Id=3} binding — a MemberInitExpression + // itself — which triggers the recursive call to rebuild it for SubDepartmentForNestedNewTest. + var srcType = typeof(CustomerForNestedNewTest); + + // Act + var lambda = DynamicExpressionParser.ParseLambda(ParsingConfig.DefaultEFCore21, srcType, srcType, + "new (new (new (3 as Id) as Sub) as Sub)"); + var @delegate = lambda.Compile(); + var result = (CustomerForNestedNewTest)@delegate.DynamicInvoke(new CustomerForNestedNewTest())!; + + // Assert + result.Should().NotBeNull(); + result.Sub.Should().NotBeNull(); + result.Sub!.Sub.Should().NotBeNull(); + result.Sub.Sub!.Id.Should().Be(3); + } + + public class CustomerForNestedNewTest + { + public int Id { get; set; } + public DepartmentForNestedNewTest? CurrentDepartment { get; set; } + public DepartmentForNestedNewTest? Sub { get; set; } + } + + public class DepartmentForNestedNewTest + { + public int Id { get; set; } + public SubDepartmentForNestedNewTest? Sub { get; set; } + } + + public class SubDepartmentForNestedNewTest + { + public int Id { get; set; } + } + public class DefaultDynamicLinqCustomTypeProviderForGenericExtensionMethod : DefaultDynamicLinqCustomTypeProvider { public DefaultDynamicLinqCustomTypeProviderForGenericExtensionMethod() : base(ParsingConfig.Default) From 99eb4c93626bf2ea767dd3d74b241f8b495ec8ad Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 4 Apr 2026 11:44:22 +0200 Subject: [PATCH 17/21] v1.7.2 --- CHANGELOG.md | 15 ++++++++++++++- Generate-ReleaseNotes.bat | 2 +- version.xml | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a4ae9bc..41364989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# v1.7.2 (04 April 2026) +- [#971](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/971) - Fix some sonarcloud issues [refactor] contributed by [StefH](https://github.com/StefH) +- [#974](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/974) - Fix unhandled exceptions from malformed expression strings [bug] contributed by [Copilot](https://github.com/apps/copilot-swe-agent) +- [#975](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/975) - Fix relational operators failing for nullable IComparable types (e.g., Instant?) [bug] contributed by [Copilot](https://github.com/apps/copilot-swe-agent) +- [#976](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/976) - Fix enum type preservation in additive arithmetic operations [bug] contributed by [Copilot](https://github.com/apps/copilot-swe-agent) +- [#977](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/977) - Support implicit operators in method argument matching [feature] contributed by [Copilot](https://github.com/apps/copilot-swe-agent) +- [#979](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/979) - Fix NotSupportedException when parsing nested object initialization [bug] contributed by [Copilot](https://github.com/apps/copilot-swe-agent) +- [#813](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/813) - Error when parsing a nested object initialization [bug] +- [#880](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/880) - Support for implicit operators [feature] +- [#969](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/969) - Unexpected type change when parsing addition of integer to enum [bug] +- [#970](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/970) - Operator '>' incompatible with operand types 'Instant?' and 'Instant?' [feature] +- [#973](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/973) - Multiple unhandled exceptions from malformed expression strings (5 crash sites found via fuzzing) [bug] + # v1.7.1 (29 November 2025) - [#961](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/961) - Fix Json when property value is null [bug] contributed by [StefH](https://github.com/StefH) - [#962](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/962) - json: fix logic when property is not found [bug] contributed by [StefH](https://github.com/StefH) @@ -30,7 +43,7 @@ - [#938](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/938) - Use TryConvertTypes also for strings [bug] contributed by [StefH](https://github.com/StefH) # v1.6.6 (11 June 2025) -- [#929](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/929) - Add GroupBy method for Z.DynamicLinq.SystemTextJson and Z.DynamicLinq.NewtonsoftJson contributed by [StefH](https://github.com/StefH) +- [#929](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/929) - Add GroupBy method for Z.DynamicLinq.SystemTextJson and Z.DynamicLinq.NewtonsoftJson [feature] contributed by [StefH](https://github.com/StefH) - [#932](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/932) - Fix "in" for nullable Enums [bug] contributed by [StefH](https://github.com/StefH) - [#931](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/931) - Syntax IN dont work with nullable Enums [bug] diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index 809d22d8..7377bb34 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.7.1 +SET version=v1.7.2 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 d0d14392..257b812f 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 1 + 2 \ No newline at end of file From e22a370ed69193d37fa1d217cd3430334e8e86d3 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Thu, 9 Apr 2026 20:39:38 +0200 Subject: [PATCH 18/21] Fix MethodFinder.FirstIsBetterThanSecond (#981) --- .../DynamicClassFactory.cs | 24 +++++-- .../Parser/SupportedMethods/MethodFinder.cs | 3 +- .../Parser/ExpressionParserTests.cs | 65 ++++++++++++++++++- 3 files changed, 84 insertions(+), 8 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs b/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs index e84d508d..6c70dcbe 100644 --- a/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs +++ b/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs @@ -296,7 +296,7 @@ private static Type EmitType(IList properties, bool createParam var equalityType = fieldTypeIsAccessible ? fieldType : typeof(object); var equalityComparerT = EqualityComparer.MakeGenericType(equalityType); - // Equals() + // Implement Equals(); MethodInfo equalityComparerTDefault = equalityComparerT.GetMethod("get_Default", BindingFlags.Static | BindingFlags.Public)!; MethodInfo equalityComparerTEquals = equalityComparerT.GetMethod(nameof(EqualityComparer.Equals), BindingFlags.Instance | BindingFlags.Public, null, [equalityType, equalityType], null)!; @@ -306,13 +306,21 @@ private static Type EmitType(IList properties, bool createParam ilgeneratorEquals.Emit(OpCodes.Call, equalityComparerTDefault); ilgeneratorEquals.Emit(OpCodes.Ldarg_0); ilgeneratorEquals.Emit(OpCodes.Ldfld, fieldBuilders[i]); - if (!fieldTypeIsAccessible) ilgeneratorEquals.Emit(OpCodes.Box, fieldType); + if (!fieldTypeIsAccessible) + { + ilgeneratorEquals.Emit(OpCodes.Box, fieldType); + } + ilgeneratorEquals.Emit(OpCodes.Ldloc_0); ilgeneratorEquals.Emit(OpCodes.Ldfld, fieldBuilders[i]); - if (!fieldTypeIsAccessible) ilgeneratorEquals.Emit(OpCodes.Box, fieldType); + if (!fieldTypeIsAccessible) + { + ilgeneratorEquals.Emit(OpCodes.Box, fieldType); + } + ilgeneratorEquals.Emit(OpCodes.Callvirt, equalityComparerTEquals); - // GetHashCode(); + // Implement GetHashCode(); MethodInfo equalityComparerTGetHashCode = equalityComparerT.GetMethod(nameof(EqualityComparer.GetHashCode), BindingFlags.Instance | BindingFlags.Public, null, [equalityType], null)!; ilgeneratorGetHashCode.Emit(OpCodes.Stloc_0); ilgeneratorGetHashCode.Emit(OpCodes.Ldc_I4, -1521134295); @@ -321,11 +329,15 @@ private static Type EmitType(IList properties, bool createParam ilgeneratorGetHashCode.Emit(OpCodes.Call, equalityComparerTDefault); ilgeneratorGetHashCode.Emit(OpCodes.Ldarg_0); ilgeneratorGetHashCode.Emit(OpCodes.Ldfld, fieldBuilders[i]); - if (!fieldTypeIsAccessible) ilgeneratorGetHashCode.Emit(OpCodes.Box, fieldType); + if (!fieldTypeIsAccessible) + { + ilgeneratorGetHashCode.Emit(OpCodes.Box, fieldType); + } + ilgeneratorGetHashCode.Emit(OpCodes.Callvirt, equalityComparerTGetHashCode); ilgeneratorGetHashCode.Emit(OpCodes.Add); - // ToString(); + // Implement ToString(); ilgeneratorToString.Emit(OpCodes.Ldloc_0); ilgeneratorToString.Emit(OpCodes.Ldstr, i == 0 ? $"{{ {fieldName} = " : $", {fieldName} = "); ilgeneratorToString.Emit(OpCodes.Callvirt, StringBuilderAppendString); diff --git a/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs b/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs index 17214eda..41ee4aa8 100644 --- a/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs +++ b/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs @@ -342,7 +342,8 @@ private static bool FirstIsBetterThanSecond(Expression[] args, MethodData first, } var better = false; - for (var i = 0; i < args.Length; i++) + var maxLength = Math.Min(first.Parameters.Length, second.Parameters.Length); + for (var i = 0; i < maxLength; i++) { var result = CompareConversions(args[i].Type, first.Parameters[i].ParameterType, second.Parameters[i].ParameterType); diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs index f6696720..8ab3ff09 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs @@ -390,7 +390,7 @@ public void Parse_When_PrioritizePropertyOrFieldOverTheType_IsTrue(string expres CustomTypeProvider = _dynamicTypeProviderMock.Object, AllowEqualsAndToStringMethodsOnObject = true }; - ParameterExpression[] parameters = [ ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "company") ]; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "company")]; var sut = new ExpressionParser(parameters, expression, null, config); // Act @@ -462,6 +462,69 @@ public void Parse_StringConcat(string expression, string result) parsedExpression.Should().Be(result); } + [Fact] + public void Parse_StringConcat3Strings() + { + // Arrange + var parameters = new[] + { + Expression.Parameter(typeof(string), "x"), + Expression.Parameter(typeof(string), "y") + }; + + var parser = new ExpressionParser( + parameters, + "string.Concat(x, \" - \", y)", + values: null, + parsingConfig: null); + + // Act + var expression = parser.Parse(typeof(string)); + + // Assert + expression.ToString().Should().Be("Concat(x, \" - \", y)"); + + // Compile and invoke + var lambda = Expression.Lambda>(expression, parameters); + var compiled = lambda.Compile(); + var result = compiled("hello", "world"); + + // Assert + result.Should().Be("hello - world"); + } + + [Fact] + public void Parse_StringConcat4Strings() + { + // Arrange + var parameters = new[] + { + Expression.Parameter(typeof(string), "x"), + Expression.Parameter(typeof(string), "y"), + Expression.Parameter(typeof(string), "z") + }; + + var parser = new ExpressionParser( + parameters, + "string.Concat(x, \" - \", y, z)", + values: null, + parsingConfig: null); + + // Act + var expression = parser.Parse(typeof(string)); + + // Assert + expression.ToString().Should().Be("Concat(x, \" - \", y, z)"); + + // Compile and invoke + var lambda = Expression.Lambda>(expression, parameters); + var compiled = lambda.Compile(); + var result = compiled("hello", "earth", "moon"); + + // Assert + result.Should().Be("hello - earthmoon"); + } + [Fact] public void Parse_InvalidExpressionShouldThrowArgumentException() { From 16d0eb52f44ccac992ccbcc03ed8cfdec98035ef Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sun, 28 Jun 2026 22:47:41 +0200 Subject: [PATCH 19/21] Add some more unit tests for DynamicExpressionParser.ParseLambda (#985) --- .../Parser/KeywordsHelper.cs | 4 +-- .../DynamicExpressionParserTests.cs | 33 +++++++++++++++---- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs b/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs index 1e816384..977aebb5 100644 --- a/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs @@ -29,10 +29,10 @@ internal class KeywordsHelper : IKeywordsHelper private readonly Dictionary> _mappings; // Pre-defined Types are not IgnoreCase - private static readonly Dictionary PreDefinedTypeMapping = new(); + private static readonly Dictionary PreDefinedTypeMapping = []; // Custom DefinedTypes are not IgnoreCase - private readonly Dictionary _customTypeMapping = new(); + private readonly Dictionary _customTypeMapping = []; static KeywordsHelper() { diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs index 4417d1a0..5ad3338d 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -82,7 +82,7 @@ public int this[int i1] get => i1 + "-" + i2; } } - + private class ComplexParseLambda1Result { public int? Age; @@ -730,7 +730,7 @@ public void DynamicExpressionParser_ParseLambda_Indexer1D() // Assert Check.That(result).Equals(4); } - + [Fact] public void DynamicExpressionParser_ParseLambda_Indexer2D() { @@ -757,7 +757,7 @@ public void DynamicExpressionParser_ParseLambda_Indexer2D() // Assert Check.That(result).Equals("3-1"); } - + [Fact] public void DynamicExpressionParser_ParseLambda_IndexerParameterMismatch() { @@ -772,11 +772,32 @@ public void DynamicExpressionParser_ParseLambda_IndexerParameterMismatch() { Expression.Parameter(typeof(ClassWithIndexers), "myObj") }; - + Assert.Throws(() => DynamicExpressionParser.ParseLambda(config, false, expressionParams, null, "myObj[3,\"1\",1]")); } + [Fact] + public void DynamicExpressionParser_ParseLambda_Int16_CaseSensitive_IsValid() + { + // Act + var lambda = DynamicExpressionParser.ParseLambda(typeof(object), null, "Int16(5)"); + var result = lambda.Compile().DynamicInvoke("x"); + + // Assert + result.Should().Be(5); + } + + [Fact] + public void DynamicExpressionParser_ParseLambda_Int16_CaseInsensitive_Throws() + { + // Act + Action act = () => DynamicExpressionParser.ParseLambda(typeof(object), null, "int16(5)"); + + // Assert + act.Should().Throw(); + } + [Fact] public void DynamicExpressionParser_ParseLambda_DuplicateParameterNames_ThrowsException() { @@ -989,7 +1010,7 @@ public void DynamicExpressionParser_ParseLambda_HexToLong(string expression, lon var parameters = Array.Empty(); // Act - var lambda = DynamicExpressionParser.ParseLambda( parameters, typeof(long), expression); + var lambda = DynamicExpressionParser.ParseLambda(parameters, typeof(long), expression); var result = lambda.Compile().DynamicInvoke(); // Assert @@ -2372,6 +2393,6 @@ public DefaultDynamicLinqCustomTypeProviderForGenericExtensionMethod() : base(Pa } public override HashSet GetCustomTypes() => - [..base.GetCustomTypes(), typeof(Methods), typeof(MethodsItemExtension)]; + [.. base.GetCustomTypes(), typeof(Methods), typeof(MethodsItemExtension)]; } } \ No newline at end of file From 807df3e4485d19c53f2355b9501e312e6dd82883 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Tue, 30 Jun 2026 17:23:31 +0200 Subject: [PATCH 20/21] By default, also support WellKnownTypes like StringComparer (#984) * By default, also support WellKnownTypes like StringComparer * . --- ...umerationsAndWellKnownTypesFromMscorlib.cs | 104 ++++++++++++++++++ .../Parser/EnumerationsFromMscorlib.cs | 59 ---------- .../Parser/ExpressionParser.cs | 11 +- .../Parser/KeywordsHelper.cs | 2 +- .../ExpressionTests.cs | 32 ++++++ 5 files changed, 147 insertions(+), 61 deletions(-) create mode 100644 src/System.Linq.Dynamic.Core/Parser/EnumerationsAndWellKnownTypesFromMscorlib.cs delete mode 100644 src/System.Linq.Dynamic.Core/Parser/EnumerationsFromMscorlib.cs diff --git a/src/System.Linq.Dynamic.Core/Parser/EnumerationsAndWellKnownTypesFromMscorlib.cs b/src/System.Linq.Dynamic.Core/Parser/EnumerationsAndWellKnownTypesFromMscorlib.cs new file mode 100644 index 00000000..78f64cb6 --- /dev/null +++ b/src/System.Linq.Dynamic.Core/Parser/EnumerationsAndWellKnownTypesFromMscorlib.cs @@ -0,0 +1,104 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; +using System.Xml; +using System.Xml.Linq; + +namespace System.Linq.Dynamic.Core.Parser; + +internal static class EnumerationsAndWellKnownTypesFromMscorlib +{ + private readonly static string SystemPrivateCoreLib = typeof(StringComparer).GetTypeInfo().Assembly.FullName!; + private readonly static string SystemPrivateUri = typeof(UriFormat).GetTypeInfo().Assembly.FullName!; + private readonly static string SystemPrivateXml = typeof(XmlNodeType).GetTypeInfo().Assembly.FullName!; + private readonly static string SystemPrivateXmlLinq = typeof(XObject).GetTypeInfo().Assembly.FullName!; + + /// + /// Enum types and well-known types. + /// + public static readonly ConcurrentDictionary PredefinedEnumerationTypes = new(StringComparer.OrdinalIgnoreCase); + + static EnumerationsAndWellKnownTypesFromMscorlib() + { + var list = AddEnumsAndWellKnownTypesFromAssembly(SystemPrivateUri); + list.AddRange(AddEnumsAndWellKnownTypesFromAssembly(SystemPrivateCoreLib)); + list.AddRange(AddEnumsAndWellKnownTypesFromAssembly(SystemPrivateXml)); + list.AddRange(AddEnumsAndWellKnownTypesFromAssembly(SystemPrivateXmlLinq)); + +#if !(NET35 || NETSTANDARD1_3) + var systemPrivateDataContractSerialization = typeof(Runtime.Serialization.DataContractResolver).GetTypeInfo().Assembly.FullName!; + list.AddRange(AddEnumsAndWellKnownTypesFromAssembly(systemPrivateDataContractSerialization)); +#endif + foreach (var group in list.GroupBy(t => t.Name)) + { + Add(group); + } + } + + private static List AddEnumsAndWellKnownTypesFromAssembly(string assemblyName) + { + try + { + var assembly = Assembly.Load(new AssemblyName(assemblyName)); + var types = assembly.GetTypes().ToArray(); + + var enumTypes = types.Where(t => t.GetTypeInfo().IsEnum && t.GetTypeInfo().IsPublic); + var enumLikeTypes = FindEnumLikeTypes(types.Where(x => x == typeof(StringComparer)).ToArray()); + + return enumTypes.Union(enumLikeTypes).ToList(); + } + catch + { + return []; + } + } + + private static Type[] FindEnumLikeTypes(Type[] types) + { + try + { + return types + .Where(t => t.GetTypeInfo().IsPublic && !t.GetTypeInfo().IsEnum && HasStaticPropertiesOrFieldsOfOwnType(t)) + .ToArray(); + } + catch + { + return []; + } + } + + private static bool HasStaticPropertiesOrFieldsOfOwnType(Type type) + { + var baseType = type.GetTypeInfo().BaseType; + + var anyStaticProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Static) + .Any(p => p.PropertyType == type || p.PropertyType == baseType); + + if (anyStaticProperties) + { + return true; + } + + var anyStaticFields = type.GetFields(BindingFlags.Public | BindingFlags.Static) + .Any(f => f.FieldType == type || f.FieldType == baseType); + + return anyStaticFields; + } + + private static void Add(IGrouping group) + { + if (group.Count() == 1) + { + var singleType = group.Single(); + PredefinedEnumerationTypes.TryAdd(group.Key, singleType); + PredefinedEnumerationTypes.TryAdd(singleType.FullName!, singleType); + } + else + { + foreach (var fullType in group) + { + PredefinedEnumerationTypes.TryAdd(fullType.FullName!, fullType); + } + } + } +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/EnumerationsFromMscorlib.cs b/src/System.Linq.Dynamic.Core/Parser/EnumerationsFromMscorlib.cs deleted file mode 100644 index 6b2f4b25..00000000 --- a/src/System.Linq.Dynamic.Core/Parser/EnumerationsFromMscorlib.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Reflection; - -namespace System.Linq.Dynamic.Core.Parser -{ - internal static class EnumerationsFromMscorlib - { - /// - /// All Enum types from mscorlib/netstandard. - /// - public static readonly IDictionary PredefinedEnumerationTypes = new ConcurrentDictionary(); - - static EnumerationsFromMscorlib() - { - var list = new List(AddEnumsFromAssembly(typeof(UriFormat).GetTypeInfo().Assembly.FullName!)); - -#if !(UAP10_0 || NETSTANDARD || NET35 || NETCOREAPP) - list.AddRange(AddEnumsFromAssembly("mscorlib")); -#else - list.AddRange(AddEnumsFromAssembly("System.Runtime")); - list.AddRange(AddEnumsFromAssembly("System.Private.Corelib")); -#endif - foreach (var group in list.GroupBy(t => t.Name)) - { - Add(group); - } - } - - private static IEnumerable AddEnumsFromAssembly(string assemblyName) - { - try - { - return Assembly.Load(new AssemblyName(assemblyName)).GetTypes().Where(t => t.GetTypeInfo().IsEnum && t.GetTypeInfo().IsPublic); - } - catch - { - return Enumerable.Empty(); - } - } - - private static void Add(IGrouping group) - { - if (group.Count() == 1) - { - var singleType = group.Single(); - PredefinedEnumerationTypes.Add(group.Key, singleType); - PredefinedEnumerationTypes.Add(singleType.FullName!, singleType); - } - else - { - foreach (var fullType in group) - { - PredefinedEnumerationTypes.Add(fullType.FullName!, fullType); - } - } - } - } -} diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 4a6319aa..d93cfde4 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -2317,7 +2317,16 @@ private bool TryParseEnumerable(Expression instance, Type enumerableType, string { if (new[] { "Concat", "Contains", "ContainsKey", "DefaultIfEmpty", "Except", "Intersect", "Skip", "Take", "Union", "SequenceEqual" }.Contains(methodName)) { - args = [instance, args[0]]; + if (args.Length == 1) + { + args = [instance, args[0]]; + } + else + { + var argsAsList = new List { instance }; + argsAsList.AddRange(args); + args = argsAsList.ToArray(); + } } else { diff --git a/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs b/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs index 977aebb5..9b88ec9c 100644 --- a/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs @@ -118,7 +118,7 @@ public bool TryGetValue(string text, out AnyOf value) } // 4. Try to get as an enum from the system namespace - if (_config.SupportEnumerationsFromSystemNamespace && EnumerationsFromMscorlib.PredefinedEnumerationTypes.TryGetValue(text, out var predefinedEnumType)) + if (_config.SupportEnumerationsFromSystemNamespace && EnumerationsAndWellKnownTypesFromMscorlib.PredefinedEnumerationTypes.TryGetValue(text, out var predefinedEnumType)) { value = predefinedEnumType; return true; diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs index 021183a5..d3d2a1e3 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs @@ -5,6 +5,8 @@ using System.Linq.Dynamic.Core.Exceptions; using System.Linq.Dynamic.Core.Tests.Helpers; using System.Linq.Dynamic.Core.Tests.Helpers.Models; +using System.Net.WebSockets; +using System.Xml; using FluentAssertions; using Moq; using Newtonsoft.Json.Linq; @@ -853,6 +855,36 @@ public void ExpressionTests_Enum() Check.That(resultEqualStringMixedCaseParamRight.Single()).Equals(TestEnum.Var5); } + [Fact] + public void ExpressionTests_Enum_XmlNodeType() + { + // Arrange + var lst = new[] { XmlNodeType.Text, XmlNodeType.Element }; + var qry = lst.AsQueryable(); + + // Act + var result = lst.Count(it => new[] { XmlNodeType.Text }.Contains(it)); + var dynamicResult = qry.Count("new XmlNodeType[] { XmlNodeType.Text }.Contains(it)"); + + // Assert + dynamicResult.Should().Be(result); + } + + [Fact] + public void ExpressionTests_WellKnownTypes_StringComparer() + { + // Arrange + var lst = new[] { "test" }; + var qry = lst.AsQueryable(); + + // Act + var result = lst.Count(it => new string[] { "Test" }.Contains(it, StringComparer.OrdinalIgnoreCase)); + var dynamicResult = qry.Count("new string[] { \"Test\" }.Contains(it, StringComparer.OrdinalIgnoreCase)"); + + // Assert + dynamicResult.Should().Be(result); + } + [Fact] public void ExpressionTests_Enum_Property_Equality_Using_Argument() { From 55b4865296f381133532ef9565496a10252b054b Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 11 Jul 2026 11:50:27 +0200 Subject: [PATCH 21/21] v1.7.3 --- CHANGELOG.md | 7 +++ Generate-ReleaseNotes.bat | 2 +- .../EFDynamicQueryableExtensions.cs | 60 +++++++++---------- ...ueryableWithFormattableStringExtensions.cs | 6 +- .../ExpressionTests.cs | 1 - version.xml | 2 +- 6 files changed, 42 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41364989..aee65783 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# v1.7.3 (11 July 2026) +- [#981](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/981) - Fix MethodFinder.FirstIsBetterThanSecond [bug] contributed by [StefH](https://github.com/StefH) +- [#984](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/984) - By default, also support WellKnownTypes like StringComparer [feature] contributed by [StefH](https://github.com/StefH) +- [#985](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/985) - Add some more unit tests for DynamicExpressionParser.ParseLambda [test] contributed by [StefH](https://github.com/StefH) +- [#980](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/980) - IndexOutOfRangeException in MethodFinder.FirstIsBetterThanSecond after upgrading to v1.7.2 [bug] +- [#983](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/983) - How to IEnumerable<string>.Contains(x, StringComparer.OrdinalIgnoreCase) ? [feature] + # v1.7.2 (04 April 2026) - [#971](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/971) - Fix some sonarcloud issues [refactor] contributed by [StefH](https://github.com/StefH) - [#974](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/974) - Fix unhandled exceptions from malformed expression strings [bug] contributed by [Copilot](https://github.com/apps/copilot-swe-agent) diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index 7377bb34..33407128 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.7.2 +SET version=v1.7.3 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/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/EFDynamicQueryableExtensions.cs b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/EFDynamicQueryableExtensions.cs index 1e3a5d6e..68bd90a0 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/EFDynamicQueryableExtensions.cs +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/EFDynamicQueryableExtensions.cs @@ -52,7 +52,7 @@ public static class EntityFrameworkDynamicQueryableExtensions [PublicAPI] public static Task AllAsync([NotNull] this IQueryable source, [NotNull] string predicate, [CanBeNull] params object[] args) { - return AllAsync(source, predicate, default(CancellationToken), args); + return AllAsync(source, predicate, default, args); } /// @@ -74,7 +74,7 @@ public static Task AllAsync([NotNull] this IQueryable source, [NotNull] st /// A task that represents the asynchronous operation. The task result contains true if every element of the source sequence passes the test in the specified predicate; otherwise, false. /// [PublicAPI] - public static Task AllAsync([NotNull] this IQueryable source, [NotNull] string predicate, CancellationToken cancellationToken = default(CancellationToken), [CanBeNull] params object[] args) + public static Task AllAsync([NotNull] this IQueryable source, [NotNull] string predicate, CancellationToken cancellationToken = default, [CanBeNull] params object[] args) { Check.NotNull(source, nameof(source)); Check.NotEmpty(predicate, nameof(predicate)); @@ -106,7 +106,7 @@ public static Task AllAsync([NotNull] this IQueryable source, [NotNull] st /// A task that represents the asynchronous operation. The task result contains true if the source sequence contains any elements; otherwise, false. /// [PublicAPI] - public static Task AnyAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) + public static Task AnyAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default) { Check.NotNull(source, nameof(source)); Check.NotNull(cancellationToken, nameof(cancellationToken)); @@ -134,7 +134,7 @@ public static Task AllAsync([NotNull] this IQueryable source, [NotNull] st [PublicAPI] public static Task AnyAsync([NotNull] this IQueryable source, [NotNull] string predicate, [CanBeNull] params object[] args) { - return AnyAsync(source, predicate, default(CancellationToken), args); + return AnyAsync(source, predicate, default, args); } /// @@ -156,7 +156,7 @@ public static Task AnyAsync([NotNull] this IQueryable source, [NotNull] st /// A task that represents the asynchronous operation. The task result contains true if the source sequence contains any elements; otherwise, false. /// [PublicAPI] - public static Task AnyAsync([NotNull] this IQueryable source, [NotNull] string predicate, CancellationToken cancellationToken = default(CancellationToken), [CanBeNull] params object[] args) + public static Task AnyAsync([NotNull] this IQueryable source, [NotNull] string predicate, CancellationToken cancellationToken = default, [CanBeNull] params object[] args) { Check.NotNull(source, nameof(source)); Check.NotEmpty(predicate, nameof(predicate)); @@ -188,7 +188,7 @@ public static Task AnyAsync([NotNull] this IQueryable source, [NotNull] st /// A task that represents the asynchronous operation. The task result contains the average of the sequence of values. /// [PublicAPI] - public static Task AverageAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) + public static Task AverageAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default) { Check.NotNull(source, nameof(source)); Check.NotNull(cancellationToken, nameof(cancellationToken)); @@ -218,7 +218,7 @@ public static Task AnyAsync([NotNull] this IQueryable source, [NotNull] st [PublicAPI] public static Task AverageAsync([NotNull] this IQueryable source, [NotNull] string selector, [CanBeNull] params object[] args) { - return AverageAsync(source, selector, default(CancellationToken), args); + return AverageAsync(source, selector, default, args); } /// @@ -242,7 +242,7 @@ public static Task AverageAsync([NotNull] this IQueryable source, [NotNu /// predicate; otherwise, false. /// [PublicAPI] - public static Task AverageAsync([NotNull] this IQueryable source, [NotNull] string selector, CancellationToken cancellationToken = default(CancellationToken), [CanBeNull] params object[] args) + public static Task AverageAsync([NotNull] this IQueryable source, [NotNull] string selector, CancellationToken cancellationToken = default, [CanBeNull] params object[] args) { Check.NotNull(source, nameof(source)); Check.NotEmpty(selector, nameof(selector)); @@ -275,7 +275,7 @@ public static Task AverageAsync([NotNull] this IQueryable source, [NotNu /// The task result contains the number of elements in the input sequence. /// [PublicAPI] - public static Task CountAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) + public static Task CountAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default) { Check.NotNull(source, nameof(source)); Check.NotNull(cancellationToken, nameof(cancellationToken)); @@ -305,7 +305,7 @@ public static Task AverageAsync([NotNull] this IQueryable source, [NotNu [PublicAPI] public static Task CountAsync([NotNull] this IQueryable source, [NotNull] string predicate, [CanBeNull] params object[] args) { - return CountAsync(source, default(CancellationToken), predicate, args); + return CountAsync(source, default, predicate, args); } /// @@ -362,7 +362,7 @@ public static Task CountAsync([NotNull] this IQueryable source, Cancellatio /// The task result contains the first element in . /// [PublicAPI] - public static Task FirstAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) + public static Task FirstAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default) { Check.NotNull(source, nameof(source)); @@ -391,7 +391,7 @@ public static Task CountAsync([NotNull] this IQueryable source, Cancellatio [PublicAPI] public static Task FirstAsync([NotNull] this IQueryable source, [NotNull] string predicate, [CanBeNull] params object[] args) { - return FirstAsync(source, default(CancellationToken), predicate, args); + return FirstAsync(source, default, predicate, args); } /// @@ -448,7 +448,7 @@ public static Task FirstAsync([NotNull] this IQueryable source, Cancell /// is empty; otherwise, the first element in . /// [PublicAPI] - public static Task FirstOrDefaultAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) + public static Task FirstOrDefaultAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default) { Check.NotNull(source, nameof(source)); Check.NotNull(cancellationToken, nameof(cancellationToken)); @@ -480,7 +480,7 @@ public static Task FirstAsync([NotNull] this IQueryable source, Cancell [PublicAPI] public static Task FirstOrDefaultAsync([NotNull] this IQueryable source, [NotNull] string predicate, [CanBeNull] params object[] args) { - return FirstOrDefaultAsync(source, default(CancellationToken), predicate, args); + return FirstOrDefaultAsync(source, default, predicate, args); } /// @@ -539,7 +539,7 @@ public static Task FirstOrDefaultAsync([NotNull] this IQueryable source /// The task result contains the last element in . /// [PublicAPI] - public static Task LastAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) + public static Task LastAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default) { Check.NotNull(source, nameof(source)); @@ -568,7 +568,7 @@ public static Task FirstOrDefaultAsync([NotNull] this IQueryable source [PublicAPI] public static Task LastAsync([NotNull] this IQueryable source, [NotNull] string predicate, [CanBeNull] params object[] args) { - return LastAsync(source, default(CancellationToken), predicate, args); + return LastAsync(source, default, predicate, args); } /// @@ -625,7 +625,7 @@ public static Task LastAsync([NotNull] this IQueryable source, Cancella /// is empty; otherwise, the last element in . /// [PublicAPI] - public static Task LastOrDefaultAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) + public static Task LastOrDefaultAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default) { Check.NotNull(source, nameof(source)); Check.NotNull(cancellationToken, nameof(cancellationToken)); @@ -657,7 +657,7 @@ public static Task LastAsync([NotNull] this IQueryable source, Cancella [PublicAPI] public static Task LastOrDefaultAsync([NotNull] this IQueryable source, [NotNull] string predicate, [CanBeNull] params object[] args) { - return LastOrDefaultAsync(source, default(CancellationToken), predicate, args); + return LastOrDefaultAsync(source, default, predicate, args); } /// @@ -716,7 +716,7 @@ public static Task LastOrDefaultAsync([NotNull] this IQueryable source, /// The task result contains the number of elements in the input sequence. /// [PublicAPI] - public static Task LongCountAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) + public static Task LongCountAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default) { Check.NotNull(source, nameof(source)); Check.NotNull(cancellationToken, nameof(cancellationToken)); @@ -746,7 +746,7 @@ public static Task LastOrDefaultAsync([NotNull] this IQueryable source, [PublicAPI] public static Task LongCountAsync([NotNull] this IQueryable source, [NotNull] string predicate, [CanBeNull] params object[] args) { - return LongCountAsync(source, default(CancellationToken), predicate, args); + return LongCountAsync(source, default, predicate, args); } /// @@ -803,7 +803,7 @@ public static Task LongCountAsync([NotNull] this IQueryable source, Cancel /// A task that represents the asynchronous operation. The task result contains the single element of the input sequence that satisfies the condition in predicate. /// [PublicAPI] - public static Task SingleOrDefaultAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) + public static Task SingleOrDefaultAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default) { Check.NotNull(source, nameof(source)); Check.NotNull(cancellationToken, nameof(cancellationToken)); @@ -832,7 +832,7 @@ public static Task LongCountAsync([NotNull] this IQueryable source, Cancel [PublicAPI] public static Task SingleOrDefaultAsync([NotNull] this IQueryable source, [NotNull] string predicate, [CanBeNull] params object[] args) { - return SingleOrDefaultAsync(source, default(CancellationToken), predicate, args); + return SingleOrDefaultAsync(source, default, predicate, args); } /// @@ -886,7 +886,7 @@ public static Task SingleOrDefaultAsync([NotNull] this IQueryable sourc /// The task result contains sum of the values in the sequence. /// [PublicAPI] - public static Task SumAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) + public static Task SumAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default) { Check.NotNull(source, nameof(source)); Check.NotNull(cancellationToken, nameof(cancellationToken)); @@ -916,7 +916,7 @@ public static Task SingleOrDefaultAsync([NotNull] this IQueryable sourc [PublicAPI] public static Task SumAsync([NotNull] this IQueryable source, [NotNull] string selector, [CanBeNull] params object[] args) { - return SumAsync(source, default(CancellationToken), selector, args); + return SumAsync(source, default, selector, args); } /// @@ -963,7 +963,7 @@ public static Task SumAsync([NotNull] this IQueryable source, Cancellat .GetMethod(nameof(ExecuteAsync), BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(MethodInfo), typeof(IQueryable), typeof(CancellationToken) }, null); #endif - private static Task ExecuteDynamicAsync(MethodInfo operatorMethodInfo, IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) + private static Task ExecuteDynamicAsync(MethodInfo operatorMethodInfo, IQueryable source, CancellationToken cancellationToken = default) { var executeAsyncMethod = _executeAsyncMethod.MakeGenericMethod(operatorMethodInfo.ReturnType); @@ -973,7 +973,7 @@ public static Task SumAsync([NotNull] this IQueryable source, Cancellat return castedTask; } - private static Task ExecuteAsync(MethodInfo operatorMethodInfo, IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) + private static Task ExecuteAsync(MethodInfo operatorMethodInfo, IQueryable source, CancellationToken cancellationToken = default) { #if EFCORE var provider = source.Provider as IAsyncQueryProvider; @@ -1002,7 +1002,7 @@ public static Task SumAsync([NotNull] this IQueryable source, Cancellat throw new InvalidOperationException(Res.IQueryableProviderNotAsync); } - private static Task ExecuteAsync(MethodInfo operatorMethodInfo, IQueryable source, LambdaExpression expression, CancellationToken cancellationToken = default(CancellationToken)) + private static Task ExecuteAsync(MethodInfo operatorMethodInfo, IQueryable source, LambdaExpression expression, CancellationToken cancellationToken = default) => ExecuteAsync(operatorMethodInfo, source, Expression.Quote(expression), cancellationToken); private static readonly MethodInfo _executeAsyncMethodWithExpression = @@ -1014,7 +1014,7 @@ public static Task SumAsync([NotNull] this IQueryable source, Cancellat .GetMethod(nameof(ExecuteAsync), BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(MethodInfo), typeof(IQueryable), typeof(Expression), typeof(CancellationToken) }, null); #endif - private static Task ExecuteDynamicAsync(MethodInfo operatorMethodInfo, IQueryable source, Expression expression, CancellationToken cancellationToken = default(CancellationToken)) + private static Task ExecuteDynamicAsync(MethodInfo operatorMethodInfo, IQueryable source, Expression expression, CancellationToken cancellationToken = default) { var executeAsyncMethod = _executeAsyncMethodWithExpression.MakeGenericMethod(operatorMethodInfo.ReturnType); @@ -1024,7 +1024,7 @@ public static Task SumAsync([NotNull] this IQueryable source, Cancellat return castedTask; } - private static Task ExecuteAsync(MethodInfo operatorMethodInfo, IQueryable source, Expression expression, CancellationToken cancellationToken = default(CancellationToken)) + private static Task ExecuteAsync(MethodInfo operatorMethodInfo, IQueryable source, Expression expression, CancellationToken cancellationToken = default) { #if EFCORE var provider = source.Provider as IAsyncQueryProvider; @@ -1081,7 +1081,7 @@ private static IQueryable CastSource(IQueryable source, MethodInfo operatorMetho return source; } - private static Task ExecuteAsync(IAsyncQueryProvider provider, Expression expression, CancellationToken cancellationToken = default(CancellationToken)) + private static Task ExecuteAsync(IAsyncQueryProvider provider, Expression expression, CancellationToken cancellationToken = default) { if (typeof(TResult) == typeof(object) && expression.Type != typeof(object)) { diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/EFDynamicQueryableWithFormattableStringExtensions.cs b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/EFDynamicQueryableWithFormattableStringExtensions.cs index 2a75ae60..ae250610 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/EFDynamicQueryableWithFormattableStringExtensions.cs +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/EFDynamicQueryableWithFormattableStringExtensions.cs @@ -43,7 +43,7 @@ public static Task AllInterpolatedAsync([NotNull] this IQueryable source, } [PublicAPI] - public static Task AllInterpolatedAsync([NotNull] this IQueryable source, [NotNull] FormattableString predicate, CancellationToken cancellationToken = default(CancellationToken)) + public static Task AllInterpolatedAsync([NotNull] this IQueryable source, [NotNull] FormattableString predicate, CancellationToken cancellationToken = default) { string predicateStr = ParseFormattableString(predicate, out object[] args); return EntityFrameworkDynamicQueryableExtensions.AllAsync(source, predicateStr, cancellationToken, args); @@ -57,7 +57,7 @@ public static Task AnyInterpolatedAsync([NotNull] this IQueryable source, } [PublicAPI] - public static Task AnyInterpolatedAsync([NotNull] this IQueryable source, [NotNull] FormattableString predicate, CancellationToken cancellationToken = default(CancellationToken)) + public static Task AnyInterpolatedAsync([NotNull] this IQueryable source, [NotNull] FormattableString predicate, CancellationToken cancellationToken = default) { string predicateStr = ParseFormattableString(predicate, out object[] args); return EntityFrameworkDynamicQueryableExtensions.AnyAsync(source, predicateStr, cancellationToken, args); @@ -72,7 +72,7 @@ public static Task AverageInterpolatedAsync([NotNull] this IQueryable so [PublicAPI] - public static Task AverageInterpolatedAsync([NotNull] this IQueryable source, [NotNull] FormattableString selector, CancellationToken cancellationToken = default(CancellationToken)) + public static Task AverageInterpolatedAsync([NotNull] this IQueryable source, [NotNull] FormattableString selector, CancellationToken cancellationToken = default) { string selectorStr = ParseFormattableString(selector, out object[] args); return EntityFrameworkDynamicQueryableExtensions.AverageAsync(source, selectorStr, cancellationToken, args); diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs index d3d2a1e3..84e5917b 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs @@ -5,7 +5,6 @@ using System.Linq.Dynamic.Core.Exceptions; using System.Linq.Dynamic.Core.Tests.Helpers; using System.Linq.Dynamic.Core.Tests.Helpers.Models; -using System.Net.WebSockets; using System.Xml; using FluentAssertions; using Moq; diff --git a/version.xml b/version.xml index 257b812f..9fefde31 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 2 + 3 \ No newline at end of file