几乎任何服务器端处理环境都有自己的直通组件管道,用于检查、重路由或修改传入请求和传出响应。经典 ASP.NET 围绕 HTTP 模块理念进行排列,而 ASP.NET Core 采用基于中间件组件的更现代的体系结构。最终目的是相同的 - 允许可配置的外部模块以请求(以及稍后的响应)在服务器环境中传递的方式进行干预。中间件组件的主要目的是以某种方式改变和筛选数据流(在某些特定情况下,只是使请求短路,停止任何进一步的处理)。
ASP.NET Core 管道自框架 1.0 版发布以来几乎没有变化,但即将发布的 ASP.NET Core 3.0 邀请了一些关于当前体系结构的评论,这些评论在很大程度上被忽略了。因此,在本文中,我将重新讨论 ASP.NET Core 运行时管道的整体功能,并着重介绍 HTTP 终结点的角色和可能的实现。
适用于 Web 后端的 ASP.NET Core
特别是在过去几年,构建前端和后端完全解耦的 Web 应用程序已经变得相当普遍。因此,大多数 ASP.NET Core 项目现在都是简单的没有 UI 的 Web API 项目,仅为单页面和/或移动应用程序提供 HTTP 外观,大多数情况下,这些应用通过 Angular、React、Vue 及其移动对等项构建而成。
当你意识到这一点,将出现一个问题:在不使用任何 Razor 工具的应用程序中,绑定到 MVC 应用程序模型是否仍有意义?MVC 模型并不是免费的,事实上,在某种程度上,一旦停止使用控制器来提供操作结果,它甚至可能不是最轻量级的选项。进一步追问:如果大部分 ASP.NET Core 代码编写只是为了返回 JSON 有效负载,那么操作结果概念本身是否绝对必要?
带着这些想法,让我们回顾一下 ASP.NET Core 管道和中间件组件的内部结构,以及可以在启动期间绑定到的内置运行时服务列表。
启动类
在任何 ASP.NET Core 应用程序中,一个类被指定为应用程序引导程序。大多数情况下,此类采用名称“Startup”。该类在 Web 主机配置中声明为启动类,Web 主机通过反射实例化并调用该类。该类可以有两种方法 - ConfigureServices(可选)和 Configure。在第一个方法中,你将接收当前(默认)运行时服务列表,并需要添加更多服务来为实际应用程序逻辑做准备。在 Configure 方法中,为默认服务和显式请求支持应用程序的服务进行配置。
Configure 方法接收至少一个应用程序生成器类实例。可以将此实例视为传递给代码的 ASP.NET 运行时管道的工作实例,以便根据需要进行配置。一旦 Configure 方法返回,管道工作流将配置完毕,并用于执行从连接的客户端发送的任何进一步请求。图 1 提供了 Startup 类的 Configure 方法的实现示例。
图 1 Startup 类中 Configure 方法的基本示例
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, nextMiddleware) =>
{await context.Response.WriteAsync("BEFORE");await nextMiddleware(); await context.Response.WriteAsync("AFTER");
});
app.Run(async (context) =>
{var obj = new SomeWork();await context
.Response
.WriteAsync("
"
+
obj.SomeMethod() +"");
});
}
Use 扩展方法是用于将中间件代码添加到其他空管道工作流的主要方法。注意,添加的中间件越多,服务器需要执行更多的工作来满足任何传入请求。最小的是管道,最快的是客户端至第一字节的时间 (TTFB)。
可以使用 lambdas 或临时中间件类向管道添加中间件代码。这完全由你来决定:lambda 更直接,但类(最好是一些扩展方法)将使整个过程更易于阅读和维护。中间件代码获取请求的 HTTP 上下文和对管道中下一个中间件的引用(如果有的话)。图 2 显示了各种中间件组件链接在一起的总体视图。
图 2 ASP.NET Core 运行时管道
每个中间件组件都有两次机会介入正在进行的请求的生命周期。它可以预先处理从先前注册运行的组件链接收到的请求,然后它会被链中的下一个组件取代。当链中的最后一个组件有机会预先处理请求时,请求会被传递给终止中间件,以便进行实际处理,目的是生成具体输出。之后,组件链按相反顺序返回,如图 2 所示,每个中间件都有第二次处理的机会 - 不过,这一次将是后处理操作。在图 1 代码中,预处理代码与后处理代码的分割线为:
终止中间件
在图 2 所示的体系结构中,关键是终止中间件的角色,它是 Configure 方法底部的代码,用于终止链并处理请求。所有演示 ASP.NET Core 应用程序都有一个终止的 lambda,如下所示:
app.Run(async (context) => { ... };
lambda 接收 HttpContext 对象,并在应用程序上下文中执行它应执行的任何操作。
实际上,有意不生成到下一个组件的中间件组件将终止链,进而导致响应被发送到请求的客户端。UseStaticFiles 中间件就是一个很好的例子,它在指定的 Web 根文件夹下提供静态资源,并终止请求。另一个示例是 UseRewriter,它可以命令客户端重定向到新 URL。在没有终止中间件的情况下,请求很难在客户端上产生一些可见输出,尽管响应仍然通过运行中间件(例如,通过添加 HTTP 标头或响应 cookie)以修改后的形式发送。
还有两个专用的中间件工具也可用于短路请求:app.Map 和 app.MapWhen。前者检查请求路径是否匹配参数并运行自己的终止中间件,如下所示:
app.Map("/now", now =>
{
now.Run(async context =>
{var time = DateTime.UtcNow.ToString("HH:mm:ss");await context
.Response
.WriteAsync(time);
});
});
而后者仅在验证了指定布尔条件后才运行自己的终止中间件。布尔条件来自接受 HttpContext 的函数计算。图 3 中的代码提供了一个非常精简的小型 Web API,它仅为单个终结点提供服务,并且在没有任何类似于控制器类项的情况下执行该操作。
图 3 极小型 ASP.NET Core Web API
public void Configure(IApplicationBuilder app,
ICountryRepository country)
{
app.Map("/country", countryApp =>
{
countryApp.Run(async (context) =>
{var query = context.Request.Query["q"];var list = country.AllBy(query).ToList();var json = JsonConvert.SerializeObject(list);await context.Response.WriteAsync(json);
});
});// Work as a catch-all
app.Run(async (context) =>
{await context.Response.WriteAsync("Invalid call");
}
});
如果 URL 匹配 /country,终止中间件将从查询字符串中读取一个参数,并安排对存储库的调用,以获得匹配国家/地区列表。然后,对列表对象以 JSON 格式直接手动序列化到输出流。通过添加一些其他映射路由,你甚至可以扩展 Web API。再简单不过了。
MVC 怎么样?
在 ASP.NET Core 中,整个 MVC 机制作为黑盒运行时服务提供。只需绑定到 ConfigureServices 方法中的服务,并在 Configure 方法中配置其路由,如下面的代码所示:
public void ConfigureServices(IServiceCollection services)
{// Bind to the MVC machinery
services.AddMvc();
}public void Configure(IApplicationBuilder app)
{// Use the MVC machinery with default route
app.UseMvcWithDefaultRoute();// (As an alternative) Use the MVC machinery with attribute routing
app.UseMvc();
}
此时,如果你打算提供 HTML,欢迎填充常用的 Controllers 文件夹,甚至 Views 文件夹。注意在 ASP.NET Core 还可以使用 POCO 控制器,它是简单的 C# 类,经修饰后可以被识别为控制器,并与 HTTP 上下文断开连接。
MVC 机制是终止中间件的另一个很好的示例。一旦请求被 MVC 中间件捕获,一切都在它的控制之下,管道会突然终止。
有趣的是,MVC 机制在内部运行自己的自定义管道。它不以中间件为中心,但它仍然是一个完整的运行时管道,控制请求如何路由到控制器操作方法,并最终将生成的操作结果呈现到输出流。MVC 管道由在各个控制器方法前后运行的各种类型的操作筛选器(操作名称选择器、授权筛选器、异常处理程序、自定义操作结果管理器)组成。在 ASP.NET Core 中,内容协商也隐藏在运行时管道中。
从更深刻的角度来看,整个 ASP.NET MVC 机制看起来就像是被固定在最新的、经重新设计的、以中间件为中心的 ASP.NET Core 管道的上方。就像 ASP.NET Core 管道和 MVC 机制是不同类型的实体,只是以某种方式连接在一起。总体情况与 MVC 被固定在现已被摒弃的 Web 窗体运行时之上的方式没有太大不同。实际上,在该上下文中,如果处理请求无法与物理文件匹配(很可能是 ASPX 文件),MVC 就会通过专用的 HTTP 处理程序启动。
这有问题吗?很可能没有。也许问题还没出现!
将 SignalR 置于循环中
将 SignalR 添加到 ASP.NET Core 应用程序时,需要做的就是创建一个中心类来公开终结点。有趣的是中心类可能与控制器完全不相关。不需要 MVC 来运行 SignalR,但中心类的行为类似于外部请求的前端控制器。从中心类公开的方法可以执行任何工作 - 甚至与框架的跨应用通知性质无关的工作,如图 4 所示。
图 4 从中心类公开方法
public class SomeHub : Hub
{public void Method1()
{// Some logic
...
Clients.All.SendAsync("...");
}public void Method2()
{// Some other logic
...
Clients.All.SendAsync("...");
}
}
能看到图片吗?
SignalR 中心类可以被视为一个控制器类,不需要整个 MVC 机制,非常适合无 UI(或者更确切地说,是 Razor)响应。
将 gRPC 置于循环中
在 3.0 版本中,ASP.NET Core 还提供了对 gRPC 框架的本机支持。该框架设计为与 RPC 准则一起使用,是围绕接口定义语言的一层代码,接口定义语言完全定义终结点,并且能够使用 HTTP/2 上的 Protobuf 二进制序列化触发连接方之间的通信。从 ASP.NET Core 3.0 角度来看,gRPC 还是另一个可调用外观,可以进行服务器端计算并返回值。下面介绍了如何启用 ASP.NET Core 服务器应用程序以支持 gRPC:
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc();
}public void Configure(IApplicationBuilder app)
{
app.UseRouting(routes =>
{
routes.MapGrpcService();
});
}
另请注意全局路由的使用,以使应用程序支持没有 MVC 机制的路由。可以将 UseRouting 视为定义 app.Map 中间件块的一种更结构化的方法。
前面代码的净效果是支持从客户端应用程序到映射服务的 RPC 样式调用,即 GreeterService 类。有趣的是,GreeterService 类在概念上等同于 POCO 控制器,只不过它不需要被识别为控制器类,如下所示:
public class GreeterService : Greeter.GreeterBase
{public GreeterService(ILogger logger)
{
}
}
基类(GreeterBase 是一个封装在静态类中的抽象类)包含执行请求/响应通信所需的管道。gRPC 服务类与 ASP.Net Core 基础结构完全集成,可以注入外部引用。
底线
特别是随着 ASP.NET Core 3.0 的发布,将会出现另外两种情况,在这些情况下,使用无 MVC 的控制器样式外观将会很有帮助。SignalR 有一个中心类,gRPC 有一个服务类,但关键是它们在概念上是相同的,必须在不同的场景以不同方式实现这些类。MVC 机制已或多或少被移植到 ASP.NET Core,因为它最初是为经典 ASP.NET 设计的,并围绕控制器和操作结果维护自己的内部管道。与此同时,随着 ASP.NET Core 越来越广泛地被用作普通后端服务提供程序(不支持视图),对 HTTP 终结点可能统一的 RPC 样式外观的需求也在增长。
原文地址:https://msdn.microsoft.com/zh-cn/magazine/mt833464
.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com