注:本文隸屬於《理解ASP.NET Core》系列文章,請檢視置頂部落格或點選此處檢視全文目錄

準備工作:一份ASP.NET Core Web API應用程式

當我們來到一個陌生的環境,第一件事就是找到廁所在哪。

當我們接觸一份新框架時,第一件事就是找到程式入口,即Main方法

  1. public class Program
  2. {
  3. public static void Main(string[] args)
  4. {
  5. CreateHostBuilder(args).Build().Run();
  6. }
  7. public static IHostBuilder CreateHostBuilder(string[] args) =>
  8. Host.CreateDefaultBuilder(args)
  9. .ConfigureWebHostDefaults(webBuilder =>
  10. {
  11. webBuilder.UseStartup<Startup>();
  12. });
  13. }

程式碼很簡單,典型的建造者模式:通過IHostBuilder建立一個通用主機(Generic Host),然後啟動它(至於什麼是通用主機,咱們後續的文章會說到)。咱們不要一上來就去研究CreateDefaultBuilderConfigureWebHostDefaults這些方法的原始碼,應該去尋找能看的見、摸得著的,很明顯,只有Startup

Startup類

Startup類承擔應用的啟動任務,所以按照約定,起名為Startup,不過你可以修改為任意類名(強烈建議類名為Startup)。

預設的Startup結構很簡單,包含:

  • 建構函式
  • Configuration屬性
  • ConfigureServices方法
  • Configure方法
  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8. // This method gets called by the runtime. Use this method to add services to the container.
  9. // 該方法由執行時呼叫,使用該方法向DI容器新增服務
  10. public void ConfigureServices(IServiceCollection services)
  11. {
  12. }
  13. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
  14. // 該方法由執行時呼叫,使用該方法配置HTTP請求管道
  15. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  16. {
  17. }
  18. }

Startup建構函式

當使用通用主機(Generic Host)時,Startup建構函式支援注入以下三種服務型別:

  • IConfiguration
  • IWebHostEnvironment
  • IHostEnvironment
  1. public Startup(
  2. IConfiguration configuration,
  3. IHostEnvironment hostEnvironment,
  4. IWebHostEnvironment webHostEnvironment)
  5. {
  6. Configuration = configuration;
  7. HostEnvironment = hostEnvironment;
  8. WebHostEnvironment = webHostEnvironment;
  9. }
  10. public IConfiguration Configuration { get; }
  11. public IHostEnvironment HostEnvironment { get; set; }
  12. public IWebHostEnvironment WebHostEnvironment { get; set; }

這裡你會發現 HostEnvironmentWebHostEnvironment 的例項是同一個。彆著急,後續文章我們聊到Host的時候,你就明白了。

ConfigureServices

  • 該方法是可選
  • 該方法用於新增服務到DI容器中
  • 該方法Configure方法之前被呼叫
  • 該方法要麼無引數,要麼只能有一個引數且型別必須為IServiceCollection
  • 該方法內的程式碼大多是形如Add{Service}的擴充套件方法

常用的服務有(部分服務框架已預設註冊):

  • AddControllers:註冊Controller相關服務,內部呼叫了AddMvcCoreAddApiExplorerAddAuthorizationAddCorsAddDataAnnotationsAddFormatterMappings等多個擴充套件方法
  • AddOptions:註冊Options相關服務,如IOptions<>IOptionsSnapshot<>IOptionsMonitor<>IOptionsFactory<>IOptionsMonitorCache<>等。很多服務都需要Options,所以很多服務註冊的擴充套件方法會在內部呼叫AddOptions
  • AddRouting:註冊路由相關服務,如IInlineConstraintResolverLinkGeneratorIConfigureOptions<RouteOptions>RoutePatternTransformer
  • AddAddLogging:註冊Logging相關服務,如ILoggerFactoryILogger<>IConfigureOptions<LoggerFilterOptions>>
  • AddAuthentication:註冊身份認證相關服務,以方便後續註冊JwtBearer、Cookie等服務
  • AddAuthorization:註冊使用者授權相關服務
  • AddMvc:註冊Mvc相關服務,比如Controllers、Views、RazorPages等
  • AddHealthChecks:註冊健康檢查相關服務,如HealthCheckServiceIHostedService

