gpt4 book ai didi

c# - 如何用修饰的实现覆盖作用域服务?

转载 作者:太空狗 更新时间:2023-10-29 21:07:45 25 4
gpt4 key购买 nike

我正在尝试编写 ASP.NET Core 2.2 集成测试,其中测试设置装饰了通常可作为 API 依赖项使用的特定服务。装饰器会给我一些额外的权力,我需要在我的集成测试中拦截对底层服务的调用,但我似乎无法在 ConfigureTestServices 中正确地装饰普通服务。 ,因为我当前的设置会给我:

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.

为了重现这一点,我刚刚使用 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项目一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: 'Cannot resolve scoped service 'Foo.Web.IBarService' from root provider.'

解决方法 A:

我可以像这样在我的小型复制中解决这个问题:

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

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

但这对我的实际应用程序造成了很多的伤害,因为BarService没有简单的无参数构造函数:它有一个中等复杂的依赖图,所以我真的很想从 Startup 解析实例的 DI 容器。


附言。我试图让这个问题完全独立,但也有 a clone-and-run rep(r)o为了您的方便。

最佳答案

与流行的看法相反,装饰器模式使用内置容器相当容易实现。

我们通常想要的是用装饰器覆盖常规实现的注册,利用原始的作为装饰器的参数。因此,请求 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>());

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);
}
}

关于c# - 如何用修饰的实现覆盖作用域服务?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55599360/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com