< Summary - AsyncResponse (Release / net8.0+net10.0 / unit+integration)

Information
Class: AsyncResponse.CallbackExpressionConverter.ParameterGuard
Assembly: AsyncResponse.Core
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/CallbackExpressionConverter.cs
Line coverage
100%
Covered lines: 3
Uncovered lines: 0
Coverable lines: 3
Total lines: 135
Line coverage: 100%
Branch coverage
100%
Covered branches: 2
Total branches: 2
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
VisitParameter(...)100%22100%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/CallbackExpressionConverter.cs

#LineLine coverage
 1using System.Linq.Expressions;
 2
 3namespace AsyncResponse;
 4
 5/// <summary>
 6/// Converts strongly-typed lambda expressions into serializable <see cref="ReflectionCallDto"/>s.
 7/// Literal arguments are evaluated and captured by value; <see cref="Placeholder"/> marker calls
 8/// become runtime placeholders. This gives compile-time safety (rename-refactoring, type checks)
 9/// over hand-written reflection descriptors.
 10/// </summary>
 11internal static class CallbackExpressionConverter
 12{
 13    /// <summary>Converts the callback expression to a reflection call descriptor.</summary>
 14    public static ReflectionCallDto ToReflectionCall<TService>(Expression<Action<TService>> expression)
 15        => Build<TService>(expression.Body);
 16
 17    /// <summary>Converts the callback expression to a reflection call descriptor.</summary>
 18    public static ReflectionCallDto ToReflectionCall<TService>(Expression<Func<TService, Task>> expression)
 19        => Build<TService>(expression.Body);
 20
 21    /// <summary>Converts the callback expression to a reflection call descriptor.</summary>
 22    public static ReflectionCallDto ToReflectionCall<TService>(Expression<Func<TService, ValueTask>> expression)
 23        => Build<TService>(expression.Body);
 24
 25    private static ReflectionCallDto Build<TService>(Expression body)
 26    {
 27        if (body is not MethodCallExpression call)
 28            throw new NotSupportedException($"Only direct method calls are supported. Got: {body.NodeType}");
 29
 30        // instance call on the lambda parameter (svc => svc.Method(...))
 31        if (call.Object is not ParameterExpression svcParam || svcParam.Type != typeof(TService))
 32            throw new NotSupportedException("Lambda must be like: svc => svc.YourMethod(args)");
 33
 34        var args = call.Arguments.Select(arg => ConvertArgument(arg, svcParam)).ToArray();
 35
 36        return new ReflectionCallDto
 37        {
 38            ServiceInterfaceFullName = typeof(TService).FullName
 39                ?? throw new InvalidOperationException("Service interface must have FullName."),
 40            MethodName = call.Method.Name,
 41            Params = args
 42        };
 43    }
 44
 45    private static CallbackParam ConvertArgument(Expression expression, ParameterExpression svcParam)
 46    {
 47        // A generic payload passed to an object parameter is wrapped in an implicit Convert node.
 48        // Strip only that harmless boxing/reference conversion so the marker remains top-level.
 49        if (expression is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } conversi
 50            && conversion.Method is null
 51            && conversion.Type.IsAssignableFrom(conversion.Operand.Type))
 52            expression = conversion.Operand;
 53
 54        // Placeholder.Payload<T>() / Placeholder.Exception() / Placeholder.CorrelationId()
 55        // used directly as an argument become runtime placeholders.
 56        if (expression is MethodCallExpression marker && marker.Method.DeclaringType == typeof(Placeholder))
 57        {
 58            return marker.Method.Name switch
 59            {
 60                nameof(Placeholder.Payload) => CallbackParam.ForPlaceholder(PlaceholderType.Payload),
 61                nameof(Placeholder.Exception) => CallbackParam.ForPlaceholder(PlaceholderType.Exception),
 62                nameof(Placeholder.CorrelationId) => CallbackParam.ForPlaceholder(PlaceholderType.CorrelationId),
 63                _ => throw new NotSupportedException($"Unknown placeholder marker '{marker.Method.Name}'.")
 64            };
 65        }
 66
 67        return CallbackParam.ForValue(EvaluateArgument(expression, svcParam));
 68    }
 69
 70    // Evaluate each argument expression as a closed lambda. Disallow:
 71    //  - method calls inside args (conservative; placeholders are handled above)
 72    //  - any reference to the svc parameter
 73    private static object? EvaluateArgument(Expression expression, ParameterExpression svcParam)
 74    {
 75        if (ContainsMethodCall(expression))
 76            throw new NotSupportedException(
 77                "Arguments must be constants/new/array/member access, or a top-level Placeholder marker; " +
 78                "other method-call arguments are not allowed.");
 79
 80        if (ReferencesParameter(expression, svcParam))
 81            throw new NotSupportedException("Argument must not reference the service parameter (e.g., svc => svc.M(svc.P
 82
 83        if (expression is ConstantExpression constant)
 84        {
 85            return constant.Value;
 86        }
 87
 88        // Use the interpreter to avoid JIT'ing lots of tiny dynamic methods, but keep the delegate
 89        // typed so argument capture does not pay DynamicInvoke's reflection dispatch cost.
 90        var boxed = expression.Type == typeof(object)
 91            ? expression
 92            : Expression.Convert(expression, typeof(object));
 93        var lambda = Expression.Lambda<Func<object?>>(boxed);
 94        return lambda.Compile(preferInterpretation: true)();
 95    }
 96
 97    private static bool ContainsMethodCall(Expression expression)
 98    {
 99        var visitor = new MethodCallGuard();
 100        visitor.Visit(expression);
 101        return visitor.Found;
 102    }
 103
 104    private static bool ReferencesParameter(Expression expression, ParameterExpression parameter)
 105    {
 106        var visitor = new ParameterGuard(parameter);
 107        visitor.Visit(expression);
 108        return visitor.Found;
 109    }
 110
 111    private sealed class MethodCallGuard : ExpressionVisitor
 112    {
 113        public bool Found;
 114
 115        /// <summary>Runs the Visit operation.</summary>
 116        public override Expression? Visit(Expression? node)
 117        {
 118            if (Found || node is null) return node;
 119            if (node.NodeType == ExpressionType.Call) { Found = true; return node; }
 120            return base.Visit(node);
 121        }
 122    }
 123
 3124    private sealed class ParameterGuard(ParameterExpression _parameter) : ExpressionVisitor
 125    {
 126        public bool Found;
 127
 128        /// <summary>Runs the VisitParameter operation.</summary>
 129        protected override Expression VisitParameter(ParameterExpression node)
 130        {
 2131            if (node == _parameter) Found = true;
 2132            return base.VisitParameter(node);
 133        }
 134    }
 135}