Skip to content

Commit 95b6ca7

Browse files
committed
Aligned DataLoader options behavior. (#8044)
1 parent 8006931 commit 95b6ca7

File tree

11 files changed

+149
-28
lines changed

11 files changed

+149
-28
lines changed

.vscode/tasks.json

+12-5
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,19 @@
1717
}
1818
},
1919
{
20-
"label": "Build src/All.sln",
20+
"type": "dotnet",
21+
"task": "build",
22+
"group": "build",
23+
"problemMatcher": [],
24+
"label": "dotnet: build"
25+
},
26+
{
27+
"label": "Build src/All.slnx",
2128
"command": "dotnet",
2229
"type": "shell",
2330
"args": [
2431
"build",
25-
"src/All.sln",
32+
"src/All.slnx",
2633
"/property:GenerateFullPaths=true",
2734
"/consoleloggerparameters:NoSummary"
2835
],
@@ -49,12 +56,12 @@
4956
"problemMatcher": "$msCompile"
5057
},
5158
{
52-
"label": "Test src/All.sln",
59+
"label": "Test src/All.slnx",
5360
"command": "dotnet",
5461
"type": "shell",
5562
"args": [
5663
"test",
57-
"src/All.sln",
64+
"src/All.slnx",
5865
"--verbosity q",
5966
"/property:GenerateFullPaths=true",
6067
"/consoleloggerparameters:NoSummary"
@@ -64,6 +71,6 @@
6471
"reveal": "silent"
6572
},
6673
"problemMatcher": "$msCompile"
67-
},
74+
}
6875
]
6976
}

src/GreenDonut/src/GreenDonut/BatchDataLoader.cs

-4
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@ protected BatchDataLoader(
2828
DataLoaderOptions options)
2929
: base(batchScheduler, options)
3030
{
31-
if (options is null)
32-
{
33-
throw new ArgumentNullException(nameof(options));
34-
}
3531
}
3632

3733
/// <inheritdoc />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
namespace GreenDonut;
2+
3+
/// <summary>
4+
/// This class represents a branched <see cref="IDataLoader{TKey, TValue}"/>.
5+
/// </summary>
6+
/// <typeparam name="TKey">
7+
/// The type of the key.
8+
/// </typeparam>
9+
/// <typeparam name="TValue">
10+
/// The type of the value.
11+
/// </typeparam>
12+
public class BranchedDataLoader<TKey, TValue>
13+
: DataLoaderBase<TKey, TValue>
14+
where TKey : notnull
15+
{
16+
private readonly DataLoaderBase<TKey, TValue> _root;
17+
18+
public BranchedDataLoader(
19+
DataLoaderBase<TKey, TValue> root,
20+
string key)
21+
: base(root.BatchScheduler, root.Options)
22+
{
23+
_root = root;
24+
CacheKeyType = $"{root.CacheKeyType}:{key}";
25+
ContextData = root.ContextData;
26+
}
27+
28+
public IDataLoader<TKey, TValue> Root => _root;
29+
30+
protected internal override string CacheKeyType { get; }
31+
32+
protected sealed override bool AllowCachePropagation => false;
33+
34+
protected override bool AllowBranching => true;
35+
36+
protected internal override ValueTask FetchAsync(
37+
IReadOnlyList<TKey> keys,
38+
Memory<Result<TValue?>> results,
39+
DataLoaderFetchContext<TValue> context,
40+
CancellationToken cancellationToken)
41+
=> _root.FetchAsync(keys, results, context, cancellationToken);
42+
}

