diff --git a/src/All.slnx b/src/All.slnx index b72a38e5519..64f38dd7e4e 100644 --- a/src/All.slnx +++ b/src/All.slnx @@ -40,12 +40,13 @@ + + - @@ -67,12 +68,12 @@ - + - + @@ -95,8 +96,8 @@ - + @@ -174,6 +175,7 @@ + @@ -182,6 +184,7 @@ + @@ -230,12 +233,12 @@ + - - + @@ -275,9 +278,9 @@ - + @@ -326,4 +329,4 @@ - + \ No newline at end of file diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/OperationResultSnapshotValueFormatter.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/OperationResultSnapshotValueFormatter.cs index e326d82e2a6..b3db816d674 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/OperationResultSnapshotValueFormatter.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/OperationResultSnapshotValueFormatter.cs @@ -9,44 +9,46 @@ internal sealed class OperationResultSnapshotValueFormatter : SnapshotValueForma { protected override void Format(IBufferWriter snapshot, OperationResult value) { - var next = false; + var writer = new Utf8JsonWriter( + snapshot, + new JsonWriterOptions + { + Indented = true, + SkipValidation = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }); + + writer.WriteStartObject(); if (value.RequestIndex.HasValue) { - snapshot.Append("RequestIndex: "); - snapshot.Append(value.RequestIndex.Value.ToString()); - next = true; + writer.WriteNumber("requestIndex", value.RequestIndex.Value); } if (value.VariableIndex.HasValue) { - snapshot.AppendLine(appendWhenTrue: next); - snapshot.Append("VariableIndex: "); - snapshot.Append(value.VariableIndex.Value.ToString()); - next = true; + writer.WriteNumber("variableIndex", value.VariableIndex.Value); } if (value.Data.ValueKind is JsonValueKind.Object) { - snapshot.AppendLine(appendWhenTrue: next); - snapshot.Append("Data: "); - snapshot.Append(value.Data.ToString()); - next = true; + writer.WritePropertyName("data"); + value.Data.WriteTo(writer); } if (value.Errors.ValueKind is JsonValueKind.Array) { - snapshot.AppendLine(appendWhenTrue: next); - snapshot.Append("Errors: "); - snapshot.Append(value.Errors.ToString()); - next = true; + writer.WritePropertyName("errors"); + value.Errors.WriteTo(writer); } if (value.Extensions.ValueKind is JsonValueKind.Object) { - snapshot.AppendLine(appendWhenTrue: next); - snapshot.Append("Extensions: "); - snapshot.Append(value.Extensions.ToString()); + writer.WritePropertyName("extensions"); + value.Extensions.WriteTo(writer); } + + writer.WriteEndObject(); + writer.Flush(); } } diff --git a/src/HotChocolate/AspNetCore/HotChocolate.AspNetCore.slnx b/src/HotChocolate/AspNetCore/HotChocolate.AspNetCore.slnx index c2295ee8086..2c97a1be58c 100644 --- a/src/HotChocolate/AspNetCore/HotChocolate.AspNetCore.slnx +++ b/src/HotChocolate/AspNetCore/HotChocolate.AspNetCore.slnx @@ -3,6 +3,7 @@ + diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/AcceptMediaType.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/AcceptMediaType.cs similarity index 76% rename from src/HotChocolate/AspNetCore/src/AspNetCore/AcceptMediaType.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/AcceptMediaType.cs index f3c42e68015..81fcec0d576 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/AcceptMediaType.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/AcceptMediaType.cs @@ -1,4 +1,3 @@ -using HotChocolate.Utilities; using Microsoft.Extensions.Primitives; namespace HotChocolate.AspNetCore; @@ -48,45 +47,47 @@ internal AcceptMediaType( Charset = charset.HasValue ? charset.Value : null; IsUtf8 = Charset?.Equals("utf-8", StringComparison.OrdinalIgnoreCase) ?? true; - if (Type.EqualsOrdinal(ContentType.Types.All) && SubType.EqualsOrdinal(ContentType.Types.All)) + if (Type.Equals(ContentType.Types.All, StringComparison.Ordinal) + && SubType.Equals(ContentType.Types.All, StringComparison.Ordinal)) { Kind = AcceptMediaTypeKind.All; } - else if (Type.EqualsOrdinal(ContentType.Types.Application)) + else if (Type.Equals(ContentType.Types.Application, StringComparison.Ordinal)) { - if (SubType.EqualsOrdinal(ContentType.Types.All)) + if (SubType.Equals(ContentType.Types.All, StringComparison.Ordinal)) { Kind = AcceptMediaTypeKind.AllApplication; } - else if (SubType.EqualsOrdinal(ContentType.SubTypes.GraphQLResponse)) + else if (SubType.Equals(ContentType.SubTypes.GraphQLResponse, StringComparison.Ordinal)) { Kind = AcceptMediaTypeKind.ApplicationGraphQL; } - else if (SubType.EqualsOrdinal(ContentType.SubTypes.GraphQLResponseStream)) + else if (SubType.Equals(ContentType.SubTypes.GraphQLResponseStream, StringComparison.Ordinal)) { Kind = AcceptMediaTypeKind.ApplicationGraphQLStream; } - else if (SubType.EqualsOrdinal(ContentType.SubTypes.Json)) + else if (SubType.Equals(ContentType.SubTypes.Json, StringComparison.Ordinal)) { Kind = AcceptMediaTypeKind.ApplicationJson; } - else if (SubType.EqualsOrdinal(ContentType.SubTypes.JsonLines)) + else if (SubType.Equals(ContentType.SubTypes.JsonLines, StringComparison.Ordinal)) { Kind = AcceptMediaTypeKind.ApplicationJsonLines; } } - else if (Type.EqualsOrdinal(ContentType.Types.MultiPart)) + else if (Type.Equals(ContentType.Types.MultiPart, StringComparison.Ordinal)) { - if (SubType.EqualsOrdinal(ContentType.Types.All)) + if (SubType.Equals(ContentType.Types.All, StringComparison.Ordinal)) { Kind = AcceptMediaTypeKind.AllMultiPart; } - else if (SubType.EqualsOrdinal(ContentType.SubTypes.Mixed)) + else if (SubType.Equals(ContentType.SubTypes.Mixed, StringComparison.Ordinal)) { Kind = AcceptMediaTypeKind.MultiPartMixed; } } - else if (Type.EqualsOrdinal(ContentType.Types.Text) && SubType.EqualsOrdinal(ContentType.SubTypes.EventStream)) + else if (Type.Equals(ContentType.Types.Text, StringComparison.Ordinal) + && SubType.Equals(ContentType.SubTypes.EventStream, StringComparison.Ordinal)) { Kind = AcceptMediaTypeKind.EventStream; } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/AcceptMediaTypeKind.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/AcceptMediaTypeKind.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/AcceptMediaTypeKind.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/AcceptMediaTypeKind.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/AllowedGetOperations.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/AllowedGetOperations.cs similarity index 93% rename from src/HotChocolate/AspNetCore/src/AspNetCore/AllowedGetOperations.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/AllowedGetOperations.cs index 193f94fdc26..fda0d645e76 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/AllowedGetOperations.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/AllowedGetOperations.cs @@ -3,6 +3,7 @@ namespace HotChocolate.AspNetCore; [Flags] public enum AllowedGetOperations { + None = 0, Query = 1, Mutation = 2, Subscription = 4, diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/ContentType.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/ContentType.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/ContentType.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/ContentType.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultHttpMethod.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/DefaultHttpMethod.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/DefaultHttpMethod.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/DefaultHttpMethod.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/ExecutorSession.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/ExecutorSession.cs new file mode 100644 index 00000000000..47298358679 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/ExecutorSession.cs @@ -0,0 +1,165 @@ +using System.Net; +using HotChocolate.AspNetCore.Formatters; +using HotChocolate.AspNetCore.Instrumentation; +using HotChocolate.AspNetCore.Parsers; +using HotChocolate.Features; +using HotChocolate.Language; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.AspNetCore; + +public sealed class ExecutorSession + : IRequestExecutor + , IHttpRequestInterceptor + , IErrorHandler +{ + private readonly IRequestExecutor _executor; + private readonly IErrorHandler _errorHandler; + private readonly IHttpRequestInterceptor _requestInterceptor; + private readonly ISocketSessionInterceptor _socketSessionInterceptor; + private readonly IHttpRequestParser _requestParser; + private readonly IHttpResponseFormatter _responseFormatter; + private readonly IServerDiagnosticEvents _diagnosticEvents; + + public ExecutorSession(IRequestExecutor executor) + { + ArgumentNullException.ThrowIfNull(executor); + + _executor = executor; + _errorHandler = executor.Schema.Services.GetRequiredService(); + _requestInterceptor = executor.Schema.Services.GetRequiredService(); + _socketSessionInterceptor = executor.Schema.Services.GetRequiredService(); + _responseFormatter = executor.Schema.Services.GetRequiredService(); + _requestParser = executor.Schema.Services.GetRequiredService(); + _diagnosticEvents = executor.Schema.Services.GetRequiredService(); + } + + public ISocketSessionInterceptor SocketSessionInterceptor => _socketSessionInterceptor; + + public IHttpResponseFormatter ResponseFormatter => _responseFormatter; + + public IHttpRequestParser RequestParser => _requestParser; + + public IServerDiagnosticEvents DiagnosticEvents => _diagnosticEvents; + + public ulong Version => _executor.Version; + + public ISchemaDefinition Schema => _executor.Schema; + + public IFeatureCollection Features => _executor.Features; + + public Task ExecuteAsync( + IOperationRequest request, + CancellationToken cancellationToken = default) + => _executor.ExecuteAsync(request, cancellationToken); + + public Task ExecuteBatchAsync( + OperationRequestBatch requestBatch, + CancellationToken cancellationToken = default) + => _executor.ExecuteBatchAsync(requestBatch, cancellationToken); + + public IError Handle(IError error) + => _errorHandler.Handle(error); + + public ValueTask OnCreateAsync( + HttpContext context, + OperationRequestBuilder requestBuilder, + CancellationToken cancellationToken) + => _requestInterceptor.OnCreateAsync(context, _executor, requestBuilder, cancellationToken); + + public ValueTask OnCreateAsync( + HttpContext context, + IRequestExecutor requestExecutor, + OperationRequestBuilder requestBuilder, + CancellationToken cancellationToken) + => _requestInterceptor.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken); + + public async Task ExecuteSingleAsync( + HttpContext context, + GraphQLRequest request, + RequestFlags flags) + { + _diagnosticEvents.StartSingleRequest(context, request); + + var requestBuilder = OperationRequestBuilder.From(request); + requestBuilder.SetFlags(flags); + + await _requestInterceptor.OnCreateAsync(context, _executor, requestBuilder, context.RequestAborted); + + return await _executor.ExecuteAsync(requestBuilder.Build(), context.RequestAborted); + } + + public async Task ExecuteOperationBatchAsync( + HttpContext context, + GraphQLRequest request, + RequestFlags flags, + IReadOnlyList operationNames) + { + _diagnosticEvents.StartOperationBatchRequest(context, request, operationNames); + + var requestBatch = new IOperationRequest[operationNames.Count]; + + for (var i = 0; i < operationNames.Count; i++) + { + var requestBuilder = OperationRequestBuilder.From(request); + requestBuilder.SetOperationName(operationNames[i]); + requestBuilder.SetFlags(flags); + + await _requestInterceptor.OnCreateAsync(context, _executor, requestBuilder, context.RequestAborted); + + requestBatch[i] = requestBuilder.Build(); + } + + return await _executor.ExecuteBatchAsync( + new OperationRequestBatch(requestBatch, services: context.RequestServices), + cancellationToken: context.RequestAborted); + } + + public async Task ExecuteBatchAsync( + HttpContext context, + IReadOnlyList requests, + RequestFlags flags) + { + _diagnosticEvents.StartBatchRequest(context, requests); + + var requestBatch = new IOperationRequest[requests.Count]; + + for (var i = 0; i < requests.Count; i++) + { + var requestBuilder = OperationRequestBuilder.From(requests[i]); + requestBuilder.SetFlags(flags); + + await _requestInterceptor.OnCreateAsync(context, _executor, requestBuilder, context.RequestAborted); + + requestBatch[i] = requestBuilder.Build(); + } + + return await _executor.ExecuteBatchAsync( + new OperationRequestBatch(requestBatch, services: context.RequestServices), + cancellationToken: context.RequestAborted); + } + + public ValueTask WriteResultAsync( + HttpContext context, + IExecutionResult result, + AcceptMediaType[] acceptMediaTypes, + HttpStatusCode? statusCode = null) + => _responseFormatter.FormatAsync( + context.Response, + result, + acceptMediaTypes, + statusCode, + context.RequestAborted); + + public async Task WriteSchemaAsync( + HttpContext context) + => await _responseFormatter.FormatAsync( + context.Response, + Schema, + Version, + context.RequestAborted); + + public RequestFlags CreateRequestFlags(AcceptMediaType[] acceptMediaTypes) + => _responseFormatter.CreateRequestFlags(acceptMediaTypes); +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/EndpointRouteBuilderExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs similarity index 82% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/EndpointRouteBuilderExtensions.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs index ad07c4d7f77..fb91b486056 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs @@ -1,12 +1,15 @@ using System.Diagnostics.CodeAnalysis; using ChilliCream.Nitro.App; +using HotChocolate.AspNetCore; +using HotChocolate.AspNetCore.Extensions; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; -using HotChocolate.AspNetCore; -using HotChocolate.AspNetCore.Extensions; -using static HotChocolate.AspNetCore.MiddlewareRoutingType; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using static Microsoft.AspNetCore.Routing.Patterns.RoutePatternFactory; +using MiddlewareFactory = HotChocolate.AspNetCore.MiddlewareFactory; // ReSharper disable once CheckNamespace namespace Microsoft.AspNetCore.Builder; @@ -108,23 +111,28 @@ public static IApplicationBuilder MapGraphQL( string schemaName) { ArgumentNullException.ThrowIfNull(applicationBuilder); + ArgumentException.ThrowIfNullOrEmpty(schemaName); path = path.ToString().TrimEnd('/'); + var executorProvider = applicationBuilder.ApplicationServices.GetRequiredService(); + var executorEvents = applicationBuilder.ApplicationServices.GetRequiredService(); + var formOptions = applicationBuilder.ApplicationServices.GetRequiredService>(); + var executor = new HttpRequestExecutorProxy(executorProvider, executorEvents, schemaName); + applicationBuilder - .UseCancellation() - .UseMiddleware(schemaName) - .UseMiddleware(schemaName) - .UseMiddleware(schemaName) - .UseMiddleware(schemaName) - .UseMiddleware(schemaName, path, Integrated) + .Use(MiddlewareFactory.CreateCancellationMiddleware()) + .Use(MiddlewareFactory.CreateWebSocketSubscriptionMiddleware(executor)) + .Use(MiddlewareFactory.CreateHttpPostMiddleware(executor)) + .Use(MiddlewareFactory.CreateHttpMultipartMiddleware(executor, formOptions)) + .Use(MiddlewareFactory.CreateHttpGetMiddleware(executor)) + .Use(MiddlewareFactory.CreateHttpGetSchemaMiddleware(executor, path, MiddlewareRoutingType.Integrated)) .UseNitroApp(path) - .Use( - _ => context => - { - context.Response.StatusCode = 404; - return Task.CompletedTask; - }); + .Use(_ => context => + { + context.Response.StatusCode = 404; + return Task.CompletedTask; + }); return applicationBuilder; } @@ -184,17 +192,21 @@ public static GraphQLHttpEndpointConventionBuilder MapGraphQLHttp( var requestPipeline = endpointRouteBuilder.CreateApplicationBuilder(); var schemaNameOrDefault = schemaName ?? ISchemaDefinition.DefaultName; + var executorProvider = endpointRouteBuilder.ServiceProvider.GetRequiredService(); + var executorEvents = endpointRouteBuilder.ServiceProvider.GetRequiredService(); + var formOptions = endpointRouteBuilder.ServiceProvider.GetRequiredService>(); + var executor = new HttpRequestExecutorProxy(executorProvider, executorEvents, schemaNameOrDefault); + requestPipeline - .UseCancellation() - .UseMiddleware(schemaNameOrDefault) - .UseMiddleware(schemaNameOrDefault) - .UseMiddleware(schemaNameOrDefault) - .Use( - _ => context => - { - context.Response.StatusCode = 404; - return Task.CompletedTask; - }); + .Use(MiddlewareFactory.CreateCancellationMiddleware()) + .Use(MiddlewareFactory.CreateHttpPostMiddleware(executor)) + .Use(MiddlewareFactory.CreateHttpMultipartMiddleware(executor, formOptions)) + .Use(MiddlewareFactory.CreateHttpGetMiddleware(executor)) + .Use(_ => context => + { + context.Response.StatusCode = 404; + return Task.CompletedTask; + }); return new GraphQLHttpEndpointConventionBuilder( endpointRouteBuilder @@ -257,15 +269,18 @@ public static WebSocketEndpointConventionBuilder MapGraphQLWebSocket( var requestPipeline = endpointRouteBuilder.CreateApplicationBuilder(); var schemaNameOrDefault = schemaName ?? ISchemaDefinition.DefaultName; + var executorProvider = endpointRouteBuilder.ServiceProvider.GetRequiredService(); + var executorEvents = endpointRouteBuilder.ServiceProvider.GetRequiredService(); + var executor = new HttpRequestExecutorProxy(executorProvider, executorEvents, schemaNameOrDefault); + requestPipeline - .UseCancellation() - .UseMiddleware(schemaNameOrDefault) - .Use( - _ => context => - { - context.Response.StatusCode = 404; - return Task.CompletedTask; - }); + .Use(MiddlewareFactory.CreateCancellationMiddleware()) + .Use(MiddlewareFactory.CreateWebSocketSubscriptionMiddleware(executor)) + .Use(_ => context => + { + context.Response.StatusCode = 404; + return Task.CompletedTask; + }); var builder = new GraphQLEndpointConventionBuilder( endpointRouteBuilder @@ -330,15 +345,18 @@ public static IEndpointConventionBuilder MapGraphQLSchema( var requestPipeline = endpointRouteBuilder.CreateApplicationBuilder(); var schemaNameOrDefault = schemaName ?? ISchemaDefinition.DefaultName; + var executorProvider = endpointRouteBuilder.ServiceProvider.GetRequiredService(); + var executorEvents = endpointRouteBuilder.ServiceProvider.GetRequiredService(); + var executor = new HttpRequestExecutorProxy(executorProvider, executorEvents, schemaNameOrDefault); + requestPipeline - .UseCancellation() - .UseMiddleware(schemaNameOrDefault, PathString.Empty, Explicit) - .Use( - _ => context => - { - context.Response.StatusCode = 404; - return Task.CompletedTask; - }); + .Use(MiddlewareFactory.CreateCancellationMiddleware()) + .Use(MiddlewareFactory.CreateHttpGetSchemaMiddleware(executor, PathString.Empty, MiddlewareRoutingType.Explicit)) + .Use(_ => context => + { + context.Response.StatusCode = 404; + return Task.CompletedTask; + }); return new GraphQLEndpointConventionBuilder( endpointRouteBuilder @@ -399,12 +417,11 @@ public static NitroAppEndpointConventionBuilder MapNitroApp( requestPipeline .UseNitroApp(toolPath) - .Use( - _ => context => - { - context.Response.StatusCode = 404; - return Task.CompletedTask; - }); + .Use(_ => context => + { + context.Response.StatusCode = 404; + return Task.CompletedTask; + }); var builder = endpointRouteBuilder .Map(pattern, requestPipeline.Build()) @@ -427,7 +444,7 @@ public static NitroAppEndpointConventionBuilder MapNitroApp( /// The name of the schema that shall be used by this endpoint. /// /// - /// Specifies if its required to provide the operation name as part of the URI. + /// Specifies if its required providing the operation name as part of the URI. /// /// /// Returns the so that @@ -452,7 +469,7 @@ public static IEndpointConventionBuilder MapGraphQLPersistedOperations( /// The name of the schema that shall be used by this endpoint. /// /// - /// Specifies if its required to provide the operation name as part of the URI. + /// Specifies if its required providing the operation name as part of the URI. /// /// /// Returns the so that @@ -463,10 +480,12 @@ public static IEndpointConventionBuilder MapGraphQLPersistedOperations( string? schemaName = null, bool requireOperationName = false) { - var schemaNameOrDefault = schemaName ?? ISchemaDefinition.DefaultName; + ArgumentNullException.ThrowIfNull(endpointRouteBuilder); + ArgumentNullException.ThrowIfNull(path); + schemaName ??= ISchemaDefinition.DefaultName; var group = endpointRouteBuilder.MapGroup(path); - group.MapPersistedOperationMiddleware(schemaNameOrDefault, requireOperationName); + group.MapPersistedOperationMiddleware(endpointRouteBuilder.ServiceProvider, schemaName, requireOperationName); return group; } @@ -553,20 +572,6 @@ public static WebSocketEndpointConventionBuilder WithOptions( GraphQLSocketOptions socketOptions) => builder.WithMetadata(new GraphQLServerOptions { Sockets = socketOptions }); - private static IApplicationBuilder UseCancellation(this IApplicationBuilder builder) - => builder.Use( - next => async context => - { - try - { - await next(context); - } - catch (OperationCanceledException) - { - // we just catch cancellations here and do nothing. - } - }); - internal static NitroAppOptions ToNitroAppOptions(this GraphQLToolOptions options) => new() { diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/GraphQLEndpointConventionBuilder.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/GraphQLEndpointConventionBuilder.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/GraphQLEndpointConventionBuilder.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/GraphQLEndpointConventionBuilder.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/GraphQLHttpEndpointConventionBuilder.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/GraphQLHttpEndpointConventionBuilder.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/GraphQLHttpEndpointConventionBuilder.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/GraphQLHttpEndpointConventionBuilder.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpContextExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs similarity index 67% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpContextExtensions.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs index 1f6170bb223..3b7e4157c15 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpContextExtensions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs @@ -27,6 +27,7 @@ public static bool IncludeQueryPlan(this HttpContext context) return false; } + // TODO : Implement this public static string? TryGetCostSwitch(this HttpContext context) { var headers = context.Request.Headers; @@ -42,15 +43,38 @@ public static bool IncludeQueryPlan(this HttpContext context) if (value.Equals(HttpHeaderValues.ReportCost, StringComparison.OrdinalIgnoreCase)) { - return WellKnownContextData.ReportCost; + return ExecutionContextData.ReportCost; } if (value.Equals(HttpHeaderValues.ValidateCost, StringComparison.OrdinalIgnoreCase)) { - return WellKnownContextData.ValidateCost; + return ExecutionContextData.ValidateCost; } } return null; } + + public static RequestContentType ParseContentType(this HttpContext context) + { + if (context.Items.TryGetValue(nameof(RequestContentType), out var value) && + value is RequestContentType contentType) + { + return contentType; + } + + var span = context.Request.ContentType.AsSpan(); + + if (span.StartsWith(ContentType.JsonSpan())) + { + return RequestContentType.Json; + } + + if (span.StartsWith(ContentType.MultiPartFormSpan())) + { + return RequestContentType.Form; + } + + return RequestContentType.None; + } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpRequestExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpRequestExtensions.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpRequestExtensions.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpRequestExtensions.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpResponseExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpResponseExtensions.cs new file mode 100644 index 00000000000..bd9c014fcc2 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpResponseExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using static System.Globalization.CultureInfo; +using static System.String; + +namespace HotChocolate.AspNetCore; + +internal static class HttpResponseExtensions +{ + private const string ContentDispositionHeader = "Content-Disposition"; + private const string ContentDispositionValue = "attachment; filename=\"{0}\""; + + public static IHeaderDictionary SetContentDisposition( + this IHeaderDictionary headers, + string fileName) + { + headers[ContentDispositionHeader] = + Format(InvariantCulture, ContentDispositionValue, fileName); + return headers; + } +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/NitroAppEndpointConventionBuilder.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/NitroAppEndpointConventionBuilder.cs similarity index 83% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/NitroAppEndpointConventionBuilder.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/NitroAppEndpointConventionBuilder.cs index 827858bffd5..a846c4cdc06 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/NitroAppEndpointConventionBuilder.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/NitroAppEndpointConventionBuilder.cs @@ -15,6 +15,6 @@ internal NitroAppEndpointConventionBuilder(IEndpointConventionBuilder builder) } /// - public void Add(Action convention) => - _builder.Add(convention); + public void Add(Action convention) + => _builder.Add(convention); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/WebSocketEndpointConventionBuilder.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/WebSocketEndpointConventionBuilder.cs similarity index 83% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/WebSocketEndpointConventionBuilder.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/WebSocketEndpointConventionBuilder.cs index 46bc70368db..aa487856be5 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/WebSocketEndpointConventionBuilder.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/WebSocketEndpointConventionBuilder.cs @@ -15,6 +15,6 @@ internal WebSocketEndpointConventionBuilder(IEndpointConventionBuilder builder) } /// - public void Add(Action convention) => - _builder.Add(convention); + public void Add(Action convention) + => _builder.Add(convention); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResponseFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultHttpResponseFormatter.cs similarity index 95% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResponseFormatter.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultHttpResponseFormatter.cs index 17289f41ed9..8b953c7a8d5 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResponseFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultHttpResponseFormatter.cs @@ -5,16 +5,15 @@ using System.Text; using System.Text.Encodings.Web; using System.Text.Json; +using HotChocolate.AspNetCore.Utilities; using HotChocolate.Transport.Formatters; -using HotChocolate.Utilities; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; using static HotChocolate.AspNetCore.AcceptMediaTypeKind; using static HotChocolate.Execution.ExecutionResultKind; -using static HotChocolate.WellKnownContextData; using HttpStatusCode = System.Net.HttpStatusCode; -namespace HotChocolate.AspNetCore.Serialization; +namespace HotChocolate.AspNetCore.Formatters; /// /// This represents the default implementation for the @@ -210,14 +209,14 @@ private async ValueTask FormatInternalAsync( response.StatusCode = statusCode; if (result.ContextData is not null - && result.ContextData.TryGetValue(WellKnownContextData.CacheControlHeaderValue, out var value) + && result.ContextData.TryGetValue(ExecutionContextData.CacheControlHeaderValue, out var value) && value is CacheControlHeaderValue cacheControlHeaderValue) { response.GetTypedHeaders().CacheControl = cacheControlHeaderValue; } if (result.ContextData is not null - && result.ContextData.TryGetValue(VaryHeaderValue, out var varyValue) + && result.ContextData.TryGetValue(ExecutionContextData.VaryHeaderValue, out var varyValue) && varyValue is string varyHeaderValue) { response.Headers.Vary = varyHeaderValue; @@ -352,7 +351,7 @@ protected virtual HttpStatusCode OnDetermineStatusCode( var contextData = result.ContextData; // First, we check if there is an explicit HTTP status code override by the user. - if (contextData.TryGetValue(WellKnownContextData.HttpStatusCode, out var value)) + if (contextData.TryGetValue(ExecutionContextData.HttpStatusCode, out var value)) { if (value is HttpStatusCode statusCode) { @@ -372,7 +371,7 @@ protected virtual HttpStatusCode OnDetermineStatusCode( return HttpStatusCode.BadRequest; } - if (result.ContextData.ContainsKey(OperationNotAllowed)) + if (result.ContextData.ContainsKey(ExecutionContextData.OperationNotAllowed)) { return HttpStatusCode.MethodNotAllowed; } @@ -442,7 +441,7 @@ protected virtual HttpStatusCode OnDetermineStatusCode( HttpStatusCode? proposedStatusCode) { // if we are sending a response stream with the multipart/mixed header or - // with a text/event-stream response content-type we as well will just + // with a text/event-stream response content-type, we as well will just // respond with an OK status code. if (format.Kind is ResponseContentType.MultiPartMixed or ResponseContentType.EventStream) { @@ -450,7 +449,7 @@ protected virtual HttpStatusCode OnDetermineStatusCode( } // we allow for users to implement alternative protocols or response content-type. - // if we end up here the user did not fully implement all necessary parts to add support + // if we end up here, the user did not fully implement all necessary parts to add support // for an alternative protocols or response content-type. throw ThrowHelper.Formatter_ResponseContentTypeNotSupported(format.ContentType); } @@ -640,14 +639,14 @@ protected virtual void OnWriteResponseHeaders( if (resultKind is ResultKind.Stream or ResultKind.Single && start.Kind is MultiPartMixed or AllMultiPart or All) { - // if the result is a stream we consider this a perfect match and + // if the result is a stream, we consider this a perfect match and // will use this format. if (resultKind is ResultKind.Stream) { possibleFormat = _multiPartFormat; } - // if the format is an event-stream or not set we will create a + // if the format is an event-stream or not set, we will create a // multipart/mixed formatInfo for the current result but also keep // on validating for a better suited format. if (possibleFormat?.Kind is not ResponseContentType.Json) @@ -658,17 +657,17 @@ protected virtual void OnWriteResponseHeaders( if (start.Kind is EventStream or All) { - // if the result is a subscription we consider this a perfect match and + // if the result is a subscription, we consider this a perfect match and // will use this format. if (resultKind is ResultKind.Subscription or ResultKind.Stream) { possibleFormat = _eventStreamFormat; } - // if the result is stream it means that we did not yet validate a + // if the result is stream, it means that we did not yet validate a // multipart content-type and thus will create a format for the case that it // is not specified; - // or we have a single result but there is no format yet specified + // or we have a single result, but there is no format yet specified // we will create a text/event-stream formatInfo for the current result // but also keep on validating for a better suited format. if (possibleFormat?.Kind is ResponseContentType.Unknown) @@ -686,7 +685,10 @@ protected virtual void OnWriteResponseHeaders( internal static DefaultHttpResponseFormatter Create( HttpResponseFormatterOptions options, ITimeProvider timeProvider) - => new SealedDefaultHttpResponseFormatter(options, timeProvider); + { + ArgumentNullException.ThrowIfNull(timeProvider); + return new SealedDefaultHttpResponseFormatter(options, timeProvider); + } /// /// Representation of a resolver format, containing the formatter and the content type. @@ -767,7 +769,7 @@ private static string CreateETag(byte[] schema, ulong version) } private static string GetSchemaFileName(ISchemaDefinition schema) - => schema.Name.EqualsOrdinal(ISchemaDefinition.DefaultName) + => schema.Name.Equals(ISchemaDefinition.DefaultName, StringComparison.OrdinalIgnoreCase) ? "schema.graphql" : schema.Name + ".schema.graphql"; } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultWebSocketPayloadFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultWebSocketPayloadFormatter.cs similarity index 75% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultWebSocketPayloadFormatter.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultWebSocketPayloadFormatter.cs index bc494643f2b..f7eda176d65 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultWebSocketPayloadFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultWebSocketPayloadFormatter.cs @@ -1,12 +1,12 @@ using System.Text.Json; using HotChocolate.Transport.Formatters; -namespace HotChocolate.AspNetCore.Serialization; +namespace HotChocolate.AspNetCore.Formatters; /// /// This represents the default implementation for the . /// -public class DefaultWebSocketPayloadFormatter(WebSocketPayloadFormatterOptions options) +public class DefaultWebSocketPayloadFormatter(WebSocketPayloadFormatterOptions options = default) : IWebSocketPayloadFormatter { private readonly JsonResultFormatter _jsonFormatter = new(options.Json); @@ -28,4 +28,10 @@ public void Format(IReadOnlyList errors, Utf8JsonWriter jsonWriter) { _jsonFormatter.FormatErrors(errors, jsonWriter); } + + /// + public void Format(IReadOnlyDictionary extensions, Utf8JsonWriter jsonWriter) + { + _jsonFormatter.FormatDictionary(extensions, jsonWriter); + } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/HttpResponseFormatterOptions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/HttpResponseFormatterOptions.cs similarity index 90% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/HttpResponseFormatterOptions.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/HttpResponseFormatterOptions.cs index bfbab44b7fb..2cf58892cc9 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/HttpResponseFormatterOptions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/HttpResponseFormatterOptions.cs @@ -1,6 +1,6 @@ using HotChocolate.Transport.Formatters; -namespace HotChocolate.AspNetCore.Serialization; +namespace HotChocolate.AspNetCore.Formatters; /// /// Represents the GraphQL over HTTP formatter options. diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IHttpResponseFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IHttpResponseFormatter.cs similarity index 97% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IHttpResponseFormatter.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IHttpResponseFormatter.cs index 4c50c47a247..40f6305ea0b 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IHttpResponseFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IHttpResponseFormatter.cs @@ -1,7 +1,7 @@ using System.Net; using Microsoft.AspNetCore.Http; -namespace HotChocolate.AspNetCore.Serialization; +namespace HotChocolate.AspNetCore.Formatters; /// /// This interface specifies how a GraphQL result is formatted to a HTTP response. diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IWebSocketPayloadFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IWebSocketPayloadFormatter.cs similarity index 62% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IWebSocketPayloadFormatter.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IWebSocketPayloadFormatter.cs index ebc5dbf38da..6833e680501 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IWebSocketPayloadFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IWebSocketPayloadFormatter.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace HotChocolate.AspNetCore.Serialization; +namespace HotChocolate.AspNetCore.Formatters; /// /// This interface specifies how a GraphQL result is formatted as a WebSocket payload. @@ -8,7 +8,7 @@ namespace HotChocolate.AspNetCore.Serialization; public interface IWebSocketPayloadFormatter { /// - /// Formats the given into a WebSocket payload. + /// Formats the into a WebSocket payload. /// /// /// The GraphQL operation result. @@ -19,7 +19,7 @@ public interface IWebSocketPayloadFormatter void Format(IOperationResult result, Utf8JsonWriter jsonWriter); /// - /// Formats the given into a WebSocket payload. + /// Formats the into a WebSocket payload. /// /// /// The GraphQL execution error. @@ -30,7 +30,7 @@ public interface IWebSocketPayloadFormatter void Format(IError error, Utf8JsonWriter jsonWriter); /// - /// Formats the given into a WebSocket payload. + /// Formats the into a WebSocket payload. /// /// /// The GraphQL execution errors. @@ -39,4 +39,15 @@ public interface IWebSocketPayloadFormatter /// The JSON writer that is used to write the errors. /// void Format(IReadOnlyList errors, Utf8JsonWriter jsonWriter); + + /// + /// Formats the into a WebSocket payload. + /// + /// + /// The GraphQL extensions. + /// + /// + /// The JSON writer that is used to write the extensions. + /// + void Format(IReadOnlyDictionary extensions, Utf8JsonWriter jsonWriter); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/WebSocketPayloadFormatterOptions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/WebSocketPayloadFormatterOptions.cs similarity index 87% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/WebSocketPayloadFormatterOptions.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/WebSocketPayloadFormatterOptions.cs index bfdf3e6f617..1badff96d4c 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/WebSocketPayloadFormatterOptions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/WebSocketPayloadFormatterOptions.cs @@ -1,6 +1,6 @@ using HotChocolate.Transport.Formatters; -namespace HotChocolate.AspNetCore.Serialization; +namespace HotChocolate.AspNetCore.Formatters; /// /// Represents the GraphQL over WebSocket payload formatter options. diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/GraphQLRequestException.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/GraphQLRequestException.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/GraphQLRequestException.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/GraphQLRequestException.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HotChocolate.AspNetCore.Pipeline.csproj b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HotChocolate.AspNetCore.Pipeline.csproj new file mode 100644 index 00000000000..fcd1760b60f --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HotChocolate.AspNetCore.Pipeline.csproj @@ -0,0 +1,59 @@ + + + + HotChocolate.AspNetCore.Pipeline + HotChocolate.AspNetCore.Pipeline + HotChocolate.AspNetCore + This package contains the GraphQL ASP.NET Core middleware for Hot Chocolate. + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ResXFileCodeGenerator + AspNetCorePipelineResources.Designer.cs + + + + + + True + True + AspNetCorePipelineResources.resx + + + + diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpGetMiddleware.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpGetMiddleware.cs similarity index 60% rename from src/HotChocolate/AspNetCore/src/AspNetCore/HttpGetMiddleware.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpGetMiddleware.cs index f45aa95c314..2c99a3710e5 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpGetMiddleware.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpGetMiddleware.cs @@ -1,37 +1,26 @@ using System.Net; using Microsoft.AspNetCore.Http; using HotChocolate.AspNetCore.Instrumentation; -using HotChocolate.AspNetCore.Serialization; -using static HotChocolate.AspNetCore.Serialization.DefaultHttpRequestParser; +using HotChocolate.AspNetCore.Utilities; +using static HotChocolate.AspNetCore.Parsers.DefaultHttpRequestParser; using HttpRequestDelegate = Microsoft.AspNetCore.Http.RequestDelegate; namespace HotChocolate.AspNetCore; public sealed class HttpGetMiddleware : MiddlewareBase { - private readonly IHttpRequestParser _requestParser; - private readonly IServerDiagnosticEvents _diagnosticEvents; - public HttpGetMiddleware( HttpRequestDelegate next, - IRequestExecutorProvider executorResolver, - IRequestExecutorEvents executorEvents, - IHttpResponseFormatter responseFormatter, - IHttpRequestParser requestParser, - IServerDiagnosticEvents diagnosticEvents, - string schemaName) - : base(next, executorResolver, executorEvents, responseFormatter, schemaName) + HttpRequestExecutorProxy executor) + : base(next, executor) { - _requestParser = requestParser ?? - throw new ArgumentNullException(nameof(requestParser)); - _diagnosticEvents = diagnosticEvents ?? - throw new ArgumentNullException(nameof(diagnosticEvents)); } public async Task InvokeAsync(HttpContext context) { if (HttpMethods.IsGet(context.Request.Method)) { + var session = await Executor.GetOrCreateSessionAsync(context.RequestAborted); var options = GetOptions(context); if (options.EnableGetRequests && @@ -50,16 +39,11 @@ public async Task InvokeAsync(HttpContext context) // Allow HTTP GraphQL GET requests if the content type is set to // application/json. - ParseContentType(context) is RequestContentType.Json)) + context.ParseContentType() is RequestContentType.Json)) { - if (!IsDefaultSchema) - { - context.Items[WellKnownContextData.SchemaName] = SchemaName; - } - - using (_diagnosticEvents.ExecuteHttpRequest(context, HttpRequestKind.HttpGet)) + using (session.DiagnosticEvents.ExecuteHttpRequest(context, HttpRequestKind.HttpGet)) { - await HandleRequestAsync(context); + await HandleRequestAsync(context, session); } return; @@ -71,17 +55,13 @@ public async Task InvokeAsync(HttpContext context) await NextAsync(context); } - private async Task HandleRequestAsync(HttpContext context) + private async Task HandleRequestAsync(HttpContext context, ExecutorSession session) { HttpStatusCode? statusCode; IExecutionResult? result; // first we validate the accept headers. - var validationResult = - MiddlewareHelper.ValidateAcceptContentType( - context, - ResponseFormatter, - _diagnosticEvents); + var validationResult = MiddlewareHelper.ValidateAcceptContentType(context, session); var acceptMediaTypes = validationResult.AcceptMediaTypes; @@ -93,16 +73,7 @@ private async Task HandleRequestAsync(HttpContext context) } // next we parse the GraphQL request. - var executor = ExecutorProxy.CurrentExecutor ?? - await ExecutorProxy.GetExecutorAsync(context.RequestAborted); - var errorHandler = executor.GetErrorHandler(); - - var parserResult = - MiddlewareHelper.ParseRequestFromParams( - context, - _requestParser, - errorHandler, - _diagnosticEvents); + var parserResult = MiddlewareHelper.ParseRequestFromParams(context, session); if (!parserResult.IsValid) { @@ -125,9 +96,7 @@ await MiddlewareHelper.ExecuteRequestAsync( request, requestFlags, context, - executor, - errorHandler, - _diagnosticEvents); + session); statusCode = executionResult.StatusCode; result = executionResult.Result; @@ -137,7 +106,6 @@ await MiddlewareHelper.WriteResultAsync( acceptMediaTypes, statusCode, context, - ResponseFormatter, - _diagnosticEvents); + session); } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpGetSchemaMiddleware.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpGetSchemaMiddleware.cs similarity index 65% rename from src/HotChocolate/AspNetCore/src/AspNetCore/HttpGetSchemaMiddleware.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpGetSchemaMiddleware.cs index ef48789af32..40d411a2db4 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpGetSchemaMiddleware.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpGetSchemaMiddleware.cs @@ -1,11 +1,8 @@ +using System.Text; using HotChocolate.AspNetCore.Instrumentation; -using HotChocolate.AspNetCore.Serialization; -using HotChocolate.Execution.Options; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using static System.Net.HttpStatusCode; -using static HotChocolate.AspNetCore.ErrorHelper; -using static HotChocolate.SchemaPrinter; +using static HotChocolate.AspNetCore.Utilities.ErrorHelper; using HttpRequestDelegate = Microsoft.AspNetCore.Http.RequestDelegate; namespace HotChocolate.AspNetCore; @@ -22,21 +19,15 @@ public sealed class HttpGetSchemaMiddleware : MiddlewareBase ]; private readonly MiddlewareRoutingType _routing; - private readonly IServerDiagnosticEvents _diagnosticEvents; private readonly PathString _path; public HttpGetSchemaMiddleware( HttpRequestDelegate next, - IRequestExecutorProvider executorProvider, - IRequestExecutorEvents executorEvents, - IHttpResponseFormatter responseFormatter, - IServerDiagnosticEvents diagnosticEvents, - string schemaName, + HttpRequestExecutorProxy executor, PathString path, MiddlewareRoutingType routing) - : base(next, executorProvider, executorEvents, responseFormatter, schemaName) + : base(next, executor) { - _diagnosticEvents = diagnosticEvents ?? throw new ArgumentNullException(nameof(diagnosticEvents)); _path = path; _routing = routing; } @@ -52,14 +43,12 @@ public async Task InvokeAsync(HttpContext context) if (handle) { - if (!IsDefaultSchema) - { - context.Items[WellKnownContextData.SchemaName] = SchemaName; - } + var session = await Executor.GetOrCreateSessionAsync(context.RequestAborted); + var options = GetOptions(context); - using (_diagnosticEvents.ExecuteHttpRequest(context, HttpRequestKind.HttpGetSchema)) + using (session.DiagnosticEvents.ExecuteHttpRequest(context, HttpRequestKind.HttpGetSchema)) { - await HandleRequestAsync(context); + await HandleRequestAsync(context, session, options); } } else @@ -82,12 +71,8 @@ private bool IsSchemaPath(HttpRequest request) return false; } - private async Task HandleRequestAsync(HttpContext context) + private async Task HandleRequestAsync(HttpContext context, ExecutorSession session, GraphQLServerOptions options) { - var executor = await GetExecutorAsync(context.RequestAborted); - context.Items[WellKnownContextData.Schema] = executor.Schema; - var options = executor.Schema.Services.GetRequiredService(); - if (!options.EnableSchemaFileSupport) { context.Response.StatusCode = 404; @@ -100,7 +85,7 @@ private async Task HandleRequestAsync(HttpContext context) if (string.IsNullOrEmpty(s)) { - await WriteResultAsync( + await session.WriteResultAsync( context, TypeNameIsEmpty(), s_mediaTypes, @@ -108,17 +93,17 @@ await WriteResultAsync( return; } - await WriteTypesAsync(context, executor.Schema, s, true); + await WriteTypesAsync(context, session, s, true); } else { - await WriteSchemaAsync(context, executor); + await session.WriteSchemaAsync(context); } } private async Task WriteTypesAsync( HttpContext context, - ISchemaDefinition schema, + ExecutorSession session, string typeNames, bool indent) { @@ -130,7 +115,7 @@ private async Task WriteTypesAsync( || coordinate.Value.MemberName is not null || coordinate.Value.ArgumentName is not null) { - await WriteResultAsync( + await session.WriteResultAsync( context, InvalidTypeName(typeName), s_mediaTypes, @@ -138,9 +123,9 @@ await WriteResultAsync( return; } - if (!schema.Types.TryGetType(coordinate.Value.Name, out var type)) + if (!session.Schema.Types.TryGetType(coordinate.Value.Name, out var type)) { - await WriteResultAsync( + await session.WriteResultAsync( context, TypeNotFound(typeName), s_mediaTypes, @@ -153,20 +138,33 @@ await WriteResultAsync( context.Response.ContentType = ContentType.GraphQL; context.Response.Headers.SetContentDisposition(GetTypesFileName(types)); - await PrintAsync(types, context.Response.Body, indent, context.RequestAborted); + await PrintTypesAsync(types, context.Response.Body, indent, context.RequestAborted); } - private ValueTask WriteSchemaAsync( - HttpContext context, - IRequestExecutor executor) - => ResponseFormatter.FormatAsync( - context.Response, - executor.Schema, - executor.Version, - context.RequestAborted); - private string GetTypesFileName(List types) => types.Count == 1 ? $"{types[0].Name}.graphql" : "types.graphql"; + + private static async Task PrintTypesAsync( + List types, + Stream stream, + bool indent, + CancellationToken cancellationToken) + { + var next = false; + await using var streamWriter = new StreamWriter(stream, Encoding.UTF8, 1024, leaveOpen: true); + + foreach (var type in types) + { + if (next) + { + await streamWriter.WriteLineAsync(); + } + + var s = type.ToSyntaxNode().ToString(indent); + await streamWriter.WriteLineAsync(s.AsMemory(), cancellationToken); + next = true; + } + } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderKeys.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpHeaderKeys.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderKeys.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpHeaderKeys.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderValues.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpHeaderValues.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderValues.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpHeaderValues.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpMultipartMiddleware.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpMultipartMiddleware.cs similarity index 80% rename from src/HotChocolate/AspNetCore/src/AspNetCore/HttpMultipartMiddleware.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpMultipartMiddleware.cs index 754a38a6779..20a78066f0c 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpMultipartMiddleware.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpMultipartMiddleware.cs @@ -1,15 +1,17 @@ using System.Text.Json; +using System.Text.Json.Serialization; +using HotChocolate.AspNetCore.Instrumentation; +using HotChocolate.AspNetCore.Parsers; +using HotChocolate.AspNetCore.Utilities; +using HotChocolate.Language; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Options; -using HotChocolate.AspNetCore.Instrumentation; -using HotChocolate.AspNetCore.Serialization; -using HotChocolate.Language; -using HotChocolate.Utilities; using static System.Net.HttpStatusCode; -using static HotChocolate.AspNetCore.ErrorHelper; -using static HotChocolate.AspNetCore.Properties.AspNetCoreResources; +using static HotChocolate.AspNetCore.Utilities.ErrorHelper; +using static HotChocolate.AspNetCore.Properties.AspNetCorePipelineResources; using HttpRequestDelegate = Microsoft.AspNetCore.Http.RequestDelegate; +using ThrowHelper = HotChocolate.AspNetCore.Utilities.ThrowHelper; namespace HotChocolate.AspNetCore; @@ -22,22 +24,11 @@ public sealed class HttpMultipartMiddleware : HttpPostMiddlewareBase public HttpMultipartMiddleware( HttpRequestDelegate next, - IRequestExecutorProvider executorProvider, - IRequestExecutorEvents executorEvents, - IHttpResponseFormatter responseFormatter, - IHttpRequestParser requestParser, - IServerDiagnosticEvents diagnosticEvents, - string schemaName, + HttpRequestExecutorProxy executor, IOptions formOptions) - : base( - next, - executorProvider, - executorEvents, - responseFormatter, - requestParser, - diagnosticEvents, - schemaName) + : base(next, executor) { + ArgumentNullException.ThrowIfNull(formOptions); _formOptions = formOptions.Value; } @@ -45,24 +36,21 @@ public override async Task InvokeAsync(HttpContext context) { if (HttpMethods.IsPost(context.Request.Method) && GetOptions(context).EnableMultipartRequests && - ParseContentType(context) == RequestContentType.Form) + context.ParseContentType() == RequestContentType.Form) { + var session = await Executor.GetOrCreateSessionAsync(context.RequestAborted); + if (!context.Request.Headers.ContainsKey(HttpHeaderKeys.Preflight) && GetOptions(context).EnforceMultipartRequestsPreflightHeader) { var headerResult = HeaderUtilities.GetAcceptHeader(context.Request); - await WriteResultAsync(context, _multipartRequestError, headerResult.AcceptMediaTypes, BadRequest); + await session.WriteResultAsync(context, _multipartRequestError, headerResult.AcceptMediaTypes, BadRequest); return; } - if (!IsDefaultSchema) + using (session.DiagnosticEvents.ExecuteHttpRequest(context, HttpRequestKind.HttpMultiPart)) { - context.Items[WellKnownContextData.SchemaName] = SchemaName; - } - - using (DiagnosticEvents.ExecuteHttpRequest(context, HttpRequestKind.HttpMultiPart)) - { - await HandleRequestAsync(context); + await HandleRequestAsync(context, session, context.RequestAborted); } } else @@ -74,15 +62,16 @@ public override async Task InvokeAsync(HttpContext context) } protected override async ValueTask> ParseRequestsFromBodyAsync( - HttpRequest httpRequest, - CancellationToken cancellationToken) + HttpContext context, + ExecutorSession session) { IFormCollection? form; + var httpRequest = context.Request; try { var formFeature = new FormFeature(httpRequest, _formOptions); - form = await formFeature.ReadFormAsync(cancellationToken); + form = await formFeature.ReadFormAsync(context.RequestAborted); } catch (Exception exception) { @@ -91,7 +80,7 @@ protected override async ValueTask> ParseRequestsF // Parse the string values of interest from the IFormCollection var multipartRequest = ParseMultipartRequest(form); - var requests = RequestParser.ParseRequest( + var requests = session.RequestParser.ParseRequest( multipartRequest.Operations); foreach (var graphQLRequest in requests) @@ -127,7 +116,7 @@ private static HttpMultipartRequest ParseMultipartRequest(IFormCollection form) try { - map = JsonSerializer.Deserialize>(mapString!); + map = JsonSerializer.Deserialize(mapString!, FormsMapJsonContext.Default.Map); } catch { @@ -152,7 +141,7 @@ private static HttpMultipartRequest ParseMultipartRequest(IFormCollection form) return new HttpMultipartRequest(operations, pathToFileMap); } - private static IDictionary MapFilesToObjectPaths( + private static Dictionary MapFilesToObjectPaths( IDictionary map, IFormFileCollection files) { @@ -165,9 +154,7 @@ private static IDictionary MapFilesToObjectPaths( throw ThrowHelper.HttpMultipartMiddleware_NoObjectPath(filename); } - var file = filename is { Length: > 0 } - ? files.GetFile(filename) - : null; + var file = filename.Length > 0 ? files.GetFile(filename) : null; if (file is null) { @@ -233,7 +220,7 @@ private static IValueNode RewriteVariable( for (var i = 0; i < ov.Fields.Count; i++) { - if (ov.Fields[i].Name.Value.EqualsOrdinal(key.Value)) + if (ov.Fields[i].Name.Value.Equals(key.Value, StringComparison.Ordinal)) { pos = i; break; @@ -267,3 +254,6 @@ key.Next is not null throw ThrowHelper.HttpMultipartMiddleware_VariableNotFound(objectPath); } } + +[JsonSerializable(typeof(Dictionary), TypeInfoPropertyName = "Map")] +internal partial class FormsMapJsonContext : JsonSerializerContext; diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpPostMiddleware.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpPostMiddleware.cs new file mode 100644 index 00000000000..75aeb867b4c --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpPostMiddleware.cs @@ -0,0 +1,6 @@ +using HttpRequestDelegate = Microsoft.AspNetCore.Http.RequestDelegate; + +namespace HotChocolate.AspNetCore; + +public sealed class HttpPostMiddleware(HttpRequestDelegate next, HttpRequestExecutorProxy executor) + : HttpPostMiddlewareBase(next, executor); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpPostMiddlewareBase.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpPostMiddlewareBase.cs similarity index 67% rename from src/HotChocolate/AspNetCore/src/AspNetCore/HttpPostMiddlewareBase.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpPostMiddlewareBase.cs index de284fef6d8..010faec00fb 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpPostMiddlewareBase.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpPostMiddlewareBase.cs @@ -3,7 +3,7 @@ using System.Net; using System.Text; using HotChocolate.AspNetCore.Instrumentation; -using HotChocolate.AspNetCore.Serialization; +using HotChocolate.AspNetCore.Utilities; using HotChocolate.Language; using Microsoft.AspNetCore.Http; using static HotChocolate.Execution.RequestFlags; @@ -11,43 +11,28 @@ namespace HotChocolate.AspNetCore; -public class HttpPostMiddlewareBase : MiddlewareBase +public abstract class HttpPostMiddlewareBase : MiddlewareBase { private const string BatchOperations = "batchOperations"; protected HttpPostMiddlewareBase( HttpRequestDelegate next, - IRequestExecutorProvider executorResolver, - IRequestExecutorEvents executorEvents, - IHttpResponseFormatter responseFormatter, - IHttpRequestParser requestParser, - IServerDiagnosticEvents diagnosticEvents, - string schemaName) - : base(next, executorResolver, executorEvents, responseFormatter, schemaName) + HttpRequestExecutorProxy executor) + : base(next, executor) { - RequestParser = requestParser ?? - throw new ArgumentNullException(nameof(requestParser)); - DiagnosticEvents = diagnosticEvents ?? - throw new ArgumentNullException(nameof(diagnosticEvents)); } - protected IHttpRequestParser RequestParser { get; } - - protected IServerDiagnosticEvents DiagnosticEvents { get; } - public virtual async Task InvokeAsync(HttpContext context) { - if (HttpMethods.IsPost(context.Request.Method) && - ParseContentType(context) is RequestContentType.Json) + if (HttpMethods.IsPost(context.Request.Method) + && context.ParseContentType() is RequestContentType.Json) { - if (!IsDefaultSchema) - { - context.Items[WellKnownContextData.SchemaName] = SchemaName; - } + var ct = context.RequestAborted; + var session = await Executor.GetOrCreateSessionAsync(ct); - using (DiagnosticEvents.ExecuteHttpRequest(context, HttpRequestKind.HttpPost)) + using (session.DiagnosticEvents.ExecuteHttpRequest(context, HttpRequestKind.HttpPost)) { - await HandleRequestAsync(context); + await HandleRequestAsync(context, session, ct); } } else @@ -58,17 +43,11 @@ public virtual async Task InvokeAsync(HttpContext context) } } - protected async Task HandleRequestAsync(HttpContext context) + protected async Task HandleRequestAsync(HttpContext context, ExecutorSession session, CancellationToken ct) { HttpStatusCode? statusCode = null; IExecutionResult? result; - // first we need to get the request executor to be able to execute requests. - var requestExecutor = await GetExecutorAsync(context.RequestAborted); - var requestInterceptor = requestExecutor.GetRequestInterceptor(); - var errorHandler = requestExecutor.GetErrorHandler(); - context.Items[WellKnownContextData.RequestExecutor] = requestExecutor; - // next we will inspect the accept headers and determine if we can execute this request. var headerResult = HeaderUtilities.GetAcceptHeader(context.Request); var acceptMediaTypes = headerResult.AcceptMediaTypes; @@ -84,11 +63,11 @@ protected async Task HandleRequestAsync(HttpContext context) var errors = headerResult.ErrorResult.Errors!; result = headerResult.ErrorResult; - DiagnosticEvents.HttpRequestError(context, errors[0]); + session.DiagnosticEvents.HttpRequestError(context, errors[0]); goto HANDLE_RESULT; } - var requestFlags = CreateRequestFlags(headerResult.AcceptMediaTypes); + var requestFlags = session.CreateRequestFlags(headerResult.AcceptMediaTypes); // if the request defines accept header values of which we cannot handle any provided // media type then we will fail the request with 406 Not Acceptable. @@ -101,18 +80,18 @@ protected async Task HandleRequestAsync(HttpContext context) var error = ErrorHelper.NoSupportedAcceptMediaType(); result = OperationResultBuilder.CreateError(error); - DiagnosticEvents.HttpRequestError(context, error); + session.DiagnosticEvents.HttpRequestError(context, error); goto HANDLE_RESULT; } // next we parse the GraphQL request. IReadOnlyList requests; - using (DiagnosticEvents.ParseHttpRequest(context)) + using (session.DiagnosticEvents.ParseHttpRequest(context)) { try { - requests = await ParseRequestsFromBodyAsync(context.Request, context.RequestAborted); + requests = await ParseRequestsFromBodyAsync(context, session); } catch (GraphQLRequestException ex) { @@ -120,9 +99,9 @@ protected async Task HandleRequestAsync(HttpContext context) // parsed. In this case we will return HTTP status code 400 and return a // GraphQL error result. statusCode = HttpStatusCode.BadRequest; - var errors = errorHandler.Handle(ex.Errors); + var errors = session.Handle(ex.Errors); result = OperationResultBuilder.CreateError(errors); - DiagnosticEvents.ParserErrors(context, errors); + session.DiagnosticEvents.ParserErrors(context, errors); goto HANDLE_RESULT; } catch (Exception ex) @@ -130,7 +109,7 @@ protected async Task HandleRequestAsync(HttpContext context) statusCode = HttpStatusCode.InternalServerError; var error = ErrorBuilder.FromException(ex).Build(); result = OperationResultBuilder.CreateError(error); - DiagnosticEvents.HttpRequestError(context, error); + session.DiagnosticEvents.HttpRequestError(context, error); goto HANDLE_RESULT; } } @@ -145,9 +124,9 @@ protected async Task HandleRequestAsync(HttpContext context) case 0: { statusCode = HttpStatusCode.BadRequest; - var error = errorHandler.Handle(ErrorHelper.RequestHasNoElements()); + var error = session.Handle(ErrorHelper.RequestHasNoElements()); result = OperationResultBuilder.CreateError(error); - DiagnosticEvents.HttpRequestError(context, error); + session.DiagnosticEvents.HttpRequestError(context, error); break; } @@ -166,21 +145,14 @@ protected async Task HandleRequestAsync(HttpContext context) TryParseOperations(operationNames, out var ops) && GetOptions(context).EnableBatching) { - result = await ExecuteOperationBatchAsync( - context, - requestExecutor, - requestInterceptor, - DiagnosticEvents, - requests[0], - requestFlags, - ops); + result = await session.ExecuteOperationBatchAsync(context, requests[0], requestFlags, ops); } else { - var error = errorHandler.Handle(ErrorHelper.InvalidRequest()); + var error = session.Handle(ErrorHelper.InvalidRequest()); statusCode = HttpStatusCode.BadRequest; result = OperationResultBuilder.CreateError(error); - DiagnosticEvents.HttpRequestError(context, error); + session.DiagnosticEvents.HttpRequestError(context, error); } break; @@ -194,13 +166,7 @@ protected async Task HandleRequestAsync(HttpContext context) // a single GraphQL query or mutation. case 1: { - result = await ExecuteSingleAsync( - context, - requestExecutor, - requestInterceptor, - DiagnosticEvents, - requests[0], - requestFlags); + result = await session.ExecuteSingleAsync(context, requests[0], requestFlags); break; } @@ -210,20 +176,14 @@ protected async Task HandleRequestAsync(HttpContext context) default: if (GetOptions(context).EnableBatching) { - result = await ExecuteBatchAsync( - context, - requestExecutor, - requestInterceptor, - DiagnosticEvents, - requests, - requestFlags); + result = await session.ExecuteBatchAsync(context, requests, requestFlags); } else { - var error = errorHandler.Handle(ErrorHelper.InvalidRequest()); + var error = session.Handle(ErrorHelper.InvalidRequest()); statusCode = HttpStatusCode.BadRequest; result = OperationResultBuilder.CreateError(error); - DiagnosticEvents.HttpRequestError(context, error); + session.DiagnosticEvents.HttpRequestError(context, error); } break; } @@ -236,7 +196,7 @@ protected async Task HandleRequestAsync(HttpContext context) foreach (var error in ex.Errors) { - DiagnosticEvents.HttpRequestError(context, error); + session.DiagnosticEvents.HttpRequestError(context, error); } } catch (Exception ex) @@ -244,7 +204,7 @@ protected async Task HandleRequestAsync(HttpContext context) statusCode = HttpStatusCode.InternalServerError; var error = ErrorBuilder.FromException(ex).Build(); result = OperationResultBuilder.CreateError(error); - DiagnosticEvents.HttpRequestError(context, error); + session.DiagnosticEvents.HttpRequestError(context, error); } HANDLE_RESULT: @@ -254,7 +214,7 @@ protected async Task HandleRequestAsync(HttpContext context) { // if cancellation is requested we will not try to attempt to write the result to the // response stream. - if (context.RequestAborted.IsCancellationRequested) + if (ct.IsCancellationRequested) { return; } @@ -265,10 +225,10 @@ protected async Task HandleRequestAsync(HttpContext context) if (result is IOperationResult queryResult) { - formatScope = DiagnosticEvents.FormatHttpResponse(context, queryResult); + formatScope = session.DiagnosticEvents.FormatHttpResponse(context, queryResult); } - await WriteResultAsync(context, result, acceptMediaTypes, statusCode); + await session.WriteResultAsync(context, result, acceptMediaTypes, statusCode); } finally { @@ -284,10 +244,10 @@ protected async Task HandleRequestAsync(HttpContext context) } } - protected virtual ValueTask> ParseRequestsFromBodyAsync( - HttpRequest request, - CancellationToken cancellationToken) - => RequestParser.ParseRequestAsync(request.Body, cancellationToken); + protected virtual async ValueTask> ParseRequestsFromBodyAsync( + HttpContext context, + ExecutorSession session) + => await session.RequestParser.ParseRequestAsync(context.Request.Body, context.RequestAborted); private static bool TryParseOperations( string operationNameString, diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpRequestExecutorProxy.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpRequestExecutorProxy.cs new file mode 100644 index 00000000000..eceba55afff --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpRequestExecutorProxy.cs @@ -0,0 +1,44 @@ +using HotChocolate.Features; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.AspNetCore; + +public sealed class HttpRequestExecutorProxy( + IRequestExecutorProvider executorProvider, + IRequestExecutorEvents executorEvents, + string schemaName) + : RequestExecutorProxy(executorProvider, executorEvents, schemaName) +{ + private ExecutorSession? _session; + + public async ValueTask GetOrCreateSessionAsync(CancellationToken cancellationToken) + { + if (_session is not null) + { + return _session; + } + + var executor = await GetExecutorAsync(cancellationToken); + return executor.Features.GetRequired(); + } + + protected override void OnRequestExecutorUpdated(IRequestExecutor? executor) + { + if (executor is null) + { + _session = null; + return; + } + + var session = new ExecutorSession(executor); + executor.Features.Set(session); + _session = session; + } + + public static HttpRequestExecutorProxy Create(IServiceProvider services, string schemaName) + { + var executorProvider = services.GetRequiredService(); + var executorEvents = services.GetRequiredService(); + return new HttpRequestExecutorProxy(executorProvider, executorEvents, schemaName); + } +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpTransportVersion.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpTransportVersion.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/HttpTransportVersion.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpTransportVersion.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Instrumentation/AggregateServerDiagnosticEventListener.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/AggregateServerDiagnosticEventListener.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Instrumentation/AggregateServerDiagnosticEventListener.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/AggregateServerDiagnosticEventListener.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Instrumentation/HttpRequestKind.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/HttpRequestKind.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Instrumentation/HttpRequestKind.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/HttpRequestKind.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Instrumentation/IServerDiagnosticEventListener.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/IServerDiagnosticEventListener.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Instrumentation/IServerDiagnosticEventListener.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/IServerDiagnosticEventListener.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Instrumentation/IServerDiagnosticEvents.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/IServerDiagnosticEvents.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Instrumentation/IServerDiagnosticEvents.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/IServerDiagnosticEvents.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Instrumentation/NoopServerDiagnosticEventListener.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/NoopServerDiagnosticEventListener.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Instrumentation/NoopServerDiagnosticEventListener.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/NoopServerDiagnosticEventListener.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Instrumentation/ServerDiagnosticEventListener.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/ServerDiagnosticEventListener.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Instrumentation/ServerDiagnosticEventListener.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Instrumentation/ServerDiagnosticEventListener.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultHttpRequestInterceptor.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DefaultHttpRequestInterceptor.cs similarity index 91% rename from src/HotChocolate/AspNetCore/src/AspNetCore/DefaultHttpRequestInterceptor.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DefaultHttpRequestInterceptor.cs index 9e84d365173..d00fabebeff 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultHttpRequestInterceptor.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DefaultHttpRequestInterceptor.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using Microsoft.AspNetCore.Http; +using static HotChocolate.ExecutionContextData; namespace HotChocolate.AspNetCore; @@ -28,7 +29,7 @@ public virtual ValueTask OnCreateAsync( if (context.IncludeQueryPlan()) { - requestBuilder.TryAddGlobalState(WellKnownContextData.IncludeQueryPlan, true); + requestBuilder.TryAddGlobalState(IncludeQueryPlan, true); } if (context.TryGetCostSwitch() is { } costSwitch) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultSocketSessionInterceptor.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DefaultSocketSessionInterceptor.cs similarity index 98% rename from src/HotChocolate/AspNetCore/src/AspNetCore/DefaultSocketSessionInterceptor.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DefaultSocketSessionInterceptor.cs index 1f39484992d..ac884615557 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultSocketSessionInterceptor.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DefaultSocketSessionInterceptor.cs @@ -3,7 +3,7 @@ using HotChocolate.AspNetCore.Subscriptions; using HotChocolate.AspNetCore.Subscriptions.Protocols; using Microsoft.Extensions.DependencyInjection; -using static HotChocolate.WellKnownContextData; +using static HotChocolate.ExecutionContextData; namespace HotChocolate.AspNetCore; diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DelegateHttpRequestInterceptor.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DelegateHttpRequestInterceptor.cs new file mode 100644 index 00000000000..44504b2a80a --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DelegateHttpRequestInterceptor.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Http; + +namespace HotChocolate.AspNetCore; + +internal sealed class DelegateHttpRequestInterceptor( + Func handler) + : DefaultHttpRequestInterceptor +{ + public override async ValueTask OnCreateAsync(HttpContext context, IRequestExecutor requestExecutor, + OperationRequestBuilder requestBuilder, + CancellationToken cancellationToken) + { + await handler(context, requestExecutor, requestBuilder, cancellationToken); + await base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken); + } +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/IHttpRequestInterceptor.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/IHttpRequestInterceptor.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/IHttpRequestInterceptor.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/IHttpRequestInterceptor.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/ISocketSessionInterceptor.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/ISocketSessionInterceptor.cs similarity index 98% rename from src/HotChocolate/AspNetCore/src/AspNetCore/ISocketSessionInterceptor.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/ISocketSessionInterceptor.cs index d9af946aab2..6945d03bca1 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/ISocketSessionInterceptor.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/ISocketSessionInterceptor.cs @@ -4,7 +4,7 @@ namespace HotChocolate.AspNetCore; /// -/// The socket interceptor allows to customize the GraphQL over Websocket behavior. +/// The socket interceptor allows customizing the GraphQL over Websocket behavior. /// public interface ISocketSessionInterceptor { diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/MiddlewareBase.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/MiddlewareBase.cs new file mode 100644 index 00000000000..82288ce5a71 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/MiddlewareBase.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Http; +using RequestDelegate = Microsoft.AspNetCore.Http.RequestDelegate; + +namespace HotChocolate.AspNetCore; + +/// +/// The Hot Chocolate ASP.NET core middleware base class. +/// +public abstract class MiddlewareBase : IDisposable +{ + private readonly RequestDelegate _next; + private GraphQLServerOptions? _options; + private bool _disposed; + + protected MiddlewareBase( + RequestDelegate next, + HttpRequestExecutorProxy executor) + { + ArgumentNullException.ThrowIfNull(next); + ArgumentNullException.ThrowIfNull(executor); + + _next = next; + Executor = executor; + } + + /// + /// Gets the request executor proxy. + /// + protected HttpRequestExecutorProxy Executor { get; } + + /// + /// Invokes the next middleware in line. + /// + /// + /// The . + /// + protected Task NextAsync(HttpContext context) => _next(context); + + protected GraphQLServerOptions GetOptions(HttpContext context) + { + if (_options is not null) + { + return _options; + } + + _options = context.GetGraphQLServerOptions() ?? new GraphQLServerOptions(); + return _options; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed && disposing) + { + Executor.Dispose(); + _disposed = true; + } + } +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/MiddlewareFactory.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/MiddlewareFactory.cs new file mode 100644 index 00000000000..4be3a60b7a5 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/MiddlewareFactory.cs @@ -0,0 +1,77 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Options; +using RequestDelegate = Microsoft.AspNetCore.Http.RequestDelegate; + +namespace HotChocolate.AspNetCore; + +internal static class MiddlewareFactory +{ + internal static Func CreateCancellationMiddleware() + { + return next => async context => + { + try + { + await next(context); + } + catch (OperationCanceledException) + { + // we just catch cancellations here and do nothing. + } + }; + } + + internal static Func CreateWebSocketSubscriptionMiddleware( + HttpRequestExecutorProxy executor) + { + return next => context => + { + var middleware = new WebSocketSubscriptionMiddleware(next, executor); + return middleware.InvokeAsync(context); + }; + } + + internal static Func CreateHttpPostMiddleware( + HttpRequestExecutorProxy executor) + { + return next => context => + { + var middleware = new HttpPostMiddleware(next, executor); + return middleware.InvokeAsync(context); + }; + } + + internal static Func CreateHttpMultipartMiddleware( + HttpRequestExecutorProxy executor, + IOptions formOptions) + { + return next => context => + { + var middleware = new HttpMultipartMiddleware(next, executor, formOptions); + return middleware.InvokeAsync(context); + }; + } + + internal static Func CreateHttpGetMiddleware( + HttpRequestExecutorProxy executor) + { + return next => context => + { + var middleware = new HttpGetMiddleware(next, executor); + return middleware.InvokeAsync(context); + }; + } + + internal static Func CreateHttpGetSchemaMiddleware( + HttpRequestExecutorProxy executor, + PathString path, + MiddlewareRoutingType routingType) + { + return next => context => + { + var middleware = new HttpGetSchemaMiddleware(next, executor, path, routingType); + return middleware.InvokeAsync(context); + }; + } +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/MiddlewareRoutingType.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/MiddlewareRoutingType.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/MiddlewareRoutingType.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/MiddlewareRoutingType.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/GraphQLHttpOptions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Options/GraphQLHttpOptions.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/GraphQLHttpOptions.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Options/GraphQLHttpOptions.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/GraphQLServerOptions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Options/GraphQLServerOptions.cs similarity index 89% rename from src/HotChocolate/AspNetCore/src/AspNetCore/GraphQLServerOptions.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Options/GraphQLServerOptions.cs index 168945629ef..c603a0f87eb 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/GraphQLServerOptions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Options/GraphQLServerOptions.cs @@ -21,6 +21,11 @@ public sealed class GraphQLServerOptions public AllowedGetOperations AllowedGetOperations { get; set; } = AllowedGetOperations.Query; + /// + /// Specifies that the transport is allowed to provide the schema SDL document as a file. + /// + public bool EnableSchemaFileSupport { get; set; } = true; + /// /// Defines if GraphQL HTTP GET requests are allowed. /// diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/GraphQLSocketOptions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Options/GraphQLSocketOptions.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/GraphQLSocketOptions.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Options/GraphQLSocketOptions.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/GraphQLToolOptions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Options/GraphQLToolOptions.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/GraphQLToolOptions.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Options/GraphQLToolOptions.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/GraphQLToolServeMode.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Options/GraphQLToolServeMode.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/GraphQLToolServeMode.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Options/GraphQLToolServeMode.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpRequestParser.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs similarity index 81% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpRequestParser.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs index de6b88f645b..49e7e04f995 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpRequestParser.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs @@ -1,13 +1,15 @@ // ReSharper disable RedundantSuppressNullableWarningExpression using System.Buffers; -using Microsoft.AspNetCore.Http; +using System.Text; +using HotChocolate.AspNetCore.Utilities; +using HotChocolate.Buffers; using HotChocolate.Language; -using HotChocolate.Utilities; -using static HotChocolate.AspNetCore.ThrowHelper; +using Microsoft.AspNetCore.Http; +using static HotChocolate.AspNetCore.Utilities.ThrowHelper; using static HotChocolate.Language.Utf8GraphQLRequestParser; -namespace HotChocolate.AspNetCore.Serialization; +namespace HotChocolate.AspNetCore.Parsers; internal sealed class DefaultHttpRequestParser : IHttpRequestParser { @@ -18,6 +20,7 @@ internal sealed class DefaultHttpRequestParser : IHttpRequestParser private const string VariablesKey = "variables"; internal const string ExtensionsKey = "extensions"; + private static readonly Encoding s_utf8 = Encoding.UTF8; private readonly IDocumentCache _documentCache; private readonly IDocumentHashProvider _documentHashProvider; private readonly ParserOptions _parserOptions; @@ -54,24 +57,28 @@ public async ValueTask ParsePersistedOperationRequestAsync( try { - GraphQLRequest Parse(byte[] buffer, int length) - => ParsePersistedOperationRequest(buffer, length, documentId, operationName); - - return await BufferHelper.ReadAsync( - requestBody, - Parse, - _maxRequestSize, - static (buffer, bytesBuffered, p) => + const int chunkSize = 256; + using var writer = new PooledArrayWriter(); + var read = 0; + + do + { + var memory = writer.GetMemory(chunkSize); + read = await requestBody.ReadAsync(memory, cancellationToken).ConfigureAwait(false); + writer.Advance(read); + + if (_maxRequestSize < writer.Length) { - if (bytesBuffered == 0) - { - throw DefaultHttpRequestParser_RequestIsEmpty(); - } - - return p(buffer, bytesBuffered); - }, - static () => throw DefaultHttpRequestParser_MaxRequestSizeExceeded(), - cancellationToken); + throw DefaultHttpRequestParser_MaxRequestSizeExceeded(); + } + } while (read == chunkSize); + + if (writer.Length == 0) + { + throw DefaultHttpRequestParser_RequestIsEmpty(); + } + + return ParsePersistedOperationRequest(writer.WrittenSpan, documentId, operationName); } catch (GraphQLRequestException) { @@ -216,7 +223,9 @@ public GraphQLRequest ParsePersistedOperationRequestFromParams( ? stackalloc byte[length] : source = ArrayPool.Shared.Rent(length); - Utf8GraphQLParser.ConvertToBytes(sourceText, ref sourceSpan); + var written = Encoding.UTF8.GetBytes(sourceText, sourceSpan); + sourceSpan = sourceSpan[..written]; + var document = Utf8GraphQLParser.Parse(sourceSpan, _parserOptions); var documentHash = _documentHashProvider.ComputeHash(sourceSpan); @@ -248,23 +257,28 @@ private async ValueTask> ReadAsync( { try { - Func> parse = ParseRequest; + const int chunkSize = 256; + using var writer = new PooledArrayWriter(); + int read; + + do + { + var memory = writer.GetMemory(chunkSize); + read = await stream.ReadAsync(memory, cancellationToken).ConfigureAwait(false); + writer.Advance(read); - return await BufferHelper.ReadAsync( - stream, - parse, - _maxRequestSize, - static (buffer, bytesBuffered, p) => + if (_maxRequestSize < writer.Length) { - if (bytesBuffered == 0) - { - throw DefaultHttpRequestParser_RequestIsEmpty(); - } - - return p(buffer, bytesBuffered); - }, - static () => throw DefaultHttpRequestParser_MaxRequestSizeExceeded(), - cancellationToken); + throw DefaultHttpRequestParser_MaxRequestSizeExceeded(); + } + } while (read == chunkSize); + + if (writer.Length == 0) + { + throw DefaultHttpRequestParser_RequestIsEmpty(); + } + + return ParseRequest(writer.WrittenSpan); } catch (GraphQLRequestException) { @@ -281,14 +295,10 @@ private async ValueTask> ReadAsync( } private IReadOnlyList ParseRequest( - byte[] buffer, - int bytesBuffered) + ReadOnlySpan request) { - var graphQLData = new ReadOnlySpan(buffer); - graphQLData = graphQLData[..bytesBuffered]; - var requestParser = new Utf8GraphQLRequestParser( - graphQLData, + request, _parserOptions, _documentCache, _documentHashProvider); @@ -297,16 +307,12 @@ private IReadOnlyList ParseRequest( } private GraphQLRequest ParsePersistedOperationRequest( - byte[] buffer, - int bytesBuffered, + ReadOnlySpan request, string documentId, string? operationName) { - var graphQLData = new ReadOnlySpan(buffer); - graphQLData = graphQLData[..bytesBuffered]; - var requestParser = new Utf8GraphQLRequestParser( - graphQLData, + request, _parserOptions, _documentCache, _documentHashProvider); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/HttpMultipartRequest.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/HttpMultipartRequest.cs new file mode 100644 index 00000000000..414797d254f --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/HttpMultipartRequest.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.AspNetCore.Parsers; + +internal sealed class HttpMultipartRequest(string operations, IDictionary fileMap) +{ + public string Operations { get; set; } = operations; + + public IDictionary FileMap { get; set; } = fileMap; +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IHttpRequestParser.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/IHttpRequestParser.cs similarity index 98% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IHttpRequestParser.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/IHttpRequestParser.cs index 8f94f65a168..d38549a4315 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IHttpRequestParser.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/IHttpRequestParser.cs @@ -1,7 +1,7 @@ -using Microsoft.AspNetCore.Http; using HotChocolate.Language; +using Microsoft.AspNetCore.Http; -namespace HotChocolate.AspNetCore.Serialization; +namespace HotChocolate.AspNetCore.Parsers; /// /// A helper to parse GraphQL HTTP requests. diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IVariablePathSegment.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/IVariablePathSegment.cs similarity index 63% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IVariablePathSegment.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/IVariablePathSegment.cs index b2f7787f410..7b69952d517 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IVariablePathSegment.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/IVariablePathSegment.cs @@ -1,4 +1,4 @@ -namespace HotChocolate.AspNetCore.Serialization; +namespace HotChocolate.AspNetCore.Parsers; internal interface IVariablePathSegment { diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IndexPathSegment.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/IndexPathSegment.cs similarity index 84% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IndexPathSegment.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/IndexPathSegment.cs index a502e3eb549..9c7f75d1c89 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/IndexPathSegment.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/IndexPathSegment.cs @@ -1,4 +1,4 @@ -namespace HotChocolate.AspNetCore.Serialization; +namespace HotChocolate.AspNetCore.Parsers; internal class IndexPathSegment : IVariablePathSegment { diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/KeyPathSegment.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/KeyPathSegment.cs similarity index 84% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/KeyPathSegment.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/KeyPathSegment.cs index 03856a11659..d1acc50e606 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/KeyPathSegment.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/KeyPathSegment.cs @@ -1,4 +1,4 @@ -namespace HotChocolate.AspNetCore.Serialization; +namespace HotChocolate.AspNetCore.Parsers; internal class KeyPathSegment : IVariablePathSegment { diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/UploadedFile.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/UploadedFile.cs new file mode 100644 index 00000000000..745b74a68e5 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/UploadedFile.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Http; + +namespace HotChocolate.AspNetCore.Parsers; + +internal sealed class UploadedFile(IFormFile file) : IFile +{ + public string Name => file.FileName; + + public long? Length => file.Length; + + public string? ContentType => file.ContentType; + + public Task CopyToAsync(Stream target, CancellationToken cancellationToken = default) + => file.CopyToAsync(target, cancellationToken); + + public Stream OpenReadStream() + => file.OpenReadStream(); +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/VariablePath.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/VariablePath.cs similarity index 81% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/VariablePath.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/VariablePath.cs index 07142f8db06..3e340f883cb 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/VariablePath.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/VariablePath.cs @@ -1,15 +1,11 @@ -using static HotChocolate.AspNetCore.Properties.AspNetCoreResources; +using HotChocolate.AspNetCore.Utilities; +using static HotChocolate.AspNetCore.Properties.AspNetCorePipelineResources; -namespace HotChocolate.AspNetCore.Serialization; +namespace HotChocolate.AspNetCore.Parsers; -internal class VariablePath +internal sealed class VariablePath(KeyPathSegment key) { - public VariablePath(KeyPathSegment key) - { - Key = key; - } - - public KeyPathSegment Key { get; } + public KeyPathSegment Key { get; } = key; public static VariablePath Parse(string s) { diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/PersistedOperationMiddleware.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/PersistedOperationMiddleware.cs new file mode 100644 index 00000000000..239de43502d --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/PersistedOperationMiddleware.cs @@ -0,0 +1,282 @@ +using System.Net; +using HotChocolate.AspNetCore.Utilities; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace HotChocolate.AspNetCore; + +internal static class PersistedOperationMiddleware +{ + internal static void MapPersistedOperationMiddleware( + this RouteGroupBuilder groupBuilder, + IServiceProvider services, + string schemaName, + bool requireOperationName) + { + var optionsHolder = new OptionsHolder(); + var executorProxy = HttpRequestExecutorProxy.Create(services, schemaName); + + groupBuilder.MapGet( + "/{operationId}", + async context => + { + var options = optionsHolder.GetOptions(context); + var operationId = context.Request.RouteValues["operationId"] as string; + + if (string.IsNullOrEmpty(operationId)) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsync("Missing operationId", context.RequestAborted); + return; + } + + await ExecuteGetRequestAsync( + context, + executorProxy, + options, + operationId, + operationName: null, + requireOperationName); + }); + + groupBuilder + .MapGet( + "/{operationId}/{operationName}", + async context => + { + var options = optionsHolder.GetOptions(context); + var operationId = context.Request.RouteValues["operationId"] as string; + var operationName = context.Request.RouteValues["operationName"] as string; + + if (string.IsNullOrEmpty(operationId)) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsync("Missing operationId", context.RequestAborted); + return; + } + + if (string.IsNullOrEmpty(operationName)) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsync("Missing operationName", context.RequestAborted); + return; + } + + await ExecuteGetRequestAsync( + context, + executorProxy, + options, + operationId, + operationName, + requireOperationName); + }); + + groupBuilder + .MapPost( + "/{operationId}", + async context => + { + var operationId = context.Request.RouteValues["operationId"] as string; + + if (string.IsNullOrEmpty(operationId)) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsync("Missing operationId", context.RequestAborted); + return; + } + + await ExecutePostRequestAsync( + context, + executorProxy, + operationId, + operationName: null, + requireOperationName); + }); + + groupBuilder + .MapPost( + "/{operationId}/{operationName}", + async context => + { + var operationId = context.Request.RouteValues["operationId"] as string; + var operationName = context.Request.RouteValues["operationName"] as string; + + if (string.IsNullOrEmpty(operationId)) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsync("Missing operationId", context.RequestAborted); + return; + } + + if (string.IsNullOrEmpty(operationName)) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsync("Missing operationName", context.RequestAborted); + return; + } + + await ExecutePostRequestAsync( + context, + executorProxy, + operationId, + operationName, + requireOperationName); + }); + } + + private static async Task ExecuteGetRequestAsync( + HttpContext context, + HttpRequestExecutorProxy executorProxy, + GraphQLServerOptions options, + string operationId, + string? operationName, + bool requireOperationName) + { + HttpStatusCode? statusCode; + IExecutionResult? result; + var ct = context.RequestAborted; + var executorSession = await executorProxy.GetOrCreateSessionAsync(ct); + + // first, we validate the accept-headers. + var validationResult = MiddlewareHelper.ValidateAcceptContentType(context, executorSession); + var acceptMediaTypes = validationResult.AcceptMediaTypes; + + if (!validationResult.IsValid) + { + statusCode = validationResult.StatusCode.Value; + result = validationResult.Error; + goto HANDLE_RESULT; + } + + // validate if the operation name is required. + if (requireOperationName && string.IsNullOrWhiteSpace(operationName)) + { + statusCode = HttpStatusCode.BadRequest; + result = ErrorHelper.OperationNameRequired(); + goto HANDLE_RESULT; + } + + // next, we parse the GraphQL request. + var parserResult = + MiddlewareHelper.ParseVariablesAndExtensionsFromParams( + operationId, + operationName, + context, + executorSession); + + if (!parserResult.IsValid) + { + statusCode = parserResult.StatusCode.Value; + result = parserResult.Error; + goto HANDLE_RESULT; + } + + // before we can execute the request, we need to determine the request flags. + var request = parserResult.Request!; + var requestFlags = + MiddlewareHelper.DetermineHttpGetRequestFlags( + validationResult.RequestFlags, + options); + + // next, we will execute the request. + var executionResult = + await MiddlewareHelper.ExecuteRequestAsync( + request, + requestFlags, + context, + executorSession); + statusCode = executionResult.StatusCode; + result = executionResult.Result; + +HANDLE_RESULT: + await MiddlewareHelper.WriteResultAsync( + result!, + acceptMediaTypes, + statusCode, + context, + executorSession); + } + + private static async Task ExecutePostRequestAsync( + HttpContext context, + HttpRequestExecutorProxy executorProxy, + string operationId, + string? operationName, + bool requireOperationName) + { + HttpStatusCode? statusCode; + IExecutionResult? result; + var ct = context.RequestAborted; + var executorSession = await executorProxy.GetOrCreateSessionAsync(ct); + + // first, we validate the accept-headers. + var validationResult = MiddlewareHelper.ValidateAcceptContentType(context, executorSession); + + var acceptMediaTypes = validationResult.AcceptMediaTypes; + + if (!validationResult.IsValid) + { + statusCode = validationResult.StatusCode.Value; + result = validationResult.Error; + goto HANDLE_RESULT; + } + + // validate if the operation name is required. + if (requireOperationName && string.IsNullOrWhiteSpace(operationName)) + { + statusCode = HttpStatusCode.BadRequest; + result = ErrorHelper.OperationNameRequired(); + goto HANDLE_RESULT; + } + + // next, we parse the GraphQL request. + var parserResult = + await MiddlewareHelper.ParseSingleRequestFromBodyAsync( + operationId, + operationName, + context, + executorSession); + + if (!parserResult.IsValid) + { + statusCode = parserResult.StatusCode.Value; + result = parserResult.Error; + goto HANDLE_RESULT; + } + + // after successfully parsing the request, we now will attempt to execute the request. + var executionResult = + await MiddlewareHelper.ExecuteRequestAsync( + parserResult.Request!, + validationResult.RequestFlags, + context, + executorSession); + statusCode = executionResult.StatusCode; + result = executionResult.Result; + +HANDLE_RESULT: + await MiddlewareHelper.WriteResultAsync( + result!, + acceptMediaTypes, + statusCode, + context, + executorSession); + } + + private sealed class OptionsHolder + { + private GraphQLServerOptions? _options; + + public GraphQLServerOptions GetOptions(HttpContext context) + { + if (_options is not null) + { + return _options; + } + + _options = context.GetGraphQLServerOptions() ?? new GraphQLServerOptions(); + return _options; + } + } +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Properties/AspNetCorePipelineResources.Designer.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Properties/AspNetCorePipelineResources.Designer.cs new file mode 100644 index 00000000000..0cc7e5e7751 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Properties/AspNetCorePipelineResources.Designer.cs @@ -0,0 +1,276 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace HotChocolate.AspNetCore.Properties { + using System; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class AspNetCorePipelineResources { + + private static System.Resources.ResourceManager resourceMan; + + private static System.Globalization.CultureInfo resourceCulture; + + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal AspNetCorePipelineResources() { + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { + get { + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("HotChocolate.AspNetCore.Properties.AspNetCorePipelineResources", typeof(AspNetCorePipelineResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + internal static string ThrowHelper_DefaultHttpRequestParser_RequestIsEmpty { + get { + return ResourceManager.GetString("ThrowHelper_DefaultHttpRequestParser_RequestIsEmpty", resourceCulture); + } + } + + internal static string ThrowHelper_DefaultHttpRequestParser_QueryAndIdMissing { + get { + return ResourceManager.GetString("ThrowHelper_DefaultHttpRequestParser_QueryAndIdMissing", resourceCulture); + } + } + + internal static string ThrowHelper_DefaultHttpRequestParser_MaxRequestSizeExceeded { + get { + return ResourceManager.GetString("ThrowHelper_DefaultHttpRequestParser_MaxRequestSizeExceeded", resourceCulture); + } + } + + internal static string ThrowHelper_DataStartMessageHandler_RequestTypeNotSupported { + get { + return ResourceManager.GetString("ThrowHelper_DataStartMessageHandler_RequestTypeNotSupported", resourceCulture); + } + } + + internal static string ThrowHelper_HttpMultipartMiddleware_Invalid_Form { + get { + return ResourceManager.GetString("ThrowHelper_HttpMultipartMiddleware_Invalid_Form", resourceCulture); + } + } + + internal static string ThrowHelper_HttpMultipartMiddleware_No_Operations_Specified { + get { + return ResourceManager.GetString("ThrowHelper_HttpMultipartMiddleware_No_Operations_Specified", resourceCulture); + } + } + + internal static string ThrowHelper_HttpMultipartMiddleware_Fields_Misordered { + get { + return ResourceManager.GetString("ThrowHelper_HttpMultipartMiddleware_Fields_Misordered", resourceCulture); + } + } + + internal static string HttpMultipartMiddleware_InsertFilesIntoRequest_VariablesImmutable { + get { + return ResourceManager.GetString("HttpMultipartMiddleware_InsertFilesIntoRequest_VariablesImmutable", resourceCulture); + } + } + + internal static string VariablePath_Parse_FirstSegmentMustBeKey { + get { + return ResourceManager.GetString("VariablePath_Parse_FirstSegmentMustBeKey", resourceCulture); + } + } + + internal static string ThrowHelper_HttpMultipartMiddleware_NoObjectPath { + get { + return ResourceManager.GetString("ThrowHelper_HttpMultipartMiddleware_NoObjectPath", resourceCulture); + } + } + + internal static string ThrowHelper_HttpMultipartMiddleware_FileMissing { + get { + return ResourceManager.GetString("ThrowHelper_HttpMultipartMiddleware_FileMissing", resourceCulture); + } + } + + internal static string ThrowHelper_HttpMultipartMiddleware_VariableNotFound { + get { + return ResourceManager.GetString("ThrowHelper_HttpMultipartMiddleware_VariableNotFound", resourceCulture); + } + } + + internal static string ThrowHelper_HttpMultipartMiddleware_VariableStructureInvalid { + get { + return ResourceManager.GetString("ThrowHelper_HttpMultipartMiddleware_VariableStructureInvalid", resourceCulture); + } + } + + internal static string ThrowHelper_HttpMultipartMiddleware_InvalidPath { + get { + return ResourceManager.GetString("ThrowHelper_HttpMultipartMiddleware_InvalidPath", resourceCulture); + } + } + + internal static string ThrowHelper_HttpMultipartMiddleware_PathMustStartWithVariable { + get { + return ResourceManager.GetString("ThrowHelper_HttpMultipartMiddleware_PathMustStartWithVariable", resourceCulture); + } + } + + internal static string ThrowHelper_HttpMultipartMiddleware_InvalidMapJson { + get { + return ResourceManager.GetString("ThrowHelper_HttpMultipartMiddleware_InvalidMapJson", resourceCulture); + } + } + + internal static string ThrowHelper_HttpMultipartMiddleware_MapNotSpecified { + get { + return ResourceManager.GetString("ThrowHelper_HttpMultipartMiddleware_MapNotSpecified", resourceCulture); + } + } + + internal static string ErrorHelper_InvalidRequest { + get { + return ResourceManager.GetString("ErrorHelper_InvalidRequest", resourceCulture); + } + } + + internal static string ErrorHelper_RequestHasNoElements { + get { + return ResourceManager.GetString("ErrorHelper_RequestHasNoElements", resourceCulture); + } + } + + internal static string WebSocketSession_SessionEnded { + get { + return ResourceManager.GetString("WebSocketSession_SessionEnded", resourceCulture); + } + } + + internal static string DataStartMessageHandler_Not_A_SubscriptionResult { + get { + return ResourceManager.GetString("DataStartMessageHandler_Not_A_SubscriptionResult", resourceCulture); + } + } + + internal static string TerminateConnectionMessageHandler_Message { + get { + return ResourceManager.GetString("TerminateConnectionMessageHandler_Message", resourceCulture); + } + } + + internal static string Apollo_OnReceive_MessageMustBeJson { + get { + return ResourceManager.GetString("Apollo_OnReceive_MessageMustBeJson", resourceCulture); + } + } + + internal static string Apollo_OnReceive_TypePropMissing { + get { + return ResourceManager.GetString("Apollo_OnReceive_TypePropMissing", resourceCulture); + } + } + + internal static string Apollo_OnReceive_ToManyInitializations { + get { + return ResourceManager.GetString("Apollo_OnReceive_ToManyInitializations", resourceCulture); + } + } + + internal static string Apollo_OnReceive_InvalidSubscribeMessage { + get { + return ResourceManager.GetString("Apollo_OnReceive_InvalidSubscribeMessage", resourceCulture); + } + } + + internal static string Apollo_OnReceive_SubscriptionIdNotUnique { + get { + return ResourceManager.GetString("Apollo_OnReceive_SubscriptionIdNotUnique", resourceCulture); + } + } + + internal static string Apollo_OnReceive_InvalidMessageType { + get { + return ResourceManager.GetString("Apollo_OnReceive_InvalidMessageType", resourceCulture); + } + } + + internal static string ConnectionStatus_Reject_Message_cannot_be_null_or_empty_ { + get { + return ResourceManager.GetString("ConnectionStatus_Reject_Message_cannot_be_null_or_empty_", resourceCulture); + } + } + + internal static string ErrorHelper_NoSupportedAcceptMediaType { + get { + return ResourceManager.GetString("ErrorHelper_NoSupportedAcceptMediaType", resourceCulture); + } + } + + internal static string ThrowHelper_Formatter_ResultKindNotSupported { + get { + return ResourceManager.GetString("ThrowHelper_Formatter_ResultKindNotSupported", resourceCulture); + } + } + + internal static string ThrowHelper_Formatter_ResponseContentTypeNotSupported { + get { + return ResourceManager.GetString("ThrowHelper_Formatter_ResponseContentTypeNotSupported", resourceCulture); + } + } + + internal static string ThrowHelper_Formatter_InvalidAcceptMediaType { + get { + return ResourceManager.GetString("ThrowHelper_Formatter_InvalidAcceptMediaType", resourceCulture); + } + } + + internal static string ErrorHelper_InvalidAcceptMediaType { + get { + return ResourceManager.GetString("ErrorHelper_InvalidAcceptMediaType", resourceCulture); + } + } + + internal static string ErrorHelper_TypeNotFound { + get { + return ResourceManager.GetString("ErrorHelper_TypeNotFound", resourceCulture); + } + } + + internal static string ErrorHelper_InvalidTypeName { + get { + return ResourceManager.GetString("ErrorHelper_InvalidTypeName", resourceCulture); + } + } + + internal static string ErrorHelper_TypeNameIsEmpty { + get { + return ResourceManager.GetString("ErrorHelper_TypeNameIsEmpty", resourceCulture); + } + } + + internal static string ErrorHelper_MultiPartRequestPreflightRequired { + get { + return ResourceManager.GetString("ErrorHelper_MultiPartRequestPreflightRequired", resourceCulture); + } + } + } +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Properties/AspNetCorePipelineResources.resx b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Properties/AspNetCorePipelineResources.resx new file mode 100644 index 00000000000..50718993e9c --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Properties/AspNetCorePipelineResources.resx @@ -0,0 +1,135 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The GraphQL request is empty. + + + Either the parameter query or the parameter id has to be set. + + + Max GraphQL request size reached. + + + The response type is not supported. + + + The multipart form could not be read. + + + No 'operations' specified. + + + Misordered multipart fields; 'map' should follow 'operations'. + + + The variables are expected to mutable at this point. + + + The first path segment must be a key. + + + No object paths specified for key '{0}' in 'map'. + + + File of key '{0}' is missing. + + + The variable path '{0}' is invalid. + + + The variable structure is invalid. + + + Invalid variable path `{0}` in `map`. + + + The variable path must start with `variables`. + + + Invalid JSON in the `map` multipart field; Expected type of Dictionary<string, string[]>. + + + No `map` specified. + + + Invalid GraphQL Request. + + + The GraphQL batch request has no elements. + + + Session ended. + + + The specified result object is not a valid subscription result. + + + Connection terminated by user. + + + A message must be a json object. + + + The type property on the message is mandatory. + + + Too many initialization requests. + + + Invalid subscribe message structure. + + + The subscription id is not unique. + + + Invalid message type. + + + Message cannot be null or empty. + + + None of the `Accept` header values is supported. + + + The execution result kind is not supported. + + + The specified response content-type `{0}` is not supported. + + + Invalid accept media types specified. + + + Unable to parse the accept header value `{0}`. + + + The type `{0}` does not exist. + + + The type name is invalid. + + + The specified types argument is empty. + + + Multi-part requests must include a GraphQL preflight header. + + diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/RequestContentType.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/RequestContentType.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/RequestContentType.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/RequestContentType.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/ResponseContentType.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/ResponseContentType.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/ResponseContentType.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/ResponseContentType.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/ConnectionCloseReason.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/ConnectionCloseReason.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/ConnectionCloseReason.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/ConnectionCloseReason.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/IOperationManager.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/IOperationManager.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/IOperationManager.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/IOperationManager.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/IOperationSession.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/IOperationSession.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/IOperationSession.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/IOperationSession.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/ISocketConnection.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/ISocketConnection.cs similarity index 87% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/ISocketConnection.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/ISocketConnection.cs index d4ffe15d38c..f8a50c7fa39 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/ISocketConnection.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/ISocketConnection.cs @@ -1,14 +1,15 @@ using HotChocolate.AspNetCore.Subscriptions.Protocols; +using HotChocolate.Features; using HotChocolate.Transport.Sockets; using Microsoft.AspNetCore.Http; namespace HotChocolate.AspNetCore.Subscriptions; /// -/// The socket connection represent an accepted connection with a socket +/// The socket connection represents an accepted connection with a socket /// where the protocol is already negotiated. /// -public interface ISocketConnection : ISocket, IHasContextData, IDisposable +public interface ISocketConnection : ISocket, IFeatureProvider, IDisposable { /// /// Gets access to the HTTP Context. @@ -25,6 +26,11 @@ public interface ISocketConnection : ISocket, IHasContextData, IDisposable /// CancellationToken RequestAborted { get; } + /// + /// Specifies if the connection is connected to a client. + /// + bool IsConnected { get; set; } + /// /// Tries to accept the connection and returns the accepted protocol handler. /// diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/ISocketSession.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/ISocketSession.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/ISocketSession.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/ISocketSession.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/OperationManager.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/OperationManager.cs similarity index 69% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/OperationManager.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/OperationManager.cs index 20103381636..3d42829e22d 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/OperationManager.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/OperationManager.cs @@ -1,6 +1,5 @@ using System.Collections; using HotChocolate.Language; -using Microsoft.Extensions.DependencyInjection; namespace HotChocolate.AspNetCore.Subscriptions; @@ -16,39 +15,40 @@ public sealed class OperationManager : IOperationManager private readonly CancellationTokenSource _cts; private readonly CancellationToken _cancellationToken; private readonly ISocketSession _socketSession; - private readonly ISocketSessionInterceptor _interceptor; - private readonly IErrorHandler _errorHandler; - private readonly IRequestExecutor _executor; + private readonly ExecutorSession _executorSession; private readonly Func _createSession; private bool _disposed; - public OperationManager( - ISocketSession socketSession, - ISocketSessionInterceptor interceptor, - IRequestExecutor executor) + public OperationManager(ISocketSession socketSession, ExecutorSession executorSession) { - _socketSession = socketSession ?? throw new ArgumentNullException(nameof(socketSession)); - _interceptor = interceptor ?? throw new ArgumentNullException(nameof(interceptor)); - _executor = executor ?? throw new ArgumentNullException(nameof(executor)); + ArgumentNullException.ThrowIfNull(socketSession); + ArgumentNullException.ThrowIfNull(executorSession); + + _socketSession = socketSession; + _executorSession = executorSession; + _createSession = CreateSession; - _errorHandler = executor.Schema.Services.GetRequiredService(); _cts = new CancellationTokenSource(); _cancellationToken = _cts.Token; + _createSession = CreateSession; } internal OperationManager( ISocketSession socketSession, - ISocketSessionInterceptor interceptor, - IRequestExecutor executor, + ExecutorSession executorSession, Func createSession) { - _socketSession = socketSession ?? throw new ArgumentNullException(nameof(socketSession)); - _interceptor = interceptor ?? throw new ArgumentNullException(nameof(interceptor)); - _executor = executor ?? throw new ArgumentNullException(nameof(executor)); - _createSession = createSession ?? throw new ArgumentNullException(nameof(createSession)); - _errorHandler = executor.Schema.Services.GetRequiredService(); + ArgumentNullException.ThrowIfNull(socketSession); + ArgumentNullException.ThrowIfNull(executorSession); + ArgumentNullException.ThrowIfNull(createSession); + + _socketSession = socketSession; + _executorSession = executorSession; + + _createSession = CreateSession; _cts = new CancellationTokenSource(); _cancellationToken = _cts.Token; + _createSession = createSession; } /// @@ -94,9 +94,8 @@ public bool Complete(string sessionId) try { - if (_subs.TryGetValue(sessionId, out var session)) + if (_subs.Remove(sessionId, out var session)) { - _subs.Remove(sessionId); session.Dispose(); return true; } @@ -110,7 +109,7 @@ public bool Complete(string sessionId) } private OperationSession CreateSession(string sessionId) - => new(_socketSession, _interceptor, _errorHandler, _executor, sessionId); + => new(_socketSession, _executorSession, sessionId); /// public void Dispose() diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/OperationSession.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/OperationSession.cs similarity index 89% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/OperationSession.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/OperationSession.cs index f60e2fb2791..56b54fe7441 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/OperationSession.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/OperationSession.cs @@ -9,24 +9,20 @@ internal sealed class OperationSession : IOperationSession private readonly CancellationToken _ct; private readonly ISocketSession _session; private readonly ISocketSessionInterceptor _interceptor; - private readonly IErrorHandler _errorHandler; - private readonly IRequestExecutor _executor; + private readonly ExecutorSession _executorSession; private bool _disposed; public event EventHandler? Completed; public OperationSession( ISocketSession session, - ISocketSessionInterceptor interceptor, - IErrorHandler errorHandler, - IRequestExecutor executor, + ExecutorSession executorSession, string id) { - _ct = _cts.Token; _session = session; - _interceptor = interceptor; - _errorHandler = errorHandler; - _executor = executor; + _executorSession = executorSession; + _interceptor = executorSession.SocketSessionInterceptor; + _ct = _cts.Token; Id = id; } @@ -47,7 +43,7 @@ private async Task SendResultsAsync(GraphQLRequest request, CancellationToken ca { var requestBuilder = CreateRequestBuilder(request); await _interceptor.OnRequestAsync(_session, Id, requestBuilder, ct); - await using var result = await _executor.ExecuteAsync(requestBuilder.Build(), ct); + await using var result = await _executorSession.ExecuteAsync(requestBuilder.Build(), ct); switch (result) { @@ -71,7 +67,7 @@ await _session.Protocol.SendErrorMessageAsync( { try { - // use original cancellation token here to keep the websocket open for other streams. + // use the original cancellation token here to keep the websocket open for other streams. await SendResultMessageAsync(item, cancellationToken); } finally @@ -82,8 +78,8 @@ await _session.Protocol.SendErrorMessageAsync( break; } - // the operation is completed and we will try to send a complete message. - // we mark completeTry true so that in case of an error we do not try to send this + // The operation is completed, and we will try to send a complete message. + // We mark 'completeTry' true so that in case of an error, we do not try to send this // message again. completeTry = true; @@ -175,7 +171,7 @@ private async Task TrySendErrorMessageAsync(Exception exception, CancellationTok if (!ct.IsCancellationRequested) { var error = ErrorBuilder.FromException(exception).Build(); - error = _errorHandler.Handle(error); + error = _executorSession.Handle(error); var errors = error is AggregateError aggregateError diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/PingPongJob.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/PingPongJob.cs similarity index 55% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/PingPongJob.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/PingPongJob.cs index a285cd3a4ff..07434a8000a 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/PingPongJob.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/PingPongJob.cs @@ -2,40 +2,31 @@ namespace HotChocolate.AspNetCore.Subscriptions; -internal sealed class PingPongJob +internal sealed class PingPongJob(ISocketSession session, GraphQLSocketOptions options) { - private readonly ISocketSession _session; - private readonly GraphQLSocketOptions _options; - - public PingPongJob(ISocketSession session, GraphQLSocketOptions options) - { - _session = session; - _options = options; - } - public async Task RunAsync(CancellationToken cancellationToken) { - var connection = _session.Connection; - var protocolHandler = _session.Protocol; + var connection = session.Connection; + var protocolHandler = session.Protocol; try { - // first we will wait for a connection to be established - await Task.Delay(_options.ConnectionInitializationTimeout, cancellationToken); + // first, we will wait for a connection to be established + await Task.Delay(options.ConnectionInitializationTimeout, cancellationToken); - // if after the timeout no connection initialization was send by the client we will + // if after the timeout no connection initialization was sent by the client, we will // close the connection. - if (!connection.ContextData.ContainsKey(ConnectionContextKeys.Connected)) + if (!connection.IsConnected) { - await _session.Protocol.OnConnectionInitTimeoutAsync(_session, cancellationToken); + await session.Protocol.OnConnectionInitTimeoutAsync(session, cancellationToken); return; } // if a keep alive interval is configured we will start sending connection // keep alive messages to the client. - if (_options.KeepAliveInterval is not null) + if (options.KeepAliveInterval is not null) { - var interval = _options.KeepAliveInterval.Value; + var interval = options.KeepAliveInterval.Value; while (!connection.IsClosed && !cancellationToken.IsCancellationRequested) { @@ -43,7 +34,7 @@ public async Task RunAsync(CancellationToken cancellationToken) if (!connection.IsClosed) { - await protocolHandler.SendKeepAliveMessageAsync(_session, cancellationToken); + await protocolHandler.SendKeepAliveMessageAsync(session, cancellationToken); } } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs similarity index 91% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs index 04e8ad4952b..f8541257cab 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs @@ -1,11 +1,10 @@ using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.Text.Json; -using HotChocolate.AspNetCore.Serialization; +using HotChocolate.AspNetCore.Formatters; using HotChocolate.Buffers; using HotChocolate.Language; -using static HotChocolate.AspNetCore.Properties.AspNetCoreResources; -using static HotChocolate.AspNetCore.Subscriptions.ConnectionContextKeys; +using static HotChocolate.AspNetCore.Properties.AspNetCorePipelineResources; using static HotChocolate.AspNetCore.Subscriptions.Protocols.Apollo.MessageProperties; using static HotChocolate.AspNetCore.Subscriptions.Protocols.MessageUtilities; using static HotChocolate.Language.Utf8GraphQLRequestParser; @@ -52,7 +51,7 @@ private async ValueTask OnReceiveInternalAsync( CancellationToken cancellationToken) { var connection = session.Connection; - var connected = connection.ContextData.ContainsKey(Connected); + var connected = connection.IsConnected; if (connected && message.IsSingleSegment && message.First.Equals(Utf8MessageBodies.KeepAlive)) @@ -108,7 +107,7 @@ await _interceptor.OnConnectAsync( if (connectionStatus.Accepted) { - connection.ContextData.Add(Connected, true); + connection.IsConnected = true; await SendConnectionAcceptMessage( session, connectionStatus.Extensions, @@ -149,7 +148,6 @@ await connection.CloseAsync( Apollo_OnReceive_SubscriptionIdNotUnique, ConnectionCloseReason.InternalServerError, cancellationToken); - return; } } catch (GraphQLRequestException ex) @@ -250,7 +248,7 @@ public async ValueTask SendResultMessageAsync( _formatter.Format(result, jsonWriter); jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(cancellationToken); - await session.Connection.SendAsync(arrayWriter.GetWrittenMemory(), cancellationToken); + await session.Connection.SendAsync(arrayWriter.WrittenMemory, cancellationToken); } public async ValueTask SendErrorMessageAsync( @@ -268,7 +266,7 @@ public async ValueTask SendErrorMessageAsync( _formatter.Format(errors[0], jsonWriter); jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(cancellationToken); - await session.Connection.SendAsync(arrayWriter.GetWrittenMemory(), cancellationToken); + await session.Connection.SendAsync(arrayWriter.WrittenMemory, cancellationToken); } public async ValueTask SendCompleteMessageAsync( @@ -277,21 +275,21 @@ public async ValueTask SendCompleteMessageAsync( CancellationToken cancellationToken) { using var writer = new PooledArrayWriter(); - SerializeMessage(writer, Utf8Messages.Complete, id: operationSessionId); - await session.Connection.SendAsync(writer.GetWrittenMemory(), cancellationToken); + SerializeMessage(writer, _formatter, Utf8Messages.Complete, id: operationSessionId); + await session.Connection.SendAsync(writer.WrittenMemory, cancellationToken); } - private static async ValueTask SendConnectionAcceptMessage( + private async ValueTask SendConnectionAcceptMessage( ISocketSession session, IReadOnlyDictionary? payload, CancellationToken cancellationToken) { using var writer = new PooledArrayWriter(); - SerializeMessage(writer, Utf8Messages.ConnectionAccept, payload); - await session.Connection.SendAsync(writer.GetWrittenMemory(), cancellationToken); + SerializeMessage(writer, _formatter, Utf8Messages.ConnectionAccept, payload); + await session.Connection.SendAsync(writer.WrittenMemory, cancellationToken); } - private static async ValueTask SendConnectionRejectMessage( + private async ValueTask SendConnectionRejectMessage( ISocketSession session, string message, IReadOnlyDictionary? extensions, @@ -299,17 +297,27 @@ private static async ValueTask SendConnectionRejectMessage( { using var arrayWriter = new PooledArrayWriter(); await using var jsonWriter = new Utf8JsonWriter(arrayWriter, WriterOptions); + jsonWriter.WriteStartObject(); jsonWriter.WriteString(MessageProperties.Type, Utf8Messages.ConnectionError); jsonWriter.WritePropertyName(Payload); jsonWriter.WriteStartObject(); jsonWriter.WriteString(Message, message); jsonWriter.WritePropertyName(MessageProperties.Extensions); - JsonSerializer.Serialize(jsonWriter, extensions); + + if (extensions is null) + { + jsonWriter.WriteNullValue(); + } + else + { + _formatter.Format(extensions, jsonWriter); + } + jsonWriter.WriteEndObject(); jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(cancellationToken); - await session.Connection.SendAsync(arrayWriter.GetWrittenMemory(), cancellationToken); + await session.Connection.SendAsync(arrayWriter.WrittenMemory, cancellationToken); } public ValueTask OnConnectionInitTimeoutAsync( diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/CloseReasons.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/CloseReasons.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/CloseReasons.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/CloseReasons.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/DataStartMessage.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/DataStartMessage.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/DataStartMessage.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/DataStartMessage.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/InitializeConnectionMessage.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/InitializeConnectionMessage.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/InitializeConnectionMessage.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/InitializeConnectionMessage.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/MessageProperties.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/MessageProperties.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/MessageProperties.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/MessageProperties.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/Messages.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/Messages.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/Messages.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/Messages.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/Utf8MessageBodies.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/Utf8MessageBodies.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/Utf8MessageBodies.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/Utf8MessageBodies.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/Utf8MessageProperties.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/Utf8MessageProperties.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/Utf8MessageProperties.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/Utf8MessageProperties.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/Utf8Messages.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/Utf8Messages.cs new file mode 100644 index 00000000000..898671b6b35 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/Utf8Messages.cs @@ -0,0 +1,22 @@ +namespace HotChocolate.AspNetCore.Subscriptions.Protocols.Apollo; + +internal static class Utf8Messages +{ + public static ReadOnlySpan ConnectionInitialize => "connection_init"u8; + + public static ReadOnlySpan ConnectionAccept => "connection_ack"u8; + + public static ReadOnlySpan ConnectionError => "connection_error"u8; + + public static ReadOnlySpan ConnectionTerminate => "connection_terminate"u8; + + public static ReadOnlySpan Start => "start"u8; + + public static ReadOnlySpan Stop => "stop"u8; + + public static ReadOnlySpan Data => "data"u8; + + public static ReadOnlySpan Error => "error"u8; + + public static ReadOnlySpan Complete => "complete"u8; +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/ConnectionStatus.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/ConnectionStatus.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/ConnectionStatus.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/ConnectionStatus.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/CloseReasons.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/CloseReasons.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/CloseReasons.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/CloseReasons.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/ConnectionExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/ConnectionExtensions.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/ConnectionExtensions.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/ConnectionExtensions.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/ConnectionInitMessage.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/ConnectionInitMessage.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/ConnectionInitMessage.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/ConnectionInitMessage.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs similarity index 89% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs index a35ef558788..de6ba6170e4 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs @@ -1,11 +1,12 @@ using System.Buffers; using System.Diagnostics.CodeAnalysis; +#if NET9_0_OR_GREATER using System.Runtime.InteropServices; +#endif using System.Text.Json; -using HotChocolate.AspNetCore.Serialization; +using HotChocolate.AspNetCore.Formatters; using HotChocolate.Buffers; using HotChocolate.Language; -using static HotChocolate.AspNetCore.Subscriptions.ConnectionContextKeys; using static HotChocolate.AspNetCore.Subscriptions.Protocols.GraphQLOverWebSocket.MessageProperties; using static HotChocolate.AspNetCore.Subscriptions.Protocols.MessageUtilities; using static HotChocolate.Language.Utf8GraphQLRequestParser; @@ -50,7 +51,7 @@ private async ValueTask OnReceiveInternalAsync( { var connection = session.Connection; - var connected = connection.ContextData.ContainsKey(Connected); + var connected = connection.IsConnected; using var document = JsonDocument.Parse(message); var root = document.RootElement; JsonElement idProp; @@ -114,7 +115,7 @@ await _interceptor.OnConnectAsync( if (connectionStatus.Accepted) { - connection.ContextData.Add(Connected, true); + connection.IsConnected = true; await SendConnectionAcceptMessage( session, connectionStatus.Extensions, @@ -149,7 +150,6 @@ await SendConnectionAcceptMessage( if (!session.Operations.Enqueue(subscribeMessage.Id, subscribeMessage.Payload)) { await connection.CloseSubscriptionIdNotUniqueAsync(cancellationToken); - return; } } catch (GraphQLRequestException ex) @@ -223,7 +223,7 @@ public async ValueTask SendResultMessageAsync( _formatter.Format(result, jsonWriter); jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(cancellationToken); - await session.Connection.SendAsync(arrayWriter.GetWrittenMemory(), cancellationToken); + await session.Connection.SendAsync(arrayWriter.WrittenMemory, cancellationToken); } public async ValueTask SendErrorMessageAsync( @@ -241,7 +241,7 @@ public async ValueTask SendErrorMessageAsync( _formatter.Format(errors, jsonWriter); jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(cancellationToken); - await session.Connection.SendAsync(arrayWriter.GetWrittenMemory(), cancellationToken); + await session.Connection.SendAsync(arrayWriter.WrittenMemory, cancellationToken); } public async ValueTask SendCompleteMessageAsync( @@ -250,8 +250,8 @@ public async ValueTask SendCompleteMessageAsync( CancellationToken cancellationToken) { using var writer = new PooledArrayWriter(); - SerializeMessage(writer, Utf8Messages.Complete, id: operationSessionId); - await session.Connection.SendAsync(writer.GetWrittenMemory(), cancellationToken); + SerializeMessage(writer, _formatter, Utf8Messages.Complete, id: operationSessionId); + await session.Connection.SendAsync(writer.WrittenMemory, cancellationToken); } public async ValueTask SendPingMessageAsync( @@ -266,12 +266,12 @@ public async ValueTask SendPingMessageAsync( else { using var writer = new PooledArrayWriter(); - SerializeMessage(writer, Utf8Messages.Ping, payload); - await session.Connection.SendAsync(writer.GetWrittenMemory(), cancellationToken); + SerializeMessage(writer, _formatter, Utf8Messages.Ping, payload); + await session.Connection.SendAsync(writer.WrittenMemory, cancellationToken); } } - private static async ValueTask SendPongMessageAsync( + private async ValueTask SendPongMessageAsync( ISocketSession session, IReadOnlyDictionary? payload, CancellationToken cancellationToken) @@ -283,19 +283,19 @@ private static async ValueTask SendPongMessageAsync( else { using var writer = new PooledArrayWriter(); - SerializeMessage(writer, Utf8Messages.Pong, payload); - await session.Connection.SendAsync(writer.GetWrittenMemory(), cancellationToken); + SerializeMessage(writer, _formatter, Utf8Messages.Pong, payload); + await session.Connection.SendAsync(writer.WrittenMemory, cancellationToken); } } - private static async ValueTask SendConnectionAcceptMessage( + private async ValueTask SendConnectionAcceptMessage( ISocketSession session, IReadOnlyDictionary? payload, CancellationToken cancellationToken) { using var writer = new PooledArrayWriter(); - SerializeMessage(writer, Utf8Messages.ConnectionAccept, payload); - await session.Connection.SendAsync(writer.GetWrittenMemory(), cancellationToken); + SerializeMessage(writer, _formatter, Utf8Messages.ConnectionAccept, payload); + await session.Connection.SendAsync(writer.WrittenMemory, cancellationToken); } public ValueTask OnConnectionInitTimeoutAsync( diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/IGraphQLOverWebSocketProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/IGraphQLOverWebSocketProtocolHandler.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/IGraphQLOverWebSocketProtocolHandler.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/IGraphQLOverWebSocketProtocolHandler.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/MessageProperties.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/MessageProperties.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/MessageProperties.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/MessageProperties.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/Messages.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/Messages.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/Messages.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/Messages.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/PingMessage.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/PingMessage.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/PingMessage.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/PingMessage.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/PongMessage.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/PongMessage.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/PongMessage.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/PongMessage.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/SubscribeMessage.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/SubscribeMessage.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/SubscribeMessage.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/SubscribeMessage.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/Utf8MessageBodies.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/Utf8MessageBodies.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/Utf8MessageBodies.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/Utf8MessageBodies.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/Utf8MessageProperties.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/Utf8MessageProperties.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/Utf8MessageProperties.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/Utf8MessageProperties.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/Utf8Messages.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/Utf8Messages.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/GraphQLOverWebSocket/Utf8Messages.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/Utf8Messages.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/IOperationMessagePayload.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/IOperationMessagePayload.cs new file mode 100644 index 00000000000..73ba430aef5 --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/IOperationMessagePayload.cs @@ -0,0 +1,14 @@ +using System.Text.Json; + +namespace HotChocolate.AspNetCore.Subscriptions.Protocols; + +/// +/// The operation message with a custom payload. +/// +public interface IOperationMessagePayload +{ + /// + /// Gets the JSON payload. + /// + JsonElement? Payload { get; } +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/IProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/IProtocolHandler.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/IProtocolHandler.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/IProtocolHandler.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/JsonOperationMessage.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/JsonOperationMessage.cs similarity index 67% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/JsonOperationMessage.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/JsonOperationMessage.cs index e10f73c2176..0c64d4416f1 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/JsonOperationMessage.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/JsonOperationMessage.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using static HotChocolate.AspNetCore.Subscriptions.Protocols.MessageUtilities; namespace HotChocolate.AspNetCore.Subscriptions.Protocols; @@ -14,8 +13,4 @@ protected JsonOperationMessage(string typeName, JsonElement? payload = null) : base(typeName, payload) { } - - /// - public T? As() where T : class - => Payload?.Deserialize(SerializerOptions); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/MessageUtilities.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/MessageUtilities.cs similarity index 91% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/MessageUtilities.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/MessageUtilities.cs index 3dafbdd4c61..7b2c2b36c3d 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/MessageUtilities.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/MessageUtilities.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using HotChocolate.AspNetCore.Formatters; using HotChocolate.AspNetCore.Subscriptions.Protocols.GraphQLOverWebSocket; using HotChocolate.Buffers; @@ -14,6 +15,7 @@ internal static class MessageUtilities public static void SerializeMessage( PooledArrayWriter pooledArrayWriter, + IWebSocketPayloadFormatter formatter, ReadOnlySpan type, IReadOnlyDictionary? payload = null, string? id = null) @@ -31,7 +33,7 @@ public static void SerializeMessage( if (payload is not null) { jsonWriter.WritePropertyName("payload"); - JsonSerializer.Serialize(jsonWriter, payload); + formatter.Format(payload, jsonWriter); } jsonWriter.WriteEndObject(); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/OperationMessage.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/OperationMessage.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/OperationMessage.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/OperationMessage.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/OperationMessage~1.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/OperationMessage~1.cs similarity index 100% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/OperationMessage~1.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/OperationMessage~1.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/WebSocketConnection.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/WebSocketConnection.cs similarity index 93% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/WebSocketConnection.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/WebSocketConnection.cs index 9c80f070d68..fc4252ffe9e 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/WebSocketConnection.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/WebSocketConnection.cs @@ -1,6 +1,7 @@ using System.Buffers; using System.Net.WebSockets; using HotChocolate.AspNetCore.Subscriptions.Protocols; +using HotChocolate.Features; using HotChocolate.Transport.Sockets; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -17,10 +18,12 @@ internal sealed class WebSocketConnection : ISocketConnection private WebSocket? _webSocket; private bool _disposed; - public WebSocketConnection(HttpContext httpContext) + public WebSocketConnection(HttpContext httpContext, IRequestExecutor executor) { - HttpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); - var executor = (IRequestExecutor)httpContext.Items[WellKnownContextData.RequestExecutor]!; + ArgumentNullException.ThrowIfNull(httpContext); + ArgumentNullException.ThrowIfNull(executor); + + HttpContext = httpContext; _protocolHandlers = executor.Schema.Services.GetServices().ToArray(); } @@ -35,7 +38,9 @@ public CancellationToken ApplicationStopping public CancellationToken RequestAborted => HttpContext.RequestAborted; - public IDictionary ContextData { get; } = new Dictionary(); + public bool IsConnected { get; set; } + + public IFeatureCollection Features { get; } = new FeatureCollection(); public async Task TryAcceptConnection() { diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/WebSocketSession.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/WebSocketSession.cs similarity index 80% rename from src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/WebSocketSession.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/WebSocketSession.cs index d1d4ad632a2..51f7de01967 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/WebSocketSession.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/WebSocketSession.cs @@ -1,8 +1,8 @@ using System.Buffers; -using Microsoft.AspNetCore.Http; using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.Transport.Sockets; -using static HotChocolate.AspNetCore.Properties.AspNetCoreResources; +using Microsoft.AspNetCore.Http; +using static HotChocolate.AspNetCore.Properties.AspNetCorePipelineResources; using static HotChocolate.AspNetCore.Subscriptions.ConnectionCloseReason; namespace HotChocolate.AspNetCore.Subscriptions; @@ -15,12 +15,11 @@ internal sealed class WebSocketSession : ISocketSession private WebSocketSession( ISocketConnection connection, IProtocolHandler protocol, - ISocketSessionInterceptor interceptor, - IRequestExecutor requestExecutor) + ExecutorSession executorSession) { Connection = connection; Protocol = protocol; - Operations = new OperationManager(this, interceptor, requestExecutor); + Operations = new OperationManager(this, executorSession); } public ISocketConnection Connection { get; } @@ -41,26 +40,26 @@ public void Dispose() public static async Task AcceptAsync( HttpContext context, - IRequestExecutor executor, - ISocketSessionInterceptor interceptor) + ExecutorSession executorSession) { - using var connection = new WebSocketConnection(context); + using var connection = new WebSocketConnection(context, executorSession); using var cts = CancellationTokenSource.CreateLinkedTokenSource( context.RequestAborted, connection.ApplicationStopping); var ct = cts.Token; var protocol = await connection.TryAcceptConnection(); + var interceptor = executorSession.SocketSessionInterceptor; if (protocol is not null) { - using var session = new WebSocketSession(connection, protocol, interceptor, executor); + using var session = new WebSocketSession(connection, protocol, executorSession); var options = context.GetGraphQLSocketOptions() ?? s_defaultOptions; try { var pingPong = new PingPongJob(session, options); var pipeline = new MessagePipeline(connection, new ProtocolMessageHandler(session)); - pipeline.Completed += (_, _) => cts.Cancel(); + pipeline.OnCompleted(static cts => cts.Cancel(), cts); await Task.WhenAll(pingPong.RunAsync(ct), pipeline.RunAsync(ct)); } catch (OperationCanceledException) @@ -73,7 +72,8 @@ public static async Task AcceptAsync( { try { - await interceptor.OnCloseAsync(session, connection.HttpContext.RequestAborted); + await executorSession.SocketSessionInterceptor.OnCloseAsync(session, + connection.HttpContext.RequestAborted); if (!connection.IsClosed) { @@ -86,7 +86,7 @@ await connection.CloseAsync( } catch { - // original exception must not be lost if new exception occurs + // the original exception must not be lost if new exception occurs // during closing session } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/ErrorHelper.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ErrorHelper.cs similarity index 96% rename from src/HotChocolate/AspNetCore/src/AspNetCore/ErrorHelper.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ErrorHelper.cs index 73029fe8055..2f250d25e4e 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/ErrorHelper.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ErrorHelper.cs @@ -1,7 +1,7 @@ using HotChocolate.Collections.Immutable; -using static HotChocolate.AspNetCore.Properties.AspNetCoreResources; +using static HotChocolate.AspNetCore.Properties.AspNetCorePipelineResources; -namespace HotChocolate.AspNetCore; +namespace HotChocolate.AspNetCore.Utilities; /// /// An internal helper class that centralizes server errors. diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HeaderUtilities.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/HeaderUtilities.cs similarity index 99% rename from src/HotChocolate/AspNetCore/src/AspNetCore/HeaderUtilities.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/HeaderUtilities.cs index ae26dc99917..b49666beb19 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/HeaderUtilities.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/HeaderUtilities.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -namespace HotChocolate.AspNetCore; +namespace HotChocolate.AspNetCore.Utilities; /// /// Utilities for handling HTTP headers. diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/MiddlewareHelper.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/MiddlewareHelper.cs similarity index 75% rename from src/HotChocolate/AspNetCore/src/AspNetCore/MiddlewareHelper.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/MiddlewareHelper.cs index 0cf2705968f..145ab13fcdd 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/MiddlewareHelper.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/MiddlewareHelper.cs @@ -1,43 +1,40 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net; -using HotChocolate.AspNetCore.Instrumentation; -using HotChocolate.AspNetCore.Serialization; using HotChocolate.Language; using Microsoft.AspNetCore.Http; -namespace HotChocolate.AspNetCore; +namespace HotChocolate.AspNetCore.Utilities; internal static class MiddlewareHelper { public static ValidateAcceptContentTypeResult ValidateAcceptContentType( HttpContext context, - IHttpResponseFormatter responseFormatter, - IServerDiagnosticEvents diagnosticEvents) + ExecutorSession executorSession) { - // first we will inspect the accept headers and determine if we can execute this request. + // first, we will inspect the accept-headers and determine if we can execute this request. var headerResult = HeaderUtilities.GetAcceptHeader(context.Request); - // if we cannot parse all media types that the user provided we will fail the request + // if we cannot parse all media types that the user provided, we will fail the request // with a 400 Bad Request. if (headerResult.HasError) { var errors = headerResult.ErrorResult.Errors!; - diagnosticEvents.HttpRequestError(context, errors[0]); + executorSession.DiagnosticEvents.HttpRequestError(context, errors[0]); return new ValidateAcceptContentTypeResult( headerResult.ErrorResult, HttpStatusCode.BadRequest); } - var requestFlags = responseFormatter.CreateRequestFlags(headerResult.AcceptMediaTypes); + var requestFlags = executorSession.ResponseFormatter.CreateRequestFlags(headerResult.AcceptMediaTypes); // if the request defines accept header values of which we cannot handle any provided - // media type then we will fail the request with 406 Not Acceptable. + // media type, then we will fail the request with 406 Not Acceptable. if (requestFlags is RequestFlags.None) { var error = ErrorHelper.NoSupportedAcceptMediaType(); - diagnosticEvents.HttpRequestError(context, error); + executorSession.DiagnosticEvents.HttpRequestError(context, error); return new ValidateAcceptContentTypeResult( error, @@ -52,31 +49,29 @@ public static ValidateAcceptContentTypeResult ValidateAcceptContentType( public static ParseRequestResult ParseRequestFromParams( HttpContext context, - IHttpRequestParser requestParser, - IErrorHandler errorHandler, - IServerDiagnosticEvents diagnosticEvents) + ExecutorSession executorSession) { - using (diagnosticEvents.ParseHttpRequest(context)) + using (executorSession.DiagnosticEvents.ParseHttpRequest(context)) { try { return new ParseRequestResult( - requestParser.ParseRequestFromParams( + executorSession.RequestParser.ParseRequestFromParams( context.Request.Query)); } catch (GraphQLRequestException ex) { // A GraphQL request exception is thrown if the HTTP request body couldn't be - // parsed. In this case we will return HTTP status code 400 and return a + // parsed. In this case, we will return HTTP status code 400 and return a // GraphQL error result. - var errors = errorHandler.Handle(ex.Errors); - diagnosticEvents.ParserErrors(context, errors); + var errors = executorSession.Handle(ex.Errors); + executorSession.DiagnosticEvents.ParserErrors(context, errors); return new ParseRequestResult(errors, HttpStatusCode.BadRequest); } catch (Exception ex) { var error = ErrorBuilder.FromException(ex).Build(); - diagnosticEvents.HttpRequestError(context, error); + executorSession.DiagnosticEvents.HttpRequestError(context, error); return new ParseRequestResult(error, HttpStatusCode.InternalServerError); } } @@ -86,16 +81,14 @@ public static ParseRequestResult ParseVariablesAndExtensionsFromParams( string operationId, string? operationName, HttpContext context, - IHttpRequestParser requestParser, - IErrorHandler errorHandler, - IServerDiagnosticEvents diagnosticEvents) + ExecutorSession executorSession) { - using (diagnosticEvents.ParseHttpRequest(context)) + using (executorSession.DiagnosticEvents.ParseHttpRequest(context)) { try { return new ParseRequestResult( - requestParser.ParsePersistedOperationRequestFromParams( + executorSession.RequestParser.ParsePersistedOperationRequestFromParams( operationId, operationName, context.Request.Query)); @@ -103,16 +96,16 @@ public static ParseRequestResult ParseVariablesAndExtensionsFromParams( catch (GraphQLRequestException ex) { // A GraphQL request exception is thrown if the HTTP request body couldn't be - // parsed. In this case we will return HTTP status code 400 and return a + // parsed. In this case, we will return HTTP status code 400 and return a // GraphQL error result. - var errors = errorHandler.Handle(ex.Errors); - diagnosticEvents.ParserErrors(context, errors); + var errors = executorSession.Handle(ex.Errors); + executorSession.DiagnosticEvents.ParserErrors(context, errors); return new ParseRequestResult(errors, HttpStatusCode.BadRequest); } catch (Exception ex) { var error = ErrorBuilder.FromException(ex).Build(); - diagnosticEvents.HttpRequestError(context, error); + executorSession.DiagnosticEvents.HttpRequestError(context, error); return new ParseRequestResult(error, HttpStatusCode.InternalServerError); } } @@ -122,17 +115,15 @@ public static async Task ParseSingleRequestFromBodyAsync( string operationId, string? operationName, HttpContext context, - IHttpRequestParser requestParser, - IErrorHandler errorHandler, - IServerDiagnosticEvents diagnosticEvents) + ExecutorSession executorSession) { GraphQLRequest request; - using (diagnosticEvents.ParseHttpRequest(context)) + using (executorSession.DiagnosticEvents.ParseHttpRequest(context)) { try { request = - await requestParser.ParsePersistedOperationRequestAsync( + await executorSession.RequestParser.ParsePersistedOperationRequestAsync( operationId, operationName, context.Request.Body, @@ -141,16 +132,16 @@ await requestParser.ParsePersistedOperationRequestAsync( catch (GraphQLRequestException ex) { // A GraphQL request exception is thrown if the HTTP request body couldn't be - // parsed. In this case we will return HTTP status code 400 and return a + // parsed. In this case, we will return HTTP status code 400 and return a // GraphQL error result. - var errors = errorHandler.Handle(ex.Errors); - diagnosticEvents.ParserErrors(context, errors); + var errors = executorSession.Handle(ex.Errors); + executorSession.DiagnosticEvents.ParserErrors(context, errors); return new ParseRequestResult(errors, HttpStatusCode.BadRequest); } catch (Exception ex) { var error = ErrorBuilder.FromException(ex).Build(); - diagnosticEvents.HttpRequestError(context, error); + executorSession.DiagnosticEvents.HttpRequestError(context, error); return new ParseRequestResult(error, HttpStatusCode.InternalServerError); } } @@ -204,27 +195,23 @@ public static async Task ExecuteRequestAsync( GraphQLRequest request, RequestFlags flags, HttpContext context, - IRequestExecutor requestExecutor, - IErrorHandler errorHandler, - IServerDiagnosticEvents diagnosticEvents) + ExecutorSession executorSession) { - // after successfully parsing the request we now will attempt to execute the request. - var requestInterceptor = requestExecutor.GetRequestInterceptor(); + // after successfully parsing the request, we now will attempt to execute the request. try { - diagnosticEvents.StartSingleRequest(context, request); + executorSession.DiagnosticEvents.StartSingleRequest(context, request); var requestBuilder = OperationRequestBuilder.From(request); requestBuilder.SetFlags(flags); - await requestInterceptor.OnCreateAsync( + await executorSession.OnCreateAsync( context, - requestExecutor, requestBuilder, context.RequestAborted); - var result = await requestExecutor.ExecuteAsync( + var result = await executorSession.ExecuteAsync( requestBuilder.Build(), context.RequestAborted); @@ -232,11 +219,11 @@ await requestInterceptor.OnCreateAsync( } catch (GraphQLException ex) { - // This allows extensions to throw GraphQL exceptions in the GraphQL interceptor. + // this allows extensions to throw GraphQL exceptions in the GraphQL interceptor. // we let the serializer determine the status code. foreach (var error in ex.Errors) { - diagnosticEvents.HttpRequestError(context, error); + executorSession.DiagnosticEvents.HttpRequestError(context, error); } return new ExecuteRequestResult( @@ -245,7 +232,7 @@ await requestInterceptor.OnCreateAsync( catch (Exception ex) { var error = ErrorBuilder.FromException(ex).Build(); - diagnosticEvents.HttpRequestError(context, error); + executorSession.DiagnosticEvents.HttpRequestError(context, error); return new ExecuteRequestResult( OperationResultBuilder.CreateError(error), HttpStatusCode.InternalServerError); @@ -257,33 +244,32 @@ public static async Task WriteResultAsync( AcceptMediaType[] acceptMediaTypes, HttpStatusCode? statusCode, HttpContext context, - IHttpResponseFormatter responseFormatter, - IServerDiagnosticEvents diagnosticEvents) + ExecutorSession executorSession) { - // query results use pooled memory an need to be disposed + // query results use pooled memory a need to be disposed // after we are finished with hem. await using var result = executionResult; IDisposable? formatScope = null; try { - // if cancellation is requested we will not try to attempt to write the result to the + // if cancellation is requested, we will not try to attempt to write the result to the // response stream. if (context.RequestAborted.IsCancellationRequested) { return; } - // in any case we will have a valid GraphQL result at this point that can be written + // in any case, we will have a valid GraphQL result at this point that can be written // to the HTTP response stream. Debug.Assert(result is not null, "No GraphQL result was created."); if (result is IOperationResult queryResult) { - formatScope = diagnosticEvents.FormatHttpResponse(context, queryResult); + formatScope = executorSession.DiagnosticEvents.FormatHttpResponse(context, queryResult); } - await responseFormatter.FormatAsync( + await executorSession.ResponseFormatter.FormatAsync( context.Response, result, acceptMediaTypes, diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/ThrowHelper.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ThrowHelper.cs similarity index 97% rename from src/HotChocolate/AspNetCore/src/AspNetCore/ThrowHelper.cs rename to src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ThrowHelper.cs index 1df66fd38b2..8e12a5a1183 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/ThrowHelper.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ThrowHelper.cs @@ -1,7 +1,7 @@ using HotChocolate.Language; -using static HotChocolate.AspNetCore.Properties.AspNetCoreResources; +using static HotChocolate.AspNetCore.Properties.AspNetCorePipelineResources; -namespace HotChocolate.AspNetCore; +namespace HotChocolate.AspNetCore.Utilities; /// /// An internal helper class that centralizes the server exceptions. diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/WebSocketSubscriptionMiddleware.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/WebSocketSubscriptionMiddleware.cs new file mode 100644 index 00000000000..7d4cfe100bd --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/WebSocketSubscriptionMiddleware.cs @@ -0,0 +1,39 @@ +using HotChocolate.AspNetCore.Subscriptions; +using Microsoft.AspNetCore.Http; +using HttpRequestDelegate = Microsoft.AspNetCore.Http.RequestDelegate; + +namespace HotChocolate.AspNetCore; + +public sealed class WebSocketSubscriptionMiddleware : MiddlewareBase +{ + public WebSocketSubscriptionMiddleware( + HttpRequestDelegate next, + HttpRequestExecutorProxy executor) + : base(next, executor) + { + } + + public Task InvokeAsync(HttpContext context) + { + return context.WebSockets.IsWebSocketRequest + ? HandleWebSocketSessionAsync(context) + : NextAsync(context); + } + + private async Task HandleWebSocketSessionAsync(HttpContext context) + { + var session = await Executor.GetOrCreateSessionAsync(context.RequestAborted); + + using (session.DiagnosticEvents.WebSocketSession(context)) + { + try + { + await WebSocketSession.AcceptAsync(context, session); + } + catch (Exception ex) + { + session.DiagnosticEvents.WebSocketSessionError(context, ex); + } + } + } +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/DelegateHttpRequestInterceptor.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/DelegateHttpRequestInterceptor.cs deleted file mode 100644 index ba265326a39..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/DelegateHttpRequestInterceptor.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace HotChocolate.AspNetCore; - -internal sealed class DelegateHttpRequestInterceptor : DefaultHttpRequestInterceptor -{ - private readonly HttpRequestInterceptorDelegate _interceptor; - - public DelegateHttpRequestInterceptor(HttpRequestInterceptorDelegate interceptor) - { - _interceptor = interceptor ?? throw new ArgumentNullException(nameof(interceptor)); - } - - public override async ValueTask OnCreateAsync( - HttpContext context, - IRequestExecutor requestExecutor, - OperationRequestBuilder requestBuilder, - CancellationToken cancellationToken) - { - await _interceptor(context, requestExecutor, requestBuilder, cancellationToken); - await base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken); - } -} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Http.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Http.cs index 5c7ffaa5c1d..914cfe828eb 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Http.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Http.cs @@ -1,9 +1,10 @@ using System.Text.Json; using Microsoft.Extensions.DependencyInjection.Extensions; using HotChocolate.AspNetCore; -using HotChocolate.AspNetCore.Serialization; +using HotChocolate.AspNetCore.Formatters; using HotChocolate.Execution.Configuration; using HotChocolate.Transport.Formatters; +using Microsoft.AspNetCore.Http; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; @@ -11,7 +12,7 @@ namespace Microsoft.Extensions.DependencyInjection; public static partial class HotChocolateAspNetCoreServiceCollectionExtensions { /// - /// Adds an interceptor for GraphQL requests to the GraphQL configuration. + /// Adds an interceptor for GraphQL over HTTP requests to the GraphQL configuration. /// /// /// The . @@ -24,14 +25,18 @@ public static partial class HotChocolateAspNetCoreServiceCollectionExtensions /// public static IRequestExecutorBuilder AddHttpRequestInterceptor( this IRequestExecutorBuilder builder) - where T : class, IHttpRequestInterceptor => - builder.ConfigureSchemaServices(s => s + where T : class, IHttpRequestInterceptor + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.ConfigureSchemaServices(s => s .RemoveAll() - .AddSingleton( - sp => ActivatorUtilities.GetServiceOrCreateInstance(sp.GetCombinedServices()))); + .AddSingleton(sp => + ActivatorUtilities.GetServiceOrCreateInstance(sp.GetCombinedServices()))); + } /// - /// Adds an interceptor for GraphQL requests to the GraphQL configuration. + /// Adds an interceptor for GraphQL over HTTP requests to the GraphQL configuration. /// /// /// The . @@ -48,43 +53,46 @@ public static IRequestExecutorBuilder AddHttpRequestInterceptor( public static IRequestExecutorBuilder AddHttpRequestInterceptor( this IRequestExecutorBuilder builder, Func factory) - where T : class, IHttpRequestInterceptor => - builder.ConfigureSchemaServices(s => s + where T : class, IHttpRequestInterceptor + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(factory); + + return builder.ConfigureSchemaServices(s => s .RemoveAll() .AddSingleton(sp => factory(sp.GetCombinedServices()))); + } /// - /// Adds an interceptor for GraphQL requests to the GraphQL configuration. + /// Adds an interceptor for GraphQL over HTTP requests to the GraphQL configuration. /// /// /// The . /// - /// - /// The interceptor instance that shall be added to the configuration. + /// + /// A delegate that allows to configure the GraphQL request. /// - /// - /// Returns the so that configuration can be chained. - /// + /// public static IRequestExecutorBuilder AddHttpRequestInterceptor( this IRequestExecutorBuilder builder, - HttpRequestInterceptorDelegate interceptor) => - AddHttpRequestInterceptor( - builder, - _ => new DelegateHttpRequestInterceptor(interceptor)); + Func handler) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.AddHttpRequestInterceptor(_ => new DelegateHttpRequestInterceptor(handler)); + } private static IRequestExecutorBuilder AddDefaultHttpRequestInterceptor( this IRequestExecutorBuilder builder) - { - return builder.ConfigureSchemaServices( + => builder.ConfigureSchemaServices( s => s.TryAddSingleton()); - } /// /// Adds the with specific formatter options /// to the DI. /// - /// - /// The . + /// + /// The . /// /// /// Defines whether the underlying @@ -94,56 +102,62 @@ private static IRequestExecutorBuilder AddDefaultHttpRequestInterceptor( /// By default, the JSON is written without extra white spaces. /// /// - /// Returns the so that configuration can be chained. + /// Returns the so that configuration can be chained. /// - public static IServiceCollection AddHttpResponseFormatter( - this IServiceCollection services, + public static IRequestExecutorBuilder AddHttpResponseFormatter( + this IRequestExecutorBuilder builder, bool indented = false) { - services.RemoveAll(); - services.AddSingleton( - sp => DefaultHttpResponseFormatter.Create( - new HttpResponseFormatterOptions - { - Json = new JsonResultFormatterOptions - { - Indented = indented - } - }, - sp.GetRequiredService())); - return services; + ArgumentNullException.ThrowIfNull(builder); + + builder.ConfigureSchemaServices( + s => s.RemoveAll() + .AddSingleton( + sp => DefaultHttpResponseFormatter.Create( + new HttpResponseFormatterOptions + { + Json = new JsonResultFormatterOptions + { + Indented = indented + } + }, + sp.GetRequiredService()))); + return builder; } /// /// Adds the with specific formatter options /// to the DI. /// - /// - /// The . + /// + /// The . /// /// /// The HTTP response formatter options /// /// - /// Returns the so that configuration can be chained. + /// Returns the so that configuration can be chained. /// - public static IServiceCollection AddHttpResponseFormatter( - this IServiceCollection services, + public static IRequestExecutorBuilder AddHttpResponseFormatter( + this IRequestExecutorBuilder builder, HttpResponseFormatterOptions options) { - services.RemoveAll(); - services.AddSingleton( - sp => DefaultHttpResponseFormatter.Create( - options, - sp.GetRequiredService())); - return services; + ArgumentNullException.ThrowIfNull(builder); + + builder.ConfigureSchemaServices( + s => s.RemoveAll() + .AddSingleton( + sp => DefaultHttpResponseFormatter.Create( + options, + sp.GetRequiredService()))); + return builder; } /// /// Adds a custom HTTP response formatter to the DI. /// - /// - /// The . + /// + /// The . /// /// /// The type of the custom . @@ -151,20 +165,23 @@ public static IServiceCollection AddHttpResponseFormatter( /// /// Returns the so that configuration can be chained. /// - public static IServiceCollection AddHttpResponseFormatter( - this IServiceCollection services) + public static IRequestExecutorBuilder AddHttpResponseFormatter( + this IRequestExecutorBuilder builder) where T : class, IHttpResponseFormatter { - services.RemoveAll(); - services.AddSingleton(); - return services; + ArgumentNullException.ThrowIfNull(builder); + + builder.ConfigureSchemaServices( + s => s.RemoveAll() + .AddSingleton()); + return builder; } /// /// Adds a custom HTTP response formatter to the DI. /// - /// - /// The . + /// + /// The . /// /// /// The service factory. @@ -175,13 +192,17 @@ public static IServiceCollection AddHttpResponseFormatter( /// /// Returns the so that configuration can be chained. /// - public static IServiceCollection AddHttpResponseFormatter( - this IServiceCollection services, + public static IRequestExecutorBuilder AddHttpResponseFormatter( + this IRequestExecutorBuilder builder, Func factory) where T : class, IHttpResponseFormatter { - services.RemoveAll(); - services.AddSingleton(factory); - return services; + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(factory); + + builder.ConfigureSchemaServices( + s => s.RemoveAll() + .AddSingleton(factory)); + return builder; } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Subscriptions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Subscriptions.cs index e2d022b8238..cbacdb1360e 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Subscriptions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Subscriptions.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using HotChocolate.AspNetCore; -using HotChocolate.AspNetCore.Serialization; +using HotChocolate.AspNetCore.Formatters; using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.AspNetCore.Subscriptions.Protocols.Apollo; using HotChocolate.AspNetCore.Subscriptions.Protocols.GraphQLOverWebSocket; @@ -60,9 +60,7 @@ private static IRequestExecutorBuilder AddSubscriptionServices( .ConfigureSchemaServices(s => { s.TryAddSingleton(); - s.TryAddSingleton( - _ => new DefaultWebSocketPayloadFormatter( - new WebSocketPayloadFormatterOptions())); + s.TryAddSingleton(_ => new DefaultWebSocketPayloadFormatter()); }) .AddApolloProtocol() .AddGraphQLOverWebSocketProtocol(); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs index 28cd5010c47..a72fe40c7b4 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs @@ -1,8 +1,9 @@ using HotChocolate.AspNetCore; +using HotChocolate.AspNetCore.Formatters; using Microsoft.Extensions.DependencyInjection.Extensions; using HotChocolate.AspNetCore.Instrumentation; using HotChocolate.AspNetCore.ParameterExpressionBuilders; -using HotChocolate.AspNetCore.Serialization; +using HotChocolate.AspNetCore.Parsers; using HotChocolate.Execution.Configuration; using HotChocolate.Internal; using HotChocolate.Language; @@ -18,65 +19,51 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static partial class HotChocolateAspNetCoreServiceCollectionExtensions { - /// - /// Adds the GraphQL server core services to the DI. - /// - /// - /// The . - /// - /// - /// The max allowed GraphQL request size. - /// - /// - /// Returns the so that configuration can be chained. - /// - /// - /// The is null. - /// - public static IServiceCollection AddGraphQLServerCore( - this IServiceCollection services, + private static IRequestExecutorBuilder AddGraphQLServerCore( + this IRequestExecutorBuilder builder, int maxAllowedRequestSize = MaxAllowedRequestSize) { - ArgumentNullException.ThrowIfNull(services); - - services.AddGraphQLCore(); - services.TryAddSingleton( - sp => DefaultHttpResponseFormatter.Create( - new HttpResponseFormatterOptions { HttpTransportVersion = HttpTransportVersion.Latest }, - sp.GetRequiredService())); - services.TryAddSingleton( - sp => new DefaultHttpRequestParser( - sp.GetRequiredService(), - sp.GetRequiredService(), - maxAllowedRequestSize, - sp.GetRequiredService())); - services.TryAddSingleton(sp => + builder.ConfigureSchemaServices(s => { - var listeners = sp.GetServices().ToArray(); - return listeners.Length switch + s.TryAddSingleton( + sp => DefaultHttpResponseFormatter.Create( + new HttpResponseFormatterOptions { HttpTransportVersion = HttpTransportVersion.Latest }, + sp.GetRequiredService())); + s.TryAddSingleton( + sp => new DefaultHttpRequestParser( + sp.GetRequiredService(), + sp.GetRequiredService(), + maxAllowedRequestSize, + sp.GetRequiredService())); + + s.TryAddSingleton(sp => { - 0 => new NoopServerDiagnosticEventListener(), - 1 => listeners[0], - _ => new AggregateServerDiagnosticEventListener(listeners) - }; + var listeners = sp.GetServices().ToArray(); + return listeners.Length switch + { + 0 => new NoopServerDiagnosticEventListener(), + 1 => listeners[0], + _ => new AggregateServerDiagnosticEventListener(listeners) + }; + }); }); - if (!services.IsImplementationTypeRegistered()) + if (!builder.Services.IsImplementationTypeRegistered()) { - services.AddSingleton(); + builder.Services.AddSingleton(); } - if (!services.IsImplementationTypeRegistered()) + if (!builder.Services.IsImplementationTypeRegistered()) { - services.AddSingleton(); + builder.Services.AddSingleton(); } - if (!services.IsImplementationTypeRegistered()) + if (!builder.Services.IsImplementationTypeRegistered()) { - services.AddSingleton(); + builder.Services.AddSingleton(); } - return services; + return builder; } /// @@ -106,8 +93,8 @@ public static IRequestExecutorBuilder AddGraphQLServer( ArgumentNullException.ThrowIfNull(services); var builder = services - .AddGraphQLServerCore(maxAllowedRequestSize) .AddGraphQL(schemaName) + .AddGraphQLServerCore(maxAllowedRequestSize) .AddDefaultHttpRequestInterceptor() .AddSubscriptionServices(); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpResponseExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpResponseExtensions.cs deleted file mode 100644 index d487618bb1e..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpResponseExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.AspNetCore.Http; -using static System.Globalization.CultureInfo; -using static System.String; - -namespace HotChocolate.AspNetCore; - -internal static class HttpResponseExtensions -{ - private const string ContentDispositionHeader = "Content-Disposition"; - private const string ContentDispositionValue = "attachment; filename=\"{0}\""; - private static readonly JsonSerializerOptions s_serializerOptions = new() - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - - internal static Task WriteAsJsonAsync( - this HttpResponse response, - TValue value, - CancellationToken cancellationToken = default) - { - response.ContentType = ContentType.Json; - response.StatusCode = 200; - - return JsonSerializer.SerializeAsync( - response.Body, - value, - s_serializerOptions, - cancellationToken); - } - - public static IHeaderDictionary SetContentDisposition( - this IHeaderDictionary headers, - string fileName) - { - headers[ContentDispositionHeader] = - Format(InvariantCulture, ContentDispositionValue, fileName); - return headers; - } -} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/RequestExecutorExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/RequestExecutorExtensions.cs deleted file mode 100644 index fa942d253ea..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/RequestExecutorExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace HotChocolate.AspNetCore; - -internal static class RequestExecutorExtensions -{ - internal static T GetRequiredService( - this IRequestExecutor requestExecutor) - where T : notnull => - requestExecutor.Schema.Services.GetRequiredService(); - - internal static IErrorHandler GetErrorHandler( - this IRequestExecutor requestExecutor) => - requestExecutor.GetRequiredService(); - - internal static IHttpRequestInterceptor GetRequestInterceptor( - this IRequestExecutor requestExecutor) => - requestExecutor.GetRequiredService(); -} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HotChocolate.AspNetCore.csproj b/src/HotChocolate/AspNetCore/src/AspNetCore/HotChocolate.AspNetCore.csproj index 0520a5c65ab..af5213135b3 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/HotChocolate.AspNetCore.csproj +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/HotChocolate.AspNetCore.csproj @@ -19,6 +19,7 @@ + diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpMultipartRequest.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/HttpMultipartRequest.cs deleted file mode 100644 index 6452e05be77..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpMultipartRequest.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace HotChocolate.AspNetCore; - -internal sealed class HttpMultipartRequest -{ - public HttpMultipartRequest(string operations, IDictionary fileMap) - { - Operations = operations; - FileMap = fileMap; - } - - public string Operations { get; set; } - - public IDictionary FileMap { get; set; } -} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpPostMiddleware.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/HttpPostMiddleware.cs deleted file mode 100644 index 49ec28f7a51..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpPostMiddleware.cs +++ /dev/null @@ -1,22 +0,0 @@ -using HotChocolate.AspNetCore.Instrumentation; -using HotChocolate.AspNetCore.Serialization; -using HttpRequestDelegate = Microsoft.AspNetCore.Http.RequestDelegate; - -namespace HotChocolate.AspNetCore; - -public sealed class HttpPostMiddleware( - HttpRequestDelegate next, - IRequestExecutorProvider executorProvider, - IRequestExecutorEvents executorEvents, - IHttpResponseFormatter responseFormatter, - IHttpRequestParser requestParser, - IServerDiagnosticEvents diagnosticEvents, - string schemaName) - : HttpPostMiddlewareBase( - next, - executorProvider, - executorEvents, - responseFormatter, - requestParser, - diagnosticEvents, - schemaName); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpRequestInterceptorDelegate.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/HttpRequestInterceptorDelegate.cs deleted file mode 100644 index 67300851fb6..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpRequestInterceptorDelegate.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace HotChocolate.AspNetCore; - -public delegate ValueTask HttpRequestInterceptorDelegate( - HttpContext context, - IRequestExecutor requestExecutor, - OperationRequestBuilder requestBuilder, - CancellationToken cancellationToken); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/MiddlewareBase.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/MiddlewareBase.cs deleted file mode 100644 index dd22b24c2c9..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/MiddlewareBase.cs +++ /dev/null @@ -1,248 +0,0 @@ -using System.Net; -using System.Runtime.CompilerServices; -using Microsoft.AspNetCore.Http; -using HotChocolate.AspNetCore.Instrumentation; -using HotChocolate.AspNetCore.Serialization; -using HotChocolate.Language; -using HotChocolate.Utilities; -using RequestDelegate = Microsoft.AspNetCore.Http.RequestDelegate; - -namespace HotChocolate.AspNetCore; - -/// -/// The Hot Chocolate ASP.NET core middleware base class. -/// -public class MiddlewareBase : IDisposable -{ - private readonly RequestDelegate _next; - private readonly IHttpResponseFormatter _responseFormatter; - private readonly RequestExecutorProxy _executorProxy; - private GraphQLServerOptions? _options; - private bool _disposed; - - protected MiddlewareBase( - RequestDelegate next, - IRequestExecutorProvider executorProvider, - IRequestExecutorEvents executorEvents, - IHttpResponseFormatter responseFormatter, - string schemaName) - { - ArgumentNullException.ThrowIfNull(executorProvider); - - _next = next ?? - throw new ArgumentNullException(nameof(next)); - _responseFormatter = responseFormatter ?? - throw new ArgumentNullException(nameof(responseFormatter)); - SchemaName = schemaName; - IsDefaultSchema = SchemaName.EqualsOrdinal(ISchemaDefinition.DefaultName); - _executorProxy = new RequestExecutorProxy(executorProvider, executorEvents, schemaName); - } - - /// - /// Gets the name of the schema that this middleware serves up. - /// - protected string SchemaName { get; } - - /// - /// Specifies if this middleware handles the default schema. - /// - protected bool IsDefaultSchema { get; } - - /// - /// Gets the request executor proxy. - /// - protected RequestExecutorProxy ExecutorProxy => _executorProxy; - - /// - /// Gets the response formatter. - /// - protected IHttpResponseFormatter ResponseFormatter => _responseFormatter; - - /// - /// Invokes the next middleware in line. - /// - /// - /// The . - /// - protected Task NextAsync(HttpContext context) => _next(context); - - /// - /// Gets the request executor for this middleware. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the request executor for this middleware. - /// - protected ValueTask GetExecutorAsync(CancellationToken cancellationToken) - => _executorProxy.GetExecutorAsync(cancellationToken); - - /// - /// Gets the schema for this middleware. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the schema for this middleware. - /// - protected async ValueTask GetSchemaAsync(CancellationToken cancellationToken) - => await _executorProxy.GetSchemaAsync(cancellationToken); - - protected ValueTask WriteResultAsync( - HttpContext context, - IExecutionResult result, - AcceptMediaType[] acceptMediaTypes, - HttpStatusCode? statusCode = null) - => _responseFormatter.FormatAsync( - context.Response, - result, - acceptMediaTypes, - statusCode, - context.RequestAborted); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected RequestFlags CreateRequestFlags(AcceptMediaType[] acceptMediaTypes) - => _responseFormatter.CreateRequestFlags(acceptMediaTypes); - - protected static async Task ExecuteSingleAsync( - HttpContext context, - IRequestExecutor requestExecutor, - IHttpRequestInterceptor requestInterceptor, - IServerDiagnosticEvents diagnosticEvents, - GraphQLRequest request, - RequestFlags flags) - { - diagnosticEvents.StartSingleRequest(context, request); - - var requestBuilder = OperationRequestBuilder.From(request); - requestBuilder.SetFlags(flags); - - await requestInterceptor.OnCreateAsync( - context, - requestExecutor, - requestBuilder, - context.RequestAborted); - - return await requestExecutor.ExecuteAsync( - requestBuilder.Build(), - context.RequestAborted); - } - - protected static async Task ExecuteOperationBatchAsync( - HttpContext context, - IRequestExecutor requestExecutor, - IHttpRequestInterceptor requestInterceptor, - IServerDiagnosticEvents diagnosticEvents, - GraphQLRequest request, - RequestFlags flags, - IReadOnlyList operationNames) - { - diagnosticEvents.StartOperationBatchRequest(context, request, operationNames); - - var requestBatch = new IOperationRequest[operationNames.Count]; - - for (var i = 0; i < operationNames.Count; i++) - { - var requestBuilder = OperationRequestBuilder.From(request); - requestBuilder.SetOperationName(operationNames[i]); - requestBuilder.SetFlags(flags); - - await requestInterceptor.OnCreateAsync( - context, - requestExecutor, - requestBuilder, - context.RequestAborted); - - requestBatch[i] = requestBuilder.Build(); - } - - return await requestExecutor.ExecuteBatchAsync( - new OperationRequestBatch(requestBatch, services: context.RequestServices), - cancellationToken: context.RequestAborted); - } - - protected static async Task ExecuteBatchAsync( - HttpContext context, - IRequestExecutor requestExecutor, - IHttpRequestInterceptor requestInterceptor, - IServerDiagnosticEvents diagnosticEvents, - IReadOnlyList requests, - RequestFlags flags) - { - diagnosticEvents.StartBatchRequest(context, requests); - - var requestBatch = new IOperationRequest[requests.Count]; - - for (var i = 0; i < requests.Count; i++) - { - var requestBuilder = OperationRequestBuilder.From(requests[i]); - requestBuilder.SetFlags(flags); - - await requestInterceptor.OnCreateAsync( - context, - requestExecutor, - requestBuilder, - context.RequestAborted); - - requestBatch[i] = requestBuilder.Build(); - } - - return await requestExecutor.ExecuteBatchAsync( - new OperationRequestBatch(requestBatch, services: context.RequestServices), - cancellationToken: context.RequestAborted); - } - - protected static RequestContentType ParseContentType(HttpContext context) - { - if (context.Items.TryGetValue(nameof(RequestContentType), out var value) && - value is RequestContentType contentType) - { - return contentType; - } - - var span = context.Request.ContentType.AsSpan(); - - if (span.StartsWith(ContentType.JsonSpan())) - { - context.Items[nameof(RequestContentType)] = RequestContentType.Json; - return RequestContentType.Json; - } - - if (span.StartsWith(ContentType.MultiPartFormSpan())) - { - context.Items[nameof(RequestContentType)] = RequestContentType.Form; - return RequestContentType.Form; - } - - context.Items[nameof(RequestContentType)] = RequestContentType.None; - return RequestContentType.None; - } - - protected GraphQLServerOptions GetOptions(HttpContext context) - { - if (_options is not null) - { - return _options; - } - - _options = context.GetGraphQLServerOptions() ?? new GraphQLServerOptions(); - return _options; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposed && disposing) - { - ExecutorProxy.Dispose(); - _disposed = true; - } - } -} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/PersistedOperationMiddleware.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/PersistedOperationMiddleware.cs deleted file mode 100644 index 180709151c0..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/PersistedOperationMiddleware.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System.Net; -using HotChocolate.AspNetCore.Instrumentation; -using HotChocolate.AspNetCore.Serialization; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; - -namespace HotChocolate.AspNetCore; - -internal static class PersistedOperationMiddleware -{ - internal static void MapPersistedOperationMiddleware( - this RouteGroupBuilder groupBuilder, - string schemaName, - bool requireOperationName) - { - var state = new State(schemaName); - - groupBuilder - .MapGet( - "/{OperationId}", - ([AsParameters] Services services, string operationId) - => ExecuteGetRequestAsync(state, services, operationId, null, requireOperationName)); - - groupBuilder - .MapGet( - "/{OperationId}/{OperationName}", - ([AsParameters] Services services, string operationId, string operationName) - => ExecuteGetRequestAsync(state, services, operationId, operationName, requireOperationName)); - - groupBuilder - .MapPost( - "/{OperationId}", - ([AsParameters] Services services, string operationId) - => ExecutePostRequestAsync(state, services, operationId, null, requireOperationName)); - - groupBuilder - .MapPost( - "/{OperationId}/{OperationName}", - ([AsParameters] Services services, string operationId, string operationName) - => ExecutePostRequestAsync(state, services, operationId, operationName, requireOperationName)); - } - - private static async Task ExecuteGetRequestAsync( - State state, - Services services, - string operationId, - string? operationName, - bool requireOperationName) - { - HttpStatusCode? statusCode; - IExecutionResult? result; - - // first we validate the accept headers. - var validationResult = - MiddlewareHelper.ValidateAcceptContentType( - services.Context, - services.ResponseFormatter, - services.DiagnosticEvents); - - var acceptMediaTypes = validationResult.AcceptMediaTypes; - - if (!validationResult.IsValid) - { - statusCode = validationResult.StatusCode.Value; - result = validationResult.Error; - goto HANDLE_RESULT; - } - - // validate if the operation name is required. - if (requireOperationName && string.IsNullOrWhiteSpace(operationName)) - { - statusCode = HttpStatusCode.BadRequest; - result = ErrorHelper.OperationNameRequired(); - goto HANDLE_RESULT; - } - - // next we parse the GraphQL request. - var executor = state.CurrentExecutor - ?? await state.GetExecutorAsync(services); - var errorHandler = executor.GetErrorHandler(); - - var parserResult = - MiddlewareHelper.ParseVariablesAndExtensionsFromParams( - operationId, - operationName, - services.Context, - services.RequestParser, - errorHandler, - services.DiagnosticEvents); - - if (!parserResult.IsValid) - { - statusCode = parserResult.StatusCode.Value; - result = parserResult.Error; - goto HANDLE_RESULT; - } - - // before we can execute the request we need to determine the request flags. - var request = parserResult.Request!; - var options = state.GetOptions(services.Context); - var requestFlags = - MiddlewareHelper.DetermineHttpGetRequestFlags( - validationResult.RequestFlags, - options); - - // next we will execute the request. - var executionResult = - await MiddlewareHelper.ExecuteRequestAsync( - request, - requestFlags, - services.Context, - executor, - errorHandler, - services.DiagnosticEvents); - statusCode = executionResult.StatusCode; - result = executionResult.Result; - -HANDLE_RESULT: - await MiddlewareHelper.WriteResultAsync( - result!, - acceptMediaTypes, - statusCode, - services.Context, - services.ResponseFormatter, - services.DiagnosticEvents); - } - - private static async Task ExecutePostRequestAsync( - State state, - Services services, - string operationId, - string? operationName, - bool requireOperationName) - { - HttpStatusCode? statusCode; - IExecutionResult? result; - - // first we validate the accept headers. - var validationResult = - MiddlewareHelper.ValidateAcceptContentType( - services.Context, - services.ResponseFormatter, - services.DiagnosticEvents); - - var acceptMediaTypes = validationResult.AcceptMediaTypes; - - if (!validationResult.IsValid) - { - statusCode = validationResult.StatusCode.Value; - result = validationResult.Error; - goto HANDLE_RESULT; - } - - // validate if the operation name is required. - if (requireOperationName && string.IsNullOrWhiteSpace(operationName)) - { - statusCode = HttpStatusCode.BadRequest; - result = ErrorHelper.OperationNameRequired(); - goto HANDLE_RESULT; - } - - // next we parse the GraphQL request. - var executor = state.CurrentExecutor ?? await state.GetExecutorAsync(services); - var errorHandler = executor.GetErrorHandler(); - - var parserResult = - await MiddlewareHelper.ParseSingleRequestFromBodyAsync( - operationId, - operationName, - services.Context, - services.RequestParser, - errorHandler, - services.DiagnosticEvents); - - if (!parserResult.IsValid) - { - statusCode = parserResult.StatusCode.Value; - result = parserResult.Error; - goto HANDLE_RESULT; - } - - // after successfully parsing the request we now will attempt to execute the request. - var executionResult = - await MiddlewareHelper.ExecuteRequestAsync( - parserResult.Request!, - validationResult.RequestFlags, - services.Context, - executor, - errorHandler, - services.DiagnosticEvents); - statusCode = executionResult.StatusCode; - result = executionResult.Result; - -HANDLE_RESULT: - await MiddlewareHelper.WriteResultAsync( - result!, - acceptMediaTypes, - statusCode, - services.Context, - services.ResponseFormatter, - services.DiagnosticEvents); - } - - private sealed class Services( - HttpContext context, - IRequestExecutorProvider executorProvider, - IRequestExecutorEvents executorEvents, - IHttpResponseFormatter responseFormatter, - IHttpRequestParser requestParser, - IServerDiagnosticEvents diagnosticEvents) - { - public HttpContext Context { get; } = context; - - public IRequestExecutorProvider ExecutorProvider { get; } = executorProvider; - - public IRequestExecutorEvents ExecutorEvents { get; } = executorEvents; - - public IHttpResponseFormatter ResponseFormatter { get; } = responseFormatter; - - public IHttpRequestParser RequestParser { get; } = requestParser; - - public IServerDiagnosticEvents DiagnosticEvents { get; } = diagnosticEvents; - - public CancellationToken RequestAborted => Context.RequestAborted; - } - - private sealed class State(string schemaName) - { - private RequestExecutorProxy? _proxy; - private GraphQLServerOptions? _options; - - public IRequestExecutor? CurrentExecutor => _proxy?.CurrentExecutor; - - public ValueTask GetExecutorAsync(Services services) - { - _proxy ??= new RequestExecutorProxy(services.ExecutorProvider, services.ExecutorEvents, schemaName); - return _proxy.GetExecutorAsync(services.RequestAborted); - } - - public GraphQLServerOptions GetOptions(HttpContext context) - { - if (_options is not null) - { - return _options; - } - - _options = context.GetGraphQLServerOptions() ?? new GraphQLServerOptions(); - return _options; - } - } -} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Resources/.keep b/src/HotChocolate/AspNetCore/src/AspNetCore/Resources/.keep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Resources/index.html b/src/HotChocolate/AspNetCore/src/AspNetCore/Resources/index.html deleted file mode 100644 index 18ecdcb795c..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Resources/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/ConnectionContextKeys.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/ConnectionContextKeys.cs deleted file mode 100644 index d30c99f477e..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/ConnectionContextKeys.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace HotChocolate.AspNetCore.Subscriptions; - -internal static class ConnectionContextKeys -{ - public const string Connected = "HotChocolate.WebSockets.Connected"; -} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/Utf8Messages.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/Utf8Messages.cs deleted file mode 100644 index a820a01a444..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/Apollo/Utf8Messages.cs +++ /dev/null @@ -1,132 +0,0 @@ -namespace HotChocolate.AspNetCore.Subscriptions.Protocols.Apollo; - -internal static class Utf8Messages -{ - // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static - public static ReadOnlySpan ConnectionInitialize => - [ - (byte)'c', - (byte)'o', - (byte)'n', - (byte)'n', - (byte)'e', - (byte)'c', - (byte)'t', - (byte)'i', - (byte)'o', - (byte)'n', - (byte)'_', - (byte)'i', - (byte)'n', - (byte)'i', - (byte)'t' - ]; - - public static ReadOnlySpan ConnectionAccept => - [ - (byte)'c', - (byte)'o', - (byte)'n', - (byte)'n', - (byte)'e', - (byte)'c', - (byte)'t', - (byte)'i', - (byte)'o', - (byte)'n', - (byte)'_', - (byte)'a', - (byte)'c', - (byte)'k' - ]; - - public static ReadOnlySpan ConnectionError => - [ - (byte)'c', - (byte)'o', - (byte)'n', - (byte)'n', - (byte)'e', - (byte)'c', - (byte)'t', - (byte)'i', - (byte)'o', - (byte)'n', - (byte)'_', - (byte)'e', - (byte)'r', - (byte)'r', - (byte)'o', - (byte)'r' - ]; - - public static ReadOnlySpan ConnectionTerminate => - [ - (byte)'c', - (byte)'o', - (byte)'n', - (byte)'n', - (byte)'e', - (byte)'c', - (byte)'t', - (byte)'i', - (byte)'o', - (byte)'n', - (byte)'_', - (byte)'t', - (byte)'e', - (byte)'r', - (byte)'m', - (byte)'i', - (byte)'n', - (byte)'a', - (byte)'t', - (byte)'e' - ]; - - public static ReadOnlySpan Start => - [ - (byte)'s', - (byte)'t', - (byte)'a', - (byte)'r', - (byte)'t' - ]; - - public static ReadOnlySpan Stop => - [ - (byte)'s', - (byte)'t', - (byte)'o', - (byte)'p' - ]; - - public static ReadOnlySpan Data => - [ - (byte)'d', - (byte)'a', - (byte)'t', - (byte)'a' - ]; - - public static ReadOnlySpan Error => - [ - (byte)'e', - (byte)'r', - (byte)'r', - (byte)'o', - (byte)'r' - ]; - - public static ReadOnlySpan Complete => - [ - (byte)'c', - (byte)'o', - (byte)'m', - (byte)'p', - (byte)'l', - (byte)'e', - (byte)'t', - (byte)'e' - ]; -} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/IOperationMessagePayload.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/IOperationMessagePayload.cs deleted file mode 100644 index 74e046c9531..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Subscriptions/Protocols/IOperationMessagePayload.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace HotChocolate.AspNetCore.Subscriptions.Protocols; - -/// -/// The operation message with a custom payload. -/// -public interface IOperationMessagePayload -{ - /// - /// Deserializes the payload as the specified type. - /// - /// - /// The type as which the payload shall be deserialized. - /// - /// - /// Returns the deserialized payload. - /// - T? As() where T : class; -} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/UploadedFile.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/UploadedFile.cs deleted file mode 100644 index 7b1c021121f..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/UploadedFile.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace HotChocolate.AspNetCore; - -internal sealed class UploadedFile : StreamFile -{ - private readonly IFormFile _file; - - public UploadedFile(IFormFile file) - : base(file.FileName, file.OpenReadStream, file.Length, file.ContentType) - { - _file = file ?? throw new ArgumentNullException(nameof(file)); - } - - public override Task CopyToAsync( - Stream target, - CancellationToken cancellationToken = default) => - _file.CopyToAsync(target, cancellationToken); -} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/WebSocketSubscriptionMiddleware.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/WebSocketSubscriptionMiddleware.cs deleted file mode 100644 index 90076f1a88d..00000000000 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/WebSocketSubscriptionMiddleware.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Microsoft.AspNetCore.Http; -using HotChocolate.AspNetCore.Instrumentation; -using HotChocolate.AspNetCore.Serialization; -using HotChocolate.AspNetCore.Subscriptions; -using RequestDelegate = Microsoft.AspNetCore.Http.RequestDelegate; - -namespace HotChocolate.AspNetCore; - -public sealed class WebSocketSubscriptionMiddleware : MiddlewareBase -{ - private readonly IServerDiagnosticEvents _diagnosticEvents; - - public WebSocketSubscriptionMiddleware( - RequestDelegate next, - IRequestExecutorProvider executorResolver, - IRequestExecutorEvents executorEvents, - IHttpResponseFormatter responseFormatter, - IServerDiagnosticEvents diagnosticEvents, - string schemaName) - : base(next, executorResolver, executorEvents, responseFormatter, schemaName) - { - _diagnosticEvents = diagnosticEvents ?? - throw new ArgumentNullException(nameof(diagnosticEvents)); - } - - public Task InvokeAsync(HttpContext context) - { - return context.WebSockets.IsWebSocketRequest - ? HandleWebSocketSessionAsync(context) - : NextAsync(context); - } - - private async Task HandleWebSocketSessionAsync(HttpContext context) - { - if (!IsDefaultSchema) - { - context.Items[WellKnownContextData.SchemaName] = SchemaName; - } - - using (_diagnosticEvents.WebSocketSession(context)) - { - try - { - var executor = await GetExecutorAsync(context.RequestAborted); - var interceptor = executor.GetRequiredService(); - context.Items[WellKnownContextData.RequestExecutor] = executor; - await WebSocketSession.AcceptAsync(context, executor, interceptor); - } - catch (Exception ex) - { - _diagnosticEvents.WebSocketSessionError(context, ex); - } - } - } -} diff --git a/src/HotChocolate/AspNetCore/src/Transport.Abstractions/HotChocolate.Transport.Abstractions.csproj b/src/HotChocolate/AspNetCore/src/Transport.Abstractions/HotChocolate.Transport.Abstractions.csproj index 0b041dae748..57b130096c8 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Abstractions/HotChocolate.Transport.Abstractions.csproj +++ b/src/HotChocolate/AspNetCore/src/Transport.Abstractions/HotChocolate.Transport.Abstractions.csproj @@ -19,6 +19,7 @@ + diff --git a/src/HotChocolate/AspNetCore/src/Transport.Abstractions/OperationResult.cs b/src/HotChocolate/AspNetCore/src/Transport.Abstractions/OperationResult.cs index b26239e200f..4df2cd0e068 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Abstractions/OperationResult.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Abstractions/OperationResult.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using HotChocolate.Buffers; using static HotChocolate.Transport.Properties.TransportAbstractionResources; using static HotChocolate.Transport.Serialization.Utf8GraphQLResultProperties; @@ -16,8 +17,7 @@ public sealed class OperationResult : IDisposable /// specified JSON document and optional data, errors, and extensions. /// /// - /// The memory owner of the json elements. - /// operation. + /// The memory owner of the JSON elements. /// /// /// A object representing the data returned by the operation. @@ -86,6 +86,21 @@ public OperationResult( public void Dispose() => _memoryOwner?.Dispose(); + public static OperationResult Parse(JsonDocumentOwner documentOwner) + { + ArgumentNullException.ThrowIfNull(documentOwner); + + var root = documentOwner.Document.RootElement; + + return new OperationResult( + documentOwner, + root.TryGetProperty(DataProp, out var data) ? data : default, + root.TryGetProperty(ErrorsProp, out var errors) ? errors : default, + root.TryGetProperty(ExtensionsProp, out var extensions) ? extensions : default, + root.TryGetProperty(RequestIndexProp, out var requestIndex) ? requestIndex.GetInt32() : null, + root.TryGetProperty(VariableIndexProp, out var variableIndex) ? variableIndex.GetInt32() : null); + } + public static OperationResult Parse(JsonDocument document) { ArgumentNullException.ThrowIfNull(document); @@ -110,11 +125,39 @@ public static OperationResult Parse(ReadOnlySpan span) nameof(span)); } - var reader = new Utf8JsonReader(span, true, default); - var root = JsonElement.ParseValue(ref reader); + var buffer = new PooledArrayWriter(); + var bufferSpan = buffer.GetSpan(span.Length); + span.CopyTo(bufferSpan); + buffer.Advance(span.Length); + + var document = JsonDocument.Parse(buffer.WrittenMemory); + var root = document.RootElement; + var documentOwner = new JsonDocumentOwner(document, buffer); + + return new OperationResult( + documentOwner, + root.TryGetProperty(DataProp, out var data) ? data : default, + root.TryGetProperty(ErrorsProp, out var errors) ? errors : default, + root.TryGetProperty(ExtensionsProp, out var extensions) ? extensions : default, + root.TryGetProperty(RequestIndexProp, out var requestIndex) ? requestIndex.GetInt32() : null, + root.TryGetProperty(VariableIndexProp, out var variableIndex) ? variableIndex.GetInt32() : null); + } + + public static OperationResult Parse(PooledArrayWriter buffer) + { + if (buffer.WrittenSpan.Length == 0) + { + throw new ArgumentException( + OperationResult_Parse_JsonDataIsEmpty, + nameof(buffer)); + } + + var document = JsonDocument.Parse(buffer.WrittenMemory); + var root = document.RootElement; + var documentOwner = new JsonDocumentOwner(document, buffer); return new OperationResult( - null, + documentOwner, root.TryGetProperty(DataProp, out var data) ? data : default, root.TryGetProperty(ErrorsProp, out var errors) ? errors : default, root.TryGetProperty(ExtensionsProp, out var extensions) ? extensions : default, diff --git a/src/HotChocolate/AspNetCore/src/Transport.Formatters/ConcurrentStreamWriter.cs b/src/HotChocolate/AspNetCore/src/Transport.Formatters/ConcurrentStreamWriter.cs index 0e149e039a7..fc1ce4ff2e3 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Formatters/ConcurrentStreamWriter.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Formatters/ConcurrentStreamWriter.cs @@ -99,7 +99,7 @@ private async Task ProcessBacklogAsync(CancellationToken ct) try { currentBuffer = message; - await _stream.WriteAsync(message.GetWrittenMemory(), ct).ConfigureAwait(false); + await _stream.WriteAsync(message.WrittenMemory, ct).ConfigureAwait(false); await _stream.FlushAsync(ct).ConfigureAwait(false); } finally diff --git a/src/HotChocolate/AspNetCore/src/Transport.Formatters/EventStreamResultFormatter.cs b/src/HotChocolate/AspNetCore/src/Transport.Formatters/EventStreamResultFormatter.cs index ffb1021651b..1a9ce6a4c0b 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Formatters/EventStreamResultFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Formatters/EventStreamResultFormatter.cs @@ -66,7 +66,7 @@ private async ValueTask FormatOperationResultAsync( if (!ct.IsCancellationRequested) { - await outputStream.WriteAsync(buffer.GetWrittenMemory(), ct).ConfigureAwait(false); + await outputStream.WriteAsync(buffer.WrittenMemory, ct).ConfigureAwait(false); await outputStream.FlushAsync(ct).ConfigureAwait(false); } } diff --git a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonLinesResultFormatter.cs b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonLinesResultFormatter.cs index 8399e65b769..b02f615843c 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonLinesResultFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonLinesResultFormatter.cs @@ -46,7 +46,7 @@ private async ValueTask FormatOperationResultAsync( if (!ct.IsCancellationRequested) { - await outputStream.WriteAsync(buffer.GetWrittenMemory(), ct).ConfigureAwait(false); + await outputStream.WriteAsync(buffer.WrittenMemory, ct).ConfigureAwait(false); await outputStream.FlushAsync(ct).ConfigureAwait(false); } } diff --git a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs index 9f771377d72..ad01e86e8a3 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs @@ -146,6 +146,14 @@ public void FormatErrors(IReadOnlyList errors, Utf8JsonWriter writer) writer.WriteEndArray(); } + public void FormatDictionary(IReadOnlyDictionary dictionary, Utf8JsonWriter writer) + { + ArgumentNullException.ThrowIfNull(dictionary); + ArgumentNullException.ThrowIfNull(writer); + + WriteDictionary(writer, dictionary, _serializerOptions, _nullIgnoreCondition); + } + public void Format(IOperationResult result, IBufferWriter writer) { ArgumentNullException.ThrowIfNull(result); @@ -181,7 +189,7 @@ private async ValueTask FormatInternalAsync( FormatInternal(result, buffer); await outputStream - .WriteAsync(buffer.GetWrittenMemory(), cancellationToken) + .WriteAsync(buffer.WrittenMemory, cancellationToken) .ConfigureAwait(false); await outputStream.FlushAsync(cancellationToken).ConfigureAwait(false); @@ -223,7 +231,7 @@ private async ValueTask FormatInternalAsync( } await outputStream - .WriteAsync(buffer.GetWrittenMemory(), cancellationToken) + .WriteAsync(buffer.WrittenMemory, cancellationToken) .ConfigureAwait(false); await outputStream.FlushAsync(cancellationToken).ConfigureAwait(false); @@ -251,7 +259,7 @@ private async ValueTask FormatInternalAsync( } await outputStream - .WriteAsync(buffer.GetWrittenMemory(), cancellationToken) + .WriteAsync(buffer.WrittenMemory, cancellationToken) .ConfigureAwait(false); await outputStream.FlushAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/HotChocolate/AspNetCore/src/Transport.Formatters/MultiPartResultFormatter.cs b/src/HotChocolate/AspNetCore/src/Transport.Formatters/MultiPartResultFormatter.cs index 27c2f63ab1b..6818239b5f3 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Formatters/MultiPartResultFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Formatters/MultiPartResultFormatter.cs @@ -103,7 +103,7 @@ private async ValueTask FormatOperationResultAsync( // Last we write the end of the part. MessageHelper.WriteEnd(buffer); - await outputStream.WriteAsync(buffer.GetInternalBuffer(), 0, buffer.Length, ct).ConfigureAwait(false); + await outputStream.WriteAsync(buffer.WrittenMemory, ct).ConfigureAwait(false); await outputStream.FlushAsync(ct).ConfigureAwait(false); } @@ -123,7 +123,7 @@ private async ValueTask FormatResultBatchAsync( MessageHelper.WriteResultHeader(buffer); MessageHelper.WritePayload(buffer, operationResult, _payloadFormatter); MessageHelper.WriteEnd(buffer); - await outputStream.WriteAsync(buffer.GetInternalBuffer(), 0, buffer.Length, ct).ConfigureAwait(false); + await outputStream.WriteAsync(buffer.WrittenMemory, ct).ConfigureAwait(false); await outputStream.FlushAsync(ct).ConfigureAwait(false); break; @@ -172,7 +172,7 @@ private async ValueTask FormatResponseStreamAsync( } // Now we can write the part to the output stream and flush this chunk. - await outputStream.WriteAsync(buffer.GetInternalBuffer(), 0, buffer.Length, ct).ConfigureAwait(false); + await outputStream.WriteAsync(buffer.WrittenMemory, ct).ConfigureAwait(false); await outputStream.FlushAsync(ct).ConfigureAwait(false); } finally @@ -186,7 +186,7 @@ private async ValueTask FormatResponseStreamAsync( // After all parts have been written, we need to write the final boundary. buffer.Reset(); MessageHelper.WriteEnd(buffer); - await outputStream.WriteAsync(buffer.GetInternalBuffer(), 0, buffer.Length, ct).ConfigureAwait(false); + await outputStream.WriteAsync(buffer.WrittenMemory, ct).ConfigureAwait(false); await outputStream.FlushAsync(ct).ConfigureAwait(false); } diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs index 1ecb5a17fa6..77196d02e2c 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs @@ -181,7 +181,8 @@ private static ByteArrayContent CreatePostContent( request.Body.WriteTo(jsonWriter); jsonWriter.Flush(); - var content = new ByteArrayContent(arrayWriter.GetInternalBuffer(), 0, arrayWriter.Length); + var internalBuffer = PooledArrayWriterMarshal.GetUnderlyingBuffer(arrayWriter); + var content = new ByteArrayContent(internalBuffer, 0, arrayWriter.Length); content.Headers.ContentType = new MediaTypeHeaderValue(ContentType.Json, "utf-8"); return content; } @@ -200,7 +201,7 @@ private static HttpContent CreateMultipartContent( var start = arrayWriter.Length; WriteOperationJson(arrayWriter, request); - var buffer = arrayWriter.GetInternalBuffer(); + var buffer = PooledArrayWriterMarshal.GetUnderlyingBuffer(arrayWriter); var form = new MultipartFormDataContent(); @@ -320,7 +321,7 @@ private static string FormatDocumentAsJson(PooledArrayWriter arrayWriter, object Utf8JsonWriterHelper.WriteFieldValue(jsonWriter, obj); jsonWriter.Flush(); - return Encoding.UTF8.GetString(arrayWriter.GetWrittenSpan()); + return Encoding.UTF8.GetString(arrayWriter.WrittenSpan); } protected override void Dispose(bool disposing) diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpClient.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpClient.cs index 6e345b21ca4..cc60b55d70d 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpClient.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpClient.cs @@ -49,6 +49,6 @@ protected virtual void Dispose(bool disposing) { } /// /// Returns the new <see cref="GraphQLHttpClient"/> instance. /// - public static GraphQLHttpClient Create(HttpClient httpClient, bool disposeHttpClient) + public static GraphQLHttpClient Create(HttpClient httpClient, bool disposeHttpClient = true) => new DefaultGraphQLHttpClient(httpClient, disposeHttpClient); } diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpEventStreamEnumerable.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpEventStreamEnumerable.cs index 97dc8988666..5d08e84c54e 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpEventStreamEnumerable.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpEventStreamEnumerable.cs @@ -1,6 +1,5 @@ using System.Buffers; using System.IO.Pipelines; -using System.Text; using HotChocolate.Buffers; using HotChocolate.Utilities; @@ -94,7 +93,7 @@ public async ValueTask MoveNextAsync() if (IsMessageComplete(_messageBuffer)) { - var readState = TryReadResult(_messageBuffer.GetWrittenSpan(), out var operationResult); + var readState = TryReadResult(_messageBuffer.WrittenSpan, out var operationResult); if (readState is ReadState.Message) { @@ -149,7 +148,7 @@ private static void WriteLineToMessage(PooledArrayWriter message, ReadOnlySequen private static bool IsMessageComplete(PooledArrayWriter message) { - var span = message.GetWrittenSpan(); + var span = message.WrittenSpan; return span.Length >= 2 && span[^1] == (byte)'\n' && span[^2] == (byte)'\n'; } @@ -200,48 +199,57 @@ private static OperationResult ParseData(ref ReadOnlySpan span) SkipWhitespaces(ref span); - using var payload = new PooledArrayWriter(); + var payload = new PooledArrayWriter(); - while (true) + try { - // read one logical line up to LF or end - var lineBreak = span.IndexOf((byte)'\n'); - ReadOnlySpan line; - if (lineBreak == -1) - { - line = span; - span = default; - } - else + while (true) { - line = span[..lineBreak]; - span = span[(lineBreak + 1)..]; - } + // read one logical line up to LF or end + var lineBreak = span.IndexOf((byte)'\n'); + ReadOnlySpan line; + if (lineBreak == -1) + { + line = span; + span = default; + } + else + { + line = span[..lineBreak]; + span = span[(lineBreak + 1)..]; + } - // Remove optional leading space - SkipWhitespaces(ref line); + // Remove optional leading space + SkipWhitespaces(ref line); - // append to buffer (insert LF between lines) - if (payload.Length > 0) - { - payload.GetSpan(1)[0] = (byte)'\n'; - payload.Advance(1); - } - line.CopyTo(payload.GetSpan(line.Length)); - payload.Advance(line.Length); + // append to buffer (insert LF between lines) + if (payload.Length > 0) + { + payload.GetSpan(1)[0] = (byte)'\n'; + payload.Advance(1); + } - // if the next part does not start with another data: line we are done - if (span.Length < 5 || !span.StartsWith(Data)) - { - break; + line.CopyTo(payload.GetSpan(line.Length)); + payload.Advance(line.Length); + + // if the next part does not start with another data: line, we are done + if (span.Length < 5 || !span.StartsWith(Data)) + { + break; + } + + // consume the next "data:" token and the following whitespace + span = span[5..]; + SkipWhitespaces(ref span); } - // consume next "data:" token & following whitespace - span = span[5..]; - SkipWhitespaces(ref span); + return OperationResult.Parse(payload); + } + catch + { + payload.Dispose(); + throw; } - - return OperationResult.Parse(payload.GetWrittenSpan()); } private static async Task ReadFromTransportAsync(Stream stream, PipeWriter writer, CancellationToken ct) @@ -286,8 +294,6 @@ private static async Task ReadFromTransportAsync(Stream stream, PipeWriter write } } - var debug = Encoding.UTF8.GetString(temp.GetWrittenSpan()); - await writer.CompleteAsync().ConfigureAwait(false); } diff --git a/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/MessageHelper.cs b/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/MessageHelper.cs index 3b96bd5a6e9..7c74a40edcf 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/MessageHelper.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/MessageHelper.cs @@ -34,7 +34,7 @@ public static async ValueTask SendConnectionInitMessage( jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(ct).ConfigureAwait(false); - await socket.SendAsync(arrayWriter.GetWrittenMemory(), Text, true, ct).ConfigureAwait(false); + await socket.SendAsync(arrayWriter.WrittenMemory, Text, true, ct).ConfigureAwait(false); } public static async ValueTask SendSubscribeMessageAsync( @@ -56,7 +56,7 @@ public static async ValueTask SendSubscribeMessageAsync( jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(ct).ConfigureAwait(false); - await socket.SendAsync(arrayWriter.GetWrittenMemory(), Text, true, ct).ConfigureAwait(false); + await socket.SendAsync(arrayWriter.WrittenMemory, Text, true, ct).ConfigureAwait(false); } public static async ValueTask SendCompleteMessageAsync( @@ -72,7 +72,7 @@ public static async ValueTask SendCompleteMessageAsync( jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(ct).ConfigureAwait(false); - await socket.SendAsync(arrayWriter.GetWrittenMemory(), Text, true, ct).ConfigureAwait(false); + await socket.SendAsync(arrayWriter.WrittenMemory, Text, true, ct).ConfigureAwait(false); } public static async ValueTask SendPongMessageAsync( @@ -86,6 +86,6 @@ public static async ValueTask SendPongMessageAsync( jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(ct).ConfigureAwait(false); - await socket.SendAsync(arrayWriter.GetWrittenMemory(), Text, true, ct).ConfigureAwait(false); + await socket.SendAsync(arrayWriter.WrittenMemory, Text, true, ct).ConfigureAwait(false); } } diff --git a/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/Messages/ErrorMessage.cs b/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/Messages/ErrorMessage.cs index 28ee8672095..75425618d1e 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/Messages/ErrorMessage.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/Messages/ErrorMessage.cs @@ -26,17 +26,21 @@ public static ErrorMessage From(ReadOnlySequence message) var arrayWriter = new PooledArrayWriter(); arrayWriter.Write(message); - var document = JsonDocument.Parse(arrayWriter.GetWrittenMemory()); + var document = JsonDocument.Parse(arrayWriter.WrittenMemory); + var root = document.RootElement; var id = root.GetProperty(Utf8MessageProperties.IdProp).GetString(); if (id is null) { + arrayWriter.Dispose(); + document.Dispose(); throw ThrowHelper.MessageHasNoId(); } + var documentOwner = new JsonDocumentOwner(document, arrayWriter); var payload = root.GetProperty(Utf8MessageProperties.PayloadProp); - var result = new OperationResult(arrayWriter, errors: payload); + var result = new OperationResult(documentOwner, errors: payload); return new ErrorMessage(id, result); } diff --git a/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/Messages/NextMessage.cs b/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/Messages/NextMessage.cs index 3ba53bc0669..18032cd3ada 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/Messages/NextMessage.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/Messages/NextMessage.cs @@ -20,20 +20,24 @@ private NextMessage(string id, OperationResult payload) public static NextMessage From(ReadOnlySequence message) { - // The ArrayWriter is used to copy the message because otherwise the buffer is reused and - // causes problems. The ArrayWriter is passed to the OperationResult where it's stored as - // the memory owner and disposed when the OperationResult is disposed. + // The ArrayWriter is used to copy the message because otherwise, the buffer is reused and + // causes problems. We bundle the arrayWriter and the document into a JsonDocumentOwner, + // which will be passed on with the operation result as the memory owner. + // The JsonDocumentOwner will be disposed when the OperationResult is disposed and in turn + // returns the memory to the pool and disposes the JsonDocument. var arrayWriter = new PooledArrayWriter(); arrayWriter.Write(message); - var document = JsonDocument.Parse(arrayWriter.GetWrittenMemory()); + var document = JsonDocument.Parse(arrayWriter.WrittenMemory); + var documentOwner = new JsonDocumentOwner(document, arrayWriter); var root = document.RootElement; - var id = root.GetProperty(Utf8MessageProperties.IdProp).GetString()!; + var id = root.GetProperty(Utf8MessageProperties.IdProp).GetString()!; var payload = root.GetProperty(Utf8MessageProperties.PayloadProp); + var result = new OperationResult( - arrayWriter, + documentOwner, TryGetProperty(payload, Utf8MessageProperties.DataProp), TryGetProperty(payload, Utf8MessageProperties.ErrorsProp), TryGetProperty(payload, Utf8MessageProperties.ExtensionsProp)); diff --git a/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/SocketClient.cs b/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/SocketClient.cs index 9db48d4245b..1f2369f306b 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/SocketClient.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/SocketClient.cs @@ -29,16 +29,19 @@ private SocketClient(WebSocket socket, IProtocolHandler protocol) _protocol = protocol; _context = new SocketClientContext(socket); _pipeline = new MessagePipeline(this, new MessageHandler(_context, protocol)); - _pipeline.Completed += (_, _) => - { - if (_context.Socket.CloseStatus is not null) + _pipeline.OnCompleted( + static context => { - _context.Messages.OnError(new SocketClosedException( - _context.Socket.CloseStatusDescription ?? "Socket was closed.", - _context.Socket.CloseStatus.Value)); - } - _context.Messages.OnCompleted(); - }; + if (context.Socket.CloseStatus is not null) + { + context.Messages.OnError( + new SocketClosedException( + context.Socket.CloseStatusDescription ?? "Socket was closed.", + context.Socket.CloseStatus.Value)); + } + context.Messages.OnCompleted(); + }, + _context); _ct = _cts.Token; } diff --git a/src/HotChocolate/AspNetCore/src/Transport.Sockets/MessagePipeline.cs b/src/HotChocolate/AspNetCore/src/Transport.Sockets/MessagePipeline.cs index 31706a11e60..076d980e54e 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Sockets/MessagePipeline.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Sockets/MessagePipeline.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.IO.Pipelines; namespace HotChocolate.Transport.Sockets; @@ -9,8 +10,7 @@ public sealed class MessagePipeline { private readonly MessageReceiver _messageReceiver; private readonly MessageProcessor _messageProcessor; - - public EventHandler? Completed; + private ImmutableArray<(Action action, object state)> _subscribers = []; /// /// Initializes a new instance of . @@ -35,6 +35,23 @@ public MessagePipeline(ISocket socket, IMessageHandler messageHandler) _messageProcessor = new MessageProcessor(messageHandler, pipe.Reader); } + /// + /// Registers a delegate that will be invoked once the message pipeline has completed. + /// + /// + /// The delegate that will be invoked on completion. + /// + /// + /// The state that is passed along. + /// + /// + /// The type of the state. + /// + public void OnCompleted(Action completed, T state) where T : class + { + _subscribers = _subscribers.Add((o => completed((T)o!), state)); + } + /// /// Run the pipeline and process messages. /// @@ -45,6 +62,10 @@ public async Task RunAsync(CancellationToken cancellationToken) await Task.WhenAll( _messageReceiver.ReceiveMessagesAsync(cancellationToken), _messageProcessor.ProcessMessagesAsync(cancellationToken)); - Completed?.Invoke(this, EventArgs.Empty); + + foreach (var (action, state) in _subscribers) + { + action(state); + } } } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/ServerTestBase.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/ServerTestBase.cs index 31e66e78f37..f893765afb4 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/ServerTestBase.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/ServerTestBase.cs @@ -36,8 +36,8 @@ protected virtual TestServer CreateStarWarsServer( services .AddSingleton(mockHostEnvironment.Object) .AddRouting() - .AddHttpResponseFormatter() .AddGraphQLServer() + .AddHttpResponseFormatter() .AddStarWarsTypes() .AddTypeExtension() .AddTypeExtension() @@ -149,8 +149,8 @@ protected virtual TestServer CreateServer( return ServerFactory.Create( services => services .AddRouting() - .AddHttpResponseFormatter() .AddGraphQLServer() + .AddHttpResponseFormatter() .AddStarWarsTypes() .AddTypeExtension() .AddTypeExtension() diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/Subscriptions/Apollo/WebSocketExtensions.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/Subscriptions/Apollo/WebSocketExtensions.cs index 380d4f88ec1..e84229a0c69 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/Subscriptions/Apollo/WebSocketExtensions.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/Subscriptions/Apollo/WebSocketExtensions.cs @@ -1,5 +1,6 @@ using System.Net.WebSockets; using System.Text; +using HotChocolate.AspNetCore.Formatters; using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.AspNetCore.Subscriptions.Protocols.Apollo; using HotChocolate.Buffers; @@ -35,8 +36,9 @@ public static async Task SendConnectionInitializeAsync( CancellationToken cancellationToken) { using var writer = new PooledArrayWriter(); - MessageUtilities.SerializeMessage(writer, Utf8Messages.ConnectionInitialize, payload); - await SendMessageAsync(webSocket, writer.GetWrittenMemory(), cancellationToken); + var formatter = new DefaultWebSocketPayloadFormatter(); + MessageUtilities.SerializeMessage(writer, formatter, Utf8Messages.ConnectionInitialize, payload); + await SendMessageAsync(webSocket, writer.WrittenMemory, cancellationToken); } public static async Task SendTerminateConnectionAsync( @@ -44,8 +46,9 @@ public static async Task SendTerminateConnectionAsync( CancellationToken cancellationToken) { using var writer = new PooledArrayWriter(); - MessageUtilities.SerializeMessage(writer, Utf8Messages.ConnectionTerminate); - await SendMessageAsync(webSocket, writer.GetWrittenMemory(), cancellationToken); + var formatter = new DefaultWebSocketPayloadFormatter(); + MessageUtilities.SerializeMessage(writer, formatter, Utf8Messages.ConnectionTerminate); + await SendMessageAsync(webSocket, writer.WrittenMemory, cancellationToken); } public static async Task SendSubscriptionStartAsync( @@ -66,8 +69,9 @@ public static async Task SendSubscriptionStopAsync( CancellationToken cancellationToken) { using var writer = new PooledArrayWriter(); - MessageUtilities.SerializeMessage(writer, Utf8Messages.Stop, id: subscriptionId); - await SendMessageAsync(webSocket, writer.GetWrittenMemory(), cancellationToken); + var formatter = new DefaultWebSocketPayloadFormatter(); + MessageUtilities.SerializeMessage(writer, formatter, Utf8Messages.Stop, id: subscriptionId); + await SendMessageAsync(webSocket, writer.WrittenMemory, cancellationToken); } public static Task SendMessageAsync( @@ -161,8 +165,9 @@ private static Stream CreateMessageStream(this OperationMessage message, bool la var array = new ArraySegment(buffer); result = await webSocket.ReceiveAsync(array, cancellationToken); - if (result.Count == 2 && result.EndOfMessage && - buffer.AsSpan()[..2].SequenceEqual(Utf8MessageBodies.KeepAlive.Span)) + if (result.Count == 2 + && result.EndOfMessage + && buffer.AsSpan()[..2].SequenceEqual(Utf8MessageBodies.KeepAlive.Span)) { skipped = true; continue; @@ -181,17 +186,11 @@ private static Stream CreateMessageStream(this OperationMessage message, bool la return (IReadOnlyDictionary?)ParseJson(stream.ToArray())!; } - private sealed class HelperOperationMessage : OperationMessage + private sealed class HelperOperationMessage(string type, string id, object payload) + : OperationMessage(type) { - public HelperOperationMessage(string type, string id, object payload) - : base(type) - { - Id = id; - Payload = payload; - } - - public string Id { get; } + public string Id { get; } = id; - public object Payload { get; } + public object Payload { get; } = payload; } } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/Subscriptions/GraphQLOverWebSocket/SubscriptionTestBase.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/Subscriptions/GraphQLOverWebSocket/SubscriptionTestBase.cs index c876be90392..458c4227460 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/Subscriptions/GraphQLOverWebSocket/SubscriptionTestBase.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/Subscriptions/GraphQLOverWebSocket/SubscriptionTestBase.cs @@ -5,13 +5,9 @@ namespace HotChocolate.AspNetCore.Tests.Utilities.Subscriptions.GraphQLOverWebSocket; -public class SubscriptionTestBase : ServerTestBase +public class SubscriptionTestBase(TestServerFactory serverFactory) + : ServerTestBase(serverFactory) { - public SubscriptionTestBase(TestServerFactory serverFactory) - : base(serverFactory) - { - } - protected Uri SubscriptionUri { get; } = new("ws://localhost:5000/graphql"); protected Task?> WaitForMessage( @@ -27,9 +23,7 @@ public SubscriptionTestBase(TestServerFactory serverFactory) CancellationToken cancellationToken) { using var timeoutCts = new CancellationTokenSource(timeout); - using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource( - cancellationToken, - timeoutCts.Token); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); try { @@ -53,7 +47,7 @@ public SubscriptionTestBase(TestServerFactory serverFactory) } catch (OperationCanceledException) { - // no massage was received in the specified time. + // no message was received in the specified time. } return null; @@ -110,7 +104,7 @@ protected static WebSocketClient CreateWebSocketClient(TestServer testServer) protected static async Task TryTest(Func action) { - // we will try four times .... + // we will try four times ... using var cts = new CancellationTokenSource(Debugger.IsAttached ? 600_000_000 : 15_000); var ct = cts.Token; var count = 0; diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/Subscriptions/GraphQLOverWebSocket/WebSocketExtensions.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/Subscriptions/GraphQLOverWebSocket/WebSocketExtensions.cs index a8545e193cd..45ee3a829fa 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/Subscriptions/GraphQLOverWebSocket/WebSocketExtensions.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests.Utilities/Subscriptions/GraphQLOverWebSocket/WebSocketExtensions.cs @@ -1,5 +1,6 @@ using System.Net.WebSockets; using System.Text; +using HotChocolate.AspNetCore.Formatters; using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.AspNetCore.Subscriptions.Protocols.GraphQLOverWebSocket; using HotChocolate.Buffers; @@ -21,8 +22,9 @@ public static async Task SendConnectionInitAsync( CancellationToken cancellationToken) { using var writer = new PooledArrayWriter(); - MessageUtilities.SerializeMessage(writer, Utf8Messages.ConnectionInitialize, payload); - await SendMessageAsync(webSocket, writer.GetWrittenMemory(), cancellationToken); + var formatter = new DefaultWebSocketPayloadFormatter(); + MessageUtilities.SerializeMessage(writer, formatter, Utf8Messages.ConnectionInitialize, payload); + await SendMessageAsync(webSocket, writer.WrittenMemory, cancellationToken); } public static async Task SendSubscribeAsync( @@ -59,8 +61,9 @@ public static async Task SendSubscribeAsync( } using var writer = new PooledArrayWriter(); - MessageUtilities.SerializeMessage(writer, Utf8Messages.Subscribe, map, subscriptionId); - await SendMessageAsync(webSocket, writer.GetWrittenMemory(), cancellationToken); + var formatter = new DefaultWebSocketPayloadFormatter(); + MessageUtilities.SerializeMessage(writer, formatter, Utf8Messages.Subscribe, map, subscriptionId); + await SendMessageAsync(webSocket, writer.WrittenMemory, cancellationToken); } public static async Task SendCompleteAsync( @@ -69,8 +72,9 @@ public static async Task SendCompleteAsync( CancellationToken cancellationToken) { using var writer = new PooledArrayWriter(); - MessageUtilities.SerializeMessage(writer, Utf8Messages.Complete, id: subscriptionId); - await SendMessageAsync(webSocket, writer.GetWrittenMemory(), cancellationToken); + var formatter = new DefaultWebSocketPayloadFormatter(); + MessageUtilities.SerializeMessage(writer, formatter, Utf8Messages.Complete, id: subscriptionId); + await SendMessageAsync(webSocket, writer.WrittenMemory, cancellationToken); } public static Task SendPingAsync( @@ -84,8 +88,9 @@ public static async Task SendPingAsync( CancellationToken cancellationToken) { using var writer = new PooledArrayWriter(); - MessageUtilities.SerializeMessage(writer, Utf8Messages.Ping, payload); - await SendMessageAsync(webSocket, writer.GetWrittenMemory(), cancellationToken); + var formatter = new DefaultWebSocketPayloadFormatter(); + MessageUtilities.SerializeMessage(writer, formatter, Utf8Messages.Ping, payload); + await SendMessageAsync(webSocket, writer.WrittenMemory, cancellationToken); } public static Task SendPongAsync( @@ -99,8 +104,9 @@ public static async Task SendPongAsync( CancellationToken cancellationToken) { using var writer = new PooledArrayWriter(); - MessageUtilities.SerializeMessage(writer, Utf8Messages.Pong, payload); - await SendMessageAsync(webSocket, writer.GetWrittenMemory(), cancellationToken); + var formatter = new DefaultWebSocketPayloadFormatter(); + MessageUtilities.SerializeMessage(writer, formatter, Utf8Messages.Pong, payload); + await SendMessageAsync(webSocket, writer.WrittenMemory, cancellationToken); } public static Task SendMessageAsync( diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Extensions/ServiceCollectionExtensionTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Extensions/ServiceCollectionExtensionTests.cs deleted file mode 100644 index 6aad582e5e2..00000000000 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Extensions/ServiceCollectionExtensionTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -using HotChocolate.AspNetCore.Serialization; - -namespace Microsoft.Extensions.DependencyInjection; - -public static class ServiceCollectionExtensionTests -{ - [Fact] - public static void AddHttpRequestSerializer_OfT() - { - // arrange - var serviceCollection = new ServiceCollection(); - - // act - serviceCollection.AddHttpResponseFormatter(); - - // assert - Assert.Collection( - serviceCollection, - t => - { - Assert.Equal(typeof(IHttpResponseFormatter), t.ServiceType); - Assert.Equal(typeof(DefaultHttpResponseFormatter), t.ImplementationType); - }); - } -} diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs index 41877cee605..cf29d4d0f51 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Http.Json; +using HotChocolate.AspNetCore.Formatters; using HotChocolate.AspNetCore.Serialization; using HotChocolate.AspNetCore.Tests.Utilities; using HotChocolate.Transport; @@ -656,7 +657,7 @@ public async Task After_Execution_StatusCode_Is_200() private HttpClient GetClient(HttpTransportVersion serverTransportVersion) { var server = CreateStarWarsServer( - configureServices: s => s.AddHttpResponseFormatter( + configureServices: s => s.AddGraphQLServer().AddHttpResponseFormatter( new HttpResponseFormatterOptions { HttpTransportVersion = serverTransportVersion diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs index bcf0027d498..ddd62292fed 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpGetSchemaMiddlewareTests.cs @@ -42,8 +42,10 @@ public async Task Download_GraphQL_Schema(string path) // arrange var server = CreateStarWarsServer( configureServices: sp => - sp.RemoveAll() - .AddSingleton()); + sp.AddGraphQLServer() + .ConfigureSchemaServices( s => + s.RemoveAll() + .AddSingleton())); var url = TestServerExtensions.CreateUrl(path); var request = new HttpRequestMessage(HttpMethod.Get, url); @@ -66,10 +68,10 @@ public async Task Download_GraphQL_Schema_Slicing_Args_Enabled(string path) // arrange var server = CreateStarWarsServer( configureServices: sp => - sp - .RemoveAll() - .AddSingleton() - .AddGraphQL() + sp.AddGraphQLServer() + .ConfigureSchemaServices( s => + s.RemoveAll() + .AddSingleton()) .ModifyPagingOptions(o => o.RequirePagingBoundaries = true)); var url = TestServerExtensions.CreateUrl(path); var request = new HttpRequestMessage(HttpMethod.Get, url); @@ -92,9 +94,8 @@ public async Task Download_GraphQL_Schema_Not_Allowed(string path) { // arrange var server = CreateStarWarsServer( - configureServices: s => - s.AddGraphQL() - .ModifyRequestOptions(o => o.EnableSchemaFileSupport = false)); + configureConventions: b => + b.WithOptions(new GraphQLServerOptions { EnableSchemaFileSupport = false })); var url = TestServerExtensions.CreateUrl(path); var request = new HttpRequestMessage(HttpMethod.Get, url); diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs index f4f340c8155..271917a1e10 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs @@ -1,6 +1,6 @@ using System.Net.Http.Json; +using HotChocolate.AspNetCore.Formatters; using HotChocolate.AspNetCore.Instrumentation; -using HotChocolate.AspNetCore.Serialization; using HotChocolate.AspNetCore.Tests.Utilities; using HotChocolate.Execution; using HotChocolate.Transport.Formatters; @@ -82,7 +82,7 @@ public async Task Serialize_Payload_With_Whitespaces() { // arrange var server = CreateStarWarsServer( - configureServices: sc => sc.AddHttpResponseFormatter(indented: true)); + configureServices: sc => sc.AddGraphQLServer().AddHttpResponseFormatter(indented: true)); // act var result = await server.PostRawAsync( @@ -97,7 +97,7 @@ public async Task Serialize_Payload_Without_Extra_Whitespaces() { // arrange var server = CreateStarWarsServer( - configureServices: sc => sc.AddHttpResponseFormatter(indented: false)); + configureServices: sc => sc.AddGraphQLServer().AddHttpResponseFormatter(indented: false)); // act var result = await server.PostRawAsync( @@ -197,7 +197,7 @@ public async Task Override_OnWriteResponseHeaders() { // arrange var server = CreateStarWarsServer( - configureServices: s => s.AddHttpResponseFormatter()); + configureServices: s => s.AddGraphQLServer().AddHttpResponseFormatter()); // act var result = @@ -307,20 +307,22 @@ public async Task Single_Diagnostic_Listener_Is_Triggered() await server.PostRawAsync( new ClientQueryRequest { - Query = @" - { - ... @defer { - wait(m: 300) - } - hero(episode: NEW_HOPE) + Query = + """ { - name - ... on Droid @defer(label: ""my_id"") + ... @defer { + wait(m: 300) + } + hero(episode: NEW_HOPE) { - id + name + ... on Droid @defer(label: "my_id") + { + id + } } } - }" + """ }); // assert @@ -966,7 +968,7 @@ public async Task Strip_Null_Values_Variant_1() { // arrange var server = CreateStarWarsServer( - configureServices: s => s.AddHttpResponseFormatter( + configureServices: s => s.AddGraphQLServer().AddHttpResponseFormatter( _ => new DefaultHttpResponseFormatter( new HttpResponseFormatterOptions { @@ -1005,7 +1007,7 @@ public async Task Strip_Null_Values_Variant_2() { // arrange var server = CreateStarWarsServer( - configureServices: s => s.AddHttpResponseFormatter( + configureServices: s => s.AddGraphQLServer().AddHttpResponseFormatter( new HttpResponseFormatterOptions { Json = new JsonResultFormatterOptions { NullIgnoreCondition = JsonNullIgnoreCondition.Fields } @@ -1043,7 +1045,6 @@ public async Task Strip_Null_Elements() configureServices: s => s .AddGraphQLServer("test") .AddQueryType() - .Services .AddHttpResponseFormatter( new HttpResponseFormatterOptions { diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Serialization/DefaultHttpRequestParserTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Serialization/DefaultHttpRequestParserTests.cs index 6f2580b1f40..669db5bbce6 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Serialization/DefaultHttpRequestParserTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Serialization/DefaultHttpRequestParserTests.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Text; +using HotChocolate.AspNetCore.Parsers; using HotChocolate.Execution.Caching; using HotChocolate.Language; using Microsoft.AspNetCore.Http; @@ -50,7 +51,7 @@ public async Task ParseRequestAsync_Invalid_QueryId(string id) // act async Task Parse() { - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); var parser = new DefaultHttpRequestParser( new DefaultDocumentCache(), diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Serialization/VariablePathTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Serialization/VariablePathTests.cs index 65bb7eefd42..92f7d0a3387 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Serialization/VariablePathTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Serialization/VariablePathTests.cs @@ -1,3 +1,5 @@ +using HotChocolate.AspNetCore.Parsers; + namespace HotChocolate.AspNetCore.Serialization; public class VariablePathTests 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 a402008e49b..c194129cc06 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs @@ -1,5 +1,7 @@ using System.Net.WebSockets; -using HotChocolate.AspNetCore.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using HotChocolate.AspNetCore.Formatters; using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.AspNetCore.Subscriptions.Protocols.Apollo; using HotChocolate.AspNetCore.Tests.Utilities; @@ -588,7 +590,7 @@ public override ValueTask OnConnectAsync( IOperationMessagePayload connectionInitMessage, CancellationToken cancellationToken = default) { - var payload = connectionInitMessage.As(); + var payload = connectionInitMessage.Payload?.Deserialize(); if (payload?.Token is not null) { @@ -600,6 +602,7 @@ public override ValueTask OnConnectAsync( private sealed class Auth { + [JsonPropertyName("token")] public string? Token { get; set; } } } 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 3c12548caa0..e681f3e8cba 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/GraphQLOverWebSocket/WebSocketProtocolTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/GraphQLOverWebSocket/WebSocketProtocolTests.cs @@ -1,4 +1,7 @@ using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Serialization; +using HotChocolate.AspNetCore.Formatters; using HotChocolate.AspNetCore.Serialization; using HotChocolate.AspNetCore.Subscriptions.Protocols; using HotChocolate.AspNetCore.Subscriptions.Protocols.GraphQLOverWebSocket; @@ -957,7 +960,7 @@ public override ValueTask OnConnectAsync( IOperationMessagePayload connectionInitMessage, CancellationToken cancellationToken = default) { - var payload = connectionInitMessage.As(); + var payload = connectionInitMessage.Payload?.Deserialize(); if (payload?.Token is not null) { @@ -967,11 +970,10 @@ public override ValueTask OnConnectAsync( return new(ConnectionStatus.Reject()); } - // ReSharper disable once ClassNeverInstantiated.Local private sealed class Auth { - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public string? Token { get; set; } + [JsonPropertyName("token")] + public string? Token { get; init; } } } @@ -986,7 +988,7 @@ private class PingPongInterceptor : DefaultSocketSessionInterceptor IOperationMessagePayload pingMessage, CancellationToken cancellationToken = default) { - var payload = pingMessage.As>(); + var payload = pingMessage.Payload?.Deserialize>(); var responsePayload = new Dictionary { ["touched"] = true }; if (payload is not null) @@ -1006,7 +1008,7 @@ public override ValueTask OnPongAsync( CancellationToken cancellationToken = default) { OnPongInvoked = true; - Payload = pongMessage.As>(); + Payload = pongMessage.Payload?.Deserialize>(); return base.OnPongAsync(session, pongMessage, cancellationToken); } } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/OperationManagerTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/OperationManagerTests.cs index 8f89db93c9d..2edadba5a40 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/OperationManagerTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/OperationManagerTests.cs @@ -10,75 +10,6 @@ namespace HotChocolate.AspNetCore.Subscriptions; public class OperationManagerTests { - [Fact] - public async Task Enqueue_SessionId_Is_Null() - { - // arrange - var executor = - await new ServiceCollection() - .AddGraphQLServer() - .AddStarWars() - .BuildRequestExecutorAsync(); - - var session = new Mock(); - var interceptor = new Mock(); - var subscriptions = new OperationManager(session.Object, interceptor.Object, executor); - - // act - void Action() => subscriptions.Enqueue(null!, new GraphQLRequest(null, documentId: "123")); - - // assert - Assert.Equal( - "sessionId", - Assert.Throws(Action).ParamName); - } - - [Fact] - public async Task Enqueue_SessionId_Is_Empty() - { - // arrange - var executor = - await new ServiceCollection() - .AddGraphQLServer() - .AddStarWars() - .BuildRequestExecutorAsync(); - - var session = new Mock(); - var interceptor = new Mock(); - var subscriptions = new OperationManager(session.Object, interceptor.Object, executor); - - // act - void Action() => subscriptions.Enqueue("", new GraphQLRequest(null, documentId: "123")); - - // assert - Assert.Equal( - "sessionId", - Assert.Throws(Action).ParamName); - } - - [Fact] - public async Task Enqueue_Request_Is_Null() - { - // arrange - var executor = - await new ServiceCollection() - .AddGraphQLServer() - .AddStarWars() - .BuildRequestExecutorAsync(); - - var session = new Mock(); - var interceptor = new Mock(); - var subscriptions = new OperationManager(session.Object, interceptor.Object, executor); - - // act - void Action() => subscriptions.Enqueue("abc", null!); - - // assert - Assert.Equal( - "request", - Assert.Throws(Action).ParamName); - } - [Fact] public async Task Enqueue_On_Disposed_Manager() { @@ -87,6 +18,8 @@ public async Task Enqueue_On_Disposed_Manager() await new ServiceCollection() .AddGraphQLServer() .AddStarWars() + .AddInMemorySubscriptions() + .AddSocketSessionInterceptor() .BuildRequestExecutorAsync(); var socketSession = new TestSocketSession(); @@ -94,19 +27,17 @@ public async Task Enqueue_On_Disposed_Manager() var mockSession = new Mock(); mockSession.SetupGet(t => t.Id).Returns("abc"); - using var subscriptions = new OperationManager( + var operations = new OperationManager( socketSession, - new DefaultSocketSessionInterceptor(), - executor, - _ => mockSession.Object); + new ExecutorSession(executor)); + socketSession.Operations = operations; + operations.Dispose(); + // act var query = Utf8GraphQLParser.Parse( "subscription { onReview(episode: NEW_HOPE) { stars } }"); var request = new GraphQLRequest(query); - subscriptions.Dispose(); - - // act - void Fail() => subscriptions.Enqueue("abc", request); + void Fail() => operations.Enqueue("abc", request); // assert Assert.Throws(Fail); @@ -120,6 +51,8 @@ public async Task Enqueue_Request() await new ServiceCollection() .AddGraphQLServer() .AddStarWars() + .AddInMemorySubscriptions() + .AddSocketSessionInterceptor() .BuildRequestExecutorAsync(); var socketSession = new TestSocketSession(); @@ -127,19 +60,18 @@ public async Task Enqueue_Request() var mockSession = new Mock(); mockSession.SetupGet(t => t.Id).Returns("abc"); - using var subscriptions = new OperationManager( + using var operations = new OperationManager( socketSession, - new DefaultSocketSessionInterceptor(), - executor, - _ => mockSession.Object); + new ExecutorSession(executor)); + socketSession.Operations = operations; var query = Utf8GraphQLParser.Parse( "subscription { onReview(episode: NEW_HOPE) { stars } }"); var request = new GraphQLRequest(query); // act - var success = subscriptions.Enqueue("abc", request); - var registered = subscriptions.ToArray(); + var success = operations.Enqueue("abc", request); + var registered = operations.ToArray(); // assert Assert.True(success); @@ -154,6 +86,8 @@ public async Task Enqueue_Request_With_Non_Unique_Id() await new ServiceCollection() .AddGraphQLServer() .AddStarWars() + .AddInMemorySubscriptions() + .AddSocketSessionInterceptor() .BuildRequestExecutorAsync(); var socketSession = new TestSocketSession(); @@ -161,21 +95,20 @@ public async Task Enqueue_Request_With_Non_Unique_Id() var mockSession = new Mock(); mockSession.SetupGet(t => t.Id).Returns("abc"); - using var subscriptions = new OperationManager( + using var operations = new OperationManager( socketSession, - new DefaultSocketSessionInterceptor(), - executor, - _ => mockSession.Object); + new ExecutorSession(executor)); + socketSession.Operations = operations; var query = Utf8GraphQLParser.Parse( "subscription { onReview(episode: NEW_HOPE) { stars } }"); var request = new GraphQLRequest(query); - var success1 = subscriptions.Enqueue("abc", request); - var registered1 = subscriptions.ToArray(); + var success1 = operations.Enqueue("abc", request); + var registered1 = operations.ToArray(); // act - var success2 = subscriptions.Enqueue("abc", request); - var registered2 = subscriptions.ToArray(); + var success2 = operations.Enqueue("abc", request); + var registered2 = operations.ToArray(); // assert Assert.True(success1); @@ -192,6 +125,8 @@ public async Task Complete_Request() await new ServiceCollection() .AddGraphQLServer() .AddStarWars() + .AddInMemorySubscriptions() + .AddSocketSessionInterceptor() .BuildRequestExecutorAsync(); var socketSession = new TestSocketSession(); @@ -199,25 +134,32 @@ public async Task Complete_Request() var mockSession = new Mock(); mockSession.SetupGet(t => t.Id).Returns("abc"); - using var subscriptions = new OperationManager( + using var operations = new OperationManager( socketSession, - new DefaultSocketSessionInterceptor(), - executor, - _ => mockSession.Object); + new ExecutorSession(executor)); + socketSession.Operations = operations; var query = Utf8GraphQLParser.Parse( - "subscription { onReview(episode: NEW_HOPE) { stars } }"); + """ + subscription { + onReview(episode: NEW_HOPE) { + stars + } + } + """); var request = new GraphQLRequest(query); - var success1 = subscriptions.Enqueue("abc", request); - var registered1 = subscriptions.ToArray(); + var success1 = operations.Enqueue("abc", request); + var registered1 = operations.ToArray(); // act - var success2 = subscriptions.Complete("abc"); - var registered2 = subscriptions.ToArray(); + var success2 = operations.Complete("abc"); + var registered2 = operations.ToArray(); // assert Assert.True(success1); - Assert.Collection(registered1, t => Assert.Equal("abc", t.Id)); + var session = Assert.Single(registered1); + Assert.Equal("abc", session.Id); + Assert.True(success2); Assert.Empty(registered2); } @@ -230,11 +172,12 @@ public async Task Complete_SessionId_Is_Null() await new ServiceCollection() .AddGraphQLServer() .AddStarWars() + .AddInMemorySubscriptions() + .AddSocketSessionInterceptor() .BuildRequestExecutorAsync(); var session = new Mock(); - var interceptor = new Mock(); - var subscriptions = new OperationManager(session.Object, interceptor.Object, executor); + var subscriptions = new OperationManager(session.Object, new ExecutorSession(executor)); // act void Action() => subscriptions.Complete(null!); @@ -253,11 +196,12 @@ public async Task Complete_SessionId_Is_Empty() await new ServiceCollection() .AddGraphQLServer() .AddStarWars() + .AddInMemorySubscriptions() + .AddSocketSessionInterceptor() .BuildRequestExecutorAsync(); var session = new Mock(); - var interceptor = new Mock(); - var subscriptions = new OperationManager(session.Object, interceptor.Object, executor); + var subscriptions = new OperationManager(session.Object, new ExecutorSession(executor)); // act void Action() => subscriptions.Complete(""); @@ -276,6 +220,8 @@ public async Task Complete_On_Disposed_Manager() await new ServiceCollection() .AddGraphQLServer() .AddStarWars() + .AddInMemorySubscriptions() + .AddSocketSessionInterceptor() .BuildRequestExecutorAsync(); var socketSession = new TestSocketSession(); @@ -283,16 +229,14 @@ public async Task Complete_On_Disposed_Manager() var mockSession = new Mock(); mockSession.SetupGet(t => t.Id).Returns("abc"); - using var subscriptions = new OperationManager( + var operations = new OperationManager( socketSession, - new DefaultSocketSessionInterceptor(), - executor, - _ => mockSession.Object); - - subscriptions.Dispose(); + new ExecutorSession(executor)); + socketSession.Operations = operations; + operations.Dispose(); // act - void Fail() => subscriptions.Complete("abc"); + void Fail() => operations.Complete("abc"); // assert Assert.Throws(Fail); @@ -306,6 +250,8 @@ public async Task Dispose_OperationManager() await new ServiceCollection() .AddGraphQLServer() .AddStarWars() + .AddInMemorySubscriptions() + .AddSocketSessionInterceptor() .BuildRequestExecutorAsync(); var socketSession = new TestSocketSession(); @@ -313,21 +259,20 @@ public async Task Dispose_OperationManager() var mockSession = new Mock(); mockSession.SetupGet(t => t.Id).Returns("abc"); - using var subscriptions = new OperationManager( + var operations = new OperationManager( socketSession, - new DefaultSocketSessionInterceptor(), - executor, - _ => mockSession.Object); + new ExecutorSession(executor)); + socketSession.Operations = operations; var query = Utf8GraphQLParser.Parse( "subscription { onReview(episode: NEW_HOPE) { stars } }"); var request = new GraphQLRequest(query); - var success = subscriptions.Enqueue("abc", request); + var success = operations.Enqueue("abc", request); Assert.True(success); // act - subscriptions.Dispose(); - var registered = subscriptions.ToArray(); + operations.Dispose(); + var registered = operations.ToArray(); // assert Assert.Empty(registered); @@ -339,7 +284,7 @@ private class TestSocketSession : ISocketSession public IProtocolHandler Protocol { get; } = new TestProtocolHandler(); - public IOperationManager Operations => throw new NotImplementedException(); + public IOperationManager Operations { get; set; } = null!; public void Dispose() { @@ -389,4 +334,16 @@ public ValueTask OnConnectionInitTimeoutAsync( CancellationToken cancellationToken) => default; } + + public sealed class TestSocketSessionInterceptor : DefaultSocketSessionInterceptor + { + public override ValueTask OnRequestAsync( + ISocketSession session, + string operationSessionId, + OperationRequestBuilder requestBuilder, + CancellationToken cancellationToken = default) + { + return default; + } + } } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/GraphQLOverHttpSpecTests.OperationBatch.md b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/GraphQLOverHttpSpecTests.OperationBatch.md index 4afdf98f586..2939a0d579d 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/GraphQLOverHttpSpecTests.OperationBatch.md +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/GraphQLOverHttpSpecTests.OperationBatch.md @@ -3,14 +3,26 @@ ## Result 1 ```text -RequestIndex: 0 -Data: {"hero":{"name":"R2-D2"}} +{ + "requestIndex": 0, + "data": { + "hero": { + "name": "R2-D2" + } + } +} ``` ## Result 2 ```text -RequestIndex: 1 -Data: {"hero":{"name":"Luke Skywalker"}} +{ + "requestIndex": 1, + "data": { + "hero": { + "name": "Luke Skywalker" + } + } +} ``` diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/GraphQLOverHttpSpecTests.VariableBatch.md b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/GraphQLOverHttpSpecTests.VariableBatch.md index ce8e7875458..59b899cfa22 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/GraphQLOverHttpSpecTests.VariableBatch.md +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/GraphQLOverHttpSpecTests.VariableBatch.md @@ -3,14 +3,26 @@ ## Result 1 ```text -VariableIndex: 0 -Data: {"hero":{"name":"R2-D2"}} +{ + "variableIndex": 0, + "data": { + "hero": { + "name": "R2-D2" + } + } +} ``` ## Result 2 ```text -VariableIndex: 1 -Data: {"hero":{"name":"Luke Skywalker"}} +{ + "variableIndex": 1, + "data": { + "hero": { + "name": "Luke Skywalker" + } + } +} ``` diff --git a/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/GraphQLHttpClientTests.cs b/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/GraphQLHttpClientTests.cs index 1b939781adc..02e18271606 100644 --- a/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/GraphQLHttpClientTests.cs +++ b/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/GraphQLHttpClientTests.cs @@ -116,7 +116,13 @@ public async Task Post_GraphQL_Query_With_RequestUri() using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"hero":{"name":"R2-D2"}} + { + "data": { + "hero": { + "name": "R2-D2" + } + } + } """); } @@ -146,7 +152,13 @@ public async Task Post_GraphQL_Query_With_RequestUriString() using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"hero":{"name":"R2-D2"}} + { + "data": { + "hero": { + "name": "R2-D2" + } + } + } """); } @@ -177,7 +189,13 @@ public async Task Post_GraphQL_Query_With_BaseAddress() using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"hero":{"name":"R2-D2"}} + { + "data": { + "hero": { + "name": "R2-D2" + } + } + } """); } @@ -213,7 +231,13 @@ public async Task Post_GraphQL_Query_With_Variables_With_RequestUri() using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"hero":{"name":"R2-D2"}} + { + "data": { + "hero": { + "name": "R2-D2" + } + } + } """); } @@ -249,7 +273,13 @@ public async Task Post_GraphQL_Query_With_Variables_With_RequestUriString() using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"hero":{"name":"R2-D2"}} + { + "data": { + "hero": { + "name": "R2-D2" + } + } + } """); } @@ -285,7 +315,13 @@ public async Task Post_GraphQL_Query_With_JsonElement_Variable() using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"heroByTraits":{"name":"Luke Skywalker"}} + { + "data": { + "heroByTraits": { + "name": "Luke Skywalker" + } + } + } """); } @@ -320,7 +356,13 @@ public async Task Post_GraphQL_Query_With_Variables_With_BaseAddress() using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"hero":{"name":"R2-D2"}} + { + "data": { + "hero": { + "name": "R2-D2" + } + } + } """); } @@ -362,7 +404,13 @@ query B($episode: Episode!) { using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"hero":{"B":"R2-D2"}} + { + "data": { + "hero": { + "B": "R2-D2" + } + } + } """); } @@ -392,7 +440,13 @@ public async Task Get_GraphQL_Query_With_RequestUri() using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"hero":{"name":"R2-D2"}} + { + "data": { + "hero": { + "name": "R2-D2" + } + } + } """); } @@ -422,7 +476,13 @@ public async Task Get_GraphQL_Query_With_RequestUriString() using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"hero":{"name":"R2-D2"}} + { + "data": { + "hero": { + "name": "R2-D2" + } + } + } """); } @@ -453,7 +513,13 @@ public async Task Get_GraphQL_Query_With_BaseAddress() using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"hero":{"name":"R2-D2"}} + { + "data": { + "hero": { + "name": "R2-D2" + } + } + } """); } @@ -489,7 +555,13 @@ public async Task Get_GraphQL_Query_With_Variables_With_RequestUri() using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"hero":{"name":"R2-D2"}} + { + "data": { + "hero": { + "name": "R2-D2" + } + } + } """); } @@ -525,7 +597,13 @@ public async Task Get_GraphQL_Query_With_Variables_With_RequestUriString() using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"hero":{"name":"R2-D2"}} + { + "data": { + "hero": { + "name": "R2-D2" + } + } + } """); } @@ -560,7 +638,13 @@ public async Task Get_GraphQL_Query_With_Variables_With_BaseAddress() using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"hero":{"name":"R2-D2"}} + { + "data": { + "hero": { + "name": "R2-D2" + } + } + } """); } @@ -602,7 +686,13 @@ query B($episode: Episode!) { using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"hero":{"B":"R2-D2"}} + { + "data": { + "hero": { + "B": "R2-D2" + } + } + } """); } @@ -657,7 +747,13 @@ mutation CreateReviewForEpisode( { result.MatchInlineSnapshot( """ - Data: {"onReview":{"stars":5}} + { + "data": { + "onReview": { + "stars": 5 + } + } + } """); break; } @@ -717,7 +813,13 @@ mutation CreateReviewForEpisode( { result.MatchInlineSnapshot( """ - Data: {"onReview":{"stars":5}} + { + "data": { + "onReview": { + "stars": 5 + } + } + } """); await cts.CancelAsync(); } @@ -740,8 +842,8 @@ public async Task Get_Subscription_Over_SSE_With_Errors() using var server = ServerFactory.Create( services => services .AddRouting() - .AddHttpResponseFormatter() .AddGraphQLServer() + .AddHttpResponseFormatter() .AddQueryType(desc => { desc.Name("Query"); @@ -816,7 +918,11 @@ public async Task Post_GraphQL_FileUpload() using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"singleUpload":"abc"} + { + "data": { + "singleUpload": "abc" + } + } """); } @@ -860,7 +966,11 @@ public async Task Post_GraphQL_FileUpload_With_ObjectValueNode() using var body = await response.ReadAsResultAsync(cts.Token); body.MatchInlineSnapshot( """ - Data: {"singleUpload":"abc"} + { + "data": { + "singleUpload": "abc" + } + } """); } diff --git a/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/VariableBatchRequestTests.cs b/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/VariableBatchRequestTests.cs index c225044b5c1..226144a6962 100644 --- a/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/VariableBatchRequestTests.cs +++ b/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/VariableBatchRequestTests.cs @@ -105,13 +105,25 @@ public async Task Post_Variable_Batch() snapshot.MatchInline( """ --------------- - VariableIndex: 0 - Data: {"hero":{"name":"R2-D2"}} + { + "variableIndex": 0, + "data": { + "hero": { + "name": "R2-D2" + } + } + } --------------- --------------- - VariableIndex: 1 - Data: {"hero":{"name":"Luke Skywalker"}} + { + "variableIndex": 1, + "data": { + "hero": { + "name": "Luke Skywalker" + } + } + } --------------- """); @@ -183,20 +195,36 @@ public async Task Post_Request_With_Nested_Variable_Batch() snapshot.MatchInline( """ --------------- - RequestIndex: 0 - VariableIndex: 0 - Data: {"hero":{"name":"R2-D2"}} + { + "requestIndex": 0, + "variableIndex": 0, + "data": { + "hero": { + "name": "R2-D2" + } + } + } --------------- --------------- - RequestIndex: 0 - VariableIndex: 1 - Data: {"hero":{"name":"Luke Skywalker"}} + { + "requestIndex": 0, + "variableIndex": 1, + "data": { + "hero": { + "name": "Luke Skywalker" + } + } + } --------------- --------------- - RequestIndex: 1 - Data: {"__typename":"Query"} + { + "requestIndex": 1, + "data": { + "__typename": "Query" + } + } --------------- """); @@ -279,18 +307,34 @@ public async Task Post_Request_Batch() --------------- --------------- - RequestIndex: 0 - Data: {"hero":{"name":"R2-D2"}} + { + "requestIndex": 0, + "data": { + "hero": { + "name": "R2-D2" + } + } + } --------------- --------------- - RequestIndex: 1 - Data: {"hero":{"name":"Luke Skywalker"}} + { + "requestIndex": 1, + "data": { + "hero": { + "name": "Luke Skywalker" + } + } + } --------------- --------------- - RequestIndex: 2 - Data: {"__typename":"Query"} + { + "requestIndex": 2, + "data": { + "__typename": "Query" + } + } --------------- """); diff --git a/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/__snapshots__/GraphQLHttpClientTests.Get_Subscription_Over_SSE_With_Errors.md b/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/__snapshots__/GraphQLHttpClientTests.Get_Subscription_Over_SSE_With_Errors.md index 86f91021d05..bff46cc2629 100644 --- a/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/__snapshots__/GraphQLHttpClientTests.Get_Subscription_Over_SSE_With_Errors.md +++ b/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/__snapshots__/GraphQLHttpClientTests.Get_Subscription_Over_SSE_With_Errors.md @@ -3,18 +3,32 @@ ## Result 1 ```text -Data: {"onError":"hello1"} +{ + "data": { + "onError": "hello1" + } +} ``` ## Result 2 ```text -Data: {"onError":"hello2"} +{ + "data": { + "onError": "hello2" + } +} ``` ## Result 3 ```text -Errors: [{"message":"Unexpected Execution Error"}] +{ + "errors": [ + { + "message": "Unexpected Execution Error" + } + ] +} ``` diff --git a/src/HotChocolate/AspNetCore/test/Transport.Sockets.Client.Tests/GraphQLOverWebSocket/WebSocketClientProtocolTests.cs b/src/HotChocolate/AspNetCore/test/Transport.Sockets.Client.Tests/GraphQLOverWebSocket/WebSocketClientProtocolTests.cs index 613026a6fe8..0c30f5d2919 100644 --- a/src/HotChocolate/AspNetCore/test/Transport.Sockets.Client.Tests/GraphQLOverWebSocket/WebSocketClientProtocolTests.cs +++ b/src/HotChocolate/AspNetCore/test/Transport.Sockets.Client.Tests/GraphQLOverWebSocket/WebSocketClientProtocolTests.cs @@ -233,7 +233,7 @@ public override ValueTask OnConnectAsync( IOperationMessagePayload connectionInitMessage, CancellationToken cancellationToken = default) { - var payload = connectionInitMessage.As(); + var payload = connectionInitMessage.Payload?.Deserialize(); if (payload?.Token is not null) { diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs index ab4437d7e26..8a740ea38f9 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs @@ -2,11 +2,15 @@ using HotChocolate; using HotChocolate.AspNetCore; using HotChocolate.AzureFunctions; +using HotChocolate.Execution; using HotChocolate.Execution.Configuration; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.Azure.WebJobs.Host.Config; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Options; +using MiddlewareFactory = HotChocolate.AspNetCore.MiddlewareFactory; namespace Microsoft.Extensions.DependencyInjection; @@ -48,15 +52,14 @@ public static IRequestExecutorBuilder AddGraphQLFunction( services.AddGraphQLServer(maxAllowedRequestSize: maxAllowedRequestSize); // Register AzFunc Custom Binding Extensions for In-Process Functions. - // NOTE: This does not work for Isolated Process due to (but is not harmful at all of - // isolated process; it just remains dormant): + // NOTE: This does not work for Isolated Process due to (but is not harmful at all to + // an isolated process; it just remains dormant): // 1) Bindings always execute in-process and values must be marshaled between // the Host Process & the Isolated Process Worker! - // 2) Currently only String values are supported (obviously due to above complexities). - // More Info. here (using Blob binding docs): + // 2) Currently only String values are supported (due to the above complexities). + // More info here (using Blob binding docs): // https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-blob-input#usage - services.TryAddEnumerable( - ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); // Add the Request Executor Dependency... services.AddAzureFunctionsGraphQLRequestExecutor(apiRoute, schemaName); @@ -65,9 +68,9 @@ public static IRequestExecutorBuilder AddGraphQLFunction( } /// - /// Internal method to adds the Request Executor dependency for Azure Functions both + /// Internal method to add the Request Executor dependency for Azure Functions both /// in-process and isolate-process. Normal configuration should use AddGraphQLFunction() - /// extension instead which correctly call this internally. + /// extension instead that correctly calls this internally. /// private static IServiceCollection AddAzureFunctionsGraphQLRequestExecutor( this IServiceCollection services, @@ -89,19 +92,21 @@ private static IServiceCollection AddAzureFunctionsGraphQLRequestExecutor( // object. options.Tool.ServeMode = GraphQLToolServeMode.Embedded; - var schemaNameOrDefault = schemaName ?? ISchemaDefinition.DefaultName; + schemaName ??= ISchemaDefinition.DefaultName; + var executorProvider = sp.GetRequiredService(); + var executorEvents = sp.GetRequiredService(); + var formOptions = sp.GetRequiredService>(); + var executor = new HttpRequestExecutorProxy(executorProvider, executorEvents, schemaName); var pipeline = new PipelineBuilder() - .UseMiddleware(schemaNameOrDefault) - .UseMiddleware(schemaNameOrDefault) - .UseMiddleware(schemaNameOrDefault) - .UseMiddleware(schemaNameOrDefault) - .UseNitroApp(path) - .UseMiddleware( - schemaNameOrDefault, - path, - MiddlewareRoutingType.Integrated) - .Compile(sp); + .Use(MiddlewareFactory.CreateCancellationMiddleware()) + .Use(MiddlewareFactory.CreateWebSocketSubscriptionMiddleware(executor)) + .Use(MiddlewareFactory.CreateHttpPostMiddleware(executor)) + .Use(MiddlewareFactory.CreateHttpMultipartMiddleware(executor, formOptions)) + .Use(MiddlewareFactory.CreateHttpGetMiddleware(executor)) + .Use(MiddlewareFactory.CreateHttpGetSchemaMiddleware(executor, path, MiddlewareRoutingType.Integrated)) + .UseNitroApp(path) + .Compile(sp); return new DefaultGraphQLRequestExecutor(pipeline, options); }); @@ -143,10 +148,26 @@ private static PipelineBuilder UseNitroApp( var forwarderAccessor = new HttpForwarderAccessor(); return requestPipeline - .UseMiddleware(path) - .UseMiddleware(path, forwarderAccessor) - .UseMiddleware(fileProvider, path) - .UseMiddleware(fileProvider, path); + .Use(next => + { + var middleware = new NitroAppOptionsFileMiddleware(next, path); + return middleware.Invoke; + }) + .Use(next => + { + var middleware = new NitroAppCdnMiddleware(next, path, forwarderAccessor); + return middleware.Invoke; + }) + .Use(next => + { + var middleware = new NitroAppDefaultFileMiddleware(next, fileProvider, path); + return middleware.Invoke; + }) + .Use(next => + { + var middleware = new NitroAppStaticFileMiddleware(next, fileProvider, path); + return middleware.Invoke; + }); } private static IFileProvider CreateFileProvider() diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/PipelineBuilder.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/PipelineBuilder.cs index b1827151622..3f24e7a212a 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/PipelineBuilder.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/PipelineBuilder.cs @@ -1,35 +1,16 @@ -using System.Linq.Expressions; -using System.Reflection; -using HotChocolate.AspNetCore; -using HotChocolate.Utilities; using Microsoft.AspNetCore.Http; namespace HotChocolate.AzureFunctions; internal sealed class PipelineBuilder { - private static readonly ParameterExpression s_context = - Expression.Parameter(typeof(HttpContext), "context"); - private static readonly ParameterExpression s_services = - Expression.Parameter(typeof(IServiceProvider), "services"); - private static readonly ParameterExpression s_next = - Expression.Parameter(typeof(RequestDelegate), "next"); - private static readonly ConstantExpression s_schemaName = - Expression.Constant(ISchemaDefinition.DefaultName, typeof(string)); - private static readonly ConstantExpression s_routing = - Expression.Constant(MiddlewareRoutingType.Integrated, typeof(MiddlewareRoutingType)); - private static readonly MethodInfo s_getService = - typeof(IServiceProvider).GetMethod(nameof(IServiceProvider.GetService))!; - private static readonly MethodInfo s_compileInvoke = - typeof(PipelineBuilder).GetMethod( - nameof(CompileInvoke), - BindingFlags.Static | BindingFlags.NonPublic)!; + private readonly List> _components = []; - private readonly List<(Type, object[])> _components = []; - - public PipelineBuilder UseMiddleware(params object[] args) + public PipelineBuilder Use(Func middleware) { - _components.Add((typeof(T), args)); + ArgumentNullException.ThrowIfNull(middleware); + + _components.Add(middleware); return this; } @@ -43,7 +24,7 @@ public RequestDelegate Compile(IServiceProvider services) "There must be at least one component in order to build a pipeline."); } - RequestDelegate pipeline = context => + RequestDelegate next = context => { context.Response.StatusCode = 404; return Task.CompletedTask; @@ -51,103 +32,9 @@ public RequestDelegate Compile(IServiceProvider services) for (var i = _components.Count - 1; i >= 0; i--) { - (Type Type, object[] Args) component = _components[i]; - var constructors = component.Type.GetConstructors(); - - if (constructors.Length != 1) - { - throw new InvalidOperationException( - "A middleware must have a single public constructor."); - } - - var invokeMethod = TryResolveInvoke(component.Type); - - if (invokeMethod is null) - { - throw new InvalidOperationException( - "A middleware must have a public method InvokeAsync that returns " + - "`Task` and has a single parameter `HttpContext`."); - } - - var next = CompileFactory(constructors[0], invokeMethod, component.Args); - pipeline = next(services, pipeline); - } - - return pipeline; - } - - private Func CompileFactory( - ConstructorInfo constructor, - MethodInfo invokeMethod, - object[] args) - { - var list = new List(); - - foreach (var parameter in constructor.GetParameters()) - { - if (parameter.ParameterType == typeof(RequestDelegate)) - { - list.Add(s_next); - } - else if (parameter.ParameterType == typeof(IServiceProvider)) - { - list.Add(s_services); - } - else if (parameter.Name.EqualsOrdinal("schemaName") && - parameter.ParameterType == typeof(string)) - { - list.Add(s_schemaName); - } - else if (parameter.ParameterType == typeof(MiddlewareRoutingType)) - { - list.Add(s_routing); - } - else - { - var resolved = false; - - if (args.Length > 0) - { - foreach (var arg in args) - { - if (parameter.ParameterType.IsInstanceOfType(arg)) - { - list.Add(Expression.Convert( - Expression.Constant(arg), - parameter.ParameterType)); - resolved = true; - break; - } - } - } - - if (!resolved) - { - Expression parameterType = Expression.Constant(parameter.ParameterType); - list.Add(Expression.Convert( - Expression.Call(s_services, s_getService, parameterType), - parameter.ParameterType)); - } - } + next = _components[i].Invoke(next); } - Expression instance = Expression.New(constructor, list); - Expression invoke = Expression.Constant(invokeMethod); - Expression requestDelegate = Expression.Call(s_compileInvoke, instance, invoke); - - return Expression.Lambda>( - requestDelegate, s_services, s_next) - .Compile(); + return next; } - - private static RequestDelegate CompileInvoke(object obj, MethodInfo invokeMethod) - { - Expression instance = Expression.Constant(obj); - var invoke = Expression.Call(instance, invokeMethod, s_context); - var lambda = Expression.Lambda(invoke, s_context); - return lambda.Compile(); - } - - private MethodInfo? TryResolveInvoke(Type type) - => type.GetMethod("InvokeAsync") ?? type.GetMethod("Invoke"); } diff --git a/src/HotChocolate/Caching/src/Caching/CacheControlConstraintsOptimizer.cs b/src/HotChocolate/Caching/src/Caching/CacheControlConstraintsOptimizer.cs index 7b9d8099840..f4817dd9525 100644 --- a/src/HotChocolate/Caching/src/Caching/CacheControlConstraintsOptimizer.cs +++ b/src/HotChocolate/Caching/src/Caching/CacheControlConstraintsOptimizer.cs @@ -19,7 +19,7 @@ public void OptimizeOperation(OperationOptimizerContext context) context.HasIncrementalParts || ContainsIntrospectionFields(context)) { - // if this is an introspection query we will not cache it. + // if this is an introspection query, we will not cache it. return; } @@ -39,18 +39,18 @@ public void OptimizeOperation(OperationOptimizerContext context) }; context.ContextData.Add( - WellKnownContextData.CacheControlConstraints, + ExecutionContextData.CacheControlConstraints, constraints); context.ContextData.Add( - WellKnownContextData.CacheControlHeaderValue, + ExecutionContextData.CacheControlHeaderValue, headerValue); } if (constraints.Vary is { Length: > 0 }) { context.ContextData.Add( - WellKnownContextData.VaryHeaderValue, + ExecutionContextData.VaryHeaderValue, string.Join(", ", constraints.Vary)); } } diff --git a/src/HotChocolate/Caching/src/Caching/QueryCacheMiddleware.cs b/src/HotChocolate/Caching/src/Caching/QueryCacheMiddleware.cs index 8fdb40d416b..6bc91bc7130 100644 --- a/src/HotChocolate/Caching/src/Caching/QueryCacheMiddleware.cs +++ b/src/HotChocolate/Caching/src/Caching/QueryCacheMiddleware.cs @@ -30,7 +30,7 @@ public async ValueTask InvokeAsync(RequestContext context) } if (!context.TryGetOperation(out var operation) - || !operation.ContextData.TryGetValue(WellKnownContextData.CacheControlHeaderValue, out var value) + || !operation.ContextData.TryGetValue(ExecutionContextData.CacheControlHeaderValue, out var value) || value is not CacheControlHeaderValue cacheControlHeaderValue) { return; @@ -46,13 +46,13 @@ operationResult.ContextData is not null ? new ExtensionData(operationResult.ContextData) : []; - contextData.Add(WellKnownContextData.CacheControlHeaderValue, cacheControlHeaderValue); + contextData.Add(ExecutionContextData.CacheControlHeaderValue, cacheControlHeaderValue); - if (operation.ContextData.TryGetValue(VaryHeaderValue, out var varyValue) + if (operation.ContextData.TryGetValue(ExecutionContextData.VaryHeaderValue, out var varyValue) && varyValue is string varyHeaderValue && !string.IsNullOrEmpty(varyHeaderValue)) { - contextData.Add(VaryHeaderValue, varyHeaderValue); + contextData.Add(ExecutionContextData.VaryHeaderValue, varyHeaderValue); } context.Result = operationResult.WithContextData(contextData); diff --git a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs index 58a8dfe7882..3518e4a173b 100644 --- a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs +++ b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs @@ -15,22 +15,6 @@ public static class WellKnownContextData /// public const string Subscription = "HotChocolate.Execution.Subscription"; - /// - /// The key allows users to override the status code behavior of the default - /// HTTP response formatter. - /// - public const string HttpStatusCode = "HotChocolate.Execution.Transport.HttpStatusCode"; - - /// - /// The key for setting a flag that an operation was not allowed during request execution. - /// - public const string OperationNotAllowed = "HotChocolate.Execution.OperationNotAllowed"; - - /// - /// Includes the query plan into the response. - /// - public const string IncludeQueryPlan = "HotChocolate.Execution.EmitQueryPlan"; - /// /// The key identifies the resolver scope on the local context. /// @@ -51,12 +35,6 @@ public static class WellKnownContextData /// public const string Schema = "HotChocolate.Schema"; - /// - /// The key to get the user provided transport operation session id when executing - /// GraphQL over Websocket. - /// - public const string OperationSessionId = "HotChocolate.Execution.Transport.OperationSessionId"; - /// /// The key to get the deferred task ID on the scoped context data. /// @@ -114,25 +92,10 @@ public static class WellKnownContextData public const string IdValue = "HotChocolate.Relay.Node.Id.Value"; /// - /// The key to get the Cache-Control header value from the context data. - /// - public const string CacheControlHeaderValue = "HotChocolate.Caching.CacheControlHeaderValue"; - - /// - /// The key to get the Vary header value from the context data. - /// - public const string VaryHeaderValue = "HotChocolate.Caching.VaryHeaderValue"; - - /// - /// The key to to ski caching a query result. + /// The key to skip caching a query result. /// public const string SkipQueryCaching = "HotChocolate.Caching.SkipQueryCaching"; - /// - /// The key to retrieve the cache constraints from the operation. - /// - public const string CacheControlConstraints = "HotChocolate.Caching.CacheControlConstraints"; - /// /// Type key to access the paging arguments in the local resolver state. /// @@ -148,28 +111,18 @@ public static class WellKnownContextData /// public const string NodeIdResultFormatter = "HotChocolate.Relay.NodeIdResultFormatter"; - /// - /// The key to set the flag that the cost should be reported in the response. - /// - public const string ReportCost = "HotChocolate.CostAnalysis.ReportCost"; - - /// - /// The key to set the flag that only the cost should be validated and the request should not be executed. - /// - public const string ValidateCost = "HotChocolate.CostAnalysis.ValidateCost"; - /// /// The key to access the paging observers stored on the local resolver state. /// public const string PagingObserver = "HotChocolate.Types.PagingObserver"; /// - /// The key to access the requirements syntax on an object field definition. + /// The key to access the requirement syntax on an object field definition. /// - public const string FieldRequirementsSyntax = "HotChocolate.Types.ObjectField.Requirements.Syntax"; + public const string FieldRequirementSyntax = "HotChocolate.Types.ObjectField.Requirements.Syntax"; /// - /// The key to access the requirements entity type on an object field definition. + /// The key to access the requirement entity type on an object field definition. /// public const string FieldRequirementsEntity = "HotChocolate.Types.ObjectField.Requirements.EntityType"; diff --git a/src/HotChocolate/Core/src/Authorization/Pipeline/AuthorizeRequestMiddleware.cs b/src/HotChocolate/Core/src/Authorization/Pipeline/AuthorizeRequestMiddleware.cs index ea9d23592cc..8bd2879d55e 100644 --- a/src/HotChocolate/Core/src/Authorization/Pipeline/AuthorizeRequestMiddleware.cs +++ b/src/HotChocolate/Core/src/Authorization/Pipeline/AuthorizeRequestMiddleware.cs @@ -48,7 +48,7 @@ public async ValueTask InvokeAsync(RequestContext context) private static IOperationResult CreateErrorResult(AuthorizeResult result) => OperationResultBuilder.New() .AddError(CreateError(result)) - .SetContextData(WellKnownContextData.HttpStatusCode, 401) + .SetContextData(ExecutionContextData.HttpStatusCode, 401) .Build(); private static IError CreateError(AuthorizeResult result) diff --git a/src/HotChocolate/Core/src/Abstractions/Execution/DefaultTimeProvider.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/DefaultTimeProvider.cs similarity index 100% rename from src/HotChocolate/Core/src/Abstractions/Execution/DefaultTimeProvider.cs rename to src/HotChocolate/Core/src/Execution.Abstractions/Execution/DefaultTimeProvider.cs diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IRequestExecutor.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IRequestExecutor.cs index d84361612a5..0ecbd56e3da 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IRequestExecutor.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IRequestExecutor.cs @@ -1,10 +1,12 @@ +using HotChocolate.Features; + namespace HotChocolate.Execution; // Leave it as IRequestExecutor /// /// This executor processes GraphQL query, mutation and subscription requests against an underlying schema. /// -public interface IRequestExecutor +public interface IRequestExecutor : IFeatureProvider { /// /// Gets an representing the version of the executor. diff --git a/src/HotChocolate/Core/src/Abstractions/Execution/ITimeProvider.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/ITimeProvider.cs similarity index 100% rename from src/HotChocolate/Core/src/Abstractions/Execution/ITimeProvider.cs rename to src/HotChocolate/Core/src/Execution.Abstractions/Execution/ITimeProvider.cs diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs index c62e51be020..a2bb7fbdce7 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs @@ -159,7 +159,7 @@ private static void WriteJsonElement( } } - private static void WriteDictionary( + public static void WriteDictionary( Utf8JsonWriter writer, IReadOnlyDictionary dict, JsonSerializerOptions options, diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/RequestExecutorEvent.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/RequestExecutorEvent.cs index 9876b50b349..bd9aacf724e 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/RequestExecutorEvent.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/RequestExecutorEvent.cs @@ -3,4 +3,11 @@ namespace HotChocolate.Execution; public sealed record RequestExecutorEvent( RequestExecutorEventType Type, string Name, - IRequestExecutor Executor); + IRequestExecutor Executor) +{ + public static RequestExecutorEvent Created(IRequestExecutor executor) + => new(RequestExecutorEventType.Created, executor.Schema.Name, executor); + + public static RequestExecutorEvent Evicted(IRequestExecutor executor) + => new(RequestExecutorEventType.Evicted, executor.Schema.Name, executor); +} diff --git a/src/HotChocolate/Core/src/Execution/RequestExecutorEventObserver.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/RequestExecutorEventObserver.cs similarity index 100% rename from src/HotChocolate/Core/src/Execution/RequestExecutorEventObserver.cs rename to src/HotChocolate/Core/src/Execution.Abstractions/Execution/RequestExecutorEventObserver.cs diff --git a/src/HotChocolate/Core/src/Execution/RequestExecutorProxy.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/RequestExecutorProxy.cs similarity index 59% rename from src/HotChocolate/Core/src/Execution/RequestExecutorProxy.cs rename to src/HotChocolate/Core/src/Execution.Abstractions/Execution/RequestExecutorProxy.cs index f3d68cfa660..7484d0b73cf 100644 --- a/src/HotChocolate/Core/src/Execution/RequestExecutorProxy.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/RequestExecutorProxy.cs @@ -1,22 +1,33 @@ +using HotChocolate.Features; + namespace HotChocolate.Execution; /// -/// The is a helper class that represents an executor for -/// one specific schema and handles the resolving and hot-swapping the specific executor. +/// Provides a proxy for managing and executing GraphQL requests against a specific schema. +/// The handles the resolution, caching, and hot-swapping +/// of the underlying instance for a given schema name, +/// ensuring thread-safe access and automatic updates in response to schema changes or evictions. /// -public sealed class RequestExecutorProxy : IDisposable +public class RequestExecutorProxy : IRequestExecutor, IDisposable { private readonly SemaphoreSlim _semaphore = new(1, 1); private readonly IRequestExecutorProvider _executorProvider; private readonly string _schemaName; - private IRequestExecutor? _executor; private readonly IDisposable? _eventSubscription; private bool _disposed; - public event EventHandler? ExecutorUpdated; - - public event EventHandler? ExecutorEvicted; - + /// + /// Initializes a new instance of . + /// + /// + /// The request executor provider. + /// + /// + /// The request executor events. + /// + /// + /// The name of the schema. + /// public RequestExecutorProxy( IRequestExecutorProvider executorProvider, IRequestExecutorEvents executorEvents, @@ -33,7 +44,29 @@ public RequestExecutorProxy( _eventSubscription = executorEvents.Subscribe(observer); } - public IRequestExecutor? CurrentExecutor => _executor; + /// + /// Gets the name of the schema that this executor serves up. + /// + public string SchemaName => _schemaName; + + /// + /// Specifies if this executor serves up the default schema. + /// + public bool IsDefaultSchema => _schemaName.Equals(ISchemaDefinition.DefaultName, StringComparison.Ordinal); + + /// + /// Gets the current request executor. + /// + public IRequestExecutor? CurrentExecutor { get; private set; } + + ulong IRequestExecutor.Version + => CurrentExecutor?.Version ?? 0; + + ISchemaDefinition IRequestExecutor.Schema + => CurrentExecutor?.Schema ?? throw new InvalidOperationException("No schema available yet."); + + IFeatureCollection IFeatureProvider.Features + => CurrentExecutor?.Features ?? throw new InvalidOperationException("No feature collection available yet."); /// /// Executes the given GraphQL . @@ -64,17 +97,10 @@ public async Task ExecuteAsync( CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); + ObjectDisposedException.ThrowIf(_disposed, this); - var executor = - await GetExecutorAsync(cancellationToken) - .ConfigureAwait(false); - - var result = - await executor - .ExecuteAsync(request, cancellationToken) - .ConfigureAwait(false); - - return result; + var executor = CurrentExecutor ?? await GetExecutorAsync(cancellationToken).ConfigureAwait(false); + return await executor.ExecuteAsync(request, cancellationToken).ConfigureAwait(false); } /// @@ -94,17 +120,10 @@ public async Task ExecuteBatchAsync( CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(requestBatch); + ObjectDisposedException.ThrowIf(_disposed, this); - var executor = - await GetExecutorAsync(cancellationToken) - .ConfigureAwait(false); - - var result = - await executor - .ExecuteBatchAsync(requestBatch, cancellationToken) - .ConfigureAwait(false); - - return result; + var executor = CurrentExecutor ?? await GetExecutorAsync(cancellationToken).ConfigureAwait(false); + return await executor.ExecuteBatchAsync(requestBatch, cancellationToken).ConfigureAwait(false); } /// @@ -119,6 +138,8 @@ await executor public async ValueTask GetSchemaAsync( CancellationToken cancellationToken) { + ObjectDisposedException.ThrowIf(_disposed, this); + var executor = await GetExecutorAsync(cancellationToken) .ConfigureAwait(false); @@ -137,7 +158,9 @@ await GetExecutorAsync(cancellationToken) public async ValueTask GetExecutorAsync( CancellationToken cancellationToken) { - var executor = _executor; + ObjectDisposedException.ThrowIf(_disposed, this); + + var executor = CurrentExecutor; if (executor is not null) { @@ -148,21 +171,18 @@ public async ValueTask GetExecutorAsync( try { - if (_executor is null) + if (CurrentExecutor is null) { executor = await _executorProvider .GetExecutorAsync(_schemaName, cancellationToken) .ConfigureAwait(false); - _executor = executor; - - ExecutorUpdated?.Invoke( - this, - new RequestExecutorUpdatedEventArgs(executor)); + CurrentExecutor = executor; + OnRequestExecutorUpdated(executor); } else { - executor = _executor; + executor = CurrentExecutor; } } finally @@ -173,34 +193,39 @@ public async ValueTask GetExecutorAsync( return executor; } - private void OnRequestExecutorEvent(RequestExecutorEvent @event) + protected virtual void OnRequestExecutorUpdated(IRequestExecutor? executor) + { + } + + private void OnRequestExecutorEvent(RequestExecutorEvent eventArgs) { - if (_disposed || !@event.Name.Equals(_schemaName) || _executor is null) + if (_disposed || !eventArgs.Name.Equals(_schemaName) || CurrentExecutor is null) { return; } - if (@event.Type is RequestExecutorEventType.Evicted) + if (eventArgs.Type is RequestExecutorEventType.Evicted) { _semaphore.Wait(); try { - ExecutorEvicted?.Invoke(this, EventArgs.Empty); + CurrentExecutor = null; + OnRequestExecutorUpdated(null); } finally { _semaphore.Release(); } } - else if (@event.Type is RequestExecutorEventType.Created) + else if (eventArgs.Type is RequestExecutorEventType.Created) { _semaphore.Wait(); try { - _executor = @event.Executor; - ExecutorUpdated?.Invoke(this, new RequestExecutorUpdatedEventArgs(@event.Executor)); + CurrentExecutor = eventArgs.Executor; + OnRequestExecutorUpdated(eventArgs.Executor); } finally { @@ -213,7 +238,7 @@ public void Dispose() { if (!_disposed) { - _executor = null; + CurrentExecutor = null; _eventSubscription?.Dispose(); _semaphore.Dispose(); _disposed = true; diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/ExecutionContextData.cs b/src/HotChocolate/Core/src/Execution.Abstractions/ExecutionContextData.cs index 6c1b94f428c..dc6fe4eeb28 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/ExecutionContextData.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/ExecutionContextData.cs @@ -2,6 +2,16 @@ namespace HotChocolate; public static class ExecutionContextData { + /// + /// The key to set the flag that the cost should be reported in the response. + /// + public const string ReportCost = "HotChocolate.CostAnalysis.ReportCost"; + + /// + /// The key to set the flag that only the cost should be validated and the request should not be executed. + /// + public const string ValidateCost = "HotChocolate.CostAnalysis.ValidateCost"; + /// /// The key to determine whether the request is a warmup request. /// @@ -13,6 +23,11 @@ public static class ExecutionContextData /// public const string HttpStatusCode = "HotChocolate.Execution.Transport.HttpStatusCode"; + /// + /// The key for setting a flag that an operation was not allowed during request execution. + /// + public const string OperationNotAllowed = "HotChocolate.Execution.OperationNotAllowed"; + /// /// The key for setting a flag the document was saved to the persisted operation storage. /// @@ -22,4 +37,30 @@ public static class ExecutionContextData /// The key for setting a flag that the execution had document validation errors. /// public const string ValidationErrors = "HotChocolate.Execution.ValidationErrors"; + + /// + /// Includes the query plan in the response. + /// + public const string IncludeQueryPlan = "HotChocolate.Execution.EmitQueryPlan"; + + /// + /// The key to get the user provided transport operation session id when executing + /// GraphQL over Websocket. + /// + public const string OperationSessionId = "HotChocolate.Execution.Transport.OperationSessionId"; + + /// + /// The key to retrieve the cache constraints from the operation. + /// + public const string CacheControlConstraints = "HotChocolate.Caching.CacheControlConstraints"; + + /// + /// The key to get the Cache-Control header value from the context data. + /// + public const string CacheControlHeaderValue = "HotChocolate.Caching.CacheControlHeaderValue"; + + /// + /// The key to get the Vary header value from the context data. + /// + public const string VaryHeaderValue = "HotChocolate.Caching.VaryHeaderValue"; } diff --git a/src/HotChocolate/Core/src/Abstractions/UserState.cs b/src/HotChocolate/Core/src/Execution.Abstractions/UserState.cs similarity index 100% rename from src/HotChocolate/Core/src/Abstractions/UserState.cs rename to src/HotChocolate/Core/src/Execution.Abstractions/UserState.cs diff --git a/src/HotChocolate/Core/src/Execution/AutoUpdateRequestExecutorProxy.cs b/src/HotChocolate/Core/src/Execution/AutoUpdateRequestExecutorProxy.cs deleted file mode 100644 index 09f5ed2002a..00000000000 --- a/src/HotChocolate/Core/src/Execution/AutoUpdateRequestExecutorProxy.cs +++ /dev/null @@ -1,149 +0,0 @@ -namespace HotChocolate.Execution; - -/// -/// The is a helper class that represents a -/// executor for one specific schema and handles the resolving and hot-swapping -/// the specific executor. -/// -public class AutoUpdateRequestExecutorProxy : IRequestExecutor, IDisposable -{ - private readonly SemaphoreSlim _semaphore = new(1, 1); - private readonly RequestExecutorProxy _executorProxy; - private IRequestExecutor _executor; - private bool _disposed; - - private AutoUpdateRequestExecutorProxy( - RequestExecutorProxy requestExecutorProxy, - IRequestExecutor initialExecutor) - { - _executorProxy = requestExecutorProxy; - _executor = initialExecutor; - - _executorProxy.ExecutorUpdated += (_, args) => _executor = args.Executor; - } - - /// - /// The inner executor is exposed for testability. - /// - internal IRequestExecutor InnerExecutor => _executor; - - /// - /// Creates a new auto-update proxy for . - /// - /// - /// The underlying manual proxy. - /// - /// - /// The cancellation token. - /// - /// - /// Returns a new auto-update proxy for . - /// - public static async ValueTask CreateAsync( - RequestExecutorProxy requestExecutorProxy, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(requestExecutorProxy); - - var executor = await requestExecutorProxy - .GetExecutorAsync(cancellationToken) - .ConfigureAwait(false); - - return new AutoUpdateRequestExecutorProxy(requestExecutorProxy, executor); - } - - /// - /// Creates a new auto-update proxy for . - /// - /// - /// The underlying manual proxy. - /// - /// - /// The initial executor instance. - /// - /// - /// Returns a new auto-update proxy for . - /// - public static AutoUpdateRequestExecutorProxy Create( - RequestExecutorProxy requestExecutorProxy, - IRequestExecutor initialExecutor) - { - ArgumentNullException.ThrowIfNull(requestExecutorProxy); - - return new AutoUpdateRequestExecutorProxy(requestExecutorProxy, initialExecutor); - } - - /// - /// Gets the schema to which this executor is bound to. - /// - public ISchemaDefinition Schema => _executor.Schema; - - /// - /// Gets the version of the executor. - /// - public ulong Version => _executor.Version; - - /// - /// Executes the given GraphQL . - /// - /// - /// The GraphQL request object. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the execution result of the given GraphQL . - /// - /// If the request operation is a simple query or mutation the result is a - /// . - /// - /// If the request operation is a query or mutation where data is deferred, streamed or - /// includes live data the result is a where each result - /// that the yields is a . - /// - /// If the request operation is a subscription the result is a - /// where each result that the - /// yields is a - /// . - /// - public Task ExecuteAsync( - IOperationRequest request, - CancellationToken cancellationToken = default) - => _executor.ExecuteAsync(request, cancellationToken); - - /// - /// Executes the given GraphQL . - /// - /// - /// The GraphQL request batch. - /// - /// - /// The cancellation token. - /// - /// - /// Returns a stream of query results. - /// - public Task ExecuteBatchAsync( - OperationRequestBatch requestBatch, - CancellationToken cancellationToken = default) - => _executor.ExecuteBatchAsync(requestBatch, cancellationToken); - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposed && disposing) - { - _executorProxy.Dispose(); - _semaphore.Dispose(); - _executor = null!; - _disposed = true; - } - } -} diff --git a/src/HotChocolate/Core/src/Execution/DefaultRequestExecutor.cs b/src/HotChocolate/Core/src/Execution/DefaultRequestExecutor.cs index 773e8cf3cb4..b786d8a9579 100644 --- a/src/HotChocolate/Core/src/Execution/DefaultRequestExecutor.cs +++ b/src/HotChocolate/Core/src/Execution/DefaultRequestExecutor.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using HotChocolate.Features; using HotChocolate.Fetching; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.ObjectPool; @@ -61,6 +62,12 @@ static void CollectEnricher(IServiceProvider services, List public ulong Version { get; } + /// + /// Get the feature collection used to store additional + /// state for custom executor features. + /// + public IFeatureCollection Features { get; } = new FeatureCollection(); + /// /// Executes a single GraphQL and returns the /// produced by the configured request pipeline. diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Instrumentation.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Instrumentation.cs index beea83229df..b49f7dad36c 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Instrumentation.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Instrumentation.cs @@ -31,12 +31,12 @@ public static IRequestExecutorBuilder AddDiagnosticEventListener( { builder.Services.TryAddSingleton(); - foreach (var attribute in - typeof(T).GetCustomAttributes(typeof(DiagnosticEventSourceAttribute), true)) + builder.ConfigureSchemaServices(static s => { + var attribute = typeof(T).GetCustomAttributes(typeof(DiagnosticEventSourceAttribute), true).First(); var listener = ((DiagnosticEventSourceAttribute)attribute).Listener; - builder.Services.AddSingleton(listener, s => s.GetRequiredService()); - } + s.AddSingleton(listener, sp => sp.GetRootServiceProvider().GetRequiredService()); + }); } else { @@ -68,10 +68,22 @@ public static IRequestExecutorBuilder AddDiagnosticEventListener( } else if (typeof(T).IsDefined(typeof(DiagnosticEventSourceAttribute), true)) { - foreach (var attribute in - typeof(T).GetCustomAttributes(typeof(DiagnosticEventSourceAttribute), true)) + var attribute = + (DiagnosticEventSourceAttribute)typeof(T) + .GetCustomAttributes(typeof(DiagnosticEventSourceAttribute), true) + .First(); + + if (attribute.IsSchemaService) { - var listener = ((DiagnosticEventSourceAttribute)attribute).Listener; + builder.ConfigureSchemaServices(s => + { + var listener = attribute.Listener; + s.AddSingleton(listener, sp => diagnosticEventListener(sp.GetCombinedServices())); + }); + } + else + { + var listener = attribute.Listener; builder.Services.AddSingleton(listener, diagnosticEventListener); } } diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs index 85c07e293e0..f97cf6c6b15 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs @@ -31,7 +31,6 @@ public static IServiceCollection AddGraphQLCore(this IServiceCollection services services.AddOptions(); - services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(sp => sp.GetRequiredService()); @@ -162,6 +161,8 @@ private static DefaultRequestExecutorBuilder CreateBuilder( builder.TryAddTypeInterceptor(_ => new SchemaNameTypeInterceptor(schemaName)); } + builder.ConfigureSchemaServices(static s => s.TryAddSingleton()); + return builder; } diff --git a/src/HotChocolate/Core/src/Execution/ErrorHelper.cs b/src/HotChocolate/Core/src/Execution/ErrorHelper.cs index 9004137bced..97637799f96 100644 --- a/src/HotChocolate/Core/src/Execution/ErrorHelper.cs +++ b/src/HotChocolate/Core/src/Execution/ErrorHelper.cs @@ -128,7 +128,7 @@ public static IOperationResult RootTypeNotFound(OperationType operationType) => .Build(), new Dictionary { - { WellKnownContextData.HttpStatusCode, HttpStatusCode.BadRequest } + { ExecutionContextData.HttpStatusCode, HttpStatusCode.BadRequest } }); public static IOperationResult StateInvalidForOperationResolver() => @@ -169,7 +169,7 @@ public static IOperationResult OperationKindNotAllowed() => .Build(), new Dictionary { - { WellKnownContextData.OperationNotAllowed, null } + { ExecutionContextData.OperationNotAllowed, null } }); public static IOperationResult RequestTypeNotAllowed() => diff --git a/src/HotChocolate/Core/src/Execution/Extensions/ExecutionResultExtensions.cs b/src/HotChocolate/Core/src/Execution/Extensions/ExecutionResultExtensions.cs index 64c141b5ce9..8e0e5c914f2 100644 --- a/src/HotChocolate/Core/src/Execution/Extensions/ExecutionResultExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/Extensions/ExecutionResultExtensions.cs @@ -60,11 +60,11 @@ public static string ToJson( if (withIndentations) { s_formatterIndented.Format(operationResult, writer); - return Encoding.UTF8.GetString(writer.GetWrittenSpan()); + return Encoding.UTF8.GetString(writer.WrittenSpan); } s_formatter.Format(operationResult, writer); - return Encoding.UTF8.GetString(writer.GetWrittenSpan()); + return Encoding.UTF8.GetString(writer.WrittenSpan); } throw new NotSupportedException(ExecutionResultExtensions_OnlyQueryResults); diff --git a/src/HotChocolate/Core/src/Execution/Options/IRequestExecutorOptionsAccessor.cs b/src/HotChocolate/Core/src/Execution/Options/IRequestExecutorOptionsAccessor.cs index 425a13f34c3..6a8287ac806 100644 --- a/src/HotChocolate/Core/src/Execution/Options/IRequestExecutorOptionsAccessor.cs +++ b/src/HotChocolate/Core/src/Execution/Options/IRequestExecutorOptionsAccessor.cs @@ -9,10 +9,4 @@ namespace HotChocolate.Execution.Options; public interface IRequestExecutorOptionsAccessor : IErrorHandlerOptionsAccessor , IRequestTimeoutOptionsAccessor - , IPersistedOperationOptionsAccessor -{ - /// - /// Specifies that the transport is allowed to provide the schema SDL document as a file. - /// - bool EnableSchemaFileSupport { get; } -} + , IPersistedOperationOptionsAccessor; diff --git a/src/HotChocolate/Core/src/Execution/Options/RequestExecutorOptions.cs b/src/HotChocolate/Core/src/Execution/Options/RequestExecutorOptions.cs index 1f23e5ab00a..ef843b4c47e 100644 --- a/src/HotChocolate/Core/src/Execution/Options/RequestExecutorOptions.cs +++ b/src/HotChocolate/Core/src/Execution/Options/RequestExecutorOptions.cs @@ -52,11 +52,6 @@ public TimeSpan ExecutionTimeout /// public bool IncludeExceptionDetails { get; set; } = Debugger.IsAttached; - /// - /// Specifies that the transport is allowed to provide the schema SDL document as a file. - /// - public bool EnableSchemaFileSupport { get; set; } = true; - /// /// Specifies the behavior of the persisted operation pipeline. /// diff --git a/src/HotChocolate/Core/src/Execution/Pipeline/PipelineTools.cs b/src/HotChocolate/Core/src/Execution/Pipeline/PipelineTools.cs index 4596128c378..bca4b16fd16 100644 --- a/src/HotChocolate/Core/src/Execution/Pipeline/PipelineTools.cs +++ b/src/HotChocolate/Core/src/Execution/Pipeline/PipelineTools.cs @@ -33,21 +33,20 @@ public static string CreateCacheId(this RequestContext context) return $"{context.Schema.Name}-{context.ExecutorVersion}-{operationId}"; } - public static IReadOnlyList CoerceVariables( - RequestContext context, + public static void CoerceVariables(RequestContext context, VariableCoercionHelper coercionHelper, IReadOnlyList variableDefinitions, IExecutionDiagnosticEvents diagnosticEvents) { if (context.VariableValues.Length > 0) { - return context.VariableValues; + return; } if (variableDefinitions.Count == 0) { context.VariableValues = s_noVariables; - return s_noVariables; + return; } if (context.Request is OperationRequest operationRequest) @@ -63,7 +62,7 @@ public static IReadOnlyList CoerceVariables( coercedValues); context.VariableValues = [new VariableValueCollection(coercedValues)]; - return context.VariableValues; + return; } } @@ -90,7 +89,7 @@ public static IReadOnlyList CoerceVariables( } context.VariableValues = ImmutableCollectionsMarshal.AsImmutableArray(variableSet); - return context.VariableValues; + return; } } diff --git a/src/HotChocolate/Core/src/Execution/Processing/SubscriptionExecutor.Subscription.cs b/src/HotChocolate/Core/src/Execution/Processing/SubscriptionExecutor.Subscription.cs index 164aeecea1d..7a5c24d650a 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/SubscriptionExecutor.Subscription.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/SubscriptionExecutor.Subscription.cs @@ -302,7 +302,7 @@ await rootSelection.Field.SubscribeResolver! } catch { - // if there is an error we will just dispose our instrumentation scope + // if there is an error, we will just dispose our instrumentation scope // the error is reported in the request level in this case. _subscriptionScope?.Dispose(); _subscriptionScope = null; diff --git a/src/HotChocolate/Core/src/Execution/RequestExecutorUpdatedEventArgs.cs b/src/HotChocolate/Core/src/Execution/RequestExecutorUpdatedEventArgs.cs deleted file mode 100644 index dfce768e7cb..00000000000 --- a/src/HotChocolate/Core/src/Execution/RequestExecutorUpdatedEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace HotChocolate.Execution; - -public sealed class RequestExecutorUpdatedEventArgs : EventArgs -{ - public RequestExecutorUpdatedEventArgs(IRequestExecutor executor) - { - Executor = executor ?? throw new ArgumentNullException(nameof(executor)); - } - - public IRequestExecutor Executor { get; } -} diff --git a/src/HotChocolate/Core/src/Subscriptions/DefaultSourceStream.cs b/src/HotChocolate/Core/src/Subscriptions/DefaultSourceStream.cs index 34abf76db15..83014744a49 100644 --- a/src/HotChocolate/Core/src/Subscriptions/DefaultSourceStream.cs +++ b/src/HotChocolate/Core/src/Subscriptions/DefaultSourceStream.cs @@ -32,7 +32,7 @@ public IAsyncEnumerable ReadEventsAsync() public ValueTask DisposeAsync() { // if the source stream is disposed, we are completing the channel which will trigger - // an unsubscribe from the topic. + // an 'unsubscribe' from the topic. _channel.Writer.TryComplete(); _topic.Unsubscribe(_channel); return default; diff --git a/src/HotChocolate/Core/src/Subscriptions/DependencyInjection/CoreSubscriptionsServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Subscriptions/DependencyInjection/CoreSubscriptionsServiceCollectionExtensions.cs index 23055f2859f..680bf48bf35 100644 --- a/src/HotChocolate/Core/src/Subscriptions/DependencyInjection/CoreSubscriptionsServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Subscriptions/DependencyInjection/CoreSubscriptionsServiceCollectionExtensions.cs @@ -54,7 +54,7 @@ public static IServiceCollection AddSubscriptionDiagnostics( { var listeners = sp.GetService>(); - // if we do not have any listeners registered we will register a noop listener. + // if we do not have any listeners registered, we will register a noop listener. if (listeners is null) { return NoopSubscriptionDiagnosticEventsListener.Default; @@ -63,19 +63,19 @@ public static IServiceCollection AddSubscriptionDiagnostics( var listenerArray = listeners.ToArray(); // we recheck if the array really has listeners; otherwise, - // if we do not have any listeners registered we will register a noop listener. + // if we do not have any listeners registered, we will register a noop listener. if (listenerArray.Length == 0) { return NoopSubscriptionDiagnosticEventsListener.Default; } - // if we only have a single listener we will just return this one. + // if we only have a single listener, we will just return this one. if (listenerArray.Length == 1) { return listenerArray[0]; } - // if we have more than one listener we will return an aggregate listener, + // if we have more than one listener, we will return an aggregate listener, // which will trigger for each event all listeners. return new AggregateSubscriptionDiagnosticEventsListener(listenerArray); }); diff --git a/src/HotChocolate/Core/src/Subscriptions/Diagnostics/ISubscriptionDiagnosticEvents.cs b/src/HotChocolate/Core/src/Subscriptions/Diagnostics/ISubscriptionDiagnosticEvents.cs index 5dbc9a39660..bbb4c72330a 100644 --- a/src/HotChocolate/Core/src/Subscriptions/Diagnostics/ISubscriptionDiagnosticEvents.cs +++ b/src/HotChocolate/Core/src/Subscriptions/Diagnostics/ISubscriptionDiagnosticEvents.cs @@ -16,7 +16,7 @@ public interface ISubscriptionDiagnosticEvents void Created(string topicName); /// - /// The event topic has connected to the pub/sub system and is now ready to receive + /// The event topic has connected to the pub-/sub-system and is now ready to receive /// event messages. /// /// @@ -25,7 +25,7 @@ public interface ISubscriptionDiagnosticEvents void Connected(string topicName); /// - /// The topic was closed and has disconnected from the pub/sub system. + /// The topic was closed and has disconnected from the pub-/sub-system. /// /// /// The name of the topic. @@ -33,7 +33,7 @@ public interface ISubscriptionDiagnosticEvents void Disconnected(string topicName); /// - /// An error occurred during message processing or while connecting to the pub/sub system. + /// An error occurred during message processing or while connecting to the pub-/sub-system. /// /// /// The name of the topic. @@ -44,7 +44,7 @@ public interface ISubscriptionDiagnosticEvents void MessageProcessingError(string topicName, Exception error); /// - /// Received a message from the pub/sub system. + /// Received a message from the pub-/sub-system. /// /// /// The name of the topic. @@ -87,7 +87,7 @@ public interface ISubscriptionDiagnosticEvents /// The name of the topic. /// /// - /// The subscribe attempt count. + /// The attempt counts to subscribe. /// void TrySubscribe(string topicName, int attempt); @@ -108,7 +108,7 @@ public interface ISubscriptionDiagnosticEvents void SubscribeFailed(string topicName); /// - /// The GraphQL execution engine has unsubscribed form the topic. + /// The GraphQL execution engine has unsubscribed from the topic. /// /// /// The name of the topic. @@ -117,7 +117,7 @@ public interface ISubscriptionDiagnosticEvents /// The shard to which the subscribers belonged to. /// /// - /// The amount of subscribers that have unsubscribed. + /// The number of subscribers that have unsubscribed. /// void Unsubscribe(string topicName, int shard, int subscribers); @@ -130,7 +130,7 @@ public interface ISubscriptionDiagnosticEvents void Close(string topicName); /// - /// An event message was send to the pub/sub system. (outgoing message) + /// An event message was sent to the pub-/sub-system. (outgoing message) /// /// /// The name of the topic. @@ -144,7 +144,7 @@ public interface ISubscriptionDiagnosticEvents void Send(string topicName, T message); /// - /// Provider specific information. + /// Provider-specific information. /// /// /// The information text. @@ -152,7 +152,7 @@ public interface ISubscriptionDiagnosticEvents void ProviderInfo(string infoText); /// - /// Provider specific information with a topic context. + /// Provider-specific information with a topic context. /// /// /// The name of the topic. diff --git a/src/HotChocolate/Core/src/Subscriptions/Diagnostics/SubscriptionDiagnosticEventsListener.cs b/src/HotChocolate/Core/src/Subscriptions/Diagnostics/SubscriptionDiagnosticEventsListener.cs index d6af508d7a3..b1d035270c3 100644 --- a/src/HotChocolate/Core/src/Subscriptions/Diagnostics/SubscriptionDiagnosticEventsListener.cs +++ b/src/HotChocolate/Core/src/Subscriptions/Diagnostics/SubscriptionDiagnosticEventsListener.cs @@ -3,7 +3,7 @@ namespace HotChocolate.Subscriptions.Diagnostics; /// /// Inherit from this listener to implement a diagnostic listener for subscriptions. /// -[DiagnosticEventSource(typeof(ISubscriptionDiagnosticEventsListener))] +[DiagnosticEventSource(typeof(ISubscriptionDiagnosticEventsListener), isSchemaService: false)] public class SubscriptionDiagnosticEventsListener : ISubscriptionDiagnosticEventsListener { /// diff --git a/src/HotChocolate/Core/src/Types.Abstractions/Serialization/SchemaDebugFormatter.cs b/src/HotChocolate/Core/src/Types.Abstractions/Serialization/SchemaDebugFormatter.cs index 1805843d309..1782e4f248d 100644 --- a/src/HotChocolate/Core/src/Types.Abstractions/Serialization/SchemaDebugFormatter.cs +++ b/src/HotChocolate/Core/src/Types.Abstractions/Serialization/SchemaDebugFormatter.cs @@ -14,7 +14,7 @@ type.Description is null : new StringValueNode(type.Description), type.Directives.Select(Format).ToArray(), type.Implements.Select(FormatTypeRef).Cast().ToArray(), - type.Fields.Select(Format).ToArray()); + type.Fields.Where(t => !t.IsIntrospectionField).Select(Format).ToArray()); public static InterfaceTypeDefinitionNode Format(IInterfaceTypeDefinition type) => new InterfaceTypeDefinitionNode( @@ -78,7 +78,21 @@ directiveDefinition.Description is null directiveDefinition.Locations.AsEnumerable().Select(Format).ToArray()); public static FieldDefinitionNode Format(IOutputFieldDefinition field) - => new FieldDefinitionNode( + { + var directives = field.Directives.Select(Format).ToList(); + + if (field.IsDeprecated) + { + var deprecatedDirective = new DirectiveNode( + DirectiveNames.Deprecated.Name, + new ArgumentNode( + DirectiveNames.Deprecated.Arguments.Reason, + field.DeprecationReason ?? DirectiveNames.Deprecated.Arguments.DefaultReason)); + + directives.Insert(0, deprecatedDirective); + } + + return new FieldDefinitionNode( null, new NameNode(field.Name), field.Description is null @@ -86,10 +100,25 @@ field.Description is null : new StringValueNode(field.Description), field.Arguments.Select(Format).ToArray(), FormatTypeRef(field.Type), - field.Directives.Select(Format).ToArray()); + directives); + } public static InputValueDefinitionNode Format(IInputValueDefinition field) - => new InputValueDefinitionNode( + { + var directives = field.Directives.Select(Format).ToList(); + + if (field.IsDeprecated) + { + var deprecatedDirective = new DirectiveNode( + DirectiveNames.Deprecated.Name, + new ArgumentNode( + DirectiveNames.Deprecated.Arguments.Reason, + field.DeprecationReason ?? DirectiveNames.Deprecated.Arguments.DefaultReason)); + + directives.Insert(0, deprecatedDirective); + } + + return new InputValueDefinitionNode( null, new NameNode(field.Name), field.Description is null @@ -97,16 +126,32 @@ field.Description is null : new StringValueNode(field.Description), FormatTypeRef(field.Type), field.DefaultValue, - field.Directives.Select(Format).ToArray()); + directives); + } public static EnumValueDefinitionNode Format(IEnumValue value) - => new EnumValueDefinitionNode( + { + var directives = value.Directives.Select(Format).ToList(); + + if (value.IsDeprecated) + { + var deprecatedDirective = new DirectiveNode( + DirectiveNames.Deprecated.Name, + new ArgumentNode( + DirectiveNames.Deprecated.Arguments.Reason, + value.DeprecationReason ?? DirectiveNames.Deprecated.Arguments.DefaultReason)); + + directives.Insert(0, deprecatedDirective); + } + + return new EnumValueDefinitionNode( null, new NameNode(value.Name), value.Description is null ? null : new StringValueNode(value.Description), - value.Directives.Select(Format).ToArray()); + directives); + } public static DirectiveNode Format(IDirective directive) => new DirectiveNode( diff --git a/src/HotChocolate/Core/src/Types/SchemaErrorBuilder.Error.cs b/src/HotChocolate/Core/src/Types/SchemaErrorBuilder.Error.cs index 07b72e057ee..e14d25080e7 100644 --- a/src/HotChocolate/Core/src/Types/SchemaErrorBuilder.Error.cs +++ b/src/HotChocolate/Core/src/Types/SchemaErrorBuilder.Error.cs @@ -55,7 +55,7 @@ public override unsafe string ToString() writer.Flush(); - fixed (byte* b = buffer.GetInternalBuffer()) + fixed (byte* b = PooledArrayWriterMarshal.GetUnderlyingBuffer(buffer)) { return Encoding.UTF8.GetString(b, buffer.Length); } diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/JsonType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/JsonType.cs index ea4a4a125bc..211871caacd 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/JsonType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/JsonType.cs @@ -1,4 +1,5 @@ #nullable enable + using System.Buffers; using System.Text.Json; using HotChocolate.Buffers; @@ -181,7 +182,7 @@ public static JsonElement Format(IValueNode node) s_visitor.Visit(node, new JsonFormatterContext(jsonWriter)); jsonWriter.Flush(); - var jsonReader = new Utf8JsonReader(bufferWriter.GetWrittenSpan()); + var jsonReader = new Utf8JsonReader(bufferWriter.WrittenSpan); return JsonElement.ParseValue(ref jsonReader); } diff --git a/src/HotChocolate/Core/test/Authorization.Tests/AnnotationBasedAuthorizationTests.cs b/src/HotChocolate/Core/test/Authorization.Tests/AnnotationBasedAuthorizationTests.cs index 079fb0def45..b354313fc9c 100644 --- a/src/HotChocolate/Core/test/Authorization.Tests/AnnotationBasedAuthorizationTests.cs +++ b/src/HotChocolate/Core/test/Authorization.Tests/AnnotationBasedAuthorizationTests.cs @@ -6,7 +6,7 @@ using HotChocolate.Types.Relay; using HotChocolate.Utilities; using Microsoft.Extensions.DependencyInjection; -using static HotChocolate.WellKnownContextData; +using static HotChocolate.ExecutionContextData; namespace HotChocolate.Authorization; diff --git a/src/HotChocolate/Core/test/Authorization.Tests/CodeFirstAuthorizationTests.cs b/src/HotChocolate/Core/test/Authorization.Tests/CodeFirstAuthorizationTests.cs index e35a779cedb..8be865a93d1 100644 --- a/src/HotChocolate/Core/test/Authorization.Tests/CodeFirstAuthorizationTests.cs +++ b/src/HotChocolate/Core/test/Authorization.Tests/CodeFirstAuthorizationTests.cs @@ -3,7 +3,6 @@ using HotChocolate.Types; using HotChocolate.Utilities; using Microsoft.Extensions.DependencyInjection; -using static HotChocolate.WellKnownContextData; namespace HotChocolate.Authorization; @@ -85,7 +84,7 @@ public async Task Authorize_Query_NoAccess() """); Assert.NotNull(result.ContextData); - Assert.True(result.ContextData!.TryGetValue(HttpStatusCode, out var value)); + Assert.True(result.ContextData!.TryGetValue(ExecutionContextData.HttpStatusCode, out var value)); Assert.Equal(401, value); } @@ -268,7 +267,7 @@ public async Task Authorize_Field_Auth_Not_Allowed_On_Validation() """); Assert.NotNull(result.ContextData); - Assert.True(result.ContextData!.TryGetValue(HttpStatusCode, out var value)); + Assert.True(result.ContextData!.TryGetValue(ExecutionContextData.HttpStatusCode, out var value)); Assert.Equal(401, value); } @@ -322,7 +321,7 @@ public async Task Authorize_Schema_Field() """); Assert.NotNull(result.ContextData); - Assert.True(result.ContextData!.TryGetValue(HttpStatusCode, out var value)); + Assert.True(result.ContextData!.TryGetValue(ExecutionContextData.HttpStatusCode, out var value)); Assert.Equal(401, value); } @@ -376,7 +375,7 @@ public async Task Authorize_Type_Field() """); Assert.NotNull(result.ContextData); - Assert.True(result.ContextData!.TryGetValue(HttpStatusCode, out var value)); + Assert.True(result.ContextData!.TryGetValue(ExecutionContextData.HttpStatusCode, out var value)); Assert.Equal(401, value); } @@ -430,7 +429,7 @@ public async Task Authorize_Node_Field() """); Assert.NotNull(result.ContextData); - Assert.True(result.ContextData!.TryGetValue(HttpStatusCode, out var value)); + Assert.True(result.ContextData!.TryGetValue(ExecutionContextData.HttpStatusCode, out var value)); Assert.Equal(401, value); } @@ -484,7 +483,7 @@ public async Task Authorize_Nodes_Field() """); Assert.NotNull(result.ContextData); - Assert.True(result.ContextData!.TryGetValue(HttpStatusCode, out var value)); + Assert.True(result.ContextData!.TryGetValue(ExecutionContextData.HttpStatusCode, out var value)); Assert.Equal(401, value); } diff --git a/src/HotChocolate/Core/test/Execution.Tests/AutoUpdateRequestExecutorProxyTests.cs b/src/HotChocolate/Core/test/Execution.Tests/AutoUpdateRequestExecutorProxyTests.cs deleted file mode 100644 index 5b838462d62..00000000000 --- a/src/HotChocolate/Core/test/Execution.Tests/AutoUpdateRequestExecutorProxyTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -using HotChocolate.StarWars; -using Microsoft.Extensions.DependencyInjection; - -namespace HotChocolate.Execution; - -public class AutoUpdateRequestExecutorProxyTests -{ - [Fact] - public async Task Ensure_Executor_Is_Cached() - { - // arrange - var resolver = - new ServiceCollection() - .AddGraphQL() - .AddStarWarsRepositories() - .AddStarWarsTypes() - .Services - .BuildServiceProvider() - .GetRequiredService(); - - var innerProxy = new RequestExecutorProxy(resolver, resolver, ISchemaDefinition.DefaultName); - - // act - var proxy = await AutoUpdateRequestExecutorProxy.CreateAsync(innerProxy); - var a = proxy.InnerExecutor; - var b = proxy.InnerExecutor; - - // assert - Assert.Same(a, b); - } - - [Fact] - public async Task Ensure_Executor_Is_Correctly_Swapped_When_Evicted() - { - // arrange - var executorUpdatedResetEvent = new ManualResetEventSlim(false); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - var resolver = - new ServiceCollection() - .AddGraphQL() - .AddStarWarsRepositories() - .AddStarWarsTypes() - .Services - .BuildServiceProvider() - .GetRequiredService(); - var evicted = false; - var updated = false; - - var innerProxy = new RequestExecutorProxy(resolver, resolver, ISchemaDefinition.DefaultName); - - var proxy = await AutoUpdateRequestExecutorProxy.CreateAsync(innerProxy, cts.Token); - innerProxy.ExecutorEvicted += (_, _) => - { - evicted = true; - executorUpdatedResetEvent.Set(); - }; - innerProxy.ExecutorUpdated += (_, _) => updated = true; - - // act - var a = proxy.InnerExecutor; - resolver.EvictRequestExecutor(); - executorUpdatedResetEvent.Wait(cts.Token); - - var b = proxy.InnerExecutor; - - // assert - Assert.NotSame(a, b); - Assert.True(evicted); - Assert.True(updated); - } - - [Fact] - public async Task Ensure_Manual_Proxy_Is_Not_Null() - { - // arrange - // act - async Task Action() => await AutoUpdateRequestExecutorProxy.CreateAsync(null!); - - // assert - await Assert.ThrowsAsync(Action); - } -} diff --git a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorProxyTests.cs b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorProxyTests.cs index af3644a4831..f2508f1befc 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorProxyTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorProxyTests.cs @@ -44,13 +44,13 @@ public async Task Ensure_Executor_Is_Correctly_Swapped_When_Evicted() var evicted = false; var updated = false; - var proxy = new RequestExecutorProxy(resolver, resolver, ISchemaDefinition.DefaultName); - proxy.ExecutorEvicted += (_, _) => + var proxy = new TestProxy(resolver, resolver, ISchemaDefinition.DefaultName); + proxy.ExecutorEvicted += () => { evicted = true; executorUpdatedResetEvent.Set(); }; - proxy.ExecutorUpdated += (_, _) => updated = true; + proxy.ExecutorUpdated += () => updated = true; // act var a = await proxy.GetExecutorAsync(CancellationToken.None); @@ -63,4 +63,26 @@ public async Task Ensure_Executor_Is_Correctly_Swapped_When_Evicted() Assert.True(evicted); Assert.True(updated); } + + private class TestProxy( + IRequestExecutorProvider executorProvider, + IRequestExecutorEvents executorEvents, + string schemaName) + : RequestExecutorProxy(executorProvider, executorEvents, schemaName) + { + public event Action? ExecutorEvicted; + public event Action? ExecutorUpdated; + + protected override void OnRequestExecutorUpdated(IRequestExecutor? executor) + { + if (executor is null) + { + ExecutorEvicted?.Invoke(); + } + else + { + ExecutorUpdated?.Invoke(); + } + } + } } diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerOperationRequestBuilderExtensions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerOperationRequestBuilderExtensions.cs index 36ab8e8e2e7..9c2fdef1ce7 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerOperationRequestBuilderExtensions.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerOperationRequestBuilderExtensions.cs @@ -9,15 +9,15 @@ public static OperationRequestBuilder ReportCost(this OperationRequestBuilder bu { ArgumentNullException.ThrowIfNull(builder); - builder.RemoveGlobalState(WellKnownContextData.ValidateCost); - return builder.AddGlobalState(WellKnownContextData.ReportCost, true); + builder.RemoveGlobalState(ExecutionContextData.ValidateCost); + return builder.AddGlobalState(ExecutionContextData.ReportCost, true); } public static OperationRequestBuilder ValidateCost(this OperationRequestBuilder builder) { ArgumentNullException.ThrowIfNull(builder); - builder.RemoveGlobalState(WellKnownContextData.ReportCost); - return builder.AddGlobalState(WellKnownContextData.ValidateCost, true); + builder.RemoveGlobalState(ExecutionContextData.ReportCost); + return builder.AddGlobalState(ExecutionContextData.ValidateCost, true); } } diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerRequestContextExtensions.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerRequestContextExtensions.cs index a6a0ee126e0..4aecfedb151 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerRequestContextExtensions.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Extensions/CostAnalyzerRequestContextExtensions.cs @@ -56,7 +56,7 @@ internal static CostAnalyzerMode GetCostAnalyzerMode( return CostAnalyzerMode.Skip; } - if (context.ContextData.ContainsKey(WellKnownContextData.ValidateCost)) + if (context.ContextData.ContainsKey(ExecutionContextData.ValidateCost)) { return CostAnalyzerMode.Analyze | CostAnalyzerMode.Report; } @@ -70,7 +70,7 @@ internal static CostAnalyzerMode GetCostAnalyzerMode( flags |= CostAnalyzerMode.Execute; - if (context.ContextData.ContainsKey(WellKnownContextData.ReportCost)) + if (context.ContextData.ContainsKey(ExecutionContextData.ReportCost)) { flags |= CostAnalyzerMode.Report; } diff --git a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Utilities/ResultHelper.cs b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Utilities/ResultHelper.cs index aeec3b14000..053de6395e4 100644 --- a/src/HotChocolate/CostAnalysis/src/CostAnalysis/Utilities/ResultHelper.cs +++ b/src/HotChocolate/CostAnalysis/src/CostAnalysis/Utilities/ResultHelper.cs @@ -10,7 +10,7 @@ internal static class ResultHelper .Add(ExecutionContextData.ValidationErrors, true); private static readonly ImmutableDictionary s_ok = ImmutableDictionary.Empty - .Add(WellKnownContextData.HttpStatusCode, 200); + .Add(ExecutionContextData.HttpStatusCode, 200); public static IExecutionResult CreateError(IError error, CostMetrics? costMetrics) { diff --git a/src/HotChocolate/Directory.Build.props b/src/HotChocolate/Directory.Build.props index 9b07268e83b..802d963c68c 100644 --- a/src/HotChocolate/Directory.Build.props +++ b/src/HotChocolate/Directory.Build.props @@ -4,6 +4,6 @@ hotchocolate-signet.png - 28.0.7 + 29.0.0-insider.21 diff --git a/src/HotChocolate/Fusion-vnext/HotChocolate.Fusion-vnext.slnx b/src/HotChocolate/Fusion-vnext/HotChocolate.Fusion-vnext.slnx index c1e12100dbd..4fe59617e65 100644 --- a/src/HotChocolate/Fusion-vnext/HotChocolate.Fusion-vnext.slnx +++ b/src/HotChocolate/Fusion-vnext/HotChocolate.Fusion-vnext.slnx @@ -1,5 +1,6 @@ + @@ -13,5 +14,6 @@ + diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.AspNetCore/DependencyInjection/FusionServerServiceCollectionExtensions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.AspNetCore/DependencyInjection/FusionServerServiceCollectionExtensions.cs new file mode 100644 index 00000000000..9ae03390f3b --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.AspNetCore/DependencyInjection/FusionServerServiceCollectionExtensions.cs @@ -0,0 +1,99 @@ +using HotChocolate.AspNetCore; +using HotChocolate.AspNetCore.Formatters; +using HotChocolate.AspNetCore.Instrumentation; +using HotChocolate.AspNetCore.Parsers; +using HotChocolate.AspNetCore.Subscriptions.Protocols; +using HotChocolate.AspNetCore.Subscriptions.Protocols.Apollo; +using HotChocolate.AspNetCore.Subscriptions.Protocols.GraphQLOverWebSocket; +using HotChocolate.Execution; +using HotChocolate.Fusion.AspNetCore; +using HotChocolate.Fusion.Configuration; +using HotChocolate.Language; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class FusionServerServiceCollectionExtensions +{ + public static IFusionGatewayBuilder AddGraphQLGatewayServer( + this IServiceCollection services, + string? name = null, + int maxAllowedRequestSize = ServerDefaults.MaxAllowedRequestSize) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentOutOfRangeException.ThrowIfNegative(maxAllowedRequestSize); + + return services + .AddGraphQLGateway(name) + .AddGraphQLGatewayServerCore() + .AddDefaultHttpRequestInterceptor() + .AddSubscriptionServices(); + } + + private static IFusionGatewayBuilder AddGraphQLGatewayServerCore( + this IFusionGatewayBuilder builder, + int maxAllowedRequestSize = ServerDefaults.MaxAllowedRequestSize) + { + builder.ConfigureSchemaServices((_, sc) => + { + sc.TryAddSingleton(); + + sc.TryAddSingleton( + sp => DefaultHttpResponseFormatter.Create( + new HttpResponseFormatterOptions { HttpTransportVersion = HttpTransportVersion.Latest }, + sp.GetRequiredService())); + + sc.TryAddSingleton( + sp => new DefaultHttpRequestParser( + sp.GetRequiredService(), + sp.GetRequiredService(), + maxAllowedRequestSize, + sp.GetRequiredService())); + + sc.TryAddSingleton(sp => + { + var listeners = sp.GetServices().ToArray(); + return listeners.Length switch + { + 0 => new NoopServerDiagnosticEventListener(), + 1 => listeners[0], + _ => new AggregateServerDiagnosticEventListener(listeners) + }; + }); + }); + + return builder; + } + + private static IFusionGatewayBuilder AddDefaultHttpRequestInterceptor( + this IFusionGatewayBuilder builder) + => builder.ConfigureSchemaServices( + (_, s) => s.TryAddSingleton()); + + private static IFusionGatewayBuilder AddSubscriptionServices( + this IFusionGatewayBuilder builder) + => builder + .ConfigureSchemaServices((_, s) => + { + s.TryAddSingleton(); + s.TryAddSingleton(_ => new DefaultWebSocketPayloadFormatter()); + }) + .AddApolloProtocol() + .AddGraphQLOverWebSocketProtocol(); + + private static IFusionGatewayBuilder AddApolloProtocol( + this IFusionGatewayBuilder builder) + => builder.ConfigureSchemaServices( + (_, s) => s.AddSingleton( + sp => new ApolloSubscriptionProtocolHandler( + sp.GetRequiredService(), + sp.GetRequiredService()))); + + private static IFusionGatewayBuilder AddGraphQLOverWebSocketProtocol( + this IFusionGatewayBuilder builder) + => builder.ConfigureSchemaServices( + (_, s) => s.AddSingleton( + sp => new GraphQLOverWebSocketProtocolHandler( + sp.GetRequiredService(), + sp.GetRequiredService()))); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.AspNetCore/HotChocolate.Fusion.AspNetCore.csproj b/src/HotChocolate/Fusion-vnext/src/Fusion.AspNetCore/HotChocolate.Fusion.AspNetCore.csproj new file mode 100644 index 00000000000..8891a95bb29 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.AspNetCore/HotChocolate.Fusion.AspNetCore.csproj @@ -0,0 +1,28 @@ + + + + HotChocolate.Fusion.AspNetCore + HotChocolate.Fusion.AspNetCore + + + + + + + + + + ResXFileCodeGenerator + FusionUtilitiesResources.Designer.cs + + + + + + True + True + FusionUtilitiesResources.resx + + + + diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.AspNetCore/ServerDefaults.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.AspNetCore/ServerDefaults.cs new file mode 100644 index 00000000000..d8e5cfffc67 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.AspNetCore/ServerDefaults.cs @@ -0,0 +1,6 @@ +namespace HotChocolate.Fusion.AspNetCore; + +internal static class ServerDefaults +{ + public const int MaxAllowedRequestSize = 20 * 1000 * 1024; +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Configuration/FileSystemFusionConfigurationProvider.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Configuration/FileSystemFusionConfigurationProvider.cs index 495373addcb..cccb9ec41cb 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Configuration/FileSystemFusionConfigurationProvider.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Configuration/FileSystemFusionConfigurationProvider.cs @@ -142,13 +142,13 @@ private async Task LoadSchemaDocumentAsync(CancellationToken cancellationToken) await pipeReader.CompleteAsync().ConfigureAwait(false); - var hash = XxHash64.HashToUInt64(buffer.GetWrittenSpan()); + var hash = XxHash64.HashToUInt64(buffer.WrittenSpan); if (_schemaDocumentHash != hash) { _schemaDocumentHash = hash; - var schemaDocument = Utf8GraphQLParser.Parse(buffer.GetWrittenSpan()); + var schemaDocument = Utf8GraphQLParser.Parse(buffer.WrittenSpan); SchemaDocument = schemaDocument; NotifyObservers(schemaDocument); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutor.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutor.cs index e267adf4141..5d49eaddba4 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutor.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutor.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; using HotChocolate.Execution; +using HotChocolate.Features; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.ObjectPool; @@ -41,6 +42,12 @@ public FusionRequestExecutor( /// public ulong Version { get; } + /// + /// Get the feature collection used to store additional + /// state for custom executor features. + /// + public IFeatureCollection Features { get; } = new FeatureCollection(); + /// /// Executes a single GraphQL and returns the /// produced by the configured request pipeline. diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs index ff4753efc9b..45b18a423f9 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Collections.Immutable; using System.IO.Hashing; using System.Text; using System.Threading.Channels; @@ -28,10 +29,20 @@ internal sealed class FusionRequestExecutorManager , IRequestExecutorEvents , IAsyncDisposable { + private readonly object _lock = new(); private readonly SemaphoreSlim _semaphore = new(1, 1); private readonly ConcurrentDictionary _registry = []; private readonly IOptionsMonitor _optionsMonitor; private readonly IServiceProvider _applicationServices; + private readonly Channel _executorEvents = + Channel.CreateBounded(new BoundedChannelOptions(1) + { + FullMode = BoundedChannelFullMode.Wait, + SingleReader = true, + SingleWriter = false + }); + private ImmutableArray _observers = ImmutableArray.Empty; + private bool _disposed; public FusionRequestExecutorManager( @@ -43,6 +54,8 @@ public FusionRequestExecutorManager( _optionsMonitor = optionsMonitor; _applicationServices = applicationServices; + + NotifyObserversAsync().FireAndForget(); } public ValueTask GetExecutorAsync( @@ -50,18 +63,15 @@ public ValueTask GetExecutorAsync( CancellationToken cancellationToken = default) { schemaName ??= ISchemaDefinition.DefaultName; - - if (_registry.TryGetValue(schemaName, out var registration)) - { - return new ValueTask(registration.Executor); - } - - return GetOrCreateRequestExecutorAsync(schemaName, cancellationToken); + return _registry.TryGetValue(schemaName, out var registration) + ? new ValueTask(registration.Executor) + : GetOrCreateRequestExecutorAsync(schemaName, cancellationToken); } public IDisposable Subscribe(IObserver observer) { - throw new NotImplementedException(); + ArgumentNullException.ThrowIfNull(observer); + return new ObserverSession(this, observer); } private async ValueTask GetOrCreateRequestExecutorAsync( @@ -79,6 +89,7 @@ private async ValueTask GetOrCreateRequestExecutorAsync( registration = await CreateInitialRegistrationAsync(schemaName, cancellationToken).ConfigureAwait(false); _registry.TryAdd(schemaName, registration); + await _executorEvents.WriteCreatedAsync(registration.Executor, cancellationToken).ConfigureAwait(false); return registration.Executor; } finally @@ -391,6 +402,17 @@ private RequestDelegate CreatePipeline( return next; } + private async Task NotifyObserversAsync() + { + await foreach (var eventArgs in _executorEvents.Reader.ReadAllAsync().ConfigureAwait(false)) + { + foreach (var observer in _observers) + { + observer.OnNext(eventArgs); + } + } + } + public async ValueTask DisposeAsync() { if (_disposed) @@ -400,10 +422,21 @@ public async ValueTask DisposeAsync() _disposed = true; + _executorEvents.Writer.TryComplete(new Exception("Completed")); + foreach (var registration in _registry.Values) { await registration.DisposeAsync().ConfigureAwait(false); } + + while(_executorEvents.Reader.TryRead(out _)) { } + + foreach (var session in _observers) + { + session.OnCompleted(); + } + + _observers = ImmutableArray.Empty; } private sealed class RequestExecutorAccessor @@ -492,8 +525,75 @@ public async ValueTask DisposeAsync() await DocumentProvider.DisposeAsync().ConfigureAwait(false); _channel.Writer.TryComplete(); - _cancellationTokenSource.Cancel(); + await _cancellationTokenSource.CancelAsync().ConfigureAwait(false); _cancellationTokenSource.Dispose(); } } + + private sealed class ObserverSession : IDisposable + { + private readonly FusionRequestExecutorManager _manager; + private readonly IObserver _observer; + private bool _disposed; + + public ObserverSession( + FusionRequestExecutorManager manager, + IObserver observer) + { + _manager = manager; + _observer = observer; + + lock (_manager._lock) + { + _manager._observers = _manager._observers.Add(this); + } + } + + public void OnNext(RequestExecutorEvent value) + { + if (_disposed) + { + return; + } + + _observer.OnNext(value); + } + + public void OnCompleted() + { + if (_disposed) + { + return; + } + + _observer.OnCompleted(); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + lock (_manager._lock) + { + _manager._observers = _manager._observers.Remove(this); + } + + _disposed = true; + } + } +} + +file static class Extensions +{ + public static async ValueTask WriteCreatedAsync( + this Channel executorEvents, + FusionRequestExecutor executor, + CancellationToken cancellationToken) + { + var eventArgs = RequestExecutorEvent.Created(executor); + await executorEvents.Writer.WriteAsync(eventArgs, cancellationToken).ConfigureAwait(false); + } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationVariableCoercionMiddleware.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationVariableCoercionMiddleware.cs index fe2b9e7519e..30b39e8dc81 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationVariableCoercionMiddleware.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationVariableCoercionMiddleware.cs @@ -41,7 +41,7 @@ public ValueTask InvokeAsync( : default; } - public static bool TryCoerceVariables( + private static bool TryCoerceVariables( RequestContext context, IReadOnlyList variableDefinitions, ICoreExecutionDiagnosticEvents diagnosticEvents) @@ -71,11 +71,9 @@ public static bool TryCoerceVariables( context.VariableValues = [new VariableValueCollection(coercedValues)]; return true; } - else - { - context.Result = OperationResultBuilder.CreateError(error); - return false; - } + + context.Result = OperationResultBuilder.CreateError(error); + return false; } } @@ -83,7 +81,6 @@ public static bool TryCoerceVariables( { using (diagnosticEvents.CoerceVariables(context)) { - var schema = context.Schema; var variableSetCount = variableBatchRequest.VariableValues?.Count ?? 0; var variableSetInput = variableBatchRequest.VariableValues!; var variableSet = new IVariableValueCollection[variableSetCount]; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/VariableCoercionHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/VariableCoercionHelper.cs index 38d9d0669ae..fd849ee9777 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/VariableCoercionHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/VariableCoercionHelper.cs @@ -42,7 +42,7 @@ public static bool TryCoerceVariableValues( throw ExceptionHelper.NonNullVariableIsNull(variableDefinition); } - // if we do not have any value we will not create an entry to the + // if we do not have any value, we will not create an entry to the // coerced variables. if (!hasValue) { @@ -71,7 +71,7 @@ public static bool TryCoerceVariableValues( else { throw new NotSupportedException( - $"The variable value of type {value?.GetType().Name} is not supported."); + $"The variable value of type {value.GetType().Name} is not supported."); } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/Extensions/ServiceCollectionExtensions.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000000..cc7feb43636 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddHttpClient( + this IServiceCollection services, + string name, + TestServer server) + { + services.TryAddSingleton(); + return services.AddSingleton(new TestServerRegistration(name, server)); + } + + private class Factory : IHttpClientFactory + { + private readonly Dictionary _registrations; + + public Factory(IEnumerable registrations) + { + _registrations = registrations.ToDictionary(r => r.Name, r => r); + } + + public HttpClient CreateClient(string name) + { + if (_registrations.TryGetValue(name, out var registration)) + { + return registration.Server.CreateClient(); + } + + throw new InvalidOperationException( + $"No test server registered with the name: {name}"); + } + } + + private record TestServerRegistration(string Name, TestServer Server); +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/FusionTestBase.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/FusionTestBase.cs new file mode 100644 index 00000000000..92807430b5d --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/FusionTestBase.cs @@ -0,0 +1,190 @@ +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Execution; +using HotChocolate.Execution.Configuration; +using HotChocolate.Fusion.Configuration; +using HotChocolate.Fusion.Execution; +using HotChocolate.Fusion.Execution.Nodes; +using HotChocolate.Fusion.Logging; +using HotChocolate.Fusion.Planning; +using HotChocolate.Fusion.Rewriters; +using HotChocolate.Fusion.Types; +using HotChocolate.Language; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ObjectPool; + +namespace HotChocolate.Fusion; + +public abstract class FusionTestBase : IDisposable +{ + private readonly TestServerSession _testServerSession = new(); + private bool _disposed; + + protected static FusionSchemaDefinition CreateCompositeSchema() + { + var compositeSchemaDoc = Utf8GraphQLParser.Parse(FileResource.Open("fusion1.graphql")); + return FusionSchemaDefinition.Create(compositeSchemaDoc); + } + + protected static FusionSchemaDefinition CreateCompositeSchema( + [StringSyntax("graphql")] string schema) + { + var compositeSchemaDoc = Utf8GraphQLParser.Parse(schema); + return FusionSchemaDefinition.Create(compositeSchemaDoc); + } + + protected static FusionSchemaDefinition ComposeSchema( + [StringSyntax("graphql")] params string[] schemas) + { + var compositionLog = new CompositionLog(); + var composer = new SchemaComposer(schemas, compositionLog); + var result = composer.Compose(); + + if (!result.IsSuccess) + { + throw new InvalidOperationException(result.Errors[0].Message); + } + + var compositeSchemaDoc = result.Value.ToSyntaxNode(); + return FusionSchemaDefinition.Create(compositeSchemaDoc); + } + + protected static DocumentNode ComposeSchemaDocument( + [StringSyntax("graphql")] params string[] schemas) + { + var compositionLog = new CompositionLog(); + var composer = new SchemaComposer(schemas, compositionLog); + var result = composer.Compose(); + + if (!result.IsSuccess) + { + throw new InvalidOperationException(result.Errors[0].Message); + } + + return result.Value.ToSyntaxNode(); + } + + public TestServer CreateSourceSchema( + string schemaName, + Action configureBuilder, + Action? configureServices = null, + Action? configureApplication = null) + { + configureApplication ??= + app => + { + app.UseWebSockets(); + app.UseRouting(); + app.UseEndpoints(endpoint => endpoint.MapGraphQL(schemaName: schemaName)); + }; + + return _testServerSession.CreateServer( + services => + { + services.AddRouting(); + var builder = services.AddGraphQLServer(schemaName); + configureBuilder(builder); + configureServices?.Invoke(services); + }, + configureApplication); + } + + public async Task CreateCompositeSchemaAsync( + (string SchemaName, TestServer Server)[] sourceSchemaServers, + Action? configureServices = null, + Action? configureApplication = null) + { + var sourceSchemas = new List(); + var gatewayServices = new ServiceCollection(); + var gatewayBuilder = gatewayServices.AddGraphQLGatewayServer(); + + foreach (var (name, server) in sourceSchemaServers) + { + var schemaDocument = await server.Services.GetSchemaAsync(name); + sourceSchemas.Add(schemaDocument.ToString()); + gatewayServices.AddHttpClient(name, server); + gatewayBuilder.AddHttpClientConfiguration(name, new Uri("http://localhost:5000/graphql")); + } + + var compositionLog = new CompositionLog(); + var composer = new SchemaComposer(sourceSchemas, compositionLog); + var result = composer.Compose(); + + if (!result.IsSuccess) + { + throw new InvalidOperationException(result.Errors[0].Message); + } + + gatewayBuilder.AddInMemoryConfiguration(result.Value.ToSyntaxNode()); + + configureApplication ??= + app => + { + app.UseWebSockets(); + app.UseRouting(); + app.UseEndpoints(endpoint => endpoint.MapGraphQL()); + }; + + return _testServerSession.CreateServer( + services => + { + services.AddRouting(); + + foreach (var serviceDescriptor in gatewayServices) + { + services.Add(serviceDescriptor); + } + + configureServices?.Invoke(services); + }, + configureApplication); + } + + protected static OperationExecutionPlan PlanOperation( + FusionSchemaDefinition schema, + [StringSyntax("graphql")] string operationText) + { + var pool = new DefaultObjectPool>>( + new DefaultPooledObjectPolicy>>()); + + var operationDoc = Utf8GraphQLParser.Parse(operationText); + + var rewriter = new InlineFragmentOperationRewriter(schema); + var rewritten = rewriter.RewriteDocument(operationDoc, operationName: null); + var operation = rewritten.Definitions.OfType().First(); + + var compiler = new OperationCompiler(schema, pool); + var planner = new OperationPlanner(schema, compiler); + return planner.CreatePlan("123", operation); + } + + protected static void MatchInline( + OperationExecutionPlan plan, + [StringSyntax("yaml")] string expected) + { + var formatter = new YamlExecutionPlanFormatter(); + var actual = formatter.Format(plan); + actual.MatchInlineSnapshot(expected + Environment.NewLine); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _testServerSession.Dispose(); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj new file mode 100644 index 00000000000..7750318df6b --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj @@ -0,0 +1,30 @@ + + + + + HotChocolate.Fusion.AspNetCore.Tests + HotChocolate.Fusion + + + + + + + + + + + + + + + + + Always + + + Always + + + + diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/IntegrationTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/IntegrationTests.cs new file mode 100644 index 00000000000..8dd83064192 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/IntegrationTests.cs @@ -0,0 +1,54 @@ +using System.Text; +using HotChocolate.Buffers; +using HotChocolate.Types; +using HotChocolate.Execution; +using HotChocolate.Transport.Formatters; +using HotChocolate.Transport.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace HotChocolate.Fusion; + +public class IntegrationTests : FusionTestBase +{ + [Fact] + public async Task Foo() + { + // arrange + using var server1 = CreateSourceSchema( + "A", + b => b.AddQueryType( + d => d.Name("Query") + .Field("foo") + .Resolve("foo")) ); + + using var server2 = CreateSourceSchema( + "B", + b => b.AddQueryType( + d => d.Name("Query") + .Field("bar") + .Resolve("bar"))); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", server1), + ("B", server2), + ]); + + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + using var result = await client.PostAsync( + """ + { + foo + bar + } + """, + new Uri("http://localhost:5000/graphql")); + + // act + using var response = await result.ReadAsResultAsync(); + response.MatchSnapshot(); + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/TestServerSession.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/TestServerSession.cs new file mode 100644 index 00000000000..4c17a23c69f --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/TestServerSession.cs @@ -0,0 +1,50 @@ +using System.Threading.Channels; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Fusion; + +internal sealed class TestServerSession : IDisposable +{ + private readonly Channel _cleanupPipeline = Channel.CreateUnbounded(); + private bool _disposed; + + public TestServer CreateServer( + Action configureServices, + Action configureApplication) + { + var builder = new WebHostBuilder() + .Configure(configureApplication) + .ConfigureServices(configureServices); + + var server = new TestServer(builder); + + if (!_cleanupPipeline.Writer.TryWrite(server)) + { + server.Dispose(); + throw new InvalidOperationException( + "Failed to add test server to cleanup pipeline."); + } + + return server; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + _cleanupPipeline.Writer.TryComplete(); + + while (_cleanupPipeline.Reader.TryRead(out var server)) + { + server.Dispose(); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntegrationTests.Foo.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntegrationTests.Foo.snap new file mode 100644 index 00000000000..41d5c23e17b --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntegrationTests.Foo.snap @@ -0,0 +1,6 @@ +{ + "data": { + "foo": "foo", + "bar": "bar" + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/IntegrationTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/IntegrationTests.cs index 78d2a14f4cc..e4f0cb8553c 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/IntegrationTests.cs @@ -57,7 +57,7 @@ public async Task Foo() using var buffer = new PooledArrayWriter(); JsonResultFormatter.Indented.Format(result.ExpectOperationResult(), buffer); - Encoding.UTF8.GetString(buffer.GetWrittenSpan()).MatchSnapshot(); + Encoding.UTF8.GetString(buffer.WrittenSpan).MatchSnapshot(); } } diff --git a/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj b/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj index 91448624930..2acd7961110 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj +++ b/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/HotChocolate/PersistedOperations/src/PersistedOperations.FileSystem/FileSystemOperationDocumentStorage.cs b/src/HotChocolate/PersistedOperations/src/PersistedOperations.FileSystem/FileSystemOperationDocumentStorage.cs index 358a87a745b..be86f5d1a94 100644 --- a/src/HotChocolate/PersistedOperations/src/PersistedOperations.FileSystem/FileSystemOperationDocumentStorage.cs +++ b/src/HotChocolate/PersistedOperations/src/PersistedOperations.FileSystem/FileSystemOperationDocumentStorage.cs @@ -1,5 +1,5 @@ +using HotChocolate.Buffers; using HotChocolate.Execution; -using HotChocolate.Utilities; using HotChocolate.Language; namespace HotChocolate.PersistedOperations.FileSystem; @@ -46,18 +46,19 @@ public FileSystemOperationDocumentStorage(IOperationDocumentFileMap documentMap) string filePath, CancellationToken cancellationToken) { + const int chunkSize = 4096; await using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + using var writer = new PooledArrayWriter(); + var read = 0; - var document = await BufferHelper.ReadAsync( - stream, - static (buffer, buffered) => - { - var span = buffer.AsSpan()[..buffered]; - return Utf8GraphQLParser.Parse(span); - }, - cancellationToken) - .ConfigureAwait(false); + do + { + var memory = writer.GetMemory(chunkSize); + read = await stream.ReadAsync(memory, cancellationToken).ConfigureAwait(false); + writer.Advance(read); + } while (read == chunkSize); + var document = Utf8GraphQLParser.Parse(writer.WrittenSpan); return new OperationDocument(document); } diff --git a/src/HotChocolate/Core/src/Abstractions/DiagnosticEventSourceAttribute.cs b/src/HotChocolate/Primitives/src/Primitives/DiagnosticEventSourceAttribute.cs similarity index 60% rename from src/HotChocolate/Core/src/Abstractions/DiagnosticEventSourceAttribute.cs rename to src/HotChocolate/Primitives/src/Primitives/DiagnosticEventSourceAttribute.cs index fbb07657d7c..434b410ec6c 100644 --- a/src/HotChocolate/Core/src/Abstractions/DiagnosticEventSourceAttribute.cs +++ b/src/HotChocolate/Primitives/src/Primitives/DiagnosticEventSourceAttribute.cs @@ -14,13 +14,24 @@ public class DiagnosticEventSourceAttribute : Attribute /// /// The listener interface. /// - public DiagnosticEventSourceAttribute(Type listener) + /// + /// Defines if this listener is a schema service. + /// + public DiagnosticEventSourceAttribute(Type listener, bool isSchemaService = true) { + ArgumentNullException.ThrowIfNull(listener, nameof(listener)); + Listener = listener; + IsSchemaService = isSchemaService; } /// /// Gets the listener interface. /// public Type Listener { get; } + + /// + /// Gets a value indicating whether this listener is a schema service. + /// + public bool IsSchemaService { get; } } diff --git a/src/HotChocolate/Core/src/Types.Scalars.Upload/FileValueNode.cs b/src/HotChocolate/Primitives/src/Primitives/Types/FileValueNode.cs similarity index 100% rename from src/HotChocolate/Core/src/Types.Scalars.Upload/FileValueNode.cs rename to src/HotChocolate/Primitives/src/Primitives/Types/FileValueNode.cs diff --git a/src/HotChocolate/Core/src/Types.Scalars.Upload/IFile.cs b/src/HotChocolate/Primitives/src/Primitives/Types/IFile.cs similarity index 100% rename from src/HotChocolate/Core/src/Types.Scalars.Upload/IFile.cs rename to src/HotChocolate/Primitives/src/Primitives/Types/IFile.cs diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/JsonDocumentOwner.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/JsonDocumentOwner.cs new file mode 100644 index 00000000000..a5940fd7f7c --- /dev/null +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/JsonDocumentOwner.cs @@ -0,0 +1,51 @@ +using System.Buffers; +using System.Text.Json; + +namespace HotChocolate.Buffers; + +/// +/// A that owns the memory it was created with. +/// This helper holds both the and the memory the document was parsed from. +/// +public sealed class JsonDocumentOwner : IDisposable +{ + private readonly IMemoryOwner _memory; + private bool _disposed; + + /// + /// Initializes a new instance of . + /// + /// + /// The to own. + /// + /// + /// The memory that was used to create the . + /// + public JsonDocumentOwner(JsonDocument document, IMemoryOwner memory) + { + ArgumentNullException.ThrowIfNull(document); + ArgumentNullException.ThrowIfNull(memory); + + Document = document; + _memory = memory; + } + + /// + /// Gets the that is owned by this instance. + /// + public JsonDocument Document { get; } + + /// + /// Disposes the and the memory the document was parsed from. + /// + public void Dispose() + { + if (!_disposed) + { + Document.Dispose(); + _memory.Dispose(); + + _disposed = true; + } + } +} diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/PooledArrayWriter.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/PooledArrayWriter.cs index 320ee2c4d67..971619c7f87 100644 --- a/src/HotChocolate/Utilities/src/Utilities.Buffers/PooledArrayWriter.cs +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/PooledArrayWriter.cs @@ -7,7 +7,7 @@ namespace HotChocolate.Buffers; /// /// A that writes to a rented buffer. /// -public sealed class PooledArrayWriter : IBufferWriter, IDisposable +public sealed class PooledArrayWriter : IBufferWriter, IMemoryOwner { private const int InitialBufferSize = 512; private byte[] _buffer; @@ -43,9 +43,9 @@ public PooledArrayWriter() /// /// /// Accessing the underlying buffer directly is not recommended. - /// If possible use or . + /// If possible use or . /// - public byte[] GetInternalBuffer() => _buffer; + internal byte[] GetInternalBuffer() => _buffer; /// /// Gets the part of the buffer that has been written to. @@ -53,16 +53,19 @@ public PooledArrayWriter() /// /// A of the written portion of the buffer. /// - public ReadOnlyMemory GetWrittenMemory() + public ReadOnlyMemory WrittenMemory => _buffer.AsMemory()[.._start]; + Memory IMemoryOwner.Memory + => _buffer.AsMemory(0, _start); + /// /// Gets the part of the buffer that has been written to. /// /// /// A of the written portion of the buffer. /// - public ReadOnlySpan GetWrittenSpan() + public ReadOnlySpan WrittenSpan => MemoryMarshal.CreateSpan(ref _buffer[0], _start); /// diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/PooledArrayWriterMarshal.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/PooledArrayWriterMarshal.cs new file mode 100644 index 00000000000..c6b6da07ecd --- /dev/null +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/PooledArrayWriterMarshal.cs @@ -0,0 +1,22 @@ +namespace HotChocolate.Buffers; + +/// +/// A helper class to marshal the underlying buffer of a . +/// +public static class PooledArrayWriterMarshal +{ + /// + /// Gets the underlying buffer of a . + /// + /// + /// The to get the underlying buffer from. + /// + /// + /// The underlying buffer of the . + /// + public static byte[] GetUnderlyingBuffer(PooledArrayWriter writer) + { + ArgumentNullException.ThrowIfNull(writer); + return writer.GetInternalBuffer(); + } +} diff --git a/src/HotChocolate/Utilities/src/Utilities/BufferHelper.cs b/src/HotChocolate/Utilities/src/Utilities/BufferHelper.cs deleted file mode 100644 index 90e889b701f..00000000000 --- a/src/HotChocolate/Utilities/src/Utilities/BufferHelper.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Buffers; - -namespace HotChocolate.Utilities; - -public static class BufferHelper -{ - public static Task ReadAsync( - Stream stream, - Func deserialize, - CancellationToken cancellationToken) - => ReadAsync(stream, deserialize, static (b, l, d) => d(b, l), cancellationToken); - - public static Task ReadAsync( - Stream stream, - TState state, - Func deserialize, - CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(stream); - ArgumentNullException.ThrowIfNull(deserialize); - - return ReadAsync(stream, state, int.MaxValue, deserialize, () => { }, cancellationToken); - } - - public static async Task ReadAsync( - Stream stream, - TState state, - int maxAllowedSize, - Func deserialize, - Action throwMaxAllowedError, - CancellationToken cancellationToken) - { - var buffer = ArrayPool.Shared.Rent(1024); - var bytesBuffered = 0; - - try - { - while (true) - { - var bytesRemaining = buffer.Length - bytesBuffered; - - if (bytesRemaining == 0) - { - var next = ArrayPool.Shared.Rent(buffer.Length * 2); - Buffer.BlockCopy(buffer, 0, next, 0, buffer.Length); - ArrayPool.Shared.Return(buffer); - buffer = next; - bytesRemaining = buffer.Length - bytesBuffered; - } - - var bytesRead = await stream.ReadAsync( - buffer, - bytesBuffered, - bytesRemaining, - cancellationToken) - .ConfigureAwait(false); - if (bytesRead == 0) - { - break; - } - - bytesBuffered += bytesRead; - - if (bytesBuffered > maxAllowedSize) - { - throwMaxAllowedError(); - } - } - - return deserialize(buffer, bytesBuffered, state); - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } -} diff --git a/src/HotChocolate/Utilities/test/Utilities.Tests/PooledArrayWriterTests.cs b/src/HotChocolate/Utilities/test/Utilities.Tests/PooledArrayWriterTests.cs index e49f1feafb0..8d8d7c88424 100644 --- a/src/HotChocolate/Utilities/test/Utilities.Tests/PooledArrayWriterTests.cs +++ b/src/HotChocolate/Utilities/test/Utilities.Tests/PooledArrayWriterTests.cs @@ -11,7 +11,7 @@ public void Constructor_ShouldInitializeProperly() using var writer = new PooledArrayWriter(); // Assert - Assert.NotNull(writer.GetInternalBuffer()); + Assert.NotNull(PooledArrayWriterMarshal.GetUnderlyingBuffer(writer)); Assert.Equal(0, writer.Length); } @@ -22,7 +22,7 @@ public void GetWrittenMemory_ShouldReturnReadOnlyMemory() using var writer = new PooledArrayWriter(); // Act - var memory = writer.GetWrittenMemory(); + var memory = writer.WrittenMemory; // Assert Assert.Equal(0, memory.Length); @@ -35,7 +35,7 @@ public void GetWrittenSpan_ShouldReturnReadOnlySpan() using var writer = new PooledArrayWriter(); // Act - var span = writer.GetWrittenSpan(); + var span = writer.WrittenSpan; // Assert Assert.Equal(0, span.Length); @@ -125,7 +125,7 @@ public void Advance_ShouldThrowWhenCountGreaterThanCapacity() // Act & Assert Assert.Throws( - () => writer.Advance(writer.GetInternalBuffer().Length + 1)); + () => writer.Advance(PooledArrayWriterMarshal.GetUnderlyingBuffer(writer).Length + 1)); } [Fact] @@ -184,7 +184,7 @@ public void WriteBytesToSpan_ShouldWriteCorrectly() // Assert Assert.Equal(4, writer.Length); - var writtenSpan = writer.GetWrittenSpan(); + var writtenSpan = writer.WrittenSpan; Assert.True(testData.SequenceEqual(writtenSpan.ToArray())); } @@ -202,7 +202,7 @@ public void WriteBytesToMemory_ShouldWriteCorrectly() // Assert Assert.Equal(4, writer.Length); - var writtenMemory = writer.GetWrittenMemory(); + var writtenMemory = writer.WrittenMemory; Assert.True(testData.SequenceEqual(writtenMemory.ToArray())); } @@ -228,7 +228,7 @@ public void WriteBytesExceedingInitialBufferSize_ShouldExpandAndWriteCorrectly() // Assert Assert.Equal(1024, writer.Length); - var writtenSpan = writer.GetWrittenSpan(); + var writtenSpan = writer.WrittenSpan; Assert.True(testData.SequenceEqual(writtenSpan.ToArray())); } @@ -239,8 +239,8 @@ public void ShouldAllocateSufficientMemory() using var writer = new PooledArrayWriter(); // Act - // NB: ask for 0x3000 bytes because the initial 512 bytes buffer size is added - // to request when doubling is insufficient and ArrayPool sizes are powers of 2 + // NB: ask for 0x3000 bytes because the initial 512-byte buffer size is added + // to request when doubling is not enough and ArrayPool sizes are powers of 2 writer.GetSpan(0x3000); writer.Advance(0x2000); writer.GetSpan(0x7000); diff --git a/src/StrawberryShake/Client/src/Transport.Http/ResponseEnumerable.cs b/src/StrawberryShake/Client/src/Transport.Http/ResponseEnumerable.cs index ee687ff9596..4bc9017a87e 100644 --- a/src/StrawberryShake/Client/src/Transport.Http/ResponseEnumerable.cs +++ b/src/StrawberryShake/Client/src/Transport.Http/ResponseEnumerable.cs @@ -112,7 +112,7 @@ public async IAsyncEnumerator> GetAsyncEnumerator( writer.Flush(); - return JsonDocument.Parse(buffer.GetWrittenMemory()); + return JsonDocument.Parse(buffer.WrittenMemory); } private static void WriteProperty(Utf8JsonWriter writer, string propertyName, JsonElement value) diff --git a/src/StrawberryShake/Client/test/Transport.InMemory.Tests/InMemoryClientTests.cs b/src/StrawberryShake/Client/test/Transport.InMemory.Tests/InMemoryClientTests.cs index a7d69bb931d..2cf817fd23a 100644 --- a/src/StrawberryShake/Client/test/Transport.InMemory.Tests/InMemoryClientTests.cs +++ b/src/StrawberryShake/Client/test/Transport.InMemory.Tests/InMemoryClientTests.cs @@ -130,6 +130,8 @@ private sealed class StubExecutor : IRequestExecutor public ulong Version { get; } + public IFeatureCollection Features { get; } = new FeatureCollection(); + public Task ExecuteAsync( IOperationRequest request, CancellationToken cancellationToken = default) diff --git a/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/IntegrationTests.cs b/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/IntegrationTests.cs index 4d21bb678a9..a0adf2ca7c3 100644 --- a/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/IntegrationTests.cs +++ b/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/IntegrationTests.cs @@ -295,7 +295,7 @@ public override ValueTask OnConnectAsync( IOperationMessagePayload connectionInitMessage, CancellationToken cancellationToken = default) { - InitializeConnectionMessage = connectionInitMessage.As>(); + InitializeConnectionMessage = connectionInitMessage.Payload?.Deserialize>(); return base.OnConnectAsync(session, connectionInitMessage, cancellationToken); } diff --git a/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/TestHelper/TestServerHelper.cs b/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/TestHelper/TestServerHelper.cs index 5c1dcaedb93..143d9d304d1 100644 --- a/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/TestHelper/TestServerHelper.cs +++ b/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/TestHelper/TestServerHelper.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using static HotChocolate.WellKnownContextData; namespace StrawberryShake.Transport.WebSockets; @@ -60,7 +59,7 @@ value is HttpContext httpContext && context.Result = result = OperationResultBuilder .FromResult(result) - .SetContextData(HttpStatusCode, 403) + .SetContextData(ExecutionContextData.HttpStatusCode, 403) .Build(); }