Configure

  • 該方法是必須
  • 該方法用於配置HTTP請求管道,通過向管道新增中介軟體,應用不同的響應方式。
  • 該方法ConfigureServices方法之後被呼叫
  • 該方法中的引數可以接受任何已注入到DI容器中的服務
  • 該方法內的程式碼大多是形如Use{Middleware}的擴充套件方法
  • 該方法內中介軟體的註冊順序與程式碼的書寫順序是一致的,先註冊的先執行,後註冊的後執行

常用的中介軟體有

  • UseDeveloperExceptionPage:當發生異常時,展示開發人員異常資訊頁。如圖

  • UseRouting:路由中介軟體,根據Url中的路徑導航到對應的Endpoint。必須與UseEndpoints搭配使用。
  • UseEndpoints:執行路由所選擇的Endpoint對應的委託。
  • UseAuthentication:身份認證中介軟體,用於對請求使用者的身份進行認證。比如,早晨上班打卡時,管理員認出你是公司員工,那麼才允許你進入公司。
  • UseAuthorization:使用者授權中介軟體,用於對請求使用者進行授權。比如,雖然你是公司員工,但是你是一名.NET開發工程師,那麼你只允許坐在.NET開發工程師區域的工位上,而不能坐在老總的辦公室裡。
  • UseMvc:Mvc中介軟體。
  • UseHealthChecks:健康檢查中介軟體。
  • UseMiddleware:用來新增匿名中介軟體的,通過該方法,可以方便的新增自定義中介軟體。

省略Startup類

另外,Startup類也可以省略,直接進行如下配置即可(雖然可以這樣做,但是不推薦):

  1. public static IHostBuilder CreateHostBuilder(string[] args) =>
  2. Host.CreateDefaultBuilder(args)
  3. .ConfigureWebHostDefaults(webBuilder =>
  4. {
  5. // ConfigureServices 可以呼叫多次,最終會將結果聚合
  6. webBuilder.ConfigureServices(services =>
  7. {
  8. })
  9. // Configure 如果呼叫多次,則只有最後一次生效
  10. .Configure(app =>
  11. {
  12. var env = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
  13. });
  14. });

IStartupFilter

  1. public interface IStartupFilter
  2. {
  3. Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next);
  4. }

有時,我們想要將一系列相關中介軟體的註冊封裝到一起,那麼我們只需要通過實現IStartupFilter,並在Startup.ConfigureServices中配置IStartupFilter的依賴注入即可。

  • IStartupFilter中配置的中介軟體,總是比Startup類中Configure方法中的中介軟體先註冊;對於多個IStartupFilter實現,執行順序與服務註冊時的順序一致

我們可以通過一個例子來驗證一下中介軟體的註冊順序。

首先是三個IStartupFilter的實現類:

  1. public class FirstStartupFilter : IStartupFilter
  2. {
  3. public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
  4. => app =>
  5. {
  6. app.Use((context, next) =>
  7. {
  8. Console.WriteLine("First");
  9. return next();
  10. });
  11. next(app);
  12. };
  13. }
  14. public class SecondStartupFilter : IStartupFilter
  15. {
  16. public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
  17. => app =>
  18. {
  19. app.Use((context, next) =>
  20. {
  21. Console.WriteLine("Second");
  22. return next();
  23. });
  24. next(app);
  25. };
  26. }
  27. public class ThirdStartupFilter : IStartupFilter
  28. {
  29. public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
  30. => app =>
  31. {
  32. app.Use((context, next) =>
  33. {
  34. Console.WriteLine("Third");
  35. return next();
  36. });
  37. next(app);
  38. };
  39. }

接下來進行註冊:

  1. public static IHostBuilder CreateHostBuilder(string[] args) =>
  2. Host.CreateDefaultBuilder(args)
  3. .ConfigureServices(services =>
  4. {
  5. // 第一個被註冊
  6. services.AddTransient<IStartupFilter, FirstStartupFilter>();
  7. })
  8. .ConfigureWebHostDefaults(webBuilder =>
  9. {
  10. webBuilder.UseStartup<Startup>();
  11. })
  12. .ConfigureServices(services =>
  13. {
  14. // 第三個被註冊
  15. services.AddTransient<IStartupFilter, ThirdStartupFilter>();
  16. });
  17. public class Startup
  18. {
  19. public void ConfigureServices(IServiceCollection services)
  20. {
  21. // 第二個被註冊
  22. services.AddTransient<IStartupFilter, SecondStartupFilter>();
  23. }
  24. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  25. {
  26. // 第四個被註冊
  27. app.Use((context, next) =>
  28. {
  29. Console.WriteLine("Forth");
  30. return next();
  31. });
  32. }
  33. }

