7 Model binding and validation in minimal APIs
7 最小 API 中的模型绑定和验证
This chapter covers
本章涵盖
-
Using request values to create binding models
使用请求值创建绑定模型 -
Customizing the model-binding process
自定义模型绑定过程 -
Validating user input using DataAnnotations attributes
使用 DataAnnotations 属性验证用户输入
In chapter 6 I showed you how to define a route with parameters—perhaps for the unique ID for a product API. But say a client sends a request to the product API. What then? How do you access the values provided in the request and read the JavaScript Object Notation (JSON) in the request body?
在第 6 章中,我向您展示了如何使用参数定义路由 — 可能是为了产品 API 的唯一 ID。但是,假设客户端向产品 API 发送请求。那又如何呢?如何访问请求中提供的值并读取请求正文中的 JavaScript 对象表示法 (JSON)?
For most of this chapter, in sections 7.1-7.9, we’ll look at model binding and how it simplifies reading data from a request in minimal APIs. You’ll see how to take the data posted in the request body or in the URL and bind it to C# objects, which are then passed to your endpoint handler methods as arguments. When your handler executes, it can use these values to do something useful—return a product’s details or change a product’s name, for example.
在本章的大部分内容中,在 7.1-7.9 节中,我们将介绍模型绑定以及它如何简化在最小 API 中从请求中读取数据的过程。您将了解如何获取请求正文或 URL 中发布的数据并将其绑定到 C# 对象,然后将这些对象作为参数传递给您的端点处理程序方法。当您的处理程序执行时,它可以使用这些值来执行一些有用的作,例如,返回产品的详细信息或更改产品的名称。
When your code is executing in an endpoint handler method, you might be forgiven for thinking that you can happily use the binding model without any further thought. Hold on, though. Where did that data come from? From a user—and you know users can’t be trusted! Section 7.10 focuses on how to make sure that the user-provided values are valid and make sense for your app.
当您的代码在终端节点处理程序方法中执行时,您可能会认为无需任何进一步考虑即可愉快地使用绑定模型,这是可以理解的。不过,请稍等。这些数据从何而来?从用户 - 以及你知道用户不可信!第 7.10 节重点介绍如何确保用户提供的值有效且对您的应用有意义。
Model binding is the process of taking the user’s raw HTTP request and making it available to your code by populating plain old CLR objects (POCOs), providing the input to your endpoint handlers. We start by looking at which values in the request are available for binding and where model binding fits in your running app.
模型绑定是通过填充普通旧 CLR 对象 (POCO) 来获取用户的原始 HTTP 请求并将其提供给您的代码的过程,从而为您的端点处理程序提供输入。我们首先查看请求中的哪些值可用于绑定,以及模型绑定在正在运行的应用程序中适合的位置。
7.1 Extracting values from a request with model binding
7.1 使用模型绑定从请求中提取值
In chapters 5 and 6 you learned that route parameters can be extracted from the request’s path and used to execute minimal API handlers. In this section we look in more detail at the process of extracting route parameters and the concept of model binding.
在第 5 章和第 6 章中,您了解了可以从请求的路径中提取路由参数,并用于执行最小的 API 处理程序。在本节中,我们将更详细地了解提取路由参数的过程和模型绑定的概念。
By now, you should be familiar with how ASP.NET Core handles a request by executing an endpoint handler. You’ve also already seen several handlers, similar to
到目前为止,您应该熟悉 ASP.NET Core 如何通过执行终端节点处理程序来处理请求。您也已经看到了几个处理程序,类似于
app.MapPost("/square/{num}", (int num) => num * num);
Endpoint handlers are normal C# methods, so the ASP.NET Core framework needs to be able to call them in the usual way. When handlers accept parameters as part of their method signature, such as num in the preceding example, the framework needs a way to generate those objects. Where do they come from, exactly, and how are they created?
端点处理程序是普通的 C# 方法,因此 ASP.NET Core 框架需要能够以通常的方式调用它们道路。当处理程序接受参数作为其方法签名的一部分时(例如前面示例中的 num),框架需要一种方法来生成这些对象。它们究竟来自哪里,又是如何产生的?
I’ve already hinted that in most cases, these values come from the request itself. But the HTTP request that the server receives is a series of strings. How does ASP.NET Core turn that into a .NET object? This is where model binding comes in.
我已经暗示过,在大多数情况下,这些值来自请求本身。但是服务器收到的 HTTP 请求是一系列字符串。ASP.NET Core 如何将其转换为 .NET 对象?这就是模型绑定的用武之地。
Definition Model binding extracts values from a request and uses them to create .NET objects. These objects are passed as method parameters to the endpoint handler being executed.
DEFINITION 模型绑定从请求中提取值,并使用它们创建 .NET 对象。这些对象作为方法参数传递给正在执行的端点处理程序。
The model binder is responsible for looking through the request that comes in and finding values to use. Then it creates objects of the appropriate type and assigns these values to your model in a process called binding.
模型绑定负责查看传入的请求并查找要使用的值。然后,它会创建适当类型的对象,并在称为 binding 的过程中将这些值分配给模型。
Note Model binding in minimal APIs (and in Razor Pages and Model-View-Controller [MVC]) is a one-way population of objects from the request, not the two-way data binding that desktop or mobile development sometimes uses.
注意 最小 API(以及 Razor Pages 和 Model-View-Controller [MVC])中的模型绑定是来自请求的对象的单向填充,而不是桌面或移动开发有时使用的双向数据绑定。
ASP.NET Core automatically creates the arguments that are passed to your handler by using the request’s properties, such as the request URL, any headers sent in the HTTP request, any data explicitly POSTed in the request body, and so on.
ASP.NET Core 使用请求的属性(例如请求 URL、HTTP 请求中发送的任何标头、请求正文中显式 POST编辑的任何数据等)自动创建传递给处理程序的参数。
Model binding happens before the filter pipeline and your endpoint handler execute, in the EndpointMiddleware, as shown in figure 7.1. The RoutingMiddleware is responsible for matching an incoming request to an endpoint and for extracting the route parameter values, but all the values at that point are strings. It’s only in the EndpointMiddleware that the string values are converted to the real argument types (such as int) needed to execute the endpoint handler.
模型绑定发生在过滤器管道和终端节点处理程序执行之前,在 EndpointMiddleware 中,如图 7.1 所示。RoutingMiddleware 负责将传入请求与终端节点匹配并提取路由参数值,但该点的所有值都是字符串s。只有在 EndpointMiddleware 中,字符串值才会被转换为执行端点处理程序所需的实际参数类型(比如 int)。
Figure 7.1 The RoutingMiddleware matches the incoming request to an endpoint and extracts the route parameters as strings. When the EndpointMiddleware executes the endpoint, the minimal API infrastructure uses model binding to create the arguments required to execute the endpoint handler, converting the string route values to real argument types such as int.
图 7.1 RoutingMiddleware 将传入的请求匹配到终端节点,并将路由参数提取为字符串s。当 EndpointMiddleware 执行端点时,最小的 API 基础设施使用模型绑定来创建执行端点处理程序所需的参数,将字符串路由值转换为实际参数类型,例如 int。
For every parameter in your minimal API endpoint handler, ASP.NET core must decide how to create the corresponding arguments. Minimal APIs can use six different binding sources to create the handler arguments:
对于最小 API 终端节点处理程序中的每个参数,ASP.NET 核心必须决定如何创建相应的参数。Minimal API 可以使用六个不同的绑定源来创建 handler 参数:
-
Route values—These values are obtained from URL segments or through default values after matching a route, as you saw in chapter 5.
路由值 — 这些值是从 URL 分段或通过匹配路由后的默认值获取的,如第 5 章所示。 -
Query string values—These values are passed at the end of the URL, not used during routing.
查询字符串值 – 这些值在 URL 末尾传递,在路由期间不使用。 -
Header values—Header values are provided in the HTTP request.
标头值 — HTTP 请求中提供标头值。 -
Body JSON—A single parameter may be bound to the JSON body of a request.
正文 JSON — 单个参数可以绑定到请求的 JSON 正文。 -
Dependency injected services—Services available through dependency injection can be used as endpoint handler arguments. We look at dependency injection in chapters 8 and 9.
依赖项注入服务 - 通过依赖项注入提供的服务可用作终端节点处理程序参数。我们将在第 8 章和第 9 章中介绍依赖注入。 -
Custom binding—ASP.NET Core exposes methods for you to customize how a type is bound by providing access to the HttpRequest object.
自定义绑定 — ASP.NET Core 公开了一些方法,供您通过提供对 HttpRequest 对象的访问来自定义类型的绑定方式。
Warning Unlike MVC controllers and Razor Pages, minimal APIs do not automatically bind to the body of requests sent as forms, using the application/x-www-form-urlencoded mime type. Minimal APIs will bind only to a JSON request body. If you need to work with form data in a minimal API endpoint, you can access it on HttpRequest.Form, but you won’t benefit from automatic binding.
警告 与 MVC 控制器和 Razor Pages 不同,最小的 API 不会使用 application/ x-www-form- urlencoded MIME 类型自动绑定到作为表单发送的请求正文。最小 API 将仅绑定到 JSON 请求正文。如果您需要在最小的 API 端点中处理表单数据,您可以在HttpRequest.Form 的 URL 请求,但你不会从自动绑定中受益。
We’ll look at the exact algorithm ASP.NET Core uses to choose which binding source to use in section 7.8, but we’ll start by looking at how ASP.NET Core binds simple types such as int and double.
我们将了解 ASP.NET Core 用于选择在 7.8 节中使用哪个绑定源的确切算法,但首先我们将了解 ASP.NET Core 如何绑定简单类型,例如 int 和 double。
7.2 Binding simple types to a request
7.2 将简单类型绑定到请求
When you’re building minimal API handlers, you’ll often want to extract a simple value from the request. If you’re loading a list of products in a category, for example, you’ll likely need the category’s ID, and in the calculator example at the start of section 7.1, you’ll need the number to square.
在构建最小 API 处理程序时,您通常需要从请求中提取一个简单的值。例如,如果要加载某个类别中的产品列表,则可能需要类别的 ID,在第 7.1 节开头的计算器示例中,需要将数字平方。
When you create an endpoint handler that contains simple types such as int, string, and double, ASP.NET Core automatically tries to bind the value to a route parameter, or a query string value:
当您创建包含简单类型(如 int、string 和 double)的终端节点处理程序时,ASP.NET Core 会自动尝试将值绑定到路由参数或查询字符串值:
-
If the name of the handler parameter matches the name of a route parameter in the route template, ASP.NET Core binds to the associated route value.
如果 handler 参数的名称与路由模板中的路由参数名称匹配,则 ASP.NET Core 将绑定到关联的路由值。 -
If the name of the handler parameter doesn’t match any parameters in the route template, ASP.NET Core tries to bind to a query string value.
如果 handler 参数的名称与路由模板中的任何参数都不匹配,则 ASP.NET Core 会尝试绑定到查询字符串值。
If you make a request to /products/123, for example, this will match the following endpoint:
例如,如果您向 /products/123 发出请求,这将匹配以下端点:
app.MapGet("/products/{id}", (int id) => $"Received {id}");
ASP.NET Core binds the id handler argument to the {id} route parameter, so the handler function is called with id=123. Conversely, if you make a request to /products?id=456, this will match the following endpoint instead:
ASP.NET Core 将 id 处理程序参数绑定到 {id} 路由参数,因此使用 id=123 调用处理程序函数。相反,如果您向 /products?id=456 发出请求,则这将匹配以下端点:
app.MapGet("/products", (int id) => $"Received {id}");
In this case, there’s no id parameter in the route template, so ASP.NET Core binds to the query string instead, and the handler function is called with id=456.
在这种情况下,路由模板中没有 id 参数,因此 ASP.NET Core 会改为绑定到查询字符串,并使用 id=456 调用处理程序函数。
In addition to this “automatic” inference, you can force ASP.NET Core to bind from a specific source by adding attributes to the parameters. [FromRoute] explicitly binds to route parameters, [FromQuery] to the query string, and [FromHeader] to header values, as shown in figure 7.2.
除了这种 “自动” 推理之外,您还可以通过向参数添加属性来强制 ASP.NET Core 从特定源进行绑定。[FromRoute] 显式绑定到路由参数,[FromQuery] 显式绑定到查询字符串,[FromHeader] 显式绑定到标头值,如图 7.2 所示。
Figure 7.2 Model binding an HTTP get request to an endpoint. The [FromRoute], [FromQuery], and [FromHeader] attributes force the endpoint parameters to bind to specific parts of the request. Only the [FromHeader] attribute is required in this case; the route parameter and query string would be inferred automatically.
图 7.2 将 HTTP get 请求绑定到终端节点的模型。[FromRoute]、[FromQuery] 和 [FromHeader] 属性强制终结点参数绑定到请求的特定部分。在这种情况下,只需要 [FromHeader] 属性;Route 参数和 Query String 将自动推断。
The [From] attributes override ASP.NET Core’s default logic and forces the parameters to load from a specific binding source. Listing 7.1 demonstrates three possible [From] attributes:
[From] 属性会覆盖 ASP.NET Core 的默认逻辑,并强制参数从特定的binding 源。清单 7.1 演示了三种可能[From]属性:
-
[FromQuery]—As you’ve already seen, this attribute forces a parameter to bind to the query string.
[发件人查询]- 如您所见,此属性强制参数绑定到查询字符串。 -
[FromRoute]—This attribute forces the parameter to bind a route parameter value. Note that if a parameter of the required name doesn’t exist in the route template, you’ll get an exception at runtime.
[从路线]- 此属性强制参数绑定路径参数值。请注意,如果路由模板中不存在所需名称的参数,您将在运行时收到异常。 -
[FromHeader]—This attribute binds a parameter to a header value in the request.
[发件人标头]— 此属性将参数绑定到请求中的标头值。
Listing 7.1 Binding simple values using [From] attributes
列表 7.1 使用 [From] 绑定简单值属性
using Microsoft.AspNetCore.Mvc; ❶
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapGet("/products/{id}/paged",
([FromRoute] int id, ❷
[FromQuery] int page, ❸
[FromHeader(Name = "PageSize")] int pageSize) ❹
=> $"Received id {id}, page {page}, pageSize {pageSize}");
app.Run();
❶ All the [From] attributes are in this namespace.
所有 [From] 属性都位于此命名空间中。
❷ [FromRoute] forces the argument to bind to the route value.
[FromRoute] 强制参数绑定到路由值。
❸ [FromQuery] forces the argument to bind to the query string.
[FromQuery] 强制参数绑定到查询字符串。
❹ [FromHeader] binds the argument to the specified header.
[FromHeader] 将参数绑定到指定的标头。
Later, you’ll see other attributes, such as [FromBody] and [FromServices], but the preceding three attributes are the only [From*] attributes that operate on simple types such as int and double. prefer to avoid using [FromQuery] and [FromRoute] wherever possible and rely on the default binding conventions instead, as I find that they clutter the method signatures, and it’s generally obvious whether a simple type is going to bind to the query string or a route value.
稍后,您将看到其他属性,例如 [FromBody] 和 [FromServices],但前面的三个属性是唯一对简单类型(如 int 和 double)进行作的 [From*] 属性。我倾向于尽可能避免使用 [FromQuery] 和 [FromRoute],而是依赖默认的绑定约定,因为我发现它们会使方法签名变得混乱,并且通常很明显地将简单类型绑定到查询字符串还是路由值。
Tip ASP.NET Core binds to route parameters and query string values based on convention, but the only way to bind to a header value is with the [FromHeader] attribute.
提示 ASP.NET Core 根据约定绑定到路由参数和查询字符串值,但绑定到标头值的唯一方法是使用 [FromHeader] 属性。
You may be wondering what would happen if you try to bind a type to an incompatible value. What if you try to bind an int to the string value "two", for example? In that case ASP.NET Core throws a BadHttpRequestException and returns a 400 Bad Request response.
您可能想知道,如果尝试将类型绑定到不兼容的值,会发生什么情况。例如,如果您尝试将 int 绑定到字符串值 “two” 怎么办?在这种情况下 ASP.NET Core 会引发 BadHttpRequestException 并返回 400 Bad Request 响应。
Note When the minimal API infrastructure fails to bind a handler parameter due to an incompatible format, it throws a BadHttpRequestException and returns a 400 Bad Request response.
注意 当最小 API 基础设施由于格式不兼容而无法绑定处理程序参数时,它会引发 BadHttpRequestException 并返回 400 Bad Request 响应。
I’ve mentioned several times in this section that you can bind route values, query string values, and headers to simple types, but what is a simple type? A simple type is defined as any type that contains either of the following TryParse methods, where T is the implementing type:
我在本节中多次提到,您可以将路由值、查询字符串值和标头绑定到简单类型,但什么是 简单类型?简单类型定义为包含以下任一 TryParse 方法的任意类型,其中 T 是实现类型:
public static bool TryParse(string value, out T result);
public static bool TryParse(
string value, IFormatProvider provider, out T result);
Types such as int and bool contain one (or both) these methods. But it’s also worth noting that you can create your own types that implement one of these methods, and they’ll be treated as simple types, capable of binding from route values, query string values, and headers.
int 和 bool 等类型包含一个(或两个)这些方法。但还值得注意的是,您可以创建自己的类型来实现这些方法之一,它们将被视为简单类型,能够从路由值、查询字符串值和标头进行绑定。
Figure 7.3 shows an example of implementing a simple strongly-typed ID[1] that’s treated as a simple type thanks to the TryParse method it exposes. When you send a request to /product/p123, ASP.NET Core sees that the ProductId type used in the endpoint handler contains a TryParse method and that the name of the id parameter has a matching route parameter name. It creates the id argument by calling ProductId.TryParse() and passes in the route value, p123.
图 7.3 显示了实现简单的强类型 ID1 的示例,由于它公开了 TryParse 方法,该 ID 1 被视为简单类型。当您向 /product/p123 发送请求时,ASP.NET Core 会发现终端节点处理程序中使用的 ProductId 类型包含 TryParse 方法,并且 id 参数的名称具有匹配的路由参数名称。它通过调用 ProductId.TryParse() 创建 id 参数,并传入路由值 p123。
Figure 7.3 The routing middleware matches the incoming URL to the endpoint. The endpoint middleware attempts to bind the route parameter id to the endpoint parameter. The endpoint parameter type ProductId implements TryParse. If parsing is successful, the parsed parameter is used to call the endpoint handler. If parsing fails, the endpoint middleware returns a 400 Bad Request response.
图 7.3 路由中间件将传入的 URL 与端点匹配。终端节点中间件尝试将路由参数 ID 绑定到 endpoint 参数。终端节点参数类型 ProductId 实现 TryParse。如果解析成功,则 parsed 参数用于调用终端节点处理程序。如果解析失败,终端节点中间件将返回 400 Bad Request 响应。
Listing 7.2 shows how you could implement the TryParse method for ProductId. This method creates a ProductId from strings that consist of an integer prefixed with 'p' (p123 or p456, for example). If the input string matches the required format, it creates a ProductId instance and returns true. If the format is invalid, it returns false, binding fails, and a 400 Bad Request is returned.
清单 7.2 展示了如何实现 ProductId 的 TryParse 方法。此方法从字符串s 创建一个 ProductId,该字符串由一个前缀为 'p' 的整数组成(例如 p123 或 p456)。如果输入字符串与所需的格式匹配,它将创建一个 ProductId 实例并返回 true。如果格式无效,则返回 false,绑定失败,并返回 400 Bad Request。
Listing 7.2 Implementing TryParse in a custom type to allow parsing from route values
清单 7.2 在自定义类型中实现 TryParse 以允许从路由值解析
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapGet("/product/{id}", (ProductId id) => $"Received {id}"); ❶
app.Run();
readonly record struct ProductId(int Id) ❷
{
public static bool TryParse(string? s, out ProductId result) ❸
{
if(s is not null ❹
&& s.StartsWith('p') ❹
&& int.TryParse( ❺
s.AsSpan().Slice(1), ❻
out int id)) ❼
{
result = new ProductId(id); ❽
return true; ❽
}
result = default; ❾
return false; ❾
}
}
❶ ProductId automatically binds to route values as it implements TryParse.
ProductId 在实现 TryParse 时会自动绑定到路由值。
❷ ProductId is a C# 10 record struct.
ProductId 是 C# 10 记录结构。
❸ It implements TryParse, so it’s treated as a simple type by minimal APIs.
它实现了 TryParse,因此它被最小的 API 视为简单类型。
❹ Checks that the string is not null and that the first character in the string is ‘p’ .
检查字符串是否不为 null,以及字符串中的第一个字符是否为 'p' 。
❺ and if it is, tries to parse the remaining characters as an integer
如果是,则尝试将剩余字符解析为整数
❻ Efficiently skips the first character by treating the string as a ReadOnlySpan
通过将字符串视为 ReadOnlySpan 来有效地跳过第一个字符
❼ If the string was parsed successfully, id contains the parsed value.
如果字符串解析成功,则 id 包含解析的值。
❽ Everything parsed successfully, so creates a new ProductId and returns true
❾ Something went wrong, so returns false and assigns a default value to the
(unused) result
所有内容都解析成功,因此创建一个新的 ProductId 并返回 true
Using modern C# and .NET features
使用现代 C# 和 .NET 功能
Listing 7.2 included some C# and .NET features that you may not have seen before, depending on your background:
清单 7.2 包含了一些您以前可能没有见过的 C# 和 .NET 功能,具体取决于您的背景:
· Pattern matching for null values—s is not null. Pattern matching features have been introduced gradually into C# since C# 7. The is not null pattern, introduced in C# 9, has some minor advantages over the common != null expression. You can read all about pattern matching at http://mng.bz/gBxl.
空值的模式匹配 - s 不为空。自 C# 7 以来,模式匹配功能已逐渐引入 C# 中。C# 9 中引入的 is not null 模式与常见的 != null 表达式相比,具有一些细微的优势。您可以在 http://mng.bz/gBxl 阅读有关模式匹配的所有信息。
· Records and struct records—readonly record struct. Records are syntactical sugar over normal class and struct declarations, which make declaring new types more succinct and provide convenience methods for working with immutable types. Record structs were introduced in C# 10. You can read more at http://mng.bz/5wWz.
记录和结构记录 - 只读记录结构。记录是普通类和结构声明的语法糖,这使得声明新类型更加简洁,并为使用不可变类型提供了便捷的方法。记录结构是在 C# 10 中引入的。您可以在 http://mng.bz/5wWz 上阅读更多内容。
· Spanfor performance—s.AsSpan(). Span and ReadOnlySpan were introduced in .NET Core 2.1 and are particularly useful for reducing allocations when working with string values. You can read more about them at http://mng.bz/6DNy.
Span用于性能 - s.AsSpan()。 Span 和 ReadOnlySpan 是在 .NET Core 2.1 中引入的,对于在处理字符串值时减少分配特别有用。您可以在 http://mng.bz/6DNy 上阅读更多关于它们的信息。
· ValueTask—It’s not shown in listing 7.2, but many of the APIs in ASP.NET Core use ValueTask instead of the more common Task for APIs that normally complete asynchronously but may complete asynchronously. You can read about why they were introduced and when to use them at http://mng.bz/o1GM.
ValueTask— 它未显示在清单 7.2 中,但 ASP.NET Core 中的许多 API 都使用 ValueTask,而不是更常见的 Task,用于通常异步完成但可能异步完成的 API。您可以在 http://mng.bz/o1GM 上阅读有关引入它们的原因以及何时使用它们的信息。
Don’t worry if you’re not familiar with these constructs. C# is a fast-moving language, so keeping up can be tricky, but there’s generally no reason you need to use the new features. Nevertheless, it’s useful to be able to recognize them sot hat you can read and understand code that uses them.
如果您不熟悉这些结构,请不要担心。C# 是一种快速发展的语言,因此跟上步伐可能很棘手,但通常没有理由需要使用新功能。尽管如此,能够识别它们还是很有用的,这样您就可以阅读和理解使用它们的代码。
If you’re keen to embrace new features, you might consider implementing the IParsable interface when you implement TryParse. This interface uses the static abstract interfaces feature, which was introduced in C# 11, and requires implementing both a TryParse and Parse method. You can read more about the IParsable interface in the announcement post at http://mng.bz/nW2K.
如果您热衷于采用新功能,则可以考虑在实现 TryParse 时实现 IParsable 接口。此接口使用 C# 11 中引入的静态抽象接口功能,并且需要实现 TryParse 和 Parse 方法。您可以在 http://mng.bz/nW2K 的公告帖子中阅读有关 IParsable 接口的更多信息。
Now we’ve looked extensively at binding simple types to route values, query strings, and headers. In section 7.3 we’ll learn about binding to the body of a request by deserializing JSON to complex types.
现在,我们已经广泛研究了如何将简单类型绑定到路由值、查询字符串和标头。在第 7.3 节中,我们将了解如何通过将 JSON 反序列化为复杂类型来绑定到请求正文。
7.3 Binding complex types to the JSON body
7.3 将复杂类型绑定到 JSON 正文
Model binding in minimal APIs relies on certain conventions to simplify the code you need to write. One such convention, which you’ve already seen, is about binding to route parameters and query string values. Another important convention is that minimal API endpoints assume that requests will be sent using JSON.
最小 API 中的模型绑定依赖于某些约定来简化您需要编写的代码。其中一个约定您已经看到,它是关于绑定到路由参数和查询字符串值的。另一个重要的约定是,最小 API 端点假定将使用 JSON 发送请求。
Minimal APIs can bind the body of a request to a single complex type in your endpoint handler by deserializing the request from JSON. That means that if you have an endpoint such as the one in the following listing, ASP.NET Core will automatically deserialize the request for you from JSON, creating the Product argument.
最小 API 可以通过从 JSON 反序列化请求,将请求正文绑定到终端节点处理程序中的单个复杂类型。这意味着,如果您有一个终端节点(如下面的清单中的终端节点),ASP.NET Core 将自动从 JSON 反序列化请求,从而创建 Product 参数。
Listing 7.3 Automatically deserializing a JSON request from the body
清单 7.3 从正文中自动反序列化 JSON 请求
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapPost("/product", (Product product) => $"Received {product}"); ❶
app.Run();
record Product(int Id, string Name, int Stock); ❷
❶ Product is a complex type, so it’s bound to the JSON body of the request.
Product 是一种复杂类型,因此它绑定到请求的 JSON 正文。
❷ Product doesn’t implement TryParse, so it’s a complex type.
Product 没有实现 TryParse,所以它是一个复杂的类型。
If you send a POST request to /product for the app in listing 7.3, you need to provide valid JSON in the request body, such as
如果向清单 7.3 中的应用程序的 /product 发送 POST 请求,则需要在请求正文中提供有效的 JSON,例如
{ "id": 1, "Name": "Shoes", "Stock": 12 }
ASP.NET Core uses the built-in System.Text.Json library to deserialize the JSON into a Product instance and uses it as the product argument in the handler.
ASP.NET Core 使用内置的 System.Text.Json 库将 JSON 反序列化为 Product 实例,并将其用作处理程序中的 product 参数。
Configuring JSON binding with System.Text.Json
使用 System.Text.Json 配置 JSON 绑定
The System.Text.Json library, introduced in .NET Core 3.0, provides a high-performance, low-allocation JSON serialization library. It was designed to be something of a successor to the ubiquitous Newtonsoft.Json library, but it trades flexibility for performance.
.NET Core 3.0 中引入的 System.Text.Json 库提供高性能、低分配的 JSON 序列化库。它旨在成为无处不在的 Newtonsoft.Json 库的继任者,但它以灵活性换取了性能。
Minimal APIs use System.Text.Json for both JSON deserialization (when binding to a request’s body) and serialization (when writing results, as you saw in chapter 6). Unlike for MVC and Razor Pages, you can’t replace the JSON serialization library used by minimal APIs, so there’s no way to use Newtonsoft.Json instead. But you can customize some of the library’s serialization behavior for your minimal APIs.
最小 API 使用 System.Text.Json 进行 JSON 反序列化(绑定到请求正文时)和序列化(写入结果时,如第 6 章所示)。与 MVC 和 Razor Pages 不同,您无法替换最小 API 使用的 JSON 序列化库,因此无法改用 Newtonsoft.Json。但是,您可以为最小的 API 自定义库的某些序列化行为。
You can set System.Text.Json, for example, to relax some of its strictness to allow trailing commas in the JSON and control how property names are serialized with code like the following example:
例如,您可以设置 System.Text.Json 以放宽其一些严格性,以允许 JSON 中使用尾部逗号,并控制如何使用代码序列化属性名称,如以下示例所示:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureRouteHandlerJsonOptions(o => {
o.SerializerOptions.AllowTrailingCommas = true;
o.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
o.SerializerOptions.PropertyNameCaseInsensitive = true;
});
Typically, the automatic binding for JSON requests is convenient, as most APIs these days are built around JSON requests and responses. The built-in binding uses the most performant approach and eliminates a lot of boilerplate that you’d otherwise need to write yourself. Nevertheless, bear several things in mind when you’re binding to the request body:
通常,JSON 请求的自动绑定很方便,因为现在大多数 API 都是围绕 JSON 请求和响应构建的。内置绑定使用性能最高的方法,并消除了许多您需要自己编写的样板文件。尽管如此,请熊绑定到请求正文时,请记住以下几点:
-
You can bind only a single handler parameter to the JSON body. If more than one complex parameter is eligible to bind to the body, you’ll get an exception at runtime when the app receives its first request.
您只能将单个处理程序参数绑定到 JSON 正文。如果多个复杂参数符合绑定到正文的条件,则当应用程序收到其第一个请求时,您将在运行时收到异常。 -
If the request body isn’t JSON, the endpoint handler won’t run, and the EndpointMiddleware will return a 415 Unsupported Media Type response.
如果请求正文不是 JSON,则终端节点处理程序不会运行,并且 EndpointMiddleware 将返回 415 Unsupported Media Type 响应。 -
If you try to bind to the body for an HTTP verb that usually doesn’t send a body (GET, HEAD, OPTIONS, DELETE, TRACE, and CONNECT), you’ll get an exception at runtime. If you change the endpoint in listing 7.3 to MapGet instead of MapPost, for example, you’ll get an exception on your first request, as shown in figure 7.4.
如果您尝试绑定到通常不发送正文的 HTTP 动词(GET、HEAD、OPTIONS、DELETE、TRACE 和 CONNECT)的正文,则会在运行时收到异常。例如,如果将清单 7.3 中的端点更改为 MapGet 而不是 MapPost,则第一个请求将收到异常,如图 7.4 所示。 -
If you’re sure that you want to bind the body of these requests, you can override the preceding behavior by applying the [FromBody] attribute to the handler parameter. I strongly advise against this approach, though: sending a body with GET requests is unusual, could confuse the consumers of your API, and is discouraged in the HTTP specification (https://www.rfc-editor.org/rfc/rfc9110#name-get).
如果确定要绑定这些请求的正文,可以通过将 [FromBody] 属性应用于 handler 参数来替代上述行为。不过,我强烈建议不要使用这种方法:发送带有 GET 请求的正文是不寻常的,可能会使 API 的使用者感到困惑,并且在 HTTP 规范 (https://www.rfc- editor.org/rfc/rfc9110#name-get) 中不建议这样做。 -
It’s uncommon to see, but you can also apply [FromBody] to a simple type parameter to force it to bind to the request body instead of to the route/query string. As for complex types, the body is deserialized from JSON into your parameter.
这种情况并不常见,但你也可以将 [FromBody] 应用于简单类型参数,以强制它绑定到请求正文,而不是路由到路由/查询字符串。对于复杂类型,正文从 JSON 反序列化为参数。
Figure 7.4 If you try to bind the body to a parameter for a GET request, you’ll get an exception when your app receives its first request.
图 7.4 如果您尝试将 body 绑定到 GET 请求的参数,则当应用程序收到其第一个请求时,您将收到异常。
We’ve discussed binding of both simple types and complex types. Unfortunately, now it’s time to admit to a gray area: arrays, which can be simple types or complex types.
我们已经讨论了简单类型和复杂类型的绑定。不幸的是,现在是时候承认一个灰色地带了:数组,它可以是简单类型或复杂类型。
7.4 Arrays: Simple types or complex types?
7.4 数组:简单类型还是复杂类型?
It’s a little-known fact that entries in the query string of a URL don’t have to be unique. The following URL is valid, for example, even though it includes a duplicate id parameter:
一个鲜为人知的事实是,URL 的查询字符串中的条目不必是唯一的。例如,以下 URL 有效,即使它包含重复的 id 参数:
/products?id=123&id=456
So how do you access these query string values with minimal APIs? If you create an endpoint like
那么,如何使用最少的 API 访问这些查询字符串值呢?如果您创建类似于
app.MapGet("/products", (int id) => $"Received {id}");
a request to /products?id=123 would bind the id parameter to the query string, as you’d expect. But a request that includes two id values in the query string, such as /products?id=123&id=456, will cause a runtime error, as shown in figure 7.5. ASP.NET Core returns a 400 Bad Request response without the handler’s or filter pipeline’s running at all.
如您所料,对 /products?id=123 的请求会将 id 参数绑定到查询字符串。但是,在查询字符串中包含两个 id 值的请求(例如 /products?id=123&id=456)将导致运行时错误,如图 7.5 所示。ASP.NET Core 返回 400 Bad Request 响应,而处理程序或筛选器管道根本没有运行。
Figure 7.5 Attempting to bind a handler with a signature such as (int id) to a query string that contains ?id=123&id=456 causes an exception at runtime and a 400 Bad Request response.
图 7.5 尝试将具有 (int id) 等签名的处理程序绑定到包含 ?id=123&id=456 的查询字符串会导致运行时出现异常和 400 Bad Request 响应。
If you want to handle query strings like this one, so that users can optionally pass multiple possible values for a parameter, you need to use arrays. The following listing shows an example of an endpoint that accepts multiple id values from the query string and binds them to an array.
如果要处理像这样的查询字符串,以便用户可以选择为参数传递多个可能的值,则需要使用数组。以下清单显示了一个终端节点示例,该终端节点接受来自查询字符串的多个 id 值并将它们绑定到数组。
Listing 7.4 Binding multiple values for a parameter in a query string to an array
清单 7.4 为 中的参数绑定多个值数组的查询字符串
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapGet("/products/search",
(int[] id) => $"Received {id.Length} ids"); ❶
app.Run();
❶ The array will bind to multiple instances of id in the query string.
数组将绑定到查询字符串中 id 的多个实例。
If you’re anything like me, the fact that the int[] handler parameter in listing 7.4 is called id and not ids will really bug you. Unfortunately, you have to use id here so that the parameter binds correctly to a query string like ?id=123&id=456. If you renamed it ids, the query string would need to be ?ids=123&ids=456.
如果你和我一样,清单 7.4 中的 int[] handler 参数叫 id 而不是 ids 这一事实真的会让你感到困扰。遗憾的是,您必须在此处使用 id,以便参数正确绑定到查询字符串,例如 ?id=123&id=456。如果您将其重命名为 ids,则查询字符串需要为 ?ids=123&ids=456。
Luckily, you have another option. You can control the name of the target that a handler parameter binds to by using the [FromQuery] and [FromRoute] attributes, similar to the way you use [FromHeader]. For this example, you can have the best of both words by renaming the handler parameter ids and adding the [FromQuery] attribute:
幸运的是,您还有另一种选择。您可以使用 [FromQuery] 和 [FromRoute] 属性来控制处理程序参数绑定到的目标的名称,类似于使用 [FromHeader] 的方式。对于此示例,您可以通过重命名处理程序参数 ID 并添加 [FromQuery] 属性来获得两全其美的效果:
app.MapGet("/products/search",
([FromQuery(Name = "id")] int[] ids) => $"Received {ids.Length} ids");
Now you can sleep easy. The handler parameter has a better name, but it still binds to the query string ?id=123&id=456 correctly.
现在您可以安心入睡了。handler 参数具有更好的名称,但它仍正确绑定到查询字符串 ?id=123&id=456。
Tip You can bind array parameters to multiple header values in the same way that you do for as query string values, using the [FromHeader] attribute.
提示 可以使用 [FromHeader] 属性,以与查询字符串值相同的方式将数组参数绑定到多个标头值。
The example in listing 7.4 binds an int[], but you can bind an array of any simple type, including custom types with a TryParse method (listing 7.2), as well as string[] and StringValues.
清单 7.4 中的示例绑定了一个 int[],但你可以绑定任何简单类型的数组,包括带有 TryParse 方法的自定义类型(清单 7.2),以及 string[] 和 StringValues。
Note StringValues is a helper type in the Microsoft.Extensions.Primitives namespace that represents zero, one, or many strings in an efficient way.
注意 StringValues 是 Microsoft.Extensions.Primitives 命名空间中的帮助程序类型,它以高效的方式表示零个、一个或多个字符串。
So where is that gray area I mentioned? Well, arrays work as I’ve described only if
那么我提到的灰色地带在哪里呢?好吧,数组只有在
-
You’re using an HTTP verb that typically doesn’t include a request body, such as GET, HEAD, or DELETE.
您使用的是通常不包含请求正文的 HTTP 动词,例如 GET、HEAD 或 DELETE。 -
The array is an array of simple types (or string[] or StringValues).
该数组是简单类型(或string[] 或 StringValues)。
If either of these statements is not true, ASP.NET Core will attempt to bind the array to the JSON body of the request instead. For POST requests (or other verbs that typically have a request body), this process works without problems: the JSON body is deserialized to the parameter array. For GET requests (and other verbs without a body), it causes the same unhandled exception you saw in figure 7.4 when a body binding is detected in one of these verbs.
如果这些语句中的任何一个不 为 true,则 ASP.NET Core 将尝试将数组绑定到请求的 JSON 正文。对于 POST 请求(或其他通常具有请求正文的动词),此过程可以正常工作:JSON 正文被反序列化为参数数组。对于 GET 请求(以及其他没有正文的动词),当在其中一个动词中检测到正文绑定时,它会导致您在图 7.4 中看到的相同未处理异常。
Note As before, when binding body parameters, you can work around this situation for GET requests by adding an explicit [FromBody] to the handler parameter, but you shouldn’t!
注意 和以前一样,在绑定 body 参数时,您可以通过向 handler 参数添加显式 [FromBody] 来解决 GET 请求的这种情况,但您不应该这样做!
We’ve covered binding both simple types and complex types, from the URL and the body, and we’ve even looked at some cases in which a mismatch between what you expect and what you receive causes errors. But what if a value you expect isn’t there? In section 7.5 we look at how you can choose what happens.
我们已经介绍了从 URL 和正文绑定简单类型和复杂类型,我们甚至研究了一些情况,在这些情况下,你期望的和你收到的内容不匹配会导致错误。但是,如果您期望的值不存在怎么办?在 7.5 节中,我们将了解如何选择发生的情况。
7.5 Making parameters optional with nullables
7.5 使参数对可为 null 值
We’ve described lots of ways to bind parameters to minimal API endpoints. If you’ve been experimenting with the code samples and sending requests, you may have noticed that if the endpoint can’t bind a parameter at runtime, you get an error and a 400 Bad Request response. If you have an endpoint that binds a parameter to the query string, such as
我们已经介绍了许多将参数绑定到最小 API 端点的方法。如果您一直在试验代码示例并发送请求,您可能已经注意到,如果终端节点在运行时无法绑定参数,则会收到错误和 400 Bad Request 响应。如果您有一个将参数绑定到查询字符串的终端节点,例如
app.MapGet("/products", (int id) => $"Received {id}");
but you send a request without a query string or with the wrong name in the query string, such as a request to /products?p=3, the EndpointMiddleware throws an exception, as shown in figure 7.6. The id parameter is required, so if it can’t bind, you’ll get an error message and a 400 Bad Request response, and the endpoint handler won’t run.
但是您发送的请求没有查询字符串或查询字符串中的名称错误,例如对/products?p=3 中,EndpointMiddleware 会抛出一个exception 的 intent 示例,如图 7.6 所示。id 参数是必需的,因此如果它无法绑定,您将收到一条错误消息和一个400 Bad Request 响应,并且终端节点处理程序不会运行。
Figure 7.6 If a parameter can’t be bound because a value is missing, the EndpointMiddleware throws an exception and returns a 400 Bad Request response. The endpoint handler doesn’t run.
图 7.6 如果某个参数因为缺少值而无法绑定,则 EndpointMiddleware 会抛出一个exception 并返回 400 Bad Request 响应。终端节点处理程序不运行。
All parameters are required regardless of which binding source they use, whether that’s from a route value, a query string value, a header, or the request body. But what if you want a handler parameter to be optional? If you have an endpoint like this one,
无论它们使用哪个绑定源,无论是来自路由值、查询字符串值、标头还是请求正文,所有参数都是必需的。但是,如果您希望 handler 参数是可选的,该怎么办?如果您有像这样的终端节点,
app.MapGet("/stock/{id?}", (int id) => $"Received {id}");
given that the route parameter is marked optional, requests to both /stock/123 and /stock will invoke the handler. But in the latter case, there’ll be no id route value, and you’ll get an error like the one shown in figure 7.6.
鉴于 route 参数标记为可选,则对 /stock/123 和 /stock 的请求都将调用处理程序。但在后一种情况下,将没有 id 路由值,并且您将收到如图 7.6 所示的错误。
The way around this problem is to mark the handler parameter as optional by making it nullable. Just as ? signifies optional in route templates, it signifies optional in the handler parameters. You can update the handler to use int? instead of int, as shown in the following listing, and the endpoint will handle both /stock/123 and /stock without errors.
解决此问题的方法是通过将 handler 参数设为 null,将其标记为可选。就像 ? 表示 optional 在路由模板中,它在处理程序参数中表示 optional。您可以更新处理程序以使用 int? 而不是 int,如下面的清单所示,并且端点将同时处理 /stock/123 和 /stock,而不会出错。
Listing 7.5 Using optional parameters in endpoint handlers
列表 7.5 在 endpoint 中使用可选参数处理器
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapGet("/stock/{id?}", (int? id) => $"Received {id}"); ❶
app.MapGet("/stock2", (int? id) => $"Received {id}"); ❷
app.MapPost("/stock", (Product? product) => $"Received {product}"); ❸
app.Run();
❶ Uses a nullable simple type to indicate that the value is optional, so id is null when calling /stock
使用可为 null 的简单类型来指示值是可选的,因此在调用 /stock 时 id 为 null
❷ This example binds to the query string. Id will be null for the request /stock2.
此示例绑定到查询字符串。请求 /stock2 的 Id 将为 null。
❸ A nullable complex type binds to the body if it’s available; otherwise, it’s null.
如果可用,则可为 null 的复杂类型绑定到正文;否则,它是 null。
If no corresponding route value or query string contains the required value and the handler parameter is optional, the EndpointHandler uses null as the argument when invoking the endpoint handler. Similarly, for complex types that bind to the request body, if the request doesn’t contain anything in the body and the parameter is optional, the handler will have a null argument.
如果没有相应的路由值或查询字符串包含所需的值,并且 handler 参数是可选的,则 EndpointHandler 在调用终端节点处理程序时使用 null 作为参数。同样,对于绑定到请求正文的复杂类型,如果请求不包含body 中的任何内容,并且参数是可选的,则处理程序将具有 null 参数。
Warning If the request body contains the literal JSON value null and the handler parameter is marked optional, the handler argument will also be null. If the parameter isn’t marked optional, you get the same error as though the request didn’t have a body.
警告 如果请求正文包含文本 JSON 值 null,并且 handler 参数标记为可选,则 handler 参数也将为 null。如果参数未标记为 optional,则会收到与请求没有正文相同的错误。
It’s worth noting that you mark complex types binding to the request body as optional by using a nullable reference type (NRT) annotation: ?. NRTs, introduced in C# 8, are an attempt to reduce the scourge of null-reference exceptions in C#, colloquially known as “the billion-dollar mistake.” See http://mng.bz/vneM.
值得注意的是,您可以使用可为 null 的引用类型 (NRT) 注释将绑定到请求正文的复杂类型标记为可选:?。C# 8 中引入的 NRT 旨在减少 C# 中 null 引用异常的祸害,俗称“十亿美元的错误”。请参阅 http://mng.bz/vneM。
ASP.NET Core in .NET 7 is built with the assumption that NRTs are enabled for your project (and they’re enabled by default in all the templates), so it’s worth using them wherever you can. If you choose to disable NRTs explicitly, you may find that some of your types are unexpectedly marked optional, which can lead to some hard-to-debug errors.
.NET 7 中的 ASP.NET Core 的构建假设是为您的项目启用了 NRT(并且它们在所有模板中都默认启用),因此值得尽可能使用它们。如果选择显式禁用 NRT,则可能会发现某些类型意外地标记为 optional,这可能会导致一些难以调试的错误。
Tip Keep NRTs enabled for your minimal API endpoints wherever possible. If you can’t use them for your whole project, consider enabling them selectively in Program.cs (or wherever you add your endpoints) by adding #nullable enable to the top of the file.
提示 尽可能为最小 API 终结点启用 NRT。如果您无法在整个项目中使用它们,请考虑通过在文件顶部添加 #nullable enable 来有选择地在 Program.cs 中启用它们(或添加终端节点的任何位置)。
The good news is that ASP.NET Core includes several analyzers built into the compiler to catch configuration problems like the ones described in this section. If you have an optional route parameter but forget to mark the corresponding handler parameter as optional, for example, integrated development environments (IDEs) such as Visual Studio will show a hint, as shown in figure 7.7, and you’ll get a build warning. You can read more about the built-in analyzers at http://mng.bz/4DMV.
好消息是 ASP.NET Core 包含编译器中内置的多个分析器来捕获配置与本节中描述的问题类似。例如,如果你有一个可选的 route 参数,但忘记将相应的 handler 参数标记为可选,则集成开发环境 (IDE)(如 Visual Studio)将显示一个提示,如图 7.7 所示,并且你将收到生成警告。您可以在 http://mng.bz/4DMV 上阅读有关内置分析器的更多信息。
Figure 7.7 Visual Studio and other IDEs use analyzers to detect potential problems with mismatched optionality.
图 7.7 Visual Studio 和其他 IDE 使用分析器来检测可选性不匹配的潜在问题。
Making your handler parameters optional is one of the approaches you can take, whether they’re bound to route parameters, headers, or the query string. Alternatively, you can provide a default value for the parameter as part of the method signature. You can’t provide default values for parameters in lambda functions in C#, so the following listing shows how to use a local function instead.
将处理程序参数设为可选是您可以采用的方法之一,无论它们是绑定到路由参数、标头还是查询字符串。或者,您可以为参数提供默认值作为方法签名的一部分。在 C# 11,2 中,您无法为 lambda 函数中的参数提供默认值,因此以下清单显示了如何改用本地函数。
Listing 7.6 Using default values for parameters in endpoint handlers
清单 7.6 对 中的参数使用默认值端点处理程序
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapGet("/stock", StockWithDefaultValue); ❶
app.Run();
string StockWithDefaultValue(int id = 0) => $"Received {id}"; ❷
❶ The local function StockWithDefaultValue is the endpoint handler.
本地函数 StockWithDefaultValue 是端点处理程序。
❷ The id parameter binds to the query string value if it’s available; otherwise, ithas the value 0.
id 参数绑定到查询字符串值(如果可用);否则,其值为 0。
We’ve thoroughly covered the differences between simple types and complex types and how they bind. In section 7.6 we look at some special types that don’t follow these rules.
我们已经彻底介绍了简单类型和复杂类型之间的区别以及它们如何绑定。在 7.6 节 中,我们看了一些不遵循这些规则的特殊类型。
7.6 Binding services and special types
7.6 绑定服务和特殊类型
In this section you’ll learn how to use some of the special types that you can bind to in your endpoint handlers. By special, I mean types that ASP.NET Core is hardcoded to understand or that aren’t created from the details of the request, by contrast with the binding you’ve seen so far. The section looks at three types of parameters:
在本节中,您将学习如何使用可在终端节点处理程序中绑定到的一些特殊类型。我所说的特殊是指 ASP.NET Core 经过硬编码以理解的类型,或者不是根据请求的详细信息创建的类型,这与你目前看到的绑定形成对比。本节介绍三种类型的参数:
-
Well-known types—that is, hard-coded types that ASP.NET Core knows about, such as HttpContext and HttpRequest
已知类型 — 即 ASP.NET Core 知道的硬编码类型,例如 HttpContext 和 HttpRequest -
IFormCollection and IFormFile for working with form data
IFormFileCollection 和 IFormFile,用于处理文件上传 -
Application services registered in WebApplicationBuilder.Services
注册的应用程序服务WebApplicationBuilder.Services
We start by looking at the well-known types you can bind to.
我们首先查看可以绑定到的已知类型。
7.6.1 Injecting well-known types
7.6.1 注入已知类型
Throughout this book you’ve seen examples of several well-known types that you can inject into your endpoint handlers, the most notable one being HttpContext. The remaining well-known types provide shortcuts for accessing various properties of the HttpContext object.
在本书中,您已经看到了几种已知类型的示例,您可以将这些类型注入到端点处理程序中,其中最著名的是 HttpContext。其余已知类型提供了用于访问 HttpContext 对象的各种属性的快捷方式。
Note As described in chapter 3, HttpContext acts as a storage box for everything related to a single a request. It contains access to all the low-level details about the request and the response, plus any application services and features you might need.
注意 如第 3 章所述,HttpContext 充当与单个 a 请求相关的所有内容的存储盒。它包含对有关请求和响应的所有低级详细信息的访问,以及您可能需要的任何应用程序服务和功能。
You can use a well-known type in your endpoint handler by including a parameter of the appropriate type. To access the HttpContext in your handler, for example, you could use
您可以通过包含相应类型的参数,在终端节点处理程序中使用已知类型。例如,要在处理程序中访问 HttpContext,您可以使用
app.MapGet("/", (HttpContext context) => "Hello world!");
You can use the following well-known types in your minimal API endpoint handlers:
您可以在最小 API 终端节点处理程序中使用以下已知类型:
- HttpContext—This type contains all the details on both the request and the response. You can access everything you need from here, but often, an easier way to access the common properties is to use one of the other well-known types.
HttpContext — 此类型包含有关请求和响应的所有详细信息。您可以从此处访问所需的一切,但通常,访问公共属性的一种更简单的方法是使用其他已知类型之一。 - HttpRequest—Equivalent to the property HttpContext.Request, this type contains all the details about the request only.
HttpRequest - 等效于属性 HttpContext.Request,此类型仅包含有关请求的所有详细信息。 - HttpResponse—Equivalent to the property HttpContext.Response, this type contains all the details about the response only.
HttpResponse — 等效于属性 HttpContext.Response,此类型仅包含有关响应的所有详细信息。 - CancellationToken—Equivalent to the property HttpContext.RequestAborted, this token is canceled if the client aborts the request. It’s useful if you need to cancel a long-running task, as described in my post at http://mng.bz/QP2j.
CancellationToken - 等效于属性 HttpContext.RequestAborted,如果客户端中止请求,则此令牌将被取消。如果您需要取消长时间运行的任务,这很有用,如我在 http://mng.bz/QP2j 上的帖子中所述。 - ClaimsPrincipal—Equivalent to the property HttpContext.User, this type contains authentication information about the user. You’ll learn more about authentication in chapter 23.
ClaimsPrincipal — 等效于属性 HttpContext.User,此类型包含有关用户的身份验证信息。您将在第 23 章中了解有关身份验证的更多信息。 - Stream—Equivalent to the property HttpRequest.Body, this parameter is a reference to the Stream object of the request. This parameter can be useful for scenarios in which you need to process large amounts of data from a request efficiently, without holding it all in memory at the same time.
Stream — 等效于属性 HttpRequest.Body,此参数是对请求的 Stream 对象的引用。此参数可用于需要高效处理请求中的大量数据,而无需同时将其全部保存在内存中的方案。 - PipeReader—Equivalent to the property HttpContext.BodyReader, PipeReader provides a higher-level API compared with Stream, but it’s useful in similar scenarios. You can read more about PipeReader and the System.IO.Pipelines namespace at http://mng.bz/XNY6.
PipeReader — 相当于属性 HttpContext.BodyReader,与 Stream 相比,PipeReader 提供了更高级别的 API,但它在类似情况下很有用。您可以在 http://mng.bz/XNY6 中阅读有关 PipeReader 和 System.IO.Pipelines 命名空间的更多信息。
You can access each of the latter well-known types by navigating via an injected HttpContext object if you prefer. But injecting the exact object you need generally makes for code that’s easier to read.
如果你愿意,你可以通过通过插入的 HttpContext 对象导航来访问后一种已知类型 。但是,注入所需的确切对象通常会使代码更易于阅读。
7.6.2 Injecting services
7.6.2 注入服务
I’ve mentioned several times in this book that you need to configure various core services to work with ASP.NET Core. Many services are registered automatically, but often, you must add more to use extra features, such as when you called AddHttpLogging() in chapter 3 to add request logging to your pipeline.
我在本书中多次提到,您需要配置各种核心服务才能与 ASP.NET Core 配合使用。许多服务是自动注册的,但通常,您必须添加更多服务才能使用额外的功能,例如当您在第 3 章中调用 AddHttpLogging() 以将请求日志记录添加到您的管道时。
Note Adding services to your application involves registering them with a dependency injection (DI) container. You’ll learn all about DI and registering services in chapters 8 and 9.
注意: 向应用程序添加服务涉及向依赖关系注入 (DI) 容器注册服务。您将在第 8 章和第 9 章中了解有关 DI 和注册服务的所有信息。
You can automatically use any registered service in your endpoint handlers, and ASP.NET Core will inject an instance of the service from the DI container. You saw an example in chapter 6 when you used the LinkGenerator service in an endpoint handler. LinkGenerator is one of the core services registered by WebApplicationBuilder, so it’s always available, as shown in the following listing.
您可以在终端节点处理程序中自动使用任何已注册的服务,ASP.NET Core 将从 DI 容器注入服务实例。您在第 6 章中看到了一个示例,当时您在endpoint 处理程序。LinkGenerator 是 WebApplicationBuilder 注册的核心服务之一,因此它始终可用,如下面的清单所示。
Listing 7.7 Using the LinkGenerator service in an endpoint handler
清单 7.7 在端点处理程序
app.MapGet("/links", (LinkGenerator links) => ❶
{
string link = links.GetPathByName("products");
return $"View the product at {link}";
});
❶ The LinkGenerator can be used as a parameter because it’s available in the DI container.
LinkGenerator 可以用作参数,因为它在 DI 容器中可用。
Minimal APIs can automatically detect when a service is available in the DI container, but if you want to be explicit, you can also decorate your parameters with the [FromServices] attribute:
最小的 API 可以自动检测 DI 容器中何时有可用的服务,但如果要显式,还可以使用 [FromServices] 属性修饰参数:
app.MapGet("/links", ([FromServices] LinkGenerator links) =>
[FromServices] may be necessary in some rare cases if you’re using a custom DI container that doesn’t support the APIs used by minimal APIs. But generally, I find that I can keep endpoints readable by avoiding the [From*] attributes wherever possible and relying on minimal APIs to do the right thing automatically.
在极少数情况下,如果你使用的自定义 DI 容器不支持最小 API 使用的 API,则可能需要 [FromServices]。但总的来说,我发现我可以通过尽可能避免使用 [From*] 属性并依靠最少的 API 来自动执行正确的作,从而保持端点的可读性。
7.6.3 Binding file uploads with IFormFile and IFormFileCollection
7.6.3 使用 IFormFile 和 IFormFileCollection 绑定文件上传
A common feature of many websites is the ability to upload files. This activity could be relatively infrequent, such as a user’s uploading a profile picture to their Stack Overflow profile, or it may be integral to the application, such as uploading photos to Facebook.
许多网站的一个共同特点是能够上传文件。此活动可能相对不频繁,例如用户将个人资料图片上传到其 Stack Overflow 个人资料,也可能是应用程序不可或缺的一部分,例如将照片上传到 Facebook。
Letting users upload files to your application
允许用户将文件上传到您的应用程序
Uploading files to websites is a common activity, but you should consider carefully whether your application needs that ability. Whenever users can upload files, the situation is fraught with danger.
将文件上传到 Web 站点是一项常见的活动,但您应该仔细考虑您的应用程序是否需要该功能。只要用户可以上传文件,情况就充满了危险。
You should be careful to treat the incoming files as potentially malicious. Don’t trust the filename provided, take care of large files being uploaded, and don’t allow the files to be executed on your server.
应小心将传入文件视为潜在恶意文件。不要相信提供的文件名,注意上传的大文件,并且不允许在您的服务器上执行这些文件。
Files also raise questions about where the data should be stored: in a database, in the filesystem, or in some other storage? None of these questions has a straightforward answer, and you should think hard about the implications of choosing one over the other. Better, don’t let users upload files if you don’t have to!
文件还引发了关于数据应该存储在哪里的问题:在数据库中、在文件系统中,还是在其他存储中?这些问题都没有直接的答案,您应该认真考虑选择一个而不是另一个的影响。更好的是,如果不需要,请不要让用户上传文件!
ASP.NET Core supports uploading files by exposing the IFormFile interface. You can use this interface in your endpoint handlers, and it will be populated with the details of the file upload:
ASP.NET Core 支持通过公开 IFormFile 接口来上传文件。您可以在终端节点处理程序中使用此接口,它将填充文件上传的详细信息:
app.MapGet("/upload", (IFormFile file) => {});
You can also use an IFormFileCollection if you need to accept multiple files:
如果需要接受多个文件,也可以使用 IFormFileCollection:
app.MapGet("/upload", (IFormFileCollection files) =>
{
foreach (IFormFile file in files)
{
}
});
The IFormFile object exposes several properties and utility methods for reading the contents of the uploaded file, some of which are shown here:
IFormFile 对象公开了几个用于读取上载文件内容的属性和实用程序方法,其中一些方法如下所示:
public interface IFormFile
{
string ContentType { get; }
long Length { get; }
string FileName { get; }
Stream OpenReadStream();
}
As you can see, this interface exposes a FileName property, which returns the filename that the file was uploaded with. But you know not to trust users, right? You should never use the filename directly in your code; users can use it to attack your website and access files that they shouldn’t. Always generate a new name for the file before you save it anywhere.
如您所见,此接口公开了一个 FileName 属性,该属性返回上传文件时使用的文件名。但您知道不要相信用户,对吧?切勿在代码中直接使用文件名;用户可以使用它来攻击您的网站并访问他们不应该访问的文件。在将文件保存到任何位置之前,请始终为文件生成新名称。
Warning There are lots of potential threats to consider when accepting file uploads from users. For more information, see http://mng.bz/yQ9q.
警告 在接受用户上传的文件时,需要考虑许多潜在威胁。有关更多信息,请参阅 http://mng.bz/yQ9q。
The IFormFile approach is fine if users are going to be uploading only small files. When your method accepts an IFormFile instance, the whole content of the file is buffered in memory and on disk before you receive it. Then you can use the OpenReadStream method to read the data out.
如果用户只上传小文件,则 IFormFile 方法很好。当您的方法接受IFormFile 实例,则文件的全部内容在您收到之前都会缓冲在内存和磁盘上。然后,您可以使用 OpenReadStream 方法读出数据。
If users post large files to your website, you may start to run out of space in memory or on disk as ASP.NET Core buffers each of the files. In that case, you may need to stream the files directly to avoid saving all the data at the same time. Unfortunately, unlike the model-binding approach, streaming large files can be complex and error-prone, so it’s outside the scope of this book. For details, see Microsoft’s documentation at http://mng.bz/MBgn.
如果用户将大型文件发布到您的网站,您可能会开始耗尽内存或磁盘中的空间,因为 ASP.NET Core 会缓冲每个文件。在这种情况下,您可能需要直接流式传输文件,以避免同时保存所有数据。遗憾的是,与模型绑定方法不同,流式处理大文件可能很复杂且容易出错,因此不在本书的讨论范围之内。有关详细信息,请参阅 Microsoft 的文档 http://mng.bz/MBgn。
Tip Don’t use the IFormFile interface to handle large file uploads, as you may see performance problem. Be aware that you can’t rely on users not to upload large files, so avoid file uploads when you can!
提示 不要使用 IFormFile 接口来处理大文件上传,因为您可能会看到性能问题。请注意,您不能指望用户不上传大文件,因此请尽可能避免上传文件!
For the vast majority of minimal API endpoints, the default configuration of model binding for simple and complex types works perfectly well. But you may find some situations in which you need to take a bit more control.
对于绝大多数最小 API 端点,简单类型和复杂类型的模型绑定的默认配置运行良好。但是您可能会发现在某些情况下,您需要采取更多的控制措施。
7.7 Custom binding with BindAsync
7.7 使用 BindAsync 的自定义绑定
The model binding you get out of the box with minimal APIs covers most of the common situations that you’ll run into when building HTTP APIs, but there are always a few edge cases in which you can’t use it.
使用最少的 API 获得的开箱即用的模型绑定涵盖了您将遇到的大多数常见情况。在构建 HTTP API 时,但总有一些边缘情况您无法使用它。
You’ve already seen that you can inject HttpContext into your endpoint handlers, so you have direct access to the request details in your handler, but often, you still want to encapsulate the logic for extracting the data you need. You can get the best of both worlds in minimal APIs by implementing BindAsync in your endpoint handler parameter types and taking advantage of completely custom model binding. To add custom binding for a parameter type, you must implement one of the following two static BindAsync methods in your type T:
您已经看到,您可以将 HttpContext 注入到终端节点处理程序中,因此您可以直接访问处理程序中的请求详细信息,但通常,您仍然希望封装用于提取所需数据的逻辑。通过在终端节点处理程序参数类型中实现 BindAsync 并利用完全自定义的模型绑定,您可以在最少的 API 中实现两全其美的效果。若要为参数类型添加自定义绑定,必须在类型 T 中实现以下两个静态 BindAsync 方法之一:
public static ValueTask<T?> BindAsync(HttpContext context);
public static ValueTask<T?> BindAsync(
HttpContext context, ParameterInfo parameter);
Both methods accept an HttpContext, so you can extract anything you need from the request. But the latter case also provides reflection details about the parameter you’re binding. In most cases the simpler signature should be sufficient, but you never know!
这两种方法都接受 HttpContext,因此您可以从请求中提取所需的任何内容。但后一种情况还提供了有关您正在绑定的参数的反射详细信息。在大多数情况下,更简单的签名应该就足够了,但您永远不知道!
Listing 7.8 shows an example of using BindAsync to bind a record to the request body by using a custom format. The implementation shown in the listing assumes that the body contains two double values, with a line break between them, and if so, it successfully parses the SizeDetails object. If there are any problems along the way, it returns null.
清单 7.8 展示了一个使用 BindAsync 通过自定义格式将记录绑定到请求正文的示例。清单中显示的实现假设主体包含两个 double 值,它们之间有一个换行符,如果是这样,它将成功解析 SizeDetails 对象。如果在此过程中出现任何问题,它将返回 null。
Listing 7.8 Using BindAsync for custom model binding
清单 7.8 使用 BindAsync 进行自定义模型绑定
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapPost("/sizes", (SizeDetails size) => $"Received {size}"); ❶
app.Run();
public record SizeDetails(double height, double width) ❷
{ ❷
public static async ValueTask<SizeDetails?> BindAsync( ❷
HttpContext context) ❷
{
using var sr = new StreamReader(context.Request.Body); ❸
string? line1 = await sr.ReadLineAsync(context.RequestAborted); ❹
if (line1 is null) { return null; } ❺
string? line2 = await sr.ReadLineAsync(context.RequestAborted); ❹
if (line2 is null) { return null; } ❺
return double.TryParse(line1, out double height) ❻
&& double.TryParse(line2, out double width) ❻
? new SizeDetails(height, width) ❼
: null; ❽
}
}
❶ No extra attributes are needed for the SizeDetails parameter, as it has a BindAsync method.
SizeDetails 参数不需要额外的属性,因为它具有 BindAsync 方法。
❷ SizeDetails implements the static BindAsync method.
SizeDetails 实现静态 BindAsync 方法。
❸ Creates a StreamReader to read the request body
创建一个 StreamReader 来读取请求正文
❹ Reads a line of text from the body
从正文中读取一行文本
❺ If either line is null, indicating no content, stops processing
如果任一行为 null,则表示无内容,则停止处理
❻ Tries to parse the two lines as doubles
尝试将这两行解析为双精度
❼ If the parsing is successful, creates the SizeDetails model and returns it . . .
如果解析成功,则创建 SizeDetails 模型并将其返回 . . .
❽ . . . otherwise, returns null
. . . .否则,返回 null
In listing 7.8 we return null if parsing fails. The endpoint shown will cause the EndpointMiddleware to throw a BadHttpRequestException and return a 400 error, because the size parameter in the endpoint is required (not marked optional). You could have thrown an exception, but it wouldn’t have been caught by the EndpointMiddleware and would have resulted in a 500 response.
在列表 7.8 中,如果解析失败,我们返回 null。显示的端点将导致EndpointMiddleware 抛出一个BadHttpRequestException 并返回 400 错误,因为终端节点中的 size 参数是必需的(未标记为可选)。您可以在 BindAsync 中引发异常,但它不会被 EndpointMiddleware 捕获,并且会导致 500 响应。
7.8 Choosing a binding source
7.8 选择绑定源
Phew! We’ve finally covered all the ways you can bind a request to parameters in minimal APIs. In many cases, things should work as you expect. Simple types such as int and string bind to route values and query string values by default, and complex types bind to the request body. But it can get confusing when you add attributes, BindAsync, and TryParse to the mix!
唷!我们终于介绍了在最小 API 中将请求绑定到参数的所有方法。在许多情况下,事情应该按照您的预期进行。默认情况下,简单类型(如 int 和 string)绑定到路由值和查询字符串值,复杂类型绑定到请求正文。但是,当您将属性、BindAsync 和 TryParse 添加到组合中时,可能会感到困惑!
When the minimal API infrastructure tries to bind a parameter, it checks all the following binding sources in order. The first binding source that matches is the one it uses:
当最小 API 基础设施尝试绑定参数时,它会按顺序检查以下所有绑定源。匹配的第一个绑定源是它使用的绑定源:
-
If the parameter defines an explicit binding source using attributes such as [FromRoute], [FromQuery], or [FromBody], the parameter binds to that part of the request.
如果参数使用 [FromRoute]、[FromQuery] 或 [FromBody] 等属性定义显式绑定源,则参数将绑定到请求的该部分。 -
If the parameter is a well-known type such as HttpContext, HttpRequest, Stream, or IFormFile, the parameter is bound to the corresponding value.
如果参数是已知类型,如 HttpContext、HttpRequest、Stream 或 IFormFile,则参数将绑定到相应的值。 -
If the parameter type has a BindAsync() method, use that method for binding.
如果参数类型具有 BindAsync()方法,请使用该方法进行绑定。 -
If the parameter is a string or has an appropriate TryParse() method (so is a simple type):
如果参数是字符串或具有适当的TryParse() 方法(简单类型也是如此):
a. If the name of the parameter matches a route parameter name, bind to the route value.
如果参数名称与路由参数名称匹配,则绑定到路由值。
b. Otherwise, bind to the query string.
否则,请绑定到查询字符串。 -
If the parameter is an array of simple types, a string[] or StringValues, the request is a GET or similar HTTP verb that normally doesn’t have a request body, bind to the query string.
如果参数是简单类型、string[] 或 StringValues 的数组,并且请求是通常没有请求正文的 GET 或类似的 HTTP 动词,请绑定到查询字符串。 -
If the parameter is a known service type from the dependency injection container, bind by injecting the service from the container.
如果参数是依赖项注入容器中的已知服务类型,则通过从容器注入服务来绑定。
7.Finally, bind to the body by deserializing from JSON.
最后,通过从 JSON 反序列化来绑定到正文。
The minimal API infrastructure follows this sequence for every parameter in a handler and stops at the first matching binding source.
最小 API 基础结构对处理程序中的每个参数都遵循此序列,并在第一个匹配的绑定源处停止。
Warning If binding fails for the entry, and the parameter isn’t optional, the request fails with a 400 Bad Request response. The minimal API doesn’t try another binding source after one source fails.
警告 如果条目的绑定失败,并且参数不是可选的,则请求将失败,并显示 400 Bad Request响应。最小 API 不会在一个源失败后尝试另一个绑定源。
Remembering this sequence of binding sources is one of the hardest things about minimal APIs to get your head around. If you’re struggling to work out why a request isn’t working as you expect, be sure to come back and check this sequence. I once had a parameter that wasn’t binding to a route parameter, despite its having a TryParse method. When I checked the sequence, I realized that it also had a BindAsync method that was taking precedence!
记住这一系列绑定源是最小 API 最难理解的事情之一。如果你正在努力找出请求没有按预期工作的原因,请务必回来检查此序列。我曾经有一个参数,尽管它有一个 TryParse 方法,但它没有绑定到路由参数。当我检查序列时,我意识到它还具有BindAsync 方法优先!
7.9 Simplifying handlers with AsParameters
7.9 使用 AsParameters 简化处理程序
Before we move on, we’ll take a quick look at a .NET 7 feature for minimal APIs that can simplify some endpoint handlers: the [AsParameters] attribute. Consider the following GET endpoint, which binds to a route value, a header value, and some query values:
在继续之前,我们将快速了解一下 .NET 7 功能,这些功能适用于可简化某些终结点处理程序的最小 API:[AsParameters] 属性。请考虑以下 GET 终端节点,该终端节点绑定到路由值、标头值和一些查询值:
app.MapGet("/category/{id}", (int id, int page, [FromHeader(Name = "sort")] bool? sortAsc, [FromQuery(Name = "q")] string search) => { });
I think you’ll agree that the handler parameters for this method are somewhat hard to read. The parameters define the expected shape of the request, which isn’t ideal. The [AsParameters] attribute lets you wrap all these arguments into a single class or struct, simplifying the method signature and making everything more readable.
我想你会同意这个方法的处理程序参数有点难以阅读。参数定义请求的预期形状,这并不理想。[AsParameters] 属性允许您包装所有这些
参数转换为单个类或结构体,从而简化方法签名并使所有内容更具可读性。
Listing 7.9 shows an example of converting this endpoint to use [AsParameters] by replacing it with a record struct. You could also use a class, record, or struct, and you can use properties instead of constructor parameters if you prefer. See the documentation for all the permutations available at http://mng.bz/a1KB.
清单 7.9 展示了一个通过将这个端点替换为 record 结构体来转换它以使用 [AsParameters] 的示例。您还可以使用 class、record 或 struct,如果您愿意,可以使用 properties 而不是 constructor 参数。请参阅 http://mng.bz/a1KB 上提供的所有排列的文档。
Listing 7.9 Using [AsParameters] to simplify endpoint handler parameters
清单 7.9 使用 [AsParameters] 简化端点处理程序参数
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapGet("/category/{id}",
([AsParameters] SearchModel model) => $"Received {model}"); ❶
app.Run();
record struct SearchModel(
int id, ❷
int page, ❷
[FromHeader(Name = "sort")] bool? sortAsc, ❷
[FromQuery(Name = "q")] string search); ❷
❶ [AsParameters] indicates that the constructor or properties of the type should be bound, not the type itself.
[AsParameters] 指示应绑定类型的构造函数或属性,而不是类型本身。
❷ Each parameter is bound as though it were written in the endpoint handler
每个参数都被绑定,就像它是在端点处理程序中写入的一样。
The same attributes and rules apply for binding an [AsParameters] type’s constructor parameters and binding endpoint handler parameters, so you can use [From*] attributes, inject services and well-known types, and read from the body. This approach can make your endpoints more readable if you find that they’re getting a bit unwieldy.
相同的属性和规则适用于绑定 [AsParameters] 类型的构造函数参数和绑定端点处理程序参数,因此您可以使用 [From*] 属性、注入服务和已知类型,以及从正文中读取。这种方法可以使您的如果你发现它们变得有点笨拙,它们会更具可读性。
Tip In chapter 16 you’ll learn about model binding in MVC and Razor Pages. You’ll be pleased to know that in those cases, the [AsParameters] approach works out of the box without the need for an extra attribute.
提示 在第 16 章中,您将了解 MVC 和 Razor Pages 中的模型绑定。您会很高兴地知道,在这些情况下,[AsParameters] 方法开箱即用,无需额外的属性。
That brings us to the end of this section on model binding. If all went well, your endpoint handler’s arguments are created, and the handler is ready to execute its logic. It’s time to handle the request, right? Nothing to worry about.
这让我们结束了本节关于模型绑定的内容。如果一切顺利,则 endpoint 处理程序的参数将创建,并且处理程序已准备好执行其 logic。是时候处理这个请求了,对吧?没什么好担心的。
Not so fast! How do you know that the data you received was valid? How do you know that you haven’t been sent malicious data attempting a SQL injection attack or a phone number full of letters? The binder is relatively blindly assigning values sent in a request, which you’re happily going to plug into your own methods. What stops nefarious little Jimmy from sending malicious values to your application? Except for basic safeguards, nothing is stopping him, which is why it’s important that you always validate the input coming in. ASP.NET Core provides a way to do this in a declarative manner out of the box, which is the focus of section 7.10.
没那么快!您如何知道您收到的数据有效?您如何知道您没有收到尝试 SQL 注入攻击的恶意数据或充满字母的电话号码?Binders 相对盲目地分配请求中发送的值,您很乐意将其插入到自己的方法中。如何阻止邪恶的小 Jimmy 向您的应用程序发送恶意值?除了基本的保护措施外,没有什么能阻止他,这就是为什么你总是验证输入很重要的原因。ASP.NET Core 提供了一种开箱即用的声明式方式执行此作的方法,这是第 7.10 节的重点。
7.10 Handling user input with model validation
7.10 使用模型验证处理用户输入
In this section, I discuss the following topics:
在本节中,我将讨论以下主题:
-
What validation is and why you need it
什么是验证以及为什么需要验证 -
How to use DataAnnotations attributes to describe the data you expect
如何使用 DataAnnotations 属性描述所需的数据 -
How to validate your endpoint handler parameters
如何验证终端节点处理程序参数
Validation in general is a big topic, one that you’ll need to consider in every app you build. Minimal APIs don’t include validation by default, instead opting to provide nonprescriptive hooks via the filters you learned about in chapter 5. This design gives you multiple options for adding validation to your app; be sure that you do add some!
一般来说,验证是一个很大的话题,您在构建的每个应用程序中都需要考虑这个话题。默认情况下,Minimal API 不包含验证,而是选择通过您在第 5 章中学到的过滤器提供非规范性钩子。此设计为您提供了多个选项,用于向应用程序添加验证;确保你确实添加了一些!
7.10.1 The need for validation
7.10.1 验证的必要性
Data can come from many sources in your web application. You could load data from files, read it from a database, or accept values that are sent in a request. Although you may be inclined to trust that the data already on your server is valid (though this assumption is sometimes dangerous!), you definitely shouldn’t trust the data sent as part of a request.
数据可以来自 Web 应用程序中的许多来源。您可以从文件中加载数据、从数据库中读取数据或接受请求中发送的值。尽管您可能倾向于相信服务器上已有的数据是有效的(尽管这种假设有时很危险!),但您绝对不应该相信作为请求的一部分发送的数据。
Tip You can read more about the goals of validation, implementation approaches, and potential attacks at http://mng.bz/gBxE.
提示 您可以在 http://mng.bz/gBxE 上阅读有关验证目标、实施方法和潜在攻击的更多信息。
You should validate your endpoint handler parameters before you use them to do anything that touches your domain, anything that touches your infrastructure, or anything that could leak information to an attacker. Note that this warning is intentionally vague, as there’s no defined point in minimal APIs where validation should occur. I advise that you do it as soon as possible in the minimal API filter pipeline.
在使用终端节点处理程序参数执行任何涉及您的域、涉及您的基础设施或可能将信息泄露给攻击者的任何作之前,您应该先验证它们。请注意,此警告故意含糊不清,因为 minimal 中没有定义点应进行验证的 API。我建议您尽快在最小 API 过滤器管道中执行此作。
Always validate data provided by users before you use it in your methods. You have no idea what the browser may have sent you. The classic example of little Bobby Tables (https://xkcd.com/327) highlights the need to always validate data sent by a user.
在方法中使用用户提供的数据之前,请始终对其进行验证。您不知道浏览器可能向您发送了什么。小 Bobby Tables (https://xkcd.com/327) 的经典示例强调了始终验证用户发送的数据的必要性。
Validation isn’t used only to check for security threats, though. It’s also needed to check for nonmalicious errors:
不过,验证不仅仅用于检查安全威胁。还需要检查非恶意错误:
-
Data should be formatted correctly. Email fields have a valid email format, for example.
数据的格式应正确。例如,电子邮件字段具有有效的电子邮件格式。 -
Numbers may need to be in a particular range. You can’t buy -1 copies of this book!
数字可能需要在特定范围内。这本书你买不到 -1 本! -
Some values may be required, but others are optional. Name may be required for a profile, but phone number is optional.
某些值可能是必需的,但其他值是可选的。配置文件可能需要名称,但电话号码是可选的。 -
Values must conform to your business requirements. You can’t convert a currency to itself; it needs to be converted to a different currency.
值必须符合您的业务要求。不能将货币转换为自身;它需要转换为其他货币。
As mentioned earlier, the minimal API framework doesn’t include anything specific to help you with these requirements, but you can use filters to implement validation, as you’ll see in section 7.10.3. .NET 7 also includes a set of attributes that you can use to simplify your validation code significantly.
如前所述,最小 API 框架不包含任何帮助您满足这些要求的特定内容,但您可以使用过滤器来实现验证,如第 7.10.3 节所示。.NET 7 还包括一组属性,您可以使用这些属性来显著简化验证代码。
7.10.2 Using DataAnnotations attributes for validation
7.10.2 使用 DataAnnotations 属性进行验证
Validation attributes—more precisely, DataAnnotations attributes—allow you to specify the rules that your parameters should conform to. They provide metadata about a parameter type by describing the sort of data the binding model should contain, as opposed to the data itself.
验证属性(更准确地说,DataAnnotations 属性)允许您指定参数应遵循的规则。它们通过描述绑定模型应包含的数据类型(而不是数据本身)来提供有关参数类型的元数据。
You can apply DataAnnotations attributes directly to your parameter types to indicate the type of data that’s acceptable. This approach allows you to check that required fields have been provided, that numbers are in the correct range, and that email fields are valid email addresses, for example.
您可以将 DataAnnotations 属性直接应用于参数类型,以指示可接受的数据类型。例如,此方法允许您检查是否提供了必填字段、数字是否在正确的范围内,以及电子邮件字段是否为有效的电子邮件地址。
Consider the checkout page for a currency-converter application. You need to collect details about the user—their name, email, and (optionally) phone number—so you create an API to capture these details. The following listing shows the outline of that API, which takes a UserModel parameter. The UserModel type is decorated with validation attributes that represent the validation rules for the model.
请考虑货币转换器应用程序的结帐页。您需要收集有关用户的详细信息 — 他们的姓名、电子邮件和(可选)电话号码 — 因此您创建一个 API 来捕获这些详细信息。下面的清单显示了该 API 的轮廓,它采用 UserModel 参数。UserModel 类型使用表示模型的验证规则的验证属性进行修饰。
Listing 7.10 Adding DataAnnotations to a type to provide metadata
清单 7.10 将 DataAnnotations 添加到类型中以提供元数据
using System.ComponentModel.DataAnnotations; ❶
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapPost("/users", (UserModel user) => user.ToString()); ❷
app.Run();
public record UserModel
{
[Required] ❸
[StringLength(100)] ❹
[Display(Name = "Your name")] ❺
public string FirstName { get; set; }
[Required]
[StringLength(100)]
[Display(Name = "Last name")]
public string LastName { get; set; }
[Required]
[EmailAddress] ❻
public string Email { get; set; }
[Phone] ❼
[Display(Name = "Phone number")]
public string PhoneNumber { get; set; }
}
❶ Adds this using statement to use the validation attributes
添加此 using 语句以使用验证属性
❷ The API takes a UserModel parameter and binds it to the request body.
API 接受 UserModel 参数并将其绑定到请求正文。
❸ Values marked Required must be provided.
必须提供标记为 Required 的值。
❹ The StringLengthAttribute sets the maximum length for the property.
StringLengthAttribute 设置属性的最大长度。
❺ Customizes the name used to describe the property
自定义用于描述属性的名称
❻ Validates that the value of Email may be a valid email address
验证 Email 的值是否为有效的电子邮件地址
❼ Validates that the value of PhoneNumber has a valid telephone number format
验证 PhoneNumber 的值是否具有有效的电话号码格式
Suddenly, your parameter type, which was sparse on details, contains a wealth of information. You’ve specified that the FirstName property should always be provided; that it should have a maximum length of 100 characters; and that when it’s referred to (in error messages, for example), it should be called "Your name" instead of "FirstName".
突然之间,您的参数类型(细节稀疏)包含了大量信息。您已指定应始终提供 FirstName 属性;最大长度应为 100 个字符;当它被引用时(例如,在错误消息中),它应该被称为 “Your name” 而不是 “FirstName”。
The great thing about these attributes is that they clearly declare the expected state of an instance of the type. By looking at these attributes, you know what the properties will contain, or at least should contain. Then you can then write code after model binding to confirm that the bound parameter is valid, as you’ll see in section 7.10.3.
这些属性的伟大之处在于,它们清楚地声明了该类型实例的预期状态。通过查看这些属性,您知道属性将包含什么,或者至少应该包含什么。然后,您可以在模型绑定后编写代码以确认 bound 参数有效,如 7.10.3 节所示。
You’ve got a plethora of attributes to choose among when you apply DataAnnotations to your types. I’ve listed some of the common ones here, but you can find more in the System.ComponentModel.DataAnnotations namespace. For a more complete list, I recommend using IntelliSense in your IDE or checking the documentation at http://mng.bz/e1Mv.
当您将 DataAnnotations 应用于您的类型时,您有大量的属性可供选择。我在这里列出了一些常见的方法,但您可以在 System.ComponentModel.DataAnnotations 命名空间中找到更多方法。有关更完整的列表,我建议在 IDE 中使用 IntelliSense 或查看 http://mng.bz/e1Mv 中的文档。
-
[CreditCard]—Validates that a property has a valid credit card format
[信用卡]- 验证属性是否具有有效的信用卡格式 -
[EmailAddress]—Validates that a property has a valid email address format
[电子邮件地址]- 验证属性是否具有有效的电子邮件地址格式 -
[StringLength(max)]—Validates that a string has at most max number of characters
[字符串长度(最大)]- 验证字符串是否最多具有最大字符数 -
[MinLength(min)]—Validates that a collection has at least the min number of items
[最小长度(min)]- 验证集合是否至少具有最小项目数 -
[Phone]—Validates that a property has a valid phone number format
[电话]- 验证属性是否具有有效的电话号码格式 -
[Range(min, max)]—Validates that a property has a value between min and max
[范围(最小值、最大值)]- 验证属性的值是否介于 min 和 max 之间 -
[RegularExpression(regex)]—Validates that a property conforms to the regex regular expression pattern
[正则表达式(正则表达式)]- 验证属性是否符合 regex 正则表达式模式 -
[Url]—Validates that a property has a valid URL format
[网址]- 验证属性是否具有有效的 URL 格式 -
[Required]—Indicates that the property must not be null
[必填]- 指示属性不能为 null -
[Compare]—Allows you to confirm that two properties have the same value (such as Email and ConfirmEmail)
[比较]- 允许您确认两个属性具有相同的值(例如 Email 和 ConfirmEmail)
Warning The [EmailAddress] and [Phone] attributes validate only that the format of the value is potentially correct. They don’t validate that the email address or phone number exists. For an example of how to do more rigorous phone number validation, see this post on the Twilio blog: http://mng.bz/xmZe.
警告 [EmailAddress] 和 [Phone] 属性仅验证值的格式是否可能正确。它们不会验证电子邮件地址或电话号码是否存在。有关如何执行更严格的电话号码验证的示例,请参阅 Twilio 博客上的这篇文章:http://mng.bz/xmZe。
The DataAnnotations attributes aren’t new; they’ve been part of the .NET Framework since version 3.5, and their use in ASP.NET Core is almost the same as in the previous version of ASP.NET. They’re also used for purposes other than validation. Entity Framework Core (among others) uses DataAnnotations to define the types of columns and rules to use when creating database tables from C# classes. You can read more about Entity Framework Core in chapter 12 and in Entity Framework Core in Action, 2nd ed., by Jon P. Smith (Manning, 2021).
DataAnnotations 属性并不新鲜;它们自 3.5 版以来一直是 .NET Framework 的一部分,它们在 ASP.NET Core 中的使用与在以前版本的 ASP.NET 中的使用几乎相同。它们还用于验证以外的目的。Entity Framework Core(以及其他)使用 DataAnnotations 来定义从 C# 类创建数据库表时要使用的列和规则的类型。您可以在第 12 章和 Jon P. Smith 的 Entity Framework Core in Action, 2nd ed.(Manning,2021 年)中阅读有关 Entity Framework Core 的更多信息。
If the DataAnnotation attributes provided out of the box don’t cover everything you need, it’s possible to write custom attributes by deriving from the base ValidationAttribute. You’ll see how to create a custom validation attribute in chapter 32.
如果现成提供的 DataAnnotation 属性不能涵盖您需要的所有内容,则可以通过从基 ValidationAttribute 派生来编写自定义属性。您将在第 32 章中了解如何创建自定义验证属性。
One common limitation with DataAnnotation attributes is that it’s hard to validate properties that depend on the values of other properties. Maybe the UserModel type from listing 7.10 requires you to provide either an email address or a phone number but not both, which is hard to achieve with attributes. In this type of situation, you can implement IValidatableObject in your models instead of, or in addition to, using attributes. In listing 7.11, a validation rule is added to UserModel whether the email or phone number is provided. If it isn’t, Validate() returns a ValidationResult describing the problem.
DataAnnotation 属性的一个常见限制是很难验证依赖于其他属性的值的属性。也许清单 7.10 中的 UserModel 类型要求您提供电子邮件地址或电话号码,但不能同时提供两者,这很难通过属性实现。在这种情况下,您可以在模型中实现 IValidatableObject ,而不是使用属性,或者同时使用属性。在列表 7.11 中,无论提供了电子邮件还是电话号码,都会向 UserModel 添加验证规则。如果不是,则 Validate() 返回描述问题的 ValidationResult。
Listing 7.11 Implementing IValidatableObject
清单 7.11 实现 IValidatableObject
using System.ComponentModel.DataAnnotations;
public record CreateUserModel : IValidatableObject ❶
{
[EmailAddress] ❷
public string Email { get; set; }
[Phone] ❷
public string PhoneNumber { get; set; }
public IEnumerable<ValidationResult> Validate( ❸
ValidationContext validationContext) ❸
{
if(string.IsNullOrEmpty(Email) ❹
&& string.IsNullOrEmpty(PhoneNumber)) ❹
{
yield return new ValidationResult( ❺
"You must provide an Email or a PhoneNumber", ❺
New[] { nameof(Email), nameof(PhoneNumber) }); ❺
}
}
}
❶ Implements the IValidatableObject interface
实现 IValidatableObject 接口
❷ The DataAnnotation attributes continue to validate basic format requirements.
DataAnnotation 属性继续验证基本格式要求。
❸ Validate is the only function to implement in IValidatableObject.
Validate 是在 IValidatableObject 中实现的唯一函数。
❹ Checks whether the object is valid . . .
检查对象是否有效 . . .
❺ . . . and if not, returns a result describing the error
. . . .如果不是,则返回描述错误的结果
IValidatableObject helps cover some of the cases that attributes alone can’t handle, but it’s not always the best option. The Validate function doesn’t give easy access to your app’s services, and the function executes only if all the DataAnnotation attribute conditions are met.
IValidatableObject 有助于涵盖某些仅靠属性无法处理的情况,但它并不总是最佳选择。Validate 函数无法轻松访问应用程序的服务,并且仅当满足所有 DataAnnotation 属性条件时,该函数才会执行。
Tip DataAnnotations are good for input validation of properties in isolation but not so good for validating complex business rules. You’ll most likely need to perform this validation outside the DataAnnotations framework.
提示 DataAnnotations 适用于隔离属性的输入验证,但不太适合验证复杂的业务规则。您很可能需要在 DataAnnotations 框架之外执行此验证。
Alternatively, if you’re not a fan of the DataAnnotation attribute-based-plus-IValidatableObject approach, you could use the popular FluentValidation library (https://github.com/JeremySkinner/FluentValidation) in your minimal APIs instead. Minimal APIs are completely flexible, so you can use whichever approach you prefer.
或者,如果您不喜欢 DataAnnotation 基于属性的加 IValidatableObject 方法,则可以在最小 API 中使用流行的 FluentValidation 库 (https://github.com/JeremySkinner/FluentValidation)。最小 API 是完全灵活的,因此您可以使用自己喜欢的任何方法。
DataAnnotations attributes provide the basic metadata for validation, but no part of listing 7.10 or listing 7.11 uses the validation attributes you added. You still need to add code to read the parameter type’s metadata, check whether the data is valid, and return an error response if it’s invalid. ASP.NET Core doesn’t include a dedicated validation API for that task in minimal APIs, but you can easily add it with a small NuGet package.
DataAnnotations 属性提供了用于验证的基本元数据,但清单 7.10 或清单 7.11 的任何部分都没有使用您添加的验证属性。您仍然需要添加code 读取参数类型的元数据,检查数据是否有效,如果无效,则返回错误响应。ASP.NET Core 在最少的 API 中不包含用于该任务的专用验证 API,但你可以使用小型 NuGet 包轻松添加它。
7.10.3 Adding a validation filter to your minimal APIs
7.10.3 将验证筛选器添加到最小 API
Microsoft decided not to include any dedicated validation APIs in minimal APIs. By contrast, validation is a built-in core feature of Razor Pages and MVC. Microsoft’s reasoning was that the company wanted to provide flexibility and choice for users to add validation in the way that works best for them, but didn’t want to affect performance for those who didn’t want to use their implementation.
Microsoft 决定不在最小 API 中包含任何专用的验证 API。相比之下,验证是 Razor Pages 和 MVC 的内置核心功能。Microsoft 的理由是,该公司希望为用户提供灵活性和选择,以便以最适合他们的方式添加验证,但又不想影响那些不想使用其实现的人的性能。
Consequently, validation in minimal APIs typically relies on the filter pipeline. As a classic cross-cutting concern, validation is a good fit for a filter. The only downside is that typically, you need to write your own filter rather than use an existing API. The positive side is that validation gives you complete flexibility, including the ability to use an alternative validation library (such as FluentValidation) if you prefer.
因此,最小 API 中的验证通常依赖于筛选器管道。作为一个典型的横切关注点,验证非常适合 filter。唯一的缺点是,通常您需要编写自己的过滤器,而不是使用现有的 API。积极的一面是,验证为您提供了完全的灵活性,包括如果您愿意,可以使用替代验证库(例如 FluentValidation)。
Luckily, Damian Edwards, a project manager architect on the ASP.NET Core team at Microsoft, has a NuGet package called MinimalApis.Extensions that provides the filter for you. Using a simple validation system that hooks into the DataAnnotations on your models, this NuGet package provides an extension method called WithParameterValidation() that you can add to your endpoints. To add the package, search for MinimalApis.Extensions from the NuGet Package Manager in your IDE (be sure to include prerelease versions), or run the following, using the .NET command-line interface:
幸运的是,Microsoft ASP.NET Core 团队的项目经理架构师 Damian Edwards 有一个名为 MinimalApis.Extensions 的 NuGet 包,可以为您提供筛选器。使用挂接到模型上的 DataAnnotations 的简单验证系统,此 NuGet 包提供了一个名为WithParameterValidation() 中,您可以将其添加到终端节点中。若要添加包,请从 IDE 中的 NuGet 包管理器中搜索 MinimalApis.Extensions(请务必包含预发行版本),或使用 .NET 命令行界面运行以下命令:
dotnet add package MinimalApis.Extensions
After you’ve added the package, you can add validation to any of your endpoints by adding a filter using WithParameterValidation(), as shown in listing 7.12. After the UserModel is bound to the JSON body of the request, the validation filter executes as part of the filter pipeline. If the user parameter is valid, execution passes to the endpoint handler. If the parameter is invalid, a 400 Bad Request Problem Details response is returned containing a description of the errors, as shown in figure 7.8.
添加包后,您可以通过使用 WithParameterValidation() 添加过滤器来向任何端点添加验证,如清单 7.12 所示。将 UserModel 绑定到请求的 JSON 正文后,验证筛选条件将作为筛选条件管道的一部分执行。如果 user 参数有效,则执行将传递给终端节点处理程序。如果该参数无效,则返回 400 Bad Request Problem Details 响应,其中包含错误描述,如图 7.8 所示。
Figure 7.8 If the data sent in the request body is not valid, the validation filter automatically returns a 400 Bad Request response, containing the validation errors, and the endpoint handler doesn’t execute.
图 7.8 如果请求正文中发送的数据无效,则验证筛选条件会自动返回 400 Bad Request 响应,其中包含验证错误,并且终端节点处理程序不会执行。
Listing 7.12 Adding validation to minimal APIs using MinimalApis. Extensions
清单 7.12 使用 MinimalApis.Extensions 向最小 API 添加验证
using System.ComponentModel.DataAnnotations;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapPost("/users", (UserModel user) => user.ToString())
.WithParameterValidation(); ❶
app.Run();
public record UserModel ❷
{
[Required]
[StringLength(100)]
[Display(Name = "Your name")]
public string Name { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
}
❶ Adds the validation filter to the endpoint
将验证过滤器添加到端点
❷ The UserModel defines its validation requirements using DataAnnotations
attributes.
UserModel 使用 DataAnnotations 属性定义其验证要求。
Listing 7.12 shows how you can validate a complex type, but in some cases, you may want to validate simple types. You may want to validate that the id value in the following handler should be between 1 and 100:
清单 7.12 展示了如何验证复杂类型,但在某些情况下,你可能想要验证简单类型。您可能需要验证以下处理程序中的 id 值是否应介于 1 和 100 之间:
app.MapGet("/user/{id}", (int id) => $"Received {id}")
.WithParameterValidation();
Unfortunately, that’s not easy to do with DataAnnotations attributes. The validation filter will check the int type, see that it’s not a type that has any DataAnnotations on its properties, and won’t validate it.
遗憾的是,使用 DataAnnotations 属性并不容易做到这一点。验证筛选器将检查 int 类型,查看它不是在其属性上具有任何 DataAnnotations 的类型,并且不会对其进行验证。
Warning Adding attributes to the handler, as in ([Range(1, 100)] int id), doesn’t work. The attributes here are added to the parameter, not to properties of the int type, so the validator won’t find them.
警告 向处理程序添加属性(如 ([Range(1, 100)] int id) )不起作用。这里的 attributes 是添加到 parameter 的,而不是 int 类型的 properties 中,所以 validator 不会找到它们。
There are several ways around this problem, but the simplest is to use the [AsParameters] attribute you saw in section 7.9 and apply annotations to the model. The following listing shows how.
有几种方法可以解决此问题,但最简单的方法是使用您在 小节 中看到的 [AsParameters] 属性7.9 并将注释应用于模型。下面的清单显示了如何作。
Listing 7.13 Adding validation to minimal APIs using MinimalApis.Extensions
清单 7.13 使用MinimalApis.扩展
using System.ComponentModel.DataAnnotations;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapPost("/user/{id}",
([AsParameters] GetUserModel model) => $"Received {model.Id}") ❶
.WithParameterValidation(); ❷
app.Run();
struct GetUserModel
{
[Range(1, 10)] ❸
Public int Id { get; set; } ❸
}
❶ Uses [AsParameters] to create a type than can be validated
使用 [AsParameters] 创建可验证的类型
❷ Adds the validation filter to the endpoint
将验证筛选器添加到端点
❸ Adds validation attributes to your simple types
将验证属性添加到简单类型
That concludes this look at model binding in minimal APIs. You saw how the ASP.NET Core framework uses model binding to simplify the process of extracting values from a request and turning them into normal .NET objects you can work with quickly. The many ways to bind may be making your head spin, but normally, you can stick to the basics and fall back to the more complex types as and when you need them.
对最小 API 中的模型绑定的介绍到此结束。您了解了 ASP.NET Core 框架如何使用模型绑定来简化从请求中提取值并将其转换为可快速使用的普通 .NET 对象的过程。许多绑定方式可能会让您头晕目眩,但通常情况下,您可以坚持使用基本方法,并在需要时回退到更复杂的类型。
Although the discussion is short, the most important aspect of this chapter is its focus on validation—a common concern for all web applications. Whether you choose to use DataAnnotations or a different validation approach, you must make sure to validate any data you receive in all your endpoints.
虽然讨论很简短,但本章最重要的方面是它对验证的关注——一个共同的关注点适用于所有 Web 应用程序。无论您选择使用 DataAnnotations 还是其他验证方法,都必须确保验证您在所有终端节点中收到的任何数据。
In chapter 8 we leave minimal APIs behind to look at dependency injection in ASP.NET Core and see how it helps create loosely coupled applications. You’ll learn how to register the ASP.NET Core framework services with a container, add your own services, and manage service lifetimes.
在第 8 章中,我们抛弃了最少的 API,看看 ASP.NET Core 中的依赖注入,看看它如何帮助创建松散耦合的应用程序。您将学习如何向容器注册 ASP.NET Core 框架服务、添加您自己的服务以及管理服务生命周期。
7.11 Summary
7.11 总结
-
Model binding is the process of creating the arguments for endpoint handlers from the details of an HTTP request. Model binding takes care of extracting and parsing the strings in the request so that you don’t have to.
模型绑定是根据 HTTP 请求的详细信息为终端节点处理程序创建参数的过程。模型绑定负责提取和分析请求中的字符串,因此您不必这样做。 -
Simple values such as int, string, and double can bind to route values, query string values, and headers. These values are common and easy to extract from the request without any manual parsing.
简单值(如 int、string 和 double)可以绑定到路由值、查询字符串值和标头。这些值很常见,并且很容易从请求中提取,而无需任何手动解析。 -
If a simple value fails to bind because the value in the request is incompatible with the handler parameter, a BadHttpRequestException is thrown, and a 400 Bad Request response is returned.
如果由于请求中的值与 handler 参数不兼容而导致 simple 值绑定失败,则会引发 BadHttpRequestException,并返回 400 Bad Request 响应。 -
You can turn a custom type into a simple type by adding a TryParse method with the signature bool TryParse(string value, out T result). If you return false from this method, minimal APIs will return a 400 Bad Request response.
您可以通过添加具有签名 bool TryParse(string value, out T result) 的 TryParse 方法,将自定义类型转换为简单类型。如果从此方法返回 false,则最少的 API 将返回 400 Bad Request 响应。 -
Complex types bind to the request body by default by deserializing from JSON. Minimal APIs can bind only to JSON bodies; you can’t use model binding to access form values.
默认情况下,复杂类型通过从 JSON 反序列化来绑定到请求正文。最小 API 只能绑定到 JSON 正文;您不能使用模型绑定来访问表单值。 -
By default, you can’t bind the body of GET requests, which goes against the expectations for GET requests. Doing so will cause an exception at runtime.
默认情况下,您无法绑定 GET 请求的正文,因为这与 GET 请求的预期背道而驰。这样做会导致运行时出现异常。 -
Arrays of simple types bind by default to query string values for GET requests and to the request body for POST requests. This difference can cause confusion, so always consider whether an array is the best option.
默认情况下,简单类型的数组绑定到 GET 请求的查询字符串值和 POST 请求的请求正文。这种差异可能会引起混淆,因此请始终考虑数组是否是最佳选择。 -
All the parameters of a handler must bind correctly. If a parameter tries to bind to a missing value, you’ll get a BadHttpRequestException and a 400 Bad Request response.
处理程序的所有参数都必须正确绑定。如果参数尝试绑定到缺失值,您将收到 BadHttpRequestException 和 400 Bad Request 响应。 -
You can use well-known types such as HttpContext and any services from the dependency injection container in your endpoint handlers. Minimal APIs check whether each complex type in your handler is registered as a service in the DI container; if not, they treat it as a complex type to bind to the request body instead.
您可以在终端节点处理程序中使用已知类型(如 HttpContext)和依赖项注入容器中的任何服务。最小 API 检查处理程序中的每个复杂类型是否在 DI 容器中注册为服务;否则,它们会将其视为复杂类型,以绑定到请求正文。 -
You can read files sent in the request by using the IFormFile and IFormFileCollection interfaces in your endpoint handlers. Take care accepting file uploads with these interfaces, as they can open your application to attacks from users.
您可以使用终端节点处理程序中的 IFormFile 和 IFormFileCollection 接口读取请求中发送的文件。请小心接受使用这些接口上传的文件,因为它们可能会使您的应用程序受到用户的攻击。 -
You can completely customize how a type binds by using custom binding. Create a static function with the signature public static ValueTask<T?> BindAsync(HttpContext context), and return the bound property. This approach can be useful for handling complex scenarios, such as arbitrary JSON uploads.
您可以使用自定义绑定完全自定义类型的绑定方式。创建签名为 public static ValueTask<T?> BindAsync(HttpContext context) 的静态函数,并返回绑定属性。此方法可用于处理复杂场景,例如任意 JSON 上传。 -
You can override the default binding source for a parameter by applying [From] attributes to your handler parameters, such as [FromHeader], [FromQuery], [FromBody], and [FromServices]. These parameters take precedence over convention-based assumptions.
您可以通过将 [From] 属性应用于处理程序参数(如 [FromHeader]、[FromQuery]、[FromBody] 和 [FromServices])来覆盖参数的默认绑定源。这些参数优先于基于约定的假设。 -
You can encapsulate an endpoint handler’s parameters by creating a type containing all the parameters as properties or a constructor argument and decorate the parameter with the [AsParameters] attribute. This approach can help you simplify your endpoint’s method signature.
可以通过创建包含所有参数作为属性或构造函数参数的类型来封装终结点处理程序的参数,并使用 [AsParameters] 属性修饰参数。此方法可以帮助您简化终端节点的方法签名。 -
Validation is necessary to check for security threats. Check that data is formatted correctly, confirm that it conforms to expected values and verify that it meets your business rules.
验证对于检查安全威胁是必要的。检查数据的格式是否正确,确认它符合预期值,并验证它是否符合您的业务规则。 -
Minimal APIs don’t have built-in validation APIs, so you typically apply validation via a minimal API filter. This approach provides flexibility ,as you can implement validation in the way that suits you best, though it typically means that you need to use a third-party package.
最小 API 没有内置的验证 API,因此您通常通过最小 API 过滤器应用验证。此方法提供了灵活性,因为您可以以最适合您的方式实施验证,但这通常意味着您需要使用第三方包。 -
The MinimalApis.Extensions NuGet package provides a validation filter that uses DataAnnotations attributes to declaratively define the expected values. You can add the filter with the extension method WithParameterValidation().
MinimalApis.Extensions NuGet 包提供了一个验证筛选器,该筛选器使用 DataAnnotations 属性以声明方式定义预期值。您可以使用扩展方法 WithParameterValidation() 添加过滤器。 -
To add custom validation of simple types with MinimalApis.Extensions, you must create a containing type and use the [AsParameters] attribute.
若要使用 MinimalApis.Extensions 添加简单类型的自定义验证,必须创建包含类型并使用 [AsParameters] 属性。
-
I have a series discussing strongly-typed IDs and their benefits on my blog at http://mng.bz/a1Kz.
我在 http://mng.bz/a1Kz 的博客上有一个系列讨论强类型 ID 及其好处。 -
C# 12, which will be released with .NET 8, should include support for default values in lambda expressions. For more details, see http://mng.bz/AoRg.
C# 12 将与 .NET 8 一起发布,它应该包括对 lambda 表达式中默认值的支持。有关更多详细信息,请参阅 http://mng.bz/AoRg。