geoder101.MoqProxy
1.0.30
dotnet add package geoder101.MoqProxy --version 1.0.30
NuGet\Install-Package geoder101.MoqProxy -Version 1.0.30
<PackageReference Include="geoder101.MoqProxy" Version="1.0.30" />
<PackageVersion Include="geoder101.MoqProxy" Version="1.0.30" />
<PackageReference Include="geoder101.MoqProxy" />
paket add geoder101.MoqProxy --version 1.0.30
#r "nuget: geoder101.MoqProxy, 1.0.30"
#:package geoder101.MoqProxy@1.0.30
#addin nuget:?package=geoder101.MoqProxy&version=1.0.30
#tool nuget:?package=geoder101.MoqProxy&version=1.0.30
MoqProxy
A powerful extension for Moq that enables proxy pattern mocking - forward calls from a mock to a real implementation while maintaining full verification capabilities.
Why MoqProxy?
MoqProxy bridges the gap between full mocking and real implementations, giving you the best of both worlds:
- Verify interactions - Use Moq's
Verify()to assert method calls on the real implementation - Selective overrides - Override specific methods/properties while forwarding everything else
- Integration testing - Test decorators and wrappers with real dependencies
- Spy pattern - Observe and verify behavior without changing it
- Works with interfaces AND classes - Unlike
CallBase, works seamlessly with interface mocks
How is this different from CallBase = 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.
Example Comparison
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)
Installation
dotnet add package geoder101.MoqProxy
Microsoft Dependency Injection Integration
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.
Quick Start
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);
Features
✅ Properties
- Read-only properties
- Write-only properties
- Read-write properties
- Complex type properties (collections, dictionaries, etc.)
- Null value handling
- State synchronization - changes to mock properties are reflected in the implementation and vice versa
✅ Events
- Event subscription (
+=) forwarding to implementation - Event unsubscription (
-=) forwarding to implementation - Standard
EventHandlerandEventHandler<TEventArgs>patterns - Custom delegate types
- Multiple handlers on the same event
- Events raised by implementation invoke handlers subscribed to mock
✅ Methods
- Void methods
- Methods with return values
- Methods with 0-4+ parameters
- Method overloads
- Generic methods - full support including type inference
- Async methods -
TaskandTask<T> - Ref/out parameters - automatic forwarding with verification support
- Various return types (primitives, objects, collections, etc.)
✅ Indexers
- Single-parameter indexers (
this[int index]) - Multi-parameter indexers (
this[int x, int y]) - Read-only indexers
- Write-only indexers (limited support due to Moq constraints)
✅ Advanced Features
- Spy pattern - Intercept and observe method calls with callbacks while forwarding to implementation
- Selective override - Override specific behaviors while keeping others proxied
- Mock reset - Call
mock.Reset()thenSetupAsProxy()again to restore proxying - Multiple instances - Proxy multiple implementations with different mocks
- Upstream access - Retrieve the upstream implementation instance from a mock proxy
✅ Spy Pattern
Spy 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));
✅ Accessing Upstream Implementation
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);
Usage Examples
Basic Proxying
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);
Selective Override
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));
Testing Decorators
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!
Async Methods
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);
Properties with State Synchronization
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);
Indexers
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
Events
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);
Generic Methods
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);
Ref/Out Parameters
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);
Advanced Scenarios
Reset and Reapply
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));
Limitations
- Ref/out parameters: Methods with ref/out parameters are automatically forwarded to the implementation. You can
verify calls using
It.Ref<T>.IsAny, but cannot verify specific out values (Moq limitation). - By-ref structs (e.g.,
Span<T>,ReadOnlySpan<T>): Not supported due to C# expression tree limitations - Indexers with 3+ parameters: Limited support due to implementation complexity
- Write-only indexers: Have limited support due to Moq API constraints
How It Works
MoqProxy uses a sophisticated approach to enable proxy mocking:
- Reflection & Expression Trees: Dynamically inspects the mocked type and creates Moq setups using expression trees for properties, methods, and indexers
- Generic Method Handling: Uses
MethodInfo.Invokefor generic methods that can't be represented in expression trees - Custom Interceptor: Injects a Castle.DynamicProxy interceptor to handle edge cases and ensure all calls are forwarded
- Sentinel Pattern: Uses a special
NullReturnValuesentinel to detect when no explicit setup was matched, triggering fallback to the real implementation
The library handles complex scenarios including:
- Method overloads with different signatures
- Generic methods with type inference
- Multi-parameter indexers
- Async/await patterns
- Property state synchronization
Requirements
- .NET 8.0 or later - The library targets .NET 8.0
- Moq 4.20.72 or later - Core mocking framework
- Castle.Core - Dependency of Moq, used for dynamic proxy generation
Building from Source
Prerequisites
- .NET 8.0 SDK or later
- Git
Clone and Build
# 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
Running the Demo
cd src/Demo
dotnet run
The demo application showcases the core functionality of MoqProxy including property synchronization, method forwarding, generic methods, and async operations.
Testing
The project includes comprehensive unit tests covering:
- Spy pattern - Parameter capture, return value capture, callback invocation with various signatures
- MockProxy accessors - Upstream instance retrieval, Mock<T> retrieval, null handling
- Property proxying - Regular properties, read-only, write-only, state synchronization
- Method proxying - Sync/async methods, various parameter counts, return types
- Event proxying - Event subscription/unsubscription, standard and custom delegates, multiple handlers
- Generic methods - Type inference, multiple type parameters
- Ref/out parameters - Automatic forwarding and verification
- Indexers - Single and multi-parameter indexers
- Edge cases - Overrides, resets, interceptor behavior
Run all tests:
dotnet test src/MoqProxy.sln
Versioning
This project uses Nerdbank.GitVersioning for automatic semantic versioning based on git history. Version numbers are automatically generated during build.
Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
License
This project is licensed under the MIT License - see the LICENSE.txt file for details.
Related Projects
- Moq - The mocking library this extends
- Castle.DynamicProxy - Used by Moq for proxy generation
Co-authored with Artificial Intelligence
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.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Moq (>= 4.20.72)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on geoder101.MoqProxy:
| Package | Downloads |
|---|---|
|
geoder101.MoqProxy.DependencyInjection.Microsoft
Microsoft.Extensions.DependencyInjection integration for MoqProxy. Enables wrapping DI-registered services with Moq proxies for testing - verify calls to real implementations, spy on service interactions, and test integration scenarios without modifying production code. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.30 | 236 | 11/6/2025 |
| 1.0.30-alpha | 223 | 11/6/2025 |
| 1.0.25-alpha | 215 | 11/4/2025 |
| 1.0.22-alpha | 228 | 11/2/2025 |
| 1.0.12-alpha | 202 | 10/21/2025 |
| 1.0.6-alpha | 206 | 10/20/2025 |
| 1.0.1-alpha | 207 | 10/19/2025 |