< Summary

Information
Class: NGql.Core.Builders.QueryBuilder
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Builders/QueryBuilder.cs
Line coverage
100%
Covered lines: 145
Uncovered lines: 0
Coverable lines: 145
Total lines: 625
Line coverage: 100%
Branch coverage
100%
Covered branches: 68
Total branches: 68
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_Definition()100%11100%
get_Variables()100%11100%
get_QueryMapInstance()100%22100%
.ctor(...)100%11100%
CreateDefaultBuilder(...)100%11100%
CreateDefaultBuilder(...)100%11100%
CreateMutationBuilder(...)100%11100%
CreateMutationBuilder(...)100%11100%
CreateSubscriptionBuilder(...)100%11100%
CreateSubscriptionBuilder(...)100%11100%
WithMergingStrategy(...)100%11100%
CreateFromDefinition(...)100%11100%
AddField(...)100%88100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%44100%
AddField(...)100%44100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%44100%
AddField(...)100%11100%
AddField(...)100%11100%
AddField(...)100%22100%
AddFragment(...)100%44100%
AddFieldFastPath(...)100%66100%
AddFieldBuilderCore(...)100%66100%
Include(...)100%11100%
AddFieldCore(...)100%2020100%
ToFieldDefinitions(...)100%44100%
IncludeImpl(...)100%11100%
InvalidateLookupCaches()100%22100%
WithMetadata(...)100%11100%
GetPathTo(...)100%22100%
get_DefinitionsCount()100%11100%
ToString()100%11100%
AppendTo(...)100%11100%
WriteTo(...)100%11100%
WriteUtf8(...)100%11100%
op_Implicit(...)100%11100%

File(s)

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

