我正在尝试编写一个ASP.NET Core 2.2集成测试,该测试设置将装饰特定的服务,该服务通常作为依赖项可用于API。装饰器将为我提供一些集成测试所需的其他功能,以拦截对基础服务的调用,但是我似乎无法在ConfigureTestServices
中正确装饰常规服务,因为我当前的设置会给我:
Microsoft.Extensions.DependencyInjection.Abstractions.dll中发生了'System.InvalidOperationException'类型的异常,但未在用户代码中处理
尚未注册类型为“ Foo.Web.BarService”的服务。
为重现这一点,我刚刚使用VS2019创建了一个新的ASP.NET Core 2.2 API Foo.Web
项目...
// 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();
}
}
...以及同一个xUnit Foo.Web.Tests
项目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);
}
}
这种行为是有道理的,或者至少我认为确实如此:我认为di => new DecoratedBarService(...)
中的小工厂lambda函数(ConfigureTestServices
)无法检索具体的BarService
来自di
容器,因为它在主服务集合中,而不在测试服务中。
如何使默认的ASP.NET Core DI容器提供具有原始具体类型作为其内部服务的装饰器实例?
尝试的解决方案2:
我尝试了以下操作:
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.ConfigureTestServices(servicesConfiguration =>
{
servicesConfiguration.AddScoped<IBarService>(di
=> new DecoratedBarService(Server.Host.Services.GetRequiredService<BarService>()));
});
}
但这令人惊讶地遇到了同样的问题。
尝试的解决方案3:
改为要求IBarService
,就像这样:
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.ConfigureTestServices(servicesConfiguration =>
{
servicesConfiguration.AddScoped<IBarService>(di
=> new DecoratedBarService(Server.Host.Services.GetRequiredService<IBarService>()));
});
}
给我一个不同的错误:
System.InvalidOperationException:'无法从根提供者解析作用域服务'Foo.Web.IBarService'。
解决方法A:
我可以这样在小型程序中解决该问题:
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.ConfigureTestServices(servicesConfiguration =>
{
servicesConfiguration.AddScoped<IBarService>(di
=> new DecoratedBarService(new BarService()));
});
}
但这在我的 actual 应用程序中对很多造成了很大的伤害,因为BarService
没有简单的无参数构造函数:它具有中等复杂的依赖关系图,所以我真的很想从Startup
的DI容器中解析实例。
PS。我试图使这个问题完全独立,但也为您提供方便a clone-and-run rep(r)o。
答案 0 :(得分:3)
这似乎是servicesConfiguration.AddXxx
方法的局限性,它将首先从传递给lambda的IServiceProvider
中删除类型。
您可以通过将servicesConfiguration.AddScoped<IBarService>(...)
更改为servicesConfiguration.TryAddScoped<IBarService>(...)
来进行验证,然后您会看到原始BarService.GetValue
在测试过程中被调用。
此外,您可以验证这一点,因为您可以解析lambda中的任何其他服务,但您将要创建/覆盖的服务除外。这可能是为了避免怪异的递归解析循环,该循环会导致堆栈溢出。
答案 1 :(得分:3)
这里实际上有一些东西。首先,在使用接口注册服务时,只能注入该接口。实际上,您是在说:“看到IBarService
时注入BarService
的实例”。服务集合对BarService
本身一无所知,因此您不能直接注入BarService
。
这导致第二个问题。现在,当您添加新的DecoratedBarService
注册时,您将为IBarService
拥有两个注册实现。它没有办法知道实际注入哪个来代替IBarService
,因此再次:失败。某些DI容器具有针对这种情况的特殊功能,允许您指定何时注入哪些Microsoft.Extensions.DependencyInjection
不注入。如果您确实需要此功能,则可以使用更高级的DI容器,但是考虑到这仅用于测试,那会是一个错误。
第三,这里有一些循环依赖项,因为DecoratedBarService
本身就依赖于IBarService
。同样,更高级的DI容器可以处理这种事情。 Microsoft.Extensions.DependencyInjection
不能。
您最好的选择是使用继承的TestStartup
类,并将此依赖项注册分解为可以覆盖的受保护虚拟方法。在您的Startup
类中:
protected virtual void AddBarService(IServiceCollection services)
{
services.AddScoped<IBarService, BarService>();
}
然后,在您进行注册的地方,调用此方法:
AddBarService(services);
接下来,在测试项目中创建一个TestStartup
并从您的SUT项目的Startup
继承。在那里重写此方法:
public class TestStartup : Startup
{
protected override void AddBarService(IServiceCollection services)
{
services.AddScoped(_ => new DecoratedBarService(new BarService()));
}
}
如果您需要获取依赖项以更新这些类中的任何一个,则可以使用传入的IServiceProvider
实例:
services.AddScoped(p =>
{
var dep = p.GetRequiredService<Dependency>();
return new DecoratedBarService(new BarService(dep));
}
最后,告诉您的WebApplicationFactory
使用此TestStartup
类。这需要通过构建器的UseStartup
方法来完成,而不是通过WebApplicationFactory
的通用类型参数来完成。通用类型参数对应于应用程序的入口点(即您的SUT),而不是实际使用的启动类。
builder.UseStartup<TestStartup>();
答案 2 :(得分:3)
所有其他答案都非常有帮助:
Startup
进行服务注册virtual
,然后覆盖在{{ {1}} TestStartup
中自己注册IWebHostBuilder
,然后使用 that 完全覆盖BarService
注册而且,我仍然想提供另一个答案。
其他答案帮助我找到了适合Google的术语。事实证明,有the "Scrutor" NuGet package可以将所需的装饰器支持添加到默认的DI容器中。您可以test this solution yourself,因为它只需要:
Startup
提到的软件包是开源(MIT),您也可以自己仅调整所需的功能,从而回答原始问题,没有外部依赖性或对 test <进行任何更改< / em>项目:
IBarService
答案 3 :(得分:2)
有一个简单的替代方案,只需在DI容器中注册BarService
,然后在执行装饰时解决该问题。只需更新ConfigureTestServices
使其首先注册BarService
,然后使用传递到IServiceProvider
的{{1}}实例来解决它。这是完整的示例:
ConfigureTestServices
请注意,这不需要对SUT项目进行任何更改。这里对builder.ConfigureTestServices(servicesConfiguration =>
{
servicesConfiguration.AddScoped<BarService>();
servicesConfiguration.AddScoped<IBarService>(di =>
new DecoratedBarService(di.GetRequiredService<BarService>()));
});
的调用实际上将覆盖AddScoped<IBarService>
类中提供的调用。
答案 4 :(得分:1)
与流行的看法相反,装饰器模式使用内置容器非常容易实现。
我们通常想要的是覆盖由修饰的对象注册常规实现,将原始的实现作为修饰符的参数。结果,请求IDependency
应该导致DecoratorImplementation
包装OriginalImplementation
。
(如果我们只想将装饰器注册为与原始装饰器不同的 TService
,则甚至是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>();
}
添加以下扩展方法后,以上代码将起作用:
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>());
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);
}
}