< Summary

Information
Class: NGql.Core.Extensions.FieldDefinitionExtensions
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Extensions/FieldDefinitionExtensions.cs
Line coverage
100%
Covered lines: 271
Uncovered lines: 0
Coverable lines: 271
Total lines: 869
Line coverage: 100%
Branch coverage
100%
Covered branches: 270
Total branches: 270
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
CanMergeFields(...)100%44100%
AreConditionalDirectivesEqual(...)100%22100%
FindConditional(...)100%88100%
AreDirectivesStructurallyEqual(...)100%66100%
HasConditionalDirectives(...)100%66100%
AreNestedFieldsCompatible(...)100%22100%
IncomingChildrenCompatible(...)100%88100%
IsIncomingChildCompatible(...)100%88100%
FindChildByNameAndAlias(...)100%11100%
ExistingExtrasCompatible(...)100%1010100%
IsExistingExtraCompatible(...)100%44100%
SubtreeHasAnyArguments(...)100%88100%
CombineDeepHash(...)100%11100%
ComputeDeepFingerprint(...)100%44100%
ConditionalDirectiveFingerprint(...)100%1212100%
ArgumentsFingerprint(...)100%11100%
DeepChildrenFingerprint(...)100%88100%
AnyChildHasArguments(...)100%88100%
HasAnyArguments(...)100%1212100%
DeepClone(...)100%2020100%
DeepClone(...)100%44100%
DeepClone(...)100%11100%
DeepCloneFragments(...)100%66100%
CloneFragmentBody(...)100%1010100%
MergeFieldsInPlace(...)100%11100%
MergeFragmentState(...)100%11100%
MergeInlineFragmentsInPlace(...)100%1010100%
MergeInlineFragmentBodyInPlace(...)100%2424100%
MergeSpreadsInPlace(...)100%22100%
MergeInlineFragmentsInto(...)100%1010100%
MergeSpreadNamesInto(...)100%11100%
MergeSpreadListInPlace(...)100%1010100%
MergeDirectivesInPlace(...)100%66100%
ThrowIfTypesConflict(...)100%22100%
MergeIncomingChildrenInPlace(...)100%66100%
MergeIncomingArgumentsInPlace(...)100%66100%
InvalidateMergeMemoAfterChildrenMerge(...)100%66100%
MergeChildInPlace(...)100%66100%
HasEffectiveNameConflict(...)100%44100%
MergeFieldArgumentsInPlace(...)100%88100%
MergeFieldArguments(...)100%88100%
AsSortedCaseInsensitive(...)100%22100%
CopyToSortedCaseInsensitive(...)100%22100%
ApplyArgumentOverrides(...)100%22100%
MergedArgumentValue(...)100%66100%

File(s)

/home/runner/work/NGql/NGql/src/Core/Extensions/FieldDefinitionExtensions.cs

