From 0c0727d04fa7d97dd8263a8ea2708c9448658f00 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 6 Feb 2026 20:59:03 +0100 Subject: [PATCH 01/46] Introduced defer to new operation compiler --- .../IOperation.cs | 10 + .../ISelection.cs | 31 +- .../ISelectionSet.cs | 10 + .../Execution/Processing/DeferCondition.cs | 104 ++++ .../Processing/DeferConditionCollection.cs | 50 ++ .../Processing/DeferExecutionCoordinator.cs | 56 ++ .../Types/Execution/Processing/DeferUsage.cs | 23 + .../Processing/FieldSelectionNode.cs | 11 +- .../Processing/IncludeConditionCollection.cs | 2 +- .../Types/Execution/Processing/Operation.cs | 34 +- .../Execution/Processing/OperationCompiler.cs | 155 ++++- .../Execution/Processing/OperationPrinter.cs | 34 +- .../Types/Execution/Processing/Selection.cs | 23 + .../Execution/Processing/SelectionSet.cs | 15 +- .../Processing/Tasks/ResolverTask.cs | 4 +- .../Processing/OperationCompilerTests.cs | 541 ++++++++++++++++++ ...erationCompilerTests.Crypto_List_Test.snap | 48 +- ...erent_Branches_Non_Overlapping_Levels.snap | 56 ++ ...Different_Branches_Overlapping_Fields.snap | 31 + ...onCompilerTests.Defer_Fragment_Spread.snap | 2 +- ...ment_Spread_Deferred_And_Non_Deferred.snap | 25 + ...ent_Spread_Non_Deferred_Then_Deferred.snap | 25 + ...ilerTests.Defer_If_False_Not_Deferred.snap | 25 + ...onCompilerTests.Defer_Inline_Fragment.snap | 4 +- ...gment_Deduplication_Non_Deferred_Wins.snap | 26 + ...r_Multiple_Levels_Field_Deduplication.snap | 53 ++ ...s.Defer_Multiple_Nested_Same_Fragment.snap | 35 ++ ...Nested_Field_Overlap_Parent_And_Child.snap | 28 + ...erTests.Defer_Nested_Inline_Fragments.snap | 27 + ...ested_With_Parent_Field_Deduplication.snap | 36 ++ 30 files changed, 1462 insertions(+), 62 deletions(-) create mode 100644 src/HotChocolate/Core/src/Types/Execution/Processing/DeferCondition.cs create mode 100644 src/HotChocolate/Core/src/Types/Execution/Processing/DeferConditionCollection.cs create mode 100644 src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs create mode 100644 src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsage.cs create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Different_Branches_Non_Overlapping_Levels.snap create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Different_Branches_Overlapping_Fields.snap create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread_Deferred_And_Non_Deferred.snap create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread_Non_Deferred_Then_Deferred.snap create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_If_False_Not_Deferred.snap create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Inline_Fragment_Deduplication_Non_Deferred_Wins.snap create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Multiple_Levels_Field_Deduplication.snap create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Multiple_Nested_Same_Fragment.snap create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_Field_Overlap_Parent_And_Child.snap create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_Inline_Fragments.snap create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_With_Parent_Field_Deduplication.snap diff --git a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/IOperation.cs b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/IOperation.cs index 103016201e7..aed72b6e1e2 100644 --- a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/IOperation.cs +++ b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/IOperation.cs @@ -44,6 +44,16 @@ public interface IOperation : IFeatureProvider /// ISelectionSet RootSelectionSet { get; } + /// + /// Gets a value indicating whether any selection set in this operation + /// contains selections that may be deferred based on @defer directives. + /// + /// + /// true if one or more selection sets in this operation contain deferred selections; + /// otherwise, false. + /// + bool HasDeferredSelections { get; } + /// /// Gets the selection set for the specified and /// . diff --git a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelection.cs b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelection.cs index 5de00d5bd0b..72869a0858c 100644 --- a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelection.cs +++ b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelection.cs @@ -119,9 +119,34 @@ public interface ISelection /// due to @skip or @include directive evaluation. /// /// - /// This method uses efficient bitwise operations to determine inclusion - /// based on the pre-computed flags. For non-conditional selections, - /// this always returns true. + /// For non-conditional selections, this always returns true. /// bool IsIncluded(ulong includeFlags); + + /// + /// Determines whether this selection is deferred based on the @defer directive flags. + /// + /// + /// The defer condition flags representing which @defer directives are active + /// for the current request, computed from the runtime variable values of the + /// if arguments on @defer directives. + /// + /// + /// true if this selection should be deferred and delivered incrementally + /// in a subsequent payload; otherwise, false if it should be included + /// in the initial response. + /// + /// + /// + /// For selections without any @defer directive, this always returns false. + /// + /// + /// The @defer directive (as specified in the GraphQL Incremental Delivery + /// specification) allows clients to mark fragment spreads or inline fragments + /// as lower priority for the initial response. The server may then choose + /// to deliver those fragments in subsequent payloads, reducing time-to-first-byte + /// for the initial result. + /// + /// + bool IsDeferred(ulong deferFlags); } diff --git a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelectionSet.cs b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelectionSet.cs index a64c48f961a..e7d63490319 100644 --- a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelectionSet.cs +++ b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelectionSet.cs @@ -24,6 +24,16 @@ public interface ISelectionSet /// bool IsConditional { get; } + /// + /// Gets a value indicating whether this selection set contains any selections + /// that may be deferred based on @defer directives. + /// + /// + /// true if one or more selections in this set can be deferred; + /// otherwise, false. + /// + bool HasDeferredSelections { get; } + /// /// Gets the type that declares this selection set. /// diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferCondition.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferCondition.cs new file mode 100644 index 00000000000..3458d6cf4ce --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferCondition.cs @@ -0,0 +1,104 @@ +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Execution.Processing; + +internal readonly struct DeferCondition(string? ifVariableName) : IEquatable +{ + public string? IfVariableName => ifVariableName; + + public bool IsDeferred(IVariableValueCollection variableValues) + { + if (ifVariableName is not null) + { + if (!variableValues.TryGetValue(ifVariableName, out var value)) + { + throw new InvalidOperationException($"The variable {ifVariableName} has an invalid value."); + } + + if (!value.Value) + { + return false; + } + } + + return true; + } + + public bool Equals(DeferCondition other) + => string.Equals(ifVariableName, other.IfVariableName, StringComparison.Ordinal); + + public override bool Equals([NotNullWhen(true)] object? obj) + => obj is DeferCondition other && Equals(other); + + public override int GetHashCode() + => HashCode.Combine(ifVariableName); + + public static bool TryCreate(InlineFragmentNode inlineFragment, out DeferCondition deferCondition) + => TryCreate(inlineFragment.Directives, out deferCondition); + + public static bool TryCreate(FragmentSpreadNode fragmentSpread, out DeferCondition deferCondition) + => TryCreate(fragmentSpread.Directives, out deferCondition); + + private static bool TryCreate(IReadOnlyList directives, out DeferCondition deferCondition) + { + if (directives.Count == 0) + { + deferCondition = default; + return false; + } + + for (var i = 0; i < directives.Count; i++) + { + var directive = directives[i]; + + if (!directive.Name.Value.Equals(DirectiveNames.Defer.Name, StringComparison.Ordinal)) + { + continue; + } + + // @defer with no arguments is unconditionally deferred. + if (directive.Arguments.Count == 0) + { + deferCondition = new DeferCondition(null); + return true; + } + + for (var j = 0; j < directive.Arguments.Count; j++) + { + var argument = directive.Arguments[j]; + + if (!argument.Name.Value.Equals(DirectiveNames.Defer.Arguments.If, StringComparison.Ordinal)) + { + continue; + } + + switch (argument.Value) + { + // @defer(if: $variable) - conditionally deferred at runtime. + case VariableNode variable: + deferCondition = new DeferCondition(variable.Name.Value); + return true; + + // @defer(if: true) - unconditionally deferred. + case BooleanValueNode { Value: true }: + deferCondition = new DeferCondition(null); + return true; + + // @defer(if: false) - statically not deferred, no condition needed. + case BooleanValueNode { Value: false }: + deferCondition = default; + return false; + } + } + + // @defer directive found but no `if` argument matched - unconditionally deferred. + deferCondition = new DeferCondition(null); + return true; + } + + deferCondition = default; + return false; + } +} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferConditionCollection.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferConditionCollection.cs new file mode 100644 index 00000000000..4d1a17d9655 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferConditionCollection.cs @@ -0,0 +1,50 @@ +using System.Collections; + +namespace HotChocolate.Execution.Processing; + +internal sealed class DeferConditionCollection : ICollection +{ + private readonly OrderedDictionary _dictionary = []; + + public DeferCondition this[int index] + => _dictionary.GetAt(index).Key; + + public int Count => _dictionary.Count; + + public bool IsReadOnly => false; + + public bool Add(DeferCondition item) + { + if (_dictionary.Count == 64) + { + throw new InvalidOperationException( + "The maximum number of defer conditions has been reached."); + } + + return _dictionary.TryAdd(item, _dictionary.Count); + } + + void ICollection.Add(DeferCondition item) + => Add(item); + + public bool Remove(DeferCondition item) + => throw new InvalidOperationException("This is an add only collection."); + + void ICollection.Clear() + => throw new InvalidOperationException("This is an add only collection."); + + public bool Contains(DeferCondition item) + => _dictionary.ContainsKey(item); + + public int IndexOf(DeferCondition item) + => _dictionary.GetValueOrDefault(item, -1); + + public void CopyTo(DeferCondition[] array, int arrayIndex) + => _dictionary.Keys.CopyTo(array, arrayIndex); + + public IEnumerator GetEnumerator() + => _dictionary.Keys.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); +} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs new file mode 100644 index 00000000000..0579954de9d --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs @@ -0,0 +1,56 @@ +using System.Collections.Concurrent; +using HotChocolate.Text.Json; + +namespace HotChocolate.Execution.Processing; + +internal sealed class DeferExecutionCoordinator +{ + private const int InitialResultId = -1; + private readonly object _sync = new(); + private readonly ConcurrentDictionary _resultIds = new(); + private readonly ConcurrentDictionary _resultInfoLookup = new(); + private readonly ConcurrentDictionary> _branches = new(); + private readonly ConcurrentDictionary _completed = new(); + private int _nextId; + + public int Branch(ResultDocument parent, Path path, DeferUsage deferUsage) + { + var resultInfo = new DeferredResultInfo(path, deferUsage); + + if (!_resultIds.TryGetValue(resultInfo, out var resultId)) + { + lock (_sync) + { + if (!_resultIds.TryGetValue(resultInfo, out resultId)) + { + resultId = _nextId++; + GetBranchesUnsafe(parent).Add(resultId); + _resultInfoLookup.TryAdd(resultId, resultInfo); + _resultIds.TryAdd(resultInfo, resultId); + } + } + } + + return resultId; + } + + public void EnqueueResult(ResultDocument result) + => _completed.TryAdd(InitialResultId, result); + + public void EnqueueResult(ResultDocument result, int resultId) + => _completed.TryAdd(resultId, result); + + private HashSet GetBranchesUnsafe(ResultDocument result) + { + if (!_branches.TryGetValue(result, out var branches)) + { + branches = []; + _branches.TryAdd(result, branches); + } + + return branches; + } + + private readonly record struct DeferredResultInfo(Path Path, DeferUsage Group); +} + diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsage.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsage.cs new file mode 100644 index 00000000000..b29e99e557a --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsage.cs @@ -0,0 +1,23 @@ +namespace HotChocolate.Execution.Processing; + +/// +/// Represents a usage of the @defer directive encountered during operation compilation. +/// Forms a parent chain to model nested defer scopes. +/// +/// +/// The optional label from @defer(label: "..."), used to identify the deferred +/// payload in the incremental delivery response. +/// +/// +/// The parent defer usage when this @defer is nested inside another deferred fragment, +/// or null if this is a top-level defer. +/// +/// +/// The index into the for the if condition +/// associated with this defer directive. This index maps to a bit position in the +/// runtime defer flags bitmask. +/// +public sealed record DeferUsage( + string? Label, + DeferUsage? Parent, + byte DeferConditionIndex); diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/FieldSelectionNode.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/FieldSelectionNode.cs index f5964f13f64..a4f31e1b859 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/FieldSelectionNode.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/FieldSelectionNode.cs @@ -3,7 +3,7 @@ namespace HotChocolate.Execution.Processing; /// -/// Represents a field selection node with its path include flags. +/// Represents a field selection node with its path include flags and defer usage. /// /// /// The syntax node that represents the field selection. @@ -11,4 +11,11 @@ namespace HotChocolate.Execution.Processing; /// /// The flags that must be all set for this selection to be included. /// -public sealed record FieldSelectionNode(FieldNode Node, ulong PathIncludeFlags); +/// +/// The defer usage context this field was collected under, or null if the field +/// is not inside a deferred fragment. +/// +public sealed record FieldSelectionNode( + FieldNode Node, + ulong PathIncludeFlags, + DeferUsage? DeferUsage = null); diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/IncludeConditionCollection.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/IncludeConditionCollection.cs index f9cbbdd292f..0ab3f5e8527 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/IncludeConditionCollection.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/IncludeConditionCollection.cs @@ -2,7 +2,7 @@ namespace HotChocolate.Execution.Processing; -internal class IncludeConditionCollection : ICollection +internal sealed class IncludeConditionCollection : ICollection { private readonly OrderedDictionary _dictionary = []; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Operation.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Operation.cs index 75b017cfd62..86418019941 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Operation.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Operation.cs @@ -18,6 +18,7 @@ public sealed class Operation : IOperation private readonly ConcurrentDictionary<(int, string), SelectionSet> _selectionSets = []; private readonly OperationCompiler _compiler; private readonly IncludeConditionCollection _includeConditions; + private readonly DeferConditionCollection _deferConditions; private readonly OperationFeatureCollection _features; private object[] _elementsById; private int _lastId; @@ -32,9 +33,11 @@ internal Operation( SelectionSet rootSelectionSet, OperationCompiler compiler, IncludeConditionCollection includeConditions, + DeferConditionCollection deferConditions, OperationFeatureCollection features, int lastId, - object[] elementsById) + object[] elementsById, + bool hasDeferredSelections) { ArgumentException.ThrowIfNullOrWhiteSpace(id); ArgumentException.ThrowIfNullOrWhiteSpace(hash); @@ -45,6 +48,7 @@ internal Operation( ArgumentNullException.ThrowIfNull(rootSelectionSet); ArgumentNullException.ThrowIfNull(compiler); ArgumentNullException.ThrowIfNull(includeConditions); + ArgumentNullException.ThrowIfNull(deferConditions); ArgumentNullException.ThrowIfNull(elementsById); Id = id; @@ -56,9 +60,11 @@ internal Operation( RootSelectionSet = rootSelectionSet; _compiler = compiler; _includeConditions = includeConditions; + _deferConditions = deferConditions; _lastId = lastId; _elementsById = elementsById; _features = features; + HasDeferredSelections = hasDeferredSelections; } /// @@ -118,6 +124,9 @@ ISelectionSet IOperation.RootSelectionSet IFeatureCollection IFeatureProvider.Features => Features; + /// + public bool HasDeferredSelections { get; } + /// /// Gets the selection set for the specified /// if the selections named return type is an object type. @@ -183,6 +192,7 @@ public SelectionSet GetSelectionSet(Selection selection, IObjectTypeDefinition t selection, objectType, _includeConditions, + _deferConditions, ref _elementsById, ref _lastId); _selectionSets.TryAdd(key, selectionSet); @@ -247,13 +257,33 @@ public ulong CreateIncludeFlags(IVariableValueCollection variables) { if (includeCondition.IsIncluded(variables)) { - includeFlags |= 1ul << index++; + includeFlags |= 1ul << index; } + + index++; } return includeFlags; } + public ulong CreateDeferFlags(IVariableValueCollection variables) + { + var index = 0; + var deferFlags = 0ul; + + foreach (var deferCondition in _deferConditions) + { + if (deferCondition.IsDeferred(variables)) + { + deferFlags |= 1ul << index; + } + + index++; + } + + return deferFlags; + } + internal Selection GetSelectionById(int id) => Unsafe.As(Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_elementsById), id)); diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationCompiler.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationCompiler.cs index 1209e28d5c0..9151ecc91b1 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationCompiler.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationCompiler.cs @@ -43,7 +43,7 @@ public static Operation Compile( DocumentNode document, Schema schema, IFeatureProvider? context = null) - => Compile(id, id, null, document, schema); + => Compile(id, id, null, document, schema, context); public static Operation Compile( string id, @@ -51,7 +51,7 @@ public static Operation Compile( DocumentNode document, Schema schema, IFeatureProvider? context = null) - => Compile(id, id, operationName, document, schema); + => Compile(id, id, operationName, document, schema, context); public static Operation Compile( string id, @@ -83,7 +83,9 @@ public Operation Compile( var operationDefinition = document.GetOperation(operationName); var includeConditions = new IncludeConditionCollection(); + var deferConditions = new DeferConditionCollection(); IncludeConditionVisitor.Instance.Visit(operationDefinition, includeConditions); + DeferConditionVisitor.Instance.Visit(operationDefinition, deferConditions); var fields = _fieldsPool.Get(); var compilationContext = new CompilationContext(s_objectArrayPool.Rent(128)); @@ -99,7 +101,9 @@ public Operation Compile( operationDefinition.SelectionSet.Selections, rootType, fields, - includeConditions); + includeConditions, + deferConditions, + parentDeferUsage: null); var selectionSet = BuildSelectionSet( SelectionPath.Root, @@ -121,9 +125,11 @@ public Operation Compile( selectionSet, compiler: this, includeConditions, + deferConditions, compilationContext.Features, lastId, - compilationContext.ElementsById); + compilationContext.ElementsById, + hasDeferredSelections: selectionSet.HasDeferredSelections); selectionSet.Complete(operation); @@ -149,6 +155,7 @@ internal SelectionSet CompileSelectionSet( Selection selection, ObjectType objectType, IncludeConditionCollection includeConditions, + DeferConditionCollection deferConditions, ref object[] elementsById, ref int lastId) { @@ -168,7 +175,9 @@ internal SelectionSet CompileSelectionSet( first.Node.SelectionSet!.Selections, objectType, fields, - includeConditions); + includeConditions, + deferConditions, + parentDeferUsage: first.DeferUsage); if (nodes.Length > 1) { @@ -181,7 +190,9 @@ internal SelectionSet CompileSelectionSet( node.Node.SelectionSet!.Selections, objectType, fields, - includeConditions); + includeConditions, + deferConditions, + parentDeferUsage: nodes[i].DeferUsage); } } @@ -203,7 +214,9 @@ private void CollectFields( IReadOnlyList selections, IObjectTypeDefinition typeContext, OrderedDictionary> fields, - IncludeConditionCollection includeConditions) + IncludeConditionCollection includeConditions, + DeferConditionCollection deferConditions, + DeferUsage? parentDeferUsage) { for (var i = 0; i < selections.Count; i++) { @@ -226,7 +239,7 @@ private void CollectFields( pathIncludeFlags |= 1ul << index; } - nodes.Add(new FieldSelectionNode(fieldNode, pathIncludeFlags)); + nodes.Add(new FieldSelectionNode(fieldNode, pathIncludeFlags, parentDeferUsage)); } else if (selection is InlineFragmentNode inlineFragmentNode && DoesTypeApply(inlineFragmentNode.TypeCondition, typeContext)) @@ -239,12 +252,24 @@ private void CollectFields( pathIncludeFlags |= 1ul << index; } + var newDeferUsage = parentDeferUsage; + + if (DeferCondition.TryCreate(inlineFragmentNode, out var deferCondition)) + { + deferConditions.Add(deferCondition); + var deferIndex = deferConditions.IndexOf(deferCondition); + var label = GetDeferLabel(inlineFragmentNode); + newDeferUsage = new DeferUsage(label, parentDeferUsage, (byte)deferIndex); + } + CollectFields( pathIncludeFlags, inlineFragmentNode.SelectionSet.Selections, typeContext, fields, - includeConditions); + includeConditions, + deferConditions, + newDeferUsage); } } } @@ -260,16 +285,20 @@ private SelectionSet BuildSelectionSet( var i = 0; var selections = new Selection[fieldMap.Count]; var isConditional = false; + var hasDeferredSelections = false; var includeFlags = new List(); + var deferUsages = new List(); var selectionSetId = ++lastId; var alwaysIncluded = false; foreach (var (responseName, nodes) in fieldMap) { includeFlags.Clear(); + deferUsages.Clear(); var first = nodes[0]; var isInternal = IsInternal(first.Node); + var hasNonDeferredNode = first.DeferUsage is null; if (first.PathIncludeFlags == 0) { @@ -280,6 +309,11 @@ private SelectionSet BuildSelectionSet( includeFlags.Add(first.PathIncludeFlags); } + if (first.DeferUsage is not null) + { + deferUsages.Add(first.DeferUsage); + } + if (nodes.Count > 1) { for (var j = 1; j < nodes.Count; j++) @@ -305,6 +339,15 @@ private SelectionSet BuildSelectionSet( includeFlags.Add(next.PathIncludeFlags); } + if (next.DeferUsage is null) + { + hasNonDeferredNode = true; + } + else if (!hasNonDeferredNode) + { + deferUsages.Add(next.DeferUsage); + } + if (isInternal) { isInternal = IsInternal(next.Node); @@ -317,6 +360,39 @@ private SelectionSet BuildSelectionSet( CollapseIncludeFlags(includeFlags); } + // If any field node is not inside a deferred fragment, the selection + // is not deferred — it must be included in the initial response. + DeferUsage[]? finalDeferUsage = null; + ulong deferMask = 0; + + if (!hasNonDeferredNode && deferUsages.Count > 0) + { + // Remove child defer usages when their parent is also in the set. + // A field should be delivered with the outermost (earliest) defer + // that contains it. + for (var j = deferUsages.Count - 1; j >= 0; j--) + { + var parent = deferUsages[j].Parent; + while (parent is not null) + { + if (deferUsages.Contains(parent)) + { + deferUsages.RemoveAt(j); + break; + } + + parent = parent.Parent; + } + } + + finalDeferUsage = deferUsages.ToArray(); + foreach (var usage in deferUsages) + { + deferMask |= 1ul << usage.DeferConditionIndex; + } + hasDeferredSelections = true; + } + if (!typeContext.Fields.TryGetField(first.Node.Name.Value, out var field)) { throw ThrowHelper.FieldDoesNotExistOnType(first.Node, typeContext.Name); @@ -336,10 +412,12 @@ private SelectionSet BuildSelectionSet( field, nodes.ToArray(), includeFlags.Count > 0 ? includeFlags.ToArray() : [], - isInternal, - arguments, - fieldDelegate, - pureFieldDelegate); + deferUsage: finalDeferUsage, + deferMask: deferMask, + isInternal: isInternal, + arguments: arguments, + resolverPipeline: fieldDelegate, + pureResolver: pureFieldDelegate); if (optimizers.Length > 0) { @@ -360,7 +438,7 @@ private SelectionSet BuildSelectionSet( // if there are no optimizers registered for this selection we exit early. if (optimizers.Length == 0) { - return new SelectionSet(selectionSetId, path, typeContext, selections, isConditional); + return new SelectionSet(selectionSetId, path, typeContext, selections, isConditional, hasDeferredSelections); } var current = ImmutableCollectionsMarshal.AsImmutableArray(selections); @@ -385,7 +463,7 @@ private SelectionSet BuildSelectionSet( // This mean we can simply construct the SelectionSet. if (current == rewritten) { - return new SelectionSet(selectionSetId, path, typeContext, selections, isConditional); + return new SelectionSet(selectionSetId, path, typeContext, selections, isConditional, hasDeferredSelections); } if (current.Length < rewritten.Length) @@ -405,7 +483,7 @@ private SelectionSet BuildSelectionSet( } selections = ImmutableCollectionsMarshal.AsArray(rewritten)!; - return new SelectionSet(selectionSetId, path, typeContext, selections, isConditional); + return new SelectionSet(selectionSetId, path, typeContext, selections, isConditional, hasDeferredSelections); } private static void CollapseIncludeFlags(List includeFlags) @@ -521,6 +599,34 @@ private static bool IsInternal(FieldNode fieldNode) return false; } + private static string? GetDeferLabel(InlineFragmentNode node) + { + for (var i = 0; i < node.Directives.Count; i++) + { + var directive = node.Directives[i]; + + if (!directive.Name.Value.Equals(DirectiveNames.Defer.Name, StringComparison.Ordinal)) + { + continue; + } + + for (var j = 0; j < directive.Arguments.Count; j++) + { + var arg = directive.Arguments[j]; + + if (arg.Name.Value.Equals(DirectiveNames.Defer.Arguments.Label, StringComparison.Ordinal) + && arg.Value is StringValueNode labelValue) + { + return labelValue.Value; + } + } + + return null; + } + + return null; + } + private class IncludeConditionVisitor : SyntaxWalker { public static readonly IncludeConditionVisitor Instance = new(); @@ -550,6 +656,23 @@ protected override ISyntaxVisitorAction Enter( } } + private class DeferConditionVisitor : SyntaxWalker + { + public static readonly DeferConditionVisitor Instance = new(); + + protected override ISyntaxVisitorAction Enter( + InlineFragmentNode node, + DeferConditionCollection context) + { + if (DeferCondition.TryCreate(node, out var condition)) + { + context.Add(condition); + } + + return base.Enter(node, context); + } + } + private class CompilationContext { private object[] _elementsById; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationPrinter.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationPrinter.cs index 427236654ff..c318832320f 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationPrinter.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationPrinter.cs @@ -114,33 +114,51 @@ private static FieldNode CreateFieldSelection( private static DirectiveNode CreateExecutionInfo(Selection selection) { - var arguments = new ArgumentNode[selection.IsInternal ? 4 : 3]; - arguments[0] = new ArgumentNode("id", new IntValueNode(selection.Id)); - arguments[1] = new ArgumentNode("kind", new EnumValueNode(selection.Strategy.ToString().ToUpperInvariant())); + var argumentCount = 3; + + if (selection.IsInternal) + { + argumentCount++; + } + + if (selection.HasDeferUsage) + { + argumentCount++; + } + + var index = 0; + var arguments = new ArgumentNode[argumentCount]; + arguments[index++] = new ArgumentNode("id", new IntValueNode(selection.Id)); + arguments[index++] = new ArgumentNode("kind", new EnumValueNode(selection.Strategy.ToString().ToUpperInvariant())); if (selection.IsList) { if (selection.IsLeaf) { - arguments[2] = new ArgumentNode("type", new EnumValueNode("LEAF_LIST")); + arguments[index++] = new ArgumentNode("type", new EnumValueNode("LEAF_LIST")); } else { - arguments[2] = new ArgumentNode("type", new EnumValueNode("COMPOSITE_LIST")); + arguments[index++] = new ArgumentNode("type", new EnumValueNode("COMPOSITE_LIST")); } } else if (selection.Type.IsCompositeType()) { - arguments[2] = new ArgumentNode("type", new EnumValueNode("COMPOSITE")); + arguments[index++] = new ArgumentNode("type", new EnumValueNode("COMPOSITE")); } else if (selection.IsLeaf) { - arguments[2] = new ArgumentNode("type", new EnumValueNode("LEAF")); + arguments[index++] = new ArgumentNode("type", new EnumValueNode("LEAF")); } if (selection.IsInternal) { - arguments[3] = new ArgumentNode("internal", BooleanValueNode.True); + arguments[index++] = new ArgumentNode("internal", BooleanValueNode.True); + } + + if (selection.HasDeferUsage) + { + arguments[index++] = new ArgumentNode("isDeferred", BooleanValueNode.True); } return new DirectiveNode("__execute", arguments); diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs index 7aacaaff091..2483ab98ad4 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs @@ -15,6 +15,8 @@ public sealed class Selection : ISelection, IFeatureProvider private readonly FieldSelectionNode[] _syntaxNodes; private readonly ulong[] _includeFlags; private readonly byte[] _utf8ResponseName; + private readonly DeferUsage[] _deferUsage; + private readonly ulong _deferMask; private Flags _flags; private SelectionSet? _declaringSelectionSet; @@ -24,6 +26,8 @@ internal Selection( ObjectField field, FieldSelectionNode[] syntaxNodes, ulong[] includeFlags, + DeferUsage[]? deferUsage = null, + ulong deferMask = 0, bool isInternal = false, ArgumentMap? arguments = null, FieldDelegate? resolverPipeline = null, @@ -50,6 +54,8 @@ internal Selection( hasPureResolver: pureResolver is not null); _syntaxNodes = syntaxNodes; _includeFlags = includeFlags; + _deferUsage = deferUsage ?? []; + _deferMask = deferMask; _flags = isInternal ? Flags.Internal : Flags.None; if (field.Type.NamedType().IsLeafType()) @@ -73,6 +79,8 @@ private Selection( IType type, FieldSelectionNode[] syntaxNodes, ulong[] includeFlags, + DeferUsage[] deferUsage, + ulong deferMask, Flags flags, ArgumentMap? arguments, SelectionExecutionStrategy strategy, @@ -89,6 +97,8 @@ private Selection( Strategy = strategy; _syntaxNodes = syntaxNodes; _includeFlags = includeFlags; + _deferUsage = deferUsage; + _deferMask = deferMask; _flags = flags; _utf8ResponseName = utf8ResponseName; } @@ -266,6 +276,15 @@ public bool IsIncluded(ulong includeFlags) return false; } + /// + /// Gets a value indicating whether this selection has any defer usage. + /// + internal bool HasDeferUsage => _deferUsage.Length > 0; + + /// + public bool IsDeferred(ulong deferFlags) + => _deferMask != 0 && (_deferMask & deferFlags) == _deferMask; + public Selection WithField(ObjectField field) { ArgumentNullException.ThrowIfNull(field); @@ -278,6 +297,8 @@ public Selection WithField(ObjectField field) field.Type, _syntaxNodes, _includeFlags, + _deferUsage, + _deferMask, _flags, Arguments, Strategy, @@ -301,6 +322,8 @@ public Selection WithType(IType type) type, _syntaxNodes, _includeFlags, + _deferUsage, + _deferMask, _flags, Arguments, Strategy, diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs index d545b1b4805..10cd0e670b6 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs @@ -23,7 +23,8 @@ internal SelectionSet( SelectionPath path, IObjectTypeDefinition type, Selection[] selections, - bool isConditional) + bool isConditional, + bool hasDeferredSelections = false) { ArgumentNullException.ThrowIfNull(selections); @@ -31,6 +32,12 @@ internal SelectionSet( Path = path; Type = type; _flags = isConditional ? Flags.Conditional : Flags.None; + + if (hasDeferredSelections) + { + _flags |= Flags.HasDeferredSelections; + } + _selections = selections; _responseNameLookup = _selections.ToFrozenDictionary(t => t.ResponseName); _utf8ResponseNameLookup = SelectionLookup.Create(this); @@ -51,6 +58,9 @@ internal SelectionSet( /// public bool IsConditional => (_flags & Flags.Conditional) == Flags.Conditional; + /// + public bool HasDeferredSelections => (_flags & Flags.HasDeferredSelections) == Flags.HasDeferredSelections; + /// /// Gets the type context of this selection set. /// @@ -122,7 +132,8 @@ private enum Flags { None = 0, Conditional = 1, - Sealed = 2 + Sealed = 2, + HasDeferredSelections = 4 } public override string ToString() diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs index 07244b911ab..c1308c74ba0 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs @@ -9,7 +9,7 @@ internal sealed partial class ResolverTask(ObjectPool objectPool) { private readonly MiddlewareContext _context = new(); private readonly List _taskBuffer = []; - private readonly Dictionary _args = new(StringComparer.Ordinal); + private readonly Dictionary _args = [with(StringComparer.Ordinal)]; private OperationContext _operationContext = null!; private Selection _selection = null!; private ExecutionTaskStatus _completionStatus = ExecutionTaskStatus.Completed; @@ -19,6 +19,8 @@ internal sealed partial class ResolverTask(ObjectPool objectPool) /// public uint Id { get; set; } + public uint DeferGroupId { get; set; } + /// /// Gets access to the resolver context for this task. /// diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/OperationCompilerTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Processing/OperationCompilerTests.cs index 67dda972039..9bf5bf5f017 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/OperationCompilerTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/OperationCompilerTests.cs @@ -658,6 +658,547 @@ fragment Foo on Droid { MatchSnapshot(document, operation); } + // ------------------------------------------------------------------ + // Defer deduplication tests + // Based on graphql-spec PR 1110 (incremental delivery) deduplication + // semantics and the graphql-js reference implementation tests. + // ------------------------------------------------------------------ + + [Fact] + public void Defer_Inline_Fragment_Deduplication_Non_Deferred_Wins() + { + // arrange + // A field that appears both inside and outside @defer should NOT + // be marked as deferred. The non-deferred usage wins per spec + // GetFilteredDeferUsageSet: if any fieldDetails has no deferUsage, + // the filtered set is cleared. + var schema = SchemaBuilder.New() + .AddStarWarsTypes() + .Create(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero(episode: EMPIRE) { + name + ... @defer { + name + id + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public void Defer_Fragment_Spread_Deferred_And_Non_Deferred() + { + // arrange + // Same named fragment used both with @defer and without. + // Non-deferred usage wins regardless of order. Per spec, + // if a fragment spread is visited without @defer first, + // the name is added to visitedFragments and the deferred + // spread is skipped. If deferred is first, the non-deferred + // revisit overrides. + var schema = SchemaBuilder.New() + .AddStarWarsTypes() + .Create(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero(episode: EMPIRE) { + ...CharFields @defer(label: "DeferCharFields") + ...CharFields + } + } + + fragment CharFields on Character { + name + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public void Defer_Fragment_Spread_Non_Deferred_Then_Deferred() + { + // arrange + // Same fragment spread used non-deferred first, then deferred. + // Non-deferred usage wins per spec. + var schema = SchemaBuilder.New() + .AddStarWarsTypes() + .Create(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero(episode: EMPIRE) { + ...CharFields + ...CharFields @defer(label: "DeferCharFields") + } + } + + fragment CharFields on Character { + name + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public void Defer_Nested_Inline_Fragments() + { + // arrange + // Two levels of nested @defer. Both fields should be deferred. + // Per spec, nested defers create a parent chain of DeferUsages. + var schema = SchemaBuilder.New() + .AddStarWarsTypes() + .Create(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero(episode: EMPIRE) { + ... @defer { + name + ... @defer { + id + } + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public void Defer_Nested_Field_Overlap_Parent_And_Child() + { + // arrange + // Field "name" appears in both parent and child @defer. + // Per spec GetFilteredDeferUsageSet, when a deferUsage's + // ancestor is also in the set, the child is removed. + // The field is still deferred (delivered with the parent defer). + var schema = SchemaBuilder.New() + .AddStarWarsTypes() + .Create(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero(episode: EMPIRE) { + ... @defer { + name + ... @defer { + name + id + } + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public void Defer_Multiple_Nested_Same_Fragment() + { + // arrange + // Multiple nested defers referencing the same fragment. + // Demonstrates deduplication across multiple defer levels. + // Per graphql-js test: "Can deduplicate multiple defers on + // the same object" + var schema = SchemaBuilder.New() + .AddStarWarsTypes() + .Create(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero(episode: EMPIRE) { + ... @defer { + ...CharFields + ... @defer { + ...CharFields + ... @defer { + ...CharFields + } + } + } + } + } + + fragment CharFields on Character { + name + id + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public void Defer_If_False_Not_Deferred() + { + // arrange + // @defer(if: false) should not produce deferred selections. + // Per spec, when if argument is false, the defer directive + // is ignored and no DeferUsage is created. + var schema = SchemaBuilder.New() + .AddStarWarsTypes() + .Create(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero(episode: EMPIRE) { + ... @defer(if: false) { + name + id + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public async Task Defer_Different_Branches_Overlapping_Fields() + { + // arrange + // Fields present in both the initial payload and a deferred + // fragment. Only fields unique to the defer should be deferred. + // Mirrors graphql-js test: "Deduplicates fields present in the + // initial payload" + var schema = + await new ServiceCollection() + .AddGraphQLServer() + .AddDocumentFromString( + """ + type Query { + foo: Foo + } + + type Foo { + bar: Bar + baz: String + } + + type Bar { + a: String + b: String + } + """) + .UseField(next => next) + .BuildSchemaAsync(); + + var document = Utf8GraphQLParser.Parse( + """ + { + foo { + bar { + a + } + ... @defer { + bar { + b + } + baz + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public async Task Defer_Different_Branches_Non_Overlapping_Levels() + { + // arrange + // Two defers at different tree levels with overlapping field + // paths. Mirrors graphql-js test: "Deduplicate fields with + // deferred fragments in different branches at multiple + // non-overlapping levels" + var schema = + await new ServiceCollection() + .AddGraphQLServer() + .AddDocumentFromString( + """ + type Query { + a: A + g: G + } + + type A { + b: B + } + + type B { + c: C + e: E + } + + type C { + d: String + } + + type E { + f: String + } + + type G { + h: String + } + """) + .UseField(next => next) + .BuildSchemaAsync(); + + var document = Utf8GraphQLParser.Parse( + """ + { + a { + b { + c { + d + } + ... @defer { + e { + f + } + } + } + } + ... @defer { + a { + b { + e { + f + } + } + } + g { + h + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public async Task Defer_Nested_With_Parent_Field_Deduplication() + { + // arrange + // When a field appears in a parent @defer and also in a nested + // child @defer, the field should only be delivered with the + // parent defer. Mirrors graphql-js test: "Deduplicates fields + // present in a parent defer payload" + var schema = + await new ServiceCollection() + .AddGraphQLServer() + .AddDocumentFromString( + """ + type Query { + hero: Hero + } + + type Hero { + nestedObject: NestedObject + } + + type NestedObject { + deeperObject: DeeperObject + } + + type DeeperObject { + foo: String + bar: String + } + """) + .UseField(next => next) + .BuildSchemaAsync(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero { + ... @defer { + nestedObject { + deeperObject { + foo + ... @defer { + foo + bar + } + } + } + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public async Task Defer_Multiple_Levels_Field_Deduplication() + { + // arrange + // Deduplication across three levels: initial has foo, first + // defer adds bar, second defer adds baz, third defer adds bak. + // Mirrors graphql-js test: "Deduplicates fields with deferred + // fragments at multiple levels" + var schema = + await new ServiceCollection() + .AddGraphQLServer() + .AddDocumentFromString( + """ + type Query { + hero: Hero + } + + type Hero { + nestedObject: NestedObject + } + + type NestedObject { + deeperObject: DeeperObject + } + + type DeeperObject { + foo: String + bar: String + baz: String + bak: String + } + """) + .UseField(next => next) + .BuildSchemaAsync(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero { + nestedObject { + deeperObject { + foo + } + } + ... @defer { + nestedObject { + deeperObject { + foo + bar + } + ... @defer { + deeperObject { + foo + bar + baz + ... @defer { + foo + bar + baz + bak + } + } + } + } + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + [Fact] public void Reuse_Selection() { diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Crypto_List_Test.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Crypto_List_Test.snap index 80ebb631801..c02363aecf7 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Crypto_List_Test.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Crypto_List_Test.snap @@ -149,42 +149,42 @@ query DashboardContainerQuery { } } } - gainers: assets(first: 5, where: { price: { change24Hour: { gt: 0 } } }, order: { price: { change24Hour: DESC } }) @__execute(id: 4, kind: DEFAULT, type: COMPOSITE) { + gainers: assets(first: 5, where: { price: { change24Hour: { gt: 0 } } }, order: { price: { change24Hour: DESC } }) @__execute(id: 4, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { ... on AssetsConnection { - nodes @__execute(id: 40, kind: DEFAULT, type: COMPOSITE_LIST) { + nodes @__execute(id: 40, kind: DEFAULT, type: COMPOSITE_LIST, isDeferred: true) { ... on Asset { - id @__execute(id: 42, kind: DEFAULT, type: LEAF) - symbol @__execute(id: 43, kind: DEFAULT, type: LEAF) - name @__execute(id: 44, kind: DEFAULT, type: LEAF) - imageUrl @__execute(id: 45, kind: DEFAULT, type: LEAF) - isInWatchlist @__execute(id: 46, kind: DEFAULT, type: LEAF) - price @__execute(id: 47, kind: DEFAULT, type: COMPOSITE) { + id @__execute(id: 42, kind: DEFAULT, type: LEAF, isDeferred: true) + symbol @__execute(id: 43, kind: DEFAULT, type: LEAF, isDeferred: true) + name @__execute(id: 44, kind: DEFAULT, type: LEAF, isDeferred: true) + imageUrl @__execute(id: 45, kind: DEFAULT, type: LEAF, isDeferred: true) + isInWatchlist @__execute(id: 46, kind: DEFAULT, type: LEAF, isDeferred: true) + price @__execute(id: 47, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { ... on AssetPrice { - currency @__execute(id: 49, kind: DEFAULT, type: LEAF) - lastPrice @__execute(id: 50, kind: DEFAULT, type: LEAF) - change24Hour @__execute(id: 51, kind: DEFAULT, type: LEAF) - id @__execute(id: 52, kind: DEFAULT, type: LEAF) + currency @__execute(id: 49, kind: DEFAULT, type: LEAF, isDeferred: true) + lastPrice @__execute(id: 50, kind: DEFAULT, type: LEAF, isDeferred: true) + change24Hour @__execute(id: 51, kind: DEFAULT, type: LEAF, isDeferred: true) + id @__execute(id: 52, kind: DEFAULT, type: LEAF, isDeferred: true) } } } } } } - losers: assets(first: 5, where: { price: { change24Hour: { lt: 0 } } }, order: { price: { change24Hour: ASC } }) @__execute(id: 5, kind: DEFAULT, type: COMPOSITE) { + losers: assets(first: 5, where: { price: { change24Hour: { lt: 0 } } }, order: { price: { change24Hour: ASC } }) @__execute(id: 5, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { ... on AssetsConnection { - nodes @__execute(id: 54, kind: DEFAULT, type: COMPOSITE_LIST) { + nodes @__execute(id: 54, kind: DEFAULT, type: COMPOSITE_LIST, isDeferred: true) { ... on Asset { - id @__execute(id: 56, kind: DEFAULT, type: LEAF) - symbol @__execute(id: 57, kind: DEFAULT, type: LEAF) - name @__execute(id: 58, kind: DEFAULT, type: LEAF) - imageUrl @__execute(id: 59, kind: DEFAULT, type: LEAF) - isInWatchlist @__execute(id: 60, kind: DEFAULT, type: LEAF) - price @__execute(id: 61, kind: DEFAULT, type: COMPOSITE) { + id @__execute(id: 56, kind: DEFAULT, type: LEAF, isDeferred: true) + symbol @__execute(id: 57, kind: DEFAULT, type: LEAF, isDeferred: true) + name @__execute(id: 58, kind: DEFAULT, type: LEAF, isDeferred: true) + imageUrl @__execute(id: 59, kind: DEFAULT, type: LEAF, isDeferred: true) + isInWatchlist @__execute(id: 60, kind: DEFAULT, type: LEAF, isDeferred: true) + price @__execute(id: 61, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { ... on AssetPrice { - currency @__execute(id: 63, kind: DEFAULT, type: LEAF) - lastPrice @__execute(id: 64, kind: DEFAULT, type: LEAF) - change24Hour @__execute(id: 65, kind: DEFAULT, type: LEAF) - id @__execute(id: 66, kind: DEFAULT, type: LEAF) + currency @__execute(id: 63, kind: DEFAULT, type: LEAF, isDeferred: true) + lastPrice @__execute(id: 64, kind: DEFAULT, type: LEAF, isDeferred: true) + change24Hour @__execute(id: 65, kind: DEFAULT, type: LEAF, isDeferred: true) + id @__execute(id: 66, kind: DEFAULT, type: LEAF, isDeferred: true) } } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Different_Branches_Non_Overlapping_Levels.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Different_Branches_Non_Overlapping_Levels.snap new file mode 100644 index 00000000000..f739813dc34 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Different_Branches_Non_Overlapping_Levels.snap @@ -0,0 +1,56 @@ +{ + a { + b { + c { + d + } + ... @defer { + e { + f + } + } + } + } + ... @defer { + a { + b { + e { + f + } + } + } + g { + h + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + a @__execute(id: 2, kind: DEFAULT, type: COMPOSITE) { + ... on A { + b @__execute(id: 5, kind: DEFAULT, type: COMPOSITE) { + ... on B { + c @__execute(id: 7, kind: DEFAULT, type: COMPOSITE) { + ... on C { + d @__execute(id: 10, kind: DEFAULT, type: LEAF) + } + } + e @__execute(id: 8, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { + ... on E { + f @__execute(id: 12, kind: DEFAULT, type: LEAF, isDeferred: true) + } + } + } + } + } + } + g @__execute(id: 3, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { + ... on G { + h @__execute(id: 14, kind: DEFAULT, type: LEAF, isDeferred: true) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Different_Branches_Overlapping_Fields.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Different_Branches_Overlapping_Fields.snap new file mode 100644 index 00000000000..539c8571e4c --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Different_Branches_Overlapping_Fields.snap @@ -0,0 +1,31 @@ +{ + foo { + bar { + a + } + ... @defer { + bar { + b + } + baz + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + foo @__execute(id: 2, kind: DEFAULT, type: COMPOSITE) { + ... on Foo { + bar @__execute(id: 4, kind: DEFAULT, type: COMPOSITE) { + ... on Bar { + a @__execute(id: 7, kind: DEFAULT, type: LEAF) + b @__execute(id: 8, kind: DEFAULT, type: LEAF, isDeferred: true) + } + } + baz @__execute(id: 5, kind: DEFAULT, type: LEAF, isDeferred: true) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread.snap index 266464b86b5..0f170598cdc 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread.snap @@ -19,7 +19,7 @@ fragment Foo on Droid { } ... on Droid { name @__execute(id: 6, kind: PURE, type: LEAF) - id @__execute(id: 7, kind: PURE, type: LEAF) + id @__execute(id: 7, kind: PURE, type: LEAF, isDeferred: true) } } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread_Deferred_And_Non_Deferred.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread_Deferred_And_Non_Deferred.snap new file mode 100644 index 00000000000..a0d1cc72cd5 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread_Deferred_And_Non_Deferred.snap @@ -0,0 +1,25 @@ +{ + hero(episode: EMPIRE) { + ... CharFields @defer(label: "DeferCharFields") + ... CharFields + } +} + +fragment CharFields on Character { + name +} + +--------------------------------------------------------- + +{ + ... on Query { + hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { + ... on Human { + name @__execute(id: 4, kind: PURE, type: LEAF) + } + ... on Droid { + name @__execute(id: 6, kind: PURE, type: LEAF) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread_Non_Deferred_Then_Deferred.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread_Non_Deferred_Then_Deferred.snap new file mode 100644 index 00000000000..8e3eeba6e7a --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread_Non_Deferred_Then_Deferred.snap @@ -0,0 +1,25 @@ +{ + hero(episode: EMPIRE) { + ... CharFields + ... CharFields @defer(label: "DeferCharFields") + } +} + +fragment CharFields on Character { + name +} + +--------------------------------------------------------- + +{ + ... on Query { + hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { + ... on Human { + name @__execute(id: 4, kind: PURE, type: LEAF) + } + ... on Droid { + name @__execute(id: 6, kind: PURE, type: LEAF) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_If_False_Not_Deferred.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_If_False_Not_Deferred.snap new file mode 100644 index 00000000000..4be263ff043 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_If_False_Not_Deferred.snap @@ -0,0 +1,25 @@ +{ + hero(episode: EMPIRE) { + ... @defer(if: false) { + name + id + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { + ... on Human { + name @__execute(id: 4, kind: PURE, type: LEAF) + id @__execute(id: 5, kind: PURE, type: LEAF) + } + ... on Droid { + name @__execute(id: 7, kind: PURE, type: LEAF) + id @__execute(id: 8, kind: PURE, type: LEAF) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Inline_Fragment.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Inline_Fragment.snap index 5a04edfda5d..720a4913373 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Inline_Fragment.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Inline_Fragment.snap @@ -14,11 +14,11 @@ hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { ... on Human { name @__execute(id: 4, kind: PURE, type: LEAF) - id @__execute(id: 5, kind: PURE, type: LEAF) + id @__execute(id: 5, kind: PURE, type: LEAF, isDeferred: true) } ... on Droid { name @__execute(id: 7, kind: PURE, type: LEAF) - id @__execute(id: 8, kind: PURE, type: LEAF) + id @__execute(id: 8, kind: PURE, type: LEAF, isDeferred: true) } } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Inline_Fragment_Deduplication_Non_Deferred_Wins.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Inline_Fragment_Deduplication_Non_Deferred_Wins.snap new file mode 100644 index 00000000000..1878071767a --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Inline_Fragment_Deduplication_Non_Deferred_Wins.snap @@ -0,0 +1,26 @@ +{ + hero(episode: EMPIRE) { + name + ... @defer { + name + id + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { + ... on Human { + name @__execute(id: 4, kind: PURE, type: LEAF) + id @__execute(id: 5, kind: PURE, type: LEAF, isDeferred: true) + } + ... on Droid { + name @__execute(id: 7, kind: PURE, type: LEAF) + id @__execute(id: 8, kind: PURE, type: LEAF, isDeferred: true) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Multiple_Levels_Field_Deduplication.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Multiple_Levels_Field_Deduplication.snap new file mode 100644 index 00000000000..639b466ae65 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Multiple_Levels_Field_Deduplication.snap @@ -0,0 +1,53 @@ +{ + hero { + nestedObject { + deeperObject { + foo + } + } + ... @defer { + nestedObject { + deeperObject { + foo + bar + } + ... @defer { + deeperObject { + foo + bar + baz + ... @defer { + foo + bar + baz + bak + } + } + } + } + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + hero @__execute(id: 2, kind: DEFAULT, type: COMPOSITE) { + ... on Hero { + nestedObject @__execute(id: 4, kind: DEFAULT, type: COMPOSITE) { + ... on NestedObject { + deeperObject @__execute(id: 6, kind: DEFAULT, type: COMPOSITE) { + ... on DeeperObject { + foo @__execute(id: 8, kind: DEFAULT, type: LEAF) + bar @__execute(id: 9, kind: DEFAULT, type: LEAF, isDeferred: true) + baz @__execute(id: 10, kind: DEFAULT, type: LEAF, isDeferred: true) + bak @__execute(id: 11, kind: DEFAULT, type: LEAF, isDeferred: true) + } + } + } + } + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Multiple_Nested_Same_Fragment.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Multiple_Nested_Same_Fragment.snap new file mode 100644 index 00000000000..ed6e98d752b --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Multiple_Nested_Same_Fragment.snap @@ -0,0 +1,35 @@ +{ + hero(episode: EMPIRE) { + ... @defer { + ... CharFields + ... @defer { + ... CharFields + ... @defer { + ... CharFields + } + } + } + } +} + +fragment CharFields on Character { + name + id +} + +--------------------------------------------------------- + +{ + ... on Query { + hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { + ... on Human { + name @__execute(id: 4, kind: PURE, type: LEAF, isDeferred: true) + id @__execute(id: 5, kind: PURE, type: LEAF, isDeferred: true) + } + ... on Droid { + name @__execute(id: 7, kind: PURE, type: LEAF, isDeferred: true) + id @__execute(id: 8, kind: PURE, type: LEAF, isDeferred: true) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_Field_Overlap_Parent_And_Child.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_Field_Overlap_Parent_And_Child.snap new file mode 100644 index 00000000000..ddb19f2faa8 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_Field_Overlap_Parent_And_Child.snap @@ -0,0 +1,28 @@ +{ + hero(episode: EMPIRE) { + ... @defer { + name + ... @defer { + name + id + } + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { + ... on Human { + name @__execute(id: 4, kind: PURE, type: LEAF, isDeferred: true) + id @__execute(id: 5, kind: PURE, type: LEAF, isDeferred: true) + } + ... on Droid { + name @__execute(id: 7, kind: PURE, type: LEAF, isDeferred: true) + id @__execute(id: 8, kind: PURE, type: LEAF, isDeferred: true) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_Inline_Fragments.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_Inline_Fragments.snap new file mode 100644 index 00000000000..bbe25fd79f4 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_Inline_Fragments.snap @@ -0,0 +1,27 @@ +{ + hero(episode: EMPIRE) { + ... @defer { + name + ... @defer { + id + } + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { + ... on Human { + name @__execute(id: 4, kind: PURE, type: LEAF, isDeferred: true) + id @__execute(id: 5, kind: PURE, type: LEAF, isDeferred: true) + } + ... on Droid { + name @__execute(id: 7, kind: PURE, type: LEAF, isDeferred: true) + id @__execute(id: 8, kind: PURE, type: LEAF, isDeferred: true) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_With_Parent_Field_Deduplication.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_With_Parent_Field_Deduplication.snap new file mode 100644 index 00000000000..694472fbc5d --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_With_Parent_Field_Deduplication.snap @@ -0,0 +1,36 @@ +{ + hero { + ... @defer { + nestedObject { + deeperObject { + foo + ... @defer { + foo + bar + } + } + } + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + hero @__execute(id: 2, kind: DEFAULT, type: COMPOSITE) { + ... on Hero { + nestedObject @__execute(id: 4, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { + ... on NestedObject { + deeperObject @__execute(id: 6, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { + ... on DeeperObject { + foo @__execute(id: 8, kind: DEFAULT, type: LEAF, isDeferred: true) + bar @__execute(id: 9, kind: DEFAULT, type: LEAF, isDeferred: true) + } + } + } + } + } + } + } +} From 491ad28b82f0e67c0a73bd0a5c1e7a731178b777 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 6 Feb 2026 20:59:28 +0100 Subject: [PATCH 02/46] Started work on integrating defer into the execution engine --- .../Execution/CompletedResult.cs | 4 +- .../Execution/IIncrementalObjectResult.cs | 47 +++++- .../Execution/IIncrementalResult.cs | 6 +- .../Execution/PendingResult.cs | 4 +- .../Processing/DeferExecutionCoordinator.cs | 155 +++++++++++++++--- 5 files changed, 183 insertions(+), 33 deletions(-) diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/CompletedResult.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/CompletedResult.cs index a036a399afc..75e3c092e5b 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/CompletedResult.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/CompletedResult.cs @@ -9,12 +9,12 @@ namespace HotChocolate.Execution; /// Field errors that caused the incremental delivery to fail due to error bubbling above the incremental result's path. /// When present, indicates the delivery has failed. /// -public sealed record CompletedResult(uint Id, IReadOnlyList? Errors = null) +public sealed record CompletedResult(int Id, IReadOnlyList? Errors = null) { /// /// Gets the request unique pending data identifier that matches a prior pending result. /// - public uint Id { get; init; } = Id; + public int Id { get; init; } = Id; /// /// Gets field errors that caused the incremental delivery to fail due to error bubbling diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalObjectResult.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalObjectResult.cs index 8e04e2dc878..3163c879495 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalObjectResult.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalObjectResult.cs @@ -1,19 +1,60 @@ +using System.Collections.Immutable; + namespace HotChocolate.Execution; /// /// Represents an incremental result that delivers additional fields for a @defer directive. /// -public interface IIncrementalObjectResult : IIncrementalResult +public sealed class IncrementalObjectResult : IIncrementalResult { + /// + /// Initializes a new instance of . + /// + /// + /// The unique identifier that correlates this result with its pending entry. + /// + /// + /// The GraphQL errors that occurred while resolving the deferred fragment. + /// + /// + /// The sub-path to concatenate with the pending result's path, or null + /// if the path is the same as the pending result's path. + /// + /// + /// The additional response fields to merge into the deferred fragment location. + /// + public IncrementalObjectResult( + int id, + ImmutableList? errors = null, + Path? subPath = null, + OperationResultData? data = null) + { + Id = id; + Errors = errors ?? []; + SubPath = subPath; + Data = data; + } + + /// + /// Gets the unique identifier that correlates this incremental result with + /// its corresponding pending entry. + /// + public int Id { get; } + + /// + /// Gets the GraphQL errors that occurred while resolving the deferred fragment. + /// + public ImmutableList Errors { get; } + /// /// Gets the sub-path that is concatenated with the pending result's path to determine /// the final path for this incremental data. When null, the path is the same /// as the pending result's path. /// - Path? SubPath { get; } + public Path? SubPath { get; } /// /// Gets the additional response fields to merge into the deferred fragment location. /// - object? Data { get; } + public OperationResultData? Data { get; } } diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalResult.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalResult.cs index 52861205a19..c31c72bad6f 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalResult.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalResult.cs @@ -1,3 +1,5 @@ +using System.Collections.Immutable; + namespace HotChocolate.Execution; /// @@ -8,11 +10,11 @@ public interface IIncrementalResult /// /// Gets the request unique pending data identifier that matches a prior pending result. /// - uint Id { get; } + int Id { get; } /// /// Gets field errors that occurred during execution of this incremental result. /// Only includes errors that did not bubble above the incremental result's path. /// - IReadOnlyList? Errors { get; } + ImmutableList Errors { get; } } diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/PendingResult.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/PendingResult.cs index 38c0fab5469..acff8018762 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/PendingResult.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/PendingResult.cs @@ -10,12 +10,12 @@ namespace HotChocolate.Execution; /// For @defer: indicates where the deferred fragment fields will be added. /// /// The label from the @defer or @stream directive's label argument, if present. -public sealed record PendingResult(uint Id, Path Path, string? Label = null) +public sealed record PendingResult(int Id, Path Path, string? Label = null) { /// /// Gets the request unique pending data identifier. /// - public uint Id { get; init; } = Id; + public int Id { get; init; } = Id; /// /// Gets the path in the response where the incremental data will be delivered. diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs index 0579954de9d..1ee06ae244d 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs @@ -1,56 +1,163 @@ using System.Collections.Concurrent; -using HotChocolate.Text.Json; +using System.Collections.Immutable; +using System.Threading.Channels; namespace HotChocolate.Execution.Processing; internal sealed class DeferExecutionCoordinator { - private const int InitialResultId = -1; + public const int MainBranchId = -1; private readonly object _sync = new(); - private readonly ConcurrentDictionary _resultIds = new(); - private readonly ConcurrentDictionary _resultInfoLookup = new(); - private readonly ConcurrentDictionary> _branches = new(); - private readonly ConcurrentDictionary _completed = new(); + private readonly ConcurrentDictionary _branchIdLookup = new(); + private readonly ConcurrentDictionary _branchInfoLookup = new(); + private readonly ConcurrentDictionary> _branches = new(); + private readonly ConcurrentDictionary _completed = new(); + private readonly HashSet _delivered = []; + private readonly Channel _resultSignal = Channel.CreateUnbounded(); private int _nextId; - public int Branch(ResultDocument parent, Path path, DeferUsage deferUsage) + public int Branch(int currentBranchId, Path path, DeferUsage deferUsage) { - var resultInfo = new DeferredResultInfo(path, deferUsage); + var resultInfo = new DeferredBranchInfo(path, deferUsage); - if (!_resultIds.TryGetValue(resultInfo, out var resultId)) + if (!_branchIdLookup.TryGetValue(resultInfo, out var newBranchId)) { lock (_sync) { - if (!_resultIds.TryGetValue(resultInfo, out resultId)) + if (!_branchIdLookup.TryGetValue(resultInfo, out newBranchId)) { - resultId = _nextId++; - GetBranchesUnsafe(parent).Add(resultId); - _resultInfoLookup.TryAdd(resultId, resultInfo); - _resultIds.TryAdd(resultInfo, resultId); + newBranchId = _nextId++; + GetBranchesUnsafe(currentBranchId).Add(newBranchId); + _branchInfoLookup.TryAdd(newBranchId, resultInfo); + _branchIdLookup.TryAdd(resultInfo, newBranchId); } } } - return resultId; + return newBranchId; } - public void EnqueueResult(ResultDocument result) - => _completed.TryAdd(InitialResultId, result); + public void EnqueueResult(OperationResult result) + { + lock (_sync) + { + ComposeAndDeliver(MainBranchId, result); + } + } + + public void EnqueueResult(OperationResult result, int branchId) + { + lock (_sync) + { + _completed.TryAdd(branchId, result); + + if (IsParentDelivered(branchId) + && _completed.TryRemove(branchId, out var readyResult)) + { + ComposeAndDeliver(branchId, readyResult); + } + } + } + + public void Complete() + { + _resultSignal.Writer.TryComplete(); + } + + public IAsyncEnumerable ReadResultsAsync( + CancellationToken cancellationToken = default) + => _resultSignal.Reader.ReadAllAsync(cancellationToken); + + private void ComposeAndDeliver(int branchId, OperationResult result) + { + var childBranches = GetBranchesUnsafe(branchId); + + if (childBranches.Count > 0) + { + var pendingBuilder = ImmutableList.CreateBuilder(); + var incrementalBuilder = ImmutableList.CreateBuilder(); + var completedBuilder = ImmutableList.CreateBuilder(); + var toProcess = new Queue(); + + foreach (var childId in childBranches) + { + var childInfo = _branchInfoLookup[childId]; + + pendingBuilder.Add( + new PendingResult(childId, childInfo.Path, childInfo.Group.Label)); + + if (_completed.TryRemove(childId, out var childResult)) + { + incrementalBuilder.Add( + new IncrementalObjectResult( + childId, + childResult.Errors, + subPath: null, + childResult.Data)); + + completedBuilder.Add(new CompletedResult(childId)); + _delivered.Add(childId); + toProcess.Enqueue(childId); + } + } + + while (toProcess.TryDequeue(out var parentId)) + { + foreach (var grandchildId in GetBranchesUnsafe(parentId)) + { + var info = _branchInfoLookup[grandchildId]; + + pendingBuilder.Add( + new PendingResult(grandchildId, info.Path, info.Group.Label)); + + if (_completed.TryRemove(grandchildId, out var gcResult)) + { + incrementalBuilder.Add( + new IncrementalObjectResult( + grandchildId, + gcResult.Errors, + subPath: null, + gcResult.Data)); + + completedBuilder.Add(new CompletedResult(grandchildId)); + _delivered.Add(grandchildId); + toProcess.Enqueue(grandchildId); + } + } + } - public void EnqueueResult(ResultDocument result, int resultId) - => _completed.TryAdd(resultId, result); + result.Pending = pendingBuilder.ToImmutable(); + result.Incremental = incrementalBuilder.ToImmutable(); + result.Completed = completedBuilder.ToImmutable(); + } + + _delivered.Add(branchId); + _resultSignal.Writer.TryWrite(result); + } + + private bool IsParentDelivered(int branchId) + { + foreach (var (parentId, children) in _branches) + { + if (children.Contains(branchId)) + { + return _delivered.Contains(parentId); + } + } - private HashSet GetBranchesUnsafe(ResultDocument result) + return false; + } + + private HashSet GetBranchesUnsafe(int resultId) { - if (!_branches.TryGetValue(result, out var branches)) + if (!_branches.TryGetValue(resultId, out var branches)) { branches = []; - _branches.TryAdd(result, branches); + _branches.TryAdd(resultId, branches); } return branches; } - private readonly record struct DeferredResultInfo(Path Path, DeferUsage Group); + private readonly record struct DeferredBranchInfo(Path Path, DeferUsage Group); } - From 0f6c93506f0c440203716067033ba7793d53bd04 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 6 Feb 2026 23:37:14 +0100 Subject: [PATCH 03/46] Reworked the coordinator --- .../Processing/DeferExecutionCoordinator.cs | 187 +++++++++++++----- 1 file changed, 133 insertions(+), 54 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs index 1ee06ae244d..6cc17ec6bff 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using System.Collections.Immutable; using System.Threading.Channels; @@ -8,85 +7,150 @@ internal sealed class DeferExecutionCoordinator { public const int MainBranchId = -1; private readonly object _sync = new(); - private readonly ConcurrentDictionary _branchIdLookup = new(); - private readonly ConcurrentDictionary _branchInfoLookup = new(); - private readonly ConcurrentDictionary> _branches = new(); - private readonly ConcurrentDictionary _completed = new(); + private readonly Dictionary _branchIdLookup = []; + private readonly Dictionary _branchInfoLookup = []; + private readonly Dictionary> _branches = []; + private readonly Dictionary _completed = []; private readonly HashSet _delivered = []; - private readonly Channel _resultSignal = Channel.CreateUnbounded(); + private ImmutableList.Builder? _pendingBuilder; + private ImmutableList.Builder? _incrementalBuilder; + private ImmutableList.Builder? _completedBuilder; + private Queue? _processQueue; + private Channel _resultChannel = null!; + private volatile bool _hasBranches; + private int _pendingBranches; private int _nextId; + /// + /// Gets whether any deferred execution branches have been registered. + /// + public bool HasBranches => _hasBranches; + + /// + /// Registers a new deferred execution branch for the specified + /// and , returning a unique branch identifier. + /// If the branch was already registered, the existing identifier is returned. + /// public int Branch(int currentBranchId, Path path, DeferUsage deferUsage) { - var resultInfo = new DeferredBranchInfo(path, deferUsage); + var branchInfo = new DeferredBranchInfo(path, deferUsage, currentBranchId); - if (!_branchIdLookup.TryGetValue(resultInfo, out var newBranchId)) + lock (_sync) { - lock (_sync) + if (!_branchIdLookup.TryGetValue(branchInfo, out var newBranchId)) { - if (!_branchIdLookup.TryGetValue(resultInfo, out newBranchId)) - { - newBranchId = _nextId++; - GetBranchesUnsafe(currentBranchId).Add(newBranchId); - _branchInfoLookup.TryAdd(newBranchId, resultInfo); - _branchIdLookup.TryAdd(resultInfo, newBranchId); - } + newBranchId = _nextId++; + GetBranchesUnsafe(currentBranchId).Add(newBranchId); + _branchInfoLookup.Add(newBranchId, branchInfo); + _branchIdLookup.Add(branchInfo, newBranchId); + _hasBranches = true; + _pendingBranches++; } - } - return newBranchId; + return newBranchId; + } } + /// + /// Enqueues the initial (non-deferred) result for delivery. + /// Any already-completed child branches are folded in as incremental data. + /// public void EnqueueResult(OperationResult result) { lock (_sync) { - ComposeAndDeliver(MainBranchId, result); + ComposeAndDeliverUnsafe(MainBranchId, result); } } + /// + /// Enqueues a deferred result for the specified branch. + /// If the parent branch has already been delivered, the result is composed + /// and delivered immediately; otherwise it is stored until the parent is delivered. + /// public void EnqueueResult(OperationResult result, int branchId) { lock (_sync) { - _completed.TryAdd(branchId, result); + _completed[branchId] = result; - if (IsParentDelivered(branchId) - && _completed.TryRemove(branchId, out var readyResult)) + if (IsParentDeliveredUnsafe(branchId) + && _completed.Remove(branchId, out var readyResult)) { - ComposeAndDeliver(branchId, readyResult); + ComposeAndDeliverUnsafe(branchId, readyResult); } } } - public void Complete() + /// + /// Returns an async stream of composed operation results in delivery order. + /// The stream completes automatically when all branches have been delivered. + /// + public IAsyncEnumerable ReadResultsAsync( + CancellationToken cancellationToken = default) + => _resultChannel.Reader.ReadAllAsync(cancellationToken); + + /// + /// Initializes the coordinator for a new execution cycle. + /// Must be called before any other operations when leased from a pool. + /// + public void Initialize() { - _resultSignal.Writer.TryComplete(); + _resultChannel = Channel.CreateUnbounded( + new UnboundedChannelOptions + { + SingleWriter = true, + SingleReader = true + }); } - public IAsyncEnumerable ReadResultsAsync( - CancellationToken cancellationToken = default) - => _resultSignal.Reader.ReadAllAsync(cancellationToken); + /// + /// Resets the coordinator to its initial state so it can be reused. + /// + public void Reset() + { + _branchIdLookup.Clear(); + _branchInfoLookup.Clear(); + _branches.Clear(); + _completed.Clear(); + _delivered.Clear(); + _resultChannel = null!; + _pendingBuilder = null; + _incrementalBuilder = null; + _completedBuilder = null; + _processQueue = null; + _hasBranches = false; + _pendingBranches = 0; + _nextId = 0; + } - private void ComposeAndDeliver(int branchId, OperationResult result) + private void ComposeAndDeliverUnsafe(int branchId, OperationResult result) { var childBranches = GetBranchesUnsafe(branchId); if (childBranches.Count > 0) { - var pendingBuilder = ImmutableList.CreateBuilder(); - var incrementalBuilder = ImmutableList.CreateBuilder(); - var completedBuilder = ImmutableList.CreateBuilder(); - var toProcess = new Queue(); + var pendingBuilder = _pendingBuilder ??= ImmutableList.CreateBuilder(); + var incrementalBuilder = _incrementalBuilder ??= ImmutableList.CreateBuilder(); + var completedBuilder = _completedBuilder ??= ImmutableList.CreateBuilder(); + var processQueue = _processQueue ??= new Queue(); + + pendingBuilder.Clear(); + incrementalBuilder.Clear(); + completedBuilder.Clear(); + processQueue.Clear(); foreach (var childId in childBranches) { var childInfo = _branchInfoLookup[childId]; pendingBuilder.Add( - new PendingResult(childId, childInfo.Path, childInfo.Group.Label)); + new PendingResult( + childId, + childInfo.Path, + childInfo.Group.Label)); - if (_completed.TryRemove(childId, out var childResult)) + if (_completed.Remove(childId, out var childResult)) { incrementalBuilder.Add( new IncrementalObjectResult( @@ -97,20 +161,24 @@ private void ComposeAndDeliver(int branchId, OperationResult result) completedBuilder.Add(new CompletedResult(childId)); _delivered.Add(childId); - toProcess.Enqueue(childId); + _pendingBranches--; + processQueue.Enqueue(childId); } } - while (toProcess.TryDequeue(out var parentId)) + while (processQueue.TryDequeue(out var parentId)) { foreach (var grandchildId in GetBranchesUnsafe(parentId)) { var info = _branchInfoLookup[grandchildId]; pendingBuilder.Add( - new PendingResult(grandchildId, info.Path, info.Group.Label)); + new PendingResult( + grandchildId, + info.Path, + info.Group.Label)); - if (_completed.TryRemove(grandchildId, out var gcResult)) + if (_completed.Remove(grandchildId, out var gcResult)) { incrementalBuilder.Add( new IncrementalObjectResult( @@ -121,7 +189,8 @@ private void ComposeAndDeliver(int branchId, OperationResult result) completedBuilder.Add(new CompletedResult(grandchildId)); _delivered.Add(grandchildId); - toProcess.Enqueue(grandchildId); + _pendingBranches--; + processQueue.Enqueue(grandchildId); } } } @@ -132,32 +201,42 @@ private void ComposeAndDeliver(int branchId, OperationResult result) } _delivered.Add(branchId); - _resultSignal.Writer.TryWrite(result); - } - private bool IsParentDelivered(int branchId) - { - foreach (var (parentId, children) in _branches) + if (branchId != MainBranchId) { - if (children.Contains(branchId)) - { - return _delivered.Contains(parentId); - } + _pendingBranches--; } - return false; + _resultChannel.Writer.TryWrite(result); + + if (_delivered.Contains(MainBranchId) && _pendingBranches == 0) + { + _resultChannel.Writer.TryComplete(); + } } - private HashSet GetBranchesUnsafe(int resultId) + /// + /// Determines whether the parent of the specified branch has already + /// delivered its result to the response stream. + /// + private bool IsParentDeliveredUnsafe(int branchId) + => _branchInfoLookup.TryGetValue(branchId, out var info) + && _delivered.Contains(info.ParentBranchId); + + /// + /// Gets the child branches that were created from the execution branch + /// represented by the specified . + /// + private HashSet GetBranchesUnsafe(int branchId) { - if (!_branches.TryGetValue(resultId, out var branches)) + if (!_branches.TryGetValue(branchId, out var branches)) { branches = []; - _branches.TryAdd(resultId, branches); + _branches.Add(branchId, branches); } return branches; } - private readonly record struct DeferredBranchInfo(Path Path, DeferUsage Group); + private readonly record struct DeferredBranchInfo(Path Path, DeferUsage Group, int ParentBranchId); } From 28222e9704470f9211451b28cfc2708bc5060785 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 9 Feb 2026 17:15:02 +0100 Subject: [PATCH 04/46] Added more defer logic ... --- .../Execution/Tasks/ExecutionTask.cs | 3 + .../Execution/Tasks/IExecutionTask.cs | 5 + .../Execution/JsonValueFormatter.cs | 4 +- .../Processing/DeferUsageEnumerator.cs | 77 +++++ .../Processing/MiddlewareContext.Arguments.cs | 2 +- .../Processing/MiddlewareContext.Pooling.cs | 4 +- .../Processing/OperationContext.Execution.cs | 33 ++- .../Processing/OperationContext.Operation.cs | 5 + .../Processing/OperationContext.Pooling.cs | 22 +- .../Types/Execution/Processing/Selection.cs | 100 +++++++ .../Execution/Processing/Tasks/DeferTask.cs | 107 +++++++ .../Processing/Tasks/ResolverTask.Pooling.cs | 16 +- .../Processing/Tasks/ResolverTask.cs | 19 +- .../Processing/Tasks/ResolverTaskFactory.cs | 275 +++++++++++------- .../Processing/ValueCompletionContext.cs | 6 +- .../Types/Execution/Processing/WorkQueue.cs | 20 +- .../Processing/WorkScheduler.Execute.cs | 6 + .../Execution/Processing/WorkScheduler.cs | 7 +- .../Types/Text/Json/ResultDocument.DbRow.cs | 11 +- .../Types/Text/Json/ResultDocument.MetaDb.cs | 6 +- .../Types/Text/Json/ResultDocument.WriteTo.cs | 5 +- .../src/Types/Text/Json/ResultDocument.cs | 65 ++++- .../Core/src/Types/Text/Json/ResultElement.cs | 7 + 23 files changed, 660 insertions(+), 145 deletions(-) create mode 100644 src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsageEnumerator.cs create mode 100644 src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs diff --git a/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs b/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs index 90838533a80..376a538d3d9 100644 --- a/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs +++ b/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs @@ -42,6 +42,9 @@ public abstract class ExecutionTask : IExecutionTask /// public bool IsRegistered { get; set; } + /// + public abstract bool IsDeferred { get; } + /// public void BeginExecute(CancellationToken cancellationToken) { diff --git a/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs b/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs index 20014f2d15f..47ec9294cda 100644 --- a/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs +++ b/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs @@ -44,6 +44,11 @@ public interface IExecutionTask /// bool IsSerial { get; set; } + /// + /// Gets a value indicating whether this task is deprioritized. + /// + bool IsDeferred { get; } + /// /// Specifies if the task was fully registered with the scheduler. /// diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs index b88efa7b486..c445087c874 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs @@ -376,7 +376,7 @@ private static void WriteIncrementalItem( WriteErrors(writer, item.Errors, options, nullIgnoreCondition); } - if (item is IIncrementalObjectResult objectResult) + if (item is IncrementalObjectResult) { writer.WritePropertyName(Data); @@ -384,7 +384,7 @@ private static void WriteIncrementalItem( writer.WriteStartObject(); writer.WriteEndObject(); } - else if (item is IIncrementalListResult listResult) + else if (item is IIncrementalListResult) { writer.WritePropertyName(Items); diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsageEnumerator.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsageEnumerator.cs new file mode 100644 index 00000000000..d2391abe941 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsageEnumerator.cs @@ -0,0 +1,77 @@ +using System.Collections; +using System.Diagnostics; + +namespace HotChocolate.Execution.Processing; + +/// +/// An enumerable and enumerator for active defer usages. +/// +[DebuggerDisplay("{Current,nq}")] +public struct DeferUsageEnumerator : IEnumerable, IEnumerator +{ + private readonly DeferUsage[] _deferUsages; + private readonly ulong _deferFlags; + private int _index; + + internal DeferUsageEnumerator(DeferUsage[] deferUsages, ulong deferFlags) + { + _deferUsages = deferUsages; + _deferFlags = deferFlags; + _index = -1; + } + + /// + public DeferUsage Current => _deferUsages[_index]; + + /// + object IEnumerator.Current => Current; + + /// + /// Returns an enumerator that iterates through active defer usages. + /// + public DeferUsageEnumerator GetEnumerator() + { + var enumerator = this; + enumerator._index = -1; + return enumerator; + } + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + /// + public bool MoveNext() + { + var usages = _deferUsages; + var flags = _deferFlags; + + if (usages.Length == 0) + { + return false; + } + + while (++_index < usages.Length) + { + var usage = usages[_index]; + var bit = 1UL << usage.DeferConditionIndex; + + if ((flags & bit) != 0) + { + return true; + } + } + + return false; + } + + /// + public void Reset() => _index = -1; + + /// + public void Dispose() => _index = _deferUsages.Length; +} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Arguments.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Arguments.cs index 6a42cba2e78..ffe7d969be9 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Arguments.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Arguments.cs @@ -173,7 +173,7 @@ public ArgumentValue ReplaceArgument(string argumentName, ArgumentValue newArgum // copy the argument state. else { - mutableArguments = new Dictionary(Arguments); + mutableArguments = [with(Arguments)]; Arguments = mutableArguments; } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pooling.cs index 49bcf648d8f..47511c5b910 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pooling.cs @@ -22,14 +22,12 @@ public void Initialize( Selection selection, ResultElement resultValue, OperationContext operationContext, - IImmutableDictionary scopedContextData, - Path? path) + IImmutableDictionary scopedContextData) { _operationContext = operationContext; _operationResultBuilder.Context = _operationContext; _services = operationContext.Services; _selection = selection; - _path = path; ResultValue = resultValue; _parent = parent; _parser = operationContext.InputParser; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs index 87c7c41ec72..d1ba3d49086 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs @@ -22,6 +22,8 @@ internal set } } + public DeferExecutionCoordinator DeferExecutionCoordinator => throw new NotImplementedException(); + public OperationResultBuilder Result { get; } = new(); public RequestContext RequestContext @@ -38,7 +40,8 @@ public ResolverTask CreateResolverTask( Selection selection, ResultElement resultValue, IImmutableDictionary scopedContextData, - Path? path = null) + int executionBranchId = DeferExecutionCoordinator.MainBranchId, + DeferUsage? deferUsage = null) { AssertInitialized(); @@ -50,8 +53,34 @@ public ResolverTask CreateResolverTask( resultValue, this, scopedContextData, - path); + executionBranchId, + deferUsage); return resolverTask; } + + public DeferTask CreateDeferTask( + SelectionSet selectionSet, + Path selectionPath, + object? parent, + IImmutableDictionary scopedContextData, + int executionBranchId, + DeferUsage deferUsage) + { + AssertInitialized(); + + // TODO : we need to pool this still + var deferTask = new DeferTask(); + + deferTask.Initialize( + this, + parent, + scopedContextData, + selectionSet, + selectionPath, + executionBranchId, + deferUsage); + + return deferTask; + } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Operation.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Operation.cs index ab9451f9a43..59fcdf97e96 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Operation.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Operation.cs @@ -46,6 +46,11 @@ public IVariableValueCollection Variables /// public ulong IncludeFlags { get; private set; } + /// + /// Gets the include flags for the current request. + /// + public ulong DeferFlags { get; private set; } + /// /// Gets the value representing the instance of the /// diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs index 4c2d9605d8a..33a8a3242f1 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs @@ -78,6 +78,7 @@ public void Initialize( _isInitialized = true; IncludeFlags = operation.CreateIncludeFlags(variables); + DeferFlags = operation.CreateDeferFlags(variables); Result.Data = new ResultDocument(operation, IncludeFlags); Result.RequestIndex = _requestContext.RequestIndex; Result.VariableIndex = variableIndex; @@ -86,7 +87,11 @@ public void Initialize( _currentWorkScheduler = _workScheduler; } - public void InitializeFrom(OperationContext context) + public void InitializeDeferContext( + OperationContext context, + SelectionSet selectionSet, + Path selectionPath, + DeferUsage deferUsage) { _requestContext = context._requestContext; _schema = context._schema; @@ -102,15 +107,20 @@ public void InitializeFrom(OperationContext context) _rootValue = context._rootValue; _resolveQueryRootValue = context._resolveQueryRootValue; _batchDispatcher = context._batchDispatcher; + _currentWorkScheduler = context._currentWorkScheduler; _isInitialized = true; - IncludeFlags = _operation.CreateIncludeFlags(_variables); - Result.Data = new ResultDocument(_operation, IncludeFlags); + IncludeFlags = context.IncludeFlags; + DeferFlags = context.DeferFlags; + Result.Data = new ResultDocument( + context.Operation, + selectionSet, + selectionPath, + context.IncludeFlags, + context.DeferFlags, + deferUsage); Result.RequestIndex = _requestContext.RequestIndex; Result.VariableIndex = context._variableIndex; - - _workScheduler.Initialize(_batchDispatcher); - _currentWorkScheduler = _workScheduler; } public void Clean() diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs index 2483ab98ad4..8d5921bd6d6 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs @@ -285,6 +285,106 @@ public bool IsIncluded(ulong includeFlags) public bool IsDeferred(ulong deferFlags) => _deferMask != 0 && (_deferMask & deferFlags) == _deferMask; + /// + /// Gets all defer usages that are active for the specified defer flags. + /// + /// The active defer flags. + /// A struct enumerator over active defer usages. + public DeferUsageEnumerator GetActiveDeferUsages(ulong deferFlags) + => new(_deferUsage, deferFlags); + + /// + /// Gets the primary defer usage for this selection given the active defer flags. + /// The primary defer usage determines which execution branch the selection belongs to. + /// If multiple defer usages are active and one is a parent of another, the parent takes precedence. + /// + /// The active defer flags. + /// + /// The primary defer usage, or null if the selection is not deferred or has no active defer usages. + /// + public DeferUsage? GetPrimaryDeferUsage(ulong deferFlags) + { + if (_deferUsage.Length == 0) + { + return null; + } + + // Fast path for single defer usage (most common case). + if (_deferUsage.Length == 1) + { + var usage = _deferUsage[0]; + + // Walk up the parent chain to find the nearest active defer. + // A defer is inactive when its condition evaluates to false at runtime + // (e.g. @defer(if: $var) with $var = false). When inactive, the fragment + // is not deferred and its content folds into the parent scope — but the + // parent scope may itself be deferred. + while (usage is not null) + { + if ((deferFlags & (1UL << usage.DeferConditionIndex)) != 0) + { + return usage; + } + + usage = usage.Parent; + } + + // No active defer in the chain — field is not deferred. + return null; + } + + // Multiple defer usages: the field was collected from multiple deferred + // fragments. Resolve each to its nearest active ancestor, then find the + // outermost (primary) among them. + DeferUsage? primary = null; + + for (var i = 0; i < _deferUsage.Length; i++) + { + // Walk up the parent chain to find the nearest active defer. + var effective = _deferUsage[i]; + + while (effective is not null) + { + if ((deferFlags & (1UL << effective.DeferConditionIndex)) != 0) + { + break; + } + + effective = effective.Parent; + } + + if (effective is null) + { + // This occurrence has no active defer in its chain — + // the field appears non-deferred and belongs in the initial response. + return null; + } + + if (primary is null || primary == effective) + { + primary = effective; + continue; + } + + // Two different active defers. Keep the outermost: check if + // effective is an ancestor of primary. + var ancestor = primary.Parent; + + while (ancestor is not null) + { + if (ancestor == effective) + { + primary = effective; + break; + } + + ancestor = ancestor.Parent; + } + } + + return primary; + } + public Selection WithField(ObjectField field) { ArgumentNullException.ThrowIfNull(field); diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs new file mode 100644 index 00000000000..9fb3a5311c6 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs @@ -0,0 +1,107 @@ +using System.Buffers; +using System.Collections.Immutable; +using HotChocolate.Execution.DependencyInjection; + +namespace HotChocolate.Execution.Processing.Tasks; + +internal sealed class DeferTask : ExecutionTask +{ + private static readonly ArrayPool s_pool = ArrayPool.Shared; + private OperationContext _parentContext = null!; + private DeferExecutionCoordinator _coordinator = null!; + private object? _parent; + private ImmutableDictionary _scopedContext = null!; + private SelectionSet _selectionSet = null!; + private Path _selectionPath = null!; + private int _executionBranchId; + private DeferUsage _deferUsage = null!; + + public override bool IsDeferred => true; + + protected override IExecutionTaskContext Context => _parentContext; + + protected override async ValueTask ExecuteAsync(CancellationToken cancellationToken) + { + var contextFactory = _parentContext.Services.GetRequiredService>(); + using var deferContextOwner = contextFactory.Create(); + var deferContext = deferContextOwner.OperationContext; + + // we first need to initialize the rented context for this defer operation. + deferContext.InitializeDeferContext( + _parentContext, + _selectionSet, + _selectionPath, + _deferUsage); + + var data = deferContext.Result.Data.Data; + var bufferedTasks = s_pool.Rent(data.GetPropertyCount()); + var i = 0; + + try + { + foreach (var field in data.EnumerateObject()) + { + bufferedTasks[i++] = + deferContext.CreateResolverTask( + _parent, + field.AssertSelection(), + field.Value, + _scopedContext, + _executionBranchId, + _deferUsage); + } + + // we register our deferred tasks for execution ... + deferContext.Scheduler.Register(bufferedTasks.AsSpan(0, i)); + } + finally + { + if (i > 1) + { + bufferedTasks.AsSpan(0, i).Clear(); + } + + s_pool.Return(bufferedTasks); + } + + // ... and then wait for the scheduler to complete the deferred tasks. + await deferContext.Scheduler.WaitForCompletionAsync(_executionBranchId, cancellationToken); + + // once the execution branch has completed we enqueue the completed + // result with the defer coordinator so it can be delivered. + _coordinator.EnqueueResult(deferContext.BuildResult(), _executionBranchId); + } + + public void Initialize( + OperationContext parentContext, + object? parent, + ImmutableDictionary scopedContext, + SelectionSet selectionSet, + Path selectionPath, + int executionBranchId, + DeferUsage deferUsage) + { + _parentContext = parentContext; + _coordinator = parentContext.DeferExecutionCoordinator; + _parent = parent; + _scopedContext = scopedContext; + _selectionSet = selectionSet; + _executionBranchId = executionBranchId; + _deferUsage = deferUsage; + _selectionPath = selectionPath; + } + + public new void Reset() + { + _parentContext = null!; + _coordinator = null!; + _parent = null!; + _scopedContext = null!; + _selectionSet = null!; + _executionBranchId = 0; + _deferUsage = null!; + _selectionPath = null!; + + base.Reset(); + } +} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs index 50e086e5c8c..6f7029808ce 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Diagnostics; using HotChocolate.Text.Json; namespace HotChocolate.Execution.Processing.Tasks; @@ -14,12 +15,21 @@ public void Initialize( ResultElement resultValue, OperationContext operationContext, IImmutableDictionary scopedContextData, - Path? path) + int executionBranchId, + DeferUsage? deferUsage) { + // defer usage must be set if the executionBranchId is not the main branch id. + // defer usage must not be set if the executionBranchId is the main branch id. + Debug.Assert( + (executionBranchId == DeferExecutionCoordinator.MainBranchId && deferUsage is null) + || (executionBranchId > DeferExecutionCoordinator.MainBranchId && deferUsage is not null)); + _operationContext = operationContext; _selection = selection; - _context.Initialize(parent, selection, resultValue, operationContext, scopedContextData, path); + _context.Initialize(parent, selection, resultValue, operationContext, scopedContextData); IsSerial = selection.Strategy is SelectionExecutionStrategy.Serial; + BranchId = executionBranchId; + DeferUsage = deferUsage; } /// @@ -34,6 +44,8 @@ internal bool Reset() _context.Clean(); Status = ExecutionTaskStatus.WaitingToRun; IsSerial = false; + BranchId = DeferExecutionCoordinator.MainBranchId; + DeferUsage = null; IsRegistered = false; Next = null; Previous = null; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs index c1308c74ba0..5654de08b7f 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs @@ -8,7 +8,7 @@ namespace HotChocolate.Execution.Processing.Tasks; internal sealed partial class ResolverTask(ObjectPool objectPool) : IExecutionTask { private readonly MiddlewareContext _context = new(); - private readonly List _taskBuffer = []; + private readonly List _taskBuffer = []; private readonly Dictionary _args = [with(StringComparer.Ordinal)]; private OperationContext _operationContext = null!; private Selection _selection = null!; @@ -19,7 +19,19 @@ internal sealed partial class ResolverTask(ObjectPool objectPool) /// public uint Id { get; set; } - public uint DeferGroupId { get; set; } + /// + /// Gets the execution branch identifier this task belongs to. + /// Used by the defer coordinator to track which deferred execution branch + /// this task contributes results to. + /// + internal int BranchId { get; private set; } + + /// + /// Gets the primary defer usage that caused this execution branch to be created. + /// Used to determine whether child tasks should create new branches when their + /// primary defer usage differs from this one. + /// + internal DeferUsage? DeferUsage { get; private set; } /// /// Gets access to the resolver context for this task. @@ -64,6 +76,9 @@ public ExecutionTaskKind Kind /// public bool IsRegistered { get; set; } + /// + public bool IsDeferred => BranchId != DeferExecutionCoordinator.MainBranchId; + /// public void BeginExecute(CancellationToken cancellationToken) { diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs index 8c056fc6e17..95755cee514 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs @@ -1,122 +1,137 @@ using System.Collections.Immutable; using System.Diagnostics; -using System.Runtime.InteropServices; using HotChocolate.Text.Json; using HotChocolate.Types; using static HotChocolate.Execution.Processing.ValueCompletion; +using static HotChocolate.Execution.Processing.DeferExecutionCoordinator; +using System.Buffers; namespace HotChocolate.Execution.Processing.Tasks; internal static class ResolverTaskFactory { - private static List? s_pooled = []; + private static readonly ArrayPool s_pool = ArrayPool.Shared; - static ResolverTaskFactory() { } - - public static void EnqueueResolverTasks( + public static void EnqueueRootResolverTasks( OperationContext operationContext, object? parent, ResultElement resultValue, - IImmutableDictionary scopedContext, - Path path) + IImmutableDictionary scopedContext) { var selectionSet = resultValue.AssertSelectionSet(); - var selections = selectionSet.Selections; - var scheduler = operationContext.Scheduler; - var bufferedTasks = Interlocked.Exchange(ref s_pooled, null) ?? []; - Debug.Assert(bufferedTasks.Count == 0, "The buffer must be clean."); + var bufferedTasks = s_pool.Rent(resultValue.GetPropertyCount()); + var data = resultValue.EnumerateObject(); + var i = 0; try { - // we are iterating reverse so that in the case of a mutation the first - // synchronous root selection is executed first, since the work scheduler - // is using two stacks one for parallel work and one for synchronous work. - // the scheduler tries to schedule new work first. - // coincidentally we can use that to schedule a mutation so that we honor the spec - // guarantees while executing efficient. - var fieldValues = selections.Length == 1 - ? resultValue.EnumerateObject() - : resultValue.EnumerateObject().Reverse(); - foreach (var field in fieldValues) + if (selectionSet.HasDeferredSelections) { - bufferedTasks.Add( - operationContext.CreateResolverTask( - parent, - field.AssertSelection(), - field.Value, - scopedContext)); - } + var coordinator = operationContext.DeferExecutionCoordinator; + var deferFlags = operationContext.DeferFlags; + var branches = ImmutableDictionary.Empty; + DeferUsage? lastDeferUsage = null; - if (bufferedTasks.Count == 0) - { - // in the case all root fields are skipped we execute a dummy task in order - // to not have extra logic for this case. - scheduler.Register(new NoOpExecutionTask(operationContext)); + foreach (var field in data) + { + var selection = field.AssertSelection(); + + if (selection.IsDeferred(deferFlags)) + { + // if IsDeferred is true then GetPrimaryDeferUsage will be guaranteed + // to return a defer usage for the same deferFlags + var deferUsage = selection.GetPrimaryDeferUsage(deferFlags); + Debug.Assert(deferUsage is not null); + + field.Value.MarkAsDeferred(); + + if (lastDeferUsage == deferUsage) + { + continue; + } + + if (!branches.TryGetValue(deferUsage, out var lastDeferBranchId)) + { + lastDeferBranchId = coordinator.Branch(MainBranchId, Path.Root, deferUsage); + branches = branches.Add(deferUsage, lastDeferBranchId); + } + + lastDeferUsage = deferUsage; + continue; + } + + bufferedTasks[i++] = + operationContext.CreateResolverTask( + parent, + selection, + field.Value, + scopedContext); + } + + if (i == 0 && branches.IsEmpty) + { + // in the case all root fields are skipped we execute a dummy task in order + // to not have extra logic for this case. + scheduler.Register(new NoOpExecutionTask(operationContext)); + } + else + { + if (i > 0) + { + scheduler.Register(bufferedTasks.AsSpan(0, i)); + } + + if (!branches.IsEmpty) + { + foreach (var (deferUsage, branchId) in branches) + { + scheduler.Register( + operationContext.CreateDeferTask( + selectionSet, + Path.Root, + parent, + scopedContext, + branchId, + deferUsage)); + } + } + } } else { - scheduler.Register(CollectionsMarshal.AsSpan(bufferedTasks)); + foreach (var field in data) + { + bufferedTasks[i++] = + operationContext.CreateResolverTask( + parent, + field.AssertSelection(), + field.Value, + scopedContext); + } + + if (i == 0) + { + // in the case all root fields are skipped we execute a dummy task in order + // to not have extra logic for this case. + scheduler.Register(new NoOpExecutionTask(operationContext)); + } + else + { + scheduler.Register(bufferedTasks.AsSpan(0, i)); + } } } finally { - bufferedTasks.Clear(); - Interlocked.Exchange(ref s_pooled!, bufferedTasks); - } - } - - // TODO : remove ? defer? - /* - public static ResolverTask EnqueueElementTasks( - OperationContext operationContext, - Selection selection, - object? parent, - Path path, - int index, - IAsyncEnumerator value, - IImmutableDictionary scopedContext) - { - var parentResult = operationContext.Result.RentObject(1); - var bufferedTasks = Interlocked.Exchange(ref s_pooled, null) ?? []; - Debug.Assert(bufferedTasks.Count == 0, "The buffer must be clean."); - - var resolverTask = - operationContext.CreateResolverTask( - selection, - parent, - parentResult, - 0, - scopedContext, - path.Append(index)); - - try - { - CompleteInline( - operationContext, - resolverTask.Context, - selection, - selection.Type.ElementType(), - 0, - parentResult, - value.Current, - bufferedTasks); - - // if we have child tasks we need to register them. - if (bufferedTasks.Count > 0) + if (i > 0) { - operationContext.Scheduler.Register(CollectionsMarshal.AsSpan(bufferedTasks)); + bufferedTasks.AsSpan(0, i).Clear(); } - } - finally - { - bufferedTasks.Clear(); - Interlocked.Exchange(ref s_pooled, bufferedTasks); - } - return resolverTask; + s_pool.Return(bufferedTasks); + } } - */ public static void EnqueueOrInlineResolverTasks( ValueCompletionContext context, @@ -132,27 +147,85 @@ public static void EnqueueOrInlineResolverTasks( resultValue.SetObjectValue(selectionSet); - foreach (var field in resultValue.EnumerateObject()) + if (selectionSet.HasDeferredSelections) { - var selection = field.AssertSelection(); + var coordinator = operationContext.DeferExecutionCoordinator; + var deferFlags = operationContext.DeferFlags; + var branches = ImmutableDictionary.Empty; + DeferUsage? lastDeferUsage = null; + Path? currentPath = null; - if (selection.Strategy is SelectionExecutionStrategy.Pure) + foreach (var field in resultValue.EnumerateObject()) { - ResolveAndCompleteInline( - context, - selection, - selectionSetType, - field.Value, - parent); + var selection = field.AssertSelection(); + + if (selection.IsDeferred(deferFlags)) + { + // if IsDeferred is true then GetPrimaryDeferUsage will be guaranteed + // to return a defer usage for the same deferFlags + var deferUsage = selection.GetPrimaryDeferUsage(deferFlags); + Debug.Assert(deferUsage is not null); + + field.Value.MarkAsDeferred(); + + if (lastDeferUsage == deferUsage) + { + continue; + } + + if (!branches.TryGetValue(deferUsage, out var lastDeferBranchId)) + { + currentPath ??= resultValue.Path; + lastDeferBranchId = coordinator.Branch(MainBranchId, currentPath, deferUsage); + branches = branches.Add(deferUsage, lastDeferBranchId); + } + + lastDeferUsage = deferUsage; + } + else if (selection.Strategy is SelectionExecutionStrategy.Pure) + { + ResolveAndCompleteInline( + context, + selection, + selectionSetType, + field.Value, + parent); + } + else + { + context.Tasks.Add( + operationContext.CreateResolverTask( + parent, + selection, + field.Value, + context.ResolverContext.ScopedContextData)); + } } - else + } + else + { + foreach (var field in resultValue.EnumerateObject()) { - context.Tasks.Add( - operationContext.CreateResolverTask( - parent, + var selection = field.AssertSelection(); + + if (selection.Strategy is SelectionExecutionStrategy.Pure) + { + ResolveAndCompleteInline( + context, selection, + selectionSetType, field.Value, - context.ResolverContext.ScopedContextData)); + parent); + } + else + { + context.Tasks.Add( + operationContext.CreateResolverTask( + parent, + selection, + field.Value, + context.ResolverContext.ScopedContextData)); + } } } } @@ -226,6 +299,8 @@ private static void ResolveAndCompleteInline( private sealed class NoOpExecutionTask(OperationContext context) : ExecutionTask { + public override bool IsDeferred => false; + protected override IExecutionTaskContext Context { get; } = context; protected override ValueTask ExecuteAsync(CancellationToken cancellationToken) diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletionContext.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletionContext.cs index 7c9adcbfa68..aaab1d61668 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletionContext.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletionContext.cs @@ -1,5 +1,3 @@ -using HotChocolate.Execution.Processing.Tasks; - namespace HotChocolate.Execution.Processing; internal readonly ref struct ValueCompletionContext @@ -7,7 +5,7 @@ internal readonly ref struct ValueCompletionContext public ValueCompletionContext( OperationContext operationContext, MiddlewareContext resolverContext, - List tasks) + List tasks) { OperationContext = operationContext; ResolverContext = resolverContext; @@ -18,5 +16,5 @@ public ValueCompletionContext( public MiddlewareContext ResolverContext { get; } - public List Tasks { get; } + public List Tasks { get; } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkQueue.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkQueue.cs index 1a19ba60bde..194bbe97d38 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkQueue.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkQueue.cs @@ -4,10 +4,11 @@ namespace HotChocolate.Execution.Processing; internal sealed class WorkQueue { - private readonly Stack _stack = new(); + private readonly Stack _immediateStack = new(); + private readonly Stack _deferredStack = new(); private int _running; - public bool IsEmpty => _stack.Count == 0; + public bool IsEmpty => _immediateStack.Count == 0 && _deferredStack.Count == 0; public bool HasRunningTasks => _running > 0; @@ -25,7 +26,8 @@ public bool Complete() public bool TryTake([MaybeNullWhen(false)] out IExecutionTask executionTask) { - if (_stack.TryPop(out executionTask)) + if (_immediateStack.TryPop(out executionTask) + || _deferredStack.TryPop(out executionTask)) { Interlocked.Increment(ref _running); return true; @@ -38,12 +40,20 @@ public void Push(IExecutionTask executionTask) { ArgumentNullException.ThrowIfNull(executionTask); - _stack.Push(executionTask); + if (executionTask.IsDeferred) + { + _deferredStack.Push(executionTask); + } + else + { + _immediateStack.Push(executionTask); + } } public void Clear() { - _stack.Clear(); + _immediateStack.Clear(); + _deferredStack.Clear(); _running = 0; } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs index d0319f43e83..eb72617696e 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs @@ -24,6 +24,12 @@ public async Task ExecuteAsync() } } + /// + /// Execute the work. + /// + public Task WaitForCompletionAsync(int executionBranchId, CancellationToken cancellationToken) + => throw new NotImplementedException(); + private async Task ExecuteInternalAsync(IExecutionTask?[] buffer) { RESTART: diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs index 6ba38989d96..5ae45e697c2 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs @@ -1,5 +1,3 @@ -using HotChocolate.Execution.Processing.Tasks; - namespace HotChocolate.Execution.Processing; /// @@ -46,14 +44,15 @@ public void Register(IExecutionTask task) /// /// Registers work with the task backlog. /// - public void Register(ReadOnlySpan tasks) + public void Register(ReadOnlySpan tasks) { AssertNotPooled(); lock (_sync) { - foreach (var task in tasks) + for (var i = tasks.Length; i >= 0; i--) { + var task = tasks[i]; task.Id = Interlocked.Increment(ref _nextId); task.IsRegistered = true; diff --git a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.DbRow.cs b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.DbRow.cs index 87eaa13f8c9..f6044c35b1a 100644 --- a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.DbRow.cs +++ b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.DbRow.cs @@ -24,7 +24,7 @@ internal readonly struct DbRow // 27 bits ParentRow + 5 reserved bits private readonly int _parentRow; - // 15 bits OperationReferenceId + 8 bits Flags + 9 reserved bits + // 15 bits OperationReferenceId + 9 bits Flags + 8 reserved bits private readonly int _opRefIdAndFlags; public DbRow( @@ -125,9 +125,9 @@ public OperationReferenceType OperationReferenceType /// Element metadata flags. /// /// - /// 8 bits = 256 combinations + /// 9 bits = 512 combinations /// - public ElementFlags Flags => (ElementFlags)((_opRefIdAndFlags >> 15) & 0xFF); + public ElementFlags Flags => (ElementFlags)((_opRefIdAndFlags >> 15) & 0x1FF); /// /// True for primitive JSON values (strings, numbers, booleans, null). @@ -143,7 +143,7 @@ internal enum OperationReferenceType : byte } [Flags] - internal enum ElementFlags : byte + internal enum ElementFlags : short { None = 0, IsRoot = 1, @@ -153,6 +153,7 @@ internal enum ElementFlags : byte IsExcluded = 16, IsNullable = 32, IsInvalidated = 64, - IsEncoded = 128 + IsEncoded = 128, + IsDeferred = 256 } } diff --git a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.MetaDb.cs b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.MetaDb.cs index af81d3cc647..9a53952b925 100644 --- a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.MetaDb.cs +++ b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.MetaDb.cs @@ -244,19 +244,19 @@ internal ElementFlags GetFlags(Cursor cursor) var span = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset + 16); var value = MemoryMarshal.Read(span); - return (ElementFlags)((value >> 15) & 0xFF); + return (ElementFlags)((value >> 15) & 0x1FF); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void SetFlags(Cursor cursor, ElementFlags flags) { AssertValidCursor(cursor); - Debug.Assert((byte)flags <= 255, "Flags value exceeds 8-bit limit"); + Debug.Assert((short)flags <= 511, "Flags value exceeds 9-bit limit"); var fieldSpan = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset + 16); var currentValue = MemoryMarshal.Read(fieldSpan); - var clearedValue = currentValue & unchecked((int)0xFF807FFF); // ~(0xFF << 15) + var clearedValue = currentValue & unchecked((int)0xFF007FFF); // ~(0x1FF << 15) var newValue = clearedValue | ((int)flags << 15); MemoryMarshal.Write(fieldSpan, newValue); diff --git a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.WriteTo.cs b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.WriteTo.cs index 6c97757b57e..da024baeee1 100644 --- a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.WriteTo.cs +++ b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.WriteTo.cs @@ -112,8 +112,9 @@ private void WriteObject(Cursor start, DbRow startRow) var row = document._metaDb.Get(current); Debug.Assert(row.TokenType is ElementTokenType.PropertyName); - if ((ElementFlags.IsInternal & row.Flags) == ElementFlags.IsInternal - || (ElementFlags.IsExcluded & row.Flags) == ElementFlags.IsExcluded) + var flags = row.Flags; + + if ((flags & (ElementFlags.IsInternal | ElementFlags.IsExcluded | ElementFlags.IsDeferred)) != 0) { // skip name+value current += 2; diff --git a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs index b54c9ae4178..3a8716bc2ef 100644 --- a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs +++ b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs @@ -15,6 +15,7 @@ public sealed partial class ResultDocument : IDisposable private static readonly Encoding s_utf8Encoding = Encoding.UTF8; private readonly Operation _operation; private readonly ulong _includeFlags; + private readonly Path _rootPath = Path.Root; internal MetaDb _metaDb; private int _nextDataIndex; private int _rentedDataSize; @@ -37,6 +38,26 @@ public ResultDocument(Operation operation, ulong includeFlags) Data = CreateObject(Cursor.Zero, operation.RootSelectionSet); } + public ResultDocument( + Operation operation, + SelectionSet selectionSet, + Path path, + ulong includeFlags, + ulong deferFlags, + DeferUsage deferUsage) + { + ArgumentNullException.ThrowIfNull(operation); + ArgumentNullException.ThrowIfNull(selectionSet); + ArgumentNullException.ThrowIfNull(deferUsage); + + _metaDb = MetaDb.CreateForEstimatedRows(Cursor.RowsPerChunk); + _operation = operation; + _includeFlags = includeFlags; + _rootPath = path; + + Data = CreateObject(Cursor.Zero, selectionSet, includeFlags, deferFlags, deferUsage); + } + public ResultElement Data { get; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -128,14 +149,14 @@ internal Path CreatePath(Cursor current) // Stop at root via IsRoot flag. if ((_metaDb.GetFlags(current) & ElementFlags.IsRoot) == ElementFlags.IsRoot) { - return Path.Root; + return _rootPath; } Span chain = stackalloc Cursor[64]; var c = current; var written = 0; - do + while (true) { chain[written++] = c; @@ -151,9 +172,9 @@ internal Path CreatePath(Cursor current) { throw new InvalidOperationException("The path is to deep."); } - } while (true); + } - var path = Path.Root; + var path = _rootPath; var parentTokenType = ElementTokenType.StartObject; chain = chain[..written]; @@ -462,6 +483,35 @@ internal ResultElement CreateObject(Cursor parent, SelectionSet selectionSet) } } + private ResultElement CreateObject( + Cursor parent, + SelectionSet selectionSet, + ulong includeFlags, + ulong deferFlags, + DeferUsage deferUsage) + { + lock (_dataChunkLock) + { + var startObjectCursor = WriteStartObject(parent, selectionSet.Id); + + var selectionCount = 0; + foreach (var selection in selectionSet.Selections) + { + if (selection.IsIncluded(includeFlags) + && selection.IsDeferred(deferFlags) + && selection.GetPrimaryDeferUsage(deferFlags) == deferUsage) + { + WriteEmptyProperty(startObjectCursor, selection); + selectionCount++; + } + } + + WriteEndObject(startObjectCursor, selectionCount); + + return new ResultElement(this, startObjectCursor); + } + } + internal ResultElement CreateObject(Cursor parent, int propertyCount) { lock (_dataChunkLock) @@ -574,6 +624,13 @@ internal void AssignNullValue(ResultElement target) parentRow: _metaDb.GetParent(target.Cursor)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void MarkAsDeferred(ResultElement target) + { + var cursor = target.Cursor; + _metaDb.SetFlags(cursor, _metaDb.GetFlags(cursor) | ElementFlags.IsDeferred); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private int ClaimDataSpace(int size) { diff --git a/src/HotChocolate/Core/src/Types/Text/Json/ResultElement.cs b/src/HotChocolate/Core/src/Types/Text/Json/ResultElement.cs index eff92f7166c..a15560dd6c3 100644 --- a/src/HotChocolate/Core/src/Types/Text/Json/ResultElement.cs +++ b/src/HotChocolate/Core/src/Types/Text/Json/ResultElement.cs @@ -1143,6 +1143,13 @@ public void SetNumberValue(decimal value) _parent.AssignNumberValue(this, buffer[..bytesWritten]); } + internal void MarkAsDeferred() + { + CheckValidInstance(); + + _parent.MarkAsDeferred(this); + } + /// /// Writes this element as JSON to the specified buffer writer. /// From f41474ed4a40ea5883515dff12e6b43ca3d0a3fe Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 9 Feb 2026 17:27:46 +0100 Subject: [PATCH 05/46] completed task handling. --- .../Tasks/ResolverTask.CompleteValue.cs | 2 +- .../Processing/Tasks/ResolverTaskFactory.cs | 22 ++++++++++++++----- .../Processing/ValueCompletionContext.cs | 6 ++++- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.CompleteValue.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.CompleteValue.cs index 7e3bb2192ae..74771cf956c 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.CompleteValue.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.CompleteValue.cs @@ -19,7 +19,7 @@ private void CompleteValue(bool success, CancellationToken cancellationToken) // we will only try to complete the resolver value if there are no known errors. if (success) { - var completionContext = new ValueCompletionContext(_operationContext, _context, _taskBuffer); + var completionContext = new ValueCompletionContext(_operationContext, _context, _taskBuffer, BranchId); Complete(completionContext, _selection, resultValue, result); } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs index 95755cee514..79e43365f2f 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs @@ -51,10 +51,10 @@ public static void EnqueueRootResolverTasks( continue; } - if (!branches.TryGetValue(deferUsage, out var lastDeferBranchId)) + if (!branches.TryGetValue(deferUsage, out var branchId)) { - lastDeferBranchId = coordinator.Branch(MainBranchId, Path.Root, deferUsage); - branches = branches.Add(deferUsage, lastDeferBranchId); + branchId = coordinator.Branch(MainBranchId, Path.Root, deferUsage); + branches = branches.Add(deferUsage, branchId); } lastDeferUsage = deferUsage; @@ -155,6 +155,8 @@ public static void EnqueueOrInlineResolverTasks( DeferUsage? lastDeferUsage = null; Path? currentPath = null; + var parentBranchId = context.ParentBranchId; + foreach (var field in resultValue.EnumerateObject()) { var selection = field.AssertSelection(); @@ -173,11 +175,19 @@ public static void EnqueueOrInlineResolverTasks( continue; } - if (!branches.TryGetValue(deferUsage, out var lastDeferBranchId)) + if (!branches.TryGetValue(deferUsage, out var branchId)) { currentPath ??= resultValue.Path; - lastDeferBranchId = coordinator.Branch(MainBranchId, currentPath, deferUsage); - branches = branches.Add(deferUsage, lastDeferBranchId); + branchId = coordinator.Branch(parentBranchId, currentPath, deferUsage); + branches = branches.Add(deferUsage, branchId); + context.Tasks.Add( + operationContext.CreateDeferTask( + selectionSet, + currentPath, + parent, + context.ResolverContext.ScopedContextData, + branchId, + deferUsage)); } lastDeferUsage = deferUsage; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletionContext.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletionContext.cs index aaab1d61668..423cded9c98 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletionContext.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletionContext.cs @@ -5,11 +5,13 @@ internal readonly ref struct ValueCompletionContext public ValueCompletionContext( OperationContext operationContext, MiddlewareContext resolverContext, - List tasks) + List tasks, + int parentBranchId) { OperationContext = operationContext; ResolverContext = resolverContext; Tasks = tasks; + ParentBranchId = parentBranchId; } public OperationContext OperationContext { get; } @@ -17,4 +19,6 @@ public ValueCompletionContext( public MiddlewareContext ResolverContext { get; } public List Tasks { get; } + + public int ParentBranchId { get; } } From 073fb57cb29380a23513cf38aa92a7d4fb1f9cea Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 9 Feb 2026 23:06:41 +0100 Subject: [PATCH 06/46] reworked scheduler and general result handling. --- .../Execution/Tasks/ExecutionTask.cs | 3 + .../Execution/Tasks/IExecutionTask.cs | 5 + .../Execution/Processing/BranchTracker.cs | 13 ++ .../DeferExecutionCoordinator.Pooling.cs | 43 +++++ .../Processing/DeferExecutionCoordinator.cs | 48 +---- .../Processing/OperationContext.Execution.cs | 22 ++- .../Processing/OperationContext.Pooling.cs | 36 +++- .../Execution/Processing/QueryExecutor.cs | 164 ++++++++++++++---- .../Execution/Processing/Tasks/DeferTask.cs | 10 +- .../Processing/Tasks/ResolverTask.Pooling.cs | 9 +- .../Processing/Tasks/ResolverTask.cs | 4 +- .../Processing/Tasks/ResolverTaskFactory.cs | 8 +- .../Processing/WorkScheduler.Execute.cs | 15 +- .../Processing/WorkScheduler.Pooling.cs | 7 +- .../Execution/Processing/WorkScheduler.cs | 50 +++++- 15 files changed, 328 insertions(+), 109 deletions(-) create mode 100644 src/HotChocolate/Core/src/Types/Execution/Processing/BranchTracker.cs create mode 100644 src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs diff --git a/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs b/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs index 376a538d3d9..6aac8baa990 100644 --- a/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs +++ b/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs @@ -16,6 +16,9 @@ public abstract class ExecutionTask : IExecutionTask /// public uint Id { get; set; } + /// + public abstract int BranchId { get; } + /// /// Gets the execution engine task context. /// diff --git a/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs b/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs index 47ec9294cda..db81778d5ae 100644 --- a/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs +++ b/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs @@ -10,6 +10,11 @@ public interface IExecutionTask /// uint Id { get; set; } + /// + /// Gets the execution branch id. + /// + int BranchId { get; } + /// /// Defines the kind of task. /// The task kind is used to apply the correct execution strategy. diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/BranchTracker.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/BranchTracker.cs new file mode 100644 index 00000000000..b5d543a7dee --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/BranchTracker.cs @@ -0,0 +1,13 @@ +namespace HotChocolate.Execution.Processing; + +internal sealed class BranchTracker +{ + private int _nextId; + + public const int SystemBranchId = -1; + + public int CreateNewBranchId() + => Interlocked.Increment(ref _nextId); + + public void Reset() => _nextId = 0; +} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs new file mode 100644 index 00000000000..88fd9443917 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs @@ -0,0 +1,43 @@ +using System.Threading.Channels; + +namespace HotChocolate.Execution.Processing; + +internal sealed partial class DeferExecutionCoordinator +{ + /// + /// Initializes the coordinator for a new execution cycle. + /// Must be called before any other operations when leased from a pool. + /// + public void Initialize(BranchTracker branchTracker, int mainBranchId) + { + _branchTracker = branchTracker; + _mainBranchId = mainBranchId; + _resultChannel = Channel.CreateUnbounded( + new UnboundedChannelOptions + { + SingleWriter = true, + SingleReader = true + }); + } + + /// + /// Resets the coordinator to its initial state so it can be reused. + /// + public void Reset() + { + _branchIdLookup.Clear(); + _branchInfoLookup.Clear(); + _branches.Clear(); + _completed.Clear(); + _delivered.Clear(); + _branchTracker = null!; + _resultChannel = null!; + _pendingBuilder = null; + _incrementalBuilder = null; + _completedBuilder = null; + _processQueue = null; + _hasBranches = false; + _mainBranchId = 0; + _pendingBranches = 0; + } +} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs index 6cc17ec6bff..07ec4f4c7ed 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs @@ -3,15 +3,16 @@ namespace HotChocolate.Execution.Processing; -internal sealed class DeferExecutionCoordinator +internal sealed partial class DeferExecutionCoordinator { - public const int MainBranchId = -1; private readonly object _sync = new(); private readonly Dictionary _branchIdLookup = []; private readonly Dictionary _branchInfoLookup = []; private readonly Dictionary> _branches = []; private readonly Dictionary _completed = []; private readonly HashSet _delivered = []; + private BranchTracker _branchTracker = null!; + private int _mainBranchId; private ImmutableList.Builder? _pendingBuilder; private ImmutableList.Builder? _incrementalBuilder; private ImmutableList.Builder? _completedBuilder; @@ -19,7 +20,6 @@ internal sealed class DeferExecutionCoordinator private Channel _resultChannel = null!; private volatile bool _hasBranches; private int _pendingBranches; - private int _nextId; /// /// Gets whether any deferred execution branches have been registered. @@ -39,7 +39,7 @@ public int Branch(int currentBranchId, Path path, DeferUsage deferUsage) { if (!_branchIdLookup.TryGetValue(branchInfo, out var newBranchId)) { - newBranchId = _nextId++; + newBranchId = _branchTracker.CreateNewBranchId(); GetBranchesUnsafe(currentBranchId).Add(newBranchId); _branchInfoLookup.Add(newBranchId, branchInfo); _branchIdLookup.Add(branchInfo, newBranchId); @@ -59,7 +59,7 @@ public void EnqueueResult(OperationResult result) { lock (_sync) { - ComposeAndDeliverUnsafe(MainBranchId, result); + ComposeAndDeliverUnsafe(_mainBranchId, result); } } @@ -90,40 +90,6 @@ public IAsyncEnumerable ReadResultsAsync( CancellationToken cancellationToken = default) => _resultChannel.Reader.ReadAllAsync(cancellationToken); - /// - /// Initializes the coordinator for a new execution cycle. - /// Must be called before any other operations when leased from a pool. - /// - public void Initialize() - { - _resultChannel = Channel.CreateUnbounded( - new UnboundedChannelOptions - { - SingleWriter = true, - SingleReader = true - }); - } - - /// - /// Resets the coordinator to its initial state so it can be reused. - /// - public void Reset() - { - _branchIdLookup.Clear(); - _branchInfoLookup.Clear(); - _branches.Clear(); - _completed.Clear(); - _delivered.Clear(); - _resultChannel = null!; - _pendingBuilder = null; - _incrementalBuilder = null; - _completedBuilder = null; - _processQueue = null; - _hasBranches = false; - _pendingBranches = 0; - _nextId = 0; - } - private void ComposeAndDeliverUnsafe(int branchId, OperationResult result) { var childBranches = GetBranchesUnsafe(branchId); @@ -202,14 +168,14 @@ private void ComposeAndDeliverUnsafe(int branchId, OperationResult result) _delivered.Add(branchId); - if (branchId != MainBranchId) + if (branchId != _mainBranchId) { _pendingBranches--; } _resultChannel.Writer.TryWrite(result); - if (_delivered.Contains(MainBranchId) && _pendingBranches == 0) + if (_delivered.Contains(_mainBranchId) && _pendingBranches == 0) { _resultChannel.Writer.TryComplete(); } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs index d1ba3d49086..d1495d5f8be 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs @@ -16,13 +16,25 @@ public WorkScheduler Scheduler AssertInitialized(); return _currentWorkScheduler; } - internal set + } + + public DeferExecutionCoordinator DeferExecutionCoordinator + { + get { - _currentWorkScheduler = value; + AssertInitialized(); + return _deferExecutionCoordinator; } } - public DeferExecutionCoordinator DeferExecutionCoordinator => throw new NotImplementedException(); + public int ExecutionBranchId + { + get + { + AssertInitialized(); + return _branchId; + } + } public OperationResultBuilder Result { get; } = new(); @@ -40,7 +52,7 @@ public ResolverTask CreateResolverTask( Selection selection, ResultElement resultValue, IImmutableDictionary scopedContextData, - int executionBranchId = DeferExecutionCoordinator.MainBranchId, + int? executionBranchId = null, DeferUsage? deferUsage = null) { AssertInitialized(); @@ -53,7 +65,7 @@ public ResolverTask CreateResolverTask( resultValue, this, scopedContextData, - executionBranchId, + executionBranchId ?? _branchId, deferUsage); return resolverTask; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs index 33a8a3242f1..f5d5e0ba606 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs @@ -14,8 +14,11 @@ namespace HotChocolate.Execution.Processing; internal sealed partial class OperationContext { private readonly IFactory _resolverTaskFactory; + private readonly BranchTracker _branchTracker = new(); private readonly WorkScheduler _workScheduler; + private readonly DeferExecutionCoordinator _deferExecutionCoordinator = new(); private WorkScheduler _currentWorkScheduler; + private BranchTracker _currentBranchTracker; private readonly AggregateServiceScopeInitializer _serviceScopeInitializer; private RequestContext _requestContext = null!; private Schema _schema = null!; @@ -30,6 +33,7 @@ internal sealed partial class OperationContext private Func _resolveQueryRootValue = null!; private IBatchDispatcher _batchDispatcher = null!; private InputParser _inputParser = null!; + private int _branchId; private int _variableIndex; private object? _rootValue; private bool _isInitialized; @@ -42,6 +46,7 @@ public OperationContext( _resolverTaskFactory = resolverTaskFactory; _workScheduler = new WorkScheduler(this); _currentWorkScheduler = _workScheduler; + _currentBranchTracker = _branchTracker; _serviceScopeInitializer = serviceScopeInitializer; Converter = typeConverter; } @@ -75,7 +80,6 @@ public void Initialize( _resolveQueryRootValue = resolveQueryRootValue; _batchDispatcher = batchDispatcher; _variableIndex = variableIndex; - _isInitialized = true; IncludeFlags = operation.CreateIncludeFlags(variables); DeferFlags = operation.CreateDeferFlags(variables); @@ -83,14 +87,21 @@ public void Initialize( Result.RequestIndex = _requestContext.RequestIndex; Result.VariableIndex = variableIndex; - _workScheduler.Initialize(batchDispatcher); + _branchId = _currentBranchTracker.CreateNewBranchId(); + _workScheduler.Initialize(_requestContext, batchDispatcher); + _deferExecutionCoordinator.Initialize(_currentBranchTracker, _branchId); + + _currentBranchTracker = _branchTracker; _currentWorkScheduler = _workScheduler; + + _isInitialized = true; } public void InitializeDeferContext( OperationContext context, SelectionSet selectionSet, Path selectionPath, + int executionBranchId, DeferUsage deferUsage) { _requestContext = context._requestContext; @@ -107,8 +118,9 @@ public void InitializeDeferContext( _rootValue = context._rootValue; _resolveQueryRootValue = context._resolveQueryRootValue; _batchDispatcher = context._batchDispatcher; + _currentBranchTracker = context._currentBranchTracker; _currentWorkScheduler = context._currentWorkScheduler; - _isInitialized = true; + _branchId = executionBranchId; IncludeFlags = context.IncludeFlags; DeferFlags = context.DeferFlags; @@ -121,14 +133,28 @@ public void InitializeDeferContext( deferUsage); Result.RequestIndex = _requestContext.RequestIndex; Result.VariableIndex = context._variableIndex; + + _isInitialized = true; + } + + public void InitializeWorkSchedulerFrom(OperationContext context) + { + _currentBranchTracker = context._currentBranchTracker; + _currentWorkScheduler = context._currentWorkScheduler; + _branchId = _currentBranchTracker.CreateNewBranchId(); } public void Clean() { if (_isInitialized) { - _currentWorkScheduler = _workScheduler; + _branchTracker.Reset(); _workScheduler.Clear(); + _deferExecutionCoordinator.Reset(); + + _currentBranchTracker = _branchTracker; + _currentWorkScheduler = _workScheduler; + _requestContext = null!; _schema = null!; _errorHandler = null!; @@ -141,6 +167,7 @@ public void Clean() _rootValue = null; _resolveQueryRootValue = null!; _batchDispatcher = null!; + _branchId = int.MinValue; _isInitialized = false; Result.Reset(); } @@ -151,6 +178,7 @@ public void ResetScheduler() if (_isInitialized) { _currentWorkScheduler = _workScheduler; + _currentBranchTracker = _branchTracker; } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs index 6fce5589c74..1bef4f15170 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs @@ -1,83 +1,175 @@ using System.Collections.Immutable; +using System.Diagnostics; using static HotChocolate.Execution.Processing.Tasks.ResolverTaskFactory; namespace HotChocolate.Execution.Processing; internal sealed class QueryExecutor { - public Task ExecuteAsync( + public Task ExecuteAsync( OperationContext operationContext) => ExecuteAsync(operationContext, ImmutableDictionary.Empty); - public Task ExecuteAsync( + public Task ExecuteAsync( OperationContext operationContext, IImmutableDictionary scopedContext) { - ArgumentNullException.ThrowIfNull(operationContext); - ArgumentNullException.ThrowIfNull(scopedContext); + if (operationContext.Operation.HasDeferredSelections) + { + return ExecuteIncrementalAsync(operationContext, scopedContext); + } - return ExecuteInternalAsync(operationContext, scopedContext); + return ExecuteNoIncrementalAsync(operationContext, scopedContext); } - private static async Task ExecuteInternalAsync( + private static async Task ExecuteIncrementalAsync( OperationContext operationContext, IImmutableDictionary scopedContext) { - EnqueueResolverTasks( + EnqueueRootResolverTasks( operationContext, operationContext.RootValue, operationContext.Result.Data.Data, - scopedContext, - Path.Root); + scopedContext); + + var branchId = operationContext.ExecutionBranchId; + var scheduler = operationContext.Scheduler; + var coordinator = operationContext.DeferExecutionCoordinator; + + var execution = scheduler.ExecuteAsync1(); + await scheduler.WaitForCompletionAsync(branchId).ConfigureAwait(false); + coordinator.EnqueueResult(operationContext.BuildResult()); + return new ResponseStream(CreateStream, ExecutionResultKind.DeferredResult); + + async IAsyncEnumerable CreateStream() + { + var requestAborted = operationContext.RequestAborted; + await foreach (var result in coordinator.ReadResultsAsync(requestAborted)) + { + yield return result; + } - await operationContext.Scheduler.ExecuteAsync().ConfigureAwait(false); + await execution.ConfigureAwait(false); + } + } + + private static async Task ExecuteNoIncrementalAsync( + OperationContext operationContext, + IImmutableDictionary scopedContext) + { + EnqueueRootResolverTasks( + operationContext, + operationContext.RootValue, + operationContext.Result.Data.Data, + scopedContext); + + await operationContext.Scheduler.ExecuteAsync1().ConfigureAwait(false); return operationContext.BuildResult(); } - public async Task ExecuteBatchAsync( - ReadOnlyMemory operationContexts, - Memory results) + public Task ExecuteBatchAsync( + OperationContextOwner[] operationContexts, + IExecutionResult[] results, + int length) { - var scopedContext = ImmutableDictionary.Empty; + Debug.Assert(length > 0); + Debug.Assert(length > operationContexts.Length); + Debug.Assert(length > results.Length); + if (operationContexts[0].OperationContext.Operation.HasDeferredSelections) + { + return ExecuteBatchIncrementalAsync(operationContexts, results, length); + } + + return ExecuteBatchNoIncrementalAsync(operationContexts, results, length); + } + + private async Task ExecuteBatchNoIncrementalAsync( + OperationContextOwner[] operationContexts, + IExecutionResult[] results, + int length) + { // when using batching we will use the same scheduler // to execute more efficiently with DataLoader. - var scheduler = operationContexts.Span[0].OperationContext.Scheduler; + var parentContext = operationContexts[0].OperationContext; - FillSchedulerWithWork(scheduler, operationContexts.Span, scopedContext); + FillSchedulerWithWork(parentContext, operationContexts, length); - await scheduler.ExecuteAsync().ConfigureAwait(false); + await parentContext.Scheduler.ExecuteAsync1().ConfigureAwait(false); - BuildResults(operationContexts.Span, results.Span); + for (var i = 0; i < length; ++i) + { + results[i] = operationContexts[i].OperationContext.BuildResult(); + } } - private static void FillSchedulerWithWork( - WorkScheduler scheduler, - ReadOnlySpan operationContexts, - ImmutableDictionary scopedContext) + private async Task ExecuteBatchIncrementalAsync( + OperationContextOwner[] operationContexts, + IExecutionResult[] results, + int length) { - foreach (var contextOwner in operationContexts) + // when using batching we will use the same scheduler + // to execute more efficiently with DataLoader. + var parentContext = operationContexts[0].OperationContext; + var scheduler = parentContext.Scheduler; + + FillSchedulerWithWork(parentContext, operationContexts, length); + + var execution = parentContext.Scheduler.ExecuteAsync1(); + + for (var i = 0; i < length; ++i) { - var context = contextOwner.OperationContext; - context.Scheduler = scheduler; + if (i == 0) + { + var branchId = parentContext.ExecutionBranchId; + await scheduler.WaitForCompletionAsync(branchId).ConfigureAwait(false); + parentContext.DeferExecutionCoordinator.EnqueueResult(parentContext.BuildResult()); + results[i] = new ResponseStream(CreateStreamAndComplete, ExecutionResultKind.DeferredResult); + } + else + { + var context = operationContexts[i].OperationContext; + var branchId = context.ExecutionBranchId; + await scheduler.WaitForCompletionAsync(branchId).ConfigureAwait(false); + context.DeferExecutionCoordinator.EnqueueResult(context.BuildResult()); + results[i] = new ResponseStream(CreateStream, ExecutionResultKind.DeferredResult); + } + } - EnqueueResolverTasks( - context, - context.RootValue, - context.Result.Data.Data, - scopedContext, - Path.Root); + async IAsyncEnumerable CreateStreamAndComplete() + { + var requestAborted = parentContext.RequestAborted; + await foreach (var result in parentContext.DeferExecutionCoordinator.ReadResultsAsync(requestAborted)) + { + yield return result; + } + + await execution.ConfigureAwait(false); + } + + IAsyncEnumerable CreateStream() + { + var requestAborted = parentContext.RequestAborted; + return parentContext.DeferExecutionCoordinator.ReadResultsAsync(requestAborted); } } - private static void BuildResults( - ReadOnlySpan operationContexts, - Span results) + private static void FillSchedulerWithWork( + OperationContext parentContext, + OperationContextOwner[] operationContexts, + int length) { - for (var i = 0; i < operationContexts.Length; ++i) + for (var i = 0; i < length; i++) { - results[i] = operationContexts[i].OperationContext.BuildResult(); + var context = operationContexts[i].OperationContext; + context.InitializeWorkSchedulerFrom(parentContext); + + EnqueueRootResolverTasks( + context, + context.RootValue, + context.Result.Data.Data, + ImmutableDictionary.Empty); } } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs index 9fb3a5311c6..2d83b404a9d 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs @@ -10,12 +10,15 @@ internal sealed class DeferTask : ExecutionTask private OperationContext _parentContext = null!; private DeferExecutionCoordinator _coordinator = null!; private object? _parent; - private ImmutableDictionary _scopedContext = null!; + private IImmutableDictionary _scopedContext = null!; private SelectionSet _selectionSet = null!; private Path _selectionPath = null!; private int _executionBranchId; private DeferUsage _deferUsage = null!; + // the defer tasks runs in the system branch as its just an orchestration task. + public override int BranchId => BranchTracker.SystemBranchId; + public override bool IsDeferred => true; protected override IExecutionTaskContext Context => _parentContext; @@ -31,6 +34,7 @@ protected override async ValueTask ExecuteAsync(CancellationToken cancellationTo _parentContext, _selectionSet, _selectionPath, + _executionBranchId, _deferUsage); var data = deferContext.Result.Data.Data; @@ -65,7 +69,7 @@ protected override async ValueTask ExecuteAsync(CancellationToken cancellationTo } // ... and then wait for the scheduler to complete the deferred tasks. - await deferContext.Scheduler.WaitForCompletionAsync(_executionBranchId, cancellationToken); + await deferContext.Scheduler.WaitForCompletionAsync(_executionBranchId); // once the execution branch has completed we enqueue the completed // result with the defer coordinator so it can be delivered. @@ -75,7 +79,7 @@ protected override async ValueTask ExecuteAsync(CancellationToken cancellationTo public void Initialize( OperationContext parentContext, object? parent, - ImmutableDictionary scopedContext, + IImmutableDictionary scopedContext, SelectionSet selectionSet, Path selectionPath, int executionBranchId, diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs index 6f7029808ce..c173be04219 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs @@ -1,5 +1,4 @@ using System.Collections.Immutable; -using System.Diagnostics; using HotChocolate.Text.Json; namespace HotChocolate.Execution.Processing.Tasks; @@ -18,12 +17,6 @@ public void Initialize( int executionBranchId, DeferUsage? deferUsage) { - // defer usage must be set if the executionBranchId is not the main branch id. - // defer usage must not be set if the executionBranchId is the main branch id. - Debug.Assert( - (executionBranchId == DeferExecutionCoordinator.MainBranchId && deferUsage is null) - || (executionBranchId > DeferExecutionCoordinator.MainBranchId && deferUsage is not null)); - _operationContext = operationContext; _selection = selection; _context.Initialize(parent, selection, resultValue, operationContext, scopedContextData); @@ -44,7 +37,7 @@ internal bool Reset() _context.Clean(); Status = ExecutionTaskStatus.WaitingToRun; IsSerial = false; - BranchId = DeferExecutionCoordinator.MainBranchId; + BranchId = int.MinValue; DeferUsage = null; IsRegistered = false; Next = null; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs index 5654de08b7f..3275e38e632 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs @@ -24,7 +24,7 @@ internal sealed partial class ResolverTask(ObjectPool objectPool) /// Used by the defer coordinator to track which deferred execution branch /// this task contributes results to. /// - internal int BranchId { get; private set; } + public int BranchId { get; private set; } /// /// Gets the primary defer usage that caused this execution branch to be created. @@ -77,7 +77,7 @@ public ExecutionTaskKind Kind public bool IsRegistered { get; set; } /// - public bool IsDeferred => BranchId != DeferExecutionCoordinator.MainBranchId; + public bool IsDeferred => DeferUsage is not null; /// public void BeginExecute(CancellationToken cancellationToken) diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs index 79e43365f2f..234df7d785f 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs @@ -1,10 +1,9 @@ using System.Collections.Immutable; +using System.Buffers; using System.Diagnostics; using HotChocolate.Text.Json; using HotChocolate.Types; using static HotChocolate.Execution.Processing.ValueCompletion; -using static HotChocolate.Execution.Processing.DeferExecutionCoordinator; -using System.Buffers; namespace HotChocolate.Execution.Processing.Tasks; @@ -21,6 +20,7 @@ public static void EnqueueRootResolverTasks( var selectionSet = resultValue.AssertSelectionSet(); var scheduler = operationContext.Scheduler; var bufferedTasks = s_pool.Rent(resultValue.GetPropertyCount()); + var mainBranchId = operationContext.ExecutionBranchId; var data = resultValue.EnumerateObject(); var i = 0; @@ -53,7 +53,7 @@ public static void EnqueueRootResolverTasks( if (!branches.TryGetValue(deferUsage, out var branchId)) { - branchId = coordinator.Branch(MainBranchId, Path.Root, deferUsage); + branchId = coordinator.Branch(mainBranchId, Path.Root, deferUsage); branches = branches.Add(deferUsage, branchId); } @@ -309,6 +309,8 @@ private static void ResolveAndCompleteInline( private sealed class NoOpExecutionTask(OperationContext context) : ExecutionTask { + public override int BranchId => context.ExecutionBranchId; + public override bool IsDeferred => false; protected override IExecutionTaskContext Context { get; } = context; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs index eb72617696e..1a2a89ade5f 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs @@ -10,7 +10,7 @@ internal sealed partial class WorkScheduler : IObserver /// /// Execute the work. /// - public async Task ExecuteAsync() + public async Task ExecuteAsync1() { AssertNotPooled(); @@ -25,10 +25,17 @@ public async Task ExecuteAsync() } /// - /// Execute the work. + /// Waits for all tasks in the specified execution branch to complete. + /// Returns immediately if the branch has already completed or was never registered. /// - public Task WaitForCompletionAsync(int executionBranchId, CancellationToken cancellationToken) - => throw new NotImplementedException(); + public ValueTask WaitForCompletionAsync(int executionBranchId) + { + AssertNotPooled(); + + return _activeBranches.TryGetValue(executionBranchId, out var branch) + ? branch.WaitForCompletionAsync(operationContext.RequestAborted) + : ValueTask.CompletedTask; + } private async Task ExecuteInternalAsync(IExecutionTask?[] buffer) { diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs index 8304359ae77..24b6ebe971a 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs @@ -27,8 +27,9 @@ internal sealed partial class WorkScheduler(OperationContext operationContext) private bool _isCompleted; private bool _isInitialized; - public void Initialize(IBatchDispatcher batchDispatcher) + public void Initialize(RequestContext requestContext, IBatchDispatcher batchDispatcher) { + _requestContext = requestContext; _batchDispatcher = batchDispatcher; _batchDispatcherSession = _batchDispatcher.Subscribe(this); @@ -44,9 +45,10 @@ public void Initialize(IBatchDispatcher batchDispatcher) public void Reset() { + var requestContext = _requestContext; var batchDispatcher = _batchDispatcher; Clear(); - Initialize(batchDispatcher); + Initialize(requestContext, batchDispatcher); } public void Clear() @@ -54,6 +56,7 @@ public void Clear() _work.Clear(); _serial.Clear(); _completed.Clear(); + _activeBranches.Clear(); _signal.Reset(); _result = null!; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs index 5ae45e697c2..850224354d9 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs @@ -5,6 +5,8 @@ namespace HotChocolate.Execution.Processing; /// internal sealed partial class WorkScheduler { + private readonly Dictionary _activeBranches = []; + /// /// Defines if the execution is completed. /// @@ -36,6 +38,14 @@ public void Register(IExecutionTask task) lock (_sync) { work.Push(task); + + if (!_activeBranches.TryGetValue(task.BranchId, out var branch)) + { + branch = new Branch(task.BranchId); + _activeBranches.Add(task.BranchId, branch); + } + + branch.RegisterTask(); } _signal.Set(); @@ -50,7 +60,7 @@ public void Register(ReadOnlySpan tasks) lock (_sync) { - for (var i = tasks.Length; i >= 0; i--) + for (var i = tasks.Length - 1; i >= 0; i--) { var task = tasks[i]; task.Id = Interlocked.Increment(ref _nextId); @@ -64,6 +74,14 @@ public void Register(ReadOnlySpan tasks) { _work.Push(task); } + + if (!_activeBranches.TryGetValue(task.BranchId, out var branch)) + { + branch = new Branch(task.BranchId); + _activeBranches.Add(task.BranchId, branch); + } + + branch.RegisterTask(); } } @@ -88,9 +106,39 @@ public void Complete(IExecutionTask task) lock (_sync) { + if (_activeBranches.TryGetValue(task.BranchId, out var branch) + && branch.CompleteTask()) + { + _activeBranches.Remove(task.BranchId); + branch.Complete(); + } + _signal.Set(); } } } } + + private sealed class Branch(int id) + { + private readonly AsyncManualResetEvent _signal = new(); + private int _runningTasks; + + public int Id { get; } = id; + + public int RunningTasks => _runningTasks; + + public void RegisterTask() => _runningTasks++; + + public bool CompleteTask() => --_runningTasks == 0; + + public void Complete() => _signal.Set(); + + public async ValueTask WaitForCompletionAsync(CancellationToken cancellationToken) + { + await using var registration = cancellationToken.Register(_signal.Set); + await _signal; + cancellationToken.ThrowIfCancellationRequested(); + } + } } From d09df589b5f39514e5ba1eddde679d74c4c1557d Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 9 Feb 2026 23:41:53 +0100 Subject: [PATCH 07/46] Removed mutation transaction scope --- ...cutorBuilderExtensions.TransactionScope.cs | 116 ------------------ ...uestExecutorServiceCollectionExtensions.cs | 1 - .../Pipeline/OperationExecutionMiddleware.cs | 53 +++----- .../Processing/DefaultTransactionScope.cs | 57 --------- .../DefaultTransactionScopeHandler.cs | 32 ----- .../Execution/Processing/ITransactionScope.cs | 12 -- .../Processing/ITransactionScopeHandler.cs | 19 --- .../Processing/NoOpTransactionScope.cs | 15 --- .../Processing/NoOpTransactionScopeHandler.cs | 12 -- .../TransactionScopeHandlerTests.cs | 106 ---------------- ..._Default_There_Is_No_TransactionScope.snap | 5 - ...andler_Creates_SystemTransactionScope.snap | 5 - 12 files changed, 16 insertions(+), 417 deletions(-) delete mode 100644 src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorBuilderExtensions.TransactionScope.cs delete mode 100644 src/HotChocolate/Core/src/Types/Execution/Processing/DefaultTransactionScope.cs delete mode 100644 src/HotChocolate/Core/src/Types/Execution/Processing/DefaultTransactionScopeHandler.cs delete mode 100644 src/HotChocolate/Core/src/Types/Execution/Processing/ITransactionScope.cs delete mode 100644 src/HotChocolate/Core/src/Types/Execution/Processing/ITransactionScopeHandler.cs delete mode 100644 src/HotChocolate/Core/src/Types/Execution/Processing/NoOpTransactionScope.cs delete mode 100644 src/HotChocolate/Core/src/Types/Execution/Processing/NoOpTransactionScopeHandler.cs delete mode 100644 src/HotChocolate/Core/test/Execution.Tests/TransactionScopeHandlerTests.cs delete mode 100644 src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TransactionScopeHandlerTests.By_Default_There_Is_No_TransactionScope.snap delete mode 100644 src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TransactionScopeHandlerTests.DefaultTransactionScopeHandler_Creates_SystemTransactionScope.snap diff --git a/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorBuilderExtensions.TransactionScope.cs b/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorBuilderExtensions.TransactionScope.cs deleted file mode 100644 index 230f155b4d3..00000000000 --- a/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorBuilderExtensions.TransactionScope.cs +++ /dev/null @@ -1,116 +0,0 @@ -using HotChocolate.Execution.Configuration; -using HotChocolate.Execution.Processing; -using Microsoft.Extensions.DependencyInjection.Extensions; - -// ReSharper disable once CheckNamespace -namespace Microsoft.Extensions.DependencyInjection; - -public static partial class RequestExecutorBuilderExtensions -{ - /// - /// Adds a custom transaction scope handler to the schema. - /// - /// - /// The request executor builder. - /// - /// - /// The concrete type of the transaction scope handler. - /// - /// - /// The request executor builder. - /// - /// - /// The is null. - /// - /// - /// The will be activated with the of the schema services. - /// If your needs to access application services you need to - /// make the services available in the schema services via . - /// - public static IRequestExecutorBuilder AddTransactionScopeHandler( - this IRequestExecutorBuilder builder) - where T : class, ITransactionScopeHandler - { - ArgumentNullException.ThrowIfNull(builder); - - // we host the transaction scope in the global DI. - builder.Services.TryAddSingleton(); - - return ConfigureSchemaServices( - builder, - static services => - { - services.RemoveAll(); - services.AddSingleton(); - }); - } - - /// - /// Adds a custom transaction scope handler to the schema. - /// - /// - /// The request executor builder. - /// - /// - /// A factory to create the transaction scope. - /// - /// - /// The request executor builder. - /// - /// - /// - /// The passed to the - /// is for the schema services. If you need to access application services - /// you need to either make the services available in the schema services - /// via or use - /// - /// to access the application services from within the schema service provider. - /// - public static IRequestExecutorBuilder AddTransactionScopeHandler( - this IRequestExecutorBuilder builder, - Func factory) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(factory); - - return ConfigureSchemaServices( - builder, - services => - { - services.RemoveAll(); - services.AddSingleton(factory); - }); - } - - /// - /// Adds the which uses - /// for mutation transactions. - /// - /// - /// The request executor builder. - /// - /// - /// The request executor builder. - /// - /// - /// The is null. - /// - public static IRequestExecutorBuilder AddDefaultTransactionScopeHandler( - this IRequestExecutorBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); - - return AddTransactionScopeHandler(builder); - } - - internal static IRequestExecutorBuilder TryAddNoOpTransactionScopeHandler( - this IRequestExecutorBuilder builder) - { - builder.Services.TryAddSingleton(); - - return ConfigureSchemaServices( - builder, - static services => - services.TryAddSingleton()); - } -} diff --git a/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs index 3e8fd18f4e5..ede52a9b255 100644 --- a/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs @@ -138,7 +138,6 @@ private static DefaultRequestExecutorBuilder CreateBuilder( { var builder = new DefaultRequestExecutorBuilder(services, schemaName); - builder.TryAddNoOpTransactionScopeHandler(); builder.TryAddTypeInterceptor(); builder.TryAddTypeInterceptor(); diff --git a/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs b/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs index da39cc93d89..129482177ee 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs @@ -17,7 +17,6 @@ internal sealed class OperationExecutionMiddleware private readonly IFactory _contextFactory; private readonly QueryExecutor _queryExecutor; private readonly SubscriptionExecutor _subscriptionExecutor; - private readonly ITransactionScopeHandler _transactionScopeHandler; private readonly IExecutionDiagnosticEvents _diagnosticEvents; private object? _cachedQuery; private object? _cachedMutation; @@ -27,20 +26,17 @@ private OperationExecutionMiddleware( IFactory contextFactory, QueryExecutor queryExecutor, SubscriptionExecutor subscriptionExecutor, - ITransactionScopeHandler transactionScopeHandler, IExecutionDiagnosticEvents diagnosticEvents) { ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(contextFactory); ArgumentNullException.ThrowIfNull(queryExecutor); ArgumentNullException.ThrowIfNull(subscriptionExecutor); - ArgumentNullException.ThrowIfNull(transactionScopeHandler); _next = next; _contextFactory = contextFactory; _queryExecutor = queryExecutor; _subscriptionExecutor = subscriptionExecutor; - _transactionScopeHandler = transactionScopeHandler; _diagnosticEvents = diagnosticEvents; } @@ -302,7 +298,7 @@ private async Task ExecuteQueryOrMutationNoStreamAsync( try { - return await ExecuteQueryOrMutationAsync( + return (OperationResult)await ExecuteQueryOrMutationAsync( context, batchDispatcher, operation, @@ -326,7 +322,7 @@ private async Task ExecuteQueryOrMutationNoStreamAsync( } } - private async Task ExecuteQueryOrMutationAsync( + private async Task ExecuteQueryOrMutationAsync( RequestContext context, IBatchDispatcher batchDispatcher, Operation operation, @@ -334,27 +330,8 @@ private async Task ExecuteQueryOrMutationAsync( IVariableValueCollection variables, int variableIndex = -1) { - if (operation.Definition.Operation is OperationType.Query) - { - var query = GetQueryRootValue(context); - - operationContext.Initialize( - context, - context.RequestServices, - batchDispatcher, - operation, - variables, - query, - () => query, - variableIndex); - - return await _queryExecutor.ExecuteAsync(operationContext).ConfigureAwait(false); - } - if (operation.Definition.Operation is OperationType.Mutation) { - using var transactionScope = _transactionScopeHandler.Create(context); - var mutation = GetMutationRootValue(context); operationContext.Initialize( @@ -367,17 +344,22 @@ private async Task ExecuteQueryOrMutationAsync( () => GetQueryRootValue(context), variableIndex); - var result = await _queryExecutor.ExecuteAsync(operationContext).ConfigureAwait(false); - - // we capture the result here so that we can capture it in the transaction scope. - context.Result = result; - - // we complete the transaction scope and are done. - transactionScope.Complete(); - return result; + return await _queryExecutor.ExecuteAsync(operationContext).ConfigureAwait(false); } - throw new InvalidOperationException(); + var query = GetQueryRootValue(context); + + operationContext.Initialize( + context, + context.RequestServices, + batchDispatcher, + operation, + variables, + query, + () => query, + variableIndex); + + return await _queryExecutor.ExecuteAsync(operationContext).ConfigureAwait(false); } private object? GetQueryRootValue(RequestContext context) @@ -439,15 +421,12 @@ public static RequestMiddlewareConfiguration Create() var contextFactory = factoryContext.Services.GetRequiredService>(); var queryExecutor = factoryContext.SchemaServices.GetRequiredService(); var subscriptionExecutor = factoryContext.SchemaServices.GetRequiredService(); - var transactionScopeHandler = - factoryContext.SchemaServices.GetRequiredService(); var diagnosticEvents = factoryContext.SchemaServices.GetRequiredService(); var middleware = new OperationExecutionMiddleware( next, contextFactory, queryExecutor, subscriptionExecutor, - transactionScopeHandler, diagnosticEvents); return async context => diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DefaultTransactionScope.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DefaultTransactionScope.cs deleted file mode 100644 index a9d6aed608e..00000000000 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/DefaultTransactionScope.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Transactions; - -namespace HotChocolate.Execution.Processing; - -/// -/// Represents the default mutation transaction scope implementation. -/// -public class DefaultTransactionScope : ITransactionScope -{ - /// - /// Initializes a new instance of . - /// - /// - /// The GraphQL request context. - /// - /// - /// The mutation transaction scope. - /// - public DefaultTransactionScope(RequestContext context, TransactionScope transaction) - { - ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(transaction); - - Context = context; - Transaction = transaction; - } - - /// - /// Gets GraphQL request context. - /// - protected RequestContext Context { get; } - - /// - /// Gets the mutation transaction scope. - /// - protected TransactionScope Transaction { get; } - - /// - /// Completes a transaction (commits or discards the changes). - /// - public void Complete() - { - if (Context.Result is OperationResult { Data: not null, Errors: null or { Count: 0 } }) - { - Transaction.Complete(); - } - } - - /// - /// Performs application-defined tasks associated with freeing, - /// releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - Transaction.Dispose(); - } -} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DefaultTransactionScopeHandler.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DefaultTransactionScopeHandler.cs deleted file mode 100644 index 01355a09534..00000000000 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/DefaultTransactionScopeHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Transactions; - -namespace HotChocolate.Execution.Processing; - -/// -/// Represents the default mutation transaction scope handler implementation. -/// -public class DefaultTransactionScopeHandler : ITransactionScopeHandler -{ - /// - /// Creates a new transaction scope for the current - /// request represented by the . - /// - /// - /// The GraphQL request context. - /// - /// - /// Returns a new . - /// - public virtual ITransactionScope Create(RequestContext context) - { - return new DefaultTransactionScope( - context, - new TransactionScope( - TransactionScopeOption.Required, - new TransactionOptions - { - IsolationLevel = IsolationLevel.ReadCommitted - }, - TransactionScopeAsyncFlowOption.Enabled)); - } -} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/ITransactionScope.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/ITransactionScope.cs deleted file mode 100644 index cc8690c559f..00000000000 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/ITransactionScope.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace HotChocolate.Execution.Processing; - -/// -/// Represents a mutation transaction scope. -/// -public interface ITransactionScope : IDisposable -{ - /// - /// Completes a transaction (commits or discards the changes). - /// - void Complete(); -} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/ITransactionScopeHandler.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/ITransactionScopeHandler.cs deleted file mode 100644 index 85b87cd9435..00000000000 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/ITransactionScopeHandler.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace HotChocolate.Execution.Processing; - -/// -/// Allows to make mutation execution transactional. -/// -public interface ITransactionScopeHandler -{ - /// - /// Creates a new transaction scope for the current - /// request represented by the . - /// - /// - /// The GraphQL request context. - /// - /// - /// Returns a new . - /// - ITransactionScope Create(RequestContext context); -} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/NoOpTransactionScope.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/NoOpTransactionScope.cs deleted file mode 100644 index 8d0a783ca05..00000000000 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/NoOpTransactionScope.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace HotChocolate.Execution.Processing; - -/// -/// This transaction scope represents a non transactional mutation transaction scope. -/// -internal sealed class NoOpTransactionScope : ITransactionScope -{ - public void Complete() - { - } - - public void Dispose() - { - } -} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/NoOpTransactionScopeHandler.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/NoOpTransactionScopeHandler.cs deleted file mode 100644 index 5080c0bc943..00000000000 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/NoOpTransactionScopeHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace HotChocolate.Execution.Processing; - -/// -/// This transaction scope handler represents creates -/// a non transactional mutation transaction scope. -/// -internal sealed class NoOpTransactionScopeHandler : ITransactionScopeHandler -{ - private readonly NoOpTransactionScope _noOpTransaction = new(); - - public ITransactionScope Create(RequestContext context) => _noOpTransaction; -} diff --git a/src/HotChocolate/Core/test/Execution.Tests/TransactionScopeHandlerTests.cs b/src/HotChocolate/Core/test/Execution.Tests/TransactionScopeHandlerTests.cs deleted file mode 100644 index b08e86b3b1f..00000000000 --- a/src/HotChocolate/Core/test/Execution.Tests/TransactionScopeHandlerTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -using HotChocolate.Execution.Processing; -using HotChocolate.Tests; -using Microsoft.Extensions.DependencyInjection; - -namespace HotChocolate.Execution; - -public class TransactionScopeHandlerTests -{ - [Fact] - public async Task Custom_Transaction_Is_Correctly_Completed_and_Disposed() - { - var completed = false; - var disposed = false; - - void Complete() => completed = true; - void Dispose() => disposed = true; - - await new ServiceCollection() - .AddGraphQL() - .AddQueryType() - .AddMutationType() - .ModifyRequestOptions(o => o.ExecutionTimeout = TimeSpan.FromMilliseconds(100)) - .AddTransactionScopeHandler(_ => new MockTransactionScopeHandler(Complete, Dispose)) - .ExecuteRequestAsync("mutation { doNothing }"); - - Assert.True(completed, "transaction must be completed"); - Assert.True(disposed, "transaction must be disposed"); - } - - [Fact] - public async Task Custom_Transaction_Is_Detects_Error_and_Disposes() - { - var completed = false; - var disposed = false; - - void Complete() => completed = true; - void Dispose() => disposed = true; - - await new ServiceCollection() - .AddGraphQL() - .AddQueryType() - .AddMutationType() - .AddTransactionScopeHandler(_ => new MockTransactionScopeHandler(Complete, Dispose)) - .ExecuteRequestAsync("mutation { doError }"); - - Assert.False(completed, "transaction was not completed due to error"); - Assert.True(disposed, "transaction must be disposed"); - } - - [Fact] - public async Task DefaultTransactionScopeHandler_Creates_SystemTransactionScope() - { - await new ServiceCollection() - .AddGraphQL() - .AddQueryType() - .AddMutationType() - .AddDefaultTransactionScopeHandler() - .ExecuteRequestAsync("mutation { foundTransactionScope }") - .MatchSnapshotAsync(); - } - - [Fact] - public async Task By_Default_There_Is_No_TransactionScope() - { - await new ServiceCollection() - .AddGraphQL() - .AddQueryType() - .AddMutationType() - .ExecuteRequestAsync("mutation { foundTransactionScope }") - .MatchSnapshotAsync(); - } - - public class Query - { - public string DoNothing() => "Hello"; - } - - public class Mutation - { - public string DoNothing() => "Hello"; - - public string DoError() => throw new GraphQLException("I am broken!"); - - public bool FoundTransactionScope() => - System.Transactions.Transaction.Current is not null; - } - - public class MockTransactionScopeHandler(Action complete, Action dispose) : ITransactionScopeHandler - { - public ITransactionScope Create(RequestContext context) - => new MockTransactionScope(complete, dispose, context); - } - - public class MockTransactionScope(Action complete, Action dispose, RequestContext context) : ITransactionScope - { - public void Complete() - { - if (context.Result is OperationResult { Data: not null, Errors: null or { Count: 0 } }) - { - complete(); - } - } - - public void Dispose() => dispose(); - } -} diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TransactionScopeHandlerTests.By_Default_There_Is_No_TransactionScope.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TransactionScopeHandlerTests.By_Default_There_Is_No_TransactionScope.snap deleted file mode 100644 index 441f405f1e7..00000000000 --- a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TransactionScopeHandlerTests.By_Default_There_Is_No_TransactionScope.snap +++ /dev/null @@ -1,5 +0,0 @@ -{ - "data": { - "foundTransactionScope": false - } -} diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TransactionScopeHandlerTests.DefaultTransactionScopeHandler_Creates_SystemTransactionScope.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TransactionScopeHandlerTests.DefaultTransactionScopeHandler_Creates_SystemTransactionScope.snap deleted file mode 100644 index 7d862e77e4e..00000000000 --- a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TransactionScopeHandlerTests.DefaultTransactionScopeHandler_Creates_SystemTransactionScope.snap +++ /dev/null @@ -1,5 +0,0 @@ -{ - "data": { - "foundTransactionScope": true - } -} From 8c813644abb612525f0a7567aefc6d5a58c29f26 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 10 Feb 2026 00:06:52 +0100 Subject: [PATCH 08/46] Reworked the execution middleware --- .../Pipeline/OperationExecutionMiddleware.cs | 207 +++++------------- 1 file changed, 51 insertions(+), 156 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs b/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs index 129482177ee..c2ae9ef309f 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs @@ -65,9 +65,21 @@ public async ValueTask InvokeAsync( using (_diagnosticEvents.ExecuteOperation(context)) { - if (context.VariableValues.Length is 0 or 1) + if (operation.Definition.Operation is OperationType.Subscription) { - await ExecuteOperationRequestAsync(context, batchDispatcher, operation).ConfigureAwait(false); + context.Result = await _subscriptionExecutor + .ExecuteAsync(context, () => GetQueryRootValue(context)) + .ConfigureAwait(false); + } + else if (context.VariableValues.Length is 0 or 1) + { + context.Result = + await ExecuteQueryOrMutationAsync( + context, + batchDispatcher, + operation, + context.VariableValues[0]) + .ConfigureAwait(false); } else { @@ -83,59 +95,16 @@ public async ValueTask InvokeAsync( } } - private async Task ExecuteOperationRequestAsync( - RequestContext context, - IBatchDispatcher batchDispatcher, - Operation operation) - { - if (operation.Definition.Operation is OperationType.Subscription) - { - context.Result = await _subscriptionExecutor - .ExecuteAsync(context, () => GetQueryRootValue(context)) - .ConfigureAwait(false); - } - else - { - context.Result = - await ExecuteQueryOrMutationAsync( - context, - batchDispatcher, - operation, - context.VariableValues[0]) - .ConfigureAwait(false); - } - } - private async Task ExecuteVariableBatchRequestAsync( RequestContext context, IBatchDispatcher batchDispatcher, Operation operation) - { - if (operation.Definition.Operation is OperationType.Query) - { - await ExecuteVariableBatchRequestOptimizedAsync(context, batchDispatcher, operation); - return; - } - - var variableSet = context.VariableValues; - var tasks = new Task[variableSet.Length]; - - for (var i = 0; i < variableSet.Length; i++) - { - tasks[i] = ExecuteQueryOrMutationNoStreamAsync(context, batchDispatcher, operation, variableSet[i], i); - } - - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - context.Result = new OperationResultBatch([.. results]); - } - - private async Task ExecuteVariableBatchRequestOptimizedAsync( - RequestContext context, - IBatchDispatcher batchDispatcher, - Operation operation) { var variableSets = context.VariableValues; - var query = GetQueryRootValue(context); + var queryRoot = GetQueryRootValue(context); + var rootValue = operation.Definition.Operation is OperationType.Mutation + ? GetMutationRootValue(context) + : queryRoot; var operationContextBuffer = ArrayPool.Shared.Rent(variableSets.Length); var resultBuffer = ArrayPool.Shared.Rent(variableSets.Length); @@ -145,7 +114,8 @@ private async Task ExecuteVariableBatchRequestOptimizedAsync( context, batchDispatcher, operation, - query, + rootValue, + queryRoot, operationContextBuffer.AsSpan(0, variableSets.Length), variableSets[variableIndex], variableIndex, @@ -155,8 +125,9 @@ private async Task ExecuteVariableBatchRequestOptimizedAsync( try { await _queryExecutor.ExecuteBatchAsync( - operationContextBuffer.AsMemory(0, variableSets.Length), - resultBuffer.AsMemory(0, variableSets.Length)); + operationContextBuffer, + resultBuffer, + variableSets.Length); context.Result = new OperationResultBatch([.. resultBuffer.AsSpan(0, variableSets.Length)]); } @@ -178,7 +149,8 @@ static void Initialize( RequestContext context, IBatchDispatcher batchDispatcher, Operation operation, - object? query, + object? rootValue, + object? queryRoot, Span operationContexts, IVariableValueCollection variables, int variableIndex, @@ -193,8 +165,8 @@ static void Initialize( batchDispatcher, operation, variables, - query, - () => query, + rootValue, + () => queryRoot, variableIndex); operationContexts[variableIndex] = operationContextOwner; @@ -249,68 +221,34 @@ private async Task ExecuteQueryOrMutationAsync( try { - var result = - await ExecuteQueryOrMutationAsync( - context, - batchDispatcher, - operation, - operationContext, - variables) - .ConfigureAwait(false); - - // TODO : DEFER - // if (operationContext.DeferredScheduler.HasResults) - // { - // var results = operationContext.DeferredScheduler.CreateResultStream(result); - // var responseStream = new ResponseStream(() => results, ExecutionResultKind.DeferredResult); - // responseStream.RegisterForCleanup(result); - // responseStream.RegisterForCleanup(operationContextOwner); - // operationContextOwner = null; - // return responseStream; - // } + var queryRoot = GetQueryRootValue(context); + var rootValue = operation.Definition.Operation is OperationType.Mutation + ? GetMutationRootValue(context) + : queryRoot; - return result; - } - catch (OperationCanceledException) - { - // if an operation is canceled we will abandon the rented operation context - // to ensure that that abandoned tasks do not leak into new operations. - operationContextOwner = null; + operationContext.Initialize( + context, + context.RequestServices, + batchDispatcher, + operation, + variables, + rootValue, + () => queryRoot); - // we rethrow so that another middleware can deal with the cancellation. - throw; - } - finally - { - operationContextOwner?.Dispose(); - } - } + var result = await _queryExecutor.ExecuteAsync(operationContext).ConfigureAwait(false); - private async Task ExecuteQueryOrMutationNoStreamAsync( - RequestContext context, - IBatchDispatcher batchDispatcher, - Operation operation, - IVariableValueCollection variables, - int variableIndex) - { - var operationContextOwner = _contextFactory.Create(); - var operationContext = operationContextOwner.OperationContext; + if (result.IsStreamResult()) + { + result.RegisterForCleanup(operationContextOwner); + operationContextOwner = null; + } - try - { - return (OperationResult)await ExecuteQueryOrMutationAsync( - context, - batchDispatcher, - operation, - operationContext, - variables, - variableIndex) - .ConfigureAwait(false); + return result; } catch (OperationCanceledException) { // if an operation is canceled we will abandon the rented operation context - // to ensure that the abandoned tasks do not leak into new operations. + // to ensure that that abandoned tasks do not leak into new operations. operationContextOwner = null; // we rethrow so that another middleware can deal with the cancellation. @@ -322,46 +260,6 @@ private async Task ExecuteQueryOrMutationNoStreamAsync( } } - private async Task ExecuteQueryOrMutationAsync( - RequestContext context, - IBatchDispatcher batchDispatcher, - Operation operation, - OperationContext operationContext, - IVariableValueCollection variables, - int variableIndex = -1) - { - if (operation.Definition.Operation is OperationType.Mutation) - { - var mutation = GetMutationRootValue(context); - - operationContext.Initialize( - context, - context.RequestServices, - batchDispatcher, - operation, - variables, - mutation, - () => GetQueryRootValue(context), - variableIndex); - - return await _queryExecutor.ExecuteAsync(operationContext).ConfigureAwait(false); - } - - var query = GetQueryRootValue(context); - - operationContext.Initialize( - context, - context.RequestServices, - batchDispatcher, - operation, - variables, - query, - () => query, - variableIndex); - - return await _queryExecutor.ExecuteAsync(operationContext).ConfigureAwait(false); - } - private object? GetQueryRootValue(RequestContext context) => RootValueResolver.Resolve( context, @@ -391,11 +289,10 @@ private static bool IsOperationAllowed(Operation operation, IOperationRequest re _ => true }; - // TODO : DEFER - // if (allowed && operation.HasIncrementalParts) - // { - // return allowed && (request.Flags & AllowStreams) == AllowStreams; - // } + if (allowed && operation.HasDeferredSelections) + { + return (request.Flags & AllowStreams) == AllowStreams; + } return allowed; } @@ -406,9 +303,7 @@ private static bool IsRequestTypeAllowed( { if (variables is { Count: > 1 }) { - // TODO : DEFER return operation.Definition.Operation is not OperationType.Subscription; - // && !operation.HasIncrementalParts; } return true; From 26f8350fc6552da622aabe4aa09c78c10beb6a2f Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 10 Feb 2026 01:13:32 +0100 Subject: [PATCH 09/46] Fixed more issues on defer --- .../JsonResultFormatter.cs | 2 - .../Execution/JsonValueFormatter.cs | 32 ++++++++++--- .../Execution/ResultFieldNames.cs | 5 ++ .../Processing/DeferExecutionCoordinator.cs | 47 ++++++++++++------- .../Processing/MiddlewareContext.Arguments.cs | 2 +- .../SubscriptionExecutor.Subscription.cs | 6 +-- .../Processing/Tasks/ResolverTask.cs | 3 +- .../Execution/Nodes/Operation.cs | 2 + .../Execution/Nodes/Selection.cs | 5 ++ .../Execution/Nodes/SelectionSet.cs | 2 + 10 files changed, 76 insertions(+), 30 deletions(-) diff --git a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs index 49d6da30b07..c8e1f420ced 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs @@ -15,7 +15,6 @@ public sealed class JsonResultFormatter : IOperationResultFormatter, IExecutionR { private readonly JsonWriterOptions _options; private readonly JsonSerializerOptions _serializerOptions; - private readonly JsonNullIgnoreCondition _nullIgnoreCondition; /// /// Initializes a new instance of with default options. @@ -38,7 +37,6 @@ public JsonResultFormatter(JsonResultFormatterOptions options) { _options = options.CreateWriterOptions() with { SkipValidation = true }; _serializerOptions = options.CreateSerializerOptions(); - _nullIgnoreCondition = options.NullIgnoreCondition; } /// diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs index c445087c874..dcc2d849f9b 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs @@ -328,7 +328,7 @@ public static void WriteIncremental( for (var i = 0; i < completed.Count; i++) { - WriteIncrementalCompletedItem(writer, completed[i]); + WriteIncrementalCompletedItem(writer, completed[i], options, nullIgnoreCondition); } writer.WriteEndArray(); @@ -376,13 +376,24 @@ private static void WriteIncrementalItem( WriteErrors(writer, item.Errors, options, nullIgnoreCondition); } - if (item is IncrementalObjectResult) + if (item is IncrementalObjectResult objectResult) { + if (objectResult.SubPath is not null) + { + writer.WritePropertyName(SubPath); + WritePathValue(writer, objectResult.SubPath); + } + writer.WritePropertyName(Data); - // TODO: Write actual data - writer.WriteStartObject(); - writer.WriteEndObject(); + if (objectResult.Data.HasValue) + { + objectResult.Data.Value.Formatter.WriteDataTo(writer); + } + else + { + writer.WriteNullValue(); + } } else if (item is IIncrementalListResult) { @@ -400,13 +411,22 @@ private static void WriteIncrementalItem( writer.WriteEndObject(); } - private static void WriteIncrementalCompletedItem(JsonWriter writer, CompletedResult item) + private static void WriteIncrementalCompletedItem( + JsonWriter writer, + CompletedResult item, + JsonSerializerOptions options, + JsonNullIgnoreCondition nullIgnoreCondition) { writer.WriteStartObject(); writer.WritePropertyName(Id); writer.WriteNumberValue(item.Id); + if (item.Errors is { Count: > 0 }) + { + WriteErrors(writer, item.Errors, options, nullIgnoreCondition); + } + writer.WriteEndObject(); } diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/ResultFieldNames.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/ResultFieldNames.cs index 79dad9f0905..c591e813f68 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/ResultFieldNames.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/ResultFieldNames.cs @@ -75,6 +75,11 @@ public static class ResultFieldNames /// public static ReadOnlySpan Label => "label"u8; + /// + /// Gets the subPath field name. + /// + public static ReadOnlySpan SubPath => "subPath"u8; + /// /// Gets the hasNext field name /// diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs index 07ec4f4c7ed..8ee8ef58ba7 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs @@ -118,14 +118,7 @@ private void ComposeAndDeliverUnsafe(int branchId, OperationResult result) if (_completed.Remove(childId, out var childResult)) { - incrementalBuilder.Add( - new IncrementalObjectResult( - childId, - childResult.Errors, - subPath: null, - childResult.Data)); - - completedBuilder.Add(new CompletedResult(childId)); + AddCompletedBranch(childId, childResult, incrementalBuilder, completedBuilder); _delivered.Add(childId); _pendingBranches--; processQueue.Enqueue(childId); @@ -146,14 +139,7 @@ private void ComposeAndDeliverUnsafe(int branchId, OperationResult result) if (_completed.Remove(grandchildId, out var gcResult)) { - incrementalBuilder.Add( - new IncrementalObjectResult( - grandchildId, - gcResult.Errors, - subPath: null, - gcResult.Data)); - - completedBuilder.Add(new CompletedResult(grandchildId)); + AddCompletedBranch(grandchildId, gcResult, incrementalBuilder, completedBuilder); _delivered.Add(grandchildId); _pendingBranches--; processQueue.Enqueue(grandchildId); @@ -173,9 +159,12 @@ private void ComposeAndDeliverUnsafe(int branchId, OperationResult result) _pendingBranches--; } + var isComplete = _delivered.Contains(_mainBranchId) && _pendingBranches == 0; + result.HasNext = !isComplete; + _resultChannel.Writer.TryWrite(result); - if (_delivered.Contains(_mainBranchId) && _pendingBranches == 0) + if (isComplete) { _resultChannel.Writer.TryComplete(); } @@ -189,6 +178,30 @@ private bool IsParentDeliveredUnsafe(int branchId) => _branchInfoLookup.TryGetValue(branchId, out var info) && _delivered.Contains(info.ParentBranchId); + private static void AddCompletedBranch( + int branchId, + OperationResult branchResult, + ImmutableList.Builder incrementalBuilder, + ImmutableList.Builder completedBuilder) + { + if (branchResult.Data.HasValue && !branchResult.Data.Value.IsValueNull) + { + // data is valid (possibly with contained errors) — deliver incremental data + incrementalBuilder.Add( + new IncrementalObjectResult( + branchId, + branchResult.Errors, + subPath: null, + branchResult.Data)); + completedBuilder.Add(new CompletedResult(branchId)); + } + else + { + // errors bubbled above the incremental result's path — no data to deliver + completedBuilder.Add(new CompletedResult(branchId, branchResult.Errors)); + } + } + /// /// Gets the child branches that were created from the execution branch /// represented by the specified . diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Arguments.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Arguments.cs index ffe7d969be9..6a42cba2e78 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Arguments.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Arguments.cs @@ -173,7 +173,7 @@ public ArgumentValue ReplaceArgument(string argumentName, ArgumentValue newArgum // copy the argument state. else { - mutableArguments = [with(Arguments)]; + mutableArguments = new Dictionary(Arguments); Arguments = mutableArguments; } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/SubscriptionExecutor.Subscription.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/SubscriptionExecutor.Subscription.cs index 1eda1d3c4a4..11ff915bf98 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/SubscriptionExecutor.Subscription.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/SubscriptionExecutor.Subscription.cs @@ -189,7 +189,8 @@ private async Task OnEvent(object payload) .ExecuteAsync(operationContext, scopedContextData) .ConfigureAwait(false); - return result; + // todo : we still need to think about defer in subscriptions + return result.ExpectOperationResult(); } catch (OperationCanceledException ex) { @@ -256,8 +257,7 @@ private async ValueTask SubscribeAsync() rootSelection, resultMap, operationContext, - _scopedContextData, - null); + _scopedContextData); // it is important that we correctly coerce the arguments before // invoking subscribe. diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs index 3275e38e632..d95750915f4 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs @@ -9,7 +9,8 @@ internal sealed partial class ResolverTask(ObjectPool objectPool) { private readonly MiddlewareContext _context = new(); private readonly List _taskBuffer = []; - private readonly Dictionary _args = [with(StringComparer.Ordinal)]; + private readonly Dictionary _args = + new Dictionary(StringComparer.Ordinal); private OperationContext _operationContext = null!; private Selection _selection = null!; private ExecutionTaskStatus _completionStatus = ExecutionTaskStatus.Completed; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Operation.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Operation.cs index 75e0f95e67b..790f691ddb0 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Operation.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Operation.cs @@ -105,6 +105,8 @@ ISelectionSet IOperation.RootSelectionSet /// public IFeatureCollection Features => _features; + public bool HasDeferredSelections => throw new NotImplementedException(); + /// /// Gets the selection set for the specified /// if the selections named return type is an object type. diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs index dcdca53e219..7eb3b4f51d0 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs @@ -158,6 +158,11 @@ internal void Seal(SelectionSet selectionSet) DeclaringSelectionSet = selectionSet; } + public bool IsDeferred(ulong deferFlags) + { + throw new NotImplementedException(); + } + [Flags] private enum Flags { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs index 2ab53071b58..8b8fc5d8bec 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs @@ -61,6 +61,8 @@ public SelectionSet(int id, IObjectTypeDefinition type, Selection[] selections, /// public ReadOnlySpan Selections => _selections; + public bool HasDeferredSelections => throw new NotImplementedException(); + IEnumerable ISelectionSet.GetSelections() => _selections; /// From 395de39ebc6c860acfb54dac0ee5e7f222264774 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 10 Feb 2026 09:54:45 +0100 Subject: [PATCH 10/46] Fixed more things around defer --- .../GraphQLOverHttpSpecTests.cs | 10 +- .../Execution/OperationResult.cs | 12 +- .../DeferExecutionCoordinator.Pooling.cs | 20 ++- .../Processing/DeferExecutionCoordinator.cs | 142 +++++++++++++----- .../Processing/OperationContext.Pooling.cs | 10 +- .../Execution/Processing/Tasks/DeferTask.cs | 2 +- .../Execution/Processing/WorkScheduler.cs | 66 ++++---- .../Processing/WorkQueueTests.cs | 4 + 8 files changed, 173 insertions(+), 93 deletions(-) diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs index cd8dd9f78f6..f443c0eb85d 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs @@ -328,11 +328,7 @@ public async Task DeferredQuery_Multipart(string? acceptHeader) --- Content-Type: application/json; charset=utf-8 - {"data":{},"hasNext":true} - --- - Content-Type: application/json; charset=utf-8 - - {"incremental":[{"data":{"__typename":"Query"},"path":[]}],"hasNext":false} + {"data":{"__typename":null},"pending":[{"id":2,"path":[]}],"incremental":[{"id":2,"data":{"__typename":"Query"}}],"completed":[{"id":2}],"hasNext":false} ----- """); @@ -371,10 +367,10 @@ public async Task DeferredQuery_EventStream(string acceptHeader) Status Code: OK --------------------------> event: next - data: {"data":{},"hasNext":true} + data: {"data":{"__typename":null},"pending":[{"id":2,"path":[]}],"hasNext":true} event: next - data: {"incremental":[{"data":{"__typename":"Query"},"path":[]}],"hasNext":false} + data: {"incremental":[{"id":2,"data":{"__typename":"Query"}}],"completed":[{"id":2}],"hasNext":false} event: complete diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/OperationResult.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/OperationResult.cs index 9b07a882801..62392d91d4c 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/OperationResult.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/OperationResult.cs @@ -143,7 +143,7 @@ public Path? Path /// /// Gets the data that is being delivered in this operation result. /// - public OperationResultData? Data { get; } + public OperationResultData? Data { get; internal set; } /// /// Gets the GraphQL errors that occurred during execution. @@ -153,7 +153,10 @@ public ImmutableList Errors get => _errors; set { - if (!Data.HasValue && Errors is null or { Count: 0 } && Extensions is null or { Count: 0 }) + if (!Data.HasValue + && Errors is null or { Count: 0 } + && Extensions is null or { Count: 0 } + && Features.Get() is null) { throw new ArgumentException("Either data, errors or extensions must be provided."); } @@ -173,7 +176,10 @@ public ImmutableList Errors get => _extensions; set { - if (!Data.HasValue && Errors is null or { Count: 0 } && Extensions is null or { Count: 0 }) + if (!Data.HasValue + && Errors is null or { Count: 0 } + && Extensions is null or { Count: 0 } + && Features.Get() is null) { throw new ArgumentException("Either data, errors or extensions must be provided."); } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs index 88fd9443917..eaa0c00f7b9 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs @@ -1,5 +1,3 @@ -using System.Threading.Channels; - namespace HotChocolate.Execution.Processing; internal sealed partial class DeferExecutionCoordinator @@ -12,12 +10,6 @@ public void Initialize(BranchTracker branchTracker, int mainBranchId) { _branchTracker = branchTracker; _mainBranchId = mainBranchId; - _resultChannel = Channel.CreateUnbounded( - new UnboundedChannelOptions - { - SingleWriter = true, - SingleReader = true - }); } /// @@ -26,18 +18,24 @@ public void Initialize(BranchTracker branchTracker, int mainBranchId) public void Reset() { _branchIdLookup.Clear(); - _branchInfoLookup.Clear(); - _branches.Clear(); + _branchLookup.Clear(); + _mainBranchChildren?.Clear(); _completed.Clear(); _delivered.Clear(); + _results.Clear(); _branchTracker = null!; - _resultChannel = null!; _pendingBuilder = null; _incrementalBuilder = null; _completedBuilder = null; _processQueue = null; _hasBranches = false; + _isComplete = false; _mainBranchId = 0; _pendingBranches = 0; + + if (_results.Capacity > 64) + { + _results.Capacity = 64; + } } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs index 8ee8ef58ba7..073e2b574d6 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs @@ -1,24 +1,28 @@ using System.Collections.Immutable; -using System.Threading.Channels; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using HotChocolate.Fetching; namespace HotChocolate.Execution.Processing; internal sealed partial class DeferExecutionCoordinator { private readonly object _sync = new(); - private readonly Dictionary _branchIdLookup = []; - private readonly Dictionary _branchInfoLookup = []; - private readonly Dictionary> _branches = []; + private readonly Dictionary _branchIdLookup = []; + private readonly Dictionary _branchLookup = []; + private HashSet? _mainBranchChildren; private readonly Dictionary _completed = []; private readonly HashSet _delivered = []; + private readonly List _results = []; + private readonly AsyncAutoResetEvent _signal = new(); private BranchTracker _branchTracker = null!; private int _mainBranchId; private ImmutableList.Builder? _pendingBuilder; private ImmutableList.Builder? _incrementalBuilder; private ImmutableList.Builder? _completedBuilder; private Queue? _processQueue; - private Channel _resultChannel = null!; private volatile bool _hasBranches; + private volatile bool _isComplete; private int _pendingBranches; /// @@ -33,16 +37,16 @@ internal sealed partial class DeferExecutionCoordinator /// public int Branch(int currentBranchId, Path path, DeferUsage deferUsage) { - var branchInfo = new DeferredBranchInfo(path, deferUsage, currentBranchId); + var key = new DeferredBranchKey(path, deferUsage, currentBranchId); lock (_sync) { - if (!_branchIdLookup.TryGetValue(branchInfo, out var newBranchId)) + if (!_branchIdLookup.TryGetValue(key, out var newBranchId)) { newBranchId = _branchTracker.CreateNewBranchId(); - GetBranchesUnsafe(currentBranchId).Add(newBranchId); - _branchInfoLookup.Add(newBranchId, branchInfo); - _branchIdLookup.Add(branchInfo, newBranchId); + GetChildrenUnsafe(currentBranchId).Add(newBranchId); + _branchLookup.Add(newBranchId, new DeferredBranch(path, deferUsage, currentBranchId)); + _branchIdLookup.Add(key, newBranchId); _hasBranches = true; _pendingBranches++; } @@ -86,15 +90,42 @@ public void EnqueueResult(OperationResult result, int branchId) /// Returns an async stream of composed operation results in delivery order. /// The stream completes automatically when all branches have been delivered. /// - public IAsyncEnumerable ReadResultsAsync( - CancellationToken cancellationToken = default) - => _resultChannel.Reader.ReadAllAsync(cancellationToken); + public async IAsyncEnumerable ReadResultsAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + List? snapshot = null; + await using var registration = cancellationToken.Register(_signal.Set); + + while (!cancellationToken.IsCancellationRequested) + { + await _signal; + + cancellationToken.ThrowIfCancellationRequested(); + + lock (_sync) + { + snapshot ??= []; + snapshot.AddRange(_results); + _results.Clear(); + } + + foreach (var result in snapshot) + { + yield return result; + } + + if (_isComplete) + { + yield break; + } + } + } private void ComposeAndDeliverUnsafe(int branchId, OperationResult result) { - var childBranches = GetBranchesUnsafe(branchId); + var children = GetChildrenUnsafe(branchId); - if (childBranches.Count > 0) + if (children.Count > 0) { var pendingBuilder = _pendingBuilder ??= ImmutableList.CreateBuilder(); var incrementalBuilder = _incrementalBuilder ??= ImmutableList.CreateBuilder(); @@ -106,15 +137,15 @@ private void ComposeAndDeliverUnsafe(int branchId, OperationResult result) completedBuilder.Clear(); processQueue.Clear(); - foreach (var childId in childBranches) + foreach (var childId in children) { - var childInfo = _branchInfoLookup[childId]; + var child = _branchLookup[childId]; pendingBuilder.Add( new PendingResult( childId, - childInfo.Path, - childInfo.Group.Label)); + child.Path, + child.Group.Label)); if (_completed.Remove(childId, out var childResult)) { @@ -127,15 +158,15 @@ private void ComposeAndDeliverUnsafe(int branchId, OperationResult result) while (processQueue.TryDequeue(out var parentId)) { - foreach (var grandchildId in GetBranchesUnsafe(parentId)) + foreach (var grandchildId in GetChildrenUnsafe(parentId)) { - var info = _branchInfoLookup[grandchildId]; + var branch = _branchLookup[grandchildId]; pendingBuilder.Add( new PendingResult( grandchildId, - info.Path, - info.Group.Label)); + branch.Path, + branch.Group.Label)); if (_completed.Remove(grandchildId, out var gcResult)) { @@ -152,6 +183,27 @@ private void ComposeAndDeliverUnsafe(int branchId, OperationResult result) result.Completed = completedBuilder.ToImmutable(); } + // For deferred branches (not main branch), transform the result's data into an incremental result. + // Per spec: only the initial payload has root "data"; subsequent payloads use "incremental" array. + if (branchId != _mainBranchId) + { + var incrementalBuilder = _incrementalBuilder ??= ImmutableList.CreateBuilder(); + var completedBuilder = _completedBuilder ??= ImmutableList.CreateBuilder(); + + if (children.Count == 0) + { + incrementalBuilder.Clear(); + completedBuilder.Clear(); + } + + AddCompletedBranch(branchId, result, incrementalBuilder, completedBuilder); + + result.Incremental = incrementalBuilder.ToImmutable(); + result.Completed = completedBuilder.ToImmutable(); + result.Data = null; + result.Errors = []; + } + _delivered.Add(branchId); if (branchId != _mainBranchId) @@ -162,12 +214,9 @@ private void ComposeAndDeliverUnsafe(int branchId, OperationResult result) var isComplete = _delivered.Contains(_mainBranchId) && _pendingBranches == 0; result.HasNext = !isComplete; - _resultChannel.Writer.TryWrite(result); - - if (isComplete) - { - _resultChannel.Writer.TryComplete(); - } + _results.Add(result); + _isComplete = isComplete; + _signal.Set(); } /// @@ -175,8 +224,8 @@ private void ComposeAndDeliverUnsafe(int branchId, OperationResult result) /// delivered its result to the response stream. /// private bool IsParentDeliveredUnsafe(int branchId) - => _branchInfoLookup.TryGetValue(branchId, out var info) - && _delivered.Contains(info.ParentBranchId); + => _branchLookup.TryGetValue(branchId, out var branch) + && _delivered.Contains(branch.ParentBranchId); private static void AddCompletedBranch( int branchId, @@ -203,19 +252,34 @@ private static void AddCompletedBranch( } /// - /// Gets the child branches that were created from the execution branch - /// represented by the specified . + /// Gets the child branches for the specified branch. + /// For the main branch, uses the dedicated field; for deferred branches, + /// uses the children set stored in the branch lookup. /// - private HashSet GetBranchesUnsafe(int branchId) + private HashSet GetChildrenUnsafe(int branchId) { - if (!_branches.TryGetValue(branchId, out var branches)) + if (branchId == _mainBranchId) { - branches = []; - _branches.Add(branchId, branches); + return _mainBranchChildren ??= []; } - return branches; + ref var branch = ref CollectionsMarshal.GetValueRefOrNullRef(_branchLookup, branchId); + + if (Unsafe.IsNullRef(ref branch)) + { + return []; + } + + return branch.Children ??= []; } - private readonly record struct DeferredBranchInfo(Path Path, DeferUsage Group, int ParentBranchId); + private readonly record struct DeferredBranchKey(Path Path, DeferUsage Group, int ParentBranchId); + + private struct DeferredBranch(Path path, DeferUsage group, int parentBranchId) + { + public Path Path { get; } = path; + public DeferUsage Group { get; } = group; + public int ParentBranchId { get; } = parentBranchId; + public HashSet? Children { get; set; } + } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs index f5d5e0ba606..0fa34f6c917 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs @@ -87,14 +87,14 @@ public void Initialize( Result.RequestIndex = _requestContext.RequestIndex; Result.VariableIndex = variableIndex; - _branchId = _currentBranchTracker.CreateNewBranchId(); - _workScheduler.Initialize(_requestContext, batchDispatcher); - _deferExecutionCoordinator.Initialize(_currentBranchTracker, _branchId); - _currentBranchTracker = _branchTracker; _currentWorkScheduler = _workScheduler; - _isInitialized = true; + + // once the operation context is marked as initialized we can initialize sub components. + _branchId = _currentBranchTracker.CreateNewBranchId(); + _workScheduler.Initialize(_requestContext, batchDispatcher); + _deferExecutionCoordinator.Initialize(_currentBranchTracker, _branchId); } public void InitializeDeferContext( diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs index 2d83b404a9d..3debb89b83e 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs @@ -60,7 +60,7 @@ protected override async ValueTask ExecuteAsync(CancellationToken cancellationTo } finally { - if (i > 1) + if (i > 0) { bufferedTasks.AsSpan(0, i).Clear(); } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs index 850224354d9..574e44a2046 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs @@ -38,14 +38,7 @@ public void Register(IExecutionTask task) lock (_sync) { work.Push(task); - - if (!_activeBranches.TryGetValue(task.BranchId, out var branch)) - { - branch = new Branch(task.BranchId); - _activeBranches.Add(task.BranchId, branch); - } - - branch.RegisterTask(); + RegisterBranchTaskUnsafe(task.BranchId); } _signal.Set(); @@ -75,13 +68,7 @@ public void Register(ReadOnlySpan tasks) _work.Push(task); } - if (!_activeBranches.TryGetValue(task.BranchId, out var branch)) - { - branch = new Branch(task.BranchId); - _activeBranches.Add(task.BranchId, branch); - } - - branch.RegisterTask(); + RegisterBranchTaskUnsafe(task.BranchId); } } @@ -97,24 +84,49 @@ public void Complete(IExecutionTask task) if (task.IsRegistered) { - // complete is thread-safe var work = task.IsSerial ? _serial : _work; + CompleteBranchTask(task.BranchId); + + // complete is thread-safe if (work.Complete()) { _completed.TryAdd(task.Id, true); + _signal.Set(); + } + } + } - lock (_sync) - { - if (_activeBranches.TryGetValue(task.BranchId, out var branch) - && branch.CompleteTask()) - { - _activeBranches.Remove(task.BranchId); - branch.Complete(); - } - - _signal.Set(); - } + private void RegisterBranchTaskUnsafe(int branchId) + { + if (branchId == BranchTracker.SystemBranchId) + { + return; + } + + if (!_activeBranches.TryGetValue(branchId, out var branch)) + { + branch = new Branch(branchId); + _activeBranches.Add(branchId, branch); + } + + branch.RegisterTask(); + } + + private void CompleteBranchTask(int branchId) + { + if (branchId == BranchTracker.SystemBranchId) + { + return; + } + + lock (_sync) + { + if (_activeBranches.TryGetValue(branchId, out var branch) + && branch.CompleteTask()) + { + _activeBranches.Remove(branchId); + branch.Complete(); } } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/WorkQueueTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Processing/WorkQueueTests.cs index 7be41b109fe..3913762b06c 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/WorkQueueTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/WorkQueueTests.cs @@ -134,6 +134,10 @@ public class MockExecutionTask : IExecutionTask public bool IsSerial { get; set; } public bool IsRegistered { get; set; } + public int BranchId => throw new NotImplementedException(); + + public bool IsDeferred => throw new NotImplementedException(); + public void BeginExecute(CancellationToken cancellationToken) { throw new NotImplementedException(); From 9ae122f36bc6c19f960120129da5d26e5f630fbe Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 10 Feb 2026 21:00:03 +0100 Subject: [PATCH 11/46] refined more --- .../MultiPartResultFormatter.cs | 8 +- .../AspNetCore.Tests/DeferOverHttpTests.cs | 277 ++++++++++++++++++ .../IOperation.cs | 8 +- .../ISelectionSet.cs | 2 +- .../Pipeline/OperationExecutionMiddleware.cs | 2 +- .../Processing/DeferExecutionCoordinator.cs | 3 + .../Types/Execution/Processing/Operation.cs | 6 +- .../Execution/Processing/OperationCompiler.cs | 5 +- .../Execution/Processing/QueryExecutor.cs | 4 +- .../Execution/Processing/SelectionSet.cs | 2 +- .../Processing/Tasks/ResolverTaskFactory.cs | 4 +- .../Processing/WorkScheduler.Pooling.cs | 10 +- .../Execution/Nodes/Operation.cs | 2 +- .../Execution/Nodes/SelectionSet.cs | 2 +- .../InlineFragmentOperationRewriter.cs | 140 ++++++++- .../InlineFragmentOperationRewriterResult.cs | 10 + .../Rewriters/MergeSelectionSetRewriter.cs | 6 +- .../InlineFragmentOperationRewriterTests.cs | 272 +++++++++++++++-- 18 files changed, 686 insertions(+), 77 deletions(-) create mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/InlineFragmentOperationRewriterResult.cs diff --git a/src/HotChocolate/AspNetCore/src/Transport.Formatters/MultiPartResultFormatter.cs b/src/HotChocolate/AspNetCore/src/Transport.Formatters/MultiPartResultFormatter.cs index 06d8160d54e..bbfb96fc10f 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Formatters/MultiPartResultFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Formatters/MultiPartResultFormatter.cs @@ -127,6 +127,8 @@ private async ValueTask FormatResponseStreamAsync( while (await enumerator.MoveNextAsync().ConfigureAwait(false)) { + var current = enumerator.Current; + try { if (first || responseStream.Kind is not DeferredResult) @@ -139,9 +141,9 @@ private async ValueTask FormatResponseStreamAsync( MessageHelper.WriteResultHeader(writer); // Next, we write the payload of the part. - MessageHelper.WritePayload(writer, enumerator.Current, _payloadFormatter); + MessageHelper.WritePayload(writer, current, _payloadFormatter); - if (responseStream.Kind is DeferredResult && (enumerator.Current.HasNext ?? false)) + if (responseStream.Kind is DeferredResult && (current.HasNext ?? false)) { // If the result is a deferred result and has a next result, we need to // write a new part so that the client knows that there is more to come. @@ -155,7 +157,7 @@ private async ValueTask FormatResponseStreamAsync( { // The result objects use pooled memory, so we need to ensure that they // return the memory by disposing them. - await enumerator.Current.DisposeAsync().ConfigureAwait(false); + await current.DisposeAsync().ConfigureAwait(false); } } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs new file mode 100644 index 00000000000..d9e121d5fb9 --- /dev/null +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs @@ -0,0 +1,277 @@ +using System.Net; +using System.Net.Http.Json; +using HotChocolate.AspNetCore.Formatters; +using HotChocolate.AspNetCore.Tests.Utilities; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.AspNetCore; + +public class DeferOverHttpTests(TestServerFactory serverFactory) : ServerTestBase(serverFactory) +{ + [Fact] + public async Task Simple_Defer_Multipart() + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = """ + { + product { + name + ... @defer { + description + } + } + } + """ + }); + request.Headers.Add("Accept", "multipart/mixed"); + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("multipart/mixed", response.Content.Headers.ContentType?.MediaType); + + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + + --- + Content-Type: application/json; charset=utf-8 + + {"data":{"product":{"name":"Abc","description":null}},"pending":[{"id":2,"path":["product"]}],"hasNext":true} + --- + Content-Type: application/json; charset=utf-8 + + {"incremental":[{"id":2,"data":{"description":"Abc desc"}}],"completed":[{"id":2}],"hasNext":false} + ----- + + """); + } + + [Fact] + public async Task Simple_Defer_EventStream() + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = """ + { + product { + name + ... @defer { + description + } + } + } + """ + }); + request.Headers.Add("Accept", "text/event-stream"); + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/event-stream", response.Content.Headers.ContentType?.MediaType); + + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + Response: + --------------------------> + TODO: Add expected snapshot + """); + } + + [Fact] + public async Task Defer_List_Multipart() + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = """ + { + products { + name + ... @defer(label: "desc") { + description + } + } + } + """ + }); + request.Headers.Add("Accept", "multipart/mixed"); + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + Response: + --------------------------> + TODO: Add expected snapshot + """); + } + + [Fact] + public async Task Defer_With_Label_Multipart() + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = """ + { + product { + name + ... @defer(label: "productDescription") { + description + } + } + } + """ + }); + request.Headers.Add("Accept", "multipart/mixed"); + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + Response: + --------------------------> + TODO: Add expected snapshot + """); + } + + [Fact] + public async Task Defer_Disabled_By_Variable() + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = """ + query($shouldDefer: Boolean!) { + product { + name + ... @defer(if: $shouldDefer) { + description + } + } + } + """, + variables = new { shouldDefer = false } + }); + request.Headers.Add("Accept", "multipart/mixed"); + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // When defer is disabled, should get a regular JSON response, not multipart + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + Response: + --------------------------> + TODO: Add expected snapshot + """); + } + + private TestServer CreateDeferServer(HttpTransportVersion serverTransportVersion = HttpTransportVersion.Latest) + { + return ServerFactory.Create( + services => services + .AddRouting() + .AddGraphQLServer() + .AddTypeExtension() + .AddDefaultBatchDispatcher() + .AddHttpResponseFormatter( + new HttpResponseFormatterOptions + { + HttpTransportVersion = serverTransportVersion + }) + .ModifyOptions(o => + { + o.EnableDefer = true; + o.EnableStream = true; + }), + app => app + .UseRouting() + .UseEndpoints(endpoints => endpoints.MapGraphQL())); + } + + public sealed class Query + { + public Product GetProduct() + => new("Abc"); + + public IEnumerable GetProducts() + { + yield return new Product("Abc"); + yield return new Product("Def"); + yield return new Product("Ghi"); + } + } + + public sealed record Product(string Name) + { + public async Task GetDescriptionAsync() + { + await Task.Delay(1000); + return Name + " desc"; + } + } +} diff --git a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/IOperation.cs b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/IOperation.cs index aed72b6e1e2..0e3db10c7e6 100644 --- a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/IOperation.cs +++ b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/IOperation.cs @@ -45,14 +45,14 @@ public interface IOperation : IFeatureProvider ISelectionSet RootSelectionSet { get; } /// - /// Gets a value indicating whether any selection set in this operation - /// contains selections that may be deferred based on @defer directives. + /// Gets a value indicating whether this operation contains incremental delivery directives + /// such as @defer or @stream. /// /// - /// true if one or more selection sets in this operation contain deferred selections; + /// true if the operation contains @defer or @stream directives; /// otherwise, false. /// - bool HasDeferredSelections { get; } + bool HasIncrementalParts { get; } /// /// Gets the selection set for the specified and diff --git a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelectionSet.cs b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelectionSet.cs index e7d63490319..f77c192bf47 100644 --- a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelectionSet.cs +++ b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelectionSet.cs @@ -32,7 +32,7 @@ public interface ISelectionSet /// true if one or more selections in this set can be deferred; /// otherwise, false. /// - bool HasDeferredSelections { get; } + bool HasIncrementalParts { get; } /// /// Gets the type that declares this selection set. diff --git a/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs b/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs index c2ae9ef309f..f90776873bd 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs @@ -289,7 +289,7 @@ private static bool IsOperationAllowed(Operation operation, IOperationRequest re _ => true }; - if (allowed && operation.HasDeferredSelections) + if (allowed && operation.HasIncrementalParts) { return (request.Flags & AllowStreams) == AllowStreams; } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs index 073e2b574d6..81b4ae803a5 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs @@ -105,6 +105,7 @@ public async IAsyncEnumerable ReadResultsAsync( lock (_sync) { snapshot ??= []; + snapshot.Clear(); snapshot.AddRange(_results); _results.Clear(); } @@ -149,6 +150,7 @@ private void ComposeAndDeliverUnsafe(int branchId, OperationResult result) if (_completed.Remove(childId, out var childResult)) { + result.RegisterForCleanup(childResult); AddCompletedBranch(childId, childResult, incrementalBuilder, completedBuilder); _delivered.Add(childId); _pendingBranches--; @@ -170,6 +172,7 @@ private void ComposeAndDeliverUnsafe(int branchId, OperationResult result) if (_completed.Remove(grandchildId, out var gcResult)) { + result.RegisterForCleanup(gcResult); AddCompletedBranch(grandchildId, gcResult, incrementalBuilder, completedBuilder); _delivered.Add(grandchildId); _pendingBranches--; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Operation.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Operation.cs index 86418019941..e638f9a283e 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Operation.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Operation.cs @@ -37,7 +37,7 @@ internal Operation( OperationFeatureCollection features, int lastId, object[] elementsById, - bool hasDeferredSelections) + bool hasIncrementalParts) { ArgumentException.ThrowIfNullOrWhiteSpace(id); ArgumentException.ThrowIfNullOrWhiteSpace(hash); @@ -64,7 +64,7 @@ internal Operation( _lastId = lastId; _elementsById = elementsById; _features = features; - HasDeferredSelections = hasDeferredSelections; + HasIncrementalParts = hasIncrementalParts; } /// @@ -125,7 +125,7 @@ ISelectionSet IOperation.RootSelectionSet IFeatureCollection IFeatureProvider.Features => Features; /// - public bool HasDeferredSelections { get; } + public bool HasIncrementalParts { get; } /// /// Gets the selection set for the specified diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationCompiler.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationCompiler.cs index 9151ecc91b1..7e8fbef5906 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationCompiler.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationCompiler.cs @@ -79,7 +79,8 @@ public Operation Compile( ArgumentNullException.ThrowIfNull(document); // Before we can plan an operation, we must de-fragmentize it and remove static include conditions. - document = _documentRewriter.RewriteDocument(document, operationName); + var result = _documentRewriter.RewriteDocument(document, operationName); + document = result.Document; var operationDefinition = document.GetOperation(operationName); var includeConditions = new IncludeConditionCollection(); @@ -129,7 +130,7 @@ public Operation Compile( compilationContext.Features, lastId, compilationContext.ElementsById, - hasDeferredSelections: selectionSet.HasDeferredSelections); + hasIncrementalParts: result.HasIncrementalParts); selectionSet.Complete(operation); diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs index 1bef4f15170..dc2c8172346 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs @@ -14,7 +14,7 @@ public Task ExecuteAsync( OperationContext operationContext, IImmutableDictionary scopedContext) { - if (operationContext.Operation.HasDeferredSelections) + if (operationContext.Operation.HasIncrementalParts) { return ExecuteIncrementalAsync(operationContext, scopedContext); } @@ -77,7 +77,7 @@ public Task ExecuteBatchAsync( Debug.Assert(length > operationContexts.Length); Debug.Assert(length > results.Length); - if (operationContexts[0].OperationContext.Operation.HasDeferredSelections) + if (operationContexts[0].OperationContext.Operation.HasIncrementalParts) { return ExecuteBatchIncrementalAsync(operationContexts, results, length); } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs index 10cd0e670b6..5f1f0db2702 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs @@ -59,7 +59,7 @@ internal SelectionSet( public bool IsConditional => (_flags & Flags.Conditional) == Flags.Conditional; /// - public bool HasDeferredSelections => (_flags & Flags.HasDeferredSelections) == Flags.HasDeferredSelections; + public bool HasIncrementalParts => (_flags & Flags.HasDeferredSelections) == Flags.HasDeferredSelections; /// /// Gets the type context of this selection set. diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs index 234df7d785f..61b87eefde9 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs @@ -26,7 +26,7 @@ public static void EnqueueRootResolverTasks( try { - if (selectionSet.HasDeferredSelections) + if (selectionSet.HasIncrementalParts) { var coordinator = operationContext.DeferExecutionCoordinator; var deferFlags = operationContext.DeferFlags; @@ -147,7 +147,7 @@ public static void EnqueueOrInlineResolverTasks( resultValue.SetObjectValue(selectionSet); - if (selectionSet.HasDeferredSelections) + if (selectionSet.HasIncrementalParts) { var coordinator = operationContext.DeferExecutionCoordinator; var deferFlags = operationContext.DeferFlags; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs index 24b6ebe971a..df826dc5f93 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs @@ -43,14 +43,6 @@ public void Initialize(RequestContext requestContext, IBatchDispatcher batchDisp _isInitialized = true; } - public void Reset() - { - var requestContext = _requestContext; - var batchDispatcher = _batchDispatcher; - Clear(); - Initialize(requestContext, batchDispatcher); - } - public void Clear() { _work.Clear(); @@ -60,7 +52,7 @@ public void Clear() _signal.Reset(); _result = null!; - _batchDispatcherSession.Dispose(); + _batchDispatcherSession?.Dispose(); _batchDispatcherSession = null!; _batchDispatcher = null!; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Operation.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Operation.cs index 790f691ddb0..17f70bfee08 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Operation.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Operation.cs @@ -105,7 +105,7 @@ ISelectionSet IOperation.RootSelectionSet /// public IFeatureCollection Features => _features; - public bool HasDeferredSelections => throw new NotImplementedException(); + public bool HasIncrementalParts => throw new NotImplementedException(); /// /// Gets the selection set for the specified diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs index 8b8fc5d8bec..4b4dca60b11 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs @@ -61,7 +61,7 @@ public SelectionSet(int id, IObjectTypeDefinition type, Selection[] selections, /// public ReadOnlySpan Selections => _selections; - public bool HasDeferredSelections => throw new NotImplementedException(); + public bool HasIncrementalParts => throw new NotImplementedException(); IEnumerable ISelectionSet.GetSelections() => _selections; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/InlineFragmentOperationRewriter.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/InlineFragmentOperationRewriter.cs index 7c95b57d7d2..25c7d5d2e22 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/InlineFragmentOperationRewriter.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/InlineFragmentOperationRewriter.cs @@ -6,6 +6,21 @@ namespace HotChocolate.Fusion.Rewriters; +/// +/// Rewrites GraphQL operation documents by inlining all fragment spreads and merging inline fragments +/// into a single flattened selection set. This eliminates fragment definitions and produces an operation +/// with all selections explicitly expanded, making it suitable for execution planning and optimization. +/// +/// +/// The rewriter performs the following transformations: +/// +/// Expands all fragment spreads by substituting them with their fragment definition's selection set +/// Merges inline fragments that share the same type condition +/// Combines duplicate field selections +/// Optionally removes selections with static @skip/@include directives +/// Detects @defer and @stream directives for incremental delivery support +/// +/// public sealed class InlineFragmentOperationRewriter( ISchemaDefinition schema, bool removeStaticallyExcludedSelections = false, @@ -23,12 +38,29 @@ [new DirectiveNode("fusion__empty")], ImmutableArray.Empty, null); - public DocumentNode RewriteDocument(DocumentNode document, string? operationName = null) + /// + /// Rewrites a GraphQL document by inlining all fragments and flattening the operation's selection set. + /// + /// The GraphQL document to rewrite. + /// + /// The name of the operation to rewrite. If null, the first or only operation in the document is used. + /// + /// + /// A result containing the rewritten document with all fragments inlined and a flag indicating + /// whether the document contains @defer or @stream directives for incremental delivery. + /// + /// + /// Thrown when the document references undefined fragments or invalid type conditions. + /// + public InlineFragmentOperationRewriterResult RewriteDocument( + DocumentNode document, + string? operationName = null) { + var hasIncrementalParts = false; var operation = document.GetOperation(operationName); var operationType = schema.GetOperationType(operation.Operation); var fragmentLookup = CreateFragmentLookup(document); - var context = new Context(operationType, fragmentLookup); + var context = new Context(operationType, fragmentLookup, ref hasIncrementalParts); CollectSelections(operation.SelectionSet, context); RewriteSelections(context); @@ -46,7 +78,8 @@ public DocumentNode RewriteDocument(DocumentNode document, string? operationName RewriteDirectives(operation.Directives), newSelectionSet); - return new DocumentNode(ImmutableArray.Empty.Add(newOperation)); + var rewrittenDocument = new DocumentNode(ImmutableArray.Empty.Add(newOperation)); + return new InlineFragmentOperationRewriterResult(rewrittenDocument, hasIncrementalParts); } internal void CollectSelections(SelectionSetNode selectionSet, Context context) @@ -56,6 +89,12 @@ internal void CollectSelections(SelectionSetNode selectionSet, Context context) switch (selection) { case FieldNode field: + // Check for @stream directive (only valid on fields) + if (HasStreamDirective(field.Directives)) + { + context.MarkAsIncremental(); + } + if (!removeStaticallyExcludedSelections || IsIncluded(field.Directives)) { context.AddField(field); @@ -198,6 +237,12 @@ private void RewriteField(FieldNode fieldNode, Context context) private void CollectInlineFragment(InlineFragmentNode inlineFragment, Context context) { + // Check for @defer directive (only valid on inline fragments) + if (HasDeferDirective(inlineFragment.Directives)) + { + context.MarkAsIncremental(); + } + if ((inlineFragment.TypeCondition is null || inlineFragment.TypeCondition.Name.Value.Equals(context.Type.Name, StringComparison.Ordinal)) && inlineFragment.Directives.Count == 0) @@ -259,6 +304,12 @@ private void CollectFragmentSpread( FragmentSpreadNode fragmentSpread, Context context) { + // Check for @defer directive (only valid on fragment spreads) + if (HasDeferDirective(fragmentSpread.Directives)) + { + context.MarkAsIncremental(); + } + var fragmentDefinition = context.GetFragmentDefinition(fragmentSpread.Name.Value); var typeName = fragmentDefinition.TypeCondition.Name.Value; @@ -568,7 +619,10 @@ private static IReadOnlyList RemoveStaticIncludeConditions( return result.Count == 0 ? [] : result; - static bool IsStaticIncludeCondition(DirectiveNode directive, ref bool skipChecked, ref bool includeChecked) + static bool IsStaticIncludeCondition( + DirectiveNode directive, + ref bool skipChecked, + ref bool includeChecked) { if (directive.Name.Value.Equals(DirectiveNames.Skip.Name, StringComparison.Ordinal)) { @@ -591,15 +645,72 @@ static bool IsStaticIncludeCondition(DirectiveNode directive, ref bool skipCheck } } - public readonly ref struct Context( - ITypeDefinition type, - Dictionary fragments, - ISelectionSetMergeObserver? mergeObserver = null) + private static bool HasDeferDirective(IReadOnlyList directives) + { + if (directives.Count == 0) + { + return false; + } + + if (directives.Count == 1) + { + return directives[0].Name.Value.Equals(DirectiveNames.Defer.Name, StringComparison.Ordinal); + } + + for (var i = 0; i < directives.Count; i++) + { + if (directives[i].Name.Value.Equals(DirectiveNames.Defer.Name, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + private static bool HasStreamDirective(IReadOnlyList directives) + { + if (directives.Count == 0) + { + return false; + } + + if (directives.Count == 1) + { + return directives[0].Name.Value.Equals(DirectiveNames.Stream.Name, StringComparison.Ordinal); + } + + for (var i = 0; i < directives.Count; i++) + { + if (directives[i].Name.Value.Equals(DirectiveNames.Stream.Name, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + public readonly ref struct Context { - public ITypeDefinition Type { get; } = type; + private readonly Dictionary _fragments; + private readonly ref bool _hasIncrementalParts; + + public Context( + ITypeDefinition type, + Dictionary fragments, + ref bool hasIncrementalParts, + ISelectionSetMergeObserver? mergeObserver = null) + { + _fragments = fragments; + _hasIncrementalParts = ref hasIncrementalParts; + Type = type; + Observer = mergeObserver ?? NoopSelectionSetMergeObserver.Instance; + } + + public ITypeDefinition Type { get; } - public ISelectionSetMergeObserver Observer { get; } = - mergeObserver ?? NoopSelectionSetMergeObserver.Instance; + public ISelectionSetMergeObserver Observer { get; } public ImmutableArray.Builder Selections { get; } = ImmutableArray.CreateBuilder(); @@ -610,7 +721,7 @@ public readonly ref struct Context( public FragmentDefinitionNode GetFragmentDefinition(string name) { - if (!fragments.TryGetValue(name, out var fragment)) + if (!_fragments.TryGetValue(name, out var fragment)) { throw new RewriterException(string.Format( InlineFragmentOperationRewriter_FragmentDoesNotExist, @@ -643,8 +754,11 @@ public void AddFragmentSpread(FragmentSpreadNode fragmentSpread) Selections.Add(fragmentSpread); } + public void MarkAsIncremental() + => _hasIncrementalParts = true; + public Context Branch(ITypeDefinition type) - => new(type, fragments, Observer); + => new(type, _fragments, ref _hasIncrementalParts, Observer); } private sealed class FieldComparer : IEqualityComparer diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/InlineFragmentOperationRewriterResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/InlineFragmentOperationRewriterResult.cs new file mode 100644 index 00000000000..7986d14a469 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/InlineFragmentOperationRewriterResult.cs @@ -0,0 +1,10 @@ +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Rewriters; + +/// +/// Represents the result of flattening a GraphQL document by inlining fragment spreads and merging inline fragments. +/// +/// The flattened document with all fragments inlined into the operation. +/// Indicates whether the document contains @defer or @stream directives for incremental delivery. +public readonly record struct InlineFragmentOperationRewriterResult(DocumentNode Document, bool HasIncrementalParts); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/MergeSelectionSetRewriter.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/MergeSelectionSetRewriter.cs index c2969dac308..cae6fe91ff5 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/MergeSelectionSetRewriter.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/MergeSelectionSetRewriter.cs @@ -24,7 +24,8 @@ public SelectionSetNode Merge( CopyTo(selectionSet1.Selections, selections, 0); CopyTo(selectionSet2.Selections, selections, selectionSet1.Selections.Count); - var context = new InlineFragmentOperationRewriter.Context(type, [], mergeObserver); + var hasIncrementalParts = false; + var context = new InlineFragmentOperationRewriter.Context(type, [], ref hasIncrementalParts, mergeObserver); var merged = new SelectionSetNode(null, selections); mergeObserver.OnMerge(selectionSet1, selectionSet2); @@ -43,7 +44,8 @@ public SelectionSetNode Merge( { mergeObserver ??= NoopSelectionSetMergeObserver.Instance; - var context = new InlineFragmentOperationRewriter.Context(type, [], mergeObserver); + var hasIncrementalParts = false; + var context = new InlineFragmentOperationRewriter.Context(type, [], ref hasIncrementalParts, mergeObserver); var merged = new SelectionSetNode(null, [.. selectionSets.SelectMany(t => t.Selections)]); mergeObserver.OnMerge(selectionSets); diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Utilities.Tests/Rewriters/InlineFragmentOperationRewriterTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Utilities.Tests/Rewriters/InlineFragmentOperationRewriterTests.cs index 4a03c817556..23ed60aa5fd 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Utilities.Tests/Rewriters/InlineFragmentOperationRewriterTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Utilities.Tests/Rewriters/InlineFragmentOperationRewriterTests.cs @@ -28,10 +28,11 @@ fragment Product on Product { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + Assert.False(result.HasIncrementalParts); + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -69,10 +70,11 @@ fragment Product2 on Product { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + Assert.False(result.HasIncrementalParts); + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -104,10 +106,11 @@ public void Inline_Inline_Fragment_Into_ProductById_SelectionSet_1() // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + Assert.False(result.HasIncrementalParts); + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -147,10 +150,10 @@ fragment Product2 on Product { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -183,10 +186,10 @@ ... @include(if: true) { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -222,10 +225,10 @@ ... @include(if: false) { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -260,10 +263,11 @@ fragment Product on Product { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + Assert.False(result.HasIncrementalParts); + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -299,10 +303,10 @@ fragment Product on Product { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -334,10 +338,10 @@ name @include(if: false) // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -365,10 +369,10 @@ id @include(if: false) // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -399,10 +403,10 @@ description @skip(if: false) // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ query( $skip: Boolean! @@ -441,10 +445,10 @@ name @skip(if: $skip) // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ query( $skip: Boolean! @@ -493,10 +497,10 @@ fragment ProductFragment2 on Product { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ query( $slug: String! @@ -540,10 +544,10 @@ public void Merge_Fields_With_Aliases() // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ query( $slug: String! @@ -577,10 +581,10 @@ id @fusion__requirement // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ query( $skip: Boolean! @@ -721,10 +725,10 @@ name @include(if: $skip) var rewriter = new InlineFragmentOperationRewriter( schemaDefinition, removeStaticallyExcludedSelections: true); - var rewritten = rewriter.RewriteDocument(doc); + var result = rewriter.RewriteDocument(doc); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ query( $skip: Boolean! @@ -736,4 +740,208 @@ name @include(if: $skip) } """); } + + [Fact] + public void Detect_Defer_On_Inline_Fragment() + { + // arrange + var sourceText = FileResource.Open("schema1.graphql"); + var schemaDefinition = SchemaParser.Parse(sourceText); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + id + ... @defer { + name + } + } + } + """); + + // act + var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); + var result = rewriter.RewriteDocument(doc); + + // assert + Assert.True(result.HasIncrementalParts); + } + + [Fact] + public void Detect_Defer_On_Fragment_Spread() + { + // arrange + var sourceText = FileResource.Open("schema1.graphql"); + var schemaDefinition = SchemaParser.Parse(sourceText); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + id + ... Product @defer + } + } + + fragment Product on Product { + name + } + """); + + // act + var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); + var result = rewriter.RewriteDocument(doc); + + // assert + Assert.True(result.HasIncrementalParts); + } + + [Fact] + public void Detect_Stream_On_Field() + { + // arrange + var sourceText = FileResource.Open("schema1.graphql"); + var schemaDefinition = SchemaParser.Parse(sourceText); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + id + reviews @stream { + nodes { + body + } + } + } + } + """); + + // act + var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); + var result = rewriter.RewriteDocument(doc); + + // assert + Assert.True(result.HasIncrementalParts); + } + + [Fact] + public void No_Incremental_Parts_Without_Defer_Or_Stream() + { + // arrange + var sourceText = FileResource.Open("schema1.graphql"); + var schemaDefinition = SchemaParser.Parse(sourceText); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + id + name + reviews { + nodes { + body + } + } + } + } + """); + + // act + var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); + var result = rewriter.RewriteDocument(doc); + + // assert + Assert.False(result.HasIncrementalParts); + } + + [Fact] + public void Detect_Multiple_Defer_Directives() + { + // arrange + var sourceText = FileResource.Open("schema1.graphql"); + var schemaDefinition = SchemaParser.Parse(sourceText); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + id + ... @defer { + name + } + ... @defer { + description + } + } + } + """); + + // act + var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); + var result = rewriter.RewriteDocument(doc); + + // assert + Assert.True(result.HasIncrementalParts); + } + + [Fact] + public void Detect_Defer_And_Stream_Together() + { + // arrange + var sourceText = FileResource.Open("schema1.graphql"); + var schemaDefinition = SchemaParser.Parse(sourceText); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + id + ... @defer { + name + } + reviews @stream { + nodes { + body + } + } + } + } + """); + + // act + var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); + var result = rewriter.RewriteDocument(doc); + + // assert + Assert.True(result.HasIncrementalParts); + } + + [Fact] + public void Detect_Defer_With_Label() + { + // arrange + var sourceText = FileResource.Open("schema1.graphql"); + var schemaDefinition = SchemaParser.Parse(sourceText); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + id + ... @defer(label: "productName") { + name + } + } + } + """); + + // act + var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); + var result = rewriter.RewriteDocument(doc); + + // assert + Assert.True(result.HasIncrementalParts); + } } From e6116680430b7bc4ac67c378b7922f74f80ea5da Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Tue, 10 Feb 2026 22:47:00 +0100 Subject: [PATCH 12/46] Reworked Defer Tests --- .../AspNetCore.Tests/DeferOverHttpTests.cs | 132 ++++++++++++------ .../GraphQLOverHttpSpecTests.cs | 117 ---------------- .../Execution/Processing/QueryExecutor.cs | 14 +- 3 files changed, 100 insertions(+), 163 deletions(-) diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs index d9e121d5fb9..24720ec37e6 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs @@ -10,8 +10,14 @@ namespace HotChocolate.AspNetCore; public class DeferOverHttpTests(TestServerFactory serverFactory) : ServerTestBase(serverFactory) { - [Fact] - public async Task Simple_Defer_Multipart() + [Theory] + [InlineData(null)] + [InlineData("*/*")] + [InlineData("multipart/mixed")] + [InlineData("multipart/*")] + [InlineData("application/graphql-response+json, multipart/mixed")] + [InlineData("text/event-stream, multipart/mixed")] + public async Task Simple_Defer_Multipart(string? acceptHeader) { // arrange using var server = CreateDeferServer(); @@ -32,7 +38,11 @@ ... @defer { } """ }); - request.Headers.Add("Accept", "multipart/mixed"); + + if (acceptHeader is not null) + { + request.Headers.Add("Accept", acceptHeader); + } using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); @@ -61,8 +71,10 @@ ... @defer { """); } - [Fact] - public async Task Simple_Defer_EventStream() + [Theory] + [InlineData("text/event-stream")] + [InlineData("application/graphql-response+json, text/event-stream")] + public async Task Simple_Defer_EventStream(string acceptHeader) { // arrange using var server = CreateDeferServer(); @@ -83,7 +95,7 @@ ... @defer { } """ }); - request.Headers.Add("Accept", "text/event-stream"); + request.Headers.Add("Accept", acceptHeader); using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); @@ -98,14 +110,26 @@ ... @defer { .Add(content, "Response") .MatchInline( """ - Response: - --------------------------> - TODO: Add expected snapshot + event: next + data: {"data":{"product":{"name":"Abc","description":null}},"pending":[{"id":2,"path":["product"]}],"hasNext":true} + + event: next + data: {"incremental":[{"id":2,"data":{"description":"Abc desc"}}],"completed":[{"id":2}],"hasNext":false} + + event: complete + + """); } - [Fact] - public async Task Defer_List_Multipart() + [Theory] + [InlineData(null)] + [InlineData("*/*")] + [InlineData("multipart/mixed")] + [InlineData("multipart/*")] + [InlineData("application/graphql-response+json, multipart/mixed")] + [InlineData("text/event-stream, multipart/mixed")] + public async Task Defer_With_Label_Multipart(string? acceptHeader) { // arrange using var server = CreateDeferServer(); @@ -117,21 +141,26 @@ public async Task Defer_List_Multipart() { query = """ { - products { + product { name - ... @defer(label: "desc") { + ... @defer(label: "productDescription") { description } } } """ }); - request.Headers.Add("Accept", "multipart/mixed"); + + if (acceptHeader is not null) + { + request.Headers.Add("Accept", acceptHeader); + } using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); // assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("multipart/mixed", response.Content.Headers.ContentType?.MediaType); var content = await response.Content.ReadAsStringAsync(); @@ -140,14 +169,26 @@ ... @defer(label: "desc") { .Add(content, "Response") .MatchInline( """ - Response: - --------------------------> - TODO: Add expected snapshot + + --- + Content-Type: application/json; charset=utf-8 + + {"data":{"product":{"name":"Abc","description":null}},"pending":[{"id":2,"path":["product"],"label":"productDescription"}],"hasNext":true} + --- + Content-Type: application/json; charset=utf-8 + + {"incremental":[{"id":2,"data":{"description":"Abc desc"}}],"completed":[{"id":2}],"hasNext":false} + ----- + """); } - [Fact] - public async Task Defer_With_Label_Multipart() + [Theory] + [InlineData(null)] + [InlineData("*/*")] + [InlineData("application/graphql-response+json, multipart/mixed")] + [InlineData("application/graphql-response+json, text/event-stream, multipart/mixed")] + public async Task Defer_Disabled_By_Variable(string? acceptHeader) { // arrange using var server = CreateDeferServer(); @@ -157,22 +198,33 @@ public async Task Defer_With_Label_Multipart() using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); request.Content = JsonContent.Create(new { - query = """ - { + query = + """ + query($shouldDefer: Boolean!) { product { name - ... @defer(label: "productDescription") { + ... @defer(if: $shouldDefer) { description } } } - """ + """, + variables = new { shouldDefer = false } }); - request.Headers.Add("Accept", "multipart/mixed"); + + if (acceptHeader is not null) + { + request.Headers.Add("Accept", acceptHeader); + } using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // When defer is disabled, should get a regular JSON response, not multipart + Assert.Equal("application/graphql-response+json", response.Content.Headers.ContentType?.MediaType); + var content = await response.Content.ReadAsStringAsync(); Snapshot @@ -180,14 +232,12 @@ ... @defer(label: "productDescription") { .Add(content, "Response") .MatchInline( """ - Response: - --------------------------> - TODO: Add expected snapshot + {"data":{"product":{"name":"Abc","description":"Abc desc"}}} """); } [Fact] - public async Task Defer_Disabled_By_Variable() + public async Task Defer_NoStreamableAcceptHeader() { // arrange using var server = CreateDeferServer(); @@ -198,25 +248,26 @@ public async Task Defer_Disabled_By_Variable() request.Content = JsonContent.Create(new { query = """ - query($shouldDefer: Boolean!) { + { product { name - ... @defer(if: $shouldDefer) { + ... @defer { description } } } - """, - variables = new { shouldDefer = false } + """ }); - request.Headers.Add("Accept", "multipart/mixed"); + request.Headers.Add("Accept", "application/graphql-response+json"); using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // Should reject the request since we have a deferred result but + // the user only accepts non-streaming JSON payload + Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); + Assert.Equal("application/graphql-response+json", response.Content.Headers.ContentType?.MediaType); - // When defer is disabled, should get a regular JSON response, not multipart var content = await response.Content.ReadAsStringAsync(); Snapshot @@ -224,9 +275,7 @@ ... @defer(if: $shouldDefer) { .Add(content, "Response") .MatchInline( """ - Response: - --------------------------> - TODO: Add expected snapshot + {"errors":[{"message":"The specified operation kind is not allowed."}]} """); } @@ -257,13 +306,6 @@ public sealed class Query { public Product GetProduct() => new("Abc"); - - public IEnumerable GetProducts() - { - yield return new Product("Abc"); - yield return new Product("Def"); - yield return new Product("Ghi"); - } } public sealed record Product(string Name) diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs index f443c0eb85d..b6889e150e3 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs @@ -292,123 +292,6 @@ public async Task UnsupportedApplicationAcceptHeaderValue() """); } - [Theory] - [InlineData(null)] - [InlineData("*/*")] - [InlineData("multipart/mixed")] - [InlineData("multipart/*")] - [InlineData("application/graphql-response+json, multipart/mixed")] - [InlineData("text/event-stream, multipart/mixed")] - public async Task DeferredQuery_Multipart(string? acceptHeader) - { - // arrange - var server = CreateStarWarsServer(); - var client = server.CreateClient(); - - // act - using var request = new HttpRequestMessage(HttpMethod.Post, s_url); - request.Content = JsonContent.Create(new ClientQueryRequest { Query = "{ ... @defer { __typename } }" }); - AddAcceptHeader(request, acceptHeader); - - using var response = await client.SendAsync(request); - - // assert - Snapshot - .Create() - .Add(response) - .MatchInline( - """ - Headers: - Cache-Control: no-cache - Content-Type: multipart/mixed; boundary="-" - --------------------------> - Status Code: OK - --------------------------> - - --- - Content-Type: application/json; charset=utf-8 - - {"data":{"__typename":null},"pending":[{"id":2,"path":[]}],"incremental":[{"id":2,"data":{"__typename":"Query"}}],"completed":[{"id":2}],"hasNext":false} - ----- - - """); - } - - [Theory] - [InlineData("text/event-stream")] - [InlineData("application/graphql-response+json, text/event-stream")] - public async Task DeferredQuery_EventStream(string acceptHeader) - { - // arrange - var server = CreateStarWarsServer(); - var client = server.CreateClient(); - - // act - using var request = new HttpRequestMessage(HttpMethod.Post, s_url); - request.Content = JsonContent.Create( - new ClientQueryRequest - { - Query = "{ ... @defer { __typename } }" - }); - request.Headers.Add("Accept", acceptHeader); - - using var response = await client.SendAsync(request, ResponseHeadersRead); - - // assert - Snapshot - .Create() - .Add(response) - .MatchInline( - """ - Headers: - Cache-Control: no-cache - Content-Type: text/event-stream; charset=utf-8 - --------------------------> - Status Code: OK - --------------------------> - event: next - data: {"data":{"__typename":null},"pending":[{"id":2,"path":[]}],"hasNext":true} - - event: next - data: {"incremental":[{"id":2,"data":{"__typename":"Query"}}],"completed":[{"id":2}],"hasNext":false} - - event: complete - - - """); - } - - [Fact] - public async Task DeferredQuery_NoStreamableAcceptHeader() - { - // arrange - var server = CreateStarWarsServer(); - var client = server.CreateClient(); - - // act - using var request = new HttpRequestMessage(HttpMethod.Post, s_url); - request.Content = JsonContent.Create(new ClientQueryRequest { Query = "{ ... @defer { __typename } }" }); - request.Headers.Add("Accept", ContentType.GraphQLResponse); - - using var response = await client.SendAsync(request, ResponseHeadersRead); - - // assert - // we are rejecting the request since we have a streamed result and - // the user requests a JSON payload. - Snapshot - .Create() - .Add(response) - .MatchInline( - """ - Headers: - Content-Type: application/graphql-response+json; charset=utf-8 - --------------------------> - Status Code: MethodNotAllowed - --------------------------> - {"errors":[{"message":"The specified operation kind is not allowed."}]} - """); - } - [Fact] public async Task EventStream_Sends_KeepAlive() { diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs index dc2c8172346..dc0a8d0f5da 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs @@ -38,7 +38,19 @@ private static async Task ExecuteIncrementalAsync( var execution = scheduler.ExecuteAsync1(); await scheduler.WaitForCompletionAsync(branchId).ConfigureAwait(false); - coordinator.EnqueueResult(operationContext.BuildResult()); + var initialResult = operationContext.BuildResult(); + + if (!coordinator.HasBranches) + { + if (!execution.IsCompletedSuccessfully) + { + await execution.ConfigureAwait(false); + } + + return initialResult; + } + + coordinator.EnqueueResult(initialResult); return new ResponseStream(CreateStream, ExecutionResultKind.DeferredResult); async IAsyncEnumerable CreateStream() From b9495adc9e840172975637a8533e4a0b27feb26e Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 11 Feb 2026 15:25:13 +0100 Subject: [PATCH 13/46] More bug fixes --- Defer.md | 384 ++++++++++++++++++ .../Parsers/DefaultHttpRequestParser.cs | 4 +- .../Utilities/ErrorHelper.cs | 8 + .../Utilities/ThrowHelper.cs | 20 +- .../AspNetCore.Tests/DeferOverHttpTests.cs | 6 +- .../HttpMultipartMiddlewareTests.cs | 8 +- ...lewareTests.IncompleteOperations_Test.snap | 7 +- ...reTests.MissingObjectPathsForKey_Test.snap | 4 +- .../src/Types/Text/Json/ResultDocument.cs | 10 +- .../OperationIdFormatException.cs | 18 - .../Language/src/Language.Web/ThrowHelper.cs | 3 + .../Language.Web/Utf8GraphQLRequestParser.cs | 4 +- 12 files changed, 431 insertions(+), 45 deletions(-) create mode 100644 Defer.md delete mode 100644 src/HotChocolate/Language/src/Language.Web/OperationIdFormatException.cs diff --git a/Defer.md b/Defer.md new file mode 100644 index 00000000000..1621a4e6330 --- /dev/null +++ b/Defer.md @@ -0,0 +1,384 @@ +# Defer and Stream PR 1110 + +## Reference Locations +- **GraphQL Spec**: `/Users/michael/local/graphql-spec/public/draft/index.html` +- **Reference Implementation**: `/Users/michael/local/graphql-js` +- **Key Reference File**: `/Users/michael/local/graphql-js/src/execution/incremental/buildExecutionPlan.ts` + +## Overview +We're integrating GraphQL `@defer` directive support into Hot Chocolate's execution engine. The defer directive allows incremental delivery of GraphQL responses - the initial response returns immediately with non-deferred fields, and deferred fragments arrive in subsequent payloads. + +## Key Concepts + +### DeferUsage +- Represents a `@defer` directive occurrence in a query +- Forms a parent chain for nested defer scopes +- Has properties: `Label`, `Parent`, `DeferConditionIndex` +- The `DeferConditionIndex` maps to a bit position in runtime defer flags (ulong bitmask) + +### Defer Flags +- Runtime bitmask (ulong) indicating which defer conditions are active +- Each bit corresponds to a defer condition (supports up to 64 defer directives) +- Variables like `@defer(if: $var)` are evaluated at runtime, not compile-time + +### Branch IDs +- Execution branches represent different execution contexts +- Branch IDs are **scheduler-issued** via `WorkScheduler.NextBranchId()` (atomic counter) +- Each operation's main branch gets a unique ID at initialization (no more constant `MainBranchId`) +- Each deferred fragment gets its own unique branch ID +- `SystemBranchId = -1` is reserved for orchestrator tasks (DeferTask) — **not tracked** for completion +- This ensures uniqueness across variable-batched operations sharing a scheduler +- Branch task counts are tracked in `WorkScheduler._branchTaskCount` (Dictionary) +- `DeferExecutionCoordinator` tracks parent-child branch relationships and defer usage mappings + +### Primary Defer Usage +- When a selection has multiple defer usages (nested defers), we need to find the "primary" one +- The primary defer usage is the **outermost** active defer that isn't covered by a parent +- This determines which execution branch the selection belongs to + +## Completed Work + +### 1. WorkQueue Priority System +**File**: `src/HotChocolate/Core/src/Types/Execution/Processing/WorkQueue.cs` + +Changed from single `Stack` to two stacks for priority-based execution: +```csharp +private readonly Stack _immediateStack = new(); +private readonly Stack _deferredStack = new(); +``` + +**Why two stacks instead of PriorityQueue?** +- Binary priority levels (immediate vs deferred) don't need heap overhead +- Stack is O(1) for push/pop vs O(log n) for PriorityQueue +- Better cache locality with Stack +- Zero-allocation when pooled + +**Logic**: +- `Push()` routes tasks to immediate or deferred stack based on `IsDeferred` +- `TryTake()` always tries immediate stack first, then deferred stack +- Ensures initial response completes as fast as possible + +### 2. IExecutionTask.IsDeferred Property +**File**: `src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs` + +Added property: +```csharp +bool IsDeferred { get; } +``` + +**Note**: Named "IsDeferred" for general execution engine concept of deprioritized tasks, not GraphQL-specific + +### 3. ResolverTask Branch Tracking +**Files**: +- `src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs` + +Added properties: +```csharp +internal int BranchId { get; private set; } +internal DeferUsage? DeferUsage { get; private set; } + +public bool IsDeferred => DeferUsage is not null; +``` + +**Why both BranchId and DeferUsage?** +- `BranchId` is used by coordinator to track which branch this task belongs to +- `DeferUsage` is used when creating child tasks to determine if they need a new branch +- When child's primary defer usage differs from parent's, a new branch is created + +### 4. Selection.GetPrimaryDeferUsage() Method + +**File**: `src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs` + +Finds the primary (outermost) active defer usage for a selection at runtime. Handles conditional defers by walking up the parent chain when a defer is inactive. + +**Algorithm**: + +1. For each entry in `_deferUsage`, walk up the parent chain to find the **nearest active** defer (bit set in `deferFlags`). +2. If any entry resolves to no active defer at all (walked to root) → return `null`. The field has a non-deferred occurrence and belongs in the initial response. +3. Among all resolved effective defers, keep the **outermost** (the one that is an ancestor of others). + +**Fast path**: Single defer usage (most common) — just walks up the parent chain and returns the first active one. + +**Example scenarios** with `_deferUsage = [B]` where B.parent = A: + +| A (conditional) | B | Result | Why | +|---|---|---|---| +| active | active | B | B is nearest active | +| inactive | active | B | A disabled, B still defers | +| active | inactive | A | B disabled, folds into A's scope | +| inactive | inactive | null | No active defer in chain | + +## Key Decisions & Rationale + +### 1. Walk parent chain on inactive defer (not immediate null) + +Previously returned null if ANY defer usage was inactive. Now walks up the parent chain to find the nearest active ancestor. This handles conditional outer defers: when `@defer(if: $var)` is disabled, the content folds into its parent scope, but a parent `@defer` may still be active. + +**Return null** only when walking the full chain finds no active defer — meaning the field truly has a non-deferred occurrence. + +### 2. Use reference equality for DeferUsage identity + +DeferUsage is a sealed record and instances are interned during compilation. Reference equality (`==`) correctly identifies the same defer directive across parent chains and array entries. + +### 3. Outermost wins among multiple effective defers + +When `_deferUsage` has multiple entries that resolve to different active defers, the outermost (ancestor) is kept as primary. If two are unrelated (different branches), the first is kept (single-return API limitation — reference impl returns a set). + +## Reference Implementation Notes + +From `graphql-js/src/execution/incremental/buildExecutionPlan.ts`: + +### `getFilteredDeferUsageSet()` (lines 51-75) + +1. Collects all defer usages from field details +2. **If ANY field has `undefined` deferUsage**, clears the set and returns empty +3. For remaining defer usages, removes children whose parents are also in the set (walks full ancestor chain) +4. What remains are the outermost (primary) defer usages + +This matches our approach: +- ✅ Check all defer usages are active (our lines 318-322) +- ✅ Check if parent is in array using reference equality (our lines 330-339) +- ✅ Return first uncovered defer usage (our line 344) + +### `buildExecutionPlan()` (lines 17-49) — Branching Logic + +Takes a `groupedFieldSet` and `parentDeferUsages` (the defer context of the current branch). +For each field: + +1. Computes `filteredDeferUsageSet` via `getFilteredDeferUsageSet()` +2. If `filteredDeferUsageSet === parentDeferUsages` → field stays in current branch (no new branch) +3. If different → field goes into a **new branch** keyed by its defer usage set + +**Branch creation rule**: A new branch is created at the **boundary** where a field's primary defer usage differs from the parent task's defer usage. Fields inside the same defer scope inherit the parent's branch. + +Example: + +```graphql +{ + ... @defer { # A + bar { # primary = A, parent branch has no defer → new branch A + age # primary = A, parent branch = A → stays in branch A + ... @defer { # B (parent = A) + baz # primary = B, parent branch = A → new branch B + } + } + } +} +``` + +## Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ GraphQL Query with @defer directives │ +└─────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Operation Compiler │ +│ - Builds Selection objects │ +│ - Assigns DeferUsage[] to each selection │ +│ - Assigns defer flags (ulong bitmask) │ +└─────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Execution Engine creates ResolverTasks │ +│ - Calls GetPrimaryDeferUsage(deferFlags) │ +│ - Determines BranchId based on primary defer usage │ +│ - Sets task.BranchId and task.DeferUsage │ +└─────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ WorkQueue.Push(task) │ +│ - Checks task.IsDeferred │ +│ - Routes to _immediateStack or _deferredStack │ +└─────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ WorkQueue.TryTake() │ +│ - Tries _immediateStack first │ +│ - Then _deferredStack │ +│ - Ensures initial response completes first │ +└─────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ DeferExecutionCoordinator │ +│ - Tracks branches and their relationships │ +│ - Composes incremental results │ +│ - Delivers responses in correct order │ +└─────────────────────────────────────────────────────┘ +``` + +## Operation Compiler — DeferUsage Construction + +**File**: `src/HotChocolate/Core/src/Types/Execution/Processing/OperationCompiler.cs` + +### CollectFields (line 212) + +Recursively walks the selection set. When an inline fragment has `@defer`: + +1. Creates a `DeferCondition` and registers it in `DeferConditionCollection` +2. Creates a new `DeferUsage(label, parentDeferUsage, deferIndex)` — parent chain is correct +3. Recurses into the fragment's selections with the new DeferUsage as `parentDeferUsage` +4. Fields get the current scope's DeferUsage via `FieldSelectionNode(fieldNode, pathIncludeFlags, parentDeferUsage)` + +### BuildSelectionSet (line 277) — Compile-Time Filtering + +For each field (by response name), collects all `FieldSelectionNode` entries: + +1. **Non-deferred check**: If ANY node has `DeferUsage == null`, the field has `hasNonDeferredNode = true` and is not deferred (lines 301, 342-344). Matches the reference impl's "clear set if any field detail has undefined deferUsage". +2. **Ancestor filtering** (lines 373-386): Walks the **full ancestor chain** (not just direct parent). If any ancestor is also in the list, removes the child. Leaves only outermost defer usages. Matches the reference impl exactly. +3. Produces `finalDeferUsage` array and `deferMask` bitmask, stored on the `Selection`. + +### Compile-Time vs Runtime Filtering + +The compile-time ancestor filtering in `BuildSelectionSet` (lines 373-386) is only safe for **constant** defers (unconditional `@defer` or `@defer(if: true)`). When a variable is involved (`@defer(if: $var)`), the child must be preserved in the `_deferUsage` array so `GetPrimaryDeferUsage` can evaluate it at runtime. + +**Safe to filter at compile time** (both constant): + +```graphql +... @defer { # A (constant) + ... @defer { # B (constant, parent = A) + field # → [A] at compile time, correct + } +} +``` + +**Must defer to runtime** (variable involved): + +```graphql +... @defer(if: $a) { # A (conditional) + ... @defer { # B (unconditional, parent = A) + field # → must keep [A, B], filter at runtime + } +} +``` + +If A is disabled at runtime ($a = false), A's content executes immediately but B should still defer `field`. If B is discarded at compile time, `GetPrimaryDeferUsage` sees only [A], A's bit is off, returns null — incorrectly putting the field in the initial response. + +**TODO**: Update `BuildSelectionSet` ancestor filtering to only remove a child when both the child and its covering ancestor are constant (no variable condition). Variable-dependent usages must stay in the array for runtime evaluation by `GetPrimaryDeferUsage`. + +## Task List + +### Done + +- [x] `IExecutionTask.IsDeferred` property +- [x] `WorkQueue` dual-stack priority system (immediate/deferred) +- [x] `DeferUsage`, `DeferCondition`, `DeferConditionCollection` metadata types +- [x] `DeferUsageEnumerator` zero-allocation enumerator +- [x] `DeferExecutionCoordinator` branch tracking, result composition, streaming +- [x] `ResolverTask` BranchId + DeferUsage properties +- [x] `OperationContext.DeferFlags` (ulong bitmask) +- [x] `SelectionSet.HasDeferredSelections` flag +- [x] `Selection.GetPrimaryDeferUsage()` — runtime algorithm with parent chain walk +- [x] Operation compiler: `CollectFields` builds DeferUsage parent chain +- [x] Operation compiler: `BuildSelectionSet` compile-time ancestor filtering +- [x] `ResultDocument` per-defer-group constructor — scoped to selections matching a specific `DeferUsage` + +### Execution Engine Integration + +- [x] `ResolverTaskFactory.EnqueueRootResolverTasks()` — defer branch grouping, DeferTask creation, ArrayPool pattern +- [x] `ResolverTaskFactory.EnqueueOrInlineResolverTasks()` — defer-aware branching with `parentBranchId` from `ValueCompletionContext` +- [x] `IExecutionTask.BranchId` — added to interface, abstract on `ExecutionTask` base class +- [x] `ValueCompletionContext.ParentBranchId` — threads parent ResolverTask's BranchId through value completion +- [x] `OperationContext.DeferExecutionCoordinator` — per-operation (not shared via `_current*` pattern), returns `_deferExecutionCoordinator` directly +- [ ] Pool `DeferTask` — `OperationContext.CreateDeferTask()` currently does `new DeferTask()`, needs a pooled factory like `ResolverTask` has + +### Scheduler — Branch Tracking + +- [x] **Branch ID generation**: `BranchTracker` with `Interlocked.Increment` atomic counter + - Each operation gets a unique main branch ID at initialization via `_currentBranchTracker.CreateNewBranchId()` + - Defer branches get unique IDs via `DeferExecutionCoordinator.Branch()` → `_branchTracker.CreateNewBranchId()` + - `BranchTracker.SystemBranchId = -1` is the only constant — for DeferTask orchestrators, not tracked for completion + - `_current*` pattern for tracker/scheduler (not coordinator) ensures uniqueness across variable-batched operations +- [x] **Branch task counting**: `Dictionary` in `WorkScheduler` with nested `Branch` class + - `Register()`: inside `lock(_sync)`, creates `Branch` on first encounter, calls `branch.RegisterTask()` + - `Complete()`: inside `lock(_sync)`, calls `branch.CompleteTask()`, removes and signals via `branch.Complete()` when count hits 0 + - `Clear()`: clears `_activeBranches` dictionary + - `Branch` uses `AsyncManualResetEvent` for single-awaiter async signaling (~56 bytes, resettable) + - Cancellation via `CancellationToken.Register` with proper `await using` disposal +- [x] **`WaitForCompletionAsync(branchId)`**: returns `ValueTask.CompletedTask` if branch already completed or never registered; otherwise delegates to `branch.WaitForCompletionAsync(operationContext.RequestAborted)` +- [x] **Initial payload signal**: `QueryExecutor.ExecuteIncrementalAsync` awaits `scheduler.WaitForCompletionAsync(branchId)` on main branch, then enqueues result via coordinator +- [x] Remove constant `DeferExecutionCoordinator.MainBranchId` — replaced with instance `_mainBranchId` set via `Initialize()` +- [x] Update `DeferExecutionCoordinator.Branch()` to use `_branchTracker.CreateNewBranchId()` +- [x] Update `OperationContext` to store assigned main branch ID (`_branchId` / `ExecutionBranchId`) + +### Compiler + +- [ ] Update `BuildSelectionSet` ancestor filtering to only remove constant defers; preserve variable-dependent usages for runtime evaluation + +### Middleware — `OperationExecutionMiddleware` Simplification + +**Done**: Removed `ITransactionScopeHandler` and all related types: + +- [x] Deleted `ITransactionScopeHandler`, `ITransactionScope`, `DefaultTransactionScopeHandler`, `DefaultTransactionScope`, `NoOpTransactionScopeHandler`, `NoOpTransactionScope` +- [x] Deleted `RequestExecutorBuilderExtensions.TransactionScope.cs` (public DI extensions) +- [x] Removed from middleware: field, constructor param, factory resolution, `using var transactionScope` / `.Complete()` in mutation path +- [x] Removed `TryAddNoOpTransactionScopeHandler()` from `RequestExecutorServiceCollectionExtensions.CreateBuilder()` +- [x] Deleted `TransactionScopeHandlerTests.cs` and 2 snapshot files +- [x] 5-arg `ExecuteQueryOrMutationAsync` now returns `IExecutionResult` (supports `ResponseStream` for defer) + +**Done** — method collapse and defer wiring: + +- [x] Collapsed 4-arg + 5-arg `ExecuteQueryOrMutationAsync` into single method (one fewer async state machine) +- [x] Replaced commented-out defer block with `result.IsStreamResult()` → `result.RegisterForCleanup(operationContextOwner)` ownership transfer +- [x] Uncommented `IsOperationAllowed` — enforces `AllowStreams` flag for operations with `HasDeferredSelections` +- [x] Removed `ExecuteQueryOrMutationNoStreamAsync` — mutation batch now uses `ExecuteQueryOrMutationAsync` +- [x] Removed `ExecuteOperationRequestAsync` — inlined subscription/query/mutation dispatch into `InvokeAsync` +- [x] Unified variable batch path — removed query-only gate, single `ExecuteVariableBatchRequestAsync` handles both queries and mutations with shared scheduler for DataLoader batching +- [x] Relaxed `IsRequestTypeAllowed` — variable batch + defer is allowed (spec only forbids `@defer` on root mutation fields, enforced by validation) + +### Result Delivery + +- [x] Wire `DeferExecutionCoordinator` into the response stream +- [x] On branch completion: create `OperationResult` from defer group's `ResultDocument`, call `coordinator.EnqueueResult(result, branchId)` +- [ ] Ensure `path` passed to `coordinator.Branch()` is the **full path from query root** (not relative to defer group document) +- [x] Fix `ResultDocument.CreatePath` for defer groups — added `_rootPath` field, defaults to `Path.Root`, defer constructor accepts `path` parameter +- [x] Compose incremental payloads in correct delivery order — coordinator composes pending/incremental/completed with error-bubbling distinction +- [x] Handle `hasNext` flag on initial and subsequent payloads — set in `ComposeAndDeliverUnsafe` + +### Serialization + +- [x] `IncrementalObjectResult` — write actual data via `Formatter.WriteDataTo` or `null` on error +- [x] `IncrementalObjectResult` — write `subPath` when present +- [x] `CompletedResult` — write `errors` for failed incremental deliveries +- [ ] Investigate `JsonNullIgnoreCondition` impact on incremental result serialization — `JsonResultFormatter` carries this setting but unclear how it interacts with deferred data + +### Validation Rules (Spec 5.7.4 & 5.7.5) + +- [ ] `@defer` not allowed on root fields of mutation type +- [ ] `@defer` not allowed on root fields of subscription type +- [ ] `@defer` and `@stream` in subscription operations must have an `if` argument that can disable them + +### Fusion + +- [ ] Implement `HasDeferredSelections` in `Fusion.Execution` — `Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Operation.cs:108` currently `throw new NotImplementedException()` +- [ ] Implement `Selection.IsDeferred(ulong deferFlags)` in `Fusion.Execution` — `Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs:161` currently `throw new NotImplementedException()` +- [ ] Implement `SelectionSet.HasDeferredSelections` in `Fusion.Execution` — `Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs:64` currently `throw new NotImplementedException()` + +### Subscriptions + +- [ ] Handle `@defer` inside subscription payloads — `SubscriptionExecutor.Subscription.cs:192` currently calls `result.ExpectOperationResult()` which won't work if the result is a `ResponseStream` + +### Testing + +- [ ] Simple `@defer` — single deferred fragment +- [ ] Nested `@defer` — defer inside defer +- [ ] Conditional `@defer(if: $var)` — variable true/false +- [ ] Nested conditional/unconditional — `@defer(if: $var) { @defer { field } }` +- [ ] Field in multiple fragments — unrelated defers on same field +- [ ] `@defer` with label — verify label propagation + +## Important Files + +- `src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs` - GetPrimaryDeferUsage +- `src/HotChocolate/Core/src/Types/Execution/Processing/WorkQueue.cs` - Priority queue +- `src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs` - Branch tracking +- `src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs` - IsDeferred property +- `src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs` - Branch coordinator +- `src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsage.cs` - Defer metadata +- `src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs` - Result document (per-operation and per-defer-group) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs index 76cf382aff4..536adc72b99 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs @@ -352,9 +352,9 @@ public GraphQLRequest[] ParseRequest(string sourceText) s_utf8.GetBytes(sourceText, span); return Parse(span, _parserOptions, _documentCache, _documentHashProvider); } - catch (OperationIdFormatException) + catch (InvalidGraphQLRequestException ex) { - throw ErrorHelper.InvalidOperationIdFormat(); + throw ErrorHelper.InvalidRequest(ex); } finally { diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ErrorHelper.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ErrorHelper.cs index 4497118695b..59474381bff 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ErrorHelper.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ErrorHelper.cs @@ -1,4 +1,5 @@ using HotChocolate.Collections.Immutable; +using HotChocolate.Language; using static HotChocolate.AspNetCore.Properties.AspNetCorePipelineResources; namespace HotChocolate.AspNetCore.Utilities; @@ -14,6 +15,13 @@ public static IError InvalidRequest() .SetCode(ErrorCodes.Server.RequestInvalid) .Build(); + public static GraphQLRequestException InvalidRequest( + InvalidGraphQLRequestException ex) => + new(ErrorBuilder.New() + .SetMessage(ex.Message) + .SetCode(ErrorCodes.Server.RequestInvalid) + .Build()); + public static IError RequestHasNoElements() => ErrorBuilder.New() .SetMessage(ErrorHelper_RequestHasNoElements) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ThrowHelper.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ThrowHelper.cs index 8ef78aad8eb..c637bf58af7 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ThrowHelper.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ThrowHelper.cs @@ -42,7 +42,7 @@ public static GraphQLRequestException DefaultHttpRequestParser_MaxRequestSizeExc .SetCode(ErrorCodes.Server.MaxRequestSize) .Build()); - public static GraphQLException HttpMultipartMiddleware_Invalid_Form( + public static GraphQLRequestException HttpMultipartMiddleware_Invalid_Form( Exception ex) => new GraphQLRequestException( ErrorBuilder.New() @@ -52,63 +52,63 @@ public static GraphQLException HttpMultipartMiddleware_Invalid_Form( .SetExtension("underlyingError", ex.Message) .Build()); - public static GraphQLException HttpMultipartMiddleware_No_Operations_Specified() => + public static GraphQLRequestException HttpMultipartMiddleware_No_Operations_Specified() => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_No_Operations_Specified) .SetCode(ErrorCodes.Server.MultiPartNoOperationsSpecified) .Build()); - public static GraphQLException HttpMultipartMiddleware_Fields_Misordered() => + public static GraphQLRequestException HttpMultipartMiddleware_Fields_Misordered() => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_Fields_Misordered) .SetCode(ErrorCodes.Server.MultiPartFieldsMisordered) .Build()); - public static GraphQLException HttpMultipartMiddleware_NoObjectPath(string filename) => + public static GraphQLRequestException HttpMultipartMiddleware_NoObjectPath(string filename) => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_NoObjectPath, filename) .SetCode(ErrorCodes.Server.MultiPartNoObjectPath) .Build()); - public static GraphQLException HttpMultipartMiddleware_FileMissing(string filename) => + public static GraphQLRequestException HttpMultipartMiddleware_FileMissing(string filename) => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_FileMissing, filename) .SetCode(ErrorCodes.Server.MultiPartFileMissing) .Build()); - public static GraphQLException HttpMultipartMiddleware_VariableStructureInvalid() => + public static GraphQLRequestException HttpMultipartMiddleware_VariableStructureInvalid() => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_VariableStructureInvalid) .SetCode(ErrorCodes.Server.MultiPartVariableStructureInvalid) .Build()); - public static GraphQLException HttpMultipartMiddleware_InvalidPath(string path) => + public static GraphQLRequestException HttpMultipartMiddleware_InvalidPath(string path) => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_InvalidPath, path) .SetCode(ErrorCodes.Server.MultiPartInvalidPath) .Build()); - public static GraphQLException HttpMultipartMiddleware_PathMustStartWithVariable() => + public static GraphQLRequestException HttpMultipartMiddleware_PathMustStartWithVariable() => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_PathMustStartWithVariable) .SetCode(ErrorCodes.Server.MultiPartPathMustStartWithVariable) .Build()); - public static GraphQLException HttpMultipartMiddleware_InvalidMapJson() => + public static GraphQLRequestException HttpMultipartMiddleware_InvalidMapJson() => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_InvalidMapJson) .SetCode(ErrorCodes.Server.MultiPartInvalidMapJson) .Build()); - public static GraphQLException HttpMultipartMiddleware_MapNotSpecified() => + public static GraphQLRequestException HttpMultipartMiddleware_MapNotSpecified() => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_MapNotSpecified) diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs index 24720ec37e6..1a3bb5130fa 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs @@ -61,7 +61,7 @@ ... @defer { --- Content-Type: application/json; charset=utf-8 - {"data":{"product":{"name":"Abc","description":null}},"pending":[{"id":2,"path":["product"]}],"hasNext":true} + {"data":{"product":{"name":"Abc"}},"pending":[{"id":2,"path":["product"]}],"hasNext":true} --- Content-Type: application/json; charset=utf-8 @@ -111,7 +111,7 @@ ... @defer { .MatchInline( """ event: next - data: {"data":{"product":{"name":"Abc","description":null}},"pending":[{"id":2,"path":["product"]}],"hasNext":true} + data: {"data":{"product":{"name":"Abc"}},"pending":[{"id":2,"path":["product"]}],"hasNext":true} event: next data: {"incremental":[{"id":2,"data":{"description":"Abc desc"}}],"completed":[{"id":2}],"hasNext":false} @@ -173,7 +173,7 @@ ... @defer(label: "productDescription") { --- Content-Type: application/json; charset=utf-8 - {"data":{"product":{"name":"Abc","description":null}},"pending":[{"id":2,"path":["product"],"label":"productDescription"}],"hasNext":true} + {"data":{"product":{"name":"Abc"}},"pending":[{"id":2,"path":["product"],"label":"productDescription"}],"hasNext":true} --- Content-Type: application/json; charset=utf-8 diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpMultipartMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpMultipartMiddlewareTests.cs index 3dec27e1b76..ad9963e5d9a 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpMultipartMiddlewareTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpMultipartMiddlewareTests.cs @@ -67,10 +67,10 @@ public async Task IncompleteOperations_Test() // act var form = new MultipartFormDataContent - { - { new StringContent("{}"), "operations" }, - { new StringContent("{}"), "map" } - }; + { + { new StringContent("{}"), "operations" }, + { new StringContent("{}"), "map" } + }; form.Headers.Add(HttpHeaderKeys.Preflight, "1"); diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.IncompleteOperations_Test.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.IncompleteOperations_Test.snap index b8f3c9524c4..27e77aef431 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.IncompleteOperations_Test.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.IncompleteOperations_Test.snap @@ -1,10 +1,13 @@ { "ContentType": "application/graphql-response+json; charset=utf-8", - "StatusCode": "InternalServerError", + "StatusCode": "BadRequest", "Data": null, "Errors": [ { - "message": "Unexpected Execution Error" + "message": "Request must contain either a query or a document id.", + "extensions": { + "code": "HC0009" + } } ], "Extensions": null diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.MissingObjectPathsForKey_Test.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.MissingObjectPathsForKey_Test.snap index 792c5172d26..d74bb9e695a 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.MissingObjectPathsForKey_Test.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.MissingObjectPathsForKey_Test.snap @@ -4,9 +4,9 @@ "Data": null, "Errors": [ { - "message": "No object paths specified for key '1' in 'map'.", + "message": "File of key '1' is missing.", "extensions": { - "code": "HC0037" + "code": "HC0038" } } ], diff --git a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs index 3a8716bc2ef..57d2ff62c4e 100644 --- a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs +++ b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs @@ -627,8 +627,14 @@ internal void AssignNullValue(ResultElement target) [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void MarkAsDeferred(ResultElement target) { - var cursor = target.Cursor; - _metaDb.SetFlags(cursor, _metaDb.GetFlags(cursor) | ElementFlags.IsDeferred); + // Selection metadata and write filters are tracked on the property row. + var propertyCursor = target.Cursor.AddRows(-1); + var elementTokenType = _metaDb.GetElementTokenType(propertyCursor, resolveReferences: false); + + CheckExpectedType(ElementTokenType.PropertyName, elementTokenType); + + var flags = _metaDb.GetFlags(propertyCursor); + _metaDb.SetFlags(propertyCursor, flags | ElementFlags.IsDeferred); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/HotChocolate/Language/src/Language.Web/OperationIdFormatException.cs b/src/HotChocolate/Language/src/Language.Web/OperationIdFormatException.cs deleted file mode 100644 index a6ab9f23530..00000000000 --- a/src/HotChocolate/Language/src/Language.Web/OperationIdFormatException.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace HotChocolate.Language; - -/// -/// Represents an error that occurs when the operation id has an invalid format. -/// -public sealed class OperationIdFormatException : SyntaxException -{ - /// - /// Initializes a new instance of . - /// - /// - /// The reader that encountered the syntax error. - /// - internal OperationIdFormatException(Utf8GraphQLReader reader) - : base(reader, "The operation id has an invalid format.") - { - } -} diff --git a/src/HotChocolate/Language/src/Language.Web/ThrowHelper.cs b/src/HotChocolate/Language/src/Language.Web/ThrowHelper.cs index c7ecde76130..fcbd9f39f72 100644 --- a/src/HotChocolate/Language/src/Language.Web/ThrowHelper.cs +++ b/src/HotChocolate/Language/src/Language.Web/ThrowHelper.cs @@ -19,6 +19,9 @@ public static InvalidGraphQLRequestException InvalidDocumentIdValue(JsonTokenTyp ThrowHelper_InvalidDocumentIdValue, tokenType)); + public static InvalidGraphQLRequestException InvalidDocumentIdFormat() + => new("The operation id has an invalid format."); + public static InvalidGraphQLRequestException InvalidOperationNameValue(JsonTokenType tokenType) => new(string.Format( CultureInfo.InvariantCulture, diff --git a/src/HotChocolate/Language/src/Language.Web/Utf8GraphQLRequestParser.cs b/src/HotChocolate/Language/src/Language.Web/Utf8GraphQLRequestParser.cs index 80895c63927..84aabf6cfad 100644 --- a/src/HotChocolate/Language/src/Language.Web/Utf8GraphQLRequestParser.cs +++ b/src/HotChocolate/Language/src/Language.Web/Utf8GraphQLRequestParser.cs @@ -200,9 +200,9 @@ private readonly GraphQLRequest ParseRequest(ref Utf8JsonReader reader, Operatio if (reader.TokenType == JsonTokenType.String) { var id = reader.GetString(); - if (!string.IsNullOrEmpty(id)) + if (!string.IsNullOrEmpty(id) && !OperationDocumentId.TryParse(id, out documentId)) { - documentId = new OperationDocumentId(id); + throw ThrowHelper.InvalidDocumentIdFormat(); } } else if (reader.TokenType == JsonTokenType.Null) From e923ed912b8415787e1c4c829427a4c3aa631dd2 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 11 Feb 2026 16:03:18 +0100 Subject: [PATCH 14/46] Fixed null propagation --- .../Core/src/Types/Execution/Processing/ValueCompletion.List.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletion.List.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletion.List.cs index 69935791071..66633045b89 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletion.List.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletion.List.cs @@ -146,6 +146,6 @@ internal static void PropagateNullValues(ResultElement result) } result.Invalidate(); - } while (result.Parent is { IsInvalidated: false }); + } while (result.Parent is { ValueKind: not JsonValueKind.Undefined, IsInvalidated: false }); } } From 57c6b93770eb23409adad4a888a463f347515971 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 11 Feb 2026 16:55:43 +0100 Subject: [PATCH 15/46] Fixed more issues --- .../AspNetCore.Tests/DeferOverHttpTests.cs | 199 +++++++++++++++++- .../HttpPostMiddlewareTests.cs | 189 ----------------- .../HttpPostMiddlewareTests.EmptyRequest.snap | 4 +- ...Correct_With_Defer_If_Condition_False.snap | 1 - ...ultipart_Format_Is_Correct_With_Defer.snap | 14 -- ..._Correct_With_Defer_If_Condition_True.snap | 14 -- ...ltipart_Format_Is_Correct_With_Stream.snap | 14 -- ...wareTests.SingleRequest_Defer_Results.snap | 5 - .../Execution/Processing/QueryExecutor.cs | 4 +- 9 files changed, 202 insertions(+), 242 deletions(-) delete mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_JSON_Format_Is_Correct_With_Defer_If_Condition_False.snap delete mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Defer.snap delete mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Defer_If_Condition_True.snap delete mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Stream.snap delete mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.SingleRequest_Defer_Results.snap diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs index 1a3bb5130fa..5cd9b75c40d 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs @@ -2,6 +2,7 @@ using System.Net.Http.Json; using HotChocolate.AspNetCore.Formatters; using HotChocolate.AspNetCore.Tests.Utilities; +using HotChocolate.Types; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; @@ -279,13 +280,189 @@ ... @defer { """); } + [Theory] + [InlineData(null)] + [InlineData("*/*")] + [InlineData("multipart/mixed")] + [InlineData("multipart/*")] + [InlineData("application/graphql-response+json, multipart/mixed")] + [InlineData("text/event-stream, multipart/mixed")] + public async Task Defer_TypeCondition_Multipart(string? acceptHeader) + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = """ + { + hero { + name + ... on Droid @defer(label: "droid_details") { + primaryFunction + } + } + } + """ + }); + + if (acceptHeader is not null) + { + request.Headers.Add("Accept", acceptHeader); + } + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("multipart/mixed", response.Content.Headers.ContentType?.MediaType); + + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + + --- + Content-Type: application/json; charset=utf-8 + + {"data":{"hero":{"name":"R2-D2"}},"pending":[{"id":2,"path":["hero"],"label":"droid_details"}],"hasNext":true} + --- + Content-Type: application/json; charset=utf-8 + + {"incremental":[{"id":2,"data":{"primaryFunction":"Astromech"}}],"completed":[{"id":2}],"hasNext":false} + ----- + + """); + } + + [Theory] + [InlineData(null)] + [InlineData("*/*")] + [InlineData("multipart/mixed")] + [InlineData("multipart/*")] + [InlineData("application/graphql-response+json, multipart/mixed")] + [InlineData("text/event-stream, multipart/mixed")] + public async Task Defer_TypeCondition_If_True(string? acceptHeader) + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = """ + query($if: Boolean!) { + hero { + name + ... on Droid @defer(label: "droid_details", if: $if) { + primaryFunction + } + } + } + """, + variables = new { @if = true } + }); + + if (acceptHeader is not null) + { + request.Headers.Add("Accept", acceptHeader); + } + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("multipart/mixed", response.Content.Headers.ContentType?.MediaType); + + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + + --- + Content-Type: application/json; charset=utf-8 + + {"data":{"hero":{"name":"R2-D2"}},"pending":[{"id":2,"path":["hero"],"label":"droid_details"}],"hasNext":true} + --- + Content-Type: application/json; charset=utf-8 + + {"incremental":[{"id":2,"data":{"primaryFunction":"Astromech"}}],"completed":[{"id":2}],"hasNext":false} + ----- + + """); + } + + [Theory] + [InlineData(null)] + [InlineData("*/*")] + [InlineData("application/graphql-response+json, multipart/mixed")] + [InlineData("application/graphql-response+json, text/event-stream, multipart/mixed")] + public async Task Defer_TypeCondition_If_False(string? acceptHeader) + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = """ + query($if: Boolean!) { + hero { + name + ... on Droid @defer(label: "droid_details", if: $if) { + primaryFunction + } + } + } + """, + variables = new { @if = false } + }); + + if (acceptHeader is not null) + { + request.Headers.Add("Accept", acceptHeader); + } + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // When defer is disabled, should get a regular JSON response + Assert.Equal("application/graphql-response+json", response.Content.Headers.ContentType?.MediaType); + + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + {"data":{"hero":{"name":"R2-D2","primaryFunction":"Astromech"}}} + """); + } + private TestServer CreateDeferServer(HttpTransportVersion serverTransportVersion = HttpTransportVersion.Latest) { return ServerFactory.Create( services => services .AddRouting() .AddGraphQLServer() - .AddTypeExtension() + .AddQueryType() + .AddType() .AddDefaultBatchDispatcher() .AddHttpResponseFormatter( new HttpResponseFormatterOptions @@ -306,6 +483,26 @@ public sealed class Query { public Product GetProduct() => new("Abc"); + + public ICharacter GetHero() + => new Droid { Name = "R2-D2" }; + } + + [InterfaceType("Character")] + public interface ICharacter + { + string Name { get; } + } + + public sealed class Droid : ICharacter + { + public string Name { get; init; } = default!; + + public async Task GetPrimaryFunctionAsync() + { + await Task.Delay(1000); + return "Astromech"; + } } public sealed record Product(string Name) diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs index 53c885309ad..edbbc881495 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs @@ -261,37 +261,6 @@ query h($id: String!) { result.MatchSnapshot(); } - [Fact] - public async Task SingleRequest_Defer_Results() - { - // arrange - var server = CreateStarWarsServer(); - - // act - var result = - await server.PostRawAsync( - new ClientQueryRequest - { - Query = - """ - { - ... @defer { - wait(m: 300) - } - hero(episode: NEW_HOPE) { - name - ... on Droid @defer(label: "my_id") { - id - } - } - } - """ - }); - - // assert - result.MatchSnapshot(); - } - [Fact] public async Task Single_Diagnostic_Listener_Is_Triggered() { @@ -365,164 +334,6 @@ ... on Droid @defer(label: "my_id") { Assert.True(listenerB.Triggered); } - [Fact] - public async Task Ensure_Multipart_Format_Is_Correct_With_Defer() - { - // arrange - var server = CreateStarWarsServer(); - - // act - var result = - await server.PostHttpAsync( - new ClientQueryRequest - { - Query = - """ - { - ... @defer { - wait(m: 300) - } - hero(episode: NEW_HOPE) { - name - ... on Droid @defer(label: "my_id") { - id - } - } - } - """ - }); - - // assert - new GraphQLHttpResponse(result).MatchInlineSnapshot( - """ - { - "data": { - "hero": { - "name": "R2-D2", - "id": "2001" - }, - "wait": true - } - } - """); - } - - [Fact] - public async Task Ensure_Multipart_Format_Is_Correct_With_Defer_If_Condition_True() - { - // arrange - var server = CreateStarWarsServer(); - - // act - var result = - await server.PostRawAsync( - new ClientQueryRequest - { - Query = - """ - query ($if: Boolean!) { - ... @defer { - wait(m: 300) - } - hero(episode: NEW_HOPE) { - name - ... on Droid @defer(label: "my_id", if: $if) { - id - } - } - } - """, - Variables = new Dictionary { ["if"] = true } - }); - - // assert - result.Content.MatchSnapshot(); - } - - [Fact] - public async Task Ensure_JSON_Format_Is_Correct_With_Defer_If_Condition_False() - { - // arrange - var server = CreateStarWarsServer(); - - // act - var result = - await server.PostRawAsync( - new ClientQueryRequest - { - Query = - """ - query ($if: Boolean!) { - hero(episode: NEW_HOPE) { - name - ... on Droid @defer(label: "my_id", if: $if) { - id - } - } - } - """, - Variables = new Dictionary { ["if"] = false } - }); - - // assert - result.Content.MatchSnapshot(); - } - - [Fact] - public async Task Ensure_Multipart_Format_Is_Correct_With_Stream() - { - // arrange - var server = CreateStarWarsServer(); - - // act - var result = await server.PostHttpAsync( - new ClientQueryRequest - { - Query = - """ - { - ... @defer { - wait(m: 300) - } - hero(episode: NEW_HOPE) { - name - friends(first: 10) { - nodes @stream(initialCount: 1, label: "foo") { - name - } - } - } - } - """ - }); - - // assert - new GraphQLHttpResponse(result).MatchInlineSnapshot( - """ - { - "data": { - "hero": { - "name": "R2-D2", - "friends": { - "nodes": [ - { - "name": "Luke Skywalker" - }, - { - "name": "Han Solo" - }, - { - "name": "Leia Organa" - } - ] - } - }, - "wait": true - } - } - """); - } - [Fact] public async Task SingleRequest_CreateReviewForEpisode_With_ObjectVariable() { diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.EmptyRequest.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.EmptyRequest.snap index 82a6fa4506b..d93d472e374 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.EmptyRequest.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.EmptyRequest.snap @@ -4,9 +4,9 @@ "Data": null, "Errors": [ { - "message": "The GraphQL request is empty.", + "message": "Invalid JSON document.", "extensions": { - "code": "HC0009" + "code": "HC0012" } } ], diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_JSON_Format_Is_Correct_With_Defer_If_Condition_False.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_JSON_Format_Is_Correct_With_Defer_If_Condition_False.snap deleted file mode 100644 index a626698e774..00000000000 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_JSON_Format_Is_Correct_With_Defer_If_Condition_False.snap +++ /dev/null @@ -1 +0,0 @@ -{"data":{"hero":{"name":"R2-D2","id":"2001"}}} diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Defer.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Defer.snap deleted file mode 100644 index 0f0c4e7f9ce..00000000000 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Defer.snap +++ /dev/null @@ -1,14 +0,0 @@ - ---- -Content-Type: application/json; charset=utf-8 - -{"data":{"hero":{"name":"R2-D2"}},"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"data":{"id":"2001"},"label":"my_id","path":["hero"]}],"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"data":{"wait":true},"path":[]}],"hasNext":false} ------ diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Defer_If_Condition_True.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Defer_If_Condition_True.snap deleted file mode 100644 index 0f0c4e7f9ce..00000000000 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Defer_If_Condition_True.snap +++ /dev/null @@ -1,14 +0,0 @@ - ---- -Content-Type: application/json; charset=utf-8 - -{"data":{"hero":{"name":"R2-D2"}},"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"data":{"id":"2001"},"label":"my_id","path":["hero"]}],"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"data":{"wait":true},"path":[]}],"hasNext":false} ------ diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Stream.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Stream.snap deleted file mode 100644 index fe8822d442a..00000000000 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Stream.snap +++ /dev/null @@ -1,14 +0,0 @@ - ---- -Content-Type: application/json; charset=utf-8 - -{"data":{"hero":{"name":"R2-D2","friends":{"nodes":[{"name":"Luke Skywalker"}]}}},"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"items":[{"name":"Han Solo"}],"label":"foo","path":["hero","friends","nodes",1]},{"items":[{"name":"Leia Organa"}],"label":"foo","path":["hero","friends","nodes",2]}],"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"data":{"wait":true},"path":[]}],"hasNext":false} ------ diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.SingleRequest_Defer_Results.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.SingleRequest_Defer_Results.snap deleted file mode 100644 index fc5c3699096..00000000000 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.SingleRequest_Defer_Results.snap +++ /dev/null @@ -1,5 +0,0 @@ -{ - "ContentType": "multipart/mixed; boundary=\"-\"", - "StatusCode": "OK", - "Content": "\r\n---\r\nContent-Type: application/json; charset=utf-8\r\n\r\n{\"data\":{\"hero\":{\"name\":\"R2-D2\"}},\"hasNext\":true}\r\n---\r\nContent-Type: application/json; charset=utf-8\r\n\r\n{\"incremental\":[{\"data\":{\"id\":\"2001\"},\"label\":\"my_id\",\"path\":[\"hero\"]}],\"hasNext\":true}\r\n---\r\nContent-Type: application/json; charset=utf-8\r\n\r\n{\"incremental\":[{\"data\":{\"wait\":true},\"path\":[]}],\"hasNext\":false}\r\n-----\r\n" -} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs index dc0a8d0f5da..ae1cc54474f 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs @@ -86,8 +86,8 @@ public Task ExecuteBatchAsync( int length) { Debug.Assert(length > 0); - Debug.Assert(length > operationContexts.Length); - Debug.Assert(length > results.Length); + Debug.Assert(length <= operationContexts.Length); + Debug.Assert(length <= results.Length); if (operationContexts[0].OperationContext.Operation.HasIncrementalParts) { From 74a2f49097302b5aa7410198d3e061f0d2cd5752 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 12 Feb 2026 11:44:55 +0100 Subject: [PATCH 16/46] Fixed more issues --- .../DefaultWebSocketPayloadFormatter.cs | 30 +++------ .../Formatters/IWebSocketPayloadFormatter.cs | 17 ++--- .../HttpMultipartMiddleware.cs | 25 +++++++- .../DefaultHttpRequestInterceptor.cs | 5 ++ .../ApolloSubscriptionProtocolHandler.cs | 6 +- .../GraphQLOverWebSocketProtocolHandler.cs | 34 ++++------ .../Protocols/MessageUtilities.cs | 2 +- .../JsonResultFormatter.cs | 29 +++++---- .../src/Types.Scalars.Upload/UploadType.cs | 44 +++++++++---- .../Types.Scalars.Upload/UploadValueNode.cs | 62 +++++++++++++++++++ .../Processing/VariableCoercionHelper.cs | 43 ++++++++----- .../Core/src/Types/HotChocolate.Types.csproj | 1 - .../src/Types/Types/Scalars/ScalarType.cs | 49 ++++++++++++++- .../Core/src/Types/Utilities/ThrowHelper.cs | 14 +++++ .../src/Language.Utf8/Utf8MemoryBuilder.cs | 4 +- .../src/Language.Web/JsonValueParser.cs | 25 +++++++- 16 files changed, 292 insertions(+), 98 deletions(-) create mode 100644 src/HotChocolate/Core/src/Types.Scalars.Upload/UploadValueNode.cs diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultWebSocketPayloadFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultWebSocketPayloadFormatter.cs index adf7c5ab3ec..c0931b28100 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultWebSocketPayloadFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultWebSocketPayloadFormatter.cs @@ -1,6 +1,6 @@ -using System.Buffers; using System.Text.Json; using HotChocolate.Text.Json; +using HotChocolate.Transport.Formatters; using static HotChocolate.Execution.JsonValueFormatter; namespace HotChocolate.AspNetCore.Formatters; @@ -8,31 +8,24 @@ namespace HotChocolate.AspNetCore.Formatters; /// /// This represents the default implementation for the . /// -public class DefaultWebSocketPayloadFormatter(WebSocketPayloadFormatterOptions options = default) +public sealed class DefaultWebSocketPayloadFormatter(WebSocketPayloadFormatterOptions options = default) : IWebSocketPayloadFormatter { - private readonly JsonWriterOptions _writerOptions = options.Json.CreateWriterOptions(); private readonly JsonSerializerOptions _serializerOptions = options.Json.CreateSerializerOptions(); + private readonly JsonResultFormatter _internalFormatter = new(options.Json); private readonly JsonNullIgnoreCondition _nullIgnoreCondition = options.Json.NullIgnoreCondition; /// - public void Format(OperationResult result, IBufferWriter bufferWriter) - { - var writer = new JsonWriter(bufferWriter, _writerOptions); - WriteValue(writer, result, _serializerOptions, _nullIgnoreCondition); - } + public void Format(OperationResult result, JsonWriter writer) + => _internalFormatter.Format(result, writer); /// - public void Format(IError error, IBufferWriter bufferWriter) - { - var writer = new JsonWriter(bufferWriter, _writerOptions); - WriteError(writer, error, _serializerOptions, _nullIgnoreCondition); - } + public void Format(IError error, JsonWriter writer) + => WriteError(writer, error, _serializerOptions, _nullIgnoreCondition); /// - public void Format(IReadOnlyList errors, IBufferWriter bufferWriter) + public void Format(IReadOnlyList errors, JsonWriter writer) { - var writer = new JsonWriter(bufferWriter, _writerOptions); writer.WriteStartArray(); for (var i = 0; i < errors.Count; i++) @@ -44,9 +37,6 @@ public void Format(IReadOnlyList errors, IBufferWriter bufferWrite } /// - public void Format(IReadOnlyDictionary extensions, IBufferWriter bufferWriter) - { - var writer = new JsonWriter(bufferWriter, _writerOptions); - WriteDictionary(writer, extensions, _serializerOptions, _nullIgnoreCondition); - } + public void Format(IReadOnlyDictionary extensions, JsonWriter writer) + => WriteDictionary(writer, extensions, _serializerOptions, _nullIgnoreCondition); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IWebSocketPayloadFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IWebSocketPayloadFormatter.cs index 6f3bb56abf0..84752a0b0b1 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IWebSocketPayloadFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IWebSocketPayloadFormatter.cs @@ -1,4 +1,5 @@ using System.Buffers; +using HotChocolate.Text.Json; namespace HotChocolate.AspNetCore.Formatters; @@ -14,9 +15,9 @@ public interface IWebSocketPayloadFormatter /// The GraphQL operation result. /// /// - /// The buffer writer that is used to write the payload. + /// The JSON writer that is used to write the payload. /// - void Format(OperationResult result, IBufferWriter writer); + void Format(OperationResult result, JsonWriter writer); /// /// Formats the into a WebSocket payload. @@ -25,9 +26,9 @@ public interface IWebSocketPayloadFormatter /// The GraphQL execution error. /// /// - /// The buffer writer that is used to write the error. + /// The JSON writer that is used to write the error. /// - void Format(IError error, IBufferWriter writer); + void Format(IError error, JsonWriter writer); /// /// Formats the into a WebSocket payload. @@ -36,9 +37,9 @@ public interface IWebSocketPayloadFormatter /// The GraphQL execution errors. /// /// - /// The buffer writer that is used to write the errors. + /// The JSON writer that is used to write the errors. /// - void Format(IReadOnlyList errors, IBufferWriter writer); + void Format(IReadOnlyList errors, JsonWriter writer); /// /// Formats the into a WebSocket payload. @@ -47,7 +48,7 @@ public interface IWebSocketPayloadFormatter /// The GraphQL extensions. /// /// - /// The buffer writer that is used to write the extensions. + /// The JSON writer that is used to write the extensions. /// - void Format(IReadOnlyDictionary extensions, IBufferWriter writer); + void Format(IReadOnlyDictionary extensions, JsonWriter writer); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpMultipartMiddleware.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpMultipartMiddleware.cs index 10fd4b64ea7..386f803870d 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpMultipartMiddleware.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpMultipartMiddleware.cs @@ -102,6 +102,10 @@ protected override async ValueTask ParseRequestsFromBodyAsync( var multipartRequest = ParseMultipartRequest(form); var requests = session.RequestParser.ParseRequest(multipartRequest.Operations); + // we add the file lookup as a feature on the HttpContext and can grab it from + // there and put it on the GraphQL request. + context.Features.Set(multipartRequest.Files); + for (var i = 0; i < requests.Length; i++) { var current = requests[i]; @@ -110,7 +114,15 @@ protected override async ValueTask ParseRequestsFromBodyAsync( if (!multipartRequest.FileMap.Root.TryGetNode(i.ToString(), out var operationRoot)) { - continue; + // Legacy multipart maps do not include an operation index. + if (requests.Length == 1) + { + operationRoot = multipartRequest.FileMap.Root; + } + else + { + continue; + } } if (current.Variables is null) @@ -132,7 +144,7 @@ protected override async ValueTask ParseRequestsFromBodyAsync( current = current with { - Variables = JsonDocument.Parse(bufferWriter.Memory), + Variables = JsonDocument.Parse(bufferWriter.WrittenMemory), VariablesMemoryOwner = bufferWriter }; context.Response.RegisterForDispose(current); @@ -210,7 +222,14 @@ private void RewriteVariables( ref Utf8JsonReader originalVariables, Utf8JsonWriter variables, FileMapTrieNode fileMapRoot) - => RewriteJsonValue(ref originalVariables, variables, fileMapRoot); + { + if (!originalVariables.Read()) + { + throw new JsonException("The variables JSON payload is empty."); + } + + RewriteJsonValue(ref originalVariables, variables, fileMapRoot); + } private static void RewriteJsonValue( ref Utf8JsonReader reader, diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DefaultHttpRequestInterceptor.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DefaultHttpRequestInterceptor.cs index 2ef47f2f00b..64afdfcca09 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DefaultHttpRequestInterceptor.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DefaultHttpRequestInterceptor.cs @@ -19,6 +19,11 @@ public virtual ValueTask OnCreateAsync( { var userState = new UserState(context.User); + if (context.Features.Get() is { } featureLookup) + { + requestBuilder.Features.Set(featureLookup); + } + requestBuilder.Features.Set(userState); requestBuilder.Features.Set(context); requestBuilder.Features.Set(context.User); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs index b2e3ac60836..4950944b520 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs @@ -249,7 +249,7 @@ public async ValueTask SendResultMessageAsync( jsonWriter.WritePropertyName(MessageProperties.Type); jsonWriter.WriteStringValue(Utf8Messages.Data); jsonWriter.WritePropertyName(Payload); - _formatter.Format(result, arrayWriter); + _formatter.Format(result, jsonWriter); jsonWriter.WriteEndObject(); await session.Connection.SendAsync(arrayWriter.WrittenMemory, cancellationToken); } @@ -268,7 +268,7 @@ public async ValueTask SendErrorMessageAsync( jsonWriter.WritePropertyName(MessageProperties.Type); jsonWriter.WriteStringValue(Utf8Messages.Error); jsonWriter.WritePropertyName(Payload); - _formatter.Format(errors[0], arrayWriter); + _formatter.Format(errors[0], jsonWriter); jsonWriter.WriteEndObject(); await session.Connection.SendAsync(arrayWriter.WrittenMemory, cancellationToken); } @@ -317,7 +317,7 @@ private async ValueTask SendConnectionRejectMessage( } else { - _formatter.Format(extensions, arrayWriter); + _formatter.Format(extensions, jsonWriter); } jsonWriter.WriteEndObject(); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs index 679599aa778..798ff20b98c 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs @@ -13,19 +13,11 @@ namespace HotChocolate.AspNetCore.Subscriptions.Protocols.GraphQLOverWebSocket; -internal sealed class GraphQLOverWebSocketProtocolHandler : IGraphQLOverWebSocketProtocolHandler +internal sealed class GraphQLOverWebSocketProtocolHandler( + ISocketSessionInterceptor interceptor, + IWebSocketPayloadFormatter formatter) + : IGraphQLOverWebSocketProtocolHandler { - private readonly ISocketSessionInterceptor _interceptor; - private readonly IWebSocketPayloadFormatter _formatter; - - public GraphQLOverWebSocketProtocolHandler( - ISocketSessionInterceptor interceptor, - IWebSocketPayloadFormatter formatter) - { - _interceptor = interceptor; - _formatter = formatter; - } - public string Name => GraphQL_Transport_WS; public async ValueTask OnReceiveAsync( @@ -76,7 +68,7 @@ private async ValueTask OnReceiveInternalAsync( : PingMessage.Default; var responsePayload = - await _interceptor.OnPingAsync(session, operationMessageObj, cancellationToken); + await interceptor.OnPingAsync(session, operationMessageObj, cancellationToken); await SendPongMessageAsync(session, responsePayload, cancellationToken); return; @@ -89,7 +81,7 @@ private async ValueTask OnReceiveInternalAsync( ? new PongMessage(payload) : PongMessage.Default; - await _interceptor.OnPongAsync(session, operationMessageObj, cancellationToken); + await interceptor.OnPongAsync(session, operationMessageObj, cancellationToken); return; } @@ -107,7 +99,7 @@ private async ValueTask OnReceiveInternalAsync( : ConnectionInitMessage.Default; var connectionStatus = - await _interceptor.OnConnectAsync( + await interceptor.OnConnectAsync( session, operationMessageObj, cancellationToken); @@ -221,7 +213,7 @@ public async ValueTask SendResultMessageAsync( jsonWriter.WritePropertyName(MessageProperties.Type); jsonWriter.WriteStringValue(Utf8Messages.Next); jsonWriter.WritePropertyName(Payload); - _formatter.Format(result, arrayWriter); + formatter.Format(result, jsonWriter); jsonWriter.WriteEndObject(); await session.Connection.SendAsync(arrayWriter.WrittenMemory, cancellationToken); } @@ -240,7 +232,7 @@ public async ValueTask SendErrorMessageAsync( jsonWriter.WritePropertyName(MessageProperties.Type); jsonWriter.WriteStringValue(Utf8Messages.Error); jsonWriter.WritePropertyName(Payload); - _formatter.Format(errors, arrayWriter); + formatter.Format(errors, jsonWriter); jsonWriter.WriteEndObject(); await session.Connection.SendAsync(arrayWriter.WrittenMemory, cancellationToken); } @@ -251,7 +243,7 @@ public async ValueTask SendCompleteMessageAsync( CancellationToken cancellationToken) { using var writer = new PooledArrayWriter(); - SerializeMessage(writer, _formatter, Utf8Messages.Complete, id: operationSessionId); + SerializeMessage(writer, formatter, Utf8Messages.Complete, id: operationSessionId); await session.Connection.SendAsync(writer.WrittenMemory, cancellationToken); } @@ -267,7 +259,7 @@ public async ValueTask SendPingMessageAsync( else { using var writer = new PooledArrayWriter(); - SerializeMessage(writer, _formatter, Utf8Messages.Ping, payload); + SerializeMessage(writer, formatter, Utf8Messages.Ping, payload); await session.Connection.SendAsync(writer.WrittenMemory, cancellationToken); } } @@ -284,7 +276,7 @@ private async ValueTask SendPongMessageAsync( else { using var writer = new PooledArrayWriter(); - SerializeMessage(writer, _formatter, Utf8Messages.Pong, payload); + SerializeMessage(writer, formatter, Utf8Messages.Pong, payload); await session.Connection.SendAsync(writer.WrittenMemory, cancellationToken); } } @@ -295,7 +287,7 @@ private async ValueTask SendConnectionAcceptMessage( CancellationToken cancellationToken) { using var writer = new PooledArrayWriter(); - SerializeMessage(writer, _formatter, Utf8Messages.ConnectionAccept, payload); + SerializeMessage(writer, formatter, Utf8Messages.ConnectionAccept, payload); await session.Connection.SendAsync(writer.WrittenMemory, cancellationToken); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/MessageUtilities.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/MessageUtilities.cs index f2c19a3b6f9..23ec241fa08 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/MessageUtilities.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/MessageUtilities.cs @@ -33,7 +33,7 @@ public static void SerializeMessage( if (payload is not null) { jsonWriter.WritePropertyName("payload"u8); - formatter.Format(payload, pooledArrayWriter); + formatter.Format(payload, jsonWriter); } jsonWriter.WriteEndObject(); diff --git a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs index c8e1f420ced..eba71eaad70 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs @@ -89,35 +89,42 @@ public ValueTask FormatAsync( private void FormatInternal(OperationResult result, IBufferWriter bufferWriter) { var jsonWriter = new JsonWriter(bufferWriter, _options); + Format(result, jsonWriter); + } + + public void Format(OperationResult result, JsonWriter writer) + { + ArgumentNullException.ThrowIfNull(result); + ArgumentNullException.ThrowIfNull(writer); - jsonWriter.WriteStartObject(); + writer.WriteStartObject(); if (result.RequestIndex.HasValue) { - jsonWriter.WritePropertyName(RequestIndex); - jsonWriter.WriteNumberValue(result.RequestIndex.Value); + writer.WritePropertyName(RequestIndex); + writer.WriteNumberValue(result.RequestIndex.Value); } if (result.VariableIndex.HasValue) { - jsonWriter.WritePropertyName(VariableIndex); - jsonWriter.WriteNumberValue(result.VariableIndex.Value); + writer.WritePropertyName(VariableIndex); + writer.WriteNumberValue(result.VariableIndex.Value); } WriteErrors( - jsonWriter, + writer, result.Errors, _serializerOptions, default); if (result.Data.HasValue) { - jsonWriter.WritePropertyName(Data); - result.Data.Value.Formatter.WriteDataTo(jsonWriter); + writer.WritePropertyName(Data); + result.Data.Value.Formatter.WriteDataTo(writer); } WriteExtensions( - jsonWriter, + writer, result.Extensions, _serializerOptions, default); @@ -125,13 +132,13 @@ private void FormatInternal(OperationResult result, IBufferWriter bufferWr if (result.IsIncremental) { WriteIncremental( - jsonWriter, + writer, result, _serializerOptions, default); } - jsonWriter.WriteEndObject(); + writer.WriteEndObject(); } private async ValueTask FormatInternalAsync( diff --git a/src/HotChocolate/Core/src/Types.Scalars.Upload/UploadType.cs b/src/HotChocolate/Core/src/Types.Scalars.Upload/UploadType.cs index 58d6bcf5696..4f52c5bbedc 100644 --- a/src/HotChocolate/Core/src/Types.Scalars.Upload/UploadType.cs +++ b/src/HotChocolate/Core/src/Types.Scalars.Upload/UploadType.cs @@ -10,7 +10,7 @@ namespace HotChocolate.Types; /// /// The GraphQL Upload scalar. /// -public sealed class UploadType : ScalarType +public sealed class UploadType : ScalarType { /// /// Initializes a new instance of the class. @@ -21,14 +21,20 @@ public UploadType() : base("Upload", BindingBehavior.Implicit) Description = UploadResources.UploadType_Description; } - /// - /// This operation is not supported. Upload scalars cannot be used in GraphQL literals. - /// - /// The GraphQL literal (not used). - /// Never returns; always throws. - /// Always thrown as literal input is not supported. - protected override IFile OnCoerceInputLiteral(StringValueNode valueLiteral) - => throw new NotSupportedException(); + public override ScalarSerializationType SerializationType => ScalarSerializationType.String; + + /// + public override object CoerceInputLiteral(IValueNode valueLiteral) + { + if (valueLiteral is not UploadValueNode uploadValue) + { + throw new LeafCoercionException( + $"Cannot coerce the literal of type `{valueLiteral.Kind}` to a file.", + this); + } + + return uploadValue.File; + } /// /// Coerces a JSON string value containing a file reference into an instance. @@ -46,8 +52,15 @@ protected override IFile OnCoerceInputLiteral(StringValueNode valueLiteral) /// /// Thrown when the file reference cannot be found in the file lookup service. /// - protected override IFile OnCoerceInputValue(JsonElement inputValue, IFeatureProvider context) + public override object CoerceInputValue(JsonElement inputValue, IFeatureProvider context) { + if (inputValue.ValueKind is not JsonValueKind.String) + { + throw new LeafCoercionException( + $"Cannot coerce the json value of kind `{inputValue.ValueKind}` to a file.", + this); + } + var fileLookup = context.Features.Get(); var fileName = inputValue.GetString()!; @@ -69,7 +82,7 @@ protected override IFile OnCoerceInputValue(JsonElement inputValue, IFeatureProv /// The runtime value (not used). /// The result element (not used). /// Always thrown as output coercion is not supported. - protected override void OnCoerceOutputValue(IFile runtimeValue, ResultElement resultValue) + public override void OnCoerceOutputValue(IFile runtimeValue, ResultElement resultValue) => throw new NotSupportedException(); /// @@ -78,6 +91,13 @@ protected override void OnCoerceOutputValue(IFile runtimeValue, ResultElement re /// The runtime value (not used). /// Never returns; always throws. /// Always thrown as value to literal conversion is not supported. - protected override StringValueNode OnValueToLiteral(IFile runtimeValue) + public override IValueNode OnValueToLiteral(IFile runtimeValue) => throw new NotSupportedException(); + + /// + public override IValueNode InputValueToLiteral(JsonElement inputValue, IFeatureProvider context) + { + var file = (IFile)CoerceInputValue(inputValue, context); + return new UploadValueNode(inputValue.GetString()!, file); + } } diff --git a/src/HotChocolate/Core/src/Types.Scalars.Upload/UploadValueNode.cs b/src/HotChocolate/Core/src/Types.Scalars.Upload/UploadValueNode.cs new file mode 100644 index 00000000000..c6b2c68dcdc --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Scalars.Upload/UploadValueNode.cs @@ -0,0 +1,62 @@ +using HotChocolate.Language; + +namespace HotChocolate.Types; + +/// +/// Represents an upload value node in the GraphQL abstract syntax tree (AST). +/// This value node is used to represent file uploads in GraphQL operations, +/// containing both a key for identification and the actual file data. +/// +public sealed class UploadValueNode : IValueNode +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The unique key identifying this upload in the multipart request. + /// + /// + /// The file data associated with this upload. + /// + /// + /// is null or empty. + /// + /// + /// is null. + /// + public UploadValueNode(string key, IFile file) + { + ArgumentException.ThrowIfNullOrEmpty(key); + ArgumentNullException.ThrowIfNull(file); + + Key = key; + File = file; + } + + /// + /// Gets the unique key identifying this upload in the multipart request. + /// + public string Key { get; } + + /// + /// Gets the file data associated with this upload. + /// + public IFile File { get; } + + object? IValueNode.Value => Key; + + /// + public SyntaxKind Kind => SyntaxKind.StringValue; + + /// + public Language.Location? Location => null; + + /// + public IEnumerable GetNodes() => []; + + /// + public override string ToString() => ToString(true); + + /// + public string ToString(bool indented) => $"\"{Key}\""; +} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/VariableCoercionHelper.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/VariableCoercionHelper.cs index bc1920bd6ce..a463ceb3686 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/VariableCoercionHelper.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/VariableCoercionHelper.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Text.Json; using HotChocolate.Features; using HotChocolate.Language; @@ -35,6 +36,28 @@ public void CoerceVariableValues( nameof(variableValues)); } + try + { + CoerceVariableValuesInternal(schema, variableDefinitions, variableValues, coercedValues, context); + } + finally + { + var memoryBuilder = context.Features.Get(); + if (memoryBuilder is not null) + { + memoryBuilder.Seal(); + context.Features.Set(null); + } + } + } + + private void CoerceVariableValuesInternal( + ISchemaDefinition schema, + IReadOnlyList variableDefinitions, + JsonElement variableValues, + Dictionary coercedValues, + IFeatureProvider context) + { var hasVariables = variableValues.ValueKind is JsonValueKind.Object; for (var i = 0; i < variableDefinitions.Count; i++) @@ -93,12 +116,11 @@ private VariableValue CoerceVariableValue( IFeatureProvider context) { var root = Path.Root.Append(variableDefinition.Variable.Name.Value); - var valueParser = new JsonValueParser(); try { var runtimeValue = _inputParser.ParseInputValue(inputValue, variableType, context, path: root); - var valueLiteral = CoerceInputLiteral(inputValue, variableType, ref valueParser, depth: 0); + var valueLiteral = CoerceInputLiteral(inputValue, variableType, context, depth: 0); return new VariableValue( variableDefinition.Variable.Name.Value, @@ -108,19 +130,12 @@ private VariableValue CoerceVariableValue( } catch (GraphQLException) { - valueParser._memory?.Abandon(); throw; } catch (Exception ex) { - valueParser._memory?.Abandon(); throw ThrowHelper.VariableValueInvalidType(variableDefinition, ex); } - finally - { - valueParser._memory?.Seal(); - valueParser._memory = null; - } } private static IInputType AssertInputType( @@ -138,7 +153,7 @@ private static IInputType AssertInputType( private IValueNode CoerceInputLiteral( JsonElement inputValue, IInputType type, - ref JsonValueParser valueParser, + IFeatureProvider context, int depth) { if (depth > 64) @@ -154,7 +169,7 @@ private IValueNode CoerceInputLiteral( switch (type.Kind) { case TypeKind.Scalar: - return valueParser.Parse(inputValue, depth); + return Unsafe.As(type).InputValueToLiteral(inputValue, context); case TypeKind.Enum: if (inputValue.ValueKind is not JsonValueKind.String) @@ -191,7 +206,7 @@ private IValueNode CoerceInputLiteral( } else { - var value = CoerceInputLiteral(property.Value, field.Type, ref valueParser, depth + 1); + var value = CoerceInputLiteral(property.Value, field.Type, context, depth + 1); fields.Add(new ObjectFieldNode(field.Name, value)); } } @@ -223,13 +238,13 @@ private IValueNode CoerceInputLiteral( foreach (var item in inputValue.EnumerateArray()) { - items.Add(CoerceInputLiteral(item, elementType, ref valueParser, elementDepth)); + items.Add(CoerceInputLiteral(item, elementType, context, elementDepth)); } return new ListValueNode(items); case TypeKind.NonNull: - return CoerceInputLiteral(inputValue, type.InnerType().EnsureInputType(), ref valueParser, depth); + return CoerceInputLiteral(inputValue, type.InnerType().EnsureInputType(), context, depth); default: throw new NotSupportedException(); diff --git a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj index 33ab8f4d448..40c34203c94 100644 --- a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj +++ b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj @@ -45,7 +45,6 @@ - diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs index 43e768d69cd..ccab59f380c 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs @@ -5,6 +5,7 @@ using HotChocolate.Text.Json; using HotChocolate.Types.Descriptors.Configurations; using HotChocolate.Utilities; +using static HotChocolate.Utilities.ThrowHelper; using static HotChocolate.Serialization.SchemaDebugFormatter; namespace HotChocolate.Types; @@ -182,7 +183,7 @@ public virtual bool IsValueCompatible(JsonElement inputValue) } if ((SerializationType & ScalarSerializationType.Boolean) == ScalarSerializationType.Boolean - && inputValue.ValueKind == JsonValueKind.True) + && (inputValue.ValueKind == JsonValueKind.True || inputValue.ValueKind == JsonValueKind.False)) { return true; } @@ -214,6 +215,52 @@ public virtual bool IsValueCompatible(JsonElement inputValue) /// public abstract IValueNode ValueToLiteral(object runtimeValue); + /// + /// Converts a JSON input value into a GraphQL literal (AST value node). + /// + /// + /// The JSON input value to convert. + /// + /// + /// Provides access to the coercion context, including features like memory builders + /// for efficient JSON parsing. + /// + /// + /// Returns a GraphQL literal representation (AST value node) of the input value. + /// + /// + /// Unable to convert the given into a literal. + /// + public virtual IValueNode InputValueToLiteral(JsonElement inputValue, IFeatureProvider context) + { + if (!IsValueCompatible(inputValue)) + { + throw CreateInputValueToLiteralError(inputValue, context); + } + + var utf8MemoryBuilder = context.Features.Get(); + var jsonValueParser = new JsonValueParser(ref utf8MemoryBuilder); + return jsonValueParser.Parse(inputValue); + } + + /// + /// Creates the exception to throw when + /// encounters an incompatible input value. + /// + /// + /// The incompatible input value. + /// + /// + /// The coercion context. + /// + /// + /// A describing the coercion failure. + /// + protected virtual LeafCoercionException CreateInputValueToLiteralError( + JsonElement inputValue, + IFeatureProvider context) + => Scalar_Cannot_ConvertValueToLiteral(this, inputValue); + /// /// Returns a string that represents the current . /// diff --git a/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs b/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs index 3590225cadc..c66cc02803e 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs @@ -672,6 +672,20 @@ public static LeafCoercionException Scalar_Cannot_ConvertValueToLiteral( scalarType); } + public static LeafCoercionException Scalar_Cannot_ConvertInputValueToLiteral( + ITypeDefinition scalarType, + JsonElement inputValue) + { + return new LeafCoercionException( + ErrorBuilder.New() + .SetMessage( + TypeResources.Scalar_Cannot_ConvertValueToLiteral, + scalarType.Name, + inputValue.ValueKind) + .Build(), + scalarType); + } + public static LeafCoercionException Scalar_Cannot_CoerceOutputValue( ITypeDefinition scalarType, object runtimeValue) diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8MemoryBuilder.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8MemoryBuilder.cs index 6c0b03651da..0ebe067af51 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8MemoryBuilder.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8MemoryBuilder.cs @@ -98,7 +98,9 @@ public void Seal() throw new InvalidOperationException("Memory is sealed."); } - var finalArray = _buffer.AsSpan().Slice(0, _written).ToArray(); + var finalArray = _written > 0 + ? _buffer.AsSpan().Slice(0, _written).ToArray() + : []; ArrayPool.Shared.Return(_buffer); _buffer = finalArray; } diff --git a/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs b/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs index 7e1ab3b545a..2ebc36b041d 100644 --- a/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs +++ b/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs @@ -12,7 +12,12 @@ public ref struct JsonValueParser { private const int DefaultMaxAllowedDepth = 64; private readonly int _maxAllowedDepth; +#if NET8_0_OR_GREATER + private readonly bool _doNotSeal; + internal ref Utf8MemoryBuilder? _memory; +#else internal Utf8MemoryBuilder? _memory; +#endif private readonly PooledArrayWriter? _externalBuffer; public JsonValueParser() @@ -37,6 +42,15 @@ public JsonValueParser(PooledArrayWriter buffer) _externalBuffer = buffer; } +#if NET8_0_OR_GREATER + internal JsonValueParser(ref Utf8MemoryBuilder? memoryBuilder) + { + _maxAllowedDepth = DefaultMaxAllowedDepth; + _memory = ref memoryBuilder; + _doNotSeal = true; + } +#endif + public IValueNode Parse(JsonElement element) { if (element.ValueKind is JsonValueKind.Undefined) @@ -213,8 +227,15 @@ public IValueNode Parse(ref Utf8JsonReader reader) } finally { - _memory?.Seal(); - _memory = null; +#if NET8_0_OR_GREATER + if (!_doNotSeal) + { +#endif + _memory?.Seal(); + _memory = null; +#if NET8_0_OR_GREATER + } +#endif } } From 635251fc9f15353bbefd0799bd9207d2c65d895d Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 12 Feb 2026 12:44:36 +0100 Subject: [PATCH 17/46] Fixed more issues --- .../Utilities/MiddlewareHelper.cs | 10 ++++++++++ ...stMiddlewareTests.SingleRequest_Incomplete.snap | 10 ++-------- ...istedOperation_HttpPost_Empty_Body_InvalidId.md | 2 +- .../Core/src/Types/Types/Scalars/ScalarType.cs | 14 ++++++++++++-- .../Language/src/Language.Web/JsonValueParser.cs | 13 +++---------- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/MiddlewareHelper.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/MiddlewareHelper.cs index d1fb10b3256..fc1c03fa696 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/MiddlewareHelper.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/MiddlewareHelper.cs @@ -132,6 +132,16 @@ await executorSession.RequestParser.ParsePersistedOperationRequestAsync( context.RequestAborted); context.Response.RegisterForDispose(request); } + catch (InvalidGraphQLRequestException ex) + { + // A GraphQL request exception is thrown if the HTTP request body couldn't be + // parsed. In this case, we will return HTTP status code 400 and return a + // GraphQL error result. + IError error = new Error { Message = ex.Message }; + error = executorSession.Handle(error); + executorSession.DiagnosticEvents.ParserErrors(context, [error]); + return new ParseRequestResult([error], HttpStatusCode.BadRequest); + } catch (GraphQLRequestException ex) { // A GraphQL request exception is thrown if the HTTP request body couldn't be diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.SingleRequest_Incomplete.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.SingleRequest_Incomplete.snap index bb4c9dbecaf..d93d472e374 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.SingleRequest_Incomplete.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.SingleRequest_Incomplete.snap @@ -4,15 +4,9 @@ "Data": null, "Errors": [ { - "message": "Expected a `String`-token, but found a `EndOfFile`-token.", - "locations": [ - { - "line": 1, - "column": 15 - } - ], + "message": "Invalid JSON document.", "extensions": { - "code": "HC0011" + "code": "HC0012" } } ], diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/PersistedOperationMiddlewareTests.ExecutePersistedOperation_HttpPost_Empty_Body_InvalidId.md b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/PersistedOperationMiddlewareTests.ExecutePersistedOperation_HttpPost_Empty_Body_InvalidId.md index 5c779d7f90d..6a65aa830ec 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/PersistedOperationMiddlewareTests.ExecutePersistedOperation_HttpPost_Empty_Body_InvalidId.md +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/PersistedOperationMiddlewareTests.ExecutePersistedOperation_HttpPost_Empty_Body_InvalidId.md @@ -4,7 +4,7 @@ { "errors": [ { - "message": "The operation id has an invalid format." + "message": "The GraphQL document ID contains invalid characters." } ] } diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs index ccab59f380c..9b5190bab99 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs @@ -239,8 +239,18 @@ public virtual IValueNode InputValueToLiteral(JsonElement inputValue, IFeaturePr } var utf8MemoryBuilder = context.Features.Get(); - var jsonValueParser = new JsonValueParser(ref utf8MemoryBuilder); - return jsonValueParser.Parse(inputValue); + var builderExists = utf8MemoryBuilder is not null; + + var jsonValueParser = new JsonValueParser(doNotSeal: true); + jsonValueParser._memory = utf8MemoryBuilder; + var literal = jsonValueParser.Parse(inputValue); + + if (!builderExists) + { + context.Features.Set(utf8MemoryBuilder); + } + + return literal; } /// diff --git a/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs b/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs index 2ebc36b041d..2f5496df951 100644 --- a/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs +++ b/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs @@ -1,6 +1,6 @@ using System.Buffers; -using System.Text.Json; using System.Runtime.InteropServices; +using System.Text.Json; using HotChocolate.Buffers; namespace HotChocolate.Language; @@ -12,12 +12,8 @@ public ref struct JsonValueParser { private const int DefaultMaxAllowedDepth = 64; private readonly int _maxAllowedDepth; -#if NET8_0_OR_GREATER private readonly bool _doNotSeal; - internal ref Utf8MemoryBuilder? _memory; -#else internal Utf8MemoryBuilder? _memory; -#endif private readonly PooledArrayWriter? _externalBuffer; public JsonValueParser() @@ -42,14 +38,11 @@ public JsonValueParser(PooledArrayWriter buffer) _externalBuffer = buffer; } -#if NET8_0_OR_GREATER - internal JsonValueParser(ref Utf8MemoryBuilder? memoryBuilder) + internal JsonValueParser(bool doNotSeal) { _maxAllowedDepth = DefaultMaxAllowedDepth; - _memory = ref memoryBuilder; - _doNotSeal = true; + _doNotSeal = doNotSeal; } -#endif public IValueNode Parse(JsonElement element) { From f2b14ae0c3d77560fa9e5256ee9b2a09dfe7c483 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 12 Feb 2026 14:50:34 +0100 Subject: [PATCH 18/46] Fixed tests --- ...TypeConverterTests.ExceptionPropagation.cs | 19 +++++++++++---- ...rFilter_Mutation_NestedDirectiveInput.snap | 22 ++++++++++------- ...ationConventions_NestedDirectiveInput.snap | 24 ++++++++++++------- ...bleInErrorFilter_NestedDirectiveInput.snap | 22 ++++++++++------- ...QueryConventions_NestedDirectiveInput.snap | 20 +++++++++------- 5 files changed, 66 insertions(+), 41 deletions(-) diff --git a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/TypeConverterTests.ExceptionPropagation.cs b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/TypeConverterTests.ExceptionPropagation.cs index 19a9252ca15..fe9a607ea91 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/TypeConverterTests.ExceptionPropagation.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/TypeConverterTests.ExceptionPropagation.cs @@ -76,7 +76,10 @@ public async Task Exception_IsAvailableInErrorFilter(TestCase testCase) .AddGraphQLServer() .AddQueryType() .AddDirectiveType() - .AddTypeConverter(x => x == "ok" ? new BrokenType(1) : throw new CustomIdSerializationException("Boom")) + .AddTypeConverter( + x => x == "ok" + ? new BrokenType(1) + : throw new CustomIdSerializationException("Boom")) .BindRuntimeType() .ModifyRequestOptions(x => x.IncludeExceptionDetails = true) .AddErrorFilter(x => @@ -116,9 +119,13 @@ public async Task Exception_IsAvailableInErrorFilter_Mutation(TestCase testCase) Exception? caughtException = null; var executor = await new ServiceCollection() .AddGraphQLServer() + .AddQueryType() .AddMutationType() .AddDirectiveType() - .AddTypeConverter(x => x == "ok" ? new BrokenType(1) : throw new CustomIdSerializationException("Boom")) + .AddTypeConverter( + x => x == "ok" + ? new BrokenType(1) + : throw new CustomIdSerializationException("Boom")) .BindRuntimeType() .ModifyRequestOptions(x => x.IncludeExceptionDetails = true) .ModifyOptions(x => x.StrictValidation = false) @@ -161,6 +168,7 @@ public async Task Exception_IsAvailableInErrorFilter_Mutation_WithMutationConven Exception? caughtException = null; var executor = await new ServiceCollection() .AddGraphQLServer() + .AddQueryType() .AddMutationType() .AddMutationConventions() .AddDirectiveType() @@ -175,6 +183,8 @@ public async Task Exception_IsAvailableInErrorFilter_Mutation_WithMutationConven }) .BuildRequestExecutorAsync(); + var s = executor.Schema.ToString(); + // act var requestBuilder = OperationRequestBuilder .New() @@ -280,11 +290,10 @@ public class SomeQuery public string? FieldWithListOfScalarsInput(List arg) => null; public string? FieldWithObjectWithListOfScalarsInput(ObjectWithListOfIds arg) => null; public string? FieldWithNestedObjectInput(NestedObject arg) => null; - // ReSharper disable once MemberHidesStaticFromOuterClass public string? FieldWithListOfObjectsInput(ListOfObjectsInput arg) => null; public string? FieldWithNonNullScalarInput([GraphQLNonNullType] BrokenType arg) => null; public string? Echo(string arg) => null; - public NestedObject? NestedObjectOutput => null; + public NestedObject? NestedObjectOutput => new NestedObject(new ObjectWithId(new BrokenType(1))); } public class SomeQueryConventionFriendlyQueryType @@ -307,7 +316,7 @@ public class SomeQueryConventionFriendlyQueryType [Error] public ObjectWithId? Echo(string arg) => null; [Error] - public NestedObject? NestedObjectOutput => null; + public NestedObject? NestedObjectOutput => new NestedObject(new ObjectWithId(new BrokenType(1))); } public class CustomIdSerializationException(string message) : Exception(message) diff --git a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_NestedDirectiveInput.snap b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_NestedDirectiveInput.snap index 9e2d4ccb7fc..45759d08ac1 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_NestedDirectiveInput.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_NestedDirectiveInput.snap @@ -8,19 +8,23 @@ "column": 54 } ], + "extensions": { + "code": "HC0001", + "coordinate": "boom.arg" + } + }, + { + "message": "Cannot return null for non-nullable field.", "path": [ "nestedObjectOutput", - "inner", - "id" + "inner" ], "extensions": { - "code": "HC0001", - "coordinate": "boom.arg", - "exception": { - "message": "Boom", - "stackTrace": "Test" - } + "code": "HC0018" } } - ] + ], + "data": { + "nestedObjectOutput": null + } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_WithMutationConventions_NestedDirectiveInput.snap b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_WithMutationConventions_NestedDirectiveInput.snap index eb4e0bb639b..8b7229c6d2a 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_WithMutationConventions_NestedDirectiveInput.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_WithMutationConventions_NestedDirectiveInput.snap @@ -8,20 +8,26 @@ "column": 70 } ], + "extensions": { + "code": "HC0001", + "coordinate": "boom.arg" + } + }, + { + "message": "Cannot return null for non-nullable field.", "path": [ "nestedObjectOutput", "nestedObject", - "inner", - "id" + "inner" ], "extensions": { - "code": "HC0001", - "coordinate": "boom.arg", - "exception": { - "message": "Boom", - "stackTrace": "Test" - } + "code": "HC0018" } } - ] + ], + "data": { + "nestedObjectOutput": { + "nestedObject": null + } + } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_NestedDirectiveInput.snap b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_NestedDirectiveInput.snap index 22ca9203d52..88ff7f8df06 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_NestedDirectiveInput.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_NestedDirectiveInput.snap @@ -8,19 +8,23 @@ "column": 46 } ], + "extensions": { + "code": "HC0001", + "coordinate": "boom.arg" + } + }, + { + "message": "Cannot return null for non-nullable field.", "path": [ "nestedObjectOutput", - "inner", - "id" + "inner" ], "extensions": { - "code": "HC0001", - "coordinate": "boom.arg", - "exception": { - "message": "Boom", - "stackTrace": "Test" - } + "code": "HC0018" } } - ] + ], + "data": { + "nestedObjectOutput": null + } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_WithQueryConventions_NestedDirectiveInput.snap b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_WithQueryConventions_NestedDirectiveInput.snap index e83aca75a15..1eeced3e4d7 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_WithQueryConventions_NestedDirectiveInput.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_WithQueryConventions_NestedDirectiveInput.snap @@ -8,19 +8,21 @@ "column": 68 } ], + "extensions": { + "code": "HC0001", + "coordinate": "boom.arg" + } + }, + { + "message": "Cannot return null for non-nullable field.", "path": [ "nestedObjectOutput", - "inner", - "id" + "inner" ], "extensions": { - "code": "HC0001", - "coordinate": "boom.arg", - "exception": { - "message": "Boom", - "stackTrace": "Test" - } + "code": "HC0018" } } - ] + ], + "data": null } From 5c02c4a21dafb3caa24cec21ca057f5fd266409f Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 12 Feb 2026 17:38:18 +0100 Subject: [PATCH 19/46] Fixed more tests --- .../Extensions/SnapshotExtensions.cs | 16 + .../GraphQLSnapshotValueFormatter.cs | 2 +- .../CookieCrumbleHotChocolate.cs | 1 - .../Extensions/SnapshotExtensions.cs | 10 - .../GraphQLSnapshotValueFormatter.cs | 40 - .../Formatters/SnapshotValueFormatters.cs | 3 - .../test/Directory.Build.props | 1 + .../AspNetCore/test/Directory.Build.props | 1 + .../Caching/test/Directory.Build.props | 1 + .../Core/test/Directory.Build.props | 1 + .../Processing/VariableCoercionHelperTests.cs | 90 ++- .../CostAnalysis/test/Directory.Build.props | 1 + .../Data/test/Directory.Build.props | 1 + ...otChocolate.Fusion.AspNetCore.Tests.csproj | 1 + .../Utilities/SyntaxSerializer.QuerySyntax.cs | 9 +- .../SyntaxSerializer.SchemaSyntax.cs | 2 +- .../Utilities/SyntaxSerializer.cs | 4 +- .../Utilities/SyntaxWriterExtensions.cs | 88 ++- .../SyntaxWriterTests.cs | 719 ++++++++++++++++++ ...irectives_Indented_MatchesSnapshot.graphql | 3 + ...efinition_Indented_MatchesSnapshot.graphql | 13 + ...efinition_Indented_MatchesSnapshot.graphql | 3 + ...efinition_Indented_MatchesSnapshot.graphql | 5 + ...irectives_Indented_MatchesSnapshot.graphql | 5 + ...jectValue_Indented_MatchesSnapshot.graphql | 3 + ...irectives_Indented_MatchesSnapshot.graphql | 11 + ...irectives_Indented_MatchesSnapshot.graphql | 12 + ...efinition_Indented_MatchesSnapshot.graphql | 5 + ...efinition_Indented_MatchesSnapshot.graphql | 8 + ...ListValue_Indented_MatchesSnapshot.graphql | 11 + ...tValue_NotIndented_MatchesSnapshot.graphql | 1 + ...efinition_Indented_MatchesSnapshot.graphql | 6 + ...jectValue_Indented_MatchesSnapshot.graphql | 7 + ...tValue_NotIndented_MatchesSnapshot.graphql | 1 + ...esOnField_Indented_MatchesSnapshot.graphql | 7 + ...irectives_Indented_MatchesSnapshot.graphql | 5 + ...ectives_NoWrapping_MatchesSnapshot.graphql | 4 + ...tives_WithWrapping_MatchesSnapshot.graphql | 14 + ...efinition_Indented_MatchesSnapshot.graphql | 1 + ...jectValue_Indented_MatchesSnapshot.graphql | 8 + .../Marten/test/Directory.Build.props | 1 + .../MongoDb/test/Directory.Build.props | 1 + .../OpenApi/test/Directory.Build.props | 1 + .../test/Directory.Build.props | 1 + .../Raven/test/Directory.Build.props | 1 + .../Spatial/test/Directory.Build.props | 1 + 46 files changed, 1014 insertions(+), 116 deletions(-) create mode 100644 src/CookieCrumble/src/CookieCrumble.HotChocolate.Language/Extensions/SnapshotExtensions.cs delete mode 100644 src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/GraphQLSnapshotValueFormatter.cs create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/SyntaxWriterTests.cs create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ArgumentsWithDirectives_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ComplexSchemaDefinition_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.DirectiveDefinition_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.EnumTypeDefinition_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.EnumValueWithDirectives_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.FieldArgument_WithObjectValue_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.FragmentWithDirectives_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InlineFragmentWithDirectives_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InputObjectTypeDefinition_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InterfaceTypeDefinition_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ListValue_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ListValue_NotIndented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectTypeDefinition_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectValue_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectValue_NotIndented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.QueryWithDirectivesOnField_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithDirectives_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithManyDirectives_NoWrapping_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithManyDirectives_WithWrapping_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.UnionTypeDefinition_Indented_MatchesSnapshot.graphql create mode 100644 src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.VariableDefinition_WithDefaultObjectValue_Indented_MatchesSnapshot.graphql diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate.Language/Extensions/SnapshotExtensions.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate.Language/Extensions/SnapshotExtensions.cs new file mode 100644 index 00000000000..f79b81ada65 --- /dev/null +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate.Language/Extensions/SnapshotExtensions.cs @@ -0,0 +1,16 @@ +using HotChocolate.Language; +using SnapshotValueFormatters = CookieCrumble.HotChocolate.Language.Formatters.SnapshotValueFormatters; + +namespace CookieCrumble.HotChocolate; + +public static class SnapshotExtensions +{ + public static void MatchSnapshot( + this ISyntaxNode? value, + string? postFix = null) + => Snapshot.Match( + value, + postFix, + extension: ".graphql", + formatter: SnapshotValueFormatters.GraphQLSyntaxNode); +} diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate.Language/Formatters/GraphQLSnapshotValueFormatter.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate.Language/Formatters/GraphQLSnapshotValueFormatter.cs index 80686dbfe9e..31e7d0dc9f0 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate.Language/Formatters/GraphQLSnapshotValueFormatter.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate.Language/Formatters/GraphQLSnapshotValueFormatter.cs @@ -9,7 +9,7 @@ internal sealed class GraphQLSyntaxNodeSnapshotValueFormatter : SnapshotValueFor { protected override void Format(IBufferWriter snapshot, ISyntaxNode value) { - var serialized = value.Print().AsSpan(); + var serialized = value.Print(indented: true).AsSpan(); var buffer = ArrayPool.Shared.Rent(serialized.Length); var span = buffer.AsSpan()[..serialized.Length]; var written = 0; diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/CookieCrumbleHotChocolate.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/CookieCrumbleHotChocolate.cs index 0f019587d48..f205603f78d 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/CookieCrumbleHotChocolate.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/CookieCrumbleHotChocolate.cs @@ -8,7 +8,6 @@ public sealed class CookieCrumbleHotChocolate : SnapshotModule protected override IEnumerable CreateFormatters() { yield return SnapshotValueFormatters.ExecutionResult; - yield return SnapshotValueFormatters.GraphQL; yield return SnapshotValueFormatters.GraphQLHttp; yield return SnapshotValueFormatters.OperationResult; yield return SnapshotValueFormatters.Schema; diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Extensions/SnapshotExtensions.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Extensions/SnapshotExtensions.cs index a993dfe6dc1..238b1ddb904 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Extensions/SnapshotExtensions.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Extensions/SnapshotExtensions.cs @@ -1,22 +1,12 @@ using CookieCrumble.HotChocolate.Formatters; using HotChocolate; using HotChocolate.Execution; -using HotChocolate.Language; using CoreFormatters = CookieCrumble.Formatters.SnapshotValueFormatters; namespace CookieCrumble.HotChocolate; public static class SnapshotExtensions { - public static void MatchSnapshot( - this ISyntaxNode? value, - string? postFix = null) - => Snapshot.Match( - value, - postFix, - extension: ".graphql", - formatter: SnapshotValueFormatters.GraphQL); - public static void MatchSnapshot( this ISchemaDefinition? value, string? postFix = null) diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/GraphQLSnapshotValueFormatter.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/GraphQLSnapshotValueFormatter.cs deleted file mode 100644 index 18f7a3b5ad5..00000000000 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/GraphQLSnapshotValueFormatter.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Buffers; -using CookieCrumble.Formatters; -using HotChocolate.Language; -using HotChocolate.Language.Utilities; - -namespace CookieCrumble.HotChocolate.Formatters; - -internal sealed class GraphQLSnapshotValueFormatter : SnapshotValueFormatter -{ - protected override void Format(IBufferWriter snapshot, ISyntaxNode value) - { - var serialized = value.Print().AsSpan(); - var buffer = ArrayPool.Shared.Rent(serialized.Length); - var span = buffer.AsSpan()[..serialized.Length]; - var written = 0; - - for (var i = 0; i < serialized.Length; i++) - { - if (serialized[i] is not '\r') - { - span[written++] = serialized[i]; - } - } - - span = span[..written]; - snapshot.Append(span); - - ArrayPool.Shared.Return(buffer); - } - - protected override void FormatMarkdown(IBufferWriter snapshot, ISyntaxNode value) - { - snapshot.Append("```graphql"); - snapshot.AppendLine(); - Format(snapshot, value); - snapshot.AppendLine(); - snapshot.Append("```"); - snapshot.AppendLine(); - } -} diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/SnapshotValueFormatters.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/SnapshotValueFormatters.cs index b65a6532e60..4b4f9e91892 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/SnapshotValueFormatters.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/SnapshotValueFormatters.cs @@ -10,9 +10,6 @@ public static class SnapshotValueFormatters public static ISnapshotValueFormatter ExecutionResult { get; } = new ExecutionResultSnapshotValueFormatter(); - public static ISnapshotValueFormatter GraphQL { get; } = - new GraphQLSnapshotValueFormatter(); - public static ISnapshotValueFormatter GraphQLHttp { get; } = new GraphQLHttpResponseFormatter(); diff --git a/src/HotChocolate/ApolloFederation/test/Directory.Build.props b/src/HotChocolate/ApolloFederation/test/Directory.Build.props index 7b5e79f43f8..0433866e225 100644 --- a/src/HotChocolate/ApolloFederation/test/Directory.Build.props +++ b/src/HotChocolate/ApolloFederation/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/AspNetCore/test/Directory.Build.props b/src/HotChocolate/AspNetCore/test/Directory.Build.props index b62497b326e..3dd14ff76c9 100644 --- a/src/HotChocolate/AspNetCore/test/Directory.Build.props +++ b/src/HotChocolate/AspNetCore/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/Caching/test/Directory.Build.props b/src/HotChocolate/Caching/test/Directory.Build.props index 5b3630fd7db..24fb7f78e32 100644 --- a/src/HotChocolate/Caching/test/Directory.Build.props +++ b/src/HotChocolate/Caching/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/Core/test/Directory.Build.props b/src/HotChocolate/Core/test/Directory.Build.props index 6ae29fd74ba..103bc78ee36 100644 --- a/src/HotChocolate/Core/test/Directory.Build.props +++ b/src/HotChocolate/Core/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs index f6bc46247b0..f0ce7e7c9d8 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs @@ -5,7 +5,6 @@ using HotChocolate.StarWars.Models; using HotChocolate.StarWars.Types; using HotChocolate.Types; -using Moq; namespace HotChocolate.Execution.Processing; @@ -26,12 +25,12 @@ public void VariableCoercionHelper_Schema_Is_Null() }; var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act void Action() => helper.CoerceVariableValues( - null!, variableDefinitions, default, coercedValues, featureProvider.Object); + null!, variableDefinitions, default, coercedValues, featureProvider); // assert Assert.Throws(Action); @@ -43,12 +42,12 @@ public void VariableCoercionHelper_VariableDefinitions_Is_Null() // arrange var schema = SchemaBuilder.New().AddStarWarsTypes().Create(); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act void Action() - => helper.CoerceVariableValues(schema, null!, default, coercedValues, featureProvider.Object); + => helper.CoerceVariableValues(schema, null!, default, coercedValues, featureProvider); // assert Assert.Throws(Action); @@ -71,12 +70,12 @@ public void VariableCoercionHelper_CoercedValues_Is_Null() Array.Empty()) }; - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act void Action() => helper.CoerceVariableValues( - schema, variableDefinitions, default, null!, featureProvider.Object); + schema, variableDefinitions, default, null!, featureProvider); // assert Assert.Throws(Action); @@ -100,12 +99,12 @@ public void Coerce_Nullable_String_Variable_With_Default_Where_Value_Is_Not_Prov }; var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, default, coercedValues, featureProvider.Object); + schema, variableDefinitions, default, coercedValues, featureProvider); // assert Assert.Collection(coercedValues, @@ -136,12 +135,12 @@ public void Coerce_Nullable_String_Variable_Where_Value_Is_Not_Provided() }; var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, default, coercedValues, featureProvider.Object); + schema, variableDefinitions, default, coercedValues, featureProvider); // assert Assert.Empty(coercedValues); @@ -166,12 +165,12 @@ public void Coerce_Nullable_String_Variable_With_Default_Where_Value_Is_Provided var variableValues = JsonDocument.Parse("""{"abc": "xyz"}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert Assert.Collection(coercedValues, @@ -203,12 +202,12 @@ public void Coerce_Nullable_String_Variable_With_Default_Where_Plain_Value_Is_Pr var variableValues = JsonDocument.Parse("""{"abc": "xyz"}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert Assert.Collection(coercedValues, @@ -240,12 +239,12 @@ public void Coerce_Nullable_String_Variable_With_Default_Where_Null_Is_Provided( var variableValues = JsonDocument.Parse("""{"abc": null}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert Assert.Collection(coercedValues, @@ -277,12 +276,16 @@ public void Coerce_Nullable_ReviewInput_Variable_With_Object() var variableValues = JsonDocument.Parse("""{"abc": {"stars": 5}}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, + variableDefinitions, + variableValues.RootElement, + coercedValues, + featureProvider); // assert Assert.Collection(coercedValues, @@ -314,12 +317,12 @@ public void Error_When_Value_Is_Null_On_Non_Null_Variable() var variableValues = JsonDocument.Parse("""{"abc": null}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act void Action() => helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert Assert.Throws(Action) @@ -358,12 +361,12 @@ public void Error_When_Value_Type_Does_Not_Match_Variable_Type() var variableValues = JsonDocument.Parse("""{"abc": 1}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act void Action() => helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert Assert.Throws(Action) @@ -403,12 +406,12 @@ public void Variable_Type_Is_Not_An_Input_Type() var variableValues = JsonDocument.Parse("""{"abc": 1}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act void Action() => helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert Assert.Throws(Action).Errors.MatchInlineSnapshot( @@ -443,12 +446,12 @@ public void Error_When_Input_Field_Has_Different_Properties_Than_Defined() var variableValues = JsonDocument.Parse("""{"abc": {"abc": "def"}}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act void Action() => helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert Assert.Throws(Action) @@ -515,12 +518,12 @@ enum TestEnum { """); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert var entry = Assert.Single(coercedValues); @@ -581,12 +584,12 @@ enum TestEnum { """); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert var entry = Assert.Single(coercedValues); @@ -645,12 +648,12 @@ enum TestEnum { """); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert var entry = Assert.Single(coercedValues); @@ -705,12 +708,12 @@ enum TestEnum { """); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert var entry = Assert.Single(coercedValues); @@ -765,12 +768,12 @@ enum TestEnum { """); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert var entry = Assert.Single(coercedValues); @@ -832,7 +835,7 @@ enum TestEnum { """); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); @@ -842,7 +845,7 @@ enum TestEnum { variableDefinitions, variableValues.RootElement, coercedValues, - featureProvider.Object); + featureProvider); // assert var entry = Assert.Single(coercedValues); @@ -877,7 +880,7 @@ public void Variable_Is_Nullable_And_Not_Set() }; var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); @@ -887,9 +890,14 @@ public void Variable_Is_Nullable_And_Not_Set() variableDefinitions, default, coercedValues, - featureProvider.Object); + featureProvider); // assert Assert.Empty(coercedValues); } + + private class MockFeatureProvider : IFeatureProvider + { + public IFeatureCollection Features { get; } = new FeatureCollection(); + } } diff --git a/src/HotChocolate/CostAnalysis/test/Directory.Build.props b/src/HotChocolate/CostAnalysis/test/Directory.Build.props index 33277d1d22c..bf9ede89e6c 100644 --- a/src/HotChocolate/CostAnalysis/test/Directory.Build.props +++ b/src/HotChocolate/CostAnalysis/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/Data/test/Directory.Build.props b/src/HotChocolate/Data/test/Directory.Build.props index 4516f1b3c88..aba8188c0cf 100644 --- a/src/HotChocolate/Data/test/Directory.Build.props +++ b/src/HotChocolate/Data/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj index 517b93269ea..e78a96ef5cc 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.QuerySyntax.cs b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.QuerySyntax.cs index 8254a3bac83..29c4ee3c314 100644 --- a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.QuerySyntax.cs +++ b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.QuerySyntax.cs @@ -75,7 +75,7 @@ private void VisitVariableDefinition(VariableDefinitionNode node, ISyntaxWriter if (node.DefaultValue is not null) { writer.Write(" = "); - writer.WriteValue(node.DefaultValue); + writer.WriteValue(node.DefaultValue, _indented); } WriteDirectives(node.Directives, writer); @@ -143,6 +143,7 @@ private void VisitSelectionSet(SelectionSetNode node, ISyntaxWriter writer) { writer.WriteLine(); writer.Indent(); + writer.WriteIndent(); separator = Environment.NewLine; } else @@ -187,8 +188,6 @@ private void VisitSelection(ISelectionNode node, ISyntaxWriter context) private void VisitField(FieldNode node, ISyntaxWriter writer) { - writer.WriteIndent(); - if (node.Alias is not null) { writer.WriteName(node.Alias); @@ -215,8 +214,6 @@ private void VisitField(FieldNode node, ISyntaxWriter writer) private void VisitFragmentSpread(FragmentSpreadNode node, ISyntaxWriter writer) { - writer.WriteIndent(); - writer.Write("... "); writer.WriteName(node.Name); @@ -225,8 +222,6 @@ private void VisitFragmentSpread(FragmentSpreadNode node, ISyntaxWriter writer) private void VisitInlineFragment(InlineFragmentNode node, ISyntaxWriter writer) { - writer.WriteIndent(); - writer.Write("..."); if (node.TypeCondition is not null) diff --git a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.SchemaSyntax.cs b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.SchemaSyntax.cs index 88394d76e47..a5148b4f433 100644 --- a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.SchemaSyntax.cs +++ b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.SchemaSyntax.cs @@ -403,7 +403,7 @@ private void WriteInputValueDefinition( writer.WriteSpace(); writer.Write("="); writer.WriteSpace(); - writer.WriteValue(value); + writer.WriteValue(value, _indented); } WriteDirectives(node.Directives, writer); diff --git a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.cs b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.cs index ce4a02627ad..28203897c12 100644 --- a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.cs +++ b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.cs @@ -67,10 +67,10 @@ public void Serialize(ISyntaxNode node, ISyntaxWriter writer) case SyntaxKind.IntValue: case SyntaxKind.NullValue: case SyntaxKind.StringValue: - writer.WriteValue((IValueNode)node); + writer.WriteValue((IValueNode)node, _indented); break; case SyntaxKind.ObjectField: - writer.WriteObjectField((ObjectFieldNode)node); + writer.WriteObjectField((ObjectFieldNode)node, _indented); break; case SyntaxKind.SchemaDefinition: VisitSchemaDefinition((SchemaDefinitionNode)node, writer); diff --git a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxWriterExtensions.cs b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxWriterExtensions.cs index 4b8d8269d01..26344afd460 100644 --- a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxWriterExtensions.cs +++ b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxWriterExtensions.cs @@ -25,11 +25,17 @@ public static void WriteMany( { if (items.Count > 0) { + var hasNewLine = separator.IndexOf('\n') >= 0 || separator.IndexOf('\r') >= 0; + action(items[0], writer); for (var i = 1; i < items.Count; i++) { writer.Write(separator); + if (hasNewLine) + { + writer.WriteIndent(); + } action(items[i], writer); } } @@ -56,6 +62,14 @@ public static void WriteMany( public static void WriteValue( this ISyntaxWriter writer, IValueNode? node) + { + WriteValue(writer, node, indented: false); + } + + public static void WriteValue( + this ISyntaxWriter writer, + IValueNode? node, + bool indented) { if (node is null) { @@ -103,11 +117,11 @@ public static void WriteValue( break; case SyntaxKind.ListValue: - WriteListValue(writer, (ListValueNode)node); + WriteListValue(writer, (ListValueNode)node, indented); break; case SyntaxKind.ObjectValue: - WriteObjectValue(writer, (ObjectValueNode)node); + WriteObjectValue(writer, (ObjectValueNode)node, indented); break; case SyntaxKind.Variable: @@ -247,21 +261,70 @@ public static void WriteNullValue(this ISyntaxWriter writer) public static void WriteListValue(this ISyntaxWriter writer, ListValueNode node) { - writer.Write("[ "); - writer.WriteMany(node.Items, (n, w) => w.WriteValue(n)); - writer.Write(" ]"); + WriteListValue(writer, node, indented: false); + } + + public static void WriteListValue(this ISyntaxWriter writer, ListValueNode node, bool indented) + { + writer.Write('['); + + if (indented && node.Items.Count > 0) + { + writer.WriteLine(); + writer.Indent(); + writer.WriteIndent(); + writer.WriteMany(node.Items, (n, w) => w.WriteValue(n, indented), "," + Environment.NewLine); + writer.WriteLine(); + writer.Unindent(); + writer.WriteIndent(); + } + else + { + writer.WriteSpace(); + writer.WriteMany(node.Items, (n, w) => w.WriteValue(n, indented)); + writer.WriteSpace(); + } + + writer.Write(']'); } public static void WriteObjectValue(this ISyntaxWriter writer, ObjectValueNode node) { - writer.Write("{ "); - writer.WriteMany(node.Fields, (n, w) => w.WriteObjectField(n)); - writer.Write(" }"); + WriteObjectValue(writer, node, indented: false); + } + + public static void WriteObjectValue(this ISyntaxWriter writer, ObjectValueNode node, bool indented) + { + writer.Write('{'); + + if (indented && node.Fields.Count > 0) + { + writer.WriteLine(); + writer.Indent(); + writer.WriteIndent(); + writer.WriteMany(node.Fields, (n, w) => w.WriteObjectField(n, indented), "," + Environment.NewLine); + writer.WriteLine(); + writer.Unindent(); + writer.WriteIndent(); + } + else + { + writer.WriteSpace(); + writer.WriteMany(node.Fields, (n, w) => w.WriteObjectField(n, indented)); + writer.WriteSpace(); + } + + writer.Write('}'); } public static void WriteObjectField(this ISyntaxWriter writer, ObjectFieldNode node) { - writer.WriteField(node.Name, node.Value); + WriteObjectField(writer, node, indented: false); + } + + public static void WriteObjectField(this ISyntaxWriter writer, ObjectFieldNode node, bool indented) + { + writer.WriteField(node.Name, node.Value, indented); } public static void WriteVariable(this ISyntaxWriter writer, VariableNode node) @@ -271,10 +334,15 @@ public static void WriteVariable(this ISyntaxWriter writer, VariableNode node) } public static void WriteField(this ISyntaxWriter writer, NameNode name, IValueNode value) + { + WriteField(writer, name, value, indented: false); + } + + public static void WriteField(this ISyntaxWriter writer, NameNode name, IValueNode value, bool indented) { writer.Write(name.Value); writer.Write(": "); - writer.WriteValue(value); + writer.WriteValue(value, indented); } public static void WriteArgument(this ISyntaxWriter writer, ArgumentNode node) diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/SyntaxWriterTests.cs b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/SyntaxWriterTests.cs new file mode 100644 index 00000000000..97e26effc3a --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/SyntaxWriterTests.cs @@ -0,0 +1,719 @@ +using HotChocolate.Language.Utilities; + +namespace HotChocolate.Language.SyntaxTree; + +public class SyntaxWriterTests +{ + [Fact] + public void WriteMany_WithNewlineSeparator_WritesIndentation() + { + // arrange + var writer = new StringSyntaxWriter(); + writer.Indent(); + var items = new[] { "item1", "item2", "item3" }; + + // act + writer.WriteMany( + items, + (item, w) => w.Write(item), + Environment.NewLine); + + var result = writer.ToString(); + + // assert + var expected = $"item1{Environment.NewLine} item2{Environment.NewLine} item3"; + Assert.Equal(expected, result); + } + + [Fact] + public void WriteMany_WithCommaSeparator_DoesNotWriteIndentation() + { + // arrange + var writer = new StringSyntaxWriter(); + writer.Indent(); + var items = new[] { "item1", "item2", "item3" }; + + // act + writer.WriteMany( + items, + (item, w) => w.Write(item), + ", "); + + var result = writer.ToString(); + + // assert + Assert.Equal("item1, item2, item3", result); + } + + [Fact] + public void WriteMany_WithMultipleIndentLevels_WritesCorrectIndentation() + { + // arrange + var writer = new StringSyntaxWriter(); + writer.Indent(); + writer.Indent(); + var items = new[] { "a", "b", "c" }; + + // act + writer.WriteMany( + items, + (item, w) => w.Write(item), + Environment.NewLine); + + var result = writer.ToString(); + + // assert + var expected = $"a{Environment.NewLine} b{Environment.NewLine} c"; + Assert.Equal(expected, result); + } + + [Fact] + public void WriteObjectValue_WithIndentation_FormatsCorrectly() + { + // arrange + var writer = new StringSyntaxWriter(); + var objectValue = new ObjectValueNode( + new ObjectFieldNode("field1", "value1"), + new ObjectFieldNode("field2", "value2"), + new ObjectFieldNode("field3", "value3")); + + // act + writer.WriteObjectValue(objectValue); + var result = writer.ToString(); + + // assert + Assert.Equal("{ field1: \"value1\", field2: \"value2\", field3: \"value3\" }", result); + } + + [Fact] + public void WriteListValue_WithIndentation_FormatsCorrectly() + { + // arrange + var writer = new StringSyntaxWriter(); + var listValue = new ListValueNode( + new IntValueNode(1), + new IntValueNode(2), + new IntValueNode(3)); + + // act + writer.WriteListValue(listValue); + var result = writer.ToString(); + + // assert + Assert.Equal("[ 1, 2, 3 ]", result); + } + + [Fact] + public void WriteMany_WithEmptyList_WritesNothing() + { + // arrange + var writer = new StringSyntaxWriter(); + var items = Array.Empty(); + + // act + writer.WriteMany( + items, + (item, w) => w.Write(item), + ", "); + + var result = writer.ToString(); + + // assert + Assert.Equal(string.Empty, result); + } + + [Fact] + public void WriteMany_WithSingleItem_DoesNotWriteSeparator() + { + // arrange + var writer = new StringSyntaxWriter(); + writer.Indent(); + var items = new[] { "single" }; + + // act + writer.WriteMany( + items, + (item, w) => w.Write(item), + Environment.NewLine); + + var result = writer.ToString(); + + // assert + Assert.Equal("single", result); + } + + [Fact] + public void SyntaxSerializer_WithIndentation_FormatsCorrectly() + { + // arrange + const string query = + """ + query GetUser($id: ID!) { + user(id: $id) { + name + email + } + } + """; + + var document = Utf8GraphQLParser.Parse(query); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + var result = writer.ToString(); + + // assert + // The result should have proper indentation with each field on its own line + Assert.Contains($"query GetUser({Environment.NewLine}", result); + Assert.Contains($"{Environment.NewLine} $id: ID!{Environment.NewLine}", result); + Assert.Contains($" user(id: $id) {{{Environment.NewLine}", result); + Assert.Contains($" name{Environment.NewLine}", result); + Assert.Contains($" email{Environment.NewLine}", result); + } + + [Fact] + public void WriteMany_WithCarriageReturnNewLine_WritesIndentation() + { + // arrange + var writer = new StringSyntaxWriter(); + writer.Indent(); + var items = new[] { "a", "b", "c" }; + + // act + writer.WriteMany( + items, + (item, w) => w.Write(item), + "\r\n"); + + var result = writer.ToString(); + + // assert + const string expected = "a\r\n b\r\n c"; + Assert.Equal(expected, result); + } + + [Fact] + public void WriteMany_WithOnlyCarriageReturn_WritesIndentation() + { + // arrange + var writer = new StringSyntaxWriter(); + writer.Indent(); + var items = new[] { "x", "y" }; + + // act + writer.WriteMany( + items, + (item, w) => w.Write(item), + "\r"); + + var result = writer.ToString(); + + // assert + const string expected = "x\r y"; + Assert.Equal(expected, result); + } + + [Fact] + public void ObjectValue_Indented_MatchesSnapshot() + { + // arrange + var objectValue = new ObjectValueNode( + new ObjectFieldNode("enum", new EnumValueNode("Foo")), + new ObjectFieldNode("enum2", new EnumValueNode("Bar")), + new ObjectFieldNode("nested", new ObjectValueNode( + new ObjectFieldNode("inner", "value")))); + + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(objectValue, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void ObjectValue_NotIndented_MatchesSnapshot() + { + // arrange + var objectValue = new ObjectValueNode( + new ObjectFieldNode("enum", new EnumValueNode("Foo")), + new ObjectFieldNode("enum2", new EnumValueNode("Bar")), + new ObjectFieldNode("nested", new ObjectValueNode( + new ObjectFieldNode("inner", "value")))); + + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = false }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(objectValue, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void ListValue_Indented_MatchesSnapshot() + { + // arrange + var listValue = new ListValueNode( + new ObjectValueNode( + new ObjectFieldNode("a", 1), + new ObjectFieldNode("b", 2)), + new ObjectValueNode( + new ObjectFieldNode("c", 3), + new ObjectFieldNode("d", 4)), + new IntValueNode(5)); + + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(listValue, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void ListValue_NotIndented_MatchesSnapshot() + { + // arrange + var listValue = new ListValueNode( + new ObjectValueNode( + new ObjectFieldNode("a", 1), + new ObjectFieldNode("b", 2)), + new ObjectValueNode( + new ObjectFieldNode("c", 3), + new ObjectFieldNode("d", 4)), + new IntValueNode(5)); + + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = false }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(listValue, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void VariableDefinition_WithDefaultObjectValue_Indented_MatchesSnapshot() + { + // arrange + const string query = """ + query GetUser($input: InputType = { field1: "value1", field2: "value2" }) { + user + } + """; + + var document = Utf8GraphQLParser.Parse(query); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void FieldArgument_WithObjectValue_Indented_MatchesSnapshot() + { + // arrange + const string query = """ + query { + user(filter: { name: "John", age: 30, active: true }) + } + """; + + var document = Utf8GraphQLParser.Parse(query); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void ObjectTypeDefinition_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + type User { + id: ID! + name: String! + email: String + posts: [Post!]! + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void InterfaceTypeDefinition_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + interface Node { + id: ID! + } + + type User implements Node { + id: ID! + name: String! + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void InputObjectTypeDefinition_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + input UserFilter { + name: String + age: Int + active: Boolean = true + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void EnumTypeDefinition_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + enum Role { + ADMIN + USER + GUEST + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void UnionTypeDefinition_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + union SearchResult = User | Post | Comment + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void TypeWithDirectives_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + type User @auth(requires: ADMIN) { + id: ID! @deprecated(reason: "Use uid instead") + uid: ID! + name: String! + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void ComplexSchemaDefinition_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + schema { + query: Query + mutation: Mutation + } + + type Query { + user(id: ID!): User + users(filter: UserFilter): [User!]! + } + + type Mutation { + createUser(input: CreateUserInput!): User! + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void QueryWithDirectivesOnField_Indented_MatchesSnapshot() + { + // arrange + const string query = """ + query { + user(id: "123") { + id @include(if: true) + name @skip(if: false) @deprecated + email @custom(arg1: "value1", arg2: 42) + } + } + """; + + var document = Utf8GraphQLParser.Parse(query); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void FragmentWithDirectives_Indented_MatchesSnapshot() + { + // arrange + const string query = """ + fragment UserFields on User @custom(value: "test") { + id + name @include(if: true) + email + } + + query { + user { + ...UserFields @defer + } + } + """; + + var document = Utf8GraphQLParser.Parse(query); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void InlineFragmentWithDirectives_Indented_MatchesSnapshot() + { + // arrange + const string query = """ + query { + search { + ... on User @defer { + id + name + } + ... on Post @defer @stream { + title + content + } + } + } + """; + + var document = Utf8GraphQLParser.Parse(query); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void TypeWithManyDirectives_NoWrapping_MatchesSnapshot() + { + // arrange + const string schema = """ + type User @auth @cache @log @validate @track { + id: ID! + name: String! @deprecated @private @readonly @indexed @unique + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void TypeWithManyDirectives_WithWrapping_MatchesSnapshot() + { + // arrange + const string schema = """ + type User @auth @cache @log @validate @track { + id: ID! + name: String! @deprecated @private @readonly @indexed @unique + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions + { + Indented = true, + MaxDirectivesPerLine = 2 + }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void DirectiveDefinition_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + directive @auth( + requires: Role = ADMIN + scopes: [String!] + ) repeatable on OBJECT | FIELD_DEFINITION + + directive @deprecated( + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ENUM_VALUE + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void ArgumentsWithDirectives_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + type Query { + user( + id: ID! @deprecated + filter: UserFilter @custom(value: "test") + ): User + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void EnumValueWithDirectives_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + enum Role { + ADMIN @description(text: "Administrator role") + USER @description(text: "Regular user") + GUEST @deprecated(reason: "Use USER instead") @internal + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ArgumentsWithDirectives_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ArgumentsWithDirectives_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..14592fae4fd --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ArgumentsWithDirectives_Indented_MatchesSnapshot.graphql @@ -0,0 +1,3 @@ +type Query { + user(id: ID! @deprecated filter: UserFilter @custom(value: "test")): User +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ComplexSchemaDefinition_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ComplexSchemaDefinition_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..65543f9e87e --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ComplexSchemaDefinition_Indented_MatchesSnapshot.graphql @@ -0,0 +1,13 @@ +schema { + query: Query + mutation: Mutation +} + +type Query { + user(id: ID!): User + users(filter: UserFilter): [User!]! +} + +type Mutation { + createUser(input: CreateUserInput!): User! +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.DirectiveDefinition_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.DirectiveDefinition_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..f8d7f1fe43f --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.DirectiveDefinition_Indented_MatchesSnapshot.graphql @@ -0,0 +1,3 @@ +directive @auth(requires: Role = ADMIN scopes: [String!]) repeatable on OBJECT | FIELD_DEFINITION + +directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.EnumTypeDefinition_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.EnumTypeDefinition_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..1b5fe4b33ee --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.EnumTypeDefinition_Indented_MatchesSnapshot.graphql @@ -0,0 +1,5 @@ +enum Role { + ADMIN + USER + GUEST +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.EnumValueWithDirectives_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.EnumValueWithDirectives_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..b7f0867ea5d --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.EnumValueWithDirectives_Indented_MatchesSnapshot.graphql @@ -0,0 +1,5 @@ +enum Role { + ADMIN @description(text: "Administrator role") + USER @description(text: "Regular user") + GUEST @deprecated(reason: "Use USER instead") @internal +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.FieldArgument_WithObjectValue_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.FieldArgument_WithObjectValue_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..718f9d252f2 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.FieldArgument_WithObjectValue_Indented_MatchesSnapshot.graphql @@ -0,0 +1,3 @@ +{ + user(filter: { name: "John", age: 30, active: true }) +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.FragmentWithDirectives_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.FragmentWithDirectives_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..683bff27555 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.FragmentWithDirectives_Indented_MatchesSnapshot.graphql @@ -0,0 +1,11 @@ +fragment UserFields on User @custom(value: "test") { + id + name @include(if: true) + email +} + +{ + user { + ... UserFields @defer + } +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InlineFragmentWithDirectives_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InlineFragmentWithDirectives_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..98073d65e6d --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InlineFragmentWithDirectives_Indented_MatchesSnapshot.graphql @@ -0,0 +1,12 @@ +{ + search { + ... on User @defer { + id + name + } + ... on Post @defer @stream { + title + content + } + } +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InputObjectTypeDefinition_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InputObjectTypeDefinition_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..8529ff41298 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InputObjectTypeDefinition_Indented_MatchesSnapshot.graphql @@ -0,0 +1,5 @@ +input UserFilter { + name: String + age: Int + active: Boolean = true +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InterfaceTypeDefinition_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InterfaceTypeDefinition_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..2b12f825d69 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InterfaceTypeDefinition_Indented_MatchesSnapshot.graphql @@ -0,0 +1,8 @@ +interface Node { + id: ID! +} + +type User implements Node { + id: ID! + name: String! +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ListValue_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ListValue_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..7d18f9284f5 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ListValue_Indented_MatchesSnapshot.graphql @@ -0,0 +1,11 @@ +[ + { + a: 1, + b: 2 + }, + { + c: 3, + d: 4 + }, + 5 +] diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ListValue_NotIndented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ListValue_NotIndented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..96d0bfe9a56 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ListValue_NotIndented_MatchesSnapshot.graphql @@ -0,0 +1 @@ +[ { a: 1, b: 2 }, { c: 3, d: 4 }, 5 ] diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectTypeDefinition_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectTypeDefinition_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..0bf7783cc5e --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectTypeDefinition_Indented_MatchesSnapshot.graphql @@ -0,0 +1,6 @@ +type User { + id: ID! + name: String! + email: String + posts: [Post!]! +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectValue_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectValue_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..c9606b60719 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectValue_Indented_MatchesSnapshot.graphql @@ -0,0 +1,7 @@ +{ + enum: Foo, + enum2: Bar, + nested: { + inner: "value" + } +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectValue_NotIndented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectValue_NotIndented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..07b2989cbed --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectValue_NotIndented_MatchesSnapshot.graphql @@ -0,0 +1 @@ +{ enum: Foo, enum2: Bar, nested: { inner: "value" } } diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.QueryWithDirectivesOnField_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.QueryWithDirectivesOnField_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..bae31149913 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.QueryWithDirectivesOnField_Indented_MatchesSnapshot.graphql @@ -0,0 +1,7 @@ +{ + user(id: "123") { + id @include(if: true) + name @skip(if: false) @deprecated + email @custom(arg1: "value1", arg2: 42) + } +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithDirectives_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithDirectives_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..fd8af41a4a3 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithDirectives_Indented_MatchesSnapshot.graphql @@ -0,0 +1,5 @@ +type User @auth(requires: ADMIN) { + id: ID! @deprecated(reason: "Use uid instead") + uid: ID! + name: String! +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithManyDirectives_NoWrapping_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithManyDirectives_NoWrapping_MatchesSnapshot.graphql new file mode 100644 index 00000000000..02fa83dd3c6 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithManyDirectives_NoWrapping_MatchesSnapshot.graphql @@ -0,0 +1,4 @@ +type User @auth @cache @log @validate @track { + id: ID! + name: String! @deprecated @private @readonly @indexed @unique +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithManyDirectives_WithWrapping_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithManyDirectives_WithWrapping_MatchesSnapshot.graphql new file mode 100644 index 00000000000..5301dcb1182 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithManyDirectives_WithWrapping_MatchesSnapshot.graphql @@ -0,0 +1,14 @@ +type User + @auth + @cache + @log + @validate + @track { + id: ID! + name: String! + @deprecated + @private + @readonly + @indexed + @unique +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.UnionTypeDefinition_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.UnionTypeDefinition_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..a45f161ccf1 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.UnionTypeDefinition_Indented_MatchesSnapshot.graphql @@ -0,0 +1 @@ +union SearchResult = User | Post | Comment diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.VariableDefinition_WithDefaultObjectValue_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.VariableDefinition_WithDefaultObjectValue_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..22a25d187ea --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.VariableDefinition_WithDefaultObjectValue_Indented_MatchesSnapshot.graphql @@ -0,0 +1,8 @@ +query GetUser( + $input: InputType = { + field1: "value1", + field2: "value2" + } +) { + user +} diff --git a/src/HotChocolate/Marten/test/Directory.Build.props b/src/HotChocolate/Marten/test/Directory.Build.props index 7b5e79f43f8..0433866e225 100644 --- a/src/HotChocolate/Marten/test/Directory.Build.props +++ b/src/HotChocolate/Marten/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/MongoDb/test/Directory.Build.props b/src/HotChocolate/MongoDb/test/Directory.Build.props index 7b5e79f43f8..0433866e225 100644 --- a/src/HotChocolate/MongoDb/test/Directory.Build.props +++ b/src/HotChocolate/MongoDb/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/OpenApi/test/Directory.Build.props b/src/HotChocolate/OpenApi/test/Directory.Build.props index 5b3630fd7db..24fb7f78e32 100644 --- a/src/HotChocolate/OpenApi/test/Directory.Build.props +++ b/src/HotChocolate/OpenApi/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/PersistedOperations/test/Directory.Build.props b/src/HotChocolate/PersistedOperations/test/Directory.Build.props index 7b5e79f43f8..0433866e225 100644 --- a/src/HotChocolate/PersistedOperations/test/Directory.Build.props +++ b/src/HotChocolate/PersistedOperations/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/Raven/test/Directory.Build.props b/src/HotChocolate/Raven/test/Directory.Build.props index 7b5e79f43f8..0433866e225 100644 --- a/src/HotChocolate/Raven/test/Directory.Build.props +++ b/src/HotChocolate/Raven/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/Spatial/test/Directory.Build.props b/src/HotChocolate/Spatial/test/Directory.Build.props index 7b5e79f43f8..0433866e225 100644 --- a/src/HotChocolate/Spatial/test/Directory.Build.props +++ b/src/HotChocolate/Spatial/test/Directory.Build.props @@ -15,6 +15,7 @@ + From 5995da09e40a67eab859b7d39c067b4c511c0e74 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 12 Feb 2026 18:26:33 +0100 Subject: [PATCH 20/46] Fixed more tests --- .../CookieCrumbleHotChocolate.cs | 1 + .../ResultElementSnapshotValueFormatter.cs | 26 +++++++++ .../Formatters/SnapshotValueFormatters.cs | 3 + .../Processing/VariableCoercionHelperTests.cs | 57 +++++++++---------- 4 files changed, 58 insertions(+), 29 deletions(-) diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/CookieCrumbleHotChocolate.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/CookieCrumbleHotChocolate.cs index f205603f78d..d958cbb59b9 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/CookieCrumbleHotChocolate.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/CookieCrumbleHotChocolate.cs @@ -13,6 +13,7 @@ protected override IEnumerable CreateFormatters() yield return SnapshotValueFormatters.Schema; yield return SnapshotValueFormatters.SchemaError; yield return SnapshotValueFormatters.Error; + yield return SnapshotValueFormatters.ErrorList; yield return SnapshotValueFormatters.ResultElement; } } diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ResultElementSnapshotValueFormatter.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ResultElementSnapshotValueFormatter.cs index 12b7bca0c30..33b84c5140f 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ResultElementSnapshotValueFormatter.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ResultElementSnapshotValueFormatter.cs @@ -1,6 +1,11 @@ using System.Buffers; +using System.Text.Encodings.Web; +using System.Text.Json; using CookieCrumble.Formatters; +using HotChocolate; +using HotChocolate.Execution; using HotChocolate.Text.Json; +using static HotChocolate.Execution.JsonValueFormatter; namespace CookieCrumble.HotChocolate.Formatters; @@ -10,3 +15,24 @@ internal sealed class ResultElementSnapshotValueFormatter protected override void Format(IBufferWriter snapshot, ResultElement element) => element.WriteTo(snapshot, indented: true); } + +internal sealed class ErrorListSnapshotValueFormatter + : SnapshotValueFormatter> +{ + protected override void Format(IBufferWriter snapshot, IReadOnlyList value) + { + var writerOptions = new JsonWriterOptions + { + Indented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + var serializationOptions = new JsonSerializerOptions + { + WriteIndented = true + }; + + var writer = new JsonWriter(snapshot, writerOptions); + WriteErrors(writer, value, serializationOptions, JsonNullIgnoreCondition.None); + } +} diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/SnapshotValueFormatters.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/SnapshotValueFormatters.cs index 4b4f9e91892..d49e5c17482 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/SnapshotValueFormatters.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/SnapshotValueFormatters.cs @@ -27,4 +27,7 @@ public static class SnapshotValueFormatters public static ISnapshotValueFormatter ResultElement { get; } = new ResultElementSnapshotValueFormatter(); + + public static ISnapshotValueFormatter ErrorList { get; } = + new ErrorListSnapshotValueFormatter(); } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs index f0ce7e7c9d8..8ab6a019d5b 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs @@ -330,13 +330,15 @@ void Action() => helper.CoerceVariableValues( .ToList() .MatchInlineSnapshot( """ - [ + "errors": [ { - "Message": "Cannot accept null for non-nullable input.", - "Code": null, - "Path": null, - "Locations": null, - "Extensions": null + "message": "Cannot accept null for non-nullable input.", + "extensions": { + "code": "HC0018", + "inputPath": [ + "abc" + ] + } } ] """); @@ -374,15 +376,12 @@ void Action() => helper.CoerceVariableValues( .ToList() .MatchInlineSnapshot( """ - [ + "errors": [ { - "Message": "The value `1` is not compatible with the type `String`.", - "Code": null, - "Path": null, - "Locations": null, - "Extensions": { - "variable": "abc" - } + "message": "String cannot coerce the given value JSON element of type `Number` to a runtime value.", + "path": [ + "abc" + ] } ] """); @@ -416,13 +415,14 @@ void Action() => helper.CoerceVariableValues( // assert Assert.Throws(Action).Errors.MatchInlineSnapshot( """ - [ + "errors": [ { - "Message": "Variable `abc` has an invalid type `Human`.", - "Code": null, - "Path": null, - "Locations": null, - "Extensions": null + "message": "Variable `abc` is not an input type.", + "extensions": { + "code": "HC0017", + "variable": "abc", + "type": "Human" + } } ] """); @@ -459,16 +459,15 @@ void Action() => helper.CoerceVariableValues( .ToList() .MatchInlineSnapshot( """ - [ + "errors": [ { - "Message": "`stars` is a required field of `ReviewInput`.", - "Code": null, - "Path": null, - "Locations": null, - "Extensions": { - "field": "stars", - "type": "ReviewInput", - "variable": "abc" + "message": "The required input field `stars` is missing.", + "extensions": { + "inputPath": [ + "abc", + "stars" + ], + "field": "ReviewInput.stars" } } ] From 2eef5797334f4aed400a183177c4eee668839db9 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 12 Feb 2026 18:31:19 +0100 Subject: [PATCH 21/46] fixed tests --- .../Processing/WorkQueueTests.cs | 184 +++++++++++++++++- 1 file changed, 182 insertions(+), 2 deletions(-) diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/WorkQueueTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Processing/WorkQueueTests.cs index 3913762b06c..eef3426fe9c 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/WorkQueueTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/WorkQueueTests.cs @@ -123,7 +123,187 @@ public void New() Assert.True(queue.IsEmpty); } - public class MockExecutionTask : IExecutionTask + [Fact] + public void Enqueue_Deferred_Task() + { + // arrange + var queue = new WorkQueue(); + var task = new MockExecutionTask(isDeferred: true); + + // act + queue.Push(task); + + // assert + Assert.False(queue.HasRunningTasks); + Assert.False(queue.IsEmpty); + } + + [Fact] + public void Immediate_Tasks_Take_Priority_Over_Deferred() + { + // arrange + var queue = new WorkQueue(); + var deferredTask = new MockExecutionTask(isDeferred: true); + var immediateTask = new MockExecutionTask(isDeferred: false); + + // Push deferred task first + queue.Push(deferredTask); + // Then push immediate task + queue.Push(immediateTask); + + // act + queue.TryTake(out var firstTask); + queue.TryTake(out var secondTask); + + // assert + Assert.Same(immediateTask, firstTask); + Assert.Same(deferredTask, secondTask); + } + + [Fact] + public void Mixed_Immediate_And_Deferred_Tasks() + { + // arrange + var queue = new WorkQueue(); + var immediate1 = new MockExecutionTask(isDeferred: false); + var deferred1 = new MockExecutionTask(isDeferred: true); + var immediate2 = new MockExecutionTask(isDeferred: false); + var deferred2 = new MockExecutionTask(isDeferred: true); + + // act + queue.Push(immediate1); + queue.Push(deferred1); + queue.Push(immediate2); + queue.Push(deferred2); + + // assert - all immediate tasks should be taken before deferred + Assert.True(queue.TryTake(out var task1)); + Assert.Same(immediate2, task1); // LIFO for immediate stack + + Assert.True(queue.TryTake(out var task2)); + Assert.Same(immediate1, task2); + + Assert.True(queue.TryTake(out var task3)); + Assert.Same(deferred2, task3); // LIFO for deferred stack + + Assert.True(queue.TryTake(out var task4)); + Assert.Same(deferred1, task4); + + Assert.True(queue.IsEmpty); + } + + [Fact] + public void TryTake_Empty_Queue_Returns_False() + { + // arrange + var queue = new WorkQueue(); + + // act + var success = queue.TryTake(out var task); + + // assert + Assert.False(success); + Assert.Null(task); + Assert.False(queue.HasRunningTasks); + } + + [Fact] + public void Complete_Returns_True_When_All_Complete() + { + // arrange + var queue = new WorkQueue(); + var task = new MockExecutionTask(); + queue.Push(task); + queue.TryTake(out _); + + // act + var allComplete = queue.Complete(); + + // assert + Assert.True(allComplete); + Assert.False(queue.HasRunningTasks); + } + + [Fact] + public void Complete_Returns_False_When_More_Tasks_Running() + { + // arrange + var queue = new WorkQueue(); + var task1 = new MockExecutionTask(); + var task2 = new MockExecutionTask(); + queue.Push(task1); + queue.Push(task2); + queue.TryTake(out _); + queue.TryTake(out _); + + // act + var allComplete = queue.Complete(); + + // assert + Assert.False(allComplete); + Assert.True(queue.HasRunningTasks); + } + + [Fact] + public void Complete_Without_Take_Throws() + { + // arrange + var queue = new WorkQueue(); + + // act & assert + Assert.Throws(() => queue.Complete()); + } + + [Fact] + public void Clear_Resets_Running_Counter() + { + // arrange + var queue = new WorkQueue(); + var task = new MockExecutionTask(); + queue.Push(task); + queue.TryTake(out _); + + // act + queue.Clear(); + + // assert + Assert.False(queue.HasRunningTasks); + Assert.True(queue.IsEmpty); + } + + [Fact] + public void Push_Null_Throws_ArgumentNullException() + { + // arrange + var queue = new WorkQueue(); + + // act & assert + Assert.Throws(() => queue.Push(null!)); + } + + [Fact] + public void Deferred_Tasks_Only() + { + // arrange + var queue = new WorkQueue(); + var deferred1 = new MockExecutionTask(isDeferred: true); + var deferred2 = new MockExecutionTask(isDeferred: true); + + // act + queue.Push(deferred1); + queue.Push(deferred2); + + // assert - LIFO order + Assert.True(queue.TryTake(out var task1)); + Assert.Same(deferred2, task1); + + Assert.True(queue.TryTake(out var task2)); + Assert.Same(deferred1, task2); + + Assert.True(queue.IsEmpty); + } + + public class MockExecutionTask(bool isDeferred = false) : IExecutionTask { public uint Id { get; set; } public ExecutionTaskKind Kind { get; } @@ -136,7 +316,7 @@ public class MockExecutionTask : IExecutionTask public int BranchId => throw new NotImplementedException(); - public bool IsDeferred => throw new NotImplementedException(); + public bool IsDeferred { get; } = isDeferred; public void BeginExecute(CancellationToken cancellationToken) { From a1de950286df77526ff676a9ffb298a20e6619ad Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 12 Feb 2026 22:31:06 +0100 Subject: [PATCH 22/46] Fixed more tests --- .../Types/Types/Introspection/__InputValue.cs | 2 +- .../MultiPartResponseStreamSerializerTests.cs | 1 + .../Core/test/StarWars/Types/CharacterType.cs | 10 ++- .../src/Diagnostics/ActivityEnricher.cs | 8 +- .../src/Diagnostics/InstrumentationOptions.cs | 2 +- ...s.Http_Post_capture_deferred_response.snap | 89 +++++++++---------- ...t_ensure_list_path_is_correctly_built.snap | 8 +- ...rTests.ParseFacebookKitchenSinkSchema.snap | 9 +- .../SyntaxRewriterTests.Rename_Field.snap | 11 ++- ...FormatValue_Should_Pass_When_Value.graphql | 19 +++- ...FormatValue_Should_Pass_When_Value.graphql | 39 +++++++- ...FormatValue_Should_Pass_When_Value.graphql | 23 ++++- ...FormatValue_Should_Pass_When_Value.graphql | 51 ++++++++++- ...FormatValue_Should_Pass_When_Value.graphql | 9 +- ...ts.FormatValue_Should_Pass_When_Value.snap | 29 +++++- 15 files changed, 236 insertions(+), 74 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/__InputValue.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/__InputValue.cs index 6535b364296..d1d11b0e10c 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__InputValue.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__InputValue.cs @@ -76,7 +76,7 @@ public static object IsDeprecated(IResolverContext context) public static object? DefaultValue(IResolverContext context) { var field = context.Parent(); - return field.DefaultValue.IsNull() ? null : field.DefaultValue!.Print(); + return field.DefaultValue.IsNull() ? null : field.DefaultValue!.ToString(indented: false); } public static object AppliedDirectives(IResolverContext context) diff --git a/src/HotChocolate/Core/test/Execution.Tests/Serialization/MultiPartResponseStreamSerializerTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Serialization/MultiPartResponseStreamSerializerTests.cs index f2029a486fe..77a2157a922 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Serialization/MultiPartResponseStreamSerializerTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Serialization/MultiPartResponseStreamSerializerTests.cs @@ -23,6 +23,7 @@ public async Task Serialize_Response_Stream() o.EnableDefer = true; o.EnableStream = true; }) + .ModifyRequestOptions(o => o.IncludeExceptionDetails = true) .ExecuteRequestAsync( """ { diff --git a/src/HotChocolate/Core/test/StarWars/Types/CharacterType.cs b/src/HotChocolate/Core/test/StarWars/Types/CharacterType.cs index cecfe575c55..5d31be234da 100644 --- a/src/HotChocolate/Core/test/StarWars/Types/CharacterType.cs +++ b/src/HotChocolate/Core/test/StarWars/Types/CharacterType.cs @@ -15,8 +15,14 @@ protected override void Configure(IInterfaceTypeDescriptor descripto descriptor.Field(f => f.Name) .Type>(); - descriptor.Field(f => f.Friends) - .UsePaging(); + descriptor + .Field(f => f.Friends) + .UsePaging() + .Resolve(async ctx => + { + await Task.Delay(250); + return ctx.Parent().Friends; + }); descriptor.Field(f => f.AppearsIn) .Type>(); diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs b/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs index 6e4c839572a..10e92743932 100644 --- a/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs +++ b/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs @@ -238,9 +238,7 @@ protected virtual void EnrichRequestVariables( GraphQLRequest request, ISyntaxNode variables, Activity activity) - { - activity.SetTag("graphql.http.request.variables", variables.Print()); - } + => activity.SetTag("graphql.http.request.variables", variables.Print(indented: false)); protected virtual void EnrichBatchVariables( HttpContext context, @@ -248,9 +246,7 @@ protected virtual void EnrichBatchVariables( ISyntaxNode variables, int index, Activity activity) - { - activity.SetTag($"graphql.http.request[{index}].variables", variables.Print()); - } + => activity.SetTag($"graphql.http.request[{index}].variables", variables.Print(indented: false)); protected virtual void EnrichRequestExtensions( HttpContext context, diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics/InstrumentationOptions.cs b/src/HotChocolate/Diagnostics/src/Diagnostics/InstrumentationOptions.cs index 6a831e02f40..a28fe4fbd41 100644 --- a/src/HotChocolate/Diagnostics/src/Diagnostics/InstrumentationOptions.cs +++ b/src/HotChocolate/Diagnostics/src/Diagnostics/InstrumentationOptions.cs @@ -23,7 +23,7 @@ public sealed class InstrumentationOptions public bool IncludeDocument { get; set; } /// - /// Specifies if DataLoader batch keys shall included into the tracing data. + /// Specifies if DataLoader batch keys shall be included into the tracing data. /// public bool IncludeDataLoaderKeys { get; set; } diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_capture_deferred_response.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_capture_deferred_response.snap index 42ce918b5ff..88a26adcf95 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_capture_deferred_response.snap +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_capture_deferred_response.snap @@ -151,60 +151,51 @@ "Value": "OK" } ], - "event": [], - "activities": [ - { - "OperationName": "ResolveFieldValue", - "DisplayName": "/hero/id", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.selection.name", - "Value": "id" - }, - { - "Key": "graphql.selection.type", - "Value": "ID!" - }, - { - "Key": "graphql.selection.path", - "Value": "/hero/id" - }, - { - "Key": "graphql.selection.hierarchy", - "Value": "/hero/id" - }, - { - "Key": "graphql.selection.field.name", - "Value": "id" - }, - { - "Key": "graphql.selection.field.coordinate", - "Value": "Droid.id" - }, - { - "Key": "graphql.selection.field.declaringType", - "Value": "Droid" - }, - { - "Key": "otel.status_code", - "Value": "OK" - } - ], - "event": [] + "event": [] + }, + { + "OperationName": "ResolveFieldValue", + "DisplayName": "/hero/id", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.selection.name", + "Value": "id" + }, + { + "Key": "graphql.selection.type", + "Value": "ID!" + }, + { + "Key": "graphql.selection.path", + "Value": "/hero/id" + }, + { + "Key": "graphql.selection.hierarchy", + "Value": "/hero/id" + }, + { + "Key": "graphql.selection.field.name", + "Value": "id" + }, + { + "Key": "graphql.selection.field.coordinate", + "Value": "Droid.id" + }, + { + "Key": "graphql.selection.field.declaringType", + "Value": "Droid" + }, + { + "Key": "otel.status_code", + "Value": "OK" } - ] + ], + "event": [] } ] } ] - }, - { - "OperationName": "ExecuteStream", - "DisplayName": "ExecuteStream", - "Status": "Unset", - "tags": [], - "event": [] } ] } diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_ensure_list_path_is_correctly_built.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_ensure_list_path_is_correctly_built.snap index 41a0dfdc2ad..8a5b3455d73 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_ensure_list_path_is_correctly_built.snap +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_ensure_list_path_is_correctly_built.snap @@ -195,7 +195,7 @@ }, { "OperationName": "ResolveFieldValue", - "DisplayName": "/hero/friends/nodes[2]/friends", + "DisplayName": "/hero/friends/nodes[0]/friends", "Status": "Ok", "tags": [ { @@ -208,7 +208,7 @@ }, { "Key": "graphql.selection.path", - "Value": "/hero/friends/nodes[2]/friends" + "Value": "/hero/friends/nodes[0]/friends" }, { "Key": "graphql.selection.hierarchy", @@ -275,7 +275,7 @@ }, { "OperationName": "ResolveFieldValue", - "DisplayName": "/hero/friends/nodes[0]/friends", + "DisplayName": "/hero/friends/nodes[2]/friends", "Status": "Ok", "tags": [ { @@ -288,7 +288,7 @@ }, { "Key": "graphql.selection.path", - "Value": "/hero/friends/nodes[0]/friends" + "Value": "/hero/friends/nodes[2]/friends" }, { "Key": "graphql.selection.hierarchy", diff --git a/src/HotChocolate/Language/test/Language.Tests/Parser/__snapshots__/KitchenSinkParserTests.ParseFacebookKitchenSinkSchema.snap b/src/HotChocolate/Language/test/Language.Tests/Parser/__snapshots__/KitchenSinkParserTests.ParseFacebookKitchenSinkSchema.snap index ae7f33c5522..e97bf5bddd7 100644 --- a/src/HotChocolate/Language/test/Language.Tests/Parser/__snapshots__/KitchenSinkParserTests.ParseFacebookKitchenSinkSchema.snap +++ b/src/HotChocolate/Language/test/Language.Tests/Parser/__snapshots__/KitchenSinkParserTests.ParseFacebookKitchenSinkSchema.snap @@ -19,8 +19,13 @@ type Foo implements Bar & Baz { """ argument: InputType!): Type three(argument: InputType other: String): Int four(argument: String = "string"): String - five(argument: [String] = [ "string", "string" ]): String - six(argument: InputType = { key: "value" }): Type + five(argument: [String] = [ + "string", + "string" + ]): String + six(argument: InputType = { + key: "value" + }): Type seven(argument: Int): Type } diff --git a/src/HotChocolate/Language/test/Language.Tests/Visitors/__snapshots__/SyntaxRewriterTests.Rename_Field.snap b/src/HotChocolate/Language/test/Language.Tests/Visitors/__snapshots__/SyntaxRewriterTests.Rename_Field.snap index 3918309bb9c..03155cea738 100644 --- a/src/HotChocolate/Language/test/Language.Tests/Visitors/__snapshots__/SyntaxRewriterTests.Rename_Field.snap +++ b/src/HotChocolate/Language/test/Language.Tests/Visitors/__snapshots__/SyntaxRewriterTests.Rename_Field.snap @@ -1,4 +1,4 @@ -schema { +schema { query: QueryType mutation: MutationType } @@ -17,8 +17,13 @@ type Foo implements Bar & Baz { """ argument: InputType!): Type three_abc(argument: InputType other: String): Int four_abc(argument: String = "string"): String - five_abc(argument: [String] = [ "string", "string" ]): String - six_abc(argument: InputType = { key: "value" }): Type + five_abc(argument: [String] = [ + "string", + "string" + ]): String + six_abc(argument: InputType = { + key: "value" + }): Type seven_abc(argument: Int): Type } diff --git a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql index 679f756d4f3..0003a181198 100644 --- a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql +++ b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql @@ -1 +1,18 @@ -{ type: LineString, coordinates: [ [ 30, 10 ], [ 10, 30 ], [ 40, 40 ] ], crs: 0 } +{ + type: LineString, + coordinates: [ + [ + 30, + 10 + ], + [ + 10, + 30 + ], + [ + 40, + 40 + ] + ], + crs: 0 +} diff --git a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql index 2258e40575d..93d1c3c2a33 100644 --- a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql +++ b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql @@ -1 +1,38 @@ -{ type: MultiLineString, coordinates: [ [ [ 10, 10 ], [ 20, 20 ], [ 10, 40 ] ], [ [ 40, 40 ], [ 30, 30 ], [ 40, 20 ], [ 30, 10 ] ] ], crs: 0 } +{ + type: MultiLineString, + coordinates: [ + [ + [ + 10, + 10 + ], + [ + 20, + 20 + ], + [ + 10, + 40 + ] + ], + [ + [ + 40, + 40 + ], + [ + 30, + 30 + ], + [ + 40, + 20 + ], + [ + 30, + 10 + ] + ] + ], + crs: 0 +} diff --git a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql index 85265655cc3..94f1f357e52 100644 --- a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql +++ b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql @@ -1 +1,22 @@ -{ type: MultiPoint, coordinates: [ [ 10, 40 ], [ 40, 30 ], [ 20, 20 ], [ 30, 10 ] ], crs: 0 } +{ + type: MultiPoint, + coordinates: [ + [ + 10, + 40 + ], + [ + 40, + 30 + ], + [ + 20, + 20 + ], + [ + 30, + 10 + ] + ], + crs: 0 +} diff --git a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPolygonSerializerTests.FormatValue_Should_Pass_When_Value.graphql b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPolygonSerializerTests.FormatValue_Should_Pass_When_Value.graphql index 11b657e16bb..5de8868372b 100644 --- a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPolygonSerializerTests.FormatValue_Should_Pass_When_Value.graphql +++ b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPolygonSerializerTests.FormatValue_Should_Pass_When_Value.graphql @@ -1 +1,50 @@ -{ type: MultiPolygon, coordinates: [ [ [ [ 30, 20 ], [ 45, 40 ], [ 10, 40 ], [ 30, 20 ] ] ], [ [ [ 15, 5 ], [ 40, 10 ], [ 10, 20 ], [ 5, 15 ], [ 15, 5 ] ] ] ], crs: 0 } +{ + type: MultiPolygon, + coordinates: [ + [ + [ + [ + 30, + 20 + ], + [ + 45, + 40 + ], + [ + 10, + 40 + ], + [ + 30, + 20 + ] + ] + ], + [ + [ + [ + 15, + 5 + ], + [ + 40, + 10 + ], + [ + 10, + 20 + ], + [ + 5, + 15 + ], + [ + 15, + 5 + ] + ] + ] + ], + crs: 0 +} diff --git a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql index 27a6e14a82f..bc6573ef262 100644 --- a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql +++ b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql @@ -1 +1,8 @@ -{ type: Point, coordinates: [ 30, 10 ], crs: 0 } +{ + type: Point, + coordinates: [ + 30, + 10 + ], + crs: 0 +} diff --git a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPolygonSerializerTests.FormatValue_Should_Pass_When_Value.snap b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPolygonSerializerTests.FormatValue_Should_Pass_When_Value.snap index b2669fa7c84..c883404c651 100644 --- a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPolygonSerializerTests.FormatValue_Should_Pass_When_Value.snap +++ b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPolygonSerializerTests.FormatValue_Should_Pass_When_Value.snap @@ -1 +1,28 @@ -{ type: Polygon, coordinates: [ [ [ 30, 10 ], [ 40, 40 ], [ 20, 40 ], [ 10, 20 ], [ 30, 10 ] ] ], crs: 0 } +{ + type: Polygon, + coordinates: [ + [ + [ + 30, + 10 + ], + [ + 40, + 40 + ], + [ + 20, + 40 + ], + [ + 10, + 20 + ], + [ + 30, + 10 + ] + ] + ], + crs: 0 +} From 1f11f6c55a1adbc31e336da44d4694066f706184 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 08:32:41 +0100 Subject: [PATCH 23/46] Fixed more tests --- .../DeferExecutionCoordinator.Pooling.cs | 17 +++++ .../Processing/DeferExecutionCoordinator.cs | 19 ++++++ .../Processing/OperationContext.Execution.cs | 2 +- .../Processing/OperationContext.Pooling.cs | 11 +++- ...alizerTests.Serialize_Response_Stream.snap | 20 +++--- .../QueryablePagingProjectionOptimizer.cs | 4 +- .../Data/Projections/ProjectionOptimizer.cs | 3 +- ...Type_Both_Have_Different_Dependencies.yaml | 34 +++++----- ...ete_Type_Linked_Field_With_Dependency.yaml | 16 ++--- ...st_Field_Linked_Field_With_Dependency.yaml | 20 +++--- ..._Different_Selection_In_Concrete_Type.yaml | 32 +++++----- ...dency_Same_Selection_In_Concrete_Type.yaml | 28 ++++---- ...terface_Object_Property_Concrete_Type.yaml | 8 +-- ...ete_Type_Linked_Field_With_Dependency.yaml | 16 ++--- ...roperty_Concrete_Type_With_Dependency.yaml | 8 +-- ...Property_Linked_Field_With_Dependency.yaml | 16 ++--- ..._Different_Selection_In_Concrete_Type.yaml | 28 ++++---- ...dency_Same_Selection_In_Concrete_Type.yaml | 24 +++---- ...on_Field_Concrete_Type_Has_Dependency.yaml | 16 ++--- ...oncrete_Type_Selection_Has_Dependency.yaml | 16 ++--- ...ions_Have_Dependency_To_Same_Subgraph.yaml | 16 ++--- ..._Type_Selections_Have_Same_Dependency.yaml | 16 ++--- ...ion_List_Concrete_Type_Has_Dependency.yaml | 40 ++++++------ ...oncrete_Type_Selection_Has_Dependency.yaml | 64 +++++++++---------- ...ions_Have_Dependency_To_Same_Subgraph.yaml | 64 +++++++++---------- ..._Type_Selections_Have_Same_Dependency.yaml | 64 +++++++++---------- ...oncrete_Type_Selection_Has_Dependency.yaml | 16 ++--- ...ions_Have_Dependency_To_Same_Subgraph.yaml | 16 ++--- ..._Type_Selections_Have_Same_Dependency.yaml | 16 ++--- ...ject_When_ResultOfLeftJoinIsNull_Deep.snap | 16 +---- 30 files changed, 348 insertions(+), 318 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs index eaa0c00f7b9..a8a7c45e778 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs @@ -1,15 +1,28 @@ +using System.Diagnostics; + namespace HotChocolate.Execution.Processing; internal sealed partial class DeferExecutionCoordinator { +#if DEBUG + private bool _isInitialized; +#endif + /// /// Initializes the coordinator for a new execution cycle. /// Must be called before any other operations when leased from a pool. /// public void Initialize(BranchTracker branchTracker, int mainBranchId) { + Debug.Assert(branchTracker is not null); + Debug.Assert(mainBranchId > 0); + _branchTracker = branchTracker; _mainBranchId = mainBranchId; + +#if DEBUG + _isInitialized = true; +#endif } /// @@ -33,6 +46,10 @@ public void Reset() _mainBranchId = 0; _pendingBranches = 0; +#if DEBUG + _isInitialized = false; +#endif + if (_results.Capacity > 64) { _results.Capacity = 64; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs index 81b4ae803a5..ec36cf2dbec 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using HotChocolate.Fetching; @@ -25,6 +26,16 @@ internal sealed partial class DeferExecutionCoordinator private volatile bool _isComplete; private int _pendingBranches; +#pragma warning disable IDE0052 // Remove unread private members + private static int s_nextId; + private readonly int _id; +#pragma warning restore IDE0052 // Remove unread private members + + public DeferExecutionCoordinator() + { + _id = Interlocked.Increment(ref s_nextId); + } + /// /// Gets whether any deferred execution branches have been registered. /// @@ -37,6 +48,8 @@ internal sealed partial class DeferExecutionCoordinator /// public int Branch(int currentBranchId, Path path, DeferUsage deferUsage) { + Debug.Assert(_isInitialized); + var key = new DeferredBranchKey(path, deferUsage, currentBranchId); lock (_sync) @@ -61,6 +74,8 @@ public int Branch(int currentBranchId, Path path, DeferUsage deferUsage) /// public void EnqueueResult(OperationResult result) { + Debug.Assert(_isInitialized); + lock (_sync) { ComposeAndDeliverUnsafe(_mainBranchId, result); @@ -74,6 +89,8 @@ public void EnqueueResult(OperationResult result) /// public void EnqueueResult(OperationResult result, int branchId) { + Debug.Assert(_isInitialized); + lock (_sync) { _completed[branchId] = result; @@ -93,6 +110,8 @@ public void EnqueueResult(OperationResult result, int branchId) public async IAsyncEnumerable ReadResultsAsync( [EnumeratorCancellation] CancellationToken cancellationToken = default) { + Debug.Assert(_isInitialized); + List? snapshot = null; await using var registration = cancellationToken.Register(_signal.Set); diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs index d1495d5f8be..c24d6765b43 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs @@ -23,7 +23,7 @@ public DeferExecutionCoordinator DeferExecutionCoordinator get { AssertInitialized(); - return _deferExecutionCoordinator; + return _currentDeferExecutionCoordinator; } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs index 0fa34f6c917..c7fd8d6df80 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Runtime.CompilerServices; using HotChocolate.Execution.DependencyInjection; using HotChocolate.Execution.Instrumentation; @@ -19,6 +20,7 @@ internal sealed partial class OperationContext private readonly DeferExecutionCoordinator _deferExecutionCoordinator = new(); private WorkScheduler _currentWorkScheduler; private BranchTracker _currentBranchTracker; + private DeferExecutionCoordinator _currentDeferExecutionCoordinator; private readonly AggregateServiceScopeInitializer _serviceScopeInitializer; private RequestContext _requestContext = null!; private Schema _schema = null!; @@ -47,6 +49,7 @@ public OperationContext( _workScheduler = new WorkScheduler(this); _currentWorkScheduler = _workScheduler; _currentBranchTracker = _branchTracker; + _currentDeferExecutionCoordinator = _deferExecutionCoordinator; _serviceScopeInitializer = serviceScopeInitializer; Converter = typeConverter; } @@ -89,6 +92,7 @@ public void Initialize( _currentBranchTracker = _branchTracker; _currentWorkScheduler = _workScheduler; + _currentDeferExecutionCoordinator = _deferExecutionCoordinator; _isInitialized = true; // once the operation context is marked as initialized we can initialize sub components. @@ -120,7 +124,9 @@ public void InitializeDeferContext( _batchDispatcher = context._batchDispatcher; _currentBranchTracker = context._currentBranchTracker; _currentWorkScheduler = context._currentWorkScheduler; + _currentDeferExecutionCoordinator = context._currentDeferExecutionCoordinator; _branchId = executionBranchId; + _isInitialized = true; IncludeFlags = context.IncludeFlags; DeferFlags = context.DeferFlags; @@ -133,15 +139,16 @@ public void InitializeDeferContext( deferUsage); Result.RequestIndex = _requestContext.RequestIndex; Result.VariableIndex = context._variableIndex; - - _isInitialized = true; } public void InitializeWorkSchedulerFrom(OperationContext context) { + Debug.Assert(_isInitialized); + _currentBranchTracker = context._currentBranchTracker; _currentWorkScheduler = context._currentWorkScheduler; _branchId = _currentBranchTracker.CreateNewBranchId(); + _deferExecutionCoordinator.Initialize(_currentBranchTracker, _branchId); } public void Clean() diff --git a/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap b/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap index f913502ff09..f25677d55d7 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap @@ -1,10 +1,10 @@ - ---- -Content-Type: application/json; charset=utf-8 - -{"data":{"hero":{"id":"2001"}},"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"data":{"friends":{"nodes":[{"id":"1000","name":"Luke Skywalker"},{"id":"1002","name":"Han Solo"},{"id":"1003","name":"Leia Organa"}]}},"label":"friends","path":["hero"]}],"hasNext":false} ------ + +--- +Content-Type: application/json; charset=utf-8 + +{"data":{"hero":{"id":"2001"}},"pending":[{"id":2,"path":["hero"],"label":"friends"}],"hasNext":true} +--- +Content-Type: application/json; charset=utf-8 + +{"incremental":[{"id":2,"errors":[{"message":"Unexpected Execution Error","path":["hero","friends"]}],"data":{"friends":null}}],"completed":[{"id":2}],"hasNext":false} +----- diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryablePagingProjectionOptimizer.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryablePagingProjectionOptimizer.cs index 4026a494ce0..10422d51f6c 100644 --- a/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryablePagingProjectionOptimizer.cs +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryablePagingProjectionOptimizer.cs @@ -60,9 +60,7 @@ private Selection CreateCombinedSelection( Array.Empty(), new SelectionSetNode(selections)); - var nodesPipeline = - selection.ResolverPipeline ?? - context.CompileResolverPipeline(nodesField, combinedField); + var nodesPipeline = context.CompileResolverPipeline(nodesField, combinedField); return new Selection( context.NewSelectionId(), diff --git a/src/HotChocolate/Data/src/Data/Projections/ProjectionOptimizer.cs b/src/HotChocolate/Data/src/Data/Projections/ProjectionOptimizer.cs index 6e6d1fe04bd..65adaa59dd1 100644 --- a/src/HotChocolate/Data/src/Data/Projections/ProjectionOptimizer.cs +++ b/src/HotChocolate/Data/src/Data/Projections/ProjectionOptimizer.cs @@ -14,10 +14,11 @@ public void OptimizeSelectionSet(SelectionSetOptimizerContext context) selectionToProcess.ExceptWith(processedSelections); foreach (var responseName in selectionToProcess) { + var selection = context.GetSelection(responseName); var rewrittenSelection = provider.RewriteSelection( context, - context.GetSelection(responseName)); + selection); context.ReplaceSelection(rewrittenSelection); diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Different_Dependencies.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Different_Dependencies.yaml index b6709c27b81..2dc29e7358c 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Different_Dependencies.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Different_Dependencies.yaml @@ -32,6 +32,10 @@ response: "__typename": "Item2", "id": "SXRlbTI6MQ==", "products": [ + { + "id": "UHJvZHVjdDox", + "name": "Product: UHJvZHVjdDox" + }, { "id": "UHJvZHVjdDoy", "name": "Product: UHJvZHVjdDoy" @@ -39,14 +43,10 @@ response: { "id": "UHJvZHVjdDoz", "name": "Product: UHJvZHVjdDoz" - }, - { - "id": "UHJvZHVjdDo0", - "name": "Product: UHJvZHVjdDo0" } ], "singularProduct": { - "name": "Product: UHJvZHVjdDox" + "name": "Product: UHJvZHVjdDo0" } } } @@ -118,17 +118,17 @@ sourceSchemas: "id": "SXRlbTI6MQ==", "products": [ { - "id": "UHJvZHVjdDoy" + "id": "UHJvZHVjdDox" }, { - "id": "UHJvZHVjdDoz" + "id": "UHJvZHVjdDoy" }, { - "id": "UHJvZHVjdDo0" + "id": "UHJvZHVjdDoz" } ], "singularProduct": { - "id": "UHJvZHVjdDox" + "id": "UHJvZHVjdDo0" } } } @@ -166,7 +166,7 @@ sourceSchemas: } variables: | { - "__fusion_1_id": "UHJvZHVjdDox" + "__fusion_1_id": "UHJvZHVjdDo0" } response: results: @@ -175,7 +175,7 @@ sourceSchemas: "data": { "node": { "__typename": "Product", - "name": "Product: UHJvZHVjdDox" + "name": "Product: UHJvZHVjdDo0" } } } @@ -194,13 +194,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDoy" + "__fusion_2_id": "UHJvZHVjdDox" }, { - "__fusion_2_id": "UHJvZHVjdDoz" + "__fusion_2_id": "UHJvZHVjdDoy" }, { - "__fusion_2_id": "UHJvZHVjdDo0" + "__fusion_2_id": "UHJvZHVjdDoz" } ] response: @@ -211,7 +211,7 @@ sourceSchemas: "data": { "node": { "__typename": "Product", - "name": "Product: UHJvZHVjdDoy" + "name": "Product: UHJvZHVjdDox" } } } @@ -220,7 +220,7 @@ sourceSchemas: "data": { "node": { "__typename": "Product", - "name": "Product: UHJvZHVjdDoz" + "name": "Product: UHJvZHVjdDoy" } } } @@ -229,7 +229,7 @@ sourceSchemas: "data": { "node": { "__typename": "Product", - "name": "Product: UHJvZHVjdDo0" + "name": "Product: UHJvZHVjdDoz" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Concrete_Type_Linked_Field_With_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Concrete_Type_Linked_Field_With_Dependency.yaml index 1c50a89b2a4..2e8119e876c 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Concrete_Type_Linked_Field_With_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Concrete_Type_Linked_Field_With_Dependency.yaml @@ -19,7 +19,7 @@ response: { "viewerCanVote": true, "author": { - "displayName": "Author: QXV0aG9yOjU=" + "displayName": "Author: QXV0aG9yOjQ=" } }, { @@ -28,7 +28,7 @@ response: { "viewerCanVote": true, "author": { - "displayName": "Author: QXV0aG9yOjQ=" + "displayName": "Author: QXV0aG9yOjU=" } } ] @@ -87,7 +87,7 @@ sourceSchemas: "__typename": "Discussion", "viewerCanVote": true, "author": { - "id": "QXV0aG9yOjU=" + "id": "QXV0aG9yOjQ=" } }, { @@ -98,7 +98,7 @@ sourceSchemas: "__typename": "Discussion", "viewerCanVote": true, "author": { - "id": "QXV0aG9yOjQ=" + "id": "QXV0aG9yOjU=" } } ] @@ -131,10 +131,10 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjU=" + "__fusion_1_id": "QXV0aG9yOjQ=" }, { - "__fusion_1_id": "QXV0aG9yOjQ=" + "__fusion_1_id": "QXV0aG9yOjU=" } ] response: @@ -144,7 +144,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjU=" + "displayName": "Author: QXV0aG9yOjQ=" } } } @@ -152,7 +152,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjQ=" + "displayName": "Author: QXV0aG9yOjU=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency.yaml index b76ac5260ed..e33fd1a6f79 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency.yaml @@ -16,8 +16,8 @@ response: "authorables": [ { "author": { - "id": "QXV0aG9yOjY=", - "displayName": "Author: QXV0aG9yOjY=" + "id": "QXV0aG9yOjQ=", + "displayName": "Author: QXV0aG9yOjQ=" } }, { @@ -28,8 +28,8 @@ response: }, { "author": { - "id": "QXV0aG9yOjQ=", - "displayName": "Author: QXV0aG9yOjQ=" + "id": "QXV0aG9yOjY=", + "displayName": "Author: QXV0aG9yOjY=" } } ] @@ -81,7 +81,7 @@ sourceSchemas: { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjY=" + "id": "QXV0aG9yOjQ=" } }, { @@ -93,7 +93,7 @@ sourceSchemas: { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjQ=" + "id": "QXV0aG9yOjY=" } } ] @@ -126,13 +126,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjY=" + "__fusion_1_id": "QXV0aG9yOjQ=" }, { "__fusion_1_id": "QXV0aG9yOjU=" }, { - "__fusion_1_id": "QXV0aG9yOjQ=" + "__fusion_1_id": "QXV0aG9yOjY=" } ] response: @@ -142,7 +142,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjY=" + "displayName": "Author: QXV0aG9yOjQ=" } } } @@ -158,7 +158,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjQ=" + "displayName": "Author: QXV0aG9yOjY=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml index c844eb1cec8..c5171eae478 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml @@ -21,9 +21,9 @@ response: "authorables": [ { "author": { - "id": "QXV0aG9yOjY=", - "displayName": "Author: QXV0aG9yOjY=", - "email": "Author: QXV0aG9yOjY=" + "id": "QXV0aG9yOjQ=", + "displayName": "Author: QXV0aG9yOjQ=", + "email": "Author: QXV0aG9yOjQ=" } }, { @@ -34,9 +34,9 @@ response: }, { "author": { - "id": "QXV0aG9yOjQ=", - "displayName": "Author: QXV0aG9yOjQ=", - "email": "Author: QXV0aG9yOjQ=" + "id": "QXV0aG9yOjY=", + "displayName": "Author: QXV0aG9yOjY=", + "email": "Author: QXV0aG9yOjY=" } } ] @@ -93,7 +93,7 @@ sourceSchemas: { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjY=" + "id": "QXV0aG9yOjQ=" } }, { @@ -105,7 +105,7 @@ sourceSchemas: { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjQ=" + "id": "QXV0aG9yOjY=" } } ] @@ -139,10 +139,10 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjY=" + "__fusion_1_id": "QXV0aG9yOjQ=" }, { - "__fusion_1_id": "QXV0aG9yOjQ=" + "__fusion_1_id": "QXV0aG9yOjY=" } ] response: @@ -152,7 +152,7 @@ sourceSchemas: { "data": { "authorById": { - "email": "Author: QXV0aG9yOjY=" + "email": "Author: QXV0aG9yOjQ=" } } } @@ -160,7 +160,7 @@ sourceSchemas: { "data": { "authorById": { - "email": "Author: QXV0aG9yOjQ=" + "email": "Author: QXV0aG9yOjY=" } } } @@ -176,13 +176,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "QXV0aG9yOjY=" + "__fusion_2_id": "QXV0aG9yOjQ=" }, { "__fusion_2_id": "QXV0aG9yOjU=" }, { - "__fusion_2_id": "QXV0aG9yOjQ=" + "__fusion_2_id": "QXV0aG9yOjY=" } ] response: @@ -192,7 +192,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjY=" + "displayName": "Author: QXV0aG9yOjQ=" } } } @@ -208,7 +208,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjQ=" + "displayName": "Author: QXV0aG9yOjY=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml index 34125ac70bf..8dc4faf67f4 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml @@ -22,8 +22,8 @@ response: "authorables": [ { "author": { - "id": "QXV0aG9yOjY=", - "displayName": "Author: QXV0aG9yOjY=" + "id": "QXV0aG9yOjQ=", + "displayName": "Author: QXV0aG9yOjQ=" } }, { @@ -34,8 +34,8 @@ response: }, { "author": { - "id": "QXV0aG9yOjQ=", - "displayName": "Author: QXV0aG9yOjQ=" + "id": "QXV0aG9yOjY=", + "displayName": "Author: QXV0aG9yOjY=" } } ] @@ -92,7 +92,7 @@ sourceSchemas: { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjY=" + "id": "QXV0aG9yOjQ=" } }, { @@ -104,7 +104,7 @@ sourceSchemas: { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjQ=" + "id": "QXV0aG9yOjY=" } } ] @@ -137,10 +137,10 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjY=" + "__fusion_1_id": "QXV0aG9yOjQ=" }, { - "__fusion_1_id": "QXV0aG9yOjQ=" + "__fusion_1_id": "QXV0aG9yOjY=" } ] response: @@ -150,7 +150,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjY=" + "displayName": "Author: QXV0aG9yOjQ=" } } } @@ -158,7 +158,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjQ=" + "displayName": "Author: QXV0aG9yOjY=" } } } @@ -174,13 +174,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "QXV0aG9yOjY=" + "__fusion_2_id": "QXV0aG9yOjQ=" }, { "__fusion_2_id": "QXV0aG9yOjU=" }, { - "__fusion_2_id": "QXV0aG9yOjQ=" + "__fusion_2_id": "QXV0aG9yOjY=" } ] response: @@ -190,7 +190,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjY=" + "displayName": "Author: QXV0aG9yOjQ=" } } } @@ -206,7 +206,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjQ=" + "displayName": "Author: QXV0aG9yOjY=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type.yaml index 1668db2e4eb..5072d3ecd66 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type.yaml @@ -19,7 +19,7 @@ response: { "votable": { "viewerCanVote": true, - "title": "Discussion: RGlzY3Vzc2lvbjo2" + "title": "Discussion: RGlzY3Vzc2lvbjo0" } }, { @@ -31,7 +31,7 @@ response: { "votable": { "viewerCanVote": true, - "title": "Discussion: RGlzY3Vzc2lvbjo0" + "title": "Discussion: RGlzY3Vzc2lvbjo2" } } ] @@ -90,7 +90,7 @@ sourceSchemas: "votable": { "__typename": "Discussion", "viewerCanVote": true, - "title": "Discussion: RGlzY3Vzc2lvbjo2" + "title": "Discussion: RGlzY3Vzc2lvbjo0" } }, { @@ -104,7 +104,7 @@ sourceSchemas: "votable": { "__typename": "Discussion", "viewerCanVote": true, - "title": "Discussion: RGlzY3Vzc2lvbjo0" + "title": "Discussion: RGlzY3Vzc2lvbjo2" } } ] diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_Linked_Field_With_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_Linked_Field_With_Dependency.yaml index 7aae9db06b4..f7928f2c3ca 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_Linked_Field_With_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_Linked_Field_With_Dependency.yaml @@ -22,7 +22,7 @@ response: "votable": { "viewerCanVote": true, "author": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } }, @@ -38,7 +38,7 @@ response: "votable": { "viewerCanVote": true, "author": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } @@ -105,7 +105,7 @@ sourceSchemas: "__typename": "Discussion", "viewerCanVote": true, "author": { - "id": "QXV0aG9yOjc=" + "id": "QXV0aG9yOjk=" } } }, @@ -123,7 +123,7 @@ sourceSchemas: "__typename": "Discussion", "viewerCanVote": true, "author": { - "id": "QXV0aG9yOjk=" + "id": "QXV0aG9yOjc=" } } } @@ -157,13 +157,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjc=" + "__fusion_1_id": "QXV0aG9yOjk=" }, { "__fusion_1_id": "QXV0aG9yOjg=" }, { - "__fusion_1_id": "QXV0aG9yOjk=" + "__fusion_1_id": "QXV0aG9yOjc=" } ] response: @@ -173,7 +173,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } } @@ -189,7 +189,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_With_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_With_Dependency.yaml index 8039b99d57e..b2743ed1ea3 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_With_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_With_Dependency.yaml @@ -90,7 +90,7 @@ sourceSchemas: "votable": { "__typename": "Discussion", "viewerCanVote": true, - "id": "RGlzY3Vzc2lvbjo2" + "id": "RGlzY3Vzc2lvbjo0" } }, { @@ -104,7 +104,7 @@ sourceSchemas: "votable": { "__typename": "Discussion", "viewerCanVote": true, - "id": "RGlzY3Vzc2lvbjo0" + "id": "RGlzY3Vzc2lvbjo2" } } ] @@ -137,13 +137,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "RGlzY3Vzc2lvbjo2" + "__fusion_1_id": "RGlzY3Vzc2lvbjo0" }, { "__fusion_1_id": "RGlzY3Vzc2lvbjo1" }, { - "__fusion_1_id": "RGlzY3Vzc2lvbjo0" + "__fusion_1_id": "RGlzY3Vzc2lvbjo2" } ] response: diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency.yaml index c67111a2557..60f862fb6ad 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency.yaml @@ -18,7 +18,7 @@ response: { "authorable": { "author": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } }, @@ -32,7 +32,7 @@ response: { "authorable": { "author": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } @@ -92,7 +92,7 @@ sourceSchemas: "authorable": { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjc=" + "id": "QXV0aG9yOjk=" } } }, @@ -108,7 +108,7 @@ sourceSchemas: "authorable": { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjk=" + "id": "QXV0aG9yOjc=" } } } @@ -142,13 +142,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjc=" + "__fusion_1_id": "QXV0aG9yOjk=" }, { "__fusion_1_id": "QXV0aG9yOjg=" }, { - "__fusion_1_id": "QXV0aG9yOjk=" + "__fusion_1_id": "QXV0aG9yOjc=" } ] response: @@ -158,7 +158,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } } @@ -174,7 +174,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml index a419789f622..43cb43ed090 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml @@ -23,8 +23,8 @@ response: { "authorable": { "author": { - "displayName": "Author: QXV0aG9yOjc=", - "email": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=", + "email": "Author: QXV0aG9yOjk=" } } }, @@ -39,8 +39,8 @@ response: { "authorable": { "author": { - "displayName": "Author: QXV0aG9yOjk=", - "email": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=", + "email": "Author: QXV0aG9yOjc=" } } } @@ -105,7 +105,7 @@ sourceSchemas: "authorable": { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjc=" + "id": "QXV0aG9yOjk=" } } }, @@ -121,7 +121,7 @@ sourceSchemas: "authorable": { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjk=" + "id": "QXV0aG9yOjc=" } } } @@ -156,13 +156,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjc=" + "__fusion_1_id": "QXV0aG9yOjk=" }, { "__fusion_1_id": "QXV0aG9yOjg=" }, { - "__fusion_1_id": "QXV0aG9yOjk=" + "__fusion_1_id": "QXV0aG9yOjc=" } ] response: @@ -172,7 +172,7 @@ sourceSchemas: { "data": { "authorById": { - "email": "Author: QXV0aG9yOjc=" + "email": "Author: QXV0aG9yOjk=" } } } @@ -188,7 +188,7 @@ sourceSchemas: { "data": { "authorById": { - "email": "Author: QXV0aG9yOjk=" + "email": "Author: QXV0aG9yOjc=" } } } @@ -204,13 +204,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "QXV0aG9yOjc=" + "__fusion_2_id": "QXV0aG9yOjk=" }, { "__fusion_2_id": "QXV0aG9yOjg=" }, { - "__fusion_2_id": "QXV0aG9yOjk=" + "__fusion_2_id": "QXV0aG9yOjc=" } ] response: @@ -220,7 +220,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } } @@ -236,7 +236,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml index 60a8e56097e..c47b6be5f64 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml @@ -23,7 +23,7 @@ response: { "authorable": { "author": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } }, @@ -37,7 +37,7 @@ response: { "authorable": { "author": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } @@ -102,7 +102,7 @@ sourceSchemas: "authorable": { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjc=" + "id": "QXV0aG9yOjk=" } } }, @@ -118,7 +118,7 @@ sourceSchemas: "authorable": { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjk=" + "id": "QXV0aG9yOjc=" } } } @@ -152,13 +152,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjc=" + "__fusion_1_id": "QXV0aG9yOjk=" }, { "__fusion_1_id": "QXV0aG9yOjg=" }, { - "__fusion_1_id": "QXV0aG9yOjk=" + "__fusion_1_id": "QXV0aG9yOjc=" } ] response: @@ -168,7 +168,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } } @@ -184,7 +184,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } @@ -200,13 +200,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "QXV0aG9yOjc=" + "__fusion_2_id": "QXV0aG9yOjk=" }, { "__fusion_2_id": "QXV0aG9yOjg=" }, { - "__fusion_2_id": "QXV0aG9yOjk=" + "__fusion_2_id": "QXV0aG9yOjc=" } ] response: @@ -216,7 +216,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } } @@ -232,7 +232,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Has_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Has_Dependency.yaml index 0c86c318ac8..c4a41df3cd0 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Has_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Has_Dependency.yaml @@ -20,7 +20,7 @@ response: "postEdges": [ { "node": { - "subgraph2": "Photo: UGhvdG86Ng==" + "subgraph2": "Photo: UGhvdG86NA==" } }, { @@ -30,7 +30,7 @@ response: }, { "node": { - "subgraph2": "Photo: UGhvdG86NA==" + "subgraph2": "Photo: UGhvdG86Ng==" } } ] @@ -86,7 +86,7 @@ sourceSchemas: { "node": { "__typename": "Photo", - "id": "UGhvdG86Ng==" + "id": "UGhvdG86NA==" } }, { @@ -98,7 +98,7 @@ sourceSchemas: { "node": { "__typename": "Photo", - "id": "UGhvdG86NA==" + "id": "UGhvdG86Ng==" } } ] @@ -131,13 +131,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "UGhvdG86Ng==" + "__fusion_1_id": "UGhvdG86NA==" }, { "__fusion_1_id": "UGhvdG86NQ==" }, { - "__fusion_1_id": "UGhvdG86NA==" + "__fusion_1_id": "UGhvdG86Ng==" } ] response: @@ -147,7 +147,7 @@ sourceSchemas: { "data": { "photoById": { - "subgraph2": "Photo: UGhvdG86Ng==" + "subgraph2": "Photo: UGhvdG86NA==" } } } @@ -163,7 +163,7 @@ sourceSchemas: { "data": { "photoById": { - "subgraph2": "Photo: UGhvdG86NA==" + "subgraph2": "Photo: UGhvdG86Ng==" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selection_Has_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selection_Has_Dependency.yaml index e3ea415d229..ad340c62cf7 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selection_Has_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selection_Has_Dependency.yaml @@ -25,7 +25,7 @@ response: { "node": { "product": { - "subgraph2": "Product: UHJvZHVjdDo3" + "subgraph2": "Product: UHJvZHVjdDo5" } } }, @@ -39,7 +39,7 @@ response: { "node": { "product": { - "subgraph2": "Product: UHJvZHVjdDo5" + "subgraph2": "Product: UHJvZHVjdDo3" } } } @@ -110,7 +110,7 @@ sourceSchemas: "node": { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo3" + "id": "UHJvZHVjdDo5" } } }, @@ -126,7 +126,7 @@ sourceSchemas: "node": { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo5" + "id": "UHJvZHVjdDo3" } } } @@ -160,13 +160,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDo3" + "__fusion_2_id": "UHJvZHVjdDo5" }, { "__fusion_2_id": "UHJvZHVjdDo4" }, { - "__fusion_2_id": "UHJvZHVjdDo5" + "__fusion_2_id": "UHJvZHVjdDo3" } ] response: @@ -176,7 +176,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo3" + "subgraph2": "Product: UHJvZHVjdDo5" } } } @@ -192,7 +192,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo5" + "subgraph2": "Product: UHJvZHVjdDo3" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml index a296d4099e9..092d79ed88a 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml @@ -25,7 +25,7 @@ response: { "node": { "product": { - "subgraph2": "Product: UHJvZHVjdDo3" + "subgraph2": "Product: UHJvZHVjdDo5" } } }, @@ -39,7 +39,7 @@ response: { "node": { "product": { - "subgraph2": "Product: UHJvZHVjdDo5" + "subgraph2": "Product: UHJvZHVjdDo3" } } } @@ -110,7 +110,7 @@ sourceSchemas: "node": { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo3" + "id": "UHJvZHVjdDo5" } } }, @@ -126,7 +126,7 @@ sourceSchemas: "node": { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo5" + "id": "UHJvZHVjdDo3" } } } @@ -166,13 +166,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDo3" + "__fusion_2_id": "UHJvZHVjdDo5" }, { "__fusion_2_id": "UHJvZHVjdDo4" }, { - "__fusion_2_id": "UHJvZHVjdDo5" + "__fusion_2_id": "UHJvZHVjdDo3" } ] response: @@ -182,7 +182,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo3" + "subgraph2": "Product: UHJvZHVjdDo5" } } } @@ -198,7 +198,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo5" + "subgraph2": "Product: UHJvZHVjdDo3" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml index b171af537c8..508c04a09c0 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml @@ -25,7 +25,7 @@ response: { "node": { "product": { - "subgraph2": "Product: UHJvZHVjdDo3" + "subgraph2": "Product: UHJvZHVjdDo5" } } }, @@ -39,7 +39,7 @@ response: { "node": { "product": { - "subgraph2": "Product: UHJvZHVjdDo5" + "subgraph2": "Product: UHJvZHVjdDo3" } } } @@ -106,7 +106,7 @@ sourceSchemas: "node": { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo3" + "id": "UHJvZHVjdDo5" } } }, @@ -122,7 +122,7 @@ sourceSchemas: "node": { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo5" + "id": "UHJvZHVjdDo3" } } } @@ -156,13 +156,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDo3" + "__fusion_2_id": "UHJvZHVjdDo5" }, { "__fusion_2_id": "UHJvZHVjdDo4" }, { - "__fusion_2_id": "UHJvZHVjdDo5" + "__fusion_2_id": "UHJvZHVjdDo3" } ] response: @@ -172,7 +172,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo3" + "subgraph2": "Product: UHJvZHVjdDo5" } } } @@ -188,7 +188,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo5" + "subgraph2": "Product: UHJvZHVjdDo3" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Has_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Has_Dependency.yaml index 942dc8e61e7..e6a01bf5f88 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Has_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Has_Dependency.yaml @@ -21,13 +21,13 @@ response: { "posts": [ { - "subgraph2": "Photo: UGhvdG86MTA=" + "subgraph2": "Photo: UGhvdG86NA==" }, { - "subgraph1": "Discussion: RGlzY3Vzc2lvbjoxMQ==" + "subgraph1": "Discussion: RGlzY3Vzc2lvbjo1" }, { - "subgraph2": "Photo: UGhvdG86MTI=" + "subgraph2": "Photo: UGhvdG86Ng==" } ] }, @@ -47,13 +47,13 @@ response: { "posts": [ { - "subgraph2": "Photo: UGhvdG86NA==" + "subgraph2": "Photo: UGhvdG86MTA=" }, { - "subgraph1": "Discussion: RGlzY3Vzc2lvbjo1" + "subgraph1": "Discussion: RGlzY3Vzc2lvbjoxMQ==" }, { - "subgraph2": "Photo: UGhvdG86Ng==" + "subgraph2": "Photo: UGhvdG86MTI=" } ] } @@ -111,15 +111,15 @@ sourceSchemas: "posts": [ { "__typename": "Photo", - "id": "UGhvdG86MTA=" + "id": "UGhvdG86NA==" }, { "__typename": "Discussion", - "subgraph1": "Discussion: RGlzY3Vzc2lvbjoxMQ==" + "subgraph1": "Discussion: RGlzY3Vzc2lvbjo1" }, { "__typename": "Photo", - "id": "UGhvdG86MTI=" + "id": "UGhvdG86Ng==" } ] }, @@ -143,15 +143,15 @@ sourceSchemas: "posts": [ { "__typename": "Photo", - "id": "UGhvdG86NA==" + "id": "UGhvdG86MTA=" }, { "__typename": "Discussion", - "subgraph1": "Discussion: RGlzY3Vzc2lvbjo1" + "subgraph1": "Discussion: RGlzY3Vzc2lvbjoxMQ==" }, { "__typename": "Photo", - "id": "UGhvdG86Ng==" + "id": "UGhvdG86MTI=" } ] } @@ -185,10 +185,10 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "UGhvdG86MTA=" + "__fusion_1_id": "UGhvdG86NA==" }, { - "__fusion_1_id": "UGhvdG86MTI=" + "__fusion_1_id": "UGhvdG86Ng==" }, { "__fusion_1_id": "UGhvdG86Nw==" @@ -197,10 +197,10 @@ sourceSchemas: "__fusion_1_id": "UGhvdG86OQ==" }, { - "__fusion_1_id": "UGhvdG86NA==" + "__fusion_1_id": "UGhvdG86MTA=" }, { - "__fusion_1_id": "UGhvdG86Ng==" + "__fusion_1_id": "UGhvdG86MTI=" } ] response: @@ -210,7 +210,7 @@ sourceSchemas: { "data": { "photoById": { - "subgraph2": "Photo: UGhvdG86MTA=" + "subgraph2": "Photo: UGhvdG86NA==" } } } @@ -218,7 +218,7 @@ sourceSchemas: { "data": { "photoById": { - "subgraph2": "Photo: UGhvdG86MTI=" + "subgraph2": "Photo: UGhvdG86Ng==" } } } @@ -242,7 +242,7 @@ sourceSchemas: { "data": { "photoById": { - "subgraph2": "Photo: UGhvdG86NA==" + "subgraph2": "Photo: UGhvdG86MTA=" } } } @@ -250,7 +250,7 @@ sourceSchemas: { "data": { "photoById": { - "subgraph2": "Photo: UGhvdG86Ng==" + "subgraph2": "Photo: UGhvdG86MTI=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selection_Has_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selection_Has_Dependency.yaml index cd113aee09d..ee6ef700286 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selection_Has_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selection_Has_Dependency.yaml @@ -26,17 +26,17 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoxNQ==" + "subgraph2": "Product: UHJvZHVjdDoxOQ==" } }, { "author": { - "subgraph3": "Author: QXV0aG9yOjE0" + "subgraph3": "Author: QXV0aG9yOjIw" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxMw==" + "subgraph2": "Product: UHJvZHVjdDoyMQ==" } } ] @@ -45,7 +45,7 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoxOA==" + "subgraph2": "Product: UHJvZHVjdDoxNg==" } }, { @@ -55,7 +55,7 @@ response: }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxNg==" + "subgraph2": "Product: UHJvZHVjdDoxOA==" } } ] @@ -64,17 +64,17 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoyMQ==" + "subgraph2": "Product: UHJvZHVjdDoxMw==" } }, { "author": { - "subgraph3": "Author: QXV0aG9yOjIw" + "subgraph3": "Author: QXV0aG9yOjE0" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxOQ==" + "subgraph2": "Product: UHJvZHVjdDoxNQ==" } } ] @@ -147,19 +147,19 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxNQ==" + "id": "UHJvZHVjdDoxOQ==" } }, { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjE0" + "id": "QXV0aG9yOjIw" } }, { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxMw==" + "id": "UHJvZHVjdDoyMQ==" } } ] @@ -169,7 +169,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxOA==" + "id": "UHJvZHVjdDoxNg==" } }, { @@ -181,7 +181,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxNg==" + "id": "UHJvZHVjdDoxOA==" } } ] @@ -191,19 +191,19 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoyMQ==" + "id": "UHJvZHVjdDoxMw==" } }, { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjIw" + "id": "QXV0aG9yOjE0" } }, { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxOQ==" + "id": "UHJvZHVjdDoxNQ==" } } ] @@ -238,22 +238,22 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDoxNQ==" + "__fusion_2_id": "UHJvZHVjdDoxOQ==" }, { - "__fusion_2_id": "UHJvZHVjdDoxMw==" + "__fusion_2_id": "UHJvZHVjdDoyMQ==" }, { - "__fusion_2_id": "UHJvZHVjdDoxOA==" + "__fusion_2_id": "UHJvZHVjdDoxNg==" }, { - "__fusion_2_id": "UHJvZHVjdDoxNg==" + "__fusion_2_id": "UHJvZHVjdDoxOA==" }, { - "__fusion_2_id": "UHJvZHVjdDoyMQ==" + "__fusion_2_id": "UHJvZHVjdDoxMw==" }, { - "__fusion_2_id": "UHJvZHVjdDoxOQ==" + "__fusion_2_id": "UHJvZHVjdDoxNQ==" } ] response: @@ -263,7 +263,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxNQ==" + "subgraph2": "Product: UHJvZHVjdDoxOQ==" } } } @@ -271,7 +271,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxMw==" + "subgraph2": "Product: UHJvZHVjdDoyMQ==" } } } @@ -279,7 +279,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxOA==" + "subgraph2": "Product: UHJvZHVjdDoxNg==" } } } @@ -287,7 +287,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxNg==" + "subgraph2": "Product: UHJvZHVjdDoxOA==" } } } @@ -295,7 +295,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoyMQ==" + "subgraph2": "Product: UHJvZHVjdDoxMw==" } } } @@ -303,7 +303,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxOQ==" + "subgraph2": "Product: UHJvZHVjdDoxNQ==" } } } @@ -334,13 +334,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjE0" + "__fusion_1_id": "QXV0aG9yOjIw" }, { "__fusion_1_id": "QXV0aG9yOjE3" }, { - "__fusion_1_id": "QXV0aG9yOjIw" + "__fusion_1_id": "QXV0aG9yOjE0" } ] response: @@ -350,7 +350,7 @@ sourceSchemas: { "data": { "authorById": { - "subgraph3": "Author: QXV0aG9yOjE0" + "subgraph3": "Author: QXV0aG9yOjIw" } } } @@ -366,7 +366,7 @@ sourceSchemas: { "data": { "authorById": { - "subgraph3": "Author: QXV0aG9yOjIw" + "subgraph3": "Author: QXV0aG9yOjE0" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml index a5f9bd8d442..fe811549b47 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml @@ -26,17 +26,17 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoxNQ==" + "subgraph2": "Product: UHJvZHVjdDoxOQ==" } }, { "author": { - "subgraph2": "Author: QXV0aG9yOjE0" + "subgraph2": "Author: QXV0aG9yOjIw" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxMw==" + "subgraph2": "Product: UHJvZHVjdDoyMQ==" } } ] @@ -45,7 +45,7 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoxOA==" + "subgraph2": "Product: UHJvZHVjdDoxNg==" } }, { @@ -55,7 +55,7 @@ response: }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxNg==" + "subgraph2": "Product: UHJvZHVjdDoxOA==" } } ] @@ -64,17 +64,17 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoyMQ==" + "subgraph2": "Product: UHJvZHVjdDoxMw==" } }, { "author": { - "subgraph2": "Author: QXV0aG9yOjIw" + "subgraph2": "Author: QXV0aG9yOjE0" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxOQ==" + "subgraph2": "Product: UHJvZHVjdDoxNQ==" } } ] @@ -147,19 +147,19 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxNQ==" + "id": "UHJvZHVjdDoxOQ==" } }, { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjE0" + "id": "QXV0aG9yOjIw" } }, { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxMw==" + "id": "UHJvZHVjdDoyMQ==" } } ] @@ -169,7 +169,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxOA==" + "id": "UHJvZHVjdDoxNg==" } }, { @@ -181,7 +181,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxNg==" + "id": "UHJvZHVjdDoxOA==" } } ] @@ -191,19 +191,19 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoyMQ==" + "id": "UHJvZHVjdDoxMw==" } }, { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjIw" + "id": "QXV0aG9yOjE0" } }, { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxOQ==" + "id": "UHJvZHVjdDoxNQ==" } } ] @@ -244,13 +244,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjE0" + "__fusion_1_id": "QXV0aG9yOjIw" }, { "__fusion_1_id": "QXV0aG9yOjE3" }, { - "__fusion_1_id": "QXV0aG9yOjIw" + "__fusion_1_id": "QXV0aG9yOjE0" } ] response: @@ -260,7 +260,7 @@ sourceSchemas: { "data": { "authorById": { - "subgraph2": "Author: QXV0aG9yOjE0" + "subgraph2": "Author: QXV0aG9yOjIw" } } } @@ -276,7 +276,7 @@ sourceSchemas: { "data": { "authorById": { - "subgraph2": "Author: QXV0aG9yOjIw" + "subgraph2": "Author: QXV0aG9yOjE0" } } } @@ -292,22 +292,22 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDoxNQ==" + "__fusion_2_id": "UHJvZHVjdDoxOQ==" }, { - "__fusion_2_id": "UHJvZHVjdDoxMw==" + "__fusion_2_id": "UHJvZHVjdDoyMQ==" }, { - "__fusion_2_id": "UHJvZHVjdDoxOA==" + "__fusion_2_id": "UHJvZHVjdDoxNg==" }, { - "__fusion_2_id": "UHJvZHVjdDoxNg==" + "__fusion_2_id": "UHJvZHVjdDoxOA==" }, { - "__fusion_2_id": "UHJvZHVjdDoyMQ==" + "__fusion_2_id": "UHJvZHVjdDoxMw==" }, { - "__fusion_2_id": "UHJvZHVjdDoxOQ==" + "__fusion_2_id": "UHJvZHVjdDoxNQ==" } ] response: @@ -317,7 +317,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxNQ==" + "subgraph2": "Product: UHJvZHVjdDoxOQ==" } } } @@ -325,7 +325,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxMw==" + "subgraph2": "Product: UHJvZHVjdDoyMQ==" } } } @@ -333,7 +333,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxOA==" + "subgraph2": "Product: UHJvZHVjdDoxNg==" } } } @@ -341,7 +341,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxNg==" + "subgraph2": "Product: UHJvZHVjdDoxOA==" } } } @@ -349,7 +349,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoyMQ==" + "subgraph2": "Product: UHJvZHVjdDoxMw==" } } } @@ -357,7 +357,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxOQ==" + "subgraph2": "Product: UHJvZHVjdDoxNQ==" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml index 04ec7d0e74b..926852c7db3 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml @@ -26,17 +26,17 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoxNQ==" + "subgraph2": "Product: UHJvZHVjdDoxOQ==" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxNA==" + "subgraph2": "Product: UHJvZHVjdDoyMA==" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxMw==" + "subgraph2": "Product: UHJvZHVjdDoyMQ==" } } ] @@ -45,7 +45,7 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoxOA==" + "subgraph2": "Product: UHJvZHVjdDoxNg==" } }, { @@ -55,7 +55,7 @@ response: }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxNg==" + "subgraph2": "Product: UHJvZHVjdDoxOA==" } } ] @@ -64,17 +64,17 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoyMQ==" + "subgraph2": "Product: UHJvZHVjdDoxMw==" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoyMA==" + "subgraph2": "Product: UHJvZHVjdDoxNA==" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxOQ==" + "subgraph2": "Product: UHJvZHVjdDoxNQ==" } } ] @@ -143,19 +143,19 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxNQ==" + "id": "UHJvZHVjdDoxOQ==" } }, { "__typename": "Discussion", "product": { - "id": "UHJvZHVjdDoxNA==" + "id": "UHJvZHVjdDoyMA==" } }, { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxMw==" + "id": "UHJvZHVjdDoyMQ==" } } ] @@ -165,7 +165,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxOA==" + "id": "UHJvZHVjdDoxNg==" } }, { @@ -177,7 +177,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxNg==" + "id": "UHJvZHVjdDoxOA==" } } ] @@ -187,19 +187,19 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoyMQ==" + "id": "UHJvZHVjdDoxMw==" } }, { "__typename": "Discussion", "product": { - "id": "UHJvZHVjdDoyMA==" + "id": "UHJvZHVjdDoxNA==" } }, { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxOQ==" + "id": "UHJvZHVjdDoxNQ==" } } ] @@ -234,13 +234,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "UHJvZHVjdDoxNA==" + "__fusion_1_id": "UHJvZHVjdDoyMA==" }, { "__fusion_1_id": "UHJvZHVjdDoxNw==" }, { - "__fusion_1_id": "UHJvZHVjdDoyMA==" + "__fusion_1_id": "UHJvZHVjdDoxNA==" } ] response: @@ -250,7 +250,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxNA==" + "subgraph2": "Product: UHJvZHVjdDoyMA==" } } } @@ -266,7 +266,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoyMA==" + "subgraph2": "Product: UHJvZHVjdDoxNA==" } } } @@ -282,22 +282,22 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDoxNQ==" + "__fusion_2_id": "UHJvZHVjdDoxOQ==" }, { - "__fusion_2_id": "UHJvZHVjdDoxMw==" + "__fusion_2_id": "UHJvZHVjdDoyMQ==" }, { - "__fusion_2_id": "UHJvZHVjdDoxOA==" + "__fusion_2_id": "UHJvZHVjdDoxNg==" }, { - "__fusion_2_id": "UHJvZHVjdDoxNg==" + "__fusion_2_id": "UHJvZHVjdDoxOA==" }, { - "__fusion_2_id": "UHJvZHVjdDoyMQ==" + "__fusion_2_id": "UHJvZHVjdDoxMw==" }, { - "__fusion_2_id": "UHJvZHVjdDoxOQ==" + "__fusion_2_id": "UHJvZHVjdDoxNQ==" } ] response: @@ -307,7 +307,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxNQ==" + "subgraph2": "Product: UHJvZHVjdDoxOQ==" } } } @@ -315,7 +315,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxMw==" + "subgraph2": "Product: UHJvZHVjdDoyMQ==" } } } @@ -323,7 +323,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxOA==" + "subgraph2": "Product: UHJvZHVjdDoxNg==" } } } @@ -331,7 +331,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxNg==" + "subgraph2": "Product: UHJvZHVjdDoxOA==" } } } @@ -339,7 +339,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoyMQ==" + "subgraph2": "Product: UHJvZHVjdDoxMw==" } } } @@ -347,7 +347,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxOQ==" + "subgraph2": "Product: UHJvZHVjdDoxNQ==" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selection_Has_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selection_Has_Dependency.yaml index aa191b84b34..4e501bdb5de 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selection_Has_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selection_Has_Dependency.yaml @@ -22,7 +22,7 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDo2" + "subgraph2": "Product: UHJvZHVjdDo0" } }, { @@ -32,7 +32,7 @@ response: }, { "product": { - "subgraph2": "Product: UHJvZHVjdDo0" + "subgraph2": "Product: UHJvZHVjdDo2" } } ] @@ -95,7 +95,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo2" + "id": "UHJvZHVjdDo0" } }, { @@ -107,7 +107,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo0" + "id": "UHJvZHVjdDo2" } } ] @@ -140,10 +140,10 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDo2" + "__fusion_2_id": "UHJvZHVjdDo0" }, { - "__fusion_2_id": "UHJvZHVjdDo0" + "__fusion_2_id": "UHJvZHVjdDo2" } ] response: @@ -153,7 +153,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo2" + "subgraph2": "Product: UHJvZHVjdDo0" } } } @@ -161,7 +161,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo0" + "subgraph2": "Product: UHJvZHVjdDo2" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml index 6aa59471114..b3b483c5898 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml @@ -22,7 +22,7 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDo2" + "subgraph2": "Product: UHJvZHVjdDo0" } }, { @@ -32,7 +32,7 @@ response: }, { "product": { - "subgraph2": "Product: UHJvZHVjdDo0" + "subgraph2": "Product: UHJvZHVjdDo2" } } ] @@ -95,7 +95,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo2" + "id": "UHJvZHVjdDo0" } }, { @@ -107,7 +107,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo0" + "id": "UHJvZHVjdDo2" } } ] @@ -169,10 +169,10 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDo2" + "__fusion_2_id": "UHJvZHVjdDo0" }, { - "__fusion_2_id": "UHJvZHVjdDo0" + "__fusion_2_id": "UHJvZHVjdDo2" } ] response: @@ -182,7 +182,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo2" + "subgraph2": "Product: UHJvZHVjdDo0" } } } @@ -190,7 +190,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo0" + "subgraph2": "Product: UHJvZHVjdDo2" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml index d28531ba1e0..5c83b3b995d 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml @@ -22,7 +22,7 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDo2" + "subgraph2": "Product: UHJvZHVjdDo0" } }, { @@ -32,7 +32,7 @@ response: }, { "product": { - "subgraph2": "Product: UHJvZHVjdDo0" + "subgraph2": "Product: UHJvZHVjdDo2" } } ] @@ -91,7 +91,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo2" + "id": "UHJvZHVjdDo0" } }, { @@ -103,7 +103,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo0" + "id": "UHJvZHVjdDo2" } } ] @@ -159,10 +159,10 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDo2" + "__fusion_2_id": "UHJvZHVjdDo0" }, { - "__fusion_2_id": "UHJvZHVjdDo0" + "__fusion_2_id": "UHJvZHVjdDo2" } ] response: @@ -172,7 +172,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo2" + "subgraph2": "Product: UHJvZHVjdDo0" } } } @@ -180,7 +180,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo0" + "subgraph2": "Product: UHJvZHVjdDo2" } } } diff --git a/src/HotChocolate/Raven/test/Data.Raven.Projections.Tests/__snapshots__/QueryableProjectionFilterTests.Should_NotInitializeObject_When_ResultOfLeftJoinIsNull_Deep.snap b/src/HotChocolate/Raven/test/Data.Raven.Projections.Tests/__snapshots__/QueryableProjectionFilterTests.Should_NotInitializeObject_When_ResultOfLeftJoinIsNull_Deep.snap index db52a3a7a25..bee18c4f138 100644 --- a/src/HotChocolate/Raven/test/Data.Raven.Projections.Tests/__snapshots__/QueryableProjectionFilterTests.Should_NotInitializeObject_When_ResultOfLeftJoinIsNull_Deep.snap +++ b/src/HotChocolate/Raven/test/Data.Raven.Projections.Tests/__snapshots__/QueryableProjectionFilterTests.Should_NotInitializeObject_When_ResultOfLeftJoinIsNull_Deep.snap @@ -4,15 +4,9 @@ Result: "errors": [ { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 9, - "column": 41 - } - ], "path": [ "root", - 2, + 1, "foo", "nestedObject", "foo", @@ -24,15 +18,9 @@ Result: }, { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 9, - "column": 41 - } - ], "path": [ "root", - 1, + 2, "foo", "nestedObject", "foo", From 156939af839a065adcbd57d39af8031f420745ca Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 09:40:15 +0100 Subject: [PATCH 24/46] Fixed more tests --- .../test/Types.MongoDb/BsonTypeTests.cs | 63 ++++++++++++++----- ....Input_Value_BsonDocument_As_Variable.snap | 4 +- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/HotChocolate/MongoDb/test/Types.MongoDb/BsonTypeTests.cs b/src/HotChocolate/MongoDb/test/Types.MongoDb/BsonTypeTests.cs index 3483cec95f7..586bc0acf25 100644 --- a/src/HotChocolate/MongoDb/test/Types.MongoDb/BsonTypeTests.cs +++ b/src/HotChocolate/MongoDb/test/Types.MongoDb/BsonTypeTests.cs @@ -561,10 +561,11 @@ public async Task Input_Value_List_As_Variable() OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") .SetVariableValues( - new Dictionary + """ { - { "foo", new List { "abc" } } - }) + "foo": ["abc"] + } + """) .Build()); // assert @@ -592,13 +593,15 @@ public async Task Input_Object_List_As_Variable() OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") .SetVariableValues( - new Dictionary + """ { + "foo": [ { - "foo", - new List { new Dictionary { { "abc", "def" } } } + "abc": "def" } - }) + ] + } + """) .Build()); // assert @@ -625,7 +628,12 @@ public async Task Input_Value_String_As_Variable() var result = await executor.ExecuteAsync( OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") - .SetVariableValues(new Dictionary { { "foo", "bar" } }) + .SetVariableValues( + """ + { + "foo": "bar" + } + """) .Build()); // assert @@ -652,7 +660,12 @@ public async Task Input_Value_Int_As_Variable() var result = await executor.ExecuteAsync( OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") - .SetVariableValues(new Dictionary { { "foo", 123 } }) + .SetVariableValues( + """ + { + "foo": 123 + } + """) .Build()); // assert @@ -679,7 +692,12 @@ public async Task Input_Value_Float_As_Variable() var result = await executor.ExecuteAsync( OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") - .SetVariableValues(new Dictionary { { "foo", 1.2 } }) + .SetVariableValues( + """ + { + "foo": 1.2 + } + """) .Build()); // assert @@ -697,7 +715,7 @@ public async Task Input_Value_BsonDocument_As_Variable() .Field("foo") .Type() .Argument("input", a => a.Type()) - .Resolve(ctx => ctx.ArgumentLiteral("input"))) + .Resolve(ctx => ctx.ArgumentValue("input"))) .Create(); var executor = schema.MakeExecutable(); @@ -707,10 +725,13 @@ public async Task Input_Value_BsonDocument_As_Variable() OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") .SetVariableValues( - new Dictionary + """ { - { "foo", new BsonDocument { { "a", "b" } } } - }) + "foo": { + "a": "b" + } + } + """) .Build()); // assert @@ -737,7 +758,12 @@ public async Task Input_Value_Boolean_As_Variable() var result = await executor.ExecuteAsync( OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") - .SetVariableValues(new Dictionary { { "foo", false } }) + .SetVariableValues( + """ + { + "foo": false + } + """) .Build()); // assert @@ -764,7 +790,12 @@ public async Task Input_Value_Null_As_Variable() var result = await executor.ExecuteAsync( OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") - .SetVariableValues(new Dictionary { { "foo", null } }) + .SetVariableValues( + """ + { + "foo": null + } + """) .Build()); // assert diff --git a/src/HotChocolate/MongoDb/test/Types.MongoDb/__snapshots__/BsonTypeTests.Input_Value_BsonDocument_As_Variable.snap b/src/HotChocolate/MongoDb/test/Types.MongoDb/__snapshots__/BsonTypeTests.Input_Value_BsonDocument_As_Variable.snap index 5c8471d7832..a01deda1f64 100644 --- a/src/HotChocolate/MongoDb/test/Types.MongoDb/__snapshots__/BsonTypeTests.Input_Value_BsonDocument_As_Variable.snap +++ b/src/HotChocolate/MongoDb/test/Types.MongoDb/__snapshots__/BsonTypeTests.Input_Value_BsonDocument_As_Variable.snap @@ -1,5 +1,7 @@ { "data": { - "foo": "{ \"a\" : \"b\" }" + "foo": { + "a": "b" + } } } From 3bbe1330f4236b9d433a6db66ed104cdcb6e88e2 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 09:52:04 +0100 Subject: [PATCH 25/46] fixed more tests --- .../Pipeline/OperationExecutionMiddleware.cs | 26 ++++++++++++++++--- ...tTypeTests.Input_Infer_Default_Values.snap | 10 +++++-- ...nputParserTests.OneOf_A_and_B_Are_Set.snap | 25 +++++------------- ...Tests.OneOf_A_is_Null_and_B_has_Value.snap | 25 +++++------------- ...FieldDefaultValue_SerializesCorrectly.snap | 4 ++- ....Apply_SemanticNonNull_To_SchemaFirst.snap | 4 ++- ...Derive_SemanticNonNull_From_CodeFirst.snap | 4 ++- ...anticNonNull_From_ImplementationFirst.snap | 4 ++- ...ationFirst_With_GraphQLType_As_String.snap | 4 ++- ...ntationFirst_With_GraphQLType_As_Type.snap | 4 ++- ...cNonNullTests.Interface_With_Id_Field.snap | 4 ++- ...anticNonNullTests.MutationConventions.snap | 4 ++- ...nticNonNullTests.Object_With_Id_Field.snap | 4 ++- .../SemanticNonNullTests.Pagination.snap | 4 ++- 14 files changed, 74 insertions(+), 52 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs b/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs index f90776873bd..6f7306d8cf5 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs @@ -261,18 +261,36 @@ private async Task ExecuteQueryOrMutationAsync( } private object? GetQueryRootValue(RequestContext context) - => RootValueResolver.Resolve( + { + var queryType = context.Schema.QueryType; + + if (queryType is null) + { + return null; + } + + return RootValueResolver.Resolve( context, context.RequestServices, - Unsafe.As(context.Schema.QueryType), + Unsafe.As(queryType), ref _cachedQuery); + } private object? GetMutationRootValue(RequestContext context) - => RootValueResolver.Resolve( + { + var mutationType = context.Schema.MutationType; + + if (mutationType is null) + { + return null; + } + + return RootValueResolver.Resolve( context, context.RequestServices, - Unsafe.As(context.Schema.MutationType)!, + Unsafe.As(ref mutationType), ref _cachedMutation); + } private static bool IsOperationAllowed(Operation operation, IOperationRequest request) { diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputObjectTypeTests.Input_Infer_Default_Values.snap b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputObjectTypeTests.Input_Infer_Default_Values.snap index 68787826546..bd380c9f2a0 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputObjectTypeTests.Input_Infer_Default_Values.snap +++ b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputObjectTypeTests.Input_Infer_Default_Values.snap @@ -1,4 +1,4 @@ -schema { +schema { query: Query } @@ -14,7 +14,13 @@ input InputWithDefaultInput { withStringDefault: String = "abc" withNullDefault: String enum: FooEnum! = BAR - complexInput: [[ComplexInput!]!]! = [ [ { foo: 1 } ] ] + complexInput: [[ComplexInput!]!]! = [ + [ + { + foo: 1 + } + ] + ] withoutDefault: String } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_and_B_Are_Set.snap b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_and_B_Are_Set.snap index 11ad4beb419..e97a024ab58 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_and_B_Are_Set.snap +++ b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_and_B_Are_Set.snap @@ -1,22 +1,11 @@ -[ +"errors": [ { - "Message": "More than one field of the OneOf Input Object `OneOfInput` is set. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", - "Code": "HC0055", - "Path": null, - "Locations": null, - "Extensions": { + "message": "More than one field of the OneOf Input Object `OneOfInput` is set. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", + "extensions": { "code": "HC0055", - "inputPath": { - "Name": "root", - "Parent": { - "Parent": null, - "Length": 0, - "IsRoot": true - }, - "Length": 1, - "IsRoot": false - } - }, - "Exception": null + "inputPath": [ + "root" + ] + } } ] diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_is_Null_and_B_has_Value.snap b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_is_Null_and_B_has_Value.snap index 11ad4beb419..e97a024ab58 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_is_Null_and_B_has_Value.snap +++ b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_is_Null_and_B_has_Value.snap @@ -1,22 +1,11 @@ -[ +"errors": [ { - "Message": "More than one field of the OneOf Input Object `OneOfInput` is set. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", - "Code": "HC0055", - "Path": null, - "Locations": null, - "Extensions": { + "message": "More than one field of the OneOf Input Object `OneOfInput` is set. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", + "extensions": { "code": "HC0055", - "inputPath": { - "Name": "root", - "Parent": { - "Parent": null, - "Length": 0, - "IsRoot": true - }, - "Length": 1, - "IsRoot": false - } - }, - "Exception": null + "inputPath": [ + "root" + ] + } } ] diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.ObjectType_FieldDefaultValue_SerializesCorrectly.snap b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.ObjectType_FieldDefaultValue_SerializesCorrectly.snap index 39512c2e9d2..a1970befda4 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.ObjectType_FieldDefaultValue_SerializesCorrectly.snap +++ b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.ObjectType_FieldDefaultValue_SerializesCorrectly.snap @@ -3,7 +3,9 @@ schema { } type Bar { - _123(_456: FooInput = { description: "hello" }): String + _123(_456: FooInput = { + description: "hello" + }): String } input FooInput { diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Apply_SemanticNonNull_To_SchemaFirst.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Apply_SemanticNonNull_To_SchemaFirst.snap index 08e902a8348..6f5c17b0637 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Apply_SemanticNonNull_To_SchemaFirst.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Apply_SemanticNonNull_To_SchemaFirst.snap @@ -24,4 +24,6 @@ type Query { innerNonNullObjectNestedArray: [[Foo]] @semanticNonNull(levels: [ 0, 2 ]) } -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_CodeFirst.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_CodeFirst.snap index 08e902a8348..6f5c17b0637 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_CodeFirst.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_CodeFirst.snap @@ -24,4 +24,6 @@ type Query { innerNonNullObjectNestedArray: [[Foo]] @semanticNonNull(levels: [ 0, 2 ]) } -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst.snap index 08e902a8348..6f5c17b0637 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst.snap @@ -24,4 +24,6 @@ type Query { innerNonNullObjectNestedArray: [[Foo]] @semanticNonNull(levels: [ 0, 2 ]) } -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_String.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_String.snap index 08e902a8348..6f5c17b0637 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_String.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_String.snap @@ -24,4 +24,6 @@ type Query { innerNonNullObjectNestedArray: [[Foo]] @semanticNonNull(levels: [ 0, 2 ]) } -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_Type.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_Type.snap index 08e902a8348..6f5c17b0637 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_Type.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_Type.snap @@ -24,4 +24,6 @@ type Query { innerNonNullObjectNestedArray: [[Foo]] @semanticNonNull(levels: [ 0, 2 ]) } -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Interface_With_Id_Field.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Interface_With_Id_Field.snap index ce542824ecb..e570f5dc042 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Interface_With_Id_Field.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Interface_With_Id_Field.snap @@ -32,7 +32,9 @@ a stable key. """ directive @lookup on FIELD_DEFINITION -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION """ By default, only a single source schema is allowed to contribute diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.MutationConventions.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.MutationConventions.snap index 2601f50bd61..bd47c6c8033 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.MutationConventions.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.MutationConventions.snap @@ -22,4 +22,6 @@ type MyError implements Error { union DoSomethingError = MyError -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_With_Id_Field.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_With_Id_Field.snap index 01d6a381ac1..a5c210d73ae 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_With_Id_Field.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_With_Id_Field.snap @@ -10,4 +10,6 @@ type Query { myNode: MyType @semanticNonNull } -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Pagination.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Pagination.snap index 146dc8ea148..e663d5e4be4 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Pagination.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Pagination.snap @@ -53,4 +53,6 @@ type QueryWithPagination { offsetPagination(skip: Int take: Int): OffsetPaginationCollectionSegment } -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION From 0596f91371cac7cde1dcca6b5e75df73f84e9ae8 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 10:01:20 +0100 Subject: [PATCH 26/46] Fixed more tests --- .../Core/src/Validation/ErrorHelper.cs | 2 +- ...getDefinedRuleTests.UndefinedFragment.snap | 29 +++++++------------ ...Tests.NotExistingTypeOnInlineFragment.snap | 29 +++++++------------ ...RuleTests.NotOnExistingTypeOnFragment.snap | 29 +++++++------------ ...ntsMustBeUsedRuleTests.UnusedFragment.snap | 17 +++++------ 5 files changed, 38 insertions(+), 68 deletions(-) diff --git a/src/HotChocolate/Core/src/Validation/ErrorHelper.cs b/src/HotChocolate/Core/src/Validation/ErrorHelper.cs index 216814019e0..447bb2f9934 100644 --- a/src/HotChocolate/Core/src/Validation/ErrorHelper.cs +++ b/src/HotChocolate/Core/src/Validation/ErrorHelper.cs @@ -19,7 +19,7 @@ public static IError DeferAndStreamNotAllowedOnMutationOrSubscriptionRoot( => ErrorBuilder.New() .SetMessage(Resources.ErrorHelper_DeferAndStreamNotAllowedOnMutationOrSubscriptionRoot) .AddLocation(selection) - .SpecifiedBy("sec-Defer-And-Stream-Directives-Are-Used-On-Valid-Root-Field") + .SpecifiedBy("sec-Defer-And-Stream-Directives-Are-Used-On-Valid-Root-Field", rfc: 1110) .Build(); extension(DocumentValidatorContext context) diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTargetDefinedRuleTests.UndefinedFragment.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTargetDefinedRuleTests.UndefinedFragment.snap index 36a5533a7ed..67ec0396494 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTargetDefinedRuleTests.UndefinedFragment.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTargetDefinedRuleTests.UndefinedFragment.snap @@ -1,27 +1,18 @@ -[ +"errors": [ { - "Message": "The specified fragment `undefinedFragment` does not exist.", - "Code": null, - "Path": { - "Name": "dog", - "Parent": { - "Parent": null, - "Length": 0, - "IsRoot": true - }, - "Length": 1, - "IsRoot": false - }, - "Locations": [ + "message": "The specified fragment `undefinedFragment` does not exist.", + "locations": [ { - "Line": 3, - "Column": 9 + "line": 3, + "column": 9 } ], - "Extensions": { + "path": [ + "dog" + ], + "extensions": { "fragment": "undefinedFragment", "specifiedBy": "https://spec.graphql.org/September2025/#sec-Fragment-Spread-Target-Defined" - }, - "Exception": null + } } ] diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotExistingTypeOnInlineFragment.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotExistingTypeOnInlineFragment.snap index b2ed37b87a5..67189828640 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotExistingTypeOnInlineFragment.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotExistingTypeOnInlineFragment.snap @@ -1,27 +1,18 @@ -[ +"errors": [ { - "Message": "Unknown type `NotInSchema`.", - "Code": null, - "Path": { - "Name": "dog", - "Parent": { - "Parent": null, - "Length": 0, - "IsRoot": true - }, - "Length": 1, - "IsRoot": false - }, - "Locations": [ + "message": "Unknown type `NotInSchema`.", + "locations": [ { - "Line": 8, - "Column": 5 + "line": 8, + "column": 5 } ], - "Extensions": { + "path": [ + "dog" + ], + "extensions": { "typeCondition": "NotInSchema", "specifiedBy": "https://spec.graphql.org/September2025/#sec-Fragment-Spread-Type-Existence" - }, - "Exception": null + } } ] diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotOnExistingTypeOnFragment.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotOnExistingTypeOnFragment.snap index 6d553eefef4..8224f40ea91 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotOnExistingTypeOnFragment.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotOnExistingTypeOnFragment.snap @@ -1,28 +1,19 @@ -[ +"errors": [ { - "Message": "Unknown type `NotInSchema`.", - "Code": null, - "Path": { - "Name": "dog", - "Parent": { - "Parent": null, - "Length": 0, - "IsRoot": true - }, - "Length": 1, - "IsRoot": false - }, - "Locations": [ + "message": "Unknown type `NotInSchema`.", + "locations": [ { - "Line": 7, - "Column": 1 + "line": 7, + "column": 1 } ], - "Extensions": { + "path": [ + "dog" + ], + "extensions": { "typeCondition": "NotInSchema", "fragment": "notOnExistingType", "specifiedBy": "https://spec.graphql.org/September2025/#sec-Fragment-Spread-Type-Existence" - }, - "Exception": null + } } ] diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentsMustBeUsedRuleTests.UnusedFragment.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentsMustBeUsedRuleTests.UnusedFragment.snap index a87ed8a4a73..8181f035de9 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentsMustBeUsedRuleTests.UnusedFragment.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentsMustBeUsedRuleTests.UnusedFragment.snap @@ -1,18 +1,15 @@ -[ +"errors": [ { - "Message": "The specified fragment `nameFragment` is not used within the current document.", - "Code": null, - "Path": null, - "Locations": [ + "message": "The specified fragment `nameFragment` is not used within the current document.", + "locations": [ { - "Line": 1, - "Column": 1 + "line": 1, + "column": 1 } ], - "Extensions": { + "extensions": { "fragment": "nameFragment", "specifiedBy": "https://spec.graphql.org/September2025/#sec-Fragments-Must-Be-Used" - }, - "Exception": null + } } ] From 0aa5f34406c33798cf314441326cc9dc178a9068 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 10:02:23 +0100 Subject: [PATCH 27/46] Fixed more tests --- ....IntrospectionNotAllowed_Schema_Field.snap | 28 ++++++------------- ...otAllowed_Schema_Field_Custom_Message.snap | 28 ++++++------------- ...ts.IntrospectionNotAllowed_Type_Field.snap | 28 ++++++------------- ...ectTypeRuleTests.BadIncorrectItemType.snap | 2 +- 4 files changed, 25 insertions(+), 61 deletions(-) diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field.snap index 63403a4069e..f7adec06a8c 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field.snap @@ -1,27 +1,15 @@ -[ +"errors": [ { - "Message": "Introspection is not allowed for the current request.", - "Code": "HC0046", - "Path": null, - "Locations": [ + "message": "Introspection is not allowed for the current request.", + "locations": [ { - "Line": 2, - "Column": 5 + "line": 2, + "column": 5 } ], - "Extensions": { + "extensions": { "code": "HC0046", - "field": { - "Kind": "Name", - "Location": { - "Start": 6, - "End": 16, - "Line": 2, - "Column": 5 - }, - "Value": "__schema" - } - }, - "Exception": null + "field": "__schema" + } } ] diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_Message.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_Message.snap index 873fa5e2ad9..de955b8d46d 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_Message.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_Message.snap @@ -1,27 +1,15 @@ -[ +"errors": [ { - "Message": "Baz", - "Code": "HC0046", - "Path": null, - "Locations": [ + "message": "Baz", + "locations": [ { - "Line": 2, - "Column": 5 + "line": 2, + "column": 5 } ], - "Extensions": { + "extensions": { "code": "HC0046", - "field": { - "Kind": "Name", - "Location": { - "Start": 6, - "End": 16, - "Line": 2, - "Column": 5 - }, - "Value": "__schema" - } - }, - "Exception": null + "field": "__schema" + } } ] diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Type_Field.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Type_Field.snap index e5e677122a1..bf57814ffa5 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Type_Field.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Type_Field.snap @@ -1,27 +1,15 @@ -[ +"errors": [ { - "Message": "Introspection is not allowed for the current request.", - "Code": "HC0046", - "Path": null, - "Locations": [ + "message": "Introspection is not allowed for the current request.", + "locations": [ { - "Line": 2, - "Column": 5 + "line": 2, + "column": 5 } ], - "Extensions": { + "extensions": { "code": "HC0046", - "field": { - "Kind": "Name", - "Location": { - "Start": 6, - "End": 13, - "Line": 2, - "Column": 5 - }, - "Value": "__type" - } - }, - "Exception": null + "field": "__type" + } } ] diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/ValuesOfCorrectTypeRuleTests.BadIncorrectItemType.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/ValuesOfCorrectTypeRuleTests.BadIncorrectItemType.snap index 80428f88102..08ce4804cfb 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/ValuesOfCorrectTypeRuleTests.BadIncorrectItemType.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/ValuesOfCorrectTypeRuleTests.BadIncorrectItemType.snap @@ -12,7 +12,7 @@ ], "extensions": { "argument": "stringListArg", - "argumentValue": "[ \u0022one\u0022, 2 ]", + "argumentValue": "[\n \u0022one\u0022,\n 2\n]", "locationType": "[String]", "specifiedBy": "https://spec.graphql.org/September2025/#sec-Values-of-Correct-Type" } From 54ebfe2599484b5a3890688d4405a47c33decf74 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 10:06:04 +0100 Subject: [PATCH 28/46] Fixed more tests --- src/HotChocolate/Core/src/Validation/ErrorHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HotChocolate/Core/src/Validation/ErrorHelper.cs b/src/HotChocolate/Core/src/Validation/ErrorHelper.cs index 447bb2f9934..08a31c05f2b 100644 --- a/src/HotChocolate/Core/src/Validation/ErrorHelper.cs +++ b/src/HotChocolate/Core/src/Validation/ErrorHelper.cs @@ -183,7 +183,7 @@ public IError ArgumentValueIsNotCompatible( .AddLocation(value) .SetPath(context.CreateErrorPath()) .SetExtension("argument", node.Name.Value) - .SetExtension("argumentValue", value.ToString()) + .SetExtension("argumentValue", value.ToString(indented: false)) .SetExtension("locationType", locationType.FullTypeName()) .SpecifiedBy("sec-Values-of-Correct-Type") .Build(); From 2b42b442c8c25b739bbbc7657572ff28cf130f78 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 10:09:25 +0100 Subject: [PATCH 29/46] fixed snapshot --- .../ValuesOfCorrectTypeRuleTests.BadIncorrectItemType.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/ValuesOfCorrectTypeRuleTests.BadIncorrectItemType.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/ValuesOfCorrectTypeRuleTests.BadIncorrectItemType.snap index 08ce4804cfb..80428f88102 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/ValuesOfCorrectTypeRuleTests.BadIncorrectItemType.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/ValuesOfCorrectTypeRuleTests.BadIncorrectItemType.snap @@ -12,7 +12,7 @@ ], "extensions": { "argument": "stringListArg", - "argumentValue": "[\n \u0022one\u0022,\n 2\n]", + "argumentValue": "[ \u0022one\u0022, 2 ]", "locationType": "[String]", "specifiedBy": "https://spec.graphql.org/September2025/#sec-Values-of-Correct-Type" } From 024a02bac7b3a062e0c8d1b1b778d9707726f28c Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 09:34:31 +0000 Subject: [PATCH 30/46] Fixed Tests --- .../Core/test/Execution.Tests/SchemaFirstTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/HotChocolate/Core/test/Execution.Tests/SchemaFirstTests.cs b/src/HotChocolate/Core/test/Execution.Tests/SchemaFirstTests.cs index 2b4c581ec39..966fe84b147 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/SchemaFirstTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/SchemaFirstTests.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using System.Text.Json; namespace HotChocolate.Execution; @@ -276,9 +277,8 @@ public Task ChangeChannelParametersAsync( ChangeChannelParameterInput input, CancellationToken _) { - var message = Assert.IsType( - Assert.IsType>( - input.ParameterChangeInfo[0].Value)["a"]); + var value = Assert.IsType(input.ParameterChangeInfo[0].Value); + var message = Assert.IsType(value.GetProperty("a").GetString()); return Task.FromResult(new ChangeChannelParameterPayload { Message = message }); } From 3a1ed0e5e51475983811f2394c7d7ab7c79582d3 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 11:13:22 +0000 Subject: [PATCH 31/46] fixed test --- .../src/Types/Types/Scalars/ScalarType.cs | 7 +++++++ .../test/Types.MongoDb/BsonTypeTests.cs | 19 +------------------ 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs index 9b5190bab99..278cd75e403 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs @@ -122,6 +122,8 @@ public bool IsAssignableFrom(ITypeDefinition type) /// public virtual bool IsValueCompatible(IValueNode valueLiteral) { + ArgumentNullException.ThrowIfNull(valueLiteral); + if ((SerializationType & ScalarSerializationType.String) == ScalarSerializationType.String && valueLiteral is { Kind: SyntaxKind.StringValue }) { @@ -164,6 +166,11 @@ public virtual bool IsValueCompatible(IValueNode valueLiteral) /// public virtual bool IsValueCompatible(JsonElement inputValue) { + if (inputValue.ValueKind is JsonValueKind.Undefined) + { + throw new ArgumentException("Undefined JSON value kind.", nameof(inputValue)); + } + if ((SerializationType & ScalarSerializationType.String) == ScalarSerializationType.String && inputValue.ValueKind == JsonValueKind.String) { diff --git a/src/HotChocolate/MongoDb/test/Types.MongoDb/BsonTypeTests.cs b/src/HotChocolate/MongoDb/test/Types.MongoDb/BsonTypeTests.cs index 586bc0acf25..fd5b9214e09 100644 --- a/src/HotChocolate/MongoDb/test/Types.MongoDb/BsonTypeTests.cs +++ b/src/HotChocolate/MongoDb/test/Types.MongoDb/BsonTypeTests.cs @@ -288,23 +288,6 @@ public async Task ValueToLiteral_Should_ReturnNullValueNode_When_CalledWithNull( Assert.IsType(value); } - [Fact] - public async Task ValueToLiteral_Should_ThrowException_When_CalledWithNonBsonValue() - { - // arrange - var type = (await new ServiceCollection() - .AddGraphQL() - .AddBsonType() - .ModifyOptions(x => x.StrictValidation = false) - .BuildSchemaAsync()).Types.GetType("Bson"); - - // act - var result = Record.Exception(() => type.ValueToLiteral("Fails")); - - // assert - Assert.IsType(result); - } - [Fact] public async Task Output_Return_Object() { @@ -983,7 +966,7 @@ public void IsInstanceOfType_NullValue_True() var result = type.IsValueCompatible(NullValueNode.Default); // assert - Assert.True(result); + Assert.False(result); } [Fact] From 207923fb7438de9e40787b1cc40de5f299607ab0 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 12:27:17 +0000 Subject: [PATCH 32/46] Fixed test --- .../Data.NodaTime.Tests/IntegrationTests.cs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/HotChocolate/Data/test/Data.NodaTime.Tests/IntegrationTests.cs b/src/HotChocolate/Data/test/Data.NodaTime.Tests/IntegrationTests.cs index 470ed682943..dd3ec400456 100644 --- a/src/HotChocolate/Data/test/Data.NodaTime.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Data/test/Data.NodaTime.Tests/IntegrationTests.cs @@ -61,20 +61,22 @@ public async Task NodaTime_Paging_Filtering_And_Sorting() """)); // assert - result.ExpectOperationResult().Data.MatchInlineSnapshot( + result.ExpectOperationResult().MatchInlineSnapshot( """ { - "books": { + "data": { + "books": { "nodes": [ - { - "title": "Book2", - "publishedDate": "2008-01-17" - }, - { - "title": "Book1", - "publishedDate": "2008-01-16" - } + { + "title": "Book2", + "publishedDate": "2008-01-17" + }, + { + "title": "Book1", + "publishedDate": "2008-01-16" + } ] + } } } """); From 4ff001abd9ba902621838aa5b3fb6b2015326d9d Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 13:33:14 +0100 Subject: [PATCH 33/46] fixed more tests --- .../Execution/JsonValueFormatter.cs | 38 +++++++++++++++++-- ....Extension_Should_BeMissingMiddleware.snap | 6 --- ...Tests.Extension_Should_BeTypeMismatch.snap | 6 --- ...FilterObjectTwoProjections_Executable.snap | 6 --- ....Extension_Should_BeMissingMiddleware.snap | 6 --- ...Tests.Extension_Should_BeTypeMismatch.snap | 6 --- ....Extension_Should_BeMissingMiddleware.snap | 6 --- ...Tests.Extension_Should_BeTypeMismatch.snap | 6 --- ..._Fail_When_SingleOrDefaultMoreThanOne.snap | 6 --- ...rationTests.Query_Owner_Animals_NET10_0.md | 6 +-- ...grationTests.Query_Owner_Animals_NET8_0.md | 6 +-- ...grationTests.Query_Owner_Animals_NET9_0.md | 6 +-- ...ry_Owner_Animals_With_Fragments_NET10_0.md | 10 ++--- ...ery_Owner_Animals_With_Fragments_NET8_0.md | 10 ++--- ...ery_Owner_Animals_With_Fragments_NET9_0.md | 10 ++--- ...y_Owner_Animals_With_TotalCount_NET10_0.md | 8 ++-- ...ry_Owner_Animals_With_TotalCount_NET8_0.md | 8 ++-- ...ry_Owner_Animals_With_TotalCount_NET9_0.md | 8 ++-- .../InterfaceIntegrationTests.Query_Pets.md | 4 +- ...faceIntegrationTests.Query_Pets_NET10_0.md | 4 +- ...tionTests.Nested_Paging_First_2_NET10_0.md | 2 +- ...ationTests.Nested_Paging_First_2_NET8_0.md | 2 +- ...ationTests.Nested_Paging_First_2_NET9_0.md | 2 +- ...Paging_First_2_With_Projections_NET10_0.md | 2 +- ..._Paging_First_2_With_Projections_NET8_0.md | 2 +- ..._Paging_First_2_With_Projections_NET9_0.md | 2 +- ...ey_To_Collection_Expression_Integration.md | 2 +- ...llection_Expression_Integration_NET10_0.md | 2 +- 28 files changed, 82 insertions(+), 100 deletions(-) diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs index dcc2d849f9b..dd42ed610b7 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs @@ -246,9 +246,12 @@ public static void WriteErrors( writer.WriteStartArray(); - for (var i = 0; i < errors.Count; i++) + // We sort errors by path to ensure a stable output: + // - Errors without paths (null) come first + // - Then errors sorted by path + foreach (var error in errors.OrderBy(e => e.Path, PathComparer.Instance)) { - WriteError(writer, errors[i], options, nullIgnoreCondition); + WriteError(writer, error, options, nullIgnoreCondition); } writer.WriteEndArray(); @@ -438,9 +441,10 @@ private static void WriteLocations(JsonWriter writer, IReadOnlyList? l writer.WriteStartArray(); - for (var i = 0; i < locations.Count; i++) + // We sort locations to ensure a stable output. + foreach (var location in locations.Order()) { - WriteLocation(writer, locations[i]); + WriteLocation(writer, location); } writer.WriteEndArray(); @@ -494,3 +498,29 @@ private static void WritePathValue(JsonWriter writer, Path path) writer.WriteEndArray(); } } + +file sealed class PathComparer : IComparer +{ + public static readonly PathComparer Instance = new(); + + public int Compare(Path? x, Path? y) + { + // Null paths should come first + if (x is null && y is null) + { + return 0; + } + + if (x is null) + { + return -1; + } + + if (y is null) + { + return 1; + } + + return x.CompareTo(y); + } +} diff --git a/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeMissingMiddleware.snap b/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeMissingMiddleware.snap index 7ccd948a1b6..df55ead1466 100644 --- a/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeMissingMiddleware.snap +++ b/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeMissingMiddleware.snap @@ -4,12 +4,6 @@ Result: "errors": [ { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 1, - "column": 3 - } - ], "path": [ "missingMiddleware" ], diff --git a/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeTypeMismatch.snap b/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeTypeMismatch.snap index 5c508686550..0223cd82ecc 100644 --- a/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeTypeMismatch.snap +++ b/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeTypeMismatch.snap @@ -4,12 +4,6 @@ Result: "errors": [ { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 1, - "column": 3 - } - ], "path": [ "typeMismatch" ], diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_Executable.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_Executable.snap index 194b934c7c5..36905a80618 100644 --- a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_Executable.snap +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_Executable.snap @@ -4,12 +4,6 @@ Result: "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 2, - "column": 25 - } - ], "path": [ "rootExecutable" ] diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeMissingMiddleware.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeMissingMiddleware.snap index 97df417ce81..679941092f9 100644 --- a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeMissingMiddleware.snap +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeMissingMiddleware.snap @@ -4,12 +4,6 @@ Result: "errors": [ { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 1, - "column": 3 - } - ], "path": [ "missingMiddleware" ], diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeTypeMismatch.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeTypeMismatch.snap index 9dd61ef1e9b..33277723934 100644 --- a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeTypeMismatch.snap +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeTypeMismatch.snap @@ -4,12 +4,6 @@ Result: "errors": [ { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 1, - "column": 3 - } - ], "path": [ "typeMismatch" ], diff --git a/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeMissingMiddleware.snap b/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeMissingMiddleware.snap index 1f2adad2388..eaf7a51c63b 100644 --- a/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeMissingMiddleware.snap +++ b/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeMissingMiddleware.snap @@ -4,12 +4,6 @@ Result: "errors": [ { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 1, - "column": 3 - } - ], "path": [ "missingMiddleware" ], diff --git a/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeTypeMismatch.snap b/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeTypeMismatch.snap index c5d93a07a31..5bf9e0da3fe 100644 --- a/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeTypeMismatch.snap +++ b/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeTypeMismatch.snap @@ -4,12 +4,6 @@ Result: "errors": [ { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 1, - "column": 3 - } - ], "path": [ "typeMismatch" ], diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.ExecuteAsync_Should_Fail_When_SingleOrDefaultMoreThanOne.snap b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.ExecuteAsync_Should_Fail_When_SingleOrDefaultMoreThanOne.snap index 8863d34f99a..143a6373df9 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.ExecuteAsync_Should_Fail_When_SingleOrDefaultMoreThanOne.snap +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.ExecuteAsync_Should_Fail_When_SingleOrDefaultMoreThanOne.snap @@ -2,12 +2,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 2, - "column": 5 - } - ], "path": [ "executable" ] diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET10_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET10_0.md index 81a944d2170..1e67951d758 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET10_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET10_0.md @@ -19,7 +19,7 @@ LIMIT @p ## SQL 1 ```sql --- @keys={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @keys={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT s."OwnerId", s1.c, s1."Id", s1."Name", s1.c0, s1."Id0" FROM ( SELECT p."OwnerId" @@ -31,7 +31,7 @@ FROM ( LEFT JOIN ( SELECT s0.c, s0."Id", s0."Name", s0.c0, s0."Id0", s0."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Dog' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Cat' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@keys) @@ -44,7 +44,7 @@ ORDER BY s."OwnerId", s1."OwnerId", s1."Name", s1."Id" ## Expression 1 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 5 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET8_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET8_0.md index 3435e62af9d..6d4d3bf7427 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET8_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET8_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT t."OwnerId", t0.c, t0."Id", t0."Name", t0.c0, t0."Id0" FROM ( SELECT p."OwnerId" @@ -31,7 +31,7 @@ FROM ( LEFT JOIN ( SELECT t1.c, t1."Id", t1."Name", t1.c0, t1."Id0", t1."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Dog' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Cat' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@__keys_0) @@ -44,7 +44,7 @@ ORDER BY t."OwnerId", t0."OwnerId", t0."Name", t0."Id" ## Expression 1 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 5 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET9_0.md index 2a43662c793..696537b58fb 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET9_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT s."OwnerId", s1.c, s1."Id", s1."Name", s1.c0, s1."Id0" FROM ( SELECT p."OwnerId" @@ -31,7 +31,7 @@ FROM ( LEFT JOIN ( SELECT s0.c, s0."Id", s0."Name", s0.c0, s0."Id0", s0."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Dog' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Cat' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@__keys_0) @@ -44,7 +44,7 @@ ORDER BY s."OwnerId", s1."OwnerId", s1."Name", s1."Id" ## Expression 1 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 5 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET10_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET10_0.md index 71dc2cf4014..ba99b34dd82 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET10_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET10_0.md @@ -19,8 +19,8 @@ LIMIT @p ## SQL 1 ```sql --- @keys={ '6', '5', '4', '3', '2', ... } (DbType = Object) -SELECT s."OwnerId", s1.c, s1."Id", s1."IsPurring", s1."Name", s1.c0, s1."IsBarking", s1."Id0" +-- @keys={ '1', '2', '3', '4', '5', ... } (DbType = Object) +SELECT s."OwnerId", s1.c, s1."Id", s1."IsBarking", s1."Name", s1.c0, s1."IsPurring", s1."Id0" FROM ( SELECT p."OwnerId" FROM "Owners" AS o @@ -29,9 +29,9 @@ FROM ( GROUP BY p."OwnerId" ) AS s LEFT JOIN ( - SELECT s0.c, s0."Id", s0."IsPurring", s0."Name", s0.c0, s0."IsBarking", s0."Id0", s0."OwnerId" + SELECT s0.c, s0."Id", s0."IsBarking", s0."Name", s0.c0, s0."IsPurring", s0."Id0", s0."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."IsPurring", p0."Name", p0."AnimalType" = 'Dog' AS c0, p0."IsBarking", o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."IsBarking", p0."Name", p0."AnimalType" = 'Cat' AS c0, p0."IsPurring", o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@keys) @@ -44,7 +44,7 @@ ORDER BY s."OwnerId", s1."OwnerId", s1."Name", s1."Id" ## Expression 1 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, IsPurring = Convert(root, Cat).IsPurring, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, IsBarking = Convert(root, Dog).IsBarking, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, IsBarking = Convert(root, Dog).IsBarking, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, IsPurring = Convert(root, Cat).IsPurring, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 5 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET8_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET8_0.md index e60974e32b0..245a0524493 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET8_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET8_0.md @@ -19,8 +19,8 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) -SELECT t."OwnerId", t0.c, t0."Id", t0."IsPurring", t0."Name", t0.c0, t0."IsBarking", t0."Id0" +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) +SELECT t."OwnerId", t0.c, t0."Id", t0."IsBarking", t0."Name", t0.c0, t0."IsPurring", t0."Id0" FROM ( SELECT p."OwnerId" FROM "Owners" AS o @@ -29,9 +29,9 @@ FROM ( GROUP BY p."OwnerId" ) AS t LEFT JOIN ( - SELECT t1.c, t1."Id", t1."IsPurring", t1."Name", t1.c0, t1."IsBarking", t1."Id0", t1."OwnerId" + SELECT t1.c, t1."Id", t1."IsBarking", t1."Name", t1.c0, t1."IsPurring", t1."Id0", t1."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."IsPurring", p0."Name", p0."AnimalType" = 'Dog' AS c0, p0."IsBarking", o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."IsBarking", p0."Name", p0."AnimalType" = 'Cat' AS c0, p0."IsPurring", o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@__keys_0) @@ -44,7 +44,7 @@ ORDER BY t."OwnerId", t0."OwnerId", t0."Name", t0."Id" ## Expression 1 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, IsPurring = Convert(root, Cat).IsPurring, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, IsBarking = Convert(root, Dog).IsBarking, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, IsBarking = Convert(root, Dog).IsBarking, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, IsPurring = Convert(root, Cat).IsPurring, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 5 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET9_0.md index d9b611f6aca..8206a9f2316 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET9_0.md @@ -19,8 +19,8 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) -SELECT s."OwnerId", s1.c, s1."Id", s1."IsPurring", s1."Name", s1.c0, s1."IsBarking", s1."Id0" +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) +SELECT s."OwnerId", s1.c, s1."Id", s1."IsBarking", s1."Name", s1.c0, s1."IsPurring", s1."Id0" FROM ( SELECT p."OwnerId" FROM "Owners" AS o @@ -29,9 +29,9 @@ FROM ( GROUP BY p."OwnerId" ) AS s LEFT JOIN ( - SELECT s0.c, s0."Id", s0."IsPurring", s0."Name", s0.c0, s0."IsBarking", s0."Id0", s0."OwnerId" + SELECT s0.c, s0."Id", s0."IsBarking", s0."Name", s0.c0, s0."IsPurring", s0."Id0", s0."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."IsPurring", p0."Name", p0."AnimalType" = 'Dog' AS c0, p0."IsBarking", o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."IsBarking", p0."Name", p0."AnimalType" = 'Cat' AS c0, p0."IsPurring", o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@__keys_0) @@ -44,7 +44,7 @@ ORDER BY s."OwnerId", s1."OwnerId", s1."Name", s1."Id" ## Expression 1 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, IsPurring = Convert(root, Cat).IsPurring, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, IsBarking = Convert(root, Dog).IsBarking, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, IsBarking = Convert(root, Dog).IsBarking, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, IsPurring = Convert(root, Cat).IsPurring, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 5 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET10_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET10_0.md index 8fe7cdca3a6..6d7576f0086 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET10_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET10_0.md @@ -19,7 +19,7 @@ LIMIT @p ## SQL 1 ```sql --- @keys={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @keys={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT p."OwnerId" AS "Key", count(*)::int AS "Count" FROM "Owners" AS o INNER JOIN "Pets" AS p ON o."Id" = p."OwnerId" @@ -36,7 +36,7 @@ GROUP BY p."OwnerId" ## SQL 2 ```sql --- @keys={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @keys={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT s."OwnerId", s1.c, s1."Id", s1."Name", s1.c0, s1."Id0" FROM ( SELECT p."OwnerId" @@ -48,7 +48,7 @@ FROM ( LEFT JOIN ( SELECT s0.c, s0."Id", s0."Name", s0.c0, s0."Id0", s0."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Dog' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Cat' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@keys) @@ -61,7 +61,7 @@ ORDER BY s."OwnerId", s1."OwnerId", s1."Name", s1."Id" ## Expression 2 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerWithCountDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerWithCountDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 7 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET8_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET8_0.md index 66c568168d1..b446cc1bc3d 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET8_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET8_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT p."OwnerId" AS "Key", count(*)::int AS "Count" FROM "Owners" AS o INNER JOIN "Pets" AS p ON o."Id" = p."OwnerId" @@ -36,7 +36,7 @@ GROUP BY p."OwnerId" ## SQL 2 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT t."OwnerId", t0.c, t0."Id", t0."Name", t0.c0, t0."Id0" FROM ( SELECT p."OwnerId" @@ -48,7 +48,7 @@ FROM ( LEFT JOIN ( SELECT t1.c, t1."Id", t1."Name", t1.c0, t1."Id0", t1."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Dog' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Cat' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@__keys_0) @@ -61,7 +61,7 @@ ORDER BY t."OwnerId", t0."OwnerId", t0."Name", t0."Id" ## Expression 2 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerWithCountDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerWithCountDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 7 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET9_0.md index b21242bc907..e1c975188e5 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET9_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT p."OwnerId" AS "Key", count(*)::int AS "Count" FROM "Owners" AS o INNER JOIN "Pets" AS p ON o."Id" = p."OwnerId" @@ -36,7 +36,7 @@ GROUP BY p."OwnerId" ## SQL 2 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT s."OwnerId", s1.c, s1."Id", s1."Name", s1.c0, s1."Id0" FROM ( SELECT p."OwnerId" @@ -48,7 +48,7 @@ FROM ( LEFT JOIN ( SELECT s0.c, s0."Id", s0."Name", s0.c0, s0."Id0", s0."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Dog' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Cat' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@__keys_0) @@ -61,7 +61,7 @@ ORDER BY s."OwnerId", s1."OwnerId", s1."Name", s1."Id" ## Expression 2 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerWithCountDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerWithCountDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 7 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets.md index 68032883f6d..a138ff6b1c6 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets.md @@ -4,7 +4,7 @@ ```sql -- @__p_0='11' -SELECT p."AnimalType" = 'Cat', p."Id", p."Name", p."AnimalType" = 'Dog' +SELECT p."AnimalType" = 'Dog', p."Id", p."Name", p."AnimalType" = 'Cat' FROM "Pets" AS p ORDER BY p."Name", p."Id" LIMIT @__p_0 @@ -13,7 +13,7 @@ LIMIT @__p_0 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11) ``` ## Result 3 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets_NET10_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets_NET10_0.md index 75c7b042fc4..36067d96eb1 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets_NET10_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets_NET10_0.md @@ -4,7 +4,7 @@ ```sql -- @p='11' -SELECT p."AnimalType" = 'Cat', p."Id", p."Name", p."AnimalType" = 'Dog' +SELECT p."AnimalType" = 'Dog', p."Id", p."Name", p."AnimalType" = 'Cat' FROM "Pets" AS p ORDER BY p."Name", p."Id" LIMIT @p @@ -13,7 +13,7 @@ LIMIT @p ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11) ``` ## Result 3 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET10_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET10_0.md index 2db8db0b122..0ac9a80f9a6 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET10_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET10_0.md @@ -19,7 +19,7 @@ LIMIT @p ## SQL 1 ```sql --- @keys={ '2', '1' } (DbType = Object) +-- @keys={ '1', '2' } (DbType = Object) SELECT p1."BrandId", p3."Id", p3."AvailableStock", p3."BrandId", p3."Description", p3."ImageFileName", p3."MaxStockThreshold", p3."Name", p3."OnReorder", p3."Price", p3."RestockThreshold", p3."TypeId" FROM ( SELECT p."BrandId" diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET8_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET8_0.md index b560f68ce8d..665e255b1cc 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET8_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET8_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '2', '1' } (DbType = Object) +-- @__keys_0={ '1', '2' } (DbType = Object) SELECT t."BrandId", t0."Id", t0."AvailableStock", t0."BrandId", t0."Description", t0."ImageFileName", t0."MaxStockThreshold", t0."Name", t0."OnReorder", t0."Price", t0."RestockThreshold", t0."TypeId" FROM ( SELECT p."BrandId" diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET9_0.md index 409cdb5263b..b1c183a796b 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET9_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '2', '1' } (DbType = Object) +-- @__keys_0={ '1', '2' } (DbType = Object) SELECT p1."BrandId", p3."Id", p3."AvailableStock", p3."BrandId", p3."Description", p3."ImageFileName", p3."MaxStockThreshold", p3."Name", p3."OnReorder", p3."Price", p3."RestockThreshold", p3."TypeId" FROM ( SELECT p."BrandId" diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET10_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET10_0.md index 8332860815f..0054b833968 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET10_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET10_0.md @@ -19,7 +19,7 @@ LIMIT @p ## SQL 1 ```sql --- @keys={ '2', '1' } (DbType = Object) +-- @keys={ '1', '2' } (DbType = Object) SELECT p1."BrandId", p3."Name", p3."BrandId", p3."Id" FROM ( SELECT p."BrandId" diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET8_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET8_0.md index 915df77d43e..72bbe8876c6 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET8_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET8_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '2', '1' } (DbType = Object) +-- @__keys_0={ '1', '2' } (DbType = Object) SELECT t."BrandId", t0."Name", t0."BrandId", t0."Id" FROM ( SELECT p."BrandId" diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET9_0.md index 7f482fb572d..c2f46a23fa2 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET9_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '2', '1' } (DbType = Object) +-- @__keys_0={ '1', '2' } (DbType = Object) SELECT p1."BrandId", p3."Name", p3."BrandId", p3."Id" FROM ( SELECT p."BrandId" diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration.md index 9cb6d640200..2406cb30ad4 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration.md @@ -3,7 +3,7 @@ ## SQL ```text --- @__keys_0={ '2', '1' } (DbType = Object) +-- @__keys_0={ '1', '2' } (DbType = Object) SELECT b."Id", p."Name", p."Id" FROM "Brands" AS b LEFT JOIN "Products" AS p ON b."Id" = p."BrandId" diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration_NET10_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration_NET10_0.md index f738f8cf7b0..d0f477bb9db 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration_NET10_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration_NET10_0.md @@ -3,7 +3,7 @@ ## SQL ```text --- @keys={ '2', '1' } (DbType = Object) +-- @keys={ '1', '2' } (DbType = Object) SELECT b."Id", p."Name", p."Id" FROM "Brands" AS b LEFT JOIN "Products" AS p ON b."Id" = p."BrandId" From 2fca9b38a074a6bb0361c51f601b18da0a6b4578 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 15:01:27 +0100 Subject: [PATCH 34/46] Fixed file upload --- .../src/Types/Types/Scalars/ScalarType.cs | 15 +- .../FusionScalarTypeDefinition.cs | 6 + .../Clients/SourceSchemaHttpClient.cs | 6 +- .../Execution/JsonVariableCoercion.cs | 471 ++++++++++++++++++ .../OperationVariableCoercionMiddleware.cs | 2 + .../Execution/VariableCoercionHelper.cs | 275 +--------- .../FileUploadTests.Upload_List_Of_Files.yaml | 20 +- ....Upload_List_Of_Files_In_Input_Object.yaml | 20 +- ..._List_Of_Files_In_Input_Object_Inline.yaml | 20 +- .../FileUploadTests.Upload_Single_File.yaml | 20 +- ...ts.Upload_Single_File_In_Input_Object.yaml | 20 +- ...ad_Single_File_In_Input_Object_Inline.yaml | 20 +- .../HotChocolate.Language.Utf8.csproj | 1 + .../src/Language.Utf8/Utf8MemoryBuilder.cs | 4 +- .../src/Language.Web/JsonValueParser.cs | 11 +- 15 files changed, 627 insertions(+), 284 deletions(-) create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/JsonVariableCoercion.cs diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs index 278cd75e403..9c034024c51 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs @@ -245,14 +245,19 @@ public virtual IValueNode InputValueToLiteral(JsonElement inputValue, IFeaturePr throw CreateInputValueToLiteralError(inputValue, context); } + // We try to get a memory builder from the context and assign it to our JsonValueParser + // which rewrites the json into a GraphQL value node. + // The memory builder allows us to store the actual values as UTF-8 string. var utf8MemoryBuilder = context.Features.Get(); - var builderExists = utf8MemoryBuilder is not null; + var builderExistedBeforeParsing = utf8MemoryBuilder is not null; - var jsonValueParser = new JsonValueParser(doNotSeal: true); - jsonValueParser._memory = utf8MemoryBuilder; - var literal = jsonValueParser.Parse(inputValue); + var parser = new JsonValueParser(doNotSeal: true) { _memory = utf8MemoryBuilder }; + var literal = parser.Parse(inputValue); - if (!builderExists) + // If no builder existed so far but we now created one by rewriting the JSON value, + // then we store the JSON builder on the context so that it can be picked up and reused by other values + // in the current coercion of input values. + if (!builderExistedBeforeParsing && utf8MemoryBuilder is not null) { context.Features.Set(utf8MemoryBuilder); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionScalarTypeDefinition.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionScalarTypeDefinition.cs index 2ecb60abaa1..287e23ba318 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionScalarTypeDefinition.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionScalarTypeDefinition.cs @@ -32,6 +32,7 @@ public FusionScalarTypeDefinition( Name = name; Description = description; IsInaccessible = isInaccessible; + IsUpload = name.Equals("Upload"); // these properties are initialized // in the type complete step. @@ -65,6 +66,11 @@ public FusionScalarTypeDefinition( /// public bool IsInaccessible { get; } + /// + /// Specifies if this scalar is the file upload scalar. + /// + public bool IsUpload { get; } + /// /// Gets the directives applied to this scalar type. /// diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs index 8da1f9d4d10..6a88341d878 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs @@ -92,14 +92,16 @@ private GraphQLHttpRequest CreateHttpRequest( return new GraphQLHttpRequest(CreateOperationBatchRequest(operationSourceText, originalRequest)) { Uri = _configuration.BaseAddress, - Accept = _configuration.BatchingAcceptHeaderValues + Accept = _configuration.BatchingAcceptHeaderValues, + EnableFileUploads = originalRequest.RequiresFileUpload }; } return new GraphQLHttpRequest(CreateVariableBatchRequest(operationSourceText, originalRequest)) { Uri = _configuration.BaseAddress, - Accept = _configuration.BatchingAcceptHeaderValues + Accept = _configuration.BatchingAcceptHeaderValues, + EnableFileUploads = originalRequest.RequiresFileUpload }; } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/JsonVariableCoercion.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/JsonVariableCoercion.cs new file mode 100644 index 00000000000..8ecd5db0a80 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/JsonVariableCoercion.cs @@ -0,0 +1,471 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using HotChocolate.Buffers; +using HotChocolate.Execution; +using HotChocolate.Features; +using HotChocolate.Fusion.Types; +using HotChocolate.Language; +using HotChocolate.Transport.Http; +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Execution; + +internal ref struct JsonVariableCoercion +{ + private const int MaxAllowedDepth = 64; + private readonly IFeatureProvider _context; + private readonly ref Utf8MemoryBuilder? _memory; + + public JsonVariableCoercion(IFeatureProvider context, ref Utf8MemoryBuilder? memory) + { + _context = context; + _memory = ref memory; + } + + public bool TryCoerceVariableValue( + string variableName, + IInputType variableType, + JsonElement inputValue, + [NotNullWhen(true)] out VariableValue? variableValue, + [NotNullWhen(false)] out IError? error) + { + if (inputValue.ValueKind is JsonValueKind.Undefined) + { + throw new ArgumentException("Undefined JSON value kind."); + } + + var root = Path.Root.Append(variableName); + + try + { + if (TryParseAndValidate(variableType, inputValue, root, 0, out var valueLiteral, out error)) + { + variableValue = new VariableValue(variableName, variableType, valueLiteral); + return true; + } + + variableValue = null; + return false; + } + catch + { + _memory?.Abandon(); + _memory = null; + throw; + } + } + + private bool TryParseAndValidate( + IInputType type, + JsonElement element, + Path path, + int depth, + [NotNullWhen(true)] out IValueNode? value, + [NotNullWhen(false)] out IError? error) + { + if (depth > MaxAllowedDepth) + { + throw new InvalidOperationException("Max allowed depth reached."); + } + + // Handle NonNull types + if (type.Kind is TypeKind.NonNull) + { + if (element.ValueKind is JsonValueKind.Null) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("The value is not a non-null value.") + .SetExtension("variable", $"{path}") + .Build(); + return false; + } + + type = (IInputType)type.InnerType(); + } + + // Handle null values + if (element.ValueKind is JsonValueKind.Null) + { + value = NullValueNode.Default; + error = null; + return true; + } + + // Handle List types + if (type.Kind is TypeKind.List) + { + if (element.ValueKind is not JsonValueKind.Array) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("The value is not a list value.") + .SetExtension("variable", $"{path}") + .Build(); + return false; + } + + var elementType = (IInputType)type.ListType().ElementType; + var buffer = ArrayPool.Shared.Rent(64); + var count = 0; + + try + { + var index = 0; + foreach (var item in element.EnumerateArray()) + { + if (count == buffer.Length) + { + var temp = buffer; + var tempSpan = temp.AsSpan(); + buffer = ArrayPool.Shared.Rent(count * 2); + tempSpan.CopyTo(buffer); + tempSpan.Clear(); + ArrayPool.Shared.Return(temp); + } + + if (!TryParseAndValidate( + elementType, + item, + path.Append(index), + depth + 1, + out var itemValue, + out error)) + { + value = null; + return false; + } + + buffer[count++] = itemValue; + index++; + } + + value = new ListValueNode(buffer.AsSpan(0, count).ToArray()); + error = null; + return true; + } + finally + { + buffer.AsSpan(0, count).Clear(); + ArrayPool.Shared.Return(buffer); + } + } + + // Handle InputObject types + if (type.Kind is TypeKind.InputObject) + { + return TryParseInputObject(type, element, path, depth, out value, out error); + } + + // Handle Scalar types + if (type is FusionScalarTypeDefinition scalarType) + { + return TryParseScalar(scalarType, element, path, out value, out error); + } + + // Handle Enum types + if (type is FusionEnumTypeDefinition enumType) + { + return TryParseEnum(enumType, element, path, out value, out error); + } + + throw new NotSupportedException( + $"The type `{type.FullTypeName()}` is not a valid input type."); + } + + private bool TryParseInputObject( + IInputType type, + JsonElement element, + Path path, + int depth, + [NotNullWhen(true)] out IValueNode? value, + [NotNullWhen(false)] out IError? error) + { + if (element.ValueKind is not JsonValueKind.Object) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("The value is not an object value.") + .SetExtension("variable", $"{path}") + .Build(); + return false; + } + + var inputObjectType = (FusionInputObjectTypeDefinition)type; + var oneOf = inputObjectType.IsOneOf; + + // Count fields first for OneOf validation + var fieldCount = 0; + foreach (var _ in element.EnumerateObject()) + { + fieldCount++; + } + + if (oneOf && fieldCount is 0) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("The OneOf Input Object `{0}` requires that exactly one field is supplied and that field must not be `null`. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", inputObjectType.Name) + .SetCode(ErrorCodes.Execution.OneOfNoFieldSet) + .SetPath(path) + .Build(); + return false; + } + + if (oneOf && fieldCount > 1) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("More than one field of the OneOf Input Object `{0}` is set. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", inputObjectType.Name) + .SetCode(ErrorCodes.Execution.OneOfMoreThanOneFieldSet) + .SetPath(path) + .Build(); + return false; + } + + var numberOfInputFields = inputObjectType.Fields.Count; + var processedCount = 0; + bool[]? processedBuffer = null; + var processed = depth <= 256 && numberOfInputFields <= 32 + ? stackalloc bool[numberOfInputFields] + : processedBuffer = ArrayPool.Shared.Rent(numberOfInputFields); + + if (processedBuffer is not null) + { + processed.Clear(); + } + + var buffer = ArrayPool.Shared.Rent(64); + var count = 0; + + try + { + foreach (var property in element.EnumerateObject()) + { + if (!inputObjectType.Fields.TryGetField(property.Name, out var fieldDefinition)) + { + value = null; + error = ErrorBuilder.New() + .SetMessage( + "The field `{0}` is not defined on the input object type `{1}`.", + property.Name, + inputObjectType.Name) + .SetExtension("variable", $"{path}") + .Build(); + return false; + } + + if (oneOf && property.Value.ValueKind is JsonValueKind.Null) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("`null` was set to the field `{0}` of the OneOf Input Object `{1}`. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", property.Name, inputObjectType.Name) + .SetCode(ErrorCodes.Execution.OneOfFieldIsNull) + .SetPath(path) + .SetCoordinate(fieldDefinition.Coordinate) + .Build(); + return false; + } + + if (count == buffer.Length) + { + var temp = buffer; + var tempSpan = temp.AsSpan(); + buffer = ArrayPool.Shared.Rent(count * 2); + tempSpan.CopyTo(buffer); + tempSpan.Clear(); + ArrayPool.Shared.Return(temp); + } + + if (!TryParseAndValidate( + fieldDefinition.Type, + property.Value, + path.Append(property.Name), + depth + 1, + out var fieldValue, + out error)) + { + value = null; + return false; + } + + buffer[count++] = new ObjectFieldNode(property.Name, fieldValue); + processed[fieldDefinition.Index] = true; + processedCount++; + } + + // Check for missing required fields + if (!oneOf && processedCount != numberOfInputFields) + { + for (var i = 0; i < numberOfInputFields; i++) + { + if (!processed[i]) + { + var field = inputObjectType.Fields[i]; + + if (field.Type.Kind == TypeKind.NonNull && field.DefaultValue is null) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("The required input field `{0}` is missing.", field.Name) + .SetPath(path.Append(field.Name)) + .SetExtension("field", field.Coordinate.ToString()) + .Build(); + return false; + } + } + } + } + + value = new ObjectValueNode(buffer.AsSpan(0, count).ToArray()); + error = null; + return true; + } + finally + { + buffer.AsSpan(0, count).Clear(); + ArrayPool.Shared.Return(buffer); + + if (processedBuffer is not null) + { + ArrayPool.Shared.Return(processedBuffer); + } + } + } + + private readonly bool TryParseScalar( + FusionScalarTypeDefinition scalarType, + JsonElement element, + Path path, + [NotNullWhen(true)] out IValueNode? value, + [NotNullWhen(false)] out IError? error) + { + if (scalarType.IsUpload) + { + if (element.ValueKind is JsonValueKind.String + && element.GetString() is { Length: > 0 } fileKey + && _context.Features.GetRequired().TryGetFile(fileKey, out var file)) + { + value = new FileReferenceNode(file.OpenReadStream, file.Name, file.ContentType); + error = null; + return true; + } + + error = ErrorBuilder.New() + .SetMessage("The value is not a valid file.") + .SetExtension("variable", $"{path}") + .Build(); + value = null; + return false; + } + else + { + value = ParseLiteral(element); + + if (!scalarType.IsValueCompatible(value)) + { + error = ErrorBuilder.New() + .SetMessage( + "The value `{0}` is not a valid value for the scalar type `{1}`.", + value, + scalarType.Name) + .SetExtension("variable", $"{path}") + .Build(); + value = null; + return false; + } + } + + error = null; + return true; + } + + private static bool TryParseEnum( + FusionEnumTypeDefinition enumType, + JsonElement element, + Path path, + [NotNullWhen(true)] out IValueNode? value, + [NotNullWhen(false)] out IError? error) + { + if (element.ValueKind is not JsonValueKind.String) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("The value is not an enum value.") + .SetExtension("variable", $"{path}") + .Build(); + return false; + } + + var enumValue = element.GetString()!; + + if (!enumType.Values.ContainsName(enumValue)) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("The value `{0}` is not a valid value for the enum type `{1}`.", enumValue, enumType.Name) + .SetExtension("variable", $"{path}") + .Build(); + return false; + } + + value = new EnumValueNode(enumValue); + error = null; + return true; + } + + private readonly IValueNode ParseLiteral(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Null: + return NullValueNode.Default; + + case JsonValueKind.True: + return BooleanValueNode.True; + + case JsonValueKind.False: + return BooleanValueNode.False; + + case JsonValueKind.String: + { + var rawValue = element.GetRawText(); + var utf8Value = System.Text.Encoding.UTF8.GetBytes(rawValue); + var span = utf8Value.AsSpan(); + span = span[1..^1]; // Remove quotes + var segment = WriteValue(span); + return new StringValueNode(null, segment, false); + } + + case JsonValueKind.Number: + { + var rawValue = element.GetRawText(); + var utf8Value = System.Text.Encoding.UTF8.GetBytes(rawValue); + var span = utf8Value.AsSpan(); + var segment = WriteValue(span); + + if (span.IndexOfAny((byte)'e', (byte)'E') > -1) + { + return new FloatValueNode(segment, FloatFormat.Exponential); + } + + if (span.IndexOf((byte)'.') > -1) + { + return new FloatValueNode(segment, FloatFormat.FixedPoint); + } + + return new IntValueNode(segment); + } + + default: + throw new InvalidOperationException($"Unexpected JSON value kind: {element.ValueKind}"); + } + } + + private readonly ReadOnlyMemorySegment WriteValue(ReadOnlySpan value) + { + _memory ??= new Utf8MemoryBuilder(); + return _memory.Write(value); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationVariableCoercionMiddleware.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationVariableCoercionMiddleware.cs index ce81dcf1492..3fd76df4ef0 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationVariableCoercionMiddleware.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationVariableCoercionMiddleware.cs @@ -59,6 +59,7 @@ private static bool TryCoerceVariables( using (diagnosticEvents.CoerceVariables(context)) { if (VariableCoercionHelper.TryCoerceVariableValues( + context, context.Schema, variableDefinitions, operationRequest.VariableValues?.Document.RootElement ?? default, @@ -85,6 +86,7 @@ private static bool TryCoerceVariables( foreach (var variableValuesInput in variableValuesSetInput.EnumerateArray()) { if (VariableCoercionHelper.TryCoerceVariableValues( + context, context.Schema, variableDefinitions, variableValuesInput, diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/VariableCoercionHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/VariableCoercionHelper.cs index 6260f0d7b1f..ef0fc3dde9c 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/VariableCoercionHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/VariableCoercionHelper.cs @@ -1,22 +1,16 @@ -using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using HotChocolate.Execution; -using HotChocolate.Fusion.Types; +using HotChocolate.Features; using HotChocolate.Language; using HotChocolate.Types; namespace HotChocolate.Fusion.Execution; -// TODO : File Upload Rewrite -// return new FileReferenceNode( -// fileValueNode.Value.OpenReadStream, -// fileValueNode.Value.Name, -// fileValueNode.Value.ContentType); - internal static class VariableCoercionHelper { public static bool TryCoerceVariableValues( + IFeatureProvider context, ISchemaDefinition schema, IReadOnlyList variableDefinitions, JsonElement variableValues, @@ -33,6 +27,7 @@ public static bool TryCoerceVariableValues( nameof(variableValues)); } + Utf8MemoryBuilder? memory = null; var hasVariables = variableValues.ValueKind is JsonValueKind.Object; coercedVariableValues = []; error = null; @@ -75,9 +70,11 @@ public static bool TryCoerceVariableValues( else { if (TryCoerceVariableValue( + context, variableDefinition, variableType, propertyValue, + ref memory, out var variableValue, out error)) { @@ -91,272 +88,26 @@ public static bool TryCoerceVariableValues( } } + memory?.Seal(); return true; } private static bool TryCoerceVariableValue( + IFeatureProvider context, VariableDefinitionNode variableDefinition, IInputType variableType, JsonElement value, + ref Utf8MemoryBuilder? memory, [NotNullWhen(true)] out VariableValue? variableValue, [NotNullWhen(false)] out IError? error) { - var root = Path.Root.Append(variableDefinition.Variable.Name.Value); - var parser = new JsonValueParser(); - var valueLiteral = parser.Parse(value); - - if (!ValidateValue( - variableType, - valueLiteral, - root, - 0, - out error)) - { - variableValue = null; - return false; - } - - variableValue = new VariableValue( + var coercion = new JsonVariableCoercion(context, ref memory); + return coercion.TryCoerceVariableValue( variableDefinition.Variable.Name.Value, variableType, - valueLiteral); - return true; - } - - private static bool ValidateValue( - IInputType type, - IValueNode value, - Path path, - int stack, - [NotNullWhen(false)] out IError? error) - { - if (type.Kind is TypeKind.NonNull) - { - if (value.Kind is SyntaxKind.NullValue) - { - error = ErrorBuilder.New() - .SetMessage("The value `{0}` is not a non-null value.", value) - .SetExtension("variable", $"{path}") - .Build(); - return false; - } - - type = (IInputType)type.InnerType(); - } - - if (value.Kind is SyntaxKind.NullValue) - { - error = null; - return true; - } - - if (type.Kind is TypeKind.List) - { - if (value is not ListValueNode listValue) - { - error = ErrorBuilder.New() - .SetMessage("The value `{0}` is not a list value.", value) - .SetExtension("variable", $"{path}") - .Build(); - return false; - } - - var elementType = (IInputType)type.ListType().ElementType; - - for (var i = 0; i < listValue.Items.Count; i++) - { - if (!ValidateValue(elementType, listValue.Items[i], path.Append(i), stack, out error)) - { - return false; - } - } - - error = null; - return true; - } - - if (type.Kind is TypeKind.InputObject) - { - if (value is not ObjectValueNode objectValue) - { - error = ErrorBuilder.New() - .SetMessage("The value `{0}` is not an object value.", value) - .SetExtension("variable", $"{path}") - .Build(); - return false; - } - - var inputObjectType = (FusionInputObjectTypeDefinition)type; - - var oneOf = inputObjectType.IsOneOf; - - if (oneOf && objectValue.Fields.Count is 0) - { - // TODO : resources - error = ErrorBuilder.New() - .SetMessage("The OneOf Input Object `{0}` requires that exactly one field is supplied and that field must not be `null`. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", inputObjectType.Name) - .SetCode(ErrorCodes.Execution.OneOfNoFieldSet) - .SetPath(path) - .Build(); - return false; - } - - if (oneOf && objectValue.Fields.Count > 1) - { - // TODO : resources - error = ErrorBuilder.New() - .SetMessage("More than one field of the OneOf Input Object `{0}` is set. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", inputObjectType.Name) - .SetCode(ErrorCodes.Execution.OneOfMoreThanOneFieldSet) - .SetPath(path) - .Build(); - return false; - } - - var numberOfInputFields = inputObjectType.Fields.Count; - - var processedCount = 0; - bool[]? processedBuffer = null; - var processed = stack <= 256 && numberOfInputFields <= 32 - ? stackalloc bool[numberOfInputFields] - : processedBuffer = ArrayPool.Shared.Rent(numberOfInputFields); - - if (processedBuffer is not null) - { - processed.Clear(); - } - - if (processedBuffer is null) - { - stack += numberOfInputFields; - } - - try - { - for (var i = 0; i < objectValue.Fields.Count; i++) - { - var field = objectValue.Fields[i]; - if (!inputObjectType.Fields.TryGetField(field.Name.Value, out var fieldDefinition)) - { - // TODO : resources - error = ErrorBuilder.New() - .SetMessage( - "The field `{0}` is not defined on the input object type `{1}`.", - field.Name.Value, - inputObjectType.Name) - .SetExtension("variable", $"{path}") - .Build(); - return false; - } - - if (oneOf && field.Value.Kind is SyntaxKind.NullValue) - { - // TODO : resources - error = ErrorBuilder.New() - .SetMessage("`null` was set to the field `{0}`of the OneOf Input Object `{1}`. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", field.Name, inputObjectType.Name) - .SetCode(ErrorCodes.Execution.OneOfFieldIsNull) - .SetPath(path) - .SetCoordinate(fieldDefinition.Coordinate) - .Build(); - return false; - } - - if (!ValidateValue( - fieldDefinition.Type, - field.Value, - path.Append(field.Name.Value), - stack, - out error)) - { - return false; - } - - processed[fieldDefinition.Index] = true; - processedCount++; - } - - // If not all fields of the input object type were specified, - // we have to check if any of the ones left out are non-null - // and do not have a default value, and if so, raise an error. - if (!oneOf && processedCount != numberOfInputFields) - { - for (var i = 0; i < numberOfInputFields; i++) - { - if (!processed[i]) - { - var field = inputObjectType.Fields[i]; - - if (field.Type.Kind == TypeKind.NonNull && field.DefaultValue is null) - { - error = ErrorBuilder.New() - .SetMessage("The required input field `{0}` is missing.", field.Name) - .SetPath(path.Append(field.Name)) - .SetExtension("field", field.Coordinate.ToString()) - .Build(); - return false; - } - } - } - } - - error = null; - return true; - } - finally - { - if (processedBuffer is not null) - { - ArrayPool.Shared.Return(processedBuffer); - } - } - } - - if (type is IScalarTypeDefinition scalarType) - { - if (!scalarType.IsValueCompatible(value)) - { - // TODO : resources - error = ErrorBuilder.New() - .SetMessage( - "The value `{0}` is not a valid value for the scalar type `{1}`.", - value, - scalarType.Name) - .SetExtension("variable", $"{path}") - .Build(); - return false; - } - - error = null; - return true; - } - - if (type is FusionEnumTypeDefinition enumType) - { - if (value is not (StringValueNode or EnumValueNode)) - { - // TODO : resources - error = ErrorBuilder.New() - .SetMessage("The value `{0}` is not an enum value.", value.Value ?? "null") - .SetExtension("variable", $"{path}") - .Build(); - return false; - } - - if (!enumType.Values.ContainsName((string)value.Value!)) - { - // TODO : resources - error = ErrorBuilder.New() - .SetMessage("The value `{0}` is not a valid value for the enum type `{1}`.", value.Value ?? "null", enumType.Name) - .SetExtension("variable", $"{path}") - .Build(); - return false; - } - - error = null; - return true; - } - - throw new NotSupportedException( - $"The type `{type.FullTypeName()}` is not a valid input type."); + value, + out variableValue, + out error); } private static IInputType AssertInputType( diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files.yaml index a6a6d959d19..1c302d16789 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files.yaml @@ -69,8 +69,26 @@ sourceSchemas: files: [Upload!]! } + "Defines the possible serialization types for GraphQL scalar values." + enum ScalarSerializationType { + "The scalar serializes to a string value." + STRING + "The scalar serializes to a boolean value." + BOOLEAN + "The scalar serializes to an integer value." + INT + "The scalar serializes to a floating-point value." + FLOAT + "The scalar serializes to an object value." + OBJECT + "The scalar serializes to a list value." + LIST + } + + directive @serializeAs("The primitive type a scalar is serialized to." type: [ScalarSerializationType!]! "The ECMA-262 regex pattern that the serialized scalar value conforms to." pattern: String) on SCALAR + "The `Upload` scalar type represents a file upload." - scalar Upload + scalar Upload @serializeAs(type: STRING) interactions: - request: contentType: multipart/form-data; boundary="f56524ab-5626-4955-b296-234a097b44f6" diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object.yaml index 08a29aa2a5b..1b8e730dcc7 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object.yaml @@ -69,8 +69,26 @@ sourceSchemas: files: [Upload!]! } + "Defines the possible serialization types for GraphQL scalar values." + enum ScalarSerializationType { + "The scalar serializes to a string value." + STRING + "The scalar serializes to a boolean value." + BOOLEAN + "The scalar serializes to an integer value." + INT + "The scalar serializes to a floating-point value." + FLOAT + "The scalar serializes to an object value." + OBJECT + "The scalar serializes to a list value." + LIST + } + + directive @serializeAs("The primitive type a scalar is serialized to." type: [ScalarSerializationType!]! "The ECMA-262 regex pattern that the serialized scalar value conforms to." pattern: String) on SCALAR + "The `Upload` scalar type represents a file upload." - scalar Upload + scalar Upload @serializeAs(type: STRING) interactions: - request: contentType: multipart/form-data; boundary="f56524ab-5626-4955-b296-234a097b44f6" diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object_Inline.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object_Inline.yaml index e4d137429d4..b3d9448298f 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object_Inline.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object_Inline.yaml @@ -69,8 +69,26 @@ sourceSchemas: files: [Upload!]! } + "Defines the possible serialization types for GraphQL scalar values." + enum ScalarSerializationType { + "The scalar serializes to a string value." + STRING + "The scalar serializes to a boolean value." + BOOLEAN + "The scalar serializes to an integer value." + INT + "The scalar serializes to a floating-point value." + FLOAT + "The scalar serializes to an object value." + OBJECT + "The scalar serializes to a list value." + LIST + } + + directive @serializeAs("The primitive type a scalar is serialized to." type: [ScalarSerializationType!]! "The ECMA-262 regex pattern that the serialized scalar value conforms to." pattern: String) on SCALAR + "The `Upload` scalar type represents a file upload." - scalar Upload + scalar Upload @serializeAs(type: STRING) interactions: - request: contentType: multipart/form-data; boundary="f56524ab-5626-4955-b296-234a097b44f6" diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File.yaml index ee9a4b5d430..2e3df203e3b 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File.yaml @@ -57,8 +57,26 @@ sourceSchemas: files: [Upload!]! } + "Defines the possible serialization types for GraphQL scalar values." + enum ScalarSerializationType { + "The scalar serializes to a string value." + STRING + "The scalar serializes to a boolean value." + BOOLEAN + "The scalar serializes to an integer value." + INT + "The scalar serializes to a floating-point value." + FLOAT + "The scalar serializes to an object value." + OBJECT + "The scalar serializes to a list value." + LIST + } + + directive @serializeAs("The primitive type a scalar is serialized to." type: [ScalarSerializationType!]! "The ECMA-262 regex pattern that the serialized scalar value conforms to." pattern: String) on SCALAR + "The `Upload` scalar type represents a file upload." - scalar Upload + scalar Upload @serializeAs(type: STRING) interactions: - request: contentType: multipart/form-data; boundary="f56524ab-5626-4955-b296-234a097b44f6" diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object.yaml index c7c6f9f2f6b..ccbcda55412 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object.yaml @@ -57,8 +57,26 @@ sourceSchemas: files: [Upload!]! } + "Defines the possible serialization types for GraphQL scalar values." + enum ScalarSerializationType { + "The scalar serializes to a string value." + STRING + "The scalar serializes to a boolean value." + BOOLEAN + "The scalar serializes to an integer value." + INT + "The scalar serializes to a floating-point value." + FLOAT + "The scalar serializes to an object value." + OBJECT + "The scalar serializes to a list value." + LIST + } + + directive @serializeAs("The primitive type a scalar is serialized to." type: [ScalarSerializationType!]! "The ECMA-262 regex pattern that the serialized scalar value conforms to." pattern: String) on SCALAR + "The `Upload` scalar type represents a file upload." - scalar Upload + scalar Upload @serializeAs(type: STRING) interactions: - request: contentType: multipart/form-data; boundary="f56524ab-5626-4955-b296-234a097b44f6" diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object_Inline.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object_Inline.yaml index 36bd4b3b71a..6ab657ea1ec 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object_Inline.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object_Inline.yaml @@ -57,8 +57,26 @@ sourceSchemas: files: [Upload!]! } + "Defines the possible serialization types for GraphQL scalar values." + enum ScalarSerializationType { + "The scalar serializes to a string value." + STRING + "The scalar serializes to a boolean value." + BOOLEAN + "The scalar serializes to an integer value." + INT + "The scalar serializes to a floating-point value." + FLOAT + "The scalar serializes to an object value." + OBJECT + "The scalar serializes to a list value." + LIST + } + + directive @serializeAs("The primitive type a scalar is serialized to." type: [ScalarSerializationType!]! "The ECMA-262 regex pattern that the serialized scalar value conforms to." pattern: String) on SCALAR + "The `Upload` scalar type represents a file upload." - scalar Upload + scalar Upload @serializeAs(type: STRING) interactions: - request: contentType: multipart/form-data; boundary="f56524ab-5626-4955-b296-234a097b44f6" diff --git a/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj b/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj index 2acd7961110..75ceee0c3e9 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj +++ b/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj @@ -18,6 +18,7 @@ + diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8MemoryBuilder.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8MemoryBuilder.cs index 0ebe067af51..cf46bfd3236 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8MemoryBuilder.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8MemoryBuilder.cs @@ -45,9 +45,7 @@ public ReadOnlyMemorySegment Write(ReadOnlySpan value) } public ReadOnlyMemorySegment GetMemorySegment(int start, int length) - { - return new ReadOnlyMemorySegment(this, start, length); - } + => new(this, start, length); public Memory GetMemory(int sizeHint = 0) { diff --git a/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs b/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs index 2f5496df951..d5a20da7fe4 100644 --- a/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs +++ b/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs @@ -63,8 +63,11 @@ public IValueNode Parse(JsonElement element) } finally { - _memory?.Seal(); - _memory = null; + if (!_doNotSeal) + { + _memory?.Seal(); + _memory = null; + } } } @@ -220,15 +223,11 @@ public IValueNode Parse(ref Utf8JsonReader reader) } finally { -#if NET8_0_OR_GREATER if (!_doNotSeal) { -#endif _memory?.Seal(); _memory = null; -#if NET8_0_OR_GREATER } -#endif } } From ac24bf8540d6b886f8ca097ffd8b6ca01c0a25a0 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 15:03:52 +0100 Subject: [PATCH 35/46] Fixed more stuff --- ...onState_With_Multiple_Variable_Values.yaml | 28 ++++++------- ...ariable_Values_And_Forwarded_Variable.yaml | 40 +++++++++---------- ...tiple_Variable_Values_Some_Items_Null.yaml | 28 ++++++------- 3 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values.yaml index 9b53fdd52a6..e0e0dd17893 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values.yaml @@ -28,8 +28,8 @@ response: "node": { "feedback": { "buyer": { - "relativeUrl": "User: VXNlcjoxMw==", - "displayName": "User: VXNlcjoxMw==" + "relativeUrl": "User: VXNlcjoxMQ==", + "displayName": "User: VXNlcjoxMQ==" } } } @@ -48,8 +48,8 @@ response: "node": { "feedback": { "buyer": { - "relativeUrl": "User: VXNlcjoxMQ==", - "displayName": "User: VXNlcjoxMQ==" + "relativeUrl": "User: VXNlcjoxMw==", + "displayName": "User: VXNlcjoxMw==" } } } @@ -95,13 +95,13 @@ sourceSchemas: variables: | [ { - "__fusion_3_id": "VXNlcjoxMw==" + "__fusion_3_id": "VXNlcjoxMQ==" }, { "__fusion_3_id": "VXNlcjoxMg==" }, { - "__fusion_3_id": "VXNlcjoxMQ==" + "__fusion_3_id": "VXNlcjoxMw==" } ] response: @@ -112,7 +112,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "displayName": "User: VXNlcjoxMw==" + "displayName": "User: VXNlcjoxMQ==" } } } @@ -130,7 +130,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "displayName": "User: VXNlcjoxMQ==" + "displayName": "User: VXNlcjoxMw==" } } } @@ -187,13 +187,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "VXNlcjoxMw==" + "__fusion_2_id": "VXNlcjoxMQ==" }, { "__fusion_2_id": "VXNlcjoxMg==" }, { - "__fusion_2_id": "VXNlcjoxMQ==" + "__fusion_2_id": "VXNlcjoxMw==" } ] response: @@ -204,7 +204,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "relativeUrl": "User: VXNlcjoxMw==" + "relativeUrl": "User: VXNlcjoxMQ==" } } } @@ -222,7 +222,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "relativeUrl": "User: VXNlcjoxMQ==" + "relativeUrl": "User: VXNlcjoxMw==" } } } @@ -302,7 +302,7 @@ sourceSchemas: "node": { "feedback": { "buyer": { - "id": "VXNlcjoxMw==" + "id": "VXNlcjoxMQ==" } } } @@ -320,7 +320,7 @@ sourceSchemas: "node": { "feedback": { "buyer": { - "id": "VXNlcjoxMQ==" + "id": "VXNlcjoxMw==" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_And_Forwarded_Variable.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_And_Forwarded_Variable.yaml index adc01c486a5..ba30c8b03ec 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_And_Forwarded_Variable.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_And_Forwarded_Variable.yaml @@ -3,7 +3,7 @@ request: document: | query( $arg1: String - $arg2: String + $arg2: String ) { userBySlug(slug: "me") { feedbacks { @@ -36,8 +36,8 @@ response: "node": { "feedback": { "buyer": { - "relativeUrl": "User: VXNlcjoxMw==", - "displayName": "User: VXNlcjoxMw==" + "relativeUrl": "User: VXNlcjoxMQ==", + "displayName": "User: VXNlcjoxMQ==" } } } @@ -56,8 +56,8 @@ response: "node": { "feedback": { "buyer": { - "relativeUrl": "User: VXNlcjoxMQ==", - "displayName": "User: VXNlcjoxMQ==" + "relativeUrl": "User: VXNlcjoxMw==", + "displayName": "User: VXNlcjoxMw==" } } } @@ -92,7 +92,7 @@ sourceSchemas: document: | query Op_b1ca18eb_4( $arg2: String - $__fusion_3_id: ID! + $__fusion_3_id: ID! ) { node(id: $__fusion_3_id) { __typename @@ -105,7 +105,7 @@ sourceSchemas: [ { "arg2": "def", - "__fusion_3_id": "VXNlcjoxMw==" + "__fusion_3_id": "VXNlcjoxMQ==" }, { "arg2": "def", @@ -113,7 +113,7 @@ sourceSchemas: }, { "arg2": "def", - "__fusion_3_id": "VXNlcjoxMQ==" + "__fusion_3_id": "VXNlcjoxMw==" } ] response: @@ -124,7 +124,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "displayName": "User: VXNlcjoxMw==" + "displayName": "User: VXNlcjoxMQ==" } } } @@ -142,7 +142,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "displayName": "User: VXNlcjoxMQ==" + "displayName": "User: VXNlcjoxMw==" } } } @@ -188,7 +188,7 @@ sourceSchemas: document: | query Op_b1ca18eb_3( $arg1: String - $__fusion_2_id: ID! + $__fusion_2_id: ID! ) { node(id: $__fusion_2_id) { __typename @@ -201,7 +201,7 @@ sourceSchemas: [ { "arg1": "abc", - "__fusion_2_id": "VXNlcjoxMw==" + "__fusion_2_id": "VXNlcjoxMQ==" }, { "arg1": "abc", @@ -209,7 +209,7 @@ sourceSchemas: }, { "arg1": "abc", - "__fusion_2_id": "VXNlcjoxMQ==" + "__fusion_2_id": "VXNlcjoxMw==" } ] response: @@ -220,7 +220,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "relativeUrl": "User: VXNlcjoxMw==" + "relativeUrl": "User: VXNlcjoxMQ==" } } } @@ -238,7 +238,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "relativeUrl": "User: VXNlcjoxMQ==" + "relativeUrl": "User: VXNlcjoxMw==" } } } @@ -318,7 +318,7 @@ sourceSchemas: "node": { "feedback": { "buyer": { - "id": "VXNlcjoxMw==" + "id": "VXNlcjoxMQ==" } } } @@ -336,7 +336,7 @@ sourceSchemas: "node": { "feedback": { "buyer": { - "id": "VXNlcjoxMQ==" + "id": "VXNlcjoxMw==" } } } @@ -351,7 +351,7 @@ operationPlan: - document: | query( $arg1: String - $arg2: String + $arg2: String ) { userBySlug(slug: "me") { feedbacks { @@ -420,7 +420,7 @@ operationPlan: operation: | query Op_b1ca18eb_3( $arg1: String - $__fusion_2_id: ID! + $__fusion_2_id: ID! ) { node(id: $__fusion_2_id) { __typename @@ -445,7 +445,7 @@ operationPlan: operation: | query Op_b1ca18eb_4( $arg2: String - $__fusion_3_id: ID! + $__fusion_3_id: ID! ) { node(id: $__fusion_3_id) { __typename diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_Some_Items_Null.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_Some_Items_Null.yaml index 2a007266a90..060cfac28a0 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_Some_Items_Null.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_Some_Items_Null.yaml @@ -28,8 +28,8 @@ response: "node": { "feedback": { "buyer": { - "relativeUrl": "User: VXNlcjoxMw==", - "displayName": "User: VXNlcjoxMw==" + "relativeUrl": "User: VXNlcjoxMQ==", + "displayName": "User: VXNlcjoxMQ==" } } } @@ -48,8 +48,8 @@ response: "node": { "feedback": { "buyer": { - "relativeUrl": "User: VXNlcjoxMQ==", - "displayName": "User: VXNlcjoxMQ==" + "relativeUrl": "User: VXNlcjoxMw==", + "displayName": "User: VXNlcjoxMw==" } } } @@ -95,13 +95,13 @@ sourceSchemas: variables: | [ { - "__fusion_3_id": "VXNlcjoxMw==" + "__fusion_3_id": "VXNlcjoxMQ==" }, { "__fusion_3_id": "VXNlcjoxMg==" }, { - "__fusion_3_id": "VXNlcjoxMQ==" + "__fusion_3_id": "VXNlcjoxMw==" } ] response: @@ -112,7 +112,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "displayName": "User: VXNlcjoxMw==" + "displayName": "User: VXNlcjoxMQ==" } } } @@ -130,7 +130,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "displayName": "User: VXNlcjoxMQ==" + "displayName": "User: VXNlcjoxMw==" } } } @@ -187,13 +187,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "VXNlcjoxMw==" + "__fusion_2_id": "VXNlcjoxMQ==" }, { "__fusion_2_id": "VXNlcjoxMg==" }, { - "__fusion_2_id": "VXNlcjoxMQ==" + "__fusion_2_id": "VXNlcjoxMw==" } ] response: @@ -204,7 +204,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "relativeUrl": "User: VXNlcjoxMw==" + "relativeUrl": "User: VXNlcjoxMQ==" } } } @@ -222,7 +222,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "relativeUrl": "User: VXNlcjoxMQ==" + "relativeUrl": "User: VXNlcjoxMw==" } } } @@ -302,7 +302,7 @@ sourceSchemas: "node": { "feedback": { "buyer": { - "id": "VXNlcjoxMw==" + "id": "VXNlcjoxMQ==" } } } @@ -320,7 +320,7 @@ sourceSchemas: "node": { "feedback": { "buyer": { - "id": "VXNlcjoxMQ==" + "id": "VXNlcjoxMw==" } } } From b3c043924cce101b55e0714c517852f7fd9c7042 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 15:18:26 +0100 Subject: [PATCH 36/46] Fixed more issues --- .../src/Types/Text/Json/ResultDocument.cs | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs index 57d2ff62c4e..49e16eb06a9 100644 --- a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs +++ b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs @@ -374,7 +374,6 @@ private void WriteRawValueTo(Utf8JsonWriter writer, DbRow row) writer.WriteRawValue(ReadRawValue(row), skipInputValidation: true); return; - // TODO : We need to handle any types. default: throw new NotSupportedException(); } @@ -449,19 +448,34 @@ private ReadOnlySpan ReadRawValue(DbRow row) [MethodImpl(MethodImplOptions.AggressiveInlining)] private ReadOnlySpan ReadLocalData(int location, int size) { - var chunkIndex = location / JsonMemory.BufferSize; - var offset = location % JsonMemory.BufferSize; + var startChunkIndex = location / JsonMemory.BufferSize; + var offsetInStartChunk = location % JsonMemory.BufferSize; // Fast path: data fits in a single chunk - if (offset + size <= JsonMemory.BufferSize) + if (offsetInStartChunk + size <= JsonMemory.BufferSize) { - return _data[chunkIndex].AsSpan(offset, size); + return _data[startChunkIndex].AsSpan(offsetInStartChunk, size); } - // Data spans chunk boundaries - this should be rare for typical JSON values - throw new NotSupportedException( - "Reading data that spans chunk boundaries as a span is not supported. " - + "Use WriteLocalDataTo for writing to an IBufferWriter instead."); + Span buffer = new byte[size]; + var bytesRead = 0; + var currentLocation = location; + + while (bytesRead < size) + { + var chunkIndex = currentLocation / JsonMemory.BufferSize; + var offsetInChunk = currentLocation % JsonMemory.BufferSize; + var chunk = _data[chunkIndex]; + + var bytesToCopyFromThisChunk = Math.Min(size - bytesRead, JsonMemory.BufferSize - offsetInChunk); + var chunkSpan = chunk.AsSpan(offsetInChunk, bytesToCopyFromThisChunk); + + chunkSpan.CopyTo(buffer[bytesRead..]); + bytesRead += bytesToCopyFromThisChunk; + currentLocation += bytesToCopyFromThisChunk; + } + + return buffer; } internal ResultElement CreateObject(Cursor parent, SelectionSet selectionSet) From 6afbbfecba8929985dc770bd095a9a0e770abdd0 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 14:34:57 +0000 Subject: [PATCH 37/46] Fixed helper --- .../Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs index 9bce97065b6..e340bf0cc7d 100644 --- a/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs @@ -22,7 +22,7 @@ public static ConnectionFlags GetConnectionFlags(IResolverContext context) private static ConnectionFlags CreateConnectionFlags(IResolverContext context) { - if (context.Selection.Field.Flags.HasFlag(CoreFieldFlags.Connection)) + if (!context.Selection.Field.Flags.HasFlag(CoreFieldFlags.Connection)) { return ConnectionFlags.None; } From 0a2b4da0d59cec6febfb7234a5054dd3b515ad30 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 13 Feb 2026 14:55:30 +0000 Subject: [PATCH 38/46] Fixed issue --- .../Processing/MiddlewareContext.Pure.cs | 36 ++++++++++++------- .../SourceObjectConversionTests.cs | 5 ++- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pure.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pure.cs index a87acc09772..baab85de8fc 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pure.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pure.cs @@ -120,16 +120,30 @@ public bool HasErrors => parentContext.ContextData; public T Parent() - => _parent switch + { + if (_parent is T casted) { - T casted => casted, - null => default!, - _ => throw ResolverContext_CannotCastParent( - Selection.Field.Coordinate, - Path, - typeof(T), - _parent.GetType()) - }; + return casted; + } + + if (_parent is null) + { + return default!; + } + + _typeConverter ??= parentContext._operationContext.Converter; + + if (_typeConverter.TryConvert(_parent, out casted)) + { + return casted; + } + + throw ResolverContext_CannotCastParent( + Selection.Field.Coordinate, + Path, + typeof(T), + _parent.GetType()); + } public T ArgumentValue(string name) { @@ -235,9 +249,7 @@ private T CoerceArgumentValue(ArgumentValue argument) return default!; } - _typeConverter ??= - parentContext.Services.GetService() ?? - DefaultTypeConverter.Default; + _typeConverter ??= parentContext._operationContext.Converter; if (value is T castedValue || _typeConverter.TryConvert(value, out castedValue, out var conversionException)) diff --git a/src/HotChocolate/Core/test/Execution.Tests/SourceObjectConversionTests.cs b/src/HotChocolate/Core/test/Execution.Tests/SourceObjectConversionTests.cs index 95e90106dbc..3210aa17025 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/SourceObjectConversionTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/SourceObjectConversionTests.cs @@ -28,9 +28,8 @@ public async Task ConvertSourceObject() var result = await executor.ExecuteAsync("{ foo { qux } }"); // assert - Assert.True( - Assert.IsType(result).Errors is null, - "There should be no errors."); + var operationResult = Assert.IsType(result); + Assert.True(operationResult.Errors.IsEmpty, "There should be no errors."); Assert.True( conversionTriggered, "The custom converter should have been hit."); From 77901f64858697d533aff1394ee49d8475337fb8 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sat, 14 Feb 2026 00:46:46 +0100 Subject: [PATCH 39/46] Fixed more issues. --- .../ErrorListSnapshotValueFormatter.cs | 30 +++ .../ExecutionResultSnapshotValueFormatter.cs | 177 ++++++++---------- .../ResultElementSnapshotValueFormatter.cs | 28 +-- .../Execution/Tasks/ExecutionTask.cs | 18 +- .../Execution/JsonValueFormatter.cs | 6 +- .../Processing/DeferUsageEnumerator.cs | 77 -------- .../Processing/MiddlewareContext.Global.cs | 2 + .../Processing/MiddlewareContext.Pooling.cs | 2 + .../Processing/OperationContext.Execution.cs | 1 - .../Types/Execution/Processing/Selection.cs | 45 ++++- .../SubscriptionExecutor.Subscription.cs | 1 + .../Execution/Processing/Tasks/DeferTask.cs | 52 +++-- .../Processing/Tasks/ExecutionTaskPool.cs | 10 +- .../Processing/Tasks/ResolverTask.Pooling.cs | 2 +- .../Processing/Tasks/ResolverTaskFactory.cs | 3 +- ...alizerTests.Serialize_Response_Stream.snap | 20 +- .../FusionComplexTypeDefinition.cs | 13 +- 17 files changed, 220 insertions(+), 267 deletions(-) create mode 100644 src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ErrorListSnapshotValueFormatter.cs delete mode 100644 src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsageEnumerator.cs diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ErrorListSnapshotValueFormatter.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ErrorListSnapshotValueFormatter.cs new file mode 100644 index 00000000000..2b017e66d7b --- /dev/null +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ErrorListSnapshotValueFormatter.cs @@ -0,0 +1,30 @@ +using System.Buffers; +using System.Text.Encodings.Web; +using System.Text.Json; +using CookieCrumble.Formatters; +using HotChocolate; +using HotChocolate.Execution; +using HotChocolate.Text.Json; + +namespace CookieCrumble.HotChocolate.Formatters; + +internal sealed class ErrorListSnapshotValueFormatter + : SnapshotValueFormatter> +{ + protected override void Format(IBufferWriter snapshot, IReadOnlyList value) + { + var writerOptions = new JsonWriterOptions + { + Indented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + var serializationOptions = new JsonSerializerOptions + { + WriteIndented = true + }; + + var writer = new JsonWriter(snapshot, writerOptions); + JsonValueFormatter.WriteErrors(writer, value, serializationOptions, JsonNullIgnoreCondition.None); + } +} diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ExecutionResultSnapshotValueFormatter.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ExecutionResultSnapshotValueFormatter.cs index b0b7ce179b6..ae8ff86c9d7 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ExecutionResultSnapshotValueFormatter.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ExecutionResultSnapshotValueFormatter.cs @@ -101,20 +101,25 @@ private static async Task FormatStreamAsync( internal sealed class JsonResultPatcher { - private const string Data = "data"; - private const string Items = "items"; - private const string Incremental = "incremental"; - private const string Path = "path"; + private const string DataProp = "data"; + private const string ItemsProp = "items"; + private const string IncrementalProp = "incremental"; + private const string PendingProp = "pending"; + private const string PathProp = "path"; + private const string SubPathProp = "subPath"; + private const string IdProp = "id"; private JsonObject? _json; + private readonly Dictionary _pendingPaths = new(); public void SetResponse(JsonDocument response) { ArgumentNullException.ThrowIfNull(response); _json = JsonObject.Create(response.RootElement); + ProcessPayload(response.RootElement); } - public void WriteResponse(IBufferWriter snapshot) + public void ApplyPatch(JsonDocument patch) { if (_json is null) { @@ -122,15 +127,10 @@ public void WriteResponse(IBufferWriter snapshot) "You must first set the initial response before you can apply patches."); } - using var writer = new Utf8JsonWriter(snapshot, new JsonWriterOptions { Indented = true }); - - _json.Remove("hasNext"); - - _json.WriteTo(writer); - writer.Flush(); + ProcessPayload(patch.RootElement); } - public void ApplyPatch(JsonDocument patch) + public void WriteResponse(IBufferWriter snapshot) { if (_json is null) { @@ -138,126 +138,101 @@ public void ApplyPatch(JsonDocument patch) "You must first set the initial response before you can apply patches."); } - if (!patch.RootElement.TryGetProperty(Incremental, out var incremental)) - { - throw new ArgumentException("A patch result must contain a property `incremental`."); - } + using var writer = new Utf8JsonWriter(snapshot, new JsonWriterOptions { Indented = true }); - foreach (var element in incremental.EnumerateArray()) - { - if (element.TryGetProperty(Data, out var data)) - { - PatchIncrementalData(element, JsonObject.Create(data)!); - } - else if (element.TryGetProperty(Items, out var items)) - { - PatchIncrementalItems(element, JsonArray.Create(items)!); - } - } - } + _json.Remove("hasNext"); + _json.Remove("pending"); + _json.Remove("incremental"); + _json.Remove("completed"); - private void PatchIncrementalData(JsonElement incremental, JsonObject data) - { - if (incremental.TryGetProperty(Path, out var pathProp)) - { - var (current, last) = SelectNodeToPatch(_json![Data]!, pathProp); - ApplyPatch(current, last, data); - } + _json.WriteTo(writer); + writer.Flush(); } - private void PatchIncrementalItems(JsonElement incremental, JsonArray items) + private void ProcessPayload(JsonElement root) { - if (incremental.TryGetProperty(Path, out var pathProp)) + if (root.TryGetProperty(PendingProp, out var pending)) { - var (current, last) = SelectNodeToPatch(_json![Data]!, pathProp); - var i = last.GetInt32(); - var target = current.AsArray(); - - while (items.Count > 0) + foreach (var entry in pending.EnumerateArray()) { - var item = items[0]; - items.RemoveAt(0); - target.Insert(i++, item); + if (entry.TryGetProperty(IdProp, out var id) + && entry.TryGetProperty(PathProp, out var path)) + { + _pendingPaths[id.GetString()!] = path.Clone(); + } } } - } - private static void ApplyPatch(JsonNode current, JsonElement last, JsonObject patchData) - { - if (last.ValueKind is JsonValueKind.Undefined) + if (root.TryGetProperty(IncrementalProp, out var incremental)) { - foreach (var prop in patchData.ToArray()) + foreach (var element in incremental.EnumerateArray()) { - patchData.Remove(prop.Key); - current[prop.Key] = prop.Value; - } - } - else if (last.ValueKind is JsonValueKind.String) - { - current = current[last.GetString()!]!; + if (!element.TryGetProperty(IdProp, out var idElement)) + { + continue; + } - foreach (var prop in patchData.ToArray()) - { - patchData.Remove(prop.Key); - current[prop.Key] = prop.Value; - } - } - else if (last.ValueKind is JsonValueKind.Number) - { - var index = last.GetInt32(); - var element = current[index]; + var id = idElement.GetString()!; - if (element is null) - { - current[index] = patchData; - } - else - { - foreach (var prop in patchData.ToArray()) + if (!_pendingPaths.TryGetValue(id, out var basePath)) + { + continue; + } + + if (element.TryGetProperty(DataProp, out var data)) + { + PatchData(basePath, element, JsonObject.Create(data)!); + } + else if (element.TryGetProperty(ItemsProp, out var items)) { - patchData.Remove(prop.Key); - element[prop.Key] = prop.Value; + PatchItems(basePath, JsonArray.Create(items)!); } } } - else + } + + private void PatchData(JsonElement basePath, JsonElement incremental, JsonObject data) + { + var current = NavigatePath(_json![DataProp]!, basePath); + + if (incremental.TryGetProperty(SubPathProp, out var subPath)) { - throw new NotSupportedException("Path segment must be int or string."); + current = NavigatePath(current, subPath); + } + + foreach (var prop in data.ToArray()) + { + data.Remove(prop.Key); + current[prop.Key] = prop.Value; } } - private static (JsonNode Node, JsonElement PathSegment) SelectNodeToPatch( - JsonNode root, - JsonElement path) + private void PatchItems(JsonElement basePath, JsonArray items) { - if (path.GetArrayLength() == 0) + var target = NavigatePath(_json![DataProp]!, basePath).AsArray(); + + while (items.Count > 0) { - return (root, default); + var item = items[0]; + items.RemoveAt(0); + target.Add(item); } + } + private static JsonNode NavigatePath(JsonNode root, JsonElement path) + { var current = root; - JsonElement? last = null; - foreach (var element in path.EnumerateArray()) + foreach (var segment in path.EnumerateArray()) { - if (last is not null) + current = segment.ValueKind switch { - current = last.Value.ValueKind switch - { - JsonValueKind.String => current[last.Value.GetString()!]!, - JsonValueKind.Number => current[last.Value.GetInt32()]!, - _ => throw new NotSupportedException("Path segment must be int or string.") - }; - } - - last = element; - } - - if (current is null || last is null) - { - throw new InvalidOperationException("Patch had invalid structure."); + JsonValueKind.String => current[segment.GetString()!]!, + JsonValueKind.Number => current[segment.GetInt32()]!, + _ => throw new NotSupportedException("Path segment must be int or string.") + }; } - return (current, last.Value); + return current; } } diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ResultElementSnapshotValueFormatter.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ResultElementSnapshotValueFormatter.cs index 33b84c5140f..8ae45b6eb8c 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ResultElementSnapshotValueFormatter.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ResultElementSnapshotValueFormatter.cs @@ -1,11 +1,6 @@ using System.Buffers; -using System.Text.Encodings.Web; -using System.Text.Json; using CookieCrumble.Formatters; -using HotChocolate; -using HotChocolate.Execution; using HotChocolate.Text.Json; -using static HotChocolate.Execution.JsonValueFormatter; namespace CookieCrumble.HotChocolate.Formatters; @@ -13,26 +8,5 @@ internal sealed class ResultElementSnapshotValueFormatter : SnapshotValueFormatter { protected override void Format(IBufferWriter snapshot, ResultElement element) - => element.WriteTo(snapshot, indented: true); -} - -internal sealed class ErrorListSnapshotValueFormatter - : SnapshotValueFormatter> -{ - protected override void Format(IBufferWriter snapshot, IReadOnlyList value) - { - var writerOptions = new JsonWriterOptions - { - Indented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; - - var serializationOptions = new JsonSerializerOptions - { - WriteIndented = true - }; - - var writer = new JsonWriter(snapshot, writerOptions); - WriteErrors(writer, value, serializationOptions, JsonNullIgnoreCondition.None); - } + => element.WriteTo(snapshot, indented: true); } diff --git a/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs b/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs index 6aac8baa990..b9fb48eda9e 100644 --- a/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs +++ b/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs @@ -84,9 +84,13 @@ private async Task ExecuteInternalAsync(CancellationToken cancellationToken) Context.ReportError(this, ex); } } + finally + { + Status = _completionStatus; + Context.Completed(this); - Status = _completionStatus; - Context.Completed(this); + await OnAfterCompletedAsync(cancellationToken).ConfigureAwait(false); + } } /// @@ -97,6 +101,16 @@ private async Task ExecuteInternalAsync(CancellationToken cancellationToken) /// protected abstract ValueTask ExecuteAsync(CancellationToken cancellationToken); + /// + /// Called after the task has completed, regardless of whether it succeeded or faulted. + /// Override this method to perform post-completion logic such as cleanup or notifications. + /// + /// + /// The cancellation token. + /// + /// A representing the asynchronous operation. + protected virtual ValueTask OnAfterCompletedAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask; + /// /// Completes the task as faulted. /// diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs index dd42ed610b7..6b27544d7f1 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs @@ -349,7 +349,7 @@ private static void WriteIncrementalPendingItem(JsonWriter writer, PendingResult writer.WriteStartObject(); writer.WritePropertyName(Id); - writer.WriteNumberValue(item.Id); + writer.WriteStringValue(item.Id.ToString()); writer.WritePropertyName(ResultFieldNames.Path); WritePathValue(writer, item.Path); @@ -372,7 +372,7 @@ private static void WriteIncrementalItem( writer.WriteStartObject(); writer.WritePropertyName(Id); - writer.WriteNumberValue(item.Id); + writer.WriteStringValue(item.Id.ToString()); if (item.Errors is { Count: > 0 }) { @@ -423,7 +423,7 @@ private static void WriteIncrementalCompletedItem( writer.WriteStartObject(); writer.WritePropertyName(Id); - writer.WriteNumberValue(item.Id); + writer.WriteStringValue(item.Id.ToString()); if (item.Errors is { Count: > 0 }) { diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsageEnumerator.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsageEnumerator.cs deleted file mode 100644 index d2391abe941..00000000000 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsageEnumerator.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Collections; -using System.Diagnostics; - -namespace HotChocolate.Execution.Processing; - -/// -/// An enumerable and enumerator for active defer usages. -/// -[DebuggerDisplay("{Current,nq}")] -public struct DeferUsageEnumerator : IEnumerable, IEnumerator -{ - private readonly DeferUsage[] _deferUsages; - private readonly ulong _deferFlags; - private int _index; - - internal DeferUsageEnumerator(DeferUsage[] deferUsages, ulong deferFlags) - { - _deferUsages = deferUsages; - _deferFlags = deferFlags; - _index = -1; - } - - /// - public DeferUsage Current => _deferUsages[_index]; - - /// - object IEnumerator.Current => Current; - - /// - /// Returns an enumerator that iterates through active defer usages. - /// - public DeferUsageEnumerator GetEnumerator() - { - var enumerator = this; - enumerator._index = -1; - return enumerator; - } - - /// - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - /// - public bool MoveNext() - { - var usages = _deferUsages; - var flags = _deferFlags; - - if (usages.Length == 0) - { - return false; - } - - while (++_index < usages.Length) - { - var usage = usages[_index]; - var bit = 1UL << usage.DeferConditionIndex; - - if ((flags & bit) != 0) - { - return true; - } - } - - return false; - } - - /// - public void Reset() => _index = -1; - - /// - public void Dispose() => _index = _deferUsages.Length; -} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Global.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Global.cs index 4cf9057205f..51acff2efb4 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Global.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Global.cs @@ -27,6 +27,8 @@ public IServiceProvider Services public Operation Operation => _operationContext.Operation; + public DeferUsage? DeferUsage { get; private set; } + public IOperationResultBuilder OperationResult => _operationResultBuilder; public IDictionary ContextData => _operationContext.ContextData; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pooling.cs index 47511c5b910..afe58a02392 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pooling.cs @@ -22,6 +22,7 @@ public void Initialize( Selection selection, ResultElement resultValue, OperationContext operationContext, + DeferUsage? deferUsage, IImmutableDictionary scopedContextData) { _operationContext = operationContext; @@ -34,6 +35,7 @@ public void Initialize( ScopedContextData = scopedContextData; LocalContextData = s_emptyLocalContextData; Arguments = _selection.Arguments; + DeferUsage = deferUsage; RequestAborted = _operationContext.RequestAborted; } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs index c24d6765b43..47d5b3aa89c 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs @@ -81,7 +81,6 @@ public DeferTask CreateDeferTask( { AssertInitialized(); - // TODO : we need to pool this still var deferTask = new DeferTask(); deferTask.Initialize( diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs index 8d5921bd6d6..66565f554de 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs @@ -286,12 +286,45 @@ public bool IsDeferred(ulong deferFlags) => _deferMask != 0 && (_deferMask & deferFlags) == _deferMask; /// - /// Gets all defer usages that are active for the specified defer flags. + /// Determines whether this selection is deferred relative to a parent defer usage. /// - /// The active defer flags. - /// A struct enumerator over active defer usages. - public DeferUsageEnumerator GetActiveDeferUsages(ulong deferFlags) - => new(_deferUsage, deferFlags); + /// + /// The defer condition flags representing which @defer directives are active + /// for the current request, computed from the runtime variable values of the + /// if arguments on @defer directives. + /// + /// + /// The defer usage of the parent context, or null if the parent is not deferred. + /// When provided, this selection is only considered deferred if its primary defer usage + /// matches the given parent, ensuring that the selection is delivered in the correct + /// incremental payload. + /// + /// + /// true if this selection is deferred and belongs to the specified parent + /// defer context; otherwise, false. + /// + public bool IsDeferred(ulong deferFlags, DeferUsage? parentDeferUsage) + { + if (_deferMask != 0 && (_deferMask & deferFlags) == _deferMask) + { + if (parentDeferUsage is null) + { + return true; + } + + // If the primary defer usage matches the parent's defer context, + // this selection is already being delivered in that context + // and does not need to be deferred separately. + if (ReferenceEquals(GetPrimaryDeferUsage(deferFlags), parentDeferUsage)) + { + return false; + } + + return true; + } + + return false; + } /// /// Gets the primary defer usage for this selection given the active defer flags. @@ -315,7 +348,7 @@ public DeferUsageEnumerator GetActiveDeferUsages(ulong deferFlags) var usage = _deferUsage[0]; // Walk up the parent chain to find the nearest active defer. - // A defer is inactive when its condition evaluates to false at runtime + // A defer directive is inactive when its condition evaluates to false at runtime // (e.g. @defer(if: $var) with $var = false). When inactive, the fragment // is not deferred and its content folds into the parent scope — but the // parent scope may itself be deferred. diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/SubscriptionExecutor.Subscription.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/SubscriptionExecutor.Subscription.cs index 11ff915bf98..266a52807b0 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/SubscriptionExecutor.Subscription.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/SubscriptionExecutor.Subscription.cs @@ -257,6 +257,7 @@ private async ValueTask SubscribeAsync() rootSelection, resultMap, operationContext, + deferUsage: null, _scopedContextData); // it is important that we correctly coerce the arguments before diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs index 3debb89b83e..2a3223c6e54 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs @@ -7,36 +7,22 @@ namespace HotChocolate.Execution.Processing.Tasks; internal sealed class DeferTask : ExecutionTask { private static readonly ArrayPool s_pool = ArrayPool.Shared; - private OperationContext _parentContext = null!; - private DeferExecutionCoordinator _coordinator = null!; + private OperationContextOwner _deferContextOwner = null!; private object? _parent; private IImmutableDictionary _scopedContext = null!; - private SelectionSet _selectionSet = null!; - private Path _selectionPath = null!; private int _executionBranchId; private DeferUsage _deferUsage = null!; - // the defer tasks runs in the system branch as its just an orchestration task. + // the defer tasks runs in the system branch as it's just an orchestration task. public override int BranchId => BranchTracker.SystemBranchId; public override bool IsDeferred => true; - protected override IExecutionTaskContext Context => _parentContext; + protected override IExecutionTaskContext Context => _deferContextOwner.OperationContext; protected override async ValueTask ExecuteAsync(CancellationToken cancellationToken) { - var contextFactory = _parentContext.Services.GetRequiredService>(); - using var deferContextOwner = contextFactory.Create(); - var deferContext = deferContextOwner.OperationContext; - - // we first need to initialize the rented context for this defer operation. - deferContext.InitializeDeferContext( - _parentContext, - _selectionSet, - _selectionPath, - _executionBranchId, - _deferUsage); - + var deferContext = _deferContextOwner.OperationContext; var data = deferContext.Result.Data.Data; var bufferedTasks = s_pool.Rent(data.GetPropertyCount()); var i = 0; @@ -73,7 +59,14 @@ protected override async ValueTask ExecuteAsync(CancellationToken cancellationTo // once the execution branch has completed we enqueue the completed // result with the defer coordinator so it can be delivered. - _coordinator.EnqueueResult(deferContext.BuildResult(), _executionBranchId); + deferContext.DeferExecutionCoordinator.EnqueueResult(deferContext.BuildResult(), _executionBranchId); + } + + protected override ValueTask OnAfterCompletedAsync(CancellationToken cancellationToken) + { + // TODO : we need to give the context back here and not rest once we have a pool. + Reset(); + return ValueTask.CompletedTask; } public void Initialize( @@ -85,26 +78,31 @@ public void Initialize( int executionBranchId, DeferUsage deferUsage) { - _parentContext = parentContext; - _coordinator = parentContext.DeferExecutionCoordinator; + var contextFactory = parentContext.Services.GetRequiredService>(); + _deferContextOwner = contextFactory.Create(); + + // we first need to initialize the rented context for this defer operation. + _deferContextOwner.OperationContext.InitializeDeferContext( + parentContext, + selectionSet, + selectionPath, + executionBranchId, + deferUsage); + _parent = parent; _scopedContext = scopedContext; - _selectionSet = selectionSet; _executionBranchId = executionBranchId; _deferUsage = deferUsage; - _selectionPath = selectionPath; } public new void Reset() { - _parentContext = null!; - _coordinator = null!; + _deferContextOwner.Dispose(); + _deferContextOwner = null!; _parent = null!; _scopedContext = null!; - _selectionSet = null!; _executionBranchId = 0; _deferUsage = null!; - _selectionPath = null!; base.Reset(); } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ExecutionTaskPool.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ExecutionTaskPool.cs index e9d83df99b8..55a0c1a73ce 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ExecutionTaskPool.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ExecutionTaskPool.cs @@ -5,7 +5,7 @@ namespace HotChocolate.Execution.Processing.Tasks; /// -/// A pool of objects. Buffers a set of objects to ensure fast, thread safe object pooling +/// A pool of objects. Buffers a set of objects to ensure fast, thread safe object pooling /// internal sealed class ExecutionTaskPool : ObjectPool where T : class, IExecutionTask @@ -22,8 +22,8 @@ public ExecutionTaskPool(TPolicy policy, int maximumRetained = 256) } /// - /// Gets an object from the buffer if one is available, otherwise get a new buffer - /// from the pool one. + /// Gets an object from the buffer if one is available, otherwise get a new buffer + /// from the pool one. /// /// A . public override T Get() @@ -49,8 +49,8 @@ public override T Get() } /// - /// Return an object from the buffer if one is available. If the buffer is full - /// return the buffer to the pool + /// Return an object from the buffer if one is available. If the buffer is full + /// return the buffer to the pool /// public override void Return(T obj) { diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs index c173be04219..34d3347da15 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs @@ -19,7 +19,7 @@ public void Initialize( { _operationContext = operationContext; _selection = selection; - _context.Initialize(parent, selection, resultValue, operationContext, scopedContextData); + _context.Initialize(parent, selection, resultValue, operationContext, deferUsage, scopedContextData); IsSerial = selection.Strategy is SelectionExecutionStrategy.Serial; BranchId = executionBranchId; DeferUsage = deferUsage; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs index 61b87eefde9..3e803b14bcb 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs @@ -144,6 +144,7 @@ public static void EnqueueOrInlineResolverTasks( Debug.Assert(resultValue.Type?.NamedType()?.IsAssignableFrom(selectionSetType) ?? false); var operationContext = context.OperationContext; + var parentDeferUsage = context.ResolverContext.DeferUsage; resultValue.SetObjectValue(selectionSet); @@ -161,7 +162,7 @@ public static void EnqueueOrInlineResolverTasks( { var selection = field.AssertSelection(); - if (selection.IsDeferred(deferFlags)) + if (selection.IsDeferred(deferFlags, parentDeferUsage)) { // if IsDeferred is true then GetPrimaryDeferUsage will be guaranteed // to return a defer usage for the same deferFlags diff --git a/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap b/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap index f25677d55d7..d50efe20262 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap @@ -1,10 +1,10 @@ - ---- -Content-Type: application/json; charset=utf-8 - -{"data":{"hero":{"id":"2001"}},"pending":[{"id":2,"path":["hero"],"label":"friends"}],"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"id":2,"errors":[{"message":"Unexpected Execution Error","path":["hero","friends"]}],"data":{"friends":null}}],"completed":[{"id":2}],"hasNext":false} ------ + +--- +Content-Type: application/json; charset=utf-8 + +{"data":{"hero":{"id":"2001"}},"pending":[{"id":2,"path":["hero"],"label":"friends"}],"hasNext":true} +--- +Content-Type: application/json; charset=utf-8 + +{"incremental":[{"id":2,"data":{"friends":{"nodes":[{"id":"1000","name":"Luke Skywalker"},{"id":"1002","name":"Han Solo"},{"id":"1003","name":"Leia Organa"}]}}}],"completed":[{"id":2}],"hasNext":false} +----- diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionComplexTypeDefinition.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionComplexTypeDefinition.cs index 073a86c5cca..405f5c84697 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionComplexTypeDefinition.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionComplexTypeDefinition.cs @@ -175,12 +175,13 @@ public override string ToString() /// Creates a from a /// . /// - public ComplexTypeDefinitionNodeBase ToSyntaxNode() => this switch - { - FusionInterfaceTypeDefinition i => SchemaDebugFormatter.Format(i), - FusionObjectTypeDefinition o => SchemaDebugFormatter.Format(o), - _ => throw new ArgumentOutOfRangeException() - }; + public ComplexTypeDefinitionNodeBase ToSyntaxNode() + => this switch + { + FusionInterfaceTypeDefinition i => SchemaDebugFormatter.Format(i), + FusionObjectTypeDefinition o => SchemaDebugFormatter.Format(o), + _ => throw new ArgumentOutOfRangeException() + }; ISyntaxNode ISyntaxNodeProvider.ToSyntaxNode() => ToSyntaxNode(); } From 55e9e79cd4eb0a049add218f665b44705d75dfc9 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sat, 14 Feb 2026 10:46:27 +0100 Subject: [PATCH 40/46] Refactor JSON serialization to support null value ignoring - Removed unnecessary using directives and cleaned up code in IWebSocketPayloadFormatter. - Enhanced JsonResultFormatter to include JsonNullIgnoreCondition for better null handling. - Updated JsonResultFormatterOptions to utilize the new null ignore condition. - Modified various test files to include necessary references for the new null handling features. - Introduced JsonNullIgnoreCondition enum to specify when null values should be ignored during serialization. - Implemented logic in JsonWriter to handle deferred property names and null value ignoring based on the specified condition. - Added comprehensive tests for JsonWriter to validate behavior with different null ignore conditions. --- .../ErrorListSnapshotValueFormatter.cs | 2 +- .../Formatters/ErrorSnapshotValueFormatter.cs | 2 +- .../DefaultWebSocketPayloadFormatter.cs | 75 ++- .../Formatters/IWebSocketPayloadFormatter.cs | 1 - .../JsonResultFormatter.cs | 14 +- .../JsonResultFormatterOptions.cs | 2 +- .../HttpPostMiddlewareTests.cs | 1 + .../Apollo/WebSocketProtocolTests.cs | 1 + .../WebSocketProtocolTests.cs | 1 + .../Execution/IResultDataJsonFormatter.cs | 6 +- .../Execution/JsonValueFormatter.cs | 93 ++-- .../src/Types/Execution/NeedsFormatting.cs | 19 +- .../DictionaryToJsonDocumentConverter.cs | 4 +- ...alizerTests.Serialize_Response_Stream.snap | 6 +- .../Text/Json/SourceResultDocument.WriteTo.cs | 17 +- .../src/Json}/JsonNullIgnoreCondition.cs | 2 +- .../Json/JsonWriter.WriteValues.Literal.cs | 13 + .../src/Json/JsonWriter.WriteValues.Number.cs | 2 + .../JsonWriter.WriteValues.PropertyName.cs | 21 + .../src/Json/JsonWriter.WriteValues.String.cs | 6 + src/HotChocolate/Json/src/Json/JsonWriter.cs | 168 ++++++- .../Json.Tests/JsonWriterNullIgnoreTests.cs | 435 ++++++++++++++++++ 22 files changed, 770 insertions(+), 121 deletions(-) rename src/HotChocolate/{Core/src/Execution.Abstractions/Execution => Json/src/Json}/JsonNullIgnoreCondition.cs (94%) create mode 100644 src/HotChocolate/Json/test/Json.Tests/JsonWriterNullIgnoreTests.cs diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ErrorListSnapshotValueFormatter.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ErrorListSnapshotValueFormatter.cs index 2b017e66d7b..8ce0b3109af 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ErrorListSnapshotValueFormatter.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ErrorListSnapshotValueFormatter.cs @@ -25,6 +25,6 @@ protected override void Format(IBufferWriter snapshot, IReadOnlyList snapshot, IError value) { var jsonWriter = new JsonWriter(snapshot, new JsonWriterOptions { Indented = true }); - JsonValueFormatter.WriteError(jsonWriter, value, new JsonSerializerOptions { WriteIndented = true }, default); + JsonValueFormatter.WriteError(jsonWriter, value, new JsonSerializerOptions { WriteIndented = true }); } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultWebSocketPayloadFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultWebSocketPayloadFormatter.cs index c0931b28100..46fb269bca3 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultWebSocketPayloadFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultWebSocketPayloadFormatter.cs @@ -17,26 +17,85 @@ public sealed class DefaultWebSocketPayloadFormatter(WebSocketPayloadFormatterOp /// public void Format(OperationResult result, JsonWriter writer) - => _internalFormatter.Format(result, writer); + { + // Save the writer's current null ignore condition so we can restore it after formatting. + var savedNullIgnoreCondition = writer.NullIgnoreCondition; + + try + { + // Apply the null ignore condition configured for this payload formatter. + writer.NullIgnoreCondition = _nullIgnoreCondition; + _internalFormatter.Format(result, writer); + } + finally + { + // Restore the original null ignore condition. + writer.NullIgnoreCondition = savedNullIgnoreCondition; + } + } /// public void Format(IError error, JsonWriter writer) - => WriteError(writer, error, _serializerOptions, _nullIgnoreCondition); + { + // Save the writer's current null ignore condition so we can restore it after formatting. + var savedNullIgnoreCondition = writer.NullIgnoreCondition; + + try + { + // Apply the null ignore condition configured for this payload formatter. + writer.NullIgnoreCondition = _nullIgnoreCondition; + WriteError(writer, error, _serializerOptions); + } + finally + { + // Restore the original null ignore condition. + writer.NullIgnoreCondition = savedNullIgnoreCondition; + } + } /// public void Format(IReadOnlyList errors, JsonWriter writer) { - writer.WriteStartArray(); + // Save the writer's current null ignore condition so we can restore it after formatting. + var savedNullIgnoreCondition = writer.NullIgnoreCondition; - for (var i = 0; i < errors.Count; i++) + try { - WriteError(writer, errors[i], _serializerOptions, _nullIgnoreCondition); - } + // Apply the null ignore condition configured for this payload formatter. + writer.NullIgnoreCondition = _nullIgnoreCondition; + + writer.WriteStartArray(); - writer.WriteEndArray(); + for (var i = 0; i < errors.Count; i++) + { + WriteError(writer, errors[i], _serializerOptions); + } + + writer.WriteEndArray(); + } + finally + { + // Restore the original null ignore condition. + writer.NullIgnoreCondition = savedNullIgnoreCondition; + } } /// public void Format(IReadOnlyDictionary extensions, JsonWriter writer) - => WriteDictionary(writer, extensions, _serializerOptions, _nullIgnoreCondition); + { + // Save the writer's current null ignore condition so we can restore it after formatting. + var savedNullIgnoreCondition = writer.NullIgnoreCondition; + + try + { + // Apply the null ignore condition configured for this payload formatter. + writer.NullIgnoreCondition = _nullIgnoreCondition; + WriteDictionary(writer, extensions, _serializerOptions); + } + finally + { + // Restore the original null ignore condition. + writer.NullIgnoreCondition = savedNullIgnoreCondition; + } + } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IWebSocketPayloadFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IWebSocketPayloadFormatter.cs index 84752a0b0b1..e984d141875 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IWebSocketPayloadFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IWebSocketPayloadFormatter.cs @@ -1,4 +1,3 @@ -using System.Buffers; using HotChocolate.Text.Json; namespace HotChocolate.AspNetCore.Formatters; diff --git a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs index eba71eaad70..b993a602350 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs @@ -15,6 +15,7 @@ public sealed class JsonResultFormatter : IOperationResultFormatter, IExecutionR { private readonly JsonWriterOptions _options; private readonly JsonSerializerOptions _serializerOptions; + private readonly JsonNullIgnoreCondition _nullIgnoreCondition; /// /// Initializes a new instance of with default options. @@ -25,6 +26,7 @@ public sealed class JsonResultFormatter : IOperationResultFormatter, IExecutionR public JsonResultFormatter(bool indented = false) : this(new JsonResultFormatterOptions { Indented = indented }) { + _nullIgnoreCondition = JsonNullIgnoreCondition.None; } /// @@ -37,6 +39,7 @@ public JsonResultFormatter(JsonResultFormatterOptions options) { _options = options.CreateWriterOptions() with { SkipValidation = true }; _serializerOptions = options.CreateSerializerOptions(); + _nullIgnoreCondition = options.NullIgnoreCondition; } /// @@ -88,7 +91,7 @@ public ValueTask FormatAsync( private void FormatInternal(OperationResult result, IBufferWriter bufferWriter) { - var jsonWriter = new JsonWriter(bufferWriter, _options); + var jsonWriter = new JsonWriter(bufferWriter, _options, _nullIgnoreCondition); Format(result, jsonWriter); } @@ -114,8 +117,7 @@ public void Format(OperationResult result, JsonWriter writer) WriteErrors( writer, result.Errors, - _serializerOptions, - default); + _serializerOptions); if (result.Data.HasValue) { @@ -126,16 +128,14 @@ public void Format(OperationResult result, JsonWriter writer) WriteExtensions( writer, result.Extensions, - _serializerOptions, - default); + _serializerOptions); if (result.IsIncremental) { WriteIncremental( writer, result, - _serializerOptions, - default); + _serializerOptions); } writer.WriteEndObject(); diff --git a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatterOptions.cs b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatterOptions.cs index f417ecd3e74..157735e8c07 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatterOptions.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatterOptions.cs @@ -1,6 +1,6 @@ using System.Text.Encodings.Web; using System.Text.Json; -using HotChocolate.Execution; +using HotChocolate.Text.Json; using static System.Text.Json.JsonSerializerDefaults; using static System.Text.Json.Serialization.JsonIgnoreCondition; diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs index edbbc881495..77cdf3be1a2 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs @@ -3,6 +3,7 @@ using HotChocolate.AspNetCore.Instrumentation; using HotChocolate.AspNetCore.Tests.Utilities; using HotChocolate.Execution; +using HotChocolate.Text.Json; using HotChocolate.Transport.Formatters; using HotChocolate.Transport.Http; using Microsoft.AspNetCore.Builder; diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs index cdc8893a3d6..f7862a2b9fb 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs @@ -8,6 +8,7 @@ using HotChocolate.AspNetCore.Tests.Utilities.Subscriptions.Apollo; using HotChocolate.Execution; using HotChocolate.Language; +using HotChocolate.Text.Json; using HotChocolate.Transport.Formatters; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/GraphQLOverWebSocket/WebSocketProtocolTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/GraphQLOverWebSocket/WebSocketProtocolTests.cs index 1081dbd3f05..8b431951616 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/GraphQLOverWebSocket/WebSocketProtocolTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/GraphQLOverWebSocket/WebSocketProtocolTests.cs @@ -8,6 +8,7 @@ using HotChocolate.AspNetCore.Tests.Utilities.Subscriptions.GraphQLOverWebSocket; using HotChocolate.Execution; using HotChocolate.Subscriptions.Diagnostics; +using HotChocolate.Text.Json; using HotChocolate.Transport.Formatters; using HotChocolate.Transport.Sockets.Client; using Microsoft.AspNetCore.Builder; diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IResultDataJsonFormatter.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IResultDataJsonFormatter.cs index bc15e8ac0bf..52751fc494c 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IResultDataJsonFormatter.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IResultDataJsonFormatter.cs @@ -18,11 +18,7 @@ public interface IResultDataJsonFormatter /// The serializer options. /// If options are set to null .Web will be used. /// - /// - /// The null ignore condition. - /// void WriteTo( JsonWriter writer, - JsonSerializerOptions? options = null, - JsonNullIgnoreCondition nullIgnoreCondition = JsonNullIgnoreCondition.None); + JsonSerializerOptions? options = null); } diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs index 6b27544d7f1..51137738718 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs @@ -8,12 +8,10 @@ namespace HotChocolate.Execution; public static class JsonValueFormatter { - // TODO : are the options still needed? public static void WriteValue( JsonWriter writer, object? value, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { if (value is null) { @@ -24,11 +22,11 @@ public static void WriteValue( switch (value) { case JsonDocument doc: - WriteJsonElement(doc.RootElement, writer, options, nullIgnoreCondition); + WriteJsonElement(doc.RootElement, writer, options); break; case JsonElement element: - WriteJsonElement(element, writer, options, nullIgnoreCondition); + WriteJsonElement(element, writer, options); break; case RawJsonValue rawJsonValue: @@ -36,19 +34,19 @@ public static void WriteValue( break; case Dictionary dict: - WriteDictionary(writer, dict, options, nullIgnoreCondition); + WriteDictionary(writer, dict, options); break; case IReadOnlyDictionary dict: - WriteDictionary(writer, dict, options, nullIgnoreCondition); + WriteDictionary(writer, dict, options); break; case IList list: - WriteList(writer, list, options, nullIgnoreCondition); + WriteList(writer, list, options); break; case IError error: - WriteError(writer, error, options, nullIgnoreCondition); + WriteError(writer, error, options); break; case string s: @@ -108,7 +106,7 @@ public static void WriteValue( break; case IResultDataJsonFormatter formatter: - formatter.WriteTo(writer, options, nullIgnoreCondition); + formatter.WriteTo(writer, options); break; default: @@ -120,8 +118,7 @@ public static void WriteValue( private static void WriteJsonElement( JsonElement element, JsonWriter writer, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { switch (element.ValueKind) { @@ -129,14 +126,8 @@ private static void WriteJsonElement( writer.WriteStartObject(); foreach (var property in element.EnumerateObject()) { - if (property.Value.ValueKind is JsonValueKind.Null - && (nullIgnoreCondition & JsonNullIgnoreCondition.Fields) == JsonNullIgnoreCondition.Fields) - { - continue; - } - writer.WritePropertyName(property.Name); - WriteValue(writer, property.Value, options, nullIgnoreCondition); + WriteValue(writer, property.Value, options); } writer.WriteEndObject(); break; @@ -145,13 +136,7 @@ private static void WriteJsonElement( writer.WriteStartArray(); foreach (var item in element.EnumerateArray()) { - if (item.ValueKind is JsonValueKind.Null - && (nullIgnoreCondition & JsonNullIgnoreCondition.Lists) == JsonNullIgnoreCondition.Lists) - { - continue; - } - - WriteValue(writer, item, options, nullIgnoreCondition); + WriteValue(writer, item, options); } writer.WriteEndArray(); break; @@ -190,21 +175,14 @@ private static void WriteJsonElement( public static void WriteDictionary( JsonWriter writer, IReadOnlyDictionary dict, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { writer.WriteStartObject(); foreach (var item in dict) { - if (item.Value is null - && (nullIgnoreCondition & JsonNullIgnoreCondition.Fields) == JsonNullIgnoreCondition.Fields) - { - continue; - } - writer.WritePropertyName(item.Key); - WriteValue(writer, item.Value, options, nullIgnoreCondition); + WriteValue(writer, item.Value, options); } writer.WriteEndObject(); @@ -213,22 +191,13 @@ public static void WriteDictionary( private static void WriteList( JsonWriter writer, IList list, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { writer.WriteStartArray(); for (var i = 0; i < list.Count; i++) { - var element = list[i]; - - if (element is null - && (nullIgnoreCondition & JsonNullIgnoreCondition.Lists) == JsonNullIgnoreCondition.Lists) - { - continue; - } - - WriteValue(writer, element, options, nullIgnoreCondition); + WriteValue(writer, list[i], options); } writer.WriteEndArray(); @@ -237,8 +206,7 @@ private static void WriteList( public static void WriteErrors( JsonWriter writer, IReadOnlyList errors, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { if (errors is { Count: > 0 }) { @@ -251,7 +219,7 @@ public static void WriteErrors( // - Then errors sorted by path foreach (var error in errors.OrderBy(e => e.Path, PathComparer.Instance)) { - WriteError(writer, error, options, nullIgnoreCondition); + WriteError(writer, error, options); } writer.WriteEndArray(); @@ -261,8 +229,7 @@ public static void WriteErrors( public static void WriteError( JsonWriter writer, IError error, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { writer.WriteStartObject(); @@ -271,7 +238,7 @@ public static void WriteError( WriteLocations(writer, error.Locations); WritePath(writer, error.Path); - WriteExtensions(writer, error.Extensions, options, nullIgnoreCondition); + WriteExtensions(writer, error.Extensions, options); writer.WriteEndObject(); } @@ -279,21 +246,19 @@ public static void WriteError( public static void WriteExtensions( JsonWriter writer, IReadOnlyDictionary? dict, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { if (dict is { Count: > 0 }) { writer.WritePropertyName(Extensions); - WriteDictionary(writer, dict, options, nullIgnoreCondition); + WriteDictionary(writer, dict, options); } } public static void WriteIncremental( JsonWriter writer, OperationResult result, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { if (result.Pending is { Count: > 0 } pending) { @@ -317,7 +282,7 @@ public static void WriteIncremental( for (var i = 0; i < incremental.Count; i++) { - WriteIncrementalItem(writer, incremental[i], options, nullIgnoreCondition); + WriteIncrementalItem(writer, incremental[i], options); } writer.WriteEndArray(); @@ -331,7 +296,7 @@ public static void WriteIncremental( for (var i = 0; i < completed.Count; i++) { - WriteIncrementalCompletedItem(writer, completed[i], options, nullIgnoreCondition); + WriteIncrementalCompletedItem(writer, completed[i], options); } writer.WriteEndArray(); @@ -366,8 +331,7 @@ private static void WriteIncrementalPendingItem(JsonWriter writer, PendingResult private static void WriteIncrementalItem( JsonWriter writer, IIncrementalResult item, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { writer.WriteStartObject(); @@ -376,7 +340,7 @@ private static void WriteIncrementalItem( if (item.Errors is { Count: > 0 }) { - WriteErrors(writer, item.Errors, options, nullIgnoreCondition); + WriteErrors(writer, item.Errors, options); } if (item is IncrementalObjectResult objectResult) @@ -417,8 +381,7 @@ private static void WriteIncrementalItem( private static void WriteIncrementalCompletedItem( JsonWriter writer, CompletedResult item, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { writer.WriteStartObject(); @@ -427,7 +390,7 @@ private static void WriteIncrementalCompletedItem( if (item.Errors is { Count: > 0 }) { - WriteErrors(writer, item.Errors, options, nullIgnoreCondition); + WriteErrors(writer, item.Errors, options); } writer.WriteEndObject(); diff --git a/src/HotChocolate/Core/src/Types/Execution/NeedsFormatting.cs b/src/HotChocolate/Core/src/Types/Execution/NeedsFormatting.cs index 59d5f97cd61..bc2692cbbcb 100644 --- a/src/HotChocolate/Core/src/Types/Execution/NeedsFormatting.cs +++ b/src/HotChocolate/Core/src/Types/Execution/NeedsFormatting.cs @@ -26,19 +26,14 @@ internal abstract class NeedsFormatting : IResultDataJsonFormatter /// /// The JSON serializer options. /// - /// - /// The null ignore condition. - /// public abstract void FormatValue( JsonWriter writer, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition); + JsonSerializerOptions options); void IResultDataJsonFormatter.WriteTo( JsonWriter writer, - JsonSerializerOptions? options, - JsonNullIgnoreCondition nullIgnoreCondition) - => FormatValue(writer, options ?? JsonSerializerOptionDefaults.GraphQL, nullIgnoreCondition); + JsonSerializerOptions? options) + => FormatValue(writer, options ?? JsonSerializerOptionDefaults.GraphQL); public static JsonNeedsFormatting Create(TValue value) { @@ -80,14 +75,10 @@ internal sealed class JsonNeedsFormatting(JsonDocument value) : NeedsFormatting /// /// The JSON serializer options. /// - /// - /// The null ignore condition. - /// public override void FormatValue( JsonWriter writer, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) - => JsonValueFormatter.WriteValue(writer, Value, options, nullIgnoreCondition); + JsonSerializerOptions options) + => JsonValueFormatter.WriteValue(writer, Value, options); /// /// Returns the string representation of the inner value. diff --git a/src/HotChocolate/Core/src/Types/Utilities/DictionaryToJsonDocumentConverter.cs b/src/HotChocolate/Core/src/Types/Utilities/DictionaryToJsonDocumentConverter.cs index 546d806ebbe..e3171b67db0 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/DictionaryToJsonDocumentConverter.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/DictionaryToJsonDocumentConverter.cs @@ -20,7 +20,7 @@ public static JsonElement FromDictionary(IReadOnlyDictionary di { using var buffer = new PooledArrayWriter(); var writer = new JsonWriter(buffer, s_options); - JsonValueFormatter.WriteDictionary(writer, dictionary, s_jsonSerializerOptions, JsonNullIgnoreCondition.None); + JsonValueFormatter.WriteDictionary(writer, dictionary, s_jsonSerializerOptions); var jsonReader = new Utf8JsonReader(buffer.WrittenSpan); return JsonElement.ParseValue(ref jsonReader); @@ -47,7 +47,7 @@ public static JsonElement FromList(IReadOnlyList list) { using var buffer = new PooledArrayWriter(); var writer = new JsonWriter(buffer, s_options); - JsonValueFormatter.WriteValue(writer, list, s_jsonSerializerOptions, JsonNullIgnoreCondition.None); + JsonValueFormatter.WriteValue(writer, list, s_jsonSerializerOptions); var jsonReader = new Utf8JsonReader(buffer.WrittenSpan); return JsonElement.ParseValue(ref jsonReader); diff --git a/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap b/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap index d50efe20262..1127ea8eae7 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap @@ -2,9 +2,5 @@ --- Content-Type: application/json; charset=utf-8 -{"data":{"hero":{"id":"2001"}},"pending":[{"id":2,"path":["hero"],"label":"friends"}],"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"id":2,"data":{"friends":{"nodes":[{"id":"1000","name":"Luke Skywalker"},{"id":"1002","name":"Han Solo"},{"id":"1003","name":"Leia Organa"}]}}}],"completed":[{"id":2}],"hasNext":false} +{"data":{"hero":{"id":"2001"}},"pending":[{"id":"2","path":["hero"],"label":"friends"}],"incremental":[{"id":"2","data":{"friends":{"nodes":[{"id":"1000","name":"Luke Skywalker"},{"id":"1002","name":"Han Solo"},{"id":"1003","name":"Leia Organa"}]}}}],"completed":[{"id":"2"}],"hasNext":false} ----- diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.WriteTo.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.WriteTo.cs index 52882b4fc07..91a30ece7c3 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.WriteTo.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.WriteTo.cs @@ -74,25 +74,26 @@ private void WriteObject(Cursor start, DbRow startRow) while (current < end) { - var row = document._parsedData.Get(current); - Debug.Assert(row.TokenType is JsonTokenType.PropertyName); + var nameRow = document._parsedData.Get(current); + Debug.Assert(nameRow.TokenType is JsonTokenType.PropertyName); // property name - writer.WritePropertyName(document.ReadRawValue(row, includeQuotes: false)); + writer.WritePropertyName(document.ReadRawValue(nameRow, includeQuotes: false)); // property value - current++; - row = document._parsedData.Get(current); - WriteValue(current, row); + var valueCursor = current + 1; + var valueRow = document._parsedData.Get(valueCursor); + current = valueCursor; + WriteValue(current, valueRow); // next property (move past value) - if (row.IsSimpleValue) + if (valueRow.IsSimpleValue) { current++; } else { - current += row.NumberOfRows; + current += valueRow.NumberOfRows; } } diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonNullIgnoreCondition.cs b/src/HotChocolate/Json/src/Json/JsonNullIgnoreCondition.cs similarity index 94% rename from src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonNullIgnoreCondition.cs rename to src/HotChocolate/Json/src/Json/JsonNullIgnoreCondition.cs index e6e05aa5d84..f7e6e4f5131 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonNullIgnoreCondition.cs +++ b/src/HotChocolate/Json/src/Json/JsonNullIgnoreCondition.cs @@ -1,4 +1,4 @@ -namespace HotChocolate.Execution; +namespace HotChocolate.Text.Json; /// /// Specifies when null values are ignored. diff --git a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Literal.cs b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Literal.cs index 5289cf48350..e9a02d5f43b 100644 --- a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Literal.cs +++ b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Literal.cs @@ -13,6 +13,17 @@ public sealed partial class JsonWriter /// public void WriteNullValue() { + if (HasDeferredPropertyName) + { + DiscardDeferredPropertyName(); + return; + } + + if (IgnoreNullListElements && IsInArray) + { + return; + } + WriteLiteralByOptions(JsonConstants.NullValue); SetFlagToAddListSeparatorBeforeNextItem(); @@ -28,6 +39,8 @@ public void WriteNullValue() /// public void WriteBooleanValue(bool value) { + FlushDeferredPropertyName(); + if (value) { WriteLiteralByOptions(JsonConstants.TrueValue); diff --git a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Number.cs b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Number.cs index b5a6fb66ca2..e4dc0629437 100644 --- a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Number.cs +++ b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Number.cs @@ -22,6 +22,8 @@ public sealed partial class JsonWriter /// public void WriteNumberValue(ReadOnlySpan utf8FormattedNumber) { + FlushDeferredPropertyName(); + ValidateValue(utf8FormattedNumber); if (_indented) diff --git a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.PropertyName.cs b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.PropertyName.cs index bcaa3068f1e..8e86bde088a 100644 --- a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.PropertyName.cs +++ b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.PropertyName.cs @@ -44,6 +44,13 @@ public void WritePropertyName(string propertyName) /// public void WritePropertyName(ReadOnlySpan propertyName) { + FlushDeferredPropertyName(); + + if (IgnoreNullFields) + { + BeginDeferPropertyName(); + } + ValidateProperty(propertyName); var propertyIdx = NeedsEscaping(propertyName, _options.Encoder); @@ -189,6 +196,13 @@ private void WriteStringIndentedPropertyName(ReadOnlySpan escapedPropertyN /// public void WritePropertyName(ReadOnlySpan utf8PropertyName) { + FlushDeferredPropertyName(); + + if (IgnoreNullFields) + { + BeginDeferPropertyName(); + } + ValidateProperty(utf8PropertyName); var propertyIdx = NeedsEscaping(utf8PropertyName, _options.Encoder); @@ -210,6 +224,13 @@ public void WritePropertyName(ReadOnlySpan utf8PropertyName) private void WritePropertyNameUnescaped(ReadOnlySpan utf8PropertyName) { + FlushDeferredPropertyName(); + + if (IgnoreNullFields) + { + BeginDeferPropertyName(); + } + ValidateProperty(utf8PropertyName); WriteStringByOptionsPropertyName(utf8PropertyName); diff --git a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.String.cs b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.String.cs index 47822a393f8..10e3b187a62 100644 --- a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.String.cs +++ b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.String.cs @@ -51,6 +51,8 @@ public void WriteStringValue(string? value) /// public void WriteStringValue(ReadOnlySpan value) { + FlushDeferredPropertyName(); + WriteStringEscape(value); SetFlagToAddListSeparatorBeforeNextItem(); @@ -192,6 +194,8 @@ private void WriteStringEscapeValue(ReadOnlySpan value, int firstEscapeInd /// public void WriteStringValue(ReadOnlySpan utf8Value, bool skipEscaping = false) { + FlushDeferredPropertyName(); + ValidateValue(utf8Value); if (skipEscaping) @@ -392,6 +396,8 @@ private void WriteStringEscapeValue(ReadOnlySpan utf8Value, int firstEscap /// internal void WriteNumberValueAsStringUnescaped(ReadOnlySpan utf8Value) { + FlushDeferredPropertyName(); + // The value has been validated prior to calling this method. WriteStringByOptions(utf8Value); diff --git a/src/HotChocolate/Json/src/Json/JsonWriter.cs b/src/HotChocolate/Json/src/Json/JsonWriter.cs index b22be865812..c73b3aa927a 100644 --- a/src/HotChocolate/Json/src/Json/JsonWriter.cs +++ b/src/HotChocolate/Json/src/Json/JsonWriter.cs @@ -9,7 +9,7 @@ namespace HotChocolate.Text.Json; public sealed partial class JsonWriter { private readonly JsonWriterOptions _options; - private readonly IBufferWriter _writer; + private IBufferWriter _writer; // The highest order bit of _currentDepth is used to discern whether we are writing the first item in a list or not. // if (_currentDepth >> 31) == 1, add a list separator before writing the item @@ -26,10 +26,28 @@ public sealed partial class JsonWriter private readonly bool _indented; private readonly int _maxDepth; - public JsonWriter(IBufferWriter writer, JsonWriterOptions options) + // Deferred property name support for transparent null field omission. + // When IgnoreNullFields is true, WritePropertyName writes to _deferBuffer instead of + // _writer. If the next value is null, we discard the deferred bytes (rollback). + // If the next value is non-null, we flush the deferred bytes to the real writer first. + private DeferBuffer? _deferBuffer; + private IBufferWriter? _realWriter; + private int _savedCurrentDepth; + private JsonTokenType _savedTokenType; + + // Container type tracking for transparent null list element omission. + // Bit N = 1 means depth N is an array, bit N = 0 means object. + // Supports up to 64 levels of nesting. + private long _containerTypeStack; + + public JsonWriter( + IBufferWriter writer, + JsonWriterOptions options, + JsonNullIgnoreCondition nullIgnoreCondition = JsonNullIgnoreCondition.None) { _writer = writer; _options = options; + NullIgnoreCondition = nullIgnoreCondition; #if NET9_0_OR_GREATER Debug.Assert(options.NewLine is "\n" or "\r\n", "Invalid NewLine string."); @@ -52,6 +70,26 @@ public JsonWriter(IBufferWriter writer, JsonWriterOptions options) /// public JsonWriterOptions Options => _options; + /// + /// Gets the null ignore condition that controls whether null values + /// are omitted from the JSON output. + /// + public JsonNullIgnoreCondition NullIgnoreCondition { get; set; } + + /// + /// Getswha a value indicating whether null fields should be omitted + /// when writing JSON objects. + /// + public bool IgnoreNullFields + => (NullIgnoreCondition & JsonNullIgnoreCondition.Fields) == JsonNullIgnoreCondition.Fields; + + /// + /// Gets a value indicating whether null elements should be omitted + /// when writing JSON arrays. + /// + public bool IgnoreNullListElements + => (NullIgnoreCondition & JsonNullIgnoreCondition.Lists) == JsonNullIgnoreCondition.Lists; + private int Indentation => CurrentDepth * _indentLength; internal JsonTokenType TokenType => _tokenType; @@ -62,6 +100,87 @@ public JsonWriter(IBufferWriter writer, JsonWriterOptions options) /// public int CurrentDepth => _currentDepth & JsonConstants.RemoveFlagsBitMask; + private bool HasDeferredPropertyName + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _realWriter is not null; + } + + private bool IsInArray + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var depth = CurrentDepth; + if (depth is 0 or > 64) + { + return false; + } + + return (_containerTypeStack & (1L << (depth - 1))) != 0; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FlushDeferredPropertyName() + { + if (_realWriter is not null) + { + FlushDeferredPropertyNameSlow(); + } + } + + private void FlushDeferredPropertyNameSlow() + { + var deferred = _deferBuffer!; + _writer = _realWriter!; + _realWriter = null; + + var span = _writer.GetSpan(deferred.WrittenCount); + deferred.WrittenSpan.CopyTo(span); + _writer.Advance(deferred.WrittenCount); + } + + private void DiscardDeferredPropertyName() + { + _writer = _realWriter!; + _realWriter = null; + _currentDepth = _savedCurrentDepth; + _tokenType = _savedTokenType; + } + + private void BeginDeferPropertyName() + { + _savedCurrentDepth = _currentDepth; + _savedTokenType = _tokenType; + + _deferBuffer ??= new DeferBuffer(); + _deferBuffer.Reset(); + + _realWriter = _writer; + _writer = _deferBuffer; + } + + private void SetContainerTypeArray() + { + var depth = CurrentDepth; + + if (depth is > 0 and <= 64) + { + _containerTypeStack |= 1L << (depth - 1); + } + } + + private void SetContainerTypeObject() + { + var depth = CurrentDepth; + + if (depth is > 0 and <= 64) + { + _containerTypeStack &= ~(1L << (depth - 1)); + } + } + /// /// Writes the beginning of a JSON array. /// @@ -71,8 +190,10 @@ public JsonWriter(IBufferWriter writer, JsonWriterOptions options) /// public void WriteStartArray() { + FlushDeferredPropertyName(); WriteStart(JsonConstants.OpenBracket); _tokenType = JsonTokenType.StartArray; + SetContainerTypeArray(); } /// @@ -84,8 +205,10 @@ public void WriteStartArray() /// public void WriteStartObject() { + FlushDeferredPropertyName(); WriteStart(JsonConstants.OpenBrace); _tokenType = JsonTokenType.StartObject; + SetContainerTypeObject(); } private void WriteStart(byte token) @@ -317,6 +440,8 @@ public static void ValidateValue(ReadOnlySpan value) /// The raw UTF-8 encoded JSON to write. public void WriteRawValue(ReadOnlySpan utf8Json) { + FlushDeferredPropertyName(); + var maxRequired = utf8Json.Length + 1; // Optionally, 1 list separator var bytesWritten = 0; @@ -334,4 +459,43 @@ public void WriteRawValue(ReadOnlySpan utf8Json) SetFlagToAddListSeparatorBeforeNextItem(); } + + /// + /// Internal buffer used for deferred property name writes. + /// + private sealed class DeferBuffer : IBufferWriter + { + private byte[] _buffer = new byte[256]; + private int _written; + + public int WrittenCount => _written; + + public ReadOnlySpan WrittenSpan => _buffer.AsSpan(0, _written); + + public void Reset() => _written = 0; + + public void Advance(int count) => _written += count; + + public Memory GetMemory(int sizeHint = 0) + { + EnsureCapacity(sizeHint); + return _buffer.AsMemory(_written); + } + + public Span GetSpan(int sizeHint = 0) + { + EnsureCapacity(sizeHint); + return _buffer.AsSpan(_written); + } + + private void EnsureCapacity(int sizeHint) + { + var required = _written + Math.Max(sizeHint, 1); + + if (required > _buffer.Length) + { + Array.Resize(ref _buffer, Math.Max(_buffer.Length * 2, required)); + } + } + } } diff --git a/src/HotChocolate/Json/test/Json.Tests/JsonWriterNullIgnoreTests.cs b/src/HotChocolate/Json/test/Json.Tests/JsonWriterNullIgnoreTests.cs new file mode 100644 index 00000000000..4ba1bf38e1c --- /dev/null +++ b/src/HotChocolate/Json/test/Json.Tests/JsonWriterNullIgnoreTests.cs @@ -0,0 +1,435 @@ +using System.Buffers; +using System.Text; +using System.Text.Json; + +namespace HotChocolate.Text.Json; + +public class JsonWriterNullIgnoreTests +{ + [Fact] + public void Default_NullIgnoreCondition_IsNone() + { + // arrange + var buffer = new ArrayBufferWriter(); + var options = new JsonWriterOptions { SkipValidation = true }; + var writer = new JsonWriter(buffer, options); + + // assert + Assert.Equal(JsonNullIgnoreCondition.None, writer.NullIgnoreCondition); + Assert.False(writer.IgnoreNullFields); + Assert.False(writer.IgnoreNullListElements); + } + + [Fact] + public void NullIgnoreCondition_Fields_SetsIgnoreNullFields() + { + // arrange + var buffer = new ArrayBufferWriter(); + var options = new JsonWriterOptions { SkipValidation = true }; + var writer = new JsonWriter(buffer, options, JsonNullIgnoreCondition.Fields); + + // assert + Assert.Equal(JsonNullIgnoreCondition.Fields, writer.NullIgnoreCondition); + Assert.True(writer.IgnoreNullFields); + Assert.False(writer.IgnoreNullListElements); + } + + [Fact] + public void NullIgnoreCondition_Lists_SetsIgnoreNullListElements() + { + // arrange + var buffer = new ArrayBufferWriter(); + var options = new JsonWriterOptions { SkipValidation = true }; + var writer = new JsonWriter(buffer, options, JsonNullIgnoreCondition.Lists); + + // assert + Assert.Equal(JsonNullIgnoreCondition.Lists, writer.NullIgnoreCondition); + Assert.False(writer.IgnoreNullFields); + Assert.True(writer.IgnoreNullListElements); + } + + [Fact] + public void NullIgnoreCondition_FieldsAndLists_SetsBoth() + { + // arrange + var buffer = new ArrayBufferWriter(); + var options = new JsonWriterOptions { SkipValidation = true }; + var writer = new JsonWriter(buffer, options, JsonNullIgnoreCondition.FieldsAndLists); + + // assert + Assert.Equal(JsonNullIgnoreCondition.FieldsAndLists, writer.NullIgnoreCondition); + Assert.True(writer.IgnoreNullFields); + Assert.True(writer.IgnoreNullListElements); + } + + [Fact] + public void IgnoreNullFields_OmitsNullFieldValues() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("name"); + writer.WriteStringValue("Alice"); + writer.WritePropertyName("age"); + writer.WriteNullValue(); + writer.WritePropertyName("email"); + writer.WriteStringValue("alice@example.com"); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"name":"Alice","email":"alice@example.com"}""", json); + } + + [Fact] + public void IgnoreNullFields_KeepsNonNullFields() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("a"); + writer.WriteNumberValue(1); + writer.WritePropertyName("b"); + writer.WriteBooleanValue(true); + writer.WritePropertyName("c"); + writer.WriteStringValue("hello"); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"a":1,"b":true,"c":"hello"}""", json); + } + + [Fact] + public void IgnoreNullFields_AllFieldsNull_WritesEmptyObject() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("a"); + writer.WriteNullValue(); + writer.WritePropertyName("b"); + writer.WriteNullValue(); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("{}", json); + } + + [Fact] + public void IgnoreNullFields_NullStringValue_OmitsField() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("name"); + writer.WriteStringValue((string?)null); + writer.WritePropertyName("value"); + writer.WriteNumberValue(42); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"value":42}""", json); + } + + [Fact] + public void IgnoreNullFields_NestedObject_OmitsNullFieldsAtAllLevels() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("outer"); + writer.WriteStartObject(); + writer.WritePropertyName("inner"); + writer.WriteStringValue("value"); + writer.WritePropertyName("nullField"); + writer.WriteNullValue(); + writer.WriteEndObject(); + writer.WritePropertyName("topNull"); + writer.WriteNullValue(); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"outer":{"inner":"value"}}""", json); + } + + [Fact] + public void IgnoreNullFields_PropertyFollowedByStartObject_FlushesPropertyName() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("data"); + writer.WriteStartObject(); + writer.WritePropertyName("id"); + writer.WriteNumberValue(1); + writer.WriteEndObject(); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"data":{"id":1}}""", json); + } + + [Fact] + public void IgnoreNullFields_PropertyFollowedByStartArray_FlushesPropertyName() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("items"); + writer.WriteStartArray(); + writer.WriteNumberValue(1); + writer.WriteNumberValue(2); + writer.WriteEndArray(); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"items":[1,2]}""", json); + } + + [Fact] + public void IgnoreNullFields_PropertyFollowedByRawValue_FlushesPropertyName() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("raw"); + writer.WriteRawValue("true"u8); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"raw":true}""", json); + } + + [Fact] + public void IgnoreNullListElements_OmitsNullsFromArray() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Lists, writer => + { + writer.WriteStartArray(); + writer.WriteNumberValue(1); + writer.WriteNullValue(); + writer.WriteNumberValue(2); + writer.WriteNullValue(); + writer.WriteNumberValue(3); + writer.WriteEndArray(); + }); + + // assert + Assert.Equal("[1,2,3]", json); + } + + [Fact] + public void IgnoreNullListElements_AllNulls_WritesEmptyArray() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Lists, writer => + { + writer.WriteStartArray(); + writer.WriteNullValue(); + writer.WriteNullValue(); + writer.WriteEndArray(); + }); + + // assert + Assert.Equal("[]", json); + } + + [Fact] + public void IgnoreNullListElements_NestedArray_OmitsNullsAtAllLevels() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Lists, writer => + { + writer.WriteStartArray(); + writer.WriteStartArray(); + writer.WriteNullValue(); + writer.WriteNumberValue(1); + writer.WriteEndArray(); + writer.WriteNullValue(); + writer.WriteEndArray(); + }); + + // assert + Assert.Equal("[[1]]", json); + } + + [Fact] + public void IgnoreNullListElements_DoesNotAffectObjectFields() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Lists, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("field"); + writer.WriteNullValue(); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"field":null}""", json); + } + + [Fact] + public void IgnoreNullFields_DoesNotAffectArrayElements() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartArray(); + writer.WriteNullValue(); + writer.WriteNumberValue(1); + writer.WriteEndArray(); + }); + + // assert + Assert.Equal("[null,1]", json); + } + + [Fact] + public void FieldsAndLists_OmitsBoth() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.FieldsAndLists, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("name"); + writer.WriteNullValue(); + writer.WritePropertyName("items"); + writer.WriteStartArray(); + writer.WriteNullValue(); + writer.WriteNumberValue(1); + writer.WriteNullValue(); + writer.WriteEndArray(); + writer.WritePropertyName("active"); + writer.WriteBooleanValue(true); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"items":[1],"active":true}""", json); + } + + [Fact] + public void None_WritesAllNulls() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.None, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("field"); + writer.WriteNullValue(); + writer.WritePropertyName("items"); + writer.WriteStartArray(); + writer.WriteNullValue(); + writer.WriteEndArray(); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"field":null,"items":[null]}""", json); + } + + [Fact] + public void IgnoreNullFields_ObjectInsideArray_OmitsNullFieldsInNestedObject() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartArray(); + writer.WriteStartObject(); + writer.WritePropertyName("id"); + writer.WriteNumberValue(1); + writer.WritePropertyName("name"); + writer.WriteNullValue(); + writer.WriteEndObject(); + writer.WriteEndArray(); + }); + + // assert + Assert.Equal("""[{"id":1}]""", json); + } + + [Fact] + public void IgnoreNullListElements_ArrayInsideObject_OmitsNullElements() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Lists, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("items"); + writer.WriteStartArray(); + writer.WriteNullValue(); + writer.WriteStringValue("a"); + writer.WriteEndArray(); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"items":["a"]}""", json); + } + + [Fact] + public void IgnoreNullFields_NullFieldBeforeLastField_CorrectCommas() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("a"); + writer.WriteNumberValue(1); + writer.WritePropertyName("b"); + writer.WriteNullValue(); + writer.WritePropertyName("c"); + writer.WriteNumberValue(3); + writer.WriteEndObject(); + }); + + // assert - verify no trailing comma after "a":1 + Assert.Equal("""{"a":1,"c":3}""", json); + } + + [Fact] + public void IgnoreNullFields_FirstFieldNull_CorrectOutput() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("first"); + writer.WriteNullValue(); + writer.WritePropertyName("second"); + writer.WriteNumberValue(2); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"second":2}""", json); + } + + private static string WriteJson(JsonNullIgnoreCondition condition, Action write) + { + var buffer = new ArrayBufferWriter(); + var options = new JsonWriterOptions { SkipValidation = true }; + var writer = new JsonWriter(buffer, options, condition); + + write(writer); + + return Encoding.UTF8.GetString(buffer.WrittenSpan); + } +} From ea1a7a1df83b077fb0af9653aa28a471370cef9c Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sat, 14 Feb 2026 19:53:52 +0100 Subject: [PATCH 41/46] Fixed more tests --- .../Parsers/DefaultHttpRequestParser.cs | 14 +++-- .../AspNetCore.Tests/DeferOverHttpTests.cs | 20 +++---- ...dlerTests.FilterOnlyNullRefExceptions.json | 12 ++-- .../Execution.Tests/MiddlewareContextTests.cs | 8 +-- ...alizerTests.Serialize_Response_Stream.snap | 16 +++-- .../StarWars/Resolvers/SharedResolvers.cs | 11 +++- .../Core/test/StarWars/Types/DroidType.cs | 2 +- .../Core/test/StarWars/Types/HumanType.cs | 2 +- ...g_Throws_On_InvalidInputs_InvalidArgs.snap | 12 ++-- .../Types/Scalars/Base64StringTypeTests.cs | 4 +- .../Types/Scalars/ByteArrayTypeTests.cs | 4 +- .../Types.Tests/Types/Scalars/UriTypeTests.cs | 4 +- .../Types.Tests/Types/Scalars/UrlTypeTests.cs | 4 +- .../Types/Scalars/UuidTypeTests.cs | 4 +- ...One_Option_Provided_But_Value_Is_Null.yaml | 2 +- .../IntegrationTests.Execution_Error.md | 60 ------------------- .../AnyScalarDefaultSerializationTest.cs | 2 +- .../Integration/MultiProfileTest.Client.cs | 6 +- ...gmentIncludeAndSkipDirectiveTest.Client.cs | 6 +- .../Integration/UploadScalarTest.Client.cs | 36 +++++------ .../UploadScalar_InMemoryTest.Client.cs | 36 +++++------ ...st.Execute_StarWarsIntrospection_Test.snap | 20 +++---- ...lient_MapperMapsEntityOnRootCorrectly.snap | 6 +- ...apsEntityOnRootCorrectly_With_Records.snap | 6 +- ...dGeneratorTests.UnionWithNestedObject.snap | 12 ++-- ...eneratorTests.Operation_With_Comments.snap | 12 ++-- ...tion_With_Comments_With_Input_Records.snap | 12 ++-- ...ests.Operation_With_Complex_Arguments.snap | 12 ++-- ...orTests.Operation_With_FirstNonUpload.snap | 6 +- ...torTests.Operation_With_LastNonUpload.snap | 6 +- ...ratorTests.Operation_With_UploadAsArg.snap | 30 +++++----- ...torTests.Operation_With_Type_Argument.snap | 6 +- ...sts.Generate_ChatClient_AllOperations.snap | 7 ++- ...sts.Operation_With_MultipleOperations.snap | 36 +++++------ .../ScalarGeneratorTests.Uri_Type.snap | 1 - ...emaGeneratorTests.Create_GetFeatsPage.snap | 6 +- ...pleSearch_From_ActiveDirectory_Schema.snap | 19 +++--- ...atorTests.Create_Query_With_Skip_Take.snap | 12 ++-- ...eratorTests.FieldsWithUnderlineInName.snap | 6 +- ...chemaGeneratorTests.QueryInterference.snap | 30 +++++++--- ...torTests.Operation_With_Type_Argument.snap | 6 +- .../RazorGeneratorTests.cs | 36 ++++++----- ...azorGeneratorTests.Query_And_Mutation.snap | 12 ++-- .../Analyzers/DocumentAnalyzerTests.cs | 2 +- 44 files changed, 270 insertions(+), 296 deletions(-) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs index 536adc72b99..7648af96dc9 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs @@ -62,8 +62,11 @@ public async ValueTask ParseRequestAsync( throw new GraphQLRequestException("Request size exceeds maximum allowed size."); } - // We tell the pipe that we've examined everything but consumed nothing yet. - requestBody.AdvanceTo(result.Buffer.Start, result.Buffer.End); + if (!result.IsCompleted && !result.IsCanceled) + { + // We tell the pipe that we've examined everything but consumed nothing yet. + requestBody.AdvanceTo(result.Buffer.Start, result.Buffer.End); + } } while (result is { IsCompleted: false, IsCanceled: false }); @@ -124,8 +127,11 @@ public async ValueTask ParsePersistedOperationRequestAsync( throw new GraphQLRequestException("Request size exceeds maximum allowed size."); } - // We tell the pipe that we've examined everything but consumed nothing yet. - requestBody.AdvanceTo(result.Buffer.Start, result.Buffer.End); + if (!result.IsCompleted && !result.IsCanceled) + { + // We tell the pipe that we've examined everything but consumed nothing yet. + requestBody.AdvanceTo(result.Buffer.Start, result.Buffer.End); + } } while (result is { IsCompleted: false, IsCanceled: false }); diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs index 5cd9b75c40d..166576cfa6c 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs @@ -62,11 +62,11 @@ ... @defer { --- Content-Type: application/json; charset=utf-8 - {"data":{"product":{"name":"Abc"}},"pending":[{"id":2,"path":["product"]}],"hasNext":true} + {"data":{"product":{"name":"Abc"}},"pending":[{"id":"2","path":["product"]}],"hasNext":true} --- Content-Type: application/json; charset=utf-8 - {"incremental":[{"id":2,"data":{"description":"Abc desc"}}],"completed":[{"id":2}],"hasNext":false} + {"incremental":[{"id":"2","data":{"description":"Abc desc"}}],"completed":[{"id":"2"}],"hasNext":false} ----- """); @@ -112,10 +112,10 @@ ... @defer { .MatchInline( """ event: next - data: {"data":{"product":{"name":"Abc"}},"pending":[{"id":2,"path":["product"]}],"hasNext":true} + data: {"data":{"product":{"name":"Abc"}},"pending":[{"id":"2","path":["product"]}],"hasNext":true} event: next - data: {"incremental":[{"id":2,"data":{"description":"Abc desc"}}],"completed":[{"id":2}],"hasNext":false} + data: {"incremental":[{"id":"2","data":{"description":"Abc desc"}}],"completed":[{"id":"2"}],"hasNext":false} event: complete @@ -174,11 +174,11 @@ ... @defer(label: "productDescription") { --- Content-Type: application/json; charset=utf-8 - {"data":{"product":{"name":"Abc"}},"pending":[{"id":2,"path":["product"],"label":"productDescription"}],"hasNext":true} + {"data":{"product":{"name":"Abc"}},"pending":[{"id":"2","path":["product"],"label":"productDescription"}],"hasNext":true} --- Content-Type: application/json; charset=utf-8 - {"incremental":[{"id":2,"data":{"description":"Abc desc"}}],"completed":[{"id":2}],"hasNext":false} + {"incremental":[{"id":"2","data":{"description":"Abc desc"}}],"completed":[{"id":"2"}],"hasNext":false} ----- """); @@ -331,11 +331,11 @@ ... on Droid @defer(label: "droid_details") { --- Content-Type: application/json; charset=utf-8 - {"data":{"hero":{"name":"R2-D2"}},"pending":[{"id":2,"path":["hero"],"label":"droid_details"}],"hasNext":true} + {"data":{"hero":{"name":"R2-D2"}},"pending":[{"id":"2","path":["hero"],"label":"droid_details"}],"hasNext":true} --- Content-Type: application/json; charset=utf-8 - {"incremental":[{"id":2,"data":{"primaryFunction":"Astromech"}}],"completed":[{"id":2}],"hasNext":false} + {"incremental":[{"id":"2","data":{"primaryFunction":"Astromech"}}],"completed":[{"id":"2"}],"hasNext":false} ----- """); @@ -393,11 +393,11 @@ ... on Droid @defer(label: "droid_details", if: $if) { --- Content-Type: application/json; charset=utf-8 - {"data":{"hero":{"name":"R2-D2"}},"pending":[{"id":2,"path":["hero"],"label":"droid_details"}],"hasNext":true} + {"data":{"hero":{"name":"R2-D2"}},"pending":[{"id":"2","path":["hero"],"label":"droid_details"}],"hasNext":true} --- Content-Type: application/json; charset=utf-8 - {"incremental":[{"id":2,"data":{"primaryFunction":"Astromech"}}],"completed":[{"id":2}],"hasNext":false} + {"incremental":[{"id":"2","data":{"primaryFunction":"Astromech"}}],"completed":[{"id":"2"}],"hasNext":false} ----- """); diff --git a/src/HotChocolate/Core/test/Execution.Tests/Errors/__snapshots__/ErrorHandlerTests.FilterOnlyNullRefExceptions.json b/src/HotChocolate/Core/test/Execution.Tests/Errors/__snapshots__/ErrorHandlerTests.FilterOnlyNullRefExceptions.json index 4cfef53f3ea..d9767e95a5e 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Errors/__snapshots__/ErrorHandlerTests.FilterOnlyNullRefExceptions.json +++ b/src/HotChocolate/Core/test/Execution.Tests/Errors/__snapshots__/ErrorHandlerTests.FilterOnlyNullRefExceptions.json @@ -1,11 +1,5 @@ { "errors": [ - { - "message": "Unexpected Execution Error", - "path": [ - "foo" - ] - }, { "message": "Unexpected Execution Error", "path": [ @@ -14,6 +8,12 @@ "extensions": { "code": "NullRef" } + }, + { + "message": "Unexpected Execution Error", + "path": [ + "foo" + ] } ], "data": { diff --git a/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs b/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs index 79fe1136c93..276b6280a31 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs @@ -334,11 +334,9 @@ public async Task SetResultContextData_Delegate_IntValue_When_Deferred() continue; } - // TODO : FIX THIS TEST - throw new InvalidOperationException(); - // Assert.NotNull(queryResult.Incremental?[0].ContextData); - // Assert.True(queryResult.Incremental[0].ContextData!.TryGetValue("abc", out var value)); - // Assert.Equal(2, value); + Assert.NotNull(queryResult.ContextData); + Assert.True(queryResult.ContextData.TryGetValue("abc", out var value)); + Assert.Equal(2, value); } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap b/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap index 1127ea8eae7..31c7de9c494 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap @@ -1,6 +1,10 @@ - ---- -Content-Type: application/json; charset=utf-8 - -{"data":{"hero":{"id":"2001"}},"pending":[{"id":"2","path":["hero"],"label":"friends"}],"incremental":[{"id":"2","data":{"friends":{"nodes":[{"id":"1000","name":"Luke Skywalker"},{"id":"1002","name":"Han Solo"},{"id":"1003","name":"Leia Organa"}]}}}],"completed":[{"id":"2"}],"hasNext":false} ------ + +--- +Content-Type: application/json; charset=utf-8 + +{"data":{"hero":{"id":"2001"}},"pending":[{"id":"2","path":["hero"],"label":"friends"}],"hasNext":true} +--- +Content-Type: application/json; charset=utf-8 + +{"incremental":[{"id":"2","data":{"friends":{"nodes":[{"id":"1000","name":"Luke Skywalker"},{"id":"1002","name":"Han Solo"},{"id":"1003","name":"Leia Organa"}]}}}],"completed":[{"id":"2"}],"hasNext":false} +----- diff --git a/src/HotChocolate/Core/test/StarWars/Resolvers/SharedResolvers.cs b/src/HotChocolate/Core/test/StarWars/Resolvers/SharedResolvers.cs index 2e4091fa3c2..473b02916e1 100644 --- a/src/HotChocolate/Core/test/StarWars/Resolvers/SharedResolvers.cs +++ b/src/HotChocolate/Core/test/StarWars/Resolvers/SharedResolvers.cs @@ -5,7 +5,16 @@ namespace HotChocolate.StarWars.Resolvers; public class SharedResolvers { - public IEnumerable GetCharacter( + public async Task> GetCharacters( + [Parent] ICharacter character, + [Service] CharacterRepository repository) + { + await Task.Delay(250); + + return GetCharactersInternal(character, repository); + } + + private IEnumerable GetCharactersInternal( [Parent] ICharacter character, [Service] CharacterRepository repository) { diff --git a/src/HotChocolate/Core/test/StarWars/Types/DroidType.cs b/src/HotChocolate/Core/test/StarWars/Types/DroidType.cs index 77a9307eea9..1857021c61f 100644 --- a/src/HotChocolate/Core/test/StarWars/Types/DroidType.cs +++ b/src/HotChocolate/Core/test/StarWars/Types/DroidType.cs @@ -20,7 +20,7 @@ protected override void Configure(IObjectTypeDescriptor descriptor) descriptor.Field(t => t.AppearsIn) .Type>(); - descriptor.Field(r => r.GetCharacter(null!, null!)) + descriptor.Field(r => r.GetCharacters(null!, null!)) .UsePaging() .Name("friends"); diff --git a/src/HotChocolate/Core/test/StarWars/Types/HumanType.cs b/src/HotChocolate/Core/test/StarWars/Types/HumanType.cs index 45ff5909165..bc883ee38f1 100644 --- a/src/HotChocolate/Core/test/StarWars/Types/HumanType.cs +++ b/src/HotChocolate/Core/test/StarWars/Types/HumanType.cs @@ -15,7 +15,7 @@ protected override void Configure(IObjectTypeDescriptor descriptor) descriptor.Field(t => t.AppearsIn).Type>(); descriptor - .Field(r => r.GetCharacter(null!, null!)) + .Field(r => r.GetCharacters(null!, null!)) .UsePaging() .Name("friends") .Parallel(); diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/__snapshots__/IdDescriptorTests.Id_Honors_CustomTypeNaming_Throws_On_InvalidInputs_InvalidArgs.snap b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/__snapshots__/IdDescriptorTests.Id_Honors_CustomTypeNaming_Throws_On_InvalidInputs_InvalidArgs.snap index 696c3baa90b..2d87122cbb5 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/__snapshots__/IdDescriptorTests.Id_Honors_CustomTypeNaming_Throws_On_InvalidInputs_InvalidArgs.snap +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/__snapshots__/IdDescriptorTests.Id_Honors_CustomTypeNaming_Throws_On_InvalidInputs_InvalidArgs.snap @@ -1,9 +1,9 @@ { "errors": [ { - "message": "The node id type name `FooFoo` does not match the expected type name `RenamedUser`.", + "message": "The node id type name `FooFooFluentSingle` does not match the expected type name `FooFooFluent`.", "path": [ - "validUserIdInput" + "validFluentFooIdInput" ] }, { @@ -13,15 +13,15 @@ ] }, { - "message": "The node id type name `FooFooFluentSingle` does not match the expected type name `FooFooFluent`.", + "message": "The node id type name `RenamedUser` does not match the expected type name `FooFooFluentSingle`.", "path": [ - "validFluentFooIdInput" + "validSingleTypeFluentFooIdInput" ] }, { - "message": "The node id type name `RenamedUser` does not match the expected type name `FooFooFluentSingle`.", + "message": "The node id type name `FooFoo` does not match the expected type name `RenamedUser`.", "path": [ - "validSingleTypeFluentFooIdInput" + "validUserIdInput" ] } ], diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/Base64StringTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/Base64StringTypeTests.cs index 2c789c6c16b..43e51873704 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/Base64StringTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/Base64StringTypeTests.cs @@ -52,10 +52,10 @@ public void IsValueCompatible_Null_ReturnsFalse() var type = new Base64StringType(); // act - var result = type.IsValueCompatible(null!); + void Error() => type.IsValueCompatible(null!); // assert - Assert.False(result); + Assert.Throws(Error); } [Fact] diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/ByteArrayTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/ByteArrayTypeTests.cs index ec9b34624aa..2e74f9c7010 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/ByteArrayTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/ByteArrayTypeTests.cs @@ -54,10 +54,10 @@ public void IsValueCompatible_Null_ReturnsFalse() var type = new ByteArrayType(); // act - var result = type.IsValueCompatible(null!); + void Error() => type.IsValueCompatible(null!); // assert - Assert.False(result); + Assert.Throws(Error); } [Fact] diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UriTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UriTypeTests.cs index 74ac81f3111..dced1c2561a 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UriTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UriTypeTests.cs @@ -245,10 +245,10 @@ public void IsValueCompatible_Null_ReturnsFalse() var type = new UriType(); // act - var compatible = type.IsValueCompatible(null!); + void Error() => type.IsValueCompatible(null!); // assert - Assert.False(compatible); + Assert.Throws(Error); } [Fact] diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UrlTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UrlTypeTests.cs index b0d093c6524..3ec78c1ae9a 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UrlTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UrlTypeTests.cs @@ -303,10 +303,10 @@ public void IsValueCompatible_Null_ReturnsFalse() var type = new UrlType(); // act - var compatible = type.IsValueCompatible(null!); + void Error() => type.IsValueCompatible(null!); // assert - Assert.False(compatible); + Assert.Throws(Error); } [Fact] diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UuidTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UuidTypeTests.cs index 0e572e35536..93557968bc7 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UuidTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UuidTypeTests.cs @@ -67,10 +67,10 @@ public void IsValueCompatible_Null() var type = new UuidType(); // act - var isCompatible = type.IsValueCompatible(null!); + void Error() => type.IsValueCompatible(null!); // assert - Assert.False(isCompatible); + Assert.Throws(Error); } [Fact] diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/VariableCoercionTests.OneOf_Only_One_Option_Provided_But_Value_Is_Null.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/VariableCoercionTests.OneOf_Only_One_Option_Provided_But_Value_Is_Null.yaml index b16b9651801..1ac9afa6bb5 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/VariableCoercionTests.OneOf_Only_One_Option_Provided_But_Value_Is_Null.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/VariableCoercionTests.OneOf_Only_One_Option_Provided_But_Value_Is_Null.yaml @@ -17,7 +17,7 @@ response: { "errors": [ { - "message": "\u0060null\u0060 was set to the field \u0060dog\u0060of the OneOf Input Object \u0060Pet\u0060. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", + "message": "\u0060null\u0060 was set to the field \u0060dog\u0060 of the OneOf Input Object \u0060Pet\u0060. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", "path": [ "pet" ], diff --git a/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/__snapshots__/IntegrationTests.Execution_Error.md b/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/__snapshots__/IntegrationTests.Execution_Error.md index 62e12732ce0..4335b92f807 100644 --- a/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/__snapshots__/IntegrationTests.Execution_Error.md +++ b/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/__snapshots__/IntegrationTests.Execution_Error.md @@ -7,12 +7,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -29,12 +23,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -51,12 +39,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -73,12 +55,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -95,12 +71,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -117,12 +87,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -139,12 +103,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -161,12 +119,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -183,12 +135,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -205,12 +151,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/AnyScalarDefaultSerializationTest.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/AnyScalarDefaultSerializationTest.cs index 52553997657..69feefb6510 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/AnyScalarDefaultSerializationTest.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/AnyScalarDefaultSerializationTest.cs @@ -19,7 +19,7 @@ public async Task Execute_AnyScalarDefaultSerialization_Test() // arrange using var cts = new CancellationTokenSource(20_000); using var host = TestServerHelper.CreateServer( - builder => builder.AddTypeExtension(), + builder => builder.AddTypeExtension().AddJsonTypeConverter(), out var port); var serviceCollection = new ServiceCollection(); serviceCollection.AddHttpClient( diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/MultiProfileTest.Client.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/MultiProfileTest.Client.cs index 2be6afab45a..89f60904c7d 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/MultiProfileTest.Client.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/MultiProfileTest.Client.cs @@ -1485,7 +1485,7 @@ public partial interface IOnReviewSubSubscription : global::StrawberryShake.IOpe /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename @@ -1523,7 +1523,7 @@ private CreateReviewMutMutationDocument() /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename @@ -1629,7 +1629,7 @@ private CreateReviewMutMutation(global::StrawberryShake.IOperationExecutor /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/StarWarsGetHeroWithFragmentIncludeAndSkipDirectiveTest.Client.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/StarWarsGetHeroWithFragmentIncludeAndSkipDirectiveTest.Client.cs index fe35c8227ff..bc55e9d51e5 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/StarWarsGetHeroWithFragmentIncludeAndSkipDirectiveTest.Client.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/StarWarsGetHeroWithFragmentIncludeAndSkipDirectiveTest.Client.cs @@ -657,7 +657,7 @@ public partial interface IGetHeroWithFragmentIncludeAndSkipDirective_Hero_Friend /// /// query GetHeroWithFragmentIncludeAndSkipDirective( /// $includePageInfo: Boolean = false - /// $skipPageInfo: Boolean = true + /// $skipPageInfo: Boolean = true /// ) { /// hero(episode: NEW_HOPE) { /// __typename @@ -723,7 +723,7 @@ private GetHeroWithFragmentIncludeAndSkipDirectiveQueryDocument() /// /// query GetHeroWithFragmentIncludeAndSkipDirective( /// $includePageInfo: Boolean = false - /// $skipPageInfo: Boolean = true + /// $skipPageInfo: Boolean = true /// ) { /// hero(episode: NEW_HOPE) { /// __typename @@ -863,7 +863,7 @@ private GetHeroWithFragmentIncludeAndSkipDirectiveQuery(global::StrawberryShake. /// /// query GetHeroWithFragmentIncludeAndSkipDirective( /// $includePageInfo: Boolean = false - /// $skipPageInfo: Boolean = true + /// $skipPageInfo: Boolean = true /// ) { /// hero(episode: NEW_HOPE) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalarTest.Client.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalarTest.Client.cs index 073de397d7b..6acad0a906c 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalarTest.Client.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalarTest.Client.cs @@ -556,12 +556,12 @@ public partial class BazInput : global::StrawberryShake.CodeGeneration.CSharp.In /// /// query TestUpload( /// $nonUpload: String - /// $single: Upload - /// $list: [Upload] - /// $nested: [[Upload]] - /// $object: TestInput - /// $objectList: [TestInput] - /// $objectNested: [[TestInput]] + /// $single: Upload + /// $list: [Upload] + /// $nested: [[Upload]] + /// $object: TestInput + /// $objectList: [TestInput] + /// $objectNested: [[TestInput]] /// ) { /// upload(nonUpload: $nonUpload, single: $single, list: $list, nested: $nested, object: $object, objectList: $objectList, objectNested: $objectNested) /// } @@ -595,12 +595,12 @@ private TestUploadQueryDocument() /// /// query TestUpload( /// $nonUpload: String - /// $single: Upload - /// $list: [Upload] - /// $nested: [[Upload]] - /// $object: TestInput - /// $objectList: [TestInput] - /// $objectNested: [[TestInput]] + /// $single: Upload + /// $list: [Upload] + /// $nested: [[Upload]] + /// $object: TestInput + /// $objectList: [TestInput] + /// $objectNested: [[TestInput]] /// ) { /// upload(nonUpload: $nonUpload, single: $single, list: $list, nested: $nested, object: $object, objectList: $objectList, objectNested: $objectNested) /// } @@ -978,12 +978,12 @@ private void MapFilesFromArgumentObjectNested(global::System.String path, global /// /// query TestUpload( /// $nonUpload: String - /// $single: Upload - /// $list: [Upload] - /// $nested: [[Upload]] - /// $object: TestInput - /// $objectList: [TestInput] - /// $objectNested: [[TestInput]] + /// $single: Upload + /// $list: [Upload] + /// $nested: [[Upload]] + /// $object: TestInput + /// $objectList: [TestInput] + /// $objectNested: [[TestInput]] /// ) { /// upload(nonUpload: $nonUpload, single: $single, list: $list, nested: $nested, object: $object, objectList: $objectList, objectNested: $objectNested) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalar_InMemoryTest.Client.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalar_InMemoryTest.Client.cs index 1fc02aba99f..8fd284d0326 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalar_InMemoryTest.Client.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalar_InMemoryTest.Client.cs @@ -556,12 +556,12 @@ public partial class BazInput : global::StrawberryShake.CodeGeneration.CSharp.In /// /// query TestUpload( /// $nonUpload: String - /// $single: Upload - /// $list: [Upload] - /// $nested: [[Upload]] - /// $object: TestInput - /// $objectList: [TestInput] - /// $objectNested: [[TestInput]] + /// $single: Upload + /// $list: [Upload] + /// $nested: [[Upload]] + /// $object: TestInput + /// $objectList: [TestInput] + /// $objectNested: [[TestInput]] /// ) { /// upload(nonUpload: $nonUpload, single: $single, list: $list, nested: $nested, object: $object, objectList: $objectList, objectNested: $objectNested) /// } @@ -595,12 +595,12 @@ private TestUploadQueryDocument() /// /// query TestUpload( /// $nonUpload: String - /// $single: Upload - /// $list: [Upload] - /// $nested: [[Upload]] - /// $object: TestInput - /// $objectList: [TestInput] - /// $objectNested: [[TestInput]] + /// $single: Upload + /// $list: [Upload] + /// $nested: [[Upload]] + /// $object: TestInput + /// $objectList: [TestInput] + /// $objectNested: [[TestInput]] /// ) { /// upload(nonUpload: $nonUpload, single: $single, list: $list, nested: $nested, object: $object, objectList: $objectList, objectNested: $objectNested) /// } @@ -978,12 +978,12 @@ private void MapFilesFromArgumentObjectNested(global::System.String path, global /// /// query TestUpload( /// $nonUpload: String - /// $single: Upload - /// $list: [Upload] - /// $nested: [[Upload]] - /// $object: TestInput - /// $objectList: [TestInput] - /// $objectNested: [[TestInput]] + /// $single: Upload + /// $list: [Upload] + /// $nested: [[Upload]] + /// $object: TestInput + /// $objectList: [TestInput] + /// $objectNested: [[TestInput]] /// ) { /// upload(nonUpload: $nonUpload, single: $single, list: $list, nested: $nested, object: $object, objectList: $objectList, objectNested: $objectNested) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap index 4935037e0db..8ff78c0829f 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap @@ -1027,7 +1027,7 @@ "Name": null, "OfType": { "Kind": "Scalar", - "Name": "JSON", + "Name": "Any", "OfType": null } }, @@ -1508,7 +1508,7 @@ "Args": [], "Type": { "Kind": "Scalar", - "Name": "JSON", + "Name": "Any", "OfType": null }, "IsDeprecated": false, @@ -1673,7 +1673,7 @@ "Args": [], "Type": { "Kind": "Scalar", - "Name": "JSON", + "Name": "Any", "OfType": null }, "IsDeprecated": false, @@ -1862,7 +1862,7 @@ "Args": [], "Type": { "Kind": "Scalar", - "Name": "JSON", + "Name": "Any", "OfType": null }, "IsDeprecated": false, @@ -2116,7 +2116,7 @@ }, { "Kind": "Scalar", - "Name": "JSON", + "Name": "Any", "Description": null, "Fields": null, "InputFields": null, @@ -4360,7 +4360,7 @@ "PossibleTypes": null, "OfType": { "__typename": "__Type", - "Name": "JSON", + "Name": "Any", "Kind": "Scalar", "Description": null, "Fields": null, @@ -5272,7 +5272,7 @@ "Args": [], "Type": { "__typename": "__Type", - "Name": "JSON", + "Name": "Any", "Kind": "Scalar", "Description": null, "Fields": null, @@ -5563,7 +5563,7 @@ "Args": [], "Type": { "__typename": "__Type", - "Name": "JSON", + "Name": "Any", "Kind": "Scalar", "Description": null, "Fields": null, @@ -5865,7 +5865,7 @@ "Args": [], "Type": { "__typename": "__Type", - "Name": "JSON", + "Name": "Any", "Kind": "Scalar", "Description": null, "Fields": null, @@ -6292,7 +6292,7 @@ }, { "__typename": "__Type", - "Name": "JSON", + "Name": "Any", "Kind": "Scalar", "Description": null, "Fields": null, diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly.snap index 9280e75cf73..4c19e7e710f 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly.snap @@ -1194,7 +1194,7 @@ namespace Foo.Bar /// /// mutation WriteMessage( /// $text: String! - /// $address: String! + /// $address: String! /// ) { /// sendMessage(input: { text: $text, recipientEmail: $address }) { /// __typename @@ -1252,7 +1252,7 @@ namespace Foo.Bar /// /// mutation WriteMessage( /// $text: String! - /// $address: String! + /// $address: String! /// ) { /// sendMessage(input: { text: $text, recipientEmail: $address }) { /// __typename @@ -1380,7 +1380,7 @@ namespace Foo.Bar /// /// mutation WriteMessage( /// $text: String! - /// $address: String! + /// $address: String! /// ) { /// sendMessage(input: { text: $text, recipientEmail: $address }) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly_With_Records.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly_With_Records.snap index 8f0a41ec59f..674703bcf27 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly_With_Records.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly_With_Records.snap @@ -1194,7 +1194,7 @@ namespace Foo.Bar /// /// mutation WriteMessage( /// $text: String! - /// $address: String! + /// $address: String! /// ) { /// sendMessage(input: { text: $text, recipientEmail: $address }) { /// __typename @@ -1252,7 +1252,7 @@ namespace Foo.Bar /// /// mutation WriteMessage( /// $text: String! - /// $address: String! + /// $address: String! /// ) { /// sendMessage(input: { text: $text, recipientEmail: $address }) { /// __typename @@ -1380,7 +1380,7 @@ namespace Foo.Bar /// /// mutation WriteMessage( /// $text: String! - /// $address: String! + /// $address: String! /// ) { /// sendMessage(input: { text: $text, recipientEmail: $address }) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityOrIdGeneratorTests.UnionWithNestedObject.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityOrIdGeneratorTests.UnionWithNestedObject.snap index b5cadd62689..f983c21967f 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityOrIdGeneratorTests.UnionWithNestedObject.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityOrIdGeneratorTests.UnionWithNestedObject.snap @@ -525,8 +525,8 @@ namespace Foo.Bar /// /// mutation StoreUserSettingFor( /// $userId: Int! - /// $customerId: Int! - /// $input: StoreUserSettingForInput! + /// $customerId: Int! + /// $input: StoreUserSettingForInput! /// ) { /// storeUserSettingFor(userId: $userId, customerId: $customerId, input: $input) { /// __typename @@ -575,8 +575,8 @@ namespace Foo.Bar /// /// mutation StoreUserSettingFor( /// $userId: Int! - /// $customerId: Int! - /// $input: StoreUserSettingForInput! + /// $customerId: Int! + /// $input: StoreUserSettingForInput! /// ) { /// storeUserSettingFor(userId: $userId, customerId: $customerId, input: $input) { /// __typename @@ -699,8 +699,8 @@ namespace Foo.Bar /// /// mutation StoreUserSettingFor( /// $userId: Int! - /// $customerId: Int! - /// $input: StoreUserSettingForInput! + /// $customerId: Int! + /// $input: StoreUserSettingForInput! /// ) { /// storeUserSettingFor(userId: $userId, customerId: $customerId, input: $input) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments.snap index df5e15bbfe1..dd5e34edc66 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments.snap @@ -412,8 +412,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -447,8 +447,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -600,8 +600,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments_With_Input_Records.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments_With_Input_Records.snap index 80a600ac18a..7cc4c2c8787 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments_With_Input_Records.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments_With_Input_Records.snap @@ -392,8 +392,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -427,8 +427,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -580,8 +580,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Complex_Arguments.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Complex_Arguments.snap index 78ddfc000fa..90dbe5587d0 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Complex_Arguments.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Complex_Arguments.snap @@ -406,8 +406,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -441,8 +441,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -594,8 +594,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_FirstNonUpload.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_FirstNonUpload.snap index 983939023a0..c730f354e16 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_FirstNonUpload.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_FirstNonUpload.snap @@ -98,7 +98,7 @@ namespace Foo.Bar /// /// query Test( /// $string: String! - /// $upload: Upload! + /// $upload: Upload! /// ) { /// foo(string: $string, upload: $upload) /// } @@ -132,7 +132,7 @@ namespace Foo.Bar /// /// query Test( /// $string: String! - /// $upload: Upload! + /// $upload: Upload! /// ) { /// foo(string: $string, upload: $upload) /// } @@ -241,7 +241,7 @@ namespace Foo.Bar /// /// query Test( /// $string: String! - /// $upload: Upload! + /// $upload: Upload! /// ) { /// foo(string: $string, upload: $upload) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_LastNonUpload.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_LastNonUpload.snap index 7484bb0da81..08eb6597e73 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_LastNonUpload.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_LastNonUpload.snap @@ -98,7 +98,7 @@ namespace Foo.Bar /// /// query Test( /// $upload: Upload! - /// $string: String! + /// $string: String! /// ) { /// foo(string: $string, upload: $upload) /// } @@ -132,7 +132,7 @@ namespace Foo.Bar /// /// query Test( /// $upload: Upload! - /// $string: String! + /// $string: String! /// ) { /// foo(string: $string, upload: $upload) /// } @@ -241,7 +241,7 @@ namespace Foo.Bar /// /// query Test( /// $upload: Upload! - /// $string: String! + /// $string: String! /// ) { /// foo(string: $string, upload: $upload) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_UploadAsArg.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_UploadAsArg.snap index e6a72eef3a0..e901618af1e 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_UploadAsArg.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_UploadAsArg.snap @@ -98,11 +98,11 @@ namespace Foo.Bar /// /// query Test( /// $upload: Upload! - /// $uploadNullable: Upload - /// $list: [Upload!]! - /// $listNullable: [Upload!] - /// $nestedList: [[Upload!]!]! - /// $nestedListNullable: [[Upload!]] + /// $uploadNullable: Upload + /// $list: [Upload!]! + /// $listNullable: [Upload!] + /// $nestedList: [[Upload!]!]! + /// $nestedListNullable: [[Upload!]] /// ) { /// foo(upload: $upload, uploadNullable: $uploadNullable, list: $list, listNullable: $listNullable, nestedList: $nestedList, nestedListNullable: $nestedListNullable) /// } @@ -136,11 +136,11 @@ namespace Foo.Bar /// /// query Test( /// $upload: Upload! - /// $uploadNullable: Upload - /// $list: [Upload!]! - /// $listNullable: [Upload!] - /// $nestedList: [[Upload!]!]! - /// $nestedListNullable: [[Upload!]] + /// $uploadNullable: Upload + /// $list: [Upload!]! + /// $listNullable: [Upload!] + /// $nestedList: [[Upload!]!]! + /// $nestedListNullable: [[Upload!]] /// ) { /// foo(upload: $upload, uploadNullable: $uploadNullable, list: $list, listNullable: $listNullable, nestedList: $nestedList, nestedListNullable: $nestedListNullable) /// } @@ -407,11 +407,11 @@ namespace Foo.Bar /// /// query Test( /// $upload: Upload! - /// $uploadNullable: Upload - /// $list: [Upload!]! - /// $listNullable: [Upload!] - /// $nestedList: [[Upload!]!]! - /// $nestedListNullable: [[Upload!]] + /// $uploadNullable: Upload + /// $list: [Upload!]! + /// $listNullable: [Upload!] + /// $nestedList: [[Upload!]!]! + /// $nestedListNullable: [[Upload!]] /// ) { /// foo(upload: $upload, uploadNullable: $uploadNullable, list: $list, listNullable: $listNullable, nestedList: $nestedList, nestedListNullable: $nestedListNullable) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/NoStoreStarWarsGeneratorTests.Operation_With_Type_Argument.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/NoStoreStarWarsGeneratorTests.Operation_With_Type_Argument.snap index 835fa7036b6..1d443994ae1 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/NoStoreStarWarsGeneratorTests.Operation_With_Type_Argument.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/NoStoreStarWarsGeneratorTests.Operation_With_Type_Argument.snap @@ -361,7 +361,7 @@ namespace Foo.Bar /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename @@ -399,7 +399,7 @@ namespace Foo.Bar /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename @@ -505,7 +505,7 @@ namespace Foo.Bar /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Generate_ChatClient_AllOperations.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Generate_ChatClient_AllOperations.snap index 02f25ebc704..472a66593ae 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Generate_ChatClient_AllOperations.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Generate_ChatClient_AllOperations.snap @@ -3280,7 +3280,7 @@ namespace Foo.Bar /// /// mutation SendMessageMut( /// $email: String! - /// $text: String! + /// $text: String! /// ) { /// sendMessage(input: { recipientEmail: $email, text: $text }) { /// __typename @@ -3350,7 +3350,7 @@ namespace Foo.Bar /// /// mutation SendMessageMut( /// $email: String! - /// $text: String! + /// $text: String! /// ) { /// sendMessage(input: { recipientEmail: $email, text: $text }) { /// __typename @@ -3490,7 +3490,7 @@ namespace Foo.Bar /// /// mutation SendMessageMut( /// $email: String! - /// $text: String! + /// $text: String! /// ) { /// sendMessage(input: { recipientEmail: $email, text: $text }) { /// __typename @@ -6042,6 +6042,7 @@ namespace Microsoft.Extensions.DependencyInjection global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, new global::StrawberryShake.Serialization.UrlSerializer("Url")); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => new global::StrawberryShake.Serialization.SerializerResolver(global::System.Linq.Enumerable.Concat(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(parentServices), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp)))); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton, global::Foo.Bar.State.GetPeopleResultFactory>(services); diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Operation_With_MultipleOperations.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Operation_With_MultipleOperations.snap index 8a0cacbe98c..5b3317c502f 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Operation_With_MultipleOperations.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Operation_With_MultipleOperations.snap @@ -552,8 +552,8 @@ namespace Foo.Bar /// /// query TestOperation( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -587,8 +587,8 @@ namespace Foo.Bar /// /// query TestOperation( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -740,8 +740,8 @@ namespace Foo.Bar /// /// query TestOperation( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -763,8 +763,8 @@ namespace Foo.Bar /// /// query TestOperation2( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -798,8 +798,8 @@ namespace Foo.Bar /// /// query TestOperation2( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -951,8 +951,8 @@ namespace Foo.Bar /// /// query TestOperation2( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -974,8 +974,8 @@ namespace Foo.Bar /// /// query TestOperation3( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -1009,8 +1009,8 @@ namespace Foo.Bar /// /// query TestOperation3( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -1162,8 +1162,8 @@ namespace Foo.Bar /// /// query TestOperation3( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/ScalarGeneratorTests.Uri_Type.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/ScalarGeneratorTests.Uri_Type.snap index 22236a3232c..78db124c2a5 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/ScalarGeneratorTests.Uri_Type.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/ScalarGeneratorTests.Uri_Type.snap @@ -592,7 +592,6 @@ namespace Microsoft.Extensions.DependencyInjection global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, new global::StrawberryShake.Serialization.UriSerializer("Uri")); - global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, new global::StrawberryShake.Serialization.UriSerializer("URI")); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => new global::StrawberryShake.Serialization.SerializerResolver(global::System.Linq.Enumerable.Concat(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(parentServices), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp)))); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton, global::Foo.Bar.State.GetPersonResultFactory>(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp)); diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_GetFeatsPage.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_GetFeatsPage.snap index 550d320531b..294d8f099a1 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_GetFeatsPage.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_GetFeatsPage.snap @@ -345,7 +345,7 @@ namespace Foo.Bar /// /// query GetFeatsPage( /// $skip: Int - /// $take: Int + /// $take: Int /// ) { /// feats(skip: $skip, take: $take) { /// __typename @@ -397,7 +397,7 @@ namespace Foo.Bar /// /// query GetFeatsPage( /// $skip: Int - /// $take: Int + /// $take: Int /// ) { /// feats(skip: $skip, take: $take) { /// __typename @@ -523,7 +523,7 @@ namespace Foo.Bar /// /// query GetFeatsPage( /// $skip: Int - /// $take: Int + /// $take: Int /// ) { /// feats(skip: $skip, take: $take) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_PeopleSearch_From_ActiveDirectory_Schema.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_PeopleSearch_From_ActiveDirectory_Schema.snap index a029eae6258..36b252ee4f6 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_PeopleSearch_From_ActiveDirectory_Schema.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_PeopleSearch_From_ActiveDirectory_Schema.snap @@ -651,9 +651,9 @@ namespace Foo.Bar /// /// query PeopleSearch( /// $term: String! - /// $skip: Int - /// $take: Int - /// $inactive: Boolean + /// $skip: Int + /// $take: Int + /// $inactive: Boolean /// ) { /// people: peopleSearch(term: $term, includeInactive: $inactive, skip: $skip, take: $take) { /// __typename @@ -728,9 +728,9 @@ namespace Foo.Bar /// /// query PeopleSearch( /// $term: String! - /// $skip: Int - /// $take: Int - /// $inactive: Boolean + /// $skip: Int + /// $take: Int + /// $inactive: Boolean /// ) { /// people: peopleSearch(term: $term, includeInactive: $inactive, skip: $skip, take: $take) { /// __typename @@ -909,9 +909,9 @@ namespace Foo.Bar /// /// query PeopleSearch( /// $term: String! - /// $skip: Int - /// $take: Int - /// $inactive: Boolean + /// $skip: Int + /// $take: Int + /// $inactive: Boolean /// ) { /// people: peopleSearch(term: $term, includeInactive: $inactive, skip: $skip, take: $take) { /// __typename @@ -1751,6 +1751,7 @@ namespace Microsoft.Extensions.DependencyInjection global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, new global::StrawberryShake.Serialization.UrlSerializer("Url")); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => new global::StrawberryShake.Serialization.SerializerResolver(global::System.Linq.Enumerable.Concat(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(parentServices), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp)))); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton, global::Foo.Bar.State.PeopleSearchResultFactory>(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp)); diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_Query_With_Skip_Take.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_Query_With_Skip_Take.snap index 94da8eaec7c..e8c9748a691 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_Query_With_Skip_Take.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_Query_With_Skip_Take.snap @@ -266,8 +266,8 @@ namespace Foo.Bar /// /// query SearchNewsItems( /// $query: String! - /// $skip: Int - /// $take: Int + /// $skip: Int + /// $take: Int /// ) { /// newsItems(skip: $skip, take: $take, query: $query) { /// __typename @@ -312,8 +312,8 @@ namespace Foo.Bar /// /// query SearchNewsItems( /// $query: String! - /// $skip: Int - /// $take: Int + /// $skip: Int + /// $take: Int /// ) { /// newsItems(skip: $skip, take: $take, query: $query) { /// __typename @@ -446,8 +446,8 @@ namespace Foo.Bar /// /// query SearchNewsItems( /// $query: String! - /// $skip: Int - /// $take: Int + /// $skip: Int + /// $take: Int /// ) { /// newsItems(skip: $skip, take: $take, query: $query) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.FieldsWithUnderlineInName.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.FieldsWithUnderlineInName.snap index 18c04f503de..8b529e65743 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.FieldsWithUnderlineInName.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.FieldsWithUnderlineInName.snap @@ -2961,7 +2961,7 @@ namespace Foo.Bar /// /// query GetBwr_TimeSeries( /// $where: bwr_TimeSeriesFilterInput - /// $readDataInput: ReadDataInput! + /// $readDataInput: ReadDataInput! /// ) { /// bwr_TimeSeries(where: $where) { /// __typename @@ -3050,7 +3050,7 @@ namespace Foo.Bar /// /// query GetBwr_TimeSeries( /// $where: bwr_TimeSeriesFilterInput - /// $readDataInput: ReadDataInput! + /// $readDataInput: ReadDataInput! /// ) { /// bwr_TimeSeries(where: $where) { /// __typename @@ -3214,7 +3214,7 @@ namespace Foo.Bar /// /// query GetBwr_TimeSeries( /// $where: bwr_TimeSeriesFilterInput - /// $readDataInput: ReadDataInput! + /// $readDataInput: ReadDataInput! /// ) { /// bwr_TimeSeries(where: $where) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.QueryInterference.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.QueryInterference.snap index 622434fface..47a1dbddc93 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.QueryInterference.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.QueryInterference.snap @@ -2226,9 +2226,13 @@ namespace Foo.Bar /// /// query GetFeatsPage( /// $skip: Int! - /// $take: Int! - /// $searchTerm: String! = "" - /// $order: [FeatSortInput!] = [ { name: ASC } ] + /// $take: Int! + /// $searchTerm: String! = "" + /// $order: [FeatSortInput!] = [ + /// { + /// name: ASC + /// } + /// ] /// ) { /// feats(skip: $skip, take: $take, order: $order, where: { or: [ { name: { contains: $searchTerm } }, { traits: { some: { name: { contains: $searchTerm } } } } ] }) { /// __typename @@ -2286,9 +2290,13 @@ namespace Foo.Bar /// /// query GetFeatsPage( /// $skip: Int! - /// $take: Int! - /// $searchTerm: String! = "" - /// $order: [FeatSortInput!] = [ { name: ASC } ] + /// $take: Int! + /// $searchTerm: String! = "" + /// $order: [FeatSortInput!] = [ + /// { + /// name: ASC + /// } + /// ] /// ) { /// feats(skip: $skip, take: $take, order: $order, where: { or: [ { name: { contains: $searchTerm } }, { traits: { some: { name: { contains: $searchTerm } } } } ] }) { /// __typename @@ -2447,9 +2455,13 @@ namespace Foo.Bar /// /// query GetFeatsPage( /// $skip: Int! - /// $take: Int! - /// $searchTerm: String! = "" - /// $order: [FeatSortInput!] = [ { name: ASC } ] + /// $take: Int! + /// $searchTerm: String! = "" + /// $order: [FeatSortInput!] = [ + /// { + /// name: ASC + /// } + /// ] /// ) { /// feats(skip: $skip, take: $take, order: $order, where: { or: [ { name: { contains: $searchTerm } }, { traits: { some: { name: { contains: $searchTerm } } } } ] }) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/StarWarsGeneratorTests.Operation_With_Type_Argument.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/StarWarsGeneratorTests.Operation_With_Type_Argument.snap index c7caa88e278..1732078ae63 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/StarWarsGeneratorTests.Operation_With_Type_Argument.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/StarWarsGeneratorTests.Operation_With_Type_Argument.snap @@ -361,7 +361,7 @@ namespace Foo.Bar /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename @@ -399,7 +399,7 @@ namespace Foo.Bar /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename @@ -505,7 +505,7 @@ namespace Foo.Bar /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/RazorGeneratorTests.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/RazorGeneratorTests.cs index b46ae17c344..506d2382e77 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/RazorGeneratorTests.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/RazorGeneratorTests.cs @@ -13,31 +13,35 @@ public void Query_And_Mutation() AssertResult( settings: new() { RazorComponents = true }, - @"query GetBars($a: String! $b: String) { - bars(a: $a b: $b) { - id - name - } + """ + query GetBars($a: String! $b: String) { + bars(a: $a b: $b) { + id + name + } } mutation SaveBars($a: String! $b: String) { - saveBar(a: $a b: $b) { - id - name - } - }", - @"type Query { - bars(a: String!, b: String): [Bar] + saveBar(a: $a b: $b) { + id + name + } + } + """, + """ + type Query { + bars(a: String!, b: String): [Bar] } type Mutation { - saveBar(a: String!, b: String): Bar + saveBar(a: String!, b: String): Bar } type Bar { - id: String! - name: String - }", + id: String! + name: String + } + """, "extend schema @key(fields: \"id\")"); } } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/__snapshots__/RazorGeneratorTests.Query_And_Mutation.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/__snapshots__/RazorGeneratorTests.Query_And_Mutation.snap index aa2bedd3935..6d5c236d1e9 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/__snapshots__/RazorGeneratorTests.Query_And_Mutation.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/__snapshots__/RazorGeneratorTests.Query_And_Mutation.snap @@ -343,7 +343,7 @@ namespace Foo.Bar /// /// query GetBars( /// $a: String! - /// $b: String + /// $b: String /// ) { /// bars(a: $a, b: $b) { /// __typename @@ -384,7 +384,7 @@ namespace Foo.Bar /// /// query GetBars( /// $a: String! - /// $b: String + /// $b: String /// ) { /// bars(a: $a, b: $b) { /// __typename @@ -497,7 +497,7 @@ namespace Foo.Bar /// /// query GetBars( /// $a: String! - /// $b: String + /// $b: String /// ) { /// bars(a: $a, b: $b) { /// __typename @@ -526,7 +526,7 @@ namespace Foo.Bar /// /// mutation SaveBars( /// $a: String! - /// $b: String + /// $b: String /// ) { /// saveBar(a: $a, b: $b) { /// __typename @@ -567,7 +567,7 @@ namespace Foo.Bar /// /// mutation SaveBars( /// $a: String! - /// $b: String + /// $b: String /// ) { /// saveBar(a: $a, b: $b) { /// __typename @@ -680,7 +680,7 @@ namespace Foo.Bar /// /// mutation SaveBars( /// $a: String! - /// $b: String + /// $b: String /// ) { /// saveBar(a: $a, b: $b) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Tests/Analyzers/DocumentAnalyzerTests.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Tests/Analyzers/DocumentAnalyzerTests.cs index 882edaf056a..b299d2022a7 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Tests/Analyzers/DocumentAnalyzerTests.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Tests/Analyzers/DocumentAnalyzerTests.cs @@ -71,7 +71,7 @@ query GetHero { }); } - [Fact] + [Fact(Skip = "We need to reimplement defer for the client.")] public async Task One_Fragment_One_Deferred_Fragment() { // arrange From cc5bc9eb5e61027f7ddd957e75577bfc4c526422 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sat, 14 Feb 2026 19:01:00 +0000 Subject: [PATCH 42/46] Fixed in-memory client --- .../DependencyInjection/InMemoryClient.cs | 62 +++++++++++++++---- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/src/StrawberryShake/Client/src/Transport.InMemory/DependencyInjection/InMemoryClient.cs b/src/StrawberryShake/Client/src/Transport.InMemory/DependencyInjection/InMemoryClient.cs index 32c29a389fb..38d632f3caf 100644 --- a/src/StrawberryShake/Client/src/Transport.InMemory/DependencyInjection/InMemoryClient.cs +++ b/src/StrawberryShake/Client/src/Transport.InMemory/DependencyInjection/InMemoryClient.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using HotChocolate; using HotChocolate.Execution; using HotChocolate.Language; @@ -64,7 +65,11 @@ public async ValueTask ExecuteAsync( } requestBuilder.SetOperationName(request.Name); - requestBuilder.SetVariableValues(CreateVariables(request)); + requestBuilder.SetVariableValues(CreateVariables(request, out var fileLookup)); + if (fileLookup is not null) + { + requestBuilder.Features.Set(fileLookup); + } requestBuilder.SetExtensions(request.GetExtensionsOrNull()); requestBuilder.SetGlobalState(request.GetContextDataOrNull()); @@ -81,11 +86,15 @@ await interceptor .ConfigureAwait(false); } - private IReadOnlyDictionary? CreateVariables(OperationRequest request) + private IReadOnlyDictionary? CreateVariables( + OperationRequest request, + out IFileLookup? fileLookup) { + fileLookup = null; + if (request.Variables is { } variables) { - var unflattened = MapFilesToLookup(request.Files); + var unflattened = MapFilesToLookup(request.Files, out fileLookup); var response = new Dictionary(); foreach (var pair in variables) @@ -127,9 +136,9 @@ await interceptor return response; } default: - if (fileValue is Upload upload) + if (variables is null && fileValue is string fileKey) { - return new StreamFile(upload.FileName, () => upload.Content, null, upload.ContentType); + return fileKey; } return variables; @@ -144,22 +153,35 @@ private static void GetFileValueOrDefault( value = (source, key) switch { (Dictionary s, string prop) when s.ContainsKey(prop) => s[prop], - (List l, int i) when i < l.Count => l[i], + (List l, int i) when i < l.Count => l[i], _ => null }; } - private static IReadOnlyDictionary MapFilesToLookup( - IReadOnlyDictionary files) + private static IReadOnlyDictionary MapFilesToLookup( + IReadOnlyDictionary files, + out IFileLookup? fileLookup) { if (files.Count == 0) { - return ImmutableDictionary.Empty; + fileLookup = null; + return ImmutableDictionary.Empty; } - var unflattened = new Dictionary(); + var unflattened = new Dictionary(); + var fileMap = new Dictionary(); + foreach (var file in files) { + if (!file.Value.HasValue) + { + continue; + } + + var upload = file.Value.Value; + fileMap[file.Key] = + new StreamFile(upload.FileName, () => upload.Content, null, upload.ContentType); + object? current = unflattened; var path = file.Key.Split('.').ToArray(); for (var i = 1; i < path.Length; i++) @@ -184,7 +206,7 @@ private static IReadOnlyDictionary MapFilesToLookup( if (nextSegment is null) { - currentList[index] = file.Value; + currentList[index] = file.Key; } else if (currentList.ElementAtOrDefault(index) is not null) { @@ -210,7 +232,7 @@ private static IReadOnlyDictionary MapFilesToLookup( if (nextSegment is null) { - currentDict[segment] = file.Value; + currentDict[segment] = file.Key; } else if (currentDict.TryGetValue(segment, out var o) && o is not null) { @@ -229,6 +251,22 @@ private static IReadOnlyDictionary MapFilesToLookup( } } + fileLookup = fileMap.Count == 0 ? null : new InMemoryFileLookup(fileMap); return unflattened; } + + private sealed class InMemoryFileLookup(IReadOnlyDictionary files) : IFileLookup + { + public bool TryGetFile(string name, [NotNullWhen(true)] out IFile? file) + { + if (files.TryGetValue(name, out var current)) + { + file = current; + return true; + } + + file = null; + return false; + } + } } From 82b165dc57ecdce55d0c81124b79101cd491ea13 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sat, 14 Feb 2026 20:10:03 +0100 Subject: [PATCH 43/46] skipped test --- .../Core/test/Execution.Tests/MiddlewareContextTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs b/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs index 276b6280a31..bb438f6ae4f 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs @@ -448,7 +448,8 @@ public async Task SetResultExtensionData_With_ObjectValue() """); } - [Fact] + // TODO : FIX BEFORE V16 RELEASE + [Fact(Skip = "We need to research how we deal with extensions")] public async Task SetResultExtensionData_With_ObjectValue_WhenDeferred() { using var cts = new CancellationTokenSource(5000); From 851328fb271fb5994a5f1a922a791e1816315848 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sat, 14 Feb 2026 19:11:14 +0000 Subject: [PATCH 44/46] Fixed variable coercion. --- .../Execution/JsonVariableCoercion.cs | 76 ++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/JsonVariableCoercion.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/JsonVariableCoercion.cs index 8ecd5db0a80..35ef6c4312b 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/JsonVariableCoercion.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/JsonVariableCoercion.cs @@ -161,7 +161,7 @@ private bool TryParseAndValidate( // Handle Scalar types if (type is FusionScalarTypeDefinition scalarType) { - return TryParseScalar(scalarType, element, path, out value, out error); + return TryParseScalar(scalarType, element, path, depth, out value, out error); } // Handle Enum types @@ -338,6 +338,7 @@ private readonly bool TryParseScalar( FusionScalarTypeDefinition scalarType, JsonElement element, Path path, + int depth, [NotNullWhen(true)] out IValueNode? value, [NotNullWhen(false)] out IError? error) { @@ -361,7 +362,7 @@ private readonly bool TryParseScalar( } else { - value = ParseLiteral(element); + value = ParseLiteral(element, depth); if (!scalarType.IsValueCompatible(value)) { @@ -415,8 +416,13 @@ private static bool TryParseEnum( return true; } - private readonly IValueNode ParseLiteral(JsonElement element) + private readonly IValueNode ParseLiteral(JsonElement element, int depth) { + if (depth > MaxAllowedDepth) + { + throw new InvalidOperationException("Max allowed depth reached."); + } + switch (element.ValueKind) { case JsonValueKind.Null: @@ -458,6 +464,70 @@ private readonly IValueNode ParseLiteral(JsonElement element) return new IntValueNode(segment); } + case JsonValueKind.Array: + { + var buffer = ArrayPool.Shared.Rent(64); + var count = 0; + + try + { + foreach (var item in element.EnumerateArray()) + { + if (count == buffer.Length) + { + var temp = buffer; + var tempSpan = temp.AsSpan(); + buffer = ArrayPool.Shared.Rent(count * 2); + tempSpan.CopyTo(buffer); + tempSpan.Clear(); + ArrayPool.Shared.Return(temp); + } + + buffer[count++] = ParseLiteral(item, depth + 1); + } + + return new ListValueNode(buffer.AsSpan(0, count).ToArray()); + } + finally + { + buffer.AsSpan(0, count).Clear(); + ArrayPool.Shared.Return(buffer); + } + } + + case JsonValueKind.Object: + { + var buffer = ArrayPool.Shared.Rent(64); + var count = 0; + + try + { + foreach (var item in element.EnumerateObject()) + { + if (count == buffer.Length) + { + var temp = buffer; + var tempSpan = temp.AsSpan(); + buffer = ArrayPool.Shared.Rent(count * 2); + tempSpan.CopyTo(buffer); + tempSpan.Clear(); + ArrayPool.Shared.Return(temp); + } + + buffer[count++] = new ObjectFieldNode( + item.Name, + ParseLiteral(item.Value, depth + 1)); + } + + return new ObjectValueNode(buffer.AsSpan(0, count).ToArray()); + } + finally + { + buffer.AsSpan(0, count).Clear(); + ArrayPool.Shared.Return(buffer); + } + } + default: throw new InvalidOperationException($"Unexpected JSON value kind: {element.ValueKind}"); } From 582bfa7c0e73f64bd607f3a5100690799d9dc839 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sat, 14 Feb 2026 20:23:39 +0100 Subject: [PATCH 45/46] cleanup --- Defer.md | 384 ------------------------------------------------------- 1 file changed, 384 deletions(-) delete mode 100644 Defer.md diff --git a/Defer.md b/Defer.md deleted file mode 100644 index 1621a4e6330..00000000000 --- a/Defer.md +++ /dev/null @@ -1,384 +0,0 @@ -# Defer and Stream PR 1110 - -## Reference Locations -- **GraphQL Spec**: `/Users/michael/local/graphql-spec/public/draft/index.html` -- **Reference Implementation**: `/Users/michael/local/graphql-js` -- **Key Reference File**: `/Users/michael/local/graphql-js/src/execution/incremental/buildExecutionPlan.ts` - -## Overview -We're integrating GraphQL `@defer` directive support into Hot Chocolate's execution engine. The defer directive allows incremental delivery of GraphQL responses - the initial response returns immediately with non-deferred fields, and deferred fragments arrive in subsequent payloads. - -## Key Concepts - -### DeferUsage -- Represents a `@defer` directive occurrence in a query -- Forms a parent chain for nested defer scopes -- Has properties: `Label`, `Parent`, `DeferConditionIndex` -- The `DeferConditionIndex` maps to a bit position in runtime defer flags (ulong bitmask) - -### Defer Flags -- Runtime bitmask (ulong) indicating which defer conditions are active -- Each bit corresponds to a defer condition (supports up to 64 defer directives) -- Variables like `@defer(if: $var)` are evaluated at runtime, not compile-time - -### Branch IDs -- Execution branches represent different execution contexts -- Branch IDs are **scheduler-issued** via `WorkScheduler.NextBranchId()` (atomic counter) -- Each operation's main branch gets a unique ID at initialization (no more constant `MainBranchId`) -- Each deferred fragment gets its own unique branch ID -- `SystemBranchId = -1` is reserved for orchestrator tasks (DeferTask) — **not tracked** for completion -- This ensures uniqueness across variable-batched operations sharing a scheduler -- Branch task counts are tracked in `WorkScheduler._branchTaskCount` (Dictionary) -- `DeferExecutionCoordinator` tracks parent-child branch relationships and defer usage mappings - -### Primary Defer Usage -- When a selection has multiple defer usages (nested defers), we need to find the "primary" one -- The primary defer usage is the **outermost** active defer that isn't covered by a parent -- This determines which execution branch the selection belongs to - -## Completed Work - -### 1. WorkQueue Priority System -**File**: `src/HotChocolate/Core/src/Types/Execution/Processing/WorkQueue.cs` - -Changed from single `Stack` to two stacks for priority-based execution: -```csharp -private readonly Stack _immediateStack = new(); -private readonly Stack _deferredStack = new(); -``` - -**Why two stacks instead of PriorityQueue?** -- Binary priority levels (immediate vs deferred) don't need heap overhead -- Stack is O(1) for push/pop vs O(log n) for PriorityQueue -- Better cache locality with Stack -- Zero-allocation when pooled - -**Logic**: -- `Push()` routes tasks to immediate or deferred stack based on `IsDeferred` -- `TryTake()` always tries immediate stack first, then deferred stack -- Ensures initial response completes as fast as possible - -### 2. IExecutionTask.IsDeferred Property -**File**: `src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs` - -Added property: -```csharp -bool IsDeferred { get; } -``` - -**Note**: Named "IsDeferred" for general execution engine concept of deprioritized tasks, not GraphQL-specific - -### 3. ResolverTask Branch Tracking -**Files**: -- `src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs` - -Added properties: -```csharp -internal int BranchId { get; private set; } -internal DeferUsage? DeferUsage { get; private set; } - -public bool IsDeferred => DeferUsage is not null; -``` - -**Why both BranchId and DeferUsage?** -- `BranchId` is used by coordinator to track which branch this task belongs to -- `DeferUsage` is used when creating child tasks to determine if they need a new branch -- When child's primary defer usage differs from parent's, a new branch is created - -### 4. Selection.GetPrimaryDeferUsage() Method - -**File**: `src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs` - -Finds the primary (outermost) active defer usage for a selection at runtime. Handles conditional defers by walking up the parent chain when a defer is inactive. - -**Algorithm**: - -1. For each entry in `_deferUsage`, walk up the parent chain to find the **nearest active** defer (bit set in `deferFlags`). -2. If any entry resolves to no active defer at all (walked to root) → return `null`. The field has a non-deferred occurrence and belongs in the initial response. -3. Among all resolved effective defers, keep the **outermost** (the one that is an ancestor of others). - -**Fast path**: Single defer usage (most common) — just walks up the parent chain and returns the first active one. - -**Example scenarios** with `_deferUsage = [B]` where B.parent = A: - -| A (conditional) | B | Result | Why | -|---|---|---|---| -| active | active | B | B is nearest active | -| inactive | active | B | A disabled, B still defers | -| active | inactive | A | B disabled, folds into A's scope | -| inactive | inactive | null | No active defer in chain | - -## Key Decisions & Rationale - -### 1. Walk parent chain on inactive defer (not immediate null) - -Previously returned null if ANY defer usage was inactive. Now walks up the parent chain to find the nearest active ancestor. This handles conditional outer defers: when `@defer(if: $var)` is disabled, the content folds into its parent scope, but a parent `@defer` may still be active. - -**Return null** only when walking the full chain finds no active defer — meaning the field truly has a non-deferred occurrence. - -### 2. Use reference equality for DeferUsage identity - -DeferUsage is a sealed record and instances are interned during compilation. Reference equality (`==`) correctly identifies the same defer directive across parent chains and array entries. - -### 3. Outermost wins among multiple effective defers - -When `_deferUsage` has multiple entries that resolve to different active defers, the outermost (ancestor) is kept as primary. If two are unrelated (different branches), the first is kept (single-return API limitation — reference impl returns a set). - -## Reference Implementation Notes - -From `graphql-js/src/execution/incremental/buildExecutionPlan.ts`: - -### `getFilteredDeferUsageSet()` (lines 51-75) - -1. Collects all defer usages from field details -2. **If ANY field has `undefined` deferUsage**, clears the set and returns empty -3. For remaining defer usages, removes children whose parents are also in the set (walks full ancestor chain) -4. What remains are the outermost (primary) defer usages - -This matches our approach: -- ✅ Check all defer usages are active (our lines 318-322) -- ✅ Check if parent is in array using reference equality (our lines 330-339) -- ✅ Return first uncovered defer usage (our line 344) - -### `buildExecutionPlan()` (lines 17-49) — Branching Logic - -Takes a `groupedFieldSet` and `parentDeferUsages` (the defer context of the current branch). -For each field: - -1. Computes `filteredDeferUsageSet` via `getFilteredDeferUsageSet()` -2. If `filteredDeferUsageSet === parentDeferUsages` → field stays in current branch (no new branch) -3. If different → field goes into a **new branch** keyed by its defer usage set - -**Branch creation rule**: A new branch is created at the **boundary** where a field's primary defer usage differs from the parent task's defer usage. Fields inside the same defer scope inherit the parent's branch. - -Example: - -```graphql -{ - ... @defer { # A - bar { # primary = A, parent branch has no defer → new branch A - age # primary = A, parent branch = A → stays in branch A - ... @defer { # B (parent = A) - baz # primary = B, parent branch = A → new branch B - } - } - } -} -``` - -## Architecture - -``` -┌─────────────────────────────────────────────────────┐ -│ GraphQL Query with @defer directives │ -└─────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ Operation Compiler │ -│ - Builds Selection objects │ -│ - Assigns DeferUsage[] to each selection │ -│ - Assigns defer flags (ulong bitmask) │ -└─────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ Execution Engine creates ResolverTasks │ -│ - Calls GetPrimaryDeferUsage(deferFlags) │ -│ - Determines BranchId based on primary defer usage │ -│ - Sets task.BranchId and task.DeferUsage │ -└─────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ WorkQueue.Push(task) │ -│ - Checks task.IsDeferred │ -│ - Routes to _immediateStack or _deferredStack │ -└─────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ WorkQueue.TryTake() │ -│ - Tries _immediateStack first │ -│ - Then _deferredStack │ -│ - Ensures initial response completes first │ -└─────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ DeferExecutionCoordinator │ -│ - Tracks branches and their relationships │ -│ - Composes incremental results │ -│ - Delivers responses in correct order │ -└─────────────────────────────────────────────────────┘ -``` - -## Operation Compiler — DeferUsage Construction - -**File**: `src/HotChocolate/Core/src/Types/Execution/Processing/OperationCompiler.cs` - -### CollectFields (line 212) - -Recursively walks the selection set. When an inline fragment has `@defer`: - -1. Creates a `DeferCondition` and registers it in `DeferConditionCollection` -2. Creates a new `DeferUsage(label, parentDeferUsage, deferIndex)` — parent chain is correct -3. Recurses into the fragment's selections with the new DeferUsage as `parentDeferUsage` -4. Fields get the current scope's DeferUsage via `FieldSelectionNode(fieldNode, pathIncludeFlags, parentDeferUsage)` - -### BuildSelectionSet (line 277) — Compile-Time Filtering - -For each field (by response name), collects all `FieldSelectionNode` entries: - -1. **Non-deferred check**: If ANY node has `DeferUsage == null`, the field has `hasNonDeferredNode = true` and is not deferred (lines 301, 342-344). Matches the reference impl's "clear set if any field detail has undefined deferUsage". -2. **Ancestor filtering** (lines 373-386): Walks the **full ancestor chain** (not just direct parent). If any ancestor is also in the list, removes the child. Leaves only outermost defer usages. Matches the reference impl exactly. -3. Produces `finalDeferUsage` array and `deferMask` bitmask, stored on the `Selection`. - -### Compile-Time vs Runtime Filtering - -The compile-time ancestor filtering in `BuildSelectionSet` (lines 373-386) is only safe for **constant** defers (unconditional `@defer` or `@defer(if: true)`). When a variable is involved (`@defer(if: $var)`), the child must be preserved in the `_deferUsage` array so `GetPrimaryDeferUsage` can evaluate it at runtime. - -**Safe to filter at compile time** (both constant): - -```graphql -... @defer { # A (constant) - ... @defer { # B (constant, parent = A) - field # → [A] at compile time, correct - } -} -``` - -**Must defer to runtime** (variable involved): - -```graphql -... @defer(if: $a) { # A (conditional) - ... @defer { # B (unconditional, parent = A) - field # → must keep [A, B], filter at runtime - } -} -``` - -If A is disabled at runtime ($a = false), A's content executes immediately but B should still defer `field`. If B is discarded at compile time, `GetPrimaryDeferUsage` sees only [A], A's bit is off, returns null — incorrectly putting the field in the initial response. - -**TODO**: Update `BuildSelectionSet` ancestor filtering to only remove a child when both the child and its covering ancestor are constant (no variable condition). Variable-dependent usages must stay in the array for runtime evaluation by `GetPrimaryDeferUsage`. - -## Task List - -### Done - -- [x] `IExecutionTask.IsDeferred` property -- [x] `WorkQueue` dual-stack priority system (immediate/deferred) -- [x] `DeferUsage`, `DeferCondition`, `DeferConditionCollection` metadata types -- [x] `DeferUsageEnumerator` zero-allocation enumerator -- [x] `DeferExecutionCoordinator` branch tracking, result composition, streaming -- [x] `ResolverTask` BranchId + DeferUsage properties -- [x] `OperationContext.DeferFlags` (ulong bitmask) -- [x] `SelectionSet.HasDeferredSelections` flag -- [x] `Selection.GetPrimaryDeferUsage()` — runtime algorithm with parent chain walk -- [x] Operation compiler: `CollectFields` builds DeferUsage parent chain -- [x] Operation compiler: `BuildSelectionSet` compile-time ancestor filtering -- [x] `ResultDocument` per-defer-group constructor — scoped to selections matching a specific `DeferUsage` - -### Execution Engine Integration - -- [x] `ResolverTaskFactory.EnqueueRootResolverTasks()` — defer branch grouping, DeferTask creation, ArrayPool pattern -- [x] `ResolverTaskFactory.EnqueueOrInlineResolverTasks()` — defer-aware branching with `parentBranchId` from `ValueCompletionContext` -- [x] `IExecutionTask.BranchId` — added to interface, abstract on `ExecutionTask` base class -- [x] `ValueCompletionContext.ParentBranchId` — threads parent ResolverTask's BranchId through value completion -- [x] `OperationContext.DeferExecutionCoordinator` — per-operation (not shared via `_current*` pattern), returns `_deferExecutionCoordinator` directly -- [ ] Pool `DeferTask` — `OperationContext.CreateDeferTask()` currently does `new DeferTask()`, needs a pooled factory like `ResolverTask` has - -### Scheduler — Branch Tracking - -- [x] **Branch ID generation**: `BranchTracker` with `Interlocked.Increment` atomic counter - - Each operation gets a unique main branch ID at initialization via `_currentBranchTracker.CreateNewBranchId()` - - Defer branches get unique IDs via `DeferExecutionCoordinator.Branch()` → `_branchTracker.CreateNewBranchId()` - - `BranchTracker.SystemBranchId = -1` is the only constant — for DeferTask orchestrators, not tracked for completion - - `_current*` pattern for tracker/scheduler (not coordinator) ensures uniqueness across variable-batched operations -- [x] **Branch task counting**: `Dictionary` in `WorkScheduler` with nested `Branch` class - - `Register()`: inside `lock(_sync)`, creates `Branch` on first encounter, calls `branch.RegisterTask()` - - `Complete()`: inside `lock(_sync)`, calls `branch.CompleteTask()`, removes and signals via `branch.Complete()` when count hits 0 - - `Clear()`: clears `_activeBranches` dictionary - - `Branch` uses `AsyncManualResetEvent` for single-awaiter async signaling (~56 bytes, resettable) - - Cancellation via `CancellationToken.Register` with proper `await using` disposal -- [x] **`WaitForCompletionAsync(branchId)`**: returns `ValueTask.CompletedTask` if branch already completed or never registered; otherwise delegates to `branch.WaitForCompletionAsync(operationContext.RequestAborted)` -- [x] **Initial payload signal**: `QueryExecutor.ExecuteIncrementalAsync` awaits `scheduler.WaitForCompletionAsync(branchId)` on main branch, then enqueues result via coordinator -- [x] Remove constant `DeferExecutionCoordinator.MainBranchId` — replaced with instance `_mainBranchId` set via `Initialize()` -- [x] Update `DeferExecutionCoordinator.Branch()` to use `_branchTracker.CreateNewBranchId()` -- [x] Update `OperationContext` to store assigned main branch ID (`_branchId` / `ExecutionBranchId`) - -### Compiler - -- [ ] Update `BuildSelectionSet` ancestor filtering to only remove constant defers; preserve variable-dependent usages for runtime evaluation - -### Middleware — `OperationExecutionMiddleware` Simplification - -**Done**: Removed `ITransactionScopeHandler` and all related types: - -- [x] Deleted `ITransactionScopeHandler`, `ITransactionScope`, `DefaultTransactionScopeHandler`, `DefaultTransactionScope`, `NoOpTransactionScopeHandler`, `NoOpTransactionScope` -- [x] Deleted `RequestExecutorBuilderExtensions.TransactionScope.cs` (public DI extensions) -- [x] Removed from middleware: field, constructor param, factory resolution, `using var transactionScope` / `.Complete()` in mutation path -- [x] Removed `TryAddNoOpTransactionScopeHandler()` from `RequestExecutorServiceCollectionExtensions.CreateBuilder()` -- [x] Deleted `TransactionScopeHandlerTests.cs` and 2 snapshot files -- [x] 5-arg `ExecuteQueryOrMutationAsync` now returns `IExecutionResult` (supports `ResponseStream` for defer) - -**Done** — method collapse and defer wiring: - -- [x] Collapsed 4-arg + 5-arg `ExecuteQueryOrMutationAsync` into single method (one fewer async state machine) -- [x] Replaced commented-out defer block with `result.IsStreamResult()` → `result.RegisterForCleanup(operationContextOwner)` ownership transfer -- [x] Uncommented `IsOperationAllowed` — enforces `AllowStreams` flag for operations with `HasDeferredSelections` -- [x] Removed `ExecuteQueryOrMutationNoStreamAsync` — mutation batch now uses `ExecuteQueryOrMutationAsync` -- [x] Removed `ExecuteOperationRequestAsync` — inlined subscription/query/mutation dispatch into `InvokeAsync` -- [x] Unified variable batch path — removed query-only gate, single `ExecuteVariableBatchRequestAsync` handles both queries and mutations with shared scheduler for DataLoader batching -- [x] Relaxed `IsRequestTypeAllowed` — variable batch + defer is allowed (spec only forbids `@defer` on root mutation fields, enforced by validation) - -### Result Delivery - -- [x] Wire `DeferExecutionCoordinator` into the response stream -- [x] On branch completion: create `OperationResult` from defer group's `ResultDocument`, call `coordinator.EnqueueResult(result, branchId)` -- [ ] Ensure `path` passed to `coordinator.Branch()` is the **full path from query root** (not relative to defer group document) -- [x] Fix `ResultDocument.CreatePath` for defer groups — added `_rootPath` field, defaults to `Path.Root`, defer constructor accepts `path` parameter -- [x] Compose incremental payloads in correct delivery order — coordinator composes pending/incremental/completed with error-bubbling distinction -- [x] Handle `hasNext` flag on initial and subsequent payloads — set in `ComposeAndDeliverUnsafe` - -### Serialization - -- [x] `IncrementalObjectResult` — write actual data via `Formatter.WriteDataTo` or `null` on error -- [x] `IncrementalObjectResult` — write `subPath` when present -- [x] `CompletedResult` — write `errors` for failed incremental deliveries -- [ ] Investigate `JsonNullIgnoreCondition` impact on incremental result serialization — `JsonResultFormatter` carries this setting but unclear how it interacts with deferred data - -### Validation Rules (Spec 5.7.4 & 5.7.5) - -- [ ] `@defer` not allowed on root fields of mutation type -- [ ] `@defer` not allowed on root fields of subscription type -- [ ] `@defer` and `@stream` in subscription operations must have an `if` argument that can disable them - -### Fusion - -- [ ] Implement `HasDeferredSelections` in `Fusion.Execution` — `Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Operation.cs:108` currently `throw new NotImplementedException()` -- [ ] Implement `Selection.IsDeferred(ulong deferFlags)` in `Fusion.Execution` — `Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs:161` currently `throw new NotImplementedException()` -- [ ] Implement `SelectionSet.HasDeferredSelections` in `Fusion.Execution` — `Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs:64` currently `throw new NotImplementedException()` - -### Subscriptions - -- [ ] Handle `@defer` inside subscription payloads — `SubscriptionExecutor.Subscription.cs:192` currently calls `result.ExpectOperationResult()` which won't work if the result is a `ResponseStream` - -### Testing - -- [ ] Simple `@defer` — single deferred fragment -- [ ] Nested `@defer` — defer inside defer -- [ ] Conditional `@defer(if: $var)` — variable true/false -- [ ] Nested conditional/unconditional — `@defer(if: $var) { @defer { field } }` -- [ ] Field in multiple fragments — unrelated defers on same field -- [ ] `@defer` with label — verify label propagation - -## Important Files - -- `src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs` - GetPrimaryDeferUsage -- `src/HotChocolate/Core/src/Types/Execution/Processing/WorkQueue.cs` - Priority queue -- `src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs` - Branch tracking -- `src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs` - IsDeferred property -- `src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs` - Branch coordinator -- `src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsage.cs` - Defer metadata -- `src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs` - Result document (per-operation and per-defer-group) From b5ffc9424c76ecc63e9453bde4cd17717972cae9 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sat, 14 Feb 2026 20:40:55 +0100 Subject: [PATCH 46/46] Fixed more tests --- ...penApi_Includes_Initial_Routes_NET9_0.json | 26 +- ...Includes_Initial_Routes_NET9_0_Fusion.json | 22 +- ...aMiddlewareTests.Download_GraphQL_SDL.snap | 4 +- ...s.Download_GraphQL_SDL_Explicit_Route.snap | 4 +- ...L_SDL_Explicit_Route_Explicit_Pattern.snap | 4 +- ...MiddlewareTests.Download_GraphQL_Schema.md | 4 +- ...oad_GraphQL_Schema_Slicing_Args_Enabled.md | 4 +- .../StarWarsCodeFirstTests.cs | 512 +++++++++++++----- 8 files changed, 426 insertions(+), 154 deletions(-) diff --git a/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0.json b/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0.json index 759a6540f64..e75a041fec9 100644 --- a/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0.json +++ b/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0.json @@ -14,9 +14,9 @@ "schema": { "required": [ "any", + "base64String", "boolean", "byte", - "byteArray", "date", "dateTime", "decimal", @@ -35,6 +35,7 @@ "string", "timeSpan", "unknown", + "uri", "url", "uuid" ], @@ -62,6 +63,10 @@ } ] }, + "base64String": { + "pattern": "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$", + "type": "string" + }, "boolean": { "type": "boolean" }, @@ -70,10 +75,6 @@ "description": "The Byte scalar type represents an 8-bit signed integer with a minimum value of -128 and a maximum value of 127.", "format": "int32" }, - "byteArray": { - "pattern": "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$", - "type": "string" - }, "date": { "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string", @@ -211,6 +212,9 @@ "unknown": { "type": "string" }, + "uri": { + "type": "string" + }, "url": { "type": "string" }, @@ -232,9 +236,9 @@ "schema": { "required": [ "any", + "base64String", "boolean", "byte", - "byteArray", "date", "dateTime", "decimal", @@ -281,6 +285,11 @@ ], "nullable": true }, + "base64String": { + "pattern": "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$", + "type": "string", + "nullable": true + }, "boolean": { "type": "boolean", "nullable": true @@ -291,11 +300,6 @@ "format": "int32", "nullable": true }, - "byteArray": { - "pattern": "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$", - "type": "string", - "nullable": true - }, "date": { "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string", diff --git a/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0_Fusion.json b/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0_Fusion.json index d384caff03e..ef88d786f44 100644 --- a/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0_Fusion.json +++ b/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0_Fusion.json @@ -14,9 +14,9 @@ "schema": { "required": [ "any", + "base64String", "boolean", "byte", - "byteArray", "date", "dateTime", "decimal", @@ -35,6 +35,7 @@ "string", "timeSpan", "unknown", + "uri", "url", "uuid" ], @@ -43,6 +44,9 @@ "any": { "type": "string" }, + "base64String": { + "type": "string" + }, "boolean": { "type": "boolean" }, @@ -50,9 +54,6 @@ "type": "string", "description": "The Byte scalar type represents an 8-bit signed integer with a minimum value of -128 and a maximum value of 127." }, - "byteArray": { - "type": "string" - }, "date": { "type": "string", "description": "The `Date` scalar represents an ISO-8601 compliant date type." @@ -159,6 +160,9 @@ "unknown": { "type": "string" }, + "uri": { + "type": "string" + }, "url": { "type": "string" }, @@ -179,9 +183,9 @@ "schema": { "required": [ "any", + "base64String", "boolean", "byte", - "byteArray", "date", "dateTime", "decimal", @@ -209,6 +213,10 @@ "type": "string", "nullable": true }, + "base64String": { + "type": "string", + "nullable": true + }, "boolean": { "type": "boolean", "nullable": true @@ -218,10 +226,6 @@ "description": "The Byte scalar type represents an 8-bit signed integer with a minimum value of -128 and a maximum value of 127.", "nullable": true }, - "byteArray": { - "type": "string", - "nullable": true - }, "date": { "type": "string", "description": "The `Date` scalar represents an ISO-8601 compliant date type.", diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL.snap index 9cd5feb1a18..6e306458850 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL.snap @@ -17,7 +17,7 @@ type Droid implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") height(unit: Unit): Float primaryFunction: String traits: Any @@ -45,7 +45,7 @@ type Human implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") otherHuman: Human height(unit: Unit): Float homePlanet: String diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route.snap index 6ee7c2556d2..f3fa98b3db9 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route.snap @@ -17,7 +17,7 @@ type Droid implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") height(unit: Unit): Float primaryFunction: String traits: Any @@ -45,7 +45,7 @@ type Human implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") otherHuman: Human height(unit: Unit): Float homePlanet: String diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route_Explicit_Pattern.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route_Explicit_Pattern.snap index 6ee7c2556d2..f3fa98b3db9 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route_Explicit_Pattern.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route_Explicit_Pattern.snap @@ -17,7 +17,7 @@ type Droid implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") height(unit: Unit): Float primaryFunction: String traits: Any @@ -45,7 +45,7 @@ type Human implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") otherHuman: Human height(unit: Unit): Float homePlanet: String diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema.md b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema.md index a178ad384d5..2125a646c56 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema.md +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema.md @@ -28,7 +28,7 @@ type Droid implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") height(unit: Unit): Float primaryFunction: String traits: Any @@ -56,7 +56,7 @@ type Human implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") otherHuman: Human height(unit: Unit): Float homePlanet: String diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Slicing_Args_Enabled.md b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Slicing_Args_Enabled.md index 09d0f7a3fcc..892ed24ccbc 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Slicing_Args_Enabled.md +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Slicing_Args_Enabled.md @@ -28,7 +28,7 @@ type Droid implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ]) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ]) @cost(weight: "10") height(unit: Unit): Float primaryFunction: String traits: Any @@ -56,7 +56,7 @@ type Human implements Character { id: ID! name: String! appearsIn: [Episode] - friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ]) + friends("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ]) @cost(weight: "10") otherHuman: Human height(unit: Unit): Float homePlanet: String diff --git a/src/HotChocolate/Core/test/Execution.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs index a31263101d3..07b64a243d6 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs @@ -11,33 +11,45 @@ public class StarWarsCodeFirstTests(ITestOutputHelper output) public async Task Schema() { // arrange + var snapshot = Snapshot.Create(); var executor = await CreateExecutorAsync(); // act var schema = executor.Schema.ToString(); + snapshot.Add(schema); // assert - schema.MatchSnapshot(); + snapshot.Match(); } [Fact] public async Task GetHeroName() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { hero { name } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgFieldExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { hero { @@ -50,14 +62,20 @@ await ExpectValid( } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgFieldArgumentExample1() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { human(id: "1000") { @@ -65,14 +83,20 @@ await ExpectValid( height } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgFieldArgumentExample2() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { human(id: "1000") { @@ -80,14 +104,20 @@ await ExpectValid( height(unit: FOOT) } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgAliasExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { empireHero: hero(episode: EMPIRE) { @@ -97,14 +127,20 @@ await ExpectValid( name } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgFragmentExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { leftComparison: hero(episode: EMPIRE) { @@ -124,14 +160,20 @@ fragment comparisonFields on Character { } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgOperationNameExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query HeroNameAndFriends { hero { @@ -143,14 +185,20 @@ query HeroNameAndFriends { } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgVariableExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query HeroNameAndFriends($episode: Episode) { hero(episode: $episode) { @@ -168,14 +216,20 @@ query HeroNameAndFriends($episode: Episode) { { "episode": "JEDI" } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgVariableWithDefaultValueExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query HeroNameAndFriends($episode: Episode = JEDI) { hero(episode: $episode) { @@ -187,14 +241,20 @@ query HeroNameAndFriends($episode: Episode = JEDI) { } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgDirectiveIncludeExample1() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query Hero($episode: Episode, $withFriends: Boolean!) { hero(episode: $episode) { @@ -213,14 +273,20 @@ friends @include(if: $withFriends) { "episode": "JEDI", "withFriends": false } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgDirectiveIncludeExample2() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query Hero($episode: Episode, $withFriends: Boolean!) { hero(episode: $episode) { @@ -239,14 +305,20 @@ friends @include(if: $withFriends) { "episode": "JEDI", "withFriends": true } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgDirectiveSkipExample1() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query Hero($episode: Episode, $withFriends: Boolean!) { hero(episode: $episode) { @@ -265,14 +337,20 @@ friends @skip(if: $withFriends) { "episode": "JEDI", "withFriends": false } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgDirectiveSkipExample2() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query Hero($episode: Episode, $withFriends: Boolean!) { hero(episode: $episode) { @@ -291,14 +369,20 @@ friends @skip(if: $withFriends) { "episode": "JEDI", "withFriends": true } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgDirectiveSkipExample1WithPlainClrVarTypes() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query Hero($episode: Episode, $withFriends: Boolean!) { hero(episode: $episode) { @@ -317,14 +401,20 @@ friends @skip(if: $withFriends) { "episode": "JEDI", "withFriends": false } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgMutationExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { createReview(episode: $ep, review: $review) { @@ -342,14 +432,20 @@ mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { "commentary": "This is a great movie!" } } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgMutationIgnoreAdditionalInputFieldsExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { createReview(episode: $ep, review: $review) { @@ -374,14 +470,20 @@ mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { "commentary": "This is a great movie!" } } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgTwoMutationsExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ mutation CreateReviewForEpisode($ep: Episode!, $ep2: Episode!, $review: ReviewInput!) { createReview(episode: $ep, review: $review) { @@ -404,14 +506,20 @@ mutation CreateReviewForEpisode($ep: Episode!, $ep2: Episode!, $review: ReviewIn "commentary": "This is a great movie!" } } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgMutationExample_With_ValueVariables() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ mutation CreateReviewForEpisode( $ep: Episode! @@ -432,14 +540,20 @@ mutation CreateReviewForEpisode( "stars": 5, "commentary": "This is a great movie!" } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgInlineFragmentExample1() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query HeroForEpisode($ep: Episode!) { hero(episode: $ep) { @@ -458,14 +572,20 @@ ... on Human { { "ep": "JEDI" } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgInlineFragmentExample2() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query HeroForEpisode($ep: Episode!) { hero(episode: $ep) { @@ -484,14 +604,20 @@ ... on Human { { "ep": "EMPIRE" } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgMetaFieldAndUnionExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { search(text: "an") { @@ -510,14 +636,20 @@ ... on Starship { } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task NonNullListVariableValues() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query op($ep: [Episode!]!) { heroes(episodes: $ep) { @@ -530,14 +662,20 @@ query op($ep: [Episode!]!) { { "ep": ["EMPIRE"] } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task ConditionalInlineFragment() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { heroes(episodes: [EMPIRE]) { @@ -547,14 +685,20 @@ ... @include(if: true) { } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task NonNullEnumsSerializeCorrectlyFromVariables() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query getHero($episode: Episode!) { hero(episode: $episode) { @@ -567,28 +711,40 @@ query getHero($episode: Episode!) { { "episode": "NEW_HOPE" } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task EnumValueIsCoercedToListValue() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { heroes(episodes: EMPIRE) { name } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task TypeNameFieldIsCorrectlyExecutedOnInterfaces() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query foo { hero(episode: NEW_HOPE) { @@ -618,14 +774,20 @@ ... on Droid { } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task Execute_ListWithNullValues_ResultContainsNullElement() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query { human(id: "1001") { @@ -636,14 +798,17 @@ await ExpectValid( } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task SubscribeToReview() { // arrange + var snapshot = Snapshot.Create(); var executor = await CreateExecutorAsync(output: output); // act @@ -659,7 +824,6 @@ public async Task SubscribeToReview() var results = subscriptionResult.ReadResultsAsync(); - // assert await executor.ExecuteAsync( """ mutation { @@ -682,13 +846,17 @@ await executor.ExecuteAsync( } } - eventResult?.MatchSnapshot(); + snapshot.Add(eventResult); + + // assert + snapshot.Match(); } [Fact] public async Task SubscribeToReview_WithInlineFragment() { // arrange + var snapshot = Snapshot.Create(); var executor = await CreateExecutorAsync(output: output); // act @@ -704,7 +872,6 @@ ... on Review { } """); - // assert await executor.ExecuteAsync( """ mutation { @@ -727,13 +894,17 @@ await executor.ExecuteAsync( } } - eventResult?.MatchSnapshot(); + snapshot.Add(eventResult); + + // assert + snapshot.Match(); } [Fact] public async Task SubscribeToReview_FragmentDefinition() { // arrange + var snapshot = Snapshot.Create(); var executor = await CreateExecutorAsync(output: output); // act @@ -751,7 +922,6 @@ fragment SomeFrag on Review { } """); - // assert await executor.ExecuteAsync( """ mutation { @@ -774,13 +944,17 @@ await executor.ExecuteAsync( } } - eventResult?.MatchSnapshot(); + snapshot.Add(eventResult); + + // assert + snapshot.Match(); } [Fact] public async Task SubscribeToReview_With_Variables() { // arrange + var snapshot = Snapshot.Create(); var executor = await CreateExecutorAsync(); // act @@ -804,7 +978,6 @@ public async Task SubscribeToReview_With_Variables() """) .Build()); - // assert await executor.ExecuteAsync( """ mutation { @@ -827,7 +1000,10 @@ await executor.ExecuteAsync( } } - eventResult?.MatchSnapshot(); + snapshot.Add(eventResult); + + // assert + snapshot.Match(); } /// @@ -1106,7 +1282,11 @@ await ExpectValid( [Theory] public async Task Include_With_Literal(string ifValue) { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(postFix: ifValue); + + // act + snapshot.Add(await ExpectValid( $$""" { human(id: "1000") { @@ -1114,8 +1294,10 @@ name @include(if: {{ifValue}}) height } } - """) - .MatchSnapshotAsync(postFix: ifValue); + """)); + + // assert + snapshot.Match(); } [InlineData(true)] @@ -1123,7 +1305,11 @@ name @include(if: {{ifValue}}) [Theory] public async Task Include_With_Variable(bool ifValue) { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(postFix: ifValue.ToString()); + + // act + snapshot.Add(await ExpectValid( """ query ($if: Boolean!) { human(id: "1000") { @@ -1137,8 +1323,10 @@ name @include(if: $if) { "if": {{ifValue.ToString().ToLowerInvariant()}} } - """)) - .MatchSnapshotAsync(postFix: ifValue); + """))); + + // assert + snapshot.Match(); } [InlineData("true")] @@ -1146,7 +1334,11 @@ name @include(if: $if) [Theory] public async Task Skip_With_Literal(string ifValue) { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(postFix: ifValue); + + // act + snapshot.Add(await ExpectValid( $$""" { human(id: "1000") { @@ -1154,8 +1346,10 @@ name @skip(if: {{ifValue}}) height } } - """) - .MatchSnapshotAsync(postFix: ifValue); + """)); + + // assert + snapshot.Match(); } [InlineData(true)] @@ -1163,7 +1357,11 @@ name @skip(if: {{ifValue}}) [Theory] public async Task Skip_With_Variable(bool ifValue) { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(postFix: ifValue.ToString()); + + // act + snapshot.Add(await ExpectValid( """ query ($if: Boolean!) { human(id: "1000") { @@ -1177,14 +1375,20 @@ name @skip(if: $if) { "if": {{ifValue.ToString().ToLowerInvariant()}} } - """)) - .MatchSnapshotAsync(postFix: ifValue); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task SkipAll() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query ($if: Boolean!) { human(id: "1000") @skip(if: $if) { @@ -1198,14 +1402,20 @@ await ExpectValid( { "if": true } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task SkipAll_Default_False() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query ($if: Boolean! = false) { human(id: "1000") @skip(if: $if) { @@ -1213,14 +1423,20 @@ await ExpectValid( height } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task SkipAll_Default_True() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query ($if: Boolean! = true) { human(id: "1000") @skip(if: $if) { @@ -1228,14 +1444,20 @@ await ExpectValid( height } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task SkipAllSecondLevelFields() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query ($if: Boolean!) { human(id: "1000") { @@ -1248,14 +1470,20 @@ name @skip(if: $if) { "if": true } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task Ensure_Type_Introspection_Returns_Null_If_Type_Not_Found() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query { a: __type(name: "Foo") { @@ -1265,42 +1493,76 @@ await ExpectValid( name } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task Ensure_Benchmark_Query_GetHeroQuery() { + // arrange + var snapshot = Snapshot.Create(); var query = FileResource.Open("GetHeroQuery.graphql"); - await ExpectValid(query).MatchSnapshotAsync(); + + // act + snapshot.Add(await ExpectValid(query)); + + // assert + snapshot.Match(); } [Fact] public async Task Ensure_Benchmark_Query_GetHeroWithFriendsQuery() { + // arrange + var snapshot = Snapshot.Create(); var query = FileResource.Open("GetHeroWithFriendsQuery.graphql"); - await ExpectValid(query).MatchSnapshotAsync(); + + // act + snapshot.Add(await ExpectValid(query)); + + // assert + snapshot.Match(); } [Fact] public async Task Ensure_Benchmark_Query_GetTwoHeroesWithFriendsQuery() { + // arrange + var snapshot = Snapshot.Create(); var query = FileResource.Open("GetTwoHeroesWithFriendsQuery.graphql"); - await ExpectValid(query).MatchSnapshotAsync(); + + // act + snapshot.Add(await ExpectValid(query)); + + // assert + snapshot.Match(); } [Fact] public async Task Ensure_Benchmark_Query_LargeQuery() { + // arrange + var snapshot = Snapshot.Create(); var query = FileResource.Open("LargeQuery.graphql"); - await ExpectValid(query).MatchSnapshotAsync(); + + // act + snapshot.Add(await ExpectValid(query)); + + // assert + snapshot.Match(); } [Fact] public async Task NestedFragmentsWithNestedObjectFieldsAndSkip() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query ($if: Boolean!) { human(id: "1000") { @@ -1349,7 +1611,9 @@ fragment Human3 on Human { { "if": true } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } }