< 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: 38
Uncovered lines: 0
Coverable lines: 38
Total lines: 124
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;
 2
 3namespace NGql.Core.Builders;
 4
 5/// <summary>
 6/// Handles expansion of navigation properties (getter-only computed properties)
 7/// to their underlying settable properties.
 8/// </summary>
 9internal static class NavigationPropertyExpander
 10{
 11    /// <summary>
 12    /// Expands a field name if it's a navigation property (getter-only computed property).
 13    /// For navigation properties, returns all settable properties from the parameter type.
 14    /// Handles nested paths like "profile.name" by only checking the first segment.
 15    /// </summary>
 16    public static HashSet<string> ExpandNavigationProperty(string fieldName, Type? parameterType)
 17    {
 42018        var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 19
 42020        if (parameterType == null)
 21        {
 3022            result.Add(fieldName);
 3023            return result;
 24        }
 25
 26        try
 27        {
 28            // Handle nested paths - only check the first segment for navigation properties
 39029            var (firstSegment, remainingPath) = SplitPath(fieldName);
 30
 31            // Get the property from the type (only first segment)
 39032            var property = parameterType.GetProperty(firstSegment, BindingFlags.Public | BindingFlags.Instance);
 37533            if (property == null)
 34            {
 10835                result.Add(fieldName);
 10836                return result;
 37            }
 38
 39            // Check if this is a navigation property (getter-only, no setter)
 26740            if (IsNavigationProperty(property))
 41            {
 5142                ExpandNavigationPropertyFields(parameterType, remainingPath, result);
 43            }
 44            else
 45            {
 21646                HandleRegularProperty(fieldName, firstSegment, remainingPath, property, result);
 47            }
 26748        }
 1249        catch (InvalidOperationException)
 50        {
 51            // Reflection failed due to ambiguous or invalid operation
 1252            result.Add(fieldName);
 1253        }
 354        catch (AmbiguousMatchException)
 55        {
 56            // Multiple matches found for property - use original field name
 357            result.Add(fieldName);
 358        }
 59
 28260        return result;
 10861    }
 62
 63    private static (string FirstSegment, string? RemainingPath) SplitPath(string fieldName)
 64    {
 39065        var dotIndex = fieldName.IndexOf('.');
 39066        if (dotIndex > 0)
 67        {
 9068            return (fieldName.Substring(0, dotIndex), fieldName.Substring(dotIndex + 1));
 69        }
 30070        return (fieldName, null);
 71    }
 72
 73    private static bool IsNavigationProperty(PropertyInfo property)
 74    {
 75        // Properties surfaced by GetProperty(BindingFlags.Public | BindingFlags.Instance)
 76        // always have a public getter, so GetGetMethod() never returns null in this scope.
 26777        return property.SetMethod == null && property.GetGetMethod()!.IsPublic;
 78    }
 79
 80    private static void ExpandNavigationPropertyFields(Type parameterType, string? remainingPath, HashSet<string> result
 81    {
 82        // This is a navigation property - get all SETTABLE properties
 41483        foreach (var prop in parameterType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
 84        {
 85            // Skip navigation properties themselves (getter-only)
 15686            if (prop.SetMethod != null)
 87            {
 88                // If there was a remaining path, append it to each expanded property
 10589                if (remainingPath != null)
 90                {
 1891                    result.Add($"{prop.Name}.{remainingPath}");
 92                }
 93                else
 94                {
 8795                    result.Add(prop.Name);
 96                }
 97            }
 98        }
 5199    }
 100
 101    private static void HandleRegularProperty(
 102        string fieldName,
 103        string firstSegment,
 104        string? remainingPath,
 105        PropertyInfo property,
 106        HashSet<string> result)
 107    {
 108        // Not a navigation property - check if we need to recurse for nested path
 216109        if (remainingPath != null && property.PropertyType != null)
 110        {
 111            // Recurse on the remaining path with the property's type
 57112            var nestedExpanded = ExpandNavigationProperty(remainingPath, property.PropertyType);
 288113            foreach (var nestedField in nestedExpanded)
 114            {
 87115                result.Add($"{firstSegment}.{nestedField}");
 116            }
 117        }
 118        else
 119        {
 120            // Just return the field name
 159121            result.Add(fieldName);
 122        }
 216123    }
 124}