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

[Fusion] Add FusionGatewayBuilder.UseRequest #7283

Merged
merged 11 commits into from
Jul 25, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,88 @@ public static FusionGatewayBuilder UseAutomaticPersistedQueryPipeline(
return builder;
}

/// <summary>
/// Adds a type that will be used to create a middleware for the execution pipeline.
/// </summary>
/// <param name="builder">
/// The gateway builder.
/// </param>
/// <returns>
/// Returns the gateway builder for configuration chaining.
/// </returns>
public static FusionGatewayBuilder UseRequest<TMiddleware>(
this FusionGatewayBuilder builder)
where TMiddleware : class
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

builder.CoreBuilder.UseRequest<TMiddleware>();
return builder;
}

/// <summary>
/// Adds a delegate that will be used to create a middleware for the execution pipeline.
/// </summary>
/// <param name="builder">
/// The gateway builder.
/// </param>
/// <param name="middleware">
/// A delegate that is used to create a middleware for the execution pipeline.
/// </param>
/// <returns>
/// Returns the gateway builder for configuration chaining.
/// </returns>
public static FusionGatewayBuilder UseRequest(
this FusionGatewayBuilder builder,
RequestCoreMiddleware middleware)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

if (middleware is null)
{
throw new ArgumentNullException(nameof(middleware));
}

builder.CoreBuilder.UseRequest(middleware);
return builder;
}

/// <summary>
/// Adds a delegate that will be used to create a middleware for the execution pipeline.
/// </summary>
/// <param name="builder">
/// The gateway builder.
/// </param>
/// <param name="middleware">
/// A delegate that is used to create a middleware for the execution pipeline.
/// </param>
/// <returns>
/// Returns the gateway builder for configuration chaining.
/// </returns>
public static FusionGatewayBuilder UseRequest(
this FusionGatewayBuilder builder,
RequestMiddleware middleware)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

if (middleware is null)
{
throw new ArgumentNullException(nameof(middleware));
}

builder.CoreBuilder.UseRequest(middleware);
return builder;
}

private static IRequestExecutorBuilder UseFusionDefaultPipeline(
this IRequestExecutorBuilder builder)
{
Expand Down Expand Up @@ -481,7 +563,7 @@ public static IRequestExecutorBuilder UseDistributedOperationExecution(
throw new ArgumentNullException(nameof(builder));
}

return builder.UseRequest<DistributedOperationExecutionMiddleware>();
return builder.UseRequest(DistributedOperationExecutionMiddleware.Create());
}

internal static void AddDefaultPipeline(this IList<RequestCoreMiddleware> pipeline)
Expand Down
159 changes: 159 additions & 0 deletions src/HotChocolate/Fusion/test/Core.Tests/RequestPipelineTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using CookieCrumble;
using HotChocolate.Execution;
using HotChocolate.Fetching;
using HotChocolate.Fusion.Composition;
using HotChocolate.Fusion.Composition.Features;
using HotChocolate.Fusion.Shared;
using HotChocolate.Skimmed.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
using static HotChocolate.Language.Utf8GraphQLParser;

namespace HotChocolate.Fusion;

public class RequestPipelineTests(ITestOutputHelper output)
{
private readonly Func<ICompositionLog> _logFactory = () => new TestCompositionLog(output);

[Fact]
public async Task Custom_Middleware_Runs_Before_DefaultPipeline()
{
// arrange
using var demoProject = await DemoProject.CreateAsync();

var fusionGraph =
await new FusionGraphComposer(logFactory: _logFactory)
.ComposeAsync(new[] { demoProject.Accounts.ToConfiguration() });

var config = new DemoIntegrationTests.HotReloadConfiguration(
new GatewayConfiguration(
SchemaFormatter.FormatAsDocument(fusionGraph)));

var executor = await new ServiceCollection()
.AddSingleton(demoProject.HttpClientFactory)
.AddFusionGatewayServer()
.RegisterGatewayConfiguration(_ => config)
.UseRequest<TestMiddleware>()
.UseDefaultPipeline()
.BuildRequestExecutorAsync();

// act
var result = await executor.ExecuteAsync(
OperationRequestBuilder.New()
.SetDocument("{ __typename }")
.AddGlobalState("short-circuit", true)
.Build());

// assert
result.MatchInlineSnapshot(
"""
{
"data": {
"result": true
},
"extensions": {
"state": "custom middleware short-circuited"
}
}
""");
michaelstaib marked this conversation as resolved.
Show resolved Hide resolved
}

[Fact]
public async Task Custom_Middleware_Falls_Through_To_DefaultPipeline()
{
// arrange
using var demoProject = await DemoProject.CreateAsync();

var fusionGraph =
await new FusionGraphComposer(logFactory: _logFactory)
.ComposeAsync(new[] { demoProject.Accounts.ToConfiguration() });

var config = new DemoIntegrationTests.HotReloadConfiguration(
new GatewayConfiguration(
SchemaFormatter.FormatAsDocument(fusionGraph)));

var executor = await new ServiceCollection()
.AddSingleton(demoProject.HttpClientFactory)
.AddFusionGatewayServer()
.RegisterGatewayConfiguration(_ => config)
.UseRequest<TestMiddleware>()
.UseDefaultPipeline()
.BuildRequestExecutorAsync();

// act
var result = await executor.ExecuteAsync("{ __typename }");

// assert
result.MatchInlineSnapshot("""
{
"data": {
"__typename": "Query"
}
}
""");
michaelstaib marked this conversation as resolved.
Show resolved Hide resolved
}

[Fact]
public async Task Custom_Middleware_Without_DefaultPipeline_No_Other_Middleware_Registered()
{
// arrange
using var demoProject = await DemoProject.CreateAsync();

var fusionGraph =
await new FusionGraphComposer(logFactory: _logFactory)
.ComposeAsync(new[] { demoProject.Accounts.ToConfiguration() });

var config = new DemoIntegrationTests.HotReloadConfiguration(
new GatewayConfiguration(
SchemaFormatter.FormatAsDocument(fusionGraph)));

var executor = await new ServiceCollection()
.AddSingleton(demoProject.HttpClientFactory)
.AddFusionGatewayServer()
.RegisterGatewayConfiguration(_ => config)
.UseRequest<TestMiddleware>()
.BuildRequestExecutorAsync();

// act
var result = await executor.ExecuteAsync("{ __typename }");

// assert
result.MatchInlineSnapshot("""
{
"data": {
"result": true
},
"extensions": {
"state": "default pipeline didn't run"
}
}
""");
michaelstaib marked this conversation as resolved.
Show resolved Hide resolved
}

private class TestMiddleware(RequestDelegate next)
{
public async ValueTask InvokeAsync(
IRequestContext context,
IBatchDispatcher batchDispatcher)
{
if (context.ContextData.ContainsKey("short-circuit"))
{
context.Result = OperationResultBuilder.New()
.SetData(new Dictionary<string, object?> { ["result"] = true })
.AddExtension("state", "custom middleware short-circuited")
.Build();
return;
}

await next(context);

if (context.Result is not IOperationResult)
{
context.Result = OperationResultBuilder.New()
.SetData(new Dictionary<string, object?> { ["result"] = true })
.AddExtension("state", "default pipeline didn't run")
.Build();
}
}
}
}
Loading