Skip to content

Commit 64b5eff

Browse files
authored
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
1 parent b038e62 commit 64b5eff

File tree

9 files changed

+266
-21
lines changed

9 files changed

+266
-21
lines changed

src-console/ConsoleAppEF6_InMemory/Program.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ static async Task Main(string[] args)
2626
await using (var context = new TestContextEF6())
2727
{
2828
var result784 = context.Products.Where("NullableInt = @0", 1).ToDynamicArray<ProductDynamic>();
29-
Console.WriteLine("a1 {0}", string.Join(",", result784.Select(r => r.Key)));
29+
Console.WriteLine("a1 {0}", string.Join(", ", result784.Select(r => r.Key)));
3030
}
3131

3232
await using (var context = new TestContextEF6())
@@ -65,5 +65,20 @@ static async Task Main(string[] args)
6565
Console.WriteLine(result.Key + ":" + JsonSerializer.Serialize(result.Dict, JsonSerializerOptions));
6666
}
6767
}
68+
69+
// #907 and #912
70+
await using (var context = new TestContextEF6())
71+
{
72+
var dynamicData = context.Products
73+
.AsQueryable()
74+
.Select("new { NullableInt as Value }")
75+
.ToDynamicArray();
76+
var dynamicResult = dynamicData
77+
.AsQueryable()
78+
.Select("Value")
79+
.ToDynamicArray();
80+
81+
Console.WriteLine("#907 and #912 = {0}", string.Join(", ", dynamicResult));
82+
}
6883
}
6984
}

src-console/ConsoleApp_net6.0/Program.cs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Data;
34
using System.Linq;
45
using System.Linq.Dynamic.Core;
56
using System.Linq.Dynamic.Core.NewtonsoftJson;
@@ -33,15 +34,15 @@ public class GroupedSalesData
3334
public string Region { get; set; }
3435
public string? Product { get; set; }
3536
public int TotalSales { get; set; }
36-
3737
public int GroupLevel { get; set; }
3838
}
3939

