devlooped/moq · error · ArgumentException

Unsupported expression

Error message

Unsupported expression: {0}
{1}

What it means

Moq's ActionObserver.ReconstructExpression rebuilds a LINQ expression for a delegate-based setup after the fact. When a recorder was attached to the action parameter but recorded no invocation, the intercepted call chain could not be reconstructed into a setup expression, so Moq throws ArgumentException with the unsupported expression plus a hint. This protects users from silently creating a setup that would never match.

Solutions

  1. Ensure the lambda passed to the action parameter actually calls at least one member on the mock (e.g. `x => x.Save()` instead of `x => { }`).
  2. Verify the invoked member is mockable (virtual, interface member, or overridable) so it can be intercepted by the recorder.
  3. Read the hint appended after the expression (NextMemberNonInterceptable) and make the last member in the chain interceptable.
  4. If the call cannot be made interceptable, switch to a conventional `mock.Setup(...)` on the interface/class directly instead of the expression-reconstruction API.

Example fix

// before
mock.SetupAction(x => { }); // no invocation recorded
// after
mock.SetupAction(x => x.Save());
Defensive patterns

Strategy: validation

Validate before calling

// Before calling the observer/setup API, ensure the lambda invokes at least one member
Action<MyMock> check = myMock;
if (lambdaBodyContainsNoInvocation) throw new InvalidOperationException("Lambda must invoke a mockable member");

Type guard

static bool HasInvocation(LambdaExpression e) =>
    e.Body is MemberExpression || e.Body is MethodCallExpression || e.Body is InvocationExpression;

Try / catch

try
{
    mock.SetupAction(lambda);
}
catch (ArgumentException ex) when (ex.Message.StartsWith("Unsupported expression"))
{
    // fall back to explicit mock.Setup(...) or fix the lambda
}

Prevention

When it happens

Trigger: Calling a Mock.Of/observer-style API where the delegate passed to the action parameter never invoked any member on the mocked object — e.g. `mock.SetupAction(x => { }, ...)` or a lambda whose body touches nothing interceptable, so the recorder has zero recorded invocations when ReconstructExpression runs.

Common situations: Empty or no-op lambdas handed to delegate-based setups; lambdas invoking non-virtual/non-interceptable members; refactoring a lambda so the mock call is removed; conditional code paths where the mocked member call is behind an if that never executes.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of devlooped/moq@89a5be629c (2026-09-16). Data as JSON: /api/errors/24e28223e50b18a2. Report an issue: GitHub.

Appendix: source

Thrown at src/Moq/ActionObserver.cs:83

                        if (resultType.IsAssignableFrom(body.Type) == false)
                        {
                            if (AwaitableFactory.TryGet(body.Type) is { } awaitableHandler
                                && awaitableHandler.ResultType.IsAssignableFrom(resultType))
                            {
                                // We are here because the current invocation cannot be chained onto the previous one,
                                // however it *can* be chained if we assume that there was a `.Result` query on the
                                // former invocation that we don't see because non-virtual members aren't recorded.
                                // In this case, we make things work by adding back the missing `.Result`:
                                body = awaitableHandler.CreateResultExpression(body);
                            }
                        }
                        body = Expression.Call(body, invocation.Method, GetArgumentExpressions(invocation, recorder.Matches.ToArray()));
                    }
                    else
                    {
                        // A recorder was set up, but it recorded no invocation. This means
                        // that the invocation could not be intercepted:
                        throw new ArgumentException(
                            string.Format(
                                CultureInfo.CurrentCulture,
                                Resources.UnsupportedExpressionWithHint,
                                $"{actionParameterName} => {body.ToStringFixed()}...",
                                Resources.NextMemberNonInterceptable));
                    }
                }

                // Now we've either got no error and a completely reconstructed expression, or
                // we have an error and a partially reconstructed expression which we can use for
                // diagnostic purposes:
                if (error == null)
                {
                    return Expression.Lambda<Action<T>>(body.Apply(UpgradePropertyAccessorMethods.Rewriter), rootExpression);
                }
                else
                {
                    throw new ArgumentException(

View on GitHub (pinned to 89a5be629c)