#LineLine coverage
 1using System.Runtime.CompilerServices;
 2using NGql.Core.Abstractions;
 3using NGql.Core.Exceptions;
 4using NGql.Core.Features;
 5
 6namespace NGql.Core.Extensions;
 7
 8/// <summary>
 9/// Extension methods and utilities for FieldDefinition operations.
 10/// </summary>
 11internal static class FieldDefinitionExtensions
 12{
 13    /// <summary>
 14    /// Determines if two fields can be merged based on their structure, arguments, and
 15    /// conditional-include state (<c>@include</c>/<c>@skip</c> — see <see cref="AreConditionalDirectivesEqual"/>).
 16    /// The recursion is bounded by the field tree's depth — the public API does not allow
 17    /// constructing cyclic trees (FieldDefinition.Fields is get-only, _children is internal),
 18    /// so the recursion always terminates. A library bug introducing a cycle would surface as
 19    /// a StackOverflowException, which is louder and more actionable than a swallowed throw.
 20    /// </summary>
 21    internal static bool CanMergeFields(FieldDefinition existingField, FieldDefinition incomingField)
 22    {
 103523        if (!Helpers.AreArgumentsEqual(existingField._arguments, incomingField._arguments))
 1824            return false;
 101725        if (!AreConditionalDirectivesEqual(existingField, incomingField))
 1226            return false;
 100527        return AreNestedFieldsCompatible(existingField, incomingField);
 28    }
 29
 30    /// <summary>
 31    /// Compares the <c>@include</c>/<c>@skip</c> conditional state of two fields — the part of a
 32    /// field's directive list that is MERGE-IDENTITY-RELEVANT. Two fragments requesting the same
 33    /// field path under different runtime conditions (different <c>if</c> variables, or one
 34    /// conditional and one not) are not the same field for merge purposes: collapsing them into one
 35    /// node would apply one side's condition to content the other side asked for unconditionally.
 36    /// Custom directives (added via the generic <see cref="Builders.FieldBuilder.Directive(string, System.Collections.G
 37    /// overload, excluding the reserved <c>include</c>/<c>skip</c> names) are intentionally NOT part
 38    /// of this comparison — they stay repeatable/mergeable exactly as before, matching the issue's
 39    /// scope (only <c>@include</c>/<c>@skip</c> are constrained here).
 40    /// </summary>
 41    private static bool AreConditionalDirectivesEqual(FieldDefinition existingField, FieldDefinition incomingField)
 42    {
 372343        var existingInclude = FindConditional(existingField._directives, "include");
 372344        var incomingInclude = FindConditional(incomingField._directives, "include");
 373245        if (!AreDirectivesStructurallyEqual(existingInclude, incomingInclude)) return false;
 46
 371447        var existingSkip = FindConditional(existingField._directives, "skip");
 371448        var incomingSkip = FindConditional(incomingField._directives, "skip");
 371449        return AreDirectivesStructurallyEqual(existingSkip, incomingSkip);
 50    }
 51
 52    [System.Diagnostics.CodeAnalysis.SuppressMessage(
 53        "Major Code Smell", "S3267:Loops should be simplified using the \"Where\" LINQ method",
 54        Justification = "The plain loop short-circuits on first match without allocating an enumerator on the merge-iden
 55    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 56    private static FieldDirective? FindConditional(List<FieldDirective>? directives, string name)
 57    {
 2995558        if (directives is not { Count: > 0 }) return null;
 125459        foreach (var directive in directives)
 60        {
 59161            if (string.Equals(directive.Name, name, StringComparison.Ordinal)) return directive;
 62        }
 14763        return null;
 22264    }
 65
 66    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 67    private static bool AreDirectivesStructurallyEqual(FieldDirective? a, FieldDirective? b)
 68    {
 1485369        if (a is null || b is null) return a is null && b is null;
 2170        return a.IsStructurallyEqualTo(b);
 71    }
 72
 73    /// <summary>
 74    /// Whether <paramref name="field"/> carries an <c>@include</c> or <c>@skip</c> directive on its
 75    /// own node. Folded into <see cref="HasAnyArguments"/>/<see cref="SubtreeHasAnyArguments"/> so a
 76    /// child that differs ONLY by conditional-include state (no arguments anywhere) is still treated
 77    /// as merge-identity-significant — otherwise <see cref="IsIncomingChildCompatible"/>/
 78    /// <see cref="IsExistingExtraCompatible"/>'s argument-free early-out would silently accept a
 79    /// mismatched condition as "absent, therefore compatible".
 80    /// </summary>
 81    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 82    private static bool HasConditionalDirectives(FieldDefinition field)
 955583        => field._directives is { Count: > 0 } directives
 955584        && (FindConditional(directives, "include") is not null || FindConditional(directives, "skip") is not null);
 85
 86    private static bool AreNestedFieldsCompatible(FieldDefinition existingField, FieldDefinition incomingField)
 371187        => IncomingChildrenCompatible(existingField._children, incomingField._children)
 371188        && ExistingExtrasCompatible(existingField, incomingField._children);
 89
 90    private static bool IncomingChildrenCompatible(FieldChildren? existingChildren, FieldChildren? incomingChildren)
 91    {
 452192        if (incomingChildren is not { Count: > 0 }) return true;
 93
 290194        var span = incomingChildren.AsSpan();
 1168295        for (int i = 0; i < span.Length; i++)
 96        {
 298297            if (!IsIncomingChildCompatible(existingChildren, span[i]))
 4298                return false;
 99        }
 2859100        return true;
 101    }
 102
 103    private static bool IsIncomingChildCompatible(FieldChildren? existingChildren, FieldDefinition incomingChild)
 104    {
 3003105        if (existingChildren is null) return !HasAnyArguments(incomingChild);
 2961106        var existingChild = FindChildByNameAndAlias(existingChildren, incomingChild);
 3204107        if (existingChild is null) return !HasAnyArguments(incomingChild);
 2718108        return Helpers.AreArgumentsEqual(existingChild._arguments, incomingChild._arguments)
 2718109            && AreConditionalDirectivesEqual(existingChild, incomingChild)
 2718110            && AreNestedFieldsCompatible(existingChild, incomingChild);
 111    }
 112
 113    /// <summary>
 114    /// Finds a child matching <paramref name="target"/>'s exact GraphQL identity — (Name, alias),
 115    /// not Name alone. Two children may legitimately share a Name while differing by alias (distinct
 116    /// response keys); a Name-only lookup would wrongly treat one as a match for the other. Thin
 117    /// wrapper over the shared ordinal probe <see cref="Helpers.FindByOrdinalNameAndAlias"/> — unlike
 118    /// <see cref="Helpers.FindExistingField(FieldChildren, FieldDefinition)"/>, an outright miss here
 119    /// stays a plain null (no Path-based fallback): every caller of this method treats null as
 120    /// "no compatible existing child", not "search further by another identity".
 121    /// </summary>
 122    private static FieldDefinition? FindChildByNameAndAlias(FieldChildren children, FieldDefinition target)
 8289123        => Helpers.FindByOrdinalNameAndAlias(children, target.Name, target._alias);
 124
 125    // Skip the existing-not-in-incoming check entirely when nothing in the existing subtree
 126    // could ever fail it — early-out when no field in the existing subtree carries arguments.
 127    private static bool ExistingExtrasCompatible(FieldDefinition existingField, FieldChildren? incomingChildren)
 128    {
 3669129        var existingChildren = existingField._children;
 3669130        if (existingChildren is not { Count: > 0 } || !SubtreeHasAnyArguments(existingField))
 1299131            return true;
 132
 2370133        var span = existingChildren.AsSpan();
 9486134        for (int i = 0; i < span.Length; i++)
 135        {
 2382136            var existingChild = span[i];
 2382137            if (!IsExistingExtraCompatible(existingChild, incomingChildren))
 9138                return false;
 139        }
 2361140        return true;
 141    }
 142
 143    private static bool IsExistingExtraCompatible(FieldDefinition existingChild, FieldChildren? incomingChildren)
 144    {
 2385145        if (incomingChildren is null) return !HasAnyArguments(existingChild);
 2379146        return FindChildByNameAndAlias(incomingChildren, existingChild) is not null || !HasAnyArguments(existingChild);
 147    }
 148
 149    // Named "HasAnyArguments" historically (pre-directives), but the memoized flag actually answers
 150    // a broader question: "is this subtree merge-identity-significant beyond bare structure?" —
 151    // which now includes conditional-include state, not just arguments. Folding
 152    // HasConditionalDirectives in here (rather than a parallel memoized flag) means every existing
 153    // caller — the fingerprint short-circuit, ExistingExtrasCompatible's early-out, ancestor
 154    // invalidation — picks up directive-significance for free, with the same invalidation contract
 155    // (ClearMergeMemo) that already covers argument mutations.
 156    private static bool SubtreeHasAnyArguments(FieldDefinition field)
 157    {
 61203158        if (field._subtreeHasAnyArguments is { } cached) return cached;
 159
 11871160        var result = field._arguments is { Count: > 0 }
 11871161            || HasConditionalDirectives(field)
 11871162            || AnyChildHasArguments(field._children);
 11871163        field._subtreeHasAnyArguments = result;
 11871164        return result;
 165    }
 166
 167    // FNV-style mixing constant shared with Helpers.ComputeArgumentFingerprint's CombineHash —
 168    // duplicated here (rather than exposed) to keep the deep-fingerprint recursion self-contained
 169    // within the file that owns its invalidation contract.
 170    private const ulong DeepFingerprintFnvPrime = 1099511628211UL;
 171
 172    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 173    private static ulong CombineDeepHash(ulong hash, ulong value)
 34866174        => (hash ^ value) * DeepFingerprintFnvPrime;
 175
 176    /// <summary>
 177    /// Computes (and memoizes on <see cref="FieldDefinition._deepArgumentFingerprint"/>) a
 178    /// conservative fingerprint over the MERGE-RELEVANT subtree of <paramref name="field"/>, for
 179    /// use as a cheap pre-filter bucket key in <c>QueryMerger.FindMergeTarget</c> — NEVER as a
 180    /// merge decision by itself. Strictly refines (never coarsens) the shallow, own-arguments-only
 181    /// fingerprint <see cref="Helpers.ComputeArgumentFingerprint"/> already provides, since this
 182    /// fingerprint folds that same value in at every level plus a commutative combination of every
 183    /// argument-carrying descendant.
 184    ///
 185    /// <para>
 186    /// <b>Conservatism proof sketch</b> (the contract this method must uphold: <c>CanMergeFields(a, b)
 187    /// == true</c> MUST imply <c>ComputeDeepFingerprint(a) == ComputeDeepFingerprint(b)</c>).
 188    /// <c>CanMergeFields</c> requires equal own arguments (already covered — matches
 189    /// <see cref="Helpers.ComputeArgumentFingerprint"/>'s own proven contract) and, recursively for
 190    /// every child on EITHER side whose subtree carries an argument anywhere
 191    /// (<see cref="SubtreeHasAnyArguments"/>), that the OTHER side has a same-named child which is
 192    /// itself pairwise <c>CanMergeFields</c>-compatible. (<see cref="IsIncomingChildCompatible"/>'s
 193    /// and <see cref="IsExistingExtraCompatible"/>'s absent/argument-free branches only accept a
 194    /// missing counterpart when the present side's subtree is ENTIRELY argument-free — so an
 195    /// argument-carrying child always demands an argument-carrying, pairwise-compatible match on
 196    /// the other side.) By induction on subtree depth, a pairwise-compatible child pair has equal
 197    /// deep fingerprints — so folding in EVERY argument-carrying child's (name, deep fingerprint)
 198    /// pair can never observe two <c>CanMergeFields</c>-compatible fields disagree.
 199    /// </para>
 200    ///
 201    /// <para>
 202    /// <b>Argument-free-child invariance.</b> A child whose entire subtree carries no arguments
 203    /// (checked via the existing memoized <see cref="SubtreeHasAnyArguments"/> — NOT the child's
 204    /// own <c>_arguments</c> in isolation) contributes NOTHING to the hash: <c>CanMergeFields</c>
 205    /// tolerates such a child being entirely absent from, or structurally different on, the other
 206    /// side (<see cref="IsIncomingChildCompatible"/> returns true for an incoming argument-free
 207    /// child missing from existing; <see cref="ExistingExtrasCompatible"/> early-outs completely
 208    /// when the existing subtree has no arguments at all). Two otherwise-identical fields differing
 209    /// only in argument-free children — at any depth — MUST hash equal, or a genuine merge would be
 210    /// falsely split. Recursing through <see cref="SubtreeHasAnyArguments"/> (not a shallow check)
 211    /// is what lets a SINGLE argument three levels down still make the whole ancestor chain down to
 212    /// it significant, while an entirely argument-free branch of arbitrary depth vanishes completely.
 213    /// </para>
 214    ///
 215    /// <para>
 216    /// <b>Order independence.</b> Children are matched by name, not position (see
 217    /// <see cref="FieldChildren.Find(string)"/>), so per-child contributions are combined with XOR —
 218    /// commutative and associative — rather than an order-sensitive fold.
 219    /// </para>
 220    /// </summary>
 221    internal static ulong ComputeDeepFingerprint(FieldDefinition field)
 222    {
 21867223        if (field._deepArgumentFingerprint is { } cached) return cached;
 224
 225        // O(1) short-circuit for the common argument-free subtree: when SubtreeHasAnyArguments(field)
 226        // is false, field._arguments is empty/null (so ownArgsFp is the fixed FnvOffsetBasis constant
 227        // — see Helpers.ComputeArgumentFingerprint) AND every descendant's own SubtreeHasAnyArguments
 228        // is also false (that is exactly what AnyChildHasArguments, which SubtreeHasAnyArguments folds
 229        // in, recursively requires). DeepChildrenFingerprint's loop would therefore `continue` past
 230        // every single child without ever computing a contribution, always returning 0UL — so skipping
 231        // straight to CombineDeepHash(ownArgsFp, 0UL) here is provably identical to running the loop,
 232        // without visiting a single child. This is what collapses the O(children)-per-call cost that
 233        // MergeIncomingChildrenInPlace's memo invalidation otherwise forces on every Include for the
 234        // (extremely common) case of a merge-irrelevant, argument-free subtree of arbitrary size.
 13107235        if (!SubtreeHasAnyArguments(field))
 236        {
 660237            var constantResult = CombineDeepHash(Helpers.ComputeArgumentFingerprint(null), 0UL);
 660238            field._deepArgumentFingerprint = constantResult;
 660239            return constantResult;
 240        }
 241
 12447242        var ownArgsFp = CombineDeepHash(Helpers.ComputeArgumentFingerprint(field._arguments), ConditionalDirectiveFinger
 12447243        var childrenFp = DeepChildrenFingerprint(field._children);
 12447244        var result = CombineDeepHash(ownArgsFp, childrenFp);
 245
 12447246        field._deepArgumentFingerprint = result;
 12447247        return result;
 248    }
 249
 250    // Distinct sentinel folded in when a field carries NEITHER @include nor @skip, so an
 251    // include/skip-bearing field's fingerprint can never coincide with one that reuses the same
 252    // combined value by coincidence of the two directives' own fingerprints XORing to the "absent"
 253    // marker's bit pattern (defense in depth — CombineDeepHash's FNV mixing already makes this
 254    // astronomically unlikely, but a named sentinel makes the "absent" case an explicit branch
 255    // rather than an emergent zero).
 256    private const ulong NoConditionalDirectiveSentinel = 0xA24BAED4963EE889UL;
 257
 258    /// <summary>
 259    /// Conservative fingerprint contribution for <paramref name="field"/>'s OWN <c>@include</c>/
 260    /// <c>@skip</c> state, folded into <see cref="ComputeDeepFingerprint"/>'s own-node hash exactly
 261    /// like <see cref="Helpers.ComputeArgumentFingerprint"/> folds in arguments. Must uphold the same
 262    /// conservatism contract: <see cref="AreConditionalDirectivesEqual"/> considering two fields
 263    /// equal MUST imply this returns the same value for both — reuses
 264    /// <see cref="Helpers.ComputeArgumentFingerprint"/> directly on each directive's (already
 265    /// case-insensitively sorted — see <see cref="FieldDirective"/>'s constructor)
 266    /// <see cref="FieldDirective.Arguments"/>, which is already proven conservative for arbitrary
 267    /// argument shapes (nested dictionaries, lists, and unrecognized value types all fold into the
 268    /// shared conservative sentinel rather than risk a false inequality).
 269    /// </summary>
 270    private static ulong ConditionalDirectiveFingerprint(FieldDefinition field)
 271    {
 24795272        if (field._directives is not { Count: > 0 } directives) return NoConditionalDirectiveSentinel;
 273
 99274        var include = FindConditional(directives, "include");
 99275        var skip = FindConditional(directives, "skip");
 102276        if (include is null && skip is null) return NoConditionalDirectiveSentinel;
 277
 278        // "include" and "skip" are hashed under distinct salts so `@include(if:$x)` alone and
 279        // `@skip(if:$x)` alone (same underlying argument shape, different directive name) never
 280        // collide into the same contribution — mirroring how ComputeArgumentFingerprint folds the
 281        // KEY hash in alongside the value hash rather than hashing values alone.
 96282        var includeFp = include is { } inc
 96283            ? CombineDeepHash((ulong)"include".GetHashCode(StringComparison.Ordinal), ArgumentsFingerprint(inc.Arguments
 96284            : 0UL;
 96285        var skipFp = skip is { } sk
 96286            ? CombineDeepHash((ulong)"skip".GetHashCode(StringComparison.Ordinal), ArgumentsFingerprint(sk.Arguments))
 96287            : 0UL;
 288
 96289        return CombineDeepHash(includeFp, skipFp);
 290    }
 291
 292    // FieldDirective's constructor always routes arguments through Helpers.SortArgumentValue into a
 293    // case-insensitive SortedDictionary (see FieldDirective.NormalizeArguments) or leaves Arguments
 294    // null for an empty/absent dictionary — there is no public construction path that produces any
 295    // other concrete type, so the cast below is unconditionally safe.
 296    private static ulong ArgumentsFingerprint(IReadOnlyDictionary<string, object?>? arguments)
 96297        => Helpers.ComputeArgumentFingerprint((SortedDictionary<string, object?>?)arguments);
 298
 299    private static ulong DeepChildrenFingerprint(FieldChildren? children)
 300    {
 15522301        if (children is not { Count: > 0 }) return 0UL;
 302
 9372303        var span = children.AsSpan();
 9372304        var accumulator = 0UL;
 37596305        for (int i = 0; i < span.Length; i++)
 306        {
 9426307            var child = span[i];
 308
 309            // Argument-free-at-every-depth children are invisible to CanMergeFields (see the
 310            // conservatism proof above) — they must be invisible to the fingerprint too, or a
 311            // genuine merge candidate that only differs by such a child would be falsely split.
 9426312            if (!SubtreeHasAnyArguments(child)) continue;
 313
 314            // XOR (commutative) so sibling iteration order never affects the result — children
 315            // are matched by NAME during the actual merge, never by position.
 9120316            var nameHash = (ulong)child.Name.GetHashCode(StringComparison.OrdinalIgnoreCase);
 9120317            var childContribution = CombineDeepHash(nameHash, ComputeDeepFingerprint(child));
 9120318            accumulator ^= childContribution;
 319        }
 9372320        return accumulator;
 321    }
 322
 323    private static bool AnyChildHasArguments(FieldChildren? children)
 324    {
 13152325        if (children is null || children.Count == 0) return false;
 10812326        var span = children.AsSpan();
 25818327        for (int i = 0; i < span.Length; i++)
 328        {
 20205329            if (SubtreeHasAnyArguments(span[i])) return true;
 330        }
 1758331        return false;
 332    }
 333
 334    // Un-memoized counterpart of SubtreeHasAnyArguments, used at the "child missing from the other
 335    // side entirely" leaf checks in IsIncomingChildCompatible/IsExistingExtraCompatible. Must apply
 336    // the exact same significance rule (arguments OR conditional directives OR a significant
 337    // descendant) — a child differing only by @include/@skip, with no arguments anywhere in its
 338    // subtree, must not be treated as "absent, therefore compatible" any more than an
 339    // argument-bearing child would be.
 340    private static bool HasAnyArguments(FieldDefinition field)
 341    {
 357342        if (field._arguments is { Count: > 0 }) return true;
 342343        if (HasConditionalDirectives(field)) return true;
 627344        if (field._children is null) return false;
 195345        foreach (var child in field._children.AsSpan())
 346        {
 57347            if (HasAnyArguments(child)) return true;
 348        }
 42349        return false;
 350    }
 351
 352    /// <summary>
 353    /// Deep-clones <paramref name="source"/> producing a new <see cref="FieldDefinition"/> that
 354    /// shares no mutable state with the original. This is THE clone boundary — it must copy
 355    /// every per-field store (children, arguments, metadata, inline fragments, spreads); any
 356    /// store it misses aliases the clone to the source and silently drops state on isolation
 357    /// paths (Preserve, Include). Used by <see cref="QueryMerger"/> and the preservation pipeline.
 358    /// </summary>
 359    internal static FieldDefinition DeepClone(this FieldDefinition source)
 360    {
 9129361        var clone = new FieldDefinition(
 9129362            source.Name,
 9129363            source._type!,
 9129364            source._alias,
 9129365            source._arguments is null ? null : new SortedDictionary<string, object?>(source._arguments, StringComparer.O
 9129366        {
 9129367            Path = source.Path,
 9129368            IsNeverMerge = source.IsNeverMerge,
 9129369        };
 370
 9129371        if (source._children is { Count: > 0 })
 372        {
 3771373            clone._children = new FieldChildren(source._children.Count);
 16518374            foreach (var child in source._children.AsSpan())
 4488375                clone._children.Append(child.DeepClone());
 376        }
 377
 9129378        clone.SetOptionalState(
 9129379            DeepCloneFragments(source._fragments),
 9129380            source._spreadFragments is { Count: > 0 } ? new List<string>(source._spreadFragments) : null,
 9129381            source._directives is { Count: > 0 } ? new List<FieldDirective>(source._directives) : null,
 9129382            source._metadata is { Count: > 0 } ? new Dictionary<string, object?>(source._metadata) : null);
 383
 9129384        return clone;
 385    }
 386
 387    internal static InlineFragmentDefinition DeepClone(this InlineFragmentDefinition source)
 388    {
 42389        var clone = new InlineFragmentDefinition(source.TypeName);
 42390        CloneFragmentBody(source._fields, source._fragments, source._spreadFragments,
 42391            out clone._fields, out clone._fragments, out clone._spreadFragments);
 392
 393        // FieldDirective is an immutable record — sharing entries is safe; only the LIST needs to
 394        // be a fresh instance so mutating the clone's directives can never reach back into source.
 42395        clone._directives = source._directives is { Count: > 0 }
 42396            ? new List<FieldDirective>(source._directives)
 42397            : null;
 398
 42399        return clone;
 400    }
 401
 402    internal static NamedFragmentDefinition DeepClone(this NamedFragmentDefinition source)
 403    {
 15404        var clone = new NamedFragmentDefinition(source.Name, source.OnType);
 15405        CloneFragmentBody(source._fields, source._fragments, source._spreadFragments,
 15406            out clone._fields, out clone._fragments, out clone._spreadFragments);
 15407        return clone;
 408    }
 409
 410    internal static Dictionary<string, InlineFragmentDefinition>? DeepCloneFragments(Dictionary<string, InlineFragmentDe
 411    {
 18357412        if (fragments is not { Count: > 0 }) return null;
 413
 15414        var clone = new Dictionary<string, InlineFragmentDefinition>(fragments.Count, StringComparer.Ordinal);
 60415        foreach (var (typeName, fragment) in fragments)
 416        {
 15417            clone[typeName] = fragment.DeepClone();
 418        }
 15419        return clone;
 420    }
 421
 422    private static void CloneFragmentBody(
 423        FieldChildren? sourceFields,
 424        Dictionary<string, InlineFragmentDefinition>? sourceFragments,
 425        List<string>? sourceSpreads,
 426        out FieldChildren? fields,
 427        out Dictionary<string, InlineFragmentDefinition>? fragments,
 428        out List<string>? spreads)
 429    {
 57430        fields = null;
 57431        if (sourceFields is { Count: > 0 })
 432        {
 48433            fields = new FieldChildren(sourceFields.Count);
 192434            foreach (var child in sourceFields.AsSpan())
 48435                fields.Append(child.DeepClone());
 436        }
 437
 57438        fragments = DeepCloneFragments(sourceFragments);
 57439        spreads = sourceSpreads is { Count: > 0 } ? new List<string>(sourceSpreads) : null;
 57440    }
 441
 442    /// <summary>
 443    /// Merges <paramref name="incoming"/> INTO <paramref name="existing"/>, mutating its child collection
 444    /// and argument dictionary in place. The caller must own <paramref name="existing"/> exclusively
 445    /// (no external aliases) — used by <see cref="QueryMerger"/> when the result will overwrite the
 446    /// dictionary entry that holds <paramref name="existing"/>.
 447    /// </summary>
 448    /// <returns><paramref name="existing"/> after mutation.</returns>
 449    internal static FieldDefinition MergeFieldsInPlace(FieldDefinition existing, FieldDefinition incoming)
 450    {
 3657451        ThrowIfTypesConflict(existing, incoming);
 3645452        MergeIncomingChildrenInPlace(existing, incoming._children);
 3633453        MergeIncomingArgumentsInPlace(existing, incoming._arguments);
 3633454        MergeFragmentState(existing, incoming);
 3633455        return existing;
 456    }
 457
 458    /// <summary>
 459    /// Merges <paramref name="incoming"/>'s inline fragments, fragment spreads, and directives INTO
 460    /// <paramref name="existing"/>, mutating its collections in place. Everything carried over is
 461    /// deep-cloned (spreads and directives are copied into fresh lists; inline fragments are cloned
 462    /// via <see cref="DeepClone(InlineFragmentDefinition)"/>) so the merged field never aliases the
 463    /// source's fragment state. Spread insertion order is preserved and duplicate spread names are
 464    /// skipped, matching <see cref="FieldDefinition.AddSpreadFragment"/>. Directives are appended in
 465    /// order, skipping any that are structurally identical to one already present (so merging the
 466    /// same directive on a same-path field does not duplicate it). Shared by every field-level
 467    /// Include path.
 468    /// </summary>
 469    internal static void MergeFragmentState(FieldDefinition existing, FieldDefinition incoming)
 470    {
 4320471        MergeInlineFragmentsInPlace(existing, incoming._fragments);
 4320472        MergeSpreadsInPlace(existing, incoming._spreadFragments);
 4320473        MergeDirectivesInPlace(existing, incoming._directives);
 4320474    }
 475
 476    private static void MergeInlineFragmentsInPlace(FieldDefinition existing, Dictionary<string, InlineFragmentDefinitio
 477    {
 8610478        if (incomingFragments is not { Count: > 0 }) return;
 479
 30480        existing._fragments ??= new Dictionary<string, InlineFragmentDefinition>(StringComparer.Ordinal);
 120481        foreach (var (typeName, incomingFragment) in incomingFragments)
 482        {
 30483            if (existing._fragments.TryGetValue(typeName, out var existingFragment))
 484            {
 15485                MergeInlineFragmentBodyInPlace(existingFragment, incomingFragment);
 486            }
 487            else
 488            {
 15489                existing._fragments[typeName] = incomingFragment.DeepClone();
 490            }
 491        }
 30492    }
 493
 494    // Merges two inline fragments of the same type: their field bodies, nested inline fragments,
 495    // and spreads. Each incoming child/fragment is deep-cloned before entering the target so the
 496    // merged fragment owns its subtree exclusively.
 497    private static void MergeInlineFragmentBodyInPlace(InlineFragmentDefinition existing, InlineFragmentDefinition incom
 498    {
 24499        if (incoming._fields is { Count: > 0 })
 500        {
 18501            var targetFields = existing._fields ??= new FieldChildren();
 72502            foreach (var child in incoming._fields.AsSpan())
 503            {
 18504                MergeChildInPlace(targetFields, child);
 505            }
 506        }
 507
 24508        if (incoming._fragments is { Count: > 0 })
 509        {
 9510            existing._fragments ??= new Dictionary<string, InlineFragmentDefinition>(StringComparer.Ordinal);
 36511            foreach (var (typeName, nested) in incoming._fragments)
 512            {
 9513                if (existing._fragments.TryGetValue(typeName, out var existingNested))
 514                {
 3515                    MergeInlineFragmentBodyInPlace(existingNested, nested);
 516                }
 517                else
 518                {
 6519                    existing._fragments[typeName] = nested.DeepClone();
 520                }
 521            }
 522        }
 523
 24524        MergeSpreadListInPlace(ref existing._spreadFragments, incoming._spreadFragments);
 525
 24526        if (incoming._directives is { Count: > 0 } incomingDirectives)
 527        {
 12528            foreach (var directive in incomingDirectives)
 529            {
 3530                existing.AddDirective(directive);
 531            }
 532        }
 24533    }
 534
 535    private static void MergeSpreadsInPlace(FieldDefinition existing, List<string>? incomingSpreads)
 536    {
 4320537        var current = existing._spreadFragments;
 4320538        var spreads = current;
 4320539        MergeSpreadListInPlace(ref spreads, incomingSpreads);
 4341540        if (!ReferenceEquals(spreads, current)) existing._spreadFragments = spreads;
 4320541    }
 542
 543    /// <summary>
 544    /// Merges <paramref name="incoming"/> inline fragments into <paramref name="target"/> (a fragment
 545    /// body's inline-fragment map), deep-cloning new entries and recursively merging matching type
 546    /// names. Used for named-fragment bodies, whose store is not a <see cref="FieldDefinition"/>.
 547    /// </summary>
 548    internal static void MergeInlineFragmentsInto(
 549        ref Dictionary<string, InlineFragmentDefinition>? target,
 550        Dictionary<string, InlineFragmentDefinition>? incoming)
 551    {
 78552        if (incoming is not { Count: > 0 }) return;
 553
 12554        target ??= new Dictionary<string, InlineFragmentDefinition>(StringComparer.Ordinal);
 48555        foreach (var (typeName, incomingFragment) in incoming)
 556        {
 12557            if (target.TryGetValue(typeName, out var existingFragment))
 558            {
 6559                MergeInlineFragmentBodyInPlace(existingFragment, incomingFragment);
 560            }
 561            else
 562            {
 6563                target[typeName] = incomingFragment.DeepClone();
 564            }
 565        }
 12566    }
 567
 568    /// <summary>
 569    /// Merges <paramref name="incoming"/> spread names into <paramref name="target"/>, preserving
 570    /// insertion order and skipping duplicates. Public wrapper over the in-place list merge for the
 571    /// named-fragment body path.
 572    /// </summary>
 573    internal static void MergeSpreadNamesInto(ref List<string>? target, List<string>? incoming)
 39574        => MergeSpreadListInPlace(ref target, incoming);
 575
 576    // Appends incoming spread names not already present, preserving insertion order — mirrors the
 577    // dedup contract of FieldDefinition.AddSpreadFragment. Order is user-visible (directives can
 578    // differ per spread site) so it is never sorted.
 579    [System.Diagnostics.CodeAnalysis.SuppressMessage(
 580        "Major Code Smell", "S3267:Loops should be simplified using the \"Where\" LINQ method",
 581        Justification = "List.Contains is the filter AND List.Add the side effect; a Where(Contains) would hide the muta
 582    private static void MergeSpreadListInPlace(ref List<string>? existingSpreads, List<string>? incomingSpreads)
 583    {
 8742584        if (incomingSpreads is not { Count: > 0 }) return;
 585
 24586        existingSpreads ??= new List<string>();
 96587        foreach (var name in incomingSpreads)
 588        {
 24589            if (!existingSpreads.Contains(name))
 590            {
 24591                existingSpreads.Add(name);
 592            }
 593        }
 24594    }
 595
 596    // Appends incoming directives in order via FieldDefinition.AddDirective — the same entry point
 597    // IncludeIf/SkipIf/Directive use, so merge gets the identical dedup AND last-call-wins collapse
 598    // rules for free: a structurally-identical directive already present is skipped (merging two
 599    // queries that each place the SAME directive on a same-path field must not emit it twice), and
 600    // for the two non-repeatable, spec-constrained names (`include`/`skip`) a differing incoming
 601    // directive REPLACES the existing one in place rather than appending — never emitting the
 602    // spec-invalid `@include(if:$x) @include(if:$x)` shape. Under MergeByFieldPath this branch is
 603    // only reached once CanMergeFields has already confirmed both sides carry the SAME conditional
 604    // state (see CanMergeFields's directive-identity check), so the replace path is unreachable
 605    // there in practice; under MergeByDefault, which merges same-name fields unconditionally, a
 606    // differing incoming @include/@skip legitimately overwrites the existing one — consistent with
 607    // how MergeByDefault already lets incoming field arguments silently override existing ones.
 608    // FieldDirective is an immutable record, so the shared reference is safe to hand to AddDirective
 609    // without cloning.
 610    private static void MergeDirectivesInPlace(FieldDefinition existing, List<FieldDirective>? incomingDirectives)
 611    {
 8622612        if (incomingDirectives is not { Count: > 0 }) return;
 613
 72614        foreach (var incoming in incomingDirectives)
 615        {
 18616            existing.AddDirective(incoming);
 617        }
 18618    }
 619
 620    private static void ThrowIfTypesConflict(FieldDefinition existing, FieldDefinition incoming)
 621    {
 622        // _type defaults to Constants.DefaultFieldType in every public constructor and is
 623        // never null on the merge path through Include — the field is always touched by
 624        // QueryBuilder.AddField which goes through FieldFactory.CreateFieldDefinition.
 7302625        if (existing._type!.AsSpan().Equals(incoming._type!.AsSpan(), StringComparison.OrdinalIgnoreCase)) return;
 12626        throw new QueryMergeException($"Type conflict: existing field has type '{existing._type}', incoming field has ty
 627    }
 628
 629    private static void MergeIncomingChildrenInPlace(FieldDefinition existing, FieldChildren? incomingChildren)
 630    {
 4437631        if (incomingChildren is null) return;
 632
 633        // existing._children can legitimately still be null here: an object-typed field created
 634        // via AddField(name, "Object") with no sub-fields yet (or one whose type was promoted to
 635        // Object by ShouldConvertToObjectType without ever gaining a child) is a leaf-shaped node
 636        // that CanMergeFields already accepted as compatible with ANY incoming children —
 637        // ExistingExtrasCompatible early-outs as soon as existing's own children collection is
 638        // absent/empty, with nothing further to check on that side. So this is not an unenforced
 639        // invariant to trust; it is a real, reachable shape that must allocate on demand rather
 640        // than asserting non-null.
 2853641        var existingChildren = existing._children ??= new FieldChildren();
 2853642        var span = incomingChildren.AsSpan();
 11544643        for (int i = 0; i < span.Length; i++)
 644        {
 2931645            MergeChildInPlace(existingChildren, span[i]);
 646        }
 647
 648        // incomingChildren is evaluated for "any arguments anywhere" BEFORE the merge loop mutated
 649        // it into existingChildren's clones — AnyChildHasArguments only reads _arguments/_children,
 650        // which the merge loop does not mutate on the SOURCE side (MergeChildInPlace only clones
 651        // incoming nodes into the target; ExistingNested merges happen on the existing side).
 2841652        InvalidateMergeMemoAfterChildrenMerge(existing, AnyChildHasArguments(incomingChildren));
 2841653    }
 654
 655    private static void MergeIncomingArgumentsInPlace(FieldDefinition existing, SortedDictionary<string, object?>? incom
 656    {
 6558657        if (incomingArguments is not { Count: > 0 }) return;
 708658        existing.MergeFieldArgumentsInPlace(incomingArguments);
 659
 660        // No memo update needed here: CanMergeFields (which every MergeFieldsInPlace caller already
 661        // ran) requires AreArgumentsEqual(existing._arguments, incoming._arguments) at this exact
 662        // node, so reaching this line with a non-empty incomingArguments means existing._arguments
 663        // was ALREADY non-empty and equal — i.e. SubtreeHasAnyArguments(existing) was already true
 664        // (and therefore memoized true, or about to memoize true next read regardless). The values
 665        // merged in refine existing keys' nested dictionary contents (see MergeFieldArgumentsInPlace)
 666        // without changing the fingerprint-relevant "has any argument" shape at this node, but the
 667        // fingerprint VALUE can still change (nested value content differs) — so only the fingerprint
 668        // memo needs invalidating, and only when it was actually cached.
 708669        if (existing._deepArgumentFingerprint is not null)
 670        {
 657671            existing._deepArgumentFingerprint = null;
 672        }
 708673    }
 674
 675    /// <summary>
 676    /// Updates <see cref="FieldDefinition._subtreeHasAnyArguments"/> and
 677    /// <see cref="FieldDefinition._deepArgumentFingerprint"/> after merging incoming CHILDREN into
 678    /// <paramref name="existing"/>'s subtree, exploiting monotonicity instead of always nulling both
 679    /// memos (which forced an O(existing-subtree-size) recompute on every single Include — the
 680    /// O(N²) driver for a chain of N Includes into one shared parent).
 681    ///
 682    /// <para>
 683    /// <b>Monotonicity.</b> Merging NEVER removes an argument or a child — <c>MergeFieldArgumentsInPlace</c>
 684    /// only adds/overwrites keys, <c>MergeChildInPlace</c> only appends new children or recurses into
 685    /// existing ones (itself subject to this same monotonic rule). So once a subtree contains any
 686    /// argument anywhere, it always will — <c>_subtreeHasAnyArguments</c> can only ever transition
 687    /// false/null → true for a given live instance, never true → false. If it was already memoized
 688    /// <c>true</c>, no merge can invalidate that fact — both memos are left untouched (a global no-op,
 689    /// O(1)) beyond the fingerprint invalidation below.
 690    /// </para>
 691    ///
 692    /// <para>
 693    /// <b>The false/unknown case.</b> If it was NOT already known <c>true</c>, whether it becomes true
 694    /// now depends solely on whether THIS merge's incoming children introduced an argument anywhere —
 695    /// i.e. <paramref name="incomingChildrenHaveArguments"/>, which the caller computes over the
 696    /// (typically small, single-fragment) INCOMING side only, never by re-walking the accumulated
 697    /// existing subtree. If nothing was contributed, the subtree provably remains argument-free:
 698    /// <c>_subtreeHasAnyArguments</c> stays false (recorded explicitly, not left null, so a future
 699    /// call is also O(1)) — and per <c>ComputeDeepFingerprint</c>'s own short-circuit, an
 700    /// argument-free subtree's fingerprint is a fixed constant independent of its children, so the
 701    /// OLD fingerprint (if already memoized) is still correct and is likewise left untouched.
 702    /// </para>
 703    ///
 704    /// <para>
 705    /// <b>The third ("was false, incoming introduces a genuinely new argument") case.</b> On the
 706    /// <see cref="QueryMerger"/> path this shape cannot arise: <c>CanMergeFields</c> runs before
 707    /// <see cref="MergeFieldsInPlace"/> there and rejects it outright — <see cref="IsIncomingChildCompatible"/>
 708    /// only tolerates an incoming child ABSENT from existing when that child's whole subtree is
 709    /// argument-free, and any incoming child that DOES exist by name on the existing side must have
 710    /// <c>AreArgumentsEqual</c>-equal own arguments before recursing further. It IS reachable through
 711    /// the inline-fragment path, however: <see cref="MergeInlineFragmentBodyInPlace"/> calls
 712    /// <see cref="MergeChildInPlace"/> — and therefore <see cref="MergeFieldsInPlace"/> — with no
 713    /// <c>CanMergeFields</c> gate at all, so merging two same-typed inline fragments whose shared
 714    /// child gains an argument-bearing descendant lands here. Full invalidation is always safe, so
 715    /// this case simply forgoes the O(1) fast path and recomputes both memos on next read.
 716    /// </para>
 717    /// </summary>
 718    private static void InvalidateMergeMemoAfterChildrenMerge(FieldDefinition existing, bool incomingChildrenHaveArgumen
 719    {
 2841720        if (existing._subtreeHasAnyArguments == true)
 721        {
 722            // Already true — monotonicity guarantees it stays true; the exact fingerprint VALUE can
 723            // still change (new argument-bearing content deeper in an already-significant subtree),
 724            // so the fingerprint alone still needs invalidating.
 2358725            existing._deepArgumentFingerprint = null;
 2358726            return;
 727        }
 728
 483729        if (!incomingChildrenHaveArguments)
 730        {
 731            // Not already known true, and this merge's incoming children contributed nothing new.
 732            // Whether the subtree is argument-free now depends on existing's OWN arguments, not
 733            // just its children — recording a bare `false` here would be actively wrong for a
 734            // field that has its own arguments but had never had SubtreeHasAnyArguments computed
 735            // (memo still null) before this merge. Recompute from existing._arguments directly
 736            // (O(1) — no child recursion needed, since AnyChildHasArguments over existing's
 737            // children is exactly what's already known to be unaffected by this incoming merge).
 480738            existing._subtreeHasAnyArguments = existing._arguments is { Count: > 0 };
 480739            return;
 740        }
 741
 742        // A previously argument-free subtree just gained an argument. Unreachable via QueryMerger
 743        // (CanMergeFields rejects that shape first) but genuinely reachable via the ungated
 744        // inline-fragment merge path — see the third-case remarks above. Drop both memos: full
 745        // invalidation is always correct, it only costs this method's O(1) fast path here.
 3746        existing._subtreeHasAnyArguments = null;
 3747        existing._deepArgumentFingerprint = null;
 3748    }
 749
 750    private static void MergeChildInPlace(FieldChildren existingChildren, FieldDefinition incomingChild)
 751    {
 2949752        var existingNested = FindChildByNameAndAlias(existingChildren, incomingChild);
 2949753        if (existingNested != null)
 754        {
 2679755            MergeFieldsInPlace(existingNested, incomingChild);
 2667756            return;
 757        }
 758
 759        // Deep-clone so target's tree never references nodes owned by the source builder. Any
 760        // later in-place merges into existingChildren must not propagate back to the source.
 270761        var clone = incomingChild.DeepClone();
 762
 763        // Effective-name conflict can only occur when the incoming field carries an alias
 764        // distinct from its name; otherwise the Name lookup above would have caught it.
 270765        if (!ReferenceEquals(incomingChild._effectiveName, incomingChild.Name) &&
 270766            HasEffectiveNameConflict(existingChildren, incomingChild._effectiveName))
 767        {
 6768            var uniqueAlias = KeyGenerator.GenerateUniqueKey(incomingChild._effectiveName, existingChildren.AsSpan());
 6769            existingChildren.Append(clone with { Alias = uniqueAlias });
 770        }
 771        else
 772        {
 264773            existingChildren.Append(clone);
 774        }
 264775    }
 776
 777    private static bool HasEffectiveNameConflict(FieldChildren children, string effectiveName)
 778    {
 762779        foreach (var f in children.AsSpan())
 780        {
 297781            if (string.Equals(f._effectiveName, effectiveName, StringComparison.OrdinalIgnoreCase))
 6782                return true;
 783        }
 81784        return false;
 785    }
 786
 787    /// <summary>
 788    /// Merges <paramref name="newArguments"/> into <paramref name="existingField"/>'s argument
 789    /// dictionary in place. Caller (MergeFieldsInPlace via CanMergeFields) guarantees that
 790    /// <c>existingField._arguments</c> already has the same keys as <paramref name="newArguments"/>;
 791    /// the merge here only refines values for keys that hold nested dictionaries.
 792    /// </summary>
 793    // Callers (MergeFieldsInPlace via CanMergeFields) guarantee newArguments has Count > 0
 794    // and existingField._arguments is non-null with the same keys.
 795    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 796    internal static void MergeFieldArgumentsInPlace(this FieldDefinition existingField, IDictionary<string, object?> new
 797    {
 708798        var target = existingField._arguments!;
 2844799        foreach (var (key, newValue) in newArguments)
 800        {
 714801            if (target.TryGetValue(key, out var existingValue)
 714802                && existingValue is IDictionary<string, object?> existingDict
 714803                && newValue is IDictionary<string, object?> newDict)
 804            {
 18805                target[key] = Helpers.MergeNullableDictionaries(existingDict, newDict);
 18806                continue;
 807            }
 696808            target[key] = newValue;
 809        }
 708810    }
 811
 812    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 813    internal static FieldDefinition MergeFieldArguments(this FieldDefinition existingField, IDictionary<string, object?>
 814    {
 876815        if (newArguments is not { Count: > 0 }) return existingField;
 816
 414817        existingField.InvalidateMergeIndex();
 818
 414819        if (existingField._arguments is null || existingField._arguments.Count == 0)
 820        {
 339821            return existingField with
 339822            {
 339823                _arguments = AsSortedCaseInsensitive(newArguments),
 339824                _deepArgumentFingerprint = null,
 339825                _subtreeHasAnyArguments = null,
 339826            };
 827        }
 828
 75829        var merged = CopyToSortedCaseInsensitive(existingField._arguments);
 75830        ApplyArgumentOverrides(merged, newArguments);
 75831        return existingField with
 75832        {
 75833            _arguments = merged,
 75834            _deepArgumentFingerprint = null,
 75835            _subtreeHasAnyArguments = null,
 75836        };
 837    }
 838
 839    private static SortedDictionary<string, object?> AsSortedCaseInsensitive(IDictionary<string, object?> source)
 339840        => source is SortedDictionary<string, object?> sd
 339841            ? sd
 339842            : new SortedDictionary<string, object?>(source, StringComparer.OrdinalIgnoreCase);
 843
 844    private static SortedDictionary<string, object?> CopyToSortedCaseInsensitive(IDictionary<string, object?> source)
 845    {
 75846        var copy = new SortedDictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 393847        foreach (var (key, value) in source) copy[key] = value;
 75848        return copy;
 849    }
 850
 851    private static void ApplyArgumentOverrides(SortedDictionary<string, object?> target, IDictionary<string, object?> ov
 852    {
 318853        foreach (var (key, newValue) in overrides)
 854        {
 84855            target[key] = MergedArgumentValue(target, key, newValue);
 856        }
 75857    }
 858
 859    private static object? MergedArgumentValue(SortedDictionary<string, object?> target, string key, object? newValue)
 860    {
 84861        if (target.TryGetValue(key, out var existingValue)
 84862            && existingValue is IDictionary<string, object?> existingDict
 84863            && newValue is IDictionary<string, object?> newDict)
 864        {
 3865            return Helpers.MergeNullableDictionaries(existingDict, newDict);
 866        }
 81867        return newValue;
 868    }
 869}

Methods/Properties

CanMergeFields(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldDefinition)
AreConditionalDirectivesEqual(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldDefinition)
FindConditional(System.Collections.Generic.List`1<NGql.Core.Abstractions.FieldDirective>,System.String)
AreDirectivesStructurallyEqual(NGql.Core.Abstractions.FieldDirective,NGql.Core.Abstractions.FieldDirective)
HasConditionalDirectives(NGql.Core.Abstractions.FieldDefinition)
AreNestedFieldsCompatible(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldDefinition)
IncomingChildrenCompatible(NGql.Core.Abstractions.FieldChildren,NGql.Core.Abstractions.FieldChildren)
IsIncomingChildCompatible(NGql.Core.Abstractions.FieldChildren,NGql.Core.Abstractions.FieldDefinition)
FindChildByNameAndAlias(NGql.Core.Abstractions.FieldChildren,NGql.Core.Abstractions.FieldDefinition)
ExistingExtrasCompatible(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldChildren)
IsExistingExtraCompatible(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldChildren)
SubtreeHasAnyArguments(NGql.Core.Abstractions.FieldDefinition)
CombineDeepHash(System.UInt64,System.UInt64)
ComputeDeepFingerprint(NGql.Core.Abstractions.FieldDefinition)
ConditionalDirectiveFingerprint(NGql.Core.Abstractions.FieldDefinition)
ArgumentsFingerprint(System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>)
DeepChildrenFingerprint(NGql.Core.Abstractions.FieldChildren)
AnyChildHasArguments(NGql.Core.Abstractions.FieldChildren)
HasAnyArguments(NGql.Core.Abstractions.FieldDefinition)
DeepClone(NGql.Core.Abstractions.FieldDefinition)
DeepClone(NGql.Core.Abstractions.InlineFragmentDefinition)
DeepClone(NGql.Core.Abstractions.NamedFragmentDefinition)
DeepCloneFragments(System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.InlineFragmentDefinition>)
CloneFragmentBody(NGql.Core.Abstractions.FieldChildren,System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.InlineFragmentDefinition>,System.Collections.Generic.List`1<System.String>,NGql.Core.Abstractions.FieldChildren&,System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.InlineFragmentDefinition>&,System.Collections.Generic.List`1<System.String>&)
MergeFieldsInPlace(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldDefinition)
MergeFragmentState(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldDefinition)
MergeInlineFragmentsInPlace(NGql.Core.Abstractions.FieldDefinition,System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.InlineFragmentDefinition>)
MergeInlineFragmentBodyInPlace(NGql.Core.Abstractions.InlineFragmentDefinition,NGql.Core.Abstractions.InlineFragmentDefinition)
MergeSpreadsInPlace(NGql.Core.Abstractions.FieldDefinition,System.Collections.Generic.List`1<System.String>)
MergeInlineFragmentsInto(System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.InlineFragmentDefinition>&,System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.InlineFragmentDefinition>)
MergeSpreadNamesInto(System.Collections.Generic.List`1<System.String>&,System.Collections.Generic.List`1<System.String>)
MergeSpreadListInPlace(System.Collections.Generic.List`1<System.String>&,System.Collections.Generic.List`1<System.String>)
MergeDirectivesInPlace(NGql.Core.Abstractions.FieldDefinition,System.Collections.Generic.List`1<NGql.Core.Abstractions.FieldDirective>)
ThrowIfTypesConflict(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldDefinition)
MergeIncomingChildrenInPlace(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldChildren)
MergeIncomingArgumentsInPlace(NGql.Core.Abstractions.FieldDefinition,System.Collections.Generic.SortedDictionary`2<System.String,System.Object>)
InvalidateMergeMemoAfterChildrenMerge(NGql.Core.Abstractions.FieldDefinition,System.Boolean)
MergeChildInPlace(NGql.Core.Abstractions.FieldChildren,NGql.Core.Abstractions.FieldDefinition)
HasEffectiveNameConflict(NGql.Core.Abstractions.FieldChildren,System.String)
MergeFieldArgumentsInPlace(NGql.Core.Abstractions.FieldDefinition,System.Collections.Generic.IDictionary`2<System.String,System.Object>)
MergeFieldArguments(NGql.Core.Abstractions.FieldDefinition,System.Collections.Generic.IDictionary`2<System.String,System.Object>)
AsSortedCaseInsensitive(System.Collections.Generic.IDictionary`2<System.String,System.Object>)
CopyToSortedCaseInsensitive(System.Collections.Generic.IDictionary`2<System.String,System.Object>)
ApplyArgumentOverrides(System.Collections.Generic.SortedDictionary`2<System.String,System.Object>,System.Collections.Generic.IDictionary`2<System.String,System.Object>)
MergedArgumentValue(System.Collections.Generic.SortedDictionary`2<System.String,System.Object>,System.String,System.Object)