< Summary

Information
Class: NGql.Core.Builders.ExpressionPreservationProcessor
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Builders/ExpressionPreservationProcessor.cs
Line coverage
100%
Covered lines: 177
Uncovered lines: 0
Coverable lines: 177
Total lines: 496
Line coverage: 100%
Branch coverage
97%
Covered branches: 140
Total branches: 144
Branch coverage: 97.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

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

#LineLine coverage
 1using System.Linq.Expressions;
 2using System.Reflection;
 3using System.Runtime.CompilerServices;
 4using NGql.Core.Abstractions;
 5using NGql.Core.Extensions;
 6using NGql.Core.Features;
 7
 8namespace NGql.Core.Builders;
 9
 10/// <summary>
 11/// Processes expressions to extract and preserve field paths from lambda expressions.
 12/// Optimized for hotpath usage with minimal allocations.
 13/// </summary>
 17714internal sealed class ExpressionPreservationProcessor(QueryBuilder sourceQuery, Action<string> preserveCallback)
 15{
 16    /// <summary>
 17    /// Processes an expression and preserves the referenced fields.
 18    /// </summary>
 19    public void ProcessExpression(
 20        Expression expression,
 21        string? nodePath,
 22        Dictionary<string, string[]>? localMap,
 23        Type? parameterType,
 24        string[]? alwaysPreserveFields)
 25    {
 17726        var extractedPaths = ExpressionFieldExtractor.ExtractFieldPaths(expression);
 27
 28        // Fast path: no nodePath means preserve directly
 17729        if (string.IsNullOrWhiteSpace(nodePath))
 30        {
 10831            foreach (var path in extractedPaths)
 32            {
 2133                preserveCallback(path);
 34            }
 3335            return;
 36        }
 37
 38        // Get parameter info once
 14439        var parameterNames = GetParameterNames(expression);
 14440        var parameterTypes = GetParameterTypes(expression);
 41
 42        // Choose strategy based on localMap availability
 14443        if (localMap != null && parameterNames != null)
 44        {
 7245            PreserveWithLocalMap(extractedPaths, parameterNames, nodePath, localMap, parameterTypes, alwaysPreserveField
 46        }
 47        else
 48        {
 7249            PreserveWithGetPathTo(extractedPaths, parameterNames?.FirstOrDefault(), nodePath, parameterType);
 50        }
 7251    }
 52
 53    private void PreserveWithLocalMap(
 54        HashSet<string> extractedPaths,
 55        string[] parameterNames,
 56        string nodePath,
 57        Dictionary<string, string[]> localMap,
 58        Dictionary<string, Type> parameterTypes,
 59        string[]? alwaysPreserveFields)
 60    {
 7261        var basePathCache = new Dictionary<object, string>();
 7262        var paramsByBasePath = GroupParametersByBasePath(parameterNames, localMap, basePathCache);
 7263        EnsureAllBasePathsForAlwaysPreserve(localMap, alwaysPreserveFields, basePathCache, paramsByBasePath);
 64
 9065        if (paramsByBasePath.Count == 0) return;
 66
 25867        foreach (var (basePathKey, paramsForPath) in paramsByBasePath)
 68        {
 7569            ProcessBasePath(extractedPaths, basePathKey, paramsForPath, nodePath, parameterTypes, alwaysPreserveFields);
 70        }
 5471    }
 72
 73    private static Dictionary<string, List<string>> GroupParametersByBasePath(
 74        string[] parameterNames,
 75        Dictionary<string, string[]> localMap,
 76        Dictionary<object, string> basePathCache)
 77    {
 7278        var paramsByBasePath = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
 36079        foreach (var paramName in parameterNames)
 80        {
 10881            if (!localMap.TryGetValue(paramName, out var basePath)) continue;
 8782            var basePathKey = GetOrAddBasePathKey(basePathCache, basePath);
 8783            GetOrAddList(paramsByBasePath, basePathKey).Add(paramName);
 84        }
 7285        return paramsByBasePath;
 86    }
 87
 88    private static void EnsureAllBasePathsForAlwaysPreserve(
 89        Dictionary<string, string[]> localMap,
 90        string[]? alwaysPreserveFields,
 91        Dictionary<object, string> basePathCache,
 92        Dictionary<string, List<string>> paramsByBasePath)
 93    {
 11494        if (alwaysPreserveFields is not { Length: > 0 }) return;
 95
 16296        foreach (var (_, basePath) in localMap)
 97        {
 5198            var basePathKey = GetOrAddBasePathKey(basePathCache, basePath);
 5199            if (!paramsByBasePath.ContainsKey(basePathKey))
 100            {
 3101                paramsByBasePath[basePathKey] = new List<string>();
 102            }
 103        }
 30104    }
 105
 106    private static string GetOrAddBasePathKey(Dictionary<object, string> cache, string[] basePath)
 107    {
 138108        if (!cache.TryGetValue(basePath, out var key))
 109        {
 93110            key = string.Join(".", basePath);
 93111            cache[basePath] = key;
 112        }
 138113        return key;
 114    }
 115
 116    private static List<string> GetOrAddList(Dictionary<string, List<string>> map, string key)
 117    {
 87118        if (!map.TryGetValue(key, out var list))
 119        {
 72120            list = new List<string>();
 72121            map[key] = list;
 122        }
 87123        return list;
 124    }
 125
 126    private void ProcessBasePath(
 127        HashSet<string> extractedPaths,
 128        string basePathKey,
 129        List<string> paramsForPath,
 130        string nodePath,
 131        Dictionary<string, Type> parameterTypes,
 132        string[]? alwaysPreserveFields)
 133    {
 75134        var fullPath = $"{basePathKey}.{nodePath}";
 75135        var nodeField = QueryDefinitionExtensions.NavigatePath(sourceQuery.Definition._fields, fullPath.AsSpan(), out _)
 99136        if (nodeField is not { HasFields: true }) return;
 137
 51138        var fieldsToPreserve = CollectFields(extractedPaths, paramsForPath, parameterTypes, alwaysPreserveFields);
 51139        PreserveFields(nodeField._children!, fieldsToPreserve, fullPath);
 51140    }
 141
 142    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 143    private HashSet<string> CollectFields(
 144        HashSet<string> extractedPaths,
 145        List<string> paramsForPath,
 146        Dictionary<string, Type> parameterTypes,
 147        string[]? alwaysPreserveFields)
 148    {
 51149        var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 150
 51151        if (ShouldExpandWithoutPrefixes(extractedPaths, paramsForPath))
 152        {
 6153            ExpandPerParameterType(extractedPaths, paramsForPath, parameterTypes, result);
 154        }
 155        else
 156        {
 45157            FilterPerParameter(extractedPaths, paramsForPath, parameterTypes, result);
 158        }
 159
 51160        AppendAlwaysPreserveFields(alwaysPreserveFields, result);
 51161        return result;
 162    }
 163
 164    /// <summary>
 165    /// True when the extracted paths carry no parameter-name prefix AND there are multiple
 166    /// parameters in scope — falls into the "expand per type" branch instead of per-parameter
 167    /// filtering.
 168    /// </summary>
 169    private static bool ShouldExpandWithoutPrefixes(HashSet<string> extractedPaths, List<string> paramsForPath)
 170    {
 90171        if (paramsForPath.Count <= 1) return false;
 172
 173        // Manual short-circuit loop — LINQ allocations would be heavier on the hot path.
 174#pragma warning disable S3267
 54175        foreach (var p in extractedPaths)
 176        {
 90177            foreach (var param in paramsForPath)
 178            {
 30179                if (p.StartsWith(param + ".", StringComparison.OrdinalIgnoreCase))
 180                {
 6181                    return false;
 182                }
 183            }
 184        }
 185#pragma warning restore S3267
 6186        return true;
 6187    }
 188
 189    private static void ExpandPerParameterType(
 190        HashSet<string> extractedPaths,
 191        List<string> paramsForPath,
 192        Dictionary<string, Type> parameterTypes,
 193        HashSet<string> result)
 194    {
 36195        foreach (var paramName in paramsForPath)
 196        {
 12197            parameterTypes.TryGetValue(paramName, out var specificParameterType);
 12198            AddFieldsToPreserve(extractedPaths, null, specificParameterType, result);
 199        }
 6200    }
 201
 202    private static void FilterPerParameter(
 203        HashSet<string> extractedPaths,
 204        List<string> paramsForPath,
 205        Dictionary<string, Type> parameterTypes,
 206        HashSet<string> result)
 207    {
 198208        foreach (var paramName in paramsForPath)
 209        {
 54210            var parameterPaths = SelectPathsForParameter(extractedPaths, paramName, paramsForPath.Count == 1);
 54211            if (parameterPaths == null) continue;
 212
 54213            parameterTypes.TryGetValue(paramName, out var specificParameterType);
 54214            AddFieldsToPreserve(parameterPaths, paramName, specificParameterType, result);
 215        }
 45216    }
 217
 218    /// <summary>
 219    /// Returns the subset of <paramref name="extractedPaths"/> that target <paramref name="paramName"/>.
 220    /// Falls back to the full set when no prefixed paths match AND this is a single-parameter lambda
 221    /// (so an unprefixed expression like <c>x =&gt; x.field</c> still preserves something).
 222    /// </summary>
 223    private static HashSet<string>? SelectPathsForParameter(
 224        HashSet<string> extractedPaths,
 225        string paramName,
 226        bool singleParameterFallback)
 227    {
 54228        var parameterPaths = CollectParameterPrefixedPaths(extractedPaths, paramName);
 54229        if ((parameterPaths is null || parameterPaths.Count == 0)
 54230            && extractedPaths.Count > 0
 54231            && singleParameterFallback)
 232        {
 27233            return extractedPaths;
 234        }
 27235        return parameterPaths;
 236    }
 237
 238    // Manual loop with lazy init beats LINQ Where().ToHashSet() when no paths match.
 239#pragma warning disable S3267
 240    private static HashSet<string>? CollectParameterPrefixedPaths(HashSet<string> extractedPaths, string paramName)
 241    {
 54242        HashSet<string>? parameterPaths = null;
 54243        var prefix = paramName + ".";
 318244        foreach (var p in extractedPaths)
 245        {
 105246            if (!IsParameterMatch(p, paramName, prefix)) continue;
 27247            parameterPaths ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 27248            parameterPaths.Add(p);
 249        }
 54250        return parameterPaths;
 251    }
 252
 253    private static bool IsParameterMatch(string path, string paramName, string prefix)
 254    {
 105255        if (string.Equals(path, paramName, StringComparison.OrdinalIgnoreCase)) return true;
 105256        return path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
 257    }
 258#pragma warning restore S3267
 259
 260    private static void AppendAlwaysPreserveFields(string[]? alwaysPreserveFields, HashSet<string> result)
 261    {
 72262        if (alwaysPreserveFields == null) return;
 120263        foreach (var field in alwaysPreserveFields)
 264        {
 30265            result.Add(field);
 266        }
 30267    }
 268
 269    private static void AddFieldsToPreserve(
 270        HashSet<string> extractedPaths,
 271        string? paramName,
 272        Type? parameterType,
 273        HashSet<string> result)
 274    {
 138275        var hasOnlyParameterName = ExpandPathsIntoResult(extractedPaths, paramName, parameterType, result);
 276
 138277        if (result.Count > 1)
 278        {
 48279            ApplyMostSpecificWins(result);
 280        }
 281
 138282        if (hasOnlyParameterName && result.Count == 0 && parameterType != null)
 283        {
 6284            AddAllPropertiesAsFallback(parameterType, result);
 285        }
 138286    }
 287
 288    /// <summary>
 289    /// Walks <paramref name="extractedPaths"/> and adds the (parameter-prefix-stripped, navigation-expanded)
 290    /// fields to <paramref name="result"/>. Returns true when at least one path was just the parameter
 291    /// name itself, signalling the "greedy preserve everything" fallback below.
 292    /// </summary>
 293    private static bool ExpandPathsIntoResult(
 294        HashSet<string> extractedPaths,
 295        string? paramName,
 296        Type? parameterType,
 297        HashSet<string> result)
 298    {
 138299        var hasOnlyParameterName = false;
 624300        foreach (var path in extractedPaths)
 301        {
 174302            if (IsBareParameterReference(path, paramName))
 303            {
 6304                hasOnlyParameterName = true;
 6305                continue;
 306            }
 307
 168308            var field = StripParameterPrefix(path, paramName);
 168309            if (string.IsNullOrEmpty(field)) continue;
 310
 732311            foreach (var expandedField in NavigationPropertyExpander.ExpandNavigationProperty(field, parameterType))
 312            {
 198313                result.Add(expandedField);
 314            }
 315        }
 138316        return hasOnlyParameterName;
 317    }
 318
 319    private static bool IsBareParameterReference(string path, string? paramName)
 174320        => !string.IsNullOrEmpty(paramName)
 174321        && string.Equals(path, paramName, StringComparison.OrdinalIgnoreCase);
 322
 323    private static string StripParameterPrefix(string path, string? paramName)
 168324        => !string.IsNullOrEmpty(paramName)
 168325           && path.StartsWith(paramName + ".", StringComparison.OrdinalIgnoreCase)
 168326            ? path.Substring(paramName.Length + 1)
 168327            : path;
 328
 329    /// <summary>Removes broader paths from <paramref name="result"/> when a more-specific child path exists.</summary>
 330    private static void ApplyMostSpecificWins(HashSet<string> result)
 331    {
 177332        result.RemoveWhere(path => HasMoreSpecificDescendant(path, result));
 48333    }
 334
 335    private static bool HasMoreSpecificDescendant(string path, HashSet<string> all)
 336    {
 129337        var prefix = path + ".";
 1002338        foreach (var other in all)
 339        {
 375340            if (other.Length > path.Length && other.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
 6341                return true;
 342        }
 123343        return false;
 6344    }
 345
 346    private static void AddAllPropertiesAsFallback(Type parameterType, HashSet<string> result)
 347    {
 54348        foreach (var prop in parameterType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
 349        {
 21350            result.Add(prop.Name);
 351        }
 6352    }
 353
 354    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 355    private void PreserveFields(
 356        IReadOnlyDictionary<string, FieldDefinition> nodeFields,
 357        HashSet<string> fieldsToPreserve,
 358        string basePathStr)
 359    {
 342360        foreach (var field in fieldsToPreserve)
 361        {
 362            // Fast path: nested field
 120363            if (field.Contains('.'))
 364            {
 36365                PreserveNestedField(nodeFields, field, basePathStr);
 36366                continue;
 367            }
 368
 369            // Fast path: direct match
 84370            var match = PreserveExtensions.FindFieldByNameOrAlias(nodeFields, field.AsSpan());
 84371            if (match.HasValue)
 372            {
 66373                PreserveMatchedField(match.Value, basePathStr);
 66374                continue;
 375            }
 376
 377            // Slow path: recursive search
 18378            PreserveRecursiveMatches(nodeFields, field, basePathStr);
 379        }
 51380    }
 381
 382    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 383    private void PreserveNestedField(IReadOnlyDictionary<string, FieldDefinition> nodeFields, string field, string baseP
 384    {
 36385        if (QueryDefinitionExtensions.NavigatePath(nodeFields, field.AsSpan(), out var resolvedPath, basePathStr) != nul
 386        {
 33387            preserveCallback(resolvedPath!);
 388        }
 36389    }
 390
 391    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 392    private void PreserveMatchedField(KeyValuePair<string, FieldDefinition> match, string basePathStr)
 393    {
 66394        var matchingKey = match.Key;
 395
 396        // Fast path: leaf field
 66397        if (!match.Value.HasFields)
 398        {
 63399            preserveCallback($"{basePathStr}.{matchingKey}");
 63400            return;
 401        }
 402
 403        // Object field: preserve all sub-fields using fast span iteration
 3404        var objectPath = $"{basePathStr}.{matchingKey}";
 18405        foreach (var child in match.Value._children!.AsSpan())
 406        {
 6407            preserveCallback($"{objectPath}.{child.Name}");
 408        }
 3409    }
 410
 411    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 412    private void PreserveRecursiveMatches(IReadOnlyDictionary<string, FieldDefinition> nodeFields, string field, string 
 413    {
 18414        var recursiveMatches = QueryDefinitionExtensions.FindFieldRecursively(nodeFields, field, "");
 72415        foreach (var recursivePath in recursiveMatches)
 416        {
 18417            preserveCallback($"{basePathStr}.{recursivePath}");
 418        }
 18419    }
 420
 421    private void PreserveWithGetPathTo(
 422        HashSet<string> extractedPaths,
 423        string? paramName,
 424        string nodePath,
 425        Type? parameterType)
 426    {
 72427        var fieldsToPreserve = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 72428        AddFieldsToPreserve(extractedPaths, paramName, parameterType, fieldsToPreserve);
 429
 72430        var lastSegment = LastSegmentOf(nodePath);
 276431        foreach (var rootField in sourceQuery.Definition.Fields.Values)
 432        {
 66433            PreserveFromRoot(rootField, nodePath, lastSegment, fieldsToPreserve);
 434        }
 72435    }
 436
 437    private static string LastSegmentOf(string nodePath)
 438    {
 72439        var nodePathSpan = nodePath.AsSpan();
 72440        var lastSegmentStart = nodePathSpan.LastIndexOf('.');
 72441        return (lastSegmentStart >= 0 ? nodePathSpan[(lastSegmentStart + 1)..] : nodePathSpan).ToString();
 442    }
 443
 444    private void PreserveFromRoot(FieldDefinition rootField, string nodePath, string lastSegment, HashSet<string> fields
 445    {
 66446        var pathToNode = sourceQuery.GetPathTo(rootField.Alias ?? rootField.Name, nodePath);
 66447        if (pathToNode.Length == 0) return;
 448
 66449        var fullNodePath = $"{string.Join(".", pathToNode)}.{lastSegment}";
 66450        var nodeField = QueryDefinitionExtensions.NavigatePath(sourceQuery.Definition._fields, fullNodePath.AsSpan(), ou
 66451        if (nodeField is null) return;
 66452        if (!nodeField.HasFields) return;
 453
 348454        foreach (var field in fieldsToPreserve)
 455        {
 108456            if (QueryDefinitionExtensions.NavigatePath(nodeField.Fields, field.AsSpan(), out var resolvedPath, fullNodeP
 457            {
 96458                preserveCallback(resolvedPath!);
 459            }
 460        }
 66461    }
 462
 463    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 464    private static string[]? GetParameterNames(Expression expression)
 465    {
 144466        if (expression is not LambdaExpression { Parameters.Count: > 0 } lambda)
 3467            return null;
 468
 469        // Lambdas authored in C# always carry parameter names. The BCL allows null names via
 470        // Expression.Parameter(type, null) but those don't reach here through the public API.
 471#pragma warning disable S3267
 141472        var names = new string[lambda.Parameters.Count];
 636473        for (int i = 0; i < lambda.Parameters.Count; i++)
 474        {
 177475            names[i] = lambda.Parameters[i].Name!;
 476        }
 477#pragma warning restore S3267
 141478        return names;
 479    }
 480
 481    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 482    private static Dictionary<string, Type> GetParameterTypes(Expression expression)
 483    {
 144484        if (expression is not LambdaExpression lambda || lambda.Parameters.Count == 0)
 3485            return new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
 486
 141487        var dict = new Dictionary<string, Type>(lambda.Parameters.Count, StringComparer.OrdinalIgnoreCase);
 636488        foreach (var p in lambda.Parameters)
 489        {
 177490            if (p.Name != null)
 177491                dict[p.Name] = p.Type;
 492        }
 493
 141494        return dict;
 495    }
 496}

Methods/Properties

.ctor(NGql.Core.Builders.QueryBuilder,System.Action`1<System.String>)
ProcessExpression(System.Linq.Expressions.Expression,System.String,System.Collections.Generic.Dictionary`2<System.String,System.String[]>,System.Type,System.String[])
PreserveWithLocalMap(System.Collections.Generic.HashSet`1<System.String>,System.String[],System.String,System.Collections.Generic.Dictionary`2<System.String,System.String[]>,System.Collections.Generic.Dictionary`2<System.String,System.Type>,System.String[])
GroupParametersByBasePath(System.String[],System.Collections.Generic.Dictionary`2<System.String,System.String[]>,System.Collections.Generic.Dictionary`2<System.Object,System.String>)
EnsureAllBasePathsForAlwaysPreserve(System.Collections.Generic.Dictionary`2<System.String,System.String[]>,System.String[],System.Collections.Generic.Dictionary`2<System.Object,System.String>,System.Collections.Generic.Dictionary`2<System.String,System.Collections.Generic.List`1<System.String>>)
GetOrAddBasePathKey(System.Collections.Generic.Dictionary`2<System.Object,System.String>,System.String[])
GetOrAddList(System.Collections.Generic.Dictionary`2<System.String,System.Collections.Generic.List`1<System.String>>,System.String)
ProcessBasePath(System.Collections.Generic.HashSet`1<System.String>,System.String,System.Collections.Generic.List`1<System.String>,System.String,System.Collections.Generic.Dictionary`2<System.String,System.Type>,System.String[])
CollectFields(System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.List`1<System.String>,System.Collections.Generic.Dictionary`2<System.String,System.Type>,System.String[])
ShouldExpandWithoutPrefixes(System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.List`1<System.String>)
ExpandPerParameterType(System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.List`1<System.String>,System.Collections.Generic.Dictionary`2<System.String,System.Type>,System.Collections.Generic.HashSet`1<System.String>)
FilterPerParameter(System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.List`1<System.String>,System.Collections.Generic.Dictionary`2<System.String,System.Type>,System.Collections.Generic.HashSet`1<System.String>)
SelectPathsForParameter(System.Collections.Generic.HashSet`1<System.String>,System.String,System.Boolean)
CollectParameterPrefixedPaths(System.Collections.Generic.HashSet`1<System.String>,System.String)
IsParameterMatch(System.String,System.String,System.String)
AppendAlwaysPreserveFields(System.String[],System.Collections.Generic.HashSet`1<System.String>)
AddFieldsToPreserve(System.Collections.Generic.HashSet`1<System.String>,System.String,System.Type,System.Collections.Generic.HashSet`1<System.String>)
ExpandPathsIntoResult(System.Collections.Generic.HashSet`1<System.String>,System.String,System.Type,System.Collections.Generic.HashSet`1<System.String>)
IsBareParameterReference(System.String,System.String)
StripParameterPrefix(System.String,System.String)
ApplyMostSpecificWins(System.Collections.Generic.HashSet`1<System.String>)
HasMoreSpecificDescendant(System.String,System.Collections.Generic.HashSet`1<System.String>)
AddAllPropertiesAsFallback(System.Type,System.Collections.Generic.HashSet`1<System.String>)
PreserveFields(System.Collections.Generic.IReadOnlyDictionary`2<System.String,NGql.Core.Abstractions.FieldDefinition>,System.Collections.Generic.HashSet`1<System.String>,System.String)
PreserveNestedField(System.Collections.Generic.IReadOnlyDictionary`2<System.String,NGql.Core.Abstractions.FieldDefinition>,System.String,System.String)
PreserveMatchedField(System.Collections.Generic.KeyValuePair`2<System.String,NGql.Core.Abstractions.FieldDefinition>,System.String)
PreserveRecursiveMatches(System.Collections.Generic.IReadOnlyDictionary`2<System.String,NGql.Core.Abstractions.FieldDefinition>,System.String,System.String)
PreserveWithGetPathTo(System.Collections.Generic.HashSet`1<System.String>,System.String,System.String,System.Type)
LastSegmentOf(System.String)
PreserveFromRoot(NGql.Core.Abstractions.FieldDefinition,System.String,System.String,System.Collections.Generic.HashSet`1<System.String>)
GetParameterNames(System.Linq.Expressions.Expression)
GetParameterTypes(System.Linq.Expressions.Expression)