#LineLine coverage
 1using System.Buffers;
 2using System.Runtime.CompilerServices;
 3using System.Text;
 4using NGql.Core.Abstractions;
 5using NGql.Core.Extensions;
 6using NGql.Core.Features;
 7
 8namespace NGql.Core.Builders;
 9
 10/// <summary>
 11///     Represents a query builder.
 12/// </summary>
 13public sealed class QueryBuilder
 14{
 15    /// <summary>
 16    ///    The query definition that this builder is working with.
 17    /// </summary>
 14548218    public QueryDefinition Definition => _definition;
 19
 20    /// <inheritdoc cref="QueryBlock.Variables"/>
 4521    public IEnumerable<Variable> Variables => Definition.Variables;
 22
 23    /// <summary>
 24    ///     Maps original query names to their merged definition names.
 25    /// </summary>
 26    private QueryMap? _queryMap;
 3878427    private QueryMap QueryMapInstance => _queryMap ??= new();
 28
 29    private readonly QueryDefinition _definition;
 30
 31    /// <summary>
 32    ///     Caches paths to fields for O(1) lookup in GetPathTo().
 33    ///     Maps field name/alias → string[] path segments from root.
 34    /// </summary>
 35    /// <summary>
 36    /// Two-level path cache: <c>rootPath → (nodePath → segments)</c>. The two-level structure avoids
 37    /// allocating a concatenated <c>"{root}.{node}"</c> string on every <c>GetPathTo</c> cache hit.
 38    /// </summary>
 39    private Dictionary<string, Dictionary<string, string[]>>? _pathIndex;
 40
 2670041    private QueryBuilder(QueryDefinition queryDefinition) => _definition = queryDefinition;
 42
 43    /// <summary>
 44    ///     Creates a new instance of <see cref="QueryBuilder"/>.
 45    /// </summary>
 46    /// <param name="name">The name of the query.</param>
 47    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 1099848    public static QueryBuilder CreateDefaultBuilder(string name) => new(new QueryDefinition(name));
 49
 50    /// <summary>
 51    ///     Creates a new instance of <see cref="QueryBuilder"/> with a specific merging strategy.
 52    /// </summary>
 53    /// <param name="name">The name of the query.</param>
 54    /// <param name="mergingStrategy">The merging strategy to use.</param>
 55    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 56    public static QueryBuilder CreateDefaultBuilder(string name, MergingStrategy mergingStrategy)
 57    {
 225658        var definition = new QueryDefinition(name) { MergingStrategy = mergingStrategy };
 225659        return new(definition);
 60    }
 61
 62    /// <summary>
 63    ///     Creates a new <see cref="QueryBuilder"/> that renders as a GraphQL <c>mutation</c>.
 64    ///     The fluent surface (<c>AddField</c>, <c>Include</c>, etc.) is identical to the
 65    ///     query path; only the operation prefix differs at render time.
 66    /// </summary>
 67    /// <param name="name">The name of the mutation.</param>
 68    /// <returns>Instance of <see cref="QueryBuilder"/> in mutation mode.</returns>
 69    public static QueryBuilder CreateMutationBuilder(string name)
 70    {
 4871        var definition = new QueryDefinition(name) { OperationType = OperationType.Mutation };
 4872        return new(definition);
 73    }
 74
 75    /// <summary>
 76    ///     Creates a new <see cref="QueryBuilder"/> that renders as a GraphQL <c>mutation</c>,
 77    ///     with a specific merging strategy.
 78    /// </summary>
 79    /// <param name="name">The name of the mutation.</param>
 80    /// <param name="mergingStrategy">The merging strategy to use.</param>
 81    /// <returns>Instance of <see cref="QueryBuilder"/> in mutation mode.</returns>
 82    public static QueryBuilder CreateMutationBuilder(string name, MergingStrategy mergingStrategy)
 83    {
 384        var definition = new QueryDefinition(name)
 385        {
 386            OperationType = OperationType.Mutation,
 387            MergingStrategy = mergingStrategy,
 388        };
 389        return new(definition);
 90    }
 91
 92    /// <summary>
 93    ///     Creates a new <see cref="QueryBuilder"/> that renders as a GraphQL <c>subscription</c>.
 94    ///     The fluent surface (<c>AddField</c>, <c>Include</c>, etc.) is identical to the
 95    ///     query path; only the operation prefix differs at render time.
 96    /// </summary>
 97    /// <param name="name">The name of the subscription.</param>
 98    /// <returns>Instance of <see cref="QueryBuilder"/> in subscription mode.</returns>
 99    public static QueryBuilder CreateSubscriptionBuilder(string name)
 100    {
 27101        var definition = new QueryDefinition(name) { OperationType = OperationType.Subscription };
 27102        return new(definition);
 103    }
 104
 105    /// <summary>
 106    ///     Creates a new <see cref="QueryBuilder"/> that renders as a GraphQL <c>subscription</c>,
 107    ///     with a specific merging strategy.
 108    /// </summary>
 109    /// <param name="name">The name of the subscription.</param>
 110    /// <param name="mergingStrategy">The merging strategy to use.</param>
 111    /// <returns>Instance of <see cref="QueryBuilder"/> in subscription mode.</returns>
 112    public static QueryBuilder CreateSubscriptionBuilder(string name, MergingStrategy mergingStrategy)
 113    {
 3114        var definition = new QueryDefinition(name)
 3115        {
 3116            OperationType = OperationType.Subscription,
 3117            MergingStrategy = mergingStrategy,
 3118        };
 3119        return new(definition);
 120    }
 121
 122    /// <summary>
 123    ///     Sets the merging strategy for this query builder.
 124    /// </summary>
 125    /// <param name="strategy">The merging strategy to use.</param>
 126    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 127    public QueryBuilder WithMergingStrategy(MergingStrategy strategy)
 128    {
 18129        Definition.MergingStrategy = strategy;
 18130        return this;
 131    }
 132
 133    /// <summary>
 134    ///     Creates a new instance of <see cref="QueryBuilder"/>.
 135    /// </summary>
 136    /// <param name="queryDefinition">The query definition.</param>
 137    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 15138    public static QueryBuilder CreateFromDefinition(QueryDefinition queryDefinition) => new(queryDefinition);
 139
 140    /// <summary>
 141    ///     Adds a field to the query.
 142    /// </summary>
 143    /// <param name="field">Field name or path.</param>
 144    /// <param name="arguments">The arguments for the field.</param>
 145    /// <param name="metadata">The field metadata.</param>
 146    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 147    /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception>
 148    public QueryBuilder AddField(string field, Dictionary<string, object?>? arguments = null, Dictionary<string, object?
 149    {
 150        // FAST PATH: Most common case - no arguments, no metadata
 84759151        if (arguments is null && metadata is null)
 152        {
 76317153            return AddFieldFastPath(field);
 154        }
 155
 8442156        if (arguments?.Count > 0)
 157        {
 8367158            SortedDictionary<string, object?>? sortedArgs = new SortedDictionary<string, object?>(arguments, StringCompa
 8367159            return AddFieldCore(field, sortedArgs, null, metadata);
 160        }
 75161        return AddFieldCore(field, null, null, metadata);
 162    }
 163
 164    /// <summary>
 165    ///     Adds a field to the query.
 166    /// </summary>
 167    /// <param name="field">Field name or path.</param>
 168    /// <param name="subFields">The subfields for the field.</param>
 169    /// <param name="metadata">The field metadata.</param>
 170    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 171    /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception>
 172    public QueryBuilder AddField(string field, string[]? subFields, Dictionary<string, object?>? metadata = null)
 381173        => AddFieldCore(field, null, ToFieldDefinitions(subFields), metadata);
 174
 175    /// <summary>
 176    ///     Adds a field to the query.
 177    /// </summary>
 178    /// <param name="field">Field name or path.</param>
 179    /// <param name="subFields">The subfields for the field.</param>
 180    /// <param name="metadata">The field metadata.</param>
 181    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 182    /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception>
 183    public QueryBuilder AddField(string field, FieldDefinition[]? subFields, Dictionary<string, object?>? metadata = nul
 39184        => AddFieldCore(field, null, subFields, metadata);
 185
 186    /// <summary>
 187    ///     Adds a field to the query using a field builder.
 188    /// </summary>
 189    /// <param name="field">Field name or path.</param>
 190    /// <param name="fieldBuilder">The field builder action.</param>
 191    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 192    /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception>
 193    /// <exception cref="ArgumentNullException">Thrown when the fieldBuilder is null.</exception>
 194    public QueryBuilder AddField(string field, Action<FieldBuilder> fieldBuilder)
 195    {
 807196        ArgumentNullException.ThrowIfNull(fieldBuilder);
 801197        return AddFieldBuilderCore(field, Constants.DefaultFieldType, null, null, fieldBuilder);
 198    }
 199
 200    /// <summary>
 201    ///     Adds a field to the query.
 202    /// </summary>
 203    /// <param name="field">Field name or path.</param>
 204    /// <param name="arguments">The arguments for the field.</param>
 205    /// <param name="subFields">The subfields for the field.</param>
 206    /// <param name="metadata">The field metadata.</param>
 207    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 208    /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception>
 209    public QueryBuilder AddField(string field, Dictionary<string, object?> arguments, string[] subFields, Dictionary<str
 210    {
 211        // Signature declares subFields non-nullable — fail fast like the previous eager
 212        // Select-based implementation did, instead of silently adding a bare leaf.
 234213        ArgumentNullException.ThrowIfNull(subFields);
 231214        SortedDictionary<string, object?>? sortedArgs = arguments?.Count > 0
 231215            ? new SortedDictionary<string, object?>(arguments, StringComparer.OrdinalIgnoreCase)
 231216            : null;
 231217        return AddFieldCore(field, sortedArgs, ToFieldDefinitions(subFields), metadata);
 218    }
 219
 220    /// <summary>
 221    ///     Adds a field to the query.
 222    /// </summary>
 223    /// <param name="field">Field name or path.</param>
 224    /// <param name="arguments">The arguments for the field.</param>
 225    /// <param name="subFields">The subfields for the field.</param>
 226    /// <param name="metadata">The field metadata.</param>
 227    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 228    /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception>
 229    public QueryBuilder AddField(string field, Dictionary<string, object?> arguments, FieldDefinition[] subFields, Dicti
 230    {
 15231        SortedDictionary<string, object?>? sortedArgs = arguments?.Count > 0
 15232            ? new SortedDictionary<string, object?>(arguments, StringComparer.OrdinalIgnoreCase)
 15233            : null;
 15234        return AddFieldCore(field, sortedArgs, subFields, metadata);
 235    }
 236
 237    /// <summary>
 238    ///     Adds a field with a specific type to the query.
 239    /// </summary>
 240    /// <param name="field">Field name or path</param>
 241    /// <param name="type">The field type</param>
 242    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 243    /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception>
 244    public QueryBuilder AddField(string field, string type)
 249245        => AddFieldBuilderCore(field, type, null, null, _ => { });
 246
 247    /// <summary>
 248    ///     Adds a field with a specific type to the query.
 249    /// </summary>
 250    /// <param name="field">Field name or path</param>
 251    /// <param name="type">The field type</param>
 252    /// <param name="metadata">The field metadata</param>
 253    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 254    /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception>
 255    public QueryBuilder AddField(string field, string type, Dictionary<string, object?>? metadata)
 78256        => AddFieldBuilderCore(field, type, null, metadata, _ => { });
 257
 258    /// <summary>
 259    ///     Adds a field to the query using a field builder with arguments.
 260    /// </summary>
 261    /// <param name="field">Field name or path</param>
 262    /// <param name="arguments">The arguments for the field</param>
 263    /// <param name="metadata">The field metadata</param>
 264    /// <param name="fieldBuilder">The field builder action</param>
 265    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 266    /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception>
 267    /// <exception cref="ArgumentNullException">Thrown when the fieldBuilder is null.</exception>
 268    public QueryBuilder AddField(string field, Dictionary<string, object?> arguments, Dictionary<string, object?>? metad
 269    {
 165270        ArgumentNullException.ThrowIfNull(fieldBuilder);
 165271        SortedDictionary<string, object?>? sortedArgs = arguments?.Count > 0
 165272            ? new SortedDictionary<string, object?>(arguments, StringComparer.OrdinalIgnoreCase)
 165273            : null;
 165274        return AddFieldBuilderCore(field, Constants.DefaultFieldType, sortedArgs, metadata, fieldBuilder);
 275    }
 276
 277    /// <summary>
 278    ///     Adds a field to the query using a field builder with a specific type.
 279    /// </summary>
 280    /// <param name="field">Field name or path</param>
 281    /// <param name="type">The field type</param>
 282    /// <param name="metadata">The field metadata</param>
 283    /// <param name="fieldBuilder">The field builder action</param>
 284    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 285    /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception>
 286    /// <exception cref="ArgumentNullException">Thrown when the fieldBuilder is null.</exception>
 287    public QueryBuilder AddField(string field, string type, Dictionary<string, object?>? metadata, Action<FieldBuilder> 
 9288        => AddFieldBuilderCore(field, type, null, metadata, fieldBuilder);
 289
 290    /// <summary>
 291    ///     Adds a field to the query using a field builder with arguments. Convenience overload —
 292    ///     equivalent to passing <c>metadata: null</c> to the four-arg form.
 293    /// </summary>
 294    /// <param name="field">Field name or path</param>
 295    /// <param name="arguments">The arguments for the field</param>
 296    /// <param name="fieldBuilder">The field builder action</param>
 297    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 298    /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception>
 299    /// <exception cref="ArgumentNullException">Thrown when the fieldBuilder is null.</exception>
 300    public QueryBuilder AddField(string field, Dictionary<string, object?> arguments, Action<FieldBuilder> fieldBuilder)
 132301        => AddField(field, arguments, metadata: null, fieldBuilder);
 302
 303    /// <summary>
 304    ///     Adds a field with explicit sub-field names and a field builder action — useful when
 305    ///     the caller wants both static sub-fields AND a chance to add nested structure via the builder.
 306    /// </summary>
 307    /// <param name="field">Field name or path</param>
 308    /// <param name="subFields">The static sub-field names to seed into the field</param>
 309    /// <param name="fieldBuilder">The field builder action invoked after the static sub-fields are added</param>
 310    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 311    /// <exception cref="ArgumentException">Thrown when the field is null or empty.</exception>
 312    /// <exception cref="ArgumentNullException">Thrown when the fieldBuilder is null.</exception>
 313    public QueryBuilder AddField(string field, string[] subFields, Action<FieldBuilder> fieldBuilder)
 314    {
 3315        ArgumentNullException.ThrowIfNull(fieldBuilder);
 3316        return AddField(field, b =>
 3317        {
 18318            foreach (var sub in subFields)
 3319            {
 6320                b.AddField(sub);
 3321            }
 3322
 3323            fieldBuilder(b);
 6324        });
 325    }
 326
 327    /// <summary>
 328    ///     Declares a named GraphQL fragment at the operation's top level. The fragment is
 329    ///     referenced from one or more fields via <c>FieldBuilder.SpreadFragment(name)</c>;
 330    ///     the renderer emits <c>fragment Name on TypeName { … }</c> after the operation block.
 331    /// </summary>
 332    /// <param name="name">Fragment identifier. Used at spread sites (<c>...Name</c>); must be
 333    /// unique per operation. Calling <c>AddFragment</c> twice with the same name and type is
 334    /// idempotent — the second call merges fields into the existing fragment, like
 335    /// <see cref="FieldBuilder.OnType(string, Action{FieldBuilder})"/> does for inline fragments.</param>
 336    /// <param name="onType">The GraphQL type the fragment is declared on.</param>
 337    /// <param name="build">Action that populates the fragment's selection set. Receives a
 338    /// <see cref="FieldBuilder"/> so the same <c>AddField</c> / <c>OnType</c> idioms work as
 339    /// inside any other selection set.</param>
 340    /// <returns>Instance of <see cref="QueryBuilder"/>.</returns>
 341    /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> or
 342    /// <paramref name="onType"/> is null or whitespace.</exception>
 343    /// <exception cref="ArgumentNullException">Thrown when <paramref name="build"/> is null.</exception>
 344    /// <exception cref="InvalidOperationException">Thrown when a fragment with the same
 345    /// <paramref name="name"/> already exists with a different <paramref name="onType"/>.</exception>
 346    public QueryBuilder AddFragment(string name, string onType, Action<FieldBuilder> build)
 347    {
 195348        if (string.IsNullOrWhiteSpace(name))
 349        {
 9350            throw new ArgumentException("Named fragment name cannot be null or whitespace.", nameof(name));
 351        }
 186352        if (string.IsNullOrWhiteSpace(onType))
 353        {
 9354            throw new ArgumentException("Named fragment type name cannot be null or whitespace.", nameof(onType));
 355        }
 177356        ArgumentNullException.ThrowIfNull(build);
 357
 174358        var fragment = _definition.GetOrAddNamedFragment(name, onType);
 359        // Named fragments have no directive storage — directives issue #23 covers are fields and
 360        // inline fragments only; fragment-spread directives are deferred alongside issue #20. This
 361        // local is a discard: a directive call directly on the top-level builder passed to `build`
 362        // is silently dropped, exactly as before this parameter was threaded through.
 171363        List<FieldDirective>? discardedDirectives = null;
 171364        FieldBuilder.PopulateFragmentSurface($"__named_fragment_{name}", fragment.GetOrCreateFieldsStore(),
 171365            ref fragment._fragments, ref fragment._spreadFragments, ref discardedDirectives, build, Definition.Variables
 366
 171367        return this;
 368    }
 369
 370    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 371    private QueryBuilder AddFieldFastPath(string field)
 372    {
 76317373        if (string.IsNullOrWhiteSpace(field))
 374        {
 21375            throw new ArgumentException("Field cannot be null or empty", nameof(field));
 376        }
 377
 378        // ULTRA FAST PATH: Direct field creation for simple cases
 76296379        var fieldSpan = field.AsSpan();
 76296380        if (fieldSpan.IsSimpleField())
 381        {
 382            // Bypass FieldBuilder.Create for maximum performance
 4152383            if (!Definition.Fields.ContainsKey(field))
 384            {
 4119385                Definition.FieldsInternal[field] = new FieldDefinition(field, Constants.DefaultFieldType)
 4119386                {
 4119387                    Path = field
 4119388                };
 389            }
 390        }
 391        else
 392        {
 393            // No builder escapes this overload, so avoid allocating its mutation tracker.
 394            // FieldFactory preserves the same dotted/typed/aliased path processing.
 72144395            FieldFactory.GetOrAddField(Definition.FieldsInternal, fieldSpan, Constants.DefaultFieldTypeSpan, null);
 396        }
 397
 398        // Phase 3: Invalidate caches after field addition
 76281399        InvalidateLookupCaches();
 400
 401        // Defer UpdateRootMapping - will be called when query is built/used
 76281402        return this;
 403    }
 404
 405    /// <summary>
 406    /// Core implementation for adding fields using FieldBuilder pattern.
 407    /// </summary>
 408    /// <param name="field">Field name or path</param>
 409    /// <param name="fieldType">The field type</param>
 410    /// <param name="arguments">Optional arguments dictionary</param>
 411    /// <param name="metadata">Optional metadata dictionary</param>
 412    /// <param name="fieldBuilder">The field builder action</param>
 413    /// <returns>Current QueryBuilder instance for method chaining</returns>
 414    private QueryBuilder AddFieldBuilderCore(string field, string fieldType, SortedDictionary<string, object?>? argument
 415    {
 1140416        if (string.IsNullOrWhiteSpace(field))
 417        {
 3418            throw new ArgumentException("Field cannot be null or empty", nameof(field));
 419        }
 420
 1137421        ArgumentNullException.ThrowIfNull(fieldBuilder);
 422
 423        // FAST PATH: Only extract variables if arguments has content
 1137424        if (arguments?.Count > 0)
 425        {
 159426            Helpers.ExtractVariablesFromValue(arguments, Definition.Variables);
 427        }
 428
 429        // Use the provided field type. Threads Definition.Variables down into the builder (and
 430        // every nested Action<FieldBuilder> scope it creates) so IncludeIf(Variable)/SkipIf(Variable)
 431        // called anywhere in the subtree promote into this operation's signature.
 1137432        var builder = FieldBuilder.Create(Definition.FieldsInternal, field, fieldType, arguments, metadata, Definition.V
 1137433        fieldBuilder(builder);
 434
 1053435        QueryMapInstance.UpdateRootMapping(_definition);
 1053436        return this;
 437    }
 438
 439    /// <summary>
 440    /// Merges <paramref name="queryBuilder"/>'s fields and variables into this builder.
 441    /// Merge behavior is governed by this builder's <see cref="MergingStrategy"/>:
 442    /// <see cref="NGql.Core.MergingStrategy.MergeByDefault"/> appends fragments,
 443    /// <see cref="NGql.Core.MergingStrategy.MergeByFieldPath"/> merges compatible same-path
 444    /// fields and auto-aliases on argument conflict, and
 445    /// <see cref="NGql.Core.MergingStrategy.NeverMerge"/> always aliases the included fields
 446    /// as <c>name_1</c>, <c>name_2</c>, …
 447    /// <para>
 448    /// Fragment-bearing queries merge correctly: named-fragment definitions carry over (same
 449    /// name and type are deduped/merged, a same name with a conflicting type throws
 450    /// <see cref="InvalidOperationException"/>), and a merged field keeps its inline fragments,
 451    /// fragment spreads (insertion order preserved), and directives. All fragment state is
 452    /// deep-cloned, so mutating the source after <c>Include</c> never affects the merged result.
 453    /// </para>
 454    /// </summary>
 455    /// <param name="queryBuilder">Builder whose fields will be merged into this one.</param>
 456    /// <returns>This builder, for chaining.</returns>
 457    /// <exception cref="InvalidOperationException">Thrown when the incoming query declares a named
 458    /// fragment whose name matches one already declared on this builder but on a different type.</exception>
 2184459    public QueryBuilder Include(QueryBuilder queryBuilder) => IncludeImpl(queryBuilder.Definition);
 460
 461    /// <summary>
 462    /// Core implementation for adding fields with optional arguments.
 463    /// </summary>
 464    /// <param name="field">Field name or path</param>
 465    /// <param name="arguments">Optional arguments dictionary</param>
 466    /// <param name="subFields">Optional array of sub-field definitions</param>
 467    /// <param name="metadata">Optional metadata dictionary</param>
 468    /// <returns>Current QueryBuilder instance for method chaining</returns>
 469    private QueryBuilder AddFieldCore(string field, SortedDictionary<string, object?>? arguments, FieldDefinition[]? sub
 470    {
 9108471        if (string.IsNullOrWhiteSpace(field))
 18472            throw new ArgumentException("Field cannot be null or empty", nameof(field));
 473
 9090474        var hasSubFields = subFields is { Length: > 0 };
 475
 9090476        if (arguments is { Count: > 0 })
 8589477            Helpers.ExtractVariablesFromValue(arguments, Definition.Variables);
 478
 9090479        if (!hasSubFields)
 480        {
 481            // No builder escapes here, so skip its tracker and ancestor chain (see AddFieldFastPath).
 8457482            FieldFactory.GetOrAddField(Definition.FieldsInternal, field, Constants.DefaultFieldType,
 8457483                arguments is { Count: > 0 } ? arguments : null, null, metadata);
 8457484            QueryMapInstance.UpdateRootMapping(_definition);
 485            // Phase 3: Invalidate caches after field addition
 8457486            InvalidateLookupCaches();
 8457487            return this;
 488        }
 489
 633490        var parent = FieldFactory.GetOrAddField(Definition.FieldsInternal, field, Constants.ObjectFieldType,
 633491            arguments is { Count: > 0 } ? arguments : null, null, metadata);
 3399492        foreach (var subField in subFields!)
 1068493            FieldBuilder.AddSubField(parent, subField);
 494
 630495        QueryMapInstance.UpdateRootMapping(_definition);
 496        // Phase 3: Invalidate caches after field addition
 630497        InvalidateLookupCaches();
 630498        return this;
 499    }
 500
 501    /// <summary>
 502    /// Materializes sub-field names into definitions once. A deferred Select here would be
 503    /// enumerated twice by AddFieldCore (emptiness check + add loop), constructing every
 504    /// <see cref="FieldDefinition"/> twice.
 505    /// </summary>
 506    private static FieldDefinition[]? ToFieldDefinitions(string[]? subFields)
 507    {
 618508        if (subFields is null) return null;
 606509        var definitions = new FieldDefinition[subFields.Length];
 3228510        for (var i = 0; i < subFields.Length; i++)
 1008511            definitions[i] = new FieldDefinition(subFields[i]);
 606512        return definitions;
 513    }
 514
 515    /// <summary>
 516    /// Core implementation for including another query definition with optimized parameter passing.
 517    /// </summary>
 518    /// <param name="queryDefinition">Query definition to include (passed by reference for performance)</param>
 519    /// <returns>Current QueryBuilder instance for method chaining</returns>
 520    private QueryBuilder IncludeImpl(in QueryDefinition queryDefinition)
 521    {
 2184522        QueryMerger.MergeQuery(_definition, QueryMapInstance, this, in queryDefinition);
 523
 524        // Phase 3: Invalidate lookup caches after merge since fields changed
 2166525        InvalidateLookupCaches();
 526
 2166527        return this;
 528    }
 529
 530    /// <summary>
 531    /// Phase 3 optimization: Invalidates both path and field lookup caches.
 532    /// Called after any modification to Definition.Fields to ensure cache consistency.
 533    /// </summary>
 534    private void InvalidateLookupCaches()
 535    {
 87534536        _pathIndex?.Clear();
 3537    }
 538
 539    /// <summary>
 540    /// Merges <paramref name="metadata"/> into the query definition's metadata bag, deeply
 541    /// combining nested dictionaries. Existing keys are overwritten by <paramref name="metadata"/>
 542    /// only when the new value is not itself a dictionary; nested dictionaries are recursively
 543    /// merged.
 544    /// </summary>
 545    /// <param name="metadata">Metadata to merge in.</param>
 546    /// <returns>This builder, for chaining.</returns>
 547    public QueryBuilder WithMetadata(Dictionary<string, object> metadata)
 548    {
 72549        var mergedMetadata = Helpers.MergeMetadata(_definition._metadata, metadata);
 72550        _definition._metadata = mergedMetadata;
 551
 72552        return this;
 553    }
 554
 555    /// <summary>
 556    /// Gets the path segments to reach a specific node within a query.
 557    /// </summary>
 558    /// <param name="queryName">The name of the query to find the path for.</param>
 559    /// <param name="nodePath">The optional node path within the query (e.g., "edges.node").</param>
 560    /// <returns>An array of path segments to reach the specified node.</returns>
 561    public string[] GetPathTo(string queryName, string? nodePath = null)
 562    {
 351563        _pathIndex ??= new();
 351564        return QueryMapInstance.GetPathTo(queryName, nodePath, _definition, _pathIndex);
 565    }
 566
 567    /// <summary>
 568    /// Gets the count of fields in the QueryDefinition.
 569    /// This represents the correct value with or without merge.
 570    /// </summary>
 162571    internal int DefinitionsCount => Definition.Fields.Count;
 572
 573    /// <inheritdoc cref="QueryBlock.ToString()"/>
 574    public override string ToString()
 575    {
 24750576        QueryMapInstance.UpdateRootMapping(_definition);
 24750577        return Definition.ToString();
 578    }
 579
 580    /// <summary>
 581    /// Renders this query's GraphQL and appends it to <paramref name="builder"/> without
 582    /// materializing the intermediate string that <see cref="ToString()"/> allocates. The
 583    /// appended text is byte-for-byte identical to <see cref="ToString()"/>.
 584    /// </summary>
 585    /// <param name="builder">The target <see cref="StringBuilder"/> to append to.</param>
 586    /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> is null.</exception>
 587    public void AppendTo(StringBuilder builder)
 588    {
 51589        ArgumentNullException.ThrowIfNull(builder);
 48590        QueryMapInstance.UpdateRootMapping(_definition);
 48591        Definition.AppendTo(builder);
 48592    }
 593
 594    /// <summary>
 595    /// Renders this query's GraphQL and writes it to <paramref name="writer"/> without
 596    /// materializing the intermediate string that <see cref="ToString()"/> allocates. The
 597    /// written text is byte-for-byte identical to <see cref="ToString()"/>.
 598    /// </summary>
 599    /// <param name="writer">The target <see cref="TextWriter"/> to write to.</param>
 600    /// <exception cref="ArgumentNullException">Thrown when <paramref name="writer"/> is null.</exception>
 601    public void WriteTo(TextWriter writer)
 602    {
 1224603        ArgumentNullException.ThrowIfNull(writer);
 1221604        QueryMapInstance.UpdateRootMapping(_definition);
 1221605        Definition.WriteTo(writer);
 1221606    }
 607
 608    /// <summary>
 609    /// Renders this query's GraphQL and transcodes it as UTF-8 directly into
 610    /// <paramref name="bufferWriter"/>, with no intermediate <see cref="string"/> or <c>byte[]</c>
 611    /// allocation. This lets consumers write a query straight into a <c>PipeWriter</c>,
 612    /// <c>ArrayBufferWriter&lt;byte&gt;</c>, or response-body buffer. The written bytes are identical
 613    /// to <c>System.Text.Encoding.UTF8.GetBytes(</c><see cref="ToString()"/><c>)</c>.
 614    /// </summary>
 615    /// <param name="bufferWriter">The target <see cref="IBufferWriter{Byte}"/> to write UTF-8 bytes to.</param>
 616    /// <exception cref="ArgumentNullException">Thrown when <paramref name="bufferWriter"/> is null.</exception>
 617    public void WriteUtf8(IBufferWriter<byte> bufferWriter)
 618    {
 93619        ArgumentNullException.ThrowIfNull(bufferWriter);
 90620        QueryMapInstance.UpdateRootMapping(_definition);
 90621        Definition.WriteUtf8(bufferWriter);
 90622    }
 623
 375624    public static implicit operator string(QueryBuilder query) => query.ToString();
 625}

Methods/Properties

get_Definition()
get_Variables()
get_QueryMapInstance()
.ctor(NGql.Core.Abstractions.QueryDefinition)
CreateDefaultBuilder(System.String)
CreateDefaultBuilder(System.String,NGql.Core.MergingStrategy)
CreateMutationBuilder(System.String)
CreateMutationBuilder(System.String,NGql.Core.MergingStrategy)
CreateSubscriptionBuilder(System.String)
CreateSubscriptionBuilder(System.String,NGql.Core.MergingStrategy)
WithMergingStrategy(NGql.Core.MergingStrategy)
CreateFromDefinition(NGql.Core.Abstractions.QueryDefinition)
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.Collections.Generic.Dictionary`2<System.String,System.Object>)
AddField(System.String,NGql.Core.Abstractions.FieldDefinition[],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.String[],System.Collections.Generic.Dictionary`2<System.String,System.Object>)
AddField(System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>,NGql.Core.Abstractions.FieldDefinition[],System.Collections.Generic.Dictionary`2<System.String,System.Object>)
AddField(System.String,System.String)
AddField(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>,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.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>)
AddFragment(System.String,System.String,System.Action`1<NGql.Core.Builders.FieldBuilder>)
AddFieldFastPath(System.String)
AddFieldBuilderCore(System.String,System.String,System.Collections.Generic.SortedDictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Action`1<NGql.Core.Builders.FieldBuilder>)
Include(NGql.Core.Builders.QueryBuilder)
AddFieldCore(System.String,System.Collections.Generic.SortedDictionary`2<System.String,System.Object>,NGql.Core.Abstractions.FieldDefinition[],System.Collections.Generic.Dictionary`2<System.String,System.Object>)
ToFieldDefinitions(System.String[])
IncludeImpl(NGql.Core.Abstractions.QueryDefinition&)
InvalidateLookupCaches()
WithMetadata(System.Collections.Generic.Dictionary`2<System.String,System.Object>)
GetPathTo(System.String,System.String)
get_DefinitionsCount()
ToString()
AppendTo(System.Text.StringBuilder)
WriteTo(System.IO.TextWriter)
WriteUtf8(System.Buffers.IBufferWriter`1<System.Byte>)
op_Implicit(NGql.Core.Builders.QueryBuilder)