1. 程式人生 > >基於ASP.NET core的MVC站點開發筆記 0x01

基於ASP.NET core的MVC站點開發筆記 0x01

# 基於ASP.NET core的MVC站點開發筆記 0x01 # 我的環境 ``` OS type:mac Software:vscode Dotnet core version:2.0/3.1 ``` > `dotnet sdk`下載地址:[https://dotnet.microsoft.com/download/dotnet-core/2.0](https://dotnet.microsoft.com/download/dotnet-core/2.0) # 準備 先到上面提供的下載地址,下載對應平臺的`dotnet`裝上,然後在命令列視窗輸入`dotnet --version`檢視輸出是否安裝成功。 ![](http://pic.scriptboy.cn/20200505181708.png) 然後,安裝`visual studio code`,安裝之後還需要安裝`C#`拓展,要不然每次開啟`cs`檔案都會報錯。 # 建立專案 新建一個空目錄,例如`mvc-test`。 使用命令`dotnet new`檢視可以新建的專案型別: ![](http://pic.scriptboy.cn/20200505182849.png) 第一次嘗試,使用`ASP.NET Core Empty`就可以,代號是`web`,使用命令`dotnet new web`就可以新建一個空專案,專案的名稱就是當前目錄的名字`mvc-test`。 # 專案結構與預設配置 目錄主要結構和檔案功能如下: ![](http://pic.scriptboy.cn/20200505183651.png) `Program.cs`是程式的主類,`Main`函式在這裡定義,內容大致可以這麼理解: ![](http://pic.scriptboy.cn/20200505185008.png) `CreateDefaultBuilder`函式會使用預設的方法載入配置,例如通過讀取`launchSettings.json`確定當前的釋出環境: ![](http://pic.scriptboy.cn/20200505190135.png) `webhost`通過`ASPNETCORE_ENVIRONMENT`讀取釋出環境,然後會讀取對應的配置檔案,`Development`對應`appsettings.Development.json`,`Production`對應`appsettings.json`。 `appsettings`檔案是整個web應用的配置檔案,如果web應用需要使用某個全域性變數,可以配置到這個檔案裡面去。 `webhost`在執行前會通過`Startup`類,進行一些中介軟體的配置和註冊,以及進行客戶端的響應內容設定: ![](http://pic.scriptboy.cn/20200505191334.png) > 注:`dotnet core 3`版本里,取消了`WebHost`,使用`Host`以更通用的方式進行程式託管。 dotnet core 3 Program.cs ```csharp public static Void Main(string[] args) { Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(builder => { builder.UseStartup(); }).Build().Run(); } ``` # 獲取配置檔案中的值 修改`launingSettings.json`中設定的釋出環境對應的配置檔案,例如`appsetttings.Delelopment.json`內容,新增一個`Welcome`欄位配置項,如下: ```json { "Logging": { "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } }, "Welcome": "Hello from appsettings.json!!" } ``` 修改`Startup.cs`檔案,新增`IConfiguration config`引數,`.net core`內部會將配置檔案內容對映到這個變數: ```csharp /// /// 註冊應用程式所需的服務 ///
public void ConfigureServices(IServiceCollection services) { } /// /// 註冊管道中介軟體 /// public void Configure(IApplicationBuilder app, IHostingEnvironment env, IConfiguration config) { // 開發環境,使用開發者異常介面 if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } var welcome = config["Welcome"]; // Run一般放在管道末尾,執行完畢之後直接終止請求,所以在其後註冊的中介軟體,將不會被執行 app.Run(async (context) =>
{ await context.Response.WriteAsync(welcome); }); } ``` 在終端中使用命令`dotnet run`可以執行這個web應用: ![](http://pic.scriptboy.cn/20200505193607.png) 瀏覽器訪問`http://localhost:5000`,可以看到已經成功獲取到`Welcome`配置項的值: ![](http://pic.scriptboy.cn/20200505193723.png) # 日誌列印 通過`ILogger`實現控制檯日誌的列印: ```csharp public void ConfigureServices(IServiceCollection services) { } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure( IApplicationBuilder app, IHostingEnvironment env, IConfiguration config, ILogger logger) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } var welcome = config["Welcome"]; logger.LogInformation(welcome); app.Run(async (context) => { await context.Response.WriteAsync(welcome); }); } ``` `ILogger`使用的時候需要指定列印日誌的類名`Startup`,最終列印效果如下: ![](http://pic.scriptboy.cn/20200505230701.png) # 服務註冊 上面的`IConfiguration`可以直接使用,是因為`IConfiguration`服務已經自動註冊過了。 對於自定義的服務,可以在`ConfigureServices`中註冊,例如自定義一個服務`WelcomeService`,專案目錄下新建兩個檔案`IWelcomeService.cs`和`WelcomeService.cs`,內容如下: ```csharp /* IWelcomeService.cs * * 該介面類定義了一個getMessage方法。 */ namespace mvc_test { public interface IWelcomeService { string getMessage(); } } ``` ```csharp /* WelcomeService.cs * * 該類實現了getMessage方法。 */ namespace mvc_test { public class WelcomeService : IWelcomeService { int c = 0; public string getMessage() { c++; return "Hello from IWelcomeService Interface!!!" + c.ToString(); } } } ``` 然後在`ConfigureServices`中註冊服務: ```csharp public void ConfigureServices(IServiceCollection services) { services.AddSingleton(); } ``` 然後在`Configure`中使用的時候需要傳參: ```csharp public void Configure( IApplicationBuilder app, IHostingEnvironment env, IConfiguration config, ILogger logger, IWelcomeService welcomeService) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //var welcome = config["Welcome"]; var welcome = welcomeService.getMessage(); logger.LogInformation(welcome); // Run一般放在管道末尾,執行完畢之後直接終止請求,所以在其後註冊的中介軟體,將不會被執行 app.Run(async (context) => { await context.Response.WriteAsync(welcome); }); } ``` 執行後結果: ![](http://pic.scriptboy.cn/20200506010043.png) 這個例子中,註冊服務使用的函式是`AddSingleton`,服務的生命週期除了`Singleton`,還有其他兩個模式:`Scoped`和`Transient`。 > 這三個模式的區別: > - Transient:瞬態模式,服務在每次請求時被建立,它最好被用於輕量級無狀態服務; > - Scoped:作用域模式,服務在每次請求時被建立,整個請求過程中都貫穿使用這個建立的服務。比如Web頁面的一次請求; > - Singleton:單例模式,服務在第一次請求時被建立,其後的每次請求都用這個已建立的服務; > > 參考資料: > - [NetCore2.0依賴注入(DI)之生命週期](https://www.bbsmax.com/A/gGdXboEpJ4/) > - [Service lifetimes](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1#service-lifetimes) 初始學習使用`AddSingleton`就行了。 # 中介軟體和管道 中介軟體是一種用來處理請求和響應的元件,一個web應用可以有多箇中間件,這些中介軟體共同組成一個管道,每次請求訊息進入管道後都會按中介軟體順序處理對應請求資料,然後響應結果原路返回: ![](http://pic.scriptboy.cn/20200506175226.png) > 參考資料: > - [ASP.NET Core 中介軟體](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1) ## 內建中介軟體的使用:處理靜態檔案訪問請求 新建一個目錄`wwwroot`,目錄下新建`index.html`檔案: ```html TEST

