< Summary

Information
Class: NGql.Core.Builders.NavigationPropertyExpander
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Builders/NavigationPropertyExpander.cs
Line coverage
100%
Covered lines: 35
Uncovered lines: 0
Coverable lines: 35
Total lines: 121
Line coverage: 100%
Branch coverage
100%
Covered branches: 22
Total branches: 22
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ExpandNavigationProperty(...)100%66100%
SplitPath(...)100%22100%
IsNavigationProperty(...)100%22100%
ExpandNavigationPropertyFields(...)100%66100%
HandleRegularProperty(...)100%66100%

File(s)

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

#LineLine coverage
 1using System.Reflection;
 2using NGql.Core.Caching;
 3
 4namespace NGql.Core.Builders;
 5
 6/// <summary>
 7/// Handles expansion of navigation properties (getter-only computed properties)
 8/// to their underlying settable properties.
 9/// </summary>
 10internal static class NavigationPropertyExpander
 11{
 12    /// <summary>
 13    /// Expands a field name if it's a navigation property (getter-only computed property).
 14    /// For navigation properties, returns all settable properties from the parameter type.
 15    /// Handles nested paths like "profile.name" by only checking the first segment.
 16    /// </summary>
 17    public static HashSet<string> ExpandNavigationProperty(string fieldName, Type? parameterType)
 18    {
 45019        var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 20
 45021        if (parameterType == null)
 22        {
 3023            result.Add(fieldName);
 3024            return result;
 25        }
 26
 27        try
 28        {
 29            // Handle nested paths - only check the first segment for navigation properties
 42030            var (firstSegment, remainingPath) = SplitPath(fieldName);
 31
 32            // Get the property from the type (only first segment) via cached metadata
 42033            var property = TypeMetadataCache.GetNavigationProperties(parameterType).GetProperty(firstSegment);
 41434            if (property == null)
 35            {
 11736                result.Add(fieldName);
 11737                return result;
 38            }
 39
 40            // Check if this is a navigation property (getter-only, no setter)
 29741            if (IsNavigationProperty(property))
 42            {
 5443                ExpandNavigationPropertyFields(parameterType, remainingPath, result);
 44            }
 45            else
 46            {
 24347                HandleRegularProperty(fieldName, firstSegment, remainingPath, property, result);
 48            }
 29749        }
 650        catch (AmbiguousMatchException)
 51        {
 52            // The first segment names a property shadowed by a `new` declaration, so there is no
 53            // single property to expand from — fall back to the original field name.
 654            result.Add(fieldName);
 655        }
 56
 30357        return result;
 11758    }
 59
 60    private static (string FirstSegment, string? RemainingPath) SplitPath(string fieldName)
 61    {
 42062        var dotIndex = fieldName.IndexOf('.');
 42063        if (dotIndex > 0)
 64        {
 9665            return (fieldName.Substring(0, dotIndex), fieldName.Substring(dotIndex + 1));
 66        }
 32467        return (fieldName, null);
 68    }
 69
 70    private static bool IsNavigationProperty(PropertyInfo property)
 71    {
 72        // Properties surfaced by GetProperty(BindingFlags.Public | BindingFlags.Instance)
 73        // always have a public getter, so GetGetMethod() never returns null in this scope.
 29774        return property.SetMethod == null && property.GetGetMethod()!.IsPublic;
 75    }
 76
 77    private static void ExpandNavigationPropertyFields(Type parameterType, string? remainingPath, HashSet<string> result
 78    {
 79        // This is a navigation property - get all SETTABLE properties
 44480        foreach (var prop in TypeMetadataCache.GetNavigationProperties(parameterType).Properties)
 81        {
 82            // Skip navigation properties themselves (getter-only)
 16883            if (prop.SetMethod != null)
 84            {
 85                // If there was a remaining path, append it to each expanded property
 11486                if (remainingPath != null)
 87                {
 1888                    result.Add($"{prop.Name}.{remainingPath}");
 89                }
 90                else
 91                {
 9692                    result.Add(prop.Name);
 93                }
 94            }
 95        }
 5496    }
 97
 98    private static void HandleRegularProperty(
 99        string fieldName,
 100        string firstSegment,
 101        string? remainingPath,
 102        PropertyInfo property,
 103        HashSet<string> result)
 104    {
 105        // Not a navigation property - check if we need to recurse for nested path
 243106        if (remainingPath != null && property.PropertyType != null)
 107        {
 108            // Recurse on the remaining path with the property's type
 60109            var nestedExpanded = ExpandNavigationProperty(remainingPath, property.PropertyType);
 300110            foreach (var nestedField in nestedExpanded)
 111            {
 90112                result.Add($"{firstSegment}.{nestedField}");
 113            }
 114        }
 115        else
 116        {
 117            // Just return the field name
 183118            result.Add(fieldName);
 119        }
 243120    }
 121}