Skip to content

microsoft/injectorppfordotnet

InjectorPP.Net

CI Release NuGet License: MIT .NET

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.

The Problem

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.

The Solution

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.

Installation

dotnet add package InjectorPP.Net

Requirements: .NET 8.0+ | Windows or Linux | x64, ARM64, or x86

Use Cases


Fake Return Values

The most common use case — force a method to return whatever you need for your test.

Static methods

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);

Instance methods

using var injector = new Injector();
injector.WhenCalled(typeof(MyService).GetMethod(nameof(MyService.IsConnected))!)
        .WillReturn(true);

var service = new MyService();
bool connected = service.IsConnected();  // Returns true

Expression syntax (cleaner for static methods)

using var injector = new Injector();
injector.WhenCalled(() => MyClass.IsFeatureEnabled())
        .WillReturn(true);

Fake with Custom Logic

Replace a method with your own delegate when you need more than a static return value.

Simple delegate

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 computation

String transformation

using 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"

Delegate with closure (captured variables)

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 = 2

Redirect to another method

public 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))!);

Verify a method was called

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);

Throw Exceptions

Test error handling paths by making methods throw.

By exception type

using var injector = new Injector();
injector.WhenCalled(typeof(Database).GetMethod(nameof(Database.Connect))!)
        .WillThrow<InvalidOperationException>();

Assert.Throws<InvalidOperationException>(() => Database.Connect());

With a specific exception instance

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);

Make Methods Do Nothing

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);

Mock Private and Protected Methods

No need to make methods internal or use [InternalsVisibleTo]. Mock them directly.

Private instance method

using var injector = new Injector();
injector.WhenCalled<MyClass>("PrivateValidate")
        .WillReturn(true);

var obj = new MyClass();
// When MyClass internally calls PrivateValidate(), it now returns true

Private static method

using var injector = new Injector();
injector.WhenCalled<MyClass>("PrivateStaticHelper")
        .WillReturn(true);

Protected method

using var injector = new Injector();
injector.WhenCalled<MyClass>("ProtectedOnInitialize")
        .WillReturn(true);

Alternative syntax (using Type)

using var injector = new Injector();
injector.WhenCalled(typeof(MyClass), "PrivateValidate")
        .WillReturn(true);

Throw from private method

using var injector = new Injector();
injector.WhenCalled<MyClass>("PrivateValidate")
        .WillThrow<InvalidOperationException>();

Mock Properties

Mock property getters and setters via their underlying methods.

Instance property getter

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);

Static property getter

using var injector = new Injector();
var getter = typeof(AppConfig).GetProperty(nameof(AppConfig.MaxRetries))!.GetGetMethod()!;
injector.WhenCalled(getter).WillReturn(100);

Assert.Equal(100, AppConfig.MaxRetries);

Mock Overloaded Methods

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);

Scoping and Automatic Cleanup

InjectorPP.Net implements IDisposable. When the injector is disposed, all original method behaviors are automatically restored. No test pollution.

Using statement (recommended)

// 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 automatically

Multiple replacements — all restored

using (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 behavior

Dispose is idempotent

var injector = new Injector();
injector.WhenCalled(...).WillReturn(true);

injector.Dispose();
injector.Dispose();  // Safe — no exception

Post-dispose safety

var injector = new Injector();
injector.Dispose();

// Attempting to register new replacements after dispose throws
Assert.Throws<ObjectDisposedException>(() =>
{
    injector.WhenCalled(...).WillReturn(true);
});

Real-World Example

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.

API Reference

Injector

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

InjectionBuilder

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

Thread Safety

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 return true and Thread B fakes it to return false, each thread sees its own value.
  • Threads without an active fake see the original method behavior.
  • Disposing an Injector only removes the current thread's replacements, leaving other threads unaffected.

Platform Support

Platform Architecture Status
Windows x64
Windows x86
Windows ARM64
Linux x64
Linux ARM64

Contributing

This project welcomes contributions and suggestions. Please see the CONTRIBUTING.md

Trademarks

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.

About

injectorpp for .NET

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages