< 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: 175
Uncovered lines: 0
Coverable lines: 175
Total lines: 500
Line coverage: 100%
Branch coverage
100%
Covered branches: 138
Total branches: 138
Branch coverage: 100%
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.Runtime.CompilerServices;
 3using NGql.Core.Abstractions;
 4using NGql.Core.Caching;
 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>
 21014internal 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    {
 21026        var extractedPaths = ExpressionFieldExtractor.ExtractFieldPaths(expression);
 27
 28        // Fast path: no nodePath means preserve directly
 21029        if (string.IsNullOrWhiteSpace(nodePath))
 30        {
 12031            foreach (var path in extractedPaths)
 32            {
 2433                preserveCallback(path);
 34            }
 3635            return;
 36        }
 37
 38        // Get parameter names once; parameter types are only needed by the local-map strategy,
 39        // so their (allocating) map is deferred into that branch.
 17440        var lambda = expression as LambdaExpression;
 17441        var parameterNames = GetParameterNames(lambda);
 42
 43        // Choose strategy based on localMap availability
 17444        if (localMap != null && parameterNames != null)
 45        {
 9346            var parameterTypes = GetParameterTypes(lambda!);
 9347            PreserveWithLocalMap(extractedPaths, parameterNames, nodePath, localMap, parameterTypes, alwaysPreserveField
 48        }
 49        else
 50        {
 8151            PreserveWithGetPathTo(extractedPaths, parameterNames?.FirstOrDefault(), nodePath, parameterType);
 52        }
 8153    }
 54
 55    private void PreserveWithLocalMap(
 56        HashSet<string> extractedPaths,
 57        string[] parameterNames,
 58        string nodePath,
 59        Dictionary<string, string[]> localMap,
 60        Dictionary<string, Type> parameterTypes,
 61        string[]? alwaysPreserveFields)
 62    {
 9363        var basePathCache = new Dictionary<object, string>();
 9364        var paramsByBasePath = GroupParametersByBasePath(parameterNames, localMap, basePathCache);
 9365        EnsureAllBasePathsForAlwaysPreserve(localMap, alwaysPreserveFields, basePathCache, paramsByBasePath);
 66
 11167        if (paramsByBasePath.Count == 0) return;
 68
 35469        foreach (var (basePathKey, paramsForPath) in paramsByBasePath)
 70        {
 10271            ProcessBasePath(extractedPaths, basePathKey, paramsForPath, nodePath, parameterTypes, alwaysPreserveFields);
 72        }
 7573    }
 74
 75    private static Dictionary<string, List<string>> GroupParametersByBasePath(
 76        string[] parameterNames,
 77        Dictionary<string, string[]> localMap,
 78        Dictionary<object, string> basePathCache)
 79    {
 9380        var paramsByBasePath = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
 47481        foreach (var paramName in parameterNames)
 82        {
 14483            if (!localMap.TryGetValue(paramName, out var basePath)) continue;
 12384            var basePathKey = GetOrAddBasePathKey(basePathCache, basePath);
 12385            GetOrAddList(paramsByBasePath, basePathKey).Add(paramName);
 86        }
 9387        return paramsByBasePath;
 88    }
 89
 90    private static void EnsureAllBasePathsForAlwaysPreserve(
 91        Dictionary<string, string[]> localMap,
 92        string[]? alwaysPreserveFields,
 93        Dictionary<object, string> basePathCache,
 94        Dictionary<string, List<string>> paramsByBasePath)
 95    {
 15396        if (alwaysPreserveFields is not { Length: > 0 }) return;
 97
 18098        foreach (var (_, basePath) in localMap)
 99        {
 57100            var basePathKey = GetOrAddBasePathKey(basePathCache, basePath);
 57101            if (!paramsByBasePath.ContainsKey(basePathKey))
 102            {
 6103                paramsByBasePath[basePathKey] = new List<string>();
 104            }
 105        }
 33106    }
 107
 108    private static string GetOrAddBasePathKey(Dictionary<object, string> cache, string[] basePath)
 109    {
 180110        if (!cache.TryGetValue(basePath, out var key))
 111        {
 132112            key = string.Join(".", basePath);
 132113            cache[basePath] = key;
 114        }
 180115        return key;
 116    }
 117
 118    private static List<string> GetOrAddList(Dictionary<string, List<string>> map, string key)
 119    {
 123120        if (!map.TryGetValue(key, out var list))
 121        {
 96122            list = new List<string>();
 96123            map[key] = list;
 124        }
 123125        return list;
 126    }
 127
 128    private void ProcessBasePath(
 129        HashSet<string> extractedPaths,
 130        string basePathKey,
 131        List<string> paramsForPath,
 132        string nodePath,
 133        Dictionary<string, Type> parameterTypes,
 134        string[]? alwaysPreserveFields)
 135    {
 102136        var fullPath = $"{basePathKey}.{nodePath}";
 102137        var nodeField = QueryDefinitionExtensions.NavigatePath(sourceQuery.Definition._fields, fullPath.AsSpan(), out _)
 129138        if (nodeField is not { HasFields: true }) return;
 139
 75140        var fieldsToPreserve = CollectFields(extractedPaths, paramsForPath, parameterTypes, alwaysPreserveFields);
 75141        PreserveFields(nodeField._children!, fieldsToPreserve, fullPath);
 75142    }
 143
 144    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 145    private HashSet<string> CollectFields(
 146        HashSet<string> extractedPaths,
 147        List<string> paramsForPath,
 148        Dictionary<string, Type> parameterTypes,
 149        string[]? alwaysPreserveFields)
 150    {
 75151        var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 152
 75153        if (ShouldExpandWithoutPrefixes(extractedPaths, paramsForPath))
 154        {
 3155            ExpandPerParameterType(extractedPaths, paramsForPath, parameterTypes, result);
 156        }
 157        else
 158        {
 72159            FilterPerParameter(extractedPaths, paramsForPath, parameterTypes, result);
 160        }
 161
 75162        AppendAlwaysPreserveFields(alwaysPreserveFields, result);
 75163        return result;
 164    }
 165
 166    /// <summary>
 167    /// True when the extracted paths carry no parameter-name prefix AND there are multiple
 168    /// parameters in scope — falls into the "expand per type" branch instead of per-parameter
 169    /// filtering.
 170    /// </summary>
 171    private static bool ShouldExpandWithoutPrefixes(HashSet<string> extractedPaths, List<string> paramsForPath)
 172    {
 126173        if (paramsForPath.Count <= 1) return false;
 174
 175        // Manual short-circuit loop — LINQ allocations would be heavier on the hot path.
 176#pragma warning disable S3267
 93177        foreach (var p in extractedPaths)
 178        {
 141179            foreach (var param in paramsForPath)
 180            {
 48181                if (p.StartsWith(param + ".", StringComparison.OrdinalIgnoreCase))
 182                {
 21183                    return false;
 184                }
 185            }
 186        }
 187#pragma warning restore S3267
 3188        return true;
 21189    }
 190
 191    private static void ExpandPerParameterType(
 192        HashSet<string> extractedPaths,
 193        List<string> paramsForPath,
 194        Dictionary<string, Type> parameterTypes,
 195        HashSet<string> result)
 196    {
 18197        foreach (var paramName in paramsForPath)
 198        {
 6199            parameterTypes.TryGetValue(paramName, out var specificParameterType);
 6200            AddFieldsToPreserve(extractedPaths, null, specificParameterType, result);
 201        }
 3202    }
 203
 204    private static void FilterPerParameter(
 205        HashSet<string> extractedPaths,
 206        List<string> paramsForPath,
 207        Dictionary<string, Type> parameterTypes,
 208        HashSet<string> result)
 209    {
 336210        foreach (var paramName in paramsForPath)
 211        {
 96212            var parameterPaths = SelectPathsForParameter(extractedPaths, paramName, paramsForPath.Count == 1);
 96213            if (parameterPaths == null) continue;
 214
 90215            parameterTypes.TryGetValue(paramName, out var specificParameterType);
 90216            AddFieldsToPreserve(parameterPaths, paramName, specificParameterType, result);
 217        }
 72218    }
 219
 220    /// <summary>
 221    /// Returns the subset of <paramref name="extractedPaths"/> that target <paramref name="paramName"/>.
 222    /// Falls back to the full set when no prefixed paths match AND this is a single-parameter lambda
 223    /// (so an unprefixed expression like <c>x =&gt; x.field</c> still preserves something).
 224    /// </summary>
 225    private static HashSet<string>? SelectPathsForParameter(
 226        HashSet<string> extractedPaths,
 227        string paramName,
 228        bool singleParameterFallback)
 229    {
 96230        var parameterPaths = CollectParameterPrefixedPaths(extractedPaths, paramName);
 96231        if ((parameterPaths is null || parameterPaths.Count == 0)
 96232            && extractedPaths.Count > 0
 96233            && singleParameterFallback)
 234        {
 27235            return extractedPaths;
 236        }
 69237        return parameterPaths;
 238    }
 239
 240    // Manual loop with lazy init beats LINQ Where().ToHashSet() when no paths match.
 241#pragma warning disable S3267
 242    private static HashSet<string>? CollectParameterPrefixedPaths(HashSet<string> extractedPaths, string paramName)
 243    {
 96244        HashSet<string>? parameterPaths = null;
 96245        var prefix = paramName + ".";
 558246        foreach (var p in extractedPaths)
 247        {
 183248            if (!IsParameterMatch(p, paramName, prefix)) continue;
 63249            parameterPaths ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 63250            parameterPaths.Add(p);
 251        }
 96252        return parameterPaths;
 253    }
 254
 255    private static bool IsParameterMatch(string path, string paramName, string prefix)
 256    {
 186257        if (string.Equals(path, paramName, StringComparison.OrdinalIgnoreCase)) return true;
 180258        return path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
 259    }
 260#pragma warning restore S3267
 261
 262    private static void AppendAlwaysPreserveFields(string[]? alwaysPreserveFields, HashSet<string> result)
 263    {
 117264        if (alwaysPreserveFields == null) return;
 132265        foreach (var field in alwaysPreserveFields)
 266        {
 33267            result.Add(field);
 268        }
 33269    }
 270
 271    private static void AddFieldsToPreserve(
 272        HashSet<string> extractedPaths,
 273        string? paramName,
 274        Type? parameterType,
 275        HashSet<string> result)
 276    {
 177277        var hasOnlyParameterName = ExpandPathsIntoResult(extractedPaths, paramName, parameterType, result);
 278
 177279        if (result.Count > 1)
 280        {
 54281            ApplyMostSpecificWins(result);
 282        }
 283
 177284        if (hasOnlyParameterName && result.Count == 0 && parameterType != null)
 285        {
 12286            AddAllPropertiesAsFallback(parameterType, result);
 287        }
 177288    }
 289
 290    /// <summary>
 291    /// Walks <paramref name="extractedPaths"/> and adds the (parameter-prefix-stripped, navigation-expanded)
 292    /// fields to <paramref name="result"/>. Returns true when at least one path was just the parameter
 293    /// name itself, signalling the "greedy preserve everything" fallback below.
 294    /// </summary>
 295    private static bool ExpandPathsIntoResult(
 296        HashSet<string> extractedPaths,
 297        string? paramName,
 298        Type? parameterType,
 299        HashSet<string> result)
 300    {
 177301        var hasOnlyParameterName = false;
 774302        foreach (var path in extractedPaths)
 303        {
 210304            if (IsBareParameterReference(path, paramName))
 305            {
 12306                hasOnlyParameterName = true;
 12307                continue;
 308            }
 309
 198310            var field = StripParameterPrefix(path, paramName);
 198311            if (string.IsNullOrEmpty(field)) continue;
 312
 852313            foreach (var expandedField in NavigationPropertyExpander.ExpandNavigationProperty(field, parameterType))
 314            {
 228315                result.Add(expandedField);
 316            }
 317        }
 177318        return hasOnlyParameterName;
 319    }
 320
 321    private static bool IsBareParameterReference(string path, string? paramName)
 210322        => !string.IsNullOrEmpty(paramName)
 210323        && string.Equals(path, paramName, StringComparison.OrdinalIgnoreCase);
 324
 325    private static string StripParameterPrefix(string path, string? paramName)
 198326        => !string.IsNullOrEmpty(paramName)
 198327           && path.StartsWith(paramName + ".", StringComparison.OrdinalIgnoreCase)
 198328            ? path.Substring(paramName.Length + 1)
 198329            : path;
 330
 331    /// <summary>Removes broader paths from <paramref name="result"/> when a more-specific child path exists.</summary>
 332    private static void ApplyMostSpecificWins(HashSet<string> result)
 333    {
 198334        result.RemoveWhere(path => HasMoreSpecificDescendant(path, result));
 54335    }
 336
 337    private static bool HasMoreSpecificDescendant(string path, HashSet<string> all)
 338    {
 144339        var prefix = path + ".";
 1101340        foreach (var other in all)
 341        {
 411342            if (other.Length > path.Length && other.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
 9343                return true;
 344        }
 135345        return false;
 9346    }
 347
 348    private static void AddAllPropertiesAsFallback(Type parameterType, HashSet<string> result)
 349    {
 90350        foreach (var prop in TypeMetadataCache.GetNavigationProperties(parameterType).Properties)
 351        {
 33352            result.Add(prop.Name);
 353        }
 12354    }
 355
 356    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 357    private void PreserveFields(
 358        IReadOnlyDictionary<string, FieldDefinition> nodeFields,
 359        HashSet<string> fieldsToPreserve,
 360        string basePathStr)
 361    {
 462362        foreach (var field in fieldsToPreserve)
 363        {
 364            // Fast path: nested field
 156365            if (field.Contains('.'))
 366            {
 42367                PreserveNestedField(nodeFields, field, basePathStr);
 42368                continue;
 369            }
 370
 371            // Fast path: direct match
 114372            var match = PreserveExtensions.FindFieldByNameOrAlias(nodeFields, field.AsSpan());
 114373            if (match.HasValue)
 374            {
 93375                PreserveMatchedField(match.Value, basePathStr);
 93376                continue;
 377            }
 378
 379            // Slow path: recursive search
 21380            PreserveRecursiveMatches(nodeFields, field, basePathStr);
 381        }
 75382    }
 383
 384    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 385    private void PreserveNestedField(IReadOnlyDictionary<string, FieldDefinition> nodeFields, string field, string baseP
 386    {
 42387        if (QueryDefinitionExtensions.NavigatePath(nodeFields, field.AsSpan(), out var resolvedPath, basePathStr) != nul
 388        {
 33389            preserveCallback(resolvedPath!);
 390        }
 42391    }
 392
 393    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 394    private void PreserveMatchedField(KeyValuePair<string, FieldDefinition> match, string basePathStr)
 395    {
 93396        var matchingKey = match.Key;
 397
 398        // Fast path: leaf field
 93399        if (!match.Value.HasFields)
 400        {
 90401            preserveCallback($"{basePathStr}.{matchingKey}");
 90402            return;
 403        }
 404
 405        // Object field: preserve all sub-fields using fast span iteration
 3406        var objectPath = $"{basePathStr}.{matchingKey}";
 18407        foreach (var child in match.Value._children!.AsSpan())
 408        {
 6409            preserveCallback($"{objectPath}.{child.Name}");
 410        }
 3411    }
 412
 413    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 414    private void PreserveRecursiveMatches(IReadOnlyDictionary<string, FieldDefinition> nodeFields, string field, string 
 415    {
 21416        var recursiveMatches = QueryDefinitionExtensions.FindFieldRecursively(nodeFields, field, "");
 78417        foreach (var recursivePath in recursiveMatches)
 418        {
 18419            preserveCallback($"{basePathStr}.{recursivePath}");
 420        }
 21421    }
 422
 423    private void PreserveWithGetPathTo(
 424        HashSet<string> extractedPaths,
 425        string? paramName,
 426        string nodePath,
 427        Type? parameterType)
 428    {
 81429        var fieldsToPreserve = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 81430        AddFieldsToPreserve(extractedPaths, paramName, parameterType, fieldsToPreserve);
 431
 81432        var lastSegment = LastSegmentOf(nodePath);
 318433        foreach (var rootField in sourceQuery.Definition.FieldsInternal.Values)
 434        {
 78435            PreserveFromRoot(rootField, nodePath, lastSegment, fieldsToPreserve);
 436        }
 81437    }
 438
 439    private static string LastSegmentOf(string nodePath)
 440    {
 81441        var nodePathSpan = nodePath.AsSpan();
 81442        var lastSegmentStart = nodePathSpan.LastIndexOf('.');
 81443        return (lastSegmentStart >= 0 ? nodePathSpan[(lastSegmentStart + 1)..] : nodePathSpan).ToString();
 444    }
 445
 446    private void PreserveFromRoot(FieldDefinition rootField, string nodePath, string lastSegment, HashSet<string> fields
 447    {
 448        // rootField is one of the query's own roots, so GetPathTo always resolves its name and
 449        // returns at least the root segment — no empty-result guard is reachable here.
 78450        var pathToNode = sourceQuery.GetPathTo(rootField.Alias ?? rootField.Name, nodePath);
 451
 78452        var fullNodePath = $"{string.Join(".", pathToNode)}.{lastSegment}";
 78453        var nodeField = QueryDefinitionExtensions.NavigatePath(sourceQuery.Definition._fields, fullNodePath.AsSpan(), ou
 81454        if (nodeField is null) return;
 78455        if (!nodeField.HasFields) return;
 456
 378457        foreach (var field in fieldsToPreserve)
 458        {
 117459            if (QueryDefinitionExtensions.NavigatePath(nodeField.Fields, field.AsSpan(), out var resolvedPath, fullNodeP
 460            {
 105461                preserveCallback(resolvedPath!);
 462            }
 463        }
 72464    }
 465
 466    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 467    private static string[]? GetParameterNames(LambdaExpression? lambda)
 468    {
 174469        if (lambda is not { Parameters.Count: > 0 })
 3470            return null;
 471
 472        // Lambdas authored in C# always carry parameter names. The BCL allows null names via
 473        // Expression.Parameter(type, null) but those don't reach here through the public API.
 474#pragma warning disable S3267
 171475        var names = new string[lambda.Parameters.Count];
 786476        for (int i = 0; i < lambda.Parameters.Count; i++)
 477        {
 222478            names[i] = lambda.Parameters[i].Name!;
 479        }
 480#pragma warning restore S3267
 171481        return names;
 482    }
 483
 484    /// <summary>
 485    /// Maps parameter name to declared type. Only called once <see cref="GetParameterNames"/> has
 486    /// confirmed a lambda with at least one parameter, so no empty-input guard is needed.
 487    /// </summary>
 488    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 489    private static Dictionary<string, Type> GetParameterTypes(LambdaExpression lambda)
 490    {
 93491        var dict = new Dictionary<string, Type>(lambda.Parameters.Count, StringComparer.OrdinalIgnoreCase);
 474492        foreach (var p in lambda.Parameters)
 493        {
 144494            if (p.Name != null)
 144495                dict[p.Name] = p.Type;
 496        }
 497
 93498        return dict;
 499    }
 500}

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.LambdaExpression)
GetParameterTypes(System.Linq.Expressions.LambdaExpression)