From b038e625aa478e2894258aadfd83c02ac1588fb2 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 23 Apr 2025 13:47:37 +0200 Subject: [PATCH 01/44] Q912 --- src-console/ConsoleApp_net6.0/Program.cs | 51 +++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src-console/ConsoleApp_net6.0/Program.cs b/src-console/ConsoleApp_net6.0/Program.cs index 8c5af2f6..3817d897 100644 --- a/src-console/ConsoleApp_net6.0/Program.cs +++ b/src-console/ConsoleApp_net6.0/Program.cs @@ -21,10 +21,29 @@ 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) { + Q912(); + return; + Json(); NewtonsoftJson(); @@ -39,7 +58,7 @@ static void Main(string[] args) { new X { Key = "x" }, new X { Key = "a" }, - new X { Key = "a", Contestants = new List { new Y() } } + 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(); @@ -48,6 +67,36 @@ static void Main(string[] args) Dynamic(); } + private static void Q912() + { + 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); + var ordered = combined.OrderBy("Product").ToDynamicList(); + + int x = 9; + } + + private static void NewtonsoftJson() { var array = JArray.Parse(@"[ From 64b5eff756fe4cfbb8a7b4cc4f9039be322b50f4 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Thu, 24 Apr 2025 19:13:17 +0200 Subject: [PATCH 02/44] Update DynamicGetMemberBinder to add BindingRestrictions to DynamicMetaObject (#913) * Update DynamicGetMemberBinder to add BindingRestrictions to DynamicMetaObject * update test * tests * fix expression parser for DataTable * fix tests? * SkipIfGitHubActionsFactAttribute --- src-console/ConsoleAppEF6_InMemory/Program.cs | 17 +- src-console/ConsoleApp_net6.0/Program.cs | 42 ++++- .../DynamicGetMemberBinder.cs | 6 +- .../Parser/ExpressionParser.cs | 12 +- .../EntityFramework.DynamicLinq.Tests.csproj | 4 + .../DynamicClassTest.cs | 8 +- .../DynamicGetMemberBinderTests.cs | 172 ++++++++++++++++++ .../EntitiesTests.Select.cs | 22 +++ ...cs => SkipIfGitHubActionsFactAttribute.cs} | 4 +- 9 files changed, 266 insertions(+), 21 deletions(-) create mode 100644 test/System.Linq.Dynamic.Core.Tests/DynamicGetMemberBinderTests.cs rename test/System.Linq.Dynamic.Core.Tests/TestHelpers/{SkipIfGitHubActionsAttribute.cs => SkipIfGitHubActionsFactAttribute.cs} (94%) diff --git a/src-console/ConsoleAppEF6_InMemory/Program.cs b/src-console/ConsoleAppEF6_InMemory/Program.cs index f9bed31a..31a9850b 100644 --- a/src-console/ConsoleAppEF6_InMemory/Program.cs +++ b/src-console/ConsoleAppEF6_InMemory/Program.cs @@ -26,7 +26,7 @@ static async Task Main(string[] args) await using (var context = new TestContextEF6()) { var result784 = context.Products.Where("NullableInt = @0", 1).ToDynamicArray(); - Console.WriteLine("a1 {0}", string.Join(",", result784.Select(r => r.Key))); + Console.WriteLine("a1 {0}", string.Join(", ", result784.Select(r => r.Key))); } await using (var context = new TestContextEF6()) @@ -65,5 +65,20 @@ static async Task Main(string[] args) Console.WriteLine(result.Key + ":" + JsonSerializer.Serialize(result.Dict, JsonSerializerOptions)); } } + + // #907 and #912 + await using (var context = new TestContextEF6()) + { + var dynamicData = context.Products + .AsQueryable() + .Select("new { NullableInt as Value }") + .ToDynamicArray(); + var dynamicResult = dynamicData + .AsQueryable() + .Select("Value") + .ToDynamicArray(); + + Console.WriteLine("#907 and #912 = {0}", string.Join(", ", dynamicResult)); + } } } \ No newline at end of file diff --git a/src-console/ConsoleApp_net6.0/Program.cs b/src-console/ConsoleApp_net6.0/Program.cs index 3817d897..e17dbe93 100644 --- a/src-console/ConsoleApp_net6.0/Program.cs +++ b/src-console/ConsoleApp_net6.0/Program.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Linq.Dynamic.Core; using System.Linq.Dynamic.Core.NewtonsoftJson; @@ -33,7 +34,6 @@ public class GroupedSalesData public string Region { get; set; } public string? Product { get; set; } public int TotalSales { get; set; } - public int GroupLevel { get; set; } } @@ -41,7 +41,8 @@ class Program { static void Main(string[] args) { - Q912(); + Q912a(); + Q912b(); return; Json(); @@ -67,7 +68,7 @@ static void Main(string[] args) Dynamic(); } - private static void Q912() + private static void Q912a() { var extractedRows = new List { @@ -90,12 +91,45 @@ private static void Q912() .GroupBy("Region") .Select("new (Key as Region, null as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 1 as GroupLevel)"); - var combined = detailed.Concat(regionSubtotal); + var combined = detailed.Concat(regionSubtotal).AsQueryable(); var ordered = combined.OrderBy("Product").ToDynamicList(); int x = 9; } + private static void Q912b() + { + 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() { diff --git a/src/System.Linq.Dynamic.Core/DynamicGetMemberBinder.cs b/src/System.Linq.Dynamic.Core/DynamicGetMemberBinder.cs index f2369f44..8c1727b8 100644 --- a/src/System.Linq.Dynamic.Core/DynamicGetMemberBinder.cs +++ b/src/System.Linq.Dynamic.Core/DynamicGetMemberBinder.cs @@ -21,13 +21,15 @@ public DynamicGetMemberBinder(string name, ParsingConfig? config) : base(name, c public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject? errorSuggestion) { - var instance = Expression.Call( + var methodCallExpression = Expression.Call( DynamicGetMemberMethod, target.Expression, Expression.Constant(Name), Expression.Constant(IgnoreCase)); - return DynamicMetaObject.Create(target.Value!, instance); + // Fix #907 and #912: "The result of the dynamic binding produced by the object with type '<>f__AnonymousType1`4' for the binder 'System.Linq.Dynamic.Core.DynamicGetMemberBinder' needs at least one restriction.". + var restrictions = BindingRestrictions.GetInstanceRestriction(target.Expression, target.Value); + return new DynamicMetaObject(methodCallExpression, restrictions, target.Value!); } public static object? GetDynamicMember(object value, string name, bool ignoreCase) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 1ec0fea6..c2fb0102 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -1609,9 +1609,9 @@ private Expression CreateNewExpression(List properties, List properties, List + + + + diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs index 100603aa..32dd3002 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs @@ -217,7 +217,7 @@ public void DynamicClass_GetRuntimeType() typeOf.ToString().Should().Be("System.Linq.Dynamic.Core.DynamicClass"); } - [SkipIfGitHubActions] + [SkipIfGitHubActionsFact] public void DynamicClassArray() { // Arrange @@ -249,7 +249,7 @@ public void DynamicClassArray() isValid.Should().BeTrue(); } - [SkipIfGitHubActions] + [SkipIfGitHubActionsFact] public void DynamicClassArray_Issue593_Fails() { // Arrange @@ -281,7 +281,7 @@ public void DynamicClassArray_Issue593_Fails() isValid.Should().BeFalse(); // This should actually be true, but fails. For solution see Issue593_Solution1 and Issue593_Solution2. } - [SkipIfGitHubActions] + [SkipIfGitHubActionsFact] public void DynamicClassArray_Issue593_Solution1() { // Arrange @@ -318,7 +318,7 @@ public void DynamicClassArray_Issue593_Solution1() isValid.Should().BeTrue(); } - [SkipIfGitHubActions] + [SkipIfGitHubActionsFact] public void DynamicClassArray_Issue593_Solution2() { // Arrange diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicGetMemberBinderTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicGetMemberBinderTests.cs new file mode 100644 index 00000000..697a053a --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicGetMemberBinderTests.cs @@ -0,0 +1,172 @@ +using System.Data; +using System.Linq.Dynamic.Core.Tests.TestHelpers; +using FluentAssertions; +using Xunit; + +namespace System.Linq.Dynamic.Core.Tests; + +public class DynamicGetMemberBinderTests +{ + public class SalesData + { + public string Region { get; set; } = null!; + + public string Product { get; set; } = null!; + + public string Sales { get; set; } = null!; + } + + public class GroupedSalesData + { + public string Region { get; set; } = null!; + public string? Product { get; set; } + public int TotalSales { get; set; } + public int GroupLevel { get; set; } + } + + [Fact] + public void DynamicGetMemberBinder_SelectOnArrayWithComplexObjects() + { + // Arrange + var rows = new SalesData[] + { + 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" } + }.AsQueryable(); + + // Act + var grouping1 = rows + .GroupBy("new (Region, Product)") + .Select("new (Key.Region as Region, Key.Product as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 0 as GroupLevel)"); + + var grouping2 = rows + .GroupBy("Region") + .Select("new (Key as Region, null as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 1 as GroupLevel)"); + + var combined = grouping1.ToDynamicArray().Concat(grouping2.ToDynamicArray()).AsQueryable(); + var ordered = combined.OrderBy("Product").ToDynamicList(); + + // Assert + ordered.Should().HaveCount(6); + } + + [Fact] + public void DynamicGetMemberBinder_SelectTypeOnArrayWithComplexObjects() + { + // Arrange + var rows = new SalesData[] + { + 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" } + }.AsQueryable(); + + // Act + var grouping1 = rows + .GroupBy("new (Region, Product)") + .Select("new (Key.Region as Region, Key.Product as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 0 as GroupLevel)"); + + var grouping2 = rows + .GroupBy("Region") + .Select("new (Key as Region, null as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 1 as GroupLevel)"); + + var combined = grouping1.Concat(grouping2).AsQueryable(); + var ordered = combined.OrderBy("Product").ToDynamicList(); + + // Assert + ordered.Should().HaveCount(6); + } + + [SkipIfGitHubActionsFact] + public void DynamicGetMemberBinder_SelectOnDataTable() + { + // Arrange + var dataTable = new DataTable(); + dataTable.Columns.Add("Region", typeof(string)); + dataTable.Columns.Add("Product", typeof(string)); + dataTable.Columns.Add("Sales", typeof(int)); + + dataTable.Rows.Add("North", "Apples", 100); + dataTable.Rows.Add("North", "Oranges", 150); + dataTable.Rows.Add("South", "Apples", 200); + dataTable.Rows.Add("South", "Oranges", 250); + + var rows = dataTable.Rows.Cast().AsQueryable(); + + // Act + var grouping1 = rows + .GroupBy("new (Region, Product)") + .Select("new (Key.Region as Region, Key.Product as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 0 as GroupLevel)"); + + var grouping2 = rows + .GroupBy("Region") + .Select("new (Key as Region, null as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 1 as GroupLevel)"); + + var combined = grouping1.ToDynamicArray().Concat(grouping2.ToDynamicArray()).AsQueryable(); + var ordered = combined.OrderBy("Product").ToDynamicList(); + + // Assert + ordered.Should().HaveCount(6); + } + + [SkipIfGitHubActionsFact] + public void DynamicGetMemberBinder_SelectTypeOnDataTable() + { + // Arrange + var dataTable = new DataTable(); + dataTable.Columns.Add("Region", typeof(string)); + dataTable.Columns.Add("Product", typeof(string)); + dataTable.Columns.Add("Sales", typeof(int)); + + dataTable.Rows.Add("North", "Apples", 100); + dataTable.Rows.Add("North", "Oranges", 150); + dataTable.Rows.Add("South", "Apples", 200); + dataTable.Rows.Add("South", "Oranges", 250); + + var rows = dataTable.Rows.Cast().AsQueryable(); + + // Act + var grouping1 = rows + .GroupBy("new (Region, Product)") + .Select("new (Key.Region as Region, Key.Product as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 0 as GroupLevel)"); + + var grouping2 = rows + .GroupBy("Region") + .Select("new (Key as Region, null as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 1 as GroupLevel)"); + + var combined = grouping1.ToDynamicArray().Concat(grouping2.ToDynamicArray()).AsQueryable(); + var ordered = combined.OrderBy("Product").ToDynamicList(); + + // Assert + ordered.Should().HaveCount(6); + } + + [Fact] + public void DynamicGetMemberBinder_SelectOnArrayWithIntegers() + { + // Arrange + var dynamicData = new[] { 1, 2 } + .AsQueryable() + .Select("new { it * 2 as Value }") + .ToDynamicArray() + .AsQueryable(); + + // Act + var dynamicResult1 = dynamicData + .Select("Value") + .ToDynamicArray(); + + var dynamicResult2 = dynamicData + .Select("Value") + .ToDynamicArray(); + + // Assert + dynamicResult1.Should().HaveCount(2); + dynamicResult2.Should().BeEquivalentTo([2, 4]); + } +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.Select.cs b/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.Select.cs index ab0b44c9..6626d842 100644 --- a/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.Select.cs +++ b/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.Select.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Linq.Dynamic.Core.Exceptions; +using FluentAssertions; using Newtonsoft.Json; #if EFCORE using Microsoft.EntityFrameworkCore; @@ -142,4 +143,25 @@ public void Entities_Select_DynamicClass_And_Call_Any() // Assert Assert.True(result); } + + /// + /// #907 + /// + [Fact] + public void Entities_Select_DynamicClass_And_Select_DynamicClass() + { + // Act + var dynamicData = _context.Blogs + .Take(2) + .Select("new (BlogId as I, Name as N)") + .ToDynamicArray(); + + // Assert + var dynamicResult = dynamicData + .AsQueryable() + .Select("I") + .ToDynamicArray(); + + dynamicResult.Should().BeEquivalentTo([1000, 1001]); + } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/TestHelpers/SkipIfGitHubActionsAttribute.cs b/test/System.Linq.Dynamic.Core.Tests/TestHelpers/SkipIfGitHubActionsFactAttribute.cs similarity index 94% rename from test/System.Linq.Dynamic.Core.Tests/TestHelpers/SkipIfGitHubActionsAttribute.cs rename to test/System.Linq.Dynamic.Core.Tests/TestHelpers/SkipIfGitHubActionsFactAttribute.cs index be6ff839..4ef92c26 100644 --- a/test/System.Linq.Dynamic.Core.Tests/TestHelpers/SkipIfGitHubActionsAttribute.cs +++ b/test/System.Linq.Dynamic.Core.Tests/TestHelpers/SkipIfGitHubActionsFactAttribute.cs @@ -2,9 +2,9 @@ namespace System.Linq.Dynamic.Core.Tests.TestHelpers; -internal class SkipIfGitHubActionsAttribute : FactAttribute +internal class SkipIfGitHubActionsFactAttribute : FactAttribute { - public SkipIfGitHubActionsAttribute() + public SkipIfGitHubActionsFactAttribute() { if (IsRunningOnGitHubActions()) { From 195bc16eac621ca52f90eb1f8692d1ef89164ba9 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Thu, 24 Apr 2025 20:29:18 +0200 Subject: [PATCH 03/44] v1.6.2 --- CHANGELOG.md | 5 +++++ Generate-ReleaseNotes.bat | 4 ++-- version.xml | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a898c28e..f245b238 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# v1.6.2 (24 April 2025) +- [#913](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/913) - Update DynamicGetMemberBinder to add BindingRestrictions to DynamicMetaObject [feature] contributed by [StefH](https://github.com/StefH) +- [#907](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/907) - Select("...") from an IQueryable of anonymous objects created via Select("new { ... }") throws InvalidOperationException [bug] +- [#912](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/912) - Concat question [bug] + # v1.6.0.2 (11 February 2025) - [#896](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/896) - Fix AbstractDynamicLinqCustomTypeProvider.ResolveTypeBySimpleName to use AdditionalTypes [bug] contributed by [StefH](https://github.com/StefH) diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index 8d5a5f9e..38dfff7e 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.6.0.2 +SET version=v1.6.2 -GitHubReleaseNotes --output CHANGELOG.md --exclude-labels known_issue out_of_scope invalid question documentation wontfix environment duplicate --language en --version %version% --token %GH_TOKEN% +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 ba47dead..257b812f 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 0.2 + 2 \ No newline at end of file From 54660efd7f88457997ed0c39d91994fd72bf4416 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 7 May 2025 11:56:17 +0200 Subject: [PATCH 04/44] 918 (DataColumnOrdinalIgnoreCaseComparer) --- .../ConsoleApp_net6.0.csproj | 9 +++-- .../DataColumnOrdinalIgnoreCaseComparer.cs | 32 ++++++++++++++++ src-console/ConsoleApp_net6.0/Program.cs | 37 +++++++++++++++++-- 3 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 src-console/ConsoleApp_net6.0/DataColumnOrdinalIgnoreCaseComparer.cs diff --git a/src-console/ConsoleApp_net6.0/ConsoleApp_net6.0.csproj b/src-console/ConsoleApp_net6.0/ConsoleApp_net6.0.csproj index 46108c0b..332f4fe5 100644 --- a/src-console/ConsoleApp_net6.0/ConsoleApp_net6.0.csproj +++ b/src-console/ConsoleApp_net6.0/ConsoleApp_net6.0.csproj @@ -1,15 +1,16 @@ - + Exe net6.0 ConsoleApp_net6._0 enable + 12 - - + + - + \ No newline at end of file diff --git a/src-console/ConsoleApp_net6.0/DataColumnOrdinalIgnoreCaseComparer.cs b/src-console/ConsoleApp_net6.0/DataColumnOrdinalIgnoreCaseComparer.cs new file mode 100644 index 00000000..e1774a82 --- /dev/null +++ b/src-console/ConsoleApp_net6.0/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_net6.0/Program.cs b/src-console/ConsoleApp_net6.0/Program.cs index e17dbe93..08e7dab5 100644 --- a/src-console/ConsoleApp_net6.0/Program.cs +++ b/src-console/ConsoleApp_net6.0/Program.cs @@ -41,8 +41,11 @@ class Program { static void Main(string[] args) { - Q912a(); - Q912b(); + Issue918(); + return; + + Issue912a(); + Issue912b(); return; Json(); @@ -68,7 +71,33 @@ static void Main(string[] args) Dynamic(); } - private static void Q912a() + 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 { @@ -97,7 +126,7 @@ private static void Q912a() int x = 9; } - private static void Q912b() + private static void Issue912b() { var eInfoJoinTable = new DataTable(); eInfoJoinTable.Columns.Add("Region", typeof(string)); From 5540fd1fc8b7b312b8ff88c732c7000c0e5be188 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 9 May 2025 19:47:03 +0200 Subject: [PATCH 05/44] Update DynamicGetMemberBinder to only add BindingRestrictions for dynamic type and cache the DynamicMetaObject (#922) * Update DynamicGetMemberBinder to only add BindingRestrictions for dynamic type and cache the DynamicMetaObject * internal * 2 * Update src/System.Linq.Dynamic.Core/DynamicGetMemberBinder.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * . --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- System.Linq.Dynamic.Core.sln | 19 +++++++++++ .../ConsoleAppPerformanceTest.csproj | 15 +++++++++ .../ConsoleAppPerformanceTest/Program.cs | 32 +++++++++++++++++++ .../DynamicGetMemberBinder.cs | 27 ++++++++++++++-- 4 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 src-console/ConsoleAppPerformanceTest/ConsoleAppPerformanceTest.csproj create mode 100644 src-console/ConsoleAppPerformanceTest/Program.cs diff --git a/System.Linq.Dynamic.Core.sln b/System.Linq.Dynamic.Core.sln index c331aac5..4bfd1ffa 100644 --- a/System.Linq.Dynamic.Core.sln +++ b/System.Linq.Dynamic.Core.sln @@ -159,6 +159,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.EntityFrameworkCo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Linq.Dynamic.Core.Tests.Net8", "test\System.Linq.Dynamic.Core.Tests.Net8\System.Linq.Dynamic.Core.Tests.Net8.csproj", "{CEBE3A33-4814-42A4-BD8E-F7F2308A4C8C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppPerformanceTest", "src-console\ConsoleAppPerformanceTest\ConsoleAppPerformanceTest.csproj", "{067C00CF-29FA-4643-814D-3A3C3C84634F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1039,6 +1041,22 @@ Global {CEBE3A33-4814-42A4-BD8E-F7F2308A4C8C}.Release|x64.Build.0 = Release|Any CPU {CEBE3A33-4814-42A4-BD8E-F7F2308A4C8C}.Release|x86.ActiveCfg = Release|Any CPU {CEBE3A33-4814-42A4-BD8E-F7F2308A4C8C}.Release|x86.Build.0 = Release|Any CPU + {067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|ARM.ActiveCfg = Debug|Any CPU + {067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|ARM.Build.0 = Debug|Any CPU + {067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|x64.ActiveCfg = Debug|Any CPU + {067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|x64.Build.0 = Debug|Any CPU + {067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|x86.ActiveCfg = Debug|Any CPU + {067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|x86.Build.0 = Debug|Any CPU + {067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|Any CPU.Build.0 = Release|Any CPU + {067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|ARM.ActiveCfg = Release|Any CPU + {067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|ARM.Build.0 = Release|Any CPU + {067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|x64.ActiveCfg = Release|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1098,6 +1116,7 @@ Global {7A31366C-DD98-41A3-A0C1-A97068BD9658} = {BCA2A024-9032-4E56-A6C4-17A15D921728} {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {94C56722-194E-4B8B-BC23-B3F754E89A20} diff --git a/src-console/ConsoleAppPerformanceTest/ConsoleAppPerformanceTest.csproj b/src-console/ConsoleAppPerformanceTest/ConsoleAppPerformanceTest.csproj new file mode 100644 index 00000000..3cce4c2a --- /dev/null +++ b/src-console/ConsoleAppPerformanceTest/ConsoleAppPerformanceTest.csproj @@ -0,0 +1,15 @@ + + + + Exe + net48 + enable + enable + 12 + + + + + + + \ No newline at end of file diff --git a/src-console/ConsoleAppPerformanceTest/Program.cs b/src-console/ConsoleAppPerformanceTest/Program.cs new file mode 100644 index 00000000..21fb15f2 --- /dev/null +++ b/src-console/ConsoleAppPerformanceTest/Program.cs @@ -0,0 +1,32 @@ +using System.Linq.Dynamic.Core; + +TestDynamic(); +return; + +static void TestDynamic() +{ + var list = new List(); + for (int i = 0; i < 100000; i++) + { + list.Add(new Demo { ID = i, Name = $"Name {i}", Description = $"Description {i}" }); + } + + var xTimeAll = System.Diagnostics.Stopwatch.StartNew(); + var query = list.AsQueryable().Select(typeof(Demo), "new { ID, Name }").ToDynamicList(); + Console.WriteLine($"Total 1st Query: {(int)xTimeAll.Elapsed.TotalMilliseconds}ms"); + + xTimeAll.Restart(); + _ = query.AsQueryable().Select("ID").Cast().ToList(); + Console.WriteLine($"Total 2nd Query: {(int)xTimeAll.Elapsed.TotalMilliseconds}ms"); + + xTimeAll.Restart(); + _ = query.AsQueryable().Select("new { it.ID as Idee } ").ToDynamicList(); + Console.WriteLine($"Total 3rd Query: {(int)xTimeAll.Elapsed.TotalMilliseconds}ms"); +} + +class Demo +{ + public int ID { get; set; } + public string Name { get; set; } + public string Description { get; set; } +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/DynamicGetMemberBinder.cs b/src/System.Linq.Dynamic.Core/DynamicGetMemberBinder.cs index 8c1727b8..cbc8542a 100644 --- a/src/System.Linq.Dynamic.Core/DynamicGetMemberBinder.cs +++ b/src/System.Linq.Dynamic.Core/DynamicGetMemberBinder.cs @@ -1,5 +1,6 @@ #if !NET35 && !UAP10_0 && !NETSTANDARD1_3 using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Dynamic; using System.Linq.Expressions; @@ -15,7 +16,15 @@ internal class DynamicGetMemberBinder : GetMemberBinder { private static readonly MethodInfo DynamicGetMemberMethod = typeof(DynamicGetMemberBinder).GetMethod(nameof(GetDynamicMember))!; - public DynamicGetMemberBinder(string name, ParsingConfig? config) : base(name, config?.IsCaseSensitive != true) + // The _metaObjectCache uses a Tuple as the key to cache DynamicMetaObject instances. + // The key components are: + // - Type: The LimitType of the target object, ensuring type-specific caching. + // - string: The member name being accessed. + // - bool: The IgnoreCase flag, indicating whether the member name comparison is case-insensitive. + // This strategy ensures that the cache correctly handles different types, member names, and case-sensitivity settings. + private readonly ConcurrentDictionary, DynamicMetaObject> _metaObjectCache = new(); + + internal DynamicGetMemberBinder(string name, ParsingConfig? config) : base(name, config?.IsCaseSensitive != true) { } @@ -28,8 +37,20 @@ public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, Dy Expression.Constant(IgnoreCase)); // Fix #907 and #912: "The result of the dynamic binding produced by the object with type '<>f__AnonymousType1`4' for the binder 'System.Linq.Dynamic.Core.DynamicGetMemberBinder' needs at least one restriction.". - var restrictions = BindingRestrictions.GetInstanceRestriction(target.Expression, target.Value); - return new DynamicMetaObject(methodCallExpression, restrictions, target.Value!); + // Fix #921: "Slow Performance" + // Only add TypeRestriction if it's a Dynamic type and make sure to cache the DynamicMetaObject. + if (target.Value is IDynamicMetaObjectProvider) + { + var key = new Tuple(target.LimitType, Name, IgnoreCase); + + return _metaObjectCache.GetOrAdd(key, _ => + { + var restrictions = BindingRestrictions.GetTypeRestriction(target.Expression, target.LimitType); + return new DynamicMetaObject(methodCallExpression, restrictions, target.Value); + }); + } + + return DynamicMetaObject.Create(target.Value!, methodCallExpression); } public static object? GetDynamicMember(object value, string name, bool ignoreCase) From ef891397a90c761794661b5398152ea5feb7ca8c Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 9 May 2025 19:49:48 +0200 Subject: [PATCH 06/44] v1.6.3 --- CHANGELOG.md | 4 ++++ Generate-ReleaseNotes.bat | 2 +- version.xml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f245b238..19a7a995 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# v1.6.3 (09 May 2025) +- [#922](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/922) - Update DynamicGetMemberBinder to only add BindingRestrictions for dynamic type and cache the DynamicMetaObject [bug] contributed by [StefH](https://github.com/StefH) +- [#921](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/921) - Strange Performance issue after upgrading from 1.6.0.2 to 1.6.2 [bug] + # v1.6.2 (24 April 2025) - [#913](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/913) - Update DynamicGetMemberBinder to add BindingRestrictions to DynamicMetaObject [feature] contributed by [StefH](https://github.com/StefH) - [#907](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/907) - Select("...") from an IQueryable of anonymous objects created via Select("new { ... }") throws InvalidOperationException [bug] diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index 38dfff7e..3b983077 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.6.2 +SET version=v1.6.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/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 From b8a558ee391ec231fcabcd5006f1c7cbaa1f5434 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Tue, 13 May 2025 22:23:39 +0200 Subject: [PATCH 07/44] Fix MethodFinder TryFindAggregateMethod to support array (#923) * Fix MethodFinder TryFindAggregateMethod to support array * 3 --- .../Parser/SupportedMethods/MethodFinder.cs | 32 +++- .../Parser/TypeHelper.cs | 174 ++++++++++-------- .../ExpressionTests.Sum.cs | 57 ++++++ .../ExpressionTests.cs | 51 ----- .../Parser/ExpressionParserTests.Sum.cs | 117 ++++++++++++ .../QueryableTests.Sum.cs | 49 +++-- 6 files changed, 317 insertions(+), 163 deletions(-) create mode 100644 test/System.Linq.Dynamic.Core.Tests/ExpressionTests.Sum.cs create mode 100644 test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.Sum.cs diff --git a/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs b/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs index 32d8315e..30877e29 100644 --- a/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs +++ b/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs @@ -10,6 +10,7 @@ internal class MethodFinder { private readonly ParsingConfig _parsingConfig; private readonly IExpressionHelper _expressionHelper; + private readonly IDictionary _cachedMethods; /// /// #794 @@ -43,19 +44,32 @@ public MethodFinder(ParsingConfig parsingConfig, IExpressionHelper expressionHel { _parsingConfig = Check.NotNull(parsingConfig); _expressionHelper = Check.NotNull(expressionHelper); + _cachedMethods = new Dictionary + { + { typeof(Enumerable), typeof(Enumerable).GetMethods().Where(m => !m.IsGenericMethodDefinition).ToArray() }, + { typeof(Queryable), typeof(Queryable).GetMethods().Where(m => !m.IsGenericMethodDefinition).ToArray() } + }; } public bool TryFindAggregateMethod(Type callType, string methodName, Type parameterType, [NotNullWhen(true)] out MethodInfo? aggregateMethod) { - aggregateMethod = callType - .GetMethods() - .Where(m => m.Name == methodName && !m.IsGenericMethodDefinition) - .SelectMany(m => m.GetParameters(), (m, p) => new { Method = m, Parameter = p }) - .Where(x => x.Parameter.ParameterType == parameterType) - .Select(x => x.Method) - .FirstOrDefault(); - - return aggregateMethod != null; + var nonGenericMethodsByName = _cachedMethods[callType] + .Where(m => m.Name == methodName) + .ToArray(); + + if (TypeHelper.TryGetAsEnumerable(parameterType, out var parameterTypeAsEnumerable)) + { + aggregateMethod = nonGenericMethodsByName + .SelectMany(m => m.GetParameters(), (m, p) => new { Method = m, Parameter = p }) + .Where(x => x.Parameter.ParameterType == parameterTypeAsEnumerable) + .Select(x => x.Method) + .FirstOrDefault(); + + return aggregateMethod != null; + } + + aggregateMethod = null; + return false; } public bool CheckAggregateMethodAndTryUpdateArgsToMatchMethodArgs(string methodName, ref Expression[] args) diff --git a/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs b/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs index 843732f7..6ffd2a19 100644 --- a/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs @@ -6,6 +6,24 @@ namespace System.Linq.Dynamic.Core.Parser; internal static class TypeHelper { + internal static bool TryGetAsEnumerable(Type type, [NotNullWhen(true)] out Type? enumerableType) + { + if (type.IsArray) + { + enumerableType = typeof(IEnumerable<>).MakeGenericType(type.GetElementType()!); + return true; + } + + if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + enumerableType = type; + return true; + } + + enumerableType = null; + return false; + } + public static bool TryGetFirstGenericArgument(Type type, [NotNullWhen(true)] out Type? genericType) { var genericArguments = type.GetTypeInfo().GetGenericTypeArguments(); @@ -196,79 +214,79 @@ public static bool IsCompatibleWith(Type source, Type target) } return false; #else - if (source == target) - { - return true; - } + if (source == target) + { + return true; + } - if (!target.GetTypeInfo().IsValueType) - { - return target.IsAssignableFrom(source); - } + if (!target.GetTypeInfo().IsValueType) + { + return target.IsAssignableFrom(source); + } - Type st = GetNonNullableType(source); - Type tt = GetNonNullableType(target); + Type st = GetNonNullableType(source); + Type tt = GetNonNullableType(target); - if (st != source && tt == target) - { - return false; - } + if (st != source && tt == target) + { + return false; + } - Type sc = st.GetTypeInfo().IsEnum ? typeof(object) : st; - Type tc = tt.GetTypeInfo().IsEnum ? typeof(object) : tt; + Type sc = st.GetTypeInfo().IsEnum ? typeof(object) : st; + Type tc = tt.GetTypeInfo().IsEnum ? typeof(object) : tt; - if (sc == typeof(sbyte)) - { - if (tc == typeof(sbyte) || tc == typeof(short) || tc == typeof(int) || tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(byte)) - { - if (tc == typeof(byte) || tc == typeof(short) || tc == typeof(ushort) || tc == typeof(int) || tc == typeof(uint) || tc == typeof(long) || tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(short)) - { - if (tc == typeof(short) || tc == typeof(int) || tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(ushort)) - { - if (tc == typeof(ushort) || tc == typeof(int) || tc == typeof(uint) || tc == typeof(long) || tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(int)) - { - if (tc == typeof(int) || tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(uint)) - { - if (tc == typeof(uint) || tc == typeof(long) || tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(long)) - { - if (tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(ulong)) - { - if (tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(float)) - { - if (tc == typeof(float) || tc == typeof(double)) - return true; - } - - if (st == tt) - { + if (sc == typeof(sbyte)) + { + if (tc == typeof(sbyte) || tc == typeof(short) || tc == typeof(int) || tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) return true; - } + } + else if (sc == typeof(byte)) + { + if (tc == typeof(byte) || tc == typeof(short) || tc == typeof(ushort) || tc == typeof(int) || tc == typeof(uint) || tc == typeof(long) || tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) + return true; + } + else if (sc == typeof(short)) + { + if (tc == typeof(short) || tc == typeof(int) || tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) + return true; + } + else if (sc == typeof(ushort)) + { + if (tc == typeof(ushort) || tc == typeof(int) || tc == typeof(uint) || tc == typeof(long) || tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) + return true; + } + else if (sc == typeof(int)) + { + if (tc == typeof(int) || tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) + return true; + } + else if (sc == typeof(uint)) + { + if (tc == typeof(uint) || tc == typeof(long) || tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) + return true; + } + else if (sc == typeof(long)) + { + if (tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) + return true; + } + else if (sc == typeof(ulong)) + { + if (tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) + return true; + } + else if (sc == typeof(float)) + { + if (tc == typeof(float) || tc == typeof(double)) + return true; + } - return false; + if (st == tt) + { + return true; + } + + return false; #endif } @@ -391,19 +409,19 @@ private static int GetNumericTypeKind(Type type) return 0; } #else - if (type.GetTypeInfo().IsEnum) - { - return 0; - } + if (type.GetTypeInfo().IsEnum) + { + return 0; + } - if (type == typeof(char) || type == typeof(float) || type == typeof(double) || type == typeof(decimal)) - return 1; - if (type == typeof(sbyte) || type == typeof(short) || type == typeof(int) || type == typeof(long)) - return 2; - if (type == typeof(byte) || type == typeof(ushort) || type == typeof(uint) || type == typeof(ulong)) - return 3; + if (type == typeof(char) || type == typeof(float) || type == typeof(double) || type == typeof(decimal)) + return 1; + if (type == typeof(sbyte) || type == typeof(short) || type == typeof(int) || type == typeof(long)) + return 2; + if (type == typeof(byte) || type == typeof(ushort) || type == typeof(uint) || type == typeof(ulong)) + return 3; - return 0; + return 0; #endif } @@ -484,7 +502,7 @@ private static void AddInterface(ICollection types, Type type) public static bool TryParseEnum(string value, Type? type, [NotNullWhen(true)] out object? enumValue) { - if (type is { } && type.GetTypeInfo().IsEnum && Enum.IsDefined(type, value)) + if (type != null && type.GetTypeInfo().IsEnum && Enum.IsDefined(type, value)) { enumValue = Enum.Parse(type, value, true); return true; diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.Sum.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.Sum.cs new file mode 100644 index 00000000..b1d26866 --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.Sum.cs @@ -0,0 +1,57 @@ +using System.Linq.Dynamic.Core.Tests.Helpers.Models; +using Xunit; + +namespace System.Linq.Dynamic.Core.Tests; + +public partial class ExpressionTests +{ + [Fact] + public void ExpressionTests_Sum() + { + // Arrange + int[] initValues = [1, 2, 3, 4, 5]; + var qry = initValues.AsQueryable().Select(x => new { strValue = "str", intValue = x }).GroupBy(x => x.strValue); + + // Act + var result = qry.Select("Sum(intValue)").AsDynamicEnumerable().ToArray()[0]; + + // Assert + Assert.Equal(15, result); + } + + [Fact] + public void ExpressionTests_Sum_LowerCase() + { + // Arrange + int[] initValues = [1, 2, 3, 4, 5]; + var qry = initValues.AsQueryable().Select(x => new { strValue = "str", intValue = x }).GroupBy(x => x.strValue); + + // Act + var result = qry.Select("sum(intValue)").AsDynamicEnumerable().ToArray()[0]; + + // Assert + Assert.Equal(15, result); + } + + [Fact] + public void ExpressionTests_Sum2() + { + // Arrange + var initValues = new[] + { + new SimpleValuesModel { FloatValue = 1 }, + new SimpleValuesModel { FloatValue = 2 }, + new SimpleValuesModel { FloatValue = 3 }, + }; + + var qry = initValues.AsQueryable(); + + // Act + var result = qry.Select("FloatValue").Sum(); + var result2 = ((IQueryable)qry.Select("FloatValue")).Sum(); + + // Assert + Assert.Equal(6.0f, result); + Assert.Equal(6.0f, result2); + } +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs index 1c00d1d6..86854d53 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs @@ -2154,57 +2154,6 @@ public void ExpressionTests_Subtract_Number() Check.That(result).ContainsExactly(expected); } - [Fact] - public void ExpressionTests_Sum() - { - // Arrange - int[] initValues = { 1, 2, 3, 4, 5 }; - var qry = initValues.AsQueryable().Select(x => new { strValue = "str", intValue = x }).GroupBy(x => x.strValue); - - // Act - var result = qry.Select("Sum(intValue)").AsDynamicEnumerable().ToArray()[0]; - - // Assert - Assert.Equal(15, result); - } - - [Fact] - public void ExpressionTests_Sum_LowerCase() - { - // Arrange - int[] initValues = { 1, 2, 3, 4, 5 }; - var qry = initValues.AsQueryable().Select(x => new { strValue = "str", intValue = x }).GroupBy(x => x.strValue); - - // Act - var result = qry.Select("sum(intValue)").AsDynamicEnumerable().ToArray()[0]; - - // Assert - Assert.Equal(15, result); - } - - [Fact] - public void ExpressionTests_Sum2() - { - // Arrange - var initValues = new[] - { - new SimpleValuesModel { FloatValue = 1 }, - new SimpleValuesModel { FloatValue = 2 }, - new SimpleValuesModel { FloatValue = 3 }, - }; - - var qry = initValues.AsQueryable(); - - // Act - var result = qry.Select("FloatValue").Sum(); - var result2 = ((IQueryable)qry.Select("FloatValue")).Sum(); - - // Assert - Assert.Equal(6.0f, result); - Assert.Equal(6.0f, result2); - } - - [Fact] public void ExpressionTests_Type_Integer() { diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.Sum.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.Sum.cs new file mode 100644 index 00000000..2c33276e --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.Sum.cs @@ -0,0 +1,117 @@ +using System.Collections.Generic; +using System.Linq.Dynamic.Core.Parser; +using System.Linq.Expressions; +using Xunit; + +namespace System.Linq.Dynamic.Core.Tests.Parser; + +public partial class ExpressionParserTests +{ + [Fact] + public void Parse_Aggregate_Sum_With_Predicate() + { + // Arrange + var childType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("Value", typeof(int)) + ]); + + var parentType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("SubFoos", childType.MakeArrayType()) + ]); + + // Act + var parser = new ExpressionParser( + [ + Expression.Parameter(parentType, "Foo") + ], + "Foo.SubFoos.Sum(s => s.Value)", + [], + new ParsingConfig()); + + // Assert + parser.Parse(typeof(int)); + } + + [Fact] + public void Parse_Aggregate_Sum_In_Sum_With_Predicate_On_IEnumerable() + { + // Arrange + var childType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("DoubleArray", typeof(IEnumerable)) + ]); + + var parentType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("SubFoos", childType.MakeArrayType()) + ]); + + // Act + var parser = new ExpressionParser( + [ + Expression.Parameter(parentType, "Foo") + ], + "Foo.SubFoos.Sum(s => s.DoubleArray.Sum())", + [], + new ParsingConfig()); + + // Assert + parser.Parse(typeof(double)); + } + + [Fact] + public void Parse_Aggregate_Sum_In_Sum_With_Predicate_On_Array() + { + // Arrange + var childType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("DoubleArray", typeof(double[])) + ]); + + var parentType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("SubFoos", childType.MakeArrayType()) + ]); + + // Act + var parser = new ExpressionParser( + [ + Expression.Parameter(parentType, "Foo") + ], + "Foo.SubFoos.Sum(s => s.DoubleArray.Sum())", + [], + new ParsingConfig()); + + // Assert + parser.Parse(typeof(double)); + } + + [Fact] + public void Parse_Aggregate_Sum_In_Sum_In_Sum_With_Predicate_On_ArrayArray() + { + // Arrange + var childType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("DoubleArrayArray", typeof(double[][])) + ]); + + var parentType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("SubFoos", childType.MakeArrayType()) + ]); + + // Act + var parser = new ExpressionParser( + [ + Expression.Parameter(parentType, "Foo") + ], + "Foo.SubFoos.Sum(s => s.DoubleArrayArray.Sum(x => x.Sum()))", + [], + new ParsingConfig()); + + // Assert + parser.Parse(typeof(double)); + } +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Sum.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Sum.cs index 7bab86be..c95db7ed 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Sum.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Sum.cs @@ -1,36 +1,35 @@ using System.Linq.Dynamic.Core.Tests.Helpers.Models; using Xunit; -namespace System.Linq.Dynamic.Core.Tests +namespace System.Linq.Dynamic.Core.Tests; + +public partial class QueryableTests { - public partial class QueryableTests + [Fact] + public void Sum() { - [Fact] - public void Sum() - { - // Arrange - var incomes = User.GenerateSampleModels(100).Select(u => u.Income); + // Arrange + var incomes = User.GenerateSampleModels(100).Select(u => u.Income); - // Act - var expected = incomes.Sum(); - var actual = incomes.AsQueryable().Sum(); + // Act + var expected = incomes.Sum(); + var actual = incomes.AsQueryable().Sum(); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); + } - [Fact] - public void Sum_Selector() - { - // Arrange - var users = User.GenerateSampleModels(100); + [Fact] + public void Sum_Selector() + { + // Arrange + var users = User.GenerateSampleModels(100); - // Act - var expected = users.Sum(u => u.Income); - var result = users.AsQueryable().Sum("Income"); + // Act + var expected = users.Sum(u => u.Income); + var result = users.AsQueryable().Sum("Income"); - // Assert - Assert.Equal(expected, result); - } + // Assert + Assert.Equal(expected, result); } -} +} \ No newline at end of file From 7d39d33fcbf2e0969eef76f730634c0b4f9d6886 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 14 May 2025 08:25:50 +0200 Subject: [PATCH 08/44] Add support for "not in" and "not_in" (#915) --- .../Parser/ExpressionParser.cs | 58 +++++++++----- .../Tokenizer/TextParser.cs | 34 ++++++++ .../ExpressionTests.cs | 68 ++++++++++++---- .../Parser/ExpressionParserTests.cs | 79 +++++++++++++------ 4 files changed, 180 insertions(+), 59 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index c2fb0102..b4a7245f 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -218,11 +218,11 @@ internal IList ParseOrdering(bool forceThenBy = false) { var expr = ParseConditionalOperator(); var ascending = true; - if (TokenIdentifierIs("asc") || TokenIdentifierIs("ascending")) + if (TokenIsIdentifier("asc") || TokenIsIdentifier("ascending")) { _textParser.NextToken(); } - else if (TokenIdentifierIs("desc") || TokenIdentifierIs("descending")) + else if (TokenIsIdentifier("desc") || TokenIsIdentifier("descending")) { _textParser.NextToken(); ascending = false; @@ -337,19 +337,34 @@ private Expression ParseAndOperator() return left; } - // in operator for literals - example: "x in (1,2,3,4)" - // in operator to mimic contains - example: "x in @0", compare to @0.Contains(x) - // Adapted from ticket submitted by github user mlewis9548 + // "in" / "not in" / "not_in" operator for literals - example: "x in (1,2,3,4)" + // "in" / "not in" / "not_in" operator to mimic contains - example: "x in @0", compare to @0.Contains(x) private Expression ParseIn() { Expression left = ParseLogicalAndOrOperator(); Expression accumulate = left; - while (TokenIdentifierIs("in")) + while (_textParser.TryGetToken(["in", "not_in", "not"], [TokenId.Exclamation], out var token)) { - var op = _textParser.CurrentToken; + var not = false; + if (token.Text == "not_in") + { + not = true; + } + else if (token.Text == "not" || token.Id == TokenId.Exclamation) + { + not = true; + + _textParser.NextToken(); + + if (!TokenIsIdentifier("in")) + { + throw ParseError(token.Pos, Res.TokenExpected, "in"); + } + } _textParser.NextToken(); + if (_textParser.CurrentToken.Id == TokenId.OpenParen) // literals (or other inline list) { while (_textParser.CurrentToken.Id != TokenId.CloseParen) @@ -364,18 +379,18 @@ private Expression ParseIn() { if (right is ConstantExpression constantExprRight) { - right = ParseEnumToConstantExpression(op.Pos, left.Type, constantExprRight); + right = ParseEnumToConstantExpression(token.Pos, left.Type, constantExprRight); } else if (_expressionHelper.TryUnwrapAsConstantExpression(right, out var unwrappedConstantExprRight)) { - right = ParseEnumToConstantExpression(op.Pos, left.Type, unwrappedConstantExprRight); + right = ParseEnumToConstantExpression(token.Pos, left.Type, unwrappedConstantExprRight); } } // else, check for direct type match else if (left.Type != right.Type) { - CheckAndPromoteOperands(typeof(IEqualitySignatures), TokenId.DoubleEqual, "==", ref left, ref right, op.Pos); + CheckAndPromoteOperands(typeof(IEqualitySignatures), TokenId.DoubleEqual, "==", ref left, ref right, token.Pos); } if (accumulate.Type != typeof(bool)) @@ -389,7 +404,7 @@ private Expression ParseIn() if (_textParser.CurrentToken.Id == TokenId.End) { - throw ParseError(op.Pos, Res.CloseParenOrCommaExpected); + throw ParseError(token.Pos, Res.CloseParenOrCommaExpected); } } @@ -413,7 +428,12 @@ private Expression ParseIn() } else { - throw ParseError(op.Pos, Res.OpenParenOrIdentifierExpected); + throw ParseError(token.Pos, Res.OpenParenOrIdentifierExpected); + } + + if (not) + { + accumulate = Expression.Not(accumulate); } } @@ -759,7 +779,7 @@ private Expression ParseAdditive() private Expression ParseArithmetic() { Expression left = ParseUnary(); - while (_textParser.CurrentToken.Id is TokenId.Asterisk or TokenId.Slash or TokenId.Percent || TokenIdentifierIs("mod")) + while (_textParser.CurrentToken.Id is TokenId.Asterisk or TokenId.Slash or TokenId.Percent || TokenIsIdentifier("mod")) { Token op = _textParser.CurrentToken; _textParser.NextToken(); @@ -787,11 +807,11 @@ private Expression ParseArithmetic() // -, !, not unary operators private Expression ParseUnary() { - if (_textParser.CurrentToken.Id == TokenId.Minus || _textParser.CurrentToken.Id == TokenId.Exclamation || TokenIdentifierIs("not")) + if (_textParser.CurrentToken.Id == TokenId.Minus || _textParser.CurrentToken.Id == TokenId.Exclamation || TokenIsIdentifier("not")) { Token op = _textParser.CurrentToken; _textParser.NextToken(); - if (op.Id == TokenId.Minus && (_textParser.CurrentToken.Id == TokenId.IntegerLiteral || _textParser.CurrentToken.Id == TokenId.RealLiteral)) + if (op.Id == TokenId.Minus && _textParser.CurrentToken.Id is TokenId.IntegerLiteral or TokenId.RealLiteral) { _textParser.CurrentToken.Text = "-" + _textParser.CurrentToken.Text; _textParser.CurrentToken.Pos = op.Pos; @@ -1445,7 +1465,7 @@ private Expression ParseNew() if (!arrayInitializer) { string? propName; - if (TokenIdentifierIs("as")) + if (TokenIsIdentifier("as")) { _textParser.NextToken(); propName = GetIdentifierAs(); @@ -2527,11 +2547,11 @@ private static Exception IncompatibleOperandsError(string opName, Expression lef #endif } - private bool TokenIdentifierIs(string id) + private bool TokenIsIdentifier(string id) { - return _textParser.CurrentToken.Id == TokenId.Identifier && string.Equals(id, _textParser.CurrentToken.Text, StringComparison.OrdinalIgnoreCase); + return _textParser.TokenIsIdentifier(id); } - + private string GetIdentifier() { _textParser.ValidateToken(TokenId.Identifier, Res.IdentifierExpected); diff --git a/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs b/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs index cea61324..4f5a3529 100644 --- a/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs +++ b/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs @@ -479,6 +479,40 @@ public void ValidateToken(TokenId tokenId, string? errorMessage = null) } } + /// + /// Check if the current token is an with the provided id . + /// + /// The id + public bool TokenIsIdentifier(string id) + { + return CurrentToken.Id == TokenId.Identifier && string.Equals(id, CurrentToken.Text, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Try to get a token based on the id or . + /// + /// The ids. + /// The tokenIds. + /// The found token, or default when not found. + public bool TryGetToken(string[] ids, TokenId[] tokenIds, out Token token) + { + token = default; + + if (ids.Any(TokenIsIdentifier)) + { + token = CurrentToken; + return true; + } + + if (tokenIds.Any(tokenId => tokenId == CurrentToken.Id)) + { + token = CurrentToken; + return true; + } + + return false; + } + private void SetTextPos(int pos) { _textPos = pos; diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs index 86854d53..6ff98396 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs @@ -1307,28 +1307,44 @@ public void ExpressionTests_In_Short() public void ExpressionTests_In_String() { // Arrange - var testRange = Enumerable.Range(1, 100).ToArray(); var testModels = User.GenerateSampleModels(10); - var testModelByUsername = string.Format("Username in (\"{0}\",\"{1}\",\"{2}\")", testModels[0].UserName, testModels[1].UserName, testModels[2].UserName); + var testModelByUsername = $"Username in (\"{testModels[0].UserName}\",\"{testModels[1].UserName}\",\"{testModels[2].UserName}\")"; + + // Act + var result1 = testModels.AsQueryable().Where(testModelByUsername).ToArray(); + var result2 = testModels.AsQueryable().Where("Id in (@0, @1, @2)", testModels[0].Id, testModels[1].Id, testModels[2].Id).ToArray(); + + // Assert + Assert.Equal(testModels.Take(3).ToArray(), result1); + Assert.Equal(testModels.Take(3).ToArray(), result2); + } + + [Fact] + public void ExpressionTests_In_IntegerArray() + { + // Arrange + var testRange = Enumerable.Range(1, 10).ToArray(); var testInExpression = new[] { 2, 4, 6, 8 }; // Act var result1a = testRange.AsQueryable().Where("it in (2,4,6,8)").ToArray(); var result1b = testRange.AsQueryable().Where("it in (2, 4, 6, 8)").ToArray(); - // https://github.com/NArnott/System.Linq.Dynamic/issues/52 - var result2 = testModels.AsQueryable().Where(testModelByUsername).ToArray(); - var result3 = - testModels.AsQueryable() - .Where("Id in (@0, @1, @2)", testModels[0].Id, testModels[1].Id, testModels[2].Id) - .ToArray(); - var result4 = testRange.AsQueryable().Where("it in @0", testInExpression).ToArray(); + var result2 = testRange.AsQueryable().Where("it in @0", testInExpression).ToArray(); // Assert - Assert.Equal(new[] { 2, 4, 6, 8 }, result1a); - Assert.Equal(new[] { 2, 4, 6, 8 }, result1b); - Assert.Equal(testModels.Take(3).ToArray(), result2); - Assert.Equal(testModels.Take(3).ToArray(), result3); - Assert.Equal(new[] { 2, 4, 6, 8 }, result4); + Assert.Equal([2, 4, 6, 8], result1a); + Assert.Equal([2, 4, 6, 8], result1b); + Assert.Equal([2, 4, 6, 8], result2); + } + + [Fact] + public void ExpressionTests_InvalidNotIn_ThrowsException() + { + // Arrange + var testRange = Enumerable.Range(1, 10).ToArray(); + + // Act + Assert + Check.ThatCode(() => testRange.AsQueryable().Where("it not not in (2,4,6,8)").ToArray()).Throws(); } [Fact] @@ -1519,6 +1535,26 @@ public void ExpressionTests_Multiply_Number() Check.That(result).ContainsExactly(expected); } + [Fact] + public void ExpressionTests_NotIn_IntegerArray() + { + // Arrange + var testRange = Enumerable.Range(1, 9).ToArray(); + var testInExpression = new[] { 2, 4, 6, 8 }; + + // Act + var result1a = testRange.AsQueryable().Where("it not in (2,4,6,8)").ToArray(); + var result1b = testRange.AsQueryable().Where("it not_in (2, 4, 6, 8)").ToArray(); + var result2 = testRange.AsQueryable().Where("it not in @0", testInExpression).ToArray(); + var result3 = testRange.AsQueryable().Where("it not_in @0", testInExpression).ToArray(); + + // Assert + Assert.Equal([1, 3, 5, 7, 9], result1a); + Assert.Equal([1, 3, 5, 7, 9], result1b); + Assert.Equal([1, 3, 5, 7, 9], result2); + Assert.Equal([1, 3, 5, 7, 9], result3); + } + [Fact] public void ExpressionTests_NullCoalescing() { @@ -1699,7 +1735,7 @@ public void ExpressionTests_NullPropagating_Config_Has_UseDefault(string test, s queryAsString = queryAsString.Substring(queryAsString.IndexOf(".Select") + 1).TrimEnd(']'); Check.That(queryAsString).Equals(query); } - + [Fact] public void ExpressionTests_NullPropagation_Method() { @@ -2103,7 +2139,7 @@ public void ExpressionTests_StringEscaping() // Act var result = baseQuery.Where("it.Value == \"ab\\\"cd\"").ToList(); - + // Assert Assert.Single(result); Assert.Equal("ab\"cd", result[0].Value); diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs index 9369d143..7c8d7e43 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs @@ -35,7 +35,7 @@ public class MyView public ExpressionParserTests() { _dynamicTypeProviderMock = new Mock(); - _dynamicTypeProviderMock.Setup(dt => dt.GetCustomTypes()).Returns(new HashSet() { typeof(Company), typeof(MainCompany) }); + _dynamicTypeProviderMock.Setup(dt => dt.GetCustomTypes()).Returns([typeof(Company), typeof(MainCompany)]); _dynamicTypeProviderMock.Setup(dt => dt.ResolveType(typeof(Company).FullName!)).Returns(typeof(Company)); _dynamicTypeProviderMock.Setup(dt => dt.ResolveType(typeof(MainCompany).FullName!)).Returns(typeof(MainCompany)); _dynamicTypeProviderMock.Setup(dt => dt.ResolveTypeBySimpleName("Company")).Returns(typeof(Company)); @@ -57,8 +57,8 @@ public void Parse_BitwiseOperatorOr_On_2EnumFlags() #else var expected = "Convert((Convert(A, Int32) | Convert(B, Int32)), ExampleFlags)"; #endif - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x") }; - var sut = new ExpressionParser(parameters, expression, new object[] { ExampleFlags.A, ExampleFlags.B }, null); + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x")]; + var sut = new ExpressionParser(parameters, expression, [ExampleFlags.A, ExampleFlags.B], null); // Act var parsedExpression = sut.Parse(null).ToString(); @@ -87,8 +87,8 @@ public void Parse_BitwiseOperatorAnd_On_2EnumFlags() #else var expected = "Convert((Convert(A, Int32) & Convert(B, Int32)), ExampleFlags)"; #endif - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x") }; - var sut = new ExpressionParser(parameters, expression, new object[] { ExampleFlags.A, ExampleFlags.B }, null); + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x")]; + var sut = new ExpressionParser(parameters, expression, [ExampleFlags.A, ExampleFlags.B], null); // Act var parsedExpression = sut.Parse(null).ToString(); @@ -117,8 +117,8 @@ public void Parse_BitwiseOperatorOr_On_3EnumFlags() #else var expected = "Convert(((Convert(A, Int32) | Convert(B, Int32)) | Convert(C, Int32)), ExampleFlags)"; #endif - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x") }; - var sut = new ExpressionParser(parameters, expression, new object[] { ExampleFlags.A, ExampleFlags.B, ExampleFlags.C }, null); + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x")]; + var sut = new ExpressionParser(parameters, expression, [ExampleFlags.A, ExampleFlags.B, ExampleFlags.C], null); // Act var parsedExpression = sut.Parse(null).ToString(); @@ -147,8 +147,8 @@ public void Parse_BitwiseOperatorAnd_On_3EnumFlags() #else var expected = "Convert(((Convert(A, Int32) & Convert(B, Int32)) & Convert(C, Int32)), ExampleFlags)"; #endif - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x") }; - var sut = new ExpressionParser(parameters, expression, new object[] { ExampleFlags.A, ExampleFlags.B, ExampleFlags.C }, null); + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x")]; + var sut = new ExpressionParser(parameters, expression, [ExampleFlags.A, ExampleFlags.B, ExampleFlags.C], null); // Act var parsedExpression = sut.Parse(null).ToString(); @@ -172,7 +172,7 @@ public void Parse_ParseBinaryInteger() { // Arrange var expression = "0b1100000011101"; - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x")]; var sut = new ExpressionParser(parameters, expression, null, null); // Act @@ -187,7 +187,7 @@ public void Parse_ParseHexadecimalInteger() { // Arrange var expression = "0xFF"; - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x")]; var sut = new ExpressionParser(parameters, expression, null, null); // Act @@ -216,7 +216,7 @@ public void Parse_ParseHexadecimalInteger() public void Parse_ParseComparisonOperator(string expression, string result) { // Arrange - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x")]; var sut = new ExpressionParser(parameters, expression, null, null); // Act @@ -233,7 +233,7 @@ public void Parse_ParseComparisonOperator(string expression, string result) public void Parse_ParseOrOperator(string expression, string result) { // Arrange - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x")]; var sut = new ExpressionParser(parameters, expression, null, null); // Act @@ -250,7 +250,7 @@ public void Parse_ParseOrOperator(string expression, string result) public void Parse_ParseAndOperator(string expression, string result) { // Arrange - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x")]; var sut = new ExpressionParser(parameters, expression, null, null); // Act @@ -264,22 +264,51 @@ public void Parse_ParseAndOperator(string expression, string result) public void Parse_ParseMultipleInOperators() { // Arrange - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "x") }; - var sut = new ExpressionParser(parameters, "MainCompanyId in (1, 2) and Name in (\"A\", \"B\")", null, null); + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "x")]; + var sut = new ExpressionParser(parameters, "MainCompanyId in (1, 2) and Name in (\"A\", \"B\") && 'y' in Name && 'z' in Name", null, null); // Act var parsedExpression = sut.Parse(null).ToString(); // Assert - Check.That(parsedExpression).Equals("(((x.MainCompanyId == 1) OrElse (x.MainCompanyId == 2)) AndAlso ((x.Name == \"A\") OrElse (x.Name == \"B\")))"); + Check.That(parsedExpression).Equals("(((((x.MainCompanyId == 1) OrElse (x.MainCompanyId == 2)) AndAlso ((x.Name == \"A\") OrElse (x.Name == \"B\"))) AndAlso x.Name.Contains(y)) AndAlso x.Name.Contains(z))"); + } + + [Fact] + public void Parse_ParseMultipleInAndNotInOperators() + { + // Arrange + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "x")]; + var sut = new ExpressionParser(parameters, "MainCompanyId in (1, 2) and Name not in (\"A\", \"B\") && 'y' in Name && 'z' not in Name", null, null); + + // Act + var parsedExpression = sut.Parse(null).ToString(); + + // Assert + Check.That(parsedExpression).Equals("(((((x.MainCompanyId == 1) OrElse (x.MainCompanyId == 2)) AndAlso Not(((x.Name == \"A\") OrElse (x.Name == \"B\")))) AndAlso x.Name.Contains(y)) AndAlso Not(x.Name.Contains(z)))"); + } + + + [Fact] + public void Parse_ParseMultipleInAndNotInAndNot_InOperators() + { + // Arrange + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "x")]; + var sut = new ExpressionParser(parameters, "MainCompanyId in (1, 2) and MainCompanyId not in (3, 4) and Name not_in (\"A\", \"B\") && 'y' in Name && 'z' not in Name && 's' not_in Name", null, null); + + // Act + var parsedExpression = sut.Parse(null).ToString(); + + // Assert + Check.That(parsedExpression).Equals("(((((((x.MainCompanyId == 1) OrElse (x.MainCompanyId == 2)) AndAlso Not(((x.MainCompanyId == 3) OrElse (x.MainCompanyId == 4)))) AndAlso Not(((x.Name == \"A\") OrElse (x.Name == \"B\")))) AndAlso x.Name.Contains(y)) AndAlso Not(x.Name.Contains(z))) AndAlso Not(x.Name.Contains(s)))"); } [Fact] public void Parse_ParseInWrappedInParenthesis() { // Arrange - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "x") }; - var sut = new ExpressionParser(parameters, "(MainCompanyId in @0)", new object[] { new long?[] { 1, 2 } }, null); + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "x")]; + var sut = new ExpressionParser(parameters, "(MainCompanyId in @0)", [new long?[] { 1, 2 }], null); // Act var parsedExpression = sut.Parse(null).ToString(); @@ -309,7 +338,7 @@ public void Parse_CastActingOnIt() public void Parse_CastStringIntShouldReturnConstantExpression(string expression, object result) { // Arrange - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x")]; var sut = new ExpressionParser(parameters, expression, null, null); // Act @@ -332,7 +361,7 @@ public void Parse_CastStringIntShouldReturnConstantExpression(string expression, public void Parse_NullableShouldReturnNullable(string expression, object resultType, object result) { // Arrange - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x")]; var sut = new ExpressionParser(parameters, expression, null, null); // Act @@ -361,7 +390,8 @@ 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 @@ -394,7 +424,8 @@ public void Parse_When_PrioritizePropertyOrFieldOverTheType_IsFalse(string expre { PrioritizePropertyOrFieldOverTheType = false }; - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "company") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "company") + ]; // Act string parsedExpression; @@ -436,7 +467,7 @@ public void Parse_StringConcat(string expression, string result) public void Parse_InvalidExpressionShouldThrowArgumentException() { // Arrange & Act - Action act = () => DynamicExpressionParser.ParseLambda(ParsingConfig.Default, false, "Properties[\"foo\"] > 2", Array.Empty()); + Action act = () => DynamicExpressionParser.ParseLambda(ParsingConfig.Default, false, "Properties[\"foo\"] > 2", []); // Assert act.Should().Throw().WithMessage("Method 'Compare' not found on type 'System.String' or 'System.Int32'"); From 44ef4fc05b2bf27939576c5d5227044bb818c98b Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Thu, 15 May 2025 12:56:59 +0200 Subject: [PATCH 09/44] Add extra unittests for NullPropagation / ToString / AllowEqualsAndToStringMethodsOnObject is true (#925) * Add extra unittests for NullPropagation / ToString / AllowEqualsAndToStringMethodsOnObject is true * ContainInOrder --- .../Parser/ExpressionHelper.cs | 2 +- .../Parser/ExpressionParserTests.cs | 3 +- .../QueryableTests.Where.cs | 52 +++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs index cc1ef839..05b26969 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs @@ -328,7 +328,7 @@ public bool TryGenerateAndAlsoNotNullExpression(Expression sourceExpression, boo { var expressions = CollectExpressions(addSelf, sourceExpression); - if (expressions.Count == 1 && !(expressions[0] is MethodCallExpression)) + if (expressions.Count == 1 && expressions[0] is not MethodCallExpression) { generatedExpression = sourceExpression; return false; diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs index 7c8d7e43..f6696720 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs @@ -390,8 +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 diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs index 1622a54e..8c3b84d2 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs @@ -389,6 +389,58 @@ public void Where_Dynamic_CompareObjectToInt_ConvertObjectToSupportComparisonIsF act.Should().Throw().And.Message.Should().MatchRegex("The binary operator .* is not defined for the types"); } + [Fact] + public void Where_Dynamic_NullPropagation_Test1_On_NullableDoubleToString_When_AllowEqualsAndToStringMethodsOnObject_True() + { + // Arrange + var config = new ParsingConfig + { + AllowEqualsAndToStringMethodsOnObject = true + }; + var queryable = new[] + { + new { id = "1", d = (double?) null }, + new { id = "2", d = (double?) 5 }, + new { id = "3", d = (double?) 50 }, + new { id = "4", d = (double?) 40 } + }.AsQueryable(); + + // Act + var result = queryable + .Where(config, """np(it.d, 0).ToString().StartsWith("5", StringComparison.OrdinalIgnoreCase)""") + .Select("d") + .ToArray(); + + // Assert + result.Should().ContainInOrder(5, 50); + } + + [Fact] + public void Where_Dynamic_NullPropagation_Test2_On_NullableDoubleToString_When_AllowEqualsAndToStringMethodsOnObject_True() + { + // Arrange + var config = new ParsingConfig + { + AllowEqualsAndToStringMethodsOnObject = true + }; + var queryable = new[] + { + new { id = "1", d = (double?) null }, + new { id = "2", d = (double?) 5 }, + new { id = "3", d = (double?) 50 }, + new { id = "4", d = (double?) 40 } + }.AsQueryable(); + + // Act + var result = queryable + .Where(config, """np(it.d.ToString(), "").StartsWith("5", StringComparison.OrdinalIgnoreCase)""") + .Select("d") + .ToArray(); + + // Assert + result.Should().ContainInOrder(5, 50); + } + [ExcludeFromCodeCoverage] private class PersonWithObject { From 6cd8fbefa7aae4e9515a115cd07a17de2d6e7e7d Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Thu, 15 May 2025 13:21:22 +0200 Subject: [PATCH 10/44] Add validation when passing ParsingConfig in args (#926) --- .../DynamicQueryableExtensions.cs | 67 ++++++++++++++----- .../Validation/Check.cs | 12 ++++ .../ExpressionTests.cs | 4 +- .../QueryableTests.Where.cs | 2 + 4 files changed, 65 insertions(+), 20 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs index 65365eae..c5d52287 100644 --- a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs +++ b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs @@ -125,6 +125,7 @@ public static bool All(this IQueryable source, ParsingConfig config, string pred Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -176,6 +177,7 @@ public static bool Any(this IQueryable source, ParsingConfig config, string pred Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -246,6 +248,7 @@ public static double Average(this IQueryable source, ParsingConfig config, strin Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -399,6 +402,7 @@ public static int Count(this IQueryable source, ParsingConfig config, string pre Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -529,6 +533,7 @@ public static dynamic First(this IQueryable source, ParsingConfig config, string Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -601,6 +606,7 @@ public static dynamic FirstOrDefault(this IQueryable source, ParsingConfig confi Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -679,8 +685,9 @@ internal static IQueryable InternalGroupBy(IQueryable source, ParsingConfig conf { Check.NotNull(source); Check.NotNull(config); - Check.NotEmpty(keySelector, nameof(keySelector)); - Check.NotEmpty(resultSelector, nameof(resultSelector)); + Check.NotEmpty(keySelector); + Check.NotEmpty(resultSelector); + Check.Args(args); bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression keyLambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, keySelector, args); @@ -807,7 +814,8 @@ internal static IQueryable InternalGroupBy(IQueryable source, ParsingConfig conf { Check.NotNull(source); Check.NotNull(config); - Check.NotEmpty(keySelector, nameof(keySelector)); + Check.NotEmpty(keySelector); + Check.Args(args); bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression keyLambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, keySelector, args); @@ -932,12 +940,13 @@ private static IEnumerable GroupByManyInternal(IEnumerabl /// An obtained by performing a grouped join on two sequences. public static IQueryable GroupJoin(this IQueryable outer, ParsingConfig config, IEnumerable inner, string outerKeySelector, string innerKeySelector, string resultSelector, params object?[] args) { - Check.NotNull(outer, nameof(outer)); + Check.NotNull(outer); Check.NotNull(config); - Check.NotNull(inner, nameof(inner)); - Check.NotEmpty(outerKeySelector, nameof(outerKeySelector)); - Check.NotEmpty(innerKeySelector, nameof(innerKeySelector)); - Check.NotEmpty(resultSelector, nameof(resultSelector)); + Check.NotNull(inner); + Check.NotEmpty(outerKeySelector); + Check.NotEmpty(innerKeySelector); + Check.NotEmpty(resultSelector); + Check.Args(args); Type outerType = outer.ElementType; Type innerType = inner.AsQueryable().ElementType; @@ -989,12 +998,13 @@ public static IQueryable Join(this IQueryable outer, ParsingConfig config, IEnum { //http://stackoverflow.com/questions/389094/how-to-create-a-dynamic-linq-join-extension-method - Check.NotNull(outer, nameof(outer)); + Check.NotNull(outer); Check.NotNull(config); - Check.NotNull(inner, nameof(inner)); - Check.NotEmpty(outerKeySelector, nameof(outerKeySelector)); - Check.NotEmpty(innerKeySelector, nameof(innerKeySelector)); - Check.NotEmpty(resultSelector, nameof(resultSelector)); + Check.NotNull(inner); + Check.NotEmpty(outerKeySelector); + Check.NotEmpty(innerKeySelector); + Check.NotEmpty(resultSelector); + Check.Args(args); Type outerType = outer.ElementType; Type innerType = inner.AsQueryable().ElementType; @@ -1094,6 +1104,7 @@ public static dynamic Last(this IQueryable source, ParsingConfig config, string Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -1166,6 +1177,7 @@ public static dynamic LastOrDefault(this IQueryable source, ParsingConfig config Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -1244,6 +1256,7 @@ public static long LongCount(this IQueryable source, ParsingConfig config, strin Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -1316,6 +1329,7 @@ public static object Max(this IQueryable source, ParsingConfig config, string pr Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, typeof(object), predicate, args); @@ -1388,6 +1402,7 @@ public static object Min(this IQueryable source, ParsingConfig config, string pr Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, typeof(object), predicate, args); @@ -1545,6 +1560,8 @@ public static IOrderedQueryable OrderBy(this IQueryable public static IOrderedQueryable OrderBy(this IQueryable source, ParsingConfig config, string ordering, params object?[] args) { + Check.Args(args); + if (args.Length > 0 && args[0] != null && args[0]!.GetType().GetInterfaces().Any(i => i.Name.Contains("IComparer`1"))) { return InternalOrderBy(source, config, ordering, args[0]!, args); @@ -1584,6 +1601,7 @@ internal static IOrderedQueryable InternalOrderBy(IQueryable source, ParsingConf Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(ordering); + Check.Args(args); ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(source.ElementType, string.Empty, config.RenameEmptyParameterExpressionNames)]; var parser = new ExpressionParser(parameters, ordering, args, config, true); @@ -1758,6 +1776,7 @@ public static IQueryable Select(this IQueryable source, ParsingConfig config, st Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(selector); + Check.Args(args); bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, selector, args); @@ -1799,6 +1818,7 @@ public static IQueryable Select(this IQueryable source, Parsin Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(selector); + Check.Args(args); bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, typeof(TResult), selector, args); @@ -1841,8 +1861,9 @@ public static IQueryable Select(this IQueryable source, ParsingConfig config, Ty { Check.NotNull(source); Check.NotNull(config); - Check.NotNull(resultType, nameof(resultType)); + Check.NotNull(resultType); Check.NotEmpty(selector); + Check.Args(args); bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, resultType, selector, args); @@ -1907,6 +1928,7 @@ public static IQueryable SelectMany(this IQueryable source, ParsingConfig config Check.NotNull(config); Check.NotNull(resultType); Check.NotEmpty(selector); + Check.Args(args); return SelectManyInternal(source, config, resultType, selector, args); } @@ -1978,6 +2000,7 @@ public static IQueryable SelectMany(this IQueryable source, Pa Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(selector); + Check.Args(args); bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, selector, args); @@ -2076,10 +2099,12 @@ public static IQueryable SelectMany( { Check.NotNull(source); Check.NotNull(config); - Check.NotEmpty(collectionSelector, nameof(collectionSelector)); - Check.NotEmpty(collectionParameterName, nameof(collectionParameterName)); - Check.NotEmpty(resultSelector, nameof(resultSelector)); - Check.NotEmpty(resultParameterName, nameof(resultParameterName)); + Check.NotEmpty(collectionSelector); + Check.NotEmpty(collectionParameterName); + Check.NotEmpty(resultSelector); + Check.NotEmpty(resultParameterName); + Check.Args(collectionSelectorArgs); + Check.Args(resultSelectorArgs); bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression sourceSelectLambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, collectionSelector, collectionSelectorArgs); @@ -2227,6 +2252,7 @@ public static dynamic SingleOrDefault(this IQueryable source, ParsingConfig conf Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -2309,6 +2335,7 @@ public static IQueryable SkipWhile(this IQueryable source, ParsingConfig config, Check.NotNull(source); Check.NotNull(config); Check.NotNull(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -2365,6 +2392,7 @@ public static object Sum(this IQueryable source, ParsingConfig config, string pr Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -2439,6 +2467,7 @@ public static IQueryable TakeWhile(this IQueryable source, ParsingConfig config, Check.NotNull(source); Check.NotNull(config); Check.NotNull(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -2553,6 +2582,7 @@ internal static IOrderedQueryable InternalThenBy(IOrderedQueryable source, Parsi Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(ordering); + Check.Args(args); ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(source.ElementType, string.Empty, config.RenameEmptyParameterExpressionNames) }; ExpressionParser parser = new ExpressionParser(parameters, ordering, args, config); @@ -2649,6 +2679,7 @@ public static IQueryable Where(this IQueryable source, ParsingConfig config, str Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); diff --git a/src/System.Linq.Dynamic.Core/Validation/Check.cs b/src/System.Linq.Dynamic.Core/Validation/Check.cs index 00994c89..e0bbb32b 100644 --- a/src/System.Linq.Dynamic.Core/Validation/Check.cs +++ b/src/System.Linq.Dynamic.Core/Validation/Check.cs @@ -8,6 +8,18 @@ namespace System.Linq.Dynamic.Core.Validation; [DebuggerStepThrough] internal static class Check { + private const string ParsingConfigError = "The ParsingConfig should be provided as first argument to this method."; + + public static object?[]? Args(object?[]? args, [CallerArgumentExpression("args")] string? parameterName = null) + { + if (args?.Any(a => a is ParsingConfig) == true) + { + throw new ArgumentException(ParsingConfigError, parameterName); + } + + return args; + } + public static T Condition(T value, Predicate predicate, [CallerArgumentExpression("value")] string? parameterName = null) { NotNull(predicate); diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs index 6ff98396..d71845ea 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs @@ -1281,8 +1281,8 @@ public void ExpressionTests_In_Enum() // Act var expected = qry.Where(x => new[] { TestEnum.Var1, TestEnum.Var2 }.Contains(x.TestEnum)).ToArray(); - var result1 = qry.Where("it.TestEnum in (\"Var1\", \"Var2\")", config).ToArray(); - var result2 = qry.Where("it.TestEnum in (0, 1)", config).ToArray(); + var result1 = qry.Where(config, "it.TestEnum in (\"Var1\", \"Var2\")").ToArray(); + var result2 = qry.Where(config, "it.TestEnum in (0, 1)").ToArray(); // Assert Check.That(result1).ContainsExactly(expected); diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs index 8c3b84d2..22d22a72 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs @@ -155,6 +155,8 @@ 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); } [Fact] From 4cc5d17e72676d9583ec1ed32f1497d127db85ac Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Mon, 19 May 2025 17:01:39 +0200 Subject: [PATCH 11/44] v1.6.4 --- CHANGELOG.md | 8 ++++++++ Generate-ReleaseNotes.bat | 2 +- version.xml | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19a7a995..41cf8a02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# v1.6.4 (19 May 2025) +- [#915](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/915) - Add support for "not in" and "not_in" [feature] contributed by [StefH](https://github.com/StefH) +- [#923](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/923) - Fix MethodFinder TryFindAggregateMethod to support array [bug] contributed by [StefH](https://github.com/StefH) +- [#925](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/925) - Add extra unittests for NullPropagation / ToString / AllowEqualsAndToStringMethodsOnObject is true [test] contributed by [StefH](https://github.com/StefH) +- [#926](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/926) - Add validation when passing ParsingConfig in args [feature] contributed by [StefH](https://github.com/StefH) +- [#914](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/914) - Add support for "not in" [feature] +- [#919](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/919) - Calling Sum in a Sum throws an InvalidOperationException [bug] + # v1.6.3 (09 May 2025) - [#922](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/922) - Update DynamicGetMemberBinder to only add BindingRestrictions for dynamic type and cache the DynamicMetaObject [bug] contributed by [StefH](https://github.com/StefH) - [#921](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/921) - Strange Performance issue after upgrading from 1.6.0.2 to 1.6.2 [bug] diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index 3b983077..d81555a9 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.6.3 +SET version=v1.6.4 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 9fefde31..e265d039 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 3 + 4 \ No newline at end of file From 81cdc845d4000e80ec948f46e28fdc0b98e1805e Mon Sep 17 00:00:00 2001 From: Renan Carlos Pereira Date: Sun, 25 May 2025 10:56:21 +0100 Subject: [PATCH 12/44] Fix: Add Fallback in ExpressionPromoter to Handle Cache Cleanup in ConstantExpressionHelper (#905) * include ParseRealLiteral tests * Change ParseRealLiteral to remove literals before parsing * Merge master * NumberParserTests to split the tests NumberParser_Parse[Type]Literal * Fixing race condition * fix enum tests * Update to set instance via reflection * Update ExpressionPromoterTests.cs * using IDynamicLinqCustomTypeProvider --------- Co-authored-by: Renan Pereira --- .../Parser/ExpressionPromoter.cs | 9 +- .../Parser/ExpressionPromoterTests.cs | 148 +++++++++++++----- 2 files changed, 119 insertions(+), 38 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs index 43f7acbf..088b755e 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs @@ -50,7 +50,12 @@ public ExpressionPromoter(ParsingConfig config) } else { - if (_constantExpressionHelper.TryGetText(ce, out var text)) + if (!_constantExpressionHelper.TryGetText(ce, out var text)) + { + text = ce.Value?.ToString(); + } + + if (text != null) { Type target = TypeHelper.GetNonNullableType(type); object? value = null; @@ -67,7 +72,7 @@ public ExpressionPromoter(ParsingConfig config) // Make sure an enum value stays an enum value if (target.IsEnum) { - value = Enum.ToObject(target, value!); + TypeHelper.TryParseEnum(text, target, out value); } break; diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionPromoterTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionPromoterTests.cs index 267ea999..bcbc370b 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionPromoterTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionPromoterTests.cs @@ -1,56 +1,132 @@ -using Moq; +using FluentAssertions; +using Moq; using System.Collections.Generic; using System.Linq.Dynamic.Core.CustomTypeProviders; using System.Linq.Dynamic.Core.Parser; using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Xunit; -namespace System.Linq.Dynamic.Core.Tests.Parser; - -public class ExpressionPromoterTests +namespace System.Linq.Dynamic.Core.Tests.Parser { - public class SampleDto + public class ExpressionPromoterTests { - public Guid Data { get; set; } - } + public class SampleDto + { + public Guid data { get; set; } + } - private readonly Mock _expressionPromoterMock; - private readonly Mock _dynamicLinkCustomTypeProviderMock; + private readonly Mock _expressionPromoterMock; + private readonly Mock _dynamicLinqCustomTypeProviderMock; - public ExpressionPromoterTests() - { - _dynamicLinkCustomTypeProviderMock = new Mock(); - _dynamicLinkCustomTypeProviderMock.Setup(d => d.GetCustomTypes()).Returns(new HashSet()); - _dynamicLinkCustomTypeProviderMock.Setup(d => d.ResolveType(It.IsAny())).Returns(typeof(SampleDto)); + public ExpressionPromoterTests() + { + _dynamicLinqCustomTypeProviderMock = new Mock(); + _dynamicLinqCustomTypeProviderMock.Setup(d => d.GetCustomTypes()).Returns(new HashSet()); + _dynamicLinqCustomTypeProviderMock.Setup(d => d.ResolveType(It.IsAny())).Returns(typeof(SampleDto)); + + _expressionPromoterMock = new Mock(); + _expressionPromoterMock.Setup(e => e.Promote(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Expression.Constant(Guid.NewGuid())); + } + + [Fact] + public void DynamicExpressionParser_ParseLambda_WithCustomExpressionPromoter() + { + // Assign + var parsingConfig = new ParsingConfig() + { + AllowNewToEvaluateAnyType = true, + CustomTypeProvider = _dynamicLinqCustomTypeProviderMock.Object, + ExpressionPromoter = _expressionPromoterMock.Object + }; + + // Act + string query = $"new {typeof(SampleDto).FullName}(@0 as data)"; + LambdaExpression expression = DynamicExpressionParser.ParseLambda(parsingConfig, null, query, new object[] { Guid.NewGuid().ToString() }); + Delegate del = expression.Compile(); + SampleDto result = (SampleDto)del.DynamicInvoke(); + + // Assert + Assert.NotNull(result); + + // Verify + _dynamicLinqCustomTypeProviderMock.Verify(d => d.GetCustomTypes(), Times.Once); + _dynamicLinqCustomTypeProviderMock.Verify(d => d.ResolveType($"{typeof(SampleDto).FullName}"), Times.Once); + + _expressionPromoterMock.Verify(e => e.Promote(It.IsAny(), typeof(Guid), true, true), Times.Once); + } - _expressionPromoterMock = new Mock(); - _expressionPromoterMock.Setup(e => e.Promote(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Expression.Constant(Guid.NewGuid())); + [Fact] + public async Task Promote_Should_Succeed_Even_When_LiteralsCache_Is_Cleaned() + { + // Arrange + var parsingConfig = new ParsingConfig() + { + ConstantExpressionCacheConfig = new Core.Util.Cache.CacheConfig + { + CleanupFrequency = TimeSpan.FromMilliseconds(500), // Run cleanup more often + TimeToLive = TimeSpan.FromMilliseconds(500), // Shorten TTL to force expiration + ReturnExpiredItems = false + } + }; + + // because the field is static only one process is setting the field, + // we need a way to set up because the instance is private we are not able to overwrite the configuration. + ConstantExpressionHelperReflection.Initiate(parsingConfig); + + var constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(parsingConfig); + var expressionPromoter = new ExpressionPromoter(parsingConfig); + + double value = 0.40; + string text = "0.40"; + Type targetType = typeof(decimal); + + // Step 1: Add constant to cache + var literalExpression = constantExpressionHelper.CreateLiteral(value, text); + Assert.NotNull(literalExpression); // Ensure it was added + + // Step 2: Manually trigger cleanup + var cts = new CancellationTokenSource(500); + await Task.Run(async () => + { + while (!cts.IsCancellationRequested) + { + constantExpressionHelper.TryGetText(literalExpression, out _); + await Task.Delay(50); // Give some time for cleanup to be triggered + } + }); + + // Ensure some cleanup cycles have passed + await Task.Delay(500); // Allow cache cleanup to happen + + // Step 3: Attempt to promote the expression after cleanup + var promotedExpression = expressionPromoter.Promote(literalExpression, targetType, exact: false, true); + + // Assert: Promotion should still work even if the cache was cleaned + promotedExpression.Should().NotBeNull(); // Ensure `Promote()` still returns a valid expression + } } - [Fact] - public void DynamicExpressionParser_ParseLambda_WithCustomExpressionPromoter() + public static class ConstantExpressionHelperReflection { - // Assign - var parsingConfig = new ParsingConfig() + private static readonly Type _constantExpressionHelperFactoryType; + + static ConstantExpressionHelperReflection() { - AllowNewToEvaluateAnyType = true, - CustomTypeProvider = _dynamicLinkCustomTypeProviderMock.Object, - ExpressionPromoter = _expressionPromoterMock.Object - }; + var assembly = Assembly.GetAssembly(typeof(DynamicClass))!; - // Act - string query = $"new {typeof(SampleDto).FullName}(@0 as Data)"; - LambdaExpression expression = DynamicExpressionParser.ParseLambda(parsingConfig, null, query, Guid.NewGuid().ToString()); - Delegate del = expression.Compile(); - SampleDto result = (SampleDto)del.DynamicInvoke(); + _constantExpressionHelperFactoryType = assembly.GetType("System.Linq.Dynamic.Core.Parser.ConstantExpressionHelperFactory")!; + } - // Assert - Assert.NotNull(result); + public static void Initiate(ParsingConfig parsingConfig) + { + var instance = new ConstantExpressionHelper(parsingConfig); - // Verify - _dynamicLinkCustomTypeProviderMock.Verify(d => d.GetCustomTypes(), Times.Once); - _dynamicLinkCustomTypeProviderMock.Verify(d => d.ResolveType($"{typeof(SampleDto).FullName}"), Times.Once); + var field = _constantExpressionHelperFactoryType.GetField("_instance", BindingFlags.NonPublic | BindingFlags.Static); - _expressionPromoterMock.Verify(e => e.Promote(It.IsAny(), typeof(Guid), true, true), Times.Once); + field?.SetValue(field, instance); + } } -} \ No newline at end of file +} From 9ea3bbb28a1f2e108b4f90e95e77bfc455f1a861 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 28 May 2025 15:08:46 +0200 Subject: [PATCH 13/44] v1.6.5 --- CHANGELOG.md | 4 ++++ Generate-ReleaseNotes.bat | 2 +- version.xml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cf8a02..a9d9c232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# v1.6.5 (28 May 2025) +- [#905](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/905) - Fix: Add Fallback in ExpressionPromoter to Handle Cache Cleanup in ConstantExpressionHelper [bug] contributed by [RenanCarlosPereira](https://github.com/RenanCarlosPereira) +- [#904](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/904) - Race Condition in ConstantExpressionHelper Causing Parsing Failures [bug] + # v1.6.4 (19 May 2025) - [#915](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/915) - Add support for "not in" and "not_in" [feature] contributed by [StefH](https://github.com/StefH) - [#923](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/923) - Fix MethodFinder TryFindAggregateMethod to support array [bug] contributed by [StefH](https://github.com/StefH) diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index d81555a9..cf49f246 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.6.4 +SET version=v1.6.5 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 e265d039..1e9ea2a1 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 4 + 5 \ No newline at end of file From 49b42af1da430c0b9f618649c0a598f99b693783 Mon Sep 17 00:00:00 2001 From: Jonathan Magnan Date: Sun, 1 Jun 2025 09:59:39 -0500 Subject: [PATCH 14/44] Add files via upload --- dapper-plus-sponsor.png | Bin 0 -> 10951 bytes entity-framework-extensions-sponsor.png | Bin 0 -> 10721 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 dapper-plus-sponsor.png create mode 100644 entity-framework-extensions-sponsor.png diff --git a/dapper-plus-sponsor.png b/dapper-plus-sponsor.png new file mode 100644 index 0000000000000000000000000000000000000000..d6233cf194c7b9026e19b2dab89b04f2cce98d89 GIT binary patch literal 10951 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 LQmm7I8S%dW>`dr_ literal 0 HcmV?d00001 diff --git a/entity-framework-extensions-sponsor.png b/entity-framework-extensions-sponsor.png new file mode 100644 index 0000000000000000000000000000000000000000..81ed135910c2ef18767a3b739e503c07d176ed65 GIT binary patch literal 10721 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 t;~4gHEV Date: Sun, 1 Jun 2025 11:01:28 -0400 Subject: [PATCH 15/44] Update README.md --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 502653a0..a1c7b643 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,8 @@ -### Library Powered By - -This library is powered by [Entity Framework Extensions](https://entityframework-extensions.net/?z=github&y=system.linq.dynamic.core) - - - -Entity Framework Extensions - - - ---- - # System.Linq.Dynamic.Core This is a **.NET Core / Standard port** of the Microsoft assembly for the .Net 4.0 Dynamic language functionality. +--- + ## Overview With this library it's possible to write Dynamic LINQ queries (string based) on an `IQueryable`: ``` c# @@ -32,6 +22,18 @@ db.Customers.WhereInterpolated($"City == {cityName} and Orders.Count >= {c}"); --- +## Sponsors + +ZZZ Projects owns and maintains **System.Linq.Dynamic.Core** as part of our [mission](https://zzzprojects.com/mission) to add value to the .NET community + +Through [Entity Framework Extensions](https://entityframework-extensions.net/?utm_source=zzzprojects&utm_medium=systemlinqdynamiccore) and [Dapper Plus](https://dapper-plus.net/?utm_source=zzzprojects&utm_medium=systemlinqdynamiccore), we actively sponsor and help key open-source libraries grow. + +[![Entity Framework Extensions](https://raw.githubusercontent.com/zzzprojects/System.Linq.Dynamic.Core/master/entity-framework-extensions-sponsor.png)](https://entityframework-extensions.net/bulk-insert?utm_source=zzzprojects&utm_medium=systemlinqdynamiccore) + +[![Dapper Plus](https://raw.githubusercontent.com/zzzprojects/System.Linq.Dynamic.Core/master/dapper-plus-sponsor.png)](https://dapper-plus.net/bulk-insert?utm_source=zzzprojects&utm_medium=systemlinqdynamiccore) + +--- + ## :exclamation: Breaking changes ### v1.3.0 From 3208964fa49477d42ea3ce203b11cdf47f267f78 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 6 Jun 2025 20:41:20 +0200 Subject: [PATCH 16/44] Add GroupBy method for Z.DynamicLinq.SystemTextJson and Z.DynamicLinq.NewtonsoftJson (#929) * Add GroupBy method for Z.DynamicLinq.SystemTextJson and Z.DynamicLinq.NewtonsoftJson * . --- .../NewtonsoftJsonExtensions.cs | 49 +++++++++- .../SystemTextJsonExtensions.cs | 49 +++++++++- .../NewtonsoftJsonTests.cs | 82 ++++++++++++++++ .../SystemTextJsonTests.cs | 98 ++++++++++++++++++- 4 files changed, 271 insertions(+), 7 deletions(-) diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs index 89a6806d..7ca9d6e3 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs @@ -2,6 +2,7 @@ using System.Linq.Dynamic.Core.NewtonsoftJson.Config; using System.Linq.Dynamic.Core.NewtonsoftJson.Extensions; using System.Linq.Dynamic.Core.Validation; +using JetBrains.Annotations; using Newtonsoft.Json.Linq; namespace System.Linq.Dynamic.Core.NewtonsoftJson; @@ -303,6 +304,42 @@ public static JToken First(this JArray source, string predicate, params object?[ } #endregion FirstOrDefault + #region GroupBy + /// + /// Groups the elements of a sequence according to a specified key string function + /// and creates a result value from each group and its key. + /// + /// A whose elements to group. + /// A string expression to specify the key for each element. + /// An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings. + /// A where each element represents a projection over a group and its key. + [PublicAPI] + public static JArray GroupBy(this JArray source, string keySelector, params object[]? args) + { + return GroupBy(source, NewtonsoftJsonParsingConfig.Default, keySelector, args); + } + + /// + /// Groups the elements of a sequence according to a specified key string function + /// and creates a result value from each group and its key. + /// + /// A whose elements to group. + /// The . + /// A string expression to specify the key for each element. + /// An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings. + /// A where each element represents a projection over a group and its key. + [PublicAPI] + public static JArray GroupBy(this JArray source, NewtonsoftJsonParsingConfig config, string keySelector, params object[]? args) + { + Check.NotNull(source); + Check.NotNull(config); + Check.NotNullOrEmpty(keySelector); + + var queryable = ToQueryable(source, config); + return ToJArray(() => queryable.GroupBy(config, keySelector, args)); + } + #endregion + #region Last /// /// Returns the last element of a sequence that satisfies a specified condition. @@ -813,7 +850,17 @@ private static JArray ToJArray(Func func) var array = new JArray(); foreach (var dynamicElement in func()) { - var element = dynamicElement is DynamicClass dynamicClass ? JObject.FromObject(dynamicClass) : dynamicElement; + var element = dynamicElement switch + { + IGrouping grouping => new JObject + { + [nameof(grouping.Key)] = JToken.FromObject(grouping.Key), + ["Values"] = ToJArray(grouping.AsQueryable) + }, + DynamicClass dynamicClass => JObject.FromObject(dynamicClass), + _ => dynamicElement + }; + array.Add(element); } diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs index a14a468e..8d1671a0 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs @@ -5,6 +5,7 @@ using System.Linq.Dynamic.Core.SystemTextJson.Utils; using System.Linq.Dynamic.Core.Validation; using System.Text.Json; +using JetBrains.Annotations; namespace System.Linq.Dynamic.Core.SystemTextJson; @@ -371,6 +372,42 @@ public static JsonElement First(this JsonDocument source, string predicate, para } #endregion FirstOrDefault + #region GroupBy + /// + /// Groups the elements of a sequence according to a specified key string function + /// and creates a result value from each group and its key. + /// + /// A whose elements to group. + /// A string expression to specify the key for each element. + /// An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings. + /// A where each element represents a projection over a group and its key. + [PublicAPI] + public static JsonDocument GroupBy(this JsonDocument source, string keySelector, params object[]? args) + { + return GroupBy(source, SystemTextJsonParsingConfig.Default, keySelector, args); + } + + /// + /// Groups the elements of a sequence according to a specified key string function + /// and creates a result value from each group and its key. + /// + /// A whose elements to group. + /// The . + /// A string expression to specify the key for each element. + /// An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings. + /// A where each element represents a projection over a group and its key. + [PublicAPI] + public static JsonDocument GroupBy(this JsonDocument source, SystemTextJsonParsingConfig config, string keySelector, params object[]? args) + { + Check.NotNull(source); + Check.NotNull(config); + Check.NotNullOrEmpty(keySelector); + + var queryable = ToQueryable(source, config); + return ToJsonDocumentArray(() => queryable.GroupBy(config, keySelector, args)); + } + #endregion + #region Last /// /// Returns the last element of a sequence. @@ -1037,7 +1074,17 @@ private static JsonDocument ToJsonDocumentArray(Func func) var array = new List(); foreach (var dynamicElement in func()) { - array.Add(ToJsonElement(dynamicElement)); + var element = dynamicElement switch + { + IGrouping grouping => ToJsonElement(new + { + Key = ToJsonElement(grouping.Key), + Values = ToJsonDocumentArray(grouping.AsQueryable).RootElement + }), + _ => ToJsonElement(dynamicElement) + }; + + array.Add(element); } return JsonDocumentUtils.FromObject(array); diff --git a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs index dafa0373..2e94a212 100644 --- a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs @@ -168,6 +168,88 @@ public void FirstOrDefault() _source.FirstOrDefault("Age > 999").Should().BeNull(); } + [Fact] + public void GroupBySimpleKeySelector() + { + // Arrange + var json = + """ + [ + { + "Name": "Mr. Test Smith", + "Type": "PAY", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + }, + { + "Name": "Mr. Test Smith", + "Type": "DISPATCH", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + }, + { + "Name": "Different Name", + "Type": "PAY", + "Something": { + "Field1": "Test3", + "Field2": "Test4" + } + } + ] + """; + var source = JArray.Parse(json); + + // Act + var resultAsJson = source.GroupBy("Type").ToString(); + + // Assert + var expected = + """ + [ + { + "Key": "PAY", + "Values": [ + { + "Name": "Mr. Test Smith", + "Type": "PAY", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + }, + { + "Name": "Different Name", + "Type": "PAY", + "Something": { + "Field1": "Test3", + "Field2": "Test4" + } + } + ] + }, + { + "Key": "DISPATCH", + "Values": [ + { + "Name": "Mr. Test Smith", + "Type": "DISPATCH", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + } + ] + } + ] + """; + + resultAsJson.Should().Be(expected); + } + [Fact] public void Last() { diff --git a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs index 7041ad7c..f5dee221 100644 --- a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs @@ -6,6 +6,11 @@ namespace System.Linq.Dynamic.Core.SystemTextJson.Tests; public class SystemTextJsonTests { + private static readonly JsonSerializerOptions _options = new() + { + WriteIndented = true + }; + private const string ExampleJsonObjectArray = """ [ @@ -142,7 +147,7 @@ public void Distinct() } ] """; - var source = JsonDocument.Parse(json); + using var source = JsonDocument.Parse(json); // Act var result = source.Select("Name").Distinct(); @@ -174,6 +179,89 @@ public void FirstOrDefault() _source.FirstOrDefault("Age > 999").Should().BeNull(); } + [Fact] + public void GroupBySimpleKeySelector() + { + // Arrange + var json = + """ + [ + { + "Name": "Mr. Test Smith", + "Type": "PAY", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + }, + { + "Name": "Mr. Test Smith", + "Type": "DISPATCH", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + }, + { + "Name": "Different Name", + "Type": "PAY", + "Something": { + "Field1": "Test3", + "Field2": "Test4" + } + } + ] + """; + using var source = JsonDocument.Parse(json); + + // Act + var result = source.GroupBy("Type"); + var resultAsJson = JsonSerializer.Serialize(result, _options); + + // Assert + var expected = + """ + [ + { + "Key": "PAY", + "Values": [ + { + "Name": "Mr. Test Smith", + "Type": "PAY", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + }, + { + "Name": "Different Name", + "Type": "PAY", + "Something": { + "Field1": "Test3", + "Field2": "Test4" + } + } + ] + }, + { + "Key": "DISPATCH", + "Values": [ + { + "Name": "Mr. Test Smith", + "Type": "DISPATCH", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + } + ] + } + ] + """; + + resultAsJson.Should().Be(expected); + } + [Fact] public void Last() { @@ -265,7 +353,7 @@ public void OrderBy_Multiple() } ] """; - var source = JsonDocument.Parse(json); + using var source = JsonDocument.Parse(json); // Act var result = source.OrderBy("Age, Name").Select("Name"); @@ -279,7 +367,7 @@ public void OrderBy_Multiple() public void Page() { var json = "[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]"; - var source = JsonDocument.Parse(json); + using var source = JsonDocument.Parse(json); // Act var result = source.Page(2, 3); @@ -293,7 +381,7 @@ public void Page() public void PageResult() { var json = "[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]"; - var source = JsonDocument.Parse(json); + using var source = JsonDocument.Parse(json); // Act var pagedResult = source.PageResult(2, 3); @@ -339,7 +427,7 @@ public void SelectMany() ] }] """; - var source = JsonDocument.Parse(json); + using var source = JsonDocument.Parse(json); // Act var result = source From c7fc9be68a53bcaf687e8abeb2c91be2ed86d0c9 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 11 Jun 2025 11:08:10 +0200 Subject: [PATCH 17/44] Fix "in" for nullable Enums (#932) --- .../Parser/ExpressionParser.cs | 4 +-- .../ExpressionTests.cs | 28 +++++++++++++++---- .../Helpers/Models/ModelWithEnum.cs | 14 +++++----- ...ts.UseParameterizedNamesInDynamicQuery.cs} | 0 4 files changed, 31 insertions(+), 15 deletions(-) rename test/System.Linq.Dynamic.Core.Tests/{QueryableTests.UseParameterizedNamesInDynamicQuery .cs => QueryableTests.UseParameterizedNamesInDynamicQuery.cs} (100%) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index b4a7245f..eba7fffc 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -374,8 +374,8 @@ private Expression ParseIn() // we need to parse unary expressions because otherwise 'in' clause will fail in use cases like 'in (-1, -1)' or 'in (!true)' Expression right = ParseUnary(); - // if the identifier is an Enum, try to convert the right-side also to an Enum. - if (left.Type.GetTypeInfo().IsEnum) + // if the identifier is an Enum (or nullable Enum), try to convert the right-side also to an Enum. + if (TypeHelper.GetNonNullableType(left.Type).GetTypeInfo().IsEnum) { if (right is ConstantExpression constantExprRight) { diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs index d71845ea..021183a5 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs @@ -1269,10 +1269,6 @@ public void ExpressionTests_HexadecimalInteger() [Fact] public void ExpressionTests_In_Enum() { - var config = new ParsingConfig(); -#if NETSTANDARD - // config.CustomTypeProvider = new NetStandardCustomTypeProvider(); -#endif // Arrange var model1 = new ModelWithEnum { TestEnum = TestEnum.Var1 }; var model2 = new ModelWithEnum { TestEnum = TestEnum.Var2 }; @@ -1281,8 +1277,28 @@ public void ExpressionTests_In_Enum() // Act var expected = qry.Where(x => new[] { TestEnum.Var1, TestEnum.Var2 }.Contains(x.TestEnum)).ToArray(); - var result1 = qry.Where(config, "it.TestEnum in (\"Var1\", \"Var2\")").ToArray(); - var result2 = qry.Where(config, "it.TestEnum in (0, 1)").ToArray(); + var result1 = qry.Where("it.TestEnum in (\"Var1\", \"Var2\")").ToArray(); + var result2 = qry.Where("it.TestEnum in (0, 1)").ToArray(); + + // Assert + Check.That(result1).ContainsExactly(expected); + Check.That(result2).ContainsExactly(expected); + } + + [Fact] + public void ExpressionTests_In_EnumIsNullable() + { + // Arrange + var model1 = new ModelWithEnum { TestEnumNullable = TestEnum.Var1 }; + var model2 = new ModelWithEnum { TestEnumNullable = TestEnum.Var2 }; + var model3 = new ModelWithEnum { TestEnumNullable = TestEnum.Var3 }; + var model4 = new ModelWithEnum { TestEnumNullable = null }; + var qry = new[] { model1, model2, model3, model4 }.AsQueryable(); + + // Act + var expected = new[] { model1, model2 }; + var result1 = qry.Where("it.TestEnumNullable in (\"Var1\", \"Var2\")").ToArray(); + var result2 = qry.Where("it.TestEnumNullable in (0, 1)").ToArray(); // Assert Check.That(result1).ContainsExactly(expected); diff --git a/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/ModelWithEnum.cs b/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/ModelWithEnum.cs index 415997a6..3a54b692 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/ModelWithEnum.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/ModelWithEnum.cs @@ -1,10 +1,10 @@ - -namespace System.Linq.Dynamic.Core.Tests.Helpers.Models +namespace System.Linq.Dynamic.Core.Tests.Helpers.Models; + +public class ModelWithEnum { - public class ModelWithEnum - { - public string Name { get; set; } + public string Name { get; set; } = null!; + + public TestEnum TestEnum { get; set; } - public TestEnum TestEnum { get; set; } - } + public TestEnum? TestEnumNullable { get; set; } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.UseParameterizedNamesInDynamicQuery .cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.UseParameterizedNamesInDynamicQuery.cs similarity index 100% rename from test/System.Linq.Dynamic.Core.Tests/QueryableTests.UseParameterizedNamesInDynamicQuery .cs rename to test/System.Linq.Dynamic.Core.Tests/QueryableTests.UseParameterizedNamesInDynamicQuery.cs From 19da876cc2254fc0e0bbf680686fcdf8cff67936 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 11 Jun 2025 12:37:09 +0200 Subject: [PATCH 18/44] v1.6.6 --- CHANGELOG.md | 5 +++++ Generate-ReleaseNotes.bat | 2 +- version.xml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9d9c232..2c1819e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 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) +- [#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] + # v1.6.5 (28 May 2025) - [#905](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/905) - Fix: Add Fallback in ExpressionPromoter to Handle Cache Cleanup in ConstantExpressionHelper [bug] contributed by [RenanCarlosPereira](https://github.com/RenanCarlosPereira) - [#904](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/904) - Race Condition in ConstantExpressionHelper Causing Parsing Failures [bug] diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index cf49f246..d2da800c 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.6.5 +SET version=v1.6.6 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 1e9ea2a1..80460ce5 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 5 + 6 \ No newline at end of file From dd7267c5786123d6b082fa7f4859d414597b3ff0 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Mon, 28 Jul 2025 16:23:56 +0200 Subject: [PATCH 19/44] Use TryConvertTypes also for strings (#938) --- .../Parser/ExpressionHelper.cs | 40 ++++++------- .../Parser/ExpressionParser.cs | 8 ++- .../Parser/IExpressionHelper.cs | 5 ++ .../QueryableTests.Where.cs | 56 +++++++++++++++++++ 4 files changed, 87 insertions(+), 22 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs index 05b26969..7b0167d8 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs @@ -378,6 +378,26 @@ public Expression ConvertAnyArrayToObjectArray(Expression arrayExpression) ); } + /// + public bool TryConvertTypes(ref Expression left, ref Expression right) + { + if (!_parsingConfig.ConvertObjectToSupportComparison || left.Type == right.Type || Constants.IsNull(left) || Constants.IsNull(right)) + { + return false; + } + + if (left.Type == typeof(object)) + { + left = Expression.Convert(left, right.Type); + } + else if (right.Type == typeof(object)) + { + right = Expression.Convert(right, left.Type); + } + + return true; + } + private Expression? GetMemberExpression(Expression? expression) { if (ExpressionQualifiesForNullPropagation(expression)) @@ -455,26 +475,6 @@ private List CollectExpressions(bool addSelf, Expression sourceExpre return list; } - /// - /// If the types are different (and not null), try to convert the object type to other type. - /// - private void TryConvertTypes(ref Expression left, ref Expression right) - { - if (!_parsingConfig.ConvertObjectToSupportComparison || left.Type == right.Type || Constants.IsNull(left) || Constants.IsNull(right)) - { - return; - } - - if (left.Type == typeof(object)) - { - left = Expression.Convert(left, right.Type); - } - else if (right.Type == typeof(object)) - { - right = Expression.Convert(right, left.Type); - } - } - private static Expression GenerateStaticMethodCall(string methodName, Expression left, Expression right) { if (!TryGetStaticMethod(methodName, left, right, out var methodInfo)) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index eba7fffc..a43b534e 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -527,7 +527,11 @@ private Expression ParseComparisonOperator() // If left or right is NullLiteral, just continue. Else check if the types differ. if (!(Constants.IsNull(left) || Constants.IsNull(right)) && left.Type != right.Type) { - if (left.Type.IsAssignableFrom(right.Type) || HasImplicitConversion(right.Type, left.Type)) + if ((left.Type == typeof(object) || right.Type == typeof(object)) && _expressionHelper.TryConvertTypes(ref left, ref right)) + { + // #937 + } + else if (left.Type.IsAssignableFrom(right.Type) || HasImplicitConversion(right.Type, left.Type)) { right = Expression.Convert(right, left.Type); } @@ -2551,7 +2555,7 @@ private bool TokenIsIdentifier(string id) { return _textParser.TokenIsIdentifier(id); } - + private string GetIdentifier() { _textParser.ValidateToken(TokenId.Identifier, Res.IdentifierExpected); diff --git a/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs index ce4b902e..bbc691cd 100644 --- a/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs @@ -48,4 +48,9 @@ internal interface IExpressionHelper Expression GenerateDefaultExpression(Type type); Expression ConvertAnyArrayToObjectArray(Expression arrayExpression); + + /// + /// 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); } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs index 22d22a72..23f6a963 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs @@ -5,6 +5,8 @@ using System.Linq.Dynamic.Core.Tests.Helpers.Entities; using System.Linq.Dynamic.Core.Tests.Helpers.Models; using System.Linq.Expressions; +using System.Text; +using Docker.DotNet.Models; using FluentAssertions; using Xunit; @@ -326,6 +328,56 @@ public void Where_Dynamic_DateTimeConstructor_Issue662() result2.Should().HaveCount(1); } + // #937 + [Theory] + [InlineData("NameCalculated == \"FooFoo\"", 1)] + [InlineData("\"FooFoo\" == NameCalculated", 1)] + [InlineData("NameCalculated == \"x\"", 0)] + [InlineData("NameCalculated != \"x\"", 2)] + [InlineData("NameCalculated <> \"x\"", 2)] + [InlineData("\"x\" == NameCalculated", 0)] + [InlineData("\"x\" != NameCalculated", 2)] + [InlineData("\"x\" <> NameCalculated", 2)] + public void Where_Dynamic_CompareObjectToString_ConvertObjectToSupportComparisonIsTrue(string expression, int expectedCount) + { + // Arrange + var config = new ParsingConfig + { + ConvertObjectToSupportComparison = true + }; + var queryable = new[] + { + new PersonWithObject { Name = "Foo", DateOfBirth = DateTime.UtcNow.AddYears(-31) }, + new PersonWithObject { Name = "Bar", DateOfBirth = DateTime.UtcNow.AddYears(-1) } + }.AsQueryable(); + + // Act + queryable.Where(config, expression).ToList().Should().HaveCount(expectedCount); + } + + // #937 + [Theory] + [InlineData("NameCalculated == \"FooFoo\"", 0)] // This is the expected behavior when ConvertObjectToSupportComparison is false because "Foo" is a string and NameCalculated is an object which is a calculated string. + [InlineData("\"FooFoo\" == NameCalculated", 0)] // Also expected. + [InlineData("NameCalculated == \"x\"", 0)] + [InlineData("NameCalculated != \"x\"", 2)] + [InlineData("NameCalculated <> \"x\"", 2)] + [InlineData("\"x\" == NameCalculated", 0)] + [InlineData("\"x\" != NameCalculated", 2)] + [InlineData("\"x\" <> NameCalculated", 2)] + public void Where_Dynamic_CompareObjectToString_ConvertObjectToSupportComparisonIsFalse(string expression, int expectedCount) + { + // Arrange + var queryable = new[] + { + new PersonWithObject { Name = "Foo", DateOfBirth = DateTime.UtcNow.AddYears(-31) }, + new PersonWithObject { Name = "Bar", DateOfBirth = DateTime.UtcNow.AddYears(-1) } + }.AsQueryable(); + + // Act + queryable.Where(expression).ToList().Should().HaveCount(expectedCount); + } + // #451 [Theory] [InlineData("Age == 99", 0)] @@ -448,7 +500,11 @@ private class PersonWithObject { // Deliberately typing these as `object` to illustrate the issue public object? Name { get; set; } + + public object? NameCalculated => Name + Encoding.ASCII.GetString(Convert.FromBase64String("Rm9v")); // "...Foo"; + public object Age => Convert.ToInt32(Math.Floor((DateTime.Today.Month - DateOfBirth.Month + 12 * DateTime.Today.Year - 12 * DateOfBirth.Year) / 12d)); + public DateTime DateOfBirth { get; set; } } From 53361d6cac16affbc70666966d8a373992fc1282 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Mon, 28 Jul 2025 17:34:13 +0200 Subject: [PATCH 20/44] 1.6.7 --- CHANGELOG.md | 3 +++ Generate-ReleaseNotes.bat | 2 +- version.xml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c1819e9..8d3e1d7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# v1.6.7 (28 July 2025) +- [#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) - [#932](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/932) - Fix "in" for nullable Enums [bug] contributed by [StefH](https://github.com/StefH) diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index d2da800c..571b9774 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.6.6 +SET version=v1.6.7 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 80460ce5..4fe8065c 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 6 + 7 \ No newline at end of file From f2e0ec7f306f020d7fd70191e91b37b554812203 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 27 Sep 2025 07:30:50 +0200 Subject: [PATCH 21/44] Fix GroupByMany using composite key and normal key (#946) * Fix GroupByMany using composite key and normal key * . * readme --- README.md | 2 +- .../DynamicQueryableExtensions.cs | 21 ++- .../Parser/ExpressionHelper.cs | 8 +- .../QueryableTests.GroupByMany.cs | 121 +++++++++++------- 4 files changed, 93 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index a1c7b643..7fc38608 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Or provide a list of additional types in the [DefaultDynamicLinqCustomTypeProvid |   **Issues** | [![GitHub issues](https://img.shields.io/github/issues/StefH/System.Linq.Dynamic.Core.svg)](https://github.com/StefH/System.Linq.Dynamic.Core/issues) | | | | | ***Quality*** |   | -|   **CI Workflow** | ![CI Workflow](https://github.com/zzzprojects/System.Linq.Dynamic.Core/actions/workflows/ci.yml/badge.svg) | +|   **CI Workflow** | [![CI Workflow](https://github.com/zzzprojects/System.Linq.Dynamic.Core/actions/workflows/ci.yml/badge.svg)](https://github.com/zzzprojects/System.Linq.Dynamic.Core/actions/workflows/ci.yml) | |   **SonarCloud** | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=zzzprojects_System.Linq.Dynamic.Core&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=zzzprojects_System.Linq.Dynamic.Core) | | | | ***NuGet*** |   | diff --git a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs index c5d52287..b3ff3092 100644 --- a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs +++ b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs @@ -871,12 +871,10 @@ public static IEnumerable GroupByMany(this IEnumerable>(keySelectors.Length); - - bool createParameterCtor = true; foreach (var selector in keySelectors) { - LambdaExpression l = DynamicExpressionParser.ParseLambda(config, createParameterCtor, typeof(TElement), typeof(object), selector); - selectors.Add((Func)l.Compile()); + var lambdaExpression = DynamicExpressionParser.ParseLambda(config, createParameterCtor: true, typeof(TElement), null, selector); + selectors.Add((Func)EnsureLambdaExpressionReturnsObject(lambdaExpression).Compile()); } return GroupByManyInternal(source, selectors.ToArray(), 0); @@ -913,8 +911,9 @@ private static IEnumerable GroupByManyInternal(IEnumerabl var selector = keySelectors[currentSelector]; - var result = source.GroupBy(selector).Select( - g => new GroupResult + var result = source + .GroupBy(selector) + .Select(g => new GroupResult { Key = g.Key, Count = g.Count(), @@ -2847,6 +2846,16 @@ private static TResult ConvertResultIfNeeded(object result) return (TResult?)Convert.ChangeType(result, typeof(TResult))!; } + + private static LambdaExpression EnsureLambdaExpressionReturnsObject(LambdaExpression lambdaExpression) + { + if (!lambdaExpression.GetReturnType().GetTypeInfo().IsSubclassOf(typeof(DynamicClass))) + { + return Expression.Lambda(Expression.Convert(lambdaExpression.Body, typeof(object)), lambdaExpression.Parameters.ToArray()); + } + + return lambdaExpression; + } #endregion Private Helpers } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs index 7b0167d8..7d72ad61 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs @@ -45,7 +45,7 @@ public bool TryUnwrapAsConstantExpression(Expression? expression, [NotNu return true; } - value = default; + value = null; return false; } @@ -53,7 +53,7 @@ public bool TryUnwrapAsConstantExpression(Expression? expression, [NotNullWhen(t { if (!_parsingConfig.UseParameterizedNamesInDynamicQuery || expression is not MemberExpression memberExpression) { - value = default; + value = null; return false; } @@ -68,7 +68,7 @@ public bool TryUnwrapAsConstantExpression(Expression? expression, [NotNullWhen(t return true; } - value = default; + value = null; return false; } @@ -531,6 +531,6 @@ private static object[] ConvertIfIEnumerableHasValues(IEnumerable? input) return input.Cast().ToArray(); } - return new object[0]; + return []; } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs index d5920a67..4c15053f 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs @@ -1,57 +1,82 @@ using System.Collections.Generic; +using FluentAssertions; using NFluent; using Xunit; -namespace System.Linq.Dynamic.Core.Tests +namespace System.Linq.Dynamic.Core.Tests; + +public partial class QueryableTests { - public partial class QueryableTests + [Fact] + public void GroupByMany_Dynamic_LambdaExpressions() + { + var lst = new List> + { + new(1, 1, 1), + new(1, 1, 2), + new(1, 1, 3), + new(2, 2, 4), + new(2, 2, 5), + new(2, 2, 6), + new(2, 3, 7) + }; + + var sel = lst.GroupByMany(x => x.Item1, x => x.Item2).ToArray(); + + Assert.Equal(2, sel.Length); + Assert.Single(sel.First().Subgroups); + Assert.Equal(2, sel.Skip(1).First().Subgroups.Count()); + } + + [Fact] + public void GroupByMany_Dynamic_StringExpressions() { - [Fact] - public void GroupByMany_Dynamic_LambdaExpressions() + var lst = new List> { - var lst = new List> - { - new Tuple(1, 1, 1), - new Tuple(1, 1, 2), - new Tuple(1, 1, 3), - new Tuple(2, 2, 4), - new Tuple(2, 2, 5), - new Tuple(2, 2, 6), - new Tuple(2, 3, 7) - }; - - var sel = lst.AsQueryable().GroupByMany(x => x.Item1, x => x.Item2); - - Assert.Equal(2, sel.Count()); - Assert.Single(sel.First().Subgroups); - Assert.Equal(2, sel.Skip(1).First().Subgroups.Count()); - } - - [Fact] - public void GroupByMany_Dynamic_StringExpressions() + new(1, 1, 1), + new(1, 1, 2), + new(1, 1, 3), + new(2, 2, 4), + new(2, 2, 5), + new(2, 2, 6), + new(2, 3, 7) + }; + + var sel = lst.GroupByMany("Item1", "Item2").ToList(); + + Check.That(sel.Count).Equals(2); + + var firstGroupResult = sel.First(); + Check.That(firstGroupResult.ToString()).Equals("1 (3)"); + Check.That(firstGroupResult.Subgroups.Count()).Equals(1); + + var skippedGroupResult = sel.Skip(1).First(); + Check.That(skippedGroupResult.ToString()).Equals("2 (4)"); + Check.That(skippedGroupResult.Subgroups.Count()).Equals(2); + } + + [Fact] + public void GroupByMany_Dynamic_CompositeKey() + { + // Arrange + var data = new[] { - var lst = new List> - { - new Tuple(1, 1, 1), - new Tuple(1, 1, 2), - new Tuple(1, 1, 3), - new Tuple(2, 2, 4), - new Tuple(2, 2, 5), - new Tuple(2, 2, 6), - new Tuple(2, 3, 7) - }; - - var sel = lst.AsQueryable().GroupByMany("Item1", "Item2").ToList(); - - Check.That(sel.Count).Equals(2); - - var firstGroupResult = sel.First(); - Check.That(firstGroupResult.ToString()).Equals("1 (3)"); - Check.That(firstGroupResult.Subgroups.Count()).Equals(1); - - var skippedGroupResult = sel.Skip(1).First(); - Check.That(skippedGroupResult.ToString()).Equals("2 (4)"); - Check.That(skippedGroupResult.Subgroups.Count()).Equals(2); - } + new { MachineId = 1, Machine = new { Id = 1, Name = "A" } }, + new { MachineId = 1, Machine = new { Id = 1, Name = "A" } }, + new { MachineId = 2, Machine = new { Id = 2, Name = "B" } } + }; + + // Act + var normalResult = data + .GroupByMany(d => new { d.MachineId, d.Machine.Name }, a => a.Machine.Id) + .Select(x => x.ToString()) + .ToList(); + var result = data + .GroupByMany("new (MachineId, Machine.Name)", "Machine.Id") + .Select(x => x.ToString()) + .ToList(); + + // Assert + result.Should().BeEquivalentTo(normalResult); } -} +} \ No newline at end of file From b0501ebaea8f02434aff4bc9bd681bcd1178cf0e Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 27 Sep 2025 08:19:36 +0200 Subject: [PATCH 22/44] Add IndexerName attribute to DynamicClass to fix naming issues with "Item" (#948) --- src/System.Linq.Dynamic.Core/DynamicClass.cs | 8 ++++-- .../DynamicClass.net35.cs | 2 ++ .../DynamicClass.uap.cs | 4 +++ .../DynamicQueryableExtensions.cs | 2 +- .../Parser/ExpressionParser.cs | 7 ++--- .../Parser/TypeHelper.cs | 5 ++++ .../EntitiesTests.Select.cs | 11 ++++++++ .../EntitiesTests.cs | 5 ++-- .../Helpers/Entities/Post.cs | 2 ++ .../QueryableTests.Select.cs | 27 +++++++++++++++++++ 10 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/DynamicClass.cs b/src/System.Linq.Dynamic.Core/DynamicClass.cs index 5dee90d3..33f06aee 100644 --- a/src/System.Linq.Dynamic.Core/DynamicClass.cs +++ b/src/System.Linq.Dynamic.Core/DynamicClass.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Dynamic; using System.Reflection; +using System.Runtime.CompilerServices; namespace System.Linq.Dynamic.Core; @@ -18,6 +19,8 @@ namespace System.Linq.Dynamic.Core; /// public abstract class DynamicClass : DynamicObject { + internal const string IndexerName = "System_Linq_Dynamic_Core_DynamicClass_Indexer"; + private Dictionary? _propertiesDictionary; private Dictionary Properties @@ -99,11 +102,12 @@ public void SetDynamicPropertyValue(string propertyName, object value) /// The . /// The name. /// Value from the property. + [IndexerName(IndexerName)] public object? this[string name] { get { - return Properties.TryGetValue(name, out object? result) ? result : null; + return Properties.TryGetValue(name, out var result) ? result : null; } set @@ -153,7 +157,7 @@ public override bool TryGetMember(GetMemberBinder binder, out object? result) /// public override bool TrySetMember(SetMemberBinder binder, object? value) { - string name = binder.Name; + var name = binder.Name; if (Properties.ContainsKey(name)) { Properties[name] = value; diff --git a/src/System.Linq.Dynamic.Core/DynamicClass.net35.cs b/src/System.Linq.Dynamic.Core/DynamicClass.net35.cs index 67237fab..d585f231 100644 --- a/src/System.Linq.Dynamic.Core/DynamicClass.net35.cs +++ b/src/System.Linq.Dynamic.Core/DynamicClass.net35.cs @@ -6,6 +6,8 @@ namespace System.Linq.Dynamic.Core; /// public abstract class DynamicClass { + internal const string IndexerName = "System_Linq_Dynamic_Core_DynamicClass_Indexer"; + /// /// Gets the dynamic property by name. /// diff --git a/src/System.Linq.Dynamic.Core/DynamicClass.uap.cs b/src/System.Linq.Dynamic.Core/DynamicClass.uap.cs index 6f6486c4..8778de98 100644 --- a/src/System.Linq.Dynamic.Core/DynamicClass.uap.cs +++ b/src/System.Linq.Dynamic.Core/DynamicClass.uap.cs @@ -1,6 +1,7 @@ #if UAP10_0 using System.Collections.Generic; using System.Dynamic; +using System.Runtime.CompilerServices; namespace System.Linq.Dynamic.Core; @@ -9,6 +10,8 @@ namespace System.Linq.Dynamic.Core; /// public class DynamicClass : DynamicObject { + internal const string IndexerName = "System_Linq_Dynamic_Core_DynamicClass_Indexer"; + private readonly Dictionary _properties = new(); /// @@ -31,6 +34,7 @@ public DynamicClass(params KeyValuePair[] propertylist) /// /// The name. /// Value from the property. + [IndexerName(IndexerName)] public object this[string name] { get diff --git a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs index b3ff3092..9f1a5772 100644 --- a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs +++ b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs @@ -2849,7 +2849,7 @@ private static TResult ConvertResultIfNeeded(object result) private static LambdaExpression EnsureLambdaExpressionReturnsObject(LambdaExpression lambdaExpression) { - if (!lambdaExpression.GetReturnType().GetTypeInfo().IsSubclassOf(typeof(DynamicClass))) + if (!TypeHelper.IsDynamicClass(lambdaExpression.GetReturnType())) { return Expression.Lambda(Expression.Convert(lambdaExpression.Body, typeof(object)), lambdaExpression.Parameters.ToArray()); } diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index a43b534e..be2a43c1 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -1580,7 +1580,7 @@ private Expression CreateNewExpression(List properties, List x.Name != "Item").ToArray(); + propertyInfos = propertyInfos.Where(x => x.Name != DynamicClass.IndexerName).ToArray(); } var propertyTypes = propertyInfos.Select(p => p.PropertyType).ToArray(); @@ -1906,7 +1906,7 @@ private Expression ParseMemberAccess(Type? type, Expression? expression, string? #if UAP10_0 || NETSTANDARD1_3 if (type == typeof(DynamicClass)) { - return Expression.MakeIndex(expression, typeof(DynamicClass).GetProperty("Item"), new[] { Expression.Constant(id) }); + return Expression.MakeIndex(expression!, typeof(DynamicClass).GetProperty(DynamicClass.IndexerName), [Expression.Constant(id)]); } #endif if (TryFindPropertyOrField(type!, id, expression, out var propertyOrFieldExpression)) @@ -1920,7 +1920,8 @@ private Expression ParseMemberAccess(Type? type, Expression? expression, string? if (!_parsingConfig.DisableMemberAccessToIndexAccessorFallback && extraCheck) { - var indexerMethod = expression?.Type.GetMethod("get_Item", new[] { typeof(string) }); + var indexerName = TypeHelper.IsDynamicClass(type!) ? DynamicClass.IndexerName : "Item"; + var indexerMethod = expression?.Type.GetMethod($"get_{indexerName}", [typeof(string)]); if (indexerMethod != null) { return Expression.Call(expression, indexerMethod, Expression.Constant(id)); diff --git a/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs b/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs index 6ffd2a19..19002c4f 100644 --- a/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs @@ -6,6 +6,11 @@ namespace System.Linq.Dynamic.Core.Parser; internal static class TypeHelper { + internal static bool IsDynamicClass(Type type) + { + return type == typeof(DynamicClass) || type.GetTypeInfo().IsSubclassOf(typeof(DynamicClass)); + } + internal static bool TryGetAsEnumerable(Type type, [NotNullWhen(true)] out Type? enumerableType) { if (type.IsArray) diff --git a/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.Select.cs b/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.Select.cs index 6626d842..87976802 100644 --- a/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.Select.cs +++ b/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.Select.cs @@ -164,4 +164,15 @@ public void Entities_Select_DynamicClass_And_Select_DynamicClass() dynamicResult.Should().BeEquivalentTo([1000, 1001]); } + + [Fact] + public void Entities_Select_ClassWithItemProperty() + { + // Act + var result = _context.Posts.Select(x => new { x.Item, x.BlogId }).ToArray(); + var resultDynamic = _context.Posts.Select("new (Item, BlogId)").ToDynamicArray(); + + // Assert + resultDynamic.Should().BeEquivalentTo(result); + } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.cs b/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.cs index 7ea211e4..96af723a 100644 --- a/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.cs @@ -11,7 +11,7 @@ namespace System.Linq.Dynamic.Core.Tests; public partial class EntitiesTests : IClassFixture { - private static readonly Random Rnd = new Random(1); + private static readonly Random Rnd = new(1); private readonly BlogContext _context; @@ -66,7 +66,8 @@ private void InternalPopulateTestData() Content = "My Content", PostDate = postDate, CloseDate = Rnd.Next(0, 10) < 5 ? postDate.AddDays(1) : null, - NumberOfReads = Rnd.Next(0, 5000) + NumberOfReads = Rnd.Next(0, 5000), + Item = "Item " + Rnd.Next(0, 1000) }; _context.Posts.Add(post); diff --git a/test/System.Linq.Dynamic.Core.Tests/Helpers/Entities/Post.cs b/test/System.Linq.Dynamic.Core.Tests/Helpers/Entities/Post.cs index 00d2e9a6..5883ab4a 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Helpers/Entities/Post.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Helpers/Entities/Post.cs @@ -22,4 +22,6 @@ public class Post public DateTime PostDate { get; set; } public DateTime? CloseDate { get; set; } + + public string? Item { get; set; } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs index 0383b1c2..364419a3 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs @@ -20,6 +20,14 @@ namespace System.Linq.Dynamic.Core.Tests { public partial class QueryableTests { + [DynamicLinqType] + public class ClassWithItem + { + public string? Item { get; set; } + + public int Value { get; set; } + } + [DynamicLinqType] public class Example { @@ -536,5 +544,24 @@ public void Select_Dynamic_StringConcatDifferentTypes(string expression, string // Act queryable.Select(config, expression).ToDynamicArray()[0].Should().Be(expectedResult); } + + [Fact] + public void Select_Dynamic_ClassWithItemProperty() + { + // Arrange + var data = new [] + { + new ClassWithItem { Item = "Value1", Value = 1 }, + new ClassWithItem { Item = "Value2", Value = 2 } + }; + var queryable = data.AsQueryable(); + + // Act + var result = queryable.Select(x => new {x.Item, x.Value }).ToArray(); + var resultDynamic = queryable.Select("new (Item, Value)").ToDynamicArray(); + + // Assert + resultDynamic.Should().BeEquivalentTo(result); + } } } \ No newline at end of file From 1cc4ea0dc37ecc137ddc24b20a94b73a8e5950ac Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sun, 28 Sep 2025 09:02:46 +0200 Subject: [PATCH 23/44] v1.6.8 --- CHANGELOG.md | 8 +++++++- Generate-ReleaseNotes.bat | 2 +- .../System.Linq.Dynamic.Core.csproj | 2 +- version.xml | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d3e1d7b..4cb7e830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ -# v1.6.7 (28 July 2025) +# v1.6.8 (28 September 2025) +- [#946](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/946) - Fix GroupByMany using composite key and normal key [bug] contributed by [StefH](https://github.com/StefH) +- [#948](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/948) - Add IndexerName attribute to DynamicClass to fix naming issues with Item [bug] contributed by [StefH](https://github.com/StefH) +- [#936](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/936) - AmbiguousMatchException when selecting a property named “Item” using EF Core DbContext [bug] +- [#945](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/945) - ParseException when using composite key for grouping in GroupByMany [bug] + +# 1.6.7 (28 July 2025) - [#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) diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index 571b9774..c90be38d 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.6.7 +SET version=v1.6.8 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/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj b/src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj index 522e517a..bcc32ad0 100644 --- a/src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj +++ b/src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj @@ -83,4 +83,4 @@ - + \ No newline at end of file diff --git a/version.xml b/version.xml index 4fe8065c..07e54a69 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 7 + 8 \ No newline at end of file From 2c58c205b0c38ef8187eae9d4c64d7da53c3e63e Mon Sep 17 00:00:00 2001 From: Thibault Reigner Date: Fri, 10 Oct 2025 21:58:43 +0800 Subject: [PATCH 24/44] DynamicExpressionParser - Handle indexed properties with any number of indices in expression (#950) * DynamicExpressionParser - Handle indexed properties with any number of indices in expression * Adjust exception message when misusing indexer (e.g : incorrect number of parameters) --- .../Parser/ExpressionParser.cs | 16 ++-- src/System.Linq.Dynamic.Core/Res.cs | 2 +- .../DynamicExpressionParserTests.cs | 85 +++++++++++++++++++ 3 files changed, 95 insertions(+), 8 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index be2a43c1..4cc77a00 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -2353,20 +2353,22 @@ private Expression ParseElementAccess(Expression expr) switch (_methodFinder.FindIndexer(expr.Type, args, out var mb)) { case 0: - throw ParseError(errorPos, Res.NoApplicableIndexer, - TypeHelper.GetTypeName(expr.Type)); + throw ParseError(errorPos, Res.NoApplicableIndexer, TypeHelper.GetTypeName(expr.Type), args.Length); case 1: var indexMethod = (MethodInfo)mb!; - var indexParameterType = indexMethod.GetParameters().First().ParameterType; + var indexMethodArguments = indexMethod.GetParameters(); - var indexArgumentExpression = args[0]; // Indexer only has 1 parameter, so we can use args[0] here - if (indexParameterType != indexArgumentExpression.Type) + var indexArgumentExpressions = new Expression[args.Length]; + for (var i = 0; i < indexMethodArguments.Length; ++i) { - indexArgumentExpression = Expression.Convert(indexArgumentExpression, indexParameterType); + var indexParameterType = indexMethodArguments[i].ParameterType; + indexArgumentExpressions[i] = indexParameterType != args[i].Type + ? Expression.Convert(args[i], indexParameterType) + : args[i]; } - return Expression.Call(expr, indexMethod, indexArgumentExpression); + return Expression.Call(expr, indexMethod, indexArgumentExpressions); default: throw ParseError(errorPos, Res.AmbiguousIndexerInvocation, TypeHelper.GetTypeName(expr.Type)); diff --git a/src/System.Linq.Dynamic.Core/Res.cs b/src/System.Linq.Dynamic.Core/Res.cs index d2831c24..3d423495 100644 --- a/src/System.Linq.Dynamic.Core/Res.cs +++ b/src/System.Linq.Dynamic.Core/Res.cs @@ -55,7 +55,7 @@ internal static class Res public const string MissingAsClause = "Expression is missing an 'as' clause"; public const string NeitherTypeConvertsToOther = "Neither of the types '{0}' and '{1}' converts to the other"; public const string NewOperatorIsNotAllowed = "Using the new operator is not allowed via the ParsingConfig."; - public const string NoApplicableIndexer = "No applicable indexer exists in type '{0}'"; + public const string NoApplicableIndexer = "No applicable indexer exists in type '{0}' with {1} parameters"; public const string NoApplicableMethod = "No applicable method '{0}' exists in type '{1}'"; public const string NoItInScope = "No 'it' is in scope"; public const string NoMatchingConstructor = "No matching constructor in type '{0}'"; diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs index a65012b0..4d5d19b5 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -70,6 +70,18 @@ public override HashSet GetCustomTypes() } } + private class ClassWithIndexers + { + public int this[int i1] + { + get => i1 + 1; + } + public string this[int i1, string i2] + { + get => i1 + "-" + i2; + } + } + private class ComplexParseLambda1Result { public int? Age; @@ -660,6 +672,79 @@ public void DynamicExpressionParser_ParseLambda_Issue58() Check.That(result).Equals(42); } + [Fact] + public void DynamicExpressionParser_ParseLambda_Indexer1D() + { + // Arrange + var customTypeProvider = new Mock(); + customTypeProvider.Setup(c => c.GetCustomTypes()).Returns([typeof(ClassWithIndexers)]); + var config = new ParsingConfig + { + CustomTypeProvider = customTypeProvider.Object + }; + var expressionParams = new[] + { + Expression.Parameter(typeof(ClassWithIndexers), "myObj") + }; + + var myClassInstance = new ClassWithIndexers(); + var invokersMerge = new List { myClassInstance }; + + // Act + var expression = DynamicExpressionParser.ParseLambda(config, false, expressionParams, null, "myObj[3]"); + var del = expression.Compile(); + var result = del.DynamicInvoke(invokersMerge.ToArray()); + + // Assert + Check.That(result).Equals(4); + } + + [Fact] + public void DynamicExpressionParser_ParseLambda_Indexer2D() + { + // Arrange + var customTypeProvider = new Mock(); + customTypeProvider.Setup(c => c.GetCustomTypes()).Returns([typeof(ClassWithIndexers)]); + var config = new ParsingConfig + { + CustomTypeProvider = customTypeProvider.Object + }; + var expressionParams = new[] + { + Expression.Parameter(typeof(ClassWithIndexers), "myObj") + }; + + var myClassInstance = new ClassWithIndexers(); + var invokersMerge = new List { myClassInstance }; + + // Act + var expression = DynamicExpressionParser.ParseLambda(config, false, expressionParams, null, "myObj[3,\"1\"]"); + var del = expression.Compile(); + var result = del.DynamicInvoke(invokersMerge.ToArray()); + + // Assert + Check.That(result).Equals("3-1"); + } + + [Fact] + public void DynamicExpressionParser_ParseLambda_IndexerParameterMismatch() + { + // Arrange + var customTypeProvider = new Mock(); + customTypeProvider.Setup(c => c.GetCustomTypes()).Returns([typeof(ClassWithIndexers)]); + var config = new ParsingConfig + { + CustomTypeProvider = customTypeProvider.Object + }; + var expressionParams = new[] + { + Expression.Parameter(typeof(ClassWithIndexers), "myObj") + }; + + Assert.Throws(() => + DynamicExpressionParser.ParseLambda(config, false, expressionParams, null, "myObj[3,\"1\",1]")); + } + [Fact] public void DynamicExpressionParser_ParseLambda_DuplicateParameterNames_ThrowsException() { From 09d558a59b3b62c1d0e0fdc3c5ed84f2469050a5 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 11 Oct 2025 10:19:07 +0200 Subject: [PATCH 25/44] v1.6.9 --- CHANGELOG.md | 3 +++ Generate-ReleaseNotes.bat | 2 +- version.xml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cb7e830..210e2f20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# v1.6.9 (10 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) - [#946](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/946) - Fix GroupByMany using composite key and normal key [bug] contributed by [StefH](https://github.com/StefH) - [#948](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/948) - Add IndexerName attribute to DynamicClass to fix naming issues with Item [bug] contributed by [StefH](https://github.com/StefH) diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index c90be38d..aa50344d 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.6.8 +SET version=v1.6.9 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 07e54a69..61507fe2 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 8 + 9 \ No newline at end of file From 9f59211be46ea6cc48f0be77ec3d907205fa456b Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 25 Oct 2025 10:53:00 +0200 Subject: [PATCH 26/44] Fixed adding Enum and integer (#953) --- .../Parser/ExpressionPromoter.cs | 2 +- .../Parser/TypeHelper.cs | 37 +++++++------- .../Parser/TypeHelperTests.cs | 48 ++++++++++++++++++- .../QueryableTests.Select.cs | 26 ++++++++++ 4 files changed, 94 insertions(+), 19 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs index 088b755e..49731b24 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs @@ -122,7 +122,7 @@ public ExpressionPromoter(ParsingConfig config) if (TypeHelper.IsCompatibleWith(returnType, type)) { - if (type == typeof(decimal) && TypeHelper.IsEnumType(sourceExpression.Type)) + if (TypeHelper.TypesAreEqual(type, typeof(decimal)) && TypeHelper.IsEnumType(sourceExpression.Type)) { return Expression.Convert(Expression.Convert(sourceExpression, Enum.GetUnderlyingType(sourceExpression.Type)), type); } diff --git a/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs b/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs index 19002c4f..f4401b63 100644 --- a/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs @@ -83,20 +83,20 @@ public static bool IsCompatibleWith(Type source, Type target) return target.IsAssignableFrom(source); } - Type st = GetNonNullableType(source); - Type tt = GetNonNullableType(target); + var sourceType = GetNonNullableType(source); + var targetType = GetNonNullableType(target); - if (st != source && tt == target) + if (sourceType != source && targetType == target) { return false; } - TypeCode sc = st.GetTypeInfo().IsEnum ? TypeCode.Int64 : Type.GetTypeCode(st); - TypeCode tc = tt.GetTypeInfo().IsEnum ? TypeCode.Int64 : Type.GetTypeCode(tt); - switch (sc) + var sourceTypeCode = sourceType.GetTypeInfo().IsEnum ? TypeCode.Int32 : Type.GetTypeCode(sourceType); + var targetTypeCode = targetType.GetTypeInfo().IsEnum ? TypeCode.Int32 : Type.GetTypeCode(targetType); + switch (sourceTypeCode) { case TypeCode.SByte: - switch (tc) + switch (targetTypeCode) { case TypeCode.SByte: case TypeCode.Int16: @@ -110,7 +110,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.Byte: - switch (tc) + switch (targetTypeCode) { case TypeCode.Byte: case TypeCode.Int16: @@ -127,7 +127,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.Int16: - switch (tc) + switch (targetTypeCode) { case TypeCode.Int16: case TypeCode.Int32: @@ -140,7 +140,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.UInt16: - switch (tc) + switch (targetTypeCode) { case TypeCode.UInt16: case TypeCode.Int32: @@ -155,7 +155,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.Int32: - switch (tc) + switch (targetTypeCode) { case TypeCode.Int32: case TypeCode.Int64: @@ -167,7 +167,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.UInt32: - switch (tc) + switch (targetTypeCode) { case TypeCode.UInt32: case TypeCode.Int64: @@ -180,7 +180,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.Int64: - switch (tc) + switch (targetTypeCode) { case TypeCode.Int64: case TypeCode.Single: @@ -191,7 +191,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.UInt64: - switch (tc) + switch (targetTypeCode) { case TypeCode.UInt64: case TypeCode.Single: @@ -202,7 +202,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.Single: - switch (tc) + switch (targetTypeCode) { case TypeCode.Single: case TypeCode.Double: @@ -211,7 +211,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; default: - if (st == tt) + if (sourceType == targetType) { return true; } @@ -471,6 +471,11 @@ public static Type GetUnderlyingType(Type type) return type; } + public static bool TypesAreEqual(Type type, Type typeToCheck) + { + return GetNullableType(type) == GetNullableType(typeToCheck); + } + public static IList GetSelfAndBaseTypes(Type type, bool excludeObject = false) { if (type.GetTypeInfo().IsInterface) diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/TypeHelperTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/TypeHelperTests.cs index c7a534ba..e6286834 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/TypeHelperTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/TypeHelperTests.cs @@ -45,7 +45,7 @@ public void TypeHelper_IsCompatibleWith_SameTypes_True() } [Fact] - public void TypeHelper_IsCompatibleWith_True() + public void TypeHelper_IsCompatibleWith_Int_And_Long_Returns_True() { // Assign + Act var result = TypeHelper.IsCompatibleWith(typeof(int), typeof(long)); @@ -54,8 +54,52 @@ public void TypeHelper_IsCompatibleWith_True() Check.That(result).IsTrue(); } + [Theory] + + // True (enum underlying Int32 compatible targets) + [InlineData(typeof(DayOfWeek), true)] + [InlineData(typeof(DayOfWeek?), true)] + [InlineData(typeof(int), true)] + [InlineData(typeof(int?), true)] + [InlineData(typeof(long), true)] + [InlineData(typeof(long?), true)] + [InlineData(typeof(float), true)] + [InlineData(typeof(float?), true)] + [InlineData(typeof(double), true)] + [InlineData(typeof(double?), true)] + [InlineData(typeof(decimal), true)] + [InlineData(typeof(decimal?), true)] + [InlineData(typeof(object), true)] + + // False (not compatible with enum's Int32 widening rules or reference types) + [InlineData(typeof(char), false)] + [InlineData(typeof(char?), false)] + [InlineData(typeof(short), false)] + [InlineData(typeof(short?), false)] + [InlineData(typeof(byte), false)] + [InlineData(typeof(byte?), false)] + [InlineData(typeof(sbyte), false)] + [InlineData(typeof(sbyte?), false)] + [InlineData(typeof(ushort), false)] + [InlineData(typeof(ushort?), false)] + [InlineData(typeof(uint), false)] + [InlineData(typeof(uint?), false)] + [InlineData(typeof(ulong), false)] + [InlineData(typeof(ulong?), false)] + [InlineData(typeof(bool), false)] + [InlineData(typeof(bool?), false)] + [InlineData(typeof(string), false)] + public void TypeHelper_IsCompatibleWith_Enum(Type targetType, bool expected) + { + // Assign + Act + var result = TypeHelper.IsCompatibleWith(typeof(DayOfWeek), targetType); + + // Assert + result.Should().Be(expected); + } + [Fact] - public void TypeHelper_IsCompatibleWith_False() + public void TypeHelper_IsCompatibleWith_Long_And_Int_Returns_False() { // Assign + Act var result = TypeHelper.IsCompatibleWith(typeof(long), typeof(int)); diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs index 364419a3..83d17e9c 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs @@ -202,6 +202,32 @@ public void Select_Dynamic_Add_Strings() Assert.Equal(range.Select(x => x + "c").ToArray(), rangeResult.Cast().ToArray()); } + [Fact] + public void Select_Dynamic_Add_DayOfWeekEnum_And_Integer() + { + // Arrange + var range = new DayOfWeek[] { DayOfWeek.Monday }; + + // Act + var rangeResult = range.AsQueryable().Select("it + 1"); + + // Assert + Assert.Equal(range.Select(x => x + 1).ToArray(), rangeResult.Cast().ToArray()); + } + + [Fact] + public void Select_Dynamic_Add_Integer_And_DayOfWeekEnum() + { + // Arrange + var range = new int[] { 1 }; + + // Act + var rangeResult = range.AsQueryable().Select("it + DayOfWeek.Monday"); + + // Assert + Assert.Equal(range.Select(x => x + DayOfWeek.Monday).Cast().ToArray(), rangeResult.Cast().ToArray()); + } + [Fact] public void Select_Dynamic_WithIncludes() { From 8874662ff18b8f73140e6cc289415b265868b584 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sun, 2 Nov 2025 14:30:05 +0100 Subject: [PATCH 27/44] 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 28/44] 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 29/44] 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 30/44] 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 31/44] .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 32/44] 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 33/44] 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 34/44] 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 35/44] 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 36/44] 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 37/44] 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 38/44] 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 39/44] 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 40/44] 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 41/44] 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 42/44] 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 43/44] 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 44/44] 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() {