From 800693145275869c835c8c504f319a76583a9eeb Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 19 Feb 2025 13:03:35 +0100 Subject: [PATCH 01/64] Fixed type DataLOader to DataLoader (#8037) --- src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs index 7feb9939efd..7dbf71f8146 100644 --- a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs +++ b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs @@ -199,7 +199,7 @@ public interface IReadOnlySchemaOptions /// /// Specifies if the elements of paginated root fields should be published - /// to the DataLOader promise cache. + /// to the DataLoader promise cache. /// bool PublishRootFieldPagesToPromiseCache { get; } } From 95b6ca76cbf380e2e7b89ad8e8ddd6b59b22a2e8 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 19 Feb 2025 22:30:39 +0100 Subject: [PATCH 02/64] Aligned DataLoader options behavior. (#8044) --- .vscode/tasks.json | 17 +++-- .../src/GreenDonut/BatchDataLoader.cs | 4 -- .../src/GreenDonut/BranchedDataLoader.cs | 42 ++++++++++++ .../src/GreenDonut/DataLoaderBase.cs | 8 ++- .../src/GreenDonut/GroupedDataLoader.cs | 4 +- .../test/GreenDonut.Tests/DataLoader.cs | 2 +- .../DataLoaderExtensionsTests.cs | 8 ++- .../GreenDonut.Tests/DataLoaderStateTests.cs | 14 ++-- ...aLoaderServiceCollectionExtensionsTests.cs | 10 ++- .../DataLoader/UseDataLoaderTests.cs | 2 +- .../v15/migrating/migrate-from-14-to-15.md | 66 +++++++++++++++++++ 11 files changed, 149 insertions(+), 28 deletions(-) create mode 100644 src/GreenDonut/src/GreenDonut/BranchedDataLoader.cs diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b85b3ea8b9d..3835de1c673 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -17,12 +17,19 @@ } }, { - "label": "Build src/All.sln", + "type": "dotnet", + "task": "build", + "group": "build", + "problemMatcher": [], + "label": "dotnet: build" + }, + { + "label": "Build src/All.slnx", "command": "dotnet", "type": "shell", "args": [ "build", - "src/All.sln", + "src/All.slnx", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], @@ -49,12 +56,12 @@ "problemMatcher": "$msCompile" }, { - "label": "Test src/All.sln", + "label": "Test src/All.slnx", "command": "dotnet", "type": "shell", "args": [ "test", - "src/All.sln", + "src/All.slnx", "--verbosity q", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" @@ -64,6 +71,6 @@ "reveal": "silent" }, "problemMatcher": "$msCompile" - }, + } ] } diff --git a/src/GreenDonut/src/GreenDonut/BatchDataLoader.cs b/src/GreenDonut/src/GreenDonut/BatchDataLoader.cs index d4b34e2ca69..d4e47df966b 100644 --- a/src/GreenDonut/src/GreenDonut/BatchDataLoader.cs +++ b/src/GreenDonut/src/GreenDonut/BatchDataLoader.cs @@ -28,10 +28,6 @@ protected BatchDataLoader( DataLoaderOptions options) : base(batchScheduler, options) { - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } } /// diff --git a/src/GreenDonut/src/GreenDonut/BranchedDataLoader.cs b/src/GreenDonut/src/GreenDonut/BranchedDataLoader.cs new file mode 100644 index 00000000000..731feff1e96 --- /dev/null +++ b/src/GreenDonut/src/GreenDonut/BranchedDataLoader.cs @@ -0,0 +1,42 @@ +namespace GreenDonut; + +/// +/// This class represents a branched . +/// +/// +/// The type of the key. +/// +/// +/// The type of the value. +/// +public class BranchedDataLoader + : DataLoaderBase + where TKey : notnull +{ + private readonly DataLoaderBase _root; + + public BranchedDataLoader( + DataLoaderBase root, + string key) + : base(root.BatchScheduler, root.Options) + { + _root = root; + CacheKeyType = $"{root.CacheKeyType}:{key}"; + ContextData = root.ContextData; + } + + public IDataLoader Root => _root; + + protected internal override string CacheKeyType { get; } + + protected sealed override bool AllowCachePropagation => false; + + protected override bool AllowBranching => true; + + protected internal override ValueTask FetchAsync( + IReadOnlyList keys, + Memory> results, + DataLoaderFetchContext context, + CancellationToken cancellationToken) + => _root.FetchAsync(keys, results, context, cancellationToken); +} diff --git a/src/GreenDonut/src/GreenDonut/DataLoaderBase.cs b/src/GreenDonut/src/GreenDonut/DataLoaderBase.cs index a9782f649bc..2b00050be0c 100644 --- a/src/GreenDonut/src/GreenDonut/DataLoaderBase.cs +++ b/src/GreenDonut/src/GreenDonut/DataLoaderBase.cs @@ -46,9 +46,11 @@ public abstract partial class DataLoaderBase /// /// Throws if is null. /// - protected DataLoaderBase(IBatchScheduler batchScheduler, DataLoaderOptions? options = null) + protected DataLoaderBase(IBatchScheduler batchScheduler, DataLoaderOptions options) { - options ??= new DataLoaderOptions(); + ArgumentNullException.ThrowIfNull(batchScheduler); + ArgumentNullException.ThrowIfNull(options); + _diagnosticEvents = options.DiagnosticEvents ?? Default; Cache = options.Cache; _batchScheduler = batchScheduler; @@ -226,7 +228,7 @@ void Initialize() ct); } } - + /// public void SetCacheEntry(TKey key, Task value) { diff --git a/src/GreenDonut/src/GreenDonut/GroupedDataLoader.cs b/src/GreenDonut/src/GreenDonut/GroupedDataLoader.cs index 1e676ba7fca..fc61d6b28e4 100644 --- a/src/GreenDonut/src/GreenDonut/GroupedDataLoader.cs +++ b/src/GreenDonut/src/GreenDonut/GroupedDataLoader.cs @@ -25,7 +25,7 @@ public abstract class GroupedDataLoader /// protected GroupedDataLoader( IBatchScheduler batchScheduler, - DataLoaderOptions? options = null) + DataLoaderOptions options) : base(batchScheduler, options) { } @@ -92,7 +92,7 @@ public abstract class StatefulGroupedDataLoader /// protected StatefulGroupedDataLoader( IBatchScheduler batchScheduler, - DataLoaderOptions? options = null) + DataLoaderOptions options) : base(batchScheduler, options) { } diff --git a/src/GreenDonut/test/GreenDonut.Tests/DataLoader.cs b/src/GreenDonut/test/GreenDonut.Tests/DataLoader.cs index d0001f962b0..168968ffe62 100644 --- a/src/GreenDonut/test/GreenDonut.Tests/DataLoader.cs +++ b/src/GreenDonut/test/GreenDonut.Tests/DataLoader.cs @@ -3,7 +3,7 @@ namespace GreenDonut; public class DataLoader( FetchDataDelegate fetch, IBatchScheduler batchScheduler, - DataLoaderOptions? options = null) + DataLoaderOptions options) : DataLoaderBase(batchScheduler, options) where TKey : notnull { diff --git a/src/GreenDonut/test/GreenDonut.Tests/DataLoaderExtensionsTests.cs b/src/GreenDonut/test/GreenDonut.Tests/DataLoaderExtensionsTests.cs index 1f05265710a..7311146d52d 100644 --- a/src/GreenDonut/test/GreenDonut.Tests/DataLoaderExtensionsTests.cs +++ b/src/GreenDonut/test/GreenDonut.Tests/DataLoaderExtensionsTests.cs @@ -1,5 +1,7 @@ // ReSharper disable InconsistentNaming +using System.Reflection.Metadata; + namespace GreenDonut; public class DataLoaderExtensionsTests @@ -24,7 +26,7 @@ public void SetCacheEntryKeyNull() // arrange var fetch = TestHelpers.CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(fetch, batchScheduler); + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var value = "Bar"; // act @@ -40,7 +42,7 @@ public void SetCacheEntryNoException() // arrange var fetch = TestHelpers.CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(fetch, batchScheduler); + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var key = "Foo"; // act @@ -149,7 +151,7 @@ public void IDataLoaderSetCacheEntryNoException() // arrange var fetch = TestHelpers.CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - IDataLoader loader = new DataLoader(fetch, batchScheduler); + IDataLoader loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); object key = "Foo"; // act diff --git a/src/GreenDonut/test/GreenDonut.Tests/DataLoaderStateTests.cs b/src/GreenDonut/test/GreenDonut.Tests/DataLoaderStateTests.cs index 71bd0da33c1..3a7497f641a 100644 --- a/src/GreenDonut/test/GreenDonut.Tests/DataLoaderStateTests.cs +++ b/src/GreenDonut/test/GreenDonut.Tests/DataLoaderStateTests.cs @@ -6,7 +6,7 @@ public static class DataLoaderStateTests public static async Task SetStateInferredKey() { // arrange - var loader = new DummyDataLoader(typeof(string).FullName!); + var loader = new DummyDataLoader(typeof(string).FullName!, new DataLoaderOptions()); // act await loader.SetState("abc").LoadAsync("def"); @@ -19,7 +19,7 @@ public static async Task SetStateInferredKey() public static async Task SetStateExplicitKey() { // arrange - var loader = new DummyDataLoader("abc"); + var loader = new DummyDataLoader("abc", new DataLoaderOptions()); // act await loader.SetState("abc", "def").LoadAsync("ghi"); @@ -32,7 +32,7 @@ public static async Task SetStateExplicitKey() public static async Task TrySetStateInferredKey() { // arrange - var loader = new DummyDataLoader(typeof(string).FullName!); + var loader = new DummyDataLoader(typeof(string).FullName!, new DataLoaderOptions()); // act await loader.SetState("abc").TrySetState("xyz").LoadAsync("def"); @@ -45,7 +45,7 @@ public static async Task TrySetStateInferredKey() public static async Task TrySetStateExplicitKey() { // arrange - var loader = new DummyDataLoader("abc"); + var loader = new DummyDataLoader("abc", new DataLoaderOptions()); // act await loader.SetState("abc", "def").TrySetState("abc", "xyz").LoadAsync("def"); @@ -58,7 +58,7 @@ public static async Task TrySetStateExplicitKey() public static async Task AddStateEnumerableInferredKey() { // arrange - var loader = new DummyDataLoader(typeof(string).FullName!); + var loader = new DummyDataLoader(typeof(string).FullName!, new DataLoaderOptions()); // act await loader.AddStateEnumerable("abc").AddStateEnumerable("xyz").LoadAsync("def"); @@ -74,7 +74,7 @@ public static async Task AddStateEnumerableInferredKey() public static async Task AddStateEnumerableExplicitKey() { // arrange - var loader = new DummyDataLoader("abc"); + var loader = new DummyDataLoader("abc", new DataLoaderOptions()); // act await loader.AddStateEnumerable("abc", "def").AddStateEnumerable("abc", "xyz").LoadAsync("def"); @@ -86,7 +86,7 @@ public static async Task AddStateEnumerableExplicitKey() item => Assert.Equal("xyz", item)); } - public class DummyDataLoader(string expectedKey, DataLoaderOptions? options = null) + public class DummyDataLoader(string expectedKey, DataLoaderOptions options) : DataLoaderBase(AutoBatchScheduler.Default, options) { public object? State { get; set; } diff --git a/src/GreenDonut/test/GreenDonut.Tests/DependencyInjection/DataLoaderServiceCollectionExtensionsTests.cs b/src/GreenDonut/test/GreenDonut.Tests/DependencyInjection/DataLoaderServiceCollectionExtensionsTests.cs index 61e6f918528..87422d458b5 100644 --- a/src/GreenDonut/test/GreenDonut.Tests/DependencyInjection/DataLoaderServiceCollectionExtensionsTests.cs +++ b/src/GreenDonut/test/GreenDonut.Tests/DependencyInjection/DataLoaderServiceCollectionExtensionsTests.cs @@ -17,7 +17,10 @@ public void ImplFactoryIsCalledWhenServiceIsResolved() .AddDataLoader(sp => { factoryCalled = true; - return new DataLoader(fetch, sp.GetRequiredService()); + return new DataLoader( + fetch, + sp.GetRequiredService(), + sp.GetRequiredService()); }); var scope = services.BuildServiceProvider().CreateScope(); @@ -40,7 +43,10 @@ public void InterfaceImplFactoryIsCalledWhenServiceIsResolved() .AddDataLoader, DataLoader>(sp => { factoryCalled = true; - return new DataLoader(fetch, sp.GetRequiredService()); + return new DataLoader( + fetch, + sp.GetRequiredService(), + sp.GetRequiredService()); }); var scope = services.BuildServiceProvider().CreateScope(); diff --git a/src/HotChocolate/Core/test/Execution.Tests/Integration/DataLoader/UseDataLoaderTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Integration/DataLoader/UseDataLoaderTests.cs index d0d704b130d..c3ef0a237fc 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Integration/DataLoader/UseDataLoaderTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Integration/DataLoader/UseDataLoaderTests.cs @@ -256,7 +256,7 @@ public class TestGroupedLoader : GroupedDataLoader { public TestGroupedLoader( IBatchScheduler batchScheduler, - DataLoaderOptions? options = null) + DataLoaderOptions options) : base(batchScheduler, options) { } diff --git a/website/src/docs/hotchocolate/v15/migrating/migrate-from-14-to-15.md b/website/src/docs/hotchocolate/v15/migrating/migrate-from-14-to-15.md index 956071560fc..42c13d641f1 100644 --- a/website/src/docs/hotchocolate/v15/migrating/migrate-from-14-to-15.md +++ b/website/src/docs/hotchocolate/v15/migrating/migrate-from-14-to-15.md @@ -50,8 +50,74 @@ Please ensure that your clients are sending date/time strings in the correct for - `DateOnly` is now bound to `LocalDateType` instead of `DateType`. - `TimeOnly` is now bound to `LocalTimeType` instead of `TimeSpanType`. +## DataLoaderOptions are now required + +Starting with Hot Chocolate 15, the `DataLoaderOptions` must be passed down to the DataLoaderBase constructor. + +```csharp +public class ProductByIdDataLoader : BatchDataLoader +{ + private readonly IServiceProvider _services; + + public ProductDataLoader1( + IBatchScheduler batchScheduler, + DataLoaderOptions options) // the options are now required ... + : base(batchScheduler, options) + { + } +} +``` + +## DataLoader Dependency Injection + +DataLoader must not be manually registered with the dependency injection and must use the extension methods provided by GreenDonut. + +```csharp +services.AddDataLoader(); +services.AddDataLoader(); +services.AddDataLoader(sp => ....); +``` + +We recommend to use the source-generated DataLoaders and let the source generator write the registration code for you. + +> If you register DataLoader manually they will be stuck in the auto-dispatch mode, which basically means that they will no longer batch. + +DataLoader are available as scoped services and can be injected like any other scoped service. + +```csharp +public class ProductService(IProductByIdDataLoader productByIdData) +{ + public async Task GetProductById(int id) + { + return await productByIdDataLoader.LoadAsync(id); + } +} +``` + # Deprecations +## GroupDataLoader + +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. + +Use the following patter to replace the `GroupDataLoader`: + +```csharp +internal static class ProductDataLoader +{ + [DataLoader] + public static async Task> GetProductsByBrandIdAsync( + IReadOnlyList brandIds, + CatalogContext context, + CancellationToken cancellationToken) + => await context.Products + .Where(t => brandIds.Contains(t.BrandId)) + .GroupBy(t => t.BrandId) + .Select(t => new { t.Key, Items = t.OrderBy(p => p.Name).ToArray() }) + .ToDictionaryAsync(t => t.Key, t => t.Items, cancellationToken); +} +``` + ## AdHoc DataLoader The ad-hoc DataLoader methods on IResolverContext have been deprecated. From ae922c8d231ea543d2664d36e9dd44279c9b6e2b Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 19 Feb 2025 22:54:32 +0100 Subject: [PATCH 03/64] Fixed DataLoader Tests --- .../test/GreenDonut.Tests/DataLoaderTests.cs | 82 ++++++++++--------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/src/GreenDonut/test/GreenDonut.Tests/DataLoaderTests.cs b/src/GreenDonut/test/GreenDonut.Tests/DataLoaderTests.cs index 0903b25a300..6db353d5e7e 100644 --- a/src/GreenDonut/test/GreenDonut.Tests/DataLoaderTests.cs +++ b/src/GreenDonut/test/GreenDonut.Tests/DataLoaderTests.cs @@ -15,7 +15,10 @@ public void ClearCacheNoException() var fetch = CreateFetch(); var services = new ServiceCollection() .AddScoped() - .AddDataLoader(sp => new DataLoader(fetch, sp.GetRequiredService())); + .AddDataLoader(sp => new DataLoader( + fetch, + sp.GetRequiredService(), + sp.GetRequiredService())); var scope = services.BuildServiceProvider().CreateScope(); var dataLoader = scope.ServiceProvider.GetRequiredService>(); @@ -52,7 +55,7 @@ public async Task LoadSingleKeyNull() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(fetch, batchScheduler); + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); // act Task Verify() => loader.LoadAsync(default(string)!, CancellationToken.None); @@ -67,7 +70,7 @@ public async Task LoadSingleResult() // arrange var fetch = CreateFetch("Bar"); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(fetch, batchScheduler); + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var key = "Foo"; // act @@ -85,7 +88,7 @@ public async Task LoadSingleResultTwice() // arrange var fetch = CreateFetch("Bar"); var batchScheduler = new DelayDispatcher(); - var loader = new DataLoader(fetch, batchScheduler); + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var key = "Foo"; // first load. @@ -106,7 +109,8 @@ public async Task LoadSingleResultNoCache() var batchScheduler = new ManualBatchScheduler(); var loader = new DataLoader( fetch, - batchScheduler); + batchScheduler, + new DataLoaderOptions()); var key = "Foo"; // act @@ -124,7 +128,7 @@ public async Task LoadSingleErrorResult() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(fetch, batchScheduler); + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var key = "Foo"; // act @@ -144,7 +148,7 @@ public async Task LoadParamsKeysNull() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(fetch, batchScheduler); + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); // act Task> Verify() => loader.LoadAsync(default(string[])!); @@ -159,7 +163,7 @@ public async Task LoadParamsZeroKeys() // arrange var fetch = TestHelpers.CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(fetch, batchScheduler); + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var keys = Array.Empty(); // act @@ -178,7 +182,7 @@ public async Task LoadParamsResult() var fetch = TestHelpers .CreateFetch("Bar"); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(fetch, batchScheduler); + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var keys = new[] { "Foo", }; // act @@ -196,7 +200,7 @@ public async Task LoadCollectionKeysNull() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(fetch, batchScheduler); + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); // act Task> Verify() @@ -212,7 +216,7 @@ public async Task LoadCollectionZeroKeys() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(fetch, batchScheduler); + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var keys = new List(); // act @@ -230,7 +234,7 @@ public async Task LoadCollectionResult() // arrange var fetch = CreateFetch("Bar"); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(fetch, batchScheduler); + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var keys = new List { "Foo", }; // act @@ -249,7 +253,8 @@ public async Task LoadCollectionResultTwice() var batchScheduler = new DelayDispatcher(); var loader = new DataLoader( fetch, - batchScheduler); + batchScheduler, + new DataLoaderOptions()); var keys = new List { "Foo", }; (await loader.LoadAsync(keys, CancellationToken.None)).MatchSnapshot(); @@ -269,7 +274,8 @@ public async Task LoadCollectionResultNoCache() var batchScheduler = new ManualBatchScheduler(); var loader = new DataLoader( fetch, - batchScheduler); + batchScheduler, + new DataLoaderOptions()); var keys = new List { "Foo", }; // act @@ -311,7 +317,7 @@ ValueTask Fetch( } var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(Fetch, batchScheduler); + var loader = new DataLoader(Fetch, batchScheduler, new DataLoaderOptions()); var requestKeys = new[] { "Foo", "Bar", "Baz", "Qux", }; // act @@ -356,7 +362,7 @@ ValueTask Fetch( } var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(Fetch, batchScheduler); + var loader = new DataLoader(Fetch, batchScheduler, new DataLoaderOptions()); var requestKeys = new[] { "Foo", "Bar", "Baz", "Qux", }; // act @@ -379,7 +385,7 @@ public async Task LoadBatchingError() // arrange var expectedException = new Exception("Foo"); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(Fetch, batchScheduler); + var loader = new DataLoader(Fetch, batchScheduler, new DataLoaderOptions()); var requestKeys = new[] { "Foo", "Bar", "Baz", "Qux", }; ValueTask Fetch( @@ -503,7 +509,7 @@ public void RemoveCacheEntryKeyNull() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(fetch, batchScheduler); + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); loader.SetCacheEntry("Foo", Task.FromResult("Bar")); @@ -520,7 +526,7 @@ public void RemoveCacheEntryNoException() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(fetch, batchScheduler); + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var key = "Foo"; // act @@ -556,7 +562,7 @@ public void SetCacheEntryKeyNull() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(fetch, batchScheduler); + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var value = Task.FromResult("Foo"); // act @@ -572,8 +578,8 @@ public void SetCacheEntryValueNull() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - var loader = new DataLoader(fetch, batchScheduler); - var key = "Foo"; + var loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); + const string key = "Foo"; // act void Verify() => loader.SetCacheEntry(key, default!); @@ -628,7 +634,7 @@ public async Task IDataLoaderLoadSingleKeyNull() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - IDataLoader loader = new DataLoader(fetch, batchScheduler); + IDataLoader loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); // act Task Verify() => loader.LoadAsync(default(object)!); @@ -643,7 +649,7 @@ public async Task IDataLoaderLoadSingleResult() // arrange var fetch = CreateFetch("Bar"); var batchScheduler = new ManualBatchScheduler(); - IDataLoader loader = new DataLoader(fetch, batchScheduler); + IDataLoader loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); object key = "Foo"; // act @@ -661,7 +667,7 @@ public async Task IDataLoaderLoadSingleErrorResult() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - IDataLoader loader = new DataLoader(fetch, batchScheduler); + IDataLoader loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); object key = "Foo"; // act @@ -683,7 +689,7 @@ public async Task IDataLoaderLoadParamsKeysNull() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - IDataLoader loader = new DataLoader(fetch, batchScheduler); + IDataLoader loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); // act Task> Verify() => loader.LoadAsync(default(object[])!); @@ -698,7 +704,7 @@ public async Task IDataLoaderLoadParamsZeroKeys() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - IDataLoader loader = new DataLoader(fetch, batchScheduler); + IDataLoader loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var keys = Array.Empty(); // act @@ -714,7 +720,7 @@ public async Task IDataLoaderLoadParamsResult() // arrange var fetch = CreateFetch("Bar"); var batchScheduler = new ManualBatchScheduler(); - IDataLoader loader = new DataLoader(fetch, batchScheduler); + IDataLoader loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var keys = new object[] { "Foo", }; // act @@ -732,7 +738,7 @@ public async Task IDataLoaderLoadCollectionKeysNull() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - IDataLoader loader = new DataLoader(fetch, batchScheduler); + IDataLoader loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); // act Task> Verify() @@ -748,7 +754,7 @@ public async Task IDataLoaderLoadCollectionZeroKeys() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - IDataLoader loader = new DataLoader(fetch, batchScheduler); + IDataLoader loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var keys = new List(); // act @@ -764,7 +770,7 @@ public async Task IDataLoaderLoadCollectionResult() // arrange var fetch = CreateFetch("Bar"); var batchScheduler = new ManualBatchScheduler(); - IDataLoader loader = new DataLoader(fetch, batchScheduler); + IDataLoader loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var keys = new List { "Foo", }; // act @@ -782,7 +788,7 @@ public void IDataLoaderRemoveCacheEntryKeyNull() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - IDataLoader loader = new DataLoader(fetch, batchScheduler); + IDataLoader loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); loader.SetCacheEntry("Foo", Task.FromResult((object?)"Bar")); @@ -799,7 +805,7 @@ public void IDataLoaderRemoveCacheEntryNoException() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - IDataLoader loader = new DataLoader(fetch, batchScheduler); + IDataLoader loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); object key = "Foo"; // act @@ -835,7 +841,7 @@ public void IDataLoaderSetCacheEntryKeyNull() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - IDataLoader loader = new DataLoader(fetch, batchScheduler); + IDataLoader loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); var value = Task.FromResult("Foo"); // act @@ -851,7 +857,7 @@ public void IDataLoaderSetCacheEntryValueNull() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - IDataLoader loader = new DataLoader(fetch, batchScheduler); + IDataLoader loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); object key = "Foo"; // act @@ -867,7 +873,7 @@ public void IDataLoaderSetCacheEntryNoException() // arrange var fetch = CreateFetch(); var batchScheduler = new ManualBatchScheduler(); - IDataLoader loader = new DataLoader(fetch, batchScheduler); + IDataLoader loader = new DataLoader(fetch, batchScheduler, new DataLoaderOptions()); object key = "Foo"; var value = Task.FromResult("Bar"); @@ -942,7 +948,7 @@ public async Task Add_Additional_Lookup_With_CacheObserver() private class TestDataLoader1( IBatchScheduler batchScheduler, - DataLoaderOptions? options = null) + DataLoaderOptions options) : DataLoaderBase(batchScheduler, options) { protected internal override ValueTask FetchAsync( @@ -965,7 +971,7 @@ private class TestDataLoader2 : DataLoaderBase { public TestDataLoader2( IBatchScheduler batchScheduler, - DataLoaderOptions? options = null) + DataLoaderOptions options) : base(batchScheduler, options) { PromiseCacheObserver From f24968c58100f8573c049fedf0ad04a3d0985444 Mon Sep 17 00:00:00 2001 From: Tobias Tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Thu, 20 Feb 2025 12:03:43 +0100 Subject: [PATCH 04/64] Ensure TypeModuleChangeMonitor is properly disposed (#8041) --- .../src/Execution/RequestExecutorResolver.cs | 61 +++++++------------ .../Configuration/TypeModuleTests.cs | 53 ++++++++++++++++ 2 files changed, 76 insertions(+), 38 deletions(-) diff --git a/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs index a688733b9fd..b766e443e60 100644 --- a/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs +++ b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs @@ -96,8 +96,20 @@ await _optionsMonitor.GetAsync(schemaName, cancellationToken) setup.SchemaBuilder ?? new SchemaBuilder(), _applicationServices); + + var typeModuleChangeMonitor = new TypeModuleChangeMonitor(this, context.SchemaName); + + // if there are any type modules we will register them with the + // type module change monitor. + // The module will track if type modules signal changes to the schema and + // start a schema eviction. + foreach (var typeModule in setup.TypeModules) + { + typeModuleChangeMonitor.Register(typeModule); + } + var schemaServices = - await CreateSchemaServicesAsync(context, setup, cancellationToken) + await CreateSchemaServicesAsync(context, setup, typeModuleChangeMonitor, cancellationToken) .ConfigureAwait(false); registeredExecutor = new RegisteredExecutor( @@ -105,7 +117,7 @@ await CreateSchemaServicesAsync(context, setup, cancellationToken) schemaServices, schemaServices.GetRequiredService(), setup, - schemaServices.GetRequiredService()); + typeModuleChangeMonitor); var executor = registeredExecutor.Executor; @@ -131,24 +143,26 @@ public void EvictRequestExecutor(string? schemaName = default) { schemaName ??= Schema.DefaultName; - if (_executors.TryRemove(schemaName, out var re)) + if (_executors.TryRemove(schemaName, out var executor)) { - re.DiagnosticEvents.ExecutorEvicted(schemaName, re.Executor); + executor.DiagnosticEvents.ExecutorEvicted(schemaName, executor.Executor); try { + executor.TypeModuleChangeMonitor.Dispose(); + RequestExecutorEvicted?.Invoke( this, - new RequestExecutorEvictedEventArgs(schemaName, re.Executor)); + new RequestExecutorEvictedEventArgs(schemaName, executor.Executor)); _events.RaiseEvent( new RequestExecutorEvent( RequestExecutorEventType.Evicted, schemaName, - re.Executor)); + executor.Executor)); } finally { - BeginRunEvictionEvents(re); + BeginRunEvictionEvents(executor); } } } @@ -157,26 +171,7 @@ private void EvictAllRequestExecutors() { foreach (var key in _executors.Keys) { - if (_executors.TryRemove(key, out var re)) - { - re.DiagnosticEvents.ExecutorEvicted(key, re.Executor); - - try - { - RequestExecutorEvicted?.Invoke( - this, - new RequestExecutorEvictedEventArgs(key, re.Executor)); - _events.RaiseEvent( - new RequestExecutorEvent( - RequestExecutorEventType.Evicted, - key, - re.Executor)); - } - finally - { - BeginRunEvictionEvents(re); - } - } + EvictRequestExecutor(key); } } @@ -201,6 +196,7 @@ private static async Task RunEvictionEvents(RegisteredExecutor registeredExecuto private async Task CreateSchemaServicesAsync( ConfigurationContext context, RequestExecutorSetup setup, + TypeModuleChangeMonitor typeModuleChangeMonitor, CancellationToken cancellationToken) { ulong version; @@ -211,22 +207,12 @@ private async Task CreateSchemaServicesAsync( } var serviceCollection = new ServiceCollection(); - var typeModuleChangeMonitor = new TypeModuleChangeMonitor(this, context.SchemaName); var lazy = new SchemaBuilder.LazySchema(); var executorOptions = await OnConfigureRequestExecutorOptionsAsync(context, setup, cancellationToken) .ConfigureAwait(false); - // if there are any type modules we will register them with the - // type module change monitor. - // The module will track if type modules signal changes to the schema and - // start a schema eviction. - foreach (var typeModule in setup.TypeModules) - { - typeModuleChangeMonitor.Register(typeModule); - } - // we allow newer type modules to apply configurations. await typeModuleChangeMonitor.ConfigureAsync(context, cancellationToken) .ConfigureAwait(false); @@ -241,7 +227,6 @@ await typeModuleChangeMonitor.ConfigureAsync(context, cancellationToken) setup.DefaultPipelineFactory, setup.Pipeline)); - serviceCollection.AddSingleton(typeModuleChangeMonitor); serviceCollection.AddSingleton(executorOptions); serviceCollection.AddSingleton( static s => s.GetRequiredService()); diff --git a/src/HotChocolate/Core/test/Execution.Tests/Configuration/TypeModuleTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Configuration/TypeModuleTests.cs index fad4e67be77..9f647b90150 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Configuration/TypeModuleTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Configuration/TypeModuleTests.cs @@ -3,6 +3,7 @@ using HotChocolate.Types.Descriptors; using HotChocolate.Types.Descriptors.Definitions; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; namespace HotChocolate.Execution.Configuration; @@ -64,6 +65,58 @@ public async Task Use_Type_Module_From_Factory() .MatchSnapshotAsync(); } + [Fact] + public async Task Ensure_Warmups_Are_Triggered_An_Appropriate_Number_Of_Times() + { + // arrange + var typeModule = new TriggerableTypeModule(); + var warmups = 0; + var resetEvent = new AutoResetEvent(false); + + var services = new ServiceCollection(); + services + .AddGraphQL() + .AddTypeModule(_ => typeModule) + .InitializeOnStartup(keepWarm: true, warmup: (_, _) => + { + warmups++; + resetEvent.Set(); + return Task.CompletedTask; + }) + .AddQueryType(d => d.Field("foo").Resolve("")); + var provider = services.BuildServiceProvider(); + var warmupService = provider.GetRequiredService(); + + using var cts = new CancellationTokenSource(); + _ = Task.Run(async () => + { + await warmupService.StartAsync(CancellationToken.None); + }, cts.Token); + + var resolver = provider.GetRequiredService(); + + await resolver.GetRequestExecutorAsync(null, cts.Token); + + // act + // assert + typeModule.TriggerChange(); + resetEvent.WaitOne(); + + // 2 since we have the initial warmup at "startup" and the one triggered above. + Assert.Equal(2, warmups); + + resetEvent.Reset(); + typeModule.TriggerChange(); + resetEvent.WaitOne(); + + Assert.Equal(3, warmups); + } + + private sealed class TriggerableTypeModule : TypeModule + { + public void TriggerChange() => OnTypesChanged(); + } + public class DummyTypeModule : ITypeModule { #pragma warning disable CS0067 From 06365e450a925d3cdd6dd0f494e3b7bfe870b770 Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 21 Feb 2025 00:31:06 +0200 Subject: [PATCH 05/64] Used the correct xUnit packages in Cookie Crumble libraries (#8054) --- .../CookieCrumble.Xunit.csproj | 4 +++- .../CookieCrumble.Xunit3.csproj | 3 ++- src/Directory.Packages.props | 15 ++++++++++----- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/CookieCrumble/src/CookieCrumble.Xunit/CookieCrumble.Xunit.csproj b/src/CookieCrumble/src/CookieCrumble.Xunit/CookieCrumble.Xunit.csproj index ecc54d94a23..b9e5062e13c 100644 --- a/src/CookieCrumble/src/CookieCrumble.Xunit/CookieCrumble.Xunit.csproj +++ b/src/CookieCrumble/src/CookieCrumble.Xunit/CookieCrumble.Xunit.csproj @@ -7,7 +7,9 @@ - + + + diff --git a/src/CookieCrumble/src/CookieCrumble.Xunit3/CookieCrumble.Xunit3.csproj b/src/CookieCrumble/src/CookieCrumble.Xunit3/CookieCrumble.Xunit3.csproj index 4f920d2ee7f..caee8700f21 100644 --- a/src/CookieCrumble/src/CookieCrumble.Xunit3/CookieCrumble.Xunit3.csproj +++ b/src/CookieCrumble/src/CookieCrumble.Xunit3/CookieCrumble.Xunit3.csproj @@ -7,7 +7,8 @@ - + + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index a3d046b0431..144732b7b7f 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -59,11 +59,16 @@ - - - - - + + + + + + + + + + From 0475635025564256e909cc75f077a6a10ccc6719 Mon Sep 17 00:00:00 2001 From: Tobias Tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:42:21 +0100 Subject: [PATCH 06/64] (Hopefully) Fix flakey test (#8056) --- .../Configuration/TypeModuleTests.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/HotChocolate/Core/test/Execution.Tests/Configuration/TypeModuleTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Configuration/TypeModuleTests.cs index 9f647b90150..c96aeb97c6e 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Configuration/TypeModuleTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Configuration/TypeModuleTests.cs @@ -71,7 +71,7 @@ public async Task Ensure_Warmups_Are_Triggered_An_Appropriate_Number_Of_Times() // arrange var typeModule = new TriggerableTypeModule(); var warmups = 0; - var resetEvent = new AutoResetEvent(false); + var warmupResetEvent = new AutoResetEvent(false); var services = new ServiceCollection(); services @@ -80,7 +80,7 @@ public async Task Ensure_Warmups_Are_Triggered_An_Appropriate_Number_Of_Times() .InitializeOnStartup(keepWarm: true, warmup: (_, _) => { warmups++; - resetEvent.Set(); + warmupResetEvent.Set(); return Task.CompletedTask; }) .AddQueryType(d => d.Field("foo").Resolve("")); @@ -99,15 +99,19 @@ public async Task Ensure_Warmups_Are_Triggered_An_Appropriate_Number_Of_Times() // act // assert + warmupResetEvent.WaitOne(); + + Assert.Equal(1, warmups); + warmupResetEvent.Reset(); + typeModule.TriggerChange(); - resetEvent.WaitOne(); + warmupResetEvent.WaitOne(); - // 2 since we have the initial warmup at "startup" and the one triggered above. Assert.Equal(2, warmups); + warmupResetEvent.Reset(); - resetEvent.Reset(); typeModule.TriggerChange(); - resetEvent.WaitOne(); + warmupResetEvent.WaitOne(); Assert.Equal(3, warmups); } From d1c0a6f76a89e219c15b0d65fd12c1e1b1d3ec2d Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 21 Feb 2025 17:03:11 +0200 Subject: [PATCH 07/64] Added missing condition to TypeRegistry#Register (#8058) --- .../src/Types/Configuration/TypeRegistry.cs | 3 ++- .../Configuration/TypeDiscoveryTests.cs | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeRegistry.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeRegistry.cs index da331219c81..18a49690f39 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeRegistry.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeRegistry.cs @@ -162,7 +162,8 @@ public void Register(RegisteredType registeredType) _nameRefs.Add(typeDef.Name, registeredType.References[0]); } else if (registeredType.Kind == TypeKind.Scalar && - registeredType.Type is ScalarType scalar) + registeredType.Type is ScalarType scalar && + !_nameRefs.ContainsKey(scalar.Name)) { _nameRefs.Add(scalar.Name, registeredType.References[0]); } diff --git a/src/HotChocolate/Core/test/Types.Tests/Configuration/TypeDiscoveryTests.cs b/src/HotChocolate/Core/test/Types.Tests/Configuration/TypeDiscoveryTests.cs index c919d211bc2..f68975bb8ab 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Configuration/TypeDiscoveryTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Configuration/TypeDiscoveryTests.cs @@ -54,6 +54,19 @@ public void InferInputTypeWithComputedProperty() .MatchSnapshot(); } + [Fact] + public void Custom_LocalDate_Should_Throw_SchemaException_When_Not_Bound() + { + static void Act() => + SchemaBuilder.New() + .AddQueryType() + .Create(); + + Assert.Equal( + "The name `LocalDate` was already registered by another type.", + Assert.Throws(Act).Errors[0].Message); + } + public class QueryWithDateTime { public DateTimeOffset DateTimeOffset(DateTimeOffset time) => time; @@ -153,4 +166,14 @@ public class QueryTypeWithComputedProperty { public int Foo(InputTypeWithReadOnlyProperties arg) => arg.Property1; } + + public class QueryTypeWithCustomLocalDate + { + public LocalDate Foo() => new(); + } + + public class LocalDate + { + public DateOnly Date { get; set; } = new(); + } } From 2ad60cbaaaa25c7593d32579b74cbe2b06d92775 Mon Sep 17 00:00:00 2001 From: Tobias Tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:36:10 +0100 Subject: [PATCH 08/64] Scope OperationCache to RequestExecutor (#8055) Co-authored-by: Michael Staib --- .../Caching/PreparedOperationCacheOptions.cs | 6 ++++ .../InternalServiceCollectionExtensions.cs | 4 +-- ...uestExecutorServiceCollectionExtensions.cs | 17 ++-------- .../Pipeline/OperationCacheMiddleware.cs | 7 ++-- .../src/Execution/RequestExecutorResolver.cs | 5 +++ .../PreparedOperationCacheTests.cs | 30 ++++++++++++++++ .../RequestExecutorResolverTests.cs | 34 +++++++++++++++++++ .../Execution.Tests/WarmupRequestTests.cs | 6 ++-- 8 files changed, 87 insertions(+), 22 deletions(-) create mode 100644 src/HotChocolate/Core/src/Execution/Caching/PreparedOperationCacheOptions.cs create mode 100644 src/HotChocolate/Core/test/Execution.Tests/PreparedOperationCacheTests.cs create mode 100644 src/HotChocolate/Core/test/Execution.Tests/RequestExecutorResolverTests.cs diff --git a/src/HotChocolate/Core/src/Execution/Caching/PreparedOperationCacheOptions.cs b/src/HotChocolate/Core/src/Execution/Caching/PreparedOperationCacheOptions.cs new file mode 100644 index 00000000000..88beb6e8e80 --- /dev/null +++ b/src/HotChocolate/Core/src/Execution/Caching/PreparedOperationCacheOptions.cs @@ -0,0 +1,6 @@ +namespace HotChocolate.Execution.Caching; + +internal sealed class PreparedOperationCacheOptions +{ + public int Capacity { get; set; } = 100; +} diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs index 53a4bac1b1f..f7d463d43dd 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs @@ -166,8 +166,8 @@ internal static IServiceCollection TryAddDefaultCaches( { services.TryAddSingleton( _ => new DefaultDocumentCache()); - services.TryAddSingleton( - _ => new DefaultPreparedOperationCache()); + services.TryAddSingleton( + _ => new PreparedOperationCacheOptions { Capacity = 100 }); return services; } diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs index 2298d90bd82..d915dedb7d4 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs @@ -163,15 +163,6 @@ private static IRequestExecutorBuilder CreateBuilder( builder.Services.AddValidation(schemaName); - builder.Configure( - (sp, e) => - { - e.OnRequestExecutorEvictedHooks.Add( - // when ever we evict this schema we will clear the caches. - new OnRequestExecutorEvictedAction( - _ => sp.GetRequiredService().Clear())); - }); - builder.TryAddNoOpTransactionScopeHandler(); builder.TryAddTypeInterceptor(); builder.TryAddTypeInterceptor(); @@ -193,11 +184,9 @@ public static IServiceCollection AddOperationCache( this IServiceCollection services, int capacity = 100) { - services.RemoveAll(); - - services.AddSingleton( - _ => new DefaultPreparedOperationCache(capacity)); - + services.RemoveAll(); + services.AddSingleton( + _ => new PreparedOperationCacheOptions{ Capacity = capacity }); return services; } diff --git a/src/HotChocolate/Core/src/Execution/Pipeline/OperationCacheMiddleware.cs b/src/HotChocolate/Core/src/Execution/Pipeline/OperationCacheMiddleware.cs index c18e6996397..44b4a2a9ded 100644 --- a/src/HotChocolate/Core/src/Execution/Pipeline/OperationCacheMiddleware.cs +++ b/src/HotChocolate/Core/src/Execution/Pipeline/OperationCacheMiddleware.cs @@ -10,9 +10,10 @@ internal sealed class OperationCacheMiddleware private readonly IExecutionDiagnosticEvents _diagnosticEvents; private readonly IPreparedOperationCache _operationCache; - private OperationCacheMiddleware(RequestDelegate next, + private OperationCacheMiddleware( + RequestDelegate next, [SchemaService] IExecutionDiagnosticEvents diagnosticEvents, - IPreparedOperationCache operationCache) + [SchemaService] IPreparedOperationCache operationCache) { _next = next ?? throw new ArgumentNullException(nameof(next)); @@ -64,7 +65,7 @@ public static RequestCoreMiddleware Create() => (core, next) => { var diagnosticEvents = core.SchemaServices.GetRequiredService(); - var cache = core.Services.GetRequiredService(); + var cache = core.SchemaServices.GetRequiredService(); var middleware = new OperationCacheMiddleware(next, diagnosticEvents, cache); return context => middleware.InvokeAsync(context); }; diff --git a/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs index b766e443e60..5b1bb06d433 100644 --- a/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs +++ b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs @@ -3,6 +3,7 @@ using System.Reflection.Metadata; using HotChocolate.Configuration; using HotChocolate.Execution; +using HotChocolate.Execution.Caching; using HotChocolate.Execution.Configuration; using HotChocolate.Execution.Errors; using HotChocolate.Execution.Instrumentation; @@ -237,6 +238,10 @@ await typeModuleChangeMonitor.ConfigureAsync(context, cancellationToken) serviceCollection.AddSingleton( static s => s.GetRequiredService()); + serviceCollection.AddSingleton( + _ => new DefaultPreparedOperationCache( + _applicationServices.GetRequiredService().Capacity)); + serviceCollection.AddSingleton(); serviceCollection.TryAddDiagnosticEvents(); diff --git a/src/HotChocolate/Core/test/Execution.Tests/PreparedOperationCacheTests.cs b/src/HotChocolate/Core/test/Execution.Tests/PreparedOperationCacheTests.cs new file mode 100644 index 00000000000..798d1c1d0d6 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/PreparedOperationCacheTests.cs @@ -0,0 +1,30 @@ +using HotChocolate.Execution.Caching; +using HotChocolate.Types; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Execution; + +public class PreparedOperationCacheTests +{ + [Fact] + public async Task Operation_Cache_Should_Have_Configured_Capacity() + { + // arrange + var operationCacheCapacity = 517; + var services = new ServiceCollection(); + services.AddOperationCache(operationCacheCapacity); + services + .AddGraphQL() + .AddQueryType(d => d.Field("foo").Resolve("")); + var provider = services.BuildServiceProvider(); + var resolver = provider.GetRequiredService(); + + // act + var executor = await resolver.GetRequestExecutorAsync(); + var operationCache = executor.Services.GetCombinedServices() + .GetRequiredService(); + + // assert + Assert.Equal(operationCache.Capacity, operationCacheCapacity); + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorResolverTests.cs b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorResolverTests.cs new file mode 100644 index 00000000000..683574d45be --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorResolverTests.cs @@ -0,0 +1,34 @@ +using HotChocolate.Execution.Caching; +using HotChocolate.Types; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Execution; + +public class RequestExecutorResolverTests +{ + [Fact] + public async Task Operation_Cache_Should_Be_Scoped_To_Executor() + { + // arrange + var services = new ServiceCollection(); + services + .AddGraphQL() + .AddQueryType(d => d.Field("foo").Resolve("")); + var provider = services.BuildServiceProvider(); + var resolver = provider.GetRequiredService(); + + // act + var firstExecutor = await resolver.GetRequestExecutorAsync(); + var firstOperationCache = firstExecutor.Services.GetCombinedServices() + .GetRequiredService(); + + resolver.EvictRequestExecutor(); + + var secondExecutor = await resolver.GetRequestExecutorAsync(); + var secondOperationCache = secondExecutor.Services.GetCombinedServices() + .GetRequiredService(); + + // assert + Assert.NotEqual(secondOperationCache, firstOperationCache); + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/WarmupRequestTests.cs b/src/HotChocolate/Core/test/Execution.Tests/WarmupRequestTests.cs index fe04071361d..f5e037f89c8 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/WarmupRequestTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/WarmupRequestTests.cs @@ -34,9 +34,9 @@ public async Task Warmup_Request_Warms_Up_Caches() // assert 1 Assert.IsType(warmupResult); - var provider = executor.Services.GetCombinedServices(); - var documentCache = provider.GetRequiredService(); - var operationCache = provider.GetRequiredService(); + var documentCache = executor.Services.GetCombinedServices() + .GetRequiredService(); + var operationCache = executor.Services.GetRequiredService(); Assert.True(documentCache.TryGetDocument(documentId, out _)); Assert.Equal(1, operationCache.Count); From 60d8bc1ad63645583b6f660297231d28b74d2ca5 Mon Sep 17 00:00:00 2001 From: Tobias Tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Thu, 27 Feb 2025 12:05:20 +0100 Subject: [PATCH 09/64] Warmup new executor before replacing old one (#8068) --- ...tCoreServiceCollectionExtensions.Warmup.cs | 2 +- .../Warmup/ExecutorWarmupService.cs | 96 ------- .../Warmup/RequestExecutorWarmupService.cs | 14 + .../src/AspNetCore/Warmup/WarmupSchemaTask.cs | 25 -- .../test/AspNetCore.Tests/EvictSchemaTests.cs | 31 ++- .../AutoUpdateRequestExecutorProxy.cs | 24 +- .../InternalServiceCollectionExtensions.cs | 6 +- .../Execution/HotChocolate.Execution.csproj | 1 + .../src/Execution/IRequestExecutorWarmup.cs | 18 ++ .../IInternalRequestExecutorResolver.cs | 25 -- .../src/Execution/RequestExecutorProxy.cs | 41 +-- .../RequestExecutorResolver.Warmup.cs | 35 +++ .../src/Execution/RequestExecutorResolver.cs | 263 +++++++++++------- .../Core/src/Execution/WarmupSchemaTask.cs | 16 ++ .../AutoUpdateRequestExecutorProxyTests.cs | 18 +- .../RequestExecutorProxyTests.cs | 4 +- .../RequestExecutorResolverTests.cs | 172 +++++++++++- 17 files changed, 479 insertions(+), 312 deletions(-) delete mode 100644 src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/ExecutorWarmupService.cs create mode 100644 src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/RequestExecutorWarmupService.cs delete mode 100644 src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/WarmupSchemaTask.cs create mode 100644 src/HotChocolate/Core/src/Execution/IRequestExecutorWarmup.cs delete mode 100644 src/HotChocolate/Core/src/Execution/Internal/IInternalRequestExecutorResolver.cs create mode 100644 src/HotChocolate/Core/src/Execution/RequestExecutorResolver.Warmup.cs create mode 100644 src/HotChocolate/Core/src/Execution/WarmupSchemaTask.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Warmup.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Warmup.cs index 83ea680cda1..61d0430c0e4 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Warmup.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Warmup.cs @@ -34,7 +34,7 @@ public static IRequestExecutorBuilder InitializeOnStartup( throw new ArgumentNullException(nameof(builder)); } - builder.Services.AddHostedService(); + builder.Services.AddHostedService(); builder.Services.AddSingleton(new WarmupSchemaTask(builder.Name, keepWarm, warmup)); return builder; } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/ExecutorWarmupService.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/ExecutorWarmupService.cs deleted file mode 100644 index c518e7f8f11..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/ExecutorWarmupService.cs +++ /dev/null @@ -1,96 +0,0 @@ -using HotChocolate.Utilities; -using Microsoft.Extensions.Hosting; - -namespace HotChocolate.AspNetCore.Warmup; - -internal class ExecutorWarmupService : BackgroundService -{ - private readonly IRequestExecutorResolver _executorResolver; - private readonly Dictionary _tasks; - private IDisposable? _eventSubscription; - private CancellationToken _stopping; - - public ExecutorWarmupService( - IRequestExecutorResolver executorResolver, - IEnumerable tasks) - { - if (tasks is null) - { - throw new ArgumentNullException(nameof(tasks)); - } - - _executorResolver = executorResolver ?? - throw new ArgumentNullException(nameof(executorResolver)); - _tasks = tasks.GroupBy(t => t.SchemaName).ToDictionary(t => t.Key, t => t.ToArray()); - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - _stopping = stoppingToken; - _eventSubscription = _executorResolver.Events.Subscribe( - new WarmupObserver(name => BeginWarmup(name))); - - foreach (var task in _tasks) - { - // initialize services - var executor = await _executorResolver.GetRequestExecutorAsync(task.Key, stoppingToken); - - // execute startup task - foreach (var warmup in task.Value) - { - await warmup.ExecuteAsync(executor, stoppingToken); - } - } - } - - private void BeginWarmup(string schemaName) - { - if (_tasks.TryGetValue(schemaName, out var value) && value.Any(t => t.KeepWarm)) - { - WarmupAsync(schemaName, value, _stopping).FireAndForget(); - } - } - - private async Task WarmupAsync( - string schemaName, - WarmupSchemaTask[] tasks, - CancellationToken ct) - { - // initialize services - var executor = await _executorResolver.GetRequestExecutorAsync(schemaName, ct); - - // execute startup task - foreach (var warmup in tasks) - { - await warmup.ExecuteAsync(executor, ct); - } - } - - public override void Dispose() - { - _eventSubscription?.Dispose(); - base.Dispose(); - } - - private sealed class WarmupObserver : IObserver - { - public WarmupObserver(Action onEvicted) - { - OnEvicted = onEvicted; - } - - public Action OnEvicted { get; } - - public void OnNext(RequestExecutorEvent value) - { - if (value.Type is RequestExecutorEventType.Evicted) - { - OnEvicted(value.Name); - } - } - - public void OnError(Exception error) { } - - public void OnCompleted() { } - } -} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/RequestExecutorWarmupService.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/RequestExecutorWarmupService.cs new file mode 100644 index 00000000000..76d5d1bdf04 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/RequestExecutorWarmupService.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Hosting; + +namespace HotChocolate.AspNetCore.Warmup; + +internal sealed class RequestExecutorWarmupService( + IRequestExecutorWarmup executorWarmup) + : IHostedService +{ + public async Task StartAsync(CancellationToken cancellationToken) + => await executorWarmup.WarmupAsync(cancellationToken).ConfigureAwait(false); + + public Task StopAsync(CancellationToken cancellationToken) + => Task.CompletedTask; +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/WarmupSchemaTask.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/WarmupSchemaTask.cs deleted file mode 100644 index 7dd8822b213..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/WarmupSchemaTask.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace HotChocolate.AspNetCore.Warmup; - -internal sealed class WarmupSchemaTask -{ - private readonly Func? _warmup; - - public WarmupSchemaTask( - string schemaName, - bool keepWarm, - Func? warmup = null) - { - _warmup = warmup; - SchemaName = schemaName; - KeepWarm = keepWarm; - } - - public string SchemaName { get; } - - public bool KeepWarm { get; } - - public Task ExecuteAsync(IRequestExecutor executor, CancellationToken cancellationToken) - => _warmup is not null - ? _warmup.Invoke(executor, cancellationToken) - : Task.CompletedTask; -} diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EvictSchemaTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EvictSchemaTests.cs index e74cd33d809..5c22a89274f 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EvictSchemaTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EvictSchemaTests.cs @@ -1,26 +1,34 @@ using HotChocolate.AspNetCore.Tests.Utilities; +using HotChocolate.Execution; +using Microsoft.Extensions.DependencyInjection; namespace HotChocolate.AspNetCore; -public class EvictSchemaTests : ServerTestBase +public class EvictSchemaTests(TestServerFactory serverFactory) : ServerTestBase(serverFactory) { - public EvictSchemaTests(TestServerFactory serverFactory) - : base(serverFactory) - { - } - [Fact] public async Task Evict_Default_Schema() { // arrange + var newExecutorCreatedResetEvent = new AutoResetEvent(false); var server = CreateStarWarsServer(); var time1 = await server.GetAsync( new ClientQueryRequest { Query = "{ time }", }); + var resolver = server.Services.GetRequiredService(); + resolver.Events.Subscribe(new RequestExecutorEventObserver(@event => + { + if (@event.Type == RequestExecutorEventType.Created) + { + newExecutorCreatedResetEvent.Set(); + } + })); + // act await server.GetAsync( new ClientQueryRequest { Query = "{ evict }", }); + newExecutorCreatedResetEvent.WaitOne(5000); // assert var time2 = await server.GetAsync( @@ -32,16 +40,27 @@ await server.GetAsync( public async Task Evict_Named_Schema() { // arrange + var newExecutorCreatedResetEvent = new AutoResetEvent(false); var server = CreateStarWarsServer(); var time1 = await server.GetAsync( new ClientQueryRequest { Query = "{ time }", }, "/evict"); + var resolver = server.Services.GetRequiredService(); + resolver.Events.Subscribe(new RequestExecutorEventObserver(@event => + { + if (@event.Type == RequestExecutorEventType.Created) + { + newExecutorCreatedResetEvent.Set(); + } + })); + // act await server.GetAsync( new ClientQueryRequest { Query = "{ evict }", }, "/evict"); + newExecutorCreatedResetEvent.WaitOne(5000); // assert var time2 = await server.GetAsync( diff --git a/src/HotChocolate/Core/src/Execution/AutoUpdateRequestExecutorProxy.cs b/src/HotChocolate/Core/src/Execution/AutoUpdateRequestExecutorProxy.cs index a7c2af5ee57..f4300cc8d8f 100644 --- a/src/HotChocolate/Core/src/Execution/AutoUpdateRequestExecutorProxy.cs +++ b/src/HotChocolate/Core/src/Execution/AutoUpdateRequestExecutorProxy.cs @@ -21,9 +21,7 @@ private AutoUpdateRequestExecutorProxy( _executorProxy = requestExecutorProxy; _executor = initialExecutor; - _executorProxy.ExecutorEvicted += (_, _) => BeginUpdateExecutor(); - - BeginUpdateExecutor(); + _executorProxy.ExecutorUpdated += (_, args) => _executor = args.Executor; } /// @@ -144,26 +142,6 @@ public Task ExecuteBatchAsync( CancellationToken cancellationToken = default) => _executor.ExecuteBatchAsync(requestBatch, cancellationToken); - private void BeginUpdateExecutor() - => UpdateExecutorAsync().FireAndForget(); - - private async ValueTask UpdateExecutorAsync() - { - await _semaphore.WaitAsync().ConfigureAwait(false); - - try - { - var executor = await _executorProxy - .GetRequestExecutorAsync(CancellationToken.None) - .ConfigureAwait(false); - _executor = executor; - } - finally - { - _semaphore.Release(); - } - } - /// public void Dispose() { diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs index f7d463d43dd..f6f4dee078f 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs @@ -154,10 +154,8 @@ internal static IServiceCollection TryAddRequestExecutorResolver( this IServiceCollection services) { services.TryAddSingleton(); - services.TryAddSingleton( - sp => sp.GetRequiredService()); - services.TryAddSingleton( - sp => sp.GetRequiredService()); + services.TryAddSingleton(sp => sp.GetRequiredService()); + services.TryAddSingleton(sp => sp.GetRequiredService()); return services; } diff --git a/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj b/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj index 0980e2d578e..1942abf89c8 100644 --- a/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj +++ b/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj @@ -12,6 +12,7 @@ + diff --git a/src/HotChocolate/Core/src/Execution/IRequestExecutorWarmup.cs b/src/HotChocolate/Core/src/Execution/IRequestExecutorWarmup.cs new file mode 100644 index 00000000000..00fd7f9d68a --- /dev/null +++ b/src/HotChocolate/Core/src/Execution/IRequestExecutorWarmup.cs @@ -0,0 +1,18 @@ +namespace HotChocolate.Execution; + +/// +/// Allows to run the initial warmup for registered s. +/// +internal interface IRequestExecutorWarmup +{ + /// + /// Runs the initial warmup tasks. + /// + /// + /// The cancellation token. + /// + /// + /// Returns a task that completes once the warmup is done. + /// + Task WarmupAsync(CancellationToken cancellationToken); +} diff --git a/src/HotChocolate/Core/src/Execution/Internal/IInternalRequestExecutorResolver.cs b/src/HotChocolate/Core/src/Execution/Internal/IInternalRequestExecutorResolver.cs deleted file mode 100644 index 9be9dd64138..00000000000 --- a/src/HotChocolate/Core/src/Execution/Internal/IInternalRequestExecutorResolver.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace HotChocolate.Execution.Internal; - -/// -/// The is an internal request executor resolver that is not meant for public usage. -/// -public interface IInternalRequestExecutorResolver -{ - /// - /// Gets or creates the request executor that is associated with the - /// given configuration . - /// - /// - /// The schema name. - /// - /// - /// The cancellation token. - /// - /// - /// Returns a request executor that is associated with the - /// given configuration . - /// - ValueTask GetRequestExecutorNoLockAsync( - string? schemaName = default, - CancellationToken cancellationToken = default); -} diff --git a/src/HotChocolate/Core/src/Execution/RequestExecutorProxy.cs b/src/HotChocolate/Core/src/Execution/RequestExecutorProxy.cs index 1f467f680fa..dab1e9a5cb6 100644 --- a/src/HotChocolate/Core/src/Execution/RequestExecutorProxy.cs +++ b/src/HotChocolate/Core/src/Execution/RequestExecutorProxy.cs @@ -29,7 +29,7 @@ public RequestExecutorProxy(IRequestExecutorResolver executorResolver, string sc _schemaName = schemaName; _eventSubscription = _executorResolver.Events.Subscribe( - new ExecutorObserver(EvictRequestExecutor)); + new RequestExecutorEventObserver(OnRequestExecutorEvent)); } public IRequestExecutor? CurrentExecutor => _executor; @@ -178,15 +178,19 @@ public async ValueTask GetRequestExecutorAsync( return executor; } - private void EvictRequestExecutor(string schemaName) + private void OnRequestExecutorEvent(RequestExecutorEvent @event) { - if (!_disposed && schemaName.Equals(_schemaName)) + if (_disposed || !@event.Name.Equals(_schemaName) || _executor is null) + { + return; + } + + if (@event.Type is RequestExecutorEventType.Evicted) { _semaphore.Wait(); try { - _executor = null; ExecutorEvicted?.Invoke(this, EventArgs.Empty); } finally @@ -194,6 +198,20 @@ private void EvictRequestExecutor(string schemaName) _semaphore.Release(); } } + else if (@event.Type is RequestExecutorEventType.Created) + { + _semaphore.Wait(); + + try + { + _executor = @event.Executor; + ExecutorUpdated?.Invoke(this, new RequestExecutorUpdatedEventArgs(@event.Executor)); + } + finally + { + _semaphore.Release(); + } + } } public void Dispose() @@ -206,19 +224,4 @@ public void Dispose() _disposed = true; } } - - private sealed class ExecutorObserver(Action evicted) : IObserver - { - public void OnNext(RequestExecutorEvent value) - { - if (value.Type is RequestExecutorEventType.Evicted) - { - evicted(value.Name); - } - } - - public void OnError(Exception error) { } - - public void OnCompleted() { } - } } diff --git a/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.Warmup.cs b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.Warmup.cs new file mode 100644 index 00000000000..9e441a1449f --- /dev/null +++ b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.Warmup.cs @@ -0,0 +1,35 @@ +namespace HotChocolate.Execution; + +internal sealed partial class RequestExecutorResolver +{ + private bool _initialWarmupDone; + + public async Task WarmupAsync(CancellationToken cancellationToken) + { + if (_initialWarmupDone) + { + return; + } + _initialWarmupDone = true; + + // we get the schema names for schemas that have warmup tasks. + var schemasToWarmup = _warmupTasksBySchema.Keys; + var tasks = new Task[schemasToWarmup.Length]; + + for (var i = 0; i < schemasToWarmup.Length; i++) + { + // next we create an initial warmup for each schema + tasks[i] = WarmupSchemaAsync(schemasToWarmup[i], cancellationToken); + } + + // last we wait for all warmup tasks to complete. + await Task.WhenAll(tasks).ConfigureAwait(false); + + async Task WarmupSchemaAsync(string schemaName, CancellationToken cancellationToken) + { + // the actual warmup tasks are executed inlined into the executor creation. + await GetRequestExecutorAsync(schemaName, cancellationToken) + .ConfigureAwait(false); + } + } +} diff --git a/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs index 5b1bb06d433..1cdd73c7105 100644 --- a/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs +++ b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs @@ -1,13 +1,14 @@ using System.Collections.Concurrent; +using System.Collections.Frozen; using System.Collections.Immutable; using System.Reflection.Metadata; +using System.Threading.Channels; using HotChocolate.Configuration; using HotChocolate.Execution; using HotChocolate.Execution.Caching; using HotChocolate.Execution.Configuration; using HotChocolate.Execution.Errors; using HotChocolate.Execution.Instrumentation; -using HotChocolate.Execution.Internal; using HotChocolate.Execution.Options; using HotChocolate.Execution.Processing; using HotChocolate.Types; @@ -25,14 +26,17 @@ namespace HotChocolate.Execution; internal sealed partial class RequestExecutorResolver : IRequestExecutorResolver - , IInternalRequestExecutorResolver + , IRequestExecutorWarmup , IDisposable { - private readonly SemaphoreSlim _semaphore = new(1, 1); + private readonly CancellationTokenSource _cts = new(); + private readonly ConcurrentDictionary _semaphoreBySchema = new(); private readonly ConcurrentDictionary _executors = new(); + private readonly FrozenDictionary _warmupTasksBySchema; private readonly IRequestExecutorOptionsMonitor _optionsMonitor; private readonly IServiceProvider _applicationServices; private readonly EventObservable _events = new(); + private readonly ChannelWriter _executorEvictionChannelWriter; private ulong _version; private bool _disposed; @@ -41,17 +45,26 @@ internal sealed partial class RequestExecutorResolver public RequestExecutorResolver( IRequestExecutorOptionsMonitor optionsMonitor, + IEnumerable warmupSchemaTasks, IServiceProvider serviceProvider) { _optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor)); _applicationServices = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + _warmupTasksBySchema = warmupSchemaTasks.GroupBy(t => t.SchemaName) + .ToFrozenDictionary(g => g.Key, g => g.ToArray()); + + var executorEvictionChannel = Channel.CreateUnbounded(); + _executorEvictionChannelWriter = executorEvictionChannel.Writer; + + ConsumeExecutorEvictionsAsync(executorEvictionChannel.Reader, _cts.Token).FireAndForget(); + _optionsMonitor.OnChange(EvictRequestExecutor); // we register the schema eviction for application updates when hot reload is used. // Whenever a hot reload update is triggered we will evict all executors. - ApplicationUpdateHandler.RegisterForApplicationUpdate(() => EvictAllRequestExecutors()); + ApplicationUpdateHandler.RegisterForApplicationUpdate(EvictAllRequestExecutors); } public IObservable Events => _events; @@ -62,109 +75,171 @@ public async ValueTask GetRequestExecutorAsync( { schemaName ??= Schema.DefaultName; - if (!_executors.TryGetValue(schemaName, out var re)) + if (_executors.TryGetValue(schemaName, out var re)) { - await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + return re.Executor; + } + + var semaphore = GetSemaphoreForSchema(schemaName); + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - try + try + { + // We check the cache again for the case that GetRequestExecutorAsync has been + // called multiple times. This should only happen, if someone calls GetRequestExecutorAsync + // themselves. Normally the RequestExecutorProxy takes care of only calling this method once. + if (_executors.TryGetValue(schemaName, out re)) { - return await GetRequestExecutorNoLockAsync(schemaName, cancellationToken) - .ConfigureAwait(false); + return re.Executor; } - finally + + var registeredExecutor = await CreateRequestExecutorAsync(schemaName, true, cancellationToken) + .ConfigureAwait(false); + + return registeredExecutor.Executor; + } + finally + { + semaphore.Release(); + } + } + + public void EvictRequestExecutor(string? schemaName = default) + { + schemaName ??= Schema.DefaultName; + + _executorEvictionChannelWriter.TryWrite(schemaName); + } + + private async ValueTask ConsumeExecutorEvictionsAsync( + ChannelReader reader, + CancellationToken cancellationToken) + { + while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) + { + while (reader.TryRead(out var schemaName)) { - _semaphore.Release(); + var semaphore = GetSemaphoreForSchema(schemaName); + await semaphore.WaitAsync(cancellationToken); + + try + { + if (_executors.TryGetValue(schemaName, out var previousExecutor)) + { + await UpdateRequestExecutorAsync(schemaName, previousExecutor); + } + } + catch + { + // Ignore + } + finally + { + semaphore.Release(); + } } } - - return re.Executor; } - public async ValueTask GetRequestExecutorNoLockAsync( - string? schemaName = default, - CancellationToken cancellationToken = default) + private SemaphoreSlim GetSemaphoreForSchema(string schemaName) + => _semaphoreBySchema.GetOrAdd(schemaName, _ => new SemaphoreSlim(1, 1)); + + private async Task CreateRequestExecutorAsync( + string schemaName, + bool isInitialCreation, + CancellationToken cancellationToken) { - schemaName ??= Schema.DefaultName; + var setup = + await _optionsMonitor.GetAsync(schemaName, cancellationToken) + .ConfigureAwait(false); + + var context = new ConfigurationContext( + schemaName, + setup.SchemaBuilder ?? new SchemaBuilder(), + _applicationServices); - if (!_executors.TryGetValue(schemaName, out var registeredExecutor)) + var typeModuleChangeMonitor = new TypeModuleChangeMonitor(this, context.SchemaName); + + // if there are any type modules we will register them with the + // type module change monitor. + // The module will track if type modules signal changes to the schema and + // start a schema eviction. + foreach (var typeModule in setup.TypeModules) { - var setup = - await _optionsMonitor.GetAsync(schemaName, cancellationToken) - .ConfigureAwait(false); + typeModuleChangeMonitor.Register(typeModule); + } - var context = new ConfigurationContext( - schemaName, - setup.SchemaBuilder ?? new SchemaBuilder(), - _applicationServices); + var schemaServices = + await CreateSchemaServicesAsync(context, setup, typeModuleChangeMonitor, cancellationToken) + .ConfigureAwait(false); + + var registeredExecutor = new RegisteredExecutor( + schemaServices.GetRequiredService(), + schemaServices, + schemaServices.GetRequiredService(), + setup, + typeModuleChangeMonitor); + var executor = registeredExecutor.Executor; - var typeModuleChangeMonitor = new TypeModuleChangeMonitor(this, context.SchemaName); + await OnRequestExecutorCreatedAsync(context, executor, setup, cancellationToken) + .ConfigureAwait(false); - // if there are any type modules we will register them with the - // type module change monitor. - // The module will track if type modules signal changes to the schema and - // start a schema eviction. - foreach (var typeModule in setup.TypeModules) + if (_warmupTasksBySchema.TryGetValue(schemaName, out var warmupTasks)) + { + if (!isInitialCreation) { - typeModuleChangeMonitor.Register(typeModule); + warmupTasks = [.. warmupTasks.Where(t => t.KeepWarm)]; } - var schemaServices = - await CreateSchemaServicesAsync(context, setup, typeModuleChangeMonitor, cancellationToken) - .ConfigureAwait(false); - - registeredExecutor = new RegisteredExecutor( - schemaServices.GetRequiredService(), - schemaServices, - schemaServices.GetRequiredService(), - setup, - typeModuleChangeMonitor); + foreach (var warmupTask in warmupTasks) + { + await warmupTask.ExecuteAsync(executor, cancellationToken).ConfigureAwait(false); + } + } - var executor = registeredExecutor.Executor; + _executors[schemaName] = registeredExecutor; - await OnRequestExecutorCreatedAsync(context, executor, setup, cancellationToken) - .ConfigureAwait(false); + registeredExecutor.DiagnosticEvents.ExecutorCreated( + schemaName, + registeredExecutor.Executor); - registeredExecutor.DiagnosticEvents.ExecutorCreated( + _events.RaiseEvent( + new RequestExecutorEvent( + RequestExecutorEventType.Created, schemaName, - registeredExecutor.Executor); - _executors.TryAdd(schemaName, registeredExecutor); + registeredExecutor.Executor)); - _events.RaiseEvent( - new RequestExecutorEvent( - RequestExecutorEventType.Created, - schemaName, - registeredExecutor.Executor)); - } - - return registeredExecutor.Executor; + return registeredExecutor; } - public void EvictRequestExecutor(string? schemaName = default) + private async Task UpdateRequestExecutorAsync(string schemaName, RegisteredExecutor previousExecutor) { - schemaName ??= Schema.DefaultName; + // We dispose the subscription to type updates so there will be no updates + // during the phase-out of the previous executor. + previousExecutor.TypeModuleChangeMonitor.Dispose(); + + // This will hot swap the request executor. + await CreateRequestExecutorAsync(schemaName, false, CancellationToken.None) + .ConfigureAwait(false); - if (_executors.TryRemove(schemaName, out var executor)) + previousExecutor.DiagnosticEvents.ExecutorEvicted(schemaName, previousExecutor.Executor); + + try { - executor.DiagnosticEvents.ExecutorEvicted(schemaName, executor.Executor); + RequestExecutorEvicted?.Invoke( + this, + new RequestExecutorEvictedEventArgs(schemaName, previousExecutor.Executor)); - try - { - executor.TypeModuleChangeMonitor.Dispose(); - - RequestExecutorEvicted?.Invoke( - this, - new RequestExecutorEvictedEventArgs(schemaName, executor.Executor)); - _events.RaiseEvent( - new RequestExecutorEvent( - RequestExecutorEventType.Evicted, - schemaName, - executor.Executor)); - } - finally - { - BeginRunEvictionEvents(executor); - } + _events.RaiseEvent( + new RequestExecutorEvent( + RequestExecutorEventType.Evicted, + schemaName, + previousExecutor.Executor)); + } + finally + { + RunEvictionEvents(previousExecutor).FireAndForget(); } } @@ -176,9 +251,6 @@ private void EvictAllRequestExecutors() } } - private static void BeginRunEvictionEvents(RegisteredExecutor registeredExecutor) - => RunEvictionEvents(registeredExecutor).FireAndForget(); - private static async Task RunEvictionEvents(RegisteredExecutor registeredExecutor) { try @@ -188,7 +260,7 @@ private static async Task RunEvictionEvents(RegisteredExecutor registeredExecuto finally { // we will give the request executor some grace period to finish all request - // in the pipeline + // in the pipeline. await Task.Delay(TimeSpan.FromMinutes(5)); registeredExecutor.Dispose(); } @@ -439,9 +511,23 @@ public void Dispose() { if (!_disposed) { + // this will stop the eviction processor. + _cts.Cancel(); + + foreach (var executor in _executors.Values) + { + executor.Dispose(); + } + + foreach (var semaphore in _semaphoreBySchema.Values) + { + semaphore.Dispose(); + } + _events.Dispose(); _executors.Clear(); - _semaphore.Dispose(); + _semaphoreBySchema.Clear(); + _cts.Dispose(); _disposed = true; } } @@ -494,20 +580,11 @@ public override void OnBeforeCompleteName( } } - private sealed class TypeModuleChangeMonitor : IDisposable + private sealed class TypeModuleChangeMonitor(RequestExecutorResolver resolver, string schemaName) : IDisposable { private readonly List _typeModules = []; - private readonly RequestExecutorResolver _resolver; private bool _disposed; - public TypeModuleChangeMonitor(RequestExecutorResolver resolver, string schemaName) - { - _resolver = resolver; - SchemaName = schemaName; - } - - public string SchemaName { get; } - public void Register(ITypeModule typeModule) { typeModule.TypesChanged += EvictRequestExecutor; @@ -532,7 +609,7 @@ public IAsyncEnumerable CreateTypesAsync(IDescriptorContext c => new TypeModuleEnumerable(_typeModules, context); private void EvictRequestExecutor(object? sender, EventArgs args) - => _resolver.EvictRequestExecutor(SchemaName); + => resolver.EvictRequestExecutor(schemaName); public void Dispose() { diff --git a/src/HotChocolate/Core/src/Execution/WarmupSchemaTask.cs b/src/HotChocolate/Core/src/Execution/WarmupSchemaTask.cs new file mode 100644 index 00000000000..ae8c8d8c990 --- /dev/null +++ b/src/HotChocolate/Core/src/Execution/WarmupSchemaTask.cs @@ -0,0 +1,16 @@ +namespace HotChocolate.Execution; + +internal sealed class WarmupSchemaTask( + string schemaName, + bool keepWarm, + Func? warmup = null) +{ + public string SchemaName { get; } = schemaName; + + public bool KeepWarm { get; } = keepWarm; + + public Task ExecuteAsync(IRequestExecutor executor, CancellationToken cancellationToken) + => warmup is not null + ? warmup.Invoke(executor, cancellationToken) + : Task.CompletedTask; +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/AutoUpdateRequestExecutorProxyTests.cs b/src/HotChocolate/Core/test/Execution.Tests/AutoUpdateRequestExecutorProxyTests.cs index 59ace2dfe58..1a421f844e7 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/AutoUpdateRequestExecutorProxyTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/AutoUpdateRequestExecutorProxyTests.cs @@ -33,6 +33,7 @@ public async Task Ensure_Executor_Is_Cached() public async Task Ensure_Executor_Is_Correctly_Swapped_When_Evicted() { // arrange + var executorUpdatedResetEvent = new AutoResetEvent(false); var resolver = new ServiceCollection() .AddGraphQL() @@ -45,30 +46,21 @@ public async Task Ensure_Executor_Is_Correctly_Swapped_When_Evicted() var updated = false; var innerProxy = new RequestExecutorProxy(resolver, Schema.DefaultName); + + var proxy = await AutoUpdateRequestExecutorProxy.CreateAsync(innerProxy); innerProxy.ExecutorEvicted += (_, _) => { evicted = true; - updated = false; + executorUpdatedResetEvent.Set(); }; innerProxy.ExecutorUpdated += (_, _) => updated = true; - var proxy = await AutoUpdateRequestExecutorProxy.CreateAsync(innerProxy); - // act var a = proxy.InnerExecutor; resolver.EvictRequestExecutor(); + executorUpdatedResetEvent.WaitOne(1000); - var i = 0; var b = proxy.InnerExecutor; - while (ReferenceEquals(a, b)) - { - await Task.Delay(100); - b = proxy.InnerExecutor; - if (i++ > 10) - { - break; - } - } // assert Assert.NotSame(a, b); diff --git a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorProxyTests.cs b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorProxyTests.cs index 62a5fa30663..0bf05913747 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorProxyTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorProxyTests.cs @@ -31,6 +31,7 @@ public async Task Ensure_Executor_Is_Cached() public async Task Ensure_Executor_Is_Correctly_Swapped_When_Evicted() { // arrange + var executorUpdatedResetEvent = new AutoResetEvent(false); var resolver = new ServiceCollection() .AddGraphQL() @@ -46,13 +47,14 @@ public async Task Ensure_Executor_Is_Correctly_Swapped_When_Evicted() proxy.ExecutorEvicted += (sender, args) => { evicted = true; - updated = false; + executorUpdatedResetEvent.Set(); }; proxy.ExecutorUpdated += (sender, args) => updated = true; // act var a = await proxy.GetRequestExecutorAsync(CancellationToken.None); resolver.EvictRequestExecutor(); + executorUpdatedResetEvent.WaitOne(1000); var b = await proxy.GetRequestExecutorAsync(CancellationToken.None); // assert diff --git a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorResolverTests.cs b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorResolverTests.cs index 683574d45be..8a98bfe527f 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorResolverTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorResolverTests.cs @@ -10,12 +10,21 @@ public class RequestExecutorResolverTests public async Task Operation_Cache_Should_Be_Scoped_To_Executor() { // arrange - var services = new ServiceCollection(); - services + var executorEvictedResetEvent = new AutoResetEvent(false); + + var resolver = new ServiceCollection() .AddGraphQL() - .AddQueryType(d => d.Field("foo").Resolve("")); - var provider = services.BuildServiceProvider(); - var resolver = provider.GetRequiredService(); + .AddQueryType(d => d.Field("foo").Resolve("")) + .Services.BuildServiceProvider() + .GetRequiredService(); + + resolver.Events.Subscribe(new RequestExecutorEventObserver(@event => + { + if (@event.Type == RequestExecutorEventType.Evicted) + { + executorEvictedResetEvent.Set(); + } + })); // act var firstExecutor = await resolver.GetRequestExecutorAsync(); @@ -23,12 +32,163 @@ public async Task Operation_Cache_Should_Be_Scoped_To_Executor() .GetRequiredService(); resolver.EvictRequestExecutor(); + executorEvictedResetEvent.WaitOne(1000); var secondExecutor = await resolver.GetRequestExecutorAsync(); var secondOperationCache = secondExecutor.Services.GetCombinedServices() .GetRequiredService(); // assert - Assert.NotEqual(secondOperationCache, firstOperationCache); + Assert.NotSame(secondOperationCache, firstOperationCache); + } + + [Fact] + public async Task Executor_Should_Only_Be_Switched_Once_It_Is_Warmed_Up() + { + // arrange + var warmupResetEvent = new AutoResetEvent(true); + var executorEvictedResetEvent = new AutoResetEvent(false); + + var resolver = new ServiceCollection() + .AddGraphQL() + .InitializeOnStartup( + keepWarm: true, + warmup: (_, _) => + { + warmupResetEvent.WaitOne(1000); + + return Task.CompletedTask; + }) + .AddQueryType(d => d.Field("foo").Resolve("")) + .Services.BuildServiceProvider() + .GetRequiredService(); + + resolver.Events.Subscribe(new RequestExecutorEventObserver(@event => + { + if (@event.Type == RequestExecutorEventType.Evicted) + { + executorEvictedResetEvent.Set(); + } + })); + + // act + // assert + var initialExecutor = await resolver.GetRequestExecutorAsync(); + warmupResetEvent.Reset(); + + resolver.EvictRequestExecutor(); + + var executorAfterEviction = await resolver.GetRequestExecutorAsync(); + + Assert.Same(initialExecutor, executorAfterEviction); + + warmupResetEvent.Set(); + executorEvictedResetEvent.WaitOne(1000); + var executorAfterWarmup = await resolver.GetRequestExecutorAsync(); + + Assert.NotSame(initialExecutor, executorAfterWarmup); + } + + [Theory] + [InlineData(false, 1)] + [InlineData(true, 2)] + public async Task WarmupSchemaTasks_Are_Applied_Correct_Number_Of_Times( + bool keepWarm, int expectedWarmups) + { + // arrange + var warmups = 0; + var executorEvictedResetEvent = new AutoResetEvent(false); + + var resolver = new ServiceCollection() + .AddGraphQL() + .InitializeOnStartup( + keepWarm: keepWarm, + warmup: (_, _) => + { + warmups++; + return Task.CompletedTask; + }) + .AddQueryType(d => d.Field("foo").Resolve("")) + .Services.BuildServiceProvider() + .GetRequiredService(); + + resolver.Events.Subscribe(new RequestExecutorEventObserver(@event => + { + if (@event.Type == RequestExecutorEventType.Evicted) + { + executorEvictedResetEvent.Set(); + } + })); + + // act + // assert + var initialExecutor = await resolver.GetRequestExecutorAsync(); + + resolver.EvictRequestExecutor(); + executorEvictedResetEvent.WaitOne(1000); + + var executorAfterEviction = await resolver.GetRequestExecutorAsync(); + + Assert.NotSame(initialExecutor, executorAfterEviction); + Assert.Equal(expectedWarmups, warmups); + } + + [Fact] + public async Task Calling_GetExecutorAsync_Multiple_Times_Only_Creates_One_Executor() + { + // arrange + var resolver = new ServiceCollection() + .AddGraphQL() + .AddQueryType(d => + { + d.Field("foo").Resolve(""); + }) + .Services.BuildServiceProvider() + .GetRequiredService(); + + // act + var executor1Task = Task.Run(async () => await resolver.GetRequestExecutorAsync()); + var executor2Task = Task.Run(async () => await resolver.GetRequestExecutorAsync()); + + var executor1 = await executor1Task; + var executor2 = await executor2Task; + + // assert + Assert.Same(executor1, executor2); + } + + [Fact] + public async Task Executor_Resolution_Should_Be_Parallel() + { + // arrange + var schema1CreationResetEvent = new AutoResetEvent(false); + + var services = new ServiceCollection(); + services + .AddGraphQL("schema1") + .AddQueryType(d => + { + schema1CreationResetEvent.WaitOne(1000); + d.Field("foo").Resolve(""); + }); + services + .AddGraphQL("schema2") + .AddQueryType(d => + { + d.Field("foo").Resolve(""); + }); + var provider = services.BuildServiceProvider(); + var resolver = provider.GetRequiredService(); + + // act + var executor1Task = Task.Run(async () => await resolver.GetRequestExecutorAsync("schema1")); + var executor2Task = Task.Run(async () => await resolver.GetRequestExecutorAsync("schema2")); + + // assert + await executor2Task; + + schema1CreationResetEvent.Set(); + + await executor1Task; } } From 23d261d350d0942de99ea6d4aafd46a7deee4a68 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 28 Feb 2025 10:42:40 +0100 Subject: [PATCH 10/64] Remove Hot Reload Hooks (#8079) --- .../src/Execution/RequestExecutorResolver.cs | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs index 1cdd73c7105..9fc0dfdc1a7 100644 --- a/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs +++ b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs @@ -1,10 +1,8 @@ using System.Collections.Concurrent; using System.Collections.Frozen; using System.Collections.Immutable; -using System.Reflection.Metadata; using System.Threading.Channels; using HotChocolate.Configuration; -using HotChocolate.Execution; using HotChocolate.Execution.Caching; using HotChocolate.Execution.Configuration; using HotChocolate.Execution.Errors; @@ -20,8 +18,6 @@ using Microsoft.Extensions.ObjectPool; using static HotChocolate.Execution.ThrowHelper; -[assembly: MetadataUpdateHandler(typeof(RequestExecutorResolver.ApplicationUpdateHandler))] - namespace HotChocolate.Execution; internal sealed partial class RequestExecutorResolver @@ -61,10 +57,6 @@ public RequestExecutorResolver( ConsumeExecutorEvictionsAsync(executorEvictionChannel.Reader, _cts.Token).FireAndForget(); _optionsMonitor.OnChange(EvictRequestExecutor); - - // we register the schema eviction for application updates when hot reload is used. - // Whenever a hot reload update is triggered we will evict all executors. - ApplicationUpdateHandler.RegisterForApplicationUpdate(EvictAllRequestExecutors); } public IObservable Events => _events; @@ -243,14 +235,6 @@ await CreateRequestExecutorAsync(schemaName, false, CancellationToken.None) } } - private void EvictAllRequestExecutors() - { - foreach (var key in _executors.Keys) - { - EvictRequestExecutor(key); - } - } - private static async Task RunEvictionEvents(RegisteredExecutor registeredExecutor) { try @@ -777,32 +761,4 @@ private sealed class SchemaSetupInfo( public IList Pipeline { get; } = pipeline; } - - /// - /// A helper calls that receives hot reload update events from the runtime and triggers - /// reload of registered components. - /// - internal static class ApplicationUpdateHandler - { - private static readonly List _actions = []; - - public static void RegisterForApplicationUpdate(Action action) - { - lock (_actions) - { - _actions.Add(action); - } - } - - public static void UpdateApplication(Type[]? updatedTypes) - { - lock (_actions) - { - foreach (var action in _actions) - { - action(); - } - } - } - } } From 78d6b762ec6db648757667733bb217364fd58273 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 6 Mar 2025 22:11:58 +0100 Subject: [PATCH 11/64] Fixed authorization type interceptor flow. (#8096) --- .../AuthorizationTypeInterceptor.cs | 183 ++++++++++-------- .../Types/Configuration/TypeInitializer.cs | 7 +- .../AnnotationBasedAuthorizationTests.cs | 26 ++- .../SchemaFirstAuthorizationTests.cs | 41 ++++ ...nTests.Authorize_Node_Field_Schema.graphql | 58 ++++++ ...sts.Authorize_Apply_Can_Be_Omitted.graphql | 23 +++ 6 files changed, 252 insertions(+), 86 deletions(-) create mode 100644 src/HotChocolate/Core/test/Authorization.Tests/SchemaFirstAuthorizationTests.cs create mode 100644 src/HotChocolate/Core/test/Authorization.Tests/__snapshots__/AnnotationBasedAuthorizationTests.Authorize_Node_Field_Schema.graphql create mode 100644 src/HotChocolate/Core/test/Authorization.Tests/__snapshots__/SchemaFirstAuthorizationTests.Authorize_Apply_Can_Be_Omitted.graphql diff --git a/src/HotChocolate/Core/src/Authorization/AuthorizationTypeInterceptor.cs b/src/HotChocolate/Core/src/Authorization/AuthorizationTypeInterceptor.cs index c473fcad7c2..53232f18d80 100644 --- a/src/HotChocolate/Core/src/Authorization/AuthorizationTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Authorization/AuthorizationTypeInterceptor.cs @@ -53,6 +53,9 @@ internal override void OnBeforeCreateSchemaInternal( schemaBuilder.SetSchema(d => _schemaContextData = d.Extend().Definition.ContextData); } + private ITypeCompletionContext _tc = default!; + private AuthorizeDirectiveType _t = default!; + public override void OnBeforeCompleteName( ITypeCompletionContext completionContext, DefinitionBase definition) @@ -69,14 +72,33 @@ public override void OnBeforeCompleteName( case UnionType when definition is UnionTypeDefinition unionTypeDef: _unionTypes.Add(new UnionTypeInfo(completionContext, unionTypeDef)); break; + + case AuthorizeDirectiveType type: + _t = type; + _tc = completionContext; + break; } // note, we do not need to collect interfaces as the object type has a // list implements that links to the interfaces that expose an object type. } - public override void OnBeforeCompleteTypes() + public override void OnAfterResolveRootType( + ITypeCompletionContext completionContext, + ObjectTypeDefinition definition, + OperationType operationType) + { + if (operationType is OperationType.Query) + { + _queryContext = completionContext; + } + } + + public override void OnBeforeCompleteMetadata() { + _t.CompleteMetadata(_tc); + ((RegisteredType)_tc).Status = TypeStatus.MetadataCompleted; + // at this stage in the type initialization we will create some state that we // will use to transform the schema authorization. var state = _state = CreateState(); @@ -99,18 +121,7 @@ public override void OnBeforeCompleteTypes() FindFieldsAndApplyAuthMiddleware(state); } - public override void OnAfterResolveRootType( - ITypeCompletionContext completionContext, - ObjectTypeDefinition definition, - OperationType operationType) - { - if (operationType is OperationType.Query) - { - _queryContext = completionContext; - } - } - - public override void OnBeforeCompleteType( + public override void OnBeforeCompleteMetadata( ITypeCompletionContext completionContext, DefinitionBase definition) { @@ -130,28 +141,30 @@ public override void OnAfterMakeExecutable() { var objectType = (ObjectType)type.TypeReg.Type; - if (objectType.ContextData.TryGetValue(NodeResolver, out var o) && - o is NodeResolverInfo nodeResolverInfo) + if (!objectType.ContextData.TryGetValue(NodeResolver, out var o) + || o is not NodeResolverInfo nodeResolverInfo) { - var pipeline = nodeResolverInfo.Pipeline; - var directives = (DirectiveCollection)objectType.Directives; - var length = directives.Count; - ref var start = ref directives.GetReference(); + continue; + } - for (var i = length - 1; i >= 0; i--) - { - var directive = Unsafe.Add(ref start, i); + var pipeline = nodeResolverInfo.Pipeline; + var directives = (DirectiveCollection)objectType.Directives; + var length = directives.Count; + ref var start = ref directives.GetReference(); - if (directive.Type.Name.EqualsOrdinal(Authorize)) - { - var authDir = directive.AsValue(); - pipeline = CreateAuthMiddleware(authDir).Middleware.Invoke(pipeline); - } - } + for (var i = length - 1; i >= 0; i--) + { + var directive = Unsafe.Add(ref start, i); - type.TypeDef.ContextData[NodeResolver] = - new NodeResolverInfo(nodeResolverInfo.QueryField, pipeline); + if (directive.Type.Name.EqualsOrdinal(Authorize)) + { + var authDir = directive.AsValue(); + pipeline = CreateAuthMiddleware(authDir).Middleware.Invoke(pipeline); + } } + + type.TypeDef.ContextData[NodeResolver] = + new NodeResolverInfo(nodeResolverInfo.QueryField, pipeline); } } @@ -159,72 +172,76 @@ private void InspectObjectTypesForAuthDirective(State state) { foreach (var type in _objectTypes) { - if (IsAuthorizedType(type.TypeDef)) + if (!IsAuthorizedType(type.TypeDef)) { - var registration = type.TypeReg; - var mainTypeRef = registration.TypeReference; + continue; + } + + var registration = type.TypeReg; + var mainTypeRef = registration.TypeReference; - // if this type is a root type we will copy type level auth down to the field. - if (registration.IsQueryType == true || - registration.IsMutationType == true || - registration.IsSubscriptionType == true) + // if this type is a root type we will copy type level auth down to the field. + if (registration.IsQueryType == true || + registration.IsMutationType == true || + registration.IsSubscriptionType == true) + { + foreach (var fieldDef in type.TypeDef.Fields) { - foreach (var fieldDef in type.TypeDef.Fields) + // we are not interested in introspection fields or the node fields. + if (fieldDef.IsIntrospectionField || fieldDef.IsNodeField()) { - // we are not interested in introspection fields or the node fields. - if (fieldDef.IsIntrospectionField || fieldDef.IsNodeField()) - { - continue; - } - - // if the field contains the AnonymousAllowed flag we will not - // apply authorization on it. - if(fieldDef.GetContextData().ContainsKey(AllowAnonymous)) - { - continue; - } + continue; + } - ApplyAuthMiddleware(fieldDef, registration, false); + // if the field contains the AnonymousAllowed flag we will not + // apply authorization on it. + if(fieldDef.GetContextData().ContainsKey(AllowAnonymous)) + { + continue; } - } - foreach (var reference in registration.References) - { - state.AuthTypes.Add(reference); - state.NeedsAuth.Add(reference); + ApplyAuthMiddleware(fieldDef, registration, false); } + } + + foreach (var reference in registration.References) + { + state.AuthTypes.Add(reference); + state.NeedsAuth.Add(reference); + } + + if (!type.TypeDef.HasInterfaces) + { + continue; + } - if (type.TypeDef.HasInterfaces) + CollectInterfaces( + type.TypeDef.GetInterfaces(), + interfaceTypeRef => { - CollectInterfaces( - type.TypeDef.GetInterfaces(), - interfaceTypeRef => + if (_typeRegistry.TryGetType( + interfaceTypeRef, + out var interfaceTypeReg)) + { + foreach (var typeRef in interfaceTypeReg.References) { - if (_typeRegistry.TryGetType( - interfaceTypeRef, - out var interfaceTypeReg)) + state.NeedsAuth.Add(typeRef); + + if (!state.AbstractToConcrete.TryGetValue( + typeRef, + out var authTypeRefs)) { - foreach (var typeRef in interfaceTypeReg.References) - { - state.NeedsAuth.Add(typeRef); - - if (!state.AbstractToConcrete.TryGetValue( - typeRef, - out var authTypeRefs)) - { - authTypeRefs = []; - state.AbstractToConcrete.Add(typeRef, authTypeRefs); - } - - authTypeRefs.Add(mainTypeRef); - } + authTypeRefs = []; + state.AbstractToConcrete.Add(typeRef, authTypeRefs); } - }, - state); - state.Completed.Clear(); - } - } + authTypeRefs.Add(mainTypeRef); + } + } + }, + state); + + state.Completed.Clear(); } } @@ -623,7 +640,7 @@ private State CreateState() } } -static file class AuthorizationTypeInterceptorExtensions +file static class AuthorizationTypeInterceptorExtensions { public static bool IsNodeField(this ObjectFieldDefinition fieldDef) { diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeInitializer.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeInitializer.cs index 54f72d507f4..fdd7bc5d4d6 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeInitializer.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeInitializer.cs @@ -595,7 +595,7 @@ private void CompleteTypes() internal bool CompleteType(RegisteredType registeredType) { - if (registeredType.Status is TypeStatus.Completed) + if (registeredType.Type.IsCompleted) { return true; } @@ -615,9 +615,10 @@ private void CompleteMetadata() foreach (var registeredType in _typeRegistry.Types) { - if (!registeredType.IsExtension) + if (registeredType is { IsExtension: false, Status: TypeStatus.Completed }) { registeredType.Type.CompleteMetadata(registeredType); + registeredType.Status = TypeStatus.MetadataCompleted; } } @@ -635,6 +636,7 @@ private void MakeExecutable() if (!registeredType.IsExtension) { registeredType.Type.MakeExecutable(registeredType); + registeredType.Status = TypeStatus.Executable; } } @@ -650,6 +652,7 @@ private void FinalizeTypes() if (!registeredType.IsExtension) { registeredType.Type.FinalizeType(registeredType); + registeredType.Status = TypeStatus.Finalized; } } diff --git a/src/HotChocolate/Core/test/Authorization.Tests/AnnotationBasedAuthorizationTests.cs b/src/HotChocolate/Core/test/Authorization.Tests/AnnotationBasedAuthorizationTests.cs index 06da7dd256f..78cea2aab53 100644 --- a/src/HotChocolate/Core/test/Authorization.Tests/AnnotationBasedAuthorizationTests.cs +++ b/src/HotChocolate/Core/test/Authorization.Tests/AnnotationBasedAuthorizationTests.cs @@ -499,6 +499,30 @@ public async Task Authorize_Type_Field() Assert.Equal(401, value); } + [Fact] + public async Task Authorize_Node_Field_Schema() + { + // arrange + var handler = new AuthHandler( + resolver: (_, _) => AuthorizeResult.Allowed, + validation: (_, d) => d.Policy.EqualsOrdinal("READ_NODE") + ? AuthorizeResult.NotAllowed + : AuthorizeResult.Allowed); + + // act + var services = CreateServices( + handler, + options => + { + options.ConfigureNodeFields = + descriptor => descriptor.Authorize("READ_NODE", ApplyPolicy.Validation); + }); + + // assert + var executor = await services.GetRequestExecutorAsync(); + executor.Schema.MatchSnapshot(); + } + [Fact] public async Task Authorize_Node_Field() { @@ -513,7 +537,7 @@ public async Task Authorize_Node_Field() options => { options.ConfigureNodeFields = - descriptor => { descriptor.Authorize("READ_NODE", ApplyPolicy.Validation); }; + descriptor => descriptor.Authorize("READ_NODE", ApplyPolicy.Validation); }); var executor = await services.GetRequestExecutorAsync(); diff --git a/src/HotChocolate/Core/test/Authorization.Tests/SchemaFirstAuthorizationTests.cs b/src/HotChocolate/Core/test/Authorization.Tests/SchemaFirstAuthorizationTests.cs new file mode 100644 index 00000000000..b1b108541dc --- /dev/null +++ b/src/HotChocolate/Core/test/Authorization.Tests/SchemaFirstAuthorizationTests.cs @@ -0,0 +1,41 @@ +using HotChocolate.Execution; +using HotChocolate.Resolvers; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Authorization; + +public class SchemaFirstAuthorizationTests +{ + [Fact] + public async Task Authorize_Apply_Can_Be_Omitted() + { + var schema = await new ServiceCollection() + .AddGraphQLServer() + .AddDocumentFromString( + """ + type Query @authorize(roles: [ "policy_tester_noupdate", "policy_tester_update_noread", "authorizationHandlerTester" ]) { + hello: String @authorize(roles: ["admin"]) + } + """) + .AddResolver("Query", "hello", "world") + .AddAuthorizationHandler() + .BuildSchemaAsync(); + + schema.MatchSnapshot(); + } + + private sealed class MockAuth : IAuthorizationHandler + { + public ValueTask AuthorizeAsync( + IMiddlewareContext context, + AuthorizeDirective directive, + CancellationToken cancellationToken = default) + => new(AuthorizeResult.NotAllowed); + + public ValueTask AuthorizeAsync( + AuthorizationContext context, + IReadOnlyList directives, + CancellationToken cancellationToken = default) + => new(AuthorizeResult.NotAllowed); + } +} diff --git a/src/HotChocolate/Core/test/Authorization.Tests/__snapshots__/AnnotationBasedAuthorizationTests.Authorize_Node_Field_Schema.graphql b/src/HotChocolate/Core/test/Authorization.Tests/__snapshots__/AnnotationBasedAuthorizationTests.Authorize_Node_Field_Schema.graphql new file mode 100644 index 00000000000..c38127fb745 --- /dev/null +++ b/src/HotChocolate/Core/test/Authorization.Tests/__snapshots__/AnnotationBasedAuthorizationTests.Authorize_Node_Field_Schema.graphql @@ -0,0 +1,58 @@ +schema { + query: Query +} + +"The node interface is implemented by entities that have a global unique identifier." +interface Node { + id: ID! +} + +type City @authorize(policy: "READ_CITY", apply: AFTER_RESOLVER) { + value: String +} + +type Person implements Node @authorize(policy: "READ_PERSON", apply: AFTER_RESOLVER) { + id: ID! + name: String +} + +type Query @foo @authorize(policy: "QUERY", apply: VALIDATION) @authorize(policy: "QUERY2") { + "Fetches an object given its ID." + node("ID of the object." id: ID!): Node @cost(weight: "10") @authorize(policy: "READ_NODE", apply: VALIDATION) + "Lookup nodes by a list of IDs." + nodes("The list of node IDs." ids: [ID!]!): [Node]! @cost(weight: "10") @authorize(policy: "READ_NODE", apply: VALIDATION) + null: String @authorize(policy: "NULL", apply: AFTER_RESOLVER) + person(id: ID!): Person + person2(id: String!): Person @allowAnonymous + cityOrStreet(street: Boolean!): ICityOrStreet + thisIsAuthorized: Boolean @authorize(policy: "READ_AUTH", apply: AFTER_RESOLVER) + thisIsAuthorizedOnValidation: Boolean @authorize(policy: "READ_AUTH", apply: VALIDATION) + test: ID! +} + +type Street implements Node @authorize(policy: "READ_STREET_ON_TYPE") { + value: String + id: ID! @cost(weight: "10") +} + +union ICityOrStreet = Street | City + +"Defines when a policy shall be executed." +enum ApplyPolicy { + "Before the resolver was executed." + BEFORE_RESOLVER + "After the resolver was executed." + AFTER_RESOLVER + "The policy is applied in the validation step before the execution." + VALIDATION +} + +directive @allowAnonymous repeatable on FIELD_DEFINITION + +"The authorize directive." +directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on OBJECT | FIELD_DEFINITION + +"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response." +directive @cost("The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." weight: String!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION + +directive @foo on OBJECT diff --git a/src/HotChocolate/Core/test/Authorization.Tests/__snapshots__/SchemaFirstAuthorizationTests.Authorize_Apply_Can_Be_Omitted.graphql b/src/HotChocolate/Core/test/Authorization.Tests/__snapshots__/SchemaFirstAuthorizationTests.Authorize_Apply_Can_Be_Omitted.graphql new file mode 100644 index 00000000000..2f803358229 --- /dev/null +++ b/src/HotChocolate/Core/test/Authorization.Tests/__snapshots__/SchemaFirstAuthorizationTests.Authorize_Apply_Can_Be_Omitted.graphql @@ -0,0 +1,23 @@ +schema { + query: Query +} + +type Query @authorize(roles: [ "policy_tester_noupdate", "policy_tester_update_noread", "authorizationHandlerTester" ]) { + hello: String @authorize(roles: [ "admin" ]) @cost(weight: "10") +} + +"Defines when a policy shall be executed." +enum ApplyPolicy { + "Before the resolver was executed." + BEFORE_RESOLVER + "After the resolver was executed." + AFTER_RESOLVER + "The policy is applied in the validation step before the execution." + VALIDATION +} + +"The authorize directive." +directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on OBJECT | FIELD_DEFINITION + +"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response." +directive @cost("The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." weight: String!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION From 14b2f52b41ce76b1d6c950dde9bbbc6d029dcbea Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 7 Mar 2025 09:15:29 +0100 Subject: [PATCH 12/64] Added `@oneof` tests (#8098) --- .../Types/OneOfIntegrationTests.cs | 73 +++++++++++++++++++ ...Of_DefaultValue_On_Directive_Argument.snap | 23 ++++++ ...ultValue_On_Directive_Argument_Fluent.snap | 23 ++++++ 3 files changed, 119 insertions(+) create mode 100644 src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/OneOfIntegrationTests.OneOf_DefaultValue_On_Directive_Argument.snap create mode 100644 src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/OneOfIntegrationTests.OneOf_DefaultValue_On_Directive_Argument_Fluent.snap diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/OneOfIntegrationTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/OneOfIntegrationTests.cs index fd177f55035..f952f20eb13 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/OneOfIntegrationTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/OneOfIntegrationTests.cs @@ -334,6 +334,43 @@ public async Task Oneof_introspection() .MatchSnapshotAsync(); } + [Fact] + public async Task OneOf_DefaultValue_On_Directive_Argument() + { + await new ServiceCollection() + .AddGraphQL() + .AddType() + .AddDocumentFromString( + """ + type Query { + foo: String @defaultValue(value: { string: "abc" }) + } + """) + .AddResolver("Query", "foo", "abc") + .ModifyOptions(o => o.EnableOneOf = true) + .BuildSchemaAsync() + .MatchSnapshotAsync(); + } + + [Fact] + public async Task OneOf_DefaultValue_On_Directive_Argument_Fluent() + { + await new ServiceCollection() + .AddGraphQL() + .AddType() + .AddType() + .AddDocumentFromString( + """ + type Query { + foo: String @defaultValue(value: { string: "abc" }) + } + """) + .AddResolver("Query", "foo", "abc") + .ModifyOptions(o => o.EnableOneOf = true) + .BuildSchemaAsync() + .MatchSnapshotAsync(); + } + public class Query { public string Example(ExampleInput input) @@ -390,4 +427,40 @@ public class Example2Input public int? B { get; set; } } + + + [DirectiveType(DirectiveLocation.FieldDefinition)] + public class DefaultValue + { + public DefaultValueInput? Value { get; set; } + } + + [OneOf] + public class DefaultValueInput + { + public string? String { get; set; } + + public int? Int { get; set; } + } + + public class DefaultValueType : InputObjectType + { + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + descriptor.Name("DefaultValue"); + descriptor.OneOf(); + descriptor.Field("string").Type(); + descriptor.Field("int").Type(); + } + } + + public class DefaultValueDirectiveType : DirectiveType + { + protected override void Configure(IDirectiveTypeDescriptor descriptor) + { + descriptor.Name("defaultValue"); + descriptor.Argument("value").Type(); + descriptor.Location(DirectiveLocation.FieldDefinition); + } + } } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/OneOfIntegrationTests.OneOf_DefaultValue_On_Directive_Argument.snap b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/OneOfIntegrationTests.OneOf_DefaultValue_On_Directive_Argument.snap new file mode 100644 index 00000000000..e16a23e924b --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/OneOfIntegrationTests.OneOf_DefaultValue_On_Directive_Argument.snap @@ -0,0 +1,23 @@ +schema { + query: Query +} + +type Query { + foo: String @defaultValue(value: { string: "abc" }) +} + +input DefaultValueInput @oneOf { + string: String + int: Int +} + +directive @defaultValue(value: DefaultValueInput) on FIELD_DEFINITION + +""" +The `@oneOf` directive is used within the type system definition language + to indicate: + + - an Input Object is a Oneof Input Object, or + - an Object Type's Field is a Oneof Field. +""" +directive @oneOf on INPUT_OBJECT diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/OneOfIntegrationTests.OneOf_DefaultValue_On_Directive_Argument_Fluent.snap b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/OneOfIntegrationTests.OneOf_DefaultValue_On_Directive_Argument_Fluent.snap new file mode 100644 index 00000000000..569ead1ab68 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/OneOfIntegrationTests.OneOf_DefaultValue_On_Directive_Argument_Fluent.snap @@ -0,0 +1,23 @@ +schema { + query: Query +} + +type Query { + foo: String @defaultValue(value: { string: "abc" }) +} + +input DefaultValue @oneOf { + string: String + int: Int +} + +directive @defaultValue(value: DefaultValue) on FIELD_DEFINITION + +""" +The `@oneOf` directive is used within the type system definition language + to indicate: + + - an Input Object is a Oneof Input Object, or + - an Object Type's Field is a Oneof Field. +""" +directive @oneOf on INPUT_OBJECT From fb2b1b833242eeece27ceb9d5be32fc838c6e6af Mon Sep 17 00:00:00 2001 From: Artem Sharypov <79115666058@ya.ru> Date: Fri, 7 Mar 2025 15:18:36 +0300 Subject: [PATCH 13/64] Fix nested sorting a new approach QueryContext (#8061) --- .../Data/Sorting/Context/SortingContext.cs | 21 ++++--- .../Data.Sorting.Tests/IntegrationTests.cs | 55 +++++++++++++++++++ ...Tests.Sorting_Should_Work_When_Nested.snap | 24 ++++++++ 3 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/IntegrationTests.Sorting_Should_Work_When_Nested.snap diff --git a/src/HotChocolate/Data/src/Data/Sorting/Context/SortingContext.cs b/src/HotChocolate/Data/src/Data/Sorting/Context/SortingContext.cs index d371adb2bd3..98e4f57f076 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Context/SortingContext.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Context/SortingContext.cs @@ -184,18 +184,25 @@ protected override ISyntaxVisitorAction Leave( Context context) { var type = context.Types.Peek(); - var field = (SortField)type.Fields[node.Name.Value]; - var fieldType = field.Type.NamedType(); - var expression = context.Parents.Pop(); - if (fieldType.IsInputObjectType()) + if (type.Fields.TryGetField(node.Name.Value, out var inputField) && inputField is SortField sortField) { - context.Types.Pop(); + var fieldType = sortField.Type.NamedType(); + var expression = context.Parents.Pop(); + + if (fieldType.IsInputObjectType()) + { + context.Types.Pop(); + } + else + { + var ascending = node.Value.Value?.Equals("ASC") ?? true; + context.Completed.Add((expression, ascending, sortField.Member!.GetReturnType())); + } } else { - var ascending = node.Value.Value?.Equals("ASC") ?? true; - context.Completed.Add((expression, ascending, field.Member!.GetReturnType())); + context.Types.Pop(); } return base.Leave(node, context); diff --git a/src/HotChocolate/Data/test/Data.Sorting.Tests/IntegrationTests.cs b/src/HotChocolate/Data/test/Data.Sorting.Tests/IntegrationTests.cs index ecfd6940769..59bc5fd7ff7 100644 --- a/src/HotChocolate/Data/test/Data.Sorting.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Data/test/Data.Sorting.Tests/IntegrationTests.cs @@ -1,5 +1,7 @@ +using GreenDonut.Data; using HotChocolate.Execution; using HotChocolate.Types; +using HotChocolate.Types.Pagination; using Microsoft.Extensions.DependencyInjection; namespace HotChocolate.Data.Tests; @@ -30,6 +32,34 @@ public async Task Sorting_Should_Work_When_UsedWithNonNullDateTime() // assert result.MatchSnapshot(); } + + [Fact] + public async Task Sorting_Should_Work_When_Nested() + { + // arrange + var executor = await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .AddSorting() + .BuildRequestExecutorAsync(); + + const string query = @" + { + books(order: [{ author: { name: ASC } }]) { + title + author { + name + } + } + } + "; + + // act + var result = await executor.ExecuteAsync(query); + + // assert + result.MatchSnapshot(); + } } public class Query @@ -41,6 +71,17 @@ public IEnumerable Foos() => new[] new Foo { CreatedUtc = new DateTime(2010, 1, 1, 1, 1, 1), }, new Foo { CreatedUtc = new DateTime(2020, 1, 1, 1, 1, 1), }, }; + + [UseSorting] + public IEnumerable GetBooks(QueryContext queryContext) + => new[] + { + new Book { Title = "Book5", Author = new Author { Name = "Author6" } }, + new Book { Title = "Book7", Author = new Author { Name = "Author17" } }, + new Book { Title = "Book1", Author = new Author { Name = "Author5" } }, + } + .AsQueryable() + .With(queryContext); } public class Foo @@ -48,3 +89,17 @@ public class Foo [GraphQLType(typeof(NonNullType))] public DateTime CreatedUtc { get; set; } } + +public class Author +{ + public string Name { get; set; } = string.Empty; + + [UseSorting] + public Book[] Books { get; set; } = Array.Empty(); +} + +public class Book +{ + public string Title { get; set; } = string.Empty; + public Author? Author { get; set; } +} diff --git a/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/IntegrationTests.Sorting_Should_Work_When_Nested.snap b/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/IntegrationTests.Sorting_Should_Work_When_Nested.snap new file mode 100644 index 00000000000..c483d6689a4 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/IntegrationTests.Sorting_Should_Work_When_Nested.snap @@ -0,0 +1,24 @@ +{ + "data": { + "books": [ + { + "title": "Book7", + "author": { + "name": "Author17" + } + }, + { + "title": "Book1", + "author": { + "name": "Author5" + } + }, + { + "title": "Book5", + "author": { + "name": "Author6" + } + } + ] + } +} From c02b939b8d2c27e9db8b2ceb13529829cf32dcf8 Mon Sep 17 00:00:00 2001 From: Sunghwan Bang Date: Fri, 7 Mar 2025 21:22:34 +0900 Subject: [PATCH 14/64] Clean SortInput postfix (#8045) --- .../Data/Sorting/Convention/SortConvention.cs | 30 +++++++++++++++++-- .../Convention/SortConventionTests.cs | 29 ++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs b/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs index e4b1b13df3e..40da78b767c 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs @@ -18,7 +18,8 @@ public class SortConvention : Convention , ISortConvention { - private const string _typePostFix = "SortInput"; + private const string _inputPostFix = "SortInput"; + private const string _inputTypePostFix = "SortInputType"; private Action? _configure; private INamingConventions _namingConventions = default!; @@ -126,8 +127,31 @@ protected internal override void Complete(IConventionContext context) } /// - public virtual string GetTypeName(Type runtimeType) => - _namingConventions.GetTypeName(runtimeType, TypeKind.Object) + _typePostFix; + public virtual string GetTypeName(Type runtimeType) + { + var name = _namingConventions.GetTypeName(runtimeType); + + var isInputObjectType = typeof(SortInputType).IsAssignableFrom(runtimeType); + var isEndingInput = name.EndsWith(_inputPostFix, StringComparison.Ordinal); + var isEndingInputType = name.EndsWith(_inputTypePostFix, StringComparison.Ordinal); + + if (isInputObjectType && isEndingInputType) + { + return name[..^4]; + } + + if (isInputObjectType && !isEndingInput && !isEndingInputType) + { + return name + _inputPostFix; + } + + if (!isInputObjectType && !isEndingInput) + { + return name + _inputPostFix; + } + + return name; + } /// public virtual string? GetTypeDescription(Type runtimeType) => diff --git a/src/HotChocolate/Data/test/Data.Sorting.Tests/Convention/SortConventionTests.cs b/src/HotChocolate/Data/test/Data.Sorting.Tests/Convention/SortConventionTests.cs index 6fb8e0e1aa6..d9ca3ae8882 100644 --- a/src/HotChocolate/Data/test/Data.Sorting.Tests/Convention/SortConventionTests.cs +++ b/src/HotChocolate/Data/test/Data.Sorting.Tests/Convention/SortConventionTests.cs @@ -367,6 +367,35 @@ public void SortConvention_Should_Work_With_ProviderExtensionsType() x => Assert.Equal("c", x.Bar)); } + [Fact] + public void GetTypeName_TypeNameEndingWithSortInputType_RemovesTypeSuffix() + { + // arrange + var provider = new QueryableSortProvider( + descriptor => + { + descriptor.AddFieldHandler(); + descriptor.AddOperationHandler(); + }); + var convention = new SortConvention(descriptor => + { + descriptor.Operation(DefaultSortOperations.Ascending).Name("asc"); + descriptor.Provider(provider); + }); + var sortInputType = new SortInputType( + d => d.Field("x").Type() + .ExtendWith( + x => x.Definition.Handler = new MatchAnyQueryableFieldHandler())); + + // act + var schema = CreateSchemaWith(sortInputType, convention); + + // assert + Assert.Equal( + "SortInput", + schema.Types.First(t => t.IsInputType() && !t.IsIntrospectionType()).Name); + } + protected ISchema CreateSchemaWithTypes( ISortInputType type, SortConvention convention, From f2784cfbe4805d2d577bb800711e62c1241fa2d0 Mon Sep 17 00:00:00 2001 From: Sunghwan Bang Date: Fri, 7 Mar 2025 21:23:51 +0900 Subject: [PATCH 15/64] Add services to support complex sort handler (#8047) --- .../Data/src/Data/DataResources.Designer.cs | 6 + .../Data/src/Data/DataResources.resx | 3 + .../Data/Sorting/Convention/SortConvention.cs | 6 +- .../Visitor/ISortProviderConvention.cs | 2 +- .../src/Data/Sorting/Visitor/SortProvider.cs | 13 +- .../Sorting/Visitor/SortProviderExtensions.cs | 2 +- src/HotChocolate/Data/src/Data/ThrowHelper.cs | 12 ++ .../QueryableSortVisitorObjectTests.cs | 195 +++++++++++++++++- .../SchemaCache.cs | 5 +- ...ests.Create_ObjectComplex_OrderBy_Sum.snap | 61 ++++++ 10 files changed, 295 insertions(+), 10 deletions(-) create mode 100644 src/HotChocolate/Data/test/Data.Sorting.InMemory.Tests/__snapshots__/QueryableSortVisitorObjectTests.Create_ObjectComplex_OrderBy_Sum.snap diff --git a/src/HotChocolate/Data/src/Data/DataResources.Designer.cs b/src/HotChocolate/Data/src/Data/DataResources.Designer.cs index 64ce2384a26..a915df2a060 100644 --- a/src/HotChocolate/Data/src/Data/DataResources.Designer.cs +++ b/src/HotChocolate/Data/src/Data/DataResources.Designer.cs @@ -471,6 +471,12 @@ internal static string FilterConvention_ProviderHasToBeInitializedByConvention { } } + internal static string SortConvention_ProviderHasToBeInitializedByConvention { + get { + return ResourceManager.GetString("SortConvention_ProviderHasToBeInitializedByConvention", resourceCulture); + } + } + internal static string UseProjection_CannotHandleType { get { return ResourceManager.GetString("UseProjection_CannotHandleType", resourceCulture); diff --git a/src/HotChocolate/Data/src/Data/DataResources.resx b/src/HotChocolate/Data/src/Data/DataResources.resx index c033310bb82..911f2b419d0 100644 --- a/src/HotChocolate/Data/src/Data/DataResources.resx +++ b/src/HotChocolate/Data/src/Data/DataResources.resx @@ -238,6 +238,9 @@ The filter provider {0} {1}was not initialized by a convention. It is only valid to register a provider over a FilterConvention + + The sort provider {0} {1}was not initialized by a convention. It is only valid to register a provider over a SortConvention + Cannot handle the specified type. diff --git a/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs b/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs index 40da78b767c..54ff81383f9 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs @@ -114,7 +114,7 @@ protected internal override void Complete(IConventionContext context) { var extensions = CollectExtensions(context.Services, Definition); - init.Initialize(context); + init.Initialize(context, this); MergeExtensions(context, init, extensions); init.Complete(context); } @@ -339,7 +339,7 @@ private static IReadOnlyList CollectExtensions( return extensions; } - private static void MergeExtensions( + private void MergeExtensions( IConventionContext context, ISortProviderConvention provider, IReadOnlyList extensions) @@ -353,7 +353,7 @@ private static void MergeExtensions( { if (extensions[m] is ISortProviderConvention extensionConvention) { - extensionConvention.Initialize(context); + extensionConvention.Initialize(context, this); extensions[m].Merge(context, providerConvention); extensionConvention.Complete(context); } diff --git a/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProviderConvention.cs b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProviderConvention.cs index 6eab30eeefa..d937e35c4c7 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProviderConvention.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProviderConvention.cs @@ -4,7 +4,7 @@ namespace HotChocolate.Data.Sorting; internal interface ISortProviderConvention { - internal void Initialize(IConventionContext context); + internal void Initialize(IConventionContext context, ISortConvention convention); internal void Complete(IConventionContext context); } diff --git a/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProvider.cs b/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProvider.cs index 1b47a22d893..3e31c14820d 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProvider.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProvider.cs @@ -23,6 +23,7 @@ public abstract class SortProvider private readonly List> _operationHandlers = []; private Action>? _configure; + private ISortConvention? _sortConvention; protected SortProvider() { @@ -63,8 +64,9 @@ protected override SortProviderDefinition CreateDefinition(IConventionContext co return descriptor.CreateDefinition(); } - void ISortProviderConvention.Initialize(IConventionContext context) + void ISortProviderConvention.Initialize(IConventionContext context, ISortConvention convention) { + _sortConvention = convention; base.Initialize(context); } @@ -86,11 +88,20 @@ protected internal override void Complete(IConventionContext context) throw SortProvider_NoOperationHandlersConfigured(this); } + if (_sortConvention is null) + { + throw SortConvention_ProviderHasToBeInitializedByConvention( + GetType(), + context.Scope); + } + var services = new CombinedServiceProvider( new DictionaryServiceProvider( (typeof(ISortProvider), this), (typeof(IConventionContext), context), (typeof(IDescriptorContext), context.DescriptorContext), + (typeof(ISortConvention), _sortConvention), + (typeof(InputParser), context.DescriptorContext.InputParser), (typeof(ITypeInspector), context.DescriptorContext.TypeInspector)), context.Services); diff --git a/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProviderExtensions.cs b/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProviderExtensions.cs index abb57a03e6c..a798726d80f 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProviderExtensions.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProviderExtensions.cs @@ -22,7 +22,7 @@ public SortProviderExtensions(Action> configur throw new ArgumentNullException(nameof(configure)); } - void ISortProviderConvention.Initialize(IConventionContext context) + void ISortProviderConvention.Initialize(IConventionContext context, ISortConvention convention) { base.Initialize(context); } diff --git a/src/HotChocolate/Data/src/Data/ThrowHelper.cs b/src/HotChocolate/Data/src/Data/ThrowHelper.cs index 8c8f4c60c0b..bec66dbd465 100644 --- a/src/HotChocolate/Data/src/Data/ThrowHelper.cs +++ b/src/HotChocolate/Data/src/Data/ThrowHelper.cs @@ -201,6 +201,18 @@ public static SchemaException SortProvider_NoOperationHandlersConfigured( .SetExtension(nameof(sortProvider), sortProvider) .Build()); + public static SchemaException SortConvention_ProviderHasToBeInitializedByConvention( + Type provider, + string? scope) => + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + DataResources.SortConvention_ProviderHasToBeInitializedByConvention, + provider.FullName ?? provider.Name, + scope is null ? "" : "in scope " + scope) + .SetExtension(nameof(scope), scope) + .Build()); + public static SchemaException SortDescriptorContextExtensions_NoConvention(string? scope) => new SchemaException( SchemaErrorBuilder.New() diff --git a/src/HotChocolate/Data/test/Data.Sorting.InMemory.Tests/QueryableSortVisitorObjectTests.cs b/src/HotChocolate/Data/test/Data.Sorting.InMemory.Tests/QueryableSortVisitorObjectTests.cs index 5cca23d7394..6cb3e53ff16 100644 --- a/src/HotChocolate/Data/test/Data.Sorting.InMemory.Tests/QueryableSortVisitorObjectTests.cs +++ b/src/HotChocolate/Data/test/Data.Sorting.InMemory.Tests/QueryableSortVisitorObjectTests.cs @@ -1,4 +1,11 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Reflection; +using HotChocolate.Configuration; using HotChocolate.Execution; +using HotChocolate.Language; +using HotChocolate.Language.Visitors; +using HotChocolate.Types; namespace HotChocolate.Data.Sorting.Expressions; @@ -454,6 +461,74 @@ await Snapshot .MatchAsync(); } + [Fact] + public async Task Create_ObjectComplex_OrderBy_Sum() + { + // arrange + var convention = new SortConvention(x => + { + x.AddDefaults().BindRuntimeType(); + x.AddProviderExtension( + new MockProviderExtension(y => + { + y.AddFieldHandler(); + y.AddFieldHandler(); + y.AddFieldHandler(); + })); + }); + var tester = _cache.CreateSchema( + _barEntities, + convention: convention); + + // act + var res1 = await tester.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument( + """ + { + root(order: [ + { foo: { complex_order_sum: { + fields: ["barShort" "barBool"] + sort: ASC + } } } + { foo: { barString: DESC } }]) { + foo { + barShort + barBool + barString + } + } + } + """) + .Build()); + + var res2 = await tester.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument( + """ + { + root(order: [ + { foo: { complex_order_sum: { + fields: ["barShort" "barBool"] + sort: DESC + } } } + { foo: { barString: ASC } }]) { + foo { + barShort + barBool + barString + } + } + } + """) + .Build()); + + // assert + await Snapshot.Create().Add(res1, "ASC").Add(res2, "13").MatchAsync(); + } + public class Foo { public int Id { get; set; } @@ -504,12 +579,31 @@ public class BarNullable public FooNullable? Foo { get; set; } } - public class BarSortType : SortInputType + public class BarSortType : SortInputType; + + public class BarNullableSortType : SortInputType; + + public class ComplexBarSortType : SortInputType { + protected override void Configure(ISortInputTypeDescriptor descriptor) + { + descriptor.Field(x => x.Foo).Type(); + } } - public class BarNullableSortType : SortInputType + public class ComplexFooSortType : SortInputType { + protected override void Configure(ISortInputTypeDescriptor descriptor) + { + descriptor + .Field("complex_order_sum") + .Type( + new SortInputType(z => + { + z.Field("fields").Type>(); + z.Field("sort").Type(); + })); + } } public enum BarEnum @@ -519,4 +613,101 @@ public enum BarEnum BAZ, QUX, } + + class ComplexOrderSumHandler(ISortConvention convention, InputParser inputParser) + : SortFieldHandler + { + private readonly Dictionary _fieldMap = typeof(Foo) + .GetProperties() + .ToDictionary(convention.GetFieldName); + + public override bool CanHandle( + ITypeCompletionContext context, + ISortInputTypeDefinition typeDefinition, + ISortFieldDefinition fieldDefinition) + { + return fieldDefinition.Name == "complex_order_sum"; + } + + public override bool TryHandleEnter( + QueryableSortContext context, + ISortField field, + ObjectFieldNode node, + [NotNullWhen(true)] out ISyntaxVisitorAction? action) + { + if (context.GetInstance() is QueryableFieldSelector fieldSelector + && field.Type is InputObjectType inputType + && node.Value is ObjectValueNode objectValueNode) + { + var fieldsField = objectValueNode.Fields.First(x => x.Name.Value == "fields"); + if (inputParser.ParseLiteral( + fieldsField.Value, + inputType.Fields["fields"], + typeof(string[])) + is string[] fields) + { + var properties = fields + .Select(x => _fieldMap[x]) + .Select(x => Expression.Property(fieldSelector.Selector, x)); + context.PushInstance( + fieldSelector.WithSelector( + properties + .Select(x => Expression.Convert(x, typeof(int)).Reduce()) + .Aggregate(Expression.Add))); + action = SyntaxVisitor.Continue; + return true; + } + } + action = SyntaxVisitor.Skip; + return false; + } + + public override bool TryHandleLeave( + QueryableSortContext context, + ISortField field, + ObjectFieldNode node, + [NotNullWhen(true)] out ISyntaxVisitorAction? action) + { + context.PopInstance(); + action = SyntaxVisitor.Continue; + return true; + } + } + + class ComplexOrderSumFieldsHandler + : SortFieldHandler + { + public override bool CanHandle( + ITypeCompletionContext context, + ISortInputTypeDefinition typeDefinition, + ISortFieldDefinition fieldDefinition) + { + return fieldDefinition.Name == "fields"; + } + } + + class ComplexOrderSumSortHandler + : SortFieldHandler + { + public override bool CanHandle( + ITypeCompletionContext context, + ISortInputTypeDefinition typeDefinition, + ISortFieldDefinition fieldDefinition) + { + return fieldDefinition.Name == "sort"; + } + + public override bool TryHandleEnter( + QueryableSortContext context, + ISortField field, + ObjectFieldNode node, + [NotNullWhen(true)] out ISyntaxVisitorAction? action) + { + action = SyntaxVisitor.Continue; + return true; + } + } + + class MockProviderExtension(Action> configure) + : SortProviderExtensions(configure); } diff --git a/src/HotChocolate/Data/test/Data.Sorting.InMemory.Tests/SchemaCache.cs b/src/HotChocolate/Data/test/Data.Sorting.InMemory.Tests/SchemaCache.cs index 8aa15e6d312..21ba717122a 100644 --- a/src/HotChocolate/Data/test/Data.Sorting.InMemory.Tests/SchemaCache.cs +++ b/src/HotChocolate/Data/test/Data.Sorting.InMemory.Tests/SchemaCache.cs @@ -9,14 +9,15 @@ public class SchemaCache : SortVisitorTestBase, IDisposable public IRequestExecutor CreateSchema( T?[] entities, - Action? configure = null) + Action? configure = null, + SortConvention? convention = null) where T : class where TType : SortInputType { (Type, Type, T?[] entites) key = (typeof(T), typeof(TType), entities); return _cache.GetOrAdd( key, - _ => base.CreateSchema(entities, configure: configure)); + _ => base.CreateSchema(entities, convention, configure)); } public void Dispose() { } diff --git a/src/HotChocolate/Data/test/Data.Sorting.InMemory.Tests/__snapshots__/QueryableSortVisitorObjectTests.Create_ObjectComplex_OrderBy_Sum.snap b/src/HotChocolate/Data/test/Data.Sorting.InMemory.Tests/__snapshots__/QueryableSortVisitorObjectTests.Create_ObjectComplex_OrderBy_Sum.snap new file mode 100644 index 00000000000..5d3dc0fb3f6 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Sorting.InMemory.Tests/__snapshots__/QueryableSortVisitorObjectTests.Create_ObjectComplex_OrderBy_Sum.snap @@ -0,0 +1,61 @@ +ASC +--------------- +{ + "data": { + "root": [ + { + "foo": { + "barShort": 13, + "barBool": false, + "barString": "testctest" + } + }, + { + "foo": { + "barShort": 12, + "barBool": true, + "barString": "testatest" + } + }, + { + "foo": { + "barShort": 14, + "barBool": true, + "barString": "testbtest" + } + } + ] + } +} +--------------- + +13 +--------------- +{ + "data": { + "root": [ + { + "foo": { + "barShort": 14, + "barBool": true, + "barString": "testbtest" + } + }, + { + "foo": { + "barShort": 12, + "barBool": true, + "barString": "testatest" + } + }, + { + "foo": { + "barShort": 13, + "barBool": false, + "barString": "testctest" + } + } + ] + } +} +--------------- From a83e3df25cc7d7bb5bd4b1b2d11995f86ae21299 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 7 Mar 2025 13:24:51 +0100 Subject: [PATCH 16/64] Abandon Response Formatting Gracefully on Cancellation (#8078) --- .../DefaultHttpResponseFormatter.cs | 17 +++++++++ .../EventStreamResultFormatter.cs | 35 +++++++++++++++---- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResponseFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResponseFormatter.cs index 8c21ac880c9..2f686787a14 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResponseFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResponseFormatter.cs @@ -168,6 +168,23 @@ public async ValueTask FormatAsync( throw ThrowHelper.Formatter_InvalidAcceptMediaType(); } + try + { + await FormatInternalAsync(response, result, proposedStatusCode, format, cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // if the request is aborted we will fail gracefully. + } + } + + private async ValueTask FormatInternalAsync( + HttpResponse response, + IExecutionResult result, + HttpStatusCode? proposedStatusCode, + FormatInfo format, + CancellationToken cancellationToken) + { switch (result) { case IOperationResult operationResult: diff --git a/src/HotChocolate/Core/src/Execution/Serialization/EventStreamResultFormatter.cs b/src/HotChocolate/Core/src/Execution/Serialization/EventStreamResultFormatter.cs index 95feceb8c91..dbd025e03f5 100644 --- a/src/HotChocolate/Core/src/Execution/Serialization/EventStreamResultFormatter.cs +++ b/src/HotChocolate/Core/src/Execution/Serialization/EventStreamResultFormatter.cs @@ -74,8 +74,11 @@ private async ValueTask FormatOperationResultAsync( MessageHelper.WriteNextMessage(_payloadFormatter, operationResult, buffer); MessageHelper.WriteCompleteMessage(buffer); - await outputStream.WriteAsync(buffer.GetInternalBuffer(), 0, buffer.Length, ct).ConfigureAwait(false); - await outputStream.FlushAsync(ct).ConfigureAwait(false); + if (!ct.IsCancellationRequested) + { + await outputStream.WriteAsync(buffer.GetInternalBuffer(), 0, buffer.Length, ct).ConfigureAwait(false); + await outputStream.FlushAsync(ct).ConfigureAwait(false); + } } catch (Exception ex) { @@ -111,8 +114,14 @@ private async ValueTask FormatResultBatchAsync( { buffer ??= new ArrayWriter(); MessageHelper.WriteNextMessage(_payloadFormatter, operationResult, buffer); + writer.Write(buffer.GetWrittenSpan()); - await writer.FlushAsync(ct).ConfigureAwait(false); + + if (!ct.IsCancellationRequested) + { + await writer.FlushAsync(ct).ConfigureAwait(false); + } + keepAlive?.Reset(); buffer.Reset(); } @@ -148,8 +157,11 @@ private async ValueTask FormatResultBatchAsync( buffer?.Dispose(); } - MessageHelper.WriteCompleteMessage(writer); - await writer.FlushAsync(ct).ConfigureAwait(false); + if (!ct.IsCancellationRequested) + { + MessageHelper.WriteCompleteMessage(writer); + await writer.FlushAsync(ct).ConfigureAwait(false); + } } private async ValueTask FormatResponseStreamAsync( @@ -165,8 +177,11 @@ private async ValueTask FormatResponseStreamAsync( await formatter.ProcessAsync(ct).ConfigureAwait(false); } - MessageHelper.WriteCompleteMessage(writer); - await writer.FlushAsync(ct).ConfigureAwait(false); + if (!ct.IsCancellationRequested) + { + MessageHelper.WriteCompleteMessage(writer); + await writer.FlushAsync(ct).ConfigureAwait(false); + } } private sealed class StreamFormatter( @@ -207,6 +222,12 @@ public async Task ProcessAsync(CancellationToken ct) } } } + catch (OperationCanceledException) + { + // if the operation was canceled we do not need to log this + // and will stop gracefully. + return; + } finally { await responseStream.DisposeAsync().ConfigureAwait(false); From d729fc25b889b3155b06fb6e432c7d8d8aff389a Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sun, 9 Mar 2025 17:53:04 +0100 Subject: [PATCH 17/64] Add support for relative cursors. (#8048) --- src/Directory.Packages.props | 8 +- .../Expressions/ExpressionHelpers.cs | 69 +- .../Extensions/PagingQueryableExtensions.cs | 103 +- .../GreenDonut.Data.Primitives.csproj | 4 + .../src/GreenDonut.Data.Primitives/Page.cs | 130 ++- .../PagingArguments.cs | 5 + .../src/GreenDonut.Data/Cursors/Cursor.cs | 25 + .../Cursors/CursorFormatter.cs | 52 +- .../GreenDonut.Data/Cursors/CursorPageInfo.cs | 70 ++ .../GreenDonut.Data/Cursors/CursorParser.cs | 77 +- .../PagingHelperIntegrationTests.cs | 85 +- .../PagingHelperTests.cs | 86 +- ...gHelperTests.BatchPaging_First_5_NET8_0.md | 12 +- ...gHelperTests.BatchPaging_First_5_NET9_0.md | 12 +- ...ngHelperTests.BatchPaging_Last_5_NET8_0.md | 12 +- ...ngHelperTests.BatchPaging_Last_5_NET9_0.md | 12 +- ...BatchPaging_With_Relative_Cursor_NET8_0.md | 168 +++ ...BatchPaging_With_Relative_Cursor_NET9_0.md | 168 +++ ...gingHelperTests.Paging_Empty_PagingArgs.md | 4 +- ...perTests.Paging_Empty_PagingArgs_NET8_0.md | 4 +- ...grationPagingHelperTests.Paging_First_5.md | 4 +- ...gHelperTests.Paging_First_5_After_Id_13.md | 4 +- ...Tests.Paging_First_5_After_Id_13_NET8_0.md | 4 +- ...HelperTests.Paging_First_5_Before_Id_96.md | 4 +- ...ests.Paging_First_5_Before_Id_96_NET8_0.md | 4 +- ...PagingHelperTests.Paging_First_5_NET8_0.md | 4 +- ...egrationPagingHelperTests.Paging_Last_5.md | 6 +- ...nPagingHelperTests.Paging_Last_5_NET8_0.md | 6 +- ...ngHelperTests.Batch_Fetch_First_2_Items.md | 12 +- ...First_2_Items_Second_Page_With_Offset_2.md | 36 + ...tems_Second_Page_With_Offset_Negative_2.md | 9 + .../Cursors/CursorFormatterTests.cs | 25 +- .../Abstractions/GraphQLIgnoreAttribute.cs | 4 +- .../Core/src/Types.Analyzers/Errors.cs | 27 + .../FileBuilders/ConnectionTypeFileBuilder.cs | 160 +++ .../FileBuilders/EdgeTypeFileBuilder.cs | 89 ++ .../InterfaceTypeExtensionFileBuilder.cs | 132 --- .../FileBuilders/InterfaceTypeFileBuilder.cs | 63 ++ .../FileBuilders/ModuleFileBuilder.cs | 1 - .../ObjectTypeExtensionFileBuilder.cs | 289 ----- .../FileBuilders/ObjectTypeFileBuilder.cs | 138 +++ .../FileBuilders/ResolverFileBuilder.cs | 771 ------------- .../FileBuilders/RootTypeFileBuilder.cs | 47 + .../FileBuilders/TypeFileBuilderBase.cs | 1006 +++++++++++++++++ .../Filters/ClassWithBaseClass.cs | 2 + .../Generators/TypeModuleSyntaxGenerator.cs | 56 +- .../Generators/TypesSyntaxGenerator.cs | 162 ++- .../Types.Analyzers/GraphQLServerGenerator.cs | 59 +- .../src/Types.Analyzers/Helpers/CodeWriter.cs | 26 +- .../Types.Analyzers/Helpers/GeneratorUtils.cs | 4 +- .../Helpers/SymbolExtensions.cs | 217 +++- .../HotChocolate.Types.Analyzers.csproj | 10 +- .../Inspectors/ClassBaseClassInspector.cs | 5 +- .../Inspectors/ClassWithBaseClassInspector.cs | 37 + .../Inspectors/ConnectionInspector.cs | 31 + .../Inspectors/ConnectionTypeTransformer.cs | 361 ++++++ .../Inspectors/DataLoaderDefaultsInspector.cs | 9 +- .../Inspectors/DataLoaderInspector.cs | 9 +- .../Inspectors/DataLoaderModuleInspector.cs | 8 +- .../IPostCollectSyntaxTransformer.cs | 15 + .../Inspectors/ISyntaxInspector.cs | 13 +- .../Inspectors/InterfaceTypeInfoInspector.cs | 10 +- .../Inspectors/ModuleInspector.cs | 8 +- ...nfoInspector.cs => ObjectTypeInspector.cs} | 54 +- .../Inspectors/OperationInspector.cs | 5 +- .../Inspectors/RequestMiddlewareInspector.cs | 7 +- .../Inspectors/TypeAttributeInspector.cs | 11 +- .../Core/src/Types.Analyzers/KnownSymbols.cs | 157 +++ .../Models/ConnectionClassInfo.cs | 139 +++ .../Models/ConnectionTypeInfo.cs | 188 +++ .../Models/DataLoaderDefaultsInfo.cs | 2 +- .../Types.Analyzers/Models/DataLoaderInfo.cs | 2 +- .../Models/DataLoaderModuleInfo.cs | 2 +- .../Types.Analyzers/Models/EdgeTypeInfo.cs | 164 +++ .../src/Types.Analyzers/Models/FieldFlags.cs | 10 + .../Types.Analyzers/Models/IOutputTypeInfo.cs | 52 +- .../Models/InterfaceTypeExtensionInfo.cs | 43 - .../Models/InterfaceTypeInfo.cs | 65 ++ .../src/Types.Analyzers/Models/ModuleInfo.cs | 2 +- .../Models/ObjectTypeExtensionInfo.cs | 46 - .../Types.Analyzers/Models/ObjectTypeInfo.cs | 69 ++ .../Types.Analyzers/Models/OperationInfo.cs | 2 +- .../Models/OperationRegistrationInfo.cs | 2 +- .../Models/RegisterDataLoaderInfo.cs | 2 +- .../Models/RequestMiddlewareInfo.cs | 2 +- .../src/Types.Analyzers/Models/Resolver.cs | 75 +- .../Types.Analyzers/Models/ResolverKind.cs | 9 + .../Models/ResolverParameter.cs | 35 +- .../Models/ResolverParameterKind.cs | 3 +- .../Models/RootTypeExtensionInfo.cs | 55 - .../Types.Analyzers/Models/RootTypeInfo.cs | 62 + .../src/Types.Analyzers/Models/SyntaxInfo.cs | 2 +- .../Types.Analyzers/WellKnownAttributes.cs | 3 + .../src/Types.Analyzers/WellKnownTypes.cs | 16 +- .../src/Types.CursorPagination/Connection.cs | 2 +- .../Types.CursorPagination/ConnectionBase.cs | 47 + .../ConnectionPageInfo.cs | 2 +- .../Types.CursorPagination/ConnectionType.cs | 12 +- .../Types.CursorPagination/Connection~1.cs | 14 +- .../Core/src/Types.CursorPagination/Edge.cs | 2 +- .../Extensions/UseConnectionAttribute.cs | 171 +++ .../Extensions/UsePagingAttribute.cs | 22 +- .../src/Types.CursorPagination/IConnection.cs | 23 + .../src/Types.CursorPagination/IEdge~1.cs | 12 + .../src/Types/Configuration/TypeRegistrar.cs | 4 +- .../src/Types/Internal/ExtendedType.Helper.cs | 116 +- .../Types/Internal/ExtendedType.SystemType.cs | 2 +- .../src/Types/Internal/TypeInfoExtensions.cs | 41 +- .../Core/src/Types/SchemaBuilder.Setup.cs | 1 + .../Core/src/Types/SchemaBuilder.cs | 9 +- .../Types/SemanticNonNullTypeInterceptor.cs | 1 + ...StoreGlobalPagingOptionsTypeInterceptor.cs | 21 + .../Descriptors/Contracts/IDescriptor~1.cs | 12 +- .../Contracts/INamedDependencyDescriptor.cs | 6 + .../IEnumTypeNameDependencyDescriptor.cs | 4 + .../IEnumTypeNameDependencyDescriptor~1.cs | 4 + ...InputObjectTypeNameDependencyDescriptor.cs | 4 + ...putObjectTypeNameDependencyDescriptor~1.cs | 4 + .../IInterfaceTypeNameDependencyDescriptor.cs | 4 + ...InterfaceTypeNameDependencyDescriptor~1.cs | 4 + .../IObjectTypeNameDependencyDescriptor.cs | 4 + .../IObjectTypeNameDependencyDescriptor~1.cs | 4 + .../IUnionTypeNameDependencyDescriptor.cs | 4 + .../Conventions/DefaultNamingConventions.cs | 52 + .../Conventions/INamingConventions.cs | 28 + .../Definitions/FieldDefinitionBase.cs | 8 + .../Descriptors/DependencyDescriptorBase~1.cs | 16 + .../Types/Descriptors/DescriptorBase~1.cs | 14 + .../EnumTypeNameDependencyDescriptor.cs | 7 + .../EnumTypeNameDependencyDescriptor~1.cs | 7 + ...InputObjectTypeNameDependencyDescriptor.cs | 7 + ...putObjectTypeNameDependencyDescriptor~1.cs | 7 + .../InterfaceTypeNameDependencyDescriptor.cs | 7 + ...InterfaceTypeNameDependencyDescriptor~1.cs | 7 + .../ObjectTypeNameDependencyDescriptor.cs | 7 + .../ObjectTypeNameDependencyDescriptor~1.cs | 7 + .../UnionTypeNameDependencyDescriptor.cs | 7 + .../NamedDependencyDescriptor~1.cs | 33 +- .../src/Types/Types/Helpers/TypeNameHelper.cs | 34 + .../Core/src/Types/Types/Pagination/IPage.cs | 10 +- .../Pagination/IPageTotalCountProvider.cs | 8 + .../Types/Types/Pagination/PagingDefaults.cs | 15 + .../Types/Types/Pagination/PagingHelper.cs | 14 +- .../Types/Types/Pagination/PagingOptions.cs | 9 +- .../src/Types/Utilities/ReflectionUtils.cs | 3 + .../Types.Analyzers.Tests/ObjectTypeTests.cs | 1 + .../Types.Analyzers.Tests/OperationTests.cs | 30 +- .../test/Types.Analyzers.Tests/PagingTests.cs | 432 +++++++ .../test/Types.Analyzers.Tests/TestHelper.cs | 3 +- .../Types/ChapterNode2.cs | 2 +- ...eSource_BatchDataLoader_MatchesSnapshot.md | 142 +-- ...OperationTests.Partial_Static_QueryType.md | 163 +-- .../OperationTests.Root_NodeResolver.md | 116 ++ ...tionTests.Root_Projection_Single_Entity.md | 233 +--- ...erateSource_ConnectionT_MatchesSnapshot.md | 129 +++ ...Source_CustomConnection_MatchesSnapshot.md | 321 ++++++ ...onnection_No_Duplicates_MatchesSnapshot.md | 481 ++++++++ ...nnection_ConnectionName_MatchesSnapshot.md | 325 ++++++ ...ction_IncludeTotalCount_MatchesSnapshot.md | 321 ++++++ ...GenericCustomConnection_MatchesSnapshot.md | 491 ++++++++ ...tion_WithConnectionName_MatchesSnapshot.md | 677 +++++++++++ ...esolverTests.Ensure_Entity_Becomes_Node.md | 287 ++--- ...y_Becomes_Node_With_Query_Node_Resolver.md | 237 ++-- ...WithGlobalStateArgument_MatchesSnapshot.md | 134 +-- ...alStateSetStateArgument_MatchesSnapshot.md | 134 +-- ...rWithLocalStateArgument_MatchesSnapshot.md | 134 +-- ...alStateSetStateArgument_MatchesSnapshot.md | 134 +-- ...WithScopedStateArgument_MatchesSnapshot.md | 134 +-- ...edStateSetStateArgument_MatchesSnapshot.md | 134 +-- .../ResolverTests.Inject_QueryContext.md | 149 +-- ...urce_TypeModuleOrdering_MatchesSnapshot.md | 162 ++- .../CustomConnectionTest.cs | 56 + ...nnections_Work_With_Legacy_Middleware.snap | 13 + .../src/CostAnalysis/CostTypeInterceptor.cs | 12 +- .../EfQueryableCursorPagingHandler.cs | 6 +- ...ests.Paging_Fetch_First_2_Items_Between.md | 4 +- ...Tests.Paging_Fetch_Last_2_Items_Between.md | 4 +- ...rst_10_With_Default_Sorting_HasNextPage.md | 2 +- ...10_With_Default_Sorting_HasPreviousPage.md | 2 +- ...ests.Paging_Next_2_With_Default_Sorting.md | 2 +- ...onTests.Paging_Next_2_With_User_Sorting.md | 2 +- ...rationTests.Paging_With_Default_Sorting.md | 2 +- ...ing_With_Default_Sorting_And_TotalCount.md | 2 +- ...tegrationTests.Paging_With_User_Sorting.md | 2 +- .../HotChocolate.Data.PostgreSQL.Tests.csproj | 2 + .../Data.PostgreSQL.Tests/IntegrationTests.cs | 60 +- .../Data.PostgreSQL.Tests/Models/Product.cs | 13 +- .../test/Data.PostgreSQL.Tests/TestHelpers.cs | 1 - .../Types/Brands/BrandNode.cs | 11 +- .../Types/Brands/BrandQueries.cs | 11 +- .../Types/Brands/CatalogConnection.cs | 80 ++ .../Types/Brands/CatalogEdge.cs | 23 + .../Types/Products/ProductConnection.cs | 98 ++ .../Types/Products/ProductNode.cs | 11 +- .../Types/Products/ProductQueries.cs | 26 +- .../Types/Products/ProductSortInputType.cs | 6 + .../Types/Products/ProductsEdge.cs | 23 + .../IntegrationTests.CreateSchema.graphql | 73 +- ...Tests.Query_Products_Exclude_TotalCount.md | 2 +- ...ry_Products_Exclude_TotalCount__net_8_0.md | 2 +- ...nTests.Query_Products_First_2_And_Brand.md | 4 +- ...ery_Products_First_2_And_Brand__net_8_0.md | 3 +- ...uery_Products_First_2_With_4_EndCursors.md | 54 + ...oducts_First_2_With_4_EndCursors_Skip_4.md | 56 + ...rst_2_With_4_EndCursors_Skip_4__net_8_0.md | 56 + ...ucts_First_2_With_4_EndCursors__net_8_0.md | 53 + ...Tests.Query_Products_Include_TotalCount.md | 2 +- ...ry_Products_Include_TotalCount__net_8_0.md | 2 +- ...onPagingHelperTests.BatchPaging_First_5.md | 12 +- ...gHelperTests.BatchPaging_First_5_NET9_0.md | 12 +- ...ionPagingHelperTests.BatchPaging_Last_5.md | 12 +- ...ngHelperTests.BatchPaging_Last_5_NET9_0.md | 12 +- ....Ensure_Nullable_Connections_Dont_Throw.md | 8 +- ...nsure_Nullable_Connections_Dont_Throw_2.md | 8 +- ...ullable_Connections_Dont_Throw_2_NET9_0.md | 8 +- ..._Nullable_Connections_Dont_Throw_NET9_0.md | 8 +- ...grationPagingHelperTests.GetDefaultPage.md | 4 +- ...rationPagingHelperTests.GetDefaultPage2.md | 4 +- ...ingHelperTests.GetDefaultPage_With_Deep.md | 24 +- ...sts.GetDefaultPage_With_Deep_SecondPage.md | 8 +- ...elperTests.GetDefaultPage_With_Nullable.md | 24 +- ...s.GetDefaultPage_With_Nullable_Fallback.md | 24 +- ...tPage_With_Nullable_Fallback_SecondPage.md | 8 +- ...GetDefaultPage_With_Nullable_SecondPage.md | 8 +- ...gHelperTests.GetSecondPage_With_2_Items.md | 4 +- ...erTests.Map_Page_To_Connection_With_Dto.md | 4 +- ...Tests.Map_Page_To_Connection_With_Dto_2.md | 4 +- ...PagingHelperTests.Nested_Paging_First_2.md | 12 +- ...elperTests.Nested_Paging_First_2_NET9_0.md | 12 +- ....Nested_Paging_First_2_With_Projections.md | 12 +- ..._Paging_First_2_With_Projections_NET9_0.md | 12 +- ...gingHelperTests.Paging_Empty_PagingArgs.md | 4 +- ...grationPagingHelperTests.Paging_First_5.md | 4 +- ...gHelperTests.Paging_First_5_After_Id_13.md | 4 +- ...HelperTests.Paging_First_5_Before_Id_96.md | 4 +- ...egrationPagingHelperTests.Paging_Last_5.md | 4 +- 236 files changed, 10034 insertions(+), 3659 deletions(-) create mode 100644 src/GreenDonut/src/GreenDonut.Data/Cursors/Cursor.cs create mode 100644 src/GreenDonut/src/GreenDonut.Data/Cursors/CursorPageInfo.cs create mode 100644 src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET8_0.md create mode 100644 src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET9_0.md create mode 100644 src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_With_Offset_2.md create mode 100644 src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_With_Offset_Negative_2.md create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ConnectionTypeFileBuilder.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/EdgeTypeFileBuilder.cs delete mode 100644 src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/InterfaceTypeExtensionFileBuilder.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/InterfaceTypeFileBuilder.cs delete mode 100644 src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ObjectTypeExtensionFileBuilder.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ObjectTypeFileBuilder.cs delete mode 100644 src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ResolverFileBuilder.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/RootTypeFileBuilder.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ClassWithBaseClassInspector.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionInspector.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Inspectors/IPostCollectSyntaxTransformer.cs rename src/HotChocolate/Core/src/Types.Analyzers/Inspectors/{ObjectTypeExtensionInfoInspector.cs => ObjectTypeInspector.cs} (88%) create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionClassInfo.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionTypeInfo.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Models/EdgeTypeInfo.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Models/FieldFlags.cs delete mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Models/InterfaceTypeExtensionInfo.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Models/InterfaceTypeInfo.cs delete mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Models/ObjectTypeExtensionInfo.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Models/ObjectTypeInfo.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverKind.cs delete mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Models/RootTypeExtensionInfo.cs create mode 100644 src/HotChocolate/Core/src/Types.Analyzers/Models/RootTypeInfo.cs create mode 100644 src/HotChocolate/Core/src/Types.CursorPagination/ConnectionBase.cs create mode 100644 src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UseConnectionAttribute.cs create mode 100644 src/HotChocolate/Core/src/Types.CursorPagination/IConnection.cs create mode 100644 src/HotChocolate/Core/src/Types.CursorPagination/IEdge~1.cs create mode 100644 src/HotChocolate/Core/src/Types/StoreGlobalPagingOptionsTypeInterceptor.cs create mode 100644 src/HotChocolate/Core/src/Types/Types/Pagination/IPageTotalCountProvider.cs create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/PagingTests.cs create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Root_NodeResolver.md create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionT_MatchesSnapshot.md create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_MatchesSnapshot.md create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_No_Duplicates_MatchesSnapshot.md create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_ConnectionName_MatchesSnapshot.md create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_IncludeTotalCount_MatchesSnapshot.md create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_MatchesSnapshot.md create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_WithConnectionName_MatchesSnapshot.md create mode 100644 src/HotChocolate/Core/test/Types.CursorPagination.Tests/CustomConnectionTest.cs create mode 100644 src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/CustomConnectionTest.Ensure_That_Custom_Connections_Work_With_Legacy_Middleware.snap create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/CatalogConnection.cs create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/CatalogEdge.cs create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductConnection.cs create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductsEdge.cs create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors.md create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4.md create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4__net_8_0.md create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors__net_8_0.md diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 144732b7b7f..50b67393645 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -26,9 +26,9 @@ - - - + + + @@ -93,6 +93,7 @@ + @@ -117,6 +118,7 @@ + diff --git a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs index 671d967e10b..dce7792f212 100644 --- a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs +++ b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs @@ -40,9 +40,9 @@ internal static class ExpressionHelpers /// /// If the number of keys does not match the number of values. /// - public static Expression> BuildWhereExpression( + public static (Expression> WhereExpression, int Offset, bool ReverseOrder) BuildWhereExpression( ReadOnlySpan keys, - ReadOnlySpan cursor, + Cursor cursor, bool forward) { if (keys.Length == 0) @@ -50,15 +50,15 @@ public static Expression> BuildWhereExpression( throw new ArgumentException("At least one key must be specified.", nameof(keys)); } - if (keys.Length != cursor.Length) + if (keys.Length != cursor.Values.Length) { - throw new ArgumentException("The number of keys must match the number of values.", nameof(cursor)); + throw new ArgumentException("The number of keys must match the number of values.", nameof(cursor.Values)); } - var cursorExpr = new Expression[cursor.Length]; - for (var i = 0; i < cursor.Length; i++) + var cursorExpr = new Expression[cursor.Values.Length]; + for (var i = 0; i < cursor.Values.Length; i++) { - cursorExpr[i] = CreateParameter(cursor[i], keys[i].Expression.ReturnType); + cursorExpr[i] = CreateParameter(cursor.Values[i], keys[i].Expression.ReturnType); } var handled = new List(); @@ -118,7 +118,10 @@ public static Expression> BuildWhereExpression( handled.Add(key); } - return Expression.Lambda>(expression!, parameter); + var offset = cursor.Offset ?? 0; + var reverseOrder = offset < 0; // Reverse order if offset is negative + + return (Expression.Lambda>(expression!, parameter), Math.Abs(offset), reverseOrder); } /// @@ -153,7 +156,7 @@ public static Expression> BuildWhereExpression( /// If the number of keys is less than one or /// the number of order expressions does not match the number of order methods. /// - public static Expression, Group>> BuildBatchSelectExpression( + public static (Expression, Group>> SelectExpression, bool ReverseOrder) BuildBatchSelectExpression( PagingArguments arguments, ReadOnlySpan keys, ReadOnlySpan orderExpressions, @@ -181,14 +184,8 @@ public static Expression, Group>> BuildBatchSelec for (var i = 0; i < orderExpressions.Length; i++) { - var methodName = orderMethods[i]; + var methodName = forward ? orderMethods[i] : ReverseOrder(orderMethods[i]); var orderExpression = orderExpressions[i]; - - if (!forward) - { - methodName = ReverseOrder(methodName); - } - var delegateType = typeof(Func<,>).MakeGenericType(typeof(TV), orderExpression.Body.Type); var typedOrderExpression = Expression.Lambda(delegateType, orderExpression.Body, orderExpression.Parameters); @@ -200,16 +197,44 @@ public static Expression, Group>> BuildBatchSelec typedOrderExpression); } + var reverseOrder = false; + var offset = 0; + if (arguments.After is not null) { var cursor = CursorParser.Parse(arguments.After, keys); - source = BuildBatchWhereExpression(source, keys, cursor, forward); + var (whereExpr, cursorOffset, reverse) = BuildWhereExpression(keys, cursor, forward: true); + source = Expression.Call(typeof(Enumerable), "Where", [typeof(TV)], source, whereExpr); + offset = cursorOffset; + reverseOrder = reverse; } if (arguments.Before is not null) { var cursor = CursorParser.Parse(arguments.Before, keys); - source = BuildBatchWhereExpression(source, keys, cursor, forward); + var (whereExpr, cursorOffset, reverse) = BuildWhereExpression(keys, cursor, forward: false); + source = Expression.Call(typeof(Enumerable), "Where", [typeof(TV)], source, whereExpr); + offset = cursorOffset; + reverseOrder = reverse; + } + + if (reverseOrder) + { + source = Expression.Call( + typeof(Enumerable), + "Reverse", + [typeof(TV)], + source); + } + + if (offset > 0) + { + source = Expression.Call( + typeof(Enumerable), + "Skip", + [typeof(TV)], + source, + Expression.Constant(offset)); } if (arguments.First is not null) @@ -248,7 +273,7 @@ public static Expression, Group>> BuildBatchSelec }; var createGroup = Expression.MemberInit(Expression.New(groupType), bindings); - return Expression.Lambda, Group>>(createGroup, group); + return (Expression.Lambda, Group>>(createGroup, group), reverseOrder); static string ReverseOrder(string method) => method switch @@ -261,12 +286,10 @@ static string ReverseOrder(string method) }; static MethodInfo GetEnumerableMethod(string methodName, Type elementType, LambdaExpression keySelector) - { - return typeof(Enumerable) + => typeof(Enumerable) .GetMethods(BindingFlags.Static | BindingFlags.Public) .First(m => m.Name == methodName && m.GetParameters().Length == 2) .MakeGenericMethod(elementType, keySelector.Body.Type); - } } private static MethodCallExpression BuildBatchWhereExpression( @@ -355,7 +378,7 @@ private static MethodCallExpression BuildBatchWhereExpression( /// public static OrderRewriterResult ExtractAndRemoveOrder(Expression expression) { - if(expression is null) + if (expression is null) { throw new ArgumentNullException(nameof(expression)); } diff --git a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs index 18b57b68fd5..bad4d368ed6 100644 --- a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs +++ b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs @@ -75,10 +75,7 @@ public static async ValueTask> ToPageAsync( bool includeTotalCount, CancellationToken cancellationToken = default) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } + ArgumentNullException.ThrowIfNull(source); source = QueryHelpers.EnsureOrderPropsAreSelected(source); @@ -98,36 +95,79 @@ public static async ValueTask> ToPageAsync( nameof(arguments)); } + if (arguments.EnableRelativeCursors + && string.IsNullOrEmpty(arguments.After) + && string.IsNullOrEmpty(arguments.Before)) + { + includeTotalCount = true; + } + var originalQuery = source; var forward = arguments.Last is null; var requestedCount = int.MaxValue; + var reverseOrder = false; + var offset = 0; + int? totalCount = null; if (arguments.After is not null) { var cursor = CursorParser.Parse(arguments.After, keys); - source = source.Where(BuildWhereExpression(keys, cursor, true)); + var (whereExpr, cursorOffset, reverse) = BuildWhereExpression(keys, cursor, true); + + source = source.Where(whereExpr); + offset = cursorOffset; + reverseOrder = reverse; + + if (!includeTotalCount) + { + totalCount ??= cursor.TotalCount; + } } if (arguments.Before is not null) { var cursor = CursorParser.Parse(arguments.Before, keys); - source = source.Where(BuildWhereExpression(keys, cursor, false)); + var (whereExpr, cursorOffset, reverse) = BuildWhereExpression(keys, cursor, false); + + source = source.Where(whereExpr); + offset = cursorOffset; + reverseOrder = reverse; + + if (!includeTotalCount) + { + totalCount ??= cursor.TotalCount; + } + } + + // Reverse order if offset is negative + if (reverseOrder) + { + source = source.Reverse(); } if (arguments.First is not null) { + if (offset > 0) + { + source = source.Skip(offset * arguments.First.Value); + } + source = source.Take(arguments.First.Value + 1); requestedCount = arguments.First.Value; } if (arguments.Last is not null) { + if (offset > 0) + { + source = source.Skip(offset * arguments.Last.Value); + } + source = source.Reverse().Take(arguments.Last.Value + 1); requestedCount = arguments.Last.Value; } var builder = ImmutableArray.CreateBuilder(); - int? totalCount = null; var fetchCount = 0; if (includeTotalCount) @@ -142,12 +182,12 @@ public static async ValueTask> ToPageAsync( totalCount ??= item.TotalCount; fetchCount++; + builder.Add(item.Item); + if (fetchCount > requestedCount) { break; } - - builder.Add(item.Item); } } else @@ -159,12 +199,12 @@ public static async ValueTask> ToPageAsync( { fetchCount++; + builder.Add(item); + if (fetchCount > requestedCount) { break; } - - builder.Add(item); } } @@ -173,11 +213,23 @@ public static async ValueTask> ToPageAsync( return Page.Empty; } - if (!forward) + if (!forward ^ reverseOrder) { builder.Reverse(); } + if (builder.Count > requestedCount) + { + if (!forward ^ reverseOrder) + { + builder.RemoveAt(0); + } + else + { + builder.RemoveAt(requestedCount); + } + } + return CreatePage(builder.ToImmutable(), arguments, keys, fetchCount, totalCount); } @@ -367,6 +419,13 @@ public static async ValueTask>> ToBatchPageAsync? counts = null; if (includeTotalCount) { @@ -383,7 +442,7 @@ public static async ValueTask>> ToBatchPageAsync( arguments, keys, @@ -405,6 +464,11 @@ public static async ValueTask>> ToBatchPageAsync.Empty); @@ -520,6 +584,17 @@ private static Page CreatePage( hasNext = true; } + if (arguments.EnableRelativeCursors && totalCount is not null) + { + return new Page( + items, + hasNext, + hasPrevious, + (item, o, p, c) => CursorFormatter.Format(item, keys, new CursorPageInfo(o, p, c)), + 0, + totalCount.Value); + } + return new Page( items, hasNext, @@ -532,7 +607,7 @@ private static CursorKey[] ParseDataSetKeys(IQueryable source) { var parser = new CursorKeyParser(); parser.Visit(source.Expression); - return parser.Keys.ToArray(); + return [.. parser.Keys]; } private sealed class InterceptorHolder diff --git a/src/GreenDonut/src/GreenDonut.Data.Primitives/GreenDonut.Data.Primitives.csproj b/src/GreenDonut/src/GreenDonut.Data.Primitives/GreenDonut.Data.Primitives.csproj index 346b4c28d6c..d8a7dfc98bc 100644 --- a/src/GreenDonut/src/GreenDonut.Data.Primitives/GreenDonut.Data.Primitives.csproj +++ b/src/GreenDonut/src/GreenDonut.Data.Primitives/GreenDonut.Data.Primitives.csproj @@ -7,4 +7,8 @@ This package contains the basic building blocks of the DataLoader linq query integration. + + + + diff --git a/src/GreenDonut/src/GreenDonut.Data.Primitives/Page.cs b/src/GreenDonut/src/GreenDonut.Data.Primitives/Page.cs index 0ce57e4c5bc..ce9ba0951ac 100644 --- a/src/GreenDonut/src/GreenDonut.Data.Primitives/Page.cs +++ b/src/GreenDonut/src/GreenDonut.Data.Primitives/Page.cs @@ -6,62 +6,122 @@ namespace GreenDonut.Data; /// /// Represents a page of a result set. /// -/// -/// The items of the page. -/// -/// -/// Defines if there is a next page. -/// -/// -/// Defines if there is a previous page. -/// -/// -/// A delegate to create a cursor for an item. -/// -/// -/// The total count of items in the dataset. -/// /// /// The type of the items. /// -public sealed class Page( - ImmutableArray items, - bool hasNextPage, - bool hasPreviousPage, - Func createCursor, - int? totalCount = null) - : IEnumerable +public sealed class Page : IEnumerable { + private readonly ImmutableArray _items; + private readonly bool _hasNextPage; + private readonly bool _hasPreviousPage; + private readonly Func _createCursor; + private readonly int? _totalCount; + private readonly int? _index; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The items of the page. + /// + /// + /// Defines if there is a next page. + /// + /// + /// Defines if there is a previous page. + /// + /// + /// A delegate to create a cursor for an item. + /// + /// + /// The total count of items in the dataset. + /// + public Page( + ImmutableArray items, + bool hasNextPage, + bool hasPreviousPage, + Func createCursor, + int? totalCount = null) + { + _items = items; + _hasNextPage = hasNextPage; + _hasPreviousPage = hasPreviousPage; + _createCursor = (item, _, _, _) => createCursor(item); + _totalCount = totalCount; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The items of the page. + /// + /// + /// Defines if there is a next page. + /// + /// + /// Defines if there is a previous page. + /// + /// + /// A delegate to create a cursor for an item. + /// + /// + /// The total count of items in the dataset. + /// + /// + /// The index number of this page. + /// + internal Page( + ImmutableArray items, + bool hasNextPage, + bool hasPreviousPage, + Func createCursor, + int index, + int totalCount) + { + _items = items; + _hasNextPage = hasNextPage; + _hasPreviousPage = hasPreviousPage; + _createCursor = createCursor; + _totalCount = totalCount; + _index = index; + } + /// /// Gets the items of this page. /// - public ImmutableArray Items => items; + public ImmutableArray Items => _items; /// /// Gets the first item of this page. /// - public T? First => items.Length > 0 ? items[0] : default; + public T? First => _items.Length > 0 ? _items[0] : default; /// /// Gets the last item of this page. /// - public T? Last => items.Length > 0 ? items[^1] : default; + public T? Last => _items.Length > 0 ? _items[^1] : default; /// /// Defines if there is a next page. /// - public bool HasNextPage => hasNextPage; + public bool HasNextPage => _hasNextPage; /// /// Defines if there is a previous page. /// - public bool HasPreviousPage => hasPreviousPage; + public bool HasPreviousPage => _hasPreviousPage; /// /// Gets the total count of items in the dataset. /// This value can be null if the total count is unknown. /// - public int? TotalCount => totalCount; + public int? TotalCount => _totalCount; + + /// + /// Gets the index number of this page. + /// + public int? Index => _index; /// /// Creates a cursor for an item of this page. @@ -72,7 +132,17 @@ public sealed class Page( /// /// Returns a cursor for the item. /// - public string CreateCursor(T item) => createCursor(item); + public string CreateCursor(T item) => _createCursor(item, 0, 0, 0); + + public string CreateCursor(T item, int offset) + { + if(_index is null || _totalCount is null) + { + throw new InvalidOperationException("This page does not allow relative cursors."); + } + + return _createCursor(item, offset, _index ?? 0, _totalCount ?? 0); + } /// /// An empty page. @@ -84,7 +154,7 @@ public sealed class Page( /// /// public IEnumerator GetEnumerator() - => ((IEnumerable)items).GetEnumerator(); + => ((IEnumerable)_items).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/src/GreenDonut/src/GreenDonut.Data.Primitives/PagingArguments.cs b/src/GreenDonut/src/GreenDonut.Data.Primitives/PagingArguments.cs index 09675044e93..e150e81e6fa 100644 --- a/src/GreenDonut/src/GreenDonut.Data.Primitives/PagingArguments.cs +++ b/src/GreenDonut/src/GreenDonut.Data.Primitives/PagingArguments.cs @@ -62,6 +62,11 @@ public PagingArguments( /// public bool IncludeTotalCount { get; init; } + /// + /// Defines if relative cursors are allowed. + /// + public bool EnableRelativeCursors { get; init; } + /// /// Deconstructs the paging arguments into its components. /// diff --git a/src/GreenDonut/src/GreenDonut.Data/Cursors/Cursor.cs b/src/GreenDonut/src/GreenDonut.Data/Cursors/Cursor.cs new file mode 100644 index 00000000000..83ce30b8200 --- /dev/null +++ b/src/GreenDonut/src/GreenDonut.Data/Cursors/Cursor.cs @@ -0,0 +1,25 @@ +using System.Collections.Immutable; + +namespace GreenDonut.Data.Cursors; + +/// +/// Represents a pagination cursor used to indicate a specific position within a dataset. +/// +/// +/// The cursor values corresponding to the fields used for pagination, typically keys. +/// +/// +/// An optional offset to skip additional items from the current cursor position. +/// Positive values skip forward, negative values skip backward. +/// +/// +/// The zero-based index of the current page within the paginated dataset. +/// +/// +/// The total number of items in the dataset, if known. Can be null if not available. +/// +public record Cursor( + ImmutableArray Values, + int? Offset = null, + int? PageIndex = null, + int? TotalCount = null); diff --git a/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorFormatter.cs b/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorFormatter.cs index f7b9e03f859..666e368ddbd 100644 --- a/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorFormatter.cs +++ b/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorFormatter.cs @@ -18,6 +18,9 @@ public static class CursorFormatter /// /// The keys that make up the cursor. /// + /// + /// The page information for this cursor. + /// /// /// The type of the entity. /// @@ -33,7 +36,7 @@ public static class CursorFormatter /// /// If a key cannot be formatted. /// - public static string Format(T entity, CursorKey[] keys) + public static string Format(T entity, CursorKey[] keys, CursorPageInfo pageInfo = default) { if (entity == null) { @@ -52,19 +55,30 @@ public static string Format(T entity, CursorKey[] keys) Span span = stackalloc byte[256]; byte[]? poolArray = null; - var totalWritten = 0; var first = true; + if (pageInfo.TotalCount == 0) + { + span[totalWritten++] = (byte)'{'; + span[totalWritten++] = (byte)'}'; + } + else + { + WriteCharacter('{', ref span, ref poolArray, ref totalWritten); + WriteNumber(pageInfo.Offset, ref span, ref poolArray, ref totalWritten); + WriteCharacter('|', ref span, ref poolArray, ref totalWritten); + WriteNumber(pageInfo.PageIndex, ref span, ref poolArray, ref totalWritten); + WriteCharacter('|', ref span, ref poolArray, ref totalWritten); + WriteNumber(pageInfo.TotalCount, ref span, ref poolArray, ref totalWritten); + WriteCharacter('}', ref span, ref poolArray, ref totalWritten); + } + foreach (var key in keys) { if (!first) { - if (totalWritten + 1 > span.Length) - { - ExpandBuffer(ref poolArray, ref span, totalWritten, 1); - } - span[totalWritten++] = (byte)':'; + WriteCharacter(':', ref span, ref poolArray, ref totalWritten); } else { @@ -103,6 +117,30 @@ public static string Format(T entity, CursorKey[] keys) } return result; + + static void WriteCharacter(char c, ref Span span, ref byte[]? poolArray, ref int totalWritten) + { + if (totalWritten + 1 > span.Length) + { + ExpandBuffer(ref poolArray, ref span, totalWritten, 1); + } + span[totalWritten++] = (byte)c; + } + + static void WriteNumber(int number, ref Span span, ref byte[]? poolArray, ref int totalWritten) + { + if (!Utf8Formatter.TryFormat(number, span[totalWritten..], out var written)) + { + ExpandBuffer(ref poolArray, ref span, totalWritten, span.Length); + + if (!Utf8Formatter.TryFormat(number, span[totalWritten..], out written)) + { + throw new InvalidOperationException(); + } + } + + totalWritten += written; + } } private static void ExpandBuffer( diff --git a/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorPageInfo.cs b/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorPageInfo.cs new file mode 100644 index 00000000000..1d4f7e349d9 --- /dev/null +++ b/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorPageInfo.cs @@ -0,0 +1,70 @@ +namespace GreenDonut.Data.Cursors; + +/// +/// Represents pagination information parsed from a cursor, including offset, current page, and total count. +/// +public readonly ref struct CursorPageInfo +{ + /// + /// Initializes a new instance of the struct. + /// + /// Offset indicating the number of items/pages skipped. + /// The zero-based index of the current page. + /// Total number of items available in the dataset. + /// + /// Thrown if an offset greater than zero is specified with a totalCount of zero. + /// + public CursorPageInfo(int offset, int pageIndex, int totalCount) + { + ArgumentOutOfRangeException.ThrowIfNegative(pageIndex); + ArgumentOutOfRangeException.ThrowIfNegative(totalCount); + + if (offset > 0 && totalCount == 0) + { + throw new ArgumentException( + "The total count must be greater than zero if an offset is set.", + nameof(totalCount)); + } + + Offset = offset; + PageIndex = pageIndex; + TotalCount = totalCount; + } + + /// + /// Gets the offset relative to the current cursor position. + /// Positive for forward offset, negative for backward offset, zero if no offset. + /// + public int Offset { get; } + + /// + /// Gets the zero-based index of the current page within the paginated dataset. + /// + public int PageIndex { get; } + + /// + /// Gets the total count of items in the entire dataset, if known. + /// + public int TotalCount { get; } + + /// + /// Deconstructs the into individual components. + /// + /// The offset, or null if no valid data. + /// The page number, or null if no valid data. + /// The total count, or null if no valid data. + public void Deconstruct(out int? offset, out int? pageIndex, out int? totalCount) + { + if (TotalCount == 0) + { + offset = null; + pageIndex = null; + totalCount = null; + return; + } + + offset = Offset; + pageIndex = PageIndex; + totalCount = TotalCount; + } +} diff --git a/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorParser.cs b/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorParser.cs index 0e172b83e2b..b5ce7d667ae 100644 --- a/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorParser.cs +++ b/src/GreenDonut/src/GreenDonut.Data/Cursors/CursorParser.cs @@ -1,5 +1,7 @@ using System.Buffers; using System.Buffers.Text; +using System.Collections.Immutable; +using System.Dynamic; using System.Text; namespace GreenDonut.Data.Cursors; @@ -30,7 +32,7 @@ public static class CursorParser /// /// If the number of keys is zero. /// - public static object?[] Parse(string cursor, ReadOnlySpan keys) + public static Cursor Parse(string cursor, ReadOnlySpan keys) { if (cursor == null) { @@ -56,7 +58,9 @@ public static class CursorParser var start = 0; var end = 0; var parsedCursor = new object?[keys.Length]; - for(var current = 0; current < bufferSpan.Length; current++) + var (offset, page, totalCount) = ParsePageInfo(ref bufferSpan); + + for (var current = 0; current < bufferSpan.Length; current++) { var code = bufferSpan[current]; end++; @@ -83,7 +87,7 @@ public static class CursorParser } ArrayPool.Shared.Return(buffer); - return parsedCursor; + return new Cursor(parsedCursor.ToImmutableArray(), offset, page, totalCount); static bool CanParse(byte code, int pos, ReadOnlySpan buffer) { @@ -100,7 +104,7 @@ static bool CanParse(byte code, int pos, ReadOnlySpan buffer) } } - if(pos == buffer.Length - 1) + if (pos == buffer.Length - 1) { return true; } @@ -108,4 +112,69 @@ static bool CanParse(byte code, int pos, ReadOnlySpan buffer) return false; } } + + private static CursorPageInfo ParsePageInfo(ref Span span) + { + const byte Open = (byte)'{'; + const byte Close = (byte)'}'; + const byte Separator = (byte)'|'; + + // Validate input: must start with `{` and end with `}` + if (span.Length < 2 || span[0] != Open) + { + return default; + } + + // the page info is empty + if (span[0] == Open && span[1] == Close) + { + span = span[2..]; + return default; + } + + // Advance span beyond opening `{` + span = span[1..]; + + var separatorIndex = ExpectSeparator(span, Separator); + var part = span[..separatorIndex]; + ParseNumber(part, out var offset, out var consumed); + var start = separatorIndex + 1; + + separatorIndex = ExpectSeparator(span[start..], Separator); + part = span.Slice(start, separatorIndex); + ParseNumber(part, out var page, out consumed); + start += separatorIndex + 1; + + separatorIndex = ExpectSeparator(span[start..], Close); + part = span.Slice(start, separatorIndex); + ParseNumber(part, out var totalCount, out consumed); + start += separatorIndex + 1; + + // Advance span beyond closing `}` + span = span[start..]; + + return new CursorPageInfo(offset, page, totalCount); + + static void ParseNumber(ReadOnlySpan span, out int value, out int consumed) + { + if (!Utf8Parser.TryParse(span, out value, out consumed)) + { + throw new InvalidOperationException( + "The cursor page info could not be parsed."); + } + } + + static int ExpectSeparator(ReadOnlySpan span, byte separator) + { + var index = span.IndexOf(separator); + + if(index == -1) + { + throw new InvalidOperationException( + "The cursor page info could not be parsed."); + } + + return index; + } + } } diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperIntegrationTests.cs b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperIntegrationTests.cs index 751cb0037c0..e4a8e317774 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperIntegrationTests.cs +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperIntegrationTests.cs @@ -133,8 +133,10 @@ await CreateSnapshot() result.HasNextPage, result.HasPreviousPage, First = result.First?.Id, + FirstName = result.First?.Name, FirstCursor = result.First is not null ? result.CreateCursor(result.First) : null, Last = result.Last?.Id, + LastName = result.Last?.Name, LastCursor = result.Last is not null ? result.CreateCursor(result.Last) : null }) .Add(result.Items) @@ -259,6 +261,47 @@ public async Task BatchPaging_Last_5() snapshot.MatchMarkdownSnapshot(); } + [Fact] + public async Task BatchPaging_With_Relative_Cursor() + { + // Arrange +#if NET8_0 + var snapshot = CreateSnapshot(); +#else + var snapshot = Snapshot.Create("NET9_0"); +#endif + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + using var capture = new CapturePagingQueryInterceptor(); + + await using var context = new CatalogContext(connectionString); + + // Act + var pagingArgs = new PagingArguments { First = 2, EnableRelativeCursors = true }; + + var results = await context.Products + .Where(t => t.BrandId == 1 || t.BrandId == 2 || t.BrandId == 3) + .OrderBy(p => p.Id) + .ToBatchPageAsync(k => k.BrandId, pagingArgs); + + // Assert + foreach (var page in results) + { + snapshot.Add( + new + { + First = page.Value.CreateCursor(page.Value.First!, 0), + Last = page.Value.CreateCursor(page.Value.Last!, 0), + page.Value.Items + }, + name: page.Key.ToString()); + } + + snapshot.AddQueries(capture.Queries); + snapshot.MatchMarkdownSnapshot(); + } + private static async Task SeedAsync(string connectionString) { await using var context = new CatalogContext(connectionString); @@ -295,48 +338,6 @@ private static async Task SeedAsync(string connectionString) await context.SaveChangesAsync(); } - private static async Task SeedFooAsync(string connectionString) - { - await using var context = new FooBarContext(connectionString); - await context.Database.EnsureCreatedAsync(); - - context.Bars.Add( - new Bar - { - Id = 1, - Description = "Bar 1", - SomeField1 = "abc", - SomeField2 = null - }); - - context.Bars.Add( - new Bar - { - Id = 2, - Description = "Bar 2", - SomeField1 = "def", - SomeField2 = "ghi" - }); - - context.Foos.Add( - new Foo - { - Id = 1, - Name = "Foo 1", - BarId = null - }); - - context.Foos.Add( - new Foo - { - Id = 2, - Name = "Foo 2", - BarId = 1 - }); - - await context.SaveChangesAsync(); - } - public class BrandDto { public BrandDto(int id, string name) diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs index 2cbd9246725..bdc2625559e 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs @@ -38,8 +38,7 @@ public async Task Fetch_First_2_Items_Second_Page() // -> get first page var arguments = new PagingArguments(2); await using var context = new CatalogContext(connectionString); - var page = await context.Products.OrderBy(t => t.Name).ThenBy(t => t.Id) - .ToPageAsync(arguments); + var page = await context.Products.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); // Act arguments = new PagingArguments(2, after: page.CreateCursor(page.Last!)); @@ -49,6 +48,87 @@ public async Task Fetch_First_2_Items_Second_Page() page.MatchMarkdownSnapshot(); } + [Fact] + public async Task Fetch_First_2_Items_Second_Page_With_Offset_2() + { + // Arrange + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + // -> get first page + var arguments = new PagingArguments(2) { EnableRelativeCursors = true }; + await using var context = new CatalogContext(connectionString); + var page = await context.Products.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + var cursor = page.CreateCursor(page.Last!, 2); + arguments = new PagingArguments(2, after: cursor); + page = await context.Products.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Assert + page.MatchMarkdownSnapshot(); + } + + [Fact] + public async Task Fetch_First_2_Items_Second_Page_With_Offset_Negative_2() + { + // Arrange + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new CatalogContext(connectionString); + + // -> get first page + var arguments = new PagingArguments(2) { EnableRelativeCursors = true }; + var first = await context.Products.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // -> get second page + var cursor = first.CreateCursor(first.Last!, 0); + arguments = new PagingArguments(2, after: cursor) { EnableRelativeCursors = true }; + var page = await context.Products.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // -> get third page + cursor = page.CreateCursor(page.Last!, 0); + arguments = new PagingArguments(2, after: cursor) { EnableRelativeCursors = true }; + page = await context.Products.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + /* + 1 Product 0-0 + 2 Product 0-1 + 11 Product 0-10 + 12 Product 0-11 + 13 Product 0-12 <- Cursor is set here - 1 + 14 Product 0-13 + 15 Product 0-14 + 16 Product 0-15 + 17 Product 0-16 + 18 Product 0-17 + */ + cursor = page.CreateCursor(page.Last!, -1); + arguments = new PagingArguments(last: 2, before: cursor); + page = await context.Products.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Assert + /* + 1 Product 0-0 <- first + 2 Product 0-1 <- last + 11 Product 0-10 + 12 Product 0-11 + 13 Product 0-12 <- Cursor is set here - 1 + 14 Product 0-13 + 15 Product 0-14 + 16 Product 0-15 + 17 Product 0-16 + 18 Product 0-17 + */ + new { + First = page.First!.Name, + Last = page.Last!.Name, + ItemsCount = page.Items.Length + }.MatchMarkdownSnapshot(); + } + [Fact] public async Task Fetch_First_2_Items_Third_Page() { @@ -198,7 +278,7 @@ public async Task QueryContext_Simple_Selector_Include_Brand_Name() .MatchMarkdown(); } - [Fact] + [Fact] public async Task QueryContext_Simple_Selector_Include_Product_List() { // Arrange diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5_NET8_0.md index 19a899208b2..06f954abb4e 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5_NET8_0.md @@ -4,8 +4,8 @@ ```json { - "First": "UHJvZHVjdCAwLTA6MQ==", - "Last": "UHJvZHVjdCAwLTE6Mg==", + "First": "e31Qcm9kdWN0IDAtMDox", + "Last": "e31Qcm9kdWN0IDAtMToy", "Items": [ { "Id": 1, @@ -45,8 +45,8 @@ ```json { - "First": "UHJvZHVjdCAxLTA6MTAx", - "Last": "UHJvZHVjdCAxLTE6MTAy", + "First": "e31Qcm9kdWN0IDEtMDoxMDE=", + "Last": "e31Qcm9kdWN0IDEtMToxMDI=", "Items": [ { "Id": 101, @@ -86,8 +86,8 @@ ```json { - "First": "UHJvZHVjdCAyLTA6MjAx", - "Last": "UHJvZHVjdCAyLTE6MjAy", + "First": "e31Qcm9kdWN0IDItMDoyMDE=", + "Last": "e31Qcm9kdWN0IDItMToyMDI=", "Items": [ { "Id": 201, diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5_NET9_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5_NET9_0.md index 84bd821e26b..14b1e38fe52 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5_NET9_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5_NET9_0.md @@ -4,8 +4,8 @@ ```json { - "First": "UHJvZHVjdCAwLTA6MQ==", - "Last": "UHJvZHVjdCAwLTE6Mg==", + "First": "e31Qcm9kdWN0IDAtMDox", + "Last": "e31Qcm9kdWN0IDAtMToy", "Items": [ { "Id": 1, @@ -45,8 +45,8 @@ ```json { - "First": "UHJvZHVjdCAxLTA6MTAx", - "Last": "UHJvZHVjdCAxLTE6MTAy", + "First": "e31Qcm9kdWN0IDEtMDoxMDE=", + "Last": "e31Qcm9kdWN0IDEtMToxMDI=", "Items": [ { "Id": 101, @@ -86,8 +86,8 @@ ```json { - "First": "UHJvZHVjdCAyLTA6MjAx", - "Last": "UHJvZHVjdCAyLTE6MjAy", + "First": "e31Qcm9kdWN0IDItMDoyMDE=", + "Last": "e31Qcm9kdWN0IDItMToyMDI=", "Items": [ { "Id": 201, diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET8_0.md index 1ff0e6d16a4..55d657dc0c0 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET8_0.md @@ -4,8 +4,8 @@ ```json { - "First": "MTAw", - "Last": "OTk=", + "First": "e30xMDA=", + "Last": "e305OQ==", "Items": [ { "Id": 100, @@ -45,8 +45,8 @@ ```json { - "First": "MjAw", - "Last": "MTk5", + "First": "e30yMDA=", + "Last": "e30xOTk=", "Items": [ { "Id": 200, @@ -86,8 +86,8 @@ ```json { - "First": "MzAw", - "Last": "Mjk5", + "First": "e30zMDA=", + "Last": "e30yOTk=", "Items": [ { "Id": 300, diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md index 34bc584642e..9742f19d2d3 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md @@ -4,8 +4,8 @@ ```json { - "First": "MTAw", - "Last": "OTk=", + "First": "e30xMDA=", + "Last": "e305OQ==", "Items": [ { "Id": 100, @@ -45,8 +45,8 @@ ```json { - "First": "MjAw", - "Last": "MTk5", + "First": "e30yMDA=", + "Last": "e30xOTk=", "Items": [ { "Id": 200, @@ -86,8 +86,8 @@ ```json { - "First": "MzAw", - "Last": "Mjk5", + "First": "e30zMDA=", + "Last": "e30yOTk=", "Items": [ { "Id": 300, diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET8_0.md new file mode 100644 index 00000000000..7ffec45316c --- /dev/null +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET8_0.md @@ -0,0 +1,168 @@ +# BatchPaging_With_Relative_Cursor + +## 1 + +```json +{ + "First": "ezB8MHwxMDB9MQ==", + "Last": "ezB8MHwxMDB9Mg==", + "Items": [ + { + "Id": 1, + "Name": "Product 0-0", + "Description": null, + "Price": 0.0, + "ImageFileName": null, + "TypeId": 1, + "Type": null, + "BrandId": 1, + "Brand": null, + "AvailableStock": 0, + "RestockThreshold": 0, + "MaxStockThreshold": 0, + "OnReorder": false + }, + { + "Id": 2, + "Name": "Product 0-1", + "Description": null, + "Price": 0.0, + "ImageFileName": null, + "TypeId": 1, + "Type": null, + "BrandId": 1, + "Brand": null, + "AvailableStock": 0, + "RestockThreshold": 0, + "MaxStockThreshold": 0, + "OnReorder": false + } + ] +} +``` + +## 2 + +```json +{ + "First": "ezB8MHwxMDB9MTAx", + "Last": "ezB8MHwxMDB9MTAy", + "Items": [ + { + "Id": 101, + "Name": "Product 1-0", + "Description": null, + "Price": 0.0, + "ImageFileName": null, + "TypeId": 1, + "Type": null, + "BrandId": 2, + "Brand": null, + "AvailableStock": 0, + "RestockThreshold": 0, + "MaxStockThreshold": 0, + "OnReorder": false + }, + { + "Id": 102, + "Name": "Product 1-1", + "Description": null, + "Price": 0.0, + "ImageFileName": null, + "TypeId": 1, + "Type": null, + "BrandId": 2, + "Brand": null, + "AvailableStock": 0, + "RestockThreshold": 0, + "MaxStockThreshold": 0, + "OnReorder": false + } + ] +} +``` + +## 3 + +```json +{ + "First": "ezB8MHwxMDB9MjAx", + "Last": "ezB8MHwxMDB9MjAy", + "Items": [ + { + "Id": 201, + "Name": "Product 2-0", + "Description": null, + "Price": 0.0, + "ImageFileName": null, + "TypeId": 1, + "Type": null, + "BrandId": 3, + "Brand": null, + "AvailableStock": 0, + "RestockThreshold": 0, + "MaxStockThreshold": 0, + "OnReorder": false + }, + { + "Id": 202, + "Name": "Product 2-1", + "Description": null, + "Price": 0.0, + "ImageFileName": null, + "TypeId": 1, + "Type": null, + "BrandId": 3, + "Brand": null, + "AvailableStock": 0, + "RestockThreshold": 0, + "MaxStockThreshold": 0, + "OnReorder": false + } + ] +} +``` + +## SQL 0 + +```sql +SELECT p."BrandId" AS "Key", count(*)::int AS "Count" +FROM "Products" AS p +WHERE p."BrandId" IN (1, 2, 3) +GROUP BY p."BrandId" +``` + +## Expression 0 + +```text +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => (((t.BrandId == 1) OrElse (t.BrandId == 2)) OrElse (t.BrandId == 3))).OrderBy(p => p.Id).GroupBy(k => k.BrandId).Select(g => new CountResult`1() {Key = g.Key, Count = g.Count()}) +``` + +## SQL 1 + +```sql +SELECT t."BrandId", t0."Id", t0."AvailableStock", t0."BrandId", t0."Description", t0."ImageFileName", t0."MaxStockThreshold", t0."Name", t0."OnReorder", t0."Price", t0."RestockThreshold", t0."TypeId" +FROM ( + SELECT p."BrandId" + FROM "Products" AS p + WHERE p."BrandId" IN (1, 2, 3) + GROUP BY p."BrandId" +) AS t +LEFT JOIN ( + SELECT t1."Id", t1."AvailableStock", t1."BrandId", t1."Description", t1."ImageFileName", t1."MaxStockThreshold", t1."Name", t1."OnReorder", t1."Price", t1."RestockThreshold", t1."TypeId" + FROM ( + SELECT p0."Id", p0."AvailableStock", p0."BrandId", p0."Description", p0."ImageFileName", p0."MaxStockThreshold", p0."Name", p0."OnReorder", p0."Price", p0."RestockThreshold", p0."TypeId", ROW_NUMBER() OVER(PARTITION BY p0."BrandId" ORDER BY p0."Id") AS row + FROM "Products" AS p0 + WHERE p0."BrandId" = 1 OR p0."BrandId" = 2 OR p0."BrandId" = 3 + ) AS t1 + WHERE t1.row <= 3 +) AS t0 ON t."BrandId" = t0."BrandId" +ORDER BY t."BrandId", t0."BrandId", t0."Id" +``` + +## Expression 1 + +```text +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => (((t.BrandId == 1) OrElse (t.BrandId == 2)) OrElse (t.BrandId == 3))).GroupBy(k => k.BrandId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(p => p.Id).Take(3).ToList()}) +``` + diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET9_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET9_0.md new file mode 100644 index 00000000000..15b0d6f91a7 --- /dev/null +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET9_0.md @@ -0,0 +1,168 @@ +# BatchPaging_With_Relative_Cursor + +## 1 + +```json +{ + "First": "ezB8MHwxMDB9MQ==", + "Last": "ezB8MHwxMDB9Mg==", + "Items": [ + { + "Id": 1, + "Name": "Product 0-0", + "Description": null, + "Price": 0.0, + "ImageFileName": null, + "TypeId": 1, + "Type": null, + "BrandId": 1, + "Brand": null, + "AvailableStock": 0, + "RestockThreshold": 0, + "MaxStockThreshold": 0, + "OnReorder": false + }, + { + "Id": 2, + "Name": "Product 0-1", + "Description": null, + "Price": 0.0, + "ImageFileName": null, + "TypeId": 1, + "Type": null, + "BrandId": 1, + "Brand": null, + "AvailableStock": 0, + "RestockThreshold": 0, + "MaxStockThreshold": 0, + "OnReorder": false + } + ] +} +``` + +## 2 + +```json +{ + "First": "ezB8MHwxMDB9MTAx", + "Last": "ezB8MHwxMDB9MTAy", + "Items": [ + { + "Id": 101, + "Name": "Product 1-0", + "Description": null, + "Price": 0.0, + "ImageFileName": null, + "TypeId": 1, + "Type": null, + "BrandId": 2, + "Brand": null, + "AvailableStock": 0, + "RestockThreshold": 0, + "MaxStockThreshold": 0, + "OnReorder": false + }, + { + "Id": 102, + "Name": "Product 1-1", + "Description": null, + "Price": 0.0, + "ImageFileName": null, + "TypeId": 1, + "Type": null, + "BrandId": 2, + "Brand": null, + "AvailableStock": 0, + "RestockThreshold": 0, + "MaxStockThreshold": 0, + "OnReorder": false + } + ] +} +``` + +## 3 + +```json +{ + "First": "ezB8MHwxMDB9MjAx", + "Last": "ezB8MHwxMDB9MjAy", + "Items": [ + { + "Id": 201, + "Name": "Product 2-0", + "Description": null, + "Price": 0.0, + "ImageFileName": null, + "TypeId": 1, + "Type": null, + "BrandId": 3, + "Brand": null, + "AvailableStock": 0, + "RestockThreshold": 0, + "MaxStockThreshold": 0, + "OnReorder": false + }, + { + "Id": 202, + "Name": "Product 2-1", + "Description": null, + "Price": 0.0, + "ImageFileName": null, + "TypeId": 1, + "Type": null, + "BrandId": 3, + "Brand": null, + "AvailableStock": 0, + "RestockThreshold": 0, + "MaxStockThreshold": 0, + "OnReorder": false + } + ] +} +``` + +## SQL 0 + +```sql +SELECT p."BrandId" AS "Key", count(*)::int AS "Count" +FROM "Products" AS p +WHERE p."BrandId" IN (1, 2, 3) +GROUP BY p."BrandId" +``` + +## Expression 0 + +```text +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => (((t.BrandId == 1) OrElse (t.BrandId == 2)) OrElse (t.BrandId == 3))).OrderBy(p => p.Id).GroupBy(k => k.BrandId).Select(g => new CountResult`1() {Key = g.Key, Count = g.Count()}) +``` + +## SQL 1 + +```sql +SELECT p1."BrandId", p3."Id", p3."AvailableStock", p3."BrandId", p3."Description", p3."ImageFileName", p3."MaxStockThreshold", p3."Name", p3."OnReorder", p3."Price", p3."RestockThreshold", p3."TypeId" +FROM ( + SELECT p."BrandId" + FROM "Products" AS p + WHERE p."BrandId" IN (1, 2, 3) + GROUP BY p."BrandId" +) AS p1 +LEFT JOIN ( + SELECT p2."Id", p2."AvailableStock", p2."BrandId", p2."Description", p2."ImageFileName", p2."MaxStockThreshold", p2."Name", p2."OnReorder", p2."Price", p2."RestockThreshold", p2."TypeId" + FROM ( + SELECT p0."Id", p0."AvailableStock", p0."BrandId", p0."Description", p0."ImageFileName", p0."MaxStockThreshold", p0."Name", p0."OnReorder", p0."Price", p0."RestockThreshold", p0."TypeId", ROW_NUMBER() OVER(PARTITION BY p0."BrandId" ORDER BY p0."Id") AS row + FROM "Products" AS p0 + WHERE p0."BrandId" = 1 OR p0."BrandId" = 2 OR p0."BrandId" = 3 + ) AS p2 + WHERE p2.row <= 3 +) AS p3 ON p1."BrandId" = p3."BrandId" +ORDER BY p1."BrandId", p3."BrandId", p3."Id" +``` + +## Expression 1 + +```text +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => (((t.BrandId == 1) OrElse (t.BrandId == 2)) OrElse (t.BrandId == 3))).GroupBy(k => k.BrandId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(p => p.Id).Take(3).ToList()}) +``` + diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md index e6e8cfb64fe..9266fe3eaf0 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md @@ -21,9 +21,9 @@ ORDER BY b."Name", b."Id" "HasNextPage": false, "HasPreviousPage": false, "First": 1, - "FirstCursor": "QnJhbmRcOjA6MQ==", + "FirstCursor": "e31CcmFuZFw6MDox", "Last": 100, - "LastCursor": "QnJhbmRcOjk5OjEwMA==" + "LastCursor": "e31CcmFuZFw6OTk6MTAw" } ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs_NET8_0.md index e6e8cfb64fe..9266fe3eaf0 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs_NET8_0.md @@ -21,9 +21,9 @@ ORDER BY b."Name", b."Id" "HasNextPage": false, "HasPreviousPage": false, "First": 1, - "FirstCursor": "QnJhbmRcOjA6MQ==", + "FirstCursor": "e31CcmFuZFw6MDox", "Last": 100, - "LastCursor": "QnJhbmRcOjk5OjEwMA==" + "LastCursor": "e31CcmFuZFw6OTk6MTAw" } ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5.md index 84fbabb9dca..5517147aaa8 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5.md @@ -23,9 +23,9 @@ LIMIT @__p_0 "HasNextPage": true, "HasPreviousPage": false, "First": 1, - "FirstCursor": "QnJhbmRcOjA6MQ==", + "FirstCursor": "e31CcmFuZFw6MDox", "Last": 13, - "LastCursor": "QnJhbmRcOjEyOjEz" + "LastCursor": "e31CcmFuZFw6MTI6MTM=" } ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md index 57afb399be8..69d7a1dfff9 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md @@ -26,9 +26,9 @@ LIMIT @__p_2 "HasNextPage": true, "HasPreviousPage": true, "First": 14, - "FirstCursor": "QnJhbmRcOjEzOjE0", + "FirstCursor": "e31CcmFuZFw6MTM6MTQ=", "Last": 18, - "LastCursor": "QnJhbmRcOjE3OjE4" + "LastCursor": "e31CcmFuZFw6MTc6MTg=" } ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13_NET8_0.md index 57afb399be8..69d7a1dfff9 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13_NET8_0.md @@ -26,9 +26,9 @@ LIMIT @__p_2 "HasNextPage": true, "HasPreviousPage": true, "First": 14, - "FirstCursor": "QnJhbmRcOjEzOjE0", + "FirstCursor": "e31CcmFuZFw6MTM6MTQ=", "Last": 18, - "LastCursor": "QnJhbmRcOjE3OjE4" + "LastCursor": "e31CcmFuZFw6MTc6MTg=" } ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md index fdff316caee..8b8f843a2f9 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md @@ -26,9 +26,9 @@ LIMIT @__p_2 "HasNextPage": true, "HasPreviousPage": true, "First": 92, - "FirstCursor": "QnJhbmRcOjkxOjky", + "FirstCursor": "e31CcmFuZFw6OTE6OTI=", "Last": 96, - "LastCursor": "QnJhbmRcOjk1Ojk2" + "LastCursor": "e31CcmFuZFw6OTU6OTY=" } ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96_NET8_0.md index fdff316caee..8b8f843a2f9 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96_NET8_0.md @@ -26,9 +26,9 @@ LIMIT @__p_2 "HasNextPage": true, "HasPreviousPage": true, "First": 92, - "FirstCursor": "QnJhbmRcOjkxOjky", + "FirstCursor": "e31CcmFuZFw6OTE6OTI=", "Last": 96, - "LastCursor": "QnJhbmRcOjk1Ojk2" + "LastCursor": "e31CcmFuZFw6OTU6OTY=" } ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_NET8_0.md index 84fbabb9dca..5517147aaa8 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_NET8_0.md @@ -23,9 +23,9 @@ LIMIT @__p_0 "HasNextPage": true, "HasPreviousPage": false, "First": 1, - "FirstCursor": "QnJhbmRcOjA6MQ==", + "FirstCursor": "e31CcmFuZFw6MDox", "Last": 13, - "LastCursor": "QnJhbmRcOjEyOjEz" + "LastCursor": "e31CcmFuZFw6MTI6MTM=" } ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md index 8b04e57e616..876b9309b11 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md @@ -23,9 +23,11 @@ LIMIT @__p_0 "HasNextPage": false, "HasPreviousPage": true, "First": 96, - "FirstCursor": "QnJhbmRcOjk1Ojk2", + "FirstName": "Brand:95", + "FirstCursor": "e31CcmFuZFw6OTU6OTY=", "Last": 100, - "LastCursor": "QnJhbmRcOjk5OjEwMA==" + "LastName": "Brand:99", + "LastCursor": "e31CcmFuZFw6OTk6MTAw" } ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5_NET8_0.md index 8b04e57e616..876b9309b11 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5_NET8_0.md @@ -23,9 +23,11 @@ LIMIT @__p_0 "HasNextPage": false, "HasPreviousPage": true, "First": 96, - "FirstCursor": "QnJhbmRcOjk1Ojk2", + "FirstName": "Brand:95", + "FirstCursor": "e31CcmFuZFw6OTU6OTY=", "Last": 100, - "LastCursor": "QnJhbmRcOjk5OjEwMA==" + "LastName": "Brand:99", + "LastCursor": "e31CcmFuZFw6OTk6MTAw" } ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Batch_Fetch_First_2_Items.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Batch_Fetch_First_2_Items.md index 5f0f515d13a..738d323a88c 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Batch_Fetch_First_2_Items.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Batch_Fetch_First_2_Items.md @@ -4,8 +4,8 @@ ```json { - "First": "UHJvZHVjdCAwLTA6MQ==", - "Last": "UHJvZHVjdCAwLTE6Mg==", + "First": "e31Qcm9kdWN0IDAtMDox", + "Last": "e31Qcm9kdWN0IDAtMToy", "Items": [ { "Id": 1, @@ -45,8 +45,8 @@ ```json { - "First": "UHJvZHVjdCAxLTA6MTAx", - "Last": "UHJvZHVjdCAxLTE6MTAy", + "First": "e31Qcm9kdWN0IDEtMDoxMDE=", + "Last": "e31Qcm9kdWN0IDEtMToxMDI=", "Items": [ { "Id": 101, @@ -86,8 +86,8 @@ ```json { - "First": "UHJvZHVjdCAyLTA6MjAx", - "Last": "UHJvZHVjdCAyLTE6MjAy", + "First": "e31Qcm9kdWN0IDItMDoyMDE=", + "Last": "e31Qcm9kdWN0IDItMToyMDI=", "Items": [ { "Id": 201, diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_With_Offset_2.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_With_Offset_2.md new file mode 100644 index 00000000000..50534accf27 --- /dev/null +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_With_Offset_2.md @@ -0,0 +1,36 @@ +# Fetch_First_2_Items_Second_Page_With_Offset_2 + +```json +[ + { + "Id": 15, + "Name": "Product 0-14", + "Description": null, + "Price": 0.0, + "ImageFileName": null, + "TypeId": 1, + "Type": null, + "BrandId": 1, + "Brand": null, + "AvailableStock": 0, + "RestockThreshold": 0, + "MaxStockThreshold": 0, + "OnReorder": false + }, + { + "Id": 16, + "Name": "Product 0-15", + "Description": null, + "Price": 0.0, + "ImageFileName": null, + "TypeId": 1, + "Type": null, + "BrandId": 1, + "Brand": null, + "AvailableStock": 0, + "RestockThreshold": 0, + "MaxStockThreshold": 0, + "OnReorder": false + } +] +``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_With_Offset_Negative_2.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_With_Offset_Negative_2.md new file mode 100644 index 00000000000..cc13a20f10b --- /dev/null +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_With_Offset_Negative_2.md @@ -0,0 +1,9 @@ +# Fetch_First_2_Items_Second_Page_With_Offset_Negative_2 + +```json +{ + "First": "Product 0-0", + "Last": "Product 0-1", + "ItemsCount": 2 +} +``` diff --git a/src/GreenDonut/test/GreenDonut.Data.Tests/Cursors/CursorFormatterTests.cs b/src/GreenDonut/test/GreenDonut.Data.Tests/Cursors/CursorFormatterTests.cs index 8f6bf4cc34c..0047c00b5e1 100644 --- a/src/GreenDonut/test/GreenDonut.Data.Tests/Cursors/CursorFormatterTests.cs +++ b/src/GreenDonut/test/GreenDonut.Data.Tests/Cursors/CursorFormatterTests.cs @@ -18,8 +18,8 @@ public void Format_Single_Key() var result = CursorFormatter.Format(entity, [new CursorKey(selector, serializer)]); // assert - Assert.Equal("dGVzdA==", result); - Assert.Equal("test", Encoding.UTF8.GetString(Convert.FromBase64String(result))); + Assert.Equal("e310ZXN0", result); + Assert.Equal("{}test", Encoding.UTF8.GetString(Convert.FromBase64String(result))); } [Fact] @@ -34,8 +34,8 @@ public void Format_Single_Key_With_Colon() var result = CursorFormatter.Format(entity, [new CursorKey(selector, serializer)]); // assert - Assert.Equal("dGVzdFw6dGVzdA==", result); - Assert.Equal("test\\:test", Encoding.UTF8.GetString(Convert.FromBase64String(result))); + Assert.Equal("e310ZXN0XDp0ZXN0", result); + Assert.Equal("{}test\\:test", Encoding.UTF8.GetString(Convert.FromBase64String(result))); } [Fact] @@ -56,8 +56,8 @@ public void Format_Two_Keys() ]); // assert - Assert.Equal("dGVzdDpkZXNjcmlwdGlvbg==", result); - Assert.Equal("test:description", Encoding.UTF8.GetString(Convert.FromBase64String(result))); + Assert.Equal("e310ZXN0OmRlc2NyaXB0aW9u", result); + Assert.Equal("{}test:description", Encoding.UTF8.GetString(Convert.FromBase64String(result))); } [Fact] @@ -78,8 +78,8 @@ public void Format_Two_Keys_With_Colon() ]); // assert - Assert.Equal("dGVzdFw6MzQ1OmRlc2NyaXB0aW9uXDoxMjM=", result); - Assert.Equal("test\\:345:description\\:123", Encoding.UTF8.GetString(Convert.FromBase64String(result))); + Assert.Equal("e310ZXN0XDozNDU6ZGVzY3JpcHRpb25cOjEyMw==", result); + Assert.Equal("{}test\\:345:description\\:123", Encoding.UTF8.GetString(Convert.FromBase64String(result))); } [Fact] @@ -98,7 +98,7 @@ public void Format_And_Parse_Two_Keys_With_Colon() new CursorKey(selector1, serializer), new CursorKey(selector2, serializer) ]); - var parsed =CursorParser.Parse( + var parsed = CursorParser.Parse( formatted, [ new CursorKey(selector1, serializer), @@ -106,8 +106,11 @@ public void Format_And_Parse_Two_Keys_With_Colon() ]); // assert - Assert.Equal("test:345", parsed[0]); - Assert.Equal("description:123", parsed[1]); + Assert.Null(parsed.Offset); + Assert.Null(parsed.PageIndex); + Assert.Null(parsed.TotalCount); + Assert.Equal("test:345", parsed.Values[0]); + Assert.Equal("description:123", parsed.Values[1]); } public class MyClass diff --git a/src/HotChocolate/Core/src/Abstractions/GraphQLIgnoreAttribute.cs b/src/HotChocolate/Core/src/Abstractions/GraphQLIgnoreAttribute.cs index 5d2e71ccc7b..8d9f0dbf158 100644 --- a/src/HotChocolate/Core/src/Abstractions/GraphQLIgnoreAttribute.cs +++ b/src/HotChocolate/Core/src/Abstractions/GraphQLIgnoreAttribute.cs @@ -1,6 +1,4 @@ namespace HotChocolate; [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Field)] -public sealed class GraphQLIgnoreAttribute : Attribute -{ -} +public sealed class GraphQLIgnoreAttribute : Attribute; diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Errors.cs b/src/HotChocolate/Core/src/Types.Analyzers/Errors.cs index 05d43557cd5..6133e98d304 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Errors.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Errors.cs @@ -85,4 +85,31 @@ public static class Errors category: "DataLoader", DiagnosticSeverity.Error, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor ConnectionSingleGenericTypeArgument = + new( + id: "HC0086", + title: "Invalid Connection Structure", + messageFormat: "A generic connection/edge type must have a single generic type argument that represents the node type", + category: "TypeSystem", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor ConnectionNameFormatIsInvalid = + new( + id: "HC0087", + title: "Invalid Connection/Edge Name Format", + messageFormat: "A connection/edge name must be in the format `{0}Edge` or `{0}Connection`", + category: "TypeSystem", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor ConnectionNameDuplicate = + new( + id: "HC0088", + title: "Invalid Connection/Edge Name", + messageFormat: "The type `{0}` cannot be mapped to the GraphQL type name `{1}` as `{2}` is already mapped to it", + category: "TypeSystem", + DiagnosticSeverity.Error, + isEnabledByDefault: true); } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ConnectionTypeFileBuilder.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ConnectionTypeFileBuilder.cs new file mode 100644 index 00000000000..8ec87212066 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ConnectionTypeFileBuilder.cs @@ -0,0 +1,160 @@ +using System.Text; +using HotChocolate.Types.Analyzers.Helpers; +using HotChocolate.Types.Analyzers.Models; +using Microsoft.CodeAnalysis; + +namespace HotChocolate.Types.Analyzers.FileBuilders; + +public sealed class ConnectionTypeFileBuilder(StringBuilder sb) : TypeFileBuilderBase(sb) +{ + public override void WriteBeginClass(IOutputTypeInfo type) + { + Writer.WriteIndentedLine( + "{0} partial class {1} : ObjectType", + type.IsPublic ? "public" : "internal", + type.Name, + type.RuntimeTypeFullName); + Writer.WriteIndentedLine("{"); + Writer.IncreaseIndent(); + } + + public override void WriteInitializeMethod(IOutputTypeInfo type) + { + if (type is not ConnectionTypeInfo connectionType) + { + throw new InvalidOperationException( + "The specified type is not a connection type."); + } + + Writer.WriteIndentedLine( + "protected override void Configure(global::{0} descriptor)", + WellKnownTypes.IObjectTypeDescriptor, + connectionType.RuntimeTypeFullName); + + Writer.WriteIndentedLine("{"); + + using (Writer.IncreaseIndent()) + { + if (connectionType.Resolvers.Length > 0) + { + Writer.WriteIndentedLine( + "var thisType = typeof(global::{0});", + connectionType.RuntimeTypeFullName); + Writer.WriteIndentedLine( + "var extend = descriptor.Extend();"); + Writer.WriteIndentedLine( + "var bindingResolver = extend.Context.ParameterBindingResolver;"); + Writer.WriteIndentedLine( + connectionType.Resolvers.Any(t => t.RequiresParameterBindings) + ? "var resolvers = new __Resolvers(bindingResolver);" + : "var resolvers = new __Resolvers();"); + } + + if (connectionType.RuntimeType.IsGenericType + && !string.IsNullOrEmpty(connectionType.NameFormat)) + { + var nodeTypeName = connectionType.RuntimeType.TypeArguments[0].ToFullyQualified(); + Writer.WriteLine(); + Writer.WriteIndentedLine( + "var nodeTypeRef = extend.Context.TypeInspector.GetTypeRef(typeof({0}));", + nodeTypeName); + Writer.WriteIndentedLine("descriptor"); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine( + ".Name(t => string.Format(\"{0}\", t.Name))", + connectionType.NameFormat); + Writer.WriteIndentedLine( + ".DependsOn(nodeTypeRef);"); + } + } + else if (!string.IsNullOrEmpty(connectionType.NameFormat) + && !connectionType.NameFormat.Contains("{0}")) + { + Writer.WriteLine(); + Writer.WriteIndentedLine( + "descriptor.Name(\"{0}\");", + connectionType.NameFormat); + } + + WriteResolverBindings(connectionType); + } + + Writer.WriteIndentedLine("}"); + Writer.WriteLine(); + } + + public override void WriteConfigureMethod(IOutputTypeInfo type) + { + } + + protected override void WriteResolverBindingDescriptor(IOutputTypeInfo type, Resolver resolver) + { + if (type is not ConnectionTypeInfo connectionType) + { + throw new InvalidOperationException( + "The specified type is not a connection type."); + } + + if ((resolver.Flags & FieldFlags.ConnectionEdgesField) == FieldFlags.ConnectionEdgesField) + { + var edgeTypeName = $"{connectionType.Namespace}.{connectionType.EdgeTypeName}"; + + Writer.WriteIndentedLine( + ".Type<{0}>()", + ToGraphQLType(resolver.UnwrappedReturnType, edgeTypeName)); + } + else + { + base.WriteResolverBindingDescriptor(type, resolver); + } + } + + private static string ToGraphQLType( + ITypeSymbol typeSymbol, + string edgeTypeName) + { + var isNullable = typeSymbol.NullableAnnotation == NullableAnnotation.Annotated; + + if (typeSymbol is INamedTypeSymbol namedTypeSymbol) + { + var innerType = GetListInnerType(namedTypeSymbol); + + if (innerType is null) + { + return isNullable + ? $"global::{edgeTypeName}" + : $"global::{WellKnownTypes.NonNullType}"; + } + + var type = ToGraphQLType(innerType, edgeTypeName); + type = $"global::{WellKnownTypes.ListType}<{type}>"; + return isNullable ? type : $"global::{WellKnownTypes.NonNullType}<{type}>"; + } + + throw new InvalidOperationException($"Unsupported type: {typeSymbol}"); + } + + private static INamedTypeSymbol? GetListInnerType(INamedTypeSymbol typeSymbol) + { + if (typeSymbol.IsGenericType && + (typeSymbol.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IReadOnlyList" || + typeSymbol.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IList")) + { + return typeSymbol.TypeArguments[0] as INamedTypeSymbol; + } + + foreach (var interfaceType in typeSymbol.AllInterfaces) + { + if (interfaceType.IsGenericType && + (interfaceType.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IReadOnlyList" || + interfaceType.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IList")) + { + return interfaceType.TypeArguments[0] as INamedTypeSymbol; + } + } + + return null; + } + +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/EdgeTypeFileBuilder.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/EdgeTypeFileBuilder.cs new file mode 100644 index 00000000000..2962eff5082 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/EdgeTypeFileBuilder.cs @@ -0,0 +1,89 @@ +using System.Text; +using HotChocolate.Types.Analyzers.Helpers; +using HotChocolate.Types.Analyzers.Models; + +namespace HotChocolate.Types.Analyzers.FileBuilders; + +public sealed class EdgeTypeFileBuilder(StringBuilder sb) : TypeFileBuilderBase(sb) +{ + public override void WriteBeginClass(IOutputTypeInfo type) + { + Writer.WriteIndentedLine( + "{0} partial class {1} : ObjectType", + type.IsPublic ? "public" : "internal", + type.Name, + type.RuntimeTypeFullName); + Writer.WriteIndentedLine("{"); + Writer.IncreaseIndent(); + } + + public override void WriteInitializeMethod(IOutputTypeInfo type) + { + if (type is not EdgeTypeInfo edgeType) + { + throw new InvalidOperationException( + "The specified type is not a edge type."); + } + + Writer.WriteIndentedLine( + "protected override void Configure(global::{0} descriptor)", + WellKnownTypes.IObjectTypeDescriptor, + edgeType.RuntimeTypeFullName); + + Writer.WriteIndentedLine("{"); + + using (Writer.IncreaseIndent()) + { + if (edgeType.Resolvers.Length > 0) + { + Writer.WriteIndentedLine( + "var thisType = typeof(global::{0});", + edgeType.RuntimeTypeFullName); + Writer.WriteIndentedLine( + "var extend = descriptor.Extend();"); + Writer.WriteIndentedLine( + "var bindingResolver = extend.Context.ParameterBindingResolver;"); + Writer.WriteIndentedLine( + edgeType.Resolvers.Any(t => t.RequiresParameterBindings) + ? "var resolvers = new __Resolvers(bindingResolver);" + : "var resolvers = new __Resolvers();"); + } + + if (edgeType.RuntimeType.IsGenericType + && !string.IsNullOrEmpty(edgeType.NameFormat)) + { + var nodeTypeName = edgeType.RuntimeType.TypeArguments[0].ToFullyQualified(); + Writer.WriteLine(); + Writer.WriteIndentedLine( + "var nodeTypeRef = extend.Context.TypeInspector.GetTypeRef(typeof({0}));", + nodeTypeName); + Writer.WriteIndentedLine("descriptor"); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine( + ".Name(t => string.Format(\"{0}\", t.Name))", + edgeType.NameFormat); + Writer.WriteIndentedLine( + ".DependsOn(nodeTypeRef);"); + } + } + else if (!string.IsNullOrEmpty(edgeType.NameFormat) + && !edgeType.NameFormat.Contains("{0}")) + { + Writer.WriteLine(); + Writer.WriteIndentedLine( + "descriptor.Name(\"{0}\");", + edgeType.NameFormat); + } + + WriteResolverBindings(edgeType); + } + + Writer.WriteIndentedLine("}"); + Writer.WriteLine(); + } + + public override void WriteConfigureMethod(IOutputTypeInfo type) + { + } +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/InterfaceTypeExtensionFileBuilder.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/InterfaceTypeExtensionFileBuilder.cs deleted file mode 100644 index fadf106e960..00000000000 --- a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/InterfaceTypeExtensionFileBuilder.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Text; -using HotChocolate.Types.Analyzers.Helpers; -using HotChocolate.Types.Analyzers.Models; - -namespace HotChocolate.Types.Analyzers.FileBuilders; - -public sealed class InterfaceTypeExtensionFileBuilder(StringBuilder sb, string ns) : IOutputTypeFileBuilder -{ - private readonly CodeWriter _writer = new(sb); - - public void WriteHeader() - { - _writer.WriteFileHeader(); - _writer.WriteIndentedLine("using Microsoft.Extensions.DependencyInjection;"); - _writer.WriteLine(); - } - - public void WriteBeginNamespace() - { - _writer.WriteIndentedLine("namespace {0}", ns); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - } - - public void WriteEndNamespace() - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - _writer.WriteLine(); - } - - public string WriteBeginClass(string typeName) - { - _writer.WriteIndentedLine("public static partial class {0}", typeName); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - return typeName; - } - - public void WriteEndClass() - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - } - - public void WriteInitializeMethod(IOutputTypeInfo typeInfo) - { - _writer.WriteIndentedLine( - "internal static void Initialize(global::HotChocolate.Types.IInterfaceTypeDescriptor<{0}> descriptor)", - typeInfo.RuntimeType!.ToFullyQualified()); - _writer.WriteIndentedLine("{"); - - using (_writer.IncreaseIndent()) - { - if (typeInfo.Resolvers.Length > 0) - { - _writer.WriteIndentedLine("const global::{0} bindingFlags =", WellKnownTypes.BindingFlags); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("global::{0}.Public", WellKnownTypes.BindingFlags); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("| global::{0}.NonPublic", WellKnownTypes.BindingFlags); - _writer.WriteIndentedLine("| global::{0}.Static;", WellKnownTypes.BindingFlags); - } - } - - _writer.WriteLine(); - - _writer.WriteIndentedLine( - "var thisType = typeof({0});", - typeInfo.Type.ToFullyQualified()); - _writer.WriteIndentedLine( - "var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver;"); - _writer.WriteIndentedLine( - "global::{0}Resolvers.InitializeBindings(bindingResolver);", - typeInfo.Type.ToDisplayString()); - } - - if (typeInfo.Resolvers.Length > 0) - { - foreach (var resolver in typeInfo.Resolvers) - { - _writer.WriteLine(); - _writer.WriteIndentedLine("descriptor"); - - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine( - ".Field(thisType.GetMember(\"{0}\", bindingFlags)[0])", - resolver.Member.Name); - - _writer.WriteIndentedLine(".ExtendWith(c =>"); - _writer.WriteIndentedLine("{"); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("c.Definition.SetSourceGeneratorFlags();"); - _writer.WriteIndentedLine( - "c.Definition.Resolvers = {0}Resolvers.{1}_{2}();", - typeInfo.Type.ToFullyQualified(), - typeInfo.Type.Name, - resolver.Member.Name); - - if (resolver.ResultKind is not ResolverResultKind.Pure - && !resolver.Member.HasPostProcessorAttribute() - && resolver.Member.IsListType(out var elementType)) - { - _writer.WriteIndentedLine( - "c.Definition.ResultPostProcessor = global::{0}<{1}>.Default;", - WellKnownTypes.ListPostProcessor, - elementType); - } - } - _writer.WriteIndentedLine("});"); - } - } - } - - _writer.WriteLine(); - _writer.WriteIndentedLine("Configure(descriptor);"); - } - - _writer.WriteIndentedLine("}"); - } - - public void WriteConfigureMethod(IOutputTypeInfo typeInfo) - { - _writer.WriteIndentedLine( - "static partial void Configure(global::HotChocolate.Types.IInterfaceTypeDescriptor<{0}> descriptor);", - typeInfo.RuntimeType!.ToFullyQualified()); - } -} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/InterfaceTypeFileBuilder.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/InterfaceTypeFileBuilder.cs new file mode 100644 index 00000000000..dc4a8df3354 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/InterfaceTypeFileBuilder.cs @@ -0,0 +1,63 @@ +using System.Text; +using HotChocolate.Types.Analyzers.Helpers; +using HotChocolate.Types.Analyzers.Models; + +namespace HotChocolate.Types.Analyzers.FileBuilders; + +public sealed class InterfaceTypeFileBuilder(StringBuilder sb) : TypeFileBuilderBase(sb) +{ + public override void WriteInitializeMethod(IOutputTypeInfo type) + { + if (type is not InterfaceTypeInfo interfaceType) + { + throw new InvalidOperationException( + "The specified type is not an object type."); + } + + Writer.WriteIndentedLine( + "internal static void Initialize(global::{0} descriptor)", + WellKnownTypes.IInterfaceTypeDescriptor, + interfaceType.RuntimeTypeFullName); + + Writer.WriteIndentedLine("{"); + + using (Writer.IncreaseIndent()) + { + if (interfaceType.Resolvers.Length > 0) + { + Writer.WriteIndentedLine( + "var thisType = typeof({0});", + interfaceType.SchemaSchemaType.ToFullyQualified()); + Writer.WriteIndentedLine( + "var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver;"); + Writer.WriteIndentedLine( + interfaceType.Resolvers.Any(t => t.RequiresParameterBindings) + ? "var resolvers = new __Resolvers(bindingResolver);" + : "var resolvers = new __Resolvers();"); + } + + WriteResolverBindings(interfaceType); + + Writer.WriteLine(); + Writer.WriteIndentedLine("Configure(descriptor);"); + } + + Writer.WriteIndentedLine("}"); + Writer.WriteLine(); + } + + public override void WriteConfigureMethod(IOutputTypeInfo type) + { + if (type is not InterfaceTypeInfo interfaceType) + { + throw new InvalidOperationException( + "The specified type is not an object type."); + } + + Writer.WriteIndentedLine( + "static partial void Configure(global::{0} descriptor);", + WellKnownTypes.IInterfaceTypeDescriptor, + interfaceType.RuntimeTypeFullName); + Writer.WriteLine(); + } +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ModuleFileBuilder.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ModuleFileBuilder.cs index 974a99bd3db..2b05b3f6c87 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ModuleFileBuilder.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ModuleFileBuilder.cs @@ -1,7 +1,6 @@ using System.Text; using HotChocolate.Types.Analyzers.Helpers; using HotChocolate.Types.Analyzers.Models; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; namespace HotChocolate.Types.Analyzers.FileBuilders; diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ObjectTypeExtensionFileBuilder.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ObjectTypeExtensionFileBuilder.cs deleted file mode 100644 index dcdda490b42..00000000000 --- a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ObjectTypeExtensionFileBuilder.cs +++ /dev/null @@ -1,289 +0,0 @@ -using System.Text; -using HotChocolate.Types.Analyzers.Helpers; -using HotChocolate.Types.Analyzers.Models; - -namespace HotChocolate.Types.Analyzers.FileBuilders; - -public sealed class ObjectTypeExtensionFileBuilder(StringBuilder sb, string ns) : IOutputTypeFileBuilder -{ - private readonly CodeWriter _writer = new(sb); - - public void WriteHeader() - { - _writer.WriteFileHeader(); - _writer.WriteIndentedLine("using Microsoft.Extensions.DependencyInjection;"); - _writer.WriteLine(); - } - - public void WriteBeginNamespace() - { - _writer.WriteIndentedLine("namespace {0}", ns); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - } - - public void WriteEndNamespace() - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - _writer.WriteLine(); - } - - public string WriteBeginClass(string typeName) - { - _writer.WriteIndentedLine("public static partial class {0}", typeName); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - return typeName; - } - - public void WriteEndClass() - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - } - - public void WriteInitializeMethod(IOutputTypeInfo typeInfo) - { - if (typeInfo is ObjectTypeExtensionInfo objectTypeExtension) - { - WriteObjectTypeInitializeMethod(objectTypeExtension); - } - else if (typeInfo is RootTypeExtensionInfo rootTypeExtension) - { - WriteRootTypeInitializeMethod(rootTypeExtension); - } - else - { - throw new NotSupportedException(); - } - } - - public void WriteObjectTypeInitializeMethod(IOutputTypeInfo typeInfo) - { - if (typeInfo is not ObjectTypeExtensionInfo objectTypeExtension) - { - return; - } - - if (typeInfo.IsRootType) - { - _writer.WriteIndentedLine( - "internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor)"); - } - else - { - _writer.WriteIndentedLine( - "internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor<{0}> descriptor)", - objectTypeExtension.RuntimeType.ToFullyQualified()); - } - - _writer.WriteIndentedLine("{"); - - using (_writer.IncreaseIndent()) - { - if (objectTypeExtension.Resolvers.Length > 0 || objectTypeExtension.NodeResolver is not null) - { - var hasRuntimeBindings = objectTypeExtension.Resolvers.Any( - t => t.Bindings.Any(b => b.Kind == MemberBindingKind.Property)); - - _writer.WriteIndentedLine("const global::{0} bindingFlags =", WellKnownTypes.BindingFlags); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("global::{0}.Public", WellKnownTypes.BindingFlags); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("| global::{0}.NonPublic", WellKnownTypes.BindingFlags); - _writer.WriteIndentedLine("| global::{0}.Static;", WellKnownTypes.BindingFlags); - } - } - - if (hasRuntimeBindings) - { - _writer.WriteIndentedLine("const global::{0} runtimeBindingFlags =", WellKnownTypes.BindingFlags); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("global::{0}.Public", WellKnownTypes.BindingFlags); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("| global::{0}.NonPublic", WellKnownTypes.BindingFlags); - _writer.WriteIndentedLine("| global::{0}.Instance", WellKnownTypes.BindingFlags); - _writer.WriteIndentedLine("| global::{0}.Static;", WellKnownTypes.BindingFlags); - } - } - } - - _writer.WriteLine(); - - _writer.WriteIndentedLine( - "var thisType = typeof({0});", - objectTypeExtension.Type.ToFullyQualified()); - if (hasRuntimeBindings) - { - _writer.WriteIndentedLine( - "var runtimeType = typeof({0});", - objectTypeExtension.RuntimeType.ToFullyQualified()); - } - - _writer.WriteIndentedLine( - "var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver;"); - _writer.WriteIndentedLine( - "global::{0}Resolvers.InitializeBindings(bindingResolver);", - objectTypeExtension.Type.ToDisplayString()); - } - - if (objectTypeExtension.NodeResolver is not null) - { - _writer.WriteLine(); - _writer.WriteIndentedLine("descriptor"); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine(".ImplementsNode()"); - _writer.WriteIndentedLine( - ".ResolveNode({0}Resolvers.{1}_{2}().Resolver!);", - objectTypeExtension.Type.ToFullyQualified(), - objectTypeExtension.Type.Name, - objectTypeExtension.NodeResolver.Member.Name); - } - } - - WriteResolverBindings(objectTypeExtension); - - _writer.WriteLine(); - _writer.WriteIndentedLine("Configure(descriptor);"); - } - - _writer.WriteIndentedLine("}"); - } - - public void WriteRootTypeInitializeMethod(IOutputTypeInfo typeInfo) - { - if (typeInfo is not RootTypeExtensionInfo rootTypeExtension) - { - return; - } - - _writer.WriteIndentedLine( - "internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor)"); - - _writer.WriteIndentedLine("{"); - - using (_writer.IncreaseIndent()) - { - if (rootTypeExtension.Resolvers.Length > 0) - { - _writer.WriteIndentedLine("const global::{0} bindingFlags =", WellKnownTypes.BindingFlags); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("global::{0}.Public", WellKnownTypes.BindingFlags); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("| global::{0}.NonPublic", WellKnownTypes.BindingFlags); - _writer.WriteIndentedLine("| global::{0}.Static;", WellKnownTypes.BindingFlags); - } - } - - _writer.WriteLine(); - - _writer.WriteIndentedLine( - "var thisType = typeof({0});", - rootTypeExtension.Type.ToFullyQualified()); - _writer.WriteIndentedLine( - "var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver;"); - _writer.WriteIndentedLine( - "global::{0}Resolvers.InitializeBindings(bindingResolver);", - rootTypeExtension.Type.ToDisplayString()); - } - - WriteResolverBindings(rootTypeExtension); - - _writer.WriteLine(); - _writer.WriteIndentedLine("Configure(descriptor);"); - } - - _writer.WriteIndentedLine("}"); - } - - private void WriteResolverBindings(IOutputTypeInfo typeInfo) - { - if (typeInfo.Resolvers.Length > 0) - { - foreach (var resolver in typeInfo.Resolvers) - { - _writer.WriteLine(); - _writer.WriteIndentedLine("descriptor"); - - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine( - ".Field(thisType.GetMember(\"{0}\", bindingFlags)[0])", - resolver.Member.Name); - - _writer.WriteIndentedLine(".ExtendWith(c =>"); - _writer.WriteIndentedLine("{"); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("c.Definition.SetSourceGeneratorFlags();"); - _writer.WriteIndentedLine( - "c.Definition.Resolvers = {0}Resolvers.{1}_{2}();", - typeInfo.Type.ToFullyQualified(), - typeInfo.Type.Name, - resolver.Member.Name); - - if (resolver.ResultKind is not ResolverResultKind.Pure - && !resolver.Member.HasPostProcessorAttribute() - && resolver.Member.IsListType(out var elementType)) - { - _writer.WriteIndentedLine( - "c.Definition.ResultPostProcessor = global::{0}<{1}>.Default;", - WellKnownTypes.ListPostProcessor, - elementType); - } - } - - _writer.WriteIndentedLine("});"); - } - - if (resolver.Bindings.Length > 0) - { - foreach (var binding in resolver.Bindings) - { - _writer.WriteLine(); - _writer.WriteIndentedLine("descriptor"); - - using (_writer.IncreaseIndent()) - { - if (binding.Kind is MemberBindingKind.Property) - { - _writer.WriteIndentedLine( - ".Field(runtimeType.GetMember(\"{0}\", runtimeBindingFlags)[0])", - binding.Name); - _writer.WriteIndentedLine(".Ignore();"); - } - else if (binding.Kind is MemberBindingKind.Property) - { - _writer.WriteIndentedLine(".Field(\"{0}\")", binding.Name); - _writer.WriteIndentedLine(".Ignore();"); - } - } - } - } - } - } - } - - public void WriteConfigureMethod(IOutputTypeInfo typeInfo) - { - if (typeInfo.RuntimeType is null) - { - _writer.WriteIndentedLine( - "static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor);"); - } - else - { - _writer.WriteIndentedLine( - "static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor<{0}> descriptor);", - typeInfo.RuntimeType.ToFullyQualified()); - } - } -} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ObjectTypeFileBuilder.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ObjectTypeFileBuilder.cs new file mode 100644 index 00000000000..5156fd43523 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ObjectTypeFileBuilder.cs @@ -0,0 +1,138 @@ +using System.Text; +using HotChocolate.Types.Analyzers.Generators; +using HotChocolate.Types.Analyzers.Models; + +namespace HotChocolate.Types.Analyzers.FileBuilders; + +public sealed class ObjectTypeFileBuilder(StringBuilder sb) : TypeFileBuilderBase(sb) +{ + public override void WriteInitializeMethod(IOutputTypeInfo type) + { + if (type is not ObjectTypeInfo objectType) + { + throw new InvalidOperationException( + "The specified type is not an object type."); + } + + Writer.WriteIndentedLine( + "internal static void Initialize(global::{0} descriptor)", + WellKnownTypes.IObjectTypeDescriptor, + objectType.RuntimeTypeFullName); + + Writer.WriteIndentedLine("{"); + + using (Writer.IncreaseIndent()) + { + if (objectType.Resolvers.Length > 0 || objectType.NodeResolver is not null) + { + Writer.WriteIndentedLine( + "var thisType = typeof(global::{0});", + objectType.SchemaTypeFullName); + Writer.WriteIndentedLine( + "var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver;"); + Writer.WriteIndentedLine( + objectType.Resolvers.Any(t => t.RequiresParameterBindings) + || (objectType.NodeResolver?.RequiresParameterBindings ?? false) + ? "var resolvers = new __Resolvers(bindingResolver);" + : "var resolvers = new __Resolvers();"); + } + + if (objectType.NodeResolver is not null) + { + Writer.WriteLine(); + Writer.WriteIndentedLine("descriptor"); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine(".ImplementsNode()"); + Writer.WriteIndentedLine( + ".ResolveNode(resolvers.{0}().Resolver!);", + objectType.NodeResolver.Member.Name); + } + } + + WriteResolverBindings(objectType); + + Writer.WriteLine(); + Writer.WriteIndentedLine("Configure(descriptor);"); + } + + Writer.WriteIndentedLine("}"); + Writer.WriteLine(); + } + + public override void WriteResolverFields(IOutputTypeInfo type) + { + if (type is not ObjectTypeInfo objectType) + { + throw new InvalidOperationException( + "The specified type is not an object type."); + } + + base.WriteResolverFields(objectType); + + if (objectType.NodeResolver is not null) + { + if (objectType.NodeResolver.RequiresParameterBindings) + { + WriteResolverField(objectType.NodeResolver); + } + } + } + + public override void WriteResolverConstructor(IOutputTypeInfo type, ILocalTypeLookup typeLookup) + { + if (type is not ObjectTypeInfo objectType) + { + throw new InvalidOperationException( + "The specified type is not an object type."); + } + + WriteResolverConstructor( + objectType, + typeLookup, + $"{objectType.SchemaTypeFullName}", + type.Resolvers.Any(t => t.RequiresParameterBindings) + || (objectType.NodeResolver?.RequiresParameterBindings ?? false)); + } + + protected override void WriteResolversBindingInitialization(IOutputTypeInfo type, ILocalTypeLookup typeLookup) + { + if (type is not ObjectTypeInfo objectType) + { + throw new InvalidOperationException( + "The specified type is not an object type."); + } + + base.WriteResolversBindingInitialization(objectType, typeLookup); + + if (objectType.NodeResolver is not null) + { + if (objectType.NodeResolver.RequiresParameterBindings) + { + WriteResolverBindingInitialization(objectType.NodeResolver, typeLookup); + } + } + } + + public override void WriteResolverMethods(IOutputTypeInfo type, ILocalTypeLookup typeLookup) + { + if (type is not ObjectTypeInfo objectType) + { + throw new InvalidOperationException( + "The specified type is not an object type."); + } + + base.WriteResolverMethods(objectType, typeLookup); + + + if (objectType.NodeResolver is not null) + { + if (objectType.Resolvers.Length > 0) + { + Writer.WriteLine(); + } + + WriteResolver(objectType.NodeResolver, typeLookup); + } + } +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ResolverFileBuilder.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ResolverFileBuilder.cs deleted file mode 100644 index fd2952dc14d..00000000000 --- a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ResolverFileBuilder.cs +++ /dev/null @@ -1,771 +0,0 @@ -using System.Collections.Immutable; -using System.Text; -using HotChocolate.Types.Analyzers.Generators; -using HotChocolate.Types.Analyzers.Helpers; -using HotChocolate.Types.Analyzers.Models; -using Microsoft.CodeAnalysis; - -namespace HotChocolate.Types.Analyzers.FileBuilders; - -public sealed class ResolverFileBuilder(StringBuilder sb) -{ - private readonly CodeWriter _writer = new(sb); - - public void WriteHeader() - { - _writer.WriteFileHeader(); - _writer.WriteIndentedLine("using HotChocolate.Internal;"); - _writer.WriteLine(); - } - - public void WriteBeginNamespace(string ns) - { - _writer.WriteIndentedLine("namespace {0}", ns); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - } - - public void WriteEndNamespace() - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - _writer.WriteLine(); - } - - public string WriteBeginClass(string typeName) - { - _writer.WriteIndentedLine("internal static class {0}", typeName); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - _writer.WriteIndentedLine("private static readonly object _sync = new object();"); - _writer.WriteIndentedLine("private static bool _bindingsInitialized;"); - return typeName; - } - - public void WriteEndClass() - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - } - - public bool AddResolverDeclarations(ImmutableArray resolvers) - { - var any = false; - - foreach (var resolver in resolvers) - { - if (resolver.Parameters.Length == 0) - { - continue; - } - - any = true; - - _writer.WriteIndentedLine( - "private readonly static global::{0}[] _args_{1}_{2} = new global::{0}[{3}];", - WellKnownTypes.ParameterBinding, - resolver.TypeName, - resolver.Member.Name, - resolver.Parameters.Length); - } - - return any; - } - - public void AddParameterInitializer(IEnumerable resolvers, ILocalTypeLookup typeLookup) - { - _writer.WriteIndentedLine( - "public static void InitializeBindings(global::{0} bindingResolver)", - WellKnownTypes.ParameterBindingResolver); - _writer.WriteIndentedLine("{"); - using (_writer.IncreaseIndent()) - { - var first = true; - foreach (var resolver in resolvers) - { - if (resolver.Parameters.Length == 0) - { - continue; - } - - if (resolver.Member is not IMethodSymbol method) - { - continue; - } - - if (first) - { - _writer.WriteIndentedLine("if (!_bindingsInitialized)"); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - - _writer.WriteIndentedLine("lock (_sync)"); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - - _writer.WriteIndentedLine("if (!_bindingsInitialized)"); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - - _writer.WriteIndentedLine( - "const global::{0} bindingFlags =", - WellKnownTypes.BindingFlags); - _writer.WriteIndentedLine( - " global::{0}.Public", - WellKnownTypes.BindingFlags); - _writer.WriteIndentedLine( - " | global::{0}.NonPublic", - WellKnownTypes.BindingFlags); - _writer.WriteIndentedLine( - " | global::{0}.Static;", - WellKnownTypes.BindingFlags); - _writer.WriteLine(); - _writer.WriteIndentedLine("var type = typeof({0});", method.ContainingType.ToFullyQualified()); - _writer.WriteIndentedLine("global::System.Reflection.MethodInfo resolver = default!;"); - _writer.WriteIndentedLine("global::System.Reflection.ParameterInfo[] parameters = default!;"); - - _writer.WriteIndentedLine("_bindingsInitialized = true;"); - first = false; - } - - _writer.WriteLine(); - _writer.WriteIndentedLine("resolver = type.GetMethod("); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("\"{0}\",", resolver.Member.Name); - _writer.WriteIndentedLine("bindingFlags,"); - if (resolver.Parameters.Length == 0) - { - _writer.WriteIndentedLine("global::System.Array.Empty());"); - } - else - { - _writer.WriteIndentedLine("new global::System.Type[]"); - _writer.WriteIndentedLine("{"); - using (_writer.IncreaseIndent()) - { - for (var i = 0; i < resolver.Parameters.Length; i++) - { - var parameter = resolver.Parameters[i]; - - if (i > 0) - { - _writer.Write(','); - _writer.WriteLine(); - } - - _writer.WriteIndented( - "typeof({0})", - ToFullyQualifiedString(parameter.Type, method, typeLookup)); - } - } - - _writer.WriteLine(); - _writer.WriteIndentedLine("})!;"); - } - } - - _writer.WriteIndentedLine("parameters = resolver.GetParameters();"); - - for (var i = 0; i < method.Parameters.Length; i++) - { - _writer.WriteIndentedLine( - "_args_{0}_{1}[{2}] = bindingResolver.GetBinding(parameters[{2}]);", - resolver.TypeName, - resolver.Member.Name, - i); - } - - if (resolver.IsNodeResolver) - { - _writer.WriteLine(); - - _writer.WriteIndentedLine("int argumentCount = 0;"); - - _writer.WriteLine(); - - using (_writer.WriteForEach("binding", $"_args_{resolver.TypeName}_{resolver.Member.Name}")) - { - using (_writer.WriteIfClause( - "binding.Kind == global::{0}.Argument", - WellKnownTypes.ArgumentKind)) - { - _writer.WriteIndentedLine("argumentCount++;"); - } - } - - _writer.WriteLine(); - - using (_writer.WriteIfClause("argumentCount > 1")) - { - _writer.WriteIndentedLine( - "throw new global::{0}(", - WellKnownTypes.SchemaException); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine( - "global::{0}.New()", - WellKnownTypes.SchemaErrorBuilder); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine( - ".SetMessage(\"The node resolver `{0}.{1}` mustn't have more than one " - + "argument. Node resolvers can only have a single argument called `id`.\")", - resolver.Member.ContainingType.ToDisplayString(), - resolver.Member.Name); - _writer.WriteIndentedLine(".Build());"); - } - } - } - } - } - - if (!first) - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - } - } - - _writer.WriteIndentedLine("}"); - } - - private static string ToFullyQualifiedString( - ITypeSymbol type, - IMethodSymbol resolverMethod, - ILocalTypeLookup typeLookup) - { - if (type.TypeKind is TypeKind.Error - && typeLookup.TryGetTypeName(type, resolverMethod, out var typeDisplayName)) - { - return typeDisplayName; - } - - return type.ToFullyQualified(); - } - - public void AddResolver(Resolver resolver, ILocalTypeLookup typeLookup) - { - if (resolver.Member is IMethodSymbol resolverMethod) - { - switch (resolver.ResultKind) - { - case ResolverResultKind.Invalid: - return; - - case ResolverResultKind.Pure: - AddStaticPureResolver(resolver, resolverMethod, typeLookup); - return; - - case ResolverResultKind.Task: - case ResolverResultKind.TaskAsyncEnumerable: - AddStaticStandardResolver(resolver, true, resolverMethod, typeLookup); - return; - - case ResolverResultKind.Executable: - case ResolverResultKind.Queryable: - case ResolverResultKind.AsyncEnumerable: - AddStaticStandardResolver(resolver, false, resolverMethod, typeLookup); - return; - } - } - - AddStaticPropertyResolver(resolver); - } - - private void AddStaticStandardResolver( - Resolver resolver, - bool async, - IMethodSymbol resolverMethod, - ILocalTypeLookup typeLookup) - { - using (_writer.WriteMethod( - "public static", - returnType: WellKnownTypes.FieldResolverDelegates, - methodName: $"{resolver.TypeName}_{resolver.Member.Name}")) - { - using (_writer.WriteIfClause(condition: "!_bindingsInitialized")) - { - _writer.WriteIndentedLine( - "throw new global::{0}(\"The bindings must be initialized before the resolvers can be created.\");", - WellKnownTypes.InvalidOperationException); - } - - _writer.WriteLine(); - - _writer.WriteIndentedLine( - "return new global::{0}(resolver: {1}_{2}_Resolver);", - WellKnownTypes.FieldResolverDelegates, - resolver.TypeName, - resolver.Member.Name); - } - - _writer.WriteLine(); - - _writer.WriteIndented("private static "); - - if (async) - { - _writer.Write("async "); - } - - _writer.WriteLine( - "global::{0} {2}_{3}_Resolver(global::{4} context)", - WellKnownTypes.ValueTask, - WellKnownTypes.Object, - resolver.TypeName, - resolver.Member.Name, - WellKnownTypes.ResolverContext); - _writer.WriteIndentedLine("{"); - using (_writer.IncreaseIndent()) - { - AddResolverArguments(resolver, resolverMethod, typeLookup); - - if (async) - { - _writer.WriteIndentedLine( - "var result = await {0}.{1}({2});", - resolver.Member.ContainingType.ToFullyQualified(), - resolver.Member.Name, - GetResolverArguments(resolver.Parameters.Length)); - - _writer.WriteIndentedLine("return result;"); - } - else - { - _writer.WriteIndentedLine( - "var result = {0}.{1}({2});", - resolver.Member.ContainingType.ToFullyQualified(), - resolver.Member.Name, - GetResolverArguments(resolver.Parameters.Length)); - - _writer.WriteIndentedLine( - "return new global::{0}(result);", - WellKnownTypes.ValueTask, - WellKnownTypes.Object); - } - } - - _writer.WriteIndentedLine("}"); - } - - private void AddStaticPureResolver(Resolver resolver, IMethodSymbol resolverMethod, ILocalTypeLookup typeLookup) - { - using (_writer.WriteMethod( - "public static", - returnType: WellKnownTypes.FieldResolverDelegates, - methodName: $"{resolver.TypeName}_{resolver.Member.Name}")) - { - using (_writer.WriteIfClause(condition: "!_bindingsInitialized")) - { - _writer.WriteIndentedLine( - "throw new global::{0}(\"The bindings must be initialized before the resolvers can be created.\");", - WellKnownTypes.InvalidOperationException); - } - - if (resolver.Parameters.Length > 0 && resolver.Parameters.Any(t => t.Kind == ResolverParameterKind.Unknown)) - { - _writer.WriteLine(); - - var firstParam = true; - _writer.WriteIndented("var isPure = "); - - for (var i = 0; i < resolver.Parameters.Length; i++) - { - var parameter = resolver.Parameters[i]; - if (parameter.Kind == ResolverParameterKind.Unknown) - { - if (!firstParam) - { - _writer.Write(" && "); - } - - firstParam = false; - - _writer.Write( - "_args_{0}_{1}[{2}].IsPure", - resolver.TypeName, - resolver.Member.Name, - i); - } - } - - _writer.WriteLine(";"); - - _writer.WriteLine(); - - _writer.WriteIndentedLine("return isPure"); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine( - "? new global::{0}(pureResolver: {1}_{2}_Resolver)", - WellKnownTypes.FieldResolverDelegates, - resolver.TypeName, - resolver.Member.Name); - _writer.WriteIndentedLine( - ": new global::{0}(resolver: c => new({1}_{2}_Resolver(c)));", - WellKnownTypes.FieldResolverDelegates, - resolver.TypeName, - resolver.Member.Name); - } - } - else - { - _writer.WriteIndentedLine( - "return new global::{0}(pureResolver: {1}_{2}_Resolver);", - WellKnownTypes.FieldResolverDelegates, - resolver.TypeName, - resolver.Member.Name); - } - } - - _writer.WriteLine(); - - _writer.WriteIndentedLine( - "private static global::{0}? {1}_{2}_Resolver(global::{3} context)", - WellKnownTypes.Object, - resolver.TypeName, - resolver.Member.Name, - WellKnownTypes.ResolverContext); - _writer.WriteIndentedLine("{"); - using (_writer.IncreaseIndent()) - { - AddResolverArguments(resolver, resolverMethod, typeLookup); - - _writer.WriteIndentedLine( - "var result = {0}.{1}({2});", - resolver.Member.ContainingType.ToFullyQualified(), - resolver.Member.Name, - GetResolverArguments(resolver.Parameters.Length)); - - _writer.WriteIndentedLine("return result;"); - } - - _writer.WriteIndentedLine("}"); - } - - private void AddStaticPropertyResolver(Resolver resolver) - { - using (_writer.WriteMethod( - "public static", - returnType: WellKnownTypes.FieldResolverDelegates, - methodName: $"{resolver.TypeName}_{resolver.Member.Name}")) - { - using (_writer.WriteIfClause(condition: "!_bindingsInitialized")) - { - _writer.WriteIndentedLine( - "throw new global::{0}(\"The bindings must be initialized before the resolvers can be created.\");", - WellKnownTypes.InvalidOperationException); - } - - _writer.WriteLine(); - - _writer.WriteIndentedLine( - "return new global::{0}(pureResolver: {1}_{2}_Resolver);", - WellKnownTypes.FieldResolverDelegates, - resolver.TypeName, - resolver.Member.Name); - } - - _writer.WriteLine(); - - _writer.WriteIndentedLine( - "private static global::{0}? {1}_{2}_Resolver(global::{3} context)", - WellKnownTypes.Object, - resolver.TypeName, - resolver.Member.Name, - WellKnownTypes.ResolverContext); - _writer.WriteIndentedLine("{"); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine( - "var result = {0}.{1};", - resolver.Member.ContainingType.ToFullyQualified(), - resolver.Member.Name); - - _writer.WriteIndentedLine("return result;"); - } - - _writer.WriteIndentedLine("}"); - } - - private void AddResolverArguments(Resolver resolver, IMethodSymbol resolverMethod, ILocalTypeLookup typeLookup) - { - if (resolver.Parameters.Length == 0) - { - return; - } - - for (var i = 0; i < resolver.Parameters.Length; i++) - { - var parameter = resolver.Parameters[i]; - - if (resolver.IsNodeResolver - && parameter.Kind is ResolverParameterKind.Argument or ResolverParameterKind.Unknown - && (parameter.Name == "id" || parameter.Key == "id")) - { - _writer.WriteIndentedLine( - "var args{0} = context.GetLocalState<{1}>(" - + "global::HotChocolate.WellKnownContextData.InternalId);", - i, - parameter.Type.ToFullyQualified()); - continue; - } - - switch (parameter.Kind) - { - case ResolverParameterKind.Parent: - _writer.WriteIndentedLine( - "var args{0} = context.Parent<{1}>();", - i, - resolver.Parameters[i].Type.ToFullyQualified()); - break; - - case ResolverParameterKind.CancellationToken: - _writer.WriteIndentedLine("var args{0} = context.RequestAborted;", i); - break; - - case ResolverParameterKind.ClaimsPrincipal: - _writer.WriteIndentedLine( - "var args{0} = context.GetGlobalState<{1}>(\"ClaimsPrincipal\");", - i, - WellKnownTypes.ClaimsPrincipal); - break; - - case ResolverParameterKind.DocumentNode: - _writer.WriteIndentedLine("var args{0} = context.Operation.Document;", i); - break; - - case ResolverParameterKind.EventMessage: - _writer.WriteIndentedLine( - "var args{0} = context.GetScopedState<{1}>(" - + "global::HotChocolate.WellKnownContextData.EventMessage);", - i, - parameter.Type.ToFullyQualified()); - break; - - case ResolverParameterKind.FieldNode: - _writer.WriteIndentedLine( - "var args{0} = context.Selection.SyntaxNode", - i, - parameter.Type.ToFullyQualified()); - break; - - case ResolverParameterKind.OutputField: - _writer.WriteIndentedLine( - "var args{0} = context.Selection.Field", - i, - parameter.Type.ToFullyQualified()); - break; - - case ResolverParameterKind.HttpContext: - _writer.WriteIndentedLine( - "var args{0} = context.GetGlobalState(nameof(global::{1}))!;", - i, - WellKnownTypes.HttpContext); - break; - - case ResolverParameterKind.HttpRequest: - _writer.WriteIndentedLine( - "var args{0} = context.GetGlobalState(nameof(global::{1}))?.Request!;", - i, - WellKnownTypes.HttpContext); - break; - - case ResolverParameterKind.HttpResponse: - _writer.WriteIndentedLine( - "var args{0} = context.GetGlobalState(nameof(global::{1}))?.Response!;", - i, - WellKnownTypes.HttpContext); - break; - - case ResolverParameterKind.GetGlobalState when parameter.Parameter.HasExplicitDefaultValue: - { - var defaultValue = parameter.Parameter.ExplicitDefaultValue; - var defaultValueString = GeneratorUtils.ConvertDefaultValueToString(defaultValue, parameter.Type); - - _writer.WriteIndentedLine( - "var args{0} = context.GetGlobalStateOrDefault<{1}{2}>(\"{3}\", {4});", - i, - parameter.Type.ToFullyQualified(), - parameter.Type.IsNullableRefType() ? "?" : string.Empty, - parameter.Key, - defaultValueString); - break; - } - - case ResolverParameterKind.GetGlobalState when !parameter.IsNullable: - _writer.WriteIndentedLine( - "var args{0} = context.GetGlobalState<{1}>(\"{2}\");", - i, - parameter.Type.ToFullyQualified(), - parameter.Key); - break; - - case ResolverParameterKind.GetGlobalState: - _writer.WriteIndentedLine( - "var args{0} = context.GetGlobalStateOrDefault<{1}>(\"{2}\");", - i, - parameter.Type.ToFullyQualified(), - parameter.Key); - break; - - case ResolverParameterKind.SetGlobalState: - _writer.WriteIndentedLine( - "var args{0} = new HotChocolate.SetState<{1}>(" - + "value => context.SetGlobalState(\"{2}\", value));", - i, - ((INamedTypeSymbol)parameter.Type).TypeArguments[0].ToFullyQualified(), - parameter.Key); - break; - - case ResolverParameterKind.GetScopedState when parameter.Parameter.HasExplicitDefaultValue: - { - var defaultValue = parameter.Parameter.ExplicitDefaultValue; - var defaultValueString = GeneratorUtils.ConvertDefaultValueToString(defaultValue, parameter.Type); - - _writer.WriteIndentedLine( - "var args{0} = context.GetScopedStateOrDefault<{1}{2}>(\"{3}\", {4});", - i, - parameter.Type.ToFullyQualified(), - parameter.Type.IsNullableRefType() ? "?" : string.Empty, - parameter.Key, - defaultValueString); - break; - } - - case ResolverParameterKind.GetScopedState when !parameter.IsNullable: - _writer.WriteIndentedLine( - "var args{0} = context.GetScopedState<{1}>(\"{2}\");", - i, - parameter.Type.ToFullyQualified(), - parameter.Key); - break; - - case ResolverParameterKind.GetScopedState: - _writer.WriteIndentedLine( - "var args{0} = context.GetScopedStateOrDefault<{1}>(\"{2}\");", - i, - parameter.Type.ToFullyQualified(), - parameter.Key); - break; - - case ResolverParameterKind.SetScopedState: - _writer.WriteIndentedLine( - "var args{0} = new HotChocolate.SetState<{1}>(" - + "value => context.SetScopedState(\"{2}\", value));", - i, - ((INamedTypeSymbol)parameter.Type).TypeArguments[0].ToFullyQualified(), - parameter.Key); - break; - - case ResolverParameterKind.GetLocalState when parameter.Parameter.HasExplicitDefaultValue: - { - var defaultValue = parameter.Parameter.ExplicitDefaultValue; - var defaultValueString = GeneratorUtils.ConvertDefaultValueToString(defaultValue, parameter.Type); - - _writer.WriteIndentedLine( - "var args{0} = context.GetLocalStateOrDefault<{1}{2}>(\"{3}\", {4});", - i, - parameter.Type.ToFullyQualified(), - parameter.Type.IsNullableRefType() ? "?" : string.Empty, - parameter.Key, - defaultValueString); - break; - } - - case ResolverParameterKind.GetLocalState when !parameter.IsNullable: - _writer.WriteIndentedLine( - "var args{0} = context.GetLocalState<{1}>(\"{2}\");", - i, - parameter.Type.ToFullyQualified(), - parameter.Key); - break; - - case ResolverParameterKind.GetLocalState: - _writer.WriteIndentedLine( - "var args{0} = context.GetLocalStateOrDefault<{1}>(\"{2}\");", - i, - parameter.Type.ToFullyQualified(), - parameter.Key); - break; - - case ResolverParameterKind.SetLocalState: - _writer.WriteIndentedLine( - "var args{0} = new HotChocolate.SetState<{1}>(" - + "value => context.SetLocalState(\"{2}\", value));", - i, - ((INamedTypeSymbol)parameter.Type).TypeArguments[0].ToFullyQualified(), - parameter.Key); - break; - - case ResolverParameterKind.Service: - case ResolverParameterKind.Argument: - case ResolverParameterKind.Unknown: - _writer.WriteIndentedLine( - "var args{0} = _args_{1}_{2}[{0}].Execute<{3}>(context);", - i, - resolver.TypeName, - resolver.Member.Name, - ToFullyQualifiedString(parameter.Type, resolverMethod, typeLookup)); - break; - - case ResolverParameterKind.QueryContext: - var entityType = parameter.TypeParameters[0].ToFullyQualified(); - _writer.WriteIndentedLine("var args{0}_selection = context.Selection;", i); - _writer.WriteIndentedLine("var args{0}_filter = {1}.GetFilterContext(context);", - i, - WellKnownTypes.FilterContextResolverContextExtensions); - _writer.WriteIndentedLine("var args{0}_sorting = {1}.GetSortingContext(context);", - i, - WellKnownTypes.SortingContextResolverContextExtensions); - _writer.WriteIndentedLine( - "var args{0} = new global::{1}<{2}>(", - i, - WellKnownTypes.QueryContext, - entityType); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine( - "{0}.AsSelector<{1}>(args{2}_selection),", - WellKnownTypes.HotChocolateExecutionSelectionExtensions, - entityType, - i); - _writer.WriteIndentedLine("args{0}_filter?.AsPredicate<{1}>(),", i, entityType); - _writer.WriteIndentedLine("args{0}_sorting?.AsSortDefinition<{1}>());", i, entityType); - } - - break; - - default: - throw new ArgumentOutOfRangeException(); - } - } - } - - private static string GetResolverArguments(int parameterCount) - { - if (parameterCount == 0) - { - return string.Empty; - } - - var arguments = new StringBuilder(); - - for (var i = 0; i < parameterCount; i++) - { - if (i > 0) - { - arguments.Append(", "); - } - - arguments.Append($"args{i}"); - } - - return arguments.ToString(); - } -} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/RootTypeFileBuilder.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/RootTypeFileBuilder.cs new file mode 100644 index 00000000000..7eeb106440e --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/RootTypeFileBuilder.cs @@ -0,0 +1,47 @@ +using System.Text; +using HotChocolate.Types.Analyzers.Helpers; +using HotChocolate.Types.Analyzers.Models; + +namespace HotChocolate.Types.Analyzers.FileBuilders; + +public sealed class RootTypeFileBuilder(StringBuilder sb) : TypeFileBuilderBase(sb) +{ + public override void WriteInitializeMethod(IOutputTypeInfo type) + { + if (type is not RootTypeInfo rootType) + { + throw new InvalidOperationException( + "The specified type is not an object type."); + } + + Writer.WriteIndentedLine( + "internal static void Initialize(global::{0} descriptor)", + WellKnownTypes.IObjectTypeDescriptor); + + Writer.WriteIndentedLine("{"); + + using (Writer.IncreaseIndent()) + { + if (rootType.Resolvers.Length > 0) + { + Writer.WriteIndentedLine( + "var thisType = typeof({0});", + rootType.SchemaSchemaType.ToFullyQualified()); + Writer.WriteIndentedLine( + "var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver;"); + Writer.WriteIndentedLine( + rootType.Resolvers.Any(t => t.RequiresParameterBindings) + ? "var resolvers = new __Resolvers(bindingResolver);" + : "var resolvers = new __Resolvers();"); + } + + WriteResolverBindings(rootType); + + Writer.WriteLine(); + Writer.WriteIndentedLine("Configure(descriptor);"); + } + + Writer.WriteIndentedLine("}"); + Writer.WriteLine(); + } +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs new file mode 100644 index 00000000000..671da85d5e7 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs @@ -0,0 +1,1006 @@ +using System.Runtime.InteropServices.Marshalling; +using System.Text; +using HotChocolate.Types.Analyzers.Generators; +using HotChocolate.Types.Analyzers.Helpers; +using HotChocolate.Types.Analyzers.Models; +using Microsoft.CodeAnalysis; + +namespace HotChocolate.Types.Analyzers.FileBuilders; + +public abstract class TypeFileBuilderBase(StringBuilder sb) +{ + public CodeWriter Writer { get; } = new(sb); + + public void WriteHeader() + { + Writer.WriteFileHeader(); + Writer.WriteIndentedLine("using Microsoft.Extensions.DependencyInjection;"); + Writer.WriteIndentedLine("using HotChocolate.Internal;"); + Writer.WriteLine(); + } + + public void WriteBeginNamespace(IOutputTypeInfo type) + { + Writer.WriteIndentedLine("namespace {0}", type.Namespace); + Writer.WriteIndentedLine("{"); + Writer.IncreaseIndent(); + } + + public void WriteEndNamespace() + { + Writer.DecreaseIndent(); + Writer.WriteIndentedLine("}"); + Writer.WriteLine(); + } + + public virtual void WriteBeginClass(IOutputTypeInfo type) + { + Writer.WriteIndentedLine( + "{0} static partial class {1}", + type.IsPublic ? "public" : "internal", + type.Name); + Writer.WriteIndentedLine("{"); + Writer.IncreaseIndent(); + } + + public void WriteEndClass() + { + Writer.DecreaseIndent(); + Writer.WriteIndentedLine("}"); + } + + public abstract void WriteInitializeMethod(IOutputTypeInfo type); + + protected virtual void WriteResolverBindings(IOutputTypeInfo type) + { + if (type.Resolvers.Length == 0) + { + return; + } + + if (type.Resolvers.Any(t => t.Bindings.Length > 0)) + { + Writer.WriteLine(); + Writer.WriteIndentedLine("var naming = descriptor.Extend().Context.Naming;"); + Writer.WriteIndentedLine("var ignoredFields = new global::System.Collections.Generic.HashSet();"); + + foreach (var binding in type.Resolvers.SelectMany(t => t.Bindings)) + { + if (binding.Kind is MemberBindingKind.Field) + { + Writer.WriteIndentedLine( + "ignoredFields.Add(\"{0}\");", + binding.Name); + } + else if (binding.Kind is MemberBindingKind.Property) + { + Writer.WriteIndentedLine( + "ignoredFields.Add(naming.GetMemberName(\"{0}\", global::{1}.ObjectField));", + binding.Name, + "HotChocolate.Types.MemberKind"); + } + } + + Writer.WriteLine(); + Writer.WriteIndentedLine("foreach(string fieldName in ignoredFields)"); + Writer.WriteIndentedLine("{"); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine("descriptor.Field(fieldName).Ignore();"); + } + + Writer.WriteIndentedLine("}"); + } + + foreach (var resolver in type.Resolvers) + { + Writer.WriteLine(); + Writer.WriteIndentedLine("descriptor"); + + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine( + ".Field(thisType.GetMember(\"{0}\", global::{1})[0])", + resolver.Member.Name, + resolver.IsStatic + ? WellKnownTypes.StaticMemberFlags + : WellKnownTypes.InstanceMemberFlags); + + if (resolver.Kind is ResolverKind.ConnectionResolver) + { + Writer.WriteIndentedLine( + ".AddPagingArguments()"); + } + + WriteResolverBindingDescriptor(type, resolver); + + Writer.WriteIndentedLine(".ExtendWith(static (c, r) =>"); + Writer.WriteIndentedLine("{"); + using (Writer.IncreaseIndent()) + { + WriteResolverBindingExtendsWith(type, resolver); + } + + Writer.WriteIndentedLine("},"); + + Writer.WriteIndentedLine("resolvers);"); + } + } + } + + protected virtual void WriteResolverBindingDescriptor(IOutputTypeInfo type, Resolver resolver) + { + if (!string.IsNullOrEmpty(resolver.SchemaTypeName)) + { + Writer.WriteIndentedLine( + ".Type<{0}>()", + resolver.UnwrappedReturnType.IsNullableType() + ? $"global::{resolver.SchemaTypeName}" + : $"global::{WellKnownTypes.NonNullType}"); + } + } + + protected virtual void WriteResolverBindingExtendsWith(IOutputTypeInfo type, Resolver resolver) + { + WriteFieldFlags(resolver); + + if (resolver.Kind is ResolverKind.ConnectionResolver) + { + Writer.WriteIndentedLine( + "var pagingOptions = global::{0}.GetPagingOptions(c.Context, null);", + WellKnownTypes.PagingHelper); + Writer.WriteIndentedLine( + "c.Definition.State = c.Definition.State.SetItem(" + + "HotChocolate.WellKnownContextData.PagingOptions, pagingOptions);"); + Writer.WriteIndentedLine( + "c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = " + + "pagingOptions;"); + } + + Writer.WriteIndentedLine( + "c.Definition.Resolvers = r.{0}();", + resolver.Member.Name); + + if (resolver.ResultKind is not ResolverResultKind.Pure + && !resolver.Member.HasPostProcessorAttribute() + && resolver.Member.IsListType(out var elementType)) + { + Writer.WriteIndentedLine( + "c.Definition.ResultPostProcessor = global::{0}<{1}>.Default;", + WellKnownTypes.ListPostProcessor, + elementType); + } + } + + protected void WriteFieldFlags(Resolver resolver) + { + Writer.WriteIndentedLine("c.Definition.SetSourceGeneratorFlags();"); + + if (resolver.Kind is ResolverKind.ConnectionResolver) + { + Writer.WriteIndentedLine("c.Definition.SetConnectionFlags();"); + } + + if ((resolver.Flags & FieldFlags.ConnectionEdgesField) == FieldFlags.ConnectionEdgesField) + { + Writer.WriteIndentedLine("c.Definition.SetConnectionEdgesFieldFlags();"); + } + + if ((resolver.Flags & FieldFlags.ConnectionNodesField) == FieldFlags.ConnectionNodesField) + { + Writer.WriteIndentedLine("c.Definition.SetConnectionNodesFieldFlags();"); + } + + if ((resolver.Flags & FieldFlags.TotalCount) == FieldFlags.TotalCount) + { + Writer.WriteIndentedLine("c.Definition.SetConnectionTotalCountFieldFlags();"); + } + } + + public virtual void WriteConfigureMethod(IOutputTypeInfo type) + { + if (type.RuntimeType is null) + { + Writer.WriteIndentedLine( + "static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor);"); + } + else + { + Writer.WriteIndentedLine( + "static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor<{0}> descriptor);", + type.RuntimeType.ToFullyQualified()); + } + + Writer.WriteLine(); + } + + public void WriteBeginResolverClass() + { + Writer.WriteIndentedLine("private sealed class __Resolvers"); + Writer.WriteIndentedLine("{"); + Writer.IncreaseIndent(); + } + + public void WriteEndResolverClass() + { + Writer.DecreaseIndent(); + Writer.WriteIndentedLine("}"); + } + + public virtual void WriteResolverFields(IOutputTypeInfo type) + { + foreach (var resolver in type.Resolvers) + { + if (resolver.RequiresParameterBindings) + { + WriteResolverField(resolver); + } + } + } + + protected void WriteResolverField(Resolver resolver) + { + if (resolver.RequiresParameterBindings) + { + Writer.WriteIndentedLine( + "private readonly global::{0}[] _args_{1} = new global::{0}[{2}];", + WellKnownTypes.ParameterBinding, + resolver.Member.Name, + resolver.Parameters.Count(t => t.RequiresBinding)); + } + } + + public virtual void WriteResolverConstructor(IOutputTypeInfo type, ILocalTypeLookup typeLookup) + { + WriteResolverConstructor( + type, + typeLookup, + type.Resolvers[0].Member.ContainingType.ToFullyQualified(), + type.Resolvers.Any(t => t.RequiresParameterBindings)); + } + + protected void WriteResolverConstructor( + IOutputTypeInfo type, + ILocalTypeLookup typeLookup, + string resolverTypeName, + bool requiresParameterBindings) + { + if (!requiresParameterBindings) + { + return; + } + + Writer.WriteLine(); + Writer.WriteIndentedLine( + "public __Resolvers(global::{0} bindingResolver)", + WellKnownTypes.ParameterBindingResolver); + Writer.WriteIndentedLine("{"); + + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine("var type = typeof({0});", resolverTypeName); + Writer.WriteIndentedLine("global::System.Reflection.MethodInfo resolver = default!;"); + Writer.WriteIndentedLine("global::System.Reflection.ParameterInfo[] parameters = default!;"); + + WriteResolversBindingInitialization(type, typeLookup); + } + + Writer.WriteIndentedLine("}"); + Writer.WriteLine(); + } + + protected virtual void WriteResolversBindingInitialization(IOutputTypeInfo type, ILocalTypeLookup typeLookup) + { + foreach (var resolver in type.Resolvers) + { + WriteResolverBindingInitialization(resolver, typeLookup); + } + } + + protected void WriteResolverBindingInitialization(Resolver resolver, ILocalTypeLookup typeLookup) + { + if (resolver.Member is not IMethodSymbol resolverMethod) + { + return; + } + + if (!resolver.RequiresParameterBindings) + { + return; + } + + Writer.WriteLine(); + Writer.WriteIndentedLine("resolver = type.GetMethod("); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine( + "\"{0}\",", + resolver.Member.Name); + Writer.WriteIndentedLine( + "global::{0},", + resolver.IsStatic + ? WellKnownTypes.StaticMemberFlags + : WellKnownTypes.InstanceMemberFlags); + if (resolver.Parameters.Length == 0) + { + Writer.WriteIndentedLine("global::System.Array.Empty());"); + } + else + { + Writer.WriteIndentedLine("new global::System.Type[]"); + Writer.WriteIndentedLine("{"); + using (Writer.IncreaseIndent()) + { + for (var i = 0; i < resolver.Parameters.Length; i++) + { + var parameter = resolver.Parameters[i]; + + if (i > 0) + { + Writer.Write(','); + Writer.WriteLine(); + } + + Writer.WriteIndented( + "typeof({0})", + ToFullyQualifiedString(parameter.Type, resolverMethod, typeLookup)); + } + } + + Writer.WriteLine(); + Writer.WriteIndentedLine("})!;"); + } + } + + Writer.WriteLine(); + Writer.WriteIndentedLine("parameters = resolver.GetParameters();"); + + var parameterIndex = 0; + var bindingIndex = 0; + + foreach (var parameter in resolver.Parameters) + { + if (!parameter.RequiresBinding) + { + parameterIndex++; + continue; + } + + Writer.WriteIndentedLine( + "_args_{0}[{1}] = bindingResolver.GetBinding(parameters[{2}]);", + resolver.Member.Name, + bindingIndex++, + parameterIndex++); + } + } + + public virtual void WriteResolverMethods(IOutputTypeInfo type, ILocalTypeLookup typeLookup) + { + var first = true; + foreach (var resolver in type.Resolvers) + { + if (!first) + { + Writer.WriteLine(); + } + + first = false; + + WriteResolver(resolver, typeLookup); + } + } + + protected void WriteResolver(Resolver resolver, ILocalTypeLookup typeLookup) + { + switch (resolver.Member) + { + case IMethodSymbol resolverMethod + when resolver.ResultKind is ResolverResultKind.Pure: + WritePureResolver(resolver, resolverMethod, typeLookup); + break; + + case IMethodSymbol resolverMethod + when resolver.ResultKind is ResolverResultKind.Task or ResolverResultKind.TaskAsyncEnumerable: + WriteResolver(resolver, true, resolverMethod, typeLookup); + break; + + case IMethodSymbol resolverMethod + when resolver.ResultKind is ResolverResultKind.Executable or + ResolverResultKind.Queryable or + ResolverResultKind.AsyncEnumerable: + WriteResolver(resolver, false, resolverMethod, typeLookup); + break; + + case IPropertySymbol: + WritePropertyResolver(resolver); + break; + } + } + + private void WriteResolver( + Resolver resolver, + bool async, + IMethodSymbol resolverMethod, + ILocalTypeLookup typeLookup) + { + Writer.WriteMethod( + "public", + returnType: WellKnownTypes.FieldResolverDelegates, + methodName: $"{resolver.Member.Name}", + [], + string.Format( + "new global::{0}(resolver: {1})", + WellKnownTypes.FieldResolverDelegates, + resolver.Member.Name)); + + Writer.WriteLine(); + + Writer.WriteIndented("private "); + + if (async) + { + Writer.Write("async "); + } + + Writer.WriteLine( + "global::{0} {2}(global::{3} context)", + WellKnownTypes.ValueTask, + WellKnownTypes.Object, + resolver.Member.Name, + WellKnownTypes.ResolverContext); + Writer.WriteIndentedLine("{"); + using (Writer.IncreaseIndent()) + { + WriteResolverArguments(resolver, resolverMethod, typeLookup); + + if (async) + { + Writer.WriteIndentedLine( + resolver.IsStatic + ? "var result = await {0}.{1}({2});" + : "var result = await context.Parent<{0}>().{1}({2});", + resolver.Member.ContainingType.ToFullyQualified(), + resolver.Member.Name, + GetResolverArgumentAssignments(resolver.Parameters.Length)); + + Writer.WriteIndentedLine("return result;"); + } + else + { + Writer.WriteIndentedLine( + resolver.IsStatic + ? "var result = {0}.{1}({2});" + : "var result = context.Parent<{0}>().{1}({2});", + resolver.Member.ContainingType.ToFullyQualified(), + resolver.Member.Name, + GetResolverArgumentAssignments(resolver.Parameters.Length)); + + Writer.WriteIndentedLine( + "return new global::{0}(result);", + WellKnownTypes.ValueTask, + WellKnownTypes.Object); + } + } + + Writer.WriteIndentedLine("}"); + } + + private void WritePureResolver(Resolver resolver, IMethodSymbol resolverMethod, ILocalTypeLookup typeLookup) + { + using (Writer.WriteMethod( + "public", + returnType: WellKnownTypes.FieldResolverDelegates, + methodName: $"{resolver.Member.Name}")) + { + if (resolver.RequiresParameterBindings) + { + var firstParam = true; + Writer.WriteIndented("var isPureResolver = "); + + var bindingIndex = 0; + foreach (var parameter in resolver.Parameters) + { + if (!parameter.RequiresBinding) + { + continue; + } + + if (!firstParam) + { + Writer.Write(" && "); + } + + firstParam = false; + + Writer.Write( + "_args_{0}[{1}].IsPure", + resolver.Member.Name, + bindingIndex++); + } + + Writer.WriteLine(";"); + + Writer.WriteLine(); + + Writer.WriteIndentedLine("return isPureResolver"); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine( + "? new global::{0}(pureResolver: {1})", + WellKnownTypes.FieldResolverDelegates, + resolver.Member.Name); + Writer.WriteIndentedLine( + ": new global::{0}(resolver: c => new({1}(c)));", + WellKnownTypes.FieldResolverDelegates, + resolver.Member.Name); + } + } + else + { + Writer.WriteIndentedLine( + "return new global::{0}(pureResolver: {1});", + WellKnownTypes.FieldResolverDelegates, + resolver.Member.Name); + } + } + + Writer.WriteLine(); + + Writer.WriteIndentedLine( + "private global::{0}? {1}(global::{2} context)", + WellKnownTypes.Object, + resolver.Member.Name, + WellKnownTypes.ResolverContext); + Writer.WriteIndentedLine("{"); + using (Writer.IncreaseIndent()) + { + WriteResolverArguments(resolver, resolverMethod, typeLookup); + + Writer.WriteIndentedLine( + resolver.IsStatic + ? "var result = {0}.{1}({2});" + : "var result = context.Parent<{0}>().{1}({2});", + resolver.Member.ContainingType.ToFullyQualified(), + resolver.Member.Name, + GetResolverArgumentAssignments(resolver.Parameters.Length)); + + Writer.WriteIndentedLine("return result;"); + } + + Writer.WriteIndentedLine("}"); + } + + private void WritePropertyResolver(Resolver resolver) + { + Writer.WriteMethod( + "public", + returnType: WellKnownTypes.FieldResolverDelegates, + methodName: $"{resolver.Member.Name}", + [], + string.Format( + "new global::{0}(pureResolver: {1})", + WellKnownTypes.FieldResolverDelegates, + resolver.Member.Name)); + + Writer.WriteLine(); + + Writer.WriteIndentedLine( + "private global::{0}? {1}(global::{2} context)", + WellKnownTypes.Object, + resolver.Member.Name, + WellKnownTypes.ResolverContext); + Writer.WriteIndentedLine("{"); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine( + resolver.IsStatic + ? "var result = {0}.{1};" + : "var result = context.Parent<{0}>().{1};", + resolver.Member.ContainingType.ToFullyQualified(), + resolver.Member.Name); + + Writer.WriteIndentedLine("return result;"); + } + + Writer.WriteIndentedLine("}"); + } + + private void WriteResolverArguments(Resolver resolver, IMethodSymbol resolverMethod, ILocalTypeLookup typeLookup) + { + if (resolver.Parameters.Length == 0) + { + return; + } + + var bindingIndex = 0; + + for (var i = 0; i < resolver.Parameters.Length; i++) + { + var parameter = resolver.Parameters[i]; + + if (resolver.Kind is ResolverKind.NodeResolver + && parameter.Kind is ResolverParameterKind.Argument or ResolverParameterKind.Unknown + && (parameter.Name == "id" || parameter.Key == "id")) + { + Writer.WriteIndentedLine( + "var args{0} = context.GetLocalState<{1}>(" + + "global::HotChocolate.WellKnownContextData.InternalId);", + i, + parameter.Type.ToFullyQualified()); + continue; + } + + switch (parameter.Kind) + { + case ResolverParameterKind.Parent: + Writer.WriteIndentedLine( + "var args{0} = context.Parent<{1}>();", + i, + resolver.Parameters[i].Type.ToFullyQualified()); + break; + + case ResolverParameterKind.CancellationToken: + Writer.WriteIndentedLine("var args{0} = context.RequestAborted;", i); + break; + + case ResolverParameterKind.ClaimsPrincipal: + Writer.WriteIndentedLine( + "var args{0} = context.GetGlobalState<{1}>(\"ClaimsPrincipal\");", + i, + WellKnownTypes.ClaimsPrincipal); + break; + + case ResolverParameterKind.DocumentNode: + Writer.WriteIndentedLine("var args{0} = context.Operation.Document;", i); + break; + + case ResolverParameterKind.EventMessage: + Writer.WriteIndentedLine( + "var args{0} = context.GetScopedState<{1}>(" + + "global::HotChocolate.WellKnownContextData.EventMessage);", + i, + parameter.Type.ToFullyQualified()); + break; + + case ResolverParameterKind.FieldNode: + Writer.WriteIndentedLine( + "var args{0} = context.Selection.SyntaxNode", + i, + parameter.Type.ToFullyQualified()); + break; + + case ResolverParameterKind.OutputField: + Writer.WriteIndentedLine( + "var args{0} = context.Selection.Field", + i, + parameter.Type.ToFullyQualified()); + break; + + case ResolverParameterKind.HttpContext: + Writer.WriteIndentedLine( + "var args{0} = context.GetGlobalState(nameof(global::{1}))!;", + i, + WellKnownTypes.HttpContext); + break; + + case ResolverParameterKind.HttpRequest: + Writer.WriteIndentedLine( + "var args{0} = context.GetGlobalState(nameof(global::{1}))?.Request!;", + i, + WellKnownTypes.HttpContext); + break; + + case ResolverParameterKind.HttpResponse: + Writer.WriteIndentedLine( + "var args{0} = context.GetGlobalState(nameof(global::{1}))?.Response!;", + i, + WellKnownTypes.HttpContext); + break; + + case ResolverParameterKind.GetGlobalState when parameter.Parameter.HasExplicitDefaultValue: + { + var defaultValue = parameter.Parameter.ExplicitDefaultValue; + var defaultValueString = GeneratorUtils.ConvertDefaultValueToString(defaultValue, parameter.Type); + + Writer.WriteIndentedLine( + "var args{0} = context.GetGlobalStateOrDefault<{1}{2}>(\"{3}\", {4});", + i, + parameter.Type.ToFullyQualified(), + parameter.Type.IsNullableRefType() ? "?" : string.Empty, + parameter.Key, + defaultValueString); + break; + } + + case ResolverParameterKind.GetGlobalState when !parameter.IsNullable: + Writer.WriteIndentedLine( + "var args{0} = context.GetGlobalState<{1}>(\"{2}\");", + i, + parameter.Type.ToFullyQualified(), + parameter.Key); + break; + + case ResolverParameterKind.GetGlobalState: + Writer.WriteIndentedLine( + "var args{0} = context.GetGlobalStateOrDefault<{1}>(\"{2}\");", + i, + parameter.Type.ToFullyQualified(), + parameter.Key); + break; + + case ResolverParameterKind.SetGlobalState: + Writer.WriteIndentedLine( + "var args{0} = new HotChocolate.SetState<{1}>(" + + "value => context.SetGlobalState(\"{2}\", value));", + i, + ((INamedTypeSymbol)parameter.Type).TypeArguments[0].ToFullyQualified(), + parameter.Key); + break; + + case ResolverParameterKind.GetScopedState when parameter.Parameter.HasExplicitDefaultValue: + { + var defaultValue = parameter.Parameter.ExplicitDefaultValue; + var defaultValueString = GeneratorUtils.ConvertDefaultValueToString(defaultValue, parameter.Type); + + Writer.WriteIndentedLine( + "var args{0} = context.GetScopedStateOrDefault<{1}{2}>(\"{3}\", {4});", + i, + parameter.Type.ToFullyQualified(), + parameter.Type.IsNullableRefType() ? "?" : string.Empty, + parameter.Key, + defaultValueString); + break; + } + + case ResolverParameterKind.GetScopedState when !parameter.IsNullable: + Writer.WriteIndentedLine( + "var args{0} = context.GetScopedState<{1}>(\"{2}\");", + i, + parameter.Type.ToFullyQualified(), + parameter.Key); + break; + + case ResolverParameterKind.GetScopedState: + Writer.WriteIndentedLine( + "var args{0} = context.GetScopedStateOrDefault<{1}>(\"{2}\");", + i, + parameter.Type.ToFullyQualified(), + parameter.Key); + break; + + case ResolverParameterKind.SetScopedState: + Writer.WriteIndentedLine( + "var args{0} = new HotChocolate.SetState<{1}>(" + + "value => context.SetScopedState(\"{2}\", value));", + i, + ((INamedTypeSymbol)parameter.Type).TypeArguments[0].ToFullyQualified(), + parameter.Key); + break; + + case ResolverParameterKind.GetLocalState when parameter.Parameter.HasExplicitDefaultValue: + { + var defaultValue = parameter.Parameter.ExplicitDefaultValue; + var defaultValueString = GeneratorUtils.ConvertDefaultValueToString(defaultValue, parameter.Type); + + Writer.WriteIndentedLine( + "var args{0} = context.GetLocalStateOrDefault<{1}{2}>(\"{3}\", {4});", + i, + parameter.Type.ToFullyQualified(), + parameter.Type.IsNullableRefType() ? "?" : string.Empty, + parameter.Key, + defaultValueString); + break; + } + + case ResolverParameterKind.GetLocalState when !parameter.IsNullable: + Writer.WriteIndentedLine( + "var args{0} = context.GetLocalState<{1}>(\"{2}\");", + i, + parameter.Type.ToFullyQualified(), + parameter.Key); + break; + + case ResolverParameterKind.GetLocalState: + Writer.WriteIndentedLine( + "var args{0} = context.GetLocalStateOrDefault<{1}>(\"{2}\");", + i, + parameter.Type.ToFullyQualified(), + parameter.Key); + break; + + case ResolverParameterKind.SetLocalState: + Writer.WriteIndentedLine( + "var args{0} = new HotChocolate.SetState<{1}>(" + + "value => context.SetLocalState(\"{2}\", value));", + i, + ((INamedTypeSymbol)parameter.Type).TypeArguments[0].ToFullyQualified(), + parameter.Key); + break; + + case ResolverParameterKind.Service: + if (parameter.Key is null) + { + Writer.WriteIndentedLine( + "var args{0} = context.Service<{1}>();", + i, + ToFullyQualifiedString(parameter.Type, resolverMethod, typeLookup)); + } + else + { + Writer.WriteIndentedLine( + "var args{0} = context.Service<{1}>(\"{2}\");", + i, + ToFullyQualifiedString(parameter.Type, resolverMethod, typeLookup), + parameter.Key); + } + + break; + + case ResolverParameterKind.Argument: + Writer.WriteIndentedLine( + "var args{0} = context.ArgumentValue<{1}>(\"{2}\");", + i, + ToFullyQualifiedString(parameter.Type, resolverMethod, typeLookup), + parameter.Key ?? parameter.Name); + break; + + case ResolverParameterKind.QueryContext: + var entityType = parameter.TypeParameters[0].ToFullyQualified(); + Writer.WriteIndentedLine("var args{0}_selection = context.Selection;", i); + Writer.WriteIndentedLine("var args{0}_filter = global::{1}.GetFilterContext(context);", + i, + WellKnownTypes.FilterContextResolverContextExtensions); + Writer.WriteIndentedLine("var args{0}_sorting = global::{1}.GetSortingContext(context);", + i, + WellKnownTypes.SortingContextResolverContextExtensions); + Writer.WriteIndentedLine( + "var args{0} = new global::{1}<{2}>(", + i, + WellKnownTypes.QueryContext, + entityType); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine( + "global::{0}.AsSelector<{1}>(args{2}_selection),", + WellKnownTypes.HotChocolateExecutionSelectionExtensions, + entityType, + i); + Writer.WriteIndentedLine("args{0}_filter?.AsPredicate<{1}>(),", i, entityType); + Writer.WriteIndentedLine("args{0}_sorting?.AsSortDefinition<{1}>());", i, entityType); + } + + break; + + case ResolverParameterKind.PagingArguments: + Writer.WriteIndentedLine( + "var args{0}_options = global::{1}.GetPagingOptions(context.Schema, context.Selection.Field);", + i, + WellKnownTypes.PagingHelper); + Writer.WriteIndentedLine("var args{0}_first = context.ArgumentValue(\"first\");", i); + Writer.WriteIndentedLine("var args{0}_after = context.ArgumentValue(\"after\");", i); + Writer.WriteIndentedLine("int? args{0}_last = null;", i); + Writer.WriteIndentedLine("string? args{0}_before = null;", i); + Writer.WriteIndentedLine("bool args{0}_includeTotalCount = false;", i); + Writer.WriteLine(); + Writer.WriteIndentedLine( + "if(args{0}_options.AllowBackwardPagination ?? global::{1}.AllowBackwardPagination)", + i, + WellKnownTypes.PagingDefaults); + Writer.WriteIndentedLine("{"); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine("args{0}_last = context.ArgumentValue(\"last\");", i); + Writer.WriteIndentedLine("args{0}_before = context.ArgumentValue(\"before\");", i); + } + + Writer.WriteIndentedLine("}"); + + Writer.WriteLine(); + Writer.WriteIndentedLine("if(args{0}_first is null && args{0}_last is null)", i); + Writer.WriteIndentedLine("{"); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine( + "args{0}_first = args{0}_options.DefaultPageSize ?? global::{1}.DefaultPageSize;", + i, + WellKnownTypes.PagingDefaults); + } + + Writer.WriteIndentedLine("}"); + + Writer.WriteLine(); + Writer.WriteIndentedLine( + "if(args{0}_options.IncludeTotalCount ?? global::{1}.IncludeTotalCount)", + i, + WellKnownTypes.PagingDefaults); + Writer.WriteIndentedLine("{"); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine("args{0}_includeTotalCount = context.IsSelected(\"totalCount\");", i); + } + + Writer.WriteIndentedLine("}"); + Writer.WriteLine(); + Writer.WriteIndentedLine( + "var args{0} = new global::{1}(", + i, + WellKnownTypes.PagingArguments); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine("args{0}_first,", i); + Writer.WriteIndentedLine("args{0}_after,", i); + Writer.WriteIndentedLine("args{0}_last,", i); + Writer.WriteIndentedLine("args{0}_before,", i); + Writer.WriteIndentedLine("args{0}_includeTotalCount)", i); + Writer.WriteIndentedLine("{"); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine( + "EnableRelativeCursors = args{0}_options.EnableRelativeCursors", + i); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine( + "?? global::{0}.EnableRelativeCursors", + WellKnownTypes.PagingDefaults); + } + } + + Writer.WriteIndentedLine("};"); + } + + break; + + case ResolverParameterKind.Unknown: + Writer.WriteIndentedLine( + "var args{0} = _args_{1}[{2}].Execute<{3}>(context);", + i, + resolver.Member.Name, + bindingIndex++, + ToFullyQualifiedString(parameter.Type, resolverMethod, typeLookup)); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + private static string GetResolverArgumentAssignments(int parameterCount) + { + if (parameterCount == 0) + { + return string.Empty; + } + + var arguments = new StringBuilder(); + + for (var i = 0; i < parameterCount; i++) + { + if (i > 0) + { + arguments.Append(", "); + } + + arguments.Append($"args{i}"); + } + + return arguments.ToString(); + } + + public void Flush() => Writer.Flush(); + + private static string ToFullyQualifiedString( + ITypeSymbol type, + IMethodSymbol resolverMethod, + ILocalTypeLookup typeLookup) + { + if (type.TypeKind is TypeKind.Error + && typeLookup.TryGetTypeName(type, resolverMethod, out var typeDisplayName)) + { + return typeDisplayName; + } + + return type.ToFullyQualified(); + } +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Filters/ClassWithBaseClass.cs b/src/HotChocolate/Core/src/Types.Analyzers/Filters/ClassWithBaseClass.cs index ce33081ee4b..42ee1715bdb 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Filters/ClassWithBaseClass.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Filters/ClassWithBaseClass.cs @@ -12,3 +12,5 @@ private ClassWithBaseClass() { } public bool IsMatch(SyntaxNode node) => node is ClassDeclarationSyntax { BaseList.Types.Count: > 0 }; } + + diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypeModuleSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypeModuleSyntaxGenerator.cs index 4c178479d7a..01eb3d2498f 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypeModuleSyntaxGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypeModuleSyntaxGenerator.cs @@ -60,8 +60,8 @@ private static void WriteConfiguration( var operations = OperationType.No; var hasConfigurations = false; - List? _objectTypeExtensions = null; - List? _interfaceTypeExtensions = null; + List? objectTypeExtensions = null; + List? interfaceTypeExtensions = null; foreach (var syntaxInfo in syntaxInfos.OrderBy(s => s.OrderByKey)) { @@ -139,46 +139,64 @@ private static void WriteConfiguration( } break; - case ObjectTypeExtensionInfo objectTypeExtension: + case ObjectTypeInfo objectTypeExtension: if ((module.Options & ModuleOptions.RegisterTypes) == ModuleOptions.RegisterTypes && objectTypeExtension.Diagnostics.Length == 0) { - _objectTypeExtensions ??= []; - _objectTypeExtensions.Add(objectTypeExtension.RuntimeType.ToFullyQualified()); + objectTypeExtensions ??= []; + objectTypeExtensions.Add(objectTypeExtension.RuntimeType.ToFullyQualified()); generator.WriteRegisterTypeExtension( - GetAssemblyQualifiedName(objectTypeExtension.Type), + GetAssemblyQualifiedName(objectTypeExtension.SchemaSchemaType), objectTypeExtension.RuntimeType.ToFullyQualified(), - objectTypeExtension.Type.ToFullyQualified()); + objectTypeExtension.SchemaSchemaType.ToFullyQualified()); hasConfigurations = true; } break; - case InterfaceTypeExtensionInfo interfaceType: + case ConnectionTypeInfo connectionType: + if ((module.Options & ModuleOptions.RegisterTypes) == ModuleOptions.RegisterTypes + && connectionType.Diagnostics.Length == 0) + { + generator.WriteRegisterType($"{connectionType.Namespace}.{connectionType.Name}"); + hasConfigurations = true; + } + break; + + case EdgeTypeInfo edgeType: + if ((module.Options & ModuleOptions.RegisterTypes) == ModuleOptions.RegisterTypes + && edgeType.Diagnostics.Length == 0) + { + generator.WriteRegisterType($"{edgeType.Namespace}.{edgeType.Name}"); + hasConfigurations = true; + } + break; + + case InterfaceTypeInfo interfaceType: if ((module.Options & ModuleOptions.RegisterTypes) == ModuleOptions.RegisterTypes && interfaceType.Diagnostics.Length == 0) { - _interfaceTypeExtensions ??= []; - _interfaceTypeExtensions.Add(interfaceType.RuntimeType.ToFullyQualified()); + interfaceTypeExtensions ??= []; + interfaceTypeExtensions.Add(interfaceType.RuntimeType.ToFullyQualified()); generator.WriteRegisterTypeExtension( - GetAssemblyQualifiedName(interfaceType.Type), + GetAssemblyQualifiedName(interfaceType.SchemaSchemaType), interfaceType.RuntimeType.ToFullyQualified(), - interfaceType.Type.ToFullyQualified()); + interfaceType.SchemaSchemaType.ToFullyQualified()); hasConfigurations = true; } break; - case RootTypeExtensionInfo rootType: + case RootTypeInfo rootType: if ((module.Options & ModuleOptions.RegisterTypes) == ModuleOptions.RegisterTypes && rootType.Diagnostics.Length == 0) { var operationType = rootType.OperationType; generator.WriteRegisterRootTypeExtension( - GetAssemblyQualifiedName(rootType.Type), + GetAssemblyQualifiedName(rootType.SchemaSchemaType), operationType, - rootType.Type.ToFullyQualified()); + rootType.SchemaSchemaType.ToFullyQualified()); hasConfigurations = true; if (operationType is not OperationType.No && (operations & operationType) != operationType) @@ -216,17 +234,17 @@ private static void WriteConfiguration( } } - if (_objectTypeExtensions is not null) + if (objectTypeExtensions is not null) { - foreach (var type in _objectTypeExtensions) + foreach (var type in objectTypeExtensions) { generator.WriteEnsureObjectTypeExtensionIsRegistered(type); } } - if (_interfaceTypeExtensions is not null) + if (interfaceTypeExtensions is not null) { - foreach (var type in _interfaceTypeExtensions) + foreach (var type in interfaceTypeExtensions) { generator.WriteEnsureInterfaceTypeExtensionIsRegistered(type); } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypesSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypesSyntaxGenerator.cs index 79685e21627..55eb7bf616d 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypesSyntaxGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypesSyntaxGenerator.cs @@ -1,4 +1,6 @@ +using System.Buffers.Text; using System.Collections.Immutable; +using System.Security.Cryptography; using System.Text; using HotChocolate.Types.Analyzers.FileBuilders; using HotChocolate.Types.Analyzers.Helpers; @@ -32,9 +34,6 @@ public void Generate( WriteTypes(context, syntaxInfos, sb); sb.Clear(); - - WriteResolvers(context, syntaxInfos, sb); - PooledObjects.Return(sb); } @@ -43,126 +42,91 @@ private static void WriteTypes( ImmutableArray syntaxInfos, StringBuilder sb) { - var hasTypes = false; - var firstNamespace = true; - foreach (var group in syntaxInfos - .OfType() - .GroupBy(t => t.Type.ContainingNamespace.ToDisplayString())) + var typeLookup = new DefaultLocalTypeLookup(syntaxInfos); + + foreach (var type in syntaxInfos.OrderBy(t => t.OrderByKey).OfType()) { - var generator = new ObjectTypeExtensionFileBuilder(sb, group.Key); + sb.Clear(); - if (firstNamespace) + if (type is ObjectTypeInfo objectType) { - generator.WriteHeader(); - firstNamespace = false; + var file = new ObjectTypeFileBuilder(sb); + WriteFile(file, objectType, typeLookup); + context.AddSource(CreateFileName(objectType), sb.ToString()); } - generator.WriteBeginNamespace(); - - var firstClass = true; - foreach (var typeInfo in group) + if(type is InterfaceTypeInfo interfaceType) { - if (typeInfo.Diagnostics.Length > 0) - { - continue; - } - - var classGenerator = typeInfo is InterfaceTypeExtensionInfo - ? new InterfaceTypeExtensionFileBuilder(sb, group.Key) - : (IOutputTypeFileBuilder)new ObjectTypeExtensionFileBuilder(sb, group.Key); - - if (!firstClass) - { - sb.AppendLine(); - } - - firstClass = false; - - classGenerator.WriteBeginClass(typeInfo.Type.Name); - classGenerator.WriteInitializeMethod(typeInfo); - sb.AppendLine(); - classGenerator.WriteConfigureMethod(typeInfo); - classGenerator.WriteEndClass(); - hasTypes = true; - + var file = new InterfaceTypeFileBuilder(sb); + WriteFile(file, interfaceType, typeLookup); + context.AddSource(CreateFileName(interfaceType), sb.ToString()); } - generator.WriteEndNamespace(); - } - - if (hasTypes) - { - context.AddSource(WellKnownFileNames.TypesFile, sb.ToString()); - } - } - - private static void WriteResolvers( - SourceProductionContext context, - ImmutableArray syntaxInfos, - StringBuilder sb) - { - var hasResolvers = false; - var typeLookup = new DefaultLocalTypeLookup(syntaxInfos); - - var generator = new ResolverFileBuilder(sb); - generator.WriteHeader(); - - var firstNamespace = true; - foreach (var group in syntaxInfos - .OfType() - .GroupBy(t => t.Type.ContainingNamespace.ToDisplayString())) - { - if (!firstNamespace) + if(type is RootTypeInfo rootType) { - sb.AppendLine(); + var file = new RootTypeFileBuilder(sb); + WriteFile(file, rootType, typeLookup); + context.AddSource(CreateFileName(rootType), sb.ToString()); } - firstNamespace = false; - - generator.WriteBeginNamespace(group.Key); - - var firstClass = true; - foreach (var typeInfo in group) + if(type is ConnectionTypeInfo connectionType) { - if (!firstClass) - { - sb.AppendLine(); - } + var file = new ConnectionTypeFileBuilder(sb); + WriteFile(file, connectionType, typeLookup); + context.AddSource(CreateFileName(connectionType), sb.ToString()); + } - firstClass = false; + if(type is EdgeTypeInfo edgeType) + { + var file = new EdgeTypeFileBuilder(sb); + WriteFile(file, edgeType, typeLookup); + context.AddSource(CreateFileName(edgeType), sb.ToString()); + } + } - var resolvers = typeInfo.Resolvers; + static string CreateFileName(IOutputTypeInfo type) + { + Span hash = stackalloc byte[64]; + var bytes = Encoding.UTF8.GetBytes(type.Namespace); + MD5.HashData(bytes, hash); + Base64.EncodeToUtf8InPlace(hash, 16, out var written); + hash = hash[..written]; - if (typeInfo is ObjectTypeExtensionInfo { NodeResolver: { } nodeResolver }) + for (var i = 0; i < hash.Length; i++) + { + if (hash[i] == (byte)'+') { - resolvers = resolvers.Add(nodeResolver); + hash[i] = (byte)'-'; } - - generator.WriteBeginClass(typeInfo.Type.Name + "Resolvers"); - - if (generator.AddResolverDeclarations(resolvers)) + else if (hash[i] == (byte)'/') { - sb.AppendLine(); + hash[i] = (byte)'_'; } - - generator.AddParameterInitializer(resolvers, typeLookup); - - foreach (var resolver in resolvers) + else if(hash[i] == (byte)'=') { - sb.AppendLine(); - generator.AddResolver(resolver, typeLookup); + hash = hash[..i]; + break; } - - generator.WriteEndClass(); - hasResolvers = true; } - generator.WriteEndNamespace(); + return $"{type.Name}.{Encoding.UTF8.GetString(hash)}.hc.g.cs"; } + } - if (hasResolvers) - { - context.AddSource(WellKnownFileNames.ResolversFile, sb.ToString()); - } + private static void WriteFile(TypeFileBuilderBase file, IOutputTypeInfo type, ILocalTypeLookup typeLookup) + { + file.WriteHeader(); + file.WriteBeginNamespace(type); + file.WriteBeginClass(type); + file.WriteInitializeMethod(type); + file.WriteConfigureMethod(type); + file.WriteBeginResolverClass(); + file.WriteResolverFields(type); + file.WriteResolverConstructor(type, typeLookup); + file.WriteResolverMethods(type, typeLookup); + file.WriteEndResolverClass(); + file.WriteEndClass(); + file.WriteEndNamespace(); + file.Flush(); } } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/GraphQLServerGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/GraphQLServerGenerator.cs index a7ac22195e8..2fb116c2d7b 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/GraphQLServerGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/GraphQLServerGenerator.cs @@ -1,16 +1,20 @@ +using System.Collections.Frozen; using System.Collections.Immutable; using HotChocolate.Types.Analyzers.Filters; using HotChocolate.Types.Analyzers.Generators; using HotChocolate.Types.Analyzers.Inspectors; using HotChocolate.Types.Analyzers.Models; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; namespace HotChocolate.Types.Analyzers; +#pragma warning disable RS1041 [Generator] +#pragma warning restore RS1041 public class GraphQLServerGenerator : IIncrementalGenerator { - private static readonly ISyntaxInspector[] _inspectors = + private static readonly ISyntaxInspector[] _allInspectors = [ new TypeAttributeInspector(), new ClassBaseClassInspector(), @@ -19,9 +23,15 @@ public class GraphQLServerGenerator : IIncrementalGenerator new DataLoaderDefaultsInspector(), new DataLoaderModuleInspector(), new OperationInspector(), - new ObjectTypeExtensionInfoInspector(), + new ObjectTypeInspector(), new InterfaceTypeInfoInspector(), - new RequestMiddlewareInspector() + new RequestMiddlewareInspector(), + new ConnectionInspector() + ]; + + private static readonly IPostCollectSyntaxTransformer[] _postCollectTransformers = + [ + new ConnectionTypeTransformer() ]; private static readonly ISyntaxGenerator[] _generators = @@ -33,18 +43,31 @@ public class GraphQLServerGenerator : IIncrementalGenerator new DataLoaderGenerator() ]; + private static readonly FrozenDictionary> _inspectorLookup; private static readonly Func _predicate; static GraphQLServerGenerator() { var filterBuilder = new SyntaxFilterBuilder(); + var inspectorLookup = new Dictionary>(); - foreach (var inspector in _inspectors) + foreach (var inspector in _allInspectors) { filterBuilder.AddRange(inspector.Filters); + + foreach (var supportedKind in inspector.SupportedKinds) + { + if(!inspectorLookup.TryGetValue(supportedKind, out var inspectors)) + { + inspectors = []; + inspectorLookup[supportedKind] = inspectors; + } + inspectors.Add(inspector); + } } _predicate = filterBuilder.Build(); + _inspectorLookup = inspectorLookup.ToFrozenDictionary(t => t.Key, t => t.Value.ToImmutableArray()); } public void Initialize(IncrementalGeneratorInitializationContext context) @@ -58,24 +81,46 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .WithComparer(SyntaxInfoComparer.Default) .Collect(); + var postProcessedSyntaxInfos = + context.CompilationProvider + .Combine(syntaxInfos) + .Select((ctx, _) => OnAfterCollect(ctx.Left, ctx.Right)); + var assemblyNameProvider = context.CompilationProvider .Select(static (c, _) => c.AssemblyName!); - var valueProvider = assemblyNameProvider.Combine(syntaxInfos); + var valueProvider = assemblyNameProvider.Combine(postProcessedSyntaxInfos); context.RegisterSourceOutput( valueProvider, static (context, source) => Execute(context, source.Left, source.Right)); } + private static ImmutableArray OnAfterCollect( + Compilation compilation, + ImmutableArray syntaxInfos) + { + foreach (var transformer in _postCollectTransformers) + { + syntaxInfos = transformer.Transform(compilation, syntaxInfos); + } + + return syntaxInfos; + } + private static bool Predicate(SyntaxNode node) => _predicate(node); private static SyntaxInfo? Transform(GeneratorSyntaxContext context) { - for (var i = 0; i < _inspectors.Length; i++) + if (!_inspectorLookup.TryGetValue(context.Node.Kind(), out var inspectors)) + { + return null; + } + + foreach (var inspector in inspectors) { - if (_inspectors[i].TryHandle(context, out var syntaxInfo)) + if (inspector.TryHandle(context, out var syntaxInfo)) { return syntaxInfo; } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriter.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriter.cs index 23317466c3e..f7f7d84da1a 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriter.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriter.cs @@ -53,6 +53,7 @@ public string GetIndentString() { return new string(' ', _indent * 4); } + return string.Empty; } @@ -94,7 +95,8 @@ public IDisposable IncreaseIndent() return new Block(DecreaseIndent); } - public IDisposable WriteMethod(string accessModifier, string returnType, string methodName, params string[] parameters) + public IDisposable WriteMethod(string accessModifier, string returnType, string methodName, + params string[] parameters) { WriteIndented("{0} {1} {2}(", accessModifier, returnType, methodName); @@ -108,6 +110,27 @@ public IDisposable WriteMethod(string accessModifier, string returnType, string return WithCurlyBrace(); } + public void WriteMethod( + string accessModifier, + string returnType, + string methodName, + string[] parameters, + string expression) + { + WriteIndented("{0} {1} {2}(", accessModifier, returnType, methodName); + + if (parameters.Length > 0) + { + Write(string.Join(", ", parameters)); + } + + Write(")"); + WriteLine(); + IncreaseIndent(); + WriteIndentedLine("=> {0};", expression); + DecreaseIndent(); + } + public IDisposable WriteForEach(string item, string collection) { WriteIndentedLine("foreach(var {0} in {1})", item, collection); @@ -173,6 +196,7 @@ protected override void Dispose(bool disposing) { _writer.Dispose(); } + _disposed = true; } } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/GeneratorUtils.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/GeneratorUtils.cs index bea2a4f5267..dce213dd454 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/GeneratorUtils.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/GeneratorUtils.cs @@ -83,7 +83,7 @@ public static string ConvertDefaultValueToString(object? defaultValue, ITypeSymb if (type.SpecialType == SpecialType.System_Boolean) { - return defaultValue.ToString().ToLower(); + return defaultValue.ToString()!.ToLower(); } if (type.SpecialType == SpecialType.System_Double || @@ -103,7 +103,7 @@ public static string ConvertDefaultValueToString(object? defaultValue, ITypeSymb return $"{defaultValue}L"; } - return defaultValue.ToString(); + return defaultValue.ToString()!; } public static string SanitizeIdentifier(string input) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs index 3266229170a..8fb4dc89750 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs @@ -8,30 +8,21 @@ namespace HotChocolate.Types.Analyzers.Helpers; public static class SymbolExtensions { public static bool IsNullableType(this ITypeSymbol typeSymbol) - { - return typeSymbol.IsNullableRefType() || - typeSymbol.IsNullableValueType(); - } + => typeSymbol.IsNullableRefType() || typeSymbol.IsNullableValueType(); public static bool IsNullableRefType(this ITypeSymbol typeSymbol) - { - return typeSymbol.IsReferenceType - && typeSymbol.NullableAnnotation == NullableAnnotation.Annotated; - } + => typeSymbol is + { + IsReferenceType: true, + NullableAnnotation: NullableAnnotation.Annotated + }; public static bool IsNullableValueType(this ITypeSymbol typeSymbol) - { - if (typeSymbol is INamedTypeSymbol namedTypeSymbol) + => typeSymbol is INamedTypeSymbol { - if (namedTypeSymbol.IsGenericType && - namedTypeSymbol.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) - { - return true; - } - } - - return false; - } + IsGenericType: true, + OriginalDefinition.SpecialType: SpecialType.System_Nullable_T + }; public static string PrintNullRefQualifier(this ITypeSymbol typeSymbol) { @@ -42,11 +33,15 @@ public static string ToFullyQualified(this ITypeSymbol typeSymbol) => typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); public static bool IsParent(this IParameterSymbol parameter) - => parameter.IsThis || - parameter + => parameter.IsThis + || parameter .GetAttributes() .Any(static t => t.AttributeClass?.ToDisplayString() == WellKnownAttributes.ParentAttribute); + public static bool IsIgnored(this ISymbol member) + => member.GetAttributes() + .Any(static t => t.AttributeClass?.ToDisplayString() == WellKnownAttributes.GraphQLIgnoreAttribute); + public static bool IsCancellationToken(this IParameterSymbol parameter) => parameter.Type.ToDisplayString() == WellKnownTypes.CancellationToken; @@ -80,8 +75,8 @@ public static bool IsSetState(this IParameterSymbol parameter, [NotNullWhen(true { if (namedTypeSymbol is { IsGenericType: true, TypeArguments.Length: 1 }) { - if (namedTypeSymbol.Name == "SetState" && - namedTypeSymbol.ContainingNamespace.ToDisplayString() == "HotChocolate") + if (namedTypeSymbol.Name == "SetState" + && namedTypeSymbol.ContainingNamespace.ToDisplayString() == "HotChocolate") { stateTypeName = namedTypeSymbol.TypeArguments[0].ToDisplayString(); return true; @@ -99,8 +94,8 @@ public static bool IsSetState(this IParameterSymbol parameter) { if (namedTypeSymbol is { IsGenericType: true, TypeArguments.Length: 1 }) { - if (namedTypeSymbol.Name == "SetState" && - namedTypeSymbol.ContainingNamespace.ToDisplayString() == "HotChocolate") + if (namedTypeSymbol.Name == "SetState" + && namedTypeSymbol.ContainingNamespace.ToDisplayString() == "HotChocolate") { return true; } @@ -122,6 +117,17 @@ public static bool IsQueryContext(this IParameterSymbol parameter) return false; } + public static bool IsPagingArguments(this IParameterSymbol parameter) + { + if (parameter.Type is INamedTypeSymbol namedTypeSymbol + && namedTypeSymbol.ToDisplayString().StartsWith(WellKnownTypes.PagingArguments)) + { + return true; + } + + return false; + } + public static bool IsGlobalState( this IParameterSymbol parameter, [NotNullWhen(true)] out string? key) @@ -132,9 +138,9 @@ public static bool IsGlobalState( { if (IsOrInheritsFrom(attributeData.AttributeClass, "HotChocolate.GlobalStateAttribute")) { - if (attributeData.ConstructorArguments.Length == 1 && - attributeData.ConstructorArguments[0].Kind == TypedConstantKind.Primitive && - attributeData.ConstructorArguments[0].Value is string keyValue) + if (attributeData.ConstructorArguments.Length == 1 + && attributeData.ConstructorArguments[0].Kind == TypedConstantKind.Primitive + && attributeData.ConstructorArguments[0].Value is string keyValue) { key = keyValue; return true; @@ -167,9 +173,9 @@ public static bool IsScopedState( { if (IsOrInheritsFrom(attributeData.AttributeClass, "HotChocolate.ScopedStateAttribute")) { - if (attributeData.ConstructorArguments.Length == 1 && - attributeData.ConstructorArguments[0].Kind == TypedConstantKind.Primitive && - attributeData.ConstructorArguments[0].Value is string keyValue) + if (attributeData.ConstructorArguments.Length == 1 + && attributeData.ConstructorArguments[0].Kind == TypedConstantKind.Primitive + && attributeData.ConstructorArguments[0].Value is string keyValue) { key = keyValue; return true; @@ -202,9 +208,9 @@ public static bool IsLocalState( { if (IsOrInheritsFrom(attributeData.AttributeClass, "HotChocolate.LocalStateAttribute")) { - if (attributeData.ConstructorArguments.Length == 1 && - attributeData.ConstructorArguments[0].Kind == TypedConstantKind.Primitive && - attributeData.ConstructorArguments[0].Value is string keyValue) + if (attributeData.ConstructorArguments.Length == 1 + && attributeData.ConstructorArguments[0].Kind == TypedConstantKind.Primitive + && attributeData.ConstructorArguments[0].Value is string keyValue) { key = keyValue; return true; @@ -251,9 +257,9 @@ public static bool IsService( { if (attributeData.AttributeClass?.ToDisplayString() == WellKnownAttributes.ServiceAttribute) { - if (attributeData.ConstructorArguments.Length == 1 && - attributeData.ConstructorArguments[0].Kind == TypedConstantKind.Primitive && - attributeData.ConstructorArguments[0].Value is string keyValue) + if (attributeData.ConstructorArguments.Length == 1 + && attributeData.ConstructorArguments[0].Kind == TypedConstantKind.Primitive + && attributeData.ConstructorArguments[0].Value is string keyValue) { key = keyValue; return true; @@ -286,9 +292,9 @@ public static bool IsArgument( { if (attributeData.AttributeClass?.ToDisplayString() == WellKnownAttributes.ArgumentAttribute) { - if (attributeData.ConstructorArguments.Length == 1 && - attributeData.ConstructorArguments[0].Kind == TypedConstantKind.Primitive && - attributeData.ConstructorArguments[0].Value is string keyValue) + if (attributeData.ConstructorArguments.Length == 1 + && attributeData.ConstructorArguments[0].Kind == TypedConstantKind.Primitive + && attributeData.ConstructorArguments[0].Value is string keyValue) { key = keyValue; return true; @@ -318,8 +324,8 @@ public static bool IsNonNullable(this IParameterSymbol parameter) return false; } - if (parameter.Type is INamedTypeSymbol namedTypeSymbol && - namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T) + if (parameter.Type is INamedTypeSymbol namedTypeSymbol + && namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T) { return false; } @@ -341,17 +347,14 @@ public static ResolverResultKind GetResultKind(this IMethodSymbol method) var returnType = method.ReturnType.ToDisplayString(); - if (returnType.Equals(WellKnownTypes.Task) || - returnType.Equals(WellKnownTypes.ValueTask)) + if (returnType.Equals(WellKnownTypes.Task) || returnType.Equals(WellKnownTypes.ValueTask)) { return ResolverResultKind.Invalid; } - if (returnType.StartsWith(task) || - returnType.StartsWith(valueTask)) + if (returnType.StartsWith(task) || returnType.StartsWith(valueTask)) { - if (returnType.StartsWith(taskEnumerable) || - returnType.StartsWith(valueTaskEnumerable)) + if (returnType.StartsWith(taskEnumerable) || returnType.StartsWith(valueTaskEnumerable)) { return ResolverResultKind.TaskAsyncEnumerable; } @@ -445,6 +448,7 @@ private static ITypeSymbol UnwrapWrapperTypes(ITypeSymbol typeSymbol) break; } } + return typeSymbol; } @@ -471,8 +475,8 @@ private static bool IsPostProcessorAttribute(INamedTypeSymbol? attributeClass) while (attributeClass != null) { var typeName = attributeClass.ToDisplayString(); - if (typeName.Equals("HotChocolate.Types.UsePagingAttribute") || - typeName.Equals("HotChocolate.Types.UseOffsetPagingAttribute")) + if (typeName.Equals("HotChocolate.Types.UsePagingAttribute") + || typeName.Equals("HotChocolate.Types.UseOffsetPagingAttribute")) { return true; } @@ -498,7 +502,7 @@ public static bool IsOrInheritsFrom(this ITypeSymbol? attributeClass, params str while (current != null) { - foreach(var typeName in fullTypeName) + foreach (var typeName in fullTypeName) { if (current.ToDisplayString() == typeName) { @@ -536,7 +540,7 @@ public static bool IsOrInheritsFrom(this ITypeSymbol? attributeClass, string ful { returnType = method.ReturnType; } - else if(member is IPropertySymbol property) + else if (member is IPropertySymbol property) { returnType = property.Type; } @@ -547,8 +551,10 @@ public static bool IsOrInheritsFrom(this ITypeSymbol? attributeClass, string ful if (returnType is INamedTypeSymbol namedTypeSymbol) { - if (namedTypeSymbol.ConstructedFrom.ToString() == "System.Threading.Tasks.Task" || - namedTypeSymbol.ConstructedFrom.ToString() == "System.Threading.Tasks.ValueTask") + var definitionName = namedTypeSymbol.ConstructedFrom.ToDisplayString(); + + if (definitionName.StartsWith("System.Threading.Tasks.Task<") + || definitionName.StartsWith("System.Threading.Tasks.ValueTask<")) { return namedTypeSymbol.TypeArguments.FirstOrDefault(); } @@ -556,4 +562,107 @@ public static bool IsOrInheritsFrom(this ITypeSymbol? attributeClass, string ful return returnType; } + + public static bool IsConnectionType(this INamedTypeSymbol typeSymbol, Compilation compilation) + { + var connectionInterface = compilation.GetTypeByMetadataName("HotChocolate.Types.Pagination.IConnection`1"); + + if (connectionInterface == null) + { + return false; + } + + return typeSymbol.AllInterfaces.Any( + s => SymbolEqualityComparer.Default.Equals(s.OriginalDefinition, connectionInterface)); + } + + public static bool IsConnectionBase(this ITypeSymbol typeSymbol) + { + if (typeSymbol is INamedTypeSymbol { IsGenericType: true } namedType) + { + var definitionName = namedType.ConstructedFrom.ToDisplayString(); + + if (definitionName.StartsWith("System.Threading.Tasks.Task<") + || definitionName.StartsWith("System.Threading.Tasks.ValueTask<")) + { + typeSymbol = namedType.TypeArguments[0]; + } + } + + var current = typeSymbol; + + while (current != null) + { + if (current is INamedTypeSymbol { IsGenericType: true } namedTypeSymbol) + { + var baseType = namedTypeSymbol.ConstructedFrom; + + if (baseType.ToDisplayString().StartsWith("HotChocolate.Types.Pagination.ConnectionBase<")) + { + return true; + } + } + + current = current.BaseType; + } + + return false; + } + + public static ITypeSymbol UnwrapTaskOrValueTask(this ITypeSymbol typeSymbol) + { + if (typeSymbol is INamedTypeSymbol { IsGenericType: true } namedType) + { + var originalDefinition = namedType.ConstructedFrom; + + if (originalDefinition.ToDisplayString() == "System.Threading.Tasks.Task" + || originalDefinition.ToDisplayString() == "System.Threading.Tasks.ValueTask") + { + return namedType.TypeArguments[0]; + } + } + + return typeSymbol; + } + + /// + /// Determines if the method is an accessor (e.g., get_Property, set_Property). + /// + public static bool IsPropertyOrEventAccessor(this IMethodSymbol method) + { + return method.MethodKind == MethodKind.PropertyGet + || method.MethodKind == MethodKind.PropertySet + || method.MethodKind == MethodKind.EventAdd + || method.MethodKind == MethodKind.EventRemove + || method.MethodKind == MethodKind.EventRaise; + } + + /// + /// Determines if the method is an operator overload (e.g., op_Addition). + /// + public static bool IsOperator(this IMethodSymbol method) + { + return method.MethodKind == MethodKind.UserDefinedOperator + || method.MethodKind == MethodKind.Conversion; + } + + public static bool IsConstructor(this IMethodSymbol method) + { + return method.MethodKind == MethodKind.Constructor + || method.MethodKind == MethodKind.SharedConstructor; + } + + public static bool IsSpecialMethod(this IMethodSymbol method) + { + return method.MethodKind == MethodKind.Destructor + || method.MethodKind == MethodKind.LocalFunction + || method.MethodKind == MethodKind.AnonymousFunction + || method.MethodKind == MethodKind.DelegateInvoke; + } + + public static bool IsCompilerGenerated(this IMethodSymbol method) + => method + .GetAttributes() + .Any(attr => attr.AttributeClass?.ToDisplayString() + == "System.Runtime.CompilerServices.CompilerGeneratedAttribute"); } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/HotChocolate.Types.Analyzers.csproj b/src/HotChocolate/Core/src/Types.Analyzers/HotChocolate.Types.Analyzers.csproj index aebc7bb71f3..90e66f8189f 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/HotChocolate.Types.Analyzers.csproj +++ b/src/HotChocolate/Core/src/Types.Analyzers/HotChocolate.Types.Analyzers.csproj @@ -1,11 +1,12 @@ - netstandard2.0 - netstandard2.0 + net8.0 + net8.0 false false true + $(NoWarn);RS2008 @@ -16,9 +17,8 @@ - - - + + diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ClassBaseClassInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ClassBaseClassInspector.cs index d431208645a..0d64f7b4829 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ClassBaseClassInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ClassBaseClassInspector.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using HotChocolate.Types.Analyzers.Filters; using HotChocolate.Types.Analyzers.Models; @@ -10,7 +11,9 @@ namespace HotChocolate.Types.Analyzers.Inspectors; public class ClassBaseClassInspector : ISyntaxInspector { - public IReadOnlyList Filters => [ClassWithBaseClass.Instance]; + public ImmutableArray Filters { get; } = [ClassWithBaseClass.Instance]; + + public IImmutableSet SupportedKinds { get; } = [SyntaxKind.ClassDeclaration]; public bool TryHandle( GeneratorSyntaxContext context, diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ClassWithBaseClassInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ClassWithBaseClassInspector.cs new file mode 100644 index 00000000000..0adcddb864a --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ClassWithBaseClassInspector.cs @@ -0,0 +1,37 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Types.Analyzers.Filters; +using HotChocolate.Types.Analyzers.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace HotChocolate.Types.Analyzers.Inspectors; + +public abstract class ClassWithBaseClassInspector : ISyntaxInspector where T : SyntaxInfo +{ + public ImmutableArray Filters { get; } = [ClassWithBaseClass.Instance]; + + public IImmutableSet SupportedKinds { get; } = ImmutableHashSet.Create(SyntaxKind.ClassDeclaration); + + public bool TryHandle(GeneratorSyntaxContext context, [NotNullWhen(true)] out SyntaxInfo? syntaxInfo) + { + if(context.Node is ClassDeclarationSyntax { BaseList.Types.Count: > 0 } classDeclaration + && context.SemanticModel.GetDeclaredSymbol(classDeclaration) is { } namedType + && TryHandle(context, classDeclaration, classDeclaration.BaseList.Types, namedType, out var result)) + { + syntaxInfo = result; + return true; + } + + syntaxInfo = null; + return false; + } + + protected abstract bool TryHandle( + GeneratorSyntaxContext context, + ClassDeclarationSyntax classDeclaration, + SeparatedSyntaxList baseTypes, + INamedTypeSymbol namedType, + [NotNullWhen(true)] out T? syntaxInfo); +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionInspector.cs new file mode 100644 index 00000000000..bb30078dcd8 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionInspector.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Types.Analyzers.Helpers; +using HotChocolate.Types.Analyzers.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace HotChocolate.Types.Analyzers.Inspectors; + +public sealed class ConnectionInspector : ClassWithBaseClassInspector +{ + protected override bool TryHandle( + GeneratorSyntaxContext context, + ClassDeclarationSyntax classDeclaration, + SeparatedSyntaxList baseTypes, + INamedTypeSymbol namedType, + [NotNullWhen(true)] out ConnectionClassInfo? syntaxInfo) + { + if (namedType is { IsStatic: false, IsAbstract: false } + && context.IsConnectionBase(namedType)) + { + syntaxInfo = ConnectionClassInfo.CreateConnection( + context.SemanticModel.Compilation, + namedType, + classDeclaration); + return true; + } + + syntaxInfo = null; + return false; + } +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs new file mode 100644 index 00000000000..456f8b416c0 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs @@ -0,0 +1,361 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Types.Analyzers.Models; +using HotChocolate.Types.Analyzers.Helpers; +using Microsoft.CodeAnalysis; + +namespace HotChocolate.Types.Analyzers.Inspectors; + +public class ConnectionTypeTransformer : IPostCollectSyntaxTransformer +{ + public ImmutableArray Transform( + Compilation compilation, + ImmutableArray syntaxInfos) + { + Dictionary? connectionResolvers = null; + Dictionary? connectionClassLookup = null; + + foreach (var syntaxInfo in syntaxInfos) + { + if (syntaxInfo is ConnectionClassInfo connectionClass) + { + connectionClassLookup ??= []; + connectionClassLookup[connectionClass.RuntimeType.ToFullyQualified()] = connectionClass; + continue; + } + + if (syntaxInfo is IOutputTypeInfo typeInfo + && typeInfo.Resolvers.Any(t => t.Kind is ResolverKind.ConnectionResolver)) + { + foreach (var resolver in typeInfo.Resolvers) + { + if (resolver.Kind is ResolverKind.ConnectionResolver) + { + connectionResolvers ??= []; + connectionResolvers.Add(resolver, typeInfo); + } + } + } + } + + if (connectionResolvers is not null) + { + connectionClassLookup ??= []; + var connectionTypeLookup = new Dictionary(); + var connectionNameLookup = new Dictionary(); + List? connectionTypeInfos = null; + + foreach (var syntaxInfo in syntaxInfos) + { + if (syntaxInfo is IOutputTypeInfo { HasRuntimeType: true } typeInfo) + { + connectionTypeLookup[typeInfo.RuntimeTypeFullName] = typeInfo; + } + } + + foreach (var (connectionResolver, owner) in connectionResolvers) + { + var connectionType = GetConnectionType(compilation, connectionResolver.Member.GetReturnType()); + ConnectionTypeInfo connectionTypeInfo; + ConnectionClassInfo? connectionClass; + EdgeTypeInfo? edgeTypeInfo; + ConnectionClassInfo? edgeClass; + + if (connectionType.IsGenericType) + { + var diagnostics = ImmutableArray.Empty; + + var connection = CreateGenericTypeInfo( + connectionType, + connectionResolver.Member, + compilation, + isConnection: true, + connectionNameLookup, + ref diagnostics); + + if (connection is null) + { + connectionTypeInfo = ConnectionTypeInfo.CreateConnection( + compilation, + connectionType, + null, + connectionType.Name); + connectionTypeInfo.AddDiagnosticRange(diagnostics); + connectionTypeInfos ??= []; + connectionTypeInfos.Add(connectionTypeInfo); + continue; + } + + var edgeType = GetEdgeType(connection.Type); + if (edgeType is null) + { + continue; + } + + var edge = CreateGenericTypeInfo( + edgeType, + connectionResolver.Member, + compilation, + isConnection: false, + connectionNameLookup, + ref diagnostics); + + if (edge is null) + { + edgeTypeInfo = EdgeTypeInfo.CreateEdge( + compilation, + connectionType, + null); + edgeTypeInfo.AddDiagnosticRange(diagnostics); + connectionTypeInfos ??= []; + connectionTypeInfos.Add(edgeTypeInfo); + continue; + } + + edgeTypeInfo = + connectionClassLookup.TryGetValue(edge.TypeDefinitionName, out edgeClass) + ? EdgeTypeInfo.CreateEdge( + compilation, + edge.Type, + edgeClass.ClassDeclarations, + edge.Name, + edge.NameFormat) + : EdgeTypeInfo.CreateEdge( + compilation, + edge.Type, + null, + edge.Name, + edge.NameFormat); + + connectionTypeInfo = + connectionClassLookup.TryGetValue(connection.TypeDefinitionName, out connectionClass) + ? ConnectionTypeInfo.CreateConnection( + compilation, + connection.Type, + connectionClass.ClassDeclarations, + edgeTypeInfo.Name, + connection.Name, + connection.NameFormat) + : ConnectionTypeInfo.CreateConnection( + compilation, + connection.Type, + null, + edgeTypeInfo.Name, + connection.Name, + connection.NameFormat); + + var connectionTypeName = "global::" + connectionTypeInfo.Namespace + "." + connectionTypeInfo.Name; + var edgeTypeName = "global::" + edgeTypeInfo.Namespace + "." + edgeTypeInfo.Name; + + if (!connectionTypeLookup.ContainsKey(connectionTypeName)) + { + connectionTypeInfos ??= []; + connectionTypeInfos.Add(connectionTypeInfo); + connectionTypeLookup.Add(connectionTypeName, connectionTypeInfo); + } + + if (!connectionTypeLookup.ContainsKey(edgeTypeName)) + { + connectionTypeInfos ??= []; + connectionTypeInfos.Add(edgeTypeInfo); + connectionTypeLookup.Add(edgeTypeName, edgeTypeInfo); + } + } + else + { + var edgeType = GetEdgeType(connectionType); + if (edgeType is null) + { + continue; + } + + string? connectionName = null; + string? edgeName = null; + + if (compilation.TryGetConnectionNameFromResolver(connectionResolver.Member, out var name)) + { + connectionName = $"{name}Connection"; + edgeName = $"{name}Edge"; + } + + edgeTypeInfo = + connectionClassLookup.TryGetValue(edgeType.ToFullyQualified(), out edgeClass) + ? EdgeTypeInfo.CreateEdgeFrom(edgeClass, edgeName, edgeName) + : EdgeTypeInfo.CreateEdge(compilation, edgeType, null, edgeName, edgeName); + + connectionTypeInfo = + connectionClassLookup.TryGetValue(connectionType.ToFullyQualified(), out connectionClass) + ? ConnectionTypeInfo.CreateConnectionFrom( + connectionClass, + edgeTypeInfo.Name, + connectionName, + connectionName) + : ConnectionTypeInfo.CreateConnection( + compilation, + connectionType, + null, + edgeType.Name, + connectionName, + connectionName); + + var connectionTypeName = "global::" + connectionTypeInfo.Namespace + "." + connectionTypeInfo.Name; + var edgeTypeName = "global::" + edgeTypeInfo.Namespace + "." + edgeTypeInfo.Name; + + if (!connectionTypeLookup.ContainsKey(connectionTypeName)) + { + connectionTypeInfos ??= []; + connectionTypeInfos.Add(connectionTypeInfo); + connectionTypeLookup.Add(connectionTypeName, connectionTypeInfo); + } + + if (!connectionTypeLookup.ContainsKey(edgeTypeName)) + { + connectionTypeInfos ??= []; + connectionTypeInfos.Add(edgeTypeInfo); + connectionTypeLookup.Add(edgeTypeName, edgeTypeInfo); + } + } + + owner.ReplaceResolver( + connectionResolver, + connectionResolver.WithSchemaTypeName( + $"{connectionTypeInfo.Namespace}.{connectionTypeInfo.Name}")); + } + + if (connectionTypeInfos is not null) + { + return syntaxInfos.AddRange(connectionTypeInfos); + } + } + + return syntaxInfos; + } + + private static INamedTypeSymbol GetConnectionType(Compilation compilation, ITypeSymbol? possibleConnectionType) + { + if (possibleConnectionType is null) + { + Throw(); + } + + if (compilation.IsTaskOrValueTask(possibleConnectionType, out var type)) + { + if (type is INamedTypeSymbol namedType1) + { + return namedType1; + } + + Throw(); + } + + if (possibleConnectionType is not INamedTypeSymbol namedType2) + { + Throw(); + } + + return namedType2; + + [DoesNotReturn] + static void Throw() => throw new InvalidOperationException("Could not resolve connection base type."); + } + + private static GenericTypeInfo? CreateGenericTypeInfo( + INamedTypeSymbol genericType, + ISymbol resolver, + Compilation compilation, + bool isConnection, + Dictionary connectionNameLookup, + ref ImmutableArray diagnostics) + { + if (genericType.TypeArguments.Length > 1) + { + // we can only handle connections/edges with a single type argument. + // the generic type argument must represent the entity. + diagnostics = diagnostics.Add( + Diagnostic.Create( + Errors.ConnectionSingleGenericTypeArgument, + resolver.Locations[0])); + return null; + } + + // first we check if there is a name template defined for the specified connection. + if (compilation.TryGetGraphQLTypeName(genericType, out var nameFormat) + && (string.IsNullOrEmpty(nameFormat) || !nameFormat.Contains("{0}"))) + { + diagnostics = diagnostics.Add( + Diagnostic.Create( + Errors.ConnectionNameFormatIsInvalid, + resolver.Locations[0])); + return null; + } + + // next we get the generic type definition for the connection type. + var typeDefinition = genericType.OriginalDefinition; + var typeDefinitionName = typeDefinition.ToFullyQualified(); + + // if we do not have a name format we will create one from the generic type definition. + nameFormat ??= $"{{0}}{typeDefinition.Name}"; + + if (compilation.TryGetConnectionNameFromResolver(resolver, out var name)) + { + // if the user specified that for this resolver there should be a specific connection name, + // then we will take the defined connection name and discard the name format. + name = isConnection ? $"{name}Connection" : $"{name}Edge"; + nameFormat = null; + } + else + { + // if there was no connection name specified we will construct it with the name format. + name = string.Format(nameFormat, genericType.TypeArguments[0].Name); + } + + // we will now create the full connection type name. + var typeName = $"{genericType.ContainingNamespace.ToDisplayString()}.{name}"; + var runtimeTypeName = genericType.ToDisplayString(); + + // we need to make sure that not different .NET types represent the same connection name. + if (connectionNameLookup.TryGetValue(typeName, out var expectedRuntimeTypeName) + && !string.Equals(expectedRuntimeTypeName, runtimeTypeName, StringComparison.Ordinal)) + { + diagnostics = diagnostics.Add( + Diagnostic.Create( + Errors.ConnectionNameDuplicate, + resolver.Locations[0], + messageArgs: [runtimeTypeName, typeName, expectedRuntimeTypeName])); + return null; + } + + // we will store the connection name and the runtime type name for later reference. + connectionNameLookup[typeName] = runtimeTypeName; + return new GenericTypeInfo(typeDefinitionName, genericType, name, nameFormat); + } + + private record GenericTypeInfo( + string TypeDefinitionName, + INamedTypeSymbol Type, + string Name, + string? NameFormat); + + private static INamedTypeSymbol? GetEdgeType(INamedTypeSymbol connectionType) + { + var property = connectionType.GetMembers() + .OfType() + .FirstOrDefault(p => p.Name == "Edges"); + + if (property is null) + { + return null; + } + + var returnType = property.GetReturnType(); + if (returnType is not INamedTypeSymbol namedType + || !namedType.IsGenericType + || namedType.TypeArguments.Length != 1 + || namedType.Name != "IReadOnlyList") + { + return null; + } + + return (INamedTypeSymbol)namedType.TypeArguments[0]; + } +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderDefaultsInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderDefaultsInspector.cs index a10b5d0c4a9..ff43f5ab448 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderDefaultsInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderDefaultsInspector.cs @@ -1,18 +1,21 @@ +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using HotChocolate.Types.Analyzers.Filters; using HotChocolate.Types.Analyzers.Helpers; using HotChocolate.Types.Analyzers.Models; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static System.StringComparison; using static HotChocolate.Types.Analyzers.WellKnownAttributes; -using static HotChocolate.Types.Analyzers.WellKnownTypes; namespace HotChocolate.Types.Analyzers.Inspectors; public class DataLoaderDefaultsInspector : ISyntaxInspector { - public IReadOnlyList Filters => [AssemblyAttributeList.Instance]; + public ImmutableArray Filters { get; } = [AssemblyAttributeList.Instance]; + + public IImmutableSet SupportedKinds { get; } = [SyntaxKind.AttributeList]; public bool TryHandle( GeneratorSyntaxContext context, @@ -22,7 +25,7 @@ public bool TryHandle( { foreach (var attributeSyntax in attributeList.Attributes) { - var symbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol; + var symbol = ModelExtensions.GetSymbolInfo(context.SemanticModel, attributeSyntax).Symbol; if (symbol is not IMethodSymbol attributeSymbol) { diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderInspector.cs index 2e3890c65d2..7d490b037cd 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderInspector.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using HotChocolate.Types.Analyzers.Filters; using HotChocolate.Types.Analyzers.Models; @@ -10,7 +11,9 @@ namespace HotChocolate.Types.Analyzers.Inspectors; public sealed class DataLoaderInspector : ISyntaxInspector { - public IReadOnlyList Filters => [MethodWithAttribute.Instance]; + public ImmutableArray Filters { get; } = [MethodWithAttribute.Instance]; + + public IImmutableSet SupportedKinds { get; } = [SyntaxKind.MethodDeclaration]; public bool TryHandle( GeneratorSyntaxContext context, @@ -32,8 +35,8 @@ public bool TryHandle( var attributeContainingTypeSymbol = attributeSymbol.ContainingType; var fullName = attributeContainingTypeSymbol.ToDisplayString(); - if (fullName.Equals(WellKnownAttributes.DataLoaderAttribute, Ordinal) && - context.SemanticModel.GetDeclaredSymbol(methodSyntax) is { } methodSymbol) + if (fullName.Equals(WellKnownAttributes.DataLoaderAttribute, Ordinal) + && context.SemanticModel.GetDeclaredSymbol(methodSyntax) is { } methodSymbol) { syntaxInfo = new DataLoaderInfo( attributeSyntax, diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderModuleInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderModuleInspector.cs index db59460b359..be6c70f9a8f 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderModuleInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderModuleInspector.cs @@ -1,9 +1,11 @@ +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using HotChocolate.Types.Analyzers.Filters; using HotChocolate.Types.Analyzers.Models; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using HotChocolate.Types.Analyzers.Helpers; +using Microsoft.CodeAnalysis.CSharp; using static System.StringComparison; using static HotChocolate.Types.Analyzers.WellKnownAttributes; @@ -11,7 +13,9 @@ namespace HotChocolate.Types.Analyzers.Inspectors; public sealed class DataLoaderModuleInspector : ISyntaxInspector { - public IReadOnlyList Filters => [AssemblyAttributeList.Instance]; + public ImmutableArray Filters { get; } = [AssemblyAttributeList.Instance]; + + public IImmutableSet SupportedKinds { get; } = [SyntaxKind.AttributeList]; public bool TryHandle( GeneratorSyntaxContext context, @@ -21,7 +25,7 @@ public bool TryHandle( { foreach (var attributeSyntax in attributeList.Attributes) { - var symbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol; + var symbol = ModelExtensions.GetSymbolInfo(context.SemanticModel, attributeSyntax).Symbol; if (symbol is not IMethodSymbol attributeSymbol) { continue; diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/IPostCollectSyntaxTransformer.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/IPostCollectSyntaxTransformer.cs new file mode 100644 index 00000000000..49584178cb5 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/IPostCollectSyntaxTransformer.cs @@ -0,0 +1,15 @@ +using System.Collections.Immutable; +using HotChocolate.Types.Analyzers.Models; +using Microsoft.CodeAnalysis; + +namespace HotChocolate.Types.Analyzers.Inspectors; + +/// +/// The post collect syntax transformer allows to create syntax infos based on the collected syntax infos. +/// +public interface IPostCollectSyntaxTransformer +{ + ImmutableArray Transform( + Compilation compilation, + ImmutableArray syntaxInfos); +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ISyntaxInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ISyntaxInspector.cs index 10255ab5f8d..276f59cbfed 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ISyntaxInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ISyntaxInspector.cs @@ -1,7 +1,9 @@ +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using HotChocolate.Types.Analyzers.Filters; using HotChocolate.Types.Analyzers.Models; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; namespace HotChocolate.Types.Analyzers.Inspectors; @@ -14,14 +16,21 @@ public interface ISyntaxInspector /// /// Gets the filters that is used to determine in what kinds of syntax nodes the inspector is interested. /// - IReadOnlyList Filters { get; } + ImmutableArray Filters { get; } + + /// + /// Gets the kinds of syntax nodes that the inspector is interested in. + /// + IImmutableSet SupportedKinds { get; } /// /// /// Inspects the current syntax node and if the current inspector can handle /// the syntax will produce a syntax info. /// - /// The syntax info is used by a syntax generator to produce source code. + /// + /// The syntax info is used by a syntax generator to produce source code. + /// /// bool TryHandle( GeneratorSyntaxContext context, diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/InterfaceTypeInfoInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/InterfaceTypeInfoInspector.cs index 7523667a539..d53247ced27 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/InterfaceTypeInfoInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/InterfaceTypeInfoInspector.cs @@ -11,7 +11,9 @@ namespace HotChocolate.Types.Analyzers.Inspectors; public class InterfaceTypeInfoInspector : ISyntaxInspector { - public IReadOnlyList Filters => [TypeWithAttribute.Instance]; + public ImmutableArray Filters { get; } = [TypeWithAttribute.Instance]; + + public IImmutableSet SupportedKinds { get; } = [SyntaxKind.ClassDeclaration]; public bool TryHandle(GeneratorSyntaxContext context, [NotNullWhen(true)] out SyntaxInfo? syntaxInfo) { @@ -70,13 +72,13 @@ public bool TryHandle(GeneratorSyntaxContext context, [NotNullWhen(true)] out Sy Array.Resize(ref resolvers, i); } - syntaxInfo = new InterfaceTypeExtensionInfo( + syntaxInfo = new InterfaceTypeInfo( classSymbol, runtimeType, possibleType, i == 0 ? ImmutableArray.Empty - : resolvers.ToImmutableArray()); + : [..resolvers]); if (diagnostics.Length > 0) { @@ -148,7 +150,7 @@ private static Resolver CreateResolver( resolverType.Name, resolverMethod, resolverMethod.GetResultKind(), - resolverParameters.ToImmutableArray(), + [..resolverParameters], ImmutableArray.Empty); } } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ModuleInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ModuleInspector.cs index 8d33abff4be..b028ea281dc 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ModuleInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ModuleInspector.cs @@ -1,7 +1,9 @@ +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using HotChocolate.Types.Analyzers.Filters; using HotChocolate.Types.Analyzers.Models; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static System.StringComparison; using static HotChocolate.Types.Analyzers.WellKnownAttributes; @@ -10,7 +12,9 @@ namespace HotChocolate.Types.Analyzers.Inspectors; public sealed class ModuleInspector : ISyntaxInspector { - public IReadOnlyList Filters => [AssemblyAttributeList.Instance]; + public ImmutableArray Filters { get; } = [AssemblyAttributeList.Instance]; + + public IImmutableSet SupportedKinds { get; } = [SyntaxKind.AttributeList]; public bool TryHandle( GeneratorSyntaxContext context, @@ -20,7 +24,7 @@ public bool TryHandle( { foreach (var attributeSyntax in attributeList.Attributes) { - var symbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol; + var symbol = ModelExtensions.GetSymbolInfo(context.SemanticModel, attributeSyntax).Symbol; if (symbol is not IMethodSymbol attributeSymbol) { continue; diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ObjectTypeExtensionInfoInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ObjectTypeInspector.cs similarity index 88% rename from src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ObjectTypeExtensionInfoInspector.cs rename to src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ObjectTypeInspector.cs index 24f55caa6c1..bcc674aa590 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ObjectTypeExtensionInfoInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ObjectTypeInspector.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; using HotChocolate.Types.Analyzers.Filters; using HotChocolate.Types.Analyzers.Helpers; using HotChocolate.Types.Analyzers.Models; @@ -11,20 +12,27 @@ namespace HotChocolate.Types.Analyzers.Inspectors; -public class ObjectTypeExtensionInfoInspector : ISyntaxInspector +public class ObjectTypeInspector : ISyntaxInspector { - public IReadOnlyList Filters => [TypeWithAttribute.Instance]; + public ImmutableArray Filters { get; } = [TypeWithAttribute.Instance]; + + public IImmutableSet SupportedKinds { get; } = [SyntaxKind.ClassDeclaration]; public bool TryHandle(GeneratorSyntaxContext context, [NotNullWhen(true)] out SyntaxInfo? syntaxInfo) { var diagnostics = ImmutableArray.Empty; + var isOperationType = false; OperationType? operationType = null; - if (!IsObjectTypeExtension(context, out var possibleType, out var classSymbol, out var runtimeType) - && !IsOperationType(context, out possibleType, out classSymbol, out operationType)) + if (!IsObjectTypeExtension(context, out var possibleType, out var classSymbol, out var runtimeType)) { - syntaxInfo = null; - return false; + if (!IsOperationType(context, out possibleType, out classSymbol, out operationType)) + { + syntaxInfo = null; + return false; + } + + isOperationType = true; } if (!possibleType.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) @@ -50,7 +58,7 @@ public bool TryHandle(GeneratorSyntaxContext context, [NotNullWhen(true)] out Sy foreach (var member in members) { - if (member.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal) + if (member.DeclaredAccessibility is Accessibility.Public && !member.IsIgnored()) { if (member is IMethodSymbol { MethodKind: MethodKind.Ordinary } methodSymbol) { @@ -59,7 +67,7 @@ public bool TryHandle(GeneratorSyntaxContext context, [NotNullWhen(true)] out Sy continue; } - if (methodSymbol.IsNodeResolver()) + if (!isOperationType && methodSymbol.IsNodeResolver()) { nodeResolver = CreateNodeResolver(context, classSymbol, methodSymbol, ref diagnostics); } @@ -89,14 +97,14 @@ public bool TryHandle(GeneratorSyntaxContext context, [NotNullWhen(true)] out Sy if (runtimeType is not null) { - syntaxInfo = new ObjectTypeExtensionInfo( + syntaxInfo = new ObjectTypeInfo( classSymbol, runtimeType, nodeResolver, possibleType, i == 0 ? ImmutableArray.Empty - : resolvers.ToImmutableArray()); + : ImmutableCollectionsMarshal.AsImmutableArray(resolvers)); if (diagnostics.Length > 0) { @@ -105,13 +113,13 @@ public bool TryHandle(GeneratorSyntaxContext context, [NotNullWhen(true)] out Sy return true; } - syntaxInfo = new RootTypeExtensionInfo( + syntaxInfo = new RootTypeInfo( classSymbol, operationType!.Value, possibleType, i == 0 ? ImmutableArray.Empty - : resolvers.ToImmutableArray()); + : ImmutableCollectionsMarshal.AsImmutableArray(resolvers)); if (diagnostics.Length > 0) { @@ -226,8 +234,14 @@ private static Resolver CreateResolver( GeneratorSyntaxContext context, INamedTypeSymbol resolverType, IMethodSymbol resolverMethod) + => CreateResolver(context.SemanticModel.Compilation, resolverType, resolverMethod); + + public static Resolver CreateResolver( + Compilation compilation, + INamedTypeSymbol resolverType, + IMethodSymbol resolverMethod, + string? resolverTypeName = null) { - var compilation = context.SemanticModel.Compilation; var parameters = resolverMethod.Parameters; var resolverParameters = new ResolverParameter[parameters.Length]; @@ -236,12 +250,17 @@ private static Resolver CreateResolver( resolverParameters[i] = ResolverParameter.Create(parameters[i], compilation); } + resolverTypeName ??= resolverType.Name; + return new Resolver( - resolverType.Name, + resolverTypeName, resolverMethod, resolverMethod.GetResultKind(), resolverParameters.ToImmutableArray(), - resolverMethod.GetMemberBindings()); + resolverMethod.GetMemberBindings(), + kind: resolverMethod.ReturnType.IsConnectionBase() + ? ResolverKind.ConnectionResolver + : ResolverKind.Default); } private static Resolver CreateNodeResolver( @@ -295,8 +314,11 @@ private static Resolver CreateNodeResolver( resolverMethod.GetResultKind(), resolverParameters.ToImmutableArray(), resolverMethod.GetMemberBindings(), - isNodeResolver: true); + kind: ResolverKind.NodeResolver); } + + public static ImmutableArray GetMemberBindings(ISymbol member) + => member.GetMemberBindings(); } file static class Extensions diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInspector.cs index 65607a512fc..aa93001d21d 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInspector.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using HotChocolate.Types.Analyzers.Filters; using HotChocolate.Types.Analyzers.Models; @@ -9,7 +10,9 @@ namespace HotChocolate.Types.Analyzers.Inspectors; public sealed class OperationInspector : ISyntaxInspector { - public IReadOnlyList Filters => [MethodWithAttribute.Instance]; + public ImmutableArray Filters { get; } = [MethodWithAttribute.Instance]; + + public IImmutableSet SupportedKinds { get; } = [SyntaxKind.MethodDeclaration]; public bool TryHandle( GeneratorSyntaxContext context, diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/RequestMiddlewareInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/RequestMiddlewareInspector.cs index 5f5966097bc..f92f5d123f9 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/RequestMiddlewareInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/RequestMiddlewareInspector.cs @@ -4,13 +4,16 @@ using HotChocolate.Types.Analyzers.Helpers; using HotChocolate.Types.Analyzers.Models; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace HotChocolate.Types.Analyzers.Inspectors; internal sealed class RequestMiddlewareInspector : ISyntaxInspector { - public IReadOnlyList Filters => [MiddlewareMethod.Instance]; + public ImmutableArray Filters { get; } = [MiddlewareMethod.Instance]; + + public IImmutableSet SupportedKinds { get; } = [SyntaxKind.InvocationExpression]; public bool TryHandle(GeneratorSyntaxContext context, [NotNullWhen(true)] out SyntaxInfo? syntaxInfo) { @@ -27,7 +30,7 @@ public bool TryHandle(GeneratorSyntaxContext context, [NotNullWhen(true)] out Sy } node) { var semanticModel = context.SemanticModel; - var middlewareType = semanticModel.GetTypeInfo(args.Arguments[0]).Type; + var middlewareType = ModelExtensions.GetTypeInfo(semanticModel, args.Arguments[0]).Type; if (middlewareType is null) { diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/TypeAttributeInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/TypeAttributeInspector.cs index 97bc3a9d375..ba103ceef3b 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/TypeAttributeInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/TypeAttributeInspector.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using HotChocolate.Types.Analyzers.Filters; using HotChocolate.Types.Analyzers.Models; @@ -12,7 +13,15 @@ namespace HotChocolate.Types.Analyzers.Inspectors; public sealed class TypeAttributeInspector : ISyntaxInspector { - public IReadOnlyList Filters => [TypeWithAttribute.Instance]; + public ImmutableArray Filters { get; } = [TypeWithAttribute.Instance]; + + public IImmutableSet SupportedKinds { get; } = + [ + SyntaxKind.ClassDeclaration, + SyntaxKind.RecordDeclaration, + SyntaxKind.InterfaceDeclaration, + SyntaxKind.EnumDeclaration + ]; public bool TryHandle( GeneratorSyntaxContext context, diff --git a/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs b/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs new file mode 100644 index 00000000000..b55520cb6b2 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs @@ -0,0 +1,157 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace HotChocolate.Types.Analyzers; + +public static class KnownSymbols +{ + public static bool TryGetConnectionNameFromResolver( + this Compilation compilation, + ISymbol resolver, + [NotNullWhen(true)] out string? name) + { + var useConnectionAttribute = compilation.GetTypeByMetadataName(WellKnownAttributes.UseConnectionAttribute); + + if (useConnectionAttribute is null) + { + name = null; + return false; + } + + const string connectionName = "ConnectionName"; + const string connection = "Connection"; + + foreach (var attributeData in resolver.GetAttributes()) + { + if (!SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, useConnectionAttribute)) + { + continue; + } + + foreach (var namedArg in attributeData.NamedArguments) + { + if (namedArg is { Key: connectionName, Value.Value: string namedValue }) + { + if (namedValue.EndsWith(connection)) + { + namedValue = namedValue[..^connection.Length]; + } + + name = namedValue; + return true; + } + } + } + + name = null; + return false; + } + + public static bool TryGetGraphQLTypeName( + this Compilation compilation, + ISymbol symbol, + [NotNullWhen(true)] out string? name) + { + var graphQLNameAttribute = compilation.GetTypeByMetadataName(WellKnownAttributes.GraphQLNameAttribute); + + if (graphQLNameAttribute is null) + { + name = null; + return false; + } + + foreach (var attributeData in symbol.GetAttributes()) + { + if (!SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, graphQLNameAttribute)) + { + continue; + } + + if (attributeData.ConstructorArguments.Length > 0 && + attributeData.ConstructorArguments[0].Value is string attributeValue) + { + name = attributeValue; + return true; + } + } + + name = null; + return false; + } + + public static INamedTypeSymbol GetConnectionBaseSymbol(this GeneratorSyntaxContext context) + => context.SemanticModel.Compilation.GetConnectionBaseSymbol(); + + public static INamedTypeSymbol GetConnectionBaseSymbol(this Compilation compilation) + => compilation.GetTypeByMetadataName("HotChocolate.Types.Pagination.ConnectionBase`3") + ?? throw new InvalidOperationException("Could not resolve connection base type."); + + public static INamedTypeSymbol GetTaskSymbol(this Compilation compilation) + => compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1") + ?? throw new InvalidOperationException("Could not resolve connection base type."); + + public static INamedTypeSymbol GetValueTaskSymbol(this Compilation compilation) + => compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask`1") + ?? throw new InvalidOperationException("Could not resolve connection base type."); + + public static bool IsConnectionBase(this GeneratorSyntaxContext context, ITypeSymbol possibleConnectionType) + => context.SemanticModel.Compilation.IsConnectionBase(possibleConnectionType); + + public static bool IsTaskOrValueTask( + this Compilation compilation, + ITypeSymbol possibleTask, + [NotNullWhen(true)] out ITypeSymbol? innerType) + { + if (possibleTask is INamedTypeSymbol { IsGenericType: true } namedType) + { + var taskSymbol = compilation.GetTaskSymbol(); + var valueTaskSymbol = compilation.GetValueTaskSymbol(); + + if (SymbolEqualityComparer.Default.Equals(namedType.ConstructedFrom, taskSymbol) || + SymbolEqualityComparer.Default.Equals(namedType.ConstructedFrom, valueTaskSymbol)) + { + innerType = namedType.TypeArguments[0]; + return true; + } + } + + innerType = null; + return false; + } + + public static bool IsConnectionBase(this Compilation compilation, ITypeSymbol possibleConnectionType) + { + if (compilation.IsTaskOrValueTask(possibleConnectionType, out var innerType)) + { + possibleConnectionType = innerType; + } + + if (possibleConnectionType is not INamedTypeSymbol namedType) + { + return false; + } + + return IsDerivedFromGenericBase(namedType, compilation.GetConnectionBaseSymbol()); + } + + private static bool IsDerivedFromGenericBase(INamedTypeSymbol typeSymbol, INamedTypeSymbol baseTypeSymbol) + { + var current = typeSymbol; + + while (current is not null) + { + if (current is { IsGenericType: true } namedTypeSymbol) + { + var baseType = namedTypeSymbol.ConstructedFrom; + if (SymbolEqualityComparer.Default.Equals(baseType.OriginalDefinition, baseTypeSymbol)) + { + return true; + } + } + + current = current.BaseType; + } + + return false; + } +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionClassInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionClassInfo.cs new file mode 100644 index 00000000000..fdeb2adfa77 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionClassInfo.cs @@ -0,0 +1,139 @@ +using System.Collections.Immutable; +using HotChocolate.Types.Analyzers.Helpers; +using HotChocolate.Types.Analyzers.Inspectors; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace HotChocolate.Types.Analyzers.Models; + +public sealed class ConnectionClassInfo : SyntaxInfo, IEquatable +{ + private ConnectionClassInfo( + INamedTypeSymbol runtimeType, + ClassDeclarationSyntax classDeclarations, + ImmutableArray resolvers) + { + RuntimeType = runtimeType; + ClassDeclarations = classDeclarations; + Resolvers = resolvers; + OrderByKey = runtimeType.ToFullyQualified(); + } + + public INamedTypeSymbol RuntimeType { get; } + + public ClassDeclarationSyntax ClassDeclarations { get; } + + public ImmutableArray Resolvers { get; } + + public override string OrderByKey { get; } + + public override bool Equals(object? obj) + => obj is ConnectionClassInfo other + && Equals(other); + + public override bool Equals(SyntaxInfo? other) + => other is ConnectionClassInfo otherConnectionClassInfo + && Equals(otherConnectionClassInfo); + + public bool Equals(ConnectionClassInfo? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return OrderByKey.Equals(other.OrderByKey, StringComparison.Ordinal) + && ClassDeclarations.SyntaxTree.IsEquivalentTo(other.ClassDeclarations.SyntaxTree); + } + + public override int GetHashCode() + => HashCode.Combine(OrderByKey, ClassDeclarations); + + public static ConnectionClassInfo CreateConnection( + Compilation compilation, + INamedTypeSymbol runtimeType, + ClassDeclarationSyntax classDeclaration) + => Create(compilation, runtimeType, classDeclaration, isConnection: true); + + public static ConnectionClassInfo CreateEdge( + Compilation compilation, + INamedTypeSymbol runtimeType, + ClassDeclarationSyntax classDeclaration) + => Create(compilation, runtimeType, classDeclaration, isConnection: false); + + private static ConnectionClassInfo Create( + Compilation compilation, + INamedTypeSymbol runtimeType, + ClassDeclarationSyntax classDeclaration, + bool isConnection) + { + var name = runtimeType.Name + "Type"; + + var resolvers = ImmutableArray.CreateBuilder(); + + foreach (var member in runtimeType.GetMembers()) + { + if (member.DeclaredAccessibility is not Accessibility.Public + || member.IsStatic + || member.IsIgnored()) + { + continue; + } + + switch (member) + { + case IMethodSymbol method: + if (method.IsPropertyOrEventAccessor() + || method.IsOperator() + || method.IsConstructor() + || method.IsSpecialMethod() + || method.IsCompilerGenerated()) + { + continue; + } + + resolvers.Add(ObjectTypeInspector.CreateResolver(compilation, runtimeType, method, name)); + break; + + case IPropertySymbol property: + var flags = FieldFlags.None; + + if (isConnection) + { + if (property.Name.Equals("Edges", StringComparison.Ordinal)) + { + flags |= FieldFlags.ConnectionEdgesField; + } + else if (property.Name.Equals("Nodes", StringComparison.Ordinal)) + { + flags |= FieldFlags.ConnectionNodesField; + } + else if (property.Name.Equals("TotalCount", StringComparison.Ordinal)) + { + flags |= FieldFlags.TotalCount; + } + } + + resolvers.Add( + new Resolver( + name, + property, + ResolverResultKind.Pure, + ImmutableArray.Empty, + ObjectTypeInspector.GetMemberBindings(member), + flags: flags)); + break; + } + } + + return new ConnectionClassInfo( + runtimeType, + classDeclaration, + resolvers.ToImmutable()); + } +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionTypeInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionTypeInfo.cs new file mode 100644 index 00000000000..f6db415e951 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionTypeInfo.cs @@ -0,0 +1,188 @@ +using System.Collections.Immutable; +using HotChocolate.Types.Analyzers.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static HotChocolate.Types.Analyzers.Inspectors.ObjectTypeInspector; + +namespace HotChocolate.Types.Analyzers.Models; + +public sealed class ConnectionTypeInfo + : SyntaxInfo + , IOutputTypeInfo +{ + private ConnectionTypeInfo( + string name, + string? nameFormat, + string edgeTypeName, + INamedTypeSymbol runtimeType, + ClassDeclarationSyntax? classDeclaration, + ImmutableArray resolvers) + { + Name = name; + NameFormat = nameFormat; + EdgeTypeName = edgeTypeName; + RuntimeTypeFullName = runtimeType.ToDisplayString(); + RuntimeType = runtimeType; + Namespace = runtimeType.ContainingNamespace.ToDisplayString(); + ClassDeclaration = classDeclaration; + Resolvers = resolvers; + } + + public string Name { get; } + + public string? NameFormat { get; } + + public string EdgeTypeName { get; } + + public string Namespace { get; } + + public bool IsPublic => RuntimeType.DeclaredAccessibility == Accessibility.Public; + + public INamedTypeSymbol? SchemaSchemaType => null; + + public string? SchemaTypeFullName => null; + + public bool HasSchemaType => false; + + public INamedTypeSymbol RuntimeType { get; } + + public string RuntimeTypeFullName { get; } + + public bool HasRuntimeType => true; + + public ClassDeclarationSyntax? ClassDeclaration { get; } + + public ImmutableArray Resolvers { get; private set; } + + public override string OrderByKey => RuntimeTypeFullName; + + public void ReplaceResolver(Resolver current, Resolver replacement) + => Resolvers = Resolvers.Replace(current, replacement); + + public override bool Equals(object? obj) + => obj is ConnectionTypeInfo other && Equals(other); + + public override bool Equals(SyntaxInfo? obj) + => obj is ConnectionTypeInfo other && Equals(other); + + private bool Equals(ConnectionTypeInfo other) + { + if (!string.Equals(OrderByKey, other.OrderByKey, StringComparison.Ordinal)) + { + return false; + } + + if (ClassDeclaration is null) + { + return other.ClassDeclaration is null; + } + + if (other.ClassDeclaration is null) + { + return false; + } + + return ClassDeclaration.SyntaxTree.IsEquivalentTo( + other.ClassDeclaration.SyntaxTree); + } + + public override int GetHashCode() + => HashCode.Combine(OrderByKey, ClassDeclaration); + + public static ConnectionTypeInfo CreateConnectionFrom( + ConnectionClassInfo connectionClass, + string edgeTypeName, + string? name = null, + string? nameFormat = null) + { + return new ConnectionTypeInfo( + (name ?? connectionClass.RuntimeType.Name) + "Type", + nameFormat, + edgeTypeName, + connectionClass.RuntimeType, + connectionClass.ClassDeclarations, + connectionClass.Resolvers); + } + + public static ConnectionTypeInfo CreateConnection( + Compilation compilation, + INamedTypeSymbol runtimeType, + ClassDeclarationSyntax? classDeclaration, + string edgeTypeName, + string? name = null, + string? nameFormat = null) + => Create(compilation, runtimeType, classDeclaration, edgeTypeName, name, nameFormat); + + private static ConnectionTypeInfo Create( + Compilation compilation, + INamedTypeSymbol runtimeType, + ClassDeclarationSyntax? classDeclaration, + string edgeTypeName, + string? name = null, + string? nameFormat = null) + { + var connectionName = (name ?? runtimeType.Name) + "Type"; + + var resolvers = ImmutableArray.CreateBuilder(); + + foreach (var member in runtimeType.GetMembers()) + { + if (member.DeclaredAccessibility is not Accessibility.Public + || member.IsStatic + || member.IsIgnored()) + { + continue; + } + + switch (member) + { + case IMethodSymbol method: + if (method.IsPropertyOrEventAccessor() + || method.IsOperator() + || method.IsConstructor() + || method.IsSpecialMethod() + || method.IsCompilerGenerated()) + { + continue; + } + + resolvers.Add(CreateResolver(compilation, runtimeType, method, connectionName)); + break; + + case IPropertySymbol property: + var flags = FieldFlags.None; + + if (property.Name.Equals("Edges", StringComparison.Ordinal)) + { + flags |= FieldFlags.ConnectionEdgesField; + } + else if (property.Name.Equals("Nodes", StringComparison.Ordinal)) + { + flags |= FieldFlags.ConnectionNodesField; + } + else if (property.Name.Equals("TotalCount", StringComparison.Ordinal)) + { + flags |= FieldFlags.TotalCount; + } + + resolvers.Add( + new Resolver( + connectionName, + property, + ResolverResultKind.Pure, + ImmutableArray.Empty, + GetMemberBindings(member), + flags: flags)); + break; + } + } + + return new ConnectionTypeInfo( + connectionName, + nameFormat, + edgeTypeName, + runtimeType, + classDeclaration, + resolvers.ToImmutable()); + } +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderDefaultsInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderDefaultsInfo.cs index e03bac44820..14ecc577012 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderDefaultsInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderDefaultsInfo.cs @@ -23,7 +23,7 @@ public sealed class DataLoaderDefaultsInfo( public override bool Equals(object? obj) => obj is DataLoaderDefaultsInfo other && Equals(other); - public override bool Equals(SyntaxInfo other) + public override bool Equals(SyntaxInfo? other) => other is DataLoaderDefaultsInfo info && Equals(info); private bool Equals(DataLoaderDefaultsInfo other) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderInfo.cs index bea56cc7943..df249aef3cc 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderInfo.cs @@ -306,7 +306,7 @@ public static bool IsKeyValuePair(ITypeSymbol returnTypeSymbol, ITypeSymbol keyT public override bool Equals(object? obj) => obj is DataLoaderInfo other && Equals(other); - public override bool Equals(SyntaxInfo obj) + public override bool Equals(SyntaxInfo? obj) => obj is DataLoaderInfo other && Equals(other); private bool Equals(DataLoaderInfo other) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderModuleInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderModuleInfo.cs index 2d4d42c6b3c..d176c735ff8 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderModuleInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderModuleInfo.cs @@ -11,7 +11,7 @@ public sealed class DataLoaderModuleInfo(string moduleName, bool isInternal) : S public override bool Equals(object? obj) => obj is DataLoaderModuleInfo other && Equals(other); - public override bool Equals(SyntaxInfo obj) + public override bool Equals(SyntaxInfo? obj) => obj is DataLoaderModuleInfo other && Equals(other); private bool Equals(DataLoaderModuleInfo other) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/EdgeTypeInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/EdgeTypeInfo.cs new file mode 100644 index 00000000000..a2d045a0c10 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/EdgeTypeInfo.cs @@ -0,0 +1,164 @@ +using System.Collections.Immutable; +using HotChocolate.Types.Analyzers.Helpers; +using HotChocolate.Types.Analyzers.Inspectors; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace HotChocolate.Types.Analyzers.Models; + +public sealed class EdgeTypeInfo + : SyntaxInfo + , IOutputTypeInfo +{ + private EdgeTypeInfo( + string name, + string? nameFormat, + INamedTypeSymbol runtimeType, + ClassDeclarationSyntax? classDeclaration, + ImmutableArray resolvers) + { + Name = name; + NameFormat = nameFormat; + RuntimeTypeFullName = runtimeType.ToDisplayString(); + RuntimeType = runtimeType; + Namespace = runtimeType.ContainingNamespace.ToDisplayString(); + ClassDeclaration = classDeclaration; + Resolvers = resolvers; + } + + public string Name { get; } + + public string? NameFormat { get; } + + public string Namespace { get; } + + public bool IsPublic => RuntimeType.DeclaredAccessibility == Accessibility.Public; + + public INamedTypeSymbol? SchemaSchemaType => null; + + public string? SchemaTypeFullName => null; + + public bool HasSchemaType => false; + + public INamedTypeSymbol RuntimeType { get; } + + public string RuntimeTypeFullName { get; } + + public bool HasRuntimeType => true; + + public ClassDeclarationSyntax? ClassDeclaration { get; } + + public ImmutableArray Resolvers { get; private set; } + + public override string OrderByKey => RuntimeTypeFullName; + + public void ReplaceResolver(Resolver current, Resolver replacement) + => Resolvers = Resolvers.Replace(current, replacement); + + public override bool Equals(object? obj) + => obj is ConnectionTypeInfo other && Equals(other); + + public override bool Equals(SyntaxInfo? obj) + => obj is ConnectionTypeInfo other && Equals(other); + + private bool Equals(ConnectionTypeInfo other) + { + if (!string.Equals(OrderByKey, other.OrderByKey, StringComparison.Ordinal)) + { + return false; + } + + if (ClassDeclaration is null) + { + return other.ClassDeclaration is null; + } + + if (other.ClassDeclaration is null) + { + return false; + } + + return ClassDeclaration.SyntaxTree.IsEquivalentTo( + other.ClassDeclaration.SyntaxTree); + } + + public override int GetHashCode() + => HashCode.Combine(OrderByKey, ClassDeclaration); + + public static EdgeTypeInfo CreateEdgeFrom( + ConnectionClassInfo connectionClass, + string? name = null, + string? nameFormat = null) + { + return new EdgeTypeInfo( + (name ?? connectionClass.RuntimeType.Name) + "Type", + nameFormat, + connectionClass.RuntimeType, + connectionClass.ClassDeclarations, + connectionClass.Resolvers); + } + + public static EdgeTypeInfo CreateEdge( + Compilation compilation, + INamedTypeSymbol runtimeType, + ClassDeclarationSyntax? classDeclaration, + string? name = null, + string? nameFormat = null) + => Create(compilation, runtimeType, classDeclaration, name, nameFormat); + + private static EdgeTypeInfo Create( + Compilation compilation, + INamedTypeSymbol runtimeType, + ClassDeclarationSyntax? classDeclaration, + string? name = null, + string? nameFormat = null) + { + var edgeName = (name ?? runtimeType.Name) + "Type"; + + var resolvers = ImmutableArray.CreateBuilder(); + + foreach (var member in runtimeType.GetMembers()) + { + if (member.DeclaredAccessibility is not Accessibility.Public + || member.IsStatic + || member.IsIgnored()) + { + continue; + } + + switch (member) + { + case IMethodSymbol method: + if (method.IsPropertyOrEventAccessor() + || method.IsOperator() + || method.IsConstructor() + || method.IsSpecialMethod() + || method.IsCompilerGenerated()) + { + continue; + } + + resolvers.Add(ObjectTypeInspector.CreateResolver(compilation, runtimeType, method, edgeName)); + break; + + case IPropertySymbol property: + resolvers.Add( + new Resolver( + edgeName, + property, + ResolverResultKind.Pure, + ImmutableArray.Empty, + ObjectTypeInspector.GetMemberBindings(member), + flags: FieldFlags.None)); + break; + } + } + + return new EdgeTypeInfo( + edgeName, + nameFormat, + runtimeType, + classDeclaration, + resolvers.ToImmutable()); + } +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/FieldFlags.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/FieldFlags.cs new file mode 100644 index 00000000000..fce80f2ad67 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/FieldFlags.cs @@ -0,0 +1,10 @@ +namespace HotChocolate.Types.Analyzers.Models; + +[Flags] +public enum FieldFlags +{ + None = 0, + TotalCount = 65536, + ConnectionEdgesField = 524288, + ConnectionNodesField = 1048576 +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/IOutputTypeInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/IOutputTypeInfo.cs index fd83b9696a1..80b0d26314a 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/IOutputTypeInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/IOutputTypeInfo.cs @@ -7,15 +7,57 @@ namespace HotChocolate.Types.Analyzers.Models; public interface IOutputTypeInfo { + /// + /// Gets the name that the generator shall use to generate the output type. + /// string Name { get; } - bool IsRootType { get; } + /// + /// Gets the namespace that the generator shall use to generate the output type. + /// + string Namespace { get; } - INamedTypeSymbol Type { get; } + /// + /// Defines if the type is a public. + /// + bool IsPublic { get; } + /// + /// Gets the schema type symbol. + /// + INamedTypeSymbol? SchemaSchemaType { get; } + + /// + /// Gets the full schema type name. + /// + string? SchemaTypeFullName { get; } + + /// + /// Specifies if this type info has a schema type. + /// + [MemberNotNull(nameof(SchemaTypeFullName), nameof(SchemaSchemaType))] + bool HasSchemaType { get; } + + /// + /// Gets the runtime type symbol. + /// INamedTypeSymbol? RuntimeType { get; } - ClassDeclarationSyntax ClassDeclarationSyntax { get; } + /// + /// Gets the full runtime type name. + /// + string? RuntimeTypeFullName { get; } + + /// + /// Specifies if this type info has a runtime type. + /// + [MemberNotNull(nameof(RuntimeTypeFullName), nameof(RuntimeType))] + bool HasRuntimeType { get; } + + /// + /// Gets the class declaration if one exists that this type info is based on. + /// + ClassDeclarationSyntax? ClassDeclaration { get; } ImmutableArray Resolvers { get; } @@ -24,4 +66,8 @@ public interface IOutputTypeInfo void AddDiagnostic(Diagnostic diagnostic); void AddDiagnosticRange(ImmutableArray diagnostics); + + void ReplaceResolver( + Resolver current, + Resolver replacement); } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/InterfaceTypeExtensionInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/InterfaceTypeExtensionInfo.cs deleted file mode 100644 index f91b242fedc..00000000000 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/InterfaceTypeExtensionInfo.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Immutable; -using HotChocolate.Types.Analyzers.Helpers; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace HotChocolate.Types.Analyzers.Models; - -public sealed class InterfaceTypeExtensionInfo( - INamedTypeSymbol type, - INamedTypeSymbol runtimeType, - ClassDeclarationSyntax classDeclarationSyntax, - ImmutableArray resolvers) - : SyntaxInfo - , IOutputTypeInfo -{ - public string Name { get; } = type.ToFullyQualified(); - - public bool IsRootType => false; - - public INamedTypeSymbol Type { get; } = type; - - public INamedTypeSymbol RuntimeType { get; } = runtimeType; - - public ClassDeclarationSyntax ClassDeclarationSyntax { get; } = classDeclarationSyntax; - - public ImmutableArray Resolvers { get; } = resolvers; - - public override string OrderByKey => Name; - - public override bool Equals(object? obj) - => obj is ObjectTypeExtensionInfo other && Equals(other); - - public override bool Equals(SyntaxInfo obj) - => obj is ObjectTypeExtensionInfo other && Equals(other); - - private bool Equals(ObjectTypeExtensionInfo other) - => string.Equals(Name, other.Name, StringComparison.Ordinal) && - ClassDeclarationSyntax.SyntaxTree.IsEquivalentTo( - other.ClassDeclarationSyntax.SyntaxTree); - - public override int GetHashCode() - => HashCode.Combine(Name, ClassDeclarationSyntax); -} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/InterfaceTypeInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/InterfaceTypeInfo.cs new file mode 100644 index 00000000000..574f41f379e --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/InterfaceTypeInfo.cs @@ -0,0 +1,65 @@ +using System.Collections.Immutable; +using HotChocolate.Types.Analyzers.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace HotChocolate.Types.Analyzers.Models; + +public sealed class InterfaceTypeInfo + : SyntaxInfo + , IOutputTypeInfo +{ + public InterfaceTypeInfo(INamedTypeSymbol schemaType, + INamedTypeSymbol runtimeType, + ClassDeclarationSyntax classDeclarationSyntax, + ImmutableArray resolvers) + { + SchemaSchemaType = schemaType; + SchemaTypeFullName = schemaType.ToDisplayString(); + RuntimeType = runtimeType; + RuntimeTypeFullName = runtimeType.ToDisplayString(); + ClassDeclaration = classDeclarationSyntax; + Resolvers = resolvers; + } + + public string Name => SchemaSchemaType.Name; + + public string Namespace => SchemaSchemaType.ContainingNamespace.ToDisplayString(); + + public bool IsPublic => SchemaSchemaType.DeclaredAccessibility == Accessibility.Public; + + public INamedTypeSymbol SchemaSchemaType { get; } + + public string SchemaTypeFullName { get; } + + public bool HasSchemaType => true; + + public INamedTypeSymbol RuntimeType { get; } + + public string? RuntimeTypeFullName { get; } + + public bool HasRuntimeType => true; + + public ClassDeclarationSyntax ClassDeclaration { get; } + + public ImmutableArray Resolvers { get; private set; } + + public override string OrderByKey => SchemaTypeFullName; + + public void ReplaceResolver(Resolver current, Resolver replacement) + => Resolvers = Resolvers.Replace(current, replacement); + + public override bool Equals(object? obj) + => obj is ObjectTypeInfo other && Equals(other); + + public override bool Equals(SyntaxInfo? obj) + => obj is ObjectTypeInfo other && Equals(other); + + private bool Equals(ObjectTypeInfo other) + => string.Equals(SchemaTypeFullName, other.SchemaTypeFullName, StringComparison.Ordinal) && + ClassDeclaration.SyntaxTree.IsEquivalentTo( + other.ClassDeclaration.SyntaxTree); + + public override int GetHashCode() + => HashCode.Combine(SchemaTypeFullName, ClassDeclaration); +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/ModuleInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/ModuleInfo.cs index d4b210b691d..8d120bc4dab 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/ModuleInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/ModuleInfo.cs @@ -11,7 +11,7 @@ public sealed class ModuleInfo(string moduleName, ModuleOptions options) : Synta public override bool Equals(object? obj) => obj is ModuleInfo other && Equals(other); - public override bool Equals(SyntaxInfo obj) + public override bool Equals(SyntaxInfo? obj) => obj is ModuleInfo other && Equals(other); private bool Equals(ModuleInfo other) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/ObjectTypeExtensionInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/ObjectTypeExtensionInfo.cs deleted file mode 100644 index c1b56b363c8..00000000000 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/ObjectTypeExtensionInfo.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Immutable; -using HotChocolate.Types.Analyzers.Helpers; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace HotChocolate.Types.Analyzers.Models; - -public sealed class ObjectTypeExtensionInfo( - INamedTypeSymbol type, - INamedTypeSymbol runtimeType, - Resolver? nodeResolver, - ClassDeclarationSyntax classDeclarationSyntax, - ImmutableArray resolvers) - : SyntaxInfo - , IOutputTypeInfo -{ - public string Name { get; } = type.ToFullyQualified(); - - public bool IsRootType => false; - - public INamedTypeSymbol Type { get; } = type; - - public INamedTypeSymbol RuntimeType { get; } = runtimeType; - - public Resolver? NodeResolver { get; } = nodeResolver; - - public ClassDeclarationSyntax ClassDeclarationSyntax { get; } = classDeclarationSyntax; - - public ImmutableArray Resolvers { get; } = resolvers; - - public override string OrderByKey => Name; - - public override bool Equals(object? obj) - => obj is ObjectTypeExtensionInfo other && Equals(other); - - public override bool Equals(SyntaxInfo obj) - => obj is ObjectTypeExtensionInfo other && Equals(other); - - private bool Equals(ObjectTypeExtensionInfo other) - => string.Equals(Name, other.Name, StringComparison.Ordinal) && - ClassDeclarationSyntax.SyntaxTree.IsEquivalentTo( - other.ClassDeclarationSyntax.SyntaxTree); - - public override int GetHashCode() - => HashCode.Combine(Name, ClassDeclarationSyntax); -} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/ObjectTypeInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/ObjectTypeInfo.cs new file mode 100644 index 00000000000..2886cc66d36 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/ObjectTypeInfo.cs @@ -0,0 +1,69 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace HotChocolate.Types.Analyzers.Models; + +public sealed class ObjectTypeInfo + : SyntaxInfo + , IOutputTypeInfo +{ + public ObjectTypeInfo(INamedTypeSymbol schemaType, + INamedTypeSymbol runtimeType, + Resolver? nodeResolver, + ClassDeclarationSyntax classDeclarationSyntax, + ImmutableArray resolvers) + { + SchemaSchemaType = schemaType; + SchemaTypeFullName = schemaType.ToDisplayString(); + RuntimeType = runtimeType; + RuntimeTypeFullName = runtimeType.ToDisplayString(); + NodeResolver = nodeResolver; + ClassDeclaration = classDeclarationSyntax; + Resolvers = resolvers; + } + + public string Name => SchemaSchemaType.Name; + + public string Namespace => SchemaSchemaType.ContainingNamespace.ToDisplayString(); + + public bool IsPublic => SchemaSchemaType.DeclaredAccessibility == Accessibility.Public; + + public bool IsRootType => false; + + public INamedTypeSymbol SchemaSchemaType { get; } + + public string SchemaTypeFullName { get; } + + public bool HasSchemaType => true; + + public INamedTypeSymbol RuntimeType { get; } + + public string RuntimeTypeFullName { get; } + + public bool HasRuntimeType => true; + + public Resolver? NodeResolver { get; } + + public ClassDeclarationSyntax ClassDeclaration { get; } + + public ImmutableArray Resolvers { get; private set; } + + public override string OrderByKey => SchemaTypeFullName; + + public void ReplaceResolver(Resolver current, Resolver replacement) + => Resolvers = Resolvers.Replace(current, replacement); + + public override bool Equals(object? obj) + => obj is ObjectTypeInfo other && Equals(other); + + public override bool Equals(SyntaxInfo? obj) + => obj is ObjectTypeInfo other && Equals(other); + + private bool Equals(ObjectTypeInfo other) + => string.Equals(SchemaTypeFullName, other.SchemaTypeFullName, StringComparison.Ordinal) + && ClassDeclaration.SyntaxTree.IsEquivalentTo( + other.ClassDeclaration.SyntaxTree); + public override int GetHashCode() + => HashCode.Combine(SchemaTypeFullName, ClassDeclaration); +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/OperationInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/OperationInfo.cs index 0e5667f650c..86e9cb1dd2e 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/OperationInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/OperationInfo.cs @@ -13,7 +13,7 @@ public sealed class OperationInfo(OperationType type, string typeName, string me public override bool Equals(object? obj) => obj is OperationInfo other && Equals(other); - public override bool Equals(SyntaxInfo obj) + public override bool Equals(SyntaxInfo? obj) => obj is OperationInfo info && Equals(info); private bool Equals(OperationInfo other) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/OperationRegistrationInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/OperationRegistrationInfo.cs index 183f3410829..d9eb152703d 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/OperationRegistrationInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/OperationRegistrationInfo.cs @@ -11,7 +11,7 @@ public sealed class OperationRegistrationInfo(OperationType type, string typeNam public override bool Equals(object? obj) => obj is OperationRegistrationInfo other && Equals(other); - public override bool Equals(SyntaxInfo other) + public override bool Equals(SyntaxInfo? other) => other is OperationRegistrationInfo info && Equals(info); private bool Equals(OperationRegistrationInfo other) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/RegisterDataLoaderInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/RegisterDataLoaderInfo.cs index 5ea029d6610..1d3321b064a 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/RegisterDataLoaderInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/RegisterDataLoaderInfo.cs @@ -9,7 +9,7 @@ public sealed class RegisterDataLoaderInfo(string name) : SyntaxInfo public override bool Equals(object? obj) => obj is RegisterDataLoaderInfo other && Equals(other); - public override bool Equals(SyntaxInfo other) + public override bool Equals(SyntaxInfo? other) => other is RegisterDataLoaderInfo info && Equals(info); private bool Equals(RegisterDataLoaderInfo other) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/RequestMiddlewareInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/RequestMiddlewareInfo.cs index a0d6dfbc21c..1aef0405890 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/RequestMiddlewareInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/RequestMiddlewareInfo.cs @@ -26,7 +26,7 @@ public sealed class RequestMiddlewareInfo( public override bool Equals(object? obj) => obj is RequestMiddlewareInfo other && Equals(other); - public override bool Equals(SyntaxInfo obj) + public override bool Equals(SyntaxInfo? obj) => obj is RequestMiddlewareInfo other && Equals(other); private bool Equals(RequestMiddlewareInfo other) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/Resolver.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/Resolver.cs index c0b1fe0071a..999bab6c140 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/Resolver.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/Resolver.cs @@ -1,30 +1,73 @@ using System.Collections.Immutable; +using HotChocolate.Types.Analyzers.Helpers; using Microsoft.CodeAnalysis; namespace HotChocolate.Types.Analyzers.Models; -public sealed class Resolver( - string typeName, - ISymbol member, - ResolverResultKind resultKind, - ImmutableArray parameters, - ImmutableArray bindings, - bool isNodeResolver = false) +public sealed class Resolver { - public string TypeName => typeName; + public Resolver( + string typeName, + ISymbol member, + ResolverResultKind resultKind, + ImmutableArray parameters, + ImmutableArray bindings, + ResolverKind kind = ResolverKind.Default, + FieldFlags flags = FieldFlags.None, + string? schemaTypeName = null) + { + TypeName = typeName; + Member = member; + SchemaTypeName = schemaTypeName; + ResultKind = resultKind; + Parameters = parameters; + Bindings = bindings; + Kind = kind; + Flags = flags; + } - public ISymbol Member => member; + public string TypeName { get; } + + public ISymbol Member { get; } + + public ITypeSymbol ReturnType + => Member.GetReturnType() + ?? throw new InvalidOperationException("Resolver has no return type."); + + public ITypeSymbol UnwrappedReturnType + => ReturnType.UnwrapTaskOrValueTask(); + + public string? SchemaTypeName { get; } + + public bool IsStatic => Member.IsStatic; public bool IsPure - => !isNodeResolver - && resultKind is ResolverResultKind.Pure - && parameters.All(t => t.IsPure); + => Kind is not ResolverKind.NodeResolver + && ResultKind is ResolverResultKind.Pure + && Parameters.All(t => t.IsPure); + + public ResolverKind Kind { get; } + + public FieldFlags Flags { get; } + + public ResolverResultKind ResultKind { get; } - public bool IsNodeResolver => isNodeResolver; + public ImmutableArray Parameters { get; } - public ResolverResultKind ResultKind => resultKind; + public bool RequiresParameterBindings => Parameters.Any(t => t.RequiresBinding); - public ImmutableArray Parameters => parameters; + public ImmutableArray Bindings { get; } - public ImmutableArray Bindings => bindings; + public Resolver WithSchemaTypeName(string? schemaTypeName) + { + return new Resolver( + TypeName, + Member, + ResultKind, + Parameters, + Bindings, + Kind, + Flags, + schemaTypeName); + } } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverKind.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverKind.cs new file mode 100644 index 00000000000..810026babee --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverKind.cs @@ -0,0 +1,9 @@ +namespace HotChocolate.Types.Analyzers.Models; + +public enum ResolverKind +{ + Default = 0, + NodeResolver = 1, + ConnectionResolver = 2, + InstanceResolver = 3 +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs index 26b27e36fe2..8a1177d9241 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs @@ -29,20 +29,22 @@ public ImmutableArray TypeParameters public ResolverParameterKind Kind { get; } public bool IsPure - => Kind == ResolverParameterKind.Argument || - Kind == ResolverParameterKind.Parent || - Kind == ResolverParameterKind.Service || - Kind == ResolverParameterKind.GetGlobalState || - Kind == ResolverParameterKind.SetGlobalState || - Kind == ResolverParameterKind.GetScopedState || - Kind == ResolverParameterKind.HttpContext || - Kind == ResolverParameterKind.HttpRequest || - Kind == ResolverParameterKind.HttpResponse || - Kind == ResolverParameterKind.DocumentNode || - Kind == ResolverParameterKind.EventMessage || - Kind == ResolverParameterKind.FieldNode || - Kind == ResolverParameterKind.OutputField || - Kind == ResolverParameterKind.ClaimsPrincipal; + => Kind is ResolverParameterKind.Argument + or ResolverParameterKind.Parent + or ResolverParameterKind.Service + or ResolverParameterKind.GetGlobalState + or ResolverParameterKind.SetGlobalState + or ResolverParameterKind.GetScopedState + or ResolverParameterKind.HttpContext + or ResolverParameterKind.HttpRequest + or ResolverParameterKind.HttpResponse + or ResolverParameterKind.DocumentNode + or ResolverParameterKind.EventMessage + or ResolverParameterKind.FieldNode + or ResolverParameterKind.OutputField + or ResolverParameterKind.ClaimsPrincipal; + public bool RequiresBinding + => Kind == ResolverParameterKind.Unknown; public bool IsNullable { get; } @@ -145,6 +147,11 @@ private static ResolverParameterKind GetParameterKind( return ResolverParameterKind.QueryContext; } + if(parameter.IsPagingArguments()) + { + return ResolverParameterKind.PagingArguments; + } + return ResolverParameterKind.Unknown; } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameterKind.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameterKind.cs index 11dcdf9ffd8..5d2296f24e6 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameterKind.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameterKind.cs @@ -21,5 +21,6 @@ public enum ResolverParameterKind SetLocalState, Service, Argument, - QueryContext + QueryContext, + PagingArguments } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/RootTypeExtensionInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/RootTypeExtensionInfo.cs deleted file mode 100644 index 3199067f8d1..00000000000 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/RootTypeExtensionInfo.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Immutable; -using HotChocolate.Types.Analyzers.Helpers; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace HotChocolate.Types.Analyzers.Models; - -public sealed class RootTypeExtensionInfo - : SyntaxInfo - , IOutputTypeInfo -{ - private readonly OperationType _operationType; - - public RootTypeExtensionInfo(INamedTypeSymbol type, - OperationType operationType, - ClassDeclarationSyntax classDeclarationSyntax, - ImmutableArray resolvers) - { - _operationType = operationType; - Name = type.ToFullyQualified(); - Type = type; - ClassDeclarationSyntax = classDeclarationSyntax; - Resolvers = resolvers; - } - - public string Name { get; } - - public bool IsRootType => true; - - public OperationType OperationType => _operationType; - - public INamedTypeSymbol Type { get; } - - public INamedTypeSymbol? RuntimeType => null; - - public ClassDeclarationSyntax ClassDeclarationSyntax { get; } - - public ImmutableArray Resolvers { get; } - - public override string OrderByKey => Name; - - public override bool Equals(object? obj) - => obj is ObjectTypeExtensionInfo other && Equals(other); - - public override bool Equals(SyntaxInfo obj) - => obj is ObjectTypeExtensionInfo other && Equals(other); - - private bool Equals(ObjectTypeExtensionInfo other) - => string.Equals(Name, other.Name, StringComparison.Ordinal) && - ClassDeclarationSyntax.SyntaxTree.IsEquivalentTo( - other.ClassDeclarationSyntax.SyntaxTree); - - public override int GetHashCode() - => HashCode.Combine(Name, ClassDeclarationSyntax); -} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/RootTypeInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/RootTypeInfo.cs new file mode 100644 index 00000000000..a44e8e28f7e --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/RootTypeInfo.cs @@ -0,0 +1,62 @@ +using System.Collections.Immutable; +using HotChocolate.Types.Analyzers.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace HotChocolate.Types.Analyzers.Models; + +public sealed class RootTypeInfo + : SyntaxInfo + , IOutputTypeInfo +{ + public RootTypeInfo( + INamedTypeSymbol schemaType, + OperationType operationType, + ClassDeclarationSyntax classDeclarationSyntax, + ImmutableArray resolvers) + { + OperationType = operationType; + SchemaSchemaType = schemaType; + SchemaTypeFullName = schemaType.ToDisplayString(); + ClassDeclaration = classDeclarationSyntax; + Resolvers = resolvers; + } + + public string Name => SchemaSchemaType.Name; + + public string Namespace => SchemaSchemaType.ContainingNamespace.ToDisplayString(); + + public bool IsPublic => SchemaSchemaType.DeclaredAccessibility == Accessibility.Public; + + public OperationType OperationType { get; } + + public INamedTypeSymbol SchemaSchemaType { get; } + + public string SchemaTypeFullName { get; } + + public bool HasSchemaType => true; + + public INamedTypeSymbol? RuntimeType => null; + + public string? RuntimeTypeFullName => null; + + public bool HasRuntimeType => false; + + public ClassDeclarationSyntax ClassDeclaration { get; } + + public ImmutableArray Resolvers { get; private set; } + + public override string OrderByKey => SchemaTypeFullName; + + public void ReplaceResolver(Resolver current, Resolver replacement) + => Resolvers = Resolvers.Replace(current, replacement); + + public override bool Equals(object? obj) + => obj is ObjectTypeInfo other && Equals(other); + + public override bool Equals(SyntaxInfo? obj) + => obj is ObjectTypeInfo other && Equals(other); + + public override int GetHashCode() + => HashCode.Combine(SchemaTypeFullName, ClassDeclaration); +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/SyntaxInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/SyntaxInfo.cs index 43fecf1f506..8755f26977f 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/SyntaxInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/SyntaxInfo.cs @@ -17,5 +17,5 @@ public void AddDiagnosticRange(ImmutableArray diagnostics) ? diagnostics : Diagnostics.AddRange(diagnostics); - public abstract bool Equals(SyntaxInfo other); + public abstract bool Equals(SyntaxInfo? other); } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs index 62992467a35..aacf708ad11 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs @@ -26,6 +26,9 @@ public static class WellKnownAttributes public const string ArgumentAttribute = "HotChocolate.ArgumentAttribute"; public const string BindMemberAttribute = "HotChocolate.Types.BindMemberAttribute"; public const string BindFieldAttribute = "HotChocolate.Types.BindFieldAttribute"; + public const string GraphQLIgnoreAttribute = "HotChocolate.GraphQLIgnoreAttribute"; + public const string UseConnectionAttribute = "HotChocolate.Types.UseConnectionAttribute"; + public const string GraphQLNameAttribute = "HotChocolate.GraphQLNameAttribute"; public static HashSet BindAttributes { get; } = [ diff --git a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs index a507182c4d4..a389e53ca9d 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs @@ -82,6 +82,17 @@ public static class WellKnownTypes "HotChocolate.Data.Filters.FilterContextResolverContextExtensions"; public const string SortingContextResolverContextExtensions = "HotChocolate.Data.Sorting.SortingContextResolverContextExtensions"; + public const string PagingHelper = "HotChocolate.Types.Pagination.PagingHelper"; + public const string PagingDefaults = "HotChocolate.Types.Pagination.PagingDefaults"; + public const string StaticMemberFlags = "HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags"; + public const string InstanceMemberFlags = "HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags"; + public const string IObjectTypeDescriptor = "HotChocolate.Types.IObjectTypeDescriptor"; + public const string IInterfaceTypeDescriptor = "HotChocolate.Types.IInterfaceTypeDescriptor"; + public const string TypeReference = "HotChocolate.Types.Descriptors.TypeReference"; + public const string IDescriptorContext = "HotChocolate.Types.Descriptors.IDescriptorContext"; + public const string ObjectTypeDefinition = "HotChocolate.Types.Descriptors.Definitions.ObjectTypeDefinition"; + public const string NonNullType = "HotChocolate.Types.NonNullType"; + public const string ListType = "HotChocolate.Types.ListType"; public static HashSet TypeClass { get; } = [ @@ -129,9 +140,8 @@ public static class WellKnownTypes }; public static HashSet TaskWrapper { get; } = - new() - { + [ "System.Threading.Tasks.Task<>", "System.Threading.Tasks.ValueTask<>" - }; + ]; } diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/Connection.cs b/src/HotChocolate/Core/src/Types.CursorPagination/Connection.cs index 6f77f47a60a..8134fff8817 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/Connection.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/Connection.cs @@ -5,7 +5,7 @@ namespace HotChocolate.Types.Pagination; /// /// The connection represents one section of a dataset / collection. /// -public class Connection : IPage +public class Connection : IConnection, IPageTotalCountProvider { /// /// Initializes . diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionBase.cs b/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionBase.cs new file mode 100644 index 00000000000..ee3266166fb --- /dev/null +++ b/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionBase.cs @@ -0,0 +1,47 @@ +using System.Buffers; + +namespace HotChocolate.Types.Pagination; + +/// +/// The connection represents one section of a dataset / collection. +/// +public abstract class ConnectionBase + : IConnection + where TEdge : IEdge + where TPageInfo : IPageInfo +{ + public abstract IReadOnlyList? Edges { get; } + + public abstract TPageInfo PageInfo { get; } + + IReadOnlyList>? IConnection.Edges => (IReadOnlyList>?)Edges; + + IReadOnlyList? IConnection.Edges => (IReadOnlyList>?)Edges; + + IReadOnlyList IPage.Items => (IReadOnlyList>?)Edges!; + + IPageInfo IPage.Info => PageInfo; + + void IPage.Accept(IPageObserver observer) + { + if (Edges is null || Edges.Count == 0) + { + ReadOnlySpan empty = []; + observer.OnAfterSliced(empty, PageInfo); + return; + } + + var buffer = ArrayPool.Shared.Rent(Edges.Count); + + for (var i = 0; i < Edges.Count; i++) + { + buffer[i] = Edges[i].Node; + } + + ReadOnlySpan items = buffer.AsSpan(0, Edges.Count); + observer.OnAfterSliced(items, PageInfo); + + buffer.AsSpan()[..Edges.Count].Clear(); + ArrayPool.Shared.Return(buffer); + } +} diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionPageInfo.cs b/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionPageInfo.cs index 694ba5a8264..9a16a87c74a 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionPageInfo.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionPageInfo.cs @@ -1,7 +1,7 @@ namespace HotChocolate.Types.Pagination; /// -/// Represents the connection paging page info. +/// Represents the connection page info. /// This class provides additional information about pagination in a connection. /// public class ConnectionPageInfo : IPageInfo diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs b/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs index 42bdaa9c16a..acbf60a1005 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs @@ -212,16 +212,16 @@ private static bool IsNodesField(ObjectFieldDefinition field) => (field.Flags & FieldFlags.ConnectionNodesField) == FieldFlags.ConnectionNodesField; private static IPageInfo GetPagingInfo(IResolverContext context) - => context.Parent().Info; + => context.Parent().Info; - private static IReadOnlyCollection GetEdges(IResolverContext context) - => context.Parent().Edges; + private static IEnumerable? GetEdges(IResolverContext context) + => context.Parent().Edges; - private static IEnumerable GetNodes(IResolverContext context) - => context.Parent().Edges.Select(t => t.Node); + private static IEnumerable? GetNodes(IResolverContext context) + => context.Parent().Edges?.Select(t => t.Node); private static object? GetTotalCount(IResolverContext context) - => context.Parent().TotalCount; + => context.Parent().TotalCount; internal static class Names { diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/Connection~1.cs b/src/HotChocolate/Core/src/Types.CursorPagination/Connection~1.cs index 09fac0d9c8f..187f19a1fdc 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/Connection~1.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/Connection~1.cs @@ -20,7 +20,7 @@ public class Connection : Connection /// The total count of items of this connection /// public Connection( - IReadOnlyList> edges, + IReadOnlyList> edges, ConnectionPageInfo info, int totalCount = 0) : base(edges, info, totalCount) @@ -31,22 +31,22 @@ public Connection( public Connection( ConnectionPageInfo info, int totalCount = 0) - : base(Array.Empty>(), info, totalCount) + : base([], info, totalCount) { - Edges = Array.Empty>(); + Edges = []; } /// /// The edges that belong to this connection. /// - public new IReadOnlyList> Edges { get; } + public new IReadOnlyList> Edges { get; } /// public override void Accept(IPageObserver observer) { - if(Edges.Count == 0) + if (Edges.Count == 0) { - ReadOnlySpan empty = Array.Empty(); + ReadOnlySpan empty = []; observer.OnAfterSliced(empty, Info); return; } @@ -61,7 +61,7 @@ public override void Accept(IPageObserver observer) ReadOnlySpan items = buffer.AsSpan(0, Edges.Count); observer.OnAfterSliced(items, Info); - buffer.AsSpan().Slice(0, Edges.Count).Clear(); + buffer.AsSpan()[..Edges.Count].Clear(); ArrayPool.Shared.Return(buffer); } } diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/Edge.cs b/src/HotChocolate/Core/src/Types.CursorPagination/Edge.cs index 84bffea1b51..7c25a336d44 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/Edge.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/Edge.cs @@ -7,7 +7,7 @@ namespace HotChocolate.Types.Pagination; /// Represents an edge in a connection. /// /// -public class Edge : IEdge +public class Edge : IEdge { private readonly Func, string>? _resolveCursor; private string? _cursor; diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UseConnectionAttribute.cs b/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UseConnectionAttribute.cs new file mode 100644 index 00000000000..aaaa49fa5e3 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UseConnectionAttribute.cs @@ -0,0 +1,171 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using HotChocolate.Types.Descriptors; +using HotChocolate.Types.Descriptors.Definitions; +using HotChocolate.Types.Pagination; +using HotChocolate.Utilities; + +// ReSharper disable once CheckNamespace +namespace HotChocolate.Types; + +/// +/// This attribute allows to override the global paging options for a specific field. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false)] +public sealed class UseConnectionAttribute : DescriptorAttribute +{ + private int? _defaultPageSize; + private int? _maxPageSize; + private bool? _includeTotalCount; + private bool? _allowBackwardPagination; + private bool? _requirePagingBoundaries; + private bool? _inferConnectionNameFromField; + private bool? _EnableRelativeCursors; + + /// + /// Overrides the global paging options for the annotated field. + /// + /// + /// The explicit order priority for this attribute. + /// + public UseConnectionAttribute([CallerLineNumber] int order = 0) + { + Order = order; + } + + /// + /// Specifies the default page size for this field. + /// + public int DefaultPageSize + { + get => _defaultPageSize ?? PagingDefaults.DefaultPageSize; + set => _defaultPageSize = value; + } + + /// + /// Specifies the maximum allowed page size. + /// + public int MaxPageSize + { + get => _maxPageSize ?? PagingDefaults.MaxPageSize; + set => _maxPageSize = value; + } + + /// + /// Include the total count field to the result type. + /// + public bool IncludeTotalCount + { + get => _includeTotalCount ?? PagingDefaults.IncludeTotalCount; + set => _includeTotalCount = value; + } + + /// + /// Allow backward paging using last and before + /// + public bool AllowBackwardPagination + { + get => _allowBackwardPagination ?? PagingDefaults.AllowBackwardPagination; + set => _allowBackwardPagination = value; + } + + /// + /// Defines if the paging middleware shall require the + /// API consumer to specify paging boundaries. + /// + public bool RequirePagingBoundaries + { + get => _requirePagingBoundaries ?? PagingDefaults.RequirePagingBoundaries; + set => _requirePagingBoundaries = value; + } + + /// + /// Connection names are by default inferred from the field name to + /// which they are bound to as opposed to the node type name. + /// + public bool InferConnectionNameFromField + { + get => _inferConnectionNameFromField ?? PagingDefaults.InferConnectionNameFromField; + set => _inferConnectionNameFromField = value; + } + + /// + /// Defines whether relative cursors are allowed. + /// + public bool EnableRelativeCursors + { + get => _EnableRelativeCursors ?? PagingDefaults.EnableRelativeCursors; + set => _EnableRelativeCursors = value; + } + + public string? ConnectionName { get; set; } + + protected internal override void TryConfigure( + IDescriptorContext context, + IDescriptor descriptor, + ICustomAttributeProvider element) + { + if (element is not MemberInfo) + { + return; + } + + var options = new PagingOptions + { + DefaultPageSize = _defaultPageSize, + MaxPageSize = _maxPageSize, + IncludeTotalCount = _includeTotalCount, + AllowBackwardPagination = _allowBackwardPagination, + RequirePagingBoundaries = _requirePagingBoundaries, + InferConnectionNameFromField = _inferConnectionNameFromField, + ProviderName = ConnectionName, + EnableRelativeCursors = _EnableRelativeCursors, + }; + + if (descriptor is IObjectFieldDescriptor fieldDesc) + { + var definition = fieldDesc.Extend().Definition; + definition.Configurations.Add( + new CreateConfiguration( + (_, d) => + { + ((ObjectFieldDefinition)d).State = + ((ObjectFieldDefinition)d).State.SetItem( + WellKnownContextData.PagingOptions, + options); + }, + definition)); + definition.Configurations.Add( + new CompleteConfiguration( + (c, d) => ApplyPagingOptions(c.DescriptorContext, d, options), + definition, + ApplyConfigurationOn.BeforeCompletion)); + } + + static void ApplyPagingOptions( + IDescriptorContext context, + ObjectFieldDefinition definition, + PagingOptions options) + { + options = context.GetPagingOptions(options); + definition.ContextData[WellKnownContextData.PagingOptions] = options; + + if (options.AllowBackwardPagination ?? PagingDefaults.AllowBackwardPagination) + { + return; + } + + var beforeArg = definition.Arguments.FirstOrDefault(t => t.Name.EqualsOrdinal("before")); + if(beforeArg is not null) + { + definition.Arguments.Remove(beforeArg); + } + + var lastArg = definition.Arguments.FirstOrDefault(t => t.Name.EqualsOrdinal("last")); + if(lastArg is not null) + { + definition.Arguments.Remove(lastArg); + } + } + } +} diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UsePagingAttribute.cs b/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UsePagingAttribute.cs index 0a49a27076c..356ac0d4cec 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UsePagingAttribute.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UsePagingAttribute.cs @@ -124,17 +124,17 @@ protected internal override void TryConfigure( string.IsNullOrEmpty(_connectionName) ? default! : _connectionName; - var options = - new PagingOptions - { - DefaultPageSize = _defaultPageSize, - MaxPageSize = _maxPageSize, - IncludeTotalCount = _includeTotalCount, - AllowBackwardPagination = _allowBackwardPagination, - RequirePagingBoundaries = _requirePagingBoundaries, - InferConnectionNameFromField = _inferConnectionNameFromField, - ProviderName = ProviderName - }; + + var options =new PagingOptions + { + DefaultPageSize = _defaultPageSize, + MaxPageSize = _maxPageSize, + IncludeTotalCount = _includeTotalCount, + AllowBackwardPagination = _allowBackwardPagination, + RequirePagingBoundaries = _requirePagingBoundaries, + InferConnectionNameFromField = _inferConnectionNameFromField, + ProviderName = ProviderName + }; if (descriptor is IObjectFieldDescriptor ofd) { diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/IConnection.cs b/src/HotChocolate/Core/src/Types.CursorPagination/IConnection.cs new file mode 100644 index 00000000000..bdf95f9715d --- /dev/null +++ b/src/HotChocolate/Core/src/Types.CursorPagination/IConnection.cs @@ -0,0 +1,23 @@ +namespace HotChocolate.Types.Pagination; + +/// +/// The connection represents one section of a dataset / collection. +/// +public interface IConnection : IPage +{ + /// + /// The edges that belong to this connection. + /// + IReadOnlyList? Edges { get; } +} + +/// +/// The connection represents one section of a dataset / collection. +/// +public interface IConnection : IConnection +{ + /// + /// The edges that belong to this connection. + /// + new IReadOnlyList>? Edges { get; } +} diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/IEdge~1.cs b/src/HotChocolate/Core/src/Types.CursorPagination/IEdge~1.cs new file mode 100644 index 00000000000..e433afd9d22 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.CursorPagination/IEdge~1.cs @@ -0,0 +1,12 @@ +namespace HotChocolate.Types.Pagination; + +/// +/// Represents an edge in a connection. +/// +public interface IEdge : IEdge +{ + /// + /// Gets the node. + /// + new T Node { get; } +} diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeRegistrar.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeRegistrar.cs index 8db1fdfbe7f..605e0b8946c 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeRegistrar.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeRegistrar.cs @@ -216,9 +216,7 @@ private RegisteredType InitializeType( registeredType.References.TryAdd(runtimeTypeRef); } - if (_interceptor.TryCreateScope( - registeredType, - out var dependencies)) + if (_interceptor.TryCreateScope(registeredType, out var dependencies)) { registeredType.Dependencies.Clear(); registeredType.Dependencies.AddRange(dependencies); diff --git a/src/HotChocolate/Core/src/Types/Internal/ExtendedType.Helper.cs b/src/HotChocolate/Core/src/Types/Internal/ExtendedType.Helper.cs index 8d2d37bb833..c61f8b6c110 100644 --- a/src/HotChocolate/Core/src/Types/Internal/ExtendedType.Helper.cs +++ b/src/HotChocolate/Core/src/Types/Internal/ExtendedType.Helper.cs @@ -23,9 +23,9 @@ internal static bool IsSchemaType(Type type) if (type.IsGenericType) { var definition = type.GetGenericTypeDefinition(); - if (typeof(ListType<>) == definition || - typeof(NonNullType<>) == definition || - typeof(NativeType<>) == definition) + if (typeof(ListType<>) == definition + || typeof(NonNullType<>) == definition + || typeof(NativeType<>) == definition) { return IsSchemaType(type.GetGenericArguments()[0]); } @@ -36,25 +36,27 @@ internal static bool IsSchemaType(Type type) internal static Type RemoveNonEssentialTypes(Type type) { - if (type.IsGenericType && - (type.GetGenericTypeDefinition() == typeof(NativeType<>) || - type.GetGenericTypeDefinition() == typeof(ValueTask<>) || - type.GetGenericTypeDefinition() == typeof(Task<>))) + if (type.IsGenericType + && (type.GetGenericTypeDefinition() == typeof(NativeType<>) + || type.GetGenericTypeDefinition() == typeof(ValueTask<>) + || type.GetGenericTypeDefinition() == typeof(Task<>))) { return RemoveNonEssentialTypes(type.GetGenericArguments()[0]); } + return type; } internal static IExtendedType RemoveNonEssentialTypes(IExtendedType type) { - if (type.IsGeneric && - (type.Definition == typeof(NativeType<>) || - type.Definition == typeof(ValueTask<>) || - type.Definition == typeof(Task<>))) + if (type.IsGeneric + && (type.Definition == typeof(NativeType<>) + || type.Definition == typeof(ValueTask<>) + || type.Definition == typeof(Task<>))) { return RemoveNonEssentialTypes(type.TypeArguments[0]); } + return type; } @@ -66,56 +68,6 @@ internal static bool IsListType(Type type) => private static bool ImplementsListInterface(Type type) => GetInnerListType(type) is not null; - internal static ExtendedType? GetInnerListType( - ExtendedType type, - TypeCache cache) - { - if (IsDictionary(type.Type)) - { - IExtendedType key = type.TypeArguments[0]; - IExtendedType value = type.TypeArguments[1]; - - var itemType = typeof(KeyValuePair<,>).MakeGenericType(key.Type, value.Type); - - return cache.GetOrCreateType(itemType, () => - { - return new ExtendedType( - itemType, - ExtendedTypeKind.Runtime, - new[] { (ExtendedType)key, (ExtendedType)value, }, - isNullable: false); - }); - } - - if (IsSupportedCollectionInterface(type.Type)) - { - return type.TypeArguments[0]; - } - - if (type.IsInterface && type.Definition == typeof(IEnumerable<>)) - { - return type.TypeArguments[0]; - } - - foreach (var interfaceType in type.Type.GetInterfaces()) - { - if (IsSupportedCollectionInterface(interfaceType)) - { - var elementType = interfaceType.GetGenericArguments()[0]; - - if (type.TypeArguments.Count == 1 && - type.TypeArguments[0].Type == elementType) - { - return type.TypeArguments[0]; - } - - return SystemType.FromType(elementType, cache); - } - } - - return null; - } - internal static Type? GetInnerListType(Type type) { var typeDefinition = type.IsGenericType ? type.GetGenericTypeDefinition() : null; @@ -137,11 +89,20 @@ private static bool ImplementsListInterface(Type type) => return interfaceType.GetGenericArguments()[0]; } - if (interfaceType == typeof(IPage) && - type.IsGenericType && - type.GenericTypeArguments.Length == 1) + if (interfaceType == typeof(IPage)) { - return type.GenericTypeArguments[0]; + if (type is { IsGenericType: true, GenericTypeArguments.Length: 1 }) + { + return type.GenericTypeArguments[0]; + } + } + + if (interfaceType.IsInterface + && interfaceType.IsGenericType + && interfaceType.IsAssignableTo(typeof(IPage)) + && interfaceType.GetGenericTypeDefinition().FullName == "HotChocolate.Types.Pagination.IConnection`1") + { + return interfaceType.GetGenericArguments()[0]; } } @@ -178,24 +139,7 @@ public static bool IsSupportedCollectionInterface(Type type) return true; } } - return false; - } - private static bool IsDictionary(Type type) - { - if (type.IsGenericType) - { - var typeDefinition = type.GetGenericTypeDefinition(); - if (typeDefinition == typeof(IDictionary<,>) - || typeDefinition == typeof(IReadOnlyDictionary<,>) - || typeDefinition == typeof(Dictionary<,>) - || typeDefinition == typeof(ConcurrentDictionary<,>) - || typeDefinition == typeof(SortedDictionary<,>) - || typeDefinition == typeof(ImmutableDictionary<,>)) - { - return true; - } - } return false; } @@ -234,9 +178,7 @@ private static ExtendedType ChangeNullability( var pos = position++; var changeNullability = - nullable.Length > pos && - nullable[pos].HasValue && - nullable[pos]!.Value != type.IsNullable; + nullable.Length > pos && nullable[pos].HasValue && nullable[pos]!.Value != type.IsNullable; var typeArguments = type.TypeArguments; if (type.TypeArguments.Count > 0 && nullable.Length > position) @@ -268,8 +210,7 @@ private static ExtendedType ChangeNullability( ? type.ElementType : null; - if (elementType is not null && - !ReferenceEquals(typeArguments, type.TypeArguments)) + if (elementType is not null && !ReferenceEquals(typeArguments, type.TypeArguments)) { for (var e = 0; e < type.TypeArguments.Count; e++) { @@ -379,6 +320,7 @@ private static uint CompactNullability(ReadOnlySpan bits) nullability |= 1u << (bits.Length - i); } } + return nullability; } } diff --git a/src/HotChocolate/Core/src/Types/Internal/ExtendedType.SystemType.cs b/src/HotChocolate/Core/src/Types/Internal/ExtendedType.SystemType.cs index 89d0d043008..d4ed23c2a24 100644 --- a/src/HotChocolate/Core/src/Types/Internal/ExtendedType.SystemType.cs +++ b/src/HotChocolate/Core/src/Types/Internal/ExtendedType.SystemType.cs @@ -32,7 +32,7 @@ private static ExtendedType FromTypeInternal(Type type, TypeCache cache) var typeArguments = type.IsArray && elementType is not null - ? new[] { elementType, } + ? [elementType] : GetGenericArguments(type, cache); return new ExtendedType( diff --git a/src/HotChocolate/Core/src/Types/Internal/TypeInfoExtensions.cs b/src/HotChocolate/Core/src/Types/Internal/TypeInfoExtensions.cs index 334e1348ffd..efdc6e27ca1 100644 --- a/src/HotChocolate/Core/src/Types/Internal/TypeInfoExtensions.cs +++ b/src/HotChocolate/Core/src/Types/Internal/TypeInfoExtensions.cs @@ -1,8 +1,13 @@ +using HotChocolate.Language; using HotChocolate.Types; +using HotChocolate.Types.Descriptors; namespace HotChocolate.Internal; -internal static class TypeInfoExtensions +/// +/// Provides extension methods for . +/// +public static class TypeInfoExtensions { /// /// Creates a type structure with the . @@ -22,4 +27,38 @@ internal static IType CreateType(this ITypeInfo typeInfo, INamedType namedType) throw new NotSupportedException( "The specified type info does not support creating new instances."); } + + /// + /// Helper to rewrite a type structure. + /// + public static TypeReference CreateTypeReference(this ITypeInfo typeInfo, NamedTypeNode namedType) + { + ArgumentNullException.ThrowIfNull(typeInfo); + ArgumentNullException.ThrowIfNull(namedType); + + ITypeNode type = namedType; + + for (var i = typeInfo.Components.Count - 1; i >= 0; i--) + { + var component = typeInfo.Components[i]; + switch (component.Kind) + { + case TypeComponentKind.Named: + continue; + + case TypeComponentKind.NonNull: + type = new NonNullTypeNode((INullableTypeNode)type); + break; + + case TypeComponentKind.List: + type = new ListTypeNode(type); + break; + + default: + throw new InvalidOperationException(); + } + } + + return TypeReference.Create(type); + } } diff --git a/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs b/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs index 705239fe4d3..a90aa467df2 100644 --- a/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs +++ b/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs @@ -60,6 +60,7 @@ public static Schema Create( typeInterceptors.Add(builder._schemaFirstTypeInterceptor); } + PagingDefaults.Apply(builder._pagingOptions); context.ContextData[typeof(PagingOptions).FullName!] = builder._pagingOptions; InitializeInterceptors( diff --git a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs index 12891446ef8..d49531432f4 100644 --- a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs +++ b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs @@ -22,13 +22,13 @@ public partial class SchemaBuilder : ISchemaBuilder { private delegate TypeReference CreateRef(ITypeInspector typeInspector); - private readonly Dictionary _contextData = new(); + private readonly Dictionary _contextData = []; private readonly List _globalComponents = []; private readonly List _documents = []; private readonly List _types = []; - private readonly Dictionary _operations = new(); - private readonly Dictionary<(Type, string?), List> _conventions = new(); - private readonly Dictionary _clrTypes = new(); + private readonly Dictionary _operations = []; + private readonly Dictionary<(Type, string?), List> _conventions = []; + private readonly Dictionary _clrTypes = []; private SchemaFirstTypeInterceptor? _schemaFirstTypeInterceptor; private readonly List _typeInterceptors = @@ -37,6 +37,7 @@ public partial class SchemaBuilder : ISchemaBuilder typeof(InterfaceCompletionTypeInterceptor), typeof(MiddlewareValidationTypeInterceptor), typeof(SemanticNonNullTypeInterceptor), + typeof(StoreGlobalPagingOptionsTypeInterceptor) ]; private SchemaOptions _options = new(); diff --git a/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs index 1be7f69f7bf..fd30c666cf4 100644 --- a/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs @@ -8,6 +8,7 @@ using HotChocolate.Types.Descriptors; using HotChocolate.Types.Descriptors.Definitions; using HotChocolate.Types.Helpers; +using HotChocolate.Types.Pagination; using HotChocolate.Types.Relay; namespace HotChocolate; diff --git a/src/HotChocolate/Core/src/Types/StoreGlobalPagingOptionsTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/StoreGlobalPagingOptionsTypeInterceptor.cs new file mode 100644 index 00000000000..42e1eb9bb43 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/StoreGlobalPagingOptionsTypeInterceptor.cs @@ -0,0 +1,21 @@ +#nullable enable + +using HotChocolate.Configuration; +using HotChocolate.Types.Descriptors.Definitions; +using HotChocolate.Types.Pagination; + +namespace HotChocolate; + +internal sealed class StoreGlobalPagingOptionsTypeInterceptor : TypeInterceptor +{ + public override void OnBeforeCompleteType( + ITypeCompletionContext completionContext, + DefinitionBase definition) + { + if(definition is SchemaTypeDefinition schemaDef) + { + var options = completionContext.DescriptorContext.GetPagingOptions(null); + schemaDef.Features.Set(options); + } + } +} diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IDescriptor~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IDescriptor~1.cs index a551f973640..552f3f1067a 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IDescriptor~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IDescriptor~1.cs @@ -17,12 +17,18 @@ public interface IDescriptor : IDescriptor where T : DefinitionBase /// /// Provides access to the underlying configuration. This is useful for extensions. /// - /// IDescriptorExtension Extend(); /// /// Provides access to the underlying configuration. This is useful for extensions. /// - /// - IDescriptorExtension ExtendWith(Action> configure); + IDescriptorExtension ExtendWith( + Action> configure); + + /// + /// Provides access to the underlying configuration. This is useful for extensions. + /// + IDescriptorExtension ExtendWith( + Action, TState> configure, + TState state); } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/INamedDependencyDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/INamedDependencyDescriptor.cs index 8dd2f4f9447..2cf4196fe71 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/INamedDependencyDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/INamedDependencyDescriptor.cs @@ -1,3 +1,5 @@ +using HotChocolate.Types.Descriptors; + namespace HotChocolate.Types; public interface INamedDependencyDescriptor @@ -15,4 +17,8 @@ INamedDependencyDescriptor DependsOn(bool mustBeNamed) INamedDependencyDescriptor DependsOn(string typeName); INamedDependencyDescriptor DependsOn(string typeName, bool mustBeNamed); + + INamedDependencyDescriptor DependsOn(TypeReference typeReference); + + INamedDependencyDescriptor DependsOn(TypeReference typeReference, bool mustBeNamed); } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IEnumTypeNameDependencyDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IEnumTypeNameDependencyDescriptor.cs index 74233fa624b..cd5dc8690cc 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IEnumTypeNameDependencyDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IEnumTypeNameDependencyDescriptor.cs @@ -1,3 +1,5 @@ +using HotChocolate.Types.Descriptors; + // ReSharper disable once CheckNamespace namespace HotChocolate.Types; @@ -7,4 +9,6 @@ IEnumTypeDescriptor DependsOn() where TDependency : IType; IEnumTypeDescriptor DependsOn(Type schemaType); + + IEnumTypeDescriptor DependsOn(TypeReference typeReference); } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IEnumTypeNameDependencyDescriptor~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IEnumTypeNameDependencyDescriptor~1.cs index 3e2087b3225..629b6136e1a 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IEnumTypeNameDependencyDescriptor~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IEnumTypeNameDependencyDescriptor~1.cs @@ -1,3 +1,5 @@ +using HotChocolate.Types.Descriptors; + // ReSharper disable once CheckNamespace namespace HotChocolate.Types; @@ -7,4 +9,6 @@ IEnumTypeDescriptor DependsOn() where TDependency : IType; IEnumTypeDescriptor DependsOn(Type schemaType); + + IEnumTypeDescriptor DependsOn(TypeReference typeReference); } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInputObjectTypeNameDependencyDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInputObjectTypeNameDependencyDescriptor.cs index 46ba8e9751a..22c3b0d1cf7 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInputObjectTypeNameDependencyDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInputObjectTypeNameDependencyDescriptor.cs @@ -1,3 +1,5 @@ +using HotChocolate.Types.Descriptors; + // ReSharper disable once CheckNamespace namespace HotChocolate.Types; @@ -7,4 +9,6 @@ IInputObjectTypeDescriptor DependsOn() where TDependency : IType; IInputObjectTypeDescriptor DependsOn(Type schemaType); + + IInputObjectTypeDescriptor DependsOn(TypeReference typeReference); } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInputObjectTypeNameDependencyDescriptor~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInputObjectTypeNameDependencyDescriptor~1.cs index b612c429e00..eefe4ba6f09 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInputObjectTypeNameDependencyDescriptor~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInputObjectTypeNameDependencyDescriptor~1.cs @@ -1,3 +1,5 @@ +using HotChocolate.Types.Descriptors; + // ReSharper disable once CheckNamespace namespace HotChocolate.Types; @@ -7,4 +9,6 @@ IInputObjectTypeDescriptor DependsOn() where TDependency : IType; IInputObjectTypeDescriptor DependsOn(Type schemaType); + + IInputObjectTypeDescriptor DependsOn(TypeReference typeReference); } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInterfaceTypeNameDependencyDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInterfaceTypeNameDependencyDescriptor.cs index 1482b333ade..c98e5bf8941 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInterfaceTypeNameDependencyDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInterfaceTypeNameDependencyDescriptor.cs @@ -1,3 +1,5 @@ +using HotChocolate.Types.Descriptors; + // ReSharper disable once CheckNamespace namespace HotChocolate.Types; @@ -7,4 +9,6 @@ IInterfaceTypeDescriptor DependsOn() where TDependency : IType; IInterfaceTypeDescriptor DependsOn(Type schemaType); + + IInterfaceTypeDescriptor DependsOn(TypeReference typeReference); } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInterfaceTypeNameDependencyDescriptor~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInterfaceTypeNameDependencyDescriptor~1.cs index 4bd475c01aa..20d76cbbf76 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInterfaceTypeNameDependencyDescriptor~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IInterfaceTypeNameDependencyDescriptor~1.cs @@ -1,3 +1,5 @@ +using HotChocolate.Types.Descriptors; + // ReSharper disable once CheckNamespace namespace HotChocolate.Types; @@ -7,4 +9,6 @@ IInterfaceTypeDescriptor DependsOn() where TDependency : IType; IInterfaceTypeDescriptor DependsOn(Type schemaType); + + IInterfaceTypeDescriptor DependsOn(TypeReference typeReference); } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IObjectTypeNameDependencyDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IObjectTypeNameDependencyDescriptor.cs index 8196be6b7ec..5a2e9d92407 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IObjectTypeNameDependencyDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IObjectTypeNameDependencyDescriptor.cs @@ -1,3 +1,5 @@ +using HotChocolate.Types.Descriptors; + // ReSharper disable once CheckNamespace namespace HotChocolate.Types; @@ -7,4 +9,6 @@ IObjectTypeDescriptor DependsOn() where TDependency : IType; IObjectTypeDescriptor DependsOn(Type schemaType); + + IObjectTypeDescriptor DependsOn(TypeReference typeReference); } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IObjectTypeNameDependencyDescriptor~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IObjectTypeNameDependencyDescriptor~1.cs index db7b79c8e61..2758b032839 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IObjectTypeNameDependencyDescriptor~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IObjectTypeNameDependencyDescriptor~1.cs @@ -1,3 +1,5 @@ +using HotChocolate.Types.Descriptors; + // ReSharper disable once CheckNamespace namespace HotChocolate.Types; @@ -7,4 +9,6 @@ IObjectTypeDescriptor DependsOn() where TDependency : IType; IObjectTypeDescriptor DependsOn(Type schemaType); + + IObjectTypeDescriptor DependsOn(TypeReference typeReference); } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IUnionTypeNameDependencyDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IUnionTypeNameDependencyDescriptor.cs index bf6a653f0d7..29cab095756 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IUnionTypeNameDependencyDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/NameDescriptors/IUnionTypeNameDependencyDescriptor.cs @@ -1,3 +1,5 @@ +using HotChocolate.Types.Descriptors; + // ReSharper disable once CheckNamespace namespace HotChocolate.Types; @@ -7,4 +9,6 @@ IUnionTypeDescriptor DependsOn() where TDependency : IType; IUnionTypeDescriptor DependsOn(Type schemaType); + + IUnionTypeDescriptor DependsOn(TypeReference typeReference); } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DefaultNamingConventions.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DefaultNamingConventions.cs index c973ffd5a7c..0523358a564 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DefaultNamingConventions.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DefaultNamingConventions.cs @@ -1,6 +1,7 @@ using System.Buffers; using System.Reflection; using HotChocolate.Internal; +using HotChocolate.Utilities; #nullable enable @@ -115,6 +116,50 @@ public virtual string GetTypeName(Type type, TypeKind kind) return name; } + public virtual string GetTypeName(string originalTypeName, TypeKind kind) + { + var name = NameUtils.MakeValidGraphQLName(originalTypeName); + ArgumentException.ThrowIfNullOrEmpty(name, nameof(originalTypeName)); + + if (_formatInterfaceName && + kind == TypeKind.Interface && + name.Length > 1 && + char.IsUpper(name[0]) && + char.IsUpper(name[1]) && + name[0] == 'I') + { + return name[1..]; + } + + if (kind == TypeKind.InputObject) + { + var isEndingInput = name.EndsWith(_inputPostfix, StringComparison.Ordinal); + + if (!isEndingInput) + { + return name + _inputPostfix; + } + } + + if (kind is TypeKind.Directive) + { + if (name.Length > _directivePostfix.Length && + name.EndsWith(_directivePostfix, StringComparison.Ordinal)) + { + name = name[..^_directivePostfix.Length]; + } + else if (name.Length > _directiveTypePostfix.Length && + name.EndsWith(_directiveTypePostfix, StringComparison.Ordinal)) + { + name = name[..^_directiveTypePostfix.Length]; + } + + name = NameFormattingHelpers.FormatFieldName(name); + } + + return name; + } + /// public virtual string? GetTypeDescription(Type type, TypeKind kind) { @@ -153,6 +198,13 @@ public virtual string GetMemberName( return member.GetGraphQLName(); } + public virtual string GetMemberName(string originalMemberName, MemberKind kind) + { + var name = NameUtils.MakeValidGraphQLName(originalMemberName); + ArgumentException.ThrowIfNullOrEmpty(name, nameof(originalMemberName)); + return NameFormattingHelpers.FormatFieldName(name); + } + /// public virtual string? GetMemberDescription( MemberInfo member, diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/INamingConventions.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/INamingConventions.cs index 9cbdaf17391..78d75805444 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/INamingConventions.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/INamingConventions.cs @@ -34,6 +34,20 @@ public interface INamingConventions : IConvention /// string GetTypeName(Type type, TypeKind kind); + /// + /// Formats a type name to abide by the current type naming convention for GraphQL types. + /// + /// + /// The original non-formatted type name. + /// + /// + /// The kind of GraphQL type the name is for. + /// + /// + /// Returns a name string that has the correct naming format. + /// + string GetTypeName(string originalTypeName, TypeKind kind); + /// /// Gets the description of a GraphQL type from its runtime type. /// @@ -62,6 +76,20 @@ public interface INamingConventions : IConvention /// string GetMemberName(MemberInfo member, MemberKind kind); + /// + /// Formats a member name to abide by the current member naming convention for GraphQL fields or arguments. + /// + /// + /// The original non-formatted member name. + /// + /// + /// The kind of GraphQL member the name is for. + /// + /// + /// Returns a name string that has the correct naming format. + /// + string GetMemberName(string originalMemberName, MemberKind kind); + /// /// Gets the description of a GraphQL field or argument from its runtime member. /// diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldDefinitionBase.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldDefinitionBase.cs index 12a5bd245ea..b88d11f796a 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldDefinitionBase.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldDefinitionBase.cs @@ -100,6 +100,14 @@ public IReadOnlyList GetDirectives() public void SetSourceGeneratorFlags() => Flags |= FieldFlags.SourceGenerator; + public void SetConnectionFlags() => Flags |= FieldFlags.Connection; + + public void SetConnectionEdgesFieldFlags() => Flags |= FieldFlags.ConnectionEdgesField; + + public void SetConnectionNodesFieldFlags() => Flags |= FieldFlags.ConnectionNodesField; + + public void SetConnectionTotalCountFieldFlags() => Flags |= FieldFlags.TotalCount; + protected void CopyTo(FieldDefinitionBase target) { base.CopyTo(target); diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/DependencyDescriptorBase~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/DependencyDescriptorBase~1.cs index 4538ab62b75..412d44e6f6b 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/DependencyDescriptorBase~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/DependencyDescriptorBase~1.cs @@ -70,4 +70,20 @@ protected void DependsOn( TypeReference.Create(new NamedTypeNode(typeName)), kind)); } + + protected void DependsOn( + TypeReference typeReference, + bool mustBeNamedOrCompleted) + { + if (typeReference is null) + { + throw new ArgumentNullException(nameof(typeReference)); + } + + var kind = mustBeNamedOrCompleted + ? DependencyFulfilled + : TypeDependencyFulfilled.Default; + + _configuration.AddDependency(new TypeDependency(typeReference, kind)); + } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/DescriptorBase~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/DescriptorBase~1.cs index 4753e56893d..8ea54491c0e 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/DescriptorBase~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/DescriptorBase~1.cs @@ -35,6 +35,20 @@ public IDescriptorExtension ExtendWith( return this; } + public IDescriptorExtension ExtendWith( + Action, TState> configure, + TState state) + { + if (configure is null) + { + throw new ArgumentNullException(nameof(configure)); + } + + configure(this, state); + return this; + } + + public T CreateDefinition() { OnCreateDefinition(Definition); diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/EnumTypeNameDependencyDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/EnumTypeNameDependencyDescriptor.cs index 8c649ae493c..874b716ead0 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/EnumTypeNameDependencyDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/EnumTypeNameDependencyDescriptor.cs @@ -1,5 +1,6 @@ using HotChocolate.Types.Helpers; +// ReSharper disable once CheckNamespace namespace HotChocolate.Types.Descriptors; internal class EnumTypeNameDependencyDescriptor @@ -30,4 +31,10 @@ public IEnumTypeDescriptor DependsOn(Type schemaType) TypeNameHelper.AddNameFunction(_descriptor, _createName, schemaType); return _descriptor; } + + public IEnumTypeDescriptor DependsOn(TypeReference typeReference) + { + TypeNameHelper.AddNameFunction(_descriptor, _createName, typeReference); + return _descriptor; + } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/EnumTypeNameDependencyDescriptor~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/EnumTypeNameDependencyDescriptor~1.cs index 1c42c3c95b6..160dee83f31 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/EnumTypeNameDependencyDescriptor~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/EnumTypeNameDependencyDescriptor~1.cs @@ -1,5 +1,6 @@ using HotChocolate.Types.Helpers; +// ReSharper disable once CheckNamespace namespace HotChocolate.Types.Descriptors; internal class EnumTypeNameDependencyDescriptor @@ -30,4 +31,10 @@ public IEnumTypeDescriptor DependsOn(Type schemaType) TypeNameHelper.AddNameFunction(_descriptor, _createName, schemaType); return _descriptor; } + + public IEnumTypeDescriptor DependsOn(TypeReference typeReference) + { + TypeNameHelper.AddNameFunction(_descriptor, _createName, typeReference); + return _descriptor; + } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InputObjectTypeNameDependencyDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InputObjectTypeNameDependencyDescriptor.cs index 0d3d93e2ad5..26c4e8f2cc0 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InputObjectTypeNameDependencyDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InputObjectTypeNameDependencyDescriptor.cs @@ -1,5 +1,6 @@ using HotChocolate.Types.Helpers; +// ReSharper disable once CheckNamespace namespace HotChocolate.Types.Descriptors; internal class InputObjectTypeNameDependencyDescriptor @@ -30,4 +31,10 @@ public IInputObjectTypeDescriptor DependsOn(Type schemaType) TypeNameHelper.AddNameFunction(_descriptor, _createName, schemaType); return _descriptor; } + + public IInputObjectTypeDescriptor DependsOn(TypeReference typeReference) + { + TypeNameHelper.AddNameFunction(_descriptor, _createName, typeReference); + return _descriptor; + } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InputObjectTypeNameDependencyDescriptor~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InputObjectTypeNameDependencyDescriptor~1.cs index 2eac724e58c..d79edaa7bff 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InputObjectTypeNameDependencyDescriptor~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InputObjectTypeNameDependencyDescriptor~1.cs @@ -1,5 +1,6 @@ using HotChocolate.Types.Helpers; +// ReSharper disable once CheckNamespace namespace HotChocolate.Types.Descriptors; internal class InputObjectTypeNameDependencyDescriptor @@ -30,4 +31,10 @@ public IInputObjectTypeDescriptor DependsOn(Type schemaType) TypeNameHelper.AddNameFunction(_descriptor, _createName, schemaType); return _descriptor; } + + public IInputObjectTypeDescriptor DependsOn(TypeReference typeReference) + { + TypeNameHelper.AddNameFunction(_descriptor, _createName, typeReference); + return _descriptor; + } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InterfaceTypeNameDependencyDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InterfaceTypeNameDependencyDescriptor.cs index 461979ea56f..c307982be33 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InterfaceTypeNameDependencyDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InterfaceTypeNameDependencyDescriptor.cs @@ -1,5 +1,6 @@ using HotChocolate.Types.Helpers; +// ReSharper disable once CheckNamespace namespace HotChocolate.Types.Descriptors; internal class InterfaceTypeNameDependencyDescriptor @@ -30,4 +31,10 @@ public IInterfaceTypeDescriptor DependsOn(Type schemaType) TypeNameHelper.AddNameFunction(_descriptor, _createName, schemaType); return _descriptor; } + + public IInterfaceTypeDescriptor DependsOn(TypeReference typeReference) + { + TypeNameHelper.AddNameFunction(_descriptor, _createName, typeReference); + return _descriptor; + } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InterfaceTypeNameDependencyDescriptor~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InterfaceTypeNameDependencyDescriptor~1.cs index 623aa0d6dce..25bd354d338 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InterfaceTypeNameDependencyDescriptor~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/InterfaceTypeNameDependencyDescriptor~1.cs @@ -1,5 +1,6 @@ using HotChocolate.Types.Helpers; +// ReSharper disable once CheckNamespace namespace HotChocolate.Types.Descriptors; internal class InterfaceTypeNameDependencyDescriptor @@ -30,4 +31,10 @@ public IInterfaceTypeDescriptor DependsOn(Type schemaType) TypeNameHelper.AddNameFunction(_descriptor, _createName, schemaType); return _descriptor; } + + public IInterfaceTypeDescriptor DependsOn(TypeReference typeReference) + { + TypeNameHelper.AddNameFunction(_descriptor, _createName, typeReference); + return _descriptor; + } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/ObjectTypeNameDependencyDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/ObjectTypeNameDependencyDescriptor.cs index 146c7a1a97f..182e82e09d6 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/ObjectTypeNameDependencyDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/ObjectTypeNameDependencyDescriptor.cs @@ -1,5 +1,6 @@ using HotChocolate.Types.Helpers; +// ReSharper disable once CheckNamespace namespace HotChocolate.Types.Descriptors; internal class ObjectTypeNameDependencyDescriptor @@ -30,4 +31,10 @@ public IObjectTypeDescriptor DependsOn(Type schemaType) TypeNameHelper.AddNameFunction(_descriptor, _createName, schemaType); return _descriptor; } + + public IObjectTypeDescriptor DependsOn(TypeReference typeReference) + { + TypeNameHelper.AddNameFunction(_descriptor, _createName, typeReference); + return _descriptor; + } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/ObjectTypeNameDependencyDescriptor~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/ObjectTypeNameDependencyDescriptor~1.cs index 2c6712e7911..83a2d577970 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/ObjectTypeNameDependencyDescriptor~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/ObjectTypeNameDependencyDescriptor~1.cs @@ -1,5 +1,6 @@ using HotChocolate.Types.Helpers; +// ReSharper disable once CheckNamespace namespace HotChocolate.Types.Descriptors; internal class ObjectTypeNameDependencyDescriptor @@ -30,4 +31,10 @@ public IObjectTypeDescriptor DependsOn(Type schemaType) TypeNameHelper.AddNameFunction(_descriptor, _createName, schemaType); return _descriptor; } + + public IObjectTypeDescriptor DependsOn(TypeReference typeReference) + { + TypeNameHelper.AddNameFunction(_descriptor, _createName, typeReference); + return _descriptor; + } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/UnionTypeNameDependencyDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/UnionTypeNameDependencyDescriptor.cs index 1b367521496..04834449241 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/UnionTypeNameDependencyDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/NameDescriptors/UnionTypeNameDependencyDescriptor.cs @@ -1,5 +1,6 @@ using HotChocolate.Types.Helpers; +// ReSharper disable once CheckNamespace namespace HotChocolate.Types.Descriptors; internal class UnionTypeNameDependencyDescriptor @@ -30,4 +31,10 @@ public IUnionTypeDescriptor DependsOn(Type schemaType) TypeNameHelper.AddNameFunction(_descriptor, _createName, schemaType); return _descriptor; } + + public IUnionTypeDescriptor DependsOn(TypeReference typeReference) + { + TypeNameHelper.AddNameFunction(_descriptor, _createName, typeReference); + return _descriptor; + } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/NamedDependencyDescriptor~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/NamedDependencyDescriptor~1.cs index cc87d65ab5f..b764411c347 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/NamedDependencyDescriptor~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/NamedDependencyDescriptor~1.cs @@ -13,12 +13,12 @@ public NamedDependencyDescriptor( { } - protected override TypeDependencyFulfilled DependencyFulfilled => - TypeDependencyFulfilled.Named; + protected override TypeDependencyFulfilled DependencyFulfilled + => TypeDependencyFulfilled.Named; public INamedDependencyDescriptor DependsOn() - where TType : ITypeSystemMember => - DependsOn(false); + where TType : ITypeSystemMember + => DependsOn(false); public new INamedDependencyDescriptor DependsOn(bool mustBeNamed) where TType : ITypeSystemMember @@ -27,25 +27,30 @@ public INamedDependencyDescriptor DependsOn() return this; } - public INamedDependencyDescriptor DependsOn(Type schemaType) => - DependsOn(schemaType, false); + public INamedDependencyDescriptor DependsOn(Type schemaType) + => DependsOn(schemaType, false); - public new INamedDependencyDescriptor DependsOn( - Type schemaType, bool mustBeNamed) + public new INamedDependencyDescriptor DependsOn(Type schemaType, bool mustBeNamed) { base.DependsOn(schemaType, mustBeNamed); return this; } - public INamedDependencyDescriptor DependsOn( - string typeName) => - DependsOn(typeName, false); + public INamedDependencyDescriptor DependsOn(string typeName) + => DependsOn(typeName, false); - public new INamedDependencyDescriptor DependsOn( - string typeName, - bool mustBeNamed) + public new INamedDependencyDescriptor DependsOn(string typeName, bool mustBeNamed) { base.DependsOn(typeName, mustBeNamed); return this; } + + public INamedDependencyDescriptor DependsOn(TypeReference typeReference) + => DependsOn(typeReference, false); + + public new INamedDependencyDescriptor DependsOn(TypeReference typeReference, bool mustBeNamed) + { + base.DependsOn(typeReference, mustBeNamed); + return this; + } } diff --git a/src/HotChocolate/Core/src/Types/Types/Helpers/TypeNameHelper.cs b/src/HotChocolate/Core/src/Types/Types/Helpers/TypeNameHelper.cs index d333e3302c4..c774b640bbe 100644 --- a/src/HotChocolate/Core/src/Types/Types/Helpers/TypeNameHelper.cs +++ b/src/HotChocolate/Core/src/Types/Types/Helpers/TypeNameHelper.cs @@ -1,5 +1,6 @@ using HotChocolate.Internal; using HotChocolate.Properties; +using HotChocolate.Types.Descriptors; using HotChocolate.Types.Descriptors.Definitions; namespace HotChocolate.Types.Helpers; @@ -53,4 +54,37 @@ public static void AddNameFunction( }) .DependsOn(dependency, true); } + + public static void AddNameFunction( + IDescriptor descriptor, + Func createName, + TypeReference dependency) + where TDefinition : DefinitionBase, ITypeDefinition + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + if (createName is null) + { + throw new ArgumentNullException(nameof(createName)); + } + + if (dependency is null) + { + throw new ArgumentNullException(nameof(dependency)); + } + + descriptor.Extend().Definition.NeedsNameCompletion = true; + + descriptor + .Extend() + .OnBeforeNaming((ctx, definition) => + { + var type = ctx.GetType(dependency); + definition.Name = createName(type.NamedType()); + }) + .DependsOn(dependency, true); + } } diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/IPage.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/IPage.cs index c3604a3b689..89ea2b274ea 100644 --- a/src/HotChocolate/Core/src/Types/Types/Pagination/IPage.cs +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/IPage.cs @@ -17,16 +17,12 @@ public interface IPage /// IPageInfo Info { get; } - /// - /// Gets the total count of the data set. - /// - int TotalCount { get; } - /// /// Accepts a page observer and will in turn report the page. /// /// - /// The page obserer. + /// The page observer. /// - void Accept(IPageObserver observer); + public void Accept(IPageObserver observer); } + diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/IPageTotalCountProvider.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/IPageTotalCountProvider.cs new file mode 100644 index 00000000000..dd541aa30f5 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/IPageTotalCountProvider.cs @@ -0,0 +1,8 @@ +#nullable enable + +namespace HotChocolate.Types.Pagination; + +public interface IPageTotalCountProvider +{ + public int TotalCount { get; } +} diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingDefaults.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingDefaults.cs index c04c2f06ea1..b39fbfc4bd3 100644 --- a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingDefaults.cs +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingDefaults.cs @@ -10,6 +10,8 @@ public static class PagingDefaults public const bool AllowBackwardPagination = true; + public const bool EnableRelativeCursors = false; + public const bool InferConnectionNameFromField = true; public const bool InferCollectionSegmentNameFromField = true; @@ -17,4 +19,17 @@ public static class PagingDefaults public const bool RequirePagingBoundaries = false; public const bool IncludeNodesField = true; + + public static void Apply(PagingOptions options) + { + options.DefaultPageSize ??= DefaultPageSize; + options.MaxPageSize ??= MaxPageSize; + options.IncludeTotalCount ??= IncludeTotalCount; + options.AllowBackwardPagination ??= AllowBackwardPagination; + options.EnableRelativeCursors ??= EnableRelativeCursors; + options.InferConnectionNameFromField ??= InferConnectionNameFromField; + options.InferCollectionSegmentNameFromField ??= InferCollectionSegmentNameFromField; + options.RequirePagingBoundaries ??= RequirePagingBoundaries; + options.IncludeNodesField ??= IncludeNodesField; + } } diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingHelper.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingHelper.cs index 5cda426c4d2..8ba504aeb82 100644 --- a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingHelper.cs +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingHelper.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using HotChocolate.Configuration; +using HotChocolate.Features; using HotChocolate.Internal; using HotChocolate.Resolvers; using HotChocolate.Types.Descriptors; @@ -196,12 +197,23 @@ internal static bool TryGetNamedType( return false; } + public static PagingOptions GetPagingOptions(ISchema schema, IObjectField field) + { + if (field.ContextData.TryGetValue(WellKnownContextData.PagingOptions, out var o) + && o is PagingOptions options) + { + return options; + } + + return schema.Features.GetRequired(); + } + internal static PagingOptions GetPagingOptions( this ITypeCompletionContext context, PagingOptions? options) => context.DescriptorContext.GetPagingOptions(options); - internal static PagingOptions GetPagingOptions( + public static PagingOptions GetPagingOptions( this IDescriptorContext context, PagingOptions? options) { diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs index ca987a9991a..4ce033a4891 100644 --- a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs @@ -59,6 +59,11 @@ public class PagingOptions /// public bool? IncludeNodesField { get; set; } + /// + /// Defines whether relative cursors are allowed. + /// + public bool? EnableRelativeCursors { get; set; } + /// /// Merges the options into this options instance wherever /// a property is not set. @@ -77,6 +82,7 @@ internal void Merge(PagingOptions other) InferCollectionSegmentNameFromField ??= other.InferCollectionSegmentNameFromField; ProviderName ??= other.ProviderName; IncludeNodesField ??= other.IncludeNodesField; + EnableRelativeCursors ??= other.EnableRelativeCursors; } /// @@ -93,6 +99,7 @@ internal PagingOptions Copy() InferConnectionNameFromField = InferConnectionNameFromField, InferCollectionSegmentNameFromField = InferCollectionSegmentNameFromField, ProviderName = ProviderName, - IncludeNodesField = IncludeNodesField + IncludeNodesField = IncludeNodesField, + EnableRelativeCursors = EnableRelativeCursors }; } diff --git a/src/HotChocolate/Core/src/Types/Utilities/ReflectionUtils.cs b/src/HotChocolate/Core/src/Types/Utilities/ReflectionUtils.cs index aa5e36f1b68..1c963c4e594 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/ReflectionUtils.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/ReflectionUtils.cs @@ -7,6 +7,9 @@ namespace HotChocolate.Utilities; public static class ReflectionUtils { + public const BindingFlags StaticMemberFlags = BindingFlags.Public | BindingFlags.NonPublic |BindingFlags.Static; + public const BindingFlags InstanceMemberFlags = BindingFlags.Public | BindingFlags.NonPublic |BindingFlags.Instance; + public static MemberInfo TryExtractMember( this Expression> memberExpression, bool allowStatic = false) diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/ObjectTypeTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/ObjectTypeTests.cs index 24a9f098f0d..625eb245ffd 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/ObjectTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/ObjectTypeTests.cs @@ -41,3 +41,4 @@ internal static partial class BookNode """).MatchMarkdownAsync(); } } + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/OperationTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/OperationTests.cs index c9ec57b3ba6..0d2113c5468 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/OperationTests.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/OperationTests.cs @@ -81,7 +81,7 @@ public static partial class Query { public static Foo GetTest(QueryContext context) { - return new Foo { Bar = ""abc"" }; + return new Foo { Bar = "abc" }; } } @@ -91,4 +91,32 @@ public class Foo } """).MatchMarkdownAsync(); } + + [Fact] + public async Task Root_NodeResolver() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + using HotChocolate.Types.Relay; + using GreenDonut.Data; + + namespace TestNamespace; + + [QueryType] + public static partial class Query + { + [NodeResolver] + public static Task GetTest(string id) + => default!; + } + + public class Foo + { + public string Id { get; set; } + } + """).MatchMarkdownAsync(); + } } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/PagingTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/PagingTests.cs new file mode 100644 index 00000000000..c97ec61410c --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/PagingTests.cs @@ -0,0 +1,432 @@ +namespace HotChocolate.Types; + +public class PagingTests +{ + [Fact] + public async Task GenerateSource_ConnectionT_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + public sealed class Author + { + public int Id { get; set; } + public string Name { get; set; } + } + + public sealed class Book + { + public int Id { get; set; } + public string Title { get; set; } + public int AuthorId { get; set; } + } + + [QueryType] + public static partial class BookPage + { + public static Task> GetAuthorsAsync( + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + } + """).MatchMarkdownAsync(); + } + + [Fact] + public async Task GenerateSource_CustomConnection_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + using HotChocolate.Types.Pagination; + + namespace TestNamespace; + + public sealed class Author + { + public int Id { get; set; } + public string Name { get; set; } + } + + [QueryType] + public static partial class AuthorQueries + { + public static Task GetAuthorsAsync( + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + } + + public class AuthorConnection : ConnectionBase + { + public override IReadOnlyList Edges => default!; + + public IReadOnlyList Nodes => default!; + + public override ConnectionPageInfo PageInfo => default!; + + public int TotalCount => 0; + } + + public class AuthorEdge : IEdge + { + public Author Node => default!; + + object? IEdge.Node => Node; + + public string Cursor => default!; + } + """).MatchMarkdownAsync(); + } + + [Fact] + public async Task GenerateSource_CustomConnection_UseConnection_IncludeTotalCount_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + using HotChocolate.Types.Pagination; + + namespace TestNamespace; + + public sealed class Author + { + public int Id { get; set; } + public string Name { get; set; } + } + + [QueryType] + public static partial class AuthorQueries + { + [UseConnection(IncludeTotalCount = true)] + public static Task GetAuthorsAsync( + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + } + + public class AuthorConnection : ConnectionBase + { + public override IReadOnlyList Edges => default!; + + public IReadOnlyList Nodes => default!; + + public override ConnectionPageInfo PageInfo => default!; + + public int TotalCount => 0; + } + + public class AuthorEdge : IEdge + { + public Author Node => default!; + + object? IEdge.Node => Node; + + public string Cursor => default!; + } + """).MatchMarkdownAsync(); + } + + [Fact] + public async Task GenerateSource_CustomConnection_UseConnection_ConnectionName_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + using HotChocolate.Types.Pagination; + + namespace TestNamespace; + + public sealed class Author + { + public int Id { get; set; } + public string Name { get; set; } + } + + [QueryType] + public static partial class AuthorQueries + { + [UseConnection(ConnectionName = "Authors123")] + public static Task GetAuthorsAsync( + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + } + + public class AuthorConnection : ConnectionBase + { + public override IReadOnlyList Edges => default!; + + public IReadOnlyList Nodes => default!; + + public override ConnectionPageInfo PageInfo => default!; + + public int TotalCount => 0; + } + + public class AuthorEdge : IEdge + { + public Author Node => default!; + + object? IEdge.Node => Node; + + public string Cursor => default!; + } + """).MatchMarkdownAsync(); + } + + [Fact] + public async Task GenerateSource_CustomConnection_No_Duplicates_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + using HotChocolate.Types.Pagination; + + namespace TestNamespace + { + public sealed class Author + { + public int Id { get; set; } + public string Name { get; set; } + } + } + + namespace TestNamespace.Types.Root + { + [QueryType] + public static partial class AuthorQueries + { + public static Task GetAuthorsAsync( + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + + public static Task GetAuthors2Async( + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + } + } + + namespace TestNamespace.Types.Nodes + { + [ObjectType] + public static partial class AuthorNode + { + public static Task GetAuthorsAsync( + [Parent] Author author, + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + } + } + + namespace TestNamespace + { + public class AuthorConnection : ConnectionBase + { + public override IReadOnlyList Edges => default!; + + public IReadOnlyList Nodes => default!; + + public override ConnectionPageInfo PageInfo => default!; + + public int TotalCount => 0; + } + + public class AuthorEdge : IEdge + { + public Author Node => default!; + + object? IEdge.Node => Node; + + public string Cursor => default!; + } + } + """).MatchMarkdownAsync(); + } + + [Fact] + public async Task GenerateSource_GenericCustomConnection_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + using HotChocolate.Types.Pagination; + + namespace TestNamespace + { + public sealed class Author + { + public int Id { get; set; } + public string Name { get; set; } + } + } + + namespace TestNamespace.Types.Root + { + [QueryType] + public static partial class AuthorQueries + { + public static Task> GetAuthorsAsync( + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + + public static Task> GetAuthors2Async( + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + } + } + + namespace TestNamespace.Types.Nodes + { + [ObjectType] + public static partial class AuthorNode + { + public static Task> GetAuthorsAsync( + [Parent] Author author, + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + } + } + + namespace TestNamespace + { + public class CustomConnection : ConnectionBase, ConnectionPageInfo> + { + public override IReadOnlyList> Edges => default!; + + public IReadOnlyList Nodes => default!; + + public override ConnectionPageInfo PageInfo => default!; + + public int TotalCount => 0; + } + + public class CustomEdge : IEdge + { + public T Node => default!; + + object? IEdge.Node => Node; + + public string Cursor => default!; + } + } + """).MatchMarkdownAsync(); + } + + [Fact] + public async Task GenerateSource_GenericCustomConnection_WithConnectionName_MatchesSnapshot() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + using HotChocolate.Types.Pagination; + + namespace TestNamespace + { + public sealed class Author + { + public int Id { get; set; } + public string Name { get; set; } + } + } + + namespace TestNamespace.Types.Root + { + [QueryType] + public static partial class AuthorQueries + { + public static Task> GetAuthorsAsync( + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + + [UseConnection(ConnectionName = "Authors2")] + public static Task> GetAuthors2Async( + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + } + } + + namespace TestNamespace.Types.Nodes + { + [ObjectType] + public static partial class AuthorNode + { + public static Task> GetAuthorsAsync( + [Parent] Author author, + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + } + } + + namespace TestNamespace + { + public class CustomConnection : ConnectionBase, ConnectionPageInfo> + { + public override IReadOnlyList>? Edges => default!; + + public IReadOnlyList Nodes => default!; + + public override ConnectionPageInfo PageInfo => default!; + + public int TotalCount => 0; + } + + public class CustomEdge : IEdge + { + public T Node => default!; + + object? IEdge.Node => Node; + + public string Cursor => default!; + } + } + """).MatchMarkdownAsync(); + } +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/TestHelper.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/TestHelper.cs index e406d51386c..b71fbb69d02 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/TestHelper.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/TestHelper.cs @@ -9,6 +9,7 @@ using GreenDonut.Data; using HotChocolate.Data.Filters; using HotChocolate.Types.Analyzers; +using HotChocolate.Types.Pagination; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -32,9 +33,9 @@ public static Snapshot GetGeneratedSourceSnapshot(string[] sourceTexts, string? #elif NET9_0 .. Net90.References.All, #endif - // HotChocolate.Types MetadataReference.CreateFromFile(typeof(ObjectTypeAttribute).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Connection).Assembly.Location), // HotChocolate.Abstractions MetadataReference.CreateFromFile(typeof(ParentAttribute).Assembly.Location), diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/ChapterNode2.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/ChapterNode2.cs index bce8bc1ac38..487103a0d00 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/ChapterNode2.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/ChapterNode2.cs @@ -6,7 +6,7 @@ public static partial class ChapterNode2 public static string FooBar => "test"; public static async Task GetBookAsync( - [HotChocolate.Parent] Chapter chapter, + [Parent] Chapter chapter, BookRepository repository, CancellationToken cancellationToken) => await repository.GetBookAsync(chapter.BookId, cancellationToken); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ObjectTypeTests.GenerateSource_BatchDataLoader_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ObjectTypeTests.GenerateSource_BatchDataLoader_MatchesSnapshot.md index 43ec255e466..01dae28a53a 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ObjectTypeTests.GenerateSource_BatchDataLoader_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ObjectTypeTests.GenerateSource_BatchDataLoader_MatchesSnapshot.md @@ -1,6 +1,6 @@ # GenerateSource_BatchDataLoader_MatchesSnapshot -## HotChocolateResolvers.735550c.g.cs +## BookNode.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -13,66 +13,54 @@ using System.Runtime.CompilerServices; using HotChocolate; using HotChocolate.Types; using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; using HotChocolate.Internal; namespace TestNamespace { - internal static class BookNodeResolvers + internal static partial class BookNode { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - private readonly static global::HotChocolate.Internal.IParameterBinding[] _args_BookNode_GetAuthorAsync = new global::HotChocolate.Internal.IParameterBinding[2]; - - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) { - if (!_bindingsInitialized) - { - lock (_sync) - { - if (!_bindingsInitialized) - { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var type = typeof(global::TestNamespace.BookNode); - global::System.Reflection.MethodInfo resolver = default!; - global::System.Reflection.ParameterInfo[] parameters = default!; - _bindingsInitialized = true; - - resolver = type.GetMethod( - "GetAuthorAsync", - bindingFlags, - new global::System.Type[] - { - typeof(global::TestNamespace.Book), - typeof(global::System.Threading.CancellationToken) - })!; - parameters = resolver.GetParameters(); - _args_BookNode_GetAuthorAsync[0] = bindingResolver.GetBinding(parameters[0]); - _args_BookNode_GetAuthorAsync[1] = bindingResolver.GetBinding(parameters[1]); - } - } - } - } + var thisType = typeof(global::TestNamespace.BookNode); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); - public static HotChocolate.Resolvers.FieldResolverDelegates BookNode_GetAuthorAsync() - { - if(!_bindingsInitialized) + var naming = descriptor.Extend().Context.Naming; + var ignoredFields = new global::System.Collections.Generic.HashSet(); + ignoredFields.Add(naming.GetMemberName("AuthorId", global::HotChocolate.Types.MemberKind.ObjectField)); + + foreach(string fieldName in ignoredFields) { - throw new global::System.InvalidOperationException("The bindings must be initialized before the resolvers can be created."); + descriptor.Field(fieldName).Ignore(); } - return new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: BookNode_GetAuthorAsync_Resolver); + descriptor + .Field(thisType.GetMember("GetAuthorAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.GetAuthorAsync(); + }, + resolvers); + + Configure(descriptor); } - private static async global::System.Threading.Tasks.ValueTask BookNode_GetAuthorAsync_Resolver(global::HotChocolate.Resolvers.IResolverContext context) + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers { - var args0 = context.Parent(); - var args1 = context.RequestAborted; - var result = await global::TestNamespace.BookNode.GetAuthorAsync(args0, args1); - return result; + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = context.Parent(); + var args1 = context.RequestAborted; + var result = await global::TestNamespace.BookNode.GetAuthorAsync(args0, args1); + return result; + } } } } @@ -111,61 +99,3 @@ namespace Microsoft.Extensions.DependencyInjection ``` -## HotChocolateTypes.735550c.g.cs - -```csharp -// - -#nullable enable -#pragma warning disable - -using System; -using System.Runtime.CompilerServices; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Execution.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace TestNamespace -{ -public static partial class BookNode -{ - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) - { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - const global::System.Reflection.BindingFlags runtimeBindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Instance - | global::System.Reflection.BindingFlags.Static; - - var thisType = typeof(global::TestNamespace.BookNode); - var runtimeType = typeof(global::TestNamespace.Book); - var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; - global::TestNamespace.BookNodeResolvers.InitializeBindings(bindingResolver); - - descriptor - .Field(thisType.GetMember("GetAuthorAsync", bindingFlags)[0]) - .ExtendWith(c => - { - c.Definition.SetSourceGeneratorFlags(); - c.Definition.Resolvers = global::TestNamespace.BookNodeResolvers.BookNode_GetAuthorAsync(); - }); - - descriptor - .Field(runtimeType.GetMember("AuthorId", runtimeBindingFlags)[0]) - .Ignore(); - - Configure(descriptor); - } - - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); -} -} - - -``` - diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Partial_Static_QueryType.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Partial_Static_QueryType.md index 9722f9840d7..201a2a36158 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Partial_Static_QueryType.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Partial_Static_QueryType.md @@ -1,86 +1,5 @@ # Partial_Static_QueryType -## HotChocolateResolvers.735550c.g.cs - -```csharp -// - -#nullable enable -#pragma warning disable - -using System; -using System.Runtime.CompilerServices; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Execution.Configuration; -using HotChocolate.Internal; - -namespace TestNamespace -{ - internal static class QueryResolvers - { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - private readonly static global::HotChocolate.Internal.IParameterBinding[] _args_Query_GetTest = new global::HotChocolate.Internal.IParameterBinding[1]; - - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) - { - if (!_bindingsInitialized) - { - lock (_sync) - { - if (!_bindingsInitialized) - { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var type = typeof(global::TestNamespace.Query); - global::System.Reflection.MethodInfo resolver = default!; - global::System.Reflection.ParameterInfo[] parameters = default!; - _bindingsInitialized = true; - - resolver = type.GetMethod( - "GetTest", - bindingFlags, - new global::System.Type[] - { - typeof(string) - })!; - parameters = resolver.GetParameters(); - _args_Query_GetTest[0] = bindingResolver.GetBinding(parameters[0]); - } - } - } - } - - public static HotChocolate.Resolvers.FieldResolverDelegates Query_GetTest() - { - if(!_bindingsInitialized) - { - throw new global::System.InvalidOperationException("The bindings must be initialized before the resolvers can be created."); - } - - var isPure = _args_Query_GetTest[0].IsPure; - - return isPure - ? new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Query_GetTest_Resolver) - : new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: c => new(Query_GetTest_Resolver(c))); - } - - private static global::System.Object? Query_GetTest_Resolver(global::HotChocolate.Resolvers.IResolverContext context) - { - var args0 = _args_Query_GetTest[0].Execute(context); - var result = global::TestNamespace.Query.GetTest(args0); - return result; - } - } -} - - -``` - ## HotChocolateTypeModule.735550c.g.cs ```csharp @@ -117,7 +36,7 @@ namespace Microsoft.Extensions.DependencyInjection ``` -## HotChocolateTypes.735550c.g.cs +## Query.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -131,35 +50,71 @@ using HotChocolate; using HotChocolate.Types; using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; namespace TestNamespace { -public static partial class Query -{ - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + public static partial class Query { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var thisType = typeof(global::TestNamespace.Query); - var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; - global::TestNamespace.QueryResolvers.InitializeBindings(bindingResolver); - - descriptor - .Field(thisType.GetMember("GetTest", bindingFlags)[0]) - .ExtendWith(c => + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.Query); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(bindingResolver); + + descriptor + .Field(thisType.GetMember("GetTest", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.GetTest(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + private readonly global::HotChocolate.Internal.IParameterBinding[] _args_GetTest = new global::HotChocolate.Internal.IParameterBinding[1]; + + public __Resolvers(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) { - c.Definition.SetSourceGeneratorFlags(); - c.Definition.Resolvers = global::TestNamespace.QueryResolvers.Query_GetTest(); - }); + var type = typeof(global::TestNamespace.Query); + global::System.Reflection.MethodInfo resolver = default!; + global::System.Reflection.ParameterInfo[] parameters = default!; + + resolver = type.GetMethod( + "GetTest", + global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags, + new global::System.Type[] + { + typeof(string) + })!; - Configure(descriptor); - } + parameters = resolver.GetParameters(); + _args_GetTest[0] = bindingResolver.GetBinding(parameters[0]); + } - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); -} + public HotChocolate.Resolvers.FieldResolverDelegates GetTest() + { + var isPureResolver = _args_GetTest[0].IsPure; + + return isPureResolver + ? new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: GetTest) + : new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: c => new(GetTest(c))); + } + + private global::System.Object? GetTest(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = _args_GetTest[0].Execute(context); + var result = global::TestNamespace.Query.GetTest(args0); + return result; + } + } + } } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Root_NodeResolver.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Root_NodeResolver.md new file mode 100644 index 00000000000..3a27558698b --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Root_NodeResolver.md @@ -0,0 +1,116 @@ +# Root_NodeResolver + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Query", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.Query.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + return builder; + } + } +} + +``` + +## Query.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public static partial class Query + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.Query); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(bindingResolver); + + descriptor + .Field(thisType.GetMember("GetTest", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.GetTest(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + private readonly global::HotChocolate.Internal.IParameterBinding[] _args_GetTest = new global::HotChocolate.Internal.IParameterBinding[1]; + + public __Resolvers(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) + { + var type = typeof(global::TestNamespace.Query); + global::System.Reflection.MethodInfo resolver = default!; + global::System.Reflection.ParameterInfo[] parameters = default!; + + resolver = type.GetMethod( + "GetTest", + global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags, + new global::System.Type[] + { + typeof(string) + })!; + + parameters = resolver.GetParameters(); + _args_GetTest[0] = bindingResolver.GetBinding(parameters[0]); + } + + public HotChocolate.Resolvers.FieldResolverDelegates GetTest() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetTest); + + private async global::System.Threading.Tasks.ValueTask GetTest(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = _args_GetTest[0].Execute(context); + var result = await global::TestNamespace.Query.GetTest(args0); + return result; + } + } + } +} + + +``` + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Root_Projection_Single_Entity.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Root_Projection_Single_Entity.md index f98cebeec25..78579248333 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Root_Projection_Single_Entity.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Root_Projection_Single_Entity.md @@ -1,87 +1,5 @@ # Root_Projection_Single_Entity -## HotChocolateResolvers.735550c.g.cs - -```csharp -// - -#nullable enable -#pragma warning disable - -using System; -using System.Runtime.CompilerServices; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Execution.Configuration; -using HotChocolate.Internal; - -namespace TestNamespace -{ - internal static class QueryResolvers - { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - private readonly static global::HotChocolate.Internal.IParameterBinding[] _args_Query_GetTest = new global::HotChocolate.Internal.IParameterBinding[1]; - - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) - { - if (!_bindingsInitialized) - { - lock (_sync) - { - if (!_bindingsInitialized) - { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var type = typeof(global::TestNamespace.Query); - global::System.Reflection.MethodInfo resolver = default!; - global::System.Reflection.ParameterInfo[] parameters = default!; - _bindingsInitialized = true; - - resolver = type.GetMethod( - "GetTest", - bindingFlags, - new global::System.Type[] - { - typeof(global::GreenDonut.Data.QueryContext) - })!; - parameters = resolver.GetParameters(); - _args_Query_GetTest[0] = bindingResolver.GetBinding(parameters[0]); - } - } - } - } - - public static HotChocolate.Resolvers.FieldResolverDelegates Query_GetTest() - { - if(!_bindingsInitialized) - { - throw new global::System.InvalidOperationException("The bindings must be initialized before the resolvers can be created."); - } - return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Query_GetTest_Resolver); - } - - private static global::System.Object? Query_GetTest_Resolver(global::HotChocolate.Resolvers.IResolverContext context) - { - var args0_selection = context.Selection; - var args0_filter = HotChocolate.Data.Filters.FilterContextResolverContextExtensions.GetFilterContext(context); - var args0_sorting = HotChocolate.Data.Sorting.SortingContextResolverContextExtensions.GetSortingContext(context); - var args0 = new global::GreenDonut.Data.QueryContext( - HotChocolate.Execution.Processing.HotChocolateExecutionSelectionExtensions.AsSelector(args0_selection), - args0_filter?.AsPredicate(), - args0_sorting?.AsSortDefinition()); - var result = global::TestNamespace.Query.GetTest(args0); - return result; - } - } -} - - -``` - ## HotChocolateTypeModule.735550c.g.cs ```csharp @@ -118,7 +36,7 @@ namespace Microsoft.Extensions.DependencyInjection ``` -## HotChocolateTypes.735550c.g.cs +## Query.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -132,124 +50,55 @@ using HotChocolate; using HotChocolate.Types; using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; namespace TestNamespace { -public static partial class Query -{ - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + public static partial class Query { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var thisType = typeof(global::TestNamespace.Query); - var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; - global::TestNamespace.QueryResolvers.InitializeBindings(bindingResolver); - - descriptor - .Field(thisType.GetMember("GetTest", bindingFlags)[0]) - .ExtendWith(c => + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.Query); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetTest", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.GetTest(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetTest() { - c.Definition.SetSourceGeneratorFlags(); - c.Definition.Resolvers = global::TestNamespace.QueryResolvers.Query_GetTest(); - }); + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: GetTest); + } - Configure(descriptor); + private global::System.Object? GetTest(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_selection = context.Selection; + var args0_filter = global::HotChocolate.Data.Filters.FilterContextResolverContextExtensions.GetFilterContext(context); + var args0_sorting = global::HotChocolate.Data.Sorting.SortingContextResolverContextExtensions.GetSortingContext(context); + var args0 = new global::GreenDonut.Data.QueryContext( + global::HotChocolate.Execution.Processing.HotChocolateExecutionSelectionExtensions.AsSelector(args0_selection), + args0_filter?.AsPredicate(), + args0_sorting?.AsSortDefinition()); + var result = global::TestNamespace.Query.GetTest(args0); + return result; + } + } } - - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); } -} - -``` -## Compilation Diagnostics - -```json -[ - { - "Id": "CS1003", - "Title": "", - "Severity": "Error", - "WarningLevel": 0, - "Location": ": (11,33)-(11,36)", - "HelpLinkUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS1003)", - "MessageFormat": "Syntax error, '{0}' expected", - "Message": "Syntax error, ',' expected", - "Category": "Compiler", - "CustomTags": [ - "Compiler", - "Telemetry", - "NotConfigurable" - ] - }, - { - "Id": "CS1003", - "Title": "", - "Severity": "Error", - "WarningLevel": 0, - "Location": ": (11,36)-(11,38)", - "HelpLinkUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS1003)", - "MessageFormat": "Syntax error, '{0}' expected", - "Message": "Syntax error, ',' expected", - "Category": "Compiler", - "CustomTags": [ - "Compiler", - "Telemetry", - "NotConfigurable" - ] - }, - { - "Id": "CS0103", - "Title": "", - "Severity": "Error", - "WarningLevel": 0, - "Location": ": (11,33)-(11,36)", - "HelpLinkUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0103)", - "MessageFormat": "The name '{0}' does not exist in the current context", - "Message": "The name 'abc' does not exist in the current context", - "Category": "Compiler", - "CustomTags": [ - "Compiler", - "Telemetry", - "NotConfigurable" - ] - }, - { - "Id": "CS0747", - "Title": "", - "Severity": "Error", - "WarningLevel": 0, - "Location": ": (11,33)-(11,36)", - "HelpLinkUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0747)", - "MessageFormat": "Invalid initializer member declarator", - "Message": "Invalid initializer member declarator", - "Category": "Compiler", - "CustomTags": [ - "Compiler", - "Telemetry", - "NotConfigurable" - ] - }, - { - "Id": "CS0747", - "Title": "", - "Severity": "Error", - "WarningLevel": 0, - "Location": ": (11,36)-(11,38)", - "HelpLinkUri": "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0747)", - "MessageFormat": "Invalid initializer member declarator", - "Message": "Invalid initializer member declarator", - "Category": "Compiler", - "CustomTags": [ - "Compiler", - "Telemetry", - "NotConfigurable" - ] - } -] ``` diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionT_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionT_MatchesSnapshot.md new file mode 100644 index 00000000000..24b8468466a --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionT_MatchesSnapshot.md @@ -0,0 +1,129 @@ +# GenerateSource_ConnectionT_MatchesSnapshot + +## BookPage.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public static partial class BookPage + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.BookPage); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetAuthorsAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.GetAuthorsAsync(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorsAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorsAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_first = context.ArgumentValue("first"); + var args0_after = context.ArgumentValue("after"); + int? args0_last = null; + string? args0_before = null; + bool args0_includeTotalCount = false; + + if(args0_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args0_last = context.ArgumentValue("last"); + args0_before = context.ArgumentValue("before"); + } + + if(args0_first is null && args0_last is null) + { + args0_first = args0_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args0_includeTotalCount = context.IsSelected("totalCount"); + } + + var args0 = new global::GreenDonut.Data.PagingArguments( + args0_first, + args0_after, + args0_last, + args0_before, + args0_includeTotalCount) + { + EnableRelativeCursors = args0_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args1 = context.RequestAborted; + var result = await global::TestNamespace.BookPage.GetAuthorsAsync(args0, args1); + return result; + } + } + } +} + + +``` + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.BookPage", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.BookPage.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + return builder; + } + } +} + +``` + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_MatchesSnapshot.md new file mode 100644 index 00000000000..e2e0461b301 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_MatchesSnapshot.md @@ -0,0 +1,321 @@ +# GenerateSource_CustomConnection_MatchesSnapshot + +## AuthorConnectionType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorConnectionType : ObjectType + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorConnection); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .Type>>>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionEdgesFieldFlags(); + c.Definition.Resolvers = r.Edges(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Nodes", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionNodesFieldFlags(); + c.Definition.Resolvers = r.Nodes(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("PageInfo", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.PageInfo(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("TotalCount", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionTotalCountFieldFlags(); + c.Definition.Resolvers = r.TotalCount(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Edges() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Edges); + + private global::System.Object? Edges(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Edges; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Nodes() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Nodes); + + private global::System.Object? Nodes(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Nodes; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates PageInfo() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: PageInfo); + + private global::System.Object? PageInfo(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().PageInfo; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates TotalCount() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TotalCount); + + private global::System.Object? TotalCount(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().TotalCount; + return result; + } + } + } +} + + +``` + +## AuthorEdgeType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorEdgeType : ObjectType + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorEdge); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Node(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Cursor", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Cursor(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Node() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Node); + + private global::System.Object? Node(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Node; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Cursor() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Cursor); + + private global::System.Object? Cursor(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Cursor; + return result; + } + } + } +} + + +``` + +## AuthorQueries.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public static partial class AuthorQueries + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorQueries); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetAuthorsAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthorsAsync(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorsAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorsAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_first = context.ArgumentValue("first"); + var args0_after = context.ArgumentValue("after"); + int? args0_last = null; + string? args0_before = null; + bool args0_includeTotalCount = false; + + if(args0_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args0_last = context.ArgumentValue("last"); + args0_before = context.ArgumentValue("before"); + } + + if(args0_first is null && args0_last is null) + { + args0_first = args0_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args0_includeTotalCount = context.IsSelected("totalCount"); + } + + var args0 = new global::GreenDonut.Data.PagingArguments( + args0_first, + args0_after, + args0_last, + args0_before, + args0_includeTotalCount) + { + EnableRelativeCursors = args0_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args1 = context.RequestAborted; + var result = await global::TestNamespace.AuthorQueries.GetAuthorsAsync(args0, args1); + return result; + } + } + } +} + + +``` + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.AddType(); + builder.AddType(); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.AuthorQueries", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.AuthorQueries.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + return builder; + } + } +} + +``` + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_No_Duplicates_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_No_Duplicates_MatchesSnapshot.md new file mode 100644 index 00000000000..e4dd560bde6 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_No_Duplicates_MatchesSnapshot.md @@ -0,0 +1,481 @@ +# GenerateSource_CustomConnection_No_Duplicates_MatchesSnapshot + +## AuthorConnectionType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorConnectionType : ObjectType + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorConnection); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .Type>>>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionEdgesFieldFlags(); + c.Definition.Resolvers = r.Edges(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Nodes", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionNodesFieldFlags(); + c.Definition.Resolvers = r.Nodes(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("PageInfo", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.PageInfo(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("TotalCount", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionTotalCountFieldFlags(); + c.Definition.Resolvers = r.TotalCount(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Edges() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Edges); + + private global::System.Object? Edges(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Edges; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Nodes() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Nodes); + + private global::System.Object? Nodes(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Nodes; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates PageInfo() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: PageInfo); + + private global::System.Object? PageInfo(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().PageInfo; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates TotalCount() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TotalCount); + + private global::System.Object? TotalCount(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().TotalCount; + return result; + } + } + } +} + + +``` + +## AuthorEdgeType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorEdgeType : ObjectType + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorEdge); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Node(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Cursor", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Cursor(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Node() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Node); + + private global::System.Object? Node(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Node; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Cursor() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Cursor); + + private global::System.Object? Cursor(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Cursor; + return result; + } + } + } +} + + +``` + +## AuthorNode.52INnvSU4xks9KALUdsc2g.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace.Types.Nodes +{ + public static partial class AuthorNode + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.Types.Nodes.AuthorNode); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetAuthorsAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthorsAsync(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorsAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorsAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = context.Parent(); + var args1_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args1_first = context.ArgumentValue("first"); + var args1_after = context.ArgumentValue("after"); + int? args1_last = null; + string? args1_before = null; + bool args1_includeTotalCount = false; + + if(args1_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args1_last = context.ArgumentValue("last"); + args1_before = context.ArgumentValue("before"); + } + + if(args1_first is null && args1_last is null) + { + args1_first = args1_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args1_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args1_includeTotalCount = context.IsSelected("totalCount"); + } + + var args1 = new global::GreenDonut.Data.PagingArguments( + args1_first, + args1_after, + args1_last, + args1_before, + args1_includeTotalCount) + { + EnableRelativeCursors = args1_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args2 = context.RequestAborted; + var result = await global::TestNamespace.Types.Nodes.AuthorNode.GetAuthorsAsync(args0, args1, args2); + return result; + } + } + } +} + + +``` + +## AuthorQueries.oWmb2tjg9NMLpwHX801V4w.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace.Types.Root +{ + public static partial class AuthorQueries + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.Types.Root.AuthorQueries); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetAuthorsAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthorsAsync(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("GetAuthors2Async", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthors2Async(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorsAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorsAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_first = context.ArgumentValue("first"); + var args0_after = context.ArgumentValue("after"); + int? args0_last = null; + string? args0_before = null; + bool args0_includeTotalCount = false; + + if(args0_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args0_last = context.ArgumentValue("last"); + args0_before = context.ArgumentValue("before"); + } + + if(args0_first is null && args0_last is null) + { + args0_first = args0_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args0_includeTotalCount = context.IsSelected("totalCount"); + } + + var args0 = new global::GreenDonut.Data.PagingArguments( + args0_first, + args0_after, + args0_last, + args0_before, + args0_includeTotalCount) + { + EnableRelativeCursors = args0_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args1 = context.RequestAborted; + var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1); + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthors2Async() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthors2Async); + + private async global::System.Threading.Tasks.ValueTask GetAuthors2Async(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_first = context.ArgumentValue("first"); + var args0_after = context.ArgumentValue("after"); + int? args0_last = null; + string? args0_before = null; + bool args0_includeTotalCount = false; + + if(args0_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args0_last = context.ArgumentValue("last"); + args0_before = context.ArgumentValue("before"); + } + + if(args0_first is null && args0_last is null) + { + args0_first = args0_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args0_includeTotalCount = context.IsSelected("totalCount"); + } + + var args0 = new global::GreenDonut.Data.PagingArguments( + args0_first, + args0_after, + args0_last, + args0_before, + args0_includeTotalCount) + { + EnableRelativeCursors = args0_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args1 = context.RequestAborted; + var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthors2Async(args0, args1); + return result; + } + } + } +} + + +``` + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.AddType(); + builder.AddType(); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Types.Nodes.AuthorNode", + () => global::TestNamespace.Types.Nodes.AuthorNode.Initialize)); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Types.Root.AuthorQueries", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.Types.Root.AuthorQueries.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + builder.AddType>(); + return builder; + } + } +} + +``` + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_ConnectionName_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_ConnectionName_MatchesSnapshot.md new file mode 100644 index 00000000000..6f00b520176 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_ConnectionName_MatchesSnapshot.md @@ -0,0 +1,325 @@ +# GenerateSource_CustomConnection_UseConnection_ConnectionName_MatchesSnapshot + +## AuthorQueries.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public static partial class AuthorQueries + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorQueries); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetAuthorsAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthorsAsync(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorsAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorsAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_first = context.ArgumentValue("first"); + var args0_after = context.ArgumentValue("after"); + int? args0_last = null; + string? args0_before = null; + bool args0_includeTotalCount = false; + + if(args0_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args0_last = context.ArgumentValue("last"); + args0_before = context.ArgumentValue("before"); + } + + if(args0_first is null && args0_last is null) + { + args0_first = args0_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args0_includeTotalCount = context.IsSelected("totalCount"); + } + + var args0 = new global::GreenDonut.Data.PagingArguments( + args0_first, + args0_after, + args0_last, + args0_before, + args0_includeTotalCount) + { + EnableRelativeCursors = args0_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args1 = context.RequestAborted; + var result = await global::TestNamespace.AuthorQueries.GetAuthorsAsync(args0, args1); + return result; + } + } + } +} + + +``` + +## Authors123ConnectionType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class Authors123ConnectionType : ObjectType + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorConnection); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor.Name("Authors123Connection"); + + descriptor + .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .Type>>>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionEdgesFieldFlags(); + c.Definition.Resolvers = r.Edges(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Nodes", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionNodesFieldFlags(); + c.Definition.Resolvers = r.Nodes(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("PageInfo", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.PageInfo(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("TotalCount", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionTotalCountFieldFlags(); + c.Definition.Resolvers = r.TotalCount(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Edges() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Edges); + + private global::System.Object? Edges(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Edges; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Nodes() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Nodes); + + private global::System.Object? Nodes(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Nodes; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates PageInfo() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: PageInfo); + + private global::System.Object? PageInfo(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().PageInfo; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates TotalCount() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TotalCount); + + private global::System.Object? TotalCount(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().TotalCount; + return result; + } + } + } +} + + +``` + +## Authors123EdgeType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class Authors123EdgeType : ObjectType + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorEdge); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor.Name("Authors123Edge"); + + descriptor + .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Node(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Cursor", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Cursor(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Node() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Node); + + private global::System.Object? Node(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Node; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Cursor() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Cursor); + + private global::System.Object? Cursor(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Cursor; + return result; + } + } + } +} + + +``` + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.AddType(); + builder.AddType(); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.AuthorQueries", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.AuthorQueries.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + return builder; + } + } +} + +``` + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_IncludeTotalCount_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_IncludeTotalCount_MatchesSnapshot.md new file mode 100644 index 00000000000..074bbb19559 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_IncludeTotalCount_MatchesSnapshot.md @@ -0,0 +1,321 @@ +# GenerateSource_CustomConnection_UseConnection_IncludeTotalCount_MatchesSnapshot + +## AuthorConnectionType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorConnectionType : ObjectType + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorConnection); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .Type>>>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionEdgesFieldFlags(); + c.Definition.Resolvers = r.Edges(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Nodes", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionNodesFieldFlags(); + c.Definition.Resolvers = r.Nodes(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("PageInfo", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.PageInfo(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("TotalCount", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionTotalCountFieldFlags(); + c.Definition.Resolvers = r.TotalCount(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Edges() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Edges); + + private global::System.Object? Edges(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Edges; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Nodes() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Nodes); + + private global::System.Object? Nodes(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Nodes; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates PageInfo() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: PageInfo); + + private global::System.Object? PageInfo(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().PageInfo; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates TotalCount() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TotalCount); + + private global::System.Object? TotalCount(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().TotalCount; + return result; + } + } + } +} + + +``` + +## AuthorEdgeType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorEdgeType : ObjectType + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorEdge); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Node(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Cursor", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Cursor(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Node() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Node); + + private global::System.Object? Node(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Node; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Cursor() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Cursor); + + private global::System.Object? Cursor(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Cursor; + return result; + } + } + } +} + + +``` + +## AuthorQueries.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public static partial class AuthorQueries + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorQueries); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetAuthorsAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthorsAsync(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorsAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorsAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_first = context.ArgumentValue("first"); + var args0_after = context.ArgumentValue("after"); + int? args0_last = null; + string? args0_before = null; + bool args0_includeTotalCount = false; + + if(args0_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args0_last = context.ArgumentValue("last"); + args0_before = context.ArgumentValue("before"); + } + + if(args0_first is null && args0_last is null) + { + args0_first = args0_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args0_includeTotalCount = context.IsSelected("totalCount"); + } + + var args0 = new global::GreenDonut.Data.PagingArguments( + args0_first, + args0_after, + args0_last, + args0_before, + args0_includeTotalCount) + { + EnableRelativeCursors = args0_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args1 = context.RequestAborted; + var result = await global::TestNamespace.AuthorQueries.GetAuthorsAsync(args0, args1); + return result; + } + } + } +} + + +``` + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.AddType(); + builder.AddType(); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.AuthorQueries", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.AuthorQueries.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + return builder; + } + } +} + +``` + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_MatchesSnapshot.md new file mode 100644 index 00000000000..06707c6567a --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_MatchesSnapshot.md @@ -0,0 +1,491 @@ +# GenerateSource_GenericCustomConnection_MatchesSnapshot + +## AuthorCustomConnectionType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorCustomConnectionType : ObjectType> + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor> descriptor) + { + var thisType = typeof(global::TestNamespace.CustomConnection); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + var nodeTypeRef = extend.Context.TypeInspector.GetTypeRef(typeof(global::TestNamespace.Author)); + descriptor + .Name(t => string.Format("{0}CustomConnection", t.Name)) + .DependsOn(nodeTypeRef); + + descriptor + .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .Type>>>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionEdgesFieldFlags(); + c.Definition.Resolvers = r.Edges(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Nodes", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionNodesFieldFlags(); + c.Definition.Resolvers = r.Nodes(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("PageInfo", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.PageInfo(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("TotalCount", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionTotalCountFieldFlags(); + c.Definition.Resolvers = r.TotalCount(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Edges() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Edges); + + private global::System.Object? Edges(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Edges; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Nodes() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Nodes); + + private global::System.Object? Nodes(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Nodes; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates PageInfo() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: PageInfo); + + private global::System.Object? PageInfo(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().PageInfo; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates TotalCount() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TotalCount); + + private global::System.Object? TotalCount(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().TotalCount; + return result; + } + } + } +} + + +``` + +## AuthorCustomEdgeType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorCustomEdgeType : ObjectType> + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor> descriptor) + { + var thisType = typeof(global::TestNamespace.CustomEdge); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + var nodeTypeRef = extend.Context.TypeInspector.GetTypeRef(typeof(global::TestNamespace.Author)); + descriptor + .Name(t => string.Format("{0}CustomEdge", t.Name)) + .DependsOn(nodeTypeRef); + + descriptor + .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Node(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Cursor", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Cursor(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Node() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Node); + + private global::System.Object? Node(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Node; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Cursor() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Cursor); + + private global::System.Object? Cursor(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Cursor; + return result; + } + } + } +} + + +``` + +## AuthorNode.52INnvSU4xks9KALUdsc2g.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace.Types.Nodes +{ + public static partial class AuthorNode + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.Types.Nodes.AuthorNode); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetAuthorsAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthorsAsync(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorsAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorsAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = context.Parent(); + var args1_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args1_first = context.ArgumentValue("first"); + var args1_after = context.ArgumentValue("after"); + int? args1_last = null; + string? args1_before = null; + bool args1_includeTotalCount = false; + + if(args1_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args1_last = context.ArgumentValue("last"); + args1_before = context.ArgumentValue("before"); + } + + if(args1_first is null && args1_last is null) + { + args1_first = args1_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args1_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args1_includeTotalCount = context.IsSelected("totalCount"); + } + + var args1 = new global::GreenDonut.Data.PagingArguments( + args1_first, + args1_after, + args1_last, + args1_before, + args1_includeTotalCount) + { + EnableRelativeCursors = args1_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args2 = context.RequestAborted; + var result = await global::TestNamespace.Types.Nodes.AuthorNode.GetAuthorsAsync(args0, args1, args2); + return result; + } + } + } +} + + +``` + +## AuthorQueries.oWmb2tjg9NMLpwHX801V4w.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace.Types.Root +{ + public static partial class AuthorQueries + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.Types.Root.AuthorQueries); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetAuthorsAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthorsAsync(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("GetAuthors2Async", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthors2Async(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorsAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorsAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_first = context.ArgumentValue("first"); + var args0_after = context.ArgumentValue("after"); + int? args0_last = null; + string? args0_before = null; + bool args0_includeTotalCount = false; + + if(args0_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args0_last = context.ArgumentValue("last"); + args0_before = context.ArgumentValue("before"); + } + + if(args0_first is null && args0_last is null) + { + args0_first = args0_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args0_includeTotalCount = context.IsSelected("totalCount"); + } + + var args0 = new global::GreenDonut.Data.PagingArguments( + args0_first, + args0_after, + args0_last, + args0_before, + args0_includeTotalCount) + { + EnableRelativeCursors = args0_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args1 = context.RequestAborted; + var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1); + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthors2Async() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthors2Async); + + private async global::System.Threading.Tasks.ValueTask GetAuthors2Async(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_first = context.ArgumentValue("first"); + var args0_after = context.ArgumentValue("after"); + int? args0_last = null; + string? args0_before = null; + bool args0_includeTotalCount = false; + + if(args0_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args0_last = context.ArgumentValue("last"); + args0_before = context.ArgumentValue("before"); + } + + if(args0_first is null && args0_last is null) + { + args0_first = args0_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args0_includeTotalCount = context.IsSelected("totalCount"); + } + + var args0 = new global::GreenDonut.Data.PagingArguments( + args0_first, + args0_after, + args0_last, + args0_before, + args0_includeTotalCount) + { + EnableRelativeCursors = args0_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args1 = context.RequestAborted; + var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthors2Async(args0, args1); + return result; + } + } + } +} + + +``` + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.AddType(); + builder.AddType(); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Types.Nodes.AuthorNode", + () => global::TestNamespace.Types.Nodes.AuthorNode.Initialize)); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Types.Root.AuthorQueries", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.Types.Root.AuthorQueries.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + builder.AddType>(); + return builder; + } + } +} + +``` + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_WithConnectionName_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_WithConnectionName_MatchesSnapshot.md new file mode 100644 index 00000000000..a8a787585e5 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_WithConnectionName_MatchesSnapshot.md @@ -0,0 +1,677 @@ +# GenerateSource_GenericCustomConnection_WithConnectionName_MatchesSnapshot + +## AuthorCustomConnectionType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorCustomConnectionType : ObjectType> + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor> descriptor) + { + var thisType = typeof(global::TestNamespace.CustomConnection); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + var nodeTypeRef = extend.Context.TypeInspector.GetTypeRef(typeof(global::TestNamespace.Author)); + descriptor + .Name(t => string.Format("{0}CustomConnection", t.Name)) + .DependsOn(nodeTypeRef); + + descriptor + .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .Type>>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionEdgesFieldFlags(); + c.Definition.Resolvers = r.Edges(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Nodes", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionNodesFieldFlags(); + c.Definition.Resolvers = r.Nodes(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("PageInfo", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.PageInfo(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("TotalCount", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionTotalCountFieldFlags(); + c.Definition.Resolvers = r.TotalCount(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Edges() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Edges); + + private global::System.Object? Edges(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Edges; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Nodes() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Nodes); + + private global::System.Object? Nodes(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Nodes; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates PageInfo() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: PageInfo); + + private global::System.Object? PageInfo(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().PageInfo; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates TotalCount() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TotalCount); + + private global::System.Object? TotalCount(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().TotalCount; + return result; + } + } + } +} + + +``` + +## AuthorCustomEdgeType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorCustomEdgeType : ObjectType> + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor> descriptor) + { + var thisType = typeof(global::TestNamespace.CustomEdge); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + var nodeTypeRef = extend.Context.TypeInspector.GetTypeRef(typeof(global::TestNamespace.Author)); + descriptor + .Name(t => string.Format("{0}CustomEdge", t.Name)) + .DependsOn(nodeTypeRef); + + descriptor + .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Node(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Cursor", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Cursor(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Node() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Node); + + private global::System.Object? Node(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Node; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Cursor() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Cursor); + + private global::System.Object? Cursor(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Cursor; + return result; + } + } + } +} + + +``` + +## AuthorNode.52INnvSU4xks9KALUdsc2g.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace.Types.Nodes +{ + public static partial class AuthorNode + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.Types.Nodes.AuthorNode); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetAuthorsAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthorsAsync(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorsAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorsAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = context.Parent(); + var args1_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args1_first = context.ArgumentValue("first"); + var args1_after = context.ArgumentValue("after"); + int? args1_last = null; + string? args1_before = null; + bool args1_includeTotalCount = false; + + if(args1_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args1_last = context.ArgumentValue("last"); + args1_before = context.ArgumentValue("before"); + } + + if(args1_first is null && args1_last is null) + { + args1_first = args1_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args1_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args1_includeTotalCount = context.IsSelected("totalCount"); + } + + var args1 = new global::GreenDonut.Data.PagingArguments( + args1_first, + args1_after, + args1_last, + args1_before, + args1_includeTotalCount) + { + EnableRelativeCursors = args1_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args2 = context.RequestAborted; + var result = await global::TestNamespace.Types.Nodes.AuthorNode.GetAuthorsAsync(args0, args1, args2); + return result; + } + } + } +} + + +``` + +## AuthorQueries.oWmb2tjg9NMLpwHX801V4w.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace.Types.Root +{ + public static partial class AuthorQueries + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.Types.Root.AuthorQueries); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetAuthorsAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthorsAsync(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("GetAuthors2Async", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthors2Async(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorsAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorsAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_first = context.ArgumentValue("first"); + var args0_after = context.ArgumentValue("after"); + int? args0_last = null; + string? args0_before = null; + bool args0_includeTotalCount = false; + + if(args0_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args0_last = context.ArgumentValue("last"); + args0_before = context.ArgumentValue("before"); + } + + if(args0_first is null && args0_last is null) + { + args0_first = args0_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args0_includeTotalCount = context.IsSelected("totalCount"); + } + + var args0 = new global::GreenDonut.Data.PagingArguments( + args0_first, + args0_after, + args0_last, + args0_before, + args0_includeTotalCount) + { + EnableRelativeCursors = args0_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args1 = context.RequestAborted; + var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1); + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthors2Async() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthors2Async); + + private async global::System.Threading.Tasks.ValueTask GetAuthors2Async(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_first = context.ArgumentValue("first"); + var args0_after = context.ArgumentValue("after"); + int? args0_last = null; + string? args0_before = null; + bool args0_includeTotalCount = false; + + if(args0_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args0_last = context.ArgumentValue("last"); + args0_before = context.ArgumentValue("before"); + } + + if(args0_first is null && args0_last is null) + { + args0_first = args0_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args0_includeTotalCount = context.IsSelected("totalCount"); + } + + var args0 = new global::GreenDonut.Data.PagingArguments( + args0_first, + args0_after, + args0_last, + args0_before, + args0_includeTotalCount) + { + EnableRelativeCursors = args0_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args1 = context.RequestAborted; + var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthors2Async(args0, args1); + return result; + } + } + } +} + + +``` + +## Authors2ConnectionType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class Authors2ConnectionType : ObjectType> + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor> descriptor) + { + var thisType = typeof(global::TestNamespace.CustomConnection); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .Type>>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionEdgesFieldFlags(); + c.Definition.Resolvers = r.Edges(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Nodes", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionNodesFieldFlags(); + c.Definition.Resolvers = r.Nodes(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("PageInfo", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.PageInfo(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("TotalCount", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionTotalCountFieldFlags(); + c.Definition.Resolvers = r.TotalCount(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Edges() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Edges); + + private global::System.Object? Edges(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Edges; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Nodes() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Nodes); + + private global::System.Object? Nodes(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Nodes; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates PageInfo() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: PageInfo); + + private global::System.Object? PageInfo(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().PageInfo; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates TotalCount() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TotalCount); + + private global::System.Object? TotalCount(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().TotalCount; + return result; + } + } + } +} + + +``` + +## Authors2EdgeType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class Authors2EdgeType : ObjectType> + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor> descriptor) + { + var thisType = typeof(global::TestNamespace.CustomEdge); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Node(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Cursor", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Cursor(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Node() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Node); + + private global::System.Object? Node(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Node; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Cursor() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Cursor); + + private global::System.Object? Cursor(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Cursor; + return result; + } + } + } +} + + +``` + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.AddType(); + builder.AddType(); + builder.AddType(); + builder.AddType(); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Types.Nodes.AuthorNode", + () => global::TestNamespace.Types.Nodes.AuthorNode.Initialize)); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Types.Root.AuthorQueries", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.Types.Root.AuthorQueries.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + builder.AddType>(); + return builder; + } + } +} + +``` + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Ensure_Entity_Becomes_Node.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Ensure_Entity_Becomes_Node.md index ec5f24e31ae..8dc51f55f3e 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Ensure_Entity_Becomes_Node.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Ensure_Entity_Becomes_Node.md @@ -1,156 +1,5 @@ # Ensure_Entity_Becomes_Node -## HotChocolateResolvers.735550c.g.cs - -```csharp -// - -#nullable enable -#pragma warning disable - -using System; -using System.Runtime.CompilerServices; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Execution.Configuration; -using HotChocolate.Internal; - -namespace TestNamespace -{ - internal static class QueryResolvers - { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - private readonly static global::HotChocolate.Internal.IParameterBinding[] _args_Query_GetTestById = new global::HotChocolate.Internal.IParameterBinding[1]; - - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) - { - if (!_bindingsInitialized) - { - lock (_sync) - { - if (!_bindingsInitialized) - { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var type = typeof(global::TestNamespace.Query); - global::System.Reflection.MethodInfo resolver = default!; - global::System.Reflection.ParameterInfo[] parameters = default!; - _bindingsInitialized = true; - - resolver = type.GetMethod( - "GetTestById", - bindingFlags, - new global::System.Type[] - { - typeof(int) - })!; - parameters = resolver.GetParameters(); - _args_Query_GetTestById[0] = bindingResolver.GetBinding(parameters[0]); - } - } - } - } - - public static HotChocolate.Resolvers.FieldResolverDelegates Query_GetTestById() - { - if(!_bindingsInitialized) - { - throw new global::System.InvalidOperationException("The bindings must be initialized before the resolvers can be created."); - } - - return new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: Query_GetTestById_Resolver); - } - - private static async global::System.Threading.Tasks.ValueTask Query_GetTestById_Resolver(global::HotChocolate.Resolvers.IResolverContext context) - { - var args0 = _args_Query_GetTestById[0].Execute(context); - var result = await global::TestNamespace.Query.GetTestById(args0); - return result; - } - } - - internal static class TestTypeResolvers - { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - private readonly static global::HotChocolate.Internal.IParameterBinding[] _args_TestType_GetTest = new global::HotChocolate.Internal.IParameterBinding[1]; - - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) - { - if (!_bindingsInitialized) - { - lock (_sync) - { - if (!_bindingsInitialized) - { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var type = typeof(global::TestNamespace.TestType); - global::System.Reflection.MethodInfo resolver = default!; - global::System.Reflection.ParameterInfo[] parameters = default!; - _bindingsInitialized = true; - - resolver = type.GetMethod( - "GetTest", - bindingFlags, - new global::System.Type[] - { - typeof(int) - })!; - parameters = resolver.GetParameters(); - _args_TestType_GetTest[0] = bindingResolver.GetBinding(parameters[0]); - - int argumentCount = 0; - - foreach(var binding in _args_TestType_GetTest) - { - if(binding.Kind == global::HotChocolate.Internal.ArgumentKind.Argument) - { - argumentCount++; - } - } - - if(argumentCount > 1) - { - throw new global::HotChocolate.SchemaException( - global::HotChocolate.SchemaErrorBuilder.New() - .SetMessage("The node resolver `TestNamespace.TestType.GetTest` mustn't have more than one argument. Node resolvers can only have a single argument called `id`.") - .Build()); - } - } - } - } - } - - public static HotChocolate.Resolvers.FieldResolverDelegates TestType_GetTest() - { - if(!_bindingsInitialized) - { - throw new global::System.InvalidOperationException("The bindings must be initialized before the resolvers can be created."); - } - - return new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: TestType_GetTest_Resolver); - } - - private static async global::System.Threading.Tasks.ValueTask TestType_GetTest_Resolver(global::HotChocolate.Resolvers.IResolverContext context) - { - var args0 = context.GetLocalState(global::HotChocolate.WellKnownContextData.InternalId); - var result = await global::TestNamespace.TestType.GetTest(args0); - return result; - } - } -} - - -``` - ## HotChocolateTypeModule.735550c.g.cs ```csharp @@ -191,7 +40,7 @@ namespace Microsoft.Extensions.DependencyInjection ``` -## HotChocolateTypes.735550c.g.cs +## Query.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -205,58 +54,118 @@ using HotChocolate; using HotChocolate.Types; using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; namespace TestNamespace { -public static partial class Query -{ - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + internal static partial class Query { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var thisType = typeof(global::TestNamespace.Query); - var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; - global::TestNamespace.QueryResolvers.InitializeBindings(bindingResolver); - - descriptor - .Field(thisType.GetMember("GetTestById", bindingFlags)[0]) - .ExtendWith(c => + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.Query); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(bindingResolver); + + descriptor + .Field(thisType.GetMember("GetTestById", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.GetTestById(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + private readonly global::HotChocolate.Internal.IParameterBinding[] _args_GetTestById = new global::HotChocolate.Internal.IParameterBinding[1]; + + public __Resolvers(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) { - c.Definition.SetSourceGeneratorFlags(); - c.Definition.Resolvers = global::TestNamespace.QueryResolvers.Query_GetTestById(); - }); + var type = typeof(global::TestNamespace.Query); + global::System.Reflection.MethodInfo resolver = default!; + global::System.Reflection.ParameterInfo[] parameters = default!; + + resolver = type.GetMethod( + "GetTestById", + global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags, + new global::System.Type[] + { + typeof(int) + })!; - Configure(descriptor); - } + parameters = resolver.GetParameters(); + _args_GetTestById[0] = bindingResolver.GetBinding(parameters[0]); + } + + public HotChocolate.Resolvers.FieldResolverDelegates GetTestById() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetTestById); - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + private async global::System.Threading.Tasks.ValueTask GetTestById(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = _args_GetTestById[0].Execute(context); + var result = await global::TestNamespace.Query.GetTestById(args0); + return result; + } + } + } } -public static partial class TestType + +``` + +## TestType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace { - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + internal static partial class TestType { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.TestType); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); - var thisType = typeof(global::TestNamespace.TestType); - var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; - global::TestNamespace.TestTypeResolvers.InitializeBindings(bindingResolver); + descriptor + .ImplementsNode() + .ResolveNode(resolvers.GetTest().Resolver!); - descriptor - .ImplementsNode() - .ResolveNode(global::TestNamespace.TestTypeResolvers.TestType_GetTest().Resolver!); + Configure(descriptor); + } - Configure(descriptor); - } + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); -} + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetTest() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetTest); + + private async global::System.Threading.Tasks.ValueTask GetTest(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = context.GetLocalState(global::HotChocolate.WellKnownContextData.InternalId); + var result = await global::TestNamespace.TestType.GetTest(args0); + return result; + } + } + } } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Ensure_Entity_Becomes_Node_With_Query_Node_Resolver.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Ensure_Entity_Becomes_Node_With_Query_Node_Resolver.md index 3a56b157f7a..03081b5d3aa 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Ensure_Entity_Becomes_Node_With_Query_Node_Resolver.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Ensure_Entity_Becomes_Node_With_Query_Node_Resolver.md @@ -1,6 +1,6 @@ # Ensure_Entity_Becomes_Node_With_Query_Node_Resolver -## HotChocolateResolvers.735550c.g.cs +## HotChocolateTypeModule.735550c.g.cs ```csharp // @@ -13,80 +13,34 @@ using System.Runtime.CompilerServices; using HotChocolate; using HotChocolate.Types; using HotChocolate.Execution.Configuration; -using HotChocolate.Internal; -namespace TestNamespace +namespace Microsoft.Extensions.DependencyInjection { - internal static class QueryResolvers - { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) - { - } - } - - internal static class TestTypeResolvers + public static partial class TestsTypesRequestExecutorBuilderExtensions { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - private readonly static global::HotChocolate.Internal.IParameterBinding[] _args_TestType_GetTest = new global::HotChocolate.Internal.IParameterBinding[1]; - - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) - { - if (!_bindingsInitialized) - { - lock (_sync) - { - if (!_bindingsInitialized) - { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var type = typeof(global::TestNamespace.TestType); - global::System.Reflection.MethodInfo resolver = default!; - global::System.Reflection.ParameterInfo[] parameters = default!; - _bindingsInitialized = true; - - resolver = type.GetMethod( - "GetTest", - bindingFlags, - new global::System.Type[] - { - typeof(int) - })!; - parameters = resolver.GetParameters(); - _args_TestType_GetTest[0] = bindingResolver.GetBinding(parameters[0]); - } - } - } - } - - public static HotChocolate.Resolvers.FieldResolverDelegates TestType_GetTest() - { - if(!_bindingsInitialized) - { - throw new global::System.InvalidOperationException("The bindings must be initialized before the resolvers can be created."); - } - - return new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: TestType_GetTest_Resolver); - } - - private static async global::System.Threading.Tasks.ValueTask TestType_GetTest_Resolver(global::HotChocolate.Resolvers.IResolverContext context) + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) { - var args0 = _args_TestType_GetTest[0].Execute(context); - var result = await global::TestNamespace.TestType.GetTest(args0); - return result; + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Query", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.Query.Initialize)); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.TestType", + () => global::TestNamespace.TestType.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + builder.AddType>(); + return builder; } } } - ``` -## HotChocolateTypeModule.735550c.g.cs +## Query.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -99,34 +53,72 @@ using System.Runtime.CompilerServices; using HotChocolate; using HotChocolate.Types; using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; -namespace Microsoft.Extensions.DependencyInjection +namespace TestNamespace { - public static partial class TestsTypesRequestExecutorBuilderExtensions + internal static partial class Query { - public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) { - builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( - "Tests::TestNamespace.Query", - global::HotChocolate.Types.OperationTypeNames.Query, - () => global::TestNamespace.Query.Initialize)); - builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( - "Tests::TestNamespace.TestType", - () => global::TestNamespace.TestType.Initialize)); - builder.ConfigureSchema( - b => b.TryAddRootType( - () => new global::HotChocolate.Types.ObjectType( - d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), - HotChocolate.Language.OperationType.Query)); - builder.AddType>(); - return builder; + var thisType = typeof(global::TestNamespace.Query); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(bindingResolver); + + descriptor + .Field(thisType.GetMember("GetTestById", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.GetTestById(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + private readonly global::HotChocolate.Internal.IParameterBinding[] _args_GetTestById = new global::HotChocolate.Internal.IParameterBinding[1]; + + public __Resolvers(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) + { + var type = typeof(global::TestNamespace.Query); + global::System.Reflection.MethodInfo resolver = default!; + global::System.Reflection.ParameterInfo[] parameters = default!; + + resolver = type.GetMethod( + "GetTestById", + global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags, + new global::System.Type[] + { + typeof(int) + })!; + + parameters = resolver.GetParameters(); + _args_GetTestById[0] = bindingResolver.GetBinding(parameters[0]); + } + + public HotChocolate.Resolvers.FieldResolverDelegates GetTestById() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetTestById); + + private async global::System.Threading.Tasks.ValueTask GetTestById(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = _args_GetTestById[0].Execute(context); + var result = await global::TestNamespace.Query.GetTestById(args0); + return result; + } } } } + ``` -## HotChocolateTypes.735550c.g.cs +## TestType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -140,46 +132,65 @@ using HotChocolate; using HotChocolate.Types; using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; namespace TestNamespace { -public static partial class Query -{ - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + internal static partial class TestType { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.TestType); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(bindingResolver); - Configure(descriptor); - } + descriptor + .Field(thisType.GetMember("GetTest", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.GetTest(); + }, + resolvers); - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); -} + Configure(descriptor); + } -public static partial class TestType -{ - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) - { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var thisType = typeof(global::TestNamespace.TestType); - var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; - global::TestNamespace.TestTypeResolvers.InitializeBindings(bindingResolver); - - descriptor - .Field(thisType.GetMember("GetTest", bindingFlags)[0]) - .ExtendWith(c => + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + private readonly global::HotChocolate.Internal.IParameterBinding[] _args_GetTest = new global::HotChocolate.Internal.IParameterBinding[1]; + + public __Resolvers(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) { - c.Definition.SetSourceGeneratorFlags(); - c.Definition.Resolvers = global::TestNamespace.TestTypeResolvers.TestType_GetTest(); - }); + var type = typeof(TestNamespace.TestType); + global::System.Reflection.MethodInfo resolver = default!; + global::System.Reflection.ParameterInfo[] parameters = default!; + + resolver = type.GetMethod( + "GetTest", + global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags, + new global::System.Type[] + { + typeof(int) + })!; - Configure(descriptor); - } + parameters = resolver.GetParameters(); + _args_GetTest[0] = bindingResolver.GetBinding(parameters[0]); + } - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); -} + public HotChocolate.Resolvers.FieldResolverDelegates GetTest() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetTest); + + private async global::System.Threading.Tasks.ValueTask GetTest(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = _args_GetTest[0].Execute(context); + var result = await global::TestNamespace.TestType.GetTest(args0); + return result; + } + } + } } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithGlobalStateArgument_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithGlobalStateArgument_MatchesSnapshot.md index 1c04449da6f..71bdbc0ce5c 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithGlobalStateArgument_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithGlobalStateArgument_MatchesSnapshot.md @@ -1,81 +1,5 @@ # GenerateSource_ResolverWithGlobalStateArgument_MatchesSnapshot -## HotChocolateResolvers.735550c.g.cs - -```csharp -// - -#nullable enable -#pragma warning disable - -using System; -using System.Runtime.CompilerServices; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Execution.Configuration; -using HotChocolate.Internal; - -namespace TestNamespace -{ - internal static class TestTypeResolvers - { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - private readonly static global::HotChocolate.Internal.IParameterBinding[] _args_TestType_GetTest = new global::HotChocolate.Internal.IParameterBinding[1]; - - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) - { - if (!_bindingsInitialized) - { - lock (_sync) - { - if (!_bindingsInitialized) - { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var type = typeof(global::TestNamespace.TestType); - global::System.Reflection.MethodInfo resolver = default!; - global::System.Reflection.ParameterInfo[] parameters = default!; - _bindingsInitialized = true; - - resolver = type.GetMethod( - "GetTest", - bindingFlags, - new global::System.Type[] - { - typeof(int) - })!; - parameters = resolver.GetParameters(); - _args_TestType_GetTest[0] = bindingResolver.GetBinding(parameters[0]); - } - } - } - } - - public static HotChocolate.Resolvers.FieldResolverDelegates TestType_GetTest() - { - if(!_bindingsInitialized) - { - throw new global::System.InvalidOperationException("The bindings must be initialized before the resolvers can be created."); - } - return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TestType_GetTest_Resolver); - } - - private static global::System.Object? TestType_GetTest_Resolver(global::HotChocolate.Resolvers.IResolverContext context) - { - var args0 = context.GetGlobalState("Test"); - var result = global::TestNamespace.TestType.GetTest(args0); - return result; - } - } -} - - -``` - ## HotChocolateTypeModule.735550c.g.cs ```csharp @@ -107,7 +31,7 @@ namespace Microsoft.Extensions.DependencyInjection ``` -## HotChocolateTypes.735550c.g.cs +## TestType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -121,35 +45,47 @@ using HotChocolate; using HotChocolate.Types; using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; namespace TestNamespace { -public static partial class TestType -{ - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + internal static partial class TestType { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var thisType = typeof(global::TestNamespace.TestType); - var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; - global::TestNamespace.TestTypeResolvers.InitializeBindings(bindingResolver); - - descriptor - .Field(thisType.GetMember("GetTest", bindingFlags)[0]) - .ExtendWith(c => + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.TestType); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetTest", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.GetTest(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetTest() { - c.Definition.SetSourceGeneratorFlags(); - c.Definition.Resolvers = global::TestNamespace.TestTypeResolvers.TestType_GetTest(); - }); + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: GetTest); + } - Configure(descriptor); + private global::System.Object? GetTest(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = context.GetGlobalState("Test"); + var result = global::TestNamespace.TestType.GetTest(args0); + return result; + } + } } - - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); -} } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithGlobalStateSetStateArgument_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithGlobalStateSetStateArgument_MatchesSnapshot.md index 2f1b6616e39..4a244df69c5 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithGlobalStateSetStateArgument_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithGlobalStateSetStateArgument_MatchesSnapshot.md @@ -1,81 +1,5 @@ # GenerateSource_ResolverWithGlobalStateSetStateArgument_MatchesSnapshot -## HotChocolateResolvers.735550c.g.cs - -```csharp -// - -#nullable enable -#pragma warning disable - -using System; -using System.Runtime.CompilerServices; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Execution.Configuration; -using HotChocolate.Internal; - -namespace TestNamespace -{ - internal static class TestTypeResolvers - { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - private readonly static global::HotChocolate.Internal.IParameterBinding[] _args_TestType_GetTest = new global::HotChocolate.Internal.IParameterBinding[1]; - - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) - { - if (!_bindingsInitialized) - { - lock (_sync) - { - if (!_bindingsInitialized) - { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var type = typeof(global::TestNamespace.TestType); - global::System.Reflection.MethodInfo resolver = default!; - global::System.Reflection.ParameterInfo[] parameters = default!; - _bindingsInitialized = true; - - resolver = type.GetMethod( - "GetTest", - bindingFlags, - new global::System.Type[] - { - typeof(global::HotChocolate.SetState) - })!; - parameters = resolver.GetParameters(); - _args_TestType_GetTest[0] = bindingResolver.GetBinding(parameters[0]); - } - } - } - } - - public static HotChocolate.Resolvers.FieldResolverDelegates TestType_GetTest() - { - if(!_bindingsInitialized) - { - throw new global::System.InvalidOperationException("The bindings must be initialized before the resolvers can be created."); - } - return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TestType_GetTest_Resolver); - } - - private static global::System.Object? TestType_GetTest_Resolver(global::HotChocolate.Resolvers.IResolverContext context) - { - var args0 = new HotChocolate.SetState(value => context.SetGlobalState("test", value)); - var result = global::TestNamespace.TestType.GetTest(args0); - return result; - } - } -} - - -``` - ## HotChocolateTypeModule.735550c.g.cs ```csharp @@ -107,7 +31,7 @@ namespace Microsoft.Extensions.DependencyInjection ``` -## HotChocolateTypes.735550c.g.cs +## TestType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -121,35 +45,47 @@ using HotChocolate; using HotChocolate.Types; using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; namespace TestNamespace { -public static partial class TestType -{ - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + internal static partial class TestType { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var thisType = typeof(global::TestNamespace.TestType); - var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; - global::TestNamespace.TestTypeResolvers.InitializeBindings(bindingResolver); - - descriptor - .Field(thisType.GetMember("GetTest", bindingFlags)[0]) - .ExtendWith(c => + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.TestType); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetTest", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.GetTest(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetTest() { - c.Definition.SetSourceGeneratorFlags(); - c.Definition.Resolvers = global::TestNamespace.TestTypeResolvers.TestType_GetTest(); - }); + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: GetTest); + } - Configure(descriptor); + private global::System.Object? GetTest(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = new HotChocolate.SetState(value => context.SetGlobalState("test", value)); + var result = global::TestNamespace.TestType.GetTest(args0); + return result; + } + } } - - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); -} } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithLocalStateArgument_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithLocalStateArgument_MatchesSnapshot.md index 2346be1e338..8cac0a5570f 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithLocalStateArgument_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithLocalStateArgument_MatchesSnapshot.md @@ -1,81 +1,5 @@ # GenerateSource_ResolverWithLocalStateArgument_MatchesSnapshot -## HotChocolateResolvers.735550c.g.cs - -```csharp -// - -#nullable enable -#pragma warning disable - -using System; -using System.Runtime.CompilerServices; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Execution.Configuration; -using HotChocolate.Internal; - -namespace TestNamespace -{ - internal static class TestTypeResolvers - { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - private readonly static global::HotChocolate.Internal.IParameterBinding[] _args_TestType_GetTest = new global::HotChocolate.Internal.IParameterBinding[1]; - - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) - { - if (!_bindingsInitialized) - { - lock (_sync) - { - if (!_bindingsInitialized) - { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var type = typeof(global::TestNamespace.TestType); - global::System.Reflection.MethodInfo resolver = default!; - global::System.Reflection.ParameterInfo[] parameters = default!; - _bindingsInitialized = true; - - resolver = type.GetMethod( - "GetTest", - bindingFlags, - new global::System.Type[] - { - typeof(int) - })!; - parameters = resolver.GetParameters(); - _args_TestType_GetTest[0] = bindingResolver.GetBinding(parameters[0]); - } - } - } - } - - public static HotChocolate.Resolvers.FieldResolverDelegates TestType_GetTest() - { - if(!_bindingsInitialized) - { - throw new global::System.InvalidOperationException("The bindings must be initialized before the resolvers can be created."); - } - return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TestType_GetTest_Resolver); - } - - private static global::System.Object? TestType_GetTest_Resolver(global::HotChocolate.Resolvers.IResolverContext context) - { - var args0 = context.GetLocalState("Test"); - var result = global::TestNamespace.TestType.GetTest(args0); - return result; - } - } -} - - -``` - ## HotChocolateTypeModule.735550c.g.cs ```csharp @@ -107,7 +31,7 @@ namespace Microsoft.Extensions.DependencyInjection ``` -## HotChocolateTypes.735550c.g.cs +## TestType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -121,35 +45,47 @@ using HotChocolate; using HotChocolate.Types; using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; namespace TestNamespace { -public static partial class TestType -{ - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + internal static partial class TestType { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var thisType = typeof(global::TestNamespace.TestType); - var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; - global::TestNamespace.TestTypeResolvers.InitializeBindings(bindingResolver); - - descriptor - .Field(thisType.GetMember("GetTest", bindingFlags)[0]) - .ExtendWith(c => + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.TestType); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetTest", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.GetTest(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetTest() { - c.Definition.SetSourceGeneratorFlags(); - c.Definition.Resolvers = global::TestNamespace.TestTypeResolvers.TestType_GetTest(); - }); + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: GetTest); + } - Configure(descriptor); + private global::System.Object? GetTest(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = context.GetLocalState("Test"); + var result = global::TestNamespace.TestType.GetTest(args0); + return result; + } + } } - - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); -} } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithLocalStateSetStateArgument_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithLocalStateSetStateArgument_MatchesSnapshot.md index 9a749f5409c..614ebda5e53 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithLocalStateSetStateArgument_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithLocalStateSetStateArgument_MatchesSnapshot.md @@ -1,81 +1,5 @@ # GenerateSource_ResolverWithLocalStateSetStateArgument_MatchesSnapshot -## HotChocolateResolvers.735550c.g.cs - -```csharp -// - -#nullable enable -#pragma warning disable - -using System; -using System.Runtime.CompilerServices; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Execution.Configuration; -using HotChocolate.Internal; - -namespace TestNamespace -{ - internal static class TestTypeResolvers - { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - private readonly static global::HotChocolate.Internal.IParameterBinding[] _args_TestType_GetTest = new global::HotChocolate.Internal.IParameterBinding[1]; - - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) - { - if (!_bindingsInitialized) - { - lock (_sync) - { - if (!_bindingsInitialized) - { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var type = typeof(global::TestNamespace.TestType); - global::System.Reflection.MethodInfo resolver = default!; - global::System.Reflection.ParameterInfo[] parameters = default!; - _bindingsInitialized = true; - - resolver = type.GetMethod( - "GetTest", - bindingFlags, - new global::System.Type[] - { - typeof(global::HotChocolate.SetState) - })!; - parameters = resolver.GetParameters(); - _args_TestType_GetTest[0] = bindingResolver.GetBinding(parameters[0]); - } - } - } - } - - public static HotChocolate.Resolvers.FieldResolverDelegates TestType_GetTest() - { - if(!_bindingsInitialized) - { - throw new global::System.InvalidOperationException("The bindings must be initialized before the resolvers can be created."); - } - return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TestType_GetTest_Resolver); - } - - private static global::System.Object? TestType_GetTest_Resolver(global::HotChocolate.Resolvers.IResolverContext context) - { - var args0 = new HotChocolate.SetState(value => context.SetLocalState("test", value)); - var result = global::TestNamespace.TestType.GetTest(args0); - return result; - } - } -} - - -``` - ## HotChocolateTypeModule.735550c.g.cs ```csharp @@ -107,7 +31,7 @@ namespace Microsoft.Extensions.DependencyInjection ``` -## HotChocolateTypes.735550c.g.cs +## TestType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -121,35 +45,47 @@ using HotChocolate; using HotChocolate.Types; using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; namespace TestNamespace { -public static partial class TestType -{ - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + internal static partial class TestType { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var thisType = typeof(global::TestNamespace.TestType); - var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; - global::TestNamespace.TestTypeResolvers.InitializeBindings(bindingResolver); - - descriptor - .Field(thisType.GetMember("GetTest", bindingFlags)[0]) - .ExtendWith(c => + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.TestType); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetTest", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.GetTest(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetTest() { - c.Definition.SetSourceGeneratorFlags(); - c.Definition.Resolvers = global::TestNamespace.TestTypeResolvers.TestType_GetTest(); - }); + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: GetTest); + } - Configure(descriptor); + private global::System.Object? GetTest(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = new HotChocolate.SetState(value => context.SetLocalState("test", value)); + var result = global::TestNamespace.TestType.GetTest(args0); + return result; + } + } } - - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); -} } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithScopedStateArgument_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithScopedStateArgument_MatchesSnapshot.md index ad580dd73df..5ab828ccebd 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithScopedStateArgument_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithScopedStateArgument_MatchesSnapshot.md @@ -1,81 +1,5 @@ # GenerateSource_ResolverWithScopedStateArgument_MatchesSnapshot -## HotChocolateResolvers.735550c.g.cs - -```csharp -// - -#nullable enable -#pragma warning disable - -using System; -using System.Runtime.CompilerServices; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Execution.Configuration; -using HotChocolate.Internal; - -namespace TestNamespace -{ - internal static class TestTypeResolvers - { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - private readonly static global::HotChocolate.Internal.IParameterBinding[] _args_TestType_GetTest = new global::HotChocolate.Internal.IParameterBinding[1]; - - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) - { - if (!_bindingsInitialized) - { - lock (_sync) - { - if (!_bindingsInitialized) - { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var type = typeof(global::TestNamespace.TestType); - global::System.Reflection.MethodInfo resolver = default!; - global::System.Reflection.ParameterInfo[] parameters = default!; - _bindingsInitialized = true; - - resolver = type.GetMethod( - "GetTest", - bindingFlags, - new global::System.Type[] - { - typeof(int) - })!; - parameters = resolver.GetParameters(); - _args_TestType_GetTest[0] = bindingResolver.GetBinding(parameters[0]); - } - } - } - } - - public static HotChocolate.Resolvers.FieldResolverDelegates TestType_GetTest() - { - if(!_bindingsInitialized) - { - throw new global::System.InvalidOperationException("The bindings must be initialized before the resolvers can be created."); - } - return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TestType_GetTest_Resolver); - } - - private static global::System.Object? TestType_GetTest_Resolver(global::HotChocolate.Resolvers.IResolverContext context) - { - var args0 = context.GetScopedState("Test"); - var result = global::TestNamespace.TestType.GetTest(args0); - return result; - } - } -} - - -``` - ## HotChocolateTypeModule.735550c.g.cs ```csharp @@ -107,7 +31,7 @@ namespace Microsoft.Extensions.DependencyInjection ``` -## HotChocolateTypes.735550c.g.cs +## TestType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -121,35 +45,47 @@ using HotChocolate; using HotChocolate.Types; using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; namespace TestNamespace { -public static partial class TestType -{ - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + internal static partial class TestType { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var thisType = typeof(global::TestNamespace.TestType); - var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; - global::TestNamespace.TestTypeResolvers.InitializeBindings(bindingResolver); - - descriptor - .Field(thisType.GetMember("GetTest", bindingFlags)[0]) - .ExtendWith(c => + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.TestType); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetTest", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.GetTest(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetTest() { - c.Definition.SetSourceGeneratorFlags(); - c.Definition.Resolvers = global::TestNamespace.TestTypeResolvers.TestType_GetTest(); - }); + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: GetTest); + } - Configure(descriptor); + private global::System.Object? GetTest(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = context.GetScopedState("Test"); + var result = global::TestNamespace.TestType.GetTest(args0); + return result; + } + } } - - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); -} } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithScopedStateSetStateArgument_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithScopedStateSetStateArgument_MatchesSnapshot.md index 5ae17efabcd..b1e72b6ddc2 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithScopedStateSetStateArgument_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.GenerateSource_ResolverWithScopedStateSetStateArgument_MatchesSnapshot.md @@ -1,81 +1,5 @@ # GenerateSource_ResolverWithScopedStateSetStateArgument_MatchesSnapshot -## HotChocolateResolvers.735550c.g.cs - -```csharp -// - -#nullable enable -#pragma warning disable - -using System; -using System.Runtime.CompilerServices; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Execution.Configuration; -using HotChocolate.Internal; - -namespace TestNamespace -{ - internal static class TestTypeResolvers - { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - private readonly static global::HotChocolate.Internal.IParameterBinding[] _args_TestType_GetTest = new global::HotChocolate.Internal.IParameterBinding[1]; - - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) - { - if (!_bindingsInitialized) - { - lock (_sync) - { - if (!_bindingsInitialized) - { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var type = typeof(global::TestNamespace.TestType); - global::System.Reflection.MethodInfo resolver = default!; - global::System.Reflection.ParameterInfo[] parameters = default!; - _bindingsInitialized = true; - - resolver = type.GetMethod( - "GetTest", - bindingFlags, - new global::System.Type[] - { - typeof(global::HotChocolate.SetState) - })!; - parameters = resolver.GetParameters(); - _args_TestType_GetTest[0] = bindingResolver.GetBinding(parameters[0]); - } - } - } - } - - public static HotChocolate.Resolvers.FieldResolverDelegates TestType_GetTest() - { - if(!_bindingsInitialized) - { - throw new global::System.InvalidOperationException("The bindings must be initialized before the resolvers can be created."); - } - return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TestType_GetTest_Resolver); - } - - private static global::System.Object? TestType_GetTest_Resolver(global::HotChocolate.Resolvers.IResolverContext context) - { - var args0 = new HotChocolate.SetState(value => context.SetScopedState("test", value)); - var result = global::TestNamespace.TestType.GetTest(args0); - return result; - } - } -} - - -``` - ## HotChocolateTypeModule.735550c.g.cs ```csharp @@ -107,7 +31,7 @@ namespace Microsoft.Extensions.DependencyInjection ``` -## HotChocolateTypes.735550c.g.cs +## TestType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -121,35 +45,47 @@ using HotChocolate; using HotChocolate.Types; using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; namespace TestNamespace { -public static partial class TestType -{ - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + internal static partial class TestType { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var thisType = typeof(global::TestNamespace.TestType); - var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; - global::TestNamespace.TestTypeResolvers.InitializeBindings(bindingResolver); - - descriptor - .Field(thisType.GetMember("GetTest", bindingFlags)[0]) - .ExtendWith(c => + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.TestType); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetTest", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.GetTest(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetTest() { - c.Definition.SetSourceGeneratorFlags(); - c.Definition.Resolvers = global::TestNamespace.TestTypeResolvers.TestType_GetTest(); - }); + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: GetTest); + } - Configure(descriptor); + private global::System.Object? GetTest(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = new HotChocolate.SetState(value => context.SetScopedState("test", value)); + var result = global::TestNamespace.TestType.GetTest(args0); + return result; + } + } } - - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); -} } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Inject_QueryContext.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Inject_QueryContext.md index 2abaa569800..399ec60ffde 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Inject_QueryContext.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Inject_QueryContext.md @@ -1,88 +1,5 @@ # Inject_QueryContext -## HotChocolateResolvers.735550c.g.cs - -```csharp -// - -#nullable enable -#pragma warning disable - -using System; -using System.Runtime.CompilerServices; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Execution.Configuration; -using HotChocolate.Internal; - -namespace TestNamespace -{ - internal static class TestTypeResolvers - { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - private readonly static global::HotChocolate.Internal.IParameterBinding[] _args_TestType_GetTest = new global::HotChocolate.Internal.IParameterBinding[1]; - - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) - { - if (!_bindingsInitialized) - { - lock (_sync) - { - if (!_bindingsInitialized) - { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var type = typeof(global::TestNamespace.TestType); - global::System.Reflection.MethodInfo resolver = default!; - global::System.Reflection.ParameterInfo[] parameters = default!; - _bindingsInitialized = true; - - resolver = type.GetMethod( - "GetTest", - bindingFlags, - new global::System.Type[] - { - typeof(global::GreenDonut.Data.QueryContext) - })!; - parameters = resolver.GetParameters(); - _args_TestType_GetTest[0] = bindingResolver.GetBinding(parameters[0]); - } - } - } - } - - public static HotChocolate.Resolvers.FieldResolverDelegates TestType_GetTest() - { - if(!_bindingsInitialized) - { - throw new global::System.InvalidOperationException("The bindings must be initialized before the resolvers can be created."); - } - - return new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: TestType_GetTest_Resolver); - } - - private static global::System.Threading.Tasks.ValueTask TestType_GetTest_Resolver(global::HotChocolate.Resolvers.IResolverContext context) - { - var args0_selection = context.Selection; - var args0_filter = HotChocolate.Data.Filters.FilterContextResolverContextExtensions.GetFilterContext(context); - var args0_sorting = HotChocolate.Data.Sorting.SortingContextResolverContextExtensions.GetSortingContext(context); - var args0 = new global::GreenDonut.Data.QueryContext( - HotChocolate.Execution.Processing.HotChocolateExecutionSelectionExtensions.AsSelector(args0_selection), - args0_filter?.AsPredicate(), - args0_sorting?.AsSortDefinition()); - var result = global::TestNamespace.TestType.GetTest(args0); - return new global::System.Threading.Tasks.ValueTask(result); - } - } -} - - -``` - ## HotChocolateTypeModule.735550c.g.cs ```csharp @@ -114,7 +31,7 @@ namespace Microsoft.Extensions.DependencyInjection ``` -## HotChocolateTypes.735550c.g.cs +## TestType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -128,36 +45,52 @@ using HotChocolate; using HotChocolate.Types; using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; namespace TestNamespace { -public static partial class TestType -{ - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + internal static partial class TestType { - const global::System.Reflection.BindingFlags bindingFlags = - global::System.Reflection.BindingFlags.Public - | global::System.Reflection.BindingFlags.NonPublic - | global::System.Reflection.BindingFlags.Static; - - var thisType = typeof(global::TestNamespace.TestType); - var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; - global::TestNamespace.TestTypeResolvers.InitializeBindings(bindingResolver); - - descriptor - .Field(thisType.GetMember("GetTest", bindingFlags)[0]) - .ExtendWith(c => - { - c.Definition.SetSourceGeneratorFlags(); - c.Definition.Resolvers = global::TestNamespace.TestTypeResolvers.TestType_GetTest(); - c.Definition.ResultPostProcessor = global::HotChocolate.Execution.ListPostProcessor.Default; - }); + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.TestType); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); - Configure(descriptor); - } + descriptor + .Field(thisType.GetMember("GetTest", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.GetTest(); + c.Definition.ResultPostProcessor = global::HotChocolate.Execution.ListPostProcessor.Default; + }, + resolvers); - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); -} + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetTest() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetTest); + + private global::System.Threading.Tasks.ValueTask GetTest(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_selection = context.Selection; + var args0_filter = global::HotChocolate.Data.Filters.FilterContextResolverContextExtensions.GetFilterContext(context); + var args0_sorting = global::HotChocolate.Data.Sorting.SortingContextResolverContextExtensions.GetSortingContext(context); + var args0 = new global::GreenDonut.Data.QueryContext( + global::HotChocolate.Execution.Processing.HotChocolateExecutionSelectionExtensions.AsSelector(args0_selection), + args0_filter?.AsPredicate(), + args0_sorting?.AsSortDefinition()); + var result = global::TestNamespace.TestType.GetTest(args0); + return new global::System.Threading.Tasks.ValueTask(result); + } + } + } } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/TypeModuleSyntaxGeneratorTests.GenerateSource_TypeModuleOrdering_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/TypeModuleSyntaxGeneratorTests.GenerateSource_TypeModuleOrdering_MatchesSnapshot.md index 4fd58af2754..b767103ea02 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/TypeModuleSyntaxGeneratorTests.GenerateSource_TypeModuleOrdering_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/TypeModuleSyntaxGeneratorTests.GenerateSource_TypeModuleOrdering_MatchesSnapshot.md @@ -1,5 +1,79 @@ # GenerateSource_TypeModuleOrdering_MatchesSnapshot +## ATestAAttrType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + internal static partial class ATestAAttrType + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + } + } +} + + +``` + +## ATestBAttrType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + internal static partial class ATestBAttrType + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + } + } +} + + +``` + ## GreenDonutDataLoader.735550c.g.cs ```csharp @@ -119,45 +193,6 @@ namespace TestNamespace } -``` - -## HotChocolateResolvers.735550c.g.cs - -```csharp -// - -#nullable enable -#pragma warning disable - -using System; -using System.Runtime.CompilerServices; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Execution.Configuration; -using HotChocolate.Internal; - -namespace TestNamespace -{ - internal static class ATestBAttrTypeResolvers - { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) - { - } - } - - internal static class ATestAAttrTypeResolvers - { - private static readonly object _sync = new object(); - private static bool _bindingsInitialized; - public static void InitializeBindings(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) - { - } - } -} - - ``` ## HotChocolateTypeModule.735550c.g.cs @@ -183,12 +218,12 @@ namespace Microsoft.Extensions.DependencyInjection builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( "Tests::TestNamespace.ATestAAttrType", () => global::TestNamespace.ATestAAttrType.Initialize)); - builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( - "Tests::TestNamespace.ATestBAttrType", - () => global::TestNamespace.ATestBAttrType.Initialize)); builder.AddTypeExtension(); builder.AddTypeExtension(); builder.AddType(); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.ATestBAttrType", + () => global::TestNamespace.ATestBAttrType.Initialize)); builder.AddTypeExtension(); builder.AddTypeExtension(); builder.AddType(); @@ -205,46 +240,3 @@ namespace Microsoft.Extensions.DependencyInjection ``` -## HotChocolateTypes.735550c.g.cs - -```csharp -// - -#nullable enable -#pragma warning disable - -using System; -using System.Runtime.CompilerServices; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Execution.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace TestNamespace -{ -public static partial class ATestBAttrType -{ - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) - { - - Configure(descriptor); - } - - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); -} - -public static partial class ATestAAttrType -{ - internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) - { - - Configure(descriptor); - } - - static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); -} -} - - -``` - diff --git a/src/HotChocolate/Core/test/Types.CursorPagination.Tests/CustomConnectionTest.cs b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/CustomConnectionTest.cs new file mode 100644 index 00000000000..d7725ee88e4 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/CustomConnectionTest.cs @@ -0,0 +1,56 @@ +using System.Collections.Immutable; +using HotChocolate.Execution; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Types.Pagination; + +public class CustomConnectionTest +{ + [Fact] + public async Task Ensure_That_Custom_Connections_Work_With_Legacy_Middleware() + { + // arrange + var executor = await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .BuildRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder.New() + .SetDocument("{ products { edges { node { name } } } }") + .Build()); + + // assert + result.ToJson().MatchSnapshot(); + } + + public class Query + { + [UsePaging] + public Task GetProductsAsync( + int? first, string? after, int? last, string? before) + => Task.FromResult( + new ProductConnection( + [new ProductEdge(new Product("Abc"))], + new ConnectionPageInfo(true, true, "Abc", "Abc"))); + } + + public class ProductConnection( + ImmutableArray edges, + ConnectionPageInfo pageInfo) + : ConnectionBase + { + public override IReadOnlyList Edges { get; } = edges; + public override ConnectionPageInfo PageInfo { get; } = pageInfo; + } + + public class ProductEdge(Product product) : IEdge + { + public string Cursor => product.Name; + public Product Node => product; + object? IEdge.Node => Node; + } + + public record Product(string Name); +} diff --git a/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/CustomConnectionTest.Ensure_That_Custom_Connections_Work_With_Legacy_Middleware.snap b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/CustomConnectionTest.Ensure_That_Custom_Connections_Work_With_Legacy_Middleware.snap new file mode 100644 index 00000000000..0dea332d337 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/CustomConnectionTest.Ensure_That_Custom_Connections_Work_With_Legacy_Middleware.snap @@ -0,0 +1,13 @@ +{ + "data": { + "products": { + "edges": [ + { + "node": { + "name": "Abc" + } + } + ] + } + } +} diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs index 514c9c95aa6..9407e943cdf 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs @@ -14,21 +14,21 @@ namespace HotChocolate.CostAnalysis; internal sealed class CostTypeInterceptor : TypeInterceptor { private readonly ImmutableArray _forwardAndBackwardSlicingArgs - = ImmutableArray.Create("first", "last"); + = ["first", "last"]; private readonly ImmutableArray _forwardSlicingArgs - = ImmutableArray.Create("first"); + = ["first"]; private readonly ImmutableArray _sizedFields - = ImmutableArray.Create("edges", "nodes"); + = ["edges", "nodes"]; private readonly ImmutableArray _offSetSlicingArgs - = ImmutableArray.Create("take"); + = ["take"]; private readonly ImmutableArray _offsetSizedFields - = ImmutableArray.Create("items"); + = ["items"]; - private CostOptions _options = default!; + private CostOptions _options = null!; internal override uint Position => int.MaxValue; diff --git a/src/HotChocolate/Data/src/EntityFramework/Pagination/EfQueryableCursorPagingHandler.cs b/src/HotChocolate/Data/src/EntityFramework/Pagination/EfQueryableCursorPagingHandler.cs index 2bcdb392e34..bed43f2d8a7 100644 --- a/src/HotChocolate/Data/src/EntityFramework/Pagination/EfQueryableCursorPagingHandler.cs +++ b/src/HotChocolate/Data/src/EntityFramework/Pagination/EfQueryableCursorPagingHandler.cs @@ -46,13 +46,15 @@ private async ValueTask SliceAsync( if (arguments.After is not null) { var cursor = CursorParser.Parse(arguments.After, keys); - query = query.Where(ExpressionHelpers.BuildWhereExpression(keys, cursor, true)); + var (whereExpr, _, _) = ExpressionHelpers.BuildWhereExpression(keys, cursor, true); + query = query.Where(whereExpr); } if (arguments.Before is not null) { var cursor = CursorParser.Parse(arguments.Before, keys); - query = query.Where(ExpressionHelpers.BuildWhereExpression(keys, cursor, false)); + var (whereExpr, _, _) = ExpressionHelpers.BuildWhereExpression(keys, cursor, false); + query = query.Where(whereExpr); } if (arguments.First is not null) diff --git a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Fetch_First_2_Items_Between.md b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Fetch_First_2_Items_Between.md index b03a878a308..159107a9c58 100644 --- a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Fetch_First_2_Items_Between.md +++ b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Fetch_First_2_Items_Between.md @@ -15,8 +15,8 @@ "pageInfo": { "hasNextPage": true, "hasPreviousPage": true, - "endCursor": "Mw==", - "startCursor": "Mg==" + "endCursor": "e30z", + "startCursor": "e30y" } } }, diff --git a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Fetch_Last_2_Items_Between.md b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Fetch_Last_2_Items_Between.md index f0a94bd8c55..0067bcc6e2c 100644 --- a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Fetch_Last_2_Items_Between.md +++ b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Fetch_Last_2_Items_Between.md @@ -15,8 +15,8 @@ "pageInfo": { "hasNextPage": true, "hasPreviousPage": true, - "endCursor": "OTk=", - "startCursor": "OTg=" + "endCursor": "e305OQ==", + "startCursor": "e305OA==" } } }, diff --git a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_First_10_With_Default_Sorting_HasNextPage.md b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_First_10_With_Default_Sorting_HasNextPage.md index 39b490dbcaf..3c5927cd5bc 100644 --- a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_First_10_With_Default_Sorting_HasNextPage.md +++ b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_First_10_With_Default_Sorting_HasNextPage.md @@ -39,7 +39,7 @@ "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, - "endCursor": "MTA=" + "endCursor": "e30xMA==" } } }, diff --git a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Last_10_With_Default_Sorting_HasPreviousPage.md b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Last_10_With_Default_Sorting_HasPreviousPage.md index 55672524000..c2e32d28cbe 100644 --- a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Last_10_With_Default_Sorting_HasPreviousPage.md +++ b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Last_10_With_Default_Sorting_HasPreviousPage.md @@ -39,7 +39,7 @@ "pageInfo": { "hasNextPage": false, "hasPreviousPage": true, - "endCursor": "MTAw" + "endCursor": "e30xMDA=" } } }, diff --git a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Next_2_With_Default_Sorting.md b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Next_2_With_Default_Sorting.md index 98f382d7ee8..b9e674d8359 100644 --- a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Next_2_With_Default_Sorting.md +++ b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Next_2_With_Default_Sorting.md @@ -13,7 +13,7 @@ } ], "pageInfo": { - "endCursor": "MTI=" + "endCursor": "e30xMg==" } } }, diff --git a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Next_2_With_User_Sorting.md b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Next_2_With_User_Sorting.md index 1396ee2f689..6f66e23f6c1 100644 --- a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Next_2_With_User_Sorting.md +++ b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_Next_2_With_User_Sorting.md @@ -13,7 +13,7 @@ } ], "pageInfo": { - "endCursor": "QnJhbmQxOToyMA==" + "endCursor": "e31CcmFuZDE5OjIw" } } }, diff --git a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_With_Default_Sorting.md b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_With_Default_Sorting.md index 6241cf3217b..d3b5d6aef0f 100644 --- a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_With_Default_Sorting.md +++ b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_With_Default_Sorting.md @@ -37,7 +37,7 @@ } ], "pageInfo": { - "endCursor": "MTA=" + "endCursor": "e30xMA==" } } }, diff --git a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_With_Default_Sorting_And_TotalCount.md b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_With_Default_Sorting_And_TotalCount.md index 88b0e4fe051..e20684c56e4 100644 --- a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_With_Default_Sorting_And_TotalCount.md +++ b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_With_Default_Sorting_And_TotalCount.md @@ -37,7 +37,7 @@ } ], "pageInfo": { - "endCursor": "MTA=" + "endCursor": "e30xMA==" }, "totalCount": 10000 } diff --git a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_With_User_Sorting.md b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_With_User_Sorting.md index d4921b7cb88..028591a273a 100644 --- a/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_With_User_Sorting.md +++ b/src/HotChocolate/Data/test/Data.EntityFramework.Pagination.Tests/__snapshots__/IntegrationTests.Paging_With_User_Sorting.md @@ -37,7 +37,7 @@ } ], "pageInfo": { - "endCursor": "QnJhbmQxNzoxOA==" + "endCursor": "e31CcmFuZDE3OjE4" } } }, diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/HotChocolate.Data.PostgreSQL.Tests.csproj b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/HotChocolate.Data.PostgreSQL.Tests.csproj index cc47c107ce3..f398dc02b23 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/HotChocolate.Data.PostgreSQL.Tests.csproj +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/HotChocolate.Data.PostgreSQL.Tests.csproj @@ -3,6 +3,8 @@ HotChocolate.Data $(InterceptorsPreviewNamespaces);HotChocolate.Execution.Generated + true + $(NoWarn);CS1591 diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs index 2411a19fe87..66383f0a051 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs @@ -238,7 +238,7 @@ public async Task Query_Products_First_2_And_Brand() } [Fact] - public async Task Query_Products_Include_TotalCount() + public async Task Query_Products_First_2_With_4_EndCursors() { // arrange using var interceptor = new TestQueryInterceptor(); @@ -248,6 +248,62 @@ public async Task Query_Products_Include_TotalCount() """ { products(first: 2) { + nodes { + name + brand { + name + } + } + endCursors(count: 4) + } + } + + """, + interceptor); + + // assert + MatchSnapshot(result, interceptor); + } + + [Fact] + public async Task Query_Products_First_2_With_4_EndCursors_Skip_4() + { + // arrange + using var interceptor = new TestQueryInterceptor(); + + // act + var result = await ExecuteAsync( + """ + { + products(first: 2, after: "ezN8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2") { + nodes { + name + brand { + name + } + } + endCursors(count: 4) + } + } + + """, + interceptor); + + // assert + MatchSnapshot(result, interceptor); + } + + [Fact] + public async Task Query_Products_Include_TotalCount() + { + // arrange + using var interceptor = new TestQueryInterceptor(); + + // act + var result = await ExecuteAsync( + """ + { + productsNonRelative(first: 2) { nodes { name } @@ -272,7 +328,7 @@ public async Task Query_Products_Exclude_TotalCount() var result = await ExecuteAsync( """ { - products(first: 2) { + productsNonRelative(first: 2) { nodes { name } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Models/Product.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Models/Product.cs index 162b494759a..ae0fb5336ba 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Models/Product.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Models/Product.cs @@ -8,8 +8,7 @@ public sealed class Product { public int Id { get; set; } - [Required] - public required string Name { get; set; } + [Required] public required string Name { get; set; } public string? Description { get; set; } @@ -35,24 +34,22 @@ public sealed class Product // Maximum number of units that can be in-stock at any time (due to physicial/logistical constraints in warehouses) public int MaxStockThreshold { get; set; } - /// Optional embedding for the catalog item's description. - // [JsonIgnore] - // public Vector Embedding { get; set; } - /// /// True if item is on reorder /// public bool OnReorder { get; set; } - /// + /// /// Decrements the quantity of a particular item in inventory and ensures the restockThreshold hasn't /// been breached. If so, a RestockRequest is generated in CheckThreshold. - /// + /// + /// /// If there is sufficient stock of an item, then the integer returned at the end of this call should be the same as quantityDesired. /// In the event that there is not sufficient stock available, the method will remove whatever stock is available and return that quantity to the client. /// In this case, it is the responsibility of the client to determine if the amount that is returned is the same as quantityDesired. /// It is invalid to pass in a negative number. + /// /// /// /// int: Returns the number actually removed from stock. diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/TestHelpers.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/TestHelpers.cs index 2f26f8a64cf..82abf9ec09d 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/TestHelpers.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/TestHelpers.cs @@ -2,7 +2,6 @@ namespace HotChocolate.Data; public static class TestHelpers { - // this is only to keep test snapshots ordered. public static IReadOnlyList EnsureOrdered(this IReadOnlyList ids) => ids.OrderBy(t => t).ToArray(); diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs index fdbf8d88662..05cf159cecd 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs @@ -1,22 +1,25 @@ using GreenDonut.Data; using HotChocolate.Data.Models; using HotChocolate.Data.Services; +using HotChocolate.Data.Types.Products; using HotChocolate.Types; -using HotChocolate.Types.Pagination; namespace HotChocolate.Data.Types.Brands; [ObjectType] public static partial class BrandNode { - [UsePaging] + [UseConnection] [UseFiltering] [UseSorting] - public static Task> GetProductsAsync( + public static async Task GetProductsAsync( [Parent(requires: nameof(Brand.Id))] Brand brand, PagingArguments pagingArgs, QueryContext query, ProductService productService, CancellationToken cancellationToken) - => productService.GetProductsByBrandAsync(brand.Id, pagingArgs, query, cancellationToken).ToConnectionAsync(); + { + var page = await productService.GetProductsByBrandAsync(brand.Id, pagingArgs, query, cancellationToken); + return new ProductConnection(page); + } } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandQueries.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandQueries.cs index 473e89100d7..aafab16af6e 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandQueries.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandQueries.cs @@ -2,22 +2,23 @@ using HotChocolate.Data.Models; using HotChocolate.Data.Services; using HotChocolate.Types; -using HotChocolate.Types.Pagination; using HotChocolate.Types.Relay; namespace HotChocolate.Data.Types.Brands; [QueryType] -public static class BrandQueries +public static partial class BrandQueries { - [UsePaging] [UseFiltering] - public static async Task> GetBrandsAsync( + public static async Task> GetBrandsAsync( PagingArguments pagingArgs, QueryContext query, BrandService brandService, CancellationToken cancellationToken) - => await brandService.GetBrandsAsync(pagingArgs, query, cancellationToken).ToConnectionAsync(); + { + var page = await brandService.GetBrandsAsync(pagingArgs, query, cancellationToken); + return new CatalogConnection(page); + } [NodeResolver] public static async Task GetBrandByIdAsync( diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/CatalogConnection.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/CatalogConnection.cs new file mode 100644 index 00000000000..b8e8addc463 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/CatalogConnection.cs @@ -0,0 +1,80 @@ +using GreenDonut.Data; +using HotChocolate.Types.Pagination; + +namespace HotChocolate.Data.Types.Brands; + +[GraphQLName("{0}Connection")] +public class CatalogConnection : ConnectionBase, ConnectionPageInfo> +{ + private readonly Page _page; + private ConnectionPageInfo? _pageInfo; + private CatalogEdge[]? _edges; + + public CatalogConnection(Page page) + { + _page = page; + } + + /// + /// A list of edges. + /// + public override IReadOnlyList> Edges + { + get + { + if (_edges is null) + { + var items = _page.Items; + var edges = new CatalogEdge[items.Length]; + + for (var i = 0; i < items.Length; i++) + { + edges[i] = new CatalogEdge(_page, items[i]); + } + + _edges = edges; + } + + return _edges; + } + } + + /// + /// A flattened list of the nodes. + /// + public IReadOnlyList Nodes => _page.Items; + + /// + /// Information to aid in pagination. + /// + public override ConnectionPageInfo PageInfo + { + get + { + if (_pageInfo is null) + { + string? startCursor = null; + string? endCursor = null; + + if (_page.First is not null) + { + startCursor = _page.CreateCursor(_page.First); + } + + if (_page.Last is not null) + { + endCursor = _page.CreateCursor(_page.Last); + } + + _pageInfo = new ConnectionPageInfo(_page.HasNextPage, _page.HasPreviousPage, startCursor, endCursor); + } + + return _pageInfo; + } + } + + /// + /// Identifies the total count of items in the connection. + /// + public int TotalCount => _page.TotalCount ?? 0; +} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/CatalogEdge.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/CatalogEdge.cs new file mode 100644 index 00000000000..8effeb11f4c --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/CatalogEdge.cs @@ -0,0 +1,23 @@ +using GreenDonut.Data; +using HotChocolate.Types.Pagination; + +namespace HotChocolate.Data.Types.Brands; + +/// +/// An edge in a connection. +/// +[GraphQLName("{0}Edge")] +public class CatalogEdge(Page page, TEntity node) : IEdge +{ + /// + /// The item at the end of the edge. + /// + public TEntity Node { get; } = node; + + object? IEdge.Node => Node; + + /// + /// A cursor for use in pagination. + /// + public string Cursor => page.CreateCursor(Node); +} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductConnection.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductConnection.cs new file mode 100644 index 00000000000..b6686824806 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductConnection.cs @@ -0,0 +1,98 @@ +using GreenDonut.Data; +using HotChocolate.Data.Models; +using HotChocolate.Types; +using HotChocolate.Types.Pagination; + +namespace HotChocolate.Data.Types.Products; + +/// +/// A connection to a list of items. +/// +public class ProductConnection : ConnectionBase +{ + private readonly Page _page; + private ConnectionPageInfo? _pageInfo; + private ProductsEdge[]? _edges; + + public ProductConnection(Page page) + { + _page = page; + } + + /// + /// A list of edges. + /// + public override IReadOnlyList? Edges + { + get + { + if (_edges is null) + { + var items = _page.Items; + var edges = new ProductsEdge[items.Length]; + + for (var i = 0; i < items.Length; i++) + { + edges[i] = new ProductsEdge(_page, items[i]); + } + + _edges = edges; + } + + return _edges; + } + } + + /// + /// A flattened list of the nodes. + /// + public IReadOnlyList? Nodes => _page.Items; + + /// + /// Information to aid in pagination. + /// + public override ConnectionPageInfo PageInfo + { + get + { + if (_pageInfo is null) + { + string? startCursor = null; + string? endCursor = null; + + if (_page.First is not null) + { + startCursor = _page.CreateCursor(_page.First); + } + + if (_page.Last is not null) + { + endCursor = _page.CreateCursor(_page.Last); + } + + _pageInfo = new ConnectionPageInfo(_page.HasNextPage, _page.HasPreviousPage, startCursor, endCursor); + } + + return _pageInfo; + } + } + + /// + /// Identifies the total count of items in the connection. + /// + public int TotalCount => _page.TotalCount ?? 0; + + [GraphQLType>>>] + public IEnumerable GetEndCursors(int count) + { + if (_page.Last is null) + { + yield break; + } + + for (var i = 0; i < count; i++) + { + yield return _page.CreateCursor(_page.Last, i); + } + } +} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductNode.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductNode.cs index f02923e2ec3..bf76f8e0f64 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductNode.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductNode.cs @@ -10,11 +10,18 @@ namespace HotChocolate.Data.Types.Products; [ObjectType] public static partial class ProductNode { - // [BindMember(nameof(Product.Brand))] + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Ignore(t => t.BrandId); + descriptor.Ignore(t => t.TypeId); + descriptor.Ignore(t => t.RemoveStock(0)); + descriptor.Ignore(t => t.AddStock(0)); + } + + [BindMember(nameof(Product.BrandId))] public static async Task GetBrandAsync( [Parent(requires: nameof(Product.BrandId))] Product product, QueryContext query, - ISelection selection, BrandService brandService, CancellationToken cancellationToken) => await brandService.GetBrandByIdAsync(product.BrandId, query, cancellationToken); diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductQueries.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductQueries.cs index 54f7b25a17f..c78f68dd363 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductQueries.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductQueries.cs @@ -1,22 +1,38 @@ using GreenDonut.Data; using HotChocolate.Data.Models; using HotChocolate.Data.Services; +using HotChocolate.Data.Types.Brands; using HotChocolate.Types; -using HotChocolate.Types.Pagination; +using HotChocolate.Types.Descriptors; namespace HotChocolate.Data.Types.Products; - [QueryType] public static partial class ProductQueries { - [UsePaging(IncludeTotalCount = true)] + [UseConnection(IncludeTotalCount = true, EnableRelativeCursors = true)] + [UseFiltering] + [UseSorting] + public static async Task GetProductsAsync( + PagingArguments pagingArgs, + QueryContext query, + ProductService productService, + CancellationToken cancellationToken) + { + var page = await productService.GetProductsAsync(pagingArgs, query, cancellationToken); + return new ProductConnection(page); + } + + [UseConnection(IncludeTotalCount = true)] [UseFiltering] [UseSorting] - public static async Task> GetProductsAsync( + public static async Task GetProductsNonRelativeAsync( PagingArguments pagingArgs, QueryContext query, ProductService productService, CancellationToken cancellationToken) - => await productService.GetProductsAsync(pagingArgs, query, cancellationToken).ToConnectionAsync(); + { + var page = await productService.GetProductsAsync(pagingArgs, query, cancellationToken); + return new ProductConnection(page); + } } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductSortInputType.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductSortInputType.cs index cb51ecaf0fe..1e21d32b749 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductSortInputType.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductSortInputType.cs @@ -1,5 +1,11 @@ using HotChocolate.Data.Models; using HotChocolate.Data.Sorting; +using HotChocolate.Internal; +using HotChocolate.Language; +using HotChocolate.Types; +using HotChocolate.Types.Descriptors; +using HotChocolate.Types.Descriptors.Definitions; +using HotChocolate.Types.Pagination; namespace HotChocolate.Data.Types.Products; diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductsEdge.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductsEdge.cs new file mode 100644 index 00000000000..0adaab8c556 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductsEdge.cs @@ -0,0 +1,23 @@ +using GreenDonut.Data; +using HotChocolate.Data.Models; +using HotChocolate.Types.Pagination; + +namespace HotChocolate.Data.Types.Products; + +/// +/// An edge in a connection. +/// +public class ProductsEdge(Page page, Product node) : IEdge +{ + /// + /// The item at the end of the edge. + /// + public Product Node { get; } = node; + + object? IEdge.Node => Node; + + /// + /// A cursor for use in pagination. + /// + public string Cursor => page.CreateCursor(Node); +} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql index a9620e2970e..17f39130803 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql @@ -8,34 +8,37 @@ interface Node { } type Brand implements Node { - products("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ProductFilterInput @cost(weight: "10") order: [ProductSortInput!] @cost(weight: "10")): ProductsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + products("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ProductFilterInput @cost(weight: "10") order: [ProductSortInput!] @cost(weight: "10")): ProductConnection! @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") id: ID! name: String! } -"A connection to a list of items." -type BrandsConnection { - "Information to aid in pagination." - pageInfo: PageInfo! - "A list of edges." - edges: [BrandsEdge!] - "A flattened list of the nodes." - nodes: [Brand!] +type BrandConnection { + edges: [BrandEdge!]! + nodes: [Brand!]! + pageInfo: ConnectionPageInfo! + totalCount: Int! @cost(weight: "10") } -"An edge in a connection." -type BrandsEdge { - "A cursor for use in pagination." - cursor: String! - "The item at the end of the edge." +type BrandEdge { node: Brand! + cursor: String! } -"Information about pagination in a connection." -type PageInfo { - "Indicates whether more edges exist following the set defined by the clients arguments." +""" +Represents the connection page info. +This class provides additional information about pagination in a connection. +""" +type ConnectionPageInfo { + """ + true if there is another page after the current one. + false if this page is the last page of the current data set / collection. + """ hasNextPage: Boolean! - "Indicates whether more edges exist prior the set defined by the clients arguments." + """ + true if there is before this page. + false if this page is the first page in the current data set / collection. + """ hasPreviousPage: Boolean! "When paginating backwards, the cursor to continue." startCursor: String @@ -46,45 +49,43 @@ type PageInfo { type Product implements Node { id: ID! brand: Brand @cost(weight: "10") - removeStock(quantityDesired: Int!): Int! - addStock(quantity: Int!): Int! name: String! description: String price: Decimal! imageFileName: String - typeId: Int! type: ProductType - brandId: Int! availableStock: Int! restockThreshold: Int! maxStockThreshold: Int! + "True if item is on reorder" onReorder: Boolean! } -type ProductType { - id: Int! - name: String! - products: [Product!]! -} - "A connection to a list of items." -type ProductsConnection { - "Information to aid in pagination." - pageInfo: PageInfo! +type ProductConnection { "A list of edges." edges: [ProductsEdge!] "A flattened list of the nodes." nodes: [Product!] + "Information to aid in pagination." + pageInfo: ConnectionPageInfo! "Identifies the total count of items in the connection." totalCount: Int! @cost(weight: "10") + endCursors(count: Int!): [String!]! +} + +type ProductType { + id: Int! + name: String! + products: [Product!]! } "An edge in a connection." type ProductsEdge { - "A cursor for use in pagination." - cursor: String! "The item at the end of the edge." node: Product! + "A cursor for use in pagination." + cursor: String! } type Query { @@ -92,9 +93,10 @@ type Query { node("ID of the object." id: ID!): Node @cost(weight: "10") "Lookup nodes by a list of IDs." nodes("The list of node IDs." ids: [ID!]!): [Node]! @cost(weight: "10") - products("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ProductFilterInput @cost(weight: "10") order: [ProductSortInput!] @cost(weight: "10")): ProductsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - brands("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: BrandFilterInput @cost(weight: "10")): BrandsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + brands("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: BrandFilterInput @cost(weight: "10")): BrandConnection! @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") brandById(id: ID!): Brand @cost(weight: "10") + products("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ProductFilterInput @cost(weight: "10") order: [ProductSortInput!] @cost(weight: "10")): ProductConnection! @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + productsNonRelative("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ProductFilterInput @cost(weight: "10") order: [ProductSortInput!] @cost(weight: "10")): ProductConnection! @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") } input BooleanOperationFilterInput { @@ -166,6 +168,7 @@ input ProductFilterInput { availableStock: IntOperationFilterInput restockThreshold: IntOperationFilterInput maxStockThreshold: IntOperationFilterInput + "True if item is on reorder" onReorder: BooleanOperationFilterInput } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Exclude_TotalCount.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Exclude_TotalCount.md index bff90c6371d..6433134103f 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Exclude_TotalCount.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Exclude_TotalCount.md @@ -5,7 +5,7 @@ ```json { "data": { - "products": { + "productsNonRelative": { "nodes": [ { "name": "Zero Gravity Ski Goggles" diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Exclude_TotalCount__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Exclude_TotalCount__net_8_0.md index bff90c6371d..6433134103f 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Exclude_TotalCount__net_8_0.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Exclude_TotalCount__net_8_0.md @@ -5,7 +5,7 @@ ```json { "data": { - "products": { + "productsNonRelative": { "nodes": [ { "name": "Zero Gravity Ski Goggles" diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand.md index b2aa54ef6a3..22ef30f649e 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand.md @@ -29,7 +29,9 @@ ```sql -- @__p_0='3' -SELECT p."Name", p."BrandId", p."Id" +SELECT ( + SELECT count(*)::int + FROM "Products" AS p0) AS "TotalCount", p."Name", p."BrandId", p."Id" FROM "Products" AS p ORDER BY p."Name" DESC, p."Id" LIMIT @__p_0 diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand__net_8_0.md index b2aa54ef6a3..11e97825ad3 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand__net_8_0.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand__net_8_0.md @@ -28,8 +28,9 @@ ## Query 1 ```sql +-- @__Count_1='101' -- @__p_0='3' -SELECT p."Name", p."BrandId", p."Id" +SELECT @__Count_1 AS "TotalCount", p."Name", p."BrandId", p."Id" FROM "Products" AS p ORDER BY p."Name" DESC, p."Id" LIMIT @__p_0 diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors.md new file mode 100644 index 00000000000..93a4c122161 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors.md @@ -0,0 +1,54 @@ +# Query_Products_First_2_With_4_EndCursors + +## Result + +```json +{ + "data": { + "products": { + "nodes": [ + { + "name": "Zero Gravity Ski Goggles", + "brand": { + "name": "Gravitator" + } + }, + { + "name": "Zenith Cycling Jersey", + "brand": { + "name": "B&R" + } + } + ], + "endCursors": [ + "ezB8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", + "ezF8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", + "ezJ8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", + "ezN8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2" + ] + } + } +} +``` + +## Query 1 + +```sql +-- @__p_0='3' +SELECT ( + SELECT count(*)::int + FROM "Products" AS p0) AS "TotalCount", p."Name", p."BrandId", p."Id" +FROM "Products" AS p +ORDER BY p."Name" DESC, p."Id" +LIMIT @__p_0 +``` + +## Query 2 + +```sql +-- @__ids_0={ '2', '5' } (DbType = Object) +SELECT b."Id", b."Name" +FROM "Brands" AS b +WHERE b."Id" = ANY (@__ids_0) +``` + diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4.md new file mode 100644 index 00000000000..0c2d2128543 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4.md @@ -0,0 +1,56 @@ +# Query_Products_First_2_With_4_EndCursors_Skip_4 + +## Result + +```json +{ + "data": { + "products": { + "nodes": [ + { + "name": "Venture 2022 Snowboard", + "brand": { + "name": "Raptor Elite" + } + }, + { + "name": "VelociX 2000 Bike Helmet", + "brand": { + "name": "Raptor Elite" + } + } + ], + "endCursors": [ + "ezB8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", + "ezF8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", + "ezJ8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", + "ezN8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4" + ] + } + } +} +``` + +## Query 1 + +```sql +-- @__value_0='Zenith Cycling Jersey' +-- @__value_1='46' +-- @__p_3='3' +-- @__p_2='6' +SELECT p."Name", p."BrandId", p."Id" +FROM "Products" AS p +WHERE p."Name" < @__value_0 OR (p."Name" = @__value_0 AND p."Id" > @__value_1) +ORDER BY p."Name" DESC, p."Id" +LIMIT @__p_3 OFFSET @__p_2 +``` + +## Query 2 + +```sql +-- @__ids_0={ '6' } (DbType = Object) +SELECT b."Id", b."Name" +FROM "Brands" AS b +WHERE b."Id" = ANY (@__ids_0) +``` + diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4__net_8_0.md new file mode 100644 index 00000000000..0c2d2128543 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4__net_8_0.md @@ -0,0 +1,56 @@ +# Query_Products_First_2_With_4_EndCursors_Skip_4 + +## Result + +```json +{ + "data": { + "products": { + "nodes": [ + { + "name": "Venture 2022 Snowboard", + "brand": { + "name": "Raptor Elite" + } + }, + { + "name": "VelociX 2000 Bike Helmet", + "brand": { + "name": "Raptor Elite" + } + } + ], + "endCursors": [ + "ezB8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", + "ezF8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", + "ezJ8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", + "ezN8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4" + ] + } + } +} +``` + +## Query 1 + +```sql +-- @__value_0='Zenith Cycling Jersey' +-- @__value_1='46' +-- @__p_3='3' +-- @__p_2='6' +SELECT p."Name", p."BrandId", p."Id" +FROM "Products" AS p +WHERE p."Name" < @__value_0 OR (p."Name" = @__value_0 AND p."Id" > @__value_1) +ORDER BY p."Name" DESC, p."Id" +LIMIT @__p_3 OFFSET @__p_2 +``` + +## Query 2 + +```sql +-- @__ids_0={ '6' } (DbType = Object) +SELECT b."Id", b."Name" +FROM "Brands" AS b +WHERE b."Id" = ANY (@__ids_0) +``` + diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors__net_8_0.md new file mode 100644 index 00000000000..48ac5f68200 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors__net_8_0.md @@ -0,0 +1,53 @@ +# Query_Products_First_2_With_4_EndCursors + +## Result + +```json +{ + "data": { + "products": { + "nodes": [ + { + "name": "Zero Gravity Ski Goggles", + "brand": { + "name": "Gravitator" + } + }, + { + "name": "Zenith Cycling Jersey", + "brand": { + "name": "B&R" + } + } + ], + "endCursors": [ + "ezB8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", + "ezF8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", + "ezJ8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", + "ezN8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2" + ] + } + } +} +``` + +## Query 1 + +```sql +-- @__Count_1='101' +-- @__p_0='3' +SELECT @__Count_1 AS "TotalCount", p."Name", p."BrandId", p."Id" +FROM "Products" AS p +ORDER BY p."Name" DESC, p."Id" +LIMIT @__p_0 +``` + +## Query 2 + +```sql +-- @__ids_0={ '2', '5' } (DbType = Object) +SELECT b."Id", b."Name" +FROM "Brands" AS b +WHERE b."Id" = ANY (@__ids_0) +``` + diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Include_TotalCount.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Include_TotalCount.md index 76b0fc31d86..35e758ff870 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Include_TotalCount.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Include_TotalCount.md @@ -5,7 +5,7 @@ ```json { "data": { - "products": { + "productsNonRelative": { "nodes": [ { "name": "Zero Gravity Ski Goggles" diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Include_TotalCount__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Include_TotalCount__net_8_0.md index 2996fdbae10..7e9e4db14bc 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Include_TotalCount__net_8_0.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_Include_TotalCount__net_8_0.md @@ -5,7 +5,7 @@ ```json { "data": { - "products": { + "productsNonRelative": { "nodes": [ { "name": "Zero Gravity Ski Goggles" diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5.md index 19a899208b2..06f954abb4e 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5.md @@ -4,8 +4,8 @@ ```json { - "First": "UHJvZHVjdCAwLTA6MQ==", - "Last": "UHJvZHVjdCAwLTE6Mg==", + "First": "e31Qcm9kdWN0IDAtMDox", + "Last": "e31Qcm9kdWN0IDAtMToy", "Items": [ { "Id": 1, @@ -45,8 +45,8 @@ ```json { - "First": "UHJvZHVjdCAxLTA6MTAx", - "Last": "UHJvZHVjdCAxLTE6MTAy", + "First": "e31Qcm9kdWN0IDEtMDoxMDE=", + "Last": "e31Qcm9kdWN0IDEtMToxMDI=", "Items": [ { "Id": 101, @@ -86,8 +86,8 @@ ```json { - "First": "UHJvZHVjdCAyLTA6MjAx", - "Last": "UHJvZHVjdCAyLTE6MjAy", + "First": "e31Qcm9kdWN0IDItMDoyMDE=", + "Last": "e31Qcm9kdWN0IDItMToyMDI=", "Items": [ { "Id": 201, diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5_NET9_0.md index 84bd821e26b..14b1e38fe52 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_First_5_NET9_0.md @@ -4,8 +4,8 @@ ```json { - "First": "UHJvZHVjdCAwLTA6MQ==", - "Last": "UHJvZHVjdCAwLTE6Mg==", + "First": "e31Qcm9kdWN0IDAtMDox", + "Last": "e31Qcm9kdWN0IDAtMToy", "Items": [ { "Id": 1, @@ -45,8 +45,8 @@ ```json { - "First": "UHJvZHVjdCAxLTA6MTAx", - "Last": "UHJvZHVjdCAxLTE6MTAy", + "First": "e31Qcm9kdWN0IDEtMDoxMDE=", + "Last": "e31Qcm9kdWN0IDEtMToxMDI=", "Items": [ { "Id": 101, @@ -86,8 +86,8 @@ ```json { - "First": "UHJvZHVjdCAyLTA6MjAx", - "Last": "UHJvZHVjdCAyLTE6MjAy", + "First": "e31Qcm9kdWN0IDItMDoyMDE=", + "Last": "e31Qcm9kdWN0IDItMToyMDI=", "Items": [ { "Id": 201, diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5.md index 1ff0e6d16a4..55d657dc0c0 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5.md @@ -4,8 +4,8 @@ ```json { - "First": "MTAw", - "Last": "OTk=", + "First": "e30xMDA=", + "Last": "e305OQ==", "Items": [ { "Id": 100, @@ -45,8 +45,8 @@ ```json { - "First": "MjAw", - "Last": "MTk5", + "First": "e30yMDA=", + "Last": "e30xOTk=", "Items": [ { "Id": 200, @@ -86,8 +86,8 @@ ```json { - "First": "MzAw", - "Last": "Mjk5", + "First": "e30zMDA=", + "Last": "e30yOTk=", "Items": [ { "Id": 300, diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md index 34bc584642e..9742f19d2d3 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md @@ -4,8 +4,8 @@ ```json { - "First": "MTAw", - "Last": "OTk=", + "First": "e30xMDA=", + "Last": "e305OQ==", "Items": [ { "Id": 100, @@ -45,8 +45,8 @@ ```json { - "First": "MjAw", - "Last": "MTk5", + "First": "e30yMDA=", + "Last": "e30xOTk=", "Items": [ { "Id": 200, @@ -86,8 +86,8 @@ ```json { - "First": "MzAw", - "Last": "Mjk5", + "First": "e30zMDA=", + "Last": "e30yOTk=", "Items": [ { "Id": 300, diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw.md index f2c40b1ebc2..d2b2d9cec25 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw.md @@ -29,10 +29,10 @@ ORDER BY t."Name", t."Id" "foos": { "edges": [ { - "cursor": "Rm9vIDE6MQ==" + "cursor": "e31Gb28gMTox" }, { - "cursor": "Rm9vIDI6Mg==" + "cursor": "e31Gb28gMjoy" } ], "nodes": [ @@ -53,8 +53,8 @@ ORDER BY t."Name", t."Id" "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, - "startCursor": "Rm9vIDE6MQ==", - "endCursor": "Rm9vIDI6Mg==" + "startCursor": "e31Gb28gMTox", + "endCursor": "e31Gb28gMjoy" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw_2.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw_2.md index e1bbd9a9c4c..5ed68a9ddbd 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw_2.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw_2.md @@ -29,10 +29,10 @@ ORDER BY t."Name", t."Id" "foos": { "edges": [ { - "cursor": "Rm9vIDE6MQ==" + "cursor": "e31Gb28gMTox" }, { - "cursor": "Rm9vIDI6Mg==" + "cursor": "e31Gb28gMjoy" } ], "nodes": [ @@ -55,8 +55,8 @@ ORDER BY t."Name", t."Id" "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, - "startCursor": "Rm9vIDE6MQ==", - "endCursor": "Rm9vIDI6Mg==" + "startCursor": "e31Gb28gMTox", + "endCursor": "e31Gb28gMjoy" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw_2_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw_2_NET9_0.md index 2f72d135ece..0710a2f2465 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw_2_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw_2_NET9_0.md @@ -29,10 +29,10 @@ ORDER BY f0."Name", f0."Id" "foos": { "edges": [ { - "cursor": "Rm9vIDE6MQ==" + "cursor": "e31Gb28gMTox" }, { - "cursor": "Rm9vIDI6Mg==" + "cursor": "e31Gb28gMjoy" } ], "nodes": [ @@ -55,8 +55,8 @@ ORDER BY f0."Name", f0."Id" "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, - "startCursor": "Rm9vIDE6MQ==", - "endCursor": "Rm9vIDI6Mg==" + "startCursor": "e31Gb28gMTox", + "endCursor": "e31Gb28gMjoy" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw_NET9_0.md index 546c292f110..93ad35a9590 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Ensure_Nullable_Connections_Dont_Throw_NET9_0.md @@ -29,10 +29,10 @@ ORDER BY f0."Name", f0."Id" "foos": { "edges": [ { - "cursor": "Rm9vIDE6MQ==" + "cursor": "e31Gb28gMTox" }, { - "cursor": "Rm9vIDI6Mg==" + "cursor": "e31Gb28gMjoy" } ], "nodes": [ @@ -53,8 +53,8 @@ ORDER BY f0."Name", f0."Id" "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, - "startCursor": "Rm9vIDE6MQ==", - "endCursor": "Rm9vIDI6Mg==" + "startCursor": "e31Gb28gMTox", + "endCursor": "e31Gb28gMjoy" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage.md index df519c8494c..e297f5fa38b 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage.md @@ -67,8 +67,8 @@ LIMIT @__p_0 "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, - "startCursor": "QnJhbmRcOjA6MQ==", - "endCursor": "QnJhbmRcOjE3OjE4" + "startCursor": "e31CcmFuZFw6MDox", + "endCursor": "e31CcmFuZFw6MTc6MTg=" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage2.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage2.md index 6b5068ae540..d315058f03e 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage2.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage2.md @@ -67,8 +67,8 @@ LIMIT @__p_0 "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, - "startCursor": "QnJhbmRcOjA6MQ==", - "endCursor": "QnJhbmRcOjE3OjE4" + "startCursor": "e31CcmFuZFw6MDox", + "endCursor": "e31CcmFuZFw6MTc6MTg=" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Deep.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Deep.md index 6c6e8b2b639..c10274298a0 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Deep.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Deep.md @@ -24,34 +24,34 @@ LIMIT @__p_0 "brandsDeep": { "edges": [ { - "cursor": "Q291bnRyeTA6MQ==" + "cursor": "e31Db3VudHJ5MDox" }, { - "cursor": "Q291bnRyeTE6Mg==" + "cursor": "e31Db3VudHJ5MToy" }, { - "cursor": "Q291bnRyeTEwOjEx" + "cursor": "e31Db3VudHJ5MTA6MTE=" }, { - "cursor": "Q291bnRyeTExOjEy" + "cursor": "e31Db3VudHJ5MTE6MTI=" }, { - "cursor": "Q291bnRyeTEyOjEz" + "cursor": "e31Db3VudHJ5MTI6MTM=" }, { - "cursor": "Q291bnRyeTEzOjE0" + "cursor": "e31Db3VudHJ5MTM6MTQ=" }, { - "cursor": "Q291bnRyeTE0OjE1" + "cursor": "e31Db3VudHJ5MTQ6MTU=" }, { - "cursor": "Q291bnRyeTE1OjE2" + "cursor": "e31Db3VudHJ5MTU6MTY=" }, { - "cursor": "Q291bnRyeTE2OjE3" + "cursor": "e31Db3VudHJ5MTY6MTc=" }, { - "cursor": "Q291bnRyeTE3OjE4" + "cursor": "e31Db3VudHJ5MTc6MTg=" } ], "nodes": [ @@ -159,8 +159,8 @@ LIMIT @__p_0 "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, - "startCursor": "Q291bnRyeTA6MQ==", - "endCursor": "Q291bnRyeTE3OjE4" + "startCursor": "e31Db3VudHJ5MDox", + "endCursor": "e31Db3VudHJ5MTc6MTg=" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Deep_SecondPage.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Deep_SecondPage.md index d2107eb8d9b..a50e27e538d 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Deep_SecondPage.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Deep_SecondPage.md @@ -27,10 +27,10 @@ LIMIT @__p_2 "brandsDeep": { "edges": [ { - "cursor": "Q291bnRyeTEwOjEx" + "cursor": "e31Db3VudHJ5MTA6MTE=" }, { - "cursor": "Q291bnRyeTExOjEy" + "cursor": "e31Db3VudHJ5MTE6MTI=" } ], "nodes": [ @@ -58,8 +58,8 @@ LIMIT @__p_2 "pageInfo": { "hasNextPage": true, "hasPreviousPage": true, - "startCursor": "Q291bnRyeTEwOjEx", - "endCursor": "Q291bnRyeTExOjEy" + "startCursor": "e31Db3VudHJ5MTA6MTE=", + "endCursor": "e31Db3VudHJ5MTE6MTI=" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable.md index d3b0ef00f1e..caf9615c1b5 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable.md @@ -24,34 +24,34 @@ LIMIT @__p_0 "brandsNullable": { "edges": [ { - "cursor": "QnJhbmRcOjA6XG51bGw6MQ==" + "cursor": "e31CcmFuZFw6MDpcbnVsbDox" }, { - "cursor": "QnJhbmRcOjE6XG51bGw6Mg==" + "cursor": "e31CcmFuZFw6MTpcbnVsbDoy" }, { - "cursor": "QnJhbmRcOjEwOlxudWxsOjEx" + "cursor": "e31CcmFuZFw6MTA6XG51bGw6MTE=" }, { - "cursor": "QnJhbmRcOjExOlxudWxsOjEy" + "cursor": "e31CcmFuZFw6MTE6XG51bGw6MTI=" }, { - "cursor": "QnJhbmRcOjEyOlxudWxsOjEz" + "cursor": "e31CcmFuZFw6MTI6XG51bGw6MTM=" }, { - "cursor": "QnJhbmRcOjEzOlxudWxsOjE0" + "cursor": "e31CcmFuZFw6MTM6XG51bGw6MTQ=" }, { - "cursor": "QnJhbmRcOjE0OlxudWxsOjE1" + "cursor": "e31CcmFuZFw6MTQ6XG51bGw6MTU=" }, { - "cursor": "QnJhbmRcOjE1OlxudWxsOjE2" + "cursor": "e31CcmFuZFw6MTU6XG51bGw6MTY=" }, { - "cursor": "QnJhbmRcOjE2OlxudWxsOjE3" + "cursor": "e31CcmFuZFw6MTY6XG51bGw6MTc=" }, { - "cursor": "QnJhbmRcOjE3OlxudWxsOjE4" + "cursor": "e31CcmFuZFw6MTc6XG51bGw6MTg=" } ], "nodes": [ @@ -159,8 +159,8 @@ LIMIT @__p_0 "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, - "startCursor": "QnJhbmRcOjA6XG51bGw6MQ==", - "endCursor": "QnJhbmRcOjE3OlxudWxsOjE4" + "startCursor": "e31CcmFuZFw6MDpcbnVsbDox", + "endCursor": "e31CcmFuZFw6MTc6XG51bGw6MTg=" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_Fallback.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_Fallback.md index 0f33dd8a386..afda9894115 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_Fallback.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_Fallback.md @@ -24,34 +24,34 @@ LIMIT @__p_0 "brandsNullableFallback": { "edges": [ { - "cursor": "QnJhbmRcOjE6Mg==" + "cursor": "e31CcmFuZFw6MToy" }, { - "cursor": "QnJhbmRcOjExOjEy" + "cursor": "e31CcmFuZFw6MTE6MTI=" }, { - "cursor": "QnJhbmRcOjEzOjE0" + "cursor": "e31CcmFuZFw6MTM6MTQ=" }, { - "cursor": "QnJhbmRcOjE1OjE2" + "cursor": "e31CcmFuZFw6MTU6MTY=" }, { - "cursor": "QnJhbmRcOjE3OjE4" + "cursor": "e31CcmFuZFw6MTc6MTg=" }, { - "cursor": "QnJhbmRcOjE5OjIw" + "cursor": "e31CcmFuZFw6MTk6MjA=" }, { - "cursor": "QnJhbmRcOjIxOjIy" + "cursor": "e31CcmFuZFw6MjE6MjI=" }, { - "cursor": "QnJhbmRcOjIzOjI0" + "cursor": "e31CcmFuZFw6MjM6MjQ=" }, { - "cursor": "QnJhbmRcOjI1OjI2" + "cursor": "e31CcmFuZFw6MjU6MjY=" }, { - "cursor": "QnJhbmRcOjI3OjI4" + "cursor": "e31CcmFuZFw6Mjc6Mjg=" } ], "nodes": [ @@ -159,8 +159,8 @@ LIMIT @__p_0 "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, - "startCursor": "QnJhbmRcOjE6Mg==", - "endCursor": "QnJhbmRcOjI3OjI4" + "startCursor": "e31CcmFuZFw6MToy", + "endCursor": "e31CcmFuZFw6Mjc6Mjg=" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_Fallback_SecondPage.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_Fallback_SecondPage.md index 724cac695e1..eb298041210 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_Fallback_SecondPage.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_Fallback_SecondPage.md @@ -27,10 +27,10 @@ LIMIT @__p_2 "brandsNullableFallback": { "edges": [ { - "cursor": "QnJhbmRcOjEzOjE0" + "cursor": "e31CcmFuZFw6MTM6MTQ=" }, { - "cursor": "QnJhbmRcOjE1OjE2" + "cursor": "e31CcmFuZFw6MTU6MTY=" } ], "nodes": [ @@ -58,8 +58,8 @@ LIMIT @__p_2 "pageInfo": { "hasNextPage": true, "hasPreviousPage": true, - "startCursor": "QnJhbmRcOjEzOjE0", - "endCursor": "QnJhbmRcOjE1OjE2" + "startCursor": "e31CcmFuZFw6MTM6MTQ=", + "endCursor": "e31CcmFuZFw6MTU6MTY=" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_SecondPage.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_SecondPage.md index ff74e8dfd76..708c1eb98ee 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_SecondPage.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_SecondPage.md @@ -27,10 +27,10 @@ LIMIT @__p_3 "brandsNullable": { "edges": [ { - "cursor": "QnJhbmRcOjExOlxudWxsOjEy" + "cursor": "e31CcmFuZFw6MTE6XG51bGw6MTI=" }, { - "cursor": "QnJhbmRcOjEyOlxudWxsOjEz" + "cursor": "e31CcmFuZFw6MTI6XG51bGw6MTM=" } ], "nodes": [ @@ -58,8 +58,8 @@ LIMIT @__p_3 "pageInfo": { "hasNextPage": true, "hasPreviousPage": true, - "startCursor": "QnJhbmRcOjExOlxudWxsOjEy", - "endCursor": "QnJhbmRcOjEyOlxudWxsOjEz" + "startCursor": "e31CcmFuZFw6MTE6XG51bGw6MTI=", + "endCursor": "e31CcmFuZFw6MTI6XG51bGw6MTM=" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetSecondPage_With_2_Items.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetSecondPage_With_2_Items.md index e86b674e184..e5ea62b3bbf 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetSecondPage_With_2_Items.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetSecondPage_With_2_Items.md @@ -38,8 +38,8 @@ LIMIT @__p_2 "pageInfo": { "hasNextPage": true, "hasPreviousPage": true, - "startCursor": "QnJhbmRcOjE4OjE5", - "endCursor": "QnJhbmRcOjE5OjIw" + "startCursor": "e31CcmFuZFw6MTg6MTk=", + "endCursor": "e31CcmFuZFw6MTk6MjA=" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Map_Page_To_Connection_With_Dto.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Map_Page_To_Connection_With_Dto.md index b90182e645e..c0b492c7cbc 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Map_Page_To_Connection_With_Dto.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Map_Page_To_Connection_With_Dto.md @@ -24,7 +24,7 @@ LIMIT @__p_0 "brands": { "edges": [ { - "cursor": "QnJhbmRcOjA6MQ==", + "cursor": "e31CcmFuZFw6MDox", "displayName": "BrandDisplay0", "node": { "id": 1, @@ -32,7 +32,7 @@ LIMIT @__p_0 } }, { - "cursor": "QnJhbmRcOjE6Mg==", + "cursor": "e31CcmFuZFw6MToy", "displayName": null, "node": { "id": 2, diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Map_Page_To_Connection_With_Dto_2.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Map_Page_To_Connection_With_Dto_2.md index 065ef3c6f9d..5f7c48614ed 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Map_Page_To_Connection_With_Dto_2.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Map_Page_To_Connection_With_Dto_2.md @@ -24,7 +24,7 @@ LIMIT @__p_0 "brands": { "edges": [ { - "cursor": "QnJhbmRcOjA6MQ==", + "cursor": "e31CcmFuZFw6MDox", "displayName": "BrandDisplay0", "node": { "id": 1, @@ -32,7 +32,7 @@ LIMIT @__p_0 } }, { - "cursor": "QnJhbmRcOjE6Mg==", + "cursor": "e31CcmFuZFw6MToy", "displayName": null, "node": { "id": 2, diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2.md index 30e4889b6bb..d6e43aab35e 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2.md @@ -53,10 +53,10 @@ ORDER BY t."BrandId", t0."BrandId", t0."Name", t0."Id" "brands": { "edges": [ { - "cursor": "QnJhbmRcOjA6MQ==" + "cursor": "e31CcmFuZFw6MDox" }, { - "cursor": "QnJhbmRcOjE6Mg==" + "cursor": "e31CcmFuZFw6MToy" } ], "nodes": [ @@ -73,8 +73,8 @@ ORDER BY t."BrandId", t0."BrandId", t0."Name", t0."Id" "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, - "startCursor": "UHJvZHVjdCAwLTA6MQ==", - "endCursor": "UHJvZHVjdCAwLTE6Mg==" + "startCursor": "e31Qcm9kdWN0IDAtMDox", + "endCursor": "e31Qcm9kdWN0IDAtMToy" } } }, @@ -91,8 +91,8 @@ ORDER BY t."BrandId", t0."BrandId", t0."Name", t0."Id" "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, - "startCursor": "UHJvZHVjdCAxLTA6MTAx", - "endCursor": "UHJvZHVjdCAxLTE6MTAy" + "startCursor": "e31Qcm9kdWN0IDEtMDoxMDE=", + "endCursor": "e31Qcm9kdWN0IDEtMToxMDI=" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2_NET9_0.md index 8ade6d72a9e..0e6a2e32ca5 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2_NET9_0.md @@ -53,10 +53,10 @@ ORDER BY p1."BrandId", p3."BrandId", p3."Name", p3."Id" "brands": { "edges": [ { - "cursor": "QnJhbmRcOjA6MQ==" + "cursor": "e31CcmFuZFw6MDox" }, { - "cursor": "QnJhbmRcOjE6Mg==" + "cursor": "e31CcmFuZFw6MToy" } ], "nodes": [ @@ -73,8 +73,8 @@ ORDER BY p1."BrandId", p3."BrandId", p3."Name", p3."Id" "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, - "startCursor": "UHJvZHVjdCAwLTA6MQ==", - "endCursor": "UHJvZHVjdCAwLTE6Mg==" + "startCursor": "e31Qcm9kdWN0IDAtMDox", + "endCursor": "e31Qcm9kdWN0IDAtMToy" } } }, @@ -91,8 +91,8 @@ ORDER BY p1."BrandId", p3."BrandId", p3."Name", p3."Id" "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, - "startCursor": "UHJvZHVjdCAxLTA6MTAx", - "endCursor": "UHJvZHVjdCAxLTE6MTAy" + "startCursor": "e31Qcm9kdWN0IDEtMDoxMDE=", + "endCursor": "e31Qcm9kdWN0IDEtMToxMDI=" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2_With_Projections.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2_With_Projections.md index edbb51224b8..82c9d73a2d3 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2_With_Projections.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2_With_Projections.md @@ -53,10 +53,10 @@ ORDER BY t."BrandId", t0."BrandId", t0."Name", t0."Id" "brands": { "edges": [ { - "cursor": "QnJhbmRcOjA6MQ==" + "cursor": "e31CcmFuZFw6MDox" }, { - "cursor": "QnJhbmRcOjE6Mg==" + "cursor": "e31CcmFuZFw6MToy" } ], "nodes": [ @@ -73,8 +73,8 @@ ORDER BY t."BrandId", t0."BrandId", t0."Name", t0."Id" "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, - "startCursor": "UHJvZHVjdCAwLTA6MQ==", - "endCursor": "UHJvZHVjdCAwLTE6Mg==" + "startCursor": "e31Qcm9kdWN0IDAtMDox", + "endCursor": "e31Qcm9kdWN0IDAtMToy" } } }, @@ -91,8 +91,8 @@ ORDER BY t."BrandId", t0."BrandId", t0."Name", t0."Id" "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, - "startCursor": "UHJvZHVjdCAxLTA6MTAx", - "endCursor": "UHJvZHVjdCAxLTE6MTAy" + "startCursor": "e31Qcm9kdWN0IDEtMDoxMDE=", + "endCursor": "e31Qcm9kdWN0IDEtMToxMDI=" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2_With_Projections_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2_With_Projections_NET9_0.md index 71f5f62f4ce..bbaac6a367e 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2_With_Projections_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Nested_Paging_First_2_With_Projections_NET9_0.md @@ -53,10 +53,10 @@ ORDER BY p1."BrandId", p3."BrandId", p3."Name", p3."Id" "brands": { "edges": [ { - "cursor": "QnJhbmRcOjA6MQ==" + "cursor": "e31CcmFuZFw6MDox" }, { - "cursor": "QnJhbmRcOjE6Mg==" + "cursor": "e31CcmFuZFw6MToy" } ], "nodes": [ @@ -73,8 +73,8 @@ ORDER BY p1."BrandId", p3."BrandId", p3."Name", p3."Id" "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, - "startCursor": "UHJvZHVjdCAwLTA6MQ==", - "endCursor": "UHJvZHVjdCAwLTE6Mg==" + "startCursor": "e31Qcm9kdWN0IDAtMDox", + "endCursor": "e31Qcm9kdWN0IDAtMToy" } } }, @@ -91,8 +91,8 @@ ORDER BY p1."BrandId", p3."BrandId", p3."Name", p3."Id" "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, - "startCursor": "UHJvZHVjdCAxLTA6MTAx", - "endCursor": "UHJvZHVjdCAxLTE6MTAy" + "startCursor": "e31Qcm9kdWN0IDEtMDoxMDE=", + "endCursor": "e31Qcm9kdWN0IDEtMToxMDI=" } } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md index e6e8cfb64fe..9266fe3eaf0 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md @@ -21,9 +21,9 @@ ORDER BY b."Name", b."Id" "HasNextPage": false, "HasPreviousPage": false, "First": 1, - "FirstCursor": "QnJhbmRcOjA6MQ==", + "FirstCursor": "e31CcmFuZFw6MDox", "Last": 100, - "LastCursor": "QnJhbmRcOjk5OjEwMA==" + "LastCursor": "e31CcmFuZFw6OTk6MTAw" } ``` diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5.md index 84fbabb9dca..5517147aaa8 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5.md @@ -23,9 +23,9 @@ LIMIT @__p_0 "HasNextPage": true, "HasPreviousPage": false, "First": 1, - "FirstCursor": "QnJhbmRcOjA6MQ==", + "FirstCursor": "e31CcmFuZFw6MDox", "Last": 13, - "LastCursor": "QnJhbmRcOjEyOjEz" + "LastCursor": "e31CcmFuZFw6MTI6MTM=" } ``` diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md index 57afb399be8..69d7a1dfff9 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md @@ -26,9 +26,9 @@ LIMIT @__p_2 "HasNextPage": true, "HasPreviousPage": true, "First": 14, - "FirstCursor": "QnJhbmRcOjEzOjE0", + "FirstCursor": "e31CcmFuZFw6MTM6MTQ=", "Last": 18, - "LastCursor": "QnJhbmRcOjE3OjE4" + "LastCursor": "e31CcmFuZFw6MTc6MTg=" } ``` diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md index fdff316caee..8b8f843a2f9 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md @@ -26,9 +26,9 @@ LIMIT @__p_2 "HasNextPage": true, "HasPreviousPage": true, "First": 92, - "FirstCursor": "QnJhbmRcOjkxOjky", + "FirstCursor": "e31CcmFuZFw6OTE6OTI=", "Last": 96, - "LastCursor": "QnJhbmRcOjk1Ojk2" + "LastCursor": "e31CcmFuZFw6OTU6OTY=" } ``` diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md index 8b04e57e616..3266f3c619b 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md @@ -23,9 +23,9 @@ LIMIT @__p_0 "HasNextPage": false, "HasPreviousPage": true, "First": 96, - "FirstCursor": "QnJhbmRcOjk1Ojk2", + "FirstCursor": "e31CcmFuZFw6OTU6OTY=", "Last": 100, - "LastCursor": "QnJhbmRcOjk5OjEwMA==" + "LastCursor": "e31CcmFuZFw6OTk6MTAw" } ``` From e9245d4b7dd672fdad50945f35f7dc1b679f3c1b Mon Sep 17 00:00:00 2001 From: Glen Date: Mon, 10 Mar 2025 22:04:21 +0200 Subject: [PATCH 18/64] Avoided setting an implicit name for enum values with an explicit name (#8108) --- .../Types/Descriptors/EnumTypeDescriptor.cs | 8 +++- .../Types/Descriptors/EnumValueDescriptor.cs | 6 ++- .../test/Types.Tests/Types/EnumTypeTests.cs | 48 +++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumTypeDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumTypeDescriptor.cs index d604d02b987..8cad3f72ee8 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumTypeDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumTypeDescriptor.cs @@ -72,12 +72,16 @@ protected void AddImplicitValues( { foreach (var value in Context.TypeInspector.GetEnumValues(typeDefinition.RuntimeType)) { + if (values.ContainsKey(value)) + { + continue; + } + var valueDefinition = EnumValueDescriptor.New(Context, value) .CreateDefinition(); - if (valueDefinition.RuntimeValue is not null && - !values.ContainsKey(valueDefinition.RuntimeValue)) + if (valueDefinition.RuntimeValue is not null) { values.Add(valueDefinition.RuntimeValue, valueDefinition); } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumValueDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumValueDescriptor.cs index b51466caff6..191fdfd8333 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumValueDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/EnumValueDescriptor.cs @@ -16,7 +16,6 @@ protected EnumValueDescriptor(IDescriptorContext context, object runtimeValue) throw new ArgumentNullException(nameof(runtimeValue)); } - Definition.Name = context.Naming.GetEnumValueName(runtimeValue); Definition.RuntimeValue = runtimeValue; Definition.Description = context.Naming.GetEnumValueDescription(runtimeValue); Definition.Member = context.TypeInspector.GetEnumValueMember(runtimeValue); @@ -53,6 +52,11 @@ protected override void OnCreateDefinition(EnumValueDefinition definition) } } + if (string.IsNullOrEmpty(definition.Name)) + { + Definition.Name = Context.Naming.GetEnumValueName(Definition.RuntimeValue!); + } + base.OnCreateDefinition(definition); Context.Descriptors.Pop(); diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/EnumTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/EnumTypeTests.cs index da57437fd1d..623a2165f27 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/EnumTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/EnumTypeTests.cs @@ -292,6 +292,46 @@ void Action() => SchemaBuilder.New() .Errors.Single().Message.MatchSnapshot(); } + [Fact] + public void EnumValue_ImplicitInvalidName_SchemaException() + { + // arrange + // act + static void Action() => + SchemaBuilder.New() + .AddQueryType() + .AddType(new EnumType()) + .ModifyOptions(o => o.StrictValidation = false) + .Create(); + + // assert + Assert.Throws(Action) + .Errors.Single().Message.MatchInlineSnapshot( + """ + `SÆT` is not a valid GraphQL name. + https://spec.graphql.org/October2021/#sec-Names + (Parameter 'value') + """); + } + + [Fact] + public void EnumValue_ExplicitName() + { + // arrange + // act + var schema = SchemaBuilder.New() + .AddQueryType() + .AddType(new EnumType(d => d.Value(UnitsEnum.SÆT).Name("SAT"))) + .ModifyOptions(o => o.StrictValidation = false) + .Create(); + + var enumType = schema.Types.OfType().Last(); + + // assert + Assert.Equal("SAT", enumType.Values[0].Name); + Assert.Equal("ANOTHER_VALUE", enumType.Values[1].Name); + } + [Fact] public void EnumValueT_ValueIsNull_SchemaException() { @@ -704,6 +744,14 @@ public enum CriticalityLevel Critical } + private enum UnitsEnum + { + // ReSharper disable once InconsistentNaming + SÆT, + // ReSharper disable once UnusedMember.Local + AnotherValue + } + public class QueryWithEnum { public CriticalityLevel GetCriticalityLevel() => CriticalityLevel.Critical; From ba9b6c40e8944f265ec4a3c6f20ec73201fa7177 Mon Sep 17 00:00:00 2001 From: Tobias Tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 10 Mar 2025 21:05:39 +0100 Subject: [PATCH 19/64] Do not overwrite Id Serializer with AddGlobalObjectIdentification (#8107) --- ...aRequestExecutorBuilderExtensions.Relay.cs | 12 +++++- .../Types/Relay/NodeIdSerializerTests.cs | 42 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/HotChocolate/Core/test/Types.Tests/Types/Relay/NodeIdSerializerTests.cs diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/SchemaRequestExecutorBuilderExtensions.Relay.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/SchemaRequestExecutorBuilderExtensions.Relay.cs index 51998faa1df..54b3d8f40ad 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/SchemaRequestExecutorBuilderExtensions.Relay.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/SchemaRequestExecutorBuilderExtensions.Relay.cs @@ -19,7 +19,11 @@ public static partial class SchemaRequestExecutorBuilderExtensions public static IRequestExecutorBuilder AddGlobalObjectIdentification( this IRequestExecutorBuilder builder) { - builder.AddDefaultNodeIdSerializer(); + if (builder.Services.All(t => t.ServiceType != typeof(INodeIdSerializer))) + { + builder.AddDefaultNodeIdSerializer(); + } + return builder.ConfigureSchema(c => c.AddGlobalObjectIdentification()); } @@ -39,7 +43,11 @@ public static IRequestExecutorBuilder AddGlobalObjectIdentification( this IRequestExecutorBuilder builder, bool registerNodeInterface) { - builder.AddDefaultNodeIdSerializer(); + if (builder.Services.All(t => t.ServiceType != typeof(INodeIdSerializer))) + { + builder.AddDefaultNodeIdSerializer(); + } + return builder.ConfigureSchema(c => c.AddGlobalObjectIdentification(registerNodeInterface)); } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/NodeIdSerializerTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/NodeIdSerializerTests.cs new file mode 100644 index 00000000000..0db550f4ed7 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/NodeIdSerializerTests.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Types.Relay; + +public class NodeIdSerializerTests +{ + [Fact] + public void Registered_NodeIdSerializer_Should_Not_Be_Overwritten_By_AddGlobalObjectIdentification() + { + // arrange + var provider = new ServiceCollection() + .AddGraphQL() + .AddLegacyNodeIdSerializer() + .AddGlobalObjectIdentification() + .Services.BuildServiceProvider(); + + // act + var serializer = provider.GetRequiredService(); + + // assert + Assert.IsType(serializer); + } + + [Fact] + public void + Registered_DefaultNodeIdSerializer_With_OutputNewIdFormat_Should_Not_Be_Overwritten_By_AddGlobalObjectIdentification() + { + // arrange + var provider = new ServiceCollection() + .AddGraphQL() + .AddDefaultNodeIdSerializer(outputNewIdFormat: false) + .AddGlobalObjectIdentification() + .Services.BuildServiceProvider(); + + // act + var serializer = provider.GetRequiredService(); + var serializedId = serializer.Format("Foo", 32); + + // assert + Assert.Equal("Rm9vCmkzMg==", serializedId); + } +} From 46f5a7861e5cbecf4020a4a199eb95c86c33d960 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 10 Mar 2025 21:17:36 +0100 Subject: [PATCH 20/64] Fixed issues with ToPageAsync() and relative cursors. (#8110) --- .../Expressions/ExpressionHelpers.cs | 214 +-- .../ReverseOrderExpressionRewriter.cs | 60 + .../Extensions/PagingQueryableExtensions.cs | 173 ++- .../src/GreenDonut.Data.Primitives/Page.cs | 2 +- .../src/GreenDonut.Data/Cursors/Cursor.cs | 7 +- .../PagingHelperTests.cs | 2 +- .../RelativeCursorTests.cs | 1297 +++++++++++++++++ .../SnapshotExtensions.cs | 12 + ...ngHelperTests.BatchPaging_Last_5_NET8_0.md | 36 +- ...ngHelperTests.BatchPaging_Last_5_NET9_0.md | 36 +- ...BatchPaging_With_Relative_Cursor_NET8_0.md | 12 +- ...BatchPaging_With_Relative_Cursor_NET9_0.md | 12 +- ...gingHelperTests.Paging_Empty_PagingArgs.md | 1090 +------------- ...perTests.Paging_Empty_PagingArgs_NET8_0.md | 1090 +------------- ...gHelperTests.Paging_First_5_After_Id_13.md | 2 +- ...Tests.Paging_First_5_After_Id_13_NET8_0.md | 2 +- ...HelperTests.Paging_First_5_Before_Id_96.md | 2 +- ...ests.Paging_First_5_Before_Id_96_NET8_0.md | 2 +- ...egrationPagingHelperTests.Paging_Last_5.md | 2 +- ...nPagingHelperTests.Paging_Last_5_NET8_0.md | 2 +- ...tems_Second_Page_With_Offset_Negative_2.md | 4 +- ...elperTests.QueryContext_Simple_Selector.md | 2 +- ...ryContext_Simple_Selector_Include_Brand.md | 2 +- ...xt_Simple_Selector_Include_Brand_NET8_0.md | 2 +- ...text_Simple_Selector_Include_Brand_Name.md | 2 +- ...mple_Selector_Include_Brand_Name_NET8_0.md | 2 +- ...xt_Simple_Selector_Include_Product_List.md | 2 +- ...le_Selector_Include_Product_List_NET8_0.md | 2 +- ...sts.QueryContext_Simple_Selector_NET8_0.md | 2 +- .../Conventions/DefaultTypeInspector.cs | 2 + .../EfQueryableCursorPagingHandler.cs | 4 +- ...uery_Products_First_2_With_4_EndCursors.md | 8 +- ...oducts_First_2_With_4_EndCursors_Skip_4.md | 8 +- ...rst_2_With_4_EndCursors_Skip_4__net_8_0.md | 8 +- ...ucts_First_2_With_4_EndCursors__net_8_0.md | 8 +- ...ionPagingHelperTests.BatchPaging_Last_5.md | 36 +- ...ngHelperTests.BatchPaging_Last_5_NET9_0.md | 36 +- ...sts.GetDefaultPage_With_Deep_SecondPage.md | 2 +- ...tPage_With_Nullable_Fallback_SecondPage.md | 2 +- ...GetDefaultPage_With_Nullable_SecondPage.md | 2 +- ...gHelperTests.GetSecondPage_With_2_Items.md | 2 +- ...gingHelperTests.Paging_Empty_PagingArgs.md | 1090 +------------- ...gHelperTests.Paging_First_5_After_Id_13.md | 2 +- ...HelperTests.Paging_First_5_Before_Id_96.md | 2 +- ...egrationPagingHelperTests.Paging_Last_5.md | 2 +- 45 files changed, 1717 insertions(+), 3572 deletions(-) create mode 100644 src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ReverseOrderExpressionRewriter.cs create mode 100644 src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/RelativeCursorTests.cs diff --git a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs index dce7792f212..bf4b9914265 100644 --- a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs +++ b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs @@ -40,7 +40,7 @@ internal static class ExpressionHelpers /// /// If the number of keys does not match the number of values. /// - public static (Expression> WhereExpression, int Offset, bool ReverseOrder) BuildWhereExpression( + public static (Expression> WhereExpression, int Offset) BuildWhereExpression( ReadOnlySpan keys, Cursor cursor, bool forward) @@ -77,53 +77,34 @@ public static (Expression> WhereExpression, int Offset, bool Rever { var handledKey = handled[j]; - keyExpr = - Expression.Equal( - Expression.Call( - ReplaceParameter(handledKey.Expression, parameter), - handledKey.CompareMethod, - cursorExpr[j]), - zero); - - current = current is null - ? keyExpr - : Expression.AndAlso(current, keyExpr); + keyExpr = Expression.Equal( + Expression.Call(ReplaceParameter(handledKey.Expression, parameter), handledKey.CompareMethod, + cursorExpr[j]), zero); + + current = current is null ? keyExpr : Expression.AndAlso(current, keyExpr); } var greaterThan = forward - ? key.Direction is CursorKeyDirection.Ascending - : key.Direction is CursorKeyDirection.Descending; - - keyExpr = - greaterThan - ? Expression.GreaterThan( - Expression.Call( - ReplaceParameter(key.Expression, parameter), - key.CompareMethod, - cursorExpr[i]), - zero) - : Expression.LessThan( - Expression.Call( - ReplaceParameter(key.Expression, parameter), - key.CompareMethod, - cursorExpr[i]), - zero); - - current = current is null - ? keyExpr - : Expression.AndAlso(current, keyExpr); - expression = expression is null - ? current - : Expression.OrElse(expression, current); + ? key.Direction == CursorKeyDirection.Ascending + : key.Direction == CursorKeyDirection.Descending; + + keyExpr = greaterThan + ? Expression.GreaterThan( + Expression.Call(ReplaceParameter(key.Expression, parameter), key.CompareMethod, cursorExpr[i]), + zero) + : Expression.LessThan( + Expression.Call(ReplaceParameter(key.Expression, parameter), key.CompareMethod, cursorExpr[i]), + zero); + + current = current is null ? keyExpr : Expression.AndAlso(current, keyExpr); + expression = expression is null ? current : Expression.OrElse(expression, current); handled.Add(key); } - var offset = cursor.Offset ?? 0; - var reverseOrder = offset < 0; // Reverse order if offset is negative - - return (Expression.Lambda>(expression!, parameter), Math.Abs(offset), reverseOrder); + return (Expression.Lambda>(expression!, parameter), cursor.Offset ?? 0); } + /// /// Build the select expression for a batch paging expression that uses grouping. /// @@ -151,12 +132,11 @@ public static (Expression> WhereExpression, int Offset, bool Rever /// /// The value type. /// - /// /// /// If the number of keys is less than one or /// the number of order expressions does not match the number of order methods. /// - public static (Expression, Group>> SelectExpression, bool ReverseOrder) BuildBatchSelectExpression( + public static BatchExpression BuildBatchExpression( PagingArguments arguments, ReadOnlySpan keys, ReadOnlySpan orderExpressions, @@ -187,7 +167,8 @@ public static (Expression, Group>> SelectExpressi var methodName = forward ? orderMethods[i] : ReverseOrder(orderMethods[i]); var orderExpression = orderExpressions[i]; var delegateType = typeof(Func<,>).MakeGenericType(typeof(TV), orderExpression.Body.Type); - var typedOrderExpression = Expression.Lambda(delegateType, orderExpression.Body, orderExpression.Parameters); + var typedOrderExpression = + Expression.Lambda(delegateType, orderExpression.Body, orderExpression.Parameters); var method = GetEnumerableMethod(methodName, typeof(TV), typedOrderExpression); @@ -197,36 +178,60 @@ public static (Expression, Group>> SelectExpressi typedOrderExpression); } - var reverseOrder = false; var offset = 0; + var usesRelativeCursors = false; + Cursor? cursor = null; if (arguments.After is not null) { - var cursor = CursorParser.Parse(arguments.After, keys); - var (whereExpr, cursorOffset, reverse) = BuildWhereExpression(keys, cursor, forward: true); + cursor = CursorParser.Parse(arguments.After, keys); + var (whereExpr, cursorOffset) = BuildWhereExpression(keys, cursor, forward: true); source = Expression.Call(typeof(Enumerable), "Where", [typeof(TV)], source, whereExpr); offset = cursorOffset; - reverseOrder = reverse; + + if (cursor.IsRelative) + { + usesRelativeCursors = true; + } } if (arguments.Before is not null) { - var cursor = CursorParser.Parse(arguments.Before, keys); - var (whereExpr, cursorOffset, reverse) = BuildWhereExpression(keys, cursor, forward: false); + if (usesRelativeCursors) + { + throw new ArgumentException( + "You cannot use `before` and `after` with relative cursors at the same time.", + nameof(arguments)); + } + + cursor = CursorParser.Parse(arguments.Before, keys); + var (whereExpr, cursorOffset) = BuildWhereExpression(keys, cursor, forward: false); source = Expression.Call(typeof(Enumerable), "Where", [typeof(TV)], source, whereExpr); offset = cursorOffset; - reverseOrder = reverse; } - if (reverseOrder) + if (arguments.First is not null) { - source = Expression.Call( - typeof(Enumerable), - "Reverse", - [typeof(TV)], - source); + requestedCount = arguments.First.Value; + } + + if (arguments.Last is not null) + { + requestedCount = arguments.Last.Value; + } + + if (arguments.EnableRelativeCursors && cursor?.IsRelative == true) + { + if ((arguments.Last is not null && cursor.Offset > 0) || (arguments.First is not null && cursor.Offset < 0)) + { + throw new ArgumentException( + "Positive offsets are not allowed with `last`, and negative offsets are not allowed with `first`.", + nameof(arguments)); + } } + offset = Math.Abs(offset); + if (offset > 0) { source = Expression.Call( @@ -234,7 +239,7 @@ public static (Expression, Group>> SelectExpressi "Skip", [typeof(TV)], source, - Expression.Constant(offset)); + Expression.Constant(offset * requestedCount)); } if (arguments.First is not null) @@ -245,7 +250,6 @@ public static (Expression, Group>> SelectExpressi [typeof(TV)], source, Expression.Constant(arguments.First.Value + 1)); - requestedCount = arguments.First.Value; } if (arguments.Last is not null) @@ -256,7 +260,6 @@ public static (Expression, Group>> SelectExpressi [typeof(TV)], source, Expression.Constant(arguments.Last.Value + 1)); - requestedCount = arguments.Last.Value; } source = Expression.Call( @@ -273,7 +276,10 @@ public static (Expression, Group>> SelectExpressi }; var createGroup = Expression.MemberInit(Expression.New(groupType), bindings); - return (Expression.Lambda, Group>>(createGroup, group), reverseOrder); + return new BatchExpression( + Expression.Lambda, Group>>(createGroup, group), + arguments.Last is not null, + cursor); static string ReverseOrder(string method) => method switch @@ -292,84 +298,6 @@ static MethodInfo GetEnumerableMethod(string methodName, Type elementType, Lambd .MakeGenericMethod(elementType, keySelector.Body.Type); } - private static MethodCallExpression BuildBatchWhereExpression( - Expression enumerable, - ReadOnlySpan keys, - object?[] cursor, - bool forward) - { - var cursorExpr = new Expression[cursor.Length]; - - for (var i = 0; i < cursor.Length; i++) - { - cursorExpr[i] = CreateParameter(cursor[i], keys[i].Expression.ReturnType); - } - - var handled = new List(); - Expression? expression = null; - - var parameter = Expression.Parameter(typeof(T), "t"); - var zero = Expression.Constant(0); - - for (var i = 0; i < keys.Length; i++) - { - var key = keys[i]; - Expression? current = null; - Expression keyExpr; - - for (var j = 0; j < handled.Count; j++) - { - var handledKey = handled[j]; - - keyExpr = - Expression.Equal( - Expression.Call( - ReplaceParameter(handledKey.Expression, parameter), - handledKey.CompareMethod, - cursorExpr[j]), - zero); - - current = current is null - ? keyExpr - : Expression.AndAlso(current, keyExpr); - } - - var greaterThan = forward - ? key.Direction is CursorKeyDirection.Ascending - : key.Direction is CursorKeyDirection.Descending; - - keyExpr = - greaterThan - ? Expression.GreaterThan( - Expression.Call( - ReplaceParameter(key.Expression, parameter), - key.CompareMethod, - cursorExpr[i]), - zero) - : Expression.LessThan( - Expression.Call( - ReplaceParameter(key.Expression, parameter), - key.CompareMethod, - cursorExpr[i]), - zero); - - current = current is null - ? keyExpr - : Expression.AndAlso(current, keyExpr); - expression = expression is null - ? current - : Expression.OrElse(expression, current); - handled.Add(key); - } - - return Expression.Call( - typeof(Enumerable), - "Where", - [typeof(T)], - enumerable, - Expression.Lambda>(expression!, parameter)); - } - /// /// Extracts and removes the orderBy and thenBy expressions from the given expression tree. /// @@ -504,4 +432,14 @@ private static Expression StripQuotes(Expression e) return e; } } + + internal readonly struct BatchExpression( + Expression, Group>> selectExpression, + bool isBackward, + Cursor? cursor) + { + public Expression, Group>> SelectExpression { get; } = selectExpression; + public bool IsBackward { get; } = isBackward; + public Cursor? Cursor { get; } = cursor; + } } diff --git a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ReverseOrderExpressionRewriter.cs b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ReverseOrderExpressionRewriter.cs new file mode 100644 index 00000000000..45b3eaafe1c --- /dev/null +++ b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ReverseOrderExpressionRewriter.cs @@ -0,0 +1,60 @@ +using System.Linq.Expressions; +using System.Reflection; + +namespace GreenDonut.Data.Expressions; + +public class ReverseOrderExpressionRewriter : ExpressionVisitor +{ + private static readonly MethodInfo _orderByMethod = typeof(Queryable).GetMethods() + .First(m => m.Name == nameof(Queryable.OrderBy) && m.GetParameters().Length == 2); + + private static readonly MethodInfo _orderByDescendingMethod = typeof(Queryable).GetMethods() + .First(m => m.Name == nameof(Queryable.OrderByDescending) && m.GetParameters().Length == 2); + + private static readonly MethodInfo _thenByMethod = typeof(Queryable).GetMethods() + .First(m => m.Name == nameof(Queryable.ThenBy) && m.GetParameters().Length == 2); + + private static readonly MethodInfo _thenByDescendingMethod = typeof(Queryable).GetMethods() + .First(m => m.Name == nameof(Queryable.ThenByDescending) && m.GetParameters().Length == 2); + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + var visitedArguments = node.Arguments.Select(Visit).Cast().ToArray(); + + if (node.Method.Name == nameof(Queryable.OrderBy)) + { + return Expression.Call( + _orderByDescendingMethod.MakeGenericMethod(node.Method.GetGenericArguments()), + visitedArguments); + } + + if (node.Method.Name == nameof(Queryable.OrderByDescending)) + { + return Expression.Call( + _orderByMethod.MakeGenericMethod(node.Method.GetGenericArguments()), + visitedArguments); + } + + if (node.Method.Name == nameof(Queryable.ThenBy)) + { + return Expression.Call( + _thenByDescendingMethod.MakeGenericMethod(node.Method.GetGenericArguments()), + visitedArguments); + } + + if (node.Method.Name == nameof(Queryable.ThenByDescending)) + { + return Expression.Call( + _thenByMethod.MakeGenericMethod(node.Method.GetGenericArguments()), + visitedArguments); + } + + return base.VisitMethodCall(node); + } + + public static IQueryable Rewrite(IQueryable query) + { + var reversedExpression = new ReverseOrderExpressionRewriter().Visit(query.Expression); + return query.Provider.CreateQuery(reversedExpression); + } +} diff --git a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs index bad4d368ed6..410675248ff 100644 --- a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs +++ b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs @@ -95,6 +95,11 @@ public static async ValueTask> ToPageAsync( nameof(arguments)); } + if (arguments.First is null && arguments.Last is null) + { + arguments = arguments with { First = 10 }; + } + if (arguments.EnableRelativeCursors && string.IsNullOrEmpty(arguments.After) && string.IsNullOrEmpty(arguments.Before)) @@ -104,34 +109,43 @@ public static async ValueTask> ToPageAsync( var originalQuery = source; var forward = arguments.Last is null; - var requestedCount = int.MaxValue; - var reverseOrder = false; + var requestedCount = forward ? arguments.First!.Value : arguments.Last!.Value; var offset = 0; int? totalCount = null; + var usesRelativeCursors = false; + Cursor? cursor = null; if (arguments.After is not null) { - var cursor = CursorParser.Parse(arguments.After, keys); - var (whereExpr, cursorOffset, reverse) = BuildWhereExpression(keys, cursor, true); - + cursor = CursorParser.Parse(arguments.After, keys); + var (whereExpr, cursorOffset) = BuildWhereExpression(keys, cursor, true); source = source.Where(whereExpr); offset = cursorOffset; - reverseOrder = reverse; if (!includeTotalCount) { totalCount ??= cursor.TotalCount; } + + if (cursor.IsRelative) + { + usesRelativeCursors = true; + } } if (arguments.Before is not null) { - var cursor = CursorParser.Parse(arguments.Before, keys); - var (whereExpr, cursorOffset, reverse) = BuildWhereExpression(keys, cursor, false); + if (usesRelativeCursors) + { + throw new ArgumentException( + "You cannot use `before` and `after` with relative cursors at the same time.", + nameof(arguments)); + } + cursor = CursorParser.Parse(arguments.Before, keys); + var (whereExpr, cursorOffset) = BuildWhereExpression(keys, cursor, false); source = source.Where(whereExpr); offset = cursorOffset; - reverseOrder = reverse; if (!includeTotalCount) { @@ -139,34 +153,33 @@ public static async ValueTask> ToPageAsync( } } - // Reverse order if offset is negative - if (reverseOrder) - { - source = source.Reverse(); - } - - if (arguments.First is not null) + if (arguments.EnableRelativeCursors && cursor?.IsRelative == true) { - if (offset > 0) + if ((arguments.Last is not null && cursor.Offset > 0) || + (arguments.First is not null && cursor.Offset < 0)) { - source = source.Skip(offset * arguments.First.Value); + throw new ArgumentException( + "Positive offsets are not allowed with `last`, and negative offsets are not allowed with `first`.", + nameof(arguments)); } - - source = source.Take(arguments.First.Value + 1); - requestedCount = arguments.First.Value; } - if (arguments.Last is not null) + var isBackward = arguments.Last is not null; + + if (isBackward) { - if (offset > 0) - { - source = source.Skip(offset * arguments.Last.Value); - } + source = ReverseOrderExpressionRewriter.Rewrite(source); + } + + var absOffset = Math.Abs(offset); - source = source.Reverse().Take(arguments.Last.Value + 1); - requestedCount = arguments.Last.Value; + if (absOffset > 0) + { + source = source.Skip(absOffset * requestedCount); } + source = source.Take(requestedCount + 1); + var builder = ImmutableArray.CreateBuilder(); var fetchCount = 0; @@ -213,24 +226,18 @@ public static async ValueTask> ToPageAsync( return Page.Empty; } - if (!forward ^ reverseOrder) + if (isBackward) { builder.Reverse(); } if (builder.Count > requestedCount) { - if (!forward ^ reverseOrder) - { - builder.RemoveAt(0); - } - else - { - builder.RemoveAt(requestedCount); - } + builder.RemoveAt(isBackward ? 0 : requestedCount); } - return CreatePage(builder.ToImmutable(), arguments, keys, fetchCount, totalCount); + var pageIndex = CreateIndex(arguments, cursor, totalCount); + return CreatePage(builder.ToImmutable(), arguments, keys, fetchCount, pageIndex, totalCount); } /// @@ -442,8 +449,8 @@ public static async ValueTask>> ToBatchPageAsync( + var batchExpression = + BuildBatchExpression( arguments, keys, ordering.OrderExpressions, @@ -455,20 +462,15 @@ public static async ValueTask>> ToBatchPageAsync(ordering.Expression); - TryGetQueryInterceptor()?.OnBeforeExecute(source.GroupBy(keySelector).Select(selectExpression)); + TryGetQueryInterceptor()?.OnBeforeExecute(source.GroupBy(keySelector).Select(batchExpression.SelectExpression)); await foreach (var item in source .GroupBy(keySelector) - .Select(selectExpression) + .Select(batchExpression.SelectExpression) .AsAsyncEnumerable() .WithCancellation(cancellationToken) .ConfigureAwait(false)) { - if (reverseOrder) - { - item.Items.Reverse(); - } - if (item.Items.Count == 0) { map.Add(item.Key, Page.Empty); @@ -478,13 +480,30 @@ public static async ValueTask>> ToBatchPageAsync item.Items.Count ? item.Items.Count : requestedCount; var builder = ImmutableArray.CreateBuilder(itemCount); - for (var i = 0; i < itemCount; i++) + if (batchExpression.IsBackward) { - builder.Add(valueSelector(item.Items[i])); + for (var i = itemCount - 1; i >= 0; i--) + { + builder.Add(valueSelector(item.Items[i])); + } + } + else + { + for (var i = 0; i < itemCount; i++) + { + builder.Add(valueSelector(item.Items[i])); + } } - var totalCount = counts?.GetValueOrDefault(item.Key); - var page = CreatePage(builder.ToImmutable(), arguments, keys, item.Items.Count, totalCount); + var totalCount = counts?.GetValueOrDefault(item.Key) ?? batchExpression.Cursor?.TotalCount; + var pageIndex = CreateIndex(arguments, batchExpression.Cursor, totalCount); + var page = CreatePage( + builder.ToImmutable(), + arguments, + keys, + item.Items.Count, + pageIndex, + totalCount); map.Add(item.Key, page); } @@ -506,7 +525,8 @@ private static async Task> GetBatchCountsAsync t.Key, t => t.Count, cancellationToken); } - private static Expression, CountResult>> GetOrCreateCountSelector() + private static Expression, CountResult>> GetOrCreateCountSelector() { return (Expression, CountResult>>) _countExpressionCache.GetOrAdd( @@ -551,7 +571,8 @@ private static Page CreatePage( PagingArguments arguments, CursorKey[] keys, int fetchCount, - int? totalCount = null) + int? index, + int? totalCount) { var hasPrevious = false; var hasNext = false; @@ -591,7 +612,7 @@ private static Page CreatePage( hasNext, hasPrevious, (item, o, p, c) => CursorFormatter.Format(item, keys, new CursorPageInfo(o, p, c)), - 0, + index ?? 1, totalCount.Value); } @@ -603,6 +624,50 @@ private static Page CreatePage( totalCount); } + private static int? CreateIndex(PagingArguments arguments, Cursor? cursor, int? totalCount) + { + if (totalCount is not null + && arguments.Last is not null + && arguments.After is null + && arguments.Before is null) + { + return Math.Max(1, (int)Math.Ceiling(totalCount.Value / (double)arguments.Last.Value)); + } + + if (cursor?.IsRelative != true) + { + return null; + } + + if (arguments.After is not null) + { + if (arguments.First is not null) + { + return (cursor.PageIndex ?? 1) + (cursor.Offset ?? 0) + 1; + } + + if (arguments.Last is not null && totalCount is not null) + { + return Math.Max(1, (int)Math.Ceiling(totalCount.Value / (double)arguments.Last.Value)); + } + } + + if (arguments.Before is not null) + { + if (arguments.First is not null) + { + return 1; + } + + if (arguments.Last is not null) + { + return (cursor.PageIndex ?? 1) - Math.Abs(cursor.Offset ?? 0) - 1; + } + } + + return null; + } + private static CursorKey[] ParseDataSetKeys(IQueryable source) { var parser = new CursorKeyParser(); diff --git a/src/GreenDonut/src/GreenDonut.Data.Primitives/Page.cs b/src/GreenDonut/src/GreenDonut.Data.Primitives/Page.cs index ce9ba0951ac..8218e925b73 100644 --- a/src/GreenDonut/src/GreenDonut.Data.Primitives/Page.cs +++ b/src/GreenDonut/src/GreenDonut.Data.Primitives/Page.cs @@ -141,7 +141,7 @@ public string CreateCursor(T item, int offset) throw new InvalidOperationException("This page does not allow relative cursors."); } - return _createCursor(item, offset, _index ?? 0, _totalCount ?? 0); + return _createCursor(item, offset, _index ?? 1, _totalCount ?? 0); } /// diff --git a/src/GreenDonut/src/GreenDonut.Data/Cursors/Cursor.cs b/src/GreenDonut/src/GreenDonut.Data/Cursors/Cursor.cs index 83ce30b8200..0f5deee7605 100644 --- a/src/GreenDonut/src/GreenDonut.Data/Cursors/Cursor.cs +++ b/src/GreenDonut/src/GreenDonut.Data/Cursors/Cursor.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; namespace GreenDonut.Data.Cursors; @@ -22,4 +23,8 @@ public record Cursor( ImmutableArray Values, int? Offset = null, int? PageIndex = null, - int? TotalCount = null); + int? TotalCount = null) +{ + [MemberNotNullWhen(true, nameof(Offset), nameof(PageIndex), nameof(TotalCount))] + public bool IsRelative => Offset.HasValue && PageIndex.HasValue && TotalCount.HasValue; +} diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs index bdc2625559e..f2cfc2ec440 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs @@ -210,7 +210,7 @@ public async Task QueryContext_Simple_Selector() await using var context = new CatalogContext(connectionString); - var page = await context.Products + await context.Products .With(query) .ToPageAsync(arguments); diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/RelativeCursorTests.cs b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/RelativeCursorTests.cs new file mode 100644 index 00000000000..9ecbbfb8b37 --- /dev/null +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/RelativeCursorTests.cs @@ -0,0 +1,1297 @@ +#if NET9_0_OR_GREATER +using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; +using Squadron; + +namespace GreenDonut.Data; + +[Collection(PostgresCacheCollectionFixture.DefinitionName)] +public class RelativeCursorTests(PostgreSqlResource resource) +{ + public PostgreSqlResource Resource { get; } = resource; + + private string CreateConnectionString() + => Resource.GetConnectionString($"db_{Guid.NewGuid():N}"); + + [Fact] + public async Task Fetch_Second_Page() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(2) { EnableRelativeCursors = true }; + var first = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { After = first.CreateCursor(first.Last!, 0) }; + var second = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Assert + + /* + 1. Aetherix + 2. Brightex <- Cursor + 3. Celestara <- Page 2 - Item 1 + 4. Dynamova <- Page 2 - Item 2 + 5. Evolvance + 6. Futurova + */ + + Snapshot.Create() + .Add(new { Page = second.Index, second.TotalCount, Items = second.Items.Select(t => t.Name).ToArray() }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 2, + "TotalCount": 20, + "Items": [ + "Celestara", + "Dynamova" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Brightex' + -- @__value_1='2' + -- @__p_2='3' + SELECT b."Id", b."GroupId", b."Name" + FROM "Brands" AS b + WHERE b."Name" > @__value_0 OR (b."Name" = @__value_0 AND b."Id" > @__value_1) + ORDER BY b."Name", b."Id" + LIMIT @__p_2 + --------------- + + """); + } + + [Fact] + public async Task BatchFetch_Second_Page() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(2) { EnableRelativeCursors = true }; + var first = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { After = first.CreateCursor(first.Last!, 0) }; + var map = await context.Brands.Where(t => t.GroupId == 1).OrderBy(t => t.Name).ThenBy(t => t.Id) + .ToBatchPageAsync(t => t.GroupId, arguments); + var second = map[1]; + + // Assert + + /* + 1. Aetherix + 2. Brightex <- Cursor + 3. Celestara <- Page 2 - Item 1 + 4. Dynamova <- Page 2 - Item 2 + 5. Evolvance + 6. Futurova + */ + + Snapshot.Create() + .Add(new { Page = second.Index, second.TotalCount, Items = second.Items.Select(t => t.Name).ToArray() }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 2, + "TotalCount": 20, + "Items": [ + "Celestara", + "Dynamova" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Brightex' + -- @__value_1='2' + SELECT b1."GroupId", b3."Id", b3."GroupId", b3."Name" + FROM ( + SELECT b."GroupId" + FROM "Brands" AS b + WHERE b."GroupId" = 1 + GROUP BY b."GroupId" + ) AS b1 + LEFT JOIN ( + SELECT b2."Id", b2."GroupId", b2."Name" + FROM ( + SELECT b0."Id", b0."GroupId", b0."Name", ROW_NUMBER() OVER(PARTITION BY b0."GroupId" ORDER BY b0."Name", b0."Id") AS row + FROM "Brands" AS b0 + WHERE b0."GroupId" = 1 AND (b0."Name" > @__value_0 OR (b0."Name" = @__value_0 AND b0."Id" > @__value_1)) + ) AS b2 + WHERE b2.row <= 3 + ) AS b3 ON b1."GroupId" = b3."GroupId" + ORDER BY b1."GroupId", b3."GroupId", b3."Name", b3."Id" + --------------- + + """); + } + + [Fact] + public async Task Fetch_Third_Page_With_Offset_1() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(2) { EnableRelativeCursors = true }; + var first = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { After = first.CreateCursor(first.Last!, 1) }; + var second = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Assert + + /* + 1. Aetherix + 2. Brightex <- Cursor + 3. Celestara + 4. Dynamova + 5. Evolvance <- Page 3 - Item 1 + 6. Futurova <- Page 3 - Item 2 + */ + + Snapshot.Create() + .Add(new { Page = second.Index, second.TotalCount, Items = second.Items.Select(t => t.Name).ToArray() }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 3, + "TotalCount": 20, + "Items": [ + "Evolvance", + "Futurova" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Brightex' + -- @__value_1='2' + -- @__p_3='3' + -- @__p_2='2' + SELECT b."Id", b."GroupId", b."Name" + FROM "Brands" AS b + WHERE b."Name" > @__value_0 OR (b."Name" = @__value_0 AND b."Id" > @__value_1) + ORDER BY b."Name", b."Id" + LIMIT @__p_3 OFFSET @__p_2 + --------------- + + """); + } + + [Fact] + public async Task BatchFetch_Third_Page_With_Offset_1() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(2) { EnableRelativeCursors = true }; + var first = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { After = first.CreateCursor(first.Last!, 1) }; + var map = await context.Brands.Where(t => t.GroupId == 1).OrderBy(t => t.Name).ThenBy(t => t.Id) + .ToBatchPageAsync(t => t.GroupId, arguments); + var second = map[1]; + + // Assert + + /* + 1. Aetherix + 2. Brightex <- Cursor + 3. Celestara + 4. Dynamova + 5. Evolvance <- Page 3 - Item 1 + 6. Futurova <- Page 3 - Item 2 + */ + + Snapshot.Create() + .Add(new { Page = second.Index, second.TotalCount, Items = second.Items.Select(t => t.Name).ToArray() }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 3, + "TotalCount": 20, + "Items": [ + "Evolvance", + "Futurova" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Brightex' + -- @__value_1='2' + SELECT b1."GroupId", b3."Id", b3."GroupId", b3."Name" + FROM ( + SELECT b."GroupId" + FROM "Brands" AS b + WHERE b."GroupId" = 1 + GROUP BY b."GroupId" + ) AS b1 + LEFT JOIN ( + SELECT b2."Id", b2."GroupId", b2."Name" + FROM ( + SELECT b0."Id", b0."GroupId", b0."Name", ROW_NUMBER() OVER(PARTITION BY b0."GroupId" ORDER BY b0."Name", b0."Id") AS row + FROM "Brands" AS b0 + WHERE b0."GroupId" = 1 AND (b0."Name" > @__value_0 OR (b0."Name" = @__value_0 AND b0."Id" > @__value_1)) + ) AS b2 + WHERE 2 < b2.row AND b2.row <= 5 + ) AS b3 ON b1."GroupId" = b3."GroupId" + ORDER BY b1."GroupId", b3."GroupId", b3."Name", b3."Id" + --------------- + + """); + } + + [Fact] + public async Task Fetch_Fourth_Page_With_Offset_1() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(2) { EnableRelativeCursors = true }; + var first = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + arguments = arguments with { After = first.CreateCursor(first.Last!, 0) }; + var second = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { After = second.CreateCursor(second.Last!, 1) }; + var fourth = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Assert + + /* + 1. Aetherix + 2. Brightex + 3. Celestara + 4. Dynamova <- Cursor + 5. Evolvance + 6. Futurova + 7. Glacient <- Page 4 - Item 1 + 8. Hyperionix <- Page 4 - Item 2 + */ + + Snapshot.Create() + .Add(new { Page = fourth.Index, fourth.TotalCount, Items = fourth.Items.Select(t => t.Name).ToArray() }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 4, + "TotalCount": 20, + "Items": [ + "Glacient", + "Hyperionix" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Dynamova' + -- @__value_1='4' + -- @__p_3='3' + -- @__p_2='2' + SELECT b."Id", b."GroupId", b."Name" + FROM "Brands" AS b + WHERE b."Name" > @__value_0 OR (b."Name" = @__value_0 AND b."Id" > @__value_1) + ORDER BY b."Name", b."Id" + LIMIT @__p_3 OFFSET @__p_2 + --------------- + + """); + } + + [Fact] + public async Task BatchFetch_Fourth_Page_With_Offset_1() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(2) { EnableRelativeCursors = true }; + var first = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + arguments = arguments with { After = first.CreateCursor(first.Last!, 0) }; + var second = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { After = second.CreateCursor(second.Last!, 1) }; + var map = await context.Brands.Where(t => t.GroupId == 1).OrderBy(t => t.Name).ThenBy(t => t.Id) + .ToBatchPageAsync(t => t.GroupId, arguments); + var fourth = map[1]; + + // Assert + + /* + 1. Aetherix + 2. Brightex + 3. Celestara + 4. Dynamova <- Cursor + 5. Evolvance + 6. Futurova + 7. Glacient <- Page 4 - Item 1 + 8. Hyperionix <- Page 4 - Item 2 + */ + + Snapshot.Create() + .Add(new { Page = fourth.Index, fourth.TotalCount, Items = fourth.Items.Select(t => t.Name).ToArray() }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 4, + "TotalCount": 20, + "Items": [ + "Glacient", + "Hyperionix" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Dynamova' + -- @__value_1='4' + SELECT b1."GroupId", b3."Id", b3."GroupId", b3."Name" + FROM ( + SELECT b."GroupId" + FROM "Brands" AS b + WHERE b."GroupId" = 1 + GROUP BY b."GroupId" + ) AS b1 + LEFT JOIN ( + SELECT b2."Id", b2."GroupId", b2."Name" + FROM ( + SELECT b0."Id", b0."GroupId", b0."Name", ROW_NUMBER() OVER(PARTITION BY b0."GroupId" ORDER BY b0."Name", b0."Id") AS row + FROM "Brands" AS b0 + WHERE b0."GroupId" = 1 AND (b0."Name" > @__value_0 OR (b0."Name" = @__value_0 AND b0."Id" > @__value_1)) + ) AS b2 + WHERE 2 < b2.row AND b2.row <= 5 + ) AS b3 ON b1."GroupId" = b3."GroupId" + ORDER BY b1."GroupId", b3."GroupId", b3."Name", b3."Id" + --------------- + + """); + } + + [Fact] + public async Task Fetch_Fourth_Page_With_Offset_2() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(2) { EnableRelativeCursors = true }; + var first = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { After = first.CreateCursor(first.Last!, 2) }; + var fourth = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Assert + + /* + 1. Aetherix + 2. Brightex <- Cursor + 3. Celestara + 4. Dynamova + 5. Evolvance + 6. Futurova + 7. Glacient <- Page 4 - Item 1 + 8. Hyperionix <- Page 4 - Item 2 + */ + + Snapshot.Create() + .Add(new { Page = fourth.Index, fourth.TotalCount, Items = fourth.Items.Select(t => t.Name).ToArray() }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 4, + "TotalCount": 20, + "Items": [ + "Glacient", + "Hyperionix" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Brightex' + -- @__value_1='2' + -- @__p_3='3' + -- @__p_2='4' + SELECT b."Id", b."GroupId", b."Name" + FROM "Brands" AS b + WHERE b."Name" > @__value_0 OR (b."Name" = @__value_0 AND b."Id" > @__value_1) + ORDER BY b."Name", b."Id" + LIMIT @__p_3 OFFSET @__p_2 + --------------- + + """); + } + + [Fact] + public async Task BatchFetch_Fourth_Page_With_Offset_2() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(2) { EnableRelativeCursors = true }; + var first = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { After = first.CreateCursor(first.Last!, 2) }; + var map = await context.Brands.Where(t => t.GroupId == 1).OrderBy(t => t.Name).ThenBy(t => t.Id) + .ToBatchPageAsync(t => t.GroupId, arguments); + var fourth = map[1]; + + // Assert + + /* + 1. Aetherix + 2. Brightex <- Cursor + 3. Celestara + 4. Dynamova + 5. Evolvance + 6. Futurova + 7. Glacient <- Page 4 - Item 1 + 8. Hyperionix <- Page 4 - Item 2 + */ + + Snapshot.Create() + .Add(new { Page = fourth.Index, fourth.TotalCount, Items = fourth.Items.Select(t => t.Name).ToArray() }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 4, + "TotalCount": 20, + "Items": [ + "Glacient", + "Hyperionix" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Brightex' + -- @__value_1='2' + SELECT b1."GroupId", b3."Id", b3."GroupId", b3."Name" + FROM ( + SELECT b."GroupId" + FROM "Brands" AS b + WHERE b."GroupId" = 1 + GROUP BY b."GroupId" + ) AS b1 + LEFT JOIN ( + SELECT b2."Id", b2."GroupId", b2."Name" + FROM ( + SELECT b0."Id", b0."GroupId", b0."Name", ROW_NUMBER() OVER(PARTITION BY b0."GroupId" ORDER BY b0."Name", b0."Id") AS row + FROM "Brands" AS b0 + WHERE b0."GroupId" = 1 AND (b0."Name" > @__value_0 OR (b0."Name" = @__value_0 AND b0."Id" > @__value_1)) + ) AS b2 + WHERE 4 < b2.row AND b2.row <= 7 + ) AS b3 ON b1."GroupId" = b3."GroupId" + ORDER BY b1."GroupId", b3."GroupId", b3."Name", b3."Id" + --------------- + + """); + } + + [Fact] + public async Task Fetch_Second_To_Last_Page_Offset_0() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(last: 2) { EnableRelativeCursors = true }; + var last = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { Before = last.CreateCursor(last.First!, 0) }; + var secondToLast = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Assert + + /* + 14. Nebularis + 15. Omniflex + 16. Pulsarix + 17. Quantumis <- Selected - Item 1 + 18. Radiantum <- Selected - Item 2 + 19. Synerflux <- Cursor + 20. Vertexis + */ + + Snapshot.Create() + .Add(new + { + Page = secondToLast.Index, + secondToLast.TotalCount, + Items = secondToLast.Items.Select(t => t.Name).ToArray() + }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 9, + "TotalCount": 20, + "Items": [ + "Quantumis", + "Radiantum" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Synerflux' + -- @__value_1='19' + -- @__p_2='3' + SELECT b."Id", b."GroupId", b."Name" + FROM "Brands" AS b + WHERE b."Name" < @__value_0 OR (b."Name" = @__value_0 AND b."Id" < @__value_1) + ORDER BY b."Name" DESC, b."Id" DESC + LIMIT @__p_2 + --------------- + + """); + } + + [Fact] + public async Task BatchFetch_Second_To_Last_Page_Offset_0() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(last: 2) { EnableRelativeCursors = true }; + var last = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { Before = last.CreateCursor(last.First!, 0) }; + var map = await context.Brands.Where(t => t.GroupId == 2).OrderBy(t => t.Name).ThenBy(t => t.Id) + .ToBatchPageAsync(t => t.GroupId, arguments); + var secondToLast = map[2]; + + // Assert + + /* + 14. Nebularis + 15. Omniflex + 16. Pulsarix + 17. Quantumis <- Selected - Item 1 + 18. Radiantum <- Selected - Item 2 + 19. Synerflux <- Cursor + 20. Vertexis + */ + + Snapshot.Create() + .Add(new + { + Page = secondToLast.Index, + secondToLast.TotalCount, + Items = secondToLast.Items.Select(t => t.Name).ToArray() + }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 9, + "TotalCount": 20, + "Items": [ + "Quantumis", + "Radiantum" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Synerflux' + -- @__value_1='19' + SELECT b1."GroupId", b3."Id", b3."GroupId", b3."Name" + FROM ( + SELECT b."GroupId" + FROM "Brands" AS b + WHERE b."GroupId" = 2 + GROUP BY b."GroupId" + ) AS b1 + LEFT JOIN ( + SELECT b2."Id", b2."GroupId", b2."Name" + FROM ( + SELECT b0."Id", b0."GroupId", b0."Name", ROW_NUMBER() OVER(PARTITION BY b0."GroupId" ORDER BY b0."Name" DESC, b0."Id" DESC) AS row + FROM "Brands" AS b0 + WHERE b0."GroupId" = 2 AND (b0."Name" < @__value_0 OR (b0."Name" = @__value_0 AND b0."Id" < @__value_1)) + ) AS b2 + WHERE b2.row <= 3 + ) AS b3 ON b1."GroupId" = b3."GroupId" + ORDER BY b1."GroupId", b3."GroupId", b3."Name" DESC, b3."Id" DESC + --------------- + + """); + } + + [Fact] + public async Task Fetch_Third_To_Last_Page_Offset_Negative_1() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(last: 2) { EnableRelativeCursors = true }; + var last = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { Before = last.CreateCursor(last.First!, -1) }; + var thirdToLast = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Assert + + /* + 14. Nebularis + 15. Omniflex <- Selected - Item 1 + 16. Pulsarix <- Selected - Item 2 + 17. Quantumis + 18. Radiantum + 19. Synerflux <- Cursor + 20. Vertexis + */ + + Snapshot.Create() + .Add(new + { + Page = thirdToLast.Index, + thirdToLast.TotalCount, + Items = thirdToLast.Items.Select(t => t.Name).ToArray() + }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 8, + "TotalCount": 20, + "Items": [ + "Omniflex", + "Pulsarix" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Synerflux' + -- @__value_1='19' + -- @__p_3='3' + -- @__p_2='2' + SELECT b."Id", b."GroupId", b."Name" + FROM "Brands" AS b + WHERE b."Name" < @__value_0 OR (b."Name" = @__value_0 AND b."Id" < @__value_1) + ORDER BY b."Name" DESC, b."Id" DESC + LIMIT @__p_3 OFFSET @__p_2 + --------------- + + """); + } + + [Fact] + public async Task BatchFetch_Third_To_Last_Page_Offset_Negative_1() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(last: 2) { EnableRelativeCursors = true }; + var last = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { Before = last.CreateCursor(last.First!, -1) }; + var map = await context.Brands.Where(t => t.GroupId == 2).OrderBy(t => t.Name).ThenBy(t => t.Id) + .ToBatchPageAsync(t => t.GroupId, arguments); + var thirdToLast = map[2]; + + // Assert + + /* + 14. Nebularis + 15. Omniflex <- Selected - Item 1 + 16. Pulsarix <- Selected - Item 2 + 17. Quantumis + 18. Radiantum + 19. Synerflux <- Cursor + 20. Vertexis + */ + + Snapshot.Create() + .Add(new + { + Page = thirdToLast.Index, + thirdToLast.TotalCount, + Items = thirdToLast.Items.Select(t => t.Name).ToArray() + }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 8, + "TotalCount": 20, + "Items": [ + "Omniflex", + "Pulsarix" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Synerflux' + -- @__value_1='19' + SELECT b1."GroupId", b3."Id", b3."GroupId", b3."Name" + FROM ( + SELECT b."GroupId" + FROM "Brands" AS b + WHERE b."GroupId" = 2 + GROUP BY b."GroupId" + ) AS b1 + LEFT JOIN ( + SELECT b2."Id", b2."GroupId", b2."Name" + FROM ( + SELECT b0."Id", b0."GroupId", b0."Name", ROW_NUMBER() OVER(PARTITION BY b0."GroupId" ORDER BY b0."Name" DESC, b0."Id" DESC) AS row + FROM "Brands" AS b0 + WHERE b0."GroupId" = 2 AND (b0."Name" < @__value_0 OR (b0."Name" = @__value_0 AND b0."Id" < @__value_1)) + ) AS b2 + WHERE 2 < b2.row AND b2.row <= 5 + ) AS b3 ON b1."GroupId" = b3."GroupId" + ORDER BY b1."GroupId", b3."GroupId", b3."Name" DESC, b3."Id" DESC + --------------- + + """); + } + + [Fact] + public async Task Fetch_Fourth_To_Last_Page_Offset_Negative_2() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(last: 2) { EnableRelativeCursors = true }; + var last = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { Before = last.CreateCursor(last.First!, -2) }; + var thirdToLast = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Assert + + /* + 11. Kinetiq + 12. Luminara + 13. Momentumix <- Selected - Item 1 + 14. Nebularis <- Selected - Item 2 + 15. Omniflex + 16. Pulsarix + 17. Quantumis + 18. Radiantum + 19. Synerflux <- Cursor + 20. Vertexis + */ + + Snapshot.Create() + .Add(new + { + Page = thirdToLast.Index, + thirdToLast.TotalCount, + Items = thirdToLast.Items.Select(t => t.Name).ToArray() + }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 7, + "TotalCount": 20, + "Items": [ + "Momentumix", + "Nebularis" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Synerflux' + -- @__value_1='19' + -- @__p_3='3' + -- @__p_2='4' + SELECT b."Id", b."GroupId", b."Name" + FROM "Brands" AS b + WHERE b."Name" < @__value_0 OR (b."Name" = @__value_0 AND b."Id" < @__value_1) + ORDER BY b."Name" DESC, b."Id" DESC + LIMIT @__p_3 OFFSET @__p_2 + --------------- + + """); + } + + [Fact] + public async Task BatchFetch_Fourth_To_Last_Page_Offset_Negative_2() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(last: 2) { EnableRelativeCursors = true }; + var last = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { Before = last.CreateCursor(last.First!, -2) }; + var map = await context.Brands.Where(t => t.GroupId == 2).OrderBy(t => t.Name).ThenBy(t => t.Id) + .ToBatchPageAsync(t => t.GroupId, arguments); + var thirdToLast = map[2]; + + // Assert + + /* + 11. Kinetiq + 12. Luminara + 13. Momentumix <- Selected - Item 1 + 14. Nebularis <- Selected - Item 2 + 15. Omniflex + 16. Pulsarix + 17. Quantumis + 18. Radiantum + 19. Synerflux <- Cursor + 20. Vertexis + */ + + Snapshot.Create() + .Add(new + { + Page = thirdToLast.Index, + thirdToLast.TotalCount, + Items = thirdToLast.Items.Select(t => t.Name).ToArray() + }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 7, + "TotalCount": 20, + "Items": [ + "Momentumix", + "Nebularis" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Synerflux' + -- @__value_1='19' + SELECT b1."GroupId", b3."Id", b3."GroupId", b3."Name" + FROM ( + SELECT b."GroupId" + FROM "Brands" AS b + WHERE b."GroupId" = 2 + GROUP BY b."GroupId" + ) AS b1 + LEFT JOIN ( + SELECT b2."Id", b2."GroupId", b2."Name" + FROM ( + SELECT b0."Id", b0."GroupId", b0."Name", ROW_NUMBER() OVER(PARTITION BY b0."GroupId" ORDER BY b0."Name" DESC, b0."Id" DESC) AS row + FROM "Brands" AS b0 + WHERE b0."GroupId" = 2 AND (b0."Name" < @__value_0 OR (b0."Name" = @__value_0 AND b0."Id" < @__value_1)) + ) AS b2 + WHERE 4 < b2.row AND b2.row <= 7 + ) AS b3 ON b1."GroupId" = b3."GroupId" + ORDER BY b1."GroupId", b3."GroupId", b3."Name" DESC, b3."Id" DESC + --------------- + + """); + } + + [Fact] + public async Task Fetch_Fourth_To_Last_Page_From_Second_To_Last_Page_Offset_Negative_1() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(last: 2) { EnableRelativeCursors = true }; + var last = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + arguments = arguments with { Before = last.CreateCursor(last.First!, 0) }; + var secondToLast = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { Before = secondToLast.CreateCursor(secondToLast.First!, -1) }; + var fourthToLast = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Assert + + /* + 11. Kinetiq + 12. Luminara + 13. Momentumix <- Selected - Item 1 + 14. Nebularis <- Selected - Item 2 + 15. Omniflex + 16. Pulsarix + 17. Quantumis <- Cursor + 18. Radiantum + 19. Synerflux + 20. Vertexis + */ + + Snapshot.Create() + .Add(new + { + Page = fourthToLast.Index, + fourthToLast.TotalCount, + Items = fourthToLast.Items.Select(t => t.Name).ToArray() + }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 7, + "TotalCount": 20, + "Items": [ + "Momentumix", + "Nebularis" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Quantumis' + -- @__value_1='17' + -- @__p_3='3' + -- @__p_2='2' + SELECT b."Id", b."GroupId", b."Name" + FROM "Brands" AS b + WHERE b."Name" < @__value_0 OR (b."Name" = @__value_0 AND b."Id" < @__value_1) + ORDER BY b."Name" DESC, b."Id" DESC + LIMIT @__p_3 OFFSET @__p_2 + --------------- + + """); + } + + [Fact] + public async Task BatchFetch_Fourth_To_Last_Page_From_Second_To_Last_Page_Offset_Negative_1() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(last: 2) { EnableRelativeCursors = true }; + var last = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + arguments = arguments with { Before = last.CreateCursor(last.First!, 0) }; + var secondToLast = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + using var capture = new CapturePagingQueryInterceptor(); + arguments = arguments with { Before = secondToLast.CreateCursor(secondToLast.First!, -1) }; + var map = await context.Brands.Where(t => t.GroupId == 2).OrderBy(t => t.Name).ThenBy(t => t.Id) + .ToBatchPageAsync(t => t.GroupId, arguments); + var fourthToLast = map[2]; + + // Assert + + /* + 11. Kinetiq + 12. Luminara + 13. Momentumix <- Selected - Item 1 + 14. Nebularis <- Selected - Item 2 + 15. Omniflex + 16. Pulsarix + 17. Quantumis <- Cursor + 18. Radiantum + 19. Synerflux + 20. Vertexis + */ + + Snapshot.Create() + .Add(new + { + Page = fourthToLast.Index, + fourthToLast.TotalCount, + Items = fourthToLast.Items.Select(t => t.Name).ToArray() + }) + .AddSql(capture) + .MatchInline( + """ + --------------- + { + "Page": 7, + "TotalCount": 20, + "Items": [ + "Momentumix", + "Nebularis" + ] + } + --------------- + + SQL 0 + --------------- + -- @__value_0='Quantumis' + -- @__value_1='17' + SELECT b1."GroupId", b3."Id", b3."GroupId", b3."Name" + FROM ( + SELECT b."GroupId" + FROM "Brands" AS b + WHERE b."GroupId" = 2 + GROUP BY b."GroupId" + ) AS b1 + LEFT JOIN ( + SELECT b2."Id", b2."GroupId", b2."Name" + FROM ( + SELECT b0."Id", b0."GroupId", b0."Name", ROW_NUMBER() OVER(PARTITION BY b0."GroupId" ORDER BY b0."Name" DESC, b0."Id" DESC) AS row + FROM "Brands" AS b0 + WHERE b0."GroupId" = 2 AND (b0."Name" < @__value_0 OR (b0."Name" = @__value_0 AND b0."Id" < @__value_1)) + ) AS b2 + WHERE 2 < b2.row AND b2.row <= 5 + ) AS b3 ON b1."GroupId" = b3."GroupId" + ORDER BY b1."GroupId", b3."GroupId", b3."Name" DESC, b3."Id" DESC + --------------- + + """); + } + + [Fact] + public async Task Fetch_Backward_With_Positive_Offset() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(last: 2) { EnableRelativeCursors = true }; + var last = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + arguments = arguments with { Before = last.CreateCursor(last.First!, 0) }; + var secondToLast = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + arguments = arguments with { Before = secondToLast.CreateCursor(secondToLast.First!, 0) }; + var thirdToLast = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + arguments = arguments with { Before = thirdToLast.CreateCursor(thirdToLast.First!, 1) }; + + async Task Error() + { + await using var ctx = new TestContext(connectionString); + await ctx.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + } + + // Assert + + await Assert.ThrowsAsync(Error); + } + + [Fact] + public async Task BatchFetch_Backward_With_Positive_Offset() + { + // Arrange + + var connectionString = CreateConnectionString(); + await SeedAsync(connectionString); + + await using var context = new TestContext(connectionString); + var arguments = new PagingArguments(last: 2) { EnableRelativeCursors = true }; + var last = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + arguments = arguments with { Before = last.CreateCursor(last.First!, 0) }; + var secondToLast = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + arguments = arguments with { Before = secondToLast.CreateCursor(secondToLast.First!, 0) }; + var thirdToLast = await context.Brands.OrderBy(t => t.Name).ThenBy(t => t.Id).ToPageAsync(arguments); + + // Act + + arguments = arguments with { Before = thirdToLast.CreateCursor(thirdToLast.First!, 1) }; + + async Task Error() + { + await using var ctx = new TestContext(connectionString); + await ctx.Brands.Where(t => t.GroupId == 2).OrderBy(t => t.Name).ThenBy(t => t.Id) + .ToBatchPageAsync(t => t.GroupId, arguments); + } + + // Assert + + await Assert.ThrowsAsync(Error); + } + + private static async Task SeedAsync(string connectionString) + { + await using var context = new TestContext(connectionString); + await context.Database.EnsureCreatedAsync(); + + /* + 1. Aetherix + 2. Brightex + 3. Celestara + 4. Dynamova + 5. Evolvance + 6. Futurova + 7. Glacient + 8. Hyperionix + 9. Innovexa + 10. Joventra + 11. Kinetiq + 12. Luminara + 13. Momentumix + 14. Nebularis + 15. Omniflex + 16. Pulsarix + 17. Quantumis + 18. Radiantum + 19. Synerflux + 20. Vertexis + */ + + context.Brands.Add(new Brand { Name = "Aetherix", GroupId = 1 }); + context.Brands.Add(new Brand { Name = "Brightex", GroupId = 1 }); + context.Brands.Add(new Brand { Name = "Celestara", GroupId = 1 }); + context.Brands.Add(new Brand { Name = "Dynamova", GroupId = 1 }); + context.Brands.Add(new Brand { Name = "Evolvance", GroupId = 1 }); + context.Brands.Add(new Brand { Name = "Futurova", GroupId = 1 }); + context.Brands.Add(new Brand { Name = "Glacient", GroupId = 1 }); + context.Brands.Add(new Brand { Name = "Hyperionix", GroupId = 1 }); + context.Brands.Add(new Brand { Name = "Innovexa", GroupId = 1 }); + context.Brands.Add(new Brand { Name = "Joventra", GroupId = 1 }); + + context.Brands.Add(new Brand { Name = "Kinetiq", GroupId = 2 }); + context.Brands.Add(new Brand { Name = "Luminara", GroupId = 2 }); + context.Brands.Add(new Brand { Name = "Momentumix", GroupId = 2 }); + context.Brands.Add(new Brand { Name = "Nebularis", GroupId = 2 }); + context.Brands.Add(new Brand { Name = "Omniflex", GroupId = 2 }); + context.Brands.Add(new Brand { Name = "Pulsarix", GroupId = 2 }); + context.Brands.Add(new Brand { Name = "Quantumis", GroupId = 2 }); + context.Brands.Add(new Brand { Name = "Radiantum", GroupId = 2 }); + context.Brands.Add(new Brand { Name = "Synerflux", GroupId = 2 }); + context.Brands.Add(new Brand { Name = "Vertexis", GroupId = 2 }); + + await context.SaveChangesAsync(); + } + + public class TestContext(string connectionString) : DbContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseNpgsql(connectionString); + } + + public DbSet Brands => Set(); + } + + public class Brand + { + public int GroupId { get; set; } + + public int Id { get; set; } + + [MaxLength(100)] public required string Name { get; set; } + } +} +#endif diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/SnapshotExtensions.cs b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/SnapshotExtensions.cs index 84edd1af410..ec2cc397d43 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/SnapshotExtensions.cs +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/SnapshotExtensions.cs @@ -15,4 +15,16 @@ public static Snapshot AddQueries( return snapshot; } + + public static Snapshot AddSql( + this Snapshot snapshot, + CapturePagingQueryInterceptor interceptor) + { + for (var i = 0; i < interceptor.Queries.Count; i++) + { + snapshot.Add(interceptor.Queries[i].QueryText, $"SQL {i}", "sql"); + } + + return snapshot; + } } diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET8_0.md index 55d657dc0c0..8226106a616 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET8_0.md @@ -4,12 +4,12 @@ ```json { - "First": "e30xMDA=", - "Last": "e305OQ==", + "First": "e305OQ==", + "Last": "e30xMDA=", "Items": [ { - "Id": 100, - "Name": "Product 0-99", + "Id": 99, + "Name": "Product 0-98", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -23,8 +23,8 @@ "OnReorder": false }, { - "Id": 99, - "Name": "Product 0-98", + "Id": 100, + "Name": "Product 0-99", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -45,12 +45,12 @@ ```json { - "First": "e30yMDA=", - "Last": "e30xOTk=", + "First": "e30xOTk=", + "Last": "e30yMDA=", "Items": [ { - "Id": 200, - "Name": "Product 1-99", + "Id": 199, + "Name": "Product 1-98", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -64,8 +64,8 @@ "OnReorder": false }, { - "Id": 199, - "Name": "Product 1-98", + "Id": 200, + "Name": "Product 1-99", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -86,12 +86,12 @@ ```json { - "First": "e30zMDA=", - "Last": "e30yOTk=", + "First": "e30yOTk=", + "Last": "e30zMDA=", "Items": [ { - "Id": 300, - "Name": "Product 2-99", + "Id": 299, + "Name": "Product 2-98", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -105,8 +105,8 @@ "OnReorder": false }, { - "Id": 299, - "Name": "Product 2-98", + "Id": 300, + "Name": "Product 2-99", "Description": null, "Price": 0.0, "ImageFileName": null, diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md index 9742f19d2d3..9d8a3d5ea4d 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md @@ -4,12 +4,12 @@ ```json { - "First": "e30xMDA=", - "Last": "e305OQ==", + "First": "e305OQ==", + "Last": "e30xMDA=", "Items": [ { - "Id": 100, - "Name": "Product 0-99", + "Id": 99, + "Name": "Product 0-98", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -23,8 +23,8 @@ "OnReorder": false }, { - "Id": 99, - "Name": "Product 0-98", + "Id": 100, + "Name": "Product 0-99", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -45,12 +45,12 @@ ```json { - "First": "e30yMDA=", - "Last": "e30xOTk=", + "First": "e30xOTk=", + "Last": "e30yMDA=", "Items": [ { - "Id": 200, - "Name": "Product 1-99", + "Id": 199, + "Name": "Product 1-98", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -64,8 +64,8 @@ "OnReorder": false }, { - "Id": 199, - "Name": "Product 1-98", + "Id": 200, + "Name": "Product 1-99", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -86,12 +86,12 @@ ```json { - "First": "e30zMDA=", - "Last": "e30yOTk=", + "First": "e30yOTk=", + "Last": "e30zMDA=", "Items": [ { - "Id": 300, - "Name": "Product 2-99", + "Id": 299, + "Name": "Product 2-98", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -105,8 +105,8 @@ "OnReorder": false }, { - "Id": 299, - "Name": "Product 2-98", + "Id": 300, + "Name": "Product 2-99", "Description": null, "Price": 0.0, "ImageFileName": null, diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET8_0.md index 7ffec45316c..d0c923b24a6 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET8_0.md @@ -4,8 +4,8 @@ ```json { - "First": "ezB8MHwxMDB9MQ==", - "Last": "ezB8MHwxMDB9Mg==", + "First": "ezB8MXwxMDB9MQ==", + "Last": "ezB8MXwxMDB9Mg==", "Items": [ { "Id": 1, @@ -45,8 +45,8 @@ ```json { - "First": "ezB8MHwxMDB9MTAx", - "Last": "ezB8MHwxMDB9MTAy", + "First": "ezB8MXwxMDB9MTAx", + "Last": "ezB8MXwxMDB9MTAy", "Items": [ { "Id": 101, @@ -86,8 +86,8 @@ ```json { - "First": "ezB8MHwxMDB9MjAx", - "Last": "ezB8MHwxMDB9MjAy", + "First": "ezB8MXwxMDB9MjAx", + "Last": "ezB8MXwxMDB9MjAy", "Items": [ { "Id": 201, diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET9_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET9_0.md index 15b0d6f91a7..03779024056 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET9_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_With_Relative_Cursor_NET9_0.md @@ -4,8 +4,8 @@ ```json { - "First": "ezB8MHwxMDB9MQ==", - "Last": "ezB8MHwxMDB9Mg==", + "First": "ezB8MXwxMDB9MQ==", + "Last": "ezB8MXwxMDB9Mg==", "Items": [ { "Id": 1, @@ -45,8 +45,8 @@ ```json { - "First": "ezB8MHwxMDB9MTAx", - "Last": "ezB8MHwxMDB9MTAy", + "First": "ezB8MXwxMDB9MTAx", + "Last": "ezB8MXwxMDB9MTAy", "Items": [ { "Id": 101, @@ -86,8 +86,8 @@ ```json { - "First": "ezB8MHwxMDB9MjAx", - "Last": "ezB8MHwxMDB9MjAy", + "First": "ezB8MXwxMDB9MjAx", + "Last": "ezB8MXwxMDB9MjAy", "Items": [ { "Id": 201, diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md index 9266fe3eaf0..48818646bd1 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md @@ -3,27 +3,29 @@ ## SQL 0 ```sql +-- @__p_0='11' SELECT b."Id", b."AlwaysNull", b."DisplayName", b."Name", b."BrandDetails_Country_Name" FROM "Brands" AS b ORDER BY b."Name", b."Id" +LIMIT @__p_0 ``` ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Take(11) ``` ## Result 3 ```json { - "HasNextPage": false, + "HasNextPage": true, "HasPreviousPage": false, "First": 1, "FirstCursor": "e31CcmFuZFw6MDox", - "Last": 100, - "LastCursor": "e31CcmFuZFw6OTk6MTAw" + "Last": 18, + "LastCursor": "e31CcmFuZFw6MTc6MTg=" } ``` @@ -150,1086 +152,6 @@ ORDER BY b."Name", b."Id" "Name": "Country17" } } - }, - { - "Id": 19, - "Name": "Brand:18", - "DisplayName": "BrandDisplay18", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country18" - } - } - }, - { - "Id": 20, - "Name": "Brand:19", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country19" - } - } - }, - { - "Id": 3, - "Name": "Brand:2", - "DisplayName": "BrandDisplay2", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country2" - } - } - }, - { - "Id": 21, - "Name": "Brand:20", - "DisplayName": "BrandDisplay20", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country20" - } - } - }, - { - "Id": 22, - "Name": "Brand:21", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country21" - } - } - }, - { - "Id": 23, - "Name": "Brand:22", - "DisplayName": "BrandDisplay22", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country22" - } - } - }, - { - "Id": 24, - "Name": "Brand:23", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country23" - } - } - }, - { - "Id": 25, - "Name": "Brand:24", - "DisplayName": "BrandDisplay24", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country24" - } - } - }, - { - "Id": 26, - "Name": "Brand:25", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country25" - } - } - }, - { - "Id": 27, - "Name": "Brand:26", - "DisplayName": "BrandDisplay26", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country26" - } - } - }, - { - "Id": 28, - "Name": "Brand:27", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country27" - } - } - }, - { - "Id": 29, - "Name": "Brand:28", - "DisplayName": "BrandDisplay28", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country28" - } - } - }, - { - "Id": 30, - "Name": "Brand:29", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country29" - } - } - }, - { - "Id": 4, - "Name": "Brand:3", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country3" - } - } - }, - { - "Id": 31, - "Name": "Brand:30", - "DisplayName": "BrandDisplay30", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country30" - } - } - }, - { - "Id": 32, - "Name": "Brand:31", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country31" - } - } - }, - { - "Id": 33, - "Name": "Brand:32", - "DisplayName": "BrandDisplay32", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country32" - } - } - }, - { - "Id": 34, - "Name": "Brand:33", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country33" - } - } - }, - { - "Id": 35, - "Name": "Brand:34", - "DisplayName": "BrandDisplay34", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country34" - } - } - }, - { - "Id": 36, - "Name": "Brand:35", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country35" - } - } - }, - { - "Id": 37, - "Name": "Brand:36", - "DisplayName": "BrandDisplay36", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country36" - } - } - }, - { - "Id": 38, - "Name": "Brand:37", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country37" - } - } - }, - { - "Id": 39, - "Name": "Brand:38", - "DisplayName": "BrandDisplay38", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country38" - } - } - }, - { - "Id": 40, - "Name": "Brand:39", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country39" - } - } - }, - { - "Id": 5, - "Name": "Brand:4", - "DisplayName": "BrandDisplay4", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country4" - } - } - }, - { - "Id": 41, - "Name": "Brand:40", - "DisplayName": "BrandDisplay40", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country40" - } - } - }, - { - "Id": 42, - "Name": "Brand:41", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country41" - } - } - }, - { - "Id": 43, - "Name": "Brand:42", - "DisplayName": "BrandDisplay42", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country42" - } - } - }, - { - "Id": 44, - "Name": "Brand:43", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country43" - } - } - }, - { - "Id": 45, - "Name": "Brand:44", - "DisplayName": "BrandDisplay44", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country44" - } - } - }, - { - "Id": 46, - "Name": "Brand:45", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country45" - } - } - }, - { - "Id": 47, - "Name": "Brand:46", - "DisplayName": "BrandDisplay46", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country46" - } - } - }, - { - "Id": 48, - "Name": "Brand:47", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country47" - } - } - }, - { - "Id": 49, - "Name": "Brand:48", - "DisplayName": "BrandDisplay48", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country48" - } - } - }, - { - "Id": 50, - "Name": "Brand:49", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country49" - } - } - }, - { - "Id": 6, - "Name": "Brand:5", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country5" - } - } - }, - { - "Id": 51, - "Name": "Brand:50", - "DisplayName": "BrandDisplay50", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country50" - } - } - }, - { - "Id": 52, - "Name": "Brand:51", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country51" - } - } - }, - { - "Id": 53, - "Name": "Brand:52", - "DisplayName": "BrandDisplay52", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country52" - } - } - }, - { - "Id": 54, - "Name": "Brand:53", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country53" - } - } - }, - { - "Id": 55, - "Name": "Brand:54", - "DisplayName": "BrandDisplay54", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country54" - } - } - }, - { - "Id": 56, - "Name": "Brand:55", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country55" - } - } - }, - { - "Id": 57, - "Name": "Brand:56", - "DisplayName": "BrandDisplay56", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country56" - } - } - }, - { - "Id": 58, - "Name": "Brand:57", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country57" - } - } - }, - { - "Id": 59, - "Name": "Brand:58", - "DisplayName": "BrandDisplay58", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country58" - } - } - }, - { - "Id": 60, - "Name": "Brand:59", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country59" - } - } - }, - { - "Id": 7, - "Name": "Brand:6", - "DisplayName": "BrandDisplay6", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country6" - } - } - }, - { - "Id": 61, - "Name": "Brand:60", - "DisplayName": "BrandDisplay60", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country60" - } - } - }, - { - "Id": 62, - "Name": "Brand:61", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country61" - } - } - }, - { - "Id": 63, - "Name": "Brand:62", - "DisplayName": "BrandDisplay62", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country62" - } - } - }, - { - "Id": 64, - "Name": "Brand:63", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country63" - } - } - }, - { - "Id": 65, - "Name": "Brand:64", - "DisplayName": "BrandDisplay64", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country64" - } - } - }, - { - "Id": 66, - "Name": "Brand:65", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country65" - } - } - }, - { - "Id": 67, - "Name": "Brand:66", - "DisplayName": "BrandDisplay66", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country66" - } - } - }, - { - "Id": 68, - "Name": "Brand:67", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country67" - } - } - }, - { - "Id": 69, - "Name": "Brand:68", - "DisplayName": "BrandDisplay68", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country68" - } - } - }, - { - "Id": 70, - "Name": "Brand:69", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country69" - } - } - }, - { - "Id": 8, - "Name": "Brand:7", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country7" - } - } - }, - { - "Id": 71, - "Name": "Brand:70", - "DisplayName": "BrandDisplay70", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country70" - } - } - }, - { - "Id": 72, - "Name": "Brand:71", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country71" - } - } - }, - { - "Id": 73, - "Name": "Brand:72", - "DisplayName": "BrandDisplay72", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country72" - } - } - }, - { - "Id": 74, - "Name": "Brand:73", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country73" - } - } - }, - { - "Id": 75, - "Name": "Brand:74", - "DisplayName": "BrandDisplay74", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country74" - } - } - }, - { - "Id": 76, - "Name": "Brand:75", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country75" - } - } - }, - { - "Id": 77, - "Name": "Brand:76", - "DisplayName": "BrandDisplay76", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country76" - } - } - }, - { - "Id": 78, - "Name": "Brand:77", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country77" - } - } - }, - { - "Id": 79, - "Name": "Brand:78", - "DisplayName": "BrandDisplay78", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country78" - } - } - }, - { - "Id": 80, - "Name": "Brand:79", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country79" - } - } - }, - { - "Id": 9, - "Name": "Brand:8", - "DisplayName": "BrandDisplay8", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country8" - } - } - }, - { - "Id": 81, - "Name": "Brand:80", - "DisplayName": "BrandDisplay80", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country80" - } - } - }, - { - "Id": 82, - "Name": "Brand:81", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country81" - } - } - }, - { - "Id": 83, - "Name": "Brand:82", - "DisplayName": "BrandDisplay82", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country82" - } - } - }, - { - "Id": 84, - "Name": "Brand:83", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country83" - } - } - }, - { - "Id": 85, - "Name": "Brand:84", - "DisplayName": "BrandDisplay84", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country84" - } - } - }, - { - "Id": 86, - "Name": "Brand:85", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country85" - } - } - }, - { - "Id": 87, - "Name": "Brand:86", - "DisplayName": "BrandDisplay86", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country86" - } - } - }, - { - "Id": 88, - "Name": "Brand:87", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country87" - } - } - }, - { - "Id": 89, - "Name": "Brand:88", - "DisplayName": "BrandDisplay88", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country88" - } - } - }, - { - "Id": 90, - "Name": "Brand:89", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country89" - } - } - }, - { - "Id": 10, - "Name": "Brand:9", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country9" - } - } - }, - { - "Id": 91, - "Name": "Brand:90", - "DisplayName": "BrandDisplay90", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country90" - } - } - }, - { - "Id": 92, - "Name": "Brand:91", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country91" - } - } - }, - { - "Id": 93, - "Name": "Brand:92", - "DisplayName": "BrandDisplay92", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country92" - } - } - }, - { - "Id": 94, - "Name": "Brand:93", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country93" - } - } - }, - { - "Id": 95, - "Name": "Brand:94", - "DisplayName": "BrandDisplay94", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country94" - } - } - }, - { - "Id": 96, - "Name": "Brand:95", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country95" - } - } - }, - { - "Id": 97, - "Name": "Brand:96", - "DisplayName": "BrandDisplay96", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country96" - } - } - }, - { - "Id": 98, - "Name": "Brand:97", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country97" - } - } - }, - { - "Id": 99, - "Name": "Brand:98", - "DisplayName": "BrandDisplay98", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country98" - } - } - }, - { - "Id": 100, - "Name": "Brand:99", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country99" - } - } } ] ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs_NET8_0.md index 9266fe3eaf0..48818646bd1 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs_NET8_0.md @@ -3,27 +3,29 @@ ## SQL 0 ```sql +-- @__p_0='11' SELECT b."Id", b."AlwaysNull", b."DisplayName", b."Name", b."BrandDetails_Country_Name" FROM "Brands" AS b ORDER BY b."Name", b."Id" +LIMIT @__p_0 ``` ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Take(11) ``` ## Result 3 ```json { - "HasNextPage": false, + "HasNextPage": true, "HasPreviousPage": false, "First": 1, "FirstCursor": "e31CcmFuZFw6MDox", - "Last": 100, - "LastCursor": "e31CcmFuZFw6OTk6MTAw" + "Last": 18, + "LastCursor": "e31CcmFuZFw6MTc6MTg=" } ``` @@ -150,1086 +152,6 @@ ORDER BY b."Name", b."Id" "Name": "Country17" } } - }, - { - "Id": 19, - "Name": "Brand:18", - "DisplayName": "BrandDisplay18", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country18" - } - } - }, - { - "Id": 20, - "Name": "Brand:19", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country19" - } - } - }, - { - "Id": 3, - "Name": "Brand:2", - "DisplayName": "BrandDisplay2", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country2" - } - } - }, - { - "Id": 21, - "Name": "Brand:20", - "DisplayName": "BrandDisplay20", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country20" - } - } - }, - { - "Id": 22, - "Name": "Brand:21", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country21" - } - } - }, - { - "Id": 23, - "Name": "Brand:22", - "DisplayName": "BrandDisplay22", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country22" - } - } - }, - { - "Id": 24, - "Name": "Brand:23", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country23" - } - } - }, - { - "Id": 25, - "Name": "Brand:24", - "DisplayName": "BrandDisplay24", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country24" - } - } - }, - { - "Id": 26, - "Name": "Brand:25", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country25" - } - } - }, - { - "Id": 27, - "Name": "Brand:26", - "DisplayName": "BrandDisplay26", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country26" - } - } - }, - { - "Id": 28, - "Name": "Brand:27", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country27" - } - } - }, - { - "Id": 29, - "Name": "Brand:28", - "DisplayName": "BrandDisplay28", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country28" - } - } - }, - { - "Id": 30, - "Name": "Brand:29", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country29" - } - } - }, - { - "Id": 4, - "Name": "Brand:3", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country3" - } - } - }, - { - "Id": 31, - "Name": "Brand:30", - "DisplayName": "BrandDisplay30", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country30" - } - } - }, - { - "Id": 32, - "Name": "Brand:31", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country31" - } - } - }, - { - "Id": 33, - "Name": "Brand:32", - "DisplayName": "BrandDisplay32", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country32" - } - } - }, - { - "Id": 34, - "Name": "Brand:33", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country33" - } - } - }, - { - "Id": 35, - "Name": "Brand:34", - "DisplayName": "BrandDisplay34", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country34" - } - } - }, - { - "Id": 36, - "Name": "Brand:35", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country35" - } - } - }, - { - "Id": 37, - "Name": "Brand:36", - "DisplayName": "BrandDisplay36", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country36" - } - } - }, - { - "Id": 38, - "Name": "Brand:37", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country37" - } - } - }, - { - "Id": 39, - "Name": "Brand:38", - "DisplayName": "BrandDisplay38", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country38" - } - } - }, - { - "Id": 40, - "Name": "Brand:39", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country39" - } - } - }, - { - "Id": 5, - "Name": "Brand:4", - "DisplayName": "BrandDisplay4", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country4" - } - } - }, - { - "Id": 41, - "Name": "Brand:40", - "DisplayName": "BrandDisplay40", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country40" - } - } - }, - { - "Id": 42, - "Name": "Brand:41", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country41" - } - } - }, - { - "Id": 43, - "Name": "Brand:42", - "DisplayName": "BrandDisplay42", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country42" - } - } - }, - { - "Id": 44, - "Name": "Brand:43", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country43" - } - } - }, - { - "Id": 45, - "Name": "Brand:44", - "DisplayName": "BrandDisplay44", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country44" - } - } - }, - { - "Id": 46, - "Name": "Brand:45", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country45" - } - } - }, - { - "Id": 47, - "Name": "Brand:46", - "DisplayName": "BrandDisplay46", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country46" - } - } - }, - { - "Id": 48, - "Name": "Brand:47", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country47" - } - } - }, - { - "Id": 49, - "Name": "Brand:48", - "DisplayName": "BrandDisplay48", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country48" - } - } - }, - { - "Id": 50, - "Name": "Brand:49", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country49" - } - } - }, - { - "Id": 6, - "Name": "Brand:5", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country5" - } - } - }, - { - "Id": 51, - "Name": "Brand:50", - "DisplayName": "BrandDisplay50", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country50" - } - } - }, - { - "Id": 52, - "Name": "Brand:51", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country51" - } - } - }, - { - "Id": 53, - "Name": "Brand:52", - "DisplayName": "BrandDisplay52", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country52" - } - } - }, - { - "Id": 54, - "Name": "Brand:53", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country53" - } - } - }, - { - "Id": 55, - "Name": "Brand:54", - "DisplayName": "BrandDisplay54", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country54" - } - } - }, - { - "Id": 56, - "Name": "Brand:55", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country55" - } - } - }, - { - "Id": 57, - "Name": "Brand:56", - "DisplayName": "BrandDisplay56", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country56" - } - } - }, - { - "Id": 58, - "Name": "Brand:57", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country57" - } - } - }, - { - "Id": 59, - "Name": "Brand:58", - "DisplayName": "BrandDisplay58", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country58" - } - } - }, - { - "Id": 60, - "Name": "Brand:59", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country59" - } - } - }, - { - "Id": 7, - "Name": "Brand:6", - "DisplayName": "BrandDisplay6", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country6" - } - } - }, - { - "Id": 61, - "Name": "Brand:60", - "DisplayName": "BrandDisplay60", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country60" - } - } - }, - { - "Id": 62, - "Name": "Brand:61", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country61" - } - } - }, - { - "Id": 63, - "Name": "Brand:62", - "DisplayName": "BrandDisplay62", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country62" - } - } - }, - { - "Id": 64, - "Name": "Brand:63", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country63" - } - } - }, - { - "Id": 65, - "Name": "Brand:64", - "DisplayName": "BrandDisplay64", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country64" - } - } - }, - { - "Id": 66, - "Name": "Brand:65", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country65" - } - } - }, - { - "Id": 67, - "Name": "Brand:66", - "DisplayName": "BrandDisplay66", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country66" - } - } - }, - { - "Id": 68, - "Name": "Brand:67", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country67" - } - } - }, - { - "Id": 69, - "Name": "Brand:68", - "DisplayName": "BrandDisplay68", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country68" - } - } - }, - { - "Id": 70, - "Name": "Brand:69", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country69" - } - } - }, - { - "Id": 8, - "Name": "Brand:7", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country7" - } - } - }, - { - "Id": 71, - "Name": "Brand:70", - "DisplayName": "BrandDisplay70", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country70" - } - } - }, - { - "Id": 72, - "Name": "Brand:71", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country71" - } - } - }, - { - "Id": 73, - "Name": "Brand:72", - "DisplayName": "BrandDisplay72", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country72" - } - } - }, - { - "Id": 74, - "Name": "Brand:73", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country73" - } - } - }, - { - "Id": 75, - "Name": "Brand:74", - "DisplayName": "BrandDisplay74", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country74" - } - } - }, - { - "Id": 76, - "Name": "Brand:75", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country75" - } - } - }, - { - "Id": 77, - "Name": "Brand:76", - "DisplayName": "BrandDisplay76", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country76" - } - } - }, - { - "Id": 78, - "Name": "Brand:77", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country77" - } - } - }, - { - "Id": 79, - "Name": "Brand:78", - "DisplayName": "BrandDisplay78", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country78" - } - } - }, - { - "Id": 80, - "Name": "Brand:79", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country79" - } - } - }, - { - "Id": 9, - "Name": "Brand:8", - "DisplayName": "BrandDisplay8", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country8" - } - } - }, - { - "Id": 81, - "Name": "Brand:80", - "DisplayName": "BrandDisplay80", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country80" - } - } - }, - { - "Id": 82, - "Name": "Brand:81", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country81" - } - } - }, - { - "Id": 83, - "Name": "Brand:82", - "DisplayName": "BrandDisplay82", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country82" - } - } - }, - { - "Id": 84, - "Name": "Brand:83", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country83" - } - } - }, - { - "Id": 85, - "Name": "Brand:84", - "DisplayName": "BrandDisplay84", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country84" - } - } - }, - { - "Id": 86, - "Name": "Brand:85", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country85" - } - } - }, - { - "Id": 87, - "Name": "Brand:86", - "DisplayName": "BrandDisplay86", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country86" - } - } - }, - { - "Id": 88, - "Name": "Brand:87", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country87" - } - } - }, - { - "Id": 89, - "Name": "Brand:88", - "DisplayName": "BrandDisplay88", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country88" - } - } - }, - { - "Id": 90, - "Name": "Brand:89", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country89" - } - } - }, - { - "Id": 10, - "Name": "Brand:9", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country9" - } - } - }, - { - "Id": 91, - "Name": "Brand:90", - "DisplayName": "BrandDisplay90", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country90" - } - } - }, - { - "Id": 92, - "Name": "Brand:91", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country91" - } - } - }, - { - "Id": 93, - "Name": "Brand:92", - "DisplayName": "BrandDisplay92", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country92" - } - } - }, - { - "Id": 94, - "Name": "Brand:93", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country93" - } - } - }, - { - "Id": 95, - "Name": "Brand:94", - "DisplayName": "BrandDisplay94", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country94" - } - } - }, - { - "Id": 96, - "Name": "Brand:95", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country95" - } - } - }, - { - "Id": 97, - "Name": "Brand:96", - "DisplayName": "BrandDisplay96", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country96" - } - } - }, - { - "Id": 98, - "Name": "Brand:97", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country97" - } - } - }, - { - "Id": 99, - "Name": "Brand:98", - "DisplayName": "BrandDisplay98", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country98" - } - } - }, - { - "Id": 100, - "Name": "Brand:99", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country99" - } - } } ] ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md index 69d7a1dfff9..04f7903b579 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md @@ -16,7 +16,7 @@ LIMIT @__p_2 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) > 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.Int32]).value) > 0)))).Take(6) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) > 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.Int32]).value) > 0)))).Take(6) ``` ## Result 3 diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13_NET8_0.md index 69d7a1dfff9..04f7903b579 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13_NET8_0.md @@ -16,7 +16,7 @@ LIMIT @__p_2 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) > 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.Int32]).value) > 0)))).Take(6) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) > 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.Int32]).value) > 0)))).Take(6) ``` ## Result 3 diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md index 8b8f843a2f9..ec12ab52cce 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md @@ -16,7 +16,7 @@ LIMIT @__p_2 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) < 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.Int32]).value) < 0)))).Reverse().Take(6) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Name).ThenByDescending(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) < 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.Int32]).value) < 0)))).Take(6) ``` ## Result 3 diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96_NET8_0.md index 8b8f843a2f9..ec12ab52cce 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96_NET8_0.md @@ -16,7 +16,7 @@ LIMIT @__p_2 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) < 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.Int32]).value) < 0)))).Reverse().Take(6) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Name).ThenByDescending(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) < 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.Int32]).value) < 0)))).Take(6) ``` ## Result 3 diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md index 876b9309b11..1be622e578b 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md @@ -13,7 +13,7 @@ LIMIT @__p_0 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Reverse().Take(6) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Name).ThenByDescending(t => t.Id).Take(6) ``` ## Result 3 diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5_NET8_0.md index 876b9309b11..1be622e578b 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5_NET8_0.md @@ -13,7 +13,7 @@ LIMIT @__p_0 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Reverse().Take(6) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Name).ThenByDescending(t => t.Id).Take(6) ``` ## Result 3 diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_With_Offset_Negative_2.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_With_Offset_Negative_2.md index cc13a20f10b..cb5ebf3d1be 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_With_Offset_Negative_2.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.Fetch_First_2_Items_Second_Page_With_Offset_Negative_2.md @@ -2,8 +2,8 @@ ```json { - "First": "Product 0-0", - "Last": "Product 0-1", + "First": "Product 0-1", + "Last": "Product 0-10", "ItemsCount": 2 } ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector.md index 1d574e2d075..6ecf4d83fa2 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector.md @@ -13,6 +13,6 @@ LIMIT @__p_0 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Id).Select(t => new Product() {Id = t.Id, Name = t.Name}).Reverse().Take(3) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Id).Select(t => new Product() {Id = t.Id, Name = t.Name}).Take(3) ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand.md index 35d0137de0d..c04f805eb3a 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand.md @@ -18,6 +18,6 @@ ORDER BY p0."Id" ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Id).Select(root => new Product() {Id = root.Id, Name = root.Name, Brand = root.Brand}).Reverse().Take(3) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Id).Select(root => new Product() {Id = root.Id, Name = root.Name, Brand = root.Brand}).Take(3) ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand_NET8_0.md index 7e2ceb69858..dd675b286d5 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand_NET8_0.md @@ -18,6 +18,6 @@ ORDER BY t."Id" ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Id).Select(root => new Product() {Id = root.Id, Name = root.Name, Brand = root.Brand}).Reverse().Take(3) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Id).Select(root => new Product() {Id = root.Id, Name = root.Name, Brand = root.Brand}).Take(3) ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand_Name.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand_Name.md index 72539cbde7a..0e99a60eb2c 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand_Name.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand_Name.md @@ -18,6 +18,6 @@ ORDER BY p0."Id" ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Id).Select(root => new Product() {Id = root.Id, Name = root.Name, Brand = new Brand() {Name = root.Brand.Name}}).Reverse().Take(3) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Id).Select(root => new Product() {Id = root.Id, Name = root.Name, Brand = new Brand() {Name = root.Brand.Name}}).Take(3) ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand_Name_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand_Name_NET8_0.md index 63c346d153f..a1843944be1 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand_Name_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Brand_Name_NET8_0.md @@ -18,6 +18,6 @@ ORDER BY t."Id" ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Id).Select(root => new Product() {Id = root.Id, Name = root.Name, Brand = new Brand() {Name = root.Brand.Name}}).Reverse().Take(3) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Id).Select(root => new Product() {Id = root.Id, Name = root.Name, Brand = new Brand() {Name = root.Brand.Name}}).Take(3) ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Product_List.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Product_List.md index 3e58c6cf082..cbce9835c8b 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Product_List.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Product_List.md @@ -18,6 +18,6 @@ ORDER BY b0."Id" ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Id).Select(root => new Brand() {Id = root.Id, Name = root.Name, Products = root.Products.Select(p => new Product() {Id = p.Id, Name = p.Name}).ToList()}).Reverse().Take(3) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Id).Select(root => new Brand() {Id = root.Id, Name = root.Name, Products = root.Products.Select(p => new Product() {Id = p.Id, Name = p.Name}).ToList()}).Take(3) ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Product_List_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Product_List_NET8_0.md index 417c22261cb..c58244765e0 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Product_List_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_Include_Product_List_NET8_0.md @@ -18,6 +18,6 @@ ORDER BY t."Id" ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Id).Select(root => new Brand() {Id = root.Id, Name = root.Name, Products = root.Products.Select(p => new Product() {Id = p.Id, Name = p.Name}).ToList()}).Reverse().Take(3) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Id).Select(root => new Brand() {Id = root.Id, Name = root.Name, Products = root.Products.Select(p => new Product() {Id = p.Id, Name = p.Name}).ToList()}).Take(3) ``` diff --git a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_NET8_0.md b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_NET8_0.md index 1d574e2d075..6ecf4d83fa2 100644 --- a/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_NET8_0.md +++ b/src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/__snapshots__/PagingHelperTests.QueryContext_Simple_Selector_NET8_0.md @@ -13,6 +13,6 @@ LIMIT @__p_0 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Id).Select(t => new Product() {Id = t.Id, Name = t.Name}).Reverse().Take(3) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Id).Select(t => new Product() {Id = t.Id, Name = t.Name}).Take(3) ``` diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DefaultTypeInspector.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DefaultTypeInspector.cs index 4b79691de96..cbb32ecd546 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DefaultTypeInspector.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DefaultTypeInspector.cs @@ -920,3 +920,5 @@ private bool TryGetDefaultValueFromConstructor( return false; } } + + diff --git a/src/HotChocolate/Data/src/EntityFramework/Pagination/EfQueryableCursorPagingHandler.cs b/src/HotChocolate/Data/src/EntityFramework/Pagination/EfQueryableCursorPagingHandler.cs index bed43f2d8a7..47bd0e93a86 100644 --- a/src/HotChocolate/Data/src/EntityFramework/Pagination/EfQueryableCursorPagingHandler.cs +++ b/src/HotChocolate/Data/src/EntityFramework/Pagination/EfQueryableCursorPagingHandler.cs @@ -46,14 +46,14 @@ private async ValueTask SliceAsync( if (arguments.After is not null) { var cursor = CursorParser.Parse(arguments.After, keys); - var (whereExpr, _, _) = ExpressionHelpers.BuildWhereExpression(keys, cursor, true); + var (whereExpr, _) = ExpressionHelpers.BuildWhereExpression(keys, cursor, true); query = query.Where(whereExpr); } if (arguments.Before is not null) { var cursor = CursorParser.Parse(arguments.Before, keys); - var (whereExpr, _, _) = ExpressionHelpers.BuildWhereExpression(keys, cursor, false); + var (whereExpr, _) = ExpressionHelpers.BuildWhereExpression(keys, cursor, false); query = query.Where(whereExpr); } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors.md index 93a4c122161..ff84dd11dd3 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors.md @@ -21,10 +21,10 @@ } ], "endCursors": [ - "ezB8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", - "ezF8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", - "ezJ8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", - "ezN8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2" + "ezB8MXwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", + "ezF8MXwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", + "ezJ8MXwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", + "ezN8MXwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2" ] } } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4.md index 0c2d2128543..73cefd4e6cd 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4.md @@ -21,10 +21,10 @@ } ], "endCursors": [ - "ezB8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", - "ezF8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", - "ezJ8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", - "ezN8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4" + "ezB8NHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", + "ezF8NHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", + "ezJ8NHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", + "ezN8NHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4" ] } } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4__net_8_0.md index 0c2d2128543..73cefd4e6cd 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4__net_8_0.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors_Skip_4__net_8_0.md @@ -21,10 +21,10 @@ } ], "endCursors": [ - "ezB8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", - "ezF8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", - "ezJ8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", - "ezN8MHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4" + "ezB8NHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", + "ezF8NHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", + "ezJ8NHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4", + "ezN8NHwxMDF9VmVsb2NpWCAyMDAwIEJpa2UgSGVsbWV0OjU4" ] } } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors__net_8_0.md index 48ac5f68200..c66d723c5c4 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors__net_8_0.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_With_4_EndCursors__net_8_0.md @@ -21,10 +21,10 @@ } ], "endCursors": [ - "ezB8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", - "ezF8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", - "ezJ8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", - "ezN8MHwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2" + "ezB8MXwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", + "ezF8MXwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", + "ezJ8MXwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2", + "ezN8MXwxMDF9WmVuaXRoIEN5Y2xpbmcgSmVyc2V5OjQ2" ] } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5.md index 55d657dc0c0..8226106a616 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5.md @@ -4,12 +4,12 @@ ```json { - "First": "e30xMDA=", - "Last": "e305OQ==", + "First": "e305OQ==", + "Last": "e30xMDA=", "Items": [ { - "Id": 100, - "Name": "Product 0-99", + "Id": 99, + "Name": "Product 0-98", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -23,8 +23,8 @@ "OnReorder": false }, { - "Id": 99, - "Name": "Product 0-98", + "Id": 100, + "Name": "Product 0-99", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -45,12 +45,12 @@ ```json { - "First": "e30yMDA=", - "Last": "e30xOTk=", + "First": "e30xOTk=", + "Last": "e30yMDA=", "Items": [ { - "Id": 200, - "Name": "Product 1-99", + "Id": 199, + "Name": "Product 1-98", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -64,8 +64,8 @@ "OnReorder": false }, { - "Id": 199, - "Name": "Product 1-98", + "Id": 200, + "Name": "Product 1-99", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -86,12 +86,12 @@ ```json { - "First": "e30zMDA=", - "Last": "e30yOTk=", + "First": "e30yOTk=", + "Last": "e30zMDA=", "Items": [ { - "Id": 300, - "Name": "Product 2-99", + "Id": 299, + "Name": "Product 2-98", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -105,8 +105,8 @@ "OnReorder": false }, { - "Id": 299, - "Name": "Product 2-98", + "Id": 300, + "Name": "Product 2-99", "Description": null, "Price": 0.0, "ImageFileName": null, diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md index 9742f19d2d3..9d8a3d5ea4d 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.BatchPaging_Last_5_NET9_0.md @@ -4,12 +4,12 @@ ```json { - "First": "e30xMDA=", - "Last": "e305OQ==", + "First": "e305OQ==", + "Last": "e30xMDA=", "Items": [ { - "Id": 100, - "Name": "Product 0-99", + "Id": 99, + "Name": "Product 0-98", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -23,8 +23,8 @@ "OnReorder": false }, { - "Id": 99, - "Name": "Product 0-98", + "Id": 100, + "Name": "Product 0-99", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -45,12 +45,12 @@ ```json { - "First": "e30yMDA=", - "Last": "e30xOTk=", + "First": "e30xOTk=", + "Last": "e30yMDA=", "Items": [ { - "Id": 200, - "Name": "Product 1-99", + "Id": 199, + "Name": "Product 1-98", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -64,8 +64,8 @@ "OnReorder": false }, { - "Id": 199, - "Name": "Product 1-98", + "Id": 200, + "Name": "Product 1-99", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -86,12 +86,12 @@ ```json { - "First": "e30zMDA=", - "Last": "e30yOTk=", + "First": "e30yOTk=", + "Last": "e30zMDA=", "Items": [ { - "Id": 300, - "Name": "Product 2-99", + "Id": 299, + "Name": "Product 2-98", "Description": null, "Price": 0.0, "ImageFileName": null, @@ -105,8 +105,8 @@ "OnReorder": false }, { - "Id": 299, - "Name": "Product 2-98", + "Id": 300, + "Name": "Product 2-99", "Description": null, "Price": 0.0, "ImageFileName": null, diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Deep_SecondPage.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Deep_SecondPage.md index a50e27e538d..5177074afd6 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Deep_SecondPage.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Deep_SecondPage.md @@ -16,7 +16,7 @@ LIMIT @__p_2 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(x => x.BrandDetails.Country.Name).ThenBy(t => t.Id).Where(t => ((t.BrandDetails.Country.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) > 0) OrElse ((t.BrandDetails.Country.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.Int32]).value) > 0)))).Take(3) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(x => x.BrandDetails.Country.Name).ThenBy(t => t.Id).Where(t => ((t.BrandDetails.Country.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) > 0) OrElse ((t.BrandDetails.Country.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.Int32]).value) > 0)))).Take(3) ``` ## Result diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_Fallback_SecondPage.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_Fallback_SecondPage.md index eb298041210..adf6f0fb443 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_Fallback_SecondPage.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_Fallback_SecondPage.md @@ -16,7 +16,7 @@ LIMIT @__p_2 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => (t.DisplayName ?? t.Name)).ThenBy(t => t.Id).Where(t => (((t.DisplayName ?? t.Name).CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) > 0) OrElse (((t.DisplayName ?? t.Name).CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.Int32]).value) > 0)))).Take(3) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => (t.DisplayName ?? t.Name)).ThenBy(t => t.Id).Where(t => (((t.DisplayName ?? t.Name).CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) > 0) OrElse (((t.DisplayName ?? t.Name).CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.Int32]).value) > 0)))).Take(3) ``` ## Result diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_SecondPage.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_SecondPage.md index 708c1eb98ee..a6fa965501c 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_SecondPage.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetDefaultPage_With_Nullable_SecondPage.md @@ -16,7 +16,7 @@ LIMIT @__p_3 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(x => x.AlwaysNull).ThenBy(t => t.Id).Where(t => (((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) > 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) == 0) AndAlso (t.AlwaysNull.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) > 0))) OrElse (((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) == 0) AndAlso (t.AlwaysNull.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) == 0)) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.Int32]).value) > 0)))).Take(3) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(x => x.AlwaysNull).ThenBy(t => t.Id).Where(t => (((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) > 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) == 0) AndAlso (t.AlwaysNull.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) > 0))) OrElse (((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) == 0) AndAlso (t.AlwaysNull.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) == 0)) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.Int32]).value) > 0)))).Take(3) ``` ## Result diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetSecondPage_With_2_Items.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetSecondPage_With_2_Items.md index e5ea62b3bbf..5b2cb0ddb0b 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetSecondPage_With_2_Items.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.GetSecondPage_With_2_Items.md @@ -16,7 +16,7 @@ LIMIT @__p_2 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) > 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.Int32]).value) > 0)))).Take(3) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) > 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.Int32]).value) > 0)))).Take(3) ``` ## Result diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md index 9266fe3eaf0..48818646bd1 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Empty_PagingArgs.md @@ -3,27 +3,29 @@ ## SQL 0 ```sql +-- @__p_0='11' SELECT b."Id", b."AlwaysNull", b."DisplayName", b."Name", b."BrandDetails_Country_Name" FROM "Brands" AS b ORDER BY b."Name", b."Id" +LIMIT @__p_0 ``` ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Take(11) ``` ## Result 3 ```json { - "HasNextPage": false, + "HasNextPage": true, "HasPreviousPage": false, "First": 1, "FirstCursor": "e31CcmFuZFw6MDox", - "Last": 100, - "LastCursor": "e31CcmFuZFw6OTk6MTAw" + "Last": 18, + "LastCursor": "e31CcmFuZFw6MTc6MTg=" } ``` @@ -150,1086 +152,6 @@ ORDER BY b."Name", b."Id" "Name": "Country17" } } - }, - { - "Id": 19, - "Name": "Brand:18", - "DisplayName": "BrandDisplay18", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country18" - } - } - }, - { - "Id": 20, - "Name": "Brand:19", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country19" - } - } - }, - { - "Id": 3, - "Name": "Brand:2", - "DisplayName": "BrandDisplay2", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country2" - } - } - }, - { - "Id": 21, - "Name": "Brand:20", - "DisplayName": "BrandDisplay20", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country20" - } - } - }, - { - "Id": 22, - "Name": "Brand:21", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country21" - } - } - }, - { - "Id": 23, - "Name": "Brand:22", - "DisplayName": "BrandDisplay22", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country22" - } - } - }, - { - "Id": 24, - "Name": "Brand:23", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country23" - } - } - }, - { - "Id": 25, - "Name": "Brand:24", - "DisplayName": "BrandDisplay24", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country24" - } - } - }, - { - "Id": 26, - "Name": "Brand:25", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country25" - } - } - }, - { - "Id": 27, - "Name": "Brand:26", - "DisplayName": "BrandDisplay26", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country26" - } - } - }, - { - "Id": 28, - "Name": "Brand:27", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country27" - } - } - }, - { - "Id": 29, - "Name": "Brand:28", - "DisplayName": "BrandDisplay28", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country28" - } - } - }, - { - "Id": 30, - "Name": "Brand:29", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country29" - } - } - }, - { - "Id": 4, - "Name": "Brand:3", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country3" - } - } - }, - { - "Id": 31, - "Name": "Brand:30", - "DisplayName": "BrandDisplay30", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country30" - } - } - }, - { - "Id": 32, - "Name": "Brand:31", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country31" - } - } - }, - { - "Id": 33, - "Name": "Brand:32", - "DisplayName": "BrandDisplay32", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country32" - } - } - }, - { - "Id": 34, - "Name": "Brand:33", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country33" - } - } - }, - { - "Id": 35, - "Name": "Brand:34", - "DisplayName": "BrandDisplay34", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country34" - } - } - }, - { - "Id": 36, - "Name": "Brand:35", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country35" - } - } - }, - { - "Id": 37, - "Name": "Brand:36", - "DisplayName": "BrandDisplay36", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country36" - } - } - }, - { - "Id": 38, - "Name": "Brand:37", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country37" - } - } - }, - { - "Id": 39, - "Name": "Brand:38", - "DisplayName": "BrandDisplay38", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country38" - } - } - }, - { - "Id": 40, - "Name": "Brand:39", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country39" - } - } - }, - { - "Id": 5, - "Name": "Brand:4", - "DisplayName": "BrandDisplay4", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country4" - } - } - }, - { - "Id": 41, - "Name": "Brand:40", - "DisplayName": "BrandDisplay40", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country40" - } - } - }, - { - "Id": 42, - "Name": "Brand:41", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country41" - } - } - }, - { - "Id": 43, - "Name": "Brand:42", - "DisplayName": "BrandDisplay42", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country42" - } - } - }, - { - "Id": 44, - "Name": "Brand:43", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country43" - } - } - }, - { - "Id": 45, - "Name": "Brand:44", - "DisplayName": "BrandDisplay44", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country44" - } - } - }, - { - "Id": 46, - "Name": "Brand:45", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country45" - } - } - }, - { - "Id": 47, - "Name": "Brand:46", - "DisplayName": "BrandDisplay46", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country46" - } - } - }, - { - "Id": 48, - "Name": "Brand:47", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country47" - } - } - }, - { - "Id": 49, - "Name": "Brand:48", - "DisplayName": "BrandDisplay48", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country48" - } - } - }, - { - "Id": 50, - "Name": "Brand:49", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country49" - } - } - }, - { - "Id": 6, - "Name": "Brand:5", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country5" - } - } - }, - { - "Id": 51, - "Name": "Brand:50", - "DisplayName": "BrandDisplay50", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country50" - } - } - }, - { - "Id": 52, - "Name": "Brand:51", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country51" - } - } - }, - { - "Id": 53, - "Name": "Brand:52", - "DisplayName": "BrandDisplay52", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country52" - } - } - }, - { - "Id": 54, - "Name": "Brand:53", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country53" - } - } - }, - { - "Id": 55, - "Name": "Brand:54", - "DisplayName": "BrandDisplay54", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country54" - } - } - }, - { - "Id": 56, - "Name": "Brand:55", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country55" - } - } - }, - { - "Id": 57, - "Name": "Brand:56", - "DisplayName": "BrandDisplay56", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country56" - } - } - }, - { - "Id": 58, - "Name": "Brand:57", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country57" - } - } - }, - { - "Id": 59, - "Name": "Brand:58", - "DisplayName": "BrandDisplay58", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country58" - } - } - }, - { - "Id": 60, - "Name": "Brand:59", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country59" - } - } - }, - { - "Id": 7, - "Name": "Brand:6", - "DisplayName": "BrandDisplay6", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country6" - } - } - }, - { - "Id": 61, - "Name": "Brand:60", - "DisplayName": "BrandDisplay60", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country60" - } - } - }, - { - "Id": 62, - "Name": "Brand:61", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country61" - } - } - }, - { - "Id": 63, - "Name": "Brand:62", - "DisplayName": "BrandDisplay62", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country62" - } - } - }, - { - "Id": 64, - "Name": "Brand:63", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country63" - } - } - }, - { - "Id": 65, - "Name": "Brand:64", - "DisplayName": "BrandDisplay64", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country64" - } - } - }, - { - "Id": 66, - "Name": "Brand:65", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country65" - } - } - }, - { - "Id": 67, - "Name": "Brand:66", - "DisplayName": "BrandDisplay66", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country66" - } - } - }, - { - "Id": 68, - "Name": "Brand:67", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country67" - } - } - }, - { - "Id": 69, - "Name": "Brand:68", - "DisplayName": "BrandDisplay68", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country68" - } - } - }, - { - "Id": 70, - "Name": "Brand:69", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country69" - } - } - }, - { - "Id": 8, - "Name": "Brand:7", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country7" - } - } - }, - { - "Id": 71, - "Name": "Brand:70", - "DisplayName": "BrandDisplay70", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country70" - } - } - }, - { - "Id": 72, - "Name": "Brand:71", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country71" - } - } - }, - { - "Id": 73, - "Name": "Brand:72", - "DisplayName": "BrandDisplay72", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country72" - } - } - }, - { - "Id": 74, - "Name": "Brand:73", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country73" - } - } - }, - { - "Id": 75, - "Name": "Brand:74", - "DisplayName": "BrandDisplay74", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country74" - } - } - }, - { - "Id": 76, - "Name": "Brand:75", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country75" - } - } - }, - { - "Id": 77, - "Name": "Brand:76", - "DisplayName": "BrandDisplay76", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country76" - } - } - }, - { - "Id": 78, - "Name": "Brand:77", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country77" - } - } - }, - { - "Id": 79, - "Name": "Brand:78", - "DisplayName": "BrandDisplay78", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country78" - } - } - }, - { - "Id": 80, - "Name": "Brand:79", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country79" - } - } - }, - { - "Id": 9, - "Name": "Brand:8", - "DisplayName": "BrandDisplay8", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country8" - } - } - }, - { - "Id": 81, - "Name": "Brand:80", - "DisplayName": "BrandDisplay80", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country80" - } - } - }, - { - "Id": 82, - "Name": "Brand:81", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country81" - } - } - }, - { - "Id": 83, - "Name": "Brand:82", - "DisplayName": "BrandDisplay82", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country82" - } - } - }, - { - "Id": 84, - "Name": "Brand:83", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country83" - } - } - }, - { - "Id": 85, - "Name": "Brand:84", - "DisplayName": "BrandDisplay84", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country84" - } - } - }, - { - "Id": 86, - "Name": "Brand:85", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country85" - } - } - }, - { - "Id": 87, - "Name": "Brand:86", - "DisplayName": "BrandDisplay86", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country86" - } - } - }, - { - "Id": 88, - "Name": "Brand:87", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country87" - } - } - }, - { - "Id": 89, - "Name": "Brand:88", - "DisplayName": "BrandDisplay88", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country88" - } - } - }, - { - "Id": 90, - "Name": "Brand:89", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country89" - } - } - }, - { - "Id": 10, - "Name": "Brand:9", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country9" - } - } - }, - { - "Id": 91, - "Name": "Brand:90", - "DisplayName": "BrandDisplay90", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country90" - } - } - }, - { - "Id": 92, - "Name": "Brand:91", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country91" - } - } - }, - { - "Id": 93, - "Name": "Brand:92", - "DisplayName": "BrandDisplay92", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country92" - } - } - }, - { - "Id": 94, - "Name": "Brand:93", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country93" - } - } - }, - { - "Id": 95, - "Name": "Brand:94", - "DisplayName": "BrandDisplay94", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country94" - } - } - }, - { - "Id": 96, - "Name": "Brand:95", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country95" - } - } - }, - { - "Id": 97, - "Name": "Brand:96", - "DisplayName": "BrandDisplay96", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country96" - } - } - }, - { - "Id": 98, - "Name": "Brand:97", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country97" - } - } - }, - { - "Id": 99, - "Name": "Brand:98", - "DisplayName": "BrandDisplay98", - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country98" - } - } - }, - { - "Id": 100, - "Name": "Brand:99", - "DisplayName": null, - "AlwaysNull": null, - "Products": [], - "BrandDetails": { - "Country": { - "Name": "Country99" - } - } } ] ``` diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md index 69d7a1dfff9..04f7903b579 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_After_Id_13.md @@ -16,7 +16,7 @@ LIMIT @__p_2 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) > 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.Int32]).value) > 0)))).Take(6) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) > 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.Int32]).value) > 0)))).Take(6) ``` ## Result 3 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md index 8b8f843a2f9..ec12ab52cce 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_First_5_Before_Id_96.md @@ -16,7 +16,7 @@ LIMIT @__p_2 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) < 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass7_0`1[System.Int32]).value) < 0)))).Reverse().Take(6) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Name).ThenByDescending(t => t.Id).Where(t => ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) < 0) OrElse ((t.Name.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.String]).value) == 0) AndAlso (t.Id.CompareTo(value(GreenDonut.Data.Expressions.ExpressionHelpers+<>c__DisplayClass6_0`1[System.Int32]).value) < 0)))).Take(6) ``` ## Result 3 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md index 3266f3c619b..054c533d926 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationPagingHelperTests.Paging_Last_5.md @@ -13,7 +13,7 @@ LIMIT @__p_0 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Reverse().Take(6) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderByDescending(t => t.Name).ThenByDescending(t => t.Id).Take(6) ``` ## Result 3 From 552d5d62d0ce0b6e97c545ec4b2284300bda8342 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 11 Mar 2025 08:48:27 +0100 Subject: [PATCH 21/64] Fixed Source Generator Visual Studio 2022 incompatibility (#8112) --- .../FileBuilders/ConnectionTypeFileBuilder.cs | 2 +- .../FileBuilders/EdgeTypeFileBuilder.cs | 2 +- .../FileBuilders/TypeFileBuilderBase.cs | 1 - .../Generators/TypesSyntaxGenerator.cs | 9 ++++++ .../HotChocolate.Types.Analyzers.csproj | 8 +++-- .../Inspectors/ConnectionTypeTransformer.cs | 29 +++++++++++++++---- .../Core/src/Types.Analyzers/KnownSymbols.cs | 4 +++ .../Types.Analyzers/Models/IOutputTypeInfo.cs | 6 +++- 8 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ConnectionTypeFileBuilder.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ConnectionTypeFileBuilder.cs index 8ec87212066..a822841c7b4 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ConnectionTypeFileBuilder.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ConnectionTypeFileBuilder.cs @@ -69,7 +69,7 @@ public override void WriteInitializeMethod(IOutputTypeInfo type) } } else if (!string.IsNullOrEmpty(connectionType.NameFormat) - && !connectionType.NameFormat.Contains("{0}")) + && !connectionType.NameFormat!.Contains("{0}")) { Writer.WriteLine(); Writer.WriteIndentedLine( diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/EdgeTypeFileBuilder.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/EdgeTypeFileBuilder.cs index 2962eff5082..d35333eeec7 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/EdgeTypeFileBuilder.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/EdgeTypeFileBuilder.cs @@ -68,7 +68,7 @@ public override void WriteInitializeMethod(IOutputTypeInfo type) } } else if (!string.IsNullOrEmpty(edgeType.NameFormat) - && !edgeType.NameFormat.Contains("{0}")) + && !edgeType.NameFormat!.Contains("{0}")) { Writer.WriteLine(); Writer.WriteIndentedLine( diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs index 671da85d5e7..15d25774a8d 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices.Marshalling; using System.Text; using HotChocolate.Types.Analyzers.Generators; using HotChocolate.Types.Analyzers.Helpers; diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypesSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypesSyntaxGenerator.cs index 55eb7bf616d..4987a492900 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypesSyntaxGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypesSyntaxGenerator.cs @@ -86,6 +86,7 @@ private static void WriteTypes( static string CreateFileName(IOutputTypeInfo type) { +#if NET8_0_OR_GREATER Span hash = stackalloc byte[64]; var bytes = Encoding.UTF8.GetBytes(type.Namespace); MD5.HashData(bytes, hash); @@ -110,6 +111,14 @@ static string CreateFileName(IOutputTypeInfo type) } return $"{type.Name}.{Encoding.UTF8.GetString(hash)}.hc.g.cs"; +#else + var bytes = Encoding.UTF8.GetBytes(type.Namespace); + var md5 = MD5.Create(); + var hash = md5.ComputeHash(bytes); + var hashString = Convert.ToBase64String(hash, Base64FormattingOptions.None); + hashString = hashString.Replace("+", "-").Replace("/", "_").TrimEnd('='); + return $"{type.Name}.{hashString}.hc.g.cs"; +#endif } } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/HotChocolate.Types.Analyzers.csproj b/src/HotChocolate/Core/src/Types.Analyzers/HotChocolate.Types.Analyzers.csproj index 90e66f8189f..3ae359edee6 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/HotChocolate.Types.Analyzers.csproj +++ b/src/HotChocolate/Core/src/Types.Analyzers/HotChocolate.Types.Analyzers.csproj @@ -1,8 +1,8 @@ - net8.0 - net8.0 + netstandard2.0; net8.0 + netstandard2.0 false false true @@ -21,6 +21,10 @@ + + + + diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs index 456f8b416c0..78373666bca 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs @@ -1,7 +1,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using HotChocolate.Types.Analyzers.Models; using HotChocolate.Types.Analyzers.Helpers; +using HotChocolate.Types.Analyzers.Models; using Microsoft.CodeAnalysis; namespace HotChocolate.Types.Analyzers.Inspectors; @@ -49,12 +49,23 @@ public ImmutableArray Transform( { if (syntaxInfo is IOutputTypeInfo { HasRuntimeType: true } typeInfo) { +#if NET8_0_OR_GREATER connectionTypeLookup[typeInfo.RuntimeTypeFullName] = typeInfo; +#else + connectionTypeLookup[typeInfo.RuntimeTypeFullName!] = typeInfo; +#endif } } +#if NET8_0_OR_GREATER foreach (var (connectionResolver, owner) in connectionResolvers) { +#else + foreach (var item in connectionResolvers.ToImmutableArray()) + { + var connectionResolver = item.Key; + var owner = item.Value; +#endif var connectionType = GetConnectionType(compilation, connectionResolver.Member.GetReturnType()); ConnectionTypeInfo connectionTypeInfo; ConnectionClassInfo? connectionClass; @@ -330,11 +341,17 @@ private static INamedTypeSymbol GetConnectionType(Compilation compilation, IType return new GenericTypeInfo(typeDefinitionName, genericType, name, nameFormat); } - private record GenericTypeInfo( - string TypeDefinitionName, - INamedTypeSymbol Type, - string Name, - string? NameFormat); + private sealed class GenericTypeInfo( + string typeDefinitionName, + INamedTypeSymbol type, + string name, + string? nameFormat) + { + public string TypeDefinitionName { get; } = typeDefinitionName; + public INamedTypeSymbol Type { get; } = type; + public string Name { get; } = name; + public string? NameFormat { get; } = nameFormat; + } private static INamedTypeSymbol? GetEdgeType(INamedTypeSymbol connectionType) { diff --git a/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs b/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs index b55520cb6b2..c353a9c21fd 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs @@ -34,7 +34,11 @@ public static bool TryGetConnectionNameFromResolver( { if (namedValue.EndsWith(connection)) { +#if NET8_0_OR_GREATER namedValue = namedValue[..^connection.Length]; +#else + namedValue = namedValue.Substring(0, namedValue.Length - connection.Length); +#endif } name = namedValue; diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/IOutputTypeInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/IOutputTypeInfo.cs index 80b0d26314a..22912ac60f9 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/IOutputTypeInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/IOutputTypeInfo.cs @@ -35,7 +35,9 @@ public interface IOutputTypeInfo /// /// Specifies if this type info has a schema type. /// - [MemberNotNull(nameof(SchemaTypeFullName), nameof(SchemaSchemaType))] +#if NET8_0_OR_GREATER + [MemberNotNull(nameof(RuntimeTypeFullName), nameof(RuntimeType))] +#endif bool HasSchemaType { get; } /// @@ -51,7 +53,9 @@ public interface IOutputTypeInfo /// /// Specifies if this type info has a runtime type. /// +#if NET8_0_OR_GREATER [MemberNotNull(nameof(RuntimeTypeFullName), nameof(RuntimeType))] +#endif bool HasRuntimeType { get; } /// From 8dadf9a5fbe583202f62a6fb1de90a4e8564462c Mon Sep 17 00:00:00 2001 From: Glen Date: Tue, 11 Mar 2025 15:05:30 +0200 Subject: [PATCH 22/64] Added Docker Hub login step to CI and coverage workflows (#8114) --- .github/workflows/ci.yml | 6 ++++++ .github/workflows/coverage.yml | 6 ++++++ dictionary.txt | 1 + 3 files changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1572aafdef1..ae1ef56c478 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -185,6 +185,12 @@ jobs: run: dotnet build ${{ matrix.path }} --framework net9.0 --verbosity q timeout-minutes: 5 + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Run tests id: run-tests timeout-minutes: 15 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8559ec02443..0e3f55e7253 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -67,6 +67,12 @@ jobs: run: dotnet build ${{ matrix.path }} --framework net9.0 --verbosity q timeout-minutes: 5 + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Run tests id: run-tests timeout-minutes: 15 diff --git a/dictionary.txt b/dictionary.txt index 579af37cf6d..796ff883ff8 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -39,6 +39,7 @@ depersist deprioritization deprioritized Dispatchable +DOCKERHUB drawio enisdenjo entityframework From 6473ae876dc4fc185cc06affcc717d23f1be1d18 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 11 Mar 2025 14:40:09 +0100 Subject: [PATCH 23/64] Fixed Node id field can no longer be provided via an extension type (#8116) --- .../Types/Relay/Attributes/NodeAttribute.cs | 7 --- .../Types/Relay/NodeResolverTests.cs | 43 +++++++++++++++++++ ...ttribute_On_Extension_With_Renamed_Id.snap | 8 ++++ 3 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 src/HotChocolate/Core/test/Types.Tests/Types/Relay/__snapshots__/NodeResolverTests.NodeAttribute_On_Extension_With_Renamed_Id.snap diff --git a/src/HotChocolate/Core/src/Types/Types/Relay/Attributes/NodeAttribute.cs b/src/HotChocolate/Core/src/Types/Types/Relay/Attributes/NodeAttribute.cs index a6e3427f8b5..f46e41a08f5 100644 --- a/src/HotChocolate/Core/src/Types/Types/Relay/Attributes/NodeAttribute.cs +++ b/src/HotChocolate/Core/src/Types/Types/Relay/Attributes/NodeAttribute.cs @@ -141,13 +141,6 @@ protected override void OnConfigure( nodeDescriptor.TryResolveNode(type); } - // we trigger a late id field configuration - var typeDescriptor = ObjectTypeDescriptor.From( - completionContext.DescriptorContext, - definition); - nodeDescriptor.ConfigureNodeField(typeDescriptor); - typeDescriptor.CreateDefinition(); - // invoke completion explicitly. nodeDescriptor.OnCompleteDefinition(completionContext, definition); }); diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/NodeResolverTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/NodeResolverTests.cs index 15b794f0240..f67595d903e 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/NodeResolverTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/NodeResolverTests.cs @@ -251,6 +251,26 @@ ... on Entity { .MatchSnapshotAsync(); } + // Ensure Issue 7829 is fixed. + [Fact] + public async Task NodeAttribute_On_Extension_With_Renamed_Id() + { + await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .AddTypeExtension() + .ExecuteRequestAsync( + """ + { + entity(id: 5) { + id + data + } + } + """) + .MatchSnapshotAsync(); + } + public class Query { public Entity GetEntity(string name) => new Entity { Name = name, }; @@ -330,5 +350,28 @@ public class EntityExtension4 { public static Entity GetEntity(string id) => new() { Name = id, }; } + + public class QueryEntityRenamed + { + public EntityNoId GetEntity(int id) + => new EntityNoId { Data = id }; + } + + public class EntityNoId + { + public int Data { get; set; } + } + + [Node] + [ExtendObjectType(typeof(EntityNoId))] + public class EntityExtensionRenamingId + { + public int GetId([Parent] EntityNoId entity) + => entity.Data; + + [NodeResolver] + public EntityNoId GetEntity(int id) + => new() { Data = id, }; + } } #pragma warning restore RCS1102 // Make class static diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/__snapshots__/NodeResolverTests.NodeAttribute_On_Extension_With_Renamed_Id.snap b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/__snapshots__/NodeResolverTests.NodeAttribute_On_Extension_With_Renamed_Id.snap new file mode 100644 index 00000000000..40479b87f83 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/__snapshots__/NodeResolverTests.NodeAttribute_On_Extension_With_Renamed_Id.snap @@ -0,0 +1,8 @@ +{ + "data": { + "entity": { + "id": "RW50aXR5Tm9JZDo1", + "data": 5 + } + } +} From e491db2dfa33d8fcc2510af7c3037ba7140d5f00 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 11 Mar 2025 15:07:41 +0100 Subject: [PATCH 24/64] Fixed DataLoader does not work with a class resolver for interface types. (#8117) --- src/All.slnx | 3 ++ src/HotChocolate/Core/HotChocolate.Core.sln | 18 ++++++++ .../Definitions/InterfaceFieldDefinition.cs | 6 ++- .../HotChocolate.Types.Analyzers.Tests.csproj | 44 +++++++++++++++++++ .../InterfaceTests.cs | 34 ++++++++++++++ .../ModuleInfo.cs | 3 ++ .../Product.cs | 25 +++++++++++ .../InterfaceTests.Schema_Snapshot.snap | 18 ++++++++ 8 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/HotChocolate.Types.Analyzers.Tests.csproj create mode 100644 src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/InterfaceTests.cs create mode 100644 src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/ModuleInfo.cs create mode 100644 src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/Product.cs create mode 100644 src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/__snapshots__/InterfaceTests.Schema_Snapshot.snap diff --git a/src/All.slnx b/src/All.slnx index 5a6b5e35bcb..8b7c570848c 100644 --- a/src/All.slnx +++ b/src/All.slnx @@ -128,6 +128,9 @@ + + + diff --git a/src/HotChocolate/Core/HotChocolate.Core.sln b/src/HotChocolate/Core/HotChocolate.Core.sln index befdccbdeeb..71616ffe387 100644 --- a/src/HotChocolate/Core/HotChocolate.Core.sln +++ b/src/HotChocolate/Core/HotChocolate.Core.sln @@ -147,6 +147,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Features.Tests EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Execution.Projections", "src\Execution.Projections\HotChocolate.Execution.Projections.csproj", "{68726F89-B254-424B-BE17-AC9312484464}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Types.Analyzer.Integration.Tests", "Types.Analyzer.Integration.Tests", "{BB753F80-E4F3-4510-2AE6-4DF7F004941D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types.Analyzers.Tests", "test\Types.Analyzer.Integration.Tests\HotChocolate.Types.Analyzers.Tests.csproj", "{5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -985,6 +989,18 @@ Global {68726F89-B254-424B-BE17-AC9312484464}.Release|x64.Build.0 = Release|Any CPU {68726F89-B254-424B-BE17-AC9312484464}.Release|x86.ActiveCfg = Release|Any CPU {68726F89-B254-424B-BE17-AC9312484464}.Release|x86.Build.0 = Release|Any CPU + {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Debug|x64.ActiveCfg = Debug|Any CPU + {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Debug|x64.Build.0 = Debug|Any CPU + {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Debug|x86.ActiveCfg = Debug|Any CPU + {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Debug|x86.Build.0 = Debug|Any CPU + {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Release|Any CPU.Build.0 = Release|Any CPU + {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Release|x64.ActiveCfg = Release|Any CPU + {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Release|x64.Build.0 = Release|Any CPU + {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Release|x86.ActiveCfg = Release|Any CPU + {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1059,6 +1075,8 @@ Global {669FA147-3B41-4841-921A-55B019C3AF26} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} {EA77D317-8767-4DDE-8038-820D582C52D6} = {7462D089-D350-44D6-8131-896D949A65B7} {68726F89-B254-424B-BE17-AC9312484464} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} + {BB753F80-E4F3-4510-2AE6-4DF7F004941D} = {7462D089-D350-44D6-8131-896D949A65B7} + {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C} = {BB753F80-E4F3-4510-2AE6-4DF7F004941D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E4D94C77-6657-4630-9D42-0A9AC5153A1B} diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/InterfaceFieldDefinition.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/InterfaceFieldDefinition.cs index d1c6c7095f5..e90ad0d59d7 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/InterfaceFieldDefinition.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/InterfaceFieldDefinition.cs @@ -24,7 +24,10 @@ public class InterfaceFieldDefinition : OutputFieldDefinitionBase /// /// Initializes a new instance of . /// - public InterfaceFieldDefinition() { } + public InterfaceFieldDefinition() + { + IsParallelExecutable = true; + } /// /// Initializes a new instance of . @@ -37,6 +40,7 @@ public InterfaceFieldDefinition( Name = name.EnsureGraphQLName(); Description = description; Type = type; + IsParallelExecutable = true; } /// diff --git a/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/HotChocolate.Types.Analyzers.Tests.csproj b/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/HotChocolate.Types.Analyzers.Tests.csproj new file mode 100644 index 00000000000..a1d487bfd2e --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/HotChocolate.Types.Analyzers.Tests.csproj @@ -0,0 +1,44 @@ + + + + false + $(InterceptorsPreviewNamespaces);HotChocolate.Execution.Generated + + + + + + HotChocolate.Types.Analyzers.Tests + HotChocolate.Types + $(NoWarn);GD0001 + + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + + diff --git a/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/InterfaceTests.cs b/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/InterfaceTests.cs new file mode 100644 index 00000000000..8ed5a6f16e4 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/InterfaceTests.cs @@ -0,0 +1,34 @@ +using HotChocolate.Execution; +using HotChocolate.Tests; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Types; + +public class InterfaceTests +{ + [Fact] + public async Task Schema_Snapshot() + { + await new ServiceCollection() + .AddGraphQLServer() + .AddIntegrationTestTypes() + .BuildSchemaAsync() + .MatchSnapshotAsync(); + } + + [Fact] + public async Task Ensure_Interface_Resolvers_Are_ParallelExecutable() + { + var schema = + await new ServiceCollection() + .AddGraphQLServer() + .AddIntegrationTestTypes() + .BuildSchemaAsync(); + + Assert.True( + schema.GetType("Book") + .Fields["kind"] + .IsParallelExecutable, + "Interface resolvers should be parallel executable"); + } +} diff --git a/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/ModuleInfo.cs b/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/ModuleInfo.cs new file mode 100644 index 00000000000..029e5a95b80 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/ModuleInfo.cs @@ -0,0 +1,3 @@ +using HotChocolate; + +[assembly: Module("IntegrationTestTypes")] diff --git a/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/Product.cs b/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/Product.cs new file mode 100644 index 00000000000..a596401cca0 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/Product.cs @@ -0,0 +1,25 @@ +namespace HotChocolate.Types; + +[InterfaceType] +public class Product +{ + public required string Id { get; set; } +} + +[InterfaceType] +public static partial class ProductType +{ + public static string Kind() => "Product"; +} + +[ObjectType] +public class Book : Product +{ + public required string Title { get; set; } +} + +[QueryType] +public static partial class Query +{ + public static Product GetProduct() => new Book { Id = "1", Title = "GraphQL in Action" }; +} diff --git a/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/__snapshots__/InterfaceTests.Schema_Snapshot.snap b/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/__snapshots__/InterfaceTests.Schema_Snapshot.snap new file mode 100644 index 00000000000..7e67827757e --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/__snapshots__/InterfaceTests.Schema_Snapshot.snap @@ -0,0 +1,18 @@ +schema { + query: Query +} + +interface Product { + kind: String! + id: String! +} + +type Book implements Product { + title: String! + id: String! + kind: String! +} + +type Query { + product: Product! +} From 9e1eb7b10e2c07201146f3a01e2deb3af2cc62cc Mon Sep 17 00:00:00 2001 From: Tobias Tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 11 Mar 2025 15:08:35 +0100 Subject: [PATCH 25/64] Fixed flaky executor tests (#8113) --- .../test/AspNetCore.Tests/EvictSchemaTests.cs | 10 +++--- .../AutoUpdateRequestExecutorProxyTests.cs | 5 +-- .../Configuration/TypeModuleTests.cs | 17 +++++----- .../RequestExecutorProxyTests.cs | 5 +-- .../RequestExecutorResolverTests.cs | 32 ++++++++++++------- 5 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EvictSchemaTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EvictSchemaTests.cs index 5c22a89274f..917156c7d7e 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EvictSchemaTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EvictSchemaTests.cs @@ -10,7 +10,8 @@ public class EvictSchemaTests(TestServerFactory serverFactory) : ServerTestBase( public async Task Evict_Default_Schema() { // arrange - var newExecutorCreatedResetEvent = new AutoResetEvent(false); + var newExecutorCreatedResetEvent = new ManualResetEventSlim(false); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var server = CreateStarWarsServer(); var time1 = await server.GetAsync( @@ -28,7 +29,7 @@ public async Task Evict_Default_Schema() // act await server.GetAsync( new ClientQueryRequest { Query = "{ evict }", }); - newExecutorCreatedResetEvent.WaitOne(5000); + newExecutorCreatedResetEvent.Wait(cts.Token); // assert var time2 = await server.GetAsync( @@ -40,7 +41,8 @@ await server.GetAsync( public async Task Evict_Named_Schema() { // arrange - var newExecutorCreatedResetEvent = new AutoResetEvent(false); + var newExecutorCreatedResetEvent = new ManualResetEventSlim(false); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var server = CreateStarWarsServer(); var time1 = await server.GetAsync( @@ -60,7 +62,7 @@ public async Task Evict_Named_Schema() await server.GetAsync( new ClientQueryRequest { Query = "{ evict }", }, "/evict"); - newExecutorCreatedResetEvent.WaitOne(5000); + newExecutorCreatedResetEvent.Wait(cts.Token); // assert var time2 = await server.GetAsync( diff --git a/src/HotChocolate/Core/test/Execution.Tests/AutoUpdateRequestExecutorProxyTests.cs b/src/HotChocolate/Core/test/Execution.Tests/AutoUpdateRequestExecutorProxyTests.cs index 1a421f844e7..46354430646 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/AutoUpdateRequestExecutorProxyTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/AutoUpdateRequestExecutorProxyTests.cs @@ -33,7 +33,8 @@ public async Task Ensure_Executor_Is_Cached() public async Task Ensure_Executor_Is_Correctly_Swapped_When_Evicted() { // arrange - var executorUpdatedResetEvent = new AutoResetEvent(false); + var executorUpdatedResetEvent = new ManualResetEventSlim(false); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var resolver = new ServiceCollection() .AddGraphQL() @@ -58,7 +59,7 @@ public async Task Ensure_Executor_Is_Correctly_Swapped_When_Evicted() // act var a = proxy.InnerExecutor; resolver.EvictRequestExecutor(); - executorUpdatedResetEvent.WaitOne(1000); + executorUpdatedResetEvent.Wait(cts.Token); var b = proxy.InnerExecutor; diff --git a/src/HotChocolate/Core/test/Execution.Tests/Configuration/TypeModuleTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Configuration/TypeModuleTests.cs index c96aeb97c6e..0526732792d 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Configuration/TypeModuleTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Configuration/TypeModuleTests.cs @@ -71,7 +71,8 @@ public async Task Ensure_Warmups_Are_Triggered_An_Appropriate_Number_Of_Times() // arrange var typeModule = new TriggerableTypeModule(); var warmups = 0; - var warmupResetEvent = new AutoResetEvent(false); + var warmupResetEvent = new ManualResetEventSlim(false); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var services = new ServiceCollection(); services @@ -87,7 +88,6 @@ public async Task Ensure_Warmups_Are_Triggered_An_Appropriate_Number_Of_Times() var provider = services.BuildServiceProvider(); var warmupService = provider.GetRequiredService(); - using var cts = new CancellationTokenSource(); _ = Task.Run(async () => { await warmupService.StartAsync(CancellationToken.None); @@ -95,23 +95,24 @@ public async Task Ensure_Warmups_Are_Triggered_An_Appropriate_Number_Of_Times() var resolver = provider.GetRequiredService(); - await resolver.GetRequestExecutorAsync(null, cts.Token); + await resolver.GetRequestExecutorAsync(); // act // assert - warmupResetEvent.WaitOne(); + warmupResetEvent.Wait(cts.Token); + warmupResetEvent.Reset(); Assert.Equal(1, warmups); - warmupResetEvent.Reset(); typeModule.TriggerChange(); - warmupResetEvent.WaitOne(); + warmupResetEvent.Wait(cts.Token); + warmupResetEvent.Reset(); Assert.Equal(2, warmups); - warmupResetEvent.Reset(); typeModule.TriggerChange(); - warmupResetEvent.WaitOne(); + warmupResetEvent.Wait(cts.Token); + warmupResetEvent.Reset(); Assert.Equal(3, warmups); } diff --git a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorProxyTests.cs b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorProxyTests.cs index 0bf05913747..8c5890c2a3a 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorProxyTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorProxyTests.cs @@ -31,7 +31,8 @@ public async Task Ensure_Executor_Is_Cached() public async Task Ensure_Executor_Is_Correctly_Swapped_When_Evicted() { // arrange - var executorUpdatedResetEvent = new AutoResetEvent(false); + var executorUpdatedResetEvent = new ManualResetEventSlim(false); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var resolver = new ServiceCollection() .AddGraphQL() @@ -54,7 +55,7 @@ public async Task Ensure_Executor_Is_Correctly_Swapped_When_Evicted() // act var a = await proxy.GetRequestExecutorAsync(CancellationToken.None); resolver.EvictRequestExecutor(); - executorUpdatedResetEvent.WaitOne(1000); + executorUpdatedResetEvent.Wait(cts.Token); var b = await proxy.GetRequestExecutorAsync(CancellationToken.None); // assert diff --git a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorResolverTests.cs b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorResolverTests.cs index 8a98bfe527f..21ab1e6d394 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorResolverTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorResolverTests.cs @@ -10,7 +10,8 @@ public class RequestExecutorResolverTests public async Task Operation_Cache_Should_Be_Scoped_To_Executor() { // arrange - var executorEvictedResetEvent = new AutoResetEvent(false); + var executorEvictedResetEvent = new ManualResetEventSlim(false); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var resolver = new ServiceCollection() .AddGraphQL() @@ -32,7 +33,7 @@ public async Task Operation_Cache_Should_Be_Scoped_To_Executor() .GetRequiredService(); resolver.EvictRequestExecutor(); - executorEvictedResetEvent.WaitOne(1000); + executorEvictedResetEvent.Wait(cts.Token); var secondExecutor = await resolver.GetRequestExecutorAsync(); var secondOperationCache = secondExecutor.Services.GetCombinedServices() @@ -46,8 +47,9 @@ public async Task Operation_Cache_Should_Be_Scoped_To_Executor() public async Task Executor_Should_Only_Be_Switched_Once_It_Is_Warmed_Up() { // arrange - var warmupResetEvent = new AutoResetEvent(true); - var executorEvictedResetEvent = new AutoResetEvent(false); + var warmupResetEvent = new ManualResetEventSlim(true); + var executorEvictedResetEvent = new ManualResetEventSlim(false); + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var resolver = new ServiceCollection() .AddGraphQL() @@ -55,7 +57,7 @@ public async Task Executor_Should_Only_Be_Switched_Once_It_Is_Warmed_Up() keepWarm: true, warmup: (_, _) => { - warmupResetEvent.WaitOne(1000); + warmupResetEvent.Wait(cts.Token); return Task.CompletedTask; }) @@ -83,10 +85,12 @@ public async Task Executor_Should_Only_Be_Switched_Once_It_Is_Warmed_Up() Assert.Same(initialExecutor, executorAfterEviction); warmupResetEvent.Set(); - executorEvictedResetEvent.WaitOne(1000); + executorEvictedResetEvent.Wait(cts.Token); var executorAfterWarmup = await resolver.GetRequestExecutorAsync(); Assert.NotSame(initialExecutor, executorAfterWarmup); + + cts.Dispose(); } [Theory] @@ -97,7 +101,8 @@ public async Task WarmupSchemaTasks_Are_Applied_Correct_Number_Of_Times( { // arrange var warmups = 0; - var executorEvictedResetEvent = new AutoResetEvent(false); + var executorEvictedResetEvent = new ManualResetEventSlim(false); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var resolver = new ServiceCollection() .AddGraphQL() @@ -125,7 +130,7 @@ public async Task WarmupSchemaTasks_Are_Applied_Correct_Number_Of_Times( var initialExecutor = await resolver.GetRequestExecutorAsync(); resolver.EvictRequestExecutor(); - executorEvictedResetEvent.WaitOne(1000); + executorEvictedResetEvent.Wait(cts.Token); var executorAfterEviction = await resolver.GetRequestExecutorAsync(); @@ -161,14 +166,15 @@ public async Task Calling_GetExecutorAsync_Multiple_Times_Only_Creates_One_Execu public async Task Executor_Resolution_Should_Be_Parallel() { // arrange - var schema1CreationResetEvent = new AutoResetEvent(false); + var schema1CreationResetEvent = new ManualResetEventSlim(false); + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var services = new ServiceCollection(); services .AddGraphQL("schema1") .AddQueryType(d => { - schema1CreationResetEvent.WaitOne(1000); + schema1CreationResetEvent.Wait(cts.Token); d.Field("foo").Resolve(""); }); services @@ -181,8 +187,8 @@ public async Task Executor_Resolution_Should_Be_Parallel() var resolver = provider.GetRequiredService(); // act - var executor1Task = Task.Run(async () => await resolver.GetRequestExecutorAsync("schema1")); - var executor2Task = Task.Run(async () => await resolver.GetRequestExecutorAsync("schema2")); + var executor1Task = Task.Run(async () => await resolver.GetRequestExecutorAsync("schema1"), cts.Token); + var executor2Task = Task.Run(async () => await resolver.GetRequestExecutorAsync("schema2"), cts.Token); // assert await executor2Task; @@ -190,5 +196,7 @@ public async Task Executor_Resolution_Should_Be_Parallel() schema1CreationResetEvent.Set(); await executor1Task; + + cts.Dispose(); } } From 28c895dc3739de763ff689afa515dd69281c7d82 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 11 Mar 2025 15:32:35 +0100 Subject: [PATCH 26/64] Fixed concurrency issue when building selector expressions. (#8118) --- .../SelectionExpressionBuilder.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/HotChocolate/Core/src/Execution.Projections/SelectionExpressionBuilder.cs b/src/HotChocolate/Core/src/Execution.Projections/SelectionExpressionBuilder.cs index 28fbb16cfdb..9096bca7532 100644 --- a/src/HotChocolate/Core/src/Execution.Projections/SelectionExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Execution.Projections/SelectionExpressionBuilder.cs @@ -11,14 +11,12 @@ namespace HotChocolate.Execution.Projections; internal sealed class SelectionExpressionBuilder { - private static readonly NullabilityInfoContext _nullabilityInfoContext = new(); - public Expression> BuildExpression(ISelection selection) { var rootType = typeof(TRoot); var parameter = Expression.Parameter(rootType, "root"); var requirements = selection.DeclaringOperation.Schema.Features.GetRequired(); - var context = new Context(parameter, rootType, requirements); + var context = new Context(parameter, rootType, requirements, new NullabilityInfoContext()); var root = new TypeContainer(); CollectTypes(context, selection, root); @@ -38,7 +36,7 @@ public Expression> BuildNodeExpression(ISelection sele var rootType = typeof(TRoot); var parameter = Expression.Parameter(rootType, "root"); var requirements = selection.DeclaringOperation.Schema.Features.GetRequired(); - var context = new Context(parameter, rootType, requirements); + var context = new Context(parameter, rootType, requirements, new NullabilityInfoContext()); var root = new TypeContainer(); var entityType = selection.DeclaringOperation @@ -252,7 +250,7 @@ private void CollectSelections( if (node.Nodes.Count == 0) { - if (IsNullableType(node.Property)) + if (IsNullableType(context, node.Property)) { var nullCheck = Expression.Condition( Expression.Equal(propertyAccessor, Expression.Constant(null)), @@ -273,7 +271,7 @@ private void CollectSelections( var newContext = context with { Parent = propertyAccessor, ParentType = node.Property.PropertyType }; var nestedExpression = BuildTypeSwitchExpression(newContext, node); - if (IsNullableType(node.Property)) + if (IsNullableType(context, node.Property)) { var nullCheck = Expression.Condition( Expression.Equal(propertyAccessor, Expression.Constant(null)), @@ -286,22 +284,22 @@ private void CollectSelections( return nestedExpression is null ? null : Expression.Bind(node.Property, nestedExpression); } - private static bool IsNullableType(PropertyInfo propertyInfo) + private static bool IsNullableType(Context context, PropertyInfo propertyInfo) { if (propertyInfo.PropertyType.IsValueType) { return Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null; } - var nullabilityInfo = _nullabilityInfoContext.Create(propertyInfo); - + var nullabilityInfo = context.NullabilityInfoContext.Create(propertyInfo); return nullabilityInfo.WriteState == NullabilityState.Nullable; } private readonly record struct Context( Expression Parent, Type ParentType, - FieldRequirementsMetadata Requirements) + FieldRequirementsMetadata Requirements, + NullabilityInfoContext NullabilityInfoContext) { public TypeNode? GetRequirements(ISelection selection) { From 836f58e4b05db0e0a246da185fe247acb133ddd4 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 11 Mar 2025 16:25:53 +0100 Subject: [PATCH 27/64] Throw error when is implicitly bound as schema type. (#8119) --- .../Properties/TypeResources.Designer.cs | 2403 +++++------------ .../src/Types/Properties/TypeResources.resx | 3 + .../Core/src/Types/SchemaBuilder.cs | 7 + .../Core/test/Types.Tests/CodeFirstTests.cs | 7 + 4 files changed, 719 insertions(+), 1701 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs index 34d4cb47131..234c40b7099 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs @@ -11,46 +11,32 @@ namespace HotChocolate.Properties { using System; - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class TypeResources { - private static global::System.Resources.ResourceManager resourceMan; + private static System.Resources.ResourceManager resourceMan; - private static global::System.Globalization.CultureInfo resourceCulture; + private static System.Globalization.CultureInfo resourceCulture; - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal TypeResources() { } - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HotChocolate.Properties.TypeResources", typeof(TypeResources).Assembly); + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("HotChocolate.Properties.TypeResources", typeof(TypeResources).Assembly); resourceMan = temp; } return resourceMan; } } - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -59,2956 +45,1971 @@ internal TypeResources() { } } - /// - /// Looks up a localized string similar to Cycle in object graph detected.. - /// - internal static string AnyType_CycleInObjectGraph { - get { - return ResourceManager.GetString("AnyType_CycleInObjectGraph", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An Applied Directive is an instances of a directive as applied to a schema element. This type is NOT specified by the graphql specification presently.. - /// - internal static string AppliedDirective_Description { - get { - return ResourceManager.GetString("AppliedDirective_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The argument `{0}` has no type. Specify the type with `.Argument("{0}", a.Type<MyType>())` to fix this issue.. - /// - internal static string Argument_TypeIsNull { + internal static string ThrowHelper_MissingDirectiveIfArgument { get { - return ResourceManager.GetString("Argument_TypeIsNull", resourceCulture); + return ResourceManager.GetString("ThrowHelper_MissingDirectiveIfArgument", resourceCulture); } } - /// - /// Looks up a localized string similar to The argument type has to be an input-type.. - /// internal static string ArgumentDescriptor_InputTypeViolation { get { return ResourceManager.GetString("ArgumentDescriptor_InputTypeViolation", resourceCulture); } } - /// - /// Looks up a localized string similar to Argument `{0}` of non-null type `{1}` must not be null.. - /// internal static string ArgumentValueBuilder_NonNull { get { return ResourceManager.GetString("ArgumentValueBuilder_NonNull", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified binding cannot be handled.. - /// - internal static string BindingCompiler_AddBinding_BindingCannotBeHandled { - get { - return ResourceManager.GetString("BindingCompiler_AddBinding_BindingCannotBeHandled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The `Boolean` scalar type represents `true` or `false`.. - /// internal static string BooleanType_Description { get { return ResourceManager.GetString("BooleanType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The `Byte` scalar type represents non-fractional whole numeric values. Byte can represent values between 0 and 255.. - /// internal static string ByteType_Description { get { return ResourceManager.GetString("ByteType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to Could not resolve the claims principal.. - /// - internal static string ClaimsPrincipalParameterExpressionBuilder_NoClaimsFound { - get { - return ResourceManager.GetString("ClaimsPrincipalParameterExpressionBuilder_NoClaimsFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A segment of a collection.. - /// - internal static string CollectionSegmentType_Description { - get { - return ResourceManager.GetString("CollectionSegmentType_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A flattened list of the items.. - /// - internal static string CollectionSegmentType_Items_Description { - get { - return ResourceManager.GetString("CollectionSegmentType_Items_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Information to aid in pagination.. - /// - internal static string CollectionSegmentType_PageInfo_Description { - get { - return ResourceManager.GetString("CollectionSegmentType_PageInfo_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified IComplexTypeFieldBindingBuilder-implementation is not supported.. - /// internal static string ComplexTypeBindingBuilder_FieldBuilderNotSupported { get { return ResourceManager.GetString("ComplexTypeBindingBuilder_FieldBuilderNotSupported", resourceCulture); } } - /// - /// Looks up a localized string similar to The field binding builder is not completed and cannot be added.. - /// internal static string ComplexTypeBindingBuilder_FieldNotComplete { get { return ResourceManager.GetString("ComplexTypeBindingBuilder_FieldNotComplete", resourceCulture); } } - /// - /// Looks up a localized string similar to A connection to a list of items.. - /// - internal static string ConnectionType_Description { - get { - return ResourceManager.GetString("ConnectionType_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A list of edges.. - /// - internal static string ConnectionType_Edges_Description { - get { - return ResourceManager.GetString("ConnectionType_Edges_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A flattened list of the nodes.. - /// - internal static string ConnectionType_Nodes_Description { - get { - return ResourceManager.GetString("ConnectionType_Nodes_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Information to aid in pagination.. - /// - internal static string ConnectionType_PageInfo_Description { - get { - return ResourceManager.GetString("ConnectionType_PageInfo_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Identifies the total count of items in the connection.. - /// - internal static string ConnectionType_TotalCount_Description { - get { - return ResourceManager.GetString("ConnectionType_TotalCount_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The DataLoader key cannot be null or empty.. - /// internal static string DataLoaderRegistry_KeyNullOrEmpty { get { return ResourceManager.GetString("DataLoaderRegistry_KeyNullOrEmpty", resourceCulture); } } - /// - /// Looks up a localized string similar to The DataLoader `{0}` needs to be register with the dependency injection provider.. - /// - internal static string DataLoaderResolverContextExtensions_CreateDataLoader_AbstractType { - get { - return ResourceManager.GetString("DataLoaderResolverContextExtensions_CreateDataLoader_AbstractType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unable to create DataLoader `{0}`.. - /// - internal static string DataLoaderResolverContextExtensions_CreateDataLoader_UnableToCreate { - get { - return ResourceManager.GetString("DataLoaderResolverContextExtensions_CreateDataLoader_UnableToCreate", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No DataLoader registry was registered with your dependency injection.. - /// internal static string DataLoaderResolverContextExtensions_RegistryIsNull { get { return ResourceManager.GetString("DataLoaderResolverContextExtensions_RegistryIsNull", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to register a DataLoader with your DataLoader registry.. - /// internal static string DataLoaderResolverContextExtensions_UnableToRegister { get { return ResourceManager.GetString("DataLoaderResolverContextExtensions_UnableToRegister", resourceCulture); } } - /// - /// Looks up a localized string similar to The `DateTime` scalar represents an ISO-8601 compliant date time type.. - /// internal static string DateTimeType_Description { get { return ResourceManager.GetString("DateTimeType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The `Date` scalar represents an ISO-8601 compliant date type.. - /// internal static string DateType_Description { get { return ResourceManager.GetString("DateType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The `Decimal` scalar type represents a decimal floating-point number.. - /// internal static string DecimalType_Description { get { return ResourceManager.GetString("DecimalType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The DataLoader `{0}` was not of the requested type `{1}`.. - /// - internal static string DefaultDataLoaderRegistry_GetOrRegister { - get { - return ResourceManager.GetString("DefaultDataLoaderRegistry_GetOrRegister", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The fieldName cannot be null or empty.. - /// - internal static string DefaultNamingConventions_FormatFieldName_EmptyOrNull { - get { - return ResourceManager.GetString("DefaultNamingConventions_FormatFieldName_EmptyOrNull", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Only methods are allowed.. - /// - internal static string DefaultResolverCompilerService_CompileSubscribe_OnlyMethodsAllowed { - get { - return ResourceManager.GetString("DefaultResolverCompilerService_CompileSubscribe_OnlyMethodsAllowed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The public method should already have ensured that we do not have members other than method or property at this point.. - /// - internal static string DefaultResolverCompilerService_CreateResolver_ArgumentValidationError { - get { - return ResourceManager.GetString("DefaultResolverCompilerService_CreateResolver_ArgumentValidationError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified member has to be a method or a property.. - /// internal static string DefaultTypeInspector_MemberInvalid { get { return ResourceManager.GetString("DefaultTypeInspector_MemberInvalid", resourceCulture); } } - /// - /// Looks up a localized string similar to The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`.. - /// - internal static string DeferDirectiveType_Description { - get { - return ResourceManager.GetString("DeferDirectiveType_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Deferred when true.. - /// - internal static string DeferDirectiveType_If_Description { - get { - return ResourceManager.GetString("DeferDirectiveType_If_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to.. - /// - internal static string DeferDirectiveType_Label_Description { - get { - return ResourceManager.GetString("DeferDirectiveType_Label_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Only type system objects are allowed as schema type.. - /// internal static string DependencyDescriptorBase_OnlyTsoIsAllowed { get { return ResourceManager.GetString("DependencyDescriptorBase_OnlyTsoIsAllowed", resourceCulture); } } - /// - /// Looks up a localized string similar to Deprecations include a reason for why it is deprecated, which is formatted using Markdown syntax (as specified by CommonMark).. - /// - internal static string DeprecatedDirectiveType_ReasonDescription { - get { - return ResourceManager.GetString("DeprecatedDirectiveType_ReasonDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The @deprecated directive is used within the type system definition language to indicate deprecated portions of a GraphQL service’s schema,such as deprecated fields on a type or deprecated enum values.. - /// - internal static string DeprecatedDirectiveType_TypeDescription { - get { - return ResourceManager.GetString("DeprecatedDirectiveType_TypeDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. - /// - ///In some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.. - /// - internal static string Directive_Description { - get { - return ResourceManager.GetString("Directive_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The argument name is invalid.. - /// - internal static string Directive_GetArgument_ArgumentNameIsInvalid { - get { - return ResourceManager.GetString("Directive_GetArgument_ArgumentNameIsInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The directive '{0}' has no argument with the name '{1}'.. - /// - internal static string Directive_GetArgumentValue_UnknownArgument { - get { - return ResourceManager.GetString("Directive_GetArgumentValue_UnknownArgument", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use `locations`.. - /// - internal static string Directive_UseLocation { - get { - return ResourceManager.GetString("Directive_UseLocation", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Directive arguments can have names and values. The values are in graphql SDL syntax printed as a string. This type is NOT specified by the graphql specification presently.. - /// - internal static string DirectiveArgument_Description { - get { - return ResourceManager.GetString("DirectiveArgument_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified directive `@{0}` is unique and cannot be added twice.. - /// internal static string DirectiveCollection_DirectiveIsUnique { get { return ResourceManager.GetString("DirectiveCollection_DirectiveIsUnique", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified directive `@{0}` is not allowed on the current location `{1}`.. - /// internal static string DirectiveCollection_LocationNotAllowed { get { return ResourceManager.GetString("DirectiveCollection_LocationNotAllowed", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to an argument definition. - /// internal static string DirectiveLocation_ArgumentDefinition { get { return ResourceManager.GetString("DirectiveLocation_ArgumentDefinition", resourceCulture); } } - /// - /// Looks up a localized string similar to A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.. - /// internal static string DirectiveLocation_Description { get { return ResourceManager.GetString("DirectiveLocation_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to an enum definition.. - /// internal static string DirectiveLocation_Enum { get { return ResourceManager.GetString("DirectiveLocation_Enum", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to an enum value definition.. - /// internal static string DirectiveLocation_EnumValue { get { return ResourceManager.GetString("DirectiveLocation_EnumValue", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to a field.. - /// internal static string DirectiveLocation_Field { get { return ResourceManager.GetString("DirectiveLocation_Field", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to a field definition.. - /// internal static string DirectiveLocation_FieldDefinition { get { return ResourceManager.GetString("DirectiveLocation_FieldDefinition", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to a fragment definition.. - /// internal static string DirectiveLocation_FragmentDefinition { get { return ResourceManager.GetString("DirectiveLocation_FragmentDefinition", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to a fragment spread.. - /// internal static string DirectiveLocation_FragmentSpread { get { return ResourceManager.GetString("DirectiveLocation_FragmentSpread", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to an inline fragment.. - /// internal static string DirectiveLocation_InlineFragment { get { return ResourceManager.GetString("DirectiveLocation_InlineFragment", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to an input object field definition.. - /// internal static string DirectiveLocation_InputFieldDefinition { get { return ResourceManager.GetString("DirectiveLocation_InputFieldDefinition", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to an input object type definition.. - /// internal static string DirectiveLocation_InputObject { get { return ResourceManager.GetString("DirectiveLocation_InputObject", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to an interface definition.. - /// internal static string DirectiveLocation_Interface { get { return ResourceManager.GetString("DirectiveLocation_Interface", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to a mutation operation.. - /// internal static string DirectiveLocation_Mutation { get { return ResourceManager.GetString("DirectiveLocation_Mutation", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to an object type definition.. - /// internal static string DirectiveLocation_Object { get { return ResourceManager.GetString("DirectiveLocation_Object", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to a query operation.. - /// internal static string DirectiveLocation_Query { get { return ResourceManager.GetString("DirectiveLocation_Query", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to a scalar definition.. - /// internal static string DirectiveLocation_Scalar { get { return ResourceManager.GetString("DirectiveLocation_Scalar", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to a schema definition.. - /// internal static string DirectiveLocation_Schema { get { return ResourceManager.GetString("DirectiveLocation_Schema", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to a subscription operation.. - /// internal static string DirectiveLocation_Subscription { get { return ResourceManager.GetString("DirectiveLocation_Subscription", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to a union definition.. - /// internal static string DirectiveLocation_Union { get { return ResourceManager.GetString("DirectiveLocation_Union", resourceCulture); } } - /// - /// Looks up a localized string similar to Location adjacent to a variable definition.. - /// - internal static string DirectiveLocation_VariableDefinition { + internal static string DirectiveTypeDescriptor_OnlyProperties { get { - return ResourceManager.GetString("DirectiveLocation_VariableDefinition", resourceCulture); + return ResourceManager.GetString("DirectiveTypeDescriptor_OnlyProperties", resourceCulture); } } - /// - /// Looks up a localized string similar to The `{0}` directive does not declare any location on which it is valid.. - /// internal static string DirectiveType_NoLocations { get { return ResourceManager.GetString("DirectiveType_NoLocations", resourceCulture); } } - /// - /// Looks up a localized string similar to Replace Middleware with `Use`.. - /// internal static string DirectiveType_ReplaceWithUse { get { return ResourceManager.GetString("DirectiveType_ReplaceWithUse", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to convert the argument value to the specified type.. - /// internal static string DirectiveType_UnableToConvert { get { return ResourceManager.GetString("DirectiveType_UnableToConvert", resourceCulture); } } - /// - /// Looks up a localized string similar to Only property expressions are allowed to describe a directive type argument.. - /// - internal static string DirectiveTypeDescriptor_OnlyProperties { + internal static string Directive_Description { get { - return ResourceManager.GetString("DirectiveTypeDescriptor_OnlyProperties", resourceCulture); + return ResourceManager.GetString("Directive_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to A cursor for use in pagination.. - /// - internal static string EdgeType_Cursor_Description { + internal static string Directive_UseLocation { get { - return ResourceManager.GetString("EdgeType_Cursor_Description", resourceCulture); + return ResourceManager.GetString("Directive_UseLocation", resourceCulture); } } - /// - /// Looks up a localized string similar to An edge in a connection.. - /// - internal static string EdgeType_Description { + internal static string EnumTypeExtension_CannotMerge { get { - return ResourceManager.GetString("EdgeType_Description", resourceCulture); + return ResourceManager.GetString("EnumTypeExtension_CannotMerge", resourceCulture); } } - /// - /// Looks up a localized string similar to Edge types that have a non-object node are not supported.. - /// - internal static string EdgeType_IsInstanceOfType_NonObject { + internal static string EnumTypeExtension_ValueTypeInvalid { get { - return ResourceManager.GetString("EdgeType_IsInstanceOfType_NonObject", resourceCulture); + return ResourceManager.GetString("EnumTypeExtension_ValueTypeInvalid", resourceCulture); } } - /// - /// Looks up a localized string similar to The item at the end of the edge.. - /// - internal static string EdgeType_Node_Description { + internal static string EnumType_NoValues { get { - return ResourceManager.GetString("EdgeType_Node_Description", resourceCulture); + return ResourceManager.GetString("EnumType_NoValues", resourceCulture); } } - /// - /// Looks up a localized string similar to The enum type `{0}` has no values.. - /// - internal static string EnumType_NoValues { + internal static string EnumValue_Description { get { - return ResourceManager.GetString("EnumType_NoValues", resourceCulture); + return ResourceManager.GetString("EnumValue_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The enum type extension can only be merged with an enum type.. - /// - internal static string EnumTypeExtension_CannotMerge { + internal static string EnumValue_ValueIsNull { get { - return ResourceManager.GetString("EnumTypeExtension_CannotMerge", resourceCulture); + return ResourceManager.GetString("EnumValue_ValueIsNull", resourceCulture); } } - /// - /// Looks up a localized string similar to The enum value `{0}` of the enum type extension is not assignable with the target enum type.. - /// - internal static string EnumTypeExtension_ValueTypeInvalid { + internal static string FieldInitHelper_InvalidDefaultValue { get { - return ResourceManager.GetString("EnumTypeExtension_ValueTypeInvalid", resourceCulture); + return ResourceManager.GetString("FieldInitHelper_InvalidDefaultValue", resourceCulture); } } - /// - /// Looks up a localized string similar to One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.. - /// - internal static string EnumValue_Description { + internal static string FieldInitHelper_NoFields { get { - return ResourceManager.GetString("EnumValue_Description", resourceCulture); + return ResourceManager.GetString("FieldInitHelper_NoFields", resourceCulture); } } - /// - /// Looks up a localized string similar to The inner value of enum value cannot be null or empty.. - /// - internal static string EnumValue_ValueIsNull { + internal static string Field_Description { get { - return ResourceManager.GetString("EnumValue_ValueIsNull", resourceCulture); + return ResourceManager.GetString("Field_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The field `{0}` must only declare additional arguments to an implemented field that are nullable.. - /// - internal static string ErrorHelper_AdditionalArgumentNotNullable { + internal static string FloatType_Description { get { - return ResourceManager.GetString("ErrorHelper_AdditionalArgumentNotNullable", resourceCulture); + return ResourceManager.GetString("FloatType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The argument `{0}` of the implemented field `{1}` must be defined. The field `{2}` must include an argument of the same name for every argument defined on the implemented field of the interface type `{3}`.. - /// - internal static string ErrorHelper_ArgumentNotImplemented { + internal static string IdType_Description { get { - return ResourceManager.GetString("ErrorHelper_ArgumentNotImplemented", resourceCulture); + return ResourceManager.GetString("IdType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to resolve the interface type. For more details look at the error object.. - /// - internal static string ErrorHelper_CompleteInterfacesHelper_UnableToResolveInterface { + internal static string InputField_CannotSetValue { get { - return ResourceManager.GetString("ErrorHelper_CompleteInterfacesHelper_UnableToResolveInterface", resourceCulture); + return ResourceManager.GetString("InputField_CannotSetValue", resourceCulture); } } - /// - /// Looks up a localized string similar to The argument `{0}` does not exist on the directive `{1}`.. - /// - internal static string ErrorHelper_DirectiveCollection_ArgumentDoesNotExist { + internal static string InputObjectTypeExtension_CannotMerge { get { - return ResourceManager.GetString("ErrorHelper_DirectiveCollection_ArgumentDoesNotExist", resourceCulture); + return ResourceManager.GetString("InputObjectTypeExtension_CannotMerge", resourceCulture); } } - /// - /// Looks up a localized string similar to The argument `{0}` of directive `{1}` mustn't be null.. - /// - internal static string ErrorHelper_DirectiveCollection_ArgumentNonNullViolation { + internal static string InputObjectType_CannotParseLiteral { get { - return ResourceManager.GetString("ErrorHelper_DirectiveCollection_ArgumentNonNullViolation", resourceCulture); + return ResourceManager.GetString("InputObjectType_CannotParseLiteral", resourceCulture); } } - /// - /// Looks up a localized string similar to The directive arguments have invalid values: '{0}' at {1}.. - /// - internal static string ErrorHelper_DirectiveCollection_ArgumentValueTypeIsWrong { + internal static string InputObjectType_NoFields { get { - return ResourceManager.GetString("ErrorHelper_DirectiveCollection_ArgumentValueTypeIsWrong", resourceCulture); + return ResourceManager.GetString("InputObjectType_NoFields", resourceCulture); } } - /// - /// Looks up a localized string similar to The field `{0}` declares the data middleware `{1}` more than once.. - /// - internal static string ErrorHelper_DuplicateDataMiddlewareDetected_Message { + internal static string InputTypeNonNullCheck_ValueIsNull { get { - return ResourceManager.GetString("ErrorHelper_DuplicateDataMiddlewareDetected_Message", resourceCulture); + return ResourceManager.GetString("InputTypeNonNullCheck_ValueIsNull", resourceCulture); } } - /// - /// Looks up a localized string similar to The following {0}{1} `{2}` {3} declared multiple times on `{4}`.. - /// - internal static string ErrorHelper_DuplicateFieldName_Message { + internal static string InputValue_DefaultValue { get { - return ResourceManager.GetString("ErrorHelper_DuplicateFieldName_Message", resourceCulture); + return ResourceManager.GetString("InputValue_DefaultValue", resourceCulture); } } - /// - /// Looks up a localized string similar to The maximum number of nodes that can be fetched at once is {0}. This selection tried to fetch {1} nodes that exceeded the maximum allowed amount.. - /// - internal static string ErrorHelper_FetchedToManyNodesAtOnce { + internal static string InputValue_Description { get { - return ResourceManager.GetString("ErrorHelper_FetchedToManyNodesAtOnce", resourceCulture); + return ResourceManager.GetString("InputValue_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The field `{0}` must be implemented by {1} type `{2}`.. - /// - internal static string ErrorHelper_FieldNotImplemented { + internal static string InterfaceImplRule_ArgumentsDoNotMatch { get { - return ResourceManager.GetString("ErrorHelper_FieldNotImplemented", resourceCulture); + return ResourceManager.GetString("InterfaceImplRule_ArgumentsDoNotMatch", resourceCulture); } } - /// - /// Looks up a localized string similar to Cannot reference Input Object `{0}` within itself through a series of non-null fields `{1}`.. - /// - internal static string ErrorHelper_InputObjectMustNotHaveRecursiveNonNullableReferencesToSelf { + internal static string InterfaceImplRule_ArgumentsNotImpl { get { - return ResourceManager.GetString("ErrorHelper_InputObjectMustNotHaveRecursiveNonNullableReferencesToSelf", resourceCulture); + return ResourceManager.GetString("InterfaceImplRule_ArgumentsNotImpl", resourceCulture); } } - /// - /// Looks up a localized string similar to There is no object type implementing interface `{0}`.. - /// - internal static string ErrorHelper_InterfaceHasNoImplementation { + internal static string InterfaceImplRule_FieldNotImpl { get { - return ResourceManager.GetString("ErrorHelper_InterfaceHasNoImplementation", resourceCulture); + return ResourceManager.GetString("InterfaceImplRule_FieldNotImpl", resourceCulture); } } - /// - /// Looks up a localized string similar to The named argument `{0}` on field `{1}` must accept the same type `{2}` (invariant) as that named argument on the interface `{3}`.. - /// - internal static string ErrorHelper_InvalidArgumentType { + internal static string InterfaceImplRule_FieldTypeInvalid { get { - return ResourceManager.GetString("ErrorHelper_InvalidArgumentType", resourceCulture); + return ResourceManager.GetString("InterfaceImplRule_FieldTypeInvalid", resourceCulture); } } - /// - /// Looks up a localized string similar to Field `{0}` must return a type which is equal to or a subtype of (covariant) the return type `{1}` of the interface field.. - /// - internal static string ErrorHelper_InvalidFieldType { + internal static string InterfaceImplRule_ReturnTypeInvalid { get { - return ResourceManager.GetString("ErrorHelper_InvalidFieldType", resourceCulture); + return ResourceManager.GetString("InterfaceImplRule_ReturnTypeInvalid", resourceCulture); } } - /// - /// Looks up a localized string similar to The middleware pipeline order for the field `{0}` is invalid. Middleware order is important especially with data pipelines. The correct order of a data pipeline is as follows: UseDbContext -> UsePaging -> UseProjection -> UseFiltering -> UseSorting. You may omit any of these middleware or have other middleware in between but you need to abide by the overall order. Your order is: {1}.. - /// - internal static string ErrorHelper_MiddlewareOrderInvalid { + internal static string InterfaceTypeExtension_CannotMerge { get { - return ResourceManager.GetString("ErrorHelper_MiddlewareOrderInvalid", resourceCulture); + return ResourceManager.GetString("InterfaceTypeExtension_CannotMerge", resourceCulture); } } - /// - /// Looks up a localized string similar to The {0} type `{1}` has to at least define one field in order to be valid.. - /// - internal static string ErrorHelper_NeedsOneAtLeastField { + internal static string IntType_Description { get { - return ResourceManager.GetString("ErrorHelper_NeedsOneAtLeastField", resourceCulture); + return ResourceManager.GetString("IntType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The node resolver `{0}` must specify exactly one argument.. - /// - internal static string ErrorHelper_NodeResolver_MustHaveExactlyOneIdArg { + internal static string LongType_Description { get { - return ResourceManager.GetString("ErrorHelper_NodeResolver_MustHaveExactlyOneIdArg", resourceCulture); + return ResourceManager.GetString("LongType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The node resolver `{0}` must return an object type.. - /// - internal static string ErrorHelper_NodeResolver_MustReturnObject { + internal static string NameType_Description { get { - return ResourceManager.GetString("ErrorHelper_NodeResolver_MustReturnObject", resourceCulture); + return ResourceManager.GetString("NameType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The type `{0}` implementing the node interface must expose an id field.. - /// - internal static string ErrorHelper_NodeResolver_NodeTypeHasNoId { + internal static string Name_Cannot_BeEmpty { get { - return ResourceManager.GetString("ErrorHelper_NodeResolver_NodeTypeHasNoId", resourceCulture); + return ResourceManager.GetString("Name_Cannot_BeEmpty", resourceCulture); } } - /// - /// Looks up a localized string similar to The type `{0}` implements the node interface but does not provide a node resolver for re-fetching.. - /// - internal static string ErrorHelper_NodeResolverMissing { + internal static string ObjectFieldDescriptorBase_FieldType { get { - return ResourceManager.GetString("ErrorHelper_NodeResolverMissing", resourceCulture); + return ResourceManager.GetString("ObjectFieldDescriptorBase_FieldType", resourceCulture); } } - /// - /// Looks up a localized string similar to The type {0} is invalid because the runtime type is a {1}. It is not supported to have type system members as runtime types.. - /// - internal static string ErrorHelper_NoSchemaTypesAllowedAsRuntimeType { + internal static string ObjectTypeDescriptor_InterfaceBaseClass { get { - return ResourceManager.GetString("ErrorHelper_NoSchemaTypesAllowedAsRuntimeType", resourceCulture); + return ResourceManager.GetString("ObjectTypeDescriptor_InterfaceBaseClass", resourceCulture); } } - /// - /// Looks up a localized string similar to The {0} type must also declare all interfaces declared by implemented interfaces.. - /// - internal static string ErrorHelper_NotTransitivelyImplemented { + internal static string InterfaceTypeDescriptor_InterfaceBaseClass { get { - return ResourceManager.GetString("ErrorHelper_NotTransitivelyImplemented", resourceCulture); + return ResourceManager.GetString("InterfaceTypeDescriptor_InterfaceBaseClass", resourceCulture); } } - /// - /// Looks up a localized string similar to The field `{0}.{1}` has no resolver.. - /// - internal static string ErrorHelper_ObjectField_HasNoResolver { + internal static string ObjectTypeDescriptor_MustBePropertyOrMethod { get { - return ResourceManager.GetString("ErrorHelper_ObjectField_HasNoResolver", resourceCulture); + return ResourceManager.GetString("ObjectTypeDescriptor_MustBePropertyOrMethod", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to infer or resolve the type of field {0}.{1}. Try to explicitly provide the type like the following: `descriptor.Field("field").Type<List<StringType>>()`.. - /// - internal static string ErrorHelper_ObjectType_UnableToInferOrResolveType { + internal static string ObjectTypeDescriptor_ResolveWith_NonAbstract { get { - return ResourceManager.GetString("ErrorHelper_ObjectType_UnableToInferOrResolveType", resourceCulture); + return ResourceManager.GetString("ObjectTypeDescriptor_ResolveWith_NonAbstract", resourceCulture); } } - /// - /// Looks up a localized string similar to Oneof Input Object `{0}` must only have nullable fields without default values. Edit your type and make the field{1} `{2}` nullable and remove any defaults.. - /// - internal static string ErrorHelper_OneofInputObjectMustHaveNullableFieldsWithoutDefaults { + internal static string NodeDescriptor_MustBeMethod { get { - return ResourceManager.GetString("ErrorHelper_OneofInputObjectMustHaveNullableFieldsWithoutDefaults", resourceCulture); + return ResourceManager.GetString("NodeDescriptor_MustBeMethod", resourceCulture); } } - /// - /// Looks up a localized string similar to There is no node resolver registered for type `{0}`.. - /// - internal static string ErrorHelper_Relay_NoNodeResolver { + internal static string NodeDescriptor_IdMember { get { - return ResourceManager.GetString("ErrorHelper_Relay_NoNodeResolver", resourceCulture); + return ResourceManager.GetString("NodeDescriptor_IdMember", resourceCulture); } } - /// - /// Looks up a localized string similar to Required argument {0} cannot be deprecated.. - /// - internal static string ErrorHelper_RequiredArgumentCannotBeDeprecated { + internal static string ObjectTypeDescriptor_Resolver_SchemaType { get { - return ResourceManager.GetString("ErrorHelper_RequiredArgumentCannotBeDeprecated", resourceCulture); + return ResourceManager.GetString("ObjectTypeDescriptor_Resolver_SchemaType", resourceCulture); } } - /// - /// Looks up a localized string similar to Required input field {0} cannot be deprecated.. - /// - internal static string ErrorHelper_RequiredFieldCannotBeDeprecated { + internal static string Reflection_MemberMust_BeMethodOrProperty { get { - return ResourceManager.GetString("ErrorHelper_RequiredFieldCannotBeDeprecated", resourceCulture); + return ResourceManager.GetString("Reflection_MemberMust_BeMethodOrProperty", resourceCulture); } } - /// - /// Looks up a localized string similar to Field names starting with `__` are reserved for the GraphQL specification.. - /// - internal static string ErrorHelper_TwoUnderscoresNotAllowedField { + internal static string ResolverCompiler_UnknownParameterType { get { - return ResourceManager.GetString("ErrorHelper_TwoUnderscoresNotAllowedField", resourceCulture); + return ResourceManager.GetString("ResolverCompiler_UnknownParameterType", resourceCulture); } } - /// - /// Looks up a localized string similar to Argument names starting with `__` are reserved for the GraphQL specification.. - /// - internal static string ErrorHelper_TwoUnderscoresNotAllowedOnArgument { + internal static string ResolverTypeBindingBuilder_FieldBuilderNotSupported { get { - return ResourceManager.GetString("ErrorHelper_TwoUnderscoresNotAllowedOnArgument", resourceCulture); + return ResourceManager.GetString("ResolverTypeBindingBuilder_FieldBuilderNotSupported", resourceCulture); } } - /// - /// Looks up a localized string similar to Names starting with `__` are reserved for the GraphQL specification.. - /// - internal static string ErrorHelper_TwoUnderscoresNotAllowedOnDirectiveName { + internal static string ResolverTypeBindingBuilder_FieldNotComplete { get { - return ResourceManager.GetString("ErrorHelper_TwoUnderscoresNotAllowedOnDirectiveName", resourceCulture); + return ResourceManager.GetString("ResolverTypeBindingBuilder_FieldNotComplete", resourceCulture); } } - /// - /// Looks up a localized string similar to The event message parameter can only be used in a subscription context.. - /// - internal static string EventMessageParameterExpressionBuilder_MessageNotFound { + internal static string Scalar_Cannot_Deserialize { get { - return ResourceManager.GetString("EventMessageParameterExpressionBuilder_MessageNotFound", resourceCulture); + return ResourceManager.GetString("Scalar_Cannot_Deserialize", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified key `{0}` does not exist on `context.ContextData`.. - /// - internal static string ExpressionHelper_GetGlobalStateWithDefault_NoDefaults { + internal static string Scalar_Cannot_ParseLiteral { get { - return ResourceManager.GetString("ExpressionHelper_GetGlobalStateWithDefault_NoDefaults", resourceCulture); + return ResourceManager.GetString("Scalar_Cannot_ParseLiteral", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified key `{0}` does not exist on `context.ScopedContextData`.. - /// - internal static string ExpressionHelper_GetScopedStateWithDefault_NoDefaultValue { + internal static string Scalar_Cannot_ParseValue { get { - return ResourceManager.GetString("ExpressionHelper_GetScopedStateWithDefault_NoDefaultValue", resourceCulture); + return ResourceManager.GetString("Scalar_Cannot_ParseValue", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified context key does not exist.. - /// - internal static string ExpressionHelper_ResolveScopedContextData_KeyDoesNotExist { + internal static string Scalar_Cannot_Serialize { get { - return ResourceManager.GetString("ExpressionHelper_ResolveScopedContextData_KeyDoesNotExist", resourceCulture); + return ResourceManager.GetString("Scalar_Cannot_Serialize", resourceCulture); } } - /// - /// Looks up a localized string similar to The non-generic IExecutable interface cannot be used as a type in the schema.. - /// - internal static string ExtendedTypeReferenceHandler_NonGenericExecutableNotAllowed { + internal static string SchemaBuilderExtensions_DirectiveTypeIsBaseType { get { - return ResourceManager.GetString("ExtendedTypeReferenceHandler_NonGenericExecutableNotAllowed", resourceCulture); + return ResourceManager.GetString("SchemaBuilderExtensions_DirectiveTypeIsBaseType", resourceCulture); } } - /// - /// Looks up a localized string similar to Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.. - /// - internal static string Field_Description { + internal static string SchemaBuilderExtensions_MustBeDirectiveType { get { - return ResourceManager.GetString("Field_Description", resourceCulture); + return ResourceManager.GetString("SchemaBuilderExtensions_MustBeDirectiveType", resourceCulture); } } - /// - /// Looks up a localized string similar to The max expected field count cannot be smaller than 1.. - /// - internal static string FieldInitHelper_CompleteFields_MaxFieldCountToSmall { + internal static string SchemaBuilderExtensions_SchemaIsEmpty { get { - return ResourceManager.GetString("FieldInitHelper_CompleteFields_MaxFieldCountToSmall", resourceCulture); + return ResourceManager.GetString("SchemaBuilderExtensions_SchemaIsEmpty", resourceCulture); } } - /// - /// Looks up a localized string similar to Could not parse the native value of input field `{0}`.. - /// - internal static string FieldInitHelper_InvalidDefaultValue { + internal static string SchemaBuilder_Binding_CannotBeHandled { get { - return ResourceManager.GetString("FieldInitHelper_InvalidDefaultValue", resourceCulture); + return ResourceManager.GetString("SchemaBuilder_Binding_CannotBeHandled", resourceCulture); } } - /// - /// Looks up a localized string similar to {0} `{1}` has no fields declared.. - /// - internal static string FieldInitHelper_NoFields { + internal static string SchemaBuilder_Binding_Invalid { get { - return ResourceManager.GetString("FieldInitHelper_NoFields", resourceCulture); + return ResourceManager.GetString("SchemaBuilder_Binding_Invalid", resourceCulture); } } - /// - /// Looks up a localized string similar to The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).. - /// - internal static string FloatType_Description { + internal static string SchemaBuilder_ISchemaNotTso { get { - return ResourceManager.GetString("FloatType_Description", resourceCulture); + return ResourceManager.GetString("SchemaBuilder_ISchemaNotTso", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to decode the id string.. - /// - internal static string IdSerializer_UnableToDecode { + internal static string SchemaBuilder_NoQueryType { get { - return ResourceManager.GetString("IdSerializer_UnableToDecode", resourceCulture); + return ResourceManager.GetString("SchemaBuilder_NoQueryType", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to encode data.. - /// - internal static string IdSerializer_UnableToEncode { + internal static string SchemaBuilder_RootType_MustBeClass { get { - return ResourceManager.GetString("IdSerializer_UnableToEncode", resourceCulture); + return ResourceManager.GetString("SchemaBuilder_RootType_MustBeClass", resourceCulture); } } - /// - /// Looks up a localized string similar to The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.. - /// - internal static string IdType_Description { + internal static string SchemaBuilder_RootType_MustBeObjectType { get { - return ResourceManager.GetString("IdType_Description", resourceCulture); + return ResourceManager.GetString("SchemaBuilder_RootType_MustBeObjectType", resourceCulture); } } - /// - /// Looks up a localized string similar to Included when true.. - /// - internal static string IncludeDirectiveType_IfDescription { + internal static string SchemaBuilder_RootType_NonGenericType { get { - return ResourceManager.GetString("IncludeDirectiveType_IfDescription", resourceCulture); + return ResourceManager.GetString("SchemaBuilder_RootType_NonGenericType", resourceCulture); } } - /// - /// Looks up a localized string similar to Directs the executor to include this field or fragment only when the `if` argument is true.. - /// - internal static string IncludeDirectiveType_TypeDescription { + internal static string SchemaBuilder_SchemaTypeInvalid { get { - return ResourceManager.GetString("IncludeDirectiveType_TypeDescription", resourceCulture); + return ResourceManager.GetString("SchemaBuilder_SchemaTypeInvalid", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to set the input field value.. - /// - internal static string InputField_CannotSetValue { + internal static string SchemaErrorBuilder_MessageIsNull { get { - return ResourceManager.GetString("InputField_CannotSetValue", resourceCulture); + return ResourceManager.GetString("SchemaErrorBuilder_MessageIsNull", resourceCulture); } } - /// - /// Looks up a localized string similar to The input object type can only parse object value literals.. - /// - internal static string InputObjectType_CannotParseLiteral { + internal static string SchemaField_Description { get { - return ResourceManager.GetString("InputObjectType_CannotParseLiteral", resourceCulture); + return ResourceManager.GetString("SchemaField_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The input object `{0}` does not have any fields.. - /// - internal static string InputObjectType_NoFields { + internal static string SchemaSyntaxVisitor_UnknownOperationType { get { - return ResourceManager.GetString("InputObjectType_NoFields", resourceCulture); + return ResourceManager.GetString("SchemaSyntaxVisitor_UnknownOperationType", resourceCulture); } } - /// - /// Looks up a localized string similar to Only properties are allowed for input types.. - /// - internal static string InputObjectTypeDescriptor_OnlyProperties { + internal static string Schema_Description { get { - return ResourceManager.GetString("InputObjectTypeDescriptor_OnlyProperties", resourceCulture); + return ResourceManager.GetString("Schema_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The input object type extension can only be merged with an input object type.. - /// - internal static string InputObjectTypeExtension_CannotMerge { + internal static string Schema_Directives { get { - return ResourceManager.GetString("InputObjectTypeExtension_CannotMerge", resourceCulture); + return ResourceManager.GetString("Schema_Directives", resourceCulture); } } - /// - /// Looks up a localized string similar to The input value of type `{0}` must not be null.. - /// - internal static string InputTypeNonNullCheck_ValueIsNull { + internal static string Schema_MutationType { get { - return ResourceManager.GetString("InputTypeNonNullCheck_ValueIsNull", resourceCulture); + return ResourceManager.GetString("Schema_MutationType", resourceCulture); + } + } + + internal static string Schema_QueryType { + get { + return ResourceManager.GetString("Schema_QueryType", resourceCulture); + } + } + + internal static string Schema_SubscriptionType { + get { + return ResourceManager.GetString("Schema_SubscriptionType", resourceCulture); + } + } + + internal static string Schema_Types { + get { + return ResourceManager.GetString("Schema_Types", resourceCulture); + } + } + + internal static string ShortType_Description { + get { + return ResourceManager.GetString("ShortType_Description", resourceCulture); + } + } + + internal static string StringType_Description { + get { + return ResourceManager.GetString("StringType_Description", resourceCulture); + } + } + + internal static string String_Argument_NullOrEmpty { + get { + return ResourceManager.GetString("String_Argument_NullOrEmpty", resourceCulture); + } + } + + internal static string TypeConfiguration_ConfigureIsNull { + get { + return ResourceManager.GetString("TypeConfiguration_ConfigureIsNull", resourceCulture); + } + } + + internal static string TypeConfiguration_DefinitionIsNull { + get { + return ResourceManager.GetString("TypeConfiguration_DefinitionIsNull", resourceCulture); + } + } + + internal static string TypeDependency_MustBeSchemaType { + get { + return ResourceManager.GetString("TypeDependency_MustBeSchemaType", resourceCulture); + } + } + + internal static string TypeExtensions_InvalidStructure { + get { + return ResourceManager.GetString("TypeExtensions_InvalidStructure", resourceCulture); + } + } + + internal static string TypeExtensions_KindIsNotSupported { + get { + return ResourceManager.GetString("TypeExtensions_KindIsNotSupported", resourceCulture); + } + } + + internal static string TypeExtensions_NoListType { + get { + return ResourceManager.GetString("TypeExtensions_NoListType", resourceCulture); + } + } + + internal static string TypeExtensions_TypeIsNotOfT { + get { + return ResourceManager.GetString("TypeExtensions_TypeIsNotOfT", resourceCulture); + } + } + + internal static string TypeField_Description { + get { + return ResourceManager.GetString("TypeField_Description", resourceCulture); + } + } + + internal static string TypeInitializer_CannotResolveDependency { + get { + return ResourceManager.GetString("TypeInitializer_CannotResolveDependency", resourceCulture); + } + } + + internal static string TypeInitializer_CompleteName_Duplicate { + get { + return ResourceManager.GetString("TypeInitializer_CompleteName_Duplicate", resourceCulture); + } + } + + internal static string TypeInitializer_Merge_KindDoesNotMatch { + get { + return ResourceManager.GetString("TypeInitializer_Merge_KindDoesNotMatch", resourceCulture); + } + } + + internal static string TypeKind_Description { + get { + return ResourceManager.GetString("TypeKind_Description", resourceCulture); + } + } + + internal static string TypeKind_Enum { + get { + return ResourceManager.GetString("TypeKind_Enum", resourceCulture); + } + } + + internal static string TypeKind_InputObject { + get { + return ResourceManager.GetString("TypeKind_InputObject", resourceCulture); + } + } + + internal static string TypeKind_Interface { + get { + return ResourceManager.GetString("TypeKind_Interface", resourceCulture); + } + } + + internal static string TypeKind_List { + get { + return ResourceManager.GetString("TypeKind_List", resourceCulture); + } + } + + internal static string TypeKind_NonNull { + get { + return ResourceManager.GetString("TypeKind_NonNull", resourceCulture); + } + } + + internal static string TypeKind_Object { + get { + return ResourceManager.GetString("TypeKind_Object", resourceCulture); + } + } + + internal static string TypeKind_Scalar { + get { + return ResourceManager.GetString("TypeKind_Scalar", resourceCulture); + } + } + + internal static string TypeKind_Union { + get { + return ResourceManager.GetString("TypeKind_Union", resourceCulture); + } + } + + internal static string TypeNameField_Description { + get { + return ResourceManager.GetString("TypeNameField_Description", resourceCulture); + } + } + + internal static string TypeNameHelper_InvalidTypeStructure { + get { + return ResourceManager.GetString("TypeNameHelper_InvalidTypeStructure", resourceCulture); + } + } + + internal static string TypeNameHelper_OnlyTypeSystemObjectsAreAllowed { + get { + return ResourceManager.GetString("TypeNameHelper_OnlyTypeSystemObjectsAreAllowed", resourceCulture); + } + } + + internal static string TypeResourceHelper_TypeNameEmptyOrNull { + get { + return ResourceManager.GetString("TypeResourceHelper_TypeNameEmptyOrNull", resourceCulture); } } - /// - /// Looks up a localized string similar to A GraphQL-formatted string representing the default value for this input value.. - /// - internal static string InputValue_DefaultValue { + internal static string Type_Description { get { - return ResourceManager.GetString("InputValue_DefaultValue", resourceCulture); + return ResourceManager.GetString("Type_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.. - /// - internal static string InputValue_Description { + internal static string UnionTypeExtension_CannotMerge { get { - return ResourceManager.GetString("InputValue_Description", resourceCulture); + return ResourceManager.GetString("UnionTypeExtension_CannotMerge", resourceCulture); } } - /// - /// Looks up a localized string similar to The arguments of the interface field {0} from interface {1} and {2} do not match and are implemented by object type {3}.. - /// - internal static string InterfaceImplRule_ArgumentsDoNotMatch { + internal static string VariableValueBuilder_InputType { get { - return ResourceManager.GetString("InterfaceImplRule_ArgumentsDoNotMatch", resourceCulture); + return ResourceManager.GetString("VariableValueBuilder_InputType", resourceCulture); } } - /// - /// Looks up a localized string similar to Object type {0} does not implement all arguments of field {1} from interface {2}.. - /// - internal static string InterfaceImplRule_ArgumentsNotImpl { + internal static string VariableValueBuilder_InvalidValue { get { - return ResourceManager.GetString("InterfaceImplRule_ArgumentsNotImpl", resourceCulture); + return ResourceManager.GetString("VariableValueBuilder_InvalidValue", resourceCulture); } } - /// - /// Looks up a localized string similar to Object type {0} does not implement the field {1} from interface {2}.. - /// - internal static string InterfaceImplRule_FieldNotImpl { + internal static string VariableValueBuilder_NodeKind { get { - return ResourceManager.GetString("InterfaceImplRule_FieldNotImpl", resourceCulture); + return ResourceManager.GetString("VariableValueBuilder_NodeKind", resourceCulture); } } - /// - /// Looks up a localized string similar to The return type of the interface field {0} from interface {1} and {2} do not match and are implemented by object type {3}.. - /// - internal static string InterfaceImplRule_FieldTypeInvalid { + internal static string VariableValueBuilder_NonNull { get { - return ResourceManager.GetString("InterfaceImplRule_FieldTypeInvalid", resourceCulture); + return ResourceManager.GetString("VariableValueBuilder_NonNull", resourceCulture); } } - /// - /// Looks up a localized string similar to The return type of the interface field {0} does not match the field declared by object type {1}.. - /// - internal static string InterfaceImplRule_ReturnTypeInvalid { + internal static string VariableValueBuilder_NonNull_In_Graph { get { - return ResourceManager.GetString("InterfaceImplRule_ReturnTypeInvalid", resourceCulture); + return ResourceManager.GetString("VariableValueBuilder_NonNull_In_Graph", resourceCulture); } } - /// - /// Looks up a localized string similar to The interface base class cannot be used as interface implementation declaration.. - /// - internal static string InterfaceTypeDescriptor_InterfaceBaseClass { + internal static string VariableValueBuilder_VarNameEmpty { get { - return ResourceManager.GetString("InterfaceTypeDescriptor_InterfaceBaseClass", resourceCulture); + return ResourceManager.GetString("VariableValueBuilder_VarNameEmpty", resourceCulture); } } - /// - /// Looks up a localized string similar to A field of an interface can only be inferred from a property or a method.. - /// - internal static string InterfaceTypeDescriptor_MustBePropertyOrMethod { + internal static string Argument_TypeIsNull { get { - return ResourceManager.GetString("InterfaceTypeDescriptor_MustBePropertyOrMethod", resourceCulture); + return ResourceManager.GetString("Argument_TypeIsNull", resourceCulture); } } - /// - /// Looks up a localized string similar to The interface type extension can only be merged with an interface type.. - /// - internal static string InterfaceTypeExtension_CannotMerge { + internal static string NonNullType_NotAnInputType { get { - return ResourceManager.GetString("InterfaceTypeExtension_CannotMerge", resourceCulture); + return ResourceManager.GetString("NonNullType_NotAnInputType", resourceCulture); } } - /// - /// Looks up a localized string similar to The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.. - /// - internal static string IntType_Description { + internal static string NonNullType_TypeIsNunNullType { get { - return ResourceManager.GetString("IntType_Description", resourceCulture); + return ResourceManager.GetString("NonNullType_TypeIsNunNullType", resourceCulture); } } - /// - /// Looks up a localized string similar to The `LocalDateTime` scalar type is a local date/time string (i.e., with no associated timezone) with the format `YYYY-MM-DDThh:mm:ss`.. - /// - internal static string LocalDateTimeType_Description { + internal static string NonNullType_ValueIsNull { get { - return ResourceManager.GetString("LocalDateTimeType_Description", resourceCulture); + return ResourceManager.GetString("NonNullType_ValueIsNull", resourceCulture); } } - /// - /// Looks up a localized string similar to The `LocalDate` scalar type represents a ISO date string, represented as UTF-8 character sequences YYYY-MM-DD. The scalar follows the specification defined in RFC3339. - /// - internal static string LocalDateType_Description { + internal static string ObjectTypeExtension_CannotMerge { get { - return ResourceManager.GetString("LocalDateType_Description", resourceCulture); + return ResourceManager.GetString("ObjectTypeExtension_CannotMerge", resourceCulture); } } - /// - /// Looks up a localized string similar to The LocalTime scalar type is a local time string (i.e., with no associated timezone) in 24-hr HH:mm:ss.. - /// - internal static string LocalTimeType_Description { + internal static string TypeSystemObjectBase_DefinitionIsNull { get { - return ResourceManager.GetString("LocalTimeType_Description", resourceCulture); + return ResourceManager.GetString("TypeSystemObjectBase_DefinitionIsNull", resourceCulture); } } - /// - /// Looks up a localized string similar to The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1.. - /// - internal static string LongType_Description { + internal static string TypeSystemObjectBase_NameIsNull { get { - return ResourceManager.GetString("LongType_Description", resourceCulture); + return ResourceManager.GetString("TypeSystemObjectBase_NameIsNull", resourceCulture); } } - /// - /// Looks up a localized string similar to The multiplier path scalar represents a valid GraphQL multiplier path string.. - /// - internal static string Name_Cannot_BeEmpty { + internal static string TypeSystemObject_DescriptionImmutable { get { - return ResourceManager.GetString("Name_Cannot_BeEmpty", resourceCulture); + return ResourceManager.GetString("TypeSystemObject_DescriptionImmutable", resourceCulture); } } - /// - /// Looks up a localized string similar to The name scalar represents a valid GraphQL name as specified in the spec and can be used to refer to fields or types.. - /// - internal static string NameType_Description { + internal static string TypeSystemObject_NameImmutable { get { - return ResourceManager.GetString("NameType_Description", resourceCulture); + return ResourceManager.GetString("TypeSystemObject_NameImmutable", resourceCulture); } } - /// - /// Looks up a localized string similar to The ID field must be a property or a method.. - /// - internal static string NodeDescriptor_IdField_MustBePropertyOrMethod { + internal static string UnionType_MustHaveTypes { get { - return ResourceManager.GetString("NodeDescriptor_IdField_MustBePropertyOrMethod", resourceCulture); + return ResourceManager.GetString("UnionType_MustHaveTypes", resourceCulture); } } - /// - /// Looks up a localized string similar to An ID-member must be a property-expression or a method-call-expression.. - /// - internal static string NodeDescriptor_IdMember { + internal static string UnionType_UnableToResolveType { get { - return ResourceManager.GetString("NodeDescriptor_IdMember", resourceCulture); + return ResourceManager.GetString("UnionType_UnableToResolveType", resourceCulture); } } - /// - /// Looks up a localized string similar to A node-resolver-expression must be a method-call-expression.. - /// - internal static string NodeDescriptor_MustBeMethod { + internal static string SchemaBuilder_MustBeSchemaType { get { - return ResourceManager.GetString("NodeDescriptor_MustBeMethod", resourceCulture); + return ResourceManager.GetString("SchemaBuilder_MustBeSchemaType", resourceCulture); } } - /// - /// Looks up a localized string similar to The node interface is implemented by entities that have a global unique identifier.. - /// - internal static string NodeType_TypeDescription { + internal static string TypeRegistrar_TypesInconsistent { get { - return ResourceManager.GetString("NodeType_TypeDescription", resourceCulture); + return ResourceManager.GetString("TypeRegistrar_TypesInconsistent", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified type is not an input type.. - /// - internal static string NonNamedType_IsInstanceOfType_NotAnInputType { + internal static string TypeConversion_ConvertNotSupported { get { - return ResourceManager.GetString("NonNamedType_IsInstanceOfType_NotAnInputType", resourceCulture); + return ResourceManager.GetString("TypeConversion_ConvertNotSupported", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified type is not an input type.. - /// - internal static string NonNullType_NotAnInputType { + internal static string SchemaBuilder_Interceptor_NotSupported { get { - return ResourceManager.GetString("NonNullType_NotAnInputType", resourceCulture); + return ResourceManager.GetString("SchemaBuilder_Interceptor_NotSupported", resourceCulture); } } - /// - /// Looks up a localized string similar to The inner type of non-null type must be a nullable type.. - /// - internal static string NonNullType_TypeIsNunNullType { + internal static string IdSerializer_UnableToEncode { get { - return ResourceManager.GetString("NonNullType_TypeIsNunNullType", resourceCulture); + return ResourceManager.GetString("IdSerializer_UnableToEncode", resourceCulture); } } - /// - /// Looks up a localized string similar to A non null type cannot parse null value literals.. - /// - internal static string NonNullType_ValueIsNull { + internal static string IdSerializer_UnableToDecode { get { - return ResourceManager.GetString("NonNullType_ValueIsNull", resourceCulture); + return ResourceManager.GetString("IdSerializer_UnableToDecode", resourceCulture); } } - /// - /// Looks up a localized string similar to The field-type must be an output-type.. - /// - internal static string ObjectFieldDescriptorBase_FieldType { + internal static string SchemaBuilder_Convention_NotSupported { get { - return ResourceManager.GetString("ObjectFieldDescriptorBase_FieldType", resourceCulture); + return ResourceManager.GetString("SchemaBuilder_Convention_NotSupported", resourceCulture); } } - /// - /// Looks up a localized string similar to Cycle in object graph detected.. - /// - internal static string ObjectToDictionaryConverter_CycleInObjectGraph { + internal static string TimeSpanType_Description { get { - return ResourceManager.GetString("ObjectToDictionaryConverter_CycleInObjectGraph", resourceCulture); + return ResourceManager.GetString("TimeSpanType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The interface base class cannot be used as interface implementation declaration.. - /// - internal static string ObjectTypeDescriptor_InterfaceBaseClass { + internal static string DefaultDataLoaderRegistry_GetOrRegister { get { - return ResourceManager.GetString("ObjectTypeDescriptor_InterfaceBaseClass", resourceCulture); + return ResourceManager.GetString("DefaultDataLoaderRegistry_GetOrRegister", resourceCulture); } } - /// - /// Looks up a localized string similar to A field-expression must be a property-expression or a method-call-expression.. - /// - internal static string ObjectTypeDescriptor_MustBePropertyOrMethod { + internal static string DataLoaderResolverContextExtensions_CreateDataLoader_AbstractType { get { - return ResourceManager.GetString("ObjectTypeDescriptor_MustBePropertyOrMethod", resourceCulture); + return ResourceManager.GetString("DataLoaderResolverContextExtensions_CreateDataLoader_AbstractType", resourceCulture); } } - /// - /// Looks up a localized string similar to Schema types cannot be used as resolver types.. - /// - internal static string ObjectTypeDescriptor_Resolver_SchemaType { + internal static string DataLoaderResolverContextExtensions_CreateDataLoader_UnableToCreate { get { - return ResourceManager.GetString("ObjectTypeDescriptor_Resolver_SchemaType", resourceCulture); + return ResourceManager.GetString("DataLoaderResolverContextExtensions_CreateDataLoader_UnableToCreate", resourceCulture); } } - /// - /// Looks up a localized string similar to The resolver type {0} cannot be used, a non-abstract type is required.. - /// - internal static string ObjectTypeDescriptor_ResolveWith_NonAbstract { + internal static string NonNamedType_IsInstanceOfType_NotAnInputType { get { - return ResourceManager.GetString("ObjectTypeDescriptor_ResolveWith_NonAbstract", resourceCulture); + return ResourceManager.GetString("NonNamedType_IsInstanceOfType_NotAnInputType", resourceCulture); } } - /// - /// Looks up a localized string similar to The object type extension can only be merged with an object type.. - /// - internal static string ObjectTypeExtension_CannotMerge { + internal static string RegisteredType_CompletionContext_Not_Initialized { get { - return ResourceManager.GetString("ObjectTypeExtension_CannotMerge", resourceCulture); + return ResourceManager.GetString("RegisteredType_CompletionContext_Not_Initialized", resourceCulture); } } - /// - /// Looks up a localized string similar to The `@oneOf` directive is used within the type system definition language - /// to indicate: - /// - /// - an Input Object is a Oneof Input Object, or - /// - an Object Type's Field is a Oneof Field.. - /// - internal static string OneOfDirectiveType_Description { + internal static string RegisteredType_CompletionContext_Already_Set { get { - return ResourceManager.GetString("OneOfDirectiveType_Description", resourceCulture); + return ResourceManager.GetString("RegisteredType_CompletionContext_Already_Set", resourceCulture); } } - /// - /// Looks up a localized string similar to The member expression must specify a property or method that is public and that belongs to the type {0}. - /// - internal static string Reflection_MemberMust_BeMethodOrProperty { + internal static string DeferDirectiveType_Description { get { - return ResourceManager.GetString("Reflection_MemberMust_BeMethodOrProperty", resourceCulture); + return ResourceManager.GetString("DeferDirectiveType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to Member is not a method!. - /// - internal static string ReflectionUtils_ExtractMethod_MethodExpected { + internal static string DeferDirectiveType_Label_Description { get { - return ResourceManager.GetString("ReflectionUtils_ExtractMethod_MethodExpected", resourceCulture); + return ResourceManager.GetString("DeferDirectiveType_Label_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The object is not yet ready for this action.. - /// - internal static string RegisteredType_Completion_NotYetReady { + internal static string DeferDirectiveType_If_Description { get { - return ResourceManager.GetString("RegisteredType_Completion_NotYetReady", resourceCulture); + return ResourceManager.GetString("DeferDirectiveType_If_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The completion context can only be set once.. - /// - internal static string RegisteredType_CompletionContext_Already_Set { + internal static string StreamDirectiveType_Description { get { - return ResourceManager.GetString("RegisteredType_CompletionContext_Already_Set", resourceCulture); + return ResourceManager.GetString("StreamDirectiveType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The completion context has not been initialized.. - /// - internal static string RegisteredType_CompletionContext_Not_Initialized { + internal static string StreamDirectiveType_Label_Description { get { - return ResourceManager.GetString("RegisteredType_CompletionContext_Not_Initialized", resourceCulture); + return ResourceManager.GetString("StreamDirectiveType_Label_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to Fetches an object given its ID.. - /// - internal static string Relay_NodeField_Description { + internal static string StreamDirectiveType_InitialCount_Description { get { - return ResourceManager.GetString("Relay_NodeField_Description", resourceCulture); + return ResourceManager.GetString("StreamDirectiveType_InitialCount_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to ID of the object.. - /// - internal static string Relay_NodeField_Id_Description { + internal static string StreamDirectiveType_If_Description { get { - return ResourceManager.GetString("Relay_NodeField_Id_Description", resourceCulture); + return ResourceManager.GetString("StreamDirectiveType_If_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to Lookup nodes by a list of IDs.. - /// - internal static string Relay_NodesField_Description { + internal static string SchemaBuilder_AddRootType_TypeAlreadyRegistered { get { - return ResourceManager.GetString("Relay_NodesField_Description", resourceCulture); + return ResourceManager.GetString("SchemaBuilder_AddRootType_TypeAlreadyRegistered", resourceCulture); } } - /// - /// Looks up a localized string similar to The list of node IDs.. - /// - internal static string Relay_NodesField_Ids_Description { + internal static string NodeDescriptor_IdField_MustBePropertyOrMethod { get { - return ResourceManager.GetString("Relay_NodesField_Ids_Description", resourceCulture); + return ResourceManager.GetString("NodeDescriptor_IdField_MustBePropertyOrMethod", resourceCulture); } } - /// - /// Looks up a localized string similar to A directive type mustn't be one of the base classes `DirectiveType` or `DirectiveType<T>` but must be a type inheriting from `DirectiveType` or `DirectiveType<T>`.. - /// - internal static string ResolverCompiler_UnknownParameterType { + internal static string DeprecatedDirectiveType_TypeDescription { get { - return ResourceManager.GetString("ResolverCompiler_UnknownParameterType", resourceCulture); + return ResourceManager.GetString("DeprecatedDirectiveType_TypeDescription", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified key `{0}` does not exist on `context.ContextData`. - /// - internal static string ResolverContextExtensions_ContextData_KeyNotFound { + internal static string DeprecatedDirectiveType_ReasonDescription { get { - return ResourceManager.GetString("ResolverContextExtensions_ContextData_KeyNotFound", resourceCulture); + return ResourceManager.GetString("DeprecatedDirectiveType_ReasonDescription", resourceCulture); } } - /// - /// Looks up a localized string similar to The field name mustn't be null, empty or consist only of white spaces.. - /// - internal static string ResolverContextExtensions_IsSelected_FieldNameEmpty { + internal static string IncludeDirectiveType_TypeDescription { get { - return ResourceManager.GetString("ResolverContextExtensions_IsSelected_FieldNameEmpty", resourceCulture); + return ResourceManager.GetString("IncludeDirectiveType_TypeDescription", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified key `{0}` does not exist on `context.LocalContextData`. - /// - internal static string ResolverContextExtensions_LocalContextData_KeyNotFound { + internal static string IncludeDirectiveType_IfDescription { get { - return ResourceManager.GetString("ResolverContextExtensions_LocalContextData_KeyNotFound", resourceCulture); + return ResourceManager.GetString("IncludeDirectiveType_IfDescription", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified key `{0}` does not exist on `context.ScopedContextData`. - /// - internal static string ResolverContextExtensions_ScopedContextData_KeyNotFound { + internal static string SkipDirectiveType_TypeDescription { get { - return ResourceManager.GetString("ResolverContextExtensions_ScopedContextData_KeyNotFound", resourceCulture); + return ResourceManager.GetString("SkipDirectiveType_TypeDescription", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified IResolverFieldBindingBuilder-implementation is not supported.. - /// - internal static string ResolverTypeBindingBuilder_FieldBuilderNotSupported { + internal static string SkipDirectiveType_IfDescription { get { - return ResourceManager.GetString("ResolverTypeBindingBuilder_FieldBuilderNotSupported", resourceCulture); + return ResourceManager.GetString("SkipDirectiveType_IfDescription", resourceCulture); } } - /// - /// Looks up a localized string similar to The field binding builder is not completed and cannot be added.. - /// - internal static string ResolverTypeBindingBuilder_FieldNotComplete { + internal static string SpecifiedByDirectiveType_TypeDescription { get { - return ResourceManager.GetString("ResolverTypeBindingBuilder_FieldNotComplete", resourceCulture); + return ResourceManager.GetString("SpecifiedByDirectiveType_TypeDescription", resourceCulture); } } - /// - /// Looks up a localized string similar to {0} cannot deserialize the given value.. - /// - internal static string Scalar_Cannot_Deserialize { + internal static string SpecifiedByDirectiveType_UrlDescription { get { - return ResourceManager.GetString("Scalar_Cannot_Deserialize", resourceCulture); + return ResourceManager.GetString("SpecifiedByDirectiveType_UrlDescription", resourceCulture); } } - /// - /// Looks up a localized string similar to {0} cannot parse the given literal of type `{1}`.. - /// - internal static string Scalar_Cannot_ParseLiteral { + internal static string NodeType_TypeDescription { get { - return ResourceManager.GetString("Scalar_Cannot_ParseLiteral", resourceCulture); + return ResourceManager.GetString("NodeType_TypeDescription", resourceCulture); } } - /// - /// Looks up a localized string similar to {0} cannot parse the given value of type `{1}`.. - /// - internal static string Scalar_Cannot_ParseValue { + internal static string AnyType_CycleInObjectGraph { get { - return ResourceManager.GetString("Scalar_Cannot_ParseValue", resourceCulture); + return ResourceManager.GetString("AnyType_CycleInObjectGraph", resourceCulture); } } - /// - /// Looks up a localized string similar to {0} cannot serialize the given value.. - /// - internal static string Scalar_Cannot_Serialize { + internal static string UuidType_FormatUnknown { get { - return ResourceManager.GetString("Scalar_Cannot_Serialize", resourceCulture); + return ResourceManager.GetString("UuidType_FormatUnknown", resourceCulture); } } - /// - /// Looks up a localized string similar to A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.. - /// - internal static string Schema_Description { + internal static string Directive_GetArgument_ArgumentNameIsInvalid { get { - return ResourceManager.GetString("Schema_Description", resourceCulture); + return ResourceManager.GetString("Directive_GetArgument_ArgumentNameIsInvalid", resourceCulture); } } - /// - /// Looks up a localized string similar to A list of all directives supported by this server.. - /// - internal static string Schema_Directives { + internal static string AppliedDirective_Description { get { - return ResourceManager.GetString("Schema_Directives", resourceCulture); + return ResourceManager.GetString("AppliedDirective_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified type `{0}` does not exist.. - /// - internal static string Schema_GetDirectiveType_DoesNotExist { + internal static string DirectiveArgument_Description { get { - return ResourceManager.GetString("Schema_GetDirectiveType_DoesNotExist", resourceCulture); + return ResourceManager.GetString("DirectiveArgument_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to If this server supports mutation, the type that mutation operations will be rooted at.. - /// - internal static string Schema_MutationType { + internal static string ThrowHelper_UsePagingAttribute_NodeTypeUnknown { get { - return ResourceManager.GetString("Schema_MutationType", resourceCulture); + return ResourceManager.GetString("ThrowHelper_UsePagingAttribute_NodeTypeUnknown", resourceCulture); } } - /// - /// Looks up a localized string similar to The type that query operations will be rooted at.. - /// - internal static string Schema_QueryType { + internal static string Schema_GetDirectiveType_DoesNotExist { get { - return ResourceManager.GetString("Schema_QueryType", resourceCulture); + return ResourceManager.GetString("Schema_GetDirectiveType_DoesNotExist", resourceCulture); } } - /// - /// Looks up a localized string similar to If this server support subscription, the type that subscription operations will be rooted at.. - /// - internal static string Schema_SubscriptionType { + internal static string ErrorHelper_ObjectField_HasNoResolver { get { - return ResourceManager.GetString("Schema_SubscriptionType", resourceCulture); + return ResourceManager.GetString("ErrorHelper_ObjectField_HasNoResolver", resourceCulture); } } - /// - /// Looks up a localized string similar to A list of all types supported by this server.. - /// - internal static string Schema_Types { + internal static string ExtendedTypeReferenceHandler_NonGenericExecutableNotAllowed { get { - return ResourceManager.GetString("Schema_Types", resourceCulture); + return ResourceManager.GetString("ExtendedTypeReferenceHandler_NonGenericExecutableNotAllowed", resourceCulture); } } - /// - /// Looks up a localized string similar to The root type `{0}` has already been registered.. - /// - internal static string SchemaBuilder_AddRootType_TypeAlreadyRegistered { + internal static string BindingCompiler_AddBinding_BindingCannotBeHandled { get { - return ResourceManager.GetString("SchemaBuilder_AddRootType_TypeAlreadyRegistered", resourceCulture); + return ResourceManager.GetString("BindingCompiler_AddBinding_BindingCannotBeHandled", resourceCulture); } } - /// - /// Looks up a localized string similar to There is no handler registered that can handle the specified schema binding.. - /// - internal static string SchemaBuilder_Binding_CannotBeHandled { + internal static string Type_SpecifiedByUrl_Description { get { - return ResourceManager.GetString("SchemaBuilder_Binding_CannotBeHandled", resourceCulture); + return ResourceManager.GetString("Type_SpecifiedByUrl_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The schema binding is not valid.. - /// - internal static string SchemaBuilder_Binding_Invalid { + internal static string SchemaBuilderExtensions_AddObjectType_TIsSchemaType { get { - return ResourceManager.GetString("SchemaBuilder_Binding_Invalid", resourceCulture); + return ResourceManager.GetString("SchemaBuilderExtensions_AddObjectType_TIsSchemaType", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified convention type is not supported.. - /// - internal static string SchemaBuilder_Convention_NotSupported { + internal static string SchemaBuilderExtensions_AddUnionType_TIsSchemaType { get { - return ResourceManager.GetString("SchemaBuilder_Convention_NotSupported", resourceCulture); + return ResourceManager.GetString("SchemaBuilderExtensions_AddUnionType_TIsSchemaType", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified interceptor type is not supported.. - /// - internal static string SchemaBuilder_Interceptor_NotSupported { + internal static string SchemaBuilderExtensions_AddEnumType_TIsSchemaType { get { - return ResourceManager.GetString("SchemaBuilder_Interceptor_NotSupported", resourceCulture); + return ResourceManager.GetString("SchemaBuilderExtensions_AddEnumType_TIsSchemaType", resourceCulture); } } - /// - /// Looks up a localized string similar to The given schema has to inherit from TypeSystemObjectBase in order to be initializable.. - /// - internal static string SchemaBuilder_ISchemaNotTso { + internal static string SchemaBuilderExtensions_AddInterfaceType_TIsSchemaType { get { - return ResourceManager.GetString("SchemaBuilder_ISchemaNotTso", resourceCulture); + return ResourceManager.GetString("SchemaBuilderExtensions_AddInterfaceType_TIsSchemaType", resourceCulture); } } - /// - /// Looks up a localized string similar to schemaType must be a schema type.. - /// - internal static string SchemaBuilder_MustBeSchemaType { + internal static string SchemaBuilderExtensions_AddInputObjectType_TIsSchemaType { get { - return ResourceManager.GetString("SchemaBuilder_MustBeSchemaType", resourceCulture); + return ResourceManager.GetString("SchemaBuilderExtensions_AddInputObjectType_TIsSchemaType", resourceCulture); } } - /// - /// Looks up a localized string similar to The schema builder was unable to identify the query type of the schema. Either specify which type is the query type or set the schema builder to non-strict validation mode.. - /// - internal static string SchemaBuilder_NoQueryType { + internal static string EventMessageParameterExpressionBuilder_MessageNotFound { get { - return ResourceManager.GetString("SchemaBuilder_NoQueryType", resourceCulture); + return ResourceManager.GetString("EventMessageParameterExpressionBuilder_MessageNotFound", resourceCulture); } } - /// - /// Looks up a localized string similar to A root type must be a class.. - /// - internal static string SchemaBuilder_RootType_MustBeClass { + internal static string DefaultResolverCompilerService_CreateResolver_ArgumentValidationError { get { - return ResourceManager.GetString("SchemaBuilder_RootType_MustBeClass", resourceCulture); + return ResourceManager.GetString("DefaultResolverCompilerService_CreateResolver_ArgumentValidationError", resourceCulture); } } - /// - /// Looks up a localized string similar to A root type must be an object type.. - /// - internal static string SchemaBuilder_RootType_MustBeObjectType { + internal static string DefaultResolverCompilerService_CompileSubscribe_OnlyMethodsAllowed { get { - return ResourceManager.GetString("SchemaBuilder_RootType_MustBeObjectType", resourceCulture); + return ResourceManager.GetString("DefaultResolverCompilerService_CompileSubscribe_OnlyMethodsAllowed", resourceCulture); } } - /// - /// Looks up a localized string similar to Non-generic schema types are not allowed.. - /// - internal static string SchemaBuilder_RootType_NonGenericType { + internal static string SchemaBuilderExtensions_AddResolverConfig_ContextInvalid { get { - return ResourceManager.GetString("SchemaBuilder_RootType_NonGenericType", resourceCulture); + return ResourceManager.GetString("SchemaBuilderExtensions_AddResolverConfig_ContextInvalid", resourceCulture); } } - /// - /// Looks up a localized string similar to The given schema has to inherit from `Schema` in order to be initializable.. - /// - internal static string SchemaBuilder_SchemaTypeInvalid { + internal static string ExpressionHelper_GetGlobalStateWithDefault_NoDefaults { get { - return ResourceManager.GetString("SchemaBuilder_SchemaTypeInvalid", resourceCulture); + return ResourceManager.GetString("ExpressionHelper_GetGlobalStateWithDefault_NoDefaults", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified type `{0}` is a GraphQL schema type. AddEnumType<T> is a helper method to register a runtime type as GraphQL enum type. Use AddType<T> to register GraphQL schema types.. - /// - internal static string SchemaBuilderExtensions_AddEnumType_TIsSchemaType { + internal static string ExpressionHelper_ResolveScopedContextData_KeyDoesNotExist { get { - return ResourceManager.GetString("SchemaBuilderExtensions_AddEnumType_TIsSchemaType", resourceCulture); + return ResourceManager.GetString("ExpressionHelper_ResolveScopedContextData_KeyDoesNotExist", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified type `{0}` is a GraphQL schema type. AddInputObjectType<T> is a helper method to register a runtime type as GraphQL input object type. Use AddType<T> to register GraphQL schema types.. - /// - internal static string SchemaBuilderExtensions_AddInputObjectType_TIsSchemaType { + internal static string ExpressionHelper_GetScopedStateWithDefault_NoDefaultValue { get { - return ResourceManager.GetString("SchemaBuilderExtensions_AddInputObjectType_TIsSchemaType", resourceCulture); + return ResourceManager.GetString("ExpressionHelper_GetScopedStateWithDefault_NoDefaultValue", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified type `{0}` is a GraphQL schema type. AddInterfaceType<T> is a helper method to register a runtime type as GraphQL interface type. Use AddType<T> to register GraphQL schema types.. - /// - internal static string SchemaBuilderExtensions_AddInterfaceType_TIsSchemaType { + internal static string ClaimsPrincipalParameterExpressionBuilder_NoClaimsFound { get { - return ResourceManager.GetString("SchemaBuilderExtensions_AddInterfaceType_TIsSchemaType", resourceCulture); + return ResourceManager.GetString("ClaimsPrincipalParameterExpressionBuilder_NoClaimsFound", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified type `{0}` is a GraphQL schema type. AddObjectType<T> is a helper method to register a runtime type as GraphQL object type. Use AddType<T> to register GraphQL schema types.. - /// - internal static string SchemaBuilderExtensions_AddObjectType_TIsSchemaType { + internal static string DirectiveLocation_VariableDefinition { get { - return ResourceManager.GetString("SchemaBuilderExtensions_AddObjectType_TIsSchemaType", resourceCulture); + return ResourceManager.GetString("DirectiveLocation_VariableDefinition", resourceCulture); } } - /// - /// Looks up a localized string similar to The resolver type needs to be a public non-abstract non-static class.. - /// internal static string SchemaBuilderExtensions_AddResolver_TypeConditionNotMet { get { return ResourceManager.GetString("SchemaBuilderExtensions_AddResolver_TypeConditionNotMet", resourceCulture); } } - /// - /// Looks up a localized string similar to The schema builder context is invalid.. - /// - internal static string SchemaBuilderExtensions_AddResolverConfig_ContextInvalid { - get { - return ResourceManager.GetString("SchemaBuilderExtensions_AddResolverConfig_ContextInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The resolver type needs to be a class or interface. - /// internal static string SchemaBuilderExtensions_AddRootResolver_NeedsToBeClassOrInterface { get { return ResourceManager.GetString("SchemaBuilderExtensions_AddRootResolver_NeedsToBeClassOrInterface", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified type `{0}` is a GraphQL schema type. AddUnionType<T> is a helper method to register a runtime type as GraphQL union type. Use AddType<T> to register GraphQL schema types.. - /// - internal static string SchemaBuilderExtensions_AddUnionType_TIsSchemaType { + internal static string Relay_NodeField_Description { get { - return ResourceManager.GetString("SchemaBuilderExtensions_AddUnionType_TIsSchemaType", resourceCulture); + return ResourceManager.GetString("Relay_NodeField_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to A directive type mustn't be one of the base classes `DirectiveType` or `DirectiveType<T>` but must be a type inheriting from `DirectiveType` or `DirectiveType<T>`.. - /// - internal static string SchemaBuilderExtensions_DirectiveTypeIsBaseType { + internal static string Relay_NodeField_Id_Description { get { - return ResourceManager.GetString("SchemaBuilderExtensions_DirectiveTypeIsBaseType", resourceCulture); + return ResourceManager.GetString("Relay_NodeField_Id_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to A directive type must inherit from `DirectiveType` or `DirectiveType<T>`.. - /// - internal static string SchemaBuilderExtensions_MustBeDirectiveType { + internal static string Relay_NodesField_Description { get { - return ResourceManager.GetString("SchemaBuilderExtensions_MustBeDirectiveType", resourceCulture); + return ResourceManager.GetString("Relay_NodesField_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The schema string cannot be null or empty.. - /// - internal static string SchemaBuilderExtensions_SchemaIsEmpty { + internal static string Relay_NodesField_Ids_Description { get { - return ResourceManager.GetString("SchemaBuilderExtensions_SchemaIsEmpty", resourceCulture); + return ResourceManager.GetString("Relay_NodesField_Ids_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The error message mustn't be null or empty.. - /// - internal static string SchemaErrorBuilder_MessageIsNull { + internal static string ErrorHelper_MiddlewareOrderInvalid { get { - return ResourceManager.GetString("SchemaErrorBuilder_MessageIsNull", resourceCulture); + return ResourceManager.GetString("ErrorHelper_MiddlewareOrderInvalid", resourceCulture); } } - /// - /// Looks up a localized string similar to For more details look at the `Errors` property.. - /// - internal static string SchemaException_ErrorSummaryText { + internal static string ErrorHelper_NoSchemaTypesAllowedAsRuntimeType { get { - return ResourceManager.GetString("SchemaException_ErrorSummaryText", resourceCulture); + return ResourceManager.GetString("ErrorHelper_NoSchemaTypesAllowedAsRuntimeType", resourceCulture); } } - /// - /// Looks up a localized string similar to Unexpected schema exception occurred.. - /// - internal static string SchemaException_UnexpectedError { + internal static string FieldInitHelper_CompleteFields_MaxFieldCountToSmall { get { - return ResourceManager.GetString("SchemaException_UnexpectedError", resourceCulture); + return ResourceManager.GetString("FieldInitHelper_CompleteFields_MaxFieldCountToSmall", resourceCulture); } } - /// - /// Looks up a localized string similar to Access the current type schema of this server.. - /// - internal static string SchemaField_Description { + internal static string RegisteredType_Completion_NotYetReady { get { - return ResourceManager.GetString("SchemaField_Description", resourceCulture); + return ResourceManager.GetString("RegisteredType_Completion_NotYetReady", resourceCulture); } } - /// - /// Looks up a localized string similar to Unknown operation type.. - /// - internal static string SchemaSyntaxVisitor_UnknownOperationType { + internal static string EdgeType_IsInstanceOfType_NonObject { get { - return ResourceManager.GetString("SchemaSyntaxVisitor_UnknownOperationType", resourceCulture); + return ResourceManager.GetString("EdgeType_IsInstanceOfType_NonObject", resourceCulture); } } - /// - /// Looks up a localized string similar to The schema types definition is in an invalid state.. - /// - internal static string SchemaTypes_DefinitionInvalid { + internal static string EdgeType_Description { get { - return ResourceManager.GetString("SchemaTypes_DefinitionInvalid", resourceCulture); + return ResourceManager.GetString("EdgeType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified type `{0}` does not exist or is not of the specified kind `{1}`.. - /// - internal static string SchemaTypes_GetType_DoesNotExist { + internal static string EdgeType_Cursor_Description { get { - return ResourceManager.GetString("SchemaTypes_GetType_DoesNotExist", resourceCulture); + return ResourceManager.GetString("EdgeType_Cursor_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The middleware order is invalid since the service scope is missing.. - /// - internal static string ServiceHelper_UseResolverServiceInternal_Order { + internal static string EdgeType_Node_Description { get { - return ResourceManager.GetString("ServiceHelper_UseResolverServiceInternal_Order", resourceCulture); + return ResourceManager.GetString("EdgeType_Node_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The `Short` scalar type represents non-fractional signed whole 16-bit numeric values. Short can represent values between -(2^15) and 2^15 - 1.. - /// - internal static string ShortType_Description { + internal static string ConnectionType_Description { get { - return ResourceManager.GetString("ShortType_Description", resourceCulture); + return ResourceManager.GetString("ConnectionType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to Skipped when true.. - /// - internal static string SkipDirectiveType_IfDescription { + internal static string ConnectionType_PageInfo_Description { get { - return ResourceManager.GetString("SkipDirectiveType_IfDescription", resourceCulture); + return ResourceManager.GetString("ConnectionType_PageInfo_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to Directs the executor to skip this field or fragment when the `if` argument is true.. - /// - internal static string SkipDirectiveType_TypeDescription { + internal static string ConnectionType_Edges_Description { get { - return ResourceManager.GetString("SkipDirectiveType_TypeDescription", resourceCulture); + return ResourceManager.GetString("ConnectionType_Edges_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The `@specifiedBy` directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar definitions.. - /// - internal static string SpecifiedByDirectiveType_TypeDescription { + internal static string ConnectionType_TotalCount_Description { get { - return ResourceManager.GetString("SpecifiedByDirectiveType_TypeDescription", resourceCulture); + return ResourceManager.GetString("ConnectionType_TotalCount_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The specifiedBy URL points to a human-readable specification. This field will only read a result for scalar types.. - /// - internal static string SpecifiedByDirectiveType_UrlDescription { + internal static string CollectionSegmentType_PageInfo_Description { get { - return ResourceManager.GetString("SpecifiedByDirectiveType_UrlDescription", resourceCulture); + return ResourceManager.GetString("CollectionSegmentType_PageInfo_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`.. - /// - internal static string StreamDirectiveType_Description { + internal static string CollectionSegmentType_Description { get { - return ResourceManager.GetString("StreamDirectiveType_Description", resourceCulture); + return ResourceManager.GetString("CollectionSegmentType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to Streamed when true.. - /// - internal static string StreamDirectiveType_If_Description { + internal static string CollectionSegmentType_Items_Description { get { - return ResourceManager.GetString("StreamDirectiveType_If_Description", resourceCulture); + return ResourceManager.GetString("CollectionSegmentType_Items_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to The initial elements that shall be send down to the consumer.. - /// - internal static string StreamDirectiveType_InitialCount_Description { + internal static string ConnectionType_Nodes_Description { get { - return ResourceManager.GetString("StreamDirectiveType_InitialCount_Description", resourceCulture); + return ResourceManager.GetString("ConnectionType_Nodes_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to.. - /// - internal static string StreamDirectiveType_Label_Description { + internal static string ServiceHelper_UseResolverServiceInternal_Order { get { - return ResourceManager.GetString("StreamDirectiveType_Label_Description", resourceCulture); + return ResourceManager.GetString("ServiceHelper_UseResolverServiceInternal_Order", resourceCulture); } } - /// - /// Looks up a localized string similar to The `{0}` cannot be null or empty.. - /// - internal static string String_Argument_NullOrEmpty { + internal static string DefaultNamingConventions_FormatFieldName_EmptyOrNull { get { - return ResourceManager.GetString("String_Argument_NullOrEmpty", resourceCulture); + return ResourceManager.GetString("DefaultNamingConventions_FormatFieldName_EmptyOrNull", resourceCulture); } } - /// - /// Looks up a localized string similar to The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.. - /// - internal static string StringType_Description { + internal static string OneOfDirectiveType_Description { get { - return ResourceManager.GetString("StringType_Description", resourceCulture); + return ResourceManager.GetString("OneOfDirectiveType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to Tag is not supported on the specified descriptor.. - /// - internal static string TagDirective_Descriptor_NotSupported { + internal static string ThrowHelper_OneOfNoFieldSet { get { - return ResourceManager.GetString("TagDirective_Descriptor_NotSupported", resourceCulture); + return ResourceManager.GetString("ThrowHelper_OneOfNoFieldSet", resourceCulture); } } - /// - /// Looks up a localized string similar to The tag name must follow the GraphQL type name rules.. - /// - internal static string TagDirective_Name_NotValid { + internal static string ThrowHelper_OneOfMoreThanOneFieldSet { get { - return ResourceManager.GetString("TagDirective_Name_NotValid", resourceCulture); + return ResourceManager.GetString("ThrowHelper_OneOfMoreThanOneFieldSet", resourceCulture); } } - /// - /// Looks up a localized string similar to Convention of type {0} in scope {1} could not be created. - /// - internal static string ThrowHelper_Convention_ConventionCouldNotBeCreated { + internal static string ThrowHelper_OneOfFieldIsNull { get { - return ResourceManager.GetString("ThrowHelper_Convention_ConventionCouldNotBeCreated", resourceCulture); + return ResourceManager.GetString("ThrowHelper_OneOfFieldIsNull", resourceCulture); } } - /// - /// Looks up a localized string similar to There are two conventions registered for {0} in scope {1}. Only one convention is allowed. Use convention extensions if additional configuration is needed. Colliding conventions are {2} and {3}. - /// - internal static string ThrowHelper_Convention_TwoConventionsRegisteredForScope { + internal static string ReflectionUtils_ExtractMethod_MethodExpected { get { - return ResourceManager.GetString("ThrowHelper_Convention_TwoConventionsRegisteredForScope", resourceCulture); + return ResourceManager.GetString("ReflectionUtils_ExtractMethod_MethodExpected", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to create a convention instance from {0}.. - /// - internal static string ThrowHelper_Convention_UnableToCreateConvention { + internal static string ResolverContextExtensions_ScopedContextData_KeyNotFound { get { - return ResourceManager.GetString("ThrowHelper_Convention_UnableToCreateConvention", resourceCulture); + return ResourceManager.GetString("ResolverContextExtensions_ScopedContextData_KeyNotFound", resourceCulture); } } - /// - /// Looks up a localized string similar to The provided type {0} is not a dataloader. - /// - internal static string ThrowHelper_DataLoader_InvalidType { + internal static string ResolverContextExtensions_LocalContextData_KeyNotFound { get { - return ResourceManager.GetString("ThrowHelper_DataLoader_InvalidType", resourceCulture); + return ResourceManager.GetString("ResolverContextExtensions_LocalContextData_KeyNotFound", resourceCulture); } } - /// - /// Looks up a localized string similar to The event message is of the type `{0}` and cannot be casted to `{1}.`. - /// - internal static string ThrowHelper_EventMessage_InvalidCast { + internal static string ResolverContextExtensions_ContextData_KeyNotFound { get { - return ResourceManager.GetString("ThrowHelper_EventMessage_InvalidCast", resourceCulture); + return ResourceManager.GetString("ResolverContextExtensions_ContextData_KeyNotFound", resourceCulture); } } - /// - /// Looks up a localized string similar to There is no event message on the context.. - /// - internal static string ThrowHelper_EventMessage_NotFound { + internal static string SchemaTypes_GetType_DoesNotExist { get { - return ResourceManager.GetString("ThrowHelper_EventMessage_NotFound", resourceCulture); + return ResourceManager.GetString("SchemaTypes_GetType_DoesNotExist", resourceCulture); } } - /// - /// Looks up a localized string similar to The field is already sealed and cannot be mutated.. - /// - internal static string ThrowHelper_FieldBase_Sealed { + internal static string SchemaTypes_DefinitionInvalid { get { - return ResourceManager.GetString("ThrowHelper_FieldBase_Sealed", resourceCulture); + return ResourceManager.GetString("SchemaTypes_DefinitionInvalid", resourceCulture); } } - /// - /// Looks up a localized string similar to The shape of the enum {0} is not known. - /// - internal static string ThrowHelper_Flags_Enum_Shape_Unknown { + internal static string InputObjectTypeDescriptor_OnlyProperties { get { - return ResourceManager.GetString("ThrowHelper_Flags_Enum_Shape_Unknown", resourceCulture); + return ResourceManager.GetString("InputObjectTypeDescriptor_OnlyProperties", resourceCulture); } } - /// - /// Looks up a localized string similar to One of the values of {0} does not have a valid name: {1}. - /// - internal static string ThrowHelper_Flags_IllegalFlagEnumName { + internal static string InterfaceTypeDescriptor_MustBePropertyOrMethod { get { - return ResourceManager.GetString("ThrowHelper_Flags_IllegalFlagEnumName", resourceCulture); + return ResourceManager.GetString("InterfaceTypeDescriptor_MustBePropertyOrMethod", resourceCulture); } } - /// - /// Looks up a localized string similar to Flags need to have at least one selection. Type: {0}. - /// - internal static string ThrowHelper_Flags_Parser_NoSelection { + internal static string ThrowHelper_FieldBase_Sealed { get { - return ResourceManager.GetString("ThrowHelper_Flags_Parser_NoSelection", resourceCulture); + return ResourceManager.GetString("ThrowHelper_FieldBase_Sealed", resourceCulture); } } - /// - /// Looks up a localized string similar to The value {0} is not known for type {1}. - /// - internal static string ThrowHelper_Flags_Parser_UnknownSelection { + internal static string TypeInitializer_CannotFindType { get { - return ResourceManager.GetString("ThrowHelper_Flags_Parser_UnknownSelection", resourceCulture); + return ResourceManager.GetString("TypeInitializer_CannotFindType", resourceCulture); } } - /// - /// Looks up a localized string similar to The type `{0}` does mot expect `{1}`.. - /// - internal static string ThrowHelper_FormatResultLeaf_InvalidSyntaxKind { + internal static string ThrowHelper_RelayIdFieldHelpers_NoFieldType { get { - return ResourceManager.GetString("ThrowHelper_FormatResultLeaf_InvalidSyntaxKind", resourceCulture); + return ResourceManager.GetString("ThrowHelper_RelayIdFieldHelpers_NoFieldType", resourceCulture); } } - /// - /// Looks up a localized string similar to The list result value of {0} must implement IList but is of the type {1}.. - /// - internal static string ThrowHelper_FormatResultList_InvalidObjectKind { + internal static string ThrowHelper_NodeResolver_ObjNoDefinition { get { - return ResourceManager.GetString("ThrowHelper_FormatResultList_InvalidObjectKind", resourceCulture); + return ResourceManager.GetString("ThrowHelper_NodeResolver_ObjNoDefinition", resourceCulture); } } - /// - /// Looks up a localized string similar to The input object `{1}` must to be of type `{2}` or serialized as `IReadOnlyDictionary<string. object?>` but not as `{0}`.. - /// - internal static string ThrowHelper_FormatResultObject_InvalidObjectKind { + internal static string ThrowHelper_NodeResolver_ArgumentTypeMissing { get { - return ResourceManager.GetString("ThrowHelper_FormatResultObject_InvalidObjectKind", resourceCulture); + return ResourceManager.GetString("ThrowHelper_NodeResolver_ArgumentTypeMissing", resourceCulture); } } - /// - /// Looks up a localized string similar to The list runtime value of {0} must implement IEnumerable or IList but is of the type {1}.. - /// - internal static string ThrowHelper_FormatValueList_InvalidObjectKind { + internal static string ThrowHelper_Schema_GetMember_TypeNotFound { get { - return ResourceManager.GetString("ThrowHelper_FormatValueList_InvalidObjectKind", resourceCulture); + return ResourceManager.GetString("ThrowHelper_Schema_GetMember_TypeNotFound", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified type `{0}` is expected to be an input type.. - /// - internal static string ThrowHelper_InputTypeExpected_Message { + internal static string ThrowHelper_Schema_GetMember_FieldNotFound { get { - return ResourceManager.GetString("ThrowHelper_InputTypeExpected_Message", resourceCulture); + return ResourceManager.GetString("ThrowHelper_Schema_GetMember_FieldNotFound", resourceCulture); } } - /// - /// Looks up a localized string similar to The fields `{0}` do not exist on the type `{1}`.. - /// - internal static string ThrowHelper_InvalidInputFieldNames { + internal static string ThrowHelper_Schema_GetMember_FieldArgNotFound { get { - return ResourceManager.GetString("ThrowHelper_InvalidInputFieldNames", resourceCulture); + return ResourceManager.GetString("ThrowHelper_Schema_GetMember_FieldArgNotFound", resourceCulture); } } - /// - /// Looks up a localized string similar to The field `{0}` does not exist on the type `{1}`.. - /// - internal static string ThrowHelper_InvalidInputFieldNames_Single { + internal static string ThrowHelper_Schema_GetMember_InvalidCoordinate { get { - return ResourceManager.GetString("ThrowHelper_InvalidInputFieldNames_Single", resourceCulture); + return ResourceManager.GetString("ThrowHelper_Schema_GetMember_InvalidCoordinate", resourceCulture); } } - /// - /// Looks up a localized string similar to The {0}-directive is missing the if-argument.. - /// - internal static string ThrowHelper_MissingDirectiveIfArgument { + internal static string ThrowHelper_Schema_GetMember_InputFieldNotFound { get { - return ResourceManager.GetString("ThrowHelper_MissingDirectiveIfArgument", resourceCulture); + return ResourceManager.GetString("ThrowHelper_Schema_GetMember_InputFieldNotFound", resourceCulture); } } - /// - /// Looks up a localized string similar to Mutation conventions infer the error name from the mutation. In this case the error union was inferred from the mutation `{0}` as `{1}`, but the type initialization encountered another object with the name `{1}`. Either rename the error object or specify a naming exception for this particular mutation. You can do that by using the `UseMutationConventionAttribute` for instance.. - /// - internal static string ThrowHelper_MutationDuplicateErrorName { + internal static string ThrowHelper_Schema_GetMember_EnumValueNotFound { get { - return ResourceManager.GetString("ThrowHelper_MutationDuplicateErrorName", resourceCulture); + return ResourceManager.GetString("ThrowHelper_Schema_GetMember_EnumValueNotFound", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified id field `{0}` does not exist on `{1}`.. - /// - internal static string ThrowHelper_NodeAttribute_IdFieldNotFound { + internal static string ThrowHelper_Schema_GetMember_DirectiveNotFound { get { - return ResourceManager.GetString("ThrowHelper_NodeAttribute_IdFieldNotFound", resourceCulture); + return ResourceManager.GetString("ThrowHelper_Schema_GetMember_DirectiveNotFound", resourceCulture); } } - /// - /// Looks up a localized string similar to A field argument at this initialization state is guaranteed to have an argument type, but we found none.. - /// - internal static string ThrowHelper_NodeResolver_ArgumentTypeMissing { + internal static string ThrowHelper_Schema_GetMember_DirectiveArgumentNotFound { get { - return ResourceManager.GetString("ThrowHelper_NodeResolver_ArgumentTypeMissing", resourceCulture); + return ResourceManager.GetString("ThrowHelper_Schema_GetMember_DirectiveArgumentNotFound", resourceCulture); } } - /// - /// Looks up a localized string similar to An object type at this point is guaranteed to have a type definition, but we found none.. - /// - internal static string ThrowHelper_NodeResolver_ObjNoDefinition { + internal static string ThrowHelper_FormatResultLeaf_InvalidSyntaxKind { get { - return ResourceManager.GetString("ThrowHelper_NodeResolver_ObjNoDefinition", resourceCulture); + return ResourceManager.GetString("ThrowHelper_FormatResultLeaf_InvalidSyntaxKind", resourceCulture); } } - /// - /// Looks up a localized string similar to Cannot accept null for non-nullable input.. - /// - internal static string ThrowHelper_NonNullInputViolation { + internal static string ThrowHelper_FormatResultList_InvalidObjectKind { get { - return ResourceManager.GetString("ThrowHelper_NonNullInputViolation", resourceCulture); + return ResourceManager.GetString("ThrowHelper_FormatResultList_InvalidObjectKind", resourceCulture); } } - /// - /// Looks up a localized string similar to `null` was set to the field `{0}`of the Oneof Input Object `{1}`. Oneof Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.. - /// - internal static string ThrowHelper_OneOfFieldIsNull { + internal static string ThrowHelper_FormatResultObject_InvalidObjectKind { get { - return ResourceManager.GetString("ThrowHelper_OneOfFieldIsNull", resourceCulture); + return ResourceManager.GetString("ThrowHelper_FormatResultObject_InvalidObjectKind", resourceCulture); } } - /// - /// Looks up a localized string similar to More than one field of the Oneof Input Object `{0}` is set. Oneof Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.. - /// - internal static string ThrowHelper_OneOfMoreThanOneFieldSet { + internal static string ThrowHelper_FormatValueList_InvalidObjectKind { get { - return ResourceManager.GetString("ThrowHelper_OneOfMoreThanOneFieldSet", resourceCulture); + return ResourceManager.GetString("ThrowHelper_FormatValueList_InvalidObjectKind", resourceCulture); } } - /// - /// Looks up a localized string similar to The Oneof Input Objects `{0}` require that exactly one field must be supplied and that field must not be `null`. Oneof Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.. - /// - internal static string ThrowHelper_OneOfNoFieldSet { + internal static string ThrowHelper_ParseList_InvalidObjectKind { get { - return ResourceManager.GetString("ThrowHelper_OneOfNoFieldSet", resourceCulture); + return ResourceManager.GetString("ThrowHelper_ParseList_InvalidObjectKind", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified type `{0}` is expected to be an output type.. - /// - internal static string ThrowHelper_OutputTypeExpected_Message { + internal static string ThrowHelper_ParseNestedList_InvalidSyntaxKind { get { - return ResourceManager.GetString("ThrowHelper_OutputTypeExpected_Message", resourceCulture); + return ResourceManager.GetString("ThrowHelper_ParseNestedList_InvalidSyntaxKind", resourceCulture); } } - /// - /// Looks up a localized string similar to The input object `{1}` must to be serialized as `{2}` or as `IReadOnlyDictionary<string. object?>` but not as `{0}`.. - /// internal static string ThrowHelper_ParseInputObject_InvalidObjectKind { get { return ResourceManager.GetString("ThrowHelper_ParseInputObject_InvalidObjectKind", resourceCulture); } } - /// - /// Looks up a localized string similar to The syntax node `{0}` is incompatible with the type `{1}`.. - /// internal static string ThrowHelper_ParseInputObject_InvalidSyntaxKind { get { return ResourceManager.GetString("ThrowHelper_ParseInputObject_InvalidSyntaxKind", resourceCulture); } } - /// - /// Looks up a localized string similar to The list `{1}` must to be serialized as `{2}` or as `IList` but not as `{0}`.. - /// - internal static string ThrowHelper_ParseList_InvalidObjectKind { + internal static string ThrowHelper_NonNullInputViolation { get { - return ResourceManager.GetString("ThrowHelper_ParseList_InvalidObjectKind", resourceCulture); + return ResourceManager.GetString("ThrowHelper_NonNullInputViolation", resourceCulture); } } - /// - /// Looks up a localized string similar to The item syntax node for a nested list must be `ListValue` but the parser found `{0}`.. - /// - internal static string ThrowHelper_ParseNestedList_InvalidSyntaxKind { + internal static string ThrowHelper_InvalidInputFieldNames { get { - return ResourceManager.GetString("ThrowHelper_ParseNestedList_InvalidSyntaxKind", resourceCulture); + return ResourceManager.GetString("ThrowHelper_InvalidInputFieldNames", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to resolve type from field `{0}`.. - /// - internal static string ThrowHelper_RelayIdFieldHelpers_NoFieldType { + internal static string ThrowHelper_RequiredInputFieldIsMissing { get { - return ResourceManager.GetString("ThrowHelper_RelayIdFieldHelpers_NoFieldType", resourceCulture); + return ResourceManager.GetString("ThrowHelper_RequiredInputFieldIsMissing", resourceCulture); } } - /// - /// Looks up a localized string similar to The required input field `{0}` is missing.. - /// - internal static string ThrowHelper_RequiredInputFieldIsMissing { + internal static string ThrowHelper_DataLoader_InvalidType { get { - return ResourceManager.GetString("ThrowHelper_RequiredInputFieldIsMissing", resourceCulture); + return ResourceManager.GetString("ThrowHelper_DataLoader_InvalidType", resourceCulture); } } - /// - /// Looks up a localized string similar to Argument `{0}` was not found on directive `@{1}`.. - /// - internal static string ThrowHelper_Schema_GetMember_DirectiveArgumentNotFound { + internal static string ThrowHelper_Convention_ConventionCouldNotBeCreated { get { - return ResourceManager.GetString("ThrowHelper_Schema_GetMember_DirectiveArgumentNotFound", resourceCulture); + return ResourceManager.GetString("ThrowHelper_Convention_ConventionCouldNotBeCreated", resourceCulture); } } - /// - /// Looks up a localized string similar to Directive `@{0}` not found.. - /// - internal static string ThrowHelper_Schema_GetMember_DirectiveNotFound { + internal static string ThrowHelper_Convention_TwoConventionsRegisteredForScope { get { - return ResourceManager.GetString("ThrowHelper_Schema_GetMember_DirectiveNotFound", resourceCulture); + return ResourceManager.GetString("ThrowHelper_Convention_TwoConventionsRegisteredForScope", resourceCulture); } } - /// - /// Looks up a localized string similar to Enum value `{0}` was not found on type `{1}`.. - /// - internal static string ThrowHelper_Schema_GetMember_EnumValueNotFound { + internal static string ThrowHelper_NodeAttribute_IdFieldNotFound { get { - return ResourceManager.GetString("ThrowHelper_Schema_GetMember_EnumValueNotFound", resourceCulture); + return ResourceManager.GetString("ThrowHelper_NodeAttribute_IdFieldNotFound", resourceCulture); } } - /// - /// Looks up a localized string similar to Argument `{0}` was not found on field `{1}.{2}`.. - /// - internal static string ThrowHelper_Schema_GetMember_FieldArgNotFound { + internal static string ThrowHelper_TypeCompletionContext_UnableToResolveType { get { - return ResourceManager.GetString("ThrowHelper_Schema_GetMember_FieldArgNotFound", resourceCulture); + return ResourceManager.GetString("ThrowHelper_TypeCompletionContext_UnableToResolveType", resourceCulture); } } - /// - /// Looks up a localized string similar to Field `{0}` was not found on type `{1}`.. - /// - internal static string ThrowHelper_Schema_GetMember_FieldNotFound { + internal static string ThrowHelper_TypeRegistrar_CreateInstanceFailed { get { - return ResourceManager.GetString("ThrowHelper_Schema_GetMember_FieldNotFound", resourceCulture); + return ResourceManager.GetString("ThrowHelper_TypeRegistrar_CreateInstanceFailed", resourceCulture); } } - /// - /// Looks up a localized string similar to Input field `{0}` was not found on type `{1}`.. - /// - internal static string ThrowHelper_Schema_GetMember_InputFieldNotFound { + internal static string ThrowHelper_Convention_UnableToCreateConvention { get { - return ResourceManager.GetString("ThrowHelper_Schema_GetMember_InputFieldNotFound", resourceCulture); + return ResourceManager.GetString("ThrowHelper_Convention_UnableToCreateConvention", resourceCulture); } } - /// - /// Looks up a localized string similar to The coordinate `{0}` is invalid for the type `{1}`.. - /// - internal static string ThrowHelper_Schema_GetMember_InvalidCoordinate { + internal static string ThrowHelper_SubscribeAttribute_SubscribeResolverNotFound { get { - return ResourceManager.GetString("ThrowHelper_Schema_GetMember_InvalidCoordinate", resourceCulture); + return ResourceManager.GetString("ThrowHelper_SubscribeAttribute_SubscribeResolverNotFound", resourceCulture); } } - /// - /// Looks up a localized string similar to A type with the name `{0}` was not found.. - /// - internal static string ThrowHelper_Schema_GetMember_TypeNotFound { + internal static string ThrowHelper_SubscribeAttribute_TopicTypeUnspecified { get { - return ResourceManager.GetString("ThrowHelper_Schema_GetMember_TypeNotFound", resourceCulture); + return ResourceManager.GetString("ThrowHelper_SubscribeAttribute_TopicTypeUnspecified", resourceCulture); } } - /// - /// Looks up a localized string similar to You need to specify the message type on {0}.{1}. (SubscribeAttribute). - /// internal static string ThrowHelper_SubscribeAttribute_MessageTypeUnspecified { get { return ResourceManager.GetString("ThrowHelper_SubscribeAttribute_MessageTypeUnspecified", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to find the subscribe resolver `{2}` defined on {0}.{1}. The subscribe resolver bust be a method that is public, non-static and on the same type as the resolver. (SubscribeAttribute). - /// - internal static string ThrowHelper_SubscribeAttribute_SubscribeResolverNotFound { + internal static string ThrowHelper_EventMessage_NotFound { get { - return ResourceManager.GetString("ThrowHelper_SubscribeAttribute_SubscribeResolverNotFound", resourceCulture); + return ResourceManager.GetString("ThrowHelper_EventMessage_NotFound", resourceCulture); } } - /// - /// Looks up a localized string similar to You need to specify the topic type on {0}.{1}. (SubscribeAttribute). - /// - internal static string ThrowHelper_SubscribeAttribute_TopicTypeUnspecified { + internal static string ThrowHelper_EventMessage_InvalidCast { get { - return ResourceManager.GetString("ThrowHelper_SubscribeAttribute_TopicTypeUnspecified", resourceCulture); + return ResourceManager.GetString("ThrowHelper_EventMessage_InvalidCast", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to resolve type reference `{0}`.. - /// - internal static string ThrowHelper_TypeCompletionContext_UnableToResolveType { + internal static string ErrorHelper_NeedsOneAtLeastField { get { - return ResourceManager.GetString("ThrowHelper_TypeCompletionContext_UnableToResolveType", resourceCulture); + return ResourceManager.GetString("ErrorHelper_NeedsOneAtLeastField", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to create instance of type `{0}`.. - /// - internal static string ThrowHelper_TypeRegistrar_CreateInstanceFailed { + internal static string ErrorHelper_TwoUnderscoresNotAllowedField { get { - return ResourceManager.GetString("ThrowHelper_TypeRegistrar_CreateInstanceFailed", resourceCulture); + return ResourceManager.GetString("ErrorHelper_TwoUnderscoresNotAllowedField", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to infer the element type from the current resolver. This often happens if the resolver is not an iterable type like IEnumerable, IQueryable, IList etc. Ensure that you either explicitly specify the element type or that the return type of your resolver is an iterable type.. - /// - internal static string ThrowHelper_UsePagingAttribute_NodeTypeUnknown { + internal static string ErrorHelper_TwoUnderscoresNotAllowedOnArgument { get { - return ResourceManager.GetString("ThrowHelper_UsePagingAttribute_NodeTypeUnknown", resourceCulture); + return ResourceManager.GetString("ErrorHelper_TwoUnderscoresNotAllowedOnArgument", resourceCulture); } } - /// - /// Looks up a localized string similar to The `TimeSpan` scalar represents an ISO-8601 compliant duration type.. - /// - internal static string TimeSpanType_Description { + internal static string ErrorHelper_TwoUnderscoresNotAllowedOnDirectiveName { get { - return ResourceManager.GetString("TimeSpanType_Description", resourceCulture); + return ResourceManager.GetString("ErrorHelper_TwoUnderscoresNotAllowedOnDirectiveName", resourceCulture); } } - /// - /// Looks up a localized string similar to The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum. - /// - ///Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other [rest of string was truncated]";. - /// - internal static string Type_Description { + internal static string ErrorHelper_NotTransitivelyImplemented { get { - return ResourceManager.GetString("Type_Description", resourceCulture); + return ResourceManager.GetString("ErrorHelper_NotTransitivelyImplemented", resourceCulture); } } - /// - /// Looks up a localized string similar to `specifiedByURL` may return a String (in the form of a URL) for custom scalars, otherwise it will return `null`.. - /// - internal static string Type_SpecifiedByUrl_Description { + internal static string ErrorHelper_InvalidFieldType { get { - return ResourceManager.GetString("Type_SpecifiedByUrl_Description", resourceCulture); + return ResourceManager.GetString("ErrorHelper_InvalidFieldType", resourceCulture); } } - /// - /// Looks up a localized string similar to The configuration delegate mustn't be null.. - /// - internal static string TypeConfiguration_ConfigureIsNull { + internal static string ErrorHelper_FieldNotImplemented { get { - return ResourceManager.GetString("TypeConfiguration_ConfigureIsNull", resourceCulture); + return ResourceManager.GetString("ErrorHelper_FieldNotImplemented", resourceCulture); } } - /// - /// Looks up a localized string similar to Definition mustn't be null.. - /// - internal static string TypeConfiguration_DefinitionIsNull { + internal static string ErrorHelper_InvalidArgumentType { get { - return ResourceManager.GetString("TypeConfiguration_DefinitionIsNull", resourceCulture); + return ResourceManager.GetString("ErrorHelper_InvalidArgumentType", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to convert type from `{0}` to `{1}`. - /// - internal static string TypeConversion_ConvertNotSupported { + internal static string ErrorHelper_AdditionalArgumentNotNullable { get { - return ResourceManager.GetString("TypeConversion_ConvertNotSupported", resourceCulture); + return ResourceManager.GetString("ErrorHelper_AdditionalArgumentNotNullable", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified type is not a schema type.. - /// - internal static string TypeDependency_MustBeSchemaType { + internal static string ErrorHelper_ArgumentNotImplemented { get { - return ResourceManager.GetString("TypeDependency_MustBeSchemaType", resourceCulture); + return ResourceManager.GetString("ErrorHelper_ArgumentNotImplemented", resourceCulture); } } - /// - /// Looks up a localized string similar to TypeReference kind not supported.. - /// - internal static string TypeDiscoveryInfo_TypeRefKindNotSupported { + internal static string ErrorHelper_OneofInputObjectMustHaveNullableFieldsWithoutDefaults { get { - return ResourceManager.GetString("TypeDiscoveryInfo_TypeRefKindNotSupported", resourceCulture); + return ResourceManager.GetString("ErrorHelper_OneofInputObjectMustHaveNullableFieldsWithoutDefaults", resourceCulture); } } - /// - /// Looks up a localized string similar to The type structure is invalid.. - /// - internal static string TypeExtensions_InvalidStructure { + internal static string ErrorHelper_InputObjectMustNotHaveRecursiveNonNullableReferencesToSelf { get { - return ResourceManager.GetString("TypeExtensions_InvalidStructure", resourceCulture); + return ResourceManager.GetString("ErrorHelper_InputObjectMustNotHaveRecursiveNonNullableReferencesToSelf", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified type kind is not supported.. - /// - internal static string TypeExtensions_KindIsNotSupported { + internal static string ErrorHelper_RequiredArgumentCannotBeDeprecated { get { - return ResourceManager.GetString("TypeExtensions_KindIsNotSupported", resourceCulture); + return ResourceManager.GetString("ErrorHelper_RequiredArgumentCannotBeDeprecated", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified type is not a valid list type.. - /// - internal static string TypeExtensions_NoListType { + internal static string ErrorHelper_RequiredFieldCannotBeDeprecated { get { - return ResourceManager.GetString("TypeExtensions_NoListType", resourceCulture); + return ResourceManager.GetString("ErrorHelper_RequiredFieldCannotBeDeprecated", resourceCulture); } } - /// - /// Looks up a localized string similar to The given type is not a {0}.. - /// - internal static string TypeExtensions_TypeIsNotOfT { + internal static string ErrorHelper_InterfaceHasNoImplementation { get { - return ResourceManager.GetString("TypeExtensions_TypeIsNotOfT", resourceCulture); + return ResourceManager.GetString("ErrorHelper_InterfaceHasNoImplementation", resourceCulture); } } - /// - /// Looks up a localized string similar to Request the type information of a single type.. - /// - internal static string TypeField_Description { + internal static string ErrorHelper_CompleteInterfacesHelper_UnableToResolveInterface { get { - return ResourceManager.GetString("TypeField_Description", resourceCulture); + return ResourceManager.GetString("ErrorHelper_CompleteInterfacesHelper_UnableToResolveInterface", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to find type(s) {0}. - /// - internal static string TypeInitializer_CannotFindType { + internal static string ErrorHelper_DirectiveCollection_ArgumentDoesNotExist { get { - return ResourceManager.GetString("TypeInitializer_CannotFindType", resourceCulture); + return ResourceManager.GetString("ErrorHelper_DirectiveCollection_ArgumentDoesNotExist", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to resolve dependencies {1} for type `{0}`.. - /// - internal static string TypeInitializer_CannotResolveDependency { + internal static string ErrorHelper_DirectiveCollection_ArgumentNonNullViolation { get { - return ResourceManager.GetString("TypeInitializer_CannotResolveDependency", resourceCulture); + return ResourceManager.GetString("ErrorHelper_DirectiveCollection_ArgumentNonNullViolation", resourceCulture); } } - /// - /// Looks up a localized string similar to The name `{0}` was already registered by another type.. - /// - internal static string TypeInitializer_CompleteName_Duplicate { + internal static string ErrorHelper_ObjectType_UnableToInferOrResolveType { get { - return ResourceManager.GetString("TypeInitializer_CompleteName_Duplicate", resourceCulture); + return ResourceManager.GetString("ErrorHelper_ObjectType_UnableToInferOrResolveType", resourceCulture); } } - /// - /// Looks up a localized string similar to The kind of the extension does not match the kind of the type `{0}`.. - /// - internal static string TypeInitializer_Merge_KindDoesNotMatch { + internal static string ErrorHelper_Relay_NoNodeResolver { get { - return ResourceManager.GetString("TypeInitializer_Merge_KindDoesNotMatch", resourceCulture); + return ResourceManager.GetString("ErrorHelper_Relay_NoNodeResolver", resourceCulture); } } - /// - /// Looks up a localized string similar to An enum describing what kind of type a given `__Type` is.. - /// - internal static string TypeKind_Description { + internal static string ErrorHelper_NodeResolver_MustHaveExactlyOneIdArg { get { - return ResourceManager.GetString("TypeKind_Description", resourceCulture); + return ResourceManager.GetString("ErrorHelper_NodeResolver_MustHaveExactlyOneIdArg", resourceCulture); } } - /// - /// Looks up a localized string similar to Indicates this type is an enum. `enumValues` is a valid field.. - /// - internal static string TypeKind_Enum { + internal static string ErrorHelper_NodeResolver_MustReturnObject { get { - return ResourceManager.GetString("TypeKind_Enum", resourceCulture); + return ResourceManager.GetString("ErrorHelper_NodeResolver_MustReturnObject", resourceCulture); } } - /// - /// Looks up a localized string similar to Indicates this type is an input object. `inputFields` is a valid field.. - /// - internal static string TypeKind_InputObject { + internal static string ErrorHelper_NodeResolver_NodeTypeHasNoId { get { - return ResourceManager.GetString("TypeKind_InputObject", resourceCulture); + return ResourceManager.GetString("ErrorHelper_NodeResolver_NodeTypeHasNoId", resourceCulture); } } - /// - /// Looks up a localized string similar to Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.. - /// - internal static string TypeKind_Interface { + internal static string ThrowHelper_InvalidInputFieldNames_Single { get { - return ResourceManager.GetString("TypeKind_Interface", resourceCulture); + return ResourceManager.GetString("ThrowHelper_InvalidInputFieldNames_Single", resourceCulture); } } - /// - /// Looks up a localized string similar to Indicates this type is a list. `ofType` is a valid field.. - /// - internal static string TypeKind_List { + internal static string ThrowHelper_MutationDuplicateErrorName { get { - return ResourceManager.GetString("TypeKind_List", resourceCulture); + return ResourceManager.GetString("ThrowHelper_MutationDuplicateErrorName", resourceCulture); } } - /// - /// Looks up a localized string similar to Indicates this type is a non-null. `ofType` is a valid field.. - /// - internal static string TypeKind_NonNull { + internal static string ErrorHelper_NodeResolverMissing { get { - return ResourceManager.GetString("TypeKind_NonNull", resourceCulture); + return ResourceManager.GetString("ErrorHelper_NodeResolverMissing", resourceCulture); } } - /// - /// Looks up a localized string similar to Indicates this type is an object. `fields` and `interfaces` are valid fields.. - /// - internal static string TypeKind_Object { + internal static string ThrowHelper_Flags_Enum_Shape_Unknown { get { - return ResourceManager.GetString("TypeKind_Object", resourceCulture); + return ResourceManager.GetString("ThrowHelper_Flags_Enum_Shape_Unknown", resourceCulture); } } - /// - /// Looks up a localized string similar to Indicates this type is a scalar.. - /// - internal static string TypeKind_Scalar { + internal static string ThrowHelper_Flags_Parser_NoSelection { get { - return ResourceManager.GetString("TypeKind_Scalar", resourceCulture); + return ResourceManager.GetString("ThrowHelper_Flags_Parser_NoSelection", resourceCulture); } } - /// - /// Looks up a localized string similar to Indicates this type is a union. `possibleTypes` is a valid field.. - /// - internal static string TypeKind_Union { + internal static string ThrowHelper_Flags_Parser_UnknownSelection { get { - return ResourceManager.GetString("TypeKind_Union", resourceCulture); + return ResourceManager.GetString("ThrowHelper_Flags_Parser_UnknownSelection", resourceCulture); } } - /// - /// Looks up a localized string similar to The name of the current Object type at runtime.. - /// - internal static string TypeNameField_Description { + internal static string ThrowHelper_Flags_IllegalFlagEnumName { get { - return ResourceManager.GetString("TypeNameField_Description", resourceCulture); + return ResourceManager.GetString("ThrowHelper_Flags_IllegalFlagEnumName", resourceCulture); } } - /// - /// Looks up a localized string similar to Invalid type structure.. - /// - internal static string TypeNameHelper_InvalidTypeStructure { + internal static string Directive_GetArgumentValue_UnknownArgument { get { - return ResourceManager.GetString("TypeNameHelper_InvalidTypeStructure", resourceCulture); + return ResourceManager.GetString("Directive_GetArgumentValue_UnknownArgument", resourceCulture); } } - /// - /// Looks up a localized string similar to Only type system objects are allowed as dependency.. - /// - internal static string TypeNameHelper_OnlyTypeSystemObjectsAreAllowed { + internal static string ErrorHelper_DirectiveCollection_ArgumentValueTypeIsWrong { get { - return ResourceManager.GetString("TypeNameHelper_OnlyTypeSystemObjectsAreAllowed", resourceCulture); + return ResourceManager.GetString("ErrorHelper_DirectiveCollection_ArgumentValueTypeIsWrong", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to infer or resolve a schema type from the type reference `{0}`.. - /// - internal static string TypeRegistrar_TypesInconsistent { + internal static string TypeDiscoveryInfo_TypeRefKindNotSupported { get { - return ResourceManager.GetString("TypeRegistrar_TypesInconsistent", resourceCulture); + return ResourceManager.GetString("TypeDiscoveryInfo_TypeRefKindNotSupported", resourceCulture); } } - /// - /// Looks up a localized string similar to The typeName mustn't be null or empty.. - /// - internal static string TypeResourceHelper_TypeNameEmptyOrNull { + internal static string ErrorHelper_FetchedToManyNodesAtOnce { get { - return ResourceManager.GetString("TypeResourceHelper_TypeNameEmptyOrNull", resourceCulture); + return ResourceManager.GetString("ErrorHelper_FetchedToManyNodesAtOnce", resourceCulture); } } - /// - /// Looks up a localized string similar to The description becomes immutable once it was assigned.. - /// - internal static string TypeSystemObject_DescriptionImmutable { + internal static string ThrowHelper_InputTypeExpected_Message { get { - return ResourceManager.GetString("TypeSystemObject_DescriptionImmutable", resourceCulture); + return ResourceManager.GetString("ThrowHelper_InputTypeExpected_Message", resourceCulture); } } - /// - /// Looks up a localized string similar to The name becomes immutable once it was assigned.. - /// - internal static string TypeSystemObject_NameImmutable { + internal static string ThrowHelper_OutputTypeExpected_Message { get { - return ResourceManager.GetString("TypeSystemObject_NameImmutable", resourceCulture); + return ResourceManager.GetString("ThrowHelper_OutputTypeExpected_Message", resourceCulture); } } - /// - /// Looks up a localized string similar to The type definition is null which means that the type was initialized incorrectly.. - /// - internal static string TypeSystemObjectBase_DefinitionIsNull { + internal static string TagDirective_Name_NotValid { get { - return ResourceManager.GetString("TypeSystemObjectBase_DefinitionIsNull", resourceCulture); + return ResourceManager.GetString("TagDirective_Name_NotValid", resourceCulture); } } - /// - /// Looks up a localized string similar to The type name was not completed correctly and is still empty. Type names are not allowed to remain empty after name completion was executed. - ///Type: `{0}`. - /// - internal static string TypeSystemObjectBase_NameIsNull { + internal static string TagDirective_Descriptor_NotSupported { get { - return ResourceManager.GetString("TypeSystemObjectBase_NameIsNull", resourceCulture); + return ResourceManager.GetString("TagDirective_Descriptor_NotSupported", resourceCulture); } } - /// - /// Looks up a localized string similar to A Union type must define one or more unique member types.. - /// - internal static string UnionType_MustHaveTypes { + internal static string ErrorHelper_DuplicateFieldName_Message { get { - return ResourceManager.GetString("UnionType_MustHaveTypes", resourceCulture); + return ResourceManager.GetString("ErrorHelper_DuplicateFieldName_Message", resourceCulture); } } - /// - /// Looks up a localized string similar to Unable to resolve the specified type reference.. - /// - internal static string UnionType_UnableToResolveType { + internal static string ErrorHelper_DuplicateDataMiddlewareDetected_Message { get { - return ResourceManager.GetString("UnionType_UnableToResolveType", resourceCulture); + return ResourceManager.GetString("ErrorHelper_DuplicateDataMiddlewareDetected_Message", resourceCulture); } } - /// - /// Looks up a localized string similar to The union type extension can only be merged with an union type.. - /// - internal static string UnionTypeExtension_CannotMerge { + internal static string SchemaException_UnexpectedError { get { - return ResourceManager.GetString("UnionTypeExtension_CannotMerge", resourceCulture); + return ResourceManager.GetString("SchemaException_UnexpectedError", resourceCulture); } } - /// - /// Looks up a localized string similar to Unknown format. Guid supports the following format chars: {{ `N`, `D`, `B`, `P` }}. - /// https://docs.microsoft.com/en-us/dotnet/api/system.buffers.text.utf8parser.tryparse?view=netcore-3.1#System_Buffers_Text_Utf8Parser_TryParse_System_ReadOnlySpan_System_Byte__System_Guid__System_Int32__System_Char. - /// - internal static string UuidType_FormatUnknown { + internal static string SchemaException_ErrorSummaryText { get { - return ResourceManager.GetString("UuidType_FormatUnknown", resourceCulture); + return ResourceManager.GetString("SchemaException_ErrorSummaryText", resourceCulture); } } - /// - /// Looks up a localized string similar to Variable `{0}` of type `{1}` must be an input type.. - /// - internal static string VariableValueBuilder_InputType { + internal static string ResolverContextExtensions_IsSelected_FieldNameEmpty { get { - return ResourceManager.GetString("VariableValueBuilder_InputType", resourceCulture); + return ResourceManager.GetString("ResolverContextExtensions_IsSelected_FieldNameEmpty", resourceCulture); } } - /// - /// Looks up a localized string similar to Variable `{0}` got invalid value.. - /// - internal static string VariableValueBuilder_InvalidValue { + internal static string ObjectToDictionaryConverter_CycleInObjectGraph { get { - return ResourceManager.GetString("VariableValueBuilder_InvalidValue", resourceCulture); + return ResourceManager.GetString("ObjectToDictionaryConverter_CycleInObjectGraph", resourceCulture); } } - /// - /// Looks up a localized string similar to The type node kind is not supported.. - /// - internal static string VariableValueBuilder_NodeKind { + internal static string LocalDateTimeType_Description { get { - return ResourceManager.GetString("VariableValueBuilder_NodeKind", resourceCulture); + return ResourceManager.GetString("LocalDateTimeType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to Variable `{0}` of type `{1}` must not be null.. - /// - internal static string VariableValueBuilder_NonNull { + internal static string LocalDateType_Description { get { - return ResourceManager.GetString("VariableValueBuilder_NonNull", resourceCulture); + return ResourceManager.GetString("LocalDateType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to Detected non-null violation in variable `{0}`.. - /// - internal static string VariableValueBuilder_NonNull_In_Graph { + internal static string LocalTimeType_Description { get { - return ResourceManager.GetString("VariableValueBuilder_NonNull_In_Graph", resourceCulture); + return ResourceManager.GetString("LocalTimeType_Description", resourceCulture); } } - /// - /// Looks up a localized string similar to Variable name mustn't be null or empty.. - /// - internal static string VariableValueBuilder_VarNameEmpty { + internal static string SchemaBuilder_BindRuntimeType_ObjectNotAllowed { get { - return ResourceManager.GetString("VariableValueBuilder_VarNameEmpty", resourceCulture); + return ResourceManager.GetString("SchemaBuilder_BindRuntimeType_ObjectNotAllowed", resourceCulture); } } } diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx index 01f33053b21..ddc00a9410f 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx @@ -1009,4 +1009,7 @@ Type: `{0}` The LocalTime scalar type is a local time string (i.e., with no associated timezone) in 24-hr HH:mm:ss. + + The type `System.Object` cannot be implicitly bound to a schema type. + diff --git a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs index d49531432f4..f207429a865 100644 --- a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs +++ b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs @@ -254,6 +254,13 @@ public ISchemaBuilder BindRuntimeType(Type runtimeType, Type schemaType) nameof(schemaType)); } + if (runtimeType == typeof(object)) + { + throw new ArgumentException( + TypeResources.SchemaBuilder_BindRuntimeType_ObjectNotAllowed, + nameof(runtimeType)); + } + var context = SchemaTypeReference.InferTypeContext(schemaType); _clrTypes[runtimeType] = (ti => ti.GetTypeRef(runtimeType, context), diff --git a/src/HotChocolate/Core/test/Types.Tests/CodeFirstTests.cs b/src/HotChocolate/Core/test/Types.Tests/CodeFirstTests.cs index 8ddbcb35596..81defd568c5 100644 --- a/src/HotChocolate/Core/test/Types.Tests/CodeFirstTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/CodeFirstTests.cs @@ -226,6 +226,13 @@ type QueryWithEnumerableArg { """); } + [Fact] + public void Disallow_Implicitly_Binding_Object() + { + Assert.Throws( + () => SchemaBuilder.New().BindRuntimeType()); + } + public class Query { public string SayHello(string name) => From bd9027ad4024075000e61502780bbf8aa67cc312 Mon Sep 17 00:00:00 2001 From: faddiv Date: Tue, 11 Mar 2025 16:28:05 +0100 Subject: [PATCH 28/64] Fixed invalid base64 ID parsing (#8101) --- .../Serialization/DefaultNodeIdSerializer.cs | 14 +++++++++++-- .../OptimizedNodeIdSerializer.cs | 14 +++++++++++-- .../Relay/DefaultNodeIdSerializerTests.cs | 20 ++++++++++++++++++ .../Relay/OptimizedNodeIdSerializerTests.cs | 21 +++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/DefaultNodeIdSerializer.cs b/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/DefaultNodeIdSerializer.cs index c01e0f0148d..c73be0f9a10 100644 --- a/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/DefaultNodeIdSerializer.cs +++ b/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/DefaultNodeIdSerializer.cs @@ -204,7 +204,12 @@ public NodeId Parse(string formattedId, INodeIdRuntimeTypeLookup runtimeTypeLook } } - Base64.DecodeFromUtf8InPlace(span, out var written); + var operationStatus = Base64.DecodeFromUtf8InPlace(span, out var written); + if (operationStatus != OperationStatus.Done) + { + throw new NodeIdInvalidFormatException(formattedId); + } + span = span.Slice(0, written); var delimiterIndex = FindDelimiterIndex(span); @@ -275,7 +280,12 @@ public NodeId Parse(string formattedId, Type runtimeType) } } - Base64.DecodeFromUtf8InPlace(span, out var written); + var operationStatus = Base64.DecodeFromUtf8InPlace(span, out var written); + if (operationStatus != OperationStatus.Done) + { + throw new NodeIdInvalidFormatException(formattedId); + } + span = span.Slice(0, written); var delimiterIndex = FindDelimiterIndex(span); diff --git a/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/OptimizedNodeIdSerializer.cs b/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/OptimizedNodeIdSerializer.cs index c487cbddd01..ca71b080699 100644 --- a/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/OptimizedNodeIdSerializer.cs +++ b/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/OptimizedNodeIdSerializer.cs @@ -106,7 +106,12 @@ public unsafe NodeId Parse(string formattedId, INodeIdRuntimeTypeLookup runtimeT } } - Base64.DecodeFromUtf8InPlace(span, out var written); + var operationStatus = Base64.DecodeFromUtf8InPlace(span, out var written); + if (operationStatus != OperationStatus.Done) + { + throw new NodeIdInvalidFormatException(formattedId); + } + span = span.Slice(0, written); var delimiterIndex = FindDelimiterIndex(span); @@ -176,7 +181,12 @@ public unsafe NodeId Parse(string formattedId, Type runtimeType) } } - Base64.DecodeFromUtf8InPlace(span, out var written); + var operationStatus = Base64.DecodeFromUtf8InPlace(span, out var written); + if (operationStatus != OperationStatus.Done) + { + throw new NodeIdInvalidFormatException(formattedId); + } + span = span.Slice(0, written); var delimiterIndex = FindDelimiterIndex(span); diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/DefaultNodeIdSerializerTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/DefaultNodeIdSerializerTests.cs index bfc0a115a77..74fb57a2387 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/DefaultNodeIdSerializerTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/DefaultNodeIdSerializerTests.cs @@ -518,6 +518,26 @@ public void Parse_Legacy_StronglyTypedId() Assert.Equal(stronglyTypedId, parsed.InternalId); } + [Fact] + public void Parse_Throws_NodeIdInvalidFormatException_On_InvalidBase64Input() + { + var serializer = CreateSerializer(new StringNodeIdValueSerializer()); + + Assert.Throws( + () => serializer.Parse("Rm9vOkJhcg", typeof(string))); + } + + [Fact] + public void ParseOnRuntimeLookup_Throws_NodeIdInvalidFormatException_On_InvalidBase64Input() + { + var lookup = new Mock(); + lookup.Setup(t => t.GetNodeIdRuntimeType(default)).Returns(default(Type)); + var serializer = CreateSerializer(new StringNodeIdValueSerializer()); + + Assert.Throws( + () => serializer.Parse("Rm9vOkJhcg", lookup.Object)); + } + [Fact] public void Ensure_Lookup_Works_With_HashCollision() { diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/OptimizedNodeIdSerializerTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/OptimizedNodeIdSerializerTests.cs index 681c8c74d50..c23d1116cf7 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/OptimizedNodeIdSerializerTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/OptimizedNodeIdSerializerTests.cs @@ -440,6 +440,27 @@ public void Parse_CompositeId() Assert.Equal(compositeId, parsed.InternalId); } + [Fact] + public void Parse_Throws_NodeIdInvalidFormatException_On_InvalidBase64Input() + { + var serializer = CreateSerializer("Foo", new StringNodeIdValueSerializer()); + + Assert.Throws( + () => serializer.Parse("Rm9vOkJhcg", typeof(string))); + } + + [Fact] + public void ParseOnRuntimeLookup_Throws_NodeIdInvalidFormatException_On_InvalidBase64Input() + { + var lookup = new Mock(); + lookup.Setup(t => t.GetNodeIdRuntimeType(default)).Returns(default(Type)); + + var serializer = CreateSerializer("Foo", new StringNodeIdValueSerializer()); + + Assert.Throws( + () => serializer.Parse("Rm9vOkJhcg", lookup.Object)); + } + [Fact] public void Ensure_Lookup_Works_With_HashCollision() { From 5e166077fa5faa135d743e0c8eb6ac04051630a4 Mon Sep 17 00:00:00 2001 From: Tobias Tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:24:50 +0100 Subject: [PATCH 29/64] [Fusion] Fix flaky Hot Reload test (#8120) --- .../test/Core.Tests/DemoIntegrationTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs index 1c27d956d6f..b9218ea9847 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs @@ -1286,6 +1286,7 @@ ... on User { public async Task Hot_Reload() { // arrange + var executorUpdatedResetEvent = new ManualResetEventSlim(false); using var demoProject = await DemoProject.CreateAsync(); var fusionGraph = @@ -1319,6 +1320,18 @@ public async Task Hot_Reload() var executorResolver = services.GetRequiredService(); var executorProxy = new RequestExecutorProxy(executorResolver, Schema.DefaultName); + var isFirstUpdate = true; + executorProxy.ExecutorUpdated += (sender, args) => + { + if (isFirstUpdate) + { + isFirstUpdate = false; + } + else + { + executorUpdatedResetEvent.Set(); + } + }; var result = await executorProxy.ExecuteAsync( OperationRequestBuilder @@ -1342,6 +1355,8 @@ public async Task Hot_Reload() config.SetConfiguration( new GatewayConfiguration( SchemaFormatter.FormatAsDocument(fusionGraph))); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + executorUpdatedResetEvent.Wait(cts.Token); result = await executorProxy.ExecuteAsync( OperationRequestBuilder From b07cb0259b9cb13d56f84801d34f0da940dc0032 Mon Sep 17 00:00:00 2001 From: Tobias Tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Thu, 13 Mar 2025 09:48:32 +0100 Subject: [PATCH 30/64] [Fusion] @semanticNonNull composition support (#7731) --- .../Extensions/ComplexTypeMergeExtensions.cs | 121 +- .../Composition/Extensions/MergeExtensions.cs | 8 +- .../SemanticNonNullCompositionTests.cs | 1458 +++++++++++++++++ .../Skimmed/src/Skimmed/BuiltIns/BuiltIns.cs | 17 + .../SemanticNonNullDirectiveDefinition.cs | 17 + .../src/Skimmed/Serialization/SchemaParser.cs | 4 + 6 files changed, 1622 insertions(+), 3 deletions(-) create mode 100644 src/HotChocolate/Fusion/test/Composition.Tests/SemanticNonNullCompositionTests.cs create mode 100644 src/HotChocolate/Skimmed/src/Skimmed/BuiltIns/SemanticNonNullDirectiveDefinition.cs diff --git a/src/HotChocolate/Fusion/src/Composition/Extensions/ComplexTypeMergeExtensions.cs b/src/HotChocolate/Fusion/src/Composition/Extensions/ComplexTypeMergeExtensions.cs index 9a2ff4430c2..d38f16f4b1d 100644 --- a/src/HotChocolate/Fusion/src/Composition/Extensions/ComplexTypeMergeExtensions.cs +++ b/src/HotChocolate/Fusion/src/Composition/Extensions/ComplexTypeMergeExtensions.cs @@ -1,3 +1,4 @@ +using HotChocolate.Language; using HotChocolate.Skimmed; using static HotChocolate.Fusion.Composition.MergeExtensions; @@ -64,6 +65,8 @@ public static void MergeField( return; } + MergeSemanticNonNullability(context, source, target); + if (!mergedType.Equals(target.Type, TypeComparison.Structural)) { target.Type = mergedType; @@ -101,7 +104,7 @@ public static void MergeField( return; } - if(!targetArgument.Type.Equals(mergedInputType, TypeComparison.Structural)) + if (!targetArgument.Type.Equals(mergedInputType, TypeComparison.Structural)) { targetArgument.Type = mergedInputType; } @@ -128,7 +131,7 @@ public static void MergeField( // If the target field is not deprecated and the source field is deprecated, copy over the target.MergeDeprecationWith(source); - target.MergeDirectivesWith(source, context); + target.MergeDirectivesWith(source, context, shouldApplySemanticNonNull: false); foreach (var sourceArgument in source.Arguments) { @@ -151,4 +154,118 @@ public static void MergeField( } } } + + private static void MergeSemanticNonNullability( + this CompositionContext context, + OutputFieldDefinition source, + OutputFieldDefinition target) + { + var sourceSemanticNonNullLevels = GetSemanticNonNullLevels(source); + var targetSemanticNonNullLevels = GetSemanticNonNullLevels(target); + + if (sourceSemanticNonNullLevels.Count < 1 && targetSemanticNonNullLevels.Count < 1) + { + return; + } + + List levels = []; + + var currentLevel = 0; + var currentSourceType = source.Type; + var currentTargetType = target.Type; + while (true) + { + if (currentTargetType is NonNullTypeDefinition targetNonNullType) + { + if (currentSourceType is not NonNullTypeDefinition) + { + if (sourceSemanticNonNullLevels.Contains(currentLevel)) + { + // Non-Null + Semantic Non-Null case + levels.Add(currentLevel); + } + } + + currentTargetType = targetNonNullType.NullableType; + } + else if (targetSemanticNonNullLevels.Contains(currentLevel)) + { + if (currentSourceType is NonNullTypeDefinition || sourceSemanticNonNullLevels.Contains(currentLevel)) + { + // Semantic Non-Null + (Non-Null || Semantic Non-Null) case + levels.Add(currentLevel); + } + } + + if (currentSourceType is NonNullTypeDefinition sourceNonNullType) + { + currentSourceType = sourceNonNullType.NullableType; + } + + if (currentTargetType is ListTypeDefinition targetListType) + { + currentTargetType = targetListType.ElementType; + + if (currentSourceType is ListTypeDefinition sourceListType) + { + currentSourceType = sourceListType.ElementType; + } + + currentLevel++; + + continue; + } + + break; + } + + var targetSemanticNonNullDirective = target.Directives + .FirstOrDefault(d => d.Name == BuiltIns.SemanticNonNull.Name); + + if (targetSemanticNonNullDirective is not null) + { + target.Directives.Remove(targetSemanticNonNullDirective); + } + + if (levels.Count < 1) + { + return; + } + + if (context.FusionGraph.DirectiveDefinitions.TryGetDirective(BuiltIns.SemanticNonNull.Name, + out var semanticNonNullDirectiveDefinition)) + { + if (levels is [0]) + { + target.Directives.Add(new Directive(semanticNonNullDirectiveDefinition)); + } + else + { + var levelsValueNode = new ListValueNode(levels.Select(l => new IntValueNode(l)).ToList()); + var levelsArgument = new ArgumentAssignment(BuiltIns.SemanticNonNull.Levels, levelsValueNode); + target.Directives.Add(new Directive(semanticNonNullDirectiveDefinition, levelsArgument)); + } + } + } + + private static List GetSemanticNonNullLevels(IDirectivesProvider provider) + { + var directive = provider.Directives + .FirstOrDefault(d => d.Name == BuiltIns.SemanticNonNull.Name); + + if (directive is null) + { + return []; + } + + if (directive.Arguments.TryGetValue(BuiltIns.SemanticNonNull.Levels, out var levelsArg)) + { + if (levelsArg is ListValueNode listValueNode) + { + return listValueNode.Items.Cast().Select(i => i.ToInt32()).ToList(); + } + } + + return [0]; + } } diff --git a/src/HotChocolate/Fusion/src/Composition/Extensions/MergeExtensions.cs b/src/HotChocolate/Fusion/src/Composition/Extensions/MergeExtensions.cs index 4e504bd2907..b9e26f20074 100644 --- a/src/HotChocolate/Fusion/src/Composition/Extensions/MergeExtensions.cs +++ b/src/HotChocolate/Fusion/src/Composition/Extensions/MergeExtensions.cs @@ -170,7 +170,8 @@ internal static void MergeDescriptionWith(this INamedTypeDefinition target, INam internal static void MergeDirectivesWith( this IDirectivesProvider target, IDirectivesProvider source, - CompositionContext context) + CompositionContext context, + bool shouldApplySemanticNonNull = true) { foreach (var directive in source.Directives) { @@ -183,6 +184,11 @@ internal static void MergeDirectivesWith( continue; } + if (directive.Name == BuiltIns.SemanticNonNull.Name && !shouldApplySemanticNonNull) + { + continue; + } + context.FusionGraph.DirectiveDefinitions.TryGetDirective(directive.Name, out var directiveDefinition); if (!target.Directives.ContainsName(directive.Name)) diff --git a/src/HotChocolate/Fusion/test/Composition.Tests/SemanticNonNullCompositionTests.cs b/src/HotChocolate/Fusion/test/Composition.Tests/SemanticNonNullCompositionTests.cs new file mode 100644 index 00000000000..ae0a0e334ef --- /dev/null +++ b/src/HotChocolate/Fusion/test/Composition.Tests/SemanticNonNullCompositionTests.cs @@ -0,0 +1,1458 @@ +using CookieCrumble; +using HotChocolate.Fusion.Metadata; +using HotChocolate.Fusion.Shared; +using HotChocolate.Language; +using HotChocolate.Skimmed; +using HotChocolate.Skimmed.Serialization; +using Xunit.Abstractions; + +namespace HotChocolate.Fusion.Composition; + +public class SemanticNonNullComposeTests(ITestOutputHelper output) +{ + # region SemantionNonNull & Nullable + + [Fact] + public async Task Merge_SemanticNonNull_With_Nullable() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType + } + + type SubType { + field: String + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: SubType + } + + type SubType { + field: String + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_Nullable_With_SemanticNonNull() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType + } + + type SubType { + field: String + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: SubType + } + + type SubType { + field: String + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_With_Nullable_List() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_Nullable_List_With_SemanticNonNull_List() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_ListItem_With_Nullable_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_Nullable_ListItem_With_SemanticNonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_And_ListItem_With_Nullable_List_And_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [0,1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [0,1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_Nullable_List_And_ListItem_With_SemanticNonNull_List_And_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [0,1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [0,1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] + } + + type SubType { + field: [String] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task + Merge_SemanticNonNull_List_Nullable_List_SemanticNonNull_ListItem_With_Nullable_List_Nullable_List_Nullable_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType]] @semanticNonNull(levels: [0, 2]) + } + + type SubType { + field: [[String]] @semanticNonNull(levels: [0, 2]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType]] + } + + type SubType { + field: [[String]] + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [[SubType]] + } + + type SubType { + field: [[String]] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task + Merge_Nullable_List_Nullable_List_Nullable_ListItem_With_SemanticNonNull_List_Nullable_List_SemanticNonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType]] + } + + type SubType { + field: [[String]] + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType]] @semanticNonNull(levels: [0, 2]) + } + + type SubType { + field: [[String]] @semanticNonNull(levels: [0, 2]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [[SubType]] + } + + type SubType { + field: [[String]] + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + # endregion + + #region SemanticNonNull & SemanticNonNull + + [Fact] + public async Task Merge_SemanticNonNull_With_SemanticNonNull() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_With_SemanticNonNull_List() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_ListItem_With_SemanticNonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull(levels: [ 1 ]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [ 1 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task + Merge_SemanticNonNull_List_Nullable_List_SemanticNonNull_ListItem_With_SemanticNonNull_List_Nullable_List_SemanticNonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType]] @semanticNonNull(levels: [0, 2]) + } + + type SubType { + field: [[String]] @semanticNonNull(levels: [0, 2]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType]] @semanticNonNull(levels: [0, 2]) + } + + type SubType { + field: [[String]] @semanticNonNull(levels: [0, 2]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [[SubType]] @semanticNonNull(levels: [ 0, 2 ]) + } + + type SubType { + field: [[String]] @semanticNonNull(levels: [ 0, 2 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_And_ListItem_With_SemanticNonNull_List_And_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [0,1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [0,1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [0,1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [0,1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull(levels: [ 0, 1 ]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [ 0, 1 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_With_Nullable_And_NonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType!] @semanticNonNull + } + + type SubType { + field: [String!] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_With_NonNull_And_Nullable_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType!] @semanticNonNull + } + + type SubType { + field: [String!] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + # endregion + + # region SemanticNonNull & NonNull + + [Fact] + public async Task Merge_SemanticNonNull_With_NonNull() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType! + } + + type SubType { + field: String! + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_NonNull_With_SemanticNonNull() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType! + } + + type SubType { + field: String! + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: SubType @semanticNonNull + } + + type SubType { + field: String @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_With_NonNull_List() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType]! + } + + type SubType { + field: [String]! + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_NonNull_List_With_SemanticNonNull_List() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType]! + } + + type SubType { + field: [String]! + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull + } + + type SubType { + field: [String] @semanticNonNull + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_ListItem_With_NonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType!] + } + + type SubType { + field: [String!] + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull(levels: [ 1 ]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [ 1 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_NonNull_ListItem_With_SemanticNonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType!] + } + + type SubType { + field: [String!] + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull(levels: [ 1 ]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [ 1 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_SemanticNonNull_List_And_ListItem_With_NonNull_List_And_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [0,1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [0,1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType!]! + } + + type SubType { + field: [String!]! + } + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull(levels: [ 0, 1 ]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [ 0, 1 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task Merge_NonNull_List_And_ListItem_With_SemanticNonNull_List_And_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType!]! + } + + type SubType { + field: [String!]! + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [SubType] @semanticNonNull(levels: [0,1]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [0,1]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [SubType] @semanticNonNull(levels: [ 0, 1 ]) + } + + type SubType { + field: [String] @semanticNonNull(levels: [ 0, 1 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + [Fact] + public async Task + Merge_NonNull_List_Nullable_List_NonNull_ListItem_With_SemanticNonNull_List_Nullable_List_SemanticNonNull_ListItem() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType!]]! + } + + type SubType { + field: [[String!]]! + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + field: [[SubType]] @semanticNonNull(levels: [0, 2]) + } + + type SubType { + field: [[String]] @semanticNonNull(levels: [0, 2]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """ + ); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + + // act + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // assert + GetSchemaWithoutFusion(fusionGraph).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: [[SubType]] @semanticNonNull(levels: [ 0, 2 ]) + } + + type SubType { + field: [[String]] @semanticNonNull(levels: [ 0, 2 ]) + } + + directive @semanticNonNull(levels: [Int] = [ 0 ]) on FIELD_DEFINITION + """); + } + + #endregion + + private static DocumentNode GetSchemaWithoutFusion(SchemaDefinition fusionGraph) + { + var sourceText = SchemaFormatter.FormatAsString(fusionGraph); + var fusionGraphDoc = Utf8GraphQLParser.Parse(sourceText); + var typeNames = FusionTypeNames.From(fusionGraphDoc); + var rewriter = new FusionGraphConfigurationToSchemaRewriter(); + + var rewrittenDocumentNode = (DocumentNode)rewriter.Rewrite(fusionGraphDoc, new(typeNames))!; + + return rewrittenDocumentNode; + } +} diff --git a/src/HotChocolate/Skimmed/src/Skimmed/BuiltIns/BuiltIns.cs b/src/HotChocolate/Skimmed/src/Skimmed/BuiltIns/BuiltIns.cs index 985e53f4c80..68aa2913219 100644 --- a/src/HotChocolate/Skimmed/src/Skimmed/BuiltIns/BuiltIns.cs +++ b/src/HotChocolate/Skimmed/src/Skimmed/BuiltIns/BuiltIns.cs @@ -105,6 +105,23 @@ public static SpecifiedByDirectiveDefinition Create(SchemaDefinition schema) } } + public static class SemanticNonNull + { + public const string Name = "semanticNonNull"; + public const string Levels = "levels"; + + public static SemanticNonNullDirectiveDefinition Create(SchemaDefinition schema) + { + if (!schema.Types.TryGetType(Int.Name, out var intTypeDef)) + { + intTypeDef = Int.Create(); + schema.Types.Add(intTypeDef); + } + + return new SemanticNonNullDirectiveDefinition(intTypeDef); + } + } + public static bool IsBuiltInScalar(string name) => name switch { diff --git a/src/HotChocolate/Skimmed/src/Skimmed/BuiltIns/SemanticNonNullDirectiveDefinition.cs b/src/HotChocolate/Skimmed/src/Skimmed/BuiltIns/SemanticNonNullDirectiveDefinition.cs new file mode 100644 index 00000000000..7b7abea56e4 --- /dev/null +++ b/src/HotChocolate/Skimmed/src/Skimmed/BuiltIns/SemanticNonNullDirectiveDefinition.cs @@ -0,0 +1,17 @@ +using HotChocolate.Language; +using DirectiveLocation = HotChocolate.Types.DirectiveLocation; + +namespace HotChocolate.Skimmed; + +public sealed class SemanticNonNullDirectiveDefinition : DirectiveDefinition +{ + internal SemanticNonNullDirectiveDefinition(ScalarTypeDefinition intType) + : base(BuiltIns.SemanticNonNull.Name) + { + var levelsArgument = new InputFieldDefinition(BuiltIns.SemanticNonNull.Levels, new ListTypeDefinition(intType)); + levelsArgument.DefaultValue = new ListValueNode(new IntValueNode(0)); + Arguments.Add(levelsArgument); + + Locations = DirectiveLocation.FieldDefinition; + } +} diff --git a/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaParser.cs b/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaParser.cs index 23a47d64270..139b54ee0ae 100644 --- a/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaParser.cs +++ b/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaParser.cs @@ -633,6 +633,10 @@ private static void BuildDirectiveCollection( { directiveType = BuiltIns.SpecifiedBy.Create(schema); } + else if (directiveNode.Name.Value == BuiltIns.SemanticNonNull.Name) + { + directiveType = BuiltIns.SemanticNonNull.Create(schema); + } else { directiveType = new DirectiveDefinition(directiveNode.Name.Value); From d489aa3d7363cb6662e3d2764f5f1b6ea86954b5 Mon Sep 17 00:00:00 2001 From: Glen Date: Wed, 12 Mar 2025 21:40:24 +0200 Subject: [PATCH 31/64] Updated node ID serializers to ensure correct padding while parsing (#8123) --- .../Serialization/DefaultNodeIdSerializer.cs | 38 +++++++++++++++++++ .../OptimizedNodeIdSerializer.cs | 38 +++++++++++++++++++ .../Relay/DefaultNodeIdSerializerTests.cs | 24 +++++++++++- .../Relay/OptimizedNodeIdSerializerTests.cs | 24 +++++++++++- 4 files changed, 120 insertions(+), 4 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/DefaultNodeIdSerializer.cs b/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/DefaultNodeIdSerializer.cs index c73be0f9a10..3e4dbb6d62f 100644 --- a/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/DefaultNodeIdSerializer.cs +++ b/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/DefaultNodeIdSerializer.cs @@ -204,6 +204,25 @@ public NodeId Parse(string formattedId, INodeIdRuntimeTypeLookup runtimeTypeLook } } + // Ensure correct padding. + var firstPaddingIndex = span.IndexOf((byte)'='); + var nonPaddedLength = firstPaddingIndex == -1 ? span.Length : firstPaddingIndex; + var actualPadding = firstPaddingIndex == -1 ? 0 : span.Length - firstPaddingIndex; + var expectedPadding = (4 - nonPaddedLength % 4) % 4; + + if (actualPadding != expectedPadding) + { + Span correctedSpan = stackalloc byte[nonPaddedLength + expectedPadding]; + span[..nonPaddedLength].CopyTo(correctedSpan); + + for (var i = nonPaddedLength; i < correctedSpan.Length; i++) + { + correctedSpan[i] = (byte)'='; + } + + span = correctedSpan; + } + var operationStatus = Base64.DecodeFromUtf8InPlace(span, out var written); if (operationStatus != OperationStatus.Done) { @@ -280,6 +299,25 @@ public NodeId Parse(string formattedId, Type runtimeType) } } + // Ensure correct padding. + var firstPaddingIndex = span.IndexOf((byte)'='); + var nonPaddedLength = firstPaddingIndex == -1 ? span.Length : firstPaddingIndex; + var actualPadding = firstPaddingIndex == -1 ? 0 : span.Length - firstPaddingIndex; + var expectedPadding = (4 - nonPaddedLength % 4) % 4; + + if (actualPadding != expectedPadding) + { + Span correctedSpan = stackalloc byte[nonPaddedLength + expectedPadding]; + span[..nonPaddedLength].CopyTo(correctedSpan); + + for (var i = nonPaddedLength; i < correctedSpan.Length; i++) + { + correctedSpan[i] = (byte)'='; + } + + span = correctedSpan; + } + var operationStatus = Base64.DecodeFromUtf8InPlace(span, out var written); if (operationStatus != OperationStatus.Done) { diff --git a/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/OptimizedNodeIdSerializer.cs b/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/OptimizedNodeIdSerializer.cs index ca71b080699..3471ed90662 100644 --- a/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/OptimizedNodeIdSerializer.cs +++ b/src/HotChocolate/Core/src/Types/Types/Relay/Serialization/OptimizedNodeIdSerializer.cs @@ -106,6 +106,25 @@ public unsafe NodeId Parse(string formattedId, INodeIdRuntimeTypeLookup runtimeT } } + // Ensure correct padding. + var firstPaddingIndex = span.IndexOf((byte)'='); + var nonPaddedLength = firstPaddingIndex == -1 ? span.Length : firstPaddingIndex; + var actualPadding = firstPaddingIndex == -1 ? 0 : span.Length - firstPaddingIndex; + var expectedPadding = (4 - nonPaddedLength % 4) % 4; + + if (actualPadding != expectedPadding) + { + Span correctedSpan = stackalloc byte[nonPaddedLength + expectedPadding]; + span[..nonPaddedLength].CopyTo(correctedSpan); + + for (var i = nonPaddedLength; i < correctedSpan.Length; i++) + { + correctedSpan[i] = (byte)'='; + } + + span = correctedSpan; + } + var operationStatus = Base64.DecodeFromUtf8InPlace(span, out var written); if (operationStatus != OperationStatus.Done) { @@ -181,6 +200,25 @@ public unsafe NodeId Parse(string formattedId, Type runtimeType) } } + // Ensure correct padding. + var firstPaddingIndex = span.IndexOf((byte)'='); + var nonPaddedLength = firstPaddingIndex == -1 ? span.Length : firstPaddingIndex; + var actualPadding = firstPaddingIndex == -1 ? 0 : span.Length - firstPaddingIndex; + var expectedPadding = (4 - nonPaddedLength % 4) % 4; + + if (actualPadding != expectedPadding) + { + Span correctedSpan = stackalloc byte[nonPaddedLength + expectedPadding]; + span[..nonPaddedLength].CopyTo(correctedSpan); + + for (var i = nonPaddedLength; i < correctedSpan.Length; i++) + { + correctedSpan[i] = (byte)'='; + } + + span = correctedSpan; + } + var operationStatus = Base64.DecodeFromUtf8InPlace(span, out var written); if (operationStatus != OperationStatus.Done) { diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/DefaultNodeIdSerializerTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/DefaultNodeIdSerializerTests.cs index 74fb57a2387..617f7eea282 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/DefaultNodeIdSerializerTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/DefaultNodeIdSerializerTests.cs @@ -524,7 +524,7 @@ public void Parse_Throws_NodeIdInvalidFormatException_On_InvalidBase64Input() var serializer = CreateSerializer(new StringNodeIdValueSerializer()); Assert.Throws( - () => serializer.Parse("Rm9vOkJhcg", typeof(string))); + () => serializer.Parse("!", typeof(string))); } [Fact] @@ -535,7 +535,27 @@ public void ParseOnRuntimeLookup_Throws_NodeIdInvalidFormatException_On_InvalidB var serializer = CreateSerializer(new StringNodeIdValueSerializer()); Assert.Throws( - () => serializer.Parse("Rm9vOkJhcg", lookup.Object)); + () => serializer.Parse("!", lookup.Object)); + } + + [Theory] + [InlineData("RW50aXR5OjE")] // No padding (length: 11). + [InlineData("RW50aXR5OjE=")] // Correct padding (length: 12). + [InlineData("RW50aXR5OjE==")] // Excess padding (length: 13). + [InlineData("RW50aXR5OjE===")] // Excess padding (length: 14). + [InlineData("RW50aXR5OjE====")] // Excess padding (length: 15). + [InlineData("RW50aXR5OjE=====")] // Excess padding (length: 16). + public void Parse_Ensures_Correct_Padding(string id) + { + var lookup = new Mock(); + lookup.Setup(t => t.GetNodeIdRuntimeType("Entity")).Returns(typeof(int)); + var serializer = CreateSerializer(new Int32NodeIdValueSerializer()); + + void Act1() => serializer.Parse(id, typeof(int)); + void Act2() => serializer.Parse(id, lookup.Object); + + Assert.Null(Record.Exception(Act1)); + Assert.Null(Record.Exception(Act2)); } [Fact] diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/OptimizedNodeIdSerializerTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/OptimizedNodeIdSerializerTests.cs index c23d1116cf7..57d5979b8bb 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/OptimizedNodeIdSerializerTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/OptimizedNodeIdSerializerTests.cs @@ -446,7 +446,7 @@ public void Parse_Throws_NodeIdInvalidFormatException_On_InvalidBase64Input() var serializer = CreateSerializer("Foo", new StringNodeIdValueSerializer()); Assert.Throws( - () => serializer.Parse("Rm9vOkJhcg", typeof(string))); + () => serializer.Parse("!", typeof(string))); } [Fact] @@ -458,7 +458,27 @@ public void ParseOnRuntimeLookup_Throws_NodeIdInvalidFormatException_On_InvalidB var serializer = CreateSerializer("Foo", new StringNodeIdValueSerializer()); Assert.Throws( - () => serializer.Parse("Rm9vOkJhcg", lookup.Object)); + () => serializer.Parse("!", lookup.Object)); + } + + [Theory] + [InlineData("RW50aXR5OjE")] // No padding (length: 11). + [InlineData("RW50aXR5OjE=")] // Correct padding (length: 12). + [InlineData("RW50aXR5OjE==")] // Excess padding (length: 13). + [InlineData("RW50aXR5OjE===")] // Excess padding (length: 14). + [InlineData("RW50aXR5OjE====")] // Excess padding (length: 15). + [InlineData("RW50aXR5OjE=====")] // Excess padding (length: 16). + public void Parse_Ensures_Correct_Padding(string id) + { + var lookup = new Mock(); + lookup.Setup(t => t.GetNodeIdRuntimeType(default)).Returns(default(Type)); + var serializer = CreateSerializer("Entity", new Int32NodeIdValueSerializer()); + + void Act1() => serializer.Parse(id, typeof(int)); + void Act2() => serializer.Parse(id, lookup.Object); + + Assert.Null(Record.Exception(Act1)); + Assert.Null(Record.Exception(Act2)); } [Fact] From 299929f8d3c3fb59279e9e3b49f531390e21fa52 Mon Sep 17 00:00:00 2001 From: Glen Date: Thu, 13 Mar 2025 09:50:19 +0200 Subject: [PATCH 32/64] Updated ISelection.AsSelector to correctly handle the `nodes` field (#8122) --- ...otChocolateExecutionSelectionExtensions.cs | 2 +- .../Types/Relay/NodeResolverTests.cs | 61 ++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/HotChocolate/Core/src/Execution.Projections/Extensions/HotChocolateExecutionSelectionExtensions.cs b/src/HotChocolate/Core/src/Execution.Projections/Extensions/HotChocolateExecutionSelectionExtensions.cs index e5ffdd1898d..dfea6be7534 100644 --- a/src/HotChocolate/Core/src/Execution.Projections/Extensions/HotChocolateExecutionSelectionExtensions.cs +++ b/src/HotChocolate/Core/src/Execution.Projections/Extensions/HotChocolateExecutionSelectionExtensions.cs @@ -74,7 +74,7 @@ public static Expression> AsSelector( } if ((flags & FieldFlags.GlobalIdNodeField) == FieldFlags.GlobalIdNodeField - || (flags & FieldFlags.GlobalIdNodeField) == FieldFlags.GlobalIdNodeField) + || (flags & FieldFlags.GlobalIdNodesField) == FieldFlags.GlobalIdNodesField) { return GetOrCreateNodeExpression(selection); } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/NodeResolverTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/NodeResolverTests.cs index f67595d903e..ca7841d05e0 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/NodeResolverTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/NodeResolverTests.cs @@ -1,5 +1,6 @@ #pragma warning disable RCS1102 // Make class static using HotChocolate.Execution; +using HotChocolate.Execution.Processing; using HotChocolate.Language; using HotChocolate.Tests; using HotChocolate.Types.Relay; @@ -271,6 +272,33 @@ public async Task NodeAttribute_On_Extension_With_Renamed_Id() .MatchSnapshotAsync(); } + [Fact] + public async Task NodeResolver_And_AsSelector() + { + // arrange + var executor = + await new ServiceCollection() + .AddGraphQLServer() + .AddGlobalObjectIdentification() + .AddTypeExtension() + .AddTypeExtension() + .AddQueryType() + .BuildRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync( + """ + { + nodes(ids: ["RW50aXR5OmZvbw=="]) { + id + } + } + """); + + // assert + Assert.Null(result.ExpectOperationResult().Errors); + } + public class Query { public Entity GetEntity(string name) => new Entity { Name = name, }; @@ -292,7 +320,12 @@ protected override void Configure( public class Entity { - public string Id => Name; + public string Id + { + get => Name; + set => Name = value; + } + public string Name { get; set; } } @@ -351,6 +384,32 @@ public class EntityExtension4 public static Entity GetEntity(string id) => new() { Name = id, }; } + [Node] + [ExtendObjectType(typeof(Entity))] + public class EntityExtension5 + { + [NodeResolver] + public static Entity GetEntity(string id, ISelection selection) + { + selection.AsSelector(); + + return new Entity { Name = id, }; + } + } + + [Node] + [ExtendObjectType(typeof(Entity2))] + public class Entity2Extension1 + { + [NodeResolver] + public static Entity2 GetEntity2(string id, ISelection selection) + { + selection.AsSelector(); + + return new Entity2 { Name = id, }; + } + } + public class QueryEntityRenamed { public EntityNoId GetEntity(int id) From cc40755ae99fe5f5e7a913159c891292694472e0 Mon Sep 17 00:00:00 2001 From: Glen Date: Thu, 13 Mar 2025 10:50:01 +0200 Subject: [PATCH 33/64] Updated CI workflow to only run Docker Hub login step for local branches (#8121) --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae1ef56c478..eb17cc0a745 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -186,6 +186,11 @@ jobs: timeout-minutes: 5 - name: Log in to Docker Hub + # Run step only if branch is local (not from a fork). + if: > + github.event_name != 'pull_request' || + (github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository) uses: docker/login-action@v3 with: username: ${{ vars.DOCKERHUB_USERNAME }} From 1e3cf1220585d70bd202764956b6f8aada01bbb0 Mon Sep 17 00:00:00 2001 From: Tobias Tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Thu, 13 Mar 2025 14:24:32 +0100 Subject: [PATCH 34/64] [Fusion] Add failing test for same selection below field returning list of union (#7685) --- .../test/Core.Tests/DemoIntegrationTests.cs | 148 +++++++ .../Fusion/test/Core.Tests/UnionTests.cs | 373 +++++++++++++++++- ...That_Require_Data_From_Another_Subgraph.md | 153 +++++++ ...That_Require_Data_From_Another_Subgraph.md | 125 ++++++ ..._With_Differing_Union_Item_Dependencies.md | 191 +++++++++ ..._Union_Item_Dependencies_SameSelections.md | 189 +++++++++ ...ches_With_Differing_Resolve_Nodes_Item1.md | 124 ++++++ ...ches_With_Differing_Resolve_Nodes_Item2.md | 124 ++++++ .../Fusion/test/Shared/TestSubgraph.cs | 8 +- 9 files changed, 1425 insertions(+), 10 deletions(-) create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph.md create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph.md create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies_SameSelections.md create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md diff --git a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs index b9218ea9847..7add7cb0212 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs @@ -15,6 +15,154 @@ public class DemoIntegrationTests(ITestOutputHelper output) { private readonly Func _logFactory = () => new TestCompositionLog(output); + [Fact] + public async Task Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + item1: Item1! + item2: Item2! + } + + type Item1 { + product: Product! + } + + type Item2 { + product: Product! + } + + type Product implements Node { + id: ID! + } + + interface Node { + id: ID! + } + """); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + node(id: ID!): Node + nodes(ids: [ID!]!): [Node]! + } + + type Product implements Node { + id: ID! + name: String! + } + + interface Node { + id: ID! + } + """); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + item1 { + product { + id + name + } + } + item2 { + product { + id + name + } + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + } + + [Fact] + public async Task Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + interface Node { + id: ID! + } + + type Query { + productsA: [Product] + productsB: [Product] + } + + type Product implements Node { + id: ID! + name: String! + } + """); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + interface Node { + id: ID! + } + + type Query { + node(id: ID!): Node + nodes(ids: [ID!]!): [Node]! + } + + type Product implements Node { + id: ID! + price: Float! + reviewCount: Int! + } + """); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + productsA { + id + name + price + reviewCount + } + productsB { + id + name + price + reviewCount + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + } + [Fact] public async Task Authors_And_Reviews_AutoCompose() { diff --git a/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs index 94eef46c543..a90249fc4dd 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs @@ -4,6 +4,7 @@ using HotChocolate.Fusion.Shared; using HotChocolate.Skimmed.Serialization; using HotChocolate.Types; +using HotChocolate.Types.Relay; using Microsoft.Extensions.DependencyInjection; using Xunit.Abstractions; using static HotChocolate.Fusion.Shared.DemoProjectSchemaExtensions; @@ -12,14 +13,9 @@ namespace HotChocolate.Fusion; -public class UnionTests +public class UnionTests(ITestOutputHelper output) { - private readonly Func _logFactory; - - public UnionTests(ITestOutputHelper output) - { - _logFactory = () => new TestCompositionLog(output); - } + private readonly Func _logFactory = () => new TestCompositionLog(output); [Fact] public async Task Error_Union_With_Inline_Fragment() @@ -263,6 +259,369 @@ mutation Upload($input: UploadProductPictureInput!) { await snapshot.MatchMarkdownAsync(cts.Token); } + [Fact] + public async Task Union_Two_Branches_With_Differing_Resolve_Nodes_Item1() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .ModifyOptions(o => o.EnsureAllNodesCanBeResolved = false) + .AddGlobalObjectIdentification(); + }); + + var subgraphB = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + var subgraphC = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB, subgraphC]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + union(item: 1) { + ... on Item1 { + something + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + Assert.False(subgraphC.HasReceivedRequest); + } + + [Fact] + public async Task Union_Two_Branches_With_Differing_Resolve_Nodes_Item2() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .ModifyOptions(o => o.EnsureAllNodesCanBeResolved = false) + .AddGlobalObjectIdentification(); + }); + + var subgraphB = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + var subgraphC = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB, subgraphC]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + union(item: 3) { + ... on Item1 { + something + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + Assert.False(subgraphB.HasReceivedRequest); + } + + [Fact(Skip = "Fix with new planner")] + public async Task Union_List_With_Differing_Union_Item_Dependencies_SameSelections() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .ModifyOptions(o => o.EnsureAllNodesCanBeResolved = false) + .AddGlobalObjectIdentification(); + }); + + var subgraphB = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + listOfUnion { + __typename + ... on Item1 { + something + product { + id + name + } + } + ... on Item2 { + other + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + // Ideally it would just be one request, but that's for another day... + Assert.Equal(3, subgraphB.NumberOfReceivedRequests); + } + + [Fact] + public async Task Union_List_With_Differing_Union_Item_Dependencies() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .ModifyOptions(o => o.EnsureAllNodesCanBeResolved = false) + .AddGlobalObjectIdentification(); + }); + + var subgraphB = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + listOfUnion { + __typename + ... on Item1 { + something + product { + id + name + } + } + ... on Item2 { + other + product { + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + // Ideally it would just be one request, but that's for another day... + Assert.Equal(3, subgraphB.NumberOfReceivedRequests); + } + + [ObjectType("Query")] + public class SubgraphA_Query + { + public ISomeUnion GetUnion(int item) + { + return item switch + { + 1 => new SubgraphA_Item1("Something", new SubgraphA_Product(1)), + _ => new SubgraphA_Item3(true, new SubgraphA_Review(2)) + }; + } + + public List GetListOfUnion() + { + return + [ + new SubgraphA_Item1("Something", new SubgraphA_Product(1)), + new SubgraphA_Item2(123, new SubgraphA_Product(2)), + new SubgraphA_Item3(true, new SubgraphA_Review(3)), + new SubgraphA_Item1("Something", new SubgraphA_Product(4)), + new SubgraphA_Item2(123, new SubgraphA_Product(5)), + new SubgraphA_Item3(true, new SubgraphA_Review(6)) + ]; + } + } + + [UnionType("SomeUnion")] + public interface ISomeUnion + { + } + + [ObjectType("Item1")] + public record SubgraphA_Item1(string Something, SubgraphA_Product Product) : ISomeUnion; + + [ObjectType("Item2")] + public record SubgraphA_Item2(int Other, SubgraphA_Product Product) : ISomeUnion; + + [ObjectType("Item3")] + public record SubgraphA_Item3(bool Another, SubgraphA_Review Review) : ISomeUnion; + + [Node] + [ObjectType("Product")] + public record SubgraphA_Product(int Id); + + [Node] + [ObjectType("Product")] + public record SubgraphB_Product(int Id, string Name) + { + [NodeResolver] + public static SubgraphB_Product Get(int id) + => new SubgraphB_Product(id, "Product_" + id); + } + + [Node] + [ObjectType("Review")] + public record SubgraphA_Review(int Id); + + [Node] + [ObjectType("Review")] + public record SubgraphB_Review(int Id, int Score) + { + [NodeResolver] + public static SubgraphB_Review Get(int id) + => new SubgraphB_Review(id, id % 5); + } + private sealed class NoWebSockets : IWebSocketConnectionFactory { public IWebSocketConnection CreateConnection(string name) diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph.md new file mode 100644 index 00000000000..f8508e0e3be --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph.md @@ -0,0 +1,153 @@ +# Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph + +## Result + +```json +{ + "data": { + "productsA": [ + { + "id": "1", + "name": "string", + "price": 123.456, + "reviewCount": 123 + }, + { + "id": "2", + "name": "string", + "price": 123.456, + "reviewCount": 123 + }, + { + "id": "3", + "name": "string", + "price": 123.456, + "reviewCount": 123 + } + ], + "productsB": [ + { + "id": "4", + "name": "string", + "price": 123.456, + "reviewCount": 123 + }, + { + "id": "5", + "name": "string", + "price": 123.456, + "reviewCount": 123 + }, + { + "id": "6", + "name": "string", + "price": 123.456, + "reviewCount": 123 + } + ] + } +} +``` + +## Request + +```graphql +{ + productsA { + id + name + price + reviewCount + } + productsB { + id + name + price + reviewCount + } +} +``` + +## QueryPlan Hash + +```text +57DE21D1B552226339A985FBFA65E0DA2E33703C +``` + +## QueryPlan + +```json +{ + "document": "{ productsA { id name price reviewCount } productsB { id name price reviewCount } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_productsA_productsB_1 { productsA { id name __fusion_exports__1: id } productsB { id name __fusion_exports__2: id } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_productsA_productsB_2($__fusion_exports__1: [ID!]!) { nodes(ids: $__fusion_exports__1) { ... on Product { price reviewCount __fusion_exports__1: id } } }", + "selectionSetId": 1, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_productsA_productsB_3($__fusion_exports__2: [ID!]!) { nodes(ids: $__fusion_exports__2) { ... on Product { price reviewCount __fusion_exports__2: id } } }", + "selectionSetId": 2, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 1, + 2 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Product_id", + "__fusion_exports__2": "Product_id" + } +} +``` + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph.md new file mode 100644 index 00000000000..bf981d0d007 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph.md @@ -0,0 +1,125 @@ +# Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph + +## Result + +```json +{ + "data": { + "item1": { + "product": { + "id": "2", + "name": "string" + } + }, + "item2": { + "product": { + "id": "1", + "name": "string" + } + } + } +} +``` + +## Request + +```graphql +{ + item1 { + product { + id + name + } + } + item2 { + product { + id + name + } + } +} +``` + +## QueryPlan Hash + +```text +E78C88B24F708E43B14A881AD2A993668E068FE2 +``` + +## QueryPlan + +```json +{ + "document": "{ item1 { product { id name } } item2 { product { id name } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_item1_item2_1 { item1 { product { id __fusion_exports__1: id } } item2 { product { id __fusion_exports__2: id } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_2", + "document": "query fetch_item1_item2_2($__fusion_exports__1: ID!) { node(id: $__fusion_exports__1) { ... on Product { name } } }", + "selectionSetId": 3, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Resolve", + "subgraph": "Subgraph_2", + "document": "query fetch_item1_item2_3($__fusion_exports__2: ID!) { node(id: $__fusion_exports__2) { ... on Product { name } } }", + "selectionSetId": 4, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 3, + 4 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Product_id", + "__fusion_exports__2": "Product_id" + } +} +``` + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md new file mode 100644 index 00000000000..182019a3c69 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md @@ -0,0 +1,191 @@ +# Union_List_With_Differing_Union_Item_Dependencies + +## Result + +```json +{ + "data": { + "listOfUnion": [ + { + "__typename": "Item1", + "something": "Something", + "product": { + "id": "UHJvZHVjdDox", + "name": "Product_1" + } + }, + { + "__typename": "Item2", + "other": 123, + "product": { + "name": "Product_2" + } + }, + { + "__typename": "Item3", + "another": true, + "review": { + "id": "UmV2aWV3OjM=", + "score": 3 + } + }, + { + "__typename": "Item1", + "something": "Something", + "product": { + "id": "UHJvZHVjdDo0", + "name": "Product_4" + } + }, + { + "__typename": "Item2", + "other": 123, + "product": { + "name": "Product_5" + } + }, + { + "__typename": "Item3", + "another": true, + "review": { + "id": "UmV2aWV3OjY=", + "score": 1 + } + } + ] + } +} +``` + +## Request + +```graphql +{ + listOfUnion { + __typename + ... on Item1 { + something + product { + id + name + } + } + ... on Item2 { + other + product { + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } +} +``` + +## QueryPlan Hash + +```text +6F3D15770F165F5A7166C5598F4B1A7D6910A88D +``` + +## QueryPlan + +```json +{ + "document": "{ listOfUnion { __typename ... on Item1 { something product { id name } } ... on Item2 { other product { name } } ... on Item3 { another review { id score } } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_listOfUnion_1 { listOfUnion { __typename ... on Item3 { __typename another review { id __fusion_exports__1: id } } ... on Item2 { __typename other product { __fusion_exports__2: id } } ... on Item1 { __typename something product { id __fusion_exports__3: id } } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + }, + { + "variable": "__fusion_exports__3" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_2($__fusion_exports__1: [ID!]!) { nodes(ids: $__fusion_exports__1) { ... on Review { score __fusion_exports__1: id } } }", + "selectionSetId": 4, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_3($__fusion_exports__2: [ID!]!) { nodes(ids: $__fusion_exports__2) { ... on Product { name __fusion_exports__2: id } } }", + "selectionSetId": 5, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_4($__fusion_exports__3: [ID!]!) { nodes(ids: $__fusion_exports__3) { ... on Product { name __fusion_exports__3: id } } }", + "selectionSetId": 6, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__3" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 4, + 5, + 6 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Review_id", + "__fusion_exports__2": "Product_id", + "__fusion_exports__3": "Product_id" + } +} +``` + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies_SameSelections.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies_SameSelections.md new file mode 100644 index 00000000000..264d103c1e9 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies_SameSelections.md @@ -0,0 +1,189 @@ +# Union_List_With_Differing_Union_Item_Dependencies_SameSelections + +## Result + +```json +{ + "data": { + "listOfUnion": [ + { + "__typename": "Item1", + "something": "Something", + "product": { + "id": "UHJvZHVjdDox", + "name": "Product_1" + } + }, + { + "__typename": "Item2", + "other": 123, + "product": { + "id": "UHJvZHVjdDoy", + "name": "Product_2" + } + }, + { + "__typename": "Item3", + "another": true, + "review": { + "id": "UmV2aWV3OjM=", + "score": 3 + } + }, + { + "__typename": "Item1", + "something": "Something", + "product": { + "id": "UHJvZHVjdDo0", + "name": "Product_4" + } + }, + { + "__typename": "Item2", + "other": 123, + "product": { + "id": "UHJvZHVjdDo1", + "name": "Product_5" + } + }, + { + "__typename": "Item3", + "another": true, + "review": { + "id": "UmV2aWV3OjY=", + "score": 1 + } + } + ] + } +} +``` + +## Request + +```graphql +{ + listOfUnion { + __typename + ... on Item1 { + something + product { + id + name + } + } + ... on Item2 { + other + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } +} +``` + +## QueryPlan Hash + +```text +51CA519135EDC9C49C71D22D7EF0562D417753EA +``` + +## QueryPlan + +```json +{ + "document": "{ listOfUnion { __typename ... on Item1 { something product { id name } } ... on Item2 { other product { id name } } ... on Item3 { another review { id score } } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_listOfUnion_1 { listOfUnion { __typename ... on Item3 { __typename another review { id __fusion_exports__1: id } } ... on Item2 { __typename other product { id __fusion_exports__2: id } } ... on Item1 { __typename something product { id __fusion_exports__2: id } } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_2($__fusion_exports__1: [ID!]!) { nodes(ids: $__fusion_exports__1) { ... on Review { score __fusion_exports__1: id } } }", + "selectionSetId": 4, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_3($__fusion_exports__2: [ID!]!) { nodes(ids: $__fusion_exports__2) { ... on Product { name __fusion_exports__2: id } } }", + "selectionSetId": 5, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_4($__fusion_exports__2: [ID!]!) { nodes(ids: $__fusion_exports__2) { ... on Product { name __fusion_exports__2: id } } }", + "selectionSetId": 5, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 4, + 5 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Review_id", + "__fusion_exports__2": "Product_id" + } +} +``` + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md new file mode 100644 index 00000000000..3202ff88d96 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md @@ -0,0 +1,124 @@ +# Union_Two_Branches_With_Differing_Resolve_Nodes_Item1 + +## Result + +```json +{ + "data": { + "union": { + "something": "Something", + "product": { + "id": "UHJvZHVjdDox", + "name": "Product_1" + } + } + } +} +``` + +## Request + +```graphql +{ + union(item: 1) { + ... on Item1 { + something + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } +} +``` + +## QueryPlan Hash + +```text +6162CA1B5815FE006F5BEC9B4A2F51035B1990DC +``` + +## QueryPlan + +```json +{ + "document": "{ union(item: 1) { ... on Item1 { something product { id name } } ... on Item3 { another review { id score } } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_union_1 { union(item: 1) { __typename ... on Item3 { another review { id __fusion_exports__1: id } } ... on Item2 { } ... on Item1 { something product { id __fusion_exports__2: id } } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_3", + "document": "query fetch_union_2($__fusion_exports__1: ID!) { node(id: $__fusion_exports__1) { ... on Review { score } } }", + "selectionSetId": 4, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Resolve", + "subgraph": "Subgraph_2", + "document": "query fetch_union_3($__fusion_exports__2: ID!) { node(id: $__fusion_exports__2) { ... on Product { name } } }", + "selectionSetId": 5, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 4, + 5 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Review_id", + "__fusion_exports__2": "Product_id" + } +} +``` + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md new file mode 100644 index 00000000000..3c7d274db82 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md @@ -0,0 +1,124 @@ +# Union_Two_Branches_With_Differing_Resolve_Nodes_Item2 + +## Result + +```json +{ + "data": { + "union": { + "another": true, + "review": { + "id": "UmV2aWV3OjI=", + "score": 2 + } + } + } +} +``` + +## Request + +```graphql +{ + union(item: 3) { + ... on Item1 { + something + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } +} +``` + +## QueryPlan Hash + +```text +18FEEDD69A2D3612AD7DAAA5D221864EE5A17514 +``` + +## QueryPlan + +```json +{ + "document": "{ union(item: 3) { ... on Item1 { something product { id name } } ... on Item3 { another review { id score } } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_union_1 { union(item: 3) { __typename ... on Item3 { another review { id __fusion_exports__1: id } } ... on Item2 { } ... on Item1 { something product { id __fusion_exports__2: id } } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_3", + "document": "query fetch_union_2($__fusion_exports__1: ID!) { node(id: $__fusion_exports__1) { ... on Review { score } } }", + "selectionSetId": 4, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Resolve", + "subgraph": "Subgraph_2", + "document": "query fetch_union_3($__fusion_exports__2: ID!) { node(id: $__fusion_exports__2) { ... on Product { name } } }", + "selectionSetId": 5, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 4, + 5 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Review_id", + "__fusion_exports__2": "Product_id" + } +} +``` + diff --git a/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs b/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs index f2800c068a1..27f1fc9e5e9 100644 --- a/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs +++ b/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs @@ -48,7 +48,7 @@ public static async Task CreateAsync( { app.Use(next => context => { - testContext.HasReceivedRequest = true; + testContext.NumberOfReceivedRequests++; return next(context); }); @@ -60,10 +60,12 @@ public static async Task CreateAsync( return new TestSubgraph(testServer, schema, testContext, extensions, isOffline); } - public bool HasReceivedRequest => Context.HasReceivedRequest; + public int NumberOfReceivedRequests => Context.NumberOfReceivedRequests; + + public bool HasReceivedRequest => Context.NumberOfReceivedRequests > 0; } public class SubgraphTestContext { - public bool HasReceivedRequest { get; set; } + public int NumberOfReceivedRequests { get; set; } } From d2b9d1f97dd1e86723a4bcfb8d397790333cbd5c Mon Sep 17 00:00:00 2001 From: Tobias Tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Thu, 13 Mar 2025 14:26:27 +0100 Subject: [PATCH 35/64] [Fusion] Add failing tests for selections below shared, non-refetchable field (#7950) --- .../test/Core.Tests/DemoIntegrationTests.cs | 231 ++++++++++++++++++ ..._Subgraph_Type_Of_Shared_Field_Not_Node.md | 95 +++++++ ...ubgraph_Type_Of_Shared_Field_Not_Node_2.md | 99 ++++++++ ...ubgraph_Type_Of_Shared_Field_Not_Node_3.md | 103 ++++++++ 4 files changed, 528 insertions(+) create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node.md create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_2.md create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_3.md diff --git a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs index 7add7cb0212..aca41b49535 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs @@ -2341,6 +2341,237 @@ type ResaleSurveyFeedback { await snapshot.MatchMarkdownAsync(); } + [Fact(Skip = "Fix this test in the new planner")] + public async Task Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node() + { + // arrange + var subgraph1 = await TestSubgraph.CreateAsync( + """ + interface Node { + id: ID! + } + type Product implements Node { + id: ID! + subgraph1Only: ProductAvailability + } + type ProductAvailability implements Node { + id: ID! + sharedLinked: ProductAvailabilityMail! + } + type ProductAvailabilityMail { + subgraph1Only: String! + } + type Query { + node("ID of the object." id: ID!): Node + productById(id: ID!): Product + productAvailabilityById(id: ID!): ProductAvailability + } + """); + + var subgraph2 = await TestSubgraph.CreateAsync( + """ + interface Node { + id: ID! + } + type ProductAvailability implements Node { + id: ID! + sharedLinked: ProductAvailabilityMail! + } + type ProductAvailabilityMail { + subgraph2Only: Boolean! + } + type Query { + node("ID of the object." id: ID!): Node + productAvailabilityById(id: ID!): ProductAvailability + } + """); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph1, subgraph2]); + var executor = await subgraphs.GetExecutorAsync(); + var request = OperationRequestBuilder.New() + .SetDocument( + """ + query($productId: ID!) { + productById(id: $productId) { + subgraph1Only { + sharedLinked { + subgraph2Only + } + } + } + } + """) + .SetVariableValues(new Dictionary + { + ["productId"] = "UHJvZHVjdAppMzg2MzE4NTk=" + }) + .Build(); + + // act + var result = await executor.ExecuteAsync(request); + + // assert + MatchMarkdownSnapshot(request, result); + } + + [Fact(Skip = "Fix this test in the new planner")] + public async Task Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_2() + { + // arrange + var subgraph1 = await TestSubgraph.CreateAsync( + """ + interface Node { + id: ID! + } + type Product implements Node { + id: ID! + subgraph1Only: ProductAvailability + } + type ProductAvailability implements Node { + id: ID! + sharedLinked: ProductAvailabilityMail! + } + type ProductAvailabilityMail { + sharedScalar: String! + } + type Query { + node("ID of the object." id: ID!): Node + productById(id: ID!): Product + productAvailabilityById(id: ID!): ProductAvailability + } + """); + + var subgraph2 = await TestSubgraph.CreateAsync( + """ + interface Node { + id: ID! + } + type ProductAvailability implements Node { + sharedLinked: ProductAvailabilityMail! + subgraph2Only: Boolean! + id: ID! + } + type ProductAvailabilityMail { + subgraph2Only: Boolean! + sharedScalar: String! + } + type Query { + node("ID of the object." id: ID!): Node + productAvailabilityById(id: ID!): ProductAvailability + } + """); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph1, subgraph2]); + var executor = await subgraphs.GetExecutorAsync(); + var request = OperationRequestBuilder.New() + .SetDocument( + """ + query($productId: ID!) { + productById(id: $productId) { + subgraph1Only { + subgraph2Only + sharedLinked { + subgraph2Only + sharedScalar + } + } + } + } + """) + .SetVariableValues(new Dictionary + { + ["productId"] = "UHJvZHVjdAppMzg2MzE4NTk=" + }) + .Build(); + + // act + var result = await executor.ExecuteAsync(request); + + // assert + MatchMarkdownSnapshot(request, result); + } + + [Fact(Skip = "Fix this test in the new planner")] + public async Task Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_3() + { + // arrange + var subgraph1 = await TestSubgraph.CreateAsync( + """ + interface Node { + id: ID! + } + type Product implements Node { + id: ID! + subgraph1Only: ProductAvailability + } + type ProductAvailability implements Node { + id: ID! + sharedLinked: ProductAvailabilityMail! + subgraph1Only: Boolean! + } + type ProductAvailabilityMail { + sharedScalar: String! + subgraph1Only: String! + } + type Query { + node("ID of the object." id: ID!): Node + productById(id: ID!): Product + productAvailabilityById(id: ID!): ProductAvailability + } + """); + + var subgraph2 = await TestSubgraph.CreateAsync( + """ + interface Node { + id: ID! + } + type ProductAvailability implements Node { + sharedLinked: ProductAvailabilityMail! + subgraph2Only: Boolean! + id: ID! + } + type ProductAvailabilityMail { + subgraph2Only: Boolean! + sharedScalar: String! + } + type Query { + node("ID of the object." id: ID!): Node + productAvailabilityById(id: ID!): ProductAvailability + } + """); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph1, subgraph2]); + var executor = await subgraphs.GetExecutorAsync(); + var request = OperationRequestBuilder.New() + .SetDocument( + """ + query($productId: ID!) { + productById(id: $productId) { + subgraph1Only { + subgraph2Only + subgraph1Only + sharedLinked { + subgraph2Only + sharedScalar + subgraph1Only + } + } + } + } + """) + .SetVariableValues(new Dictionary + { + ["productId"] = "UHJvZHVjdAppMzg2MzE4NTk=" + }) + .Build(); + + // act + var result = await executor.ExecuteAsync(request); + + // assert + MatchMarkdownSnapshot(request, result); + } + public sealed class HotReloadConfiguration : IObservable { private GatewayConfiguration _configuration; diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node.md new file mode 100644 index 00000000000..768f2bacbfd --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node.md @@ -0,0 +1,95 @@ +# Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node + +## Result + +```json +{ + "data": { + "productById": { + "subgraph1Only": { + "sharedLinked": { + "subgraph2Only": true + } + } + } + } +} +``` + +## Request + +```graphql +query($productId: ID!) { + productById(id: $productId) { + subgraph1Only { + sharedLinked { + subgraph2Only + } + } + } +} +``` + +## QueryPlan Hash + +```text +248A8F4DE404D0D21F7ABBC9255D2880DFDF4C2C +``` + +## QueryPlan + +```json +{ + "document": "query($productId: ID!) { productById(id: $productId) { subgraph1Only { sharedLinked { subgraph2Only } } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_productById_1($productId: ID!) { productById(id: $productId) { subgraph1Only { __fusion_exports__1: id } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + } + ], + "forwardedVariables": [ + { + "variable": "productId" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Resolve", + "subgraph": "Subgraph_2", + "document": "query fetch_productById_2($__fusion_exports__1: ID!) { productAvailabilityById(id: $__fusion_exports__1) { sharedLinked { subgraph2Only } } }", + "selectionSetId": 2, + "path": [ + "productAvailabilityById" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 2 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "ProductAvailability_id" + } +} +``` diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_2.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_2.md new file mode 100644 index 00000000000..ddc8f4cdb46 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_2.md @@ -0,0 +1,99 @@ +# Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_2 + +## Result + +```json +{ + "data": { + "productById": { + "subgraph1Only": { + "subgraph2Only": true, + "sharedLinked": { + "subgraph2Only": true, + "sharedScalar": "string" + } + } + } + } +} +``` + +## Request + +```graphql +query($productId: ID!) { + productById(id: $productId) { + subgraph1Only { + subgraph2Only + sharedLinked { + subgraph2Only + sharedScalar + } + } + } +} +``` + +## QueryPlan Hash + +```text +16221984386FDA488A6F5EAB821D9A7090826999 +``` + +## QueryPlan + +```json +{ + "document": "query($productId: ID!) { productById(id: $productId) { subgraph1Only { subgraph2Only sharedLinked { subgraph2Only sharedScalar } } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_productById_1($productId: ID!) { productById(id: $productId) { subgraph1Only { sharedLinked { sharedScalar } __fusion_exports__1: id } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + } + ], + "forwardedVariables": [ + { + "variable": "productId" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Resolve", + "subgraph": "Subgraph_2", + "document": "query fetch_productById_2($__fusion_exports__1: ID!) { productAvailabilityById(id: $__fusion_exports__1) { subgraph2Only sharedLinked { subgraph2Only } } }", + "selectionSetId": 2, + "path": [ + "productAvailabilityById" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 2 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "ProductAvailability_id" + } +} +``` diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_3.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_3.md new file mode 100644 index 00000000000..bc33c7b491e --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_3.md @@ -0,0 +1,103 @@ +# Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_3 + +## Result + +```json +{ + "data": { + "productById": { + "subgraph1Only": { + "subgraph2Only": true, + "subgraph1Only": "string", + "sharedLinked": { + "subgraph2Only": true, + "sharedScalar": "string", + "subgraph1Only": true + } + } + } + } +} +``` + +## Request + +```graphql +query($productId: ID!) { + productById(id: $productId) { + subgraph1Only { + subgraph2Only + subgraph1Only + sharedLinked { + subgraph2Only + sharedScalar + subgraph1Only + } + } + } +} +``` + +## QueryPlan Hash + +```text +F2B9754A30AA3182C2DA7EFA077C433AEF212E55 +``` + +## QueryPlan + +```json +{ + "document": "query($productId: ID!) { productById(id: $productId) { subgraph1Only { subgraph2Only subgraph1Only sharedLinked { subgraph2Only sharedScalar subgraph1Only } } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_productById_1($productId: ID!) { productById(id: $productId) { subgraph1Only { subgraph1Only sharedLinked { sharedScalar subgraph1Only } __fusion_exports__1: id } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + } + ], + "forwardedVariables": [ + { + "variable": "productId" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_productById_2($__fusion_exports__1: ID!) { productAvailabilityById(id: $__fusion_exports__1) { subgraph2Only sharedLinked { subgraph2Only } } }", + "selectionSetId": 2, + "path": [ + "productAvailabilityById" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 2 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "ProductAvailability_id" + } +} +``` From b58d72c9529e73d711ba26a8676dbf838cbb2470 Mon Sep 17 00:00:00 2001 From: Glen Date: Thu, 13 Mar 2025 15:23:44 +0200 Subject: [PATCH 36/64] Added strict mode to Cookie Crumble (#8126) --- .../src/CookieCrumble/Snapshot.cs | 18 ++++ .../test/CookieCrumble.Tests/SnapshotTests.cs | 84 +++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/CookieCrumble/src/CookieCrumble/Snapshot.cs b/src/CookieCrumble/src/CookieCrumble/Snapshot.cs index e4d8f1e71be..b6ea6a72d1d 100644 --- a/src/CookieCrumble/src/CookieCrumble/Snapshot.cs +++ b/src/CookieCrumble/src/CookieCrumble/Snapshot.cs @@ -242,6 +242,7 @@ public async ValueTask MatchAsync(CancellationToken cancellationToken = default) if (!File.Exists(snapshotFile)) { + CheckStrictMode(); EnsureDirectoryExists(snapshotFile); await using var stream = File.Create(snapshotFile); await stream.WriteAsync(writer.WrittenMemory, cancellationToken); @@ -274,6 +275,7 @@ public void Match() if (!File.Exists(snapshotFile)) { + CheckStrictMode(); EnsureDirectoryExists(snapshotFile); using var stream = File.Create(snapshotFile); stream.Write(writer.WrittenSpan); @@ -309,6 +311,7 @@ public async ValueTask MatchMarkdownAsync(CancellationToken cancellationToken = if (!File.Exists(snapshotFile)) { + CheckStrictMode(); EnsureDirectoryExists(snapshotFile); await using var stream = File.Create(snapshotFile); await stream.WriteAsync(writer.WrittenMemory, cancellationToken); @@ -346,6 +349,7 @@ public void MatchMarkdown() if (!File.Exists(snapshotFile)) { + CheckStrictMode(); EnsureDirectoryExists(snapshotFile); using var stream = File.Create(snapshotFile); stream.Write(writer.WrittenSpan); @@ -720,6 +724,20 @@ from methodInfo in classDeclaringType.GetMethods() return actualMethodInfo; } + private static void CheckStrictMode() + { + var value = Environment.GetEnvironmentVariable("COOKIE_CRUMBLE_STRICT_MODE"); + + if (string.Equals(value, "on", StringComparison.Ordinal) + || (bool.TryParse(value, out var b) && b)) + { + _testFramework.ThrowTestException( + "Strict mode is enabled and no snapshot has been found " + + "for the current test. Create a new snapshot locally and " + + "rerun your tests."); + } + } + private readonly struct SnapshotSegment(string? name, object? value, ISnapshotValueFormatter formatter) : ISnapshotSegment { diff --git a/src/CookieCrumble/test/CookieCrumble.Tests/SnapshotTests.cs b/src/CookieCrumble/test/CookieCrumble.Tests/SnapshotTests.cs index 56cbeae2caa..cd3ef8ccb35 100644 --- a/src/CookieCrumble/test/CookieCrumble.Tests/SnapshotTests.cs +++ b/src/CookieCrumble/test/CookieCrumble.Tests/SnapshotTests.cs @@ -1,12 +1,19 @@ using System.Buffers; +using System.Runtime.CompilerServices; using System.Text; using CookieCrumble.Formatters; using CookieCrumble.Xunit; +using Xunit.Sdk; namespace CookieCrumble; public class SnapshotTests { + private const string _strictModeExceptionMessage = + "Strict mode is enabled and no snapshot has been found " + + "for the current test. Create a new snapshot locally and " + + "rerun your tests."; + static SnapshotTests() { Snapshot.RegisterTestFramework(new XunitFramework()); @@ -109,6 +116,83 @@ public void SnapshotBuilder_Segment_Custom_Global_Serializer() snapshot.Match(); } + [Theory] + [InlineData("on")] + [InlineData("true")] + public async Task Match_StrictMode_On(string strictMode) + { + Environment.SetEnvironmentVariable("COOKIE_CRUMBLE_STRICT_MODE", strictMode); + + var snapshot = new Snapshot(); + snapshot.Add(new MyClass { Foo = "123", }); + + async Task Act1() => await snapshot.MatchAsync(); + void Act2() => snapshot.Match(); + async Task Act3() => await snapshot.MatchMarkdownAsync(); + void Act4() => snapshot.MatchMarkdown(); + + try + { + Assert.Equal( + _strictModeExceptionMessage, + (await Assert.ThrowsAsync(Act1)).Message); + + Assert.Equal(_strictModeExceptionMessage, Assert.Throws(Act2).Message); + + Assert.Equal( + _strictModeExceptionMessage, + (await Assert.ThrowsAsync(Act3)).Message); + + Assert.Equal(_strictModeExceptionMessage, Assert.Throws(Act4).Message); + } + finally + { + Environment.SetEnvironmentVariable("COOKIE_CRUMBLE_STRICT_MODE", null); + } + } + + [Theory] + [InlineData(1, "off")] + [InlineData(2, "false")] + [InlineData(3, null)] + public async Task Match_StrictMode_Off(int number, string? strictMode) + { + Environment.SetEnvironmentVariable("COOKIE_CRUMBLE_STRICT_MODE", strictMode); + + var snapshot = new Snapshot(); + snapshot.Add(new MyClass { Foo = "123", }); + + async Task Act1() => await snapshot.SetPostFix($"MA_{number}").MatchAsync(); + void Act2() => snapshot.SetPostFix($"M_{number}").Match(); + async Task Act3() => await snapshot.SetPostFix($"MMA_{number}").MatchMarkdownAsync(); + void Act4() => snapshot.SetPostFix($"MM_{number}").MatchMarkdown(); + + try + { + var result1 = await Record.ExceptionAsync(Act1); + var result2 = Record.Exception(Act2); + var result3 = await Record.ExceptionAsync(Act3); + var result4 = Record.Exception(Act4); + + static string GetCallerFilePath([CallerFilePath] string filePath = "") => filePath; + var directory = Path.GetDirectoryName(GetCallerFilePath()) + "/__snapshots__"; + + File.Delete($"{directory}/SnapshotTests.Match_StrictMode_Off_MA_{number}.snap"); + File.Delete($"{directory}/SnapshotTests.Match_StrictMode_Off_M_{number}.snap"); + File.Delete($"{directory}/SnapshotTests.Match_StrictMode_Off_MMA_{number}.md"); + File.Delete($"{directory}/SnapshotTests.Match_StrictMode_Off_MM_{number}.md"); + + Assert.Null(result1); + Assert.Null(result2); + Assert.Null(result3); + Assert.Null(result4); + } + finally + { + Environment.SetEnvironmentVariable("COOKIE_CRUMBLE_STRICT_MODE", null); + } + } + public class MyClass { public string Foo { get; set; } = "Bar"; From 7376a43c68bb61c794ea55c38b0ad68127d05cdc Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 13 Mar 2025 11:57:01 +0100 Subject: [PATCH 37/64] Fixed GraphQL schema does not support operation validation. (#8125) --- .../Core/src/Validation/ErrorHelper.cs | 11 + .../Properties/Resources.Designer.cs | 324 +++++------------- .../src/Validation/Properties/Resources.resx | 3 + .../Core/src/Validation/Rules/FieldVisitor.cs | 8 + ...ErrorBehaviorTests.RootTypeNotDefined.json | 2 +- .../FieldMustBeDefinedRuleTests.cs | 37 ++ .../test/CostAnalysis.Tests/PagingTests.cs | 53 +++ ...agingTests.Execute_On_Missing_Root_Type.md | 35 ++ 8 files changed, 240 insertions(+), 233 deletions(-) create mode 100644 src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Execute_On_Missing_Root_Type.md diff --git a/src/HotChocolate/Core/src/Validation/ErrorHelper.cs b/src/HotChocolate/Core/src/Validation/ErrorHelper.cs index 4f84b35b050..e818dd4869c 100644 --- a/src/HotChocolate/Core/src/Validation/ErrorHelper.cs +++ b/src/HotChocolate/Core/src/Validation/ErrorHelper.cs @@ -280,6 +280,17 @@ public static IError FieldsAreNotMergeable( .Build(); } + public static IError OperationNotSupported( + this IDocumentValidatorContext context, + OperationType operationType) + { + return ErrorBuilder.New() + .SetMessage( + Resources.ErrorHelper_OperationNotSupported, + operationType) + .Build(); + } + public static IError FragmentNameNotUnique( this IDocumentValidatorContext context, FragmentDefinitionNode fragmentDefinition) diff --git a/src/HotChocolate/Core/src/Validation/Properties/Resources.Designer.cs b/src/HotChocolate/Core/src/Validation/Properties/Resources.Designer.cs index 7324aafa346..57a0d8f64f0 100644 --- a/src/HotChocolate/Core/src/Validation/Properties/Resources.Designer.cs +++ b/src/HotChocolate/Core/src/Validation/Properties/Resources.Designer.cs @@ -11,46 +11,32 @@ namespace HotChocolate.Validation.Properties { using System; - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { - private static global::System.Resources.ResourceManager resourceMan; + private static System.Resources.ResourceManager resourceMan; - private static global::System.Globalization.CultureInfo resourceCulture; + private static System.Globalization.CultureInfo resourceCulture; - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resources() { } - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HotChocolate.Validation.Properties.Resources", typeof(Resources).Assembly); + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("HotChocolate.Validation.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; } } - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -59,399 +45,273 @@ internal Resources() { } } - /// - /// Looks up a localized string similar to The schema name is mandatory in order to create a validator.. - /// + internal static string ServiceCollectionExtensions_Schema_Name_Is_Mandatory { + get { + return ResourceManager.GetString("ServiceCollectionExtensions_Schema_Name_Is_Mandatory", resourceCulture); + } + } + internal static string DefaultDocumentValidatorFactory_Schema_Name_Is_Mandatory { get { return ResourceManager.GetString("DefaultDocumentValidatorFactory_Schema_Name_Is_Mandatory", resourceCulture); } } - /// - /// Looks up a localized string similar to The context has an invalid state and is missing the schema.. - /// internal static string DocumentValidatorContext_Context_Invalid_State { get { return ResourceManager.GetString("DocumentValidatorContext_Context_Invalid_State", resourceCulture); } } - /// - /// Looks up a localized string similar to The argument `{0}` does not exist.. - /// - internal static string ErrorHelper_ArgumentDoesNotExist { + internal static string ErrorHelper_VariableIsNotCompatible { get { - return ResourceManager.GetString("ErrorHelper_ArgumentDoesNotExist", resourceCulture); + return ResourceManager.GetString("ErrorHelper_VariableIsNotCompatible", resourceCulture); } } - /// - /// Looks up a localized string similar to More than one argument with the same name in an argument set is ambiguous and invalid.. - /// - internal static string ErrorHelper_ArgumentNotUnique { + internal static string ErrorHelper_DirectiveNotValidInLocation { get { - return ResourceManager.GetString("ErrorHelper_ArgumentNotUnique", resourceCulture); + return ResourceManager.GetString("ErrorHelper_DirectiveNotValidInLocation", resourceCulture); } } - /// - /// Looks up a localized string similar to The argument `{0}` is required.. - /// - internal static string ErrorHelper_ArgumentRequired { + internal static string ErrorHelper_DirectiveNotSupported { get { - return ResourceManager.GetString("ErrorHelper_ArgumentRequired", resourceCulture); + return ResourceManager.GetString("ErrorHelper_DirectiveNotSupported", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified argument value does not match the argument type.. - /// - internal static string ErrorHelper_ArgumentValueIsNotCompatible { + internal static string ErrorHelper_TypeSystemDefinitionNotAllowed { get { - return ResourceManager.GetString("ErrorHelper_ArgumentValueIsNotCompatible", resourceCulture); + return ResourceManager.GetString("ErrorHelper_TypeSystemDefinitionNotAllowed", resourceCulture); } } - /// - /// Looks up a localized string similar to If a label is passed, it must be unique within all other @defer and @stream directives in the document.. - /// - internal static string ErrorHelper_DeferAndStreamDuplicateLabel { + internal static string ErrorHelper_UnionFieldError { get { - return ResourceManager.GetString("ErrorHelper_DeferAndStreamDuplicateLabel", resourceCulture); + return ResourceManager.GetString("ErrorHelper_UnionFieldError", resourceCulture); } } - /// - /// Looks up a localized string similar to If a label for @defer or @stream is passed, it must not be a variable.. - /// - internal static string ErrorHelper_DeferAndStreamLabelIsVariable { + internal static string ErrorHelper_FieldDoesNotExist { get { - return ResourceManager.GetString("ErrorHelper_DeferAndStreamLabelIsVariable", resourceCulture); + return ResourceManager.GetString("ErrorHelper_FieldDoesNotExist", resourceCulture); } } - /// - /// Looks up a localized string similar to The defer and stream directives are not allowed to be used on root fields of the mutation or subscription type.. - /// - internal static string ErrorHelper_DeferAndStreamNotAllowedOnMutationOrSubscriptionRoot { + internal static string ErrorHelper_LeafFieldsCannotHaveSelections { get { - return ResourceManager.GetString("ErrorHelper_DeferAndStreamNotAllowedOnMutationOrSubscriptionRoot", resourceCulture); + return ResourceManager.GetString("ErrorHelper_LeafFieldsCannotHaveSelections", resourceCulture); } } - /// - /// Looks up a localized string similar to Only one of each directive is allowed per location.. - /// - internal static string ErrorHelper_DirectiveMustBeUniqueInLocation { + internal static string ErrorHelper_ArgumentValueIsNotCompatible { get { - return ResourceManager.GetString("ErrorHelper_DirectiveMustBeUniqueInLocation", resourceCulture); + return ResourceManager.GetString("ErrorHelper_ArgumentValueIsNotCompatible", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified directive `{0}` is not supported by the current schema.. - /// - internal static string ErrorHelper_DirectiveNotSupported { + internal static string ErrorHelper_FieldValueIsNotCompatible { get { - return ResourceManager.GetString("ErrorHelper_DirectiveNotSupported", resourceCulture); + return ResourceManager.GetString("ErrorHelper_FieldValueIsNotCompatible", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified directive is not valid the current location.. - /// - internal static string ErrorHelper_DirectiveNotValidInLocation { + internal static string ErrorHelper_VariableDefaultValueIsNotCompatible { get { - return ResourceManager.GetString("ErrorHelper_DirectiveNotValidInLocation", resourceCulture); + return ResourceManager.GetString("ErrorHelper_VariableDefaultValueIsNotCompatible", resourceCulture); } } - /// - /// Looks up a localized string similar to The field `{0}` does not exist on the type `{1}`.. - /// - internal static string ErrorHelper_FieldDoesNotExist { + internal static string ErrorHelper_NoSelectionOnCompositeField { get { - return ResourceManager.GetString("ErrorHelper_FieldDoesNotExist", resourceCulture); + return ResourceManager.GetString("ErrorHelper_NoSelectionOnCompositeField", resourceCulture); + } + } + + internal static string ErrorHelper_NoSelectionOnRootType { + get { + return ResourceManager.GetString("ErrorHelper_NoSelectionOnRootType", resourceCulture); } } - /// - /// Looks up a localized string similar to `{0}` is a required field and cannot be null.. - /// internal static string ErrorHelper_FieldIsRequiredButNull { get { return ResourceManager.GetString("ErrorHelper_FieldIsRequiredButNull", resourceCulture); } } - /// - /// Looks up a localized string similar to Encountered fields for the same object that cannot be merged.. - /// internal static string ErrorHelper_FieldsAreNotMergeable { get { return ResourceManager.GetString("ErrorHelper_FieldsAreNotMergeable", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified value type of field `{0}` does not match the field type.. - /// - internal static string ErrorHelper_FieldValueIsNotCompatible { + internal static string ErrorHelper_FragmentNameNotUnique { get { - return ResourceManager.GetString("ErrorHelper_FieldValueIsNotCompatible", resourceCulture); + return ResourceManager.GetString("ErrorHelper_FragmentNameNotUnique", resourceCulture); } } - /// - /// Looks up a localized string similar to The graph of fragment spreads must not form any cycles including spreading itself. Otherwise an operation could infinitely spread or infinitely execute on cycles in the underlying data.. - /// - internal static string ErrorHelper_FragmentCycleDetected { + internal static string ErrorHelper_FragmentNotUsed { get { - return ResourceManager.GetString("ErrorHelper_FragmentCycleDetected", resourceCulture); + return ResourceManager.GetString("ErrorHelper_FragmentNotUsed", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified fragment `{0}` does not exist.. - /// - internal static string ErrorHelper_FragmentDoesNotExist { + internal static string ErrorHelper_FragmentCycleDetected { get { - return ResourceManager.GetString("ErrorHelper_FragmentDoesNotExist", resourceCulture); + return ResourceManager.GetString("ErrorHelper_FragmentCycleDetected", resourceCulture); } } - /// - /// Looks up a localized string similar to There are multiple fragments with the name `{0}`.. - /// - internal static string ErrorHelper_FragmentNameNotUnique { + internal static string ErrorHelper_FragmentDoesNotExist { get { - return ResourceManager.GetString("ErrorHelper_FragmentNameNotUnique", resourceCulture); + return ResourceManager.GetString("ErrorHelper_FragmentDoesNotExist", resourceCulture); } } - /// - /// Looks up a localized string similar to The parent type does not match the type condition on the fragment.. - /// internal static string ErrorHelper_FragmentNotPossible { get { return ResourceManager.GetString("ErrorHelper_FragmentNotPossible", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified fragment `{0}` is not used within the current document.. - /// - internal static string ErrorHelper_FragmentNotUsed { + internal static string ErrorHelper_FragmentTypeConditionUnknown { get { - return ResourceManager.GetString("ErrorHelper_FragmentNotUsed", resourceCulture); + return ResourceManager.GetString("ErrorHelper_FragmentTypeConditionUnknown", resourceCulture); } } - /// - /// Looks up a localized string similar to Fragments can only be declared on unions, interfaces, and objects.. - /// internal static string ErrorHelper_FragmentOnlyCompositeType { get { return ResourceManager.GetString("ErrorHelper_FragmentOnlyCompositeType", resourceCulture); } } - /// - /// Looks up a localized string similar to Unknown type `{0}`.. - /// - internal static string ErrorHelper_FragmentTypeConditionUnknown { - get { - return ResourceManager.GetString("ErrorHelper_FragmentTypeConditionUnknown", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There can be only one input field named `{0}`.. - /// internal static string ErrorHelper_InputFieldAmbiguous { get { return ResourceManager.GetString("ErrorHelper_InputFieldAmbiguous", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified input object field `{0}` does not exist.. - /// internal static string ErrorHelper_InputFieldDoesNotExist { get { return ResourceManager.GetString("ErrorHelper_InputFieldDoesNotExist", resourceCulture); } } - /// - /// Looks up a localized string similar to `{0}` is a required field and cannot be null.. - /// internal static string ErrorHelper_InputFieldRequired { get { return ResourceManager.GetString("ErrorHelper_InputFieldRequired", resourceCulture); } } - /// - /// Looks up a localized string similar to Introspection is not allowed for the current request.. - /// - internal static string ErrorHelper_IntrospectionNotAllowed { + internal static string ErrorHelper_OperationNameNotUnique { get { - return ResourceManager.GetString("ErrorHelper_IntrospectionNotAllowed", resourceCulture); + return ResourceManager.GetString("ErrorHelper_OperationNameNotUnique", resourceCulture); } } - /// - /// Looks up a localized string similar to Field "{0}" must not have a selection since type "{1}" has no subfields.. - /// - internal static string ErrorHelper_LeafFieldsCannotHaveSelections { + internal static string ErrorHelper_OperationAnonymousMoreThanOne { get { - return ResourceManager.GetString("ErrorHelper_LeafFieldsCannotHaveSelections", resourceCulture); + return ResourceManager.GetString("ErrorHelper_OperationAnonymousMoreThanOne", resourceCulture); } } - /// - /// Looks up a localized string similar to The GraphQL document has an execution depth of {0} which exceeds the max allowed execution depth of {1}.. - /// - internal static string ErrorHelper_MaxExecutionDepth { + internal static string ErrorHelper_VariableNotInputType { get { - return ResourceManager.GetString("ErrorHelper_MaxExecutionDepth", resourceCulture); + return ResourceManager.GetString("ErrorHelper_VariableNotInputType", resourceCulture); } } - /// - /// Looks up a localized string similar to Field "{0}" of type "{1}" must have a selection of subfields. Did you mean "{0} {{ ... }}"?. - /// - internal static string ErrorHelper_NoSelectionOnCompositeField { + internal static string ErrorHelper_VariableNameNotUnique { get { - return ResourceManager.GetString("ErrorHelper_NoSelectionOnCompositeField", resourceCulture); + return ResourceManager.GetString("ErrorHelper_VariableNameNotUnique", resourceCulture); } } - /// - /// Looks up a localized string similar to Operation `{0}` has a empty selection set. Root types without subfields are disallowed.. - /// - internal static string ErrorHelper_NoSelectionOnRootType { + internal static string ErrorHelper_ArgumentNotUnique { get { - return ResourceManager.GetString("ErrorHelper_NoSelectionOnRootType", resourceCulture); + return ResourceManager.GetString("ErrorHelper_ArgumentNotUnique", resourceCulture); } } - /// - /// Looks up a localized string similar to The Oneof Input Object `{0}` requires that exactly one field must be supplied and that field must not be `null`.. - /// - internal static string ErrorHelper_OneOfMustHaveExactlyOneField { + internal static string ErrorHelper_ArgumentRequired { get { - return ResourceManager.GetString("ErrorHelper_OneOfMustHaveExactlyOneField", resourceCulture); + return ResourceManager.GetString("ErrorHelper_ArgumentRequired", resourceCulture); } } - /// - /// Looks up a localized string similar to The variable `${0}` assigned to the field `{1}` of the Oneof Input Object `{2}` must be non-null.. - /// - internal static string ErrorHelper_OneOfVariablesMustBeNonNull { + internal static string ErrorHelper_ArgumentDoesNotExist { get { - return ResourceManager.GetString("ErrorHelper_OneOfVariablesMustBeNonNull", resourceCulture); + return ResourceManager.GetString("ErrorHelper_ArgumentDoesNotExist", resourceCulture); } } - /// - /// Looks up a localized string similar to GraphQL allows a short‐hand form for defining query operations when only that one operation exists in the document.. - /// - internal static string ErrorHelper_OperationAnonymousMoreThanOne { + internal static string ErrorHelper_SubscriptionSingleRootField { get { - return ResourceManager.GetString("ErrorHelper_OperationAnonymousMoreThanOne", resourceCulture); + return ResourceManager.GetString("ErrorHelper_SubscriptionSingleRootField", resourceCulture); } } - /// - /// Looks up a localized string similar to The operation name `{0}` is not unique.. - /// - internal static string ErrorHelper_OperationNameNotUnique { + internal static string ErrorHelper_SubscriptionNoTopLevelIntrospectionField { get { - return ResourceManager.GetString("ErrorHelper_OperationNameNotUnique", resourceCulture); + return ResourceManager.GetString("ErrorHelper_SubscriptionNoTopLevelIntrospectionField", resourceCulture); } } - /// - /// Looks up a localized string similar to Subscription must not select an introspection top level field.. - /// - internal static string ErrorHelper_SubscriptionNoTopLevelIntrospectionField { + internal static string ErrorHelper_MaxExecutionDepth { get { - return ResourceManager.GetString("ErrorHelper_SubscriptionNoTopLevelIntrospectionField", resourceCulture); + return ResourceManager.GetString("ErrorHelper_MaxExecutionDepth", resourceCulture); } } - /// - /// Looks up a localized string similar to Subscription operations must have exactly one root field.. - /// - internal static string ErrorHelper_SubscriptionSingleRootField { + internal static string ErrorHelper_DirectiveMustBeUniqueInLocation { get { - return ResourceManager.GetString("ErrorHelper_SubscriptionSingleRootField", resourceCulture); + return ResourceManager.GetString("ErrorHelper_DirectiveMustBeUniqueInLocation", resourceCulture); } } - /// - /// Looks up a localized string similar to A document containing TypeSystemDefinition is invalid for execution.. - /// - internal static string ErrorHelper_TypeSystemDefinitionNotAllowed { + internal static string ErrorHelper_IntrospectionNotAllowed { get { - return ResourceManager.GetString("ErrorHelper_TypeSystemDefinitionNotAllowed", resourceCulture); + return ResourceManager.GetString("ErrorHelper_IntrospectionNotAllowed", resourceCulture); } } - /// - /// Looks up a localized string similar to A union type cannot declare a field directly. Use inline fragments or fragments instead.. - /// - internal static string ErrorHelper_UnionFieldError { + internal static string ErrorHelper_OneOfMustHaveExactlyOneField { get { - return ResourceManager.GetString("ErrorHelper_UnionFieldError", resourceCulture); + return ResourceManager.GetString("ErrorHelper_OneOfMustHaveExactlyOneField", resourceCulture); } } - /// - /// Looks up a localized string similar to The specified value type of variable `{0}` does not match the variable type.. - /// - internal static string ErrorHelper_VariableDefaultValueIsNotCompatible { + internal static string ErrorHelper_OneOfVariablesMustBeNonNull { get { - return ResourceManager.GetString("ErrorHelper_VariableDefaultValueIsNotCompatible", resourceCulture); + return ResourceManager.GetString("ErrorHelper_OneOfVariablesMustBeNonNull", resourceCulture); } } - /// - /// Looks up a localized string similar to The variable `{0}` is not compatible with the type of the current location.. - /// - internal static string ErrorHelper_VariableIsNotCompatible { + internal static string ErrorHelper_DeferAndStreamNotAllowedOnMutationOrSubscriptionRoot { get { - return ResourceManager.GetString("ErrorHelper_VariableIsNotCompatible", resourceCulture); + return ResourceManager.GetString("ErrorHelper_DeferAndStreamNotAllowedOnMutationOrSubscriptionRoot", resourceCulture); } } - /// - /// Looks up a localized string similar to A document containing operations that define more than one variable with the same name is invalid for execution.. - /// - internal static string ErrorHelper_VariableNameNotUnique { + internal static string ErrorHelper_DeferAndStreamDuplicateLabel { get { - return ResourceManager.GetString("ErrorHelper_VariableNameNotUnique", resourceCulture); + return ResourceManager.GetString("ErrorHelper_DeferAndStreamDuplicateLabel", resourceCulture); } } - /// - /// Looks up a localized string similar to The type of variable `{0}` is not an input type.. - /// - internal static string ErrorHelper_VariableNotInputType { + internal static string ErrorHelper_DeferAndStreamLabelIsVariable { get { - return ResourceManager.GetString("ErrorHelper_VariableNotInputType", resourceCulture); + return ResourceManager.GetString("ErrorHelper_DeferAndStreamLabelIsVariable", resourceCulture); } } - /// - /// Looks up a localized string similar to The schema name is mandatory in order to add validation rules.. - /// - internal static string ServiceCollectionExtensions_Schema_Name_Is_Mandatory { + internal static string ErrorHelper_OperationNotSupported { get { - return ResourceManager.GetString("ServiceCollectionExtensions_Schema_Name_Is_Mandatory", resourceCulture); + return ResourceManager.GetString("ErrorHelper_OperationNotSupported", resourceCulture); } } } diff --git a/src/HotChocolate/Core/src/Validation/Properties/Resources.resx b/src/HotChocolate/Core/src/Validation/Properties/Resources.resx index fd84901b3e8..298589ad3b1 100644 --- a/src/HotChocolate/Core/src/Validation/Properties/Resources.resx +++ b/src/HotChocolate/Core/src/Validation/Properties/Resources.resx @@ -230,4 +230,7 @@ If a label for @defer or @stream is passed, it must not be a variable. + + This GraphQL schema does not support `{0}` operations. + diff --git a/src/HotChocolate/Core/src/Validation/Rules/FieldVisitor.cs b/src/HotChocolate/Core/src/Validation/Rules/FieldVisitor.cs index f62a8572742..5da8c725209 100644 --- a/src/HotChocolate/Core/src/Validation/Rules/FieldVisitor.cs +++ b/src/HotChocolate/Core/src/Validation/Rules/FieldVisitor.cs @@ -32,6 +32,14 @@ protected override ISyntaxVisitorAction Enter( OperationDefinitionNode node, IDocumentValidatorContext context) { + var operationType = context.Schema.GetOperationType(node.Operation); + + if (operationType == null) + { + context.ReportError(context.OperationNotSupported(node.Operation)); + return Skip; + } + context.FieldSets.Clear(); context.SelectionSets.Clear(); diff --git a/src/HotChocolate/Core/test/Execution.Tests/Errors/__snapshots__/ErrorBehaviorTests.RootTypeNotDefined.json b/src/HotChocolate/Core/test/Execution.Tests/Errors/__snapshots__/ErrorBehaviorTests.RootTypeNotDefined.json index 68dc49d4261..ce9f27a1a29 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Errors/__snapshots__/ErrorBehaviorTests.RootTypeNotDefined.json +++ b/src/HotChocolate/Core/test/Execution.Tests/Errors/__snapshots__/ErrorBehaviorTests.RootTypeNotDefined.json @@ -1,7 +1,7 @@ { "errors": [ { - "message": "The specified root type `Mutation` is not supported by this server." + "message": "This GraphQL schema does not support `Mutation` operations." } ] } diff --git a/src/HotChocolate/Core/test/Validation.Tests/FieldMustBeDefinedRuleTests.cs b/src/HotChocolate/Core/test/Validation.Tests/FieldMustBeDefinedRuleTests.cs index 597a1f6d458..d7658179753 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/FieldMustBeDefinedRuleTests.cs +++ b/src/HotChocolate/Core/test/Validation.Tests/FieldMustBeDefinedRuleTests.cs @@ -243,4 +243,41 @@ fragment interfaceFieldSelection on Cat { // assert Assert.Empty(context.Errors); } + + [Fact] + public void Ensure_Non_Existent_Root_Types_Cause_Error() + { + // arrange + IDocumentValidatorContext context = ValidationUtils.CreateContext(CreateQueryOnlySchema()); + var query = Utf8GraphQLParser.Parse( + """ + subscription { + foo + } + """); + context.Prepare(query); + + // act + Rule.Validate(context, query); + + // assert + Assert.Collection( + context.Errors, + t => Assert.Equal( + "This GraphQL schema does not support `Subscription` operations.", + t.Message)); + } + + private ISchema CreateQueryOnlySchema() + { + return SchemaBuilder.New() + .AddDocumentFromString( + """ + type Query { + foo: String + } + """) + .Use(next => next) + .Create(); + } } diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/PagingTests.cs b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/PagingTests.cs index d4651378df8..1d2be3189a1 100644 --- a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/PagingTests.cs +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/PagingTests.cs @@ -95,6 +95,59 @@ await snapshot .MatchMarkdownAsync(); } + [Fact] + public async Task Execute_On_Missing_Root_Type() + { + // arrange + var snapshot = new Snapshot(); + + var operation = + Utf8GraphQLParser.Parse( + """ + subscription { + books { + nodes { + title + } + } + } + """); + + var request = + OperationRequestBuilder.New() + .SetDocument(operation) + .ReportCost() + .Build(); + + var executor = + await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .ModifyPagingOptions(o => o.RequirePagingBoundaries = false) + .AddFiltering() + .AddSorting() + .BuildRequestExecutorAsync(); + + // act + var response = await executor.ExecuteAsync(request); + + // assert + var expectation = + JsonDocument.Parse( + """ + { + "fieldCost": 6, + "typeCost": 12 + } + """); + + await snapshot + .Add(operation, "Operation") + .Add(expectation.RootElement, "Expected") + .Add(response, "Response") + .MatchMarkdownAsync(); + } + [Fact] public async Task Require_Paging_Boundaries_By_Default_With_Connections() { diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Execute_On_Missing_Root_Type.md b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Execute_On_Missing_Root_Type.md new file mode 100644 index 00000000000..9b84dba574e --- /dev/null +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/__snapshots__/PagingTests.Execute_On_Missing_Root_Type.md @@ -0,0 +1,35 @@ +# Execute_On_Missing_Root_Type + +## Operation + +```graphql +subscription { + books { + nodes { + title + } + } +} +``` + +## Expected + +```json +{ + "fieldCost": 6, + "typeCost": 12 +} +``` + +## Response + +```json +{ + "errors": [ + { + "message": "This GraphQL schema does not support `Subscription` operations." + } + ] +} +``` + From a0f15267d318419d91e31b6c0387821df0449c54 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 13 Mar 2025 10:36:22 +0100 Subject: [PATCH 38/64] Make `includeDeprecated` non nullable. (#8124) --- .../Types/Types/Introspection/__Directive.cs | 3 +- .../src/Types/Types/Introspection/__Field.cs | 3 +- .../src/Types/Types/Introspection/__Type.cs | 11 +- ...sts.Ensure_Benchmark_Query_LargeQuery.snap | 50 +++++-- ...ectionTests.DefaultValueIsInputObject.snap | 50 +++++-- ...sts.ExecuteGraphiQLIntrospectionQuery.snap | 50 +++++-- ...cuteGraphiQLIntrospectionQuery_ToJson.snap | 50 +++++-- ...rstTests.DescriptionsAreCorrectlyRead.snap | 50 +++++-- ...rrectly_Exposed_Through_Introspection.snap | 50 +++++-- ...st.Execute_StarWarsIntrospection_Test.snap | 135 ++++++++++++++---- 10 files changed, 321 insertions(+), 131 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/__Directive.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/__Directive.cs index c8dcf809e39..5b8bc2c9473 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__Directive.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__Directive.cs @@ -19,7 +19,6 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c var stringType = Create(ScalarNames.String); var nonNullStringType = Parse($"{ScalarNames.String}!"); var nonNullBooleanType = Parse($"{ScalarNames.Boolean}!"); - var booleanType = Parse($"{ScalarNames.Boolean}"); var argumentListType = Parse($"[{nameof(__InputValue)}!]!"); var locationListType = Parse($"[{nameof(__DirectiveLocation)}!]!"); @@ -37,7 +36,7 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c { Arguments = { - new(Names.IncludeDeprecated, type: booleanType) + new(Names.IncludeDeprecated, type: nonNullBooleanType) { DefaultValue = BooleanValueNode.False, RuntimeDefaultValue = false, diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/__Field.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/__Field.cs index 62a860fd98b..999418d7472 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__Field.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__Field.cs @@ -20,7 +20,6 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c var nonNullStringType = Parse($"{ScalarNames.String}!"); var nonNullTypeType = Parse($"{nameof(__Type)}!"); var nonNullBooleanType = Parse($"{ScalarNames.Boolean}!"); - var booleanType = Parse($"{ScalarNames.Boolean}"); var argumentListType = Parse($"[{nameof(__InputValue)}!]!"); var directiveListType = Parse($"[{nameof(__AppliedDirective)}!]!"); @@ -37,7 +36,7 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c { Arguments = { - new(Names.IncludeDeprecated, type: booleanType) + new(Names.IncludeDeprecated, type: nonNullBooleanType) { DefaultValue = BooleanValueNode.False, RuntimeDefaultValue = false, diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/__Type.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/__Type.cs index 6e221afd789..74fa9ff852d 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__Type.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__Type.cs @@ -18,6 +18,7 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c { var stringType = Create(ScalarNames.String); var booleanType = Create(ScalarNames.Boolean); + var nonNullBooleanType = Parse($"{ScalarNames.Boolean}!"); var kindType = Parse($"{nameof(__TypeKind)}!"); var typeType = Create(nameof(__Type)); var fieldListType = Parse($"[{nameof(__Field)}!]"); @@ -40,7 +41,7 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c { Arguments = { - new(Names.IncludeDeprecated, type: booleanType) + new(Names.IncludeDeprecated, type: nonNullBooleanType) { DefaultValue = BooleanValueNode.False, RuntimeDefaultValue = false, @@ -53,10 +54,8 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c { Arguments = { - new() + new(Names.IncludeDeprecated, type: nonNullBooleanType) { - Name = Names.IncludeDeprecated, - Type = booleanType, DefaultValue = BooleanValueNode.False, RuntimeDefaultValue = false, }, @@ -68,10 +67,8 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c { Arguments = { - new() + new(Names.IncludeDeprecated, type: nonNullBooleanType) { - Name = Names.IncludeDeprecated, - Type = booleanType, DefaultValue = BooleanValueNode.False, RuntimeDefaultValue = false, }, diff --git a/src/HotChocolate/Core/test/Execution.Tests/Integration/StarWarsCodeFirst/__snapshots__/StarWarsCodeFirstTests.Ensure_Benchmark_Query_LargeQuery.snap b/src/HotChocolate/Core/test/Execution.Tests/Integration/StarWarsCodeFirst/__snapshots__/StarWarsCodeFirstTests.Ensure_Benchmark_Query_LargeQuery.snap index b1c1d918c35..82986a833f4 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Integration/StarWarsCodeFirst/__snapshots__/StarWarsCodeFirstTests.Ensure_Benchmark_Query_LargeQuery.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Integration/StarWarsCodeFirst/__snapshots__/StarWarsCodeFirstTests.Ensure_Benchmark_Query_LargeQuery.snap @@ -4986,9 +4986,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false" } @@ -5315,9 +5319,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false" } @@ -5648,9 +5656,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false" } @@ -5719,9 +5731,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false" } @@ -5750,9 +5766,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false" } diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.DefaultValueIsInputObject.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.DefaultValueIsInputObject.snap index 9124689ba1f..ee1a0f4e01c 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.DefaultValueIsInputObject.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.DefaultValueIsInputObject.snap @@ -71,9 +71,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -402,9 +406,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -737,9 +745,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -810,9 +822,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -843,9 +859,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.ExecuteGraphiQLIntrospectionQuery.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.ExecuteGraphiQLIntrospectionQuery.snap index c336eec6dcb..24cd217c0da 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.ExecuteGraphiQLIntrospectionQuery.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.ExecuteGraphiQLIntrospectionQuery.snap @@ -71,9 +71,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -402,9 +406,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -737,9 +745,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -810,9 +822,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -843,9 +859,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.ExecuteGraphiQLIntrospectionQuery_ToJson.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.ExecuteGraphiQLIntrospectionQuery_ToJson.snap index c336eec6dcb..24cd217c0da 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.ExecuteGraphiQLIntrospectionQuery_ToJson.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/IntrospectionTests.ExecuteGraphiQLIntrospectionQuery_ToJson.snap @@ -71,9 +71,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -402,9 +406,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -737,9 +745,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -810,9 +822,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -843,9 +859,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SchemaFirstTests.DescriptionsAreCorrectlyRead.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SchemaFirstTests.DescriptionsAreCorrectlyRead.snap index b64045b6cec..42fd24f7647 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SchemaFirstTests.DescriptionsAreCorrectlyRead.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SchemaFirstTests.DescriptionsAreCorrectlyRead.snap @@ -20,9 +20,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -374,9 +378,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -709,9 +717,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -742,9 +754,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -775,9 +791,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SchemaFirstTests.Interfaces_Impl_Interfaces_Are_Correctly_Exposed_Through_Introspection.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SchemaFirstTests.Interfaces_Impl_Interfaces_Are_Correctly_Exposed_Through_Introspection.snap index 8f864ae4652..c955585f0c0 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SchemaFirstTests.Interfaces_Impl_Interfaces_Are_Correctly_Exposed_Through_Introspection.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SchemaFirstTests.Interfaces_Impl_Interfaces_Are_Correctly_Exposed_Through_Introspection.snap @@ -71,9 +71,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -402,9 +406,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -737,9 +745,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -810,9 +822,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, @@ -843,9 +859,13 @@ "name": "includeDeprecated", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "defaultValue": "false", "isDeprecated": false, diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap index 8153fdad7a9..e0432b4565b 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap @@ -75,9 +75,13 @@ "Name": "includeDeprecated", "Description": null, "Type": { - "Kind": "Scalar", - "Name": "Boolean", - "OfType": null + "Kind": "NonNull", + "Name": null, + "OfType": { + "Kind": "Scalar", + "Name": "Boolean", + "OfType": null + } }, "DefaultValue": "false" } @@ -404,9 +408,13 @@ "Name": "includeDeprecated", "Description": null, "Type": { - "Kind": "Scalar", - "Name": "Boolean", - "OfType": null + "Kind": "NonNull", + "Name": null, + "OfType": { + "Kind": "Scalar", + "Name": "Boolean", + "OfType": null + } }, "DefaultValue": "false" } @@ -737,9 +745,13 @@ "Name": "includeDeprecated", "Description": null, "Type": { - "Kind": "Scalar", - "Name": "Boolean", - "OfType": null + "Kind": "NonNull", + "Name": null, + "OfType": { + "Kind": "Scalar", + "Name": "Boolean", + "OfType": null + } }, "DefaultValue": "false" } @@ -808,9 +820,13 @@ "Name": "includeDeprecated", "Description": null, "Type": { - "Kind": "Scalar", - "Name": "Boolean", - "OfType": null + "Kind": "NonNull", + "Name": null, + "OfType": { + "Kind": "Scalar", + "Name": "Boolean", + "OfType": null + } }, "DefaultValue": "false" } @@ -839,9 +855,13 @@ "Name": "includeDeprecated", "Description": null, "Type": { - "Kind": "Scalar", - "Name": "Boolean", - "OfType": null + "Kind": "NonNull", + "Name": null, + "OfType": { + "Kind": "Scalar", + "Name": "Boolean", + "OfType": null + } }, "DefaultValue": "false" } @@ -2685,15 +2705,26 @@ "Description": null, "Type": { "__typename": "__Type", - "Name": "Boolean", - "Kind": "Scalar", + "Name": null, + "Kind": "NonNull", "Description": null, "Fields": null, "InputFields": null, "Interfaces": null, "EnumValues": null, "PossibleTypes": null, - "OfType": null + "OfType": { + "__typename": "__Type", + "Name": "Boolean", + "Kind": "Scalar", + "Description": null, + "Fields": null, + "InputFields": null, + "Interfaces": null, + "EnumValues": null, + "PossibleTypes": null, + "OfType": null + } }, "DefaultValue": "false" } @@ -3206,15 +3237,26 @@ "Description": null, "Type": { "__typename": "__Type", - "Name": "Boolean", - "Kind": "Scalar", + "Name": null, + "Kind": "NonNull", "Description": null, "Fields": null, "InputFields": null, "Interfaces": null, "EnumValues": null, "PossibleTypes": null, - "OfType": null + "OfType": { + "__typename": "__Type", + "Name": "Boolean", + "Kind": "Scalar", + "Description": null, + "Fields": null, + "InputFields": null, + "Interfaces": null, + "EnumValues": null, + "PossibleTypes": null, + "OfType": null + } }, "DefaultValue": "false" } @@ -3820,15 +3862,26 @@ "Description": null, "Type": { "__typename": "__Type", - "Name": "Boolean", - "Kind": "Scalar", + "Name": null, + "Kind": "NonNull", "Description": null, "Fields": null, "InputFields": null, "Interfaces": null, "EnumValues": null, "PossibleTypes": null, - "OfType": null + "OfType": { + "__typename": "__Type", + "Name": "Boolean", + "Kind": "Scalar", + "Description": null, + "Fields": null, + "InputFields": null, + "Interfaces": null, + "EnumValues": null, + "PossibleTypes": null, + "OfType": null + } }, "DefaultValue": "false" } @@ -3965,15 +4018,26 @@ "Description": null, "Type": { "__typename": "__Type", - "Name": "Boolean", - "Kind": "Scalar", + "Name": null, + "Kind": "NonNull", "Description": null, "Fields": null, "InputFields": null, "Interfaces": null, "EnumValues": null, "PossibleTypes": null, - "OfType": null + "OfType": { + "__typename": "__Type", + "Name": "Boolean", + "Kind": "Scalar", + "Description": null, + "Fields": null, + "InputFields": null, + "Interfaces": null, + "EnumValues": null, + "PossibleTypes": null, + "OfType": null + } }, "DefaultValue": "false" } @@ -4026,15 +4090,26 @@ "Description": null, "Type": { "__typename": "__Type", - "Name": "Boolean", - "Kind": "Scalar", + "Name": null, + "Kind": "NonNull", "Description": null, "Fields": null, "InputFields": null, "Interfaces": null, "EnumValues": null, "PossibleTypes": null, - "OfType": null + "OfType": { + "__typename": "__Type", + "Name": "Boolean", + "Kind": "Scalar", + "Description": null, + "Fields": null, + "InputFields": null, + "Interfaces": null, + "EnumValues": null, + "PossibleTypes": null, + "OfType": null + } }, "DefaultValue": "false" } From da68fb359daef51a6b572d48839c192dcaeaa97d Mon Sep 17 00:00:00 2001 From: Glen Date: Thu, 13 Mar 2025 18:01:20 +0200 Subject: [PATCH 39/64] Updated Squadron packages (#8127) --- src/Directory.Packages.props | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 50b67393645..17aea4e353d 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -47,13 +47,13 @@ - - - - - - - + + + + + + + From 2b53375b022ba46c20212eac4d712a1b2ac4f8c1 Mon Sep 17 00:00:00 2001 From: stoyanovskydmitry <128645619+stoyanovskydmitry@users.noreply.github.com> Date: Thu, 13 Mar 2025 20:05:43 +0100 Subject: [PATCH 40/64] [OPA] Fix OPA middleware to comply with OPA V1 (#8084) Co-authored-by: Glen --- src/Directory.Packages.props | 2 +- ...hocolateAuthorizeRequestExecutorBuilder.cs | 36 ++++++- .../IOpaService.cs | 10 ++ .../OpaAuthorizationHandler.cs | 45 ++++---- .../OpaAuthorizationHandlerContext.cs | 25 +++++ .../OpaOptions.cs | 34 +++++- .../OpaService.cs | 27 ++--- .../QueryResponse.cs | 20 ++-- .../Request/DefaultQueryRequestFactory.cs | 47 +++++--- .../Request/IOpaQueryRequestFactory.cs | 13 ++- .../Request/IPAndPort.cs | 21 +++- .../Request/OpaQueryRequest.cs | 101 +++++++++++------- .../Request/OriginalRequest.cs | 67 +++++++----- .../Request/Policy.cs | 21 ++-- .../AuthorizationAttributeTestData.cs | 14 +-- .../AuthorizationTestData.cs | 24 +++-- .../AuthorizationTests.cs | 58 ++++++++-- .../Claims.cs | 6 ++ .../HasAgeDefinedResponse.cs | 4 + ....AspNetCore.Authorization.Opa.Tests.csproj | 10 +- .../OpaProcess/OpaProcess.cs | 36 +++++++ .../Policies/has_age_defined.rego | 18 +++- .../AuthorizationTests.Policy_Authorized.snap | 2 +- ...ests.Policy_Authorized_WithExtensions.snap | 9 ++ ...thorizationTests.Policy_NotAuthorized.snap | 2 +- .../AuthorizationTests.Policy_NotFound.snap | 6 +- 26 files changed, 465 insertions(+), 193 deletions(-) create mode 100644 src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandlerContext.cs create mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/OpaProcess/OpaProcess.cs create mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized_WithExtensions.snap diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 17aea4e353d..dc52ca2ef50 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -39,7 +39,6 @@ - @@ -59,6 +58,7 @@ + diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Extensions/HotChocolateAuthorizeRequestExecutorBuilder.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Extensions/HotChocolateAuthorizeRequestExecutorBuilder.cs index a802bb5e6a2..7bdd2ecfdc4 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Extensions/HotChocolateAuthorizeRequestExecutorBuilder.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Extensions/HotChocolateAuthorizeRequestExecutorBuilder.cs @@ -1,8 +1,8 @@ using HotChocolate.AspNetCore.Authorization; using HotChocolate.Execution.Configuration; -using Microsoft.Extensions.Configuration; using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection; @@ -42,7 +42,7 @@ public static IRequestExecutorBuilder AddOpaAuthorization( var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; jsonOptions.Converters.Add( new JsonStringEnumConverter( @@ -55,6 +55,15 @@ public static IRequestExecutorBuilder AddOpaAuthorization( return builder; } + /// + /// Adds result handler to the OPA options. + /// + /// Instance of . + /// The path to the policy. + /// The PDP decision result. + /// + /// Returns the for chaining in more configurations. + /// public static IRequestExecutorBuilder AddOpaResultHandler( this IRequestExecutorBuilder builder, string policyPath, @@ -66,4 +75,27 @@ public static IRequestExecutorBuilder AddOpaResultHandler( (o, _) => o.PolicyResultHandlers.Add(policyPath, parseResult)); return builder; } + + /// + /// Adds OPA query request extensions handler to the OPA options. + /// + /// Instance of . + /// The path to the policy. + /// The handler for the extensions associated with the Policy. + /// + /// + /// Returns the for chaining in more configurations. + /// + public static IRequestExecutorBuilder AddOpaQueryRequestExtensionsHandler( + this IRequestExecutorBuilder builder, + string policyPath, + OpaQueryRequestExtensionsHandler opaQueryRequestExtensionsHandler) + { + builder.Services + .AddOptions() + .Configure( + (o, _) => o.OpaQueryRequestExtensionsHandlers.Add(policyPath, opaQueryRequestExtensionsHandler)); + return builder; + } + } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/IOpaService.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/IOpaService.cs index 4218692373b..9840c7af544 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/IOpaService.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/IOpaService.cs @@ -1,7 +1,17 @@ namespace HotChocolate.AspNetCore.Authorization; +/// +/// The OPA service interface communicating with OPA server. +/// public interface IOpaService { + /// + /// The method used to query OPA PDP decision based on the request input. + /// + /// The string parameter representing path of the evaluating policy. + /// The instance . + /// Cancellation token. + /// Task QueryAsync( string policyPath, OpaQueryRequest request, diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandler.cs index d2b7c17c779..6ca37a71ed4 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandler.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandler.cs @@ -19,43 +19,36 @@ public OpaAuthorizationHandler( IOpaQueryRequestFactory requestFactory, IOptions options) { - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } + ArgumentNullException.ThrowIfNull(options); _opaService = opaService ?? throw new ArgumentNullException(nameof(opaService)); _requestFactory = requestFactory ?? throw new ArgumentNullException(nameof(requestFactory)); _options = options.Value; } - /// - /// Authorize current directive using OPA (Open Policy Agent). - /// - /// The current middleware context. - /// The authorization directive. - /// The cancellation token. - /// - /// Returns a value indicating if the current session is authorized to - /// access the resolver data. - /// + /// public async ValueTask AuthorizeAsync( IMiddlewareContext context, AuthorizeDirective directive, - CancellationToken ct) + CancellationToken cancellationToken = default) { - var authorizationContext = new AuthorizationContext( - context.Schema, - context.Services, - context.ContextData, - context.Operation.Document, - context.Operation.Id); - return await AuthorizeAsync(authorizationContext, directive, ct).ConfigureAwait(false); + return await AuthorizeAsync( + new OpaAuthorizationHandlerContext(context), [directive], cancellationToken).ConfigureAwait(false); } + /// public async ValueTask AuthorizeAsync( AuthorizationContext context, IReadOnlyList directives, + CancellationToken cancellationToken = default) + { + return await AuthorizeAsync( + new OpaAuthorizationHandlerContext(context), directives, cancellationToken).ConfigureAwait(false); + } + + private async ValueTask AuthorizeAsync( + OpaAuthorizationHandlerContext context, + IReadOnlyList directives, CancellationToken ct) { if (directives.Count == 1) @@ -89,12 +82,12 @@ public async ValueTask AuthorizeAsync( return AuthorizeResult.Allowed; static async Task ExecuteAsync( - AuthorizationContext context, + OpaAuthorizationHandlerContext context, IEnumerator partition, Authorize authorize, CancellationToken ct) { - while (partition.MoveNext() && partition.Current is not null) + while (partition.MoveNext()) { var directive = partition.Current; var result = await authorize(context, directive, ct).ConfigureAwait(false); @@ -110,7 +103,7 @@ static async Task ExecuteAsync( } private async ValueTask AuthorizeAsync( - AuthorizationContext context, + OpaAuthorizationHandlerContext context, AuthorizeDirective directive, CancellationToken ct) { @@ -122,7 +115,7 @@ private async ValueTask AuthorizeAsync( } private delegate ValueTask Authorize( - AuthorizationContext context, + OpaAuthorizationHandlerContext context, AuthorizeDirective directive, CancellationToken ct); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandlerContext.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandlerContext.cs new file mode 100644 index 00000000000..59811e92efb --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandlerContext.cs @@ -0,0 +1,25 @@ +namespace HotChocolate.AspNetCore.Authorization; + +/// +/// The OPA authorization handler context. +/// +public class OpaAuthorizationHandlerContext +{ + /// + /// The constructor. + /// + /// Either IMiddlewareContext or AuthorizationContext depending on the phase of + /// a rule execution. + /// + public OpaAuthorizationHandlerContext(object resource) + { + ArgumentNullException.ThrowIfNull(resource); + + Resource = resource; + } + + /// + /// The object representing instance of either IMiddlewareContext or AuthorizationContext. + /// + public object Resource { get; } +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaOptions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaOptions.cs index 955487008f6..a0b8af88034 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaOptions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaOptions.cs @@ -5,6 +5,9 @@ namespace HotChocolate.AspNetCore.Authorization; +/// +/// The class representing OPA configuration options. +/// public sealed class OpaOptions { private readonly ConcurrentDictionary _handlerKeysRegexes = new(); @@ -17,14 +20,34 @@ public sealed class OpaOptions public Dictionary PolicyResultHandlers { get; } = new(); + public Dictionary OpaQueryRequestExtensionsHandlers { get; } = new(); + + public OpaQueryRequestExtensionsHandler? GetOpaQueryRequestExtensionsHandler(string policyPath) + { + if (OpaQueryRequestExtensionsHandlers.Count == 0) + { + return null; + } + return OpaQueryRequestExtensionsHandlers.TryGetValue(policyPath, out var handler) + ? handler : + FindHandler(policyPath, OpaQueryRequestExtensionsHandlers); + } + public ParseResult GetPolicyResultParser(string policyPath) { if (PolicyResultHandlers.TryGetValue(policyPath, out var handler)) { return handler; } + handler = FindHandler(policyPath, PolicyResultHandlers); + return handler ?? + throw new InvalidOperationException( + $"No result handler found for policy: {policyPath}"); + } - var maybeHandler = PolicyResultHandlers.SingleOrDefault( + private THandler? FindHandler(string policyPath, Dictionary handlers) + { + var maybeHandler = handlers.SingleOrDefault( k => { var regex = _handlerKeysRegexes.GetOrAdd( @@ -33,14 +56,15 @@ public ParseResult GetPolicyResultParser(string policyPath) k.Key, RegexOptions.Compiled | RegexOptions.Singleline | - RegexOptions.CultureInvariant)); + RegexOptions.CultureInvariant, + TimeSpan.FromMilliseconds(500))); return regex.IsMatch(policyPath); }); - return maybeHandler.Value ?? - throw new InvalidOperationException( - $"No result handler found for policy: {policyPath}"); + return maybeHandler.Value; } } public delegate AuthorizeResult ParseResult(OpaQueryResponse response); + +public delegate object? OpaQueryRequestExtensionsHandler(OpaAuthorizationHandlerContext context); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaService.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaService.cs index 578763b186d..7576f3e1c67 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaService.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaService.cs @@ -11,36 +11,29 @@ internal sealed class OpaService : IOpaService public OpaService(HttpClient httpClient, IOptions options) { - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } + ArgumentNullException.ThrowIfNull(options); - _client = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + ArgumentNullException.ThrowIfNull(httpClient); + + _client = httpClient; _options = options.Value; } public async Task QueryAsync( string policyPath, OpaQueryRequest request, - CancellationToken ct) + CancellationToken cancellationToken = default) { - if (policyPath is null) - { - throw new ArgumentNullException(nameof(policyPath)); - } + ArgumentNullException.ThrowIfNull(policyPath); - if (request is null) - { - throw new ArgumentNullException(nameof(request)); - } + ArgumentNullException.ThrowIfNull(request); using var body = JsonContent.Create(request, options: _options.JsonSerializerOptions); - using var response = await _client.PostAsync(policyPath, body, ct).ConfigureAwait(false); + using var response = await _client.PostAsync(policyPath, body, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false); - var document = await JsonDocument.ParseAsync(stream, default, ct); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + var document = await JsonDocument.ParseAsync(stream, default, cancellationToken); return new OpaQueryResponse(document); } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/QueryResponse.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/QueryResponse.cs index 18f2a00e917..2c770dbf897 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/QueryResponse.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/QueryResponse.cs @@ -2,24 +2,20 @@ namespace HotChocolate.AspNetCore.Authorization; -public sealed class OpaQueryResponse : IDisposable +/// +/// The class representing OPA query response. +/// +public sealed class OpaQueryResponse(JsonDocument document) : IDisposable { - private readonly JsonDocument _document; - private readonly JsonElement _root; - - public OpaQueryResponse(JsonDocument document) - { - _document = document; - _root = document.RootElement; - } + private readonly JsonElement _root = document.RootElement; public Guid? DecisionId - => _root.TryGetProperty("decisionId", out var value) + => _root.TryGetProperty("decision_id", out var value) ? value.GetGuid() : null; public T? GetResult() - => _root.TryGetProperty("decisionId", out var value) + => _root.TryGetProperty("result", out var value) ? value.Deserialize() : default; @@ -28,5 +24,5 @@ public bool IsEmpty _root.EnumerateObject().Any(); public void Dispose() - => _document.Dispose(); + => document.Dispose(); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/DefaultQueryRequestFactory.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/DefaultQueryRequestFactory.cs index 77e6b349659..1decfec9c1f 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/DefaultQueryRequestFactory.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/DefaultQueryRequestFactory.cs @@ -1,23 +1,39 @@ using HotChocolate.Authorization; +using HotChocolate.Resolvers; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; namespace HotChocolate.AspNetCore.Authorization; -public sealed class DefaultQueryRequestFactory : IOpaQueryRequestFactory +/// +/// Default implementation of . +/// +internal sealed class DefaultQueryRequestFactory : IOpaQueryRequestFactory { - public OpaQueryRequest CreateRequest(AuthorizationContext context, AuthorizeDirective directive) + private readonly OpaOptions _options; + + public DefaultQueryRequestFactory(IOptions options) { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } + ArgumentNullException.ThrowIfNull(options); - if (directive is null) - { - throw new ArgumentNullException(nameof(directive)); - } + _options = options.Value; + } - var httpContext = (HttpContext)context.ContextData[nameof(HttpContext)]!; + /// + public OpaQueryRequest CreateRequest(OpaAuthorizationHandlerContext context, AuthorizeDirective directive) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(directive); + + var httpContext = + context.Resource switch + { + IMiddlewareContext middlewareContext => + (HttpContext)middlewareContext.ContextData[nameof(HttpContext)]!, + AuthorizationContext authorizationContext => + (HttpContext)authorizationContext.ContextData[nameof(HttpContext)]!, + _ => throw new ArgumentException("Invalid context data.") + }; var connection = httpContext.Connection; var policy = new Policy( @@ -40,6 +56,13 @@ public OpaQueryRequest CreateRequest(AuthorizationContext context, AuthorizeDire connection.LocalIpAddress!.ToString(), connection.LocalPort); - return new OpaQueryRequest(policy, originalRequest, source, destination); + object? extensions = null; + if (directive.Policy is not null && + _options.GetOpaQueryRequestExtensionsHandler(directive.Policy) is { } extensionsHandler) + { + extensions = extensionsHandler(context); + } + + return new OpaQueryRequest(policy, originalRequest, source, destination, extensions); } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IOpaQueryRequestFactory.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IOpaQueryRequestFactory.cs index 92a35f2c95c..0ed248eb250 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IOpaQueryRequestFactory.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IOpaQueryRequestFactory.cs @@ -2,9 +2,20 @@ namespace HotChocolate.AspNetCore.Authorization; +/// +/// The OPA query request factory interface. +/// public interface IOpaQueryRequestFactory { + /// + /// Creates . + /// + /// The OPA authorization handler context. + /// Depending on the query execution phase the context's Resource is different + /// see for details. + /// + /// The OPA authorization directive. See . OpaQueryRequest CreateRequest( - AuthorizationContext context, + OpaAuthorizationHandlerContext context, AuthorizeDirective directive); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IPAndPort.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IPAndPort.cs index 55cfd1f9611..d19ea2c1f04 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IPAndPort.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IPAndPort.cs @@ -1,21 +1,34 @@ namespace HotChocolate.AspNetCore.Authorization; // ReSharper disable once InconsistentNaming +/// +/// A structure to store information about IP address and port in OPA query request input. +/// public sealed class IPAndPort { + /// + /// Public constructor. + /// + /// IP address. + /// Port number. + /// Thrown if port values is out of range: [0:65535]. public IPAndPort(string ipAddress, int port = 0) { - if (port <= 0) - { - throw new ArgumentOutOfRangeException(nameof(port)); - } + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(port); + ArgumentOutOfRangeException.ThrowIfGreaterThan(port, (1 << 16) - 1); IPAddress = ipAddress ?? throw new ArgumentNullException(nameof(ipAddress)); Port = port; } // ReSharper disable once InconsistentNaming + /// + /// IP address string. + /// public string IPAddress { get; } + /// + /// Port value. + /// public int Port { get; } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs index 579af63a72a..2108e2920d9 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs @@ -1,7 +1,22 @@ namespace HotChocolate.AspNetCore.Authorization; +/// +/// A structure representing OPA query request input. +/// public sealed class OpaQueryRequest { + /// + /// The constructor. + /// + /// The instance of . + /// The instance of . + /// Stores information about the original GraphQl request. + /// The instance . + /// Stores information about the source address of the original request + /// The instance . + /// Stores information about the destination address of the original request. + /// The instance of object that provides extended information for the OPA query request. + /// Usually is represented as a dictionary. public OpaQueryRequest( Policy policy, OriginalRequest request, @@ -9,55 +24,63 @@ public OpaQueryRequest( IPAndPort destination, object? extensions = null) { - if (policy is null) - { - throw new ArgumentNullException(nameof(policy)); - } - - if (request is null) - { - throw new ArgumentNullException(nameof(request)); - } - - if (source is null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (destination is null) - { - throw new ArgumentNullException(nameof(destination)); - } + ArgumentNullException.ThrowIfNull(policy); + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(destination); Input = new OpaQueryRequestInput(policy, request, source, destination, extensions); } + /// + /// The property to get the instance of . + /// public OpaQueryRequestInput Input { get; } - public sealed class OpaQueryRequestInput + /// + /// The class representing an input that will be sent to the OPA server. + /// + /// The instance of . + /// The instance of . + /// Stores information about the original GraphQl request. + /// The instance . + /// Stores information about the source address of the original request + /// The instance . + /// Stores information about the destination address of the original request. + /// The instance of object the provides extended information for the OPA query request. + /// Usually is represented as a dictionary. + public sealed class OpaQueryRequestInput( + Policy policy, + OriginalRequest request, + IPAndPort source, + IPAndPort destination, + object? extensions) { - public OpaQueryRequestInput( - Policy policy, - OriginalRequest request, - IPAndPort source, - IPAndPort destination, - object? extensions) - { - Policy = policy; - Request = request; - Source = source; - Destination = destination; - Extensions = extensions; - } - - public Policy Policy { get; } + /// + /// The property to get instance of . + /// + public Policy Policy { get; } = policy; - public OriginalRequest Request { get; } + /// + /// The property to get instance of . + /// + public OriginalRequest Request { get; } = request; - public IPAndPort Source { get; } + /// + /// The property to get instance of representing + /// the original request source IP address and port. + /// + public IPAndPort Source { get; } = source; - public IPAndPort Destination { get; } + /// + /// The property to get instance of representing + /// the original request destination IP address and port. + /// + public IPAndPort Destination { get; } = destination; - public object? Extensions { get; } + /// + /// The property to get instance of object representing OPA query input extension data. + /// + public object? Extensions { get; } = extensions; } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OriginalRequest.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OriginalRequest.cs index 5d31d0c67e4..f86f96ebf0f 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OriginalRequest.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OriginalRequest.cs @@ -3,33 +3,44 @@ namespace HotChocolate.AspNetCore.Authorization; -public sealed class OriginalRequest +/// +/// The class representing the information about the original GraphQl request. +/// +public sealed class OriginalRequest( + IHeaderDictionary headers, + string host, + string method, + string path, + IEnumerable>? query, + string scheme) { - public OriginalRequest( - IHeaderDictionary headers, - string host, - string method, - string path, - IEnumerable>? query, - string scheme) - { - Headers = headers ?? throw new ArgumentNullException(nameof(headers)); - Host = host ?? throw new ArgumentNullException(nameof(host)); - Method = method ?? throw new ArgumentNullException(nameof(method)); - Path = path ?? throw new ArgumentNullException(nameof(path)); - Query = query; - Scheme = scheme ?? throw new ArgumentNullException(nameof(scheme)); - } - - public IHeaderDictionary Headers { get; } - - public string Host { get; } - - public string Method { get; } - - public string Path { get; } - - public IEnumerable>? Query { get; } - - public string Scheme { get; } + /// + /// Original request headers. + /// + public IHeaderDictionary Headers { get; } = headers ?? throw new ArgumentNullException(nameof(headers)); + + /// + /// Information about the host sent request. + /// + public string Host { get; } = host ?? throw new ArgumentNullException(nameof(host)); + + /// + /// The HTTP request method. + /// + public string Method { get; } = method ?? throw new ArgumentNullException(nameof(method)); + + /// + /// Path of the request. + /// + public string Path { get; } = path ?? throw new ArgumentNullException(nameof(path)); + + /// + /// The query of the request. + /// + public IEnumerable>? Query { get; } = query; + + /// + /// GraphQl schema of the request. + /// + public string Scheme { get; } = scheme ?? throw new ArgumentNullException(nameof(scheme)); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/Policy.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/Policy.cs index 3f88bd1fc85..0c7e689252d 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/Policy.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/Policy.cs @@ -1,14 +1,17 @@ namespace HotChocolate.AspNetCore.Authorization; -public sealed class Policy +/// +/// The structure representing information about an OPA policy to be evaluated by the OPA server. +/// +public sealed class Policy(string path, IReadOnlyList roles) { - public Policy(string path, IReadOnlyList roles) - { - Path = path ?? throw new ArgumentNullException(nameof(path)); - Roles = roles ?? throw new ArgumentNullException(nameof(roles)); - } + /// + /// Path of the policy. Contains the path string appended to the OPA base address. + /// + public string Path { get; } = path ?? throw new ArgumentNullException(nameof(path)); - public string Path { get; } - - public IReadOnlyList Roles { get; } + /// + /// Roles associated with the user to evaluate by the policy rule. + /// + public IReadOnlyList Roles { get; } = roles ?? throw new ArgumentNullException(nameof(roles)); } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationAttributeTestData.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationAttributeTestData.cs index 9f9317f971e..d621181de44 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationAttributeTestData.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationAttributeTestData.cs @@ -13,7 +13,7 @@ public class Query public string GetDefault() => "foo"; [Authorize(Policy = Policies.HasDefinedAge)] - public string GetAge() => "foo"; + public string? GetAge() => "foo"; [Authorize(Roles = ["a",])] public string GetRoles() => "foo"; @@ -40,11 +40,13 @@ private Action CreateSchema() => }) .AddOpaResultHandler( Policies.HasDefinedAge, - response => response.GetResult() switch - { - { Allow: true, } => AuthorizeResult.Allowed, - _ => AuthorizeResult.NotAllowed, - }); + response => response.DecisionId is null + ? AuthorizeResult.NotAllowed + : response.GetResult() switch + { + { Allow: true, } => AuthorizeResult.Allowed, + _ => AuthorizeResult.NotAllowed, + }); public IEnumerator GetEnumerator() { diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTestData.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTestData.cs index d762e9ada9c..04e37b255ce 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTestData.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTestData.cs @@ -37,11 +37,13 @@ private Action CreateSchema() => }) .AddOpaResultHandler( Policies.HasDefinedAge, - response => response.GetResult() switch - { - { Allow: true, } => AuthorizeResult.Allowed, - _ => AuthorizeResult.NotAllowed, - }) + response => response.DecisionId is null + ? AuthorizeResult.NotAllowed + : response.GetResult() switch + { + { Allow: true, } => AuthorizeResult.Allowed, + _ => AuthorizeResult.NotAllowed, + }) .UseField(_schemaMiddleware); private Action CreateSchemaWithBuilder() => @@ -54,11 +56,13 @@ private Action CreateSchemaWithBuilder() => }) .AddOpaResultHandler( Policies.HasDefinedAge, - response => response.GetResult() switch - { - { Allow: true, } => AuthorizeResult.Allowed, - _ => AuthorizeResult.NotAllowed, - }) + response => response.DecisionId is null + ? AuthorizeResult.NotAllowed + : response.GetResult() switch + { + { Allow: true, } => AuthorizeResult.Allowed, + _ => AuthorizeResult.NotAllowed, + }) .UseField(_schemaMiddleware); public IEnumerator GetEnumerator() diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs index 86a58554872..d9a3568ba20 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs @@ -1,17 +1,18 @@ using System.Net; using HotChocolate.AspNetCore.Tests.Utilities; +using HotChocolate.Authorization; using HotChocolate.Execution.Configuration; +using HotChocolate.Resolvers; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; -using Opa.Native; namespace HotChocolate.AspNetCore.Authorization; public class AuthorizationTests : ServerTestBase, IAsyncLifetime { - private OpaHandle? _opaHandle; + private OpaProcess? _opaHandle; public AuthorizationTests(TestServerFactory serverFactory) : base(serverFactory) @@ -29,7 +30,7 @@ private static void SetUpHttpContext(HttpContext context) public async Task InitializeAsync() => _opaHandle = await OpaProcess.StartServerAsync(); - [Theory(Skip = "The local server needs to be packaged with squadron")] + [Theory] [ClassData(typeof(AuthorizationTestData))] [ClassData(typeof(AuthorizationAttributeTestData))] public async Task Policy_NotFound(Action configure) @@ -51,7 +52,7 @@ public async Task Policy_NotFound(Action configure) result.MatchSnapshot(); } - [Theory(Skip = "The local server needs to be packaged with squadron")] + [Theory] [ClassData(typeof(AuthorizationTestData))] [ClassData(typeof(AuthorizationAttributeTestData))] public async Task Policy_NotAuthorized(Action configure) @@ -70,7 +71,7 @@ public async Task Policy_NotAuthorized(Action configure "iwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; })); - var hasAgeDefinedPolicy = await File.ReadAllTextAsync("policies/has_age_defined.rego"); + var hasAgeDefinedPolicy = await File.ReadAllTextAsync("Policies/has_age_defined.rego"); using var client = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:8181"), }; var putPolicyResponse = await client.PutAsync( @@ -86,7 +87,7 @@ public async Task Policy_NotAuthorized(Action configure result.MatchSnapshot(); } - [Theory(Skip = "The local server needs to be packaged with squadron")] + [Theory] [ClassData(typeof(AuthorizationTestData))] [ClassData(typeof(AuthorizationAttributeTestData))] public async Task Policy_Authorized(Action configure) @@ -106,7 +107,50 @@ public async Task Policy_Authorized(Action configure) "jXBglnxac"; })); - var hasAgeDefinedPolicy = await File.ReadAllTextAsync("policies/has_age_defined.rego"); + var hasAgeDefinedPolicy = await File.ReadAllTextAsync("Policies/has_age_defined.rego"); + using var client = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:8181"), }; + + var putPolicyResponse = await client.PutAsync( + "/v1/policies/has_age_defined", + new StringContent(hasAgeDefinedPolicy)); + putPolicyResponse.EnsureSuccessStatusCode(); + + // act + var result = await server.PostAsync(new ClientQueryRequest { Query = "{ age }", }); + + // assert + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + result.MatchSnapshot(); + } + + [Theory] + [ClassData(typeof(AuthorizationTestData))] + [ClassData(typeof(AuthorizationAttributeTestData))] + public async Task Policy_Authorized_WithExtensions(Action configure) + { + // arrange + var server = CreateTestServer( + builder => + { + configure(builder); + builder.Services.AddAuthorization(); + builder.AddOpaQueryRequestExtensionsHandler(Policies.HasDefinedAge, + context => context.Resource is IMiddlewareContext or AuthorizationContext + ? new Dictionary { { "secret", "secret" } } + : null); + }, + SetUpHttpContext + (Action)(c => + { + // The token is the same but swapped alg and typ, + // as a result Base64 representation is not the one as expected by Rego rule + // See policies/has_age_defined.rego file for details + c.Request.Headers["Authorization"] = + "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." + + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJiaXJ0aGRhdGUiOiIxNy0x" + + "MS0yMDAwIn0.01Hb6X-HXl9ASf3X82Mt63RMpZ4SVJZT9hTI2dYet-k"; + })); + + var hasAgeDefinedPolicy = await File.ReadAllTextAsync("Policies/has_age_defined.rego"); using var client = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:8181"), }; var putPolicyResponse = await client.PutAsync( diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Claims.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Claims.cs index 585c4d11d33..d667b63e708 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Claims.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Claims.cs @@ -1,12 +1,18 @@ +using System.Text.Json.Serialization; + namespace HotChocolate.AspNetCore.Authorization; public class Claims { + [JsonPropertyName("birthdate")] public string Birthdate { get; set; } = default!; + [JsonPropertyName("iat")] public long Iat { get; set; } + [JsonPropertyName("name")] public string Name { get; set; } = default!; + [JsonPropertyName("sub")] public string Sub { get; set; } = default!; } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HasAgeDefinedResponse.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HasAgeDefinedResponse.cs index 1f26c7b92a0..45bf4ee62b8 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HasAgeDefinedResponse.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HasAgeDefinedResponse.cs @@ -1,8 +1,12 @@ +using System.Text.Json.Serialization; + namespace HotChocolate.AspNetCore.Authorization; public class HasAgeDefinedResponse { + [JsonPropertyName("allow")] public bool Allow { get; set; } = default!; + [JsonPropertyName("claims")] public Claims Claims { get; set; } = default!; } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HotChocolate.AspNetCore.Authorization.Opa.Tests.csproj b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HotChocolate.AspNetCore.Authorization.Opa.Tests.csproj index 6d2e1e41a02..02a418b5774 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HotChocolate.AspNetCore.Authorization.Opa.Tests.csproj +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HotChocolate.AspNetCore.Authorization.Opa.Tests.csproj @@ -13,11 +13,7 @@ - - - - - + Always @@ -26,4 +22,8 @@ + + + + diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/OpaProcess/OpaProcess.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/OpaProcess/OpaProcess.cs new file mode 100644 index 00000000000..ab301dcba34 --- /dev/null +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/OpaProcess/OpaProcess.cs @@ -0,0 +1,36 @@ +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; + +namespace HotChocolate.AspNetCore.Authorization; + +public class OpaProcess +{ + private readonly IContainer _container; + + public OpaProcess(IContainer container) + { + _container = container; + } + public static async Task StartServerAsync() + { + var opaProcess = new OpaProcess(new ContainerBuilder() + .WithImage("openpolicyagent/opa") + .WithPortBinding(8181, 8181) + .WithCommand( + "run", "--server", + "--addr", ":8181", + "--log-level", "debug", + "--set", "decision_logs.console=true") + // Wait until the HTTP endpoint of the container is available. + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(8181))) + // Build the container configuration. + .Build()); + await opaProcess._container.StartAsync(); + return opaProcess; + } + + public async Task DisposeAsync() + { + await _container.DisposeAsync(); + } +} diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Policies/has_age_defined.rego b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Policies/has_age_defined.rego index 70e0f0e5319..82119d20d7c 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Policies/has_age_defined.rego +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Policies/has_age_defined.rego @@ -7,14 +7,24 @@ import input.request default allow = { "allow" : false } -valid_jwt = [is_valid, claims] { - token := replace(request.headers["Authorization"], "Bearer ", "") +valid_jwt := [is_valid, claims] if { + token := replace(request.headers["Authorization"][0], "Bearer ", "") claims := io.jwt.decode(token)[1] - is_valid := startswith(token, "eyJhbG") # a toy validation + + exts := object.get(input, "extensions", {}) + secret := object.get(exts, "secret", "") + + is_valid := is_valid_token_or_secret(token, secret) is_valid } -allow = {"allow": is_valid, "claims": claims } { +is_valid_token_or_secret(token, secret) if { + # a toy validation + checks := { startswith(token, "eyJhbG"), secret == "secret" } # imitate OR + checks[true] +} + +allow := {"allow": is_valid, "claims": claims } if { [is_valid, claims] := valid_jwt claims.birthdate } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized.snap index 3d4b82771e9..f1bcfb78bf9 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized.snap @@ -1,5 +1,5 @@ { - "ContentType": "application/json; charset=utf-8", + "ContentType": "application/graphql-response+json; charset=utf-8", "StatusCode": "OK", "Data": { "age": "foo" diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized_WithExtensions.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized_WithExtensions.snap new file mode 100644 index 00000000000..f1bcfb78bf9 --- /dev/null +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized_WithExtensions.snap @@ -0,0 +1,9 @@ +{ + "ContentType": "application/graphql-response+json; charset=utf-8", + "StatusCode": "OK", + "Data": { + "age": "foo" + }, + "Errors": null, + "Extensions": null +} diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotAuthorized.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotAuthorized.snap index 39a2261460f..7290ee0650c 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotAuthorized.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotAuthorized.snap @@ -1,5 +1,5 @@ { - "ContentType": "application/json; charset=utf-8", + "ContentType": "application/graphql-response+json; charset=utf-8", "StatusCode": "OK", "Data": { "age": null diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotFound.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotFound.snap index 7fb288f4848..7290ee0650c 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotFound.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotFound.snap @@ -1,12 +1,12 @@ { - "ContentType": "application/json; charset=utf-8", + "ContentType": "application/graphql-response+json; charset=utf-8", "StatusCode": "OK", "Data": { "age": null }, "Errors": [ { - "message": "The `graphql/authz/has_age_defined/allow` authorization policy does not exist.", + "message": "The current user is not authorized to access this resource.", "locations": [ { "line": 1, @@ -17,7 +17,7 @@ "age" ], "extensions": { - "code": "AUTH_POLICY_NOT_FOUND" + "code": "AUTH_NOT_AUTHORIZED" } } ], From 775481cb5ac375884071b6a433364e618aa32eba Mon Sep 17 00:00:00 2001 From: Tobias Tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Fri, 14 Mar 2025 21:46:26 +0100 Subject: [PATCH 41/64] [Fusion] @semanticNonNull support (#7894) --- .../Types/SemanticNonNullTypeInterceptor.cs | 11 +- .../Core/src/Types/Utilities/ErrorHelper.cs | 9 + .../FusionRequestExecutorBuilderExtensions.cs | 1 + .../src/Core/Execution/ExecutionUtils.cs | 159 +- .../Pipeline/SemanticNonNullOptimizer.cs | 50 + .../test/Core.Tests/SemanticNonNullTests.cs | 2971 +++++++++++++++++ .../Fusion/test/Shared/TestSubgraph.cs | 14 +- 7 files changed, 3165 insertions(+), 50 deletions(-) create mode 100644 src/HotChocolate/Fusion/src/Core/Execution/Pipeline/SemanticNonNullOptimizer.cs create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/SemanticNonNullTests.cs diff --git a/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs index fd30c666cf4..01263751036 100644 --- a/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs @@ -8,8 +8,8 @@ using HotChocolate.Types.Descriptors; using HotChocolate.Types.Descriptors.Definitions; using HotChocolate.Types.Helpers; -using HotChocolate.Types.Pagination; using HotChocolate.Types.Relay; +using HotChocolate.Utilities; namespace HotChocolate; @@ -346,7 +346,7 @@ private static void CheckResultForSemanticNonNullViolations(object? result, IRes { if (result is null && levels.Contains(currentLevel)) { - context.ReportError(CreateSemanticNonNullViolationError(path)); + context.ReportError(ErrorHelper.CreateSemanticNonNullViolationError(path, context.Selection)); return; } @@ -367,11 +367,4 @@ private static void CheckResultForSemanticNonNullViolations(object? result, IRes } } } - - private static IError CreateSemanticNonNullViolationError(Path path) - => ErrorBuilder.New() - .SetMessage("Cannot return null for semantic non-null field.") - .SetCode(ErrorCodes.Execution.SemanticNonNullViolation) - .SetPath(path) - .Build(); } diff --git a/src/HotChocolate/Core/src/Types/Utilities/ErrorHelper.cs b/src/HotChocolate/Core/src/Types/Utilities/ErrorHelper.cs index 04479e33ece..41838dd38e2 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/ErrorHelper.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/ErrorHelper.cs @@ -1,4 +1,5 @@ using System.Globalization; +using HotChocolate.Execution.Processing; using HotChocolate.Language; using HotChocolate.Properties; using HotChocolate.Types; @@ -538,4 +539,12 @@ public static ISchemaError DuplicateFieldName( .SetTypeSystemObject(type) .Build(); } + + public static IError CreateSemanticNonNullViolationError(Path path, ISelection selection) + => ErrorBuilder.New() + .SetMessage("Cannot return null for semantic non-null field.") + .SetCode(ErrorCodes.Execution.SemanticNonNullViolation) + .AddLocation(selection.SyntaxNode) + .SetPath(path) + .Build(); } diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs index 7deb82e82c6..cc73fa4c759 100644 --- a/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs +++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs @@ -62,6 +62,7 @@ public static FusionGatewayBuilder AddFusionGatewayServer( .UseField(next => next) .AddOperationCompilerOptimizer() .AddOperationCompilerOptimizer() + .AddOperationCompilerOptimizer() .AddConvention(_ => new DefaultNamingConventions()) .ModifyCostOptions(o => o.ApplyCostDefaults = false) .Configure( diff --git a/src/HotChocolate/Fusion/src/Core/Execution/ExecutionUtils.cs b/src/HotChocolate/Fusion/src/Core/Execution/ExecutionUtils.cs index 3d41997e3d9..987c5a9b78f 100644 --- a/src/HotChocolate/Fusion/src/Core/Execution/ExecutionUtils.cs +++ b/src/HotChocolate/Fusion/src/Core/Execution/ExecutionUtils.cs @@ -4,6 +4,7 @@ using System.Text.Json; using HotChocolate.Execution.Processing; using HotChocolate.Fusion.Execution.Nodes; +using HotChocolate.Fusion.Execution.Pipeline; using HotChocolate.Fusion.Metadata; using HotChocolate.Fusion.Planning; using HotChocolate.Fusion.Utilities; @@ -12,6 +13,7 @@ using HotChocolate.Types; using HotChocolate.Utilities; using static HotChocolate.Execution.Processing.Selection; +using ErrorHelper = HotChocolate.Utilities.ErrorHelper; using IType = HotChocolate.Types.IType; using ObjectType = HotChocolate.Types.ObjectType; @@ -41,7 +43,8 @@ private static void ComposeResult( SelectionSet selectionSet, SelectionData[] selectionSetData, ObjectResult selectionSetResult, - bool partialResult = false) + bool partialResult = false, + int level = 0) { if (selectionSetResult.IsInvalidated) { @@ -69,14 +72,19 @@ private static void ComposeResult( if (!field.IsIntrospectionField) { - var nullable = selection.TypeKind is not TypeKind.NonNull; - var namedType = selectionType.NamedType(); + var isSemanticNonNull = IsSemanticNonNull(selection, level); + var nullable = selectionType.IsNullableType(); + var nullableType = selectionType.NullableType(); if (!data.HasValue) { if (!partialResult) { - if (!nullable) + if (isSemanticNonNull) + { + AddSemanticNonNullViolation(context.Result, selection, selectionSetResult, responseIndex); + } + else if (!nullable) { PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex); break; @@ -85,14 +93,21 @@ private static void ComposeResult( result.Set(responseName, null, nullable); } } - else if (namedType.IsType(TypeKind.Scalar)) + else if (nullableType.IsType(TypeKind.Scalar)) { var value = data.Single.Element; - if (value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined && !nullable) + if (value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) { - PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex); - break; + if (isSemanticNonNull) + { + AddSemanticNonNullViolation(context.Result, selection, selectionSetResult, responseIndex); + } + else if (!nullable) + { + PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex); + break; + } } result.Set(responseName, value, nullable); @@ -105,15 +120,21 @@ private static void ComposeResult( result.Set(responseName, reformattedId, nullable); } } - else if (namedType.IsType(TypeKind.Enum)) + else if (nullableType.IsType(TypeKind.Enum)) { - // we might need to map the enum value! var value = data.Single.Element; - if (value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined && !nullable) + if (value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) { - PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex); - break; + if (isSemanticNonNull) + { + AddSemanticNonNullViolation(context.Result, selection, selectionSetResult, responseIndex); + } + else if (!nullable) + { + PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex); + break; + } } result.Set(responseName, value, nullable); @@ -122,7 +143,7 @@ private static void ComposeResult( { if (!result.IsInitialized) { - // we add a placeholder here so if the ComposeObject propagates an error + // we add a placeholder here so if ComposeObject propagates an error // there is a value here. result.Set(responseName, null, nullable); @@ -131,12 +152,20 @@ private static void ComposeResult( selectionSetResult, responseIndex, selection, - data); + data, + level); - if (value is null && !nullable) + if (value is null) { - PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex); - break; + if (isSemanticNonNull) + { + AddSemanticNonNullViolation(context.Result, selection, selectionSetResult, responseIndex); + } + else if (!nullable) + { + PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex); + break; + } } result.Set(responseName, value, nullable); @@ -146,18 +175,30 @@ private static void ComposeResult( { if (!result.IsInitialized) { + // we add a placeholder here so if ComposeList propagates an error + // there is a value here. + result.Set(responseName, null, nullable); + var value = ComposeList( context, selectionSetResult, responseIndex, selection, data, - selectionType); + selectionType, + level + 1); - if (value is null && !nullable) + if (value is null) { - PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex); - break; + if (isSemanticNonNull) + { + AddSemanticNonNullViolation(context.Result, selection, selectionSetResult, responseIndex); + } + else if (!nullable) + { + PropagateNullValues(context.Result, selection, selectionSetResult, responseIndex); + break; + } } result.Set(responseName, value, nullable); @@ -191,7 +232,8 @@ private static void ComposeResult( int parentIndex, Selection selection, SelectionData selectionData, - IType type) + IType type, + int level) { if (selectionData.IsNull()) { @@ -206,6 +248,7 @@ private static void ComposeResult( var index = 0; var elementType = type.ElementType(); var nullable = elementType.IsNullableType(); + var isSemanticNonNull = IsSemanticNonNull(selection, level); var result = context.Result.RentList(json.GetArrayLength()); result.IsNullable = nullable; @@ -213,7 +256,7 @@ private static void ComposeResult( foreach (var item in json.EnumerateArray()) { - // we add a placeholder here so if the ComposeElement propagates an error + // we add a placeholder here so if ComposeElement propagates an error // there is a value here. result.AddUnsafe(null); @@ -223,12 +266,20 @@ private static void ComposeResult( index, selection, new SelectionData(new JsonResult(schemaName, item)), - elementType); + elementType, + level); - if (!nullable && element is null) + if (element is null) { - PropagateNullValues(context.Result, selection, result, index); - return null; + if (isSemanticNonNull) + { + AddSemanticNonNullViolation(context.Result, selection, result, index); + } + else if (!nullable) + { + PropagateNullValues(context.Result, selection, result, index); + break; + } } result.SetUnsafe(index++, element); @@ -248,16 +299,17 @@ private static void ComposeResult( int parentIndex, Selection selection, SelectionData selectionData, - IType valueType) + IType valueType, + int level) { - var namedType = valueType.NamedType(); + var nullableType = valueType.NullableType(); if (!selectionData.HasValue) { return null; } - if (namedType.IsType(TypeKind.Scalar)) + if (nullableType.IsType(TypeKind.Scalar)) { var value = selectionData.Single.Element; @@ -276,7 +328,7 @@ private static void ComposeResult( return value; } - if (namedType.IsType(TypeKind.Enum)) + if (nullableType.IsType(TypeKind.Enum)) { var value = selectionData.Single.Element; @@ -288,9 +340,9 @@ private static void ComposeResult( return value; } - return TypeExtensions.IsCompositeType(valueType) - ? ComposeObject(context, parent, parentIndex, selection, selectionData) - : ComposeList(context, parent, parentIndex, selection, selectionData, valueType); + return nullableType.IsCompositeType() + ? ComposeObject(context, parent, parentIndex, selection, selectionData, 0) + : ComposeList(context, parent, parentIndex, selection, selectionData, valueType, level + 1); } private static ObjectResult? ComposeObject( @@ -298,7 +350,8 @@ private static void ComposeResult( ResultData parent, int parentIndex, ISelection selection, - SelectionData selectionData) + SelectionData selectionData, + int level) { if (selectionData.IsNull()) { @@ -330,13 +383,13 @@ private static void ComposeResult( var childSelectionResults = new SelectionData[selectionCount]; ExtractSelectionResults(selectionData, selectionSet, childSelectionResults); - ComposeResult(context, selectionSet, childSelectionResults, result, true); + ComposeResult(context, selectionSet, childSelectionResults, result, true, level); } else { var childSelectionResults = new SelectionData[selectionCount]; ExtractSelectionResults(selectionData, selectionSet, childSelectionResults); - ComposeResult(context, selectionSet, childSelectionResults, result); + ComposeResult(context, selectionSet, childSelectionResults, result, false, level); } return result.IsInvalidated ? null : result; @@ -716,6 +769,17 @@ public static void ExtractVariables( } } + private static void AddSemanticNonNullViolation( + ResultBuilder resultBuilder, + Selection selection, + ResultData selectionSetResult, + int responseIndex) + { + var path = PathHelper.CreatePathFromContext(selection, selectionSetResult, responseIndex); + var error = ErrorHelper.CreateSemanticNonNullViolationError(path, selection); + resultBuilder.AddError(error); + } + private static void PropagateNullValues( ResultBuilder resultBuilder, Selection selection, @@ -727,6 +791,25 @@ private static void PropagateNullValues( ValueCompletion.PropagateNullValues(selectionSetResult); } + private static readonly CustomOptionsFlags[] _levelOptions = + [ + CustomOptionsFlags.Option5, + CustomOptionsFlags.Option6, + CustomOptionsFlags.Option7 + ]; + + private static bool IsSemanticNonNull(Selection selection, int level) + { + if (level >= SemanticNonNullOptimizer.MaxLevels) + { + return true; + } + + var optionForLevel = _levelOptions[level]; + + return selection.CustomOptions.HasFlag(optionForLevel); + } + private sealed class ErrorPathContext { public string Current { get; set; } = string.Empty; diff --git a/src/HotChocolate/Fusion/src/Core/Execution/Pipeline/SemanticNonNullOptimizer.cs b/src/HotChocolate/Fusion/src/Core/Execution/Pipeline/SemanticNonNullOptimizer.cs new file mode 100644 index 00000000000..70d3a365431 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Execution/Pipeline/SemanticNonNullOptimizer.cs @@ -0,0 +1,50 @@ +using HotChocolate.Execution.Processing; + +namespace HotChocolate.Fusion.Execution.Pipeline; + +internal sealed class SemanticNonNullOptimizer : ISelectionSetOptimizer +{ + // This is an arbitrary limit to only use the last three options, + // so there's still room for other features requiring options. + // This is not ideal, but acceptable as this is just a prototype. + public const int MaxLevels = 3; + + private static readonly Selection.CustomOptionsFlags[] _levelOptions = + [ + Selection.CustomOptionsFlags.Option5, + Selection.CustomOptionsFlags.Option6, + Selection.CustomOptionsFlags.Option7 + ]; + + public void OptimizeSelectionSet(SelectionSetOptimizerContext context) + { + foreach (var selection in context.Selections.Values) + { + var semanticNonNullDirective = selection.Field.Directives + .FirstOrDefault(d => d.Type.Name == WellKnownDirectives.SemanticNonNull); + + if (semanticNonNullDirective is null) + { + continue; + } + + var levels = semanticNonNullDirective.GetArgumentValue>(WellKnownDirectives.Levels).Order(); + + var levelOption = Selection.CustomOptionsFlags.None; + + foreach (var level in levels) + { + // We only have 8 flags available. 3 are already taken and we want to allow for potential other + // flags later, so we're only using the last 3 flags. + if (level >= MaxLevels) + { + continue; + } + + levelOption |= _levelOptions[level]; + } + + selection.SetOption(levelOption); + } + } +} diff --git a/src/HotChocolate/Fusion/test/Core.Tests/SemanticNonNullTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/SemanticNonNullTests.cs new file mode 100644 index 00000000000..1fbe2eb0075 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/SemanticNonNullTests.cs @@ -0,0 +1,2971 @@ +using HotChocolate.Execution; +using HotChocolate.Fusion.Shared; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace HotChocolate.Fusion; + +public class SemanticNonNullTests(ITestOutputHelper output) +{ + # region Scalar + + [Fact] + public async Task Scalar_Field_Nullable_Subgraph_Returns_Null_Gateway_Nulls_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + field: String @null + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + field + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "data": { + "field": null + } + } + """); + } + + [Fact] + public async Task Scalar_Field_SemanticNonNull_Subgraph_Returns_Null_Gateway_Nulls_And_Errors_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + field: String @semanticNonNull + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary { ["field"] = null }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + field + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Cannot return null for semantic non-null field.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "field" + ], + "extensions": { + "code": "HC0088" + } + } + ], + "data": { + "field": null + } + } + """); + } + + [Fact] + public async Task Scalar_Field_SemanticNonNull_Subgraph_Returns_Error_For_Field_Gateway_Nulls_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + field: String @semanticNonNull @error + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + field + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "field" + ] + } + ], + "data": { + "field": null + } + } + """); + } + + #endregion + + #region Enum + + [Fact] + public async Task Enum_Field_Nullable_Subgraph_Returns_Null_Gateway_Nulls_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + field: MyEnum @null + } + + enum MyEnum { + VALUE + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + field + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "data": { + "field": null + } + } + """); + } + + [Fact] + public async Task Enum_Field_SemanticNonNull_Subgraph_Returns_Null_Gateway_Nulls_And_Errors_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + field: MyEnum @semanticNonNull + } + + enum MyEnum { + VALUE + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary { ["field"] = null }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + field + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Cannot return null for semantic non-null field.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "field" + ], + "extensions": { + "code": "HC0088" + } + } + ], + "data": { + "field": null + } + } + """); + } + + [Fact] + public async Task Enum_Field_SemanticNonNull_Subgraph_Returns_Error_For_Field_Gateway_Only_Nulls_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + field: MyEnum @semanticNonNull @error + } + + enum MyEnum { + VALUE + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + field + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "field" + ] + } + ], + "data": { + "field": null + } + } + """); + } + + #endregion + + #region Composite + + [Fact] + public async Task Composite_Field_Nullable_Subgraph_Returns_Null_Gateway_Nulls_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + field: MyObject @null + } + + type MyObject { + anotherField: String + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + field { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "data": { + "field": null + } + } + """); + } + + [Fact] + public async Task Composite_Field_SemanticNonNull_Subgraph_Returns_Null_Gateway_Nulls_And_Errors_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + field: MyObject @semanticNonNull + } + + type MyObject { + anotherField: String + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary { ["field"] = null }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + field { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Cannot return null for semantic non-null field.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "field" + ], + "extensions": { + "code": "HC0088" + } + } + ], + "data": { + "field": null + } + } + """); + } + + [Fact] + public async Task Composite_Field_SemanticNonNull_Subgraph_Returns_Error_For_Field_Gateway_Only_Nulls_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + field: MyObject @semanticNonNull @error + } + + type MyObject { + anotherField: String + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + field { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "field" + ] + } + ], + "data": { + "field": null + } + } + """); + } + + #endregion + + #region Composite Subfield + + [Fact] + public async Task Composite_Field_SubField_Nullable_Subgraph_Returns_Null_Gateway_Nulls_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + field: MyObject + } + + type MyObject { + anotherField: String @null + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + field { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "data": { + "field": { + "anotherField": null + } + } + } + """); + } + + [Fact] + public async Task Composite_Field_SubField_SemanticNonNull_Subgraph_Returns_Null_Gateway_Nulls_And_Errors_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + field: MyObject + } + + type MyObject { + anotherField: String @semanticNonNull + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary + { + ["field"] = new Dictionary { ["anotherField"] = null } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + field { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Cannot return null for semantic non-null field.", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": [ + "field", + "anotherField" + ], + "extensions": { + "code": "HC0088" + } + } + ], + "data": { + "field": { + "anotherField": null + } + } + } + """); + } + + [Fact] + public async Task + Composite_Field_SubField_SemanticNonNull_Subgraph_Returns_Error_For_Field_Gateway_Only_Nulls_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + field: MyObject + } + + type MyObject { + anotherField: String @semanticNonNull @error + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + field { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": [ + "field", + "anotherField" + ] + } + ], + "data": { + "field": { + "anotherField": null + } + } + } + """); + } + + #endregion + + #region List + + [Fact] + public async Task List_Nullable_Subgraph_Returns_Null_For_List_Gateway_Nulls_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + fields: [String] @null + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "data": { + "fields": null + } + } + """); + } + + [Fact] + public async Task List_SemanticNonNull_Subgraph_Returns_Null_For_List_Gateway_Nulls_And_Errors_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [String] @semanticNonNull + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary { ["fields"] = null }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Cannot return null for semantic non-null field.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields" + ], + "extensions": { + "code": "HC0088" + } + } + ], + "data": { + "fields": null + } + } + """); + } + + [Fact] + public async Task List_SemanticNonNull_Subgraph_Returns_Error_For_List_Gateway_Only_Nulls_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + fields: [String] @semanticNonNull @error + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields" + ] + } + ], + "data": { + "fields": null + } + } + """); + } + + #endregion + + #region Scalar List Item + + [Fact] + public async Task Scalar_ListItem_Nullable_Subgraph_Returns_Null_For_ListItem_Gateway_Nulls_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + fields: [String] @null(atIndex: 1) + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "data": { + "fields": [ + "string", + null, + "string" + ] + } + } + """); + } + + [Fact] + public async Task + Scalar_ListItem_SemanticNonNull_Subgraph_Returns_Null_For_ListItem_Gateway_Nulls_And_Errors_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [String] @semanticNonNull(levels: [1]) + } + """) + .UseField(_ => _ => default) + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary { ["fields"] = new[] { "a", null, "c" } }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Cannot return null for semantic non-null field.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields", + 1 + ], + "extensions": { + "code": "HC0088" + } + } + ], + "data": { + "fields": [ + "a", + null, + "c" + ] + } + } + """); + } + + [Fact] + public async Task Scalar_ListItem_SemanticNonNull_Subgraph_Returns_Error_For_Field_Gateway_Only_Nulls_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [String] @semanticNonNull(levels: [1]) + } + """) + .UseField(_ => _ => default) + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .AddError(ErrorBuilder.New().SetMessage("Some error").SetPath(["fields", 1]).Build()) + .SetData(new Dictionary { ["fields"] = new[] { "a", null, "c" } }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Some error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields", + 1 + ] + } + ], + "data": { + "fields": [ + "a", + null, + "c" + ] + } + } + """); + } + + #endregion + + #region Enum List Item + + [Fact] + public async Task Enum_ListItem_Nullable_Subgraph_Returns_Null_For_ListItem_Gateway_Nulls_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + fields: [MyEnum] @null(atIndex: 1) + } + + enum MyEnum { + VALUE + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "data": { + "fields": [ + "VALUE", + null, + "VALUE" + ] + } + } + """); + } + + [Fact] + public async Task + Enum_ListItem_SemanticNonNull_Subgraph_Returns_Null_For_ListItem_Gateway_Nulls_And_Errors_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [MyEnum] @semanticNonNull(levels: [1]) + } + + enum MyEnum { + VALUE + } + """) + .UseField(_ => _ => default) + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary { ["fields"] = new[] { "VALUE", null, "VALUE" } }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Cannot return null for semantic non-null field.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields", + 1 + ], + "extensions": { + "code": "HC0088" + } + } + ], + "data": { + "fields": [ + "VALUE", + null, + "VALUE" + ] + } + } + """); + } + + [Fact] + public async Task Enum_ListItem_SemanticNonNull_Subgraph_Returns_Error_For_Field_Gateway_Only_Nulls_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [MyEnum] @semanticNonNull(levels: [1]) + } + + enum MyEnum { + VALUE + } + """) + .UseField(_ => _ => default) + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .AddError(ErrorBuilder.New().SetMessage("Some error").SetPath(["fields", 1]).Build()) + .SetData(new Dictionary { ["fields"] = new[] { "VALUE", null, "VALUE" } }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Some error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields", + 1 + ] + } + ], + "data": { + "fields": [ + "VALUE", + null, + "VALUE" + ] + } + } + """); + } + + #endregion + + #region Composite List Item + + [Fact] + public async Task Composite_ListItem_Nullable_Subgraph_Returns_Null_For_ListItem_Gateway_Nulls_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + fields: [MyObject] @null(atIndex: 1) + } + + type MyObject { + anotherField: String + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "data": { + "fields": [ + { + "anotherField": "string" + }, + null, + { + "anotherField": "string" + } + ] + } + } + """); + } + + [Fact] + public async Task + Composite_ListItem_SemanticNonNull_Subgraph_Returns_Null_For_ListItem_Gateway_Nulls_And_Errors_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [MyObject] @semanticNonNull(levels: [1]) + } + + type MyObject { + anotherField: String + } + """) + .UseField(_ => _ => default) + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary + { + ["fields"] = new[] + { + new Dictionary { ["anotherField"] = "1" }, null, + new Dictionary { ["anotherField"] = "2" }, + } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Cannot return null for semantic non-null field.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields", + 1 + ], + "extensions": { + "code": "HC0088" + } + } + ], + "data": { + "fields": [ + { + "anotherField": "1" + }, + null, + { + "anotherField": "2" + } + ] + } + } + """); + } + + [Fact] + public async Task Composite_ListItem_SemanticNonNull_Subgraph_Returns_Error_For_Field_Gateway_Only_Nulls_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [MyObject] @semanticNonNull(levels: [1]) + } + + type MyObject { + anotherField: String + } + """) + .UseField(_ => _ => default) + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .AddError(ErrorBuilder.New().SetMessage("Some error").SetPath(["fields", 1]).Build()) + .SetData(new Dictionary + { + ["fields"] = new[] + { + new Dictionary { ["anotherField"] = "1" }, null, + new Dictionary { ["anotherField"] = "2" }, + } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Some error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields", + 1 + ] + } + ], + "data": { + "fields": [ + { + "anotherField": "1" + }, + null, + { + "anotherField": "2" + } + ] + } + } + """); + } + + #endregion + + #region Composite List Item Subfield + + [Fact] + public async Task Composite_ListItem_SubField_Nullable_Subgraph_Returns_Null_For_ListItem_Gateway_Nulls_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + fields: [MyObject] + } + + type MyObject { + anotherField: String @null(atIndex: 1) + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "data": { + "fields": [ + { + "anotherField": "string" + }, + { + "anotherField": null + }, + { + "anotherField": "string" + } + ] + } + } + """); + } + + [Fact] + public async Task + Composite_ListItem_SubField_SemanticNonNull_Subgraph_Returns_Null_For_Field_Gateway_Nulls_And_Errors_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [MyObject] + } + + type MyObject { + anotherField: String @semanticNonNull + } + """) + .UseField(_ => _ => default) + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary + { + ["fields"] = new[] + { + new Dictionary { ["anotherField"] = "1" }, + new Dictionary { ["anotherField"] = null }, + new Dictionary { ["anotherField"] = "2" }, + } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Cannot return null for semantic non-null field.", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": [ + "fields", + 1, + "anotherField" + ], + "extensions": { + "code": "HC0088" + } + } + ], + "data": { + "fields": [ + { + "anotherField": "1" + }, + { + "anotherField": null + }, + { + "anotherField": "2" + } + ] + } + } + """); + } + + [Fact] + public async Task + Composite_ListItem_SubField_SemanticNonNull_Subgraph_Returns_Error_For_Field_Gateway_Only_Nulls_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [MyObject] + } + + type MyObject { + anotherField: String @semanticNonNull + } + """) + .UseField(_ => _ => default) + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .AddError(ErrorBuilder.New().SetMessage("Some error").SetPath(["fields", 1, "anotherField"]) + .Build()) + .SetData(new Dictionary + { + ["fields"] = new[] + { + new Dictionary { ["anotherField"] = "1" }, + new Dictionary { ["anotherField"] = null }, + new Dictionary { ["anotherField"] = "2" }, + } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Some error", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": [ + "fields", + 1, + "anotherField" + ] + } + ], + "data": { + "fields": [ + { + "anotherField": "1" + }, + { + "anotherField": null + }, + { + "anotherField": "2" + } + ] + } + } + """); + } + + #endregion + + #region List of List + + [Fact] + public async Task List_Of_List_Nullable_Subgraph_Returns_Null_For_List_Gateway_Nulls_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + fields: [[String]] @null + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "data": { + "fields": null + } + } + """); + } + + [Fact] + public async Task List_Of_List_SemanticNonNull_Subgraph_Returns_Null_For_List_Gateway_Nulls_And_Errors_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[String]] @semanticNonNull + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary { ["fields"] = null }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Cannot return null for semantic non-null field.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields" + ], + "extensions": { + "code": "HC0088" + } + } + ], + "data": { + "fields": null + } + } + """); + } + + [Fact] + public async Task List_Of_List_SemanticNonNull_Subgraph_Returns_Error_For_List_Gateway_Only_Nulls_Field() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + """ + type Query { + fields: [[String]] @semanticNonNull @error + } + """, + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields" + ] + } + ], + "data": { + "fields": null + } + } + """); + } + + #endregion + + #region List of List Inner List + + [Fact] + public async Task List_Of_List_Inner_List_Nullable_Subgraph_Returns_Null_For_Inner_List_Gateway_Nulls_Inner_List() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[String]] + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary + { + ["fields"] = new[] { new[] { "a" }, null, new[] { "b" } } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "data": { + "fields": [ + [ + "a" + ], + null, + [ + "b" + ] + ] + } + } + """); + } + + [Fact] + public async Task + List_Of_List_Inner_List_SemanticNonNull_Subgraph_Returns_Null_For_Inner_List_Gateway_Nulls_And_Errors_Inner_List() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[String]] @semanticNonNull(levels: [1]) + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary + { + ["fields"] = new[] { new[] { "a" }, null, new[] { "b" } } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Cannot return null for semantic non-null field.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields", + 1 + ], + "extensions": { + "code": "HC0088" + } + } + ], + "data": { + "fields": [ + [ + "a" + ], + null, + [ + "b" + ] + ] + } + } + """); + } + + [Fact] + public async Task + List_Of_List_Inner_List_SemanticNonNull_Subgraph_Returns_Error_For_Inner_List_Gateway_Only_Nulls_Inner_List() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[String]] @semanticNonNull(levels: [1]) + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .AddError(ErrorBuilder.New().SetMessage("Some error").SetPath(["fields", 1]).Build()) + .SetData(new Dictionary + { + ["fields"] = new[] { new[] { "a" }, null, new[] { "b" } } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Some error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields", + 1 + ] + } + ], + "data": { + "fields": [ + [ + "a" + ], + null, + [ + "b" + ] + ] + } + } + """); + } + + #endregion + + #region List of List Scalar List Item + + [Fact] + public async Task List_Of_List_Scalar_ListItem_Nullable_Subgraph_Returns_Null_For_ListItem_Gateway_Nulls_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[String]] + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary + { + ["fields"] = new[] { new[] { "a" }, new string?[] { null }, new[] { "b" } } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "data": { + "fields": [ + [ + "a" + ], + [ + null + ], + [ + "b" + ] + ] + } + } + """); + } + + [Fact] + public async Task List_Of_List_Scalar_ListItem_Nullable_Subgraph_Returns_Null_For_ListItem_Gateway_Nulls_And_Errors_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[String]] @semanticNonNull(levels: [2]) + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary + { + ["fields"] = new[] { new[] { "a" }, new string?[] { null }, new[] { "b" } } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Cannot return null for semantic non-null field.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields", + 1, + 0 + ], + "extensions": { + "code": "HC0088" + } + } + ], + "data": { + "fields": [ + [ + "a" + ], + [ + null + ], + [ + "b" + ] + ] + } + } + """); + } + + [Fact] + public async Task List_Of_List_Scalar_ListItem_Nullable_Subgraph_Returns_Error_For_ListItem_Gateway_Only_Nulls_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[String]] @semanticNonNull(levels: [2]) + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .AddError(ErrorBuilder.New().SetMessage("Some error").SetPath(["fields", 1, 0]).Build()) + .SetData(new Dictionary + { + ["fields"] = new[] { new[] { "a" }, new string?[] { null }, new[] { "b" } } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Some error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields", + 1, + 0 + ] + } + ], + "data": { + "fields": [ + [ + "a" + ], + [ + null + ], + [ + "b" + ] + ] + } + } + """); + } + + #endregion + + #region List of List Enum List Item + + [Fact] + public async Task List_Of_List_Enum_ListItem_Nullable_Subgraph_Returns_Null_For_ListItem_Gateway_Nulls_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[MyEnum]] + } + + enum MyEnum { + VALUE + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary + { + ["fields"] = new[] { new[] { "VALUE" }, new string?[] { null }, new[] { "VALUE" } } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "data": { + "fields": [ + [ + "VALUE" + ], + [ + null + ], + [ + "VALUE" + ] + ] + } + } + """); + } + + [Fact] + public async Task List_Of_List_Enum_ListItem_Nullable_Subgraph_Returns_Null_For_ListItem_Gateway_Nulls_And_Errors_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[MyEnum]] @semanticNonNull(levels: [2]) + } + + enum MyEnum { + VALUE + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary + { + ["fields"] = new[] { new[] { "VALUE" }, new string?[] { null }, new[] { "VALUE" } } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Cannot return null for semantic non-null field.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields", + 1, + 0 + ], + "extensions": { + "code": "HC0088" + } + } + ], + "data": { + "fields": [ + [ + "VALUE" + ], + [ + null + ], + [ + "VALUE" + ] + ] + } + } + """); + } + + [Fact] + public async Task List_Of_List_Enum_ListItem_Nullable_Subgraph_Returns_Error_For_ListItem_Gateway_Only_Nulls_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[MyEnum]] @semanticNonNull(levels: [2]) + } + + enum MyEnum { + VALUE + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .AddError(ErrorBuilder.New().SetMessage("Some error").SetPath(["fields", 1, 0]).Build()) + .SetData(new Dictionary + { + ["fields"] = new[] { new[] { "VALUE" }, new string?[] { null }, new[] { "VALUE" } } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Some error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields", + 1, + 0 + ] + } + ], + "data": { + "fields": [ + [ + "VALUE" + ], + [ + null + ], + [ + "VALUE" + ] + ] + } + } + """); + } + + #endregion + + #region List of List Composite List Item + + [Fact] + public async Task List_Of_List_Composite_ListItem_Nullable_Subgraph_Returns_Null_For_ListItem_Gateway_Nulls_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[MyObject]] + } + + type MyObject { + anotherField: String + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary + { + ["fields"] = new[] + { + new[] { + new Dictionary + { + ["anotherField"] = "a" + } + }, + new Dictionary?[] { + null + }, + new[] { + new Dictionary + { + ["anotherField"] = "b" + } + }, + } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "data": { + "fields": [ + [ + { + "anotherField": "a" + } + ], + [ + null + ], + [ + { + "anotherField": "b" + } + ] + ] + } + } + """); + } + + [Fact] + public async Task List_Of_List_Composite_ListItem_Nullable_Subgraph_Returns_Null_For_ListItem_Gateway_Nulls_And_Errors_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[MyObject]] @semanticNonNull(levels: [2]) + } + + type MyObject { + anotherField: String + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary + { + ["fields"] = new[] + { + new[] { + new Dictionary + { + ["anotherField"] = "a" + } + }, + new Dictionary?[] { + null + }, + new[] { + new Dictionary + { + ["anotherField"] = "b" + } + }, + } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Cannot return null for semantic non-null field.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields", + 1, + 0 + ], + "extensions": { + "code": "HC0088" + } + } + ], + "data": { + "fields": [ + [ + { + "anotherField": "a" + } + ], + [ + null + ], + [ + { + "anotherField": "b" + } + ] + ] + } + } + """); + } + + [Fact] + public async Task List_Of_List_Composite_ListItem_Nullable_Subgraph_Returns_Error_For_ListItem_Gateway_Only_Nulls_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[MyObject]] @semanticNonNull(levels: [2]) + } + + type MyObject { + anotherField: String + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .AddError(ErrorBuilder.New().SetMessage("Some error").SetPath(["fields", 1, 0]).Build()) + .SetData(new Dictionary + { + ["fields"] = new[] + { + new[] { + new Dictionary + { + ["anotherField"] = "a" + } + }, + new Dictionary?[] { + null + }, + new[] { + new Dictionary + { + ["anotherField"] = "b" + } + }, + } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Some error", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "fields", + 1, + 0 + ] + } + ], + "data": { + "fields": [ + [ + { + "anotherField": "a" + } + ], + [ + null + ], + [ + { + "anotherField": "b" + } + ] + ] + } + } + """); + } + + #endregion + + #region List of List Composite List Item Subfield + + [Fact] + public async Task List_Of_List_Composite_ListItem_SubField_Nullable_Subgraph_Returns_Null_For_ListItem_Gateway_Nulls_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[MyObject]] + } + + type MyObject { + anotherField: String + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary + { + ["fields"] = new[] + { + new[] { + new Dictionary + { + ["anotherField"] = "a" + } + }, + new Dictionary?[] { + new Dictionary + { + ["anotherField"] = null + } + }, + new[] { + new Dictionary + { + ["anotherField"] = "b" + } + }, + } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "data": { + "fields": [ + [ + { + "anotherField": "a" + } + ], + [ + { + "anotherField": null + } + ], + [ + { + "anotherField": "b" + } + ] + ] + } + } + """); + } + + [Fact] + public async Task List_Of_List_Composite_ListItem_SubField_Nullable_Subgraph_Returns_Null_For_ListItem_Gateway_Nulls_And_Errors_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[MyObject]] + } + + type MyObject { + anotherField: String @semanticNonNull + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary + { + ["fields"] = new[] + { + new[] { + new Dictionary + { + ["anotherField"] = "a" + } + }, + new Dictionary?[] { + new Dictionary + { + ["anotherField"] = null + } + }, + new[] { + new Dictionary + { + ["anotherField"] = "b" + } + }, + } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Cannot return null for semantic non-null field.", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": [ + "fields", + 1, + 0, + "anotherField" + ], + "extensions": { + "code": "HC0088" + } + } + ], + "data": { + "fields": [ + [ + { + "anotherField": "a" + } + ], + [ + { + "anotherField": null + } + ], + [ + { + "anotherField": "b" + } + ] + ] + } + } + """); + } + + [Fact] + public async Task List_Of_List_Composite_ListItem_SubField_Nullable_Subgraph_Returns_Error_For_ListItem_Gateway_Only_Nulls_ListItem() + { + // arrange + var subgraph = await TestSubgraph.CreateAsync( + configure: builder => builder + .AddDocumentFromString(""" + type Query { + fields: [[MyObject]] + } + + type MyObject { + anotherField: String @semanticNonNull + } + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .AddError(ErrorBuilder.New().SetMessage("Some error").SetPath(["fields", 1, 0, "anotherField"]).Build()) + .SetData(new Dictionary + { + ["fields"] = new[] + { + new[] { + new Dictionary + { + ["anotherField"] = "a" + } + }, + new Dictionary?[] { + new Dictionary + { + ["anotherField"] = null + } + }, + new[] { + new Dictionary + { + ["anotherField"] = "b" + } + }, + } + }) + .Build(); + return default; + }) + , + enableSemanticNonNull: true); + + using var subgraphs = new TestSubgraphCollection(output, [subgraph]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + fields { + anotherField + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + result.MatchInlineSnapshot(""" + { + "errors": [ + { + "message": "Some error", + "locations": [ + { + "line": 3, + "column": 5 + } + ], + "path": [ + "fields", + 1, + 0, + "anotherField" + ] + } + ], + "data": { + "fields": [ + [ + { + "anotherField": "a" + } + ], + [ + { + "anotherField": null + } + ], + [ + { + "anotherField": "b" + } + ] + ] + } + } + """); + } + + #endregion +} diff --git a/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs b/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs index 27f1fc9e5e9..affec363d20 100644 --- a/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs +++ b/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs @@ -18,19 +18,22 @@ public record TestSubgraph( public static Task CreateAsync( [StringSyntax("graphql")] string schemaText, [StringSyntax("graphql")] string extensions = "", - bool isOffline = false) + bool isOffline = false, + bool enableSemanticNonNull = false) => CreateAsync( configure: builder => builder .AddDocumentFromString(schemaText) .AddResolverMocking() .AddTestDirectives(), extensions: extensions, - isOffline: isOffline); + isOffline: isOffline, + enableSemanticNonNull: enableSemanticNonNull); public static async Task CreateAsync( Action configure, [StringSyntax("graphql")] string extensions = "", - bool isOffline = false) + bool isOffline = false, + bool enableSemanticNonNull = false) { var testServerFactory = new TestServerFactory(); var testContext = new SubgraphTestContext(); @@ -42,6 +45,11 @@ public static async Task CreateAsync( .AddRouting() .AddGraphQLServer(disableDefaultSecurity: true); + if (enableSemanticNonNull) + { + builder.ModifyOptions(o => o.EnableSemanticNonNull = true); + } + configure(builder); }, app => From 950e3f3ca926fe233cf13cff3a83529e7d9b0993 Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 14 Mar 2025 11:53:36 +0200 Subject: [PATCH 42/64] Set DOCKER_CONFIG environment variable in CI and coverage workflows (#8128) --- .github/workflows/ci.yml | 2 ++ .github/workflows/coverage.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb17cc0a745..1f702000dcb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,6 +162,8 @@ jobs: runs-on: ubuntu-latest needs: [configure, check-changes] if: needs.check-changes.outputs.library_changes == 'true' + env: + DOCKER_CONFIG: $HOME/.docker strategy: fail-fast: false diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 0e3f55e7253..d99a5b5730a 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -44,6 +44,8 @@ jobs: name: Run ${{ matrix.name }} runs-on: ubuntu-latest needs: configure + env: + DOCKER_CONFIG: $HOME/.docker strategy: fail-fast: false From bd92368f38da448cd696f05fc5af616dd4067cc2 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 14 Mar 2025 14:40:22 +0100 Subject: [PATCH 43/64] Downgraded Postgres Squadron (#8131) --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index dc52ca2ef50..987e221a7f1 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -48,7 +48,7 @@ - + From f4078e1fec0f56c82d7446ea8a810705f06dbff7 Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 14 Mar 2025 15:43:42 +0200 Subject: [PATCH 44/64] Improved error message when query/mutation conventions are not enabled (#8130) --- .../Properties/ErrorResources.Designer.cs | 44 ++++++++++++++----- .../Properties/ErrorResources.resx | 2 +- .../AnnotationBasedMutations.cs | 20 +++++++++ .../AnnotationBasedSchemaTests.cs | 20 +++++++++ 4 files changed, 73 insertions(+), 13 deletions(-) diff --git a/src/HotChocolate/Core/src/Types.Errors/Properties/ErrorResources.Designer.cs b/src/HotChocolate/Core/src/Types.Errors/Properties/ErrorResources.Designer.cs index 05bd9a8200b..1add2120d13 100644 --- a/src/HotChocolate/Core/src/Types.Errors/Properties/ErrorResources.Designer.cs +++ b/src/HotChocolate/Core/src/Types.Errors/Properties/ErrorResources.Designer.cs @@ -11,32 +11,46 @@ namespace HotChocolate.Types.Properties { using System; - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [System.Diagnostics.DebuggerNonUserCodeAttribute()] - [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class ErrorResources { - private static System.Resources.ResourceManager resourceMan; + private static global::System.Resources.ResourceManager resourceMan; - private static System.Globalization.CultureInfo resourceCulture; + private static global::System.Globalization.CultureInfo resourceCulture; - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal ErrorResources() { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Resources.ResourceManager ResourceManager { + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { get { - if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("HotChocolate.Types.Properties.ErrorResources", typeof(ErrorResources).Assembly); + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HotChocolate.Types.Properties.ErrorResources", typeof(ErrorResources).Assembly); resourceMan = temp; } return resourceMan; } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Globalization.CultureInfo Culture { + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -45,12 +59,18 @@ internal static System.Globalization.CultureInfo Culture { } } + /// + /// Looks up a localized string similar to Adding an error type `{0}` to field `{1}` failed as query or mutation conventions were not enabled.. + /// internal static string ErrorConventionDisabled_Message { get { return ResourceManager.GetString("ErrorConventionDisabled_Message", resourceCulture); } } + /// + /// Looks up a localized string similar to Error while building type {0}. The runtime type {1} does not define a property named `Message`. + /// internal static string ThrowHelper_ErrorObjectType_MessageWasNotDefinedOnError { get { return ResourceManager.GetString("ThrowHelper_ErrorObjectType_MessageWasNotDefinedOnError", resourceCulture); diff --git a/src/HotChocolate/Core/src/Types.Errors/Properties/ErrorResources.resx b/src/HotChocolate/Core/src/Types.Errors/Properties/ErrorResources.resx index 9f245d5843d..171b7b5f1c1 100644 --- a/src/HotChocolate/Core/src/Types.Errors/Properties/ErrorResources.resx +++ b/src/HotChocolate/Core/src/Types.Errors/Properties/ErrorResources.resx @@ -19,7 +19,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Adding an error type `{0}` to field `{1}` failed as error conventions weren't enabled. + Adding an error type `{0}` to field `{1}` failed as query or mutation conventions were not enabled. Error while building type {0}. The runtime type {1} does not define a property named `Message` diff --git a/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs b/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs index 7af8ef79ab7..86a463b5ccd 100644 --- a/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs +++ b/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs @@ -1303,6 +1303,26 @@ public async Task InferErrorEvenIfExplicitFieldBindingIsUsed() schema.Print().MatchSnapshot(); } + [Fact] + public async Task MutationWithError_MutationConventionsNotEnabled_ThrowsSchemaException() + { + // arrange + async Task Act() => + await new ServiceCollection() + .AddGraphQL() + .AddMutationType() + .BuildSchemaAsync(); + + // act & assert + var exception = + (SchemaException?)(await Assert.ThrowsAsync(Act)).Errors[0].Exception; + + Assert.Equal( + "Adding an error type `CustomException` to field `doSomething` failed as query or " + + "mutation conventions were not enabled.", + exception?.Errors[0].Message); + } + public class ExplicitMutation { public FieldResult DoSomething(int status) diff --git a/src/HotChocolate/Core/test/Types.Queries.Tests/AnnotationBasedSchemaTests.cs b/src/HotChocolate/Core/test/Types.Queries.Tests/AnnotationBasedSchemaTests.cs index 28ed75a74b9..38e3acb838c 100644 --- a/src/HotChocolate/Core/test/Types.Queries.Tests/AnnotationBasedSchemaTests.cs +++ b/src/HotChocolate/Core/test/Types.Queries.Tests/AnnotationBasedSchemaTests.cs @@ -394,6 +394,26 @@ async Task Error() => exception.Errors[0].Message.MatchSnapshot(); } + [Fact] + public async Task QueryWithError_QueryConventionsNotEnabled_ThrowsSchemaException() + { + // arrange + async Task Act() => + await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .BuildSchemaAsync(); + + // act & assert + var exception = + (SchemaException?)(await Assert.ThrowsAsync(Act)).Errors[0].Exception; + + Assert.Equal( + "Adding an error type `InvalidUserIdException` to field `userById` failed as query or " + + "mutation conventions were not enabled.", + exception?.Errors[0].Message); + } + public class QueryWithException { [Error] From d0c528cd88120eeea0f0924ccf3ddc4be81cd5a3 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 14 Mar 2025 15:40:24 +0100 Subject: [PATCH 45/64] Added page based connection type (#8132) --- src/All.slnx | 1 + .../Extensions/PagingQueryableExtensions.cs | 25 +- .../GreenDonut.Data.Primitives.csproj | 1 + .../src/GreenDonut.Data.Primitives/Page.cs | 30 +- .../Extensions/GreenDonutPageExtensions.cs | 135 ++ .../src/GreenDonut.Data/PageCursor.cs | 3 + .../HotChocolate.Caching.Tests.csproj | 1 + src/HotChocolate/Core/HotChocolate.Core.sln | 1084 ----------------- src/HotChocolate/Core/HotChocolate.Core.slnx | 87 ++ .../Core/src/Core/HotChocolate.Core.csproj | 2 +- .../FileBuilders/ConnectionTypeFileBuilder.cs | 6 +- .../FileBuilders/EdgeTypeFileBuilder.cs | 6 +- .../Inspectors/ConnectionTypeTransformer.cs | 8 +- .../Core/src/Types.Analyzers/KnownSymbols.cs | 2 +- ...e.Types.CursorPagination.Extensions.csproj | 17 + .../PageConnection.cs | 80 ++ .../PageCursorType.cs | 21 + .../PageEdge.cs | 33 + .../PageInfo.cs | 101 ++ .../Types.CursorPagination/ConnectionBase.cs | 1 + .../Extensions/UseConnectionAttribute.cs | 4 +- .../test/Types.Analyzers.Tests/PagingTests.cs | 4 +- ...tion_WithConnectionName_MatchesSnapshot.md | 4 + ...colate.Types.OffsetPagination.Tests.csproj | 1 + .../HotChocolate.Types.Tests.csproj | 1 + .../HotChocolate.CostAnalysis.Tests.csproj | 1 + ...hocolate.Data.EntityFramework.Tests.csproj | 1 + .../Data.PostgreSQL.Tests/IntegrationTests.cs | 70 +- .../Types/Brands/BrandNode.cs | 8 +- .../Types/Products/ProductNode.cs | 1 - .../IntegrationTests.CreateSchema.graphql | 45 +- ...ery_Brands_First_2_And_Products_First_2.md | 10 + ..._First_2_And_Products_First_2_Name_Desc.md | 10 + ...d_Products_First_2_Name_Desc_Brand_Name.md | 12 +- ...s_First_2_Name_Desc_Brand_Name__net_8_0.md | 12 +- ...And_Products_First_2_Name_Desc__net_8_0.md | 10 + ...s_First_2_And_Products_First_2__net_8_0.md | 10 + ...First_2_Products_First_2_ForwardCursors.md | 83 ++ ...roducts_First_2_ForwardCursors__net_8_0.md | 83 ++ 39 files changed, 858 insertions(+), 1156 deletions(-) create mode 100644 src/GreenDonut/src/GreenDonut.Data/Extensions/GreenDonutPageExtensions.cs create mode 100644 src/GreenDonut/src/GreenDonut.Data/PageCursor.cs delete mode 100644 src/HotChocolate/Core/HotChocolate.Core.sln create mode 100644 src/HotChocolate/Core/HotChocolate.Core.slnx create mode 100644 src/HotChocolate/Core/src/Types.CursorPagination.Extensions/HotChocolate.Types.CursorPagination.Extensions.csproj create mode 100644 src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageConnection.cs create mode 100644 src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageCursorType.cs create mode 100644 src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageEdge.cs create mode 100644 src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageInfo.cs create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_Products_First_2_ForwardCursors.md create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_Products_First_2_ForwardCursors__net_8_0.md diff --git a/src/All.slnx b/src/All.slnx index 8b7c570848c..b76dcd8cbc8 100644 --- a/src/All.slnx +++ b/src/All.slnx @@ -89,6 +89,7 @@ + diff --git a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs index 410675248ff..8644a5f5148 100644 --- a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs +++ b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs @@ -155,8 +155,7 @@ public static async ValueTask> ToPageAsync( if (arguments.EnableRelativeCursors && cursor?.IsRelative == true) { - if ((arguments.Last is not null && cursor.Offset > 0) || - (arguments.First is not null && cursor.Offset < 0)) + if ((arguments.Last is not null && cursor.Offset > 0) || (arguments.First is not null && cursor.Offset < 0)) { throw new ArgumentException( "Positive offsets are not allowed with `last`, and negative offsets are not allowed with `first`.", @@ -237,7 +236,7 @@ public static async ValueTask> ToPageAsync( } var pageIndex = CreateIndex(arguments, cursor, totalCount); - return CreatePage(builder.ToImmutable(), arguments, keys, fetchCount, pageIndex, totalCount); + return CreatePage(builder.ToImmutable(), arguments, keys, fetchCount, pageIndex, requestedCount, totalCount); } /// @@ -433,12 +432,6 @@ public static async ValueTask>> ToBatchPageAsync? counts = null; - if (includeTotalCount) - { - counts = await GetBatchCountsAsync(source, keySelector, cancellationToken); - } - source = QueryHelpers.EnsureOrderPropsAreSelected(source); source = QueryHelpers.EnsureGroupPropsAreSelected(source, keySelector); @@ -447,6 +440,12 @@ public static async ValueTask>> ToBatchPageAsync? counts = null; + if (includeTotalCount) + { + counts = await GetBatchCountsAsync(source, keySelector, cancellationToken); + } + var forward = arguments.Last is null; var requestedCount = int.MaxValue; var batchExpression = @@ -503,6 +502,7 @@ public static async ValueTask>> ToBatchPageAsync> GetBatchCountsAsync t.Key, t => t.Count, cancellationToken); } - private static Expression, CountResult>> GetOrCreateCountSelector() + private static Expression, CountResult>> GetOrCreateCountSelector() { return (Expression, CountResult>>) _countExpressionCache.GetOrAdd( @@ -572,6 +571,7 @@ private static Page CreatePage( CursorKey[] keys, int fetchCount, int? index, + int? requestedPageSize, int? totalCount) { var hasPrevious = false; @@ -605,7 +605,7 @@ private static Page CreatePage( hasNext = true; } - if (arguments.EnableRelativeCursors && totalCount is not null) + if (arguments.EnableRelativeCursors && totalCount is not null && requestedPageSize is not null) { return new Page( items, @@ -613,6 +613,7 @@ private static Page CreatePage( hasPrevious, (item, o, p, c) => CursorFormatter.Format(item, keys, new CursorPageInfo(o, p, c)), index ?? 1, + requestedPageSize.Value, totalCount.Value); } diff --git a/src/GreenDonut/src/GreenDonut.Data.Primitives/GreenDonut.Data.Primitives.csproj b/src/GreenDonut/src/GreenDonut.Data.Primitives/GreenDonut.Data.Primitives.csproj index d8a7dfc98bc..ff39a60e3e3 100644 --- a/src/GreenDonut/src/GreenDonut.Data.Primitives/GreenDonut.Data.Primitives.csproj +++ b/src/GreenDonut/src/GreenDonut.Data.Primitives/GreenDonut.Data.Primitives.csproj @@ -8,6 +8,7 @@ + diff --git a/src/GreenDonut/src/GreenDonut.Data.Primitives/Page.cs b/src/GreenDonut/src/GreenDonut.Data.Primitives/Page.cs index 8218e925b73..147eae62796 100644 --- a/src/GreenDonut/src/GreenDonut.Data.Primitives/Page.cs +++ b/src/GreenDonut/src/GreenDonut.Data.Primitives/Page.cs @@ -15,8 +15,9 @@ public sealed class Page : IEnumerable private readonly bool _hasNextPage; private readonly bool _hasPreviousPage; private readonly Func _createCursor; - private readonly int? _totalCount; + private readonly int? _requestedPageSize; private readonly int? _index; + private readonly int? _totalCount; /// /// Initializes a new instance of the class. @@ -46,7 +47,7 @@ public Page( _items = items; _hasNextPage = hasNextPage; _hasPreviousPage = hasPreviousPage; - _createCursor = (item, _, _, _) => createCursor(item); + _createCursor = (item, _, _, _) => createCursor(item); _totalCount = totalCount; } @@ -71,20 +72,25 @@ public Page( /// /// The index number of this page. /// + /// + /// The requested page size. + /// internal Page( ImmutableArray items, bool hasNextPage, bool hasPreviousPage, Func createCursor, int index, + int requestedPageSize, int totalCount) { _items = items; _hasNextPage = hasNextPage; _hasPreviousPage = hasPreviousPage; _createCursor = createCursor; - _totalCount = totalCount; _index = index; + _requestedPageSize = requestedPageSize; + _totalCount = totalCount; } /// @@ -113,15 +119,21 @@ internal Page( public bool HasPreviousPage => _hasPreviousPage; /// - /// Gets the total count of items in the dataset. - /// This value can be null if the total count is unknown. + /// Gets the index number of this page. /// - public int? TotalCount => _totalCount; + public int? Index => _index; /// - /// Gets the index number of this page. + /// Gets the requested page size. + /// This value can be null if the page size is unknown. /// - public int? Index => _index; + internal int? RequestedSize => _requestedPageSize; + + /// + /// Gets the total count of items in the dataset. + /// This value can be null if the total count is unknown. + /// + public int? TotalCount => _totalCount; /// /// Creates a cursor for an item of this page. @@ -136,7 +148,7 @@ internal Page( public string CreateCursor(T item, int offset) { - if(_index is null || _totalCount is null) + if (_index is null || _totalCount is null) { throw new InvalidOperationException("This page does not allow relative cursors."); } diff --git a/src/GreenDonut/src/GreenDonut.Data/Extensions/GreenDonutPageExtensions.cs b/src/GreenDonut/src/GreenDonut.Data/Extensions/GreenDonutPageExtensions.cs new file mode 100644 index 00000000000..2b0654f12fc --- /dev/null +++ b/src/GreenDonut/src/GreenDonut.Data/Extensions/GreenDonutPageExtensions.cs @@ -0,0 +1,135 @@ +using System.Collections.Immutable; + +namespace GreenDonut.Data; + +/// +/// Extensions for the class. +/// +public static class GreenDonutPageExtensions +{ + /// + /// Creates a relative cursor for backwards pagination. + /// + /// + /// The page to create cursors for. + /// + /// + /// The maximum number of cursors to create. + /// + /// + /// An array of cursors. + /// + /// + /// Thrown if the page is null. + /// + /// + /// Thrown if the maximum number of cursors is less than 0. + /// + /// + /// Thrown if the page does not allow relative cursors. + /// + /// + /// This method creates cursors for the previous pages based on the current page. + /// The cursors are created using the method. + /// + public static ImmutableArray CreateRelativeBackwardCursors(this Page page, int maxCursors = 5) + { + if (page == null) + { + throw new ArgumentNullException(nameof(page)); + } + + if (maxCursors < 0) + { + throw new ArgumentOutOfRangeException( + nameof(maxCursors), + "Max cursors must be greater than or equal to 0."); + } + + if (page.First is null || page.Index is null || page.Index == 1) + { + return []; + } + + var previousPages = page.Index.Value - 1; + var cursors = ImmutableArray.CreateBuilder(); + + maxCursors *= -1; + + for (var i = 0; i > maxCursors && previousPages + i - 1 >= 0; i--) + { + cursors.Insert( + 0, + new PageCursor( + page.CreateCursor(page.First, i), + previousPages + i)); + } + + return cursors.ToImmutable(); + } + + /// + /// Creates a relative cursor for forwards pagination. + /// + /// + /// The page to create cursors for. + /// + /// + /// The maximum number of cursors to create. + /// + /// + /// An array of cursors. + /// + /// + /// Thrown if the page is null. + /// + /// + /// Thrown if the maximum number of cursors is less than 0. + /// + /// + /// Thrown if the page does not allow relative cursors. + /// + /// + /// This method creates cursors for the next pages based on the current page. + /// The cursors are created using the method. + /// + public static ImmutableArray CreateRelativeForwardCursors(this Page page, int maxCursors = 5) + { + if (page == null) + { + throw new ArgumentNullException(nameof(page)); + } + + if (maxCursors < 0) + { + throw new ArgumentOutOfRangeException( + nameof(maxCursors), + "Max cursors must be greater than or equal to 0."); + } + + if (page.Last is null || page.Index is null) + { + return []; + } + + var totalPages = (page.TotalCount ?? 0) / (page.RequestedSize ?? 10); + + if (page.Index >= totalPages) + { + return []; + } + + var cursors = ImmutableArray.CreateBuilder(); + cursors.Add(new PageCursor(page.CreateCursor(page.Last, 0), page.Index.Value + 1)); + + for (int i = 1; i < maxCursors && page.Index + i < totalPages; i++) + { + cursors.Add( + new PageCursor( + page.CreateCursor(page.Last, i), + page.Index.Value + i + 1)); + } + + return cursors.ToImmutable(); + } +} diff --git a/src/GreenDonut/src/GreenDonut.Data/PageCursor.cs b/src/GreenDonut/src/GreenDonut.Data/PageCursor.cs new file mode 100644 index 00000000000..ca38aa5dcaf --- /dev/null +++ b/src/GreenDonut/src/GreenDonut.Data/PageCursor.cs @@ -0,0 +1,3 @@ +namespace GreenDonut.Data; + +public record PageCursor(string Cursor, int Page); diff --git a/src/HotChocolate/Caching/test/Caching.Tests/HotChocolate.Caching.Tests.csproj b/src/HotChocolate/Caching/test/Caching.Tests/HotChocolate.Caching.Tests.csproj index 065019cb64d..c34db375bfd 100644 --- a/src/HotChocolate/Caching/test/Caching.Tests/HotChocolate.Caching.Tests.csproj +++ b/src/HotChocolate/Caching/test/Caching.Tests/HotChocolate.Caching.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/src/HotChocolate/Core/HotChocolate.Core.sln b/src/HotChocolate/Core/HotChocolate.Core.sln deleted file mode 100644 index 71616ffe387..00000000000 --- a/src/HotChocolate/Core/HotChocolate.Core.sln +++ /dev/null @@ -1,1084 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.33516.290 -MinimumVisualStudioVersion = 15.0.26124.0 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".referenced", ".referenced", "{7637D30E-7339-4D4E-9424-87CF2394D234}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7462D089-D350-44D6-8131-896D949A65B7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GreenDonut", "..\..\GreenDonut\src\GreenDonut\GreenDonut.csproj", "{015D2107-B5CD-4A45-A650-B29AECC6FBE3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Abstractions", "src\Abstractions\HotChocolate.Abstractions.csproj", "{38EB59B2-A76B-48F7-B99D-C99FB7793903}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Utilities", "..\Utilities\src\Utilities\HotChocolate.Utilities.csproj", "{AFCDB23E-7988-4BE7-BD1A-23912AB1FCAE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Execution", "src\Execution\HotChocolate.Execution.csproj", "{BEC2D863-C095-4D83-8EDF-7B8AED26A834}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Language", "..\Language\src\Language\HotChocolate.Language.csproj", "{7B9BE76A-1472-4028-BB8C-6C0C54342222}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Language.SyntaxTree", "..\Language\src\Language.SyntaxTree\HotChocolate.Language.SyntaxTree.csproj", "{52631D27-685E-4D2B-92D7-0D53CAF2DD82}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Language.Utf8", "..\Language\src\Language.Utf8\HotChocolate.Language.Utf8.csproj", "{A8D2A7D1-483F-4FC3-867B-DB6E0DA95EF5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Language.Visitors", "..\Language\src\Language.Visitors\HotChocolate.Language.Visitors.csproj", "{ED81B543-714A-484C-ACB9-CF70308B045F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Core", "src\Core\HotChocolate.Core.csproj", "{BBD6E614-37A8-4E86-88A7-2CE8821C7CB2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Subscriptions", "src\Subscriptions\HotChocolate.Subscriptions.csproj", "{350488E5-F800-4CBD-9278-1D5C84C78958}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Subscriptions.InMemory", "src\Subscriptions.InMemory\HotChocolate.Subscriptions.InMemory.csproj", "{CF768605-BAB8-42D8-A748-66791575BCFD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Subscriptions.Redis", "src\Subscriptions.Redis\HotChocolate.Subscriptions.Redis.csproj", "{35C5F3EF-B192-4A07-B664-A3D360283907}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Validation", "src\Validation\HotChocolate.Validation.csproj", "{29FEE730-0D2C-4F31-934E-9BE2E9DC729A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Abstractions.Tests", "test\Abstractions.Tests\HotChocolate.Abstractions.Tests.csproj", "{80F8703D-804D-4921-A409-AF4BB26E59BE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.StarWars.Tests", "test\StarWars\HotChocolate.StarWars.Tests.csproj", "{AADFF9B1-B275-48B4-8F32-F6CD837619E5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.Tests", "test\Types.Tests\HotChocolate.Types.Tests.csproj", "{CD2B383F-2957-4A83-B965-070DC8CF36F6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.Tests.Documentation", "test\Types.Tests.Documentation\HotChocolate.Types.Tests.Documentation.csproj", "{3DFDEFBE-D2BD-4F25-8A26-66AFB62B9C4C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Validation.Tests", "test\Validation.Tests\HotChocolate.Validation.Tests.csproj", "{C08D0A27-6F28-4845-91E7-5C1A0C926939}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Utilities.Introspection", "..\Utilities\src\Utilities.Introspection\HotChocolate.Utilities.Introspection.csproj", "{39592692-847D-49F0-97DE-3F2F21677319}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Tests.Utilities", "test\Utilities\HotChocolate.Tests.Utilities.csproj", "{E2574A41-D9BF-4071-84D0-DB0F6D1148EE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Execution.Tests", "test\Execution.Tests\HotChocolate.Execution.Tests.csproj", "{1D3C84AE-2818-4873-A222-BE6D329E850D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Fetching", "src\Fetching\HotChocolate.Fetching.csproj", "{3ED68361-0D3D-45F5-8A83-444DD3D5C1BF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Subscriptions.Redis.Tests", "test\Subscriptions.Redis.Tests\HotChocolate.Subscriptions.Redis.Tests.csproj", "{14648C7D-8958-4C94-B837-7F90F2F10081}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Subscriptions.InMemory.Tests", "test\Subscriptions.InMemory.Tests\HotChocolate.Subscriptions.InMemory.Tests.csproj", "{F72E72DE-F163-4BCC-A442-94DB331D3C35}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Fetching.Tests", "test\Fetching.Tests\HotChocolate.Fetching.Tests.csproj", "{2A319702-6C2E-4184-8E71-43F87745B38D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.CursorPagination", "src\Types.CursorPagination\HotChocolate.Types.CursorPagination.csproj", "{91F00C73-78E5-4400-8F9F-910BE99EDFD5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.OffsetPagination", "src\Types.OffsetPagination\HotChocolate.Types.OffsetPagination.csproj", "{738F2483-286E-4517-9660-5BFEEF343595}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.CursorPagination.Tests", "test\Types.CursorPagination.Tests\HotChocolate.Types.CursorPagination.Tests.csproj", "{31AF3FED-A6C5-4DD5-99DF-03D4B4186D39}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.OffsetPagination.Tests", "test\Types.OffsetPagination.Tests\HotChocolate.Types.OffsetPagination.Tests.csproj", "{9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.Records.Tests", "test\Types.Records.Tests\HotChocolate.Types.Records.Tests.csproj", "{CF2FA420-6295-4F83-8F87-340D8365282D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.Scalars", "src\Types.Scalars\HotChocolate.Types.Scalars.csproj", "{8B7EEF6D-DFF2-4D7B-9E5D-6CA34A8BC260}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.Scalars.Tests", "test\Types.Scalars.Tests\HotChocolate.Types.Scalars.Tests.csproj", "{480E09E8-A0C4-49A7-AAC4-4AA5EC0733C5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.Scalars.Upload", "src\Types.Scalars.Upload\HotChocolate.Types.Scalars.Upload.csproj", "{670D3B20-401F-4004-AD0B-865560D02CC3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.NodaTime", "src\Types.NodaTime\HotChocolate.Types.NodaTime.csproj", "{07F3E312-EC4C-4B71-A095-E478DF4CB52B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.NodaTime.Tests", "test\Types.NodaTime.Tests\HotChocolate.Types.NodaTime.Tests.csproj", "{414B6DBE-1A3A-42DC-9116-3F9A433C6BBB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.AspNetCore", "..\AspNetCore\src\AspNetCore\HotChocolate.AspNetCore.csproj", "{4AD904D1-7727-48EF-88D9-7B38D98EB314}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.Mutations", "src\Types.Mutations\HotChocolate.Types.Mutations.csproj", "{2B7E7416-E093-4763-B839-504CBFBC8F1C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.Mutations.Tests", "test\Types.Mutations.Tests\HotChocolate.Types.Mutations.Tests.csproj", "{F6FFA925-8C49-4594-9FF2-8F571C2D6389}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types", "src\Types\HotChocolate.Types.csproj", "{151FB103-4BCA-41D2-9C8E-C5BDA7A68320}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.Analyzers", "src\Types.Analyzers\HotChocolate.Types.Analyzers.csproj", "{D27B9558-0498-401A-AD3E-9BF15EF66E88}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.Analyzers.Tests", "test\Types.Analyzers.Tests\HotChocolate.Types.Analyzers.Tests.csproj", "{545245AB-A255-4176-91E6-C837A21648D8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.Json", "src\Types.Json\HotChocolate.Types.Json.csproj", "{34F7650B-6135-4C08-9B42-7E7DBA7D186F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Types.Json.Tests", "test\Types.Json.Tests\HotChocolate.Types.Json.Tests.csproj", "{935A1DDC-6709-4521-A072-37ACC288371F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Subscriptions.Nats", "src\Subscriptions.Nats\HotChocolate.Subscriptions.Nats.csproj", "{560AAB82-9530-4A59-9C68-A9C8C586A5AA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Subscriptions.Nats.Tests", "test\Subscriptions.Nats.Tests\HotChocolate.Subscriptions.Nats.Tests.csproj", "{DD1057EF-C9AE-42B9-B56D-DC49013D175D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Subscriptions.Serializers.Newtonsoft", "src\Subscriptions.Serializers.Newtonsoft\HotChocolate.Subscriptions.Serializers.Newtonsoft.csproj", "{7FCCCA0F-FE75-4847-B464-982C02661F07}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Execution.Abstractions", "src\Execution.Abstractions\HotChocolate.Execution.Abstractions.csproj", "{52E5E2D0-7DD2-429F-8D3F-805648332D66}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Subscriptions.Tests", "test\Subscriptions.Tests\HotChocolate.Subscriptions.Tests.csproj", "{0B08ACE1-F3BF-4375-9381-22ACA1ECD387}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Subscriptions.RabbitMQ", "src\Subscriptions.RabbitMQ\HotChocolate.Subscriptions.RabbitMQ.csproj", "{E541AEB3-45D8-4089-A2BD-45859999A30D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Subscriptions.RabbitMQ.Tests", "test\Subscriptions.RabbitMQ.Tests\HotChocolate.Subscriptions.RabbitMQ.Tests.csproj", "{EDA108F7-2C3D-4C7B-A1C1-286D6FB87761}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Authorization", "src\Authorization\HotChocolate.Authorization.csproj", "{30214E4D-7E4E-4C98-B489-FCB0A16DF2C4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Authorization.Tests", "test\Authorization.Tests\HotChocolate.Authorization.Tests.csproj", "{83926ADB-A63C-4575-AC99-2D420FFE32CA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Language.Web", "..\Language\src\Language.Web\HotChocolate.Language.Web.csproj", "{2A8E659F-3C92-4C5F-BB82-5EE91C15123A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CookieCrumble", "..\..\CookieCrumble\src\CookieCrumble\CookieCrumble.csproj", "{A26F7BAB-493A-45F4-AB0C-86BE84B20DBA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Fusion", "..\Fusion\src\Core\HotChocolate.Fusion.csproj", "{0638755B-FA20-4A94-98D4-396AFAA5789F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Fusion.Abstractions", "..\Fusion\src\Abstractions\HotChocolate.Fusion.Abstractions.csproj", "{FA37C0D9-306B-42E9-890E-73BED531C091}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Transport.Sockets.Client", "..\AspNetCore\src\Transport.Sockets.Client\HotChocolate.Transport.Sockets.Client.csproj", "{317C2668-0920-492A-90D7-FF9670344DEE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Transport.Sockets", "..\AspNetCore\src\Transport.Sockets\HotChocolate.Transport.Sockets.csproj", "{AD65B505-8EDE-4B54-9298-6B71D0283EB1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Subscriptions.Postgres.Tests", "test\Subscriptions.Postgres.Tests\HotChocolate.Subscriptions.Postgres.Tests.csproj", "{AA04790E-58AC-40CC-9DEC-570D78D252AD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Subscriptions.Postgres", "src\Subscriptions.Postgres\HotChocolate.Subscriptions.Postgres.csproj", "{B98BC881-DDDD-4988-AC43-187C733895CD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Transport.Http", "..\AspNetCore\src\Transport.Http\HotChocolate.Transport.Http.csproj", "{D1B67116-DB1C-4CE7-954F-AD9E17C06061}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotChocolate.Transport.Abstractions", "..\AspNetCore\src\Transport.Abstractions\HotChocolate.Transport.Abstractions.csproj", "{08D371ED-04DE-4CE3-AD1B-F6CC6D341706}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types.Shared", "src\Types.Shared\HotChocolate.Types.Shared.csproj", "{E5B248B8-A2E1-46CB-9281-D5871F1483C3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types.Errors", "src\Types.Errors\HotChocolate.Types.Errors.csproj", "{78979585-F881-4ACD-9E83-CCB866EB971C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types.Queries", "src\Types.Queries\HotChocolate.Types.Queries.csproj", "{655739D7-87E9-43EF-B522-AC421F7451A4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types.Queries.Tests", "test\Types.Queries.Tests\HotChocolate.Types.Queries.Tests.csproj", "{AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Features", "src\Features\HotChocolate.Features.csproj", "{669FA147-3B41-4841-921A-55B019C3AF26}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Features.Tests", "test\Features.Tests\HotChocolate.Features.Tests.csproj", "{EA77D317-8767-4DDE-8038-820D582C52D6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Execution.Projections", "src\Execution.Projections\HotChocolate.Execution.Projections.csproj", "{68726F89-B254-424B-BE17-AC9312484464}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Types.Analyzer.Integration.Tests", "Types.Analyzer.Integration.Tests", "{BB753F80-E4F3-4510-2AE6-4DF7F004941D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types.Analyzers.Tests", "test\Types.Analyzer.Integration.Tests\HotChocolate.Types.Analyzers.Tests.csproj", "{5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {015D2107-B5CD-4A45-A650-B29AECC6FBE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {015D2107-B5CD-4A45-A650-B29AECC6FBE3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {015D2107-B5CD-4A45-A650-B29AECC6FBE3}.Debug|x64.ActiveCfg = Debug|Any CPU - {015D2107-B5CD-4A45-A650-B29AECC6FBE3}.Debug|x64.Build.0 = Debug|Any CPU - {015D2107-B5CD-4A45-A650-B29AECC6FBE3}.Debug|x86.ActiveCfg = Debug|Any CPU - {015D2107-B5CD-4A45-A650-B29AECC6FBE3}.Debug|x86.Build.0 = Debug|Any CPU - {015D2107-B5CD-4A45-A650-B29AECC6FBE3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {015D2107-B5CD-4A45-A650-B29AECC6FBE3}.Release|Any CPU.Build.0 = Release|Any CPU - {015D2107-B5CD-4A45-A650-B29AECC6FBE3}.Release|x64.ActiveCfg = Release|Any CPU - {015D2107-B5CD-4A45-A650-B29AECC6FBE3}.Release|x64.Build.0 = Release|Any CPU - {015D2107-B5CD-4A45-A650-B29AECC6FBE3}.Release|x86.ActiveCfg = Release|Any CPU - {015D2107-B5CD-4A45-A650-B29AECC6FBE3}.Release|x86.Build.0 = Release|Any CPU - {38EB59B2-A76B-48F7-B99D-C99FB7793903}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38EB59B2-A76B-48F7-B99D-C99FB7793903}.Debug|Any CPU.Build.0 = Debug|Any CPU - {38EB59B2-A76B-48F7-B99D-C99FB7793903}.Debug|x64.ActiveCfg = Debug|Any CPU - {38EB59B2-A76B-48F7-B99D-C99FB7793903}.Debug|x64.Build.0 = Debug|Any CPU - {38EB59B2-A76B-48F7-B99D-C99FB7793903}.Debug|x86.ActiveCfg = Debug|Any CPU - {38EB59B2-A76B-48F7-B99D-C99FB7793903}.Debug|x86.Build.0 = Debug|Any CPU - {38EB59B2-A76B-48F7-B99D-C99FB7793903}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38EB59B2-A76B-48F7-B99D-C99FB7793903}.Release|Any CPU.Build.0 = Release|Any CPU - {38EB59B2-A76B-48F7-B99D-C99FB7793903}.Release|x64.ActiveCfg = Release|Any CPU - {38EB59B2-A76B-48F7-B99D-C99FB7793903}.Release|x64.Build.0 = Release|Any CPU - {38EB59B2-A76B-48F7-B99D-C99FB7793903}.Release|x86.ActiveCfg = Release|Any CPU - {38EB59B2-A76B-48F7-B99D-C99FB7793903}.Release|x86.Build.0 = Release|Any CPU - {AFCDB23E-7988-4BE7-BD1A-23912AB1FCAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AFCDB23E-7988-4BE7-BD1A-23912AB1FCAE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AFCDB23E-7988-4BE7-BD1A-23912AB1FCAE}.Debug|x64.ActiveCfg = Debug|Any CPU - {AFCDB23E-7988-4BE7-BD1A-23912AB1FCAE}.Debug|x64.Build.0 = Debug|Any CPU - {AFCDB23E-7988-4BE7-BD1A-23912AB1FCAE}.Debug|x86.ActiveCfg = Debug|Any CPU - {AFCDB23E-7988-4BE7-BD1A-23912AB1FCAE}.Debug|x86.Build.0 = Debug|Any CPU - {AFCDB23E-7988-4BE7-BD1A-23912AB1FCAE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AFCDB23E-7988-4BE7-BD1A-23912AB1FCAE}.Release|Any CPU.Build.0 = Release|Any CPU - {AFCDB23E-7988-4BE7-BD1A-23912AB1FCAE}.Release|x64.ActiveCfg = Release|Any CPU - {AFCDB23E-7988-4BE7-BD1A-23912AB1FCAE}.Release|x64.Build.0 = Release|Any CPU - {AFCDB23E-7988-4BE7-BD1A-23912AB1FCAE}.Release|x86.ActiveCfg = Release|Any CPU - {AFCDB23E-7988-4BE7-BD1A-23912AB1FCAE}.Release|x86.Build.0 = Release|Any CPU - {BEC2D863-C095-4D83-8EDF-7B8AED26A834}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BEC2D863-C095-4D83-8EDF-7B8AED26A834}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BEC2D863-C095-4D83-8EDF-7B8AED26A834}.Debug|x64.ActiveCfg = Debug|Any CPU - {BEC2D863-C095-4D83-8EDF-7B8AED26A834}.Debug|x64.Build.0 = Debug|Any CPU - {BEC2D863-C095-4D83-8EDF-7B8AED26A834}.Debug|x86.ActiveCfg = Debug|Any CPU - {BEC2D863-C095-4D83-8EDF-7B8AED26A834}.Debug|x86.Build.0 = Debug|Any CPU - {BEC2D863-C095-4D83-8EDF-7B8AED26A834}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BEC2D863-C095-4D83-8EDF-7B8AED26A834}.Release|Any CPU.Build.0 = Release|Any CPU - {BEC2D863-C095-4D83-8EDF-7B8AED26A834}.Release|x64.ActiveCfg = Release|Any CPU - {BEC2D863-C095-4D83-8EDF-7B8AED26A834}.Release|x64.Build.0 = Release|Any CPU - {BEC2D863-C095-4D83-8EDF-7B8AED26A834}.Release|x86.ActiveCfg = Release|Any CPU - {BEC2D863-C095-4D83-8EDF-7B8AED26A834}.Release|x86.Build.0 = Release|Any CPU - {7B9BE76A-1472-4028-BB8C-6C0C54342222}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7B9BE76A-1472-4028-BB8C-6C0C54342222}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7B9BE76A-1472-4028-BB8C-6C0C54342222}.Debug|x64.ActiveCfg = Debug|Any CPU - {7B9BE76A-1472-4028-BB8C-6C0C54342222}.Debug|x64.Build.0 = Debug|Any CPU - {7B9BE76A-1472-4028-BB8C-6C0C54342222}.Debug|x86.ActiveCfg = Debug|Any CPU - {7B9BE76A-1472-4028-BB8C-6C0C54342222}.Debug|x86.Build.0 = Debug|Any CPU - {7B9BE76A-1472-4028-BB8C-6C0C54342222}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7B9BE76A-1472-4028-BB8C-6C0C54342222}.Release|Any CPU.Build.0 = Release|Any CPU - {7B9BE76A-1472-4028-BB8C-6C0C54342222}.Release|x64.ActiveCfg = Release|Any CPU - {7B9BE76A-1472-4028-BB8C-6C0C54342222}.Release|x64.Build.0 = Release|Any CPU - {7B9BE76A-1472-4028-BB8C-6C0C54342222}.Release|x86.ActiveCfg = Release|Any CPU - {7B9BE76A-1472-4028-BB8C-6C0C54342222}.Release|x86.Build.0 = Release|Any CPU - {52631D27-685E-4D2B-92D7-0D53CAF2DD82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {52631D27-685E-4D2B-92D7-0D53CAF2DD82}.Debug|Any CPU.Build.0 = Debug|Any CPU - {52631D27-685E-4D2B-92D7-0D53CAF2DD82}.Debug|x64.ActiveCfg = Debug|Any CPU - {52631D27-685E-4D2B-92D7-0D53CAF2DD82}.Debug|x64.Build.0 = Debug|Any CPU - {52631D27-685E-4D2B-92D7-0D53CAF2DD82}.Debug|x86.ActiveCfg = Debug|Any CPU - {52631D27-685E-4D2B-92D7-0D53CAF2DD82}.Debug|x86.Build.0 = Debug|Any CPU - {52631D27-685E-4D2B-92D7-0D53CAF2DD82}.Release|Any CPU.ActiveCfg = Release|Any CPU - {52631D27-685E-4D2B-92D7-0D53CAF2DD82}.Release|Any CPU.Build.0 = Release|Any CPU - {52631D27-685E-4D2B-92D7-0D53CAF2DD82}.Release|x64.ActiveCfg = Release|Any CPU - {52631D27-685E-4D2B-92D7-0D53CAF2DD82}.Release|x64.Build.0 = Release|Any CPU - {52631D27-685E-4D2B-92D7-0D53CAF2DD82}.Release|x86.ActiveCfg = Release|Any CPU - {52631D27-685E-4D2B-92D7-0D53CAF2DD82}.Release|x86.Build.0 = Release|Any CPU - {A8D2A7D1-483F-4FC3-867B-DB6E0DA95EF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A8D2A7D1-483F-4FC3-867B-DB6E0DA95EF5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A8D2A7D1-483F-4FC3-867B-DB6E0DA95EF5}.Debug|x64.ActiveCfg = Debug|Any CPU - {A8D2A7D1-483F-4FC3-867B-DB6E0DA95EF5}.Debug|x64.Build.0 = Debug|Any CPU - {A8D2A7D1-483F-4FC3-867B-DB6E0DA95EF5}.Debug|x86.ActiveCfg = Debug|Any CPU - {A8D2A7D1-483F-4FC3-867B-DB6E0DA95EF5}.Debug|x86.Build.0 = Debug|Any CPU - {A8D2A7D1-483F-4FC3-867B-DB6E0DA95EF5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A8D2A7D1-483F-4FC3-867B-DB6E0DA95EF5}.Release|Any CPU.Build.0 = Release|Any CPU - {A8D2A7D1-483F-4FC3-867B-DB6E0DA95EF5}.Release|x64.ActiveCfg = Release|Any CPU - {A8D2A7D1-483F-4FC3-867B-DB6E0DA95EF5}.Release|x64.Build.0 = Release|Any CPU - {A8D2A7D1-483F-4FC3-867B-DB6E0DA95EF5}.Release|x86.ActiveCfg = Release|Any CPU - {A8D2A7D1-483F-4FC3-867B-DB6E0DA95EF5}.Release|x86.Build.0 = Release|Any CPU - {ED81B543-714A-484C-ACB9-CF70308B045F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ED81B543-714A-484C-ACB9-CF70308B045F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ED81B543-714A-484C-ACB9-CF70308B045F}.Debug|x64.ActiveCfg = Debug|Any CPU - {ED81B543-714A-484C-ACB9-CF70308B045F}.Debug|x64.Build.0 = Debug|Any CPU - {ED81B543-714A-484C-ACB9-CF70308B045F}.Debug|x86.ActiveCfg = Debug|Any CPU - {ED81B543-714A-484C-ACB9-CF70308B045F}.Debug|x86.Build.0 = Debug|Any CPU - {ED81B543-714A-484C-ACB9-CF70308B045F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ED81B543-714A-484C-ACB9-CF70308B045F}.Release|Any CPU.Build.0 = Release|Any CPU - {ED81B543-714A-484C-ACB9-CF70308B045F}.Release|x64.ActiveCfg = Release|Any CPU - {ED81B543-714A-484C-ACB9-CF70308B045F}.Release|x64.Build.0 = Release|Any CPU - {ED81B543-714A-484C-ACB9-CF70308B045F}.Release|x86.ActiveCfg = Release|Any CPU - {ED81B543-714A-484C-ACB9-CF70308B045F}.Release|x86.Build.0 = Release|Any CPU - {BBD6E614-37A8-4E86-88A7-2CE8821C7CB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BBD6E614-37A8-4E86-88A7-2CE8821C7CB2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BBD6E614-37A8-4E86-88A7-2CE8821C7CB2}.Debug|x64.ActiveCfg = Debug|Any CPU - {BBD6E614-37A8-4E86-88A7-2CE8821C7CB2}.Debug|x64.Build.0 = Debug|Any CPU - {BBD6E614-37A8-4E86-88A7-2CE8821C7CB2}.Debug|x86.ActiveCfg = Debug|Any CPU - {BBD6E614-37A8-4E86-88A7-2CE8821C7CB2}.Debug|x86.Build.0 = Debug|Any CPU - {BBD6E614-37A8-4E86-88A7-2CE8821C7CB2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BBD6E614-37A8-4E86-88A7-2CE8821C7CB2}.Release|Any CPU.Build.0 = Release|Any CPU - {BBD6E614-37A8-4E86-88A7-2CE8821C7CB2}.Release|x64.ActiveCfg = Release|Any CPU - {BBD6E614-37A8-4E86-88A7-2CE8821C7CB2}.Release|x64.Build.0 = Release|Any CPU - {BBD6E614-37A8-4E86-88A7-2CE8821C7CB2}.Release|x86.ActiveCfg = Release|Any CPU - {BBD6E614-37A8-4E86-88A7-2CE8821C7CB2}.Release|x86.Build.0 = Release|Any CPU - {350488E5-F800-4CBD-9278-1D5C84C78958}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {350488E5-F800-4CBD-9278-1D5C84C78958}.Debug|Any CPU.Build.0 = Debug|Any CPU - {350488E5-F800-4CBD-9278-1D5C84C78958}.Debug|x64.ActiveCfg = Debug|Any CPU - {350488E5-F800-4CBD-9278-1D5C84C78958}.Debug|x64.Build.0 = Debug|Any CPU - {350488E5-F800-4CBD-9278-1D5C84C78958}.Debug|x86.ActiveCfg = Debug|Any CPU - {350488E5-F800-4CBD-9278-1D5C84C78958}.Debug|x86.Build.0 = Debug|Any CPU - {350488E5-F800-4CBD-9278-1D5C84C78958}.Release|Any CPU.ActiveCfg = Release|Any CPU - {350488E5-F800-4CBD-9278-1D5C84C78958}.Release|Any CPU.Build.0 = Release|Any CPU - {350488E5-F800-4CBD-9278-1D5C84C78958}.Release|x64.ActiveCfg = Release|Any CPU - {350488E5-F800-4CBD-9278-1D5C84C78958}.Release|x64.Build.0 = Release|Any CPU - {350488E5-F800-4CBD-9278-1D5C84C78958}.Release|x86.ActiveCfg = Release|Any CPU - {350488E5-F800-4CBD-9278-1D5C84C78958}.Release|x86.Build.0 = Release|Any CPU - {CF768605-BAB8-42D8-A748-66791575BCFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CF768605-BAB8-42D8-A748-66791575BCFD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CF768605-BAB8-42D8-A748-66791575BCFD}.Debug|x64.ActiveCfg = Debug|Any CPU - {CF768605-BAB8-42D8-A748-66791575BCFD}.Debug|x64.Build.0 = Debug|Any CPU - {CF768605-BAB8-42D8-A748-66791575BCFD}.Debug|x86.ActiveCfg = Debug|Any CPU - {CF768605-BAB8-42D8-A748-66791575BCFD}.Debug|x86.Build.0 = Debug|Any CPU - {CF768605-BAB8-42D8-A748-66791575BCFD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CF768605-BAB8-42D8-A748-66791575BCFD}.Release|Any CPU.Build.0 = Release|Any CPU - {CF768605-BAB8-42D8-A748-66791575BCFD}.Release|x64.ActiveCfg = Release|Any CPU - {CF768605-BAB8-42D8-A748-66791575BCFD}.Release|x64.Build.0 = Release|Any CPU - {CF768605-BAB8-42D8-A748-66791575BCFD}.Release|x86.ActiveCfg = Release|Any CPU - {CF768605-BAB8-42D8-A748-66791575BCFD}.Release|x86.Build.0 = Release|Any CPU - {35C5F3EF-B192-4A07-B664-A3D360283907}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {35C5F3EF-B192-4A07-B664-A3D360283907}.Debug|Any CPU.Build.0 = Debug|Any CPU - {35C5F3EF-B192-4A07-B664-A3D360283907}.Debug|x64.ActiveCfg = Debug|Any CPU - {35C5F3EF-B192-4A07-B664-A3D360283907}.Debug|x64.Build.0 = Debug|Any CPU - {35C5F3EF-B192-4A07-B664-A3D360283907}.Debug|x86.ActiveCfg = Debug|Any CPU - {35C5F3EF-B192-4A07-B664-A3D360283907}.Debug|x86.Build.0 = Debug|Any CPU - {35C5F3EF-B192-4A07-B664-A3D360283907}.Release|Any CPU.ActiveCfg = Release|Any CPU - {35C5F3EF-B192-4A07-B664-A3D360283907}.Release|Any CPU.Build.0 = Release|Any CPU - {35C5F3EF-B192-4A07-B664-A3D360283907}.Release|x64.ActiveCfg = Release|Any CPU - {35C5F3EF-B192-4A07-B664-A3D360283907}.Release|x64.Build.0 = Release|Any CPU - {35C5F3EF-B192-4A07-B664-A3D360283907}.Release|x86.ActiveCfg = Release|Any CPU - {35C5F3EF-B192-4A07-B664-A3D360283907}.Release|x86.Build.0 = Release|Any CPU - {29FEE730-0D2C-4F31-934E-9BE2E9DC729A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {29FEE730-0D2C-4F31-934E-9BE2E9DC729A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {29FEE730-0D2C-4F31-934E-9BE2E9DC729A}.Debug|x64.ActiveCfg = Debug|Any CPU - {29FEE730-0D2C-4F31-934E-9BE2E9DC729A}.Debug|x64.Build.0 = Debug|Any CPU - {29FEE730-0D2C-4F31-934E-9BE2E9DC729A}.Debug|x86.ActiveCfg = Debug|Any CPU - {29FEE730-0D2C-4F31-934E-9BE2E9DC729A}.Debug|x86.Build.0 = Debug|Any CPU - {29FEE730-0D2C-4F31-934E-9BE2E9DC729A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {29FEE730-0D2C-4F31-934E-9BE2E9DC729A}.Release|Any CPU.Build.0 = Release|Any CPU - {29FEE730-0D2C-4F31-934E-9BE2E9DC729A}.Release|x64.ActiveCfg = Release|Any CPU - {29FEE730-0D2C-4F31-934E-9BE2E9DC729A}.Release|x64.Build.0 = Release|Any CPU - {29FEE730-0D2C-4F31-934E-9BE2E9DC729A}.Release|x86.ActiveCfg = Release|Any CPU - {29FEE730-0D2C-4F31-934E-9BE2E9DC729A}.Release|x86.Build.0 = Release|Any CPU - {80F8703D-804D-4921-A409-AF4BB26E59BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {80F8703D-804D-4921-A409-AF4BB26E59BE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {80F8703D-804D-4921-A409-AF4BB26E59BE}.Debug|x64.ActiveCfg = Debug|Any CPU - {80F8703D-804D-4921-A409-AF4BB26E59BE}.Debug|x64.Build.0 = Debug|Any CPU - {80F8703D-804D-4921-A409-AF4BB26E59BE}.Debug|x86.ActiveCfg = Debug|Any CPU - {80F8703D-804D-4921-A409-AF4BB26E59BE}.Debug|x86.Build.0 = Debug|Any CPU - {80F8703D-804D-4921-A409-AF4BB26E59BE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {80F8703D-804D-4921-A409-AF4BB26E59BE}.Release|Any CPU.Build.0 = Release|Any CPU - {80F8703D-804D-4921-A409-AF4BB26E59BE}.Release|x64.ActiveCfg = Release|Any CPU - {80F8703D-804D-4921-A409-AF4BB26E59BE}.Release|x64.Build.0 = Release|Any CPU - {80F8703D-804D-4921-A409-AF4BB26E59BE}.Release|x86.ActiveCfg = Release|Any CPU - {80F8703D-804D-4921-A409-AF4BB26E59BE}.Release|x86.Build.0 = Release|Any CPU - {AADFF9B1-B275-48B4-8F32-F6CD837619E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AADFF9B1-B275-48B4-8F32-F6CD837619E5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AADFF9B1-B275-48B4-8F32-F6CD837619E5}.Debug|x64.ActiveCfg = Debug|Any CPU - {AADFF9B1-B275-48B4-8F32-F6CD837619E5}.Debug|x64.Build.0 = Debug|Any CPU - {AADFF9B1-B275-48B4-8F32-F6CD837619E5}.Debug|x86.ActiveCfg = Debug|Any CPU - {AADFF9B1-B275-48B4-8F32-F6CD837619E5}.Debug|x86.Build.0 = Debug|Any CPU - {AADFF9B1-B275-48B4-8F32-F6CD837619E5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AADFF9B1-B275-48B4-8F32-F6CD837619E5}.Release|Any CPU.Build.0 = Release|Any CPU - {AADFF9B1-B275-48B4-8F32-F6CD837619E5}.Release|x64.ActiveCfg = Release|Any CPU - {AADFF9B1-B275-48B4-8F32-F6CD837619E5}.Release|x64.Build.0 = Release|Any CPU - {AADFF9B1-B275-48B4-8F32-F6CD837619E5}.Release|x86.ActiveCfg = Release|Any CPU - {AADFF9B1-B275-48B4-8F32-F6CD837619E5}.Release|x86.Build.0 = Release|Any CPU - {CD2B383F-2957-4A83-B965-070DC8CF36F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CD2B383F-2957-4A83-B965-070DC8CF36F6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CD2B383F-2957-4A83-B965-070DC8CF36F6}.Debug|x64.ActiveCfg = Debug|Any CPU - {CD2B383F-2957-4A83-B965-070DC8CF36F6}.Debug|x64.Build.0 = Debug|Any CPU - {CD2B383F-2957-4A83-B965-070DC8CF36F6}.Debug|x86.ActiveCfg = Debug|Any CPU - {CD2B383F-2957-4A83-B965-070DC8CF36F6}.Debug|x86.Build.0 = Debug|Any CPU - {CD2B383F-2957-4A83-B965-070DC8CF36F6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CD2B383F-2957-4A83-B965-070DC8CF36F6}.Release|Any CPU.Build.0 = Release|Any CPU - {CD2B383F-2957-4A83-B965-070DC8CF36F6}.Release|x64.ActiveCfg = Release|Any CPU - {CD2B383F-2957-4A83-B965-070DC8CF36F6}.Release|x64.Build.0 = Release|Any CPU - {CD2B383F-2957-4A83-B965-070DC8CF36F6}.Release|x86.ActiveCfg = Release|Any CPU - {CD2B383F-2957-4A83-B965-070DC8CF36F6}.Release|x86.Build.0 = Release|Any CPU - {3DFDEFBE-D2BD-4F25-8A26-66AFB62B9C4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3DFDEFBE-D2BD-4F25-8A26-66AFB62B9C4C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3DFDEFBE-D2BD-4F25-8A26-66AFB62B9C4C}.Debug|x64.ActiveCfg = Debug|Any CPU - {3DFDEFBE-D2BD-4F25-8A26-66AFB62B9C4C}.Debug|x64.Build.0 = Debug|Any CPU - {3DFDEFBE-D2BD-4F25-8A26-66AFB62B9C4C}.Debug|x86.ActiveCfg = Debug|Any CPU - {3DFDEFBE-D2BD-4F25-8A26-66AFB62B9C4C}.Debug|x86.Build.0 = Debug|Any CPU - {3DFDEFBE-D2BD-4F25-8A26-66AFB62B9C4C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3DFDEFBE-D2BD-4F25-8A26-66AFB62B9C4C}.Release|Any CPU.Build.0 = Release|Any CPU - {3DFDEFBE-D2BD-4F25-8A26-66AFB62B9C4C}.Release|x64.ActiveCfg = Release|Any CPU - {3DFDEFBE-D2BD-4F25-8A26-66AFB62B9C4C}.Release|x64.Build.0 = Release|Any CPU - {3DFDEFBE-D2BD-4F25-8A26-66AFB62B9C4C}.Release|x86.ActiveCfg = Release|Any CPU - {3DFDEFBE-D2BD-4F25-8A26-66AFB62B9C4C}.Release|x86.Build.0 = Release|Any CPU - {C08D0A27-6F28-4845-91E7-5C1A0C926939}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C08D0A27-6F28-4845-91E7-5C1A0C926939}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C08D0A27-6F28-4845-91E7-5C1A0C926939}.Debug|x64.ActiveCfg = Debug|Any CPU - {C08D0A27-6F28-4845-91E7-5C1A0C926939}.Debug|x64.Build.0 = Debug|Any CPU - {C08D0A27-6F28-4845-91E7-5C1A0C926939}.Debug|x86.ActiveCfg = Debug|Any CPU - {C08D0A27-6F28-4845-91E7-5C1A0C926939}.Debug|x86.Build.0 = Debug|Any CPU - {C08D0A27-6F28-4845-91E7-5C1A0C926939}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C08D0A27-6F28-4845-91E7-5C1A0C926939}.Release|Any CPU.Build.0 = Release|Any CPU - {C08D0A27-6F28-4845-91E7-5C1A0C926939}.Release|x64.ActiveCfg = Release|Any CPU - {C08D0A27-6F28-4845-91E7-5C1A0C926939}.Release|x64.Build.0 = Release|Any CPU - {C08D0A27-6F28-4845-91E7-5C1A0C926939}.Release|x86.ActiveCfg = Release|Any CPU - {C08D0A27-6F28-4845-91E7-5C1A0C926939}.Release|x86.Build.0 = Release|Any CPU - {39592692-847D-49F0-97DE-3F2F21677319}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {39592692-847D-49F0-97DE-3F2F21677319}.Debug|Any CPU.Build.0 = Debug|Any CPU - {39592692-847D-49F0-97DE-3F2F21677319}.Debug|x64.ActiveCfg = Debug|Any CPU - {39592692-847D-49F0-97DE-3F2F21677319}.Debug|x64.Build.0 = Debug|Any CPU - {39592692-847D-49F0-97DE-3F2F21677319}.Debug|x86.ActiveCfg = Debug|Any CPU - {39592692-847D-49F0-97DE-3F2F21677319}.Debug|x86.Build.0 = Debug|Any CPU - {39592692-847D-49F0-97DE-3F2F21677319}.Release|Any CPU.ActiveCfg = Release|Any CPU - {39592692-847D-49F0-97DE-3F2F21677319}.Release|Any CPU.Build.0 = Release|Any CPU - {39592692-847D-49F0-97DE-3F2F21677319}.Release|x64.ActiveCfg = Release|Any CPU - {39592692-847D-49F0-97DE-3F2F21677319}.Release|x64.Build.0 = Release|Any CPU - {39592692-847D-49F0-97DE-3F2F21677319}.Release|x86.ActiveCfg = Release|Any CPU - {39592692-847D-49F0-97DE-3F2F21677319}.Release|x86.Build.0 = Release|Any CPU - {E2574A41-D9BF-4071-84D0-DB0F6D1148EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E2574A41-D9BF-4071-84D0-DB0F6D1148EE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E2574A41-D9BF-4071-84D0-DB0F6D1148EE}.Debug|x64.ActiveCfg = Debug|Any CPU - {E2574A41-D9BF-4071-84D0-DB0F6D1148EE}.Debug|x64.Build.0 = Debug|Any CPU - {E2574A41-D9BF-4071-84D0-DB0F6D1148EE}.Debug|x86.ActiveCfg = Debug|Any CPU - {E2574A41-D9BF-4071-84D0-DB0F6D1148EE}.Debug|x86.Build.0 = Debug|Any CPU - {E2574A41-D9BF-4071-84D0-DB0F6D1148EE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E2574A41-D9BF-4071-84D0-DB0F6D1148EE}.Release|Any CPU.Build.0 = Release|Any CPU - {E2574A41-D9BF-4071-84D0-DB0F6D1148EE}.Release|x64.ActiveCfg = Release|Any CPU - {E2574A41-D9BF-4071-84D0-DB0F6D1148EE}.Release|x64.Build.0 = Release|Any CPU - {E2574A41-D9BF-4071-84D0-DB0F6D1148EE}.Release|x86.ActiveCfg = Release|Any CPU - {E2574A41-D9BF-4071-84D0-DB0F6D1148EE}.Release|x86.Build.0 = Release|Any CPU - {1D3C84AE-2818-4873-A222-BE6D329E850D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1D3C84AE-2818-4873-A222-BE6D329E850D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1D3C84AE-2818-4873-A222-BE6D329E850D}.Debug|x64.ActiveCfg = Debug|Any CPU - {1D3C84AE-2818-4873-A222-BE6D329E850D}.Debug|x64.Build.0 = Debug|Any CPU - {1D3C84AE-2818-4873-A222-BE6D329E850D}.Debug|x86.ActiveCfg = Debug|Any CPU - {1D3C84AE-2818-4873-A222-BE6D329E850D}.Debug|x86.Build.0 = Debug|Any CPU - {1D3C84AE-2818-4873-A222-BE6D329E850D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1D3C84AE-2818-4873-A222-BE6D329E850D}.Release|Any CPU.Build.0 = Release|Any CPU - {1D3C84AE-2818-4873-A222-BE6D329E850D}.Release|x64.ActiveCfg = Release|Any CPU - {1D3C84AE-2818-4873-A222-BE6D329E850D}.Release|x64.Build.0 = Release|Any CPU - {1D3C84AE-2818-4873-A222-BE6D329E850D}.Release|x86.ActiveCfg = Release|Any CPU - {1D3C84AE-2818-4873-A222-BE6D329E850D}.Release|x86.Build.0 = Release|Any CPU - {3ED68361-0D3D-45F5-8A83-444DD3D5C1BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3ED68361-0D3D-45F5-8A83-444DD3D5C1BF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3ED68361-0D3D-45F5-8A83-444DD3D5C1BF}.Debug|x64.ActiveCfg = Debug|Any CPU - {3ED68361-0D3D-45F5-8A83-444DD3D5C1BF}.Debug|x64.Build.0 = Debug|Any CPU - {3ED68361-0D3D-45F5-8A83-444DD3D5C1BF}.Debug|x86.ActiveCfg = Debug|Any CPU - {3ED68361-0D3D-45F5-8A83-444DD3D5C1BF}.Debug|x86.Build.0 = Debug|Any CPU - {3ED68361-0D3D-45F5-8A83-444DD3D5C1BF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3ED68361-0D3D-45F5-8A83-444DD3D5C1BF}.Release|Any CPU.Build.0 = Release|Any CPU - {3ED68361-0D3D-45F5-8A83-444DD3D5C1BF}.Release|x64.ActiveCfg = Release|Any CPU - {3ED68361-0D3D-45F5-8A83-444DD3D5C1BF}.Release|x64.Build.0 = Release|Any CPU - {3ED68361-0D3D-45F5-8A83-444DD3D5C1BF}.Release|x86.ActiveCfg = Release|Any CPU - {3ED68361-0D3D-45F5-8A83-444DD3D5C1BF}.Release|x86.Build.0 = Release|Any CPU - {14648C7D-8958-4C94-B837-7F90F2F10081}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {14648C7D-8958-4C94-B837-7F90F2F10081}.Debug|Any CPU.Build.0 = Debug|Any CPU - {14648C7D-8958-4C94-B837-7F90F2F10081}.Debug|x64.ActiveCfg = Debug|Any CPU - {14648C7D-8958-4C94-B837-7F90F2F10081}.Debug|x64.Build.0 = Debug|Any CPU - {14648C7D-8958-4C94-B837-7F90F2F10081}.Debug|x86.ActiveCfg = Debug|Any CPU - {14648C7D-8958-4C94-B837-7F90F2F10081}.Debug|x86.Build.0 = Debug|Any CPU - {14648C7D-8958-4C94-B837-7F90F2F10081}.Release|Any CPU.ActiveCfg = Release|Any CPU - {14648C7D-8958-4C94-B837-7F90F2F10081}.Release|Any CPU.Build.0 = Release|Any CPU - {14648C7D-8958-4C94-B837-7F90F2F10081}.Release|x64.ActiveCfg = Release|Any CPU - {14648C7D-8958-4C94-B837-7F90F2F10081}.Release|x64.Build.0 = Release|Any CPU - {14648C7D-8958-4C94-B837-7F90F2F10081}.Release|x86.ActiveCfg = Release|Any CPU - {14648C7D-8958-4C94-B837-7F90F2F10081}.Release|x86.Build.0 = Release|Any CPU - {F72E72DE-F163-4BCC-A442-94DB331D3C35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F72E72DE-F163-4BCC-A442-94DB331D3C35}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F72E72DE-F163-4BCC-A442-94DB331D3C35}.Debug|x64.ActiveCfg = Debug|Any CPU - {F72E72DE-F163-4BCC-A442-94DB331D3C35}.Debug|x64.Build.0 = Debug|Any CPU - {F72E72DE-F163-4BCC-A442-94DB331D3C35}.Debug|x86.ActiveCfg = Debug|Any CPU - {F72E72DE-F163-4BCC-A442-94DB331D3C35}.Debug|x86.Build.0 = Debug|Any CPU - {F72E72DE-F163-4BCC-A442-94DB331D3C35}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F72E72DE-F163-4BCC-A442-94DB331D3C35}.Release|Any CPU.Build.0 = Release|Any CPU - {F72E72DE-F163-4BCC-A442-94DB331D3C35}.Release|x64.ActiveCfg = Release|Any CPU - {F72E72DE-F163-4BCC-A442-94DB331D3C35}.Release|x64.Build.0 = Release|Any CPU - {F72E72DE-F163-4BCC-A442-94DB331D3C35}.Release|x86.ActiveCfg = Release|Any CPU - {F72E72DE-F163-4BCC-A442-94DB331D3C35}.Release|x86.Build.0 = Release|Any CPU - {2A319702-6C2E-4184-8E71-43F87745B38D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2A319702-6C2E-4184-8E71-43F87745B38D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2A319702-6C2E-4184-8E71-43F87745B38D}.Debug|x64.ActiveCfg = Debug|Any CPU - {2A319702-6C2E-4184-8E71-43F87745B38D}.Debug|x64.Build.0 = Debug|Any CPU - {2A319702-6C2E-4184-8E71-43F87745B38D}.Debug|x86.ActiveCfg = Debug|Any CPU - {2A319702-6C2E-4184-8E71-43F87745B38D}.Debug|x86.Build.0 = Debug|Any CPU - {2A319702-6C2E-4184-8E71-43F87745B38D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2A319702-6C2E-4184-8E71-43F87745B38D}.Release|Any CPU.Build.0 = Release|Any CPU - {2A319702-6C2E-4184-8E71-43F87745B38D}.Release|x64.ActiveCfg = Release|Any CPU - {2A319702-6C2E-4184-8E71-43F87745B38D}.Release|x64.Build.0 = Release|Any CPU - {2A319702-6C2E-4184-8E71-43F87745B38D}.Release|x86.ActiveCfg = Release|Any CPU - {2A319702-6C2E-4184-8E71-43F87745B38D}.Release|x86.Build.0 = Release|Any CPU - {91F00C73-78E5-4400-8F9F-910BE99EDFD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {91F00C73-78E5-4400-8F9F-910BE99EDFD5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {91F00C73-78E5-4400-8F9F-910BE99EDFD5}.Debug|x64.ActiveCfg = Debug|Any CPU - {91F00C73-78E5-4400-8F9F-910BE99EDFD5}.Debug|x64.Build.0 = Debug|Any CPU - {91F00C73-78E5-4400-8F9F-910BE99EDFD5}.Debug|x86.ActiveCfg = Debug|Any CPU - {91F00C73-78E5-4400-8F9F-910BE99EDFD5}.Debug|x86.Build.0 = Debug|Any CPU - {91F00C73-78E5-4400-8F9F-910BE99EDFD5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {91F00C73-78E5-4400-8F9F-910BE99EDFD5}.Release|Any CPU.Build.0 = Release|Any CPU - {91F00C73-78E5-4400-8F9F-910BE99EDFD5}.Release|x64.ActiveCfg = Release|Any CPU - {91F00C73-78E5-4400-8F9F-910BE99EDFD5}.Release|x64.Build.0 = Release|Any CPU - {91F00C73-78E5-4400-8F9F-910BE99EDFD5}.Release|x86.ActiveCfg = Release|Any CPU - {91F00C73-78E5-4400-8F9F-910BE99EDFD5}.Release|x86.Build.0 = Release|Any CPU - {738F2483-286E-4517-9660-5BFEEF343595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {738F2483-286E-4517-9660-5BFEEF343595}.Debug|Any CPU.Build.0 = Debug|Any CPU - {738F2483-286E-4517-9660-5BFEEF343595}.Debug|x64.ActiveCfg = Debug|Any CPU - {738F2483-286E-4517-9660-5BFEEF343595}.Debug|x64.Build.0 = Debug|Any CPU - {738F2483-286E-4517-9660-5BFEEF343595}.Debug|x86.ActiveCfg = Debug|Any CPU - {738F2483-286E-4517-9660-5BFEEF343595}.Debug|x86.Build.0 = Debug|Any CPU - {738F2483-286E-4517-9660-5BFEEF343595}.Release|Any CPU.ActiveCfg = Release|Any CPU - {738F2483-286E-4517-9660-5BFEEF343595}.Release|Any CPU.Build.0 = Release|Any CPU - {738F2483-286E-4517-9660-5BFEEF343595}.Release|x64.ActiveCfg = Release|Any CPU - {738F2483-286E-4517-9660-5BFEEF343595}.Release|x64.Build.0 = Release|Any CPU - {738F2483-286E-4517-9660-5BFEEF343595}.Release|x86.ActiveCfg = Release|Any CPU - {738F2483-286E-4517-9660-5BFEEF343595}.Release|x86.Build.0 = Release|Any CPU - {31AF3FED-A6C5-4DD5-99DF-03D4B4186D39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {31AF3FED-A6C5-4DD5-99DF-03D4B4186D39}.Debug|Any CPU.Build.0 = Debug|Any CPU - {31AF3FED-A6C5-4DD5-99DF-03D4B4186D39}.Debug|x64.ActiveCfg = Debug|Any CPU - {31AF3FED-A6C5-4DD5-99DF-03D4B4186D39}.Debug|x64.Build.0 = Debug|Any CPU - {31AF3FED-A6C5-4DD5-99DF-03D4B4186D39}.Debug|x86.ActiveCfg = Debug|Any CPU - {31AF3FED-A6C5-4DD5-99DF-03D4B4186D39}.Debug|x86.Build.0 = Debug|Any CPU - {31AF3FED-A6C5-4DD5-99DF-03D4B4186D39}.Release|Any CPU.ActiveCfg = Release|Any CPU - {31AF3FED-A6C5-4DD5-99DF-03D4B4186D39}.Release|Any CPU.Build.0 = Release|Any CPU - {31AF3FED-A6C5-4DD5-99DF-03D4B4186D39}.Release|x64.ActiveCfg = Release|Any CPU - {31AF3FED-A6C5-4DD5-99DF-03D4B4186D39}.Release|x64.Build.0 = Release|Any CPU - {31AF3FED-A6C5-4DD5-99DF-03D4B4186D39}.Release|x86.ActiveCfg = Release|Any CPU - {31AF3FED-A6C5-4DD5-99DF-03D4B4186D39}.Release|x86.Build.0 = Release|Any CPU - {9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}.Debug|x64.ActiveCfg = Debug|Any CPU - {9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}.Debug|x64.Build.0 = Debug|Any CPU - {9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}.Debug|x86.ActiveCfg = Debug|Any CPU - {9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}.Debug|x86.Build.0 = Debug|Any CPU - {9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}.Release|Any CPU.Build.0 = Release|Any CPU - {9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}.Release|x64.ActiveCfg = Release|Any CPU - {9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}.Release|x64.Build.0 = Release|Any CPU - {9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}.Release|x86.ActiveCfg = Release|Any CPU - {9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8}.Release|x86.Build.0 = Release|Any CPU - {CF2FA420-6295-4F83-8F87-340D8365282D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CF2FA420-6295-4F83-8F87-340D8365282D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CF2FA420-6295-4F83-8F87-340D8365282D}.Debug|x64.ActiveCfg = Debug|Any CPU - {CF2FA420-6295-4F83-8F87-340D8365282D}.Debug|x64.Build.0 = Debug|Any CPU - {CF2FA420-6295-4F83-8F87-340D8365282D}.Debug|x86.ActiveCfg = Debug|Any CPU - {CF2FA420-6295-4F83-8F87-340D8365282D}.Debug|x86.Build.0 = Debug|Any CPU - {CF2FA420-6295-4F83-8F87-340D8365282D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CF2FA420-6295-4F83-8F87-340D8365282D}.Release|Any CPU.Build.0 = Release|Any CPU - {CF2FA420-6295-4F83-8F87-340D8365282D}.Release|x64.ActiveCfg = Release|Any CPU - {CF2FA420-6295-4F83-8F87-340D8365282D}.Release|x64.Build.0 = Release|Any CPU - {CF2FA420-6295-4F83-8F87-340D8365282D}.Release|x86.ActiveCfg = Release|Any CPU - {CF2FA420-6295-4F83-8F87-340D8365282D}.Release|x86.Build.0 = Release|Any CPU - {8B7EEF6D-DFF2-4D7B-9E5D-6CA34A8BC260}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8B7EEF6D-DFF2-4D7B-9E5D-6CA34A8BC260}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8B7EEF6D-DFF2-4D7B-9E5D-6CA34A8BC260}.Debug|x64.ActiveCfg = Debug|Any CPU - {8B7EEF6D-DFF2-4D7B-9E5D-6CA34A8BC260}.Debug|x64.Build.0 = Debug|Any CPU - {8B7EEF6D-DFF2-4D7B-9E5D-6CA34A8BC260}.Debug|x86.ActiveCfg = Debug|Any CPU - {8B7EEF6D-DFF2-4D7B-9E5D-6CA34A8BC260}.Debug|x86.Build.0 = Debug|Any CPU - {8B7EEF6D-DFF2-4D7B-9E5D-6CA34A8BC260}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8B7EEF6D-DFF2-4D7B-9E5D-6CA34A8BC260}.Release|Any CPU.Build.0 = Release|Any CPU - {8B7EEF6D-DFF2-4D7B-9E5D-6CA34A8BC260}.Release|x64.ActiveCfg = Release|Any CPU - {8B7EEF6D-DFF2-4D7B-9E5D-6CA34A8BC260}.Release|x64.Build.0 = Release|Any CPU - {8B7EEF6D-DFF2-4D7B-9E5D-6CA34A8BC260}.Release|x86.ActiveCfg = Release|Any CPU - {8B7EEF6D-DFF2-4D7B-9E5D-6CA34A8BC260}.Release|x86.Build.0 = Release|Any CPU - {480E09E8-A0C4-49A7-AAC4-4AA5EC0733C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {480E09E8-A0C4-49A7-AAC4-4AA5EC0733C5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {480E09E8-A0C4-49A7-AAC4-4AA5EC0733C5}.Debug|x64.ActiveCfg = Debug|Any CPU - {480E09E8-A0C4-49A7-AAC4-4AA5EC0733C5}.Debug|x64.Build.0 = Debug|Any CPU - {480E09E8-A0C4-49A7-AAC4-4AA5EC0733C5}.Debug|x86.ActiveCfg = Debug|Any CPU - {480E09E8-A0C4-49A7-AAC4-4AA5EC0733C5}.Debug|x86.Build.0 = Debug|Any CPU - {480E09E8-A0C4-49A7-AAC4-4AA5EC0733C5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {480E09E8-A0C4-49A7-AAC4-4AA5EC0733C5}.Release|Any CPU.Build.0 = Release|Any CPU - {480E09E8-A0C4-49A7-AAC4-4AA5EC0733C5}.Release|x64.ActiveCfg = Release|Any CPU - {480E09E8-A0C4-49A7-AAC4-4AA5EC0733C5}.Release|x64.Build.0 = Release|Any CPU - {480E09E8-A0C4-49A7-AAC4-4AA5EC0733C5}.Release|x86.ActiveCfg = Release|Any CPU - {480E09E8-A0C4-49A7-AAC4-4AA5EC0733C5}.Release|x86.Build.0 = Release|Any CPU - {670D3B20-401F-4004-AD0B-865560D02CC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {670D3B20-401F-4004-AD0B-865560D02CC3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {670D3B20-401F-4004-AD0B-865560D02CC3}.Debug|x64.ActiveCfg = Debug|Any CPU - {670D3B20-401F-4004-AD0B-865560D02CC3}.Debug|x64.Build.0 = Debug|Any CPU - {670D3B20-401F-4004-AD0B-865560D02CC3}.Debug|x86.ActiveCfg = Debug|Any CPU - {670D3B20-401F-4004-AD0B-865560D02CC3}.Debug|x86.Build.0 = Debug|Any CPU - {670D3B20-401F-4004-AD0B-865560D02CC3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {670D3B20-401F-4004-AD0B-865560D02CC3}.Release|Any CPU.Build.0 = Release|Any CPU - {670D3B20-401F-4004-AD0B-865560D02CC3}.Release|x64.ActiveCfg = Release|Any CPU - {670D3B20-401F-4004-AD0B-865560D02CC3}.Release|x64.Build.0 = Release|Any CPU - {670D3B20-401F-4004-AD0B-865560D02CC3}.Release|x86.ActiveCfg = Release|Any CPU - {670D3B20-401F-4004-AD0B-865560D02CC3}.Release|x86.Build.0 = Release|Any CPU - {07F3E312-EC4C-4B71-A095-E478DF4CB52B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {07F3E312-EC4C-4B71-A095-E478DF4CB52B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07F3E312-EC4C-4B71-A095-E478DF4CB52B}.Debug|x64.ActiveCfg = Debug|Any CPU - {07F3E312-EC4C-4B71-A095-E478DF4CB52B}.Debug|x64.Build.0 = Debug|Any CPU - {07F3E312-EC4C-4B71-A095-E478DF4CB52B}.Debug|x86.ActiveCfg = Debug|Any CPU - {07F3E312-EC4C-4B71-A095-E478DF4CB52B}.Debug|x86.Build.0 = Debug|Any CPU - {07F3E312-EC4C-4B71-A095-E478DF4CB52B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07F3E312-EC4C-4B71-A095-E478DF4CB52B}.Release|Any CPU.Build.0 = Release|Any CPU - {07F3E312-EC4C-4B71-A095-E478DF4CB52B}.Release|x64.ActiveCfg = Release|Any CPU - {07F3E312-EC4C-4B71-A095-E478DF4CB52B}.Release|x64.Build.0 = Release|Any CPU - {07F3E312-EC4C-4B71-A095-E478DF4CB52B}.Release|x86.ActiveCfg = Release|Any CPU - {07F3E312-EC4C-4B71-A095-E478DF4CB52B}.Release|x86.Build.0 = Release|Any CPU - {414B6DBE-1A3A-42DC-9116-3F9A433C6BBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {414B6DBE-1A3A-42DC-9116-3F9A433C6BBB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {414B6DBE-1A3A-42DC-9116-3F9A433C6BBB}.Debug|x64.ActiveCfg = Debug|Any CPU - {414B6DBE-1A3A-42DC-9116-3F9A433C6BBB}.Debug|x64.Build.0 = Debug|Any CPU - {414B6DBE-1A3A-42DC-9116-3F9A433C6BBB}.Debug|x86.ActiveCfg = Debug|Any CPU - {414B6DBE-1A3A-42DC-9116-3F9A433C6BBB}.Debug|x86.Build.0 = Debug|Any CPU - {414B6DBE-1A3A-42DC-9116-3F9A433C6BBB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {414B6DBE-1A3A-42DC-9116-3F9A433C6BBB}.Release|Any CPU.Build.0 = Release|Any CPU - {414B6DBE-1A3A-42DC-9116-3F9A433C6BBB}.Release|x64.ActiveCfg = Release|Any CPU - {414B6DBE-1A3A-42DC-9116-3F9A433C6BBB}.Release|x64.Build.0 = Release|Any CPU - {414B6DBE-1A3A-42DC-9116-3F9A433C6BBB}.Release|x86.ActiveCfg = Release|Any CPU - {414B6DBE-1A3A-42DC-9116-3F9A433C6BBB}.Release|x86.Build.0 = Release|Any CPU - {4AD904D1-7727-48EF-88D9-7B38D98EB314}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4AD904D1-7727-48EF-88D9-7B38D98EB314}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4AD904D1-7727-48EF-88D9-7B38D98EB314}.Debug|x64.ActiveCfg = Debug|Any CPU - {4AD904D1-7727-48EF-88D9-7B38D98EB314}.Debug|x64.Build.0 = Debug|Any CPU - {4AD904D1-7727-48EF-88D9-7B38D98EB314}.Debug|x86.ActiveCfg = Debug|Any CPU - {4AD904D1-7727-48EF-88D9-7B38D98EB314}.Debug|x86.Build.0 = Debug|Any CPU - {4AD904D1-7727-48EF-88D9-7B38D98EB314}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4AD904D1-7727-48EF-88D9-7B38D98EB314}.Release|Any CPU.Build.0 = Release|Any CPU - {4AD904D1-7727-48EF-88D9-7B38D98EB314}.Release|x64.ActiveCfg = Release|Any CPU - {4AD904D1-7727-48EF-88D9-7B38D98EB314}.Release|x64.Build.0 = Release|Any CPU - {4AD904D1-7727-48EF-88D9-7B38D98EB314}.Release|x86.ActiveCfg = Release|Any CPU - {4AD904D1-7727-48EF-88D9-7B38D98EB314}.Release|x86.Build.0 = Release|Any CPU - {2B7E7416-E093-4763-B839-504CBFBC8F1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B7E7416-E093-4763-B839-504CBFBC8F1C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B7E7416-E093-4763-B839-504CBFBC8F1C}.Debug|x64.ActiveCfg = Debug|Any CPU - {2B7E7416-E093-4763-B839-504CBFBC8F1C}.Debug|x64.Build.0 = Debug|Any CPU - {2B7E7416-E093-4763-B839-504CBFBC8F1C}.Debug|x86.ActiveCfg = Debug|Any CPU - {2B7E7416-E093-4763-B839-504CBFBC8F1C}.Debug|x86.Build.0 = Debug|Any CPU - {2B7E7416-E093-4763-B839-504CBFBC8F1C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2B7E7416-E093-4763-B839-504CBFBC8F1C}.Release|Any CPU.Build.0 = Release|Any CPU - {2B7E7416-E093-4763-B839-504CBFBC8F1C}.Release|x64.ActiveCfg = Release|Any CPU - {2B7E7416-E093-4763-B839-504CBFBC8F1C}.Release|x64.Build.0 = Release|Any CPU - {2B7E7416-E093-4763-B839-504CBFBC8F1C}.Release|x86.ActiveCfg = Release|Any CPU - {2B7E7416-E093-4763-B839-504CBFBC8F1C}.Release|x86.Build.0 = Release|Any CPU - {F6FFA925-8C49-4594-9FF2-8F571C2D6389}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F6FFA925-8C49-4594-9FF2-8F571C2D6389}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F6FFA925-8C49-4594-9FF2-8F571C2D6389}.Debug|x64.ActiveCfg = Debug|Any CPU - {F6FFA925-8C49-4594-9FF2-8F571C2D6389}.Debug|x64.Build.0 = Debug|Any CPU - {F6FFA925-8C49-4594-9FF2-8F571C2D6389}.Debug|x86.ActiveCfg = Debug|Any CPU - {F6FFA925-8C49-4594-9FF2-8F571C2D6389}.Debug|x86.Build.0 = Debug|Any CPU - {F6FFA925-8C49-4594-9FF2-8F571C2D6389}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F6FFA925-8C49-4594-9FF2-8F571C2D6389}.Release|Any CPU.Build.0 = Release|Any CPU - {F6FFA925-8C49-4594-9FF2-8F571C2D6389}.Release|x64.ActiveCfg = Release|Any CPU - {F6FFA925-8C49-4594-9FF2-8F571C2D6389}.Release|x64.Build.0 = Release|Any CPU - {F6FFA925-8C49-4594-9FF2-8F571C2D6389}.Release|x86.ActiveCfg = Release|Any CPU - {F6FFA925-8C49-4594-9FF2-8F571C2D6389}.Release|x86.Build.0 = Release|Any CPU - {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Debug|Any CPU.Build.0 = Debug|Any CPU - {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Debug|x64.ActiveCfg = Debug|Any CPU - {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Debug|x64.Build.0 = Debug|Any CPU - {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Debug|x86.ActiveCfg = Debug|Any CPU - {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Debug|x86.Build.0 = Debug|Any CPU - {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Release|Any CPU.ActiveCfg = Release|Any CPU - {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Release|Any CPU.Build.0 = Release|Any CPU - {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Release|x64.ActiveCfg = Release|Any CPU - {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Release|x64.Build.0 = Release|Any CPU - {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Release|x86.ActiveCfg = Release|Any CPU - {151FB103-4BCA-41D2-9C8E-C5BDA7A68320}.Release|x86.Build.0 = Release|Any CPU - {D27B9558-0498-401A-AD3E-9BF15EF66E88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D27B9558-0498-401A-AD3E-9BF15EF66E88}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D27B9558-0498-401A-AD3E-9BF15EF66E88}.Debug|x64.ActiveCfg = Debug|Any CPU - {D27B9558-0498-401A-AD3E-9BF15EF66E88}.Debug|x64.Build.0 = Debug|Any CPU - {D27B9558-0498-401A-AD3E-9BF15EF66E88}.Debug|x86.ActiveCfg = Debug|Any CPU - {D27B9558-0498-401A-AD3E-9BF15EF66E88}.Debug|x86.Build.0 = Debug|Any CPU - {D27B9558-0498-401A-AD3E-9BF15EF66E88}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D27B9558-0498-401A-AD3E-9BF15EF66E88}.Release|Any CPU.Build.0 = Release|Any CPU - {D27B9558-0498-401A-AD3E-9BF15EF66E88}.Release|x64.ActiveCfg = Release|Any CPU - {D27B9558-0498-401A-AD3E-9BF15EF66E88}.Release|x64.Build.0 = Release|Any CPU - {D27B9558-0498-401A-AD3E-9BF15EF66E88}.Release|x86.ActiveCfg = Release|Any CPU - {D27B9558-0498-401A-AD3E-9BF15EF66E88}.Release|x86.Build.0 = Release|Any CPU - {545245AB-A255-4176-91E6-C837A21648D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {545245AB-A255-4176-91E6-C837A21648D8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {545245AB-A255-4176-91E6-C837A21648D8}.Debug|x64.ActiveCfg = Debug|Any CPU - {545245AB-A255-4176-91E6-C837A21648D8}.Debug|x64.Build.0 = Debug|Any CPU - {545245AB-A255-4176-91E6-C837A21648D8}.Debug|x86.ActiveCfg = Debug|Any CPU - {545245AB-A255-4176-91E6-C837A21648D8}.Debug|x86.Build.0 = Debug|Any CPU - {545245AB-A255-4176-91E6-C837A21648D8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {545245AB-A255-4176-91E6-C837A21648D8}.Release|Any CPU.Build.0 = Release|Any CPU - {545245AB-A255-4176-91E6-C837A21648D8}.Release|x64.ActiveCfg = Release|Any CPU - {545245AB-A255-4176-91E6-C837A21648D8}.Release|x64.Build.0 = Release|Any CPU - {545245AB-A255-4176-91E6-C837A21648D8}.Release|x86.ActiveCfg = Release|Any CPU - {545245AB-A255-4176-91E6-C837A21648D8}.Release|x86.Build.0 = Release|Any CPU - {34F7650B-6135-4C08-9B42-7E7DBA7D186F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {34F7650B-6135-4C08-9B42-7E7DBA7D186F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {34F7650B-6135-4C08-9B42-7E7DBA7D186F}.Debug|x64.ActiveCfg = Debug|Any CPU - {34F7650B-6135-4C08-9B42-7E7DBA7D186F}.Debug|x64.Build.0 = Debug|Any CPU - {34F7650B-6135-4C08-9B42-7E7DBA7D186F}.Debug|x86.ActiveCfg = Debug|Any CPU - {34F7650B-6135-4C08-9B42-7E7DBA7D186F}.Debug|x86.Build.0 = Debug|Any CPU - {34F7650B-6135-4C08-9B42-7E7DBA7D186F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {34F7650B-6135-4C08-9B42-7E7DBA7D186F}.Release|Any CPU.Build.0 = Release|Any CPU - {34F7650B-6135-4C08-9B42-7E7DBA7D186F}.Release|x64.ActiveCfg = Release|Any CPU - {34F7650B-6135-4C08-9B42-7E7DBA7D186F}.Release|x64.Build.0 = Release|Any CPU - {34F7650B-6135-4C08-9B42-7E7DBA7D186F}.Release|x86.ActiveCfg = Release|Any CPU - {34F7650B-6135-4C08-9B42-7E7DBA7D186F}.Release|x86.Build.0 = Release|Any CPU - {935A1DDC-6709-4521-A072-37ACC288371F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {935A1DDC-6709-4521-A072-37ACC288371F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {935A1DDC-6709-4521-A072-37ACC288371F}.Debug|x64.ActiveCfg = Debug|Any CPU - {935A1DDC-6709-4521-A072-37ACC288371F}.Debug|x64.Build.0 = Debug|Any CPU - {935A1DDC-6709-4521-A072-37ACC288371F}.Debug|x86.ActiveCfg = Debug|Any CPU - {935A1DDC-6709-4521-A072-37ACC288371F}.Debug|x86.Build.0 = Debug|Any CPU - {935A1DDC-6709-4521-A072-37ACC288371F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {935A1DDC-6709-4521-A072-37ACC288371F}.Release|Any CPU.Build.0 = Release|Any CPU - {935A1DDC-6709-4521-A072-37ACC288371F}.Release|x64.ActiveCfg = Release|Any CPU - {935A1DDC-6709-4521-A072-37ACC288371F}.Release|x64.Build.0 = Release|Any CPU - {935A1DDC-6709-4521-A072-37ACC288371F}.Release|x86.ActiveCfg = Release|Any CPU - {935A1DDC-6709-4521-A072-37ACC288371F}.Release|x86.Build.0 = Release|Any CPU - {560AAB82-9530-4A59-9C68-A9C8C586A5AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {560AAB82-9530-4A59-9C68-A9C8C586A5AA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {560AAB82-9530-4A59-9C68-A9C8C586A5AA}.Debug|x64.ActiveCfg = Debug|Any CPU - {560AAB82-9530-4A59-9C68-A9C8C586A5AA}.Debug|x64.Build.0 = Debug|Any CPU - {560AAB82-9530-4A59-9C68-A9C8C586A5AA}.Debug|x86.ActiveCfg = Debug|Any CPU - {560AAB82-9530-4A59-9C68-A9C8C586A5AA}.Debug|x86.Build.0 = Debug|Any CPU - {560AAB82-9530-4A59-9C68-A9C8C586A5AA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {560AAB82-9530-4A59-9C68-A9C8C586A5AA}.Release|Any CPU.Build.0 = Release|Any CPU - {560AAB82-9530-4A59-9C68-A9C8C586A5AA}.Release|x64.ActiveCfg = Release|Any CPU - {560AAB82-9530-4A59-9C68-A9C8C586A5AA}.Release|x64.Build.0 = Release|Any CPU - {560AAB82-9530-4A59-9C68-A9C8C586A5AA}.Release|x86.ActiveCfg = Release|Any CPU - {560AAB82-9530-4A59-9C68-A9C8C586A5AA}.Release|x86.Build.0 = Release|Any CPU - {DD1057EF-C9AE-42B9-B56D-DC49013D175D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DD1057EF-C9AE-42B9-B56D-DC49013D175D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DD1057EF-C9AE-42B9-B56D-DC49013D175D}.Debug|x64.ActiveCfg = Debug|Any CPU - {DD1057EF-C9AE-42B9-B56D-DC49013D175D}.Debug|x64.Build.0 = Debug|Any CPU - {DD1057EF-C9AE-42B9-B56D-DC49013D175D}.Debug|x86.ActiveCfg = Debug|Any CPU - {DD1057EF-C9AE-42B9-B56D-DC49013D175D}.Debug|x86.Build.0 = Debug|Any CPU - {DD1057EF-C9AE-42B9-B56D-DC49013D175D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DD1057EF-C9AE-42B9-B56D-DC49013D175D}.Release|Any CPU.Build.0 = Release|Any CPU - {DD1057EF-C9AE-42B9-B56D-DC49013D175D}.Release|x64.ActiveCfg = Release|Any CPU - {DD1057EF-C9AE-42B9-B56D-DC49013D175D}.Release|x64.Build.0 = Release|Any CPU - {DD1057EF-C9AE-42B9-B56D-DC49013D175D}.Release|x86.ActiveCfg = Release|Any CPU - {DD1057EF-C9AE-42B9-B56D-DC49013D175D}.Release|x86.Build.0 = Release|Any CPU - {7FCCCA0F-FE75-4847-B464-982C02661F07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7FCCCA0F-FE75-4847-B464-982C02661F07}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7FCCCA0F-FE75-4847-B464-982C02661F07}.Debug|x64.ActiveCfg = Debug|Any CPU - {7FCCCA0F-FE75-4847-B464-982C02661F07}.Debug|x64.Build.0 = Debug|Any CPU - {7FCCCA0F-FE75-4847-B464-982C02661F07}.Debug|x86.ActiveCfg = Debug|Any CPU - {7FCCCA0F-FE75-4847-B464-982C02661F07}.Debug|x86.Build.0 = Debug|Any CPU - {7FCCCA0F-FE75-4847-B464-982C02661F07}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7FCCCA0F-FE75-4847-B464-982C02661F07}.Release|Any CPU.Build.0 = Release|Any CPU - {7FCCCA0F-FE75-4847-B464-982C02661F07}.Release|x64.ActiveCfg = Release|Any CPU - {7FCCCA0F-FE75-4847-B464-982C02661F07}.Release|x64.Build.0 = Release|Any CPU - {7FCCCA0F-FE75-4847-B464-982C02661F07}.Release|x86.ActiveCfg = Release|Any CPU - {7FCCCA0F-FE75-4847-B464-982C02661F07}.Release|x86.Build.0 = Release|Any CPU - {52E5E2D0-7DD2-429F-8D3F-805648332D66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {52E5E2D0-7DD2-429F-8D3F-805648332D66}.Debug|Any CPU.Build.0 = Debug|Any CPU - {52E5E2D0-7DD2-429F-8D3F-805648332D66}.Debug|x64.ActiveCfg = Debug|Any CPU - {52E5E2D0-7DD2-429F-8D3F-805648332D66}.Debug|x64.Build.0 = Debug|Any CPU - {52E5E2D0-7DD2-429F-8D3F-805648332D66}.Debug|x86.ActiveCfg = Debug|Any CPU - {52E5E2D0-7DD2-429F-8D3F-805648332D66}.Debug|x86.Build.0 = Debug|Any CPU - {52E5E2D0-7DD2-429F-8D3F-805648332D66}.Release|Any CPU.ActiveCfg = Release|Any CPU - {52E5E2D0-7DD2-429F-8D3F-805648332D66}.Release|Any CPU.Build.0 = Release|Any CPU - {52E5E2D0-7DD2-429F-8D3F-805648332D66}.Release|x64.ActiveCfg = Release|Any CPU - {52E5E2D0-7DD2-429F-8D3F-805648332D66}.Release|x64.Build.0 = Release|Any CPU - {52E5E2D0-7DD2-429F-8D3F-805648332D66}.Release|x86.ActiveCfg = Release|Any CPU - {52E5E2D0-7DD2-429F-8D3F-805648332D66}.Release|x86.Build.0 = Release|Any CPU - {0B08ACE1-F3BF-4375-9381-22ACA1ECD387}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0B08ACE1-F3BF-4375-9381-22ACA1ECD387}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0B08ACE1-F3BF-4375-9381-22ACA1ECD387}.Debug|x64.ActiveCfg = Debug|Any CPU - {0B08ACE1-F3BF-4375-9381-22ACA1ECD387}.Debug|x64.Build.0 = Debug|Any CPU - {0B08ACE1-F3BF-4375-9381-22ACA1ECD387}.Debug|x86.ActiveCfg = Debug|Any CPU - {0B08ACE1-F3BF-4375-9381-22ACA1ECD387}.Debug|x86.Build.0 = Debug|Any CPU - {0B08ACE1-F3BF-4375-9381-22ACA1ECD387}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0B08ACE1-F3BF-4375-9381-22ACA1ECD387}.Release|Any CPU.Build.0 = Release|Any CPU - {0B08ACE1-F3BF-4375-9381-22ACA1ECD387}.Release|x64.ActiveCfg = Release|Any CPU - {0B08ACE1-F3BF-4375-9381-22ACA1ECD387}.Release|x64.Build.0 = Release|Any CPU - {0B08ACE1-F3BF-4375-9381-22ACA1ECD387}.Release|x86.ActiveCfg = Release|Any CPU - {0B08ACE1-F3BF-4375-9381-22ACA1ECD387}.Release|x86.Build.0 = Release|Any CPU - {E541AEB3-45D8-4089-A2BD-45859999A30D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E541AEB3-45D8-4089-A2BD-45859999A30D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E541AEB3-45D8-4089-A2BD-45859999A30D}.Debug|x64.ActiveCfg = Debug|Any CPU - {E541AEB3-45D8-4089-A2BD-45859999A30D}.Debug|x64.Build.0 = Debug|Any CPU - {E541AEB3-45D8-4089-A2BD-45859999A30D}.Debug|x86.ActiveCfg = Debug|Any CPU - {E541AEB3-45D8-4089-A2BD-45859999A30D}.Debug|x86.Build.0 = Debug|Any CPU - {E541AEB3-45D8-4089-A2BD-45859999A30D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E541AEB3-45D8-4089-A2BD-45859999A30D}.Release|Any CPU.Build.0 = Release|Any CPU - {E541AEB3-45D8-4089-A2BD-45859999A30D}.Release|x64.ActiveCfg = Release|Any CPU - {E541AEB3-45D8-4089-A2BD-45859999A30D}.Release|x64.Build.0 = Release|Any CPU - {E541AEB3-45D8-4089-A2BD-45859999A30D}.Release|x86.ActiveCfg = Release|Any CPU - {E541AEB3-45D8-4089-A2BD-45859999A30D}.Release|x86.Build.0 = Release|Any CPU - {EDA108F7-2C3D-4C7B-A1C1-286D6FB87761}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EDA108F7-2C3D-4C7B-A1C1-286D6FB87761}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EDA108F7-2C3D-4C7B-A1C1-286D6FB87761}.Debug|x64.ActiveCfg = Debug|Any CPU - {EDA108F7-2C3D-4C7B-A1C1-286D6FB87761}.Debug|x64.Build.0 = Debug|Any CPU - {EDA108F7-2C3D-4C7B-A1C1-286D6FB87761}.Debug|x86.ActiveCfg = Debug|Any CPU - {EDA108F7-2C3D-4C7B-A1C1-286D6FB87761}.Debug|x86.Build.0 = Debug|Any CPU - {EDA108F7-2C3D-4C7B-A1C1-286D6FB87761}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EDA108F7-2C3D-4C7B-A1C1-286D6FB87761}.Release|Any CPU.Build.0 = Release|Any CPU - {EDA108F7-2C3D-4C7B-A1C1-286D6FB87761}.Release|x64.ActiveCfg = Release|Any CPU - {EDA108F7-2C3D-4C7B-A1C1-286D6FB87761}.Release|x64.Build.0 = Release|Any CPU - {EDA108F7-2C3D-4C7B-A1C1-286D6FB87761}.Release|x86.ActiveCfg = Release|Any CPU - {EDA108F7-2C3D-4C7B-A1C1-286D6FB87761}.Release|x86.Build.0 = Release|Any CPU - {30214E4D-7E4E-4C98-B489-FCB0A16DF2C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {30214E4D-7E4E-4C98-B489-FCB0A16DF2C4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {30214E4D-7E4E-4C98-B489-FCB0A16DF2C4}.Debug|x64.ActiveCfg = Debug|Any CPU - {30214E4D-7E4E-4C98-B489-FCB0A16DF2C4}.Debug|x64.Build.0 = Debug|Any CPU - {30214E4D-7E4E-4C98-B489-FCB0A16DF2C4}.Debug|x86.ActiveCfg = Debug|Any CPU - {30214E4D-7E4E-4C98-B489-FCB0A16DF2C4}.Debug|x86.Build.0 = Debug|Any CPU - {30214E4D-7E4E-4C98-B489-FCB0A16DF2C4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {30214E4D-7E4E-4C98-B489-FCB0A16DF2C4}.Release|Any CPU.Build.0 = Release|Any CPU - {30214E4D-7E4E-4C98-B489-FCB0A16DF2C4}.Release|x64.ActiveCfg = Release|Any CPU - {30214E4D-7E4E-4C98-B489-FCB0A16DF2C4}.Release|x64.Build.0 = Release|Any CPU - {30214E4D-7E4E-4C98-B489-FCB0A16DF2C4}.Release|x86.ActiveCfg = Release|Any CPU - {30214E4D-7E4E-4C98-B489-FCB0A16DF2C4}.Release|x86.Build.0 = Release|Any CPU - {83926ADB-A63C-4575-AC99-2D420FFE32CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {83926ADB-A63C-4575-AC99-2D420FFE32CA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {83926ADB-A63C-4575-AC99-2D420FFE32CA}.Debug|x64.ActiveCfg = Debug|Any CPU - {83926ADB-A63C-4575-AC99-2D420FFE32CA}.Debug|x64.Build.0 = Debug|Any CPU - {83926ADB-A63C-4575-AC99-2D420FFE32CA}.Debug|x86.ActiveCfg = Debug|Any CPU - {83926ADB-A63C-4575-AC99-2D420FFE32CA}.Debug|x86.Build.0 = Debug|Any CPU - {83926ADB-A63C-4575-AC99-2D420FFE32CA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {83926ADB-A63C-4575-AC99-2D420FFE32CA}.Release|Any CPU.Build.0 = Release|Any CPU - {83926ADB-A63C-4575-AC99-2D420FFE32CA}.Release|x64.ActiveCfg = Release|Any CPU - {83926ADB-A63C-4575-AC99-2D420FFE32CA}.Release|x64.Build.0 = Release|Any CPU - {83926ADB-A63C-4575-AC99-2D420FFE32CA}.Release|x86.ActiveCfg = Release|Any CPU - {83926ADB-A63C-4575-AC99-2D420FFE32CA}.Release|x86.Build.0 = Release|Any CPU - {2A8E659F-3C92-4C5F-BB82-5EE91C15123A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2A8E659F-3C92-4C5F-BB82-5EE91C15123A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2A8E659F-3C92-4C5F-BB82-5EE91C15123A}.Debug|x64.ActiveCfg = Debug|Any CPU - {2A8E659F-3C92-4C5F-BB82-5EE91C15123A}.Debug|x64.Build.0 = Debug|Any CPU - {2A8E659F-3C92-4C5F-BB82-5EE91C15123A}.Debug|x86.ActiveCfg = Debug|Any CPU - {2A8E659F-3C92-4C5F-BB82-5EE91C15123A}.Debug|x86.Build.0 = Debug|Any CPU - {2A8E659F-3C92-4C5F-BB82-5EE91C15123A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2A8E659F-3C92-4C5F-BB82-5EE91C15123A}.Release|Any CPU.Build.0 = Release|Any CPU - {2A8E659F-3C92-4C5F-BB82-5EE91C15123A}.Release|x64.ActiveCfg = Release|Any CPU - {2A8E659F-3C92-4C5F-BB82-5EE91C15123A}.Release|x64.Build.0 = Release|Any CPU - {2A8E659F-3C92-4C5F-BB82-5EE91C15123A}.Release|x86.ActiveCfg = Release|Any CPU - {2A8E659F-3C92-4C5F-BB82-5EE91C15123A}.Release|x86.Build.0 = Release|Any CPU - {A26F7BAB-493A-45F4-AB0C-86BE84B20DBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A26F7BAB-493A-45F4-AB0C-86BE84B20DBA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A26F7BAB-493A-45F4-AB0C-86BE84B20DBA}.Debug|x64.ActiveCfg = Debug|Any CPU - {A26F7BAB-493A-45F4-AB0C-86BE84B20DBA}.Debug|x64.Build.0 = Debug|Any CPU - {A26F7BAB-493A-45F4-AB0C-86BE84B20DBA}.Debug|x86.ActiveCfg = Debug|Any CPU - {A26F7BAB-493A-45F4-AB0C-86BE84B20DBA}.Debug|x86.Build.0 = Debug|Any CPU - {A26F7BAB-493A-45F4-AB0C-86BE84B20DBA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A26F7BAB-493A-45F4-AB0C-86BE84B20DBA}.Release|Any CPU.Build.0 = Release|Any CPU - {A26F7BAB-493A-45F4-AB0C-86BE84B20DBA}.Release|x64.ActiveCfg = Release|Any CPU - {A26F7BAB-493A-45F4-AB0C-86BE84B20DBA}.Release|x64.Build.0 = Release|Any CPU - {A26F7BAB-493A-45F4-AB0C-86BE84B20DBA}.Release|x86.ActiveCfg = Release|Any CPU - {A26F7BAB-493A-45F4-AB0C-86BE84B20DBA}.Release|x86.Build.0 = Release|Any CPU - {0638755B-FA20-4A94-98D4-396AFAA5789F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0638755B-FA20-4A94-98D4-396AFAA5789F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0638755B-FA20-4A94-98D4-396AFAA5789F}.Debug|x64.ActiveCfg = Debug|Any CPU - {0638755B-FA20-4A94-98D4-396AFAA5789F}.Debug|x64.Build.0 = Debug|Any CPU - {0638755B-FA20-4A94-98D4-396AFAA5789F}.Debug|x86.ActiveCfg = Debug|Any CPU - {0638755B-FA20-4A94-98D4-396AFAA5789F}.Debug|x86.Build.0 = Debug|Any CPU - {0638755B-FA20-4A94-98D4-396AFAA5789F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0638755B-FA20-4A94-98D4-396AFAA5789F}.Release|Any CPU.Build.0 = Release|Any CPU - {0638755B-FA20-4A94-98D4-396AFAA5789F}.Release|x64.ActiveCfg = Release|Any CPU - {0638755B-FA20-4A94-98D4-396AFAA5789F}.Release|x64.Build.0 = Release|Any CPU - {0638755B-FA20-4A94-98D4-396AFAA5789F}.Release|x86.ActiveCfg = Release|Any CPU - {0638755B-FA20-4A94-98D4-396AFAA5789F}.Release|x86.Build.0 = Release|Any CPU - {FA37C0D9-306B-42E9-890E-73BED531C091}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FA37C0D9-306B-42E9-890E-73BED531C091}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FA37C0D9-306B-42E9-890E-73BED531C091}.Debug|x64.ActiveCfg = Debug|Any CPU - {FA37C0D9-306B-42E9-890E-73BED531C091}.Debug|x64.Build.0 = Debug|Any CPU - {FA37C0D9-306B-42E9-890E-73BED531C091}.Debug|x86.ActiveCfg = Debug|Any CPU - {FA37C0D9-306B-42E9-890E-73BED531C091}.Debug|x86.Build.0 = Debug|Any CPU - {FA37C0D9-306B-42E9-890E-73BED531C091}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FA37C0D9-306B-42E9-890E-73BED531C091}.Release|Any CPU.Build.0 = Release|Any CPU - {FA37C0D9-306B-42E9-890E-73BED531C091}.Release|x64.ActiveCfg = Release|Any CPU - {FA37C0D9-306B-42E9-890E-73BED531C091}.Release|x64.Build.0 = Release|Any CPU - {FA37C0D9-306B-42E9-890E-73BED531C091}.Release|x86.ActiveCfg = Release|Any CPU - {FA37C0D9-306B-42E9-890E-73BED531C091}.Release|x86.Build.0 = Release|Any CPU - {317C2668-0920-492A-90D7-FF9670344DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {317C2668-0920-492A-90D7-FF9670344DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {317C2668-0920-492A-90D7-FF9670344DEE}.Debug|x64.ActiveCfg = Debug|Any CPU - {317C2668-0920-492A-90D7-FF9670344DEE}.Debug|x64.Build.0 = Debug|Any CPU - {317C2668-0920-492A-90D7-FF9670344DEE}.Debug|x86.ActiveCfg = Debug|Any CPU - {317C2668-0920-492A-90D7-FF9670344DEE}.Debug|x86.Build.0 = Debug|Any CPU - {317C2668-0920-492A-90D7-FF9670344DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {317C2668-0920-492A-90D7-FF9670344DEE}.Release|Any CPU.Build.0 = Release|Any CPU - {317C2668-0920-492A-90D7-FF9670344DEE}.Release|x64.ActiveCfg = Release|Any CPU - {317C2668-0920-492A-90D7-FF9670344DEE}.Release|x64.Build.0 = Release|Any CPU - {317C2668-0920-492A-90D7-FF9670344DEE}.Release|x86.ActiveCfg = Release|Any CPU - {317C2668-0920-492A-90D7-FF9670344DEE}.Release|x86.Build.0 = Release|Any CPU - {AD65B505-8EDE-4B54-9298-6B71D0283EB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AD65B505-8EDE-4B54-9298-6B71D0283EB1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AD65B505-8EDE-4B54-9298-6B71D0283EB1}.Debug|x64.ActiveCfg = Debug|Any CPU - {AD65B505-8EDE-4B54-9298-6B71D0283EB1}.Debug|x64.Build.0 = Debug|Any CPU - {AD65B505-8EDE-4B54-9298-6B71D0283EB1}.Debug|x86.ActiveCfg = Debug|Any CPU - {AD65B505-8EDE-4B54-9298-6B71D0283EB1}.Debug|x86.Build.0 = Debug|Any CPU - {AD65B505-8EDE-4B54-9298-6B71D0283EB1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AD65B505-8EDE-4B54-9298-6B71D0283EB1}.Release|Any CPU.Build.0 = Release|Any CPU - {AD65B505-8EDE-4B54-9298-6B71D0283EB1}.Release|x64.ActiveCfg = Release|Any CPU - {AD65B505-8EDE-4B54-9298-6B71D0283EB1}.Release|x64.Build.0 = Release|Any CPU - {AD65B505-8EDE-4B54-9298-6B71D0283EB1}.Release|x86.ActiveCfg = Release|Any CPU - {AD65B505-8EDE-4B54-9298-6B71D0283EB1}.Release|x86.Build.0 = Release|Any CPU - {AA04790E-58AC-40CC-9DEC-570D78D252AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AA04790E-58AC-40CC-9DEC-570D78D252AD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AA04790E-58AC-40CC-9DEC-570D78D252AD}.Debug|x64.ActiveCfg = Debug|Any CPU - {AA04790E-58AC-40CC-9DEC-570D78D252AD}.Debug|x64.Build.0 = Debug|Any CPU - {AA04790E-58AC-40CC-9DEC-570D78D252AD}.Debug|x86.ActiveCfg = Debug|Any CPU - {AA04790E-58AC-40CC-9DEC-570D78D252AD}.Debug|x86.Build.0 = Debug|Any CPU - {AA04790E-58AC-40CC-9DEC-570D78D252AD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AA04790E-58AC-40CC-9DEC-570D78D252AD}.Release|Any CPU.Build.0 = Release|Any CPU - {AA04790E-58AC-40CC-9DEC-570D78D252AD}.Release|x64.ActiveCfg = Release|Any CPU - {AA04790E-58AC-40CC-9DEC-570D78D252AD}.Release|x64.Build.0 = Release|Any CPU - {AA04790E-58AC-40CC-9DEC-570D78D252AD}.Release|x86.ActiveCfg = Release|Any CPU - {AA04790E-58AC-40CC-9DEC-570D78D252AD}.Release|x86.Build.0 = Release|Any CPU - {B98BC881-DDDD-4988-AC43-187C733895CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B98BC881-DDDD-4988-AC43-187C733895CD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B98BC881-DDDD-4988-AC43-187C733895CD}.Debug|x64.ActiveCfg = Debug|Any CPU - {B98BC881-DDDD-4988-AC43-187C733895CD}.Debug|x64.Build.0 = Debug|Any CPU - {B98BC881-DDDD-4988-AC43-187C733895CD}.Debug|x86.ActiveCfg = Debug|Any CPU - {B98BC881-DDDD-4988-AC43-187C733895CD}.Debug|x86.Build.0 = Debug|Any CPU - {B98BC881-DDDD-4988-AC43-187C733895CD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B98BC881-DDDD-4988-AC43-187C733895CD}.Release|Any CPU.Build.0 = Release|Any CPU - {B98BC881-DDDD-4988-AC43-187C733895CD}.Release|x64.ActiveCfg = Release|Any CPU - {B98BC881-DDDD-4988-AC43-187C733895CD}.Release|x64.Build.0 = Release|Any CPU - {B98BC881-DDDD-4988-AC43-187C733895CD}.Release|x86.ActiveCfg = Release|Any CPU - {B98BC881-DDDD-4988-AC43-187C733895CD}.Release|x86.Build.0 = Release|Any CPU - {D1B67116-DB1C-4CE7-954F-AD9E17C06061}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D1B67116-DB1C-4CE7-954F-AD9E17C06061}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D1B67116-DB1C-4CE7-954F-AD9E17C06061}.Debug|x64.ActiveCfg = Debug|Any CPU - {D1B67116-DB1C-4CE7-954F-AD9E17C06061}.Debug|x64.Build.0 = Debug|Any CPU - {D1B67116-DB1C-4CE7-954F-AD9E17C06061}.Debug|x86.ActiveCfg = Debug|Any CPU - {D1B67116-DB1C-4CE7-954F-AD9E17C06061}.Debug|x86.Build.0 = Debug|Any CPU - {D1B67116-DB1C-4CE7-954F-AD9E17C06061}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D1B67116-DB1C-4CE7-954F-AD9E17C06061}.Release|Any CPU.Build.0 = Release|Any CPU - {D1B67116-DB1C-4CE7-954F-AD9E17C06061}.Release|x64.ActiveCfg = Release|Any CPU - {D1B67116-DB1C-4CE7-954F-AD9E17C06061}.Release|x64.Build.0 = Release|Any CPU - {D1B67116-DB1C-4CE7-954F-AD9E17C06061}.Release|x86.ActiveCfg = Release|Any CPU - {D1B67116-DB1C-4CE7-954F-AD9E17C06061}.Release|x86.Build.0 = Release|Any CPU - {08D371ED-04DE-4CE3-AD1B-F6CC6D341706}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08D371ED-04DE-4CE3-AD1B-F6CC6D341706}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08D371ED-04DE-4CE3-AD1B-F6CC6D341706}.Debug|x64.ActiveCfg = Debug|Any CPU - {08D371ED-04DE-4CE3-AD1B-F6CC6D341706}.Debug|x64.Build.0 = Debug|Any CPU - {08D371ED-04DE-4CE3-AD1B-F6CC6D341706}.Debug|x86.ActiveCfg = Debug|Any CPU - {08D371ED-04DE-4CE3-AD1B-F6CC6D341706}.Debug|x86.Build.0 = Debug|Any CPU - {08D371ED-04DE-4CE3-AD1B-F6CC6D341706}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08D371ED-04DE-4CE3-AD1B-F6CC6D341706}.Release|Any CPU.Build.0 = Release|Any CPU - {08D371ED-04DE-4CE3-AD1B-F6CC6D341706}.Release|x64.ActiveCfg = Release|Any CPU - {08D371ED-04DE-4CE3-AD1B-F6CC6D341706}.Release|x64.Build.0 = Release|Any CPU - {08D371ED-04DE-4CE3-AD1B-F6CC6D341706}.Release|x86.ActiveCfg = Release|Any CPU - {08D371ED-04DE-4CE3-AD1B-F6CC6D341706}.Release|x86.Build.0 = Release|Any CPU - {E5B248B8-A2E1-46CB-9281-D5871F1483C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E5B248B8-A2E1-46CB-9281-D5871F1483C3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E5B248B8-A2E1-46CB-9281-D5871F1483C3}.Debug|x64.ActiveCfg = Debug|Any CPU - {E5B248B8-A2E1-46CB-9281-D5871F1483C3}.Debug|x64.Build.0 = Debug|Any CPU - {E5B248B8-A2E1-46CB-9281-D5871F1483C3}.Debug|x86.ActiveCfg = Debug|Any CPU - {E5B248B8-A2E1-46CB-9281-D5871F1483C3}.Debug|x86.Build.0 = Debug|Any CPU - {E5B248B8-A2E1-46CB-9281-D5871F1483C3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E5B248B8-A2E1-46CB-9281-D5871F1483C3}.Release|Any CPU.Build.0 = Release|Any CPU - {E5B248B8-A2E1-46CB-9281-D5871F1483C3}.Release|x64.ActiveCfg = Release|Any CPU - {E5B248B8-A2E1-46CB-9281-D5871F1483C3}.Release|x64.Build.0 = Release|Any CPU - {E5B248B8-A2E1-46CB-9281-D5871F1483C3}.Release|x86.ActiveCfg = Release|Any CPU - {E5B248B8-A2E1-46CB-9281-D5871F1483C3}.Release|x86.Build.0 = Release|Any CPU - {78979585-F881-4ACD-9E83-CCB866EB971C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {78979585-F881-4ACD-9E83-CCB866EB971C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {78979585-F881-4ACD-9E83-CCB866EB971C}.Debug|x64.ActiveCfg = Debug|Any CPU - {78979585-F881-4ACD-9E83-CCB866EB971C}.Debug|x64.Build.0 = Debug|Any CPU - {78979585-F881-4ACD-9E83-CCB866EB971C}.Debug|x86.ActiveCfg = Debug|Any CPU - {78979585-F881-4ACD-9E83-CCB866EB971C}.Debug|x86.Build.0 = Debug|Any CPU - {78979585-F881-4ACD-9E83-CCB866EB971C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {78979585-F881-4ACD-9E83-CCB866EB971C}.Release|Any CPU.Build.0 = Release|Any CPU - {78979585-F881-4ACD-9E83-CCB866EB971C}.Release|x64.ActiveCfg = Release|Any CPU - {78979585-F881-4ACD-9E83-CCB866EB971C}.Release|x64.Build.0 = Release|Any CPU - {78979585-F881-4ACD-9E83-CCB866EB971C}.Release|x86.ActiveCfg = Release|Any CPU - {78979585-F881-4ACD-9E83-CCB866EB971C}.Release|x86.Build.0 = Release|Any CPU - {655739D7-87E9-43EF-B522-AC421F7451A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {655739D7-87E9-43EF-B522-AC421F7451A4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {655739D7-87E9-43EF-B522-AC421F7451A4}.Debug|x64.ActiveCfg = Debug|Any CPU - {655739D7-87E9-43EF-B522-AC421F7451A4}.Debug|x64.Build.0 = Debug|Any CPU - {655739D7-87E9-43EF-B522-AC421F7451A4}.Debug|x86.ActiveCfg = Debug|Any CPU - {655739D7-87E9-43EF-B522-AC421F7451A4}.Debug|x86.Build.0 = Debug|Any CPU - {655739D7-87E9-43EF-B522-AC421F7451A4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {655739D7-87E9-43EF-B522-AC421F7451A4}.Release|Any CPU.Build.0 = Release|Any CPU - {655739D7-87E9-43EF-B522-AC421F7451A4}.Release|x64.ActiveCfg = Release|Any CPU - {655739D7-87E9-43EF-B522-AC421F7451A4}.Release|x64.Build.0 = Release|Any CPU - {655739D7-87E9-43EF-B522-AC421F7451A4}.Release|x86.ActiveCfg = Release|Any CPU - {655739D7-87E9-43EF-B522-AC421F7451A4}.Release|x86.Build.0 = Release|Any CPU - {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}.Debug|x64.ActiveCfg = Debug|Any CPU - {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}.Debug|x64.Build.0 = Debug|Any CPU - {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}.Debug|x86.ActiveCfg = Debug|Any CPU - {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}.Debug|x86.Build.0 = Debug|Any CPU - {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}.Release|Any CPU.Build.0 = Release|Any CPU - {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}.Release|x64.ActiveCfg = Release|Any CPU - {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}.Release|x64.Build.0 = Release|Any CPU - {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}.Release|x86.ActiveCfg = Release|Any CPU - {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}.Release|x86.Build.0 = Release|Any CPU - {669FA147-3B41-4841-921A-55B019C3AF26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {669FA147-3B41-4841-921A-55B019C3AF26}.Debug|Any CPU.Build.0 = Debug|Any CPU - {669FA147-3B41-4841-921A-55B019C3AF26}.Debug|x64.ActiveCfg = Debug|Any CPU - {669FA147-3B41-4841-921A-55B019C3AF26}.Debug|x64.Build.0 = Debug|Any CPU - {669FA147-3B41-4841-921A-55B019C3AF26}.Debug|x86.ActiveCfg = Debug|Any CPU - {669FA147-3B41-4841-921A-55B019C3AF26}.Debug|x86.Build.0 = Debug|Any CPU - {669FA147-3B41-4841-921A-55B019C3AF26}.Release|Any CPU.ActiveCfg = Release|Any CPU - {669FA147-3B41-4841-921A-55B019C3AF26}.Release|Any CPU.Build.0 = Release|Any CPU - {669FA147-3B41-4841-921A-55B019C3AF26}.Release|x64.ActiveCfg = Release|Any CPU - {669FA147-3B41-4841-921A-55B019C3AF26}.Release|x64.Build.0 = Release|Any CPU - {669FA147-3B41-4841-921A-55B019C3AF26}.Release|x86.ActiveCfg = Release|Any CPU - {669FA147-3B41-4841-921A-55B019C3AF26}.Release|x86.Build.0 = Release|Any CPU - {EA77D317-8767-4DDE-8038-820D582C52D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EA77D317-8767-4DDE-8038-820D582C52D6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA77D317-8767-4DDE-8038-820D582C52D6}.Debug|x64.ActiveCfg = Debug|Any CPU - {EA77D317-8767-4DDE-8038-820D582C52D6}.Debug|x64.Build.0 = Debug|Any CPU - {EA77D317-8767-4DDE-8038-820D582C52D6}.Debug|x86.ActiveCfg = Debug|Any CPU - {EA77D317-8767-4DDE-8038-820D582C52D6}.Debug|x86.Build.0 = Debug|Any CPU - {EA77D317-8767-4DDE-8038-820D582C52D6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EA77D317-8767-4DDE-8038-820D582C52D6}.Release|Any CPU.Build.0 = Release|Any CPU - {EA77D317-8767-4DDE-8038-820D582C52D6}.Release|x64.ActiveCfg = Release|Any CPU - {EA77D317-8767-4DDE-8038-820D582C52D6}.Release|x64.Build.0 = Release|Any CPU - {EA77D317-8767-4DDE-8038-820D582C52D6}.Release|x86.ActiveCfg = Release|Any CPU - {EA77D317-8767-4DDE-8038-820D582C52D6}.Release|x86.Build.0 = Release|Any CPU - {68726F89-B254-424B-BE17-AC9312484464}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {68726F89-B254-424B-BE17-AC9312484464}.Debug|Any CPU.Build.0 = Debug|Any CPU - {68726F89-B254-424B-BE17-AC9312484464}.Debug|x64.ActiveCfg = Debug|Any CPU - {68726F89-B254-424B-BE17-AC9312484464}.Debug|x64.Build.0 = Debug|Any CPU - {68726F89-B254-424B-BE17-AC9312484464}.Debug|x86.ActiveCfg = Debug|Any CPU - {68726F89-B254-424B-BE17-AC9312484464}.Debug|x86.Build.0 = Debug|Any CPU - {68726F89-B254-424B-BE17-AC9312484464}.Release|Any CPU.ActiveCfg = Release|Any CPU - {68726F89-B254-424B-BE17-AC9312484464}.Release|Any CPU.Build.0 = Release|Any CPU - {68726F89-B254-424B-BE17-AC9312484464}.Release|x64.ActiveCfg = Release|Any CPU - {68726F89-B254-424B-BE17-AC9312484464}.Release|x64.Build.0 = Release|Any CPU - {68726F89-B254-424B-BE17-AC9312484464}.Release|x86.ActiveCfg = Release|Any CPU - {68726F89-B254-424B-BE17-AC9312484464}.Release|x86.Build.0 = Release|Any CPU - {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Debug|x64.ActiveCfg = Debug|Any CPU - {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Debug|x64.Build.0 = Debug|Any CPU - {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Debug|x86.ActiveCfg = Debug|Any CPU - {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Debug|x86.Build.0 = Debug|Any CPU - {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Release|Any CPU.Build.0 = Release|Any CPU - {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Release|x64.ActiveCfg = Release|Any CPU - {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Release|x64.Build.0 = Release|Any CPU - {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Release|x86.ActiveCfg = Release|Any CPU - {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {015D2107-B5CD-4A45-A650-B29AECC6FBE3} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {38EB59B2-A76B-48F7-B99D-C99FB7793903} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {AFCDB23E-7988-4BE7-BD1A-23912AB1FCAE} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {BEC2D863-C095-4D83-8EDF-7B8AED26A834} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {7B9BE76A-1472-4028-BB8C-6C0C54342222} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {52631D27-685E-4D2B-92D7-0D53CAF2DD82} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {A8D2A7D1-483F-4FC3-867B-DB6E0DA95EF5} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {ED81B543-714A-484C-ACB9-CF70308B045F} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {BBD6E614-37A8-4E86-88A7-2CE8821C7CB2} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {350488E5-F800-4CBD-9278-1D5C84C78958} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {CF768605-BAB8-42D8-A748-66791575BCFD} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {35C5F3EF-B192-4A07-B664-A3D360283907} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {29FEE730-0D2C-4F31-934E-9BE2E9DC729A} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {80F8703D-804D-4921-A409-AF4BB26E59BE} = {7462D089-D350-44D6-8131-896D949A65B7} - {AADFF9B1-B275-48B4-8F32-F6CD837619E5} = {7462D089-D350-44D6-8131-896D949A65B7} - {CD2B383F-2957-4A83-B965-070DC8CF36F6} = {7462D089-D350-44D6-8131-896D949A65B7} - {3DFDEFBE-D2BD-4F25-8A26-66AFB62B9C4C} = {7462D089-D350-44D6-8131-896D949A65B7} - {C08D0A27-6F28-4845-91E7-5C1A0C926939} = {7462D089-D350-44D6-8131-896D949A65B7} - {39592692-847D-49F0-97DE-3F2F21677319} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {E2574A41-D9BF-4071-84D0-DB0F6D1148EE} = {7462D089-D350-44D6-8131-896D949A65B7} - {1D3C84AE-2818-4873-A222-BE6D329E850D} = {7462D089-D350-44D6-8131-896D949A65B7} - {3ED68361-0D3D-45F5-8A83-444DD3D5C1BF} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {14648C7D-8958-4C94-B837-7F90F2F10081} = {7462D089-D350-44D6-8131-896D949A65B7} - {F72E72DE-F163-4BCC-A442-94DB331D3C35} = {7462D089-D350-44D6-8131-896D949A65B7} - {2A319702-6C2E-4184-8E71-43F87745B38D} = {7462D089-D350-44D6-8131-896D949A65B7} - {91F00C73-78E5-4400-8F9F-910BE99EDFD5} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {738F2483-286E-4517-9660-5BFEEF343595} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {31AF3FED-A6C5-4DD5-99DF-03D4B4186D39} = {7462D089-D350-44D6-8131-896D949A65B7} - {9EF119AD-4168-49F8-B2B2-8F6F1B5E47C8} = {7462D089-D350-44D6-8131-896D949A65B7} - {CF2FA420-6295-4F83-8F87-340D8365282D} = {7462D089-D350-44D6-8131-896D949A65B7} - {8B7EEF6D-DFF2-4D7B-9E5D-6CA34A8BC260} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {480E09E8-A0C4-49A7-AAC4-4AA5EC0733C5} = {7462D089-D350-44D6-8131-896D949A65B7} - {670D3B20-401F-4004-AD0B-865560D02CC3} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {07F3E312-EC4C-4B71-A095-E478DF4CB52B} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {414B6DBE-1A3A-42DC-9116-3F9A433C6BBB} = {7462D089-D350-44D6-8131-896D949A65B7} - {4AD904D1-7727-48EF-88D9-7B38D98EB314} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {2B7E7416-E093-4763-B839-504CBFBC8F1C} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {F6FFA925-8C49-4594-9FF2-8F571C2D6389} = {7462D089-D350-44D6-8131-896D949A65B7} - {151FB103-4BCA-41D2-9C8E-C5BDA7A68320} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {D27B9558-0498-401A-AD3E-9BF15EF66E88} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {545245AB-A255-4176-91E6-C837A21648D8} = {7462D089-D350-44D6-8131-896D949A65B7} - {34F7650B-6135-4C08-9B42-7E7DBA7D186F} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {935A1DDC-6709-4521-A072-37ACC288371F} = {7462D089-D350-44D6-8131-896D949A65B7} - {560AAB82-9530-4A59-9C68-A9C8C586A5AA} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {DD1057EF-C9AE-42B9-B56D-DC49013D175D} = {7462D089-D350-44D6-8131-896D949A65B7} - {7FCCCA0F-FE75-4847-B464-982C02661F07} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {52E5E2D0-7DD2-429F-8D3F-805648332D66} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {0B08ACE1-F3BF-4375-9381-22ACA1ECD387} = {7462D089-D350-44D6-8131-896D949A65B7} - {E541AEB3-45D8-4089-A2BD-45859999A30D} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {EDA108F7-2C3D-4C7B-A1C1-286D6FB87761} = {7462D089-D350-44D6-8131-896D949A65B7} - {30214E4D-7E4E-4C98-B489-FCB0A16DF2C4} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {83926ADB-A63C-4575-AC99-2D420FFE32CA} = {7462D089-D350-44D6-8131-896D949A65B7} - {2A8E659F-3C92-4C5F-BB82-5EE91C15123A} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {A26F7BAB-493A-45F4-AB0C-86BE84B20DBA} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {0638755B-FA20-4A94-98D4-396AFAA5789F} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {FA37C0D9-306B-42E9-890E-73BED531C091} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {317C2668-0920-492A-90D7-FF9670344DEE} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {AD65B505-8EDE-4B54-9298-6B71D0283EB1} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {AA04790E-58AC-40CC-9DEC-570D78D252AD} = {7462D089-D350-44D6-8131-896D949A65B7} - {B98BC881-DDDD-4988-AC43-187C733895CD} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {D1B67116-DB1C-4CE7-954F-AD9E17C06061} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {08D371ED-04DE-4CE3-AD1B-F6CC6D341706} = {7637D30E-7339-4D4E-9424-87CF2394D234} - {E5B248B8-A2E1-46CB-9281-D5871F1483C3} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {78979585-F881-4ACD-9E83-CCB866EB971C} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {655739D7-87E9-43EF-B522-AC421F7451A4} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22} = {7462D089-D350-44D6-8131-896D949A65B7} - {669FA147-3B41-4841-921A-55B019C3AF26} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {EA77D317-8767-4DDE-8038-820D582C52D6} = {7462D089-D350-44D6-8131-896D949A65B7} - {68726F89-B254-424B-BE17-AC9312484464} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} - {BB753F80-E4F3-4510-2AE6-4DF7F004941D} = {7462D089-D350-44D6-8131-896D949A65B7} - {5C9FBB6B-2CD6-4812-9CD7-BFA36734D55C} = {BB753F80-E4F3-4510-2AE6-4DF7F004941D} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {E4D94C77-6657-4630-9D42-0A9AC5153A1B} - EndGlobalSection -EndGlobal diff --git a/src/HotChocolate/Core/HotChocolate.Core.slnx b/src/HotChocolate/Core/HotChocolate.Core.slnx new file mode 100644 index 00000000000..cadf7faeb97 --- /dev/null +++ b/src/HotChocolate/Core/HotChocolate.Core.slnx @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Core/HotChocolate.Core.csproj b/src/HotChocolate/Core/src/Core/HotChocolate.Core.csproj index 699f67e5426..8c5f1883691 100644 --- a/src/HotChocolate/Core/src/Core/HotChocolate.Core.csproj +++ b/src/HotChocolate/Core/src/Core/HotChocolate.Core.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ConnectionTypeFileBuilder.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ConnectionTypeFileBuilder.cs index a822841c7b4..99a483db7e0 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ConnectionTypeFileBuilder.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ConnectionTypeFileBuilder.cs @@ -51,7 +51,8 @@ public override void WriteInitializeMethod(IOutputTypeInfo type) } if (connectionType.RuntimeType.IsGenericType - && !string.IsNullOrEmpty(connectionType.NameFormat)) + && !string.IsNullOrEmpty(connectionType.NameFormat) + && connectionType.NameFormat!.Contains("{0}")) { var nodeTypeName = connectionType.RuntimeType.TypeArguments[0].ToFullyQualified(); Writer.WriteLine(); @@ -68,8 +69,7 @@ public override void WriteInitializeMethod(IOutputTypeInfo type) ".DependsOn(nodeTypeRef);"); } } - else if (!string.IsNullOrEmpty(connectionType.NameFormat) - && !connectionType.NameFormat!.Contains("{0}")) + else if (!string.IsNullOrEmpty(connectionType.NameFormat)) { Writer.WriteLine(); Writer.WriteIndentedLine( diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/EdgeTypeFileBuilder.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/EdgeTypeFileBuilder.cs index d35333eeec7..b5a74a4cb7c 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/EdgeTypeFileBuilder.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/EdgeTypeFileBuilder.cs @@ -50,7 +50,8 @@ public override void WriteInitializeMethod(IOutputTypeInfo type) } if (edgeType.RuntimeType.IsGenericType - && !string.IsNullOrEmpty(edgeType.NameFormat)) + && !string.IsNullOrEmpty(edgeType.NameFormat) + && edgeType.NameFormat!.Contains("{0}")) { var nodeTypeName = edgeType.RuntimeType.TypeArguments[0].ToFullyQualified(); Writer.WriteLine(); @@ -67,8 +68,7 @@ public override void WriteInitializeMethod(IOutputTypeInfo type) ".DependsOn(nodeTypeRef);"); } } - else if (!string.IsNullOrEmpty(edgeType.NameFormat) - && !edgeType.NameFormat!.Contains("{0}")) + else if (!string.IsNullOrEmpty(edgeType.NameFormat)) { Writer.WriteLine(); Writer.WriteIndentedLine( diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs index 78373666bca..5f1feab0913 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs @@ -130,13 +130,13 @@ public ImmutableArray Transform( edge.Type, edgeClass.ClassDeclarations, edge.Name, - edge.NameFormat) + edge.NameFormat ?? edge.Name) : EdgeTypeInfo.CreateEdge( compilation, edge.Type, null, edge.Name, - edge.NameFormat); + edge.NameFormat ?? edge.Name); connectionTypeInfo = connectionClassLookup.TryGetValue(connection.TypeDefinitionName, out connectionClass) @@ -146,14 +146,14 @@ public ImmutableArray Transform( connectionClass.ClassDeclarations, edgeTypeInfo.Name, connection.Name, - connection.NameFormat) + connection.NameFormat ?? connection.Name) : ConnectionTypeInfo.CreateConnection( compilation, connection.Type, null, edgeTypeInfo.Name, connection.Name, - connection.NameFormat); + connection.NameFormat ?? connection.Name); var connectionTypeName = "global::" + connectionTypeInfo.Namespace + "." + connectionTypeInfo.Name; var edgeTypeName = "global::" + edgeTypeInfo.Namespace + "." + edgeTypeInfo.Name; diff --git a/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs b/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs index c353a9c21fd..a76024dcfd8 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs @@ -18,7 +18,7 @@ public static bool TryGetConnectionNameFromResolver( return false; } - const string connectionName = "ConnectionName"; + const string connectionName = "Name"; const string connection = "Connection"; foreach (var attributeData in resolver.GetAttributes()) diff --git a/src/HotChocolate/Core/src/Types.CursorPagination.Extensions/HotChocolate.Types.CursorPagination.Extensions.csproj b/src/HotChocolate/Core/src/Types.CursorPagination.Extensions/HotChocolate.Types.CursorPagination.Extensions.csproj new file mode 100644 index 00000000000..5feb13efc35 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.CursorPagination.Extensions/HotChocolate.Types.CursorPagination.Extensions.csproj @@ -0,0 +1,17 @@ + + + + HotChocolate.Types.CursorPagination.Extensions + HotChocolate.Types.CursorPagination.Extensions + HotChocolate.Types.Pagination + Contains middleware and types for cursor based pagination. + + + + + + + + + + diff --git a/src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageConnection.cs b/src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageConnection.cs new file mode 100644 index 00000000000..d1dd52f0826 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageConnection.cs @@ -0,0 +1,80 @@ +using GreenDonut.Data; + +namespace HotChocolate.Types.Pagination; + +/// +/// A connection to a list of items. +/// +/// +/// The type of the node. +/// +[GraphQLName("{0}Connection")] +[GraphQLDescription("A connection to a list of items.")] +public class PageConnection : ConnectionBase, PageInfo> +{ + private readonly Page _page; + private readonly int _maxRelativeCursorCount; + private PageEdge[]? _edges; + private PageInfo? _pageInfo; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The page that contains the data. + /// + /// + /// The maximum number of relative cursors to create. + /// + public PageConnection(Page page, int maxRelativeCursorCount = 5) + { + ArgumentNullException.ThrowIfNull(page); + ArgumentOutOfRangeException.ThrowIfNegative(maxRelativeCursorCount); + + _page = page; + _maxRelativeCursorCount = maxRelativeCursorCount; + } + + /// + /// A list of edges. + /// + [GraphQLDescription("A list of edges.")] + public override IReadOnlyList>? Edges + { + get + { + if (_edges is null) + { + var items = _page.Items; + var edges = new PageEdge[items.Length]; + + for (var i = 0; i < items.Length; i++) + { + edges[i] = new PageEdge(_page, items[i]); + } + + _edges = edges; + } + + return _edges; + } + } + + /// + /// A flattened list of the nodes. + /// + [GraphQLDescription("A flattened list of the nodes")] + public virtual IReadOnlyList? Nodes => _page.Items; + + /// + /// Information to aid in pagination. + /// + [GraphQLDescription("Information to aid in pagination.")] + public override PageInfo PageInfo => _pageInfo ??= new PageInfo(_page, _maxRelativeCursorCount); + + /// + /// Identifies the total count of items in the connection. + /// + [GraphQLDescription("Identifies the total count of items in the connection.")] + public int TotalCount => _page.TotalCount ?? -1; +} diff --git a/src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageCursorType.cs b/src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageCursorType.cs new file mode 100644 index 00000000000..6f34091307d --- /dev/null +++ b/src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageCursorType.cs @@ -0,0 +1,21 @@ +using GreenDonut.Data; + +namespace HotChocolate.Types.Pagination; + +internal sealed class PageCursorType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Name("PageCursor") + .Description("A cursor that points to a specific page."); + + descriptor + .Field(t => t.Page) + .Description("The page number."); + + descriptor + .Field(t => t.Cursor) + .Description("The cursor."); + } +} diff --git a/src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageEdge.cs b/src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageEdge.cs new file mode 100644 index 00000000000..e8eca765fd5 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageEdge.cs @@ -0,0 +1,33 @@ +using GreenDonut.Data; + +namespace HotChocolate.Types.Pagination; + +/// +/// An edge in a connection. +/// +/// +/// The page that contains the node. +/// +/// +/// The node that is part of the edge. +/// +/// +/// The type of the node. +/// +[GraphQLName("{0}Edge")] +public class PageEdge(Page page, TNode node) : IEdge +{ + /// + /// The item at the end of the edge. + /// + [GraphQLDescription("The item at the end of the edge.")] + public TNode Node => node; + + /// + /// A cursor for use in pagination. + /// + [GraphQLDescription("A cursor for use in pagination.")] + public string Cursor => page.CreateCursor(node); + + object? IEdge.Node => Node; +} diff --git a/src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageInfo.cs b/src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageInfo.cs new file mode 100644 index 00000000000..4d7ea79fda8 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.CursorPagination.Extensions/PageInfo.cs @@ -0,0 +1,101 @@ +using GreenDonut.Data; + +namespace HotChocolate.Types.Pagination; + +/// +/// Information about pagination in a connection. +/// +[GraphQLDescription( + "Information about pagination in a connection.")] +public abstract class PageInfo : IPageInfo +{ + + /// + /// Indicates whether more edges exist following + /// the set defined by the clients arguments. + /// + [GraphQLDescription( + "Indicates whether more edges exist following " + + "the set defined by the clients arguments.")] + public abstract bool HasNextPage { get; } + + /// + /// Indicates whether more edges exist prior + /// the set defined by the clients arguments. + /// + [GraphQLDescription( + "Indicates whether more edges exist prior " + + "the set defined by the clients arguments.")] + public abstract bool HasPreviousPage { get; } + + /// + /// When paginating backwards, the cursor to continue. + /// + [GraphQLDescription( + "When paginating backwards, the cursor to continue.")] + public abstract string? StartCursor { get; } + + /// + /// When paginating forwards, the cursor to continue. + /// + [GraphQLDescription( + "When paginating forwards, the cursor to continue.")] + public abstract string? EndCursor { get; } + + /// + /// A list of cursors to continue paginating forwards. + /// + [GraphQLDescription( + "A list of cursors to continue paginating forwards.")] + [GraphQLType>>>] + public abstract IReadOnlyList ForwardCursors { get; } + + /// + /// A list of cursors to continue paginating backwards. + /// + [GraphQLDescription( + "A list of cursors to continue paginating backwards.")] + [GraphQLType>>>] + public abstract IReadOnlyList BackwardCursors { get; } +} + +/// +/// Information about pagination in a connection. +/// +/// +/// The page that contains the data. +/// +/// +/// The maximum number of relative cursors to create. +/// +/// +/// The type of the node. +/// +public class PageInfo(Page page, int maxRelativeCursorCount = 5) : PageInfo +{ + /// + public override bool HasNextPage => page.HasNextPage; + + /// + public override bool HasPreviousPage => page.HasPreviousPage; + + /// + public override string? StartCursor + => page.First is not null + ? page.CreateCursor(page.First) + : null; + + /// + public override string? EndCursor + => page.Last is not null + ? page.CreateCursor(page.Last) + : null; + + /// + public override IReadOnlyList ForwardCursors + => page.CreateRelativeForwardCursors(maxRelativeCursorCount); + + /// + public override IReadOnlyList BackwardCursors + => page.CreateRelativeBackwardCursors(maxRelativeCursorCount); +} diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionBase.cs b/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionBase.cs index ee3266166fb..cf25128f358 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionBase.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionBase.cs @@ -45,3 +45,4 @@ void IPage.Accept(IPageObserver observer) ArrayPool.Shared.Return(buffer); } } + diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UseConnectionAttribute.cs b/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UseConnectionAttribute.cs index aaaa49fa5e3..626ff2d86e4 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UseConnectionAttribute.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UseConnectionAttribute.cs @@ -98,7 +98,7 @@ public bool EnableRelativeCursors set => _EnableRelativeCursors = value; } - public string? ConnectionName { get; set; } + public string? Name { get; set; } protected internal override void TryConfigure( IDescriptorContext context, @@ -118,7 +118,7 @@ protected internal override void TryConfigure( AllowBackwardPagination = _allowBackwardPagination, RequirePagingBoundaries = _requirePagingBoundaries, InferConnectionNameFromField = _inferConnectionNameFromField, - ProviderName = ConnectionName, + ProviderName = Name, EnableRelativeCursors = _EnableRelativeCursors, }; diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/PagingTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/PagingTests.cs index c97ec61410c..75cf32a3bd7 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/PagingTests.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/PagingTests.cs @@ -169,7 +169,7 @@ public sealed class Author [QueryType] public static partial class AuthorQueries { - [UseConnection(ConnectionName = "Authors123")] + [UseConnection(Name = "Authors123")] public static Task GetAuthorsAsync( GreenDonut.Data.PagingArguments pagingArgs, CancellationToken cancellationToken) @@ -384,7 +384,7 @@ public static Task> GetAuthorsAsync( CancellationToken cancellationToken) => default!; - [UseConnection(ConnectionName = "Authors2")] + [UseConnection(Name = "Authors2")] public static Task> GetAuthors2Async( GreenDonut.Data.PagingArguments pagingArgs, CancellationToken cancellationToken) diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_WithConnectionName_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_WithConnectionName_MatchesSnapshot.md index a8a787585e5..7a461f9ddcc 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_WithConnectionName_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_WithConnectionName_MatchesSnapshot.md @@ -474,6 +474,8 @@ namespace TestNamespace var bindingResolver = extend.Context.ParameterBindingResolver; var resolvers = new __Resolvers(); + descriptor.Name("Authors2Connection"); + descriptor .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) .Type>>() @@ -586,6 +588,8 @@ namespace TestNamespace var bindingResolver = extend.Context.ParameterBindingResolver; var resolvers = new __Resolvers(); + descriptor.Name("Authors2Edge"); + descriptor .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) .ExtendWith(static (c, r) => diff --git a/src/HotChocolate/Core/test/Types.OffsetPagination.Tests/HotChocolate.Types.OffsetPagination.Tests.csproj b/src/HotChocolate/Core/test/Types.OffsetPagination.Tests/HotChocolate.Types.OffsetPagination.Tests.csproj index c7a172e7841..9168efc130e 100644 --- a/src/HotChocolate/Core/test/Types.OffsetPagination.Tests/HotChocolate.Types.OffsetPagination.Tests.csproj +++ b/src/HotChocolate/Core/test/Types.OffsetPagination.Tests/HotChocolate.Types.OffsetPagination.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/src/HotChocolate/Core/test/Types.Tests/HotChocolate.Types.Tests.csproj b/src/HotChocolate/Core/test/Types.Tests/HotChocolate.Types.Tests.csproj index 3b56f864487..d64c10bb944 100644 --- a/src/HotChocolate/Core/test/Types.Tests/HotChocolate.Types.Tests.csproj +++ b/src/HotChocolate/Core/test/Types.Tests/HotChocolate.Types.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/HotChocolate.CostAnalysis.Tests.csproj b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/HotChocolate.CostAnalysis.Tests.csproj index f4bdd467f68..cc2f42f54f2 100644 --- a/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/HotChocolate.CostAnalysis.Tests.csproj +++ b/src/HotChocolate/CostAnalysis/test/CostAnalysis.Tests/HotChocolate.CostAnalysis.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/src/HotChocolate/Data/test/Data.EntityFramework.Tests/HotChocolate.Data.EntityFramework.Tests.csproj b/src/HotChocolate/Data/test/Data.EntityFramework.Tests/HotChocolate.Data.EntityFramework.Tests.csproj index 7a388b1b185..c8d945351cf 100644 --- a/src/HotChocolate/Data/test/Data.EntityFramework.Tests/HotChocolate.Data.EntityFramework.Tests.csproj +++ b/src/HotChocolate/Data/test/Data.EntityFramework.Tests/HotChocolate.Data.EntityFramework.Tests.csproj @@ -5,6 +5,7 @@ + diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs index 66383f0a051..4994afb7f5d 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs @@ -39,8 +39,7 @@ public async Task Query_Brands() } } } - """, - interceptor); + """); // assert MatchSnapshot(result, interceptor); @@ -63,8 +62,7 @@ public async Task Query_Brands_First_2() } } } - """, - interceptor); + """); // assert MatchSnapshot(result, interceptor); @@ -93,8 +91,7 @@ public async Task Query_Brands_First_2_And_Products_First_2() } } } - """, - interceptor); + """); // assert MatchSnapshot(result, interceptor); @@ -123,8 +120,7 @@ public async Task Query_Brands_First_2_And_Products_First_2_Name_Desc() } } } - """, - interceptor); + """); // assert MatchSnapshot(result, interceptor); @@ -155,8 +151,7 @@ public async Task Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name } } } - """, - interceptor); + """); // assert MatchSnapshot(result, interceptor); @@ -179,8 +174,7 @@ ... on Product { } } } - """, - interceptor); + """); // assert MatchSnapshot(result, interceptor); @@ -203,8 +197,7 @@ ... on Brand { } } } - """, - interceptor); + """); // assert MatchSnapshot(result, interceptor); @@ -230,8 +223,7 @@ public async Task Query_Products_First_2_And_Brand() } } - """, - interceptor); + """); // assert MatchSnapshot(result, interceptor); @@ -258,8 +250,7 @@ public async Task Query_Products_First_2_With_4_EndCursors() } } - """, - interceptor); + """); // assert MatchSnapshot(result, interceptor); @@ -286,8 +277,37 @@ public async Task Query_Products_First_2_With_4_EndCursors_Skip_4() } } - """, - interceptor); + """); + + // assert + MatchSnapshot(result, interceptor); + } + + [Fact] + public async Task Query_Brands_First_2_Products_First_2_ForwardCursors() + { + // arrange + using var interceptor = new TestQueryInterceptor(); + + // act + var result = await ExecuteAsync( + """ + { + brands(first: 1) { + nodes { + name + products(first: 2) { + nodes { + name + } + pageInfo { + forwardCursors { page cursor } + } + } + } + } + } + """); // assert MatchSnapshot(result, interceptor); @@ -311,8 +331,7 @@ public async Task Query_Products_Include_TotalCount() } } - """, - interceptor); + """); // assert MatchSnapshot(result, interceptor); @@ -335,8 +354,7 @@ public async Task Query_Products_Exclude_TotalCount() } } - """, - interceptor); + """); // assert MatchSnapshot(result, interceptor); @@ -368,9 +386,7 @@ private static ServiceProvider CreateServer(string connectionString) return services.BuildServiceProvider(); } - private async Task ExecuteAsync( - string sourceText, - TestQueryInterceptor queryInterceptor) + private async Task ExecuteAsync(string sourceText) { var db = "db_" + Guid.NewGuid().ToString("N"); var connectionString = resource.GetConnectionString(db); diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs index 05cf159cecd..251e6a7fbb3 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs @@ -1,18 +1,18 @@ using GreenDonut.Data; using HotChocolate.Data.Models; using HotChocolate.Data.Services; -using HotChocolate.Data.Types.Products; using HotChocolate.Types; +using HotChocolate.Types.Pagination; namespace HotChocolate.Data.Types.Brands; [ObjectType] public static partial class BrandNode { - [UseConnection] + [UseConnection(Name = "BrandProducts", EnableRelativeCursors = true)] [UseFiltering] [UseSorting] - public static async Task GetProductsAsync( + public static async Task> GetProductsAsync( [Parent(requires: nameof(Brand.Id))] Brand brand, PagingArguments pagingArgs, QueryContext query, @@ -20,6 +20,6 @@ public static async Task GetProductsAsync( CancellationToken cancellationToken) { var page = await productService.GetProductsByBrandAsync(brand.Id, pagingArgs, query, cancellationToken); - return new ProductConnection(page); + return new PageConnection(page); } } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductNode.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductNode.cs index bf76f8e0f64..692d38492d1 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductNode.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductNode.cs @@ -1,7 +1,6 @@ using GreenDonut.Data; using HotChocolate.Data.Models; using HotChocolate.Data.Services; -using HotChocolate.Execution.Processing; using HotChocolate.Types; using HotChocolate.Types.Relay; diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql index 17f39130803..34e9a949caa 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql @@ -8,7 +8,7 @@ interface Node { } type Brand implements Node { - products("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ProductFilterInput @cost(weight: "10") order: [ProductSortInput!] @cost(weight: "10")): ProductConnection! @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + products("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ProductFilterInput @cost(weight: "10") order: [ProductSortInput!] @cost(weight: "10")): BrandProductsConnection! @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") id: ID! name: String! } @@ -25,6 +25,25 @@ type BrandEdge { cursor: String! } +"A connection to a list of items." +type BrandProductsConnection { + "A list of edges." + edges: [BrandProductsEdge!] + "A flattened list of the nodes" + nodes: [Product!] + "Information to aid in pagination." + pageInfo: PageInfo! + "Identifies the total count of items in the connection." + totalCount: Int! @cost(weight: "10") +} + +type BrandProductsEdge { + "The item at the end of the edge." + node: Product! + "A cursor for use in pagination." + cursor: String! +} + """ Represents the connection page info. This class provides additional information about pagination in a connection. @@ -46,6 +65,30 @@ type ConnectionPageInfo { endCursor: String } +"A cursor that points to a specific page." +type PageCursor { + "The page number." + page: Int! + "The cursor." + cursor: String! +} + +"Information about pagination in a connection." +type PageInfo { + "Indicates whether more edges exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more edges exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! + "When paginating backwards, the cursor to continue." + startCursor: String + "When paginating forwards, the cursor to continue." + endCursor: String + "A list of cursors to continue paginating forwards." + forwardCursors: [PageCursor!]! + "A list of cursors to continue paginating backwards." + backwardCursors: [PageCursor!]! +} + type Product implements Node { id: ID! brand: Brand @cost(weight: "10") diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2.md index 557b3e9603f..39e218f0435 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2.md @@ -57,6 +57,16 @@ LIMIT @__p_0 ## Query 2 +```sql +-- @__brandIds_0={ '11', '13' } (DbType = Object) +SELECT p."BrandId" AS "Key", count(*)::int AS "Count" +FROM "Products" AS p +WHERE p."BrandId" = ANY (@__brandIds_0) +GROUP BY p."BrandId" +``` + +## Query 3 + ```sql -- @__brandIds_0={ '11', '13' } (DbType = Object) SELECT p1."BrandId", p3."Id", p3."Name", p3."BrandId" diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc.md index 5555f930a48..fcbc1189af7 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc.md @@ -57,6 +57,16 @@ LIMIT @__p_0 ## Query 2 +```sql +-- @__brandIds_0={ '11', '13' } (DbType = Object) +SELECT p."BrandId" AS "Key", count(*)::int AS "Count" +FROM "Products" AS p +WHERE p."BrandId" = ANY (@__brandIds_0) +GROUP BY p."BrandId" +``` + +## Query 3 + ```sql -- @__brandIds_0={ '11', '13' } (DbType = Object) SELECT p1."BrandId", p3."Id", p3."Name", p3."BrandId" diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name.md index a61d56c2869..592fe185f15 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name.md @@ -67,6 +67,16 @@ LIMIT @__p_0 ## Query 2 +```sql +-- @__brandIds_0={ '11', '13' } (DbType = Object) +SELECT p."BrandId" AS "Key", count(*)::int AS "Count" +FROM "Products" AS p +WHERE p."BrandId" = ANY (@__brandIds_0) +GROUP BY p."BrandId" +``` + +## Query 3 + ```sql -- @__brandIds_0={ '11', '13' } (DbType = Object) SELECT p1."BrandId", p3."Id", p3."Name", p3."BrandId" @@ -88,7 +98,7 @@ LEFT JOIN ( ORDER BY p1."BrandId", p3."BrandId", p3."Name" DESC, p3."Id" ``` -## Query 3 +## Query 4 ```sql -- @__ids_0={ '11', '13' } (DbType = Object) diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name__net_8_0.md index 5dfd0017038..8ad2f211de8 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name__net_8_0.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name__net_8_0.md @@ -67,6 +67,16 @@ LIMIT @__p_0 ## Query 2 +```sql +-- @__brandIds_0={ '11', '13' } (DbType = Object) +SELECT p."BrandId" AS "Key", count(*)::int AS "Count" +FROM "Products" AS p +WHERE p."BrandId" = ANY (@__brandIds_0) +GROUP BY p."BrandId" +``` + +## Query 3 + ```sql -- @__brandIds_0={ '11', '13' } (DbType = Object) SELECT t."BrandId", t0."Id", t0."Name", t0."BrandId" @@ -88,7 +98,7 @@ LEFT JOIN ( ORDER BY t."BrandId", t0."BrandId", t0."Name" DESC, t0."Id" ``` -## Query 3 +## Query 4 ```sql -- @__ids_0={ '11', '13' } (DbType = Object) diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc__net_8_0.md index 9bd64e70920..44906f23898 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc__net_8_0.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc__net_8_0.md @@ -57,6 +57,16 @@ LIMIT @__p_0 ## Query 2 +```sql +-- @__brandIds_0={ '11', '13' } (DbType = Object) +SELECT p."BrandId" AS "Key", count(*)::int AS "Count" +FROM "Products" AS p +WHERE p."BrandId" = ANY (@__brandIds_0) +GROUP BY p."BrandId" +``` + +## Query 3 + ```sql -- @__brandIds_0={ '11', '13' } (DbType = Object) SELECT t."BrandId", t0."Id", t0."Name", t0."BrandId" diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2__net_8_0.md index 65f7aa4680e..7a9a1939736 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2__net_8_0.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2__net_8_0.md @@ -57,6 +57,16 @@ LIMIT @__p_0 ## Query 2 +```sql +-- @__brandIds_0={ '11', '13' } (DbType = Object) +SELECT p."BrandId" AS "Key", count(*)::int AS "Count" +FROM "Products" AS p +WHERE p."BrandId" = ANY (@__brandIds_0) +GROUP BY p."BrandId" +``` + +## Query 3 + ```sql -- @__brandIds_0={ '11', '13' } (DbType = Object) SELECT t."BrandId", t0."Id", t0."Name", t0."BrandId" diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_Products_First_2_ForwardCursors.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_Products_First_2_ForwardCursors.md new file mode 100644 index 00000000000..8ea463f6b9a --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_Products_First_2_ForwardCursors.md @@ -0,0 +1,83 @@ +# Query_Brands_First_2_Products_First_2_ForwardCursors + +## Result + +```json +{ + "data": { + "brands": { + "nodes": [ + { + "name": "Zephyr", + "products": { + "nodes": [ + { + "name": "Powder Pro Snowboard" + }, + { + "name": "Summit Pro Climbing Harness" + } + ], + "pageInfo": { + "forwardCursors": [ + { + "page": 2, + "cursor": "ezB8MXw2fTIz" + }, + { + "page": 3, + "cursor": "ezF8MXw2fTIz" + } + ] + } + } + } + ] + } + } +} +``` + +## Query 1 + +```sql +-- @__p_0='2' +SELECT b."Name", b."Id" +FROM "Brands" AS b +ORDER BY b."Name" DESC, b."Id" +LIMIT @__p_0 +``` + +## Query 2 + +```sql +-- @__brandIds_0={ '11' } (DbType = Object) +SELECT p."BrandId" AS "Key", count(*)::int AS "Count" +FROM "Products" AS p +WHERE p."BrandId" = ANY (@__brandIds_0) +GROUP BY p."BrandId" +``` + +## Query 3 + +```sql +-- @__brandIds_0={ '11' } (DbType = Object) +SELECT p1."BrandId", p3."Name", p3."Id", p3."BrandId" +FROM ( + SELECT p."BrandId" + FROM "Products" AS p + WHERE p."BrandId" = ANY (@__brandIds_0) + GROUP BY p."BrandId" +) AS p1 +LEFT JOIN ( + SELECT p2."Name", p2."Id", p2."BrandId" + FROM ( + SELECT p0."Name", p0."Id", p0."BrandId", ROW_NUMBER() OVER(PARTITION BY p0."BrandId" ORDER BY p0."Id") AS row + FROM "Products" AS p0 + WHERE p0."BrandId" = ANY (@__brandIds_0) + ) AS p2 + WHERE p2.row <= 3 +) AS p3 ON p1."BrandId" = p3."BrandId" +ORDER BY p1."BrandId", p3."BrandId", p3."Id" +``` + diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_Products_First_2_ForwardCursors__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_Products_First_2_ForwardCursors__net_8_0.md new file mode 100644 index 00000000000..dd4de596661 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_Products_First_2_ForwardCursors__net_8_0.md @@ -0,0 +1,83 @@ +# Query_Brands_First_2_Products_First_2_ForwardCursors + +## Result + +```json +{ + "data": { + "brands": { + "nodes": [ + { + "name": "Zephyr", + "products": { + "nodes": [ + { + "name": "Powder Pro Snowboard" + }, + { + "name": "Summit Pro Climbing Harness" + } + ], + "pageInfo": { + "forwardCursors": [ + { + "page": 2, + "cursor": "ezB8MXw2fTIz" + }, + { + "page": 3, + "cursor": "ezF8MXw2fTIz" + } + ] + } + } + } + ] + } + } +} +``` + +## Query 1 + +```sql +-- @__p_0='2' +SELECT b."Name", b."Id" +FROM "Brands" AS b +ORDER BY b."Name" DESC, b."Id" +LIMIT @__p_0 +``` + +## Query 2 + +```sql +-- @__brandIds_0={ '11' } (DbType = Object) +SELECT p."BrandId" AS "Key", count(*)::int AS "Count" +FROM "Products" AS p +WHERE p."BrandId" = ANY (@__brandIds_0) +GROUP BY p."BrandId" +``` + +## Query 3 + +```sql +-- @__brandIds_0={ '11' } (DbType = Object) +SELECT t."BrandId", t0."Name", t0."Id", t0."BrandId" +FROM ( + SELECT p."BrandId" + FROM "Products" AS p + WHERE p."BrandId" = ANY (@__brandIds_0) + GROUP BY p."BrandId" +) AS t +LEFT JOIN ( + SELECT t1."Name", t1."Id", t1."BrandId" + FROM ( + SELECT p0."Name", p0."Id", p0."BrandId", ROW_NUMBER() OVER(PARTITION BY p0."BrandId" ORDER BY p0."Id") AS row + FROM "Products" AS p0 + WHERE p0."BrandId" = ANY (@__brandIds_0) + ) AS t1 + WHERE t1.row <= 3 +) AS t0 ON t."BrandId" = t0."BrandId" +ORDER BY t."BrandId", t0."BrandId", t0."Id" +``` + From 66a8999352c4c8dfdbd5f4aae1f24af9a0ab47ee Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 14 Mar 2025 22:43:27 +0200 Subject: [PATCH 46/64] Refactored OPA authorization tests to work with a dynamic port (#8133) --- .../AuthorizationAttributeTestData.cs | 5 ++-- .../AuthorizationTestData.cs | 10 ++++--- .../AuthorizationTests.cs | 26 +++++++++++-------- .../OpaProcess/OpaProcess.cs | 7 ++++- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationAttributeTestData.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationAttributeTestData.cs index d621181de44..3c37c40b6d5 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationAttributeTestData.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationAttributeTestData.cs @@ -30,12 +30,13 @@ public class Query public string GetAfterResolver() => "foo"; } - private Action CreateSchema() => - builder => builder + private Action CreateSchema() => + (builder, port) => builder .AddQueryType() .AddOpaAuthorization( (_, o) => { + o.BaseAddress = new Uri($"http://127.0.0.1:{port}/v1/data/"); o.Timeout = TimeSpan.FromMilliseconds(60000); }) .AddOpaResultHandler( diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTestData.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTestData.cs index 04e37b255ce..9d2f91c532a 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTestData.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTestData.cs @@ -27,12 +27,13 @@ type Query {{ return next.Invoke(context); }; - private Action CreateSchema() => - sb => sb + private Action CreateSchema() => + (builder, port) => builder .AddDocumentFromString(_sdl) .AddOpaAuthorization( (_, o) => { + o.BaseAddress = new Uri($"http://127.0.0.1:{port}/v1/data/"); o.Timeout = TimeSpan.FromMilliseconds(60000); }) .AddOpaResultHandler( @@ -46,12 +47,13 @@ private Action CreateSchema() => }) .UseField(_schemaMiddleware); - private Action CreateSchemaWithBuilder() => - sb => sb + private Action CreateSchemaWithBuilder() => + (builder, port) => builder .AddDocumentFromString(_sdl) .AddOpaAuthorization( (_, o) => { + o.BaseAddress = new Uri($"http://127.0.0.1:{port}/v1/data/"); o.Timeout = TimeSpan.FromMilliseconds(60000); }) .AddOpaResultHandler( diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs index d9a3568ba20..a46c85b4e9b 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs @@ -33,13 +33,14 @@ private static void SetUpHttpContext(HttpContext context) [Theory] [ClassData(typeof(AuthorizationTestData))] [ClassData(typeof(AuthorizationAttributeTestData))] - public async Task Policy_NotFound(Action configure) + public async Task Policy_NotFound(Action configure) { // arrange + var port = _opaHandle!.GetPort(); var server = CreateTestServer( builder => { - configure(builder); + configure(builder, port); builder.Services.AddAuthorization(); }, SetUpHttpContext); @@ -55,13 +56,14 @@ public async Task Policy_NotFound(Action configure) [Theory] [ClassData(typeof(AuthorizationTestData))] [ClassData(typeof(AuthorizationAttributeTestData))] - public async Task Policy_NotAuthorized(Action configure) + public async Task Policy_NotAuthorized(Action configure) { // arrange + var port = _opaHandle!.GetPort(); var server = CreateTestServer( builder => { - configure(builder); + configure(builder, port); builder.Services.AddAuthorization(); }, SetUpHttpContext + (Action)(c => @@ -72,7 +74,7 @@ public async Task Policy_NotAuthorized(Action configure })); var hasAgeDefinedPolicy = await File.ReadAllTextAsync("Policies/has_age_defined.rego"); - using var client = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:8181"), }; + using var client = new HttpClient { BaseAddress = new Uri($"http://127.0.0.1:{port}"), }; var putPolicyResponse = await client.PutAsync( "/v1/policies/has_age_defined", @@ -90,13 +92,14 @@ public async Task Policy_NotAuthorized(Action configure [Theory] [ClassData(typeof(AuthorizationTestData))] [ClassData(typeof(AuthorizationAttributeTestData))] - public async Task Policy_Authorized(Action configure) + public async Task Policy_Authorized(Action configure) { // arrange + var port = _opaHandle!.GetPort(); var server = CreateTestServer( builder => { - configure(builder); + configure(builder, port); builder.Services.AddAuthorization(); }, SetUpHttpContext + (Action)(c => @@ -108,7 +111,7 @@ public async Task Policy_Authorized(Action configure) })); var hasAgeDefinedPolicy = await File.ReadAllTextAsync("Policies/has_age_defined.rego"); - using var client = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:8181"), }; + using var client = new HttpClient { BaseAddress = new Uri($"http://127.0.0.1:{port}"), }; var putPolicyResponse = await client.PutAsync( "/v1/policies/has_age_defined", @@ -126,13 +129,14 @@ public async Task Policy_Authorized(Action configure) [Theory] [ClassData(typeof(AuthorizationTestData))] [ClassData(typeof(AuthorizationAttributeTestData))] - public async Task Policy_Authorized_WithExtensions(Action configure) + public async Task Policy_Authorized_WithExtensions(Action configure) { // arrange + var port = _opaHandle!.GetPort(); var server = CreateTestServer( builder => { - configure(builder); + configure(builder, port); builder.Services.AddAuthorization(); builder.AddOpaQueryRequestExtensionsHandler(Policies.HasDefinedAge, context => context.Resource is IMiddlewareContext or AuthorizationContext @@ -151,7 +155,7 @@ public async Task Policy_Authorized_WithExtensions(Action StartServerAsync() { var opaProcess = new OpaProcess(new ContainerBuilder() .WithImage("openpolicyagent/opa") - .WithPortBinding(8181, 8181) + .WithPortBinding(8181, assignRandomHostPort: true) .WithCommand( "run", "--server", "--addr", ":8181", @@ -29,6 +29,11 @@ public static async Task StartServerAsync() return opaProcess; } + public int GetPort() + { + return _container.GetMappedPublicPort(8181); + } + public async Task DisposeAsync() { await _container.DisposeAsync(); From fe3401c489babb61a131db6cc1ca5a03700a896b Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 17 Mar 2025 17:08:16 +0100 Subject: [PATCH 47/64] [Fusion] Fixed the composition for pattern based lookups. (#8138) --- .../Enrichers/PatternEntityEnricher.cs | 112 +++++++++++------- 1 file changed, 70 insertions(+), 42 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Composition/Pipeline/Enrichers/PatternEntityEnricher.cs b/src/HotChocolate/Fusion/src/Composition/Pipeline/Enrichers/PatternEntityEnricher.cs index 1dc3cfd6b71..26aea740a81 100644 --- a/src/HotChocolate/Fusion/src/Composition/Pipeline/Enrichers/PatternEntityEnricher.cs +++ b/src/HotChocolate/Fusion/src/Composition/Pipeline/Enrichers/PatternEntityEnricher.cs @@ -17,60 +17,82 @@ public ValueTask EnrichAsync( EntityGroup entity, CancellationToken cancellationToken = default) { + var completed = new HashSet<(string, string)>(); var regex = CreateRegex(); - foreach (var (type, schema) in entity.Parts) + foreach (var entitySchema in entity.Parts.Select(t => t.Schema)) { - if (schema.QueryType is null) + if (entitySchema.QueryType is null) { continue; } - foreach (var entityResolver in schema.QueryType.Fields) + foreach (var entityType in entity.Parts.Select(t => t.Type)) { - var originalTypeName = type.GetOriginalName(); - if (entityResolver.Name.AsSpan().StartsWith(originalTypeName, OrdinalIgnoreCase)) + var localTypeName = entity.Parts.First(t => t.Schema == entitySchema).Type.GetOriginalName(); + + foreach (var entityResolver in entitySchema.QueryType.Fields) { - var splits = regex.Split(entityResolver.Name); - if (splits.Length == 5) + if (completed.Contains((entitySchema.Name, entityResolver.Name))) { - var typeName = splits[1]; - var fieldName = splits[3]; - var isList = entityResolver.Type.IsListType(); + continue; + } - if (!isList && typeName.Equals(originalTypeName, OrdinalIgnoreCase)) - { - var field = type.Fields.FirstOrDefault(f => f.Name.Equals(fieldName, OrdinalIgnoreCase)); - if (field is not null) - { - TryRegisterEntityResolver(entity, type, entityResolver, field, schema); - } - } - else if (isList && typeName.Equals(originalTypeName, OrdinalIgnoreCase) || - (typeName.Length - 1 == originalTypeName.Length && - typeName.AsSpan()[typeName.Length - 1] == 's')) + + if (entityResolver.Name.StartsWith(localTypeName, OrdinalIgnoreCase)) + { + var splits = regex.Split(entityResolver.Name); + if (splits.Length == 5) { - var field = type.Fields.FirstOrDefault(f => f.Name.Equals(fieldName, OrdinalIgnoreCase)); + var typeName = splits[1]; + var fieldName = splits[3]; + var isList = entityResolver.Type.IsListType(); - if (field is null) + if (!isList && typeName.Equals(localTypeName, OrdinalIgnoreCase)) { - var fieldPlural = fieldName[..^1]; - field = type.Fields.FirstOrDefault(f => f.Name.Equals(fieldPlural, OrdinalIgnoreCase)); + var field = entityType.Fields.FirstOrDefault( + f => f.Name.Equals(fieldName, OrdinalIgnoreCase)); + if (field is not null && + TryRegisterEntityResolver(entity, entityType, entityResolver, field, entitySchema)) + { + completed.Add((entitySchema.Name, entityResolver.Name)); + } } - - if (field is not null) + else if ((isList && typeName.Equals(localTypeName, OrdinalIgnoreCase)) || + (typeName.Length - 1 == localTypeName.Length && + typeName.AsSpan()[typeName.Length - 1] == 's')) { - TryRegisterBatchEntityResolver(entity, type, entityResolver, field, schema); + var field = entityType.Fields.FirstOrDefault( + f => f.Name.Equals(fieldName, OrdinalIgnoreCase)); + + if (field is null) + { + var fieldPlural = fieldName[..^1]; + field = entityType.Fields.FirstOrDefault( + f => f.Name.Equals(fieldPlural, OrdinalIgnoreCase)); + } + + if (field is not null && + TryRegisterBatchEntityResolver( + entity, + entityType, + entityResolver, + field, + entitySchema)) + { + completed.Add((entitySchema.Name, entityResolver.Name)); + } } } } } } } + return default; } - private static void TryRegisterEntityResolver( + private static bool TryRegisterEntityResolver( EntityGroup entity, ObjectTypeDefinition entityType, OutputFieldDefinition entityResolverField, @@ -79,12 +101,12 @@ private static void TryRegisterEntityResolver( { if (!TryResolveKeyArgument(entityResolverField, keyField, out var keyArg)) { - return; + return false; } - if (entityResolverField.Type == entityType || + if (entityResolverField.Type.Equals(entityType, TypeComparison.Structural) || (entityResolverField.Type.Kind is TypeKind.NonNull && - entityResolverField.Type.InnerType() == entityType)) + entityResolverField.Type.InnerType().Equals(entityType, TypeComparison.Structural))) { var arguments = new List(); @@ -93,12 +115,12 @@ private static void TryRegisterEntityResolver( null, new NameNode(entityResolverField.GetOriginalName()), null, - Array.Empty(), + [], arguments, null); // Create a new SelectionSetNode for the entity resolver - var selectionSet = new SelectionSetNode(new[] { selection, }); + var selectionSet = new SelectionSetNode([selection]); // Create a new EntityResolver for the entity var resolver = new EntityResolver( @@ -111,8 +133,8 @@ private static void TryRegisterEntityResolver( null, new NameNode(keyField.Name), null, - Array.Empty(), - Array.Empty(), + [], + [], null); var keyFieldDirective = new IsDirective(keyFieldNode); @@ -122,7 +144,10 @@ private static void TryRegisterEntityResolver( // Add the new EntityResolver to the entity metadata entity.Metadata.EntityResolvers.TryAdd(resolver); + return true; } + + return false; } private static bool TryResolveKeyArgument( @@ -167,7 +192,7 @@ private static bool TryResolveKeyArgument( !keyArgument.ContainsIsDirective(); } - private static void TryRegisterBatchEntityResolver( + private static bool TryRegisterBatchEntityResolver( EntityGroup entity, ObjectTypeDefinition entityType, OutputFieldDefinition entityResolverField, @@ -176,7 +201,7 @@ private static void TryRegisterBatchEntityResolver( { if (!TryResolveBatchKeyArgument(entityResolverField, keyField, out var keyArg)) { - return; + return false; } var returnType = entityResolverField.Type; @@ -186,16 +211,16 @@ private static void TryRegisterBatchEntityResolver( returnType = returnType.InnerType(); } - if(returnType.Kind != TypeKind.List) + if (returnType.Kind != TypeKind.List) { - return; + return false; } returnType = returnType.InnerType(); - if (returnType == entityType || + if (returnType.Equals(entityType, TypeComparison.Structural) || (returnType.Kind is TypeKind.NonNull && - returnType.InnerType() == entityType)) + returnType.InnerType().Equals(entityType, TypeComparison.Structural))) { var arguments = new List(); @@ -233,7 +258,10 @@ private static void TryRegisterBatchEntityResolver( // Add the new EntityResolver to the entity metadata entity.Metadata.EntityResolvers.TryAdd(resolver); + return true; } + + return false; } private static bool TryResolveBatchKeyArgument( From d52c105803cb38444b6a354bccdf6247a3522b77 Mon Sep 17 00:00:00 2001 From: Tobias Tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 17 Mar 2025 16:12:06 +0100 Subject: [PATCH 48/64] Do not apply `@semanticNonNull` to `id` fields (#8136) --- .../Types/SemanticNonNullTypeInterceptor.cs | 8 +------ .../test/Types.Tests/SemanticNonNullTests.cs | 12 +++++----- ...NonNullTests.Object_Implementing_Node.snap | 22 ------------------- ...nticNonNullTests.Object_With_Id_Field.snap | 13 +++++++++++ 4 files changed, 19 insertions(+), 36 deletions(-) delete mode 100644 src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_Implementing_Node.snap create mode 100644 src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_With_Id_Field.snap diff --git a/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs index 01263751036..d7f93bb0924 100644 --- a/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs @@ -8,7 +8,6 @@ using HotChocolate.Types.Descriptors; using HotChocolate.Types.Descriptors.Definitions; using HotChocolate.Types.Helpers; -using HotChocolate.Types.Relay; using HotChocolate.Utilities; namespace HotChocolate; @@ -16,7 +15,6 @@ namespace HotChocolate; internal sealed class SemanticNonNullTypeInterceptor : TypeInterceptor { private ITypeInspector _typeInspector = null!; - private ExtendedTypeReference _nodeTypeReference = null!; internal override bool IsEnabled(IDescriptorContext context) => context.Options.EnableSemanticNonNull; @@ -29,8 +27,6 @@ internal override void InitializeContext( TypeReferenceResolver typeReferenceResolver) { _typeInspector = context.TypeInspector; - - _nodeTypeReference = _typeInspector.GetTypeRef(typeof(NodeType)); } /// @@ -92,8 +88,6 @@ public override void OnAfterCompleteName(ITypeCompletionContext completionContex return; } - var implementsNode = objectDef.Interfaces.Any(i => i.Equals(_nodeTypeReference)); - foreach (var field in objectDef.Fields) { if (field.IsIntrospectionField) @@ -101,7 +95,7 @@ public override void OnAfterCompleteName(ITypeCompletionContext completionContex continue; } - if (implementsNode && field.Name == "id") + if (field.Name == "id") { continue; } diff --git a/src/HotChocolate/Core/test/Types.Tests/SemanticNonNullTests.cs b/src/HotChocolate/Core/test/Types.Tests/SemanticNonNullTests.cs index f20ad4eaa9e..73805f235ca 100644 --- a/src/HotChocolate/Core/test/Types.Tests/SemanticNonNullTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/SemanticNonNullTests.cs @@ -11,7 +11,7 @@ namespace HotChocolate; public class SemanticNonNullTests { [Fact] - public async Task Object_Implementing_Node() + public async Task Object_With_Id_Field() { await new ServiceCollection() .AddGraphQL() @@ -20,8 +20,7 @@ public async Task Object_Implementing_Node() o.EnableSemanticNonNull = true; o.EnsureAllNodesCanBeResolved = false; }) - .AddQueryType() - .AddGlobalObjectIdentification() + .AddQueryType() .BuildSchemaAsync() .MatchSnapshotAsync(); } @@ -306,13 +305,12 @@ public class Foo } [ObjectType("Query")] - public class QueryWithNode + public class QueryWithTypeWithId { - public MyNode GetMyNode() => new(1); + public MyType GetMyNode() => new(1); } - [Node] - public record MyNode([property: ID] int Id); + public record MyType([property: ID] int Id); public class Mutation { diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_Implementing_Node.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_Implementing_Node.snap deleted file mode 100644 index 454e9932455..00000000000 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_Implementing_Node.snap +++ /dev/null @@ -1,22 +0,0 @@ -schema { - query: Query -} - -"The node interface is implemented by entities that have a global unique identifier." -interface Node { - id: ID! -} - -type MyNode implements Node { - id: ID! -} - -type Query { - "Fetches an object given its ID." - node("ID of the object." id: ID!): Node - "Lookup nodes by a list of IDs." - nodes("The list of node IDs." ids: [ID!]!): [Node]! - myNode: MyNode @semanticNonNull -} - -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_With_Id_Field.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_With_Id_Field.snap new file mode 100644 index 00000000000..01d6a381ac1 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_With_Id_Field.snap @@ -0,0 +1,13 @@ +schema { + query: Query +} + +type MyType { + id: ID! +} + +type Query { + myNode: MyType @semanticNonNull +} + +directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION From c73b1efebf79942b03c4448f628fda30fffb9c35 Mon Sep 17 00:00:00 2001 From: Glen Date: Mon, 17 Mar 2025 18:13:53 +0200 Subject: [PATCH 49/64] Added WebSocket payload formatter and options for GraphQL over WebSocket (#8135) Co-authored-by: Michael Staib --- ...rviceCollectionExtensions.Subscriptions.cs | 70 +++++++++++++++++-- .../DefaultWebSocketPayloadFormatter.cs | 31 ++++++++ .../IWebSocketPayloadFormatter.cs | 42 +++++++++++ .../WebSocketPayloadFormatterOptions.cs | 14 ++++ .../ApolloSubscriptionProtocolHandler.cs | 10 +-- .../GraphQLOverWebSocketProtocolHandler.cs | 10 +-- .../Apollo/WebSocketProtocolTests.cs | 58 +++++++++++++++ .../WebSocketProtocolTests.cs | 65 +++++++++++++++++ 8 files changed, 288 insertions(+), 12 deletions(-) create mode 100644 src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultWebSocketPayloadFormatter.cs create mode 100644 src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IWebSocketPayloadFormatter.cs create mode 100644 src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/WebSocketPayloadFormatterOptions.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Subscriptions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Subscriptions.cs index d075ca9cda5..e2d022b8238 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Subscriptions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Subscriptions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using HotChocolate.AspNetCore; +using HotChocolate.AspNetCore.Serialization; using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.AspNetCore.Subscriptions.Protocols.Apollo; using HotChocolate.AspNetCore.Subscriptions.Protocols.GraphQLOverWebSocket; @@ -56,8 +57,13 @@ public static IRequestExecutorBuilder AddSocketSessionInterceptor( private static IRequestExecutorBuilder AddSubscriptionServices( this IRequestExecutorBuilder builder) => builder - .ConfigureSchemaServices(s => s - .TryAddSingleton()) + .ConfigureSchemaServices(s => + { + s.TryAddSingleton(); + s.TryAddSingleton( + _ => new DefaultWebSocketPayloadFormatter( + new WebSocketPayloadFormatterOptions())); + }) .AddApolloProtocol() .AddGraphQLOverWebSocketProtocol(); @@ -66,12 +72,68 @@ private static IRequestExecutorBuilder AddApolloProtocol( => builder.ConfigureSchemaServices( s => s.AddSingleton( sp => new ApolloSubscriptionProtocolHandler( - sp.GetRequiredService()))); + sp.GetRequiredService(), + sp.GetRequiredService()))); private static IRequestExecutorBuilder AddGraphQLOverWebSocketProtocol( this IRequestExecutorBuilder builder) => builder.ConfigureSchemaServices( s => s.AddSingleton( sp => new GraphQLOverWebSocketProtocolHandler( - sp.GetRequiredService()))); + sp.GetRequiredService(), + sp.GetRequiredService()))); + + /// + /// Adds a custom WebSocket payload formatter to the DI. + /// + /// + /// The . + /// + /// + /// The type of the custom . + /// + /// + /// Returns the so that configuration can be chained. + /// + public static IRequestExecutorBuilder AddWebSocketPayloadFormatter( + this IRequestExecutorBuilder builder) + where T : class, IWebSocketPayloadFormatter + { + builder.ConfigureSchemaServices(services => + { + services.RemoveAll(); + services.AddSingleton(); + }); + + return builder; + } + + /// + /// Adds a custom WebSocket payload formatter to the DI. + /// + /// + /// The . + /// + /// + /// The service factory. + /// + /// + /// The type of the custom . + /// + /// + /// Returns the so that configuration can be chained. + /// + public static IRequestExecutorBuilder AddWebSocketPayloadFormatter( + this IRequestExecutorBuilder builder, + Func factory) + where T : class, IWebSocketPayloadFormatter + { + builder.ConfigureSchemaServices(services => + { + services.RemoveAll(); + services.AddSingleton(factory); + }); + + return builder; + } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultWebSocketPayloadFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultWebSocketPayloadFormatter.cs new file mode 100644 index 00000000000..924ec038168 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultWebSocketPayloadFormatter.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using HotChocolate.Execution.Serialization; + +namespace HotChocolate.AspNetCore.Serialization; + +/// +/// This represents the default implementation for the . +/// +public class DefaultWebSocketPayloadFormatter(WebSocketPayloadFormatterOptions options) + : IWebSocketPayloadFormatter +{ + private readonly JsonResultFormatter _jsonFormatter = new(options.Json); + + /// + public void Format(IOperationResult result, Utf8JsonWriter jsonWriter) + { + _jsonFormatter.Format(result, jsonWriter); + } + + /// + public void Format(IError error, Utf8JsonWriter jsonWriter) + { + _jsonFormatter.FormatError(error, jsonWriter); + } + + /// + public void Format(IReadOnlyList errors, Utf8JsonWriter jsonWriter) + { + _jsonFormatter.FormatErrors(errors, jsonWriter); + } +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IWebSocketPayloadFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IWebSocketPayloadFormatter.cs new file mode 100644 index 00000000000..ebc5dbf38da --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IWebSocketPayloadFormatter.cs @@ -0,0 +1,42 @@ +using System.Text.Json; + +namespace HotChocolate.AspNetCore.Serialization; + +/// +/// This interface specifies how a GraphQL result is formatted as a WebSocket payload. +/// +public interface IWebSocketPayloadFormatter +{ + /// + /// Formats the given into a WebSocket payload. + /// + /// + /// The GraphQL operation result. + /// + /// + /// The JSON writer that is used to write the payload. + /// + void Format(IOperationResult result, Utf8JsonWriter jsonWriter); + + /// + /// Formats the given into a WebSocket payload. + /// + /// + /// The GraphQL execution error. + /// + /// + /// The JSON writer that is used to write the error. + /// + void Format(IError error, Utf8JsonWriter jsonWriter); + + /// + /// Formats the given into a WebSocket payload. + /// + /// + /// The GraphQL execution errors. + /// + /// + /// The JSON writer that is used to write the errors. + /// + void Format(IReadOnlyList errors, Utf8JsonWriter jsonWriter); +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/WebSocketPayloadFormatterOptions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/WebSocketPayloadFormatterOptions.cs new file mode 100644 index 00000000000..7ac8b3978e8 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/WebSocketPayloadFormatterOptions.cs @@ -0,0 +1,14 @@ +using HotChocolate.Execution.Serialization; + +namespace HotChocolate.AspNetCore.Serialization; + +/// +/// Represents the GraphQL over WebSocket payload formatter options. +/// +public struct WebSocketPayloadFormatterOptions +{ + /// + /// Gets or sets the JSON result formatter options. + /// + public JsonResultFormatterOptions Json { get; set; } +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs index 07f68a04f2a..d94e5dcad5d 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs @@ -3,7 +3,6 @@ using System.Text.Json; using HotChocolate.AspNetCore.Serialization; using HotChocolate.Language; -using HotChocolate.Execution.Serialization; using HotChocolate.Utilities; using static HotChocolate.AspNetCore.Properties.AspNetCoreResources; using static HotChocolate.AspNetCore.Subscriptions.ConnectionContextKeys; @@ -16,12 +15,15 @@ namespace HotChocolate.AspNetCore.Subscriptions.Protocols.Apollo; internal sealed class ApolloSubscriptionProtocolHandler : IProtocolHandler { - private readonly JsonResultFormatter _formatter = new(); private readonly ISocketSessionInterceptor _interceptor; + private readonly IWebSocketPayloadFormatter _formatter; - public ApolloSubscriptionProtocolHandler(ISocketSessionInterceptor interceptor) + public ApolloSubscriptionProtocolHandler( + ISocketSessionInterceptor interceptor, + IWebSocketPayloadFormatter formatter) { _interceptor = interceptor; + _formatter = formatter; } public string Name => GraphQL_WS; @@ -263,7 +265,7 @@ public async ValueTask SendErrorMessageAsync( jsonWriter.WriteString(Id, operationSessionId); jsonWriter.WriteString(MessageProperties.Type, Utf8Messages.Error); jsonWriter.WritePropertyName(Payload); - _formatter.FormatError(errors[0], jsonWriter); + _formatter.Format(errors[0], jsonWriter); jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(cancellationToken); await session.Connection.SendAsync(arrayWriter.GetWrittenMemory(), cancellationToken); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs index 57e1754a76f..c64c53f1f0c 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs @@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using HotChocolate.AspNetCore.Serialization; -using HotChocolate.Execution.Serialization; using HotChocolate.Language; using HotChocolate.Utilities; using static HotChocolate.Transport.Sockets.WellKnownProtocols; @@ -15,12 +14,15 @@ namespace HotChocolate.AspNetCore.Subscriptions.Protocols.GraphQLOverWebSocket; internal sealed class GraphQLOverWebSocketProtocolHandler : IGraphQLOverWebSocketProtocolHandler { - private readonly JsonResultFormatter _formatter = new(); private readonly ISocketSessionInterceptor _interceptor; + private readonly IWebSocketPayloadFormatter _formatter; - public GraphQLOverWebSocketProtocolHandler(ISocketSessionInterceptor interceptor) + public GraphQLOverWebSocketProtocolHandler( + ISocketSessionInterceptor interceptor, + IWebSocketPayloadFormatter formatter) { _interceptor = interceptor; + _formatter = formatter; } public string Name => GraphQL_Transport_WS; @@ -239,7 +241,7 @@ public async ValueTask SendErrorMessageAsync( jsonWriter.WriteString(Id, operationSessionId); jsonWriter.WriteString(MessageProperties.Type, Utf8Messages.Error); jsonWriter.WritePropertyName(Payload); - _formatter.FormatErrors(errors, jsonWriter); + _formatter.Format(errors, jsonWriter); jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(cancellationToken); await session.Connection.SendAsync(arrayWriter.GetWrittenMemory(), cancellationToken); diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs index 1e092bf3d61..612b7c3c396 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs @@ -1,8 +1,10 @@ using System.Net.WebSockets; +using HotChocolate.AspNetCore.Serialization; using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.AspNetCore.Subscriptions.Protocols.Apollo; using HotChocolate.AspNetCore.Tests.Utilities; using HotChocolate.AspNetCore.Tests.Utilities.Subscriptions.Apollo; +using HotChocolate.Execution.Serialization; using HotChocolate.Language; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; @@ -518,6 +520,62 @@ public Task Send_Invalid_Message_Not_An_Object() Assert.Equal(CloseReasons.InvalidMessage, (int)webSocket.CloseStatus!.Value); }); + [Fact] + public Task Send_Start_ReceiveDataOnMutation_StripNull() => + TryTest( + async ct => + { + // arrange + using var testServer = CreateStarWarsServer( + configureServices: c => + c.AddGraphQL() + .AddWebSocketPayloadFormatter( + _ => new DefaultWebSocketPayloadFormatter( + new WebSocketPayloadFormatterOptions + { + Json = new JsonResultFormatterOptions() + { + NullIgnoreCondition = JsonNullIgnoreCondition.All + } + }))); + var client = CreateWebSocketClient(testServer); + var webSocket = await ConnectToServerAsync(client, ct); + + var document = Utf8GraphQLParser.Parse( + "subscription { onReview(episode: NEW_HOPE) { stars, commentary } }"); + var request = new GraphQLRequest(document); + const string subscriptionId = "abc"; + + // act + await webSocket.SendSubscriptionStartAsync(subscriptionId, request); + + // assert + await testServer.SendPostRequestAsync( + new ClientQueryRequest + { + Query = + """ + mutation { + createReview(episode: NEW_HOPE review: { + stars: 5 + commentary: null + }) { + stars + commentary + } + } + """ + }); + + var message = await WaitForMessage(webSocket, "data", ct); + Assert.NotNull(message); + var messagePayload = (Dictionary?)message["payload"]; + var messageData = (Dictionary?)messagePayload?["data"]; + var messageOnReview = (Dictionary?)messageData?["onReview"]; + Assert.NotNull(messageOnReview); + Assert.DoesNotContain("commentary", messageOnReview); + }); + private class AuthInterceptor : DefaultSocketSessionInterceptor { public override ValueTask OnConnectAsync( diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/GraphQLOverWebSocket/WebSocketProtocolTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/GraphQLOverWebSocket/WebSocketProtocolTests.cs index 5056fe0c09d..080c7d5e1d8 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/GraphQLOverWebSocket/WebSocketProtocolTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/GraphQLOverWebSocket/WebSocketProtocolTests.cs @@ -1,8 +1,10 @@ using System.Diagnostics; +using HotChocolate.AspNetCore.Serialization; using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.AspNetCore.Subscriptions.Protocols.GraphQLOverWebSocket; using HotChocolate.AspNetCore.Tests.Utilities; using HotChocolate.AspNetCore.Tests.Utilities.Subscriptions.GraphQLOverWebSocket; +using HotChocolate.Execution.Serialization; using HotChocolate.Subscriptions.Diagnostics; using HotChocolate.Transport; using HotChocolate.Transport.Sockets.Client; @@ -893,6 +895,69 @@ await socketResult.ReadResultsAsync() }); } + [Fact] + public Task Subscribe_ReceiveDataOnMutation_StripNull() + => TryTest( + async ct => + { + // arrange + var diagnostics = new SubscriptionTestDiagnostics(); + using var testServer = CreateStarWarsServer( + configureServices: c => + c.AddGraphQL() + .AddDiagnosticEventListener(_ => diagnostics) + .AddWebSocketPayloadFormatter( + _ => new DefaultWebSocketPayloadFormatter( + new WebSocketPayloadFormatterOptions + { + Json = new JsonResultFormatterOptions() + { + NullIgnoreCondition = JsonNullIgnoreCondition.All + } + })), + output: _output); + var client = CreateWebSocketClient(testServer); + using var webSocket = await ConnectToServerAsync(client, ct); + + var payload = new SubscribePayload( + "subscription { onReview(episode: NEW_HOPE) { stars, commentary } }"); + const string subscriptionId = "abc"; + + // act + await webSocket.SendSubscribeAsync(subscriptionId, payload, ct); + + while (diagnostics.Subscribed is not 1) + { + await Task.Delay(10, ct); + } + + await testServer.SendPostRequestAsync( + new ClientQueryRequest + { + Query = + """ + mutation { + createReview(episode: NEW_HOPE review: { + stars: 5 + commentary: null + }) { + stars + commentary + } + } + """, + }); + + // assert + var message = await WaitForMessage(webSocket, Messages.Next, ct); + Assert.NotNull(message); + var messagePayload = (Dictionary?)message["payload"]; + var messageData = (Dictionary?)messagePayload?["data"]; + var messageOnReview = (Dictionary?)messageData?["onReview"]; + Assert.NotNull(messageOnReview); + Assert.DoesNotContain("commentary", messageOnReview); + }); + private class AuthInterceptor : DefaultSocketSessionInterceptor { public override ValueTask OnConnectAsync( From 16095a70a8e10f7dee48931c3986d302c120e829 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 17 Mar 2025 17:16:06 +0100 Subject: [PATCH 50/64] Better error for invalid root type. (#8104) --- .../Core/src/Types/SchemaBuilder.Setup.cs | 124 ++++++++++++------ 1 file changed, 83 insertions(+), 41 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs b/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs index a90aa467df2..5e9865f6b66 100644 --- a/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs +++ b/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs @@ -1,5 +1,6 @@ #nullable enable +using System.Diagnostics.CodeAnalysis; using HotChocolate.Configuration; using HotChocolate.Configuration.Validation; using HotChocolate.Language; @@ -37,25 +38,25 @@ public static Schema Create( { var typeInterceptors = new List(); - if (context.Options.StrictRuntimeTypeValidation && - !builder._typeInterceptors.Contains(typeof(TypeValidationTypeInterceptor))) + if (context.Options.StrictRuntimeTypeValidation + && !builder._typeInterceptors.Contains(typeof(TypeValidationTypeInterceptor))) { builder._typeInterceptors.Add(typeof(TypeValidationTypeInterceptor)); } - if (context.Options.EnableFlagEnums && - !builder._typeInterceptors.Contains(typeof(FlagsEnumInterceptor))) + if (context.Options.EnableFlagEnums + && !builder._typeInterceptors.Contains(typeof(FlagsEnumInterceptor))) { builder._typeInterceptors.Add(typeof(FlagsEnumInterceptor)); } - if (context.Options.RemoveUnusedTypeSystemDirectives && - !builder._typeInterceptors.Contains(typeof(DirectiveTypeInterceptor))) + if (context.Options.RemoveUnusedTypeSystemDirectives + && !builder._typeInterceptors.Contains(typeof(DirectiveTypeInterceptor))) { builder._typeInterceptors.Add(typeof(DirectiveTypeInterceptor)); } - if(builder._schemaFirstTypeInterceptor is not null) + if (builder._schemaFirstTypeInterceptor is not null) { typeInterceptors.Add(builder._schemaFirstTypeInterceptor); } @@ -259,8 +260,7 @@ private static void InitializeInterceptors( List interceptors) where T : class { - if (services is not EmptyServiceProvider && - services.GetService>() is { } fromService) + if (services is not EmptyServiceProvider && services.GetService>() is { } fromService) { interceptors.AddRange(fromService); } @@ -293,28 +293,28 @@ private static RootTypeKind GetOperationKind( if (type is ObjectType objectType) { if (IsOperationType( - objectType, - OperationType.Query, - typeInspector, - operations)) + objectType, + OperationType.Query, + typeInspector, + operations)) { return RootTypeKind.Query; } if (IsOperationType( - objectType, - OperationType.Mutation, - typeInspector, - operations)) + objectType, + OperationType.Mutation, + typeInspector, + operations)) { return RootTypeKind.Mutation; } if (IsOperationType( - objectType, - OperationType.Subscription, - typeInspector, - operations)) + objectType, + OperationType.Subscription, + typeInspector, + operations)) { return RootTypeKind.Subscription; } @@ -338,8 +338,8 @@ private static bool IsOperationType( if (typeRef is ExtendedTypeReference cr) { - return cr.Type.Equals(typeInspector.GetType(objectType.GetType())) || - cr.Type.Equals(typeInspector.GetType(objectType.RuntimeType)); + return cr.Type.Equals(typeInspector.GetType(objectType.GetType())) + || cr.Type.Equals(typeInspector.GetType(objectType.RuntimeType)); } if (typeRef is SyntaxTypeReference str) @@ -437,9 +437,9 @@ private static void ResolveOperations( { if (operations.Count == 0) { - schemaDef.QueryType = GetObjectType(OperationTypeNames.Query); - schemaDef.MutationType = GetObjectType(OperationTypeNames.Mutation); - schemaDef.SubscriptionType = GetObjectType(OperationTypeNames.Subscription); + schemaDef.QueryType = GetObjectType(OperationTypeNames.Query, OperationType.Query); + schemaDef.MutationType = GetObjectType(OperationTypeNames.Mutation, OperationType.Mutation); + schemaDef.SubscriptionType = GetObjectType(OperationTypeNames.Subscription, OperationType.Subscription); } else { @@ -447,16 +447,19 @@ private static void ResolveOperations( schemaDef.MutationType = GetOperationType(OperationType.Mutation); schemaDef.SubscriptionType = GetOperationType(OperationType.Subscription); } - return; - ObjectType? GetObjectType(string typeName) + ObjectType? GetObjectType(string typeName, OperationType expectedOperation) { foreach (var registeredType in typeRegistry.Types) { - if (registeredType.Type is ObjectType objectType && - objectType.Name.EqualsOrdinal(typeName)) + if (registeredType.Type.Name.EqualsOrdinal(typeName)) { + if (registeredType.Type is not ObjectType objectType) + { + Throw((INamedType)registeredType.Type, expectedOperation); + } + return objectType; } } @@ -466,30 +469,69 @@ private static void ResolveOperations( ObjectType? GetOperationType(OperationType operation) { - if (operations.TryGetValue(operation, out var reference)) + if (!operations.TryGetValue(operation, out var reference)) { - if (reference is SchemaTypeReference sr) + return null; + } + + switch (reference) + { + case SchemaTypeReference str: { - return (ObjectType)sr.Type; + if (str.Type is not ObjectType ot) + { + Throw((INamedType)str.Type, operation); + } + + return ot; } - if (reference is ExtendedTypeReference cr && - typeRegistry.TryGetType(cr, out var registeredType)) + case ExtendedTypeReference cr when typeRegistry.TryGetType(cr, out var registeredType): { - return (ObjectType)registeredType.Type; + if (registeredType.Type is not ObjectType ot) + { + Throw((INamedType)registeredType.Type, operation); + } + + return ot; } - if (reference is SyntaxTypeReference str) + case SyntaxTypeReference str: { var namedType = str.Type.NamedType(); - return typeRegistry.Types + var type = typeRegistry.Types .Select(t => t.Type) - .OfType() .FirstOrDefault(t => t.Name.EqualsOrdinal(namedType.Name.Value)); + + if (type is null) + { + return null; + } + + if (type is not ObjectType ot) + { + Throw((INamedType)type, operation); + } + + return ot; } + + default: + return null; } + } - return null; + [DoesNotReturn] + static void Throw(INamedType namedType, OperationType operation) + { + throw SchemaErrorBuilder.New() + .SetMessage( + "Cannot register `{0}` as {1} type as it is not an object type. `{0}` is of type `{2}`.", + namedType.Name, + operation, + namedType.GetType().FullName) + .SetTypeSystemObject((TypeSystemObjectBase)namedType) + .BuildException(); } } @@ -529,7 +571,7 @@ internal static void AddCoreSchemaServices(IServiceCollection services, LazySche .ToArray(); } - if(allSerializers is null || allSerializers.Length == 0) + if (allSerializers is null || allSerializers.Length == 0) { allSerializers = [ From 82886c50fd6a4f81202597f4eea3af90ca69ee3b Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 17 Mar 2025 17:55:59 +0100 Subject: [PATCH 51/64] Fixed issue where projections fallback was not possible. (#8139) --- .../SelectionExpressionBuilder.cs | 42 +++++ .../src/Execution/RequestExecutorResolver.cs | 39 +++-- .../Data/CatalogContext.cs | 2 + .../Data.PostgreSQL.Tests/IntegrationTests.cs | 42 +++++ .../20240317214259_Initial.Designer.cs | 148 ------------------ .../Migrations/20240317214259_Initial.cs | 103 ------------ .../Migrations/CatalogContextModelSnapshot.cs | 145 ----------------- .../Migrations/CatalogContextSeed.cs | 22 +-- .../Models/SingleProperty.cs | 6 + .../SingleProperties/SinglePropertyQueries.cs | 21 +++ .../SingleProperties/SinglePropertyType.cs | 13 ++ .../IntegrationTests.CreateSchema.graphql | 5 + ...Ensure_That_Self_Requirement_Is_Honored.md | 26 +++ ...at_Self_Requirement_Is_Honored__net_8_0.md | 26 +++ ...me_Properties_When_No_Field_Is_Bindable.md | 26 +++ ...ties_When_No_Field_Is_Bindable__net_8_0.md | 26 +++ 16 files changed, 269 insertions(+), 423 deletions(-) delete mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/20240317214259_Initial.Designer.cs delete mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/20240317214259_Initial.cs delete mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/CatalogContextModelSnapshot.cs create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Models/SingleProperty.cs create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/SingleProperties/SinglePropertyQueries.cs create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/SingleProperties/SinglePropertyType.cs create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Ensure_That_Self_Requirement_Is_Honored.md create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Ensure_That_Self_Requirement_Is_Honored__net_8_0.md create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Fallback_To_Runtime_Properties_When_No_Field_Is_Bindable.md create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Fallback_To_Runtime_Properties_When_No_Field_Is_Bindable__net_8_0.md diff --git a/src/HotChocolate/Core/src/Execution.Projections/SelectionExpressionBuilder.cs b/src/HotChocolate/Core/src/Execution.Projections/SelectionExpressionBuilder.cs index 9096bca7532..54684a6ec52 100644 --- a/src/HotChocolate/Core/src/Execution.Projections/SelectionExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Execution.Projections/SelectionExpressionBuilder.cs @@ -11,6 +11,32 @@ namespace HotChocolate.Execution.Projections; internal sealed class SelectionExpressionBuilder { + private static readonly NullabilityInfoContext _nullabilityInfoContext = new(); + private static readonly HashSet _runtimeLeafTypes = + [ + typeof(string), + typeof(byte), + typeof(short), + typeof(int), + typeof(long), + typeof(float), + typeof(byte), + typeof(decimal), + typeof(Guid), + typeof(bool), + typeof(char), + typeof(byte?), + typeof(short?), + typeof(int?), + typeof(long?), + typeof(float?), + typeof(byte?), + typeof(decimal?), + typeof(Guid?), + typeof(bool?), + typeof(char?) + ]; + public Expression> BuildExpression(ISelection selection) { var rootType = typeof(TRoot); @@ -214,11 +240,27 @@ private static void TryAddAnyLeafField( } else { + // if id does not exist we will try to select any leaf field from the type. var anyProperty = selectionType.Fields.FirstOrDefault(t => t.Type.IsLeafType() && t.Member is PropertyInfo); + if (anyProperty?.Member is PropertyInfo anyPropertyInfo) { parent.AddOrGetNode(anyPropertyInfo); } + else + { + // if we still have not found any leaf we will inspect the runtime type and + // try to select any leaf property. + var properties = selectionType.RuntimeType.GetProperties(BindingFlags.Public | BindingFlags.Instance); + foreach (var property in properties) + { + if (_runtimeLeafTypes.Contains(property.PropertyType)) + { + parent.AddOrGetNode(property); + break; + } + } + } } } diff --git a/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs index 9fc0dfdc1a7..b288c056b71 100644 --- a/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs +++ b/src/HotChocolate/Core/src/Execution/RequestExecutorResolver.cs @@ -107,30 +107,37 @@ private async ValueTask ConsumeExecutorEvictionsAsync( ChannelReader reader, CancellationToken cancellationToken) { - while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) + try { - while (reader.TryRead(out var schemaName)) + while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) { - var semaphore = GetSemaphoreForSchema(schemaName); - await semaphore.WaitAsync(cancellationToken); - - try + while (reader.TryRead(out var schemaName)) { - if (_executors.TryGetValue(schemaName, out var previousExecutor)) + var semaphore = GetSemaphoreForSchema(schemaName); + await semaphore.WaitAsync(cancellationToken); + + try { - await UpdateRequestExecutorAsync(schemaName, previousExecutor); + if (_executors.TryGetValue(schemaName, out var previousExecutor)) + { + await UpdateRequestExecutorAsync(schemaName, previousExecutor); + } + } + catch + { + // Ignore + } + finally + { + semaphore.Release(); } - } - catch - { - // Ignore - } - finally - { - semaphore.Release(); } } } + catch (OperationCanceledException) + { + // ignore + } } private SemaphoreSlim GetSemaphoreForSchema(string schemaName) diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Data/CatalogContext.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Data/CatalogContext.cs index b57da084ebb..7b371b3d407 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Data/CatalogContext.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Data/CatalogContext.cs @@ -12,6 +12,8 @@ public class CatalogContext(DbContextOptions options) : DbContex public DbSet Brands => Set(); + public DbSet SingleProperties => Set(); + protected override void OnModelCreating(ModelBuilder builder) { builder.ApplyConfiguration(new BrandEntityTypeConfiguration()); diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs index 4994afb7f5d..570aff0e04f 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs @@ -360,6 +360,48 @@ public async Task Query_Products_Exclude_TotalCount() MatchSnapshot(result, interceptor); } + [Fact] + public async Task Ensure_That_Self_Requirement_Is_Honored() + { + // arrange + using var interceptor = new TestQueryInterceptor(); + + // act + var result = await ExecuteAsync( + """ + { + singleProperties { + id + } + } + + """); + + // assert + MatchSnapshot(result, interceptor); + } + + [Fact] + public async Task Fallback_To_Runtime_Properties_When_No_Field_Is_Bindable() + { + // arrange + using var interceptor = new TestQueryInterceptor(); + + // act + var result = await ExecuteAsync( + """ + { + singleProperties { + __typename + } + } + + """); + + // assert + MatchSnapshot(result, interceptor); + } + private static ServiceProvider CreateServer(string connectionString) { var services = new ServiceCollection(); diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/20240317214259_Initial.Designer.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/20240317214259_Initial.Designer.cs deleted file mode 100644 index e71c8ddca6f..00000000000 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/20240317214259_Initial.Designer.cs +++ /dev/null @@ -1,148 +0,0 @@ -// - -using HotChocolate.Data.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace eShop.Catalog.Migrations -{ - [DbContext(typeof(CatalogContext))] - [Migration("20240317214259_Initial")] - partial class Initial - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.3") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("eShop.Catalog.Models.Brand", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.HasKey("Id"); - - b.ToTable("Brands", (string)null); - }); - - modelBuilder.Entity("eShop.Catalog.Models.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AvailableStock") - .HasColumnType("integer"); - - b.Property("BrandId") - .HasColumnType("integer"); - - b.Property("Description") - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("ImageFileName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("MaxStockThreshold") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("OnReorder") - .HasColumnType("boolean"); - - b.Property("Price") - .HasColumnType("numeric"); - - b.Property("RestockThreshold") - .HasColumnType("integer"); - - b.Property("TypeId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("BrandId"); - - b.HasIndex("Name"); - - b.HasIndex("TypeId"); - - b.ToTable("Products", (string)null); - }); - - modelBuilder.Entity("eShop.Catalog.Models.ProductType", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.HasKey("Id"); - - b.ToTable("ProductTypes", (string)null); - }); - - modelBuilder.Entity("eShop.Catalog.Models.Product", b => - { - b.HasOne("eShop.Catalog.Models.Brand", "Brand") - .WithMany("Products") - .HasForeignKey("BrandId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("eShop.Catalog.Models.ProductType", "Type") - .WithMany("Products") - .HasForeignKey("TypeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Brand"); - - b.Navigation("Type"); - }); - - modelBuilder.Entity("eShop.Catalog.Models.Brand", b => - { - b.Navigation("Products"); - }); - - modelBuilder.Entity("eShop.Catalog.Models.ProductType", b => - { - b.Navigation("Products"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/20240317214259_Initial.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/20240317214259_Initial.cs deleted file mode 100644 index 61053423d3d..00000000000 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/20240317214259_Initial.cs +++ /dev/null @@ -1,103 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace eShop.Catalog.Migrations -{ - /// - public partial class Initial : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Brands", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Brands", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ProductTypes", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ProductTypes", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Products", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - Description = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), - Price = table.Column(type: "numeric", nullable: false), - ImageFileName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - TypeId = table.Column(type: "integer", nullable: false), - BrandId = table.Column(type: "integer", nullable: false), - AvailableStock = table.Column(type: "integer", nullable: false), - RestockThreshold = table.Column(type: "integer", nullable: false), - MaxStockThreshold = table.Column(type: "integer", nullable: false), - OnReorder = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Products", x => x.Id); - table.ForeignKey( - name: "FK_Products_Brands_BrandId", - column: x => x.BrandId, - principalTable: "Brands", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Products_ProductTypes_TypeId", - column: x => x.TypeId, - principalTable: "ProductTypes", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Products_BrandId", - table: "Products", - column: "BrandId"); - - migrationBuilder.CreateIndex( - name: "IX_Products_Name", - table: "Products", - column: "Name"); - - migrationBuilder.CreateIndex( - name: "IX_Products_TypeId", - table: "Products", - column: "TypeId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Products"); - - migrationBuilder.DropTable( - name: "Brands"); - - migrationBuilder.DropTable( - name: "ProductTypes"); - } - } -} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/CatalogContextModelSnapshot.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/CatalogContextModelSnapshot.cs deleted file mode 100644 index 33bb1f00334..00000000000 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/CatalogContextModelSnapshot.cs +++ /dev/null @@ -1,145 +0,0 @@ -// - -using HotChocolate.Data.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace eShop.Catalog.Migrations -{ - [DbContext(typeof(CatalogContext))] - partial class CatalogContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.3") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("eShop.Catalog.Models.Brand", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.HasKey("Id"); - - b.ToTable("Brands", (string)null); - }); - - modelBuilder.Entity("eShop.Catalog.Models.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AvailableStock") - .HasColumnType("integer"); - - b.Property("BrandId") - .HasColumnType("integer"); - - b.Property("Description") - .HasMaxLength(2048) - .HasColumnType("character varying(2048)"); - - b.Property("ImageFileName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("MaxStockThreshold") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("OnReorder") - .HasColumnType("boolean"); - - b.Property("Price") - .HasColumnType("numeric"); - - b.Property("RestockThreshold") - .HasColumnType("integer"); - - b.Property("TypeId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("BrandId"); - - b.HasIndex("Name"); - - b.HasIndex("TypeId"); - - b.ToTable("Products", (string)null); - }); - - modelBuilder.Entity("eShop.Catalog.Models.ProductType", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.HasKey("Id"); - - b.ToTable("ProductTypes", (string)null); - }); - - modelBuilder.Entity("eShop.Catalog.Models.Product", b => - { - b.HasOne("eShop.Catalog.Models.Brand", "Brand") - .WithMany("Products") - .HasForeignKey("BrandId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("eShop.Catalog.Models.ProductType", "Type") - .WithMany("Products") - .HasForeignKey("TypeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Brand"); - - b.Navigation("Type"); - }); - - modelBuilder.Entity("eShop.Catalog.Models.Brand", b => - { - b.Navigation("Products"); - }); - - modelBuilder.Entity("eShop.Catalog.Models.ProductType", b => - { - b.Navigation("Products"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/CatalogContextSeed.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/CatalogContextSeed.cs index cb9b6fac731..57acda970e7 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/CatalogContextSeed.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Migrations/CatalogContextSeed.cs @@ -2,20 +2,14 @@ using HotChocolate.Data.Data; using HotChocolate.Data.Models; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Npgsql; namespace HotChocolate.Data.Migrations; -public sealed class CatalogContextSeed( - ILogger logger) - : IDbSeeder +public sealed class CatalogContextSeed : IDbSeeder { public async Task SeedAsync(CatalogContext context) { - // Workaround from https://github.com/npgsql/efcore.pg/issues/292#issuecomment-388608426 - await context.Database.OpenConnectionAsync(); - await ((NpgsqlConnection)context.Database.GetDbConnection()).ReloadTypesAsync(); + await context.Database.EnsureCreatedAsync(); if (!context.Products.Any()) { @@ -25,12 +19,10 @@ public async Task SeedAsync(CatalogContext context) context.Brands.RemoveRange(context.Brands); await context.Brands.AddRangeAsync( sourceItems.Select(x => x.Brand).Distinct().Select(brandName => new Brand { Name = brandName, })); - logger.LogInformation("Seeded catalog with {NumBrands} brands", context.Brands.Count()); context.ProductTypes.RemoveRange(context.ProductTypes); await context.ProductTypes.AddRangeAsync( sourceItems.Select(x => x.Type).Distinct().Select(typeName => new ProductType { Name = typeName, })); - logger.LogInformation("Seeded catalog with {NumTypes} types", context.ProductTypes.Count()); await context.SaveChangesAsync(); @@ -52,7 +44,15 @@ await context.Products.AddRangeAsync( ImageFileName = $"images/{source.Id}.webp", })); - logger.LogInformation("Seeded catalog with {NumItems} items", context.Products.Count()); + for(var i = 0; i < 100; i++) + { + await context.SingleProperties.AddAsync( + new SingleProperty + { + Id = i.ToString() + }); + } + await context.SaveChangesAsync(); } } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Models/SingleProperty.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Models/SingleProperty.cs new file mode 100644 index 00000000000..ae6791b7799 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Models/SingleProperty.cs @@ -0,0 +1,6 @@ +namespace HotChocolate.Data.Models; + +public class SingleProperty +{ + public required string Id { get; set; } +} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/SingleProperties/SinglePropertyQueries.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/SingleProperties/SinglePropertyQueries.cs new file mode 100644 index 00000000000..1b21118b6c1 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/SingleProperties/SinglePropertyQueries.cs @@ -0,0 +1,21 @@ +using GreenDonut.Data; +using HotChocolate.Data.Data; +using HotChocolate.Data.Models; +using HotChocolate.Types; +using Microsoft.EntityFrameworkCore; + +namespace HotChocolate.Data.Types.SingleProperties; + +[QueryType] +public static partial class SinglePropertyQueries +{ + public static async Task> GetSingleProperties( + CatalogContext context, + QueryContext query, + CancellationToken cancellationToken) + { + var queryable = context.SingleProperties.With(query); + PagingQueryInterceptor.Publish(queryable); + return await queryable.Take(2).ToListAsync(cancellationToken); + } +} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/SingleProperties/SinglePropertyType.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/SingleProperties/SinglePropertyType.cs new file mode 100644 index 00000000000..bce95048f4e --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/SingleProperties/SinglePropertyType.cs @@ -0,0 +1,13 @@ +using HotChocolate.Data.Models; +using HotChocolate.Types; + +namespace HotChocolate.Data.Types.SingleProperties; + +[ObjectType] +public static partial class SinglePropertyType +{ + public static string GetId( + [Parent(requires: nameof(singleProperty.Id))] + SingleProperty singleProperty) + => singleProperty.Id; +} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql index 34e9a949caa..1285943b65e 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql @@ -140,6 +140,11 @@ type Query { brandById(id: ID!): Brand @cost(weight: "10") products("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ProductFilterInput @cost(weight: "10") order: [ProductSortInput!] @cost(weight: "10")): ProductConnection! @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") productsNonRelative("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ProductFilterInput @cost(weight: "10") order: [ProductSortInput!] @cost(weight: "10")): ProductConnection! @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + singleProperties: [SingleProperty!]! @cost(weight: "10") +} + +type SingleProperty { + id: String! } input BooleanOperationFilterInput { diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Ensure_That_Self_Requirement_Is_Honored.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Ensure_That_Self_Requirement_Is_Honored.md new file mode 100644 index 00000000000..1021cf04199 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Ensure_That_Self_Requirement_Is_Honored.md @@ -0,0 +1,26 @@ +# Ensure_That_Self_Requirement_Is_Honored + +## Result + +```json +{ + "data": { + "singleProperties": [ + { + "id": "0" + }, + { + "id": "1" + } + ] + } +} +``` + +## Query 1 + +```sql +SELECT s."Id" +FROM "SingleProperties" AS s +``` + diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Ensure_That_Self_Requirement_Is_Honored__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Ensure_That_Self_Requirement_Is_Honored__net_8_0.md new file mode 100644 index 00000000000..1021cf04199 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Ensure_That_Self_Requirement_Is_Honored__net_8_0.md @@ -0,0 +1,26 @@ +# Ensure_That_Self_Requirement_Is_Honored + +## Result + +```json +{ + "data": { + "singleProperties": [ + { + "id": "0" + }, + { + "id": "1" + } + ] + } +} +``` + +## Query 1 + +```sql +SELECT s."Id" +FROM "SingleProperties" AS s +``` + diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Fallback_To_Runtime_Properties_When_No_Field_Is_Bindable.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Fallback_To_Runtime_Properties_When_No_Field_Is_Bindable.md new file mode 100644 index 00000000000..30b6a3e0fdf --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Fallback_To_Runtime_Properties_When_No_Field_Is_Bindable.md @@ -0,0 +1,26 @@ +# Fallback_To_Runtime_Properties_When_No_Field_Is_Bindable + +## Result + +```json +{ + "data": { + "singleProperties": [ + { + "__typename": "SingleProperty" + }, + { + "__typename": "SingleProperty" + } + ] + } +} +``` + +## Query 1 + +```sql +SELECT s."Id" +FROM "SingleProperties" AS s +``` + diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Fallback_To_Runtime_Properties_When_No_Field_Is_Bindable__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Fallback_To_Runtime_Properties_When_No_Field_Is_Bindable__net_8_0.md new file mode 100644 index 00000000000..30b6a3e0fdf --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Fallback_To_Runtime_Properties_When_No_Field_Is_Bindable__net_8_0.md @@ -0,0 +1,26 @@ +# Fallback_To_Runtime_Properties_When_No_Field_Is_Bindable + +## Result + +```json +{ + "data": { + "singleProperties": [ + { + "__typename": "SingleProperty" + }, + { + "__typename": "SingleProperty" + } + ] + } +} +``` + +## Query 1 + +```sql +SELECT s."Id" +FROM "SingleProperties" AS s +``` + From 24f2e2ed2997a6c5108a3413371e542cd82c4314 Mon Sep 17 00:00:00 2001 From: Tobias Tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 17 Mar 2025 23:06:02 +0100 Subject: [PATCH 52/64] [Fusion] Fail Query Plan generation early (#7989) --- .../ExecutionStepDiscoveryMiddleware.cs | 4 +- .../Planning/Steps/SelectionExecutionStep.cs | 12 +-- .../src/Core/Planning/Steps/SelectionPath.cs | 4 +- .../test/Core.Tests/RequestPlannerTests.cs | 74 +++++++++++++++++++ 4 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs index e3007e4d965..d11205d0871 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs @@ -797,9 +797,9 @@ private bool EnsureStepCanBeResolvedFromRoot( while (current is not null) { - var typeMetadata = _config.GetType(path.Selection.DeclaringType.Name); + var typeMetadata = _config.GetType(current.Selection.DeclaringType.Name); - if (!typeMetadata.Fields[path.Selection.Field.Name].Bindings.ContainsSubgraph(subgraphName)) + if (!typeMetadata.Fields[current.Selection.Field.Name].Bindings.ContainsSubgraph(subgraphName)) { return false; } diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionExecutionStep.cs b/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionExecutionStep.cs index 09ffb23951f..db38d24337a 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionExecutionStep.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionExecutionStep.cs @@ -22,7 +22,7 @@ internal sealed class SelectionExecutionStep : ExecutionStep /// /// The name of the subgraph from which this execution step will fetch data. /// - /// + /// /// The selection set that is part of this execution step. /// /// @@ -31,9 +31,9 @@ internal sealed class SelectionExecutionStep : ExecutionStep public SelectionExecutionStep( int id, string subgraphName, - IObjectType selectionSet, + IObjectType selectionSetType, ObjectTypeMetadata selectionSetTypeMetadata) - : this(id, subgraphName, null, null, selectionSet, selectionSetTypeMetadata) + : this(id, subgraphName, null, null, selectionSetType, selectionSetTypeMetadata) { } @@ -52,7 +52,7 @@ public SelectionExecutionStep( /// /// The selection path from which this execution step was spawned. /// - /// + /// /// The selection set that is part of this execution step. /// /// @@ -63,9 +63,9 @@ public SelectionExecutionStep( string subgraphName, ISelection? parentSelection, SelectionPath? parentSelectionPath, - IObjectType selectionSet, + IObjectType selectionSetType, ObjectTypeMetadata selectionSetTypeMetadata) - : base(id, parentSelection, selectionSet, selectionSetTypeMetadata) + : base(id, parentSelection, selectionSetType, selectionSetTypeMetadata) { SubgraphName = subgraphName; ParentSelectionPath = parentSelectionPath; diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionPath.cs b/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionPath.cs index 1b28b7b951b..31a4a1b26b8 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionPath.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionPath.cs @@ -35,8 +35,8 @@ public override string ToString() while (current is not null) { - sb.Append('/'); - sb.Append(Selection.ResponseName); + sb.Insert(0, '/'); + sb.Insert(1, current.Selection.ResponseName); current = current.Parent; } diff --git a/src/HotChocolate/Fusion/test/Core.Tests/RequestPlannerTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/RequestPlannerTests.cs index a0d1ed10604..9db0d179890 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/RequestPlannerTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/RequestPlannerTests.cs @@ -2650,6 +2650,80 @@ query Requires { await snapshot.MatchMarkdownAsync(); } + [Fact] + public async Task Throw_Instead_Of_Creating_Faulty_QueryPlan() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + node(id: ID!): Node + productById(id: ID!): Product + } + + interface Node { + id: ID! + } + + type Product implements Node { + id: ID! + review: Review + } + + type Review implements Node { + id: ID! + ratings: Ratings + } + + type Ratings { + upvotes: Int! + } + """); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + node(id: ID!): Node + } + + interface Node { + id: ID! + } + + type Review implements Node { + id: ID! + ratings: Ratings + } + + type Ratings { + downvotes: Int! + } + """); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // act + var act = () => CreateQueryPlanAsync( + fusionGraph, + """ + query { + productById(id: "1") { + review { + ratings { + upvotes + downvotes + } + } + } + } + """); + + // assert + var exception = await Assert.ThrowsAsync(act); + Assert.Equal(FusionResources.ThrowHelper_NoResolverInContext, exception.Message); + } + private static async Task<(DocumentNode UserRequest, Execution.Nodes.QueryPlan QueryPlan)> CreateQueryPlanAsync( Skimmed.SchemaDefinition fusionGraph, [StringSyntax("graphql")] string query) From 2a5244607886d755591fd35eaf39e991a2fd0d06 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 17 Mar 2025 23:16:51 +0100 Subject: [PATCH 53/64] Updated Nitro to Version 26.0.3 (#8142) --- src/HotChocolate/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HotChocolate/Directory.Build.props b/src/HotChocolate/Directory.Build.props index d94f89d96b1..0f648699797 100644 --- a/src/HotChocolate/Directory.Build.props +++ b/src/HotChocolate/Directory.Build.props @@ -4,6 +4,6 @@ hotchocolate-signet.png - 23.0.4 + 26.0.3 From 89fe3b80c47749af691a34ee36b2ee9b2effeed9 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 18 Mar 2025 16:31:40 +0100 Subject: [PATCH 54/64] [Fusion] Fix variables in context selection. (#8146) Co-authored-by: glen@chillicream.com --- .../ExecutionStepDiscoveryMiddleware.cs | 129 +++++++++++++----- .../Pipeline/RequirementsPlannerMiddleware.cs | 2 +- .../src/Core/Planning/QueryPlanContext.cs | 2 + .../src/Core/Planning/Steps/SelectionPath.cs | 39 +++++- .../test/Core.Tests/RequestPlannerTests.cs | 70 ++++++++++ ...sure_Subgraphs_In_Context_Are_Preferred.md | 65 +++++++++ 6 files changed, 269 insertions(+), 38 deletions(-) create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RequestPlannerTests.Ensure_Subgraphs_In_Context_Are_Preferred.md diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs index d11205d0871..3b249639ccd 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs @@ -6,6 +6,7 @@ using HotChocolate.Types; using HotChocolate.Types.Introspection; using HotChocolate.Utilities; +using Microsoft.AspNetCore.Components.Forms; using ThrowHelper = HotChocolate.Fusion.Utilities.ThrowHelper; namespace HotChocolate.Fusion.Planning.Pipeline; @@ -34,6 +35,7 @@ internal sealed class ExecutionStepDiscoveryMiddleware( { private readonly FusionGraphConfiguration _config = configuration ?? throw new ArgumentNullException(nameof(configuration)); + private readonly ISchema _schema = schema ?? throw new ArgumentNullException(nameof(schema)); @@ -92,6 +94,11 @@ private void CreateExecutionSteps( List? leftovers = null; var path = new List(); + var subgraphsInContext = + parentSelectionPath is null + ? new HashSet() + : context.SubgraphInContext[parentSelectionPath]; + // if this is the root selection set of a query we will // look for some special selections. if (!context.HasHandledSpecialQueryFields && parentSelection is null) @@ -101,7 +108,8 @@ private void CreateExecutionSteps( operation, ref selections, selectionSetTypeMetadata, - backlog); + backlog, + subgraphsInContext); context.HasHandledSpecialQueryFields = true; if (selections.Count == 0) @@ -135,7 +143,8 @@ private void CreateExecutionSteps( preferBatching, variablesInContext, subgraph, - executionStep); + executionStep, + subgraphsInContext); foreach (var selection in current) { @@ -154,6 +163,7 @@ private void CreateExecutionSteps( ResolverDefinition? resolver = null; GatherVariablesInContext( + subgraphsInContext, selection, selectionSetTypeMetadata, parentSelection, @@ -196,6 +206,7 @@ private void CreateExecutionSteps( if (selection.SelectionSet is not null) { CollectNestedSelections( + context, backlog, operation, selection, @@ -203,7 +214,8 @@ private void CreateExecutionSteps( path, executionStep, preferBatching, - context.ParentSelections); + context.ParentSelections, + subgraphsInContext); } path.RemoveAt(pathIndex); @@ -211,10 +223,10 @@ private void CreateExecutionSteps( // if the current execution step has now way to resolve the data // we will try to resolve it from the root. - if(executionStep.ParentSelection is not null && - executionStep.ParentSelectionPath is not null && - executionStep.Resolver is null && - executionStep.SelectionResolvers.Count == 0) + if (executionStep.ParentSelection is not null + && executionStep.ParentSelectionPath is not null + && executionStep.Resolver is null + && executionStep.SelectionResolvers.Count == 0) { if (!EnsureStepCanBeResolvedFromRoot( executionStep.SubgraphName, @@ -233,7 +245,8 @@ private void HandleSpecialQuerySelections( IOperation operation, ref IReadOnlyList selections, ObjectTypeMetadata selectionSetTypeMetadata, - Queue backlog) + Queue backlog, + HashSet subgraphsInContext) { if (operation.Type is OperationType.Query) { @@ -262,7 +275,8 @@ private void HandleSpecialQuerySelections( backlog, selection, selectionSetTypeMetadata, - context.ParentSelections); + context.ParentSelections, + subgraphsInContext); (processed ??= []).Add(i); } } @@ -282,6 +296,7 @@ private void HandleSpecialQuerySelections( } private void CollectNestedSelections( + QueryPlanContext context, Queue backlog, IOperation operation, ISelection parentSelection, @@ -289,7 +304,8 @@ private void CollectNestedSelections( List path, SelectionExecutionStep executionStep, bool preferBatching, - Dictionary parentSelectionLookup) + Dictionary parentSelectionLookup, + HashSet subgraphsInContext) { if (!preferBatching) { @@ -299,6 +315,7 @@ private void CollectNestedSelections( foreach (var possibleType in operation.GetPossibleTypes(parentSelection)) { CollectNestedSelections( + context, backlog, operation, parentSelection, @@ -307,11 +324,13 @@ private void CollectNestedSelections( executionStep, possibleType, preferBatching, - parentSelectionLookup); + parentSelectionLookup, + subgraphsInContext); } } private void CollectNestedSelections( + QueryPlanContext context, Queue backlog, IOperation operation, ISelection parentSelection, @@ -320,7 +339,8 @@ private void CollectNestedSelections( SelectionExecutionStep executionStep, IObjectType possibleType, bool preferBatching, - Dictionary parentSelectionLookup) + Dictionary parentSelectionLookup, + HashSet subgraphsInContext) { var declaringType = _config.GetType(possibleType.Name); var selectionSet = operation.GetSelectionSet(parentSelection, possibleType); @@ -345,6 +365,7 @@ private void CollectNestedSelections( var variablesInContext = new HashSet(); GatherVariablesInContext( + subgraphsInContext, selection, declaringType, parentSelection, @@ -379,6 +400,7 @@ private void CollectNestedSelections( if (selection.SelectionSet is not null) { CollectNestedSelections( + context, backlog, operation, selection, @@ -386,7 +408,8 @@ private void CollectNestedSelections( path, executionStep, preferBatching, - parentSelectionLookup); + parentSelectionLookup, + subgraphsInContext); } } else @@ -399,10 +422,23 @@ private void CollectNestedSelections( if (leftovers is not null) { + var selectionSetPath = CreateSelectionPath(rootSelectionPath, path); + + if (selectionSetPath is not null) + { + if (!context.SubgraphInContext.TryGetValue(selectionSetPath, out var subgraph)) + { + subgraph = []; + context.SubgraphInContext[selectionSetPath] = subgraph; + } + + subgraph.Add(executionStep.SubgraphName); + } + backlog.Enqueue( new BacklogItem( parentSelection, - CreateSelectionPath(rootSelectionPath, path), + selectionSetPath, declaringType, leftovers, preferBatching)); @@ -429,10 +465,10 @@ private static void AddIntrospectionStepIfNotExists( IObjectType queryType, ObjectTypeMetadata queryTypeMetadata) { - if (!context.HasIntrospectionSelections && - (field.Name.EqualsOrdinal(IntrospectionFields.Schema) || - field.Name.EqualsOrdinal(IntrospectionFields.Type) || - field.Name.EqualsOrdinal(IntrospectionFields.TypeName))) + if (!context.HasIntrospectionSelections + && (field.Name.EqualsOrdinal(IntrospectionFields.Schema) + || field.Name.EqualsOrdinal(IntrospectionFields.Type) + || field.Name.EqualsOrdinal(IntrospectionFields.TypeName))) { var step = new IntrospectionExecutionStep( context.NextStepId(), @@ -449,7 +485,8 @@ private void AddNodeExecutionStep( Queue backlog, ISelection nodeSelection, ObjectTypeMetadata queryTypeMetadata, - Dictionary parentSelectionLookup) + Dictionary parentSelectionLookup, + HashSet subgraphsInContext) { var operation = context.Operation; var queryType = operation.RootType; @@ -475,7 +512,8 @@ private void AddNodeExecutionStep( entityTypeInfo, selectionSet, preferBatching: false, - parentSelectionLookup: parentSelectionLookup); + parentSelectionLookup: parentSelectionLookup, + subgraphsInContext); var nodeEntityExecutionStep = new NodeEntityExecutionStep( @@ -504,7 +542,8 @@ private SelectionExecutionStep CreateNodeNestedExecutionSteps( ObjectTypeMetadata entityTypeMetadata, ISelectionSet entityTypeSelectionSet, bool preferBatching, - Dictionary parentSelectionLookup) + Dictionary parentSelectionLookup, + HashSet subgraphsInContext) { var variablesInContext = new HashSet(); var operation = context.Operation; @@ -532,12 +571,13 @@ private SelectionExecutionStep CreateNodeNestedExecutionSteps( queryTypeMetadata); var preference = ChoosePreferredResolverKind(operation, null, preferBatching); - GatherVariablesInContext(nodeSelection, queryTypeMetadata, null, variablesInContext); + GatherVariablesInContext(subgraphsInContext, nodeSelection, queryTypeMetadata, null, variablesInContext); if (!TryGetResolver(fieldInfo, subgraph, variablesInContext, preference, out var resolver)) { throw ThrowHelper.NoResolverInContext(); } + executionStep.AllSelections.Add(nodeSelection); executionStep.RootSelections.Add(new RootSelection(nodeSelection, resolver)); @@ -549,6 +589,7 @@ private SelectionExecutionStep CreateNodeNestedExecutionSteps( executionStep.Requires); CollectNestedSelections( + context, backlog, operation, nodeSelection, @@ -557,7 +598,8 @@ private SelectionExecutionStep CreateNodeNestedExecutionSteps( executionStep, entityType, preferBatching, - parentSelectionLookup); + parentSelectionLookup, + subgraphsInContext); context.Steps.Add(executionStep); @@ -570,14 +612,20 @@ private void TrySetEntityResolver( bool preferBatching, HashSet variablesInContext, string subgraph, - SelectionExecutionStep executionStep) + SelectionExecutionStep executionStep, + HashSet subgraphsInContext) + { if (parentSelection is null || !selectionSetTypeMetadata.Resolvers.ContainsResolvers(subgraph)) { return; } - GatherVariablesInContext(selectionSetTypeMetadata, parentSelection, variablesInContext); + GatherVariablesInContext( + subgraphsInContext, + selectionSetTypeMetadata, + parentSelection, + variablesInContext); if (TryGetResolver( selectionSetTypeMetadata, @@ -595,12 +643,11 @@ private static PreferredResolverKind ChoosePreferredResolverKind( IOperation operation, ISelection? parentSelection, bool preferBatching) - => operation.Type is OperationType.Subscription && - parentSelection is null - ? PreferredResolverKind.Subscription - : preferBatching - ? PreferredResolverKind.Batch - : PreferredResolverKind.Query; + => operation.Type is OperationType.Subscription && parentSelection is null + ? PreferredResolverKind.Subscription + : preferBatching + ? PreferredResolverKind.Batch + : PreferredResolverKind.Query; private static bool TryGetResolver( ObjectFieldInfo fieldInfo, @@ -693,6 +740,7 @@ private static bool TryGetResolver( } private void GatherVariablesInContext( + HashSet subgraphsInContext, ISelection selection, ObjectTypeMetadata declaringTypeMetadata, ISelection? parent, @@ -713,7 +761,11 @@ private void GatherVariablesInContext( foreach (var variable in declaringTypeMetadata.Variables) { - variablesInContext.Add(variable.Name); + if (subgraphsInContext.Count == 0 + || subgraphsInContext.Contains(variable.SubgraphName)) + { + variablesInContext.Add(variable.Name); + } } var field = declaringTypeMetadata.Fields[selection.Field.Name]; @@ -725,6 +777,7 @@ private void GatherVariablesInContext( } private void GatherVariablesInContext( + HashSet subgraphsInContext, ObjectTypeMetadata declaringTypeMetadata, ISelection parent, HashSet variablesInContext) @@ -741,7 +794,11 @@ private void GatherVariablesInContext( foreach (var variable in declaringTypeMetadata.Variables) { - variablesInContext.Add(variable.Name); + if (subgraphsInContext.Count == 0 + || subgraphsInContext.Contains(variable.SubgraphName)) + { + variablesInContext.Add(variable.Name); + } } } @@ -785,9 +842,9 @@ private void DetermineRequiredVariables( [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsNodeField(IObjectField field, IOperation operation) - => operation.Type is OperationType.Query && - field.DeclaringType.Equals(operation.RootType) && - (field.Name.EqualsOrdinal("node") || field.Name.EqualsOrdinal("nodes")); + => operation.Type is OperationType.Query + && field.DeclaringType.Equals(operation.RootType) + && (field.Name.EqualsOrdinal("node") || field.Name.EqualsOrdinal("nodes")); private bool EnsureStepCanBeResolvedFromRoot( string subgraphName, diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/RequirementsPlannerMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/RequirementsPlannerMiddleware.cs index 71f9c0b66bb..594cc69b48c 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/RequirementsPlannerMiddleware.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/RequirementsPlannerMiddleware.cs @@ -157,7 +157,7 @@ private static void Plan(QueryPlanContext context) // to evaluate the schemas that we did skip for efficiency reasons. if (requires.Count > 0) { - // if the schema meta data are not consistent we could end up with no way to + // if the schema metadata are not consistent we could end up with no way to // execute the current execution step. In this case we will fail here. throw new InvalidOperationException("The schema metadata are not consistent."); } diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlanContext.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlanContext.cs index 25e90889517..66ef44b51db 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlanContext.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlanContext.cs @@ -34,6 +34,8 @@ internal sealed class QueryPlanContext(IOperation operation) public Dictionary ParentSelections { get; } = new(); + public Dictionary> SubgraphInContext { get; } = new(); + public bool HasIntrospectionSelections { get; set; } public bool HasHandledSpecialQueryFields { get; set; } diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionPath.cs b/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionPath.cs index 31a4a1b26b8..2c870d18129 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionPath.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionPath.cs @@ -3,7 +3,7 @@ namespace HotChocolate.Fusion.Planning; -internal sealed class SelectionPath +internal sealed class SelectionPath : IEquatable { public SelectionPath(ISelection selection) : this(selection, null) @@ -23,6 +23,43 @@ private SelectionPath(ISelection selection, SelectionPath? parent) public SelectionPath Append(ISelection selection) => new(selection, this); + public bool Equals(SelectionPath? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if(Parent is null) + { + if(other.Parent is not null) + { + return false; + } + + return other.Selection.ResponseName.Equals(Selection.ResponseName); + } + + if (other.Parent is null) + { + return false; + } + + return Parent.Equals(other.Parent) + && other.Selection.ResponseName.Equals(Selection.ResponseName); + } + + public override bool Equals(object? obj) + => obj is SelectionPath other && Equals(other); + + public override int GetHashCode() + => HashCode.Combine(Selection.ResponseName, Parent?.GetHashCode()); + public override string ToString() { if (Parent is null) diff --git a/src/HotChocolate/Fusion/test/Core.Tests/RequestPlannerTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/RequestPlannerTests.cs index 9db0d179890..2cad49c2bcd 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/RequestPlannerTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/RequestPlannerTests.cs @@ -2724,6 +2724,76 @@ type Ratings { Assert.Equal(FusionResources.ThrowHelper_NoResolverInContext, exception.Message); } + [Fact] + public async Task Ensure_Subgraphs_In_Context_Are_Preferred() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + schema { + query: Query + } + + type Foo { + id: Int! + code: String! + name: String! + } + + type Query { + fooById(id: Int!): Foo! + fooByCode(code: String!): Foo! + } + """ + ); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + schema { + query: Query + } + + type Foo { + id: Int! + } + + type Query { + subgraph2Foo: Foo! + } + """ + ); + + var subgraphC = await TestSubgraph.CreateAsync( + """ + schema { + query: Query + } + + type Foo { + code: String! + } + + type Query { + subgraph3Foo: Foo! + } + """); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB, subgraphC]); + var fusionGraph = await subgraphs.GetFusionGraphAsync(); + + // act + var result = await CreateQueryPlanAsync( + fusionGraph, + //"query { subgraph2Foo { name } }"); + "query { subgraph3Foo { name } }"); + + // assert + var snapshot = new Snapshot(); + snapshot.Add(result.UserRequest, nameof(result.UserRequest)); + snapshot.Add(result.QueryPlan, nameof(result.QueryPlan)); + await snapshot.MatchMarkdownAsync(); + } + private static async Task<(DocumentNode UserRequest, Execution.Nodes.QueryPlan QueryPlan)> CreateQueryPlanAsync( Skimmed.SchemaDefinition fusionGraph, [StringSyntax("graphql")] string query) diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RequestPlannerTests.Ensure_Subgraphs_In_Context_Are_Preferred.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RequestPlannerTests.Ensure_Subgraphs_In_Context_Are_Preferred.md new file mode 100644 index 00000000000..4c8753d1530 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RequestPlannerTests.Ensure_Subgraphs_In_Context_Are_Preferred.md @@ -0,0 +1,65 @@ +# Ensure_Subgraphs_In_Context_Are_Preferred + +## UserRequest + +```graphql +{ + subgraph3Foo { + name + } +} +``` + +## QueryPlan + +```json +{ + "document": "{ subgraph3Foo { name } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_3", + "document": "query fetch_subgraph3Foo_1 { subgraph3Foo { __fusion_exports__1: code } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_subgraph3Foo_2($__fusion_exports__1: String!) { fooByCode(code: $__fusion_exports__1) { name } }", + "selectionSetId": 1, + "path": [ + "fooByCode" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 1 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Foo_code" + } +} +``` + From 03e7804af4e4774eaf4517207aba3e85c7f07a8e Mon Sep 17 00:00:00 2001 From: Glen Date: Tue, 18 Mar 2025 15:47:49 +0200 Subject: [PATCH 55/64] Skipped flaky test in Apollo WebSocketProtocolTests (#8143) --- .../Subscriptions/Apollo/WebSocketProtocolTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs index 612b7c3c396..ff327d2a179 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs @@ -520,7 +520,8 @@ public Task Send_Invalid_Message_Not_An_Object() Assert.Equal(CloseReasons.InvalidMessage, (int)webSocket.CloseStatus!.Value); }); - [Fact] + // TODO : FIX Flaky Test + [Fact(Skip = "Flaky")] public Task Send_Start_ReceiveDataOnMutation_StripNull() => TryTest( async ct => From 670583069e7ba5535443c19296963fb023bb5565 Mon Sep 17 00:00:00 2001 From: Glen Date: Tue, 18 Mar 2025 18:17:19 +0200 Subject: [PATCH 56/64] Fixed issue with parameter replacement in queryable field handlers (#8145) Co-authored-by: Darren Clark --- .../Handlers/QueryableDefaultFieldHandler.cs | 10 +- .../QueryableDefaultSortFieldHandler.cs | 10 +- .../QueryableFilterVisitorObjectTests.cs | 102 ++++++++++++++++++ ...ts.Create_ObjectStringEqual_Flattened.snap | 76 +++++++++++++ ..._ObjectStringEquals_Related_Flattened.snap | 73 +++++++++++++ 5 files changed, 259 insertions(+), 12 deletions(-) create mode 100644 src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorObjectTests.Create_ObjectStringEqual_Flattened.snap create mode 100644 src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorObjectTests.Create_ObjectStringEquals_Related_Flattened.snap diff --git a/src/HotChocolate/Data/src/Data/Filters/Expressions/Handlers/QueryableDefaultFieldHandler.cs b/src/HotChocolate/Data/src/Data/Filters/Expressions/Handlers/QueryableDefaultFieldHandler.cs index faa4061931b..58f6a18d998 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Expressions/Handlers/QueryableDefaultFieldHandler.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Expressions/Handlers/QueryableDefaultFieldHandler.cs @@ -65,8 +65,7 @@ public override bool TryHandleEnter( } nestedProperty = ReplaceVariableExpressionVisitor - .ReplaceParameter(expression, expression.Parameters[0], context.GetInstance()) - .Body; + .ReplaceParameter(expression.Body, expression.Parameters[0], context.GetInstance()); } else { @@ -172,12 +171,11 @@ protected override Expression VisitParameter(ParameterExpression node) return base.VisitParameter(node); } - public static LambdaExpression ReplaceParameter( - LambdaExpression lambda, + public static Expression ReplaceParameter( + Expression lambdaBody, ParameterExpression parameter, Expression replacement) - => (LambdaExpression) - new ReplaceVariableExpressionVisitor(replacement, parameter).Visit(lambda); + => new ReplaceVariableExpressionVisitor(replacement, parameter).Visit(lambdaBody); } } diff --git a/src/HotChocolate/Data/src/Data/Sorting/Expressions/Handlers/QueryableDefaultSortFieldHandler.cs b/src/HotChocolate/Data/src/Data/Sorting/Expressions/Handlers/QueryableDefaultSortFieldHandler.cs index f59e53a8934..203d7af5781 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Expressions/Handlers/QueryableDefaultSortFieldHandler.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Expressions/Handlers/QueryableDefaultSortFieldHandler.cs @@ -55,8 +55,7 @@ public override bool TryHandleEnter( } nextSelector = ReplaceVariableExpressionVisitor - .ReplaceParameter(expression, expression.Parameters[0], lastSelector) - .Body; + .ReplaceParameter(expression.Body, expression.Parameters[0], lastSelector); } else { @@ -127,11 +126,10 @@ protected override Expression VisitParameter(ParameterExpression node) return base.VisitParameter(node); } - public static LambdaExpression ReplaceParameter( - LambdaExpression lambda, + public static Expression ReplaceParameter( + Expression lambdaBody, ParameterExpression parameter, Expression replacement) - => (LambdaExpression) - new ReplaceVariableExpressionVisitor(replacement, parameter).Visit(lambda); + => new ReplaceVariableExpressionVisitor(replacement, parameter).Visit(lambdaBody); } } diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/QueryableFilterVisitorObjectTests.cs b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/QueryableFilterVisitorObjectTests.cs index d42575c10ba..6bc38e62627 100644 --- a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/QueryableFilterVisitorObjectTests.cs +++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/QueryableFilterVisitorObjectTests.cs @@ -104,6 +104,9 @@ public class QueryableFilterVisitorObjectTests }, ]; + private static readonly Baz[] _bazEntities = + _barEntities.Select(b => new Baz { Bar = b }).ToArray(); + private readonly SchemaCache _cache = new(); [Fact] @@ -620,6 +623,85 @@ await Snapshot .MatchAsync(); } + [Fact] + public async Task Create_ObjectStringEqual_Flattened() + { + // arrange + var tester = _cache.CreateSchema(_barEntities); + + // act + var res1 = await tester.ExecuteAsync( + OperationRequestBuilder.New() + .SetDocument( + "{ root(where: { fooBarString: { eq: \"testatest\" } }) " + + "{ foo { barString } } }") + .Build()); + + var res2 = await tester.ExecuteAsync( + OperationRequestBuilder.New() + .SetDocument( + "{ root(where: { fooBarString: { eq: \"testbtest\" } }) " + + "{ foo { barString } } }") + .Build()); + + var res3 = await tester.ExecuteAsync( + OperationRequestBuilder.New() + .SetDocument( + "{ root(where: { fooBarString: { eq: null } }) { foo { barString } } }") + .Build()); + + // assert + Assert.Null(Assert.IsType(res1).Errors); + Assert.Null(Assert.IsType(res2).Errors); + Assert.Null(Assert.IsType(res3).Errors); + await Snapshot + .Create() + .AddResult(res1, "testatest") + .AddResult(res2, "testbtest") + .AddResult(res3, "null") + .MatchAsync(); + } + + [Fact] + public async Task Create_ObjectStringEquals_Related_Flattened() + { + // arrange + var tester = _cache.CreateSchema(_bazEntities); + + // act + var res1 = await tester.ExecuteAsync( + OperationRequestBuilder.New() + .SetDocument( + "{ root(where: { bar: { fooBarString: { eq: \"testatest\" } } }) " + + "{ bar { foo { barString } } } }") + .Build()); + + var res2 = await tester.ExecuteAsync( + OperationRequestBuilder.New() + .SetDocument( + "{ root(where: { bar: { fooBarString: { eq: \"testbtest\" } } }) " + + "{ bar { foo { barString } } } }") + .Build()); + + var res3 = await tester.ExecuteAsync( + OperationRequestBuilder.New() + .SetDocument( + "{ root(where: { bar: { fooBarString: { eq: null } } }) " + + "{ bar { foo { barString } } } }") + .Build()); + + // assert + Assert.Null(Assert.IsType(res1).Errors); + Assert.Null(Assert.IsType(res2).Errors); + Assert.Null(Assert.IsType(res3).Errors); + await Snapshot + .Create() + .AddResult(res1, "testatest") + .AddResult(res2, "testbtest") + .AddResult(res3, "null") + .MatchAsync(); + } + public class Foo { public int Id { get; set; } @@ -664,14 +746,34 @@ public class BarNullable public FooNullable? Foo { get; set; } } + public class Baz + { + public int Id { get; set; } + public Bar Bar { get; set; } = default!; + } + public class BarFilterInput : FilterInputType { + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor.Field(t => t.Foo.BarString) + .Name("fooBarString"); + } } public class BarNullableFilterInput : FilterInputType { } + public class BazFilterInput : FilterInputType + { + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor.Field(b => b.Bar) + .Type(); + } + } + public enum BarEnum { FOO, diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorObjectTests.Create_ObjectStringEqual_Flattened.snap b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorObjectTests.Create_ObjectStringEqual_Flattened.snap new file mode 100644 index 00000000000..a30aaf90887 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorObjectTests.Create_ObjectStringEqual_Flattened.snap @@ -0,0 +1,76 @@ +testatest Result: +--------------- +{ + "data": { + "root": [ + { + "foo": { + "barString": "testatest" + } + }, + { + "foo": { + "barString": "testatest" + } + } + ] + } +} +--------------- + +testatest SQL: +--------------- +.param set @__p_0 'testatest' + +SELECT "d"."Id", "d"."FooId" +FROM "Data" AS "d" +INNER JOIN "Foo" AS "f" ON "d"."FooId" = "f"."Id" +WHERE "f"."BarString" = @__p_0 +--------------- + +testbtest Result: +--------------- +{ + "data": { + "root": [ + { + "foo": { + "barString": "testbtest" + } + }, + { + "foo": { + "barString": "testbtest" + } + } + ] + } +} +--------------- + +testbtest SQL: +--------------- +.param set @__p_0 'testbtest' + +SELECT "d"."Id", "d"."FooId" +FROM "Data" AS "d" +INNER JOIN "Foo" AS "f" ON "d"."FooId" = "f"."Id" +WHERE "f"."BarString" = @__p_0 +--------------- + +null Result: +--------------- +{ + "data": { + "root": [] + } +} +--------------- + +null SQL: +--------------- +SELECT "d"."Id", "d"."FooId" +FROM "Data" AS "d" +INNER JOIN "Foo" AS "f" ON "d"."FooId" = "f"."Id" +WHERE 0 +--------------- diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorObjectTests.Create_ObjectStringEquals_Related_Flattened.snap b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorObjectTests.Create_ObjectStringEquals_Related_Flattened.snap new file mode 100644 index 00000000000..016bb0dd8e4 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorObjectTests.Create_ObjectStringEquals_Related_Flattened.snap @@ -0,0 +1,73 @@ +testatest Result: +--------------- +{ + "data": { + "root": [ + { + "bar": { + "foo": { + "barString": "testatest" + } + } + } + ] + } +} +--------------- + +testatest SQL: +--------------- +.param set @__p_0 'testatest' + +SELECT "d"."Id", "d"."BarId" +FROM "Data" AS "d" +INNER JOIN "Bar" AS "b" ON "d"."BarId" = "b"."Id" +INNER JOIN "Foo" AS "f" ON "b"."FooId" = "f"."Id" +WHERE "f"."BarString" = @__p_0 +--------------- + +testbtest Result: +--------------- +{ + "data": { + "root": [ + { + "bar": { + "foo": { + "barString": "testbtest" + } + } + } + ] + } +} +--------------- + +testbtest SQL: +--------------- +.param set @__p_0 'testbtest' + +SELECT "d"."Id", "d"."BarId" +FROM "Data" AS "d" +INNER JOIN "Bar" AS "b" ON "d"."BarId" = "b"."Id" +INNER JOIN "Foo" AS "f" ON "b"."FooId" = "f"."Id" +WHERE "f"."BarString" = @__p_0 +--------------- + +null Result: +--------------- +{ + "data": { + "root": [] + } +} +--------------- + +null SQL: +--------------- +SELECT "d"."Id", "d"."BarId" +FROM "Data" AS "d" +INNER JOIN "Bar" AS "b" ON "d"."BarId" = "b"."Id" +INNER JOIN "Foo" AS "f" ON "b"."FooId" = "f"."Id" +WHERE 0 +--------------- From fdf48d541e80244f1ebf520c14bbbf47d29c3d11 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 18 Mar 2025 17:23:41 +0100 Subject: [PATCH 57/64] Fixes various analyzer issues related to paging. (#8148) --- .../FileBuilders/TypeFileBuilderBase.cs | 124 ++++-- .../Generators/DataLoaderGenerator.cs | 12 +- .../Generators/DataLoaderModuleGenerator.cs | 6 +- .../Generators/DefaultLocalTypeLookup.cs | 3 +- .../Generators/ISyntaxGenerator.cs | 4 +- .../Generators/MiddlewareGenerator.cs | 6 +- .../Generators/TypeModuleSyntaxGenerator.cs | 36 +- .../Generators/TypesSyntaxGenerator.cs | 20 +- .../Types.Analyzers/GraphQLServerGenerator.cs | 34 +- .../Types.Analyzers/Helpers/PooledObjects.cs | 62 ++- .../Helpers/SymbolExtensions.cs | 59 ++- .../Inspectors/ConnectionInspector.cs | 26 +- .../Inspectors/ConnectionTypeTransformer.cs | 71 ++-- .../Inspectors/ObjectTypeInspector.cs | 4 +- .../Core/src/Types.Analyzers/KnownSymbols.cs | 42 +- .../Models/ConnectionClassInfo.cs | 20 +- .../Models/ConnectionTypeInfo.cs | 22 +- .../Types.Analyzers/Models/DataLoaderInfo.cs | 2 +- .../Types.Analyzers/Models/EdgeTypeInfo.cs | 20 +- .../Models/ResolverParameter.cs | 9 +- .../Models/ResolverParameterKind.cs | 3 +- .../src/Types.Analyzers/WellKnownTypes.cs | 1 + .../DefaultParameterBindingResolver.cs | 1 + ...nnectionFlagsParameterExpressionBuilder.cs | 57 +++ .../Types/Types/Pagination/ConnectionFlags.cs | 38 ++ .../Types.Analyzers.Tests/OperationTests.cs | 21 + .../test/Types.Analyzers.Tests/PagingTests.cs | 260 +++++++++++++ .../test/Types.Analyzers.Tests/TestHelper.cs | 1 + .../OperationTests.Root_Empty.md | 75 ++++ ...ingTests.GenerateSource_ConnectionFlags.md | 359 ++++++++++++++++++ ...Source_CustomConnection_MatchesSnapshot.md | 4 + ...onnection_No_Duplicates_MatchesSnapshot.md | 4 + ...ction_IncludeTotalCount_MatchesSnapshot.md | 4 + ...it_From_ConnectionBase_Inherit_PageEdge.md | 343 +++++++++++++++++ ...erit_From_ConnectionBase_Reuse_PageEdge.md | 325 ++++++++++++++++ ...m_ConnectionBase_Reuse_PageEdge_Generic.md | 331 ++++++++++++++++ ...erateSource_Inherit_From_PageConnection.md | 345 +++++++++++++++++ .../IntegrationTests.CreateSchema.graphql | 16 +- 38 files changed, 2550 insertions(+), 220 deletions(-) create mode 100644 src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ConnectionFlagsParameterExpressionBuilder.cs create mode 100644 src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlags.cs create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Root_Empty.md create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionFlags.md create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Inherit_PageEdge.md create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge.md create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge_Generic.md create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_PageConnection.md diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs index 15d25774a8d..b9feeeb90d9 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs @@ -251,10 +251,15 @@ protected void WriteResolverField(Resolver resolver) public virtual void WriteResolverConstructor(IOutputTypeInfo type, ILocalTypeLookup typeLookup) { + var resolverType = + type.SchemaSchemaType + ?? type.RuntimeType + ?? throw new InvalidOperationException("Schema type and runtime type are null."); + WriteResolverConstructor( type, typeLookup, - type.Resolvers[0].Member.ContainingType.ToFullyQualified(), + resolverType.ToFullyQualified(), type.Resolvers.Any(t => t.RequiresParameterBindings)); } @@ -697,19 +702,19 @@ private void WriteResolverArguments(Resolver resolver, IMethodSymbol resolverMet break; case ResolverParameterKind.GetGlobalState when parameter.Parameter.HasExplicitDefaultValue: - { - var defaultValue = parameter.Parameter.ExplicitDefaultValue; - var defaultValueString = GeneratorUtils.ConvertDefaultValueToString(defaultValue, parameter.Type); + { + var defaultValue = parameter.Parameter.ExplicitDefaultValue; + var defaultValueString = GeneratorUtils.ConvertDefaultValueToString(defaultValue, parameter.Type); - Writer.WriteIndentedLine( - "var args{0} = context.GetGlobalStateOrDefault<{1}{2}>(\"{3}\", {4});", - i, - parameter.Type.ToFullyQualified(), - parameter.Type.IsNullableRefType() ? "?" : string.Empty, - parameter.Key, - defaultValueString); - break; - } + Writer.WriteIndentedLine( + "var args{0} = context.GetGlobalStateOrDefault<{1}{2}>(\"{3}\", {4});", + i, + parameter.Type.ToFullyQualified(), + parameter.Type.IsNullableRefType() ? "?" : string.Empty, + parameter.Key, + defaultValueString); + break; + } case ResolverParameterKind.GetGlobalState when !parameter.IsNullable: Writer.WriteIndentedLine( @@ -737,19 +742,19 @@ private void WriteResolverArguments(Resolver resolver, IMethodSymbol resolverMet break; case ResolverParameterKind.GetScopedState when parameter.Parameter.HasExplicitDefaultValue: - { - var defaultValue = parameter.Parameter.ExplicitDefaultValue; - var defaultValueString = GeneratorUtils.ConvertDefaultValueToString(defaultValue, parameter.Type); + { + var defaultValue = parameter.Parameter.ExplicitDefaultValue; + var defaultValueString = GeneratorUtils.ConvertDefaultValueToString(defaultValue, parameter.Type); - Writer.WriteIndentedLine( - "var args{0} = context.GetScopedStateOrDefault<{1}{2}>(\"{3}\", {4});", - i, - parameter.Type.ToFullyQualified(), - parameter.Type.IsNullableRefType() ? "?" : string.Empty, - parameter.Key, - defaultValueString); - break; - } + Writer.WriteIndentedLine( + "var args{0} = context.GetScopedStateOrDefault<{1}{2}>(\"{3}\", {4});", + i, + parameter.Type.ToFullyQualified(), + parameter.Type.IsNullableRefType() ? "?" : string.Empty, + parameter.Key, + defaultValueString); + break; + } case ResolverParameterKind.GetScopedState when !parameter.IsNullable: Writer.WriteIndentedLine( @@ -777,19 +782,19 @@ private void WriteResolverArguments(Resolver resolver, IMethodSymbol resolverMet break; case ResolverParameterKind.GetLocalState when parameter.Parameter.HasExplicitDefaultValue: - { - var defaultValue = parameter.Parameter.ExplicitDefaultValue; - var defaultValueString = GeneratorUtils.ConvertDefaultValueToString(defaultValue, parameter.Type); + { + var defaultValue = parameter.Parameter.ExplicitDefaultValue; + var defaultValueString = GeneratorUtils.ConvertDefaultValueToString(defaultValue, parameter.Type); - Writer.WriteIndentedLine( - "var args{0} = context.GetLocalStateOrDefault<{1}{2}>(\"{3}\", {4});", - i, - parameter.Type.ToFullyQualified(), - parameter.Type.IsNullableRefType() ? "?" : string.Empty, - parameter.Key, - defaultValueString); - break; - } + Writer.WriteIndentedLine( + "var args{0} = context.GetLocalStateOrDefault<{1}{2}>(\"{3}\", {4});", + i, + parameter.Type.ToFullyQualified(), + parameter.Type.IsNullableRefType() ? "?" : string.Empty, + parameter.Key, + defaultValueString); + break; + } case ResolverParameterKind.GetLocalState when !parameter.IsNullable: Writer.WriteIndentedLine( @@ -947,7 +952,52 @@ private void WriteResolverArguments(Resolver resolver, IMethodSymbol resolverMet Writer.WriteIndentedLine("};"); } + break; + + case ResolverParameterKind.ConnectionFlags: + Writer.WriteIndentedLine( + "var args{0} = global::{1}.Nothing;", + i, + WellKnownTypes.ConnectionFlags); + Writer.WriteLine(); + + Writer.WriteIndentedLine( + "if(context.IsSelected(\"edges\"))"); + Writer.WriteIndentedLine("{"); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine( + "args{0} |= global::{1}.Edges;", + i, + WellKnownTypes.ConnectionFlags); + } + Writer.WriteIndentedLine("}"); + Writer.WriteLine(); + + Writer.WriteIndentedLine( + "if(context.IsSelected(\"nodes\"))"); + Writer.WriteIndentedLine("{"); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine( + "args{0} |= global::{1}.Nodes;", + i, + WellKnownTypes.ConnectionFlags); + } + Writer.WriteIndentedLine("}"); + Writer.WriteLine(); + Writer.WriteIndentedLine( + "if(context.IsSelected(\"totalCount\"))"); + Writer.WriteIndentedLine("{"); + using (Writer.IncreaseIndent()) + { + Writer.WriteIndentedLine( + "args{0} |= global::{1}.TotalCount;", + i, + WellKnownTypes.ConnectionFlags); + } + Writer.WriteIndentedLine("}"); break; case ResolverParameterKind.Unknown: diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderGenerator.cs index b4f9b31aa5c..0ea236b8ac7 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderGenerator.cs @@ -4,6 +4,7 @@ using HotChocolate.Types.Analyzers.Inspectors; using HotChocolate.Types.Analyzers.Models; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; namespace HotChocolate.Types.Analyzers.Generators; @@ -12,16 +13,17 @@ public sealed class DataLoaderGenerator : ISyntaxGenerator public void Generate( SourceProductionContext context, string assemblyName, - ImmutableArray syntaxInfos) + ImmutableArray syntaxInfos, + Action addSource) { var dataLoaderDefaults = syntaxInfos.GetDataLoaderDefaults(); - WriteDataLoader(context, syntaxInfos, dataLoaderDefaults); + WriteDataLoader(syntaxInfos, dataLoaderDefaults, addSource); } private static void WriteDataLoader( - SourceProductionContext context, ImmutableArray syntaxInfos, - DataLoaderDefaultsInfo defaults) + DataLoaderDefaultsInfo defaults, + Action addSource) { var dataLoaders = new List(); @@ -122,7 +124,7 @@ private static void WriteDataLoader( if (hasDataLoaders) { - context.AddSource(WellKnownFileNames.DataLoaderFile, generator.ToSourceText()); + addSource(WellKnownFileNames.DataLoaderFile, generator.ToSourceText()); } } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderModuleGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderModuleGenerator.cs index 2ba6f29d3c3..532e1a7377e 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderModuleGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderModuleGenerator.cs @@ -3,6 +3,7 @@ using HotChocolate.Types.Analyzers.Helpers; using HotChocolate.Types.Analyzers.Models; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; namespace HotChocolate.Types.Analyzers.Generators; @@ -11,7 +12,8 @@ public sealed class DataLoaderModuleGenerator : ISyntaxGenerator public void Generate( SourceProductionContext context, string assemblyName, - ImmutableArray syntaxInfos) + ImmutableArray syntaxInfos, + Action addSource) { var module = GetDataLoaderModuleInfo(syntaxInfos); var dataLoaderDefaults = syntaxInfos.GetDataLoaderDefaults(); @@ -71,7 +73,7 @@ public void Generate( generator.WriteEndClass(); generator.WriteEndNamespace(); - context.AddSource(WellKnownFileNames.DataLoaderModuleFile, generator.ToSourceText()); + addSource(WellKnownFileNames.DataLoaderModuleFile, generator.ToSourceText()); } private static DataLoaderModuleInfo? GetDataLoaderModuleInfo( diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/DefaultLocalTypeLookup.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/DefaultLocalTypeLookup.cs index e43d7e8d7d3..eca6c6f32e7 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/DefaultLocalTypeLookup.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/DefaultLocalTypeLookup.cs @@ -9,7 +9,6 @@ namespace HotChocolate.Types.Analyzers.Generators; public sealed class DefaultLocalTypeLookup(ImmutableArray syntaxInfos) : ILocalTypeLookup { private Dictionary>? _typeNameLookup; - private readonly ImmutableArray _syntaxInfos = syntaxInfos; public bool TryGetTypeName( ITypeSymbol type, @@ -48,7 +47,7 @@ private Dictionary> GetTypeNameLookup() if (_typeNameLookup is null) { _typeNameLookup = new Dictionary>(); - foreach (var syntaxInfo in _syntaxInfos) + foreach (var syntaxInfo in syntaxInfos) { if(syntaxInfo is not DataLoaderInfo dataLoaderInfo) { diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ISyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ISyntaxGenerator.cs index 88d75e8edfe..465fa9b8b33 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ISyntaxGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ISyntaxGenerator.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using HotChocolate.Types.Analyzers.Models; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; namespace HotChocolate.Types.Analyzers.Generators; @@ -9,5 +10,6 @@ public interface ISyntaxGenerator void Generate( SourceProductionContext context, string assemblyName, - ImmutableArray syntaxInfos); + ImmutableArray syntaxInfos, + Action addSource); } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/MiddlewareGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/MiddlewareGenerator.cs index 5106319ffee..647a8695ede 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/MiddlewareGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/MiddlewareGenerator.cs @@ -3,6 +3,7 @@ using HotChocolate.Types.Analyzers.Helpers; using HotChocolate.Types.Analyzers.Models; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; namespace HotChocolate.Types.Analyzers.Generators; @@ -13,7 +14,8 @@ public sealed class MiddlewareGenerator : ISyntaxGenerator public void Generate( SourceProductionContext context, string assemblyName, - ImmutableArray syntaxInfos) + ImmutableArray syntaxInfos, + Action addSource) { if (syntaxInfos.IsEmpty) { @@ -63,6 +65,6 @@ public void Generate( generator.WriteEndNamespace(); - context.AddSource(WellKnownFileNames.MiddlewareFile, generator.ToSourceText()); + addSource(WellKnownFileNames.MiddlewareFile, generator.ToSourceText()); } } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypeModuleSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypeModuleSyntaxGenerator.cs index 01eb3d2498f..fadf5e43c0d 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypeModuleSyntaxGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypeModuleSyntaxGenerator.cs @@ -3,6 +3,7 @@ using HotChocolate.Types.Analyzers.Helpers; using HotChocolate.Types.Analyzers.Models; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; using TypeInfo = HotChocolate.Types.Analyzers.Models.TypeInfo; namespace HotChocolate.Types.Analyzers.Generators; @@ -12,13 +13,8 @@ public sealed class TypeModuleSyntaxGenerator : ISyntaxGenerator public void Generate( SourceProductionContext context, string assemblyName, - ImmutableArray syntaxInfos) - => Execute(context, assemblyName, syntaxInfos); - - private static void Execute( - SourceProductionContext context, - string assemblyName, - ImmutableArray syntaxInfos) + ImmutableArray syntaxInfos, + Action addSource) { if (syntaxInfos.IsEmpty) { @@ -40,14 +36,14 @@ private static void Execute( } var syntaxInfoList = syntaxInfos.ToList(); - WriteOperationTypes(context, syntaxInfoList, module); - WriteConfiguration(context, syntaxInfoList, module); + WriteOperationTypes(syntaxInfoList, module, addSource); + WriteConfiguration(syntaxInfoList, module, addSource); } private static void WriteConfiguration( - SourceProductionContext context, List syntaxInfos, - ModuleInfo module) + ModuleInfo module, + Action addSource) { var dataLoaderDefaults = syntaxInfos.GetDataLoaderDefaults(); HashSet<(string InterfaceName, string ClassName)>? groups = null; @@ -78,6 +74,7 @@ private static void WriteConfiguration( generator.WriteRegisterType(type.Name); hasConfigurations = true; } + break; case TypeExtensionInfo extension: @@ -91,6 +88,7 @@ private static void WriteConfiguration( operations |= extension.Type; } } + break; case RegisterDataLoaderInfo dataLoader: @@ -99,6 +97,7 @@ private static void WriteConfiguration( generator.WriteRegisterDataLoader(dataLoader.Name); hasConfigurations = true; } + break; case DataLoaderInfo dataLoader: @@ -124,6 +123,7 @@ private static void WriteConfiguration( } } } + break; case OperationRegistrationInfo operation: @@ -137,6 +137,7 @@ private static void WriteConfiguration( operations |= operation.Type; } } + break; case ObjectTypeInfo objectTypeExtension: @@ -152,6 +153,7 @@ private static void WriteConfiguration( objectTypeExtension.SchemaSchemaType.ToFullyQualified()); hasConfigurations = true; } + break; case ConnectionTypeInfo connectionType: @@ -161,6 +163,7 @@ private static void WriteConfiguration( generator.WriteRegisterType($"{connectionType.Namespace}.{connectionType.Name}"); hasConfigurations = true; } + break; case EdgeTypeInfo edgeType: @@ -170,6 +173,7 @@ private static void WriteConfiguration( generator.WriteRegisterType($"{edgeType.Namespace}.{edgeType.Name}"); hasConfigurations = true; } + break; case InterfaceTypeInfo interfaceType: @@ -185,6 +189,7 @@ private static void WriteConfiguration( interfaceType.SchemaSchemaType.ToFullyQualified()); hasConfigurations = true; } + break; case RootTypeInfo rootType: @@ -204,6 +209,7 @@ private static void WriteConfiguration( operations |= operationType; } } + break; } } @@ -256,14 +262,14 @@ private static void WriteConfiguration( if (hasConfigurations) { - context.AddSource(WellKnownFileNames.TypeModuleFile, generator.ToSourceText()); + addSource(WellKnownFileNames.TypeModuleFile, generator.ToSourceText()); } } private static void WriteOperationTypes( - SourceProductionContext context, List syntaxInfos, - ModuleInfo module) + ModuleInfo module, + Action addSource) { var operations = new List(); @@ -299,7 +305,7 @@ private static void WriteOperationTypes( generator.WriteEndNamespace(); - context.AddSource(WellKnownFileNames.RootTypesFile, generator.ToSourceText()); + addSource(WellKnownFileNames.RootTypesFile, generator.ToSourceText()); } public static string GetAssemblyQualifiedName(ITypeSymbol typeSymbol) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypesSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypesSyntaxGenerator.cs index 4987a492900..7e3d776194b 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypesSyntaxGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/TypesSyntaxGenerator.cs @@ -6,6 +6,7 @@ using HotChocolate.Types.Analyzers.Helpers; using HotChocolate.Types.Analyzers.Models; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; namespace HotChocolate.Types.Analyzers.Generators; @@ -14,7 +15,8 @@ public sealed class TypesSyntaxGenerator : ISyntaxGenerator public void Generate( SourceProductionContext context, string assemblyName, - ImmutableArray syntaxInfos) + ImmutableArray syntaxInfos, + Action addSource) { if (syntaxInfos.IsEmpty) { @@ -31,16 +33,16 @@ public void Generate( var sb = PooledObjects.GetStringBuilder(); - WriteTypes(context, syntaxInfos, sb); + WriteTypes(syntaxInfos, sb, addSource); sb.Clear(); PooledObjects.Return(sb); } private static void WriteTypes( - SourceProductionContext context, ImmutableArray syntaxInfos, - StringBuilder sb) + StringBuilder sb, + Action addSource) { var typeLookup = new DefaultLocalTypeLookup(syntaxInfos); @@ -52,35 +54,35 @@ private static void WriteTypes( { var file = new ObjectTypeFileBuilder(sb); WriteFile(file, objectType, typeLookup); - context.AddSource(CreateFileName(objectType), sb.ToString()); + addSource(CreateFileName(objectType), SourceText.From(sb.ToString(), Encoding.UTF8)); } if(type is InterfaceTypeInfo interfaceType) { var file = new InterfaceTypeFileBuilder(sb); WriteFile(file, interfaceType, typeLookup); - context.AddSource(CreateFileName(interfaceType), sb.ToString()); + addSource(CreateFileName(interfaceType), SourceText.From(sb.ToString(), Encoding.UTF8)); } if(type is RootTypeInfo rootType) { var file = new RootTypeFileBuilder(sb); WriteFile(file, rootType, typeLookup); - context.AddSource(CreateFileName(rootType), sb.ToString()); + addSource(CreateFileName(rootType), SourceText.From(sb.ToString(), Encoding.UTF8)); } if(type is ConnectionTypeInfo connectionType) { var file = new ConnectionTypeFileBuilder(sb); WriteFile(file, connectionType, typeLookup); - context.AddSource(CreateFileName(connectionType), sb.ToString()); + addSource(CreateFileName(connectionType), SourceText.From(sb.ToString(), Encoding.UTF8)); } if(type is EdgeTypeInfo edgeType) { var file = new EdgeTypeFileBuilder(sb); WriteFile(file, edgeType, typeLookup); - context.AddSource(CreateFileName(edgeType), sb.ToString()); + addSource(CreateFileName(edgeType), SourceText.From(sb.ToString(), Encoding.UTF8)); } } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/GraphQLServerGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/GraphQLServerGenerator.cs index 2fb116c2d7b..b11c172249e 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/GraphQLServerGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/GraphQLServerGenerator.cs @@ -2,10 +2,12 @@ using System.Collections.Immutable; using HotChocolate.Types.Analyzers.Filters; using HotChocolate.Types.Analyzers.Generators; +using HotChocolate.Types.Analyzers.Helpers; using HotChocolate.Types.Analyzers.Inspectors; using HotChocolate.Types.Analyzers.Models; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; namespace HotChocolate.Types.Analyzers; @@ -57,11 +59,12 @@ static GraphQLServerGenerator() foreach (var supportedKind in inspector.SupportedKinds) { - if(!inspectorLookup.TryGetValue(supportedKind, out var inspectors)) + if (!inspectorLookup.TryGetValue(supportedKind, out var inspectors)) { inspectors = []; inspectorLookup[supportedKind] = inspectors; } + inspectors.Add(inspector); } } @@ -134,20 +137,37 @@ private static void Execute( string assemblyName, ImmutableArray syntaxInfos) { - foreach (var syntaxInfo in syntaxInfos.AsSpan()) + var processedFiles = PooledObjects.GetStringSet(); + + try { - if (syntaxInfo.Diagnostics.Length > 0) + foreach (var syntaxInfo in syntaxInfos.AsSpan()) { - foreach (var diagnostic in syntaxInfo.Diagnostics.AsSpan()) + if (syntaxInfo.Diagnostics.Length > 0) { - context.ReportDiagnostic(diagnostic); + foreach (var diagnostic in syntaxInfo.Diagnostics.AsSpan()) + { + context.ReportDiagnostic(diagnostic); + } } } + + foreach (var generator in _generators.AsSpan()) + { + generator.Generate(context, assemblyName, syntaxInfos, AddSource); + } + } + finally + { + PooledObjects.Return(processedFiles); } - foreach (var generator in _generators.AsSpan()) + void AddSource(string fileName, SourceText sourceText) { - generator.Generate(context, assemblyName, syntaxInfos); + if (processedFiles.Add(fileName)) + { + context.AddSource(fileName, sourceText); + } } } } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/PooledObjects.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/PooledObjects.cs index e49af3e92f1..aaa7eb59e0a 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/PooledObjects.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/PooledObjects.cs @@ -4,17 +4,71 @@ namespace HotChocolate.Types.Analyzers.Helpers; public static class PooledObjects { - private static StringBuilder? _stringBuilder; + private static readonly HashSet?[] _stringSets = new HashSet[8]; + private static int _nextStringSetIndex = -1; + + private static readonly StringBuilder?[] _stringBuilders = new StringBuilder[8]; + private static int _nextStringBuilderIndex = -1; + + private static readonly object _lock = new(); public static StringBuilder GetStringBuilder() { - var stringBuilder = Interlocked.Exchange(ref _stringBuilder, null); - return stringBuilder ?? new StringBuilder(); + lock (_lock) + { + if (_nextStringBuilderIndex >= 0) + { + var sb = _stringBuilders[_nextStringBuilderIndex]; + _stringBuilders[_nextStringBuilderIndex] = null; + _nextStringBuilderIndex--; + return sb ?? new StringBuilder(); + } + } + + return new StringBuilder(); + } + + public static HashSet GetStringSet() + { + lock (_lock) + { + if (_nextStringSetIndex >= 0) + { + var set = _stringSets[_nextStringSetIndex]; + _stringSets[_nextStringSetIndex] = null; + _nextStringSetIndex--; + return set ?? []; + } + } + + return []; } public static void Return(StringBuilder stringBuilder) { stringBuilder.Clear(); - Interlocked.CompareExchange(ref _stringBuilder, stringBuilder, null); + + lock (_lock) + { + if (_nextStringBuilderIndex + 1 < _stringBuilders.Length) + { + _nextStringBuilderIndex++; + _stringBuilders[_nextStringBuilderIndex] = stringBuilder; + } + } + } + + public static void Return(HashSet stringSet) + { + stringSet.Clear(); + + lock (_lock) + { + if (_nextStringSetIndex + 1 < _stringSets.Length) + { + _nextStringSetIndex++; + _stringSets[_nextStringSetIndex] = stringSet; + } + } } } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs index 8fb4dc89750..a15cb319561 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/SymbolExtensions.cs @@ -576,39 +576,6 @@ public static bool IsConnectionType(this INamedTypeSymbol typeSymbol, Compilatio s => SymbolEqualityComparer.Default.Equals(s.OriginalDefinition, connectionInterface)); } - public static bool IsConnectionBase(this ITypeSymbol typeSymbol) - { - if (typeSymbol is INamedTypeSymbol { IsGenericType: true } namedType) - { - var definitionName = namedType.ConstructedFrom.ToDisplayString(); - - if (definitionName.StartsWith("System.Threading.Tasks.Task<") - || definitionName.StartsWith("System.Threading.Tasks.ValueTask<")) - { - typeSymbol = namedType.TypeArguments[0]; - } - } - - var current = typeSymbol; - - while (current != null) - { - if (current is INamedTypeSymbol { IsGenericType: true } namedTypeSymbol) - { - var baseType = namedTypeSymbol.ConstructedFrom; - - if (baseType.ToDisplayString().StartsWith("HotChocolate.Types.Pagination.ConnectionBase<")) - { - return true; - } - } - - current = current.BaseType; - } - - return false; - } - public static ITypeSymbol UnwrapTaskOrValueTask(this ITypeSymbol typeSymbol) { if (typeSymbol is INamedTypeSymbol { IsGenericType: true } namedType) @@ -665,4 +632,30 @@ public static bool IsCompilerGenerated(this IMethodSymbol method) .GetAttributes() .Any(attr => attr.AttributeClass?.ToDisplayString() == "System.Runtime.CompilerServices.CompilerGeneratedAttribute"); + + public static IEnumerable AllPublicInstanceMembers(this ITypeSymbol type) + { + var processed = PooledObjects.GetStringSet(); + var current = type; + + while (current is not null && current.SpecialType != SpecialType.System_Object) + { + foreach (var member in current.GetMembers()) + { + if (member.DeclaredAccessibility == Accessibility.Public + && member.Kind is SymbolKind.Property or SymbolKind.Method + && !member.IsStatic + && !member.IsIgnored() + && processed.Add(member.Name)) + { + yield return member; + } + } + + current = current.BaseType; + } + + processed.Clear(); + PooledObjects.Return(processed); + } } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionInspector.cs index bb30078dcd8..9f9b2670662 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionInspector.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using HotChocolate.Types.Analyzers.Helpers; using HotChocolate.Types.Analyzers.Models; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -15,14 +14,25 @@ protected override bool TryHandle( INamedTypeSymbol namedType, [NotNullWhen(true)] out ConnectionClassInfo? syntaxInfo) { - if (namedType is { IsStatic: false, IsAbstract: false } - && context.IsConnectionBase(namedType)) + if (namedType is { IsStatic: false, IsAbstract: false }) { - syntaxInfo = ConnectionClassInfo.CreateConnection( - context.SemanticModel.Compilation, - namedType, - classDeclaration); - return true; + if (context.IsConnectionType(namedType)) + { + syntaxInfo = ConnectionClassInfo.CreateConnection( + context.SemanticModel.Compilation, + namedType, + classDeclaration); + return true; + } + + if (context.IsEdgeType(namedType)) + { + syntaxInfo = ConnectionClassInfo.CreateEdge( + context.SemanticModel.Compilation, + namedType, + classDeclaration); + return true; + } } syntaxInfo = null; diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs index 5f1feab0913..6c7acbdfb03 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs @@ -180,6 +180,7 @@ public ImmutableArray Transform( continue; } + string connectionFullTypeName = connectionType.ToFullyQualified(); string? connectionName = null; string? edgeName = null; @@ -188,6 +189,19 @@ public ImmutableArray Transform( connectionName = $"{name}Connection"; edgeName = $"{name}Edge"; } + else + { + name = + connectionClassLookup.TryGetValue(connectionFullTypeName, out connectionClass) + ? connectionClass.RuntimeType.Name + : connectionType.Name; + if (name.EndsWith("Connection")) + { + name = name.Substring(0, name.Length - "Connection".Length); + connectionName = name + "Connection"; + edgeName = name + "Edge"; + } + } edgeTypeInfo = connectionClassLookup.TryGetValue(edgeType.ToFullyQualified(), out edgeClass) @@ -195,7 +209,7 @@ public ImmutableArray Transform( : EdgeTypeInfo.CreateEdge(compilation, edgeType, null, edgeName, edgeName); connectionTypeInfo = - connectionClassLookup.TryGetValue(connectionType.ToFullyQualified(), out connectionClass) + connectionClassLookup.TryGetValue(connectionFullTypeName, out connectionClass) ? ConnectionTypeInfo.CreateConnectionFrom( connectionClass, edgeTypeInfo.Name, @@ -242,7 +256,9 @@ public ImmutableArray Transform( return syntaxInfos; } - private static INamedTypeSymbol GetConnectionType(Compilation compilation, ITypeSymbol? possibleConnectionType) + private static INamedTypeSymbol GetConnectionType( + Compilation compilation, + ITypeSymbol? possibleConnectionType) { if (possibleConnectionType is null) { @@ -341,30 +357,12 @@ private static INamedTypeSymbol GetConnectionType(Compilation compilation, IType return new GenericTypeInfo(typeDefinitionName, genericType, name, nameFormat); } - private sealed class GenericTypeInfo( - string typeDefinitionName, - INamedTypeSymbol type, - string name, - string? nameFormat) + private static INamedTypeSymbol? GetEdgeType( + INamedTypeSymbol connectionType) { - public string TypeDefinitionName { get; } = typeDefinitionName; - public INamedTypeSymbol Type { get; } = type; - public string Name { get; } = name; - public string? NameFormat { get; } = nameFormat; - } - - private static INamedTypeSymbol? GetEdgeType(INamedTypeSymbol connectionType) - { - var property = connectionType.GetMembers() - .OfType() - .FirstOrDefault(p => p.Name == "Edges"); - - if (property is null) - { - return null; - } + var property = GetEdgesProperty(connectionType); + var returnType = property?.GetReturnType(); - var returnType = property.GetReturnType(); if (returnType is not INamedTypeSymbol namedType || !namedType.IsGenericType || namedType.TypeArguments.Length != 1 @@ -375,4 +373,29 @@ private sealed class GenericTypeInfo( return (INamedTypeSymbol)namedType.TypeArguments[0]; } + + private static IPropertySymbol? GetEdgesProperty( + INamedTypeSymbol connectionType) + { + var member = connectionType.AllPublicInstanceMembers().FirstOrDefault(p => p.Name == "Edges"); + + if (member is IPropertySymbol property) + { + return property; + } + + return null; + } + + private sealed class GenericTypeInfo( + string typeDefinitionName, + INamedTypeSymbol type, + string name, + string? nameFormat) + { + public string TypeDefinitionName { get; } = typeDefinitionName; + public INamedTypeSymbol Type { get; } = type; + public string Name { get; } = name; + public string? NameFormat { get; } = nameFormat; + } } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ObjectTypeInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ObjectTypeInspector.cs index bcc674aa590..def6d455300 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ObjectTypeInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ObjectTypeInspector.cs @@ -256,9 +256,9 @@ public static Resolver CreateResolver( resolverTypeName, resolverMethod, resolverMethod.GetResultKind(), - resolverParameters.ToImmutableArray(), + [..resolverParameters], resolverMethod.GetMemberBindings(), - kind: resolverMethod.ReturnType.IsConnectionBase() + kind: compilation.IsConnectionType(resolverMethod.ReturnType) ? ResolverKind.ConnectionResolver : ResolverKind.Default); } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs b/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs index a76024dcfd8..005460c587a 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs @@ -90,6 +90,10 @@ public static INamedTypeSymbol GetConnectionBaseSymbol(this Compilation compilat => compilation.GetTypeByMetadataName("HotChocolate.Types.Pagination.ConnectionBase`3") ?? throw new InvalidOperationException("Could not resolve connection base type."); + public static INamedTypeSymbol GetEdgeInterfaceSymbol(this Compilation compilation) + => compilation.GetTypeByMetadataName("HotChocolate.Types.Pagination.IEdge`1") + ?? throw new InvalidOperationException("Could not resolve edge interface type."); + public static INamedTypeSymbol GetTaskSymbol(this Compilation compilation) => compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1") ?? throw new InvalidOperationException("Could not resolve connection base type."); @@ -98,8 +102,15 @@ public static INamedTypeSymbol GetValueTaskSymbol(this Compilation compilation) => compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask`1") ?? throw new InvalidOperationException("Could not resolve connection base type."); - public static bool IsConnectionBase(this GeneratorSyntaxContext context, ITypeSymbol possibleConnectionType) - => context.SemanticModel.Compilation.IsConnectionBase(possibleConnectionType); + public static INamedTypeSymbol GetConnectionFlagsSymbol(this Compilation compilation) + => compilation.GetTypeByMetadataName("HotChocolate.Types.Pagination.ConnectionFlags") + ?? throw new InvalidOperationException("Could not resolve connection flags type."); + + public static bool IsConnectionType(this GeneratorSyntaxContext context, ITypeSymbol possibleConnectionType) + => context.SemanticModel.Compilation.IsConnectionType(possibleConnectionType); + + public static bool IsEdgeType(this GeneratorSyntaxContext context, ITypeSymbol possibleEdgeType) + => context.SemanticModel.Compilation.IsEdgeType(possibleEdgeType); public static bool IsTaskOrValueTask( this Compilation compilation, @@ -123,7 +134,7 @@ public static bool IsTaskOrValueTask( return false; } - public static bool IsConnectionBase(this Compilation compilation, ITypeSymbol possibleConnectionType) + public static bool IsConnectionType(this Compilation compilation, ITypeSymbol possibleConnectionType) { if (compilation.IsTaskOrValueTask(possibleConnectionType, out var innerType)) { @@ -138,6 +149,31 @@ public static bool IsConnectionBase(this Compilation compilation, ITypeSymbol po return IsDerivedFromGenericBase(namedType, compilation.GetConnectionBaseSymbol()); } + public static bool IsEdgeType(this Compilation compilation, ITypeSymbol possibleEdgeType) + { + if (compilation.IsTaskOrValueTask(possibleEdgeType, out var innerType)) + { + possibleEdgeType = innerType; + } + + if (possibleEdgeType is not INamedTypeSymbol namedType) + { + return false; + } + + return IsDerivedFromGenericBase(namedType, compilation.GetEdgeInterfaceSymbol()); + } + + public static bool IsConnectionFlagsType(this Compilation compilation, ITypeSymbol possibleConnectionFlagsType) + { + if (possibleConnectionFlagsType is not INamedTypeSymbol namedType) + { + return false; + } + + return SymbolEqualityComparer.Default.Equals(namedType, compilation.GetConnectionFlagsSymbol()); + } + private static bool IsDerivedFromGenericBase(INamedTypeSymbol typeSymbol, INamedTypeSymbol baseTypeSymbol) { var current = typeSymbol; diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionClassInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionClassInfo.cs index fdeb2adfa77..f0382997403 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionClassInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionClassInfo.cs @@ -76,27 +76,11 @@ private static ConnectionClassInfo Create( var resolvers = ImmutableArray.CreateBuilder(); - foreach (var member in runtimeType.GetMembers()) + foreach (var member in runtimeType.AllPublicInstanceMembers()) { - if (member.DeclaredAccessibility is not Accessibility.Public - || member.IsStatic - || member.IsIgnored()) - { - continue; - } - switch (member) { - case IMethodSymbol method: - if (method.IsPropertyOrEventAccessor() - || method.IsOperator() - || method.IsConstructor() - || method.IsSpecialMethod() - || method.IsCompilerGenerated()) - { - continue; - } - + case IMethodSymbol { MethodKind: MethodKind.Ordinary } method: resolvers.Add(ObjectTypeInspector.CreateResolver(compilation, runtimeType, method, name)); break; diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionTypeInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionTypeInfo.cs index f6db415e951..a27ed802a69 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionTypeInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/ConnectionTypeInfo.cs @@ -8,7 +8,7 @@ namespace HotChocolate.Types.Analyzers.Models; public sealed class ConnectionTypeInfo : SyntaxInfo - , IOutputTypeInfo + , IOutputTypeInfo { private ConnectionTypeInfo( string name, @@ -125,27 +125,11 @@ private static ConnectionTypeInfo Create( var resolvers = ImmutableArray.CreateBuilder(); - foreach (var member in runtimeType.GetMembers()) + foreach (var member in runtimeType.AllPublicInstanceMembers()) { - if (member.DeclaredAccessibility is not Accessibility.Public - || member.IsStatic - || member.IsIgnored()) - { - continue; - } - switch (member) { - case IMethodSymbol method: - if (method.IsPropertyOrEventAccessor() - || method.IsOperator() - || method.IsConstructor() - || method.IsSpecialMethod() - || method.IsCompilerGenerated()) - { - continue; - } - + case IMethodSymbol { MethodKind: MethodKind.Ordinary } method: resolvers.Add(CreateResolver(compilation, runtimeType, method, connectionName)); break; diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderInfo.cs index df249aef3cc..5b022a473d2 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/DataLoaderInfo.cs @@ -90,7 +90,7 @@ public ImmutableArray GetLookups(ITypeSymbol keyType, ITypeSymbol v { foreach (var method in MethodSymbol.ContainingType.GetMembers() .OfType() - .Where(m => m.Name == lookup)) + .Where(m => m.Name == lookup && m.MethodKind is MethodKind.Ordinary)) { if (method.Parameters.Length == 1 && method.Parameters[0].Type.Equals(valueType, SymbolEqualityComparer.Default) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/EdgeTypeInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/EdgeTypeInfo.cs index a2d045a0c10..b829a0b73e2 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/EdgeTypeInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/EdgeTypeInfo.cs @@ -117,27 +117,11 @@ private static EdgeTypeInfo Create( var resolvers = ImmutableArray.CreateBuilder(); - foreach (var member in runtimeType.GetMembers()) + foreach (var member in runtimeType.AllPublicInstanceMembers()) { - if (member.DeclaredAccessibility is not Accessibility.Public - || member.IsStatic - || member.IsIgnored()) - { - continue; - } - switch (member) { - case IMethodSymbol method: - if (method.IsPropertyOrEventAccessor() - || method.IsOperator() - || method.IsConstructor() - || method.IsSpecialMethod() - || method.IsCompilerGenerated()) - { - continue; - } - + case IMethodSymbol { MethodKind: MethodKind.Ordinary } method: resolvers.Add(ObjectTypeInspector.CreateResolver(compilation, runtimeType, method, edgeName)); break; diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs index 8a1177d9241..774e0c902c4 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameter.cs @@ -42,7 +42,9 @@ or ResolverParameterKind.DocumentNode or ResolverParameterKind.EventMessage or ResolverParameterKind.FieldNode or ResolverParameterKind.OutputField - or ResolverParameterKind.ClaimsPrincipal; + or ResolverParameterKind.ClaimsPrincipal + or ResolverParameterKind.ConnectionFlags; + public bool RequiresBinding => Kind == ResolverParameterKind.Unknown; @@ -152,6 +154,11 @@ private static ResolverParameterKind GetParameterKind( return ResolverParameterKind.PagingArguments; } + if(compilation.IsConnectionFlagsType(parameter.Type)) + { + return ResolverParameterKind.ConnectionFlags; + } + return ResolverParameterKind.Unknown; } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameterKind.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameterKind.cs index 5d2296f24e6..a7c653e5d41 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameterKind.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/ResolverParameterKind.cs @@ -22,5 +22,6 @@ public enum ResolverParameterKind Service, Argument, QueryContext, - PagingArguments + PagingArguments, + ConnectionFlags } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs index a389e53ca9d..850be96787d 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs @@ -93,6 +93,7 @@ public static class WellKnownTypes public const string ObjectTypeDefinition = "HotChocolate.Types.Descriptors.Definitions.ObjectTypeDefinition"; public const string NonNullType = "HotChocolate.Types.NonNullType"; public const string ListType = "HotChocolate.Types.ListType"; + public const string ConnectionFlags = "HotChocolate.Types.Paging.ConnectionFlags"; public static HashSet TypeClass { get; } = [ diff --git a/src/HotChocolate/Core/src/Types/Resolvers/DefaultParameterBindingResolver.cs b/src/HotChocolate/Core/src/Types/Resolvers/DefaultParameterBindingResolver.cs index ecce7b8ac7c..88047721c2a 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/DefaultParameterBindingResolver.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/DefaultParameterBindingResolver.cs @@ -54,6 +54,7 @@ public DefaultParameterBindingResolver( bindingFactories.Add(new FieldParameterExpressionBuilder()); bindingFactories.Add(new ClaimsPrincipalParameterExpressionBuilder()); bindingFactories.Add(new PathParameterExpressionBuilder()); + bindingFactories.Add(new ConnectionFlagsParameterExpressionBuilder()); if (customBindingFactories is not null) { diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ConnectionFlagsParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ConnectionFlagsParameterExpressionBuilder.cs new file mode 100644 index 00000000000..1b4374d851b --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ConnectionFlagsParameterExpressionBuilder.cs @@ -0,0 +1,57 @@ +using System.Linq.Expressions; +using System.Reflection; +using HotChocolate.Internal; +using HotChocolate.Types.Pagination; + +namespace HotChocolate.Resolvers.Expressions.Parameters; + +internal sealed class ConnectionFlagsParameterExpressionBuilder + : IParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding +{ + public ArgumentKind Kind => ArgumentKind.Custom; + + public bool IsPure => true; + + public bool IsDefaultHandler => false; + + public bool CanHandle(ParameterInfo parameter) + => parameter.ParameterType == typeof(ConnectionFlags); + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public Expression Build(ParameterExpressionBuilderContext context) + => CreateInvokeExpression(context.ResolverContext, ctx => Execute(ctx)); + + private InvocationExpression CreateInvokeExpression( + Expression context, + Expression> lambda) + => Expression.Invoke(lambda, context); + + public T Execute(IResolverContext context) + => (T)(object)Execute(context); + + private static ConnectionFlags Execute(IResolverContext context) + { + var flags = ConnectionFlags.Nothing; + + if (context.IsSelected("totalCount")) + { + flags |= ConnectionFlags.TotalCount; + } + + if (context.IsSelected("edges")) + { + flags |= ConnectionFlags.Edges; + } + + if (context.IsSelected("nodes")) + { + flags |= ConnectionFlags.Nodes; + } + + return flags; + } +} diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlags.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlags.cs new file mode 100644 index 00000000000..463ae00c3a4 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlags.cs @@ -0,0 +1,38 @@ +namespace HotChocolate.Types.Pagination; + +/// +/// This enum specified what parts of the connection where requested by the user. +/// +[Flags] +public enum ConnectionFlags +{ + /// + /// No flags are set. + /// + Nothing = 0, + + /// + /// The edges field was requested by the user. + /// + Edges = 1, + + /// + /// The nodes field was requested by the user. + /// + Nodes = 2, + + /// + /// The total count field was requested by the user. + /// + TotalCount = 4, + + /// + /// The nodes or edges field was requested by the user. + /// + NodesOrEdges = Edges | Nodes, + + /// + /// All fields were requested by the user. + /// + All = Edges | Nodes | TotalCount +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/OperationTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/OperationTests.cs index 0d2113c5468..dde26abf7dd 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/OperationTests.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/OperationTests.cs @@ -119,4 +119,25 @@ public class Foo } """).MatchMarkdownAsync(); } + + [Fact] + public async Task Root_Empty() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + using HotChocolate.Types.Relay; + using GreenDonut.Data; + + namespace TestNamespace; + + [QueryType] + public static partial class Query + { + + } + """).MatchMarkdownAsync(); + } } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/PagingTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/PagingTests.cs index 75cf32a3bd7..87d28b1a0c9 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/PagingTests.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/PagingTests.cs @@ -429,4 +429,264 @@ public class CustomEdge : IEdge } """).MatchMarkdownAsync(); } + + [Fact] + public async Task GenerateSource_Inherit_From_PageConnection() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + using HotChocolate.Types.Pagination; + + namespace TestNamespace + { + public sealed class Author + { + public int Id { get; set; } + public string Name { get; set; } + } + } + + namespace TestNamespace.Types.Root + { + [QueryType] + public static partial class AuthorQueries + { + public static Task GetAuthorsAsync( + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + } + } + + namespace TestNamespace + { + public class AuthorConnection : PageConnection + { + public AuthorConnection(GreenDonut.Data.Page page) + : base(page) + { + } + + public string CustomResolver() => "Foo"; + } + } + """).MatchMarkdownAsync(); + } + + [Fact] + public async Task GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + using HotChocolate.Types.Pagination; + + namespace TestNamespace + { + public sealed class Author + { + public int Id { get; set; } + public string Name { get; set; } + } + } + + namespace TestNamespace.Types.Root + { + [QueryType] + public static partial class AuthorQueries + { + public static Task GetAuthorsAsync( + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + } + } + + namespace TestNamespace + { + public class AuthorConnection : ConnectionBase, ConnectionPageInfo> + { + public override IReadOnlyList>? Edges => default!; + + public IReadOnlyList Nodes => default!; + + public override ConnectionPageInfo PageInfo => default!; + + public int TotalCount => 0; + } + } + """).MatchMarkdownAsync(); + } + + [Fact] + public async Task GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge_Generic() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + using HotChocolate.Types.Pagination; + + namespace TestNamespace + { + public sealed class Author + { + public int Id { get; set; } + public string Name { get; set; } + } + } + + namespace TestNamespace.Types.Root + { + [QueryType] + public static partial class AuthorQueries + { + public static Task> GetAuthorsAsync( + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + } + } + + namespace TestNamespace + { + public class CustomConnection : ConnectionBase, ConnectionPageInfo> + { + public override IReadOnlyList>? Edges => default!; + + public IReadOnlyList Nodes => default!; + + public override ConnectionPageInfo PageInfo => default!; + + public int TotalCount => 0; + } + } + """).MatchMarkdownAsync(); + } + + [Fact] + public async Task GenerateSource_Inherit_From_ConnectionBase_Inherit_PageEdge() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + using HotChocolate.Types.Pagination; + + namespace TestNamespace + { + public sealed class Author + { + public int Id { get; set; } + public string Name { get; set; } + } + } + + namespace TestNamespace.Types.Root + { + [QueryType] + public static partial class AuthorQueries + { + public static Task GetAuthorsAsync( + GreenDonut.Data.PagingArguments pagingArgs, + CancellationToken cancellationToken) + => default!; + } + } + + namespace TestNamespace + { + public class AuthorConnection : ConnectionBase + { + public override IReadOnlyList? Edges => default!; + + public IReadOnlyList Nodes => default!; + + public override ConnectionPageInfo PageInfo => default!; + + public int TotalCount => 0; + } + + public class AuthorEdge(GreenDonut.Data.Page page, Author author) : PageEdge(page, author) + { + public Author Author => Node; + } + } + """).MatchMarkdownAsync(); + } + + [Fact] + public async Task GenerateSource_ConnectionFlags() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + using HotChocolate.Types.Pagination; + + namespace TestNamespace + { + public sealed class Author + { + public int Id { get; set; } + public string Name { get; set; } + } + } + + namespace TestNamespace.Types.Root + { + [QueryType] + public static partial class AuthorQueries + { + public static Task GetAuthorsAsync( + GreenDonut.Data.PagingArguments pagingArgs, + ConnectionFlags flags, + CancellationToken cancellationToken) + => default!; + } + } + + namespace TestNamespace + { + public class AuthorConnection : ConnectionBase + { + public override IReadOnlyList? Edges => default!; + + public IReadOnlyList Nodes => default!; + + public override ConnectionPageInfo PageInfo => default!; + + public int TotalCount => 0; + } + + public class AuthorEdge(GreenDonut.Data.Page page, Author author) : PageEdge(page, author) + { + public Author Author => Node; + } + } + """).MatchMarkdownAsync(); + } } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/TestHelper.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/TestHelper.cs index b71fbb69d02..23f4f752747 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/TestHelper.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/TestHelper.cs @@ -36,6 +36,7 @@ public static Snapshot GetGeneratedSourceSnapshot(string[] sourceTexts, string? // HotChocolate.Types MetadataReference.CreateFromFile(typeof(ObjectTypeAttribute).Assembly.Location), MetadataReference.CreateFromFile(typeof(Connection).Assembly.Location), + MetadataReference.CreateFromFile(typeof(PageConnection<>).Assembly.Location), // HotChocolate.Abstractions MetadataReference.CreateFromFile(typeof(ParentAttribute).Assembly.Location), diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Root_Empty.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Root_Empty.md new file mode 100644 index 00000000000..a5a8f7f4d7f --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Root_Empty.md @@ -0,0 +1,75 @@ +# Root_Empty + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Query", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.Query.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + return builder; + } + } +} + +``` + +## Query.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public static partial class Query + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + } + } +} + + +``` + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionFlags.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionFlags.md new file mode 100644 index 00000000000..15c02963080 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionFlags.md @@ -0,0 +1,359 @@ +# GenerateSource_ConnectionFlags + +## AuthorConnectionType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorConnectionType : ObjectType + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorConnection); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor.Name("AuthorConnection"); + + descriptor + .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .Type>>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionEdgesFieldFlags(); + c.Definition.Resolvers = r.Edges(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Nodes", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionNodesFieldFlags(); + c.Definition.Resolvers = r.Nodes(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("PageInfo", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.PageInfo(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("TotalCount", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionTotalCountFieldFlags(); + c.Definition.Resolvers = r.TotalCount(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Edges() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Edges); + + private global::System.Object? Edges(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Edges; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Nodes() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Nodes); + + private global::System.Object? Nodes(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Nodes; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates PageInfo() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: PageInfo); + + private global::System.Object? PageInfo(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().PageInfo; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates TotalCount() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TotalCount); + + private global::System.Object? TotalCount(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().TotalCount; + return result; + } + } + } +} + + +``` + +## AuthorEdgeType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorEdgeType : ObjectType + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorEdge); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor.Name("AuthorEdge"); + + descriptor + .Field(thisType.GetMember("Author", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Author(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Node(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Cursor", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Cursor(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Author() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Author); + + private global::System.Object? Author(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Author; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Node() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Node); + + private global::System.Object? Node(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Node; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Cursor() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Cursor); + + private global::System.Object? Cursor(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Cursor; + return result; + } + } + } +} + + +``` + +## AuthorQueries.oWmb2tjg9NMLpwHX801V4w.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace.Types.Root +{ + public static partial class AuthorQueries + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.Types.Root.AuthorQueries); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetAuthorsAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthorsAsync(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorsAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorsAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_first = context.ArgumentValue("first"); + var args0_after = context.ArgumentValue("after"); + int? args0_last = null; + string? args0_before = null; + bool args0_includeTotalCount = false; + + if(args0_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args0_last = context.ArgumentValue("last"); + args0_before = context.ArgumentValue("before"); + } + + if(args0_first is null && args0_last is null) + { + args0_first = args0_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args0_includeTotalCount = context.IsSelected("totalCount"); + } + + var args0 = new global::GreenDonut.Data.PagingArguments( + args0_first, + args0_after, + args0_last, + args0_before, + args0_includeTotalCount) + { + EnableRelativeCursors = args0_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args1 = global::HotChocolate.Types.Paging.ConnectionFlags.Nothing; + + if(context.IsSelected("edges")) + { + args1 |= global::HotChocolate.Types.Paging.ConnectionFlags.Edges; + } + + if(context.IsSelected("nodes")) + { + args1 |= global::HotChocolate.Types.Paging.ConnectionFlags.Nodes; + } + + if(context.IsSelected("totalCount")) + { + args1 |= global::HotChocolate.Types.Paging.ConnectionFlags.TotalCount; + } + var args2 = context.RequestAborted; + var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1, args2); + return result; + } + } + } +} + + +``` + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.AddType(); + builder.AddType(); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Types.Root.AuthorQueries", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.Types.Root.AuthorQueries.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + return builder; + } + } +} + +``` + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_MatchesSnapshot.md index e2e0461b301..190a93c6993 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_MatchesSnapshot.md @@ -27,6 +27,8 @@ namespace TestNamespace var bindingResolver = extend.Context.ParameterBindingResolver; var resolvers = new __Resolvers(); + descriptor.Name("AuthorConnection"); + descriptor .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) .Type>>>() @@ -139,6 +141,8 @@ namespace TestNamespace var bindingResolver = extend.Context.ParameterBindingResolver; var resolvers = new __Resolvers(); + descriptor.Name("AuthorEdge"); + descriptor .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) .ExtendWith(static (c, r) => diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_No_Duplicates_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_No_Duplicates_MatchesSnapshot.md index e4dd560bde6..7ba514c9f30 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_No_Duplicates_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_No_Duplicates_MatchesSnapshot.md @@ -27,6 +27,8 @@ namespace TestNamespace var bindingResolver = extend.Context.ParameterBindingResolver; var resolvers = new __Resolvers(); + descriptor.Name("AuthorConnection"); + descriptor .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) .Type>>>() @@ -139,6 +141,8 @@ namespace TestNamespace var bindingResolver = extend.Context.ParameterBindingResolver; var resolvers = new __Resolvers(); + descriptor.Name("AuthorEdge"); + descriptor .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) .ExtendWith(static (c, r) => diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_IncludeTotalCount_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_IncludeTotalCount_MatchesSnapshot.md index 074bbb19559..70600196f6a 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_IncludeTotalCount_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_IncludeTotalCount_MatchesSnapshot.md @@ -27,6 +27,8 @@ namespace TestNamespace var bindingResolver = extend.Context.ParameterBindingResolver; var resolvers = new __Resolvers(); + descriptor.Name("AuthorConnection"); + descriptor .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) .Type>>>() @@ -139,6 +141,8 @@ namespace TestNamespace var bindingResolver = extend.Context.ParameterBindingResolver; var resolvers = new __Resolvers(); + descriptor.Name("AuthorEdge"); + descriptor .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) .ExtendWith(static (c, r) => diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Inherit_PageEdge.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Inherit_PageEdge.md new file mode 100644 index 00000000000..ac24a78cc7d --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Inherit_PageEdge.md @@ -0,0 +1,343 @@ +# GenerateSource_Inherit_From_ConnectionBase_Inherit_PageEdge + +## AuthorConnectionType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorConnectionType : ObjectType + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorConnection); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor.Name("AuthorConnection"); + + descriptor + .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .Type>>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionEdgesFieldFlags(); + c.Definition.Resolvers = r.Edges(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Nodes", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionNodesFieldFlags(); + c.Definition.Resolvers = r.Nodes(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("PageInfo", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.PageInfo(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("TotalCount", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionTotalCountFieldFlags(); + c.Definition.Resolvers = r.TotalCount(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Edges() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Edges); + + private global::System.Object? Edges(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Edges; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Nodes() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Nodes); + + private global::System.Object? Nodes(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Nodes; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates PageInfo() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: PageInfo); + + private global::System.Object? PageInfo(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().PageInfo; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates TotalCount() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TotalCount); + + private global::System.Object? TotalCount(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().TotalCount; + return result; + } + } + } +} + + +``` + +## AuthorEdgeType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorEdgeType : ObjectType + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorEdge); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor.Name("AuthorEdge"); + + descriptor + .Field(thisType.GetMember("Author", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Author(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Node(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Cursor", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Cursor(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Author() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Author); + + private global::System.Object? Author(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Author; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Node() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Node); + + private global::System.Object? Node(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Node; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Cursor() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Cursor); + + private global::System.Object? Cursor(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Cursor; + return result; + } + } + } +} + + +``` + +## AuthorQueries.oWmb2tjg9NMLpwHX801V4w.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace.Types.Root +{ + public static partial class AuthorQueries + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.Types.Root.AuthorQueries); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetAuthorsAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthorsAsync(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorsAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorsAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_first = context.ArgumentValue("first"); + var args0_after = context.ArgumentValue("after"); + int? args0_last = null; + string? args0_before = null; + bool args0_includeTotalCount = false; + + if(args0_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args0_last = context.ArgumentValue("last"); + args0_before = context.ArgumentValue("before"); + } + + if(args0_first is null && args0_last is null) + { + args0_first = args0_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args0_includeTotalCount = context.IsSelected("totalCount"); + } + + var args0 = new global::GreenDonut.Data.PagingArguments( + args0_first, + args0_after, + args0_last, + args0_before, + args0_includeTotalCount) + { + EnableRelativeCursors = args0_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args1 = context.RequestAborted; + var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1); + return result; + } + } + } +} + + +``` + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.AddType(); + builder.AddType(); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Types.Root.AuthorQueries", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.Types.Root.AuthorQueries.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + return builder; + } + } +} + +``` + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge.md new file mode 100644 index 00000000000..22624e5dd4d --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge.md @@ -0,0 +1,325 @@ +# GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge + +## AuthorConnectionType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorConnectionType : ObjectType + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorConnection); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor.Name("AuthorConnection"); + + descriptor + .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .Type>>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionEdgesFieldFlags(); + c.Definition.Resolvers = r.Edges(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Nodes", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionNodesFieldFlags(); + c.Definition.Resolvers = r.Nodes(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("PageInfo", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.PageInfo(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("TotalCount", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionTotalCountFieldFlags(); + c.Definition.Resolvers = r.TotalCount(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Edges() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Edges); + + private global::System.Object? Edges(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Edges; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Nodes() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Nodes); + + private global::System.Object? Nodes(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().Nodes; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates PageInfo() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: PageInfo); + + private global::System.Object? PageInfo(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().PageInfo; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates TotalCount() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TotalCount); + + private global::System.Object? TotalCount(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().TotalCount; + return result; + } + } + } +} + + +``` + +## AuthorEdgeType.GMN8-ZMy56jZWutjKaKAXQ.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace HotChocolate.Types.Pagination +{ + public partial class AuthorEdgeType : ObjectType> + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor> descriptor) + { + var thisType = typeof(global::HotChocolate.Types.Pagination.PageEdge); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor.Name("AuthorEdge"); + + descriptor + .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Node(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Cursor", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Cursor(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Node() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Node); + + private global::System.Object? Node(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Node; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Cursor() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Cursor); + + private global::System.Object? Cursor(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Cursor; + return result; + } + } + } +} + + +``` + +## AuthorQueries.oWmb2tjg9NMLpwHX801V4w.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace.Types.Root +{ + public static partial class AuthorQueries + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.Types.Root.AuthorQueries); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetAuthorsAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthorsAsync(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorsAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorsAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_first = context.ArgumentValue("first"); + var args0_after = context.ArgumentValue("after"); + int? args0_last = null; + string? args0_before = null; + bool args0_includeTotalCount = false; + + if(args0_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args0_last = context.ArgumentValue("last"); + args0_before = context.ArgumentValue("before"); + } + + if(args0_first is null && args0_last is null) + { + args0_first = args0_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args0_includeTotalCount = context.IsSelected("totalCount"); + } + + var args0 = new global::GreenDonut.Data.PagingArguments( + args0_first, + args0_after, + args0_last, + args0_before, + args0_includeTotalCount) + { + EnableRelativeCursors = args0_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args1 = context.RequestAborted; + var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1); + return result; + } + } + } +} + + +``` + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.AddType(); + builder.AddType(); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Types.Root.AuthorQueries", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.Types.Root.AuthorQueries.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + return builder; + } + } +} + +``` + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge_Generic.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge_Generic.md new file mode 100644 index 00000000000..72992eaab12 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge_Generic.md @@ -0,0 +1,331 @@ +# GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge_Generic + +## AuthorCustomConnectionType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorCustomConnectionType : ObjectType> + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor> descriptor) + { + var thisType = typeof(global::TestNamespace.CustomConnection); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + var nodeTypeRef = extend.Context.TypeInspector.GetTypeRef(typeof(global::TestNamespace.Author)); + descriptor + .Name(t => string.Format("{0}CustomConnection", t.Name)) + .DependsOn(nodeTypeRef); + + descriptor + .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .Type>>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionEdgesFieldFlags(); + c.Definition.Resolvers = r.Edges(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Nodes", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionNodesFieldFlags(); + c.Definition.Resolvers = r.Nodes(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("PageInfo", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.PageInfo(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("TotalCount", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionTotalCountFieldFlags(); + c.Definition.Resolvers = r.TotalCount(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Edges() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Edges); + + private global::System.Object? Edges(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Edges; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Nodes() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Nodes); + + private global::System.Object? Nodes(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Nodes; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates PageInfo() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: PageInfo); + + private global::System.Object? PageInfo(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().PageInfo; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates TotalCount() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TotalCount); + + private global::System.Object? TotalCount(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().TotalCount; + return result; + } + } + } +} + + +``` + +## AuthorEdgeType.GMN8-ZMy56jZWutjKaKAXQ.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace HotChocolate.Types.Pagination +{ + public partial class AuthorEdgeType : ObjectType> + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor> descriptor) + { + var thisType = typeof(global::HotChocolate.Types.Pagination.PageEdge); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + var nodeTypeRef = extend.Context.TypeInspector.GetTypeRef(typeof(global::TestNamespace.Author)); + descriptor + .Name(t => string.Format("{0}Edge", t.Name)) + .DependsOn(nodeTypeRef); + + descriptor + .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Node(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Cursor", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Cursor(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Node() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Node); + + private global::System.Object? Node(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Node; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Cursor() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Cursor); + + private global::System.Object? Cursor(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Cursor; + return result; + } + } + } +} + + +``` + +## AuthorQueries.oWmb2tjg9NMLpwHX801V4w.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace.Types.Root +{ + public static partial class AuthorQueries + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.Types.Root.AuthorQueries); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetAuthorsAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthorsAsync(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorsAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorsAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_first = context.ArgumentValue("first"); + var args0_after = context.ArgumentValue("after"); + int? args0_last = null; + string? args0_before = null; + bool args0_includeTotalCount = false; + + if(args0_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args0_last = context.ArgumentValue("last"); + args0_before = context.ArgumentValue("before"); + } + + if(args0_first is null && args0_last is null) + { + args0_first = args0_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args0_includeTotalCount = context.IsSelected("totalCount"); + } + + var args0 = new global::GreenDonut.Data.PagingArguments( + args0_first, + args0_after, + args0_last, + args0_before, + args0_includeTotalCount) + { + EnableRelativeCursors = args0_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args1 = context.RequestAborted; + var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1); + return result; + } + } + } +} + + +``` + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.AddType(); + builder.AddType(); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Types.Root.AuthorQueries", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.Types.Root.AuthorQueries.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + return builder; + } + } +} + +``` + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_PageConnection.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_PageConnection.md new file mode 100644 index 00000000000..c9b9afaba7c --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_PageConnection.md @@ -0,0 +1,345 @@ +# GenerateSource_Inherit_From_PageConnection + +## AuthorConnectionType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public partial class AuthorConnectionType : ObjectType + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.AuthorConnection); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor.Name("AuthorConnection"); + + descriptor + .Field(thisType.GetMember("CustomResolver", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.CustomResolver(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Edges", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .Type>>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionEdgesFieldFlags(); + c.Definition.Resolvers = r.Edges(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Nodes", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionNodesFieldFlags(); + c.Definition.Resolvers = r.Nodes(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("PageInfo", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.PageInfo(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("TotalCount", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionTotalCountFieldFlags(); + c.Definition.Resolvers = r.TotalCount(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates CustomResolver() + { + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: CustomResolver); + } + + private global::System.Object? CustomResolver(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent().CustomResolver(); + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Edges() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Edges); + + private global::System.Object? Edges(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Edges; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Nodes() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Nodes); + + private global::System.Object? Nodes(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Nodes; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates PageInfo() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: PageInfo); + + private global::System.Object? PageInfo(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().PageInfo; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates TotalCount() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: TotalCount); + + private global::System.Object? TotalCount(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().TotalCount; + return result; + } + } + } +} + + +``` + +## AuthorEdgeType.GMN8-ZMy56jZWutjKaKAXQ.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace HotChocolate.Types.Pagination +{ + public partial class AuthorEdgeType : ObjectType> + { + protected override void Configure(global::HotChocolate.Types.IObjectTypeDescriptor> descriptor) + { + var thisType = typeof(global::HotChocolate.Types.Pagination.PageEdge); + var extend = descriptor.Extend(); + var bindingResolver = extend.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor.Name("AuthorEdge"); + + descriptor + .Field(thisType.GetMember("Node", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Node(); + }, + resolvers); + + descriptor + .Field(thisType.GetMember("Cursor", global::HotChocolate.Utilities.ReflectionUtils.InstanceMemberFlags)[0]) + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.Resolvers = r.Cursor(); + }, + resolvers); + } + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates Node() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Node); + + private global::System.Object? Node(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Node; + return result; + } + + public HotChocolate.Resolvers.FieldResolverDelegates Cursor() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: Cursor); + + private global::System.Object? Cursor(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = context.Parent>().Cursor; + return result; + } + } + } +} + + +``` + +## AuthorQueries.oWmb2tjg9NMLpwHX801V4w.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace.Types.Root +{ + public static partial class AuthorQueries + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var thisType = typeof(global::TestNamespace.Types.Root.AuthorQueries); + var bindingResolver = descriptor.Extend().Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + descriptor + .Field(thisType.GetMember("GetAuthorsAsync", global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags)[0]) + .AddPagingArguments() + .Type>() + .ExtendWith(static (c, r) => + { + c.Definition.SetSourceGeneratorFlags(); + c.Definition.SetConnectionFlags(); + var pagingOptions = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(c.Context, null); + c.Definition.State = c.Definition.State.SetItem(HotChocolate.WellKnownContextData.PagingOptions, pagingOptions); + c.Definition.ContextData[HotChocolate.WellKnownContextData.PagingOptions] = pagingOptions; + c.Definition.Resolvers = r.GetAuthorsAsync(); + }, + resolvers); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetAuthorsAsync() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: GetAuthorsAsync); + + private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_first = context.ArgumentValue("first"); + var args0_after = context.ArgumentValue("after"); + int? args0_last = null; + string? args0_before = null; + bool args0_includeTotalCount = false; + + if(args0_options.AllowBackwardPagination ?? global::HotChocolate.Types.Pagination.PagingDefaults.AllowBackwardPagination) + { + args0_last = context.ArgumentValue("last"); + args0_before = context.ArgumentValue("before"); + } + + if(args0_first is null && args0_last is null) + { + args0_first = args0_options.DefaultPageSize ?? global::HotChocolate.Types.Pagination.PagingDefaults.DefaultPageSize; + } + + if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) + { + args0_includeTotalCount = context.IsSelected("totalCount"); + } + + var args0 = new global::GreenDonut.Data.PagingArguments( + args0_first, + args0_after, + args0_last, + args0_before, + args0_includeTotalCount) + { + EnableRelativeCursors = args0_options.EnableRelativeCursors + ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + }; + var args1 = context.RequestAborted; + var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1); + return result; + } + } + } +} + + +``` + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.AddType(); + builder.AddType(); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Types.Root.AuthorQueries", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.Types.Root.AuthorQueries.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + return builder; + } + } +} + +``` + diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql index 1285943b65e..eea1d2ce940 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql @@ -107,7 +107,7 @@ type Product implements Node { "A connection to a list of items." type ProductConnection { "A list of edges." - edges: [ProductsEdge!] + edges: [ProductEdge!] "A flattened list of the nodes." nodes: [Product!] "Information to aid in pagination." @@ -117,20 +117,20 @@ type ProductConnection { endCursors(count: Int!): [String!]! } -type ProductType { - id: Int! - name: String! - products: [Product!]! -} - "An edge in a connection." -type ProductsEdge { +type ProductEdge { "The item at the end of the edge." node: Product! "A cursor for use in pagination." cursor: String! } +type ProductType { + id: Int! + name: String! + products: [Product!]! +} + type Query { "Fetches an object given its ID." node("ID of the object." id: ID!): Node @cost(weight: "10") From 4be35be90b6d985b79d9bfd9961e1c88ed716974 Mon Sep 17 00:00:00 2001 From: Tobias Tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 18 Mar 2025 22:15:59 +0100 Subject: [PATCH 58/64] Make TypeInterceptor.IsEnabled public (#8149) --- .../Core/src/Fetching/DataLoaderRootFieldTypeInterceptor.cs | 2 +- .../Core/src/Types/Configuration/TypeInterceptor.cs | 2 +- .../Core/src/Types/SemanticNonNullTypeInterceptor.cs | 2 +- .../CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/HotChocolate/Core/src/Fetching/DataLoaderRootFieldTypeInterceptor.cs b/src/HotChocolate/Core/src/Fetching/DataLoaderRootFieldTypeInterceptor.cs index 15775ab300c..f2a5c417b97 100644 --- a/src/HotChocolate/Core/src/Fetching/DataLoaderRootFieldTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Fetching/DataLoaderRootFieldTypeInterceptor.cs @@ -26,7 +26,7 @@ internal override void InitializeContext( _services = context.Services.GetService(); } - internal override bool IsEnabled(IDescriptorContext context) + public override bool IsEnabled(IDescriptorContext context) => context.Options.PublishRootFieldPagesToPromiseCache; public override void OnAfterResolveRootType( diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs index bc815f5b1ea..be1d108b089 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs @@ -22,7 +22,7 @@ public abstract class TypeInterceptor /// internal virtual uint Position => _defaultPosition; - internal virtual bool IsEnabled(IDescriptorContext context) => true; + public virtual bool IsEnabled(IDescriptorContext context) => true; internal virtual bool IsMutationAggregator(IDescriptorContext context) => false; diff --git a/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs index d7f93bb0924..79bcdf72ff9 100644 --- a/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/SemanticNonNullTypeInterceptor.cs @@ -16,7 +16,7 @@ internal sealed class SemanticNonNullTypeInterceptor : TypeInterceptor { private ITypeInspector _typeInspector = null!; - internal override bool IsEnabled(IDescriptorContext context) + public override bool IsEnabled(IDescriptorContext context) => context.Options.EnableSemanticNonNull; internal override void InitializeContext( diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs index 9407e943cdf..16db0975271 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/CostTypeInterceptor.cs @@ -32,7 +32,7 @@ private readonly ImmutableArray _offsetSizedFields internal override uint Position => int.MaxValue; - internal override bool IsEnabled(IDescriptorContext context) + public override bool IsEnabled(IDescriptorContext context) => context.Services.GetRequiredService().ApplyCostDefaults; internal override void InitializeContext( From d3cb1e2815dcc7fa5dc7b46e56a991d282c6021b Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 19 Mar 2025 09:55:15 +0100 Subject: [PATCH 59/64] Reduce totalCount requests when using relative cursors. (#8150) --- .../Expressions/ExpressionHelpers.cs | 2 +- .../Extensions/PagingQueryableExtensions.cs | 7 +- .../FileBuilders/TypeFileBuilderBase.cs | 65 +++---------- .../src/Types.Analyzers/WellKnownTypes.cs | 3 +- .../Extensions/UseConnectionAttribute.cs | 8 +- .../DefaultParameterBindingResolver.cs | 3 + .../Resolvers/DefaultResolverCompiler.cs | 1 + ...nnectionFlagsParameterExpressionBuilder.cs | 2 +- .../IsSelectedParameterExpressionBuilder.cs | 96 +++---------------- .../src/Types/Resolvers/IsSelectedContext.cs | 37 +++++++ .../src/Types/Resolvers/IsSelectedVisitor.cs | 64 +++++++++++++ .../Types/Types/Pagination/ConnectionFlags.cs | 9 +- .../Types/Pagination/ConnectionFlagsHelper.cs | 89 +++++++++++++++++ .../Types/Types/Pagination/PagingOptions.cs | 12 ++- ...ingTests.GenerateSource_ConnectionFlags.md | 23 +---- ...erateSource_ConnectionT_MatchesSnapshot.md | 6 +- ...Source_CustomConnection_MatchesSnapshot.md | 6 +- ...onnection_No_Duplicates_MatchesSnapshot.md | 18 ++-- ...nnection_ConnectionName_MatchesSnapshot.md | 6 +- ...ction_IncludeTotalCount_MatchesSnapshot.md | 6 +- ...GenericCustomConnection_MatchesSnapshot.md | 18 ++-- ...tion_WithConnectionName_MatchesSnapshot.md | 18 ++-- ...it_From_ConnectionBase_Inherit_PageEdge.md | 6 +- ...erit_From_ConnectionBase_Reuse_PageEdge.md | 6 +- ...m_ConnectionBase_Reuse_PageEdge_Generic.md | 6 +- ...erateSource_Inherit_From_PageConnection.md | 6 +- .../Data.PostgreSQL.Tests/IntegrationTests.cs | 3 +- .../Types/Brands/BrandNode.cs | 3 + ...ery_Brands_First_2_And_Products_First_2.md | 10 -- ..._First_2_And_Products_First_2_Name_Desc.md | 10 -- ...d_Products_First_2_Name_Desc_Brand_Name.md | 12 +-- ...s_First_2_Name_Desc_Brand_Name__net_8_0.md | 12 +-- ...And_Products_First_2_Name_Desc__net_8_0.md | 10 -- ...s_First_2_And_Products_First_2__net_8_0.md | 10 -- ...nTests.Query_Products_First_2_And_Brand.md | 4 +- ...ery_Products_First_2_And_Brand__net_8_0.md | 3 +- 36 files changed, 315 insertions(+), 285 deletions(-) create mode 100644 src/HotChocolate/Core/src/Types/Resolvers/IsSelectedContext.cs create mode 100644 src/HotChocolate/Core/src/Types/Resolvers/IsSelectedVisitor.cs create mode 100644 src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs diff --git a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs index bf4b9914265..e037e1ed18d 100644 --- a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs +++ b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs @@ -220,7 +220,7 @@ public static BatchExpression BuildBatchExpression( requestedCount = arguments.Last.Value; } - if (arguments.EnableRelativeCursors && cursor?.IsRelative == true) + if (cursor?.IsRelative == true) { if ((arguments.Last is not null && cursor.Offset > 0) || (arguments.First is not null && cursor.Offset < 0)) { diff --git a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs index 8644a5f5148..706028694d1 100644 --- a/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs +++ b/src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs @@ -100,6 +100,8 @@ public static async ValueTask> ToPageAsync( arguments = arguments with { First = 10 }; } + // if relative cursors are enabled and no cursor is provided + // we must do an initial count of the dataset. if (arguments.EnableRelativeCursors && string.IsNullOrEmpty(arguments.After) && string.IsNullOrEmpty(arguments.Before)) @@ -153,9 +155,10 @@ public static async ValueTask> ToPageAsync( } } - if (arguments.EnableRelativeCursors && cursor?.IsRelative == true) + if (cursor?.IsRelative == true) { - if ((arguments.Last is not null && cursor.Offset > 0) || (arguments.First is not null && cursor.Offset < 0)) + if ((arguments.Last is not null && cursor.Offset > 0) + || (arguments.First is not null && cursor.Offset < 0)) { throw new ArgumentException( "Positive offsets are not allowed with `last`, and negative offsets are not allowed with `first`.", diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs index b9feeeb90d9..2dca6346561 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs @@ -880,6 +880,10 @@ private void WriteResolverArguments(Resolver resolver, IMethodSymbol resolverMet "var args{0}_options = global::{1}.GetPagingOptions(context.Schema, context.Selection.Field);", i, WellKnownTypes.PagingHelper); + Writer.WriteIndentedLine( + "var args{0}_flags = global::{1}.GetConnectionFlags(context);", + i, + WellKnownTypes.ConnectionFlagsHelper); Writer.WriteIndentedLine("var args{0}_first = context.ArgumentValue(\"first\");", i); Writer.WriteIndentedLine("var args{0}_after = context.ArgumentValue(\"after\");", i); Writer.WriteIndentedLine("int? args{0}_last = null;", i); @@ -920,7 +924,10 @@ private void WriteResolverArguments(Resolver resolver, IMethodSymbol resolverMet Writer.WriteIndentedLine("{"); using (Writer.IncreaseIndent()) { - Writer.WriteIndentedLine("args{0}_includeTotalCount = context.IsSelected(\"totalCount\");", i); + Writer.WriteIndentedLine( + "args{0}_includeTotalCount = args{0}_flags.HasFlag(global::{1}.TotalCount);", + i, + WellKnownTypes.ConnectionFlags); } Writer.WriteIndentedLine("}"); @@ -940,14 +947,9 @@ private void WriteResolverArguments(Resolver resolver, IMethodSymbol resolverMet using (Writer.IncreaseIndent()) { Writer.WriteIndentedLine( - "EnableRelativeCursors = args{0}_options.EnableRelativeCursors", - i); - using (Writer.IncreaseIndent()) - { - Writer.WriteIndentedLine( - "?? global::{0}.EnableRelativeCursors", - WellKnownTypes.PagingDefaults); - } + "EnableRelativeCursors = args{0}_flags.HasFlag(global::{1}.RelativeCursor)", + i, + WellKnownTypes.ConnectionFlags); } Writer.WriteIndentedLine("};"); @@ -956,48 +958,9 @@ private void WriteResolverArguments(Resolver resolver, IMethodSymbol resolverMet case ResolverParameterKind.ConnectionFlags: Writer.WriteIndentedLine( - "var args{0} = global::{1}.Nothing;", + "var args{0} = global::{1}.GetConnectionFlags(context);", i, - WellKnownTypes.ConnectionFlags); - Writer.WriteLine(); - - Writer.WriteIndentedLine( - "if(context.IsSelected(\"edges\"))"); - Writer.WriteIndentedLine("{"); - using (Writer.IncreaseIndent()) - { - Writer.WriteIndentedLine( - "args{0} |= global::{1}.Edges;", - i, - WellKnownTypes.ConnectionFlags); - } - Writer.WriteIndentedLine("}"); - Writer.WriteLine(); - - Writer.WriteIndentedLine( - "if(context.IsSelected(\"nodes\"))"); - Writer.WriteIndentedLine("{"); - using (Writer.IncreaseIndent()) - { - Writer.WriteIndentedLine( - "args{0} |= global::{1}.Nodes;", - i, - WellKnownTypes.ConnectionFlags); - } - Writer.WriteIndentedLine("}"); - Writer.WriteLine(); - - Writer.WriteIndentedLine( - "if(context.IsSelected(\"totalCount\"))"); - Writer.WriteIndentedLine("{"); - using (Writer.IncreaseIndent()) - { - Writer.WriteIndentedLine( - "args{0} |= global::{1}.TotalCount;", - i, - WellKnownTypes.ConnectionFlags); - } - Writer.WriteIndentedLine("}"); + WellKnownTypes.ConnectionFlagsHelper); break; case ResolverParameterKind.Unknown: @@ -1007,7 +970,7 @@ private void WriteResolverArguments(Resolver resolver, IMethodSymbol resolverMet resolver.Member.Name, bindingIndex++, ToFullyQualifiedString(parameter.Type, resolverMethod, typeLookup)); - break; + break; default: throw new ArgumentOutOfRangeException(); diff --git a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs index 850be96787d..3b2c4b12319 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs @@ -93,7 +93,8 @@ public static class WellKnownTypes public const string ObjectTypeDefinition = "HotChocolate.Types.Descriptors.Definitions.ObjectTypeDefinition"; public const string NonNullType = "HotChocolate.Types.NonNullType"; public const string ListType = "HotChocolate.Types.ListType"; - public const string ConnectionFlags = "HotChocolate.Types.Paging.ConnectionFlags"; + public const string ConnectionFlags = "HotChocolate.Types.Pagination.ConnectionFlags"; + public const string ConnectionFlagsHelper = "HotChocolate.Types.Pagination.ConnectionFlagsHelper"; public static HashSet TypeClass { get; } = [ diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UseConnectionAttribute.cs b/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UseConnectionAttribute.cs index 626ff2d86e4..a319cc8f1a1 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UseConnectionAttribute.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UseConnectionAttribute.cs @@ -20,7 +20,7 @@ public sealed class UseConnectionAttribute : DescriptorAttribute private bool? _allowBackwardPagination; private bool? _requirePagingBoundaries; private bool? _inferConnectionNameFromField; - private bool? _EnableRelativeCursors; + private bool? _enableRelativeCursors; /// /// Overrides the global paging options for the annotated field. @@ -94,8 +94,8 @@ public bool InferConnectionNameFromField /// public bool EnableRelativeCursors { - get => _EnableRelativeCursors ?? PagingDefaults.EnableRelativeCursors; - set => _EnableRelativeCursors = value; + get => _enableRelativeCursors ?? PagingDefaults.EnableRelativeCursors; + set => _enableRelativeCursors = value; } public string? Name { get; set; } @@ -119,7 +119,7 @@ protected internal override void TryConfigure( RequirePagingBoundaries = _requirePagingBoundaries, InferConnectionNameFromField = _inferConnectionNameFromField, ProviderName = Name, - EnableRelativeCursors = _EnableRelativeCursors, + EnableRelativeCursors = _enableRelativeCursors }; if (descriptor is IObjectFieldDescriptor fieldDesc) diff --git a/src/HotChocolate/Core/src/Types/Resolvers/DefaultParameterBindingResolver.cs b/src/HotChocolate/Core/src/Types/Resolvers/DefaultParameterBindingResolver.cs index 88047721c2a..fbbf1502692 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/DefaultParameterBindingResolver.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/DefaultParameterBindingResolver.cs @@ -17,6 +17,7 @@ public DefaultParameterBindingResolver( { var serviceInspector = applicationServices.GetService(); + // explicit internal expression builders will be added first. var bindingFactories = new List { new ParentParameterExpressionBuilder(), @@ -31,6 +32,8 @@ public DefaultParameterBindingResolver( if (customBindingFactories is not null) { + // then we will add custom parameter expression builder and + // give the user a chance to override our implicit expression builder. bindingFactories.AddRange( customBindingFactories .Where(t => !t.IsDefaultHandler) diff --git a/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs b/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs index 13613dda36c..a2bd5413f8e 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs @@ -91,6 +91,7 @@ public DefaultResolverCompiler( expressionBuilders.Add(new FieldParameterExpressionBuilder()); expressionBuilders.Add(new ClaimsPrincipalParameterExpressionBuilder()); expressionBuilders.Add(new PathParameterExpressionBuilder()); + expressionBuilders.Add(new ConnectionFlagsParameterExpressionBuilder()); if (serviceInspector is not null) { diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ConnectionFlagsParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ConnectionFlagsParameterExpressionBuilder.cs index 1b4374d851b..d23650226c2 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ConnectionFlagsParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ConnectionFlagsParameterExpressionBuilder.cs @@ -35,7 +35,7 @@ public T Execute(IResolverContext context) private static ConnectionFlags Execute(IResolverContext context) { - var flags = ConnectionFlags.Nothing; + var flags = ConnectionFlags.None; if (context.IsSelected("totalCount")) { diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/IsSelectedParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/IsSelectedParameterExpressionBuilder.cs index 10530ed43d1..25f1332dd07 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/IsSelectedParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/IsSelectedParameterExpressionBuilder.cs @@ -3,8 +3,6 @@ using System.Linq.Expressions; using System.Reflection; using HotChocolate.Internal; -using HotChocolate.Language; -using HotChocolate.Language.Visitors; using HotChocolate.Types; using HotChocolate.Types.Attributes; using HotChocolate.Types.Descriptors; @@ -46,18 +44,19 @@ public void ApplyConfiguration(ParameterInfo parameter, ObjectFieldDescriptor de var definition = descriptor.Extend().Definition; definition.Configurations.Add( new CompleteConfiguration((ctx, def) => - { - if(!ctx.DescriptorContext.ContextData.TryGetValue(WellKnownContextData.PatternValidationTasks, out var value)) { - value = new List(); - ctx.DescriptorContext.ContextData[WellKnownContextData.PatternValidationTasks] = value; - } - - var patterns = (List)value!; - patterns.Add(new IsSelectedPattern((ObjectType)ctx.Type, def.Name, attribute.Fields)); - }, - definition, - ApplyConfigurationOn.AfterCompletion)); + if (!ctx.DescriptorContext.ContextData.TryGetValue(WellKnownContextData.PatternValidationTasks, + out var value)) + { + value = new List(); + ctx.DescriptorContext.ContextData[WellKnownContextData.PatternValidationTasks] = value; + } + + var patterns = (List)value!; + patterns.Add(new IsSelectedPattern((ObjectType)ctx.Type, def.Name, attribute.Fields)); + }, + definition, + ApplyConfigurationOn.AfterCompletion)); descriptor.Use( next => async ctx => @@ -133,77 +132,6 @@ public void ApplyConfiguration(ParameterInfo parameter, ObjectFieldDescriptor de } } - private sealed class IsSelectedVisitor : SyntaxWalker - { - protected override ISyntaxVisitorAction Enter(FieldNode node, IsSelectedContext context) - { - var selections = context.Selections.Peek(); - var responseName = node.Alias?.Value ?? node.Name.Value; - - if (!selections.IsSelected(responseName)) - { - context.AllSelected = false; - return Break; - } - - if (node.SelectionSet is not null) - { - context.Selections.Push(selections.Select(responseName)); - } - - return base.Enter(node, context); - } - - protected override ISyntaxVisitorAction Leave(FieldNode node, IsSelectedContext context) - { - if (node.SelectionSet is not null) - { - context.Selections.Pop(); - } - - return base.Leave(node, context); - } - - protected override ISyntaxVisitorAction Enter(InlineFragmentNode node, IsSelectedContext context) - { - if (node.TypeCondition is not null) - { - var typeContext = context.Schema.GetType(node.TypeCondition.Name.Value); - var selections = context.Selections.Peek(); - context.Selections.Push(selections.Select(typeContext)); - } - - return base.Enter(node, context); - } - - protected override ISyntaxVisitorAction Leave(InlineFragmentNode node, IsSelectedContext context) - { - if (node.TypeCondition is not null) - { - context.Selections.Pop(); - } - - return base.Leave(node, context); - } - - public static IsSelectedVisitor Instance { get; } = new(); - } - - private sealed class IsSelectedContext - { - public IsSelectedContext(ISchema schema, ISelectionCollection selections) - { - Schema = schema; - Selections.Push(selections); - } - - public ISchema Schema { get; } - - public Stack Selections { get; } = new(); - - public bool AllSelected { get; set; } = true; - } - private class IsSelectedBinding(string key) : IParameterBinding { public ArgumentKind Kind => ArgumentKind.LocalState; diff --git a/src/HotChocolate/Core/src/Types/Resolvers/IsSelectedContext.cs b/src/HotChocolate/Core/src/Types/Resolvers/IsSelectedContext.cs new file mode 100644 index 00000000000..e39cc4f1494 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Resolvers/IsSelectedContext.cs @@ -0,0 +1,37 @@ +namespace HotChocolate.Resolvers; + +/// +/// Represents the context of the . +/// +public sealed class IsSelectedContext +{ + /// + /// Initializes a new instance of with the + /// + /// + /// The schema that is used to resolve the type of the selection set. + /// + /// + /// The selection set that is used to determine if a field is selected. + /// + public IsSelectedContext(ISchema schema, ISelectionCollection selections) + { + Schema = schema; + Selections.Push(selections); + } + + /// + /// Gets the schema that is used to resolve the type of the selection set. + /// + public ISchema Schema { get; } + + /// + /// Gets the selections that is used to determine if a field is selected. + /// + public Stack Selections { get; } = new(); + + /// + /// Defines if all fields of the SelectionSet are selected. + /// + public bool AllSelected { get; set; } = true; +} diff --git a/src/HotChocolate/Core/src/Types/Resolvers/IsSelectedVisitor.cs b/src/HotChocolate/Core/src/Types/Resolvers/IsSelectedVisitor.cs new file mode 100644 index 00000000000..dd24b463ef8 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Resolvers/IsSelectedVisitor.cs @@ -0,0 +1,64 @@ +using HotChocolate.Language; +using HotChocolate.Language.Visitors; +using HotChocolate.Types; + +namespace HotChocolate.Resolvers; + +/// +/// Represents a visitor that checks what fields are selected. +/// +public sealed class IsSelectedVisitor : SyntaxWalker +{ + protected override ISyntaxVisitorAction Enter(FieldNode node, IsSelectedContext context) + { + var selections = context.Selections.Peek(); + var responseName = node.Alias?.Value ?? node.Name.Value; + + if (!selections.IsSelected(responseName)) + { + context.AllSelected = false; + return Break; + } + + if (node.SelectionSet is not null) + { + context.Selections.Push(selections.Select(responseName)); + } + + return base.Enter(node, context); + } + + protected override ISyntaxVisitorAction Leave(FieldNode node, IsSelectedContext context) + { + if (node.SelectionSet is not null) + { + context.Selections.Pop(); + } + + return base.Leave(node, context); + } + + protected override ISyntaxVisitorAction Enter(InlineFragmentNode node, IsSelectedContext context) + { + if (node.TypeCondition is not null) + { + var typeContext = context.Schema.GetType(node.TypeCondition.Name.Value); + var selections = context.Selections.Peek(); + context.Selections.Push(selections.Select(typeContext)); + } + + return base.Enter(node, context); + } + + protected override ISyntaxVisitorAction Leave(InlineFragmentNode node, IsSelectedContext context) + { + if (node.TypeCondition is not null) + { + context.Selections.Pop(); + } + + return base.Leave(node, context); + } + + public static IsSelectedVisitor Instance { get; } = new(); +} diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlags.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlags.cs index 463ae00c3a4..e6b4f1a112e 100644 --- a/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlags.cs +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlags.cs @@ -9,7 +9,7 @@ public enum ConnectionFlags /// /// No flags are set. /// - Nothing = 0, + None = 0, /// /// The edges field was requested by the user. @@ -26,6 +26,11 @@ public enum ConnectionFlags /// TotalCount = 4, + /// + /// The relative cursor field was requested by the user. + /// + RelativeCursor = 8, + /// /// The nodes or edges field was requested by the user. /// @@ -34,5 +39,5 @@ public enum ConnectionFlags /// /// All fields were requested by the user. /// - All = Edges | Nodes | TotalCount + All = Edges | Nodes | TotalCount | RelativeCursor } diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs new file mode 100644 index 00000000000..4ed202fbb7e --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs @@ -0,0 +1,89 @@ +using System.Collections.Concurrent; +using HotChocolate.Language; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors.Definitions; +using static HotChocolate.Language.Utf8GraphQLParser.Syntax; + +namespace HotChocolate.Types.Pagination; + +/// +/// An internal helper to get connection flags from the current resolver context. +/// +public static class ConnectionFlagsHelper +{ + private const string _keyFormat = "HotChocolate.Types.Pagination.ConnectionFlags_{0}"; + private static readonly ConcurrentDictionary _parsedSelectionSets = new(); + + /// + /// Gets the connection flags from the current resolver context. + /// + public static ConnectionFlags GetConnectionFlags(IResolverContext context) + { + return context.Operation.GetOrAddState( + string.Format(_keyFormat, context.Selection.Id), + static (_, ctx) => + { + if(ctx.Selection.Field is ObjectField field + && !field.Flags.HasFlag(FieldFlags.Connection)) + { + return ConnectionFlags.None; + } + + var options = PagingHelper.GetPagingOptions(ctx.Schema, ctx.Selection.Field); + + var connectionFlags = ConnectionFlags.None; + + if(ctx.IsSelected("edges")) + { + connectionFlags |= ConnectionFlags.Edges; + } + + if(ctx.IsSelected("nodes")) + { + connectionFlags |= ConnectionFlags.Nodes; + } + + if(ctx.IsSelected("totalCount")) + { + connectionFlags |= ConnectionFlags.TotalCount; + } + + if ((options.EnableRelativeCursors ?? PagingDefaults.EnableRelativeCursors) + && options.RelativeCursorFields.Count > 0) + { + var startSelections = ctx.Select(); + var selectionContext = new IsSelectedContext(ctx.Schema, startSelections); + + foreach (var relativeCursor in options.RelativeCursorFields) + { + // we reset the state here so that each visitation starts fresh. + selectionContext.AllSelected = true; + selectionContext.Selections.Clear(); + selectionContext.Selections.Push(startSelections); + + // we parse the selection pattern, we in essence use + // a SelectionSetNode as a selection pattern. + var selectionPattern = ParsePattern(relativeCursor); + + // then we visit the selection and if one selection of the selection pattern + // is not hit we break the loop and do not set the relative cursor flag. + IsSelectedVisitor.Instance.Visit(selectionPattern, selectionContext); + + // if however all selections of the selection pattern are + // hit we set the relative cursor flag. + if (selectionContext.AllSelected) + { + connectionFlags |= ConnectionFlags.RelativeCursor; + break; + } + } + } + + return connectionFlags; + }, + context); + } + + private static SelectionSetNode ParsePattern(string selectionSet) + => _parsedSelectionSets.GetOrAdd(selectionSet, static s => ParseSelectionSet($"{{ {s} }}")); +} diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs index 4ce033a4891..20b321d3ce1 100644 --- a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs @@ -1,5 +1,7 @@ #nullable enable +using System.Collections.Immutable; + namespace HotChocolate.Types.Pagination; /// @@ -64,6 +66,12 @@ public class PagingOptions /// public bool? EnableRelativeCursors { get; set; } + /// + /// Gets or sets the fields that represent relative cursors. + /// + public ImmutableHashSet RelativeCursorFields { get; set; } = + ["pageInfo { forwardCursors }", "pageInfo { backwardCursors }"]; + /// /// Merges the options into this options instance wherever /// a property is not set. @@ -83,6 +91,7 @@ internal void Merge(PagingOptions other) ProviderName ??= other.ProviderName; IncludeNodesField ??= other.IncludeNodesField; EnableRelativeCursors ??= other.EnableRelativeCursors; + RelativeCursorFields = RelativeCursorFields.Union(other.RelativeCursorFields); } /// @@ -100,6 +109,7 @@ internal PagingOptions Copy() InferCollectionSegmentNameFromField = InferCollectionSegmentNameFromField, ProviderName = ProviderName, IncludeNodesField = IncludeNodesField, - EnableRelativeCursors = EnableRelativeCursors + EnableRelativeCursors = EnableRelativeCursors, + RelativeCursorFields = RelativeCursorFields }; } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionFlags.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionFlags.md index 15c02963080..6c707518423 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionFlags.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionFlags.md @@ -260,6 +260,7 @@ namespace TestNamespace.Types.Root private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) { var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args0_first = context.ArgumentValue("first"); var args0_after = context.ArgumentValue("after"); int? args0_last = null; @@ -279,7 +280,7 @@ namespace TestNamespace.Types.Root if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args0_includeTotalCount = context.IsSelected("totalCount"); + args0_includeTotalCount = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args0 = new global::GreenDonut.Data.PagingArguments( @@ -289,25 +290,9 @@ namespace TestNamespace.Types.Root args0_before, args0_includeTotalCount) { - EnableRelativeCursors = args0_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; - var args1 = global::HotChocolate.Types.Paging.ConnectionFlags.Nothing; - - if(context.IsSelected("edges")) - { - args1 |= global::HotChocolate.Types.Paging.ConnectionFlags.Edges; - } - - if(context.IsSelected("nodes")) - { - args1 |= global::HotChocolate.Types.Paging.ConnectionFlags.Nodes; - } - - if(context.IsSelected("totalCount")) - { - args1 |= global::HotChocolate.Types.Paging.ConnectionFlags.TotalCount; - } + var args1 = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args2 = context.RequestAborted; var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1, args2); return result; diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionT_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionT_MatchesSnapshot.md index 24b8468466a..5fdb3a87eec 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionT_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_ConnectionT_MatchesSnapshot.md @@ -48,6 +48,7 @@ namespace TestNamespace private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) { var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args0_first = context.ArgumentValue("first"); var args0_after = context.ArgumentValue("after"); int? args0_last = null; @@ -67,7 +68,7 @@ namespace TestNamespace if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args0_includeTotalCount = context.IsSelected("totalCount"); + args0_includeTotalCount = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args0 = new global::GreenDonut.Data.PagingArguments( @@ -77,8 +78,7 @@ namespace TestNamespace args0_before, args0_includeTotalCount) { - EnableRelativeCursors = args0_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args1 = context.RequestAborted; var result = await global::TestNamespace.BookPage.GetAuthorsAsync(args0, args1); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_MatchesSnapshot.md index 190a93c6993..ef997ccbca1 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_MatchesSnapshot.md @@ -242,6 +242,7 @@ namespace TestNamespace private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) { var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args0_first = context.ArgumentValue("first"); var args0_after = context.ArgumentValue("after"); int? args0_last = null; @@ -261,7 +262,7 @@ namespace TestNamespace if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args0_includeTotalCount = context.IsSelected("totalCount"); + args0_includeTotalCount = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args0 = new global::GreenDonut.Data.PagingArguments( @@ -271,8 +272,7 @@ namespace TestNamespace args0_before, args0_includeTotalCount) { - EnableRelativeCursors = args0_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args1 = context.RequestAborted; var result = await global::TestNamespace.AuthorQueries.GetAuthorsAsync(args0, args1); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_No_Duplicates_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_No_Duplicates_MatchesSnapshot.md index 7ba514c9f30..9d44c4c1166 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_No_Duplicates_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_No_Duplicates_MatchesSnapshot.md @@ -243,6 +243,7 @@ namespace TestNamespace.Types.Nodes { var args0 = context.Parent(); var args1_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args1_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args1_first = context.ArgumentValue("first"); var args1_after = context.ArgumentValue("after"); int? args1_last = null; @@ -262,7 +263,7 @@ namespace TestNamespace.Types.Nodes if(args1_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args1_includeTotalCount = context.IsSelected("totalCount"); + args1_includeTotalCount = args1_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args1 = new global::GreenDonut.Data.PagingArguments( @@ -272,8 +273,7 @@ namespace TestNamespace.Types.Nodes args1_before, args1_includeTotalCount) { - EnableRelativeCursors = args1_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args1_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args2 = context.RequestAborted; var result = await global::TestNamespace.Types.Nodes.AuthorNode.GetAuthorsAsync(args0, args1, args2); @@ -355,6 +355,7 @@ namespace TestNamespace.Types.Root private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) { var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args0_first = context.ArgumentValue("first"); var args0_after = context.ArgumentValue("after"); int? args0_last = null; @@ -374,7 +375,7 @@ namespace TestNamespace.Types.Root if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args0_includeTotalCount = context.IsSelected("totalCount"); + args0_includeTotalCount = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args0 = new global::GreenDonut.Data.PagingArguments( @@ -384,8 +385,7 @@ namespace TestNamespace.Types.Root args0_before, args0_includeTotalCount) { - EnableRelativeCursors = args0_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args1 = context.RequestAborted; var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1); @@ -398,6 +398,7 @@ namespace TestNamespace.Types.Root private async global::System.Threading.Tasks.ValueTask GetAuthors2Async(global::HotChocolate.Resolvers.IResolverContext context) { var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args0_first = context.ArgumentValue("first"); var args0_after = context.ArgumentValue("after"); int? args0_last = null; @@ -417,7 +418,7 @@ namespace TestNamespace.Types.Root if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args0_includeTotalCount = context.IsSelected("totalCount"); + args0_includeTotalCount = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args0 = new global::GreenDonut.Data.PagingArguments( @@ -427,8 +428,7 @@ namespace TestNamespace.Types.Root args0_before, args0_includeTotalCount) { - EnableRelativeCursors = args0_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args1 = context.RequestAborted; var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthors2Async(args0, args1); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_ConnectionName_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_ConnectionName_MatchesSnapshot.md index 6f00b520176..574059fa9a9 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_ConnectionName_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_ConnectionName_MatchesSnapshot.md @@ -54,6 +54,7 @@ namespace TestNamespace private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) { var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args0_first = context.ArgumentValue("first"); var args0_after = context.ArgumentValue("after"); int? args0_last = null; @@ -73,7 +74,7 @@ namespace TestNamespace if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args0_includeTotalCount = context.IsSelected("totalCount"); + args0_includeTotalCount = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args0 = new global::GreenDonut.Data.PagingArguments( @@ -83,8 +84,7 @@ namespace TestNamespace args0_before, args0_includeTotalCount) { - EnableRelativeCursors = args0_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args1 = context.RequestAborted; var result = await global::TestNamespace.AuthorQueries.GetAuthorsAsync(args0, args1); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_IncludeTotalCount_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_IncludeTotalCount_MatchesSnapshot.md index 70600196f6a..2b0d1237b9d 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_IncludeTotalCount_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_CustomConnection_UseConnection_IncludeTotalCount_MatchesSnapshot.md @@ -242,6 +242,7 @@ namespace TestNamespace private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) { var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args0_first = context.ArgumentValue("first"); var args0_after = context.ArgumentValue("after"); int? args0_last = null; @@ -261,7 +262,7 @@ namespace TestNamespace if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args0_includeTotalCount = context.IsSelected("totalCount"); + args0_includeTotalCount = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args0 = new global::GreenDonut.Data.PagingArguments( @@ -271,8 +272,7 @@ namespace TestNamespace args0_before, args0_includeTotalCount) { - EnableRelativeCursors = args0_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args1 = context.RequestAborted; var result = await global::TestNamespace.AuthorQueries.GetAuthorsAsync(args0, args1); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_MatchesSnapshot.md index 06707c6567a..b8c8d9ee0f7 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_MatchesSnapshot.md @@ -249,6 +249,7 @@ namespace TestNamespace.Types.Nodes { var args0 = context.Parent(); var args1_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args1_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args1_first = context.ArgumentValue("first"); var args1_after = context.ArgumentValue("after"); int? args1_last = null; @@ -268,7 +269,7 @@ namespace TestNamespace.Types.Nodes if(args1_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args1_includeTotalCount = context.IsSelected("totalCount"); + args1_includeTotalCount = args1_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args1 = new global::GreenDonut.Data.PagingArguments( @@ -278,8 +279,7 @@ namespace TestNamespace.Types.Nodes args1_before, args1_includeTotalCount) { - EnableRelativeCursors = args1_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args1_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args2 = context.RequestAborted; var result = await global::TestNamespace.Types.Nodes.AuthorNode.GetAuthorsAsync(args0, args1, args2); @@ -361,6 +361,7 @@ namespace TestNamespace.Types.Root private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) { var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args0_first = context.ArgumentValue("first"); var args0_after = context.ArgumentValue("after"); int? args0_last = null; @@ -380,7 +381,7 @@ namespace TestNamespace.Types.Root if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args0_includeTotalCount = context.IsSelected("totalCount"); + args0_includeTotalCount = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args0 = new global::GreenDonut.Data.PagingArguments( @@ -390,8 +391,7 @@ namespace TestNamespace.Types.Root args0_before, args0_includeTotalCount) { - EnableRelativeCursors = args0_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args1 = context.RequestAborted; var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1); @@ -404,6 +404,7 @@ namespace TestNamespace.Types.Root private async global::System.Threading.Tasks.ValueTask GetAuthors2Async(global::HotChocolate.Resolvers.IResolverContext context) { var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args0_first = context.ArgumentValue("first"); var args0_after = context.ArgumentValue("after"); int? args0_last = null; @@ -423,7 +424,7 @@ namespace TestNamespace.Types.Root if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args0_includeTotalCount = context.IsSelected("totalCount"); + args0_includeTotalCount = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args0 = new global::GreenDonut.Data.PagingArguments( @@ -433,8 +434,7 @@ namespace TestNamespace.Types.Root args0_before, args0_includeTotalCount) { - EnableRelativeCursors = args0_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args1 = context.RequestAborted; var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthors2Async(args0, args1); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_WithConnectionName_MatchesSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_WithConnectionName_MatchesSnapshot.md index 7a461f9ddcc..5c1022b3725 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_WithConnectionName_MatchesSnapshot.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_GenericCustomConnection_WithConnectionName_MatchesSnapshot.md @@ -249,6 +249,7 @@ namespace TestNamespace.Types.Nodes { var args0 = context.Parent(); var args1_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args1_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args1_first = context.ArgumentValue("first"); var args1_after = context.ArgumentValue("after"); int? args1_last = null; @@ -268,7 +269,7 @@ namespace TestNamespace.Types.Nodes if(args1_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args1_includeTotalCount = context.IsSelected("totalCount"); + args1_includeTotalCount = args1_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args1 = new global::GreenDonut.Data.PagingArguments( @@ -278,8 +279,7 @@ namespace TestNamespace.Types.Nodes args1_before, args1_includeTotalCount) { - EnableRelativeCursors = args1_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args1_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args2 = context.RequestAborted; var result = await global::TestNamespace.Types.Nodes.AuthorNode.GetAuthorsAsync(args0, args1, args2); @@ -361,6 +361,7 @@ namespace TestNamespace.Types.Root private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) { var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args0_first = context.ArgumentValue("first"); var args0_after = context.ArgumentValue("after"); int? args0_last = null; @@ -380,7 +381,7 @@ namespace TestNamespace.Types.Root if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args0_includeTotalCount = context.IsSelected("totalCount"); + args0_includeTotalCount = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args0 = new global::GreenDonut.Data.PagingArguments( @@ -390,8 +391,7 @@ namespace TestNamespace.Types.Root args0_before, args0_includeTotalCount) { - EnableRelativeCursors = args0_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args1 = context.RequestAborted; var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1); @@ -404,6 +404,7 @@ namespace TestNamespace.Types.Root private async global::System.Threading.Tasks.ValueTask GetAuthors2Async(global::HotChocolate.Resolvers.IResolverContext context) { var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args0_first = context.ArgumentValue("first"); var args0_after = context.ArgumentValue("after"); int? args0_last = null; @@ -423,7 +424,7 @@ namespace TestNamespace.Types.Root if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args0_includeTotalCount = context.IsSelected("totalCount"); + args0_includeTotalCount = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args0 = new global::GreenDonut.Data.PagingArguments( @@ -433,8 +434,7 @@ namespace TestNamespace.Types.Root args0_before, args0_includeTotalCount) { - EnableRelativeCursors = args0_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args1 = context.RequestAborted; var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthors2Async(args0, args1); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Inherit_PageEdge.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Inherit_PageEdge.md index ac24a78cc7d..5ae17ca96e6 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Inherit_PageEdge.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Inherit_PageEdge.md @@ -260,6 +260,7 @@ namespace TestNamespace.Types.Root private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) { var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args0_first = context.ArgumentValue("first"); var args0_after = context.ArgumentValue("after"); int? args0_last = null; @@ -279,7 +280,7 @@ namespace TestNamespace.Types.Root if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args0_includeTotalCount = context.IsSelected("totalCount"); + args0_includeTotalCount = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args0 = new global::GreenDonut.Data.PagingArguments( @@ -289,8 +290,7 @@ namespace TestNamespace.Types.Root args0_before, args0_includeTotalCount) { - EnableRelativeCursors = args0_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args1 = context.RequestAborted; var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge.md index 22624e5dd4d..2c38e830beb 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge.md @@ -242,6 +242,7 @@ namespace TestNamespace.Types.Root private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) { var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args0_first = context.ArgumentValue("first"); var args0_after = context.ArgumentValue("after"); int? args0_last = null; @@ -261,7 +262,7 @@ namespace TestNamespace.Types.Root if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args0_includeTotalCount = context.IsSelected("totalCount"); + args0_includeTotalCount = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args0 = new global::GreenDonut.Data.PagingArguments( @@ -271,8 +272,7 @@ namespace TestNamespace.Types.Root args0_before, args0_includeTotalCount) { - EnableRelativeCursors = args0_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args1 = context.RequestAborted; var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge_Generic.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge_Generic.md index 72992eaab12..9d3c1e27fdb 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge_Generic.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge_Generic.md @@ -248,6 +248,7 @@ namespace TestNamespace.Types.Root private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) { var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args0_first = context.ArgumentValue("first"); var args0_after = context.ArgumentValue("after"); int? args0_last = null; @@ -267,7 +268,7 @@ namespace TestNamespace.Types.Root if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args0_includeTotalCount = context.IsSelected("totalCount"); + args0_includeTotalCount = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args0 = new global::GreenDonut.Data.PagingArguments( @@ -277,8 +278,7 @@ namespace TestNamespace.Types.Root args0_before, args0_includeTotalCount) { - EnableRelativeCursors = args0_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args1 = context.RequestAborted; var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_PageConnection.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_PageConnection.md index c9b9afaba7c..9a44cf17cc3 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_PageConnection.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_PageConnection.md @@ -262,6 +262,7 @@ namespace TestNamespace.Types.Root private async global::System.Threading.Tasks.ValueTask GetAuthorsAsync(global::HotChocolate.Resolvers.IResolverContext context) { var args0_options = global::HotChocolate.Types.Pagination.PagingHelper.GetPagingOptions(context.Schema, context.Selection.Field); + var args0_flags = global::HotChocolate.Types.Pagination.ConnectionFlagsHelper.GetConnectionFlags(context); var args0_first = context.ArgumentValue("first"); var args0_after = context.ArgumentValue("after"); int? args0_last = null; @@ -281,7 +282,7 @@ namespace TestNamespace.Types.Root if(args0_options.IncludeTotalCount ?? global::HotChocolate.Types.Pagination.PagingDefaults.IncludeTotalCount) { - args0_includeTotalCount = context.IsSelected("totalCount"); + args0_includeTotalCount = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.TotalCount); } var args0 = new global::GreenDonut.Data.PagingArguments( @@ -291,8 +292,7 @@ namespace TestNamespace.Types.Root args0_before, args0_includeTotalCount) { - EnableRelativeCursors = args0_options.EnableRelativeCursors - ?? global::HotChocolate.Types.Pagination.PagingDefaults.EnableRelativeCursors + EnableRelativeCursors = args0_flags.HasFlag(global::HotChocolate.Types.Pagination.ConnectionFlags.RelativeCursor) }; var args1 = context.RequestAborted; var result = await global::TestNamespace.Types.Root.AuthorQueries.GetAuthorsAsync(args0, args1); diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs index 570aff0e04f..0fe36109eb5 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs @@ -421,7 +421,8 @@ private static ServiceProvider CreateServer(string connectionString) .AddPagingArguments() .AddFiltering() .AddSorting() - .ModifyRequestOptions(o => o.IncludeExceptionDetails = true); + .ModifyRequestOptions(o => o.IncludeExceptionDetails = true) + .ModifyPagingOptions(o => o.RelativeCursorFields = o.RelativeCursorFields.Add("endCursors")); services.AddSingleton, CatalogContextSeed>(); diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs index 251e6a7fbb3..702d17e859d 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs @@ -1,6 +1,7 @@ using GreenDonut.Data; using HotChocolate.Data.Models; using HotChocolate.Data.Services; +using HotChocolate.Execution.Processing; using HotChocolate.Types; using HotChocolate.Types.Pagination; @@ -17,6 +18,8 @@ public static async Task> GetProductsAsync( PagingArguments pagingArgs, QueryContext query, ProductService productService, + ConnectionFlags connectionFlags, + ISelection selection, CancellationToken cancellationToken) { var page = await productService.GetProductsByBrandAsync(brand.Id, pagingArgs, query, cancellationToken); diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2.md index 39e218f0435..557b3e9603f 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2.md @@ -57,16 +57,6 @@ LIMIT @__p_0 ## Query 2 -```sql --- @__brandIds_0={ '11', '13' } (DbType = Object) -SELECT p."BrandId" AS "Key", count(*)::int AS "Count" -FROM "Products" AS p -WHERE p."BrandId" = ANY (@__brandIds_0) -GROUP BY p."BrandId" -``` - -## Query 3 - ```sql -- @__brandIds_0={ '11', '13' } (DbType = Object) SELECT p1."BrandId", p3."Id", p3."Name", p3."BrandId" diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc.md index fcbc1189af7..5555f930a48 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc.md @@ -57,16 +57,6 @@ LIMIT @__p_0 ## Query 2 -```sql --- @__brandIds_0={ '11', '13' } (DbType = Object) -SELECT p."BrandId" AS "Key", count(*)::int AS "Count" -FROM "Products" AS p -WHERE p."BrandId" = ANY (@__brandIds_0) -GROUP BY p."BrandId" -``` - -## Query 3 - ```sql -- @__brandIds_0={ '11', '13' } (DbType = Object) SELECT p1."BrandId", p3."Id", p3."Name", p3."BrandId" diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name.md index 592fe185f15..a61d56c2869 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name.md @@ -67,16 +67,6 @@ LIMIT @__p_0 ## Query 2 -```sql --- @__brandIds_0={ '11', '13' } (DbType = Object) -SELECT p."BrandId" AS "Key", count(*)::int AS "Count" -FROM "Products" AS p -WHERE p."BrandId" = ANY (@__brandIds_0) -GROUP BY p."BrandId" -``` - -## Query 3 - ```sql -- @__brandIds_0={ '11', '13' } (DbType = Object) SELECT p1."BrandId", p3."Id", p3."Name", p3."BrandId" @@ -98,7 +88,7 @@ LEFT JOIN ( ORDER BY p1."BrandId", p3."BrandId", p3."Name" DESC, p3."Id" ``` -## Query 4 +## Query 3 ```sql -- @__ids_0={ '11', '13' } (DbType = Object) diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name__net_8_0.md index 8ad2f211de8..5dfd0017038 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name__net_8_0.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc_Brand_Name__net_8_0.md @@ -67,16 +67,6 @@ LIMIT @__p_0 ## Query 2 -```sql --- @__brandIds_0={ '11', '13' } (DbType = Object) -SELECT p."BrandId" AS "Key", count(*)::int AS "Count" -FROM "Products" AS p -WHERE p."BrandId" = ANY (@__brandIds_0) -GROUP BY p."BrandId" -``` - -## Query 3 - ```sql -- @__brandIds_0={ '11', '13' } (DbType = Object) SELECT t."BrandId", t0."Id", t0."Name", t0."BrandId" @@ -98,7 +88,7 @@ LEFT JOIN ( ORDER BY t."BrandId", t0."BrandId", t0."Name" DESC, t0."Id" ``` -## Query 4 +## Query 3 ```sql -- @__ids_0={ '11', '13' } (DbType = Object) diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc__net_8_0.md index 44906f23898..9bd64e70920 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc__net_8_0.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2_Name_Desc__net_8_0.md @@ -57,16 +57,6 @@ LIMIT @__p_0 ## Query 2 -```sql --- @__brandIds_0={ '11', '13' } (DbType = Object) -SELECT p."BrandId" AS "Key", count(*)::int AS "Count" -FROM "Products" AS p -WHERE p."BrandId" = ANY (@__brandIds_0) -GROUP BY p."BrandId" -``` - -## Query 3 - ```sql -- @__brandIds_0={ '11', '13' } (DbType = Object) SELECT t."BrandId", t0."Id", t0."Name", t0."BrandId" diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2__net_8_0.md index 7a9a1939736..65f7aa4680e 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2__net_8_0.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Brands_First_2_And_Products_First_2__net_8_0.md @@ -57,16 +57,6 @@ LIMIT @__p_0 ## Query 2 -```sql --- @__brandIds_0={ '11', '13' } (DbType = Object) -SELECT p."BrandId" AS "Key", count(*)::int AS "Count" -FROM "Products" AS p -WHERE p."BrandId" = ANY (@__brandIds_0) -GROUP BY p."BrandId" -``` - -## Query 3 - ```sql -- @__brandIds_0={ '11', '13' } (DbType = Object) SELECT t."BrandId", t0."Id", t0."Name", t0."BrandId" diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand.md index 22ef30f649e..b2aa54ef6a3 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand.md @@ -29,9 +29,7 @@ ```sql -- @__p_0='3' -SELECT ( - SELECT count(*)::int - FROM "Products" AS p0) AS "TotalCount", p."Name", p."BrandId", p."Id" +SELECT p."Name", p."BrandId", p."Id" FROM "Products" AS p ORDER BY p."Name" DESC, p."Id" LIMIT @__p_0 diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand__net_8_0.md index 11e97825ad3..b2aa54ef6a3 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand__net_8_0.md +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Query_Products_First_2_And_Brand__net_8_0.md @@ -28,9 +28,8 @@ ## Query 1 ```sql --- @__Count_1='101' -- @__p_0='3' -SELECT @__Count_1 AS "TotalCount", p."Name", p."BrandId", p."Id" +SELECT p."Name", p."BrandId", p."Id" FROM "Products" AS p ORDER BY p."Name" DESC, p."Id" LIMIT @__p_0 From f1b9da5cf20196d4c84ea60d4d08379693c606d8 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 19 Mar 2025 11:30:49 +0100 Subject: [PATCH 60/64] Colocate Edge and Connection Type (#8152) --- .../Inspectors/ConnectionTypeTransformer.cs | 18 ++++++++++++++---- .../src/Types.Analyzers/Models/EdgeTypeInfo.cs | 10 ++++++++-- ...herit_From_ConnectionBase_Reuse_PageEdge.md | 6 +++--- ...om_ConnectionBase_Reuse_PageEdge_Generic.md | 6 +++--- ...nerateSource_Inherit_From_PageConnection.md | 6 +++--- 5 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs index 6c7acbdfb03..b075b759291 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ConnectionTypeTransformer.cs @@ -116,7 +116,8 @@ public ImmutableArray Transform( edgeTypeInfo = EdgeTypeInfo.CreateEdge( compilation, connectionType, - null); + null, + connectionType.ContainingNamespace.ToDisplayString()); edgeTypeInfo.AddDiagnosticRange(diagnostics); connectionTypeInfos ??= []; connectionTypeInfos.Add(edgeTypeInfo); @@ -129,12 +130,14 @@ public ImmutableArray Transform( compilation, edge.Type, edgeClass.ClassDeclarations, + connectionType.ContainingNamespace.ToDisplayString(), edge.Name, edge.NameFormat ?? edge.Name) : EdgeTypeInfo.CreateEdge( compilation, edge.Type, null, + connectionType.ContainingNamespace.ToDisplayString(), edge.Name, edge.NameFormat ?? edge.Name); @@ -180,7 +183,7 @@ public ImmutableArray Transform( continue; } - string connectionFullTypeName = connectionType.ToFullyQualified(); + var connectionFullTypeName = connectionType.ToFullyQualified(); string? connectionName = null; string? edgeName = null; @@ -205,8 +208,15 @@ public ImmutableArray Transform( edgeTypeInfo = connectionClassLookup.TryGetValue(edgeType.ToFullyQualified(), out edgeClass) - ? EdgeTypeInfo.CreateEdgeFrom(edgeClass, edgeName, edgeName) - : EdgeTypeInfo.CreateEdge(compilation, edgeType, null, edgeName, edgeName); + ? EdgeTypeInfo.CreateEdgeFrom( + edgeClass, + connectionType.ContainingNamespace.ToDisplayString(), + edgeName, + edgeName) + : EdgeTypeInfo.CreateEdge(compilation, edgeType, null, + connectionType.ContainingNamespace.ToDisplayString(), + edgeName, + edgeName); connectionTypeInfo = connectionClassLookup.TryGetValue(connectionFullTypeName, out connectionClass) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/EdgeTypeInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/EdgeTypeInfo.cs index b829a0b73e2..f9d23715539 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/EdgeTypeInfo.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/EdgeTypeInfo.cs @@ -13,6 +13,7 @@ public sealed class EdgeTypeInfo private EdgeTypeInfo( string name, string? nameFormat, + string @namespace, INamedTypeSymbol runtimeType, ClassDeclarationSyntax? classDeclaration, ImmutableArray resolvers) @@ -21,7 +22,7 @@ private EdgeTypeInfo( NameFormat = nameFormat; RuntimeTypeFullName = runtimeType.ToDisplayString(); RuntimeType = runtimeType; - Namespace = runtimeType.ContainingNamespace.ToDisplayString(); + Namespace = @namespace; ClassDeclaration = classDeclaration; Resolvers = resolvers; } @@ -87,12 +88,14 @@ public override int GetHashCode() public static EdgeTypeInfo CreateEdgeFrom( ConnectionClassInfo connectionClass, + string @namespace, string? name = null, string? nameFormat = null) { return new EdgeTypeInfo( (name ?? connectionClass.RuntimeType.Name) + "Type", nameFormat, + @namespace, connectionClass.RuntimeType, connectionClass.ClassDeclarations, connectionClass.Resolvers); @@ -102,14 +105,16 @@ public static EdgeTypeInfo CreateEdge( Compilation compilation, INamedTypeSymbol runtimeType, ClassDeclarationSyntax? classDeclaration, + string @namespace, string? name = null, string? nameFormat = null) - => Create(compilation, runtimeType, classDeclaration, name, nameFormat); + => Create(compilation, runtimeType, classDeclaration, @namespace, name, nameFormat); private static EdgeTypeInfo Create( Compilation compilation, INamedTypeSymbol runtimeType, ClassDeclarationSyntax? classDeclaration, + string @namespace, string? name = null, string? nameFormat = null) { @@ -141,6 +146,7 @@ private static EdgeTypeInfo Create( return new EdgeTypeInfo( edgeName, nameFormat, + @namespace, runtimeType, classDeclaration, resolvers.ToImmutable()); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge.md index 2c38e830beb..814e59ef6cd 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge.md @@ -114,7 +114,7 @@ namespace TestNamespace ``` -## AuthorEdgeType.GMN8-ZMy56jZWutjKaKAXQ.hc.g.cs +## AuthorEdgeType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -130,7 +130,7 @@ using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; using HotChocolate.Internal; -namespace HotChocolate.Types.Pagination +namespace TestNamespace { public partial class AuthorEdgeType : ObjectType> { @@ -305,7 +305,7 @@ namespace Microsoft.Extensions.DependencyInjection { public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) { - builder.AddType(); + builder.AddType(); builder.AddType(); builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( "Tests::TestNamespace.Types.Root.AuthorQueries", diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge_Generic.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge_Generic.md index 9d3c1e27fdb..6164c0864a3 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge_Generic.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_ConnectionBase_Reuse_PageEdge_Generic.md @@ -117,7 +117,7 @@ namespace TestNamespace ``` -## AuthorEdgeType.GMN8-ZMy56jZWutjKaKAXQ.hc.g.cs +## AuthorEdgeType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -133,7 +133,7 @@ using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; using HotChocolate.Internal; -namespace HotChocolate.Types.Pagination +namespace TestNamespace { public partial class AuthorEdgeType : ObjectType> { @@ -311,7 +311,7 @@ namespace Microsoft.Extensions.DependencyInjection { public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) { - builder.AddType(); + builder.AddType(); builder.AddType(); builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( "Tests::TestNamespace.Types.Root.AuthorQueries", diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_PageConnection.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_PageConnection.md index 9a44cf17cc3..0a79e62ae1b 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_PageConnection.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/PagingTests.GenerateSource_Inherit_From_PageConnection.md @@ -134,7 +134,7 @@ namespace TestNamespace ``` -## AuthorEdgeType.GMN8-ZMy56jZWutjKaKAXQ.hc.g.cs +## AuthorEdgeType.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs ```csharp // @@ -150,7 +150,7 @@ using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; using HotChocolate.Internal; -namespace HotChocolate.Types.Pagination +namespace TestNamespace { public partial class AuthorEdgeType : ObjectType> { @@ -325,7 +325,7 @@ namespace Microsoft.Extensions.DependencyInjection { public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) { - builder.AddType(); + builder.AddType(); builder.AddType(); builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( "Tests::TestNamespace.Types.Root.AuthorQueries", From efa4da51d49be25f9fc22c48d8c0bff58a218339 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 19 Mar 2025 12:25:12 +0100 Subject: [PATCH 61/64] Added pageInfo as connection flag (#8153) --- ...nnectionFlagsParameterExpressionBuilder.cs | 21 +----- .../Types/Types/Pagination/ConnectionFlags.cs | 10 +-- .../Types/Pagination/ConnectionFlagsHelper.cs | 74 +++++++++++++------ .../Types/Types/Pagination/PagingOptions.cs | 20 ++++- .../Data.PostgreSQL.Tests/IntegrationTests.cs | 26 +++++++ .../Types/Brands/BrandNode.cs | 6 ++ ...hat_PageInfo_Flag_Is_Correctly_Inferred.md | 32 ++++++++ ...nfo_Flag_Is_Correctly_Inferred__net_8_0.md | 32 ++++++++ 8 files changed, 170 insertions(+), 51 deletions(-) create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Verify_That_PageInfo_Flag_Is_Correctly_Inferred.md create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Verify_That_PageInfo_Flag_Is_Correctly_Inferred__net_8_0.md diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ConnectionFlagsParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ConnectionFlagsParameterExpressionBuilder.cs index d23650226c2..4df712e4e23 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ConnectionFlagsParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ConnectionFlagsParameterExpressionBuilder.cs @@ -34,24 +34,5 @@ public T Execute(IResolverContext context) => (T)(object)Execute(context); private static ConnectionFlags Execute(IResolverContext context) - { - var flags = ConnectionFlags.None; - - if (context.IsSelected("totalCount")) - { - flags |= ConnectionFlags.TotalCount; - } - - if (context.IsSelected("edges")) - { - flags |= ConnectionFlags.Edges; - } - - if (context.IsSelected("nodes")) - { - flags |= ConnectionFlags.Nodes; - } - - return flags; - } + => ConnectionFlagsHelper.GetConnectionFlags(context); } diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlags.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlags.cs index e6b4f1a112e..1ec662d8a48 100644 --- a/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlags.cs +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlags.cs @@ -27,17 +27,17 @@ public enum ConnectionFlags TotalCount = 4, /// - /// The relative cursor field was requested by the user. + /// The page info field was requested by the user. /// - RelativeCursor = 8, + PageInfo = 8, /// - /// The nodes or edges field was requested by the user. + /// The relative cursor field was requested by the user. /// - NodesOrEdges = Edges | Nodes, + RelativeCursor = 16, /// /// All fields were requested by the user. /// - All = Edges | Nodes | TotalCount | RelativeCursor + All = Edges | Nodes | TotalCount | PageInfo | RelativeCursor } diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs index 4ed202fbb7e..04962ab082d 100644 --- a/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Collections.Immutable; using HotChocolate.Language; using HotChocolate.Resolvers; using HotChocolate.Types.Descriptors.Definitions; @@ -23,7 +24,7 @@ public static ConnectionFlags GetConnectionFlags(IResolverContext context) string.Format(_keyFormat, context.Selection.Id), static (_, ctx) => { - if(ctx.Selection.Field is ObjectField field + if (ctx.Selection.Field is ObjectField field && !field.Flags.HasFlag(FieldFlags.Connection)) { return ConnectionFlags.None; @@ -33,48 +34,42 @@ public static ConnectionFlags GetConnectionFlags(IResolverContext context) var connectionFlags = ConnectionFlags.None; - if(ctx.IsSelected("edges")) + if (ctx.IsSelected("edges")) { connectionFlags |= ConnectionFlags.Edges; } - if(ctx.IsSelected("nodes")) + if (ctx.IsSelected("nodes")) { connectionFlags |= ConnectionFlags.Nodes; } - if(ctx.IsSelected("totalCount")) + if (ctx.IsSelected("totalCount")) { connectionFlags |= ConnectionFlags.TotalCount; } - if ((options.EnableRelativeCursors ?? PagingDefaults.EnableRelativeCursors) - && options.RelativeCursorFields.Count > 0) + if (options.PageInfoFields.Count > 0 + || ((options.EnableRelativeCursors ?? PagingDefaults.EnableRelativeCursors) + && options.RelativeCursorFields.Count > 0)) { var startSelections = ctx.Select(); var selectionContext = new IsSelectedContext(ctx.Schema, startSelections); - foreach (var relativeCursor in options.RelativeCursorFields) + if (options.PageInfoFields.Count > 0) { - // we reset the state here so that each visitation starts fresh. - selectionContext.AllSelected = true; - selectionContext.Selections.Clear(); - selectionContext.Selections.Push(startSelections); - - // we parse the selection pattern, we in essence use - // a SelectionSetNode as a selection pattern. - var selectionPattern = ParsePattern(relativeCursor); - - // then we visit the selection and if one selection of the selection pattern - // is not hit we break the loop and do not set the relative cursor flag. - IsSelectedVisitor.Instance.Visit(selectionPattern, selectionContext); - - // if however all selections of the selection pattern are - // hit we set the relative cursor flag. - if (selectionContext.AllSelected) + if (ArePatternsMatched(startSelections, selectionContext, options.PageInfoFields)) + { + connectionFlags |= ConnectionFlags.PageInfo; + } + } + + if ((options.EnableRelativeCursors ?? PagingDefaults.EnableRelativeCursors) + && options.RelativeCursorFields.Count > 0) + { + if (ArePatternsMatched(startSelections, selectionContext, options.RelativeCursorFields)) { connectionFlags |= ConnectionFlags.RelativeCursor; - break; } } } @@ -84,6 +79,37 @@ public static ConnectionFlags GetConnectionFlags(IResolverContext context) context); } + private static bool ArePatternsMatched( + ISelectionCollection startSelections, + IsSelectedContext selectionContext, + ImmutableHashSet patterns) + { + foreach (var fieldPattern in patterns) + { + // we reset the state here so that each visitation starts fresh. + selectionContext.AllSelected = true; + selectionContext.Selections.Clear(); + selectionContext.Selections.Push(startSelections); + + // we parse the selection pattern, we in essence use + // a SelectionSetNode as a selection pattern. + var selectionPattern = ParsePattern(fieldPattern); + + // then we visit the selection and if one selection of the selection pattern + // is not hit we break the loop and signal that the pattern was not hit. + IsSelectedVisitor.Instance.Visit(selectionPattern, selectionContext); + + // if however all selections of the selection pattern are + // hit we exit early and return true. + if (selectionContext.AllSelected) + { + return true; + } + } + + return false; + } + private static SelectionSetNode ParsePattern(string selectionSet) => _parsedSelectionSets.GetOrAdd(selectionSet, static s => ParseSelectionSet($"{{ {s} }}")); } diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs index 20b321d3ce1..76520fdc183 100644 --- a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs @@ -70,7 +70,21 @@ public class PagingOptions /// Gets or sets the fields that represent relative cursors. /// public ImmutableHashSet RelativeCursorFields { get; set; } = - ["pageInfo { forwardCursors }", "pageInfo { backwardCursors }"]; + [ + "pageInfo { forwardCursors }", + "pageInfo { backwardCursors }" + ]; + + /// + /// Gets or sets the fields that represent page infos like hasNextPage or startCursor. + /// + public ImmutableHashSet PageInfoFields { get; set; } = + [ + "pageInfo { startCursor }", + "pageInfo { endCursor }" , + "pageInfo { hasNextPage }" , + "pageInfo { hasPreviousPage }" + ]; /// /// Merges the options into this options instance wherever @@ -92,6 +106,7 @@ internal void Merge(PagingOptions other) IncludeNodesField ??= other.IncludeNodesField; EnableRelativeCursors ??= other.EnableRelativeCursors; RelativeCursorFields = RelativeCursorFields.Union(other.RelativeCursorFields); + PageInfoFields = PageInfoFields.Union(other.PageInfoFields); } /// @@ -110,6 +125,7 @@ internal PagingOptions Copy() ProviderName = ProviderName, IncludeNodesField = IncludeNodesField, EnableRelativeCursors = EnableRelativeCursors, - RelativeCursorFields = RelativeCursorFields + RelativeCursorFields = RelativeCursorFields, + PageInfoFields = PageInfoFields }; } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs index 0fe36109eb5..2911f24ef6d 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs @@ -313,6 +313,32 @@ public async Task Query_Brands_First_2_Products_First_2_ForwardCursors() MatchSnapshot(result, interceptor); } + [Fact] + public async Task Verify_That_PageInfo_Flag_Is_Correctly_Inferred() + { + // arrange + using var interceptor = new TestQueryInterceptor(); + + // act + var result = await ExecuteAsync( + """ + { + brands(first: 1) { + nodes { + products(first: 2) { + pageInfo { + endCursor + } + } + } + } + } + """); + + // assert + MatchSnapshot(result, interceptor); + } + [Fact] public async Task Query_Products_Include_TotalCount() { diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs index 702d17e859d..8f7224ad40b 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs @@ -22,6 +22,12 @@ public static async Task> GetProductsAsync( ISelection selection, CancellationToken cancellationToken) { + // we for test purposes only return an empty page if the connection flags are set to PageInfo + if(connectionFlags == ConnectionFlags.PageInfo) + { + return new PageConnection(Page.Empty); + } + var page = await productService.GetProductsByBrandAsync(brand.Id, pagingArgs, query, cancellationToken); return new PageConnection(page); } diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Verify_That_PageInfo_Flag_Is_Correctly_Inferred.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Verify_That_PageInfo_Flag_Is_Correctly_Inferred.md new file mode 100644 index 00000000000..3c6874e4ea9 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Verify_That_PageInfo_Flag_Is_Correctly_Inferred.md @@ -0,0 +1,32 @@ +# Verify_That_PageInfo_Flag_Is_Correctly_Inferred + +## Result + +```json +{ + "data": { + "brands": { + "nodes": [ + { + "products": { + "pageInfo": { + "endCursor": null + } + } + } + ] + } + } +} +``` + +## Query 1 + +```sql +-- @__p_0='2' +SELECT b."Id", b."Name" +FROM "Brands" AS b +ORDER BY b."Name" DESC, b."Id" +LIMIT @__p_0 +``` + diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Verify_That_PageInfo_Flag_Is_Correctly_Inferred__net_8_0.md b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Verify_That_PageInfo_Flag_Is_Correctly_Inferred__net_8_0.md new file mode 100644 index 00000000000..3c6874e4ea9 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.Verify_That_PageInfo_Flag_Is_Correctly_Inferred__net_8_0.md @@ -0,0 +1,32 @@ +# Verify_That_PageInfo_Flag_Is_Correctly_Inferred + +## Result + +```json +{ + "data": { + "brands": { + "nodes": [ + { + "products": { + "pageInfo": { + "endCursor": null + } + } + } + ] + } + } +} +``` + +## Query 1 + +```sql +-- @__p_0='2' +SELECT b."Id", b."Name" +FROM "Brands" AS b +ORDER BY b."Name" DESC, b."Id" +LIMIT @__p_0 +``` + From 253c43a0c9a18d8cf2e3125f8654cc9a411afe91 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 21 Mar 2025 14:43:01 +0100 Subject: [PATCH 62/64] Allow for scalar overrides when using schema-first (#8164) --- .../src/Abstractions/WellKnownContextData.cs | 5 ++ ...aRequestExecutorBuilderExtensions.Types.cs | 55 +++++++++++++++++ .../Handlers/SyntaxTypeReferenceHandler.cs | 46 ++++++++++++--- .../src/Types/Configuration/ITypeRegistrar.cs | 2 + .../src/Types/Configuration/TypeDiscoverer.cs | 56 +++++++++++++++--- .../src/Types/Configuration/TypeRegistrar.cs | 7 +++ .../src/Types/Types/Scalars/ScalarNames.cs | 1 - .../Core/src/Types/Types/Scalars/Scalars.cs | 12 ++++ ...LocalTimeTypeGeneralIsoIntegrationTests.cs | 4 +- .../LocalTimeTypeTests.cs | 59 +++++++++++++++++++ ...odaTimeRequestExecutorBuilderExtensions.cs | 24 +++++--- ...ure_Schema_First_Can_Override_Type.graphql | 18 ++++++ ...e_Schema_First_Can_Override_Type_2.graphql | 18 ++++++ ...ure_Schema_First_Obverride_Is_Lazy.graphql | 7 +++ .../src/CodeGeneration/BuiltInScalarNames.cs | 1 - 15 files changed, 287 insertions(+), 28 deletions(-) create mode 100644 src/HotChocolate/Core/test/Types.NodaTime.Tests/__snapshots__/LocalTimeTypeIntegrationTests.Ensure_Schema_First_Can_Override_Type.graphql create mode 100644 src/HotChocolate/Core/test/Types.NodaTime.Tests/__snapshots__/LocalTimeTypeIntegrationTests.Ensure_Schema_First_Can_Override_Type_2.graphql create mode 100644 src/HotChocolate/Core/test/Types.NodaTime.Tests/__snapshots__/LocalTimeTypeIntegrationTests.Ensure_Schema_First_Obverride_Is_Lazy.graphql diff --git a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs index 788484c4c4c..67ac71c5ad2 100644 --- a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs +++ b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs @@ -343,4 +343,9 @@ public static class WellKnownContextData /// The key to determine whether the @authorize directive was already registered. /// public const string AreAuthorizeDirectivesRegistered = "HotChocolate.Authorization.AuthDirectivesRegistered"; + + /// + /// he key to get the scalar name overrides. + /// + public const string ScalarNameOverrides = "HotChocolate.Types.Scalars.Overrides"; } diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/SchemaRequestExecutorBuilderExtensions.Types.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/SchemaRequestExecutorBuilderExtensions.Types.cs index b8ec3e02e8a..60838a7e7e4 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/SchemaRequestExecutorBuilderExtensions.Types.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/SchemaRequestExecutorBuilderExtensions.Types.cs @@ -1665,4 +1665,59 @@ public static IRequestExecutorBuilder BindRuntimeType( return builder.ConfigureSchema(b => b.BindRuntimeType(runtimeType, typeName)); } + + /// + /// Binds a scalar type to a type name for schema first. + /// + /// + /// The GraphQL builder. + /// + /// + /// The type name. + /// + /// + /// The scalar type. + /// + /// + /// Returns the GraphQL builder for configuration chaining. + /// + public static IRequestExecutorBuilder BindScalarType( + this IRequestExecutorBuilder builder, + string typeName) + where TScalarType : ScalarType + => BindScalarType(builder, typeof(TScalarType), typeName); + + /// + /// Binds a scalar type to a type name for schema first. + /// + /// + /// The GraphQL builder. + /// + /// + /// The scalar type. + /// + /// + /// The type name. + /// + /// + /// Returns the GraphQL builder for configuration chaining. + /// + public static IRequestExecutorBuilder BindScalarType( + this IRequestExecutorBuilder builder, + Type scalarType, + string typeName) + { + builder.ConfigureSchema(b => + { + if (!b.ContextData.TryGetValue(WellKnownContextData.ScalarNameOverrides, out var value) + || value is not List<(string, Type)> nameOverrides) + { + nameOverrides = new List<(string, Type)>(); + b.ContextData[WellKnownContextData.ScalarNameOverrides] = nameOverrides; + } + + nameOverrides.Add((typeName, scalarType)); + }); + return builder; + } } diff --git a/src/HotChocolate/Core/src/Types/Configuration/Handlers/SyntaxTypeReferenceHandler.cs b/src/HotChocolate/Core/src/Types/Configuration/Handlers/SyntaxTypeReferenceHandler.cs index 92fb49ec930..495d1c47774 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/Handlers/SyntaxTypeReferenceHandler.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/Handlers/SyntaxTypeReferenceHandler.cs @@ -5,11 +5,25 @@ namespace HotChocolate.Configuration; -internal sealed class SyntaxTypeReferenceHandler(ITypeInspector typeInspector) : ITypeRegistrarHandler +internal sealed class SyntaxTypeReferenceHandler : ITypeRegistrarHandler { private readonly HashSet _handled = []; - private readonly ITypeInspector _typeInspector = typeInspector ?? - throw new ArgumentNullException(nameof(typeInspector)); + private readonly ITypeInspector _typeInspector; + private readonly Dictionary _scalarTypes = new(); + + public SyntaxTypeReferenceHandler(IDescriptorContext context) + { + _typeInspector = context.TypeInspector; + + if (context.ContextData.TryGetValue(WellKnownContextData.ScalarNameOverrides, out var value) + && value is List<(string, Type)> nameOverrides) + { + foreach (var (name, type) in nameOverrides) + { + _scalarTypes.TryAdd(name, type); + } + } + } public TypeReferenceKind Kind => TypeReferenceKind.Syntax; @@ -17,15 +31,31 @@ public void Handle(ITypeRegistrar typeRegistrar, TypeReference typeReference) { var typeRef = (SyntaxTypeReference)typeReference; - if (_handled.Add(typeRef.Name) && - Scalars.TryGetScalar(typeRef.Name, out var scalarType)) + if (_handled.Add(typeRef.Name) + && !typeRegistrar.Scalars.Contains(typeRef.Name)) { - var namedTypeReference = _typeInspector.GetTypeRef(scalarType); + ExtendedTypeReference? scalarTypeRef = null; + + if (_scalarTypes.TryGetValue(typeRef.Name, out var scalarType)) + { + if (Scalars.IsSpec(typeRef.Name)) + { + throw new InvalidOperationException( + $"Type {typeRef.Name} is a spec scalar and cannot be overriden."); + } + } + + if (scalarType is not null + || Scalars.TryGetScalar(typeRef.Name, out scalarType)) + { + scalarTypeRef = _typeInspector.GetTypeRef(scalarType); + } - if (!typeRegistrar.IsResolved(namedTypeReference)) + if (scalarTypeRef is not null && + !typeRegistrar.IsResolved(scalarTypeRef)) { typeRegistrar.Register( - typeRegistrar.CreateInstance(namedTypeReference.Type.Type), + typeRegistrar.CreateInstance(scalarTypeRef.Type.Type), typeRef.Scope); } } diff --git a/src/HotChocolate/Core/src/Types/Configuration/ITypeRegistrar.cs b/src/HotChocolate/Core/src/Types/Configuration/ITypeRegistrar.cs index 6d000057699..3deae465d30 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/ITypeRegistrar.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/ITypeRegistrar.cs @@ -7,6 +7,8 @@ namespace HotChocolate.Configuration; internal interface ITypeRegistrar { + ISet Scalars { get; } + void Register( TypeSystemObjectBase obj, string? scope, diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeDiscoverer.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeDiscoverer.cs index cea1541ded5..e839f27cacf 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeDiscoverer.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeDiscoverer.cs @@ -60,8 +60,48 @@ public TypeDiscoverer( _unregistered.AddRange(Directives.CreateReferences(context)); } - _unregistered.AddRange(typeRegistry.GetTypeRefs()); - _unregistered.AddRange(initialTypes.Distinct()); + var first = new List(); + var second = new List(); + var third = new List(); + var fourth = new List(); + + foreach (var typeRef in typeRegistry.GetTypeRefs().Concat(initialTypes.Distinct())) + { + switch (typeRef) + { + case ExtendedTypeReference { Type.IsSchemaType: true } extendedTypeRef: + if (typeof(ScalarType).IsAssignableFrom(extendedTypeRef.Type.Type)) + { + first.Add(typeRef); + } + else + { + second.Add(typeRef); + } + break; + + case ExtendedTypeReference: + third.Add(typeRef); + break; + + case SchemaTypeReference { Type: ScalarType }: + first.Add(typeRef); + break; + + case SchemaTypeReference: + second.Add(typeRef); + break; + + default: + fourth.Add(typeRef); + break; + } + } + + _unregistered.AddRange(first); + _unregistered.AddRange(second); + _unregistered.AddRange(third); + _unregistered.AddRange(fourth); _typeRegistrar = new TypeRegistrar(context, typeRegistry, typeLookup, interceptor); @@ -69,7 +109,7 @@ public TypeDiscoverer( [ new ExtendedTypeReferenceHandler(context.TypeInspector), new SchemaTypeReferenceHandler(), - new SyntaxTypeReferenceHandler(context.TypeInspector), + new SyntaxTypeReferenceHandler(context), new FactoryTypeReferenceHandler(context), new DependantFactoryTypeReferenceHandler(context), new ExtendedTypeDirectiveReferenceHandler(context.TypeInspector), @@ -144,7 +184,7 @@ private void RegisterTypes() { foreach (var typeRef in _unregistered) { - var index = (int) typeRef.Kind; + var index = (int)typeRef.Kind; if (_handlers.Length > index) { @@ -165,8 +205,8 @@ private bool TryInferTypes() { // first we will check if we have a type binding for the unresolved type. // type bindings are types that will be registered instead of the actual discovered type. - if (unresolvedTypeRef is ExtendedTypeReference extendedTypeRef && - _typeRegistry.RuntimeTypeRefs.TryGetValue(extendedTypeRef, out var typeReference)) + if (unresolvedTypeRef is ExtendedTypeReference extendedTypeRef + && _typeRegistry.RuntimeTypeRefs.TryGetValue(extendedTypeRef, out var typeReference)) { inferred = true; _unregistered.Add(typeReference); @@ -175,8 +215,8 @@ private bool TryInferTypes() } // if we do not have a type binding or if we have a directive we will try to infer the type. - if (unresolvedTypeRef is ExtendedTypeReference or ExtendedTypeDirectiveReference && - _context.TryInferSchemaType(unresolvedTypeRef, out var schemaTypeRefs)) + if (unresolvedTypeRef is ExtendedTypeReference or ExtendedTypeDirectiveReference + && _context.TryInferSchemaType(unresolvedTypeRef, out var schemaTypeRefs)) { inferred = true; diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeRegistrar.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeRegistrar.cs index 605e0b8946c..1a28323ccb6 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeRegistrar.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeRegistrar.cs @@ -40,6 +40,8 @@ public TypeRegistrar(IDescriptorContext context, : new CombinedServiceProvider(_schemaServices, _applicationServices); } + public ISet Scalars { get; } = new HashSet(); + public void Register( TypeSystemObjectBase obj, string? scope, @@ -51,6 +53,11 @@ public void Register( throw new ArgumentNullException(nameof(obj)); } + if (obj is ScalarType scalar) + { + Scalars.Add(scalar.Name); + } + var registeredType = InitializeType(obj, scope, inferred); configure?.Invoke(registeredType); diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarNames.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarNames.cs index 46ecd86099d..fa11417702c 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarNames.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarNames.cs @@ -18,7 +18,6 @@ public static class ScalarNames public const string DateTime = nameof(DateTime); public const string Date = nameof(Date); public const string TimeSpan = nameof(TimeSpan); - public const string Name = nameof(Name); public const string JSON = nameof(JSON); public const string LocalDate = nameof(LocalDate); public const string LocalDateTime = nameof(LocalDateTime); diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/Scalars.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/Scalars.cs index 0f4b5261f7c..71b2a2c3a97 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/Scalars.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/Scalars.cs @@ -92,6 +92,15 @@ public static class Scalars { typeof(bool?), ValueKind.Float }, }; + private static readonly HashSet _specScalars = + [ + ScalarNames.ID, + ScalarNames.String, + ScalarNames.Int, + ScalarNames.Float, + ScalarNames.Boolean, + ]; + internal static bool TryGetScalar( Type runtimeType, [NotNullWhen(true)] out Type? schemaType) => @@ -173,4 +182,7 @@ public static bool TryGetKind(object? value, out ValueKind kind) kind = ValueKind.Unknown; return false; } + + internal static bool IsSpec(string typeName) + => _specScalars.Contains(typeName); } diff --git a/src/HotChocolate/Core/test/Types.NodaTime.Tests/LocalTimeTypeGeneralIsoIntegrationTests.cs b/src/HotChocolate/Core/test/Types.NodaTime.Tests/LocalTimeTypeGeneralIsoIntegrationTests.cs index 0b3e5118698..89a8a84b9ee 100644 --- a/src/HotChocolate/Core/test/Types.NodaTime.Tests/LocalTimeTypeGeneralIsoIntegrationTests.cs +++ b/src/HotChocolate/Core/test/Types.NodaTime.Tests/LocalTimeTypeGeneralIsoIntegrationTests.cs @@ -25,7 +25,7 @@ public void QueryReturns() [Fact] public void ParsesVariable() { - IExecutionResult? result = _testExecutor + var result = _testExecutor .Execute(OperationRequestBuilder.New() .SetDocument("mutation($arg: LocalTime!) { test(arg: $arg) }") .SetVariableValues(new Dictionary { {"arg", "12:42:13" }, }) @@ -37,7 +37,7 @@ public void ParsesVariable() [Fact] public void DoesntParseAnIncorrectVariable() { - IExecutionResult? result = _testExecutor + var result = _testExecutor .Execute(OperationRequestBuilder.New() .SetDocument("mutation($arg: LocalTime!) { test(arg: $arg) }") .SetVariableValues(new Dictionary { {"arg", "12:42" }, }) diff --git a/src/HotChocolate/Core/test/Types.NodaTime.Tests/LocalTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.NodaTime.Tests/LocalTimeTypeTests.cs index 5f113af9d4c..d0a983351e1 100644 --- a/src/HotChocolate/Core/test/Types.NodaTime.Tests/LocalTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.NodaTime.Tests/LocalTimeTypeTests.cs @@ -1,5 +1,6 @@ using System.Globalization; using HotChocolate.Execution; +using Microsoft.Extensions.DependencyInjection; using NodaTime; using NodaTime.Text; @@ -159,4 +160,62 @@ public void LocalTimeType_DescriptionUnknownPatterns_MatchesSnapshot() localTimeType.Description.MatchInlineSnapshot( "LocalTime represents a time of day, with no reference to a particular calendar, time zone, or date."); } + + [Fact] + public async Task Ensure_Schema_First_Can_Override_Type() + { + var schema = await new ServiceCollection() + .AddGraphQL() + .AddDocumentFromString( + """ + type Query { + foo: LocalTime + } + + scalar LocalTime + """) + .AddType() + .UseField(next => next) + .BuildSchemaAsync(); + + schema.MatchSnapshot(); + } + + [Fact] + public async Task Ensure_Schema_First_Can_Override_Type_2() + { + var schema = await new ServiceCollection() + .AddGraphQL() + .AddDocumentFromString( + """ + type Query { + foo: LocalTime + } + + scalar LocalTime + """) + .BindScalarType("LocalTime") + .UseField(next => next) + .BuildSchemaAsync(); + + schema.MatchSnapshot(); + } + + [Fact] + public async Task Ensure_Schema_First_Obverride_Is_Lazy() + { + var schema = await new ServiceCollection() + .AddGraphQL() + .AddDocumentFromString( + """ + type Query { + foo: String + } + """) + .BindScalarType("LocalTime") + .UseField(next => next) + .BuildSchemaAsync(); + + schema.MatchSnapshot(); + } } diff --git a/src/HotChocolate/Core/test/Types.NodaTime.Tests/NodaTimeRequestExecutorBuilderExtensions.cs b/src/HotChocolate/Core/test/Types.NodaTime.Tests/NodaTimeRequestExecutorBuilderExtensions.cs index ed2ddf16563..eff0beeed57 100644 --- a/src/HotChocolate/Core/test/Types.NodaTime.Tests/NodaTimeRequestExecutorBuilderExtensions.cs +++ b/src/HotChocolate/Core/test/Types.NodaTime.Tests/NodaTimeRequestExecutorBuilderExtensions.cs @@ -14,12 +14,20 @@ public static ISchemaBuilder AddNodaTime( return schemaBuilder; } - private static readonly IReadOnlyList _nodaTimeTypes = new[] - { - typeof(DateTimeZoneType), typeof(DurationType), typeof(InstantType), - typeof(IsoDayOfWeekType), typeof(LocalDateTimeType), typeof(LocalDateType), - typeof(LocalTimeType), typeof(OffsetDateTimeType), typeof(OffsetDateType), - typeof(OffsetTimeType), typeof(OffsetType), typeof(PeriodType), - typeof(ZonedDateTimeType), - }; + private static readonly IReadOnlyList _nodaTimeTypes = + [ + typeof(DateTimeZoneType), + typeof(DurationType), + typeof(InstantType), + typeof(IsoDayOfWeekType), + typeof(LocalDateTimeType), + typeof(LocalDateType), + typeof(LocalTimeType), + typeof(OffsetDateTimeType), + typeof(OffsetDateType), + typeof(OffsetTimeType), + typeof(OffsetType), + typeof(PeriodType), + typeof(ZonedDateTimeType) + ]; } diff --git a/src/HotChocolate/Core/test/Types.NodaTime.Tests/__snapshots__/LocalTimeTypeIntegrationTests.Ensure_Schema_First_Can_Override_Type.graphql b/src/HotChocolate/Core/test/Types.NodaTime.Tests/__snapshots__/LocalTimeTypeIntegrationTests.Ensure_Schema_First_Can_Override_Type.graphql new file mode 100644 index 00000000000..c1933c1ff26 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.NodaTime.Tests/__snapshots__/LocalTimeTypeIntegrationTests.Ensure_Schema_First_Can_Override_Type.graphql @@ -0,0 +1,18 @@ +schema { + query: Query +} + +type Query { + foo: LocalTime +} + +""" +LocalTime represents a time of day, with no reference to a particular calendar, time zone, or date. + +Allowed patterns: +- `hh:mm:ss.sssssssss` + +Examples: +- `20:00:00.999` +""" +scalar LocalTime diff --git a/src/HotChocolate/Core/test/Types.NodaTime.Tests/__snapshots__/LocalTimeTypeIntegrationTests.Ensure_Schema_First_Can_Override_Type_2.graphql b/src/HotChocolate/Core/test/Types.NodaTime.Tests/__snapshots__/LocalTimeTypeIntegrationTests.Ensure_Schema_First_Can_Override_Type_2.graphql new file mode 100644 index 00000000000..c1933c1ff26 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.NodaTime.Tests/__snapshots__/LocalTimeTypeIntegrationTests.Ensure_Schema_First_Can_Override_Type_2.graphql @@ -0,0 +1,18 @@ +schema { + query: Query +} + +type Query { + foo: LocalTime +} + +""" +LocalTime represents a time of day, with no reference to a particular calendar, time zone, or date. + +Allowed patterns: +- `hh:mm:ss.sssssssss` + +Examples: +- `20:00:00.999` +""" +scalar LocalTime diff --git a/src/HotChocolate/Core/test/Types.NodaTime.Tests/__snapshots__/LocalTimeTypeIntegrationTests.Ensure_Schema_First_Obverride_Is_Lazy.graphql b/src/HotChocolate/Core/test/Types.NodaTime.Tests/__snapshots__/LocalTimeTypeIntegrationTests.Ensure_Schema_First_Obverride_Is_Lazy.graphql new file mode 100644 index 00000000000..07fd17957b6 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.NodaTime.Tests/__snapshots__/LocalTimeTypeIntegrationTests.Ensure_Schema_First_Obverride_Is_Lazy.graphql @@ -0,0 +1,7 @@ +schema { + query: Query +} + +type Query { + foo: String +} diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/BuiltInScalarNames.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/BuiltInScalarNames.cs index fbf1248aab0..0cc091c8063 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/BuiltInScalarNames.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/BuiltInScalarNames.cs @@ -27,7 +27,6 @@ public static class BuiltInScalarNames ScalarNames.LocalDate, ScalarNames.LocalDateTime, ScalarNames.LocalTime, - ScalarNames.Name, ScalarNames.ByteArray, ScalarNames.Any, ScalarNames.JSON, From 3fbf6f7290357836f0af1e5128718fce3aa78086 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 21 Mar 2025 15:31:12 +0100 Subject: [PATCH 63/64] Relaxed type symbol check for connections. (#8165) --- .../Core/src/Types.Analyzers/KnownSymbols.cs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs b/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs index 005460c587a..6692e6642d5 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/KnownSymbols.cs @@ -83,28 +83,23 @@ public static bool TryGetGraphQLTypeName( return false; } - public static INamedTypeSymbol GetConnectionBaseSymbol(this GeneratorSyntaxContext context) + public static INamedTypeSymbol? GetConnectionBaseSymbol(this GeneratorSyntaxContext context) => context.SemanticModel.Compilation.GetConnectionBaseSymbol(); - public static INamedTypeSymbol GetConnectionBaseSymbol(this Compilation compilation) - => compilation.GetTypeByMetadataName("HotChocolate.Types.Pagination.ConnectionBase`3") - ?? throw new InvalidOperationException("Could not resolve connection base type."); + public static INamedTypeSymbol? GetConnectionBaseSymbol(this Compilation compilation) + => compilation.GetTypeByMetadataName("HotChocolate.Types.Pagination.ConnectionBase`3"); - public static INamedTypeSymbol GetEdgeInterfaceSymbol(this Compilation compilation) - => compilation.GetTypeByMetadataName("HotChocolate.Types.Pagination.IEdge`1") - ?? throw new InvalidOperationException("Could not resolve edge interface type."); + public static INamedTypeSymbol? GetEdgeInterfaceSymbol(this Compilation compilation) + => compilation.GetTypeByMetadataName("HotChocolate.Types.Pagination.IEdge`1"); - public static INamedTypeSymbol GetTaskSymbol(this Compilation compilation) - => compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1") - ?? throw new InvalidOperationException("Could not resolve connection base type."); + public static INamedTypeSymbol? GetTaskSymbol(this Compilation compilation) + => compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1"); - public static INamedTypeSymbol GetValueTaskSymbol(this Compilation compilation) - => compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask`1") - ?? throw new InvalidOperationException("Could not resolve connection base type."); + public static INamedTypeSymbol? GetValueTaskSymbol(this Compilation compilation) + => compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask`1"); - public static INamedTypeSymbol GetConnectionFlagsSymbol(this Compilation compilation) - => compilation.GetTypeByMetadataName("HotChocolate.Types.Pagination.ConnectionFlags") - ?? throw new InvalidOperationException("Could not resolve connection flags type."); + public static INamedTypeSymbol? GetConnectionFlagsSymbol(this Compilation compilation) + => compilation.GetTypeByMetadataName("HotChocolate.Types.Pagination.ConnectionFlags"); public static bool IsConnectionType(this GeneratorSyntaxContext context, ITypeSymbol possibleConnectionType) => context.SemanticModel.Compilation.IsConnectionType(possibleConnectionType); @@ -174,8 +169,14 @@ public static bool IsConnectionFlagsType(this Compilation compilation, ITypeSymb return SymbolEqualityComparer.Default.Equals(namedType, compilation.GetConnectionFlagsSymbol()); } - private static bool IsDerivedFromGenericBase(INamedTypeSymbol typeSymbol, INamedTypeSymbol baseTypeSymbol) + private static bool IsDerivedFromGenericBase(INamedTypeSymbol? typeSymbol, INamedTypeSymbol? baseTypeSymbol) { + // if we are generating only for GreenDonut some base types might not exist. + if (baseTypeSymbol is null) + { + return false; + } + var current = typeSymbol; while (current is not null) From 73914c6909f36bab72147d36ad6a8d681aa55c17 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 21 Mar 2025 16:52:52 +0100 Subject: [PATCH 64/64] Fixed missing global:: in generated code (#8166) --- .../src/Types.Analyzers/FileBuilders/ObjectTypeFileBuilder.cs | 2 +- ...Tests.Ensure_Entity_Becomes_Node_With_Query_Node_Resolver.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ObjectTypeFileBuilder.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ObjectTypeFileBuilder.cs index 5156fd43523..d70ce532f92 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ObjectTypeFileBuilder.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/ObjectTypeFileBuilder.cs @@ -90,7 +90,7 @@ public override void WriteResolverConstructor(IOutputTypeInfo type, ILocalTypeLo WriteResolverConstructor( objectType, typeLookup, - $"{objectType.SchemaTypeFullName}", + $"global::{objectType.SchemaTypeFullName}", type.Resolvers.Any(t => t.RequiresParameterBindings) || (objectType.NodeResolver?.RequiresParameterBindings ?? false)); } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Ensure_Entity_Becomes_Node_With_Query_Node_Resolver.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Ensure_Entity_Becomes_Node_With_Query_Node_Resolver.md index 03081b5d3aa..30dda49821d 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Ensure_Entity_Becomes_Node_With_Query_Node_Resolver.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/ResolverTests.Ensure_Entity_Becomes_Node_With_Query_Node_Resolver.md @@ -164,7 +164,7 @@ namespace TestNamespace public __Resolvers(global::HotChocolate.Internal.IParameterBindingResolver bindingResolver) { - var type = typeof(TestNamespace.TestType); + var type = typeof(global::TestNamespace.TestType); global::System.Reflection.MethodInfo resolver = default!; global::System.Reflection.ParameterInfo[] parameters = default!;