8

I'm trying to write an ASP.NET Core 2.2 integration test, where the test setup decorates a specific service that would normally be available to the API as a dependency. The decorator would give me some additional powers I'd need in my integration tests to intercept calls to the underlying service, but I can't seem to properly decorate a normal service in ConfigureTestServices, as my current setup will give me:

An exception of type 'System.InvalidOperationException' occurred in Microsoft.Extensions.DependencyInjection.Abstractions.dll but was not handled in user code

No service for type 'Foo.Web.BarService' has been registered.

To reproduce this, I've just used VS2019 to create a fresh ASP.NET Core 2.2 API Foo.Web project...

// In `Startup.cs`:
services.AddScoped<IBarService, BarService>();
public interface IBarService
{
    string GetValue();
}
public class BarService : IBarService
{
    public string GetValue() => "Service Value";
}
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IBarService barService;

    public ValuesController(IBarService barService)
    {
        this.barService = barService;
    }

    [HttpGet]
    public ActionResult<string> Get()
    {
        return barService.GetValue();
    }
}

...and a companion xUnit Foo.Web.Tests project I utilize a WebApplicationfactory<TStartup>...

public class DecoratedBarService : IBarService
{
    private readonly IBarService innerService;

    public DecoratedBarService(IBarService innerService)
    {
        this.innerService = innerService;
    }

    public string GetValue() => $"{innerService.GetValue()} (decorated)";
}
public class IntegrationTestsFixture : WebApplicationFactory<Startup>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        base.ConfigureWebHost(builder);

        builder.ConfigureTestServices(servicesConfiguration =>
        {
            servicesConfiguration.AddScoped<IBarService>(di
                => new DecoratedBarService(di.GetRequiredService<BarService>()));
        });
    }
}
public class ValuesControllerTests : IClassFixture<IntegrationTestsFixture>
{
    private readonly IntegrationTestsFixture fixture;

    public ValuesControllerTests(IntegrationTestsFixture fixture)
    {
        this.fixture = fixture;
    }

    [Fact]
    public async Task Integration_test_uses_decorator()
    {
        var client = fixture.CreateClient();
        var result = await client.GetAsync("/api/values");
        var data = await result.Content.ReadAsStringAsync();
        result.EnsureSuccessStatusCode();
        Assert.Equal("Service Value (decorated)", data);
    }
}

The behavior kind of makes sense, or at least I think it does: I suppose that the little factory lambda function (di => new DecoratedBarService(...)) in ConfigureTestServices cannot retrieve the concrete BarService from the di container because it's in the main service collection, not in the test services.

How can I make the default ASP.NET Core DI container provide decorator instances that have the original concrete type as their inner service?

Attempted solution 2:

I've tried the following:

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
    base.ConfigureWebHost(builder);

    builder.ConfigureTestServices(servicesConfiguration =>
    {
        servicesConfiguration.AddScoped<IBarService>(di
            => new DecoratedBarService(Server.Host.Services.GetRequiredService<BarService>()));
    });            
}

But this surprisingly runs into the same problem.

Attempted solution 3:

Asking for IBarService instead, like this:

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
    base.ConfigureWebHost(builder);

    builder.ConfigureTestServices(servicesConfiguration =>
    {
        servicesConfiguration.AddScoped<IBarService>(di
            => new DecoratedBarService(Server.Host.Services.GetRequiredService<IBarService>()));
    });            
}

Gives me a different error:

System.InvalidOperationException: 'Cannot resolve scoped service 'Foo.Web.IBarService' from root provider.'

Workaround A:

I can work around the issue in my small repro like this:

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
    base.ConfigureWebHost(builder);

    builder.ConfigureTestServices(servicesConfiguration =>
    {
        servicesConfiguration.AddScoped<IBarService>(di
            => new DecoratedBarService(new BarService()));
    });            
}

But this hurts a lot in my actual application, because BarService doesn't have a simple parameterless constructor: it has a moderately complex dependency graph, so I really would like to resolve instances from the Startup's DI container.


