Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using HotChocolate.AspNetCore.Parsers;
using HotChocolate.AspNetCore.Warmup;
using HotChocolate.Execution.Configuration;
using HotChocolate.Execution.Internal;
using HotChocolate.Internal;
using HotChocolate.Language;
using HotChocolate.Utilities;
Expand Down Expand Up @@ -165,6 +166,10 @@ private static IRequestExecutorBuilder AddGraphQLServerCore(
IPostConfigureOptions<GraphQLServerOptions>,
SourceSchemaServerOptionsPostConfigure>());

builder.Services.TryAddSingleton<
ITransportCapabilitiesProvider,
TransportCapabilitiesProvider>();

if (!builder.Services.IsImplementationTypeRegistered<HttpContextParameterExpressionBuilder>())
{
builder.Services.AddSingleton<IParameterExpressionBuilder, HttpContextParameterExpressionBuilder>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using HotChocolate.Execution.Internal;
using Microsoft.Extensions.Options;

namespace HotChocolate.AspNetCore;

/// <summary>
/// Derives the transport capabilities declared in an exported schema settings file
/// from the <see cref="GraphQLServerOptions"/> of the schema.
/// </summary>
internal sealed class TransportCapabilitiesProvider(
IOptionsMonitor<GraphQLServerOptions> serverOptions)
: ITransportCapabilitiesProvider
{
public TransportCapabilities GetCapabilities(string schemaName)
{
var batching = serverOptions.Get(schemaName).Batching;

return new TransportCapabilities(
VariableBatching: batching.HasFlag(AllowedBatching.VariableBatching),
RequestBatching: batching.HasFlag(AllowedBatching.RequestBatching));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using HotChocolate.Execution.Internal;
using Microsoft.Extensions.DependencyInjection;

namespace HotChocolate.AspNetCore;

public class TransportCapabilitiesProviderTests
{
[Fact]
public void GetCapabilities_Should_DeclareNoBatching_When_ServerOptionsAllowNoBatching()
{
// arrange
var services = new ServiceCollection();
services.AddGraphQLServer("a");
var provider = GetProvider(services);

// act
var capabilities = provider.GetCapabilities("a");

// assert
Assert.Equal(
new TransportCapabilities(VariableBatching: false, RequestBatching: false),
capabilities);
}

[Fact]
public void GetCapabilities_Should_DeclareBothBatchingModes_When_SourceSchemaDefaultsApplied()
{
// arrange
var services = new ServiceCollection();
services.AddGraphQLServer("a").AddSourceSchemaDefaults();
var provider = GetProvider(services);

// act
var capabilities = provider.GetCapabilities("a");

// assert
Assert.Equal(
new TransportCapabilities(VariableBatching: true, RequestBatching: true),
capabilities);
}

[Fact]
public void GetCapabilities_Should_MapEachFlagSeparately_When_OnlyVariableBatchingIsAllowed()
{
// arrange
var services = new ServiceCollection();
services
.AddGraphQLServer("a")
.ModifyServerOptions(o => o.Batching = AllowedBatching.VariableBatching);
var provider = GetProvider(services);

// act
var capabilities = provider.GetCapabilities("a");

// assert
Assert.Equal(
new TransportCapabilities(VariableBatching: true, RequestBatching: false),
capabilities);
}

[Fact]
public void GetCapabilities_Should_ReadNamedSchemaOptions_When_MultipleSchemasAreRegistered()
{
// arrange
var services = new ServiceCollection();
services.AddGraphQLServer("a").AddSourceSchemaDefaults();
services.AddGraphQLServer("b");
var provider = GetProvider(services);

// act
var capabilities = provider.GetCapabilities("b");

// assert
Assert.Equal(
new TransportCapabilities(VariableBatching: false, RequestBatching: false),
capabilities);
}

private static ITransportCapabilitiesProvider GetProvider(IServiceCollection services)
=> services.BuildServiceProvider().GetRequiredService<ITransportCapabilitiesProvider>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace HotChocolate.Execution.Internal;

/// <summary>
/// Supplies the transport capabilities that <see cref="SchemaFileExporter"/> declares
/// in the settings file it creates for a schema.
/// </summary>
internal interface ITransportCapabilitiesProvider
{
/// <summary>
/// Gets the transport capabilities of the schema with the given name.
/// </summary>
/// <param name="schemaName">
/// The name of the schema.
/// </param>
/// <returns>
/// The transport capabilities to declare for the schema.
/// </returns>
TransportCapabilities GetCapabilities(string schemaName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,30 @@ await File.WriteAllTextAsync(
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true),
cancellationToken);

await WriteSettingsFile(settingsFileName, executor.Schema.Name, cancellationToken);
var capabilities = executor.Schema
.GetRootServiceProvider()
.GetService<ITransportCapabilitiesProvider>()
?.GetCapabilities(executor.Schema.Name)
?? new TransportCapabilities(VariableBatching: true, RequestBatching: true);

await WriteSettingsFile(
settingsFileName,
executor.Schema.Name,
capabilities,
cancellationToken);

return new SchemaFileInfo(schemaFileName, settingsFileName);
}

private static async Task WriteSettingsFile(
string fileName,
string schemaName,
TransportCapabilities capabilities,
CancellationToken cancellationToken)
{
if (!await TryUpdateSettingsFile(fileName, schemaName, cancellationToken))
{
await CreateNewSettingsFile(fileName, schemaName, cancellationToken);
await CreateNewSettingsFile(fileName, schemaName, capabilities, cancellationToken);
}
Comment thread
glen-84 marked this conversation as resolved.
}

Expand Down Expand Up @@ -109,6 +120,7 @@ private static async Task<bool> TryUpdateSettingsFile(
private static async Task CreateNewSettingsFile(
string fileName,
string schemaName,
TransportCapabilities capabilities,
CancellationToken cancellationToken)
{
await using var settingsFileStream = File.Create(fileName);
Expand All @@ -124,13 +136,13 @@ private static async Task CreateNewSettingsFile(

jsonWriter.WriteString("url", "http://localhost:5000/graphql");

// A Hot Chocolate source schema knows which transport extensions it implements, so the
// exported template declares them instead of leaving the gateway on the defaults.
// The exported template declares the transport extensions the server accepts
// instead of leaving the gateway on the defaults.
jsonWriter.WriteStartObject("capabilities");

jsonWriter.WriteStartObject("batching");
jsonWriter.WriteBoolean("variableBatching", true);
jsonWriter.WriteBoolean("requestBatching", true);
jsonWriter.WriteBoolean("variableBatching", capabilities.VariableBatching);
jsonWriter.WriteBoolean("requestBatching", capabilities.RequestBatching);
jsonWriter.WriteBoolean("aliasBatching", true);
jsonWriter.WriteEndObject();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace HotChocolate.Execution.Internal;

/// <summary>
/// The transport capabilities that <see cref="SchemaFileExporter"/> declares
/// in the settings file it creates for a schema.
/// </summary>
/// <param name="VariableBatching">
/// Whether the server accepts variable batching requests.
/// </param>
/// <param name="RequestBatching">
/// Whether the server accepts request batching requests.
/// </param>
internal readonly record struct TransportCapabilities(
bool VariableBatching,
bool RequestBatching);
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using HotChocolate.Types;
using Microsoft.Extensions.DependencyInjection;

namespace HotChocolate.Execution.Internal;

public class SchemaFileExporterTests : IDisposable
{
private readonly string _directory = System.IO.Path.Combine(
System.IO.Path.GetTempPath(),
System.IO.Path.GetRandomFileName());

[Fact]
public async Task Export_Should_DeclareBothBatchingModes_When_NoProviderIsRegistered()
{
// arrange
var services = new ServiceCollection();
services
.AddGraphQL()
.AddQueryType(d => d.Name("Query").Field("foo").Resolve("bar"));
var executor = await GetExecutorAsync(services);
Directory.CreateDirectory(_directory);

// act
var result = await SchemaFileExporter.Export(
System.IO.Path.Combine(_directory, "schema.graphqls"),
executor,
rewriteToSemanticNonNull: false,
TestContext.Current.CancellationToken);

// assert
var settings = await File.ReadAllTextAsync(
result.SettingsFileName,
TestContext.Current.CancellationToken);
settings.ReplaceLineEndings("\n").MatchInlineSnapshot(
"""
{
"name": "_Default",
"transports": {
"http": {
"url": "http://localhost:5000/graphql",
"capabilities": {
"batching": {
"variableBatching": true,
"requestBatching": true,
"aliasBatching": true
},
"onError": "propagate"
}
}
}
}
""" + "\n");
}

[Fact]
public async Task Export_Should_DeclareProviderCapabilities_When_ProviderIsRegistered()
{
// arrange
var services = new ServiceCollection();
services.AddSingleton<ITransportCapabilitiesProvider>(
new FixedCapabilitiesProvider(
new TransportCapabilities(VariableBatching: false, RequestBatching: true)));
services
.AddGraphQL()
.AddQueryType(d => d.Name("Query").Field("foo").Resolve("bar"));
var executor = await GetExecutorAsync(services);
Directory.CreateDirectory(_directory);

// act
var result = await SchemaFileExporter.Export(
System.IO.Path.Combine(_directory, "schema.graphqls"),
executor,
rewriteToSemanticNonNull: false,
TestContext.Current.CancellationToken);

// assert
var settings = await File.ReadAllTextAsync(
result.SettingsFileName,
TestContext.Current.CancellationToken);
settings.ReplaceLineEndings("\n").MatchInlineSnapshot(
"""
{
"name": "_Default",
"transports": {
"http": {
"url": "http://localhost:5000/graphql",
"capabilities": {
"batching": {
"variableBatching": false,
"requestBatching": true,
"aliasBatching": true
},
"onError": "propagate"
}
}
}
}
""" + "\n");
}

public void Dispose()
{
if (Directory.Exists(_directory))
{
Directory.Delete(_directory, recursive: true);
}
}

private static async Task<IRequestExecutor> GetExecutorAsync(IServiceCollection services)
=> await services
.BuildServiceProvider()
.GetRequiredService<IRequestExecutorProvider>()
.GetExecutorAsync(cancellationToken: TestContext.Current.CancellationToken);

private sealed class FixedCapabilitiesProvider(TransportCapabilities capabilities)
: ITransportCapabilitiesProvider
{
public TransportCapabilities GetCapabilities(string schemaName) => capabilities;
}
}
2 changes: 1 addition & 1 deletion website/content/docs/fusion/batching.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ The connector kind only sets the starting value, so partial settings mix with it
- `{ "variableBatching": false, "requestBatching": false }` on an Apollo Federation subgraph still leaves alias batching on. Declare `"aliasBatching": false` as well to turn batching off completely.

> [!NOTE]
> The settings template a Hot Chocolate subgraph exports declares all three flags explicitly, so a gateway that uses it does not fall back to the defaults.
> The settings template a Hot Chocolate subgraph exports declares all three flags explicitly, so a gateway that uses it does not fall back to the defaults. `variableBatching` and `requestBatching` mirror the batching the subgraph's server options allow, which `AddSourceSchemaDefaults()` turns on, and `aliasBatching` is always `true`.

# Apollo Federation Subgraphs

Expand Down