< Summary

Information
Class: NGql.Core.Builders.FieldBuilder
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Builders/FieldBuilder.cs
Line coverage
100%
Covered lines: 225
Uncovered lines: 0
Coverable lines: 225
Total lines: 1059
Line coverage: 100%
Branch coverage
100%
Covered branches: 122
Total branches: 122
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%66100%
ClearAncestorMergeMemos()100%22100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddFieldCore(...)100%1616100%
Create(...)100%11100%
Create(...)100%44100%
Create(...)100%11100%
CreateAncestorChain(...)100%1414100%
TryWalkPath(...)100%1212100%
TryFindAncestors(...)100%1010100%
Build()100%11100%
RecursiveCreateField(...)100%66100%
RecursiveCreateField(...)100%66100%
Include(...)100%11100%
Include(...)100%11100%
AddSubField(...)100%11100%
ValidateFieldNameSegments(...)100%1010100%
WithAlias(...)100%11100%
WithType(...)100%11100%
WithMetadata(...)100%11100%
Where(...)100%88100%
OnType(...)100%22100%
PopulateFragmentSurface(...)100%11100%
SpreadFragment(...)100%22100%
IncludeIf(...)100%11100%
IncludeIf(...)100%11100%
SkipIf(...)100%11100%
SkipIf(...)100%11100%
Directive(...)100%1010100%
AddIfDirective(...)100%44100%
AddIfDirective(...)100%44100%
IsBooleanType(...)100%22100%
InvalidateMergeMemoIfConditional(...)100%44100%

File(s)

/home/runner/work/NGql/NGql/src/Core/Builders/FieldBuilder.cs

