Skip to content

Commit

Permalink
Add IUserContextBuilder<TSchema> (#1118)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shane32 committed Feb 6, 2024
1 parent f903c2e commit dcc49f0
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 5 deletions.
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

0 comments on commit dcc49f0

Please sign in to comment.