diff --git a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs
index 43be07d7fa0..a5575f9bdd8 100644
--- a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs
+++ b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs
@@ -274,4 +274,9 @@ 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";
}
diff --git a/src/HotChocolate/Core/src/Abstractions/WellKnownDirectives.cs b/src/HotChocolate/Core/src/Abstractions/WellKnownDirectives.cs
index e7980b8cb62..7939a6d8da4 100644
--- a/src/HotChocolate/Core/src/Abstractions/WellKnownDirectives.cs
+++ b/src/HotChocolate/Core/src/Abstractions/WellKnownDirectives.cs
@@ -69,4 +69,8 @@ public static class WellKnownDirectives
/// The name of the @tag argument name.
///
public const string Name = "name";
+
+ public const string NullBubbling = "nullBubbling";
+
+ public const string Enable = "enable";
}
diff --git a/src/HotChocolate/Core/src/Execution/ErrorHelper.cs b/src/HotChocolate/Core/src/Execution/ErrorHelper.cs
index 017695f88a1..43ec590e3b9 100644
--- a/src/HotChocolate/Core/src/Execution/ErrorHelper.cs
+++ b/src/HotChocolate/Core/src/Execution/ErrorHelper.cs
@@ -249,4 +249,22 @@ public static IError ReadPersistedQueryMiddleware_PersistedQueryNotFound()
.SetMessage("PersistedQueryNotFound")
.SetCode(ErrorCodes.Execution.PersistedQueryNotFound)
.Build();
+
+ public static IError NoNullBubbling_ArgumentValue_NotAllowed(
+ ArgumentNode argument)
+ {
+ var errorBuilder = ErrorBuilder.New();
+
+ if (argument.Value.Location is not null)
+ {
+ errorBuilder.AddLocation(
+ argument.Value.Location.Line,
+ argument.Value.Location.Column);
+ }
+
+ errorBuilder.SetSyntaxNode(argument.Value);
+ errorBuilder.SetMessage(ErrorHelper_NoNullBubbling_ArgumentValue_NotAllowed);
+
+ return errorBuilder.Build();
+ }
}
diff --git a/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs b/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs
index 32d4b220287..e598f6b68ad 100644
--- a/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs
+++ b/src/HotChocolate/Core/src/Execution/Pipeline/OperationResolverMiddleware.cs
@@ -5,7 +5,12 @@
using HotChocolate.Execution.Processing;
using HotChocolate.Language;
using HotChocolate.Types;
+using HotChocolate.Utilities;
using Microsoft.Extensions.ObjectPool;
+using static HotChocolate.Execution.ErrorHelper;
+using static HotChocolate.WellKnownDirectives;
+using static HotChocolate.Execution.Pipeline.PipelineTools;
+using static HotChocolate.WellKnownContextData;
namespace HotChocolate.Execution.Pipeline;
@@ -13,20 +18,26 @@ internal sealed class OperationResolverMiddleware
{
private readonly RequestDelegate _next;
private readonly ObjectPool _operationCompilerPool;
+ private readonly VariableCoercionHelper _coercionHelper;
private readonly IReadOnlyList? _optimizers;
public OperationResolverMiddleware(
RequestDelegate next,
ObjectPool operationCompilerPool,
- IEnumerable optimizers)
+ IEnumerable optimizers,
+ VariableCoercionHelper coercionHelper)
{
if (optimizers is null)
{
throw new ArgumentNullException(nameof(optimizers));
}
- _next = next ?? throw new ArgumentNullException(nameof(next));
- _operationCompilerPool = operationCompilerPool;
+ _next = next ??
+ throw new ArgumentNullException(nameof(next));
+ _operationCompilerPool = operationCompilerPool ??
+ throw new ArgumentNullException(nameof(operationCompilerPool));
+ _coercionHelper = coercionHelper ??
+ throw new ArgumentNullException(nameof(coercionHelper));
_optimizers = optimizers.ToArray();
}
@@ -45,7 +56,7 @@ public async ValueTask InvokeAsync(IRequestContext context)
if (operationType is null)
{
- context.Result = ErrorHelper.RootTypeNotFound(operationDef.Operation);
+ context.Result = RootTypeNotFound(operationDef.Operation);
return;
}
@@ -61,7 +72,7 @@ public async ValueTask InvokeAsync(IRequestContext context)
}
else
{
- context.Result = ErrorHelper.StateInvalidForOperationResolver();
+ context.Result = StateInvalidForOperationResolver();
}
}
@@ -78,7 +89,8 @@ private IOperation CompileOperation(
operationType,
context.Document!,
context.Schema,
- _optimizers);
+ _optimizers,
+ IsNullBubblingEnabled(context, operationDefinition));
_operationCompilerPool.Return(compiler);
return operation;
}
@@ -93,4 +105,60 @@ private IOperation CompileOperation(
OperationType.Subscription => schema.SubscriptionType,
_ => throw ThrowHelper.RootTypeNotSupported(operationType)
};
-}
+
+ private bool IsNullBubblingEnabled(IRequestContext context, OperationDefinitionNode operationDefinition)
+ {
+ if (!context.Schema.ContextData.ContainsKey(EnableTrueNullability) ||
+ operationDefinition.Directives.Count == 0)
+ {
+ return true;
+ }
+
+ var enabled = true;
+
+ for (var i = 0; i < operationDefinition.Directives.Count; i++)
+ {
+ var directive = operationDefinition.Directives[i];
+
+ if (!directive.Name.Value.EqualsOrdinal(NullBubbling))
+ {
+ continue;
+ }
+
+ for (var j = 0; j < directive.Arguments.Count; j++)
+ {
+ var argument = directive.Arguments[j];
+
+ if (argument.Name.Value.EqualsOrdinal(Enable))
+ {
+ if (argument.Value is BooleanValueNode b)
+ {
+ enabled = b.Value;
+ break;
+ }
+
+ if (argument.Value is VariableNode v)
+ {
+ enabled = CoerceVariable(context, operationDefinition, v);
+ break;
+ }
+
+ throw new GraphQLException(NoNullBubbling_ArgumentValue_NotAllowed(argument));
+ }
+ }
+
+ break;
+ }
+
+ return enabled;
+ }
+
+ private bool CoerceVariable(
+ IRequestContext context,
+ OperationDefinitionNode operationDefinition,
+ VariableNode variable)
+ {
+ var variables = CoerceVariables(context, _coercionHelper, operationDefinition.VariableDefinitions);
+ return variables.GetVariable(variable.Name.Value);
+ }
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Execution/Pipeline/PipelineTools.cs b/src/HotChocolate/Core/src/Execution/Pipeline/PipelineTools.cs
index f8f7d890cfb..3e72a233cf9 100644
--- a/src/HotChocolate/Core/src/Execution/Pipeline/PipelineTools.cs
+++ b/src/HotChocolate/Core/src/Execution/Pipeline/PipelineTools.cs
@@ -25,19 +25,20 @@ public static string CreateCacheId(
string? operationName)
=> CreateCacheId(context, CreateOperationId(documentId, operationName));
- public static void CoerceVariables(
+ public static IVariableValueCollection CoerceVariables(
IRequestContext context,
VariableCoercionHelper coercionHelper,
IReadOnlyList variableDefinitions)
{
if (context.Variables is not null)
{
- return;
+ return context.Variables;
}
if (variableDefinitions.Count == 0)
{
context.Variables = _noVariables;
+ return _noVariables;
}
else
{
@@ -52,6 +53,7 @@ public static void CoerceVariables(
coercedValues);
context.Variables = new VariableValueCollection(coercedValues);
+ return context.Variables;
}
}
}
diff --git a/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.CompilerContext.cs b/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.CompilerContext.cs
index 8f81ca6eadb..6659b83b39b 100644
--- a/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.CompilerContext.cs
+++ b/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.CompilerContext.cs
@@ -8,17 +8,11 @@ namespace HotChocolate.Execution.Processing;
public sealed partial class OperationCompiler
{
- internal sealed class CompilerContext
+ internal sealed class CompilerContext(ISchema schema, DocumentNode document, bool enableNullBubbling)
{
- public CompilerContext(ISchema schema, DocumentNode document)
- {
- Schema = schema;
- Document = document;
- }
-
- public ISchema Schema { get; }
+ public ISchema Schema { get; } = schema;
- public DocumentNode Document { get; }
+ public DocumentNode Document { get; } = document;
public ObjectType Type { get; private set; } = default!;
@@ -35,6 +29,8 @@ public CompilerContext(ISchema schema, DocumentNode document)
public IImmutableList Optimizers { get; private set; } =
ImmutableList.Empty;
+
+ public bool EnableNullBubbling { get; } = enableNullBubbling;
public void Initialize(
ObjectType type,
diff --git a/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.cs b/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.cs
index 260834358d8..4f5f548b684 100644
--- a/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.cs
+++ b/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.cs
@@ -24,6 +24,7 @@ public sealed partial class OperationCompiler
{
private static readonly ImmutableList _emptyOptimizers =
ImmutableList.Empty;
+
private readonly InputParser _parser;
private readonly CreateFieldPipeline _createFieldPipeline;
private readonly Stack _backlog = new();
@@ -65,7 +66,8 @@ public IOperation Compile(
ObjectType operationType,
DocumentNode document,
ISchema schema,
- IReadOnlyList? optimizers = null)
+ IReadOnlyList? optimizers = null,
+ bool enableNullBubbling = true)
{
if (string.IsNullOrEmpty(operationId))
{
@@ -112,7 +114,7 @@ public IOperation Compile(
var variants = GetOrCreateSelectionVariants(id);
SelectionSetInfo[] infos = { new(operationDefinition.SelectionSet, 0) };
- var context = new CompilerContext(schema, document);
+ var context = new CompilerContext(schema, document, enableNullBubbling);
context.Initialize(operationType, variants, infos, rootPath, rootOptimizers);
CompileSelectionSet(context);
@@ -378,19 +380,19 @@ private void CompleteSelectionSet(CompilerContext context)
// For now we only allow streams on lists of composite types.
if (selection.SyntaxNode.IsStreamable())
{
- var streamDirective = selection.SyntaxNode.GetStreamDirectiveNode();
- var nullValue = NullValueNode.Default;
- var ifValue = streamDirective?.GetIfArgumentValueOrDefault() ?? nullValue;
- long ifConditionFlags = 0;
-
- if (ifValue.Kind is not SyntaxKind.NullValue)
- {
- var ifCondition = new IncludeCondition(ifValue, nullValue);
- ifConditionFlags = GetSelectionIncludeCondition(ifCondition, 0);
- }
-
- selection.MarkAsStream(ifConditionFlags);
- _hasIncrementalParts = true;
+ var streamDirective = selection.SyntaxNode.GetStreamDirectiveNode();
+ var nullValue = NullValueNode.Default;
+ var ifValue = streamDirective?.GetIfArgumentValueOrDefault() ?? nullValue;
+ long ifConditionFlags = 0;
+
+ if (ifValue.Kind is not SyntaxKind.NullValue)
+ {
+ var ifCondition = new IncludeCondition(ifValue, nullValue);
+ ifConditionFlags = GetSelectionIncludeCondition(ifCondition, 0);
+ }
+
+ selection.MarkAsStream(ifConditionFlags);
+ _hasIncrementalParts = true;
}
}
@@ -449,21 +451,21 @@ private void ResolveFields(
case SyntaxKind.Field:
ResolveField(
context,
- (FieldNode)selection,
+ (FieldNode) selection,
includeCondition);
break;
case SyntaxKind.InlineFragment:
ResolveInlineFragment(
context,
- (InlineFragmentNode)selection,
+ (InlineFragmentNode) selection,
includeCondition);
break;
case SyntaxKind.FragmentSpread:
ResolveFragmentSpread(
context,
- (FragmentSpreadNode)selection,
+ (FragmentSpreadNode) selection,
includeCondition);
break;
}
@@ -481,7 +483,9 @@ private void ResolveField(
if (context.Type.Fields.TryGetField(fieldName, out var field))
{
- var fieldType = field.Type.RewriteNullability(selection.Required);
+ var fieldType = context.EnableNullBubbling
+ ? field.Type.RewriteNullability(selection.Required)
+ : field.Type.RewriteToNullableType();
if (context.Fields.TryGetValue(responseName, out var preparedSelection))
{
@@ -516,7 +520,9 @@ selection.SelectionSet is not null
responseName: responseName,
isParallelExecutable: field.IsParallelExecutable,
arguments: CoerceArgumentValues(field, selection, responseName),
- includeConditions: includeCondition == 0 ? null : new[] { includeCondition });
+ includeConditions: includeCondition == 0
+ ? null
+ : new[] { includeCondition });
context.Fields.Add(responseName, preparedSelection);
@@ -586,6 +592,7 @@ private void ResolveFragment(
var ifValue = deferDirective?.GetIfArgumentValueOrDefault() ?? nullValue;
long ifConditionFlags = 0;
+
if (ifValue.Kind is not SyntaxKind.NullValue)
{
var ifCondition = new IncludeCondition(ifValue, nullValue);
@@ -637,8 +644,8 @@ private static bool DoesTypeApply(IType typeCondition, IObjectType current)
=> typeCondition.Kind switch
{
TypeKind.Object => ReferenceEquals(typeCondition, current),
- TypeKind.Interface => current.IsImplementing((InterfaceType)typeCondition),
- TypeKind.Union => ((UnionType)typeCondition).Types.ContainsKey(current.Name),
+ TypeKind.Interface => current.IsImplementing((InterfaceType) typeCondition),
+ TypeKind.Union => ((UnionType) typeCondition).Types.ContainsKey(current.Name),
_ => false
};
@@ -789,7 +796,7 @@ private CompilerContext RentContext(CompilerContext context)
{
if (_deferContext is null)
{
- return new CompilerContext(context.Schema, context.Document);
+ return new CompilerContext(context.Schema, context.Document, context.EnableNullBubbling);
}
var temp = _deferContext;
@@ -862,4 +869,4 @@ public override bool Equals(object? obj)
public override int GetHashCode()
=> HashCode.Combine(SelectionSet, Path);
}
-}
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Execution/Properties/Resources.Designer.cs b/src/HotChocolate/Core/src/Execution/Properties/Resources.Designer.cs
index 726a6c6dc9c..e8b1603dd57 100644
--- a/src/HotChocolate/Core/src/Execution/Properties/Resources.Designer.cs
+++ b/src/HotChocolate/Core/src/Execution/Properties/Resources.Designer.cs
@@ -476,5 +476,11 @@ internal static string ComplexityAnalyzerCompiler_Enter_OnlyOperations {
return ResourceManager.GetString("ComplexityAnalyzerCompiler_Enter_OnlyOperations", resourceCulture);
}
}
+
+ internal static string ErrorHelper_NoNullBubbling_ArgumentValue_NotAllowed {
+ get {
+ return ResourceManager.GetString("ErrorHelper_NoNullBubbling_ArgumentValue_NotAllowed", resourceCulture);
+ }
+ }
}
}
diff --git a/src/HotChocolate/Core/src/Execution/Properties/Resources.resx b/src/HotChocolate/Core/src/Execution/Properties/Resources.resx
index c4ba2fcadbb..032ad979e66 100644
--- a/src/HotChocolate/Core/src/Execution/Properties/Resources.resx
+++ b/src/HotChocolate/Core/src/Execution/Properties/Resources.resx
@@ -333,4 +333,7 @@
We only compile operations.
+
+ Only boolean values are allowed to switch null bubbling on or off.
+
diff --git a/src/HotChocolate/Core/src/Types/Configuration/AggregateTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Configuration/AggregateTypeInterceptor.cs
index a03db528339..3e0011eb866 100644
--- a/src/HotChocolate/Core/src/Types/Configuration/AggregateTypeInterceptor.cs
+++ b/src/HotChocolate/Core/src/Types/Configuration/AggregateTypeInterceptor.cs
@@ -15,12 +15,7 @@ namespace HotChocolate.Configuration;
internal sealed class AggregateTypeInterceptor : TypeInterceptor
{
private readonly List _typeReferences = new();
- private TypeInterceptor[] _typeInterceptors;
-
- public AggregateTypeInterceptor()
- {
- _typeInterceptors = Array.Empty();
- }
+ private TypeInterceptor[] _typeInterceptors = Array.Empty();
public void SetInterceptors(IReadOnlyCollection typeInterceptors)
{
@@ -37,12 +32,47 @@ public override void OnBeforeCreateSchema(
IDescriptorContext context,
ISchemaBuilder schemaBuilder)
{
- ref var first = ref GetReference();
- var length = _typeInterceptors.Length;
+ ref var start = ref GetReference();
+ ref var current = ref Unsafe.Add(ref start, 0);
+ ref var end = ref Unsafe.Add(ref current, _typeInterceptors.Length);
- for (var i = 0; i < length; i++)
+ // we first initialize all schema context ...
+ while (Unsafe.IsAddressLessThan(ref current, ref end))
+ {
+ current.OnBeforeCreateSchema(context, schemaBuilder);
+ current = ref Unsafe.Add(ref current, 1);
+ }
+
+ current = ref Unsafe.Add(ref start, 0);
+ var i = 0;
+ TypeInterceptor[]? temp = null;
+
+ // next we determine the type interceptors that are enabled ...
+ while (Unsafe.IsAddressLessThan(ref current, ref end))
+ {
+ if (temp is null && !current.IsEnabled(context))
+ {
+ temp ??= new TypeInterceptor[_typeInterceptors.Length];
+ ref var next = ref Unsafe.Add(ref start, 0);
+ while (Unsafe.IsAddressLessThan(ref next, ref current))
+ {
+ temp[i++] = next;
+ next = ref Unsafe.Add(ref next, 1);
+ }
+ }
+
+ if (temp is not null && current.IsEnabled(context))
+ {
+ temp[i++] = current;
+ }
+
+ current = ref Unsafe.Add(ref current, 1);
+ }
+
+ if (temp is not null)
{
- Unsafe.Add(ref first, i).OnBeforeCreateSchema(context, schemaBuilder);
+ Array.Resize(ref temp, i);
+ _typeInterceptors = temp;
}
}
diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs
index a511fb11ebe..a737996b541 100644
--- a/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs
+++ b/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs
@@ -23,6 +23,8 @@ public abstract class TypeInterceptor
/// A weight to order interceptors.
///
internal virtual uint Position => _position;
+
+ public virtual bool IsEnabled(IDescriptorContext context) => true;
///
/// This hook is invoked before anything else any allows for additional modification
diff --git a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs
index 900c95eb04d..f920fb82423 100644
--- a/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs
+++ b/src/HotChocolate/Core/src/Types/IReadOnlySchemaOptions.cs
@@ -87,4 +87,7 @@ public interface IReadOnlySchemaOptions
///
bool StripLeadingIFromInterface { get; }
+
+ ///
+ bool EnableTrueNullability { get; }
}
diff --git a/src/HotChocolate/Core/src/Types/ReadOnlySchemaOptions.cs b/src/HotChocolate/Core/src/Types/ReadOnlySchemaOptions.cs
index 48ff818cdc0..bf06cd35beb 100644
--- a/src/HotChocolate/Core/src/Types/ReadOnlySchemaOptions.cs
+++ b/src/HotChocolate/Core/src/Types/ReadOnlySchemaOptions.cs
@@ -53,6 +53,7 @@ public ReadOnlySchemaOptions(IReadOnlySchemaOptions options)
EnableStream = options.EnableStream;
MaxAllowedNodeBatchSize = options.MaxAllowedNodeBatchSize;
StripLeadingIFromInterface = options.StripLeadingIFromInterface;
+ EnableTrueNullability = options.EnableTrueNullability;
}
///
@@ -128,4 +129,7 @@ public ReadOnlySchemaOptions(IReadOnlySchemaOptions options)
///
public bool StripLeadingIFromInterface { get; }
+
+ ///
+ public bool EnableTrueNullability { get; }
}
diff --git a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs
index 41b93fd3ead..adf143be404 100644
--- a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs
+++ b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs
@@ -35,7 +35,8 @@ public partial class SchemaBuilder : ISchemaBuilder
typeof(IntrospectionTypeInterceptor),
typeof(InterfaceCompletionTypeInterceptor),
typeof(CostTypeInterceptor),
- typeof(MiddlewareValidationTypeInterceptor)
+ typeof(MiddlewareValidationTypeInterceptor),
+ typeof(EnableTrueNullabilityTypeInterceptor)
};
private SchemaOptions _options = new();
diff --git a/src/HotChocolate/Core/src/Types/SchemaOptions.cs b/src/HotChocolate/Core/src/Types/SchemaOptions.cs
index 61d7bc48a42..ecb3883bd9a 100644
--- a/src/HotChocolate/Core/src/Types/SchemaOptions.cs
+++ b/src/HotChocolate/Core/src/Types/SchemaOptions.cs
@@ -203,6 +203,11 @@ public FieldBindingFlags DefaultFieldBindingFlags
///
public bool StripLeadingIFromInterface { get; set; } = false;
+ ///
+ /// Specifies that the true nullability proto type shall be enabled.
+ ///
+ public bool EnableTrueNullability { get; set; } = false;
+
///
/// Creates a mutable options object from a read-only options object.
///
@@ -236,7 +241,8 @@ public static SchemaOptions FromOptions(IReadOnlySchemaOptions options)
EnableStream = options.EnableStream,
DefaultFieldBindingFlags = options.DefaultFieldBindingFlags,
MaxAllowedNodeBatchSize = options.MaxAllowedNodeBatchSize,
- StripLeadingIFromInterface = options.StripLeadingIFromInterface
+ StripLeadingIFromInterface = options.StripLeadingIFromInterface,
+ EnableTrueNullability = options.EnableTrueNullability
};
}
}
diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/Directives.cs b/src/HotChocolate/Core/src/Types/Types/Directives/Directives.cs
index 4741348b0c5..08fbaa72f12 100644
--- a/src/HotChocolate/Core/src/Types/Types/Directives/Directives.cs
+++ b/src/HotChocolate/Core/src/Types/Types/Directives/Directives.cs
@@ -40,6 +40,11 @@ internal static IReadOnlyList CreateReferences(
{
directiveTypes.Add(typeInspector.GetTypeRef(typeof(StreamDirectiveType)));
}
+
+ if (descriptorContext.Options.EnableTrueNullability)
+ {
+ directiveTypes.Add(typeInspector.GetTypeRef(typeof(NullBubblingDirective)));
+ }
directiveTypes.Add(typeInspector.GetTypeRef(typeof(SkipDirectiveType)));
directiveTypes.Add(typeInspector.GetTypeRef(typeof(IncludeDirectiveType)));
diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/NullBubblingDirective.cs b/src/HotChocolate/Core/src/Types/Types/Directives/NullBubblingDirective.cs
new file mode 100644
index 00000000000..4e8fec48fc9
--- /dev/null
+++ b/src/HotChocolate/Core/src/Types/Types/Directives/NullBubblingDirective.cs
@@ -0,0 +1,18 @@
+namespace HotChocolate.Types;
+
+[DirectiveType(
+ WellKnownDirectives.NullBubbling,
+ DirectiveLocation.Query |
+ DirectiveLocation.Mutation |
+ DirectiveLocation.Subscription)]
+public class NullBubblingDirective
+{
+ public NullBubblingDirective(bool enable = true)
+ {
+ Enable = enable;
+ }
+
+ [DefaultValue(true)]
+ [GraphQLName(WellKnownDirectives.Enable)]
+ public bool Enable { get; }
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types/Types/Extensions/TypeExtensions.cs b/src/HotChocolate/Core/src/Types/Types/Extensions/TypeExtensions.cs
index 1b0e7f999d0..6fec956d0b8 100644
--- a/src/HotChocolate/Core/src/Types/Types/Extensions/TypeExtensions.cs
+++ b/src/HotChocolate/Core/src/Types/Types/Extensions/TypeExtensions.cs
@@ -763,4 +763,9 @@ public static IType RewriteNullability(this IType type, INullabilityNode? nullab
throw RewriteNullability_InvalidNullabilityStructure();
}
}
+
+ public static IType RewriteToNullableType(this IType type)
+ => type.Kind is TypeKind.NonNull
+ ? type.InnerType()
+ : type;
}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types/Types/Interceptors/MiddlewareValidationTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Types/Interceptors/MiddlewareValidationTypeInterceptor.cs
index b7c3c97c3b3..0cc4868f7b0 100644
--- a/src/HotChocolate/Core/src/Types/Types/Interceptors/MiddlewareValidationTypeInterceptor.cs
+++ b/src/HotChocolate/Core/src/Types/Types/Interceptors/MiddlewareValidationTypeInterceptor.cs
@@ -2,6 +2,7 @@
using System.Text;
using HotChocolate.Configuration;
using HotChocolate.Language;
+using HotChocolate.Types.Descriptors;
using HotChocolate.Types.Descriptors.Definitions;
using HotChocolate.Utilities;
@@ -173,3 +174,22 @@ void PrintOther()
}
}
}
+
+internal sealed class EnableTrueNullabilityTypeInterceptor : TypeInterceptor
+{
+ public override bool IsEnabled(IDescriptorContext context)
+ => context.Options.EnableTrueNullability;
+
+ public override void OnBeforeCreateSchema(IDescriptorContext context, ISchemaBuilder schemaBuilder)
+ {
+ base.OnBeforeCreateSchema(context, schemaBuilder);
+ }
+
+ public override void OnAfterInitialize(ITypeDiscoveryContext discoveryContext, DefinitionBase definition)
+ {
+ if (definition is SchemaTypeDefinition schemaDef)
+ {
+ schemaDef.ContextData[WellKnownContextData.EnableTrueNullability] = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/test/Execution.Tests/ClientControlledNullabilityTests.cs b/src/HotChocolate/Core/test/Execution.Tests/ClientControlledNullabilityTests.cs
index ce9f6d4665d..a07f0385e97 100644
--- a/src/HotChocolate/Core/test/Execution.Tests/ClientControlledNullabilityTests.cs
+++ b/src/HotChocolate/Core/test/Execution.Tests/ClientControlledNullabilityTests.cs
@@ -75,4 +75,4 @@ public class Person
public string? Bio { get; set; }
}
-}
+}
\ 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
new file mode 100644
index 00000000000..512ce55b61d
--- /dev/null
+++ b/src/HotChocolate/Core/test/Execution.Tests/TrueNullabilityTests.cs
@@ -0,0 +1,147 @@
+#nullable enable
+using CookieCrumble;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace HotChocolate.Execution;
+
+public class TrueNullabilityTests
+{
+ [Fact]
+ public async Task Schema_Without_TrueNullability()
+ {
+ var schema =
+ await new ServiceCollection()
+ .AddGraphQLServer()
+ .AddQueryType()
+ .ModifyOptions(o => o.EnableTrueNullability = false)
+ .BuildSchemaAsync();
+
+ schema.MatchSnapshot();
+ }
+
+ [Fact]
+ public async Task Schema_With_TrueNullability()
+ {
+ var schema =
+ await new ServiceCollection()
+ .AddGraphQLServer()
+ .AddQueryType()
+ .ModifyOptions(o => o.EnableTrueNullability = true)
+ .BuildSchemaAsync();
+
+ schema.MatchSnapshot();
+ }
+
+ [Fact]
+ public async Task Error_Query_With_TrueNullability_And_NullBubbling_Enabled_By_Default()
+ {
+ var response =
+ await new ServiceCollection()
+ .AddGraphQLServer()
+ .AddQueryType()
+ .ModifyOptions(o => o.EnableTrueNullability = true)
+ .ExecuteRequestAsync(
+ """
+ query {
+ book {
+ name
+ author {
+ name
+ }
+ }
+ }
+ """);
+
+ response.MatchSnapshot();
+ }
+
+ [Fact]
+ public async Task Error_Query_With_TrueNullability_And_NullBubbling_Enabled()
+ {
+ var response =
+ await new ServiceCollection()
+ .AddGraphQLServer()
+ .AddQueryType()
+ .ModifyOptions(o => o.EnableTrueNullability = true)
+ .ExecuteRequestAsync(
+ """
+ query @nullBubbling {
+ book {
+ name
+ author {
+ name
+ }
+ }
+ }
+ """);
+
+ response.MatchSnapshot();
+ }
+
+ [Fact]
+ public async Task Error_Query_With_TrueNullability_And_NullBubbling_Disabled()
+ {
+ var response =
+ await new ServiceCollection()
+ .AddGraphQLServer()
+ .AddQueryType()
+ .ModifyOptions(o => o.EnableTrueNullability = true)
+ .ExecuteRequestAsync(
+ """
+ query @nullBubbling(enable: false) {
+ book {
+ name
+ author {
+ name
+ }
+ }
+ }
+ """);
+
+ response.MatchSnapshot();
+ }
+
+ [Fact]
+ public async Task Error_Query_With_TrueNullability_And_NullBubbling_Disabled_With_Variable()
+ {
+ var response =
+ await new ServiceCollection()
+ .AddGraphQLServer()
+ .AddQueryType()
+ .ModifyOptions(o => o.EnableTrueNullability = true)
+ .ExecuteRequestAsync(
+ QueryRequestBuilder.New()
+ .SetQuery(
+ """
+ query($enable: Boolean!) @nullBubbling(enable: $enable) {
+ book {
+ name
+ author {
+ name
+ }
+ }
+ }
+ """)
+ .SetVariableValue("enable", false)
+ .Create());
+
+ 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_TrueNullability_And_NullBubbling_Disabled.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_TrueNullability_And_NullBubbling_Disabled.snap
new file mode 100644
index 00000000000..5f1271ea8b6
--- /dev/null
+++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_TrueNullability_And_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
diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_TrueNullability_And_NullBubbling_Disabled_With_Variable.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_TrueNullability_And_NullBubbling_Disabled_With_Variable.snap
new file mode 100644
index 00000000000..5f1271ea8b6
--- /dev/null
+++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_TrueNullability_And_NullBubbling_Disabled_With_Variable.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
diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_TrueNullability_And_NullBubbling_Enabled.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_TrueNullability_And_NullBubbling_Enabled.snap
new file mode 100644
index 00000000000..4bb9b286d1b
--- /dev/null
+++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_TrueNullability_And_NullBubbling_Enabled.snap
@@ -0,0 +1,37 @@
+{
+ "errors": [
+ {
+ "message": "Cannot return null for non-nullable field.",
+ "locations": [
+ {
+ "line": 4,
+ "column": 9
+ }
+ ],
+ "path": [
+ "book",
+ "author"
+ ],
+ "extensions": {
+ "code": "HC0018"
+ }
+ },
+ {
+ "message": "Unexpected Execution Error",
+ "locations": [
+ {
+ "line": 5,
+ "column": 13
+ }
+ ],
+ "path": [
+ "book",
+ "author",
+ "name"
+ ]
+ }
+ ],
+ "data": {
+ "book": null
+ }
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_TrueNullability_And_NullBubbling_Enabled_By_Default.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_TrueNullability_And_NullBubbling_Enabled_By_Default.snap
new file mode 100644
index 00000000000..4bb9b286d1b
--- /dev/null
+++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Error_Query_With_TrueNullability_And_NullBubbling_Enabled_By_Default.snap
@@ -0,0 +1,37 @@
+{
+ "errors": [
+ {
+ "message": "Cannot return null for non-nullable field.",
+ "locations": [
+ {
+ "line": 4,
+ "column": 9
+ }
+ ],
+ "path": [
+ "book",
+ "author"
+ ],
+ "extensions": {
+ "code": "HC0018"
+ }
+ },
+ {
+ "message": "Unexpected Execution Error",
+ "locations": [
+ {
+ "line": 5,
+ "column": 13
+ }
+ ],
+ "path": [
+ "book",
+ "author",
+ "name"
+ ]
+ }
+ ],
+ "data": {
+ "book": null
+ }
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Schema_With_TrueNullability.graphql b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Schema_With_TrueNullability.graphql
new file mode 100644
index 00000000000..b202b0bbeab
--- /dev/null
+++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Schema_With_TrueNullability.graphql
@@ -0,0 +1,18 @@
+schema {
+ query: Query
+}
+
+type Author {
+ name: String!
+}
+
+type Book {
+ name: String!
+ author: Author!
+}
+
+type Query {
+ book: Book
+}
+
+directive @nullBubbling(enable: Boolean! = true) on QUERY | MUTATION | SUBSCRIPTION
\ No newline at end of file
diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Schema_Without_TrueNullability.graphql b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Schema_Without_TrueNullability.graphql
new file mode 100644
index 00000000000..ae51fdf5769
--- /dev/null
+++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TrueNullabilityTests.Schema_Without_TrueNullability.graphql
@@ -0,0 +1,16 @@
+schema {
+ query: Query
+}
+
+type Author {
+ name: String!
+}
+
+type Book {
+ name: String!
+ author: Author!
+}
+
+type Query {
+ book: Book
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/test/Types.Tests/Configuration/__snapshots__/ReadOnlySchemaOptionsTests.Copy_Options.snap b/src/HotChocolate/Core/test/Types.Tests/Configuration/__snapshots__/ReadOnlySchemaOptionsTests.Copy_Options.snap
index 1f95f9df09a..c5191927baa 100644
--- a/src/HotChocolate/Core/test/Types.Tests/Configuration/__snapshots__/ReadOnlySchemaOptionsTests.Copy_Options.snap
+++ b/src/HotChocolate/Core/test/Types.Tests/Configuration/__snapshots__/ReadOnlySchemaOptionsTests.Copy_Options.snap
@@ -23,5 +23,6 @@
"EnableDefer": true,
"EnableStream": true,
"MaxAllowedNodeBatchSize": 20,
- "StripLeadingIFromInterface": false
+ "StripLeadingIFromInterface": false,
+ "EnableTrueNullability": false
}
diff --git a/src/HotChocolate/Core/test/Types.Tests/Configuration/__snapshots__/ReadOnlySchemaOptionsTests.Copy_Options_Defaults.snap b/src/HotChocolate/Core/test/Types.Tests/Configuration/__snapshots__/ReadOnlySchemaOptionsTests.Copy_Options_Defaults.snap
index 7872414efd6..0f8dc0f9a43 100644
--- a/src/HotChocolate/Core/test/Types.Tests/Configuration/__snapshots__/ReadOnlySchemaOptionsTests.Copy_Options_Defaults.snap
+++ b/src/HotChocolate/Core/test/Types.Tests/Configuration/__snapshots__/ReadOnlySchemaOptionsTests.Copy_Options_Defaults.snap
@@ -23,5 +23,6 @@
"EnableDefer": false,
"EnableStream": false,
"MaxAllowedNodeBatchSize": 50,
- "StripLeadingIFromInterface": false
+ "StripLeadingIFromInterface": false,
+ "EnableTrueNullability": false
}
diff --git a/src/HotChocolate/Core/test/Types.Tests/Configuration/__snapshots__/ReadOnlySchemaOptionsTests.Copy_Options_EnableOneOf_EnableDirectiveIntrospection.snap b/src/HotChocolate/Core/test/Types.Tests/Configuration/__snapshots__/ReadOnlySchemaOptionsTests.Copy_Options_EnableOneOf_EnableDirectiveIntrospection.snap
index 02bf7bb7a04..0b6a5682b06 100644
--- a/src/HotChocolate/Core/test/Types.Tests/Configuration/__snapshots__/ReadOnlySchemaOptionsTests.Copy_Options_EnableOneOf_EnableDirectiveIntrospection.snap
+++ b/src/HotChocolate/Core/test/Types.Tests/Configuration/__snapshots__/ReadOnlySchemaOptionsTests.Copy_Options_EnableOneOf_EnableDirectiveIntrospection.snap
@@ -23,5 +23,6 @@
"EnableDefer": false,
"EnableStream": false,
"MaxAllowedNodeBatchSize": 50,
- "StripLeadingIFromInterface": false
+ "StripLeadingIFromInterface": false,
+ "EnableTrueNullability": false
}