避免在ASP.NET Core 3.0中为启动类注入服务

本篇是如何升级到ASP.NET Core 3.0系列文章的第二篇。html

在本篇博客中,我将描述从ASP.NET Core 2.x应用升级到.NET Core 3.0须要作的一个修改:你不在须要在Startup构造函数中注入服务了。c#

在ASP.NET Core 3.0中迁移到通用主机

在.NET Core 3.0中, ASP.NET Core 3.0的托管基础已经被从新设计为通用主机,而再也不与之并行使用。那么这对于那些正在使用ASP.NET Core 2.x开发应用的开发人员,这意味着什么呢?在目前这个阶段,我已经迁移了多个应用,到目前为止,一切都进展顺利。官方的迁移指导文档能够很好的指导你完成所需的步骤,所以,我强烈建议你读一下这篇文档。安全

在迁移过程当中,我遇到的最多两个问题是:app

  • ASP.NET Core 3.0中配置中间件的推荐方式是使用端点路由(Endpoint Routing)。
  • 通用主机不容许为Startup类注入服务

其中第一点,我以前已经讲解过了。端点路由(Endpoint Routing)是在ASP.NET Core 2.2中引入的,可是被限制只能在MVC中使用。在ASP.NET Core 3.0中,端点路由已是推荐的终端中间件实现了,由于它提供了不少好处。其中最重要的是,它容许中间件获取哪个端点最终会被执行,而且能够检索有关这个端点的元数据(metadata)。例如,你能够为健康检查端点应用受权。asp.net

端点路由是在配置中间件顺序时须要特别注意。我建议你再升级你的应用前,先阅读一下官方迁移文档针对此处的说明,后续我将写一篇博客来介绍如何将终端中间件转换为端点路由。ide

第二点,是已经提到了的将服务注入Startup类,可是并无获得足够的宣传。我不太肯定是否是由于这样作的人很少,仍是在一些场景下,它很容易解决。在本篇中,我将展现一些问题场景,并提供一些解决方案。函数

ASP.NET Core 2.x启动类中注入服务

在ASP.NET Core 2.x版本中,有一个不为人知的特性,就是你能够在Program.cs文件中配置你的依赖注入容器。之前我曾经使用这种方式来进行强类型选项,而后在配置依赖注入容器的其他剩余部分时使用这些配置。visual-studio

下面咱们来看一下ASP.NET Core 2.x的例子:测试

public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .ConfigureSettings(); // 配置服务,后续将在Startup中使用
}

这里有没有注意到在CreateWebHostBuilder中调用了一个ConfigureSettings()的方法?这是一个我用来配置应用强类型选项的扩展方法。例如,这个扩展方法可能看起来是这样的:ui

public static class SettingsinstallerExtensions
{
    public static IWebHostBuilder ConfigureSettings(this IWebHostBuilder builder)
    {
        return builder.ConfigureServices((context, services) =>
        {
            var config = context.Configuration;

            services.Configure<ConnectionStrings>(config.GetSection("ConnectionStrings"));
            services.AddSingleton<ConnectionStrings>(
                ctx => ctx.GetService<IOptions<ConnectionStrings>>().Value)
        });
    }
}

因此这里,ConfigureSettings()方法调用了IWebHostBuilder实例的ConfigureServices()方法,配置了一些设置。因为这些服务会在Startup初始化以前被配置到依赖注入容器,因此在Startup类的构造函数中,这些以配置的服务是能够被注入的。

public static class Startup
{
    public class Startup
    {
        public Startup(
            IConfiguration configuration, 
            ConnectionStrings ConnectionStrings) // 注入预配置服务
        {
            Configuration = configuration;
            ConnectionStrings = ConnectionStrings;
        }

        public IConfiguration Configuration { get; }
        public ConnectionStrings ConnectionStrings { get; }

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

            // 使用配置中的链接字符串
            services.AddDbContext<BloggingContext>(options =>
                options.UseSqlServer(ConnectionStrings.BloggingDatabase));
        }

