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
- 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 => { }`).
- Verify the invoked member is mockable (virtual, interface member, or overridable) so it can be intercepted by the recorder.
- Read the hint appended after the expression (NextMemberNonInterceptable) and make the last member in the chain interceptable.
- 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
- Never pass empty or no-op lambdas to delegate-based setup APIs
- Ensure every member invoked in the lambda is virtual/interface-based
- Read the appended hint in the message to find the non-interceptable member
- Prefer explicit mock.Setup(...) for non-trivial lambdas
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
- Unsupported expression
- Expression is not a property access
- Expression involves a field access, which is not supported…
- Property . does not have a getter.
- Property . does not have a setter.
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)