Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IUserContextBuilder<TSchema> #1118

Merged
merged 2 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,10 @@ are long-lived, using scoped services within a user context builder will result
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.

### Mutations within GET requests

For security reasons and pursuant to current recommendations, mutation GraphQL requests
Expand Down
20 changes: 15 additions & 5 deletions src/Transports.AspNetCore/GraphQLHttpMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ public GraphQLHttpMiddleware(
: base(next, serializer, documentExecuter, serviceScopeFactory, options, hostApplicationLifetime)
{
}

/// <inheritdoc/>
protected override ValueTask<IDictionary<string, object?>?> BuildUserContextAsync(HttpContext context, object? payload)
{
var userContextBuilder = context.RequestServices.GetService<IUserContextBuilder<TSchema>>();
return userContextBuilder == null
? base.BuildUserContextAsync(context, payload)
: userContextBuilder.BuildUserContextAsync(context, payload);
}
}

/// <summary>
Expand Down Expand Up @@ -677,17 +686,18 @@ protected virtual async Task<ExecutionResult> ExecuteRequestAsync(HttpContext co
/// <see cref="IHttpContextAccessor"/> via <see cref="ExecutionOptions.RequestServices"/>
/// if needed.
/// <br/><br/>
/// By default this method pulls the registered <see cref="IUserContextBuilder"/>,
/// if any, within the service scope and executes it to build the user context.
/// By default this method pulls the registered <see cref="IUserContextBuilder{TSchema}"/>
/// or <see cref="IUserContextBuilder"/> instance, if any, within the service scope
/// and executes it to build the user context.
/// In this manner, both scoped and singleton <see cref="IUserContextBuilder"/>
/// instances are supported, although singleton instances are recommended.
/// </summary>
protected virtual async ValueTask<IDictionary<string, object?>?> BuildUserContextAsync(HttpContext context, object? payload)
protected virtual ValueTask<IDictionary<string, object?>?> BuildUserContextAsync(HttpContext context, object? payload)
{
var userContextBuilder = context.RequestServices.GetService<IUserContextBuilder>();
return userContextBuilder == null
? null
: await userContextBuilder.BuildUserContextAsync(context, payload);
? default // successful result of null
: userContextBuilder.BuildUserContextAsync(context, payload);
}

ValueTask<IDictionary<string, object?>?> IUserContextBuilder.BuildUserContextAsync(HttpContext context, object? payload)
Expand Down
6 changes: 6 additions & 0 deletions src/Transports.AspNetCore/IUserContextBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ public interface IUserContextBuilder
/// <returns>Dictionary object representing user context. Return <see langword="null"/> to use default user context.</returns>
ValueTask<IDictionary<string, object?>?> BuildUserContextAsync(HttpContext context, object? payload);
}