        public void Configure(IApplicationBuilder app)
        {

        }
    }
}

我发现,当我先要在ConfigureServices方法中使用强类型选项对象配置其余服务时,这种模式很是的有用。在我上面的例子中,ConnectionStrings对象是一个强类型对象,而且这个对象在程序进入Startup以前,就已经进行非空验证。这并非一种正规的基础技术,可是实时证实使用起来很是的顺手。

PS: 如何为ASP.NET Core的强类型选项对象添加验证

然而,若是切换到ASP.NET Core 3.0通用主机以后,你会发现这种实现方式在运行时会收到如下的错误信息。

Unhandled exception. System.InvalidOperationException: Unable to resolve service for type 'ExampleProject.ConnectionStrings' while attempting to activate 'ExampleProject.Startup'.
   at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.ConstructorMatcher.CreateInstance(IServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters)
   at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.UseStartup(Type startupType, HostBuilderContext context, IServiceCollection services)
   at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.<>c__DisplayClass12_0.<UseStartup>b__0(HostBuilderContext context, IServiceCollection services)
   at Microsoft.Extensions.Hosting.HostBuilder.CreateServiceProvider()
   at Microsoft.Extensions.Hosting.HostBuilder.Build()
   at ExampleProject.Program.Main(String[] args) in C:\repos\ExampleProject\Program.cs:line 21

这种方式在ASP.NET Core 3.0中已经再也不支持了。你能够在Startup类的构造函数注入IHostEnvironmentIConfiguration, 可是仅此而已。至于缘由,应该是以前的实现方式会带来一些问题,下面我将给你们详细描述一下。

注意:若是你坚持在ASP.NET Core 3.0中使用IWebHostBuilder, 而不使用的通用主机的话,你依然可使用以前的实现方式。可是我强烈建议你不要这样作,并尽量的尝试迁移到通用主机的方式。

两个单例?

注入服务到Startup类的根本问题是,它会致使系统须要构建依赖注入容器两次。在我以前展现的例子中,ASP.NET Core知道你须要一个ConnectionStrings对象,可是惟一知道如何构建该对象的方法是基于“部分”配置构建IServiceProvider(在以前的例子中,咱们使用ConfigureSettings()扩展方法提供了这个“部分”配置)。

那么为何这个会是一个问题呢?问题是这个ServiceProvider是一个临时的“根”ServiceProvider.它建立了服务并将服务注入到Startup中。而后,剩余的依赖注入容器配置将做为ConfigureServices方法的一部分运行,而且临时的ServiceProvider在这时就已经被丢弃了。而后一个新的ServiceProvider会被建立出来,在其中包含了应用程序“完整”的配置。

这样,即便服务配置使用Singleton生命周期,也会被建立两次:

  • 当使用“部分”ServiceProvider时,建立了一次,并针对Startup进行了注入
  • 当使用"完整"ServiceProvider时,建立了一次

对于个人用例,强类型选项,这多是可有可无的。系统并非只能够有一个配置实例,这只是一个更好的选择。可是这并不是老是如此。服务的这种“泄露”彷佛是更改通用主机行为的主要缘由 - 它让东西看起来更安全了。

那么若是我须要ConfigureServices内部的服务怎么办?

虽然咱们已经不能像之前那样配置服务了,可是仍是须要一种能够替换的方式来知足一些场景的须要!

其中最多见的场景是经过注入服务到Startup,针对Startup.ConfigureServices方法中注册的其余服务进行状态控制。例如,如下是一个很是基本的例子。

public class Startup
{
    public Startup(IdentitySettings identitySettings)
    {
        IdentitySettings = identitySettings;
    }

    public IdentitySettings IdentitySettings { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        if(IdentitySettings.UseFakeIdentity)
        {
            services.AddScoped<IIdentityService, FakeIdentityService>();
        }
        else
        {
            services.AddScoped<IIdentityService, RealIdentityService>();
        }
    }

