【问题标题】:How to overwrite a scoped service with a decorated implementation?如何用修饰的实现覆盖范围服务?
【发布时间】:2019-08-31 03:12:24
【问题描述】:

我正在尝试编写一个 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&lt;TStartup&gt;...

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

这种行为是有道理的,或者至少我认为是有道理的:我想ConfigureTestServices 中的小工厂 lambda 函数 (di =&gt; new DecoratedBarService(...)) 无法从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

【问题讨论】:

    标签: c# asp.net-core .net-core integration-testing xunit


    【解决方案1】:

    这似乎是 servicesConfiguration.AddXxx 方法的一个限制,该方法将首先从传递给 lambda 的 IServiceProvider 中删除类型。

    您可以通过将servicesConfiguration.AddScoped&lt;IBarService&gt;(...) 更改为servicesConfiguration.TryAddScoped&lt;IBarService&gt;(...) 来验证这一点,您会看到原来的BarService.GetValue 在测试期间被调用。

    此外,您可以验证这一点,因为您可以解析 lambda 中的任何其他服务,除了您将要创建/覆盖的服务。这可能是为了避免奇怪的递归解析循环,这会导致堆栈溢出。

    【讨论】:

    • 谢谢,在您的帮助以及@ChrisPratt 的回答下,我发现这确实是默认 DI 容器的限制。知道这有助于我搜索更多内容,导致the possible use of the "Scrutor" NuGet package 将此功能添加到 DI 容器中。
    【解决方案2】:

    这里实际上有一些东西。首先,当您使用接口注册服务时,您只能注入该接口。您实际上是在说:“当您看到 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>();
    

    【讨论】:

    • 我猜BarService 有其他依赖关系,否则new BarService() 确实是要走的路。
    • @huysentruitw 啊,是的,我在写这个答案时在我的问题中添加了这样一个注释。
    • 无论如何,非常有帮助的答案!我确实假设(基于与其他 DI 框架的合作)这是可能的,忘记考虑内置 DI 容器可能无法胜任这项任务。
    • p lambda 参数实际上是IServiceProvider 的一个实例,因此您可以从中检索您需要创建这些的任何其他服务。
    • 我更新了我的答案,给出了一个检索服务的例子。
    【解决方案3】:

    所有其他答案都非常有帮助:

    • @ChrisPratt clearly explains 是潜在的问题,并提供了一个解决方案,其中Startup 进行服务注册virtual,然后在TestStartup 中覆盖它,这是强制IWebHostBuilder
    • @huysentruitw answers 以及这是对底层默认 DI 容器的限制
    • @KirkLarkin offers a pragmatic solutionBarService 中注册 Startup 然后使用 that 完全覆盖 IBarService 注册

    不过,我想再提供一个答案。

    其他答案帮助我找到了适合 Google 的条款。事实证明,the "Scrutor" NuGet package 将所需的装饰器支持添加到默认的 DI 容器中。你可以test this solution yourself,因为它只需要:

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

    提到的包是开源的 (MIT),你也可以只调整自己需要的特性,因此 回答原来的问题,除了 测试 项目

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

    【讨论】:

      【解决方案4】:

      对此有一个简单的替代方案,只需将BarService 注册到 DI 容器,然后在执行装饰时解决该问题。只需更新ConfigureTestServices 以首先注册BarService,然后使用传递给ConfigureTestServicesIServiceProvider 实例来解决它。这是完整的示例:

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

      请注意,这不需要对 SUT 项目进行任何更改。此处对AddScoped&lt;IBarService&gt; 的调用有效地覆盖了Startup 类中提供的调用。

      【讨论】:

        【解决方案5】:

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

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

        【讨论】:

        • 您的第二种方法不起作用serviceProviderdecorated 未定义。但是第一个效果很好。谢谢你。由于某些原因,Scutor 的 AddDecorator 不起作用,但这个起作用。
        • @LukeP 感谢您指出底部方法中的错误。我已经纠正了错误的行。无参数重载现在应该可以工作了:services.AddDecorator&lt;IDependency, DecoratorImplementation&gt;();
        猜你喜欢
        • 2015-04-02
        • 1970-01-01
        • 1970-01-01
        • 2016-02-25
        • 2016-06-10
        • 2017-05-29
        • 1970-01-01
        • 2020-12-30
        • 2019-11-11
        相关资源
        最近更新 更多