Make legacy C# code testable before you refactor it. InjectorPP.Net is a runtime method replacement library for C# that helps you put tests around code that is not unit testable yet — without changing production code first.
In many legacy C# systems, the code you need to change is already in production, but it depends on static calls, framework APIs, OS integrations, or tightly coupled collaborators. A longer-term cleanup often looks like this:
// Step 1: You have simple, working production code
public class OrderService
{
public bool ProcessOrder(Order order)
{
bool isValid = CertValidator.VerifyCertInMachine();
if (!isValid) return false;
PaymentGateway.Charge(order.Amount);
return true;
}
}// Step 2: A later refactoring might introduce interfaces, constructors, and wiring
public interface ICertValidator { bool VerifyCertInMachine(); }
public interface IPaymentGateway { void Charge(decimal amount); }
public class OrderService
{
private readonly ICertValidator _certValidator;
private readonly IPaymentGateway _paymentGateway;
public OrderService(ICertValidator certValidator, IPaymentGateway paymentGateway)
{
_certValidator = certValidator;
_paymentGateway = paymentGateway;
}
public bool ProcessOrder(Order order)
{
bool isValid = _certValidator.VerifyCertInMachine();
if (!isValid) return false;
_paymentGateway.Charge(order.Amount);
return true;
}
}That refactoring may still be worth doing. But across hundreds of classes, it can mean:
- New seams to introduce everywhere — interfaces, wrappers, adapters, and registrations
- Constructor and container churn — more dependencies, more wiring, more files changing at once
- A large refactor before you have a safety net — structure and behavior change together
- More risk in legacy areas — harder to tell whether a failure comes from the cleanup or the original code
On legacy systems, teams often need tests first. Once current behavior is covered, you can refactor toward cleaner dependency injection and interface-based design with confidence.
InjectorPP.Net is designed for that first step. It replaces method behavior at runtime so you can get tests around hard-to-isolate code while your production code stays exactly as it is today:
// Test code: just fake the dependencies and test ProcessOrder directly
[Fact]
public void ProcessOrder_WhenCertIsValid_ShouldSucceed()
{
using var injector = new Injector();
injector.WhenCalled(typeof(CertValidator).GetMethod(nameof(CertValidator.VerifyCertInMachine))!)
.WillReturn(true);
injector.WhenCalled(typeof(PaymentGateway).GetMethod(nameof(PaymentGateway.Charge))!)
.WillDoNothing();
var service = new OrderService();
bool result = service.ProcessOrder(order);
Assert.True(result); // Success path tested without real certificate or payment
}No production-code changes required to get the first safety net in place.
If your codebase already uses dependency injection and narrow interfaces well, keep doing that. InjectorPP.Net is for the places where legacy code is not unit testable yet, not a replacement for good design.
dotnet add package InjectorPP.Net
Requirements: .NET 8.0+ | Windows or Linux | x64, ARM64, or x86
- Fake Return Values
- Fake with Custom Logic
- Throw Exceptions
- Make Methods Do Nothing
- Mock Private and Protected Methods
- Mock Properties
- Mock Overloaded Methods
- Scoping and Automatic Cleanup
The most common use case — force a method to return whatever you need for your test.
using var injector = new Injector();
// Bool
injector.WhenCalled(typeof(MyClass).GetMethod(nameof(MyClass.IsFeatureEnabled))!)
.WillReturn(true);
// Int
injector.WhenCalled(typeof(MyClass).GetMethod(nameof(MyClass.GetRetryCount))!)
.WillReturn(999);
// String
injector.WhenCalled(typeof(MyClass).GetMethod(nameof(MyClass.GetConnectionString))!)
.WillReturn("Server=test;Database=mock");
// Double
injector.WhenCalled(typeof(MyClass).GetMethod(nameof(MyClass.GetTimeout))!)
.WillReturn(2.718);
// Long
injector.WhenCalled(typeof(MyClass).GetMethod(nameof(MyClass.GetTimestamp))!)
.WillReturn(987654321L);
// Complex objects
var fakeList = new List<int> { 10, 20, 30 };
injector.WhenCalled(typeof(MyClass).GetMethod(nameof(MyClass.GetItems))!)
.WillReturn(fakeList);
// Null
injector.WhenCalled(typeof(MyClass).GetMethod(nameof(MyClass.GetItems))!)
.WillReturn<List<int>?>(null);using var injector = new Injector();
injector.WhenCalled(typeof(MyService).GetMethod(nameof(MyService.IsConnected))!)
.WillReturn(true);
var service = new MyService();
bool connected = service.IsConnected(); // Returns trueusing var injector = new Injector();
injector.WhenCalled(() => MyClass.IsFeatureEnabled())
.WillReturn(true);Replace a method with your own delegate when you need more than a static return value.
using var injector = new Injector();
injector.WhenCalled(typeof(Calculator).GetMethod(nameof(Calculator.Compute))!)
.WillExecute(new Func<int, int>(input => input * 10));
int result = Calculator.Compute(5); // Returns 50 instead of the real computationusing var injector = new Injector();
injector.WhenCalled(typeof(Formatter).GetMethod(nameof(Formatter.Format))!)
.WillExecute(new Func<string, string>(input => "Mock_" + input));
string result = Formatter.Format("Test"); // Returns "Mock_Test"int callCount = 0;
using var injector = new Injector();
injector.WhenCalled(typeof(MyService).GetMethod(nameof(MyService.Process))!)
.WillExecute(new Func<int, int>(input =>
{
callCount++;
return input + 42;
}));
MyService.Process(8); // Returns 50, callCount = 1
MyService.Process(8); // Returns 50, callCount = 2public static class FakeImplementation
{
public static bool AlwaysTrue() => true;
}
using var injector = new Injector();
injector.WhenCalled(typeof(RealService).GetMethod(nameof(RealService.CheckStatus))!)
.WillExecute(typeof(FakeImplementation).GetMethod(nameof(FakeImplementation.AlwaysTrue))!);bool wasCalled = false;
using var injector = new Injector();
injector.WhenCalled(typeof(Logger).GetMethod(nameof(Logger.Log))!)
.WillExecute(new Func<bool>(() =>
{
wasCalled = true;
return true;
}));
// Run your code...
Assert.True(wasCalled);Test error handling paths by making methods throw.
using var injector = new Injector();
injector.WhenCalled(typeof(Database).GetMethod(nameof(Database.Connect))!)
.WillThrow<InvalidOperationException>();
Assert.Throws<InvalidOperationException>(() => Database.Connect());var exception = new ArgumentException("Connection string is invalid");
using var injector = new Injector();
injector.WhenCalled(typeof(Database).GetMethod(nameof(Database.Connect))!)
.WillThrow(exception);
var ex = Assert.Throws<ArgumentException>(() => Database.Connect());
Assert.Equal("Connection string is invalid", ex.Message);Neutralize side effects — the method returns default for its type (false, 0, null, etc.).
using var injector = new Injector();
// Int method returns 0
injector.WhenCalled(typeof(MyClass).GetMethod(nameof(MyClass.GetCount))!)
.WillDoNothing();
Assert.Equal(0, MyClass.GetCount());
// Bool method returns false
injector.WhenCalled(typeof(MyClass).GetMethod(nameof(MyClass.IsReady))!)
.WillDoNothing();
Assert.False(MyClass.IsReady());
// Void method with ref param — param is not modified
string value = "Original";
injector.WhenCalled(typeof(MyClass).GetMethod(nameof(MyClass.Modify))!)
.WillDoNothing();
MyClass.Modify(ref value);
Assert.Equal("Original", value);No need to make methods internal or use [InternalsVisibleTo]. Mock them directly.
using var injector = new Injector();
injector.WhenCalled<MyClass>("PrivateValidate")
.WillReturn(true);
var obj = new MyClass();
// When MyClass internally calls PrivateValidate(), it now returns trueusing var injector = new Injector();
injector.WhenCalled<MyClass>("PrivateStaticHelper")
.WillReturn(true);using var injector = new Injector();
injector.WhenCalled<MyClass>("ProtectedOnInitialize")
.WillReturn(true);using var injector = new Injector();
injector.WhenCalled(typeof(MyClass), "PrivateValidate")
.WillReturn(true);using var injector = new Injector();
injector.WhenCalled<MyClass>("PrivateValidate")
.WillThrow<InvalidOperationException>();Mock property getters and setters via their underlying methods.
using var injector = new Injector();
var getter = typeof(MyClass).GetProperty(nameof(MyClass.Name))!.GetGetMethod()!;
injector.WhenCalled(getter).WillReturn("MockedName");
var obj = new MyClass();
Assert.Equal("MockedName", obj.Name);using var injector = new Injector();
var getter = typeof(AppConfig).GetProperty(nameof(AppConfig.MaxRetries))!.GetGetMethod()!;
injector.WhenCalled(getter).WillReturn(100);
Assert.Equal(100, AppConfig.MaxRetries);Target specific overloads by specifying parameter types.
using var injector = new Injector();
// Mock the overload with one bool parameter
injector.WhenCalled(
typeof(MyClass).GetMethod(nameof(MyClass.Process), new[] { typeof(bool) })!)
.WillReturn(true);
// Mock the overload with two bool parameters
injector.WhenCalled(
typeof(MyClass).GetMethod(nameof(MyClass.Process), new[] { typeof(bool), typeof(bool) })!)
.WillReturn(true);InjectorPP.Net implements IDisposable. When the injector is disposed, all original method behaviors are automatically restored. No test pollution.
// Original behavior
Assert.Equal(42, Calculator.GetAnswer());
using (var injector = new Injector())
{
injector.WhenCalled(typeof(Calculator).GetMethod(nameof(Calculator.GetAnswer))!)
.WillReturn(999);
Assert.Equal(999, Calculator.GetAnswer()); // Mocked
}
Assert.Equal(42, Calculator.GetAnswer()); // Restored automaticallyusing (var injector = new Injector())
{
injector.WhenCalled(typeof(MyClass).GetMethod(nameof(MyClass.GetA))!).WillReturn(true);
injector.WhenCalled(typeof(MyClass).GetMethod(nameof(MyClass.GetB))!).WillReturn(999);
injector.WhenCalled(typeof(MyClass).GetMethod(nameof(MyClass.GetC))!).WillReturn("Fake");
// All three methods are mocked inside this scope
}
// All three methods are back to their original behaviorvar injector = new Injector();
injector.WhenCalled(...).WillReturn(true);
injector.Dispose();
injector.Dispose(); // Safe — no exceptionvar injector = new Injector();
injector.Dispose();
// Attempting to register new replacements after dispose throws
Assert.Throws<ObjectDisposedException>(() =>
{
injector.WhenCalled(...).WillReturn(true);
});Here's the kind of legacy code InjectorPP.Net was built for. Your production code calls an OS-level API that is hard to isolate in its current form:
public class OrderService
{
public bool ProcessOrder(Order order)
{
// Calls a static method that talks to the OS certificate store — hard to isolate in this design
bool isValid = CertValidator.VerifyCertInMachine();
if (!isValid) return false;
PaymentGateway.Charge(order.Amount);
return true;
}
}With InjectorPP.Net, you can test both paths by faking the dependencies first — without touching the production code:
[Fact]
public void ProcessOrder_WhenCertIsValid_ShouldSucceed()
{
using var injector = new Injector();
injector.WhenCalled(typeof(CertValidator).GetMethod(nameof(CertValidator.VerifyCertInMachine))!)
.WillReturn(true);
injector.WhenCalled(typeof(PaymentGateway).GetMethod(nameof(PaymentGateway.Charge))!)
.WillDoNothing();
var service = new OrderService();
Assert.True(service.ProcessOrder(order));
}
[Fact]
public void ProcessOrder_WhenCertIsInvalid_ShouldFail()
{
using var injector = new Injector();
injector.WhenCalled(typeof(CertValidator).GetMethod(nameof(CertValidator.VerifyCertInMachine))!)
.WillReturn(false);
var service = new OrderService();
Assert.False(service.ProcessOrder(order)); // PaymentGateway.Charge is never reached
}Get tests in place first, then refactor toward cleaner seams later with confidence.
| Method | Description |
|---|---|
WhenCalled(MethodInfo) |
Target a method by its MethodInfo |
WhenCalled(() => Method()) |
Target a static method via lambda expression |
WhenCalled<T>(string) |
Target a private/protected method by name |
WhenCalled(Type, string) |
Target a private/protected method by type and name |
Dispose() |
Restore all original method behaviors |
| Method | Description |
|---|---|
WillReturn<T>(T value) |
Make the method return a specific value |
WillThrow<TException>() |
Make the method throw an exception |
WillThrow(Exception) |
Make the method throw a specific exception instance |
WillExecute(Delegate) |
Replace the method with a custom delegate |
WillExecute(MethodInfo) |
Replace the method with another method |
WillDoNothing() |
Make the method return default for its type |
InjectorPP.Net uses thread-local dispatch so that each test thread gets its own method replacement. This means:
- Parallel tests just work. No
[Collection("Sequential")], no[assembly: NonParallelizable], no special configuration needed. - Each thread's fake is fully isolated — if Thread A fakes
CertValidator.VerifyCertInMachine()to returntrueand Thread B fakes it to returnfalse, each thread sees its own value. - Threads without an active fake see the original method behavior.
- Disposing an
Injectoronly removes the current thread's replacements, leaving other threads unaffected.
| Platform | Architecture | Status |
|---|---|---|
| Windows | x64 | ✅ |
| Windows | x86 | ✅ |
| Windows | ARM64 | ✅ |
| Linux | x64 | ✅ |
| Linux | ARM64 | ✅ |
This project welcomes contributions and suggestions. Please see the CONTRIBUTING.md
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft's Trademark & Brand Guidelines. Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies.