Skip to content

Commit

Permalink
Update to GraphQL.NET v7 (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shane32 authored Aug 16, 2022
1 parent 1e3eb2b commit 8ba2049
Show file tree
Hide file tree
Showing 29 changed files with 174 additions and 119 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<ImplicitUsings>true</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<GraphQLVersion>[5.3.3,6.0.0)</GraphQLVersion>
<GraphQLVersion>7.0.0</GraphQLVersion>
<NoWarn>$(NoWarn);IDE0056;IDE0057</NoWarn>
</PropertyGroup>

Expand Down
22 changes: 10 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,7 @@ for changes from previous versions.
### Typical configuration with HTTP middleware

First add the `GraphQL.AspNetCore3` nuget package to your application. It requires
`GraphQL` version 5.3.3 or a later 5.x version.

Second, install the `GraphQL.SystemTextJson` or `GraphQL.NewtonsoftJson` package within your
application if you have not already done so. For best performance, please use the
`GraphQL.SystemTextJson` package.
`GraphQL` version 7.0.0 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
Expand All @@ -58,8 +54,7 @@ Below is a complete sample of a .NET 6 console app that hosts a GraphQL endpoint
</PropertyGroup>

<ItemGroup>
<PackageReference Include="GraphQL.AspNetCore3" Version="4.0.1" />
<PackageReference Include="GraphQL.SystemTextJson" Version="5.3.3" />
<PackageReference Include="GraphQL.AspNetCore3" Version="5.0.0" />
</ItemGroup>

</Project>
Expand Down Expand Up @@ -264,14 +259,14 @@ if you allow anonymous requests.
#### For individual graphs, fields and query arguments

To configure ASP.NET Core authorization for GraphQL, add the corresponding
validation rule during GraphQL configuration, typically by calling `.AddAuthorization()`
validation rule during GraphQL configuration, typically by calling `.AddAuthorizationRule()`
as shown below:

```csharp
builder.Services.AddGraphQL(b => b
.AddAutoSchema<Query>()
.AddSystemTextJson()
.AddAuthorization());
.AddAuthorizationRule());
```

Both roles and policies are supported for output graph types, fields on output graph types,
Expand Down Expand Up @@ -315,7 +310,7 @@ fields are marked with `AllowAnonymous`.

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](https://github.com/graphql-dotnet/server)
repository which work well with ASP.Net Core 3.1+. Below is a list of the nuget packages offered:
repository which work well with ASP.Net Core 2.1+. Below is a list of the nuget packages offered:

| Package | Downloads | NuGet Latest |
|------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
Expand Down Expand Up @@ -408,12 +403,15 @@ and [this](http://www.breachattack.com/#howitworks) 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.

The only additional requirement is that you must add this code in your `Startup.cs` file:
One additional requirement is that you must add this code in your `Startup.cs` file:

```csharp
services.AddHostApplicationLifetime();
```

You will also 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.
Expand Down Expand Up @@ -513,7 +511,7 @@ and Subscription portions of your schema, as shown below:
builder.Services.AddGraphQL(b => b
.AddSchema<MySchema>()
.AddSystemTextJson()
.AddAuthorization()); // add authorization validation rule
.AddAuthorizationRule()); // add authorization validation rule
var app = builder.Build();
app.UseDeveloperExceptionPage();
Expand Down
22 changes: 22 additions & 0 deletions migration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# Version history / migration notes

## 5.0.0

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

`builder.AddAuthorization()` has been renamed to `builder.AddAuthorizationRule()`.
The old method has been marked as deprecated.

The authorization validation rule and supporting methods have been changed to be
asynchronous, to match the new asynchronous signatures of `IValidationRule` in
GraphQL.NET v7. If you override any methods, they will need to be updated with
the new signature.

The authorization rule now pulls `ClaimsPrincipal` indirectly from
`ExecutionOptions.User`. This value must be set properly from the ASP.NET middleware.
While the default implementation has this update in place, if you override
`GraphQLHttpMiddleware.ExecuteRequestAsync` or do not use the provided ASP.NET
middleware, you must set the value in your code. Another consequence of this
change is that the constructor of `AuthorizationValidationRule` does not require
`IHttpContextAccessor`, and `IHttpContextAccessor` is not required to be registered
within the dependency injection framework (previously provided automatically by
`builder.AddAuthorization()`).

## 4.0.0

Remove `AllowEmptyQuery` option, as this error condition is now handled by the
Expand Down
18 changes: 4 additions & 14 deletions src/GraphQL.AspNetCore3/AuthorizationValidationRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,19 @@ namespace GraphQL.AspNetCore3;
/// </summary>
public class AuthorizationValidationRule : IValidationRule
{
private readonly IHttpContextAccessor _contextAccessor;

/// <inheritdoc cref="AuthorizationValidationRule"/>
public AuthorizationValidationRule(IHttpContextAccessor httpContextAccessor)
{
_contextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}

/// <inheritdoc/>
public virtual ValueTask<INodeVisitor?> ValidateAsync(ValidationContext context)
public virtual async ValueTask<INodeVisitor?> ValidateAsync(ValidationContext context)
{
var httpContext = _contextAccessor.HttpContext
?? throw new InvalidOperationException("HttpContext could not be retrieved from IHttpContextAccessor.");
var user = httpContext.User
?? throw new InvalidOperationException("ClaimsPrincipal could not be retrieved from HttpContext.");
var user = context.User
?? throw new InvalidOperationException("User could not be retrieved from ValidationContext. Please be sure it is set in ExecutionOptions.User.");
var provider = context.RequestServices
?? throw new MissingRequestServicesException();
var authService = provider.GetService<IAuthorizationService>()
?? throw new InvalidOperationException("An instance of IAuthorizationService could not be pulled from the dependency injection framework.");

var visitor = new AuthorizationVisitor(context, user, authService);
// if the schema fails authentication, report the error and do not perform any additional authorization checks.
return visitor.ValidateSchema(context) ? new(visitor) : default;
return await visitor.ValidateSchemaAsync(context) ? visitor : null;
}
}

4 changes: 2 additions & 2 deletions src/GraphQL.AspNetCore3/AuthorizationVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ protected override bool IsInRole(string role)
=> ClaimsPrincipal.IsInRole(role);

/// <inheritdoc/>
protected override AuthorizationResult Authorize(string policy)
=> AuthorizationService.AuthorizeAsync(ClaimsPrincipal, policy).GetAwaiter().GetResult();
protected override ValueTask<AuthorizationResult> AuthorizeAsync(string policy)
=> new(AuthorizationService.AuthorizeAsync(ClaimsPrincipal, policy));
}
14 changes: 7 additions & 7 deletions src/GraphQL.AspNetCore3/AuthorizationVisitorBase.Validation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ public partial class AuthorizationVisitorBase
/// Validates authorization rules for the schema.
/// Returns a value indicating if validation was successful.
/// </summary>
public virtual bool ValidateSchema(ValidationContext context)
=> Validate(context.Schema, null, context);
public virtual ValueTask<bool> ValidateSchemaAsync(ValidationContext context)
=> ValidateAsync(context.Schema, null, context);