Hello from index.html!!!

``` 使用之前的程式碼,`dotnet run`執行之後訪問`http://localhost:5000/index.html`,發現還是之前的結果,並沒有訪問到`index.html`。 這時候需要使用中介軟體`StaticFiles`來處理靜態檔案的請求,修改`Startup.cs`的部分內容如下: ```csharp public void Configure( IApplicationBuilder app, IHostingEnvironment env, IConfiguration config, ILogger logger, IWelcomeService welcomeService) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseStaticFiles(); //var welcome = config["Welcome"]; app.Run(async (context) => { var welcome = welcomeService.getMessage(); logger.LogInformation(welcome); await context.Response.WriteAsync(welcome); }); } ``` 重新啟動後可正常訪問到`index.html`: ![](http://pic.scriptboy.cn/20200506181705.png) 前面講到請求進入管道之後是安裝中介軟體新增順序處理的請求,如果當前中介軟體不能處理,才會交給下一個中介軟體,所以可以嘗試一下將上面的程式碼調整一下順序: ```csharp public void Configure( IApplicationBuilder app, IHostingEnvironment env, IConfiguration config, ILogger logger, IWelcomeService welcomeService) { if (env.IsDevelopment()) {å app.UseDeveloperExceptionPage(); } //var welcome = config["Welcome"]; app.Run(async (context) => { var welcome = welcomeService.getMessage(); logger.LogInformation(welcome); await context.Response.WriteAsync(welcome); }); app.UseStaticFiles(); } ``` 可以看到`StaticFiles`放到了最後,這樣的話因為`index.html`請求會先到`Run`的地方,直接返回了,所以不能進入到`StaticFiles`裡,訪問得到的內容就是: ![](http://pic.scriptboy.cn/20200506182221.png) 通過`StaticFiles`可以成功訪問到`index.html`,但是如果想要`index.html`成為預設網站主頁,需要使用中介軟體`DefaultFiles`,修改上面程式碼為: ```csharp public void Configure( IApplicationBuilder app, IHostingEnvironment env, IConfiguration config, ILogger logger, IWelcomeService welcomeService) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseDefaultFiles(); app.UseStaticFiles(); //var welcome = config["Welcome"]; app.Run(async (context) => { var welcome = welcomeService.getMessage(); logger.LogInformation(welcome); await context.Response.WriteAsync(welcome); }); } ``` `DefaultFiles`內部會自動將`/`修改為`index.html`然後交給其他中介軟體處理,所以需要放在`StaticFiles`的前面。 使用`FileServer`也可以實現同樣的效果: ```csharp public void Configure( IApplicationBuilder app, IHostingEnvironment env, IConfiguration config, ILogger logger, IWelcomeService welcomeService) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseFileServer(); //var welcome = config["Welcome"]; app.Run(async (context) => { var welcome = welcomeService.getMessage(); logger.LogInformation(welcome); await context.Response.WriteAsync(welcome); }); } ``` ## 中介軟體的一般註冊方式 除了使用內建的中介軟體之外,還可以用以下幾種方式註冊中介軟體: - Use - UseWhen - Map - MapWhen - Run `Use`和`UseWhen`註冊的中介軟體在執行完畢之後可以回到原來的管道上; `Map`和`MapWhen`可以在新的管道分支上註冊中介軟體,不能回到原來的管道上; `When`的方法可以通過`context`做更多的中介軟體執行的條件; `Run`用法和`Use`差不多,只不過不需要接收`next`引數,放在管道尾部; 例如實現返回對應路徑內容: ```csharp /// /// 註冊應用程式所需的服務 /// public void ConfigureServices(IServiceCollection service) { } /// /// 註冊管道中介軟體 /// public void Configure(IApplicationBuilder app, IHostEnvironment env) { // 開發環境,新增開發者異常頁面 if(env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // Use 方式 app.Use(async (context, next) => { if(context.Request.Path == new PathString("/use")) { await context.Response.WriteAsync($"Path: {context.Request.Path}"); } await next(); }); // UseWhen 方式 app.UseWhen(context => context.Request.Path == new PathString("/usewhen"), a => a.Use(async (context, next) => { await context.Response.WriteAsync($"Path: {context.Request.Path}"); await next(); })); // Map 方式 app.Map(new PathString("/map"), a => a.Use(async (context, next) => { // context.request.path 獲取不到正確的路徑 //await context.Response.WriteAsync($"Path: {context.Request.Path}"); await context.Response.WriteAsync($"PathBase: {context.Request.PathBase}"); foreach(var item in context.Request.Headers) { await context.Response.WriteAsync($"\n{item.Key}: {item.Value}"); } })); // MapWhen 方式 app.MapWhen(context => context.Request.Path == new PathString("/mapwhen"), a => a.Use(async (context, next) => { await context.Response.WriteAsync($"Path: {context.Request.Path}"); await next(); })); // Run 放在最後,可有可無,主要為了驗證是否可以回到原來的管道上繼續執行 app.Run(async (context)=> { await context.Response.WriteAsync("\nCongratulation, return to the original pipe."); }); } ``` 可以看到只有`/use`和`/usewhen`可以執行到`Run`。 > 注:這裡碰到一個問題,就是訪問`/map`路徑的時候獲取到的`context.Request.Path`為空,其他欄位獲取都挺正常,神奇。不過,可以使用`context.Request.PathBase`獲取到。 ## 自己封裝中介軟體 對於上面註冊中介軟體的幾種方式,比如`Use`內部如果寫太多的程式碼也不合適,所以可以自己封裝中介軟體,封裝完成之後可以像內建中介軟體一樣使用`UseXxxx`的方式註冊。 本例目標要完成一箇中間件可以檢測`HTTP`請求方法,僅接受`GET`、`HEAD`方法,步驟如下: 新建一個資料夾`mymiddleware`,新建檔案`HttpMethodCheckMiddleware.cs`,中介軟體封裝需要實現兩個方法: - `HttpMethodCheckMiddleware`: 建構函式,引數型別為`RequestDelegate`; - `Invoke`: 中介軟體排程函式,引數型別為`HttpContext`,返回型別為`Task`; 檔案內容如下: ```csharp using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace middleware.mymiddleware { /// /// 請求方法檢查中介軟體,僅處理HEAD和GET方法 /// public class HttpMethodCheckMiddleware { private readonly RequestDelegate _next; /// /// 構造方法,必須有的 /// /// 下一個中介軟體 public HttpMethodCheckMiddleware(RequestDelegate requestDelegate) { this._next = requestDelegate; } /// /// 中介軟體排程方法 /// /// HTTP上下文 /// TASK任務狀態 public Task Invoke(HttpContext context) { // 如果符合條件,則將httpcontext傳給下一個中介軟體處理 if(context.Request.Method.ToUpper().Equals(HttpMethods.Head) || context.Request.Method.ToUpper().Equals(HttpMethods.Get)) { return _next(context); } // 否則直接返回處理完成 context.Response.StatusCode = 400; context.Response.Headers.Add("X-AllowedHTTPVerb", new[] {"GET,HEAD"}); context.Response.ContentType = "text/plain;charset=utf-8"; // 防止中文亂碼 context.Response.WriteAsync("只支援GET、HEAD方法"); return Task.CompletedTask; } } } ``` 這樣就可以直接在`Startup`中使用了: ```csharp app.UseMiddleware(); ``` 還可以編寫一個擴充套件類,封裝成類似內建中介軟體的方式`UseXxx`。新建`CustomMiddlewareExtension.cs`檔案,內容如下: ```csharp using Microsoft.AspNetCore.Builder; namespace middleware.mymiddleware { /// /// 封裝中介軟體的擴充套件類 /// public static class CustomMiddlewareExtension { /// /// 新增HttpMethodCheckMiddleware中介軟體的擴充套件方法 /// public static IApplicationBuilder UseHttpMethodCheckMiddleware(this IApplicationBuilder app) { return app.UseMiddleware(); } } } ``` 現在就可以直接呼叫`UseHttpMethodCheckMiddleware`註冊中介軟體了. 執行結果截圖省略。 **疑問**:那個`CustomMiddlewareExtension`也沒見引用,怎麼就可以直接使用`app.UseHttpMethodCheckMiddleware`方法了? 有的可能和我一樣,c#都沒有學明白就直接開始擼dotnet了,看到這一臉懵逼,不過經過一番搜尋,原來這是c#中對已有類或介面進行方法擴充套件的一種方式,參考[C#程式設計指南](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods)。 # 內建路由 > 這一節先當了解,暫時用處不大,學完也會忘掉 先簡單看一下`ASP.NET core`內建的路由方式(直接上startup.cs程式碼內容): ```csharp using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; namespace routing { public class Startup { public void ConfigureServices(IServiceCollection servcies) { } public void Configure(IApplicationBuilder app) { // 新建一個路由處理器 var trackPackageRouteHandler = new RouteHandler(context => { var routeValues = context.GetRouteData().Values; return context.Response.WriteAsync($"Hello! Route values: {string.Join(", ", routeValues)}"); }); var routeBuilder = new RouteBuilder(app, trackPackageRouteHandler); // 通過MapRoute新增路由模板 routeBuilder.MapRoute("Track Package Route", "package/{opration}/{id:int}"); routeBuilder.MapGet("hello/{name}", context => { var name = context.GetRouteValue("name"); return context.Response.WriteAsync($"Hi, {name}!"); }); var routes = routeBuilder.Build(); app.UseRouter(routes); } } } ``` 從程式碼中可知,需要先建立一個路由處理器`trackPackageRouteHandler`,然後通過`RouteBuilder`將`app`和`trackPackageRouteHandler`繫結,而且需要新增一個匹配模板,最後將生成的路由器新增到app中。 其中新增路由匹配模板是使用了不同的方法: - MapRoute: 這個方法設定一個路由模板,匹配成功的請求會路由到`trackPackageRouteHandler`; - MapGet: 這個方法新增的模板,只適用於`GET`請求方式,並且第二個引數可以指定處理請求的邏輯; 上面設定路由的方式過於複雜,所以一般情況下通常使用`MVC`將對應的URL請求路由到`Controller`中處理,簡化路由規則。 # Controller和Action 在開始`MVC`路由之前,先來學習一下`Controller`和`Action`他們的關係以及如何建立。 `Controller`一般是一些`public`類,`Action`對應`Controller`中的`public`函式,所以他們的關係也很明瞭:一個`Controller`可以有多個`Action`。 `Controller`如何建立,預設情況下滿足下面的條件就可以作為一個`Controller`: - 在專案根目錄的`Controllers`中 - 類名稱以`Controller`結尾並繼承自`Controller`,或被`[Controller]`標記的類 - 共有類 - 沒有被`[NotController]`被標記 例如一個`Contoller`的常用模式如下: ```csharp using Microsoft.AspNetCore.Mvc; public class HomeController : Controller { //... } ``` 而`Action`就不需要許多條條框框了,只要寫在`Controller`中的方法函式都會被當成`Action`對待,如果不想一個函式被當做`Action`則需要新增`[NotAction]`標記。 > 留待測試: > > 1. 如果同時新增`[Controller]`和`[NotController]`會發生什麼狀況?是誰在最後誰生效嗎還是報錯? > 2. 是不是隻需要滿足`Controller`字尾就可以了,不一定非得繼承`Controller`,繼承他只是為了使用一些已經打包好的父類函式。 # MVC路由 首先建立一個`HomeController`測試路由用,需要建立到`Controllers`目錄下: ```csharp using Microsoft.AspNetCore.Mvc; namespace routing.Controllers { public class HomeController: Controller { public string Index() { return "Hello from HomeController.Index"; } } } ``` `.net core 2.0`和`.net core 3.0`建立路由的方式有所不同,現在分開說一下,先說一下舊的方式。 先在`ConfigureServices`中註冊`MVC`服務,然後`Configure`中配置路由模板: ```csharp public void ConfigureServices(IServiceCollection service) { // 註冊服務 service.AddMvc(); } public void Configure(IApplicationBuilder app, IHostEnvironment env) { if(env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // 路由模板 app.UseMvc(routes => { routes.MapRoute(template: "{controller}/{action}/{id?}", defaults: new {controller = "Home", action = "Index"}); }); app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); } ``` 但是放到`dotnet3`裡面是會報錯的: ``` MVCRouteStartup.cs(23,13): warning MVC1005: Using 'UseMvc' to configure MVC is not supported while using Endpoint Routing. To continue using 'UseMvc', please set 'MvcOptions.EnableEndpointRouting = false' inside 'ConfigureServices'. ``` 提示`UseMvc`不支援`Endpoint Routing`,通過查資料([stackoverflow](https://stackoverflow.com/questions/57684093/using-usemvc-to-configure-mvc-is-not-supported-while-using-endpoint-routing))找到原因,說的很清楚:2的時候`MVC`路由基於`IRoute`,3改成`Endpoint`了,官方推薦將`UseMVC`使用`UseEndpoiont`替換: ```csharp app.UseRouting(); // 必須寫,如果使用了UseStaticFiles要放在他之前 app.UseEndpoints(endpoionts => { endpoionts.MapControllerRoute(name: "MVC TEST ROUTE", pattern: "{controller}/{action}/{id?}", defaults: new {controller = "Home", action = "Index"}); }); ``` `ConfigureServices`中註冊`MVC`也有兩種方式: ```csharp services.AddMVC(); ``` 或 ```csharp service.AddControllersWithViews(); service.AddRazorPages(); ``` 當然,如果不想把`UseMap`去掉,那麼可以按照報錯的提示在`AddMVC`的時候配置一下引數禁用`EndpointRoute`: ```csharp services.AddMvc(options => options.EnableEndpointRouting = false); ``` 然後就可以跑起來了: ![](https://cdn.jsdelivr.net/gh/cnsimo/pic_bed/20200705183134.png) 好,扯了半天報錯,還是回到mvc路由上,上面是簡單演示了一下在`Startup`中如何建立路由,其實mvc路由有兩種定義方式: - **約定路由**:上面使用的方式就是約定路由,需要在`Startup`中配置; - **特性路由**:使用`[Route]`直接對`controller`或`action`進行標記; 修改`HomeController`加上路由標記: ```csharp using Microsoft.AspNetCore.Mvc; namespace routing.Controllers { [Route("h")] [Route("[controller]")] public class HomeController: Controller { [Route("")] [Route("[action]")] public string Index() { return "Hello from HomeController.Index"; } } } ``` 通過`[controller]`和`[action]`就可以動態的指代`home`和`index`(路徑不區分大小寫),這樣如果路由會隨著類名或方法名稱的改變自動調整。 並且可以看出,可以多個`[Route]`標記重疊使用,例如訪問`/h`和`/home/index`效果一樣: ![](https://cdn.jsdelivr.net/gh/cnsimo/pic_bed/20200705185240.png) 通過實驗可以看出,**特性路由會覆蓋掉約定路由**。 先總結這些吧,突然發現`asp.net core`這個東西還是挺先進的,比如依賴注入,`Startup`中的函式多數都是`interface`,為什麼直接對介面操作就可以改變一些東西或者讓我們可以自己註冊一箇中間件到app上,然後為什麼都不需要引用或者例項化就可以直接用app呼叫了,這都和依賴注入有關係吧,還有介面的設計理念也好像和其他語言的不太一樣,神奇了。 # 實驗程式碼 放到了[github](https://github.com/cnsimo/ASP.NET-MVC-labs)上,部分程式碼好像丟失了,不過應該不