A Moq extension that allows you to set up a mock to proxy/forward all calls to a real implementation, while still being able to verify calls and override specific behaviors.
$ dotnet add package geoder101.MoqProxyA powerful extension for Moq that enables proxy pattern mocking - forward calls from a mock to a real implementation while maintaining full verification capabilities.
MoqProxy bridges the gap between full mocking and real implementations, giving you the best of both worlds:
Verify() to assert method calls on the real implementationCallBase, works seamlessly with interface mocksCallBase = true?| Feature | MoqProxy (SetupAsProxy) | CallBase = true |
|---|---|---|
| Use case | ✅ Spy on existing objects, test decorators | ⚠️ Partial mocking of concrete classes |
| Works with interfaces | ✅ Yes - forwards to any implementation | ❌ No - interfaces have no base implementation |
| Separate implementation | ✅ Forwards to a different instance | ❌ Only calls the mock's own base methods |
| Property synchronization | ✅ Mock and implementation stay in sync | ⚠️ Only if mock is the implementation |
| Event forwarding | ✅ Event subscriptions forwarded | ⚠️ Only if mock is the implementation |
| Generic method support | ✅ Full support via custom interceptor | ✅ Supported |
| Indexer support | ✅ 1-2 parameter indexers | ✅ Supported |
Key Difference: CallBase = true only works with abstract or virtual members of the mocked class itself.
SetupAsProxy works with interfaces and forwards calls to a separate implementation instance, making it perfect
for the spy pattern and testing decorators.
public interface ICalculator
{
int Add(int x, int y);
}
public class Calculator : ICalculator
{
public int Add(int x, int y) => x + y;
}
// ❌ This DOESN'T work - interface has no base implementation
var mock = new Mock<ICalculator> { CallBase = true };
mock.Object.Add(2, 3); // Throws - no implementation!
// ✅ This DOES work - forwards to real implementation
var realCalc = new Calculator();
var mock = new Mock<ICalculator>();
mock.SetupAsProxy(realCalc);
mock.Object.Add(2, 3); // Returns 5, calls realCalc.Add(2, 3)
dotnet add package geoder101.MoqProxy
For ASP.NET Core and Microsoft.Extensions.DependencyInjection scenarios, install the integration package:
dotnet add package geoder101.MoqProxy.DependencyInjection.Microsoft
This package allows you to wrap services registered in your DI container with Moq proxies, making it easy to verify calls and spy on real implementations in integration tests. See the package README for details.
using Moq;
using MoqProxy;
// Create a mock and a real implementation
var realService = new MyService();
var mock = new Mock<IMyService>();
// Set up the mock to proxy all calls to the real implementation
mock.SetupAsProxy(realService);
// Use the mock - calls are forwarded to realService
mock.Object.DoSomething();
// Verify the call was made
mock.Verify(m => m.DoSomething(), Times.Once);
+=) forwarding to implementation-=) forwarding to implementationEventHandler and EventHandler<TEventArgs> patternsTask and Task<T>this[int index])this[int x, int y])mock.Reset() then SetupAsProxy() again to restore proxyingSpy on specific methods to observe parameters and return values while still forwarding calls to the real implementation:
var impl = new Calculator();
var mock = new Mock<ICalculator>();
mock.SetupAsProxy(impl);
// Spy on a method with a callback that receives parameters
var capturedParams = new List<(int x, int y)>();
mock.Spy(
m => m.Add(It.IsAny<int>(), It.IsAny<int>()),
(int x, int y) => capturedParams.Add((x, y)));
var result1 = mock.Object.Add(2, 3); // Returns 5, forwards to impl
var result2 = mock.Object.Add(10, 20); // Returns 30, forwards to impl
// The callback captured all parameters
Assert.Equal(2, capturedParams.Count);
Assert.Equal((2, 3), capturedParams[0]);
Assert.Equal((10, 20), capturedParams[1]);
// You can also capture the return value
var capturedResults = new List<(int x, int y, int result)>();
mock.Spy(
m => m.Add(It.IsAny<int>(), It.IsAny<int>()),
(int x, int y, int result) => capturedResults.Add((x, y, result)));
mock.Object.Add(5, 7); // Returns 12
Assert.Equal((5, 7, 12), capturedResults[0]);
// Verify the calls were made
mock.Verify(m => m.Add(It.IsAny<int>(), It.IsAny<int>()), Times.Exactly(3));
Retrieve the real implementation instance from a mock proxy:
var impl = new Calculator();
var mock = new Mock<ICalculator>();
mock.SetupAsProxy(impl);
// Get the upstream implementation (returns null if not a proxy)
var upstream = MockProxy.GetUpstreamInstance(mock.Object);
Assert.NotNull(upstream);
Assert.Same(impl, upstream);
// Get the Mock<T> from any mock instance (returns null if not a mock)
var retrievedMock = MockProxy.GetMock(mock.Object);
Assert.NotNull(retrievedMock);
Assert.Same(mock, retrievedMock);
var impl = new Calculator();
var mock = new Mock<ICalculator>();
mock.SetupAsProxy(impl);
var result = mock.Object.Add(2, 3);
Assert.Equal(5, result);
mock.Verify(m => m.Add(2, 3), Times.Once);
var impl = new Calculator();
var mock = new Mock<ICalculator>();
mock.SetupAsProxy(impl);
// Override specific behavior
mock.Setup(m => m.Add(2, 3)).Returns(100);
// This call uses the override
Assert.Equal(100, mock.Object.Add(2, 3));
// Other calls are forwarded to the real implementation
Assert.Equal(7, mock.Object.Add(3, 4));
This is where MoqProxy really shines - testing decorator patterns:
public class CachingCalculatorDecorator : ICalculator
{
private readonly ICalculator _inner;
private readonly Dictionary<(int, int), int> _cache = new();
public CachingCalculatorDecorator(ICalculator inner)
{
_inner = inner;
}
public int Add(int x, int y)
{
if (_cache.TryGetValue((x, y), out var cached))
return cached;
var result = _inner.Add(x, y);
_cache[(x, y)] = result;
return result;
}
}
var impl = new Calculator();
var mock = new Mock<ICalculator>();
mock.SetupAsProxy(impl);
var decorator = new CachingCalculatorDecorator(mock.Object);
// First call - should call through
decorator.Add(2, 3);
mock.Verify(m => m.Add(2, 3), Times.Once);
// Second call - should be cached
decorator.Add(2, 3);
mock.Verify(m => m.Add(2, 3), Times.Once); // Still once - decorator cached it!
public interface IAsyncService
{
Task<string> GetDataAsync(int id);
Task ProcessAsync();
}
public class AsyncService : IAsyncService
{
public async Task<string> GetDataAsync(int id)
{
await Task.Delay(100); // Simulate async work
return $"Data for ID: {id}";
}
public async Task ProcessAsync()
{
await Task.Delay(100); // Simulate async processing
}
}
var impl = new AsyncService();
var mock = new Mock<IAsyncService>();
mock.SetupAsProxy(impl);
var result = await mock.Object.GetDataAsync(42);
await mock.Object.ProcessAsync();
mock.Verify(m => m.GetDataAsync(42), Times.Once);
mock.Verify(m => m.ProcessAsync(), Times.Once);
public interface IConfig
{
string ConnectionString { get; set; }
}
public class Config : IConfig
{
public string ConnectionString { get; set; } = string.Empty;
}
var impl = new Config { ConnectionString = "Server=localhost" };
var mock = new Mock<IConfig>();
mock.SetupAsProxy(impl);
// Get property
Assert.Equal("Server=localhost", mock.Object.ConnectionString);
// Set property through mock
mock.Object.ConnectionString = "Server=production";
// Change is reflected in the implementation
Assert.Equal("Server=production", impl.ConnectionString);
// Both mock and impl are synchronized
Assert.Equal(impl.ConnectionString, mock.Object.ConnectionString);
public interface IMatrix
{
int this[int x, int y] { get; set; }
}
var impl = new Matrix();
var mock = new Mock<IMatrix>();
mock.SetupAsProxy(impl);
// Set through indexer
impl[0, 0] = 42; // Caution: `mock.Object[0, 0] = 42;` would not forward to impl due to how Moq handles indexer setters
// Get through indexer
var value = mock.Object[0, 0];
Assert.Equal(42, value);
Assert.Equal(42, impl[0, 0]); // Synchronized
public class DataEventArgs : EventArgs
{
public string Data { get; set; } = string.Empty;
}
public interface INotifier
{
event EventHandler? StatusChanged;
event EventHandler<DataEventArgs>? DataReceived;
void UpdateStatus();
void NotifyData(string data);
}
public class Notifier : INotifier
{
public event EventHandler? StatusChanged;
public event EventHandler<DataEventArgs>? DataReceived;
public void UpdateStatus()
{
StatusChanged?.Invoke(this, EventArgs.Empty);
}
public void NotifyData(string data)
{
DataReceived?.Invoke(this, new DataEventArgs { Data = data });
}
}
var impl = new Notifier();
var mock = new Mock<INotifier>();
mock.SetupAsProxy(impl);
var statusChangedCount = 0;
var receivedData = new List<string>();
// Subscribe to events on the mock
mock.Object.StatusChanged += (sender, e) => statusChangedCount++;
mock.Object.DataReceived += (sender, e) => receivedData.Add(e.Data);
// When implementation raises events, handlers subscribed to mock are invoked
mock.Object.UpdateStatus(); // Raises StatusChanged
Assert.Equal(1, statusChangedCount);
// Works with custom event args
mock.Object.NotifyData("Hello"); // Raises DataReceived
Assert.Single(receivedData);
Assert.Equal("Hello", receivedData[0]);
// Verify event-related interactions if needed
mock.Verify(m => m.UpdateStatus(), Times.Once);
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
public interface IRepository
{
T GetById<T>(int id) where T : class;
void Save<T>(T entity) where T : class;
}
public class Repository : IRepository
{
public T GetById<T>(int id) where T : class
{
// Simulate fetching from a data source
return (Activator.CreateInstance(typeof(T)) as T)!;
}
public void Save<T>(T entity) where T : class
{
// Simulate saving to a data source
}
}
var impl = new Repository();
var mock = new Mock<IRepository>();
mock.SetupAsProxy(impl);
var user = mock.Object.GetById<User>(123);
mock.Object.Save(user);
mock.Verify(m => m.GetById<User>(123), Times.Once);
mock.Verify(m => m.Save(user), Times.Once);
public interface IParser
{
bool TryParse(string input, out int result);
void Increment(ref int value);
}
public class Parser : IParser
{
public bool TryParse(string input, out int result)
{
return int.TryParse(input, out result);
}
public void Increment(ref int value)
{
value++;
}
}
var impl = new Parser();
var mock = new Mock<IParser>();
mock.SetupAsProxy(impl);
// Out parameters are automatically forwarded
var success = mock.Object.TryParse("123", out var value);
Assert.True(success);
Assert.Equal(123, value);
// Ref parameters work too
int number = 5;
mock.Object.Increment(ref number);
Assert.Equal(6, number);
// Verify calls with It.Ref<T>.IsAny
mock.Verify(m => m.TryParse("123", out It.Ref<int>.IsAny), Times.Once);
mock.Verify(m => m.Increment(ref It.Ref<int>.IsAny), Times.Once);
var impl = new Calculator();
var mock = new Mock<ICalculator>();
mock.SetupAsProxy(impl);
// Use the mock...
mock.Object.Add(2, 3);
// Override some behavior
mock.Setup(m => m.Add(It.IsAny<int>(), It.IsAny<int>())).Returns(999);
// Reset and reapply proxying
mock.Reset();
mock.SetupAsProxy(impl);
// Now back to forwarding to real implementation
Assert.Equal(5, mock.Object.Add(2, 3));
It.Ref<T>.IsAny, but cannot verify specific out values (Moq limitation).Span<T>, ReadOnlySpan<T>): Not supported due to C# expression tree limitationsMoqProxy uses a sophisticated approach to enable proxy mocking:
MethodInfo.Invoke for generic methods that can't be represented in expression
treesNullReturnValue sentinel to detect when no explicit setup was matched,
triggering fallback to the real implementationThe library handles complex scenarios including:
# Clone the repository
git clone https://github.com/geoder101/MoqProxy.git
cd MoqProxy
# Restore dependencies
dotnet restore src/MoqProxy.sln
# Build the solution
dotnet build src/MoqProxy.sln
# Run tests
dotnet test src/MoqProxy.sln
# Create NuGet packages (optional)
dotnet pack --output out/nupkgs src/MoqProxy.sln
cd src/Demo
dotnet run
The demo application showcases the core functionality of MoqProxy including property synchronization, method forwarding, generic methods, and async operations.
The project includes comprehensive unit tests covering:
Run all tests:
dotnet test src/MoqProxy.sln
This project uses Nerdbank.GitVersioning for automatic semantic versioning based on git history. Version numbers are automatically generated during build.
Contributions are welcome! Please feel free to submit issues or pull requests.
This project is licensed under the MIT License - see the LICENSE.txt file for details.
This repository is part of an ongoing exploration into human-AI co-creation.
The code, comments, and structure emerged through dialogue between human intent and LLM reasoning — reviewed, refined,
and grounded in human understanding.