< Summary

Information
Class: NGql.Core.Extensions.Helpers
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Extensions/Helpers.cs
Line coverage
100%
Covered lines: 235
Uncovered lines: 0
Coverable lines: 235
Total lines: 596
Line coverage: 100%
Branch coverage
100%
Covered branches: 258
Total branches: 258
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ExtractVariablesFromValue(...)100%11100%
ExtractVariablesFromValueCore(...)100%1010100%
ExtractVariablesFromDictionary(...)100%66100%
ExtractVariablesFromList(...)100%66100%
ShouldExtractFromObjectProperties(...)100%1010100%
ExtractVariablesFromObjectProperties(...)100%88100%
MergeNullableDictionaries(...)100%1010100%
MergeMetadata(...)100%1010100%
ConvertToNullable(...)100%22100%
MergeNullableMetadata(...)100%1414100%
MergedNullableMetadataValue(...)100%66100%
MergeMetadataDictionaries(...)100%44100%
MergedMetadataValue(...)100%66100%
MergeNullableMetadataDictionaries(...)100%44100%
SortArgumentValue(...)100%44100%
SortNonPrimitive(...)100%1010100%
SortDictionary(...)100%11100%
IsDecomposable(...)100%88100%
DecomposeToDictionary(...)100%11100%
SortListItems(...)100%22100%
AreArgumentsEqual(...)100%44100%
CompareArgumentShapes(...)100%1010100%
ArgumentEntriesMatch(...)100%66100%
AreValuesEqual(...)100%1212100%
AreReferenceTypedValuesEqual(...)100%88100%
AreDictionariesEqual(...)100%88100%
AreListsEqual(...)100%66100%
AreObjectsStructurallyEqual(...)100%44100%
ParseFieldTypeFromPath(...)100%44100%
LooksLikeTypeAnnotation(...)100%1010100%
CreateFieldDefinition(...)100%1010100%
FindExistingField(...)100%88100%
FindExistingField(...)100%66100%
ValidateFieldName(...)100%1010100%
ValidateIdentifier(...)100%88100%
IsValidGraphQlNameStart(...)100%88100%
IsValidGraphQlNameChar(...)100%1212100%
WriteCollection(...)100%44100%

File(s)

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

