Skip to content

Commit b2dc87e

Browse files
authored
Added QueryContext extension methods for Select and Include. (#8013)
1 parent 13c074d commit b2dc87e

15 files changed

+415
-53
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// ReSharper disable once CheckNamespace
2+
using System.Linq.Expressions;
3+
using GreenDonut.Data.Internal;
4+
5+
namespace GreenDonut.Data;
6+
7+
/// <summary>
8+
/// Provides extension methods for the <see cref="QueryContext{TEntity}"/>.
9+
/// </summary>
10+
public static class GreenDonutQueryContextExtensions
11+
{
12+
public static QueryContext<TEntity> Include<TEntity, TValue>(
13+
this QueryContext<TEntity> context,
14+
Expression<Func<TEntity, TValue>>? propertySelector)
15+
{
16+
ArgumentNullException.ThrowIfNull(context);
17+
18+
if(propertySelector is null)
19+
{
20+
return context;
21+
}
22+
23+
var normalizedSelector = ExpressionHelpers.Rewrite(propertySelector);
24+
25+
if(context.Selector is null)
26+
{
27+
return context with
28+
{
29+
Selector = normalizedSelector
30+
};
31+
}
32+
else
33+
{
34+
return context with
35+
{
36+
Selector = ExpressionHelpers.Combine(context.Selector, normalizedSelector)
37+
};
38+
}
39+
}
40+
41+
public static QueryContext<TEntity> Select<TEntity>(
42+
this QueryContext<TEntity> context,
43+
Expression<Func<TEntity, TEntity>>? selector)
44+
{
45+
ArgumentNullException.ThrowIfNull(context);
46+
47+
if(selector is null)
48+
{
49+
return context;
50+
}
51+
52+
if(context.Selector is null)
53+
{
54+
return context with
55+
{
56+
Selector = selector
57+
};
58+
}
59+
else
60+
{
61+
return context with
62+
{
63+
Selector = ExpressionHelpers.Combine(context.Selector, selector)
64+
};
65+
}
66+
}
67+
}

Diff for: src/GreenDonut/src/GreenDonut.Data/Internal/ExpressionHelpers.cs

-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ private static Expression CombineExpressions(Expression first, Expression second
6464
return second;
6565
}
6666

67-
6867
private static Expression CombineWithConvertExpression(UnaryExpression first, Expression second)
6968
{
7069
if (second is MemberInitExpression otherMemberInit)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Microsoft.EntityFrameworkCore;
2+
3+
namespace GreenDonut.Data;
4+
5+
public sealed class CapturePagingQueryInterceptor : PagingQueryInterceptor
6+
{
7+
public List<QueryInfo> Queries { get; } = new();
8+
9+
public override void OnBeforeExecute<T>(IQueryable<T> query)
10+
{
11+
Queries.Add(
12+
new QueryInfo
13+
{
14+
ExpressionText = query.Expression.ToString(),
15+
QueryText = query.ToQueryString()
16+
});
17+
}
18+
}

Diff for: src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperIntegrationTests.cs

+14-51
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ public async Task Paging_Empty_PagingArgs()
1919
// Arrange
2020
var connectionString = CreateConnectionString();
2121
await SeedAsync(connectionString);
22-
var queries = new List<QueryInfo>();
23-
using var capture = new CapturePagingQueryInterceptor(queries);
22+
using var capture = new CapturePagingQueryInterceptor();
2423

2524
// Act
2625
await using var context = new CatalogContext(connectionString);
@@ -30,7 +29,7 @@ public async Task Paging_Empty_PagingArgs()
3029

3130
// Assert
3231
await CreateSnapshot()
33-
.AddQueries(queries)
32+
.AddQueries(capture.Queries)
3433
.Add(
3534
new
3635
{
@@ -51,8 +50,7 @@ public async Task Paging_First_5()
5150
// Arrange
5251
var connectionString = CreateConnectionString();
5352
await SeedAsync(connectionString);
54-
var queries = new List<QueryInfo>();
55-
using var capture = new CapturePagingQueryInterceptor(queries);
53+
using var capture = new CapturePagingQueryInterceptor();
5654

5755
// Act
5856
await using var context = new CatalogContext(connectionString);
@@ -62,7 +60,7 @@ public async Task Paging_First_5()
6260

6361
// Assert
6462
await CreateSnapshot()
65-
.AddQueries(queries)
63+
.AddQueries(capture.Queries)
6664
.Add(
6765
new
6866
{
@@ -83,8 +81,7 @@ public async Task Paging_First_5_After_Id_13()
8381
// Arrange
8482
var connectionString = CreateConnectionString();
8583
await SeedAsync(connectionString);
86-
var queries = new List<QueryInfo>();
87-
using var capture = new CapturePagingQueryInterceptor(queries);
84+
using var capture = new CapturePagingQueryInterceptor();
8885

8986
// Act
9087
await using var context = new CatalogContext(connectionString);
@@ -98,7 +95,7 @@ public async Task Paging_First_5_After_Id_13()
9895

9996
// Assert
10097
await CreateSnapshot()
101-
.AddQueries(queries)
98+
.AddQueries(capture.Queries)
10299
.Add(
103100
new
104101
{
@@ -119,8 +116,7 @@ public async Task Paging_Last_5()
119116
// Arrange
120117
var connectionString = CreateConnectionString();
121118
await SeedAsync(connectionString);
122-
var queries = new List<QueryInfo>();
123-
using var capture = new CapturePagingQueryInterceptor(queries);
119+
using var capture = new CapturePagingQueryInterceptor();
124120

125121
// Act
126122
await using var context = new CatalogContext(connectionString);
@@ -130,7 +126,7 @@ public async Task Paging_Last_5()
130126

131127
// Assert
132128
await CreateSnapshot()
133-
.AddQueries(queries)
129+
.AddQueries(capture.Queries)
134130
.Add(
135131
new
136132
{
@@ -151,8 +147,7 @@ public async Task Paging_First_5_Before_Id_96()
151147
// Arrange
152148
var connectionString = CreateConnectionString();
153149
await SeedAsync(connectionString);
154-
var queries = new List<QueryInfo>();
155-
using var capture = new CapturePagingQueryInterceptor(queries);
150+
using var capture = new CapturePagingQueryInterceptor();
156151

157152
// Act
158153
await using var context = new CatalogContext(connectionString);
@@ -166,7 +161,7 @@ public async Task Paging_First_5_Before_Id_96()
166161

167162
// Assert
168163
await CreateSnapshot()
169-
.AddQueries(queries)
164+
.AddQueries(capture.Queries)
170165
.Add(
171166
new
172167
{
@@ -193,8 +188,7 @@ public async Task BatchPaging_First_5()
193188

194189
var connectionString = CreateConnectionString();
195190
await SeedAsync(connectionString);
196-
var queries = new List<QueryInfo>();
197-
using var capture = new CapturePagingQueryInterceptor(queries);
191+
using var capture = new CapturePagingQueryInterceptor();
198192

199193
// Act
200194
await using var context = new CatalogContext(connectionString);
@@ -220,7 +214,7 @@ public async Task BatchPaging_First_5()
220214
name: page.Key.ToString());
221215
}
222216

223-
snapshot.AddQueries(queries);
217+
snapshot.AddQueries(capture.Queries);
224218
snapshot.MatchMarkdownSnapshot();
225219
}
226220

@@ -236,8 +230,7 @@ public async Task BatchPaging_Last_5()
236230

237231
var connectionString = CreateConnectionString();
238232
await SeedAsync(connectionString);
239-
var queries = new List<QueryInfo>();
240-
using var capture = new CapturePagingQueryInterceptor(queries);
233+
using var capture = new CapturePagingQueryInterceptor();
241234

242235
// Act
243236
await using var context = new CatalogContext(connectionString);
@@ -262,7 +255,7 @@ public async Task BatchPaging_Last_5()
262255
name: page.Key.ToString());
263256
}
264257

265-
snapshot.AddQueries(queries);
258+
snapshot.AddQueries(capture.Queries);
266259
snapshot.MatchMarkdownSnapshot();
267260
}
268261

@@ -398,33 +391,3 @@ private static Snapshot CreateSnapshot()
398391
#endif
399392
}
400393
}
401-
402-
file static class Extensions
403-
{
404-
public static Snapshot AddQueries(
405-
this Snapshot snapshot,
406-
List<QueryInfo> queries)
407-
{
408-
for (var i = 0; i < queries.Count; i++)
409-
{
410-
snapshot
411-
.Add(queries[i].QueryText, $"SQL {i}", "sql")
412-
.Add(queries[i].ExpressionText, $"Expression {i}");
413-
}
414-
415-
return snapshot;
416-
}
417-
}
418-
419-
file sealed class CapturePagingQueryInterceptor(List<QueryInfo> queries) : PagingQueryInterceptor
420-
{
421-
public override void OnBeforeExecute<T>(IQueryable<T> query)
422-
{
423-
queries.Add(
424-
new QueryInfo
425-
{
426-
ExpressionText = query.Expression.ToString(),
427-
QueryText = query.ToQueryString()
428-
});
429-
}
430-
}

Diff for: src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs

+123
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,120 @@ public async Task Fetch_Last_2_Items()
113113
page.MatchMarkdownSnapshot();
114114
}
115115

116+
[Fact]
117+
public async Task QueryContext_Simple_Selector()
118+
{
119+
// Arrange
120+
using var interceptor = new CapturePagingQueryInterceptor();
121+
var connectionString = CreateConnectionString();
122+
await SeedAsync(connectionString);
123+
124+
// Act
125+
var query = new QueryContext<Product>(
126+
Selector: t => new Product { Id = t.Id, Name = t.Name },
127+
Sorting: new SortDefinition<Product>().AddDescending(t => t.Id));
128+
129+
var arguments = new PagingArguments(last: 2);
130+
131+
await using var context = new CatalogContext(connectionString);
132+
133+
var page = await context.Products
134+
.With(query)
135+
.ToPageAsync(arguments);
136+
137+
// Assert
138+
CreateSnapshot()
139+
.AddQueries(interceptor.Queries)
140+
.MatchMarkdown();
141+
}
142+
143+
[Fact]
144+
public async Task QueryContext_Simple_Selector_Include_Brand()
145+
{
146+
// Arrange
147+
using var interceptor = new CapturePagingQueryInterceptor();
148+
var connectionString = CreateConnectionString();
149+
await SeedAsync(connectionString);
150+
151+
// Act
152+
var query = new QueryContext<Product>(
153+
Selector: t => new Product { Id = t.Id, Name = t.Name },
154+
Sorting: new SortDefinition<Product>().AddDescending(t => t.Id));
155+
156+
query = query.Include(t => t.Brand);
157+
158+
var arguments = new PagingArguments(last: 2);
159+
160+
await using var context = new CatalogContext(connectionString);
161+
162+
var page = await context.Products
163+
.With(query)
164+
.ToPageAsync(arguments);
165+
166+
// Assert
167+
CreateSnapshot()
168+
.AddQueries(interceptor.Queries)
169+
.MatchMarkdown();
170+
}
171+
172+
[Fact]
173+
public async Task QueryContext_Simple_Selector_Include_Brand_Name()
174+
{
175+
// Arrange
176+
using var interceptor = new CapturePagingQueryInterceptor();
177+
var connectionString = CreateConnectionString();
178+
await SeedAsync(connectionString);
179+
180+
// Act
181+
var query = new QueryContext<Product>(
182+
Selector: t => new Product { Id = t.Id, Name = t.Name },
183+
Sorting: new SortDefinition<Product>().AddDescending(t => t.Id));
184+
185+
query = query.Select(t => new Product { Brand = new Brand { Name = t.Brand!.Name } });
186+
187+
var arguments = new PagingArguments(last: 2);
188+
189+
await using var context = new CatalogContext(connectionString);
190+
191+
var page = await context.Products
192+
.With(query)
193+
.ToPageAsync(arguments);
194+
195+
// Assert
196+
CreateSnapshot()
197+
.AddQueries(interceptor.Queries)
198+
.MatchMarkdown();
199+
}
200+
201+
[Fact]
202+
public async Task QueryContext_Simple_Selector_Include_Product_List()
203+
{
204+
// Arrange
205+
using var interceptor = new CapturePagingQueryInterceptor();
206+
var connectionString = CreateConnectionString();
207+
await SeedAsync(connectionString);
208+
209+
// Act
210+
var query = new QueryContext<Brand>(
211+
Selector: t => new Brand { Id = t.Id, Name = t.Name },
212+
Sorting: new SortDefinition<Brand>().AddDescending(t => t.Id));
213+
214+
query = query.Select(t => new Brand { Products = t.Products.Select(p => new Product { Id = p.Id, Name = p.Name }).ToList() });
215+
216+
var arguments = new PagingArguments(last: 2);
217+
218+
await using var context = new CatalogContext(connectionString);
219+
220+
var page = await context.Brands
221+
.With(query)
222+
.ToPageAsync(arguments);
223+
224+
// Assert
225+
CreateSnapshot()
226+
.AddQueries(interceptor.Queries)
227+
.MatchMarkdown();
228+
}
229+
116230
[Fact]
117231
public async Task Fetch_Last_2_Items_Before_Last_Page()
118232
{
@@ -306,4 +420,13 @@ private static async Task SeedTestAsync(string connectionString)
306420

307421
await context.SaveChangesAsync();
308422
}
423+
424+
private static Snapshot CreateSnapshot()
425+
{
426+
#if NET9_0_OR_GREATER
427+
return Snapshot.Create();
428+
#else
429+
return Snapshot.Create("NET8_0");
430+
#endif
431+
}
309432
}

0 commit comments

Comments
 (0)