From 54ae2989ebc831d09ca48c54fec6fb187ff07550 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Fri, 9 Feb 2024 21:00:07 +0100 Subject: [PATCH 1/4] Add option to disable null-bubbling --- .../src/Abstractions/WellKnownContextData.cs | 11 +++++++--- .../Pipeline/OperationResolverMiddleware.cs | 5 +++++ .../Core/src/Types/IReadOnlySchemaOptions.cs | 9 +++++++-- .../Core/src/Types/SchemaBuilder.cs | 3 ++- .../Core/src/Types/SchemaOptions.cs | 6 ++++++ .../DisableNullBubblingTypeInterceptor.cs | 20 +++++++++++++++++++ 6 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 src/HotChocolate/Core/src/Types/Types/Interceptors/DisableNullBubblingTypeInterceptor.cs diff --git a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs index 421e741b84f..73bc1d4d456 100644 --- a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs +++ b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs @@ -274,17 +274,22 @@ public static class WellKnownContextData /// The key to access the authorization allowed flag on the member context. /// public const string AllowAnonymous = "HotChocolate.Authorization.AllowAnonymous"; - + /// /// The key to access the true nullability flag on the execution context. /// public const string EnableTrueNullability = "HotChocolate.Types.EnableTrueNullability"; - + + /// + /// The key to access the disable null-bubbling flag on the execution context. + /// + public const string DisableNullBubbling = "HotChocolate.Types.DisableNullBubbling"; + /// /// The key to access the tag options object. /// public const string TagOptions = "HotChocolate.Types.TagOptions"; - + /// /// Type key to access the internal schema options. /// diff --git a/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs b/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs index 257d98021e3..bf065002ced 100644 --- a/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs +++ b/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs @@ -109,6 +109,11 @@ public async ValueTask InvokeAsync(IRequestContext context) private bool IsNullBubblingEnabled(IRequestContext context, OperationDefinitionNode operationDefinition) { + if (context.Schema.ContextData.ContainsKey(DisableNullBubbling)) + { + return false; + } + if (!context.Schema.ContextData.ContainsKey(EnableTrueNullability) || operationDefinition.Directives.Count == 0) { diff --git a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs index 1bb10098e6a..c51de86b420 100644 --- a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs +++ b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs @@ -55,7 +55,7 @@ public interface IReadOnlySchemaOptions /// unreachable from the root types. /// bool RemoveUnreachableTypes { get; } - + /// /// Defines if unused type system directives shall /// be removed from the schema. @@ -97,7 +97,7 @@ public interface IReadOnlySchemaOptions /// Defines if the order of important middleware components shall be validated. /// bool ValidatePipelineOrder { get; } - + /// /// Defines if the runtime types of types shall be validated. /// @@ -181,6 +181,11 @@ public interface IReadOnlySchemaOptions /// bool EnableTrueNullability { get; } + /// + /// Specifies whether null-bubbling shall be disabled. + /// + bool DisableNullBubbling { get; } + /// /// Specifies that the @tag directive shall be registered with the type system. /// diff --git a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs index 43e3450d759..a2f4c6e12e0 100644 --- a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs +++ b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs @@ -39,6 +39,7 @@ public partial class SchemaBuilder : ISchemaBuilder typeof(CostTypeInterceptor), typeof(MiddlewareValidationTypeInterceptor), typeof(EnableTrueNullabilityTypeInterceptor), + typeof(DisableNullBubblingTypeInterceptor), ]; private SchemaOptions _options = new(); @@ -263,7 +264,7 @@ public ISchemaBuilder AddType(INamedTypeExtension typeExtension) _types.Add(_ => TypeReference.Create(typeExtension)); return this; } - + internal void AddTypeReference(TypeReference typeReference) { if (typeReference is null) diff --git a/src/HotChocolate/Core/src/Types/SchemaOptions.cs b/src/HotChocolate/Core/src/Types/SchemaOptions.cs index f55f4c8ac4c..63c4f6beec6 100644 --- a/src/HotChocolate/Core/src/Types/SchemaOptions.cs +++ b/src/HotChocolate/Core/src/Types/SchemaOptions.cs @@ -211,6 +211,11 @@ public FieldBindingFlags DefaultFieldBindingFlags /// public bool EnableTrueNullability { get; set; } + /// + /// Specifies whether null-bubbling shall be disabled. + /// + public bool DisableNullBubbling { get; set; } + /// /// Specifies that the @tag directive shall be registered with the type system. /// @@ -263,6 +268,7 @@ public static SchemaOptions FromOptions(IReadOnlySchemaOptions options) MaxAllowedNodeBatchSize = options.MaxAllowedNodeBatchSize, StripLeadingIFromInterface = options.StripLeadingIFromInterface, EnableTrueNullability = options.EnableTrueNullability, + DisableNullBubbling = options.DisableNullBubbling, EnableTag = options.EnableTag, DefaultQueryDependencyInjectionScope = options.DefaultQueryDependencyInjectionScope, DefaultMutationDependencyInjectionScope = options.DefaultMutationDependencyInjectionScope, diff --git a/src/HotChocolate/Core/src/Types/Types/Interceptors/DisableNullBubblingTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Types/Interceptors/DisableNullBubblingTypeInterceptor.cs new file mode 100644 index 00000000000..f220b15f5a3 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Interceptors/DisableNullBubblingTypeInterceptor.cs @@ -0,0 +1,20 @@ +#nullable enable +using HotChocolate.Configuration; +using HotChocolate.Types.Descriptors; +using HotChocolate.Types.Descriptors.Definitions; + +namespace HotChocolate.Types.Interceptors; + +internal sealed class DisableNullBubblingTypeInterceptor : TypeInterceptor +{ + internal override bool IsEnabled(IDescriptorContext context) + => context.Options.DisableNullBubbling; + + public override void OnAfterInitialize(ITypeDiscoveryContext discoveryContext, DefinitionBase definition) + { + if (definition is SchemaTypeDefinition schemaDef) + { + schemaDef.ContextData[WellKnownContextData.DisableNullBubbling] = true; + } + } +} From 0a94e864c2330efb509acb8ec2349c02cd107a9a Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Sat, 10 Feb 2024 11:31:40 +0100 Subject: [PATCH 2/4] Add GraphQL-Disable-NullBubbling header --- .../DefaultHttpRequestInterceptor.cs | 5 ++++ .../DefaultSocketSessionInterceptor.cs | 5 ++++ .../Extensions/HttpContextExtensions.cs | 13 +++++++++ .../src/AspNetCore/HttpHeaderKeys.cs | 2 ++ .../src/AspNetCore/HttpHeaderValues.cs | 2 ++ .../HttpRequestHeadersExtensions.cs | 27 +++++++++++++++++-- .../src/Abstractions/WellKnownContextData.cs | 9 ++----- .../Pipeline/OperationResolverMiddleware.cs | 3 ++- .../Core/src/Types/IReadOnlySchemaOptions.cs | 5 ---- .../Core/src/Types/SchemaBuilder.cs | 1 - .../Core/src/Types/SchemaOptions.cs | 6 ----- .../DisableNullBubblingTypeInterceptor.cs | 20 -------------- 12 files changed, 56 insertions(+), 42 deletions(-) delete mode 100644 src/HotChocolate/Core/src/Types/Types/Interceptors/DisableNullBubblingTypeInterceptor.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultHttpRequestInterceptor.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultHttpRequestInterceptor.cs index 0f3701f7338..7928bb943fb 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultHttpRequestInterceptor.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultHttpRequestInterceptor.cs @@ -34,6 +34,11 @@ public class DefaultHttpRequestInterceptor : IHttpRequestInterceptor requestBuilder.TryAddGlobalState(WellKnownContextData.IncludeQueryPlan, true); } + if (context.IsNullBubblingDisabled()) + { + requestBuilder.TryAddGlobalState(WellKnownContextData.DisableNullBubbling, true); + } + return default; } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultSocketSessionInterceptor.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultSocketSessionInterceptor.cs index 486f0556c7a..9d746f40c81 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultSocketSessionInterceptor.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/DefaultSocketSessionInterceptor.cs @@ -44,6 +44,11 @@ public class DefaultSocketSessionInterceptor : ISocketSessionInterceptor requestBuilder.TryAddGlobalState(IncludeQueryPlan, true); } + if (context.IsNullBubblingDisabled()) + { + requestBuilder.TryAddGlobalState(WellKnownContextData.DisableNullBubbling, true); + } + return default; } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpContextExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpContextExtensions.cs index 62db2dbdfce..314b1bc11ae 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpContextExtensions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HttpContextExtensions.cs @@ -35,4 +35,17 @@ public static bool IncludeQueryPlan(this HttpContext context) return false; } + + public static bool IsNullBubblingDisabled(this HttpContext context) + { + var headers = context.Request.Headers; + + if (headers.TryGetValue(HttpHeaderKeys.DisableNullBubbling, out var values) && + values.Any(v => v == HttpHeaderValues.DisableNullBubbling)) + { + return true; + } + + return false; + } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderKeys.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderKeys.cs index d946a1a0dee..ad9d2b55b2f 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderKeys.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderKeys.cs @@ -11,4 +11,6 @@ internal static class HttpHeaderKeys public const string CacheControl = "Cache-Control"; public const string Preflight = "GraphQL-Preflight"; + + public const string DisableNullBubbling = "GraphQL-Disable-NullBubbling"; } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderValues.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderValues.cs index 02bf31125f3..5f29e488e62 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderValues.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/HttpHeaderValues.cs @@ -6,5 +6,7 @@ internal static class HttpHeaderValues public const string IncludeQueryPlan = "1"; + public const string DisableNullBubbling = "1"; + public const string NoCache = "no-cache"; } diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/HttpRequestHeadersExtensions.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/HttpRequestHeadersExtensions.cs index 74abc5616dd..1a13eada177 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Http/HttpRequestHeadersExtensions.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/HttpRequestHeadersExtensions.cs @@ -29,5 +29,28 @@ public static HttpRequestHeaders AddGraphQLPreflight(this HttpRequestHeaders hea headers.Add("GraphQL-Preflight", "1"); return headers; - } -} \ No newline at end of file + } + + /// + /// Adds the GraphQL-Disable-NullBubbling header to the request. + /// + /// + /// The to add the header to. + /// + /// + /// Returns the for configuration chaining. + /// + /// + /// is . + /// + public static HttpRequestHeaders AddGraphQLDisableNullBubbling(this HttpRequestHeaders headers) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } + + headers.Add("GraphQL-Disable-NullBubbling", "1"); + return headers; + } +} diff --git a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs index 73bc1d4d456..2a5ddd8ed82 100644 --- a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs +++ b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs @@ -169,11 +169,6 @@ public static class WellKnownContextData /// public const string NodeResolver = "HotChocolate.Relay.Node.Resolver"; - /// - /// The key to check if relay support is enabled. - /// - public const string IsRelaySupportEnabled = "HotChocolate.Relay.IsEnabled"; - /// /// The key to check if the global identification spec is enabled. /// @@ -281,9 +276,9 @@ public static class WellKnownContextData public const string EnableTrueNullability = "HotChocolate.Types.EnableTrueNullability"; /// - /// The key to access the disable null-bubbling flag on the execution context. + /// Disables null-bubbling for the current request. /// - public const string DisableNullBubbling = "HotChocolate.Types.DisableNullBubbling"; + public const string DisableNullBubbling = "HotChocolate.Execution.DisableNullBubbling"; /// /// The key to access the tag options object. diff --git a/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs b/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs index bf065002ced..077ffa71107 100644 --- a/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs +++ b/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs @@ -109,7 +109,8 @@ public async ValueTask InvokeAsync(IRequestContext context) private bool IsNullBubblingEnabled(IRequestContext context, OperationDefinitionNode operationDefinition) { - if (context.Schema.ContextData.ContainsKey(DisableNullBubbling)) + if (context.ContextData.TryGetValue(DisableNullBubbling, out var disableNullBubbling) + && disableNullBubbling is true) { return false; } diff --git a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs index c51de86b420..16895e6ff8a 100644 --- a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs +++ b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs @@ -181,11 +181,6 @@ public interface IReadOnlySchemaOptions /// bool EnableTrueNullability { get; } - /// - /// Specifies whether null-bubbling shall be disabled. - /// - bool DisableNullBubbling { get; } - /// /// Specifies that the @tag directive shall be registered with the type system. /// diff --git a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs index a2f4c6e12e0..0612dbba5fb 100644 --- a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs +++ b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs @@ -39,7 +39,6 @@ public partial class SchemaBuilder : ISchemaBuilder typeof(CostTypeInterceptor), typeof(MiddlewareValidationTypeInterceptor), typeof(EnableTrueNullabilityTypeInterceptor), - typeof(DisableNullBubblingTypeInterceptor), ]; private SchemaOptions _options = new(); diff --git a/src/HotChocolate/Core/src/Types/SchemaOptions.cs b/src/HotChocolate/Core/src/Types/SchemaOptions.cs index 63c4f6beec6..f55f4c8ac4c 100644 --- a/src/HotChocolate/Core/src/Types/SchemaOptions.cs +++ b/src/HotChocolate/Core/src/Types/SchemaOptions.cs @@ -211,11 +211,6 @@ public FieldBindingFlags DefaultFieldBindingFlags /// public bool EnableTrueNullability { get; set; } - /// - /// Specifies whether null-bubbling shall be disabled. - /// - public bool DisableNullBubbling { get; set; } - /// /// Specifies that the @tag directive shall be registered with the type system. /// @@ -268,7 +263,6 @@ public static SchemaOptions FromOptions(IReadOnlySchemaOptions options) MaxAllowedNodeBatchSize = options.MaxAllowedNodeBatchSize, StripLeadingIFromInterface = options.StripLeadingIFromInterface, EnableTrueNullability = options.EnableTrueNullability, - DisableNullBubbling = options.DisableNullBubbling, EnableTag = options.EnableTag, DefaultQueryDependencyInjectionScope = options.DefaultQueryDependencyInjectionScope, DefaultMutationDependencyInjectionScope = options.DefaultMutationDependencyInjectionScope, diff --git a/src/HotChocolate/Core/src/Types/Types/Interceptors/DisableNullBubblingTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Types/Interceptors/DisableNullBubblingTypeInterceptor.cs deleted file mode 100644 index f220b15f5a3..00000000000 --- a/src/HotChocolate/Core/src/Types/Types/Interceptors/DisableNullBubblingTypeInterceptor.cs +++ /dev/null @@ -1,20 +0,0 @@ -#nullable enable -using HotChocolate.Configuration; -using HotChocolate.Types.Descriptors; -using HotChocolate.Types.Descriptors.Definitions; - -namespace HotChocolate.Types.Interceptors; - -internal sealed class DisableNullBubblingTypeInterceptor : TypeInterceptor -{ - internal override bool IsEnabled(IDescriptorContext context) - => context.Options.DisableNullBubbling; - - public override void OnAfterInitialize(ITypeDiscoveryContext discoveryContext, DefinitionBase definition) - { - if (definition is SchemaTypeDefinition schemaDef) - { - schemaDef.ContextData[WellKnownContextData.DisableNullBubbling] = true; - } - } -} From def180cf02e1cd1a13c988dea18c27d548234fe3 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Sat, 10 Feb 2024 18:37:36 +0100 Subject: [PATCH 3/4] Add simple test --- .../Execution.Tests/TrueNullabilityTests.cs | 58 ++++++++++++++----- ...rror_Query_With_NullBubbling_Disabled.snap | 26 +++++++++ 2 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_NullBubbling_Disabled.snap diff --git a/src/HotChocolate/Core/test/Execution.Tests/TrueNullabilityTests.cs b/src/HotChocolate/Core/test/Execution.Tests/TrueNullabilityTests.cs index 512ce55b61d..09ded9bb63b 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/TrueNullabilityTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/TrueNullabilityTests.cs @@ -15,10 +15,10 @@ public async Task Schema_Without_TrueNullability() .AddQueryType() .ModifyOptions(o => o.EnableTrueNullability = false) .BuildSchemaAsync(); - + schema.MatchSnapshot(); } - + [Fact] public async Task Schema_With_TrueNullability() { @@ -28,10 +28,10 @@ public async Task Schema_With_TrueNullability() .AddQueryType() .ModifyOptions(o => o.EnableTrueNullability = true) .BuildSchemaAsync(); - + schema.MatchSnapshot(); } - + [Fact] public async Task Error_Query_With_TrueNullability_And_NullBubbling_Enabled_By_Default() { @@ -51,10 +51,10 @@ public async Task Error_Query_With_TrueNullability_And_NullBubbling_Enabled_By_D } } """); - + response.MatchSnapshot(); } - + [Fact] public async Task Error_Query_With_TrueNullability_And_NullBubbling_Enabled() { @@ -74,10 +74,10 @@ public async Task Error_Query_With_TrueNullability_And_NullBubbling_Enabled() } } """); - + response.MatchSnapshot(); } - + [Fact] public async Task Error_Query_With_TrueNullability_And_NullBubbling_Disabled() { @@ -97,10 +97,10 @@ public async Task Error_Query_With_TrueNullability_And_NullBubbling_Disabled() } } """); - + response.MatchSnapshot(); } - + [Fact] public async Task Error_Query_With_TrueNullability_And_NullBubbling_Disabled_With_Variable() { @@ -124,24 +124,50 @@ public async Task Error_Query_With_TrueNullability_And_NullBubbling_Disabled_Wit """) .SetVariableValue("enable", false) .Create()); - + + response.MatchSnapshot(); + } + + [Fact] + public async Task Error_Query_With_NullBubbling_Disabled() + { + var request = QueryRequestBuilder.New() + .SetQuery(""" + query { + book { + name + author { + name + } + } + } + """) + .TryAddGlobalState(WellKnownContextData.DisableNullBubbling, true) + .Create(); + + var response = + await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .ExecuteRequestAsync(request); + response.MatchSnapshot(); } - + public class Query { public Book? GetBook() => new(); } - + public class Book { public string Name => "Some book!"; public Author Author => new(); } - + public class Author { public string Name => throw new Exception(); - } -} \ No newline at end of file + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_NullBubbling_Disabled.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_NullBubbling_Disabled.snap new file mode 100644 index 00000000000..5f1271ea8b6 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_NullBubbling_Disabled.snap @@ -0,0 +1,26 @@ +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 5, + "column": 13 + } + ], + "path": [ + "book", + "author", + "name" + ] + } + ], + "data": { + "book": { + "name": "Some book!", + "author": { + "name": null + } + } + } +} \ No newline at end of file From 69e564d605e589fffc0c7a30f957b11df7730837 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Fri, 22 Mar 2024 19:49:25 +0100 Subject: [PATCH 4/4] Add AllowDisablingNullBubbling option --- .../Options/IRequestExecutorOptionsAccessor.cs | 12 +++++++++--- .../src/Execution/Options/RequestExecutorOptions.cs | 9 +++++++-- .../Pipeline/OperationResolverMiddleware.cs | 12 +++++++++--- .../test/Execution.Tests/TrueNullabilityTests.cs | 1 + 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/HotChocolate/Core/src/Execution/Options/IRequestExecutorOptionsAccessor.cs b/src/HotChocolate/Core/src/Execution/Options/IRequestExecutorOptionsAccessor.cs index 1207b425206..2f7738e5557 100644 --- a/src/HotChocolate/Core/src/Execution/Options/IRequestExecutorOptionsAccessor.cs +++ b/src/HotChocolate/Core/src/Execution/Options/IRequestExecutorOptionsAccessor.cs @@ -8,6 +8,12 @@ namespace HotChocolate.Execution.Options; /// public interface IRequestExecutorOptionsAccessor : IErrorHandlerOptionsAccessor - , IRequestTimeoutOptionsAccessor - , IComplexityAnalyzerOptionsAccessor - , IPersistedQueryOptionsAccessor; + , IRequestTimeoutOptionsAccessor + , IComplexityAnalyzerOptionsAccessor + , IPersistedQueryOptionsAccessor +{ + /// + /// Determine whether null-bubbling can be disabled on a per-request basis. + /// + bool AllowDisablingNullBubbling { get; } +} diff --git a/src/HotChocolate/Core/src/Execution/Options/RequestExecutorOptions.cs b/src/HotChocolate/Core/src/Execution/Options/RequestExecutorOptions.cs index 229128b2361..b37dc655ead 100644 --- a/src/HotChocolate/Core/src/Execution/Options/RequestExecutorOptions.cs +++ b/src/HotChocolate/Core/src/Execution/Options/RequestExecutorOptions.cs @@ -72,9 +72,14 @@ public IError OnlyPersistedQueriesAreAllowedError get => _onlyPersistedQueriesAreAllowedError; set { - _onlyPersistedQueriesAreAllowedError = value - ?? throw new ArgumentNullException( + _onlyPersistedQueriesAreAllowedError = value ?? + throw new ArgumentNullException( nameof(OnlyPersistedQueriesAreAllowedError)); } } + + /// + /// Determine whether null-bubbling can be disabled on a per-request basis. + /// + public bool AllowDisablingNullBubbling { get; set; } = false; } diff --git a/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs b/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs index 077ffa71107..22513c4dee5 100644 --- a/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs +++ b/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using HotChocolate.Execution.Options; using HotChocolate.Execution.Processing; using HotChocolate.Language; using HotChocolate.Types; @@ -19,6 +20,7 @@ internal sealed class OperationResolverMiddleware { private readonly RequestDelegate _next; private readonly ObjectPool _operationCompilerPool; + private readonly IRequestExecutorOptionsAccessor _options; private readonly VariableCoercionHelper _coercionHelper; private readonly IReadOnlyList? _optimizers; @@ -26,6 +28,7 @@ internal sealed class OperationResolverMiddleware RequestDelegate next, ObjectPool operationCompilerPool, IEnumerable optimizers, + IRequestExecutorOptionsAccessor options, VariableCoercionHelper coercionHelper) { if (optimizers is null) @@ -37,6 +40,8 @@ internal sealed class OperationResolverMiddleware throw new ArgumentNullException(nameof(next)); _operationCompilerPool = operationCompilerPool ?? throw new ArgumentNullException(nameof(operationCompilerPool)); + _options = options ?? + throw new ArgumentNullException(nameof(options)); _coercionHelper = coercionHelper ?? throw new ArgumentNullException(nameof(coercionHelper)); _optimizers = optimizers.ToArray(); @@ -109,8 +114,7 @@ public async ValueTask InvokeAsync(IRequestContext context) private bool IsNullBubblingEnabled(IRequestContext context, OperationDefinitionNode operationDefinition) { - if (context.ContextData.TryGetValue(DisableNullBubbling, out var disableNullBubbling) - && disableNullBubbling is true) + if (_options.AllowDisablingNullBubbling && context.ContextData.ContainsKey(DisableNullBubbling)) { return false; } @@ -175,12 +179,14 @@ public static RequestCoreMiddleware Create() var operationCompilerPool = core.Services.GetRequiredService>(); var optimizers1 = core.Services.GetRequiredService>(); var optimizers2 = core.SchemaServices.GetRequiredService>(); + var options = core.SchemaServices.GetRequiredService(); var coercionHelper = core.Services.GetRequiredService(); var middleware = new OperationResolverMiddleware( next, operationCompilerPool, optimizers1.Concat(optimizers2), + options, coercionHelper); return context => middleware.InvokeAsync(context); }; -} \ No newline at end of file +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/TrueNullabilityTests.cs b/src/HotChocolate/Core/test/Execution.Tests/TrueNullabilityTests.cs index 09ded9bb63b..5e4b1cebc32 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/TrueNullabilityTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/TrueNullabilityTests.cs @@ -148,6 +148,7 @@ public async Task Error_Query_With_NullBubbling_Disabled() var response = await new ServiceCollection() .AddGraphQLServer() + .ModifyRequestOptions(options => options.AllowDisablingNullBubbling = true) .AddQueryType() .ExecuteRequestAsync(request);