Extension for Moq to proxy method calls to a real implementation using reflection, with optional interception.
$ dotnet add package MoqExtensions.ReflectionProxyUnofficial extension for Moq that enables method call proxying to real implementations using reflection, with comprehensive interception capabilities.
MethodInfo or lambda expressionsdotnet add package MoqExtensions.ReflectionProxy
Forward individual methods to their real implementations while keeping others mocked:
var realService = new UserService(/* dependencies */);
var mock = new Mock<IUserService>();
// Forward only GetUserById to the real implementation
mock.Setup(x => x.GetUserById(It.IsAny<int>()))
.ForwardTo(realService);
// Other methods remain mocked
mock.Setup(x => x.CreateUser(It.IsAny<CreateUserRequest>()))
.Returns(new User { Id = 123 });
Automatically forward all interface methods to a real implementation:
serviceCollection.AddScoped<IUserService>(provider => {
var mock = new Mock<IUserService>();
// Add custom setups here if needed
mock.Setup(x => x.GetUserById(It.IsAny<int>()))
.Throws<Exception>();
// Forward all other calls to real implementation
return mock.DefaultForwardTo(provider.GetRequiredService<UserService>()).Object;
});
Add sophisticated interception logic for logging, monitoring, and behavior modification:
var interceptor = new MethodInterceptor(
onInterceptEntry: context =>
{
Console.WriteLine($"Calling {context.FromMethod.Name} with {context.ParameterValues.Count} arguments");
// Optional: Override method behavior
if (context.FromMethod.Name == "GetUserById" && (int)context.ParameterValues[0]! == 42)
{
return new InterceptSubstitution
{ ByValue = new OptionalNullable<object>(new User { Id = 42, Name = "ReplacedUser" }) };
}
// Store additional context for later interceptors
context.AdditionalContext = "some context";
return null; // Continue with normal execution
},
onInterceptException: context =>
{
Console.WriteLine($"Exception in {context.FromMethod.Name} after {context.GetElapsedTime()}ms: " +
$"{(context.IsUnwrapTask ? context.UnwrapException! : context.Exception!).Message}");
},
onInterceptValue: context =>
{
// context.AdditionalContext
var result = context.IsUnwrapTask ? context.UnwrapReturnTaskValue : context.ReturnValue.Value;
Console.WriteLine($"{context.FromMethod.Name} completed in {context.GetElapsedTime()}ms, " +
$"returned: {result}");
}
);
mock.SetupFunction(x => x.GetUserById)
.ForwardTo(realService, interceptor);
Replace Moq setup with reflection-based setup configuration:
// Traditional Moq approach
mock.Setup(x => x.SaveCity(It.IsAny<City>(), It.IsAny<CancellationToken>()))
.Callback<City, CancellationToken>((city, token) => {
// callback logic
});
// Reflection-based approach
mock.SetupAction(x => x.SaveCity)
.Callback<City, CancellationToken>((city, token) => {
// callback logic
});
// Traditional Moq approach
mock.Setup(x => x.SearchCity(It.IsAny<string>(), It.IsAny<Zone>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new City { Name = "Paris" });
// Reflection-based approach
mock.SetupFunction(x => x.SearchCity)
.HasReturnType<City>()
.ReturnsAsync(new City { Name = "Paris" });
For methods with overloads, generic parameters, or ambiguous signatures, use the explicit MethodInfo approach:
// Handle complex method signatures explicitly
var searchMethod = typeof(IUserService).GetMethod(nameof(IUserService.SearchUsers));
mock.SetupFunction(searchMethod)
.Returns(new List<User>());
| Method | Description |
|---|---|
ForwardTo | Forward mock calls to a real implementation |
DefaultForwardTo | Forward all interface methods to target |
SetupAction | Setup void methods via reflection |
SetupFunction | Setup methods with return values via reflection |
| Component | Description |
|---|---|
IMethodInterceptor | Interface for implementing custom interceptors |
MethodInterceptor | Built-in delegate-based interceptor implementation |
InvocationContext | Rich context object containing method and execution information |
InterceptSubstitution | Return object to override default method behavior |
The InvocationContext provides comprehensive information about method invocations
ref and out parameters are not currently supportedThis project is licensed under the MIT License - see the LICENSE file for complete details.