最後通過輸出可以看到,執行順序的確是這樣子的。

  1. First
  2. Second
  3. Third
  4. Forth

IHostingStartup

IStartupFilter不同的是,IHostingStartup可以在啟動時通過外部程式集嚮應用增加更多功能。不過這要求必須呼叫ConfigureWebHostDefaults擴充套件方法

我們經常使用的Nuget包SkyApm.Agent.AspNetCore就使用了該特性。

下面我們就來看一下該如何使用它。

HostingStartup 程式集

要建立HostingStartup程式集,可以通過建立類庫專案或無入口點的控制檯應用來實現。

接下來咱們還是看一下上面提到過的SkyApm.Agent.AspNetCore

  1. using SkyApm.Agent.AspNetCore;
  2. [assembly: HostingStartup(typeof(SkyApmHostingStartup))]
  3. namespace SkyApm.Agent.AspNetCore
  4. {
  5. internal class SkyApmHostingStartup : IHostingStartup
  6. {
  7. public void Configure(IWebHostBuilder builder)
  8. {
  9. builder.ConfigureServices(services => services.AddSkyAPM(ext => ext.AddAspNetCoreHosting()));
  10. }
  11. }
  12. }

該HostingStartup類:

  • 實現了IHostingStartup介面
  • Configure方法中使用IWebHostBuilder來新增增強功能
  • 配置了HostingStartup特性

HostingStartup 特性

HostingStartup特性用於標識哪個類是HostingStartup類,HostingStartup類需要實現IHostingStartup介面。

當程式啟動時,會自動掃描入口程式集和配置的待啟用的的程式集列表(參見下方:啟用HostingStarup程式集),來找到所有的HostingStartup特性,並通過反射的方式建立Startup並呼叫Configure方法。

SkyApm.Agent.AspNetCore為例

  1. using SkyApm.Agent.AspNetCore;
  2. [assembly: HostingStartup(typeof(SkyApmHostingStartup))]
  3. namespace SkyApm.Agent.AspNetCore
  4. {
  5. internal class SkyApmHostingStartup : IHostingStartup
  6. {
  7. public void Configure(IWebHostBuilder builder)
  8. {
  9. builder.ConfigureServices(services => services.AddSkyAPM(ext => ext.AddAspNetCoreHosting()));
  10. }
  11. }
  12. }

啟用HostingStarup程式集

要啟用HostingStarup程式集,我們有兩種配置方式:

1.使用環境變數(推薦)

使用環境變數,無需侵入程式程式碼,所以我更推薦大家使用這種方式。

配置環境變數ASPNETCORE_HOSTINGSTARTUPASSEMBLIES,多個程式集使用分號(;)進行分隔,用於新增要啟用的程式集。變數WebHostDefaults.HostingStartupAssembliesKey就是指代這個環境變數的Key。

另外,還有一個環境變數,叫做ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES,多個程式集使用分號(;)進行分隔,用於排除要啟用的程式集。變數WebHostDefaults.HostingStartupExcludeAssembliesKey就是指代這個環境變數的Key。

我們在 launchSettings.json 中新增兩個程式集:

  1. "environmentVariables": {
  2. "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "SkyAPM.Agent.AspNetCore;HostingStartupLibrary"
  3. }

2.在程式中配置

  1. public static IHostBuilder CreateHostBuilder(string[] args) =>
  2. Host.CreateDefaultBuilder(args)
  3. .ConfigureWebHostDefaults(webBuilder =>
  4. {
  5. webBuilder.UseSetting(
  6. WebHostDefaults.HostingStartupAssembliesKey,
  7. "SkyAPM.Agent.AspNetCore;HostingStartupLibrary")
  8. .UseStartup<Startup>();
  9. });

這樣就配置完成了,很的一個功能點吧!