#LineLine coverage
 1using System.Runtime.CompilerServices;
 2using NGql.Core.Abstractions;
 3using NGql.Core.Extensions;
 4
 5namespace NGql.Core.Builders;
 6
 7/// <summary>
 8/// Fluent helper passed to the <c>fieldBuilder</c> action overloads of
 9/// <see cref="QueryBuilder.AddField(string, Action{FieldBuilder})"/>. Use it to add
 10/// nested fields, configure a parent field's type/arguments/metadata, or compose
 11/// sub-trees without leaving the outer chain.
 12/// </summary>
 13public sealed class FieldBuilder
 14{
 15    private FieldDefinition _fieldDefinition;
 16
 17    // Tracks the enclosing FieldBuilder for nested Action<FieldBuilder> scopes (null for a root
 18    // builder, e.g. one created via QueryBuilder.AddField or a fragment surface). FieldDefinition
 19    // itself carries no parent back-pointer — adding one would ripple through Equals/DeepClone/
 20    // `with` semantics — so this is the only place a captured, since-detached FieldBuilder (see
 21    // Where()'s remarks) can still discover which ancestors' memoized deep fingerprints it must
 22    // invalidate.
 23    private readonly FieldBuilder? _parent;
 24
 25    // The owning operation's variable set, threaded down from QueryBuilder.AddField(...) at the
 26    // root and inherited by every nested Action<FieldBuilder> scope. Null for a FieldBuilder with
 27    // no owning operation (the public FieldBuilder.Create(FieldDefinition) factory, used
 28    // standalone e.g. by tests or FieldFactory-adjacent code) — IncludeIf(Variable)/SkipIf(Variable)
 29    // still attach the directive in that case, they simply have nothing to promote the variable
 30    // into.
 31    private readonly SortedSet<Variable>? _variableSink;
 32
 264933    private FieldBuilder(FieldDefinition fieldDefinition, FieldBuilder? parent = null, SortedSet<Variable>? variableSink
 34    {
 264935        _fieldDefinition = fieldDefinition;
 468936        if (parent is null) fieldDefinition.EnsureMergeMemoTracker();
 264937        _parent = parent;
 264938        _variableSink = variableSink ?? parent?._variableSink;
 264939    }
 40
 41    // Clears the memoized deep-fingerprint/subtree-has-arguments caches on every ancestor in this
 42    // builder's nesting chain, walking from the immediate parent up to the root. Used by mutation
 43    // sites (Where()) that can run long after the Action<FieldBuilder> that produced this builder
 44    // has returned — at which point AddFieldCore's own cascading ClearMergeMemo calls (one per
 45    // nesting level, run as each action unwinds) have already happened and cannot see this later
 46    // mutation. Idempotent and cheap: ClearMergeMemo is O(1) per node, and a chain of unrelated
 47    // Where() calls simply repeats the same small walk.
 48    private void ClearAncestorMergeMemos()
 49    {
 88550        var ancestor = _parent;
 114051        while (ancestor != null)
 52        {
 25553            ancestor._fieldDefinition.ClearMergeMemo();
 25554            ancestor = ancestor._parent;
 55        }
 88556    }
 57
 58    /// <summary>
 59    /// Adds a field definition to the builder.
 60    /// </summary>
 61    /// <param name="fieldDefinition">The field definition to add.</param>
 62    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 63    /// <exception cref="ArgumentNullException">Thrown when fieldDefinition is null.</exception>
 64    public FieldBuilder AddField(FieldDefinition fieldDefinition)
 65    {
 366        AddSubField(_fieldDefinition, fieldDefinition);
 367        return this;
 68    }
 69
 70    /// <summary>
 71    /// Adds a field with nested subfields, optional arguments, and metadata to the builder.
 72    /// This creates an object-type field that contains the specified subfields.
 73    /// </summary>
 74    /// <param name="fieldName">The name of the parent field to add. Supports dotted notation for nested fields.</param>
 75    /// <param name="subFields">Array of subfield names to add under this field. Each subfield will have default String 
 76    /// <param name="arguments">Optional GraphQL arguments for the parent field (e.g., filtering, pagination parameters)
 77    /// <param name="metadata">Optional metadata dictionary to associate with the parent field.</param>
 78    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 79    /// <exception cref="ArgumentNullException">Thrown when fieldName or subFields is null.</exception>
 80    /// <exception cref="ArgumentException">Thrown when fieldName is empty or subFields array is empty.</exception>
 81    public FieldBuilder AddField(string fieldName, string[] subFields, Dictionary<string, object?>? arguments = null, Di
 11182        => AddFieldCore(fieldName, Constants.ObjectFieldType, arguments, subFields, metadata);
 83
 84    /// <summary>
 85    /// Adds a field with arguments, then nested subfields and optional metadata. This is the args-first
 86    /// counterpart to <see cref="AddField(string, string[], Dictionary{string, object?}?, Dictionary{string, object?}?)
 87    /// and matches the parameter order used by <see cref="QueryBuilder.AddField(string, Dictionary{string, object?}, st
 88    /// </summary>
 89    /// <param name="fieldName">The name of the parent field to add.</param>
 90    /// <param name="arguments">GraphQL arguments for the parent field.</param>
 91    /// <param name="subFields">Array of subfield names to add under this field.</param>
 92    /// <param name="metadata">Optional metadata dictionary to associate with the parent field.</param>
 93    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 94    public FieldBuilder AddField(string fieldName, Dictionary<string, object?>? arguments, string[] subFields, Dictionar
 1295        => AddFieldCore(fieldName, Constants.ObjectFieldType, arguments, subFields, metadata);
 96
 97    /// <summary>
 98    /// Adds a field with a specific type, GraphQL arguments, and optional metadata to the builder.
 99    /// </summary>
 100    /// <param name="fieldName">The name of the field to add. Supports dotted notation for nested fields.</param>
 101    /// <param name="type">The GraphQL type of the field (e.g., "String!", "[String]", custom types).</param>
 102    /// <param name="arguments">GraphQL arguments for the field such as filters, pagination, or custom parameters.</para
 103    /// <param name="metadata">Optional metadata dictionary to associate with the field.</param>
 104    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 105    /// <exception cref="ArgumentNullException">Thrown when fieldName or type is null.</exception>
 106    /// <example>
 107    /// <code>
 108    /// builder.AddField("users", "User", new Dictionary&lt;string, object?&gt; { ["first"] = 10 });
 109    /// </code>
 110    /// </example>
 111    public FieldBuilder AddField(string fieldName, string type, Dictionary<string, object?>? arguments, Dictionary<strin
 129112        => AddFieldCore(fieldName, type, arguments, metadata: metadata);
 113
 114    /// <summary>
 115    /// Adds a field with a specific type, nested subfields, and optional metadata to the builder.
 116    /// </summary>
 117    /// <param name="fieldName">The name of the parent field to add. Supports dotted notation for nested fields.</param>
 118    /// <param name="type">The GraphQL type of the parent field (typically an object type or array type).</param>
 119    /// <param name="subFields">Array of subfield names to add under this field. Each subfield will have default String 
 120    /// <param name="metadata">Optional metadata dictionary to associate with the parent field.</param>
 121    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 122    /// <exception cref="ArgumentNullException">Thrown when fieldName, type, or subFields is null.</exception>
 123    /// <example>
 124    /// <code>
 125    /// builder.AddField("user", "User", new[] { "id", "name", "email" });
 126    /// </code>
 127    /// </example>
 128    public FieldBuilder AddField(string fieldName, string type, string[] subFields, Dictionary<string, object?>? metadat
 84129        => AddFieldCore(fieldName, type, subFields: subFields, metadata: metadata);
 130
 131    /// <summary>
 132    /// Adds a field with GraphQL arguments to the builder. The field will have the default String type.
 133    /// </summary>
 134    /// <param name="fieldName">The name of the field to add. Supports dotted notation for nested fields.</param>
 135    /// <param name="arguments">GraphQL arguments for the field such as filters, pagination, or custom parameters.</para
 136    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 137    /// <exception cref="ArgumentNullException">Thrown when fieldName is null.</exception>
 138    /// <example>
 139    /// <code>
 140    /// builder.AddField("searchResults", new SortedDictionary&lt;string, object?&gt; { ["query"] = "GraphQL" });
 141    /// </code>
 142    /// </example>
 143    public FieldBuilder AddField(string fieldName, Dictionary<string, object?>? arguments)
 54144        => AddFieldCore(fieldName, arguments: arguments);
 145
 146    /// <summary>
 147    /// Adds a field with GraphQL arguments and metadata to the builder. The field will have the default String type.
 148    /// </summary>
 149    /// <param name="fieldName">The name of the field to add. Supports dotted notation for nested fields.</param>
 150    /// <param name="arguments">GraphQL arguments for the field such as filters, pagination, or custom parameters.</para
 151    /// <param name="metadata">Optional metadata dictionary to associate with the field for custom processing.</param>
 152    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 153    /// <exception cref="ArgumentNullException">Thrown when fieldName is null.</exception>
 154    public FieldBuilder AddField(string fieldName, Dictionary<string, object?>? arguments, Dictionary<string, object?>? 
 9155        => AddFieldCore(fieldName, arguments: arguments, metadata: metadata);
 156
 157    /// <summary>
 158    /// Adds a field with a specific type, nested subfields, GraphQL arguments, and metadata to the builder.
 159    /// This is the most comprehensive overload that supports all field configuration options.
 160    /// </summary>
 161    /// <param name="fieldName">The name of the parent field to add. Supports dotted notation for nested fields.</param>
 162    /// <param name="type">The GraphQL type of the parent field (typically an object type or array type).</param>
 163    /// <param name="subFields">Array of subfield names to add under this field. Each subfield will have default String 
 164    /// <param name="arguments">GraphQL arguments for the parent field such as filters, pagination, or custom parameters
 165    /// <param name="metadata">Optional metadata dictionary to associate with the parent field for custom processing.</p
 166    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 167    /// <exception cref="ArgumentNullException">Thrown when fieldName, type, or subFields is null.</exception>
 168    public FieldBuilder AddField(string fieldName, string type, string[] subFields, Dictionary<string, object?>? argumen
 6169        => AddFieldCore(fieldName, type, arguments, subFields, metadata);
 170
 171    /// <summary>
 172    /// Adds a field with a nested builder action for configuring subfields dynamically.
 173    /// The field will have the default String type and allows fluent configuration of nested fields.
 174    /// </summary>
 175    /// <param name="fieldName">The name of the field to add. Supports dotted notation for nested fields.</param>
 176    /// <param name="action">A delegate that receives a FieldBuilder instance for configuring nested fields within this 
 177    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 178    /// <exception cref="ArgumentNullException">Thrown when fieldName or action is null.</exception>
 179    /// <example>
 180    /// <code>
 181    /// builder.AddField("user", user => {
 182    ///     user.AddField("id");
 183    ///     user.AddField("profile", profile => {
 184    ///         profile.AddField("name");
 185    ///         profile.AddField("email");
 186    ///     });
 187    /// });
 188    /// </code>
 189    /// </example>
 190    public FieldBuilder AddField(string fieldName, Action<FieldBuilder> action)
 312191        => AddFieldCore(fieldName, action: action);
 192
 193    /// <summary>
 194    /// Adds a field with GraphQL arguments and a nested builder action for configuring subfields dynamically.
 195    /// The field will have the default String type.
 196    /// </summary>
 197    /// <remarks>
 198    /// <b>Behavior change in 2.1:</b> the dictionary at this position is now interpreted as
 199    /// <c>arguments</c>, matching the conventions of <see cref="QueryBuilder.AddField(string, Dictionary{string, object
 200    /// and the rest of the <see cref="FieldBuilder"/> args-first overloads. In NGql 2.0 the same
 201    /// signature was interpreted as <c>metadata</c>; callers that relied on the old behavior must
 202    /// switch to the four-arg form with named arguments:
 203    /// <c>AddField(field, arguments: null, metadata: dict, action)</c>.
 204    /// </remarks>
 205    /// <param name="fieldName">The name of the field to add. Supports dotted notation for nested fields.</param>
 206    /// <param name="arguments">GraphQL arguments for the field.</param>
 207    /// <param name="action">A delegate that receives a FieldBuilder instance for configuring nested fields within this 
 208    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 209    /// <exception cref="ArgumentNullException">Thrown when fieldName or action is null.</exception>
 210    public FieldBuilder AddField(string fieldName, Dictionary<string, object?>? arguments, Action<FieldBuilder> action)
 60211        => AddFieldCore(fieldName, arguments: arguments, action: action);
 212
 213    /// <summary>
 214    /// Adds a field with a specific type and a nested builder action for configuring subfields dynamically.
 215    /// </summary>
 216    /// <param name="fieldName">The name of the field to add. Supports dotted notation for nested fields.</param>
 217    /// <param name="type">The GraphQL type of the field (typically an object type when using nested actions).</param>
 218    /// <param name="action">A delegate that receives a FieldBuilder instance for configuring nested fields within this 
 219    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 220    /// <exception cref="ArgumentNullException">Thrown when fieldName, type, or action is null.</exception>
 221    /// <example>
 222    /// <code>
 223    /// builder.AddField("posts", "[Post]", posts => {
 224    ///     posts.AddField("id", "ID!");
 225    ///     posts.AddField("title");
 226    ///     posts.AddField("author", "User", author => {
 227    ///         author.AddField("name");
 228    ///     });
 229    /// });
 230    /// </code>
 231    /// </example>
 232    public FieldBuilder AddField(string fieldName, string type, Action<FieldBuilder> action)
 15233        => AddFieldCore(fieldName, type, action: action);
 234
 235    /// <summary>
 236    /// Adds a field with a specific type, metadata, and a nested builder action for configuring subfields dynamically.
 237    /// </summary>
 238    /// <param name="fieldName">The name of the field to add. Supports dotted notation for nested fields.</param>
 239    /// <param name="type">The GraphQL type of the field (typically an object type when using nested actions).</param>
 240    /// <param name="metadata">Optional metadata dictionary to associate with the field for custom processing.</param>
 241    /// <param name="action">A delegate that receives a FieldBuilder instance for configuring nested fields within this 
 242    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 243    /// <exception cref="ArgumentNullException">Thrown when fieldName, type, or action is null.</exception>
 244    public FieldBuilder AddField(string fieldName, string type, Dictionary<string, object?>? metadata, Action<FieldBuild
 6245        => AddFieldCore(fieldName, type, metadata: metadata, action: action);
 246
 247    /// <summary>
 248    /// Adds a field with GraphQL arguments, metadata, and a nested builder action for configuring subfields dynamically
 249    /// The field will have the default String type.
 250    /// </summary>
 251    /// <param name="fieldName">The name of the field to add. Supports dotted notation for nested fields.</param>
 252    /// <param name="arguments">GraphQL arguments for the field such as filters, pagination, or custom parameters.</para
 253    /// <param name="metadata">Optional metadata dictionary to associate with the field for custom processing.</param>
 254    /// <param name="action">A delegate that receives a FieldBuilder instance for configuring nested fields within this 
 255    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 256    /// <exception cref="ArgumentNullException">Thrown when fieldName or action is null.</exception>
 257    public FieldBuilder AddField(string fieldName, Dictionary<string, object?>? arguments, Dictionary<string, object?>? 
 9258        => AddFieldCore(fieldName, arguments: arguments, metadata: metadata, action: action);
 259
 260    /// <summary>
 261    /// Adds a field with a specific type, GraphQL arguments, metadata, and a nested builder action for configuring subf
 262    /// This is the most comprehensive action-based overload that supports all field configuration options.
 263    /// </summary>
 264    /// <param name="fieldName">The name of the field to add. Supports dotted notation for nested fields.</param>
 265    /// <param name="type">The GraphQL type of the field (typically an object type when using nested actions).</param>
 266    /// <param name="arguments">GraphQL arguments for the field such as filters, pagination, or custom parameters.</para
 267    /// <param name="metadata">Optional metadata dictionary to associate with the field for custom processing.</param>
 268    /// <param name="action">A delegate that receives a FieldBuilder instance for configuring nested fields within this 
 269    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 270    /// <exception cref="ArgumentNullException">Thrown when fieldName, type, or action is null.</exception>
 271    public FieldBuilder AddField(string fieldName, string type, Dictionary<string, object?>? arguments, Dictionary<strin
 3272        => AddFieldCore(fieldName, type, arguments, metadata: metadata, action: action);
 273
 274    /// <summary>
 275    /// Adds a field with predefined subfields and a nested builder action for additional dynamic configuration.
 276    /// The field will have an object type and includes both the specified subfields and any fields configured via the a
 277    /// </summary>
 278    /// <param name="fieldName">The name of the parent field to add. Supports dotted notation for nested fields.</param>
 279    /// <param name="subFields">Array of subfield names to add under this field. Each subfield will have a default Strin
 280    /// <param name="action">A delegate that receives a FieldBuilder instance for configuring additional nested fields w
 281    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 282    /// <exception cref="ArgumentNullException">Thrown when fieldName, subFields, or action is null.</exception>
 283    public FieldBuilder AddField(string fieldName, string[] subFields, Action<FieldBuilder> action)
 6284        => AddFieldCore(fieldName, Constants.ObjectFieldType, subFields: subFields, action: action);
 285
 286    /// <summary>
 287    /// Adds a field with predefined subfields, metadata, and a nested builder action for additional dynamic configurati
 288    /// The field will have an object type.
 289    /// </summary>
 290    /// <param name="fieldName">The name of the parent field to add. Supports dotted notation for nested fields.</param>
 291    /// <param name="subFields">Array of subfield names to add under this field. Each subfield will have a default Strin
 292    /// <param name="metadata">Optional metadata dictionary to associate with the parent field for custom processing.</p
 293    /// <param name="action">A delegate that receives a FieldBuilder instance for configuring additional nested fields w
 294    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 295    /// <exception cref="ArgumentNullException">Thrown when fieldName, subFields, or action is null.</exception>
 296    public FieldBuilder AddField(string fieldName, string[] subFields, Dictionary<string, object?>? metadata, Action<Fie
 3297        => AddFieldCore(fieldName, Constants.ObjectFieldType, subFields: subFields, metadata: metadata, action: action);
 298
 299    /// <summary>
 300    /// Adds a field with a specific type, predefined subfields, and a nested builder action for additional dynamic conf
 301    /// </summary>
 302    /// <param name="fieldName">The name of the parent field to add. Supports dotted notation for nested fields.</param>
 303    /// <param name="type">The GraphQL type of the parent field (typically an object type or array type).</param>
 304    /// <param name="subFields">Array of subfield names to add under this field. Each subfield will have a default Strin
 305    /// <param name="action">A delegate that receives a FieldBuilder instance for configuring additional nested fields w
 306    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 307    /// <exception cref="ArgumentNullException">Thrown when fieldName, type, subFields, or action is null.</exception>
 308    public FieldBuilder AddField(string fieldName, string type, string[] subFields, Action<FieldBuilder> action)
 6309        => AddFieldCore(fieldName, type, subFields: subFields, action: action);
 310
 311    /// <summary>
 312    /// Adds a field with a specific type, predefined subfields, metadata, and a nested builder action for additional dy
 313    /// </summary>
 314    /// <param name="fieldName">The name of the parent field to add. Supports dotted notation for nested fields.</param>
 315    /// <param name="type">The GraphQL type of the parent field (typically an object type or array type).</param>
 316    /// <param name="subFields">Array of subfield names to add under this field. Each subfield will have a default Strin
 317    /// <param name="metadata">Optional metadata dictionary to associate with the parent field for custom processing.</p
 318    /// <param name="action">A delegate that receives a FieldBuilder instance for configuring additional nested fields w
 319    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 320    /// <exception cref="ArgumentNullException">Thrown when fieldName, type, subFields, or action is null.</exception>
 321    public FieldBuilder AddField(string fieldName, string type, string[] subFields, Dictionary<string, object?>? metadat
 3322        => AddFieldCore(fieldName, type, subFields: subFields, metadata: metadata, action: action);
 323
 324    /// <summary>
 325    /// Adds a field with predefined subfields, GraphQL arguments, metadata, and a nested builder action for additional 
 326    /// The field will have an object type. This is the most comprehensive subFields + action overload.
 327    /// </summary>
 328    /// <param name="fieldName">The name of the parent field to add. Supports dotted notation for nested fields.</param>
 329    /// <param name="subFields">Array of subfield names to add under this field. Each subfield will have a default Strin
 330    /// <param name="arguments">GraphQL arguments for the parent field such as filters, pagination, or custom parameters
 331    /// <param name="metadata">Optional metadata dictionary to associate with the parent field for custom processing.</p
 332    /// <param name="action">A delegate that receives a FieldBuilder instance for configuring additional nested fields w
 333    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 334    /// <exception cref="ArgumentNullException">Thrown when fieldName, subFields, or action is null.</exception>
 335    public FieldBuilder AddField(string fieldName, string[] subFields, Dictionary<string, object?>? arguments, Dictionar
 3336        => AddFieldCore(fieldName, Constants.ObjectFieldType, arguments, subFields, metadata, action);
 337
 338    /// <summary>
 339    /// Args-first counterpart of <see cref="AddField(string, string[], Dictionary{string, object?}?, Dictionary{string,
 340    /// Mirrors the parameter order used by <see cref="QueryBuilder.AddField(string, Dictionary{string, object?}, string
 341    /// </summary>
 342    /// <param name="fieldName">The name of the parent field to add.</param>
 343    /// <param name="arguments">GraphQL arguments for the parent field.</param>
 344    /// <param name="subFields">Array of subfield names to add under this field.</param>
 345    /// <param name="metadata">Optional metadata dictionary to associate with the parent field.</param>
 346    /// <param name="action">A delegate that receives a FieldBuilder instance for configuring additional nested fields.<
 347    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 348    public FieldBuilder AddField(string fieldName, Dictionary<string, object?>? arguments, string[] subFields, Dictionar
 3349        => AddFieldCore(fieldName, Constants.ObjectFieldType, arguments, subFields, metadata, action);
 350
 351    /// <summary>
 352    /// Args-first variant of <see cref="AddField(string, string[], Dictionary{string, object?}?, Dictionary{string, obj
 353    /// without metadata. Convenience overload that passes <c>metadata: null</c>.
 354    /// </summary>
 355    /// <param name="fieldName">The name of the parent field to add.</param>
 356    /// <param name="arguments">GraphQL arguments for the parent field.</param>
 357    /// <param name="subFields">Array of subfield names to add under this field.</param>
 358    /// <param name="action">A delegate that receives a FieldBuilder instance for configuring additional nested fields.<
 359    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 360    public FieldBuilder AddField(string fieldName, Dictionary<string, object?>? arguments, string[] subFields, Action<Fi
 3361        => AddFieldCore(fieldName, Constants.ObjectFieldType, arguments, subFields, action: action);
 362
 363    /// <summary>
 364    /// Adds a field with a specific type, predefined subfields, GraphQL arguments, metadata, and a nested builder actio
 365    /// This is the ultimate comprehensive overload that supports all possible field configuration options, including bo
 366    /// </summary>
 367    /// <param name="fieldName">The name of the parent field to add. Supports dotted notation for nested fields.</param>
 368    /// <param name="type">The GraphQL type of the parent field (typically an object type or array type).</param>
 369    /// <param name="subFields">Array of subfield names to add under this field. Each subfield will have a default Strin
 370    /// <param name="arguments">GraphQL arguments for the parent field such as filters, pagination, or custom parameters
 371    /// <param name="metadata">Optional metadata dictionary to associate with the parent field for custom processing.</p
 372    /// <param name="action">A delegate that receives a FieldBuilder instance for configuring additional nested fields w
 373    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 374    /// <exception cref="ArgumentNullException">Thrown when fieldName, type, subFields, or action is null.</exception>
 375    /// <example>
 376    /// <code>
 377    /// builder.AddField("users", "[User]", new[] { "id", "email" },
 378    ///     new SortedDictionary&lt;string, object?&gt; { ["first"] = 10 },
 379    ///     new Dictionary&lt;string, object?&gt; { ["cached"] = true },
 380    ///     users => {
 381    ///         users.AddField("profile", "Profile", profile => {
 382    ///             profile.AddField("name");
 383    ///             profile.AddField("avatar", "String");
 384    ///         });
 385    ///     });
 386    /// </code>
 387    /// </example>
 388    public FieldBuilder AddField(string fieldName, string type, string[] subFields, Dictionary<string, object?>? argumen
 3389        => AddFieldCore(fieldName, type, arguments, subFields, metadata, action);
 390
 391    /// <summary>
 392    /// Adds a simple field with an optional type to the builder. This is the most basic overload for adding scalar fiel
 393    /// </summary>
 394    /// <param name="fieldName">The name of the field to add. Supports dotted notation for nested fields (e.g., "user.pr
 395    /// <param name="type">The GraphQL type of the field. Defaults to "String" if not specified. Common types include "S
 396    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 397    /// <exception cref="ArgumentNullException">Thrown when fieldName is null.</exception>
 398    /// <exception cref="ArgumentException">Thrown when fieldName is empty or whitespace.</exception>
 399    /// <example>
 400    /// <code>
 401    /// builder.AddField("name");                    // String field (default)
 402    /// builder.AddField("age", "Int");              // Integer field
 403    /// builder.AddField("isActive", "Boolean");     // Boolean field
 404    /// builder.AddField("user.profile.email");     // Nested field with dotted notation
 405    /// </code>
 406    /// </example>
 407    public FieldBuilder AddField(string fieldName, string type = Constants.DefaultFieldType)
 1869408        => AddFieldCore(fieldName, type);
 409
 410    /// <summary>
 411    /// Core method that handles all field addition logic - simplified and optimized
 412    /// </summary>
 413    /// <param name="fieldName">The name of the field.</param>
 414    /// <param name="type">The type of the field (optional).</param>
 415    /// <param name="arguments">The arguments for the field (optional).</param>
 416    /// <param name="subFields">The subfields for the field (optional).</param>
 417    /// <param name="metadata">The metadata for the field (optional).</param>
 418    /// <param name="action">The action to configure nested fields (optional).</param>
 419    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 420    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 421    private FieldBuilder AddFieldCore(string fieldName, string? type = null, Dictionary<string, object?>? arguments = nu
 422        string[]? subFields = null, Dictionary<string, object?>? metadata = null, Action<FieldBuilder>? action = null)
 423    {
 2709424        ValidateFieldNameSegments(fieldName.AsSpan());
 2706425        var fieldType = type ?? Constants.DefaultFieldType;
 2706426        var field = FieldFactory.GetOrAddField(_fieldDefinition, fieldName, fieldType, arguments, _fieldDefinition.Path,
 427
 428        // FAST PATH: Skip subFields processing if array is null or empty
 2706429        if (subFields?.Length > 0)
 430        {
 1098431            foreach (var subField in subFields)
 432            {
 381433                FieldFactory.GetOrAddField(field, subField, Constants.DefaultFieldType, null, field.Path);
 434            }
 435        }
 436
 437        // FAST PATH: Skip action processing if null
 2706438        if (action != null)
 439        {
 435440            var fieldBuilder = new FieldBuilder(field, this);
 435441            action(fieldBuilder);
 442            // GetOrAddField above already added `field` as a child of _fieldDefinition,
 443            // so _fieldDefinition._children is non-null at this point. Replace by REFERENCE (not
 444            // by Name) so a same-named/different-alias sibling already in _children is never
 445            // mistaken for `field`'s own slot.
 417446            _fieldDefinition._children!.ReplaceReference(field, fieldBuilder._fieldDefinition);
 447
 448            // The action may have mutated arguments anywhere in field's subtree (nested Where()/
 449            // AddField calls), which GetOrAddField above could not have anticipated. _fieldDefinition
 450            // is field's parent, so its memoized deep fingerprint — if any — is now stale too.
 417451            _fieldDefinition.ClearMergeMemo();
 452        }
 453
 454        // A captured builder can add argument-bearing fields long after its own action unwound
 455        // and a merge index fingerprinted the root. Only arguments (directly, or set inside the
 456        // action) change a fingerprint, so plain field additions skip the ancestor walk.
 2688457        if (action != null || arguments is { Count: > 0 })
 458        {
 594459            ClearAncestorMergeMemos();
 460        }
 461
 2688462        return this;
 463    }
 464
 465    /// <summary>
 466    /// Creates a new FieldBuilder instance for the specified field with type, arguments, and metadata.
 467    /// </summary>
 468    /// <param name="fieldDefinitions">The collection of existing field definitions.</param>
 469    /// <param name="fieldName">The name of the field to create.</param>
 470    /// <param name="type">The type of the field.</param>
 471    /// <param name="arguments">The arguments for the field.</param>
 472    /// <param name="metadata">The metadata for the field.</param>
 473    /// <returns>A new FieldBuilder instance.</returns>
 474    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 475    public static FieldBuilder Create(Dictionary<string, FieldDefinition> fieldDefinitions, string fieldName, string typ
 531476        => Create(fieldDefinitions, fieldName, type, arguments, metadata, variableSink: null);
 477
 478    /// <summary>
 479    /// Internal counterpart of <see cref="Create(Dictionary{string, FieldDefinition}, string, string, IDictionary{strin
 480    /// that additionally threads the owning operation's variable set down into the returned
 481    /// builder (and every nested <c>Action&lt;FieldBuilder&gt;</c> scope built from it), so
 482    /// <see cref="IncludeIf(Variable)"/>/<see cref="SkipIf(Variable)"/> called anywhere in the
 483    /// resulting subtree can promote their variable into the operation signature. Used by
 484    /// <see cref="QueryBuilder"/>, which owns the <see cref="Abstractions.QueryDefinition.Variables"/>
 485    /// set this flows from.
 486    /// </summary>
 487    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 488    internal static FieldBuilder Create(Dictionary<string, FieldDefinition> fieldDefinitions, string fieldName, string t
 489    {
 490        // Empty/whitespace dotted segments are collapsed by FieldFactory.GetOrAddField below, the
 491        // same shared routine the instance AddField overloads use — so this factory no longer
 492        // bypasses that handling. Full per-segment identifier validation is intentionally NOT done
 493        // here: Create legitimately receives complex paths (type prefixes, alias:name, array
 494        // indices like results[0]) that the complex-path parser resolves.
 495        // FAIL-FAST: Use empty arguments if null or empty
 1668496        var argumentsToUse = arguments is { Count: > 0 } ? arguments : null;
 497
 498        // Use FieldFactory for field creation
 1668499        var field = FieldFactory.GetOrAddField(fieldDefinitions, fieldName, type, argumentsToUse, null, metadata);
 500
 1665501        return new FieldBuilder(field, CreateAncestorChain(fieldDefinitions, field, variableSink), variableSink);
 502    }
 503
 504    /// <summary>
 505    /// Creates a new FieldBuilder instance from an existing field definition.
 506    /// </summary>
 507    /// <param name="fieldDefinition">The field definition to create the builder from.</param>
 508    /// <returns>A new FieldBuilder instance.</returns>
 3509    public static FieldBuilder Create(FieldDefinition fieldDefinition) => new(fieldDefinition);
 510
 511    // A dotted or complex path resolves to a nested field. Its builder can be captured and mutated
 512    // after a merge index has fingerprinted the root, so it needs the same ancestor chain a nested
 513    // Action<FieldBuilder> scope gets: Where()/IncludeIf()/SkipIf() clear every ancestor's memo and
 514    // reach the root's tracker through it. Root-level fields return null and keep the direct path.
 515    private static FieldBuilder? CreateAncestorChain(Dictionary<string, FieldDefinition> fieldDefinitions, FieldDefiniti
 516    {
 1665517        if (fieldDefinitions.TryGetValue(field._effectiveName, out var rootCandidate) && ReferenceEquals(rootCandidate, 
 518        {
 1545519            return null;
 520        }
 521
 522        // The leaf's Path names its ancestors, so one lookup per segment normally finds them.
 523        // Aliased or typed segments may not resolve by name; the reference search covers those.
 120524        var ancestors = new List<FieldDefinition>(4);
 120525        if (!TryWalkPath(fieldDefinitions, field, ancestors))
 526        {
 15527            ancestors.Clear();
 15528            var found = false;
 72529            foreach (var root in fieldDefinitions.Values)
 530            {
 24531                found = TryFindAncestors(root, field, ancestors);
 30532                if (found) break;
 533            }
 534
 24535            if (!found) return null;
 536        }
 537
 111538        FieldBuilder? parent = null;
 570539        foreach (var ancestor in ancestors)
 540        {
 174541            parent = new FieldBuilder(ancestor, parent, variableSink);
 542        }
 543
 111544        return parent;
 545    }
 546
 547    private static bool TryWalkPath(Dictionary<string, FieldDefinition> fieldDefinitions, FieldDefinition target, List<F
 548    {
 120549        var remaining = target.Path.AsSpan();
 120550        var dot = remaining.IndexOf('.');
 129551        if (dot <= 0 || !fieldDefinitions.TryGetValue(remaining[..dot].ToString(), out var current)) return false;
 552
 60553        while (true)
 554        {
 171555            ancestors.Add(current);
 171556            remaining = remaining[(dot + 1)..];
 171557            dot = remaining.IndexOf('.');
 171558            var segment = dot < 0 ? remaining : remaining[..dot];
 177559            if (current._children is null || !current._children.TryGetValue(segment, out var child)) return false;
 270560            if (dot < 0) return ReferenceEquals(child, target);
 60561            current = child;
 562        }
 563    }
 564
 565    private static bool TryFindAncestors(FieldDefinition current, FieldDefinition target, List<FieldDefinition> ancestor
 566    {
 60567        if (current._children is not { Count: > 0 } children) return false;
 568
 24569        ancestors.Add(current);
 87570        foreach (var child in children.AsSpan())
 571        {
 33572            if (ReferenceEquals(child, target) || TryFindAncestors(child, target, ancestors)) return true;
 573        }
 574
 15575        ancestors.RemoveAt(ancestors.Count - 1);
 15576        return false;
 577    }
 578
 579    /// <summary>
 580    /// Builds and returns the final field definition.
 581    /// </summary>
 582    /// <returns>The constructed FieldDefinition.</returns>
 516583    public FieldDefinition Build() => _fieldDefinition;
 584
 585    /// <summary>
 586    /// Recursively creates and merges field definitions into the target field collection.
 587    /// This method handles field merging, argument consolidation, and nested field creation.
 588    /// </summary>
 589    /// <param name="fields">Target field collection</param>
 590    /// <param name="fieldDefinition">Field definition to create/merge</param>
 591    private static void RecursiveCreateField(Dictionary<string, FieldDefinition> fields, FieldDefinition fieldDefinition
 592    {
 276593        var parentField = FieldFactory.CreateOrMergeField(fields, fieldDefinition);
 594
 595        // Carry over the incoming field's inline fragments, spreads, and directives (deep-cloned)
 596        // — CreateOrMergeField only copies name/type/alias/arguments/metadata, so without this the
 597        // merged field would render without its fragment state.
 276598        FieldDefinitionExtensions.MergeFragmentState(parentField, fieldDefinition);
 599
 600        // Recursively process all child fields using direct span iteration (zero-alloc)
 276601        if (fieldDefinition._children != null)
 602        {
 210603            var childrenSpan = fieldDefinition._children.AsSpan();
 210604            parentField._children ??= new FieldChildren();
 210605            var parentChildren = parentField._children;
 846606            for (int i = 0; i < childrenSpan.Length; i++)
 607            {
 213608                RecursiveCreateField(parentChildren, childrenSpan[i]);
 609            }
 610
 611            // Any descendant merge above may have changed the merge-relevant subtree beneath
 612            // parentField — its own memoized fingerprint (if any, from a prior FindMergeTarget
 613            // call) is now stale.
 210614            parentField.ClearMergeMemo();
 615        }
 276616    }
 617
 618    private static void RecursiveCreateField(FieldChildren children, FieldDefinition fieldDefinition)
 619    {
 411620        var parentField = FieldFactory.CreateOrMergeField(children, fieldDefinition);
 621
 411622        FieldDefinitionExtensions.MergeFragmentState(parentField, fieldDefinition);
 623
 624        // Recursively process all child fields using direct span iteration (zero-alloc)
 411625        if (fieldDefinition._children != null)
 626        {
 99627            var childrenSpan = fieldDefinition._children.AsSpan();
 99628            parentField._children ??= new FieldChildren();
 99629            var parentChildren = parentField._children;
 492630            for (int i = 0; i < childrenSpan.Length; i++)
 631            {
 147632                RecursiveCreateField(parentChildren, childrenSpan[i]);
 633            }
 634
 635            // Any descendant merge above may have changed the merge-relevant subtree beneath
 636            // parentField — its own memoized fingerprint (if any, from a prior FindMergeTarget
 637            // call) is now stale.
 99638            parentField.ClearMergeMemo();
 639        }
 411640    }
 641
 642    internal static void Include(Dictionary<string, FieldDefinition> fields, FieldDefinition fieldDefinition)
 276643        => RecursiveCreateField(fields, fieldDefinition);
 644
 645    internal static void Include(FieldChildren children, FieldDefinition fieldDefinition)
 51646        => RecursiveCreateField(children, fieldDefinition);
 647
 648    // Shared with QueryBuilder's sub-field overloads, which add children without exposing a
 649    // builder and so need neither a FieldBuilder nor the merge tracker its constructor creates.
 650    internal static void AddSubField(FieldDefinition parent, FieldDefinition fieldDefinition)
 651    {
 1071652        ArgumentNullException.ThrowIfNull(fieldDefinition);
 653        // Dotted names are valid here — they are expanded into nested fields by FieldFactory,
 654        // matching the string-overload behavior. Validate each segment individually.
 1071655        ValidateFieldNameSegments(fieldDefinition.Name.AsSpan());
 656
 657        // FieldDefinition._type is always set non-null by every constructor path.
 1068658        FieldFactory.GetOrAddField(parent, fieldDefinition.Name, fieldDefinition._type!, fieldDefinition._arguments, par
 1068659    }
 660
 661    private static void ValidateFieldNameSegments(ReadOnlySpan<char> fieldName)
 662    {
 3780663        if (fieldName.IsEmpty)
 3664            throw new ArgumentException("Field name cannot be empty.", nameof(fieldName));
 665
 666        // Validate each dot-separated segment individually. Empty/whitespace segments (e.g. from
 667        // "user..name" or "user. .name") are skipped — they are collapsed by FieldFactory rather
 668        // than rejected, so validation must agree with that skip contract.
 8979669        while (!fieldName.IsEmpty)
 670        {
 5205671            var dotIndex = fieldName.IndexOf('.');
 5205672            var segment = dotIndex >= 0 ? fieldName[..dotIndex] : fieldName;
 5205673            if (!segment.IsWhiteSpace())
 5181674                Helpers.ValidateFieldName(segment);
 5202675            fieldName = dotIndex >= 0 ? fieldName[(dotIndex + 1)..] : ReadOnlySpan<char>.Empty;
 676        }
 3774677    }
 678
 679    /// <summary>
 680    /// Sets an alias for the current field.
 681    /// </summary>
 682    /// <param name="alias">The alias to set for the field.</param>
 683    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 684    public FieldBuilder WithAlias(string alias)
 685    {
 12686        _fieldDefinition = _fieldDefinition with { Alias = alias };
 687
 12688        return this;
 689    }
 690
 691    /// <summary>
 692    /// Sets the type for the current field.
 693    /// </summary>
 694    /// <param name="type">The type to set for the field.</param>
 695    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 696    public FieldBuilder WithType(string type)
 697    {
 6698        _fieldDefinition = _fieldDefinition with { Type = type };
 699
 6700        return this;
 701    }
 702
 703    /// <summary>
 704    /// Sets or merges metadata for the current field.
 705    /// </summary>
 706    /// <param name="metadata">The metadata to set or merge with existing metadata.</param>
 707    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 708    public FieldBuilder WithMetadata(Dictionary<string, object> metadata)
 709    {
 51710        var mergedMetadata = Helpers.MergeMetadata(_fieldDefinition._metadata, metadata);
 711
 51712        _fieldDefinition.Metadata = mergedMetadata;
 713
 51714        return this;
 715    }
 716
 717    /// <summary>
 718    /// Adds or updates an argument for the current field.
 719    /// </summary>
 720    /// <param name="key">The argument key.</param>
 721    /// <param name="value">The argument value.</param>
 722    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 723    public FieldBuilder Where(string key, object? value)
 724    {
 725        // Ensure that _arguments exist (lazy initialization)
 81726        if (_fieldDefinition._arguments is null)
 727        {
 24728            _fieldDefinition = _fieldDefinition with
 24729            {
 24730                _arguments = new(StringComparer.OrdinalIgnoreCase),
 24731                _deepArgumentFingerprint = null,
 24732                _subtreeHasAnyArguments = null,
 24733            };
 734        }
 735
 736        // Determine the final value: merge dictionaries if both are dictionaries, otherwise set/override
 81737        _fieldDefinition._arguments[key] = _fieldDefinition._arguments.TryGetValue(key, out var existingValue)
 81738            ? (existingValue, value) switch
 81739            {
 81740                // Both values are dictionaries - merge them recursively
 81741                (IDictionary<string, object?> existingDict, IDictionary<string, object?> newDict)
 6742                    => Helpers.MergeNullableDictionaries(existingDict, newDict),
 81743
 81744                // Not dictionaries or mixed types - override with new value
 48745                _ => value
 81746            }
 81747            : value; // Key doesn't exist - set the value
 748
 749        // Mutating arguments in place (above) invalidates any memoized fingerprint on this field —
 750        // including the case where _arguments already existed and only its contents changed.
 81751        _fieldDefinition._deepArgumentFingerprint = null;
 81752        _fieldDefinition._subtreeHasAnyArguments = null;
 753
 754        // This field's own memo is cleared above, but a captured, since-detached FieldBuilder (the
 755        // Action<FieldBuilder> that produced it already returned) means AddFieldCore's own cascading
 756        // ClearMergeMemo calls — one per nesting level, run as each action unwinds — happened BEFORE
 757        // this Where() call and cannot see it. Every ancestor in this builder's nesting chain still
 758        // has this field in its merge-relevant subtree, so their memoized deep fingerprints are
 759        // equally stale and must be cleared explicitly here.
 81760        ClearAncestorMergeMemos();
 761
 81762        _fieldDefinition.InvalidateMergeIndex();
 763
 81764        return this;
 765    }
 766
 767    /// <summary>
 768    /// Adds an inline GraphQL fragment narrowing the current field's selection set to a
 769    /// concrete type. Renders as <c>... on TypeName { … }</c>. Use when the parent field's
 770    /// schema return type is a union or interface and you need fields that only exist on
 771    /// a specific implementation.
 772    /// </summary>
 773    /// <param name="typeName">The concrete GraphQL type to narrow to (e.g. <c>"Repository"</c>).
 774    /// Case-sensitive — emitted verbatim into the rendered GraphQL.</param>
 775    /// <param name="action">Builder action for the fragment's selection set. The builder it
 776    /// receives writes into the fragment, not the parent field.</param>
 777    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 778    /// <remarks>
 779    /// Multiple <c>OnType("Repository", …)</c> calls on the same parent merge into one
 780    /// fragment definition (idempotent registration, then the lambda extends the existing
 781    /// selection set). This matches the GraphQL semantics where two adjacent inline fragments
 782    /// for the same type are equivalent to one combined fragment.
 783    /// </remarks>
 784    /// <exception cref="ArgumentException">Thrown when <paramref name="typeName"/> is null or whitespace.</exception>
 785    /// <exception cref="ArgumentNullException">Thrown when <paramref name="action"/> is null.</exception>
 786    public FieldBuilder OnType(string typeName, Action<FieldBuilder> action)
 787    {
 213788        if (string.IsNullOrWhiteSpace(typeName))
 789        {
 9790            throw new ArgumentException("Inline fragment type name cannot be null or whitespace.", nameof(typeName));
 791        }
 204792        ArgumentNullException.ThrowIfNull(action);
 793
 201794        var fragment = _fieldDefinition.GetOrAddInlineFragment(typeName);
 201795        PopulateFragmentSurface($"__inline_fragment_{typeName}", fragment.GetOrCreateFieldsStore(),
 201796            ref fragment._fragments, ref fragment._spreadFragments, ref fragment._directives, action, _variableSink);
 797
 201798        return this;
 799    }
 800
 801    /// <summary>
 802    /// Runs a user selection-set lambda against a synthetic field whose stores alias
 803    /// <paramref name="fieldsStore"/> and the given fragment maps, then writes the lambda's
 804    /// final maps back through the ref parameters. Shared by <see cref="OnType"/> and
 805    /// <see cref="QueryBuilder.AddFragment"/> so the aliasing rules live in one place.
 806    /// </summary>
 807    /// <remarks>
 808    /// The final state is read from the builder's CURRENT field definition, not the original
 809    /// surface: fluent calls like <see cref="Where"/>/<see cref="WithAlias"/>/<see cref="WithType"/>
 810    /// replace the definition with a <c>with</c> record copy, so maps first created after such
 811    /// a call exist only on the copy. The <c>_children</c> store needs no reflect-back — it is
 812    /// created non-null up front and `with` copies carry the same reference.
 813    /// <para>
 814    /// <paramref name="directives"/> writes back <c>IncludeIf</c>/<c>SkipIf</c>/<c>Directive</c>
 815    /// calls made directly on the lambda's top-level builder into the caller's own directive store
 816    /// — <see cref="OnType"/> passes its <see cref="InlineFragmentDefinition"/>'s <c>_directives</c>
 817    /// so directives on an inline fragment (<c>... on Admin @include(if:$a){ … }</c>) survive.
 818    /// <see cref="QueryBuilder.AddFragment"/> passes a throwaway local: named-fragment-level
 819    /// directives are out of scope until fragment-spread directives ship (tracked alongside issue
 820    /// #20), so a call there is a silent no-op today, unchanged from before this parameter existed.
 821    /// </para>
 822    /// </remarks>
 823    internal static void PopulateFragmentSurface(
 824        string surfaceName,
 825        FieldChildren fieldsStore,
 826        ref Dictionary<string, InlineFragmentDefinition>? fragments,
 827        ref List<string>? spreadFragments,
 828        ref List<FieldDirective>? directives,
 829        Action<FieldBuilder> action,
 830        SortedSet<Variable>? variableSink = null)
 831    {
 372832        var surface = new FieldDefinition(surfaceName, Constants.DefaultFieldType)
 372833        {
 372834            _children = fieldsStore,
 372835            _fragments = fragments,
 372836            _spreadFragments = spreadFragments,
 372837            _directives = directives,
 372838        };
 839
 372840        var builder = new FieldBuilder(surface, variableSink: variableSink);
 372841        action(builder);
 842
 372843        var final = builder._fieldDefinition;
 372844        fragments = final._fragments;
 372845        spreadFragments = final._spreadFragments;
 372846        directives = final._directives;
 372847    }
 848
 849    /// <summary>
 850    /// Spreads a named fragment into this field's selection set. The renderer emits
 851    /// <c>...Name</c> after plain fields and inline fragments. The fragment itself is declared
 852    /// at the operation's top level via <c>QueryBuilder.AddFragment(name, onType, build)</c>;
 853    /// this method only records the spread reference.
 854    /// </summary>
 855    /// <param name="name">The name of the fragment to spread. NGql does not validate that the
 856    /// fragment is actually declared — an undeclared spread renders verbatim and the server
 857    /// rejects it. NGql is schemaless.</param>
 858    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 859    /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is null or whitespace.</exception>
 860    public FieldBuilder SpreadFragment(string name)
 861    {
 171862        if (string.IsNullOrWhiteSpace(name))
 863        {
 9864            throw new ArgumentException("Spread fragment name cannot be null or whitespace.", nameof(name));
 865        }
 866
 162867        _fieldDefinition.AddSpreadFragment(name);
 162868        return this;
 869    }
 870
 871    /// <summary>
 872    /// Attaches an <c>@include(if:$var)</c> directive to the current field. The field is kept in
 873    /// the response only when the runtime value of <paramref name="ifVariable"/> is <c>true</c>.
 874    /// </summary>
 875    /// <param name="ifVariable">The variable name, with or without the leading <c>$</c> — both
 876    /// <c>"$show"</c> and <c>"show"</c> render as <c>if:$show</c>.</param>
 877    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 878    /// <exception cref="ArgumentException">Thrown when <paramref name="ifVariable"/> is null or whitespace.</exception>
 879    /// <remarks>
 880    /// <b>This overload does NOT promote a variable declaration into the operation signature</b> —
 881    /// a bare <see cref="string"/> carries no GraphQL type, so there is nothing to promote. Use it
 882    /// only when the variable is already declared some other way (e.g. via
 883    /// <c>QueryBuilder.Definition.Variables.Add(...)</c>, or because another field argument already
 884    /// promoted a <see cref="Variable"/> with this same name). If you want the variable declared
 885    /// for you, use <see cref="IncludeIf(Variable)"/> instead — it both renders the directive and
 886    /// promotes the variable into <c>query Name($var:Type!){ … }</c>. Calling this overload with a
 887    /// variable that is never declared elsewhere renders a document a GraphQL server will reject.
 888    /// </remarks>
 889    public FieldBuilder IncludeIf(string ifVariable)
 84890        => AddIfDirective("include", ifVariable);
 891
 892    /// <summary>
 893    /// Attaches an <c>@include(if:$var)</c> directive to the current field and promotes
 894    /// <paramref name="condition"/> into the operation's variable signature (deduplicated with any
 895    /// other variable of the same name already declared), exactly as a <see cref="Variable"/>
 896    /// passed as a field argument does today. The field is kept in the response only when the
 897    /// runtime value of the variable is <c>true</c>.
 898    /// </summary>
 899    /// <param name="condition">The Boolean variable that gates inclusion. <see cref="Variable.Type"/>
 900    /// must reduce to <c>Boolean!</c> or <c>Boolean</c> — the GraphQL spec requires the <c>if</c>
 901    /// argument of <c>@include</c> to be a Boolean expression.</param>
 902    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 903    /// <exception cref="ArgumentException">Thrown when <paramref name="condition"/>'s type is not
 904    /// <c>Boolean</c> or <c>Boolean!</c>.</exception>
 905    public FieldBuilder IncludeIf(Variable condition)
 99906        => AddIfDirective("include", condition);
 907
 908    /// <summary>
 909    /// Attaches a <c>@skip(if:$var)</c> directive to the current field. The field is omitted from
 910    /// the response when the runtime value of <paramref name="ifVariable"/> is <c>true</c>.
 911    /// </summary>
 912    /// <param name="ifVariable">The variable name, with or without the leading <c>$</c> — both
 913    /// <c>"$hide"</c> and <c>"hide"</c> render as <c>if:$hide</c>.</param>
 914    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 915    /// <exception cref="ArgumentException">Thrown when <paramref name="ifVariable"/> is null or whitespace.</exception>
 916    /// <remarks>
 917    /// <b>This overload does NOT promote a variable declaration into the operation signature</b> —
 918    /// a bare <see cref="string"/> carries no GraphQL type, so there is nothing to promote. Use it
 919    /// only when the variable is already declared some other way (e.g. via
 920    /// <c>QueryBuilder.Definition.Variables.Add(...)</c>, or because another field argument already
 921    /// promoted a <see cref="Variable"/> with this same name). If you want the variable declared
 922    /// for you, use <see cref="SkipIf(Variable)"/> instead — it both renders the directive and
 923    /// promotes the variable into <c>query Name($var:Type!){ … }</c>. Calling this overload with a
 924    /// variable that is never declared elsewhere renders a document a GraphQL server will reject.
 925    /// </remarks>
 926    public FieldBuilder SkipIf(string ifVariable)
 21927        => AddIfDirective("skip", ifVariable);
 928
 929    /// <summary>
 930    /// Attaches a <c>@skip(if:$var)</c> directive to the current field and promotes
 931    /// <paramref name="condition"/> into the operation's variable signature (deduplicated with any
 932    /// other variable of the same name already declared), exactly as a <see cref="Variable"/>
 933    /// passed as a field argument does today. The field is omitted from the response when the
 934    /// runtime value of the variable is <c>true</c>.
 935    /// </summary>
 936    /// <param name="condition">The Boolean variable that gates the skip. <see cref="Variable.Type"/>
 937    /// must reduce to <c>Boolean!</c> or <c>Boolean</c> — the GraphQL spec requires the <c>if</c>
 938    /// argument of <c>@skip</c> to be a Boolean expression.</param>
 939    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 940    /// <exception cref="ArgumentException">Thrown when <paramref name="condition"/>'s type is not
 941    /// <c>Boolean</c> or <c>Boolean!</c>.</exception>
 942    public FieldBuilder SkipIf(Variable condition)
 36943        => AddIfDirective("skip", condition);
 944
 945    /// <summary>
 946    /// Attaches an arbitrary directive to the current field, rendered as <c>@name</c> optionally
 947    /// followed by <c>(argName:value, …)</c>. Use for custom or server-defined directives such as
 948    /// <c>@deprecated</c> or <c>@format(as:"ISO8601")</c>.
 949    /// </summary>
 950    /// <param name="name">The directive name, with or without the leading <c>@</c> — both
 951    /// <c>"@format"</c> and <c>"format"</c> render as <c>@format</c>.</param>
 952    /// <param name="arguments">Optional directive arguments; <c>null</c> or empty for a
 953    /// no-argument directive like <c>@deprecated</c>. Any <see cref="Variable"/> values found in
 954    /// this dictionary (including nested inside dictionaries/lists/objects) are promoted into the
 955    /// operation's variable signature, exactly like field arguments.</param>
 956    /// <returns>The current FieldBuilder instance for method chaining.</returns>
 957    /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is null or whitespace.</exception>
 958    public FieldBuilder Directive(string name, Dictionary<string, object?>? arguments = null)
 959    {
 66960        if (string.IsNullOrWhiteSpace(name))
 961        {
 9962            throw new ArgumentException("Directive name cannot be null or whitespace.", nameof(name));
 963        }
 964
 965        // Strip ALL leading '@' (not just one) so pathological input like "@@foo" still yields a
 966        // single-'@' directive on render. A name that is only '@' characters leaves nothing behind.
 57967        var normalizedName = name.TrimStart('@');
 57968        if (normalizedName.Length == 0)
 969        {
 15970            throw new ArgumentException("Directive name cannot consist only of '@' characters.", nameof(name));
 971        }
 972
 42973        if (arguments?.Count > 0 && _variableSink is not null)
 974        {
 27975            Helpers.ExtractVariablesFromValue(arguments, _variableSink);
 976        }
 977
 42978        _fieldDefinition.AddDirective(new FieldDirective(normalizedName, arguments));
 39979        InvalidateMergeMemoIfConditional(normalizedName);
 39980        return this;
 981    }
 982
 983    // Shared helper for the bare-string @include / @skip overloads — both take a single `if:$var`
 984    // argument. The variable is normalized so callers may pass the name with or without the
 985    // leading `$`; it is always rendered with a single `$`. Does NOT promote — see the string
 986    // overloads' XML doc remarks for why.
 987    private FieldBuilder AddIfDirective(string directiveName, string ifVariable)
 988    {
 105989        if (string.IsNullOrWhiteSpace(ifVariable))
 990        {
 12991            throw new ArgumentException("Directive variable cannot be null or whitespace.", nameof(ifVariable));
 992        }
 993
 994        // Collapse ANY number of leading '$' to exactly one so input like "$$show" renders a
 995        // single-'$' variable. A value that is only '$' characters carries no variable name.
 93996        var bareName = ifVariable.TrimStart('$');
 93997        if (bareName.Length == 0)
 998        {
 15999            throw new ArgumentException("Directive variable must contain a name after the '$'.", nameof(ifVariable));
 1000        }
 781001        var name = "$" + bareName;
 781002        var arguments = new Dictionary<string, object?>(StringComparer.Ordinal)
 781003        {
 781004            ["if"] = new Variable(name, "Boolean!"),
 781005        };
 781006        _fieldDefinition.AddDirective(new FieldDirective(directiveName, arguments));
 781007        InvalidateMergeMemoIfConditional(directiveName);
 781008        return this;
 1009    }
 1010
 1011    // Shared helper for the Variable-typed IncludeIf/SkipIf overloads — validates that the
 1012    // variable's declared type reduces to a Boolean per the GraphQL spec's requirement for the
 1013    // `if` argument of @include/@skip, attaches the directive, and — unlike AddIfDirective(string,
 1014    // string) — promotes the variable into the owning operation's signature via the same
 1015    // Helpers.ExtractVariablesFromValue path field-argument Variables already use.
 1016    private FieldBuilder AddIfDirective(string directiveName, Variable condition)
 1017    {
 1351018        if (!IsBooleanType(condition.Type))
 1019        {
 91020            throw new ArgumentException(
 91021                $"Variable '{condition.Name}' passed to @{directiveName}'s condition must have type 'Boolean' or 'Boolea
 91022                nameof(condition));
 1023        }
 1024
 1261025        var arguments = new Dictionary<string, object?>(StringComparer.Ordinal)
 1261026        {
 1261027            ["if"] = condition,
 1261028        };
 1261029        _fieldDefinition.AddDirective(new FieldDirective(directiveName, arguments));
 1261030        InvalidateMergeMemoIfConditional(directiveName);
 1031
 1032        // Same promotion path field-argument Variables already use (Helpers.ExtractVariablesFromValue
 1033        // adds the Variable to the set, deduping via Variable's value equality) rather than a
 1034        // parallel mechanism.
 1261035        if (_variableSink is not null)
 1036        {
 1261037            Helpers.ExtractVariablesFromValue(condition, _variableSink);
 1038        }
 1039
 1261040        return this;
 1041    }
 1042
 1043    private static bool IsBooleanType(string type)
 1351044        => type.Equals("Boolean", StringComparison.Ordinal) || type.Equals("Boolean!", StringComparison.Ordinal);
 1045
 1046    private void InvalidateMergeMemoIfConditional(string directiveName)
 1047    {
 2431048        if (!directiveName.Equals("include", StringComparison.Ordinal)
 2431049            && !directiveName.Equals("skip", StringComparison.Ordinal))
 1050        {
 331051            return;
 1052        }
 1053
 2101054        _fieldDefinition._deepArgumentFingerprint = null;
 2101055        _fieldDefinition._subtreeHasAnyArguments = null;
 2101056        ClearAncestorMergeMemos();
 2101057        _fieldDefinition.InvalidateMergeIndex();
 2101058    }
 1059}

Methods/Properties

.ctor(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Builders.FieldBuilder,System.Collections.Generic.SortedSet`1<NGql.Core.Variable>)
ClearAncestorMergeMemos()
AddField(NGql.Core.Abstractions.FieldDefinition)
AddField(System.String,System.String[],System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
AddField(System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.String[],System.Collections.Generic.Dictionary`2<System.String,System.Object>)
AddField(System.String,System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
AddField(System.String,System.String,System.String[],System.Collections.Generic.Dictionary`2<System.String,System.Object>)
AddField(System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
AddField(System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
AddField(System.String,System.String,System.String[],System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
AddField(System.String,System.Action`1<NGql.Core.Builders.FieldBuilder>)
AddField(System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Action`1<NGql.Core.Builders.FieldBuilder>)
AddField(System.String,System.String,System.Action`1<NGql.Core.Builders.FieldBuilder>)
AddField(System.String,System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Action`1<NGql.Core.Builders.FieldBuilder>)
AddField(System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Action`1<NGql.Core.Builders.FieldBuilder>)
AddField(System.String,System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Action`1<NGql.Core.Builders.FieldBuilder>)
AddField(System.String,System.String[],System.Action`1<NGql.Core.Builders.FieldBuilder>)
AddField(System.String,System.String[],System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Action`1<NGql.Core.Builders.FieldBuilder>)
AddField(System.String,System.String,System.String[],System.Action`1<NGql.Core.Builders.FieldBuilder>)
AddField(System.String,System.String,System.String[],System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Action`1<NGql.Core.Builders.FieldBuilder>)
AddField(System.String,System.String[],System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Action`1<NGql.Core.Builders.FieldBuilder>)
AddField(System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.String[],System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Action`1<NGql.Core.Builders.FieldBuilder>)
AddField(System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.String[],System.Action`1<NGql.Core.Builders.FieldBuilder>)
AddField(System.String,System.String,System.String[],System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Action`1<NGql.Core.Builders.FieldBuilder>)
AddField(System.String,System.String)
AddFieldCore(System.String,System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.String[],System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Action`1<NGql.Core.Builders.FieldBuilder>)
Create(System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.FieldDefinition>,System.String,System.String,System.Collections.Generic.IDictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
Create(System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.FieldDefinition>,System.String,System.String,System.Collections.Generic.IDictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.SortedSet`1<NGql.Core.Variable>)
Create(NGql.Core.Abstractions.FieldDefinition)
CreateAncestorChain(System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.FieldDefinition>,NGql.Core.Abstractions.FieldDefinition,System.Collections.Generic.SortedSet`1<NGql.Core.Variable>)
TryWalkPath(System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.FieldDefinition>,NGql.Core.Abstractions.FieldDefinition,System.Collections.Generic.List`1<NGql.Core.Abstractions.FieldDefinition>)
TryFindAncestors(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldDefinition,System.Collections.Generic.List`1<NGql.Core.Abstractions.FieldDefinition>)
Build()
RecursiveCreateField(System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.FieldDefinition>,NGql.Core.Abstractions.FieldDefinition)
RecursiveCreateField(NGql.Core.Abstractions.FieldChildren,NGql.Core.Abstractions.FieldDefinition)
Include(System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.FieldDefinition>,NGql.Core.Abstractions.FieldDefinition)
Include(NGql.Core.Abstractions.FieldChildren,NGql.Core.Abstractions.FieldDefinition)
AddSubField(NGql.Core.Abstractions.FieldDefinition,NGql.Core.Abstractions.FieldDefinition)
ValidateFieldNameSegments(System.ReadOnlySpan`1<System.Char>)
WithAlias(System.String)
WithType(System.String)
WithMetadata(System.Collections.Generic.Dictionary`2<System.String,System.Object>)
Where(System.String,System.Object)
OnType(System.String,System.Action`1<NGql.Core.Builders.FieldBuilder>)
PopulateFragmentSurface(System.String,NGql.Core.Abstractions.FieldChildren,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.Action`1<NGql.Core.Builders.FieldBuilder>,System.Collections.Generic.SortedSet`1<NGql.Core.Variable>)
SpreadFragment(System.String)
IncludeIf(System.String)
IncludeIf(NGql.Core.Variable)
SkipIf(System.String)
SkipIf(NGql.Core.Variable)
Directive(System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
AddIfDirective(System.String,System.String)
AddIfDirective(System.String,NGql.Core.Variable)
IsBooleanType(System.String)
InvalidateMergeMemoIfConditional(System.String)