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.