基於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
准備
先到上面提供的下載地址,下載對應平台的dotnet
裝上,然后在命令行窗口輸入dotnet --version
查看輸出是否安裝成功。
然后,安裝visual studio code
,安裝之后還需要安裝C#
拓展,要不然每次打開cs
文件都會報錯。
創建項目
新建一個空目錄,例如mvc-test
。
使用命令dotnet new
查看可以新建的項目類型:
第一次嘗試,使用ASP.NET Core Empty
就可以,代號是web
,使用命令dotnet new web
就可以新建一個空項目,項目的名稱就是當前目錄的名字mvc-test
。
項目結構與默認配置
目錄主要結構和文件功能如下:
Program.cs
是程序的主類,Main
函數在這里定義,內容大致可以這么理解:
CreateDefaultBuilder
函數會使用默認的方法載入配置,例如通過讀取launchSettings.json
確定當前的發布環境:
webhost
通過ASPNETCORE_ENVIRONMENT
讀取發布環境,然后會讀取對應的配置文件,Development
對應appsettings.Development.json
,Production
對應appsettings.json
。
appsettings
文件是整個web應用的配置文件,如果web應用需要使用某個全局變量,可以配置到這個文件里面去。
webhost
在運行前會通過Startup
類,進行一些中間件的配置和注冊,以及進行客戶端的響應內容設置:
注:
dotnet core 3
版本里,取消了WebHost
,使用Host
以更通用的方式進行程序托管。
dotnet core 3 Program.cs
public static Void Main(string[] args)
{
Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(builder =>
{
builder.UseStartup<Startup>();
}).Build().Run();
}
獲取配置文件中的值
修改launingSettings.json
中設置的發布環境對應的配置文件,例如appsetttings.Delelopment.json
內容,添加一個Welcome
字段配置項,如下:
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"Welcome": "Hello from appsettings.json!!"
}
修改Startup.cs
文件,添加IConfiguration config
參數,.net core
內部會將配置文件內容映射到這個變量:
/// <summary>
/// 注冊應用程序所需的服務
/// </summary>
public void ConfigureServices(IServiceCollection services)
{
}
/// <summary>
/// 注冊管道中間件
/// </summary>
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://localhost:5000
,可以看到已經成功獲取到Welcome
配置項的值:
日志打印
通過ILogger
實現控制台日志的打印:
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<Startup> logger)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
var welcome = config["Welcome"];
logger.LogInformation(welcome);
app.Run(async (context) =>
{
await context.Response.WriteAsync(welcome);
});
}
ILogger
使用的時候需要指定打印日志的類名Startup
,最終打印效果如下:
服務注冊
上面的IConfiguration
可以直接使用,是因為IConfiguration
服務已經自動注冊過了。
對於自定義的服務,可以在ConfigureServices
中注冊,例如自定義一個服務WelcomeService
,項目目錄下新建兩個文件IWelcomeService.cs
和WelcomeService.cs
,內容如下:
/* IWelcomeService.cs
*
* 該接口類定義了一個getMessage方法。
*/
namespace mvc_test
{
public interface IWelcomeService
{
string getMessage();
}
}
/* 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
中注冊服務:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IWelcomeService, WelcomeService>();
}
然后在Configure
中使用的時候需要傳參:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
IConfiguration config,
ILogger<Startup> 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);
});
}
運行后結果:
這個例子中,注冊服務使用的函數是AddSingleton
,服務的生命周期除了Singleton
,還有其他兩個模式:Scoped
和Transient
。
這三個模式的區別:
- Transient:瞬態模式,服務在每次請求時被創建,它最好被用於輕量級無狀態服務;
- Scoped:作用域模式,服務在每次請求時被創建,整個請求過程中都貫穿使用這個創建的服務。比如Web頁面的一次請求;
- Singleton:單例模式,服務在第一次請求時被創建,其后的每次請求都用這個已創建的服務;
參考資料:
初始學習使用AddSingleton
就行了。
中間件和管道
中間件是一種用來處理請求和響應的組件,一個web應用可以有多個中間件,這些中間件共同組成一個管道,每次請求消息進入管道后都會按中間件順序處理對應請求數據,然后響應結果原路返回:
參考資料:
內置中間件的使用:處理靜態文件訪問請求
新建一個目錄wwwroot
,目錄下新建index.html
文件:
<html>
<head>
<title>TEST</title>
</head>
<body>
<h1>Hello from index.html!!!</h1>
</body>
</html>
使用之前的代碼,dotnet run
運行之后訪問http://localhost:5000/index.html
,發現還是之前的結果,並沒有訪問到index.html
。
這時候需要使用中間件StaticFiles
來處理靜態文件的請求,修改Startup.cs
的部分內容如下:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
IConfiguration config,
ILogger<Startup> 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
:
前面講到請求進入管道之后是安裝中間件添加順序處理的請求,如果當前中間件不能處理,才會交給下一個中間件,所以可以嘗試一下將上面的代碼調整一下順序:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
IConfiguration config,
ILogger<Startup> 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
里,訪問得到的內容就是:
通過StaticFiles
可以成功訪問到index.html
,但是如果想要index.html
成為默認網站主頁,需要使用中間件DefaultFiles
,修改上面代碼為:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
IConfiguration config,
ILogger<Startup> 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
也可以實現同樣的效果:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
IConfiguration config,
ILogger<Startup> 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
參數,放在管道尾部;
例如實現返回對應路徑內容:
/// <summary>
/// 注冊應用程序所需的服務
/// </summary>
public void ConfigureServices(IServiceCollection service)
{
}
/// <summary>
/// 注冊管道中間件
/// </summary>
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
;
文件內容如下:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace middleware.mymiddleware
{
/// <summary>
/// 請求方法檢查中間件,僅處理HEAD和GET方法
/// </summary>
public class HttpMethodCheckMiddleware
{
private readonly RequestDelegate _next;
/// <summary>
/// 構造方法,必須有的
/// </summary>
/// <param name="requestDelegate">下一個中間件</param>
public HttpMethodCheckMiddleware(RequestDelegate requestDelegate)
{
this._next = requestDelegate;
}
/// <summary>
/// 中間件調度方法
/// </summary>
/// <param name="context">HTTP上下文</param>
/// <returns>TASK任務狀態</returns>
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
中使用了:
app.UseMiddleware<HttpMethodCheckMiddleware>();
還可以編寫一個擴展類,封裝成類似內置中間件的方式UseXxx
。新建CustomMiddlewareExtension.cs
文件,內容如下:
using Microsoft.AspNetCore.Builder;
namespace middleware.mymiddleware
{
/// <summary>
/// 封裝中間件的擴展類
/// </summary>
public static class CustomMiddlewareExtension
{
/// <summary>
/// 添加HttpMethodCheckMiddleware中間件的擴展方法
/// </summary>
public static IApplicationBuilder UseHttpMethodCheckMiddleware(this IApplicationBuilder app)
{
return app.UseMiddleware<HttpMethodCheckMiddleware>();
}
}
}
現在就可以直接調用UseHttpMethodCheckMiddleware
注冊中間件了.
執行結果截圖省略。
疑問:那個CustomMiddlewareExtension
也沒見引用,怎么就可以直接使用app.UseHttpMethodCheckMiddleware
方法了?
有的可能和我一樣,c#都沒有學明白就直接開始擼dotnet了,看到這一臉懵逼,不過經過一番搜索,原來這是c#中對已有類或接口進行方法擴展的一種方式,參考C#編程指南。
內置路由
這一節先當了解,暫時用處不大,學完也會忘掉
先簡單看一下ASP.NET core
內置的路由方式(直接上startup.cs代碼內容):
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
的常用模式如下:
using Microsoft.AspNetCore.Mvc;
public class HomeController : Controller
{
//...
}
而Action
就不需要許多條條框框了,只要寫在Controller
中的方法函數都會被當成Action
對待,如果不想一個函數被當做Action
則需要添加[NotAction]
標記。
留待測試:
- 如果同時添加
[Controller]
和[NotController]
會發生什么狀況?是誰在最后誰生效嗎還是報錯?- 是不是只需要滿足
Controller
后綴就可以了,不一定非得繼承Controller
,繼承他只是為了使用一些已經打包好的父類函數。
MVC路由
首先創建一個HomeController
測試路由用,需要創建到Controllers
目錄下:
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
中配置路由模板:
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)找到原因,說的很清楚:2的時候MVC
路由基於IRoute
,3改成Endpoint
了,官方推薦將UseMVC
使用UseEndpoiont
替換:
app.UseRouting(); // 必須寫,如果使用了UseStaticFiles要放在他之前
app.UseEndpoints(endpoionts =>
{
endpoionts.MapControllerRoute(name: "MVC TEST ROUTE",
pattern: "{controller}/{action}/{id?}",
defaults: new {controller = "Home", action = "Index"});
});
ConfigureServices
中注冊MVC
也有兩種方式:
services.AddMVC();
或
service.AddControllersWithViews();
service.AddRazorPages();
當然,如果不想把UseMap
去掉,那么可以按照報錯的提示在AddMVC
的時候配置一下參數禁用EndpointRoute
:
services.AddMvc(options => options.EnableEndpointRouting = false);
然后就可以跑起來了:
好,扯了半天報錯,還是回到mvc路由上,上面是簡單演示了一下在Startup
中如何創建路由,其實mvc路由有兩種定義方式:
- 約定路由:上面使用的方式就是約定路由,需要在
Startup
中配置; - 特性路由:使用
[Route]
直接對controller
或action
進行標記;
修改HomeController
加上路由標記:
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
效果一樣:
通過實驗可以看出,特性路由會覆蓋掉約定路由。
先總結這些吧,突然發現asp.net core
這個東西還是挺先進的,比如依賴注入,Startup
中的函數多數都是interface
,為什么直接對接口操作就可以改變一些東西或者讓我們可以自己注冊一個中間件到app上,然后為什么都不需要引用或者實例化就可以直接用app調用了,這都和依賴注入有關系吧,還有接口的設計理念也好像和其他語言的不太一樣,神奇了。
實驗代碼
放到了github上,部分代碼好像丟失了,不過應該不要緊。