< Summary

Information
Class: NGql.Core.Abstractions.FieldDefinition
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Abstractions/FieldDefinition.cs
Line coverage
100%
Covered lines: 67
Uncovered lines: 0
Coverable lines: 67
Total lines: 262
Line coverage: 100%
Branch coverage
100%
Covered branches: 66
Total branches: 66
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_Path()100%11100%
.ctor(...)100%22100%
.ctor(...)100%66100%
.ctor(...)100%22100%
ToSortedArguments(...)100%66100%
AsChildren(...)100%66100%
get_Name()100%11100%
get_Type()100%11100%
set_Type(...)100%11100%
get_Alias()100%11100%
set_Alias(...)100%22100%
.cctor()100%11100%
get_Fields()100%22100%
get_InlineFragments()100%22100%
get_Arguments()100%22100%
get_Metadata()100%22100%
set_Metadata(...)100%11100%
get_IsArray()100%22100%
get_IsNullable()100%22100%
get_HasFields()100%22100%
get_HasInlineFragments()100%22100%
GetOrAddInlineFragment(...)100%44100%
get_IsNeverMerge()100%11100%
Equals(...)100%1212100%
GetHashCode()100%44100%
ToString()100%66100%

File(s)

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

#LineLine coverage
 1using System.Diagnostics.CodeAnalysis;
 2using System.Text.Json.Serialization;
 3using NGql.Core.Extensions;
 4
 5namespace NGql.Core.Abstractions;
 6
 7/// <summary>
 8///     Represents a field definition.
 9/// </summary>
 10[SuppressMessage("Minor Code Smell", "S2292:Trivial properties should be auto-implemented")]
 11public sealed record FieldDefinition
 12{
 13    // Fields
 14    internal FieldChildren? _children;
 15    internal Dictionary<string, InlineFragmentDefinition>? _fragments;
 16    internal string? _type;
 17    internal string? _alias;
 18    internal string _effectiveName;
 19    internal SortedDictionary<string, object?>? _arguments;
 20    internal Dictionary<string, object?>? _metadata;
 12615321    internal string Path { get; init; } = string.Empty;
 22
 23    /// <summary>
 24    /// Cached result of "does this field's subtree contain any arguments?".
 25    /// Null = not yet computed. Reset to null whenever the subtree mutates.
 26    /// </summary>
 27    internal bool? _subtreeHasAnyArguments;
 28
 29    private bool? _isArray;
 30    private bool? _isNullable;
 31
 32    /// <summary>
 33    /// Creates a field definition with a name and optional type and alias.
 34    /// <paramref name="type"/> defaults to <see cref="Constants.DefaultFieldType"/> when null.
 35    /// </summary>
 36    /// <param name="name">Field name as it appears in the rendered GraphQL.</param>
 37    /// <param name="type">Optional type-annotation metadata (rendered nowhere; consumed by tooling).</param>
 38    /// <param name="alias">Optional response-side alias.</param>
 39    public FieldDefinition(string name, string? type = null, string? alias = null)
 2734540        : this(name, type ?? Constants.DefaultFieldType, alias, null)
 41    {
 2734542    }
 43
 44    /// <summary>
 45    /// Creates a field definition with a pre-sorted argument dictionary and an optional
 46    /// child-field collection. Used internally on the hot path to avoid re-sorting.
 47    /// </summary>
 48    /// <param name="name">Field name.</param>
 49    /// <param name="type">Type-annotation metadata.</param>
 50    /// <param name="alias">Optional response-side alias.</param>
 51    /// <param name="sortedArguments">Pre-sorted argument map (case-insensitive); null/empty stores null.</param>
 52    /// <param name="fields">Optional initial children; null/empty leaves the field as a leaf.</param>
 7219253    public FieldDefinition(string name, string type, string? alias, SortedDictionary<string, object?>? sortedArguments =
 54    {
 7219255        Name = name;
 7219256        _alias = alias;
 7219257        _type = type;
 7219258        _arguments = sortedArguments?.Count > 0 ? sortedArguments : null;
 7219259        _children = AsChildren(fields);
 7219260        _effectiveName = !string.IsNullOrEmpty(_alias) ? _alias : Name;
 7219261    }
 62
 63    /// <summary>
 64    /// Creates a field definition from an unsorted argument dictionary (e.g. one produced by
 65    /// callers using collection initializers). The dictionary is copied into a case-insensitive
 66    /// sorted store so output ordering is stable.
 67    /// </summary>
 68    /// <param name="name">Field name.</param>
 69    /// <param name="type">Type-annotation metadata.</param>
 70    /// <param name="alias">Optional response-side alias.</param>
 71    /// <param name="arguments">Unsorted argument map; null/empty stores null.</param>
 72    /// <param name="fields">Optional initial children; null/empty leaves the field as a leaf.</param>
 21373    public FieldDefinition(string name, string type, string? alias, IDictionary<string, object?>? arguments, Dictionary<
 74    {
 21375        Name = name;
 21376        _alias = alias;
 21377        _type = type;
 21378        _arguments = ToSortedArguments(arguments);
 21379        _children = AsChildren(fields);
 21380        _effectiveName = !string.IsNullOrEmpty(_alias) ? _alias : Name;
 21381    }
 82
 83    private static SortedDictionary<string, object?>? ToSortedArguments(IDictionary<string, object?>? arguments)
 84    {
 21685        if (arguments is null) return null;
 30986        if (arguments.Count == 0) return null;
 11187        var sorted = new SortedDictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 160888        foreach (var kvp in arguments) sorted[kvp.Key] = kvp.Value;
 11189        return sorted;
 90    }
 91
 92    private static FieldChildren? AsChildren(Dictionary<string, FieldDefinition>? fields)
 93    {
 14470294        if (fields is null || fields.Count == 0) return null;
 10895        var children = new FieldChildren();
 72096        foreach (var kvp in fields) children.Append(kvp.Value);
 10897        return children;
 98    }
 99
 100    // Properties
 101    /// <summary>
 102    /// The name of the field.
 103    /// </summary>
 104    [JsonPropertyName("name")]
 19137837105    public string Name { get; init; }
 106
 107    /// <summary>
 108    /// The type of the field. Defaults to <see cref="Constants.DefaultFieldType"/> if not specified.
 109    /// This is used to define the data type of the field, such as "String", "Int", "Boolean", etc.
 110    /// </summary>
 111    [JsonPropertyName("type")]
 112    public string? Type
 113    {
 1176114        get => _type;
 318115        init => _type = value;
 116    }
 117
 118    /// <summary>
 119    /// The alias of the field, if any. This is used to provide a more readable or meaningful name for the field in quer
 120    /// If not specified, the field will use its original name.
 121    /// </summary>
 122    [JsonPropertyName("alias")]
 123    public string? Alias
 124    {
 118272125        get => _alias;
 126        init
 127        {
 150128            _alias = value;
 150129            _effectiveName = !string.IsNullOrEmpty(value) ? value : Name;
 150130        }
 131    }
 132
 3133    private static readonly IReadOnlyDictionary<string, FieldDefinition> EmptyReadOnlyFields
 3134        = new FieldChildren();
 135
 136    /// <summary>
 137    ///     The collection of fields related to <see cref="FieldDefinition"/>.
 138    /// </summary>
 139    [JsonPropertyName("fields")]
 140    public IReadOnlyDictionary<string, FieldDefinition> Fields
 2829141        => _children ?? EmptyReadOnlyFields;
 142
 3143    private static readonly IReadOnlyDictionary<string, InlineFragmentDefinition> EmptyReadOnlyFragments
 3144        = new Dictionary<string, InlineFragmentDefinition>();
 145
 146    /// <summary>
 147    /// Inline fragments attached to this field, keyed by the fragment's GraphQL type name
 148    /// (case-sensitive). Each fragment renders as <c>... on TypeName { … }</c> after the
 149    /// field's plain children, alphabetical by type name.
 150    /// </summary>
 151    /// <remarks>
 152    /// Used when the field's schema return type is a union or interface and the caller needs
 153    /// type narrowing. See <see cref="NGql.Core.Builders.FieldBuilder.OnType(string, Action{NGql.Core.Builders.FieldBui
 154    /// for the builder-side API. Multiple <c>OnType</c> calls for the same type name merge
 155    /// into one fragment definition.
 156    /// </remarks>
 157    [JsonPropertyName("inlineFragments")]
 158    public IReadOnlyDictionary<string, InlineFragmentDefinition> InlineFragments
 21159        => (IReadOnlyDictionary<string, InlineFragmentDefinition>?)_fragments ?? EmptyReadOnlyFragments;
 160
 3161    private static readonly IReadOnlyDictionary<string, object?> EmptyReadOnlyArguments
 3162        = new SortedDictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 163
 164    /// <summary>
 165    /// Sorted, case-insensitive view of the field's GraphQL arguments. Returns an empty
 166    /// dictionary (never null) when the field has no arguments.
 167    /// </summary>
 168    [JsonPropertyName("arguments")]
 169    public IReadOnlyDictionary<string, object?> Arguments
 333170        => _arguments ?? EmptyReadOnlyArguments;
 171
 172    /// <summary>
 173    /// Metadata associated with the field definition.
 174    /// This can include additional information such as descriptions, tags, or any other relevant data.
 175    ///
 176    /// Not used during query text generation but can be useful for documentation or introspection purposes.
 177    /// </summary>
 178    [JsonPropertyName("metadata")]
 179    public Dictionary<string, object?> Metadata
 180    {
 1314181        get => _metadata ??= [];
 51182        set => _metadata = value;
 183    }
 184
 185    /// <summary>
 186    /// Gets a value indicating whether this field type is an array.
 187    /// </summary>
 188    [JsonIgnore]
 174189    public bool IsArray => _isArray ??= _type.IsArrayType();
 190
 191    /// <summary>
 192    /// Gets a value indicating whether this field type is nullable.
 193    /// </summary>
 194    [JsonIgnore]
 144195    public bool IsNullable => _isNullable ??= _type.IsNullableType();
 196
 197    /// <summary>
 198    /// Gets a value indicating whether this field has child fields.
 199    /// </summary>
 200    [JsonIgnore]
 294201    public bool HasFields => _children is { Count: > 0 };
 202
 203    /// <summary>
 204    /// Gets a value indicating whether this field has any inline fragments.
 205    /// </summary>
 206    [JsonIgnore]
 6207    public bool HasInlineFragments => _fragments is { Count: > 0 };
 208
 209    /// <summary>
 210    /// Returns the existing inline fragment for <paramref name="typeName"/>, or appends a new
 211    /// one. Used by the builder to merge multiple <c>OnType("Repository", …)</c> calls on the
 212    /// same parent into a single fragment definition.
 213    /// </summary>
 214    internal InlineFragmentDefinition GetOrAddInlineFragment(string typeName)
 215    {
 51216        _fragments ??= new Dictionary<string, InlineFragmentDefinition>(StringComparer.Ordinal);
 51217        if (!_fragments.TryGetValue(typeName, out var fragment))
 218        {
 48219            fragment = new InlineFragmentDefinition(typeName);
 48220            _fragments[typeName] = fragment;
 221        }
 51222        return fragment;
 223    }
 224
 225    /// <summary>
 226    /// When <c>true</c>, the merger treats this field as opaque — it will not be merged with
 227    /// other fields of the same path; instead it gets aliased (<c>name_1</c>, <c>name_2</c>, …)
 228    /// during <see cref="NGql.Core.Builders.QueryBuilder.Include(NGql.Core.Builders.QueryBuilder)"/>.
 229    /// The setter is internal; the flag is set by <see cref="MergingStrategy.NeverMerge"/>.
 230    /// </summary>
 231    [JsonIgnore]
 3336232    public bool IsNeverMerge { get; internal set; }
 233
 234    // Methods
 235    public bool Equals(FieldDefinition? other)
 236    {
 24789237        if (other is null) return false;
 42238        if (ReferenceEquals(this, other)) return true;
 12239        return string.Equals(Name, other.Name, StringComparison.Ordinal)
 12240            && string.Equals(Path, other.Path, StringComparison.Ordinal)
 12241            && string.Equals(_type, other._type, StringComparison.OrdinalIgnoreCase)
 12242            && string.Equals(_alias, other._alias, StringComparison.OrdinalIgnoreCase)
 12243            && IsNeverMerge == other.IsNeverMerge;
 244    }
 245
 246    // FieldDefinition holds mutable internal state by design (in-place merging in QueryMerger).
 247    // The hash captures identity at evaluation time; callers do not stash hashes across mutations.
 248#pragma warning disable S2328
 249    public override int GetHashCode()
 75250        => HashCode.Combine(Name, Path, _type?.ToLowerInvariant(), _alias?.ToLowerInvariant(), IsNeverMerge);
 251#pragma warning restore S2328
 252
 253    public override string ToString()
 254    {
 18255        if (string.IsNullOrWhiteSpace(Type))
 256        {
 6257            return string.IsNullOrWhiteSpace(Alias) ? Name : $"{Alias}:{Name}";
 258        }
 259
 12260        return string.IsNullOrWhiteSpace(Alias) ? $"{Type} {Name}" : $"{Type} {Alias}:{Name}";
 261    }
 262}