注:本文隸屬於《理解ASP.NET Core》系列文章,請檢視置頂部落格或點選此處檢視全文目錄
準備工作:一份ASP.NET Core Web API應用程式
當我們來到一個陌生的環境,第一件事就是找到廁所在哪。
當我們接觸一份新框架時,第一件事就是找到程式入口,即Main方法
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
程式碼很簡單,典型的建造者模式:通過IHostBuilder
建立一個通用主機(Generic Host)
,然後啟動它(至於什麼是通用主機,我們們後續的文章會說到)。我們們不要一上來就去研究CreateDefaultBuilder
、ConfigureWebHostDefaults
這些方法的原始碼,應該去尋找能看的見、摸得著的,很明顯,只有Startup
。
Startup類
Startup
類承擔應用的啟動任務,所以按照約定,起名為Startup
,不過你可以修改為任意類名(強烈建議類名為Startup)。
預設的Startup
結構很簡單,包含:
- 建構函式
Configuration
屬性ConfigureServices
方法Configure
方法
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// 該方法由執行時呼叫,使用該方法向DI容器新增服務
public void ConfigureServices(IServiceCollection services)
{
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
// 該方法由執行時呼叫,使用該方法配置HTTP請求管道
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
}
}
Startup建構函式
當使用通用主機(Generic Host)時,Startup建構函式支援注入以下三種服務型別:
IConfiguration
IWebHostEnvironment
IHostEnvironment
public Startup(
IConfiguration configuration,
IHostEnvironment hostEnvironment,
IWebHostEnvironment webHostEnvironment)
{
Configuration = configuration;
HostEnvironment = hostEnvironment;
WebHostEnvironment = webHostEnvironment;
}
public IConfiguration Configuration { get; }
public IHostEnvironment HostEnvironment { get; set; }
public IWebHostEnvironment WebHostEnvironment { get; set; }
這裡你會發現
HostEnvironment
和WebHostEnvironment
的例項是同一個。彆著急,後續文章我們聊到Host的時候,你就明白了。
ConfigureServices
- 該方法是可選的
- 該方法用於新增服務到DI容器中
- 該方法在
Configure
方法之前被呼叫 - 該方法要麼無引數,要麼只能有一個引數且型別必須為
IServiceCollection
- 該方法內的程式碼大多是形如
Add{Service}
的擴充套件方法
常用的服務有(部分服務框架已預設註冊):
AddControllers
:註冊Controller相關服務,內部呼叫了AddMvcCore
、AddApiExplorer
、AddAuthorization
、AddCors
、AddDataAnnotations
、AddFormatterMappings
等多個擴充套件方法AddOptions
:註冊Options相關服務,如IOptions<>
、IOptionsSnapshot<>
、IOptionsMonitor<>
、IOptionsFactory<>
、IOptionsMonitorCache<>
等。很多服務都需要Options,所以很多服務註冊的擴充套件方法會在內部呼叫AddOptions
AddRouting
:註冊路由相關服務,如IInlineConstraintResolver
、LinkGenerator
、IConfigureOptions<RouteOptions>
、RoutePatternTransformer
等AddAddLogging
:註冊Logging相關服務,如ILoggerFactory
、ILogger<>
、IConfigureOptions<LoggerFilterOptions>>
等AddAuthentication
:註冊身份認證相關服務,以方便後續註冊JwtBearer、Cookie等服務AddAuthorization
:註冊使用者授權相關服務AddMvc
:註冊Mvc相關服務,比如Controllers、Views、RazorPages等AddHealthChecks
:註冊健康檢查相關服務,如HealthCheckService
、IHostedService
等
Configure
- 該方法是必須的
- 該方法用於配置HTTP請求管道,通過向管道新增中介軟體,應用不同的響應方式。
- 該方法在
ConfigureServices
方法之後被呼叫 - 該方法中的引數可以接受任何已注入到DI容器中的服務
- 該方法內的程式碼大多是形如
Use{Middleware}
的擴充套件方法 - 該方法內中介軟體的註冊順序與程式碼的書寫順序是一致的,先註冊的先執行,後註冊的後執行
常用的中介軟體有
UseDeveloperExceptionPage
:當發生異常時,展示開發人員異常資訊頁。如圖
UseRouting
:路由中介軟體,根據Url中的路徑導航到對應的Endpoint。必須與UseEndpoints
搭配使用。UseEndpoints
:執行路由所選擇的Endpoint對應的委託。UseAuthentication
:身份認證中介軟體,用於對請求使用者的身份進行認證。比如,早晨上班打卡時,管理員認出你是公司員工,那麼才允許你進入公司。UseAuthorization
:使用者授權中介軟體,用於對請求使用者進行授權。比如,雖然你是公司員工,但是你是一名.NET開發工程師,那麼你只允許坐在.NET開發工程師區域的工位上,而不能坐在老總的辦公室裡。UseMvc
:Mvc中介軟體。UseHealthChecks
:健康檢查中介軟體。UseMiddleware
:用來新增匿名中介軟體的,通過該方法,可以方便的新增自定義中介軟體。
省略Startup類
另外,Startup
類也可以省略,直接進行如下配置即可(雖然可以這樣做,但是不推薦):
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
// ConfigureServices 可以呼叫多次,最終會將結果聚合
webBuilder.ConfigureServices(services =>
{
})
// Configure 如果呼叫多次,則只有最後一次生效
.Configure(app =>
{
var env = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
});
});
IStartupFilter
public interface IStartupFilter
{
Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next);
}
有時,我們想要將一系列相關中介軟體的註冊封裝到一起,那麼我們只需要通過實現IStartupFilter
,並在Startup.ConfigureServices
中配置IStartupFilter
的依賴注入即可。
- 在
IStartupFilter
中配置的中介軟體,總是比Startup
類中Configure
方法中的中介軟體先註冊;對於多個IStartupFilter
實現,執行順序與服務註冊時的順序一致
我們可以通過一個例子來驗證一下中介軟體的註冊順序。
首先是三個IStartupFilter
的實現類:
public class FirstStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
=> app =>
{
app.Use((context, next) =>
{
Console.WriteLine("First");
return next();
});
next(app);
};
}
public class SecondStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
=> app =>
{
app.Use((context, next) =>
{
Console.WriteLine("Second");
return next();
});
next(app);
};
}
public class ThirdStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
=> app =>
{
app.Use((context, next) =>
{
Console.WriteLine("Third");
return next();
});
next(app);
};
}
接下來進行註冊:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
// 第一個被註冊
services.AddTransient<IStartupFilter, FirstStartupFilter>();
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.ConfigureServices(services =>
{
// 第三個被註冊
services.AddTransient<IStartupFilter, ThirdStartupFilter>();
});
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// 第二個被註冊
services.AddTransient<IStartupFilter, SecondStartupFilter>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 第四個被註冊
app.Use((context, next) =>
{
Console.WriteLine("Forth");
return next();
});
}
}
最後通過輸出可以看到,執行順序的確是這樣子的。
First
Second
Third
Forth
IHostingStartup
與IStartupFilter
不同的是,IHostingStartup
可以在啟動時通過外部程式集嚮應用增加更多功能。不過這要求必須呼叫ConfigureWebHost
、ConfigureWebHostDefaults
等類似用來配置Web主機的擴充套件方法
我們經常使用的Nuget包
SkyApm.Agent.AspNetCore
就使用了該特性。
下面我們就來看一下該如何使用它。
HostingStartup 程式集
要建立HostingStartup程式集,可以通過建立類庫專案或無入口點的控制檯應用來實現。
接下來我們們還是看一下上面提到過的SkyApm.Agent.AspNetCore
:
using SkyApm.Agent.AspNetCore;
[assembly: HostingStartup(typeof(SkyApmHostingStartup))]
namespace SkyApm.Agent.AspNetCore
{
internal class SkyApmHostingStartup : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureServices(services => services.AddSkyAPM(ext => ext.AddAspNetCoreHosting()));
}
}
}
該HostingStartup類:
- 實現了
IHostingStartup
介面 Configure
方法中使用IWebHostBuilder
來新增增強功能- 配置了
HostingStartup
特性
HostingStartup 特性
HostingStartup
特性用於標識哪個類是HostingStartup類,HostingStartup類需要實現IHostingStartup
介面。
當程式啟動時,會自動掃描入口程式集和配置的待啟用的的程式集列表(參見下方:啟用HostingStarup程式集),來找到所有的HostingStartup
特性,並通過反射的方式建立Startup
並呼叫Configure
方法。
using SkyApm.Agent.AspNetCore;
[assembly: HostingStartup(typeof(SkyApmHostingStartup))]
namespace SkyApm.Agent.AspNetCore
{
internal class SkyApmHostingStartup : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureServices(services => services.AddSkyAPM(ext => ext.AddAspNetCoreHosting()));
}
}
}
啟用HostingStarup程式集
要啟用HostingStarup程式集,我們有兩種配置方式:
1.使用環境變數(推薦)
使用環境變數,無需侵入程式程式碼,所以我更推薦大家使用這種方式。
配置環境變數ASPNETCORE_HOSTINGSTARTUPASSEMBLIES
,多個程式集使用分號(;)進行分隔,用於新增要啟用的程式集。變數WebHostDefaults.HostingStartupAssembliesKey
就是指代這個環境變數的Key。
另外,還有一個環境變數,叫做ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES
,多個程式集使用分號(;)進行分隔,用於排除要啟用的程式集。變數WebHostDefaults.HostingStartupExcludeAssembliesKey
就是指代這個環境變數的Key。
我們在 launchSettings.json 中新增兩個程式集:
"environmentVariables": {
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "SkyAPM.Agent.AspNetCore;HostingStartupLibrary"
}
2.在程式中配置
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseSetting(
WebHostDefaults.HostingStartupAssembliesKey,
"SkyAPM.Agent.AspNetCore;HostingStartupLibrary")
.UseStartup<Startup>();
});
這樣就配置完成了,很??的一個功能點吧!
需要注意的是,無論使用哪種配置方式,當存在多個HostingStartup程式集時,將按配置這些程式集時的書寫順序執行 Configure
方法。
多環境配置
一款軟體,一般要經過需求分析、設計編碼,單元測試、整合測試以及系統測試等一系列測試流程,驗收,最終上線。那麼,就至少需要4套環境來保證系統執行:
Development
:開發環境,用於開發人員在本地對應用進行除錯執行Test
:測試環境,用於測試人員對應用進行測試Staging
:預釋出環境,用於在正式上線之前,對應用進行整合、測試和預覽,或用於驗收Production
:生產環境,應用的正式線上環境
環境配置方式
通過環境變數ASPNETCORE_ENVIRONMENT
指定執行環境
注意:如果未指定環境,預設情況下,為 Production
在專案的Properties資料夾裡面,有一個“launchSettings.json”檔案,該檔案是用於配置VS中專案啟動的。
接下來我們就在launchSettings.json
中配置一下。
先解釋一下該檔案中出現的幾個引數:
commandName
:指定要啟動的Web伺服器,有三個可選值:- Project:啟動 Kestrel
- IISExpress:啟動IIS Express
- IIS:不啟用任何Web伺服器,使用IIS
dotnetRunMessages
:bool字串,指示當使用 dotnet run 命令時,終端能夠及時響應並輸出訊息,具體參考stackoverflow和github issuelaunchBrowser
:bool值,指示當程式啟動後,是否開啟瀏覽器launchUrl
:預設啟動路徑applicationUrl
:應用程式Url列表,多個URL之間使用分號(;
)進行分隔。當launchBrowser為true時,將{applicationUrl}/{launchUrl}作為瀏覽器預設訪問的UrlenvironmentVariables
:環境變數集合,在該集合內配置環境變數
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
// 如果不指定profile,則預設選擇第一個
// Development
"ASP.NET.WebAPI": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
// Test
"ASP.NET.WebAPI.Test": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Test"
}
},
// Staging
"ASP.NET.WebAPI.Staging": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Staging"
}
},
// Production
"ASP.NET.WebAPI.Production": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Production"
}
},
// 用於測試在未指定環境時,預設是否為Production
"ASP.NET.WebAPI.Default": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"applicationUrl": "http://localhost:5000"
}
}
}
配置完成後,就可以在VS上方工具欄中的專案啟動處選擇啟動項了
基於環境的 Startup
Startup類支援針對不同環境進行個性化配置,有三種方式:
- 將
IWebHostEnvironment
注入 Startup 類 - Startup 方法約定
- Startup 類約定
1.將IWebHostEnvironment
注入 Startup 類
通過將IWebHostEnvironment
注入 Startup 類,然後在方法中使用條件判斷書寫不同環境下的程式碼。該方式適用於多環境下,程式碼差異較少的情況。
public class Startup
{
public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
{
Configuration = configuration;
WebHostEnvironment = webHostEnvironment;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment WebHostEnvironment { get; }
public void ConfigureServices(IServiceCollection services)
{
if (WebHostEnvironment.IsDevelopment())
{
Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");
}
else if (WebHostEnvironment.IsTest())
{
Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");
}
else if (WebHostEnvironment.IsStaging())
{
Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");
}
else if (WebHostEnvironment.IsProduction())
{
Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");
}
}
public void Configure(IApplicationBuilder app)
{
if (WebHostEnvironment.IsDevelopment())
{
Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");
}
else if (WebHostEnvironment.IsTest())
{
Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");
}
else if (WebHostEnvironment.IsStaging())
{
Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");
}
else if (WebHostEnvironment.IsProduction())
{
Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");
}
}
}
public static class AppHostEnvironmentEnvExtensions
{
public static bool IsTest(this IHostEnvironment hostEnvironment)
{
if (hostEnvironment == null)
{
throw new ArgumentNullException(nameof(hostEnvironment));
}
return hostEnvironment.IsEnvironment(AppEnvironments.Test);
}
}
public static class AppEnvironments
{
public static readonly string Test = nameof(Test);
}
2.Startup 方法約定
上面的方式把不同環境的程式碼放在了同一個方法中,看起來比較混亂也不容易區分。因此我們希望ConfigureServices
和Configure
能夠根據不同的環境進行程式碼拆分。
我們可以通過方法命名約定來解決,約定Configure{EnvironmentName}Services
和Configure{EnvironmentName}Services
來裝載不同環境的程式碼。如果當前環境沒有對應的方法,則使用原來的ConfigureServices
和Configure
方法。
我就只拿 Development 和 Production 舉例了
public class Startup
{
// 我這裡注入 IWebHostEnvironment,僅僅是為了列印出來當前環境資訊
public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
{
Configuration = configuration;
WebHostEnvironment = webHostEnvironment;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment WebHostEnvironment { get; }
#region ConfigureServices
private void StartupConfigureServices(IServiceCollection services)
{
Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");
}
public void ConfigureDevelopmentServices(IServiceCollection services)
{
StartupConfigureServices(services);
}
public void ConfigureProductionServices(IServiceCollection services)
{
StartupConfigureServices(services);
}
public void ConfigureServices(IServiceCollection services)
{
StartupConfigureServices(services);
}
#endregion
#region Configure
private void StartupConfigure(IApplicationBuilder app)
{
Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");
}
public void ConfigureDevelopment(IApplicationBuilder app)
{
StartupConfigure(app);
}
public void ConfigureProduction(IApplicationBuilder app)
{
StartupConfigure(app);
}
public void Configure(IApplicationBuilder app)
{
StartupConfigure(app);
}
#endregion
}
3.Startup 類約定
該方式適用於多環境下,程式碼差異較大的情況。
程式啟動時,會優先尋找當前環境命名符合Startup{EnvironmentName}
的 Startup 類,如果找不到,則使用名稱為Startup
的類
首先,CreateHostBuilder
方法需要做一處修改
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
//webBuilder.UseStartup<Startup>();
webBuilder.UseStartup(typeof(Startup).GetTypeInfo().Assembly.FullName);
});
接下來,就是為各個環境定義 Startup 類了(我就只拿 Development 和 Production 舉例了)
public class StartupDevelopment
{
// 我這裡注入 IWebHostEnvironment,僅僅是為了列印出來當前環境資訊
public StartupDevelopment(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
{
Configuration = configuration;
WebHostEnvironment = webHostEnvironment;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment WebHostEnvironment { get; }
public void ConfigureServices(IServiceCollection services)
{
Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");
}
public void Configure(IApplicationBuilder app)
{
Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");
}
}
public class StartupProduction
{
public StartupProduction(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
{
Configuration = configuration;
WebHostEnvironment = webHostEnvironment;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment WebHostEnvironment { get; }
public void ConfigureServices(IServiceCollection services)
{
Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");
}
public void Configure(IApplicationBuilder app)
{
Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");
}
}