避免在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中移除這個功能。最後我展現了,當須要這種實現方式的時候改如何去作。

相關文章
相關標籤/搜索