ASP.NET Core in Action 5 Creating a JSON API with minimal APIs

5 Creating a JSON API with minimal APIs
5 使用最少的 API 创建 JSON API

This chapter covers

本章涵盖

  • Creating a minimal API application to return JSON to clients
    创建最小 API 应用程序以将 JSON 返回给客户端

  • Generating responses with IResult
    生成响应IResult

  • Using filters to perform common actions like validation
    使用筛选器执行常见作,如验证

  • Organizing your APIs with route groups
    使用路由组组织 API

So far in this book you’ve seen several examples of minimal API applications that return simple Hello World! responses. These examples are great for getting started, but you can also use minimal APIs to build full-featured HTTP API applications. In this chapter you’ll learn about HTTP APIs, see how they differ from a server-rendered application, and find out when to use them.

到目前为止,在本书中,您已经看到了几个最小 API 应用程序的示例,这些应用程序返回简单的 Hello World!。这些示例非常适合入门,但您也可以使用最少的 API 来构建功能齐全的 HTTP API 应用程序。在本章中,您将了解 HTTP API,了解它们与服务器呈现的应用程序有何不同,并了解何时使用它们。

Section 5.2 starts by expanding on the minimal API applications you’ve already seen. You’ll explore some basic routing concepts and show how values can be extracted from the URL automatically. Then you’ll learn how to handle additional HTTP verbs such as POST and PUT, and explore various ways to define your APIs.

在5.2 中节,首先扩展了您已经见过的最小 API 应用程序。您将探索一些基本的路由概念,并展示如何从 URL 中自动提取值。然后,您将学习如何处理其他 HTTP 动词(如 POST 和 PUT),并探索定义 API 的各种方法。

In section 5.3 you’ll learn about the different return types you can use with minimal APIs. You’ll see how to use the Results and TypedResults helper classes to easily create HTTP responses that use status codes like 201 Created and 404 Not Found. You’ll also learn how to follow web standards for describing your errors by using the built-in support for Problem Details.

在5.3 中节,您将了解可以与最少的 API 一起使用的不同返回类型。您将看到如何使用Results 和 TypedResults 帮助程序类轻松创建使用状态代码(如 201 Created 和 404 Not Found)的 HTTP 响应。您还将学习如何使用对 Problem Details 的内置支持来遵循 Web 标准来描述错误。

Section 5.4 introduces one of the big features added to minimal APIs in .NET 7: filters. You can use filters to build a mini pipeline (similar to the middleware pipeline from chapter 4) for each of your endpoints. Like middleware, filters are great for extracting common code from your endpoint handlers, making your handlers easier to read.

在5.3 中节,介绍了 .NET 7 中添加到最小 API 的重要功能之一:过滤器。您可以使用过滤器为每个终端节点构建一个微型管道(类似于第 4 章中的中间件管道)。与中间件一样,筛选器非常适合从终结点处理程序中提取常见代码,从而使处理程序更易于阅读。

You’ll learn about the other big .NET 7 feature for minimal APIs in section 5.5: route groups. You can use route groups to reduce the duplication in your minimal APIs, extracting common routing prefixes and filters, making your APIs easier to read, and reducing boilerplate. In conjunction with filters, route groups address many of the common complaints raised against minimal APIs when they were released in .NET 6.

您将在 5.5:路由组 部分了解最小 API 的其他重要 .NET 7 功能。您可以使用路由组来减少最小 API 中的重复,提取常见的路由前缀和筛选条件,使您的 API 更易于阅读,并减少样板文件。与筛选器结合使用,路由组可以解决在 .NET 6 中发布最小 API 时针对最小 API 提出的许多常见投诉。

One great aspect of ASP.NET Core is the variety of applications you can create with it. The ability to easily build a generalized HTTP API presents the possibility of using ASP.NET Core in a greater range of situations than can be achieved with traditional web apps alone. But should you build an HTTP API, and if so, why? In the first section of this chapter, I’ll go over some of the reasons why you may—or may not—want to create a web API.

ASP.NET Core 的一个重要方面是您可以使用它创建的各种应用程序。与单独使用传统 Web 应用程序相比,轻松构建通用 HTTP API 的能力为在更大范围内使用 ASP.NET Core 提供了可能性。但是,您应该构建 HTTP API,如果是这样,为什么?在本章的第一部分中,我将介绍您可能希望(也可能不希望)创建 Web API 的一些原因。

5.1 What is an HTTP API, and when should you use one?

5.1 什么是 HTTP API,何时应使用 HTTP API?

Traditional web applications handle requests by returning HTML, which is displayed to the user in a web browser. You can easily build applications like that by using Razor Pages to generate HTML with Razor templates, as you’ll learn in part 2 of this book. This approach is common and well understood, but the modern application developer has other possibilities to consider (figure 5.1), as you first saw in chapter 2.
传统的 Web 应用程序通过返回 HTML 来处理请求,HTML 在 Web 浏览器中显示给用户。通过使用 Razor Pages 通过 Razor 模板生成 HTML,可以轻松构建此类应用程序,您将在本书的第 2 部分中学习。这种方法很常见,也很容易理解,但现代应用程序开发人员还有其他可能性需要考虑(图 5.1),正如您在第 2 章中第一次看到的那样。

alt text

Figure 5.1 Modern developers have to consider several consumers of their applications. As well as traditional users with web browsers, these users could be single-page applications, mobile applications, or other apps.
图 5.1 现代开发人员必须考虑其应用程序的多个使用者。除了使用 Web 浏览器的传统用户外,这些用户还可以是单页应用程序、移动应用程序或其他应用程序。

Client-side single-page applications (SPAs) have become popular in recent years with the development of frameworks such as Angular, React, and Vue. These frameworks typically use JavaScript running in a web browser to generate the HTML that users see and interact with. The server sends this initial JavaScript to the browser when the user first reaches the app. The user’s browser loads the JavaScript and initializes the SPA before loading any application data from the server.

近年来,随着 Angular、React 和 Vue 等框架的发展,客户端单页应用程序 (SPA) 变得流行起来。这些框架通常使用在 Web 浏览器中运行的 JavaScript 来生成用户看到并与之交互的 HTML。当用户首次访问应用程序时,服务器会将此初始 JavaScript 发送到浏览器。用户的浏览器在从服务器加载任何应用程序数据之前加载 JavaScript 并初始化 SPA。

Note Blazor WebAssembly is an exciting new SPA framework. Blazor lets you write an SPA that runs in the browser like other SPAs, but it uses C# and Razor templates instead of JavaScript by using the new web standard, WebAssembly. I don’t cover Blazor in this book, so to find out more, I recommend Blazor in Action, by Chris Sainty (Manning, 2022).

注意:Blazor WebAssembly 是一个令人兴奋的新 SPA 框架。Blazor 允许您编写一个在浏览器,但它使用新的 Web 标准 WebAssembly 使用 C# 和 Razor 模板,而不是 JavaScript。我在这本书中没有介绍 Blazor,因此要了解更多信息,我推荐 Chris Sainty 的 Blazor in Action(曼宁,2022 年)。

Once the SPA is loaded in the browser, communication with a server still occurs over HTTP, but instead of sending HTML directly to the browser in response to requests, the server-side application sends data—normally, in the ubiquitous JavaScript Object Notation (JSON) format—to the client-side application. Then the SPA parses the data and generates the appropriate HTML to show to a user, as shown in figure 5.2. The server-side application endpoint that the client communicates with is sometimes called an HTTP API, a JSON API, or a REST API, depending on the specifics of the API’s design.

在浏览器中加载 SPA 后,与服务器的通信仍通过 HTTP 进行,但服务器端应用程序不是直接向浏览器发送 HTML 以响应请求,而是将数据(通常以无处不在的 JavaScript 对象表示法 (JSON) 格式)发送到客户端应用程序。然后,SPA 解析数据并生成相应的 HTML 以向用户显示,如图 5.2 所示。客户端与之通信的服务器端应用程序终端节点有时称为 HTTP API、JSON API 或 REST API,具体取决于 API 设计的具体情况。

alt text

Figure 5.2 A sample client-side SPA using Blazor WebAssembly. The initial requests load the SPA files into the browser, and subsequent requests fetch data from a web API, formatted as JSON.
图 5.2 使用 Blazor WebAssembly 的客户端 SPA 示例。初始请求将 SPA 文件加载到浏览器中,后续请求从 Web API 获取数据,格式为 JSON。

Definition An HTTP API exposes multiple URLs via HTTP that can be used to access or change data on a server. It typically returns data using the JSON format. HTTP APIs are sometimes called web APIs, but as web API refers to a specific technology in ASP.NET Core, in this book I use HTTP API to refer to the generic concept.
定义 HTTP API 通过 HTTP 公开多个 URL,可用于访问或更改服务器上的数据。它通常使用 JSON 格式返回数据。HTTP API 有时也称为 Web API,但由于 Web API 指的是 ASP.NET Core 中的特定技术,因此在本书中,我使用 HTTP API 来指代通用概念。

These days, mobile applications are common and, from the server application’s point of view, similar to client-side SPAs. A mobile application typically communicates with a server application by using an HTTP API, receiving data in JSON format, just like an SPA. Then it modifies the application’s UI depending on the data it receives.

如今,移动应用程序很常见,从服务器应用程序的角度来看,它类似于客户端 SPA。移动应用程序通常使用 HTTP API 与服务器应用程序通信,以 JSON 格式接收数据,就像 SPA 一样。然后,它根据接收到的数据修改应用程序的 UI。

One final use case for an HTTP API is where your application is designed to be partially or solely consumed by other backend services. Imagine that you’ve built a web application to send emails. By creating an HTTP API, you can allow other application developers to use your email service by sending you an email address and a message. Virtually all languages and platforms have access to an HTTP library they could use to access your service from code.

HTTP API 的最后一个用例是,您的应用程序被设计为部分或全部由其他后端服务使用。假设您已经构建了一个用于发送电子邮件的 Web 应用程序。通过创建 HTTP API,您可以允许其他应用程序开发人员通过向您发送电子邮件地址和消息来使用您的电子邮件服务。几乎所有语言和平台都可以访问 HTTP 库,它们可以使用该库从代码访问您的服务。

That’s all there is to an HTTP API: it exposes endpoints (URLs) that client applications can send requests to and retrieve data from. These endpoints are used to power the behavior of the client apps, as well as to provide all the data the client apps need to display the correct interface to a user.

这就是 HTTP API 的全部内容:它公开了客户端应用程序可以向其发送请求和检索数据的端点 (URL)。这些端点用于支持客户端应用程序的行为,以及提供客户端应用程序向用户显示正确界面所需的所有数据。

Note You have even more options when it comes to creating APIs in ASP.NET Core. You can create remote procedure call APIs using gRPC, for example, or provide an alternative style of HTTP API using the GraphQL standard. I don’t cover those technologies in this book, but you can read about gRPC at https://docs.microsoft.com/aspnet/core/grpc and find out about GraphQL in Building Web APIs with ASP.NET Core, by Valerio De Sanctis (Manning, 2023).
注意:在 ASP.NET Core 中创建 API 时,您有更多选择。例如,您可以使用 gRPC 创建远程过程调用 API,或使用 GraphQL 标准提供 HTTP API 的替代样式。我不会在本书中介绍这些技术,但您可以在 https://docs.microsoft.com/aspnet/core/grpc 阅读有关 gRPC 的信息 ,并在 Valerio De Sanctis(Manning,2023 年)撰写的使用 ASP.NET Core 构建 Web API 中了解 GraphQL。

Whether you need or want to create an HTTP API for your ASP.NET Core application depends on the type of application you want to build. Perhaps you’re familiar with client-side frameworks, or maybe you need to develop a mobile application, or you already have an SPA build pipeline configured. In each case, you’ll most likely want to add HTTP APIs for the client apps to access your application.

您是否需要或想要为 ASP.NET Core 应用程序创建 HTTP API 取决于要构建的应用程序类型。也许您熟悉客户端框架,或者您需要开发移动应用程序,或者您已经配置了 SPA 构建管道。在每种情况下,您很可能希望为客户端应用程序添加 HTTP API 以访问您的应用程序。

One selling point for using an HTTP API is that it can serve as a generalized backend for all your client applications. You could start by building a client-side application that uses an HTTP API. Later, you could add a mobile app that uses the same HTTP API, making little or no modification to your ASP.NET Core code.

使用 HTTP API 的一个卖点是它可以用作所有客户端应用程序的通用后端。您可以从构建使用 HTTP API 的客户端应用程序开始。稍后,您可以添加使用相同 HTTP API 的移动应用程序,对 ASP.NET Core 代码进行少量修改或不进行修改。

If you’re new to web development, HTTP APIs can also be easier to understand initially, as they typically return only JSON. Part 1 of this book focuses on minimal APIs so that you can focus on the mechanics of ASP.NET Core without needing to write HTML or CSS.

如果您不熟悉 Web 开发,HTTP API 最初也更容易理解,因为它们通常只返回 JSON。本书的第 1 部分重点介绍最少的 API,以便您可以专注于 ASP.NET Core 的机制,而无需编写 HTML 或 CSS。

In part 3, you’ll learn how to use Razor Pages to create server-rendered applications instead of minimal APIs. Server-rendered applications can be highly productive. They’re generally recommended when you have no need to call your application from outside a web browser or when you don’t want or need to make the effort of configuring a client-side application.

在第 3 部分中,您将学习如何使用 Razor Pages 创建服务器呈现的应用程序,而不是最少的 API。服务器渲染的应用程序可以非常高效。当您不需要从 Web 浏览器外部调用应用程序,或者当您不想或不需要配置客户端应用程序时,通常建议使用它们。

Note Although there’s been an industry shift toward client-side frameworks, server-side rendering using Razor is still relevant. Which approach you choose depends largely on your preference for building HTML applications in the traditional manner versus using JavaScript (or Blazor!) on the client.
注意 尽管行业已经转向客户端框架,但使用 Razor 的服务器端渲染仍然很重要。选择哪种方法在很大程度上取决于您在传统方式与在客户端上使用 JavaScript(或 Blazor)的比较。

Having said that, whether to use HTTP APIs in your application isn’t something you necessarily have to worry about ahead of time. You can always add them to an ASP.NET Core app later in development, as the need arises.

