diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.AspNetCore/RequestExecutorWarmupService.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.AspNetCore/RequestExecutorWarmupService.cs index 1866f6efee6..03ee44722ab 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.AspNetCore/RequestExecutorWarmupService.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.AspNetCore/RequestExecutorWarmupService.cs @@ -18,7 +18,7 @@ public async Task StartAsync(CancellationToken cancellationToken) { var setup = optionsMonitor.Get(schemaName); - var requestOptions = FusionRequestExecutorManager.CreateRequestOptions(setup); + var requestOptions = FusionRequestExecutorManager.CreateOptions(setup); if (!requestOptions.LazyInitialization) { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Configuration/FusionGatewaySetup.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Configuration/FusionGatewaySetup.cs index 80028e5e89c..3b7c0fc0fb7 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Configuration/FusionGatewaySetup.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Configuration/FusionGatewaySetup.cs @@ -11,6 +11,8 @@ internal sealed class FusionGatewaySetup { public Func? DocumentProvider { get; set; } + public List> OptionsModifiers { get; } = []; + public List> RequestOptionsModifiers { get; } = []; public List> ParserOptionsModifiers { get; } = []; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/DependencyInjection/CoreFusionGatewayBuilderExtensions.Options.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/DependencyInjection/CoreFusionGatewayBuilderExtensions.Options.cs index f89cef72ba8..2da2c10c40b 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/DependencyInjection/CoreFusionGatewayBuilderExtensions.Options.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/DependencyInjection/CoreFusionGatewayBuilderExtensions.Options.cs @@ -5,6 +5,18 @@ namespace Microsoft.Extensions.DependencyInjection; public static partial class CoreFusionGatewayBuilderExtensions { + public static IFusionGatewayBuilder ModifyOptions( + this IFusionGatewayBuilder builder, + Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); + + return FusionSetupUtilities.Configure( + builder, + options => options.OptionsModifiers.Add(configure)); + } + public static IFusionGatewayBuilder ModifyRequestOptions( this IFusionGatewayBuilder builder, Action configure) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Extensions/FusionRequestContextExtensions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Extensions/FusionRequestContextExtensions.cs index a74afb8e5e3..3af7f62c1d4 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Extensions/FusionRequestContextExtensions.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Extensions/FusionRequestContextExtensions.cs @@ -125,7 +125,7 @@ internal static ErrorHandlingMode ErrorHandlingMode( return errorHandlingMode; } - return requestOptions.DefaultErrorHandlingMode; + return context.Schema.GetOptions().DefaultErrorHandlingMode; } internal static bool AllowErrorHandlingModeOverride( diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Extensions/FusionSchemaDefinitionExtensions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Extensions/FusionSchemaDefinitionExtensions.cs index 0fe41c16756..729f822609d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Extensions/FusionSchemaDefinitionExtensions.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Extensions/FusionSchemaDefinitionExtensions.cs @@ -12,6 +12,13 @@ namespace HotChocolate.Execution; /// public static class FusionSchemaDefinitionExtensions { + public static FusionOptions GetOptions(this ISchemaDefinition schema) + { + ArgumentNullException.ThrowIfNull(schema); + + return schema.Features.GetRequired(); + } + public static FusionRequestOptions GetRequestOptions( this ISchemaDefinition schema) { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionOptions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionOptions.cs new file mode 100644 index 00000000000..831dc473966 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionOptions.cs @@ -0,0 +1,159 @@ +using HotChocolate.Caching.Memory; +using HotChocolate.Execution.Relay; +using HotChocolate.Language; +using HotChocolate.PersistedOperations; + +namespace HotChocolate.Fusion.Execution; + +public sealed class FusionOptions : ICloneable +{ + private bool _isReadOnly; + + /// + /// Gets or sets the time that the executor manager waits to dispose the schema services. + /// 30s by default. + /// + public TimeSpan EvictionTimeout + { + get; + set + { + ExpectMutableOptions(); + + field = value; + } + } = TimeSpan.FromSeconds(30); + + /// + /// Gets or sets the size of the operation execution plan cache. + /// 256 by default. 16 is the minimum. + /// + public int OperationExecutionPlanCacheSize + { + get; + set + { + ExpectMutableOptions(); + + field = value < 16 + ? 16 + : value; + } + } = 256; + + /// + /// Gets or sets the diagnostics for the operation execution plan cache. + /// + public CacheDiagnostics? OperationExecutionPlanCacheDiagnostics + { + get; + set + { + ExpectMutableOptions(); + + field = value; + } + } + + /// + /// Gets or sets the size of the operation document cache. + /// 256 by default. 16 is the minimum. + /// + public int OperationDocumentCacheSize + { + get; + set + { + ExpectMutableOptions(); + + field = value < 16 + ? 16 + : value; + } + } = 256; + + /// + /// Gets or sets the default error handling mode. + /// by default. + /// + public ErrorHandlingMode DefaultErrorHandlingMode + { + get; + set + { + ExpectMutableOptions(); + + field = value; + } + } = ErrorHandlingMode.Propagate; + + /// + /// Gets or sets whether the request executor should be initialized lazily. + /// false by default. + /// + /// + /// When set to false the creation of the schema and request executor, as well as + /// the load of the Fusion configuration, is deferred until the request executor + /// is first requested. + /// This can significantly slow down and block initial requests. + /// Therefore it is recommended to not use this option for production environments. + /// + public bool LazyInitialization + { + get; + set + { + ExpectMutableOptions(); + + field = value; + } + } + + /// + /// Specifies the format for Global Object Identifiers. + /// by default. + /// + public NodeIdSerializerFormat NodeIdSerializerFormat + { + get; + set + { + ExpectMutableOptions(); + + field = value; + } + } = NodeIdSerializerFormat.Base64; + + /// + /// Clones the options into a new mutable instance. + /// + /// + /// A new mutable instance of with the same properties. + /// + public FusionOptions Clone() + { + return new FusionOptions + { + EvictionTimeout = EvictionTimeout, + OperationExecutionPlanCacheSize = OperationExecutionPlanCacheSize, + OperationExecutionPlanCacheDiagnostics = OperationExecutionPlanCacheDiagnostics, + OperationDocumentCacheSize = OperationDocumentCacheSize, + DefaultErrorHandlingMode = DefaultErrorHandlingMode, + LazyInitialization = LazyInitialization, + NodeIdSerializerFormat = NodeIdSerializerFormat + }; + } + + object ICloneable.Clone() => Clone(); + + internal void MakeReadOnly() + => _isReadOnly = true; + + private void ExpectMutableOptions() + { + if (_isReadOnly) + { + throw new InvalidOperationException("The options are read-only."); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs index e92b7964fad..5dea9906024 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs @@ -123,8 +123,7 @@ private async ValueTask EvictExecutorAsync(FusionRequestExecutor executor, Cance private static async Task EvictRequestExecutorAsync(FusionRequestExecutor previousExecutor) { - var evictionTimeout = previousExecutor.Schema.Features - .GetRequired().EvictionTimeout; + var evictionTimeout = previousExecutor.Schema.GetOptions().EvictionTimeout; // we will give the request executor some grace period to finish all requests // in the pipeline. @@ -159,11 +158,17 @@ private FusionRequestExecutor CreateRequestExecutor( { var setup = _optionsMonitor.Get(schemaName); + var options = CreateOptions(setup); var requestOptions = CreateRequestOptions(setup); var parserOptions = CreateParserOptions(setup); var clientConfigurations = CreateClientConfigurations(setup, configuration.Settings.Document); - var features = CreateSchemaFeatures(setup, requestOptions, parserOptions, clientConfigurations); - var schemaServices = CreateSchemaServices(setup, requestOptions); + var features = CreateSchemaFeatures( + setup, + options, + requestOptions, + parserOptions, + clientConfigurations); + var schemaServices = CreateSchemaServices(setup, options, requestOptions); var schema = CreateSchema(schemaName, configuration.Schema, schemaServices, features); var pipeline = CreatePipeline(setup, schema, schemaServices, requestOptions); @@ -201,7 +206,21 @@ private async Task WarmupExecutorAsync(IRequestExecutor executor, CancellationTo return (await documentPromise.Task.ConfigureAwait(false), documentProvider); } - internal static FusionRequestOptions CreateRequestOptions(FusionGatewaySetup setup) + public static FusionOptions CreateOptions(FusionGatewaySetup setup) + { + var options = new FusionOptions(); + + foreach (var configure in setup.OptionsModifiers) + { + configure.Invoke(options); + } + + options.MakeReadOnly(); + + return options; + } + + private static FusionRequestOptions CreateRequestOptions(FusionGatewaySetup setup) { var options = new FusionRequestOptions(); @@ -269,12 +288,14 @@ private SourceSchemaClientConfigurations CreateClientConfigurations( private FeatureCollection CreateSchemaFeatures( FusionGatewaySetup setup, + FusionOptions options, FusionRequestOptions requestOptions, ParserOptions parserOptions, SourceSchemaClientConfigurations clientConfigurations) { var features = new FeatureCollection(); + features.Set(options); features.Set(requestOptions); features.Set(requestOptions.PersistedOperations); features.Set(parserOptions); @@ -304,11 +325,12 @@ private static Dictionary CreateTypeResolverIn private ServiceProvider CreateSchemaServices( FusionGatewaySetup setup, + FusionOptions options, FusionRequestOptions requestOptions) { var schemaServices = new ServiceCollection(); - AddCoreServices(schemaServices, requestOptions); + AddCoreServices(schemaServices, options, requestOptions); AddOperationPlanner(schemaServices); AddParserServices(schemaServices); AddDocumentValidator(setup, schemaServices); @@ -322,7 +344,10 @@ private ServiceProvider CreateSchemaServices( return schemaServices.BuildServiceProvider(); } - private void AddCoreServices(IServiceCollection services, FusionRequestOptions requestOptions) + private void AddCoreServices( + IServiceCollection services, + FusionOptions options, + FusionRequestOptions requestOptions) { services.AddSingleton( new RootServiceProviderAccessor(_applicationServices)); @@ -333,7 +358,7 @@ private void AddCoreServices(IServiceCollection services, FusionRequestOptions r services.AddSingleton(static sp => sp.GetRequiredService().GetRequestOptions()); services.TryAddSingleton( static sp => new DefaultNodeIdParser( - sp.GetRequiredService().NodeIdSerializerFormat)); + sp.GetRequiredService().NodeIdSerializerFormat)); services.AddSingleton(static sp => new DefaultErrorHandler(sp.GetServices())); if (requestOptions.IncludeExceptionDetails) @@ -345,6 +370,7 @@ private void AddCoreServices(IServiceCollection services, FusionRequestOptions r services.AddSingleton(static sp => sp.GetRequiredService().Schema); services.AddSingleton(static sp => sp.GetRequiredService()); + services.AddSingleton(options); services.AddSingleton(requestOptions); services.AddSingleton(requestOptions.PersistedOperations); @@ -366,7 +392,7 @@ private static void AddOperationPlanner(IServiceCollection services) services.AddSingleton( static sp => { - var options = sp.GetRequiredService().GetRequestOptions(); + var options = sp.GetRequiredService().GetOptions(); return new Cache( options.OperationExecutionPlanCacheSize, options.OperationExecutionPlanCacheDiagnostics); @@ -390,7 +416,7 @@ private static void AddParserServices(IServiceCollection services) services.AddSingleton( static sp => { - var options = sp.GetRequiredService().GetRequestOptions(); + var options = sp.GetRequiredService().GetOptions(); return new DefaultDocumentCache(options.OperationDocumentCacheSize); }); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestOptions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestOptions.cs index a8a6c6b92dd..99a23294808 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestOptions.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestOptions.cs @@ -27,69 +27,6 @@ public TimeSpan ExecutionTimeout } } = TimeSpan.FromSeconds(30); - /// - /// Gets or sets the time that the executor manager waits to dispose the schema services. - /// 30s by default. - /// - public TimeSpan EvictionTimeout - { - get; - set - { - ExpectMutableOptions(); - - field = value; - } - } = TimeSpan.FromSeconds(30); - - /// - /// Gets or sets the size of the operation execution plan cache. - /// 256 by default. 16 is the minimum. - /// - public int OperationExecutionPlanCacheSize - { - get; - set - { - ExpectMutableOptions(); - - field = value < 16 - ? 16 - : value; - } - } = 256; - - /// - /// Gets or sets the diagnostics for the operation execution plan cache. - /// - public CacheDiagnostics? OperationExecutionPlanCacheDiagnostics - { - get; - set - { - ExpectMutableOptions(); - - field = value; - } - } - - /// - /// Gets or sets the size of the operation document cache. - /// 256 by default. 16 is the minimum. - /// - public int OperationDocumentCacheSize - { - get; - set - { - ExpectMutableOptions(); - - field = value < 16 - ? 16 - : value; - } - } = 256; - /// /// Gets or sets whether telemetry data like status and duration /// of operation plan nodes should be collected. @@ -107,22 +44,7 @@ public bool CollectOperationPlanTelemetry } /// - /// Gets or sets the default error handling mode. - /// by default. - /// - public ErrorHandlingMode DefaultErrorHandlingMode - { - get; - set - { - ExpectMutableOptions(); - - field = value; - } - } = ErrorHandlingMode.Propagate; - - /// - /// Gets or sets whether the can be overriden + /// Gets or sets whether the can be overriden /// on a per-request basis. /// false by default. /// @@ -173,43 +95,6 @@ public bool IncludeExceptionDetails } } - /// - /// Gets or sets whether the request executor should be initialized lazily. - /// false by default. - /// - /// - /// When set to false the creation of the schema and request executor, as well as - /// the load of the Fusion configuration, is deferred until the request executor - /// is first requested. - /// This can significantly slow down and block initial requests. - /// Therefore it is recommended to not use this option for production environments. - /// - public bool LazyInitialization - { - get; - set - { - ExpectMutableOptions(); - - field = value; - } - } - - /// - /// Specifies the format for Global Object Identifiers. - /// by default. - /// - public NodeIdSerializerFormat NodeIdSerializerFormat - { - get; - set - { - ExpectMutableOptions(); - - field = value; - } - } = NodeIdSerializerFormat.Base64; - /// /// Clones the request options into a new mutable instance. /// @@ -221,17 +106,10 @@ public FusionRequestOptions Clone() return new FusionRequestOptions { ExecutionTimeout = ExecutionTimeout, - EvictionTimeout = EvictionTimeout, - OperationExecutionPlanCacheSize = OperationExecutionPlanCacheSize, - OperationExecutionPlanCacheDiagnostics = OperationExecutionPlanCacheDiagnostics, - OperationDocumentCacheSize = OperationDocumentCacheSize, CollectOperationPlanTelemetry = CollectOperationPlanTelemetry, - DefaultErrorHandlingMode = DefaultErrorHandlingMode, AllowErrorHandlingModeOverride = AllowErrorHandlingModeOverride, PersistedOperations = PersistedOperations, - IncludeExceptionDetails = IncludeExceptionDetails, - LazyInitialization = LazyInitialization, - NodeIdSerializerFormat = NodeIdSerializerFormat + IncludeExceptionDetails = IncludeExceptionDetails }; } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/CancellationTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/CancellationTests.cs index 382203d5263..0938f74eb70 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/CancellationTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/CancellationTests.cs @@ -67,7 +67,7 @@ public async Task Execution_Is_Halted_While_Http_Request_In_Node_Is_Still_Ongoin ("B", server2) ], configureGatewayBuilder: builder => - builder.ModifyRequestOptions(o => o.DefaultErrorHandlingMode = ErrorHandlingMode.Halt)); + builder.ModifyOptions(o => o.DefaultErrorHandlingMode = ErrorHandlingMode.Halt)); using var client = GraphQLHttpClient.Create(gateway.CreateClient()); @@ -107,7 +107,7 @@ public async Task Execution_Is_Halted_While_Subscription_Is_Still_Ongoing() ("A", server1) ], configureGatewayBuilder: builder => - builder.ModifyRequestOptions(o => o.DefaultErrorHandlingMode = ErrorHandlingMode.Halt)); + builder.ModifyOptions(o => o.DefaultErrorHandlingMode = ErrorHandlingMode.Halt)); using var client = GraphQLHttpClient.Create(gateway.CreateClient()); @@ -178,7 +178,7 @@ public async Task Default_ErrorHandlingMode_Can_Be_Changed() ("A", server1) ], configureGatewayBuilder: builder => builder - .ModifyRequestOptions(o => o.DefaultErrorHandlingMode = ErrorHandlingMode.Halt)); + .ModifyOptions(o => o.DefaultErrorHandlingMode = ErrorHandlingMode.Halt)); var request = new OperationRequest( """