Skip to content

Commit cb9b235

Browse files
authored
Merge pull request #50 from joadan/fix-projection
Fixed Expand Bug
2 parents a380012 + 17058a7 commit cb9b235

3 files changed

Lines changed: 70 additions & 15 deletions

File tree

src/Linq2OData.Core/Expressions/QueryNodeVisitor.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,11 @@ protected override Expression VisitMethodCall(MethodCallExpression node)
153153
}
154154

155155
// Visit the method arguments (typically lambdas)
156-
// Skip the first argument if it's the source (for extension methods)
157-
var startIndex = (node.Object == null && node.Arguments.Count > 0 && node.Arguments[0] == sourceMember) ? 1 : 0;
156+
// Skip the first argument if it's the source (for extension methods).
157+
// Also skip if it's a MethodCallExpression that was already visited in the early
158+
// else block above (e.g. the inner Select in .Select(...).ToList()).
159+
var startIndex = (node.Object == null && node.Arguments.Count > 0 &&
160+
(node.Arguments[0] == sourceMember || node.Arguments[0] is MethodCallExpression)) ? 1 : 0;
158161
for (int i = startIndex; i < node.Arguments.Count; i++)
159162
{
160163
Visit(node.Arguments[i]);

test/Linq2OData.TestClients/Program.cs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//using DemoClientV4.ODataDemo;
2-
using DemoClientV4.ODataDemo;
2+
//using DemoClientV4.ODataDemo;
33
using Linq2OData.Core.Metadata;
44
using Linq2OData.Generator.Models;
55

@@ -17,14 +17,15 @@ static async Task Main(string[] args)
1717
{
1818
Console.WriteLine("Here we go!");
1919

20-
//await GenerateDemoClientV2Async();
20+
// await GenerateDemoClientV2Async();
2121
//await GenerateDemoClientV4Async();
2222
//await GenerateTripPinClientAsync();
2323

2424
//GenerateLargeClientAsync();
2525

26-
// await TestTripPinAsync();
27-
await TestV4ClientAsync();
26+
// await TestTripPinAsync();
27+
//await TestV4ClientAsync();
28+
await TestV2ClientAsync();
2829
}
2930

3031
private static async Task TestTripPinAsync()
@@ -90,11 +91,17 @@ private static async Task TestV2ClientAsync()
9091

9192
var kalle = clientV2.Services;
9293

93-
var queryResult = await clientV2
94-
.Query<DemoClientV2.ODataDemo.Product>()
95-
.Filter(e => e.ID != 1)
96-
.Expand(expand => expand.Category!.Products)
97-
.ExecuteAsync();
94+
//var queryResult = await clientV2
95+
// .Query<DemoClientV2.ODataDemo.Product>()
96+
// .Filter(e => e.ID != 1)
97+
// .Select(e => e.Select(e => new DemoClientV2.ODataDemo.Product { Name = e.Name, Price = e.Price, Supplier = new DemoClientV2.ODataDemo.Supplier { Name = e.Name } }))
98+
// .ExecuteAsync();
99+
100+
var query = clientV2
101+
.Query<DemoClientV2.ODataDemo.Supplier>()
102+
.Select(e => e.Select(e => new DemoClientV2.ODataDemo.Supplier { Name = e.Name, Products = e.Products!.Select(p => new DemoClientV2.ODataDemo.Product { Name = p.Name }).ToList() }));
103+
104+
var queryResult = await query.ExecuteAsync();
98105

99106

100107
var r = queryResult;
@@ -112,7 +119,7 @@ private static async Task TestV4ClientAsync()
112119
var clientV4 = new DemoClientV4.ODataDemoClientV4(httpClient);
113120

114121
var queryResult = await clientV4
115-
.Get<Product>(e => e.ID = 4)
122+
.Get<DemoClientV4.ODataDemo.Product>(e => e.ID = 4)
116123
.Expand(e => e.Categories)
117124
.Expand(e => e.Supplier)
118125
.ExecuteAsync();
@@ -185,7 +192,7 @@ private static async Task GenerateTripPinClientAsync()
185192

186193
private static void GenerateLargeClientAsync()
187194
{
188-
195+
189196
var metadata = File.ReadAllText("Metadata/LargeMetadata.xml");
190197

191198

@@ -212,8 +219,8 @@ private static void GenerateLargeClientAsync()
212219

213220
var files = generator.GenerateClient(Path.Combine(projectDirectory, "LargeClientV4"));
214221

215-
// var files = generator.GenerateClient();
216-
222+
// var files = generator.GenerateClient();
223+
217224
}
218225

219226

test/Linq2OData.Tests/ExpandTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,51 @@ public void Expand_MultiplePropertiesFromSameNestedEntity_V4_GroupsCorrectly()
781781

782782
#endregion
783783

784+
#region Collection Select with ToList
785+
786+
[Fact]
787+
public void Select_WithToListOnNavigationProperty_V2_DoesNotDuplicateExpand()
788+
{
789+
// Regression: when a nav-property collection is projected via .Select(...).ToList(),
790+
// the inner Select MethodCallExpression was visited twice in VisitMethodCall —
791+
// once by the early "visit chained source" path and once by the normal arguments loop.
792+
// The second visit placed the nav-property as a child of itself, producing
793+
// $expand=OrderItems/OrderItems instead of $expand=OrderItems.
794+
var visitor = new QueryNodeVisitor();
795+
Expression<Func<List<TestOrder>, List<TestOrder>>> expression = list =>
796+
list.Select(o => new TestOrder
797+
{
798+
ID = o.ID,
799+
OrderItems = o.OrderItems!.Select(i => new TestOrderItem { Quantity = i.Quantity }).ToList()
800+
}).ToList();
801+
802+
var node = visitor.Parse(expression);
803+
var result = node.GetSelectExpand(ODataVersion.V2);
804+
805+
Assert.Equal("ID,OrderItems", result.select);
806+
Assert.Equal("OrderItems", result.expand);
807+
}
808+
809+
[Fact]
810+
public void Select_WithToListOnNavigationProperty_V4_DoesNotDuplicateExpand()
811+
{
812+
var visitor = new QueryNodeVisitor();
813+
Expression<Func<List<TestOrder>, List<TestOrder>>> expression = list =>
814+
list.Select(o => new TestOrder
815+
{
816+
ID = o.ID,
817+
OrderItems = o.OrderItems!.Select(i => new TestOrderItem { Quantity = i.Quantity }).ToList()
818+
}).ToList();
819+
820+
var node = visitor.Parse(expression);
821+
var result = node.GetSelectExpand(ODataVersion.V4);
822+
823+
Assert.Equal("ID", result.select);
824+
Assert.Equal("OrderItems($select=Quantity)", result.expand);
825+
}
826+
827+
#endregion
828+
784829
#region OData Version Comparison Tests
785830

786831
[Fact]

0 commit comments

Comments
 (0)