基于ASP.NET core的MVC站点开发笔记 0x01
我的环境
OS type:macSoftware:vscodeDotnet 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调用了,这都和依赖注入有关系吧,还有接口的设计理念也好像和其他语言的不太一样,神奇了。