< Summary

Information
Class: NGql.Core.Abstractions.FieldDefinition
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Abstractions/FieldDefinition.cs
Line coverage
100%
Covered lines: 141
Uncovered lines: 0
Coverable lines: 141
Total lines: 545
Line coverage: 100%
Branch coverage
100%
Covered branches: 146
Total branches: 146
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get__fragments()100%22100%
set__fragments(...)100%66100%
get__spreadFragments()100%22100%
set__spreadFragments(...)100%66100%
get__directives()100%22100%
set__directives(...)100%66100%
get__metadata()100%22100%
set__metadata(...)100%66100%
ReplaceOptional(...)100%22100%
SetOptionalState(...)100%11100%
get__effectiveName()100%22100%
.ctor(...)100%11100%
Create(...)100%88100%
get_Path()100%11100%
get__subtreeHasAnyArguments()100%22100%
set__subtreeHasAnyArguments(...)100%44100%
get__deepArgumentFingerprint()100%22100%
set__deepArgumentFingerprint(...)100%22100%
EnsureMergeMemoTracker()100%11100%
InvalidateMergeIndex()100%22100%
.ctor(...)100%22100%
.ctor(...)100%44100%
.ctor(...)100%11100%
ToSortedArguments(...)100%66100%
AsChildren(...)100%66100%
get_Name()100%11100%
get_Type()100%11100%
set_Type(...)100%11100%
get_Alias()100%11100%
set_Alias(...)100%11100%
.cctor()100%11100%
get_Fields()100%22100%
get_InlineFragments()100%22100%
get_SpreadFragments()100%22100%
AddSpreadFragment(...)100%44100%
get_Directives()100%22100%
get_HasDirectives()100%22100%
AddDirective(...)100%22100%
get_Arguments()100%22100%
get_Metadata()100%1212100%
set_Metadata(...)100%11100%
SetMetadata(...)100%66100%
get_HasMetadata()100%22100%
get_IsArray()100%44100%
get_IsNullable()100%44100%
get_HasFields()100%22100%
get_HasInlineFragments()100%22100%
GetOrAddInlineFragment(...)100%44100%
get_IsNeverMerge()100%11100%
ClearMergeMemo()100%11100%
Equals(...)100%1212100%
GetHashCode()100%11100%
ToString()100%66100%

File(s)

/home/runner/work/NGql/NGql/src/Core/Abstractions/FieldDefinition.cs