需要注意的是,無論使用哪種配置方式,當存在多個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 命令時,終端能夠及時響應並輸出訊息,具體參考stackoverflowgithub issue
  • launchBrowser:bool值,指示當程式啟動後,是否開啟瀏覽器
  • launchUrl:預設啟動路徑
  • applicationUrl:應用程式Url列表,多個URL之間使用分號(;)進行分隔。當launchBrowser為true時,將{applicationUrl}/{launchUrl}作為瀏覽器預設訪問的Url
  • environmentVariables:環境變數集合,在該集合內配置環境變數
  1. {
  2. "$schema": "http://json.schemastore.org/launchsettings.json",
  3. "profiles": {
  4. // 如果不指定profile,則預設選擇第一個
  5. // Development
  6. "ASP.NET.WebAPI": {
  7. "commandName": "Project",
  8. "dotnetRunMessages": "true",
  9. "launchBrowser": true,
  10. "launchUrl": "weatherforecast",
  11. "applicationUrl": "http://localhost:5000",
  12. "environmentVariables": {
  13. "ASPNETCORE_ENVIRONMENT": "Development"
  14. }
  15. },
  16. // Test
  17. "ASP.NET.WebAPI.Test": {
  18. "commandName": "Project",
  19. "dotnetRunMessages": "true",
  20. "launchBrowser": true,
  21. "launchUrl": "weatherforecast",
  22. "applicationUrl": "http://localhost:5000",
  23. "environmentVariables": {
  24. "ASPNETCORE_ENVIRONMENT": "Test"
  25. }
  26. },
  27. // Staging
  28. "ASP.NET.WebAPI.Staging": {
  29. "commandName": "Project",
  30. "dotnetRunMessages": "true",
  31. "launchBrowser": true,
  32. "launchUrl": "weatherforecast",
  33. "applicationUrl": "http://localhost:5000",
  34. "environmentVariables": {
  35. "ASPNETCORE_ENVIRONMENT": "Staging"
  36. }
  37. },
  38. // Production
  39. "ASP.NET.WebAPI.Production": {
  40. "commandName": "Project",
  41. "dotnetRunMessages": "true",
  42. "launchBrowser": true,
  43. "launchUrl": "weatherforecast",
  44. "applicationUrl": "http://localhost:5000",
  45. "environmentVariables": {
  46. "ASPNETCORE_ENVIRONMENT": "Production"
  47. }
  48. },
  49. // 用於測試在未指定環境時,預設是否為Production
  50. "ASP.NET.WebAPI.Default": {
  51. "commandName": "Project",
  52. "dotnetRunMessages": "true",
  53. "launchBrowser": true,
  54. "launchUrl": "weatherforecast",
  55. "applicationUrl": "http://localhost:5000"
  56. }
  57. }
  58. }

配置完成後,就可以在VS上方工具欄中的專案啟動處選擇啟動項了

基於環境的 Startup

Startup類支援針對不同環境進行個性化配置,有三種方式:

  1. IWebHostEnvironment注入 Startup 類
  2. Startup 方法約定
  3. Startup 類約定

1.將IWebHostEnvironment注入 Startup 類

通過將IWebHostEnvironment注入 Startup 類,然後在方法中使用條件判斷書寫不同環境下的程式碼。該方式適用於多環境下,程式碼差異較少的情況。

  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
  4. {
  5. Configuration = configuration;
  6. WebHostEnvironment = webHostEnvironment;
  7. }
  8. public IConfiguration Configuration { get; }
  9. public IWebHostEnvironment WebHostEnvironment { get; }
  10. public void ConfigureServices(IServiceCollection services)
  11. {
  12. if (WebHostEnvironment.IsDevelopment())
  13. {
  14. Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");
  15. }
  16. else if (WebHostEnvironment.IsTest())
  17. {
  18. Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");
  19. }
  20. else if (WebHostEnvironment.IsStaging())
  21. {
  22. Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");
  23. }
  24. else if (WebHostEnvironment.IsProduction())
  25. {
  26. Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");
  27. }
  28. }
  29. public void Configure(IApplicationBuilder app)
  30. {
  31. if (WebHostEnvironment.IsDevelopment())
  32. {
  33. Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");
  34. }
  35. else if (WebHostEnvironment.IsTest())
  36. {
  37. Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");
  38. }
  39. else if (WebHostEnvironment.IsStaging())
  40. {
  41. Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");
  42. }
  43. else if (WebHostEnvironment.IsProduction())
  44. {
  45. Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");
  46. }
  47. }
  48. }
  49. public static class AppHostEnvironmentEnvExtensions
  50. {
  51. public static bool IsTest(this IHostEnvironment hostEnvironment)
  52. {
  53. if (hostEnvironment == null)
  54. {
  55. throw new ArgumentNullException(nameof(hostEnvironment));
  56. }
  57. return hostEnvironment.IsEnvironment(AppEnvironments.Test);
  58. }
  59. }
  60. public static class AppEnvironments
  61. {
  62. public static readonly string Test = nameof(Test);
  63. }