#LineLine coverage
 1using System.Collections;
 2using System.Diagnostics.CodeAnalysis;
 3using System.Runtime.CompilerServices;
 4using System.Text;
 5using NGql.Core.Abstractions;
 6
 7namespace NGql.Core.Extensions;
 8
 9[SuppressMessage("Minor Code Smell", "S3267:Loops should be simplified with \"LINQ\" expressions")]
 10internal static class Helpers
 11{
 12    internal static void ExtractVariablesFromValue(object? value, SortedSet<Variable> variables)
 13    {
 745814        ExtractVariablesFromValueCore(value, variables, null);
 745815    }
 16
 17    private static void ExtractVariablesFromValueCore(object? value, SortedSet<Variable> variables, HashSet<object>? vis
 18    {
 2295319        if (value == null)
 20        {
 1821            return;
 22        }
 23
 2293524        if (value is Variable variable)
 25        {
 21626            variables.Add(variable);
 21627            return;
 28        }
 29
 2271930        if (value is IDictionary dict)
 31        {
 798032            ExtractVariablesFromDictionary(dict, variables, visited);
 798033            return;
 34        }
 35
 1473936        if (value is IList list)
 37        {
 8438            ExtractVariablesFromList(list, variables, visited);
 8439            return;
 40        }
 41
 1465542        if (ShouldExtractFromObjectProperties(value))
 43        {
 11144            ExtractVariablesFromObjectProperties(value, variables, visited);
 45        }
 1465546    }
 47
 48    private static void ExtractVariablesFromDictionary(IDictionary dict, SortedSet<Variable> variables, HashSet<object>?
 49    {
 798050        visited ??= new HashSet<object>(ReferenceEqualityComparer.Instance);
 798951        if (!visited.Add(dict)) return; // cycle detected
 52
 4629653        foreach (var val in dict.Values)
 54        {
 1517755            ExtractVariablesFromValueCore(val, variables, visited);
 56        }
 797157    }
 58
 59    private static void ExtractVariablesFromList(IList list, SortedSet<Variable> variables, HashSet<object>? visited)
 60    {
 8461        visited ??= new HashSet<object>(ReferenceEqualityComparer.Instance);
 9062        if (!visited.Add(list)) return; // cycle detected
 63
 42664        foreach (var item in list)
 65        {
 13566            ExtractVariablesFromValueCore(item, variables, visited);
 67        }
 7868    }
 69
 70    private static bool ShouldExtractFromObjectProperties(object obj)
 71    {
 1465572        return obj is not string &&
 1465573               obj is not Variable &&
 1465574               obj is not QueryBlock &&
 1465575               obj is not IDictionary &&
 1465576               obj is not IList &&
 1465577               !ValueFormatter.IsPrimitiveType(obj);
 78    }
 79
 80    private static void ExtractVariablesFromObjectProperties(object obj, SortedSet<Variable> variables, HashSet<object>?
 81    {
 11182        visited ??= new HashSet<object>(ReferenceEqualityComparer.Instance);
 11483        if (!visited.Add(obj)) return; // cycle detected
 10884        var properties = obj.GetType().GetProperties();
 58885        foreach (var property in properties)
 86        {
 18687            var propertyValue = property.GetValue(obj);
 18688            if (propertyValue != null)
 89            {
 18390                ExtractVariablesFromValueCore(propertyValue, variables, visited);
 91            }
 92        }
 10893    }
 94
 95    /// <summary>
 96    /// Generic dictionary merging with recursive support for nested dictionaries.
 97    /// </summary>
 98    /// <summary>
 99    /// Merges two dictionaries with support for nullable values and recursive merging of nested dictionaries.
 100    /// This is the primary efficient implementation that avoids extra allocations.
 101    /// </summary>
 102    /// <param name="existing">The existing dictionary</param>
 103    /// <param name="update">The dictionary to merge in</param>
 104    /// <returns>A merged SortedDictionary with nullable values</returns>
 105    internal static SortedDictionary<string, object?> MergeNullableDictionaries(
 106        IDictionary<string, object?> existing,
 107        IDictionary<string, object?> update)
 108    {
 45109        var result = new SortedDictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 110
 111        // Copy existing entries
 186112        foreach (var kvp in existing)
 113        {
 48114            result[kvp.Key] = kvp.Value;
 115        }
 116
 117        // Merge update entries with recursive handling for nested dictionaries
 180118        foreach (var (key, newValue) in update)
 119        {
 45120            if (result.TryGetValue(key, out var existingValue) &&
 45121                existingValue is IDictionary<string, object?> existingDict &&
 45122                newValue is IDictionary<string, object?> newDict)
 123            {
 124                // Recursively merge nested dictionaries
 12125                result[key] = MergeNullableDictionaries(existingDict, newDict);
 126            }
 127            else
 128            {
 129                // Override with new value for non-dictionary values or new keys
 33130                result[key] = newValue;
 131            }
 132        }
 133
 45134        return result;
 135    }
 136
 137    /// <summary>
 138    /// Merges metadata dictionaries, handling nullable values appropriately with deep/recursive merging for nested dict
 139    /// </summary>
 140    /// <param name="existing">The existing metadata dictionary</param>
 141    /// <param name="update">The metadata dictionary to merge in</param>
 142    /// <returns>A merged Dictionary with nullable values suitable for metadata</returns>
 143    internal static Dictionary<string, object?> MergeMetadata(
 144        Dictionary<string, object?>? existing,
 145        Dictionary<string, object> update)
 146    {
 225147        if (existing is null || existing.Count == 0) return ConvertToNullable(update);
 63148        if (update.Count == 0) return existing;
 149
 51150        var result = new Dictionary<string, object?>(existing.Count + update.Count, StringComparer.OrdinalIgnoreCase);
 372151        foreach (var (key, value) in existing) result[key] = value;
 264152        foreach (var (key, newValue) in update)
 153        {
 81154            result[key] = MergedMetadataValue(result, key, newValue);
 155        }
 156
 51157        return result;
 158    }
 159
 160    private static Dictionary<string, object?> ConvertToNullable(Dictionary<string, object> source)
 161    {
 84162        var converted = new Dictionary<string, object?>(source.Count, StringComparer.OrdinalIgnoreCase);
 636163        foreach (var (key, value) in source) converted[key] = value;
 84164        return converted;
 165    }
 166
 167    /// <summary>
 168    /// Merges metadata dictionaries where both existing and update can have nullable values.
 169    /// </summary>
 170    /// <param name="existing">The existing metadata dictionary</param>
 171    /// <param name="update">The metadata dictionary to merge in</param>
 172    /// <returns>A merged Dictionary with nullable values suitable for metadata</returns>
 173    internal static Dictionary<string, object?> MergeNullableMetadata(
 174        Dictionary<string, object?>? existing,
 175        Dictionary<string, object?>? update)
 176    {
 39177        if (update is null || update.Count == 0)
 6178            return existing ?? new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 179
 33180        if (existing is null || existing.Count == 0)
 6181            return new Dictionary<string, object?>(update, StringComparer.OrdinalIgnoreCase);
 182
 27183        var result = new Dictionary<string, object?>(existing.Count + update.Count, StringComparer.OrdinalIgnoreCase);
 153184        foreach (var (key, value) in existing) result[key] = value;
 114185        foreach (var (key, newValue) in update)
 186        {
 30187            result[key] = MergedNullableMetadataValue(result, key, newValue);
 188        }
 27189        return result;
 190    }
 191
 192    private static object? MergedNullableMetadataValue(Dictionary<string, object?> target, string key, object? newValue)
 193    {
 45194        if (target.TryGetValue(key, out var existingValue)
 45195            && existingValue is Dictionary<string, object?> existingDict
 45196            && newValue is Dictionary<string, object?> newDict)
 197        {
 15198            return MergeNullableMetadataDictionaries(existingDict, newDict);
 199        }
 30200        return newValue;
 201    }
 202
 203    /// <summary>
 204    /// Deep merges two metadata dictionaries recursively.
 205    /// </summary>
 206    /// <param name="existing">The existing dictionary</param>
 207    /// <param name="update">The dictionary to merge in</param>
 208    /// <returns>A merged dictionary with deep merging of nested dictionaries</returns>
 209    private static Dictionary<string, object?> MergeMetadataDictionaries(
 210        Dictionary<string, object?> existing,
 211        Dictionary<string, object> update)
 212    {
 18213        var result = new Dictionary<string, object?>(existing.Count + update.Count, StringComparer.OrdinalIgnoreCase);
 126214        foreach (var (key, value) in existing) result[key] = value;
 90215        foreach (var (key, newValue) in update)
 216        {
 27217            result[key] = MergedMetadataValue(result, key, newValue);
 218        }
 18219        return result;
 220    }
 221
 222    private static object? MergedMetadataValue(Dictionary<string, object?> target, string key, object newValue)
 223    {
 108224        if (target.TryGetValue(key, out var existingValue)
 108225            && existingValue is Dictionary<string, object?> existingNested
 108226            && newValue is Dictionary<string, object> newNested)
 227        {
 18228            return MergeMetadataDictionaries(existingNested, newNested);
 229        }
 90230        return newValue;
 231    }
 232
 233    private static Dictionary<string, object?> MergeNullableMetadataDictionaries(
 234        Dictionary<string, object?> existing,
 235        Dictionary<string, object?> update)
 236    {
 15237        var result = new Dictionary<string, object?>(existing.Count + update.Count, StringComparer.OrdinalIgnoreCase);
 75238        foreach (var (key, value) in existing) result[key] = value;
 60239        foreach (var (key, newValue) in update)
 240        {
 15241            result[key] = MergedNullableMetadataValue(result, key, newValue);
 242        }
 15243        return result;
 244    }
 245
 246    internal static object? SortArgumentValue(object? value)
 247    {
 15873248        if (value is null) return null;
 30690249        if (ValueFormatter.IsPrimitiveType(value)) return value;
 960250        return SortNonPrimitive(value);
 251    }
 252
 960253    private static object? SortNonPrimitive(object value) => value switch
 960254    {
 774255        IDictionary<string, object?> dict => SortDictionary(dict),
 60256        Array arr => arr.Cast<object>().Select(SortArgumentValue).ToArray(),
 36257        IEnumerable<object> list when !value.GetType().IsArray => SortListItems(list),
 108258        _ => IsDecomposable(value) ? DecomposeToDictionary(value) : value,
 960259    };
 260
 261    private static SortedDictionary<string, object?> SortDictionary(IDictionary<string, object?> dict)
 1929262        => new(dict.ToDictionary(kvp => kvp.Key,
 1155263                                  elementSelector: kvp => SortArgumentValue(kvp.Value),
 774264                                  comparer: StringComparer.OrdinalIgnoreCase),
 774265               comparer: StringComparer.OrdinalIgnoreCase);
 266
 267    /// <summary>True for arbitrary CLR objects whose properties should be reflected into a
 268    /// sorted dictionary. Excludes types we already format/serialize specially.</summary>
 269    private static bool IsDecomposable(object obj)
 108270        => obj is not string and not Variable and not QueryBlock and not IDictionary and not IList;
 271
 272    private static SortedDictionary<string, object?> DecomposeToDictionary(object obj)
 102273        => new(obj.GetType().GetProperties()
 180274                  .OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
 180275                  .ToDictionary(p => p.Name,
 180276                                p => SortArgumentValue(p.GetValue(obj)),
 102277                                comparer: StringComparer.OrdinalIgnoreCase),
 102278               comparer: StringComparer.OrdinalIgnoreCase);
 279
 280    /// <summary>
 281    /// Efficiently sorts list items in a single pass without double allocations.
 282    /// Avoids: .Select().ToList() which creates intermediate IEnumerable + List allocations.
 283    /// </summary>
 284    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 285    private static List<object?> SortListItems(IEnumerable<object> list)
 286    {
 18287        var result = new List<object?>();
 96288        foreach (var item in list)
 289        {
 30290            result.Add(SortArgumentValue(item));
 291        }
 18292        return result;
 293    }
 294
 295    /// <summary>
 296    /// Compares two argument dictionaries for equality.
 297    /// </summary>
 298    /// <param name="args1">First argument dictionary</param>
 299    /// <param name="args2">Second argument dictionary</param>
 300    /// <returns>True if arguments are equal, false otherwise</returns>
 301    internal static bool AreArgumentsEqual(SortedDictionary<string, object?>? args1, SortedDictionary<string, object?>? 
 302    {
 513303        var status = CompareArgumentShapes(args1, args2);
 513304        return status switch
 513305        {
 345306            ArgumentShape.Equal => true,
 24307            ArgumentShape.Mismatch => false,
 144308            _ => ArgumentEntriesMatch(args1!, args2!),
 513309        };
 310    }
 311
 312    private enum ArgumentShape { Equal, Mismatch, NeedsEntryComparison }
 313
 314    private static ArgumentShape CompareArgumentShapes(SortedDictionary<string, object?>? a, SortedDictionary<string, ob
 315    {
 855316        if (ReferenceEquals(a, b)) return ArgumentShape.Equal;
 189317        if (a is null || b is null) return ArgumentShape.Mismatch;
 159318        if (a.Count != b.Count) return ArgumentShape.Mismatch;
 150319        if (a.Count == 0) return ArgumentShape.Equal;
 144320        return ArgumentShape.NeedsEntryComparison;
 321    }
 322
 323    private static bool ArgumentEntriesMatch(SortedDictionary<string, object?> a, SortedDictionary<string, object?> b)
 324    {
 549325        foreach (var (key, value1) in a)
 326        {
 171327            if (!b.TryGetValue(key, out var value2)) return false;
 222328            if (!AreValuesEqual(value1, value2)) return false;
 329        }
 75330        return true;
 69331    }
 332
 333    /// <summary>
 334    /// Compares two values for equality, using optimized comparison strategies.
 335    /// </summary>
 336    /// <param name="value1">First value</param>
 337    /// <param name="value2">Second value</param>
 338    /// <returns>True if values are equal, false otherwise</returns>
 339    private static bool AreValuesEqual(object? value1, object? value2)
 340    {
 453341        if (ReferenceEquals(value1, value2)) return true;
 264342        if (value1 is null || value2 is null) return false;
 343
 258344        var type1 = value1.GetType();
 279345        if (type1 != value2.GetType()) return false;
 346
 351347        if (type1.IsValueType || value1 is string) return value1.Equals(value2);
 348
 123349        return AreReferenceTypedValuesEqual(value1, value2);
 350    }
 351
 352    private static bool AreReferenceTypedValuesEqual(object value1, object value2)
 353    {
 123354        if (value1 is IDictionary<string, object?> dict1 && value2 is IDictionary<string, object?> dict2)
 66355            return AreDictionariesEqual(dict1, dict2);
 356
 57357        if (value1 is IList list1 && value2 is IList list2)
 33358            return AreListsEqual(list1, list2);
 359
 24360        return AreObjectsStructurallyEqual(value1, value2);
 361    }
 362
 363    /// <summary>
 364    /// Optimized dictionary equality comparison
 365    /// </summary>
 366    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 367    private static bool AreDictionariesEqual(IDictionary<string, object?> dict1, IDictionary<string, object?> dict2)
 368    {
 69369        if (dict1.Count != dict2.Count) return false;
 243370        foreach (var kvp in dict1)
 371        {
 75372            if (!dict2.TryGetValue(kvp.Key, out var value2)) return false;
 78373            if (!AreValuesEqual(kvp.Value, value2)) return false;
 374        }
 42375        return true;
 21376    }
 377
 378    /// <summary>
 379    /// Optimized list equality comparison
 380    /// </summary>
 381    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 382    private static bool AreListsEqual(IList list1, IList list2)
 383    {
 33384        var count = list1.Count;
 36385        if (count != list2.Count) return false;
 192386        for (int i = 0; i < count; i++)
 387        {
 84388            if (!AreValuesEqual(list1[i], list2[i])) return false;
 389        }
 21390        return true;
 391    }
 392
 393    /// <summary>
 394    /// Structural equality comparison for complex objects using reflection
 395    /// </summary>
 396    private static bool AreObjectsStructurallyEqual(object obj1, object obj2)
 397    {
 24398        var type = obj1.GetType();
 24399        var properties = type.GetProperties();
 400
 165401        foreach (var property in properties)
 402        {
 60403            var value1 = property.GetValue(obj1);
 60404            var value2 = property.GetValue(obj2);
 405
 60406            if (!AreValuesEqual(value1, value2))
 407            {
 3408                return false;
 409            }
 410        }
 411
 21412        return true;
 413    }
 414
 415    /// <summary>
 416    /// Parses field type from path if provided in format "Type fieldPath".
 417    /// </summary>
 418    /// <param name="fieldPath">Field path to parse</param>
 419    /// <param name="defaultType">Default type to use if none specified</param>
 420    /// <param name="type">Parsed type output</param>
 421    /// <returns>Field path with type removed and trimmed</returns>
 422    internal static ReadOnlySpan<char> ParseFieldTypeFromPath(ReadOnlySpan<char> fieldPath, ReadOnlySpan<char> defaultTy
 423    {
 4908424        var spaceIndex = fieldPath.IndexOf(' ');
 4908425        if (spaceIndex <= 0)
 426        {
 4452427            type = defaultType;
 4452428            return fieldPath.TrimEndDotsAndSpaces();
 429        }
 430
 456431        var potentialType = fieldPath[..spaceIndex];
 456432        if (LooksLikeTypeAnnotation(potentialType))
 433        {
 426434            type = potentialType;
 426435            fieldPath = fieldPath[(spaceIndex + 1)..];
 436        }
 437        else
 438        {
 30439            type = defaultType;
 440        }
 456441        return fieldPath.TrimEndDotsAndSpaces();
 442    }
 443
 444    // Type annotation must start with a letter or '[', contain no dots, and either include a
 445    // letter/digit OR be the bare "[]" array marker.
 446    private static bool LooksLikeTypeAnnotation(ReadOnlySpan<char> candidate)
 456447        => candidate.Length > 0
 456448        && (char.IsLetter(candidate[0]) || candidate[0] == '[')
 456449        && candidate.IndexOf('.') < 0
 456450        && (candidate.HasLetterOrDigit() || candidate.SequenceEqual("[]".AsSpan()));
 451
 452    /// <summary>
 453    /// Creates a new FieldDefinition with sorted arguments for consistent behavior.
 454    /// Arguments are passed by reference to avoid unnecessary copying of potentially large dictionaries.
 455    /// <param name="name">Field name</param>
 456    /// <param name="type">Field type</param>
 457    /// <param name="alias">Optional field alias</param>
 458    /// <param name="arguments">Field arguments (passed by reference for performance)</param>
 459    /// <param name="path">Field path for caching</param>
 460    /// <param name="metadata">Optional field metadata</param>
 461    /// <returns>New FieldDefinition instance</returns>
 462    /// </summary>
 463    internal static FieldDefinition CreateFieldDefinition(ReadOnlySpan<char> name, ReadOnlySpan<char> type, ReadOnlySpan
 464    {
 465        // Use type interning for memory efficiency
 43335466        var nameStr = name.ToString();
 43335467        var typeStr = Caching.TypeCache.GetInternedType(type);
 43335468        var aliasStr = alias.IsEmpty ? null : alias.ToString();
 43335469        var pathStr = path.ToString();
 470
 471        // FAST PATH: Skip dictionary operations when arguments are empty or null
 43335472        if (arguments?.Count == 0 || arguments == null)
 473        {
 36249474            return new FieldDefinition(nameStr, typeStr, aliasStr, null)
 36249475            {
 36249476                Path = pathStr,
 36249477                _metadata = metadata
 36249478            };
 479        }
 480
 481        // Create a new sorted dictionary to ensure consistent argument ordering
 7086482        var sortedArguments = new SortedDictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 42432483        foreach (var kvp in arguments)
 484        {
 14130485            sortedArguments[kvp.Key] = SortArgumentValue(kvp.Value);
 486        }
 487
 7086488        return new FieldDefinition(nameStr, typeStr, aliasStr, sortedArguments)
 7086489        {
 7086490            Path = pathStr,
 7086491            _metadata = metadata
 7086492        };
 493    }
 494
 495    /// <summary>
 496    /// Finds an existing field in the collection by name, alias, or path.
 497    /// </summary>
 498    /// <param name="fields">Field collection to search</param>
 499    /// <param name="fieldDefinition">Field definition to find</param>
 500    /// <returns>Existing field if found, null otherwise</returns>
 501    internal static FieldDefinition? FindExistingField(Dictionary<string, FieldDefinition> fields, FieldDefinition field
 502    {
 81503        FieldDefinition? existingField = null;
 225504        foreach (var f in fields.Values)
 505        {
 51506            if (f.Name == fieldDefinition.Name && f._alias == fieldDefinition._alias)
 507            {
 39508                existingField = f;
 39509                break;
 510            }
 511        }
 81512        return existingField ?? fields.GetValueOrDefault(fieldDefinition.Path);
 513    }
 514
 515    internal static FieldDefinition? FindExistingField(FieldChildren children, FieldDefinition fieldDefinition)
 516    {
 594517        foreach (var f in children.AsSpan())
 518        {
 111519            if (f.Name == fieldDefinition.Name && f._alias == fieldDefinition._alias)
 30520                return f;
 521        }
 171522        return children.Find(fieldDefinition.Path.AsSpan());
 523    }
 524
 525    /// <summary>
 526    /// Validates that a field name conforms to GraphQL identifier rules: [_A-Za-z][_0-9A-Za-z]*
 527    /// Handles type-annotation prefixes (e.g. "[User!]! user" → validates "user") and
 528    /// alias prefixes (e.g. "alias:name" → validates both "alias" and "name").
 529    /// For dotted paths, validate each segment individually before calling this method.
 530    /// </summary>
 531    internal static void ValidateFieldName(ReadOnlySpan<char> name)
 532    {
 3918533        if (name.IsEmpty)
 3534            throw new ArgumentException("Field name cannot be empty.");
 535
 536        // Strip type annotation prefix: take identifier part after the last space
 3915537        var spaceIndex = name.LastIndexOf(' ');
 3915538        var identifier = spaceIndex >= 0 ? name[(spaceIndex + 1)..].Trim() : name.Trim();
 539
 540        // Handle alias:name syntax
 3915541        var colonIndex = identifier.IndexOf(':');
 3915542        if (colonIndex >= 0)
 543        {
 144544            var alias = identifier[..colonIndex].Trim();
 144545            var fieldName = identifier[(colonIndex + 1)..].Trim();
 285546            if (!alias.IsEmpty) ValidateIdentifier(alias);
 276547            if (!fieldName.IsEmpty) ValidateIdentifier(fieldName);
 129548            return;
 549        }
 550
 3771551        ValidateIdentifier(identifier);
 3759552    }
 553
 554    private static void ValidateIdentifier(ReadOnlySpan<char> name)
 555    {
 4050556        if (name.IsEmpty)
 3557            throw new ArgumentException("Field name cannot be empty.");
 558
 4047559        if (!IsValidGraphQlNameStart(name[0]))
 9560            throw new ArgumentException($"Invalid GraphQL field name '{name.ToString()}': must start with a letter or un
 561
 40812562        for (int i = 1; i < name.Length; i++)
 563        {
 16383564            if (!IsValidGraphQlNameChar(name[i]))
 15565                throw new ArgumentException($"Invalid GraphQL field name '{name.ToString()}': contains invalid character
 566        }
 4023567    }
 568
 569    private static bool IsValidGraphQlNameStart(char c)
 4047570        => c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
 571
 572    private static bool IsValidGraphQlNameChar(char c)
 16383573        => c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
 574
 575    /// <summary>
 576    /// Writes a collection with specified prefix/suffix characters and custom item writer
 577    /// </summary>
 578    internal static void WriteCollection(char prefix, char suffix, IEnumerable list, StringBuilder builder, Action<Strin
 579    {
 954580        builder.Append(prefix);
 581
 954582        bool first = true;
 5016583        foreach (var obj in list)
 584        {
 1554585            if (!first)
 586            {
 621587                builder.Append(", ");
 588            }
 589
 1554590            first = false;
 1554591            itemWriter(builder, obj);
 592        }
 593
 954594        builder.Append(suffix);
 954595    }
 596}

Methods/Properties

ExtractVariablesFromValue(System.Object,System.Collections.Generic.SortedSet`1<NGql.Core.Variable>)
ExtractVariablesFromValueCore(System.Object,System.Collections.Generic.SortedSet`1<NGql.Core.Variable>,System.Collections.Generic.HashSet`1<System.Object>)
ExtractVariablesFromDictionary(System.Collections.IDictionary,System.Collections.Generic.SortedSet`1<NGql.Core.Variable>,System.Collections.Generic.HashSet`1<System.Object>)
ExtractVariablesFromList(System.Collections.IList,System.Collections.Generic.SortedSet`1<NGql.Core.Variable>,System.Collections.Generic.HashSet`1<System.Object>)
ShouldExtractFromObjectProperties(System.Object)
ExtractVariablesFromObjectProperties(System.Object,System.Collections.Generic.SortedSet`1<NGql.Core.Variable>,System.Collections.Generic.HashSet`1<System.Object>)
MergeNullableDictionaries(System.Collections.Generic.IDictionary`2<System.String,System.Object>,System.Collections.Generic.IDictionary`2<System.String,System.Object>)
MergeMetadata(System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
ConvertToNullable(System.Collections.Generic.Dictionary`2<System.String,System.Object>)
MergeNullableMetadata(System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
MergedNullableMetadataValue(System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.String,System.Object)
MergeMetadataDictionaries(System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
MergedMetadataValue(System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.String,System.Object)
MergeNullableMetadataDictionaries(System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
SortArgumentValue(System.Object)
SortNonPrimitive(System.Object)
SortDictionary(System.Collections.Generic.IDictionary`2<System.String,System.Object>)
IsDecomposable(System.Object)
DecomposeToDictionary(System.Object)
SortListItems(System.Collections.Generic.IEnumerable`1<System.Object>)
AreArgumentsEqual(System.Collections.Generic.SortedDictionary`2<System.String,System.Object>,System.Collections.Generic.SortedDictionary`2<System.String,System.Object>)
CompareArgumentShapes(System.Collections.Generic.SortedDictionary`2<System.String,System.Object>,System.Collections.Generic.SortedDictionary`2<System.String,System.Object>)
ArgumentEntriesMatch(System.Collections.Generic.SortedDictionary`2<System.String,System.Object>,System.Collections.Generic.SortedDictionary`2<System.String,System.Object>)
AreValuesEqual(System.Object,System.Object)
AreReferenceTypedValuesEqual(System.Object,System.Object)
AreDictionariesEqual(System.Collections.Generic.IDictionary`2<System.String,System.Object>,System.Collections.Generic.IDictionary`2<System.String,System.Object>)
AreListsEqual(System.Collections.IList,System.Collections.IList)
AreObjectsStructurallyEqual(System.Object,System.Object)
ParseFieldTypeFromPath(System.ReadOnlySpan`1<System.Char>,System.ReadOnlySpan`1<System.Char>,System.ReadOnlySpan`1<System.Char>&)
LooksLikeTypeAnnotation(System.ReadOnlySpan`1<System.Char>)
CreateFieldDefinition(System.ReadOnlySpan`1<System.Char>,System.ReadOnlySpan`1<System.Char>,System.ReadOnlySpan`1<System.Char>,System.Collections.Generic.IDictionary`2<System.String,System.Object>,System.ReadOnlySpan`1<System.Char>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
FindExistingField(System.Collections.Generic.Dictionary`2<System.String,NGql.Core.Abstractions.FieldDefinition>,NGql.Core.Abstractions.FieldDefinition)
FindExistingField(NGql.Core.Abstractions.FieldChildren,NGql.Core.Abstractions.FieldDefinition)
ValidateFieldName(System.ReadOnlySpan`1<System.Char>)
ValidateIdentifier(System.ReadOnlySpan`1<System.Char>)
IsValidGraphQlNameStart(System.Char)
IsValidGraphQlNameChar(System.Char)
WriteCollection(System.Char,System.Char,System.Collections.IEnumerable,System.Text.StringBuilder,System.Action`2<System.Text.StringBuilder,System.Object>)