避免在ASP.NET Core 3.0中為啟動類注入服務
本篇是如何升級到ASP.NET Core 3.0
系列文章的第二篇。
- Part 1 - 將.NET Standard 2.0類庫轉換為.NET Core 3.0類庫
- Part 2 -
IHostingEnvironment
VSIHostEnvironent
- .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開發應用的開發人員,這意味著什麼呢?在目前這個階段,我已經遷移了多個應用,到目前為止,一切都進展順利。官方的遷移指導文件可以很好的指導你完成所需的步驟,因此,我強烈建議你讀一下這篇文件。
在遷移過程中,我遇到的最多兩個問題是:
- ASP.NET Core 3.0中配置中介軟體的推薦方式是使用端點路由(Endpoint Routing)。
- 通用主機不允許為
Startup
其中第一點,我之前已經講解過了。端點路由(Endpoint Routing)是在ASP.NET Core 2.2中引入的,但是被限制只能在MVC中使用。在ASP.NET Core 3.0中,端點路由已經是推薦的終端中介軟體實現了,因為它提供了很多好處。其中最重要的是,它允許中介軟體獲取哪一個端點最終會被執行,並且可以檢索有關這個端點的元資料(metadata)。例如,你可以為健康檢查端點應用授權。
端點路由是在配置中介軟體順序時需要特別注意。我建議你再升級你的應用前,先閱讀一下官方遷移文件針對此處的說明,後續我將寫一篇部落格來介紹如何將終端中介軟體轉換為端點路由。
第二點,是已經提到了的將服務注入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<Startup>()
.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<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
類的建構函式注入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中移除這個功能。最後我展示了,當需要這種實現方式的時候改如何去做