From 7effd87d7a274569e8710a860da0d7de35b370b8 Mon Sep 17 00:00:00 2001
From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com>
Date: Thu, 21 May 2026 07:04:06 +0200
Subject: [PATCH 1/3] Add AddHttpResponseFormatter and
AddSocketSessionInterceptor
---
...uilderExtensions.HttpRequestInterceptor.cs | 216 ++++++++++++++++++
.../GatewayBuilderInterceptorTests.cs | 192 ++++++++++++++++
2 files changed, 408 insertions(+)
create mode 100644 src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/GatewayBuilderInterceptorTests.cs
diff --git a/src/HotChocolate/Fusion/src/Fusion.AspNetCore/DependencyInjection/AspNetCoreFusionGatewayBuilderExtensions.HttpRequestInterceptor.cs b/src/HotChocolate/Fusion/src/Fusion.AspNetCore/DependencyInjection/AspNetCoreFusionGatewayBuilderExtensions.HttpRequestInterceptor.cs
index b70beec7249..ccb429bd6f2 100644
--- a/src/HotChocolate/Fusion/src/Fusion.AspNetCore/DependencyInjection/AspNetCoreFusionGatewayBuilderExtensions.HttpRequestInterceptor.cs
+++ b/src/HotChocolate/Fusion/src/Fusion.AspNetCore/DependencyInjection/AspNetCoreFusionGatewayBuilderExtensions.HttpRequestInterceptor.cs
@@ -1,5 +1,11 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
using HotChocolate.AspNetCore;
+using HotChocolate.AspNetCore.Formatters;
+using HotChocolate.AspNetCore.Subscriptions.Protocols;
+using HotChocolate.Execution;
using HotChocolate.Fusion.Configuration;
+using HotChocolate.Transport.Formatters;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.Extensions.DependencyInjection;
@@ -66,4 +72,214 @@ public static IFusionGatewayBuilder AddHttpRequestInterceptor(
s.AddSingleton(factory);
});
}
+
+ ///
+ /// Adds an interceptor for GraphQL socket sessions.
+ ///
+ ///
+ /// The .
+ ///
+ ///
+ /// The implementation.
+ ///
+ ///
+ /// Returns the so that configuration can be chained.
+ ///
+ ///
+ /// The will be activated with the of
+ /// the schema services. If your needs to access application services
+ /// you need to make the services available in the schema services via
+ /// .
+ ///
+ public static IFusionGatewayBuilder AddSocketSessionInterceptor<
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(
+ this IFusionGatewayBuilder builder)
+ where T : class, ISocketSessionInterceptor
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ return builder.ConfigureSchemaServices(
+ (_, s) =>
+ {
+ s.RemoveAll();
+ s.AddSingleton();
+ });
+ }
+
+ ///
+ /// Adds an interceptor for GraphQL socket sessions.
+ ///
+ ///
+ /// The .
+ ///
+ ///
+ /// A factory that creates the interceptor instance.
+ ///
+ ///
+ /// The implementation.
+ ///
+ ///
+ /// Returns the so that configuration can be chained.
+ ///
+ ///
+ /// The passed to the
+ /// is for the schema services. If you need to access application services
+ /// you need to either make the services available in the schema services
+ /// via or use
+ ///
+ /// to access the application services from within the schema service provider.
+ ///
+ public static IFusionGatewayBuilder AddSocketSessionInterceptor(
+ this IFusionGatewayBuilder builder,
+ Func factory)
+ where T : class, ISocketSessionInterceptor
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(factory);
+
+ return builder.ConfigureSchemaServices(
+ (_, s) =>
+ {
+ s.RemoveAll();
+ s.AddSingleton(factory);
+ });
+ }
+
+ ///
+ /// Adds the with specific formatter options.
+ ///
+ ///
+ /// The .
+ ///
+ ///
+ /// Defines whether the underlying
+ /// should pretty print the JSON which includes:
+ /// indenting nested JSON tokens, adding new lines, and adding
+ /// white space between property names and values.
+ /// By default, the JSON is written without extra white spaces.
+ ///
+ ///
+ /// The default incremental delivery format to use when the client does not specify one
+ /// via the Accept header. Defaults to .
+ ///
+ ///
+ /// Returns the so that configuration can be chained.
+ ///
+ public static IFusionGatewayBuilder AddHttpResponseFormatter(
+ this IFusionGatewayBuilder builder,
+ bool indented = false,
+ IncrementalDeliveryFormat incrementalDeliveryFormat = IncrementalDeliveryFormat.Version_0_2)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ return builder.ConfigureSchemaServices(
+ (_, s) =>
+ {
+ s.RemoveAll();
+ s.AddSingleton(
+ sp => DefaultHttpResponseFormatter.Create(
+ new HttpResponseFormatterOptions
+ {
+ Json = new JsonResultFormatterOptions
+ {
+ Indented = indented
+ }
+ },
+ sp.GetRequiredService(),
+ incrementalDeliveryFormat));
+ });
+ }
+
+ ///
+ /// Adds the with specific formatter options.
+ ///
+ ///
+ /// The .
+ ///
+ ///
+ /// The HTTP response formatter options.
+ ///
+ ///
+ /// The default incremental delivery format to use when the client does not specify one
+ /// via the Accept header. Defaults to .
+ ///
+ ///
+ /// Returns the so that configuration can be chained.
+ ///
+ public static IFusionGatewayBuilder AddHttpResponseFormatter(
+ this IFusionGatewayBuilder builder,
+ HttpResponseFormatterOptions options,
+ IncrementalDeliveryFormat incrementalDeliveryFormat = IncrementalDeliveryFormat.Version_0_2)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ return builder.ConfigureSchemaServices(
+ (_, s) =>
+ {
+ s.RemoveAll();
+ s.AddSingleton(
+ sp => DefaultHttpResponseFormatter.Create(
+ options,
+ sp.GetRequiredService(),
+ incrementalDeliveryFormat));
+ });
+ }
+
+ ///
+ /// Adds a custom HTTP response formatter.
+ ///
+ ///
+ /// The .
+ ///
+ ///
+ /// The type of the custom .
+ ///
+ ///
+ /// Returns the so that configuration can be chained.
+ ///
+ public static IFusionGatewayBuilder AddHttpResponseFormatter<
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(
+ this IFusionGatewayBuilder builder)
+ where T : class, IHttpResponseFormatter
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ return builder.ConfigureSchemaServices(
+ (_, s) =>
+ {
+ s.RemoveAll();
+ s.AddSingleton();
+ });
+ }
+
+ ///
+ /// Adds a custom HTTP response formatter.
+ ///
+ ///
+ /// The .
+ ///
+ ///
+ /// The service factory.
+ ///
+ ///
+ /// The type of the custom .
+ ///
+ ///
+ /// Returns the so that configuration can be chained.
+ ///
+ public static IFusionGatewayBuilder AddHttpResponseFormatter(
+ this IFusionGatewayBuilder builder,
+ Func factory)
+ where T : class, IHttpResponseFormatter
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(factory);
+
+ return builder.ConfigureSchemaServices(
+ (_, s) =>
+ {
+ s.RemoveAll();
+ s.AddSingleton(factory);
+ });
+ }
}
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/GatewayBuilderInterceptorTests.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/GatewayBuilderInterceptorTests.cs
new file mode 100644
index 00000000000..2624474aa4c
--- /dev/null
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/GatewayBuilderInterceptorTests.cs
@@ -0,0 +1,192 @@
+using HotChocolate.AspNetCore;
+using HotChocolate.AspNetCore.Formatters;
+using HotChocolate.AspNetCore.Subscriptions.Protocols;
+using HotChocolate.Execution;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace HotChocolate.Fusion;
+
+public class GatewayBuilderInterceptorTests : FusionTestBase
+{
+ private const string SimpleSchema =
+ """
+ type Query {
+ field: String
+ }
+ """;
+
+ [Fact]
+ public async Task AddHttpRequestInterceptor_Generic_Should_Register_Custom_Interceptor()
+ {
+ // arrange
+ using var server = CreateSourceSchema("A", SimpleSchema);
+
+ using var gateway = await CreateCompositeSchemaAsync(
+ [("A", server)],
+ configureGatewayBuilder: b => b.AddHttpRequestInterceptor());
+
+ // act
+ var executor = await gateway.Services.GetRequestExecutorAsync();
+ var interceptor = executor.Schema.Services.GetRequiredService();
+
+ // assert
+ Assert.IsType(interceptor);
+ }
+
+ [Fact]
+ public async Task AddHttpRequestInterceptor_Factory_Should_Register_Custom_Interceptor()
+ {
+ // arrange
+ var factoryInvoked = false;
+ using var server = CreateSourceSchema("A", SimpleSchema);
+
+ using var gateway = await CreateCompositeSchemaAsync(
+ [("A", server)],
+ configureGatewayBuilder: b => b.AddHttpRequestInterceptor(
+ _ =>
+ {
+ factoryInvoked = true;
+ return new CustomHttpRequestInterceptor();
+ }));
+
+ // act
+ var executor = await gateway.Services.GetRequestExecutorAsync();
+ var interceptor = executor.Schema.Services.GetRequiredService();
+
+ // assert
+ Assert.IsType(interceptor);
+ Assert.True(factoryInvoked);
+ }
+
+ [Fact]
+ public async Task AddSocketSessionInterceptor_Generic_Should_Register_Custom_Interceptor()
+ {
+ // arrange
+ using var server = CreateSourceSchema("A", SimpleSchema);
+
+ using var gateway = await CreateCompositeSchemaAsync(
+ [("A", server)],
+ configureGatewayBuilder: b => b.AddSocketSessionInterceptor());
+
+ // act
+ var executor = await gateway.Services.GetRequestExecutorAsync();
+ var interceptor = executor.Schema.Services.GetRequiredService();
+
+ // assert
+ Assert.IsType(interceptor);
+ }
+
+ [Fact]
+ public async Task AddSocketSessionInterceptor_Factory_Should_Register_Custom_Interceptor()
+ {
+ // arrange
+ var factoryInvoked = false;
+ using var server = CreateSourceSchema("A", SimpleSchema);
+
+ using var gateway = await CreateCompositeSchemaAsync(
+ [("A", server)],
+ configureGatewayBuilder: b => b.AddSocketSessionInterceptor(
+ _ =>
+ {
+ factoryInvoked = true;
+ return new CustomSocketSessionInterceptor();
+ }));
+
+ // act
+ var executor = await gateway.Services.GetRequestExecutorAsync();
+ var interceptor = executor.Schema.Services.GetRequiredService();
+
+ // assert
+ Assert.IsType(interceptor);
+ Assert.True(factoryInvoked);
+ }
+
+ [Fact]
+ public async Task AddHttpResponseFormatter_Generic_Should_Register_Custom_Formatter()
+ {
+ // arrange
+ using var server = CreateSourceSchema("A", SimpleSchema);
+
+ using var gateway = await CreateCompositeSchemaAsync(
+ [("A", server)],
+ configureGatewayBuilder: b => b.AddHttpResponseFormatter());
+
+ // act
+ var executor = await gateway.Services.GetRequestExecutorAsync();
+ var formatter = executor.Schema.Services.GetRequiredService();
+
+ // assert
+ Assert.IsType(formatter);
+ }
+
+ [Fact]
+ public async Task AddHttpResponseFormatter_Factory_Should_Register_Custom_Formatter()
+ {
+ // arrange
+ var factoryInvoked = false;
+ using var server = CreateSourceSchema("A", SimpleSchema);
+
+ using var gateway = await CreateCompositeSchemaAsync(
+ [("A", server)],
+ configureGatewayBuilder: b => b.AddHttpResponseFormatter(
+ _ =>
+ {
+ factoryInvoked = true;
+ return new CustomHttpResponseFormatter();
+ }));
+
+ // act
+ var executor = await gateway.Services.GetRequestExecutorAsync();
+ var formatter = executor.Schema.Services.GetRequiredService();
+
+ // assert
+ Assert.IsType(formatter);
+ Assert.True(factoryInvoked);
+ }
+
+ [Fact]
+ public async Task AddHttpResponseFormatter_Indented_Should_Replace_Default_Formatter()
+ {
+ // arrange
+ using var server = CreateSourceSchema("A", SimpleSchema);
+
+ using var gateway = await CreateCompositeSchemaAsync(
+ [("A", server)],
+ configureGatewayBuilder: b => b.AddHttpResponseFormatter(indented: true));
+
+ // act
+ var executor = await gateway.Services.GetRequestExecutorAsync();
+ var formatter = executor.Schema.Services.GetRequiredService();
+
+ // assert
+ Assert.IsAssignableFrom(formatter);
+ }
+
+ [Fact]
+ public async Task AddHttpResponseFormatter_Options_Should_Replace_Default_Formatter()
+ {
+ // arrange
+ using var server = CreateSourceSchema("A", SimpleSchema);
+ var options = new HttpResponseFormatterOptions
+ {
+ HttpTransportVersion = HttpTransportVersion.Latest
+ };
+
+ using var gateway = await CreateCompositeSchemaAsync(
+ [("A", server)],
+ configureGatewayBuilder: b => b.AddHttpResponseFormatter(options));
+
+ // act
+ var executor = await gateway.Services.GetRequestExecutorAsync();
+ var formatter = executor.Schema.Services.GetRequiredService();
+
+ // assert
+ Assert.IsAssignableFrom(formatter);
+ }
+
+ private sealed class CustomHttpRequestInterceptor : DefaultHttpRequestInterceptor;
+
+ private sealed class CustomSocketSessionInterceptor : DefaultSocketSessionInterceptor;
+
+ private sealed class CustomHttpResponseFormatter : DefaultHttpResponseFormatter;
+}
From b81736ddd4698b2493bbb44df60b438d7742f59f Mon Sep 17 00:00:00 2001
From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com>
Date: Thu, 21 May 2026 07:48:42 +0200
Subject: [PATCH 2/3] Update tests
---
.../Fusion.AspNetCore.Tests/FusionTestBase.cs | 2 +
.../GatewayBuilderInterceptorTests.cs | 260 +++++++++++++-----
...otChocolate.Fusion.AspNetCore.Tests.csproj | 1 +
3 files changed, 193 insertions(+), 70 deletions(-)
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/FusionTestBase.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/FusionTestBase.cs
index 5ff6821164b..50f4e821060 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/FusionTestBase.cs
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/FusionTestBase.cs
@@ -278,6 +278,8 @@ protected class Gateway(
{
public HttpClient CreateClient() => testServer.CreateClient();
+ public WebSocketClient CreateWebSocketClient() => testServer.CreateWebSocketClient();
+
public IServiceProvider Services => testServer.Services;
public List SourceSchemas => sourceSchemas;
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/GatewayBuilderInterceptorTests.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/GatewayBuilderInterceptorTests.cs
index 2624474aa4c..76da3e356b3 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/GatewayBuilderInterceptorTests.cs
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/GatewayBuilderInterceptorTests.cs
@@ -1,8 +1,18 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Text.Json;
using HotChocolate.AspNetCore;
using HotChocolate.AspNetCore.Formatters;
-using HotChocolate.AspNetCore.Subscriptions.Protocols;
+using HotChocolate.AspNetCore.Subscriptions;
+using HotChocolate.Collections.Immutable;
using HotChocolate.Execution;
+using HotChocolate.Transport.Http;
+using HotChocolate.Transport.Sockets;
+using HotChocolate.Transport.Sockets.Client;
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
+using OperationResult = HotChocolate.Execution.OperationResult;
+using TransportOperationRequest = HotChocolate.Transport.OperationRequest;
namespace HotChocolate.Fusion;
@@ -15,26 +25,39 @@ type Query {
}
""";
+ private const string ExtensionKey = "captured";
+
[Fact]
- public async Task AddHttpRequestInterceptor_Generic_Should_Register_Custom_Interceptor()
+ public async Task AddHttpRequestInterceptor_Generic_Should_Be_Invoked()
{
// arrange
using var server = CreateSourceSchema("A", SimpleSchema);
using var gateway = await CreateCompositeSchemaAsync(
[("A", server)],
- configureGatewayBuilder: b => b.AddHttpRequestInterceptor());
+ configureGatewayBuilder: b => b
+ .AddHttpRequestInterceptor()
+ .UseRequest(EchoExtensionsMiddleware, key: "EchoExtensions"));
+
+ using var client = GraphQLHttpClient.Create(gateway.CreateClient());
// act
- var executor = await gateway.Services.GetRequestExecutorAsync();
- var interceptor = executor.Schema.Services.GetRequiredService();
+ using var result = await client.PostAsync("{ field }", new Uri("http://localhost:5000/graphql"));
// assert
- Assert.IsType(interceptor);
+ using var response = await result.ReadAsResultAsync();
+ response.MatchInlineSnapshot(
+ """
+ {
+ "extensions": {
+ "captured": "from-http-interceptor"
+ }
+ }
+ """);
}
[Fact]
- public async Task AddHttpRequestInterceptor_Factory_Should_Register_Custom_Interceptor()
+ public async Task AddHttpRequestInterceptor_Factory_Should_Be_Invoked()
{
// arrange
var factoryInvoked = false;
@@ -42,42 +65,54 @@ public async Task AddHttpRequestInterceptor_Factory_Should_Register_Custom_Inter
using var gateway = await CreateCompositeSchemaAsync(
[("A", server)],
- configureGatewayBuilder: b => b.AddHttpRequestInterceptor(
- _ =>
- {
- factoryInvoked = true;
- return new CustomHttpRequestInterceptor();
- }));
+ configureGatewayBuilder: b => b
+ .AddHttpRequestInterceptor(
+ _ =>
+ {
+ factoryInvoked = true;
+ return new ExtensionsHttpRequestInterceptor();
+ })
+ .UseRequest(EchoExtensionsMiddleware, key: "EchoExtensions"));
+
+ using var client = GraphQLHttpClient.Create(gateway.CreateClient());
// act
- var executor = await gateway.Services.GetRequestExecutorAsync();
- var interceptor = executor.Schema.Services.GetRequiredService();
+ using var result = await client.PostAsync("{ field }", new Uri("http://localhost:5000/graphql"));
// assert
- Assert.IsType(interceptor);
+ using var response = await result.ReadAsResultAsync();
+ response.MatchInlineSnapshot(
+ """
+ {
+ "extensions": {
+ "captured": "from-http-interceptor"
+ }
+ }
+ """);
Assert.True(factoryInvoked);
}
[Fact]
- public async Task AddSocketSessionInterceptor_Generic_Should_Register_Custom_Interceptor()
+ public async Task AddSocketSessionInterceptor_Generic_Should_Be_Invoked()
{
// arrange
using var server = CreateSourceSchema("A", SimpleSchema);
using var gateway = await CreateCompositeSchemaAsync(
[("A", server)],
- configureGatewayBuilder: b => b.AddSocketSessionInterceptor());
+ configureGatewayBuilder: b => b
+ .AddSocketSessionInterceptor()
+ .UseRequest(EchoExtensionsMiddleware, key: "EchoExtensions"));
// act
- var executor = await gateway.Services.GetRequestExecutorAsync();
- var interceptor = executor.Schema.Services.GetRequiredService();
+ var extensions = await SubscribeOverWebSocketAsync(gateway);
// assert
- Assert.IsType(interceptor);
+ Assert.Equal("from-socket-interceptor", extensions.GetProperty(ExtensionKey).GetString());
}
[Fact]
- public async Task AddSocketSessionInterceptor_Factory_Should_Register_Custom_Interceptor()
+ public async Task AddSocketSessionInterceptor_Factory_Should_Be_Invoked()
{
// arrange
var factoryInvoked = false;
@@ -85,108 +120,193 @@ public async Task AddSocketSessionInterceptor_Factory_Should_Register_Custom_Int
using var gateway = await CreateCompositeSchemaAsync(
[("A", server)],
- configureGatewayBuilder: b => b.AddSocketSessionInterceptor(
- _ =>
- {
- factoryInvoked = true;
- return new CustomSocketSessionInterceptor();
- }));
+ configureGatewayBuilder: b => b
+ .AddSocketSessionInterceptor(
+ _ =>
+ {
+ factoryInvoked = true;
+ return new ExtensionsSocketSessionInterceptor();
+ })
+ .UseRequest(EchoExtensionsMiddleware, key: "EchoExtensions"));
// act
- var executor = await gateway.Services.GetRequestExecutorAsync();
- var interceptor = executor.Schema.Services.GetRequiredService();
+ var extensions = await SubscribeOverWebSocketAsync(gateway);
// assert
- Assert.IsType(interceptor);
+ Assert.Equal("from-socket-interceptor", extensions.GetProperty(ExtensionKey).GetString());
Assert.True(factoryInvoked);
}
[Fact]
- public async Task AddHttpResponseFormatter_Generic_Should_Register_Custom_Formatter()
+ public async Task AddHttpResponseFormatter_Indented_Should_Indent_Response_Body()
{
// arrange
using var server = CreateSourceSchema("A", SimpleSchema);
using var gateway = await CreateCompositeSchemaAsync(
[("A", server)],
- configureGatewayBuilder: b => b.AddHttpResponseFormatter());
+ configureGatewayBuilder: b => b
+ .AddHttpRequestInterceptor()
+ .AddHttpResponseFormatter(indented: true));
// act
- var executor = await gateway.Services.GetRequestExecutorAsync();
- var formatter = executor.Schema.Services.GetRequiredService();
+ var body = await PostQueryAndReadBodyAsync(gateway, "{ field }");
// assert
- Assert.IsType(formatter);
+ Assert.Equal(
+ """
+ {
+ "data": {
+ "field": "Query"
+ }
+ }
+ """,
+ body);
}
[Fact]
- public async Task AddHttpResponseFormatter_Factory_Should_Register_Custom_Formatter()
+ public async Task AddHttpResponseFormatter_Generic_Should_Use_Custom_Formatter()
{
// arrange
- var factoryInvoked = false;
using var server = CreateSourceSchema("A", SimpleSchema);
using var gateway = await CreateCompositeSchemaAsync(
[("A", server)],
- configureGatewayBuilder: b => b.AddHttpResponseFormatter(
- _ =>
- {
- factoryInvoked = true;
- return new CustomHttpResponseFormatter();
- }));
+ configureGatewayBuilder: b => b.AddHttpResponseFormatter());
+
+ var client = gateway.CreateClient();
// act
- var executor = await gateway.Services.GetRequestExecutorAsync();
- var formatter = executor.Schema.Services.GetRequiredService();
+ var statusCode = await PostQueryAndReadStatusAsync(client, "{ field }");
// assert
- Assert.IsType(formatter);
- Assert.True(factoryInvoked);
+ Assert.Equal((HttpStatusCode)418, statusCode);
}
[Fact]
- public async Task AddHttpResponseFormatter_Indented_Should_Replace_Default_Formatter()
+ public async Task AddHttpResponseFormatter_Factory_Should_Use_Custom_Formatter()
{
// arrange
+ var factoryInvoked = false;
using var server = CreateSourceSchema("A", SimpleSchema);
using var gateway = await CreateCompositeSchemaAsync(
[("A", server)],
- configureGatewayBuilder: b => b.AddHttpResponseFormatter(indented: true));
+ configureGatewayBuilder: b => b.AddHttpResponseFormatter(
+ _ =>
+ {
+ factoryInvoked = true;
+ return new TeapotHttpResponseFormatter();
+ }));
+
+ var client = gateway.CreateClient();
// act
- var executor = await gateway.Services.GetRequestExecutorAsync();
- var formatter = executor.Schema.Services.GetRequiredService();
+ var statusCode = await PostQueryAndReadStatusAsync(client, "{ field }");
// assert
- Assert.IsAssignableFrom(formatter);
+ Assert.Equal((HttpStatusCode)418, statusCode);
+ Assert.True(factoryInvoked);
}
- [Fact]
- public async Task AddHttpResponseFormatter_Options_Should_Replace_Default_Formatter()
+ private static RequestMiddleware EchoExtensionsMiddleware =>
+ (_, _) => context =>
+ {
+ if (context.Request.Extensions is { } extensions
+ && extensions.Document.RootElement.TryGetProperty(ExtensionKey, out var value)
+ && value.GetString() is { } stringValue)
+ {
+ context.Result = new OperationResult(
+ ImmutableOrderedDictionary.Empty.Add(ExtensionKey, stringValue));
+ }
+
+ return default;
+ };
+
+ private static async Task PostQueryAndReadBodyAsync(Gateway gateway, string query)
{
- // arrange
- using var server = CreateSourceSchema("A", SimpleSchema);
- var options = new HttpResponseFormatterOptions
+ using var http = gateway.CreateClient();
+ using var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/graphql")
{
- HttpTransportVersion = HttpTransportVersion.Latest
+ Content = JsonContent.Create(new { query })
};
+ request.Headers.Add("Accept", "application/graphql-response+json");
+ using var response = await http.SendAsync(request);
+ return await response.Content.ReadAsStringAsync();
+ }
- using var gateway = await CreateCompositeSchemaAsync(
- [("A", server)],
- configureGatewayBuilder: b => b.AddHttpResponseFormatter(options));
+ private static async Task PostQueryAndReadStatusAsync(HttpClient client, string query)
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/graphql")
+ {
+ Content = JsonContent.Create(new { query })
+ };
+ request.Headers.Add("Accept", "application/graphql-response+json");
+ using var response = await client.SendAsync(request);
+ return response.StatusCode;
+ }
- // act
- var executor = await gateway.Services.GetRequestExecutorAsync();
- var formatter = executor.Schema.Services.GetRequiredService();
+ private static async Task SubscribeOverWebSocketAsync(Gateway gateway)
+ {
+ var webSocketClient = gateway.CreateWebSocketClient();
+ webSocketClient.ConfigureRequest = r => r.Headers.SecWebSocketProtocol = WellKnownProtocols.GraphQL_Transport_WS;
- // assert
- Assert.IsAssignableFrom(formatter);
+ using var webSocket = await webSocketClient.ConnectAsync(
+ new Uri("ws://localhost:5000/graphql"),
+ CancellationToken.None);
+
+ await using var client = await SocketClient.ConnectAsync(webSocket);
+ using var result = await client.ExecuteAsync(new TransportOperationRequest("{ field }"));
+
+ await foreach (var operationResult in result.ReadResultsAsync())
+ {
+ using (operationResult)
+ {
+ return operationResult.Extensions.Clone();
+ }
+ }
+
+ throw new InvalidOperationException("No result received over the WebSocket.");
}
- private sealed class CustomHttpRequestInterceptor : DefaultHttpRequestInterceptor;
+ private sealed class ExtensionsHttpRequestInterceptor : DefaultHttpRequestInterceptor
+ {
+ public override async ValueTask OnCreateAsync(
+ HttpContext context,
+ IRequestExecutor requestExecutor,
+ OperationRequestBuilder requestBuilder,
+ CancellationToken cancellationToken)
+ {
+ await base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken);
- private sealed class CustomSocketSessionInterceptor : DefaultSocketSessionInterceptor;
+ requestBuilder.SetExtensions(
+ JsonSerializer.SerializeToDocument(
+ new Dictionary { [ExtensionKey] = "from-http-interceptor" }));
+ }
+ }
- private sealed class CustomHttpResponseFormatter : DefaultHttpResponseFormatter;
+ private sealed class ExtensionsSocketSessionInterceptor : DefaultSocketSessionInterceptor
+ {
+ public override ValueTask OnRequestAsync(
+ ISocketSession session,
+ string operationSessionId,
+ OperationRequestBuilder requestBuilder,
+ CancellationToken cancellationToken = default)
+ {
+ requestBuilder.SetExtensions(
+ JsonSerializer.SerializeToDocument(
+ new Dictionary { [ExtensionKey] = "from-socket-interceptor" }));
+
+ return default;
+ }
+ }
+
+ private sealed class TeapotHttpResponseFormatter : DefaultHttpResponseFormatter
+ {
+ protected override HttpStatusCode OnDetermineStatusCode(
+ OperationResult result,
+ FormatInfo format,
+ HttpStatusCode? proposedStatusCode)
+ => (HttpStatusCode)418;
+ }
}
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj
index e78a96ef5cc..73dec6154f1 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj
@@ -11,6 +11,7 @@
+
From bf496c654a26462c280d9bcdf30b42b1cc09515a Mon Sep 17 00:00:00 2001
From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com>
Date: Thu, 21 May 2026 08:00:45 +0200
Subject: [PATCH 3/3] Cleanup
---
...NetCoreServiceCollectionExtensions.Http.cs | 13 +++++++++
...uilderExtensions.HttpRequestInterceptor.cs | 14 +++++++++
.../GatewayBuilderInterceptorTests.cs | 29 ++++++-------------
3 files changed, 36 insertions(+), 20 deletions(-)
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Http.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Http.cs
index 2d4c9ef2115..e56141b7eb2 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Http.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.Http.cs
@@ -193,6 +193,11 @@ public static IRequestExecutorBuilder AddHttpResponseFormatter(
///
/// Returns the so that configuration can be chained.
///
+ ///
+ /// The will be activated with the of the schema services.
+ /// If your needs to access application services you need to
+ /// make the services available in the schema services via .
+ ///
public static IRequestExecutorBuilder AddHttpResponseFormatter<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(
this IRequestExecutorBuilder builder)
@@ -221,6 +226,14 @@ public static IRequestExecutorBuilder AddHttpResponseFormatter<
///
/// Returns the so that configuration can be chained.
///
+ ///
+ /// The passed to the
+ /// is for the schema services. If you need to access application services
+ /// you need to either make the services available in the schema services
+ /// via or use
+ ///
+ /// to access the application services from within the schema service provider.
+ ///
public static IRequestExecutorBuilder AddHttpResponseFormatter(
this IRequestExecutorBuilder builder,
Func factory)
diff --git a/src/HotChocolate/Fusion/src/Fusion.AspNetCore/DependencyInjection/AspNetCoreFusionGatewayBuilderExtensions.HttpRequestInterceptor.cs b/src/HotChocolate/Fusion/src/Fusion.AspNetCore/DependencyInjection/AspNetCoreFusionGatewayBuilderExtensions.HttpRequestInterceptor.cs
index ccb429bd6f2..216220dfedf 100644
--- a/src/HotChocolate/Fusion/src/Fusion.AspNetCore/DependencyInjection/AspNetCoreFusionGatewayBuilderExtensions.HttpRequestInterceptor.cs
+++ b/src/HotChocolate/Fusion/src/Fusion.AspNetCore/DependencyInjection/AspNetCoreFusionGatewayBuilderExtensions.HttpRequestInterceptor.cs
@@ -237,6 +237,12 @@ public static IFusionGatewayBuilder AddHttpResponseFormatter(
///
/// Returns the so that configuration can be chained.
///
+ ///
+ /// The will be activated with the of
+ /// the schema services. If your needs to access application services
+ /// you need to make the services available in the schema services via
+ /// .
+ ///
public static IFusionGatewayBuilder AddHttpResponseFormatter<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(
this IFusionGatewayBuilder builder)
@@ -267,6 +273,14 @@ public static IFusionGatewayBuilder AddHttpResponseFormatter<
///
/// Returns the so that configuration can be chained.
///
+ ///
+ /// The passed to the
+ /// is for the schema services. If you need to access application services
+ /// you need to either make the services available in the schema services
+ /// via or use
+ ///
+ /// to access the application services from within the schema service provider.
+ ///
public static IFusionGatewayBuilder AddHttpResponseFormatter(
this IFusionGatewayBuilder builder,
Func factory)
diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/GatewayBuilderInterceptorTests.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/GatewayBuilderInterceptorTests.cs
index 76da3e356b3..c2edfa359ad 100644
--- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/GatewayBuilderInterceptorTests.cs
+++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/GatewayBuilderInterceptorTests.cs
@@ -46,14 +46,7 @@ public async Task AddHttpRequestInterceptor_Generic_Should_Be_Invoked()
// assert
using var response = await result.ReadAsResultAsync();
- response.MatchInlineSnapshot(
- """
- {
- "extensions": {
- "captured": "from-http-interceptor"
- }
- }
- """);
+ Assert.Equal("from-http-interceptor", response.Extensions.GetProperty(ExtensionKey).GetString());
}
[Fact]
@@ -81,14 +74,7 @@ public async Task AddHttpRequestInterceptor_Factory_Should_Be_Invoked()
// assert
using var response = await result.ReadAsResultAsync();
- response.MatchInlineSnapshot(
- """
- {
- "extensions": {
- "captured": "from-http-interceptor"
- }
- }
- """);
+ Assert.Equal("from-http-interceptor", response.Extensions.GetProperty(ExtensionKey).GetString());
Assert.True(factoryInvoked);
}
@@ -248,17 +234,20 @@ private static async Task PostQueryAndReadStatusAsync(HttpClient
private static async Task SubscribeOverWebSocketAsync(Gateway gateway)
{
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+ var ct = cts.Token;
+
var webSocketClient = gateway.CreateWebSocketClient();
webSocketClient.ConfigureRequest = r => r.Headers.SecWebSocketProtocol = WellKnownProtocols.GraphQL_Transport_WS;
using var webSocket = await webSocketClient.ConnectAsync(
new Uri("ws://localhost:5000/graphql"),
- CancellationToken.None);
+ ct);
- await using var client = await SocketClient.ConnectAsync(webSocket);
- using var result = await client.ExecuteAsync(new TransportOperationRequest("{ field }"));
+ await using var client = await SocketClient.ConnectAsync(webSocket, ct);
+ using var result = await client.ExecuteAsync(new TransportOperationRequest("{ field }"), ct);
- await foreach (var operationResult in result.ReadResultsAsync())
+ await foreach (var operationResult in result.ReadResultsAsync().WithCancellation(ct))
{
using (operationResult)
{