PS. I've tried to make this question fully self-contained, but there's also a clone-and-run rep(r)o for your convenience.

Jeroen
  • 60,696
  • 40
  • 206
  • 339

5 Answers5

4

Contrary to popular belief, the decorator pattern is fairly easy to implement using the built-in container.

What we generally want is to overwrite the registration of the regular implementation by the decorated one, making use of the original one as a parameter to the decorator. As a result, asking for an IDependency should lead to a DecoratorImplementation wrapping the OriginalImplementation.

(If we merely want to register the decorator as a different TService than the original, things are even easier.)

public void ConfigureServices(IServiceCollection services)
{
    // First add the regular implementation
    services.AddSingleton<IDependency, OriginalImplementation>();

    // Wouldn't it be nice if we could do this...
    services.AddDecorator<IDependency>(
        (serviceProvider, decorated) => new DecoratorImplementation(decorated));
            
    // ...or even this?
    services.AddDecorator<IDependency, DecoratorImplementation>();
}

The above code works once we add the following extension methods:

public static class DecoratorRegistrationExtensions
{
    /// <summary>
    /// Registers a <typeparamref name="TService"/> decorator on top of the previous registration of that type.
    /// </summary>
    /// <param name="decoratorFactory">Constructs a new instance based on the the instance to decorate and the <see cref="IServiceProvider"/>.</param>
    /// <param name="lifetime">If no lifetime is provided, the lifetime of the previous registration is used.</param>
    public static IServiceCollection AddDecorator<TService>(
        this IServiceCollection services,
        Func<IServiceProvider, TService, TService> decoratorFactory,
        ServiceLifetime? lifetime = null)
        where TService : class
    {
        // By convention, the last registration wins
        var previousRegistration = services.LastOrDefault(
            descriptor => descriptor.ServiceType == typeof(TService));

        if (previousRegistration is null)
            throw new InvalidOperationException($"Tried to register a decorator for type {typeof(TService).Name} when no such type was registered.");

        // Get a factory to produce the original implementation
        var decoratedServiceFactory = previousRegistration.ImplementationFactory;
        if (decoratedServiceFactory is null && previousRegistration.ImplementationInstance != null)
            decoratedServiceFactory = _ => previousRegistration.ImplementationInstance;
        if (decoratedServiceFactory is null && previousRegistration.ImplementationType != null)
            decoratedServiceFactory = serviceProvider => ActivatorUtilities.CreateInstance(
                serviceProvider, previousRegistration.ImplementationType, Array.Empty<object>());

        if (decoratedServiceFactory is null) // Should be impossible
            throw new Exception($"Tried to register a decorator for type {typeof(TService).Name}, but the registration being wrapped specified no implementation at all.");

        var registration = new ServiceDescriptor(
            typeof(TService), CreateDecorator, lifetime ?? previousRegistration.Lifetime);

        services.Add(registration);

        return services;

        // Local function that creates the decorator instance
        TService CreateDecorator(IServiceProvider serviceProvider)
        {
            var decoratedInstance = (TService)decoratedServiceFactory(serviceProvider);
            var decorator = decoratorFactory(serviceProvider, decoratedInstance);
            return decorator;
        }
    }

    /// <summary>
    /// Registers a <typeparamref name="TService"/> decorator on top of the previous registration of that type.
    /// </summary>
    /// <param name="lifetime">If no lifetime is provided, the lifetime of the previous registration is used.</param>
    public static IServiceCollection AddDecorator<TService, TImplementation>(
        this IServiceCollection services,
        ServiceLifetime? lifetime = null)
        where TService : class
        where TImplementation : TService
    {
        return AddDecorator<TService>(
            services,
            (serviceProvider, decoratedInstance) =>
                ActivatorUtilities.CreateInstance<TImplementation>(serviceProvider, decoratedInstance),
            lifetime);
    }
}
Timo
  • 7,992
  • 4
  • 49
  • 67
  • Your 2nd method doesn't work `serviceProvider` and `decorated` are undefined. But the first one works well. Thank you. For some reasons Scrutor's AddDecorator didn't work but this one does. – LukeP Dec 14 '20 at 23:09
  • @LukeP Thanks for pointing out the mistake in the bottom method. I have corrected the erroneous line. The parameterless overload should now work: `services.AddDecorator();` – Timo Dec 17 '20 at 13:59
