diff --git a/Directory.Build.props b/Directory.Build.props index f07b1137..1309e405 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 diff --git a/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs b/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs index e2d752fc..5c844a46 100644 --- a/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs +++ b/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs @@ -1,4 +1,8 @@ +#nullable enable + +using System.Diagnostics; using GraphQL.Server.Transports.AspNetCore; +using GraphQL.Transport; using GraphQL.Types; namespace GraphQL.Samples.Server; @@ -10,32 +14,28 @@ public class GraphQLHttpMiddlewareWithLogs : GraphQLHttpMiddleware> logger, - IGraphQLTextSerializer requestDeserializer) - : base(requestDeserializer) + 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) { - if (requestExecutionResult.Result.Errors != null) + var timer = Stopwatch.StartNew(); + var ret = await base.ExecuteRequestAsync(context, request, serviceProvider, userContext); + if (ret.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); + _logger.LogError("GraphQL execution completed in {Elapsed} with error(s): {Errors}", timer.Elapsed, ret.Errors); } else - _logger.LogInformation("GraphQL execution successfully completed in {Elapsed}", requestExecutionResult.Elapsed); - - return base.RequestExecutedAsync(requestExecutionResult); - } + _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 381bbf2f..ee80f2e8 100644 --- a/samples/Samples.Server/Startup.cs +++ b/samples/Samples.Server/Startup.cs @@ -4,6 +4,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; @@ -34,7 +35,6 @@ public void ConfigureServices(IServiceCollection services) services.AddGraphQL(builder => builder .AddApolloTracing() - .AddHttpMiddleware>() .AddWebSocketsHttpMiddleware() .AddSchema() .ConfigureExecutionOptions(options => @@ -63,7 +63,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 33decaa7..4df68fa0 100644 --- a/samples/Samples.Server/StartupWithRouting.cs +++ b/samples/Samples.Server/StartupWithRouting.cs @@ -35,7 +35,6 @@ public void ConfigureServices(IServiceCollection services) services.AddGraphQL(builder => builder .AddApolloTracing() - .AddHttpMiddleware>() .AddWebSocketsHttpMiddleware() .AddSchema() .ConfigureExecutionOptions(options => @@ -69,7 +68,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..f87c9315 --- /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..7acf36ef --- /dev/null +++ b/src/Transports.AspNetCore/AuthorizationParameters.cs @@ -0,0 +1,71 @@ +#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. +/// 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 readonly 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; } + + /// + public bool AuthorizationRequired { get; } + + /// + public List? AuthorizedRoles { get; } + + /// + public string? AuthorizedPolicy { get; } + + /// + /// A delegate which executes if is set + /// but returns . + /// + public Func? OnNotAuthenticated { get; } + + /// + /// A delegate which executes if is set but + /// returns + /// for all roles. + /// + public Func? OnNotAuthorizedRole { get; } + + /// + /// A delegate which executes if is set but + /// + /// returns an unsuccessful for the specified policy. + /// + public Func? OnNotAuthorizedPolicy { get; } +} 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..1594f800 --- /dev/null +++ b/src/Transports.AspNetCore/Errors/BatchedRequestsNotSupportedError.cs @@ -0,0 +1,14 @@ +#nullable enable + +using GraphQL.Execution; + +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..c8f1eb04 --- /dev/null +++ b/src/Transports.AspNetCore/Errors/InvalidContentTypeError.cs @@ -0,0 +1,17 @@ +#nullable enable + +using GraphQL.Execution; + +namespace GraphQL.Server.Transports.AspNetCore.Errors; + +/// +/// Represents an error indicating that the content-type is invalid, for example, could not be parsed or is not supported. +/// +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..94d3a626 --- /dev/null +++ b/src/Transports.AspNetCore/Errors/JsonInvalidError.cs @@ -0,0 +1,17 @@ +#nullable enable + +using GraphQL.Execution; + +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/WebSocketSubProtocolNotSupportedError.cs b/src/Transports.AspNetCore/Errors/WebSocketSubProtocolNotSupportedError.cs new file mode 100644 index 00000000..d56b678d --- /dev/null +++ b/src/Transports.AspNetCore/Errors/WebSocketSubProtocolNotSupportedError.cs @@ -0,0 +1,17 @@ +#nullable enable + +using GraphQL.Execution; + +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 requested 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 518882bc..00000000 --- a/src/Transports.AspNetCore/Extensions/GraphQLBuilderMiddlewareExtensions.cs +++ /dev/null @@ -1,28 +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 0ca17f24..47f14362 100644 --- a/src/Transports.AspNetCore/Extensions/GraphQLBuilderUserContextExtensions.cs +++ b/src/Transports.AspNetCore/Extensions/GraphQLBuilderUserContextExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + using GraphQL.DI; using GraphQL.Server.Transports.AspNetCore; using Microsoft.AspNetCore.Http; @@ -22,9 +24,10 @@ public static IGraphQLBuilder AddUserContextBuilder(this IG { 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); } }); @@ -39,14 +42,15 @@ public static IGraphQLBuilder AddUserContextBuilder(this IG /// 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 + where TUserContext : class, IDictionary { builder.Services.Register(new UserContextBuilder(creator)); builder.ConfigureExecutionOptions(options => { if (options.UserContext == null || options.UserContext.Count == 0 && options.UserContext.GetType() == typeof(Dictionary)) { - var httpContext = options.RequestServices.GetRequiredService().HttpContext; + var requestServices = options.RequestServices ?? throw new MissingRequestServicesException(); + var httpContext = requestServices.GetRequiredService().HttpContext!; options.UserContext = creator(httpContext); } }); @@ -62,14 +66,15 @@ public static IGraphQLBuilder AddUserContextBuilder(this IGraphQLB /// 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 + where TUserContext : class, IDictionary { - builder.Services.Register(new UserContextBuilder(creator)); + builder.Services.Register(new UserContextBuilder(context => new(creator(context)))); builder.ConfigureExecutionOptions(async options => { if (options.UserContext == null || options.UserContext.Count == 0 && options.UserContext.GetType() == typeof(Dictionary)) { - var httpContext = options.RequestServices.GetRequiredService().HttpContext; + var requestServices = options.RequestServices ?? throw new MissingRequestServicesException(); + var httpContext = requestServices.GetRequiredService().HttpContext!; options.UserContext = await creator(httpContext); } }); diff --git a/src/Transports.AspNetCore/Extensions/GraphQLHttpApplicationBuilderExtensions.cs b/src/Transports.AspNetCore/Extensions/GraphQLHttpApplicationBuilderExtensions.cs index 5dd21205..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; @@ -11,58 +13,86 @@ namespace Microsoft.AspNetCore.Builder; public static class GraphQLHttpApplicationBuilderExtensions { /// - /// Add the GraphQL middleware to 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. + ///
+ /// 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. + ///

