如何验证ASP.NET Core中的DI容器?

36
在我的Startup类中,我使用ConfigureServices(IServiceCollection services)方法来设置服务容器,使用来自Microsoft.Extensions.DependencyInjection的内置DI容器。
我想在单元测试中验证依赖关系图,以检查是否可以构建所有服务,这样我就可以在单元测试期间修复任何缺失的服务,而不是在运行时崩溃。在之前的项目中,我使用过Simple Injector,它有一个用于容器的.Verify()方法。但我没有找到ASP.NET Core中类似的东西。
是否有任何内置的(或至少是推荐的)方法来验证整个依赖关系图是否可以构建?
(我能想到的最愚蠢的方法是像这样,但它仍然会因为框架本身注入的开放泛型而失败):
startup.ConfigureServices(serviceCollection);
var provider = serviceCollection.BuildServiceProvider();
foreach (var serviceDescriptor in serviceCollection)
{
    provider.GetService(serviceDescriptor.ServiceType);
}

4
我认为答案是,如果你需要这样的功能,可以使用拥有此功能的第三方 DI 容器。或者,您可以查看拥有此功能的 DI 容器的源代码,并构建自己的扩展程序来提供它,以便用于 Microsoft.DependencyInjection - NightOwl888
4
在以前的项目中,我使用过简单注入器(Simple Injector)。为什么不再使用简单注入器呢?它似乎能够优雅地解决你的问题。 - Steven
1
@Steven 确实!由于这是一个新项目,我尽可能地使用内置功能,但如果发现在框架中没有好的方法来进行验证,那么使用Simple Injector可能是解决方案。 - Martin Wedvich
你找到了一个好的解决方案吗?我刚刚发布了同样的问题。 - mcintyre321
1
@mcintyre321 终于在 aspnetcore 3 中搞定了 https://dev59.com/rVUM5IYBdhLWcg3wOeJY#60374778 - Ilya Chumakov
5个回答

28

在ASP.NET Core 3中添加了内置DI容器验证,并默认仅在Development环境下启用。如果有内容缺失,容器将在启动时抛出致命异常。

请注意,默认情况下控制器不会在DI容器中创建,因此在控制器在DI中注册之前,典型的Web应用程序不会从此检查中获得太多好处:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
        .AddControllersAsServices();
}

要禁用/自定义验证,请添加IHostBuilder.UseDefaultServiceProvider调用:

public class Program
{
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            //...
            .UseDefaultServiceProvider((context, options) =>
            {
                options.ValidateOnBuild = false;
            });

这个验证功能有几个限制,详细信息请阅读:https://andrewlock.net/new-in-asp-net-core-3-service-provider-validation/


关键在于 AddControllersAsServices。如果没有它,控制器将不会被注册,因此也无法进行验证。 - abatishchev

2
这是一个你可以添加到你的项目中的单元测试:
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using [X/N]Unit;

namespace MyProject.UnitTests
{
    public class DITests
    {
        [Fact or Test]
        public void AllServicesShouldConstructSuccessfully()
        {
            Host.CreateDefaultBuilder()
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder
                        .UseDefaultServiceProvider((context, options) =>
                        {
                            options.ValidateOnBuild = true;
                        })
                        .UseStartup<Startup>();
                }).Build();
        })
    }
}

2

实际上,我只是在您的问题示例基础上进行了一些修改,并且对我来说效果很好。通过在我的命名空间中过滤类,背后的理论是它们最终会请求我关心的所有其他内容。

我的测试看起来很像这样:

Original Answer翻译成"最初的回答"

[Test or Fact or Whatever]
public void AllDependenciesPresentAndAccountedFor()
{
    // Arrange
    var startup = new Startup();

    // Act
    startup.ConfigureServices(serviceCollection);

    // Assert
    var exceptions = new List<InvalidOperationException>();
    var provider = serviceCollection.BuildServiceProvider();
    foreach (var serviceDescriptor in services)
    {
        var serviceType = serviceDescriptor.ServiceType;
        if (serviceType.Namespace.StartsWith("my.namespace.here"))
        {
            try
            {
                provider.GetService(serviceType);
            }
            catch (InvalidOperationException e)
            {
                exceptions.Add(e);
            }
        }
    }

    if (exceptions.Any())
    {
        throw new AggregateException("Some services are missing", exceptions);
    }
}

2
请问您能否更新您的回答并添加您迭代的serviceCollectionservices来自哪里? - OlegI
如果任何服务实现了IDisposable,处理提供程序是否是一个好主意呢? - slimbofat
我还会创建一个服务范围,用于创建作用域服务。 - Jeremy Lakeman

0

我在我的一个项目中遇到了同样的问题。我的解决方法:

  1. 添加像AddScopedService、AddTransientService和AddSingletonService这样的方法,将服务添加到DI中,然后将其添加到某个列表中。使用这些方法而不是AddScoped、AddSingleton和AddTransient。

  2. 在启动应用程序时,我首先迭代此列表并调用GetRequiredService。如果无法解析任何服务,则应用程序将无法启动。

  3. 我有CI:自动构建和部署提交到开发分支。因此,如果有人合并破坏DI的更改,应用程序将失败,我们都会知道。

  4. 如果您想更快地完成它,可以将Dmitry Pavlov的答案中的TestServer与我的解决方案一起使用。


0
Microsoft.Extensions.DependencyInjection容器没有内置的.Verify()方法。 但是,您可以创建一个自定义方法来执行相同的功能。 您在代码片段中走在了正确的轨道上,但是您需要进行一些调整来处理开放泛型,正如您所提到的那样。
以下是一个示例,展示了如何设置一个实用方法来验证您的依赖关系图:
 //example

public static class ServiceCollectionExtensions
{
    public static void ValidateDependencyGraph(this IServiceCollection services)
    {
        var serviceProvider = services.BuildServiceProvider();
        foreach (var serviceDescriptor in services)
        {
            // Skip open generics
            if (serviceDescriptor.ServiceType.IsGenericTypeDefinition)
            {
                continue;
            }

            // Skip singletons that have already been instantiated
            if (serviceProvider.GetService(serviceDescriptor.ServiceType) is IDisposable disposable)
            {
                disposable.Dispose();
            }
            else
            {
                serviceProvider.GetRequiredService(serviceDescriptor.ServiceType);
            }
        }
    }
}

现在,你可以在单元测试中使用这个扩展方法来验证你的依赖关系图:
public void TestDependencyGraph()
{
var serviceCollection = new ServiceCollection();
var startup = new Startup();
startup.ConfigureServices(serviceCollection);
serviceCollection.ValidateDependencyGraph();
}


这将让你了解你的依赖图是否能够成功构建。然而,请记住,一些服务可能具有在单元测试期间无法验证的运行时依赖关系。因此,在真实环境中测试你的应用程序仍然非常重要。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接