    public void Configure(IApplicationBuilder app)
    {
        // ...
    }
}

这个例子中,代码经过检查注入的IdentitySettings对象中的布尔值属性,决定了IIdentityService接口使用哪一个实现来注册:或者使用假服务,或者使用真服务。

经过将静态服务注册转换为工厂函数的方式,可使须要注入IdentitySetting对象的实现方式与通用主机兼容。例如:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // 为依赖注入容器,配置IdentitySetting
        services.Configure<IdentitySettings>(Configuration.GetSection("Identity")); 

        // 注册不一样的实现
        services.AddScoped<FakeIdentityService>();
        services.AddScoped<RealIdentityService>();

        // 根据IdentitySetting配置,在运行时返回一个正确的实现
        services.AddScoped<IIdentityService>(ctx => 
        {
            var identitySettings = ctx.GetRequiredService<IdentitySettings>();
            return identitySettings.UseFakeIdentity
                ? ctx.GetRequiredService<FakeIdentityService>()
                : ctx.GetRequiredService<RealIdentityService>();
            }
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        // ...
    }
}

这个实现显然比以前的版本要复杂的多,可是至少能够兼容通用主机的方式。

实际上,若是仅须要一个强类型选项,那么这个方法就有点过头了。相反的,这里我可能只会从新绑定一下配置:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // 为依赖注入容器,配置IdentitySetting
        services.Configure<IdentitySettings>(Configuration.GetSection("Identity")); 

        // 从新建立强类型选项对象,并绑定
        var identitySettings = new IdentitySettings();
        Configuration.GetSection("Identity").Bind(identitySettings)

        // 根据条件配置正确的服务
        if(identitySettings.UseFakeIdentity)
        {
            services.AddScoped<IIdentityService, FakeIdentityService>();
        }
        else
        {
            services.AddScoped<IIdentityService, RealIdentityService>();
        }
    }

    public void Configure(IApplicationBuilder app)
    {
        // ...
    }
}

除此以外,若是仅仅只须要从配置文件中加载一个字符串,我可能根本不会使用强类型选项。这是.NET Core默认模板中拥堵配置ASP.NET Core身份系统的方法 - 直接经过IConfiguration实例检索链接字符串。

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // 针对依赖注入容器,配置ConnectionStrings
        services.Configure<ConnectionStrings>(Configuration.GetSection("ConnectionStrings")); 

        // 直接获取配置,不使用强类型选项
        var connectionString = Configuration["ConnectionString:BloggingDatabase"];

        services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlite(connectionString));
    }

    public void Configure(IApplicationBuilder app)
    {
        // ...
    }
}

这个实现方式都不是最好的,可是他们均可以知足咱们的需求,以及大部分的场景。若是你之前不知道Startup的服务注入特性,那么你确定使用了以上方式中的一种。

使用IConfigureOptions来对IdentityServer进行配置

另一个使用注入配置的常见场景是配置IdentityServer的验证。

public class Startup
{
    public Startup(IdentitySettings identitySettings)
    {
        IdentitySettings = identitySettings;
    }

    public IdentitySettings IdentitySettings { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // 配置IdentityServer的验证方式
        services
            .AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
            .AddIdentityServerAuthentication(options =>
            {
                // 使用强类型选项来配置验证处理器
                options.Authority = identitySettings.ServerFullPath;
                options.ApiName = identitySettings.ApiName;
            });
    }

    public void Configure(IApplicationBuilder app)
    {
        // ...
    }
}

在这个例子中,IdentityServer实例的基本地址和API资源名都是经过强类型选项选项IdentitySettings设置的. 这种实现方式在.NET Core 3.0中已经再也不适用了,因此咱们须要一个可替换的方案。咱们可使用以前提到的方式 - 从新绑定强类型选项或者直接使用IConfiguration对象检索配置。

除此以外,第三种选择是使用IConfigureOptions, 这是我经过查看AddIdentityServerAuthentication方法的底层代码发现的。

