本篇是如何升級到ASP.NET Core 3.0
系列文章的第二篇。html
IHostingEnvironment
VS IHostEnvironent
- .NET Core 3.0中的廢棄類型在本篇博客中,我將描述從ASP.NET Core 2.x應用升級到.NET Core 3.0須要作的一個修改:你不在須要在Startup
構造函數中注入服務了。c#
在.NET Core 3.0中, ASP.NET Core 3.0的託管基礎已經被從新設計爲通用主機,而再也不與之並行使用。那麼這對於那些正在使用ASP.NET Core 2.x開發應用的開發人員,這意味着什麼呢?在目前這個階段,我已經遷移了多個應用,到目前爲止,一切都進展順利。官方的遷移指導文檔能夠很好的指導你完成所需的步驟,所以,我強烈建議你讀一下這篇文檔。安全
在遷移過程當中,我遇到的最多兩個問題是:app
Startup
類注入服務其中第一點,我以前已經講解過了。端點路由(Endpoint Routing)是在ASP.NET Core 2.2中引入的,可是被限制只能在MVC中使用。在ASP.NET Core 3.0中,端點路由已是推薦的終端中間件實現了,由於它提供了不少好處。其中最重要的是,它容許中間件獲取哪個端點最終會被執行,而且能夠檢索有關這個端點的元數據(metadata)。例如,你能夠爲健康檢查端點應用受權。asp.net
端點路由是在配置中間件順序時須要特別注意。我建議你再升級你的應用前,先閱讀一下官方遷移文檔針對此處的說明,後續我將寫一篇博客來介紹如何將終端中間件轉換爲端點路由。ide
第二點,是已經提到了的將服務注入Startup
類,可是並無獲得足夠的宣傳。我不太肯定是否是由於這樣作的人很少,仍是在一些場景下,它很容易解決。在本篇中,我將展現一些問題場景,並提供一些解決方案。函數
在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
以前,就已經進行非空驗證。這並非一種正規的基礎技術,可是實時證實使用起來很是的順手。
然而,若是切換到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
類的構造函數注入IHostEnvironment
和IConfiguration
, 可是僅此而已。至於緣由,應該是以前的實現方式會帶來一些問題,下面我將給你們詳細描述一下。
注意:若是你堅持在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(); } }
TestService
和IConfigureOptions<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中移除這個功能。最後我展現了,當須要這種實現方式的時候改如何去作。