diff --git a/src-console/ConsoleAppEF6_InMemory/Program.cs b/src-console/ConsoleAppEF6_InMemory/Program.cs index f9bed31ae..31a9850b0 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 3817d8976..e17dbe93f 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 f2369f448..8c1727b88 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 1ec0fea66..c2fb0102a 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 100603aa6..32dd3002e 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 000000000..697a053ae --- /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 ab0b44c94..6626d8422 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 be6ff839d..4ef92c26e 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()) {