/// <summary>
/// Validate a node that is current within the context.
/// </summary>
private bool Validate(IProvideMetadata obj, ASTNode? node, ValidationContext context)
=> Validate(BuildValidationInfo(node, obj, context));
private ValueTask<bool> ValidateAsync(IProvideMetadata obj, ASTNode? node, ValidationContext context)
=> ValidateAsync(BuildValidationInfo(node, obj, context));

/// <summary>
/// Initializes a new <see cref="ValidationInfo"/> instance for the specified node.
Expand Down Expand Up @@ -67,7 +67,7 @@ public readonly record struct ValidationInfo(
/// as this is handled elsewhere.
/// Returns a value indicating if validation was successful for this node.
/// </summary>
protected virtual bool Validate(ValidationInfo info)
protected virtual async ValueTask<bool> ValidateAsync(ValidationInfo info)
{
bool requiresAuthorization = info.Obj.IsAuthorizationRequired();
if (!requiresAuthorization)
Expand All @@ -84,7 +84,7 @@ protected virtual bool Validate(ValidationInfo info)
_policyResults ??= new Dictionary<string, AuthorizationResult>();
foreach (var policy in policies) {
if (!_policyResults.TryGetValue(policy, out var result)) {
result = Authorize(policy);
result = await AuthorizeAsync(policy);
_policyResults.Add(policy, result);
}
if (!result.Succeeded) {
Expand Down Expand Up @@ -120,7 +120,7 @@ protected virtual bool Validate(ValidationInfo info)
protected abstract bool IsInRole(string role);

/// <inheritdoc cref="IAuthorizationService.AuthorizeAsync(ClaimsPrincipal, object, string)"/>
protected abstract AuthorizationResult Authorize(string policy);
protected abstract ValueTask<AuthorizationResult> AuthorizeAsync(string policy);

/// <summary>
/// Adds a error to the validation context indicating that the user is not authenticated
Expand Down
24 changes: 12 additions & 12 deletions src/GraphQL.AspNetCore3/AuthorizationVisitorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public AuthorizationVisitorBase(ValidationContext context)
private List<TodoInfo>? _todos;

/// <inheritdoc/>
public virtual void Enter(ASTNode node, ValidationContext context)
public virtual async ValueTask EnterAsync(ASTNode node, ValidationContext context)
{
// if the node is the selected operation, or if it is a fragment referenced by the current operation,
// then enable authorization checks on decendant nodes (_checkTree = true)
Expand Down Expand Up @@ -58,7 +58,7 @@ public virtual void Enter(ASTNode node, ValidationContext context)

// Fields, unlike types, are validated immediately.
if (!fieldAnonymousAllowed) {
Validate(field, node, context);
await ValidateAsync(field, node, context);
}
}

Expand Down Expand Up @@ -92,15 +92,15 @@ public virtual void Enter(ASTNode node, ValidationContext context)
// verify field argument
var arg = context.TypeInfo.GetArgument();
if (arg != null) {
Validate(arg, node, context);
await ValidateAsync(arg, node, context);
}
}
}
}
}

