Skip to content

Commit

Permalink
Bump to v6 / GraphQL.NET v8 (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shane32 authored Aug 19, 2024
1 parent f1b6afe commit 065a29e
Show file tree
Hide file tree
Showing 16 changed files with 113 additions and 52 deletions.
7 changes: 4 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>

<PropertyGroup>
<VersionPrefix>5.0.0-preview</VersionPrefix>
<VersionPrefix>6.0.0-preview</VersionPrefix>
<LangVersion>12.0</LangVersion>
<Copyright>Shane Krueger</Copyright>
<Authors>Shane Krueger</Authors>
Expand All @@ -20,12 +20,13 @@
<ImplicitUsings>true</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<GraphQLVersion>[7.0.0,8.0.0)</GraphQLVersion>
<!--<GraphQLVersion>[8.0.0,9.0.0)</GraphQLVersion>-->
<GraphQLVersion>8.0.0-preview-1068</GraphQLVersion>
<NoWarn>$(NoWarn);IDE0056;IDE0057;NU1902;NU1903</NoWarn>
</PropertyGroup>

<ItemGroup Condition="'$(IsPackable)' == 'true'">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
<None Include="..\..\logo.64x64.png" Pack="true" PackagePath="\"/>
<None Include="..\..\LICENSE" Pack="true" PackagePath="\"/>
<None Include="..\..\README.md" Pack="true" PackagePath="\"/>
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![NuGet](https://img.shields.io/nuget/v/GraphQL.AspNetCore3.svg)](https://www.nuget.org/packages/GraphQL.AspNetCore3) [![Coverage Status](https://coveralls.io/repos/github/Shane32/GraphQL.AspNetCore3/badge.svg?branch=master)](https://coveralls.io/github/Shane32/GraphQL.AspNetCore3?branch=master)

This package is designed for ASP.Net Core (2.1 through 6.0) to facilitate easy set-up of GraphQL requests
This package is designed for ASP.Net Core (2.1 through 8.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.
Expand Down
48 changes: 47 additions & 1 deletion migration.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,54 @@
# Version history / migration notes

## 6.0.0

GraphQL.AspNetCore3 v6 requires GraphQL.NET v8 or newer.

### New features

- When using `FormFileGraphType` with type-first schemas, you may specify the allowed media
types for the file by using the new `[MediaType]` attribute on the argument or input object field.
- Cross-site request forgery (CSRF) protection has been added for both GET and POST requests,
enabled by default.
- Status codes for validation errors are now, by default, determined by the response content type,
and for authentication errors may return a 401 or 403 status code. These changes are purusant
to the [GraphQL over HTTP specification](https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md).
See the breaking changes section below for more information.

### Breaking changes

- `GraphQLHttpMiddlewareOptions.ValidationErrorsReturnBadRequest` is now a nullable boolean where
`null` means "use the default behavior". The default behavior is to return a 200 status code
when the response content type is `application/json` and a 400 status code otherwise. The
default value for this in v7 was `true`; set this option to retain the v7 behavior.
- The validation rules' signatures have changed slightly due to the underlying changes to the
GraphQL.NET library. Please see the GraphQL.NET v8 migration document for more information.
- Cross-site request forgery (CSRF) protection has been enabled for all requests by default.
This will require that the `GraphQL-Require-Preflight` header be sent with all GET requests and
all form-POST requests. To disable this feature, set the `CsrfProtectionEnabled` property on the
`GraphQLMiddlewareOptions` class to `false`. You may also configure the headers list by modifying
the `CsrfProtectionHeaders` property on the same class. See the readme for more details.
- Form POST requests are disabled by default; to enable them, set the `ReadFormOnPost` setting
to `true`.
- Validation errors such as authentication errors may now be returned with a 'preferred' status
code instead of a 400 status code. This occurs when (1) the response would otherwise contain
a 400 status code (e.g. the execution of the document has not yet begun), and (2) all errors
in the response prefer the same status code. For practical purposes, this means that the included
errors triggered by the authorization validation rule will now return 401 or 403 when appropriate.
- The `SelectResponseContentType` method now returns a `MediaTypeHeaderValue` instead of a string.
- The `AuthorizationVisitorBase.GetRecursivelyReferencedUsedFragments` method has been removed as
`ValidationContext` now provides an overload to `GetRecursivelyReferencedFragments` which will only
return fragments in use by the specified operation.
- The `AuthorizationVisitorBase.SkipNode` method has been removed as `ValidationContext` now provides
a `ShouldIncludeNode` method.

## Other changes

- GraphiQL has been bumped from 1.5.1 to 3.2.0.

## 5.0.0

GraphQL.AspNetCore3 v5 requires GraphQL.NET v7 or newer.
GraphQL.AspNetCore3 v5 requires GraphQL.NET v7.

`builder.AddAuthorization()` has been renamed to `builder.AddAuthorizationRule()`.
The old method has been marked as deprecated.
Expand Down
4 changes: 2 additions & 2 deletions src/GraphQL.AspNetCore3/AuthorizationValidationRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ namespace GraphQL.AspNetCore3;
/// <summary>
/// Validates a document against the configured set of policy and role requirements.
/// </summary>
public class AuthorizationValidationRule : IValidationRule
public class AuthorizationValidationRule : ValidationRuleBase
{
/// <inheritdoc/>
public virtual async ValueTask<INodeVisitor?> ValidateAsync(ValidationContext context)
public override async ValueTask<INodeVisitor?> GetPreNodeVisitorAsync(ValidationContext context)
{
var user = context.User
?? throw new InvalidOperationException("User could not be retrieved from ValidationContext. Please be sure it is set in ExecutionOptions.User.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public virtual ValueTask<bool> ValidateSchemaAsync(ValidationContext context)
/// <summary>
/// Validate a node that is current within the context.
/// </summary>
private ValueTask<bool> ValidateAsync(IProvideMetadata obj, ASTNode? node, ValidationContext context)
private ValueTask<bool> ValidateAsync(IMetadataReader obj, ASTNode? node, ValidationContext context)
=> ValidateAsync(BuildValidationInfo(node, obj, context));

/// <summary>
Expand All @@ -25,7 +25,7 @@ private ValueTask<bool> ValidateAsync(IProvideMetadata obj, ASTNode? node, Valid
/// <param name="node">The specified <see cref="ASTNode"/>.</param>
/// <param name="obj">The <see cref="IGraphType"/>, <see cref="IFieldType"/> or <see cref="QueryArgument"/> which has been matched to the node specified in <paramref name="node"/>.</param>
/// <param name="context">The validation context.</param>
private static ValidationInfo BuildValidationInfo(ASTNode? node, IProvideMetadata obj, ValidationContext context)
private static ValidationInfo BuildValidationInfo(ASTNode? node, IMetadataReader obj, ValidationContext context)
{
IFieldType? parentFieldType = null;
IGraphType? parentGraphType = null;
Expand All @@ -50,7 +50,7 @@ private static ValidationInfo BuildValidationInfo(ASTNode? node, IProvideMetadat
/// <param name="ParentFieldType">For graph types other than operations, the field where this type was referenced; for query arguments, the field to which this argument belongs.</param>
/// <param name="ParentGraphType">For graph types, the graph type for the field where this type was referenced; for field types, the graph type to which this field belongs; for query arguments, the graph type for the field to which this argument belongs.</param>
public readonly record struct ValidationInfo(
IProvideMetadata Obj,
IMetadataReader Obj,
ASTNode? Node,
IFieldType? ParentFieldType,
IGraphType? ParentGraphType,
Expand All @@ -63,7 +63,7 @@ public readonly record struct ValidationInfo(

/// <summary>
/// Validates authorization rules for the specified schema, graph, field or query argument.
/// Does not consider <see cref="AuthorizationExtensions.IsAnonymousAllowed(IProvideMetadata)"/>
/// Does not consider <see cref="AuthorizationExtensions.IsAnonymousAllowed(IMetadataReader)"/>
/// as this is handled elsewhere.
/// Returns a value indicating if validation was successful for this node.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions src/GraphQL.AspNetCore3/HttpGetValidationRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ namespace GraphQL.AspNetCore3;
/// <summary>
/// Validates that HTTP GET requests only execute queries; not mutations or subscriptions.
/// </summary>
public sealed class HttpGetValidationRule : IValidationRule
public sealed class HttpGetValidationRule : ValidationRuleBase
{
/// <inheritdoc/>
public ValueTask<INodeVisitor?> ValidateAsync(ValidationContext context)
public override ValueTask<INodeVisitor?> GetPreNodeVisitorAsync(ValidationContext context)
{
if (context.Operation.Operation != OperationType.Query) {
context.ReportError(new HttpMethodValidationError(context.Document.Source, context.Operation, "Only query operations allowed for GET requests."));
Expand Down
4 changes: 2 additions & 2 deletions src/GraphQL.AspNetCore3/HttpPostValidationRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ namespace GraphQL.AspNetCore3;
/// <summary>
/// Validates that HTTP POST requests do not execute subscriptions.
/// </summary>
public sealed class HttpPostValidationRule : IValidationRule
public sealed class HttpPostValidationRule : ValidationRuleBase
{
/// <inheritdoc/>
public ValueTask<INodeVisitor?> ValidateAsync(ValidationContext context)
public override ValueTask<INodeVisitor?> GetPreNodeVisitorAsync(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."));
Expand Down
24 changes: 12 additions & 12 deletions src/Tests.ApiApprovals/GraphQL.AspNetCore3.approved.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ namespace GraphQL.AspNetCore3
public System.Func<TState, Microsoft.AspNetCore.Authorization.AuthorizationResult, System.Threading.Tasks.Task>? OnNotAuthorizedPolicy { get; set; }
public System.Func<TState, System.Threading.Tasks.Task>? OnNotAuthorizedRole { get; set; }
}
public class AuthorizationValidationRule : GraphQL.Validation.IValidationRule
public class AuthorizationValidationRule : GraphQL.Validation.ValidationRuleBase
{
public AuthorizationValidationRule() { }
public virtual System.Threading.Tasks.ValueTask<GraphQL.Validation.INodeVisitor?> ValidateAsync(GraphQL.Validation.ValidationContext context) { }
public override System.Threading.Tasks.ValueTask<GraphQL.Validation.INodeVisitor?> GetPreNodeVisitorAsync(GraphQL.Validation.ValidationContext context) { }
}
public class AuthorizationVisitor : GraphQL.AspNetCore3.AuthorizationVisitorBase
{
Expand Down Expand Up @@ -47,12 +47,12 @@ namespace GraphQL.AspNetCore3
public virtual System.Threading.Tasks.ValueTask<bool> ValidateSchemaAsync(GraphQL.Validation.ValidationContext context) { }
public readonly struct ValidationInfo : System.IEquatable<GraphQL.AspNetCore3.AuthorizationVisitorBase.ValidationInfo>
{
public ValidationInfo(GraphQL.Types.IProvideMetadata Obj, GraphQLParser.AST.ASTNode? Node, GraphQL.Types.IFieldType? ParentFieldType, GraphQL.Types.IGraphType? ParentGraphType, GraphQL.Validation.ValidationContext Context) { }
public GraphQL.Validation.ValidationContext Context { get; set; }
public GraphQLParser.AST.ASTNode? Node { get; set; }
public GraphQL.Types.IProvideMetadata Obj { get; set; }
public GraphQL.Types.IFieldType? ParentFieldType { get; set; }
public GraphQL.Types.IGraphType? ParentGraphType { get; set; }
public ValidationInfo(GraphQL.Types.IMetadataReader Obj, GraphQLParser.AST.ASTNode? Node, GraphQL.Types.IFieldType? ParentFieldType, GraphQL.Types.IGraphType? ParentGraphType, GraphQL.Validation.ValidationContext Context) { }
public GraphQL.Validation.ValidationContext Context { get; init; }
public GraphQLParser.AST.ASTNode? Node { get; init; }
public GraphQL.Types.IMetadataReader Obj { get; init; }
public GraphQL.Types.IFieldType? ParentFieldType { get; init; }
public GraphQL.Types.IGraphType? ParentGraphType { get; init; }
}
}
public class ExecutionResultActionResult : Microsoft.AspNetCore.Mvc.IActionResult
Expand Down Expand Up @@ -183,15 +183,15 @@ namespace GraphQL.AspNetCore3
public GraphQLHttpMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, GraphQL.IGraphQLTextSerializer serializer, GraphQL.IDocumentExecuter<TSchema> documentExecuter, Microsoft.Extensions.DependencyInjection.IServiceScopeFactory serviceScopeFactory, GraphQL.AspNetCore3.GraphQLHttpMiddlewareOptions options, Microsoft.Extensions.Hosting.IHostApplicationLifetime hostApplicationLifetime) { }
protected override System.Threading.Tasks.ValueTask<System.Collections.Generic.IDictionary<string, object?>> BuildUserContextAsync(Microsoft.AspNetCore.Http.HttpContext context, object? payload) { }
}
public sealed class HttpGetValidationRule : GraphQL.Validation.IValidationRule
public sealed class HttpGetValidationRule : GraphQL.Validation.ValidationRuleBase
{
public HttpGetValidationRule() { }
public System.Threading.Tasks.ValueTask<GraphQL.Validation.INodeVisitor?> ValidateAsync(GraphQL.Validation.ValidationContext context) { }
public override System.Threading.Tasks.ValueTask<GraphQL.Validation.INodeVisitor?> GetPreNodeVisitorAsync(GraphQL.Validation.ValidationContext context) { }
}
public sealed class HttpPostValidationRule : GraphQL.Validation.IValidationRule
public sealed class HttpPostValidationRule : GraphQL.Validation.ValidationRuleBase
{
public HttpPostValidationRule() { }
public System.Threading.Tasks.ValueTask<GraphQL.Validation.INodeVisitor?> ValidateAsync(GraphQL.Validation.ValidationContext context) { }
public override System.Threading.Tasks.ValueTask<GraphQL.Validation.INodeVisitor?> GetPreNodeVisitorAsync(GraphQL.Validation.ValidationContext context) { }
}
public interface IAuthorizationOptions
{
Expand Down
8 changes: 4 additions & 4 deletions src/Tests.ApiApprovals/Tests.ApiTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="PublicApiGenerator" Version="10.3.0" />
<PackageReference Include="Shouldly" Version="4.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.3" />
<PackageReference Include="PublicApiGenerator" Version="11.1.0" />
<PackageReference Include="Shouldly" Version="4.2.1" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
Expand Down
16 changes: 8 additions & 8 deletions src/Tests/AuthorizationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ private IValidationResult Validate(string query, bool shouldPassCoreRules = true
var inputs = new GraphQLSerializer().Deserialize<Inputs>(variables) ?? Inputs.Empty;

var validator = new DocumentValidator();
var (coreRulesResult, _) = validator.ValidateAsync(new ValidationOptions {
var validationResult = validator.ValidateAsync(new ValidationOptions {
Document = document,
Extensions = Inputs.Empty,
Operation = (GraphQLOperationDefinition)document.Definitions.First(x => x.Kind == ASTNodeKind.OperationDefinition),
Expand All @@ -61,9 +61,9 @@ private IValidationResult Validate(string query, bool shouldPassCoreRules = true
RequestServices = mockServices.Object,
User = _principal,
}).GetAwaiter().GetResult(); // there is no async code being tested
coreRulesResult.IsValid.ShouldBe(shouldPassCoreRules);
validationResult.IsValid.ShouldBe(shouldPassCoreRules);

var (result, _) = validator.ValidateAsync(new ValidationOptions {
var result = validator.ValidateAsync(new ValidationOptions {
Document = document,
Extensions = Inputs.Empty,
Operation = (GraphQLOperationDefinition)document.Definitions.First(x => x.Kind == ASTNodeKind.OperationDefinition),
Expand Down Expand Up @@ -495,7 +495,7 @@ public void UnusedFragmentsAreIgnored()
ret.IsValid.ShouldBeFalse();
}

private void Apply(IProvideMetadata obj, Mode mode)
private void Apply(IMetadataWriter obj, Mode mode)
{
switch (mode) {
case Mode.None:
Expand Down Expand Up @@ -576,7 +576,7 @@ public void MiscErrors(bool noClaimsPrincipal, bool noRequestServices, bool noAu
}

[Fact]
public void NullIdentity()
public async Task NullIdentity()
{
var mockAuthorizationService = new Mock<IAuthorizationService>(MockBehavior.Strict);
var mockServices = new Mock<IServiceProvider>(MockBehavior.Strict);
Expand All @@ -585,7 +585,7 @@ public void NullIdentity()
var validator = new DocumentValidator();
_schema.Authorize();

var (result, _) = validator.ValidateAsync(new ValidationOptions {
var result = await validator.ValidateAsync(new ValidationOptions {
Document = document,
Extensions = Inputs.Empty,
Operation = (GraphQLOperationDefinition)document.Definitions.Single(x => x.Kind == ASTNodeKind.OperationDefinition),
Expand All @@ -595,7 +595,7 @@ public void NullIdentity()
Variables = Inputs.Empty,
RequestServices = mockServices.Object,
User = new ClaimsPrincipal(),
}).GetAwaiter().GetResult(); // there is no async code being tested
});

result.Errors.ShouldHaveSingleItem().ShouldBeOfType<AccessDeniedError>().Message.ShouldBe("Access denied for schema.");
}
Expand Down Expand Up @@ -665,7 +665,7 @@ public void WithInterface(bool interfaceRequiresAuth, bool queryRequiresAuth, bo
[InlineData(@"{ parent { child(invalid: true) } }", null, true)]
[InlineData(@"query { ...frag1 }", null, true)]
[InlineData(@"query { ...frag1 } fragment frag1 on QueryType { ...frag1 }", null, true)]
public void TestDefective(string query, string variables, bool expectedIsValid)
public void TestDefective(string query, string? variables, bool expectedIsValid)
{
_query.AddField(new FieldType { Name = "test", Type = typeof(StringGraphType) }).Authorize();

Expand Down
1 change: 1 addition & 0 deletions src/Tests/BuilderMethodTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ public async Task EndpointRouting(string pattern, string url)
await VerifyAsync(url);
}

[Fact]
public async Task EndpointRouting_Invalid()
{
_hostBuilder.ConfigureServices(services => services.AddRouting());
Expand Down
4 changes: 2 additions & 2 deletions src/Tests/ChatTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public async Task AllMessages()
str.ShouldBe("{\"data\":{\"allMessages\":[{\"id\":\"1\",\"message\":\"hello\"},{\"id\":\"2\",\"message\":\"hello\"}]}}");
}

public async Task AddMessageInternal(int id, string name = "John Doe")
private async Task AddMessageInternal(int id, string name = "John Doe")
{
var str = await _app.ExecutePost(
"/graphql",
Expand All @@ -106,7 +106,7 @@ public async Task DeleteMessage()
str.ShouldBe("{\"data\":{\"count\":1}}");
}

public async Task DeleteMessageInternal(int id)
private async Task DeleteMessageInternal(int id)
{
var str = await _app.ExecutePost(
"/graphql",
Expand Down
1 change: 1 addition & 0 deletions src/Tests/Middleware/AuthorizationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ private TestServer CreateServer(Action<IServiceCollection>? configureServices =
.AddAutoSchema<Chat.Schema.Query>()
.AddErrorInfoProvider(new CustomErrorInfoProvider(this))
.AddSystemTextJson());
services.AddRouting();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters.ValidateIssuerSigningKey = false;
Expand Down
Loading

0 comments on commit 065a29e

Please sign in to comment.