Skip to content

Commit e4e00fd

Browse files
authored
Support IResult via GraphQLExecutionHttpResult for .NET 6+ (#1191)
1 parent e8aa5ff commit e4e00fd

File tree

15 files changed

+986
-23
lines changed

15 files changed

+986
-23
lines changed

GraphQL.Server.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.Upload.Tests", "tes
124124
EndProject
125125
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.NativeAot", "samples\Samples.NativeAot\Samples.NativeAot.csproj", "{56042483-2E36-41DF-9DC4-71DC527A36E4}"
126126
EndProject
127+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.HttpResult", "samples\Samples.HttpResult\Samples.HttpResult.csproj", "{C457FCA0-217D-91E4-5C76-5E37DE4C4CB0}"
128+
EndProject
129+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.HttpResult.Tests", "tests\Samples.HttpResult.Tests\Samples.HttpResult.Tests.csproj", "{F23D0C12-3E26-61F3-2A5E-279D3F611F2E}"
130+
EndProject
127131
Global
128132
GlobalSection(SolutionConfigurationPlatforms) = preSolution
129133
Debug|Any CPU = Debug|Any CPU
@@ -270,6 +274,14 @@ Global
270274
{56042483-2E36-41DF-9DC4-71DC527A36E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
271275
{56042483-2E36-41DF-9DC4-71DC527A36E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
272276
{56042483-2E36-41DF-9DC4-71DC527A36E4}.Release|Any CPU.Build.0 = Release|Any CPU
277+
{C457FCA0-217D-91E4-5C76-5E37DE4C4CB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
278+
{C457FCA0-217D-91E4-5C76-5E37DE4C4CB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
279+
{C457FCA0-217D-91E4-5C76-5E37DE4C4CB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
280+
{C457FCA0-217D-91E4-5C76-5E37DE4C4CB0}.Release|Any CPU.Build.0 = Release|Any CPU
281+
{F23D0C12-3E26-61F3-2A5E-279D3F611F2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
282+
{F23D0C12-3E26-61F3-2A5E-279D3F611F2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
283+
{F23D0C12-3E26-61F3-2A5E-279D3F611F2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
284+
{F23D0C12-3E26-61F3-2A5E-279D3F611F2E}.Release|Any CPU.Build.0 = Release|Any CPU
273285
EndGlobalSection
274286
GlobalSection(SolutionProperties) = preSolution
275287
HideSolutionNode = FALSE
@@ -311,6 +323,8 @@ Global
311323
{33E2CDF5-F854-4F1A-80D5-DBF0BDF8EEA8} = {5C07AFA3-12F2-40EA-807D-7A1EEF29012B}
312324
{DE3059F4-B548-4091-BFC0-5879246A2DF9} = {BBD07745-C962-4D2D-B302-6DA1BCC2FF43}
313325
{56042483-2E36-41DF-9DC4-71DC527A36E4} = {5C07AFA3-12F2-40EA-807D-7A1EEF29012B}
326+
{C457FCA0-217D-91E4-5C76-5E37DE4C4CB0} = {5C07AFA3-12F2-40EA-807D-7A1EEF29012B}
327+
{F23D0C12-3E26-61F3-2A5E-279D3F611F2E} = {BBD07745-C962-4D2D-B302-6DA1BCC2FF43}
314328
EndGlobalSection
315329
GlobalSection(ExtensibilityGlobals) = postSolution
316330
SolutionGuid = {3FC7FA59-E938-453C-8C4A-9D5635A9489A}

README.md

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,11 @@ as defined in the [apollographql/subscriptions-transport-ws](https://github.com/
5353
and [enisdenjo/graphql-ws](https://github.com/enisdenjo/graphql-ws) repositories, respectively.
5454

5555
The middleware can be configured through the `IApplicationBuilder` or `IEndpointRouteBuilder`
56-
builder interfaces. In addition, an `ExecutionResultActionResult` class is added for returning
57-
`ExecutionResult` instances directly from a controller action.
56+
builder interfaces. Alternatively, route handlers (such as `MapGet` and `MapPost`) can return
57+
a `GraphQLExecutionHttpResult` for direct GraphQL execution, or `ExecutionResultHttpResult` for
58+
returning pre-executed GraphQL responses. Similarly, `GraphQLExecutionActionResult`
59+
and `ExecutionResultActionResult` classes can be used to return GraphQL responses from
60+
controller actions.
5861

5962
Authorization is also supported with the included `AuthorizationValidationRule`. It will
6063
scan GraphQL documents and validate that the schema and all referenced output graph types, fields of
@@ -211,6 +214,50 @@ app.UseEndpoints(endpoints =>
211214
await app.RunAsync();
212215
```
213216

217+
### Configuration with route handlers (.NET 6+)
218+
219+
Although not recommended, you may set up [route handlers](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/route-handlers)
220+
to execute GraphQL requests using `MapGet` and `MapPost` that return an `IResult`.
221+
You will not need `UseGraphQL` or `MapGraphQL` in the application startup. Note that GET must be
222+
mapped to support WebSocket connections, as WebSocket connections upgrade from HTTP GET requests.
223+
224+
#### Using `GraphQLExecutionHttpResult`
225+
226+
```csharp
227+
var app = builder.Build();
228+
app.UseDeveloperExceptionPage();
229+
app.UseWebSockets();
230+
231+
// configure the graphql endpoint at "/graphql", using GraphQLExecutionHttpResult
232+
// map GET in order to support both GET and WebSocket requests
233+
app.MapGet("/graphql", () => new GraphQLExecutionHttpResult());
234+
// map POST to handle standard GraphQL POST requests
235+
app.MapPost("/graphql", () => new GraphQLExecutionHttpResult());
236+
237+
await app.RunAsync();
238+
```
239+
240+
#### Using `ExecutionResultHttpResult`
241+
242+
```csharp
243+
app.MapPost("/graphql", async (HttpContext context, IDocumentExecuter<ISchema> documentExecuter, IGraphQLSerializer serializer) =>
244+
{
245+
var request = await serializer.ReadAsync<GraphQLRequest>(context.Request.Body, context.RequestAborted);
246+
var opts = new ExecutionOptions
247+
{
248+
Query = request?.Query,
249+
DocumentId = request?.DocumentId,
250+
Variables = request?.Variables,
251+
Extensions = request?.Extensions,
252+
CancellationToken = context.RequestAborted,
253+
RequestServices = context.RequestServices,
254+
User = context.User,
255+
};
256+
257+
return new ExecutionResultHttpResult(await documentExecuter.ExecuteAsync(opts));
258+
});
259+
```
260+
214261
### Configuration with a MVC controller
215262

216263
Although not recommended, you may set up a controller action to execute GraphQL
@@ -1136,6 +1183,7 @@ typical ASP.NET Core scenarios.
11361183
| Controller | .NET 8 Minimal | MVC implementation; does not include WebSocket support |
11371184
| Cors | .NET 8 Minimal | Demonstrates configuring a GraphQL endpoint to use a specified CORS policy |
11381185
| EndpointRouting | .NET 8 Minimal | Demonstrates configuring GraphQL through endpoint routing |
1186+
| HttpResult | .NET 8 Minimal | Demonstrates using `MapGet` and/or `MapPost` to return a GraphQL response |
11391187
| Jwt | .NET 8 Minimal | Demonstrates authenticating GraphQL requests with a JWT bearer token over HTTP POST and WebSocket connections |
11401188
| MultipleSchemas | .NET 8 Minimal | Demonstrates configuring multiple schemas within a single server |
11411189
| NativeAot | .NET 8 Slim | Demonstrates configuring GraphQL for Native AOT publishing |

samples/Samples.Controller/Controllers/HomeController.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ public IActionResult GraphQLAsync()
3737
/******* Sample with custom logic only using ExecutionResultActionResult to return the result ********/
3838
[HttpGet]
3939
[ActionName("graphql2")]
40-
public Task<IActionResult> GraphQL2GetAsync(string query, string? operationName)
40+
public Task<IActionResult> GraphQL2GetAsync(string query, string? documentId, string? operationName)
4141
{
4242
if (HttpContext.WebSockets.IsWebSocketRequest)
4343
{
4444
return Task.FromResult<IActionResult>(BadRequest());
4545
}
4646
else
4747
{
48-
return ExecuteGraphQLRequestAsync(BuildRequest(query, operationName));
48+
return ExecuteGraphQLRequestAsync(BuildRequest(query, documentId, operationName));
4949
}
5050
}
5151

@@ -56,7 +56,7 @@ public async Task<IActionResult> GraphQL2PostAsync()
5656
if (HttpContext.Request.HasFormContentType)
5757
{
5858
var form = await HttpContext.Request.ReadFormAsync(HttpContext.RequestAborted);
59-
return await ExecuteGraphQLRequestAsync(BuildRequest(form["query"].ToString(), form["operationName"].ToString(), form["variables"].ToString(), form["extensions"].ToString()));
59+
return await ExecuteGraphQLRequestAsync(BuildRequest(form["query"].ToString(), form["documentId"].ToString(), form["operationName"].ToString(), form["variables"].ToString(), form["extensions"].ToString()));
6060
}
6161
else if (HttpContext.Request.HasJsonContentType())
6262
{
@@ -66,10 +66,11 @@ public async Task<IActionResult> GraphQL2PostAsync()
6666
return BadRequest();
6767
}
6868

69-
private GraphQLRequest BuildRequest(string query, string? operationName, string? variables = null, string? extensions = null)
69+
private GraphQLRequest BuildRequest(string query, string? documentId, string? operationName, string? variables = null, string? extensions = null)
7070
=> new GraphQLRequest
7171
{
7272
Query = query == "" ? null : query,
73+
DocumentId = documentId == "" ? null : documentId,
7374
OperationName = operationName == "" ? null : operationName,
7475
Variables = _serializer.Deserialize<Inputs>(variables == "" ? null : variables),
7576
Extensions = _serializer.Deserialize<Inputs>(extensions == "" ? null : extensions),
@@ -82,6 +83,7 @@ private async Task<IActionResult> ExecuteGraphQLRequestAsync(GraphQLRequest? req
8283
var opts = new ExecutionOptions
8384
{
8485
Query = request?.Query,
86+
DocumentId = request?.DocumentId,
8587
OperationName = request?.OperationName,
8688
Variables = request?.Variables,
8789
Extensions = request?.Extensions,

samples/Samples.HttpResult/Program.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using GraphQL;
2+
using GraphQL.Server.Transports.AspNetCore;
3+
using GraphQL.Transport;
4+
using GraphQL.Types;
5+
using Chat = GraphQL.Samples.Schemas.Chat;
6+
7+
var builder = WebApplication.CreateBuilder(args);
8+
9+
builder.Services.AddSingleton<Chat.IChat, Chat.Chat>();
10+
builder.Services.AddGraphQL(b => b
11+
.AddAutoSchema<Chat.Query>(s => s
12+
.WithMutation<Chat.Mutation>()
13+
.WithSubscription<Chat.Subscription>())
14+
.AddSystemTextJson());
15+
16+
var app = builder.Build();
17+
app.UseDeveloperExceptionPage();
18+
app.UseWebSockets();
19+
20+
// configure the graphql endpoint at "/graphql", using GraphQLExecutionHttpResult
21+
// map GET in order to support both GET and WebSocket requests
22+
app.MapGet("/graphql", () => new GraphQLExecutionHttpResult());
23+
// map POST to handle standard GraphQL POST requests
24+
app.MapPost("/graphql", () => new GraphQLExecutionHttpResult());
25+
26+
// Example endpoint demonstrating ExecutionResultHttpResult with custom logic
27+
app.MapPost("/graphql-result", async (HttpContext context, IDocumentExecuter<ISchema> documentExecuter, IGraphQLTextSerializer serializer) =>
28+
{
29+
GraphQLRequest? request;
30+
31+
if (context.Request.HasFormContentType)
32+
{
33+
var form = await context.Request.ReadFormAsync(context.RequestAborted);
34+
request = new GraphQLRequest
35+
{
36+
Query = form["query"].ToString() == "" ? null : form["query"].ToString(),
37+
DocumentId = form["documentId"].ToString() == "" ? null : form["documentId"].ToString(),
38+
OperationName = form["operationName"].ToString() == "" ? null : form["operationName"].ToString(),
39+
Variables = serializer.Deserialize<Inputs>(form["variables"].ToString() == "" ? null : form["variables"].ToString()),
40+
Extensions = serializer.Deserialize<Inputs>(form["extensions"].ToString() == "" ? null : form["extensions"].ToString()),
41+
};
42+
}
43+
else if (context.Request.HasJsonContentType())
44+
{
45+
request = await serializer.ReadAsync<GraphQLRequest>(context.Request.Body, context.RequestAborted);
46+
}
47+
else
48+
{
49+
return Results.BadRequest();
50+
}
51+
52+
var opts = new ExecutionOptions
53+
{
54+
Query = request?.Query,
55+
DocumentId = request?.DocumentId,
56+
OperationName = request?.OperationName,
57+
Variables = request?.Variables,
58+
Extensions = request?.Extensions,
59+
CancellationToken = context.RequestAborted,
60+
RequestServices = context.RequestServices,
61+
User = context.User,
62+
};
63+
64+
var result = await documentExecuter.ExecuteAsync(opts);
65+
return new ExecutionResultHttpResult(result);
66+
});
67+
68+
// configure GraphiQL at "/"
69+
app.UseGraphQLGraphiQL(
70+
"/",
71+
new GraphQL.Server.Ui.GraphiQL.GraphiQLOptions
72+
{
73+
GraphQLEndPoint = "/graphql",
74+
SubscriptionsEndPoint = "/graphql",
75+
});
76+
77+
await app.RunAsync();
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"iisSettings": {
3+
"windowsAuthentication": false,
4+
"anonymousAuthentication": true,
5+
"iisExpress": {
6+
"applicationUrl": "http://localhost:51526/",
7+
"sslPort": 44334
8+
}
9+
},
10+
"profiles": {
11+
"IIS Express": {
12+
"commandName": "IISExpress",
13+
"launchBrowser": true,
14+
"environmentVariables": {
15+
"ASPNETCORE_ENVIRONMENT": "Development"
16+
}
17+
},
18+
"Typical": {
19+
"commandName": "Project",
20+
"launchBrowser": true,
21+
"environmentVariables": {
22+
"ASPNETCORE_ENVIRONMENT": "Development"
23+
},
24+
"applicationUrl": "https://localhost:5001;http://localhost:5000"
25+
}
26+
}
27+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="..\..\src\Transports.AspNetCore\Transports.AspNetCore.csproj" />
12+
<ProjectReference Include="..\Samples.Schemas.Chat\Samples.Schemas.Chat.csproj" />
13+
<ProjectReference Include="..\..\src\Ui.GraphiQL\Ui.GraphiQL.csproj" />
14+
</ItemGroup>
15+
16+
</Project>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace GraphQL.Server.Transports.AspNetCore;
2+
3+
internal class NullHostApplicationLifetime : IHostApplicationLifetime
4+
{
5+
private NullHostApplicationLifetime()
6+
{
7+
}
8+
9+
public static NullHostApplicationLifetime Instance { get; } = new();
10+
11+
public CancellationToken ApplicationStarted => default;
12+
13+
public CancellationToken ApplicationStopped => default;
14+
15+
public CancellationToken ApplicationStopping => default;
16+
17+
public void StopApplication() { }
18+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#if NET6_0_OR_GREATER
2+
namespace GraphQL.Server.Transports.AspNetCore;
3+
4+
/// <summary>
5+
/// An <see cref="IResult"/> that formats the <see cref="ExecutionResult"/> as JSON.
6+
/// </summary>
7+
public sealed class ExecutionResultHttpResult : IResult
8+
{
9+
private readonly ExecutionResult _executionResult;
10+
private readonly HttpStatusCode _statusCode;
11+
12+
/// <inheritdoc cref="ExecutionResultHttpResult"/>
13+
public ExecutionResultHttpResult(ExecutionResult executionResult)
14+
{
15+
_executionResult = executionResult;
16+
_statusCode = executionResult.Executed ? HttpStatusCode.OK : HttpStatusCode.BadRequest;
17+
}
18+
19+
/// <inheritdoc cref="ExecutionResultHttpResult"/>
20+
public ExecutionResultHttpResult(ExecutionResult executionResult, HttpStatusCode statusCode)
21+
{
22+
_executionResult = executionResult;
23+
_statusCode = statusCode;
24+
}
25+
26+
/// <inheritdoc cref="HttpResponse.ContentType"/>
27+
public string ContentType { get; set; } = GraphQLHttpMiddleware.CONTENTTYPE_GRAPHQLRESPONSEJSON;
28+
29+
/// <inheritdoc/>
30+
public async Task ExecuteAsync(HttpContext httpContext)
31+
{
32+
var serializer = httpContext.RequestServices.GetRequiredService<IGraphQLSerializer>();
33+
var response = httpContext.Response;
34+
response.ContentType = ContentType;
35+
response.StatusCode = (int)_statusCode;
36+
await serializer.WriteAsync(response.Body, _executionResult, httpContext.RequestAborted);
37+
}
38+
}
39+
#endif

src/Transports.AspNetCore/GraphQLExecutionActionResult.cs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,23 +49,6 @@ public virtual Task ExecuteResultAsync(ActionContext context)
4949

5050
return middleware.InvokeAsync(context.HttpContext);
5151
}
52-
53-
private class NullHostApplicationLifetime : IHostApplicationLifetime
54-
{
55-
private NullHostApplicationLifetime()
56-
{
57-
}
58-
59-
public static NullHostApplicationLifetime Instance { get; } = new();
60-
61-
public CancellationToken ApplicationStarted => default;
62-
63-
public CancellationToken ApplicationStopped => default;
64-
65-
public CancellationToken ApplicationStopping => default;
66-
67-
public void StopApplication() { }
68-
}
6952
}
7053

7154
/// <summary>

0 commit comments

Comments
 (0)