2.Startup 方法約定

上面的方式把不同環境的程式碼放在了同一個方法中,看起來比較混亂也不容易區分。因此我們希望ConfigureServicesConfigure能夠根據不同的環境進行程式碼拆分。

我們可以通過方法命名約定來解決,約定Configure{EnvironmentName}ServicesConfigure{EnvironmentName}Services來裝載不同環境的程式碼。如果當前環境沒有對應的方法,則使用原來的ConfigureServicesConfigure方法。

我就只拿 Development 和 Production 舉例了

  1. public class Startup
  2. {
  3. // 我這裡注入 IWebHostEnvironment,僅僅是為了打印出來當前環境資訊
  4. public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
  5. {
  6. Configuration = configuration;
  7. WebHostEnvironment = webHostEnvironment;
  8. }
  9. public IConfiguration Configuration { get; }
  10. public IWebHostEnvironment WebHostEnvironment { get; }
  11. #region ConfigureServices
  12. private void StartupConfigureServices(IServiceCollection services)
  13. {
  14. Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");
  15. }
  16. public void ConfigureDevelopmentServices(IServiceCollection services)
  17. {
  18. StartupConfigureServices(services);
  19. }
  20. public void ConfigureProductionServices(IServiceCollection services)
  21. {
  22. StartupConfigureServices(services);
  23. }
  24. public void ConfigureServices(IServiceCollection services)
  25. {
  26. StartupConfigureServices(services);
  27. }
  28. #endregion
  29. #region Configure
  30. private void StartupConfigure(IApplicationBuilder app)
  31. {
  32. Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");
  33. }
  34. public void ConfigureDevelopment(IApplicationBuilder app)
  35. {
  36. StartupConfigure(app);
  37. }
  38. public void ConfigureProduction(IApplicationBuilder app)
  39. {
  40. StartupConfigure(app);
  41. }
  42. public void Configure(IApplicationBuilder app)
  43. {
  44. StartupConfigure(app);
  45. }
  46. #endregion
  47. }

3.Startup 類約定

該方式適用於多環境下,程式碼差異較大的情況。

程式啟動時,會優先尋找當前環境命名符合Startup{EnvironmentName}的 Startup 類,如果找不到,則使用名稱為Startup的類

首先,CreateHostBuilder方法需要做一處修改

  1. public static IHostBuilder CreateHostBuilder(string[] args) =>
  2. Host.CreateDefaultBuilder(args)
  3. .ConfigureWebHostDefaults(webBuilder =>
  4. {
  5. //webBuilder.UseStartup<Startup>();
  6. webBuilder.UseStartup(typeof(Startup).GetTypeInfo().Assembly.FullName);
  7. });

接下來,就是為各個環境定義 Startup 類了(我就只拿 Development 和 Production 舉例了)

  1. public class StartupDevelopment
  2. {
  3. // 我這裡注入 IWebHostEnvironment,僅僅是為了打印出來當前環境資訊
  4. public StartupDevelopment(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
  5. {
  6. Configuration = configuration;
  7. WebHostEnvironment = webHostEnvironment;
  8. }
  9. public IConfiguration Configuration { get; }
  10. public IWebHostEnvironment WebHostEnvironment { get; }
  11. public void ConfigureServices(IServiceCollection services)
  12. {
  13. Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");
  14. }
  15. public void Configure(IApplicationBuilder app)
  16. {
  17. Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");
  18. }
  19. }
  20. public class StartupProduction
  21. {
  22. public StartupProduction(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
  23. {
  24. Configuration = configuration;
  25. WebHostEnvironment = webHostEnvironment;
  26. }
  27. public IConfiguration Configuration { get; }
  28. public IWebHostEnvironment WebHostEnvironment { get; }
  29. public void ConfigureServices(IServiceCollection services)
  30. {
  31. Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");
  32. }
  33. public void Configure(IApplicationBuilder app)
  34. {
  35. Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");
  36. }
  37. }