Search code examples
c#dependency-injectionsimple-injectorspy

Intercept all instances using Simple Injector


I want to do some integration testing using Simple Injector. However, sometimes I need to check whether a specific internal service has been called with the correct arguments. I am already using FakeItEasy in the test project, so my approach was to do this:

container.Options.RegisterResolveInterceptor(
    (context, producer) =>
    {
        // this is the instance as it was provided by the container 
        object instance = producer();
        object spy = FakeItEasy.Sdk.Create.Fake(context.Producer.ServiceType, options => options.Wrapping(instance));
        Register(context.Producer.ServiceType, spy);
        return spy;
    });

While Register(...) keeps track of spies so that I can look the spy up after the invocation and do the required check later.

But I learnt, this only intercepts the directly resolved instances, not the implicitly created dependent instances. So I checked out the Interception Extensions of Simple Injector. But these snippets still rely on RealProxy and I didn't manage to make it .NET Core compliant using DispatchProxy, probably just because of my lack of deep experience with expression tree manipulation.

Then I found this answer and the ApplyInterceptor snippet, but while this approach seems promising, I am struggling to adapt the snippet so that the interceptor not just gets the factory method, but also gets to know the resolved service type:

container.Options.ApplyInterceptor(
    factory =>
    {
        object instance = factory();
        object spy = FakeItEasy.Sdk.Create.Fake(howToGetTheServiceType, options => options.Wrapping(instance));
        Register(howToGetTheServiceType, spy);
        return instance;
    });

Any other suggestions?


Solution

  • The ApplyInterceptor extension method from the documentation makes use of the ExpressionBuilding event. This event is unsuited for interception. Although it allows you to replace the constructed instance with another and even change types, the post condition is that the returned type must be the same implementation type of a sub type of the implementation.

    For instance, when a Register<ILogger, ConsoleLogger>() registration is made, an ExpressionBuilding handler may replace the expression for something that returns ConsoleLogger or ConsoleLoggerSubClass : ConsoleLogger, but never for a FileLogger : ILogger.

    For your quest, you need to use the ExpressionBuilt event instead, as this is allowed to replace ConsoleLogger with a FileLogger.

    The following snippet, hopefully, gets you started:

    container.ExpressionBuilt += (s, e) =>
    {
        // Compile the original object creation into a delegate.
        var factory =
          (Func<object>)Expression.Lambda(typeof(Func<object>), e.Expression).Compile();
    
        // Create a registration for the spy, based on the original lifestyle
        var spyregistration = e.Lifestyle.CreateRegistration(
            e.RegisteredServiceType,
            () =>
            {
                var instance = factory();
                var spy = FakeItEasy.Sdk.Create
                  .Fake(e.RegisteredServiceType, options => options.Wrapping(instance));
                RegisterSpy(e.RegisteredServiceType, spy);
                return spy;
            },
            container);
    
        // Replace expression of the registration with the spy registration.
        e.Expression = spyregistration.BuildExpression();
    };