Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to overwrite a scoped service with a decorated implementation?

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.

like image 672
Jeroen Avatar asked Apr 09 '19 18:04

Jeroen


1 Answers

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.

like image 169
huysentruitw Avatar answered Nov 03 '22 00:11

huysentruitw