< Summary

Information
Class: NGql.Core.Features.FieldSignatureGenerator
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Features/FieldSignatureGenerator.cs
Line coverage
100%
Covered lines: 75
Uncovered lines: 0
Coverable lines: 75
Total lines: 195
Line coverage: 100%
Branch coverage
100%
Covered branches: 36
Total branches: 36
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
GenerateSignature(...)100%88100%
AppendFieldSignature(...)100%66100%
AppendFieldSignatureRemainder(...)100%1212100%
AppendArgumentValue(...)100%44100%
AppendComplexValue(...)100%66100%
AppendDictionary(...)100%11100%
AppendEnumerable(...)100%11100%

File(s)

/home/runner/work/NGql/NGql/src/Core/Features/FieldSignatureGenerator.cs

#LineLine coverage
 1using System.Buffers;
 2using System.Collections;
 3using System.Text;
 4using NGql.Core.Abstractions;
 5using NGql.Core.Extensions;
 6
 7namespace NGql.Core.Features;
 8
 9/// <summary>
 10/// Generates unique signatures for field definitions based on their arguments/filters.
 11/// Optimized for performance using Span operations and reduced string allocations.
 12/// </summary>
 13public static class FieldSignatureGenerator
 14{
 15    // Pre-allocated StringBuilder for signature generation to avoid repeated allocations
 616    private static readonly ThreadLocal<StringBuilder> SignatureBuilder = new(() => new StringBuilder(256));
 17
 18    // Singleton comparer for sorting by Name for deterministic signature generation.
 319    private static readonly IComparer<FieldDefinition> FieldNameComparer =
 7820        Comparer<FieldDefinition>.Create(static (a, b) => StringComparer.OrdinalIgnoreCase.Compare(a.Name, b.Name));
 21
 22    /// <summary>
 23    /// Generates a unique hash signature for a collection of field definitions.
 24    /// </summary>
 25    /// <param name="fields">The field definitions to generate signature for.</param>
 26    /// <returns>A unique hash representing the filter signature of the fields.</returns>
 27    public static int GenerateSignature(Dictionary<string, FieldDefinition> fields)
 28    {
 23429        if (fields.Count == 0)
 30        {
 631            return 0;
 32        }
 33
 22834        var builder = SignatureBuilder.Value!;
 22835        builder.Clear();
 36
 102637        foreach (var field in fields.Values)
 38        {
 28539            AppendFieldSignature(builder, field, ReadOnlySpan<char>.Empty);
 40        }
 41
 42        // builder.Length is always > 0 here: the empty-fields case was handled above and
 43        // every field appends at least its name to the signature.
 44        // Hash directly over StringBuilder chunks — avoids allocating a temporary string.
 45        unchecked
 46        {
 22847            int hash = 5381;
 92448            foreach (var chunk in builder.GetChunks())
 49            {
 5926850                foreach (var c in chunk.Span)
 2940051                    hash = (hash << 5) + hash + c; // djb2: hash*33 + c
 52            }
 22853            return hash;
 54        }
 55    }
 56
 57    /// <summary>
 58    /// Appends field signature to the builder using Span operations for efficient path construction.
 59    /// </summary>
 60    /// <param name="builder">StringBuilder to append to</param>
 61    /// <param name="field">Field definition to process</param>
 62    /// <param name="parentPath">Parent path as ReadOnlySpan to avoid string allocations</param>
 63    private static void AppendFieldSignature(StringBuilder builder, FieldDefinition field, ReadOnlySpan<char> parentPath
 64    {
 65        // Build the current path efficiently using Span operations
 50466        Span<char> currentPathBuffer = stackalloc char[256]; // Stack allocation for path building
 50467        int pathLength = 0;
 68
 50469        if (!parentPath.IsEmpty)
 70        {
 21971            parentPath.CopyTo(currentPathBuffer);
 21972            pathLength = parentPath.Length;
 21973            currentPathBuffer[pathLength++] = '.';
 74        }
 75
 50476        var fieldNameSpan = field.Name.AsSpan();
 50477        if (pathLength + fieldNameSpan.Length < currentPathBuffer.Length)
 78        {
 48079            fieldNameSpan.CopyTo(currentPathBuffer[pathLength..]);
 48080            pathLength += fieldNameSpan.Length;
 81        }
 82        else
 83        {
 84            // Fallback to string concatenation for very long paths.
 2485            var currentPath = parentPath.IsEmpty ? field.Name : $"{parentPath.ToString()}.{field.Name}";
 2486            builder.Append(currentPath);
 2487            AppendFieldSignatureRemainder(builder, field, currentPath.AsSpan());
 2488            return;
 89        }
 90
 48091        var currentPathSpan = currentPathBuffer[..pathLength];
 92
 93        // Append the path to the signature
 48094        builder.Append(currentPathSpan);
 95
 48096        AppendFieldSignatureRemainder(builder, field, currentPathSpan);
 48097    }
 98
 99    /// <summary>
 100    /// Appends the remainder of the field signature (arguments and child fields).
 101    /// </summary>
 102    /// <param name="builder">StringBuilder to append to</param>
 103    /// <param name="field">Field definition to process</param>
 104    /// <param name="currentPath">Current path (either as span or string)</param>
 105    private static void AppendFieldSignatureRemainder(StringBuilder builder, FieldDefinition field, ReadOnlySpan<char> c
 106    {
 107        // Add arguments if present
 504108        if (field._arguments is { Count: > 0 })
 109        {
 129110            builder.Append('[');
 111            // _arguments is SortedDictionary — already ordered by key, no OrderBy needed.
 1302112            foreach (var arg in field._arguments)
 113            {
 522114                builder.Append(arg.Key);
 522115                builder.Append(':');
 522116                AppendArgumentValue(builder, arg.Value);
 522117                builder.Append(';');
 118            }
 129119            builder.Append(']');
 120        }
 121
 504122        builder.Append('|');
 123
 124        // Recursively process child fields sorted by Name for a deterministic hash.
 125        // Dictionary<TKey,TValue> is unordered; explicit sort ensures signature stability.
 504126        if (field._children is { Count: > 0 })
 127        {
 153128            var fieldCount = field._children.Count;
 153129            var rented = ArrayPool<FieldDefinition>.Shared.Rent(fieldCount);
 130            try
 131            {
 153132                field._children.AsSpan().CopyTo(rented);
 153133                Array.Sort(rented, 0, fieldCount, FieldNameComparer);
 744134                for (var i = 0; i < fieldCount; i++)
 219135                    AppendFieldSignature(builder, rented[i], currentPath);
 153136            }
 137            finally
 138            {
 153139                ArrayPool<FieldDefinition>.Shared.Return(rented, clearArray: false);
 153140            }
 141        }
 504142    }
 143
 144
 145    /// <summary>
 146    /// Appends argument value to the signature with optimized handling for common types.
 147    /// </summary>
 148    /// <param name="builder">StringBuilder to append to</param>
 149    /// <param name="value">Argument value to append</param>
 150    private static void AppendArgumentValue(StringBuilder builder, object? value)
 151    {
 582152        if (value is null)
 153        {
 3154            builder.Append("null");
 3155            return;
 156        }
 157
 579158        if (ValueFormatter.TryAppendPrimitive(value, builder))
 549159            return;
 160
 30161        AppendComplexValue(builder, value);
 30162    }
 163
 164    private static void AppendComplexValue(StringBuilder builder, object value)
 165    {
 30166        switch (value)
 167        {
 168            case IDictionary<string, object?> dict:
 15169                AppendDictionary(builder, dict);
 15170                break;
 12171            case IEnumerable enumerable when value is not string:
 12172                AppendEnumerable(builder, enumerable);
 12173                break;
 174            default:
 3175                builder.Append(value);
 176                break;
 177        }
 3178    }
 179
 180    private static void AppendDictionary(StringBuilder builder, IDictionary<string, object?> dict)
 181    {
 182        // Argument dictionaries reach here through Helpers.SortArgumentValue, which converts
 183        // every nested dict to SortedDictionary; the IOrderedEnumerable fallback for plain
 184        // Dictionary instances was a defensive guard that the public API never triggers.
 15185        Helpers.WriteCollection('{', '}', dict, builder, (sb, item) =>
 15186        {
 24187            var kvp = (KeyValuePair<string, object?>)item!;
 24188            sb.Append(kvp.Key).Append(':');
 24189            AppendArgumentValue(sb, kvp.Value);
 39190        });
 15191    }
 192
 193    private static void AppendEnumerable(StringBuilder builder, IEnumerable enumerable)
 48194        => Helpers.WriteCollection('[', ']', enumerable, builder, (sb, item) => AppendArgumentValue(sb, item));
 195}