From 9f447201434f8b97e15ea233f4285beb0a56c380 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 12 Aug 2022 17:47:22 +0200 Subject: [PATCH] Integrated Gateway into Request Pipeline (#5298) --- .../InternalServiceCollectionExtensions.cs | 7 +- .../Processing/DeferredWorkScheduler.cs | 17 +-- .../Processing/OperationContext.Execution.cs | 2 +- .../Processing/OperationContext.Pooling.cs | 17 ++- .../Processing/Result/ResultBuilder.cs | 2 - .../src/Core/Clients/GraphQLClientFactory.cs | 14 ++ .../GraphQLHttpClient.cs} | 27 +++- .../Request.cs => Clients/GraphQLRequest.cs} | 6 +- .../src/Core/Clients/GraphQLResponse.cs | 42 ++++++ .../Fusion/src/Core/Clients/IGraphQLClient.cs | 18 +++ .../src/Core/Clients/ResponseProperties.cs | 8 ++ .../RequestExecutorBuilderExtensions.cs | 62 +++++++++ .../src/Core/Execution/ArgumentContext.cs | 65 --------- ...torContext.cs => FederatedQueryContext.cs} | 4 +- ...yExecutor.cs => FederatedQueryExecutor.cs} | 84 +++++------- .../Core/Execution/IRemoteRequestExecutor.cs | 8 -- .../Execution/RemoteRequestExecutorFactory.cs | 14 -- .../src/Core/Execution/RequestHandler.cs | 8 +- .../Fusion/src/Core/Execution/Response.cs | 25 ---- .../Fusion/src/Core/Execution/WorkItem.cs | 7 +- .../Pipeline/OperationExecutionMiddleware.cs | 60 +++++++++ .../src/Core/Pipeline/PipelineProperties.cs | 6 + .../src/Core/Pipeline/QueryPlanMiddleware.cs | 48 +++++++ .../Utilities/JsonQueryResultFormatter.cs | 2 +- .../JsonRequestFormatter.cs | 7 +- .../Core.Tests/RemoteQueryExecutorTests.cs | 126 +++++++----------- 26 files changed, 392 insertions(+), 294 deletions(-) create mode 100644 src/HotChocolate/Fusion/src/Core/Clients/GraphQLClientFactory.cs rename src/HotChocolate/Fusion/src/Core/{Execution/HttpRequestExecutor.cs => Clients/GraphQLHttpClient.cs} (77%) rename src/HotChocolate/Fusion/src/Core/{Execution/Request.cs => Clients/GraphQLRequest.cs} (82%) create mode 100644 src/HotChocolate/Fusion/src/Core/Clients/GraphQLResponse.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Clients/IGraphQLClient.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Clients/ResponseProperties.cs create mode 100644 src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs delete mode 100644 src/HotChocolate/Fusion/src/Core/Execution/ArgumentContext.cs rename src/HotChocolate/Fusion/src/Core/Execution/{RemoteExecutorContext.cs => FederatedQueryContext.cs} (90%) rename src/HotChocolate/Fusion/src/Core/Execution/{RemoteQueryExecutor.cs => FederatedQueryExecutor.cs} (85%) delete mode 100644 src/HotChocolate/Fusion/src/Core/Execution/IRemoteRequestExecutor.cs delete mode 100644 src/HotChocolate/Fusion/src/Core/Execution/RemoteRequestExecutorFactory.cs delete mode 100644 src/HotChocolate/Fusion/src/Core/Execution/Response.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Pipeline/OperationExecutionMiddleware.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Pipeline/PipelineProperties.cs create mode 100644 src/HotChocolate/Fusion/src/Core/Pipeline/QueryPlanMiddleware.cs rename src/HotChocolate/Fusion/src/Core/{Execution => Utilities}/JsonRequestFormatter.cs (92%) diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs index b9a6b9f6748..2e4d8853963 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs @@ -50,6 +50,7 @@ internal static class InternalServiceCollectionExtensions services.TryAddSingleton(_ => new ObjectResultPool(maximumRetained, maximumArrayCapacity)); services.TryAddSingleton(_ => new ListResultPool(maximumRetained, maximumArrayCapacity)); services.TryAddSingleton(); + services.TryAddTransient(); return services; } @@ -129,18 +130,12 @@ internal static class InternalServiceCollectionExtensions { var pool = sp.GetRequiredService>(); var state = pool.Get(); - return new DeferredWorkStateOwner(state, pool); }); - services.TryAddTransient(); - services.TryAddScoped>( sp => new ServiceFactory(sp)); - services.TryAddScoped>( - sp => new ServiceFactory(sp)); - return services; } diff --git a/src/HotChocolate/Core/src/Execution/Processing/DeferredWorkScheduler.cs b/src/HotChocolate/Core/src/Execution/Processing/DeferredWorkScheduler.cs index 98f93d3f95c..a7727b175a4 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/DeferredWorkScheduler.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/DeferredWorkScheduler.cs @@ -2,26 +2,19 @@ using System.Threading; using HotChocolate.Execution.DependencyInjection; using HotChocolate.Execution.Instrumentation; +using Microsoft.Extensions.DependencyInjection; using static HotChocolate.Execution.QueryResultBuilder; namespace HotChocolate.Execution.Processing; internal sealed class DeferredWorkScheduler : IDeferredWorkScheduler { - private readonly IFactory _operationContextFactory; - private readonly IFactory _deferredWorkStateFactory; private readonly object _stateSync = new(); + private IFactory _operationContextFactory = default!; + private IFactory _deferredWorkStateFactory = default!; private OperationContext _parentContext = default!; private DeferredWorkStateOwner? _stateOwner; - public DeferredWorkScheduler( - IFactory operationContextFactory, - IFactory deferredWorkStateFactory) - { - _operationContextFactory = operationContextFactory; - _deferredWorkStateFactory = deferredWorkStateFactory; - } - private DeferredWorkStateOwner StateOwner { get @@ -45,7 +38,11 @@ private DeferredWorkStateOwner StateOwner public void Initialize(OperationContext operationContext) { + var services = operationContext.Services; + _parentContext = operationContext; + _operationContextFactory = services.GetRequiredService>(); + _deferredWorkStateFactory = services.GetRequiredService>(); } public void InitializeFrom(OperationContext operationContext, DeferredWorkScheduler scheduler) diff --git a/src/HotChocolate/Core/src/Execution/Processing/OperationContext.Execution.cs b/src/HotChocolate/Core/src/Execution/Processing/OperationContext.Execution.cs index f8d49a7f803..72b9b8a891b 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/OperationContext.Execution.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/OperationContext.Execution.cs @@ -39,7 +39,7 @@ public ResultBuilder Result get { AssertInitialized(); - return _resultHelper; + return _resultBuilder; } } diff --git a/src/HotChocolate/Core/src/Execution/Processing/OperationContext.Pooling.cs b/src/HotChocolate/Core/src/Execution/Processing/OperationContext.Pooling.cs index b611b316dd7..7a310e91571 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/OperationContext.Pooling.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/OperationContext.Pooling.cs @@ -16,7 +16,7 @@ internal sealed partial class OperationContext private readonly IFactory _resolverTaskFactory; private readonly WorkScheduler _workScheduler; private readonly DeferredWorkScheduler _deferredWorkScheduler; - private readonly ResultBuilder _resultHelper; + private readonly ResultBuilder _resultBuilder; private readonly PooledPathFactory _pathFactory; private ISchema _schema = default!; private IErrorHandler _errorHandler = default!; @@ -35,15 +35,14 @@ internal sealed partial class OperationContext public OperationContext( IFactory resolverTaskFactory, PooledPathFactory pathFactory, - ResultPool resultPool, - ITypeConverter typeConverter, - DeferredWorkScheduler deferredWorkScheduler) + ResultBuilder resultBuilder, + ITypeConverter typeConverter) { _resolverTaskFactory = resolverTaskFactory; _pathFactory = pathFactory; _workScheduler = new(this); - _deferredWorkScheduler = deferredWorkScheduler; - _resultHelper = new(resultPool); + _deferredWorkScheduler = new(); + _resultBuilder = resultBuilder; Converter = typeConverter; } @@ -75,7 +74,7 @@ internal sealed partial class OperationContext IncludeFlags = _operation.CreateIncludeFlags(variables); _workScheduler.Initialize(batchDispatcher); _deferredWorkScheduler.Initialize(this); - _resultHelper.Initialize(_operation, _errorHandler, _diagnosticEvents); + _resultBuilder.Initialize(_operation, _errorHandler, _diagnosticEvents); } public void InitializeFrom(OperationContext context) @@ -97,7 +96,7 @@ public void InitializeFrom(OperationContext context) IncludeFlags = _operation.CreateIncludeFlags(_variables); _workScheduler.Initialize(_batchDispatcher); _deferredWorkScheduler.InitializeFrom(this, context._deferredWorkScheduler); - _resultHelper.Initialize(_operation, _errorHandler, _diagnosticEvents); + _resultBuilder.Initialize(_operation, _errorHandler, _diagnosticEvents); } public void Clean() @@ -106,7 +105,7 @@ public void Clean() { _pathFactory.Clear(); _workScheduler.Clear(); - _resultHelper.Clear(); + _resultBuilder.Clear(); _schema = default!; _errorHandler = default!; _activator = default!; diff --git a/src/HotChocolate/Core/src/Execution/Processing/Result/ResultBuilder.cs b/src/HotChocolate/Core/src/Execution/Processing/Result/ResultBuilder.cs index 29bf88faa58..74c694c734f 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/Result/ResultBuilder.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/Result/ResultBuilder.cs @@ -221,5 +221,3 @@ public int Compare(IError? x, IError? y) public static readonly ErrorComparer Default = new(); } } - - diff --git a/src/HotChocolate/Fusion/src/Core/Clients/GraphQLClientFactory.cs b/src/HotChocolate/Fusion/src/Core/Clients/GraphQLClientFactory.cs new file mode 100644 index 00000000000..c60f0207719 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Clients/GraphQLClientFactory.cs @@ -0,0 +1,14 @@ +namespace HotChocolate.Fusion.Clients; + +internal sealed class GraphQLClientFactory +{ + private readonly Dictionary _executors; + + public GraphQLClientFactory(IEnumerable executors) + { + _executors = executors.ToDictionary(t => t.SchemaName); + } + + public IGraphQLClient Create(string schemaName) + => _executors[schemaName]; +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/HttpRequestExecutor.cs b/src/HotChocolate/Fusion/src/Core/Clients/GraphQLHttpClient.cs similarity index 77% rename from src/HotChocolate/Fusion/src/Core/Execution/HttpRequestExecutor.cs rename to src/HotChocolate/Fusion/src/Core/Clients/GraphQLHttpClient.cs index 57cb6ba2785..25f8a373632 100644 --- a/src/HotChocolate/Fusion/src/Core/Execution/HttpRequestExecutor.cs +++ b/src/HotChocolate/Fusion/src/Core/Clients/GraphQLHttpClient.cs @@ -2,16 +2,17 @@ using System.Net.Http.Headers; using System.Text; using System.Text.Json; +using HotChocolate.Fusion.Utilities; using HotChocolate.Utilities; -namespace HotChocolate.Fusion.Execution; +namespace HotChocolate.Fusion.Clients; -public sealed class HttpRequestExecutor : IRemoteRequestExecutor +public sealed class GraphQLHttpClient : IGraphQLClient { private readonly IHttpClientFactory _httpClientFactory; private readonly JsonRequestFormatter _formatter = new(); - public HttpRequestExecutor(string schemaName, IHttpClientFactory httpClientFactory) + public GraphQLHttpClient(string schemaName, IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; SchemaName = schemaName; @@ -19,7 +20,7 @@ public HttpRequestExecutor(string schemaName, IHttpClientFactory httpClientFacto public string SchemaName { get; } - public async Task ExecuteAsync(Request request, CancellationToken cancellationToken) + public async Task ExecuteAsync(GraphQLRequest request, CancellationToken cancellationToken) { // todo : this is just a naive dummy implementation using var writer = new ArrayWriter(); @@ -41,10 +42,24 @@ public async Task ExecuteAsync(Request request, CancellationToken canc } var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); - return new Response(document); + return new GraphQLResponse(document); } - private HttpRequestMessage CreateRequestMessage(ArrayWriter writer, Request request) + public Task> ExecuteBatchAsync( + IReadOnlyList requests, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> SubscribeAsync( + GraphQLRequest graphQLRequests, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + private HttpRequestMessage CreateRequestMessage(ArrayWriter writer, GraphQLRequest request) { _formatter.Write(writer, request); diff --git a/src/HotChocolate/Fusion/src/Core/Execution/Request.cs b/src/HotChocolate/Fusion/src/Core/Clients/GraphQLRequest.cs similarity index 82% rename from src/HotChocolate/Fusion/src/Core/Execution/Request.cs rename to src/HotChocolate/Fusion/src/Core/Clients/GraphQLRequest.cs index 71569a48670..1bf2208ed80 100644 --- a/src/HotChocolate/Fusion/src/Core/Execution/Request.cs +++ b/src/HotChocolate/Fusion/src/Core/Clients/GraphQLRequest.cs @@ -1,10 +1,10 @@ using HotChocolate.Language; -namespace HotChocolate.Fusion.Execution; +namespace HotChocolate.Fusion.Clients; -public readonly struct Request +public readonly struct GraphQLRequest { - public Request( + public GraphQLRequest( string schemaName, DocumentNode document, ObjectValueNode? variableValues, diff --git a/src/HotChocolate/Fusion/src/Core/Clients/GraphQLResponse.cs b/src/HotChocolate/Fusion/src/Core/Clients/GraphQLResponse.cs new file mode 100644 index 00000000000..af080ae50a6 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Clients/GraphQLResponse.cs @@ -0,0 +1,42 @@ +using System.Text.Json; + +namespace HotChocolate.Fusion.Clients; + +public sealed class GraphQLResponse : IDisposable +{ + private readonly JsonDocument? _document; + + public GraphQLResponse(JsonDocument? document) + { + _document = document; + + if (_document is not null) + { + if (_document.RootElement.TryGetProperty(ResponseProperties.Data, out var value)) + { + Data = value; + } + + if (_document.RootElement.TryGetProperty(ResponseProperties.Errors, out value)) + { + Errors = value; + } + + if (_document.RootElement.TryGetProperty(ResponseProperties.Extensions, out value)) + { + Extensions = value; + } + } + } + + public JsonElement Data { get; } + + public JsonElement Errors { get; } + + public JsonElement Extensions { get; } + + public void Dispose() + { + _document?.Dispose(); + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Clients/IGraphQLClient.cs b/src/HotChocolate/Fusion/src/Core/Clients/IGraphQLClient.cs new file mode 100644 index 00000000000..65b01025be8 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Clients/IGraphQLClient.cs @@ -0,0 +1,18 @@ +namespace HotChocolate.Fusion.Clients; + +public interface IGraphQLClient +{ + string SchemaName { get; } + + Task ExecuteAsync( + GraphQLRequest request, + CancellationToken cancellationToken); + + Task> ExecuteBatchAsync( + IReadOnlyList requests, + CancellationToken cancellationToken); + + Task> SubscribeAsync( + GraphQLRequest graphQLRequests, + CancellationToken cancellationToken); +} diff --git a/src/HotChocolate/Fusion/src/Core/Clients/ResponseProperties.cs b/src/HotChocolate/Fusion/src/Core/Clients/ResponseProperties.cs new file mode 100644 index 00000000000..97c45d386a7 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Clients/ResponseProperties.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.Fusion.Clients; + +internal static class ResponseProperties +{ + public const string Data = "data"; + public const string Errors = "errors"; + public const string Extensions = "extensions"; +} diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs new file mode 100644 index 00000000000..8358d3edcbb --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/RequestExecutorBuilderExtensions.cs @@ -0,0 +1,62 @@ +using HotChocolate.Execution.Configuration; +using HotChocolate.Fusion.Clients; +using HotChocolate.Fusion.Execution; +using HotChocolate.Fusion.Metadata; +using HotChocolate.Fusion.Pipeline; +using HotChocolate.Fusion.Planning; +using Microsoft.Extensions.DependencyInjection.Extensions; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection; + +public static class RequestExecutorBuilderExtensions +{ + public static IRequestExecutorBuilder AddGraphQLGateway( + this IRequestExecutorBuilder builder, + string serviceConfig, + string sdl) + { + var configuration = ServiceConfiguration.Load(serviceConfig); + + return builder + .AddDocumentFromString(sdl) + .UseField(next => next) + .UseDefaultGatewayPipeline() + .ConfigureSchemaServices( + sc => + { + foreach (var schemaName in configuration.Bindings) + { + sc.AddSingleton( + sp => new GraphQLHttpClient( + schemaName, + sp.GetApplicationService())); + } + + sc.TryAddSingleton(configuration); + sc.TryAddSingleton(); + sc.TryAddSingleton(); + sc.TryAddSingleton(); + sc.TryAddSingleton(); + sc.TryAddSingleton(); + }); + } + + public static IRequestExecutorBuilder UseDefaultGatewayPipeline( + this IRequestExecutorBuilder builder) + { + return builder + .UseInstrumentations() + .UseExceptions() + .UseTimeout() + .UseDocumentCache() + .UseDocumentParser() + .UseDocumentValidation() + .UseOperationCache() + .UseOperationComplexityAnalyzer() + .UseOperationResolver() + .UseOperationVariableCoercion() + .UseRequest() + .UseRequest(); + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/ArgumentContext.cs b/src/HotChocolate/Fusion/src/Core/Execution/ArgumentContext.cs deleted file mode 100644 index 0cf3556e01f..00000000000 --- a/src/HotChocolate/Fusion/src/Core/Execution/ArgumentContext.cs +++ /dev/null @@ -1,65 +0,0 @@ -using HotChocolate.Utilities; - -namespace HotChocolate.Fusion.Execution; - -internal readonly struct ArgumentContext -{ - private readonly List _items; - - private ArgumentContext(List items) - { - _items = items; - } - - public ArgumentContext Push(IReadOnlyList arguments) - { - var items = new List(); - - foreach (var argument in arguments) - { - items.Add(new Item(0, argument)); - } - - if (_items is not null) - { - foreach (var currentItem in _items) - { - if (currentItem.Level > 0) - { - continue; - } - - var add = true; - - foreach (var newItem in items) - { - if (currentItem.Argument.Name.EqualsOrdinal(newItem.Argument.Name)) - { - add = false; - break; - } - } - - if (add) - { - items.Add(new Item(currentItem.Level + 1, currentItem.Argument)); - } - } - } - - return new ArgumentContext(items); - } - - private readonly struct Item - { - public Item(int level, Argument argument) - { - Level = level; - Argument = argument; - } - - public int Level { get; } - - public Argument Argument { get; } - } -} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/RemoteExecutorContext.cs b/src/HotChocolate/Fusion/src/Core/Execution/FederatedQueryContext.cs similarity index 90% rename from src/HotChocolate/Fusion/src/Core/Execution/RemoteExecutorContext.cs rename to src/HotChocolate/Fusion/src/Core/Execution/FederatedQueryContext.cs index 7478bca6b52..8d5f6e7c245 100644 --- a/src/HotChocolate/Fusion/src/Core/Execution/RemoteExecutorContext.cs +++ b/src/HotChocolate/Fusion/src/Core/Execution/FederatedQueryContext.cs @@ -3,9 +3,9 @@ namespace HotChocolate.Fusion.Execution; -internal sealed class RemoteExecutorContext +internal sealed class FederatedQueryContext { - public RemoteExecutorContext( + public FederatedQueryContext( ISchema schema, ResultBuilder result, IOperation operation, diff --git a/src/HotChocolate/Fusion/src/Core/Execution/RemoteQueryExecutor.cs b/src/HotChocolate/Fusion/src/Core/Execution/FederatedQueryExecutor.cs similarity index 85% rename from src/HotChocolate/Fusion/src/Core/Execution/RemoteQueryExecutor.cs rename to src/HotChocolate/Fusion/src/Core/Execution/FederatedQueryExecutor.cs index 0a2c5e8ea7f..49bdacdd7c2 100644 --- a/src/HotChocolate/Fusion/src/Core/Execution/RemoteQueryExecutor.cs +++ b/src/HotChocolate/Fusion/src/Core/Execution/FederatedQueryExecutor.cs @@ -2,31 +2,38 @@ using System.Text.Json; using HotChocolate.Execution; using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Clients; +using HotChocolate.Fusion.Metadata; using HotChocolate.Language; using HotChocolate.Types; -using static HotChocolate.Fusion.Utilities.JsonValueToGraphQLValueConverter; +using IType = HotChocolate.Types.IType; using ObjectType = HotChocolate.Fusion.Metadata.ObjectType; +using static HotChocolate.Fusion.Utilities.JsonValueToGraphQLValueConverter; namespace HotChocolate.Fusion.Execution; -internal sealed class RemoteQueryExecutor +internal sealed class FederatedQueryExecutor { - private readonly Metadata.ServiceConfiguration _serviceConfiguration; - private readonly RemoteRequestExecutorFactory _executorFactory; + private readonly ServiceConfiguration _serviceConfiguration; + private readonly GraphQLClientFactory _executorFactory; - public RemoteQueryExecutor(Metadata.ServiceConfiguration serviceConfiguration, RemoteRequestExecutorFactory executorFactory) + public FederatedQueryExecutor( + ServiceConfiguration serviceConfiguration, + GraphQLClientFactory executorFactory) { - _serviceConfiguration = serviceConfiguration; - _executorFactory = executorFactory; + _serviceConfiguration = serviceConfiguration ?? + throw new ArgumentNullException(nameof(serviceConfiguration)); + _executorFactory = executorFactory ?? + throw new ArgumentNullException(nameof(executorFactory)); } public async Task ExecuteAsync( - RemoteExecutorContext context, + FederatedQueryContext context, CancellationToken cancellationToken = default) { var rootSelectionSet = context.Operation.RootSelectionSet; var rootResult = context.Result.RentObject(rootSelectionSet.Selections.Count); - var rootWorkItem = new WorkItem(rootSelectionSet, new ArgumentContext(), rootResult); + var rootWorkItem = new WorkItem(rootSelectionSet, rootResult); context.Fetch.Add(rootWorkItem); while (context.Fetch.Count > 0) @@ -43,7 +50,7 @@ public RemoteQueryExecutor(Metadata.ServiceConfiguration serviceConfiguration, R } // note: this is inefficient and we want to group here, for now we just want to get it working. - private async Task FetchAsync(RemoteExecutorContext context, CancellationToken ct) + private async Task FetchAsync(FederatedQueryContext context, CancellationToken ct) { foreach (var workItem in context.Fetch) { @@ -87,21 +94,19 @@ private async Task FetchAsync(RemoteExecutorContext context, CancellationToken c } private void ComposeResult( - RemoteExecutorContext context, + FederatedQueryContext context, WorkItem workItem) => ComposeResult( context, workItem.SelectionSet.Selections, workItem.SelectionResults, - workItem.Result, - workItem.Variables); + workItem.Result); private void ComposeResult( - RemoteExecutorContext context, + FederatedQueryContext context, IReadOnlyList selections, IReadOnlyList selectionResults, - ObjectResult selectionSetResult, - ArgumentContext variables) + ObjectResult selectionSetResult) { for (var i = 0; i < selections.Count; i++) { @@ -132,24 +137,23 @@ private async Task FetchAsync(RemoteExecutorContext context, CancellationToken c selectionSetResult.SetValueUnsafe( i, selection.ResponseName, - ComposeObject(context, selection, selectionResult, variables)); + ComposeObject(context, selection, selectionResult)); } else { selectionSetResult.SetValueUnsafe( i, selection.ResponseName, - ComposeList(context, selection, selectionResult, variables, selection.Type)); + ComposeList(context, selection, selectionResult, selection.Type)); } } } private ListResult? ComposeList( - RemoteExecutorContext context, + FederatedQueryContext context, ISelection selection, SelectionResult selectionResult, - ArgumentContext variables, - Types.IType type) + IType type) { if (selectionResult.IsNull()) { @@ -172,8 +176,7 @@ private async Task FetchAsync(RemoteExecutorContext context, CancellationToken c ComposeObject( context, selection, - new SelectionResult(new JsonResult(schemaName, item)), - variables)); + new SelectionResult(new JsonResult(schemaName, item)))); } } else @@ -185,7 +188,6 @@ private async Task FetchAsync(RemoteExecutorContext context, CancellationToken c context, selection, new SelectionResult(new JsonResult(schemaName, item)), - variables, elementType)); } } @@ -194,10 +196,9 @@ private async Task FetchAsync(RemoteExecutorContext context, CancellationToken c } private ObjectResult? ComposeObject( - RemoteExecutorContext context, + FederatedQueryContext context, ISelection selection, - SelectionResult selectionResult, - ArgumentContext variables) + SelectionResult selectionResult) { if (selectionResult.IsNull()) { @@ -231,7 +232,7 @@ private async Task FetchAsync(RemoteExecutorContext context, CancellationToken c typeMetadata); context.Fetch.Add( - new WorkItem(fetchArguments, selectionSet, variables, result) + new WorkItem(fetchArguments, selectionSet, result) { SelectionResults = { [0] = selectionResult } }); @@ -241,29 +242,14 @@ private async Task FetchAsync(RemoteExecutorContext context, CancellationToken c var selections = selectionSet.Selections; var childSelectionResults = new SelectionResult[selections.Count]; ExtractSelectionResults(selectionResult, selections, childSelectionResults); - - /* - var childVariables = CreateVariables( - context, - selection, - selectionResult, - typeMetadata, - variables); - */ - - ComposeResult( - context, - selectionSet.Selections, - childSelectionResults, - result, - variables); + ComposeResult(context, selectionSet.Selections, childSelectionResults, result); } return result; } private IReadOnlyList CreateArguments( - RemoteExecutorContext context, + FederatedQueryContext context, ISelection selection, SelectionResult selectionResult, ObjectType typeMetadata) @@ -271,14 +257,6 @@ private async Task FetchAsync(RemoteExecutorContext context, CancellationToken c return new List(); } - private ArgumentContext CreateVariables( - RemoteExecutorContext context, - ISelection selection, - SelectionResult selectionResult, - ObjectType typeMetadata, - ArgumentContext variables) - => throw new NotImplementedException(); - private static void ExtractSelectionResults( SelectionResult parent, IReadOnlyList selections, diff --git a/src/HotChocolate/Fusion/src/Core/Execution/IRemoteRequestExecutor.cs b/src/HotChocolate/Fusion/src/Core/Execution/IRemoteRequestExecutor.cs deleted file mode 100644 index cf151405e65..00000000000 --- a/src/HotChocolate/Fusion/src/Core/Execution/IRemoteRequestExecutor.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace HotChocolate.Fusion.Execution; - -public interface IRemoteRequestExecutor -{ - string SchemaName { get; } - - Task ExecuteAsync(Request request, CancellationToken cancellationToken); -} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/RemoteRequestExecutorFactory.cs b/src/HotChocolate/Fusion/src/Core/Execution/RemoteRequestExecutorFactory.cs deleted file mode 100644 index 395084171ef..00000000000 --- a/src/HotChocolate/Fusion/src/Core/Execution/RemoteRequestExecutorFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace HotChocolate.Fusion.Execution; - -internal sealed class RemoteRequestExecutorFactory -{ - private readonly Dictionary _executors; - - public RemoteRequestExecutorFactory(IEnumerable executors) - { - _executors = executors.ToDictionary(t => t.SchemaName); - } - - public IRemoteRequestExecutor Create(string schemaName) - => _executors[schemaName]; -} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/RequestHandler.cs b/src/HotChocolate/Fusion/src/Core/Execution/RequestHandler.cs index 44baff946b8..79883d6de61 100644 --- a/src/HotChocolate/Fusion/src/Core/Execution/RequestHandler.cs +++ b/src/HotChocolate/Fusion/src/Core/Execution/RequestHandler.cs @@ -1,6 +1,8 @@ using System.Text.Json; using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Clients; using HotChocolate.Language; +using GraphQLRequest = HotChocolate.Fusion.Clients.GraphQLRequest; namespace HotChocolate.Fusion.Execution; @@ -42,7 +44,7 @@ internal sealed class RequestHandler /// public IReadOnlyList Requires { get; } - public Request CreateRequest(IReadOnlyDictionary variableValues) + public GraphQLRequest CreateRequest(IReadOnlyDictionary variableValues) { ObjectValueNode? vars = null; @@ -69,10 +71,10 @@ public Request CreateRequest(IReadOnlyDictionary variableVal vars ??= new ObjectValueNode(fields); } - return new Request(SchemaName, Document, vars, null); + return new GraphQLRequest(SchemaName, Document, vars, null); } - public JsonElement UnwrapResult(Response response) + public JsonElement UnwrapResult(GraphQLResponse response) { if (_path.Count == 0) { diff --git a/src/HotChocolate/Fusion/src/Core/Execution/Response.cs b/src/HotChocolate/Fusion/src/Core/Execution/Response.cs deleted file mode 100644 index ebbe60cac09..00000000000 --- a/src/HotChocolate/Fusion/src/Core/Execution/Response.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Text.Json; - -namespace HotChocolate.Fusion.Execution; - -public sealed class Response : IDisposable -{ - private readonly JsonDocument? _document; - - public Response(JsonDocument? document) - { - _document = document; - - if (_document is not null && _document.RootElement.TryGetProperty("data", out var data)) - { - Data = data; - } - } - - public JsonElement Data { get; } - - public void Dispose() - { - _document?.Dispose(); - } -} diff --git a/src/HotChocolate/Fusion/src/Core/Execution/WorkItem.cs b/src/HotChocolate/Fusion/src/Core/Execution/WorkItem.cs index 3451b9bcf35..7f1ba9482d3 100644 --- a/src/HotChocolate/Fusion/src/Core/Execution/WorkItem.cs +++ b/src/HotChocolate/Fusion/src/Core/Execution/WorkItem.cs @@ -6,20 +6,17 @@ internal struct WorkItem { public WorkItem( ISelectionSet selectionSet, - ArgumentContext variables, ObjectResult result) - : this(Array.Empty(), selectionSet, variables, result) { } + : this(Array.Empty(), selectionSet, result) { } public WorkItem( IReadOnlyList arguments, ISelectionSet selectionSet, - ArgumentContext variables, ObjectResult result) { Arguments = arguments; SelectionSet = selectionSet; SelectionResults = Array.Empty(); - Variables = variables; Result = result; SelectionResults = new SelectionResult[selectionSet.Selections.Count]; } @@ -30,7 +27,5 @@ internal struct WorkItem public SelectionResult[] SelectionResults { get; } - public ArgumentContext Variables { get; set; } - public ObjectResult Result { get; } } diff --git a/src/HotChocolate/Fusion/src/Core/Pipeline/OperationExecutionMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Pipeline/OperationExecutionMiddleware.cs new file mode 100644 index 00000000000..5a0b5e933cc --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Pipeline/OperationExecutionMiddleware.cs @@ -0,0 +1,60 @@ +using HotChocolate.Execution; +using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Execution; +using HotChocolate.Fusion.Planning; + +namespace HotChocolate.Fusion.Pipeline; + +internal sealed class OperationExecutionMiddleware +{ + private readonly RequestDelegate _next; + private readonly FederatedQueryExecutor _executor; + private readonly ISchema _schema; + + public OperationExecutionMiddleware( + RequestDelegate next, + [SchemaService] FederatedQueryExecutor executor, + [SchemaService] ISchema schema) + { + _next = next ?? + throw new ArgumentNullException(nameof(next)); + _executor = executor ?? + throw new ArgumentNullException(nameof(executor)); + _schema = schema; + } + + public async ValueTask InvokeAsync(IRequestContext context, ResultBuilder resultBuilder) + { + if (context.Operation is not null && + context.Variables is not null && + context.ContextData.TryGetValue(PipelineProperties.QueryPlan, out var value) && + value is QueryPlan queryPlan) + { + resultBuilder.Initialize( + context.Operation, + context.ErrorHandler, + context.DiagnosticEvents); + + var federatedQueryContext = new FederatedQueryContext( + _schema, + resultBuilder, + context.Operation, + queryPlan, + new HashSet( + queryPlan.ExecutionNodes + .OfType() + .Select(t => t.Handler.SelectionSet))); + + context.Result = await _executor.ExecuteAsync( + federatedQueryContext, + context.RequestAborted) + .ConfigureAwait(false); + + await _next(context).ConfigureAwait(false); + } + else + { + context.Result = ErrorHelper.StateInvalidForOperationExecution(); + } + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Pipeline/PipelineProperties.cs b/src/HotChocolate/Fusion/src/Core/Pipeline/PipelineProperties.cs new file mode 100644 index 00000000000..ef227d7134c --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Pipeline/PipelineProperties.cs @@ -0,0 +1,6 @@ +namespace HotChocolate.Fusion.Pipeline; + +internal static class PipelineProperties +{ + public const string QueryPlan = "HotChocolate.Fusion.Pipeline.QueryPlan"; +} diff --git a/src/HotChocolate/Fusion/src/Core/Pipeline/QueryPlanMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Pipeline/QueryPlanMiddleware.cs new file mode 100644 index 00000000000..0ee1d5e0ede --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Pipeline/QueryPlanMiddleware.cs @@ -0,0 +1,48 @@ +using HotChocolate.Execution; +using HotChocolate.Fusion.Planning; + +namespace HotChocolate.Fusion.Pipeline; + +/// +/// Creates the query plan for the federated request. +/// +internal sealed class QueryPlanMiddleware +{ + private readonly RequestDelegate _next; + private readonly RequestPlaner _requestPlaner; + private readonly RequirementsPlaner _requirementsPlaner; + private readonly ExecutionPlanBuilder _executionPlanBuilder; + + public QueryPlanMiddleware( + RequestDelegate next, + [SchemaService] RequestPlaner requestPlaner, + [SchemaService] RequirementsPlaner requirementsPlaner, + [SchemaService] ExecutionPlanBuilder executionPlanBuilder) + { + _next = next ?? + throw new ArgumentNullException(nameof(next)); + _requestPlaner = requestPlaner ?? + throw new ArgumentNullException(nameof(requestPlaner)); + _requirementsPlaner = requirementsPlaner ?? + throw new ArgumentNullException(nameof(requirementsPlaner)); + _executionPlanBuilder = executionPlanBuilder ?? + throw new ArgumentNullException(nameof(executionPlanBuilder)); + } + + public async ValueTask InvokeAsync(IRequestContext context) + { + if (context.Operation is not null && context.Variables is not null) + { + var queryPlanContext = new QueryPlanContext(context.Operation); + _requestPlaner.Plan(queryPlanContext); + _requirementsPlaner.Plan(queryPlanContext); + var queryPlan = _executionPlanBuilder.Build(queryPlanContext); + context.ContextData[PipelineProperties.QueryPlan] = queryPlan; + await _next(context).ConfigureAwait(false); + } + else + { + context.Result = ErrorHelper.StateInvalidForOperationExecution(); + } + } +} diff --git a/src/HotChocolate/Fusion/src/Core/Utilities/JsonQueryResultFormatter.cs b/src/HotChocolate/Fusion/src/Core/Utilities/JsonQueryResultFormatter.cs index 9a7ef81c2c3..06d3f831135 100644 --- a/src/HotChocolate/Fusion/src/Core/Utilities/JsonQueryResultFormatter.cs +++ b/src/HotChocolate/Fusion/src/Core/Utilities/JsonQueryResultFormatter.cs @@ -12,7 +12,7 @@ namespace HotChocolate.Fusion.Utilities; -public sealed class JsonQueryResultFormatter : IQueryResultFormatter +internal sealed class JsonQueryResultFormatter : IQueryResultFormatter { private readonly JsonWriterOptions _options; diff --git a/src/HotChocolate/Fusion/src/Core/Execution/JsonRequestFormatter.cs b/src/HotChocolate/Fusion/src/Core/Utilities/JsonRequestFormatter.cs similarity index 92% rename from src/HotChocolate/Fusion/src/Core/Execution/JsonRequestFormatter.cs rename to src/HotChocolate/Fusion/src/Core/Utilities/JsonRequestFormatter.cs index 9e51ebf0a64..52af944039f 100644 --- a/src/HotChocolate/Fusion/src/Core/Execution/JsonRequestFormatter.cs +++ b/src/HotChocolate/Fusion/src/Core/Utilities/JsonRequestFormatter.cs @@ -1,9 +1,12 @@ using System.Buffers; using System.Text.Encodings.Web; using System.Text.Json; +using HotChocolate.Fusion.Clients; +using HotChocolate.Fusion.Execution; using HotChocolate.Language; +using GraphQLRequest = HotChocolate.Fusion.Clients.GraphQLRequest; -namespace HotChocolate.Fusion.Execution; +namespace HotChocolate.Fusion.Utilities; internal sealed class JsonRequestFormatter { @@ -13,7 +16,7 @@ internal sealed class JsonRequestFormatter Encoder = JavaScriptEncoder.Default }; - public void Write(IBufferWriter bufferWriter, Request request) + public void Write(IBufferWriter bufferWriter, GraphQLRequest request) { using var jsonWriter = new Utf8JsonWriter(bufferWriter, _options); jsonWriter.WriteStartObject(); diff --git a/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs index bc7570bd981..7a34a312fef 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/RemoteQueryExecutorTests.cs @@ -1,6 +1,7 @@ using CookieCrumble; using HotChocolate.Execution; using HotChocolate.Execution.Processing; +using HotChocolate.Fusion.Clients; using HotChocolate.Fusion.Execution; using HotChocolate.Fusion.Planning; using HotChocolate.Fusion.Utilities; @@ -18,6 +19,7 @@ public class RemoteQueryExecutorTests [Fact] public async Task Do() { + // arrange using var server1 = _testServerFactory.Create( s => s .AddRouting() @@ -38,7 +40,30 @@ public async Task Do() .UseRouting() .UseEndpoints(endpoints => endpoints.MapGraphQL())); - // arrange + var clients = new Dictionary> + { + { + "a", + () => + { + var httpClient = server1.CreateClient(); + httpClient.BaseAddress = new Uri("http://localhost:5000/graphql"); + return httpClient; + } + }, + { + "b", + () => + { + var httpClient = server2.CreateClient(); + httpClient.BaseAddress = new Uri("http://localhost:5000/graphql"); + return httpClient; + } + }, + }; + + var clientFactory = new MockHttpClientFactory(clients); + const string sdl = @" type Query { personById(id: ID!) : Person @@ -51,7 +76,7 @@ public async Task Do() friends: [Person!] }"; - const string serviceDefinition = @" + const string serviceConfiguration = @" type Query { personById(id: ID!): Person @variable(name: ""personId"", argument: ""id"") @@ -84,86 +109,29 @@ type Person query: Query }"; - var schema = await new ServiceCollection() - .AddGraphQL() - .AddDocumentFromString(sdl) - .UseField(n => n) - .BuildSchemaAsync(); - - var serviceConfig = Metadata.ServiceConfiguration.Load(serviceDefinition); - - var request = - Parse( - @"query GetPersonById { - personById(id: 4) { + var request = Parse( + @"query GetPersonById { + personById(id: 4) { + name + friends { name - friends { - name - bio - } + bio } - }"); - - var operationCompiler = new OperationCompiler(new()); - var operation = operationCompiler.Compile( - "abc", - (OperationDefinitionNode)request.Definitions.First(), - schema.QueryType, - request, - schema); - - // act - var queryPlanContext = new QueryPlanContext(operation); - var requestPlaner = new RequestPlaner(serviceConfig); - var requirementsPlaner = new RequirementsPlaner(); - var executionPlanBuilder = new ExecutionPlanBuilder(serviceConfig, schema); - - requestPlaner.Plan(queryPlanContext); - requirementsPlaner.Plan(queryPlanContext); - var queryPlan = executionPlanBuilder.Build(queryPlanContext); - - var clients = new Dictionary> - { - { - "a", - () => - { - var httpClient = server1.CreateClient(); - httpClient.BaseAddress = new Uri("http://localhost:5000/graphql"); - return httpClient; } - }, - { - "b", - () => - { - var httpClient = server2.CreateClient(); - httpClient.BaseAddress = new Uri("http://localhost:5000/graphql"); - return httpClient; - } - }, - }; - - var clientFactory = new MockHttpClientFactory(clients); - - var executor1 = new HttpRequestExecutor("a", clientFactory); - var executor2 = new HttpRequestExecutor("b", clientFactory); + }"); - var executorFactory = new RemoteRequestExecutorFactory(new[] { executor1, executor2 }); + var executor = await new ServiceCollection() + .AddSingleton(clientFactory) + .AddGraphQLServer() + .AddGraphQLGateway(serviceConfiguration, sdl) + .BuildRequestExecutorAsync(); - var executor = new RemoteQueryExecutor(serviceConfig, executorFactory); - var context = new RemoteExecutorContext( - schema, - new ResultBuilder( - new ResultPool( - new ObjectResultPool(32, 32), - new ListResultPool(32, 32))), - operation, - queryPlan, - new HashSet(queryPlan.ExecutionNodes.OfType().Select(t => t.Handler.SelectionSet))); - - - var result = await executor.ExecuteAsync(context); + // act + var result = await executor.ExecuteAsync( + QueryRequestBuilder + .New() + .SetQuery(request) + .Create()); // assert var index = 0; @@ -172,6 +140,7 @@ type Person snapshot.Add(request, "User Request"); + /* foreach (var executionNode in queryPlan.ExecutionNodes) { if (executionNode is RequestNode rn) @@ -179,8 +148,9 @@ type Person snapshot.Add(rn.Handler.Document, $"Request {++index}"); } } + */ - snapshot.Add(formatter.Format(result), "Result"); + snapshot.Add(formatter.Format((QueryResult)result), "Result"); await snapshot.MatchAsync(); }