2017 年 5 月

第 32 卷,第 5 期

领先技术 - 适用于 ASP.NET 开发者的 ASP.NET Core

作者 Dino Esposito | 2017 年 5 月

Dino Esposito关于 ASP.NET Core 的大多数热点话题都是围绕它支持的多平台体验。虽然这是一项巨大成就,但如果你是常规 ASP.NET 用户,编写了大量基本 .NET 4.x 代码,并打算继续使用熟悉的 IIS 和 Windows 环境,那么这就不是一个加分项。在这种情况下,ASP.NET Core 向此类常规 ASP.NET 开发者传达的价值主张是什么呢?

这一全新平台最初可能看起来完全不同,就像有人突然偷偷地动了你的奶酪一样。ASP.NET Core 是根据较为新式的做法重新生成的全新产品。此产品可能会也可能不会提升你的编程能力和满足客户需求的能力。实际上,没有人可以代表你回答这个问题。本专栏将拨开任何大肆宣传、基准和技术重点的迷雾,直奔讨论的要旨。如果你继续使用当前平台,那么 ASP.NET Core 的哪些方面可以吸引你的注意?

框架中采用的常见做法

ASP.NET 团队成员在设计原始 ASP.NET 框架时,采用了 Active Server Pages 的大多数最佳做法,并在新框架中采用了这些做法。在此过程中,他们还引入了许多新内容,如编译代码和托管代码、自动回发和服务器控件。ASP.NET Core 采用同一演化模式。

常见开发做法(如初始加载配置数据、依赖关系注入、NuGet 包、基于声明的身份验证和 Razor 改进)是新框架的原生特性。新框架还具有不同的启动过程、更为模块化的请求响应中间件,以及用于定义控制器和视图的略为灵活的基础结构。ASP.NET Core 还是跨平台框架,可方便你开发并在 Windows、macOS 和 Linux 上托管应用程序。这样一来,ASP.NET Core 会强制你编写更优质的代码,即默认强制实现其他一些水平的关注点分离。你也可以通过自律实现此目标。

对于任何形式的新开发,ASP.NET Core 绝对是理想之选。然而,作为全新的框架,一些初始成本是不可避免的: 所有团队成员都必须精通此框架。此外,所有成员还必须精通“模型-视图-控制器 (MVC)”应用程序模型。并不是所有可标记为新开发的框架都是全新的。可取的做法是,重用现有代码块,或至少利用所掌握的现有技能(即数据访问或安全技术)。这样做的现实可能性有多大? 针对这一点,ASP.NET Core 分为两种类型。

ASP.NET Core 的类型

图 1 展示了用于新建项目的 Visual Studio 2015 对话框。(与 Visual Studio 2017 对话框基本相同。)

在 Visual Studio 中新建 ASP.NET Core 项目
图 1:在 Visual Studio 中新建 ASP.NET Core 项目

第一个模板可创建经典的非 Core 项目。另外两个模板可创建定位不同 .NET Framework 的 ASP.NET Core 项目。这是探索 ASP.NET Core 未知领域过程中遇到的第一个十字路口。

选择使用完整的 .NET Framework 可以访问任何现有 .NET 类库,但仅限为在 Windows 和 IIS 上托管。图 2 总结了两种类型的区别。

图 2:ASP.NET Core 两种类型的根本区别

框架 事实
.NET Framework

仅含 ASP.NET MVC;无 WebForm

新的运行时环境和编程 API

定位选定版本 .NET Framework 的任意库 

仅限 IIS 托管

.NET Core

仅含 ASP.NET MVC;无 WebForm

新的运行时环境和编程 API

仅 .NET Core 库

跨平台托管

无论选择哪个版本的 .NET Framework,使用 ASP.NET Core 都会在新的运行时环境中公开代码,此环境整合了基于 system.web 的 MVC 运行时与由 OWIN 原则驱动的 Web API。

与 IIS 分离