/// <inheritdoc cref="IUserContextBuilder"/>
public interface IUserContextBuilder<TSchema> : IUserContextBuilder
where TSchema : ISchema
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ namespace GraphQL.Server.Transports.AspNetCore
where TSchema : GraphQL.Types.ISchema
{
public GraphQLHttpMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, GraphQL.IGraphQLTextSerializer serializer, GraphQL.IDocumentExecuter<TSchema> documentExecuter, Microsoft.Extensions.DependencyInjection.IServiceScopeFactory serviceScopeFactory, GraphQL.Server.Transports.AspNetCore.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
{
Expand All @@ -166,6 +167,8 @@ namespace GraphQL.Server.Transports.AspNetCore
{
System.Threading.Tasks.ValueTask<System.Collections.Generic.IDictionary<string, object?>?> BuildUserContextAsync(Microsoft.AspNetCore.Http.HttpContext context, object? payload);
}
public interface IUserContextBuilder<TSchema> : GraphQL.Server.Transports.AspNetCore.IUserContextBuilder
where TSchema : GraphQL.Types.ISchema { }
public class UserContextBuilder<TUserContext> : GraphQL.Server.Transports.AspNetCore.IUserContextBuilder
where TUserContext : System.Collections.Generic.IDictionary<string, object?>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ namespace GraphQL.Server.Transports.AspNetCore
where TSchema : GraphQL.Types.ISchema
{
public GraphQLHttpMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, GraphQL.IGraphQLTextSerializer serializer, GraphQL.IDocumentExecuter<TSchema> documentExecuter, Microsoft.Extensions.DependencyInjection.IServiceScopeFactory serviceScopeFactory, GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddlewareOptions options, GraphQL.Server.Transports.AspNetCore.IHostApplicationLifetime hostApplicationLifetime) { }
protected override System.Threading.Tasks.ValueTask<System.Collections.Generic.IDictionary<string, object?>?> BuildUserContextAsync(Microsoft.AspNetCore.Http.HttpContext context, object? payload) { }
}
public class HostApplicationLifetime : GraphQL.Server.Transports.AspNetCore.IHostApplicationLifetime, Microsoft.Extensions.Hosting.IHostedService
{
Expand Down Expand Up @@ -184,6 +185,8 @@ namespace GraphQL.Server.Transports.AspNetCore
{
System.Threading.Tasks.ValueTask<System.Collections.Generic.IDictionary<string, object?>?> BuildUserContextAsync(Microsoft.AspNetCore.Http.HttpContext context, object? payload);
}
public interface IUserContextBuilder<TSchema> : GraphQL.Server.Transports.AspNetCore.IUserContextBuilder
where TSchema : GraphQL.Types.ISchema { }
public class UserContextBuilder<TUserContext> : GraphQL.Server.Transports.AspNetCore.IUserContextBuilder
where TUserContext : System.Collections.Generic.IDictionary<string, object?>
{
Expand Down
31 changes: 31 additions & 0 deletions tests/Transports.AspNetCore.Tests/Middleware/MiscTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,37 @@ public void Constructors()
_ = new GraphQLHttpMiddleware(next, serializer, executer, scopeFactory, options, appLifetime);
}

[Fact]
public async Task SchemaSpecificBuilders()
{
var middleware = new MyMiddleware<Schema>(
Mock.Of<RequestDelegate>(MockBehavior.Strict),
Mock.Of<IGraphQLTextSerializer>(MockBehavior.Strict),
Mock.Of<IDocumentExecuter<Schema>>(MockBehavior.Strict),
Mock.Of<IServiceScopeFactory>(MockBehavior.Strict),
new GraphQLHttpMiddlewareOptions(),
Mock.Of<IHostApplicationLifetime>(MockBehavior.Strict));
var builder = new Mock<IUserContextBuilder<Schema>>(MockBehavior.Strict);
var d = new Dictionary<string, object?> { { "test", "test" } };
builder.Setup(x => x.BuildUserContextAsync(It.IsAny<HttpContext>(), It.IsAny<object?>())).ReturnsAsync(d);
var serviceProviderMock = new Mock<IServiceProvider>(MockBehavior.Strict);
serviceProviderMock.Setup(x => x.GetService(typeof(IUserContextBuilder<Schema>))).Returns(builder.Object);
var contextMock = new Mock<HttpContext>(MockBehavior.Strict);
contextMock.Setup(x => x.RequestServices).Returns(serviceProviderMock.Object);
(await middleware.MyBuildUserContextAsync(contextMock.Object, null)).ShouldBe(d);
}

private class MyMiddleware<TSchema> : GraphQLHttpMiddleware<TSchema>
where TSchema : ISchema
{
public MyMiddleware(RequestDelegate next, IGraphQLTextSerializer serializer, IDocumentExecuter<TSchema> executer, IServiceScopeFactory scopeFactory, GraphQLHttpMiddlewareOptions options, IHostApplicationLifetime hostApplicationLifetime)
: base(next, serializer, executer, scopeFactory, options, hostApplicationLifetime)
{
}

public ValueTask<IDictionary<string, object?>?> MyBuildUserContextAsync(HttpContext context, object? payload) => base.BuildUserContextAsync(context, payload);
}

[Fact]
public async Task WriteErrorResponseString()
{
Expand Down
Loading