Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions GraphQL.Server.sln
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.Upload.Tests", "tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.NativeAot", "samples\Samples.NativeAot\Samples.NativeAot.csproj", "{56042483-2E36-41DF-9DC4-71DC527A36E4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.HttpResult", "samples\Samples.HttpResult\Samples.HttpResult.csproj", "{C457FCA0-217D-91E4-5C76-5E37DE4C4CB0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.HttpResult.Tests", "tests\Samples.HttpResult.Tests\Samples.HttpResult.Tests.csproj", "{F23D0C12-3E26-61F3-2A5E-279D3F611F2E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -270,6 +274,14 @@ Global
{56042483-2E36-41DF-9DC4-71DC527A36E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{56042483-2E36-41DF-9DC4-71DC527A36E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{56042483-2E36-41DF-9DC4-71DC527A36E4}.Release|Any CPU.Build.0 = Release|Any CPU
{C457FCA0-217D-91E4-5C76-5E37DE4C4CB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C457FCA0-217D-91E4-5C76-5E37DE4C4CB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C457FCA0-217D-91E4-5C76-5E37DE4C4CB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C457FCA0-217D-91E4-5C76-5E37DE4C4CB0}.Release|Any CPU.Build.0 = Release|Any CPU
{F23D0C12-3E26-61F3-2A5E-279D3F611F2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F23D0C12-3E26-61F3-2A5E-279D3F611F2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F23D0C12-3E26-61F3-2A5E-279D3F611F2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F23D0C12-3E26-61F3-2A5E-279D3F611F2E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -311,6 +323,8 @@ Global
{33E2CDF5-F854-4F1A-80D5-DBF0BDF8EEA8} = {5C07AFA3-12F2-40EA-807D-7A1EEF29012B}
{DE3059F4-B548-4091-BFC0-5879246A2DF9} = {BBD07745-C962-4D2D-B302-6DA1BCC2FF43}
{56042483-2E36-41DF-9DC4-71DC527A36E4} = {5C07AFA3-12F2-40EA-807D-7A1EEF29012B}
{C457FCA0-217D-91E4-5C76-5E37DE4C4CB0} = {5C07AFA3-12F2-40EA-807D-7A1EEF29012B}
{F23D0C12-3E26-61F3-2A5E-279D3F611F2E} = {BBD07745-C962-4D2D-B302-6DA1BCC2FF43}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3FC7FA59-E938-453C-8C4A-9D5635A9489A}
Expand Down
52 changes: 50 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@ as defined in the [apollographql/subscriptions-transport-ws](https://github.com/
and [enisdenjo/graphql-ws](https://github.com/enisdenjo/graphql-ws) repositories, respectively.

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.
builder interfaces. Alternatively, route handlers (such as `MapGet` and `MapPost`) can return
a `GraphQLExecutionHttpResult` for direct GraphQL execution, or `ExecutionResultHttpResult` for
returning pre-executed GraphQL responses. In addition, `GraphQLExecutionActionResult`
and `ExecutionResultActionResult` classes are added for returning GraphQL responses directly from
controller actions.

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
Expand Down Expand Up @@ -211,6 +214,50 @@ app.UseEndpoints(endpoints =>
await app.RunAsync();
```

### Configuration with route handlers (.NET 6+)

Although not recommended, you may set up [route handlers](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/route-handlers)
to execute GraphQL requests using `MapGet` and `MapPost` that return an `IResult`.
You will not need `UseGraphQL` or `MapGraphQL` in the application startup. Note that GET must be
mapped to support WebSocket connections, as WebSocket connections upgrade from HTTP GET requests.

#### Using `GraphQLExecutionHttpResult`

```csharp
var app = builder.Build();
app.UseDeveloperExceptionPage();
app.UseWebSockets();

// configure the graphql endpoint at "/graphql", using GraphQLExecutionHttpResult
// map GET in order to support both GET and WebSocket requests
app.MapGet("/graphql", () => new GraphQLExecutionHttpResult());
// map POST to handle standard GraphQL POST requests
app.MapPost("/graphql", () => new GraphQLExecutionHttpResult());

await app.RunAsync();
```

#### Using `ExecutionResultHttpResult`

```csharp
app.MapPost("/graphql", async (HttpContext context, IDocumentExecuter<ISchema> documentExecuter, IGraphQLSerializer serializer) =>
{
var request = await serializer.ReadAsync<GraphQLRequest>(context.Request.Body, context.RequestAborted);
var opts = new ExecutionOptions
{
Query = request?.Query,
DocumentId = request?.DocumentId,
Variables = request?.Variables,
Extensions = request?.Extensions,
CancellationToken = context.RequestAborted,
RequestServices = context.RequestServices,
User = context.User,
};

return new ExecutionResultHttpResult(await documentExecuter.ExecuteAsync(opts));
});
```

### Configuration with a MVC controller

Although not recommended, you may set up a controller action to execute GraphQL
Expand Down Expand Up @@ -1136,6 +1183,7 @@ typical ASP.NET Core scenarios.
| Controller | .NET 8 Minimal | MVC implementation; does not include WebSocket support |
| Cors | .NET 8 Minimal | Demonstrates configuring a GraphQL endpoint to use a specified CORS policy |
| EndpointRouting | .NET 8 Minimal | Demonstrates configuring GraphQL through endpoint routing |
| HttpResult | .NET 8 Minimal | Demonstrates using `MapGet` and/or `MapPost` to return a GraphQL response |
| Jwt | .NET 8 Minimal | Demonstrates authenticating GraphQL requests with a JWT bearer token over HTTP POST and WebSocket connections |
| MultipleSchemas | .NET 8 Minimal | Demonstrates configuring multiple schemas within a single server |
| NativeAot | .NET 8 Slim | Demonstrates configuring GraphQL for Native AOT publishing |
Expand Down
10 changes: 6 additions & 4 deletions samples/Samples.Controller/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ public IActionResult GraphQLAsync()
/******* Sample with custom logic only using ExecutionResultActionResult to return the result ********/
[HttpGet]
[ActionName("graphql2")]
public Task<IActionResult> GraphQL2GetAsync(string query, string? operationName)
public Task<IActionResult> GraphQL2GetAsync(string query, string? documentId, string? operationName)
{
if (HttpContext.WebSockets.IsWebSocketRequest)
{
return Task.FromResult<IActionResult>(BadRequest());
}
else
{
return ExecuteGraphQLRequestAsync(BuildRequest(query, operationName));
return ExecuteGraphQLRequestAsync(BuildRequest(query, documentId, operationName));
}
}

Expand All @@ -56,7 +56,7 @@ public async Task<IActionResult> GraphQL2PostAsync()
if (HttpContext.Request.HasFormContentType)
{
var form = await HttpContext.Request.ReadFormAsync(HttpContext.RequestAborted);
return await ExecuteGraphQLRequestAsync(BuildRequest(form["query"].ToString(), form["operationName"].ToString(), form["variables"].ToString(), form["extensions"].ToString()));
return await ExecuteGraphQLRequestAsync(BuildRequest(form["query"].ToString(), form["documentId"].ToString(), form["operationName"].ToString(), form["variables"].ToString(), form["extensions"].ToString()));
}
else if (HttpContext.Request.HasJsonContentType())
{
Expand All @@ -66,10 +66,11 @@ public async Task<IActionResult> GraphQL2PostAsync()
return BadRequest();
}

private GraphQLRequest BuildRequest(string query, string? operationName, string? variables = null, string? extensions = null)
private GraphQLRequest BuildRequest(string query, string? documentId, string? operationName, string? variables = null, string? extensions = null)
=> new GraphQLRequest
{
Query = query == "" ? null : query,
DocumentId = documentId == "" ? null : documentId,
OperationName = operationName == "" ? null : operationName,
Variables = _serializer.Deserialize<Inputs>(variables == "" ? null : variables),
Extensions = _serializer.Deserialize<Inputs>(extensions == "" ? null : extensions),
Expand All @@ -82,6 +83,7 @@ private async Task<IActionResult> ExecuteGraphQLRequestAsync(GraphQLRequest? req
var opts = new ExecutionOptions
{
Query = request?.Query,
DocumentId = request?.DocumentId,
OperationName = request?.OperationName,
Variables = request?.Variables,
Extensions = request?.Extensions,
Expand Down
77 changes: 77 additions & 0 deletions samples/Samples.HttpResult/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using GraphQL;
using GraphQL.Server.Transports.AspNetCore;
using GraphQL.Transport;
using GraphQL.Types;
using Chat = GraphQL.Samples.Schemas.Chat;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<Chat.IChat, Chat.Chat>();
builder.Services.AddGraphQL(b => b
.AddAutoSchema<Chat.Query>(s => s
.WithMutation<Chat.Mutation>()
.WithSubscription<Chat.Subscription>())
.AddSystemTextJson());

var app = builder.Build();
app.UseDeveloperExceptionPage();
app.UseWebSockets();

// configure the graphql endpoint at "/graphql", using GraphQLExecutionHttpResult
// map GET in order to support both GET and WebSocket requests
app.MapGet("/graphql", () => new GraphQLExecutionHttpResult());
// map POST to handle standard GraphQL POST requests
app.MapPost("/graphql", () => new GraphQLExecutionHttpResult());

// Example endpoint demonstrating ExecutionResultHttpResult with custom logic
app.MapPost("/graphql-result", async (HttpContext context, IDocumentExecuter<ISchema> documentExecuter, IGraphQLTextSerializer serializer) =>
{
GraphQLRequest? request;

if (context.Request.HasFormContentType)
{
var form = await context.Request.ReadFormAsync(context.RequestAborted);
request = new GraphQLRequest
{
Query = form["query"].ToString() == "" ? null : form["query"].ToString(),
DocumentId = form["documentId"].ToString() == "" ? null : form["documentId"].ToString(),
OperationName = form["operationName"].ToString() == "" ? null : form["operationName"].ToString(),
Variables = serializer.Deserialize<Inputs>(form["variables"].ToString() == "" ? null : form["variables"].ToString()),
Extensions = serializer.Deserialize<Inputs>(form["extensions"].ToString() == "" ? null : form["extensions"].ToString()),
};
}
else if (context.Request.HasJsonContentType())
{
request = await serializer.ReadAsync<GraphQLRequest>(context.Request.Body, context.RequestAborted);
}
else
{
return Results.BadRequest();
}

var opts = new ExecutionOptions
{
Query = request?.Query,
DocumentId = request?.DocumentId,
OperationName = request?.OperationName,
Variables = request?.Variables,
Extensions = request?.Extensions,
CancellationToken = context.RequestAborted,
RequestServices = context.RequestServices,
User = context.User,
};

var result = await documentExecuter.ExecuteAsync(opts);
return new ExecutionResultHttpResult(result);
});

// configure GraphiQL at "/"
app.UseGraphQLGraphiQL(
"/",
new GraphQL.Server.Ui.GraphiQL.GraphiQLOptions
{
GraphQLEndPoint = "/graphql",
SubscriptionsEndPoint = "/graphql",
});

await app.RunAsync();
27 changes: 27 additions & 0 deletions samples/Samples.HttpResult/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:51526/",
"sslPort": 44334
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Typical": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}
16 changes: 16 additions & 0 deletions samples/Samples.HttpResult/Samples.HttpResult.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Transports.AspNetCore\Transports.AspNetCore.csproj" />
<ProjectReference Include="..\Samples.Schemas.Chat\Samples.Schemas.Chat.csproj" />
<ProjectReference Include="..\..\src\Ui.GraphiQL\Ui.GraphiQL.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace GraphQL.Server.Transports.AspNetCore;

internal class NullHostApplicationLifetime : IHostApplicationLifetime
{
private NullHostApplicationLifetime()
{
}

public static NullHostApplicationLifetime Instance { get; } = new();

public CancellationToken ApplicationStarted => default;

public CancellationToken ApplicationStopped => default;

public CancellationToken ApplicationStopping => default;

public void StopApplication() { }
}
39 changes: 39 additions & 0 deletions src/Transports.AspNetCore/ExecutionResultHttpResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#if NET6_0_OR_GREATER
namespace GraphQL.Server.Transports.AspNetCore;

/// <summary>
/// An <see cref="IResult"/> that formats the <see cref="ExecutionResult"/> as JSON.
/// </summary>
public sealed class ExecutionResultHttpResult : IResult
{
private readonly ExecutionResult _executionResult;
private readonly HttpStatusCode _statusCode;

/// <inheritdoc cref="ExecutionResultHttpResult"/>
public ExecutionResultHttpResult(ExecutionResult executionResult)
{
_executionResult = executionResult;
_statusCode = executionResult.Executed ? HttpStatusCode.OK : HttpStatusCode.BadRequest;
}

/// <inheritdoc cref="ExecutionResultHttpResult"/>
public ExecutionResultHttpResult(ExecutionResult executionResult, HttpStatusCode statusCode)
{
_executionResult = executionResult;
_statusCode = statusCode;
}

/// <inheritdoc cref="HttpResponse.ContentType"/>
public string ContentType { get; set; } = GraphQLHttpMiddleware.CONTENTTYPE_GRAPHQLRESPONSEJSON;

/// <inheritdoc/>
public async Task ExecuteAsync(HttpContext httpContext)
{
var serializer = httpContext.RequestServices.GetRequiredService<IGraphQLSerializer>();
var response = httpContext.Response;
response.ContentType = ContentType;
response.StatusCode = (int)_statusCode;
await serializer.WriteAsync(response.Body, _executionResult, httpContext.RequestAborted);
}
}
#endif
16 changes: 0 additions & 16 deletions src/Transports.AspNetCore/GraphQLExecutionActionResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,6 @@ public virtual Task ExecuteResultAsync(ActionContext context)
return middleware.InvokeAsync(context.HttpContext);
}

private class NullHostApplicationLifetime : IHostApplicationLifetime
{
private NullHostApplicationLifetime()
{
}

public static NullHostApplicationLifetime Instance { get; } = new();

public CancellationToken ApplicationStarted => default;

public CancellationToken ApplicationStopped => default;

public CancellationToken ApplicationStopping => default;

public void StopApplication() { }
}
}

/// <summary>
Expand Down
Loading
Loading