近年来,Web API 框架试图解决瘦服务器的高需求问题,此类服务器能够向任何启用了 HTTP 的客户端公开 RESTful 接口。Web API 将应用程序模型与 Web 服务器分离开来,形成了 OWIN 规范,即一组 Web 服务器和应用程序互操作规则。然而,Web API 需要主机,当在 ASP.NET 应用程序上下文中托管时,它会直接将另一个运行时环境添加到内存占用中。这是整个行业向最简化 Web 发展的产物。最简化 Web 服务器是用于尽快获取内容的 HTTP 终结点,只是在一些业务逻辑的基础上添加了瘦 HTTP 层。最简化服务器只需根据需要处理请求,并返回响应(无开销,业务逻辑除外)即可。

虽然可以在一定程度上进行自定义,但目前的 ASP.NET 运行时环境并不旨在处理类似方案。将 ASP.NET 环境与宿主环境分离开来是 ASP.NET Core 的主要变化,也是后续许多应用程序级变化的原因所在。

应用程序启动

新建项目后,你首先会注意到缺少 global.asax 文件,但有 program.cs 文件。令人震惊的是,ASP.NET Core 应用程序是由 .NET 驱动程序工具启动的简单控制台应用程序 (bit.ly/2mLyHxe)。

.NET 工具是多平台支持的关键。一旦命令行工具(和 .NET Core 框架)可用于新平台,那么托管便会简化为连接 Web 服务器和工具。在 IIS 控制下,发布是通过特别模块 .NET Core Windows 服务器托管包完成 (bit.ly/2i9cF4d)。在 Apache 控制下,发布时通过配置文件在 Ubuntu 服务器上实现。有关示例,请访问 bit.ly/2lSd0aF

实际的 Web 服务器起到反向代理的作用,并通过配置的端口与控制台应用程序进行通信。控制台应用程序是在另一个更简单的 Web 服务器的基础上构建而成,此服务器用于接收请求,并触发内部应用程序管道来处理这些请求。下面的代码就执行此任务:

var host = new WebHostBuilder()
  .UseKestrel()
  .UseContentRoot(Directory.GetCurrentDirectory())
  .UseIISIntegration()
  .UseStartup<Startup>()
  .Build();
Host.Run();

Kestrel 是接收入站请求并通过管道处理这些请求的 ASP.NET Web 服务器的名称。只有为 IIS 托管时,才需要在代码片段中调用 IIS 集成模块。

推荐对 ASP.NET Core 应用程序使用反向代理主要是为了提高安全性,因为 Kestrel 内部 Web 服务器(目前)并不包含可防止分布式拒绝服务 (DDoS) 等攻击发生的筛选器。从纯功能角度来看,不一定需要启用反向代理。

如上所述,ASP.NET Core 应用程序中不再有 global.asax 文件,web.config 文件的作用也被大大削弱。实际上,它只起到让 IIS 能够代表应用程序执行某任务(如响应生成一些静态错误页面)的作用。关键任务(如配置错误处理、日志记录、身份验证和存储全局配置数据)都是通过启动类操控的新 API 完成。

启动类

启动类至少包含主机将在初始化阶段调用的一些方法:

public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  public void Configure(IApplicationBuilder app)
  {
    app.Run(async (context) =>
    {
      await context.Response.WriteAsync(DateTime.Now)
    });
  }
}

通过 ConfigureServices 方法,可以声明应用程序将使用的系统服务。从技术角度来讲,此为可选方法,但我认为在任何实际方案中都有必要使用此方法。传统的 ASP.NET 开发者可能会感到震惊的是,即使是使用 MVC 应用程序模型也必须进行显式声明和启用。不过,这一事实衡量了 ASP.NET Core 对模块化的重视程度:

public void ConfigureServices(IServiceCollection services)
{
  services.AddMvc();
}

在方法 Configure 中,可以配置先前请求的任意服务。例如,如果请求了 ASP.NET MVC 服务,可以在方法 Configure 中指定支持的路由列表。请注意,还需要进行显式调用,才能启用内部 Web 服务器来响应生成静态文件,包括 jQuery 和 Bootstrap 等常见文件:

public void Configure(IServiceCollection services)
{
  app.UseStaticFiles();
  app.UseMvcWithDefaultRoute();
  ...
}

还可以在启动类中配置应用程序中间件。中间件是一个新词,在概念上与目前 ASP.NET 的 HTTP 模块有相当程度的重叠。在 ASP.NET Core 中,中间件的工作方式如图 3 中所示。

