ASP.NET Core應用本質上就是一個由中間件構成的管道,承載系統將應用承載于一個托管進程中運行起來,其核心任務就是將這個管道構建起來。在ASP.NET Core的發展歷史上先后出現了三種應用承載的編程方式,而且后一種編程模式都提供了針對之前編程模式的全部或者部分兼容,這就導致了一種現象:相同的功能具有N種實現方式。對這個發展歷程不是特別了解的讀者會有很多疑問?為什么這么多不同的編程模式都在做同一件事?它們之間的有什么差別之處?為什么有的API在最新的Minimal API又不能用了呢?[本文部分內容來源于《ASP.NET Core 6框架揭秘》第15章]
目錄
一、應用承載過程中需要哪些初始化工作?
二、第一代應用承載模型
???? 基本編程模式
???? 利用環境變量和命令行參數
??? 承載環境設置方法
??? 使用Startup類型
三、第二代應用承載模型
???? 基本編程模式
???? 承載環境設置方法
???? 針對IWebHostBuilder的適配
??? Startup構造函數注入的限制
一、應用承載過程中需要哪些初始化工作?
我們所謂的應用承載(Hosting)本就是將一個ASP.NET Core應用在一個具體的進程(Self-Host進程、IIS工作進程或者Windows Service進程等)中被啟動的過程,在這個過程中需要利用提供的API完成一些必要的初始化工作。由于ASP.NET Core應用本質上就是一個由中間件構成的管道,所有整個初始化過程的目的就是為了構建這一中間件管道,毫不夸張地說,構建的中間件管道就是“應用”本身,所以“中間件注冊”是最為核心的初始化工作。由于依賴注入的廣泛應用,中間件的功能基本都依賴于注入的服務來完成,所以將依賴服務注冊到依賴注入框架是另一項核心的初始化工作。
和任何類型的應用一樣,ASP.NET Core同樣需要通過配置來動態改變其運行時行為,所以針對配置的設置也是并不可少的。一個ASP.NET Core應用的配置分為兩類,一種是用在中間件管道構建過程中,也就是應用承載過程中,我們將其稱為“承載配置(Hosting Configuration)”。另一類配置則被用來控制中間件管道處理請求的行為,正如上面所說,中間件管道就是應用本身,所以這類配置被稱為應用配置(App Configuration)。承載配置中有一個重要的組成部分,那就是描述當前的承載環境(Hosting Environment),比如應用的標識、部署環境的名稱、存放內容文件和Web資源的目錄等。承載配置最終會合并到應用配置中。
綜上所示,ASP.NET Core應用承載的編程模型主要完成如下幾種初始化工作,這些工作都具有N種實現方法。在接下來的內容中,我們將逐個介紹在三種不同的應用承載方式中,這些功能都有哪些實現方式。
中間件注冊
服務注冊
承載配置的設置
應用配置的設置
承載環境的設置
二、第一代應用承載模型
ASP.NET Core 1.X/2.X采用的承載模型以如下圖所示的IWebHostBuilder和IWebHost為核心。IWebHost對象代表承載Web應用的宿主(Host),管道隨著IWebHost對象的啟動被構建出來。IWebHostBuilder對象作為宿主對象的構建者,我們針對管道構建的設置都應用在它上面。
基本編程模式
現在我們將針對上述5種初始化設置放在一個簡單的演示實例中。該演示實例會注冊如下這個FoobarMiddleware中間件,后者利用注入的IHandler服務完成請求的處理工作。作為IHandler接口的默認實現類型,Handler利用構造函數注入的IOptions<FoobarbazOptions>對象得到配置選項FoobarbazOptions,并將其內容作為請求的響應。
public?class?FoobarMiddleware
{private?readonly?RequestDelegate?_next;public?FoobarMiddleware(RequestDelegate?_)?{?}public?Task?InvokeAsync(HttpContext?httpContext,?IHandler?handler)?=>?handler.InvokeAsync(httpContext);
}public?interface?IHandler
{Task?InvokeAsync(HttpContext?httpContext);
}public?class?Handler?:?IHandler
{private?readonly?FoobarbazOptions?_options;private?readonly?IWebHostEnvironment?_environment;public?Handler(IOptions<FoobarbazOptions>?optionsAccessor,?IWebHostEnvironment?environment){_options?=?optionsAccessor.Value;_environment?=?environment;}public?Task?InvokeAsync(HttpContext?httpContext){var?payload?=?@$"
Environment.ApplicationName:?{_environment.ApplicationName}
Environment.EnvironmentName:?{_environment.EnvironmentName}
Environment.ContentRootPath:?{_environment.ContentRootPath}
Environment.WebRootPath:?{_environment.WebRootPath}
Foo:?{_options.Foo}
Bar:?{_options.Bar}
Baz:?{_options.Baz}
";return?httpContext.Response.WriteAsync(payload);}
}public?class?FoobarbazOptions
{public?string?Foo?{?get;?set;?}?=?default!;public?string?Bar?{?get;?set;?}?=?default!;public?string?Baz?{?get;?set;?}?=?default!;
}
我們會利用與當前“承載環境”對應配置來綁定配置選項FoobarbazOptions,后者的三個屬性分別來源于三個獨立的配置文件。其中settings.json被所有環境共享,settings.dev.json針對名為“dev”的開發環境。我們為承載環境提供更高的要求,在環境基礎上進步劃分子環境,settings.dev.dev1.json針對的就是dev下的子環境dev1。針對子環境的設置需要利用上述的承載配置來提供。
如下所示的就是上述三個配置文件的內容。如果當前環境和子環境分別為dev和dev1,那么配置選項FoobarbazOptions的內容將來源于這三個配置文件。細心的朋友可能還注意到了:我們并沒有放在默認的根目錄下,而是放在創建的resources目錄下,這是因為我們需要利用針對承載環境的設置改變ASP.NET Core應用存放內容文件和Web資源文件的根目錄。
settings.json
{"Foo":?"123"
}settings.dev.json
{"Bar":?"abc"
}settings.dev.dev1.json
{"Baz":?"xyz"
}
如下的應用承載程序涵蓋了上述的5種初始化操作。中間件的注冊通過調用IWebHostBuilder的Configure方法來完成,該方法的參數類型為Action<IApplicationBuilder>,中間件就是通過調用UseMiddleware<TMiddleware>方法注冊到IApplicationBuilder對象上。IWebHostBuilder并未對承載配置定義專門的方法,但是我們可以利用UseSettings方法以鍵值對的形式對其進行設置,這里我們采用這種方式完成了針對“環境”、“內容文件根目錄”、“Web資源文件根目錄”和“子環境”的設置,前三個是“承載環境”的三個重要屬性。承載配置最終會體現到表示承載上下文的WebHostBuilderContext對象上。
using?App;
new?WebHostBuilder().UseKestrel().UseSetting(WebHostDefaults.EnvironmentKey,"dev").UseSetting(WebHostDefaults.ContentRootKey?,?Path.Combine(Directory.GetCurrentDirectory(),?"resources")).UseSetting(WebHostDefaults.WebRootKey,?Path.Combine(Directory.GetCurrentDirectory(),?"resources",?"web")).UseSetting("SubEnvironment",?"dev1").ConfigureAppConfiguration((context,?configBuilder)?=>?configBuilder.AddJsonFile(path:?"settings.json",?optional:?false).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.json",?optional:?true).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.{context.Configuration["SubEnvironment"]}.json",?optional:?true)).ConfigureServices((context,?services)?=>?services.AddSingleton<IHandler,?Handler>().Configure<FoobarbazOptions>(context.Configuration)).Configure(app?=>?app.UseMiddleware<FoobarMiddleware>()).Build().Run();
依賴服務利用IWebHostBuilder的ConfigureServices方法進行注冊,該方法的參數類型為Action<WebHostBuilderContext, IServiceCollection>,意味著我們可以針對之前提供的承載配置(比如承載環境)進行針對性的服務注冊。在這里我們不僅注冊了依賴服務Handler,還利用當前配置對配置選項FoobarbazOptions實施了綁定。應用配置通過專門的方法ConfigureAppConfiguration進行設置,該方法的參數類型為Action<WebHostBuilderContext, IConfigurationBuilder>,意味著承載配置依然可以利用WebHostBuilderContext上下文獲取到,這里我們這是利用它得到對當前環境匹配的三個配置文件。程序啟動后,請求可以得到如下的響應內容。
利用環境變量和命令行參數
由于ASP.NET Core應用在啟動時會使用前綴為“ASPNETCORE_”的環境變量作為承載配置,所以上述利用UseSettings方法針對承載配置的設置都可以按照如下的方式利用環境變量代替。
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT",?"dev");
Environment.SetEnvironmentVariable("ASPNETCORE_SUBENVIRONMENT",?"dev1");
Environment.SetEnvironmentVariable("ASPNETCORE_CONTENTROOT",?Path.Combine(Directory.GetCurrentDirectory(),?"resources"));
Environment.SetEnvironmentVariable("ASPNETCORE_WEBROOT",?Path.Combine(Directory.GetCurrentDirectory(),?"resources",?"web"));WebHost.CreateDefaultBuilder(args).ConfigureAppConfiguration((context,?configBuilder)?=>?configBuilder.AddJsonFile(path:?"settings.json",?optional:?false).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.json",?optional:?true).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.{context.Configuration["SubEnvironment"]}.json",?optional:?true)).ConfigureServices((context,?services)?=>?services.AddSingleton<IHandler,?Handler>().Configure<FoobarbazOptions>(context.Configuration)).Configure(app?=>?app.UseMiddleware<FoobarMiddleware>()).Build().Run();
上面的代碼片段并沒有直接創建WebHostBuilder對象,而是調用WebHost的靜態方法CreateDefaultBuilder方法創建了一個具有默認配置的IWebHostBuilder對象。由于該方法傳入了命令行參數args,它會將命令行參數作為承載配置源之一,所以程序中四個針對承載配置選項也可以利用命令行參數來完成。
承載環境設置方法
其實承載環境(環境名稱、內容文件根目錄和Web資源文件根目錄)具有專門的方法,所以最方便的還是直接按照如下的方式調用這些方法對它們進行設置。對于我們演示的實例來說,針對環境名稱、內容文件和Web資源文件根目錄的設置可以直接調用IWebHostBuilder的UseEnvironment、UseContentRoot和UseWebRoot擴展方法來完成。
WebHost.CreateDefaultBuilder(args).UseEnvironment("dev").UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(),?"resources")).UseWebRoot(Path.Combine(Directory.GetCurrentDirectory(),?"resources",?"web")).UseSetting("SubEnvironment",?"dev1").ConfigureAppConfiguration((context,?configBuilder)?=>?configBuilder.AddJsonFile(path:?"settings.json",?optional:?false).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.json",?optional:?true).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.{context.Configuration["SubEnvironment"]}.json",?optional:?true)).ConfigureServices((context,?services)?=>?services.AddSingleton<IHandler,?Handler>().Configure<FoobarbazOptions>(context.Configuration)).Configure(app?=>?app.UseMiddleware<FoobarMiddleware>()).Build().Run();
使用Startup類型
為了不讓應用承載程序代碼顯得過于臃腫,我們一般都會將服務注冊和中間件注冊移到按照約定定義的Startup類型中。如下面的代碼片段所示,中間件和服務注冊分別實現在Startup類型的ConfigureServices和Configure方法中,我們直接在構造函數中注入IConfiguration對象得到承載配置對象。值得一提,對于第一代應用承載方式,我們可以在Startup類型的構造函數中注入通過調用IWebHostBuilder的ConfigureServices方法注冊的任何服務(包括ASP.NET Core內部通過調用這個方法注冊的服務,比如本例的IConfiguration對象)。Startup類型只需要調用IWebHostBuilder的UseStartup<TStartup>擴展方法進行注冊即可。
WebHost.CreateDefaultBuilder(args).UseEnvironment("dev").UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(),?"resources")).UseWebRoot(Path.Combine(Directory.GetCurrentDirectory(),?"resources",?"web")).UseSetting("SubEnvironment",?"dev1").ConfigureAppConfiguration((context,?configBuilder)?=>?configBuilder.AddJsonFile(path:?"settings.json",?optional:?false).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.json",?optional:?true).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.{context.Configuration["SubEnvironment"]}.json",?optional:?true)).UseStartup<Startup>().Build().Run();public?class?Startup
{public?IConfiguration?Configuration?{?get;?}public?Startup(IConfiguration?configuration)?=>?Configuration?=?configuration;public?void?ConfigureServices(IServiceCollection?services)?=>?services.AddSingleton<IHandler,?Handler>().Configure<FoobarbazOptions>(Configuration);public?void?Configure(IApplicationBuilder?app)?=>?app.UseMiddleware<FoobarMiddleware>();
}
三、第二代應用承載模型
除了承載Web應用,我們還有很多針對后臺服務(比如很多批處理任務)的承載需求,為此微軟推出了以IHostBuilder/IHost為核心的服務承載系統。Web應用本身實際上就是一個長時間運行的后臺服務,我們完全可以將應用定義成一個IHostedService服務,該類型就是下圖所示的GenericWebHostService。如果將上面介紹的稱為第一代應用承載模式的話,這就是第二代承載模式。
基本編程模式
和所有的Builder模式一樣,絕大部分API都落在作為構建者的IHostBuilder接口上,服務注冊、承載配置、應用配置都具有對應的方法。由于中間件隸屬于GenericWebHostService這一單一的承載服務,所以只能記住與IWebHostBuilder。如果采用第二代應用承載模型,上面演示的程序可以改寫成如下的形式。
Host.CreateDefaultBuilder().ConfigureHostConfiguration(config?=>?config.AddInMemoryCollection(new?Dictionary<string,?string>?{[WebHostDefaults.EnvironmentKey]?=?"dev",[WebHostDefaults.ContentRootKey]?=?Path.Combine(Directory.GetCurrentDirectory(),?"resources"),[WebHostDefaults.WebRootKey]?=?Path.Combine(Directory.GetCurrentDirectory(),?"resources","web"),["SubEnvironment"]?=?"dev1"})).ConfigureAppConfiguration((context,?configBuilder)?=>?configBuilder.AddJsonFile(path:?"settings.json",?optional:?false).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.json",?optional:?true).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.{context.Configuration["SubEnvironment"]}.json",?optional:?true)).ConfigureServices((context,services)?=>?services.AddSingleton<IHandler,?Handler>().Configure<FoobarbazOptions>(context.Configuration)).ConfigureWebHost(webHostBuilder?=>?webHostBuilder.Configure(app=>app.UseMiddleware<FoobarMiddleware>())).Build().Run();
如上面的代碼片段所示,我們通過調用Host的靜態方法CreateDefaultBuilder方法創建一個具有默認配置的IHostBuidler對象。IHostBuilder為承載配置的設置提供了獨立的ConfigureHostConfiguration方法,該方法的參數類型為Action<IConfigurationBuilder>,我們演示的例子利用這個方法注冊了一個基于內存字典的配置源,承載環境(環境名稱、內容文件和Web資源文件根目錄)和子環境名稱在這里進行了設置。針對應用配置的設置通過ConfigureAppConfiguration方法來完成,該方法的參數類型為Action<HostBuilderContext, IConfigurationBuilder>,代表承載上下文的HostBuilderContext可以得到預先設定的承載環境和承載配置,我們的例子利用到定位與當前環境相匹配的配置文件。
IHostBuilder同樣定義了ConfigureServices方法,該方法的參數類型為Action<HostBuilderContext, IServiceCollection>,意味著服務依然可以針對承載環境和承載配置進行注冊。由于中間件的注冊依然落在IWebHostBuilder上,所以IHostBuilder提供了ConfigureWebHost/ConfigureWebHostDefaults這兩個擴展方法予以適配,它們具有一個類型為Action<IWebHostBuilder>的參數。
承載環境設置方法
和IWebHostBuilder一樣,IHostBuidler同樣提供了用來直接設置承載環境的方法。對于我們演示的實例來說,針對環境名稱、內容文件和Web資源文件根目錄的設置可以直接調用IHostBuidler的UseEnvironment、UseContentRoot和UseWebRoot擴展方法來完成。由于Web資源文件并未“服務承載”的范疇,所以針對Web資源文件根目錄的設置還得采用直接設置承載配置的方式(或者調用IWebHostBuilder的UseWebRoot擴展方法)。
Host.CreateDefaultBuilder().UseEnvironment("dev").UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(),?"resources")).ConfigureHostConfiguration(config?=>?config.AddInMemoryCollection(new?Dictionary<string,?string>?{[WebHostDefaults.WebRootKey]?=?Path.Combine(Directory.GetCurrentDirectory(),?"resources","web"),["SubEnvironment"]?=?"dev1"})).ConfigureAppConfiguration((context,?configBuilder)?=>?configBuilder.AddJsonFile(path:?"settings.json",?optional:?false).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.json",?optional:?true).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.{context.Configuration["SubEnvironment"]}.json",?optional:?true)).ConfigureServices((context,services)?=>?services.AddSingleton<IHandler,?Handler>().Configure<FoobarbazOptions>(context.Configuration)).ConfigureWebHost(webHostBuilder?=>?webHostBuilder.Configure(app=>app.UseMiddleware<FoobarMiddleware>())).Build().Run();
針對IWebHostBuilder的適配
由于IHostBuilder利用擴展方法ConfigureWebHost/ConfigureWebHostDefaults提供了針對IWebHostBuilder的適配,意味著前面采用第一代應用承載方法編寫的代碼可以直接移植過來。如下面的代碼片段所示,靜態方法ConfigureWebHost完全依然利用IWebHostBuilder完成所有的初始化工作,我們只需要將指向該方法的Action<IWebHostBuilder>委托傳入IHostBuilder的ConfigureWebHostDefaults擴展方法就可以了。
Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(ConfigureWebHost).Build().Run();static?void?ConfigureWebHost(IWebHostBuilder?webHostBuilder)
{webHostBuilder.UseEnvironment("dev").UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(),?"resources")).UseWebRoot(Path.Combine(Directory.GetCurrentDirectory(),?"resources",?"web")).UseSetting("SubEnvironment",?"dev1").ConfigureAppConfiguration((context,?configBuilder)?=>?configBuilder.AddJsonFile(path:?"settings.json",?optional:?false).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.json",?optional:?true).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.{context.Configuration["SubEnvironment"]}.json",?optional:?true)).UseStartup<Startup>();
}public?class?Startup
{public?IConfiguration?Configuration?{?get;?}public?Startup(IConfiguration?configuration)?=>?Configuration?=?configuration;public?void?ConfigureServices(IServiceCollection?services)?=>?services.AddSingleton<IHandler,?Handler>().Configure<FoobarbazOptions>(Configuration);public?void?Configure(IApplicationBuilder?app)?=>?app.UseMiddleware<FoobarMiddleware>();
}
Startup構造函數注入的限制
第二代應用承載模型利用ConfigureWebHost/ConfigureWebHostDefaults擴展方法對之前定義在IWebHostBuilder上的API(絕大部分是擴展方法)提供了100%的支持(除了Build方法),但是針對Startup構造函數中注入的服務則不再那么自由。如果采用基于IWebHostBuilder/IWebHost的應用承載方式,通過調用IWebHostBuilder的ConfigureServices方法注冊的服務都可以注入Startup的構造函數中,如果采用基于IHostBuilder/IHost的應用承載方式,只有與“承載配置(承載環境屬于承載配置的一部分)”相關的如下三個服務能夠注入到Startup的構造函數中。
IHostingEnvironment
IWebHostEnvironment
IHostEnvironment
IConfiguration
對于如下這段代碼,雖然注入Startup構造函數的Foobar同時通過調用IHostBuilder和IWebHostBuilder的ConfigureServices方法中進行了注冊,但是在創建Startup實例的時候依然會拋出異常。
Host.CreateDefaultBuilder(args).ConfigureServices(sevices=>sevices.AddSingleton<Foobar>()).ConfigureWebHostDefaults(ConfigureWebHost).Build().Run();static?void?ConfigureWebHost(IWebHostBuilder?webHostBuilder)
{webHostBuilder.UseEnvironment("dev").ConfigureServices(sevices?=>?sevices.AddSingleton<Foobar>()).UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(),?"resources")).UseWebRoot(Path.Combine(Directory.GetCurrentDirectory(),?"resources",?"web")).UseSetting("SubEnvironment",?"dev1").ConfigureAppConfiguration((context,?configBuilder)?=>?configBuilder.AddJsonFile(path:?"settings.json",?optional:?false).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.json",?optional:?true).AddJsonFile(path:?$"settings.{context.HostingEnvironment.EnvironmentName}.{context.Configuration["SubEnvironment"]}.json",?optional:?true)).UseStartup<Startup>();
}public?class?Startup
{public?IConfiguration?Configuration?{?get;?}public?Startup(IConfiguration?configuration,Foobar?foobar)?=>?Configuration?=?configuration;public?void?ConfigureServices(IServiceCollection?services)?=>?services.AddSingleton<IHandler,?Handler>().Configure<FoobarbazOptions>(Configuration);public?void?Configure(IApplicationBuilder?app)?=>?app.UseMiddleware<FoobarMiddleware>();
}public?class?Foobar
{?}
綜上所述,最初版本的ASP.NET Core由于只考慮到Web應用自身的承載,所以設計出了基于IWebHostBuilder/IWebHost模型。后來產生了基于后臺服務承載的需求,所以推出了基于IHostBuilder/IHost的服務承載模型,原本的Web應用作為一個“后臺服務(GenericWebHostService.)”進行承載。由于之前很多API都落在IWebHostBuilder(主要無數的擴展方法),出于兼容性的需求,一個名為GenericWebHostBuilder的實現類型被定義出來,它將針對IWebHostBuilder的方法調用轉移到IHostBuilder/IHost的服務承載模型中。
.NET 6在IHostBuilder/IHost服務承載模型基礎上推出了更加簡潔的Minimal API,此時又面臨相同的“抉擇”。這次它不僅需要兼容IWebHostBuilder,還得兼容IHostBuilder,再加上Minimal API自身提供的API,所以“一題多解”的現象就更多了。如果你對ASP.NET Core的歷史不甚了解,將會感到非常困惑。令你們更加感到困惑的時,此時定義在IWebHostBuilder和IHostBuilder的API并非全部可用,本文的下篇將為你一一解惑。