< Summary

Information
Class: NGql.Core.Builders.PreservationBuilder
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Builders/PreservationBuilder.cs
Line coverage
100%
Covered lines: 63
Uncovered lines: 0
Coverable lines: 63
Total lines: 226
Line coverage: 100%
Branch coverage
97%
Covered branches: 37
Total branches: 38
Branch coverage: 97.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%
Create(...)100%11100%
Preserve(...)100%66100%
PreserveAtPath(...)100%66100%
PreserveAtPathForRoot(...)87.5%88100%
PreserveResolvedNestedPath(...)100%44100%
PreserveDirectMatch(...)100%22100%
JoinPath(...)100%44100%
PreserveFromExpression(...)100%11100%
PreserveFromExpression(...)100%11100%
PreserveFromExpression(...)100%11100%
PreserveFromExpression(...)100%11100%
PreserveFromExpression(...)100%11100%
Build()100%22100%
InferTypeFromLambda(...)100%44100%
PreserveFromExpressionCore(...)100%11100%

File(s)

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

#LineLine coverage
 1using System.Linq.Expressions;
 2using NGql.Core.Extensions;
 3using NGql.Core.Features;
 4
 5namespace NGql.Core.Builders;
 6
 7/// <summary>
 8/// Builds a derived <see cref="QueryBuilder"/> that contains only a chosen subset of an
 9/// existing query's fields.
 10/// </summary>
 11/// <remarks>
 12/// Use <see cref="Create(QueryBuilder)"/> to start, accumulate paths via <see cref="Preserve(string[])"/>
 13/// or <see cref="PreserveFromExpression{T}(Expression{Func{T, bool}}, string?)"/>, then call
 14/// <see cref="Build"/> to materialize the trimmed query. The source builder is never mutated.
 15/// <para>
 16/// Typical use: role-based field filtering — start from a "full" query and emit a smaller
 17/// projection per caller without re-building from scratch.
 18/// </para>
 19/// </remarks>
 20public sealed class PreservationBuilder
 21{
 22    private readonly QueryBuilder _sourceQuery;
 23    private readonly HashSet<string> _pathsToPreserve;
 24
 30325    private PreservationBuilder(QueryBuilder sourceQuery)
 26    {
 30327        _sourceQuery = sourceQuery ?? throw new ArgumentNullException(nameof(sourceQuery));
 30028        _pathsToPreserve = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 30029    }
 30
 31    /// <summary>Creates a new <see cref="PreservationBuilder"/> over <paramref name="query"/>.</summary>
 32    /// <param name="query">The source query to project from. Not mutated.</param>
 33    /// <exception cref="ArgumentNullException"><paramref name="query"/> is null.</exception>
 30334    public static PreservationBuilder Create(QueryBuilder query) => new(query);
 35
 36    /// <summary>
 37    /// Adds dotted field paths to the preservation set. When a more-specific child path is
 38    /// added, any previously-added parent paths are dropped automatically (you don't end up
 39    /// preserving both <c>user</c> and <c>user.name</c> — only the more specific wins).
 40    /// </summary>
 41    /// <param name="fieldPaths">Dot-separated field paths to preserve. Null/whitespace entries are skipped.</param>
 42    /// <returns>This builder, for chaining.</returns>
 43    public PreservationBuilder Preserve(params string[]? fieldPaths)
 44    {
 44745        if (fieldPaths == null || fieldPaths.Length == 0) return this;
 46
 228947        foreach (var path in fieldPaths.Where(p => !string.IsNullOrWhiteSpace(p)))
 48        {
 49            // Remove parent paths when adding more specific child
 47450            _pathsToPreserve.RemoveWhere(existing =>
 80751                path.StartsWith(existing + ".", StringComparison.OrdinalIgnoreCase));
 52
 47453            _pathsToPreserve.Add(path);
 54        }
 55
 42956        return this;
 57    }
 58
 59    /// <summary>
 60    /// Preserves <paramref name="fieldPath"/> within the subtree at <paramref name="nodePath"/>.
 61    /// Useful when the same field name appears multiple times in the tree and you want to
 62    /// scope the preservation to one specific parent.
 63    /// </summary>
 64    /// <param name="fieldPath">Field name (or dotted relative path) inside the node.</param>
 65    /// <param name="nodePath">Dotted path identifying which parent node to scope to.</param>
 66    /// <returns>This builder, for chaining.</returns>
 67    public PreservationBuilder PreserveAtPath(string fieldPath, string nodePath)
 68    {
 2769        var lastIndex = nodePath.LastIndexOf('.');
 2770        var lastSegment = lastIndex == -1 ? nodePath : nodePath.Substring(lastIndex + 1);
 2771        var fieldPathHasDot = fieldPath.Contains('.');
 72
 11473        foreach (var rootField in _sourceQuery.Definition.Fields.Values)
 74        {
 3075            PreserveAtPathForRoot(rootField.Alias ?? rootField.Name, fieldPath, nodePath, lastSegment, fieldPathHasDot);
 76        }
 2777        return this;
 78    }
 79
 80    private void PreserveAtPathForRoot(string rootName, string fieldPath, string nodePath, string lastSegment, bool fiel
 81    {
 3082        var pathToNode = _sourceQuery.GetPathTo(rootName, nodePath);
 3083        if (pathToNode.Length == 0) return;
 84
 3085        var fullNodePath = JoinPath(pathToNode, lastSegment);
 3086        var nodeField = QueryDefinitionExtensions.NavigatePath(_sourceQuery.Definition.Fields, fullNodePath.AsSpan(), ou
 3387        if (nodeField is null) return;
 3388        if (!nodeField.HasFields) return;
 89
 2190        if (fieldPathHasDot)
 91        {
 692            PreserveResolvedNestedPath(nodeField.Fields, fieldPath, fullNodePath);
 93        }
 94        else
 95        {
 1596            PreserveDirectMatch(nodeField.Fields, fieldPath, fullNodePath);
 97        }
 1598    }
 99
 100    private void PreserveResolvedNestedPath(IReadOnlyDictionary<string, Abstractions.FieldDefinition> nodeFields, string
 101    {
 6102        if (QueryDefinitionExtensions.NavigatePath(nodeFields, fieldPath.AsSpan(), out var resolved, fullNodePath) != nu
 6103            && resolved is not null)
 104        {
 3105            Preserve(resolved);
 106        }
 6107    }
 108
 109    private void PreserveDirectMatch(IReadOnlyDictionary<string, Abstractions.FieldDefinition> nodeFields, string fieldP
 110    {
 15111        var match = PreserveExtensions.FindFieldByNameOrAlias(nodeFields, fieldPath.AsSpan());
 15112        if (match.HasValue)
 113        {
 9114            Preserve(string.Concat(fullNodePath, ".", match.Value.Key));
 115        }
 15116    }
 117
 118    /// <summary>
 119    /// Builds <c>{segments[0]}.{segments[1]}...{segments[N-1]}.{tail}</c> in a single allocation —
 120    /// replaces the prior <c>string.Join + "." + tail</c> two-step.
 121    /// </summary>
 122    private static string JoinPath(string[] segments, string tail)
 123    {
 30124        var totalLength = tail.Length + segments.Length; // dots between segments + before tail
 204125        for (int i = 0; i < segments.Length; i++) totalLength += segments[i].Length;
 126
 30127        return string.Create(totalLength, (segments, tail), static (span, st) =>
 30128        {
 30129            var pos = 0;
 156130            for (int i = 0; i < st.segments.Length; i++)
 30131            {
 48132                var seg = st.segments[i].AsSpan();
 48133                seg.CopyTo(span[pos..]);
 48134                pos += seg.Length;
 48135                span[pos++] = '.';
 30136            }
 30137            st.tail.AsSpan().CopyTo(span[pos..]);
 60138        });
 139    }
 140
 141    /// <summary>
 142    /// Preserves every field touched by the predicate <paramref name="expression"/>. The
 143    /// expression is walked (not executed) to collect member access chains; comparisons,
 144    /// logical operators, ternaries, null-coalescing, and LINQ method calls (<c>Any</c>,
 145    /// <c>Where</c>, <c>First</c>) are all supported.
 146    /// </summary>
 147    /// <typeparam name="T">CLR type whose property hierarchy mirrors the query tree.</typeparam>
 148    /// <param name="expression">Predicate expression whose member access chains identify fields to preserve.</param>
 149    /// <param name="nodePath">Optional dotted path to scope preservation to a specific subtree.</param>
 150    /// <returns>This builder, for chaining.</returns>
 151    public PreservationBuilder PreserveFromExpression<T>(Expression<Func<T, bool>> expression, string? nodePath = null)
 78152        => PreserveFromExpressionCore(expression, nodePath, null, typeof(T), null);
 153
 154    /// <summary>
 155    /// Same as <see cref="PreserveFromExpression{T}(Expression{Func{T, bool}}, string?)"/>
 156    /// but adds a <paramref name="localMap"/> that translates expression parameter names
 157    /// to relative dotted paths inside <paramref name="nodePath"/>'s subtree. Useful when
 158    /// the predicate references multiple sibling fields.
 159    /// </summary>
 160    /// <param name="expression">Predicate expression.</param>
 161    /// <param name="nodePath">Dotted path to scope preservation to.</param>
 162    /// <param name="localMap">Map of expression-parameter name to relative path segments.</param>
 163    /// <returns>This builder, for chaining.</returns>
 164    public PreservationBuilder PreserveFromExpression<T>(Expression<Func<T, bool>> expression, string nodePath, Dictiona
 3165        => PreserveFromExpressionCore(expression, nodePath, localMap, typeof(T), null);
 166
 167    /// <summary>
 168    /// Untyped overload of <see cref="PreserveFromExpression{T}(Expression{Func{T, bool}}, string?)"/>.
 169    /// Useful when the predicate is constructed dynamically (e.g. via DynamicExpresso) and
 170    /// you don't have a compile-time <c>T</c>.
 171    /// </summary>
 172    /// <param name="expression">Lambda expression whose body is walked for member-access chains.</param>
 173    /// <param name="nodePath">Optional dotted path to scope preservation to.</param>
 174    /// <returns>This builder, for chaining.</returns>
 175    public PreservationBuilder PreserveFromExpression(Expression expression, string? nodePath = null)
 9176        => PreserveFromExpressionCore(expression, nodePath, null, InferTypeFromLambda(expression), null);
 177
 178    /// <summary>Untyped overload accepting a parameter-name-to-path <paramref name="localMap"/>.</summary>
 179    /// <param name="expression">Lambda expression.</param>
 180    /// <param name="nodePath">Dotted path to scope preservation to.</param>
 181    /// <param name="localMap">Map of expression-parameter name to relative path segments.</param>
 182    /// <returns>This builder, for chaining.</returns>
 183    public PreservationBuilder PreserveFromExpression(Expression expression, string nodePath, Dictionary<string, string[
 15184        => PreserveFromExpressionCore(expression, nodePath, localMap, InferTypeFromLambda(expression), null);
 185
 186    /// <summary>
 187    /// Untyped overload that, in addition to expression-derived paths, unconditionally
 188    /// preserves <paramref name="alwaysPreserveFields"/> at <paramref name="nodePath"/> —
 189    /// useful for IDs and tracking columns that callers always want regardless of predicate.
 190    /// </summary>
 191    /// <param name="expression">Lambda expression.</param>
 192    /// <param name="nodePath">Dotted path to scope preservation to.</param>
 193    /// <param name="localMap">Map of expression-parameter name to relative path segments.</param>
 194    /// <param name="alwaysPreserveFields">Field names always added under <paramref name="nodePath"/>.</param>
 195    /// <returns>This builder, for chaining.</returns>
 196    public PreservationBuilder PreserveFromExpression(Expression expression, string nodePath, Dictionary<string, string[
 27197        => PreserveFromExpressionCore(expression, nodePath, localMap, InferTypeFromLambda(expression), alwaysPreserveFie
 198
 199    /// <summary>
 200    /// Materializes a new <see cref="QueryBuilder"/> containing only the accumulated
 201    /// preserved paths. Returns the source builder unchanged when no paths were added.
 202    /// The source builder is never mutated.
 203    /// </summary>
 204    /// <returns>A new builder containing the preserved subset, or the source if no paths were added.</returns>
 205    public QueryBuilder Build()
 285206        => _pathsToPreserve.Count == 0
 285207            ? _sourceQuery
 285208            : _sourceQuery.Preserve(_pathsToPreserve.ToArray());
 209
 210    private static Type? InferTypeFromLambda(Expression expression)
 51211        => expression is LambdaExpression lambda && lambda.Parameters.Count > 0
 51212            ? lambda.Parameters[0].Type
 51213            : null;
 214
 215    private PreservationBuilder PreserveFromExpressionCore(
 216        Expression expression,
 217        string? nodePath,
 218        Dictionary<string, string[]>? localMap,
 219        Type? parameterType,
 220        string[]? alwaysPreserveFields)
 221    {
 363222        var processor = new ExpressionPreservationProcessor(_sourceQuery, path => Preserve(path));
 132223        processor.ProcessExpression(expression, nodePath, localMap, parameterType, alwaysPreserveFields);
 132224        return this;
 225    }
 226}

Methods/Properties

.ctor(NGql.Core.Builders.QueryBuilder)
Create(NGql.Core.Builders.QueryBuilder)
Preserve(System.String[])
PreserveAtPath(System.String,System.String)
PreserveAtPathForRoot(System.String,System.String,System.String,System.String,System.Boolean)
PreserveResolvedNestedPath(System.Collections.Generic.IReadOnlyDictionary`2<System.String,NGql.Core.Abstractions.FieldDefinition>,System.String,System.String)
PreserveDirectMatch(System.Collections.Generic.IReadOnlyDictionary`2<System.String,NGql.Core.Abstractions.FieldDefinition>,System.String,System.String)
JoinPath(System.String[],System.String)
PreserveFromExpression(System.Linq.Expressions.Expression`1<System.Func`2<T,System.Boolean>>,System.String)
PreserveFromExpression(System.Linq.Expressions.Expression`1<System.Func`2<T,System.Boolean>>,System.String,System.Collections.Generic.Dictionary`2<System.String,System.String[]>)
PreserveFromExpression(System.Linq.Expressions.Expression,System.String)
PreserveFromExpression(System.Linq.Expressions.Expression,System.String,System.Collections.Generic.Dictionary`2<System.String,System.String[]>)
PreserveFromExpression(System.Linq.Expressions.Expression,System.String,System.Collections.Generic.Dictionary`2<System.String,System.String[]>,System.String[])
Build()
InferTypeFromLambda(System.Linq.Expressions.Expression)
PreserveFromExpressionCore(System.Linq.Expressions.Expression,System.String,System.Collections.Generic.Dictionary`2<System.String,System.String[]>,System.Type,System.String[])