ASP.NET Core 中间件
图 3:ASP.NET Core 中间件

可以注册有机会预处理和后处理任何传入请求的代码块。也就是说,每个中间件都可以注册在终止中间件(即启动类的 Configure 方法中的 Run 方法)之前或之后运行的代码。整个模型类似于 IIS 的旧版 ISAPI 模型。下面的示例展示了某中间件:

app.Use(async (httpContext, next) =>
{
  // Pre-process the request
  // Yield to the next middleware
  await next();
  // Post-process the request   
});

中间件是需要使用 HttpContext 对象并返回 Task 的函数。中间件组件列表中的终止中间件是 Run 方法。与目前的 ASP.NET 管道相比,ASP.NET Core 管道是双向的,完全可自定义。此外,管道还默认是空的。

最简化 Web 服务

不言而喻,如果管道中没有 Run 方法,那么没有请求会生成响应。同时,只需调用 Run 方法即可生成响应。这表明 ASP.NET Core 应用程序管道非常之短。在 ASP.NET WebForm 和 MVC 中,需要先完成许多任务,然后才能对每个请求运行你自己的代码。例如,解析控制器方法是一个相当长的过程,涉及以操作调用程序为重点的整个子系统。相反,Run 方法是在收到请求之后立即直接调用。

假设要创建一个拥有图像文件(例如,标志)列表的文件服务器,用于根据一些输入参数或设备属性返回适当大小的图像。在目前的 ASP.NET 中,最快速的做法可能是编写特别 HTTP 处理程序,即通过实现映射到固定 URL 路由的 IHttpHandler 接口创建的类。HTTP 处理程序的速度比 ASPX 终结点和 MVC 控制器更快,因为它的管道更细。此外,它占用的内存比 Web API 终结点更小,因为它不需要在基本 ASP.NET 管道的基础之上使用另一个 OWIN 管道。(如果 Web API 解决方案在 IIS 外部托管,情况就不是这样了。)

在 ASP.NET Core 中,创建有效的文件服务器变得空前简单和有效。只需编写附加逻辑(即重设大小/检索),然后将逻辑绑定到 ConfigureServices 方法的 Run 方法中:

public void Configure(IApplicationBuilder app)
{
  app.Run(async (context) =>
  {
    var code = context.Request.Query["c"];
    var size = context.Request.Query["s"];
    var file = FindAndResizeFlag(code, file);
    await context.Response.SendFileAsync(file);
  });
}

在此示例中,我假设使用某自定义逻辑来查找与提供的参数匹配的服务器文件名,然后通过 Response 对象将其返回给调用方。无需使用其他任何代码,无论是可见,还是不可见。坦率地说,这种方法实际上是再简单不过了。

无论你对 ASP.NET Core 的直觉情绪如何(怀疑态度还是青睐有加),ASP.NET Core 都提供了其他任何 ASP.NET 平台不曾有的一种特殊功能:创建最简化 Web 服务。同时,方便你生成最简化 Web 服务的同一基础结构也是能以最低合理开销响应任何请求的最好保证。

总结

要成为一名高效率的 ASP.NET Core 开发者,只需熟悉比 WebForm 更新式的应用程序模型(即 ASP.NET MVC 或单页应用程序模型)即可。无论表象如何,ASP.NET Core 的大部分重大变革都体现在运行时环境中。了解托管模型和中间件就足以运用新平台了。最后,它仍是用于创建控制器并呈现 Razor 视图的工具。需要使用不同的 API 来执行身份验证、日志记录和配置等常见任务,但学习起来并不费时。在我看来,挑战在于如何发现适合自己的功能。


Dino Esposito*是《Microsoft .NET: 构建面向企业的应用程序》(Microsoft Press,2014 年)和《使用 ASP.NET 构建新型 Web 应用程序》(Microsoft Press,2016 年)的作者。Esposito 是 JetBrains 公司 .NET 和 Android 平台的技术推广专家,经常在全球性行业活动上发表演讲,他在 software2cents.wordpress.com 和 Twitter: @despos.*上分享了他的软件构想。

衷心感谢以下 Microsoft 技术专家对本文的审阅: James McCaffrey