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

Information
Class: AsyncResponse.CallbackExpressionConverter<TService>
Assembly: AsyncResponse.Core
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/CallbackExpressionConverter.cs
Line coverage
98%
Covered lines: 52
Uncovered lines: 1
Coverable lines: 53
Total lines: 135
Line coverage: 98.1%
Branch coverage
88%
Covered branches: 39
Total branches: 44
Branch coverage: 88.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ToReflectionCall<TService>(...)100%11100%
ToReflectionCall<TService>(...)100%11100%
ToReflectionCall<TService>(...)100%11100%
Build<TService>(...)87.5%88100%
ConvertArgument(...)95%202092.31%
EvaluateArgument(...)87.5%88100%
ContainsMethodCall(...)100%11100%
ReferencesParameter(...)100%11100%
Visit(...)66.67%66100%
.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)
 215        => 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)
 319        => 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)
 223        => Build<TService>(expression.Body);
 24
 25    private static ReflectionCallDto Build<TService>(Expression body)
 26    {
 327        if (body is not MethodCallExpression call)
 228            throw new NotSupportedException($"Only direct method calls are supported. Got: {body.NodeType}");
 29
 30        // instance call on the lambda parameter (svc => svc.Method(...))
 331        if (call.Object is not ParameterExpression svcParam || svcParam.Type != typeof(TService))
 232            throw new NotSupportedException("Lambda must be like: svc => svc.YourMethod(args)");
 33
 334        var args = call.Arguments.Select(arg => ConvertArgument(arg, svcParam)).ToArray();
 35
 336        return new ReflectionCallDto
 337        {
 338            ServiceInterfaceFullName = typeof(TService).FullName
 339                ?? throw new InvalidOperationException("Service interface must have FullName."),
 340            MethodName = call.Method.Name,
 341            Params = args
 342        };
 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.
 349        if (expression is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } conversi
 350            && conversion.Method is null
 351            && conversion.Type.IsAssignableFrom(conversion.Operand.Type))
 352            expression = conversion.Operand;
 53
 54        // Placeholder.Payload<T>() / Placeholder.Exception() / Placeholder.CorrelationId()
 55        // used directly as an argument become runtime placeholders.
 356        if (expression is MethodCallExpression marker && marker.Method.DeclaringType == typeof(Placeholder))
 57        {
 358            return marker.Method.Name switch
 359            {
 360                nameof(Placeholder.Payload) => CallbackParam.ForPlaceholder(PlaceholderType.Payload),
 361                nameof(Placeholder.Exception) => CallbackParam.ForPlaceholder(PlaceholderType.Exception),
 362                nameof(Placeholder.CorrelationId) => CallbackParam.ForPlaceholder(PlaceholderType.CorrelationId),
 063                _ => throw new NotSupportedException($"Unknown placeholder marker '{marker.Method.Name}'.")
 364            };
 65        }
 66
 367        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    {
 375        if (ContainsMethodCall(expression))
 276            throw new NotSupportedException(
 277                "Arguments must be constants/new/array/member access, or a top-level Placeholder marker; " +
 278                "other method-call arguments are not allowed.");
 79
 380        if (ReferencesParameter(expression, svcParam))
 281            throw new NotSupportedException("Argument must not reference the service parameter (e.g., svc => svc.M(svc.P
 82
 383        if (expression is ConstantExpression constant)
 84        {
 385            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.
 390        var boxed = expression.Type == typeof(object)
 391            ? expression
 392            : Expression.Convert(expression, typeof(object));
 393        var lambda = Expression.Lambda<Func<object?>>(boxed);
 394        return lambda.Compile(preferInterpretation: true)();
 95    }
 96
 97    private static bool ContainsMethodCall(Expression expression)
 98    {
 399        var visitor = new MethodCallGuard();
 3100        visitor.Visit(expression);
 3101        return visitor.Found;
 102    }
 103
 104    private static bool ReferencesParameter(Expression expression, ParameterExpression parameter)
 105    {
 3106        var visitor = new ParameterGuard(parameter);
 3107        visitor.Visit(expression);
 3108        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        {
 3118            if (Found || node is null) return node;
 3119            if (node.NodeType == ExpressionType.Call) { Found = true; return node; }
 3120            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}