+ /// 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 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") + public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, string path = "/graphql", Action? configureMiddleware = null) where TSchema : ISchema - => builder.UseGraphQL(new PathString(path)); + => builder.UseGraphQL(new PathString(path), configureMiddleware); /// - /// Add the GraphQL middleware to the HTTP request pipeline + /// 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) + 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>()); + b => b.UseMiddleware>(opts)); } /// - /// Add the GraphQL custom middleware to the HTTP request pipeline + /// Add the GraphQL custom middleware to the HTTP request pipeline for the specified schema. /// - /// The implementation of to use /// 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") - where TSchema : ISchema - where TMiddleware : GraphQLHttpMiddleware - => builder.UseGraphQL(new PathString(path)); + 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 + /// Add the GraphQL custom middleware to the HTTP request pipeline for the specified schema. /// - /// The implementation of to use /// 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) - where TSchema : ISchema - where TMiddleware : GraphQLHttpMiddleware + 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()); + b => b.UseMiddleware(args)); } } diff --git a/src/Transports.AspNetCore/Extensions/GraphQLHttpEndpointRouteBuilderExtensions.cs b/src/Transports.AspNetCore/Extensions/GraphQLHttpEndpointRouteBuilderExtensions.cs index 56f359cf..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; @@ -11,39 +13,49 @@ namespace Microsoft.AspNetCore.Builder; public static class GraphQLHttpEndpointRouteBuilderExtensions { /// - /// Add the GraphQL middleware to 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. + ///
+ /// 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 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 GraphQLHttpEndpointConventionBuilder MapGraphQL(this IEndpointRouteBuilder endpoints, string pattern = "graphql") - where TSchema : ISchema + public static GraphQLEndpointConventionBuilder MapGraphQL(this IEndpointRouteBuilder endpoints, string pattern = "graphql", Action? configureMiddleware = null) + where TSchema : ISchema { - if (endpoints == null) - throw new ArgumentNullException(nameof(endpoints)); + 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")); } /// - /// Add the GraphQL middleware to the HTTP request pipeline + /// Add the GraphQL middleware to the HTTP request pipeline for the specified schema. /// - /// 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 arguments to pass to the middleware type instance's constructor. /// The received as parameter - public static GraphQLHttpEndpointConventionBuilder MapGraphQL(this IEndpointRouteBuilder endpoints, string pattern = "graphql") - where TSchema : ISchema - where TMiddleware : GraphQLHttpMiddleware + public static GraphQLEndpointConventionBuilder MapGraphQL(this IEndpointRouteBuilder endpoints, string pattern = "graphql", params object[] args) + where TMiddleware : GraphQLHttpMiddleware { - if (endpoints == null) - throw new ArgumentNullException(nameof(endpoints)); - - var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware().Build(); - return new GraphQLHttpEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL")); + var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware(args).Build(); + return new GraphQLEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL")); } } @@ -51,11 +63,11 @@ public static GraphQLHttpEndpointConventionBuilder MapGraphQL -public class GraphQLHttpEndpointConventionBuilder : IEndpointConventionBuilder +public class GraphQLEndpointConventionBuilder : IEndpointConventionBuilder { private readonly IEndpointConventionBuilder _builder; - internal GraphQLHttpEndpointConventionBuilder(IEndpointConventionBuilder builder) + internal GraphQLEndpointConventionBuilder(IEndpointConventionBuilder builder) { _builder = builder; } diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 5d03c4ce..93a0ffbf 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -1,43 +1,221 @@ +#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; -/// -/// 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. -///
-/// Type of GraphQL schema that is used to validate and process requests. -public class GraphQLHttpMiddleware : IMiddleware +/// +/// +/// 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 +/// will be pulled (the "default" schema). +/// +public class GraphQLHttpMiddleware : GraphQLHttpMiddleware where TSchema : ISchema { - private const string DOCS_URL = "See: http://graphql.org/learn/serving-over-http/."; + 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 + + /// + /// 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. + /// + 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 }; + } + + private static IEnumerable> CreateWebSocketHandlers( + IGraphQLSerializer serializer, + IDocumentExecuter documentExecuter, + IServiceScopeFactory serviceScopeFactory, + IServiceProvider provider, + IHostApplicationLifetime hostApplicationLifetime, + GraphQLHttpMiddlewareOptions options) + { + 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()), + }; + } + + ******************************/ + + /// + protected override async Task ExecuteScopedRequestAsync(HttpContext context, GraphQLRequest? request, IDictionary userContext) + { + var scope = _serviceScopeFactory.CreateScope(); + if (scope is IAsyncDisposable ad) + { + await using (ad.ConfigureAwait(false)) + return await ExecuteRequestAsync(context, request, scope.ServiceProvider, userContext); + } + else + { + using (scope) + return await ExecuteRequestAsync(context, request, scope.ServiceProvider, userContext); + } + } + + /// + protected override async Task ExecuteRequestAsync(HttpContext context, GraphQLRequest? request, IServiceProvider serviceProvider, IDictionary userContext) + { + 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 GraphQLHttpMiddleware(IGraphQLTextSerializer serializer) + 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 */) { - _serializer = serializer; + _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, RequestDelegate next) + /// + public virtual async Task InvokeAsync(HttpContext context) { if (context.WebSockets.IsWebSocketRequest) { - await next(context); + /************* WebSocket support ************ + if (Options.HandleWebSockets) + { + if (await HandleAuthorizeWebSocketConnectionAsync(context, _next)) + return; + // Process WebSocket request + await HandleWebSocketAsync(context, _next); + } + else + { + await HandleInvalidHttpMethodErrorAsync(context, _next); + } + ************************/ + await HandleInvalidHttpMethodErrorAsync(context, _next); return; } @@ -46,58 +224,59 @@ public virtual async Task InvokeAsync(HttpContext context, RequestDelegate next) 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 && !isPost) + if (isGet && !Options.HandleGet || isPost && !Options.HandlePost || !isGet && !isPost) { - httpResponse.Headers["Allow"] = "GET, POST"; - await HandleInvalidHttpMethodErrorAsync(context); + await HandleInvalidHttpMethodErrorAsync(context, _next); return; } + // Authenticate request if necessary + if (await HandleAuthorizeAsync(context, _next)) + return; + // Parse POST body - GraphQLRequest bodyGQLRequest = null; - IList bodyGQLBatchRequest = null; + GraphQLRequest? bodyGQLRequest = null; + IList? bodyGQLBatchRequest = null; if (isPost) { if (!MediaTypeHeaderValue.TryParse(httpRequest.ContentType, out var mediaTypeHeader)) { - await HandleContentTypeCouldNotBeParsedErrorAsync(context); + await HandleContentTypeCouldNotBeParsedErrorAsync(context, _next); return; } switch (mediaTypeHeader.MediaType) { - case MediaType.JSON: - IList deserializationResult; + case MEDIATYPE_JSON: + IList? deserializationResult; try { #if NET5_0_OR_GREATER if (!TryGetEncoding(mediaTypeHeader.CharSet, out var sourceEncoding)) { - await HandleContentTypeCouldNotBeParsedErrorAsync(context); + 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, cancellationToken); + deserializationResult = await _serializer.ReadAsync>(tempStream, context.RequestAborted); } else { - deserializationResult = await _serializer.ReadAsync>(httpRequest.Body, cancellationToken); + 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, ex)) + if (!await HandleDeserializationErrorAsync(context, _next, ex)) throw; return; } @@ -108,217 +287,395 @@ public virtual async Task InvokeAsync(HttpContext context, RequestDelegate next) bodyGQLBatchRequest = deserializationResult; break; - case MediaType.GRAPH_QL: + case MEDIATYPE_GRAPHQL: bodyGQLRequest = await DeserializeFromGraphBodyAsync(httpRequest.Body); break; default: if (httpRequest.HasFormContentType) { - var formCollection = await httpRequest.ReadFormAsync(cancellationToken); + 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; } break; } - await HandleInvalidContentTypeErrorAsync(context); + await HandleInvalidContentTypeErrorAsync(context, _next); return; } } - // 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 urlGQLRequest = null; - try + // 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; + GraphQLRequest? urlGQLRequest = null; + if (isGet || Options.ReadQueryStringOnPost) { - urlGQLRequest = DeserializeFromQueryString(httpRequest.Query); - } - catch (Exception ex) - { - if (!await HandleDeserializationErrorAsync(context, ex)) - throw; - return; + try + { + urlGQLRequest = DeserializeFromQueryString(httpRequest.Query); + } + catch (Exception 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 + Query = urlGQLRequest?.Query ?? bodyGQLRequest?.Query, + Variables = urlGQLRequest?.Variables ?? bodyGQLRequest?.Variables, + Extensions = urlGQLRequest?.Extensions ?? bodyGQLRequest?.Extensions, + OperationName = urlGQLRequest?.OperationName ?? bodyGQLRequest?.OperationName }; - } - // 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); + await HandleRequestAsync(context, _next, gqlRequest); + } + else if (Options.EnableBatchedRequests) + { + await HandleBatchRequestAsync(context, _next, bodyGQLBatchRequest); + } + else + { + await HandleBatchedRequestsNotSupportedAsync(context, _next); + } + } - var executer = context.RequestServices.GetRequiredService>(); - await HandleRequestAsync(context, next, userContext, bodyGQLBatchRequest, gqlRequest, executer, cancellationToken); + /// + /// 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 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. + ///
+ protected virtual ValueTask HandleAuthorizeWebSocketConnectionAsync(HttpContext context, RequestDelegate next) + => new(false); + + /// + /// Handles a single GraphQL request. + /// protected virtual async Task HandleRequestAsync( HttpContext context, RequestDelegate next, - IDictionary userContext, - IList bodyGQLBatchRequest, - GraphQLRequest gqlRequest, - IDocumentExecuter executer, - CancellationToken cancellationToken) + GraphQLRequest gqlRequest) { // 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 RequestExecutedAsync(new GraphQLRequestExecutionResult(gqlRequest, result, stopwatch.Elapsed)); + 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); + } - await WriteResponseAsync(context.Response, _serializer, cancellationToken, 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); } - // Execute multiple graphql requests in one batch else { - var executionResults = new ExecutionResult[bodyGQLBatchRequest.Count]; - for (int i = 0; i < bodyGQLBatchRequest.Count; ++i) + // Batched execution with multiple graphql requests + if (Options.ExecuteBatchedRequestsInParallel) { - 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; + var resultTasks = new Task[gqlRequests.Count]; + for (int i = 0; i < gqlRequests.Count; i++) + { + resultTasks[i] = ExecuteScopedRequestAsync(context, gqlRequests[i], userContext); + } + await Task.WhenAll(resultTasks); + for (int i = 0; i < gqlRequests.Count; i++) + { + 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 WriteResponseAsync(context.Response, _serializer, cancellationToken, executionResults); } + await WriteJsonResponseAsync(context, HttpStatusCode.OK, results); } - protected virtual async ValueTask HandleDeserializationErrorAsync(HttpContext context, Exception ex) + /// + /// 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); + + /// + /// 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); + + /// + /// 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) { - await WriteErrorResponseAsync(context, $"JSON body text could not be parsed. {ex.Message}", HttpStatusCode.BadRequest); - return true; + var userContextBuilder = context.RequestServices.GetService(); + var userContext = userContextBuilder == null + ? new Dictionary() + : await userContextBuilder.BuildUserContextAsync(context); + return userContext; + } + + /// + /// 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) + { + context.Response.ContentType = MEDIATYPE_JSON; + context.Response.StatusCode = (int)httpStatusCode; + + return _serializer.WriteAsync(context.Response.Body, result, context.RequestAborted); } - protected virtual Task HandleNoQueryErrorAsync(HttpContext context) - => WriteErrorResponseAsync(context, "GraphQL query is missing.", HttpStatusCode.BadRequest); + /****** WebSocket support ********* + + /// + /// Handles a WebSocket connection request. + /// + protected virtual async Task HandleWebSocketAsync(HttpContext context, RequestDelegate next) + { + if (_webSocketHandlers == null || !_webSocketHandlers.Any()) + { + await next(context); + return; + } - protected virtual Task HandleContentTypeCouldNotBeParsedErrorAsync(HttpContext context) - => WriteErrorResponseAsync(context, $"Invalid 'Content-Type' header: value '{context.Request.ContentType}' could not be parsed.", HttpStatusCode.UnsupportedMediaType); + string selectedProtocol; + IWebSocketHandler selectedHandler; + // select a sub-protocol, preferring the first sub-protocol requested by the client + foreach (var protocol in context.WebSockets.WebSocketRequestedProtocols) + { + foreach (var handler in _webSocketHandlers) + { + if (handler.SupportedSubProtocols.Contains(protocol)) + { + selectedProtocol = protocol; + selectedHandler = handler; + goto MatchedHandler; + } + } + } - 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); + await HandleWebSocketSubProtocolNotSupportedAsync(context, next); + return; - protected virtual Task HandleInvalidHttpMethodErrorAsync(HttpContext context) - => WriteErrorResponseAsync(context, $"Invalid HTTP method. Only GET and POST are supported. {DOCS_URL}", HttpStatusCode.MethodNotAllowed); + MatchedHandler: + var socket = await context.WebSockets.AcceptWebSocketAsync(selectedProtocol); - protected virtual Task ExecuteRequestAsync( - GraphQLRequest gqlRequest, - IDictionary userContext, - IDocumentExecuter executer, - IServiceProvider requestServices, - CancellationToken token) - => executer.ExecuteAsync(new ExecutionOptions + if (socket.SubProtocol != selectedProtocol) { - Query = gqlRequest.Query, - OperationName = gqlRequest.OperationName, - Variables = gqlRequest.Variables, - Extensions = gqlRequest.Extensions, - UserContext = userContext, - RequestServices = requestServices, - CancellationToken = token - }); + await socket.CloseAsync( + WebSocketCloseStatus.ProtocolError, + $"Invalid sub-protocol; expected '{selectedProtocol}'", + context.RequestAborted); + return; + } - protected virtual CancellationToken GetCancellationToken(HttpContext context) => context.RequestAborted; + // 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); + } - protected virtual Task RequestExecutingAsync(GraphQLRequest request, int? indexInBatch = 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")); + + /// + /// 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. By default returns . + /// + protected virtual async ValueTask HandleDeserializationErrorAsync(HttpContext context, RequestDelegate next, Exception exception) { - // nothing to do in this middleware - return Task.CompletedTask; + await WriteErrorResponseAsync(context, HttpStatusCode.BadRequest, new JsonInvalidError(exception)); + return true; } - protected virtual Task RequestExecutedAsync(in GraphQLRequestExecutionResult requestExecutionResult) + /// + /// 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 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)); + + /// + /// 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 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.")); + + /// + /// 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) { - // nothing to do in this middleware - return Task.CompletedTask; + //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); } - protected virtual Task WriteErrorResponseAsync(HttpContext context, string errorMessage, HttpStatusCode httpStatusCode) + /// + /// 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 { Errors = new ExecutionErrors { - new ExecutionError(errorMessage) - } + executionError + }, }; - context.Response.ContentType = "application/json"; - context.Response.StatusCode = (int)httpStatusCode; - - return _serializer.WriteAsync(context.Response.Body, result, GetCancellationToken(context)); + return WriteJsonResponseAsync(context, httpStatusCode, result); } - protected virtual Task WriteResponseAsync(HttpResponse httpResponse, IGraphQLSerializer serializer, CancellationToken cancellationToken, TResult result) - { - httpResponse.ContentType = "application/json"; - httpResponse.StatusCode = result is not ExecutionResult executionResult || executionResult.Executed ? 200 : 400; // BadRequest when fails validation; OK otherwise - - return serializer.WriteAsync(httpResponse.Body, result, cancellationToken); - } - - 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 GraphQLRequest DeserializeFromQueryString(IQueryCollection queryCollection) => new GraphQLRequest + private GraphQLRequest DeserializeFromQueryString(IQueryCollection queryCollection) => new() { 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 + 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, }; - 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, 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 + OperationName = formCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null, }; - private async Task DeserializeFromGraphBodyAsync(Stream bodyStream) + /// + /// Reads body of content type: application/graphql + /// + private static async Task DeserializeFromGraphBodyAsync(Stream bodyStream) { - // In this case, the query is the raw value in the POST body + // do not close underlying HTTP connection + using var streamReader = new StreamReader(bodyStream, leaveOpen: true); - // 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(); + // read query text + string query = await streamReader.ReadToEndAsync(); - return new GraphQLRequest { Query = query }; // application/graphql MediaType supports only query text + // return request; application/graphql MediaType supports only query text + return new GraphQLRequest { Query = query }; } #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; diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs b/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs new file mode 100644 index 00000000..5a7a71b4 --- /dev/null +++ b/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs @@ -0,0 +1,116 @@ +#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. + /// Supported media types are: + /// + /// application/x-www-form-urlencoded + /// multipart/form-data + /// application/json + /// application/graphql + /// + /// + 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 ExecuteBatchedRequestsInParallel { 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. + /// 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; } + + /// + /// 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/GraphQLRequestExecutionResult.cs b/src/Transports.AspNetCore/GraphQLRequestExecutionResult.cs deleted file mode 100644 index 1d39f636..00000000 --- a/src/Transports.AspNetCore/GraphQLRequestExecutionResult.cs +++ /dev/null @@ -1,44 +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/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 f7afa4bc..b132afa5 100644 --- a/src/Transports.AspNetCore/IUserContextBuilder.cs +++ b/src/Transports.AspNetCore/IUserContextBuilder.cs @@ -1,16 +1,17 @@ +#nullable enable + using Microsoft.AspNetCore.Http; 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 for one or more GraphQL requests or +/// subscriptions over the same HTTP connection. ///
public interface IUserContextBuilder { - /// - /// Builds the UserContext using the specified - /// - /// The for the current request - /// Returns the UserContext - Task> BuildUserContext(HttpContext httpContext); + /// + ValueTask> BuildUserContextAsync(HttpContext context); } 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/src/Transports.AspNetCore/UserContextBuilder.cs b/src/Transports.AspNetCore/UserContextBuilder.cs index cc8c845e..1c82d002 100644 --- a/src/Transports.AspNetCore/UserContextBuilder.cs +++ b/src/Transports.AspNetCore/UserContextBuilder.cs @@ -1,24 +1,37 @@ +#nullable enable + using Microsoft.AspNetCore.Http; namespace GraphQL.Server.Transports.AspNetCore; +/// +/// Represents a user context builder based on a delegate. +/// public class UserContextBuilder : IUserContextBuilder - where TUserContext : IDictionary + where TUserContext : IDictionary { - private readonly Func> _func; + private readonly Func> _func; - public UserContextBuilder(Func> func) + /// + /// Initializes a new instance with the specified delegate. + /// + public UserContextBuilder(Func> func) { _func = func ?? throw new ArgumentNullException(nameof(func)); } + /// + /// Initializes a new instance with the specified delegate. + /// public UserContextBuilder(Func func) { if (func == null) throw new ArgumentNullException(nameof(func)); - _func = x => Task.FromResult(func(x)); + _func = x => new(func(x)); } - public async Task> BuildUserContext(HttpContext httpContext) => await _func(httpContext); + /// + 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..7699de2f 100644 --- a/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -1,95 +1,160 @@ 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 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; } + 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 + { + 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 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 EnableBatchedRequests { get; set; } + public bool ExecuteBatchedRequestsInParallel { 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) { } - } - 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 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 interface IUserContextBuilder + public sealed class HttpGetValidationRule : GraphQL.Validation.IValidationRule { - System.Threading.Tasks.Task> BuildUserContext(Microsoft.AspNetCore.Http.HttpContext httpContext); + public HttpGetValidationRule() { } + public System.Threading.Tasks.ValueTask ValidateAsync(GraphQL.Validation.ValidationContext context) { } } - public static class MediaType + public sealed class HttpPostValidationRule : GraphQL.Validation.IValidationRule { - public const string FORM = "application/x-www-form-urlencoded"; - public const string GRAPH_QL = "application/graphql"; - public const string JSON = "application/json"; + public HttpPostValidationRule() { } + public System.Threading.Tasks.ValueTask ValidateAsync(GraphQL.Validation.ValidationContext context) { } + } + public interface IUserContextBuilder + { + System.Threading.Tasks.ValueTask> BuildUserContextAsync(Microsoft.AspNetCore.Http.HttpContext context); } 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.Execution.RequestError + { + public BatchedRequestsNotSupportedError() { } + } + public class HttpMethodValidationError : GraphQL.Validation.ValidationError + { + public HttpMethodValidationError(GraphQLParser.ROM originalQuery, GraphQLParser.AST.ASTNode node, string message) { } + } + public class InvalidContentTypeError : GraphQL.Execution.RequestError + { + public InvalidContentTypeError() { } + public InvalidContentTypeError(string message) { } + } + public class JsonInvalidError : GraphQL.Execution.RequestError + { + public JsonInvalidError() { } + public JsonInvalidError(System.Exception innerException) { } + } + public class WebSocketSubProtocolNotSupportedError : GraphQL.Execution.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/BaseTest.cs b/tests/Samples.Server.Tests/BaseTest.cs index 11776da0..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; @@ -85,7 +84,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 +97,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; diff --git a/tests/Samples.Server.Tests/ResponseTests.cs b/tests/Samples.Server.Tests/ResponseTests.cs index 063cb3dd..341e63c9 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, @@ -78,7 +86,10 @@ public async Task Wrong_Query_Should_Return_Error(HttpMethod httpMethod, HttpCon 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[][] @@ -88,8 +99,8 @@ 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, - @"{""errors"":[{""message"":""Invalid HTTP method. Only GET and POST are supported. See: http://graphql.org/learn/serving-over-http/.""}]}", + HttpStatusCode.NotFound, + null, }, // POST with unsupported mime type should be a unsupported media type @@ -98,7 +109,7 @@ 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, - @"{""errors"":[{""message"":""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/.""}]}" + @"{""errors"":[{""message"":""Invalid 'Content-Type' header: non-supported media type 'something/unknown; charset=utf-8'. Must be 'application/json', 'application/graphql' or a form body."",""extensions"":{""code"":""INVALID_CONTENT_TYPE"",""codes"":[""INVALID_CONTENT_TYPE""]}}]}" }, // MediaTypeHeaderValue ctor throws exception @@ -108,7 +119,7 @@ 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, "application/json; charset=utf-3"), // HttpStatusCode.UnsupportedMediaType, - // "Invalid 'Content-Type' header: non-supported media type 'application/json; charset=utf-3'. Must be of 'application/json', 'application/graphql' or 'application/x-www-form-urlencoded'. See: http://graphql.org/learn/serving-over-http/." + // @"{""errors"":[{""message"":""Invalid 'Content-Type' header: non-supported media type 'application/json; charset=utf-3'. Must be 'application/json', 'application/graphql' or a form body."",""extensions"":{""code"":""INVALID_CONTENT_TYPE"",""codes"":[""INVALID_CONTENT_TYPE""]}}]}" //}, // POST with JSON mime type that doesn't start with an object or array token should be a bad request @@ -117,7 +128,7 @@ public async Task Wrong_Query_Should_Return_Error(HttpMethod httpMethod, HttpCon HttpMethod.Post, new StringContent("Oops", Encoding.UTF8, "application/json"), HttpStatusCode.BadRequest, - @"{""errors"":[{""message"":""JSON body text could not be parsed. 'O' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.""}]}" + @"{""errors"":[{""message"":""JSON body text could not be parsed. 'O' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0."",""extensions"":{""code"":""JSON_INVALID"",""codes"":[""JSON_INVALID""]}}]}" }, // POST with JSON mime type that is invalid JSON should be a bad request @@ -126,7 +137,7 @@ public async Task Wrong_Query_Should_Return_Error(HttpMethod httpMethod, HttpCon HttpMethod.Post, new StringContent("{oops}", Encoding.UTF8, "application/json"), HttpStatusCode.BadRequest, - @"{""errors"":[{""message"":""JSON body text could not be parsed. 'o' is an invalid start of a property name. Expected a '""'. Path: $ | LineNumber: 0 | BytePositionInLine: 1.""}]}" + @"{""errors"":[{""message"":""JSON body text could not be parsed. 'o' is an invalid start of a property name. Expected a '""'. Path: $ | LineNumber: 0 | BytePositionInLine: 1."",""extensions"":{""code"":""JSON_INVALID"",""codes"":[""JSON_INVALID""]}}]}" }, // POST with JSON mime type that is null JSON should be a bad request @@ -176,7 +187,6 @@ public async Task Wrong_Query_Should_Return_Error(HttpMethod httpMethod, HttpCon }; [Theory] - [InlineData(RequestType.Get)] [InlineData(RequestType.PostWithJson)] [InlineData(RequestType.PostWithGraph)] [InlineData(RequestType.PostWithForm)] @@ -193,7 +203,6 @@ public async Task Serializer_Should_Handle_Inline_Variables(RequestType requestT } [Theory] - [InlineData(RequestType.Get)] [InlineData(RequestType.PostWithJson)] [InlineData(RequestType.PostWithGraph)] [InlineData(RequestType.PostWithForm)] @@ -211,7 +220,6 @@ public async Task Serializer_Should_Handle_Variables(RequestType requestType) } [Theory] - [InlineData(RequestType.Get)] [InlineData(RequestType.PostWithJson)] [InlineData(RequestType.PostWithGraph)] [InlineData(RequestType.PostWithForm)]