This package is designed for ASP.Net Core (2.1 through 9.0) to facilitate easy set-up of GraphQL requests
over HTTP. The code is designed to be used as middleware within the ASP.Net Core pipeline,
serving GET, POST or WebSocket requests. GET requests process requests from the querystring.
POST requests can be in the form of JSON requests, form submissions, or raw GraphQL strings.
Form submissions either accepts query
, operationName
, variables
and extensions
parameters,
or operations
and map
parameters along with file uploads as defined in the
GraphQL multipart request spec.
WebSocket requests can use the graphql-ws
or graphql-transport-ws
WebSocket sub-protocol,
as defined in the apollographql/subscriptions-transport-ws
and enisdenjo/graphql-ws respoitories, respectively.
The graphql-subscriptions
sub-protocol is not supported.
The middleware can be configured through the IApplicationBuilder
or IEndpointRouteBuilder
builder interfaces.
In addition, an ExecutionResultActionResult
class is added for returning ExecutionResult
instances directly from a controller action.
Authorization is also supported with the included AuthorizationValidationRule
. It will
scan GraphQL documents and validate that the schema and all referenced output graph types, fields of
output graph types, and query arguments meet the specified policy and/or roles held by the
authenticated user within the ASP.NET Core authorization framework. It does not validate
any policies or roles specified for input graph types, fields of input graph types, or
directives. It does not skip validation for fields that are marked with the @skip
or
@include
directives.
See version history / migration notes for changes from previous versions.
First add the GraphQL.AspNetCore3
nuget package to your application. It requires
GraphQL
version 8.2.1 or a later.
Then update your Program.cs
or Startup.cs
to register the schema, the serialization engine,
and optionally the HTTP middleware and WebSocket services. Configure WebSockets and GraphQL
in the HTTP pipeline by calling UseWebSockets
and UseGraphQL
at the appropriate point.
Below is a complete sample of a .NET 6 console app that hosts a GraphQL endpoint at
http://localhost:5000/graphql
:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GraphQL.AspNetCore3" Version="5.0.0" />
</ItemGroup>
</Project>
using GraphQL;
using GraphQL.MicrosoftDI;
using GraphQL.SystemTextJson;
using GraphQL.AspNetCore3;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGraphQL(b => b
.AddAutoSchema<Query>() // schema
.AddSystemTextJson()); // serializer
var app = builder.Build();
app.UseDeveloperExceptionPage();
app.UseWebSockets();
app.UseGraphQL("/graphql"); // url to host GraphQL endpoint
await app.RunAsync();
public class Query
{
public static string Hero() => "Luke Skywalker";
}
http://localhost:5000/graphql?query={hero}
{"data":{"hero":"Luke Skywalker"}}
By default, the middleware will be installed with these configurable options:
- GET, POST, and WebSocket requests are all enabled
- Form content types are disabled, and cross-site request forgery (CSRF) protection is enabled
- There are no authentication or authorization requirements
- The default response content type is
application/graphql-response+json
- The middleware will use the default schema instance
To configure these options, pass a confiuguration delegate to the UseGraphQL
method as demonstrated below:
app.UseGraphQL("/graphql", opts => {
opts.ReadFormOnPost = true;
});
Configuration of these options and more are further described below in this document.
To use endpoint routing, call MapGraphQL
from inside the endpoint configuration
builder rather than UseGraphQL
on the application builder. See below for the
sample of the application builder code:
var app = builder.Build();
app.UseDeveloperExceptionPage();
app.UseWebSockets();
app.UseRouting();
app.UseEndpoints(endpoints => {
endpoints.MapGraphQL("graphql");
});
await app.RunAsync();
Using endpoint routing is particularly useful when you want to select a specific CORS configuration for the GraphQL endpoint. See the CORS section below for a sample.
Please note that when using endpoint routing, you cannot use WebSocket connections while a UI package is also configured at the same URL. You will need to use a different URL for the UI package, or use UI middleware prior to endpoint routing. So long as different URLs are used, there are no issues. Below is a sample when the UI and GraphQL reside at the same URL:
var app = builder.Build();
app.UseDeveloperExceptionPage();
app.UseWebSockets();
app.UseRouting();
app.UseGraphQLVoyager("/graphql");
app.UseEndpoints(endpoints =>
{
endpoints.MapGraphQL("/graphql");
});
await app.RunAsync();
Although not recommended, you may set up a controller action to execute GraphQL
requests. You will not need UseGraphQL
or MapGraphQL
in the application
startup. Below is a very basic sample; a much more complete sample can be found
in the ControllerSample
project within this repository. Although technically
possible, the sample does not include support for WebSocket requests.
public class HomeController : Controller
{
private readonly IDocumentExecuter _documentExecuter;
public TestController(IDocumentExecuter<ISchema> documentExecuter)
{
_documentExecuter = documentExecuter;
}
[HttpGet]
public async Task<IActionResult> GraphQL(string query)
{
var result = await _documentExecuter.ExecuteAsync(new() {
Query = query,
RequestServices = HttpContext.RequestServices,
CancellationToken = HttpContext.RequestAborted,
});
return new ExecutionResultActionResult(result);
}
}
To set the user context to be used during the execution of GraphQL requests,
call AddUserContextBuilder
during the GraphQL service setup to set a delegate
which will be called when the user context is built. Alternatively, you can
register an IUserContextBuilder
implementation to do the same.
builder.Services.AddGraphQL(b => b
.AddAutoSchema<Query>()
.AddSystemTextJson()
.AddUserContextBuilder(httpContext => new MyUserContext(httpContext));
public class MyUserContext : Dictionary<string, object?>
{
public ClaimsPrincipal User { get; }
public MyUserContext(HttpContext context) : base()
{
User = context.User;
}
}
You can configure authorization for all GraphQL requests, or for individual graphs, fields and query arguments within your schema. Both can be used if desired.
Be sure to call app.UseAuthentication()
and app.UseAuthorization()
prior
to the call to app.UseGraphQL()
. For example:
app.UseAuthentication();
app.UseAuthorization();
app.UseWebSockets();
app.UseGraphQL("/graphql");
When calling UseGraphQL, specify options as necessary to enable authorization as required.
app.UseGraphQL("/graphql", config => {
// require that the user be authenticated
config.AuthorizationRequired = true;
// require that the user be a member of at least one role listed
config.AuthorizedRoles.Add("MyRole");
config.AuthorizedRoles.Add("MyAlternateRole");
// require that the user pass a specific authorization policy
config.AuthorizedPolicy = "MyPolicy";
});
Once configured, the request is authorized prior to parsing of the document or accepting
the WebSocket request. Since WebSocket requests from browsers cannot typically carry a HTTP
Authorization header, for JWT Bearer authentication you may need to disable authorization
requirements for WebSocket connections and instead install an authentication handler for
connection requests. To do so, use set the WebSocketsRequireAuthorization
property to
false
and register an IWebSocketAuthorizationService
as a singleton within your
dependency injection framework. Below is an example:
services.AddSingleton<IWebSocketAuthorizationService, MyAuthService>();
app.UseGraphQL("/graphql", config => {
// require that the user be authenticated
config.AuthorizationRequired = true;
});
class MyAuthService : IWebSocketAuthenticationService
{
private readonly IGraphQLSerializer _serializer;
public MyWSAuthService(IGraphQLSerializer serializer)
{
_serializer = serializer;
}
public async ValueTask<bool> AuthenticateAsync(IWebSocketConnection connection, OperationMessage operationMessage)
{
var payload = _serializer.ReadNode<Inputs>(operationMessage.Payload);
if (payload?.TryGetValue("Authorization", out var value) ?? false) {
ClaimsPrincipal user;
// validate token here and set user
connection.HttpContext.User = user;
return true; // authentication successful
}
return false; // authentication failed
}
}
You may also wish to use IWebSocketAuthenticationService
to set the HttpContext.User
property, even
if you allow anonymous requests.
To configure ASP.NET Core authorization for GraphQL, add the corresponding
validation rule during GraphQL configuration, typically by calling .AddAuthorizationRule()
as shown below:
builder.Services.AddGraphQL(b => b
.AddAutoSchema<Query>()
.AddSystemTextJson()
.AddAuthorizationRule());
Both roles and policies are supported for output graph types, fields on output graph types,
and query arguments. If multiple policies are specified, all must match; if multiple roles
are specified, any one role must match. You may also use .Authorize()
or the
[Authorize]
attribute to validate that the user has authenticated. You may also use
.AllowAnonymous()
and [AllowAnonymous]
to allow fields to be returned to
unauthenticated users within an graph that has an authorization requirement defined.
Please note that authorization rules do not apply to values returned within introspection requests,
potentially leaking information about protected areas of the schema to unauthenticated users.
You may use the ISchemaFilter
to restrict what information is returned from introspection
requests, but it will apply to both authenticated and unauthenticated users alike.
Introspection requests are allowed unless the schema has an authorization requirement set on it.
The @skip
and @include
directives are honored, skipping authorization checks for fields
skipped by @skip
or @include
.
Please note that if you use interfaces, validation might be executed against the graph field or the interface field, depending on the structure of the query. For instance:
{
cat {
# validates against Cat.Name
name
# validates against Animal.Name
... on Animal {
name
}
}
}
Similarly for unions, validation occurs on the exact type that is queried. Be sure to carefully
consider placement of authorization rules when using interfaces and unions, especially when some
fields are marked with AllowAnonymous
.
To provide custom authentication code, bypassing ASP.NET Core's authentication, derive from the
GraphQLHttpMiddleware<T>
class and override HandleAuthorizeAsync
, setting HttpContext.User
to an appropriate ClaimsPrincipal
instance.
See 'Customizing middleware behavior' below for an example of deriving from GraphQLHttpMiddleware
.
Since WebSocket requests from browsers cannot typically carry a HTTP Authorization header, you
will need to authorize requests via the ConnectionInit
WebSocket message or carry the authorization
token within the URL. Below is a sample of the former:
builder.Services.AddGraphQL(b => b
.AddAutoSchema<Query>()
.AddSystemTextJson()
.AddAuthorizationRule() // not required for endpoint authorization
.AddWebSocketAuthentication<MyAuthService>());
app.UseGraphQL("/graphql", config =>
{
// require that the user be authenticated
config.AuthorizationRequired = true;
});
class MyAuthService : IWebSocketAuthenticationService
{
private readonly IGraphQLSerializer _serializer;
public MyAuthService(IGraphQLSerializer serializer)
{
_serializer = serializer;
}
public async ValueTask<bool> AuthenticateAsync(IWebSocketConnection connection, OperationMessage operationMessage)
{
// read payload of ConnectionInit message and look for an "Authorization" entry that starts with "Bearer "
var payload = _serializer.ReadNode<Inputs>(operationMessage.Payload);
if ((payload?.TryGetValue("Authorization", out var value) ?? false) && value is string valueString)
{
var user = ParseToken(valueString);
if (user != null)
{
// set user and indicate authentication was successful
connection.HttpContext.User = user;
return true;
}
}
return false; // authentication failed
}
private ClaimsPrincipal? ParseToken(string authorizationHeaderValue)
{
// parse header value and return user, or null if unable
}
}
To authorize based on information within the query string, it is recommended to
derive from GraphQLHttpMiddleware<T>
and override InvokeAsync
, setting
HttpContext.User
based on the query string parameters, and then calling base.InvokeAsync
.
Alternatively you may override HandleAuthorizeAsync
which will execute for GET/POST requests,
and HandleAuthorizeWebSocketConnectionAsync
for WebSocket requests.
Note that InvokeAsync
will execute even if the protocol is disabled in the options via
disabling HandleGet
or similar; HandleAuthorizeAsync
and HandleAuthorizeWebSocketConnectionAsync
will not.
JWT Bearer authentication is provided by the GraphQL.AspNetCore3.JwtBearer
package.
Like the above sample, it will look for an "Authorization" entry that starts with "Bearer "
and validate the token using the configured ASP.Net Core JWT Bearer authentication handler.
Configure it using the AddJwtBearerAuthentication
extension method as shown
in the example below:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();
builder.Services.AddGraphQL(b => b
.AddAutoSchema<Query>()
.AddSystemTextJson()
.AddAuthorizationRule()
.AddJwtBearerAuthentication()
);
app.UseGraphQL("/graphql", config =>
{
// require that the user be authenticated
config.AuthorizationRequired = true;
});
Please note:
-
If JWT Bearer is not the default authentication scheme, you will need to specify the authentication scheme to use for GraphQL requests. See 'Authentication schemes' below for more information.
-
Events configured through
JwtBearerEvents
are not currently supported.
By default the role and policy requirements are validated against the current user as defined by
HttpContext.User
. This is typically set by ASP.NET Core's authentication middleware and is based
on the default authentication scheme set during the call to AddAuthentication
in Startup.cs
.
You may override this behavior by specifying a different authentication scheme via the AuthenticationSchemes
option. For instance, if you wish to authenticate using JWT authentication when Cookie authentication is
the default, you may specify the scheme as follows:
app.UseGraphQL("/graphql", config =>
{
// specify a specific authentication scheme to use
config.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
});
This will overwrite the HttpContext.User
property when handling GraphQL requests, which will in turn
set the IResolveFieldContext.User
property to the same value (unless being overridden via an
IWebSocketAuthenticationService
implementation as shown above). So both endpoint authorization and
field authorization will perform role and policy checks against the same authentication scheme.
This project does not include user interfaces, such as GraphiQL or Playground, but you can include references to the ones provided by the GraphQL Server repository which work well with ASP.Net Core 2.1+. Below is a list of the nuget packages offered:
Package | Downloads | NuGet Latest |
---|---|---|
GraphQL.Server.Ui.Altair | ||
GraphQL.Server.Ui.Playground | ||
GraphQL.Server.Ui.GraphiQL | ||
GraphQL.Server.Ui.Voyager |
Here is a sample of how this would be configured in your Program.cs
file:
app.UseGraphQL("/graphql");
// add this:
app.UseGraphQLPlayground(
new GraphQL.Server.Ui.Playground.PlaygroundOptions {
GraphQLEndPoint = new PathString("/graphql"),
SubscriptionsEndPoint = new PathString("/graphql"),
},
"/"); // url to host Playground at
await app.RunAsync();
ASP.NET Core supports CORS requests independently of GraphQL, including CORS pre-flight
requests. To configure your application for CORS requests, add AddCors()
and UseCors()
into the application pipeline.
builder.Services.AddCors();
app.UseCors(policy => {
// configure default policy here
});
To configure GraphQL to use a named CORS policy, configure the application to use endpoint routing
and call RequireCors()
on the endpoint configuration builder.
// ...
builder.Services.AddRouting();
builder.Services.AddCors(builder => {
// configure named and/or default policies here
});
var app = builder.Build();
app.UseDeveloperExceptionPage();
app.UseWebSockets();
app.UseRouting();
app.UseCors();
app.UseEndpoints(endpoints => {
// configure the graphql endpoint with the specified CORS policy
endpoints.MapGraphQL()
.RequireCors("MyCorsPolicy");
});
await app.RunAsync();
In order to ensure that all requests trigger CORS preflight requests, by default the server will reject requests that do not meet one of the following criteria:
- The request is a POST request that includes a Content-Type header that is not
application/x-www-form-urlencoded
,multipart/form-data
, ortext/plain
. - The request includes a non-empty
GraphQL-Require-Preflight
header.
To disable this behavior, set the CsrfProtectionEnabled
option to false
.
app.UseGraphQL("/graphql", config =>
{
config.CsrfProtectionEnabled = false;
});
You may also change the allowed headers by modifying the CsrfProtectionHeaders
option.
app.UseGraphQL("/graphql", config =>
{
config.CsrfProtectionHeaders = ["MyCustomHeader"];
});
ASP.NET Core supports response compression independently of GraphQL, with brotli and gzip support automatically based on the compression formats listed as supported in the request headers. To configure your application for response compression, configure your Program/Startup file as follows:
// add and configure the service
builder.Services.AddResponseCompression(options => {
options.EnableForHttps = true; //may lead to CRIME and BREACH attacks
options.MimeTypes = new[] { "application/json", "application/graphql+json" };
})
// place this first/early in the pipeline
app.UseResponseCompression();
In order to compress GraphQL responses, the application/graphql+json
content type must be
added to the MimeTypes
option. You may choose to enable other content types as well.
Please note that enabling response compression over HTTPS can lead to CRIME and BREACH attacks. These side-channel attacks typically affects sites that rely on cookies for authentication. Please read this and this for more details.
You may choose to use the .NET Core 2.1 runtime or a .NET Framework runtime. This library has been tested with .NET Core 2.1 and .NET Framework 4.8.
You will need to reference a serializer package such as GraphQL.NewtonsoftJson
or GraphQL.SystemTextJson
, as GraphQL.SystemTextJson
is not included in this case.
Besides that requirement, all features are supported in exactly the same manner as when using ASP.NET Core 3.1+. You may find differences in the ASP.NET Core runtime, such as CORS implementation differences, which are outside the scope of this project.
For more advanced configurations, see the overloads and configuration options available for the various builder methods, listed below. Methods and properties contain XML comments to provide assistance while coding with Visual Studio.
Builder interface | Method | Description |
---|---|---|
IGraphQLBuilder |
AddUserContextBuilder |
Sets up a delegate to create the UserContext for each GraphQL request. |
IApplicationBuilder |
UseGraphQL |
Adds the GraphQL middleware to the HTTP request pipeline. |
IEndpointRouteBuilder |
MapGraphQL |
Adds the GraphQL middleware to the HTTP request pipeline. |
A number of the methods contain optional parameters or configuration delegates to allow further customization. Please review the overloads of each method to determine which options are available. In addition, many methods have more descriptive XML comments than shown above.
Below are descriptions of the options available when registering the HTTP middleware
or WebSocket handler. Note that the HTTP middleware options are configured via the
UseGraphQL
or MapGraphQL
methods allowing for different options for each configured
endpoint.
Property | Description | Default value |
---|---|---|
AuthorizationRequired |
Requires HttpContext.User to represent an authenticated user. |
False |
AuthorizedPolicy |
If set, requires HttpContext.User to pass authorization of the specified policy. |
|
AuthorizedRoles |
If set, requires HttpContext.User to be a member of any one of a list of roles. |
|
CsrfProtectionEnabled |
Enables cross-site request forgery (CSRF) protection for both GET and POST requests. | True |
CsrfProtectionHeaders |
Sets the headers used for CSRF protection when necessary. | GraphQL-Require-Preflight |
EnableBatchedRequests |
Enables handling of batched GraphQL requests for POST requests when formatted as JSON. | True |
ExecuteBatchedRequestsInParallel |
Enables parallel execution of batched GraphQL requests. | True |
HandleGet |
Enables handling of GET requests. | True |
HandlePost |
Enables handling of POST requests. | True |
HandleWebSockets |
Enables handling of WebSockets requests. | True |
MaximumFileSize |
Sets the maximum file size allowed for GraphQL multipart requests. | unlimited |
MaximumFileCount |
Sets the maximum number of files allowed for GraphQL multipart requests. | unlimited |
ReadExtensionsFromQueryString |
Enables reading extensions from the query string. | True |
ReadFormOnPost |
Enables parsing of form data for POST requests (may have security implications). | False |
ReadQueryStringOnPost |
Enables parsing the query string on POST requests. | True |
ReadVariablesFromQueryString |
Enables reading variables from the query string. | True |
ValidationErrorsReturnBadRequest |
When enabled, GraphQL requests with validation errors have the HTTP status code set to 400 Bad Request. | True |
WebSockets |
Returns a set of configuration properties for WebSocket connections. |
Property | Description | Default value |
---|---|---|
ConnectionInitWaitTimeout |
The amount of time to wait for a GraphQL initialization packet before the connection is closed. | 10 seconds |
DisconnectionTimeout |
The amount of time to wait to attempt a graceful teardown of the WebSockets protocol. | 10 seconds |
DisconnectAfterErrorEvent |
Disconnects a subscription from the client if the subscription source dispatches an OnError event. |
True |
DisconnectAfterAnyError |
Disconnects a subscription from the client there are any GraphQL errors during a subscription. | False |
KeepAliveMode |
The mode to use for sending keep-alive packets. | protocol-dependent |
KeepAliveTimeout |
The amount of time to wait between sending keep-alive packets. | disabled |
SupportedWebSocketSubProtocols |
A list of supported WebSocket sub-protocols. | graphql-ws , graphql-transport-ws |
You may use the generic versions of the various builder methods to map a URL to a particular schema.
var app = builder.Build();
app.UseDeveloperExceptionPage();
app.UseGraphQL<DogSchema>("/graphql/dogs");
app.UseGraphQL<CatSchema>("/graphql/cats");
await app.RunAsync();
You may register the same endpoint multiple times if necessary to configure GET connections with certain authorization options, and POST connections with other authorization options.
var app = builder.Build();
app.UseDeveloperExceptionPage();
app.UseGraphQL("/graphql", options => {
options.HandleGet = true;
options.HandlePost = false;
options.HandleWebSockets = false;
options.AuthorizationRequired = false;
});
app.UseGraphQL("/graphql", options => {
options.HandleGet = false;
options.HandlePost = true;
options.HandleWebSockets = true;
options.AuthorizationRequired = true;
});
await app.RunAsync();
Since POST and WebSockets can be used for query requests, it is recommended not to do the above, but instead add the authorization validation rule and add authorization metadata on the Mutation and Subscription portions of your schema, as shown below:
builder.Services.AddGraphQL(b => b
.AddSchema<MySchema>()
.AddSystemTextJson()
.AddAuthorizationRule()); // add authorization validation rule
var app = builder.Build();
app.UseDeveloperExceptionPage();
app.UseGraphQL();
await app.RunAsync();
// demonstration of code-first schema; also possible with schema-first or type-first schemas
public class MySchema : Schema
{
public MySchema(IServiceProvider provider, MyQuery query, MyMutation mutation) : base(provider)
{
Query = query;
Mutation = mutation;
mutation.Authorize(); // require authorization for any mutation request
}
}
By default, the middleware will not send keep-alive packets to the client. As the underlying
operating system may not detect a disconnected client until a message is sent, you may wish to
enable keep-alive packets to be sent periodically. The default mode for keep-alive packets
differs depending on whether the client connected with the graphql-ws
or graphql-transport-ws
sub-protocol. The graphql-ws
sub-protocol will send a unidirectional keep-alive packet to the
client on a fixed schedule, while the graphql-transport-ws
sub-protocol will only send
unidirectional keep-alive packets when the client has not sent a message within a certain time.
The differing behavior is due to the default implementation of the graphql-ws
sub-protocol
client, which after receiving a single keep-alive packet, expects additional keep-alive packets
to be sent sooner than every 20 seconds, regardless of the client's activity.
To configure keep-alive packets, set the KeepAliveMode
and KeepAliveTimeout
properties
within the GraphQLWebSocketOptions
object. Set the KeepAliveTimeout
property to
enable keep-alive packets, or use TimeSpan.Zero
or Timeout.InfiniteTimeSpan
to disable it.
The KeepAliveMode
property is only applicable to the graphql-transport-ws
sub-protocol and
can be set to the options listed below:
Keep-alive mode | Description |
---|---|
Default |
Same as Timeout . |
Timeout |
Sends a unidirectional keep-alive message when no message has been received within the specified timeout period. |
Interval |
Sends a unidirectional keep-alive message at a fixed interval, regardless of message activity. |
TimeoutWithPayload |
Sends a bidirectional keep-alive message with a payload on a fixed interval, and validates the payload matches in the response. |
The TimeoutWithPayload
model is particularly useful when the server may send messages to the
client at a faster pace than the client can process them. In this case queued messages will be
limited to double the timeout period, as the keep-alive message is queued along with other
packets sent from the server to the client. The client will need to respond to process queued
messages and respond to the keep-alive message within the timeout period or the server will
disconnect the client. When the server forcibly disconnects the client, no graceful teardown
of the WebSocket protocol occurs, and any queued messages are discarded.
When using the TimeoutWithPayload
keep-alive mode, you may wish to enforce that the
graphql-transport-ws
sub-protocol is in use by the client, as the graphql-ws
sub-protocol
does not support bidirectional keep-alive packets. This can be done by setting the
SupportedWebSocketSubProtocols
property to only include the graphql-transport-ws
sub-protocol.
app.UseGraphQL("/graphql", options =>
{
// configure keep-alive packets
options.WebSockets.KeepAliveTimeout = TimeSpan.FromSeconds(10);
options.WebSockets.KeepAliveMode = KeepAliveMode.TimeoutWithPayload;
// set the supported sub-protocols to only include the graphql-transport-ws sub-protocol
options.WebSockets.SupportedWebSocketSubProtocols = [GraphQLWs.SubscriptionServer.SubProtocol];
});
Please note that the included UI packages are configured to use the graphql-ws
sub-protocol.
GET/POST requests are handled directly by the GraphQLHttpMiddleware
.
For WebSocket requests an WebSocketConnection
instance is instated to dispatching incoming
messages and send outgoing messages. Depending on the WebSocket sub-protocols supported by the
client, the proper implementation of IOperationMessageProcessor
is instated to act as a
state machine, processing incoming messages and sending outgoing messages through the
WebSocketConnection
instance.
The base middleware functionality is contained within GraphQLHttpMiddleware
, with code
to perform execution of GraphQL requests in the derived class GraphQLHttpMiddleware<TSchema>
.
The classes are organized as follows:
InvokeAsync
is the entry point to the middleware. For WebSocket connection requests, execution is immediately passed toHandleWebSocketsAsync
.- Methods that start with
Handle
are passed theHttpContext
andRequestDelegate
instance, and may handle the request or pass execution to theRequestDelegate
thereby skipping this execution handler. This includes methods to handle execution of single or batch queries or returning error conditions. - Methods that start with
Write
are for writing responses to the output stream. - Methods that start with
Execute
are for executing GraphQL requests.
A list of methods are as follows:
Method | Description |
---|---|
InvokeAsync |
Entry point of the middleware |
HandleRequestAsync |
Handles a single GraphQL request. |
HandleBatchRequestAsync |
Handles a batched GraphQL request. |
HandleWebSocketAsync |
Handles a WebSocket connection request. |
BuildUserContextAsync |
Builds the user context based on a HttpContext . |
ExecuteRequestAsync |
Executes a GraphQL request. |
ExecuteScopedRequestAsync |
Executes a GraphQL request with a scoped service provider. |
SelectResponseContentType |
Selects a content-type header for the JSON-formatted GraphQL response. |
WriteErrorResponseAsync |
Writes the specified error message as a JSON-formatted GraphQL response, with the specified HTTP status code. |
WriteJsonResponseAsync |
Writes the specified object (usually a GraphQL response) as JSON to the HTTP response stream. |
Error handling method | Description |
---|---|
HandleBatchedRequestsNotSupportedAsync |
Writes a '400 Batched requests are not supported.' message to the output. |
HandleContentTypeCouldNotBeParsedErrorAsync |
Writes a '415 Invalid Content-Type header: could not be parsed.' message to the output. |
HandleDeserializationErrorAsync |
Writes a '400 JSON body text could not be parsed.' message to the output. |
HandleInvalidContentTypeErrorAsync |
Writes a '415 Invalid Content-Type header: non-supported type.' message to the output. |
HandleInvalidHttpMethodErrorAsync |
Indicates that an unsupported HTTP method was requested. Executes the next delegate in the chain by default. |
HandleWebSocketSubProtocolNotSupportedAsync |
Writes a '400 Invalid WebSocket sub-protocol.' message to the output. |
The WebSocket handling code is organized as follows:
Interface / Class | Description |
---|---|
IWebSocketConnection |
Provides methods to a send a message to a client or close the connection. |
IOperationMessageProcessor |
Handles incoming messages from the client. |
GraphQLWebSocketOptions |
Provides configuration options for WebSocket connections. |
WebSocketConnection |
Standard implementation of a message pump for OperationMessage messages across a WebSockets connection. Implements IWebSocketConnection and delivers messages to a specified IOperationMessageProcessor . |
BaseSubscriptionServer |
Abstract implementation of IOperationMessageProcessor , a message handler for OperationMessage messages. Provides base functionality for managing subscriptions and requests. |
GraphQLWs.SubscriptionServer |
Implementation of IOperationMessageProcessor for the graphql-transport-ws sub-protocol. |
SubscriptionsTransportWs.SubscriptionServer |
Implementation of IOperationMessageProcessor for the graphql-ws sub-protocol. |
IWebSocketAuthorizationService |
Allows authorization of GraphQL requests for WebSocket connections. |
Typically if you wish to change functionality or support another sub-protocol you will need to perform the following:
- Derive from either
SubscriptionServer
class, modifying functionality as needed, or to support a new protocol, derive fromBaseSubscriptionServer
. - Derive from
GraphQLHttpMiddleware
and overrideCreateMessageProcessor
and/orSupportedWebSocketSubProtocols
as needed. - Change the
app.AddGraphQL()
call to use your custom middleware, being sure to include an instance of the options class that your middleware requires (typicallyGraphQLHttpMiddlewareOptions
).
There exists a few additional classes to support the above. Please refer to the source code
of GraphQLWs.SubscriptionServer
if you are attempting to add support for another protocol.
By default, a dependency injection service scope is created for each GraphQL execution in cases where it is possible that multiple GraphQL requests may be executing within the same service scope:
- A batched GraphQL request is executed.
- A GraphQL request over a WebSocket connection is executed.
However, field resolvers for child fields of subscription nodes will not by default execute
with a service scope. Rather, the context.RequestServices
property will contain a reference
to a disposed service scope that cannot be used.
To solve this issue, please configure the scoped subscription execution strategy from the GraphQL.MicrosoftDI package as follows:
services.AddGraphQL(b => b
.AddAutoSchema<Query>()
.AddSystemTextJson()
// configure queries for serial execution (optional)
.AddExecutionStrategy<SerialExecutionStrategy>(OperationType.Query)
// configure subscription field resolvers for scoped serial execution (parallel is optional)
.AddScopedSubscriptionExecutionStrategy());
For single GET / POST requests, the service scope from the underlying HTTP context is used.
The user context builder interface is executed only once, within the dependency injection service scope of the original HTTP request. For batched requests, the same user context instance is passed to each GraphQL execution. For WebSocket requests, the same user context instance is passed to each GraphQL subscription and data event resolver execution.
As such, do not create objects within the user context that rely on having the same dependency injection service scope as the field resolvers. Since WebSocket connections are long-lived, using scoped services within a user context builder will result in those scoped services having a matching long lifetime. You may wish to alleviate this by creating a service scope temporarily within your user context builder.
For applications that service multiple schemas, you may register IUserContextBuilder<TSchema>
to create a user context for a specific schema. This is useful when you need to create
a user context that is specific to a particular schema.
For security reasons and pursuant to current recommendations, mutation GraphQL requests
are rejected over HTTP GET connections. Derive from GraphQLHttpMiddleware
and override
ExecuteRequestAsync
to prevent injection of the validation rules that enforce this behavior.
As would be expected, subscription requests are only allowed over WebSocket channels.
The GraphQL over HTTP specification does not outline a procedure for transmitting GraphQL requests via
HTTP POST connections using a Content-Type
of application/x-www-form-urlencoded
or multipart/form-data
.
Allowing the processing of such requests could be advantageous in avoiding CORS preflight requests when
sending GraphQL queries from a web browser. Nevertheless, enabling this feature may give rise to security
risks when utilizing cookie authentication, since transmitting cookies with these requests does not trigger
a pre-flight CORS check. As a consequence, GraphQL.NET might execute a request and potentially modify data
even when the CORS policy prohibits it, regardless of whether the sender has access to the response.
This situation exposes the system to security vulnerabilities, which should be carefully evaluated and
mitigated to ensure the safe handling of GraphQL requests and maintain the integrity of the data.
To mitigate this potential security vulnerability, CSRF protection is enabled by default, requiring a
GraphQL-Require-Preflight
header to be sent with form data requests, which will trigger a CORS preflight
request. In addition, form data requests are disabled by default, as they are not recommended for typical
use.
To enable form data for POST request, set the ReadFormOnPost
setting to true
. GraphQL.NET Server supports
two formats of application/x-www-form-urlencoded
or multipart/form-data
requests:
-
The following keys are read from the form data and used to populate the GraphQL request:
query
: The GraphQL query string.operationName
: The name of the operation to execute.variables
: A JSON-encoded object containing the variables for the operation.extensions
: A JSON-encoded object containing the extensions for the operation.
-
The following keys are read from the form data and used to populate the GraphQL request:
operations
: A JSON-encoded object containing the GraphQL request, in the same format as typical requests sent viaapplication/json
. This can be a single object or an array of objects if batching is enabled.map
: An optional JSON-encoded map of file keys to file objects. This is used to map attached files into the GraphQL request's variables property. See the section below titled 'File uploading/downloading' and the GraphQL multipart request specification for additional details. Sinceapplication/x-www-form-urlencoded
cannot transmit files, this feature is only available formultipart/form-data
requests.
When hosting a WebSockets endpoint, it may be common for clients to simply disconnect rather
than gracefully terminating the connection — most specifically when the client is a web browser.
If you log exceptions, you may notice an OperationCanceledException
logged any time this occurs.
In some scenarios you may wish to log these exceptions — for instance, when the GraphQL endpoint is
used in server-to-server communications — but if you wish to ignore these exceptions, simply call
app.UseIgnoreDisconnections();
immediately after any exception handling or logging configuration calls.
This will consume any OperationCanceledException
s when HttpContext.RequestAborted
is signaled — for
a WebSocket request or any other request.
var app = builder.Build();
app.UseDeveloperExceptionPage();
app.UseIgnoreDisconnections();
app.UseWebSockets();
app.UseGraphQL();
A common question is how to upload or download files attached to GraphQL data. For instance, storing and retrieving photographs attached to product data.
One common technique is to encode the data as Base64 and transmitting as a custom GraphQL scalar (encoded as a string value within the JSON transport). This may not be ideal, but works well for smaller files. It can also couple with response compression (details listed above) to reduce the impact of the Base64 encoding.
Another technique is to get or store the data out-of-band. For responses, this can be as simple as a Uri pointing to a location to retrieve the data, especially if the data are photographs used in a SPA client application. This may have additional security complications, especially when used with JWT bearer authentication. This answer often works well for GraphQL queries, but may not be desired during uploads (mutations).
An option for uploading is to upload file data alongside a mutation with the multipart/form-data
content type. Please see Issue 307 and
FileUploadTests.cs
for discussion and demonstration of this capability.
An option for uploading is to upload file data alongside a mutation with the
multipart/form-data
content type as described by the
GraphQL multipart request specification.
Uploaded files are mapped into the GraphQL request's variables as IFormFile
objects.
You can use the provided FormFileGraphType
scalar graph type in your GraphQL schema
to access these files. The AddFormFileGraphType()
builder extension method adds this scalar
to the DI container and configures a CLR type mapping for it to be used for IFormFile
objects.
services.AddGraphQL(b => b
.AddAutoSchema<Query>()
.AddFormFileGraphType()
.AddSystemTextJson());
Please see the 'Upload' sample for a demonstration of this technique, which also
demonstrates the use of the MediaTypeAttribute
to restrict the allowable media
types that will be accepted. Note that using the FormFileGraphType
scalar requires
that the uploaded files be sent only via the multipart/form-data
content type as
attached files, with the ReadFormOnPost
option enabled. If you also wish to allow
clients to send files as base-64 encoded strings, you can write a custom scalar
better suited to your needs.
The following samples are provided to show how to integrate this project with various typical ASP.Net Core scenarios.
Sample project | Description |
---|---|
AuthorizationSample |
Demonstrates a GraphQL server added to an ASP.NET Core web authentication-enabled template. |
BasicSample |
Demonstrates the minimum required setup for a HTTP GraphQL server. |
Chat |
A basic schema common to all samples; demonstrates queries, mutations and subscriptions. |
ControllerSample |
Demonstrates using a controller action to serve GraphQL requests; does not support subscriptions. |
CorsSample |
Demonstrates configuring a GraphQL endpoint to use a specified CORS policy. |
EndpointRoutingSample |
Demonstrates configuring GraphQL endpoints through endpoint routing. |
MultipleSchema |
Demonstrates multiple GraphQL endpoints served through a single project. |
Net48Sample |
Demonstrates a GraphQL server running on .NET Framework 4.8 (or for linux machines, .NET Core 2.1) |
PagesSample |
Demonstrates configuring GraphQL within a ASP.NET Core Pages project. |
Glory to Jehovah, Lord of Lords and King of Kings, creator of Heaven and Earth, who through his Son Jesus Christ, has reedemed me to become a child of God. -Shane32