话虽如此,是否在应用程序中使用 HTTP API 并不是您必须提前担心的事情。您始终可以在以后的开发过程中根据需要将它们添加到 ASP.NET Core 应用程序中。

SPAs with ASP.NET Core
具有 ASP.NET Core 的 SPA
The cross-platform, lightweight design of ASP.NET Core means that it lends itself well to acting as a backend for your SPA framework of choice. Given the focus of this book and the broad scope of SPAs in general, I won’t be looking at Angular, React, or other SPAs here. Instead, I suggest checking out the resources appropriate to your chosen SPA. Books are available from Manning for all the common client-side JavaScript frameworks, as well as Blazor:
ASP.NET Core 的跨平台轻量级设计意味着它非常适合充当所选 SPA 框架的后端。鉴于本书的重点和 SPA 的广泛范围,我不会在这里讨论 Angular、React 或其他 SPA。相反,我建议查看适用于您选择的 SPA 的资源。Manning 提供了适用于所有常见客户端 JavaScript 框架以及 Blazor 的书籍:
· React in Action, by Mark Tielens Thomas (Manning, 2018)
React in Action,作者:Mark Tielens Thomas(曼宁出版社,2018 年)
· Angular in Action, by Jeremy Wilken (Manning, 2018)
Angular in Action,作者:Jeremy Wilken(曼宁,2018 年)
· Vue.js in Action, by Erik Hanchett with Benjamin Listwon (Manning, 2018)
Vue.js in Action,埃里克·汉切特 (Erik Hanchett) 和本杰明·利斯特旺 (Benjamin Listwon) 著(曼宁出版社,2018 年)
· Blazor in Action, by Chris Sainty (Manning, 2022)
Blazor in Action,作者:Chris Sainty(曼宁,2022 年)

After you’ve established that you need an HTTP API for your application, creating one is easy, as it’s the default application type in ASP.NET Core! In the next section we look at various ways you can create minimal API endpoints and ways to handle multiple HTTP verbs.

在您确定应用程序需要 HTTP API 后,创建一个 API 很容易,因为它是 ASP.NET Core 中的默认应用程序类型!在下一节中,我们将介绍创建最小 API 端点的各种方法以及处理多个 HTTP 动词的方法。

5.2 Defining minimal API endpoints

5.2 定义最小 API 端点

Chapters 3 and 4 gave you an introduction to basic minimal API endpoints. In this section, we’ll build on those basic apps to show how you can handle multiple HTTP verbs and explore various ways to write your endpoint handlers.

第 3 章和第 4 章介绍了基本的最小 API 端点。在本节中,我们将以这些基本应用程序为基础,展示如何处理多个 HTTP 动词,并探索编写终端节点处理程序的各种方法。

5.2.1 Extracting values from the URL with routing

5.2.1 使用路由 从 URL 中提取值

You’ve seen several minimal API applications in this book, but so far, all the examples have used fixed paths to define the APIs, as in this example:

您在本书中已经看到了几个最小的 API 应用程序,但到目前为止,所有示例都使用固定路径来定义 API,如以下示例所示:

app.MapGet("/", () => "Hello World!");
app.MapGet("/person", () => new Person("Andrew", "Lock"));

These two APIs correspond to the paths / and /person, respectively. This basic functionality is useful, but typically you need some of your APIs to be more dynamic. It’s unlikely, for example, that the /person API would be useful in practice, as it always returns the same Person object. What might be more useful is an API to which you can provide the user’s first name, and the API returns all the users with that name.

这两个 API 分别对应于路径 / 和 /person。此基本功能很有用,但通常需要某些 API 更加动态。例如,/person API 在实践中不太可能有用,因为它总是返回相同的 Person 对象。可能更有用的是 API,您可以向其提供用户的名字,并且 API 会返回具有该名称的所有用户。

You can achieve this goal by using parameterized routes for your API definitions. You can create a parameter in a minimal API route using the expression {someValue}, where someValue is any name you choose. The value will be extracted from the request URL’s path and can be used in the lambda function endpoint.

您可以通过对 API 定义使用参数化路由来实现此目标。您可以在最小使用表达式 {someValue} 的 API 路由,其中 someValue 是您选择的任何名称。该值将从请求 URL 的路径中提取,并可在 lambda 函数终端节点中使用。

Note I introduce only the basics of extracting values from routes in this chapter. You’ll learn a lot more about routing in chapter 6, including why we use routing and how it fits into the ASP.NET Core pipeline, as well as the syntax you can use.
注意 在本章中,我只介绍了从 routes 中提取值的基础知识。在第 6 章中,您将了解有关 routing 的更多信息,包括我们为什么使用 routing、它如何适应 ASP.NET Core 管道,以及您可以使用的语法。

If you create an API using the route template /person/{name}, for example, and send a request to the path /person/Andrew, the name parameter will have the value "Andrew". You can use this feature to build more useful APIs, such as the one shown in the following listing.

如果您使用路由模板创建 API /person/{name} 并向路径 /person/Andrew 发送请求,则 name 参数将具有值 “Andrew”。您可以使用此功能来构建更有用的 API,例如以下清单中所示的 API。

Listing 5.1 A minimal API that uses a value from the URL
清单 5.1 一个最小的 API,它使用网址

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
var people = new List<Person> ❶
{ ❶
    new("Tom", "Hanks"), ❶
    new("Denzel", "Washington"), ❶
    new("Leondardo", "DiCaprio"), ❶
    new("Al", "Pacino"), ❶
    new("Morgan", "Freeman"), ❶
}; ❶
app.MapGet("/person/{name}", (string name) => ❷
people.Where(p => p.FirstName.StartsWith(name))); ❸

app.Run();

public record Person(string FirstName, string LastName);

❶ Creates a list of people as the data for the API
创建人员列表作为 API 的数据

❷ The route is parameterized to extract the name from the URL.
路由参数化以从 URL 中提取名称。

❸ The extracted value can be injected into the lambda handler.
提取的值可以注入到 lambda 处理程序中。

If you send a request to /person/Al for the app defined in listing 5.1, the name parameter will have the value "Al", and the API will return the following JSON:
如果您向 /person/Al 发送列表 5.1 中定义的应用程序的请求,则 name 参数的值将为 “Al”,并且 API 将返回以下 JSON:

[{"firstName":"Al","lastName":"Pacino"}]

Note By default, minimal APIs serialize C# objects to JSON. You’ll see how to return other types of results in section 5.3.
注意 默认情况下,最小 API 将 C# 对象序列化为 JSON。您将在 5.3 节 中看到如何返回其他类型的结果。

The ASP.NET Core routing system is quite powerful, and we’ll explore it in more detail in chapter 6. But with this simple capability, you can already build more complex applications.
ASP.NET Core 路由系统非常强大,我们将在第 6 章中更详细地探讨它。但借助这个简单的功能,您已经可以构建更复杂的应用程序。

5.2.2 Mapping verbs to endpoints

5.2.2 将动词映射到端点

So far in this book we’ve defined all our minimal API endpoints by using the MapGet() function. This function matches requests that use the GET HTTP verb. GET is the most-used verb; it’s what a browser uses when you enter a URL in the address bar of your browser or follow a link on a web page.

到目前为止,在本书中,我们已经使用 MapGet() 函数定义了所有最小 API 端点 。此函数匹配使用 GET HTTP 动词的请求。GET 是最常用的动词;当您在浏览器的地址栏中输入 URL 或点击网页上的链接时,浏览器会使用它。

You should use GET only to get data from the server, however. You should never use it to send data or to change data on the server. Instead, you should use an HTTP verb such as POST or DELETE. You generally can’t use these verbs by navigating web pages in the browser, but they’re easy to send from a client-side SPA or mobile app.

但是,您应该只使用 GET 从服务器获取数据。切勿使用它来发送数据或更改服务器上的数据。 相反,您应该使用 HTTP 动词,例如 POST 或 DELETE。您通常不能使用这些动词,但它们很容易从客户端 SPA 或移动应用程序发送。

Tip If you’re new to web programming or are looking for a refresher, Mozilla Developer Network (MDN), maker of the Firefox web browser, has a good introduction to HTTP at http://mng.bz/KeMK.
提示 如果您是 Web 编程的新手或正在寻找复习者,Firefox Web 浏览器的制造商 Mozilla Developer Network (MDN) 在 http://mng.bz/KeMK 上对 HTTP 进行了很好的介绍。

In theory, each of the HTTP verbs has a well-defined purpose, but in practice, you may see apps that only ever use POST and GET. This is often fine for server-rendered applications like Razor Pages, as it’s typically simpler, but if you’re creating an API, I recommend that you use the HTTP verbs with the appropriate semantics wherever possible.

理论上,每个 HTTP 动词都有一个明确定义的用途,但在实践中,您可能会看到仅使用 POST 和 GET 的应用程序。这通常适用于服务器呈现的应用程序(如 Razor Pages),因为它通常更简单,但如果您正在创建 API,我建议您尽可能使用具有适当语义的 HTTP 动词。

You can define endpoints for other verbs with minimal APIs by using the appropriate Map functions. To map a POST endpoint, for example, you’d use MapPost(). Table 5.1 shows the minimal API Map methods available, the corresponding HTTP verbs, and the typical semantic expectations of each verb on the types of operations that the API performs.

您可以使用适当的 Map 函数,使用最少的 API 为其他谓词定义端点。例如,要映射 POST 终端节点,您可以使用 MapPost()。表 5.1 显示了可用的最小 API Map 方法、相应的 HTTP 动词以及每个动词对 API 执行的作类型的典型语义期望。

Table 5.1 The minimal API map endpoints and the corresponding HTML verbs
表 5.1 最小 API 映射端点和相应的 HTML 动词

Method HTTP verb Expected operation
MapGet(path, handler) GET Fetch data only; no modification of state. May be safe to cache. 仅获取数据;不修改状态。可以安全地缓存。
MapPost(path, handler) POST Create a new resource. 创建新资源。
MapPut(path, handler) PUT Create or replace an existing resource.创建或替换现有资源。
MapDelete(path, handler) DELETE Delete the given resource. 删除给定的资源。
MapPatch(path, handler) PATCH Modify the given resource. 修改给定的资源。
MapMethods(path, methods,handler) Multiple verbs Multiple operations 多个动作
Map(path, handler) All verbs Multiple operations 多个动作
MapFallback(handler) All verbs Useful for SPA fallback routes 对 SPA 回退路由很有用。

RESTful applications (as described in chapter 2) typically stick close to these verb uses where possible, but some of the actual implementations can differ, and people can easily get caught up in pedantry. Generally, if you stick to the expected operations described in table 5.1, you’ll create a more understandable interface for consumers of the API.

RESTful 应用程序(如第 2 章所述)通常尽可能地使用这些动词用法,但一些实际实现可能会有所不同,人们很容易陷入迂腐。通常,如果您坚持使用 表 5.1 中描述的预期作,您将为 API 的使用者创建一个更易于理解的界面。

Note You may notice that if you use the MapMethods() and Map() methods listed in table 5.1, your API probably doesn’t correspond to the expected operations of the HTTP verbs it supports, so I avoid these methods where possible. MapFallback() doesn’t have a path and is called only if no other endpoint matches. Fallback routes can be useful when you have a SPA that uses client-side routing. See http://mng.bz/9DMl for a description of the problem and an alternative solution.
注意 您可能会注意到,如果使用表 5.1 中列出的 MapMethods() 和 Map() 方法,则 API 可能与它支持的 HTTP 动词的预期作不对应,因此我尽可能避免使用这些方法。MapFallback() 没有路径,仅当没有其他终端节点匹配时才会调用。当您拥有使用客户端路由的 SPA 时,回退路由可能很有用。有关问题的说明和替代解决方案,请参阅 http://mng.bz/9DMl

