在前五章中,您已经完成了服务器端渲染的 ASP.NET Core 应用程序的每一层,使用 Razor Pages 将 HTML 渲染到浏览器。 在本章中,您将看到对 ASP.NET Core 应用程序的不同看法。 我们将探索 Web API,而不是使用 Razor Pages,它充当客户端 SPA 和移动应用程序的后端。
您可以将已经学到的大部分知识应用到 Web API; 它们使用相同的 MVC 设计模式,并且路由、模型绑定和验证的概念都得以贯彻。 与传统 Web 应用程序的区别主要在于 MVC 的视图部分。 它们不是返回 HTML,而是以 JSON 或 XML 形式返回数据,客户端应用程序使用这些数据来控制其行为或更新 UI。
在本章中,您将学习如何定义控制器和操作,并了解它们与您已经知道的 Razor 页面和控制器有多么相似。 您将学习如何创建 API 模型以响应请求,以客户端应用程序可以理解的方式返回数据和 HTTP 状态代码。
在探索 MVC 设计模式如何应用于 Web API 之后,您将看到相关主题如何与 Web API 一起使用:路由。 我们将看看属性路由如何重用第 5 章中的许多相同概念,但将它们应用于您的操作方法而不是 Razor 页面。
ASP.NET Core 2.1 中添加的一大特性是 [ApiController]
属性。该属性应用了 Web API 中使用的几种常见约定,从而减少了您必须自己编写的代码量。 在第 9.5 节中,您将了解针对无效请求的自动 400 Bad Requests、模型绑定参数推断和 ProblemDetails
对象如何使构建 API 更容易、更一致。
您还将学习如何使用内容协商来格式化您的操作方法返回的 API 模型,以确保您生成调用客户端可以理解的响应。 作为其中的一部分,您将学习如何添加对其他格式类型(例如 XML)的支持,以便在客户端请求时生成 XML 响应。
ASP.NET Core 的一大优点是您可以使用它创建的各种应用程序。 与单独使用传统 Web 应用程序相比,轻松构建通用 HTTP Web API 的能力提供了在更广泛的情况下使用 ASP.NET Core 的可能性。 但是你应该构建一个 Web API 吗?为什么? 在本章的第一部分,我将讨论您可能希望或可能不希望创建 Web API 的一些原因。
传统的 Web 应用程序通过向用户返回 HTML 来处理请求,该 HTML 显示在 Web 浏览器中。 您可以使用 Razor 页面轻松构建这种性质的应用程序,以使用 Razor 模板生成 HTML,正如您在最近的章节中看到的那样。 这种方法很常见并且很好理解,但现代应用程序开发人员还有许多其他可能性需要考虑,如图 9.1 所示。
近年来,随着 Angular、React 和 Vue 等框架的开发,客户端单页应用程序 (SPA) 变得流行起来。 这些框架通常使用在用户的 Web 浏览器中运行的 JavaScript 来生成他们看到并与之交互的 HTML。 当用户第一次访问应用程序时,服务器会将此初始 JavaScript 发送到浏览器。 用户的浏览器在从服务器加载任何应用程序数据之前加载 JavaScript 并初始化 SPA。
图 9.1 现代开发人员必须考虑其应用程序的许多不同消费者。 除了使用 Web 浏览器的传统用户外,这些用户还可以是 SPA、移动应用程序或其他应用程序。 |
---|
一旦 SPA 加载到浏览器中,与服务器的通信仍然通过 HTTP 进行,但服务器端应用程序不会直接向浏览器发送 HTML 以响应请求,而是将数据(通常以 JSON 等格式)发送到 客户端应用程序。 然后 SPA 解析数据并生成适当的 HTML 以显示给用户,如图 9.2 所示。 客户端与之通信的服务器端应用程序端点有时称为 Web API。
定义 Web API 公开多个 URL,可用于访问或更改服务器上的数据。 它通常使用 HTTP 访问。
如今,移动应用程序很常见,从服务器应用程序的角度来看,它类似于客户端 SPA。 移动应用程序通常会使用 HTTP Web API 与服务器应用程序进行通信,接收通用格式的数据,例如 JSON,就像 SPA 一样。 然后它会根据接收到的数据修改应用程序的 UI。
图 9.2 使用 Blazor WebAssembly 的示例客户端 SPA。 初始请求将 SPA 文件加载到浏览器中,随后的请求从 Web API 获取数据,格式为 JSON。 |
---|
Web API 的最后一个用例是您的应用程序被设计为部分或完全由其他后端服务使用。 想象一下,您已经构建了一个 Web 应用程序来发送电子邮件。 通过创建 Web API,您可以允许其他应用程序开发人员通过向您发送电子邮件地址和消息来使用您的电子邮件服务。 几乎所有语言和平台都可以访问 HTTP 库,它们可以用来从代码访问您的服务。
这就是 Web API 的全部内容。 它公开了许多端点 (URL),客户端应用程序可以向这些端点发送请求并从中检索数据。 这些用于驱动客户端应用程序的行为,以及提供客户端应用程序向用户显示正确界面所需的所有数据。
您是否需要或想要为您的 ASP.NET Core 应用程序创建 Web API 取决于您要构建的应用程序的类型。 如果您熟悉客户端框架,您需要开发一个移动应用程序,或者您已经配置了一个 SPA 构建管道,那么您很可能希望添加他们可以用来访问您的应用程序的 Web API。
使用 Web API 的卖点之一是它可以作为所有客户端应用程序的通用后端。 例如,您可以从构建使用 Web API 的客户端应用程序开始。 稍后,您可以添加一个使用相同 Web API 的移动应用程序,只需对 ASP.NET Core 代码进行少量修改或无需修改。
如果您是 Web 开发的新手,您无需从 Web 浏览器外部调用您的应用程序,或者您不希望或不需要配置客户端应用程序所涉及的工作,您可能不需要 Web API 最初。 您可以坚持使用 Razor Pages 生成您的 UI,毫无疑问,您会非常高效!
注意 尽管行业已经转向客户端框架,但使用 Razor 的服务器端渲染仍然是相关的。 您选择哪种方法在很大程度上取决于您对以传统方式构建 HTML 应用程序与在客户端上使用 JavaScript 的偏好。
话虽如此,向您的应用程序添加 Web API 并不是您必须提前担心的事情。 稍后添加它们很简单,因此您始终可以在开始时忽略它们并在需要时添加它们。 在许多情况下,这将是最好的方法。
一旦你确定你的应用程序需要一个 Web API,创建一个很容易,因为它内置在 ASP.NET Core 中。 在下一节中,您将看到如何创建 Web API 项目和您的第一个 API 控制器。
在本节中,您将学习如何创建 ASP.NET Core Web API 项目并创建您的第一个 Web API 控制器。 您将看到如何使用控制器操作方法来处理 HTTP 请求,以及如何使用 ActionResults 来生成响应。
有些人认为 MVC 设计模式只适用于直接渲染其 UI 的应用程序,例如您在前几章中看到的 Razor 视图。 在 ASP.NET Core 中,MVC 模式在构建 Web API 时同样适用,但 MVC 的视图部分涉及生成机器友好的响应而不是用户友好的响应。
与此并行的是,您在 ASP.NET Core 中创建 Web API 控制器的方式与创建传统 MVC 控制器的方式相同。 从代码的角度来看,它们的唯一区别是它们返回的数据类型——MVC 控制器通常返回 ViewResult; Web API 控制器通常从它们的操作方法中返回原始 .NET 对象,或者返回一个 IActionResult,例如 StatusCodeResult,正如您在第 4 章中看到的那样。
为了让您初步了解您正在使用什么,图 9.3 显示了从浏览器调用 Web API 端点的结果。 您收到的数据不是友好的 HTML UI,而是可以在代码中轻松使用。 在此示例中,当您请求 URL /fruit 时,Web API 以 JSON 格式返回字符串水果名称列表
图 9.3 通过在浏览器中访问 URL 来测试 Web API。 向 /fruit URL 发出 GET 请求,该 URL 返回一个 List |
---|
TIP Web API 通常由 SPA 或移动应用程序从代码中访问,但通过直接访问 Web 浏览器中的 URL,您可以查看 API 返回的数据。
ASP.NET Core 5.0 应用程序还包括一个有用的端点,用于测试和探索开发中的 Web API 项目,称为 Swagger UI,如图 9.4 所示。 这使您可以浏览应用程序中的端点、查看预期响应并通过发送请求进行试验。
您可以使用在第 2 章中看到的相同的新建项目过程在 Visual Studio 中创建一个新的 Web API 项目。创建一个提供项目名称的新 ASP.NET Core 应用程序,然后,当您到达“新建项目”对话框时,选择 ASP .NET Core Web API 模板,如图 9.5 所示。 如果您使用的是 CLI,则可以使用 dotnet new webapi -o WebApplication1
创建一个类似的模板。
图 9.5 Web 应用程序模板屏幕。 此屏幕出现在“配置您的项目”对话框中,允许您自定义将生成应用程序的模板。选择 ASP.NET Core Web API 模板以创建 Web API 项目。 |
---|
API 模板仅为 Web API 控制器配置 ASP.NET Core 项目。 此配置出现在 Startup.cs 文件中,如清单 9.1 所示。 如果将此模板与 Razor Pages 项目进行比较,您将看到 Web API 项目在 ConfigureServices 方法中使用 AddControllers() 而不是 AddRazorPages()。此外,通过调用 MapControllers() 添加 API 控制器而不是 Razor Pages 在 UseEndpoints 方法中。 默认的 Web API 模板还添加了 Swagger UI 所需的 Swagger 服务和端点,如图 9.4 所示。 如果您在项目中同时使用 Razor 页面和 Web API,则需要将所有这些方法调用添加到您的应用程序中。
清单 9.1 默认 Web API 项目的 Startup 类
public class Startup { public void ConfigureServices(IServiceCollection services) { //AddControllers 将 API 控制器的必要服务添加到您的应用程序。 services.AddControllers(); //添加生成 Swagger/OpenAPI 规范文档所需的服务 services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "DefaultApiTemplate", Version = "v1" }); }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); //添加 Swagger UI 中间件以探索您的 Web API app.UseSwagger(); app.UseSwaggerUI(c => c.SwaggerEndpoint( "/swagger/v1/swagger.json", "DefaultApiTemplate v1")); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { //MapControllers 将应用程序中的 API 控制器操作配置为端点。 endpoints.MapControllers(); }); } }
清单 9.1 中的 Startup.cs 文件指示您的应用程序查找应用程序中的所有 API 控制器并在 EndpointMiddleware 中配置它们。 当 RoutingMiddleware 将传入的 URL 映射到操作方法时,每个操作方法都成为一个端点并且可以接收请求。
注意 您不必为 Razor 页面和 Web API 控制器使用单独的项目,但我更喜欢尽可能这样做。 采用这种方法可以使某些方面(例如错误处理和身份验证)变得更容易。 当然,运行两个独立的应用程序有其自身的困难!
您可以通过在项目的任意位置创建新的 .cs 文件来将 Web API 控制器添加到项目中。 传统上,这些都放在一个名为 Controllers 的文件夹中,但这不是技术要求。 清单 9.2 显示了用于创建图 9.3 中演示的 Web API 的代码。 这个简单的例子突出了与传统 MVC 控制器的相似之处。
清单 9.2 一个简单的 Web API 控制器
[ApiController] //Web API 控制器通常使用 [ApiController] 属性来选择加入通用约定。 public class FruitController : ControllerBase //ControllerBase 类为创建 IActionResults 提供了几个有用的函数。 { //返回的数据通常会从真实应用程序中的应用程序模型中获取。 List<string> _fruit = new List<string> { "Pear", "Lemon", "Peach" }; [HttpGet("fruit")] //[HttpGet] 属性定义用于调用操作的路由模板。 public IEnumerable<string> Index() //操作方法的名称 Index 不用于路由。 它可以是你喜欢的任何东西。 { return _fruit; //控制器公开了一个返回水果列表的操作方法。 } }
Web API 通常在 API 控制器上使用 [ApiController] 属性(在 .NET Core 2.1 中引入)并派生自 ControllerBase 类。 基类提供了几个帮助方法来生成结果,并且 [ApiController] 属性自动应用一些常见的约定,如您将在第 9.5 节中看到的。
提示 还有一个 Controller 基类,通常在使用带有 Razor 视图的 MVC 控制器时使用。 这对于 Web API 控制器来说不是必需的,因此 ControllerBase 是更好的选择。
在清单 9.2 中,您可以看到操作方法 Index 直接从操作方法返回一个字符串列表。 当您从这样的操作返回数据时,您正在为请求提供 API 模型。 客户端将收到此数据。 它被格式化为适当的响应,即图 9.3 中列表的 JSON 表示,并以 200 OK 状态码发送回浏览器。
公开 Web API 控制器操作的 URL 的处理方式与传统 MVC 控制器和 Razor 页面的处理方式相同 - 使用路由。 应用于 Index 方法的 [HttpGet("fruit")] 属性指示该方法应使用路由模板“fruit”并应响应 HTTP GET 请求。 您将在第 9.5 节中了解有关属性路由的更多信息。
在清单 9.2 中,数据直接从 action 方法返回,但您不必这样做。 你可以自由地返回一个 ActionResult ,这通常是必需的。 根据您的 API 所需的行为,您有时可能希望返回数据,有时您可能希望返回原始 HTTP 状态代码,指示请求是否成功。 例如,如果 API 调用请求不存在产品的详细信息,您可能希望返回 404 Not Found 状态代码。
清单 9.3 显示了这种情况的一个示例。 它显示了与以前一样在同一个 FruitController 上的另一个操作。 此方法为客户端提供了一种通过 id 获取特定水果的方法,我们假设在此示例中它是您在上一个清单中定义的 _fruit 列表中的索引。 模型绑定用于从请求中设置 id 参数的值。
注意 API 控制器使用模型绑定,正如您在第 6 章中看到的,将操作方法参数绑定到传入请求。 模型绑定和验证的工作方式与 Razor Pages 完全相同:您可以将请求绑定到简单的原语以及复杂的 C# 对象。 唯一的区别是没有具有 [BindProperty] 属性的 PageModel - 您只能绑定到操作方法参数。
清单 9.3 返回 IActionResult 以处理错误情况的 Web API 操作
[HttpGet("fruit/{id}")] //定义动作方法的路由模版 public ActionResult<string> View(int id) //action方法返回一个ActionResult<string>,因此它可以返回一个字符串或一个 ActionResult。 { if (id >= 0 && id < _fruit.Count) //只有当 id 值是有效的_fruit 元素索引时,才能返回元素。 { return _fruit[id];//接返回数据会返回带有 200 状态码的数据 } return NotFound(); //NotFound 返回一个 NotFoundResult,它将发送一个 404 状态码。 }
在action方法的成功路径中,id参数的值大于0且小于_fruit中的元素个数。 如果为真,则将元素的值返回给调用者。 如清单 9.2 所示,这是通过简单地直接返回数据来实现的,这会生成 200 状态码并返回响应正文中的元素,如图 9.6 所示。 您也可以使用 OkResult 返回数据,方法是返回 Ok(_fruit[id]),使用 ControllerBase 类上的 Ok 辅助方法 1 — 在后台,结果是相同的。
图 9.6 动作方法返回的数据被序列化到响应体中,并生成状态码为 200 OK 的响应。 |
---|
如果 id 超出 _fruit 列表的范围,则该方法调用 NotFound 以创建 NotFoundResult。 执行时,此方法会生成 404 Not Found 状态代码响应。 [ApiController] 属性自动将响应转换为标准的 ProblemDetails 实例,如图 9.7 所示。
图 9.7 [ApiController] 属性将错误响应(在本例中为 404 响应)转换为标准的 ProblemDetails 格式。 |
---|
定义 ProblemDetails 是一个 Web 规范,用于为 HTTP API 提供机器可读错误。 您将在第 9.5 节中了解有关它们的更多信息。
从清单 9.3 中您可能会感到困惑的一个方面是,对于成功的情况,我们返回一个字符串,但 View 的方法签名说我们返回一个 ActionResult
通用的 ActionResult
您可以自由地从 Web API 控制器返回任何类型的 ActionResult,但您通常会返回 StatusCodeResult 实例,这些实例将响应设置为特定的状态代码,有或没有相关数据。 例如,NotFoundResult 和 OkResult 都派生自 StatusCodeResult。 另一个常用的状态码是 400 Bad Request,通常在请求中提供的数据验证失败时返回。 这可以使用 BadRequestResult 生成。 在许多情况下,[ApiController] 属性可以自动为您生成 400 个响应,您将在第 9.5 节中看到。
提示 您在第 4 章中了解了各种 ActionResult。BadRequestResult、OkResult 和 NotFoundResult 都继承自 StatusCodeResult 并为它们的类型设置适当的状态码(分别为 200、404 和 400)。使用这些包装类使您的代码的意图 比依赖其他开发人员更清楚地了解各种状态码数字的意义。
一旦您从控制器返回了 ActionResult(或其他对象),它就会被序列化为适当的响应。 这以多种方式起作用,具体取决于
您将在第 9.6 节中了解有关格式化程序和序列化数据的更多信息,但在我们进一步讨论之前,有必要稍微缩小一下并探索传统服务器端呈现的应用程序和 Web API 端点之间的相似之处。 两者相似,因此确定它们共享的模式和不同之处很重要。
在之前的 ASP.NET 版本中,微软征用了通用术语“Web API”来创建 ASP.NET Web API 框架。 正如您所料,该框架用于创建 HTTP 端点,这些端点可以返回格式化的 JSON 或 XML 以响应请求。
ASP.NET Web API 框架与 MVC 框架完全分离,尽管它使用类似的对象和范例。 他们的底层网络堆栈是完全不同的野兽,无法互操作。
在 ASP.NET Core 中,这一切都改变了。 您现在拥有一个可用于构建传统 Web 应用程序和 Web API 的框架。 相同的底层框架与 Web API 控制器、Razor 页面和带有视图的 MVC 控制器一起使用。 你自己已经看过了; 您在第 9.2 节中创建的 Web API FruitController 看起来与您在前几章中短暂看到的 MVC 控制器非常相似。
因此,即使您正在构建一个完全由 Web API 组成的应用程序,不使用 HTML 的服务器端呈现,MVC 设计模式仍然适用。 无论您是构建传统的 Web 应用程序还是 Web API,您都可以以几乎相同的方式构建您的应用程序。
现在,我希望您已经非常熟悉 ASP.NET Core 如何处理请求了。但万一您不熟悉,图 9.8 显示了框架如何处理通过中间件管道后的典型 Razor Pages 请求。 此示例显示了在传统杂货店网站上查看可用水果的请求的外观。
图 9.8 处理对传统 Razor Pages 应用程序的请求,其中视图会生成一个 HTML 响应并发送回用户。 这张图现在应该很熟悉了! |
---|
RoutingMiddleware 将查看苹果类别中列出的所有水果的请求路由到 Fruit.cshtml Razor 页面。 EndpointMiddleware 然后构造一个绑定模型,对其进行验证,将其设置为 Razor Page 的 PageModel 上的属性,并在 PageModel 基类上设置 ModelState 属性以及任何验证错误的详细信息。 页面处理程序通过调用服务、与数据库通信以及获取任何必要的数据来与应用程序模型进行交互。
最后,Razor 页面使用 PageModel 执行其 Razor 视图以生成 HTML 响应。 响应通过中间件管道返回到用户的浏览器。
如果请求来自客户端或移动应用程序,这将如何改变? 如果您想提供机器可读的 JSON 而不是 HTML,有什么不同? 如图 9.9 所示,答案是“很少”。 主要变化与从 Razor 页面切换到控制器和动作有关,但正如您在第 4 章中看到的,两种方法都使用相同的通用范式。
和以前一样,路由中间件根据传入的 URL 选择要调用的端点。 对于 API 控制器,这是一个控制器和操作,而不是 Razor 页面。
在路由之后是模型绑定,其中绑定器创建一个绑定模型并使用来自请求的值填充它。 Web API 通常接受比 Razor 页面更多格式的数据,例如 XML,但模型绑定过程与 Razor 页面请求相同。 验证也以相同的方式进行,并且 ControllerBase 基类上的 ModelState 属性会填充任何验证错误。
action 方法相当于 Razor Page 处理程序; 它以完全相同的方式与应用程序模型交互。 这是很重要的一点; 通过将应用程序的行为分离到应用程序模型中,而不是将其合并到页面和控制器本身中,您可以使用多个 UI 范例重用应用程序的业务逻辑。
提示 尽可能让您的页面处理程序和控制器尽可能简单。 将所有业务逻辑决策转移到构成应用程序模型的服务中,并使 Razor 页面和 API 控制器专注于与用户交互的机制。
图 9.9 在电子商务 ASP.NET Core Web 应用程序中调用 Web API 端点。 图的虚线部分与图 9.8 相同。 |
---|
在应用程序模型返回服务请求所需的数据(苹果类别中的水果对象)后,您会看到 API 控制器和 Razor 页面之间的第一个显着差异。 操作方法不是向要在 Razor 视图中使用的 PageModel 添加值,而是创建一个 API 模型。 这类似于 PageModel,但它不包含用于生成 HTML 视图的数据,而是包含将在响应中发回的数据
定义 视图模型和页面模型包含构建响应所需的数据和有关如何构建响应的元数据。 API 模型通常只包含要在响应中返回的数据
当我们查看 Razor Pages 应用程序时,我们将 PageModel 与 Razor 视图模板结合使用来构建最终响应。 对于 Web API 应用程序,我们将 API 模型与输出格式化程序结合使用。 顾名思义,输出格式化程序将 API 模型序列化为机器可读的响应,例如 JSON 或 XML。 输出格式化程序通过选择要返回的数据的适当表示形式,在 MVC 的 Web API 版本中形成“V”。
最后,对于 Razor Pages 应用程序,生成的响应然后通过中间件管道发送回,通过每个配置的中间件组件,然后返回给原始调用者。
希望 Razor 页面和 Web API 之间的相似之处很清楚; 大多数行为是相同的——只是反应不同。 从请求到达到与应用程序模型的交互,范式之间的一切都是相似的。
Razor 页面和 Web API 之间的大多数差异与框架在底层的工作方式关系不大,而是与如何使用不同的范例有关。 例如,在下一节中,您将了解您在第 5 章中学到的路由构造如何与 Web API 一起使用,使用属性路由。
在本节中,您将了解属性路由:将 API 控制器操作与给定路由模板相关联的机制。 您将看到如何将控制器操作与特定的 HTTP 动词(如 GET 和 POST)相关联,以及如何避免模板中的重复。
我们在第 5 章的 Razor Pages 上下文中深入介绍了路由模板,您会很高兴知道您使用与 API 控制器完全相同的路由模板。 唯一的区别是您如何指定模板:对于 Razor Pages,您使用 @page 指令,而对于 API 控制器,您使用路由属性。
注意 Razor 页面和 API 控制器都在后台使用属性路由。 替代方案,传统路由,通常与传统的 MVC 控制器和视图一起使用。 如前所述,我不推荐使用这种方法,因此我不会在本书中介绍传统路由。
使用属性路由,您可以使用属性装饰 API 控制器中的每个操作方法,并为操作方法提供关联的路由模板,如下面的清单所示。
清单 9.4 属性路由示例
public class HomeController: Controller { [Route("")] //请求 / URL 时将执行索引操作。 public IActionResult Index() { /* method implementation*/ } [Route("contact")] //当请求 /contact URL 时,将执行 Contact 操作。 public IActionResult Contact() { /* method implementation*/ } }
每个 [Route] 属性定义了一个应该与操作方法相关联的路由模板。 在提供的示例中,/ URL 直接映射到 Index 方法,而 /contact URL 映射到 Contact 方法。
属性路由将 URL 映射到特定的操作方法,但单个操作方法仍然可以有多个路由模板,因此可以对应多个 URL。 每个模板都必须使用自己的 RouteAttribute 进行声明,如此清单所示,它显示了赛车游戏的 Web API 框架。
清单 9.5 具有多个属性的属性路由
public class CarController { [Route("car/start")] [Route("car/ignition")] [Route("start-car")] public IActionResult Start() { /* method implementation*/ } [Route("car/speed/{speed}")] [Route("set-speed/{speed}")] public IActionResult SetCarSpeed(int speed) { /* method implementation*/ } }
该清单显示了两种不同的操作方法,这两种方法都可以从多个 URL 访问。 例如,当请求以下任何 URL 时,将执行 Start 方法:
/car/start /zar/ignition /start-car
这些 URL 完全独立于控制器和操作方法名称;只有 RouteAttribute 中的值很重要。
注意 默认情况下,使用 RouteAttributes 时,控制器和操作名称与 URL 或路由模板无关。
路由属性中使用的模板是标准路由模板,与第 5 章中使用的相同。您可以使用文字段,并且可以自由定义将从 URL 中提取值的路由参数,如 以前的清单。 该方法定义了两个路由模板,这两个模板都定义了一个路由参数 {speed}。
提示 在本示例中,我在每个操作上使用了多个 [Route] 属性,但最佳做法是在单个 URL 上公开您的操作。 这将使您的 API 更易于理解和其他应用程序使用。
路由参数的处理方式与 Razor 页面的处理方式相同——它们表示可以变化的 URL 段。 对于 Razor 页面,您的 RouteAttribute 模板中的路由参数可以
例如,您可以更新前面清单中的 SetCarSpeed 方法,以将 {speed} 约束为整数并默认为 20,如下所示:
[Route("car/speed/{speed=20:int}")] [Route("set-speed/{speed=20:int}")] public IActionResult SetCarSpeed(int speed)
注意 如第 5 章所述,请勿使用路由约束进行验证。例如,如果您使用无效的速度值调用前面的“set-speed/{speed=20:int}”路由,则 /set-speed/ 糟糕,您将收到 404 Not Found 响应,因为路由不匹配。 如果没有 int 约束,您将收到更合理的 400 Bad Request 响应。
如果您在第 5 章中成功理解了路由,那么使用 API 控制器进行路由应该不会让您感到意外。 当您开始使用 API 控制器的属性路由时,您可能会开始注意到的一件事是您自己重复的数量。 Razor Pages 通过使用约定来根据 Razor Page 的文件名计算路由模板,从而消除了很多重复。
幸运的是,有一些功能可以让您的生活更轻松一些。 特别是,结合路由属性和令牌替换可以帮助减少代码中的重复。
将路由属性添加到所有 API 控制器可能会有点乏味,尤其是当您主要遵循路由具有标准前缀(例如“api”或控制器名称)的约定时。 通常,您需要确保在涉及这些字符串时不会重复自己 (DRY)。 下面的清单显示了两个具有多个 [Route] 属性的操作方法。 (这仅用于演示目的。如果可以,请坚持每个动作一个!)
清单 9.6 RouteAttribute 模板中的重复
public class CarController { //多个路线模板使用相同的“api/car”前缀。 [Route("api/car/start")] [Route("api/car/ignition")] [Route("/start-car")] public IActionResult Start() { /* method implementation*/ } [Route("api/car/speed/{speed}")] [Route("/set-speed/{speed}")] public IActionResult SetCarSpeed(int speed) { /* method implementation*/ } }
这里有很多重复——你在你的大部分路线中添加了“api/car”。 据推测,如果您决定将其更改为“api/vehicles”,则必须检查每个属性并更新它。 像这样的代码要求输入一个错字!
为了减轻这种痛苦,除了动作方法之外,还可以将 RouteAttributes 应用于控制器,正如您在第 5 章中简要介绍的那样。当控制器和动作方法都具有路由属性时,该方法的整体路由模板由下式计算 结合两个模板。
清单 9.7 组合 RouteAttribute 模板
[Route("api/car")] public class CarController { [Route("start")]//结合起来给出“api/car/start”模板 [Route("ignition")]//结合起来给出“api/car/ignition”模板 [Route("/start-car")]//不合并,因为它以 / 开头; 给出“start-car”模板 public IActionResult Start() { /* method implementation*/ } [Route("speed/{speed}")]//结合起来给出“api/car/speed/{speed}”模板 [Route("/set-speed/{speed}")]//不合并,因为它以 / 开头; 给出“set-speed/{speed}”模板 public IActionResult SetCarSpeed(int speed) { /* method implementation*/ } }
以这种方式组合属性可以减少路线模板中的一些重复,并且可以更轻松地为多个操作方法添加或更改前缀(例如将“car”切换为“vehicle”)。 要忽略控制器上的 RouteAttribute 并创建绝对路由模板,请使用斜杠 (/) 开始您的操作方法路由模板。 使用控制器 RouteAttribute 可以减少很多重复,但是您可以通过使用令牌替换来做得更好。
组合属性路由的能力很方便,但如果你在路由前加上控制器的名称,或者你的路由模板总是使用动作名称,你仍然会留下一些重复。 如果您愿意,您可以进一步简化!
属性路由支持自动替换属性路由中的 [action] 和 [controller] 标记。 这些将分别替换为动作和控制器的名称(没有“控制器”后缀)。 在组合所有属性后替换标记,因此当您具有控制器继承层次结构时,这很有用。 此清单显示了如何创建 BaseController 类,您可以使用该类将一致的路由模板前缀应用于应用程序中的所有 API 控制器。
清单 9.8 RouteAttributes 中的标记替换
[Route("api/[controller]")]//您可以将属性应用于基类,派生类将继承它们。 public abstract class BaseController { } //令牌替换最后发生,因此 [controller] 替换为“car”而不是“base”。 public class CarController : BaseController { [Route("[action]")] //组合并替换标记以提供“api/car/start”模板 [Route("ignition")] //组合并替换标记以提供“api/car/ignition”模板 [Route("/start-car")] //不与基本属性结合,因为它以 / 开头,所以它仍然是“start-car” public IActionResult Start() { /* method implementation*/ } }
警告 如果您对 [controller] 或 [action] 使用令牌替换,请记住重命名类和方法将更改您的公共 API。 如果这让你担心,你可以坚持使用像“car”这样的静态字符串。
结合您在第 5 章中学到的所有内容,我们几乎涵盖了有关属性路由的所有知识。 还有一件事需要考虑:处理不同的 HTTP 请求类型,例如 GET 和 POST。
在 Razor 页面中,HTTP 动词(例如 GET 或 POST)不是路由过程的一部分。RoutingMiddleware 仅根据与 Razor 页面关联的路由模板来确定要执行的 Razor 页面。 只有在 Razor 页面即将执行时,才会使用 HTTP 谓词来决定要执行哪个页面处理程序:例如,对于 GET 谓词是 OnGet,对于 POST 谓词是 OnPost。
使用 API 控制器,事情会有所不同。 对于 API 控制器,HTTP 动词本身参与路由过程,因此 GET 请求可能被路由到一个操作,而 POST 请求可能被路由到不同的操作,即使请求使用相同的 URL。 这种模式,其中 HTTP 动词是路由的重要组成部分,在 HTTP API 设计中很常见。
例如,假设您正在构建一个 API 来管理您的日历。 您希望能够列出和创建约会。 好吧,传统的 HTTP REST 服务可能会定义以下 URL 和 HTTP 动词来实现这一点:
请注意,这两个端点使用相同的 URL; 只有 HTTP 动词不同。 到目前为止,我们使用的 [Route] 属性响应所有 HTTP 动词,这对我们没有好处——我们希望根据 URL 和动词的组合选择不同的操作。 这种模式在构建 Web API 时很常见,幸运的是,它很容易在 ASP.NET Core 中建模。
ASP.NET Core 提供了一组属性,可用于指示操作应响应的动词。 例如,
所有标准 HTTP 动词都有类似的属性,例如 DELETE 和 OPTIONS。您可以使用这些属性而不是 [Route] 属性来指定操作方法应对应于单个动词,如下面的清单所示。
清单 9.9 使用带有属性路由的 HTTP 动词属性
public class AppointmentController { //仅响应 GET /appointments 执行 [HttpGet("/appointments")] public IActionResult ListAppointments() { /* method implementation */ } //仅响应 POST /appointments 执行 [HttpPost("/appointments")] public IActionResult CreateAppointment() { /* method implementation */ } }
如果您的应用程序接收到与操作方法的路由模板匹配的请求,但与所需的 HTTP 动词不匹配,您将收到 405 Method not allowed 错误响应。 例如,如果您向前面列表中的 /appointments URL 发送 DELETE 请求,您将收到 405 错误响应。
从 ASP.NET Core 的第一天开始,属性路由就与 API 控制器一起使用,因为它允许对应用程序公开的 URL 进行严格控制。 当您构建 API 控制器时,您会发现自己会重复编写一些代码。 ASP.NET Core 2.1 中引入的 [ApiController] 属性旨在为您处理其中的一些问题并减少您需要的样板数量。
在本节中,您将了解 [ApiController] 属性以及它如何减少创建一致的 Web API 控制器所需编写的代码量。您将了解它应用的约定、它们为何有用以及 如果需要,如何关闭它们。
[ApiController] 属性是在 .NET Core 2.1 中引入的,以简化创建 Web API 控制器的过程。 要了解它的作用,看一个示例,了解如何编写没有 [ApiController] 属性的 Web API 控制器,并将其与使用该属性实现相同目的所需的代码进行比较,这很有用。
清单 9.10 创建一个没有 [ApiController] 属性的 Web API 控制器
public class FruitController : ControllerBase { List<string> _fruit = new List<string> { "Pear", "Lemon", "Peach" }; [HttpPost("fruit")] public ActionResult Update([FromBody] UpdateModel model) { if (!ModelState.IsValid) { return BadRequest( new ValidationProblemDetails(ModelState)); } if (model.Id < 0 || model.Id > _fruit.Count) { return NotFound(new ProblemDetails() { Status = 404, Title = "Not Found", Type = "https://tools.ietf.org/html/rfc7231" + "#section-6.5.4", }); } _fruit[model.Id] = model.Name; return Ok(); } public class UpdateModel { public int Id { get; set; } [Required] public string Name { get; set; } } }
此示例演示了与 Web API 控制器一起使用的许多常见功能和模式:
清单 9.10 中的代码代表了您在 .NET Core 2.1 之前的 ASP.NET Core API 控制器中可能看到的内容。 在 .NET Core 2.1 中引入了 [ApiController] 属性(以及后续版本中的后续改进),使相同的代码更加简单,如下面的清单所示。
清单 9.11 创建一个带有 [ApiController] 属性的 Web API 控制器
[ApiController] //添加 [ApiController] 属性适用于 API 控制器常见的几个约定。 public class FruitController : ControllerBase { List<string> _fruit = new List<string> { "Pear", "Lemon", "Peach" }; [HttpPost("fruit")] public ActionResult Update(UpdateModel model) //[FromBody] 属性假定用于复杂的操作方法参数。 { //自动检查模型验证,如果无效,则返回 400 响应。 if (model.Id < 0 || model.Id > _fruit.Count) { //错误状态代码会自动转换为 ProblemDetails 对象。 return NotFound(); } _fruit[model.Id] = model.Name; return Ok(); } public class UpdateModel { public int Id { get; set; } [Required] public string Name { get; set; } } }
如果您将清单 9.10 与清单 9.11 进行比较,您会发现清单 9.10 中的所有粗体代码都可以删除并替换为清单 9.11 中的 [ApiController] 属性。 [ApiController] 属性自动将几个约定应用于您的控制器:
[ApiController] 属性的一个关键特性是使用问题详细信息格式在所有控制器中以一致的格式返回错误。3 典型的 ProblemDetails 对象如下所示,其中显示了为无效请求自动生成的示例 ValidationProblemDetails 对象 :
{ type: "https://tools.ietf.org/html/rfc7231#section-6.5.1" title: "One or more validation errors occurred." status: 400 traceId: "|17a2706d-43736ad54bed2e65." errors: { name: ["The name field is required."] } }
[ApiController] 约定可以显着减少您必须编写的样板代码量。 它们还确保整个应用程序的一致性。 例如,当收到错误请求时,您可以确保所有控制器都将返回相同的错误类型 ValidationProblemDetails(ProblemDetails 的子类型)。
将所有错误转换为 ProblemDetails
[ApiController] 属性可确保您的 API 控制器返回的所有错误响应都转换为 ProblemDetails 对象,从而使您的应用程序中的错误响应保持一致。
唯一的问题是你的 API 控制器不是唯一可能产生错误的东西。 例如,如果接收到的 URL 与控制器中的任何操作都不匹配,我们在第 3 章中讨论的管道末端中间件将生成 404 Not Found 响应。 由于此错误是在 API 控制器之外生成的,因此不会使用 ProblemDetails。 同样,当您的代码引发异常时,您也希望它作为 ProblemDetails 对象返回,但默认情况下不会发生这种情况。
在第 3 章中,我描述了几种类型的错误处理中间件,您可以使用它们来处理这些情况,但处理所有边缘情况可能会很复杂。 我更喜欢使用社区创建的包 Hellang.Middleware.ProblemDetails,它会为您解决这个问题。 你可以在我的博客 http://mng.bz/Gx7D 上了解如何使用这个包。
在 ASP.NET Core 中很常见,如果您遵循约定而不是试图与它们作斗争,您的工作效率最高。 但是,如果您不喜欢某些约定,或者想要自定义它们,您可以轻松地做到这一点。
您可以通过在 Startup.cs 文件中的 AddControllers() 方法返回的 IMvcBuilder 对象上调用 ConfigureApiBehaviorOptions() 来自定义应用程序使用的约定。 例如,您可以禁用验证失败时的自动 400 响应,如下面的清单所示。
清单 9.12 自定义 [ApiAttribute] 行为
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddControllers() //通过提供配置 lambda 来控制应用哪些约定。 .ConfigureApiBehaviorOptions(options => { //这将禁用无效请求的自动 400 响应 options.SuppressModelStateInvalidFilter = true; }) } // ... }
自定义 ASP.NET Core 各个方面的能力是使其与以前版本的 ASP.NET 不同的功能之一。 ASP.NET Core 使用以下两种机制之一配置其绝大多数内部组件 — 依赖注入或在将服务添加到应用程序时配置 Options 对象,您将在第 10 章(依赖注入)和第 11 章中看到( 选项对象)。
在下一节中,您将学习如何控制 Web API 控制器返回的数据格式——无论是 JSON、XML 还是其他自定义格式。
这将我们带到本章的最后一个主题:格式化响应。 如今,API 控制器返回 JSON 很常见,但情况并非总是如此。 在本节中,您将了解内容协商以及如何启用其他输出格式,例如 XML。 您还将了解 ASP.NET Core 3.0 的 JSON 格式化程序的一项重要更改。
考虑这种情况:您已经创建了一个用于返回汽车列表的 Web API 操作方法,如下面的清单所示。 它在您的应用程序模型上调用一个方法,该方法将数据列表交还给控制器。 现在您需要格式化响应并将其返回给调用者。
清单 9.13 返回汽车列表的 Web API 控制器
[ApiController] public class CarsController : Controller { [HttpGet("api/cars")] //该操作通过对 GET /api/cars 的请求执行。 public IEnumerable<string> ListCars()//包含数据的 API 模型是一个 IEnumerable<string>。 { //该数据通常会从应用程序模型中获取。 return new string[] { "Nissan Micra", "Ford Focus" }; } }
您在 9.2 节中看到可以直接从操作方法返回数据,在这种情况下,中间件会对其进行格式化并将格式化后的数据返回给调用者。但是中间件如何知道使用哪种格式? 毕竟,您可以将其序列化为 JSON、XML,甚至使用简单的 ToString() 调用。
确定要发送给客户端的数据格式的过程通常称为内容协商 (conneg)。 在高层次上,客户端发送一个标头,指示它可以理解的内容类型 - Accept 标头 - 服务器选择其中一个,格式化响应,并在响应中发送一个 content-type 标头,指示它是哪种类型 选择。
您不会被迫只发送客户端期望的内容类型,而且在某些情况下,您甚至可能无法处理它请求的类型。 如果请求规定它只能接受 Excel 电子表格怎么办? 即使这是请求包含的唯一内容类型,您也不太可能支持它。
当您从操作方法返回 API 模型时,无论是直接(如清单 9.13 所示)还是通过 OkResult 或其他 StatusCodeResult,ASP.NET Core 总是会返回一些内容。 如果它不能接受 Accept 标头中规定的任何类型,它将默认退回到返回 JSON。 图 9.10 显示,即使请求了 XML,API 控制器仍将响应格式化为 JSON。
注意 在以前的 ASP.NET 版本中,对象使用 PascalCase 序列化为 JSON,其中属性以大写字母开头。 在 ASP.NET Core 中,对象默认使用 camelCase 进行序列化,其中属性以小写字母开头。
图 9.10 尽管请求是使用 text/xml 的 Accept 标头发出的,但返回的响应是 JSON,因为服务器未配置为返回 XML。 |
---|
无论数据是如何发送的,它都由 IOutputFormatter 实现进行序列化。ASP.NET Core 附带了数量有限的开箱即用的输出格式化程序,但与往常一样,添加额外的格式化程序或更改默认值的工作方式很容易。
与大多数 ASP.NET Core 一样,Web API 格式化程序是完全可定制的。默认情况下,仅配置纯文本 (text/plain)、HTML (text/html) 和 JSON (application/json) 的格式化程序。 鉴于 SPA 和移动应用程序的常见用例,这将使您走得更远。 但有时您需要能够以不同的格式返回数据,例如 XML。
Newtonsoft.Json vs. System.Text.Json
Newtonsoft.Json,也称为 Json.NET,长期以来一直是在 .NET 中使用 JSON 的规范方式。 它与所有版本的 .NET 兼容,几乎所有 .NET 开发人员都会熟悉它。 它的影响如此之大,以至于 ASP.NET Core 都依赖它!
这一切随着 ASP.NET Core 3.0 中新库 System.Text.Json 的引入而改变,该库专注于性能。 从 ASP.NET Core 3.0 开始,ASP.NET Core 默认使用 System.Text.Json 而不是 Newtonsoft.Json。
库之间的主要区别在于 System.Text.Json 对其 JSON 非常挑剔。 它通常只会反序列化符合其预期的 JSON。 例如,System.Text.Json 不会反序列化在字符串周围使用单引号的 JSON; 你必须使用双引号。
如果您正在创建一个新应用程序,这通常不是问题——您很快就会学会生成正确的 JSON。 但是,如果您从 ASP.NET Core 2.0 迁移应用程序或从第三方接收 JSON,这些限制可能是真正的绊脚石。
幸运的是,您可以轻松切换回 Newtonsoft.Json 库。 将 Microsoft.AspNetCore.Mvc.NewtonsoftJson 包安装到您的项目中,并将 Startup.cs 中的 AddControllers() 方法更新为以下内容:
services.AddControllers() .AddNewtonsoftJson();
您可以通过添加输出格式化程序将 XML 输出添加到您的应用程序。 通过自定义从 AddControllers() 返回的 IMvcBuilder 对象,您可以在 Startup.cs 中配置应用程序的格式化程序。 要添加 XML 输出格式化程序,5 使用以下命令:
services.AddControllers() .AddXmlSerializerFormatters();
通过这个简单的更改,您的 API 控制器现在可以将响应格式化为 XML。 在启用 XML 支持的情况下运行如图 9.10 所示的相同请求意味着应用程序将尊重 text/xml 接受标头。 格式化程序根据请求将字符串数组序列化为 XML,而不是默认为 JSON,如图 9.11 所示。
图 9.11 添加 XML 输出格式化程序后,text/xml Accept 标头得到尊重,响应可以序列化为 XML。 |
---|
这是内容协商的一个示例,其中客户端指定了它可以处理的格式,服务器根据它可以生成的内容选择其中一种。 这种方法是 HTTP 协议的一部分,但是在 ASP.NET Core 中依赖它时需要注意一些怪癖。 你不会经常遇到这些,但如果你在它们击中你时没有意识到它们,它们可能会让你挠头好几个小时!
内容协商是客户端使用 Accept 标头说明它可以接受哪些类型的数据,然后服务器选择它可以处理的最佳数据类型。 一般来说,这会如您所愿:服务器使用客户端可以理解的类型格式化数据。
ASP.NET Core 实现有一些值得牢记的特殊情况:
这些默认设置相对合理,但是如果您不知道它们,它们肯定会咬您一口。 尤其是最后一点,对来自浏览器的请求的响应实际上总是格式化为 JSON,当我尝试在本地测试 XML 请求时,这肯定让我很吃惊!
正如您现在所期望的那样,所有这些规则都是可配置的; 如果它不符合您的要求,您可以轻松更改应用程序中的默认行为。 例如,来自 Startup.cs 的以下清单显示了如何强制中间件尊重浏览器的 Accept 标头,并删除字符串的 text/plain 格式化程序。
清单 9.14 自定义 MVC 以尊重 Web API 中浏览器的 Accept 标头
public void ConfigureServices(IServiceCollection services) { //AddControllers 有一个采用 lambda 函数的重载。 services.AddControllers(options => { //默认情况下为 False,还可以设置许多其他属性。 options.RespectBrowserAcceptHeader = true; //删除将字符串格式化为文本/纯文本的输出格式化程序 options.OutputFormatters.RemoveType<StringOutputFormatter>(); }); }
在大多数情况下,无论您是构建 SPA 还是移动应用程序,conneg 都应该开箱即用。 在某些情况下,您可能会发现您需要绕过特定操作方法的常用连接机制,并且有很多方法可以实现这一点,但我不会在本书中介绍它们,因为我发现我很少需要 使用它们。 有关详细信息,请参阅 Microsoft 的“在 ASP.NET Core Web API 中格式化响应数据”文档:http://mng.bz/zx11。
这将我们带到了关于 Web API 的本章的结尾,以及本书的第 1 部分! 这是一次非常紧张的 ASP.NET Core 之旅,重点关注 Razor Pages 和 MVC 模式。 通过做到这一点,您现在拥有开始使用 Razor Pages 构建简单应用程序或为您的 SPA 或移动应用程序创建 Web API 服务器所需的所有知识。
在第 2 部分中,您将进入一些有趣的主题:您将了解构建完整应用程序所需的详细信息,例如将用户添加到应用程序、将数据保存到数据库以及部署应用程序。
在第 10 章中,我们将了解 ASP.NET Core 中的依赖注入以及它如何帮助创建松散耦合的应用程序。 您将学习如何使用容器注册 ASP.NET Core 框架服务并将您自己的类设置为依赖注入服务。 最后,您将看到如何用第三方替代品替换内置容器。