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

Information
Class: AsyncResponse.CallbackExpressionConverter
Assembly: AsyncResponse.Core
File(s): /_/src/AsyncResponse.Core/CallbackExpressionConverter.cs
Line coverage
98%
Covered lines: 58
Uncovered lines: 1
Coverable lines: 59
Total lines: 161
Line coverage: 98.3%
Branch coverage
88%
Covered branches: 46
Total branches: 52
Branch coverage: 88.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ToReflectionCall(...)100%11100%
ToReflectionCall(...)100%11100%
ToReflectionCall(...)100%11100%
Build(...)87.5%88100%
ConvertArgument(...)90%202092.3%
EvaluateArgument(...)93.75%1616100%
ContainsMethodCall(...)100%11100%
ReferencesParameter(...)100%11100%
Visit(...)83.33%66100%
.ctor(...)100%11100%
VisitParameter(...)50%22100%

File(s)

/_/src/AsyncResponse.Core/CallbackExpressionConverter.cs

#LineLine coverage
 1using System.Diagnostics.CodeAnalysis;
 2using System.Linq.Expressions;
 3using System.Reflection;
 4
 5namespace AsyncResponse;
 6
 7/// <summary>
 8/// Converts strongly-typed lambda expressions into serializable <see cref="ReflectionCallDto"/>s.
 9/// Literal arguments are evaluated and captured by value; <see cref="Placeholder"/> marker calls
 10/// become runtime placeholders. This gives compile-time safety (rename-refactoring, type checks)
 11/// over hand-written reflection descriptors.
 12/// </summary>
 13internal static class CallbackExpressionConverter
 14{
 15    /// <summary>Converts the callback expression to a reflection call descriptor.</summary>
 16    public static ReflectionCallDto ToReflectionCall<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMe
 217        => Build<TService>(expression.Body);
 18
 19    /// <summary>Converts the callback expression to a reflection call descriptor.</summary>
 20    public static ReflectionCallDto ToReflectionCall<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMe
 932221        => Build<TService>(expression.Body);
 22
 23    /// <summary>Converts the callback expression to a reflection call descriptor.</summary>
 24    public static ReflectionCallDto ToReflectionCall<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMe
 225        => Build<TService>(expression.Body);
 26
 27    private static ReflectionCallDto Build<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TS
 28    {
 932629        if (body is not MethodCallExpression call)
 230            throw new NotSupportedException($"Only direct method calls are supported. Got: {body.NodeType}");
 31
 32        // instance call on the lambda parameter (svc => svc.Method(...))
 932433        if (call.Object is not ParameterExpression svcParam || svcParam.Type != typeof(TService))
 234            throw new NotSupportedException("Lambda must be like: svc => svc.YourMethod(args)");
 35
 2448936        var args = call.Arguments.Select(arg => ConvertArgument(arg, svcParam)).ToArray();
 37
 38        // The compiler resolved `call.Method` from full signatures, but the descriptor persists only
 39        // its NAME and ARITY — the wire contract every deployment reading it shares. Validate here,
 40        // where the caller's stack is, that name + arity still select exactly this one method (no
 41        // overload set sharing both, no by-ref or open-generic parameters). Without this an
 42        // interface such as `Run(int)` / `Run(string)` accepted `svc => svc.Run(1)` and every
 43        // dispatch of the job then failed as ambiguous — after publication, on a worker, burning
 44        // the transport's retries or stranding a recovery registration.
 931845        ReflectionExtensions.EnsureBindable(typeof(TService), call.Method.Name, args.Length);
 46
 931247        return new ReflectionCallDto
 931248        {
 931249            ServiceInterfaceFullName = typeof(TService).FullName
 931250                ?? throw new InvalidOperationException("Service interface must have FullName."),
 931251            MethodName = call.Method.Name,
 931252            Params = args
 931253        };
 54    }
 55
 56    private static CallbackParam ConvertArgument(Expression expression, ParameterExpression svcParam)
 57    {
 58        // A generic payload passed to an object parameter is wrapped in an implicit Convert node.
 59        // Strip only that harmless boxing/reference conversion so the marker remains top-level.
 1516760        if (expression is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } conversi
 1516761            && conversion.Method is null
 1516762            && conversion.Type.IsAssignableFrom(conversion.Operand.Type))
 149163            expression = conversion.Operand;
 64
 65        // Placeholder.Payload<T>() / Placeholder.Exception() / Placeholder.CorrelationId()
 66        // used directly as an argument become runtime placeholders.
 1516767        if (expression is MethodCallExpression marker && marker.Method.DeclaringType == typeof(Placeholder))
 68        {
 601469            return marker.Method.Name switch
 601470            {
 151371                nameof(Placeholder.Payload) => CallbackParam.ForPlaceholder(PlaceholderType.Payload),
 149972                nameof(Placeholder.Exception) => CallbackParam.ForPlaceholder(PlaceholderType.Exception),
 300273                nameof(Placeholder.CorrelationId) => CallbackParam.ForPlaceholder(PlaceholderType.CorrelationId),
 074                _ => throw new NotSupportedException($"Unknown placeholder marker '{marker.Method.Name}'.")
 601475            };
 76        }
 77
 915378        return CallbackParam.ForValue(EvaluateArgument(expression, svcParam));
 79    }
 80
 81    // Evaluate each argument expression as a closed lambda. Disallow:
 82    //  - method calls inside args (conservative; placeholders are handled above)
 83    //  - any reference to the svc parameter
 84    private static object? EvaluateArgument(Expression expression, ParameterExpression svcParam)
 85    {
 915386        if (ContainsMethodCall(expression))
 287            throw new NotSupportedException(
 288                "Arguments must be constants/new/array/member access, or a top-level Placeholder marker; " +
 289                "other method-call arguments are not allowed.");
 90
 915191        if (ReferencesParameter(expression, svcParam))
 292            throw new NotSupportedException("Argument must not reference the service parameter (e.g., svc => svc.M(svc.P
 93
 914994        if (expression is ConstantExpression constant)
 95        {
 74896            return constant.Value;
 97        }
 98
 99        // The dominant non-constant shape is a C# closure capture — a field read off the
 100        // compiler's display class (a ConstantExpression), or a static field. Read it directly:
 101        // building and interpreting a lambda for it cost ~60x in time and allocations, per
 102        // argument, per enqueue. Fields cannot throw, so behavior is identical to the
 103        // interpreted read. Anything deeper (nested members, properties, new/array) falls
 104        // through to the interpreter below.
 8401105        if (expression is MemberExpression { Member: FieldInfo field } fieldAccess)
 106        {
 8387107            if (fieldAccess.Expression is ConstantExpression closure)
 8373108                return field.GetValue(closure.Value);
 109
 14110            if (fieldAccess.Expression is null)
 2111                return field.GetValue(null);
 112        }
 113
 114        // Use the interpreter to avoid JIT'ing lots of tiny dynamic methods, but keep the delegate
 115        // typed so argument capture does not pay DynamicInvoke's reflection dispatch cost.
 26116        var boxed = expression.Type == typeof(object)
 26117            ? expression
 26118            : Expression.Convert(expression, typeof(object));
 26119        var lambda = Expression.Lambda<Func<object?>>(boxed);
 26120        return lambda.Compile(preferInterpretation: true)();
 121    }
 122
 123    private static bool ContainsMethodCall(Expression expression)
 124    {
 9153125        var visitor = new MethodCallGuard();
 9153126        visitor.Visit(expression);
 9153127        return visitor.Found;
 128    }
 129
 130    private static bool ReferencesParameter(Expression expression, ParameterExpression parameter)
 131    {
 9151132        var visitor = new ParameterGuard(parameter);
 9151133        visitor.Visit(expression);
 9151134        return visitor.Found;
 135    }
 136
 137    private sealed class MethodCallGuard : ExpressionVisitor
 138    {
 139        public bool Found;
 140
 141        /// <summary>Runs the Visit operation.</summary>
 142        public override Expression? Visit(Expression? node)
 143        {
 17584144            if (Found || node is null) return node;
 17580145            if (node.NodeType == ExpressionType.Call) { Found = true; return node; }
 17574146            return base.Visit(node);
 147        }
 148    }
 149
 9151150    private sealed class ParameterGuard(ParameterExpression _parameter) : ExpressionVisitor
 151    {
 152        public bool Found;
 153
 154        /// <summary>Runs the VisitParameter operation.</summary>
 155        protected override Expression VisitParameter(ParameterExpression node)
 156        {
 4157            if (node == _parameter) Found = true;
 2158            return base.VisitParameter(node);
 159        }
 160    }
 161}