As I mentioned at the start of section 5.2.2, testing APIs that use verbs other than GET is tricky in the browser. You need to use a tool that allows sending arbitrary requests such as Postman (https://www.postman.com) or the HTTP Client plugin in JetBrains Rider. In chapter 11 you’ll learn how to use a tool called Swagger UI to visualize and test your APIs.

正如我在 5.2.2 节开头提到的,在浏览器中测试使用 GET 以外的动词的 API 是很棘手的。您需要使用允许发送任意请求的工具,例如 Postman (https://www.postman.com) 或 JetBrains Rider 中的 HTTP Client 插件。在第 11 章中,您将学习如何使用名为 Swagger UI 的工具来可视化和测试您的 API。

Tip The HTTP client plugin in JetBrains Rider makes it easy to craft HTTP requests from inside your API, and even discovers all the endpoints in your application automatically, making them easier to test. You can read more about it at https://www.jetbrains.com/help/rider/Http_client_in_product__code_editor.html.
提示 JetBrains Rider 中的 HTTP 客户端插件可以轻松地从 API 内部构建 HTTP 请求,甚至可以自动发现应用程序中的所有端点,使其更易于测试。您可以在 https://www.jetbrains.com/help/rider/Http_client_in_product__code_editor.html 上阅读更多相关信息。

As a final note before we move on, it’s worth mentioning the behavior you get when you call a method with the wrong HTTP verb. If you define an API like the one in listing 5.1,and call it by using a POST request to /person/Al instead of a GET request, the handler won’t run, and the response you get will have status code 405 Method Not Allowed.
在我们继续之前,最后要注意的是,当你使用错误的 HTTP 动词调用方法时,你得到的行为是值得一提的。如果你定义了一个类似于清单 5.1 中的 API。并使用对 /person/Al 的 POST 请求而不是 GET 请求来调用它,则处理程序不会运行,并且您获得的响应将具有状态代码 405 Method Not Allowed。

app.MapGet("/person/{name}", (string name) =>
    people.Where(p => p.FirstName.StartsWith(name)));

Tip You should never see this response when you’re calling the API correctly, so if you receive a 405 response, make sure to check that you’re using the right HTTP verb and the right path. Often when I see a 405, I’ve used the correct verb but made a typo in the URL!
提示 正确调用 API 时,您应该永远不会看到此响应,因此,如果您收到 405 响应,请务必检查您是否使用了正确的 HTTP 动词和正确的路径。通常,当我看到 405 时,我使用了正确的动词,但在 URL 中打错了字!

In all the examples in this book so far, you provide a lambda function as the handler for an endpoint. But in section 5.2.3, you’ll see that there are many ways to define the handler.

到目前为止,在本书的所有示例中,您都提供了一个 lambda 函数作为终端节点的处理程序。但是在 5.2.3 节 中,你会看到有很多方法可以定义处理程序。

5.2.3 Defining route handlers with functions

5.2.3 使用函数定义路由处理程序

For basic examples, using a lambda function as the handler for an endpoint is often the simplest approach, but you can take many approaches, as shown in listing 5.2. This listing also demonstrates creating a simple CRUD (Create, Read, Update, Delete) API using different HTTP verbs, as discussed in section 5.2.1.

对于基本示例,使用 lambda 函数作为终端节点的处理程序通常是最简单的方法,但您可以采用多种方法,如清单 5.2 所示。此清单还演示了使用不同的 HTTP 动词创建简单的 CRUD(创建、读取、更新、删除)API,如 5.2.1 节所述。

Listing 5.2 Creating route handlers for a simple CRUD API
清单 5.2 为简单的 CRUD API 创建路由处理程序

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();

app.MapGet("/fruit", () => Fruit.All); ❶

var getFruit = (string id) => Fruit.All[id]; ❷
app.MapGet("/fruit/{id}", getFruit); ❷

app.MapPost("/fruit/{id}", Handlers.AddFruit); ❸

Handlers handlers = new(); ❹
app.MapPut("/fruit/{id}", handlers.ReplaceFruit); ❹

app.MapDelete("/fruit/{id}", DeleteFruit); ❺

app.Run();

void DeleteFruit(string id) ❺
{
    Fruit.All.Remove(id);
}

record Fruit(string Name, int Stock)
{
    public static readonly Dictionary<string, Fruit> All = new();
};

class Handlers
{
    public void ReplaceFruit(string id, Fruit fruit) ❻
    {
        Fruit.All[id] = fruit;
    }
    public static void AddFruit(string id, Fruit fruit) ❼
    {
        Fruit.All.Add(id, fruit);
    }
}

❶ Lambda expressions are the simplest but least descriptive way to create a handler.
Lambda 表达式是创建处理程序的最简单但描述性最差的方法。

❷ Storing the lambda expression as a variable means you can name it—getFruit in this case.
将 lambda 表达式存储为变量意味着您可以将其命名为 getFruit,在本例中为 getFruit。

❸ Handlers can be static methods in any class.
处理程序可以是任何类中的静态方法。

❹ Handlers can also be instance methods.
处理程序也可以是实例方法。

❺ You can also use local functions, introduced in C# 7.0, as handler methods.
还可以使用 C# 7.0 中引入的本地函数作为处理程序方法。

❻ Handlers can also be instance methods.
处理程序也可以是实例方法。

❼ Converts the response to a JsonObject
将响应转换为 JsonObject

Listing 5.2 demonstrates the various ways you can pass handlers to an endpoint by simulating a simple API for interacting with a collection of Fruit items:
清单 5.2 通过模拟一个简单的 API 来与 Fruit 项目集合交互,演示了将处理程序传递给端点的各种方法:

  • A lambda expression, as in the MapGet("/fruit") endpoint
    lambda 表达式,如终端节点中所示MapGet("/fruit")

  • A Func<T, TResult> variable, as in the MapGet("/fruit/{id}") endpoint
    一个变量,如端点Func<T, TResult>MapGet("/fruit/{id}")

  • A static method, as in the MapPost endpoint
    静态方法,如端点MapPost

  • A method on an instance variable, as in the MapPut endpoint
    实例变量上的方法,如端点中所示MapPut

  • A local function, as in the MapDelete endpoint
    本地函数,如端点MapDelete

All these approaches are functionally identical, so you can use whichever pattern works best for you.
所有这些方法在功能上都是相同的,因此您可以使用最适合您的模式。

Each Fruit record in listing 5.2 has a Name and a Stock level and is stored in a dictionary with an id. You call the API by using different HTTP verbs to perform the CRUD operations against the dictionary.

清单 5.2 中的每个 Fruit 记录都有一个 Name 和一个 Stock 级别,并存储在一个带有 id 的字典中。您可以使用不同的 HTTP 动词调用 API,以对字典执行 CRUD作。

Warning This API is simple. It isn’t thread-safe, doesn’t validate user input, and doesn’t handle edge cases. We’ll remedy some of those deficiencies in section 5.3.
警告 此 API 很简单。它不是线程安全的,不验证用户输入,也不处理边缘情况。我们将在第 5.3 节中纠正其中一些缺陷。

The handlers for the POST and PUT endpoints in listing 5.2 accept both an id parameter and a Fruit parameter, showing another important feature of minimal APIs. Complex types—that is, types that can’t be extracted from the URL by means of route parameters—are created by deserializing the JSON body of a request.

清单 5.2 中 POST 和 PUT 端点的处理程序同时接受 id 参数和 Fruit 参数,这显示了最小 API 的另一个重要特性。复杂类型(即无法通过路由参数从 URL 中提取的类型)是通过反序列化请求的 JSON 正文来创建的。

Note By contrast with APIs built using ASP.NET and ASP.NET Core web API controllers (which we cover in chapter 20), minimal APIs can bind only to JSON bodies and always use the System.Text.Json library for JSON deserialization.
注意 与使用 ASP.NET 和 ASP.NET Core Web API 控制器构建的 API(我们将在第 20 章中介绍)相比,最小的 API 只能绑定到 JSON 正文,并始终使用 System.Text.Json 库进行 JSON 反序列化。

Figure 5.3 shows an example of a POST request sent with Postman. Postman sends the request body as JSON, which the minimal API automatically deserializes into a Fruit instance before calling the endpoint handler. You can bind only a single object in your endpoint handler to the request body in this way. I cover model binding in detail in chapter 7.

图 5.3 显示了使用 Postman 发送的 POST 请求的示例。Postman 以 JSON 格式发送请求正文,最小 API 会自动将其反序列化为 Fruit
实例。通过这种方式,您只能将终端节点处理程序中的单个对象绑定到请求正文。我在第 7 章中详细介绍了模型绑定。

alt text

Figure 5.3 Sending a POST request with Postman. The minimal API automatically deserializes the JSON in the request body to a Fruit instance before calling the endpoint handler.
图 5.3 使用 Postman 发送 POST 请求。在调用终端节点处理程序之前,最小 API 会自动将请求正文中的 JSON 反序列化为 Fruit 实例。

Minimal APIs leave you free to organize your endpoints any way you choose. That flexibility is often cited as a reason to not use them, due to the fear that developers will keep all the functionality in a single file, as in most examples (such as listing 5.2). In practice, you’ll likely want to extract your endpoints to separate files so as to modularize them and make them easier to understand. Embrace that urge; that’s the way they were intended to be used!

最少的 API 让您可以自由地按照您选择的任何方式组织终端节点。这种灵活性经常被引用为不使用它们的理由,因为担心开发人员会保留所有功能在单个文件中,就像大多数示例一样(比如清单 5.2)。在实践中,您可能希望将终端节点提取到单独的文件中,以便将它们模块化并使其更易于理解。拥抱这种冲动;这就是它们的使用方式!

Now you have a simple API, but if you try it out, you’ll quickly run into scenarios in which your API seems to break. In section 5.3 you learn how to handle some of these scenarios by returning status codes.

现在你有一个简单的 API,但如果你尝试一下,你很快就会遇到 API 似乎崩溃的情况。在 Section 5.3 中,您将学习如何通过返回状态代码来处理其中一些场景。

5.3 Generating responses with IResult

5.3 使用 IResult 生成响应

You’ve seen the basics of minimal APIs, but so far, we’ve looked only at the happy path, where you can handle the request successfully and return a response. In this section we look at how to handle bad requests and other errors by returning different status codes from your API.

您已经了解了最小 API 的基础知识,但到目前为止,我们只了解了 Happy Path,您可以在其中成功处理请求并返回响应。在本节中,我们将了解如何通过从 API 返回不同的状态代码来处理错误请求和其他错误。

The API in listing 5.2 works well as long as you perform only operations that are valid for the current state of the application. If you send a GET request to /fruit, for example, you’ll always get a 200 success response, but if you send a GET request to /fruit/f1 before you create a Fruit with the id f1, you’ll get an exception and a 500 Internal Server Error response, as shown in figure 5.4.

清单 5.2 中的 API 运行良好,只要您只执行对应用程序当前状态有效的作。例如,如果向 /fruit 发送 GET 请求,则始终会收到 200 成功响应,但如果在创建 ID 为 f1 的 Fruit 之前向 /fruit/f1 发送 GET 请求,则会收到异常和 500 Internal Server Error 响应,如图 5.4 所示。

alt text

Figure 5.4 If you try to retrieve a fruit by using a nonexistent id for the simplistic API in listing 5.2, the endpoint throws an exception. This exception is handled by the DeveloperExceptionPageMiddleware but provides a poor experience.
图 5.4 如果你尝试使用清单 5.2 中简单 API 的不存在的 id 来检索水果,端点会抛出一个异常。此异常由 DeveloperExceptionPage-Middleware 处理,但提供的体验很差。

Throwing an exception whenever a user requests an id that doesn’t exist clearly makes for a poor experience all round. A better approach is to return a status code indicating the problem, such as 404 Not Found or 400 Bad Request. The most declarative way to do this with minimal APIs is to return an IResult instance.

每当用户请求不存在的 ID 时引发异常,显然会导致整体体验不佳。更好的方法是返回指示问题的状态代码,例如 404 Not Found 或 400 Bad Request。使用最少的 API 执行此作的最声明性方法是返回 IResult 实例。

All the endpoint handlers you’ve seen so far in this book have returned void, a string, or a plain old CLR object (POCO) such as Person or Fruit. There is one other type of object you can return from an endpoint: an IResult implementation. In summary, the endpoint middleware handles each return type as follows:

到目前为止,您在本书中看到的所有端点处理程序都返回了 void、字符串或普通的旧 CLR 对象 (POCO),例如 Person 或 Fruit。您可以从端点返回另一种类型的对象:IResult 实现。总之,终端节点中间件按如下方式处理每个返回类型:

  • void or Task—The endpoint returns a 200 response with no body.
    void或Task200 — 终端节点返回没有正文的响应。

  • string or Task—The endpoint returns a 200 response with the string serialized to the body as text/plain.
    string或 Task— 终端节点返回一个200响应,其中字符串序列化为正文text/plain。

  • IResult or Task—The endpoint executes the IResult.ExecuteAsync method. Depending on the implementation, this type can customize the response, returning any status code.
    IResult或Task - 端点执行IResult.ExecuteAsync方法。根据实现,此类型可以自定义响应,返回任何状态代码。

  • T or Task—All other types (such as POCO objects) are serialized to JSON and returned in the body of a 200 response as application/json.
    T或Task — 所有其他类型的(如 POCO 对象)都序列化为 JSON,并在响应正文中作为application/json .

The IResult implementations provide much of the flexibility in minimal APIs, as you’ll see in section 5.3.1.

IResult 实现在最小的 API 中提供了很大的灵活性,正如您将在 5.3.1 节中看到的那样。

5.3.1 Returning status codes with Results and TypedResults

5.3.1 使用 Results 和 TypedResults 返回状态代码

A well-designed API uses status codes to indicate to a client what went wrong when a request failed, as well as potentially provide more descriptive codes when a request is successful. You should anticipate common problems that may occur when clients call your API and return appropriate status codes to indicate the causes to users.

设计良好的 API 使用状态代码向客户端指示请求失败时出了什么问题,并可能在请求成功时提供更具描述性的代码。您应该预见到客户端调用 API 时可能出现的常见问题,并返回适当的状态代码以向用户指示原因。

ASP.NET Core exposes the simple static helper types Results and TypedResults in the namespace Microsoft.AspNetCore.Http. You can use these helpers to create a response with common status codes, optionally including a JSON body. Each of the methods on Results and TypedResults returns an implementation of IResult, which the endpoint middleware executes to generate the final response.

ASP.NET Core 在命名空间 Microsoft.AspNetCore.Http 中公开了简单的静态帮助程序类型 Results 和 TypedResults。您可以使用这些帮助程序创建具有常见状态代码的响应,可以选择包括 JSON 正文。Results 和 TypedResults 上的每个方法都返回 IResult 的实现,端点中间件执行该实现以生成最终响应。

Note Results and TypedResults perform the same function, as helpers for generating common status codes. The only difference is that the Results methods return an IResult, whereas TypedResults return a concrete generic type, such as Ok. There’s no difference in terms of functionality, but the generic types are easier to use in unit tests and in OpenAPI documentation, as you’ll see in chapters 36 and 11. TypedResults were added in .NET 7.
注意 Results 和 TypedResults 执行相同的功能,作为生成常见状态代码的帮助程序。唯一的区别是 Results 方法返回 IResult,而 TypedResults 返回具体的泛型类型,例如 Ok。在功能方面没有区别,但泛型类型更易于使用单元测试和 OpenAPI 文档,如第 36 章和第 11 章所示。TypedResults 已添加到 .NET 7 中。

The following listing shows an updated version of listing 5.2, in which we address some of the deficiencies in the API and use Results and TypedResults to return different status codes to clients.

下面的清单显示了清单 5.2 的更新版本,其中我们解决了 API 中的一些缺陷,并使用 Results 和 TypedResults 向客户端返回不同的状态代码。

Listing 5.3 Using Results and TypedResults in a minimal API
清单 5.3 在最小 API 中使用 和ResultsTypedResults

using System.Collections.Concurrent;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();

var _fruit = new ConcurrentDictionary<string, Fruit>(); ❶

app.MapGet("/fruit", () => _fruit);

app.MapGet("/fruit/{id}", (string id) =>
    _fruit.TryGetValue(id, out var fruit) ❷
        ? TypedResults.Ok(fruit) ❸
        : Results.NotFound()); ❹

app.MapPost("/fruit/{id}", (string id, Fruit fruit) =>
    _fruit.TryAdd(id, fruit) ❺
        ? TypedResults.Created($"/fruit/{id}", fruit) ❻
        : Results.BadRequest(new ❼
            { id = "A fruit with this id already exists" })); ❼

app.MapPut("/fruit/{id}", (string id, Fruit fruit) =>
{
    _fruit[id] = fruit;
    return Results.NoContent(); ❽
});

app.MapDelete("/fruit/{id}", (string id) =>
{
    _fruit.TryRemove(id, out _); ❾
    return Results.NoContent(); ❾
});

app.Run();

record Fruit(string Name, int stock);

❶ Uses a concurrent dictionary to make the API thread-safe
使用并发字典使 API 线程安全

❷ Tries to get the fruit from the dictionary. If the ID exists in the dictionary, this returns true . . .
尝试从字典中获取 fruit。如果字典中存在 ID,则返回 true . . .

❸ . . . and we return a 200 OK response, serializing the fruit in the body as JSON.
. . . .然后我们返回一个 200 OK 响应,将 body 中的水果序列化为 JSON。

❹ If the ID doesn’t exist, returns a 404 Not Found response
如果 ID 不存在,则返回 404 Not Found 响应

❺ Tries to add the fruit to the dictionary. If the ID hasn’t been added yet. this returns true . . .
尝试将 fruit 添加到字典中。如果尚未添加 ID。这将返回 true . . .

❻ . . . and we return a 201 response with a JSON body and set the Location
header to the given path.
. . . .我们返回一个带有 JSON 正文的 201 响应,并将 Location 标头设置为给定路径。

❼ If the ID already exists, returns a 400 Bad Request response with an error message
如果 ID 已存在,则返回 400 Bad Request 响应,并显示错误消息

❽ After adding or replacing the fruit, returns a 204 No Content response
添加或替换水果后,返回 204 No Content 响应

❾ After deleting the fruit, always returns a 204 No Content response
删除水果后,始终返回 204 No Content 响应

Listing 5.3 demonstrates several status codes, some of which you may not be familiar with:

清单 5.3 演示了几个状态代码,其中一些你可能不熟悉:

  • 200 OK—The standard successful response. It often includes content in the body of the response but doesn’t have to.
    200 OK - 标准成功响应。它通常在响应正文中包含内容,但并非必须。

  • 201 Created—Often returned when you successfully created an entity on the server. The Created result in listing 5.3 also includes a Location header to describe the URL where the entity can be found, as well as the JSON entity itself in the body of the response.
    201 Created (已创建) – 当您在服务器上成功创建实体时,通常会返回。清单 5.3 中的 Created 结果还包括一个 Location 标头,用于描述可以找到实体的 URL,以及响应正文中的 JSON 实体本身。

  • 204 No Content—Similar to a 200 response but without any content in the response body.
    204 无内容 — 类似于 200 响应,但响应正文中没有任何内容。

  • 400 Bad Request—Indicates that the request was invalid in some way; often used to indicate data validation failures
    400 Bad Request — 表示请求在某种程度上无效;通常用于表示数据验证失败。

  • 404 Not Found—Indicates that the requested entity could not be found
    404 Not Found - 表示找不到请求的实体。

These status codes more accurately describe your API and can make an API easier to use. That said, if you use only 200 OK responses for all your successful responses, few people will mind or think less of you! You can see a summary of all the possible status codes and their expected uses at http://mng.bz/jP4x.

这些状态代码可以更准确地描述您的 API,并使 API 更易于使用。也就是说,如果你只使用 200 个 OK 回复来获得所有成功的回复,那么很少有人会介意或少看你!您可以在 http://mng.bz/jP4x 上查看所有可能的状态代码及其预期用途的摘要。

Note The 404 status code in particular causes endless debate in online forums. Should it be only used if the request didn’t match an endpoint? Is it OK to use 404 to indicate a missing entity (as in the previous example)? There are endless proponents in both camps, so take your pick!
注意 尤其是 404 状态代码在在线论坛中引起了无休止的争论。是否应仅在请求与终端节点不匹配时才使用它?是否可以使用 404 来表示缺少的实体(如前面的示例所示)?两个阵营都有无穷无尽的支持者,所以任你选择吧!

Results and TypedResults include methods for all the common status code results you could need, but if you don’t want to use them for some reason, you can always set the status code yourself directly on the HttpResponse, as in listing 5.4. In fact, the listing shows how to define the entire response manually, including the status code, the content type, and the response body. You won’t need to take this manual approach often, but it can be useful in some situations.

Results 和 TypedResults 包含你可能需要的所有常见状态码结果的方法,但是如果你由于某种原因不想使用它们,你总是可以直接在 HttpResponse 上自己设置状态码,如清单 5.4 所示。事实上,该清单显示了如何手动定义整个响应,包括状态代码、内容类型和响应正文。您不需要经常采用这种手动方法,但它在某些情况下可能很有用。

Listing 5.4 Writing the response manually using HttpResponse
清单 5.4 使用HttpResponse

using System.Net.Mime
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();

app.MapGet("/teapot", (HttpResponse response) => ❶
{
    response.StatusCode = 418; ❷
    response.ContentType = MediaTypeNames.Text.Plain; ❸
    return response.WriteAsync("I'm a teapot!"); ❹
});

app.Run();

❶ Accesses the HttpResponse by including it as a parameter in your endpoint
handler
通过将 HttpResponse 作为参数包含在端点处理程序中来访问 HttpResponse

❷ You can set the status code directly on the response.
您可以直接在响应中设置状态代码。

❸ Defines the content type that will be sent in the response
定义将在响应中发送的内容类型

❹ You can write data to the response stream manually.
您可以手动将数据写入响应流。

HttpResponse represents the response that will be sent to the client and is one of the special types that minimal APIs know to inject into your endpoint handlers (instead of trying to create it by deserializing from the request body). You’ll learn about the other types you can use in your endpoint handlers in chapter 7.

HttpResponse 表示将发送到客户端的响应,并且是最小 API 知道注入到端点处理程序中的特殊类型之一(而不是尝试通过从请求正文反序列化来创建它)。您将在第 7 章中了解可以在端点处理程序中使用的其他类型。

5.3.2 Returning useful errors with Problem Details

5.3.2 使用 Problem Details 返回有用的错误

In the MapPost endpoint of listing 5.3, we checked to see whether an entity with the given id already existed. If it did, we returned a 400 response with a description of the error. The problem with this approach is that the client—typically, a mobile app or SPA—must know how to read and parse that response. If each of your APIs has a different format for errors, that arrangement can make for a confusing API. Luckily, a web standard called Problem Details describes a consistent format to use.

在清单 5.3 的 MapPost 端点中,我们检查了是否已经存在具有给定 id 的实体。如果是这样,我们将返回 400 响应,其中包含错误说明。‌这种方法的问题在于,客户端(通常是移动应用程序或 SPA)必须知道如何读取和解析该响应。如果每个 API 的错误,这种安排可能会导致 API 混乱。幸运的是,一个名为 Problem Details 的 Web 标准描述了要使用的一致格式。

Definition Problem Details is a web specification (https://www.rfc-editor.org/rfc/rfc7807.html) for providing machine-readable errors for HTTP APIs. It defines the required and optional fields that should be in the JSON body for errors.
定义 问题详细信息 是一个 Web 规范 (https://www.rfc-editor.org/rfc/rfc7807.xhtml),用于为 HTTP API 提供机器可读的错误。它定义了 JSON 正文中应包含的 errors 必填字段和可选字段。

ASP.NET Core includes two helper methods for generating Problem Details responses from minimal APIs: Results.Problem() and Results.ValidationProblem() (plus their TypedResults counterparts). Both of these methods return Problem Details JSON. The only difference is that Problem() defaults to a 500 status code, whereas ValidationProblem() defaults to a 400 status and requires you to pass in a Dictionary of validation errors, as shown in the following listing.

ASP.NET Core 包含两个帮助程序方法,用于从最小 API 生成问题详细信息响应:Results.Problem() 和 Results.ValidationProblem()(以及它们的 TypedResults 对应项)。这两种方法都返回 Problem Details JSON。唯一的区别是 Problem() 默认为 500 状态代码,而 ValidationProblem() 默认为 400 状态,并要求您传入验证错误的 Dictionary,如下面的清单所示。

Listing 5.5 Returning Problem Details using Results.Problem
清单 5.5 使用Results.Problem

using System.Collections.Concurrent;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();

var _fruit = new ConcurrentDictionary<string, Fruit>();

app.MapGet("/fruit", () => _fruit);

app.MapGet("/fruit/{id}", (string id) =>
    _fruit.TryGetValue(id, out var fruit)
        ? TypedResults.Ok(fruit)
        : Results.Problem(statusCode: 404)); ❶

app.MapPost("/fruit/{id}", (string id, Fruit fruit) =>
    _fruit.TryAdd(id, fruit)
     ? TypedResults.Created($"/fruit/{id}", fruit)
     : Results.ValidationProblem(new Dictionary<string, string[]> ❷
        { ❷
            {"id", new[] {"A fruit with this id already exists"}} ❷
        }));

❶ Returns a Problem Details object with a 404 status code
返回状态代码为 404 的 Problem Details 对象

❷ Returns a Problem Details object with a 400 status code and includes the
validation errors
返回具有 400 状态代码的 Problem Details 对象,并包含验证错误

The ProblemHttpResult returned by these methods takes care of including the correct title and description based on the status code, and generates the appropriate JSON, as shown in figure 5.5. You can override the default title and description by passing additional arguments to the Problem() and ValidationProblem() methods.
这些方法返回的 ProblemHttpResult 负责根据状态代码包含正确的标题和描述,并生成适当的 JSON,如图 5.5 所示。您可以通过向 Problem() 和 ValidationProblem() 方法传递其他参数来覆盖默认的标题和描述。

alt text

Figure 5.5 You can return a Problem Details response by using the Problem and ValidationProblem methods. The ValidationProblem response shown here includes a description of the error, along with the validation errors in a standard format. This example shows the response when you try to create a fruit with an id that has already been used.
图 5.5 您可以使用 Problem 和 ValidationProblem 方法返回 Problem Details 响应。此处显示的 ValidationProblem 响应包括错误说明,以及标准格式的验证错误。此示例显示了当您尝试创建具有已使用的 id 的水果时的响应。

Deciding on an error format is an important step whenever you create an API, and as Problem Details is already a web standard, it should be your go-to approach, especially for validation errors. Next, you’ll learn how to ensure that all your error responses are Problem Details.

无论何时创建 API,确定错误格式都是一个重要的步骤,由于 Problem Details 已经是一个 Web 标准,因此它应该是您的首选方法,尤其是对于验证错误。接下来,您将学习如何确保所有错误响应都是 Problem Details。

5.3.3 Converting all your responses to Problem Details

5.3.3 将您的所有响应转换为 Problem Details

In section 5.3.2 you saw how to use the Results.Problem() and Results.ValidationProblem() methods in your minimal API endpoints to return Problem Details JSON. The only catch is that your minimal API endpoints aren’t the only thing that could generate errors. In this section you’ll learn how to make sure that all your errors return Problem Details JSON, keeping the error responses consistent across your application.

在第 5.3.2 节中,您了解了如何在最小 API 端点中使用 Results.Problem() 和 Results.ValidationProblem() 方法来返回问题详细信息 JSON。唯一的问题是,您的最小 API 终端节点并不是唯一可能产生错误的东西。在本节中,您将学习如何确保所有错误都返回 Problem Details JSON,从而在整个应用程序中保持错误响应的一致性。

A minimal API application could generate an error response in several ways:

最小的 API 应用程序可以通过多种方式生成错误响应:

  • Returning an error status code from an endpoint handler
    从终端节点处理程序返回错误状态代码

  • Throwing an exception in an endpoint handler, which is caught by the ExceptionHandlerMiddleware or the DeveloperExceptionPageMiddleware and converted to an error response.
    在端点处理程序中抛出异常,该异常被 ExceptionHandlerMiddleware 或 DeveloperExceptionPageMiddleware 捕获并转换为错误响应

  • The middleware pipeline returning a 404 response because a request isn’t handled by an endpoint
    中间件管道返回 404 响应,因为请求未由终端节点处理

  • A middleware component in the pipeline throwing an exception
    管道中引发异常的中间件组件

  • A middleware component returning an error response because a request requires authentication, and no credentials were provided
    中间件组件返回错误响应,因为请求需要身份验证,并且未提供凭据

There are essentially two classes of errors, which are handled differently: exceptions and error status code responses. To create a consistent API for consumers, we need to make sure that both error types return Problem Details JSON in the response.
基本上有两类错误,它们的处理方式不同:异常和错误状态代码响应。要为使用者创建一致的 API,我们需要确保两种错误类型在响应中都返回 Problem Details JSON。

Converting exceptions to Problem Details

将异常转换为 Problem Details

In chapter 4 you learned how to handle exceptions with the ExceptionHandlerMiddleware. You saw that the middleware catches any exceptions from later middleware and generates an error response by executing an error-handling path. You could add the middleware to your pipeline with an error-handling path of "/error":

在第 4 章中,您学习了如何使用 ExceptionHandlerMiddleware 处理异常。您看到中间件从后面的中间件中捕获任何异常,并通过执行错误处理路径生成错误响应。您可以将中间件添加到错误处理路径为 “/error” 的管道中:

app.UseExceptionHandler("/error");

ExceptionHandlerMiddleware invokes this path after it captures an exception to generate the final response. The trouble with this approach for minimal APIs is that you need a dedicated error endpoint, the sole purpose of which is to generate a Problem Details response.

ExceptionHandlerMiddleware 在捕获异常后调用此路径以生成最终响应。对于最小 API,这种方法的问题在于,您需要一个专用的错误终端节点,其唯一目的是生成 Problem Details 响应。

Luckily, in .NET 7, you can configure the ExceptionHandlerMiddleware (and DeveloperExceptionPageMiddleware) to convert an exception to a Problem Details response automatically. In .NET 7, you can add the new IProblemDetailsService to your app by calling AddProblemDetails() on WebApplicationBuilder.Services. When the ExceptionHandlerMiddleware is configured without an error-handling path, it automatically uses the IProblemDetailsService to generate the response, as shown in figure 5.6.

幸运的是,在 .NET 7 中,您可以配置 ExceptionHandlerMiddleware(和 DeveloperExceptionPageMiddleware)以自动将异常转换为 Problem Details 响应。在.NET 7 中,您可以通过在 WebApplicationBuilder.Services 上调用 AddProblemDetails() 将新的 IProblemDetailsService 添加到您的应用程序。当 ExceptionHandlerMiddleware 配置为没有错误处理路径时,它会自动使用 IProblemDetailsService 来生成响应,如图 5.6 所示。

Warning Calling AddProblemDetails() registers the IProblemDetailsService service in the dependency injection container so that other services and middleware can use it. If you configure ExceptionHandlerMiddleware without an error-handling path but forget to call AddProblemDetails(), you’ll get an exception when your app starts. You’ll learn more about dependency injection in chapters 8 and 9.
警告 调用 AddProblemDetails() 会在依赖项注入容器中注册 IProblemDetailsService 服务,以便其他服务和中间件可以使用它。如果你在配置 ExceptionHandlerMiddleware 时没有错误处理路径,但忘记调用 AddProblemDetails(),那么当应用启动时会出现异常。您将在第 8 章和第 9 章中了解有关依赖关系注入的更多信息。

alt text

Figure 5.6. The ExceptionHandlerMiddleware catches exceptions that occur later in the middleware pipeline. If the middleware isn’t configured to reexecute the pipeline, it generates a Problem Details response by using the IProblemDetailsService.
图 5.6 ExceptionHandlerMiddleware 捕获中间件管道中稍后发生的异常。如果中间件未配置为重新执行管道,它将使用 IProblemDetailsService 生成 Problem Details 响应。

Listing 5.6 shows how to configure Problem Details generation in your exception handlers. Add the required IProblemDetailsService service to your app, and call UseExceptionHandler() without providing an error-handling path, and the middleware will generate a Problem Details response automatically when it catches an exception.

清单 5.6 展示了如何在异常处理程序中配置 Problem Details 生成。将所需的 IProblemDetailsService 服务添加到您的应用程序,并在不提供错误处理路径的情况下调用 UseExceptionHandler(),中间件将在捕获异常时自动生成 Problem Details 响应。

Listing 5.6 Configuring ExceptionHandlerMiddleware to use Problem Details
清单 5.6 配置ExceptionHandlerMiddleware使用 Problem Details

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails(); ❶

WebApplication app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(); ❷
}

app.MapGet("/", void () => throw new Exception()); ❸

app.Run();

❶ Adds the IProblemDetailsService implementation
添加 IProblemDetailsService 实现

❷ Configures the ExceptionHandlerMiddleware without a path so that it uses the IProblemDetailsService
配置不带路径的 ExceptionHandlerMiddleware,以便它使用 IProblemDetailsService

❸ Throws an exception to demonstrate the behavior
引发异常以演示行为

As discussed in chapter 4, WebApplication automatically adds the DeveloperExceptionPageMiddleware to your app in the development environment. This middleware similarly supports returning Problem Details when two conditions are satisfied:

如第 4 章所述,WebApplication 会自动将 DeveloperExceptionPageMiddleware 添加到开发环境中的应用程序中。此中间件同样支持在满足两个条件时返回 Problem Details:

  • You’ve registered an IProblemDetailsService with the app (by calling AddProblemDetails() in Program.cs)
    您已向应用程序注册了 IProblemDetailsService(通过在 Program.cs 中调用 AddProblemDetails())。

  • The request indicates that it doesn’t support HTML. If the client supports HTML, middleware uses the HTML developer exception page from chapter 4 instead.
    该请求指示它不支持 HTML。如果客户端支持 HTML,中间件会改用第 4 章中的 HTML 开发人员例外页面。

The ExceptionHandlerMiddleware and DeveloperExceptionPageMiddleware take care of converting all your exceptions to Problem Details responses, but you still need to think about nonexception errors, such as the automatic 404 response generated when a request doesn’t match any endpoints.

ExceptionHandlerMiddleware 和 DeveloperExceptionPageMiddleware 负责将所有异常转换为 Problem Details 响应,但您仍然需要考虑非异常错误,例如当请求与任何端点不匹配时生成的自动 404 响应。

Converting error status codes to Problem Details

C将错误状态代码反转为 PROBLEM DETAILS

Returning error status codes is the common way to communicate errors to a client with minimal APIs. To ensure a consistent API for consumers, you should return a Problem Details response whenever you return an error. Unfortunately, as already mentioned, you don’t control all the places where an error code may be created. The middleware pipeline automatically returns a 404 response when an unmatched request reaches the end of the pipeline, for example.

返回错误状态代码是使用最少 API 将错误传达给客户端的常用方法。为了确保使用者的 API 一致,您应该在返回错误时返回 Problem Details 响应。遗憾的是,如前所述,您无法控制可能创建错误代码的所有位置。例如,当不匹配的请求到达管道末尾时,中间件管道会自动返回 404 响应。

Instead of generating a Problem Details response in your endpoint handlers, you can add middleware to convert responses to Problem Details automatically by using the StatusCodePagesMiddleware, as shown in figure 5.7. Any response that reaches the middleware with an error status code and doesn’t already have a body has a Problem Details body added by the middleware. The middleware converts all error responses automatically, regardless of whether they were generated by an endpoint or from other middleware.

您可以添加中间件来转换使用 StatusCodePagesMiddleware 自动响应 Problem Details,如图 5.7 所示。任何到达中间件时带有错误状态代码且尚未具有正文的响应都会由中间件添加 Problem Details 正文。中间件会自动转换所有错误响应,无论它们是由终端节点还是来自其他中间件。

alt text

Figure 5.7 The StatusCodePagesMiddleware intercepts responses with an error status code that have no response body and adds a Problem Details response body.

图 5.7 StatusCodePagesMiddleware 拦截带有错误状态码且没有响应体的响应,并添加 Problem Details 响应体。

Note You can also use the StatusCodePagesMiddleware to reexecute the middleware pipeline with an error handling path, as you can with the ExceptionHandlerMiddleware (chapter 4). This technique is most useful for Razor Pages applications when you want to have a different error page for specific status codes, as you’ll see in chapter 15.
注意 您还可以使用 StatusCodePagesMiddleware 通过错误处理路径重新执行中间件管道,就像使用 ExceptionHandlerMiddleware(第 4 章)一样。当希望为特定状态代码使用不同的错误页时,此技术对 Razor Pages 应用程序最有用,如第 15 章所示。

Add the StatusCodePagesMiddleware to your app by using the UseStatusCodePages() extension method, as shown in listing 5.7. Ensure that you also add the IProblemDetailsService to your app by using AddProblemDetails().

使用 UseStatusCodePages() 扩展方法将 StatusCodePagesMiddleware 添加到您的应用程序中,如下面的清单所示。确保还使用 AddProblemDetails() 将 IProblemDetailsService 添加到您的应用程序。

Listing 5.7 Using StatusCodePagesMiddleware to return Problem Details
清单 5.7 使用 StatusCodePagesMiddleware 返回问题详情

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails(); ❶

WebApplication app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
}

app.UseStatusCodePages(); ❷

app.MapGet("/", () => Results.NotFound()); ❸

app.Run();

❶ Adds the IProblemDetailsService implementation
添加 IProblemDetailsService 实现

❷ Adds the StatusCodePagesMiddleware
添加了 StatusCodePagesMiddleware

❸ The StatusCodePagesMiddleware automatically adds a Problem Details body to the 404 response.
StatusCodePagesMiddleware 会自动将 Problem Details 正文添加到 404 响应中。

The StatusCodePagesMiddleware, coupled with exception-handling middleware, ensures that your API returns a Problem Details response for all error responses.

StatusCodePagesMiddleware 与异常处理中间件相结合,可确保 API 为所有错误响应返回 Problem Details 响应。

Tip You can also customize how the Problem Details response is generated by passing parameters to the AddProblemDetails() method or by implementing your own IProblemDetailsService.
提示 您还可以通过向 AddProblemDetails() 方法传递参数或通过实现您自己的 IProblemDetailsService 来自定义生成问题详细信息响应的方式。

So far in section 5.3, I’ve described returning objects as JSON, returning strings as text, and returning custom status codes and Problem Details by using Results. Sometimes, however, you need to return something bigger, such as a file or a binary. Luckily, you can use the convenient Results class for that task too.
到目前为止,在第 5.3 节中,我已经介绍了以 JSON 格式返回对象、以文本格式返回字符串以及使用 Results 返回自定义状态代码和问题详细信息。但是,有时您需要返回更大的内容,例如文件或二进制文件。幸运的是,您也可以使用方便的 Results 类来完成该任务。

5.3.4 Returning other data types

5.3.4 返回其他数据类型

The methods on Results and TypedResults are convenient ways of returning common responses, so it’s only natural that they include helpers for other common scenarios, such as returning a file or binary data:
Results 和 TypedResults 上的方法是返回常见响应的便捷方法,因此它们包含其他常见方案(例如返回文件或二进制数据)的帮助程序是很自然的:

  • Results.File()—Pass in the path of the file to return, and ASP.NET Core takes care of streaming it to the client.
    Results.File() - 传入要返回的文件的路径,ASP.NET Core 负责将其流式传输到客户端。

  • Results.Byte()—For returning binary data, you can pass this method a byte[] to return.
    Results.Byte() — 要返回二进制数据,您可以向此方法传递一个 byte[] 以返回。

  • Results.Stream()—You can send data to the client asynchronously by using a Stream.
    Results.Stream() — 您可以使用 Stream 将数据异步发送到客户端。

In each of these cases, you can provide a content type for the data, and a filename to be used by the client. Browsers offer to save binary data files using the suggested filename. The File and Byte methods even support range requests by specifying enableRangeProcessing as true.
在上述每种情况下,您都可以为数据提供内容类型,以及客户端要使用的文件名。浏览器提供使用建议的文件名保存二进制数据文件。File 和 Byte 方法甚至通过将 enableRangeProcessing 指定为 true 来支持范围请求。

Definition Clients can create range requests using the Range header to request a specific range of bytes from the server instead of the whole file, reducing the bandwidth required for a request. When range requests are enabled for Results.File() or Results.Byte(), ASP.NET Core automatically handles generating an appropriate response. You can read more about range requests at http://mng.bz/Wzd0.
定义 客户端可以使用 Range 标头创建范围请求,以从服务器而不是整个文件请求特定范围的字节,从而减少请求所需的带宽。当为 Results.File() 或 Results.Byte() 启用范围请求时,ASP.NET Core 会自动处理生成适当的响应。您可以在 http://mng.bz/Wzd0 中阅读有关范围请求的更多信息。

If the built-in Results helpers don’t provide the functionality you need, you can always fall back to creating a response manually, as in listing 5.4. If you find yourself creating the same manual response several times, you could consider creating a custom IResult type to encapsulate this logic. I show how to create a custom IResult that returns XML and registers it as an extension in this blog post: http://mng.bz/8rNP.

如果内置的 Results 帮助程序没有提供你需要的功能,你总是可以回退到手动创建响应,如清单 5.4 所示。如果您发现自己多次创建相同的手动响应,则可以考虑创建自定义 IResult 类型来封装此逻辑。在以下博客文章中,我将介绍如何创建返回 XML 并将其注册为扩展的自定义 IResult:http://mng.bz/8rNP

5.4 Running common code with endpoint filters

使用终端节点筛选器运行通用代码

In section 5.3 you learned how to use Results to return different responses when the request isn’t valid. We’ll look at validation in more detail in chapter 7, but in this section, you’ll learn how to use filters to extract common code that executes before (or after) an endpoint executes.

在第 5.3 节中,您学习了如何在请求无效时使用 Results 返回不同的响应。我们将在第 7 章中更详细地介绍验证,但在本节中,您将学习如何使用过滤器来提取在端点执行之前(或之后)执行的常见代码。

Let’s start by adding some extra validation to the fruit API from listing 5.5. The following listing adds an additional check to the MapGet endpoint to ensure that the provided id isn’t empty and that it starts with the letter f.

让我们首先向清单 5.5 中的 fruit API 添加一些额外的验证。下面的清单向 MapGet 端点添加了一项额外的检查,以确保提供的 id 不为空,并且它以字母 f 开头。

Listing 5.8 Adding basic validation to minimal API endpoints
清单 5.8 向最小API添加基本验证端点

using System.Collections.Concurrent;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();

var _fruit = new ConcurrentDictionary<string, Fruit>();

app.MapGet("/fruit/{id}", (string id) =>
{
    if (string.IsNullOrEmpty(id) || !id.StartsWith('f')) ❶
    {
        return Results.ValidationProblem(new Dictionary<string, string[]>
        {
            {"id", new[] {"Invalid format. Id must start with 'f'"}}
        });
    }
    return _fruit.TryGetValue(id, out var fruit)
        ? TypedResults.Ok(fruit)
        : Results.Problem(statusCode: 404);
});

app.Run()

❶ Adds extra validation that the provided id has the required format
添加额外的验证,证明提供的 ID 具有所需的格式

Even though this check is basic, it starts to clutter our endpoint handler, making it harder to read what the endpoint is doing. One improvement would be to move the validation code to a helper function. But you’re still inevitably going to clutter your endpoint handlers with calls to methods that are tangential to the main function of your endpoint.

尽管此检查是基本的,但它开始使我们的 endpoint 处理程序变得混乱,从而更难读取 endpoint 正在做什么。一项改进是将验证代码移动到 helper 函数。但是,您仍然不可避免地会因为对与终端节点的主函数相切的方法的调用而使终端节点处理程序变得混乱。

Note Chapter 7 discusses additional validation patterns in detail.
注意 第 7 章详细讨论了其他验证模式。

It’s common to perform various cross-cutting activities for every endpoint. I’ve already mentioned validation; other cross-cutting activities include logging, authorization, and auditing. ASP.NET Core has built-in support for some of these features, such as authorization (chapter 24), but you’re likely to have some common code that doesn’t fit into the specific pigeonholes of validation or authorization.

通常为每个端点执行各种横切活动。我已经提到了验证;其他横切活动包括日志记录、授权和审计。ASP.NET Core 内置了对其中一些功能的支持,例如授权(第 24 章),但您可能有一些通用代码不适合验证或授权的特定分类。

Luckily, ASP.NET Core includes a feature in minimal APIs for running these tangential concerns: endpoint filters. You can specify a filter for an endpoint by calling AddEndpointFilter()on the result of a call to MapGet (or similar) and passing in a function to execute. You can even add multiple calls to AddEndpointFilter(), which builds up an endpoint filter pipeline, analogous to the middleware pipeline. Figure 5.8 shows that the pipeline is functionally identical to the middleware pipeline in figure 4.3.

幸运的是,ASP.NET Core 在最小的 API 中包含一个功能,用于运行这些无关紧要的问题:终端节点筛选器。您可以通过对 MapGet(或类似)的调用结果调用 AddEndpointFilter() 并传入要执行的函数来为终端节点指定过滤器。您甚至可以添加对 AddEndpointFilter() 的多个调用,这将构建一个端点过滤器管道,类似于中间件管道。图 5.8 显示,该管道在功能上与图 4.3 中的中间件管道相同。

alt text

Figure 5.8. The endpoint filter pipeline. Filters execute code and then call next(context) to invoke the next filter in the pipeline. If there are no more filters in the pipeline, the endpoint handler is invoked. After the handler has executed, the filters may run further code.
图 5.8 端点过滤器管道。筛选器执行代码,然后调用 next(context) 以调用管道中的下一个筛选器。如果管道中没有更多筛选器,则调用终端节点处理程序。处理程序执行后,过滤器可以运行更多代码。

Each endpoint filter has two parameters: a context parameter, which provides details about the selected endpoint handler, and the next parameter, which represents the filter pipeline. When you invoke the methodlike next parameter by calling next(context), you invoke the remainder of the filter pipeline. If there are no more filters in the pipeline, you invoke the endpoint handler, as shown in figure 5.8.

每个终端节点筛选条件都有两个参数:一个 context 参数(提供有关所选终端节点处理程序的详细信息)和 next 参数(表示筛选条件管道)。当您通过调用 next(context) 调用类似 next 参数的 methodlike 时,将调用筛选管道的其余部分。如果管道中,您可以调用端点处理程序,如图 5.8 所示。

Listing 5.9 shows how to run the same validation logic you saw in listing 5.8 in an endpoint filter. The filter function accesses the endpoint method arguments by using the context.GetArgument() function, passing in a position; 0 is the first argument of your endpoint handler, 1 is the second argument, and so on. If the argument isn’t valid, the filter function returns an IResult object response. If the argument is valid, the filter calls await next(context) instead, executing the endpoint handler.

清单 5.9 展示了如何在端点过滤器中运行清单 5.8 中看到的相同验证逻辑。filter 函数使用上下文访问端点方法参数。GetArgument() 函数,传入一个位置;0 是终端节点处理程序的第一个参数,1 是第二个参数,依此类推。如果参数无效,则 filter 函数将返回 IResult 对象响应。如果参数有效,则筛选器将改为调用 await next(context),并执行端点处理程序。

Listing 5.9 Using AddEndpointFilter to extract common code
清单 5.9 使用 AddEndpointFilter 提取通用代码

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
var _fruit = new ConcurrentDictionary<string, Fruit>();

app.MapGet("/fruit/{id}", (string id) =>
    _fruit.TryGetValue(id, out var fruit)
        ? TypedResults.Ok(fruit)
        : Results.Problem(statusCode: 404))
    .AddEndpointFilter(ValidationHelper.ValidateId); ❶

app.Run();

class ValidationHelper
{
    internal static async ValueTask<object?> ValidateId( ❷
        EndpointFilterInvocationContext context, ❸
        EndpointFilterDelegate next) ❹
    {
        var id = context.GetArgument<string>(0); ❺
        if (string.IsNullOrEmpty(id) || !id.StartsWith('f'))
        {
            return Results.ValidationProblem(
                new Dictionary<string, string[]>
                {
                    {"id", new[]{"Invalid format. Id must start with 'f'"}}
                });
        }

        return await next(context); ❻
    }
}  

❶ Adds the filter to the endpoint using AddEndpointFilter
使用 AddEndpointFilter 将筛选器添加到端点

❷ The method must return a ValueTask.
该方法必须返回 ValueTask。

❸ context exposes the endpoint method arguments and the HttpContext.
context 公开端点方法参数和 HttpContext。

❹ next represents the filter method (or endpoint) that will be called next.
next 表示接下来将调用的 filter 方法 (或 endpoint)。

❺ You can retrieve the method arguments from the context.
您可以从上下文中检索方法参数。

❻ Calling next executes the remaining filters in the pipeline.
调用 next 将执行管道中的剩余过滤器。

Note The EndpointFilterDelegate is a named delegate type. It’s effectively a Func<EndpointFilterInvocationContext, ValueTask<object?>>.
注意 EndpointFilterDelegate 是一种命名委托类型。它实际上是 Func<EndpointFilterInvocationContext, ValueTask<object?>>。

There are many parallels between the middleware pipeline and the filter endpoint pipeline, and we’ll explore them in section 5.4.1.
middleware pipeline 和 filter endpoint pipeline 之间有许多相似之处,我们将在 Section 5.4.1 中探讨它们。

5.4.1 Adding multiple filters to an endpoint

5.4.1 向终端节点 添加多个筛选条件

The middleware pipeline is typically the best place for handling cross-cutting concerns such as logging, authentication, and authorization, as these functions apply to all requests. Nevertheless, it can be common to have additional cross-cutting concerns that are endpoint-specific, as we’ve already discussed. If you need many endpoint-specific operations, you might consider using multiple endpoint filters.

中间件管道通常是处理横切关注点(如日志记录、身份验证和授权)的最佳位置,因为这些功能适用于所有请求。尽管如此,正如我们已经讨论过的,通常还会有特定于端点的其他横切关注点。如果您需要许多特定于终端节点的作,则可以考虑使用多个终端节点筛选条件。

As you saw in figure 5.8, adding multiple filters to an endpoint builds up a pipeline. Like the middleware pipeline, the endpoint filter pipeline can execute code both before and after the rest of the pipeline executes. Similarly, the filter pipeline can short-circuit in the same way as the middleware pipeline by returning a result and not calling next.

如图 5.8 所示,向端点添加多个过滤器会构建一个管道。与中间件管道一样,终端节点筛选器管道可以在管道的其余部分执行之前和之后执行代码。同样,filter 管道可以像中间件管道一样短路,方法是返回 result 而不调用 next。

Note You’ve already seen an example of a short circuit in the filter pipeline. In listing 5.9 we short-circuit the pipeline if the id is invalid by returning a Problem Details object instead of calling next(context).
注意 您已经看到了 filter pipeline 中短路的示例。在示例 5.9 中,如果 id 无效,我们通过返回 Problem Details 对象而不是调用 next(context) 来短路管道。

As with middleware, the order in which you add filters to the endpoint filter pipeline is important. The filters you add first are called first in the pipeline, and filters you add last are called last. On the return journey through the pipeline, after the endpoint handler is invoked, the filters are called in reverse order, as with the middleware pipeline. As an example, consider the following listing, which adds an extra filter to the endpoint shown in listing 5.9.

与中间件一样,将筛选器添加到终端节点筛选器管道的顺序也很重要。您首先添加的过滤器在管道中称为 first,您最后添加的过滤器称为 last。在通过管道的返回旅程中,调用终端节点处理程序后,过滤器将按相反的顺序调用,就像中间件管道一样。例如,考虑下面的清单,它向清单 5.9 中所示的端点添加了一个额外的过滤器。

Listing 5.10 Adding multiple filters to the endpoint filter pipeline
列表 5.10 向端点添加多个过滤器过滤管道

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
var _fruit = new ConcurrentDictionary<string, Fruit>();

app.MapGet("/fruit/{id}", (string id) =>
    _fruit.TryGetValue(id, out var fruit)
        ? TypedResults.Ok(fruit)
        : Results.Problem(statusCode: 404))
    .AddEndpointFilter(ValidationHelper.ValidateId) ❶
    .AddEndpointFilter(async (context, next) => ❷
    {
        app.Logger.LogInformation("Executing filter..."); ❸
        object? result = await next(context); ❹
        app.Logger.LogInformation($"Handler result: {result}"); ❺
        return result; ❻
    });

app.Run();

❶ Adds the validation filter as before
像以前一样添加验证过滤器

❷ Adds a new filter using a lambda function
使用 lambda 函数添加新的筛选条件

❸ Logs a message before executing the rest of the pipeline
在执行管道的其余部分之前记录一条消息

❹ Executes the remainder of the pipeline and the endpoint handler
执行管道的其余部分和端点处理程序

❺ Logs the result returned by the rest of the pipeline
记录管道其余部分返回的结果

❻ Returns the result unmodified
返回未经修改的结果

The extra filter is implemented as a lambda function and simply writes a log message when it executes. Then it runs the rest of the filter pipeline (which contains only the endpoint handler in this example) and logs the result returned by the pipeline. Chapter 26 covers logging in detail. For this example, we’ll look at the logs written to the console.

额外的筛选条件作为 lambda 函数实现,只需在执行时写入日志消息。然后,它运行筛选条件管道的其余部分(在本例中仅包含终端节点处理程序)并记录管道返回的结果。第 26 章详细介绍了日志记录。在此示例中,我们将查看写入控制台的日志。

Figure 5.9 shows the log messages written when we send two requests to the API in listing 5.10. The first request is for an entry that exists, so it returns a 200 OK result. The second request uses an invalid id format, so the first filter rejects it. Figure 5.9 shows that neither the second filter nor the endpoint handler runs in this case; the filter pipeline has been short-circuited.
图 5.9 显示了我们向 API 发送两个请求时写入的日志消息 5.10.第一个请求是针对存在的条目,因此它返回 200 OK 结果。第二个请求使用无效的 id 格式,因此第一个筛选条件会拒绝它。图 5.9 显示在这种情况下,第二个过滤器和端点处理程序都没有运行;过滤器管道已短路。

alt text

Figure 5.9 Sending two requests to the API from listing 5.10. The first request is valid, so both filters execute. An invalid id is provided in the second request, so the first filter short-circuits the requests, and the second filter doesn’t execute.
图 5.9 向清单 5.10 中的 API 发送两个请求.第一个请求有效,因此两个筛选条件都执行。第二个请求中提供了无效的 ID,因此第一个筛选条件会使请求短路,第二个筛选条件不会执行。

By adding calls to AddEndpointFilter, you can create arbitrarily large endpoint filter pipelines, but the fact that you can doesn’t mean you should. Moving code to filters can reduce clutter in your endpoints, but it makes the flow of your application harder to understand. I suggest that you avoid using filters unless you find duplicated code in multiple endpoints, and then favor a filter over a simple method call only if it significantly simplifies the code required.

通过添加对 AddEndpointFilter 的调用,您可以创建任意大的终端节点筛选器管道,但您可以创建的事实并不意味着您应该这样做。将代码移动到筛选器可以减少终端节点中的混乱,但会使应用程序流更难理解。我建议您避免使用筛选器,除非您在多个终端节点中发现重复的代码,然后只有在它显著简化了所需的代码时,才使用筛选器而不是简单的方法调用。

5.4.2 Filters or middleware: Which should you choose?

5.4.2 过滤器或中间件:您应该选择哪个?

The endpoint filter pipeline is similar to the middleware pipeline in many ways, but you should consider several subtle differences when deciding which approach to use. The similarities include three main parallels:

终端节点筛选管道在许多方面与中间件管道相似,但在决定使用哪种方法时,应考虑几个细微的差异。相似之处包括三个主要的相似之处:

  • Requests pass through a middleware component on the way in, and responses pass through again on the way out. Similarly, endpoint filters can run code before calling the next filter in the pipeline and can run code after the response is generated, as shown in figure 5.8.
    请求在传入时通过中间件组件,响应在传出时再次传递。同样,终端节点筛选器可以在调用管道中的下一个筛选器之前运行代码,并且可以在生成响应后运行代码,如图 5.8 所示。

  • Middleware can short-circuit a request by returning a response instead of passing it on to later middleware. Filters can also short-circuit the filter pipeline by returning a response.
    中间件可以通过返回响应而不是将其传递给后续中间件来使请求短路。筛选器还可以通过返回响应来使筛选器管道短路。

  • Middleware is often used for cross-cutting application concerns, such as logging, performance profiling, and exception handling. Filters also lend themselves to cross-cutting concerns.
    中间件通常用于横切应用程序问题,例如日志记录、性能分析和异常处理。过滤器还适用于横切关注点。

By contrast, there are three main differences between middleware and filters:

相比之下,中间件和过滤器之间有三个主要区别:

  • Middleware can run for all requests; filters will run only for requests that reach the EndpointMiddleware and execute the associated endpoint.
    Filters have access to additional details about the endpoint that will execute, such as the return value of the endpoint, such as an IResult.
    中间件可以针对所有请求运行filters 将仅针对到达 EndpointMiddleware 并执行关联端点的请求运行。筛选器可以访问有关将要执行的终结点的其他详细信息,例如终结点的返回值,例如 IResult。

  • Middleware in general won’t see these intermediate steps, so it sees only the generated response.
    中间件通常不会看到这些中间步骤,因此它只能看到生成的响应。

  • Filters can easily be restricted to a subset of requests, such as a single endpoint or a group of endpoints. Middleware generally applies to all requests (though you can achieve something similar with custom middleware components).
    筛选器可以轻松地限制为请求的子集,例如单个终端节点或一组终端节点。中间件通常适用于所有请求(尽管您可以使用自定义中间件组件实现类似的功能)。

That’s all well and good, but how should we interpret these differences? When should we choose one over the other?
这一切都很好,但我们应该如何解释这些差异呢?我们什么时候应该选择一个而不是另一个?

I like to think of middleware versus filters as a question of specificity. Middleware is the more general concept, operating on lower-level primitives such as HttpContext, so it has wider reach. If the functionality you need has no endpoint-specific requirements, you should use a middleware component. Exception handling is a great example; exceptions could happen anywhere in your application, and you need to handle them, so using exception-handling middleware makes sense.

我喜欢将中间件与过滤器视为一个特异性问题。中间件是更通用的概念,它在 HttpContext 等较低级别的原语上运行,因此它的范围更广。如果您需要的功能没有特定于端点的要求,则应使用中间件组件。异常处理就是一个很好的例子;异常可能发生在应用程序中的任何位置,您需要处理它们,因此使用异常处理中间件是有意义的。

On the other hand, if you do need access to endpoint details, or if you want to behave differently for some requests, you should consider using a filter. Validation is a good example. Not all requests need the same validation. Requests for static files, for example, don’t need parameter validation, the way requests to an API endpoint do. Applying validation to the endpoints via filters makes sense in this case.

另一方面,如果您确实需要访问终端节点详细信息,或者您希望对某些请求采取不同的行为,则应考虑使用筛选条件。验证就是一个很好的例子。并非所有请求都需要相同的验证。例如,对静态文件的请求不需要参数验证,对 API 终端节点的请求方式。在这种情况下,通过过滤器对终端节点应用验证是有意义的。

Tip Where possible, consider using middleware for cross-cutting concerns. Use filters when you need different behavior for different endpoints or where the functionality relies on endpoint concepts such as IResult objects.
提示 在可能的情况下,考虑将中间件用于横切关注点。当您需要对不同的终端节点进行不同的行为时,或者当功能依赖于终端节点概念(如 IResult 对象)时,请使用过滤器。

So far, the filters we’ve looked at have been specific to a single endpoint. In section 5.4.3 we look at creating generic filters that you can apply to multiple endpoints.

到目前为止,我们查看的筛选器特定于单个终结点。在第 5.4.3 节中,我们将介绍如何创建可应用于多个终端节点的通用过滤器。

5.4.3 Generalizing your endpoint filters

5.4.3 通用化终端节点筛选条件

One common problem with filters is that they end up closely tied to the implementation of your endpoint handlers. Listing 5.9, for example, assumes that the id parameter is the first parameter in the method. In this section you’ll learn how to create generalized versions of filters that work with multiple endpoint handlers.

筛选器的一个常见问题是,它们最终与终结点处理程序的实现密切相关。例如,清单 5.9 假设 id 参数是方法中的第一个参数。在本节中,您将学习如何创建使用多个终端节点处理程序的 filters 的通用版本。

The fruit API we’ve been working with in this chapter contains several endpoint handlers that take multiple parameters. The MapPost handler, for example, takes a string id parameter and a Fruit fruit parameter:

我们在本章中使用的 fruit API 包含几个采用多个参数的端点处理程序。例如,MapPost 处理程序采用字符串 id 参数和 Fruit fruit 参数:

app.MapPost("/fruit/{id}", (string id, Fruit fruit) => { /* */ });

In this example, the id parameter is listed first, but there’s no requirement for that to be the case. The parameters to the handler could be reversed, and the endpoint would be functionally identical:
在此示例中,首先列出 id 参数,但不需要这样做。处理程序的参数可以反转,并且端点在功能上是相同的:

app.MapPost("/fruit/{id}", (Fruit fruit, string id) => { /* */ });

Unfortunately, with this order, the ValidateId filter described in listing 5.9 won’t work. The ValidateId filter assumes that the first parameter to the handler is id, which isn’t the case in our revised MapPost implementation.

遗憾的是,按照这个顺序,清单 5.9 中描述的 ValidateId 过滤器将不起作用。ValidateId 筛选器假定处理程序的第一个参数是 id,而在我们修订后的 MapPost 实现中,情况并非如此。

ASP.NET Core provides a solution that uses a factory pattern for filters. You can register a filter factory by using the AddEndpointFilterFactory() method. A filter factory is a method that returns a filter function. ASP.NET Core executes the filter factory when it’s building your app and incorporates the returned filter into the filter pipeline for the app, as shown in figure 5.10. You can use the same filter-factory function to emit a different filter for each endpoint, with each filter tailored to the endpoint’s parameters.

ASP.NET Core 提供了一种对筛选器使用工厂模式的解决方案。您可以使用 AddEndpointFilterFactory() 方法注册过滤器工厂。过滤器工厂是一种返回过滤器函数的方法。 ASP.NET Core 在构建应用程序时执行过滤器工厂,并将返回的过滤器合并到应用程序的过滤器管道中,如图 5.10 所示。您可以使用相同的 filter- factory 函数为每个终端节点发出不同的过滤器,每个过滤器都根据终端节点的参数进行定制。

alt text

Figure 5.10 A filter factory is a generalized way to add endpoint filters. The factory reads details about the endpoint, such as its method signature, and builds a filter function. This function is incorporated into the final filter pipeline for the endpoint. The build step means that a single filter factory can create filters for multiple endpoints with different method signatures.
图 5.10 过滤器工厂是添加端点过滤器的一种通用方法。工厂读取有关端点的详细信息,例如其方法签名,并构建一个 filter 函数。此函数将合并到终端节点的最终筛选管道中。构建步骤意味着单个过滤器工厂可以为具有不同方法签名的多个端点创建过滤器。

Listing 5.11 shows an example of the factory pattern in practice. The filter factory is applied to multiple endpoints. For each endpoint, the factory first checks for a parameter called id; if it doesn’t exist, the factory returns next and doesn’t add a filter to the pipeline. If the id parameter exists, the factory returns a filter function, which is virtually identical to the filter function in listing 5.9; the main difference is that this filter handles a variable location of the id parameter.

清单 5.11 显示了实践中工厂模式的一个例子。过滤器工厂应用于多个端点。对于每个终端节点,工厂首先检查名为 ;如果不存在,则 Factory 将返回并且不会向管道添加筛选器。如果参数存在,工厂将返回一个 filter 函数,该函数与清单 5.9 中的 filter 函数几乎相同;主要区别在于此筛选器处理参数的可变位置.

Listing 5.11 Using a filter factory to create an endpoint filter
Listing 5.11 使用过滤器工厂创建端点过滤器

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
var _fruit = new ConcurrentDictionary<string, Fruit>();

app.MapGet("/fruit/{id}", (string id) =>
    _fruit.TryGetValue(id, out var fruit)
        ? TypedResults.Ok(fruit)
        : Results.Problem(statusCode: 404))
    .AddEndpointFilterFactory(ValidationHelper.ValidateIdFactory); ❶

app.MapPost("/fruit/{id}", (Fruit fruit, string id) =>
    _fruit.TryAdd(id, fruit)
        ? TypedResults.Created($"/fruit/{id}", fruit)
        : Results.ValidationProblem(new Dictionary<string, string[]>
            {
                { "id", new[] { "A fruit with this id already exists" } }
            }))
    .AddEndpointFilterFactory(ValidationHelper.ValidateIdFactory); ❶
app.Run();

class ValidationHelper
{
    internal static EndpointFilterDelegate ValidateIdFactory(
        EndpointFilterFactoryContext context, ❷
        EndpointFilterDelegate next)
    {
        ParameterInfo[] parameters = ❸
            context.MethodInfo.GetParameters(); ❸
        int? idPosition = null;
        for (int i = 0; i < parameters.Length; i++) ❹
        { ❹
            if (parameters[i].Name == "id" && ❹
             parameters[i].ParameterType == typeof(string)) ❹
            { ❹
                idPosition = i; ❹
                break; ❹
            } ❹
        } ❹

        if (!idPosition.HasValue) ❺
        { ❺
            return next; ❺
        } ❺

        return async (invocationContext) => ❻
        {
            var id = invocationContext ❼
                .GetArgument<string>(idPosition.Value); ❼
            if (string.IsNullOrEmpty(id) || !id.StartsWith('f')) ❼
            { ❼
                return Results.ValidationProblem( ❼
                    new Dictionary<string, string[]> ❼
                {{ "id", new[] { "Id must start with 'f'" }}}); ❼
            } ❼

            return await next(invocationContext); ❽
        };
    }
}

❶ The filter factory can handle endpoints with different method signatures.
过滤器工厂可以处理具有不同方法签名的端点。

❷ The context parameter provides details about the endpoint handler method.
context 参数提供有关端点处理程序方法的详细信息。

❸ GetParameters() provides details about the parameters of the handler being called.
GetParameters() 提供有关正在调用的处理程序的参数的详细信息。

❹ Loops through the parameters to find the string id parameter and record its position
遍历参数以找到字符串 id 参数并记录其位置

❺ If the id parameter isn’t not found, doesn’t add a filter, but returns the remainder of the pipeline
如果未找到 id 参数,则不添加筛选器,但返回管道的其余部分

❻ If the id parameter exists, returns a filter function (the filter executed for the endpoint)
如果 id 参数存在,则返回一个 filter 函数(为终端节点执行的 filter)

❼ If the id isn’t valid, returns a Problem Details result
如果 ID 无效,则返回 Problem Details 结果

❽ If the id is valid, executes the next filter in the pipeline
如果 id 有效,则执行管道中的下一个过滤器

The code in listing 5.11 is more complex than anything else we’ve seen so far, as it has an extra layer of abstraction. The endpoint middleware passes an EndpointFilterFactoryContext object to the factory function, which contains extra details about the endpoint in comparison to the context passed to a normal filter function. Specifically, it includes a MethodInfo property and an EndpointMetadata property.

清单 5.11 中的代码比我们目前看到的任何其他代码都要复杂,因为它有一个额外的抽象层。端点中间件将 EndpointFilterFactoryContext 对象传递给工厂函数,与传递给普通 filter 函数的上下文相比,该对象包含有关端点的额外详细信息。具体来说,它包括 MethodInfo 属性和 EndpointMetadata 属性。

Note You’ll learn about endpoint metadata in chapter 6.
注意: 您将在第 6 章中了解终端节点元数据。

The MethodInfo property can be used to control how the filter is created based on the definition of the endpoint handler. Listing 5.11 shows how you can loop through the parameters to check for the details you need—a string id parameter, in this case—and customize the filter function you return.
MethodInfo 属性可用于控制如何根据端点处理程序的定义创建过滤器。清单 5.11 展示了如何遍历参数来检查所需的细节 — 在本例中为 string id 参数 — 并自定义返回的 filter 函数。

If you find all these method signatures to be confusing, I don’t blame you. Remembering the difference between an EndpointFilterFactoryContext and EndpointFilterInvocationContext and then trying to satisfy the compiler with your lambda methods can be annoying. Sometimes, you yearn for a good ol’ interface to implement. Let’s do that now.

如果您发现所有这些方法签名都令人困惑,我不怪您。记住 EndpointFilterFactoryContext 和 EndpointFilterInvocationContext 之间的区别,然后尝试使用 lambda 方法满足编译器可能会很烦人。有时,您渴望实现一个好的 ol' 接口。我们现在就开始吧。

5.4.4 Implementing the IEndpointFilter interface

5.4.4 实现 IEndpointFilter 接口

Creating a lambda method for AddEndpointFilter() that satisfies the compiler can be a frustrating experience, depending on the level of support your integrated development environment (IDE) provides. In this section you’ll learn how to sidestep the issue by defining a class that implements IEndpointFilter instead.

为 AddEndpointFilter() 创建满足编译器的 lambda 方法可能是一种令人沮丧的体验,具体取决于集成开发环境 (IDE) 提供的支持级别。在本节中,您将学习如何通过定义一个实现 IEndpointFilter 的类来回避这个问题。

You can implement IEndpointFilter by defining a class with an InvokeAsync() that has the same signature as the lambda defined in listing 5.9. The advantage of using IEndpointFilter is that you get IntelliSense and autocompletion for the method signature. The following listing shows how to implement an IEndpointFilter class that’s equivalent to listing 5.9.

您可以通过使用 InvokeAsync() 定义一个类来实现 IEndpointFilter,该类与清单 5.9 中定义的 lambda 具有相同的签名。使用 IEndpointFilter 的优点是您可以获得方法签名的 IntelliSense 和自动完成。下面的清单显示了如何实现一个等效于清单 5.9 的 IEndpointFilter 类。

Listing 5.12 Implementing IEndpointFilter
清单 5.12 实现IEndpointFilter

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
var _fruit = new ConcurrentDictionary<string, Fruit>();

app.MapGet("/fruit/{id}", (string id) =>
    _fruit.TryGetValue(id, out var fruit)
        ? TypedResults.Ok(fruit)
        : Results.Problem(statusCode: 404))
    .AddEndpointFilter<IdValidationFilter>(); ❶

app.Run();

class IdValidationFilter : IEndpointFilter ❷
{
    public async ValueTask<object?> InvokeAsync( ❸
        EndpointFilterInvocationContext context, ❸
        EndpointFilterDelegate next) ❸
    {
        var id = context.GetArgument<string>(0);
        if (string.IsNullOrEmpty(id) || !id.StartsWith('f'))
        {
            return Results.ValidationProblem(
                new Dictionary<string, string[]>
                {
                    {"id", new[]{"Invalid format. Id must start with 'f'"}}
                });
        }

    return await next(context);
    }
}

❶ Adds the filter using the generic AddEndpointFilter method
使用泛型 AddEndpointFilter 方法添加筛选器

❷ The filter must implement IEndpointFilter . . .
筛选器必须实现 IEndpointFilter . . . .

❸ . . . which requires implementing a single method.
. . . .这需要实现单个方法。

Implementing IEndpointFilter is a good option when your filters become more complex, but note that there’s no equivalent interface for the filter-factory pattern shown in section 5.4.3. If you want to generalize your filters with a filter factory, you’ll have to stick to the lambda (or helper-method) approach shown in listing 5.11.

当筛选器变得更加复杂时,实现 IEndpointFilter 是一个不错的选择,但请注意,第 5.4.3 节中所示的筛选器工厂模式没有等效的接口。如果你想用一个过滤器工厂来推广你的过滤器,你就必须坚持使用 lambda(或辅助方法)方法,如清单 5.11 所示。

5.5 Organizing your APIs with route groups

使用路由组组织 API

One criticism levied against minimal APIs in .NET 6 was that they were necessarily quite verbose, required a lot of duplicated code, and often led to large endpoint handler methods. .NET 7 introduced two new mechanisms to address these critiques:

对 .NET 6 中最小 API 的一个批评是,它们必然非常冗长,需要大量重复的代码,并且经常导致大型端点处理程序方法。.NET 7 引入了两种新机制来解决这些批评:

  • Filters—Introduced in section 5.4, filters help separate validation checks and cross-cutting functions such as logging from the important logic in your endpoint handler functions.
    过滤器 — 在第 5.4 节中介绍,过滤器有助于将验证检查和横切函数(如日志记录)与端点处理程序函数中的重要逻辑分开。

  • Route groups—Described in this section, route groups help reduce duplication by applying filters and routing to multiple handlers at the same time.
    路由组 — 本节介绍了路由组,通过同时将筛选条件和路由应用于多个处理程序来帮助减少重复。

When designing APIs, it’s important to maintain consistency in the routes you use for your endpoints, which often means duplicating part of the route pattern across multiple APIs. As an example, all the endpoints in the fruit API described throughout this chapter (such as in listing 5.3) start with the route prefix /fruit:
在设计 API 时,保持用于终端节点的路由的一致性非常重要,这通常意味着跨多个 API 复制部分路由模式。例如,本章中描述的 fruit API 中的所有端点(例如清单 5.3)都以路由前缀 /fruit 开头:

  • MapGet("/fruit", () => {/ /})

  • MapGet("/fruit/{id}", (string id) => {/ /})

  • MapPost("/fruit/{id}", (Fruit fruit, string id) => {/ /})

  • MapPut("/fruit/{id}", (Fruit fruit, string id) => {/ /})

  • MapDelete("/fruit/{id}", (string id) => {/ /})

Additionally, the last four endpoints need to validate the id parameter. This validation can be extracted to a helper method and applied as a filter, but you still need to remember to apply the filter when you add a new endpoint.

此外,最后四个端点需要验证 id 参数。此验证可以提取到帮助程序方法并作为筛选器应用,但您仍需要记住在添加新终端节点时应用筛选器。

All this duplication can be removed by using route groups. You can use route groups to extract common path segments or filters to a single location, reducing the duplication in your endpoint definitions. You create a route group by calling MapGroup("/fruit") on the WebApplication instance, providing a route prefix for the group ("/fruit", in this case), and MapGroup() returns a RouteGroupBuilder.

有这些重复都可以通过使用路由组来删除。您可以使用路由组将公共路径段或筛选条件提取到单个位置,从而减少终端节点定义。通过在 WebApplication 实例上调用 MapGroup(“/fruit”) 来创建路由组,为该组提供路由前缀(在本例中为 “/fruit”),MapGroup() 将返回 RouteGroupBuilder。

When you have a RouteGroupBuilder, you can call the same Map* extension methods on RouteGroupBuilder as you do on WebApplication. The only difference is that all the endpoints you define on the group will have the prefix "/fruit" applied to each endpoint you define, as shown in figure 5.11. Similarly, you can call AddEndpointFilter() on a route group, and all the endpoints on the group will also use the filter.

当您拥有 RouteGroupBuilder 时,您可以在 RouteGroupBuilder 上调用与在 WebApplication 上相同的 Map* 扩展方法。唯一的区别是,您在组上定义的所有端点都将将前缀 “/fruit” 应用于您定义的每个端点,如图 5.11 所示。同样,您可以在路由组上调用 AddEndpointFilter(),该组上的所有端点也将使用该过滤器。

alt text

Figure 5.11 Using route groups to simplify the definition of endpoints. You can create a route group by calling MapGroup() and providing a prefix. Any endpoints created on the route group inherit the route template prefix, as well as any filters added to the group.
图 5.11 使用路由组简化终端节点的定义。您可以通过调用 MapGroup() 并提供前缀来创建路由组。在路由组上创建的任何终端节点都会继承路由模板前缀,以及添加到组的任何筛选条件。

You can even create nested groups by calling MapGroup() on a group. The prefixes are applied to your endpoints in order, so the first MapGroup() call defines the prefix used at the start of the route. app.MapGroup("/fruit").MapGroup("/citrus"), for example, would have the prefix "/fruit/citrus".

您甚至可以通过对组调用 MapGroup() 来创建嵌套组。前缀按顺序应用于您的终端节点,因此第一个 MapGroup() 调用定义使用的前缀在路线的起点处。应用程序。MapGroup(“/fruit”) 的例如,MapGroup(“/citrus”) 将具有前缀 “/fruit/citrus”。

Tip If you don’t want to add a prefix but still want to use the route group for applying filters, you can pass the prefix "/" to MapGroup().
提示 如果您不想添加前缀,但仍想使用路由组来应用筛选条件,则可以将前缀“/”传递给 MapGroup()。

Listing 5.13 shows an example of rewriting the fruit API to use route groups. It creates a top-level fruitApi, which applies the "/fruit" prefix, and creates a nested route group called fruitApiWithValidation for the endpoints that require a filter. You can find the complete example comparing the versions with and without route groups in the source code for this chapter.

清单 5.13 展示了一个重写 fruit API 以使用路由组的示例。它创建一个顶级 fruitApi,该 fruitApi 应用“/fruit”前缀,并为需要筛选条件的终端节点创建一个名为 fruitApiWithValidation 的嵌套路由组。您可以在本章的源代码中找到比较带和不带路由组的版本的完整示例。

Listing 5.13 Reducing duplication with route groups
清单 5.13 减少路由组的重复

using System.Collections.Concurrent;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();

var _fruit = new ConcurrentDictionary<string, Fruit>();

RouteGroupBuilder fruitApi = app.MapGroup("/fruit"); ❶

fruitApi.MapGet("/", () => _fruit); ❷

RouteGroupBuilder fruitApiWithValidation = fruitApi.MapGroup("/") ❸
    .AddEndpointFilter(ValidationHelper.ValidateIdFactory); ❹

fruitApiWithValidation.MapGet("/{id}", (string id) => ❺
    _fruit.TryGetValue(id, out var fruit)
        ? TypedResults.Ok(fruit)
        : Results.Problem(statusCode: 404));

fruitApiWithValidation.MapPost("/{id}", (Fruit fruit, string id) => ❺
    _fruit.TryAdd(id, fruit)
        ? TypedResults.Created($"/fruit/{id}", fruit)
        : Results.ValidationProblem(new Dictionary<string, string[]>
            {
                { "id", new[] { "A fruit with this id already exists" } }
        }));

fruitApiWithValidation.MapPut("/{id}", (string id, Fruit fruit) => ❺
{
    _fruit[id] = fruit;
    return Results.NoContent();
});

fruitApiWithValidation.MapDelete("/fruit/{id}", (string id) => ❺
{
    _fruit.TryRemove(id, out _);
    return Results.NoContent();
});

app.Run();

❶ Creates a route group by calling MapGroup and providing a prefix
通过调用 MapGroup 并提供前缀来创建路由组

❷ Endpoints defined on the route group will have the group prefix prepended to the route.
在路由组上定义的终端节点将在路由前面加上组前缀。

❸ You can create nested route groups with multiple prefixes.
您可以创建具有多个前缀的嵌套路由组。

❹ You can add filters to the route group . . .
您可以向路由组添加过滤器 . . .

❺ . . . and the filter will be applied to all the endpoints defined on the route group.
. . . .筛选条件将应用于路由组上定义的所有终端节点。

In .NET 6, minimal APIs were a bit too verbose to be generally recommended, but with the addition of route groups and filters, minimal APIs have come into their own. In chapter 6 you’ll learn more about routing and route template syntax, as well as how to generate links to other endpoints.
在 .NET 6 中,最小 API 有点过于冗长,通常不推荐使用,但随着路由组和筛选器的添加,最小 API 已经有了自己的功能。在第 6 章中,您将了解有关路由和路由模板语法的更多信息,以及如何生成指向其他终端节点的链接。

5.6 Summary

5.6 总结

  • HTTP verbs define the semantic expectation for a request. GET is used to fetch data, POST creates a resource, PUT creates or replaces a resource, and DELETE removes a resource. Following these conventions will make your API easier to consume.
    HTTP 动词定义请求的语义期望。GET 用于获取数据,POST 创建资源,PUT 创建或替换资源,DELETE 删除资源。遵循这些约定将使 API 更易于使用。

  • Each HTTP response includes a status code. Common codes include 200 OK, 201 Created, 400 Bad Request, and 404 Not Found. It’s important to use the correct status code, as clients use these status codes to infer the behavior of your API.
    每个 HTTP 响应都包含一个状态代码。常见代码包括 200 OK、201 Created、400 错误请求,404 未找到。使用正确的状态代码非常重要,因为客户端使用这些状态代码来推断 API 的行为。

  • An HTTP API exposes methods or endpoints that you can use to access or change data on a server using the HTTP protocol. An HTTP API is typically called by mobile or client-side web applications.
    HTTP API 公开可用于使用 HTTP 协议访问或更改服务器上的数据的方法或端点。HTTP API 通常由移动或客户端 Web 应用程序调用。

  • You define minimal API endpoints by calling Map functions on the WebApplication instance, passing in a route pattern to match and a handler function. The handler functions runs in response to matching requests.
    通过在 WebApplication 实例上调用 Map
    函数,传入要匹配的路由模式和处理程序函数,可以定义最小的 API 端点。处理程序函数运行以响应匹配的请求。

  • There are different extension methods for each HTTP verb. MapGet handles GET requests, for example, and MapPost maps POST requests. You use these extension methods to define how your app handles a given route and HTTP verb.
    每个 HTTP 动词都有不同的扩展方法。例如,MapGet 处理 GET 请求,而 MapPost 映射 POST 请求。您可以使用这些扩展方法来定义您的应用程序如何处理给定的路由和 HTTP 动词。

  • You can define your endpoint handlers as lambda expressions, Func<T, TResult> and Action variables, local functions, instance methods, or static methods. The best approach depends on how complex your handler is, as well as personal preference.
    您可以将终端节点处理程序定义为 lambda 表达式、Func、TResult 和 Action 变量、本地函数、实例方法或静态方法。最好的方法取决于您的处理程序的复杂程度,以及个人喜好。

  • Returning void from your endpoint handler generates a 200 response with no body by default. Returning a string generates a text/plain response. Returning an IResult instance can generate any response. Any other object returned from your endpoint handler is serialized to JSON. This convention helps keep your endpoint handlers succinct.
    默认情况下,从终端节点处理程序返回 void 会生成一个没有正文的 200 响应。返回字符串会生成 text/plain 响应。返回 IResult 实例可以生成任何响应。从终端节点处理程序返回的任何其他对象都将序列化为 JSON。此约定有助于保持终结点处理程序的简洁性。

  • You can customize the response by injecting an HttpResponse object into your endpoint handler and then setting the status code and response body. This approach can be useful if you have complex requirements for an endpoint.
    您可以通过将 HttpResponse 对象注入终端节点处理程序,然后设置状态代码和响应正文来自定义响应。如果您对终端节点有复杂的要求,则此方法可能很有用。

  • The Results and TypedResults helpers contain static methods for generating common responses, such as a 404 Not Found response using Results.NotFound(). These helpers simplifying returning common status codes.
    Results 和 TypedResults 帮助程序包含用于生成常见响应的静态方法,例如使用 Results.NotFound() 的 404 Not Found 响应。这些帮助程序简化了返回常见状态代码的过程。

  • You can return a standard Problem Details object by using Results.Problem() and Results.ValiationProblem(). Problem() generates a 500 response by default (which can be changed), and ValidationProblem() generates a 400 response, with a list of validation errors. These methods make returning Problem Details objects more concise than generating the response manually.
    您可以使用 Results.Problem() 和 Results.ValiationProblem() 返回标准 Problem Details 对象。默认情况下,Problem() 会生成 500 响应(可以更改),而 ValidationProblem() 会生成 400 响应,其中包含验证错误列表。这些方法使返回 Problem Details 对象比手动生成响应更简洁。

  • You can use helper methods to generate other common result types on Results, such as File() for returning a file from disk, Bytes() for returning arbitrary binary data, and Stream() for returning an arbitrary stream.
    您可以使用帮助程序方法在 Results 上生成其他常见的结果类型,例如用于从磁盘返回文件的 File()、用于返回任意二进制数据的 Bytes() 和用于返回任意流的 Stream()。

  • You can extract common or tangential code from your endpoint handlers by using endpoint filters, which can keep your endpoint handlers easy to read.
    您可以使用终端节点筛选条件从终端节点处理程序中提取常见或无关代码,这可以使您的终端节点处理程序易于阅读。

  • Add a filter to an endpoint by calling AddEndpointFilter() and providing the lambda function to run (or use a static/instance method). You can also implement IEndpointFilter and call AddEndpointFilter(), where T is the name of your implementing class.
    通过调用 AddEndpointFilter() 并提供要运行的 lambda 函数(或使用 static/instance 方法),向终端节点添加筛选条件。您还可以实现 IEndpointFilter 并调用 AddEndpointFilter(),其中 T 是实现类的名称。

  • You can generalize your filter functions by creating a factory, using the overload of AddEndpointFilter() that takes an EndpointFilterFactoryContext. You can use this approach to support endpoint handlers with various method signatures.
    你可以通过创建一个工厂来通用你的过滤器函数,使用采用EndpointFilterFactoryContext的AddEndpointFilter()的重载。您可以使用此方法来支持具有各种方法签名的终端节点处理程序。

  • You can reduce duplication in your endpoint routes and filter configuration by using route groups. Call MapGroup() on WebApplication, and provide a prefix. All endpoints created on the returned RouteGroupBuilder will use the prefix in their route templates.
    您可以使用路由组减少终端节点路由和筛选条件配置中的重复。在 WebApplication 上调用 MapGroup() 并提供前缀。在返回的 RouteGroupBuilder 上创建的所有终端节点都将在其路由模板中使用该前缀。

  • You can also call AddEndpointFilter() on route groups. Any endpoints defined on the group will also have the filter, as though you defined them on the endpoint directly, removing the need to duplicate the call on each endpoint.
    您还可以在路由组上调用 AddEndpointFilter()。 在组上定义的任何终端节点也将具有筛选条件,就像您直接在终端节点上定义它们一样,无需在每个终端节点上复制调用。

Leave a Reply

Your email address will not be published. Required fields are marked *