src/GreenDonut/src/GreenDonut/DataLoaderBase.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,11 @@ public abstract partial class DataLoaderBase<TKey, TValue>
4646
/// <exception cref="ArgumentNullException">
4747
/// Throws if <paramref name="options"/> is <c>null</c>.
4848
/// </exception>
49-
protected DataLoaderBase(IBatchScheduler batchScheduler, DataLoaderOptions? options = null)
49+
protected DataLoaderBase(IBatchScheduler batchScheduler, DataLoaderOptions options)
5050
{
51-
options ??= new DataLoaderOptions();
51+
ArgumentNullException.ThrowIfNull(batchScheduler);
52+
ArgumentNullException.ThrowIfNull(options);
53+
5254
_diagnosticEvents = options.DiagnosticEvents ?? Default;
5355
Cache = options.Cache;
5456
_batchScheduler = batchScheduler;
@@ -226,7 +228,7 @@ void Initialize()
226228
ct);
227229
}
228230
}
229-
231+
230232
/// <inheritdoc />
231233
public void SetCacheEntry(TKey key, Task<TValue?> value)
232234
{

src/GreenDonut/src/GreenDonut/GroupedDataLoader.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public abstract class GroupedDataLoader<TKey, TValue>
2525
/// </exception>
2626
protected GroupedDataLoader(
2727
IBatchScheduler batchScheduler,
28-
DataLoaderOptions? options = null)
28+
DataLoaderOptions options)
2929
: base(batchScheduler, options)
3030
{ }
3131

@@ -92,7 +92,7 @@ public abstract class StatefulGroupedDataLoader<TKey, TValue>
9292
/// </exception>
9393
protected StatefulGroupedDataLoader(
9494
IBatchScheduler batchScheduler,
95-
DataLoaderOptions? options = null)
95+
DataLoaderOptions options)
9696
: base(batchScheduler, options)
9797
{ }
9898

