< Summary

Information
Class: NGql.Core.Extensions.QueryBlockObjectExtensions
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Extensions/QueryBlockObjectExtensions.cs
Line coverage
100%
Covered lines: 78
Uncovered lines: 0
Coverable lines: 78
Total lines: 208
Line coverage: 100%
Branch coverage
100%
Covered branches: 40
Total branches: 40
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
IncludeAtPath(...)100%88100%
Include(...)100%11100%
Include(...)100%11100%
GetSelectableProperties(...)100%88100%
IsMoreDerivedThan(...)100%22100%
HandleProperties(...)100%22100%
HandleProperty(...)100%88100%
AddSimpleProperty(...)100%44100%
HandleDictionary(...)100%44100%
.cctor()100%11100%
IsSimpleType(...)100%44100%

File(s)

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

#LineLine coverage
 1using System.Collections;
 2using System.Reflection;
 3using NGql.Core.Abstractions;
 4
 5namespace NGql.Core.Extensions;
 6
 7internal static class QueryBlockObjectExtensions
 8{
 9    /// <summary>
 10    /// Adds the given type properties into <see cref="QueryBlock.FieldsList"/> part of the query.
 11    /// </summary>
 12    /// <param name="block">The query block</param>
 13    /// <param name="path">The path to use</param>
 14    /// <param name="name">The name to use</param>
 15    /// <param name="alias">The alias to use</param>
 16    /// <typeparam name="T">The type to include</typeparam>
 17    public static void IncludeAtPath<T>(this QueryBlock block, string path, string name, string? alias = null)
 18    {
 19        // Use Span to avoid string allocations during path parsing
 1820        var pathSpan = path.AsSpan();
 1821        var currentBlock = block;
 22
 5123        while (!pathSpan.IsEmpty)
 24        {
 3325            var dotIndex = pathSpan.IndexOf('.');
 26            ReadOnlySpan<char> currentSegment;
 27
 3328            if (dotIndex == -1)
 29            {
 30                // Last segment
 1231                currentSegment = pathSpan;
 1232                pathSpan = ReadOnlySpan<char>.Empty;
 33            }
 34            else
 35            {
 36                // Extract current segment and advance
 2137                currentSegment = pathSpan[..dotIndex];
 2138                pathSpan = pathSpan[(dotIndex + 1)..];
 39            }
 40
 41            // Skip empty segments
 3342            if (currentSegment.IsEmpty || currentSegment.IsWhiteSpace())
 43            {
 44                continue;
 45            }
 46
 47            // Convert to string only when creating the QueryBlock
 2748            var segmentString = currentSegment.ToString();
 2749            var subQuery = new QueryBlock(segmentString);
 2750            currentBlock.AddField(subQuery);
 2751            currentBlock = subQuery;
 52        }
 53
 1854        currentBlock.Include<T>(name, alias);
 1855    }
 56
 57    /// <summary>
 58    /// Adds the given type properties into <see cref="QueryBlock.FieldsList"/> part of the query.
 59    /// </summary>
 60    /// <param name="block">The query block</param>
 61    /// <param name="name">The name to use</param>
 62    /// <param name="alias"></param>
 63    /// <returns>Query</returns>
 64    public static void Include<T>(this QueryBlock block, string name, string? alias = null)
 65    {
 3666        var properties = GetSelectableProperties(typeof(T));
 67
 3668        var subQuery = new QueryBlock(name, alias: alias);
 69
 3670        HandleProperties(subQuery, null, properties);
 71
 3672        block.AddField(subQuery);
 3673    }
 74
 75    /// <summary>
 76    /// Adds the given object properties into <see cref="QueryBlock.FieldsList"/> part of the query.
 77    /// </summary>
 78    /// <param name="block">The query block</param>
 79    /// <param name="obj">A value</param>
 80    /// <returns>Query</returns>
 81    public static void Include(this QueryBlock block, object obj)
 82    {
 2183        var type = obj.GetType();
 2184        var properties = GetSelectableProperties(type);
 85
 2186        HandleProperties(block, obj, properties);
 2187    }
 88
 89    /// <summary>
 90    /// Returns the properties of <paramref name="type"/> that map to GraphQL selection fields:
 91    /// indexer properties are excluded (they are not addressable as fields and would throw when
 92    /// read without index arguments), and <c>new</c>-shadowed properties are collapsed to their
 93    /// most-derived declaration so a response name is never emitted twice. Ordering matches the
 94    /// original alphabetical (case-insensitive) sort so normal types render unchanged.
 95    /// </summary>
 96    private static PropertyInfo[] GetSelectableProperties(Type type)
 97    {
 98        // A name may appear more than once when a derived type re-declares a base property with
 99        // `new`; GetProperties() returns every declaration and does not guarantee derived-first
 100        // ordering, so keep the declaration whose DeclaringType is furthest down the hierarchy.
 57101        var byName = new Dictionary<string, PropertyInfo>(StringComparer.Ordinal);
 102
 396103        foreach (var property in type.GetProperties())
 104        {
 141105            if (property.GetIndexParameters().Length > 0)
 106            {
 107                continue;
 108            }
 109
 135110            if (!byName.TryGetValue(property.Name, out var existing) || IsMoreDerivedThan(property, existing))
 111            {
 132112                byName[property.Name] = property;
 113            }
 114        }
 115
 57116        return byName.Values
 124117            .OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
 57118            .ToArray();
 119    }
 120
 121    /// <summary>
 122    /// True when <paramref name="candidate"/> is declared further down the hierarchy than
 123    /// <paramref name="current"/>. <c>GetProperties()</c> happens to return most-derived
 124    /// declarations first on the runtimes NGql targets, but that ordering is not contractual, so
 125    /// <see cref="GetSelectableProperties"/> compares rather than trusting position. Internal so
 126    /// both orderings stay directly testable. Both arguments come from
 127    /// <c>Type.GetProperties()</c>, whose results always carry a non-null DeclaringType.
 128    /// </summary>
 129    internal static bool IsMoreDerivedThan(PropertyInfo candidate, PropertyInfo current)
 12130        => candidate.DeclaringType != current.DeclaringType
 12131           && current.DeclaringType!.IsAssignableFrom(candidate.DeclaringType);
 132
 133    private static void HandleProperties(QueryBlock block, object? obj, PropertyInfo[] properties)
 134    {
 378135        foreach (var property in properties)
 136        {
 132137            HandleProperty(block, obj, property);
 138        }
 57139    }
 140
 141    private static void HandleProperty(QueryBlock block, object? obj, PropertyInfo property)
 142    {
 132143        var value = obj is null ? null : property.GetValue(obj);
 132144        var alias = property.GetAlias();
 145
 132146        if (value is IDictionary dict)
 147        {
 3148            HandleDictionary(block, property.Name, alias, dict);
 3149            return;
 150        }
 129151        if (value is not null && !IsSimpleType(value.GetType()))
 152        {
 6153            var subQuery = new QueryBlock(property.Name, alias: alias);
 6154            subQuery.Include(value);
 6155            block.AddField(subQuery);
 6156            return;
 157        }
 123158        AddSimpleProperty(block, property.Name, alias);
 123159    }
 160
 161    private static void AddSimpleProperty(QueryBlock block, string propertyName, string? alias)
 162    {
 123163        if (alias == propertyName)
 164        {
 27165            block.AddField(propertyName);
 27166            return;
 167        }
 96168        block.AddField(alias is null
 96169            ? new QueryBlock(propertyName) { IsEmpty = true }
 96170            : new QueryBlock(alias, alias: propertyName) { IsEmpty = true });
 96171    }
 172
 173    private static void HandleDictionary(QueryBlock block, string name, string? alias, IDictionary dict)
 174    {
 6175        var subQuery = new QueryBlock(name, alias: alias);
 6176        var sortedKeys = dict.Keys.Cast<object>()
 18177            .OrderBy(k => k.ToString(), StringComparer.OrdinalIgnoreCase);
 178
 36179        foreach (var key in sortedKeys)
 180        {
 12181            if (dict[key] is IDictionary nestedDict)
 182            {
 3183                HandleDictionary(subQuery, key.ToString()!, null, nestedDict);
 184            }
 185            else
 186            {
 9187                subQuery.AddField(key.ToString()!);
 188            }
 189        }
 190
 6191        block.AddField(subQuery);
 6192    }
 193
 3194    private static readonly HashSet<Type> SimpleTypes =
 3195    [
 3196        typeof(string),
 3197        typeof(decimal),
 3198        typeof(DateTime),
 3199        typeof(DateTimeOffset),
 3200        typeof(TimeSpan),
 3201        typeof(Guid)
 3202    ];
 203
 204    private static bool IsSimpleType(Type type)
 33205        => type.IsPrimitive ||
 33206            SimpleTypes.Contains(type) ||
 33207            Convert.GetTypeCode(type) != TypeCode.Object;
 208}