#LineLine coverage
 1using System.Diagnostics.CodeAnalysis;
 2using System.Text.Json.Serialization;
 3using NGql.Core.Extensions;
 4
 5namespace NGql.Core.Abstractions;
 6
 7/// <summary>
 8///     Represents a field definition.
 9/// </summary>
 10[SuppressMessage("Minor Code Smell", "S2292:Trivial properties should be auto-implemented")]
 11public sealed record FieldDefinition
 12{
 13    // Fields
 14    internal FieldChildren? _children;
 15    internal string? _type;
 16    internal string? _alias;
 17    internal SortedDictionary<string, object?>? _arguments;
 18
 19    // Fragments, spreads, directives and metadata are absent on most fields. Keeping them behind
 20    // one reference saves three object slots per field. The holder is immutable and replaced on
 21    // assignment, so record copies that share it never observe one another's reassignments.
 22    private OptionalState? _optional;
 23
 24    internal Dictionary<string, InlineFragmentDefinition>? _fragments
 25    {
 682664226        get => _optional?.Fragments;
 2034527        set => ReplaceOptional(static (current, fragments) => OptionalState.Create(fragments, current?.SpreadFragments, 
 28    }
 29
 30    internal List<string>? _spreadFragments
 31    {
 683077632        get => _optional?.SpreadFragments;
 2032633        set => ReplaceOptional(static (current, spreads) => OptionalState.Create(current?.Fragments, spreads, current?.D
 34    }
 35
 36    internal List<FieldDirective>? _directives
 37    {
 687201738        get => _optional?.Directives;
 2574139        set => ReplaceOptional(static (current, directives) => OptionalState.Create(current?.Fragments, current?.SpreadF
 40    }
 41
 42    internal Dictionary<string, object?>? _metadata
 43    {
 9779544        get => _optional?.Metadata;
 22170645        init => _optional = OptionalState.Create(_optional?.Fragments, _optional?.SpreadFragments, _optional?.Directives
 46    }
 47
 48    // Reading Metadata attaches a dictionary, so a reader can race a builder adding a directive
 49    // or fragment. Compare-and-swap keeps either replacement from discarding the other's member.
 50    private void ReplaceOptional<T>(Func<OptionalState?, T, OptionalState?> replace, T value)
 51    {
 52        while (true)
 53        {
 5307954            var current = Volatile.Read(ref _optional);
 10610255            if (ReferenceEquals(Interlocked.CompareExchange(ref _optional, replace(current, value), current), current)) 
 56        }
 57    }
 58
 59    /// <summary>
 60    /// Assigns all optional members with a single holder allocation (none when all are null).
 61    /// </summary>
 62    internal void SetOptionalState(Dictionary<string, InlineFragmentDefinition>? fragments, List<string>? spreadFragment
 63        List<FieldDirective>? directives, Dictionary<string, object?>? metadata)
 912964        => Volatile.Write(ref _optional, OptionalState.Create(fragments, spreadFragments, directives, metadata));
 65
 1582566    internal string _effectiveName => !string.IsNullOrEmpty(_alias) ? _alias : Name;
 67
 68    private sealed class OptionalState
 69    {
 70        internal readonly Dictionary<string, InlineFragmentDefinition>? Fragments;
 71        internal readonly List<string>? SpreadFragments;
 72        internal readonly List<FieldDirective>? Directives;
 73        internal readonly Dictionary<string, object?>? Metadata;
 74
 5074775        private OptionalState(Dictionary<string, InlineFragmentDefinition>? fragments, List<string>? spreadFragments,
 5074776            List<FieldDirective>? directives, Dictionary<string, object?>? metadata)
 77        {
 5074778            Fragments = fragments;
 5074779            SpreadFragments = spreadFragments;
 5074780            Directives = directives;
 5074781            Metadata = metadata;
 5074782        }
 83
 84        internal static OptionalState? Create(Dictionary<string, InlineFragmentDefinition>? fragments, List<string>? spr
 85            List<FieldDirective>? directives, Dictionary<string, object?>? metadata)
 28386586            => fragments is null && spreadFragments is null && directives is null && metadata is null
 28386587                ? null
 28386588                : new OptionalState(fragments, spreadFragments, directives, metadata);
 89    }
 53970990    internal string Path { get; init; } = string.Empty;
 91
 92    /// <summary>
 93    /// Cached result of "does this field's subtree contain any arguments?".
 94    /// Null = not yet computed. Reset to null whenever the subtree mutates.
 95    /// </summary>
 96    internal bool? _subtreeHasAnyArguments
 97    {
 3978698        get => _subtreeArgumentState == CachedBoolean.Unknown ? null : _subtreeArgumentState == CachedBoolean.True;
 2053299        set => _subtreeArgumentState = value switch
 20532100        {
 9459101            true => CachedBoolean.True,
 2901102            false => CachedBoolean.False,
 8172103            null => CachedBoolean.Unknown
 20532104        };
 105    }
 106
 107    /// <summary>
 108    /// Cached conservative fingerprint over this field's own arguments plus, recursively, every
 109    /// descendant whose subtree carries arguments anywhere (see
 110    /// <see cref="Extensions.FieldDefinitionExtensions.ComputeDeepFingerprint"/>). Null = not yet
 111    /// computed. Reset to null at exactly the same two sites as <see cref="_subtreeHasAnyArguments"/>
 112    /// whenever the subtree mutates — the two caches share an invalidation contract by design.
 113    /// </summary>
 114    internal ulong? _deepArgumentFingerprint
 115    {
 18609116        get => _hasDeepArgumentFingerprint ? _deepArgumentFingerprintValue : null;
 117        set
 118        {
 24303119            if (value.HasValue)
 120            {
 13116121                _deepArgumentFingerprintValue = value.GetValueOrDefault();
 13116122                _hasDeepArgumentFingerprint = true;
 123            }
 124            else
 125            {
 11187126                _hasDeepArgumentFingerprint = false;
 127            }
 11187128        }
 129    }
 130
 131    // Keep cache state in independent bytes. Packing unrelated caches into shared bit flags
 132    // would let concurrent readers lose one another's updates. Splitting the fingerprint's
 133    // payload from its presence flag also avoids Nullable<ulong>'s alignment padding.
 134    private ulong _deepArgumentFingerprintValue;
 135    private volatile bool _hasDeepArgumentFingerprint;
 136    private CachedBoolean _subtreeArgumentState;
 137
 138    internal Features.MergeMemoTracker? _mergeMemoTracker;
 139
 140    internal Features.MergeMemoTracker EnsureMergeMemoTracker()
 7176141        => LazyInitializer.EnsureInitialized(ref _mergeMemoTracker, static () => new Features.MergeMemoTracker());
 142
 8142143    internal void InvalidateMergeIndex() => _mergeMemoTracker?.Invalidate();
 144
 145    private CachedBoolean _isArray;
 146    private CachedBoolean _isNullable;
 147
 148    private enum CachedBoolean : byte
 149    {
 150        Unknown,
 151        False,
 152        True
 153    }
 154
 155    /// <summary>
 156    /// Creates a field definition with a name and optional type and alias.
 157    /// <paramref name="type"/> defaults to <see cref="Constants.DefaultFieldType"/> when null.
 158    /// </summary>
 159    /// <param name="name">Field name as it appears in the rendered GraphQL.</param>
 160    /// <param name="type">Optional type-annotation metadata (rendered nowhere; consumed by tooling).</param>
 161    /// <param name="alias">Optional response-side alias.</param>
 162    public FieldDefinition(string name, string? type = null, string? alias = null)
 50673163        : this(name, type ?? Constants.DefaultFieldType, alias, null)
 164    {
 50673165    }
 166
 167    /// <summary>
 168    /// Creates a field definition with a pre-sorted argument dictionary and an optional
 169    /// child-field collection. Used internally on the hot path to avoid re-sorting.
 170    /// </summary>
 171    /// <param name="name">Field name.</param>
 172    /// <param name="type">Type-annotation metadata.</param>
 173    /// <param name="alias">Optional response-side alias.</param>
 174    /// <param name="sortedArguments">Pre-sorted argument map (case-insensitive); null/empty stores null.</param>
 175    /// <param name="fields">Optional initial children; null/empty leaves the field as a leaf.</param>
 284280176    public FieldDefinition(string name, string type, string? alias, SortedDictionary<string, object?>? sortedArguments =
 177    {
 284280178        Name = name;
 284280179        _alias = alias;
 284280180        _type = type;
 284280181        _arguments = sortedArguments?.Count > 0 ? sortedArguments : null;
 284280182        _children = AsChildren(fields);
 284280183    }
 184
 185    /// <summary>
 186    /// Creates a field definition from an unsorted argument dictionary (e.g. one produced by
 187    /// callers using collection initializers). The dictionary is copied into a case-insensitive
 188    /// sorted store so output ordering is stable.
 189    /// </summary>
 190    /// <param name="name">Field name.</param>
 191    /// <param name="type">Type-annotation metadata.</param>
 192    /// <param name="alias">Optional response-side alias.</param>
 193    /// <param name="arguments">Unsorted argument map; null/empty stores null.</param>
 194    /// <param name="fields">Optional initial children; null/empty leaves the field as a leaf.</param>
 654195    public FieldDefinition(string name, string type, string? alias, IDictionary<string, object?>? arguments, Dictionary<
 196    {
 654197        Name = name;
 654198        _alias = alias;
 654199        _type = type;
 654200        _arguments = ToSortedArguments(arguments);
 654201        _children = AsChildren(fields);
 654202    }
 203
 204    private static SortedDictionary<string, object?>? ToSortedArguments(IDictionary<string, object?>? arguments)
 205    {
 657206        if (arguments is null) return null;
 768207        if (arguments.Count == 0) return null;
 534208        var sorted = new SortedDictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 3723209        foreach (var kvp in arguments) sorted[kvp.Key] = kvp.Value;
 534210        return sorted;
 211    }
 212
 213    private static FieldChildren? AsChildren(Dictionary<string, FieldDefinition>? fields)
 214    {
 569742215        if (fields is null || fields.Count == 0) return null;
 126216        var children = new FieldChildren(fields.Count);
 810217        foreach (var kvp in fields) children.Append(kvp.Value);
 126218        return children;
 219    }
 220
 221    // Properties
 222    /// <summary>
 223    /// The name of the field.
 224    /// </summary>
 225    [JsonPropertyName("name")]
 154358132226    public string Name { get; init; }
 227
 228    /// <summary>
 229    /// The type of the field. Defaults to <see cref="Constants.DefaultFieldType"/> if not specified.
 230    /// This is used to define the data type of the field, such as "String", "Int", "Boolean", etc.
 231    /// </summary>
 232    [JsonPropertyName("type")]
 233    public string? Type
 234    {
 1206235        get => _type;
 720236        init => _type = value;
 237    }
 238
 239    /// <summary>
 240    /// The alias of the field, if any. This is used to provide a more readable or meaningful name for the field in quer
 241    /// If not specified, the field will use its original name.
 242    /// </summary>
 243    [JsonPropertyName("alias")]
 244    public string? Alias
 245    {
 153943031246        get => _alias;
 798247        init => _alias = value;
 248    }
 249
 3250    private static readonly IReadOnlyDictionary<string, FieldDefinition> EmptyReadOnlyFields
 3251        = new FieldChildren();
 252
 253    /// <summary>
 254    ///     The collection of fields related to <see cref="FieldDefinition"/>.
 255    /// </summary>
 256    [JsonPropertyName("fields")]
 257    public IReadOnlyDictionary<string, FieldDefinition> Fields
 3009258        => _children ?? EmptyReadOnlyFields;
 259
 3260    private static readonly IReadOnlyDictionary<string, InlineFragmentDefinition> EmptyReadOnlyFragments
 3261        = new Dictionary<string, InlineFragmentDefinition>();
 262
 263    /// <summary>
 264    /// Inline fragments attached to this field, keyed by the fragment's GraphQL type name
 265    /// (case-sensitive). Each fragment renders as <c>... on TypeName { … }</c> after the
 266    /// field's plain children, alphabetical by type name.
 267    /// </summary>
 268    /// <remarks>
 269    /// Used when the field's schema return type is a union or interface and the caller needs
 270    /// type narrowing. See <see cref="NGql.Core.Builders.FieldBuilder.OnType(string, Action{NGql.Core.Builders.FieldBui
 271    /// for the builder-side API. Multiple <c>OnType</c> calls for the same type name merge
 272    /// into one fragment definition.
 273    /// </remarks>
 274    [JsonPropertyName("inlineFragments")]
 275    public IReadOnlyDictionary<string, InlineFragmentDefinition> InlineFragments
 9648276        => (IReadOnlyDictionary<string, InlineFragmentDefinition>?)_fragments ?? EmptyReadOnlyFragments;
 277
 3278    private static readonly IReadOnlyList<string> EmptyReadOnlySpreadFragments = Array.Empty<string>();
 279
 280    /// <summary>
 281    /// Names of <see cref="NamedFragmentDefinition"/>s spread into this field's selection set
 282    /// (rendered as <c>...Name</c>). Order is preserved from <c>SpreadFragment</c> calls; the
 283    /// renderer emits spreads in declaration order after plain fields and inline fragments.
 284    /// </summary>
 285    /// <remarks>
 286    /// A list, not a set: the same fragment may legitimately appear multiple times at the
 287    /// same selection set once directives are added in a future release (e.g.
 288    /// <c>...Foo @include(if: $a) ...Foo @include(if: $b)</c>). Today, repeated spreads of
 289    /// the same name produce duplicate <c>...Name</c> output that the server collapses; the
 290    /// builder de-duplicates eagerly at <c>FieldBuilder.SpreadFragment</c> to keep render
 291    /// output minimal.
 292    ///
 293    /// Spreads are not validated against <see cref="QueryDefinition.NamedFragments"/> — an
 294    /// undeclared spread renders verbatim and the server rejects it. NGql is schemaless.
 295    /// </remarks>
 296    [JsonPropertyName("spreadFragments")]
 297    public IReadOnlyList<string> SpreadFragments
 9657298        => (IReadOnlyList<string>?)_spreadFragments ?? EmptyReadOnlySpreadFragments;
 299
 300    /// <summary>
 301    /// Append a fragment-spread reference to this field's selection set. No-ops when the
 302    /// fragment name is already present, keeping the render output minimal until directives
 303    /// (which would distinguish duplicate spreads) are supported.
 304    /// </summary>
 305    internal void AddSpreadFragment(string name)
 306    {
 9801307        var spreads = _spreadFragments;
 9801308        if (spreads is null)
 309        {
 9768310            spreads = new List<string>();
 9768311            _spreadFragments = spreads;
 312        }
 313
 314        // List<string>.Contains is ordinal by default; avoids the enumerator allocation of the
 315        // LINQ Contains(value, comparer) overload.
 9801316        if (!spreads.Contains(name))
 317        {
 9795318            spreads.Add(name);
 319        }
 9801320    }
 321
 3322    private static readonly IReadOnlyList<FieldDirective> EmptyReadOnlyDirectives = Array.Empty<FieldDirective>();
 323
 324    /// <summary>
 325    /// Directives attached to this field, in the order they were added. Rendered after the field's
 326    /// name and arguments and before its selection set, e.g. <c>@include(if:$a) @skip(if:$b)</c>.
 327    /// Returns an empty list (never null) when the field has no directives.
 328    /// </summary>
 329    /// <remarks>
 330    /// Order is user-visible and preserved verbatim — directives are never sorted. See
 331    /// <see cref="NGql.Core.Builders.FieldBuilder.IncludeIf(Variable)"/>,
 332    /// <see cref="NGql.Core.Builders.FieldBuilder.SkipIf(Variable)"/>, and
 333    /// <see cref="NGql.Core.Builders.FieldBuilder.Directive(string, System.Collections.Generic.Dictionary{string, objec
 334    /// for the builder-side API.
 335    /// </remarks>
 336    [JsonPropertyName("directives")]
 337    public IReadOnlyList<FieldDirective> Directives
 9633338        => (IReadOnlyList<FieldDirective>?)_directives ?? EmptyReadOnlyDirectives;
 339
 340    /// <summary>
 341    /// Gets a value indicating whether this field carries any directives. Unlike reading
 342    /// <see cref="Directives"/>, this check allocates nothing on directive-less fields — prefer it
 343    /// as the guard when scanning field trees.
 344    /// </summary>
 345    [JsonIgnore]
 36346    public bool HasDirectives => _directives is { Count: > 0 };
 347
 348    /// <summary>
 349    /// Append a directive to this field's directive list. Order is preserved. A directive that is
 350    /// structurally identical to one already present is skipped, so calling e.g.
 351    /// <c>.Directive("format", …).Directive("format", …)</c> with identical arguments on one field
 352    /// renders the directive once rather than duplicating it. Directives that differ (name or
 353    /// arguments) are all kept, EXCEPT for the two non-repeatable, spec-constrained directive names
 354    /// <c>include</c> and <c>skip</c> — see <see cref="DirectiveListOps.Add"/>, which every
 355    /// <c>@include</c>/<c>@skip</c> call site (both <see cref="Builders.FieldBuilder.IncludeIf(Variable)"/>/
 356    /// <see cref="Builders.FieldBuilder.SkipIf(Variable)"/> and the generic
 357    /// <see cref="Builders.FieldBuilder.Directive(string, System.Collections.Generic.Dictionary{string, object?})"/>
 358    /// called with <c>"include"</c>/<c>"skip"</c> directly — routes through) shares with
 359    /// <see cref="InlineFragmentDefinition.AddDirective"/>.
 360    /// </summary>
 361    internal void AddDirective(FieldDirective directive)
 362    {
 9891363        var current = _directives;
 9891364        var directives = current;
 9891365        DirectiveListOps.Add(ref directives, directive);
 19737366        if (!ReferenceEquals(directives, current)) _directives = directives;
 9891367    }
 368
 3369    private static readonly IReadOnlyDictionary<string, object?> EmptyReadOnlyArguments
 3370        = new SortedDictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 371
 372    /// <summary>
 373    /// Sorted, case-insensitive view of the field's GraphQL arguments. Returns an empty
 374    /// dictionary (never null) when the field has no arguments.
 375    /// </summary>
 376    [JsonPropertyName("arguments")]
 377    public IReadOnlyDictionary<string, object?> Arguments
 339378        => _arguments ?? EmptyReadOnlyArguments;
 379
 380    /// <summary>
 381    /// Metadata associated with the field definition.
 382    /// This can include additional information such as descriptions, tags, or any other relevant data.
 383    ///
 384    /// Not used during query text generation but can be useful for documentation or introspection purposes.
 385    /// </summary>
 386    /// <remarks>
 387    /// Reading this property materializes (and permanently attaches) an empty dictionary on
 388    /// fields that have no metadata, so the returned instance is always safe to mutate. When
 389    /// walking large trees just to test for metadata, check <see cref="HasMetadata"/> first —
 390    /// it allocates nothing.
 391    /// </remarks>
 392    [JsonPropertyName("metadata")]
 393    public Dictionary<string, object?> Metadata
 394    {
 395        get
 396        {
 65679397            var metadata = _metadata;
 111605398            if (metadata is not null) return metadata;
 399
 39552400            ReplaceOptional(static (current, created) => current?.Metadata is not null
 39552401                ? current
 39552402                : OptionalState.Create(current?.Fragments, current?.SpreadFragments, current?.Directives, created), new 
 19753403            return _metadata!;
 404        }
 69405        set => SetMetadata(value);
 406    }
 407
 408    private void SetMetadata(Dictionary<string, object?>? metadata)
 138409        => ReplaceOptional(static (current, value) => OptionalState.Create(current?.Fragments, current?.SpreadFragments,
 410
 411    /// <summary>
 412    /// Gets a value indicating whether this field carries any metadata. Unlike reading
 413    /// <see cref="Metadata"/>, this check does not allocate or attach an empty dictionary
 414    /// to metadata-less fields — prefer it as the guard when scanning field trees.
 415    /// </summary>
 416    [JsonIgnore]
 39417    public bool HasMetadata => _metadata is { Count: > 0 };
 418
 419    /// <summary>
 420    /// Gets a value indicating whether this field type is an array.
 421    /// </summary>
 422    [JsonIgnore]
 423    public bool IsArray
 424    {
 425        get
 426        {
 3270427            if (_isArray == CachedBoolean.Unknown)
 519428                _isArray = _type.IsArrayType() ? CachedBoolean.True : CachedBoolean.False;
 3270429            return _isArray == CachedBoolean.True;
 430        }
 431    }
 432
 433    /// <summary>
 434    /// Gets a value indicating whether this field type is nullable.
 435    /// </summary>
 436    [JsonIgnore]
 437    public bool IsNullable
 438    {
 439        get
 440        {
 3240441            if (_isNullable == CachedBoolean.Unknown)
 495442                _isNullable = _type.IsNullableType() ? CachedBoolean.True : CachedBoolean.False;
 3240443            return _isNullable == CachedBoolean.True;
 444        }
 445    }
 446
 447    /// <summary>
 448    /// Gets a value indicating whether this field has child fields.
 449    /// </summary>
 450    [JsonIgnore]
 387451    public bool HasFields => _children is { Count: > 0 };
 452
 453    /// <summary>
 454    /// Gets a value indicating whether this field has any inline fragments.
 455    /// </summary>
 456    [JsonIgnore]
 33457    public bool HasInlineFragments => _fragments is { Count: > 0 };
 458
 459    /// <summary>
 460    /// Returns the existing inline fragment for <paramref name="typeName"/>, or appends a new
 461    /// one. Used by the builder to merge multiple <c>OnType("Repository", …)</c> calls on the
 462    /// same parent into a single fragment definition.
 463    /// </summary>
 464    internal InlineFragmentDefinition GetOrAddInlineFragment(string typeName)
 465    {
 9816466        var fragments = _fragments;
 9816467        if (fragments is null)
 468        {
 9783469            fragments = new Dictionary<string, InlineFragmentDefinition>(StringComparer.Ordinal);
 9783470            _fragments = fragments;
 471        }
 472
 9816473        if (!fragments.TryGetValue(typeName, out var fragment))
 474        {
 9810475            fragment = new InlineFragmentDefinition(typeName);
 9810476            fragments[typeName] = fragment;
 477        }
 9816478        return fragment;
 479    }
 480
 481    /// <summary>
 482    /// When <c>true</c>, the merger treats this field as opaque — it will not be merged with
 483    /// other fields of the same path; instead it gets aliased (<c>name_1</c>, <c>name_2</c>, …)
 484    /// during <see cref="NGql.Core.Builders.QueryBuilder.Include(NGql.Core.Builders.QueryBuilder)"/>.
 485    /// The setter is internal; the flag is set by <see cref="MergingStrategy.NeverMerge"/>.
 486    /// </summary>
 487    [JsonIgnore]
 25740488    public bool IsNeverMerge { get; internal set; }
 489
 490    /// <summary>
 491    /// Clears this field's memoized <see cref="_deepArgumentFingerprint"/> and
 492    /// <see cref="_subtreeHasAnyArguments"/> caches. Both caches summarize this field's entire
 493    /// subtree, so any code path that mutates arguments or children anywhere beneath a field —
 494    /// not just on the field itself — must call this on every ancestor between the root and the
 495    /// mutation point, not only on the mutated node. A stale fingerprint that undercounts a
 496    /// descendant's arguments causes <see cref="NGql.Core.Features.QueryMerger"/> to skip a
 497    /// genuine merge candidate (a silent false split), which is strictly worse than the cost of
 498    /// clearing eagerly.
 499    /// </summary>
 500    internal void ClearMergeMemo()
 501    {
 7437502        _deepArgumentFingerprint = null;
 7437503        _subtreeHasAnyArguments = null;
 7437504        InvalidateMergeIndex();
 7437505    }
 506
 507    // Methods
 508    public bool Equals(FieldDefinition? other)
 509    {
 40965510        if (other is null) return false;
 261511        if (ReferenceEquals(this, other)) return true;
 213512        return string.Equals(Name, other.Name, StringComparison.Ordinal)
 213513            && string.Equals(Path, other.Path, StringComparison.Ordinal)
 213514            && string.Equals(_type, other._type, StringComparison.OrdinalIgnoreCase)
 213515            && string.Equals(_alias, other._alias, StringComparison.OrdinalIgnoreCase)
 213516            && IsNeverMerge == other.IsNeverMerge;
 517    }
 518
 519    // FieldDefinition holds mutable internal state by design (in-place merging in QueryMerger).
 520    // The hash captures identity at evaluation time; callers do not stash hashes across mutations.
 521#pragma warning disable S2328
 522    public override int GetHashCode()
 523    {
 524        // Comparer-based hashing for the case-insensitive members — equal under
 525        // OrdinalIgnoreCase implies equal hash, without the ToLowerInvariant string allocations.
 81526        var hash = new HashCode();
 81527        hash.Add(Name);
 81528        hash.Add(Path);
 81529        hash.Add(_type, StringComparer.OrdinalIgnoreCase);
 81530        hash.Add(_alias, StringComparer.OrdinalIgnoreCase);
 81531        hash.Add(IsNeverMerge);
 81532        return hash.ToHashCode();
 533    }
 534#pragma warning restore S2328
 535
 536    public override string ToString()
 537    {
 18538        if (string.IsNullOrWhiteSpace(Type))
 539        {
 6540            return string.IsNullOrWhiteSpace(Alias) ? Name : $"{Alias}:{Name}";
 541        }
 542
 12543        return string.IsNullOrWhiteSpace(Alias) ? $"{Type} {Name}" : $"{Type} {Alias}:{Name}";
 544    }
 545}

Methods/Properties

get__fragments()
set__fragments(System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.InlineFragmentDefinition>)
get__spreadFragments()
set__spreadFragments(System.Collections.Generic.List`1<System.String>)
get__directives()
set__directives(System.Collections.Generic.List`1<NGql.Core.Abstractions.FieldDirective>)
get__metadata()
set__metadata(System.Collections.Generic.Dictionary`2<System.String,System.Object>)
ReplaceOptional(System.Func`3<NGql.Core.Abstractions.FieldDefinition/OptionalState,T,NGql.Core.Abstractions.FieldDefinition/OptionalState>,T)
SetOptionalState(System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.InlineFragmentDefinition>,System.Collections.Generic.List`1<System.String>,System.Collections.Generic.List`1<NGql.Core.Abstractions.FieldDirective>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
get__effectiveName()
.ctor(System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.InlineFragmentDefinition>,System.Collections.Generic.List`1<System.String>,System.Collections.Generic.List`1<NGql.Core.Abstractions.FieldDirective>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
Create(System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.InlineFragmentDefinition>,System.Collections.Generic.List`1<System.String>,System.Collections.Generic.List`1<NGql.Core.Abstractions.FieldDirective>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
get_Path()
get__subtreeHasAnyArguments()
set__subtreeHasAnyArguments(System.Nullable`1<System.Boolean>)
get__deepArgumentFingerprint()
set__deepArgumentFingerprint(System.Nullable`1<System.UInt64>)
EnsureMergeMemoTracker()
InvalidateMergeIndex()
.ctor(System.String,System.String,System.String)
.ctor(System.String,System.String,System.String,System.Collections.Generic.SortedDictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.FieldDefinition>)
.ctor(System.String,System.String,System.String,System.Collections.Generic.IDictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.FieldDefinition>)
ToSortedArguments(System.Collections.Generic.IDictionary`2<System.String,System.Object>)
AsChildren(System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.FieldDefinition>)
get_Name()
get_Type()
set_Type(System.String)
get_Alias()
set_Alias(System.String)
.cctor()
get_Fields()
get_InlineFragments()
get_SpreadFragments()
AddSpreadFragment(System.String)
get_Directives()
get_HasDirectives()
AddDirective(NGql.Core.Abstractions.FieldDirective)
get_Arguments()
get_Metadata()
set_Metadata(System.Collections.Generic.Dictionary`2<System.String,System.Object>)
SetMetadata(System.Collections.Generic.Dictionary`2<System.String,System.Object>)
get_HasMetadata()
get_IsArray()
get_IsNullable()
get_HasFields()
get_HasInlineFragments()
GetOrAddInlineFragment(System.String)
get_IsNeverMerge()
ClearMergeMemo()
Equals(NGql.Core.Abstractions.FieldDefinition)
GetHashCode()
ToString()