由於ASP.NET Core框架在本質上就是由服務器和中間件構建的消息處理管道,所以在它上面構建的應用開發框架都是建立在某種類型的中間件上,整個ASP.NET Core MVC開發框架就是建立在用來實現路由的EndpointRoutingMiddleware和EndpointMiddleware中間件上。ASP.NET Core MVC利用路由系統為它分發請求,並在此基礎上實現針對目標Controller的激活、Action方法的選擇和執行,以及最終對於執行結果的響應。在介紹的實例演示中,我們將對上面創建的ASP.NET Core作進一步改造,使之轉變成一個MVC應用。
一、注冊服務與中間件
ASP.NET Core框架內置了一個原生的依賴注入框架,該框架利用一個依賴注入容器提供管道在構建以及請求處理過程中所需的服務,而這些服務需要在應用啟動的時候被預先注冊。對於ASP.NET Core MVC框架來說,它在處理HTTP請求的過程中所需的一系列服務同樣需要預先注冊。對這個概念有了基本的了解之后,相信讀者朋友們對如下所示的代碼就容易理解了。
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace helloworld { class Program { static void Main() { Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder .ConfigureServices(servicecs => servicecs .AddRouting() .AddControllersWithViews()) .Configure(app => app .UseRouting() .UseEndpoints(endpoints => endpoints.MapControllers()))) .Build() .Run(); } } }
整個ASP.NET MVC框架建立在EndpointRoutingMiddleware和EndpointMiddleware中間件構建的路由系統上,這兩個中間件采用“終結點(Endpoint)映射”的方式實現針對HTTP請求的路由。這里所謂的終結點可以視為應用程序提供的針對HTTP請求的處理器,這兩個終結點通過預先設置的規則將具有某些特征的請求(比如路徑、HTTP方法等)映射到對應的終結點,進而實現路由的功能。對於一個MVC應用程序來說,我們可以將定義在Controller類型中的Action方法視為一個終結點,那么路由映射最終體現在HTTP請求與目標Action方法的映射上。
如上面的代碼片段所示,我們先后調用了IApplicationBuilder接口的UseRouting和UseEndpoints擴展方法注冊了EndpointRoutingMiddleware和EndpointMiddleware中間件。在調用UseEndpoints方法的時候,我們利用指定的Action<IEndpointRouteBuilder>委托對象調用了IEndpointRouteBuilder接口的MapControllers擴展方法完成了針對定義在Controller類型中所有Action方法的映射。
由於注冊的中間件具有對其他服務的依賴,我們需要預先將這些服務注冊到依賴注入框架中。依賴服務的注冊通過調用IWebHostBuilder的ConfigureServices方法來完成,該方法的參數類型為Action<IServiceCollection>,添加的服務注冊就保存在IServiceCollection接口表示的集合中。在上面的演示程序中,兩個中間件依賴的服務是通過調用IServiceCollection接口的AddRouting和AddControllersWithViews方法進行注冊的。
如下所示的HelloController是我們定義的Controller類型。按照約定,所有的Controller類型名稱都應該以“Controller”字符作為后綴。與之前版本的ASP.NET MVC不同,ASP.NET Core MVC下的Controller類型並不要求強制繼承某個基類。我們在HelloController中定義了一個唯一的Action方法SayHello,該方法直接返回一個內容為“Hello World”的字符串。
public class HelloController { [HttpGet("/hello")] public string SayHello() => "Hello World."; }
我們在Action方法SayHello上通過標注的HttpGetAttribute特性注冊了一個模板為“/hello”的路由,意味着請求地址為“/hello”的GET請求最終會被路由到這個Action方法上,而該方法執行的結果將作為請求的響應內容。所以啟動該程序后使用瀏覽器訪問地址“http://localhost:5000/hello”,我們依然會得到如下圖所示的輸出結果。
二、引入視圖
上面這個程序並沒有涉及視圖,所以算不上一個典型的MVC應用,接下來我們對它做進一步改造。為了讓HelloController具有視圖呈現的能力,我們讓它派生於基類Controller。Action方法SayHello的返回類型被修改為IActionResult接口,它表示Action方法執行的結果。我們為該方法定義了一個表示姓名的參數name,通過HttpGetAttribute特性注冊的路由模板(“/hello/{name}”)中具有與之對應的路由參數。換句話說,滿足該路徑模式的請求URL攜帶的姓名將自動綁定到該Action方法的name參數上。在SayHello方法中,我們利用ViewBag將代表姓名的name參數值傳遞給呈現的視圖,該方法最終調用View方法返回當前Action方法對應的ViewResult對象。
public class HelloController : Controller { [HttpGet("/hello/{name}")] public IActionResult SayHello(string name) { ViewBag.Name = name; return View(); } }
由於我們調用View方法時沒有顯式指定視圖的名稱,所以視圖引擎會將當前Action的名稱(“SayHello”)作為視圖的名稱。如果該視圖還沒有經過編譯(部署時針對View的預編譯,或者在這之前針對該View的動態編譯),視圖引擎將從若干候選的路徑中讀取對應的.cshtml 文件進行編譯,其中首選的路徑為“{ContentRoot}\Views\{ControllerName}\{ViewName}.cshtml”。為了迎合視圖引擎定位視圖文件的規則,我們需要將SayHello對應的視圖文件(SayHello.cshtml)定義在目錄“\Views\Hello\”下。
如下所示的就是SayHello.cshtml這個文件的內容,這是一個針對Razor引擎的視圖文件。從文件的擴展名(.cshtml)我們看出可以這樣的文件可以同時包含HTML標簽和C#代碼。總的來說,視圖文件會在服務端生成最終在瀏覽器呈現出來的HTML,我們可以在這個文件中直接提供原樣輸出的HTML標簽,也可以內嵌一段動態執行的C#代碼。雖然Razor引擎對View文件的編寫制定了嚴格的語法,但是我個人覺得沒有必要在Razor語法上花太多的精力,因為Razor語法的目的就是讓我們很“自然”地將動態C#代碼和靜態HTML標簽結合起來,並最終生成一份完整的HTML文檔,因此它的語法和普通的思維基本是一致。比如下面這個View最終會生成一個完整的HTML文檔,其主體部分只有一個<p>標簽。該標簽的內容是動態的,因為包含利用ViewBag從Controller傳進來的姓名。
<html> <head> <title>Hello World</title> </head> <body> <p>Hello, @ViewBag.Name</p> </body> </html>
再次運行該程序后,我們利用瀏覽器訪問地址“http://localhost:5000/hello/foobar”。由於請求地址與Action方法SayHello上的路由規則相匹配,所以路徑攜帶的姓名(foobar)會綁定到該方法的name參數上,所以我們最終將在瀏覽器上得到如下圖所示的輸出結果。
三、使用Startup類型
任何一個ASP.NET Core應用在初始化的時候都會根據請求處理的需求注冊對應的中間件。在前面演示的實例中,我們都是直接調用IWebHostBuilder的Configure擴展方法來注冊所需的中間件,但是在大部分真實的開發場景中我們一般會將中間件以及依賴服務的注冊定義在一個單獨的類型中。按照約定,我們通常會將這個類型命名為Startup,比如我們演示實例中針對服務和中間件的注冊就可以放在如下定義的這個Startup類中。
public class Startup { public void ConfigureServices(IServiceCollection services) => services .AddRouting() .AddControllersWithViews(); public void Configure(IApplicationBuilder app) => app .UseRouting() .UseEndpoints(endpoints => endpoints.MapControllers()); }
如上面的代碼片段所示,我們不需要讓Startup類實現某個預定義的接口或者繼承某個預定義基類,所采用的完全是一種基於“約定”的定義方式。隨着對ASP.NET Core框架認識的加深,我們會發現這種“約定優於配置”的設計廣泛地應用在整個框架之中。按照約定,服務注冊和中間件注冊分別實現在ConfigureServices和Configure方法中,它們的第一個參數類型分別為IServiceCollection和IApplicationBuilder接口。由於已經將兩種核心的操作轉移到了Startup類型中,所以我們需要注冊該類型。Startup類型可以調用IWebHostBuilder接口的UseStartup<TStartup>擴展方法進行注冊。
class Program { static void Main() { Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.UseStartup<Startup>()) .Build() .Run(); } }
我們在前面的內容中對.NET Core、ASP.NET Core以及ASP.NET Core MVC應用的編程作了初步的體驗,但是這僅僅限於我們熟悉的Windows平台。作為一個號稱跨平台的開發框架,我們有必要在其他操作系統平台上體驗一下.NET Core開發的樂趣。
[ASP.NET Core 3框架揭秘] 跨平台開發體驗: Windows [上篇]
[ASP.NET Core 3框架揭秘] 跨平台開發體驗: Windows [中篇]
[ASP.NET Core 3框架揭秘] 跨平台開發體驗: Windows [下篇]
[ASP.NET Core 3框架揭秘] 跨平台開發體驗: Mac OS
[ASP.NET Core 3框架揭秘] 跨平台開發體驗: Linux
[ASP.NET Core 3框架揭秘] 跨平台開發體驗: Docker