diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/RequestExecutorWarmupService.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/RequestExecutorWarmupService.cs index f3b14fb503c..4b738dd9292 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/RequestExecutorWarmupService.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/RequestExecutorWarmupService.cs @@ -1,10 +1,11 @@ using HotChocolate.Execution.Configuration; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; namespace HotChocolate.AspNetCore.Warmup; internal sealed class RequestExecutorWarmupService( - IRequestExecutorOptionsMonitor executorOptionsMonitor, + IOptionsMonitor optionsMonitor, IRequestExecutorProvider provider) : IHostedService { public async Task StartAsync(CancellationToken cancellationToken) @@ -13,8 +14,8 @@ public async Task StartAsync(CancellationToken cancellationToken) foreach (var schemaName in provider.SchemaNames) { - var setup = await executorOptionsMonitor.GetAsync(schemaName, cancellationToken); - var options = CreateSchemaOptions(setup); + var setup = optionsMonitor.Get(schemaName); + var options = setup.CreateSchemaOptions(); if (!options.LazyInitialization) { @@ -32,16 +33,4 @@ private async Task WarmupAsync(string schemaName, CancellationToken cancellation { await provider.GetExecutorAsync(schemaName, cancellationToken).ConfigureAwait(false); } - - private static SchemaOptions CreateSchemaOptions(RequestExecutorSetup setup) - { - var options = new SchemaOptions(); - - foreach (var configure in setup.SchemaOptionModifiers) - { - configure(options); - } - - return options; - } } diff --git a/src/HotChocolate/Core/src/Execution/Caching/PreparedOperationCacheOptions.cs b/src/HotChocolate/Core/src/Execution/Caching/PreparedOperationCacheOptions.cs deleted file mode 100644 index 09d271a7f73..00000000000 --- a/src/HotChocolate/Core/src/Execution/Caching/PreparedOperationCacheOptions.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace HotChocolate.Execution.Caching; - -internal sealed class PreparedOperationCacheOptions -{ - public int Capacity { get; set; } = 256; -} diff --git a/src/HotChocolate/Core/src/Execution/Configuration/DefaultRequestExecutorOptionsMonitor.cs b/src/HotChocolate/Core/src/Execution/Configuration/DefaultRequestExecutorOptionsMonitor.cs deleted file mode 100644 index 0ea1b6f99eb..00000000000 --- a/src/HotChocolate/Core/src/Execution/Configuration/DefaultRequestExecutorOptionsMonitor.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System.Collections.Immutable; -using Microsoft.Extensions.Options; - -namespace HotChocolate.Execution.Configuration; - -internal sealed class DefaultRequestExecutorOptionsMonitor( - IOptionsMonitor optionsMonitor, - IEnumerable optionsProviders) - : IRequestExecutorOptionsMonitor - , IDisposable -{ - private readonly SemaphoreSlim _semaphore = new(1, 1); - private readonly IRequestExecutorOptionsProvider[] _optionsProviders = optionsProviders.ToArray(); - private readonly Dictionary> _configs = []; - private readonly List _disposables = []; - private readonly List> _listeners = []; - private bool _initialized; - private bool _disposed; - - public async ValueTask GetAsync( - string schemaName, - CancellationToken cancellationToken = default) - { - await TryInitializeAsync(cancellationToken).ConfigureAwait(false); - - var options = new RequestExecutorSetup(); - optionsMonitor.Get(schemaName).CopyTo(options); - - if (_configs.TryGetValue(schemaName, out var configurations)) - { - foreach (var configuration in configurations) - { - configuration.Configure(options); - } - } - - return options; - } - - public async ValueTask> GetSchemaNamesAsync( - CancellationToken cancellationToken) - { - await TryInitializeAsync(cancellationToken).ConfigureAwait(false); - return [.. _configs.Keys.Order()]; - } - - private async ValueTask TryInitializeAsync(CancellationToken cancellationToken) - { - if (!_initialized) - { - await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - await TryInitializeUnsafeAsync(cancellationToken).ConfigureAwait(false); - } - finally - { - _semaphore.Release(); - } - } - } - - private async ValueTask TryInitializeUnsafeAsync(CancellationToken cancellationToken) - { - if (!_initialized) - { - _configs.Clear(); - - foreach (var provider in _optionsProviders) - { - _disposables.Add(provider.OnChange(OnChange)); - - var allConfigurations = - await provider.GetOptionsAsync(cancellationToken) - .ConfigureAwait(false); - - foreach (var configuration in allConfigurations) - { - if (!_configs.TryGetValue( - configuration.SchemaName, - out var configurations)) - { - configurations = []; - _configs.Add(configuration.SchemaName, configurations); - } - - configurations.Add(configuration); - } - } - - _initialized = true; - } - } - - public IDisposable OnChange(Action listener) - => new Session(this, listener); - - private void OnChange(IConfigureRequestExecutorSetup changes) - { - _initialized = false; - - lock (_listeners) - { - foreach (var listener in _listeners) - { - listener.Invoke(changes.SchemaName); - } - } - } - - public void Dispose() - { - if (!_disposed) - { - _semaphore.Dispose(); - - foreach (var disposable in _disposables) - { - disposable.Dispose(); - } - - _disposed = true; - } - } - - private sealed class Session : IDisposable - { - private readonly DefaultRequestExecutorOptionsMonitor _monitor; - private readonly Action _listener; - - public Session( - DefaultRequestExecutorOptionsMonitor monitor, - Action listener) - { - lock (monitor._listeners) - { - _monitor = monitor; - _listener = listener; - monitor._listeners.Add(listener); - } - } - - public void Dispose() - { - lock (_monitor._listeners) - { - _monitor._listeners.Remove(_listener); - } - } - } -} diff --git a/src/HotChocolate/Core/src/Execution/Configuration/IRequestExecutorOptionsMonitor.cs b/src/HotChocolate/Core/src/Execution/Configuration/IRequestExecutorOptionsMonitor.cs deleted file mode 100644 index ea414f70480..00000000000 --- a/src/HotChocolate/Core/src/Execution/Configuration/IRequestExecutorOptionsMonitor.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace HotChocolate.Execution.Configuration; - -/// -/// Used for notifications when instances change. -/// -public interface IRequestExecutorOptionsMonitor -{ - /// - /// Returns a configured - /// instance with the given name. - /// - ValueTask GetAsync( - string schemaName, - CancellationToken cancellationToken); - - /// - /// Registers a listener to be called whenever a named - /// changes. - /// - /// - /// The action to be invoked when has changed. - /// - /// - /// An which should be disposed to stop listening for changes. - /// - IDisposable OnChange(Action listener); -} diff --git a/src/HotChocolate/Core/src/Execution/Configuration/IRequestExecutorOptionsProvider.cs b/src/HotChocolate/Core/src/Execution/Configuration/IRequestExecutorOptionsProvider.cs deleted file mode 100644 index 1fcbc3b909c..00000000000 --- a/src/HotChocolate/Core/src/Execution/Configuration/IRequestExecutorOptionsProvider.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace HotChocolate.Execution.Configuration; - -/// -/// Provides dynamic configurations. -/// -public interface IRequestExecutorOptionsProvider -{ - /// - /// Gets named configuration options. - /// - /// - /// The . - /// - /// - /// Returns the configuration options of this provider. - /// - ValueTask> GetOptionsAsync( - CancellationToken cancellationToken); - - /// - /// Registers a listener to be called whenever a named - /// changes. - /// - /// - /// The action to be invoked when has changed. - /// - /// - /// An which should be disposed to stop listening for changes. - /// - IDisposable OnChange(Action listener); -} diff --git a/src/HotChocolate/Core/src/Execution/Configuration/RequestExecutorSetup.cs b/src/HotChocolate/Core/src/Execution/Configuration/RequestExecutorSetup.cs index 708d7052a9c..49f16e66fb8 100644 --- a/src/HotChocolate/Core/src/Execution/Configuration/RequestExecutorSetup.cs +++ b/src/HotChocolate/Core/src/Execution/Configuration/RequestExecutorSetup.cs @@ -134,4 +134,16 @@ public void CopyTo(RequestExecutorSetup options) options.DefaultPipelineFactory = DefaultPipelineFactory; } } + + internal SchemaOptions CreateSchemaOptions() + { + var options = new SchemaOptions(); + + foreach (var configure in SchemaOptionModifiers) + { + configure(options); + } + + return options; + } } diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs index 967b8e660e7..e17ec6ba541 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs @@ -21,16 +21,6 @@ namespace Microsoft.Extensions.DependencyInjection; internal static class InternalServiceCollectionExtensions { - internal static IServiceCollection TryAddRequestExecutorFactoryOptionsMonitor( - this IServiceCollection services) - { - services.TryAddSingleton( - sp => new DefaultRequestExecutorOptionsMonitor( - sp.GetRequiredService>(), - sp.GetServices())); - return services; - } - internal static IServiceCollection TryAddVariableCoercion( this IServiceCollection services) { @@ -159,16 +149,6 @@ internal static IServiceCollection TryAddRequestExecutorResolver( return services; } - internal static IServiceCollection TryAddDefaultCaches( - this IServiceCollection services) - { - services.TryAddSingleton(_ => new PreparedOperationCacheOptions { Capacity = 256 }); - services.TryAddSingleton( - sp => new DefaultDocumentCache( - sp.GetRequiredService().Capacity)); - return services; - } - internal static IServiceCollection TryAddDefaultDocumentHashProvider( this IServiceCollection services) { diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Caches.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Caches.cs new file mode 100644 index 00000000000..38b344f308f --- /dev/null +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Caches.cs @@ -0,0 +1,33 @@ +using HotChocolate; +using HotChocolate.Execution.Caching; +using HotChocolate.Execution.Configuration; +using HotChocolate.Language; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection; + +public static partial class RequestExecutorBuilderExtensions +{ + internal static IRequestExecutorBuilder AddDocumentCache(this IRequestExecutorBuilder builder) + { + builder.Services.TryAddKeyedSingleton( + builder.Name, + static (sp, schemaName) => + { + var optionsMonitor = sp.GetRequiredService>(); + var setup = optionsMonitor.Get((string)schemaName!); + var options = setup.CreateSchemaOptions(); + + return new DefaultDocumentCache(options.OperationDocumentCacheSize); + }); + + return builder.ConfigureSchemaServices( + static (applicationServices, s) => + s.AddSingleton(schemaServices => + { + var schemaName = schemaServices.GetRequiredService().Name; + return applicationServices.GetRequiredKeyedService(schemaName); + })); + } +} diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs index 2dfb0f12dfa..dfed0690cf4 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs @@ -46,11 +46,9 @@ public static IServiceCollection AddGraphQLCore(this IServiceCollection services // core services services - .TryAddRequestExecutorFactoryOptionsMonitor() .TryAddTypeConverter() .TryAddInputFormatter() .TryAddInputParser() - .TryAddDefaultCaches() .TryAddDefaultDocumentHashProvider() .TryAddDefaultBatchDispatcher() .TryAddDefaultDataLoaderRegistry() @@ -156,6 +154,8 @@ private static DefaultRequestExecutorBuilder CreateBuilder( builder.TryAddTypeInterceptor(); builder.TryAddTypeInterceptor(); + builder.AddDocumentCache(); + if (!services.Any(t => t.ServiceType == typeof(SchemaName) && t.ImplementationInstance is SchemaName s @@ -169,25 +169,6 @@ private static DefaultRequestExecutorBuilder CreateBuilder( return builder; } - public static IServiceCollection AddDocumentCache( - this IServiceCollection services, - int capacity = 256) - { - services.RemoveAll(); - services.AddSingleton( - _ => new DefaultDocumentCache(capacity)); - return services; - } - - public static IServiceCollection AddOperationCache( - this IServiceCollection services, - int capacity = 256) - { - services.RemoveAll(); - services.AddSingleton(_ => new PreparedOperationCacheOptions { Capacity = capacity }); - return services; - } - public static IServiceCollection AddMD5DocumentHashProvider( this IServiceCollection services, HashFormat format = HashFormat.Base64) diff --git a/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj b/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj index c0fcc306227..20b80b28ea4 100644 --- a/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj +++ b/src/HotChocolate/Core/src/Execution/HotChocolate.Execution.csproj @@ -229,6 +229,9 @@ RequestExecutorManager.cs + + RequestExecutorBuilderExtensions.cs + diff --git a/src/HotChocolate/Core/src/Execution/RequestExecutorManager.cs b/src/HotChocolate/Core/src/Execution/RequestExecutorManager.cs index 60a7759750c..73c84856eaf 100644 --- a/src/HotChocolate/Core/src/Execution/RequestExecutorManager.cs +++ b/src/HotChocolate/Core/src/Execution/RequestExecutorManager.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; using static HotChocolate.Execution.ThrowHelper; namespace HotChocolate.Execution; @@ -30,7 +31,7 @@ internal sealed partial class RequestExecutorManager private readonly CancellationTokenSource _cts = new(); private readonly ConcurrentDictionary _semaphoreBySchema = new(); private readonly ConcurrentDictionary _executors = new(); - private readonly IRequestExecutorOptionsMonitor _optionsMonitor; + private readonly IOptionsMonitor _optionsMonitor; private readonly IServiceProvider _applicationServices; private readonly EventObservable _events = new(); private readonly ChannelWriter _executorEvictionChannelWriter; @@ -38,7 +39,7 @@ internal sealed partial class RequestExecutorManager private bool _disposed; public RequestExecutorManager( - IRequestExecutorOptionsMonitor optionsMonitor, + IOptionsMonitor optionsMonitor, IServiceProvider serviceProvider) { ArgumentNullException.ThrowIfNull(optionsMonitor); @@ -52,7 +53,6 @@ public RequestExecutorManager( ConsumeExecutorEvictionsAsync(executorEvictionChannel.Reader, _cts.Token).FireAndForget(); - _optionsMonitor.OnChange(EvictExecutor); var schemaNames = _applicationServices.GetService>()? .Select(x => x.Value).Distinct().Order().ToImmutableArray(); SchemaNames = schemaNames ?? []; @@ -151,9 +151,7 @@ private async Task CreateRequestExecutorAsync( bool isInitialCreation, CancellationToken cancellationToken) { - var setup = - await _optionsMonitor.GetAsync(schemaName, cancellationToken) - .ConfigureAwait(false); + var setup = _optionsMonitor.Get(schemaName); var context = new ConfigurationContext( schemaName, @@ -270,26 +268,28 @@ await typeModuleChangeMonitor.ConfigureAsync(context, cancellationToken) serviceCollection.AddSingleton(executorOptions); serviceCollection.AddSingleton( - static s => s.GetRequiredService()); + static sp => sp.GetRequiredService()); serviceCollection.AddSingleton( - static s => s.GetRequiredService()); + static sp => sp.GetRequiredService()); serviceCollection.AddSingleton( - static s => s.GetRequiredService()); + static sp => sp.GetRequiredService()); serviceCollection.AddSingleton( - static s => s.GetRequiredService()); + static sp => sp.GetRequiredService()); serviceCollection.AddSingleton( - static s => s.GetRequiredService().PersistedOperations); + static sp => sp.GetRequiredService().PersistedOperations); - serviceCollection.AddSingleton(static sp => new DefaultPreparedOperationCache( - sp.GetRootServiceProvider().GetRequiredService().Capacity)); + serviceCollection.AddSingleton( + static sp => + { + var options = sp.GetRequiredService().GetOptions(); + return new DefaultPreparedOperationCache(options.PreparedOperationCacheSize); + }); serviceCollection.AddSingleton(); serviceCollection.AddSingleton( static sp => sp.GetRootServiceProvider().GetRequiredService()); serviceCollection.AddSingleton( static sp => sp.GetRootServiceProvider().GetRequiredService()); - serviceCollection.AddSingleton( - static sp => sp.GetRootServiceProvider().GetRequiredService()); serviceCollection.TryAddDiagnosticEvents(); serviceCollection.TryAddOperationExecutors(); diff --git a/src/HotChocolate/Core/src/Types/Extensions/SchemaExtensions.cs b/src/HotChocolate/Core/src/Types/Extensions/SchemaExtensions.cs index ef7a52d801d..fa8a39f604a 100644 --- a/src/HotChocolate/Core/src/Types/Extensions/SchemaExtensions.cs +++ b/src/HotChocolate/Core/src/Types/Extensions/SchemaExtensions.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using HotChocolate.Features; using HotChocolate.Language; using HotChocolate.Types; using TypeThrowHelper = HotChocolate.Utilities.ThrowHelper; @@ -295,4 +296,18 @@ public static ITypeSystemMember GetMember( throw TypeThrowHelper.Schema_GetMember_TypeNotFound(coordinate); } + + /// + /// Gets the of the . + /// + /// + /// The schema to get the options for. + /// + /// + /// The schema options. + /// + internal static IReadOnlySchemaOptions GetOptions(this ISchemaDefinition schema) + { + return schema.Features.GetRequired(); + } } diff --git a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs index 72dc9051922..c9f679d2082 100644 --- a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs +++ b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs @@ -193,7 +193,17 @@ public interface IReadOnlySchemaOptions /// /// Specifies that the should be constructed - /// laz + /// lazily. /// bool LazyInitialization { get; } + + /// + /// Specifies the size of the prepared operation cache. + /// + int PreparedOperationCacheSize { get; } + + /// + /// Specifies the size of the operation document cache. + /// + int OperationDocumentCacheSize { get; } } diff --git a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs index e0ea515ed18..9c8db7b2fc9 100644 --- a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs +++ b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs @@ -36,6 +36,7 @@ private SchemaBuilder() typeInterceptors.TryAdd(new MiddlewareValidationTypeInterceptor()); typeInterceptors.TryAdd(new SemanticNonNullTypeInterceptor()); typeInterceptors.TryAdd(new StoreGlobalPagingOptionsTypeInterceptor()); + typeInterceptors.TryAdd(new StoreGlobalSchemaOptionsTypeInterceptor()); Features.Set(typeInterceptors); } diff --git a/src/HotChocolate/Core/src/Types/SchemaOptions.cs b/src/HotChocolate/Core/src/Types/SchemaOptions.cs index 1b387539aed..e7800a5e736 100644 --- a/src/HotChocolate/Core/src/Types/SchemaOptions.cs +++ b/src/HotChocolate/Core/src/Types/SchemaOptions.cs @@ -127,7 +127,7 @@ public FieldBindingFlags DefaultFieldBindingFlags public bool PublishRootFieldPagesToPromiseCache { get; set; } = true; /// - /// Gets or sets whether the should be initialized lazily. + /// Gets or sets whether the schema and request executor should be initialized lazily. /// false by default. /// /// @@ -139,6 +139,44 @@ public FieldBindingFlags DefaultFieldBindingFlags /// public bool LazyInitialization { get; set; } + /// + /// Gets or sets the size of the prepared operation cache. + /// 256 by default. 16 is the minimum. + /// + public int PreparedOperationCacheSize + { + get; + set + { + if (value < 16) + { + throw new ArgumentException( + "The size of prepared operation cache must be at least 16."); + } + + field = value; + } + } = 256; + + /// + /// Gets or sets the size of the operation document cache. + /// 256 by default. 16 is the minimum. + /// + public int OperationDocumentCacheSize + { + get; + set + { + if (value < 16) + { + throw new ArgumentException( + "The size of operation document cache must be at least 16."); + } + + field = value; + } + } = 256; + /// /// Creates a mutable options object from a read-only options object. /// @@ -175,7 +213,9 @@ public static SchemaOptions FromOptions(IReadOnlySchemaOptions options) EnableTag = options.EnableTag, DefaultQueryDependencyInjectionScope = options.DefaultQueryDependencyInjectionScope, DefaultMutationDependencyInjectionScope = options.DefaultMutationDependencyInjectionScope, - LazyInitialization = options.LazyInitialization + LazyInitialization = options.LazyInitialization, + PreparedOperationCacheSize = options.PreparedOperationCacheSize, + OperationDocumentCacheSize = options.OperationDocumentCacheSize }; } } diff --git a/src/HotChocolate/Core/src/Types/StoreGlobalSchemaOptionsTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/StoreGlobalSchemaOptionsTypeInterceptor.cs new file mode 100644 index 00000000000..1a5f06d1c07 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/StoreGlobalSchemaOptionsTypeInterceptor.cs @@ -0,0 +1,18 @@ +using HotChocolate.Configuration; +using HotChocolate.Types.Descriptors.Configurations; + +namespace HotChocolate; + +internal sealed class StoreGlobalSchemaOptionsTypeInterceptor : TypeInterceptor +{ + public override void OnBeforeCompleteType( + ITypeCompletionContext completionContext, + TypeSystemConfiguration configuration) + { + if (configuration is SchemaTypeConfiguration schemaDef) + { + var options = completionContext.DescriptorContext.Options; + schemaDef.Features.Set(options); + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/DocumentCacheTests.cs b/src/HotChocolate/Core/test/Execution.Tests/DocumentCacheTests.cs new file mode 100644 index 00000000000..270b53dde5a --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/DocumentCacheTests.cs @@ -0,0 +1,97 @@ +using HotChocolate.Language; +using HotChocolate.Types; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Execution; + +public class DocumentCacheTests +{ + [Fact] + public async Task Document_Cache_Should_Have_Configured_Capacity() + { + // arrange + const int cacheCapacity = 517; + var services = new ServiceCollection(); + services + .AddGraphQL() + .ModifyOptions(o => o.OperationDocumentCacheSize = cacheCapacity) + .AddQueryType(d => d.Field("foo").Resolve("")); + var executor = await services.BuildServiceProvider().GetRequestExecutorAsync(); + + // act + var documentCache = executor.Schema.Services.GetRequiredService(); + + // assert + Assert.Equal(cacheCapacity, documentCache.Capacity); + } + + [Fact] + public async Task Document_Cache_Should_Not_Be_Scoped_To_Executor() + { + // arrange + var executorEvictedResetEvent = new ManualResetEventSlim(false); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + var services = + new ServiceCollection() + .AddGraphQL() + .AddQueryType(d => d.Field("foo").Resolve("")) + .Services + .BuildServiceProvider(); + + var manager = services.GetRequiredService(); + + manager.Subscribe(new RequestExecutorEventObserver(@event => + { + if (@event.Type == RequestExecutorEventType.Evicted) + { + executorEvictedResetEvent.Set(); + } + })); + + // act + var firstExecutor = await manager.GetExecutorAsync(cancellationToken: cts.Token); + var firstDocumentCache = firstExecutor.Schema.Services.GetRequiredService(); + + await firstExecutor.ExecuteAsync("{ __typename }", cts.Token); + + manager.EvictExecutor(); + executorEvictedResetEvent.Wait(cts.Token); + + var secondExecutor = await manager.GetExecutorAsync(cancellationToken: cts.Token); + var secondDocumentCache = secondExecutor.Schema.Services.GetRequiredService(); + + // assert + Assert.NotSame(secondExecutor, firstExecutor); + Assert.Same(secondDocumentCache, firstDocumentCache); + Assert.Equal(1, secondDocumentCache.Count); + } + + [Fact] + public async Task Document_Cache_Should_Be_Scoped_To_Schema() + { + // arrange + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + var services = new ServiceCollection(); + services + .AddGraphQL("a") + .AddQueryType(d => d.Field("foo").Resolve("")); + services + .AddGraphQL("b") + .AddQueryType(d => d.Field("foo").Resolve("")); + + var manager = services.BuildServiceProvider().GetRequiredService(); + + // act + var executorA = await manager.GetExecutorAsync("a", cts.Token); + var documentCacheA = executorA.Schema.Services.GetRequiredService(); + + var executorB = await manager.GetExecutorAsync("b", cts.Token); + var documentCacheB = executorB.Schema.Services.GetRequiredService(); + + // assert + Assert.NotSame(executorB, executorA); + Assert.NotSame(documentCacheB, documentCacheA); + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/PreparedOperationCacheTests.cs b/src/HotChocolate/Core/test/Execution.Tests/PreparedOperationCacheTests.cs index 2010345a99b..98aa18fdc60 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/PreparedOperationCacheTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/PreparedOperationCacheTests.cs @@ -10,21 +10,56 @@ public class PreparedOperationCacheTests public async Task Operation_Cache_Should_Have_Configured_Capacity() { // arrange - const int operationCacheCapacity = 517; + const int cacheCapacity = 517; var services = new ServiceCollection(); - services.AddOperationCache(operationCacheCapacity); services .AddGraphQL() + .ModifyOptions(o => o.PreparedOperationCacheSize = cacheCapacity) .AddQueryType(d => d.Field("foo").Resolve("")); - var provider = services.BuildServiceProvider(); - var resolver = provider.GetRequiredService(); + var executor = await services.BuildServiceProvider().GetRequestExecutorAsync(); // act - var executor = await resolver.GetExecutorAsync(); - var operationCache = executor.Schema.Services.GetCombinedServices() + var operationCache = executor.Schema.Services.GetRequiredService(); + + // assert + Assert.Equal(cacheCapacity, operationCache.Capacity); + } + + [Fact] + public async Task Operation_Cache_Should_Be_Scoped_To_Executor() + { + // arrange + var executorEvictedResetEvent = new ManualResetEventSlim(false); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + var manager = new ServiceCollection() + .AddGraphQL() + .AddQueryType(d => d.Field("foo").Resolve("")) + .Services.BuildServiceProvider() + .GetRequiredService(); + + manager.Subscribe(new RequestExecutorEventObserver(@event => + { + if (@event.Type == RequestExecutorEventType.Evicted) + { + executorEvictedResetEvent.Set(); + } + })); + + // act + var firstExecutor = await manager.GetExecutorAsync(cancellationToken: cts.Token); + var firstOperationCache = firstExecutor.Schema.Services + .GetRequiredService(); + + manager.EvictExecutor(); + executorEvictedResetEvent.Wait(cts.Token); + + var secondExecutor = await manager.GetExecutorAsync(cancellationToken: cts.Token); + var secondOperationCache = secondExecutor.Schema.Services .GetRequiredService(); // assert - Assert.Equal(operationCacheCapacity, operationCache.Capacity); + Assert.NotSame(secondExecutor, firstExecutor); + Assert.NotSame(secondOperationCache, firstOperationCache); } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorManagerTests.cs b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorManagerTests.cs index c226ef24d7f..3c69b81e768 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorManagerTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorManagerTests.cs @@ -1,4 +1,3 @@ -using HotChocolate.Execution.Caching; using HotChocolate.Execution.Configuration; using HotChocolate.Language; using HotChocolate.Types; @@ -29,43 +28,6 @@ public async Task GetExecutorAsync_Throws_If_Schema_Does_Not_Exist() Assert.Equal($"The requested schema 'unknown-name' does not exist.", exception.Message); } - [Fact] - public async Task Operation_Cache_Should_Be_Scoped_To_Executor() - { - // arrange - var executorEvictedResetEvent = new ManualResetEventSlim(false); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - - var manager = new ServiceCollection() - .AddGraphQL() - .AddQueryType(d => d.Field("foo").Resolve("")) - .Services.BuildServiceProvider() - .GetRequiredService(); - - manager.Subscribe(new RequestExecutorEventObserver(@event => - { - if (@event.Type == RequestExecutorEventType.Evicted) - { - executorEvictedResetEvent.Set(); - } - })); - - // act - var firstExecutor = await manager.GetExecutorAsync(cancellationToken: cts.Token); - var firstOperationCache = firstExecutor.Schema.Services - .GetRequiredService(); - - manager.EvictExecutor(); - executorEvictedResetEvent.Wait(cts.Token); - - var secondExecutor = await manager.GetExecutorAsync(cancellationToken: cts.Token); - var secondOperationCache = secondExecutor.Schema.Services - .GetRequiredService(); - - // assert - Assert.NotSame(secondOperationCache, firstOperationCache); - } - [Fact] public async Task Executor_Should_Only_Be_Switched_Once_It_Is_Warmed_Up() { diff --git a/src/HotChocolate/Core/test/Execution.Tests/WarmupRequestTests.cs b/src/HotChocolate/Core/test/Execution.Tests/WarmupRequestTests.cs index 1378fea1e76..9274f7f9d17 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/WarmupRequestTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/WarmupRequestTests.cs @@ -35,7 +35,7 @@ public async Task Warmup_Request_Warms_Up_Caches() // assert 1 Assert.IsType(warmupResult); - var documentCache = executor.Schema.Services.GetCombinedServices().GetRequiredService(); + var documentCache = executor.Schema.Services.GetRequiredService(); var operationCache = executor.Schema.Services.GetRequiredService(); Assert.True(documentCache.TryGetDocument(documentId, out _)); @@ -82,7 +82,7 @@ public async Task Warmup_Request_Can_Skip_Persisted_Operation_Check() // assert Assert.IsType(warmupResult); - var provider = executor.Schema.Services.GetCombinedServices(); + var provider = executor.Schema.Services; var documentCache = provider.GetRequiredService(); var operationCache = provider.GetRequiredService(); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/DependencyInjection/HotChocolateFusionServiceCollectionExtensions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/DependencyInjection/HotChocolateFusionServiceCollectionExtensions.cs index 27b0880e215..c59f17d0edd 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/DependencyInjection/HotChocolateFusionServiceCollectionExtensions.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/DependencyInjection/HotChocolateFusionServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using HotChocolate.Fusion.Configuration; using HotChocolate.Fusion.Execution; using HotChocolate.Fusion.Execution.Clients; +using HotChocolate.Language; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; @@ -74,7 +75,31 @@ private static DefaultFusionGatewayBuilder CreateBuilder( } var builder = new DefaultFusionGatewayBuilder(services, name); + builder.AddDocumentCache(); builder.UseDefaultPipeline(); return builder; } + + private static IFusionGatewayBuilder AddDocumentCache(this IFusionGatewayBuilder builder) + { + builder.Services.TryAddKeyedSingleton( + builder.Name, + static (sp, schemaName) => + { + var optionsMonitor = sp.GetRequiredService>(); + var setup = optionsMonitor.Get((string)schemaName!); + + var options = FusionRequestExecutorManager.CreateOptions(setup); + + return new DefaultDocumentCache(options.OperationDocumentCacheSize); + }); + + return builder.ConfigureSchemaServices( + static (applicationServices, s) => + s.AddSingleton(schemaServices => + { + var schemaName = schemaServices.GetRequiredService().Name; + return applicationServices.GetRequiredKeyedService(schemaName); + })); + } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionOptions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionOptions.cs index 7a329b19714..6f664b88b87 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionOptions.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionOptions.cs @@ -35,9 +35,13 @@ public int OperationExecutionPlanCacheSize { ExpectMutableOptions(); - field = value < 16 - ? 16 - : value; + if (value < 16) + { + throw new ArgumentException( + "The size of operation execution plan cache must be at least 16."); + } + + field = value; } } = 256; @@ -66,9 +70,13 @@ public int OperationDocumentCacheSize { ExpectMutableOptions(); - field = value < 16 - ? 16 - : value; + if (value < 16) + { + throw new ArgumentException( + "The size of operation document cache must be at least 16."); + } + + field = value; } } = 256; 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 13332f89de4..14a614c2feb 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs @@ -431,12 +431,6 @@ private static void AddParserServices(IServiceCollection services) { services.AddSingleton(static _ => new MD5DocumentHashProvider(HashFormat.Hex)); services.AddSingleton(static sp => sp.GetRequiredService().GetParserOptions()); - services.AddSingleton( - static sp => - { - var options = sp.GetRequiredService().GetOptions(); - return new DefaultDocumentCache(options.OperationDocumentCacheSize); - }); } private void AddDocumentValidator( diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/FusionDocumentCacheTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/FusionDocumentCacheTests.cs new file mode 100644 index 00000000000..1ba4dec6d91 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/FusionDocumentCacheTests.cs @@ -0,0 +1,130 @@ +using HotChocolate.Execution; +using HotChocolate.Language; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Fusion.Execution; + +public class FusionDocumentCacheTests : FusionTestBase +{ + [Fact] + public async Task Document_Cache_Should_Have_Configured_Capacity() + { + // arrange + const int cacheCapacity = 517; + var services = new ServiceCollection(); + services + .AddGraphQLGateway() + .ModifyOptions(o => o.OperationDocumentCacheSize = cacheCapacity) + .AddInMemoryConfiguration( + ComposeSchemaDocument( + """ + type Query { + field: String! + } + """)); + var executor = await services.BuildServiceProvider().GetRequestExecutorAsync(); + + // act + var documentCache = executor.Schema.Services.GetRequiredService(); + + // assert + Assert.Equal(cacheCapacity, documentCache.Capacity); + } + + [Fact] + public async Task Document_Cache_Should_Not_Be_Scoped_To_Executor() + { + // arrange + var executorEvictedResetEvent = new ManualResetEventSlim(false); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + var configProvider = new TestFusionConfigurationProvider( + CreateFusionConfiguration( + """ + type Query { + field1: String! + } + """)); + + var services = + new ServiceCollection() + .AddHttpClient() + .AddGraphQLGateway() + .AddConfigurationProvider(_ => configProvider) + .Services + .BuildServiceProvider(); + + var manager = services.GetRequiredService(); + + manager.Subscribe(new RequestExecutorEventObserver(@event => + { + if (@event.Type == RequestExecutorEventType.Evicted) + { + executorEvictedResetEvent.Set(); + } + })); + + // act + var firstExecutor = await manager.GetExecutorAsync(cancellationToken: cts.Token); + var firstDocumentCache = firstExecutor.Schema.Services.GetRequiredService(); + + await firstExecutor.ExecuteAsync("{ __typename }", cts.Token); + + configProvider.UpdateConfiguration( + CreateFusionConfiguration( + """ + type Query { + field2: String! + } + """)); + executorEvictedResetEvent.Wait(cts.Token); + + var secondExecutor = await manager.GetExecutorAsync(cancellationToken: cts.Token); + var secondDocumentCache = secondExecutor.Schema.Services.GetRequiredService(); + + // assert + Assert.NotSame(secondExecutor, firstExecutor); + Assert.Same(secondDocumentCache, firstDocumentCache); + Assert.Equal(1, secondDocumentCache.Count); + } + + [Fact] + public async Task Document_Cache_Should_Be_Scoped_To_Schema() + { + // arrange + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + var services = new ServiceCollection().AddHttpClient(); + services + .AddGraphQLGateway("a") + .AddInMemoryConfiguration( + ComposeSchemaDocument( + """ + type Query { + fieldA: String! + } + """)); + services + .AddGraphQLGateway("b") + .AddInMemoryConfiguration( + ComposeSchemaDocument( + """ + type Query { + fieldB: String! + } + """)); + + var manager = services.BuildServiceProvider().GetRequiredService(); + + // act + var executorA = await manager.GetExecutorAsync("a", cts.Token); + var documentCacheA = executorA.Schema.Services.GetRequiredService(); + + var executorB = await manager.GetExecutorAsync("b", cts.Token); + var documentCacheB = executorB.Schema.Services.GetRequiredService(); + + // assert + Assert.NotSame(executorB, executorA); + Assert.NotSame(documentCacheB, documentCacheA); + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/FusionRequestExecutorManagerTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/FusionRequestExecutorManagerTests.cs index 0385793225c..a6ec523a8fe 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/FusionRequestExecutorManagerTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/FusionRequestExecutorManagerTests.cs @@ -39,7 +39,7 @@ type Query { } [Fact] - public async Task CreateExecutor() + public async Task Create_Executor() { // arrange var schemaDocument = @@ -67,7 +67,7 @@ type Query { } [Fact] - public async Task GetOperationPlanFromExecution() + public async Task Get_Plan_From_Execution_Result() { // arrange var schemaDocument = @@ -130,54 +130,6 @@ query Test { Assert.Equal("Test", Assert.IsType(operationPlan).OperationName); } - [Fact] - public async Task Plan_Cache_Should_Be_Scoped_To_Executor() - { - // arrange - var executorEvictedResetEvent = new ManualResetEventSlim(false); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - - var configProvider = new TestFusionConfigurationProvider(CreateConfiguration()); - - var services = - new ServiceCollection() - .AddGraphQLGateway() - .AddConfigurationProvider(_ => configProvider) - .Services - .BuildServiceProvider(); - - var manager = services.GetRequiredService(); - - manager.Subscribe(new RequestExecutorEventObserver(@event => - { - if (@event.Type == RequestExecutorEventType.Evicted) - { - executorEvictedResetEvent.Set(); - } - })); - - // act - var firstExecutor = await manager.GetExecutorAsync(cancellationToken: cts.Token); - var firstPlanCache = firstExecutor.Schema.Services - .GetRequiredService>(); - - configProvider.UpdateConfiguration( - CreateConfiguration( - """ - type Query { - field2: String! - } - """)); - executorEvictedResetEvent.Wait(cts.Token); - - var secondExecutor = await manager.GetExecutorAsync(cancellationToken: cts.Token); - var secondPlanCache = secondExecutor.Schema.Services - .GetRequiredService>(); - - // assert - Assert.NotSame(secondPlanCache, firstPlanCache); - } - [Fact] public async Task Executor_Should_Only_Be_Switched_Once_It_Is_Warmed_Up() { diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/OperationPlanCacheTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/OperationPlanCacheTests.cs new file mode 100644 index 00000000000..bf59d9edde2 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/OperationPlanCacheTests.cs @@ -0,0 +1,89 @@ +using HotChocolate.Caching.Memory; +using HotChocolate.Execution; +using HotChocolate.Fusion.Execution.Nodes; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Fusion.Execution; + +public class OperationPlanCacheTests : FusionTestBase +{ + [Fact] + public async Task Plan_Cache_Should_Have_Configured_Capacity() + { + // arrange + const int cacheCapacity = 517; + var services = new ServiceCollection(); + services + .AddGraphQLGateway() + .ModifyOptions(o => o.OperationExecutionPlanCacheSize = cacheCapacity) + .AddInMemoryConfiguration( + ComposeSchemaDocument( + """ + type Query { + field: String! + } + """)); + var executor = await services.BuildServiceProvider().GetRequestExecutorAsync(); + + // act + var operationPlanCache = executor.Schema.Services.GetRequiredService>(); + + // assert + Assert.Equal(cacheCapacity, operationPlanCache.Capacity); + } + + [Fact] + public async Task Plan_Cache_Should_Be_Scoped_To_Executor() + { + // arrange + var executorEvictedResetEvent = new ManualResetEventSlim(false); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + var configProvider = new TestFusionConfigurationProvider( + CreateFusionConfiguration( + """ + type Query { + field1: String! + } + """)); + + var services = + new ServiceCollection() + .AddGraphQLGateway() + .AddConfigurationProvider(_ => configProvider) + .Services + .BuildServiceProvider(); + + var manager = services.GetRequiredService(); + + manager.Subscribe(new RequestExecutorEventObserver(@event => + { + if (@event.Type == RequestExecutorEventType.Evicted) + { + executorEvictedResetEvent.Set(); + } + })); + + // act + var firstExecutor = await manager.GetExecutorAsync(cancellationToken: cts.Token); + var firstPlanCache = firstExecutor.Schema.Services + .GetRequiredService>(); + + configProvider.UpdateConfiguration( + CreateFusionConfiguration( + """ + type Query { + field2: String! + } + """)); + executorEvictedResetEvent.Wait(cts.Token); + + var secondExecutor = await manager.GetExecutorAsync(cancellationToken: cts.Token); + var secondPlanCache = secondExecutor.Schema.Services + .GetRequiredService>(); + + // assert + Assert.NotSame(secondExecutor, firstExecutor); + Assert.NotSame(secondPlanCache, firstPlanCache); + } +} diff --git a/website/src/docs/hotchocolate/v16/migrating/migrate-from-15-to-16.md b/website/src/docs/hotchocolate/v16/migrating/migrate-from-15-to-16.md index d4f6a1e7962..b90d0ed7c54 100644 --- a/website/src/docs/hotchocolate/v16/migrating/migrate-from-15-to-16.md +++ b/website/src/docs/hotchocolate/v16/migrating/migrate-from-15-to-16.md @@ -38,28 +38,59 @@ builder.Services.AddGraphQLServer() .ModifyOptions(options => options.LazyInitialization = true); ``` -## MaxAllowedNodeBatchSize & EnsureAllNodesCanBeResolved options moved +## Cache size configuration -**Before** +Previously, configuring document and operation cache sizes required calling methods directly on `IServiceCollection` rather than using the standard `IRequestExecutorBuilder` pattern. We've now consolidated cache configuration with other GraphQL options for consistency. +If you're currently using `AddOperationCache` or `AddDocumentCache`, update your code as follows: + +```diff +-builder.Services.AddDocumentCache(200); +-builder.Services.AddOperationCache(100); -```csharp builder.Services.AddGraphQLServer() - .ModifyOptions(options => - { - options.MaxAllowedNodeBatchSize = 100; - options.EnsureAllNodesCanBeResolved = false; - }); ++ .ModifyOptions(options => ++ { ++ options.OperationDocumentCacheSize = 200; ++ options.PreparedOperationCacheSize = 100; ++ }); ``` -**After** +If you were previously accessing `IDocumentCache` or `IPreparedOperationCache` through the root service provider, you now need to access it through the schema-specific service provider instead. +For instance, to populate the document cache during startup, create a custom `IRequestExecutorWarmupTask` that injects `IDocumentCache`: ```csharp -builder.Services.AddGraphQLServer() - .AddGlobalObjectIdentification(options => +builder.Services + .AddGraphQLServer() + .AddWarmupTask(); + +public class MyWarmupTask(IDocumentCache cache) : IRequestExecutorWarmupTask +{ + public bool ApplyOnlyOnStartup => false; + + public async Task WarmupAsync( + IRequestExecutor executor, + CancellationToken cancellationToken) { - options.MaxAllowedNodeBatchSize = 100; - options.EnsureAllNodesCanBeResolved = false; - }); + // Modify the cache + } +} +``` + +## MaxAllowedNodeBatchSize & EnsureAllNodesCanBeResolved options moved + +```diff +builder.Services.AddGraphQLServer() +- .ModifyOptions(options => +- { +- options.MaxAllowedNodeBatchSize = 100; +- options.EnsureAllNodesCanBeResolved = false; +- }) +- .AddGlobalObjectIdentification() ++ .AddGlobalObjectIdentification(options => ++ { ++ options.MaxAllowedNodeBatchSize = 100; ++ options.EnsureAllNodesCanBeResolved = false; ++ }); ``` ## IRequestContext