src/GreenDonut/test/GreenDonut.Tests/DataLoader.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ namespace GreenDonut;
33
public class DataLoader<TKey, TValue>(
44
FetchDataDelegate<TKey, TValue> fetch,
55
IBatchScheduler batchScheduler,
6-
DataLoaderOptions? options = null)
6+
DataLoaderOptions options)
77
: DataLoaderBase<TKey, TValue>(batchScheduler, options)
88
where TKey : notnull
99
{

src/GreenDonut/test/GreenDonut.Tests/DataLoaderExtensionsTests.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// ReSharper disable InconsistentNaming
22

3+
using System.Reflection.Metadata;
4+
35
namespace GreenDonut;
46

57
public class DataLoaderExtensionsTests
@@ -24,7 +26,7 @@ public void SetCacheEntryKeyNull()
2426
// arrange
2527
var fetch = TestHelpers.CreateFetch<string, string>();
2628
var batchScheduler = new ManualBatchScheduler();
27-
var loader = new DataLoader<string, string>(fetch, batchScheduler);
29+
var loader = new DataLoader<string, string>(fetch, batchScheduler, new DataLoaderOptions());
2830
var value = "Bar";
2931

3032
// act
@@ -40,7 +42,7 @@ public void SetCacheEntryNoException()
4042
// arrange
4143
var fetch = TestHelpers.CreateFetch<string, string>();
4244
var batchScheduler = new ManualBatchScheduler();
43-
var loader = new DataLoader<string, string>(fetch, batchScheduler);
45+
var loader = new DataLoader<string, string>(fetch, batchScheduler, new DataLoaderOptions());
4446
var key = "Foo";
4547

4648
// act
@@ -149,7 +151,7 @@ public void IDataLoaderSetCacheEntryNoException()
149151
// arrange
150152
var fetch = TestHelpers.CreateFetch<string, string>();
151153
var batchScheduler = new ManualBatchScheduler();
152-
IDataLoader loader = new DataLoader<string, string>(fetch, batchScheduler);
154+
IDataLoader loader = new DataLoader<string, string>(fetch, batchScheduler, new DataLoaderOptions());
153155
object key = "Foo";
154156

155157
// act

src/GreenDonut/test/GreenDonut.Tests/DataLoaderStateTests.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ public static class DataLoaderStateTests
66
public static async Task SetStateInferredKey()
77
{
88
// arrange
9-
var loader = new DummyDataLoader(typeof(string).FullName!);
9+
var loader = new DummyDataLoader(typeof(string).FullName!, new DataLoaderOptions());
1010

1111
// act
1212
await loader.SetState("abc").LoadAsync("def");
@@ -19,7 +19,7 @@ public static async Task SetStateInferredKey()
1919
public static async Task SetStateExplicitKey()
2020
{
2121
// arrange
22-
var loader = new DummyDataLoader("abc");
22+
var loader = new DummyDataLoader("abc", new DataLoaderOptions());
2323

2424
// act
2525
await loader.SetState("abc", "def").LoadAsync("ghi");
@@ -32,7 +32,7 @@ public static async Task SetStateExplicitKey()
3232
public static async Task TrySetStateInferredKey()
3333
{
3434
// arrange
35-
var loader = new DummyDataLoader(typeof(string).FullName!);
35+
var loader = new DummyDataLoader(typeof(string).FullName!, new DataLoaderOptions());
3636

3737
// act
3838
await loader.SetState("abc").TrySetState("xyz").LoadAsync("def");
@@ -45,7 +45,7 @@ public static async Task TrySetStateInferredKey()
4545
public static async Task TrySetStateExplicitKey()
4646
{
4747
// arrange
48-
var loader = new DummyDataLoader("abc");
48+
var loader = new DummyDataLoader("abc", new DataLoaderOptions());
4949

5050
// act
5151
await loader.SetState("abc", "def").TrySetState("abc", "xyz").LoadAsync("def");
@@ -58,7 +58,7 @@ public static async Task TrySetStateExplicitKey()
5858
public static async Task AddStateEnumerableInferredKey()
5959
{
6060
// arrange
61-
var loader = new DummyDataLoader(typeof(string).FullName!);
61+
var loader = new DummyDataLoader(typeof(string).FullName!, new DataLoaderOptions());
6262

6363
// act
6464
await loader.AddStateEnumerable("abc").AddStateEnumerable("xyz").LoadAsync("def");
@@ -74,7 +74,7 @@ public static async Task AddStateEnumerableInferredKey()
7474
public static async Task AddStateEnumerableExplicitKey()
7575
{
7676
// arrange
77-
var loader = new DummyDataLoader("abc");
77+
var loader = new DummyDataLoader("abc", new DataLoaderOptions());
7878

7979
// act
8080
await loader.AddStateEnumerable("abc", "def").AddStateEnumerable("abc", "xyz").LoadAsync("def");
@@ -86,7 +86,7 @@ public static async Task AddStateEnumerableExplicitKey()
8686
item => Assert.Equal("xyz", item));
8787
}
8888

89-
public class DummyDataLoader(string expectedKey, DataLoaderOptions? options = null)
89+
public class DummyDataLoader(string expectedKey, DataLoaderOptions options)
9090
: DataLoaderBase<string, string>(AutoBatchScheduler.Default, options)
9191
{
9292
public object? State { get; set; }

src/GreenDonut/test/GreenDonut.Tests/DependencyInjection/DataLoaderServiceCollectionExtensionsTests.cs

+8-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ public void ImplFactoryIsCalledWhenServiceIsResolved()
1717
.AddDataLoader(sp =>
1818
{
1919
factoryCalled = true;
20-
return new DataLoader<string, string>(fetch, sp.GetRequiredService<IBatchScheduler>());
20+
return new DataLoader<string, string>(
21+
fetch,
22+
sp.GetRequiredService<IBatchScheduler>(),
23+
sp.GetRequiredService<DataLoaderOptions>());
2124
});
2225
var scope = services.BuildServiceProvider().CreateScope();
2326

@@ -40,7 +43,10 @@ public void InterfaceImplFactoryIsCalledWhenServiceIsResolved()
4043
.AddDataLoader<IDataLoader<string, string>, DataLoader<string, string>>(sp =>
4144
{
4245
factoryCalled = true;
43-
return new DataLoader<string, string>(fetch, sp.GetRequiredService<IBatchScheduler>());
46+
return new DataLoader<string, string>(
47+
fetch,
48+
sp.GetRequiredService<IBatchScheduler>(),
49+
sp.GetRequiredService<DataLoaderOptions>());
4450
});
4551
var scope = services.BuildServiceProvider().CreateScope();
4652

src/HotChocolate/Core/test/Execution.Tests/Integration/DataLoader/UseDataLoaderTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ public class TestGroupedLoader : GroupedDataLoader<int, Foo>
256256
{
257257
public TestGroupedLoader(
258258
IBatchScheduler batchScheduler,
259-
DataLoaderOptions? options = null)
259+
DataLoaderOptions options)
260260
: base(batchScheduler, options)
261261
{
262262
}

website/src/docs/hotchocolate/v15/migrating/migrate-from-14-to-15.md

+66
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,74 @@ Please ensure that your clients are sending date/time strings in the correct for
5050
- `DateOnly` is now bound to `LocalDateType` instead of `DateType`.
5151
- `TimeOnly` is now bound to `LocalTimeType` instead of `TimeSpanType`.
5252

53+
## DataLoaderOptions are now required
54+
55+
Starting with Hot Chocolate 15, the `DataLoaderOptions` must be passed down to the DataLoaderBase constructor.
56+
57+
```csharp
58+
public class ProductByIdDataLoader : BatchDataLoader<int, Product>
59+
{
60+
private readonly IServiceProvider _services;
61+
62+
public ProductDataLoader1(
63+
IBatchScheduler batchScheduler,
64+
DataLoaderOptions options) // the options are now required ...
65+
: base(batchScheduler, options)
66+
{
67+
}
68+
}
69+
```
70+
71+
## DataLoader Dependency Injection
72+
73+
DataLoader must not be manually registered with the dependency injection and must use the extension methods provided by GreenDonut.
74+
75+
```csharp
76+
services.AddDataLoader<ProductByIdDataLoader>();
77+
services.AddDataLoader<IProductByIdDataLoader, ProductByIdDataLoader>();
78+
services.AddDataLoader<IProductByIdDataLoader>(sp => ....);
79+
```
80+
81+
We recommend to use the source-generated DataLoaders and let the source generator write the registration code for you.
82+
83+
> If you register DataLoader manually they will be stuck in the auto-dispatch mode, which basically means that they will no longer batch.
84+
85+
DataLoader are available as scoped services and can be injected like any other scoped service.
86+
87+
```csharp
88+
public class ProductService(IProductByIdDataLoader productByIdData)
89+
{
90+
public async Task<Product> GetProductById(int id)
91+
{
92+
return await productByIdDataLoader.LoadAsync(id);
93+
}
94+
}
95+
```
96+
5397
# Deprecations
5498

99+
## GroupDataLoader
100+
101+
We no longer recommend using the `GroupDataLoader`, as the same functionality can be achieved with a BatchDataLoader, which provides greater flexibility in determining the type of list returned.
102+
103+
Use the following patter to replace the `GroupDataLoader`:
104+
105+
```csharp
106+
internal static class ProductDataLoader
107+
{
108+
[DataLoader]
109+
public static async Task<Dictionary<int, Product[]>> GetProductsByBrandIdAsync(
110+
IReadOnlyList<int> brandIds,
111+
CatalogContext context,
112+
CancellationToken cancellationToken)
113+
=> await context.Products
114+
.Where(t => brandIds.Contains(t.BrandId))
115+
.GroupBy(t => t.BrandId)
116+
.Select(t => new { t.Key, Items = t.OrderBy(p => p.Name).ToArray() })
117+
.ToDictionaryAsync(t => t.Key, t => t.Items, cancellationToken);
118+
}
119+
```
120+
55121
## AdHoc DataLoader
56122

57123
The ad-hoc DataLoader methods on IResolverContext have been deprecated.

0 commit comments

Comments
 (0)