3

This seems like a limitation of the servicesConfiguration.AddXxx method which will first remove the type from the IServiceProvider passed to the lambda.

You can verify this by changing servicesConfiguration.AddScoped<IBarService>(...) to servicesConfiguration.TryAddScoped<IBarService>(...) and you'll see that the original BarService.GetValue is getting called during the test.

Additionally, you can verify this because you can resolve any other service inside the lambda except the one you're about to create/override. This is probably to avoid weird recursive resolve loops which would lead to a stack-overflow.

huysentruitw
  • 27,376
  • 9
  • 90
  • 133
  • 1
    Thanks, with your help as well as @ChrisPratt's answer, I found that this indeed is a limitation of the default DI container. Knowing that helped me search around some more, leading to [the possible use of the "Scrutor" NuGet package](https://github.com/jeroenheijmans/sample-asp-net-core-integration-tests-di/compare/fix-with-nuget-package-scrutor) which adds this feature to the DI container. – Jeroen Apr 09 '19 at 20:48
3

There's actually a few things here. First, when you register a service with an interface, you can only inject that interface. You are in fact saying: "when you see IBarService inject an instance of BarService". The service collection doesn't know anything about BarService itself, so you cannot inject BarService directly.

Which leads to the second issue. When you add your new DecoratedBarService registration, you now have two registered implementations for IBarService. There's no way for it to know which to actually inject in place of IBarService, so again: failure. Some DI containers have specialized functionality for this type of scenario, allowing you to specify when to inject which, Microsoft.Extensions.DependencyInjection does not. If you truly need this functionality, you can use a more advanced DI container instead, but considering this is only for testing, that would like be a mistake.

Third, you have a bit of a circular dependency here, as DecoratedBarService itself takes a dependency on IBarService. Again, a more advanced DI container can handle this sort of thing; Microsoft.Extensions.DependencyInjection cannot.

Your best bet here is to use an inherited TestStartup class and factor out this dependency registration into a protected virtual method you can override. In your Startup class:

protected virtual void AddBarService(IServiceCollection services)
{
    services.AddScoped<IBarService, BarService>();
}

Then, where you were doing the registration, call this method instead:

AddBarService(services);

Next, in your test project create a TestStartup and inherit from your SUT project's Startup. Override this method there:

public class TestStartup : Startup
{
    protected override void AddBarService(IServiceCollection services)
    {
        services.AddScoped(_ => new DecoratedBarService(new BarService()));
    }
}

If you need to get dependencies in order to new up any of these classes, then you can use the passed in IServiceProvider instance:

