From 92b04dab9f6ebcf81379613d18bbfff7f80949e5 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Thu, 14 Apr 2022 13:42:42 -0400 Subject: [PATCH 01/47] Update middleware with options and new design --- .../GraphQLHttpMiddlewareWithLogs.cs | 61 +- samples/Samples.Server/Startup.cs | 4 +- samples/Samples.Server/StartupWithRouting.cs | 3 +- .../AuthorizationHelper.cs | 66 ++ .../AuthorizationParameters.cs | 68 ++ .../Errors/AccessDeniedError.cs | 38 + .../BatchedRequestsNotSupportedError.cs | 12 + .../Errors/HttpMethodValidationError.cs | 19 + .../Errors/InvalidContentTypeError.cs | 15 + .../Errors/JsonInvalidError.cs | 15 + .../Errors/QueryMissingError.cs | 12 + .../Errors/RequestError.cs | 35 + .../WebSocketSubProtocolNotSupportedError.cs | 15 + .../GraphQLBuilderMiddlewareExtensions.cs | 29 - .../GraphQLBuilderUserContextExtensions.cs | 144 +-- ...GraphQLHttpApplicationBuilderExtensions.cs | 141 +-- ...aphQLHttpEndpointRouteBuilderExtensions.cs | 105 ++- .../GraphQLHttpMiddleware.cs | 845 ++++++++++++------ .../GraphQLHttpMiddlewareOptions.cs | 107 +++ .../HttpGetValidationRule.cs | 23 + .../HttpPostValidationRule.cs | 23 + .../IUserContextBuilder.cs | 2 +- .../UserContextBuilder.cs | 46 +- ....Server.Transports.AspNetCore.approved.txt | 179 +++- tests/Samples.Server.Tests/ResponseTests.cs | 32 +- 25 files changed, 1473 insertions(+), 566 deletions(-) create mode 100644 src/Transports.AspNetCore/AuthorizationHelper.cs create mode 100644 src/Transports.AspNetCore/AuthorizationParameters.cs create mode 100644 src/Transports.AspNetCore/Errors/AccessDeniedError.cs create mode 100644 src/Transports.AspNetCore/Errors/BatchedRequestsNotSupportedError.cs create mode 100644 src/Transports.AspNetCore/Errors/HttpMethodValidationError.cs create mode 100644 src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs create mode 100644 src/Transports.AspNetCore/Errors/JsonInvalidError.cs create mode 100644 src/Transports.AspNetCore/Errors/QueryMissingError.cs create mode 100644 src/Transports.AspNetCore/Errors/RequestError.cs create mode 100644 src/Transports.AspNetCore/Errors/WebSocketSubProtocolNotSupportedError.cs delete mode 100644 src/Transports.AspNetCore/Extensions/GraphQLBuilderMiddlewareExtensions.cs create mode 100644 src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs create mode 100644 src/Transports.AspNetCore/HttpGetValidationRule.cs create mode 100644 src/Transports.AspNetCore/HttpPostValidationRule.cs diff --git a/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs b/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs index 5b8565da..1ee61ada 100644 --- a/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs +++ b/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs @@ -1,42 +1,41 @@ +#nullable enable + +using System.Diagnostics; using GraphQL.Server.Transports.AspNetCore; +using GraphQL.Transport; using GraphQL.Types; -namespace GraphQL.Samples.Server +namespace GraphQL.Samples.Server; + +// Example of a custom GraphQL Middleware that sends execution result to Microsoft.Extensions.Logging API +public class GraphQLHttpMiddlewareWithLogs : GraphQLHttpMiddleware + where TSchema : ISchema { - // Example of a custom GraphQL Middleware that sends execution result to Microsoft.Extensions.Logging API - public class GraphQLHttpMiddlewareWithLogs : GraphQLHttpMiddleware - where TSchema : ISchema - { - private readonly ILogger _logger; + private readonly ILogger _logger; - public GraphQLHttpMiddlewareWithLogs( - ILogger> logger, - IGraphQLTextSerializer requestDeserializer) - : base(requestDeserializer) - { - _logger = logger; - } + public GraphQLHttpMiddlewareWithLogs( + RequestDelegate next, + IGraphQLTextSerializer serializer, + IDocumentExecuter documentExecuter, + IServiceScopeFactory serviceScopeFactory, + GraphQLHttpMiddlewareOptions options, + ILogger> logger) + : base(next, serializer, documentExecuter, serviceScopeFactory, options) + { + _logger = logger; + } - protected override Task RequestExecutedAsync(in GraphQLRequestExecutionResult requestExecutionResult) + protected override async Task ExecuteRequestAsync(HttpContext context, GraphQLRequest request, IServiceProvider serviceProvider, IDictionary userContext) + { + var timer = Stopwatch.StartNew(); + var ret = await base.ExecuteRequestAsync(context, request, serviceProvider, userContext); + if (ret.Errors != null) { - if (requestExecutionResult.Result.Errors != null) - { - if (requestExecutionResult.IndexInBatch.HasValue) - _logger.LogError("GraphQL execution completed in {Elapsed} with error(s) in batch [{Index}]: {Errors}", requestExecutionResult.Elapsed, requestExecutionResult.IndexInBatch, requestExecutionResult.Result.Errors); - else - _logger.LogError("GraphQL execution completed in {Elapsed} with error(s): {Errors}", requestExecutionResult.Elapsed, requestExecutionResult.Result.Errors); - } - else - _logger.LogInformation("GraphQL execution successfully completed in {Elapsed}", requestExecutionResult.Elapsed); - - return base.RequestExecutedAsync(requestExecutionResult); + _logger.LogError("GraphQL execution completed in {Elapsed} with error(s): {Errors}", timer.Elapsed, ret.Errors); } + else + _logger.LogInformation("GraphQL execution successfully completed in {Elapsed}", timer.Elapsed); - protected override CancellationToken GetCancellationToken(HttpContext context) - { - // custom CancellationToken example - var cts = CancellationTokenSource.CreateLinkedTokenSource(base.GetCancellationToken(context), new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token); - return cts.Token; - } + return ret; } } diff --git a/samples/Samples.Server/Startup.cs b/samples/Samples.Server/Startup.cs index 4f4da614..f0fe2850 100644 --- a/samples/Samples.Server/Startup.cs +++ b/samples/Samples.Server/Startup.cs @@ -5,6 +5,7 @@ using GraphQL.Samples.Schemas.Chat; using GraphQL.Server; using GraphQL.Server.Authorization.AspNetCore; +using GraphQL.Server.Transports.AspNetCore; using GraphQL.Server.Ui.Altair; using GraphQL.Server.Ui.GraphiQL; using GraphQL.Server.Ui.Playground; @@ -36,7 +37,6 @@ public void ConfigureServices(IServiceCollection services) services.AddGraphQL(builder => builder .AddMetrics() .AddDocumentExecuter() - .AddHttpMiddleware>() .AddWebSocketsHttpMiddleware() .AddSchema() .ConfigureExecutionOptions(options => @@ -65,7 +65,7 @@ public void Configure(IApplicationBuilder app) app.UseWebSockets(); app.UseGraphQLWebSockets(); - app.UseGraphQL>(); + app.UseGraphQL>("/graphql", new GraphQLHttpMiddlewareOptions()); app.UseGraphQLPlayground(new PlaygroundOptions { diff --git a/samples/Samples.Server/StartupWithRouting.cs b/samples/Samples.Server/StartupWithRouting.cs index d50ef957..b0fb68d8 100644 --- a/samples/Samples.Server/StartupWithRouting.cs +++ b/samples/Samples.Server/StartupWithRouting.cs @@ -37,7 +37,6 @@ public void ConfigureServices(IServiceCollection services) services.AddGraphQL(builder => builder .AddMetrics() .AddDocumentExecuter() - .AddHttpMiddleware>() .AddWebSocketsHttpMiddleware() .AddSchema() .ConfigureExecutionOptions(options => @@ -71,7 +70,7 @@ public void Configure(IApplicationBuilder app) app.UseEndpoints(endpoints => { endpoints.MapGraphQLWebSockets(); - endpoints.MapGraphQL>(); + endpoints.MapGraphQL>(); endpoints.MapGraphQLPlayground(new PlaygroundOptions { diff --git a/src/Transports.AspNetCore/AuthorizationHelper.cs b/src/Transports.AspNetCore/AuthorizationHelper.cs new file mode 100644 index 00000000..96464dc3 --- /dev/null +++ b/src/Transports.AspNetCore/AuthorizationHelper.cs @@ -0,0 +1,66 @@ +#nullable enable + +using System.Security.Claims; +using System.Security.Principal; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; + +namespace GraphQL.Server.Transports.AspNetCore; + +/// +/// Helper methods for performing connection authorization. +/// +public static class AuthorizationHelper +{ + /// + /// Performs connection authorization according to the options set within + /// . Returns + /// if authorization was successful or not required. + /// + public static async ValueTask AuthorizeAsync(AuthorizationParameters options, TState state) + { + if (options.AuthorizationRequired) + { + if (!((options.HttpContext.User ?? NoUser()).Identity ?? NoIdentity()).IsAuthenticated) + { + if (options.OnNotAuthenticated != null) + await options.OnNotAuthenticated(state); + return false; + } + } + + if (options.AuthorizedRoles?.Count > 0) + { + var user = options.HttpContext.User ?? NoUser(); + foreach (var role in options.AuthorizedRoles) + { + if (user.IsInRole(role)) + goto PassRoleCheck; + } + if (options.OnNotAuthorizedRole != null) + await options.OnNotAuthorizedRole(state); + return false; + } + PassRoleCheck: + + if (options.AuthorizedPolicy != null) + { + var authorizationService = options.HttpContext.RequestServices.GetRequiredService(); + var authResult = await authorizationService.AuthorizeAsync(options.HttpContext.User ?? NoUser(), null, options.AuthorizedPolicy); + if (!authResult.Succeeded) + { + if (options.OnNotAuthorizedPolicy != null) + await options.OnNotAuthorizedPolicy(state, authResult); + return false; + } + } + + return true; + } + + private static IIdentity NoIdentity() + => throw new InvalidOperationException($"IIdentity could not be retrieved from HttpContext.User.Identity."); + + private static ClaimsPrincipal NoUser() + => throw new InvalidOperationException("ClaimsPrincipal could not be retrieved from HttpContext.User."); +} diff --git a/src/Transports.AspNetCore/AuthorizationParameters.cs b/src/Transports.AspNetCore/AuthorizationParameters.cs new file mode 100644 index 00000000..83f87f1c --- /dev/null +++ b/src/Transports.AspNetCore/AuthorizationParameters.cs @@ -0,0 +1,68 @@ +#nullable enable + +using System.Security.Claims; +using System.Security.Principal; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace GraphQL.Server.Transports.AspNetCore; + +/// +/// Authorization parameters +/// +public struct AuthorizationParameters +{ + /// + /// Initializes an instance with a specified + /// and parameters copied from the specified instance of . + /// + public AuthorizationParameters( + HttpContext httpContext, + GraphQLHttpMiddlewareOptions middlewareOptions, + Func? onNotAuthenticated, + Func? onNotAuthorizedRole, + Func? onNotAuthorizedPolicy) + { + HttpContext = httpContext; + AuthorizationRequired = middlewareOptions.AuthorizationRequired; + AuthorizedRoles = middlewareOptions.AuthorizedRoles; + AuthorizedPolicy = middlewareOptions.AuthorizedPolicy; + OnNotAuthenticated = onNotAuthenticated; + OnNotAuthorizedRole = onNotAuthorizedRole; + OnNotAuthorizedPolicy = onNotAuthorizedPolicy; + } + + /// + /// Gets or sets the for the request. + /// + public HttpContext HttpContext { get; set; } + + /// + public bool AuthorizationRequired { get; set; } + + /// + public List? AuthorizedRoles { get; set; } + + /// + public string? AuthorizedPolicy { get; set; } + + /// + /// A delegate which executes if is set + /// but returns . + /// + public Func? OnNotAuthenticated { get; set; } + + /// + /// A delegate which executes if is set but + /// returns + /// for all roles. + /// + public Func? OnNotAuthorizedRole { get; set; } + + /// + /// A delegate which executes if is set but + /// + /// returns an unsuccessful for the specified policy. + /// + public Func? OnNotAuthorizedPolicy { get; set; } +} diff --git a/src/Transports.AspNetCore/Errors/AccessDeniedError.cs b/src/Transports.AspNetCore/Errors/AccessDeniedError.cs new file mode 100644 index 00000000..92eff04e --- /dev/null +++ b/src/Transports.AspNetCore/Errors/AccessDeniedError.cs @@ -0,0 +1,38 @@ +#nullable enable + +using GraphQL.Validation; +using GraphQLParser.AST; +using Microsoft.AspNetCore.Authorization; + +namespace GraphQL.Server.Transports.AspNetCore.Errors; + +/// +/// Represents an error indicating that the user is not allowed access to the specified resource. +/// +public class AccessDeniedError : ValidationError +{ + /// + public AccessDeniedError(string resource) + : base($"Access denied for {resource}.") + { + } + + /// + public AccessDeniedError(string resource, GraphQLParser.ROM originalQuery, params ASTNode[] nodes) + : base(originalQuery, null!, $"Access denied for {resource}.", nodes) + { + } + + /// + /// Returns the policy that would allow access to these node(s). + /// + public string? PolicyRequired { get; set; } + + /// + public AuthorizationResult? PolicyAuthorizationResult { get; set; } + + /// + /// Returns the list of role memberships that would allow access to these node(s). + /// + public List? RolesRequired { get; set; } +} diff --git a/src/Transports.AspNetCore/Errors/BatchedRequestsNotSupportedError.cs b/src/Transports.AspNetCore/Errors/BatchedRequestsNotSupportedError.cs new file mode 100644 index 00000000..fca86560 --- /dev/null +++ b/src/Transports.AspNetCore/Errors/BatchedRequestsNotSupportedError.cs @@ -0,0 +1,12 @@ +#nullable enable + +namespace GraphQL.Server.Transports.AspNetCore.Errors; + +/// +/// Represents an error indicating that batched requests are not supported. +/// +public class BatchedRequestsNotSupportedError : RequestError +{ + /// + public BatchedRequestsNotSupportedError() : base("Batched requests are not supported.") { } +} diff --git a/src/Transports.AspNetCore/Errors/HttpMethodValidationError.cs b/src/Transports.AspNetCore/Errors/HttpMethodValidationError.cs new file mode 100644 index 00000000..97219195 --- /dev/null +++ b/src/Transports.AspNetCore/Errors/HttpMethodValidationError.cs @@ -0,0 +1,19 @@ +#nullable enable + +using GraphQL.Validation; +using GraphQLParser.AST; + +namespace GraphQL.Server.Transports.AspNetCore.Errors; + +/// +/// Represents a validation error indicating that the requested operation is not valid +/// for the type of HTTP request. +/// +public class HttpMethodValidationError : ValidationError +{ + /// + public HttpMethodValidationError(GraphQLParser.ROM originalQuery, ASTNode node, string message) + : base(originalQuery, null!, message, node) + { + } +} diff --git a/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs b/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs new file mode 100644 index 00000000..54293693 --- /dev/null +++ b/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs @@ -0,0 +1,15 @@ +#nullable enable + +namespace GraphQL.Server.Transports.AspNetCore.Errors; + +/// +/// Represents an error indicating that the content-type was invalid. +/// +public class InvalidContentTypeError : RequestError +{ + /// + public InvalidContentTypeError() : base("Invalid 'Content-Type' header.") { } + + /// + public InvalidContentTypeError(string message) : base("Invalid 'Content-Type' header: " + message) { } +} diff --git a/src/Transports.AspNetCore/Errors/JsonInvalidError.cs b/src/Transports.AspNetCore/Errors/JsonInvalidError.cs new file mode 100644 index 00000000..a322b992 --- /dev/null +++ b/src/Transports.AspNetCore/Errors/JsonInvalidError.cs @@ -0,0 +1,15 @@ +#nullable enable + +namespace GraphQL.Server.Transports.AspNetCore.Errors; + +/// +/// Represents an error indicating that the JSON provided could not be parsed. +/// +public class JsonInvalidError : RequestError +{ + /// + public JsonInvalidError() : base($"JSON body text could not be parsed.") { } + + /// + public JsonInvalidError(Exception innerException) : base($"JSON body text could not be parsed. {innerException.Message}") { } +} diff --git a/src/Transports.AspNetCore/Errors/QueryMissingError.cs b/src/Transports.AspNetCore/Errors/QueryMissingError.cs new file mode 100644 index 00000000..239d9b4d --- /dev/null +++ b/src/Transports.AspNetCore/Errors/QueryMissingError.cs @@ -0,0 +1,12 @@ +#nullable enable + +namespace GraphQL.Server.Transports.AspNetCore.Errors; + +/// +/// Represents an error indicating that no GraphQL query was provided to the request. +/// +public class QueryMissingError : RequestError +{ + /// + public QueryMissingError() : base("GraphQL query is missing.") { } +} diff --git a/src/Transports.AspNetCore/Errors/RequestError.cs b/src/Transports.AspNetCore/Errors/RequestError.cs new file mode 100644 index 00000000..3c518fc5 --- /dev/null +++ b/src/Transports.AspNetCore/Errors/RequestError.cs @@ -0,0 +1,35 @@ +#nullable enable + +using GraphQL.Execution; + +namespace GraphQL.Server.Transports.AspNetCore.Errors; + +/// +/// Represents an error that occurred prior to the execution of the request. +/// +public class RequestError : ExecutionError +{ + /// + /// Initializes an instance with the specified message. + /// + public RequestError(string message) : base(message) + { + Code = GetErrorCode(); + } + + /// + /// Initializes an instance with the specified message and inner exception. + /// + public RequestError(string message, Exception? innerException) : base(message, innerException) + { + Code = GetErrorCode(); + } + + internal string GetErrorCode() + { + var code = ErrorInfoProvider.GetErrorCode(GetType()); + if (code != "REQUEST_ERROR" && code.EndsWith("_ERROR", StringComparison.Ordinal)) + code = code[0..^6]; + return code; + } +} diff --git a/src/Transports.AspNetCore/Errors/WebSocketSubProtocolNotSupportedError.cs b/src/Transports.AspNetCore/Errors/WebSocketSubProtocolNotSupportedError.cs new file mode 100644 index 00000000..3eef4603 --- /dev/null +++ b/src/Transports.AspNetCore/Errors/WebSocketSubProtocolNotSupportedError.cs @@ -0,0 +1,15 @@ +#nullable enable + +namespace GraphQL.Server.Transports.AspNetCore.Errors; + +/// +/// Represents an error indicating that none of the requested websocket sub-protocols are supported. +/// +public class WebSocketSubProtocolNotSupportedError : RequestError +{ + /// + public WebSocketSubProtocolNotSupportedError(IEnumerable requestedSubProtocols) + : base($"Invalid WebSocket sub-protocol(s): {string.Join(",", requestedSubProtocols.Select(x => $"'{x}'"))}") + { + } +} diff --git a/src/Transports.AspNetCore/Extensions/GraphQLBuilderMiddlewareExtensions.cs b/src/Transports.AspNetCore/Extensions/GraphQLBuilderMiddlewareExtensions.cs deleted file mode 100644 index a1bf4a09..00000000 --- a/src/Transports.AspNetCore/Extensions/GraphQLBuilderMiddlewareExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -#nullable enable - -using GraphQL.DI; -using GraphQL.Server.Transports.AspNetCore; -using GraphQL.Types; - -namespace GraphQL.Server -{ - /// - /// GraphQL specific extension methods for . - /// - public static class GraphQLBuilderMiddlewareExtensions - { - public static IGraphQLBuilder AddHttpMiddleware(this IGraphQLBuilder builder) - where TSchema : ISchema - { - builder.Services.Register, GraphQLHttpMiddleware>(ServiceLifetime.Singleton); - return builder; - } - - public static IGraphQLBuilder AddHttpMiddleware(this IGraphQLBuilder builder) - where TSchema : ISchema - where TMiddleware : GraphQLHttpMiddleware - { - builder.Services.Register(ServiceLifetime.Singleton); - return builder; - } - } -} diff --git a/src/Transports.AspNetCore/Extensions/GraphQLBuilderUserContextExtensions.cs b/src/Transports.AspNetCore/Extensions/GraphQLBuilderUserContextExtensions.cs index 78d2f25b..47f14362 100644 --- a/src/Transports.AspNetCore/Extensions/GraphQLBuilderUserContextExtensions.cs +++ b/src/Transports.AspNetCore/Extensions/GraphQLBuilderUserContextExtensions.cs @@ -1,93 +1,97 @@ +#nullable enable + using GraphQL.DI; using GraphQL.Server.Transports.AspNetCore; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -namespace GraphQL.Server +namespace GraphQL.Server; + +public static class GraphQLBuilderUserContextExtensions { - public static class GraphQLBuilderUserContextExtensions + /// + /// Adds an as a singleton. + /// + /// The type of the implementation. + /// The GraphQL builder. + /// The GraphQL builder. + public static IGraphQLBuilder AddUserContextBuilder(this IGraphQLBuilder builder) + where TUserContextBuilder : class, IUserContextBuilder { - /// - /// Adds an as a singleton. - /// - /// The type of the implementation. - /// The GraphQL builder. - /// The GraphQL builder. - public static IGraphQLBuilder AddUserContextBuilder(this IGraphQLBuilder builder) - where TUserContextBuilder : class, IUserContextBuilder + builder.Services.Register(DI.ServiceLifetime.Singleton); + builder.ConfigureExecutionOptions(async options => { - builder.Services.Register(DI.ServiceLifetime.Singleton); - builder.ConfigureExecutionOptions(async options => + if (options.UserContext == null || options.UserContext.Count == 0 && options.UserContext.GetType() == typeof(Dictionary)) { - if (options.UserContext == null || options.UserContext.Count == 0 && options.UserContext.GetType() == typeof(Dictionary)) - { - var httpContext = options.RequestServices.GetRequiredService().HttpContext; - var contextBuilder = options.RequestServices.GetRequiredService(); - options.UserContext = await contextBuilder.BuildUserContext(httpContext); - } - }); + var requestServices = options.RequestServices ?? throw new MissingRequestServicesException(); + var httpContext = requestServices.GetRequiredService().HttpContext!; + var contextBuilder = requestServices.GetRequiredService(); + options.UserContext = await contextBuilder.BuildUserContextAsync(httpContext); + } + }); - return builder; - } + return builder; + } - /// - /// Set up a delegate to create the UserContext for each GraphQL request - /// - /// - /// The GraphQL builder. - /// A delegate used to create the user context from the . - /// The GraphQL builder. - public static IGraphQLBuilder AddUserContextBuilder(this IGraphQLBuilder builder, Func creator) - where TUserContext : class, IDictionary + /// + /// Set up a delegate to create the UserContext for each GraphQL request + /// + /// + /// The GraphQL builder. + /// A delegate used to create the user context from the . + /// The GraphQL builder. + public static IGraphQLBuilder AddUserContextBuilder(this IGraphQLBuilder builder, Func creator) + where TUserContext : class, IDictionary + { + builder.Services.Register(new UserContextBuilder(creator)); + builder.ConfigureExecutionOptions(options => { - builder.Services.Register(new UserContextBuilder(creator)); - builder.ConfigureExecutionOptions(options => + if (options.UserContext == null || options.UserContext.Count == 0 && options.UserContext.GetType() == typeof(Dictionary)) { - if (options.UserContext == null || options.UserContext.Count == 0 && options.UserContext.GetType() == typeof(Dictionary)) - { - var httpContext = options.RequestServices.GetRequiredService().HttpContext; - options.UserContext = creator(httpContext); - } - }); + var requestServices = options.RequestServices ?? throw new MissingRequestServicesException(); + var httpContext = requestServices.GetRequiredService().HttpContext!; + options.UserContext = creator(httpContext); + } + }); - return builder; - } + return builder; + } - /// - /// Set up a delegate to create the UserContext for each GraphQL request - /// - /// - /// The GraphQL builder. - /// A delegate used to create the user context from the . - /// The GraphQL builder. - public static IGraphQLBuilder AddUserContextBuilder(this IGraphQLBuilder builder, Func> creator) - where TUserContext : class, IDictionary + /// + /// Set up a delegate to create the UserContext for each GraphQL request + /// + /// + /// The GraphQL builder. + /// A delegate used to create the user context from the . + /// The GraphQL builder. + public static IGraphQLBuilder AddUserContextBuilder(this IGraphQLBuilder builder, Func> creator) + where TUserContext : class, IDictionary + { + builder.Services.Register(new UserContextBuilder(context => new(creator(context)))); + builder.ConfigureExecutionOptions(async options => { - builder.Services.Register(new UserContextBuilder(creator)); - builder.ConfigureExecutionOptions(async options => + if (options.UserContext == null || options.UserContext.Count == 0 && options.UserContext.GetType() == typeof(Dictionary)) { - if (options.UserContext == null || options.UserContext.Count == 0 && options.UserContext.GetType() == typeof(Dictionary)) - { - var httpContext = options.RequestServices.GetRequiredService().HttpContext; - options.UserContext = await creator(httpContext); - } - }); + var requestServices = options.RequestServices ?? throw new MissingRequestServicesException(); + var httpContext = requestServices.GetRequiredService().HttpContext!; + options.UserContext = await creator(httpContext); + } + }); - return builder; - } + return builder; + } - /// - /// Set up default policy for matching endpoints. It is required when both GraphQL HTTP and - /// GraphQL WebSockets middlewares are mapped to the same endpoint (by default 'graphql'). - /// - /// The GraphQL builder. - /// The GraphQL builder. - public static IGraphQLBuilder AddDefaultEndpointSelectorPolicy(this IGraphQLBuilder builder) - { - builder.Services.TryRegister(DI.ServiceLifetime.Singleton, RegistrationCompareMode.ServiceTypeAndImplementationType); + /// + /// Set up default policy for matching endpoints. It is required when both GraphQL HTTP and + /// GraphQL WebSockets middlewares are mapped to the same endpoint (by default 'graphql'). + /// + /// The GraphQL builder. + /// The GraphQL builder. + public static IGraphQLBuilder AddDefaultEndpointSelectorPolicy(this IGraphQLBuilder builder) + { + builder.Services.TryRegister(DI.ServiceLifetime.Singleton, RegistrationCompareMode.ServiceTypeAndImplementationType); - return builder; - } + return builder; } } diff --git a/src/Transports.AspNetCore/Extensions/GraphQLHttpApplicationBuilderExtensions.cs b/src/Transports.AspNetCore/Extensions/GraphQLHttpApplicationBuilderExtensions.cs index fe5b5a4e..d84cb30f 100644 --- a/src/Transports.AspNetCore/Extensions/GraphQLHttpApplicationBuilderExtensions.cs +++ b/src/Transports.AspNetCore/Extensions/GraphQLHttpApplicationBuilderExtensions.cs @@ -2,68 +2,95 @@ using GraphQL.Types; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for to add +/// or its descendants in the HTTP request pipeline. +/// +public static class GraphQLHttpApplicationBuilderExtensions { /// - /// Extensions for to add - /// or its descendants in the HTTP request pipeline. + /// Add the GraphQL middleware to the HTTP request pipeline. + ///

