Skip to content

Commit 6878fe9

Browse files
authored
Added more include overloads. (#8002)
1 parent 77b8d12 commit 6878fe9

14 files changed

+420
-28
lines changed

src/GreenDonut/src/GreenDonut.Data/Extensions/GreenDonutPaginationBatchingDataLoaderExtensions.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,18 +102,15 @@ private static IDataLoader<TKey, Page<TValue>> WithInternal<TKey, TValue>(
102102
/// <typeparam name="TValue">
103103
/// The value type of the DataLoader.
104104
/// </typeparam>
105-
/// <typeparam name="TElement">
106-
/// The element type of the projection.
107-
/// </typeparam>
108105
/// <returns>
109106
/// Returns the DataLoader with the added projection.
110107
/// </returns>
111108
/// <exception cref="ArgumentNullException">
112109
/// Throws if the <paramref name="dataLoader"/> is <c>null</c>.
113110
/// </exception>
114-
public static IDataLoader<TKey, Page<TValue>> Select<TElement, TKey, TValue>(
111+
public static IDataLoader<TKey, Page<TValue>> Select<TKey, TValue>(
115112
this IDataLoader<TKey, Page<TValue>> dataLoader,
116-
Expression<Func<TElement, TElement>>? selector)
113+
Expression<Func<TValue, TValue>>? selector)
117114
where TKey : notnull
118115
{
119116
if (dataLoader is null)

src/GreenDonut/src/GreenDonut.Data/Extensions/GreenDonutSelectionDataLoaderExtensions.cs

Lines changed: 139 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ public static IDataLoader<TKey, List<TValue>> Select<TKey, TValue>(
170170
/// <typeparam name="TValue">
171171
/// The value type.
172172
/// </typeparam>
173+
/// <typeparam name="TResult">
174+
/// The selector result type.
175+
/// </typeparam>
173176
/// <returns>
174177
/// Returns the DataLoader with the property included.
175178
/// </returns>
@@ -179,10 +182,144 @@ public static IDataLoader<TKey, List<TValue>> Select<TKey, TValue>(
179182
/// <exception cref="ArgumentException">
180183
/// Throws if the include selector is not a property selector.
181184
/// </exception>
182-
public static IDataLoader<TKey, TValue> Include<TKey, TValue>(
185+
public static IDataLoader<TKey, TValue> Include<TKey, TValue, TResult>(
183186
this IDataLoader<TKey, TValue> dataLoader,
184-
Expression<Func<TValue, object?>> includeSelector)
187+
Expression<Func<TValue, TResult>> includeSelector)
188+
where TKey : notnull
189+
{
190+
AssertIncludePossible(dataLoader, includeSelector);
191+
192+
var context = dataLoader.GetOrSetState(
193+
DataLoaderStateKeys.Selector,
194+
_ => new DefaultSelectorBuilder());
195+
context.Add(Rewrite(includeSelector));
196+
return dataLoader;
197+
}
198+
199+
/// <summary>
200+
/// Includes a property in the query.
201+
/// </summary>
202+
/// <param name="dataLoader">
203+
/// The DataLoader to include the property in.
204+
/// </param>
205+
/// <param name="includeSelector">
206+
/// The property selector.
207+
/// </param>
208+
/// <typeparam name="TKey">
209+
/// The key type.
210+
/// </typeparam>
211+
/// <typeparam name="TValue">
212+
/// The value type.
213+
/// </typeparam>
214+
/// <typeparam name="TResult">
215+
/// The selector result type.
216+
/// </typeparam>
217+
/// <returns>
218+
/// Returns the DataLoader with the property included.
219+
/// </returns>
220+
/// <exception cref="ArgumentNullException">
221+
/// Throws if <paramref name="dataLoader"/> is <c>null</c>.
222+
/// </exception>
223+
/// <exception cref="ArgumentException">
224+
/// Throws if the include selector is not a property selector.
225+
/// </exception>
226+
public static IDataLoader<TKey, Page<TValue>> Include<TKey, TValue, TResult>(
227+
this IDataLoader<TKey, Page<TValue>> dataLoader,
228+
Expression<Func<TValue, TResult>> includeSelector)
185229
where TKey : notnull
230+
{
231+
AssertIncludePossible(dataLoader, includeSelector);
232+
233+
var context = dataLoader.GetOrSetState(
234+
DataLoaderStateKeys.Selector,
235+
_ => new DefaultSelectorBuilder());
236+
context.Add(Rewrite(includeSelector));
237+
return dataLoader;
238+
}
239+
240+
/// <summary>
241+
/// Includes a property in the query.
242+
/// </summary>
243+
/// <param name="dataLoader">
244+
/// The DataLoader to include the property in.
245+
/// </param>
246+
/// <param name="includeSelector">
247+
/// The property selector.
248+
/// </param>
249+
/// <typeparam name="TKey">
250+
/// The key type.
251+
/// </typeparam>
252+
/// <typeparam name="TValue">
253+
/// The value type.
254+
/// </typeparam>
255+
/// <typeparam name="TResult">
256+
/// The selector result type.
257+
/// </typeparam>
258+
/// <returns>
259+
/// Returns the DataLoader with the property included.
260+
/// </returns>
261+
/// <exception cref="ArgumentNullException">
262+
/// Throws if <paramref name="dataLoader"/> is <c>null</c>.
263+
/// </exception>
264+
/// <exception cref="ArgumentException">
265+
/// Throws if the include selector is not a property selector.
266+
/// </exception>
267+
public static IDataLoader<TKey, List<TValue>> Include<TKey, TValue, TResult>(
268+
this IDataLoader<TKey, List<TValue>> dataLoader,
269+
Expression<Func<TValue, TResult>> includeSelector)
270+
where TKey : notnull
271+
{
272+
AssertIncludePossible(dataLoader, includeSelector);
273+
274+
var context = dataLoader.GetOrSetState(
275+
DataLoaderStateKeys.Selector,
276+
_ => new DefaultSelectorBuilder());
277+
context.Add(Rewrite(includeSelector));
278+
return dataLoader;
279+
}
280+
281+
/// <summary>
282+
/// Includes a property in the query.
283+
/// </summary>
284+
/// <param name="dataLoader">
285+
/// The DataLoader to include the property in.
286+
/// </param>
287+
/// <param name="includeSelector">
288+
/// The property selector.
289+
/// </param>
290+
/// <typeparam name="TKey">
291+
/// The key type.
292+
/// </typeparam>
293+
/// <typeparam name="TValue">
294+
/// The value type.
295+
/// </typeparam>
296+
/// <typeparam name="TResult">
297+
/// The selector result type.
298+
/// </typeparam>
299+
/// <returns>
300+
/// Returns the DataLoader with the property included.
301+
/// </returns>
302+
/// <exception cref="ArgumentNullException">
303+
/// Throws if <paramref name="dataLoader"/> is <c>null</c>.
304+
/// </exception>
305+
/// <exception cref="ArgumentException">
306+
/// Throws if the include selector is not a property selector.
307+
/// </exception>
308+
public static IDataLoader<TKey, TValue[]> Include<TKey, TValue, TResult>(
309+
this IDataLoader<TKey, TValue[]> dataLoader,
310+
Expression<Func<TValue, TResult>> includeSelector)
311+
where TKey : notnull
312+
{
313+
AssertIncludePossible(dataLoader, includeSelector);
314+
315+
var context = dataLoader.GetOrSetState(
316+
DataLoaderStateKeys.Selector,
317+
_ => new DefaultSelectorBuilder());
318+
context.Add(Rewrite(includeSelector));
319+
return dataLoader;
320+
}
321+
322+
private static void AssertIncludePossible(IDataLoader dataLoader, Expression includeSelector)
186323
{
187324
if (dataLoader is null)
188325
{
@@ -214,11 +351,5 @@ public static IDataLoader<TKey, TValue> Include<TKey, TValue>(
214351
"The include selector must be a property selector.",
215352
nameof(includeSelector));
216353
}
217-
218-
var context = dataLoader.GetOrSetState(
219-
DataLoaderStateKeys.Selector,
220-
_ => new DefaultSelectorBuilder());
221-
context.Add(Rewrite(includeSelector));
222-
return dataLoader;
223354
}
224355
}

src/GreenDonut/src/GreenDonut.Data/Internal/ExpressionHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ private static Expression CombineConditionalAndMemberInit(
180180
}
181181

182182
public static Expression<Func<TRoot, TRoot>> Rewrite<TRoot, TKey>(
183-
Expression<Func<TRoot, TKey?>> selector)
183+
Expression<Func<TRoot, TKey>> selector)
184184
{
185185
var parameter = selector.Parameters[0];
186186
var bindings = new List<MemberBinding>();

src/HotChocolate/Core/src/Execution.Projections/Extensions/HotChocolateExecutionDataLoaderExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Linq.Expressions;
12
using HotChocolate.Execution.Processing;
23

34
// ReSharper disable once CheckNamespace
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
using GreenDonut;
2+
using GreenDonut.Data;
3+
using HotChocolate.Data.Data;
4+
using HotChocolate.Data.Migrations;
5+
using HotChocolate.Data.Models;
6+
using HotChocolate.Data.Services;
7+
using Microsoft.EntityFrameworkCore;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Squadron;
10+
11+
namespace HotChocolate.Data;
12+
13+
[Collection(PostgresCacheCollectionFixture.DefinitionName)]
14+
public class DataLoaderTests(PostgreSqlResource resource)
15+
{
16+
[Fact]
17+
public async Task Include_On_List_Results()
18+
{
19+
// arrange
20+
using var interceptor = new TestQueryInterceptor();
21+
using var cts = new CancellationTokenSource(2000);
22+
await using var services = CreateServer();
23+
await using var scope = services.CreateAsyncScope();
24+
await using var context = scope.ServiceProvider.GetRequiredService<CatalogContext>();
25+
var seeder = scope.ServiceProvider.GetRequiredService<IDbSeeder<CatalogContext>>();
26+
await context.Database.EnsureCreatedAsync(cts.Token);
27+
await seeder.SeedAsync(context);
28+
29+
var productByBrand = scope.ServiceProvider.GetRequiredService<IProductListByBrandDataLoader>();
30+
31+
// act
32+
var products = await productByBrand
33+
.Select(t => new Product { BrandId = t.BrandId, Name = t.Name })
34+
.Include(t => t.Price)
35+
.LoadRequiredAsync(1, cts.Token);
36+
37+
// assert
38+
Assert.Equal(10, products.Count);
39+
interceptor.MatchSnapshot();
40+
}
41+
42+
[Fact]
43+
public async Task Include_On_Array_Results()
44+
{
45+
// arrange
46+
using var interceptor = new TestQueryInterceptor();
47+
using var cts = new CancellationTokenSource(2000);
48+
await using var services = CreateServer();
49+
await using var scope = services.CreateAsyncScope();
50+
await using var context = scope.ServiceProvider.GetRequiredService<CatalogContext>();
51+
var seeder = scope.ServiceProvider.GetRequiredService<IDbSeeder<CatalogContext>>();
52+
await context.Database.EnsureCreatedAsync(cts.Token);
53+
await seeder.SeedAsync(context);
54+
55+
var productByBrand = scope.ServiceProvider.GetRequiredService<IProductArrayByBrandDataLoader>();
56+
57+
// act
58+
var products = await productByBrand
59+
.Select(t => new Product { BrandId = t.BrandId, Name = t.Name })
60+
.Include(t => t.Price)
61+
.LoadRequiredAsync(1, cts.Token);
62+
63+
// assert
64+
Assert.Equal(10, products.Length);
65+
interceptor.MatchSnapshot();
66+
}
67+
68+
[Fact]
69+
public async Task Include_On_Page_Results()
70+
{
71+
// arrange
72+
using var interceptor = new TestQueryInterceptor();
73+
using var cts = new CancellationTokenSource(2000);
74+
await using var services = CreateServer();
75+
await using var scope = services.CreateAsyncScope();
76+
await using var context = scope.ServiceProvider.GetRequiredService<CatalogContext>();
77+
var seeder = scope.ServiceProvider.GetRequiredService<IDbSeeder<CatalogContext>>();
78+
await context.Database.EnsureCreatedAsync(cts.Token);
79+
await seeder.SeedAsync(context);
80+
81+
var productByBrand = scope.ServiceProvider.GetRequiredService<IProductsByBrandDataLoader>();
82+
83+
// act
84+
var products = await productByBrand
85+
.With(new PagingArguments { First = 5 })
86+
.Select(t => new Product { BrandId = t.BrandId, Name = t.Name })
87+
.Include(t => t.Price)
88+
.LoadRequiredAsync(1, cts.Token);
89+
90+
// assert
91+
Assert.Equal(5, products.Items.Length);
92+
interceptor.MatchSnapshot();
93+
}
94+
95+
private ServiceProvider CreateServer()
96+
{
97+
var db = "db_" + Guid.NewGuid().ToString("N");
98+
var connectionString = resource.GetConnectionString(db);
99+
100+
var services = new ServiceCollection();
101+
102+
services
103+
.AddLogging()
104+
.AddDbContext<CatalogContext>(c => c.UseNpgsql(connectionString));
105+
106+
services
107+
.AddSingleton<BrandService>()
108+
.AddSingleton<ProductService>();
109+
110+
services
111+
.AddGraphQLServer()
112+
.AddCustomTypes()
113+
.AddGlobalObjectIdentification()
114+
.AddPagingArguments()
115+
.AddFiltering()
116+
.AddSorting()
117+
.ModifyRequestOptions(o => o.IncludeExceptionDetails = true);
118+
119+
services.AddSingleton<IDbSeeder<CatalogContext>, CatalogContextSeed>();
120+
121+
return services.BuildServiceProvider();
122+
}
123+
}
124+
125+
file static class Extensions
126+
{
127+
public static void MatchSnapshot(
128+
this TestQueryInterceptor queryInterceptor)
129+
{
130+
#if NET9_0_OR_GREATER
131+
var snapshot = Snapshot.Create();
132+
#else
133+
var snapshot = Snapshot.Create(postFix: "_net_8_0");
134+
#endif
135+
136+
for (var i = 0; i < queryInterceptor.Queries.Count; i++)
137+
{
138+
var sql = queryInterceptor.Queries[i];
139+
snapshot.Add(sql, $"Query {i + 1}", MarkdownLanguages.Sql);
140+
}
141+
142+
snapshot.MatchMarkdown();
143+
}
144+
}

src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using GreenDonut.Data;
21
using HotChocolate.Data.Data;
32
using HotChocolate.Data.Migrations;
43
using HotChocolate.Data.Services;
@@ -301,17 +300,4 @@ private static void MatchSnapshot(
301300

302301
snapshot.MatchMarkdown();
303302
}
304-
305-
private class TestQueryInterceptor : PagingQueryInterceptor
306-
{
307-
public List<string> Queries { get; } = new();
308-
309-
public override void OnBeforeExecute<T>(IQueryable<T> query)
310-
{
311-
lock(Queries)
312-
{
313-
Queries.Add(query.ToQueryString());
314-
}
315-
}
316-
}
317303
}

0 commit comments

Comments
 (0)