services.AddScoped(p =>
{
    var dep = p.GetRequiredService<Dependency>();
    return new DecoratedBarService(new BarService(dep));
}

Finally, tell your WebApplicationFactory to use this TestStartup class. This will need to be done via the UseStartup method of the builder, not the generic type param of WebApplicationFactory. That generic type param corresponds to the entry point of the application (i.e. your SUT), not which startup class is actually used.

builder.UseStartup<TestStartup>();
Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • I'm guessing that `BarService` has other dependencies, otherwise `new BarService()` would indeed be the way to go. – huysentruitw Apr 09 '19 at 20:13
  • @huysentruitw Ah yes, I added such a note to my question while this answer was being written. – Jeroen Apr 09 '19 at 20:14
  • Either way, very helpful answer! I did indeed assume (based on working with other DI frameworks) that it would be possible, forgot to consider that the built-in DI container might just not be up to this task. – Jeroen Apr 09 '19 at 20:16
  • 1
    That `p` lambda param is actually an instance of `IServiceProvider`, so any other services you need to create these can be retrieved from that. – Chris Pratt Apr 09 '19 at 20:26
  • I updated my answer to give an example of retrieving a service. – Chris Pratt Apr 09 '19 at 20:52
  • Thank you once more, your answer was crucial for understanding the problem, and the offered solution is helpful too. It also helped me find/uncover [another solution](https://stackoverflow.com/a/55601564/419956) which I'll likely end up using, as it requires only changes to the test project by adding some "poor-man's" decorator support to the DI container. – Jeroen Apr 09 '19 at 21:27
3

All the other answers were very helpful:

  • @ChrisPratt clearly explains the underlying problem, and offers a solution where Startup makes the service registration virtual and then overrides that in a TestStartup that is forced upon the IWebHostBuilder
  • @huysentruitw answers as well that this is a limitation of the underlying default DI container
  • @KirkLarkin offers a pragmatic solution where you register BarService itself in Startup and then use that to overwrite the IBarService registration completely

And still, I'd like to offer yet another answer.

The other answers helped me find the right terms to Google for. Turns out, there is the "Scrutor" NuGet package which adds the needed decorator support to the default DI container. You can test this solution yourself as it simply requires:

builder.ConfigureTestServices(servicesConfiguration =>
{
    // Requires "Scrutor" from NuGet:
    servicesConfiguration.Decorate<IBarService, DecoratedBarService>();
});

Mentioned package is open source (MIT), and you can also just adapt only the needed features yourself, thus answering the original question as it stood, without external dependencies or changes to anything except the test project:

public class IntegrationTestsFixture : WebApplicationFactory<Startup>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        base.ConfigureWebHost(builder);

        builder.ConfigureTestServices(servicesConfiguration =>
        {
            // The chosen solution here is adapted from the "Scrutor" NuGet package, which
            // is MIT licensed, and can be found at: https://github.com/khellang/Scrutor
            // This solution might need further adaptation for things like open generics...

            var descriptor = servicesConfiguration.Single(s => s.ServiceType == typeof(IBarService));

            servicesConfiguration.AddScoped<IBarService>(di 
                => new DecoratedBarService(GetInstance<IBarService>(di, descriptor)));
        });
    }

    // Method loosely based on Scrutor, MIT licensed: https://github.com/khellang/Scrutor/blob/68787e28376c640589100f974a5b759444d955b3/src/Scrutor/ServiceCollectionExtensions.Decoration.cs#L319
    private static T GetInstance<T>(IServiceProvider provider, ServiceDescriptor descriptor)
    {
        if (descriptor.ImplementationInstance != null)
        {
            return (T)descriptor.ImplementationInstance;
        }

        if (descriptor.ImplementationType != null)
        {
            return (T)ActivatorUtilities.CreateInstance(provider, descriptor.ImplementationType);
        }

        if (descriptor.ImplementationFactory != null)
        {
            return (T)descriptor.ImplementationFactory(provider);
        }

        throw new InvalidOperationException($"Could not create instance for {descriptor.ServiceType}");
    }
}
Jeroen
  • 60,696
  • 40
  • 206
  • 339
2

There's a simple alternative to this that just requires registering BarService with the DI container and then resolving that when performing the decoration. All it takes is updating ConfigureTestServices to first register BarService and then use the instance of IServiceProvider that's passed into ConfigureTestServices to resolve it. Here's the complete example:

builder.ConfigureTestServices(servicesConfiguration =>
{
    servicesConfiguration.AddScoped<BarService>();

    servicesConfiguration.AddScoped<IBarService>(di =>
        new DecoratedBarService(di.GetRequiredService<BarService>()));
});

Note that this doesn't require any changes to the SUT project. The call to AddScoped<IBarService> here effectively overrides the one provided in the Startup class.

Kirk Larkin
  • 84,915
  • 16
  • 214
  • 203
  • This works when the decorator has no other dependencies. This gets really tiresome and error prone as the decorators become more complex (for example, a caching decorator) – Luís Guilherme Apr 04 '23 at 14:53