4040
class Program
4141
{
4242
static void Main(string[] args)
4343
{
44-
Q912();
44+
Q912a();
45+
Q912b();
4546
return;
4647

4748
Json();
@@ -67,7 +68,7 @@ static void Main(string[] args)
6768
Dynamic();
6869
}
6970

70-
private static void Q912()
71+
private static void Q912a()
7172
{
7273
var extractedRows = new List<SalesData>
7374
{
@@ -90,12 +91,45 @@ private static void Q912()
9091
.GroupBy("Region")
9192
.Select<GroupedSalesData>("new (Key as Region, null as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 1 as GroupLevel)");
9293

93-
var combined = detailed.Concat(regionSubtotal);
94+
var combined = detailed.Concat(regionSubtotal).AsQueryable();
9495
var ordered = combined.OrderBy("Product").ToDynamicList();
9596

9697
int x = 9;
9798
}
9899

100+
private static void Q912b()
101+
{
102+
var eInfoJoinTable = new DataTable();
103+
eInfoJoinTable.Columns.Add("Region", typeof(string));
104+
eInfoJoinTable.Columns.Add("Product", typeof(string));
105+
eInfoJoinTable.Columns.Add("Sales", typeof(int));
106+
107+
eInfoJoinTable.Rows.Add("North", "Apples", 100);
108+
eInfoJoinTable.Rows.Add("North", "Oranges", 150);
109+
eInfoJoinTable.Rows.Add("South", "Apples", 200);
110+
eInfoJoinTable.Rows.Add("South", "Oranges", 250);
111+
112+
var extractedRows =
113+
from row in eInfoJoinTable.AsEnumerable()
114+
select row;
115+
116+
var rows = extractedRows.AsQueryable();
117+
118+
// GROUPING SET 1: (Region, Product)
119+
var detailed = rows
120+
.GroupBy("new (Region, Product)")
121+
.Select("new (Key.Region as Region, Key.Product as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 0 as GroupLevel)");
122+
123+
// GROUPING SET 2: (Region)
124+
var regionSubtotal = rows
125+
.GroupBy("Region")
126+
.Select("new (Key as Region, null as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 1 as GroupLevel)");
127+
128+
var combined = detailed.ToDynamicArray().Concat(regionSubtotal.ToDynamicArray()).AsQueryable();
129+
var ordered = combined.OrderBy("Product").ToDynamicList();
130+
131+
int x = 9;
132+
}
99133

100134
private static void NewtonsoftJson()
101135
{

src/System.Linq.Dynamic.Core/DynamicGetMemberBinder.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ public DynamicGetMemberBinder(string name, ParsingConfig? config) : base(name, c
2121

2222
public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject? errorSuggestion)
2323
{
24-
var instance = Expression.Call(
24+
var methodCallExpression = Expression.Call(
2525
DynamicGetMemberMethod,
2626
target.Expression,
2727
Expression.Constant(Name),
2828
Expression.Constant(IgnoreCase));
2929

30-
return DynamicMetaObject.Create(target.Value!, instance);
30+
// 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.".
31+
var restrictions = BindingRestrictions.GetInstanceRestriction(target.Expression, target.Value);
32+
return new DynamicMetaObject(methodCallExpression, restrictions, target.Value!);
3133
}
3234

3335
public static object? GetDynamicMember(object value, string name, bool ignoreCase)

src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1609,9 +1609,9 @@ private Expression CreateNewExpression(List<DynamicProperty> properties, List<Ex
16091609

16101610
// Option 2. Call the default (empty) constructor and set the members
16111611
var memberBindings = new MemberBinding[properties.Count];
1612-
for (int i = 0; i < memberBindings.Length; i++)
1612+
for (var i = 0; i < memberBindings.Length; i++)
16131613
{
1614-
string propertyOrFieldName = properties[i].Name;
1614+
var propertyOrFieldName = properties[i].Name;
16151615
Type propertyOrFieldType;
16161616
MemberInfo memberInfo;
16171617
var propertyInfo = type.GetProperty(propertyOrFieldName);
@@ -1632,12 +1632,8 @@ private Expression CreateNewExpression(List<DynamicProperty> properties, List<Ex
16321632
propertyOrFieldType = fieldInfo.FieldType;
16331633
}
16341634

1635-
// Promote from Type to Nullable Type if needed
1636-
var promoted = _parsingConfig.ExpressionPromoter.Promote(expressions[i], propertyOrFieldType, true, true);
1637-
if (promoted is null)
1638-
{
1639-
throw new NotSupportedException($"Unable to promote expression '{expressions[i]}'.");
1640-
}
1635+
// Call Promote and if that returns false, just try to convert the expression to the destination type using Expression.Convert
1636+
var promoted = _parsingConfig.ExpressionPromoter.Promote(expressions[i], propertyOrFieldType, true, true) ?? Expression.Convert(expressions[i], propertyOrFieldType);
16411637
memberBindings[i] = Expression.Bind(memberInfo, promoted);
16421638
}
16431639

test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
<Compile Include="..\System.Linq.Dynamic.Core.Tests\TestClasses\*.cs" />
3030
</ItemGroup>
3131

32+
<ItemGroup>
33+
<Compile Remove="..\System.Linq.Dynamic.Core.Tests\TestHelpers\SkipIfGitHubActionsAttribute.cs" />
34+
</ItemGroup>
35+
3236
<ItemGroup>
3337
<ProjectReference Include="..\..\src\System.Linq.Dynamic.Core\System.Linq.Dynamic.Core.csproj" />
3438
<ProjectReference Include="..\..\src\EntityFramework.DynamicLinq\EntityFramework.DynamicLinq.csproj" />

test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ public void DynamicClass_GetRuntimeType()
217217
typeOf.ToString().Should().Be("System.Linq.Dynamic.Core.DynamicClass");
218218
}
219219

220-
[SkipIfGitHubActions]
220+
[SkipIfGitHubActionsFact]
221221
public void DynamicClassArray()
222222
{
223223
// Arrange
@@ -249,7 +249,7 @@ public void DynamicClassArray()
249249
isValid.Should().BeTrue();
250250
}
251251

252-
[SkipIfGitHubActions]
252+
[SkipIfGitHubActionsFact]
253253
public void DynamicClassArray_Issue593_Fails()
254254
{
255255
// Arrange
@@ -281,7 +281,7 @@ public void DynamicClassArray_Issue593_Fails()
281281
isValid.Should().BeFalse(); // This should actually be true, but fails. For solution see Issue593_Solution1 and Issue593_Solution2.
282282
}
283283

284-
[SkipIfGitHubActions]
284+
[SkipIfGitHubActionsFact]
285285
public void DynamicClassArray_Issue593_Solution1()
286286
{
287287
// Arrange
@@ -318,7 +318,7 @@ public void DynamicClassArray_Issue593_Solution1()
318318
isValid.Should().BeTrue();
319319
}
320320

321-
[SkipIfGitHubActions]
321+
[SkipIfGitHubActionsFact]
322322
public void DynamicClassArray_Issue593_Solution2()
323323
{
324324
// Arrange
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
using System.Data;
2+
using System.Linq.Dynamic.Core.Tests.TestHelpers;
3+
using FluentAssertions;
4+
using Xunit;
5+
6+
namespace System.Linq.Dynamic.Core.Tests;
7+
8+
public class DynamicGetMemberBinderTests
9+
{
10+
public class SalesData
11+
{
12+
public string Region { get; set; } = null!;
13+
14+
public string Product { get; set; } = null!;
15+
16+
public string Sales { get; set; } = null!;
17+
}
18+
19+
public class GroupedSalesData
20+
{
21+
public string Region { get; set; } = null!;
22+
public string? Product { get; set; }
23+
public int TotalSales { get; set; }
24+
public int GroupLevel { get; set; }
25+
}
26+
27+
[Fact]
28+
public void DynamicGetMemberBinder_SelectOnArrayWithComplexObjects()
29+
{
30+
// Arrange
31+
var rows = new SalesData[]
32+
{
33+
new() { Region = "North", Product = "Widget", Sales = "100" },
34+
new() { Region = "North", Product = "Gadget", Sales = "150" },
35+
new() { Region = "South", Product = "Widget", Sales = "200" },
36+
new() { Region = "South", Product = "Gadget", Sales = "100" },
37+
new() { Region = "North", Product = "Widget", Sales = "50" }
38+
}.AsQueryable();
39+
40+
// Act
41+
var grouping1 = rows
42+
.GroupBy("new (Region, Product)")
43+
.Select("new (Key.Region as Region, Key.Product as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 0 as GroupLevel)");
44+
45+
var grouping2 = rows
46+
.GroupBy("Region")
47+
.Select("new (Key as Region, null as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 1 as GroupLevel)");
48+
49+
var combined = grouping1.ToDynamicArray().Concat(grouping2.ToDynamicArray()).AsQueryable();
50+
var ordered = combined.OrderBy("Product").ToDynamicList();
51+
52+
// Assert
53+
ordered.Should().HaveCount(6);
54+
}
55+
56+
[Fact]
57+
public void DynamicGetMemberBinder_SelectTypeOnArrayWithComplexObjects()
58+
{
59+
// Arrange
60+
var rows = new SalesData[]
61+
{
62+
new() { Region = "North", Product = "Widget", Sales = "100" },
63+
new() { Region = "North", Product = "Gadget", Sales = "150" },
64+
new() { Region = "South", Product = "Widget", Sales = "200" },
65+
new() { Region = "South", Product = "Gadget", Sales = "100" },
66+
new() { Region = "North", Product = "Widget", Sales = "50" }
67+
}.AsQueryable();
68+
69+
// Act
70+
var grouping1 = rows
71+
.GroupBy("new (Region, Product)")
72+
.Select<GroupedSalesData>("new (Key.Region as Region, Key.Product as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 0 as GroupLevel)");
73+
74+
var grouping2 = rows
75+
.GroupBy("Region")
76+
.Select<GroupedSalesData>("new (Key as Region, null as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 1 as GroupLevel)");
77+
78+
var combined = grouping1.Concat(grouping2).AsQueryable();
79+
var ordered = combined.OrderBy("Product").ToDynamicList();
80+
81+
// Assert
82+
ordered.Should().HaveCount(6);
83+
}
84+
85+
[SkipIfGitHubActionsFact]
86+
public void DynamicGetMemberBinder_SelectOnDataTable()
87+
{
88+
// Arrange
89+
var dataTable = new DataTable();
90+
dataTable.Columns.Add("Region", typeof(string));
91+
dataTable.Columns.Add("Product", typeof(string));
92+
dataTable.Columns.Add("Sales", typeof(int));
93+
94+
dataTable.Rows.Add("North", "Apples", 100);
95+
dataTable.Rows.Add("North", "Oranges", 150);
96+
dataTable.Rows.Add("South", "Apples", 200);
97+
dataTable.Rows.Add("South", "Oranges", 250);
98+
99+
var rows = dataTable.Rows.Cast<DataRow>().AsQueryable();
100+
101+
// Act
102+
var grouping1 = rows
103+
.GroupBy("new (Region, Product)")
104+
.Select("new (Key.Region as Region, Key.Product as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 0 as GroupLevel)");
105+
106+
var grouping2 = rows
107+
.GroupBy("Region")
108+
.Select("new (Key as Region, null as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 1 as GroupLevel)");
109+
110+
var combined = grouping1.ToDynamicArray().Concat(grouping2.ToDynamicArray()).AsQueryable();
111+
var ordered = combined.OrderBy("Product").ToDynamicList();
112+
113+
// Assert
114+
ordered.Should().HaveCount(6);
115+
}
116+
117+
[SkipIfGitHubActionsFact]
118+
public void DynamicGetMemberBinder_SelectTypeOnDataTable()
119+
{
120+
// Arrange
121+
var dataTable = new DataTable();
122+
dataTable.Columns.Add("Region", typeof(string));
123+
dataTable.Columns.Add("Product", typeof(string));
124+
dataTable.Columns.Add("Sales", typeof(int));
125+
126+
dataTable.Rows.Add("North", "Apples", 100);
127+
dataTable.Rows.Add("North", "Oranges", 150);
128+
dataTable.Rows.Add("South", "Apples", 200);
129+
dataTable.Rows.Add("South", "Oranges", 250);
130+
131+
var rows = dataTable.Rows.Cast<DataRow>().AsQueryable();
132+
133+
// Act
134+
var grouping1 = rows
135+
.GroupBy("new (Region, Product)")
136+
.Select<GroupedSalesData>("new (Key.Region as Region, Key.Product as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 0 as GroupLevel)");
137+
138+
var grouping2 = rows
139+
.GroupBy("Region")
140+
.Select<GroupedSalesData>("new (Key as Region, null as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 1 as GroupLevel)");
141+
142+
var combined = grouping1.ToDynamicArray().Concat(grouping2.ToDynamicArray()).AsQueryable();
143+
var ordered = combined.OrderBy("Product").ToDynamicList();
144+
145+
// Assert
146+
ordered.Should().HaveCount(6);
147+
}
148+
149+
[Fact]
150+
public void DynamicGetMemberBinder_SelectOnArrayWithIntegers()
151+
{
152+
// Arrange
153+
var dynamicData = new[] { 1, 2 }
154+
.AsQueryable()
155+
.Select("new { it * 2 as Value }")
156+
.ToDynamicArray()
157+
.AsQueryable();
158+
159+
// Act
160+
var dynamicResult1 = dynamicData
161+
.Select("Value")
162+
.ToDynamicArray();
163+
164+
var dynamicResult2 = dynamicData
165+
.Select("Value")
166+
.ToDynamicArray<int>();
167+
168+
// Assert
169+
dynamicResult1.Should().HaveCount(2);
170+
dynamicResult2.Should().BeEquivalentTo([2, 4]);
171+
}
172+
}

0 commit comments

Comments
 (0)