/// <inheritdoc/>
public virtual void Leave(ASTNode node, ValidationContext context)
public virtual async ValueTask LeaveAsync(ASTNode node, ValidationContext context)
{
if (!_checkTree) {
// if we are within a field skipped by a directive, resume auth checks at the appropriate time
Expand All @@ -114,30 +114,30 @@ public virtual void Leave(ASTNode node, ValidationContext context)
}
if (node == context.Operation) {
_checkTree = false;
PopAndProcess();
await PopAndProcessAsync();
} else if (node is GraphQLFragmentDefinition fragmentDefinition) {
// once a fragment is done being processed, apply it to all types waiting on fragment checks,
// and process checks for types that are not waiting on any fragments
_checkTree = false;
var fragmentName = fragmentDefinition.FragmentName.Name.StringValue;
var ti = _onlyAnonymousSelected.Pop();
RecursiveResolve(fragmentName, ti, context);
await RecursiveResolveAsync(fragmentName, ti, context);
_fragments ??= new();
_fragments.TryAdd(fragmentName, ti);
} else if (_checkTree && node is GraphQLField) {
PopAndProcess();
await PopAndProcessAsync();
}

// pop the current type info, and validate the type if it does not contain only fields marked
// with AllowAnonymous (assuming it is not waiting on fragments)
void PopAndProcess()
async ValueTask PopAndProcessAsync()
{
var info = _onlyAnonymousSelected.Pop();
var type = context.TypeInfo.GetLastType()?.GetNamedType();
if (type == null)
return;
if (info.AnyAuthenticated || (!info.AnyAnonymous && (info.WaitingOnFragments?.Count ?? 0) == 0)) {
Validate(type, node, context);
await ValidateAsync(type, node, context);
} else if (info.WaitingOnFragments?.Count > 0) {
_todos ??= new();
_todos.Add(new(BuildValidationInfo(node, type, context), info));
Expand Down Expand Up @@ -205,7 +205,7 @@ static bool GetDirectiveValue(GraphQLDirective directive, ValidationContext cont
/// Runs when a fragment is added or updated; the fragment might not be waiting on any
/// other fragments, or it still might be.
/// </summary>
private void RecursiveResolve(string fragmentName, TypeInfo ti, ValidationContext context)
private async ValueTask RecursiveResolveAsync(string fragmentName, TypeInfo ti, ValidationContext context)
{
// first see if any other fragments are waiting on this fragment
if (_fragments != null) {
Expand All @@ -216,7 +216,7 @@ private void RecursiveResolve(string fragmentName, TypeInfo ti, ValidationContex
ti2.AnyAuthenticated |= ti.AnyAuthenticated;
ti2.AnyAnonymous |= ti.AnyAnonymous;
_fragments[fragment.Key] = ti2;
RecursiveResolve(fragment.Key, ti2, context);
await RecursiveResolveAsync(fragment.Key, ti2, context);
goto Retry; // modifying a collection at runtime is not supported
}
}
Expand All @@ -234,7 +234,7 @@ private void RecursiveResolve(string fragmentName, TypeInfo ti, ValidationContex
_todos.RemoveAt(i);
count--;
if (todo.AnyAuthenticated || !todo.AnyAnonymous) {
Validate(todo.ValidationInfo);
await ValidateAsync(todo.ValidationInfo);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/GraphQL.AspNetCore3/GraphQL.AspNetCore3.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

<ItemGroup Condition="'$(TargetFramework)' != 'netstandard2.0' AND '$(TargetFramework)' != 'netcoreapp2.1'">
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="GraphQL.SystemTextJson" Version="$(GraphQLVersion)" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' OR '$(TargetFramework)' == 'netcoreapp2.1'">
Expand Down
9 changes: 8 additions & 1 deletion src/GraphQL.AspNetCore3/GraphQLBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ public static class GraphQLBuilderExtensions
/// Registers <see cref="AuthorizationValidationRule"/> with the dependency injection framework
/// and configures it to be used when executing a request.
/// </summary>
[Obsolete("Please use AddAuthorizationRule")]
public static IGraphQLBuilder AddAuthorization(this IGraphQLBuilder builder)
=> AddAuthorizationRule(builder);

/// <summary>
/// Registers <see cref="AuthorizationValidationRule"/> with the dependency injection framework
/// and configures it to be used when executing a request.
/// </summary>
public static IGraphQLBuilder AddAuthorizationRule(this IGraphQLBuilder builder)
{
builder.AddValidationRule<AuthorizationValidationRule>(true);
builder.Services.TryRegister<IHttpContextAccessor, HttpContextAccessor>(ServiceLifetime.Singleton);
return builder;
}

Expand Down
1 change: 1 addition & 0 deletions src/GraphQL.AspNetCore3/GraphQLHttpMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ protected virtual async Task<ExecutionResult> ExecuteRequestAsync(HttpContext co
OperationName = request?.OperationName,
RequestServices = serviceProvider,
UserContext = userContext,
User = context.User,
};
if (!context.WebSockets.IsWebSocketRequest) {
if (HttpMethods.IsGet(context.Request.Method)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ protected override async Task<ExecutionResult> ExecuteRequestAsync(OperationMess
OperationName = request.OperationName,
RequestServices = scope.ServiceProvider,
CancellationToken = CancellationToken,
User = Connection.HttpContext.User,
};
if (UserContext != null)
options.UserContext = UserContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ protected override async Task<ExecutionResult> ExecuteRequestAsync(OperationMess
OperationName = request.OperationName,
RequestServices = scope.ServiceProvider,
CancellationToken = CancellationToken,
User = Connection.HttpContext.User,
};
if (UserContext != null)
options.UserContext = UserContext;
Expand Down
3 changes: 1 addition & 2 deletions src/Samples/AuthorizationSample/AuthorizationSample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
<ItemGroup>
<ProjectReference Include="..\..\GraphQL.AspNetCore3\GraphQL.AspNetCore3.csproj" />
<ProjectReference Include="..\ChatSchema\Chat.csproj" />
<PackageReference Include="GraphQL.Server.Ui.Playground" Version="5.2.1" />
<PackageReference Include="GraphQL.SystemTextJson" Version="$(GraphQLVersion)" />
<PackageReference Include="GraphQL.Server.Ui.Playground" Version="7.0.0" />
</ItemGroup>

</Project>
6 changes: 3 additions & 3 deletions src/Samples/AuthorizationSample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
builder.Services.AddGraphQL(b => b
.AddAutoSchema<AuthorizationSample.Schema.Query>(s => s.WithMutation<AuthorizationSample.Schema.Mutation>())
.AddSystemTextJson()
.AddAuthorization());
.AddAuthorizationRule());
// ------------------------------------

var app = builder.Build();
Expand Down Expand Up @@ -61,11 +61,11 @@
app.UseGraphQL("/graphql");
// configure Playground at "/ui/graphql"
app.UseGraphQLPlayground(
"/ui/graphql",
new GraphQL.Server.Ui.Playground.PlaygroundOptions {
GraphQLEndPoint = new PathString("/graphql"),
SubscriptionsEndPoint = new PathString("/graphql"),
},
"/ui/graphql");
});
// -------------------------------------

app.MapRazorPages();
Expand Down
3 changes: 1 addition & 2 deletions src/Samples/BasicSample/BasicSample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
<ItemGroup>
<ProjectReference Include="..\..\GraphQL.AspNetCore3\GraphQL.AspNetCore3.csproj" />
<ProjectReference Include="..\ChatSchema\Chat.csproj" />
<PackageReference Include="GraphQL.Server.Ui.Playground" Version="5.2.1" />
<PackageReference Include="GraphQL.SystemTextJson" Version="$(GraphQLVersion)" />
<PackageReference Include="GraphQL.Server.Ui.Playground" Version="7.0.0" />
</ItemGroup>

</Project>
Loading

0 comments on commit 8ba2049

Please sign in to comment.