由于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