事实证实,AddIdentityServerAuthentication()方法能够作一些不一样的事情。首先,它配置了JWT Bearer验证,而且经过强类型选项指定了验证的方式。咱们能够利用它来延迟配置命名选项(named options), 改成使用IConfigureOptions实例。

IConfigureOptions接口容许你使用Service Provider中的其余依赖项延迟配置强类型选项对象。例如,若是要配置个人TestSettings服务时,我须要调用TestService类中的一个方法,我能够建立一个IConfigureOptions对象实例,代码以下:

public class MyTestSettingsConfigureOptions : IConfigureOptions<TestSettings>
{
    private readonly TestService _testService;
    public MyTestSettingsConfigureOptions(TestService testService)
    {
        _testService = testService;
    }

    public void Configure(TestSettings options)
    {
        options.MyTestValue = _testService.GetValue();
    }
}

TestServiceIConfigureOptions<TestSettings>都是在Startup.ConfigureServices方法中同时配置的。

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<TestService>();
    services.ConfigureOptions<MyTestSettingsConfigureOptions>();
}

这里最重要的一点是,你可使用标准的构造函数依赖注入一个IOptions<TestSettings>对象。这里再也不须要在ConfigureServices方法中“部分构建”Service Provider, 便可配置TestSettings. 相反的,咱们注册了配置TestSettings的意图,可是真正的配置会被推迟到配置对象被使用的时候。

那么这对于咱们配置IdentityServer,有什么帮助呢?

AddIdentityServerAuthentication使用了强类型选项的一种变体,咱们称之为命名选项(named options). 这种方式在验证配置的时候很是常见,就像咱们上面的例子同样。

简而言之,你可使用IConfigureOptions方式将验证处理程序使用的命名选项IdentityServerAuthenticationOptions的配置延迟。所以,你能够建立一个将IdentitySettings做为构造参数的ConfigureIdentityServerOptions对象。

public class ConfigureIdentityServerOptions : IConfigureNamedOptions<IdentityServerAuthenticationOptions>
{
    readonly IdentitySettings _identitySettings;
    public ConfigureIdentityServerOptions(IdentitySettings identitySettings)
    {
        _identitySettings = identitySettings;
        _hostingEnvironment = hostingEnvironment;
    }

    public void Configure(string name, IdentityServerAuthenticationOptions options)
    { 
        // Only configure the options if this is the correct instance
        if (name == IdentityServerAuthenticationDefaults.AuthenticationScheme)
        {
            // 使用强类型IdentitySettings对象中的值
            options.Authority = _identitySettings.ServerFullPath; 
            options.ApiName = _identitySettings.ApiName;
        }
    }

    // This won't be called, but is required for the IConfigureNamedOptions interface
    public void Configure(IdentityServerAuthenticationOptions options) => Configure(Options.DefaultName, options);
}

Startup.cs文件中,你须要配置强类型IdentitySettings对象,添加所需的IdentityServer服务,并注册ConfigureIdentityServerOptions类,以便当须要时,它能够配置IdentityServerAuthenticationOptions.

public void ConfigureServices(IServiceCollection services)
{
    // 配置强类型IdentitySettings选项
    services.Configure<IdentitySettings>(Configuration.GetSection("Identity"));

    // 配置IdentityServer验证方式
    services
        .AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
        .AddIdentityServerAuthentication();

    // 添加其余配置
    services.ConfigureOptions<ConfigureIdentityServerOptions>();
}

这里,咱们无需向Startup类中注入任何内容,可是你依然能够得到强类型选项的好处。因此这里咱们获得一个共赢的结果。

总结

在本文中,我描述了升级到ASP.NET Core 3.0时,能够须要对Startup 类进行的一些修改。我经过在Startup类中注入服务,描述了ASP.NET Core 2.x中的问题,以及如何在ASP.NET Core 3.0中移除这个功能。最后我展现了,当须要这种实现方式的时候改如何去作。

相关文章
相关标签/搜索