避免在 ASP.NET Core 3.0 中为启动类注入服务
本篇是如何升級到ASP.NET Core 3.0系列文章的第二篇。
-
Part 1 - 將.NET Standard 2.0 類庫轉換為.NET Core 3.0 類庫
-
Part 2 - IHostingEnvironment VS IHostEnvironent - .NET Core 3.0 中的廢棄類型
-
Part 3 - 避免在 ASP.NET Core 3.0 中為啟動類注入服務(本篇)
-
Part 4 - 將終端中間件轉換為 ASP.NET Core 3.0 中的端點路由
-
Part 5 - 將集成測試的轉換為 NET Core 3.0
在本篇博客中,我將描述從 ASP.NET Core 2.x 應用升級到.NET Core 3.0 需要做的一個修改:你不在需要在Startup構造函數中注入服務了。
在 ASP.NET Core 3.0 中遷移到通用主機
在.NET Core 3.0 中, ASP.NET Core 3.0 的托管基礎已經被重新設計為通用主機,而不再與之并行使用。那么這對于那些正在使用 ASP.NET Core 2.x 開發應用的開發人員,這意味著什么呢?在目前這個階段,我已經遷移了多個應用,到目前為止,一切都進展順利。官方的遷移指導文檔[1]可以很好的指導你完成所需的步驟,因此,我強烈建議你讀一下這篇文檔。
在遷移過程中,我遇到的最多兩個問題是:
-
ASP.NET Core 3.0 中配置中間件的推薦方式是使用端點路由(Endpoint Routing)。
-
通用主機不允許為Startup類注入服務
其中第一點,我之前已經講解過了。端點路由(Endpoint Routing)是在 ASP.NET Core 2.2 中引入的,但是被限制只能在 MVC 中使用。在 ASP.NET Core 3.0 中,端點路由已經是推薦的終端中間件實現了,因為它提供了很多好處。其中最重要的是,它允許中間件獲取哪一個端點最終會被執行,并且可以檢索有關這個端點的元數據(metadata)。例如,你可以為健康檢查端點應用授權。
端點路由是在配置中間件順序時需要特別注意。我建議你再升級你的應用前,先閱讀一下官方遷移文檔[2]針對此處的說明,后續我將寫一篇博客來介紹如何將終端中間件轉換為端點路由。
第二點,是已經提到了的將服務注入Startup類,但是并沒有得到足夠的宣傳。我不太確定是不是因為這樣做的人不多,還是在一些場景下,它很容易解決。在本篇中,我將展示一些問題場景,并提供一些解決方案。
ASP.NET Core 2.x 啟動類中注入服務
在 ASP.NET Core 2.x 版本中,有一個鮮為人知的特性,就是你可以在Program.cs文件中配置你的依賴注入容器。以前我曾經使用這種方式來進行強類型選項,然后在配置依賴注入容器的其余剩余部分時使用這些配置。
下面我們來看一下 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().ConfigureSettings(); // 配置服務,后續將在Startup中使用 }這里有沒有注意到在CreateWebHostBuilder中調用了一個ConfigureSettings()的方法?這是一個我用來配置應用強類型選項的擴展方法。例如,這個擴展方法可能看起來是這樣的:
public static class SettingsinstallerExtensions {public static IWebHostBuilder ConfigureSettings(this IWebHostBuilder builder){return builder.ConfigureServices((context, services) =>{var config = context.Configuration;services.Configure(config.GetSection("ConnectionStrings"));services.AddSingleton(ctx => ctx.GetService>().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(options =>options.UseSqlServer(ConnectionStrings.BloggingDatabase));}public void Configure(IApplicationBuilder app){}} }我發現,當我先要在ConfigureServices方法中使用強類型選項對象配置其他服務時,這種模式非常的有用。在我上面的例子中,ConnectionStrings對象是一個強類型對象,并且這個對象在程序進入Startup之前,就已經進行非空驗證。這并不是一種正規的基礎技術,但是實時證明使用起來非常的順手。
PS:如何為 ASP.NET Core 的強類型選項對象添加驗證[3]
然而,如果切換到 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.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();}else{services.AddScoped();}}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){// 為依賴注入容器,配置IdentitySettingservices.Configure(Configuration.GetSection("Identity"));// 注冊不同的實現services.AddScoped();services.AddScoped();// 根據IdentitySetting配置,在運行時返回一個正確的實現services.AddScoped(ctx =>{var identitySettings = ctx.GetRequiredService();return identitySettings.UseFakeIdentity? ctx.GetRequiredService(): ctx.GetRequiredService();}});}public void Configure(IApplicationBuilder app){// ...} }這個實現顯然比之前的版本要復雜的多,但是至少可以兼容通用主機的方式。
實際上,如果僅需要一個強類型選項,那么這個方法就有點過頭了。相反的,這里我可能只會重新綁定一下配置:
public class Startup {public Startup(IConfiguration configuration){Configuration = configuration;}public IConfiguration Configuration { get; }public void ConfigureServices(IServiceCollection services){// 為依賴注入容器,配置IdentitySettingservices.Configure(Configuration.GetSection("Identity"));// 重新創建強類型選項對象,并綁定var identitySettings = new IdentitySettings();Configuration.GetSection("Identity").Bind(identitySettings)// 根據條件配置正確的服務if(identitySettings.UseFakeIdentity){services.AddScoped();}else{services.AddScoped();}}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){// 針對依賴注入容器,配置ConnectionStringsservices.Configure(Configuration.GetSection("ConnectionStrings"));// 直接獲取配置,不使用強類型選項var connectionString = Configuration["ConnectionString:BloggingDatabase"];services.AddDbContext(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 {private readonly TestService _testService;public MyTestSettingsConfigureOptions(TestService testService){_testService = testService;}public void Configure(TestSettings options){options.MyTestValue = _testService.GetValue();} }TestService和IConfigureOptions都是在Startup.ConfigureServices方法中同時配置的。
public void ConfigureServices(IServiceCollection services) {services.AddScoped();services.ConfigureOptions(); }這里最重要的一點是,你可以使用標準的構造函數依賴注入一個IOptions對象。這里不再需要在ConfigureServices方法中“部分構建”Service Provider, 即可配置TestSettings. 相反的,我們注冊了配置TestSettings的意圖,但是真正的配置會被推遲到配置對象被使用的時候。
那么這對于我們配置 IdentityServer,有什么幫助呢?
AddIdentityServerAuthentication使用了強類型選項的一種變體,我們稱之為命名選項(named options). 這種方式在驗證配置的時候非常常見,就像我們上面的例子一樣。
簡而言之,你可以使用IConfigureOptions方式將驗證處理程序使用的命名選項IdentityServerAuthenticationOptions的配置延遲。因此,你可以創建一個將IdentitySettings作為構造參數的ConfigureIdentityServerOptions對象。
public class ConfigureIdentityServerOptions : IConfigureNamedOptions {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 instanceif (name == IdentityServerAuthenticationDefaults.AuthenticationScheme){// 使用強類型IdentitySettings對象中的值options.Authority = _identitySettings.ServerFullPath;options.ApiName = _identitySettings.ApiName;}}// This won't be called, but is required for the IConfigureNamedOptions interfacepublic void Configure(IdentityServerAuthenticationOptions options) => Configure(Options.DefaultName, options); }在Startup.cs文件中,你需要配置強類型IdentitySettings對象,添加所需的 IdentityServer 服務,并注冊ConfigureIdentityServerOptions類,以便當需要時,它可以配置IdentityServerAuthenticationOptions.
public void ConfigureServices(IServiceCollection services) {// 配置強類型IdentitySettings選項services.Configure(Configuration.GetSection("Identity"));// 配置IdentityServer驗證方式services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme).AddIdentityServerAuthentication();// 添加其他配置services.ConfigureOptions(); }這里,我們無需向Startup類中注入任何內容,但是你依然可以獲得強類型選項的好處。所以這里我們得到一個雙贏的結果。
總結
在本文中,我描述了升級到 ASP.NET Core 3.0 時,可以需要對Startup 類進行的一些修改。我通過在Startup類中注入服務,描述了 ASP.NET Core 2.x 中的問題,以及如何在 ASP.NET Core 3.0 中移除這個功能。最后我展示了,當需要這種實現方式的時候改如何去做。
參考資料
[1]
遷移指導文檔: https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.1&tabs=visual-studio
[2]官方遷移文檔: https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.1&tabs=visual-studio#routing-startup-code
[3]如何為ASP.NET Core的強類型選項對象添加驗證: https://www.cnblogs.com/lwqlun/p/10084047.html
總結
以上是生活随笔為你收集整理的避免在 ASP.NET Core 3.0 中为启动类注入服务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET CORE(C#) WPF简单菜
- 下一篇: 我的 .NET Core 博客性能优化经