+ /// Uses the GraphQL schema registered as within the dependency injection + /// framework to execute the query. ///
- public static class GraphQLHttpApplicationBuilderExtensions - { - /// - /// Add the GraphQL middleware to the HTTP request pipeline - /// - /// The implementation of to use - /// The application builder - /// The path to the GraphQL endpoint which defaults to '/graphql' - /// The received as parameter - public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, string path = "/graphql") - where TSchema : ISchema - => builder.UseGraphQL(new PathString(path)); + /// The application builder + /// The path to the GraphQL endpoint which defaults to '/graphql' + /// A delegate to configure the middleware + /// The received as parameter + public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, string path = "/graphql", Action? configureMiddleware = null) + => builder.UseGraphQL(path, configureMiddleware); - /// - /// Add the GraphQL middleware to the HTTP request pipeline - /// - /// The implementation of to use - /// The application builder - /// The path to the GraphQL endpoint - /// The received as parameter - public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, PathString path) - where TSchema : ISchema - { - return builder.UseWhen( - context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), - b => b.UseMiddleware>()); - } + /// + /// Add the GraphQL middleware to the HTTP request pipeline. + ///

+ /// Uses the GraphQL schema registered as within the dependency injection + /// framework to execute the query. + ///
+ /// The application builder + /// The path to the GraphQL endpoint + /// A delegate to configure the middleware + /// The received as parameter + public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, PathString path, Action? configureMiddleware = null) + => builder.UseGraphQL(path, configureMiddleware); - /// - /// Add the GraphQL custom middleware to the HTTP request pipeline - /// - /// The implementation of to use - /// Custom middleware inherited from - /// The application builder - /// The path to the GraphQL endpoint which defaults to '/graphql' - /// The received as parameter - public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, string path = "/graphql") - where TSchema : ISchema - where TMiddleware : GraphQLHttpMiddleware - => builder.UseGraphQL(new PathString(path)); + /// + /// Add the GraphQL middleware to the HTTP request pipeline for the specified schema. + /// + /// The implementation of to use + /// The application builder + /// The path to the GraphQL endpoint which defaults to '/graphql' + /// A delegate to configure the middleware + /// The received as parameter + public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, string path = "/graphql", Action? configureMiddleware = null) + where TSchema : ISchema + => builder.UseGraphQL(new PathString(path), configureMiddleware); - /// - /// Add the GraphQL custom middleware to the HTTP request pipeline - /// - /// The implementation of to use - /// Custom middleware inherited from - /// The application builder - /// The path to the GraphQL endpoint - /// The received as parameter - public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, PathString path) - where TSchema : ISchema - where TMiddleware : GraphQLHttpMiddleware - { - return builder.UseWhen( - context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), - b => b.UseMiddleware()); - } + /// + /// Add the GraphQL middleware to the HTTP request pipeline for the specified schema. + /// + /// The implementation of to use + /// The application builder + /// The path to the GraphQL endpoint + /// A delegate to configure the middleware + /// The received as parameter + public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, PathString path, Action? configureMiddleware = null) + where TSchema : ISchema + { + var opts = new GraphQLHttpMiddlewareOptions(); + configureMiddleware?.Invoke(opts); + return builder.UseWhen( + context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), + b => b.UseMiddleware>(opts)); + } + + /// + /// Add the GraphQL custom middleware to the HTTP request pipeline for the specified schema. + /// + /// Custom middleware inherited from + /// The application builder + /// The path to the GraphQL endpoint which defaults to '/graphql' + /// The arguments to pass to the middleware type instance's constructor. + /// The received as parameter + public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, string path = "/graphql", params object[] args) + where TMiddleware : GraphQLHttpMiddleware + => builder.UseGraphQL(new PathString(path), args); + + /// + /// Add the GraphQL custom middleware to the HTTP request pipeline for the specified schema. + /// + /// Custom middleware inherited from + /// The application builder + /// The path to the GraphQL endpoint + /// The arguments to pass to the middleware type instance's constructor. + /// The received as parameter + public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, PathString path, params object[] args) + where TMiddleware : GraphQLHttpMiddleware + { + return builder.UseWhen( + context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), + b => b.UseMiddleware(args)); } } diff --git a/src/Transports.AspNetCore/Extensions/GraphQLHttpEndpointRouteBuilderExtensions.cs b/src/Transports.AspNetCore/Extensions/GraphQLHttpEndpointRouteBuilderExtensions.cs index d6bfc913..63dff4c9 100644 --- a/src/Transports.AspNetCore/Extensions/GraphQLHttpEndpointRouteBuilderExtensions.cs +++ b/src/Transports.AspNetCore/Extensions/GraphQLHttpEndpointRouteBuilderExtensions.cs @@ -2,65 +2,74 @@ using GraphQL.Types; using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for to add +/// or its descendants in the HTTP request pipeline. +/// +public static class GraphQLHttpEndpointRouteBuilderExtensions { /// - /// Extensions for to add - /// or its descendants in the HTTP request pipeline. + /// Add the GraphQL middleware to the HTTP request pipeline. + ///

+ /// Uses the GraphQL schema registered as within the dependency injection + /// framework to execute the query. ///
- public static class GraphQLHttpEndpointRouteBuilderExtensions - { - /// - /// Add the GraphQL middleware to the HTTP request pipeline - /// - /// The implementation of to use - /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. - /// The route pattern. - /// The received as parameter - public static GraphQLHttpEndpointConventionBuilder MapGraphQL(this IEndpointRouteBuilder endpoints, string pattern = "graphql") - where TSchema : ISchema - { - if (endpoints == null) - throw new ArgumentNullException(nameof(endpoints)); - - var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware>().Build(); - return new GraphQLHttpEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL")); - } + /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// The route pattern. + /// A delegate to configure the middleware + /// The received as parameter + public static GraphQLEndpointConventionBuilder MapGraphQL(this IEndpointRouteBuilder endpoints, string pattern = "graphql", Action? configureMiddleware = null) + => endpoints.MapGraphQL(pattern, configureMiddleware); - /// - /// Add the GraphQL middleware to the HTTP request pipeline - /// - /// The implementation of to use - /// Custom middleware inherited from - /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. - /// The route pattern. - /// The received as parameter - public static GraphQLHttpEndpointConventionBuilder MapGraphQL(this IEndpointRouteBuilder endpoints, string pattern = "graphql") - where TSchema : ISchema - where TMiddleware : GraphQLHttpMiddleware - { - if (endpoints == null) - throw new ArgumentNullException(nameof(endpoints)); + /// + /// Add the GraphQL middleware to the HTTP request pipeline for the specified schema. + /// + /// The implementation of to use + /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// The route pattern. + /// A delegate to configure the middleware + /// The received as parameter + public static GraphQLEndpointConventionBuilder MapGraphQL(this IEndpointRouteBuilder endpoints, string pattern = "graphql", Action? configureMiddleware = null) + where TSchema : ISchema + { + var opts = new GraphQLHttpMiddlewareOptions(); + configureMiddleware?.Invoke(opts); - var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware().Build(); - return new GraphQLHttpEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL")); - } + var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware>(opts).Build(); + return new GraphQLEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL")); } /// - /// Builds conventions that will be used for customization of Microsoft.AspNetCore.Builder.EndpointBuilder instances. - /// Special convention builder that allows you to write specific extension methods for ASP.NET Core routing subsystem. + /// Add the GraphQL middleware to the HTTP request pipeline for the specified schema. /// - public class GraphQLHttpEndpointConventionBuilder : IEndpointConventionBuilder + /// Custom middleware inherited from + /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// The route pattern. + /// The arguments to pass to the middleware type instance's constructor. + /// The received as parameter + public static GraphQLEndpointConventionBuilder MapGraphQL(this IEndpointRouteBuilder endpoints, string pattern = "graphql", params object[] args) + where TMiddleware : GraphQLHttpMiddleware { - private readonly IEndpointConventionBuilder _builder; + var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware(args).Build(); + return new GraphQLEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL")); + } +} - internal GraphQLHttpEndpointConventionBuilder(IEndpointConventionBuilder builder) - { - _builder = builder; - } +/// +/// Builds conventions that will be used for customization of Microsoft.AspNetCore.Builder.EndpointBuilder instances. +/// Special convention builder that allows you to write specific extension methods for ASP.NET Core routing subsystem. +/// +public class GraphQLEndpointConventionBuilder : IEndpointConventionBuilder +{ + private readonly IEndpointConventionBuilder _builder; - /// - public void Add(Action convention) => _builder.Add(convention); + internal GraphQLEndpointConventionBuilder(IEndpointConventionBuilder builder) + { + _builder = builder; } + + /// + public void Add(Action convention) => _builder.Add(convention); } diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 74208219..4dd01354 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -1,355 +1,698 @@ +#nullable enable + using System.Net; using System.Net.Http.Headers; -using GraphQL.Instrumentation; +using GraphQL.Server.Transports.AspNetCore.Errors; using GraphQL.Transport; using GraphQL.Types; +using GraphQL.Validation; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace GraphQL.Server.Transports.AspNetCore +namespace GraphQL.Server.Transports.AspNetCore; + + +/// +/// +/// Type of GraphQL schema that is used to validate and process requests. +/// Specifying will pull the registered default schema. +/// +public class GraphQLHttpMiddleware : GraphQLHttpMiddleware + where TSchema : ISchema { + private readonly IDocumentExecuter _documentExecuter; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly IEnumerable _getValidationRules; + private readonly IEnumerable _getCachedDocumentValidationRules; + private readonly IEnumerable _postValidationRules; + private readonly IEnumerable _postCachedDocumentValidationRules; + + // important: when using convention-based ASP.NET Core middleware, the first constructor is always used + + /************* WebSocket support ************ + /// - /// ASP.NET Core middleware for processing GraphQL requests. Can processes both single and batch requests. - /// See Transport-level batching - /// for more information. This middleware useful with and without ASP.NET Core routing. - ///

- /// GraphQL over HTTP spec says: - /// GET requests can be used for executing ONLY queries. If the values of query and operationName indicates that - /// a non-query operation is to be executed, the server should immediately respond with an error status code, and - /// halt execution. - ///

- /// Attention! The current implementation does not impose such a restriction and allows mutations in GET requests. + /// Initializes a new instance. ///
- /// Type of GraphQL schema that is used to validate and process requests. - public class GraphQLHttpMiddleware : IMiddleware - where TSchema : ISchema + public GraphQLHttpMiddleware( + RequestDelegate next, + IGraphQLTextSerializer serializer, + IDocumentExecuter documentExecuter, + IServiceScopeFactory serviceScopeFactory, + GraphQLHttpMiddlewareOptions options, + IServiceProvider provider, + IHostApplicationLifetime hostApplicationLifetime) + : this(next, serializer, documentExecuter, serviceScopeFactory, options, + CreateWebSocketHandlers(serializer, documentExecuter, serviceScopeFactory, provider, hostApplicationLifetime, options)) + { + } + + *********************************/ + + /// + /// Initializes a new instance. + /// + protected GraphQLHttpMiddleware( + RequestDelegate next, + IGraphQLTextSerializer serializer, + IDocumentExecuter documentExecuter, + IServiceScopeFactory serviceScopeFactory, + GraphQLHttpMiddlewareOptions options /*, + IEnumerable>? webSocketHandlers = null */) + : base(next, serializer, options /*, webSocketHandlers */) + { + _documentExecuter = documentExecuter ?? throw new ArgumentNullException(nameof(documentExecuter)); + _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + var getRule = new HttpGetValidationRule(); + _getValidationRules = DocumentValidator.CoreRules.Append(getRule).ToArray(); + _getCachedDocumentValidationRules = new[] { getRule }; + var postRule = new HttpPostValidationRule(); + _postValidationRules = DocumentValidator.CoreRules.Append(postRule).ToArray(); + _postCachedDocumentValidationRules = new[] { postRule }; + } + + /************* WebSocket support *********** + + private static IEnumerable> CreateWebSocketHandlers( + IGraphQLSerializer serializer, + IDocumentExecuter documentExecuter, + IServiceScopeFactory serviceScopeFactory, + IServiceProvider provider, + IHostApplicationLifetime hostApplicationLifetime, + GraphQLHttpMiddlewareOptions options) { - private const string DOCS_URL = "See: http://graphql.org/learn/serving-over-http/."; + if (hostApplicationLifetime == null) + throw new ArgumentNullException(nameof(hostApplicationLifetime)); + if (provider == null) + throw new ArgumentNullException(nameof(provider)); + + return new IWebSocketHandler[] { + new WebSocketHandler(serializer, documentExecuter, serviceScopeFactory, options, + hostApplicationLifetime, provider.GetService()), + }; + } - private readonly IGraphQLTextSerializer _serializer; + ******************************/ - public GraphQLHttpMiddleware(IGraphQLTextSerializer serializer) + /// + protected override async Task ExecuteScopedRequestAsync(HttpContext context, GraphQLRequest request, IDictionary userContext) + { + using var scope = _serviceScopeFactory.CreateScope(); + return await ExecuteRequestAsync(context, request, scope.ServiceProvider, userContext); + } + + /// + protected override async Task ExecuteRequestAsync(HttpContext context, GraphQLRequest request, IServiceProvider serviceProvider, IDictionary userContext) + { + if (request?.Query == null) { - _serializer = serializer; + return new ExecutionResult + { + Errors = new ExecutionErrors { + new QueryMissingError() + } + }; } + var opts = new ExecutionOptions + { + Query = request.Query, + Variables = request.Variables, + Extensions = request.Extensions, + CancellationToken = context.RequestAborted, + OperationName = request.OperationName, + RequestServices = serviceProvider, + UserContext = userContext, + }; + if (!context.WebSockets.IsWebSocketRequest) + { + if (HttpMethods.IsGet(context.Request.Method)) + { + opts.ValidationRules = _getValidationRules; + opts.CachedDocumentValidationRules = _getCachedDocumentValidationRules; + } + else if (HttpMethods.IsPost(context.Request.Method)) + { + opts.ValidationRules = _postValidationRules; + opts.CachedDocumentValidationRules = _postCachedDocumentValidationRules; + } + } + return await _documentExecuter.ExecuteAsync(opts); + } +} + +/// +/// ASP.NET Core middleware for processing GraphQL requests. Handles both single and batch requests, +/// and dispatches WebSocket requests to the registered . +/// +public abstract class GraphQLHttpMiddleware +{ + private readonly IGraphQLTextSerializer _serializer; + //private readonly IEnumerable? _webSocketHandlers; + private readonly RequestDelegate _next; - public virtual async Task InvokeAsync(HttpContext context, RequestDelegate next) + private const string QUERY_KEY = "query"; + private const string VARIABLES_KEY = "variables"; + private const string EXTENSIONS_KEY = "extensions"; + private const string OPERATION_NAME_KEY = "operationName"; + private const string MEDIATYPE_JSON = "application/json"; + private const string MEDIATYPE_GRAPHQL = "application/graphql"; + + /// + /// Gets the options configured for this instance. + /// + protected GraphQLHttpMiddlewareOptions Options { get; } + + /// + /// Initializes a new instance. + /// + public GraphQLHttpMiddleware( + RequestDelegate next, + IGraphQLTextSerializer serializer, + GraphQLHttpMiddlewareOptions options /*, + IEnumerable? webSocketHandlers = null */) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + Options = options ?? throw new ArgumentNullException(nameof(options)); + //_webSocketHandlers = webSocketHandlers; + } + + /// + public virtual async Task InvokeAsync(HttpContext context) + { + if (context.WebSockets.IsWebSocketRequest) { - if (context.WebSockets.IsWebSocketRequest) + /************* WebSocket support ************ + if (Options.HandleWebSockets) { - await next(context); - return; + if (await HandleAuthorizeWebSocketConnectionAsync(context, _next)) + return; + // Process WebSocket request + await HandleWebSocketAsync(context, _next); + } + else + { + await HandleInvalidHttpMethodErrorAsync(context, _next); } + ************************/ + await HandleInvalidHttpMethodErrorAsync(context, _next); + return; + } - // Handle requests as per recommendation at http://graphql.org/learn/serving-over-http/ - // Inspiration: https://github.com/graphql/express-graphql/blob/master/src/index.js - var httpRequest = context.Request; - var httpResponse = context.Response; + // Handle requests as per recommendation at http://graphql.org/learn/serving-over-http/ + // Inspiration: https://github.com/graphql/express-graphql/blob/master/src/index.js + var httpRequest = context.Request; + var httpResponse = context.Response; - var cancellationToken = GetCancellationToken(context); + // GraphQL HTTP only supports GET and POST methods + bool isGet = HttpMethods.IsGet(httpRequest.Method); + bool isPost = HttpMethods.IsPost(httpRequest.Method); + if (isGet && !Options.HandleGet || isPost && !Options.HandlePost || !isGet && !isPost) + { + await HandleInvalidHttpMethodErrorAsync(context, _next); + return; + } - // GraphQL HTTP only supports GET and POST methods - bool isGet = HttpMethods.IsGet(httpRequest.Method); - bool isPost = HttpMethods.IsPost(httpRequest.Method); - if (!isGet && !isPost) + // Authenticate request if necessary + if (await HandleAuthorizeAsync(context, _next)) + return; + + // Parse POST body + GraphQLRequest? bodyGQLRequest = null; + IList? bodyGQLBatchRequest = null; + if (isPost) + { + if (!MediaTypeHeaderValue.TryParse(httpRequest.ContentType, out var mediaTypeHeader)) { - httpResponse.Headers["Allow"] = "GET, POST"; - await HandleInvalidHttpMethodErrorAsync(context); + await HandleContentTypeCouldNotBeParsedErrorAsync(context, _next); return; } - // Parse POST body - GraphQLRequest bodyGQLRequest = null; - IList bodyGQLBatchRequest = null; - if (isPost) + switch (mediaTypeHeader.MediaType) { - if (!MediaTypeHeaderValue.TryParse(httpRequest.ContentType, out var mediaTypeHeader)) - { - await HandleContentTypeCouldNotBeParsedErrorAsync(context); - return; - } - - switch (mediaTypeHeader.MediaType) - { - case MediaType.JSON: - IList deserializationResult; - try - { + case MEDIATYPE_JSON: + IList? deserializationResult; + try + { #if NET5_0_OR_GREATER - if (!TryGetEncoding(mediaTypeHeader.CharSet, out var sourceEncoding)) - { - await HandleContentTypeCouldNotBeParsedErrorAsync(context); - return; - } - // Wrap content stream into a transcoding stream that buffers the data transcoded from the sourceEncoding to utf-8. - if (sourceEncoding != null && sourceEncoding != System.Text.Encoding.UTF8) - { - using var tempStream = System.Text.Encoding.CreateTranscodingStream(httpRequest.Body, innerStreamEncoding: sourceEncoding, outerStreamEncoding: System.Text.Encoding.UTF8, leaveOpen: true); - deserializationResult = await _serializer.ReadAsync>(tempStream, cancellationToken); - } - else - { - deserializationResult = await _serializer.ReadAsync>(httpRequest.Body, cancellationToken); - } + if (!TryGetEncoding(mediaTypeHeader.CharSet, out var sourceEncoding)) + { + await HandleContentTypeCouldNotBeParsedErrorAsync(context, _next); + return; + } + // Wrap content stream into a transcoding stream that buffers the data transcoded from the sourceEncoding to utf-8. + if (sourceEncoding != null && sourceEncoding != System.Text.Encoding.UTF8) + { + using var tempStream = System.Text.Encoding.CreateTranscodingStream(httpRequest.Body, innerStreamEncoding: sourceEncoding, outerStreamEncoding: System.Text.Encoding.UTF8, leaveOpen: true); + deserializationResult = await _serializer.ReadAsync>(tempStream, context.RequestAborted); + } + else + { + deserializationResult = await _serializer.ReadAsync>(httpRequest.Body, context.RequestAborted); + } #else - deserializationResult = await _serializer.ReadAsync>(httpRequest.Body, cancellationToken); + deserializationResult = await _serializer.ReadAsync>(httpRequest.Body, context.RequestAborted); #endif + } + catch (Exception ex) + { + if (!await HandleDeserializationErrorAsync(context, _next, ex)) + throw; + return; + } + // https://github.com/graphql-dotnet/server/issues/751 + if (deserializationResult is GraphQLRequest[] array && array.Length == 1) + bodyGQLRequest = deserializationResult[0]; + else + bodyGQLBatchRequest = deserializationResult; + break; + + case MEDIATYPE_GRAPHQL: + bodyGQLRequest = await DeserializeFromGraphBodyAsync(httpRequest.Body); + break; + + default: + if (httpRequest.HasFormContentType) + { + var formCollection = await httpRequest.ReadFormAsync(context.RequestAborted); + try + { + bodyGQLRequest = DeserializeFromFormBody(formCollection); } catch (Exception ex) { - if (!await HandleDeserializationErrorAsync(context, ex)) + if (!await HandleDeserializationErrorAsync(context, _next, ex)) throw; return; } - // https://github.com/graphql-dotnet/server/issues/751 - if (deserializationResult is GraphQLRequest[] array && array.Length == 1) - bodyGQLRequest = deserializationResult[0]; - else - bodyGQLBatchRequest = deserializationResult; - break; - - case MediaType.GRAPH_QL: - bodyGQLRequest = await DeserializeFromGraphBodyAsync(httpRequest.Body); break; - - default: - if (httpRequest.HasFormContentType) - { - var formCollection = await httpRequest.ReadFormAsync(cancellationToken); - try - { - bodyGQLRequest = DeserializeFromFormBody(formCollection); - } - catch (Exception ex) - { - if (!await HandleDeserializationErrorAsync(context, ex)) - throw; - return; - } - break; - } - await HandleInvalidContentTypeErrorAsync(context); - return; - } + } + await HandleInvalidContentTypeErrorAsync(context, _next); + return; } + } + if (bodyGQLBatchRequest == null) + { // If we don't have a batch request, parse the query from URL too to determine the actual request to run. // Query string params take priority. - GraphQLRequest gqlRequest = null; - if (bodyGQLBatchRequest == null) + GraphQLRequest? gqlRequest = null; + GraphQLRequest? urlGQLRequest = null; + if (isGet || Options.ReadQueryStringOnPost) { - GraphQLRequest urlGQLRequest = null; try { urlGQLRequest = DeserializeFromQueryString(httpRequest.Query); } catch (Exception ex) { - if (!await HandleDeserializationErrorAsync(context, ex)) + if (!await HandleDeserializationErrorAsync(context, _next, ex)) throw; return; } + } - gqlRequest = new GraphQLRequest - { - Query = urlGQLRequest.Query ?? bodyGQLRequest?.Query, - Variables = urlGQLRequest.Variables ?? bodyGQLRequest?.Variables, - Extensions = urlGQLRequest.Extensions ?? bodyGQLRequest?.Extensions, - OperationName = urlGQLRequest.OperationName ?? bodyGQLRequest?.OperationName - }; + gqlRequest = new GraphQLRequest + { + Query = urlGQLRequest?.Query ?? bodyGQLRequest?.Query!, + Variables = urlGQLRequest?.Variables ?? bodyGQLRequest?.Variables, + Extensions = urlGQLRequest?.Extensions ?? bodyGQLRequest?.Extensions, + OperationName = urlGQLRequest?.OperationName ?? bodyGQLRequest?.OperationName + }; - if (string.IsNullOrWhiteSpace(gqlRequest.Query)) - { - await HandleNoQueryErrorAsync(context); - return; - } + if (string.IsNullOrWhiteSpace(gqlRequest.Query)) + { + await HandleNoQueryErrorAsync(context, _next); + return; } // Prepare context and execute - var userContextBuilder = context.RequestServices.GetService(); - var userContext = userContextBuilder == null - ? new Dictionary() // in order to allow resolvers to exchange their state through this object - : await userContextBuilder.BuildUserContext(context); - - var executer = context.RequestServices.GetRequiredService>(); - await HandleRequestAsync(context, next, userContext, bodyGQLBatchRequest, gqlRequest, executer, cancellationToken); + await HandleRequestAsync(context, _next, gqlRequest); } - - protected virtual async Task HandleRequestAsync( - HttpContext context, - RequestDelegate next, - IDictionary userContext, - IList bodyGQLBatchRequest, - GraphQLRequest gqlRequest, - IDocumentExecuter executer, - CancellationToken cancellationToken) + else if (Options.EnableBatchedRequests) { - // Normal execution with single graphql request - if (bodyGQLBatchRequest == null) - { - var stopwatch = ValueStopwatch.StartNew(); - await RequestExecutingAsync(gqlRequest); - var result = await ExecuteRequestAsync(gqlRequest, userContext, executer, context.RequestServices, cancellationToken); + await HandleBatchRequestAsync(context, _next, bodyGQLBatchRequest); + } + else + { + await HandleBatchedRequestsNotSupportedAsync(context, _next); + } + } - await RequestExecutedAsync(new GraphQLRequestExecutionResult(gqlRequest, result, stopwatch.Elapsed)); + /// + /// Perform authentication, if required, and return if the + /// request was handled (typically by returning an error message). If + /// is returned, the request is processed normally. + /// + protected virtual async ValueTask HandleAuthorizeAsync(HttpContext context, RequestDelegate next) + { + var success = await AuthorizationHelper.AuthorizeAsync( + new AuthorizationParameters<(GraphQLHttpMiddleware Middleware, HttpContext Context, RequestDelegate Next)>( + context, + Options, + static info => info.Middleware.HandleNotAuthenticatedAsync(info.Context, info.Next), + static info => info.Middleware.HandleNotAuthorizedRoleAsync(info.Context, info.Next), + static (info, result) => info.Middleware.HandleNotAuthorizedPolicyAsync(info.Context, info.Next, result)), + (this, context, next)); + + return !success; + } + + /// + /// Perform authorization, if required, and return if the + /// request was handled (typically by returning an error message). If + /// is returned, the request is processed normally. + ///

+ /// By default this does not check authorization rules becuase authentication may take place within + /// the WebSocket connection during the ConnectionInit message. Authorization checks for + /// WebSocket connections occur then, after authorization has taken place. + ///
+ protected virtual ValueTask HandleAuthorizeWebSocketConnectionAsync(HttpContext context, RequestDelegate next) + => new(false); - await WriteResponseAsync(context.Response, _serializer, cancellationToken, result); + /// + /// Handles a single GraphQL request. + /// + protected virtual async Task HandleRequestAsync( + HttpContext context, + RequestDelegate next, + GraphQLRequest gqlRequest) + { + // Normal execution with single graphql request + var userContext = await BuildUserContextAsync(context); + var result = await ExecuteRequestAsync(context, gqlRequest, context.RequestServices, userContext); + var statusCode = Options.ValidationErrorsReturnBadRequest && !result.Executed + ? HttpStatusCode.BadRequest + : HttpStatusCode.OK; + await WriteJsonResponseAsync(context, statusCode, result); + } + + /// + /// Handles a batched GraphQL request. + /// + protected virtual async Task HandleBatchRequestAsync( + HttpContext context, + RequestDelegate next, + IList gqlRequests) + { + var userContext = await BuildUserContextAsync(context); + var results = new ExecutionResult[gqlRequests.Count]; + if (gqlRequests.Count == 1) + { + results[0] = await ExecuteRequestAsync(context, gqlRequests[0], context.RequestServices, userContext); + } + else + { + // Batched execution with multiple graphql requests + if (!Options.BatchedRequestsExecuteInParallel) + { + for (int i = 0; i < gqlRequests.Count; i++) + { + results[i] = await ExecuteRequestAsync(context, gqlRequests[i], context.RequestServices, userContext); + } } - // Execute multiple graphql requests in one batch else { - var executionResults = new ExecutionResult[bodyGQLBatchRequest.Count]; - for (int i = 0; i < bodyGQLBatchRequest.Count; ++i) + var resultTasks = new Task[gqlRequests.Count]; + for (int i = 0; i < gqlRequests.Count; i++) { - var gqlRequestInBatch = bodyGQLBatchRequest[i]; - - var stopwatch = ValueStopwatch.StartNew(); - await RequestExecutingAsync(gqlRequestInBatch, i); - var result = await ExecuteRequestAsync(gqlRequestInBatch, userContext, executer, context.RequestServices, cancellationToken); - - await RequestExecutedAsync(new GraphQLRequestExecutionResult(gqlRequestInBatch, result, stopwatch.Elapsed, i)); - - executionResults[i] = result; + resultTasks[i] = ExecuteScopedRequestAsync(context, gqlRequests[i], userContext); + } + await Task.WhenAll(resultTasks); + for (int i = 0; i < gqlRequests.Count; i++) + { + results[i] = await resultTasks[i]; } - - await WriteResponseAsync(context.Response, _serializer, cancellationToken, executionResults); } } + await WriteJsonResponseAsync(context, HttpStatusCode.OK, results); + } - protected virtual async ValueTask HandleDeserializationErrorAsync(HttpContext context, Exception ex) - { - await WriteErrorResponseAsync(context, $"JSON body text could not be parsed. {ex.Message}", HttpStatusCode.BadRequest); - return true; - } - - protected virtual Task HandleNoQueryErrorAsync(HttpContext context) - => WriteErrorResponseAsync(context, "GraphQL query is missing.", HttpStatusCode.BadRequest); + /// + /// Executes a GraphQL request with a scoped service provider. + ///

+ /// Typically this method should create a service scope and call + /// ExecuteRequestAsync, + /// disposing of the scope when the asynchronous operation completes. + ///
+ protected abstract Task ExecuteScopedRequestAsync(HttpContext context, GraphQLRequest request, IDictionary userContext); - protected virtual Task HandleContentTypeCouldNotBeParsedErrorAsync(HttpContext context) - => WriteErrorResponseAsync(context, $"Invalid 'Content-Type' header: value '{context.Request.ContentType}' could not be parsed.", HttpStatusCode.UnsupportedMediaType); + /// + /// Executes a GraphQL request. + ///

+ /// It is suggested to use the and + /// to ensure that only query operations + /// are executed for GET requests, and query or mutation operations for + /// POST requests. + /// This should be set in both and + /// , as shown below: + /// + /// var rule = isGet ? new HttpGetValidationRule() : new HttpPostValidationRule(); + /// options.ValidationRules = DocumentValidator.CoreRules.Append(rule); + /// options.CachedDocumentValidationRules = new[] { rule }; + /// + ///
+ protected abstract Task ExecuteRequestAsync(HttpContext context, GraphQLRequest request, IServiceProvider serviceProvider, IDictionary userContext); - protected virtual Task HandleInvalidContentTypeErrorAsync(HttpContext context) - => WriteErrorResponseAsync(context, $"Invalid 'Content-Type' header: non-supported media type '{context.Request.ContentType}'. Must be of '{MediaType.JSON}', '{MediaType.GRAPH_QL}' or '{MediaType.FORM}'. {DOCS_URL}", HttpStatusCode.UnsupportedMediaType); + /// + /// Builds the user context based on a . + ///

+ /// Note that for batch or WebSocket requests, the user context is created once + /// and re-used for each GraphQL request or data event that applies to the same + /// . + ///

+ /// To tailor the user context individually for each request, call + /// + /// to set or modify the user context, pulling the HTTP context from + /// via + /// if needed. + ///

+ /// By default this method pulls the registered , + /// if any, within the service scope and executes it to build the user context. + /// In this manner, both scoped and singleton + /// instances are supported, although singleton instances are recommended. + ///
+ protected virtual async ValueTask> BuildUserContextAsync(HttpContext context) + { + var userContextBuilder = context.RequestServices.GetService(); + var userContext = userContextBuilder == null + ? new Dictionary() + : await userContextBuilder.BuildUserContextAsync(context); + return userContext; + } - protected virtual Task HandleInvalidHttpMethodErrorAsync(HttpContext context) - => WriteErrorResponseAsync(context, $"Invalid HTTP method. Only GET and POST are supported. {DOCS_URL}", HttpStatusCode.MethodNotAllowed); + /// + /// Writes the specified object (usually a GraphQL response) as JSON to the HTTP response stream. + /// + protected virtual Task WriteJsonResponseAsync(HttpContext context, HttpStatusCode httpStatusCode, TResult result) + { + context.Response.ContentType = MEDIATYPE_JSON; + context.Response.StatusCode = (int)httpStatusCode; - protected virtual Task ExecuteRequestAsync( - GraphQLRequest gqlRequest, - IDictionary userContext, - IDocumentExecuter executer, - IServiceProvider requestServices, - CancellationToken token) - => executer.ExecuteAsync(new ExecutionOptions - { - Query = gqlRequest.Query, - OperationName = gqlRequest.OperationName, - Variables = gqlRequest.Variables, - Extensions = gqlRequest.Extensions, - UserContext = userContext, - RequestServices = requestServices, - CancellationToken = token - }); - - protected virtual CancellationToken GetCancellationToken(HttpContext context) => context.RequestAborted; - - protected virtual Task RequestExecutingAsync(GraphQLRequest request, int? indexInBatch = null) - { - // nothing to do in this middleware - return Task.CompletedTask; - } + return _serializer.WriteAsync(context.Response.Body, result, context.RequestAborted); + } - protected virtual Task RequestExecutedAsync(in GraphQLRequestExecutionResult requestExecutionResult) + /****** WebSocket support ********* + + /// + /// Handles a WebSocket connection request. + /// + protected virtual async Task HandleWebSocketAsync(HttpContext context, RequestDelegate next) + { + if (_webSocketHandlers == null || !_webSocketHandlers.Any()) { - // nothing to do in this middleware - return Task.CompletedTask; + await next(context); + return; } - protected virtual Task WriteErrorResponseAsync(HttpContext context, string errorMessage, HttpStatusCode httpStatusCode) + string selectedProtocol; + IWebSocketHandler selectedHandler; + // select a sub-protocol, preferring the first sub-protocol requested by the client + foreach (var protocol in context.WebSockets.WebSocketRequestedProtocols) { - var result = new ExecutionResult + foreach (var handler in _webSocketHandlers) { - Errors = new ExecutionErrors + if (handler.SupportedSubProtocols.Contains(protocol)) { - new ExecutionError(errorMessage) + selectedProtocol = protocol; + selectedHandler = handler; + goto MatchedHandler; } - }; + } + } - context.Response.ContentType = "application/json"; - context.Response.StatusCode = (int)httpStatusCode; + await HandleWebSocketSubProtocolNotSupportedAsync(context, next); + return; - return _serializer.WriteAsync(context.Response.Body, result, GetCancellationToken(context)); - } + MatchedHandler: + var socket = await context.WebSockets.AcceptWebSocketAsync(selectedProtocol); - protected virtual Task WriteResponseAsync(HttpResponse httpResponse, IGraphQLSerializer serializer, CancellationToken cancellationToken, TResult result) + if (socket.SubProtocol != selectedProtocol) { - httpResponse.ContentType = "application/json"; - httpResponse.StatusCode = 200; // OK - - return serializer.WriteAsync(httpResponse.Body, result, cancellationToken); + await socket.CloseAsync( + WebSocketCloseStatus.ProtocolError, + $"Invalid sub-protocol; expected '{selectedProtocol}'", + context.RequestAborted); + return; } - private const string QUERY_KEY = "query"; - private const string VARIABLES_KEY = "variables"; - private const string EXTENSIONS_KEY = "extensions"; - private const string OPERATION_NAME_KEY = "operationName"; + // Prepare user context + var userContext = await BuildUserContextAsync(context); + // Connect, then wait until the websocket has disconnected (and all subscriptions ended) + await selectedHandler.ExecuteAsync(context, socket, selectedProtocol, userContext); + } - private GraphQLRequest DeserializeFromQueryString(IQueryCollection queryCollection) => new GraphQLRequest - { - Query = queryCollection.TryGetValue(QUERY_KEY, out var queryValues) ? queryValues[0] : null, - Variables = queryCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize(variablesValues[0]) : null, - Extensions = queryCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize(extensionsValues[0]) : null, - OperationName = queryCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null - }; + *****************************/ + + /// + /// Writes a '401 Access denied.' message to the output when the user is not authenticated. + /// + protected virtual Task HandleNotAuthenticatedAsync(HttpContext context, RequestDelegate next) + => WriteErrorResponseAsync(context, HttpStatusCode.Unauthorized, new AccessDeniedError("schema")); - private GraphQLRequest DeserializeFromFormBody(IFormCollection formCollection) => new GraphQLRequest + /// + /// Writes a '401 Access denied.' message to the output when the user fails the role checks. + /// + protected virtual Task HandleNotAuthorizedRoleAsync(HttpContext context, RequestDelegate next) + => WriteErrorResponseAsync(context, HttpStatusCode.Unauthorized, new AccessDeniedError("schema") { RolesRequired = Options.AuthorizedRoles }); + + /// + /// Writes a '401 Access denied.' message to the output when the user fails the policy check. + /// + protected virtual Task HandleNotAuthorizedPolicyAsync(HttpContext context, RequestDelegate next, AuthorizationResult authorizationResult) + => WriteErrorResponseAsync(context, HttpStatusCode.Unauthorized, new AccessDeniedError("schema") { PolicyRequired = Options.AuthorizedPolicy, PolicyAuthorizationResult = authorizationResult }); + + /// + /// Writes a '400 JSON body text could not be parsed.' message to the output. + /// Return to rethrow the exception or + /// if it has been handled. + /// + protected virtual async ValueTask HandleDeserializationErrorAsync(HttpContext context, RequestDelegate next, Exception exception) + { + await WriteErrorResponseAsync(context, HttpStatusCode.BadRequest, new JsonInvalidError(exception)); + return true; + } + + /// + /// Writes a '400 Batched requests are not supported.' message to the output. + /// + protected virtual Task HandleBatchedRequestsNotSupportedAsync(HttpContext context, RequestDelegate next) + => WriteErrorResponseAsync(context, HttpStatusCode.BadRequest, new BatchedRequestsNotSupportedError()); + + /// + /// Writes a '400 Invalid WebSocket sub-protocol.' message to the output. + /// + protected virtual Task HandleWebSocketSubProtocolNotSupportedAsync(HttpContext context, RequestDelegate next) + => WriteErrorResponseAsync(context, HttpStatusCode.BadRequest, new WebSocketSubProtocolNotSupportedError(context.WebSockets.WebSocketRequestedProtocols)); + + /// + /// Writes a '400 GraphQL query is missing.' message to the output. + /// + protected virtual Task HandleNoQueryErrorAsync(HttpContext context, RequestDelegate next) + => WriteErrorResponseAsync(context, Options.ValidationErrorsReturnBadRequest ? HttpStatusCode.BadRequest : HttpStatusCode.OK, new QueryMissingError()); + + /// + /// Writes a '415 Invalid Content-Type header: could not be parsed.' message to the output. + /// + protected virtual Task HandleContentTypeCouldNotBeParsedErrorAsync(HttpContext context, RequestDelegate next) + => WriteErrorResponseAsync(context, HttpStatusCode.UnsupportedMediaType, new InvalidContentTypeError($"value '{context.Request.ContentType}' could not be parsed.")); + + /// + /// Writes a '415 Invalid Content-Type header: non-supported type.' message to the output. + /// + protected virtual Task HandleInvalidContentTypeErrorAsync(HttpContext context, RequestDelegate next) + => WriteErrorResponseAsync(context, HttpStatusCode.UnsupportedMediaType, new InvalidContentTypeError($"non-supported media type '{context.Request.ContentType}'. Must be '{MEDIATYPE_JSON}', '{MEDIATYPE_GRAPHQL}' or a form body.")); + + /// + /// Indicates that an unsupported HTTP method was requested. + /// Executes the next delegate in the chain by default. + /// + protected virtual Task HandleInvalidHttpMethodErrorAsync(HttpContext context, RequestDelegate next) + { + //context.Response.Headers["Allow"] = Options.HandleGet && Options.HandlePost ? "GET, POST" : Options.HandleGet ? "GET" : Options.HandlePost ? "POST" : ""; + //return WriteErrorResponseAsync(context, $"Invalid HTTP method.{(Options.HandleGet || Options.HandlePost ? $" Only {(Options.HandleGet && Options.HandlePost ? "GET and POST are" : Options.HandleGet ? "GET is" : "POST is")} supported." : "")}", HttpStatusCode.MethodNotAllowed); + return next(context); + } + + /// + /// Writes the specified error message as a JSON-formatted GraphQL response, with the specified HTTP status code. + /// + protected virtual Task WriteErrorResponseAsync(HttpContext context, HttpStatusCode httpStatusCode, string errorMessage) + => WriteErrorResponseAsync(context, httpStatusCode, new ExecutionError(errorMessage)); + + /// + /// Writes the specified error as a JSON-formatted GraphQL response, with the specified HTTP status code. + /// + protected virtual Task WriteErrorResponseAsync(HttpContext context, HttpStatusCode httpStatusCode, ExecutionError executionError) + { + var result = new ExecutionResult { - Query = formCollection.TryGetValue(QUERY_KEY, out var queryValues) ? queryValues[0] : null, - Variables = formCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize(variablesValues[0]) : null, - Extensions = formCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize(extensionsValues[0]) : null, - OperationName = formCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null + Errors = new ExecutionErrors { + executionError + }, }; - private async Task DeserializeFromGraphBodyAsync(Stream bodyStream) - { - // In this case, the query is the raw value in the POST body + return WriteJsonResponseAsync(context, httpStatusCode, result); + } - // Do not explicitly or implicitly (via using, etc.) call dispose because StreamReader will dispose inner stream. - // This leads to the inability to use the stream further by other consumers/middlewares of the request processing - // pipeline. In fact, it is absolutely not dangerous not to dispose StreamReader as it does not perform any useful - // work except for the disposing inner stream. - string query = await new StreamReader(bodyStream).ReadToEndAsync(); + private GraphQLRequest DeserializeFromQueryString(IQueryCollection queryCollection) => new GraphQLRequest + { + Query = queryCollection.TryGetValue(QUERY_KEY, out var queryValues) ? queryValues[0] : null!, + Variables = Options.ReadVariablesFromQueryString && queryCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize(variablesValues[0]) : null, + Extensions = Options.ReadExtensionsFromQueryString && queryCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize(extensionsValues[0]) : null, + OperationName = queryCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null, + }; - return new GraphQLRequest { Query = query }; // application/graphql MediaType supports only query text - } + private GraphQLRequest DeserializeFromFormBody(IFormCollection formCollection) => new GraphQLRequest + { + Query = formCollection.TryGetValue(QUERY_KEY, out var queryValues) ? queryValues[0] : null!, + Variables = formCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize(variablesValues[0]) : null, + Extensions = formCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize(extensionsValues[0]) : null, + OperationName = formCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null, + }; + + /// + /// Reads body of content type: application/graphql + /// + private static async Task DeserializeFromGraphBodyAsync(Stream bodyStream) + { + // do not close underlying HTTP connection + using var streamReader = new StreamReader(bodyStream, leaveOpen: true); + + // read query text + string query = await streamReader.ReadToEndAsync(); + + // return request + return new GraphQLRequest { Query = query }; + } #if NET5_0_OR_GREATER - private static bool TryGetEncoding(string charset, out System.Text.Encoding encoding) - { - encoding = null; + private static bool TryGetEncoding(string? charset, out System.Text.Encoding encoding) + { + encoding = null!; - if (string.IsNullOrEmpty(charset)) - return true; + if (string.IsNullOrEmpty(charset)) + return true; - try + try + { + // Remove at most a single set of quotes. + if (charset.Length > 2 && charset[0] == '\"' && charset[^1] == '\"') { - // Remove at most a single set of quotes. - if (charset.Length > 2 && charset[0] == '\"' && charset[^1] == '\"') - { - encoding = System.Text.Encoding.GetEncoding(charset[1..^1]); - } - else - { - encoding = System.Text.Encoding.GetEncoding(charset); - } + encoding = System.Text.Encoding.GetEncoding(charset[1..^1]); } - catch (ArgumentException) + else { - return false; + encoding = System.Text.Encoding.GetEncoding(charset); } - - return true; } -#endif + catch (ArgumentException) + { + return false; + } + + return true; } +#endif } diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs b/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs new file mode 100644 index 00000000..43821334 --- /dev/null +++ b/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs @@ -0,0 +1,107 @@ +#nullable enable + +using System.Security.Claims; +using System.Security.Principal; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace GraphQL.Server.Transports.AspNetCore; + +/// +/// Configuration options for . +/// +public class GraphQLHttpMiddlewareOptions +{ + /// + /// Enables handling of GET requests. + /// + public bool HandleGet { get; set; } = true; + + /// + /// Enables handling of POST requests, including form submissions, JSON-formatted requests and raw query requests. + /// + public bool HandlePost { get; set; } = true; + + /********** WebSockets support ******** + + /// + /// Enables handling of WebSockets requests. + ///

+ /// Requires calling + /// to initialize the WebSocket pipeline within the ASP.NET Core framework. + ///
+ public bool HandleWebSockets { get; set; } = true; + + ******************/ + + /// + /// Enables handling of batched GraphQL requests for POST requests when formatted as JSON. + /// + public bool EnableBatchedRequests { get; set; } = true; + + /// + /// Enables parallel execution of batched GraphQL requests. + /// + public bool BatchedRequestsExecuteInParallel { get; set; } = true; + + /// + /// When enabled, GraphQL requests with validation errors + /// have the HTTP status code set to 400 Bad Request. + /// GraphQL requests with execution errors are unaffected. + ///

+ /// Does not apply to batched or WebSocket requests. + ///
+ public bool ValidationErrorsReturnBadRequest { get; set; } = true; + + /// + /// Enables parsing the query string on POST requests. + /// If enabled, the query string properties override those in the body of the request. + /// + public bool ReadQueryStringOnPost { get; set; } = true; + + /// + /// Enables reading variables from the query string. + /// Variables are interpreted as JSON and deserialized before being + /// provided to the . + /// + public bool ReadVariablesFromQueryString { get; set; } = true; + + /// + /// Enables reading extensions from the query string. + /// Extensions are interpreted as JSON and deserialized before being + /// provided to the . + /// + public bool ReadExtensionsFromQueryString { get; set; } = true; + + /// + /// If set, requires that return + /// for the user within + /// prior to executing the GraphQL request or accepting the WebSocket connection. + /// + public bool AuthorizationRequired { get; set; } + + /// + /// Requires that return + /// for the user within + /// for at least one role in the list prior to executing the GraphQL request or accepting + /// the WebSocket connection. If no roles are specified, authorization is not checked. + /// + public List AuthorizedRoles { get; set; } = new(); + + /// + /// If set, requires that + /// return a successful result for the user within + /// for the specified policy before executing the GraphQL + /// request or accepting the WebSocket connection. + /// + public string? AuthorizedPolicy { get; set; } + + /************ WebSockets support ********** + + /// + /// Returns an options class for WebSocket connections. + /// + public GraphQLWebSocketOptions WebSockets { get; set; } = new(); + + ****************************/ +} diff --git a/src/Transports.AspNetCore/HttpGetValidationRule.cs b/src/Transports.AspNetCore/HttpGetValidationRule.cs new file mode 100644 index 00000000..7720c617 --- /dev/null +++ b/src/Transports.AspNetCore/HttpGetValidationRule.cs @@ -0,0 +1,23 @@ +#nullable enable + +using GraphQL.Server.Transports.AspNetCore.Errors; +using GraphQL.Validation; +using GraphQLParser.AST; + +namespace GraphQL.Server.Transports.AspNetCore; + +/// +/// Validates that HTTP GET requests only execute queries; not mutations or subscriptions. +/// +public sealed class HttpGetValidationRule : IValidationRule +{ + /// + public ValueTask ValidateAsync(ValidationContext context) + { + if (context.Operation.Operation != OperationType.Query) + { + context.ReportError(new HttpMethodValidationError(context.Document.Source, context.Operation, "Only query operations allowed for GET requests.")); + } + return default; + } +} diff --git a/src/Transports.AspNetCore/HttpPostValidationRule.cs b/src/Transports.AspNetCore/HttpPostValidationRule.cs new file mode 100644 index 00000000..d5c10c6d --- /dev/null +++ b/src/Transports.AspNetCore/HttpPostValidationRule.cs @@ -0,0 +1,23 @@ +#nullable enable + +using GraphQL.Server.Transports.AspNetCore.Errors; +using GraphQL.Validation; +using GraphQLParser.AST; + +namespace GraphQL.Server.Transports.AspNetCore; + +/// +/// Validates that HTTP POST requests do not execute subscriptions. +/// +public sealed class HttpPostValidationRule : IValidationRule +{ + /// + public ValueTask ValidateAsync(ValidationContext context) + { + if (context.Operation.Operation == OperationType.Subscription) + { + context.ReportError(new HttpMethodValidationError(context.Document.Source, context.Operation, "Subscription operations are not supported for POST requests.")); + } + return default; + } +} diff --git a/src/Transports.AspNetCore/IUserContextBuilder.cs b/src/Transports.AspNetCore/IUserContextBuilder.cs index 34c7c836..be027a81 100644 --- a/src/Transports.AspNetCore/IUserContextBuilder.cs +++ b/src/Transports.AspNetCore/IUserContextBuilder.cs @@ -12,6 +12,6 @@ public interface IUserContextBuilder /// /// The for the current request /// Returns the UserContext - Task> BuildUserContext(HttpContext httpContext); + ValueTask> BuildUserContextAsync(HttpContext httpContext); } } diff --git a/src/Transports.AspNetCore/UserContextBuilder.cs b/src/Transports.AspNetCore/UserContextBuilder.cs index a409f458..1c82d002 100644 --- a/src/Transports.AspNetCore/UserContextBuilder.cs +++ b/src/Transports.AspNetCore/UserContextBuilder.cs @@ -1,25 +1,37 @@ +#nullable enable + using Microsoft.AspNetCore.Http; -namespace GraphQL.Server.Transports.AspNetCore -{ - public class UserContextBuilder : IUserContextBuilder - where TUserContext : IDictionary - { - private readonly Func> _func; +namespace GraphQL.Server.Transports.AspNetCore; - public UserContextBuilder(Func> func) - { - _func = func ?? throw new ArgumentNullException(nameof(func)); - } +/// +/// Represents a user context builder based on a delegate. +/// +public class UserContextBuilder : IUserContextBuilder + where TUserContext : IDictionary +{ + private readonly Func> _func; - public UserContextBuilder(Func func) - { - if (func == null) - throw new ArgumentNullException(nameof(func)); + /// + /// Initializes a new instance with the specified delegate. + /// + public UserContextBuilder(Func> func) + { + _func = func ?? throw new ArgumentNullException(nameof(func)); + } - _func = x => Task.FromResult(func(x)); - } + /// + /// Initializes a new instance with the specified delegate. + /// + public UserContextBuilder(Func func) + { + if (func == null) + throw new ArgumentNullException(nameof(func)); - public async Task> BuildUserContext(HttpContext httpContext) => await _func(httpContext); + _func = x => new(func(x)); } + + /// + public async ValueTask> BuildUserContextAsync(HttpContext context) + => await _func(context); } diff --git a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt index 8191d441..368aed11 100644 --- a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -1,43 +1,80 @@ namespace GraphQL.Server { - public static class GraphQLBuilderMiddlewareExtensions - { - public static GraphQL.DI.IGraphQLBuilder AddHttpMiddleware(this GraphQL.DI.IGraphQLBuilder builder) - where TSchema : GraphQL.Types.ISchema { } - public static GraphQL.DI.IGraphQLBuilder AddHttpMiddleware(this GraphQL.DI.IGraphQLBuilder builder) - where TSchema : GraphQL.Types.ISchema - where TMiddleware : GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddleware { } - } public static class GraphQLBuilderUserContextExtensions { public static GraphQL.DI.IGraphQLBuilder AddDefaultEndpointSelectorPolicy(this GraphQL.DI.IGraphQLBuilder builder) { } public static GraphQL.DI.IGraphQLBuilder AddUserContextBuilder(this GraphQL.DI.IGraphQLBuilder builder) where TUserContextBuilder : class, GraphQL.Server.Transports.AspNetCore.IUserContextBuilder { } public static GraphQL.DI.IGraphQLBuilder AddUserContextBuilder(this GraphQL.DI.IGraphQLBuilder builder, System.Func> creator) - where TUserContext : class, System.Collections.Generic.IDictionary { } + where TUserContext : class, System.Collections.Generic.IDictionary { } public static GraphQL.DI.IGraphQLBuilder AddUserContextBuilder(this GraphQL.DI.IGraphQLBuilder builder, System.Func creator) - where TUserContext : class, System.Collections.Generic.IDictionary { } + where TUserContext : class, System.Collections.Generic.IDictionary { } } } namespace GraphQL.Server.Transports.AspNetCore { - public class GraphQLHttpMiddleware : Microsoft.AspNetCore.Http.IMiddleware + public static class AuthorizationHelper + { + public static System.Threading.Tasks.ValueTask AuthorizeAsync(GraphQL.Server.Transports.AspNetCore.AuthorizationParameters options, TState state) { } + } + public struct AuthorizationParameters + { + public AuthorizationParameters(Microsoft.AspNetCore.Http.HttpContext httpContext, GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions middlewareOptions, System.Func? onNotAuthenticated, System.Func? onNotAuthorizedRole, System.Func? onNotAuthorizedPolicy) { } + public bool AuthorizationRequired { get; set; } + public string? AuthorizedPolicy { get; set; } + public System.Collections.Generic.List? AuthorizedRoles { get; set; } + public Microsoft.AspNetCore.Http.HttpContext HttpContext { get; set; } + public System.Func? OnNotAuthenticated { get; set; } + public System.Func? OnNotAuthorizedPolicy { get; set; } + public System.Func? OnNotAuthorizedRole { get; set; } + } + public abstract class GraphQLHttpMiddleware + { + public GraphQLHttpMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, GraphQL.IGraphQLTextSerializer serializer, GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions options) { } + protected GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions Options { get; } + protected virtual System.Threading.Tasks.ValueTask> BuildUserContextAsync(Microsoft.AspNetCore.Http.HttpContext context) { } + protected abstract System.Threading.Tasks.Task ExecuteRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest request, System.IServiceProvider serviceProvider, System.Collections.Generic.IDictionary userContext); + protected abstract System.Threading.Tasks.Task ExecuteScopedRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest request, System.Collections.Generic.IDictionary userContext); + protected virtual System.Threading.Tasks.ValueTask HandleAuthorizeAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } + protected virtual System.Threading.Tasks.ValueTask HandleAuthorizeWebSocketConnectionAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } + protected virtual System.Threading.Tasks.Task HandleBatchRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next, System.Collections.Generic.IList gqlRequests) { } + protected virtual System.Threading.Tasks.Task HandleBatchedRequestsNotSupportedAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } + protected virtual System.Threading.Tasks.Task HandleContentTypeCouldNotBeParsedErrorAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } + protected virtual System.Threading.Tasks.ValueTask HandleDeserializationErrorAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next, System.Exception exception) { } + protected virtual System.Threading.Tasks.Task HandleInvalidContentTypeErrorAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } + protected virtual System.Threading.Tasks.Task HandleInvalidHttpMethodErrorAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } + protected virtual System.Threading.Tasks.Task HandleNoQueryErrorAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } + protected virtual System.Threading.Tasks.Task HandleNotAuthenticatedAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } + protected virtual System.Threading.Tasks.Task HandleNotAuthorizedPolicyAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.AspNetCore.Authorization.AuthorizationResult authorizationResult) { } + protected virtual System.Threading.Tasks.Task HandleNotAuthorizedRoleAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } + protected virtual System.Threading.Tasks.Task HandleRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next, GraphQL.Transport.GraphQLRequest gqlRequest) { } + protected virtual System.Threading.Tasks.Task HandleWebSocketSubProtocolNotSupportedAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } + public virtual System.Threading.Tasks.Task InvokeAsync(Microsoft.AspNetCore.Http.HttpContext context) { } + protected virtual System.Threading.Tasks.Task WriteErrorResponseAsync(Microsoft.AspNetCore.Http.HttpContext context, System.Net.HttpStatusCode httpStatusCode, GraphQL.ExecutionError executionError) { } + protected virtual System.Threading.Tasks.Task WriteErrorResponseAsync(Microsoft.AspNetCore.Http.HttpContext context, System.Net.HttpStatusCode httpStatusCode, string errorMessage) { } + protected virtual System.Threading.Tasks.Task WriteJsonResponseAsync(Microsoft.AspNetCore.Http.HttpContext context, System.Net.HttpStatusCode httpStatusCode, TResult result) { } + } + public class GraphQLHttpMiddlewareOptions + { + public GraphQLHttpMiddlewareOptions() { } + public bool AuthorizationRequired { get; set; } + public string? AuthorizedPolicy { get; set; } + public System.Collections.Generic.List AuthorizedRoles { get; set; } + public bool BatchedRequestsExecuteInParallel { get; set; } + public bool EnableBatchedRequests { get; set; } + public bool HandleGet { get; set; } + public bool HandlePost { get; set; } + public bool ReadExtensionsFromQueryString { get; set; } + public bool ReadQueryStringOnPost { get; set; } + public bool ReadVariablesFromQueryString { get; set; } + public bool ValidationErrorsReturnBadRequest { get; set; } + } + public class GraphQLHttpMiddleware : GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddleware where TSchema : GraphQL.Types.ISchema { - public GraphQLHttpMiddleware(GraphQL.IGraphQLTextSerializer serializer) { } - protected virtual System.Threading.Tasks.Task ExecuteRequestAsync(GraphQL.Transport.GraphQLRequest gqlRequest, System.Collections.Generic.IDictionary userContext, GraphQL.IDocumentExecuter executer, System.IServiceProvider requestServices, System.Threading.CancellationToken token) { } - protected virtual System.Threading.CancellationToken GetCancellationToken(Microsoft.AspNetCore.Http.HttpContext context) { } - protected virtual System.Threading.Tasks.Task HandleContentTypeCouldNotBeParsedErrorAsync(Microsoft.AspNetCore.Http.HttpContext context) { } - protected virtual System.Threading.Tasks.ValueTask HandleDeserializationErrorAsync(Microsoft.AspNetCore.Http.HttpContext context, System.Exception ex) { } - protected virtual System.Threading.Tasks.Task HandleInvalidContentTypeErrorAsync(Microsoft.AspNetCore.Http.HttpContext context) { } - protected virtual System.Threading.Tasks.Task HandleInvalidHttpMethodErrorAsync(Microsoft.AspNetCore.Http.HttpContext context) { } - protected virtual System.Threading.Tasks.Task HandleNoQueryErrorAsync(Microsoft.AspNetCore.Http.HttpContext context) { } - protected virtual System.Threading.Tasks.Task HandleRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next, System.Collections.Generic.IDictionary userContext, System.Collections.Generic.IList bodyGQLBatchRequest, GraphQL.Transport.GraphQLRequest gqlRequest, GraphQL.IDocumentExecuter executer, System.Threading.CancellationToken cancellationToken) { } - public virtual System.Threading.Tasks.Task InvokeAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } - protected virtual System.Threading.Tasks.Task RequestExecutedAsync(in GraphQL.Server.Transports.AspNetCore.GraphQLRequestExecutionResult requestExecutionResult) { } - protected virtual System.Threading.Tasks.Task RequestExecutingAsync(GraphQL.Transport.GraphQLRequest request, int? indexInBatch = default) { } - protected virtual System.Threading.Tasks.Task WriteErrorResponseAsync(Microsoft.AspNetCore.Http.HttpContext context, string errorMessage, System.Net.HttpStatusCode httpStatusCode) { } - protected virtual System.Threading.Tasks.Task WriteResponseAsync(Microsoft.AspNetCore.Http.HttpResponse httpResponse, GraphQL.IGraphQLSerializer serializer, System.Threading.CancellationToken cancellationToken, TResult result) { } + protected GraphQLHttpMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, GraphQL.IGraphQLTextSerializer serializer, GraphQL.IDocumentExecuter documentExecuter, Microsoft.Extensions.DependencyInjection.IServiceScopeFactory serviceScopeFactory, GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions options) { } + protected override System.Threading.Tasks.Task ExecuteRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest request, System.IServiceProvider serviceProvider, System.Collections.Generic.IDictionary userContext) { } + protected override System.Threading.Tasks.Task ExecuteScopedRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest request, System.Collections.Generic.IDictionary userContext) { } } public readonly struct GraphQLRequestExecutionResult { @@ -47,9 +84,19 @@ namespace GraphQL.Server.Transports.AspNetCore public GraphQL.Transport.GraphQLRequest Request { get; } public GraphQL.ExecutionResult Result { get; } } + public sealed class HttpGetValidationRule : GraphQL.Validation.IValidationRule + { + public HttpGetValidationRule() { } + public System.Threading.Tasks.ValueTask ValidateAsync(GraphQL.Validation.ValidationContext context) { } + } + public sealed class HttpPostValidationRule : GraphQL.Validation.IValidationRule + { + public HttpPostValidationRule() { } + public System.Threading.Tasks.ValueTask ValidateAsync(GraphQL.Validation.ValidationContext context) { } + } public interface IUserContextBuilder { - System.Threading.Tasks.Task> BuildUserContext(Microsoft.AspNetCore.Http.HttpContext httpContext); + System.Threading.Tasks.ValueTask> BuildUserContextAsync(Microsoft.AspNetCore.Http.HttpContext httpContext); } public static class MediaType { @@ -58,38 +105,80 @@ namespace GraphQL.Server.Transports.AspNetCore public const string JSON = "application/json"; } public class UserContextBuilder : GraphQL.Server.Transports.AspNetCore.IUserContextBuilder - where TUserContext : System.Collections.Generic.IDictionary + where TUserContext : System.Collections.Generic.IDictionary { - public UserContextBuilder(System.Func> func) { } + public UserContextBuilder(System.Func> func) { } public UserContextBuilder(System.Func func) { } - public System.Threading.Tasks.Task> BuildUserContext(Microsoft.AspNetCore.Http.HttpContext httpContext) { } + public System.Threading.Tasks.ValueTask> BuildUserContextAsync(Microsoft.AspNetCore.Http.HttpContext context) { } + } +} +namespace GraphQL.Server.Transports.AspNetCore.Errors +{ + public class AccessDeniedError : GraphQL.Validation.ValidationError + { + public AccessDeniedError(string resource) { } + public AccessDeniedError(string resource, GraphQLParser.ROM originalQuery, params GraphQLParser.AST.ASTNode[] nodes) { } + public Microsoft.AspNetCore.Authorization.AuthorizationResult? PolicyAuthorizationResult { get; set; } + public string? PolicyRequired { get; set; } + public System.Collections.Generic.List? RolesRequired { get; set; } + } + public class BatchedRequestsNotSupportedError : GraphQL.Server.Transports.AspNetCore.Errors.RequestError + { + public BatchedRequestsNotSupportedError() { } + } + public class HttpMethodValidationError : GraphQL.Validation.ValidationError + { + public HttpMethodValidationError(GraphQLParser.ROM originalQuery, GraphQLParser.AST.ASTNode node, string message) { } + } + public class InvalidContentTypeError : GraphQL.Server.Transports.AspNetCore.Errors.RequestError + { + public InvalidContentTypeError() { } + public InvalidContentTypeError(string message) { } + } + public class JsonInvalidError : GraphQL.Server.Transports.AspNetCore.Errors.RequestError + { + public JsonInvalidError() { } + public JsonInvalidError(System.Exception innerException) { } + } + public class QueryMissingError : GraphQL.Server.Transports.AspNetCore.Errors.RequestError + { + public QueryMissingError() { } + } + public class RequestError : GraphQL.ExecutionError + { + public RequestError(string message) { } + public RequestError(string message, System.Exception? innerException) { } + } + public class WebSocketSubProtocolNotSupportedError : GraphQL.Server.Transports.AspNetCore.Errors.RequestError + { + public WebSocketSubProtocolNotSupportedError(System.Collections.Generic.IEnumerable requestedSubProtocols) { } } } namespace Microsoft.AspNetCore.Builder { + public class GraphQLEndpointConventionBuilder : Microsoft.AspNetCore.Builder.IEndpointConventionBuilder + { + public void Add(System.Action convention) { } + } public static class GraphQLHttpApplicationBuilderExtensions { - public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseGraphQL(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder, Microsoft.AspNetCore.Http.PathString path) + public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseGraphQL(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder, Microsoft.AspNetCore.Http.PathString path, System.Action? configureMiddleware = null) { } + public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseGraphQL(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder, string path = "/graphql", System.Action? configureMiddleware = null) { } + public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseGraphQL(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder, Microsoft.AspNetCore.Http.PathString path, System.Action? configureMiddleware = null) where TSchema : GraphQL.Types.ISchema { } - public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseGraphQL(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder, string path = "/graphql") + public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseGraphQL(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder, Microsoft.AspNetCore.Http.PathString path, params object[] args) + where TMiddleware : GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddleware { } + public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseGraphQL(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder, string path = "/graphql", System.Action? configureMiddleware = null) where TSchema : GraphQL.Types.ISchema { } - public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseGraphQL(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder, Microsoft.AspNetCore.Http.PathString path) - where TSchema : GraphQL.Types.ISchema - where TMiddleware : GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddleware { } - public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseGraphQL(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder, string path = "/graphql") - where TSchema : GraphQL.Types.ISchema - where TMiddleware : GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddleware { } - } - public class GraphQLHttpEndpointConventionBuilder : Microsoft.AspNetCore.Builder.IEndpointConventionBuilder - { - public void Add(System.Action convention) { } + public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseGraphQL(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder, string path = "/graphql", params object[] args) + where TMiddleware : GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddleware { } } public static class GraphQLHttpEndpointRouteBuilderExtensions { - public static Microsoft.AspNetCore.Builder.GraphQLHttpEndpointConventionBuilder MapGraphQL(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern = "graphql") + public static Microsoft.AspNetCore.Builder.GraphQLEndpointConventionBuilder MapGraphQL(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern = "graphql", System.Action? configureMiddleware = null) { } + public static Microsoft.AspNetCore.Builder.GraphQLEndpointConventionBuilder MapGraphQL(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern = "graphql", System.Action? configureMiddleware = null) where TSchema : GraphQL.Types.ISchema { } - public static Microsoft.AspNetCore.Builder.GraphQLHttpEndpointConventionBuilder MapGraphQL(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern = "graphql") - where TSchema : GraphQL.Types.ISchema - where TMiddleware : GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddleware { } + public static Microsoft.AspNetCore.Builder.GraphQLEndpointConventionBuilder MapGraphQL(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern = "graphql", params object[] args) + where TMiddleware : GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddleware { } } } \ No newline at end of file diff --git a/tests/Samples.Server.Tests/ResponseTests.cs b/tests/Samples.Server.Tests/ResponseTests.cs index e5208475..0778e680 100644 --- a/tests/Samples.Server.Tests/ResponseTests.cs +++ b/tests/Samples.Server.Tests/ResponseTests.cs @@ -71,15 +71,18 @@ public async Task Batched_Query_Should_Return_Single_Result_As_Array() [Theory] [MemberData(nameof(WrongQueryData))] public async Task Wrong_Query_Should_Return_Error(HttpMethod httpMethod, HttpContent httpContent, - HttpStatusCode expectedStatusCode, string expectedErrorMsg) + HttpStatusCode expectedStatusCode, string expectedErrorMsg, string code) { var response = await SendRequestAsync(httpMethod, httpContent); - string expected = @"{""errors"":[{""message"":""" + expectedErrorMsg + @"""}]}"; + string expected = expectedErrorMsg == null ? null : @"{""errors"":[{""message"":""" + expectedErrorMsg + @""",""extensions"":{""code"":""" + code + @""",""codes"":[""" + code + @"""]}}]}"; response.StatusCode.ShouldBe(expectedStatusCode); string content = await response.Content.ReadAsStringAsync(); - content.ShouldBeEquivalentJson(expected); + if (expected == null) + content.ShouldBe(""); + else + content.ShouldBeEquivalentJson(expected); } public static IEnumerable WrongQueryData => new object[][] @@ -89,8 +92,9 @@ public async Task Wrong_Query_Should_Return_Error(HttpMethod httpMethod, HttpCon { HttpMethod.Put, new StringContent(Serializer.ToJson(new GraphQLRequest { Query = "query { __schema { queryType { name } } }" }), Encoding.UTF8, "application/json"), - HttpStatusCode.MethodNotAllowed, - "Invalid HTTP method. Only GET and POST are supported. See: http://graphql.org/learn/serving-over-http/.", + HttpStatusCode.NotFound, + null, + null, }, // POST with unsupported mime type should be a unsupported media type @@ -99,7 +103,8 @@ public async Task Wrong_Query_Should_Return_Error(HttpMethod httpMethod, HttpCon HttpMethod.Post, new StringContent(Serializer.ToJson(new GraphQLRequest { Query = "query { __schema { queryType { name } } }" }), Encoding.UTF8, "something/unknown"), HttpStatusCode.UnsupportedMediaType, - "Invalid 'Content-Type' header: non-supported media type 'something/unknown; charset=utf-8'. Must be of 'application/json', 'application/graphql' or 'application/x-www-form-urlencoded'. See: http://graphql.org/learn/serving-over-http/." + @"Invalid 'Content-Type' header: non-supported media type 'something/unknown; charset=utf-8'. Must be 'application/json', 'application/graphql' or a form body.", + "INVALID_CONTENT_TYPE", }, // MediaTypeHeaderValue ctor throws exception @@ -118,7 +123,8 @@ public async Task Wrong_Query_Should_Return_Error(HttpMethod httpMethod, HttpCon HttpMethod.Post, new StringContent("Oops", Encoding.UTF8, "application/json"), HttpStatusCode.BadRequest, - "JSON body text could not be parsed. 'O' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0." + "JSON body text could not be parsed. 'O' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.", + "JSON_INVALID", }, // POST with JSON mime type that is invalid JSON should be a bad request @@ -127,7 +133,8 @@ public async Task Wrong_Query_Should_Return_Error(HttpMethod httpMethod, HttpCon HttpMethod.Post, new StringContent("{oops}", Encoding.UTF8, "application/json"), HttpStatusCode.BadRequest, - "JSON body text could not be parsed. 'o' is an invalid start of a property name. Expected a '\"'. Path: $ | LineNumber: 0 | BytePositionInLine: 1." + "JSON body text could not be parsed. 'o' is an invalid start of a property name. Expected a '\"'. Path: $ | LineNumber: 0 | BytePositionInLine: 1.", + "JSON_INVALID", }, // POST with JSON mime type that is null JSON should be a bad request @@ -136,7 +143,8 @@ public async Task Wrong_Query_Should_Return_Error(HttpMethod httpMethod, HttpCon HttpMethod.Post, new StringContent("null", Encoding.UTF8, "application/json"), HttpStatusCode.BadRequest, - "GraphQL query is missing." + "GraphQL query is missing.", + "QUERY_MISSING", }, // GET with an empty QueryString should be a bad request @@ -145,12 +153,12 @@ public async Task Wrong_Query_Should_Return_Error(HttpMethod httpMethod, HttpCon HttpMethod.Get, null, HttpStatusCode.BadRequest, - "GraphQL query is missing." + "GraphQL query is missing.", + "QUERY_MISSING", }, }; [Theory] - [InlineData(RequestType.Get)] [InlineData(RequestType.PostWithJson)] [InlineData(RequestType.PostWithGraph)] [InlineData(RequestType.PostWithForm)] @@ -167,7 +175,6 @@ public async Task Serializer_Should_Handle_Inline_Variables(RequestType requestT } [Theory] - [InlineData(RequestType.Get)] [InlineData(RequestType.PostWithJson)] [InlineData(RequestType.PostWithGraph)] [InlineData(RequestType.PostWithForm)] @@ -185,7 +192,6 @@ public async Task Serializer_Should_Handle_Variables(RequestType requestType) } [Theory] - [InlineData(RequestType.Get)] [InlineData(RequestType.PostWithJson)] [InlineData(RequestType.PostWithGraph)] [InlineData(RequestType.PostWithForm)] From e2e919606262a272e82ffcd404545d5c5f730355 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Thu, 14 Apr 2022 13:52:45 -0400 Subject: [PATCH 02/47] Add failing check for mutation for get --- tests/Samples.Server.Tests/ResponseTests.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Samples.Server.Tests/ResponseTests.cs b/tests/Samples.Server.Tests/ResponseTests.cs index 0778e680..e2673d14 100644 --- a/tests/Samples.Server.Tests/ResponseTests.cs +++ b/tests/Samples.Server.Tests/ResponseTests.cs @@ -68,6 +68,14 @@ public async Task Batched_Query_Should_Return_Single_Result_As_Array() response.ShouldBeEquivalentJson(@"[{""data"":{""__schema"":{""queryType"":{""name"":""ChatQuery""}}}}]", ignoreExtensions: true); } + [Fact] + public async Task Mutation_For_Get_Fails() + { + var response = await SendRequestAsync(new GraphQLRequest { Query = "mutation { __typename }" }, RequestType.Get); + + response.ShouldBe(@"{""errors"":[{""message"":""Only query operations allowed for GET requests."",""locations"":[{""line"":1,""column"":1}],""extensions"":{""code"":""HTTP_METHOD_VALIDATION"",""codes"":[""HTTP_METHOD_VALIDATION""]}}]}"); + } + [Theory] [MemberData(nameof(WrongQueryData))] public async Task Wrong_Query_Should_Return_Error(HttpMethod httpMethod, HttpContent httpContent, From d22a09fb72c42ec0d8b41fdd2750cdeab5455a12 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Thu, 14 Apr 2022 13:54:09 -0400 Subject: [PATCH 03/47] Update --- .../GraphQLRequestExecutionResult.cs | 45 ------------------- ....Server.Transports.AspNetCore.approved.txt | 8 ---- 2 files changed, 53 deletions(-) delete mode 100644 src/Transports.AspNetCore/GraphQLRequestExecutionResult.cs diff --git a/src/Transports.AspNetCore/GraphQLRequestExecutionResult.cs b/src/Transports.AspNetCore/GraphQLRequestExecutionResult.cs deleted file mode 100644 index 7623b9e0..00000000 --- a/src/Transports.AspNetCore/GraphQLRequestExecutionResult.cs +++ /dev/null @@ -1,45 +0,0 @@ -using GraphQL.Transport; - -namespace GraphQL.Server.Transports.AspNetCore -{ - /// - /// Represents the result of a GraphQL operation. Single GraphQL request may contain several operations, that is, be a batched request. - /// - public readonly struct GraphQLRequestExecutionResult - { - /// - /// Creates . - /// - /// Executed GraphQL request. - /// Result of execution. - /// Elapsed time. - /// Index of the executed request (starting with 0) in case of a batched request, otherwise . - public GraphQLRequestExecutionResult(GraphQLRequest request, ExecutionResult result, TimeSpan elapsed, int? indexInBatch = null) - { - Request = request; - Result = result; - Elapsed = elapsed; - IndexInBatch = indexInBatch; - } - - /// - /// Executed GraphQL request. - /// - public GraphQLRequest Request { get; } - - /// - /// Result of execution. - /// - public ExecutionResult Result { get; } - - /// - /// Elapsed time. - /// - public TimeSpan Elapsed { get; } - - /// - /// Index of the executed request (starting with 0) in case of a batched request, otherwise . - /// - public int? IndexInBatch { get; } - } -} diff --git a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt index 368aed11..1482fca0 100644 --- a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -76,14 +76,6 @@ namespace GraphQL.Server.Transports.AspNetCore protected override System.Threading.Tasks.Task ExecuteRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest request, System.IServiceProvider serviceProvider, System.Collections.Generic.IDictionary userContext) { } protected override System.Threading.Tasks.Task ExecuteScopedRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest request, System.Collections.Generic.IDictionary userContext) { } } - public readonly struct GraphQLRequestExecutionResult - { - public GraphQLRequestExecutionResult(GraphQL.Transport.GraphQLRequest request, GraphQL.ExecutionResult result, System.TimeSpan elapsed, int? indexInBatch = default) { } - public System.TimeSpan Elapsed { get; } - public int? IndexInBatch { get; } - public GraphQL.Transport.GraphQLRequest Request { get; } - public GraphQL.ExecutionResult Result { get; } - } public sealed class HttpGetValidationRule : GraphQL.Validation.IValidationRule { public HttpGetValidationRule() { } From d54d09cfea7138decbe67c1130eee727a42cb199 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 14 Apr 2022 15:17:14 -0400 Subject: [PATCH 04/47] Update --- .../GraphQLHttpMiddleware.cs | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 4dd01354..8080464f 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -30,8 +30,29 @@ public class GraphQLHttpMiddleware : GraphQLHttpMiddleware // important: when using convention-based ASP.NET Core middleware, the first constructor is always used - /************* WebSocket support ************ - + /// + /// Initializes a new instance. + /// + public GraphQLHttpMiddleware( + RequestDelegate next, + IGraphQLTextSerializer serializer, + IDocumentExecuter documentExecuter, + IServiceScopeFactory serviceScopeFactory, + GraphQLHttpMiddlewareOptions options) + : base(next, serializer, options) + { + _documentExecuter = documentExecuter ?? throw new ArgumentNullException(nameof(documentExecuter)); + _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + var getRule = new HttpGetValidationRule(); + _getValidationRules = DocumentValidator.CoreRules.Append(getRule).ToArray(); + _getCachedDocumentValidationRules = new[] { getRule }; + var postRule = new HttpPostValidationRule(); + _postValidationRules = DocumentValidator.CoreRules.Append(postRule).ToArray(); + _postCachedDocumentValidationRules = new[] { postRule }; + } + + /************* WebSocket support *********** + /// /// Initializes a new instance. /// @@ -48,8 +69,6 @@ public GraphQLHttpMiddleware( { } - *********************************/ - /// /// Initializes a new instance. /// @@ -58,9 +77,9 @@ protected GraphQLHttpMiddleware( IGraphQLTextSerializer serializer, IDocumentExecuter documentExecuter, IServiceScopeFactory serviceScopeFactory, - GraphQLHttpMiddlewareOptions options /*, - IEnumerable>? webSocketHandlers = null */) - : base(next, serializer, options /*, webSocketHandlers */) + GraphQLHttpMiddlewareOptions options, + IEnumerable>? webSocketHandlers = null) + : base(next, serializer, options, webSocketHandlers) { _documentExecuter = documentExecuter ?? throw new ArgumentNullException(nameof(documentExecuter)); _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); @@ -72,8 +91,6 @@ protected GraphQLHttpMiddleware( _postCachedDocumentValidationRules = new[] { postRule }; } - /************* WebSocket support *********** - private static IEnumerable> CreateWebSocketHandlers( IGraphQLSerializer serializer, IDocumentExecuter documentExecuter, From 31a251210773a5b40cb3c56e8759f509f9c418e7 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 14 Apr 2022 15:30:28 -0400 Subject: [PATCH 05/47] Update api approvals --- .../GraphQL.Server.Transports.AspNetCore.approved.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt index 1482fca0..876c8bac 100644 --- a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -72,7 +72,7 @@ namespace GraphQL.Server.Transports.AspNetCore public class GraphQLHttpMiddleware : GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddleware where TSchema : GraphQL.Types.ISchema { - protected GraphQLHttpMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, GraphQL.IGraphQLTextSerializer serializer, GraphQL.IDocumentExecuter documentExecuter, Microsoft.Extensions.DependencyInjection.IServiceScopeFactory serviceScopeFactory, GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions options) { } + public GraphQLHttpMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, GraphQL.IGraphQLTextSerializer serializer, GraphQL.IDocumentExecuter documentExecuter, Microsoft.Extensions.DependencyInjection.IServiceScopeFactory serviceScopeFactory, GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions options) { } protected override System.Threading.Tasks.Task ExecuteRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest request, System.IServiceProvider serviceProvider, System.Collections.Generic.IDictionary userContext) { } protected override System.Threading.Tasks.Task ExecuteScopedRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest request, System.Collections.Generic.IDictionary userContext) { } } From b505c713ab12d1da41e4464a4971de0dd2d45fc8 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Sat, 16 Apr 2022 20:58:28 -0400 Subject: [PATCH 06/47] Convert IUserContextBuilder to file-scoped namespace --- .../IUserContextBuilder.cs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Transports.AspNetCore/IUserContextBuilder.cs b/src/Transports.AspNetCore/IUserContextBuilder.cs index be027a81..322e3831 100644 --- a/src/Transports.AspNetCore/IUserContextBuilder.cs +++ b/src/Transports.AspNetCore/IUserContextBuilder.cs @@ -1,17 +1,16 @@ using Microsoft.AspNetCore.Http; -namespace GraphQL.Server.Transports.AspNetCore +namespace GraphQL.Server.Transports.AspNetCore; + +/// +/// Interface which is responsible of building a UserContext for a GraphQL request +/// +public interface IUserContextBuilder { /// - /// Interface which is responsible of building a UserContext for a GraphQL request + /// Builds the UserContext using the specified /// - public interface IUserContextBuilder - { - /// - /// Builds the UserContext using the specified - /// - /// The for the current request - /// Returns the UserContext - ValueTask> BuildUserContextAsync(HttpContext httpContext); - } + /// The for the current request + /// Returns the UserContext + ValueTask> BuildUserContextAsync(HttpContext httpContext); } From ca9801a585c30a31c6c170c2720b519255d98c3a Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Sat, 16 Apr 2022 20:58:43 -0400 Subject: [PATCH 07/47] Enable NRT for app builder extensions --- .../Extensions/GraphQLHttpApplicationBuilderExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Transports.AspNetCore/Extensions/GraphQLHttpApplicationBuilderExtensions.cs b/src/Transports.AspNetCore/Extensions/GraphQLHttpApplicationBuilderExtensions.cs index d84cb30f..3ef5008f 100644 --- a/src/Transports.AspNetCore/Extensions/GraphQLHttpApplicationBuilderExtensions.cs +++ b/src/Transports.AspNetCore/Extensions/GraphQLHttpApplicationBuilderExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + using GraphQL.Server.Transports.AspNetCore; using GraphQL.Types; using Microsoft.AspNetCore.Http; From 7c7935157bfbff864fcd8c2766d7240fd9384da0 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Sat, 16 Apr 2022 20:59:13 -0400 Subject: [PATCH 08/47] Enable NRT for IUserContextBuilder --- src/Transports.AspNetCore/IUserContextBuilder.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/IUserContextBuilder.cs b/src/Transports.AspNetCore/IUserContextBuilder.cs index 322e3831..e6877f5d 100644 --- a/src/Transports.AspNetCore/IUserContextBuilder.cs +++ b/src/Transports.AspNetCore/IUserContextBuilder.cs @@ -1,3 +1,5 @@ +#nullable enable + using Microsoft.AspNetCore.Http; namespace GraphQL.Server.Transports.AspNetCore; @@ -12,5 +14,5 @@ public interface IUserContextBuilder /// /// The for the current request /// Returns the UserContext - ValueTask> BuildUserContextAsync(HttpContext httpContext); + ValueTask> BuildUserContextAsync(HttpContext httpContext); } From c8fea7c59c9eea60c679092d94b2420f76465ad0 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Sat, 16 Apr 2022 21:06:24 -0400 Subject: [PATCH 09/47] Update user context comments --- src/Transports.AspNetCore/IUserContextBuilder.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Transports.AspNetCore/IUserContextBuilder.cs b/src/Transports.AspNetCore/IUserContextBuilder.cs index e6877f5d..250d617a 100644 --- a/src/Transports.AspNetCore/IUserContextBuilder.cs +++ b/src/Transports.AspNetCore/IUserContextBuilder.cs @@ -5,14 +5,13 @@ namespace GraphQL.Server.Transports.AspNetCore; /// -/// Interface which is responsible of building a UserContext for a GraphQL request +/// Creates a user context from a . +///

+/// The generated user context may be used one or more times while executing +/// a GraphQL request or subscription. ///
public interface IUserContextBuilder { - /// - /// Builds the UserContext using the specified - /// - /// The for the current request - /// Returns the UserContext - ValueTask> BuildUserContextAsync(HttpContext httpContext); + /// + ValueTask> BuildUserContextAsync(HttpContext context); } From 51ff27b51bbe0b16eeef24ef85caeb4d7de0e9a2 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Sat, 16 Apr 2022 21:25:47 -0400 Subject: [PATCH 10/47] Update comment for GraphQLHttpMiddlewareOptions --- src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs b/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs index 43821334..7bab700a 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs @@ -8,7 +8,7 @@ namespace GraphQL.Server.Transports.AspNetCore; /// -/// Configuration options for . +/// Configuration options for . /// public class GraphQLHttpMiddlewareOptions { From 9b2c642c716721e562639d415ee1194c0e7b9438 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Sat, 16 Apr 2022 21:25:54 -0400 Subject: [PATCH 11/47] Update api approvals --- .../GraphQL.Server.Transports.AspNetCore.approved.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt index 876c8bac..8ce0d681 100644 --- a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -88,7 +88,7 @@ namespace GraphQL.Server.Transports.AspNetCore } public interface IUserContextBuilder { - System.Threading.Tasks.ValueTask> BuildUserContextAsync(Microsoft.AspNetCore.Http.HttpContext httpContext); + System.Threading.Tasks.ValueTask> BuildUserContextAsync(Microsoft.AspNetCore.Http.HttpContext context); } public static class MediaType { From f2a7011c74ac5317f22fe4149c969d4ee3f241f7 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Sun, 17 Apr 2022 00:03:51 -0400 Subject: [PATCH 12/47] Update src/Transports.AspNetCore/IUserContextBuilder.cs --- src/Transports.AspNetCore/IUserContextBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Transports.AspNetCore/IUserContextBuilder.cs b/src/Transports.AspNetCore/IUserContextBuilder.cs index 250d617a..b132afa5 100644 --- a/src/Transports.AspNetCore/IUserContextBuilder.cs +++ b/src/Transports.AspNetCore/IUserContextBuilder.cs @@ -7,8 +7,8 @@ namespace GraphQL.Server.Transports.AspNetCore; /// /// Creates a user context from a . ///

-/// The generated user context may be used one or more times while executing -/// a GraphQL request or subscription. +/// The generated user context may be used for one or more GraphQL requests or +/// subscriptions over the same HTTP connection. ///
public interface IUserContextBuilder { From 6a65c3b2892ed78c9bb10945dd3f767be6e10c04 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 13 May 2022 04:46:01 -0400 Subject: [PATCH 13/47] Update src/Transports.AspNetCore/Errors/RequestError.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/Errors/RequestError.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/Errors/RequestError.cs b/src/Transports.AspNetCore/Errors/RequestError.cs index 3c518fc5..97ec7d52 100644 --- a/src/Transports.AspNetCore/Errors/RequestError.cs +++ b/src/Transports.AspNetCore/Errors/RequestError.cs @@ -5,7 +5,8 @@ namespace GraphQL.Server.Transports.AspNetCore.Errors; /// -/// Represents an error that occurred prior to the execution of the request. +/// Represents an error that occurred prior to the execution of the GraphQL request. +/// This refers to any errors that arise before passing the request inside the GraphQL engine, that is, even before its validation. /// public class RequestError : ExecutionError { From 9398d10192de4bc325d9389c29422d68fc5ed325 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Sun, 15 May 2022 08:25:05 -0400 Subject: [PATCH 14/47] Bump to GraphQL 5.3.0 with necessary changes --- Directory.Build.props | 3 +- .../GraphQLHttpMiddlewareWithLogs.cs | 2 +- src/All/All.csproj | 6 +- .../Authorization.AspNetCore.csproj | 2 +- .../BatchedRequestsNotSupportedError.cs | 2 + .../Errors/InvalidContentTypeError.cs | 2 + .../Errors/JsonInvalidError.cs | 2 + .../Errors/QueryMissingError.cs | 12 ---- .../Errors/RequestError.cs | 36 ------------ .../WebSocketSubProtocolNotSupportedError.cs | 2 + ...aphQLHttpEndpointRouteBuilderExtensions.cs | 2 + .../GraphQLHttpMiddleware.cs | 57 +++++++------------ .../Transports.AspNetCore.csproj | 2 +- ...ansports.Subscriptions.Abstractions.csproj | 2 +- ....Server.Transports.AspNetCore.approved.txt | 24 +++----- 15 files changed, 48 insertions(+), 108 deletions(-) delete mode 100644 src/Transports.AspNetCore/Errors/QueryMissingError.cs delete mode 100644 src/Transports.AspNetCore/Errors/RequestError.cs diff --git a/Directory.Build.props b/Directory.Build.props index 4d36c2ad..1aca9113 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 6.0.0-preview + 7.0.0-preview latest MIT logo.64x64.png @@ -26,6 +26,7 @@ GraphQL.Server.$(MSBuildProjectName) GraphQL.Server.$(MSBuildProjectName) GraphQL.Server.$(MSBuildProjectName) + 5.3.0 diff --git a/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs b/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs index 1ee61ada..5c844a46 100644 --- a/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs +++ b/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs @@ -25,7 +25,7 @@ public GraphQLHttpMiddlewareWithLogs( _logger = logger; } - protected override async Task ExecuteRequestAsync(HttpContext context, GraphQLRequest request, IServiceProvider serviceProvider, IDictionary userContext) + protected override async Task ExecuteRequestAsync(HttpContext context, GraphQLRequest? request, IServiceProvider serviceProvider, IDictionary userContext) { var timer = Stopwatch.StartNew(); var ret = await base.ExecuteRequestAsync(context, request, serviceProvider, userContext); diff --git a/src/All/All.csproj b/src/All/All.csproj index 39a2884b..58274e61 100644 --- a/src/All/All.csproj +++ b/src/All/All.csproj @@ -18,9 +18,9 @@ - - - + + + diff --git a/src/Authorization.AspNetCore/Authorization.AspNetCore.csproj b/src/Authorization.AspNetCore/Authorization.AspNetCore.csproj index 293936ff..8650f910 100644 --- a/src/Authorization.AspNetCore/Authorization.AspNetCore.csproj +++ b/src/Authorization.AspNetCore/Authorization.AspNetCore.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Transports.AspNetCore/Errors/BatchedRequestsNotSupportedError.cs b/src/Transports.AspNetCore/Errors/BatchedRequestsNotSupportedError.cs index fca86560..1594f800 100644 --- a/src/Transports.AspNetCore/Errors/BatchedRequestsNotSupportedError.cs +++ b/src/Transports.AspNetCore/Errors/BatchedRequestsNotSupportedError.cs @@ -1,5 +1,7 @@ #nullable enable +using GraphQL.Execution; + namespace GraphQL.Server.Transports.AspNetCore.Errors; /// diff --git a/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs b/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs index 54293693..13727269 100644 --- a/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs +++ b/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs @@ -1,5 +1,7 @@ #nullable enable +using GraphQL.Execution; + namespace GraphQL.Server.Transports.AspNetCore.Errors; /// diff --git a/src/Transports.AspNetCore/Errors/JsonInvalidError.cs b/src/Transports.AspNetCore/Errors/JsonInvalidError.cs index a322b992..94d3a626 100644 --- a/src/Transports.AspNetCore/Errors/JsonInvalidError.cs +++ b/src/Transports.AspNetCore/Errors/JsonInvalidError.cs @@ -1,5 +1,7 @@ #nullable enable +using GraphQL.Execution; + namespace GraphQL.Server.Transports.AspNetCore.Errors; /// diff --git a/src/Transports.AspNetCore/Errors/QueryMissingError.cs b/src/Transports.AspNetCore/Errors/QueryMissingError.cs deleted file mode 100644 index 239d9b4d..00000000 --- a/src/Transports.AspNetCore/Errors/QueryMissingError.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable enable - -namespace GraphQL.Server.Transports.AspNetCore.Errors; - -/// -/// Represents an error indicating that no GraphQL query was provided to the request. -/// -public class QueryMissingError : RequestError -{ - /// - public QueryMissingError() : base("GraphQL query is missing.") { } -} diff --git a/src/Transports.AspNetCore/Errors/RequestError.cs b/src/Transports.AspNetCore/Errors/RequestError.cs deleted file mode 100644 index 97ec7d52..00000000 --- a/src/Transports.AspNetCore/Errors/RequestError.cs +++ /dev/null @@ -1,36 +0,0 @@ -#nullable enable - -using GraphQL.Execution; - -namespace GraphQL.Server.Transports.AspNetCore.Errors; - -/// -/// Represents an error that occurred prior to the execution of the GraphQL request. -/// This refers to any errors that arise before passing the request inside the GraphQL engine, that is, even before its validation. -/// -public class RequestError : ExecutionError -{ - /// - /// Initializes an instance with the specified message. - /// - public RequestError(string message) : base(message) - { - Code = GetErrorCode(); - } - - /// - /// Initializes an instance with the specified message and inner exception. - /// - public RequestError(string message, Exception? innerException) : base(message, innerException) - { - Code = GetErrorCode(); - } - - internal string GetErrorCode() - { - var code = ErrorInfoProvider.GetErrorCode(GetType()); - if (code != "REQUEST_ERROR" && code.EndsWith("_ERROR", StringComparison.Ordinal)) - code = code[0..^6]; - return code; - } -} diff --git a/src/Transports.AspNetCore/Errors/WebSocketSubProtocolNotSupportedError.cs b/src/Transports.AspNetCore/Errors/WebSocketSubProtocolNotSupportedError.cs index 3eef4603..973079cd 100644 --- a/src/Transports.AspNetCore/Errors/WebSocketSubProtocolNotSupportedError.cs +++ b/src/Transports.AspNetCore/Errors/WebSocketSubProtocolNotSupportedError.cs @@ -1,5 +1,7 @@ #nullable enable +using GraphQL.Execution; + namespace GraphQL.Server.Transports.AspNetCore.Errors; /// diff --git a/src/Transports.AspNetCore/Extensions/GraphQLHttpEndpointRouteBuilderExtensions.cs b/src/Transports.AspNetCore/Extensions/GraphQLHttpEndpointRouteBuilderExtensions.cs index 63dff4c9..9e20ea96 100644 --- a/src/Transports.AspNetCore/Extensions/GraphQLHttpEndpointRouteBuilderExtensions.cs +++ b/src/Transports.AspNetCore/Extensions/GraphQLHttpEndpointRouteBuilderExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + using GraphQL.Server.Transports.AspNetCore; using GraphQL.Types; using Microsoft.AspNetCore.Routing; diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 8080464f..1c4a87a5 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -116,28 +116,27 @@ private static IEnumerable> CreateWebSocketHandlers( protected override async Task ExecuteScopedRequestAsync(HttpContext context, GraphQLRequest request, IDictionary userContext) { using var scope = _serviceScopeFactory.CreateScope(); - return await ExecuteRequestAsync(context, request, scope.ServiceProvider, userContext); + try + { + return await ExecuteRequestAsync(context, request, scope.ServiceProvider, userContext); + } + finally + { + if (scope is IAsyncDisposable ad) + await ad.DisposeAsync(); + } } /// - protected override async Task ExecuteRequestAsync(HttpContext context, GraphQLRequest request, IServiceProvider serviceProvider, IDictionary userContext) + protected override async Task ExecuteRequestAsync(HttpContext context, GraphQLRequest? request, IServiceProvider serviceProvider, IDictionary userContext) { - if (request?.Query == null) - { - return new ExecutionResult - { - Errors = new ExecutionErrors { - new QueryMissingError() - } - }; - } var opts = new ExecutionOptions { - Query = request.Query, - Variables = request.Variables, - Extensions = request.Extensions, + Query = request?.Query, + Variables = request?.Variables, + Extensions = request?.Extensions, CancellationToken = context.RequestAborted, - OperationName = request.OperationName, + OperationName = request?.OperationName, RequestServices = serviceProvider, UserContext = userContext, }; @@ -237,7 +236,7 @@ public virtual async Task InvokeAsync(HttpContext context) // Parse POST body GraphQLRequest? bodyGQLRequest = null; - IList? bodyGQLBatchRequest = null; + IList? bodyGQLBatchRequest = null; if (isPost) { if (!MediaTypeHeaderValue.TryParse(httpRequest.ContentType, out var mediaTypeHeader)) @@ -249,7 +248,7 @@ public virtual async Task InvokeAsync(HttpContext context) switch (mediaTypeHeader.MediaType) { case MEDIATYPE_JSON: - IList? deserializationResult; + IList? deserializationResult; try { #if NET5_0_OR_GREATER @@ -262,14 +261,14 @@ public virtual async Task InvokeAsync(HttpContext context) if (sourceEncoding != null && sourceEncoding != System.Text.Encoding.UTF8) { using var tempStream = System.Text.Encoding.CreateTranscodingStream(httpRequest.Body, innerStreamEncoding: sourceEncoding, outerStreamEncoding: System.Text.Encoding.UTF8, leaveOpen: true); - deserializationResult = await _serializer.ReadAsync>(tempStream, context.RequestAborted); + deserializationResult = await _serializer.ReadAsync>(tempStream, context.RequestAborted); } else { - deserializationResult = await _serializer.ReadAsync>(httpRequest.Body, context.RequestAborted); + deserializationResult = await _serializer.ReadAsync>(httpRequest.Body, context.RequestAborted); } #else - deserializationResult = await _serializer.ReadAsync>(httpRequest.Body, context.RequestAborted); + deserializationResult = await _serializer.ReadAsync>(httpRequest.Body, context.RequestAborted); #endif } catch (Exception ex) @@ -332,18 +331,12 @@ public virtual async Task InvokeAsync(HttpContext context) gqlRequest = new GraphQLRequest { - Query = urlGQLRequest?.Query ?? bodyGQLRequest?.Query!, + Query = urlGQLRequest?.Query ?? bodyGQLRequest?.Query, Variables = urlGQLRequest?.Variables ?? bodyGQLRequest?.Variables, Extensions = urlGQLRequest?.Extensions ?? bodyGQLRequest?.Extensions, OperationName = urlGQLRequest?.OperationName ?? bodyGQLRequest?.OperationName }; - if (string.IsNullOrWhiteSpace(gqlRequest.Query)) - { - await HandleNoQueryErrorAsync(context, _next); - return; - } - // Prepare context and execute await HandleRequestAsync(context, _next, gqlRequest); } @@ -411,7 +404,7 @@ protected virtual async Task HandleRequestAsync( protected virtual async Task HandleBatchRequestAsync( HttpContext context, RequestDelegate next, - IList gqlRequests) + IList gqlRequests) { var userContext = await BuildUserContextAsync(context); var results = new ExecutionResult[gqlRequests.Count]; @@ -470,7 +463,7 @@ protected virtual async Task HandleBatchRequestAsync( /// options.CachedDocumentValidationRules = new[] { rule }; /// /// - protected abstract Task ExecuteRequestAsync(HttpContext context, GraphQLRequest request, IServiceProvider serviceProvider, IDictionary userContext); + protected abstract Task ExecuteRequestAsync(HttpContext context, GraphQLRequest? request, IServiceProvider serviceProvider, IDictionary userContext); /// /// Builds the user context based on a . @@ -603,12 +596,6 @@ protected virtual Task HandleBatchedRequestsNotSupportedAsync(HttpContext contex protected virtual Task HandleWebSocketSubProtocolNotSupportedAsync(HttpContext context, RequestDelegate next) => WriteErrorResponseAsync(context, HttpStatusCode.BadRequest, new WebSocketSubProtocolNotSupportedError(context.WebSockets.WebSocketRequestedProtocols)); - /// - /// Writes a '400 GraphQL query is missing.' message to the output. - /// - protected virtual Task HandleNoQueryErrorAsync(HttpContext context, RequestDelegate next) - => WriteErrorResponseAsync(context, Options.ValidationErrorsReturnBadRequest ? HttpStatusCode.BadRequest : HttpStatusCode.OK, new QueryMissingError()); - /// /// Writes a '415 Invalid Content-Type header: could not be parsed.' message to the output. /// diff --git a/src/Transports.AspNetCore/Transports.AspNetCore.csproj b/src/Transports.AspNetCore/Transports.AspNetCore.csproj index bc352063..d2c21f14 100644 --- a/src/Transports.AspNetCore/Transports.AspNetCore.csproj +++ b/src/Transports.AspNetCore/Transports.AspNetCore.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Transports.Subscriptions.Abstractions/Transports.Subscriptions.Abstractions.csproj b/src/Transports.Subscriptions.Abstractions/Transports.Subscriptions.Abstractions.csproj index 8b9b2745..ae6bf8e2 100644 --- a/src/Transports.Subscriptions.Abstractions/Transports.Subscriptions.Abstractions.csproj +++ b/src/Transports.Subscriptions.Abstractions/Transports.Subscriptions.Abstractions.csproj @@ -6,7 +6,7 @@ - + diff --git a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt index 8ce0d681..ff6da198 100644 --- a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -33,17 +33,16 @@ namespace GraphQL.Server.Transports.AspNetCore public GraphQLHttpMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, GraphQL.IGraphQLTextSerializer serializer, GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions options) { } protected GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions Options { get; } protected virtual System.Threading.Tasks.ValueTask> BuildUserContextAsync(Microsoft.AspNetCore.Http.HttpContext context) { } - protected abstract System.Threading.Tasks.Task ExecuteRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest request, System.IServiceProvider serviceProvider, System.Collections.Generic.IDictionary userContext); + protected abstract System.Threading.Tasks.Task ExecuteRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest? request, System.IServiceProvider serviceProvider, System.Collections.Generic.IDictionary userContext); protected abstract System.Threading.Tasks.Task ExecuteScopedRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest request, System.Collections.Generic.IDictionary userContext); protected virtual System.Threading.Tasks.ValueTask HandleAuthorizeAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } protected virtual System.Threading.Tasks.ValueTask HandleAuthorizeWebSocketConnectionAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } - protected virtual System.Threading.Tasks.Task HandleBatchRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next, System.Collections.Generic.IList gqlRequests) { } + protected virtual System.Threading.Tasks.Task HandleBatchRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next, System.Collections.Generic.IList gqlRequests) { } protected virtual System.Threading.Tasks.Task HandleBatchedRequestsNotSupportedAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } protected virtual System.Threading.Tasks.Task HandleContentTypeCouldNotBeParsedErrorAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } protected virtual System.Threading.Tasks.ValueTask HandleDeserializationErrorAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next, System.Exception exception) { } protected virtual System.Threading.Tasks.Task HandleInvalidContentTypeErrorAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } protected virtual System.Threading.Tasks.Task HandleInvalidHttpMethodErrorAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } - protected virtual System.Threading.Tasks.Task HandleNoQueryErrorAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } protected virtual System.Threading.Tasks.Task HandleNotAuthenticatedAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } protected virtual System.Threading.Tasks.Task HandleNotAuthorizedPolicyAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.AspNetCore.Authorization.AuthorizationResult authorizationResult) { } protected virtual System.Threading.Tasks.Task HandleNotAuthorizedRoleAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } @@ -73,7 +72,7 @@ namespace GraphQL.Server.Transports.AspNetCore where TSchema : GraphQL.Types.ISchema { public GraphQLHttpMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, GraphQL.IGraphQLTextSerializer serializer, GraphQL.IDocumentExecuter documentExecuter, Microsoft.Extensions.DependencyInjection.IServiceScopeFactory serviceScopeFactory, GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions options) { } - protected override System.Threading.Tasks.Task ExecuteRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest request, System.IServiceProvider serviceProvider, System.Collections.Generic.IDictionary userContext) { } + protected override System.Threading.Tasks.Task ExecuteRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest? request, System.IServiceProvider serviceProvider, System.Collections.Generic.IDictionary userContext) { } protected override System.Threading.Tasks.Task ExecuteScopedRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest request, System.Collections.Generic.IDictionary userContext) { } } public sealed class HttpGetValidationRule : GraphQL.Validation.IValidationRule @@ -114,7 +113,7 @@ namespace GraphQL.Server.Transports.AspNetCore.Errors public string? PolicyRequired { get; set; } public System.Collections.Generic.List? RolesRequired { get; set; } } - public class BatchedRequestsNotSupportedError : GraphQL.Server.Transports.AspNetCore.Errors.RequestError + public class BatchedRequestsNotSupportedError : GraphQL.Execution.RequestError { public BatchedRequestsNotSupportedError() { } } @@ -122,26 +121,17 @@ namespace GraphQL.Server.Transports.AspNetCore.Errors { public HttpMethodValidationError(GraphQLParser.ROM originalQuery, GraphQLParser.AST.ASTNode node, string message) { } } - public class InvalidContentTypeError : GraphQL.Server.Transports.AspNetCore.Errors.RequestError + public class InvalidContentTypeError : GraphQL.Execution.RequestError { public InvalidContentTypeError() { } public InvalidContentTypeError(string message) { } } - public class JsonInvalidError : GraphQL.Server.Transports.AspNetCore.Errors.RequestError + public class JsonInvalidError : GraphQL.Execution.RequestError { public JsonInvalidError() { } public JsonInvalidError(System.Exception innerException) { } } - public class QueryMissingError : GraphQL.Server.Transports.AspNetCore.Errors.RequestError - { - public QueryMissingError() { } - } - public class RequestError : GraphQL.ExecutionError - { - public RequestError(string message) { } - public RequestError(string message, System.Exception? innerException) { } - } - public class WebSocketSubProtocolNotSupportedError : GraphQL.Server.Transports.AspNetCore.Errors.RequestError + public class WebSocketSubProtocolNotSupportedError : GraphQL.Execution.RequestError { public WebSocketSubProtocolNotSupportedError(System.Collections.Generic.IEnumerable requestedSubProtocols) { } } From f57505c7a3d1eb1c129fd09818e400f5ca2b5270 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Sun, 15 May 2022 08:29:13 -0400 Subject: [PATCH 15/47] Update --- samples/Samples.Server/Startup.cs | 4 +--- samples/Samples.Server/StartupWithRouting.cs | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/samples/Samples.Server/Startup.cs b/samples/Samples.Server/Startup.cs index 1cf2ba5a..ee80f2e8 100644 --- a/samples/Samples.Server/Startup.cs +++ b/samples/Samples.Server/Startup.cs @@ -1,6 +1,5 @@ using GraphQL.DataLoader; using GraphQL.Execution; -using GraphQL.Instrumentation; using GraphQL.MicrosoftDI; using GraphQL.Samples.Schemas.Chat; using GraphQL.Server; @@ -35,8 +34,7 @@ public void ConfigureServices(IServiceCollection services) .AddTransient(); // required by CustomErrorInfoProvider services.AddGraphQL(builder => builder - .AddMetrics() - .AddDocumentExecuter() + .AddApolloTracing() .AddWebSocketsHttpMiddleware() .AddSchema() .ConfigureExecutionOptions(options => diff --git a/samples/Samples.Server/StartupWithRouting.cs b/samples/Samples.Server/StartupWithRouting.cs index 7ad57690..4df68fa0 100644 --- a/samples/Samples.Server/StartupWithRouting.cs +++ b/samples/Samples.Server/StartupWithRouting.cs @@ -1,6 +1,5 @@ using GraphQL.DataLoader; using GraphQL.Execution; -using GraphQL.Instrumentation; using GraphQL.MicrosoftDI; using GraphQL.Samples.Schemas.Chat; using GraphQL.Server; @@ -35,8 +34,7 @@ public void ConfigureServices(IServiceCollection services) .AddTransient(); // required by CustomErrorInfoProvider services.AddGraphQL(builder => builder - .AddMetrics() - .AddDocumentExecuter() + .AddApolloTracing() .AddWebSocketsHttpMiddleware() .AddSchema() .ConfigureExecutionOptions(options => From c673740d77be79a8483b07e661c795cef7f3aa88 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 19 May 2022 15:50:33 -0400 Subject: [PATCH 16/47] Updates --- .../AuthorizationParameters.cs | 14 +++++++------- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 2 +- .../GraphQLHttpMiddlewareOptions.cs | 2 +- ...aphQL.Server.Transports.AspNetCore.approved.txt | 12 ++++++------ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Transports.AspNetCore/AuthorizationParameters.cs b/src/Transports.AspNetCore/AuthorizationParameters.cs index 83f87f1c..2d856f72 100644 --- a/src/Transports.AspNetCore/AuthorizationParameters.cs +++ b/src/Transports.AspNetCore/AuthorizationParameters.cs @@ -10,7 +10,7 @@ namespace GraphQL.Server.Transports.AspNetCore; /// /// Authorization parameters /// -public struct AuthorizationParameters +public struct AuthorizationParameters { /// /// Initializes an instance with a specified @@ -19,9 +19,9 @@ public struct AuthorizationParameters public AuthorizationParameters( HttpContext httpContext, GraphQLHttpMiddlewareOptions middlewareOptions, - Func? onNotAuthenticated, - Func? onNotAuthorizedRole, - Func? onNotAuthorizedPolicy) + Func? onNotAuthenticated, + Func? onNotAuthorizedRole, + Func? onNotAuthorizedPolicy) { HttpContext = httpContext; AuthorizationRequired = middlewareOptions.AuthorizationRequired; @@ -50,19 +50,19 @@ public AuthorizationParameters( /// A delegate which executes if is set /// but returns . /// - public Func? OnNotAuthenticated { get; set; } + public Func? OnNotAuthenticated { get; set; } /// /// A delegate which executes if is set but /// returns /// for all roles. /// - public Func? OnNotAuthorizedRole { get; set; } + public Func? OnNotAuthorizedRole { get; set; } /// /// A delegate which executes if is set but /// /// returns an unsuccessful for the specified policy. /// - public Func? OnNotAuthorizedPolicy { get; set; } + public Func? OnNotAuthorizedPolicy { get; set; } } diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 1c4a87a5..862d9545 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -415,7 +415,7 @@ protected virtual async Task HandleBatchRequestAsync( else { // Batched execution with multiple graphql requests - if (!Options.BatchedRequestsExecuteInParallel) + if (!Options.ExecuteBatchedRequestsInParallel) { for (int i = 0; i < gqlRequests.Count; i++) { diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs b/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs index 7bab700a..62ba1b43 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs @@ -42,7 +42,7 @@ public class GraphQLHttpMiddlewareOptions /// /// Enables parallel execution of batched GraphQL requests. /// - public bool BatchedRequestsExecuteInParallel { get; set; } = true; + public bool ExecuteBatchedRequestsInParallel { get; set; } = true; /// /// When enabled, GraphQL requests with validation errors diff --git a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt index ff6da198..ad6423fb 100644 --- a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -17,16 +17,16 @@ namespace GraphQL.Server.Transports.AspNetCore { public static System.Threading.Tasks.ValueTask AuthorizeAsync(GraphQL.Server.Transports.AspNetCore.AuthorizationParameters options, TState state) { } } - public struct AuthorizationParameters + public struct AuthorizationParameters { - public AuthorizationParameters(Microsoft.AspNetCore.Http.HttpContext httpContext, GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions middlewareOptions, System.Func? onNotAuthenticated, System.Func? onNotAuthorizedRole, System.Func? onNotAuthorizedPolicy) { } + public AuthorizationParameters(Microsoft.AspNetCore.Http.HttpContext httpContext, GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions middlewareOptions, System.Func? onNotAuthenticated, System.Func? onNotAuthorizedRole, System.Func? onNotAuthorizedPolicy) { } public bool AuthorizationRequired { get; set; } public string? AuthorizedPolicy { get; set; } public System.Collections.Generic.List? AuthorizedRoles { get; set; } public Microsoft.AspNetCore.Http.HttpContext HttpContext { get; set; } - public System.Func? OnNotAuthenticated { get; set; } - public System.Func? OnNotAuthorizedPolicy { get; set; } - public System.Func? OnNotAuthorizedRole { get; set; } + public System.Func? OnNotAuthenticated { get; set; } + public System.Func? OnNotAuthorizedPolicy { get; set; } + public System.Func? OnNotAuthorizedRole { get; set; } } public abstract class GraphQLHttpMiddleware { @@ -59,8 +59,8 @@ namespace GraphQL.Server.Transports.AspNetCore public bool AuthorizationRequired { get; set; } public string? AuthorizedPolicy { get; set; } public System.Collections.Generic.List AuthorizedRoles { get; set; } - public bool BatchedRequestsExecuteInParallel { get; set; } public bool EnableBatchedRequests { get; set; } + public bool ExecuteBatchedRequestsInParallel { get; set; } public bool HandleGet { get; set; } public bool HandlePost { get; set; } public bool ReadExtensionsFromQueryString { get; set; } From 5671c49dafea993500d7a26d93da4065a9f70390 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 19 May 2022 15:53:34 -0400 Subject: [PATCH 17/47] Update --- src/Transports.AspNetCore/AuthorizationHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/AuthorizationHelper.cs b/src/Transports.AspNetCore/AuthorizationHelper.cs index 96464dc3..f87c9315 100644 --- a/src/Transports.AspNetCore/AuthorizationHelper.cs +++ b/src/Transports.AspNetCore/AuthorizationHelper.cs @@ -14,7 +14,7 @@ public static class AuthorizationHelper { /// /// Performs connection authorization according to the options set within - /// . Returns + /// . Returns /// if authorization was successful or not required. /// public static async ValueTask AuthorizeAsync(AuthorizationParameters options, TState state) From e1c5ab55eb6297d68491fe724a6957eb71f32552 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 19 May 2022 16:28:07 -0400 Subject: [PATCH 18/47] Update src/Transports.AspNetCore/AuthorizationParameters.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/AuthorizationParameters.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/AuthorizationParameters.cs b/src/Transports.AspNetCore/AuthorizationParameters.cs index 2d856f72..47910c12 100644 --- a/src/Transports.AspNetCore/AuthorizationParameters.cs +++ b/src/Transports.AspNetCore/AuthorizationParameters.cs @@ -8,7 +8,8 @@ namespace GraphQL.Server.Transports.AspNetCore; /// -/// Authorization parameters +/// Authorization parameters. This struct is used to group all necessary parameters together and perform arbitrary +/// actions based on provided authentication properties/attributes/etc. It is not intended to be called from user code. /// public struct AuthorizationParameters { From 27741bf7663b7ac601d6f2e81c0bbda7f9c91bff Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 19 May 2022 16:28:45 -0400 Subject: [PATCH 19/47] Update --- src/Transports.AspNetCore/AuthorizationParameters.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Transports.AspNetCore/AuthorizationParameters.cs b/src/Transports.AspNetCore/AuthorizationParameters.cs index 47910c12..85e892dd 100644 --- a/src/Transports.AspNetCore/AuthorizationParameters.cs +++ b/src/Transports.AspNetCore/AuthorizationParameters.cs @@ -8,8 +8,10 @@ namespace GraphQL.Server.Transports.AspNetCore; /// -/// Authorization parameters. This struct is used to group all necessary parameters together and perform arbitrary -/// actions based on provided authentication properties/attributes/etc. It is not intended to be called from user code. +/// Authorization parameters. +/// This struct is used to group all necessary parameters together and perform arbitrary +/// actions based on provided authentication properties/attributes/etc. +/// It is not intended to be called from user code. /// public struct AuthorizationParameters { From b2caa068a49839a5e579c3b35a3029e35a4b6d95 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 19 May 2022 16:29:43 -0400 Subject: [PATCH 20/47] Update src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs b/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs index 62ba1b43..f1b33131 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs @@ -77,6 +77,8 @@ public class GraphQLHttpMiddlewareOptions /// If set, requires that return /// for the user within /// prior to executing the GraphQL request or accepting the WebSocket connection. + /// Technically this property should be named as AuthenticationRequired but for + /// ASP.NET Core / GraphQL.NET naming and design decisions it was called so. /// public bool AuthorizationRequired { get; set; } From 83b0b84c143db9ba043b7cf34c6eda386ffc109f Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 19 May 2022 16:38:40 -0400 Subject: [PATCH 21/47] Readonly struct --- .../AuthorizationParameters.cs | 16 ++++++++-------- ...hQL.Server.Transports.AspNetCore.approved.txt | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Transports.AspNetCore/AuthorizationParameters.cs b/src/Transports.AspNetCore/AuthorizationParameters.cs index 85e892dd..7acf36ef 100644 --- a/src/Transports.AspNetCore/AuthorizationParameters.cs +++ b/src/Transports.AspNetCore/AuthorizationParameters.cs @@ -13,7 +13,7 @@ namespace GraphQL.Server.Transports.AspNetCore; /// actions based on provided authentication properties/attributes/etc. /// It is not intended to be called from user code. /// -public struct AuthorizationParameters +public readonly struct AuthorizationParameters { /// /// Initializes an instance with a specified @@ -38,34 +38,34 @@ public AuthorizationParameters( /// /// Gets or sets the for the request. /// - public HttpContext HttpContext { get; set; } + public HttpContext HttpContext { get; } /// - public bool AuthorizationRequired { get; set; } + public bool AuthorizationRequired { get; } /// - public List? AuthorizedRoles { get; set; } + public List? AuthorizedRoles { get; } /// - public string? AuthorizedPolicy { get; set; } + public string? AuthorizedPolicy { get; } /// /// A delegate which executes if is set /// but returns . /// - public Func? OnNotAuthenticated { get; set; } + public Func? OnNotAuthenticated { get; } /// /// A delegate which executes if is set but /// returns /// for all roles. /// - public Func? OnNotAuthorizedRole { get; set; } + public Func? OnNotAuthorizedRole { get; } /// /// A delegate which executes if is set but /// /// returns an unsuccessful for the specified policy. /// - public Func? OnNotAuthorizedPolicy { get; set; } + public Func? OnNotAuthorizedPolicy { get; } } diff --git a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt index ad6423fb..bfdb297f 100644 --- a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -17,16 +17,16 @@ namespace GraphQL.Server.Transports.AspNetCore { public static System.Threading.Tasks.ValueTask AuthorizeAsync(GraphQL.Server.Transports.AspNetCore.AuthorizationParameters options, TState state) { } } - public struct AuthorizationParameters + public readonly struct AuthorizationParameters { public AuthorizationParameters(Microsoft.AspNetCore.Http.HttpContext httpContext, GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions middlewareOptions, System.Func? onNotAuthenticated, System.Func? onNotAuthorizedRole, System.Func? onNotAuthorizedPolicy) { } - public bool AuthorizationRequired { get; set; } - public string? AuthorizedPolicy { get; set; } - public System.Collections.Generic.List? AuthorizedRoles { get; set; } - public Microsoft.AspNetCore.Http.HttpContext HttpContext { get; set; } - public System.Func? OnNotAuthenticated { get; set; } - public System.Func? OnNotAuthorizedPolicy { get; set; } - public System.Func? OnNotAuthorizedRole { get; set; } + public bool AuthorizationRequired { get; } + public string? AuthorizedPolicy { get; } + public System.Collections.Generic.List? AuthorizedRoles { get; } + public Microsoft.AspNetCore.Http.HttpContext HttpContext { get; } + public System.Func? OnNotAuthenticated { get; } + public System.Func? OnNotAuthorizedPolicy { get; } + public System.Func? OnNotAuthorizedRole { get; } } public abstract class GraphQLHttpMiddleware { From 0edb99bad13e7c488c6b96eefe06daa8a75c714d Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 19 May 2022 17:28:19 -0400 Subject: [PATCH 22/47] Update HandlePost comment --- src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs b/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs index f1b33131..5a7a71b4 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs @@ -19,6 +19,13 @@ public class GraphQLHttpMiddlewareOptions /// /// Enables handling of POST requests, including form submissions, JSON-formatted requests and raw query requests. + /// Supported media types are: + /// + /// application/x-www-form-urlencoded + /// multipart/form-data + /// application/json + /// application/graphql + /// /// public bool HandlePost { get; set; } = true; From 7ba55ff0ff7cb29d0ff10f2911e4a34c8a215006 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 19 May 2022 18:25:42 -0400 Subject: [PATCH 23/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 862d9545..2d989c3f 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -632,7 +632,8 @@ protected virtual Task WriteErrorResponseAsync(HttpContext context, HttpStatusCo { var result = new ExecutionResult { - Errors = new ExecutionErrors { + Errors = new ExecutionErrors + { executionError }, }; From 7a11025eabe067fc064748825c43ec1fc95fd676 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 19 May 2022 18:26:44 -0400 Subject: [PATCH 24/47] Update src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs b/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs index 13727269..c8f1eb04 100644 --- a/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs +++ b/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs @@ -5,7 +5,7 @@ namespace GraphQL.Server.Transports.AspNetCore.Errors; /// -/// Represents an error indicating that the content-type was invalid. +/// Represents an error indicating that the content-type is invalid, for example, could not be parsed or is not supported. /// public class InvalidContentTypeError : RequestError { From 0f2e282f4522366a5959e63d6701c3829116b783 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Thu, 19 May 2022 18:27:26 -0400 Subject: [PATCH 25/47] Update src/Transports.AspNetCore/Errors/WebSocketSubProtocolNotSupportedError.cs Co-authored-by: Ivan Maximov --- .../Errors/WebSocketSubProtocolNotSupportedError.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/Errors/WebSocketSubProtocolNotSupportedError.cs b/src/Transports.AspNetCore/Errors/WebSocketSubProtocolNotSupportedError.cs index 973079cd..d56b678d 100644 --- a/src/Transports.AspNetCore/Errors/WebSocketSubProtocolNotSupportedError.cs +++ b/src/Transports.AspNetCore/Errors/WebSocketSubProtocolNotSupportedError.cs @@ -11,7 +11,7 @@ public class WebSocketSubProtocolNotSupportedError : RequestError { /// public WebSocketSubProtocolNotSupportedError(IEnumerable requestedSubProtocols) - : base($"Invalid WebSocket sub-protocol(s): {string.Join(",", requestedSubProtocols.Select(x => $"'{x}'"))}") + : base($"Invalid requested WebSocket sub-protocol(s): {string.Join(",", requestedSubProtocols.Select(x => $"'{x}'"))}") { } } From ed44adeac83b654d5f818dea3b9b66fb1eb8cdc1 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 20 May 2022 15:25:25 -0400 Subject: [PATCH 26/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 2d989c3f..17403992 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -12,7 +12,6 @@ namespace GraphQL.Server.Transports.AspNetCore; - /// /// /// Type of GraphQL schema that is used to validate and process requests. From 0c84acdfc6f49cdec2d75d5ac7a62f7a47e2d664 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Fri, 20 May 2022 16:08:55 -0400 Subject: [PATCH 27/47] Update NRT --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 4 ++-- .../GraphQL.Server.Transports.AspNetCore.approved.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 1c4a87a5..a2b6742d 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -113,7 +113,7 @@ private static IEnumerable> CreateWebSocketHandlers( ******************************/ /// - protected override async Task ExecuteScopedRequestAsync(HttpContext context, GraphQLRequest request, IDictionary userContext) + protected override async Task ExecuteScopedRequestAsync(HttpContext context, GraphQLRequest? request, IDictionary userContext) { using var scope = _serviceScopeFactory.CreateScope(); try @@ -446,7 +446,7 @@ protected virtual async Task HandleBatchRequestAsync( /// ExecuteRequestAsync, /// disposing of the scope when the asynchronous operation completes. /// - protected abstract Task ExecuteScopedRequestAsync(HttpContext context, GraphQLRequest request, IDictionary userContext); + protected abstract Task ExecuteScopedRequestAsync(HttpContext context, GraphQLRequest? request, IDictionary userContext); /// /// Executes a GraphQL request. diff --git a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt index ff6da198..a0856620 100644 --- a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -34,7 +34,7 @@ namespace GraphQL.Server.Transports.AspNetCore protected GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions Options { get; } protected virtual System.Threading.Tasks.ValueTask> BuildUserContextAsync(Microsoft.AspNetCore.Http.HttpContext context) { } protected abstract System.Threading.Tasks.Task ExecuteRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest? request, System.IServiceProvider serviceProvider, System.Collections.Generic.IDictionary userContext); - protected abstract System.Threading.Tasks.Task ExecuteScopedRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest request, System.Collections.Generic.IDictionary userContext); + protected abstract System.Threading.Tasks.Task ExecuteScopedRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest? request, System.Collections.Generic.IDictionary userContext); protected virtual System.Threading.Tasks.ValueTask HandleAuthorizeAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } protected virtual System.Threading.Tasks.ValueTask HandleAuthorizeWebSocketConnectionAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } protected virtual System.Threading.Tasks.Task HandleBatchRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next, System.Collections.Generic.IList gqlRequests) { } @@ -73,7 +73,7 @@ namespace GraphQL.Server.Transports.AspNetCore { public GraphQLHttpMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, GraphQL.IGraphQLTextSerializer serializer, GraphQL.IDocumentExecuter documentExecuter, Microsoft.Extensions.DependencyInjection.IServiceScopeFactory serviceScopeFactory, GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions options) { } protected override System.Threading.Tasks.Task ExecuteRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest? request, System.IServiceProvider serviceProvider, System.Collections.Generic.IDictionary userContext) { } - protected override System.Threading.Tasks.Task ExecuteScopedRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest request, System.Collections.Generic.IDictionary userContext) { } + protected override System.Threading.Tasks.Task ExecuteScopedRequestAsync(Microsoft.AspNetCore.Http.HttpContext context, GraphQL.Transport.GraphQLRequest? request, System.Collections.Generic.IDictionary userContext) { } } public sealed class HttpGetValidationRule : GraphQL.Validation.IValidationRule { From 254f738d34cd47f8d6899738b8953f053361db20 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Fri, 20 May 2022 16:12:46 -0400 Subject: [PATCH 28/47] Remove MediaTypes static class --- src/Transports.AspNetCore/MediaTypes.cs | 8 -------- .../GraphQL.Server.Transports.AspNetCore.approved.txt | 6 ------ tests/Samples.Server.Tests/BaseTest.cs | 4 ++-- 3 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 src/Transports.AspNetCore/MediaTypes.cs diff --git a/src/Transports.AspNetCore/MediaTypes.cs b/src/Transports.AspNetCore/MediaTypes.cs deleted file mode 100644 index f2622ae6..00000000 --- a/src/Transports.AspNetCore/MediaTypes.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace GraphQL.Server.Transports.AspNetCore; - -public static class MediaType -{ - public const string JSON = "application/json"; - public const string GRAPH_QL = "application/graphql"; - public const string FORM = "application/x-www-form-urlencoded"; -} diff --git a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt index 3e616dae..7699de2f 100644 --- a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -89,12 +89,6 @@ namespace GraphQL.Server.Transports.AspNetCore { System.Threading.Tasks.ValueTask> BuildUserContextAsync(Microsoft.AspNetCore.Http.HttpContext context); } - public static class MediaType - { - public const string FORM = "application/x-www-form-urlencoded"; - public const string GRAPH_QL = "application/graphql"; - public const string JSON = "application/json"; - } public class UserContextBuilder : GraphQL.Server.Transports.AspNetCore.IUserContextBuilder where TUserContext : System.Collections.Generic.IDictionary { diff --git a/tests/Samples.Server.Tests/BaseTest.cs b/tests/Samples.Server.Tests/BaseTest.cs index 11776da0..e82e6d40 100644 --- a/tests/Samples.Server.Tests/BaseTest.cs +++ b/tests/Samples.Server.Tests/BaseTest.cs @@ -85,7 +85,7 @@ protected async Task SendRequestAsync(GraphQLRequest request, RequestTyp case RequestType.PostWithJson: // Details passed in body content as JSON, with url query string params also allowed string json = Serializer.ToJson(request); - var jsonContent = new StringContent(json, Encoding.UTF8, MediaType.JSON); + var jsonContent = new StringContent(json, Encoding.UTF8, "application/json"); response = await Client.PostAsync(url, jsonContent); break; @@ -98,7 +98,7 @@ protected async Task SendRequestAsync(GraphQLRequest request, RequestTyp OperationName = queryStringOverride?.OperationName ?? request.OperationName, Variables = queryStringOverride?.Variables ?? request.Variables }); - var graphContent = new StringContent(request.Query, Encoding.UTF8, MediaType.GRAPH_QL); + var graphContent = new StringContent(request.Query, Encoding.UTF8, "application/graphql"); response = await Client.PostAsync(urlWithParams, graphContent); break; From 77fd8c61e203691f25d2f4a31641c77c4ec3bf66 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Fri, 20 May 2022 16:17:35 -0400 Subject: [PATCH 29/47] Update --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 43247477..d1ae8701 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -114,15 +114,16 @@ private static IEnumerable> CreateWebSocketHandlers( /// protected override async Task ExecuteScopedRequestAsync(HttpContext context, GraphQLRequest? request, IDictionary userContext) { - using var scope = _serviceScopeFactory.CreateScope(); - try + var scope = _serviceScopeFactory.CreateScope(); + if (scope is IAsyncDisposable ad) { - return await ExecuteRequestAsync(context, request, scope.ServiceProvider, userContext); + await using (ad.ConfigureAwait(false)) + return await ExecuteRequestAsync(context, request, scope.ServiceProvider, userContext); } - finally + else { - if (scope is IAsyncDisposable ad) - await ad.DisposeAsync(); + using (scope) + return await ExecuteRequestAsync(context, request, scope.ServiceProvider, userContext); } } From 73c1ac6bb0761fa4059c2781976ebbde5396eb3b Mon Sep 17 00:00:00 2001 From: Shane32 Date: Fri, 20 May 2022 16:18:14 -0400 Subject: [PATCH 30/47] Update --- tests/Samples.Server.Tests/BaseTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Samples.Server.Tests/BaseTest.cs b/tests/Samples.Server.Tests/BaseTest.cs index e82e6d40..c79d6277 100644 --- a/tests/Samples.Server.Tests/BaseTest.cs +++ b/tests/Samples.Server.Tests/BaseTest.cs @@ -1,7 +1,6 @@ using System.Text; using GraphQL.Samples.Server; using GraphQL.Server; -using GraphQL.Server.Transports.AspNetCore; using GraphQL.Transport; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Hosting; From 38df9250e0a63f916bface00d146f4e77919c27e Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 20 May 2022 16:26:47 -0400 Subject: [PATCH 31/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index d1ae8701..03e9e871 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -15,7 +15,10 @@ namespace GraphQL.Server.Transports.AspNetCore; /// /// /// Type of GraphQL schema that is used to validate and process requests. -/// Specifying will pull the registered default schema. +/// This may be a typed schema as well as . In both cases registered schemas will be pulled from +/// the dependency injection framework. Note that when specifying the first schema registered via +/// AddSchema +/// will be pulled (the "default" schema). /// public class GraphQLHttpMiddleware : GraphQLHttpMiddleware where TSchema : ISchema From 05f8e85456b5186485be0b46e5ebd8be484f409a Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 20 May 2022 16:36:48 -0400 Subject: [PATCH 32/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 03e9e871..bd6c0136 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -340,7 +340,6 @@ public virtual async Task InvokeAsync(HttpContext context) OperationName = urlGQLRequest?.OperationName ?? bodyGQLRequest?.OperationName }; - // Prepare context and execute await HandleRequestAsync(context, _next, gqlRequest); } else if (Options.EnableBatchedRequests) From ce950c5431d0805f0b26a711a84c7161d24f7aee Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 20 May 2022 16:41:57 -0400 Subject: [PATCH 33/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index bd6c0136..acaa62ba 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -376,7 +376,7 @@ protected virtual async ValueTask HandleAuthorizeAsync(HttpContext context /// request was handled (typically by returning an error message). If /// is returned, the request is processed normally. ///

- /// By default this does not check authorization rules becuase authentication may take place within + /// By default this does not check authorization rules because authentication may take place within /// the WebSocket connection during the ConnectionInit message. Authorization checks for /// WebSocket connections occur then, after authorization has taken place. ///
From d9b852927ac1e8a26a8c692ddbd759eed35cf7c7 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Fri, 20 May 2022 16:48:02 -0400 Subject: [PATCH 34/47] Invert condition --- .../GraphQLHttpMiddleware.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index acaa62ba..220d35c7 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -417,14 +417,7 @@ protected virtual async Task HandleBatchRequestAsync( else { // Batched execution with multiple graphql requests - if (!Options.ExecuteBatchedRequestsInParallel) - { - for (int i = 0; i < gqlRequests.Count; i++) - { - results[i] = await ExecuteRequestAsync(context, gqlRequests[i], context.RequestServices, userContext); - } - } - else + if (Options.ExecuteBatchedRequestsInParallel) { var resultTasks = new Task[gqlRequests.Count]; for (int i = 0; i < gqlRequests.Count; i++) @@ -437,6 +430,13 @@ protected virtual async Task HandleBatchRequestAsync( results[i] = await resultTasks[i]; } } + else + { + for (int i = 0; i < gqlRequests.Count; i++) + { + results[i] = await ExecuteRequestAsync(context, gqlRequests[i], context.RequestServices, userContext); + } + } } await WriteJsonResponseAsync(context, HttpStatusCode.OK, results); } From c02e17ba6b0f573097e9d039af6f1b0eebd59580 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Fri, 20 May 2022 17:01:20 -0400 Subject: [PATCH 35/47] Update --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 220d35c7..e807f03c 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -17,7 +17,7 @@ namespace GraphQL.Server.Transports.AspNetCore; /// Type of GraphQL schema that is used to validate and process requests. /// This may be a typed schema as well as . In both cases registered schemas will be pulled from /// the dependency injection framework. Note that when specifying the first schema registered via -/// AddSchema +/// AddSchema /// will be pulled (the "default" schema). /// public class GraphQLHttpMiddleware : GraphQLHttpMiddleware From bd145826d73c6d962449f30ed80de15855d56a10 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Fri, 20 May 2022 17:02:08 -0400 Subject: [PATCH 36/47] Update --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index e807f03c..38d5f7f9 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -475,7 +475,7 @@ protected virtual async Task HandleBatchRequestAsync( /// . ///

/// To tailor the user context individually for each request, call - /// + /// /// to set or modify the user context, pulling the HTTP context from /// via /// if needed. From b4c6501ba36b6ae1ab7d8bf387783a2fc9e4e51e Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 20 May 2022 17:03:03 -0400 Subject: [PATCH 37/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 38d5f7f9..b1bd9406 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -495,7 +495,7 @@ protected virtual async Task HandleBatchRequestAsync( } /// - /// Writes the specified object (usually a GraphQL response) as JSON to the HTTP response stream. + /// Writes the specified object (usually a GraphQL response represented as an instance of ) as JSON to the HTTP response stream. /// protected virtual Task WriteJsonResponseAsync(HttpContext context, HttpStatusCode httpStatusCode, TResult result) { From b9e3636eb3b0263df390d940a8367199fdb70c6d Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 20 May 2022 17:14:35 -0400 Subject: [PATCH 38/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index b1bd9406..6e76613a 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -578,7 +578,7 @@ protected virtual Task HandleNotAuthorizedPolicyAsync(HttpContext context, Reque /// /// Writes a '400 JSON body text could not be parsed.' message to the output. /// Return to rethrow the exception or - /// if it has been handled. + /// if it has been handled. By default returns . /// protected virtual async ValueTask HandleDeserializationErrorAsync(HttpContext context, RequestDelegate next, Exception exception) { From 65a177e12de6368b2b4ced7ad984e6b44f6d525c Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 20 May 2022 17:14:53 -0400 Subject: [PATCH 39/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 6e76613a..e606edbf 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -593,7 +593,7 @@ protected virtual Task HandleBatchedRequestsNotSupportedAsync(HttpContext contex => WriteErrorResponseAsync(context, HttpStatusCode.BadRequest, new BatchedRequestsNotSupportedError()); /// - /// Writes a '400 Invalid WebSocket sub-protocol.' message to the output. + /// Writes a '400 Invalid requested WebSocket sub-protocol(s).' message to the output. /// protected virtual Task HandleWebSocketSubProtocolNotSupportedAsync(HttpContext context, RequestDelegate next) => WriteErrorResponseAsync(context, HttpStatusCode.BadRequest, new WebSocketSubProtocolNotSupportedError(context.WebSockets.WebSocketRequestedProtocols)); From 60c9fbc710da37d6e783484112896da66be4a658 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 20 May 2022 17:18:34 -0400 Subject: [PATCH 40/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index e606edbf..fb61b14d 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -605,7 +605,7 @@ protected virtual Task HandleContentTypeCouldNotBeParsedErrorAsync(HttpContext c => WriteErrorResponseAsync(context, HttpStatusCode.UnsupportedMediaType, new InvalidContentTypeError($"value '{context.Request.ContentType}' could not be parsed.")); /// - /// Writes a '415 Invalid Content-Type header: non-supported type.' message to the output. + /// Writes a '415 Invalid Content-Type header: non-supported media type.' message to the output. /// protected virtual Task HandleInvalidContentTypeErrorAsync(HttpContext context, RequestDelegate next) => WriteErrorResponseAsync(context, HttpStatusCode.UnsupportedMediaType, new InvalidContentTypeError($"non-supported media type '{context.Request.ContentType}'. Must be '{MEDIATYPE_JSON}', '{MEDIATYPE_GRAPHQL}' or a form body.")); From 0318669ba0447ffb929fd98c99caf717e09f5c57 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 20 May 2022 17:39:24 -0400 Subject: [PATCH 41/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index fb61b14d..e40fcf8e 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -643,7 +643,7 @@ protected virtual Task WriteErrorResponseAsync(HttpContext context, HttpStatusCo return WriteJsonResponseAsync(context, httpStatusCode, result); } - private GraphQLRequest DeserializeFromQueryString(IQueryCollection queryCollection) => new GraphQLRequest + private GraphQLRequest DeserializeFromQueryString(IQueryCollection queryCollection) => new() { Query = queryCollection.TryGetValue(QUERY_KEY, out var queryValues) ? queryValues[0] : null!, Variables = Options.ReadVariablesFromQueryString && queryCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize(variablesValues[0]) : null, From d67efe245a7169b89feaa769285d0f3ef61d647b Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 20 May 2022 17:39:31 -0400 Subject: [PATCH 42/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index e40fcf8e..7621389e 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -651,7 +651,7 @@ protected virtual Task WriteErrorResponseAsync(HttpContext context, HttpStatusCo OperationName = queryCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null, }; - private GraphQLRequest DeserializeFromFormBody(IFormCollection formCollection) => new GraphQLRequest + private GraphQLRequest DeserializeFromFormBody(IFormCollection formCollection) => new() { Query = formCollection.TryGetValue(QUERY_KEY, out var queryValues) ? queryValues[0] : null!, Variables = formCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize(variablesValues[0]) : null, From ef8dbcf828a4f90a7b597554ff416a085032c49f Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 20 May 2022 17:39:39 -0400 Subject: [PATCH 43/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 7621389e..bb90ad15 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -653,7 +653,7 @@ protected virtual Task WriteErrorResponseAsync(HttpContext context, HttpStatusCo private GraphQLRequest DeserializeFromFormBody(IFormCollection formCollection) => new() { - Query = formCollection.TryGetValue(QUERY_KEY, out var queryValues) ? queryValues[0] : null!, + Query = formCollection.TryGetValue(QUERY_KEY, out var queryValues) ? queryValues[0] : null, Variables = formCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize(variablesValues[0]) : null, Extensions = formCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize(extensionsValues[0]) : null, OperationName = formCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null, From e8920ff059669e46f79ff0d67fb81f8b06b46f46 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 20 May 2022 17:39:46 -0400 Subject: [PATCH 44/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index bb90ad15..51025279 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -645,7 +645,7 @@ protected virtual Task WriteErrorResponseAsync(HttpContext context, HttpStatusCo private GraphQLRequest DeserializeFromQueryString(IQueryCollection queryCollection) => new() { - Query = queryCollection.TryGetValue(QUERY_KEY, out var queryValues) ? queryValues[0] : null!, + Query = queryCollection.TryGetValue(QUERY_KEY, out var queryValues) ? queryValues[0] : null, Variables = Options.ReadVariablesFromQueryString && queryCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize(variablesValues[0]) : null, Extensions = Options.ReadExtensionsFromQueryString && queryCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize(extensionsValues[0]) : null, OperationName = queryCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null, From 7557bba3441636d5807c7414608e70f3f62c297c Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 20 May 2022 17:40:09 -0400 Subject: [PATCH 45/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 51025279..3eac63a6 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -670,7 +670,7 @@ private static async Task DeserializeFromGraphBodyAsync(Stream b // read query text string query = await streamReader.ReadToEndAsync(); - // return request + // return request; application/graphql MediaType supports only query text return new GraphQLRequest { Query = query }; } From dc7354a9b6f4d9943d20d19f09703f8094e772b1 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 20 May 2022 17:41:24 -0400 Subject: [PATCH 46/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 3eac63a6..e23b436b 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -675,7 +675,7 @@ private static async Task DeserializeFromGraphBodyAsync(Stream b } #if NET5_0_OR_GREATER - private static bool TryGetEncoding(string? charset, out System.Text.Encoding encoding) + private static bool TryGetEncoding(string? charset, out System.Text.Encoding? encoding) { encoding = null!; From 5f7979627f67bf950104023c62723e107356ad09 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 20 May 2022 17:41:31 -0400 Subject: [PATCH 47/47] Update src/Transports.AspNetCore/GraphQLHttpMiddleware.cs Co-authored-by: Ivan Maximov --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index e23b436b..93a0ffbf 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -677,7 +677,7 @@ private static async Task DeserializeFromGraphBodyAsync(Stream b #if NET5_0_OR_GREATER private static bool TryGetEncoding(string? charset, out System.Text.Encoding? encoding) { - encoding = null!; + encoding = null; if (string.IsNullOrEmpty(charset)) return true;