Author Archives: usernames

ASP.NET Core in Action 23 Authentication: Adding users to your application with Identity

Part 4 Securing and deploying your applications
第 4 部分:保护和部署应用程序

So far in the book you’ve learned how to use minimal APIs,Razor Pages, and Model-View-Controller (MVC) controllers to build both server-rendered applications and APIs. You know how to dynamically generate JavaScript Object Notation (JSON) and HTML code based on incoming requests, and how to use configuration and dependency injection to customize your app’s behavior at runtime. In part 4 you’ll learn how to add users and profiles to your app and how to publish and secure your apps.
到目前为止,在本书中,您已经学习了如何使用最少的 API、Razor Pages 和模型-视图-控制器 (MVC) 控制器来构建服务器渲染的应用程序和 API。您知道如何根据传入请求动态生成 JavaScript 对象表示法 (JSON) 和 HTML 代码,以及如何使用配置和依赖项注入来自定义应用程序在运行时的行为。在第 4 部分中,您将学习如何将用户和配置文件添加到您的应用程序,以及如何发布和保护您的应用程序。

In chapters 23 through 25 you’ll learn how to protect your applications with authentication and authorization. In chapter 23 you’ll see how you can add ASP.NET Core Identity to your apps so that users can log in and enjoy a customized experience. You’ll learn how to protect your Razor Pages apps using authorization in chapter 24 so that only some users can access certain pages in your app. In chapter 25 you’ll learn how to apply the same protections to your minimal API and web API applications.
在第 23 章到第 25 章中,您将学习如何使用身份验证和授权保护您的应用程序。在第 23 章中,您将了解如何将 ASP.NET Core Identity 添加到您的应用程序中,以便用户可以登录并享受自定义体验。您将在第 24 章中了解如何使用授权保护 Razor Pages 应用程序,以便只有部分用户可以访问应用程序中的某些页面。在第 25 章中,您将学习如何将相同的保护应用于您的最小 API 和 Web API 应用程序。

Adding logging to your application is one of those activities that’s often left until after you discover a problem in production. Adding sensible logging from the get-go will help you quickly diagnose and fix errors as they arise. Chapter 26 introduces the logging framework built into ASP.NET Core. You’ll see how you can use it to write log messages to a wide variety of locations, whether it’s the console, a file, or a third-party remote-logging service.
向应用程序添加日志记录是通常要等到您在生产中发现问题后才进行的活动之一。从一开始就添加合理的日志记录将帮助您快速诊断和修复出现的错误。第 26 章介绍了 ASP.NET Core 中内置的日志记录框架。您将了解如何使用它将日志消息写入各种位置,无论是控制台、文件还是第三方远程日志记录服务。

By this point you’ll have all the fundamentals to build a production application with ASP.NET Core. In chapter 27 I cover the steps required to make your app live, including how to publish an app to Internet Information Services (IIS) and how to configure the URLs your app listens on.
此时,您将拥有使用 ASP.NET Core 构建生产应用程序的所有基础知识。在第 27 章中,我将介绍使您的应用程序上线所需的步骤,包括如何将应用程序发布到 Internet Information Services (IIS) 以及如何配置您的应用程序侦听的 URL。

Before you expose your application to the world, an important part of web development is securing your app correctly. Even if you don’t feel you have any sensitive data in your application, you must make sure to protect your users from attacks by adhering to security best practices. You’ll learn how to configure HTTPS for your application in chapter 28 and why this is a vital step for modern web development. Similarly, in chapter 29 I describe some common security vulnerabilities, how attackers can exploit them, and what you can do to protect your applications.
在向全世界公开您的应用程序之前,Web 开发的一个重要部分是正确保护您的应用程序。即使您认为应用程序中没有任何敏感数据,也必须确保通过遵守安全最佳实践来保护您的用户免受攻击。您将在第 28 章中学习如何为您的应用程序配置 HTTPS,以及为什么这是现代 Web 开发的关键步骤。同样,在第 29 章中,我描述了一些常见的安全漏洞,攻击者如何利用它们,以及您可以采取哪些措施来保护您的应用程序。

23 Authentication: Adding users to your application with Identity
23 身份验证:使用 Identity 将用户添加到您的应用程序

This chapter covers
本章介绍

• Seeing how authentication works in web apps in ASP.NET Core
了解身份验证在 ASP.NET Core中的 Web 应用程序中的工作原理
• Creating a project using the ASP.NET Core Identity system
使用 ASP.NET Core Identity 系统创建项目
• Adding user functionality to an existing web app
向现有 Web 应用程序添加用户功能
• Customizing the default ASP.NET Core Identity UI
自定义默认 ASP.NET Core Identity UI

One of the selling points of a web framework like ASP.NET Core is the ability to provide a dynamic app, customized to individual users. Many apps have the concept of an “account” with the service, which you can “sign in” to and get a different experience.
像 ASP.NET Core 这样的 Web 框架的卖点之一是能够提供针对个人用户定制的动态应用程序。许多应用程序都具有该服务的“帐户”概念,您可以“登录”该帐户并获得不同的体验。

Depending on the service, an account gives you varying things. On some apps you may have to sign in to get access to additional features, and on others you might see suggested articles. On an e-commerce app, you’d be able to place orders and view your past orders; on Stack Overflow you can post questions and answers; on a news site you might get a customized experience based on previous articles you’ve viewed.
根据服务的不同,账户会为您提供不同的内容。在某些应用程序上,您可能必须登录才能访问其他功能,而在其他应用程序上,您可能会看到推荐的文章。在电子商务应用程序上,您将能够下订单并查看您过去的订单;在 Stack Overflow 上,您可以发布问题和答案;在新闻网站上,您可能会根据您以前查看过的文章获得自定义体验。

When you think about adding users to your application, you typically have two aspects to consider:
当您考虑向应用程序添加用户时,通常需要考虑两个方面:

• Authentication—The process of creating users and letting them log in to your app
身份验证 - 创建用户并允许其登录应用程序的过程
• Authorization—Customizing the experience and controlling what users can do, based on the current logged-in user
授权 - 根据当前登录的用户自定义体验并控制用户可以执行的作

In this chapter I’m going to be discussing the first of these points, authentication and membership. In the next chapter I’ll tackle the second point, authorization. In section 23.1 I discuss the difference between authentication and authorization, how authentication works in a traditional ASP.NET Core web app, and ways you can architect your system to provide sign-in functionality. I don’t discuss API applications in detail in this chapter, though many of the authentication principles apply to both styles of app. I discuss API applications chapter 25.
在本章中,我将讨论第一点,身份验证和成员资格。在下一章中,我将讨论第二点,授权。在第 23.1 节中,我将讨论身份验证和授权之间的区别、身份验证在传统 ASP.NET Core Web 应用程序中的工作原理,以及构建系统以提供登录功能的方法。在本章中,我不会详细讨论 API 应用程序,尽管许多身份验证原则适用于这两种类型的应用程序。我将讨论 API 应用程序第 25 章。

In section 23.2 I introduce a user-management system called ASP.NET Core Identity (Identity for short). Identity integrates with Entity Framework Core (EF Core) and provides services for creating and managing users, storing and validating passwords, and signing users in and out of your app.
在 Section 23.2 中,我介绍了一个名为 ASP.NET Core Identity(简称 Identity)的用户管理系统。Identity 与 Entity Framework Core (EF Core) 集成,并提供用于创建和管理用户、存储和验证密码以及让用户登录和注销应用程序的服务。

In section 23.3 you’ll create an app using a default template that includes ASP.NET Core Identity out of the box. This gives you an app to explore and see the features Identity provides, as well as everything it doesn’t.
在 Section 23.3 中,您将使用默认模板创建一个应用程序,该模板包含开箱即用 ASP.NET Core Identity。这为您提供了一个应用程序来探索和查看 Identity 提供的功能,以及它不提供的所有内容。

Creating an app is great for seeing how the pieces fit together, but you’ll often need to add users and authentication to an existing app. In section 23.4 you’ll see the steps required to add ASP.NET Core Identity to an existing app.
创建应用程序非常适合查看各个部分如何组合在一起,但您通常需要向现有应用程序添加用户和身份验证。在 Section 23.4 中,您将看到将 ASP.NET Core Identity 添加到现有应用程序所需的步骤。

In sections 23.5 and 23.6 you’ll learn how to replace pages from the default Identity UI by scaffolding individual pages. In section 23.5 you’ll see how to customize the Razor templates to generate different HTML on the user registration page, and in section 23.6 you’ll learn how to customize the logic associated with a Razor Page. You’ll see how to store additional information about a user (such as their name or date of birth) and how to provide them permissions that you can later use to customize the app’s behavior (if the user is a VIP, for example).
在第 23.5 节和第 23.6 节中,您将学习如何通过搭建单个页面的基架来替换默认 Identity UI 中的页面。在第 23.5 节中,你将了解如何自定义 Razor 模板以在用户注册页上生成不同的 HTML,在第 23.6 节中,你将了解如何自定义与 Razor 页面关联的逻辑。您将了解如何存储有关用户的其他信息(例如他们的姓名或出生日期),以及如何为他们提供稍后可用于自定义应用程序行为的权限(例如,如果用户是 VIP)。

Before we look at the ASP.NET Core Identity system specifically, let’s take a look at authentication and authorization in ASP.NET Core—what’s happening when you sign in to a website and how you can design your apps to provide this functionality.
在我们具体研究 ASP.NET Core Identity 系统之前,让我们先看一下 ASP.NET Core 中的身份验证和授权 - 当您登录网站时会发生什么,以及如何设计您的应用程序来提供此功能。

23.1 Introducing authentication and authorization

23.1 身份验证和授权简介

When you add sign-in functionality to your app and control access to certain functions based on the currently signed-in user, you’re using two distinct aspects of security:
当您向应用添加登录功能并根据当前登录的用户控制对某些功能的访问时,您将使用两个不同的安全性方面:

• Authentication—The process of determining who you are
身份验证 - 确定您是谁的过程
• Authorization—The process of determining what you’re allowed to do
授权 - 确定允许您执行的作的过程

Generally you need to know who the user is before you can determine what they’re allowed to do, so authentication always comes first, followed by authorization. In this chapter we’re looking only at authentication; we’ll cover authorization in chapter 24.
通常,您需要先知道用户是谁,然后才能确定允许他们做什么,因此身份验证始终排在第一位,然后是授权。在本章中,我们只关注身份验证;我们将在第 24 章中介绍授权。

In this section I start by discussing how ASP.NET Core thinks about users, and I cover some of the terminology and concepts that are central to authentication. I found this to be the hardest part to grasp when I learned about authentication, so I’ll take it slow.
在本节中,我首先讨论 ASP.NET Core 如何看待用户,并介绍一些对身份验证至关重要的术语和概念。当我了解身份验证时,我发现这是最难掌握的部分,因此我会慢慢来。

Next, we’ll look at what it means to sign in to a traditional web app. After all, you only provide your password and sign into an app on a single page; how does the app know the request came from you for subsequent requests?
接下来,我们将了解登录到传统 Web 应用程序意味着什么。毕竟,您只需在单个页面上提供密码并登录应用程序;应用程序如何知道您的后续请求来自您?

23.1.1 Understanding users and claims in ASP.NET Core

23.1.1 了解 ASP.NET Core 中的用户和声明

The concept of a user is baked into ASP.NET Core. In chapter 3 you learned that the HTTP server, Kestrel, creates an HttpContext object for every request it receives. This object is responsible for storing all the details related to that request, such as the request URL, any headers sent, and the body of the request.
用户的概念已融入 ASP.NET Core。在第 3 章中,您了解了 HTTP 服务器 Kestrel 为它收到的每个请求创建一个 HttpContext 对象。此对象负责存储与该请求相关的所有详细信息,例如请求 URL、发送的任何标头以及请求正文。

The HttpContext object also exposes the current principal for a request as the User property. This is ASP.NET Core’s view of which user made the request. Any time your app needs to know who the current user is or what they’re allowed to do, it can look at the HttpContext.User principal.
HttpContext 对象还将请求的当前主体公开为 User 属性。这是 ASP.NET Core 对哪个用户发出请求的视图。每当你的应用程序需要知道当前用户是谁或允许他们做什么时,它都可以查看 HttpContext.User 主体。

DEFINITION You can think of the principal as the user of your app.
定义:您可以将主体视为应用程序的用户。

In ASP.NET Core, principals are implemented using the ClaimsPrincipal class, which has a collection of claims associated with it, as shown in figure 23.1.
在 ASP.NET Core 中,主体是使用 ClaimsPrincipal 类实现的,该类具有与之关联的声明集合,如图 23.1 所示。

alt text

Figure 23.1 The principal is the current user, implemented as ClaimsPrincipal. It contains a collection of Claims that describe the user.
图 23.1 主体是当前用户,实现为 ClaimsPrincipal。它包含描述用户的 Claims 集合。

You can think about claims as properties of the current user. For example, you could have claims for things like email, name, and date of birth.
您可以将声明视为当前用户的属性。例如,您可以对电子邮件、姓名和出生日期等内容提出索赔。

DEFINITION A claim is a single piece of information about a principal; it consists of a claim type and an optional value.
定义:索赔是有关委托人的单个信息;它由 Claim 类型和 Optional Value 组成。

Claims can also be indirectly related to permissions and authorization, so you could have a claim called HasAdminAccess or IsVipCustomer. These would be stored in the same way—as claims associated with the user principal.
声明也可以与权限和授权间接相关,因此您可以有一个名为 HasAdminAccess 或 IsVipCustomer 的声明。这些请求的存储方式与与用户主体关联的声明相同。

NOTE Earlier versions of ASP.NET used a role-based approach to security rather than a claims-based approach. The ClaimsPrincipal used in ASP.NET Core is compatible with this approach for legacy reasons, but you should use the claims-based approach for new apps.
注意:早期版本的 ASP.NET 使用基于角色的安全方法,而不是基于声明的方法。由于遗留原因,ASP.NET Core 中使用的 ClaimsPrincipal 与此方法兼容,但对于新应用,应使用基于声明的方法。

Kestrel assigns a user principal to every request that arrives at your app. Initially, that principal is a generic, anonymous, unauthenticated principal with no claims. How do you log in, and how does ASP.NET Core know that you’ve logged in on subsequent requests?
Kestrel 为到达应用程序的每个请求分配一个用户主体。最初,该委托人是通用的、匿名的、未经身份验证的委托人,没有声明。您如何登录,ASP.NET Core 如何知道您已登录后续请求?

In the next section we’ll look at how authentication works in a traditional web app using ASP.NET Core and the process of signing into a user account.
在下一节中,我们将了解使用 ASP.NET Core 在传统 Web 应用程序中进行身份验证的工作原理,以及登录用户帐户的过程。

23.1.2 Authentication in ASP.NET Core: Services and middleware

23.1.2 ASP.NET Core 中的身份验证:服务和中间件

Adding authentication to any web app involves a few moving parts. The same general process applies whether you’re building a traditional web app or a client-side app (though there are often differences in the latter, as I discuss in chapter 25):
向任何 Web 应用程序添加身份验证都涉及一些移动部件。无论您是构建传统的 Web 应用程序还是客户端应用程序,相同的一般过程都适用(尽管后者经常存在差异,正如我在第 25 章中讨论的那样):

  1. The client sends an identifier and a secret to the app to identify the current user. For example, you could send an email address (identifier) and a password (secret).
    客户端向应用程序发送标识符和密钥以识别当前用户。例如,您可以发送电子邮件地址 (identifier) 和密码 (secret)。

  2. The app verifies that the identifier corresponds to a user known by the app and that the corresponding secret is correct.
    应用程序验证标识符是否对应于应用程序已知的用户,以及相应的密钥是否正确。

  3. If the identifier and secret are valid, the app can set the principal for the current request, but it also needs a way of storing these details for subsequent requests. For traditional web apps, this is typically achieved by storing an encrypted version of the user principal in a cookie.
    如果标识符和密钥有效,则应用程序可以为当前请求设置主体,但它还需要一种方法来存储这些详细信息以供后续请求使用。对于传统的 Web 应用程序,这通常是通过将用户主体的加密版本存储在 Cookie 中来实现的。

This is the typical flow for most web apps, but in this section I’m going to look at how it works in ASP.NET Core. The overall process is the same, but it’s good to see how this pattern fits into the services, middleware, and Model-View-Controller (MVC) aspects of an ASP.NET Core application. We’ll step through the various pieces at play in a typical app when you sign in as a user, what that means, and how you can make subsequent requests as that user.
这是大多数 Web 应用程序的典型流程,但在本节中,我将介绍它在 ASP.NET Core 中的工作原理。整个过程是相同的,但很高兴看到此模式如何适应 ASP.NET Core 应用程序的服务、中间件和模型-视图-控制器 (MVC) 方面。我们将逐步介绍当您以用户身份登录时,典型应用程序中的各个部分、这意味着什么,以及您如何以该用户身份发出后续请求。

Signing in to an ASP.NET Core application
登录到 ASP.NET Core 应用程序

When you first arrive on a site and sign in to a traditional web app, the app will send you to a sign-in page and ask you to enter your username and password. After you submit the form to the server, the app redirects you to a new page, and you’re magically logged in! Figure 23.2 shows what’s happening behind the scenes in an ASP.NET Core app when you submit the form.
当您首次访问站点并登录到传统的 Web 应用程序时,该应用程序会将您转到登录页面,并要求您输入用户名和密码。将表单提交到服务器后,应用程序会将您重定向到新页面,然后您神奇地登录了!图 23.2 显示了当您提交表单时 ASP.NET Core 应用程序中的幕后情况。

alt text
Figure 23.2 Signing in to an ASP.NET Core application. SignInManager is responsible for setting HttpContext.User to the new principal and serializing the principal to the encrypted cookie.
图 23.2 登录到 ASP.NET Core 应用程序。SignInManager 负责将 HttpContext.User 设置为新主体,并将主体序列化为加密的 Cookie。

This figure shows the series of steps from the moment you submit the login form on a Razor Page to the point the redirect is returned to the browser. When the request first arrives, Kestrel creates an anonymous user principal and assigns it to the HttpContext.User property. The request is then routed to the Login.cshtml Razor Page, which reads the email and password from the request using model binding.
此图显示了从您在 Razor 页面上提交登录表单到将重定向返回到浏览器的一系列步骤。当请求首次到达时,Kestrel 会创建一个匿名用户主体,并将其分配给 HttpContext.User 属性。然后,该请求将路由到 Login.cshtml Razor 页面,该页面使用模型绑定从请求中读取电子邮件和密码。

The meaty work happens inside the SignInManager service. This is responsible for loading a user entity with the provided username from the database and validating that the password they provided is correct.
繁重的工作发生在 SignInManager 服务内部。这负责使用从数据库中提供的用户名加载用户实体,并验证他们提供的密码是否正确。

Warning Never store passwords in the database directly. They should be hashed using a strong one-way algorithm. The ASP.NET Core Identity system does this for you, but it’s always wise to reiterate this point!
警告:切勿将密码直接存储在数据库中。它们应该使用强大的单向算法进行哈希处理。ASP.NET Core Identity 系统为您执行此作,但重申这一点始终是明智的!

If the password is correct, SignInManager creates a new ClaimsPrincipal from the user entity it loaded from the database and adds the appropriate claims, such as the email address. It then replaces the old, anonymous HttpContext.User principal with the new, authenticated principal.
如果密码正确,SignInManager 将从它从数据库中加载的用户实体创建新的 ClaimsPrincipal,并添加相应的声明,例如电子邮件地址。然后,它将旧的匿名 HttpContext.User 主体替换为经过身份验证的新主体。

Finally, SignInManager serializes the principal, encrypts it, and stores it as a cookie. A cookie is a small piece of text that’s sent back and forth between the browser and your app along with each request, consisting of a name and a value.
最后,SignInManager 序列化主体,对其进行加密,并将其存储为 Cookie。Cookie 是一小段文本,它与每个请求一起在浏览器和应用程序之间来回发送,由名称和值组成。

This authentication process explains how you can set the user for a request when they first log in to your app, but what about subsequent requests? You send your password only when you first log in to an app, so how does the app know that it’s the same user making the request?
此身份验证过程说明了如何在用户首次登录您的应用程序时为用户设置请求,但后续请求呢?您仅在首次登录应用程序时发送密码,那么该应用程序如何知道它是发出请求的同一用户?

Authenticating users for subsequent requests
为后续请求对用户进行身份验证

The key to persisting your identity across multiple requests lies in the final step of figure 23.2, where you serialized the principal in a cookie. Browsers automatically send this cookie with all requests made to your app, so you don’t need to provide your password with every request.
在多个请求中保留身份的关键在于图 23.2 的最后一步,在该步骤中,您在 cookie 中序列化了主体。浏览器会自动将此 Cookie 与向您的应用发出的所有请求一起发送,因此您无需为每个请求提供密码。

ASP.NET Core uses the authentication cookie sent with the requests to rehydrate a ClaimsPrincipal and set the HttpContext.User principal for the request, as shown in figure 23.3. The important thing to note is when this process happens—in the AuthenticationMiddleware.
ASP.NET Core 使用随请求发送的身份验证 Cookie 来解除冻结 ClaimsPrincipal 并为请求设置 HttpContext.User 主体,如图 23.3 所示。需要注意的重要一点是此过程何时发生 — 在 AuthenticationMiddleware 中。

alt text

Figure 23.3 A subsequent request after signing in to an application. The cookie sent with the request contains the user principal, which is validated and used to authenticate the request.
图 23.3 登录应用程序后的后续请求。随请求发送的 Cookie 包含用户主体,该主体经过验证并用于对请求进行身份验证。

When a request containing the authentication cookie is received, Kestrel creates the default, unauthenticated, anonymous principal and assigns it to the HttpContext.User principal. Any middleware that runs before the AuthenticationMiddleware sees the request as unauthenticated, even if there’s a valid cookie.
收到包含身份验证 Cookie 的请求时,Kestrel 会创建默认的、未经身份验证的匿名主体,并将其分配给 HttpContext.User 主体。在 AuthenticationMiddleware 之前运行的任何中间件都会将请求视为未经身份验证,即使存在有效的 cookie。

Tip If it looks like your authentication system isn’t working, double-check your middleware pipeline. Only middleware that runs after AuthenticationMiddleware will see the request as authenticated.
提示:如果您的身份验证系统看起来无法正常工作,请仔细检查您的中间件管道。只有在 AuthenticationMiddleware 之后运行的中间件才会看到请求经过身份验证。

The AuthenticationMiddleware is responsible for setting the current user for a request. The middleware calls the authentication services, which reads the cookie from the request, decrypts it, and deserializes it to obtain the ClaimsPrincipal created when the user logged in.
AuthenticationMiddleware 负责为请求设置当前用户。中间件调用身份验证服务,该服务从请求中读取 Cookie,对其进行解密,然后对其进行反序列化,以获取在用户登录时创建的 ClaimsPrincipal。

The AuthenticationMiddleware sets the HttpContext.User principal to the new, authenticated principal. All subsequent middleware now knows the user principal for the request and can adjust its behavior accordingly (for example, displaying the user’s name on the home page or restricting access to some areas of the app).
AuthenticationMiddleware 将 HttpContext.User 主体设置为经过身份验证的新主体。现在,所有后续中间件都知道请求的用户主体,并可以相应地调整其行为(例如,在主页上显示用户名或限制对应用程序某些区域的访问)。

NOTE The AuthenticationMiddleware is responsible only for authenticating incoming requests and setting the ClaimsPrincipal if the request contains an authentication cookie. It is not responsible for redirecting unauthenticated requests to the login page or rejecting unauthorized requests; that is handled by the AuthorizationMiddleware, as you’ll see in chapter 24.
注意:AuthenticationMiddleware 只负责对传入请求进行身份验证,并在请求包含身份验证 Cookie 时设置 ClaimsPrincipal。它不负责将未经身份验证的请求重定向到登录页面或拒绝未经授权的请求;它由 AuthorizationMiddleware 处理,您将在第 24 章中看到。

The process described so far, in which a single app authenticates the user when they log in and sets a cookie that’s read on subsequent requests, is common with traditional web apps, but it isn’t the only possibility. In chapter 25 we’ll take a look at authentication for web API applications, used by client-side and mobile apps and at how the authentication system changes for those scenarios.
到目前为止描述的过程,即单个应用程序在用户登录时对用户进行身份验证,并设置在后续请求中读取的 cookie,这在传统 Web 应用程序中很常见,但并不是唯一的可能性。在第 25 章中,我们将介绍客户端和移动应用程序使用的 Web API 应用程序的身份验证,以及这些场景的身份验证系统如何变化。

Another thing to consider is where you store the authentication details for users of your app. In figure 23.2 I showed the authentication services loading the user authentication details from your app’s database, but that’s only one option.
要考虑的另一件事是存储应用程序用户的身份验证详细信息的位置。在图 23.2 中,我展示了从应用程序数据库中加载用户身份验证详细信息的身份验证服务,但这只是一个选项。

Another option is to delegate the authentication responsibilities to a third-party identity provider, such as Okta, Auth0, Azure Active Directory B2B/B2C, or even Facebook. These manage users for you, so user information and passwords are stored in their database rather than your own. The biggest advantage of this approach is that you don’t have to worry about making sure your customer data is safe; you can be pretty sure that a third party will protect it, as it’s their whole business.
另一种选择是将身份验证责任委托给第三方身份提供商,例如 Okta、Auth0、Azure Active Directory B2B/B2C 甚至 Facebook。这些 Bug 会为您管理用户,因此用户信息和密码存储在他们的数据库中,而不是您自己的数据库中。这种方法的最大优点是您不必担心确保客户数据的安全;您可以非常确定第三方会保护它,因为这是他们的全部业务。

Tip Wherever possible, I recommend this approach, as it delegates security responsibilities to someone else. You can’t lose your users’ details if you never had them! Make sure to understand the differences in providers, however. With a provider like Auth0, you would own the profiles created, whereas with a provider like Facebook, you don’t!
提示:我尽可能推荐这种方法,因为它将安全责任委托给其他人。如果您从未拥有用户的详细信息,您就不会丢失它们!但是,请务必了解提供程序之间的差异。使用像 Auth0 这样的提供商,您将拥有创建的配置文件,而使用像 Facebook 这样的提供商,您将不拥有!

Each provider provides instructions on how to integrate with their identity services, ideally using the OpenID Connect (OIDC) specification. This typically involves configuring some authentication services in your application, adding some configuration, and delegating the authentication process itself to the external provider. These providers can be used with your API apps too, as I discuss in chapter 25.
每个提供商都提供了有关如何与其身份服务集成的说明,最好使用 OpenID Connect (OIDC) 规范。这通常涉及在应用程序中配置一些身份验证服务、添加一些配置以及将身份验证过程本身委托给外部提供商。这些提供程序也可以用于您的 API 应用程序,正如我在第 25 章中讨论的那样。

NOTE Hooking up your apps and APIs to use an identity provider can require a fair amount of tedious configuration, both in the app and the identity provider, but if you follow the provider’s documentation you should have plain sailing. For example, you can follow the documentation for adding authentication to a traditional web app using Microsoft’s Identity Platform here: http://mng.bz/4D9w.
注意:将应用程序和 API 挂接以使用身份提供商可能需要在应用程序和身份提供商中进行大量繁琐的配置,但如果您遵循提供商的文档,您应该会一帆风顺。例如,您可以按照以下文档使用 Microsoft 的 Identity Platform 将身份验证添加到传统 Web 应用程序:http://mng.bz/4D9w

While I recommend using an external identity provider where possible, sometimes you really want to store all the authentication details of your users directly in your app. That’s the approach I describe in this chapter.
虽然我建议尽可能使用外部身份提供商,但有时您确实希望将用户的所有身份验证详细信息直接存储在您的应用程序中。这就是我在本章中描述的方法。

ASP.NET Core Identity (hereafter shortened to Identity) is a system that makes building the user-management aspect of your app. It handles all the boilerplate for saving and loading users to a database, as well as best practices for security, such as user lockout, password hashing, and multifactor authentication.
ASP.NET Core Identity(以下简称 Identity)是一个用于构建应用程序的用户管理方面的系统。它处理将用户保存和加载到数据库的所有样板,以及安全性最佳实践,例如用户锁定、密码哈希和多重身份验证。

DEFINITION Multifactor authentication (MFA), and the subset two-factor authentication (2FA) require both a password and an extra piece of information to sign in. This could involve sending a code to a user’s phone by Short Message Service (SMS) or using a mobile app to generate a code, for example.
定义:多重身份验证 (MFA) 和子集双重身份验证 (2FA) 需要密码和额外的信息才能登录。例如,这可能涉及通过短信服务 (SMS) 向用户的手机发送验证码,或使用移动应用生成验证码。

In the next section I’m going to talk about the ASP.NET Core Identity system, the problems it solves, when you’d want to use it, and when you might not want to use it. In section 23.3 we take a look at some code and see ASP.NET Core Identity in action.
在下一节中,我将讨论 ASP.NET Core Identity 系统、它解决的问题、何时要使用它以及何时可能不想使用它。在 Section 23.3 中,我们看了一些代码,并看到了 Core Identity ASP.NET 实际应用。

23.2 What is ASP.NET Core Identity?

23.2 什么是 ASP.NET Core Identity?

Whenever you need to add nontrivial behaviors to your application, you typically need to add users and authentication. That means you’ll need a way of persisting details about your users, such as their usernames and passwords.
每当需要向应用程序添加重要行为时,通常需要添加用户和身份验证。这意味着您需要一种方法来保留有关用户的详细信息,例如他们的用户名和密码。

This might seem like a relatively simple requirement, but given that this is related to security and people’s personal details, it’s important you get it right. As well as storing the claims for each user, it’s important to store passwords using a strong hashing algorithm to allow users to use MFA where possible and to protect against brute-force attacks, to name a few of the many requirements. Although it’s perfectly possible to write all the code to do this manually and to build your own authentication and membership system, I highly recommend you don’t.
这似乎是一个相对简单的要求,但考虑到这与安全和人们的个人详细信息有关,因此请务必正确处理。除了存储每个用户的声明外,使用强大的哈希算法存储密码也很重要,这样用户就可以尽可能使用 MFA 并防止暴力攻击,仅举几例。尽管完全可以编写所有代码来手动执行此作并构建您自己的身份验证和成员资格系统,但我强烈建议您不要这样做。

I’ve already mentioned third-party identity providers such as Auth0 and Azure Active Directory. These Software as a Service (SaaS) solutions take care of the user-management and authentication aspects of your app for you. If you’re in the process of moving apps to the cloud generally, solutions like these can make a lot of sense.
我已经提到了第三方身份提供商,例如 Auth0 和 Azure Active Directory。这些软件即服务 (SaaS) 解决方案为您处理应用程序的用户管理和身份验证方面。如果您通常正在将应用程序迁移到云,那么像这样的解决方案可能非常有意义。

If you can’t or don’t want to use these third-party solutions, I recommend you consider using the ASP.NET Core Identity system to store and manage user details in your database. ASP.NET Core Identity takes care of most of the boilerplate associated with authentication, but it remains flexible and lets you control the login process for users if you need to.
如果您不能或不想使用这些第三方解决方案,我建议您考虑使用 ASP.NET Core Identity 系统在您的数据库中存储和管理用户详细信息。ASP.NET Core Identity 负责与身份验证相关的大部分样板,但它仍然保持灵活性,并允许您根据需要控制用户的登录过程。

NOTE ASP.NET Core Identity is an evolution of the legacy .NET Framework ASP.NET Identity system, with some design improvements and update to work with ASP.NET Core.
注意: ASP.NET Core Identity 是旧版 .NET Framework ASP.NET Identity 系统的演变,经过一些设计改进和更新以与 ASP.NET Core 配合使用。

By default, ASP.NET Core Identity uses EF Core to store user details in the database. If you’re already using EF Core in your project, this is a perfect fit. Alternatively, it’s possible to write your own stores for loading and saving user details in another way.
默认情况下,ASP.NET Core Identity 使用 EF Core 将用户详细信息存储在数据库中。如果你已在项目中使用 EF Core,则这是一个完美的选择。或者,可以编写自己的 store 以另一种方式加载和保存用户详细信息。

Identity takes care of the low-level parts of user management, as shown in table 23.1. As you can see from this list, Identity gives you a lot, but not everything—by a long shot!
Identity 负责用户 Management 的低级部分,如 Table 23.1 所示。从这个列表中可以看出,Identity 能给你很多,但不是全部——很长一段时间!

Table 23.1 Which services are and aren’t handled by ASP.NET Core Identity
表 23.1 哪些服务由 ASP.NET Core Identity 处理,哪些服务不由 Core Identity 处理

Managed by ASP.NET Core Identity Requires implementing by the developer
Database schema for storing users and claims UI for logging in, creating, and managing users (Razor Pages or controllers); included in an optional package that provides a default UI
Creating a user in the database Sending email messages
Password validation and rules Customizing claims for users (adding new claims)
Handling user account lockout (to prevent brute-force attacks) Configuring third-party identity providers
Managing and generating MFA/2FA codes Integration into MFA such as sending SMS messages, time-based one-time password (TOTP) authenticator apps, or hardware keys
Generating password-reset tokens -
Saving additional claims to the database -
Managing third-party identity providers (for example, Facebook, Google, and Twitter) -

The biggest missing piece is the fact that you need to provide all the UI for the application, as well as tying all the individual Identity services together to create a functioning sign-in process. That’s a big missing piece, but it makes the Identity system extremely flexible.
最大的缺失部分是您需要为应用程序提供所有 UI,以及将所有单独的 Identity 服务捆绑在一起以创建有效的登录过程。这是一个很大的缺失部分,但它使 Identity 系统非常灵活。

Luckily, ASP.NET Core includes a helper NuGet library, Microsoft.AspNetCore.Identity.UI, that gives you the whole of the UI boilerplate for free. That’s over 30 Razor Pages with functionality for logging in, registering users, using 2FA, and using external login providers, among other features. You can still customize these pages if you need to, but having a whole login process working out of the box, with no code required on your part, is a huge win. We’ll look at this library and how you use it in sections 23.3 and 23.4.
幸运的是,ASP.NET Core 包含一个帮助程序 NuGet 库 Microsoft.AspNetCore.Identity.UI,它免费为您提供整个 UI 样板。这是 30 多个 Razor 页面,具有登录、注册用户、使用 2FA 和使用外部登录提供程序等功能。如果需要,您仍然可以自定义这些页面,但是拥有一个开箱即用的整个登录过程,而无需您编写任何代码,这是一个巨大的胜利。我们将在 23.3 和 23.4 节中介绍这个库以及你如何使用它。

For that reason, I strongly recommend using the default UI as a starting point, whether you’re creating an app or adding user management to an existing app. But the question remains as to when you should use Identity and when you should consider rolling your own.
因此,我强烈建议使用默认 UI 作为起点,无论您是创建应用程序还是向现有应用程序添加用户管理。但问题仍然存在,何时应该使用 Identity 以及何时应该考虑推出自己的 Identity。

I’m a big fan of Identity when you need to store your own users, so I tend to suggest it in most situations, as it handles a lot of security-related things for you that are easy to mess up. I’ve heard several arguments against it, some valid and others less so:
当您需要存储自己的用户时,我是 Identity 的忠实粉丝,因此我倾向于在大多数情况下建议使用它,因为它可以为您处理很多与安全相关的事情,这些事情很容易搞砸。我听到了几个反对它的论点,有些是有效的,有些则不太有效:

• I already have user authentication in my app. Great! In that case, you’re probably right, Identity may not be necessary. But does your custom implementation use MFA? Do you have account lockout? If not, and if you need to add them, considering Identity may be worthwhile.
我的应用程序中已经有用户身份验证。太好了!在这种情况下,您可能是对的,Identity 可能不是必需的。但是您的自定义实施是否使用 MFA?您是否有帐户锁定?如果没有,并且您需要添加它们,考虑 Identity 可能是值得的。

• I don’t want to use EF Core. That’s a reasonable stance. You could be using Dapper, some other object-relational mapper (ORM), or even a document database for your database access. Luckily, the database integration in Identity is pluggable, so you could swap out the EF Core integration and use your own database integration libraries instead.
我不想使用 EF Core。这是一个合理的立场。您可以使用 Dapper、其他一些对象关系映射器 (ORM),甚至是文档数据库来访问数据库。幸运的是,Identity 中的数据库集成是可插拔的,因此您可以换掉 EF Core 集成并改用自己的数据库集成库。

• My use case is too complex for Identity. Identity provides lower-level services for authentication, so you can compose the pieces however you like. It’s also extensible, so if you need to, for example, transform claims before creating a principal, you can.
我的用例对于 Identity 来说太复杂了。Identity 提供较低级别的身份验证服务,因此您可以根据自己的喜好组合各个部分。它也是可扩展的,因此,如果需要在创建主体之前转换声明,则可以。

• I don’t like the default Razor Pages UI. The default UI for Identity is entirely optional. You can still use the Identity services and user management but provide your own UI for logging in and registering users. However, be aware that although doing this gives you a lot of flexibility, it’s also easy to introduce a security flaw in your user-management system—the last place you want security flaws!
我不喜欢默认的 Razor Pages UI。Identity 的默认 UI 完全是可选的。您仍然可以使用 Identity 服务和用户管理,但提供自己的 UI 来登录和注册用户。但是,请注意,尽管这样做可以为您提供很大的灵活性,但也很容易在用户管理系统中引入安全漏洞 - 这是您最不希望出现安全漏洞的地方!

• I’m not using Bootstrap to style my application. The default Identity UI uses Bootstrap as a styling framework, the same as the default ASP.NET Core templates. Unfortunately, you can’t easily change that, so if you’re using a different framework or need to customize the HTML generated, you can still use Identity, but you’ll need to provide your own UI.
我没有使用 Bootstrap 来设置应用程序的样式。默认身份 UI 使用 Bootstrap 作为样式框架,与默认的 ASP.NET Core 模板相同。遗憾的是,您无法轻松更改此设置,因此,如果您使用的是其他框架或需要自定义生成的 HTML,您仍然可以使用 Identity,但需要提供自己的 UI。

• I don’t want to build my own identity system. I’m glad to hear it. Using an external identity provider like Azure Active Directory or Auth0 is a great way of shifting the responsibility and risk associated with storing users’ personal information to a third party.
我不想构建自己的身份系统。我很高兴听到这个消息。使用 Azure Active Directory 或 Auth0 等外部身份提供商是将与存储用户个人信息相关的责任和风险转移给第三方的好方法。

Any time you’re considering adding user management to your ASP.NET Core application, I’d recommend looking at Identity as a great option for doing so. In the next section I’ll demonstrate what Identity provides by creating a new Razor Pages application using the default Identity UI. In section 23.4 we’ll take that template and apply it to an existing app instead, and in sections 23.5 and 23.6 you’ll see how to override the default pages.
每当你考虑向 ASP.NET Core 应用程序添加用户管理时,我建议将 Identity 视为一个不错的选择。在下一节中,我将通过使用默认标识 UI 创建新的 Razor Pages 应用程序来演示标识提供的功能。在 23.4 节中,我们将获取该模板并将其应用于现有应用程序,在 23.5 和 23.6 节中,您将看到如何覆盖默认页面。

23.3 Creating a project that uses ASP.NET Core Identity

23.3 创建使用 ASP.NET Core Identity 的项目

I’ve covered authentication and Identity in general terms, but the best way to get a feel for it is to see some working code. In this section we’re going to look at the default code generated by the ASP.NET Core templates with Identity, how the project works, and where Identity fits in.
我已经大致介绍了身份验证和标识,但了解它的最佳方法是查看一些工作代码。在本节中,我们将了解使用 Identity 的 ASP.NET Core 模板生成的默认代码、项目的工作原理以及 Identity 的适用范围。

23.3.1 Creating the project from a template

23.3.1 从模板创建项目

You’ll start by using the Visual Studio templates to generate a simple Razor Pages application that uses Identity for storing individual user accounts in a database.
首先,使用 Visual Studio 模板生成一个简单的 Razor Pages 应用程序,该应用程序使用 Identity 将各个用户帐户存储在数据库中。

Tip You can create a similar project using the .NET CLI by running dotnet new webapp -au Individual. The Visual Studio template uses a LocalDB database, but the dotnet new template uses SQLite by default. To use LocalDB instead, run dotnet new webapp -au Individual --use-local-db.
提示:您可以通过运行 dotnet new webapp -au Individual 来使用 .NET CLI 创建类似的项目。Visual Studio 模板使用 LocalDB 数据库,但 dotnet 新模板默认使用 SQLite。要改用 LocalDB,请运行 dotnet new webapp -au Individual --use-local-db。

To create the template using Visual Studio, you must be using the 2022 version or later and have the .NET 7 software development kit (SDK) installed. Follow these steps:
要使用 Visual Studio 创建模板,您必须使用 2022 版本或更高版本,并安装 .NET 7 软件开发工具包 (SDK)。请执行以下步骤:

  1. Choose File > New > Project or choose Create a New Project on the splash screen.
    在初始屏幕上选择File > New > Project 创建新项目。

  2. From the list of templates, choose ASP.NET Core Web Application, ensuring that you select the C# language template.
    从模板列表中,选择 ASP.NET Core Web Application(核心 Web 应用程序),确保选择 C# 语言模板。

  3. On the next screen, enter a project name, location, and a solution name, and choose Create.
    在下一个屏幕上,输入项目名称、位置和解决方案名称,然后选择 Create (创建)。

  4. On the Additional Information screen, change the Authentication type to Individual Accounts, as shown in figure 23.4. Leave the other settings at their defaults, and choose Create to create the application.
    在 Additional Information 屏幕上,将 Authentication type 更改为 Individual Accounts,如图 23.4 所示。将其他设置保留为默认值,然后选择 Create (创建) 以创建应用程序。

Visual Studio automatically runs dotnet restore to restore all the necessary NuGet packages for the project.
Visual Studio 会自动运行 dotnet restore 来还原项目所需的所有 NuGet 包。

alt text
Figure 23.4 Choosing the authentication mode of the new ASP.NET Core application template in VS 2022
图 23.4 在 VS 2022 中选择新 ASP.NET Core 应用程序模板的身份验证模式

  1. Run the application to see the default app, as shown in figure 23.5.
    运行应用程序以查看默认应用程序,如图 23.5 所示。

NOTE The Visual Studio template configures the application to use LocalDB and includes EF Core migrations for SQL Server. If you want to use a different database provider, you can replace the configuration and migrations with your database of choice, as described in chapter 12.
注意:Visual Studio 模板将应用程序配置为使用 LocalDB,并包括 SQL Server 的 EF Core 迁移。如果要使用不同的数据库提供程序,可以将配置和迁移替换为您选择的数据库,如第 12 章所述。

alt text

Figure 23.5 The default template with individual account authentication looks similar to the no authentication template, with the addition of a Login widget at the top right of the page.
图 23.5 具有个人帐户身份验证的默认模板看起来类似于无身份验证模板,只是在页面右上角添加了一个 Login 小部件。

This template should look familiar, with one twist: you now have Register and Login buttons! Feel free to play with the template—creating a user, logging in and out—to get a feel for the app. Once you’re happy, look at the code generated by the template and the boilerplate it saved you from writing.
这个模板应该看起来很熟悉,但有一个变化:您现在有 Register 和 Login 按钮了!您可以随意使用模板 — 创建用户、登录和注销 — 以感受应用程序。满意后,请查看模板生成的代码以及它使您免于编写的样板。

Tip Don’t forget to run the included EF Core migrations before trying to create users. Run dotnet ef database update from the project folder.
提示:在尝试创建用户之前,请不要忘记运行包含的 EF Core 迁移。从项目文件夹运行 dotnet ef database update。

23.3.2 Exploring the template in Solution Explorer

23.3.2 在解决方案资源管理器中浏览模板

The project generated by the template, shown in figure 23.6, is similar to the default no-authentication template. That’s largely due to the default UI library, which brings in a big chunk of functionality without exposing you to the nitty-gritty details.
该模板生成的项目(如图 23.6 所示)类似于默认的 no-authentication 模板。这主要是由于默认的 UI 库,它带来了大量功能,而不会让您了解细节。

alt text

Figure 23.6 The project layout of the default template with individual authentication
图 23.6 使用单独身份验证的默认模板的项目布局

The biggest addition is the Areas folder in the root of your project, which contains an Identity subfolder. Areas are sometimes used for organizing sections of functionality. Each area can contain its own Pages folder, which is analogous to the main Pages folder in your application.
最大的新增功能是项目根目录中的 Areas 文件夹,其中包含一个 Identity 子文件夹。区域有时用于组织功能部分。每个区域都可以包含自己的 Pages 文件夹,该文件夹类似于应用程序中的主 Pages 文件夹。

DEFINITION Areas are used to group Razor Pages into separate hierarchies for organizational purposes. I rarely use areas and prefer to create subfolders in the main Pages folder instead. The one exception is the Identity UI, which uses a separate Identity area by default. For more details on areas, see Microsoft’s “Areas in ASP.NET Core” documentation: http://mng.bz/7Vw9.
定义:区域用于将 Razor 页面分组到单独的层次结构中,以便进行组织。我很少使用区域,更喜欢在主 Pages 文件夹中创建子文件夹。一个例外是 Identity UI,默认情况下,它使用单独的 Identity 区域。有关区域的更多详细信息,请参阅 Microsoft 的“ASP.NET Core 中的区域”文档:http://mng.bz/7Vw9

The Microsoft.AspNetCore.Identity.UI package creates Razor Pages in the Identity area. You can override any page in this default UI by creating a corresponding page in the Areas/Identity/Pages folder in your application. In figure 23.6, the default template adds a _ViewStart.cshtml file that overrides the template that is included as part of the default UI. This file contains the following code, which sets the default Identity UI Razor Pages to use your project’s default _Layout.cshtml file:
Microsoft.AspNetCore.Identity.UI 包在“标识”区域中创建 Razor Pages。您可以通过在应用程序的 Areas/Identity/Pages 文件夹中创建相应的页面来覆盖此默认 UI 中的任何页面。在图 23.6 中,默认模板添加了一个 _ViewStart.cshtml 文件,该文件将替代作为默认 UI 的一部分包含的模板。此文件包含以下代码,该代码将默认标识 UI Razor 页面设置为使用项目的默认 _Layout.cshtml 文件:

@{
    Layout = "/Pages/Shared/_Layout.cshtml";
}

Some obvious questions at this point are “How do you know what’s included in the default UI?” and “Which files can you override?” You’ll see the answers to both in section 23.5, but in general you should try to avoid overriding files where possible. After all, the goal with the default UI is to reduce the amount of code you have to write!
此时,一些明显的问题是“您如何知道默认 UI 中包含哪些内容”和“您可以覆盖哪些文件?您将在 Section 23.5 中看到这两个问题的答案,但一般来说,您应该尽可能避免覆盖文件。毕竟,默认 UI 的目标是减少您必须编写的代码量!

The Data folder in your new project template contains your application’s EF Core DbContext, called ApplicationDbContext, and the migrations for configuring the database schema to use Identity. I’ll discuss this schema in more detail in section 23.3.3.
新项目模板中的 Data 文件夹包含应用程序的 EF Core DbContext(称为 ApplicationDbContext)和用于将数据库架构配置为使用 Identity 的迁移。我将在 Section 23.3.3 中更详细地讨论这个模式。

The final additional file included in this template compared with the no-authentication version is the partial Razor view Pages/Shared/_LoginPartial.cshtml. This provides the Register and Login links you saw in figure 23.5, and it’s rendered in the default Razor layout, _Layout.cshtml.
与无身份验证版本相比,此模板中包含的最后一个附加文件是部分 Razor 视图 Pages/Shared/_LoginPartial.cshtml。这提供了你在图 23.5 中看到的 Register 和 Login 链接,并呈现在默认的 Razor 布局 _Layout.cshtml 中。

If you look inside _LoginPartial.cshtml, you can see how routing works with areas by combining the Razor Page path with an {area} route parameter using Tag Helpers. For example, the Login link specifies that the Razor Page /Account/Login is in the Identity area using the asp-area attribute:
如果查看 _LoginPartial.cshtml,则可以通过使用标记帮助程序将 Razor Page 路径与 {area} 路由参数组合在一起,了解路由如何与区域配合使用。例如,Login 链接使用 asp-area 属性指定 Razor Page /Account/Login 位于 Identity 区域中:

<a asp-area="Identity" asp-page="/Account/Login">Login</a>

Tip You can reference Razor Pages in the Identity area by setting the area route value to Identity. You can use the asp-area attribute in Tag Helpers that generate links.
提示:可以通过将区域路由值设置为 Identity 来引用 Identity 区域中的 Razor Pages。您可以在生成链接的标记帮助程序中使用 asp-area 属性。

In addition to viewing the new files included thanks to ASP.NET Core Identity, open Program.cs and look at the changes there. The most obvious change is the additional configuration, which adds all the services Identity requires, as shown in the following listing.
除了查看 ASP.NET Core Identity 包含的新文件外,还可以打开 Program.cs 并查看其中的更改。最明显的变化是额外的配置,它添加了 Identity 所需的所有服务,如下面的清单所示。

Listing 23.1 Adding ASP.NET Core Identity services to ConfigureServices
清单 23.1 向 ConfigureServices 添加 ASP.NET Core Identity 服务

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

string connectionString = builder.Configuration     #A
    .GetConnectionString("DefaultConnection");    #A
builder.Services.AddDbContext<ApplicationDbContext>(options =>  #A
    options.UseSqlServer(connectionString));    #A

builder.Services.AddDatabaseDeveloperPageExceptionFilter();   #B

builder.Services.AddDefaultIdentity<IdentityUser>(options =>    #C
    options.SignIn.RequireConfirmedAccount = true)    #D
        .AddEntityFrameworkStores<ApplicationDbContext>();    #E
builder.Services.AddRazorPages();

// remaining configuration not show

❶ ASP.NET Core Identity uses EF Core, so it includes the standard EF Core configuration.
ASP.NET Core Identity 使用 EF Core,因此它包括标准 EF Core 配置。
❷ Adds optional database services to enhance the DeveloperExceptionPage
添加可选的数据库服务以增强 DeveloperExceptionPage
❸ Adds the Identity system, including the default UI, and configures the user type as IdentityUser
添加标识系统,包括默认 UI,并将用户类型配置为 IdentityUser
❹ Requires users to confirm their accounts (typically by email) before they log in
要求用户在登录前确认其帐户(通常通过电子邮件)
❺ Configures Identity to store its data in EF Core
配置标识以将其数据存储在 EF Core 中

The AddDefaultIdentity() extension method does several things:
AddDefaultIdentity() 扩展方法执行以下几项作:

• Adds the core ASP.NET Core Identity services.
添加核心 ASP.NET 核心身份服务。
• Configures the application user type to be IdentityUser. This is the entity model that is stored in the database and represents a “user” in your application. You can extend this type if you need to, but that’s not always necessary, as you’ll see in section 23.6.
将应用程序用户类型配置为 IdentityUser。这是存储在数据库中的实体模型,表示应用程序中的 “用户”。如果需要,您可以扩展此类型,但这并不总是必要的,如第 23.6 节所示。
• Adds the default UI Razor Pages for registering, logging in, and managing users.
添加用于注册、登录和管理用户的默认 UI Razor Pages。
• Configures token providers for generating MFA and email confirmation tokens.
配置用于生成 MFA 和电子邮件确认令牌的令牌提供程序。

Where is the authentication middleware?
身份验证中间件在哪里?

If you’re already familiar with previous versions of ASP.NET Core, you might be surprised to notice the lack of any authentication middleware in the default template. Given everything you’ve learned about how authentication works, that should be surprising!
如果您已经熟悉 ASP.NET Core 的早期版本,您可能会惊讶地注意到默认模板中缺少任何身份验证中间件。鉴于您学到的有关身份验证工作原理的所有信息,这应该令人惊讶!

The answer to this riddle is that the authentication middleware is in the pipeline, even though you can’t see it. As I discussed in chapter 4, WebApplication automatically adds many middleware components to the pipeline for you, including the routing middleware, the endpoint middleware, and—yes—the authentication middleware. So the reason you don’t see it in the pipeline is that it’s already been added.
这个谜题的答案是,身份验证中间件正在开发中,即使您看不到它。正如我在第 4 章中所讨论的,WebApplication 会自动将许多中间件组件添加到管道中,包括路由中间件、端点中间件,是的,还有身份验证中间件。因此,您在管道中没有看到它的原因是它已被添加。

In fact, WebApplication also automatically adds the authorization middleware to the pipeline, but in this case the template still calls UseAuthorization(). Why? For the same reason that the template also calls UseRouting(): to control exactly where in the pipeline the middleware is added.
事实上,WebApplication 还会自动将授权中间件添加到管道中,但在这种情况下,模板仍然调用 UseAuthorization()。为什么?出于与模板还调用 UseRouting() 相同的原因:以准确控制中间件在管道中的添加位置。

As I mentioned in chapter 4, you can override the automatically added middleware by adding it yourself manually. It’s crucial that the authorization middleware be placed after the routing middleware, and as mentioned in chapter 4, you typically want to place your routing middleware after the static file middleware. As the routing middleware needs to move, so does the authorization middleware!
正如我在第 4 章中提到的,你可以通过自己手动添加来覆盖自动添加的中间件。将授权中间件放在路由中间件之后至关重要,如第 4 章所述,您通常希望将路由中间件放在静态文件中间件之后。由于路由中间件需要移动,授权中间件也需要移动!

Traditionally, the authentication middleware is also placed after the routing middleware, before the authorization middleware, but this isn’t crucial. The only requirement is that it’s placed before any middleware that requires an authenticated user, such as the authorization middleware.
传统上,身份验证中间件也放在路由中间件之后,授权中间件之前,但这并不重要。唯一的要求是,它位于任何需要经过身份验证的用户的中间件(例如授权中间件)之前。

》If you wish, you can move the location of the authentication middleware by calling UseAuthentication() at the appropriate point. I prefer to limit the work done on requests where possible, so I typically take this approach, moving it between the call to UseRouting() and UseAuthorization():
如果需要,可以通过在适当的位置调用 UseAuthentication() 来移动身份验证中间件的位置。我更喜欢尽可能限制对请求所做的工作,因此我通常采用这种方法,在对 UseRouting() 和 UseAuthorization() 的调用之间移动它:

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.Run();

If you don’t place the authentication middleware at the correct point in the pipeline, you can run into strange bugs where users aren’t authenticated correctly or authorization policies aren’t applied correctly. The templates work out of the box, but you need to take care if you’re working with an existing application or moving middleware around.
如果您没有将身份验证中间件放置在管道中的正确位置,则可能会遇到奇怪的错误,即用户未正确进行身份验证或授权策略未正确应用。这些模板开箱即用,但如果您正在使用现有应用程序或移动中间件,则需要小心。

Now that you’ve got an overview of the additions made by Identity, we’ll look in a bit more detail at the database schema and how Identity stores users in the database.
现在,您已经大致了解了 Identity 所做的添加,我们将更详细地了解数据库架构以及 Identity 如何在数据库中存储用户。

23.3.3 The ASP.NET Core Identity data model

23.3.3 ASP.NET Core Identity 数据模型

Out of the box, and in the default templates, Identity uses EF Core to store user accounts. It provides a base DbContext that you can inherit from, called IdentityDbContext, which uses an IdentityUser as the user entity for your application.
在默认模板中,Identity 使用 EF Core 来存储用户帐户。它提供了一个可以从中继承的基本 DbContext,称为 IdentityDbContext,它使用 IdentityUser 作为应用程序的用户实体。

In the template, the app’s DbContext is called ApplicationDbContext. If you open this file, you’ll see it’s sparse; it inherits from the IdentityDbContext base class I described earlier, and that’s it. What does this base class give you? The easiest way to see is to update a database with the migrations and take a look.
在模板中,应用的 DbContext 称为 ApplicationDbContext。如果打开此文件,您将看到它是稀疏的;它继承自我前面介绍的 IdentityDbContext 基类,仅此而已。这个基类为您提供了什么?最简单的方法是使用迁移更新数据库并查看。

Applying the migrations is the same process as in chapter 12. Ensure that the connection string points to where you want to create the database, open a command prompt in your project folder, and run this command to update the database with the migrations:
应用迁移的过程与第 12 章中的过程相同。确保连接字符串指向要创建数据库的位置,在项目文件夹中打开命令提示符,然后运行以下命令以使用迁移更新数据库:

dotnet ef database update

Tip If you see an error after running the dotnet ef command, ensure that you have the .NET tool installed by following the instructions provided in section 12.3.1. Also make sure that you run the command from the project folder, not the solution folder.
提示:如果在运行 dotnet ef 命令后看到错误,请确保按照第 12.3.1 节中提供的说明安装了 .NET 工具。此外,请确保从项目文件夹(而不是解决方案文件夹)运行命令。

If the database doesn’t exist, the command-line interface (CLI) creates it. Figure 23.7 shows what the database looks like for the default template.
如果数据库不存在,则命令行界面 (CLI) 会创建该数据库。图 23.7 显示了默认模板的数据库外观。

Tip If you’re using MS SQL Server (or LocalDB), you can use the SQL Server Object Explorer in Visual Studio to browse tables and objects in your database. See Microsoft’s “How to: Connect to a Database and Browse Existing Objects” article for details: http://mng.bz/mg8r.
提示:如果您使用的是 MS SQL Server(或 LocalDB),则可以使用 Visual Studio 中的 SQL Server 对象资源管理器浏览数据库中的表和对象。有关详细信息,请参阅 Microsoft 的“如何:连接到数据库并浏览现有对象”一文:http://mng.bz/mg8r

alt text

Figure 23.7 The database schema used by ASP.NET Core Identity
图 23.7 ASP.NET Core Identity 使用的数据库架构

That’s a lot of tables! You shouldn’t need to interact with these tables directly (Identity handles that for you), but it doesn’t hurt to have a basic grasp of what they’re for:
好多表啊!您不需要直接与这些表交互(Identity 会为您处理),但对它们的用途有基本的了解并没有什么坏处:

EFMigrationsHistory—The standard EF Core migrations table that records which migrations have been applied.
EFMigrationsHistory - 标准 EF Core 迁移表,用于记录已应用的迁移。

• AspNetUsers—The user profile table itself. This is where IdentityUser is serialized to. We’ll take a closer look at this table shortly.
AspNetUsers — 用户配置文件表本身。这是 IdentityUser 序列化到的位置。我们稍后会仔细看看这个表格。

• AspNetUserClaims—The claims associated with a given user. A user can have many claims, so it’s modeled as a many-to-one relationship.
AspNetUserClaims - 与给定用户关联的声明。一个用户可以有多个声明,因此它被建模为多对一关系。

• AspNetUserLogins and AspNetUserTokens—These are related to third-party logins. When configured, these let users sign in with a Google or Facebook account (for example) instead of creating a password on your app.
AspNetUserLogins 和 AspNetUserTokens - 这些与第三方登录相关。配置后,这些表允许用户使用 Google 或 Facebook 帐户 (例如) 登录,而不是在您的应用程序上创建密码。

• AspNetUserRoles, AspNetRoles, and AspNetRoleClaims—These tables are somewhat of a legacy left over from the old role-based permission model of the pre-.NET 4.5 days, instead of the claims-based permission model. These tables let you define roles that multiple users can belong to. Each role can be assigned multiple claims. These claims are effectively inherited by a user principal when they are assigned that role.
AspNetUserRoles、AspNetRoles 和 AspNetRoleClaims - 这些表在某种程度上是 pre-.NET 4.5 天的基于角色的旧权限模型遗留下来的遗留问题,而不是基于声明的权限模型。这些表允许您定义多个用户可以属于的角色。每个角色都可以分配多个声明。当为用户主体分配该角色时,这些声明将由用户主体有效地继承。

You can explore these tables yourself, but the most interesting of them is the AspNetUsers table, shown in figure 23.8.
您可以自己浏览这些表,但其中最有趣的是 AspNetUsers 表,如图 23.8 所示。

alt text

Figure 23.8 The AspNetUsers table is used to store all the details required to authenticate a user.
图 23.8 AspNetUsers 表用于存储验证用户所需的所有详细信息。

Most of the columns in the AspNetUsers table are security-related—the user’s email, password hash, whether they have confirmed their email, whether they have MFA enabled, and so on. By default, there are no columns for additional information, like the user’s name.
AspNetUsers 表中的大多数列都与安全相关 — 用户的电子邮件、密码哈希、他们是否已确认其电子邮件、他们是否启用了 MFA 等。默认情况下,没有其他信息(如用户名)的列。

NOTE You can see from figure 23.8 that the primary key Id is stored as a string column. By default, Identity uses Guid for the identifier. To customize the data type, see the “Change the primary key type” section of Microsoft’s “Identity model customization in ASP.NET Core” documentation: http://mng.bz/5jdB.
注意:从图 23.8 中可以看出,主键 Id 存储为字符串列。默认情况下,Identity 使用 Guid 作为标识符。要自定义数据类型,请参阅 Microsoft 的“ASP.NET Core 中的身份模型自定义”文档的“更改主键类型”部分:http://mng.bz/5jdB

Any additional properties of the user are stored as claims in the AspNetUserClaims table associated with that user. This lets you add arbitrary additional information without having to change the database schema to accommodate it. Want to store the user’s date of birth? You could add a claim to that user; there’s no need to change the database schema. You’ll see this in action in section 23.6, when you add a Name claim to every new user.
用户的任何其他属性都作为声明存储在与该用户关联的 AspNetUserClaims 表中。这样,您就可以添加任意的附加信息,而不必更改数据库架构来容纳它。想要存储用户的出生日期?您可以向该用户添加声明;无需更改数据库架构。您将在第 23.6 节中看到这一点,当您为每个新用户添加 Name 声明时。

NOTE Adding claims is often the easiest way to extend the default IdentityUser, but you can add properties to the IdentityUser directly. This requires database changes but is nevertheless useful in many situations. You can read how to add custom data using this approach here: http://mng.bz/Xd61.
注意:添加声明通常是扩展默认 IdentityUser 的最简单方法,但您可以直接向 IdentityUser 添加属性。这需要更改数据库,但在许多情况下仍然很有用。您可以在此处阅读如何使用此方法添加自定义数据:http://mng.bz/Xd61

It’s important to understand the difference between the IdentityUser entity (stored in the AspNetUsers table) and the ClaimsPrincipal, which is exposed on HttpContext.User. When a user first logs in, an IdentityUser is loaded from the database. This entity is combined with additional claims for the user from the AspNetUserClaims table to create a ClaimsPrincipal. It’s this ClaimsPrincipal that is used for authentication and is serialized to the authentication cookie, not the IdentityUser.
了解 IdentityUser 实体(存储在 AspNetUsers 表中)和 ClaimsPrincipal(在 HttpContext.User 上公开)之间的区别非常重要。当用户首次登录时,将从数据库中加载 IdentityUser。此实体与 AspNetUserClaims 表中用户的其他声明组合在一起,以创建 ClaimsPrincipal。此 ClaimsPrincipal 用于身份验证,并序列化为身份验证 Cookie,而不是 IdentityUser。

It’s useful to have a mental model of the underlying database schema Identity uses, but in day-to-day work, you shouldn’t have to interact with it directly. That’s what Identity is for, after all! In the next section we’ll look at the other end of the scale: the UI of the app and what you get out of the box with the default UI.
拥有 Identity 使用的基础数据库架构的心智模型很有用,但在日常工作中,您不应该直接与之交互。毕竟,这就是 Identity 的意义所在!在下一节中,我们将了解天平的另一端:应用程序的 UI 以及您使用默认 UI 开箱即用的功能。

23.3.4 Interacting with ASP.NET Core Identity

23.3.4 与 ASP.NET Core Identity 交互

You’ll want to explore the default UI yourself to get a feel for how the pieces fit together, but in this section I’ll highlight what you get out of the box, as well as areas that typically require additional attention right away.
您需要亲自探索默认 UI,以了解各个部分是如何组合在一起的,但在本节中,我将重点介绍您开箱即用的功能,以及通常需要立即额外注意的领域。

The entry point to the default UI is the user registration page of the application, shown in figure 23.9. The register page enables users to sign up to your application by creating a new IdentityUser with an email and a password. After creating an account, users are redirected to a screen indicating that they should confirm their email. No email service is enabled by default, as this is dependent on your configuring an external email service. You can read how to enable email sending in Microsoft’s “Account confirmation and password recovery in ASP.NET Core” documentation at http://mng.bz/6gBo. Once you configure this, users will automatically receive an email with a link to confirm their account.
默认 UI 的入口点是应用程序的用户注册页面,如图 23.9 所示。通过注册页面,用户可以通过使用电子邮件和密码创建新的 IdentityUser 来注册您的应用程序。创建账户后,用户将被重定向到一个屏幕,指示他们应该确认他们的电子邮件。默认情况下,不启用任何电子邮件服务,因为这取决于您配置外部电子邮件服务。您可以在 http://mng.bz/6gBo 的 Microsoft 的“ASP.NET Core 中的帐户确认和密码恢复”文档中阅读如何启用电子邮件发送。配置此项后,用户将自动收到一封电子邮件,其中包含用于确认其帐户的链接。

alt text

Figure 23.9 The registration flow for users using the default Identity UI. Users enter an email and password and are redirected to a “confirm your email” page. This is a placeholder page by default, but if you enable email confirmation, this page will update appropriately.
图 23.9 使用默认 Identity UI 的用户的注册流程。用户输入电子邮件和密码,并被重定向到“确认您的电子邮件”页面。默认情况下,这是一个占位符页面,但如果您启用电子邮件确认,此页面将相应地更新。

By default, user emails must be unique (you can’t have two users with the same email), and the password must meet various length and complexity requirements. You can customize these options and more in the configuration lambda of the call to AddDefaultIdentity() in Program.cs, as shown in the following listing.
默认情况下,用户电子邮件必须是唯一的(您不能让两个用户使用同一电子邮件),并且密码必须满足各种长度和复杂性要求。您可以在 Program.cs 中调用 AddDefaultIdentity() 的配置 lambda 中自定义这些选项以及更多选项,如下面的清单所示。

Listing 23.2 Customizing Identity settings in ConfigureServices in Startup.cs
清单 23.2 在 Startup.cs 的 ConfigureServices 中自定义身份设置

builder.Services.AddDefaultIdentity<IdentityUser>(options =>
{
    options.SignIn.RequireConfirmedAccount = true;     #A
    options.Lockout.AllowedForNewUsers = true;    #B
    options.Password.RequiredLength = 12;               #C
    options.Password.RequireNonAlphanumeric = false;    #C
    options.Password.RequireDigit = false;              #C
})
.AddEntityFrameworkStores<AppDbContext>();

❶ Requires users to confirm their account by email before they can log in
要求用户在登录之前通过电子邮件确认其帐户
❷ Enables user lockout, to prevent brute-force attacks against user passwords
启用用户锁定,以防止对用户密码的暴力攻击
❸ Updates password requirements. Current guidance is to require long passwords.
更新密码要求。当前的指导是要求使用长密码。

After a user has registered with your application, they need to log in, as shown in figure 23.10. On the right side of the login page, the default UI templates describe how you, the developer, can configure external login providers, such as Facebook and Google. This is useful information for you, but it’s one of the reasons you may need to customize the default UI templates, as you’ll see in section 23.5.
用户注册到您的应用程序后,他们需要登录,如图 23.10 所示。在登录页面的右侧,默认 UI 模板描述了您(开发人员)如何配置外部登录提供程序,例如 Facebook 和 Google。这对您有用,但这也是您可能需要自定义默认 UI 模板的原因之一,如第 23.5 节所示。

alt text

Figure 23.10 Logging in with an existing user and managing the user account. The Login page describes how to configure external login providers, such as Facebook and Google. The user-management pages allow users to change their email and password and to configure MFA.
图 23.10 使用现有用户登录并管理用户帐户。Login (登录) 页面介绍了如何配置外部登录提供程序,例如 Facebook 和 Google。用户管理页面允许用户更改其电子邮件和密码以及配置 MFA。

Once a user has signed in, they can access the management pages of the identity UI. These allow users to change their email, change their password, configure MFA with an authenticator app, or delete all their personal data. Most of these functions work without any effort on your part, assuming that you’ve already configured an email-sending service.
用户登录后,他们可以访问身份 UI 的管理页面。这些允许用户更改他们的电子邮件、更改他们的密码、使用身份验证器应用程序配置 MFA 或删除他们的所有个人数据。这些函数中的大多数都无需您执行任何作即可工作,前提是您已经配置了电子邮件发送服务。

That covers everything you get in the default UI templates. It may seem somewhat minimal, but it covers a lot of the requirements that are common to almost all apps. Nevertheless, there are a few things you’ll nearly always want to customize:
这涵盖了您在默认 UI 模板中获得的所有内容。它可能看起来有些微不足道,但它涵盖了几乎所有应用程序通用的许多要求。不过,您几乎总是需要自定义一些内容:

• Configure an email-sending service, to enable account confirmation and password recovery, as described in Microsoft’s “Account confirmation and password recovery in ASP.NET Core” documentation: http://mng.bz/vzy7.
配置电子邮件发送服务,以启用帐户确认和密码恢复,如 Microsoft 的“ASP.NET Core 中的帐户确认和密码恢复”文档中所述:http://mng.bz/vzy7

• Add a QR code generator for the enable MFA page, as described in Microsoft’s “Enable QR Code generation for TOTP authenticator apps in ASP.NET Core” documentation: http://mng.bz/4Zmw.
为启用 MFA 页面添加 QR 码生成器,如 Microsoft 的“在 ASP.NET Core 中为 TOTP 验证器应用程序启用 QR 码生成”文档中所述:http://mng.bz/4Zmw

• Customize the register and login pages to remove the documentation link for enabling external services. You’ll see how to do this in section 23.5. Alternatively, you may want to disable user registration entirely, as described in Microsoft’s “Scaffold Identity in ASP.NET Core projects” documentation: http://mng.bz/QmMG.
自定义注册和登录页面,以删除用于启用外部服务的文档链接。您将在 Section 23.5 中看到如何执行此作。或者,您可能希望完全禁用用户注册,如 Microsoft 的“ASP.NET Core 项目中的基架身份”文档中所述:http://mng.bz/QmMG

• Collect additional information about users on the registration page. You’ll see how to do this in section 23.6.
在注册页面上收集有关用户的其他信息。您将在 Section 23.6 中看到如何执行此作。

There are many more ways you can extend or update the Identity system and lots of options available, so I encourage you to explore Microsoft’s “Overview of ASP.NET Core authentication” at http://mng.bz/XdGv to see your options. In the next section you’ll see how to achieve another common requirement: adding users to an existing application.
还有更多方法可以扩展或更新 Identity 系统,并且有很多可用选项,因此我鼓励您在 http://mng.bz/XdGv 上浏览 Microsoft 的“ASP.NET Core 身份验证概述”以查看您的选项。在下一节中,您将了解如何实现另一个常见要求:将用户添加到现有应用程序。

23.4 Adding ASP.NET Core Identity to an existing project

23.4 将 ASP.NET Core Identity 添加到现有工程

In this section we’re going to add users to an existing application. The initial app is a Razor Pages app, based on recipe application from chapter 12. This is a working app that you want to add user functionality to. In chapter 24 we’ll extend this work to restrict control regarding who’s allowed to edit recipes on the app.
在本节中,我们将向现有应用程序添加用户。初始应用是 Razor Pages 应用,基于第 12 章中的配方应用。这是您要向其添加用户功能的工作应用程序。在第 24 章中,我们将扩展这项工作,以限制对谁可以在应用程序上编辑配方的控制。

By the end of this section, you’ll have an application with a registration page, a login screen, and a manage account screen, like the default templates. You’ll also have a persistent widget in the top right of the screen showing the login status of the current user, as shown in figure 23.11.
在本部分结束时,您将拥有一个应用程序,其中包含注册页面、登录屏幕和管理帐户屏幕,就像默认模板一样。屏幕右上角还有一个持久小部件,显示当前用户的登录状态,如图 23.11 所示。

alt text

Figure 23.11 The recipe app after adding authentication, showing the login widget
图 23.11 添加身份验证后的配方应用程序,显示登录小部件

As in section 23.3, I’m not going to customize any of the defaults at this point, so we won’t set up external login providers, email confirmation, or MFA. I’m concerned only with adding ASP.NET Core Identity to an existing app that’s already using EF Core.
与 Section 23.3 一样,我此时不打算自定义任何默认值,因此我们不会设置外部登录提供程序、电子邮件确认或 MFA。我只关心将 ASP.NET Core Identity 添加到已在使用 EF Core 的现有应用程序。

Tip It’s worth making sure you’re comfortable with the new project templates before you go about adding Identity to an existing project. Create a test app, and consider setting up an external login provider, configuring an email provider, and enabling MFA. This will take a bit of time, but it’ll be invaluable for deciphering errors when you come to adding Identity to existing apps.
提示在将 Identity 添加到现有项目之前,值得确保您熟悉新的项目模板。创建测试应用程序,并考虑设置外部登录提供程序、配置电子邮件提供程序并启用 MFA。这将花费一些时间,但在您将 Identity 添加到现有应用程序时,它对于破译错误非常宝贵。

To add Identity to your app, you’ll need to do the following:
要将 Identity 添加到您的应用程序,您需要执行以下作:

  1. Add the ASP.NET Core Identity NuGet packages.
    添加 ASP.NET Core Identity NuGet 包。
  2. Add the required Identity services to the dependency injection (DI) container.
    将所需的身份服务添加到依赖关系注入 (DI) 容器中。
  3. Update the EF Core data model with the Identity entities.
    使用 Identity 实体更新 EF Core 数据模型。
  4. Update your Razor Pages and layouts to provide links to the Identity UI.
    更新 Razor 页面和布局,以提供指向标识 UI 的链接。

This section tackles each of these steps in turn. At the end of section 23.4 you’ll have successfully added user accounts to the recipe app.
本节将依次介绍这些步骤中的每一个。在第 23.4 节结束时,您将成功地将用户帐户添加到配方应用程序。

23.4.1 Configuring the ASP.NET Core Identity services

23.4.1 配置 ASP.NET Core Identity 服务
You can add ASP.NET Core Identity with the default UI to an existing app by referencing two NuGet packages:
您可以通过引用两个 NuGet 包,将带有默认 UI 的 ASP.NET Core Identity 添加到现有应用程序:

• Microsoft.AspNetCore.Identity.EntityFrameworkCore—Provides all the core Identity services and integration with EF Core
Microsoft.AspNetCore.Identity.EntityFrameworkCore - 提供所有核心身份服务以及与 EF Core的集成
• Microsoft.AspNetCore.Identity.UI—Provides the default UI Razor Pages
Microsoft.AspNetCore.Identity.UI - 提供默认 UI Razor 页面

Update your project .csproj file to include these two packages:
更新项目 .csproj 文件以包含以下两个包:

<PackageReference
    Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore"
    Version="7.0.0" />
<PackageReference
    Include="Microsoft.AspNetCore.Identity.UI" Version="7.0.0" />

These packages bring in all the additional required dependencies you need to add Identity with the default UI. Be sure to run dotnet restore after adding them to your project.
这些包引入了使用默认 UI 添加 Identity 所需的所有额外必需依赖项。请务必在将它们添加到项目后运行 dotnet restore。

Once you’ve added the Identity packages, you can update your Program.cs file to include the Identity services, as shown in the following listing. This is similar to the default template setup you saw in listing 23.1, but make sure to reference your existing AppDbContext.
添加 Identity 包后,您可以更新 Program.cs 文件以包含 Identity 服务,如以下清单所示。这类似于您在清单 23.1 中看到的默认模板设置,但请确保引用您现有的 AppDbContext。

Listing 23.3 Adding ASP.NET Core Identity services to the recipe app
清单 23.3 将 ASP.NET Core Identity 服务添加到 recipe 应用程序

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>   #A
    options.UseSqlite(builder.Configuration    #A
        .GetConnectionString("DefaultConnection")!));   #A

builder.Services.AddDefaultIdentity<ApplicationUser>(options =>      #B
        options.SignIn.RequireConfirmedAccount = true)       #B
    .AddEntityFrameworkStores<AppDbContext>();     #C

builder.Services.AddRazorPages();
builder.Services.AddScoped<RecipeService>();

❶ The existing service configuration is unchanged.
现有服务配置保持不变。
❷ Adds the Identity services to the DI container and uses a custom user type, ApplicationUser
将身份服务添加到 DI 容器并使用自定义用户类型 ApplicationUser
❸ Makes sure you use the name of your existing DbContext app
确保您使用现有 DbContext 应用程序的名称

This adds all the necessary services and configures Identity to use EF Core. I’ve introduced a new type here, ApplicationUser, which we’ll use to customize our user entity later. You’ll see how to add this type in section 23.4.2.
这将添加所有必要的服务,并将 Identity 配置为使用 EF Core。我在这里引入了一个新类型 ApplicationUser,我们稍后将使用它来自定义我们的用户实体。您将在 Section 23.4.2 中看到如何添加此类型。

The next step is optional: add the AuthenticationMiddleware after the call to UseRouting() on WebApplication, as shown in the following listing. As I mentioned previously, the authentication middleware is added automatically by WebApplication, so this step is optional. I prefer to delay authentication until after the call to UseRouting(), as it eliminates the need to perform unnecessary work decrypting the authentication cookie for requests that don’t reach the routing middleware, such as requests for static files.
下一步是可选的:在调用 WebApplication 上的 UseRouting() 之后添加 AuthenticationMiddleware,如下面的清单所示。正如我前面提到的,身份验证中间件是由 WebApplication 自动添加的,因此此步骤是可选的。我更喜欢将身份验证延迟到调用 UseRouting() 之后,因为这样就无需为未到达路由中间件的请求(例如静态文件请求)执行不必要的身份验证 Cookie 解密工作。

Listing 23.4 Adding AuthenticationMiddleware to the recipe app
列表 23.4 将 AuthenticationMiddleware 添加到 recipe 应用程序

app.UseStaticFiles();            #A

app.UseRouting();

app.UseAuthentication();        #B
app.UseAuthorization();          #C

app.MapRazorPages();
app.Run

❶ StaticFileMiddleware will never see requests as authenticated, even after you sign in.
StaticFileMiddleware 永远不会将请求视为已验证,即使在你登录后也是如此。
❷ Adds AuthenticationMiddleware after UseRouting() and before UseAuthorization
在 UseRouting() 之后和 UseAuthorization之前添加 AuthenticationMiddleware
❸ Middleware after AuthenticationMiddleware can read the user principal from HttpContext.User.
AuthenticationMiddleware 之后的中间件可以从 HttpContext.User 读取用户主体。

You’ve configured your app to use Identity, so the next step is updating EF Core’s data model. You’re already using EF Core in this app, so you need to update your database schema to include the tables that Identity requires.
你已将应用配置为使用 Identity,因此下一步是更新 EF Core 的数据模型。你已在此应用中使用 EF Core,因此需要更新数据库架构以包含 Identity 所需的表。

23.4.2 Updating the EF Core data model to support Identity

23.4.2 更新 EF Core 数据模型以支持身份

The code in listing 23.3 won’t compile, as it references the ApplicationUser type, which doesn’t yet exist. Create the ApplicationUser in the Data folder, using the following line:
清单 23.3 中的代码无法编译,因为它引用了尚不存在的 ApplicationUser 类型。使用以下行在 Data 文件夹中创建 ApplicationUser:

public class ApplicationUser : IdentityUser { }

It’s not strictly necessary to create a custom user type in this case (for example, the default templates use the raw IdentityUser), but I find it’s easier to add the derived type now rather than try to retrofit it later if you need to add extra properties to your user type.
在这种情况下,并非绝对需要创建自定义用户类型(例如,默认模板使用原始 IdentityUser),但我发现,如果您需要向用户类型添加额外的属性,现在添加派生类型比以后尝试修改它更容易。

In section 23.3.3 you saw that Identity provides a DbContext called IdentityDbContext, which you can inherit from. The IdentityDbContext base class includes the necessary DbSet to store your user entities using EF Core.
在第 23.3.3 节中,您看到 Identity 提供了一个名为 IdentityDbContext 的 DbContext,您可以从中继承。IdentityDbContext 基类包括使用 EF Core 存储用户实体所需的 DbSet。

Updating an existing DbContext for Identity is simple: update your app’s DbContext to inherit from IdentityDbContext (which itself inherits from DbContext), as shown in the following listing. We’re using the generic version of the base Identity context in this case and providing the ApplicationUser type.
更新现有的 DbContext for Identity 很简单:更新应用程序的 DbContext 以从 IdentityDbContext(它本身继承自 DbContext)继承,如下面的清单所示。在本例中,我们使用基本 Identity 上下文的通用版本,并提供 ApplicationUser 类型。

Listing 23.5 Updating AppDbContext to use IdentityDbContext
列表 23.5 更新 AppDbContext 以使用 IdentityDbContext

public class AppDbContext : IdentityDbContext<ApplicationUser>    #A
{
    public AppDbContext(DbContextOptions<AppDbContext> options)  #B
        : base(options)                                          #B
    { }                                                          #B

    public DbSet<Recipe> Recipes { get; set; }                   #B
}

❶ Updates to inherit from the Identity context instead of directly from DbContext
更新以从 Identity 上下文继承,而不是直接从 DbContext继承
❷ The remainder of the class remains the same.
类的其余部分保持不变。

Effectively, by updating the base class of your context in this way, you’ve added a whole load of new entities to EF Core’s data model. As you saw in chapter 12, whenever EF Core’s data model changes, you need to create a new migration and apply those changes to the database.
实际上,通过以这种方式更新上下文的基类,你已向 EF Core 的数据模型添加了大量新实体。如第 12 章所示,每当 EF Core 的数据模型发生更改时,都需要创建新的迁移并将这些更改应用于数据库。

At this point, your app should compile, so you can add a new migration called AddIdentitySchema using
此时,你的应用应该会编译,因此你可以使用 AddIdentitySchema 添加名为 AddIdentitySchema 的新迁移

dotnet ef migrations add AddIdentitySchema

The final step is updating your application’s Razor Pages and layouts to reference the default identity UI. Normally, adding 30 new Razor Pages to your application would be a lot of work, but using the default Identity UI makes it a breeze.
最后一步是更新应用程序的 Razor Pages 和布局以引用默认标识 UI。通常,向应用程序添加 30 个新的 Razor 页面会是一项艰巨的工作,但使用默认标识 UI 会变得轻而易举。

23.4.3 Updating the Razor views to link to the Identity UI

23.4.3 更新 Razor 视图以链接到身份 UI

Technically, you don’t have to update your Razor Pages to reference the pages included in the default UI, but you probably want to add the login widget to your app’s layout at a minimum. You’ll also want to make sure that your Identity Razor Pages use the same base Layout.cshtml as the rest of your application.
从技术上讲,您不必更新 Razor Pages 来引用默认 UI 中包含的页面,但您可能希望至少将登录小组件添加到应用程序的布局中。还需要确保 Identity Razor 页面使用与应用程序其余部分相同的基 Layout.cshtml。

We’ll start by fixing the layout for your Identity pages. Create a file at the “magic” path Areas/Identity/Pages/_ViewStart.cshtml, and add the following contents:
首先,我们将修复 Identity 页面的布局。在“magic”路径 Areas/Identity/Pages/_ViewStart.cshtml 处创建一个文件,并添加以下内容:

@{ Layout = "/Pages/Shared/_Layout.cshtml"; }

This sets the default layout for your Identity pages to your application’s default layout. Next, add a _LoginPartial.cshtml file in Pages/Shared to define the login widget, as shown in the following listing. This is pretty much identical to the template generated by the default template, but it uses our custom ApplicationUser instead of the default IdentityUser.
这会将 Identity 页面的默认布局设置为应用程序的默认布局。接下来,在 Pages/Shared 中添加 _LoginPartial.cshtml 文件以定义登录小组件,如下面的清单所示。这与默认模板生成的模板几乎相同,但它使用我们的自定义 ApplicationUser 而不是默认的 IdentityUser。

Listing 23.6 Adding a _LoginPartial.cshtml to an existing app
列表 23.6 将 _LoginPartial.cshtml 添加到现有应用程序

@using Microsoft.AspNetCore.Identity
@using RecipeApplication.Data;                 #A
@inject SignInManager<ApplicationUser> SignInManager    #B
@inject UserManager<ApplicationUser> UserManager        #B

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
  <li class="nav-item">
    <a  class="nav-link text-dark" asp-area="Identity"
    asp-page="/Account/Manage/Index" title="Manage">
        Hello @User.Identity.Name!</a>
  </li>
    <li class="nav-item">
      <form class="form-inline" asp-page="/Account/Logout"
      asp-route-returnUrl="@Url.Page("/", new { area = "" })"
      asp-area="Identity" method="post" >
        <button  class="nav-link btn btn-link text-dark"
          type="submit">Logout</button>
        </form>
    </li>
}
else
{
  <li class="nav-item">
    <a class="nav-link text-dark" asp-area="Identity"
      asp-page="/Account/Register">Register</a>
  </li>
  <li class="nav-item">
    <a class="nav-link text-dark" asp-area="Identity"
      asp-page="/Account/Login">Login</a>
  </li>
}
</ul>

❶ Updates to your project’s namespace that contains ApplicationUser
更新包含 ApplicationUser的项目命名空间
❷ The default template uses IdentityUser. Update to use ApplicationUser instead.
默认模板使用 IdentityUser。更新以改用 ApplicationUser。

This partial shows the current login status of the user and provides links to register or sign in. All that remains is to render the partial by calling
此部分显示用户的当前登录状态,并提供用于注册或登录的链接。剩下的就是通过调用

<partial name="_LoginPartial" />

in the main layout file of your app, _Layout.cshtml.
在应用程序的主布局文件中,_Layout.cshtml。

And there you have it: you’ve added Identity to an existing application. The default UI makes doing this relatively simple, and you can be sure you haven’t introduced any security holes by building your own UI!
好了:您已将 Identity 添加到现有应用程序。默认 UI 使此作相对简单,您可以确保通过构建自己的 UI 没有引入任何安全漏洞!

As I described in section 23.3.4, there are some features that the default UI doesn’t provide and you need to implement yourself, such as email confirmation and MFA QR code generation. It’s also common to find that you want to update a single page here and there. In the next section I’ll show how you can replace a page in the default UI, without having to rebuild the entire UI yourself.
正如我在第 23.3.4 节中所描述的,默认 UI 不提供一些功能,您需要自行实现,例如电子邮件确认和 MFA QR 码生成。你也经常会发现你想在这里和那里更新单个页面。在下一节中,我将展示如何替换默认 UI 中的页面,而不必自己重新构建整个 UI。

23.5 Customizing a page in ASP.NET Core Identity’s default UI

23.5 在 ASP.NET Core Identity 的默认 UI 中自定义页面

In this section you’ll learn how to use scaffolding to replace individual pages in the default Identity UI. You’ll learn to scaffold a page so that it overrides the default UI, allowing you to customize both the Razor template and the PageModel page handlers.
在本节中,您将学习如何使用基架替换默认 Identity UI 中的各个页面。您将学习如何搭建页面基架,使其覆盖默认 UI,从而允许您自定义 Razor 模板和 PageModel 页面处理程序。

Having Identity provide the whole UI for your application is great in theory, but in practice there are a few wrinkles, as you saw in section 23.3.4. The default UI provides as much as it can, but there are some things you may want to tweak. For example, both the login and register pages describe how to configure external login providers for your ASP.NET Core applications, as you saw in figures 23.12 and 23.13. That’s useful information for you as a developer, but it’s not something you want to be showing to your users. Another often-cited requirement is the desire to change the look and feel of one or more pages.
让 Identity 为您的应用程序提供整个 UI 在理论上很好,但在实践中存在一些问题,正如您在第 23.3.4 节中看到的那样。默认 UI 提供了尽可能多的功能,但您可能需要调整一些内容。例如,登录和注册页面都描述了如何为 ASP.NET Core 应用程序配置外部登录提供程序,如图 23.12 和 23.13 所示。这对开发人员来说很有用,但不是您希望向用户展示的信息。另一个经常被引用的要求是希望更改一个或多个页面的外观。

Luckily, the default Identity UI is designed to be incrementally replaceable, so you can override a single page without having to rebuild the entire UI yourself. On top of that, both Visual Studio and the .NET CLI have functions that allow you to scaffold any (or all) of the pages in the default UI so that you don’t have to start from scratch when you want to tweak a page.
幸运的是,默认的 Identity UI 设计为可增量替换,因此您可以覆盖单个页面,而无需自己重新构建整个 UI。最重要的是,Visual Studio 和 .NET CLI 都具有允许您在默认 UI 中搭建任何(或所有)页面的基架的功能,这样当您想要调整页面时,就不必从头开始。

DEFINITION Scaffolding is the process of generating files in your project that serve as the basis for customization. The Identity scaffolder adds Razor Pages in the correct locations so they override equivalent pages with the default UI. Initially, the code in the scaffolded pages matches that in the default Identity UI, but you are free to customize it.
定义:基架是在项目中生成文件作为自定义基础的过程。Identity 基架将 Razor Pages 添加到正确的位置,以便它们使用默认 UI 覆盖等效页面。最初,基架页面中的代码与默认 Identity UI 中的代码匹配,但您可以自由自定义它。

As an example of the changes you can easily make, we’ll scaffold the registration page and remove the additional information section about external providers. The following steps describe how to scaffold the Register.cshtml page in Visual Studio:
作为您可以轻松进行的更改的示例,我们将搭建注册页面并删除有关外部提供商的其他信息部分。以下步骤介绍如何在 Visual Studio 中搭建 Register.cshtml 页面的基架:

  1. Add the Microsoft.VisualStudio.Web.CodeGeneration.Design and Microsoft .EntityFrameworkCore.Tools NuGet packages to your project file, if they’re not already added. Visual Studio uses these packages to scaffold your application correctly, and without them you may get an error running the scaffolder:
    添加 Microsoft.VisualStudio.Web.CodeGeneration.Design 和 Microsoft 。EntityFrameworkCore.Tools NuGet 包添加到项目文件中(如果尚未添加)。Visual Studio 使用这些包来正确搭建应用程序基架,如果没有它们,运行 Scaffolder 时可能会遇到错误:
<PackageReference Version="7.0.0"
    Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" />
<PackageReference Version="7.0.0"
    Include="Microsoft.EntityFrameworkCore.Tools" />
  1. Ensure that your project builds. If it doesn’t build, the scaffolder will fail before adding your new pages.
    确保您的项目构建成功。如果它没有构建,则 scaffolder 将在添加新页面之前失败。

  2. Right-click your project, and choose Add > New Scaffolded Item from the contextual menu.
    右键单击您的项目,然后从上下文菜单中选择 Add > New Scaffolded Item (添加新基架项)。

  3. In the selection dialog box, choose Identity from the category, and choose Add.
    在选择对话框中,从类别中选择 Identity (身份),然后选择 Add (添加)。

  4. In the Add Identity dialog box, select the Account/Register page, and select your application’s AppDbContext as the Data context class, as shown in figure 23.12. Choose Add to scaffold the page.
    在 Add Identity 对话框中,选择 Account/Register 页面,然后选择应用程序的 AppDbContext 作为 Data 上下文类,如图 23.12 所示。选择 Add (添加) 以搭建页面基架。

alt text

Figure 23.12 Using Visual Studio to scaffold Identity pages. The generated Razor Pages will override the versions provided by the default UI.
图 23.12 使用 Visual Studio 搭建 Identity 页面的基架。生成的 Razor Pages 将替代默认 UI 提供的版本。

Tip To scaffold the registration page using the .NET CLI, install the required tools and packages as described in Microsoft’s “Scaffold Identity in ASP.NET Core projects” documentation: http://mng.bz/QPRv. Then run dotnet aspnet-codegenerator identity -dc RecipeApplication.Data.AppDbContext --files "Account.Register".
提示:要使用 .NET CLI 搭建注册页面的基架,请按照 Microsoft 的“ASP.NET Core 项目中的基架标识”文档中所述安装所需的工具和包:http://mng.bz/QPRv。然后运行 dotnet aspnet-codegenerator identity -dc RecipeApplication.Data.AppDbContext --files “Account.Register”。

Visual Studio builds your application and then generates the Register.cshtml page for you, placing it in the Areas/Identity/Pages/Account folder. It also generates several supporting files, as shown in figure 23.13. These are required mostly to ensure that your new Register.cshtml page can reference the remaining pages in the default Identity UI.
Visual Studio 生成应用程序,然后生成 Register.cshtml 页面,将其放置在 Areas/Identity/Pages/Account 文件夹中。它还会生成几个支持文件,如图 23.13 所示。这些主要是为了确保新的 Register.cshtml 页面可以引用默认标识 UI 中的其余页面。

alt text

Figure 23.13 The scaffolder generates the Register.cshtml Razor Page, along with supporting files required to integrate with the remainder of the default Identity UI.
图 23.13 基架生成 Register.cshtml Razor 页面,以及与默认标识 UI 的其余部分集成所需的支持文件。

We’re interested in the Register.cshtml page, as we want to customize the UI on the Register page, but if we look inside the code-behind page, Register.cshtml.cs, we see how much complexity the default Identity UI is hiding from us. It’s not insurmountable (we’ll customize the page handler in section 23.6), but it’s always good to avoid writing code if we can help it.
我们对 Register.cshtml 页面感兴趣,因为我们希望自定义 Register 页面上的 UI,但如果我们查看代码隐藏页面 Register.cshtml.cs,我们会看到默认身份 UI 对我们隐藏了多少复杂性。这并不是不可克服的(我们将在 Section 23.6 中自定义页面处理程序),但如果我们可以提供帮助,避免编写代码总是好的。

Now that you have the Razor template in your application, you can customize it to your heart’s content. The downside is that you’re now maintaining more code than you were with the default UI. You didn’t have to write it, but you may still have to update it when a new version of ASP.NET Core is released.
现在,您的应用程序中已有 Razor 模板,您可以根据自己的喜好对其进行自定义。缺点是,您现在维护的代码比使用默认 UI 时要多。您不必编写它,但在 ASP.NET Core 的新版本发布时,您可能仍需要更新它。

I like to use a bit of a trick when it comes to overriding the default Identity UI like this. In many cases, you don’t want to change the page handlers for the Razor Page—only the Razor view. You can achieve this by deleting the Register.cshtml.cs PageModel file, and pointing your newly scaffolded .cshtml file at the original PageModel, which is part of the default UI NuGet package.
在覆盖像这样的默认身份 UI 时,我喜欢使用一些技巧。在许多情况下,您不希望更改 Razor 页面的页面处理程序,而只需要更改 Razor 视图。为此,您可以删除Register.cshtml.cs PageModel 文件,并将新搭建的 .cshtml 文件指向原始 PageModel,该 PageModel 是默认 UI NuGet 包的一部分。

The other benefit of this approach is that you can delete some of the other files that were autoscaffolded. In total, you can make the following changes:
此方法的另一个好处是,您可以删除一些自动基架的其他文件。总的来说,您可以进行以下更改:

• Update the @model directive in Register.cshtml to point to the default UI PageModel:
更新 Register.cshtml 中的 @model 指令以指向默认 UI PageModel:

@model Microsoft.AspNetCore.Identity.UI.V5.Pages.Account.Internal.RegisterModel

• Update Areas/Identity/Pages/_ViewImports.cshtml to the following:
将 Areas/Identity/Pages/_ViewImports.cshtml 更新为以下内容:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

• Delete Areas/Identity/Pages/_ValidationScriptsPartial.cshtml.
• Delete Areas/Identity/Pages/Account/Register.cshtml.cs.
• Delete Areas/Identity/Pages/Account/_ViewImports.cshtml.

After making all these changes, you’ll have the best of both worlds: you can update the default UI Razor Pages HTML without taking on the responsibility of maintaining the default UI code-behind.
进行所有这些更改后,你将获得两全其美的效果:可以更新默认 UI Razor Pages HTML,而无需负责维护默认 UI 代码隐藏。

Tip In the source code for the book, you can see these changes in action, where the Register view has been customized to remove the references to external identity providers.
提示:在本书的源代码中,您可以看到这些实际更改,其中 Register 视图已自定义以删除对外部身份提供者的引用。

Unfortunately, it’s not always possible to use the default UI PageModel. Sometimes you need to update the page handlers, such as when you want to change the functionality of your Identity area rather than only the look and feel. A common requirement is needing to store additional information about a user, as you’ll see in the next section.
遗憾的是,并非总是可以使用默认的 UI PageModel。有时,您需要更新页面处理程序,例如,当您想要更改 Identity 区域的功能,而不仅仅是外观时。一个常见的要求是需要存储有关用户的其他信息,您将在下一节中看到。

23.6 Managing users: Adding custom data to users

23.6 管理用户:向用户添加自定义数据

In this section you’ll see how to customize the ClaimsPrincipal assigned to your users by adding claims to the AspNetUserClaims table when the user is created. You’ll also see how to access these claims in your Razor Pages and templates.
在本节中,您将了解如何通过在创建用户时向 AspNetUserClaims 表添加声明来自定义分配给用户的 ClaimsPrincipal。您还将了解如何在 Razor 页面和模板中访问这些声明。

Often, the next step after adding Identity to an application is customizing it. The default templates require only an email and password to register. What if you need more details, like a friendly name for the user? Also, I’ve mentioned that we use claims for security, so what if you want to add a claim called IsAdmin to certain users?
通常,将 Identity 添加到应用程序后的下一步是对其进行自定义。默认模板只需要电子邮件和密码即可注册。如果您需要更多详细信息,例如用户的友好名称,该怎么办?此外,我还提到过我们使用声明来实现安全性,那么,如果您想向某些用户添加一个名为 IsAdmin 的声明,该怎么办?

You know that every user principal has a collection of claims, so conceptually, adding any claim requires adding it to the user’s collection. There are two main times that you would want to grant a claim to a user:
您知道每个用户主体都有一个声明集合,因此从概念上讲,添加任何声明都需要将其添加到用户的集合中。您希望向用户授予声明的主要时间有两个:

• For every user, when they register on the app—For example, you might want to add a Name field to the Register form and add that as a claim to the user when they register.
对于每个用户,当他们在应用程序上注册时 - 例如,您可能希望将“名称”字段添加到“注册”表单中,并在用户注册时将其作为声明添加到用户。
• Manually, after the user has registered—This is common for claims used as permissions, where an existing user might want to add an IsAdmin claim to a specific user after they have registered on the app.
在用户注册后手动 - 这在用作权限的声明中很常见,其中现有用户可能希望在特定用户注册应用程序后向特定用户添加 IsAdmin 声明。

In this section I’ll show you the first approach, automatically adding new claims to a user when they’re created. The latter approach is more flexible and ultimately is the approach many apps will need, especially line-of-business apps. Luckily, there’s nothing conceptually difficult to it; it requires a simple UI that lets you view users and add a claim through the same mechanism I’ll show here.
在本节中,我将向您展示第一种方法,即在创建用户时自动向用户添加新声明。后一种方法更灵活,最终是许多应用程序需要的方法,尤其是业务线应用程序。幸运的是,它在概念上没有什么困难;它需要一个简单的 UI,允许您通过我将在此处展示的相同机制查看用户并添加声明。

Tip Another common approach is to customize the IdentityUser entity, by adding a Name property, for example. This approach is sometimes easier to work with if you want to give users the ability to edit that property. Microsoft’s “Add, download, and delete custom user data to Identity in an ASP.NET Core project” documentation describes the steps required to achieve that: http://mng.bz/aoe7.
提示:另一种常见方法是自定义 IdentityUser 实体,例如,通过添加 Name 属性。如果您想让用户能够编辑该属性,这种方法有时更容易使用。Microsoft 的“在 ASP.NET Core 项目中向 Identity 添加、下载和删除自定义用户数据”文档介绍了实现此目的所需的步骤:http://mng.bz/aoe7

Let’s say you want to add a new Claim to a user, called FullName. A typical approach would be as follows:
假设您要向名为 FullName 的用户添加新声明。典型的方法如下:

  1. Scaffold the Register.cshtml Razor Page, as you did in section 23.5.
    搭建 Register.cshtml Razor 页面的基架,就像在第 23.5 节中所做的那样。

  2. Add a Name field to the InputModel in the Register.cshtml.cs PageModel.
    将 Name 字段添加到 Register.cshtml.cs PageModel 中的 InputModel。

  3. Add a Name input field to the Register.cshtml Razor view template.
    将 Name 输入字段添加到 Register.cshtml Razor 视图模板。

  4. Create the new ApplicationUser entity as before in the OnPost() page handler by calling CreateAsync on UserManager<ApplicationUser>.
    像以前一样,通过在 UserManager 上调用 CreateAsync 在 OnPost() 页面处理程序中创建新的 UserManager<ApplicationUser>实体。

  5. Add a new Claim to the user by calling UserManager.AddClaimAsync().
    通过调用 UserManager.AddClaimAsync() 向用户添加新的声明。

  6. Continue the method as before, sending a confirmation email or signing the user in if email confirmation is not required.
    像以前一样继续该方法,发送确认电子邮件,如果不需要电子邮件确认,则让用户登录。

Steps 1–3 are fairly self-explanatory and require only updating the existing templates with the new field. Steps 4–6 take place in Register.cshtml.cs in the OnPostAsync() page handler, which is summarized in the following listing. In practice, the page handler has more error checking, boilerplate, extra features, and abstraction. I’ve simplified the code in listing 23.7 to focus on the additional lines that add the extra Claim to the ApplicationUser; you can find the full code in the sample code for this chapter.
步骤 1-3 相当不言自明,只需要使用新字段更新现有模板。步骤 4-6 在 OnPostAsync() 页面处理程序中Register.cshtml.cs进行,下面的清单对此进行了总结。在实践中,页面处理程序具有更多的错误检查、样板、额外功能和抽象。我简化了清单 23.7 中的代码,以专注于向 ApplicationUser 添加额外 Claim 的其他行;您可以在本章的示例代码中找到完整代码。

Listing 23.7 Adding a custom claim to a new user in the Register.cshtml.cs page
清单 23.7 在 Register.cshtml.cs 页面中为新用户添加自定义声明

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser {                    #A
            UserName = Input.Email, Email = Input.Email };  #A
        var result = await _userManager.CreateAsync(      #B
            user, Input.Password);                        #B
        if (result.Succeeded)
        {
            var claim = new Claim("FullName", Input.Name);   #C
            await _userManager.AddClaimAsync(user, claim);   #D
            var code = await _userManager                      #E
                .GenerateEmailConfirmationTokenAsync(user);    #E
            await _emailSender.SendEmailAsync(                 #E
                 Input.Email, "Confirm your email", code );    #E
            await _signInManager.SignInAsync(user);   #F
            return LocalRedirect(returnUrl);
        }
        foreach (var error in result.Errors)         #G
        {                                            #G
            ModelState.AddModelError(                #G
                string.Empty, error.Description);    #G
        }                                            #G
    }
       return Page();                                #G
}

❶ Creates an instance of the ApplicationUser entity
创建 ApplicationUser 实体的实例
❷ Validates that the provided password meets requirements, and creates the user in the database
验证提供的密码是否满足要求,并在数据库中创建用户
❸ Creates a claim, with a string name of “FullName” and the provided value
创建字符串名称为“FullName”且提供的值的声明
❹ Adds the new claim to the ApplicationUser’s collection
将新声明添加到 ApplicationUser 的集合
❺ Sends a confirmation email to the user, if you have configured the email sender
向用户发送确认电子邮件(如果您已配置电子邮件发件人)
❻ Signs the user in by setting the HttpContext.User; the principal will include the custom claim
通过设置 HttpContext.User 来登录用户;主体将包含自定义声明
❼ There was a problem creating the user. Adds the errors to the ModelState and redisplays the page.
创建用户时出现问题。将错误添加到 ModelState 并重新显示页面。

Tip Listing 23.7 shows how you can add extra claims at registration time, but you will often need to add more data later, such as permission-related claims or other information. You will need to create additional endpoints and pages for adding this data, securing the pages as appropriate (so that users can’t update their own permissions, for example).
提示:清单 23.7 展示了如何在注册时添加额外的声明,但您通常需要稍后添加更多数据,例如与权限相关的声明或其他信息。您需要创建其他终端节点和页面来添加此数据,并根据需要保护页面(例如,使用户无法更新自己的权限)。

This is all that’s required to add the new claim, but you’re not using it anywhere currently. What if you want to display it? Well, you’ve added a claim to the ClaimsPrincipal, which was assigned to the HttpContext.User property when you called SignInAsync. That means you can retrieve the claims anywhere you have access to the ClaimsPrincipal—including in your page handlers and in view templates. For example, you could display the user’s FullName claim anywhere in a Razor template with the following statement:
这就是添加新声明所需的全部内容,但您目前没有在任何地方使用它。如果要显示它怎么办?嗯,您已经向 ClaimsPrincipal 添加了一个声明,该声明在调用 SignInAsync 时分配给 HttpContext.User 属性。这意味着您可以在任何有权访问 ClaimsPrincipal 的位置检索声明,包括在页面处理程序和视图模板中。例如,您可以使用以下语句在 Razor 模板中的任意位置显示用户的 FullName 声明:

@User.Claims.FirstOrDefault(x=>x.Type == "FullName")?.Value

This finds the first claim on the current user principal with a Type of "FullName" and prints the assigned value (or, if the claim is not found, prints nothing). The Identity system even includes a handy extension method that tidies up this LINQ expression (found in the System.Security.Claims namespace):
这将在当前用户主体上查找 Type 为 “FullName” 的第一个声明,并打印分配的值(或者,如果未找到声明,则不打印任何内容)。Identity 系统甚至包括一个方便的扩展方法,用于整理此 LINQ 表达式(位于 System.Security.Claims 命名空间中):

@User.FindFirstValue("FullName")

With that last tidbit, we’ve reached the end of this chapter on ASP.NET Core Identity. I hope you’ve come to appreciate the amount of effort using Identity can save you, especially when you make use of the default Identity UI package.
说到最后的花絮,我们关于 ASP.NET Core Identity 的本章已经结束了。我希望您已经意识到使用 Identity 可以节省大量精力,尤其是在使用默认的 Identity UI 包时。

Adding user accounts and authentication to an app is typically the first step in customizing your app further. Once you have authentication, you can have authorization, which lets you lock down certain actions in your app, based on the current user. In the next chapter you’ll learn about the ASP.NET Core authorization system and how you can use it to customize your apps; in particular, the recipe application, which is coming along nicely!
向应用程序添加用户帐户和身份验证通常是进一步自定义应用程序的第一步。获得身份验证后,您可以获得授权,从而允许您根据当前用户锁定应用程序中的某些作。在下一章中,您将了解 ASP.NET Core 授权系统以及如何使用它来自定义您的应用程序;特别是 recipe 应用程序,它进展顺利!

23.7 Summary

23.7 总结

Authentication is the process of determining who you are, and authorization is the process of determining what you’re allowed to do. You need to authenticate users before you can apply authorization.
身份验证是确定您是谁的过程,授权是确定允许您执行哪些作的过程。您需要先对用户进行身份验证,然后才能应用授权。

Every request in ASP.NET Core is associated with a user, also known as a principal. By default, without authentication, this is an anonymous user. You can use the claims principal to behave differently depending on who made a request.
ASP.NET Core 中的每个请求都与一个用户(也称为委托人)相关联。默认情况下,如果不进行身份验证,则此用户为匿名用户。您可以使用 claims principal 根据发出请求的人员来执行不同的行为。

The current principal for a request is exposed on HttpContext.User. You can access this value from your Razor Pages and views to find out properties of the user such as their, ID, name, or email.
请求的当前主体在 HttpContext.User 上公开。可以从 Razor 页面和视图访问此值,以查找用户的属性,例如他们的 ID、名称或电子邮件。

Every user has a collection of claims. These claims are single pieces of information about the user. Claims could be properties of the physical user, such as Name and Email, or they could be related to things the user has, such as HasAdminAccess or IsVipCustomer.
每个用户都有一个声明集合。这些声明是有关用户的单个信息。声明可以是物理用户的属性,例如 Name 和 Email,也可以与用户拥有的内容相关,例如 HasAdminAccess 或 IsVipCustomer。

Legacy versions of ASP.NET used roles instead of claims. You can still use roles if you need to, but you should typically use claims where possible.
旧版本的 ASP.NET 使用角色而不是声明。如果需要,您仍然可以使用角色,但通常应尽可能使用声明。

Authentication in ASP.NET Core is provided by AuthenticationMiddleware and a number of authentication services. These services are responsible for setting the current principal when a user logs in, saving it to a cookie, and loading the principal from the cookie on subsequent requests.
ASP.NET Core 中的身份验证由 AuthenticationMiddleware 和许多身份验证服务提供。这些服务负责在用户登录时设置当前主体,将其保存到 Cookie,并在后续请求中从 Cookie 加载主体。

The AuthenticationMiddleware is added automatically by WebApplication. You can ensure that it’s inserted at a specific point in the middleware pipeline by calling UseAuthentication(). It must be placed before any middleware that requires authentication, such as UseAuthorization().
AuthenticationMiddleware 由 WebApplication 自动添加。您可以通过调用 UseAuthentication() 来确保它插入到中间件管道中的特定点。它必须放在任何需要身份验证的中间件之前,例如 UseAuthorization()。

ASP.NET Core Identity handles low-level services needed for storing users in a database, ensuring that their passwords are stored safely, and for logging users in and out. You must provide the UI for the functionality yourself and wire it up to the Identity subsystem.
ASP.NET Core Identity 处理将用户存储在数据库中、确保其密码安全存储以及登录和注销用户所需的低级服务。您必须自己提供该功能的 UI,并将其连接到 Identity 子系统。

The Microsoft.AspNetCore.Identity.UI package provides a default UI for the Identity system and includes email confirmation, MFA, and external login provider support. You need to do some additional configuration to enable these features.
Microsoft.AspNetCore.Identity.UI 包为标识系统提供默认 UI,并包括电子邮件确认、MFA 和外部登录提供程序支持。您需要进行一些额外的配置才能启用这些功能。

The default template for Web Application with Individual Account Authentication uses ASP.NET Core Identity to store users in the database with EF Core. It includes all the boilerplate code required to wire the UI up to the Identity system.
具有个人帐户身份验证的 Web 应用程序的默认模板使用 ASP.NET Core Identity 将用户存储在具有 EF Core 的数据库中。它包括将 UI 连接到标识系统所需的所有样板代码。

You can use the UserManager<T> class to create new user accounts, load them from the database, and change their passwords. SignInManager is used to sign a user in and out by assigning the principal for the request and by setting an authentication cookie. The default UI uses these classes for you, to facilitate user registration and login.
您可以使用UserManager<T>该类创建新的用户帐户,从数据库中加载它们,并更改其密码。SignInManager 用于通过为请求分配主体和设置身份验证 cookie 来登录和注销用户。默认 UI 会为您使用这些类,以方便用户注册和登录。

You can update an EF Core DbContext to support Identity by deriving from IdentityDbContext, where TUser is a class that derives from IdentityUser.
您可以通过从 IdentityDbContext 派生来更新 EF Core DbContext 以支持 Identity,其中 TUser 是从 IdentityUser 派生的类。

You can add additional claims to a user using the UserManager.AddClaimAsync(TUser user, Claim claim) method. These claims are added to the HttpContext.User object when the user logs in to your app.
您可以使用 UserManager 向用户添加其他声明。AddClaimAsync(TUser user, Claim claim) 方法。当用户登录到您的应用时,这些声明将添加到 HttpContext.User 对象中。

Claims consist of a type and a value. Both values are strings. You can use standard values for types exposed on the ClaimTypes class, such as ClaimTypes.GivenName and ClaimTypes.FirstName, or you can use a custom string, such as "FullName".
声明由类型和值组成。这两个值都是字符串。您可以对 ClaimTypes 类上公开的类型使用标准值,例如 ClaimTypes.GivenName 和 ClaimTypes.FirstName,也可以使用自定义字符串,例如“FullName”。

ASP.NET Core in Action 22 Creating custom MVC and Razor Page filters

22 Creating custom MVC and Razor Page filters
22 创建自定义 MVC 和 Razor 页面筛选器

This chapter covers
本章涵盖

• Creating custom filters to refactor complex action methods
创建自定义筛选器以重构复杂的作方法
• Using authorization filters to protect your action methods and Razor Pages
使用授权筛选器保护作方法和 Razor 页面
• Short-circuiting the filter pipeline to bypass action and page handler execution
使筛选器管道短路以绕过作和页面处理程序执行
• Injecting dependencies into filters
将依赖项注入筛选器

In chapter 21 I introduced the Model-View-Controller (MVC) and Razor Pages filter pipeline and showed where it fits into the life cycle of a request. You learned how to apply filters to your action method, controllers, and Razor Pages, and the effect of scope on the filter execution order.
在第 21 章中,我介绍了模型-视图-控制器 (MVC) 和 Razor Pages 过滤器管道,并展示了它在请求生命周期中的位置。你了解了如何将筛选器应用于作方法、控制器和 Razor Pages,以及范围对筛选器执行顺序的影响。

In this chapter you’ll take that knowledge and apply it to a concrete example. You’ll learn to create custom filters that you can use in your own apps and how to use them to reduce duplicate code in your action methods.
在本章中,您将利用这些知识并将其应用于具体示例。您将学习如何创建可在自己的应用程序中使用的自定义过滤器,以及如何使用它们来减少作方法中的重复代码。

In section 22.1 I take you through the filter types in detail, how they fit into the MVC pipeline, and what to use them for. For each one, I’ll provide example implementations that you might use in your own application and describe the built-in options available.
在 Section 22.1 中,我将向您详细介绍过滤器类型,它们如何适应 MVC 管道,以及使用它们的用途。对于每个选项,我将提供您可以在自己的应用程序中使用的示例实现,并描述可用的内置选项。

A key feature of filters is the ability to short-circuit a request by generating a response and halting progression through the filter pipeline. This is similar to the way short-circuiting works in middleware, but there are subtle differences for MVC filters. On top of that, the exact behavior is slightly different for each filter, and I cover that in section 22.2.
过滤器的一个关键功能是能够通过生成响应并停止通过过滤器管道的进程来短路请求。这类似于中间件中短路的工作方式,但 MVC 筛选器存在细微的差异。最重要的是,每个过滤器的确切行为略有不同,我在 22.2 节中介绍了这一点。

You typically add MVC filters to the pipeline by implementing them as attributes added to your controller classes, action methods, and Razor Pages. Unfortunately, you can’t easily use dependency injection (DI) with attributes due to the limitations of C#. In section 22.3 I show you how to use the ServiceFilterAttribute and TypeFilterAttribute base classes to enable DI in your filters.
通常,通过将 MVC 筛选器实现为添加到控制器类、作方法和 Razor Pages 的属性,将 MVC 筛选器添加到管道中。遗憾的是,由于 C# 的限制,你不能轻松地将依赖项注入 (DI) 与属性一起使用。在第 22.3 节中,我将向您展示如何使用 ServiceFilterAttribute 和 TypeFilterAttribute 基类在过滤器中启用 DI。

We covered all the background for filters in chapter 21, so in the next section we jump straight into the code and start creating custom MVC filters.
我们在第 21 章中介绍了过滤器的所有背景,因此在下一节中,我们将直接进入代码并开始创建自定义 MVC 过滤器。

22.1 Creating custom filters for your application

22.1 为您的应用程序创建自定义过滤器

ASP.NET Core includes several filters that you can use out of the box, but often the most useful filters are the custom ones that are specific to your own apps. In this section we’ll work through each of the six types of filters I covered in chapter 21. I’ll explain in more detail what they’re for and when you should use them. I’ll point out examples of these filters that are part of ASP.NET Core itself, and you’ll see how to create custom filters for an example application.
ASP.NET Core 包含多个开箱即用的筛选器,但通常最有用的筛选器是特定于你自己的应用程序的自定义筛选器。在本节中,我们将介绍我在第 21 章中介绍的六种类型的过滤器中的每一种。我将更详细地解释它们的用途以及何时应该使用它们。我将指出这些筛选器的示例,这些筛选器是 ASP.NET Core 本身的一部分,您将了解如何为示例应用程序创建自定义筛选器。

To give you something realistic to work with, we’ll start with a web API controller for accessing the recipe application from chapter 12. This controller contains two actions: one for fetching a RecipeDetailViewModel and another for updating a Recipe with new values. The following listing shows your starting point for this chapter, including both action methods.
为了给你一些实际的使用,我们将从一个 Web API 控制器开始,用于访问第 12 章中的配方应用程序。此控制器包含两个作:一个用于获取 RecipeDetailViewModel,另一个用于使用新值更新 Recipe。下面的清单显示了本章的起点,包括两种作方法。

Listing 22.1 Recipe web API controller before refactoring to use filters
列表 22.1 重构以使用过滤器之前的配方 Web API 控制器

[Route("api/recipe")]
public class RecipeApiController : ControllerBase
{
    private readonly bool IsEnabled = true;            #A
    public RecipeService _service; 
    public RecipeApiController(RecipeService service) 
    { 
        _service = service;
    } 

    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        if (!IsEnabled) { return BadRequest(); }   #B
        try
        {
            if (!_service.DoesRecipeExist(id))   #C
            {                                    #C
                return NotFound();               #C
            }                                    #C
            var detail = _service.GetRecipeDetail(id);    #D
            Response.GetTypedHeaders().LastModified =     #E
                detail.LastModified;                      #E
            return Ok(detail);    #F
        }
        catch (Exception ex)                #G
        {                                   #G
            return GetErrorResponse(ex);    #G
        }                                   #G
    }

    [HttpPost("{id}")]
    public IActionResult Edit(
        int id, [FromBody] UpdateRecipeCommand command)
    {
        if (!IsEnabled) { return BadRequest(); }     #H
        try
        {
            if (!ModelState.IsValid)             #I
            {                                    #I
                return BadRequest(ModelState);   #I
            }                                    #I
            if (!_service.DoesRecipeExist(id))    #J
            {                                     #J
                return NotFound();                #J
            }                                     #J
            _service.UpdateRecipe(command);    #K
            return Ok();                       #K
        }
        catch (Exception ex)               #L
        {                                  #L
            return GetErrorResponse(ex);   #L
        }                                  #L
    }

    private static IActionResult GetErrorResponse(Exception ex)
    {
        var error = new ProblemDetails         
        {
            Title = "An error occurred",         
            Detail = context.Exception.Message,
            Status = 500,                      
            Type = "https://httpstatuses.com/500"
        };                                              

        return new ObjectResult(error)
        {
            StatusCode = 500
        };
    }
}

❶ This field would be passed in as configuration and is used to control access to actions.
此字段将作为配置传入,用于控制对作的访问。
❷ If the API isn’t enabled, blocks further execution
如果未启用 API,则阻止进一步执行
❸ If the requested Recipe doesn’t exist, returns a 404 response
如果请求的配方不存在,则返回 404 响应
❹ Fetches RecipeDetailViewModel
获取 RecipeDetailViewModel
❺ Sets the Last-Modified response header to the value in the model
将 Last-Modified 响应标头设置为模型中的值
❻ Returns the view model with a 200 response
返回响应为 200 的视图模型
❼ If an exception occurs, catches it and returns the error in an expected format, as a 500 error
如果发生异常,则捕获它并以预期的格式返回错误, 作为 500 错误
❽ If the API isn’t enabled, blocks further execution
如果未启用 API,则阻止进一步执行
❾ Validates the binding model and returns a 400 response if there are errors
验证绑定模型并在出现错误时返回 400 响应
❿ If the requested Recipe doesn’t exist, returns a 404 response
如果请求的配方不存在,则返回 404 响应
⓫ Updates the Recipe from the command and returns a 200 response
从命令更新配方并返回 200 响应
⓬ If an exception occurs, catches it and returns the error in an expected format, as a 500 error
如果发生异常,则捕获它并以预期的格式返回错误, 作为 500 错误

These action methods currently have a lot of code to them, which hides the intent of each action. There’s also quite a lot of duplication between the methods, such as checking that the Recipe entity exists and formatting exceptions.
这些 action method 目前有很多代码,这隐藏了每个 action 的意图。方法之间也存在相当多的重复,例如检查 Recipe 实体是否存在和格式化异常。

In this section you’re going to refactor this controller to use filters for all the code in the methods that’s unrelated to the intent of each action. By the end of the chapter you’ll have a much simpler controller controller that’s far easier to understand, as shown here.
在本节中,您将重构此控制器,以便对方法中与每个作的意图无关的所有代码使用过滤器。在本章结束时,您将拥有一个更简单的控制器控制器,它更容易理解,如下所示。

Listing 22.2 Recipe web API controller after refactoring to use filters
列表 22.2 重构为使用过滤器后的配方 Web API 控制器

[Route("api/recipe")]
[ValidateModel]      #A
[HandleException]    #A
[FeatureEnabled(IsEnabled = true)]     #A
public class RecipeApiController : ControllerBase
{
    public RecipeService _service;
    public RecipeApiController(RecipeService service)
    {
        _service = service;
    }

    [HttpGet("{id}")]
    [EnsureRecipeExists]    #B
    [AddLastModifiedHeader]    #B
    public IActionResult Get(int id)
    {
        var detail = _service.GetRecipeDetail(id);     #C
        return Ok(detail);                             #C
    }

    [HttpPost("{id}")]
    [EnsureRecipeExists]        #D
    public IActionResult Edit(
        int id, [FromBody] UpdateRecipeCommand command)
    {
        _service.UpdateRecipe(command);    #E
        return Ok();                       #E
    }
}

❶ The filters encapsulate the majority of logic common to multiple action methods.
过滤器封装了多个方法通用的大多数逻辑。
❷ Placing filters at the action level limits them to a single action.
将过滤器放在作级别会将它们限制为单个作。
❸ The intent of the action, return a Recipe view model, is much clearer.
作的意图(返回配方视图模型)要清晰得多。
❹ Placing filters at the action level can control the order in which they execute.
将过滤器放在作级别可以控制它们的执行顺序。
❺ The intent of the action, update a Recipe, is much clearer.
作 update a Recipe 的意图要明确得多。

I think you’ll have to agree that the controller in listing 22.2 is much easier to read! In this section you’ll refactor the controller bit by bit, removing cross-cutting code to get to something more manageable. All the filters we’ll create in this section will use the sync filter interfaces. I’ll leave it to you, as an exercise, to create their async counterparts. We’ll start by looking at authorization filters and how they relate to security in ASP.NET Core.
我想你得同意,清单 22.2 中的控制器要容易得多!在本节中,您将逐步重构控制器,删除横切代码以获得更易于管理的内容。我们将在本节中创建的所有过滤器都将使用 sync 过滤器接口。作为练习,我将留给您创建它们的异步对应项。首先,我们将了解授权筛选器以及它们与 ASP.NET Core 中的安全性有何关系。

22.1.1 Authorization filters: Protecting your APIs

22.1.1 授权过滤器:保护您的 API

Authentication and authorization are related, fundamental concepts in security that we’ll be looking at in detail in chapters 23 and 24.
身份验证和授权是安全中相关的基本概念,我们将在第 23 章和第 24 章中详细介绍。

DEFINITION Authentication is concerned with determining who made a request. Authorization is concerned with what a user is allowed to access.
定义:身份验证与确定谁发出了请求有关。授权与允许用户访问的内容有关。

Authorization filters run first in the MVC filter pipeline, before any other filters. They control access to the action method by immediately short-circuiting the pipeline when a request doesn’t meet the necessary requirements.
授权筛选器首先在 MVC 筛选器管道中运行,然后再运行任何其他筛选器。当请求不满足必要的要求时,它们通过立即使管道短路来控制对作方法的访问。

ASP.NET Core has a built-in authorization framework that you should use when you need to protect your MVC application or your web APIs. You can configure this framework with custom policies that let you finely control access to your actions.
ASP.NET Core 有一个内置的授权框架,当您需要保护 MVC 应用程序或 Web API 时,您应该使用该框架。您可以使用自定义策略配置此框架,以便精细控制对作的访问。

Tip It’s possible to write your own authorization filters by implementing IAuthorizationFilter or IAsyncAuthorizationFilter, but I strongly advise against it. The ASP.NET Core authorization framework is highly configurable and should meet all your needs.
提示:通过实现 IAuthorizationFilter 或 IAsyncAuthorizationFilter 可以编写自己的授权筛选器,但我强烈建议不要这样做。ASP.NET Core 授权框架是高度可配置的,应该可以满足您的所有需求。

At the heart of MVC authorization is an authorization filter, AuthorizeFilter, which you can add to the filter pipeline by decorating your actions or controllers with the [Authorize] attribute. In its simplest form, adding the [Authorize] attribute to an action, as in the following listing, means that the request must be made by an authenticated user to be allowed to continue. If you’re not logged in, it will short-circuit the pipeline, returning a 401 Unauthorized response to the browser.
MVC 授权的核心是授权筛选器 AuthorizeFilter,您可以通过使用 [Authorize] 属性修饰作或控制器来将其添加到筛选器管道中。在最简单的形式中,将 [Authorize] 属性添加到作中,如下面的清单所示,意味着请求必须由经过身份验证的用户发出才能继续。如果您未登录,它将使管道短路,向浏览器返回 401 Unauthorized 响应。

Listing 22.3 Adding [Authorize] to an action method
清单 22.3 向作方法添加 [Authorize]

public class RecipeApiController : ControllerBase
{
    public IActionResult Get(int id)    #A
    {
        // method body
    }

    [Authorize]                  #B
    public IActionResult Edit(                             #C
        int id, [FromBody] UpdateRecipeCommand command)    #C
    {
        // method body
    }
}

❶ The Get method has no [Authorize] attribute, so it can be executed by anyone.
Get 方法没有 [Authorize] 属性,因此任何人都可以执行它。
❷ Adds the AuthorizeFilter to the filter pipeline using [Authorize]
使用 [Authorize]将 AuthorizeFilter 添加到筛选器管道
❸ The Edit method can be executed only if you’re logged in.
只有在您登录后才能执行 Edit 方法。

As with all filters, you can apply the [Authorize] attribute at the controller level to protect all the actions on a controller, to a Razor Page to protect all the page handler methods in a page, or even globally to protect every endpoint in your app.
与所有筛选器一样,可以在控制器级别应用 [Authorize] 属性以保护控制器上的所有作,将 [Authorize] 属性应用于 Razor 页面以保护页面中的所有页面处理程序方法,甚至可以全局应用以保护应用中的每个终结点。

NOTE We’ll explore authorization in detail in chapter 24, including how to add more detailed requirements so that only specific sets of users can execute an action.
注意:我们将在第 24 章中详细探讨授权,包括如何添加更详细的要求,以便只有特定的用户集才能执行作。

The next filters in the pipeline are resource filters. In the next section you’ll extract some of the common code from RecipeApiController and see how easy it is to create a short-circuiting filter.
管道中的下一个筛选器是资源筛选器。在下一节中,您将从 RecipeApiController 中提取一些通用代码,并了解创建短路过滤器是多么容易。

22.1.2 Resource filters: Short-circuiting your action methods

22.1.2 资源过滤器:使作方法短路

Resource filters are the first general-purpose filters in the MVC filter pipeline. In chapter 21 you saw minimal examples of both sync and async resource filters, which logged to the console. In your own apps, you can use resource filters for a wide range of purposes, thanks to the fact that they execute so early (and late) in the filter pipeline.
资源筛选器是 MVC 筛选器管道中的第一个通用筛选器。在第 21 章中,您看到了 sync 和 async 资源过滤器的最小示例,它们记录到控制台中。在您自己的应用程序中,您可以将资源筛选条件用于多种用途,这要归功于它们在筛选管道中执行得如此早(和延迟)的事实。

The ASP.NET Core framework includes a few implementations of resource filters you can use in your apps:
ASP.NET Core 框架包含一些可在应用中使用的资源筛选器实现:

• ConsumesAttribute—Can be used to restrict the allowed formats an action method can accept. If your action is decorated with [Consumes("application/json")], but the client sends the request as Extensible Markup Language (XML), the resource filter will short-circuit the pipeline and return a 415 Unsupported Media Type response.
ConsumesAttribute - 可用于限制作方法可以接受的允许格式。如果您的作使用 [Consumes(“application/json”)] 修饰,但客户端以可扩展标记语言 (XML) 的形式发送请求,则资源筛选器将使管道短路并返回 415 不支持的媒体类型响应。
• SkipStatusCodePagesAttribute—This filter prevents the StatusCodePagesMiddleware from running for the response. This can be useful if, for example, you have both web API controllers and Razor Pages in the same application. You can apply this attribute to the controllers to ensure that API error responses are passed untouched, but all error responses from Razor Pages are handled by the middleware.
SkipStatusCodePagesAttribute - 此筛选器可防止 StatusCodePagesMiddleware 针对响应运行。例如,如果同一应用程序中同时具有 Web API 控制器和 Razor Pages,这可能非常有用。可以将此属性应用于控制器,以确保 API 错误响应原封不动地传递,但来自 Razor Pages 的所有错误响应都由中间件处理。

Resource filters are useful when you want to ensure that the filter runs early in the pipeline, before model binding. They provide an early hook into the pipeline for your logic so you can quickly short-circuit the request if you need to.
当您想要确保筛选条件在模型绑定之前在管道中尽早运行时,资源筛选条件非常有用。它们为您的 logic 提供了管道的早期钩子,因此您可以在需要时快速短路请求。

Look back at listing 22.1 and see whether you can refactor any of the code into a resource filter. One candidate line appears at the start of both the Get and Edit methods:
回顾一下清单 22.1,看看你是否可以将任何代码重构为资源过滤器。一个候选行出现在 Get 和 Edit 方法的开头:

if (!IsEnabled) { return BadRequest(); }

This line of code is a feature toggle that you can use to disable the availability of the whole API, based on the IsEnabled field. In practice, you’d probably load the IsEnabled field from a database or configuration file so you could control the availability dynamically at runtime, but for this example I’m using a hardcoded value.
这行代码是一个功能切换,可用于根据 IsEnabled 字段禁用整个 API 的可用性。在实践中,您可能会从数据库或配置文件加载 IsEnabled 字段,以便您可以在运行时动态控制可用性,但对于此示例,我使用的是硬编码值。

Tip To read more about using feature toggles in your applications, see my series “Adding feature flags to an ASP.NET Core app” at http://mng.bz/2e40.
提示:要了解有关在应用程序中使用功能切换的更多信息,请参阅我在 http://mng.bz/2e40 上的系列文章“向 ASP.NET Core 应用程序添加功能标志”。

This piece of code is self-contained cross-cutting logic, which is somewhat orthogonal to the main intent of each action method—a perfect candidate for a filter. You want to execute the feature toggle early in the pipeline, before any other logic, so a resource filter makes sense.
这段代码是自包含的横切逻辑,它与每个作方法的主要意图在某种程度上正交,是过滤器的完美候选者。您希望在管道的早期、任何其他逻辑之前执行功能切换,因此资源筛选条件是有意义的。

Tip Technically, you could also use an authorization filter for this example, but I’m following my own advice of “Don’t write your own authorization filters!”
提示:从技术上讲,您也可以对此示例使用授权过滤器,但我遵循我自己的建议,即“不要编写自己的授权过滤器!

The next listing shows an implementation of FeatureEnabledAttribute, which extracts the logic from the action methods and moves it into the filter. I’ve also exposed the IsEnabled field as a property on the filter.
下一个清单显示了 FeatureEnabledAttribute 的实现,它从作方法中提取逻辑并将其移动到过滤器中。我还将 IsEnabled 字段作为筛选器上的属性公开。

Listing 22.4 The FeatureEnabledAttribute resource filter
列表 22.4 FeatureEnabledAttribute 资源过滤器

public class FeatureEnabledAttribute : Attribute, IResourceFilter
{
    public bool IsEnabled { get; set; }   #A
    public void OnResourceExecuting(        #B
        ResourceExecutingContext context)   #B
    {
        if (!IsEnabled)                                #C
        {                                              #C
            context.Result = new BadRequestResult();   #C
        }                                              #C
    }
    public void OnResourceExecuted(             #D
        ResourceExecutedContext context) { }    #D
}

❶ Defines whether the feature is enabled
定义是否启用该功能
❷ Executes before model binding, early in the filter pipeline
在模型绑定之前执行,在过滤器管道的早期
❸ If the feature isn’t enabled, short-circuits the pipeline by setting the context.Result property
如果未启用该功能,则通过设置上下文来短路管道。结果属性
❹ Must be implemented to satisfy IResourceFilter, but not needed in this case
必须实现以满足 IResourceFilter,但在这种情况下不需要

This simple resource filter demonstrates a few important concepts, which are applicable to most filter types:
这个简单的资源筛选条件演示了几个重要的概念,这些概念适用于大多数筛选条件类型:

• The filter is an attribute as well as a filter. This lets you decorate your controller, action methods, and Razor Pages with it using [FeatureEnabled(IsEnabled = true)].
过滤器既是属性又是过滤器。这样,您就可以使用 [FeatureEnabled(IsEnabled = true)] 使用它来装饰控制器、作方法和 Razor 页面。

• The filter interface consists of two methods: Executing, which runs before model binding, and Executed, which runs after the result has executed. You must implement both, even if you only need one for your use case.
筛选器接口由两种方法组成:Executing (在模型绑定之前运行)和 Executed (在结果执行之后运行)。您必须同时实施这两个项目,即使您的用例只需要一个项目。

• The filter execution methods provide a context object. This provides access to, among other things, the HttpContext for the request and metadata about the action method that was selected.
过滤器执行方法提供上下文对象。这提供了对请求的 HttpContext 和有关所选作方法的元数据等的访问。

• To short-circuit the pipeline, set the context.Result property to an IactionResult instance. The framework will execute this result to generate the response, bypassing any remaining filters in the pipeline and skipping the action method (or page handler) entirely. In this example, if the feature isn’t enabled, you bypass the pipeline by returning BadRequestResult, which returns a 400 error to the client.
要使管道短路,请设置上下文。Result 属性设置为 IactionResult 实例。框架将执行此结果以生成响应,绕过管道中的任何剩余筛选器,并完全跳过作方法(或页面处理程序)。在此示例中,如果未启用该功能,则通过返回 BadRequestResult 来绕过管道,这会向客户端返回 400 错误。

By moving this logic into the resource filter, you can remove it from your action methods and instead decorate the whole API controller with a simple attribute:
通过将此逻辑移动到资源过滤器中,您可以将其从 action 方法中删除,而是使用 simple 属性装饰整个 API 控制器:

[FeatureEnabled(IsEnabled = true)]
[Route("api/recipe")]
public class RecipeApiController : ControllerBase

You’ve extracted only two lines of code from your action methods so far, but you’re on the right track. In the next section we’ll move on to action filters and extract two more filters from the action method code.
到目前为止,您只从 action 方法中提取了两行代码,但您走在正确的轨道上。在下一节中,我们将继续讨论作筛选器,并从作方法代码中提取另外两个筛选器。

22.1.3 Action filters: Customizing model binding and action results

22.1.3作过滤器:自定义模型绑定和作结果

Action filters run just after model binding, before the action method executes. Thanks to this positioning, action filters can access all the arguments that will be used to execute the action method, which makes them a powerful way of extracting common logic out of your actions.
Action筛选器在模型绑定之后,在作方法执行之前运行。由于这种定位,动作过滤器可以访问将用于执行动作方法的所有参数,这使它们成为从动作中提取通用逻辑的强大方式。

On top of this, they run after the action method has executed and can completely change or replace the IActionResult returned by the action if you want. They can even handle exceptions thrown in the action.
最重要的是,它们在作方法执行后运行,并且可以根据需要完全更改或替换作返回的 IActionResult。它们甚至可以处理作中引发的异常。

NOTE Action filters don’t execute for Razor Pages. Similarly, page filters don’t execute for action methods.
注意:不会对 Razor Pages 执行作筛选器。同样,页面过滤器不会对作方法执行。

The ASP.NET Core framework includes several action filters out of the box. One of these commonly used filters is ResponseCacheFilter, which sets HTTP caching headers on your action-method responses.
ASP.NET Core 框架包含多个现成的作筛选器。其中一个常用的过滤器是 ResponseCacheFilter,它在作方法响应上设置 HTTP 缓存标头。

NOTE I have described filters as being attributes, but that’s not always the case. For example, the action filter is called ResponseCacheFilter, but this type is internal to the ASP.NET Core framework. To apply the filter, you use the public [ResponseCache] attribute instead, and the framework automatically configures the ResponseCacheFilter as appropriate. This separation between attribute and filter is largely an artifact of the internal design, but it can be useful, as you’ll see in section 22.3.
注意:我曾将过滤器描述为属性,但情况并非总是如此。例如,作筛选器称为 ResponseCacheFilter,但此类型是 ASP.NET Core 框架的内部类型。若要应用筛选器,请改用公共 [ResponseCache] 属性,框架会根据需要自动配置 ResponseCacheFilter。attribute 和 filter 之间的这种分离在很大程度上是内部设计的产物,但它可能很有用,正如您将在 Section 22.3 中看到的那样。

Response caching vs. output caching
响应缓存与输出缓存

Caching is a broad topic that aims to improve the performance of an application over the naive approach. But caching can also make debugging issues difficult and may even be undesirable in some situations. Consequently, I often apply ResponseCacheFilter to my action methods to set HTTP caching headers that disable caching! You can read about this and other approaches to caching in Microsoft’s “Response caching in ASP.NET Core” documentation at http://mng.bz/2eGd.
缓存是一个广泛的主题,旨在通过简单的方法提高应用程序的性能。但是,缓存也会使调试问题变得困难,在某些情况下甚至可能是不可取的。因此,我经常将 ResponseCacheFilter 应用于我的作方法,以设置禁用缓存的 HTTP 缓存标头!您可以在 http://mng.bz/2eGd 的 Microsoft 的“ASP.NET Core 中的响应缓存”文档中阅读有关此方法和其他缓存方法的信息。

Note that the ResponseCacheFilter applies cache control headers only to your outgoing responses; it doesn’t cache the response on the server. These headers tell the client (such as a browser) whether it can skip sending a request and reuse the response. If you have relatively static endpoints, this can massively reduce the load on your app.
请注意,ResponseCacheFilter 仅将缓存控制标头应用于您的传出响应;它不会在服务器上缓存响应。这些标头告诉客户端(例如浏览器)是否可以跳过发送请求并重用响应。如果您有相对静态的终端节点,这可以大大减少应用程序的负载。

This is different from output caching, introduced in .NET 7. Output caching involves storing a generated response on the server and reusing it for subsequent requests. In the simplest case, the response is stored in memory and reused for appropriate requests, but you can configure ASP.NET Core to store the output elsewhere, such as a database.
这与 .NET 7 中引入的输出缓存不同。输出缓存涉及将生成的响应存储在服务器上,并将其重新用于后续请求。在最简单的情况下,响应存储在内存中并重复用于适当的请求,但您可以将 ASP.NET Core 配置为将输出存储在其他位置,例如数据库。

Output caching is generally more configurable than response caching, as you can choose exactly what to cache and when to invalidate it, but it is also much more resource-heavy. For details on how to enable output caching for an endpoint, see the documentation at http://mng.bz/Bmlv.
输出缓存通常比响应缓存更易于配置,因为您可以准确选择要缓存的内容以及何时使其失效,但它的资源消耗也要大得多。有关如何为终端节点启用输出缓存的详细信息,请参阅 http://mng.bz/Bmlv 中的文档。

The real power of action filters comes when you build filters tailored to your own apps by extracting common code from your action methods. To demonstrate, I’m going to create two custom filters for RecipeApiController:
当您通过从作方法中提取通用代码来构建针对自己的应用程序量身定制的过滤器时,作过滤器的真正强大之处在于。为了演示,我将为 RecipeApiController 创建两个自定义过滤器:

• ValidateModelAttribute—This will return BadRequestResult if the model state indicates that the binding model is invalid and will short-circuit the action execution. This attribute used to be a staple of my web API applications, but the [ApiController] attribute now handles this (and more) for you. Nevertheless, I think it’s useful to understand what’s going on behind the scenes.
ValidateModelAttribute - 如果模型状态指示绑定模型无效,并且将使作执行短路,则将返回 BadRequestResult。此属性曾经是我的 Web API 应用程序的主要内容,但 [ApiController] 属性现在为您处理此 (以及更多) 。尽管如此,我认为了解幕后发生的事情是有用的。

• EnsureRecipeExistsAttribute—This uses each action method’s id argument to validate that the requested Recipe entity exists before the action method runs. If the Recipe doesn’t exist, the filter returns NotFoundResult and short-circuits the pipeline.
EnsureRecipeExistsAttribute - 在作方法运行之前,使用每个作方法的 id 参数来验证请求的配方实体是否存在。如果 Recipe 不存在,则筛选条件将返回 NotFoundResult 并使管道短路。

As you saw in chapter 16, the MVC framework automatically validates your binding models before executing your actions and Razor Page handlers, but it’s up to you to decide what to do about it. For web API controllers, it’s common to return a 400 Bad Request response containing a list of the errors, as shown in figure 22.1.
正如您在第 16 章中所看到的,MVC 框架会在执行您的作和 Razor Page 处理程序之前自动验证您的绑定模型,但由您决定如何处理它。对于 Web API 控制器,通常会返回包含错误列表的 400 Bad Request 响应,如图 22.1 所示。

alt text

Figure 22.1 Posting data to a web API using Postman. The data is bound to the action method’s binding model and validated. If validation fails, it’s common to return a 400 Bad Request response with a list of the validation errors.
图 22.1 使用 Postman 将数据发布到 Web API。数据将绑定到作方法的绑定模型并进行验证。如果验证失败,通常会返回 400 Bad Request 响应,其中包含验证错误列表。

You should ordinarily use the [ApiController] attribute on your web API controllers, which gives you this behavior (and uses Problem Details responses) automatically. But if you can’t or don’t want to use that attribute, you can create a custom action filter instead. The following listing shows a basic implementation that is similar to the behavior you get with the [ApiController] attribute.
通常应在 Web API 控制器上使用 [ApiController] 属性,该属性会自动提供此行为 (并使用问题详细信息响应) 。但是,如果您不能或不想使用该属性,则可以改为创建自定义作筛选条件。下面的列表显示了一个基本实现,它类似于您使用 [ApiController] 属性获得的行为。

Listing 22.5 The action filter for validating ModelState
列表 22.5 用于验证 ModelState 的动作过滤器

public class ValidateModelAttribute : ActionFilterAttribute      #A
{
    public override void OnActionExecuting(    #B
        ActionExecutingContext context)        #B
    {
        if (!context.ModelState.IsValid)    #C
        {
            context.Result =                                      #D
                new BadRequestObjectResult(context.ModelState);   #D
        }
    }
}

❶ For convenience, you derive from the ActionFilterAttribute base class.
为方便起见,您可以从 ActionFilterAttribute 基类派生。
❷ Overrides the Executing method to run the filter before the Action executes
重写 Executing 方法以在 Action 执行之前运行过滤器
❸ Model binding and validation have already run at this point, so you can check the state.
此时模型绑定和验证已经运行,因此您可以检查状态。
❹ If the model isn’t valid, sets the Result property, which short-circuits the action execution
如果模型无效,则设置 Result 属性,这将使作执行短路

This attribute is self-explanatory and follows a similar pattern to the resource filter in section 22.1.2, but with a few interesting points:
此属性是不言自明的,并且遵循与第 22.1.2 节中的资源过滤器类似的模式,但有一些有趣的点:

• I have derived from the abstract ActionFilterAttribute. This class implements IActionFilter and IResultFilter, as well as their async counterparts, so you can override the methods you need as appropriate. This prevents needing to add an unused OnActionExecuted() method, but using the base class is entirely optional and a matter of preference.
我从抽象的 ActionFilterAttribute 派生而来。此类实现 IActionFilter 和 IResultFilter 以及它们的异步对应项,因此您可以根据需要重写所需的方法。这样就不需要添加未使用的 OnActionExecuted() 方法,但使用基类完全是可选的,并且是首选项问题。

• Action filters run after model binding has taken place, so context.ModelState contains the validation errors if validation failed.
Action筛选器在模型绑定发生后运行,因此 context.ModelState 包含验证错误(如果验证失败)。

• Setting the Result property on context short-circuits the pipeline. But due to the position of the action filter stage, only the action method execution and later action filters are bypassed; all the other stages of the pipeline run as though the action executed as normal.
在上下文上设置 Result 属性会使管道短路。但是,由于作筛选器阶段的位置,仅绕过作方法执行和以后的作筛选器;管道的所有其他阶段都像正常执行作一样运行。

If you apply this action filter to your RecipeApiController, you can remove this code from the start of both the action methods, as it will run automatically in the filter pipeline:
如果你将此作筛选器应用于 RecipeApiController,则可以从两个作方法的开头删除此代码,因为它将在筛选器管道中自动运行:

if (!ModelState.IsValid)
{
    return BadRequest(ModelState);
}

You’ll use a similar approach to remove the duplicate code that checks whether the id provided as an argument to the action methods corresponds to an existing Recipe entity.
您将使用类似的方法来删除重复代码,该代码检查作为作方法的参数提供的 id 是否对应于现有 Recipe 实体。

The following listing shows the EnsureRecipeExistsAttribute action filter. This uses an instance of RecipeService to check whether the Recipe exists and returns a 404 Not Found if it doesn’t.
以下清单显示了 EnsureRecipeExistsAttribute作筛选器。这将使用 RecipeService 的实例来检查 Recipe 是否存在,如果不存在,则返回 404 Not Found。

Listing 22.6 An action filter to check whether a Recipe exists
清单 22.6 用于检查 Recipe 是否存在的 action filter

public class EnsureRecipeExistsAtribute : ActionFilterAttribute
{
    public override void OnActionExecuting(
        ActionExecutingContext context)
    {
        var service = context.HttpContext.RequestServices    #A
            .GetService<RecipeService>();    #A
        var recipeId = (int) context.ActionArguments["id"];  #B
        if (!service.DoesRecipeExist(recipeId))    #C
        {
            context.Result = new NotFoundResult();    #D
        }
    }
}

❶ Fetches an instance of RecipeService from the DI container
从 DI 容器中获取 RecipeService 的实例
❷ Retrieves the id parameter that will be passed to the action method when it executes
检索执行时将传递给作方法的 id 参数
❸ Checks whether a Recipe entity with the given RecipeId exists
检查具有给定 RecipeId 的 Recipe 实体是否存在
❹ If it doesn’t exist, returns a 404 Not Found result and short-circuits the pipeline
如果不存在,则返回 404 Not Found 结果并短路管道

As before, you’ve derived from ActionFilterAttribute for simplicity and overridden the OnActionExecuting method. The main functionality of the filter relies on the DoesRecipeExist() method of RecipeService, so the first step is to obtain an instance of RecipeService. The context parameter provides access to the HttpContext for the request, which in turn lets you access the DI container and use RequestServices.GetService() to return an instance of RecipeService.
与以前一样,为了简单起见,您从 ActionFilterAttribute 派生并重写了 OnActionExecuting 方法。过滤器的主要功能依赖于 RecipeService 的 DoesRecipeExist() 方法,因此第一步是获取 RecipeService 的实例。context 参数提供对请求的 HttpContext 的访问,这反过来又允许您访问 DI 容器并使用 RequestServices.GetService() 返回 RecipeService 的实例。

Warning This technique for obtaining dependencies is known as service location and is generally considered to be an antipattern. In section 22.3 I’ll show you a better way to use the DI container to inject dependencies into your filters.
警告:这种用于获取依赖项的技术称为 服务定位,通常被视为反模式。在 Section 22.3 中,我将向您展示一种更好的方法 使用 DI 容器将依赖项注入过滤器。

As well as RecipeService, the other piece of information you need is the id argument of the Get and Edit action methods. In action filters, model binding has already occurred, so the arguments that the framework will use to execute the action method are already known and are exposed on context.ActionArguments.
除了 RecipeService 之外,您还需要的另一条信息是 Get 和 Edit作方法的 id 参数。在作筛选器中,模型绑定已经发生,因此框架将用于执行作方法的参数是已知的,并在上下文中公开。ActionArguments 的 API 参数。

The action arguments are exposed as Dictionary<string, object>, so you can obtain the id parameter using the "id" string key. Remember to cast the object to the correct type.
action参数公开为 Dictionary<string, object>,因此您可以使用 “id” 字符串键获取 id 参数。请记住将对象强制转换为正确的类型。

Tip Whenever I see magic strings like this, I always try to replace them by using the nameof operator. Unfortunately, nameof won’t work for method arguments like this, so be careful when refactoring your code. I suggest explicitly applying the action filter to the action method (instead of globally, or to a controller) to remind you about that implicit coupling.
提示:每当我看到这样的魔术字符串时,我总是尝试使用 nameof 运算符替换它们。不幸的是,nameof 不适用于这样的方法参数,因此在重构代码时要小心。我建议将 action 过滤器显式地应用于 action 方法(而不是全局应用,或应用于控制器),以提醒您这种隐式耦合。

With RecipeService and id in place, it’s a case of checking whether the identifier corresponds to an existing Recipe entity and if not, setting context.Result to NotFoundResult. This short-circuits the pipeline and bypasses the action method altogether.
有了 RecipeService 和 id,就可以检查标识符是否对应于现有的 Recipe 实体,如果不是,则设置 context。Result 设置为 NotFoundResult。这会使管道短路并完全绕过 action 方法。

NOTE Remember that you can have multiple action filters running in a single stage. Short-circuiting the pipeline by setting context.Result prevents later filters in the stage from running and bypasses the action method execution.
注意:请记住,您可以在单个阶段中运行多个作筛选器。通过设置 context 使管道短路。Result 会阻止阶段中以后的过滤器运行,并绕过作方法的执行。

Before we move on, it’s worth mentioning a special case for action filters. The ControllerBase base class implements IActionFilter and IAsyncActionFilter itself. If you find yourself creating an action filter for a single controller and want to apply it to every action in that controller, you can override the appropriate methods on your controller instead, as in the following listing.
在我们继续之前,值得一提的是作过滤器的一个特殊情况。ControllerBase 基类实现 IActionFilter 和 IAsyncActionFilter 本身。如果您发现自己为单个控制器创建了一个作过滤器,并希望将其应用于该控制器中的每个作,则可以改为覆盖控制器上的相应方法,如下面的清单所示。

Listing 22.7 Overriding action filter methods directly on ControllerBase
清单 22.7 直接在 ControllerBase 上覆盖动作过滤器方法

public class HomeController : ControllerBase      #A
{
    public override void OnActionExecuting(     #B
        ActionExecutingContext context)         #B
    { }                                         #B
    public override void OnActionExecuted(    #C
        ActionExecutedContext context)        #C
    { }                                       #C
}

❶ Derives from the ControllerBase class
派生自 ControllerBase 类
❷ Runs before any other action filters for every action in the controller
在控制器中每个动作的任何其他动作过滤器之前运行
❸ Runs after all other action filters for every action in the controller
在控制器中每个动作的所有其他动作过滤器之后运行

If you override these methods on your controller, they’ll run in the action filter stage of the filter pipeline for every action on the controller. The OnActionExecuting method runs before any other action filters, regardless of ordering or scope, and the OnActionExecuted method runs after all other action filters.
如果您在控制器上覆盖这些方法,则它们将在控制器上每个作的过滤器管道的作过滤器阶段中运行。OnActionExecuting 方法在任何其他作筛选器之前运行,而不管顺序或范围如何,而 OnActionExecuted 方法在所有其他作筛选器之后运行。

Tip The controller implementation can be useful in some cases, but you can’t control the ordering related to other filters. Personally, I generally prefer to break logic into explicit, declarative filter attributes, but it depends on the situation, and as always, the choice is yours.
提示:控制器实现在某些情况下可能很有用,但您无法控制与其他过滤器相关的 Sequences。就个人而言,我通常更喜欢将 logic 分解为显式的声明性 filter 属性,但这取决于具体情况,并且一如既往,选择权在您手中。

With the resource and action filters complete, your controller is looking much tidier, but there’s one aspect in particular that would be nice to remove: the exception handling. In the next section we’ll look at how to create a custom exception filter for your controller and why you might want to do this instead of using exception handling middleware.
完成资源和作筛选器后,您的控制器看起来更加整洁,但有一个方面特别值得删除:异常处理。在下一节中,我们将了解如何为控制器创建自定义异常筛选器,以及为什么您可能希望执行此作而不是使用异常处理中间件。

22.1.4 Exception filters: Custom exception handling for your action methods

22.1.4 异常过滤器:作方法的自定义异常处理

In chapter 4 I went into some depth about types of error-handling middleware you can add to your apps. These let you catch exceptions thrown from any later middleware and handle them appropriately. If you’re using exception handling middleware, you may be wondering why we need exception filters at all.
在第 4 章中,我深入探讨了您可以添加到应用程序中的错误处理中间件的类型。这些允许您捕获任何后续中间件引发的异常并适当地处理它们。如果你正在使用异常处理中间件,你可能想知道为什么我们需要异常过滤器。

The answer to this is pretty much the same as I outlined in chapter 21: filters are great for cross-cutting concerns, when you need behavior that’s specific to MVC or that should only apply to certain routes.
这个问题的答案与我在第 21 章中概述的几乎相同:当您需要特定于 MVC 的行为或应仅适用于某些路由的行为时,过滤器非常适合横切关注点。

Both of these can apply in exception handling. Exception filters are part of the MVC framework, so they have access to the context in which the error occurred, such as the action or Razor Page that was executing. This can be useful for logging additional details when errors occur, such as the action parameters that caused the error.
这两者都可以应用于异常处理。异常筛选器是 MVC 框架的一部分,因此它们有权访问发生错误的上下文,例如正在执行的作或 Razor Page。这对于在发生错误时记录其他详细信息(如导致错误的作参数)非常有用。

Warning If you use exception filters to record action method arguments, make sure you’re not storing sensitive data in your logs, such as passwords or credit card details.
警告:如果您使用异常筛选条件来记录作方法参数,请确保您没有在日志中存储敏感数据,例如密码或信用卡详细信息。

You can also use exception filters to handle errors from different routes in different ways. Imagine you have both Razor Pages and web API controllers in your app, as we do in the recipe app. What happens when an exception is thrown by a Razor Page?
您还可以使用异常筛选条件以不同方式处理来自不同路由的错误。假设您的应用程序中同时有 Razor Pages 和 Web API 控制器,就像我们在配方应用程序中所做的那样。当 Razor Page 引发异常时会发生什么情况?

As you saw in chapter 4, the exception travels back up the middleware pipeline and is caught by exception handler middleware. The exception handler middleware reexecutes the pipeline and generates an HTML error page.
正如您在第 4 章中看到的,异常沿中间件管道向上传输,并被异常处理程序中间件捕获。异常处理程序中间件将重新执行管道并生成 HTML 错误页。

That’s great for your Razor Pages, but what about exceptions in your web API controllers? If your API throws an exception and consequently returns HTML generated by the exception handler middleware, that’s going to break a client that called the API expecting a JavaScript Object Notation (JSON) response!
这对 Razor Pages 来说非常有用,但 Web API 控制器中的异常呢?如果您的 API 引发异常,并因此返回异常处理程序中间件生成的 HTML,这将破坏调用 API 的客户端,该客户端需要 JavaScript 对象表示法 (JSON) 响应!

Tip The added complexity introduced by having to handle these two very different clients is the reason I prefer to create separate applications for APIs and server-rendered apps.
提示:必须处理这两个截然不同的客户端所带来的复杂性增加了,这就是我更喜欢为 API 和服务器呈现的应用程序创建单独的应用程序的原因。

Instead, exception filters let you handle the exception in the filter pipeline and generate an appropriate response body for API clients. The exception handler middleware intercepts only errors without a body, so it will let the modified web API response pass untouched.
相反,异常筛选条件允许您处理筛选条件管道中的异常,并为 API 客户端生成适当的响应正文。异常处理程序中间件仅拦截没有正文的错误,因此它将允许修改后的 Web API 响应原封不动地通过。

NOTE The [ApiController] attribute converts error StatusCodeResults to a ProblemDetails object, but it doesn’t catch exceptions.
注意:[ApiController] 属性将错误 StatusCodeResults 转换为 ProblemDetails 对象,但它不会捕获异常。

Exception filters can catch exceptions from more than your action methods and page handlers. They’ll run if an exception occurs at these times:
异常筛选器可以捕获来自多个作方法和页面处理程序的异常。如果在以下时间发生异常,它们将运行:

• During model binding or validation
在模型绑定或验证期间
• When the action method or page handler is executing
当作方法或页面处理程序正在执行时
• When an action filter or page filter is executing
当作过滤器或页面过滤器正在执行时

You should note that exception filters won’t catch exceptions thrown in any filters other than action and page filters, so it’s important that your resource and result filters don’t throw exceptions. Similarly, they won’t catch exceptions thrown when executing an IActionResult, such as when rendering a Razor view to HTML.
您应该注意,异常筛选器不会捕获除作和页面筛选器之外的任何筛选器中引发的异常,因此您的资源和结果筛选器不会引发异常非常重要。同样,它们不会捕获在执行 IActionResult 时引发的异常,例如在将 Razor 视图呈现为 HTML 时。

Now that you know why you might want an exception filter, go ahead and implement one for RecipeApiController, as shown next. This lets you safely remove the try-catch block from your action methods, knowing that your filter will catch any errors.
现在,您知道为什么可能需要异常过滤器,请继续为 RecipeApiController 实现一个异常过滤器,如下所示。这样,您就可以安全地从作方法中删除 try-catch 块,因为您知道过滤器将捕获任何错误。

Listing 22.8 The HandleExceptionAttribute exception filter
示例 22.8 HandleExceptionAttribute 异常过滤器

public class HandleExceptionAttribute : ExceptionFilterAttribute      #A
{
    public override void OnException(ExceptionContext context)      #B
    {
        var error = new ProblemDetails               #C
        {                                            #C
            Title = "An error occurred",             #C
            Detail = context.Exception.Message,      #C
            Status = 500,                            #C
            Type = " https://httpwg.org/specs/rfc9110.html#status.500"  #C
        };                                           #C

        context.Result = new ObjectResult(error)    #D
        {                                           #D
            StatusCode = 500                        #D
        };                                          #D
        context.ExceptionHandled = true;    #E
    }
}

❶ ExceptionFilterAttribute is an abstract base class that implements IExceptionFilter.
ExceptionFilterAttribute 是实现IExceptionFilter 的抽象基类。

❷ There’s only a single method to override for IExceptionFilter.
只有一个方法可以覆盖 IExceptionFilter。

❸ Building a problem details object to return in the response
构建要在响应中返回的问题详细信息对象

❹ Creates an ObjectResult to serialize the ProblemDetails and to set the response status code
创建一个 ObjectResult 来序列化 ProblemDetails 并设置响应状态代码

❺ Marks the exception as handled to prevent it propagating into the middleware pipeline
将异常标记为已处理,以防止其传播到中间件管道中

It’s quite common to have an exception filter in your application if you are mixing API controllers and Razor Pages in your application, but they’re not always necessary. If you can handle all the exceptions in your application with a single piece of middleware, ditch the exception filters and go with that instead.
如果在应用程序中混合使用 API 控制器和 Razor Pages,则应用程序中的异常筛选器很常见,但它们并不总是必要的。如果可以使用单个中间件处理应用程序中的所有异常,请放弃异常筛选器,改用它。

You’re almost done refactoring your RecipeApiController. You have one more filter type to add: result filters. Custom result filters tend to be relatively rare in the apps I’ve written, but they have their uses, as you’ll see.
您几乎完成了 RecipeApiController 的重构。您还需要添加一种筛选条件类型:结果筛选条件。自定义结果过滤器在我编写的应用程序中往往相对较少,但正如您将看到的,它们有其用途。

22.1.5 Result filters: Customizing action results before they execute

22.1.5 结果过滤器:在执行作结果之前自定义作结果

If everything runs successfully in the pipeline, and there’s no short-circuiting, the next stage of the pipeline after action filters is result filters. These run before and after the IActionResult returned by the action method (or action filters) is executed.
如果管道中的所有内容都成功运行,并且没有短路,则作筛选器后管道的下一阶段是结果筛选器。这些作在执行作方法(或作筛选器)返回的 IActionResult 之前和之后运行。

Warning If the pipeline is short-circuited by setting context.Result, the result filter stage won’t run, but the IActionResult will still be executed to generate the response. The exceptions to this rule are action and page filters, which only short-circuit the action execution, as you saw in chapter 21. Result filters run as normal, as though the action or page handler itself generated the response.
警告:如果通过设置 context 使 pipeline 短路。Result,则结果筛选阶段不会运行,但仍会执行 IActionResult 以生成响应。此规则的例外情况是 action 和 page 过滤器,它们只会使 action 执行短路,如第 21 章所示。结果筛选器照常运行,就像作或页面处理程序本身生成响应一样。

Result filters run immediately after action filters, so many of their use cases are similar, but you typically use result filters to customize the way the IActionResult executes. For example, ASP.NET Core has several result filters built into its framework:
结果筛选条件在作筛选条件之后立即运行,因此它们的许多使用案例相似,但您通常使用结果筛选条件来自定义 IActionResult 的执行方式。例如,ASP.NET Core 的框架中内置了多个结果筛选器:

• ProducesAttribute—This forces a web API result to be serialized to a specific output format. For example, decorating your action method with [Produces ("application/xml")] forces the formatters to try to format the response as XML, even if the client doesn’t list XML in its Accept header.
ProducesAttribute - 强制将 Web API 结果序列化为特定输出格式。例如,使用 [Produces (“application/xml”)] 修饰作方法会强制格式化程序尝试将响应格式化为 XML,即使客户端未在其 Accept 标头中列出 XML。

• FormatFilterAttribute—Decorating an action method with this filter tells the formatter to look for a route value or query string parameter called format and to use that to determine the output format. For example, you could call /api/recipe/11?format=json and FormatFilter will format the response as JSON or call api/recipe/11?format=xml and get the response as XML.
FormatFilterAttribute - 使用此过滤器修饰作方法会告知格式化程序查找名为 format 的路由值或查询字符串参数,并使用它来确定输出格式。例如,您可以调用 /api/recipe/11?format=json,FormatFilter 会将响应格式化为 JSON,或者调用 api/recipe/11?format=xml 并获取 XML 形式的响应。

NOTE Remember that you need to explicitly configure the XML formatters if you want to serialize to XML, as described in chapter 20. For details on formatting results based on the URL, see my blog entry on the topic: http://mng.bz/1rYV.
注意:请记住,如果要序列化为 XML,则需要显式配置 XML 格式化程序,如第 20 章所述。有关基于 URL 设置结果格式的详细信息,请参阅我关于主题 http://mng.bz/1rYV 的博客文章。

As well as controlling the output formatters, you can use result filters to make any last-minute adjustments before IActionResult is executed and the response is generated.
除了控制输出格式化程序外,您还可以使用结果筛选器在执行 IActionResult 并生成响应之前进行任何最后一刻的调整。

As an example of the kind of flexibility available, in the following listing I demonstrate setting the LastModified header, based on the object returned from the action. This is a somewhat contrived example—it’s specific enough to a single action that it likely doesn’t warrant being moved to a result filter—but I hope you get the idea.
作为可用灵活性类型的一个示例,在下面的清单中,我演示了如何根据从作返回的对象设置 LastModified 标头。这是一个有点人为的示例 — 它对单个作足够具体,以至于它不一定需要移动到结果筛选器 — 但我希望您能理解。

Listing 22.9 Setting a response header in a result filter
清单 22.9 在结果过滤器中设置响应头

public class AddLastModifedHeaderAttribute : ResultFilterAttribute    #A
{
    public override void OnResultExecuting(     #B
        ResultExecutingContext context)         #B
    {
        if (context.Result is OkObjectResult result          #C
            && result.Value is RecipeDetailViewModel detail)    #D
        {
            var viewModelDate = detail.LastModified;            #E
            context.HttpContext.Response                        #E
              .GetTypedHeaders().LastModified = viewModelDate;  #E
        }
    }
}

❶ ResultFilterAttribute provides a useful base class you can override.
ResultFilterAttribute 提供了一个可以重写的有用基类。
❷ You could also override the Executed method, but the response would already be sent by then.
你也可以重写 Executed 方法,但那时响应已经发送了。
❸ Checks whether the action result returned a 200 Ok result with a view model.
检查作结果是否返回了视图模型的 200 Ok 结果。
❹ Checks whether the view model type is RecipeDetailViewModel . . .
检查视图模型类型是否为 RecipeDetailViewModel . . .
❺ . . . and if it is, fetches the LastModified property and sets the Last-Modified header in the response
. . . .如果是,则获取 LastModified 属性并在响应中设置 Last-Modified 标头

I’ve used another helper base class here, ResultFilterAttribute, so you need to override only a single method to implement the filter. Fetch the current IActionResult, exposed on context.Result, and check that it’s an OkObjectResult instance with a RecipeDetailViewModel value. If it is, fetch the LastModified field from the view model and add a Last-Modified header to the response.
我在这里使用了另一个帮助程序基类 ResultFilterAttribute,因此您只需重写一个方法即可实现筛选器。提取在上下文中公开的当前 IActionResult。Result,并检查它是否是具有 RecipeDetailViewModel 值的 OkObjectResult 实例。如果是,请从视图模型中提取 LastModified 字段,并将 Last-Modified 标头添加到响应中。

Tip GetTypedHeaders() is an extension method that provides strongly typed access to request and response headers. It takes care of parsing and formatting the values for you. You can find it in the Microsoft.AspNetCore.Http namespace.
提示GetTypedHeaders() 是一种扩展方法,它提供对请求和响应标头的强类型访问。它负责为您解析和格式化值。您可以在 Microsoft.AspNetCore.Http 命名空间中找到它。

As with resource and action filters, result filters can implement a method that runs after the result has executed: OnResultExecuted. You can use this method, for example, to inspect exceptions that happened during the execution of IActionResult.
与资源和作筛选器一样,结果筛选器可以实现在结果执行后运行的方法:OnResultExecuted。例如,您可以使用此方法检查在执行 IActionResult 期间发生的异常。

Warning Generally, you can’t modify the response in the OnResultExecuted method, as you may have already started streaming the response to the client.
警告:通常,您无法在 OnResultExecuted 方法中修改响应,因为您可能已经开始将响应流式传输到客户端。

We’ve finished simplifying the RecipeApiController now. By extracting various pieces of functionality to filters, the original controller in listing 22.1 has been simplified to the version in listing 22.2. This is obviously a somewhat extreme and contrived demonstration, and I’m not advocating that filters should always be your go-to option.
我们现在已经完成了 RecipeApiController 的简化。通过将各种功能提取到过滤器中,清单 22.1 中的原始控制器已简化为清单 22.2 中的版本。这显然是一个有点极端和做作的演示,我并不是提倡过滤器应该始终是您的首选。

Tip Filters should be a last resort in most cases. Where possible, it is often preferable to use a simple private method in a controller, or to push functionality into the domain instead of using filters. Filters should generally be used to extract repetitive, HTTP-related, or common cross-cutting code from your controllers.
提示:在大多数情况下,过滤器应该是最后的手段。在可能的情况下,通常最好在控制器中使用简单的私有方法,或者将功能推送到域中而不是使用过滤器。过滤器通常用于从控制器中提取重复的、与 HTTP 相关的或常见的横切代码。

There’s still one more filter we haven’t looked at yet, because it applies only to Razor Pages: page filters.
还有一个筛选器我们还没有查看,因为它仅适用于 Razor Pages:页面筛选器。

22.1.6 Page filters: Customizing model binding for Razor Pages

22.1.6 页面筛选器:自定义 Razor 页面的模型绑定

As already discussed, action filters apply only to controllers and actions; they have no effect on Razor Pages. Similarly, page filters have no effect on controllers and actions. Nevertheless, page filters and action filters fulfill similar roles.
如前所述,作筛选器仅适用于控制器和作;它们对 Razor Pages 没有影响。同样,页面过滤器对控制器和作也没有影响。不过,页面过滤器和作过滤器的作用相似。

As is the case for action filters, the ASP.NET Core framework includes several page filters out of the box. One of these is the Razor Page equivalent of the caching action filter, ResponseCacheFilter, called PageResponseCacheFilter. This works identically to the action-filter equivalent I described in section 22.1.3, setting HTTP caching headers on your Razor Page responses.
与作筛选器一样,ASP.NET Core 框架包含多个现成的页面筛选器。其中一个是缓存作筛选器的 Razor Page 等效项 ResponseCacheFilter,称为 PageResponseCacheFilter。这与我在第 22.1.3 节 在 Razor Page 响应上设置 HTTP 缓存标头中描述的等效作过滤器的工作原理相同。

Page filters are somewhat unusual, as they implement three methods, as discussed in section 22.1.2. In practice, I’ve rarely seen a page filter that implements all three. It’s unusual to need to run code immediately after page handler selection and before model validation. It’s far more common to perform a role directly analogous to action filters. The following listing shows a page filter equivalent to the EnsureRecipeExistsAttribute action filter.
页面过滤器有些不寻常,因为它们实现了三种方法,如 Section 22.1.2 中所述。在实践中,我很少见过实现所有这三个的页面过滤器。在选择页面处理程序之后和模型验证之前需要立即运行代码是不常见的。执行直接类似于作筛选器的角色更为常见。以下清单显示了与 EnsureRecipeExistsAttribute作筛选器等效的页面筛选器。

Listing 22.10 A page filter to check whether a Recipe exists
清单 22.10 用于检查 Recipe 是否存在的页面过滤器

public class PageEnsureRecipeExistsAttribute : Attribute, IPageFilter  #A
{
    public void OnPageHandlerSelected(          #B
        PageHandlerSelectedContext context)     #B
    {}                                          #B

    public void OnPageHandlerExecuting(         #C
        PageHandlerExecutingContext context)    #C
    {
        var service = context.HttpContext.RequestServices        #D
            .GetService<RecipeService>();  #D
        var recipeId = (int) context.HandlerArguments["id"];     #E
        if (!service.DoesRecipeExist(recipeId))        #F
        {
            context.Result = new NotFoundResult();   #G
        }
    }

    public void OnPageHandlerExecuted(        #H
        PageHandlerExecutedContext context)   #H
    { }                                       #H
}

❶ Implements IPageFilter and as an attribute so you can decorate the Razor Page PageModel
实现 IPageFilter 并作为属性,以便您可以装饰 Razor Page PageModel

❷ Executed after handler selection and before model binding—not used in this example
在处理程序选择之后和模型绑定之前执行 - 本例中未使用

❸ Executed after model binding and validation, and before page handler execution
在模型绑定和验证之后以及页面处理程序执行之前执行

❹ Fetches an instance of RecipeService from the DI container
从 DI 容器中获取 RecipeService 的实例

❺ Retrieves the id parameter that will be passed to the page handler method when it executes
检索 id 参数,该参数将在执行时传递给页面处理程序方法

❻ Checks whether a Recipe entity with the given RecipeId exists . . .
检查是否存在具有给定 RecipeId 的 Recipe 实体 . . .

❼ . . . and if it doesn’t exist, returns a 404 Not Found result and short-circuits the pipeline
. . . .如果不存在,则返回 404 Not Found 结果,并在页面处理程序执行(或短路)后将管道

❽ Executed after page handler execution (or short-circuiting)—not used in this example
Executed 短路 — 本例中未使用

The page filter is similar to the action filter equivalent. The most obvious difference is the need to implement three methods to satisfy the IPageFilter interface. You’ll commonly want to implement the OnPageHandlerExecuting method, which runs after model binding and validation, and before the page handler executes.
页面过滤器类似于等效的动作过滤器。最明显的区别是需要实现三种方法来满足 IPageFilter 接口。您通常需要实现 OnPageHandlerExecuting 方法,该方法在模型绑定和验证之后、页面处理程序执行之前运行。

A subtle difference between the action filter code and the page filter code is that the action filter accesses the model-bound action arguments using context.ActionArguments. The page filter uses context.HandlerArguments in the example, but there’s also another option.
作筛选条件代码和页面筛选条件代码之间的细微差别是,作筛选条件使用上下文访问模型绑定的作参数。ActionArguments 的 API 参数。页面过滤器使用 context。HandlerArguments 的 HandlerArguments 进行匹配,但还有另一个选项。

Remember from chapter 16 that Razor Pages often bind to public properties on the PageModel using the [BindProperty] attribute. You can access those properties directly instead of using magic strings by casting a HandlerInstance property to the correct PageModel type and accessing the property directly, as in this example:
请记住,在第 16 章中,Razor Pages 通常使用 [BindProperty] 属性绑定到 PageModel 上的公共属性。你可以通过将 HandlerInstance 属性强制转换为正确的 PageModel 类型并直接访问该属性,直接访问这些属性,而不是使用魔术字符串,如下例所示:

var recipeId = ((ViewRecipePageModel)context.HandlerInstance).Id

This is similar to the way the ControllerBase class implements IActionFilter and PageModel implements IPageFilter and IAsyncPageFilterT. If you want to create an action filter for a single Razor Page, you could save yourself the trouble of creating a separate page filter and override these methods directly in your Razor Page.
这类似于 ControllerBase 类实现 IActionFilter 和 PageModel 实现 IPageFilter 和 IAsyncPageFilterT 的方式。如果要为单个 Razor 页面创建作筛选器,则可以省去创建单独页面筛选器的麻烦,并直接在 Razor 页面中重写这些方法。

Tip I generally find it’s not worth the hassle of using page filters unless you have a common requirement. The extra level of indirection that page filters add, coupled with the typically bespoke nature of individual Razor Pages, means that I normally find they aren’t worth using. Your mileage may vary, of course, but don’t jump to them as a first option.
提示:我通常发现不值得使用页面过滤器的麻烦,除非你有共同的要求。页面过滤器添加的额外间接级别,再加上单个 Razor 页面的典型定制性质,意味着我通常会发现它们不值得使用。当然,您的里程可能会有所不同,但不要将它们作为首选。

That brings us to the end of this detailed look at each of the filters in the MVC pipeline. Looking back and comparing listings 22.1 and 22.2, you can see filters allowed us to refactor the controllers and make the intent of each action method much clearer. Writing your code in this way makes it easier to reason about, as each filter and action has a single responsibility.
这样,我们就结束了对 MVC 管道中每个过滤器的详细介绍。回顾并比较清单 22.1 和 22.2,您可以看到过滤器允许我们重构控制器,并使每个作方法的意图更加清晰。以这种方式编写代码可以更轻松地进行推理,因为每个过滤器和作都有单一的责任。

In the next section we’ll take a slight detour into exactly what happens when you short-circuit a filter. I’ve described how to do this, by setting the context.Result property on a filter, but I haven’t described exactly what happens. For example, what if there are multiple filters in the stage when it’s short-circuited? Do those still run?
在下一节中,我们将稍微绕道介绍一下当 filter 短路时会发生什么。我已经介绍了如何通过设置上下文来执行此作。Result 属性,但我还没有具体描述会发生什么。例如,如果 stage 中有多个 filter 短路怎么办?那些还在运行吗?

22.2 Understanding pipeline short-circuiting

22.2 了解管道短路

In this short section you’ll learn about the details of filter-pipeline short-circuiting. You’ll see what happens to the other filters in a stage when the pipeline is short-circuited and how to short-circuit each type of filter.
在这个简短的部分中,您将了解 filter-pipeline 短路的详细信息。您将看到当管道短路时,某个阶段中的其他过滤器会发生什么情况,以及如何使每种类型的过滤器短路。

A brief warning: the topic of filter short-circuiting can be a little confusing. Unlike middleware short-circuiting, which is cut-and-dried, the filter pipeline is a bit more nuanced. Luckily, you won’t often need to dig into it, but when you do, you’ll be glad for the detail.
一个简短的警告:滤波器短路的话题可能有点令人困惑。与中间件短路不同,筛选器管道更加微妙。幸运的是,您通常不需要深入研究它,但当您这样做时,您会为细节感到高兴。

You short-circuit the authorization, resource, action, page, and result filters by setting context.Result to IActionResult. Setting an action result in this way causes some or all of the remaining pipeline to be bypassed. But the filter pipeline isn’t entirely linear, as you saw in chapter 21, so short-circuiting doesn’t always do an about-face back down the pipeline. For example, short-circuited action filters bypass only action method execution; the result filters and result execution stages still run.
您可以通过设置 context 来短路 authorization、resource、action、page 和 result 过滤器。Result 设置为 IActionResult。以这种方式设置作结果会导致绕过部分或全部剩余管道。但是 filter pipeline 并不是完全线性的,正如你在第 21 章中看到的那样,所以短路并不总是在管道上做一个反转。例如,短路的动作过滤器仅绕过动作方法的执行;结果筛选条件和结果执行阶段仍在运行。

The other difficultly is what happens if you have more than one filter in a stage. Let’s say you have three resource filters executing in a pipeline. What happens if the second filter causes a short circuit? Any remaining filters are bypassed, but the first resource filter has already run its Executing command, as shown in figure 22.2. This earlier filter gets to run its Executed command too, with context.Cancelled = true, indicating that a filter in that stage (the resource filter stage) short-circuited the pipeline.
另一个困难是在一个阶段中有多个过滤器时会发生什么。假设您在一个管道中执行了三个资源筛选器。如果第二个滤波器导致短路怎么办?任何剩余的过滤器都将被绕过,但第一个资源过滤器已经运行了其 Executing 命令,如图 22.2 所示。这个前面的过滤器也可以运行它的 Executed 命令,其中包含 context。Cancelled = true,指示该阶段(资源筛选器阶段)中的筛选器使管道短路。

alt text

Figure 22.2 The effect of short-circuiting a resource filter on other resource filters in that stage. Later filters in the stage won’t run at all, but earlier filters run their OnResourceExecuted function.
图 22.2 在该阶段中,将资源过滤器短路对其他资源过滤器的影响。阶段中较晚的筛选器根本不会运行,但较早的筛选器会运行其 OnResourceExecuted 函数。

Running result filters after short-circuits with IAlwaysRunResultFilter
使用 IAlwaysRunResultFilter

Result filters are designed to wrap the execution of an IActionResult returned by an action method or action filter so that you can customize how the action result is executed. However, this customization doesn’t apply to the IActionResult set when you short-circuit the filter pipeline by setting context.Result in an authorization filter, resource filter, or exception filter.
在短路后运行结果筛选器 结果筛选器旨在包装作方法或作筛选器返回的 IActionResult 的执行,以便您可以自定义作结果的执行方式。但是,当您通过设置 context 使筛选器管道短路时,此自定义不适用于 IActionResult 集。生成授权筛选条件、资源筛选条件或异常筛选条件。

That’s often not a problem, as many result filters are designed to handle “happy path” transformations. But sometimes you want to make sure that a transformation is always applied to an IActionResult, regardless of whether it was returned by an action method or a short-circuiting filter.
这通常不是问题,因为许多结果筛选器都旨在处理 “happy path” 转换。但有时你希望确保转换始终应用于 IActionResult,而不管它是由作方法还是短路筛选器返回的。

For those cases, you can implement IAlwaysRunResultFilter or IAsyncAlwaysRunResultFilter. These interfaces extend (and are identical) to the standard result filter interfaces, so they run like normal result filters in the filter pipeline. But these interfaces mark the filter to also run after an authorization filter, resource filter, or exception filter short-circuits the pipeline, where standard result filters won’t run.
对于这些情况,您可以实现 IAlwaysRunResultFilter 或 IAsyncAlwaysRunResultFilter。这些接口扩展(并且相同)到标准结果筛选器接口,因此它们像筛选器管道中的普通结果筛选器一样运行。但是,这些接口将筛选器标记为在授权筛选器、资源筛选器或异常筛选器使管道短路后也运行,其中标准结果筛选器不会运行。

You can use IAlwaysRunResultFilter to ensure that certain action results are always updated. For example, the documentation shows how to use an IAlwaysRunResultFilter to convert a 415 StatusCodeResult to a 422 StatusCodeResult, regardless of the source of the action result. See the “IAlwaysRunResultFilter and IAsyncAlwaysRunResultFilter” section of Microsoft’s “Filters in ASP.NET Core” documentation: http://mng.bz/JDo0.
您可以使用 IAlwaysRunResultFilter 来确保某些作结果始终更新。例如,该文档显示了如何使用 IAlwaysRunResultFilter 将 415 StatusCodeResult 转换为 422 StatusCodeResult,而不管作结果的来源如何。请参阅 Microsoft 的“ASP.NET Core 中的筛选器”文档的“IAlwaysRunResultFilter 和 IAsyncAlwaysRunResultFilter”部分:http://mng.bz/JDo0

Understanding which other filters run when you short-circuit a filter can be somewhat of a chore, but I’ve summarized each filter in table 22.1. You’ll also find it useful to refer to the pipeline diagrams in chapter 21 to visualize the shape of the pipeline when thinking about short circuits.
了解在使 filter 短路时运行哪些其他 filters 可能有点麻烦,但我在表 22.1 中总结了每个 filter 。在考虑短路时,您还会发现参考第 21 章中的流水线图以可视化流水线的形状很有用。

Table 22.1 The effect of short-circuiting filters on filter-pipeline execution
表 22.1 短路 filters 对 filter-pipeline 执行的影响

Filter type How to short-circuit? What else runs?
Authorization filters Set context.Result. Runs only IAlwaysRunResultFilters.
Resource filters Set context.Result. Resource-filter *Executed functions from earlier filters run with context.Cancelled = true. Runs IAlwaysRunResultFilters before executing the IActionResult.
Action filters Set context.Result. Bypasses only action method execution. Action filters earlier in the pipeline run their Executed methods with context.Cancelled = true, then result filters, result execution, and resource filters’ Executed methods all run as normal.
Page filters Set context.Result in OnPageHandlerSelected. Bypasses only page handler execution. Page filters earlier in the pipeline run their Executed methods with context.Cancelled = true, then result filters, result execution, and resource filters’ Executed methods all run as normal.
Exception filters Set context.Result and Exception.Handled = true. All resource-filter *Executed functions run. Runs IAlwaysRunResultFilters before executing the IActionResult.
Result filters Set context.Cancelled = true. Result filters earlier in the pipeline run their Executed functions with context.Cancelled = true. All resource-filter Executed functions run as normal.

The most interesting point here is that short-circuiting an action filter (or a page filter) doesn’t short-circuit much of the pipeline at all. In fact, it bypasses only later action filters and the action method execution itself. By building primarily action filters, you can ensure that other filters, such as result filters that define the output format, run as usual, even when your action filters short-circuit.
这里最有趣的一点是,短路作筛选器(或页面筛选器)根本不会使管道的大部分短路。事实上,它只绕过后面的作筛选器和作方法执行本身。通过主要构建作筛选器,您可以确保其他筛选器(例如定义输出格式的结果筛选器)照常运行,即使作筛选器短路时也是如此。

The last thing I’d like to talk about in this chapter is how to use DI with your filters. You saw in chapters 8 and 9 that DI is integral to ASP.NET Core, and in the next section you’ll see how to design your filters so that the framework can inject service dependencies into them for you.
本章我想谈的最后一件事是如何将 DI 与你的过滤器一起使用。您在第 8 章和第 9 章中看到了 DI 是 ASP.NET Core 不可或缺的一部分,在下一节中,您将了解如何设计过滤器,以便框架可以为您注入服务依赖项。

22.3 Using dependency injection with filter attributes

22.3 将依赖项注入与 filter 属性一起使用

In this section you’ll learn how to inject services into your filters so you can take advantage of the simplicity of DI in your filters. You’ll learn to use two helper filters to achieve this, TypeFilterAttribute and ServiceFilterAttribute, and you’ll see how they can be used to simplify the action filter you defined in section 22.1.3.
在本节中,您将学习如何将服务注入过滤器,以便您可以在过滤器中利用 DI 的简单性。您将学习使用两个辅助过滤器来实现此目的,TypeFilterAttribute和ServiceFilterAttribute,并且您将了解如何使用它们来简化您在第 22.1.3 节中定义的作过滤器。

The filters we’ve created so far have been created as attributes. This is useful for applying filters to action methods and controllers, but it means you can’t use DI to inject services into the constructor. C# attributes don’t let you pass dependencies into their constructors (other than constant values), and they’re created as singletons, so there’s only a single instance of an attribute for the lifetime of your app. So what happens if you need to access a transient or scoped service from inside the singleton attribute?
到目前为止,我们创建的过滤器已创建为 attributes。这对于将筛选器应用于作方法和控制器非常有用,但这意味着您不能使用 DI 将服务注入构造函数。C# 属性不允许将依赖项传递到其构造函数中(常量值除外),并且它们被创建为单一实例,因此在应用的生命周期内只有一个属性实例。那么,如果您需要从 singleton 属性内部访问临时或范围服务,会发生什么情况呢?

Listing 22.6 showed one way of doing this, using a pseudo-service locator pattern to reach into the DI container and pluck out RecipeService at runtime. This works but is generally frowned upon as a pattern in favor of proper DI. So how can you add DI to your filters?
清单 22.6 展示了一种实现此目的的方法,使用伪服务定位器模式进入 DI 容器并在运行时提取 RecipeService。这有效,但通常不被看作是一种支持适当 DI 的模式。那么如何将 DI 添加到过滤器中呢?

The key is to split the filter in two. Instead of creating a class that’s both an attribute and a filter, create a filter class that contains the functionality and an attribute that tells the framework when and where to use the filter.
关键是将过滤器一分为二。不要创建一个既是属性又是筛选器的类,而应创建一个包含功能和属性的筛选器类,该类告诉框架何时何地使用筛选器。

Let’s apply this to the action filter from listing 22.6. Previously, I derived from ActionFilterAttribute and obtained an instance of RecipeService from the context passed to the method. In the following listing I show two classes, EnsureRecipeExistsFilter and EnsureRecipeExistsAttribute. The filter class is responsible for the functionality and takes in RecipeService as a constructor dependency.
让我们将其应用于清单 22.6 中的 action filter。以前,我从 ActionFilterAttribute 派生,并从传递给该方法的上下文中获取 RecipeService 的实例。在下面的清单中,我显示了两个类,EnsureRecipeExistsFilter 和 EnsureRecipeExistsAttribute。filter 类负责功能,并将 RecipeService 作为构造函数依赖项。

Listing 22.11 Using DI in a filter by not deriving from Attribute
清单 22.11 在过滤器中使用 DI 而不是从 Attribute 派生

public class EnsureRecipeExistsFilter : IActionFilter    #A
{
    private readonly RecipeService _service;                #B
    public EnsureRecipeExistsFilter(RecipeService service)  #B
    {                                                       #B
        _service = service;                                 #B
    }                                                       #B
    public void OnActionExecuting(ActionExecutingContext context)  #C
    {                                                              #C
        var recipeId = (int) context.ActionArguments["id"];        #C
        if (!_service.DoesRecipeExist(recipeId))                   #C
        {                                                          #C
            context.Result = new NotFoundResult();                 #C
        }                                                          #C
    }                                                              #C

    public void OnActionExecuted(ActionExecutedContext context) { }   #D
}

public class EnsureRecipeExistsAttribute : TypeFilterAttribute    #E
{
    public EnsureRecipeExistsAttribute()               #F
        : base(typeof(EnsureRecipeExistsFilter)) {}    #F
}

❶ Doesn’t derive from an Attribute class
不从 Attribute 类派生
❷ RecipeService is injected into the constructor.
RecipeService 被注入到构造函数中。
❸ The rest of the method remains the same.
方法的其余部分保持不变。
❹ You must implement the Executed action to satisfy the interface.
您必须实现 Executed作才能满足接口。
❺ Derives from TypeFilter, which is used to fill dependencies using the DI container
派生自 TypeFilter,用于使用 DI 容器填充依赖项
❻ Passes the type EnsureRecipeExistsFilter as an argument to the base TypeFilter constructor
将类型 EnsureRecipeExistsFilter 作为参数传递给基本 TypeFilter 构造函数

EnsureRecipeExistsFilter is a valid filter; you could use it on its own by adding it as a global filter (as global filters don’t need to be attributes). But you can’t use it directly by decorating controller classes and action methods, as it’s not an attribute. That’s where EnsureRecipeExistsAttribute comes in.
EnsureRecipeExistsFilter 是有效的筛选器;您可以通过将其添加为全局过滤器来单独使用它(因为全局过滤器不需要是属性)。但是你不能通过装饰控制器类和作方法来直接使用它,因为它不是一个属性。这就是 EnsureRecipeExistsAttribute 的用武之地。

You can decorate your methods with EnsureRecipeExistsAttribute instead. This attribute inherits from TypeFilterAttribute and passes the Type of filter to create as an argument to the base constructor. This attribute acts as a factory for EnsureRecipeExistsFilter by implementing IFilterFactory.
您可以改用 EnsureRecipeExistsAttribute 来修饰您的方法。此属性继承自 TypeFilterAttribute,并将要创建的过滤器的 Type 作为参数传递给基本构造函数。此属性通过实现 IFilterFactory 充当 EnsureRecipeExistsFilter 的工厂。

When ASP.NET Core initially loads your app, it scans your actions and controllers, looking for filters and filter factories. It uses these to form a filter pipeline for every action in your app, as shown in figure 22.3.
当 ASP.NET Core 最初加载您的应用程序时,它会扫描您的作和控制器,查找过滤器和过滤器工厂。它使用这些数据为应用程序中的每个作形成一个 filter pipeline ,如图 22.3 所示。

alt text

Figure 22.3 The framework scans your app on startup to find both filters and attributes that implement IFilterFactory. At runtime, the framework calls CreateInstance() to get an instance of the filter
图 22.3 框架在启动时扫描您的应用程序,以查找实现 IFilterFactory 的过滤器和属性。在运行时,框架调用 CreateInstance() 来获取过滤器的实例

When an action decorated with EnsureRecipeExistsAttribute is called, the framework calls CreateInstance() on the IFilterFactory attribute. This creates a new instance of EnsureRecipeExistsFilter and uses the DI container to populate its dependencies (RecipeService).
调用使用 EnsureRecipeExistsAttribute 修饰的作时,框架将对 IFilterFactory 属性调用 CreateInstance()。这将创建一个新的 EnsureRecipeExistsFilter 实例,并使用 DI 容器填充其依赖项 (RecipeService)。

By using this IFilterFactory approach, you get the best of both worlds: you can decorate your controllers and actions with attributes, and you can use DI in your filters. Out of the box, two similar classes provide this functionality, which have slightly different behaviors:
通过使用这种 IFilterFactory 方法,您可以两全其美:您可以使用属性装饰控制器和作,并且可以在过滤器中使用 DI。开箱即用,两个类似的类提供了此功能,它们的行为略有不同:

• TypeFilterAttribute—Loads all the filter’s dependencies from the DI container and uses them to create a new instance of the filter.
TypeFilterAttribute - 从 DI 容器中加载所有筛选器的依赖项,并使用它们创建筛选器的新实例。

• ServiceFilterAttribute—Loads the filter itself from the DI container. The DI container takes care of the service lifetime and building the dependency graph. Unfortunately, you must also explicitly register your filter with the DI container:
ServiceFilterAttribute - 从 DI 容器加载筛选器本身。DI 容器负责服务生命周期并构建依赖项关系图。遗憾的是,您还必须向 DI 容器显式注册过滤器:

builder.Services.AddTransient<EnsureRecipeExistsFilter>();

Tip You can register your services with any lifetime you choose. If your service is registered as a singleton, you can consider setting the IsReusable flag, as described in the documentation: http://mng.bz/d1JD.
提示您可以使用您选择的任何生命周期来注册您的服务。如果您的服务注册为单一实例,则可以考虑设置 IsReusable 标志,如文档中所述:http://mng.bz/d1JD

If you choose to use ServiceFilterAttribute instead of TypeFilterAttribute, and register the EnsureRecipeExistsFilter as a service in the DI container, you can apply the ServiceFilterAttribute directly to an action method:
如果您选择使用 ServiceFilterAttribute 而不是 TypeFilterAttribute,并在 DI 容器中将 EnsureRecipeExistsFilter 注册为服务,则可以将 ServiceFilterAttribute 直接应用于作方法:

[ServiceFilter(typeof(EnsureRecipeExistsFilter))]
public IActionResult Index() => Ok();

Whether you choose to use TypeFilterAttribute or ServiceFilterAttribute is somewhat a matter of preference, and you can always implement a custom IFilterFactory if you need to. The key takeaway is that you can now use DI in your filters. If you don’t need to use DI for a filter, implement it as an attribute directly, for simplicity.
选择使用 TypeFilterAttribute 还是 ServiceFilterAttribute 在某种程度上是一个首选项问题,如果需要,您始终可以实现自定义 IFilterFactory。关键要点是您现在可以在过滤器中使用 DI。如果您不需要将 DI 用于过滤器,请直接将其作为属性实现,以便简单起见。

Tip I like to create my filters as a nested class of the attribute class when using this pattern. This keeps all the code nicely contained in a single file and indicates the relationship between the classes.
提示:使用此模式时,我喜欢将过滤器创建为 attribute 类的嵌套类。这将使所有代码很好地包含在单个文件中,并指示类之间的关系。

That brings us to the end of this chapter on the filter pipeline. Filters are a somewhat advanced topic, in that they aren’t strictly necessary for building basic apps, but I find them extremely useful for ensuring that my controller and action methods are simple and easy to understand.
这将我们带到了本章关于过滤器管道的结尾。筛选器是一个比较高级的主题,因为它们对于构建基本应用程序并不是绝对必要的,但我发现它们对于确保我的控制器和作方法简单易懂非常有用。

In the next chapter we’ll take our first look at securing your app. We’ll discuss the difference between authentication and authorization, the concept of identity in ASP.NET Core, and how you can use the ASP.NET Core Identity system to let users register and log in to your app.
在下一章中,我们将首先了解如何保护您的应用程序。我们将讨论身份验证和授权之间的区别、ASP.NET Core 中的身份概念,以及如何使用 ASP.NET Core Identity 系统让用户注册和登录您的应用。

22.4 Summary

22.4 总结

The filter pipeline executes as part of the MVC or Razor Pages execution. It consists of authorization filters, resource filters, action filters, page filters, exception filters, and result filters.
筛选器管道作为 MVC 或 Razor Pages 执行的一部分执行。它由授权筛选条件、资源筛选条件、作筛选条件、页面筛选条件、异常筛选条件和结果筛选条件组成。

ASP.NET Core includes many built-in filters, but you can also create custom filters tailored to your application. You can use custom filters to extract common cross-cutting functionality out of your MVC controllers and Razor Pages, reducing duplication and ensuring consistency across your endpoints.
ASP.NET Core 包含许多内置筛选器,但您也可以创建针对您的应用程序定制的自定义筛选器。您可以使用自定义筛选器从 MVC 控制器和 Razor 页面中提取常见的横切功能,从而减少重复并确保端点之间的一致性。

Authorization filters run first in the pipeline and control access to APIs. ASP.NET Core includes an [Authorization] attribute that you can apply to action methods so that only logged-in users can execute the action.
授权过滤器首先在管道中运行并控制对 API 的访问。ASP.NET Core 包含一个 [Authorization] 属性,您可以将其应用于作方法,以便只有登录用户才能执行该作。

Resource filters run after authorization filters and again after an IActionResult has been executed. They can be used to short-circuit the pipeline so that an action method is never executed. They can also be used to customize the model-binding process for an action method.
资源筛选条件在授权筛选条件之后运行,并在执行 IActionResult 后再次运行。它们可用于使管道短路,以便永远不会执行作方法。它们还可用于自定义作方法的模型绑定过程。

Action filters run after model binding has occurred and before an action method executes. They also run after the action method has executed. They can be used to extract common code out of an action method to prevent duplication. They don’t execute for Razor Pages, only for MVC controllers.
作筛选器在模型绑定发生之后和作方法执行之前运行。它们还会在执行作方法后运行。它们可用于从 action method 中提取公共代码,以防止重复。它们不为 Razor Pages 执行,只为 MVC 控制器执行。

The ControllerBase base class also implements IActionFilter and IAsyncActionFilter. They run at the start and end of the action filter pipeline, regardless of the ordering or scope of other action filters. They can be used to create action filters that are specific to one controller.
ControllerBase 基类还实现 IActionFilter 和 IAsyncActionFilter。它们在作筛选器管道的开头和结尾运行,而不管其他作筛选器的顺序或范围如何。它们可用于创建特定于一个控制器的作筛选器。

Page filters run three times: after page handler selection, after model binding, and after the page handler method executes. You can use page filters for similar purposes as action filters. Page filters execute only for Razor Pages; they don’t run for MVC controllers.
页面过滤器运行三次:选择页面处理程序后、模型绑定后和页面处理程序方法执行后。您可以将页面过滤器用于与作过滤器类似的目的。页面筛选器仅对 Razor Pages 执行;它们不为 MVC 控制器运行。

Razor Page PageModels implement IPageFilter and IAsyncPageFilter, so they can be used to implement page-specific page filters. These are rarely used, as you can typically achieve similar results with simple private methods.
Razor Page PageModel 实现 IPageFilter 和 IAsyncPageFilter,因此它们可用于实现特定于页面的页面筛选器。这些方法很少使用,因为通常可以使用简单的私有方法获得类似的结果。

Exception filters execute after action and page filters, when an action method or page handler has thrown an exception. They can be used to provide custom error handling specific to the action executed.
当作方法或页面处理程序引发异常时,异常筛选器在作和页面筛选器之后执行。它们可用于提供特定于所执行作的自定义错误处理。

Generally, you should handle exceptions at the middleware level, but you can use exception filters to customize how you handle exceptions for specific actions, controllers, or Razor Pages.
通常,您应该在中间件级别处理异常,但您可以使用异常筛选器来自定义处理特定作、控制器或 Razor Pages 的异常的方式。

Result filters run before and after an IActionResult is executed. You can use them to control how the action result is executed or to completely change the action result that will be executed.
结果筛选器在执行 IActionResult 之前和之后运行。您可以使用它们来控制作结果的执行方式,或完全更改将要执行的作结果。

All filters can short-circuit the pipeline by setting a response. This generally prevents the request progressing further in the filter pipeline, but the exact behavior varies with the type of filter that is short-circuited.
所有过滤器都可以通过设置响应来短路管道。这通常可以防止请求在筛选条件管道中进一步进行,但具体行为会因短路的筛选条件类型而异。

Result filters aren’t executed when you short-circuit the pipeline using authorization, resource, or exception filters. You can ensure that result filters also run for these short-circuit cases by implementing a result filter as IAlwaysRunResultFilter or IAsyncAlwaysRunResultFilter.
当您使用授权、资源或异常筛选条件对管道进行短路时,不会执行结果筛选条件。您可以通过将结果筛选器实现为 IAlwaysRunResultFilter 或 IAsyncAlwaysRunResultFilter 来确保结果筛选器也针对这些短路情况运行。

You can use ServiceFilterAttribute and TypeFilterAttribute to allow dependency injection in your custom filters. ServiceFilterAttribute requires that you register your filter and all its dependencies with the DI container, whereas TypeFilterAttribute requires only that the filter’s dependencies have been registered.
您可以使用 ServiceFilterAttribute 和 TypeFilterAttribute 在自定义筛选条件中允许依赖项注入。ServiceFilterAttribute 要求您向 DI 容器注册过滤器及其所有依赖项,而 TypeFilterAttribute 仅要求已注册过滤器的依赖项。

ASP.NET Core in Action 21 The MVC and Razor Pages filter pipeline

21 The MVC and Razor Pages filter pipeline
21 MVC 和 Razor Pages 筛选器管道

This chapter covers
本章涵盖
• The filter pipeline and how it differs from middleware
过滤器管道及其与中间件的区别
• The different types of filters
过滤器的不同类型的
• Filter ordering
过滤器排序

Part 3 of this book has covered the Model-View-Controller (MVC) and Razor Pages frameworks of ASP.NET Core in some detail. You learned how routing is used to select a Razor Page or action to execute. You also saw model binding, validation, and how to generate a response by returning an IActionResult from your actions and page handlers. In this chapter I’m going to head deeper into the MVC/Razor Pages frameworks and look at the filter pipeline, sometimes called the action invocation pipeline, which is analogous to the minimal API endpoint filter pipeline you learned about in chapter 5.
本书的第 3 部分详细介绍了 ASP.NET Core 的模型-视图-控制器 (MVC) 和 Razor Pages 框架。你了解了如何使用路由来选择要执行的 Razor 页面或作。您还了解了模型绑定、验证以及如何通过从作和页面处理程序返回 IActionResult 来生成响应。在本章中,我将更深入地研究 MVC/Razor Pages 框架,并查看筛选器管道,有时称为作调用管道,它类似于您在第 5 章中了解的最小 API 端点筛选器管道。

MVC and Razor Pages use several built-in filters to handle cross-cutting concerns, such as authorization (controlling which users can access which action methods and pages in your application). Any application that has the concept of users will use authorization filters as a minimum, but filters are much more powerful than this single use case. In sections 21.1 and 21.2 you’ll learn about all the different types of filters and how they combine to create the MVC filter pipeline for a request that reaches the MVC or Razor Pages framework.
MVC 和 Razor Pages 使用多个内置筛选器来处理横切关注点,例如授权(控制哪些用户可以访问应用程序中的哪些作方法和页面)。任何具有用户概念的应用程序都将至少使用授权过滤器,但过滤器比这个单一用例强大得多。在第 21.1 节和第 21.2 节中,您将了解所有不同类型的筛选器,以及它们如何组合起来为到达 MVC 或 Razor Pages 框架的请求创建 MVC 筛选器管道。

Think of the MVC filter pipeline as a mini middleware pipeline running inside the MVC and Razor Pages frameworks, like the minimal API endpoint filter pipeline. Like the middleware pipeline in ASP.NET Core, the MVC filter pipeline consists of a series of components connected as a pipe, so the output of one filter feeds into the input of the next. In section 21.3 we’ll look at the similarities and differences between these two pipelines, and when you should choose one over the other.
将 MVC 筛选器管道视为在 MVC 和 Razor Pages 框架内运行的微型中间件管道,就像最小 API 终结点筛选器管道一样。与 ASP.NET Core 中的中间件管道一样,MVC 筛选器管道由一系列作为管道连接的组件组成,因此一个筛选器的输出会馈送到下一个筛选器的输入中。在 Section 21.3 中,我们将了解这两个 pipelines 之间的相似之处和不同之处,以及何时应该选择一个而不是另一个。

In section 21.4 you’ll see how to create a simple custom filter. Rather than focus on the functionality of the filter itself, you’ll learn how to apply it to multiple endpoints in section 21.5. In section 21.6 you’ll see how the choice of where you apply your attributes affects the order in which your filters execute.
在 Section 21.4 中,您将看到如何创建简单的自定义过滤器。您将学习 21.5 节中的如何将它应用于多个端点,而不是关注过滤器本身的功能。在 Section 21.6 中,您将看到选择应用属性的位置如何影响过滤器的执行顺序。

The filter pipeline is a complex topic, but it can enable some advanced behaviors in your app and potentially reduce overall complexity. In this chapter you’ll learn the basics of the pipeline and how it works. In chapter 22 we dig into practical examples of filters, looking at the filters that come out of the box in ASP.NET Core, as well as building custom filters to extract common code from your controllers and Razor Pages.
筛选管道是一个复杂的主题,但它可以在您的应用中启用一些高级行为,并可能降低整体复杂性。在本章中,您将学习管道的基础知识及其工作原理。在第 22 章中,我们深入探讨了筛选器的实际示例,查看了 ASP.NET Core 中开箱即用的筛选器,并构建了自定义筛选器以从控制器和 Razor 页面中提取常见代码。

Before we can start writing code, we should get to grips with the basics of the filter pipeline. The first section of this chapter explains what the pipeline is, why you might want to use it, and how it differs from the middleware pipeline.
在开始编写代码之前,我们应该先了解过滤器管道的基础知识。本章的第一部分介绍了管道是什么,为什么您可能希望使用它,以及它与中间件管道有何不同。

21.1 Understanding the MVC filter pipeline

21.1 了解 MVC 过滤器管道

In this section you’ll learn all about the MVC filter pipeline. You’ll see where it fits in the life cycle of a typical request and the roles of the six types of filters available.
在本节中,您将了解有关 MVC 筛选器管道的所有信息。您将看到它在典型请求的生命周期中的位置,以及可用的六种筛选器的角色。

The filter pipeline is a relatively simple concept in that it provides hooks into the normal MVC request, as shown in figure 21.1. For example, say you wanted to ensure that users can create or edit products on an e-commerce app only if they’re logged in. The app would redirect anonymous users to a login page instead of executing the action.
过滤器管道是一个相对简单的概念,因为它为普通的 MVC 请求提供了钩子,如图 21.1 所示。例如,假设您希望确保用户只有在登录后才能在电子商务应用程序上创建或编辑产品。该应用程序会将匿名用户重定向到登录页面,而不是执行作。

alt text

Figure 21.1 Filters run at multiple points in the EndpointMiddleware as part of the normal handling of an MVC request. A similar pipeline exists for Razor Page requests.
图 21.1 过滤器在 EndpointMiddleware 中的多个点运行,作为 MVC 请求的正常处理的一部分。Razor Page 请求存在类似的管道。

Without filters, you’d need to include the same code to check for a logged-in user at the start of each specific action method. With this approach, the MVC framework would still execute the model binding and validation, even if the user were not logged in.
如果没有筛选器,则需要在每个特定作方法的开头包含相同的代码来检查已登录的用户。使用这种方法,MVC 框架仍将执行模型绑定和验证,即使用户未登录。

With filters, you can use the hooks in the MVC request to run common code across all requests or a subset of requests. This way you can do a wide range of things, such as
借助筛选器,您可以使用 MVC 请求中的挂钩在所有请求或请求子集中运行通用代码。通过这种方式,您可以执行各种作,例如

• Ensure that a user is logged in before an action method, model binding, or validation runs.
确保在作方法、模型绑定或验证运行之前登录用户。
• Customize the output format of particular action methods.
自定义特定作方法的输出格式。
• Handle model validation failures before an action method is invoked.
在调用作方法之前处理模型验证失败。
• Catch exceptions from an action method and handle them in a special way.
从作方法捕获异常,并以特殊方式处理它们。

In many ways, the MVC filter pipeline is like an extra middleware pipeline, restricted to MVC and Razor Pages requests only. Like middleware, filters are good for handling cross-cutting concerns for your application and are useful tools for reducing code duplication in many cases.
在许多方面,MVC 筛选器管道就像一个额外的中间件管道,仅限于 MVC 和 Razor Pages 请求。与 middleware 一样,过滤器非常适合处理应用程序的横切关注点,并且在许多情况下是减少代码重复的有用工具。

The linear1 view of an MVC request and the filter pipeline that I’ve used so far doesn’t quite match up with how these filters execute. There are five types of filters that apply to MVC requests, each of which runs at a different stage in the MVC framework, as shown in figure 21.2.
到目前为止,我使用的 MVC 请求的 linear1 视图和筛选器管道与这些筛选器的执行方式并不完全匹配。有五种类型的过滤器适用于 MVC 请求,每一种都在 MVC 框架的不同阶段运行,如图 21.2 所示。

alt text

Figure 21.2 The MVC filter pipeline, including the five filter stages. Some filter stages (resource, action, and result) run twice, before and after the remainder of the pipeline.
图 21.2 MVC 过滤器管道,包括 5 个过滤器阶段。某些筛选阶段(resource、action 和 result)在管道的其余部分之前和之后运行两次。

Each filter stage lends itself to a particular use case, thanks to its specific location in the pipeline, with respect to model binding, action execution, and result execution:
每个过滤器阶段都适用于特定的用例,这要归功于它在管道中的特定位置,包括模型绑定、作执行和结果执行:

• Authorization filters—These run first in the pipeline, so they’re useful for protecting your APIs and action methods. If an authorization filter deems the request unauthorized, it short-circuits the request, preventing the rest of the filter pipeline (or action) from running.
授权过滤器 - 这些过滤器首先在管道中运行,因此它们可用于保护您的 API 和作方法。如果授权筛选条件认为请求未经授权,则会使请求短路,从而阻止筛选条件管道(或作)的其余部分运行。
• Resource filters—After authorization, resource filters are the next filters to run in the pipeline. They can also execute at the end of the pipeline, in much the same way that middleware components can handle both the incoming request and the outgoing response. Alternatively, resource filters can completely short-circuit the request pipeline and return a response directly.
资源过滤器 - 授权后,资源过滤器是管道中运行的下一个过滤器。它们还可以在管道的末尾执行,就像中间件组件可以同时处理传入请求和传出响应一样。或者,资源筛选条件可以完全短路请求管道并直接返回响应。

Thanks to their early position in the pipeline, resource filters can have a variety of uses. You could add metrics to an action method; prevent an action method from executing if an unsupported content type is requested; or, as they run before model binding, control the way model binding works for that request.
由于它们在管道中的早期位置,资源过滤器可以有多种用途。您可以向作方法添加度量;在请求不受支持的内容类型时阻止执行作方法;或者,当它们在模型绑定之前运行时,控制模型绑定对该请求的工作方式。

• Action filters—Action filters run immediately before and after an action method is executed. As model binding has already happened, action filters let you manipulate the arguments to the method—before it executes—or they can short-circuit the action completely and return a different IActionResult. Because they also run after the action executes, they can optionally customize an IActionResult returned by the action before the action result is executed.
作筛选器 -作筛选器在执行作方法之前和之后立即运行。由于模型绑定已经发生,因此作筛选器允许您在方法执行之前作方法的参数,或者它们可以完全短路作并返回不同的 IActionResult。由于它们也在作执行后运行,因此可以选择在执行作结果之前自定义作返回的 IActionResult。
• Exception filters—Exception filters catch exceptions that occur in the filter pipeline and handle them appropriately. You can use exception filters to write custom, MVC-specific error-handling code, which can be useful in some situations. For example, you could catch exceptions in API actions and format them differently from exceptions in your Razor Pages.
异常过滤器 - 异常过滤器可捕获过滤器管道中发生的异常并对其进行适当处理。您可以使用异常筛选器编写特定于 MVC 的自定义错误处理代码,这在某些情况下可能很有用。例如,可以在 API作中捕获异常,并对其进行不同于 Razor Pages 中的异常格式设置。
• Result filters—Result filters run before and after an action method’s IActionResult is executed. You can use result filters to control the execution of the result or even to short-circuit the execution of the result.
结果筛选器 - 结果筛选器在执行作方法的 IActionResult 之前和之后运行。您可以使用结果筛选器来控制结果的执行,甚至可以缩短结果的执行。

Exactly which filter you pick to implement will depend on the functionality you’re trying to introduce. Want to short-circuit a request as early as possible? Resource filters are a good fit. Need access to the action method parameters? Use an action filter.
您选择实施哪个过滤器将取决于您尝试引入的功能。想要尽早使请求短路?资源筛选器非常适合。需要访问作方法参数?使用作筛选器。

Think of the filter pipeline as a small middleware pipeline that lives by itself in the MVC framework. Alternatively, you could think of filters as hooks into the MVC action invocation process that let you run code at a particular point in a request’s life cycle.
将过滤器管道视为一个小型中间件管道,它独立存在于 MVC 框架中。或者,您可以将筛选器视为 MVC作调用过程的挂钩,允许您在请求生命周期的特定点运行代码。

NOTE The design of the MVC filter pipeline is quite different from the minimal API endpoint filter pipeline you saw in chapter 5. The endpoint filter pipeline is linear and doesn’t have multiple types of filters.
注意:MVC 过滤器管道的设计与您在第 5 章中看到的最小 API 端点过滤器管道完全不同。终端节点筛选条件管道是线性的,没有多种类型的筛选条件。

This section described how the filter pipeline works for MVC and Web API controllers; Razor Pages use an almost-identical filter pipeline.
本部分介绍了筛选器管道如何用于 MVC 和 Web API 控制器;Razor Pages 使用几乎相同的筛选管道。

21.2 The Razor Pages filter pipeline

21.2 Razor Pages 筛选器管道

The Razor Pages framework uses the same underlying architecture as MVC and Web API controllers, so it’s perhaps not surprising that the filter pipeline is virtually identical. The only difference between the pipelines is that Razor Pages do not use action filters. Instead, they use page filters, as shown in figure 21.3.
Razor Pages 框架使用与 MVC 和 Web API 控制器相同的底层体系结构,因此筛选器管道几乎相同可能不足为奇。管道之间的唯一区别是 Razor Pages 不使用作筛选器。相反,它们使用页面过滤器,如图 21.3 所示。

alt text

Figure 21.3 The Razor Pages filter pipeline, including the five filter stages. Authorization, resource, exception, and result filters execute in exactly the same way as for the MVC pipeline. Page filters are specific to Razor Pages and execute in three places: after page hander selection, after model binding and validation, and after page handler execution.
图 21.3 Razor Pages 筛选器管道,包括 5 个筛选器阶段。授权、资源、异常和结果筛选器的执行方式与 MVC 管道的执行方式完全相同。页面筛选器特定于 Razor 页面,并在三个位置执行:选择页面处理程序后、模型绑定和验证后以及页面处理程序执行后。

The authorization, resource, exception, and result filters are exactly the same filters you saw for the MVC pipeline. They execute in the same way, serve the same purposes, and can be short-circuited in the same way.
authorization、resource、exception 和 result 过滤器与您在 MVC 管道中看到的过滤器完全相同。它们以相同的方式执行,服务于相同的目的,并且可以以相同的方式短路。

NOTE These filters are literally the same classes shared between the Razor Pages and MVC frameworks.
注意:这些筛选器实际上是 Razor Pages 和 MVC 框架之间共享的相同类。

The difference with the Razor Pages filter pipeline is that it uses page filters instead of action filters. By contrast with other filter types, page filters run three times in the filter pipeline:
与 Razor Pages 筛选器管道的不同之处在于,它使用页面筛选器而不是作筛选器。与其他筛选器类型相比,页面筛选器在筛选器管道中运行三次:

• After page handler selection—After the resource filters have executed, a page handler is selected, based on the request’s HTTP verb and the {handler} route value, as you learned in chapter 15. After page handler selection, a page filter method executes for the first time. You can’t short-circuit the pipeline at this stage, and model binding and validation have not yet executed.
选择页面处理程序后 - 执行资源过滤器后,将根据请求的 HTTP 动词和 {handler} 路由值选择页面处理程序,如第 15 章所述。选择页面处理程序后,将首次执行页面筛选方法。在此阶段,您不能使管道短路,并且模型绑定和验证尚未执行。

• After model binding—After the first page filter execution, the request is model-bound to the Razor Page’s binding models and is validated. This execution is highly analogous to the action filter execution for API controllers. At this point you could manipulate the model-bound data or short-circuit the page handler execution completely by returning a different IActionResult.
模型绑定后 - 在执行第一个页面筛选器后,请求将模型绑定到 Razor 页面的绑定模型并进行验证。此执行与 API 控制器的 action filter 执行高度相似。此时,您可以通过返回不同的 IActionResult 来作模型绑定数据或完全短路页面处理程序执行。

• After page handler execution—If you don’t short-circuit the page handler execution, the page filter runs a third and final time after the page handler has executed. At this point you could customize the IActionResult returned by the page handler before the result is executed.
页面处理程序执行后 - 如果不使页面处理程序执行短路,则页面过滤器将在页面处理程序执行后第三次也是最后一次运行。此时,您可以在执行结果之前自定义页面处理程序返回的 IActionResult。

The triple execution of page filters makes it a bit harder to visualize the pipeline, but you can generally think of them as beefed-up action filters. Everything you can do with an action filter, you can do with a page filter, and you can hook in after page handler selection if necessary.
页面过滤器的三重执行使得可视化管道有点困难,但您通常可以将它们视为增强的作过滤器。你可以用 action filter 做的所有事情,都可以用 page filter 做,如果需要,你可以在 Page handler 选择后挂接。

Tip Each execution of a filter executes a different method of the appropriate interface, so it’s easy to know where you are in the pipeline and to execute a filter in only one of its possible locations if you wish.
提示:每次执行筛选条件都会执行相应接口的不同方法,因此很容易知道您在管道中的位置,并且如果您愿意,只需在其一个可能的位置执行筛选条件。

One of the main questions I hear when people learn about filters in ASP.NET Core is “Why do we need them?” If the filter pipeline is like a mini middleware pipeline, why not use a middleware component directly, instead of introducing the filter concept? That’s an excellent point, which I’ll tackle in the next section.
当人们了解 ASP.NET Core 中的过滤器时,我听到的主要问题之一是“我们为什么需要它们?如果 filter pipeline 就像一个迷你的 middleware pipeline ,为什么不直接使用一个 middleware 组件,而不是引入 filter 概念呢?这是一个很好的观点,我将在下一节中讨论。

21.3 Filters or middleware: Which should you choose?

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

The filter pipeline is similar to the middleware pipeline in many ways, but there are several subtle differences that you should consider when deciding which approach to use. The considerations are essentially the same as those for the minimal API endpoint filter I discussed in chapter 5. MVC filters and middleware are similar in three ways:
filter 管道在许多方面与中间件管道相似,但在决定使用哪种方法时,应考虑几个细微的差异。这些注意事项与我在第 5 章中讨论的最小 API 端点过滤器的注意事项基本相同。MVC 筛选器和中间件在三个方面相似:

• Requests pass through a middleware component on the way “in,” and responses pass through again on the way “out.” Resource, action, and result filters are also two-way, though authorization and exception filters run only once for a request, and page filters run three times.
请求在“in”途中通过中间件组件,响应在“out”途中再次传递。资源、作和结果筛选器也是双向的,但授权和异常筛选器只为请求运行一次,而页面筛选器运行三次。
• Middleware can short-circuit a request by returning a response instead of passing it on to later middleware. MVC and page filters can also short-circuit the filter pipeline by returning a response.
中间件可以通过返回响应而不是将其传递给后续中间件来使请求短路。MVC 和页面筛选器还可以通过返回响应来使筛选器管道短路。
• 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.
中间件通常用于横切应用程序问题,例如日志记录、性能分析和异常处理。过滤器还适用于横切关注点。

Filters and middleware also differ primarily in three ways:
筛选器和中间件也主要在三个方面有所不同:

• Middleware can run for all requests; filters run only for requests that reach the EndpointMiddleware and execute a controller action or Razor Page handler.
中间件可以针对所有请求运行筛选器仅针对到达 EndpointMiddleware 并执行控制器作或 Razor Page 处理程序的请求运行。
• Filters have access to MVC constructs such as ModelState and IActionResults. Middleware in general is independent from MVC and Razor Pages and works at a lower level, so it can’t use these concepts.
筛选器可以访问 MVC 构造,例如 ModelState 和 IActionResults。中间件通常独立于 MVC 和 Razor Pages,并且在较低级别工作,因此它不能使用这些概念。
• Filters can be easily applied to a subset of requests, such as all actions on a single controller or a single Razor Page. Middleware generally applies to all requests that reach a given point in the middleware pipeline.
筛选器可以轻松应用于请求的子集,例如单个控制器或单个 Razor 页面上的所有作。中间件通常适用于到达中间件管道中给定点的所有请求。

As for the endpoint filter pipeline, I like to think of middleware versus MVC filters as a question of specificity. Middleware is the more general concept, so it has the wider reach. But if you need to access to MVC constructs or want to behave differently for some MVC actions or Razor Pages, you should consider using a filter.
至于端点过滤器管道,我喜欢将中间件与 MVC 过滤器视为一个特异性问题。中间件是更通用的概念,因此它的范围更广。但是,如果需要访问 MVC 构造或希望对某些 MVC作或 Razor Pages 采取不同的行为,则应考虑使用筛选器。

The middleware-versus-filters argument is a subtle one, and it doesn’t matter which you choose as long as it works for you. You can even use middleware components inside the MVC filter pipeline, effectively turning a middleware component into a filter!
middleware-versus-filters 的参数是一个微妙的参数,只要它适合你,你选择哪一个并不重要。您甚至可以在 MVC 过滤器管道中使用中间件组件,从而有效地将中间件组件转换为过滤器!

Tip The middleware-as-filters feature was introduced in ASP.NET Core 1.1 and is also available in later versions. The canonical use case is for localizing requests to multiple languages. I have a blog series on how to use the feature here: http://mng.bz/RXa0.
提示middleware-as-filters 功能是在 ASP.NET Core 1.1 中引入的,在以后的版本中也可用。规范用例是将请求本地化为多种语言。我有一个关于如何使用该功能的博客系列:http://mng.bz/RXa0

Filters can be a little abstract in isolation, so in the next section we’ll look at some code and learn how to write a custom MVC filter in ASP.NET Core.
筛选器可以单独使用一些抽象,因此在下一节中,我们将查看一些代码并学习如何在 ASP.NET Core 中编写自定义 MVC 筛选器。

21.4 Creating a simple filter

21.4 创建简单过滤器

In this section, I show you how to create your first filters; in section 21.5 you’ll see how to apply them to MVC controllers and actions. We’ll start small, creating filters that only write to the console, but in chapter 22 we look at some more practical examples and discuss some of their nuances.
在本节中,我将向您展示如何创建您的第一个过滤器;在 Section 21.5 中,您将看到如何将它们应用于 MVC 控制器和作。我们将从小处着手,创建仅写入控制台的过滤器,但在第 22 章中,我们将查看一些更实际的示例并讨论它们的一些细微差别。

You implement a filter for a given stage by implementing one of a pair of interfaces, one synchronous (sync) and one asynchronous (async):
您可以通过实现一对接口之一(一个同步 (sync)和一个异步 (async))来为给定阶段实现筛选器:

• Authorization filters—IAuthorizationFilter or IAsyncAuthorizationFilter
授权筛选器 - IAuthorizationFilter 或 IAsyncAuthorizationFilter
• Resource filters—IResourceFilter or IAsyncResourceFilter
资源筛选器 - IResourceFilter 或 IAsyncResourceFilter
• Action filters—IActionFilter or IAsyncActionFilter
动作筛选器 - IActionFilter 或 IAsyncActionFilter
• Page filters—IPageFilter or IAsyncPageFilter
页面筛选器 - IPageFilter 或 IAsyncPageFilter
• Exception filters—IExceptionFilter or IAsyncExceptionFilter
异常筛选器 - IExceptionFilter 或 IAsyncExceptionFilter
• Result filters—IResultFilter or IAsyncResultFilter
结果筛选器 - IResultFilter 或 IAsyncResultFilter

You can use any plain old CLR object (POCO) class to implement a filter, but you’ll typically implement them as C# attributes, which you can use to decorate your controllers, actions, and Razor Pages, as you’ll see in section 21.5. You can achieve the same results with either the sync or async interface, so which you choose should depend on whether any services you call in the filter require async support.
您可以使用任何普通的旧 CLR 对象 (POCO) 类来实现过滤器,但您通常会将它们实现为 C# 属性,您可以使用这些属性来装饰您的控制器、作和 Razor 页面,如第 21.5 节所示。您可以使用 sync 或 async 接口实现相同的结果,因此您选择哪个接口取决于您在过滤器中调用的任何服务是否需要异步支持。

NOTE You should implement either the sync interface or the async interface, not both. If you implement both, only the async interface will be used.
注意:您应该实现 sync 接口或 async 接口,而不是两者兼而有之。如果同时实现这两个接口,则仅使用异步接口。

Listing 21.1 shows a resource filter that implements IResourceFilter and writes to the console when it executes. The OnResourceExecuting method is called when a request first reaches the resource filter stage of the filter pipeline. By contrast, the OnResourceExecuted method is called after the rest of the pipeline has executed: after model binding, action execution, result execution, and all intermediate filters have run.
清单 21.1 显示了一个资源过滤器,它实现 IResourceFilter 并在执行时写入控制台。当请求首次到达筛选管道的资源筛选阶段时,将调用 OnResourceExecuting 方法。相比之下,OnResourceExecuted 方法是在管道的其余部分执行之后调用的:在模型绑定、作执行、结果执行和所有中间筛选器运行之后。

Listing 21.1 Example resource filter implementing IResourceFilter
清单 21.1 实现 IResourceFilter 的示例资源过滤器

public class LogResourceFilter : Attribute, IResourceFilter
{
    public void OnResourceExecuting(          #A
        ResourceExecutingContext context)    #B
    {
        Console.WriteLine("Executing!");
    }

    public void OnResourceExecuted(             #C
        ResourceExecutedContext context)    #D
    {
        Console.WriteLine("Executed");
    }
}

❶ Executed at the start of the pipeline, after authorization filters
在管道开始时执行,在授权过滤器之后
❷ The context contains the HttpContext, routing details, and information about the current action.
上下文包含 HttpContext、路由详细信息和有关当前作的信息。
❸ Executed after model binding, action execution, and result execution
在模型绑定、作执行和结果执行之后执行
❹ Contains additional context information, such as the IActionResult returned by the action
包含其他上下文信息,例如作返回的 IActionResult

The interface methods are simple and are similar for each stage in the filter pipeline, passing a context object as a method parameter. Each of the two-method sync filters has an Executing and an Executed method. The type of the argument is different for each filter, but it contains all the details for the filter pipeline.
接口方法很简单,并且对于筛选器管道中的每个阶段都类似,将上下文对象作为方法参数传递。两种方法的同步筛选器中的每一个都有一个 Executing 和一个 Executed 方法。每个筛选条件的参数类型都不同,但它包含筛选条件管道的所有详细信息。

For example, the ResourceExecutingContext passed to the resource filter contains the HttpContext object itself, details about the route that selected this action, details about the action itself, and so on. Contexts for later filters contain additional details, such as the action method arguments for an action filter and the ModelState.
例如,传递给资源筛选器的 ResourceExecutingContext 包含 HttpContext 对象本身、有关选择此作的路由的详细信息、有关作本身的详细信息等。更高筛选器的上下文包含其他详细信息,例如作筛选器的作方法参数和 ModelState。

The context object for the ResourceExecutedContext method is similar, but it also contains details about how the rest of the pipeline executed. You can check whether an unhandled exception occurred, you can see if another filter from the same stage short-circuited the pipeline, or you can see the IActionResult used to generate the response.
ResourceExecutedContext 方法的上下文对象类似,但它还包含有关管道其余部分如何执行的详细信息。您可以检查是否发生了未经处理的异常,可以查看同一阶段中的另一个筛选器是否使管道短路,或者您可以查看用于生成响应的 IActionResult。

These context objects are powerful and are the key to advanced filter behaviors like short-circuiting the pipeline and handling exceptions. We’ll make use of them in chapter 22 when we create more complex filter examples.
这些上下文对象功能强大,是高级筛选行为(如短路管道和处理异常)的关键。我们将在第 22 章创建更复杂的过滤器示例时使用它们。

The async version of the resource filter requires implementing a single method, as shown in listing 21.2. As for the sync version, you’re passed a ResourceExecutingContext object as an argument, and you’re passed a delegate representing the remainder of the filter pipeline. You must call this delegate (asynchronously) to execute the remainder of the pipeline, which returns an instance of ResourceExecutedContext.
资源过滤器的异步版本需要实现一个方法,如清单 21.2 所示。对于同步版本,将向您传递一个 ResourceExecutingContext 对象作为参数,并传递一个表示筛选管道其余部分的委托。您必须(异步)调用此委托来执行管道的其余部分,该管道将返回 ResourceExecutedContext 的实例。

Listing 21.2 Example resource filter implementing IAsyncResourceFilter
列表 21.2 实现 IAsyncResourceFilter 的示例资源过滤器

public class LogAsyncResourceFilter : Attribute, IAsyncResourceFilter
{
public async Task OnResourceExecutionAsync( ❶
ResourceExecutingContext context,
ResourceExecutionDelegate next) ❷
{
Console.WriteLine("Executing async!"); ❸
ResourceExecutedContext executedContext = await next(); ❹
Console.WriteLine("Executed async!"); ❺
}
}

❶ Executed at the start of the pipeline, after authorization filters
在管道开始时执行,授权过滤器之后
❷ You’re provided a delegate, which encapsulates the remainder of the filter pipeline.
为您提供一个委托,它封装了过滤器管道的其余部分。
❸ Called before the rest of the pipeline executes
在管道的其余部分执行之前调用
❹ Executes the rest of the pipeline and obtains a ResourceExecutedContext instance
执行管道的其余部分并获取 ResourceExecutedContext 实例
❺ Called after the rest of the pipeline executes
在管道的其余部分执行之后调用

The sync and async filter implementations have subtle differences, but for most purposes they’re identical. I recommend implementing the sync version for simplicity, falling back to the async version only if you need to.
sync 和 async filter 实现有细微的差异,但在大多数情况下它们是相同的。为简单起见,我建议实现同步版本,仅在需要时回退到异步版本。

You’ve created a couple of filters now, so we should look at how to use them in the application. In the next section we’ll tackle two specific issues: how to control which requests execute your new filters and how to control the order in which they execute.
您现在已经创建了几个过滤器,因此我们应该看看如何在应用程序中使用它们。在下一节中,我们将解决两个具体问题:如何控制哪些请求执行您的新过滤器,以及如何控制它们的执行顺序。

21.5 Adding filters to your actions and Razor Pages

向动作和 Razor 页面添加过滤器

In section 21.3 I discussed the similarities and differences between middleware and filters. One of those differences is that filters can be scoped to specific actions or controllers so that they run only for certain requests. Alternatively, you can apply a filter globally so that it runs for every MVC action and Razor Page.
在 Section 21.3 中,我讨论了 middleware 和 filters 之间的异同。其中一个区别是,过滤器的范围可以限定为特定的作或控制器,以便它们仅针对特定请求运行。或者,您可以全局应用筛选器,以便它针对每个 MVC作和 Razor 页面运行。

By adding filters in different ways, you can achieve several different results. Imagine you have a filter that forces you to log in to execute an action. How you add the filter to your app will significantly change your app’s behavior:
通过以不同的方式添加过滤器,您可以获得多种不同的结果。假设您有一个过滤器,它强制您登录以执行作。向应用程序添加过滤器的方式将显著改变应用程序的行为:

• Apply the filter to a single action or Razor Page. Anonymous users could browse the app as normal, but if they tried to access the protected action or Razor Page, they would be forced to log in.
将筛选器应用于单个作或 Razor 页面。匿名用户可以正常浏览应用程序,但如果他们尝试访问受保护的作或 Razor 页面,他们将被迫登录。
• Apply the filter to a controller. Anonymous users could access actions from other controllers, but accessing any action on the protected controller would force them to log in.
将过滤器应用于控制器。匿名用户可以访问来自其他控制器的作,但访问受保护控制器上的任何作都会强制他们登录。
• Apply the filter globally. Users couldn’t use the app without logging in. Any attempt to access an action or Razor Page would redirect the user to the login page.
全局应用筛选器。用户如果不登录就无法使用该应用程序。任何访问作或 Razor 页面的尝试都会将用户重定向到登录页面。

NOTE ASP.NET Core comes with such a filter out of the box: AuthorizeFilter. I discuss this filter in chapter 22, and you’ll be seeing a lot more of it in chapter 24.
注意: ASP.NET Core 附带了这样一个开箱即用的筛选器:AuthorizeFilter。我在第 22 章中讨论了这个过滤器,您将在第 24 章中看到更多内容。

As I described in the previous section, you normally create filters as attributes, and for good reason: it makes it easy for you to apply them to MVC controllers, actions, and Razor Pages. In this section you’ll see how to apply LogResourceFilter from listing 21.1 to an action, a controller, a Razor Page, and globally. The level at which the filter applies is called its scope.
正如我在上一节中所描述的,您通常将筛选器创建为属性,这是有充分理由的:它使您可以轻松地将它们应用于 MVC 控制器、作和 Razor 页面。在本节中,您将了解如何将清单 21.1 中的 LogResourceFilter 应用于作、控制器、Razor Page 和全局应用。筛选器应用的级别称为其范围。

DEFINITION The scope of a filter refers to how many different actions it applies to. A filter can be scoped to the action method, to the controller, to a Razor Page, or globally.
定义:筛选器的范围是指它应用于多少个不同的作。筛选器的范围可以限定为作方法、控制器、Razor 页面或全局。

You’ll start at the most specific scope: applying filters to a single action. The following listing shows an example of an MVC controller that has two action methods, one with LogResourceFilter and one without.
您将从最具体的范围开始:将筛选条件应用于单个作。下面的清单显示了一个 MVC 控制器的示例,该控制器具有两个作方法,一个带有 LogResourceFilter,另一个没有。

Listing 21.3 Applying filters to an action method
清单 21.3 将过滤器应用于作方法

public class RecipeController : ControllerBase
{
    [LogResourceFilter]            #A
    public IActionResult Index()   #A
    {                              #A
        return Ok();               #A
    }                              #A
    public IActionResult View()   #B
    {                             #B
        return OK();              #B
    }                             #B
}

❶ LogResourceFilter runs as part of the pipeline when executing this action.
LogResourceFilter 在执行此作时作为管道的一部分运行。
❷ This action method has no filters at the action level.
此作方法在作级别没有筛选器。

Alternatively, if you want to apply the same filter to every action method, you could add the attribute at the controller scope, as in the next listing. Every action method in the controller uses LogResourceFilter without having to specifically decorate each method.
或者,如果要将相同的过滤器应用于每个作方法,则可以在控制器范围内添加该属性,如下一个清单所示。控制器中的每个 action method 都使用 LogResourceFilter,而不必专门修饰每个方法。

Listing 21.4 Applying filters to a controller
清单 21.4 将过滤器应用于控制器

[LogResourceFilter]                             #A
public class RecipeController : ControllerBase
{
    public IActionResult Index ()   #B
    {                               #B
        return Ok();                #B
    }                               #B
    public IActionResult View()     #B
    {                               #B
        return Ok();                #B
    }                               #B
}

❶ The LogResourceFilter is added to every action on the controller.
LogResourceFilter 被添加到控制器上的每个作中。
❷ Every action in the controller is decorated with the filter.
控制器中的每个作都用过滤器装饰。

For Razor Pages, you can apply attributes to your PageModel, as shown in the following listing. The filter applies to all page handlers in the Razor Page. It’s not possible to apply filters to a single page handler; you must apply them at the page level.
对于 Razor Pages,您可以将属性应用于 PageModel,如下面的清单所示。筛选器适用于 Razor 页面中的所有页面处理程序。无法将过滤器应用于单个页面处理程序;您必须在页面级别应用它们。

Listing 21.5 Applying filters to a Razor Page
清单 21.5 将过滤器应用于 Razor 页面

[LogResourceFilter]             #A
public class IndexModel : PageModel
{
    public void OnGet()    #B
    {                      #B
    }                      #B

    public void OnPost()   #B
    {                      #B
    }                      #B
}

❶ The LogResourceFilter is added to the Razor Page’s PageModel.
LogResourceFilter 已添加到 Razor 页面的 PageModel。
❷ The filter applies to every page handler in the page.
过滤器适用于页面中的每个页面处理程序。

Filters you apply as attributes to controllers, actions, and Razor Pages are automatically discovered by the framework when your application starts up. For common attributes, you can go one step further and apply filters globally without having to decorate individual classes.
当应用程序启动时,框架会自动发现作为属性应用于控制器、作和 Razor 页面的筛选器。对于通用属性,您可以更进一步,全局应用过滤器,而不必装饰单个类。

You add global filters in a different way from controller- or action-scoped filters—by adding a filter directly to the MVC services when configuring your controllers and Razor Pages. The next listing shows three equivalent ways to add a globally scoped filter.
添加全局筛选器的方式与控制器或作范围的筛选器不同,即在配置控制器和 Razor Pages 时直接向 MVC 服务添加筛选器。下一个清单显示了添加全局范围过滤器的三种等效方法。

Listing 21.6 Applying filters globally to an application
清单 21.6 将过滤器全局应用于应用程序

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>    #A
{
    options.Filters.Add(new LogResourceFilter());     #B
    options.Filters.Add(typeof(LogResourceFilter));   #C
    options.Filters.Add<LogResourceFilter>();    #D
});

❶ Adds filters using the MvcOptions object
使用 MvcOptions 对象添加过滤器
❷ You can pass an instance of the filter directly. . .
您可以直接传递过滤器的实例。 . .
❸ . . . or pass in the Type of the filter and let the framework create it.
. . . .或者传入过滤器的 Type 并让框架创建它。
❹ Alternatively, the framework can create a global filter using a generic type parameter.
或者,框架可以使用泛型类型参数创建全局过滤器。

You can configure the MvcOptions by using the AddControllers() overload. When you configure filters globally, they apply both to controllers and to any Razor Pages in your application. If you wish to configure a global filter for a Razor Pages application, there isn’t an overload for configuring the MvcOptions. Instead, you need to use the AddMvcOptions() extension method to configure the filters, as shown in the following listing.
您可以使用 AddControllers() 重载配置 MvcOptions。全局配置筛选器时,它们将同时应用于控制器和应用程序中的任何 Razor Pages。如果要为 Razor Pages 应用程序配置全局筛选器,则不会有用于配置 MvcOptions 的重载。相反,您需要使用 AddMvcOptions() 扩展方法来配置过滤器,如下面的清单所示。

Listing 21.7 Applying filters globally to a Razor Pages application
列表 21.7 将过滤器全局应用于 Razor Pages 应用程序

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.RazorPages()  #A
    .AddMvcOptions(options =>    #B
    {
        options.Filters.Add(new LogResourceFilter());     #C
        options.Filters.Add(typeof(LogResourceFilter));   #C
        options.Filters.Add<LogResourceFilter>();    #C
    });

❶ This method doesn’t let you pass a lambda to configure the MvcOptions.
此方法不允许您传递 lambda 来配置 MvcOptions。
❷ You must use an extension method to add the filters to the MvcOptions object.
必须使用扩展方法将筛选器添加到 MvcOptions 对象。
❸ You can configure the filters in any of the ways shown previously.
您可以按照前面显示的任何方式配置过滤器。

With potentially three different scopes in play, you’ll often find action methods that have multiple filters applied to them, some applied directly to the action method and others inherited from the controller or globally. Then the question becomes which filter runs first.
由于可能有三种不同的范围,您通常会发现应用了多个过滤器的作方法,其中一些直接应用于作方法,而另一些则从控制器或全局继承。然后问题就变成了哪个过滤器先运行。

21.6 Understanding the order of filter execution

21.6 了解过滤器的执行顺序

You’ve seen that the filter pipeline contains five stages, one for each type of filter. These stages always run in the fixed order I described in sections 21.1 and 21.2. But within each stage, you can also have multiple filters of the same type (for example, multiple resource filters) that are part of a single action method’s pipeline. These could all have multiple scopes, depending on how you added them, as you saw in the preceding section.
您已经看到 filter 管道包含 5 个阶段,每种类型的 filter 对应一个阶段。这些阶段始终按照我在第 21.1 节和第 21.2 节中描述的固定顺序运行。但在每个阶段中,您还可以拥有多个相同类型的筛选条件(例如,多个资源筛选条件),这些筛选条件是单个作方法的管道的一部分。这些范围可能都具有多个范围,具体取决于您添加它们的方式,如上一节所示。

In this section we’re thinking about the order of filters within a given stage and how scope affects this. We’ll start by looking at the default order and then move on to ways to customize the order to your own requirements.
在本节中,我们将考虑给定阶段中过滤器的顺序以及范围如何影响这一点。我们将首先查看默认顺序,然后继续讨论根据自己的要求自定义顺序的方法。

21.6.1 The default scope execution order

21.6.1 默认范围执行顺序

When thinking about filter ordering, it’s important to remember that resource, action, and result filters implement two methods: an Executing before method and an Executed after method. On top of that, page filters implement three methods! The order in which each method executes depends on the scope of the filter, as shown in figure 21.4 for the resource filter stage.
在考虑筛选器排序时,请务必记住,资源、作和结果筛选器实现两种方法:Executing before 方法和 Executed after 方法。最重要的是,页面过滤器实现了三种方法!每个方法的执行顺序取决于过滤器的范围,如图 21.4 所示,用于资源过滤器阶段。

alt text

Figure 21.4 The default filter ordering within a given stage, based on the scope of the filters. For the Executing method, globally scoped filters run first, followed by controller-scoped, and finally action-scoped filters. For the Executed method, the filters run in reverse order.
图 21.4 给定阶段中基于过滤器范围的默认过滤器排序。对于 Executing 方法,全局范围的筛选器首先运行,然后是控制器范围的筛选器,最后是作范围的筛选器。对于 Executed 方法,筛选器按相反的顺序运行。

By default, filters execute from the broadest scope (global) to the narrowest (action) when running the Executing method for each stage. The filters’ Executed methods run in reverse order, from the narrowest scope (action) to the broadest (global).
默认情况下,在为每个阶段运行 Executing 方法时,筛选器从最广泛的范围 (全局) 到最窄的 (作) 执行。筛选器的 Executed 方法按相反的顺序运行,从最窄的范围 (作) 到最广泛的范围 (全局)。

The ordering for Razor Pages is somewhat simpler, given that you have only two scopes: global scope filters and Razor Page scope filters. For Razor Pages, global scope filters run the Executing and PageHandlerSelected methods first, followed by the page scope filters. For the Executed methods, the filters run in reverse order.
Razor Pages 的排序稍微简单一些,因为你只有两个范围:全局范围筛选器和 Razor Page 范围筛选器。对于 Razor Pages,全局范围筛选器首先运行 Executing 和 PageHandlerSelected 方法,然后运行页面范围筛选器。对于 Executed 方法,筛选器按相反的顺序运行。

You’ll sometimes find you need a bit more control over this order, especially if you have, for example, multiple action filters applied at the same scope. The filter pipeline caters to this requirement by way of the IOrderedFilter interface.
您有时会发现需要对此顺序进行更多控制,尤其是在您在同一范围内应用了多个作筛选器时。筛选器管道通过 IOrderedFilter 接口满足此要求。

21.6.2 Overriding the default order of filter execution with IOrderedFilter

21.6.2 使用 IOrderedFilter 覆盖过滤器执行的默认顺序

Filters are great for extracting cross-cutting concerns from your controller actions and Razor Page, but if you have multiple filters applied to an action, you’ll often need to control the precise order in which they execute.
筛选器非常适合从控制器作和 Razor Page 中提取横切关注点,但如果将多个筛选器应用于一个作,则通常需要控制它们的执行精确顺序。

Scope can get you some of the way, but for those other cases, you can implement IOrderedFilter. This interface consists of a single property, Order:
Scope 可以为您提供一些方法,但对于其他情况,您可以实现 IOrderedFilter。此接口由单个属性 Order 组成:

public interface IOrderedFilter
{
    int Order { get; }
}

You can implement this property in your filters to set the order in which they execute. The filter pipeline orders the filters in each stage based on the Order property first, from lowest to highest, and uses the default scope order to handle ties, as shown in figure 21.5.
您可以在筛选条件中实现此属性,以设置筛选条件的执行顺序。filter 管道首先根据 Order 属性从最低到最高对每个阶段中的过滤器进行排序,并使用默认的作用域 order 来处理平局,如图 21.5 所示。

alt text

Figure 21.5 Controlling the filter order for a stage using the IOrderedFilter interface. Filters are ordered by the Order property first, and then by scope.
图 21.5 使用 IOrderedFilter 接口控制阶段的过滤器顺序。筛选器首先按 Order 属性排序,然后按范围排序。

The filters for Order = -1 execute first, as they have the lowest Order value. The controller filter executes first because it has a broader scope than the action-scope filter. The filters with Order = 0 execute next, in the default scope order, as shown in figure 21.5. Finally, the filter with Order = 1 executes.
首先执行 Order = -1 的筛选器,因为它们具有最低的 Order 值。首先执行 controller 筛选器,因为它的范围比 action-scope 筛选器更广。Order = 0 的 filters 接下来以默认的 scope 顺序执行,如图 21.5 所示。最后,执行 Order = 1 的筛选器。

By default, if a filter doesn’t implement IOrderedFilter, it’s assumed to have Order = 0. All the filters that ship as part of ASP.NET Core have Order = 0, so you can implement your own filters relative to these.
默认情况下,如果筛选器未实现 IOrderedFilter,则假定其 Order = 0。作为 ASP.NET Core 的一部分提供的所有筛选器的 Order = 0,因此您可以实现自己的筛选器。

NOTE You can completely customize how the filter pipeline is built by customizing the MVC frameworks application model conventions. These control everything about how controllers and Razor Pages are discovered, how they’re added to the pipeline, and how filters are discovered. This is an advanced concept, that you won’t often need, but it may occasionally come in handy. You can read about the MVC application model in the documentation at http://mng.bz/nWNa.
注意:您可以通过自定义 MVC 框架应用程序模型约定来完全自定义筛选器管道的构建方式。它们控制有关如何发现控制器和 Razor 页面、如何将它们添加到管道以及如何发现筛选器的所有内容。这是一个高级概念,您通常不需要它,但它偶尔可能会派上用场。您可以在 http://mng.bz/nWNa 的文档 中阅读有关 MVC 应用程序模型的信息。

This chapter has provided a lot of background on the MVC filter pipeline, and we covered most of the technical details you need to use filters and create custom implementations for your own application. In chapter 22 you’ll see some of the built-in filters provided by ASP.NET Core, as well as some practical examples of filters you might want to use in your own applications.
本章提供了许多关于 MVC 过滤器管道的背景知识,我们介绍了使用过滤器和为自己的应用程序创建自定义实现所需的大部分技术细节。在第 22 章中,您将看到 ASP.NET Core 提供的一些内置过滤器,以及您可能希望在自己的应用程序中使用的一些过滤器的实际示例。

21.7 Summary

21.7 总结

The filter pipeline provides hooks into an MVC request so you can run functions at various points within an MVC request. With filters you can run code at specific points in the MVC process across all requests or a subset of requests. This is particularly useful for handling cross-cutting concerns that are specific to MVC.
筛选器管道提供 MVC 请求的挂钩,以便您可以在 MVC 请求中的不同点运行函数。使用筛选器,您可以在 MVC 进程中的特定点跨所有请求或请求子集运行代码。这对于处理特定于 MVC 的横切关注点特别有用。

The filter pipeline executes as part of the MVC or Razor Pages execution. It consists of authorization filters, resource filters, action filters, page filters, exception filters, and result filters. Each filter type is grouped in a stage and can be used to achieve effects specific to that stage.
筛选器管道作为 MVC 或 Razor Pages 执行的一部分执行。它由授权筛选条件、资源筛选条件、作筛选条件、页面筛选条件、异常筛选条件和结果筛选条件组成。每种滤镜类型都分组在一个阶段中,可用于实现特定于该阶段的效果。

Resource, action, and result filters run twice in the pipeline: an Executing method on the way in and an Executed method on the way out. Page filters run three times: after page handler selection, and before and after page handler execution.
资源、作和结果筛选器在管道中运行两次:一个 Executing 方法在流入中,一个 Executed 方法在流出。页面过滤器运行三次:选择页面处理程序之后,以及页面处理程序执行之前和之后。

Authorization and exception filters run only once as part of the pipeline; they don’t run after a response has been generated.
授权和异常筛选器仅作为管道的一部分运行一次;它们在生成响应后不会运行。

Each type of filter has both a sync and an async version. For example, resource filters can implement either the IResourceFilter interface or the IAsync-ResourceFilter interface. You should use the synchronous interface unless your filter needs to use asynchronous method calls.
每种类型的筛选器都有同步版本和异步版本。例如,资源筛选器可以实现 IResourceFilter 接口或 IAsync-ResourceFilter 接口。除非 filter 需要使用异步方法调用,否则应使用 synchronous interface。

You can add filters globally, at the controller level, at the Razor Page level, or at the action level. This is called the scope of the filter. Which scope you should choose depends on how broadly you want to apply the filter.
您可以在控制器级别、Razor Page 级别或作级别全局添加筛选器。这称为筛选器的范围。您应该选择哪个范围取决于您要应用过滤器的范围。

Within a given stage, global-scoped filters run first, then controller-scoped, and finally action-scoped. You can also override the default order by implementing the IOrderedFilter interface. Filters run from lowest to highest Order and use scope to break ties.
在给定的阶段中,全局范围的过滤器首先运行,然后是控制器范围的,最后是作范围的。您还可以通过实现 IOrderedFilter 接口来覆盖默认顺序。筛选器从最低顺序到最高顺序运行,并使用范围来打破关系。

ASP.NET Core in Action 20 Creating an HTTP API using web API controllers

20 Creating an HTTP API using web API controllers
20 使用 Web API 控制器创建 HTTP API

This chapter covers
本章涵盖
• Creating a web API controller to return JavaScript Object Notation (JSON) to clients
创建 Web API 控制器以将 JavaScript 对象表示法 (JSON) 返回给客户端
• Using attribute routing to customize your URLs
使用属性路由自定义 URL
• Generating a response using content negotiation
使用内容协商生成响应
• Applying common conventions with the [ApiController] attribute
使用 [ApiController] 属性应用常见约定

In chapters 13 through 19 you worked through each layer of a server-side rendered ASP.NET Core application, using Razor Pages and Model-View-Controller (MVC) controllers to render HTML to the browser. In part 1 of this book you saw a different type of ASP.NET Core application, using minimal APIs to serve JSON for client-side SPAs or mobile apps. In this chapter you’ll learn about web API controllers, which fit somewhere in between!
在第 13 章到第 19 章中,您完成了 ASP.NET Core 应用程序呈现的服务器端的每一层,使用 Razor Pages 和模型-视图-控制器 (MVC) 控制器将 HTML 呈现到浏览器。在本书的第 1 部分中,您了解了不同类型的 ASP.NET Core 应用程序,它使用最少的 API 为客户端 SPA 或移动应用程序提供 JSON。在本章中,您将了解 Web API 控制器,它们介于两者之间!

You can apply much of what you’ve already learned to web API controllers; they use the same routing system as minimal APIs and the same MVC design pattern, model binding, and validation as Razor Pages.
您可以将已经学到的大部分知识应用到 Web API 控制器中;它们使用与最小 API 相同的路由系统,以及与 Razor Pages 相同的 MVC 设计模式、模型绑定和验证。

In this chapter you’ll learn how to define web API controllers and actions, and see how similar they are to the Razor Pages and controllers you already know. You’ll learn how to create an API model to return data and HTTP status codes in response to a request, in a way that client apps can understand.
在本章中,您将学习如何定义 Web API 控制器和作,并了解它们与您已经知道的 Razor Pages 和控制器的相似之处。您将学习如何创建 API 模型,以客户端应用程序可以理解的方式返回数据和 HTTP 状态代码以响应请求。

After exploring how the MVC design pattern applies to web API controllers, you’ll see how a related topic works with web APIs: routing. We’ll look at how explicit attribute routing works with action methods, touching on many of the same concepts we covered in chapters 6 and 14.
在探索了 MVC 设计模式如何应用于 Web API 控制器之后,您将了解相关主题如何与 Web API 配合使用:路由。我们将了解显式属性路由如何与 action 方法一起工作,并涉及我们在第 6 章和第 14 章中介绍的许多相同概念。

One of the big features added in ASP.NET Core 2.1 was the [ApiController] attribute. This attribute applies several common conventions used in web APIs, reducing the amount of code you must write yourself. In section 20.5 you’ll learn how automatic 400 Bad Requests for invalid requests, model-binding parameter inference, and ProblemDetails support make building APIs easier and more consistent.
ASP.NET Core 2.1 中添加的重要功能之一是 [ApiController] 属性。此属性应用 Web API 中使用的几个常见约定,从而减少您必须自己编写的代码量。在第 20.5 节中,您将了解针对无效请求的自动 400 错误请求、模型绑定参数推理和 ProblemDetails 支持如何使构建 API 更轻松、更一致。

You’ll also learn how to format the API models returned by your action methods using content negotiation, to ensure that you generate a response that the calling client can understand. As part of this, you’ll learn how to add support for additional format types, such as Extensible Markup Language (XML), so that you can generate XML responses if the client requests it.
您还将学习如何使用内容协商来格式化作方法返回的 API 模型,以确保您生成的响应是调用客户端可以理解的。作为其中的一部分,您将学习如何添加对其他格式类型(如可扩展标记语言 (XML))的支持,以便在客户端请求时生成 XML 响应。

Finally, I discuss some of the differences between API controllers and minimal API applications, and when you should choose one over the other. Before we get to that, we look at how to get started. In section 20.1 you’ll see how to create a web API project and add your first API controller.
最后,我将讨论 API 控制器和最小 API 应用程序之间的一些差异,以及何时应该选择一个而不是另一个。在开始之前,我们先看看如何开始。在 Section 20.1 中,您将看到如何创建 Web API 项目并添加您的第一个 API 控制器。

20.1 Creating your first web API project

20.1 创建您的第一个 Web API 项目

In this section you’ll learn how to create an ASP.NET Core web API project and create your first web API controllers. You’ll see how to use controller action methods to handle HTTP requests and how to use ActionResults to generate a response.
在本部分中,你将了解如何创建 ASP.NET Core Web API 项目并创建你的第一个 Web API 控制器。您将了解如何使用控制器作方法处理 HTTP 请求,以及如何使用 ActionResults 生成响应。

NOTE as I mentioned previously that a web API project is a standard ASP.NET Core project, which uses the MVC framework and web API controllers.
注意:如前所述,Web API 项目是标准的 ASP.NET Core 项目,它使用 MVC 框架和 Web API 控制器。

Some people think of the MVC design pattern as applying only to applications that render their UI directly, like the Razor views you’ve seen in previous chapters or MVC controllers with Razor views. However, in ASP.NET Core, I feel the MVC pattern applies equally well when building a web API. For web APIs, the view part of the MVC pattern involves generating a machine-friendly response rather than a user-friendly response.
有些人认为 MVC 设计模式仅适用于直接呈现其 UI 的应用程序,例如您在前几章中看到的 Razor 视图或具有 Razor 视图的 MVC 控制器。但是,在 ASP.NET Core 中,我觉得 MVC 模式在构建 Web API 时同样适用。对于 Web API,MVC 模式的视图部分涉及生成计算机友好的响应,而不是用户友好的响应。

As a parallel to this, you create web API controllers in ASP.NET Core in the same way you create traditional MVC controllers. The only thing that differentiates them from a code perspective is the type of data they return. MVC controllers typically return a ViewResult; web API controllers generally return raw .NET objects from their action methods, or an IActionResult instance such as StatusCodeResult, as you saw in chapter 15.
与此并行,您可以在 ASP.NET Core 中创建 Web API 控制器,其方式与创建传统 MVC 控制器的方式相同。从代码角度来看,它们的唯一区别是它们返回的数据类型。MVC 控制器通常返回 ViewResult;Web API 控制器通常从其作方法或 IActionResult 实例(如 StatusCodeResult)返回原始 .NET 对象,如第 15 章所示。

You can create a new web API project in Visual Studio using the same process you’ve seen previously in Visual Studio. Choose File > New, and in the Create a new project dialog box, select the ASP.NET Core Web API template. Enter your project name in the Configure your new project dialog box, and review the Additional information box, shown in figure 20.1, before choosing Create. If you’re using the command-line interface (CLI), you can create a similar template using dotnet new webapi.
您可以使用之前在 Visual Studio 中看到的相同过程在 Visual Studio 中创建新的 Web API 项目。选择 File > New,然后在 Create a new project (创建新项目) 对话框中,选择 ASP.NET Core Web API 模板。在 Configure your new project (配置您的新项目) 对话框中输入您的项目名称,然后查看 Additional information (其他信息) 框,如图 20.1 所示,然后选择 Create (创建)。如果您使用的是命令行界面 (CLI),则可以使用 dotnet new webapi 创建类似的模板。

alt text

Figure 20.1 The Additional information screen. This screen follows on from the Configure your new project dialog box and lets you customize the template that generates your application.
图 20.1 “其他信息”屏幕。此屏幕是 Configure your new project 对话框的后续屏幕,允许您自定义生成应用程序的模板。

The web API template configures the ASP.NET Core project for web API controllers only in Program.cs, as shown in listing 20.1. If you compare this template with the MVC controller project in chapter 19, you’ll see that the web API project uses AddControllers() instead of AddControllersWithViews(). This adds only the services needed for controllers but omits the services for rendering Razor views. Also, the API controllers are added using MapControllers() instead of MapControllerRoute(), as web API controller typically use explicit routing instead of conventional routing. The default web API template also adds the OpenAPI services and endpoints required by the Swagger UI, as you saw in chapter 11.
Web API 模板仅在 Program.cs 中为 Web API 控制器配置 ASP.NET Core 项目,如清单 20.1 所示。如果将此模板与第 19 章中的 MVC 控制器项目进行比较,你将看到 Web API 项目使用 AddControllers() 而不是 AddControllersWithViews()。这仅添加控制器所需的服务,但省略了用于呈现 Razor 视图的服务。此外,API 控制器是使用 MapControllers() 而不是 MapControllerRoute() 添加的,因为 Web API 控制器通常使用显式路由而不是传统路由。默认 Web API 模板还添加了 Swagger UI 所需的 OpenAPI 服务和端点,如第 11 章所示。

Listing 20.1 Program.cs for the default web API project
清单 20.1 默认 Web API 项目的 Program.cs

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();  #A

builder.Services.AddEndpointsApiExplorer();    #B
builder.Services.AddSwaggerGen();    #B

WebApplication app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();    #C
    app.UseSwaggerUI();  #C
}

app.UseHttpsRedirection();
app.UseAuthorization();

app.MapControllers();  #D

app.Run();

❶ AddControllers adds the necessary services for web API controllers to your application.
AddControllers 将 Web API 控制器的必要服务添加到您的应用程序中。
❷ Adds services required to generate the Swagger/OpenAPI specification document
添加生成 Swagger/OpenAPI 规范文档所需的服务
❸ Adds Swagger UI middleware for exploring your web API endpoints
添加用于探索 Web API 端点的 Swagger UI 中间件
❹ MapControllers configures the web API controller actions in your app as endpoints.
MapControllers 将应用程序中的 Web API 控制器作配置为端点。

The program in listing 20.1 instructs your application to find all the web API controllers in your application and configure them in the EndpointMiddleware. Each action method becomes an endpoint and can receive requests when the RoutingMiddleware maps an incoming URL to the action method.
清单 20.1 中的程序指示您的应用程序查找应用程序中的所有 Web API 控制器,并在 EndpointMiddleware 中配置它们。每个作方法都成为一个端点,当 RoutingMiddleware 将传入的 URL 映射到作方法时,可以接收请求。

NOTE Technically, you can include Razor Pages, minimal APIs, and web API controllers in the same app, but I prefer to keep them separate where possible. There are certain aspects (such as error handling and authentication) that are made easier by keeping them separate. Of course, running two separate applications has its own difficulties!
注意:从技术上讲,您可以在同一个应用程序中包含 Razor Pages、最小 API 和 Web API 控制器,但我更喜欢尽可能将它们分开。通过将某些方面 (例如错误处理和身份验证) 分开,可以更轻松地使用它们。当然,运行两个单独的应用程序有其自身的困难!

You can add a web API controller to your project by creating a new .cs file anywhere in your project. Traditionally, this file is placed in a folder called Controllers, but that’s not a technical requirement.
您可以通过在项目中的任何位置创建新的 .cs 文件,将 Web API 控制器添加到项目中。传统上,此文件放置在名为 Controllers 的文件夹中,但这不是技术要求。

Tip Vertical slice architecture and feature folders are (fortunately) becoming more popular in .NET circles. With these approaches, you organize your project based on features instead of technical concepts like controllers and models.
提示:垂直切片体系结构和功能文件夹(幸运的是)在 .NET 圈子中越来越流行。使用这些方法,您可以根据功能而不是控制器和模型等技术概念来组织项目。

Listing 20.2 shows an example of a simple controller, with a single endpoint, that returns an IEnumerable when executed. This example highlights the similarity with traditional MVC controllers (using action methods and a base class) and minimal APIs (returning the response object directly to be serialized later).
清单 20.2 展示了一个简单的控制器示例,它只有一个端点,执行时返回一个 IEnumerable。此示例突出了与传统 MVC 控制器(使用作方法和基类)和最小 API(直接返回响应对象以供稍后序列化)的相似性。

Listing 20.2 A simple web API controller

[ApiController]    #A
public class FruitController : ControllerBase         #B
{
    List<string> _fruit = new List<string>    #C
    {                                         #C
        "Pear",                               #C
        "Lemon",                              #C
        "Peach"                               #C
    };                                        #C
    [HttpGet("fruit")]                           #D
    public IEnumerable<string> Index()      #E
    {                      #F
        return _fruit;     #F
    }                      #F
}

❶ The [ApiController] attribute opts in to common conventions.
[ApiController] 属性选择加入常见约定。
❷ The ControllerBase class provides helper functions.
ControllerBase 类提供了帮助程序函数。
❸ This would typically come from a dependency injection (DI) injected service instead.
这通常来自依赖注入 (DI) 注入的服务。
❹ The [HttpGet] attribute defines the route template used to call the action.
[HttpGet] 属性定义用于调用作的路由模板。
❺ The name of the action method, Index, isn’t used for routing. It can be anything you like.
action方法的名称 Index 不用于路由。它可以是你喜欢的任何东西。
❻ The controller exposes a single action method that returns the list of fruit.
控制器公开一个返回 fruit 列表的 action 方法。

When invoked, this endpoint returns the list of strings serialized to JSON, as shown in figure 20.2.
调用时,此终端节点返回序列化为 JSON 的字符串列表,如图 20.2 所示。

alt text

Figure 20.2 Testing the web API in listing 20.2 by accessing the URL in the browser. A GET request is made to the /fruit URL, which returns a List<string> that is serialized to JSON.
图 20.2 通过在浏览器中访问 URL 来测试清单 20.2 中的 Web API。向 /fruit URL 发出 GET 请求,该请求返回序列化为 JSON 的 List<string>

Web API controllers typically use the [ApiController] attribute (introduced in .NET Core 2.1) and derive from the ControllerBase class. The base class provides several helper methods for generating results, and the [ApiController] attribute automatically applies some common conventions, as you’ll see in section 20.5.
Web API 控制器通常使用 [ApiController] 属性(在 .NET Core 2.1 中引入)并从 ControllerBase 类派生。基类提供了几个用于生成结果的帮助程序方法,并且 [ApiController] 属性会自动应用一些常见约定,如第 20.5 节所示。

Tip The Controller base class is typically used when you use MVC controllers with Razor views. You don’t need to return Razor views with web API controllers, so ControllerBase is the better option.
提示:将 Controller 基类通常用于 Razor 视图的 MVC 控制器。无需使用 Web API 控制器返回 Razor 视图,因此 ControllerBase 是更好的选择。

In listing 20.2 you can see that the action method, Index, returns a list of strings directly from the action method. When you return data from an action like this, you’re providing the API model for the request. The client will receive this data. It’s formatted into an appropriate response, a JSON representation of the list in the case of figure 20.2, and sent back to the browser with a 200 OK status code.
在清单 20.2 中,你可以看到作方法 Index 直接从作方法返回一个字符串列表。当您从此类作返回数据时,您将为请求提供 API 模型。客户端将收到此数据。它被格式化为适当的响应,即图 20.2 中列表的 JSON 表示形式,并使用 200 OK 状态代码发送回浏览器。

Tip Web API controllers format data as JSON by default. You’ll see how to format the returned data in other ways in section 20.6. Minimal API endpoints that return data directly (rather than via an IResult) will format data only as JSON; there are no other options.
提示:默认情况下,Web API 控制器将数据格式化为 JSON。您将在 Section 20.6 中看到如何以其他方式格式化返回的数据。直接返回数据(而不是通过 IResult)的最小 API 端点将仅将数据格式化为 JSON;没有其他选择。

The URL at which a web API controller action is exposed is handled in the same way as for traditional MVC controllers and Razor Pages: using routing. The [HttpGet("fruit")] attribute applied to the Index method indicates that the method should use the route template "fruit" and should respond to HTTP GET requests. You’ll learn more about attribute routing in section 20.4, but it’s similar to the minimal API routing that you’re already familiar with.
公开 Web API 控制器作的 URL 的处理方式与传统 MVC 控制器和 Razor Pages 的处理方式相同:使用路由。应用于 Index 方法的 [HttpGet(“fruit”)] 属性指示该方法应使用路由模板 “fruit” 并应响应 HTTP GET 请求。您将在 Section 20.4 中了解有关 attribute routing 的更多信息,但它类似于您已经熟悉的最小 API 路由。

In listing 20.2 data is returned directly from the action method, but you don’t have to do that. You’re free to return an IActionResult instead, and often this is required. Depending on the desired behavior of your API, you sometimes want to return data, and other times you may want to return a raw HTTP status code, indicating whether the request was successful. For example, if an API call is made requesting details of a product that does not exist, you might want to return a 404 Not Found status code.
在示例 20.2 中,数据直接从 action 方法返回,但你不必这样做。您可以自由地返回 IActionResult,这通常是必需的。根据 API 的所需行为,您有时希望返回数据,有时您可能希望返回原始 HTTP 状态代码,以指示请求是否成功。例如,如果进行 API 调用,请求不存在的产品的详细信息,则可能需要返回 404 Not Found 状态代码。

NOTE This is similar to the patterns you used in minimal APIs. But remember, minimal APIs use IResult, web API controllers, MVC controllers, and Razor Pages use IActionResult.
注意:这类似于您在最小 API 中使用的模式。但请记住,最小的 API 使用 IResult,Web API 控制器、MVC 控制器和 Razor 页面使用 IActionResult。

Listing 20.3 shows an example of where you must return an IActionResult. It shows another action on the same FruitController as before. This method exposes a way for clients to fetch a specific fruit by an id, which we’ll assume for this example is an index into the list of _fruit you defined in the previous listing. Model binding is used to set the value of the id parameter from the request.
清单 20.3 显示了一个必须返回 IActionResult 的示例。它显示了与以前一样,在同一个 FruitController 上的另一个作。此方法为 Client 端提供了一种通过 id 获取特定水果的方法,在本例中,我们假设该 ID 是您在上一个清单中定义的 _fruit 列表的索引。模型绑定用于设置请求中 id 参数的值。

NOTE API controllers use the same model binding infrastructure as Razor Pages to bind action method parameters to the incoming request. Model binding and validation work the same way you saw in chapter 16: you can bind the request to simple primitives, as well as to complex C# objects. The only difference is that there isn’t a PageModel with [BindProperty] properties; you can bind only to action method parameters.
注意:API 控制器使用与 Razor Pages 相同的模型绑定基础结构将作方法参数绑定到传入请求。模型绑定和验证的工作方式与第 16 章中介绍的相同:可以将请求绑定到简单的基元,也可以绑定到复杂的 C# 对象。唯一的区别是没有具有 [BindProperty] 属性的 PageModel;您只能绑定到作方法参数。

Listing 20.3 A web API action returning IActionResult to handle error conditions
列表 20.3 返回 IActionResult 以处理错误条件的 Web API action

[HttpGet("fruit/{id}")]                #A
public ActionResult<string> View(int id)    #B
{
    if (id >= 0 && id < _fruit.Count)   #C
    {
        return _fruit[id];    #D
    }
    return NotFound();    #E
}

❶ Defines the route template for the action method
定义action方法的路由模板
❷ The action method returns an ActionResult<string>, so it can return a string or an IActionResult.
作方法返回 ActionResult<string>,因此它可以返回字符串或 IActionResult。
❸ An element can be returned only if the id value is a valid _fruit element index.
仅当 id 值是有效的 _fruit 元素索引时,才能返回元素。
❹ Returning the data directly returns the data with a 200 status code.
返回数据直接返回状态代码为 200 的数据。
❺ NotFound returns a NotFoundResult, which sends a 404 status code.
NotFound 返回 NotFoundResult, ,这会发送 404 状态代码。

In the successful path for the action method, the id parameter has a value greater than 0 and less than the number of elements in _fruit. When that’s true, the value of the element is returned to the caller. As in listing 20.2, this is achieved by simply returning the data directly, which generates a 200 status code and returns the element in the response body, as shown in figure 20.3. You could also have returned the data using an OkResult, by returning Ok(_fruit[id]), using the Ok helper method on the ControllerBase class; under the hood, the result is identical.
在作方法的成功路径中, id 参数的值大于 0 且小于 _fruit 中的元素数。如果为 true,则元素的值将返回给调用方。与清单 20.2 一样,这是通过简单地直接返回数据来实现的,这会生成一个 200 状态代码并返回响应正文中的元素,如图 20.3 所示。您还可以通过 OkResult 返回 Ok(_fruit[id]),使用 ControllerBase 类上的 Ok 帮助程序方法返回数据;在后台,结果是相同的。

NOTE Some people get uneasy when they see the phrase helper method, but there’s nothing magic about the ControllerBase helpers; they’re shorthand for creating a new IActionResult of a given type. You don’t have to take my word for it, though. You can always view the source code for the base class on GitHub at http://mng.bz/5wQB.
注意:有些人在看到短语 helper method 时会感到不安,但 ControllerBase helpers 并没有什么神奇之处;它们是创建给定类型的新 IActionResult 的简写。不过,你不必相信我的话。您始终可以在 GitHub 上查看基类的源代码,网址为 http://mng.bz/5wQB

alt text

Figure 20.3 Data returned from an action method is serialized into the response body, and it generates a response with status code 200 OK.
图 20.3 从作方法返回的数据被序列化到响应正文中,并生成状态码为 200 OK 的响应。

If the id is outside the bounds of the _fruit list, the method calls NotFound() to create a NotFoundResult. When executed, this method generates a 404 Not Found status code response. The [ApiController] attribute automatically converts the response into a standard ProblemDetails instance, as shown in figure 20.4.
如果 ID 超出 _fruit 列表的边界,该方法将调用 NotFound() 来创建 NotFoundResult。执行时,此方法会生成 404 Not Found 状态代码响应。[ApiController] 属性会自动将响应转换为标准 ProblemDetails 实例,如图 20.4 所示。

alt text

Figure 20.4 The [ApiController] attribute converts error responses (in this case a 404 response) into the standard ProblemDetails format.
图 20.4 [ApiController] 属性将错误响应(在本例中为 404 响应)转换为标准 ProblemDetails 格式。

One aspect you might find confusing from listing 20.3 is that for the successful case, we return a string, but the method signature of View says we return an ActionResult<string>. How is that possible? Why isn’t there a compiler error?
清单 20.3 中您可能会感到困惑的一个方面是,对于成功的情况,我们返回一个字符串,但 View 的方法签名说我们返回一个 ActionResult<string>.这怎么可能呢?为什么没有编译器错误?

The generic ActionResult<T> uses some fancy C# gymnastics with implicit conversions to make this possible. Using ActionResult<T> has two benefits:
泛型ActionResult<T>使用一些带有隐式转换的花哨 C# 体来实现这一点。使用ActionResult<T> 有两个好处:

• You can return either an instance of T or an ActionResult implementation like NotFoundResult from the same method. This can be convenient, as in listing 20.3.
您可以从同一方法返回 T 的实例或 ActionResult 实现(如 NotFoundResult)。这很方便,如 清单 20.3 所示。
• It enables better integration with ASP.NET Core’s OpenAPI support.
它支持更好地与 ASP.NET Core 的 OpenAPI 支持集成。

You’re free to return any type of ActionResult from your web API controllers, but you’ll commonly return StatusCodeResult instances, which set the response to a specific status code, with or without associated data. NotFoundResult and OkResult both derive from StatusCodeResult, for example. Another commonly used status code is 400 Bad Request, which is normally returned when the data provided in the request fails validation. You can generate this using a BadRequestResult, but in many cases the [ApiController] attribute can automatically generate 400 responses for you, as you’ll see in section 20.5.
您可以自由地从 Web API 控制器返回任何类型的 ActionResult,但通常会返回 StatusCodeResult 实例,这些实例将响应设置为特定状态代码,无论是否包含关联数据。例如,NotFoundResult 和 OkResult 都派生自 StatusCodeResult。另一个常用的状态代码是 400 Bad Request,通常在请求中提供的数据未通过验证时返回。你可以使用 BadRequestResult 来生成它,但在许多情况下, [ApiController] 属性可以自动生成 400 个响应,如第 20.5 节所示。

Tip You learned about various ActionResults in chapter 15. BadRequestResult, OkResult, and NotFoundResult all inherit from StatusCodeResult and set the appropriate status code for their type (400, 200, and 404, respectively). Using these wrapper classes makes the intention of your code clearer than relying on other developers to understand the significance of the various status code numbers.
提示您在第 15 章中了解了各种 ActionResult。BadRequestResult、OkResult 和 NotFoundResult 都继承自 StatusCodeResult,并为其类型设置适当的状态代码(分别为 400、200 和 404)。使用这些包装类可以比依赖其他开发人员来了解各种状态代码编号的重要性更清楚地了解代码的意图。

Once you’ve returned an ActionResult (or other object) from your controller, it’s serialized to an appropriate response. This works in several ways, depending on
从控制器返回 ActionResult(或其他对象)后,它将被序列化为适当的响应。这以多种方式工作,具体取决于
• The formatters that your app supports
您的应用程序支持的格式化程序
• The data you return from your method
您从方法返回的数据
• The data formats the requesting client can handle
请求客户端可以处理的数据格式

You’ll learn more about formatters and serializing data in section 20.6, but before we go any further, it’s worth zooming out a little and exploring the parallels between traditional server-side rendered applications and web API endpoints. The two are similar, so it’s important to establish the patterns that they share and where they differ.
您将在 Section 20.6 中了解有关格式化程序和序列化数据的更多信息,但在我们进一步讨论之前,值得稍微缩小并探索传统服务器端渲染的应用程序和 Web API 端点之间的相似之处。这两者相似,因此确定它们的共同模式和不同之处非常重要。

20.2 Applying the MVC design pattern to a web API

20.2 将 MVC 设计模式应用于 Web API

In ASP.NET Core, the same underlying framework is used in conjunction with web API controllers, Razor Pages, and MVC controllers with views. You’ve already seen this yourself; the web API FruitController you created in section 20.2 looks similar to the MVC controllers you saw in chapter 19.
在 ASP.NET Core 中,相同的基础框架与 Web API 控制器、Razor Pages 和具有视图的 MVC 控制器结合使用。你自己已经见过了;您在第 20.2 节中创建的 Web API FruitController 看起来类似于您在第 19 章中看到的 MVC 控制器。

Consequently, even if you’re building an application that consists entirely of web APIs, using no server-side rendering of HTML, the MVC design pattern still applies. Whether you’re building traditional web applications or web APIs, you can structure your application virtually identically.
因此,即使您正在构建一个完全由 Web API 组成的应用程序,不使用服务器端的 HTML 呈现,MVC 设计模式仍然适用。无论您是构建传统的 Web 应用程序还是 Web API,您都可以以几乎相同的方式构建您的应用程序。

By now I hope you’re nicely familiar with how ASP.NET Core handles a request. But in case you’re not, figure 20.5 shows how the framework handles a typical Razor Pages request after it passes through the middleware pipeline. This example shows how a request to view the available fruit on a traditional grocery store website might look.
到目前为止,我希望您已经非常熟悉 ASP.NET Core 如何处理请求。但如果你不是,图 20.5 显示了框架在典型的 Razor Pages 请求通过中间件管道后如何处理该请求。此示例显示了在传统杂货店网站上查看可用水果的请求可能是什么样子的。

alt text

Figure 20.5 Handling a request to a traditional Razor Pages application, in which the view generates an HTML response that’s sent back to the user. This diagram should be familiar by now!
图 20.5 处理对传统 Razor Pages 应用程序的请求,其中视图生成 HTML 响应,该响应将发送回给用户。这张图现在应该很熟悉了!

The RoutingMiddleware routes the request to view all the fruit listed in the apples category to the Fruit.cshtml Razor Page. The EndpointMiddleware then constructs a binding model, validates it, sets it as a property on the Razor Page’s PageModel, and sets the ModelState property on the PageModel base class with details of any validation errors. The page handler interacts with the application model by calling into services, talking to a database, and fetching any necessary data.
RoutingMiddleware 将请求路由到 Fruit.cshtml Razor 页面,以查看 apples 类别中列出的所有水果。然后,EndpointMiddleware 构造一个绑定模型,对其进行验证,将其设置为 Razor 页面的 PageModel 上的属性,并在 PageModel 基类上设置 ModelState 属性,其中包含任何验证错误的详细信息。页面处理程序通过调用服务、与数据库通信以及获取任何必要的数据来与应用程序模型进行交互。

Finally, the Razor Page executes its Razor view using the PageModel to generate the HTML response. The response returns through the middleware pipeline and out to the user’s browser.
最后,Razor 页面使用 PageModel 执行其 Razor 视图以生成 HTML 响应。响应通过中间件管道返回并输出到用户的浏览器。

How would this change if the request came from a client-side or mobile application? If you want to serve machine-readable JSON instead of HTML, what is different for web API controllers? As shown in figure 20.6, the answer is “very little.” The main changes are related to switching from Razor Pages to controllers and actions, but as you saw in chapter 19, both approaches use the same general paradigms.
如果请求来自客户端或移动应用程序,情况会如何变化?如果要提供机器可读的 JSON 而不是 HTML,那么 Web API 控制器有什么不同?如图 20.6 所示,答案是 “very little”。主要更改与从 Razor Pages 切换到控制器和作有关,但正如您在第 19 章中看到的那样,这两种方法都使用相同的通用范例。

alt text

Figure 20.6 A call to a web API endpoint in an e-commerce ASP.NET Core web application. The ghosted portion of the diagram is identical to figure 20.5.
图 20.6 对电子商务 ASP.NET Core Web 应用程序中的 Web API 终端节点的调用。该图的重影部分与图 20.5 相同。

As before, the routing middleware selects an endpoint to invoke based on the incoming URL. For API controllers this is a controller and action instead of a Razor Page.
与以前一样,路由中间件根据传入 URL 选择要调用的终端节点。对于 API 控制器,这是控制器和作,而不是 Razor 页面。

After routing comes model-binding, in which the binder creates a binding model and populates it with values from the request. web API controllers often accept data in more formats than Razor Pages, such as XML, but otherwise the model-binding process is the same as for the Razor Pages request. Validation also occurs in the same way, and the ModelState property on the ControllerBase base class is populated with any validation errors.
路由之后是模型绑定,其中 Binder 创建一个绑定模型,并使用请求中的值填充它。Web API 控制器通常接受比 Razor Pages 更多格式的数据(例如 XML),但其他方面,模型绑定过程与 Razor Pages 请求相同。验证也以相同的方式进行,并且 ControllerBase 基类上的 ModelState 属性中填充了任何验证错误。

NOTE Web APIs use input formatters to accept data sent to them in a variety of formats. Commonly these formats are JSON or XML, but you can create input formatters for any sort of type, such as CSV. I show how to enable the XML input formatter in section 20.6. You can see how to create a custom input formatter at http://mng.bz/e5gG.
注意:Web API 使用输入格式化程序来接受以各种格式发送给它们的数据。这些格式通常为 JSON 或 XML,但您可以为任何类型的输入格式化程序创建,例如 CSV。我在 Section 20.6 中展示了如何启用 XML input 格式化程序。您可以在 http://mng.bz/e5gG 中了解如何创建自定义输入格式化程序。

The action method is the equivalent of the Razor Page handler; it interacts with the application model in the same way. This is an important point; by separating the behavior of your app into an application model instead of incorporating it into your pages and controllers themselves, you’re able to reuse the business logic of your application with multiple UI paradigms.
action方法等效于 Razor Page 处理程序;它以相同的方式与应用程序模型交互。这是很重要的一点;通过将应用程序的行为分离到应用程序模型中,而不是将其合并到页面和控制器本身中,您可以利用多个 UI 范例重用应用程序的业务逻辑。

Tip Where possible, keep your page handlers and controllers as simple as practicable. Move all your business logic decisions into the services that make up your application model, and keep your Razor Pages and API controllers focused on the mechanics of interacting with a user or client.
提示:在可能的情况下,请尽可能保持页面处理程序和控制器的简单性。将所有业务逻辑决策移动到构成应用程序模型的服务中,并使 Razor Pages 和 API 控制器专注于与用户或客户端交互的机制。

After the application model has returned the data necessary to service the request—the fruit objects in the apples category—you see the first significant difference between API controllers and Razor Pages. Instead of adding values to the PageModel to be used in a Razor view, the action method creates an API model. This is analogous to the PageModel, but rather than containing data used to generate an HTML view, it contains the data that will be sent back in the response.
在应用程序模型返回为请求提供服务所需的数据(apples 类别中的 fruit 对象)后,您会看到 API 控制器和 Razor Pages 之间的第一个显著差异。作方法不是向 PageModel 添加值以在 Razor 视图中使用的,而是创建一个 API 模型。这类似于 PageModel,但它不包含用于生成 HTML 视图的数据,而是包含将在响应中发回的数据。

DEFINITION View models and PageModels contain both the data required to build a response and metadata about how to build the response. API models typically contain only the data to be returned in the response.
定义:视图模型和 PageModel 包含构建响应所需的数据以及有关如何构建响应的元数据。API 模型通常仅包含要在响应中返回的数据。

When we looked at the Razor Pages app, we used the PageModel in conjunction with a Razor view template to build the final response. With the web API app, we use the API model in conjunction with an output formatter. An output formatter, as the name suggests, serializes the API model into a machine-readable response, such as JSON or XML. The output formatter forms the V in the web API version of MVC by choosing an appropriate representation of the data to return.
当我们查看 Razor Pages 应用时,我们将 PageModel 与 Razor 视图模板结合使用来构建最终响应。对于 Web API 应用程序,我们将 API 模型与输出格式化程序结合使用。顾名思义,输出格式化程序将 API 模型序列化为机器可读的响应,例如 JSON 或 XML。输出格式化程序通过选择要返回的数据的适当表示形式,在 MVC 的 Web API 版本中形成 V。

Finally, as for the Razor Pages app, the generated response is sent back through the middleware pipeline, passing through each of the configured middleware components, and back to the original caller.
最后,对于 Razor Pages 应用,生成的响应通过中间件管道发送回,通过每个配置的中间件组件,并返回给原始调用方。

I hope the parallels between Razor Pages and web APIs are clear. The majority of the behavior is identical; only the response varies. Everything from when the request arrives to the interaction with the application model is similar between the paradigms.
我希望 Razor Pages 和 Web API 之间的相似之处是明确的。大多数行为是相同的;只是反应不同。从请求到达到与应用程序模型的交互,范例之间的一切都是相似的。

Most of the differences between Razor Pages and web APIs have less to do with the way the framework works under the hood and are instead related to how the different paradigms are used. For example, in the next section you’ll learn how the routing constructs you learned about in chapters 6 and 15 are used with web APIs, using attribute routing.
Razor Pages 和 Web API 之间的大多数差异与框架在后台的工作方式关系不大,而是与不同范例的使用方式有关。例如,在下一节中,您将了解如何使用属性路由将您在第 6 章和第 15 章中学到的路由结构与 Web API 一起使用。

20.3 Attribute routing: Linking action methods to URLs

20.3 属性路由:将作方法链接到 URL

In this section you’ll learn about attribute routing: the mechanism for associating web API controller actions with a given route template. You’ll see how to associate controller actions with specific HTTP verbs like GET and POST and how to avoid duplication in your templates.
在本节中,您将了解属性路由:将 Web API 控制器作与给定路由模板关联的机制。您将了解如何将控制器作与特定的 HTTP 动词(如 GET 和 POST)相关联,以及如何避免模板中的重复。

We covered route templates in depth in chapter 6 in the context of minimal APIs, and again in chapter 14 with Razor Pages, and you’ll be pleased to know that you use exactly the same route templates with API controllers. The only difference is how you specify the templates. With Razor Pages you use the @page directive, and with minimal APIs you use MapGet() or MapPost(), whereas with API controllers you use routing attributes.
我们在第 6 章中深入介绍了最小 API 的路由模板,并在第 14 章中再次介绍了 Razor Pages,您会很高兴地知道您对 API 控制器使用完全相同的路由模板。唯一的区别是指定模板的方式。对于 Razor Pages,您可以使用 @page 指令,对于最少的 API,您可以使用 MapGet() 或 MapPost(),而对于 API 控制器,您可以使用路由属性。

NOTE All three paradigms use explicit routing under the hood. The alternative, conventional routing, is typically used with traditional MVC controllers and views, as described in chapter 19. As I’ve mentioned, I don’t recommend using that approach generally, so I don’t cover conventional routing in this book.
注意:这三种范例都在后台使用显式路由。另一种选择,即传统路由,通常与传统的 MVC 控制器和视图一起使用,如第 19 章所述。正如我所提到的,我不建议通常使用这种方法,因此我在本书中不介绍传统路由。

With attribute routing, you decorate each action method in an API controller with an attribute and provide the associated route template for the action method, as shown in the following listing.
使用属性路由,您可以使用属性修饰 API 控制器中的每个作方法,并为作方法提供关联的路由模板,如下面的清单所示。

Listing 20.4 Attribute routing example
列表 20.4 属性路由示例

public class HomeController: Controller
{
    [Route("")]                  #A
    public IActionResult Index()
    {
         /* method implementation*/
    }

    [Route("contact")]             #B
    public IActionResult Contact()
    {
         /* method implementation*/
    }
}

❶ The Index action will be executed when the / URL is requested.
请求 / URL 时,将执行 Index action。
❷ The Contact action will be executed when the /contact URL is requested.
请求 /contact URL 时,将执行 Contact action。

Each [Route] attribute defines a route template that should be associated with the action method. In the example provided, the / URL maps directly to the Index method and the /contact URL maps to the Contact method.
每个 [Route] 属性都定义一个应与作方法关联的路由模板。在提供的示例中,/ URL 直接映射到 Index 方法,而 /contact URL 映射到 Contact 方法。

Attribute routing maps URLs to a specific action method, but a single action method can still have multiple route templates and hence can correspond to multiple URLs. Each template must be declared with its own RouteAttribute, as shown in this listing, which shows the skeleton of a web API for a car-racing game.
属性路由将 URL 映射到特定的作方法,但单个作方法仍可以具有多个路由模板,因此可以对应于多个 URL。每个模板都必须使用自己的 RouteAttribute 进行声明,如下面的清单所示,它显示了一个赛车游戏的 Web API 的框架。

Listing 20.5 Attribute routing with multiple attributes
示例 20.5 具有多个属性的属性路由

public class CarController
{
    [Route("car/start")]         #A
    [Route("car/ignition")]      #A
    [Route("start-car")]         #A
    public IActionResult Start()    #B
    {
         /* method implementation*/
    }

    [Route("car/speed/{speed}")]             #C
    [Route("set-speed/{speed}")]             #C
    public IActionResult SetCarSpeed(int speed)  
    {
         /* method implementation*/
    }
}

❶ The Start method will be executed when any of these route templates is matched.
当这些路由模板中的任何一个匹配时,将执行 Start 方法。
❷ The name of the action method has no effect on the route template.
action方法的名称对路由模板没有影响。
❸ The RouteAttribute template can contain route parameters, in this case {speed}.
RouteAttribute 模板可以包含路由参数,在本例中为 {speed}。

The listing shows two different action methods, both of which can be accessed from multiple URLs. For example, the Start method will be executed when any of the following URLs is requested:
该列表显示了两种不同的作方法,这两种方法都可以从多个 URL 访问。例如,当请求以下任一 URL 时,将执行 Start 方法:

/car/start
/car/ignition
/start-car

These URLs are completely independent of the controller and action method names; only the value in the RouteAttribute matters.
这些 URL 完全独立于控制器和作方法名称;只有 RouteAttribute 中的值才重要。

NOTE By default, the controller and action name have no bearing on the URLs or route templates when RouteAttributes are used.
注意:默认情况下,当使用 RouteAttributes 时,控制器和作名称与 URL 或路由模板无关。

The templates used in route attributes are standard route templates, the same as you used in chapter 6. You can use literal segments, and you’re free to define route parameters that will extract values from the URL, as shown by the SetCarSpeed method in listing 20.5. That method defines two route templates, both of which define a route parameter, {speed}.
路由属性中使用的模板是标准路由模板,与您在第 6 章中使用的模板相同。您可以使用文字段,并且可以自由定义将从 URL 中提取值的路由参数,如清单 20.5 中的 SetCarSpeed 方法所示。该方法定义了两个路由模板,这两个模板都定义了一个路由参数 {speed}。

Tip I’ve used multiple [Route] attributes on each action in this example, but it’s best practice to expose your action at a single URL. This will make your API easier to understand and for other applications to consume.
提示:在此示例中,我对每个作使用了多个 [Route] 属性,但最佳做法是在单个 URL 上公开您的作。这将使您的 API 更易于理解,并可供其他应用程序使用。

As in all parts of ASP.NET Core, route parameters represent a segment of the URL that can vary. As with minimal APIs, and Razor Pages, the route parameters in your RouteAttribute templates can
与 ASP.NET Core 的所有部分一样,路由参数表示 URL 中可以变化的一段。与最少的 API 和 Razor Pages 一样,RouteAttribute 模板中的路由参数可以

• Be optional
• Have default values
• Use route constraints

For example, you could update the SetCarSpeed method in the previous listing to constrain {speed} to an integer and to default to 20 like so:
例如,您可以更新上一个清单中的 SetCarSpeed 方法,将 {speed} 约束为整数,并默认为 20,如下所示:

[Route("car/speed/{speed=20:int}")]
[Route("set-speed/{speed=20:int}")]
public IActionResult SetCarSpeed(int speed)

NOTE As discussed in chapter 6, don’t use route constraints for validation. For example, if you call the preceding "set-speed/{speed=20:int}" route with an invalid value for speed, /set-speed/oops, you will get a 404 Not Found response, as the route does not match. Without the int constraint, you would receive the more sensible 400 Bad Request response.
注意:如第 6 章所述,不要使用 route constraints 进行验证。例如,如果您使用无效的 speed 值 /set-speed/oops 调用前面的 “set-speed/{speed=20:int}” 路由,您将收到 404 Not Found 响应,因为路由不匹配。如果没有 int 约束,您将收到更明智的 400 Bad Request 响应。

If you managed to get your head around routing in chapter 6, routing with web API controllers shouldn’t hold any surprises for you. One thing you might begin noticing when you start using attribute routing with web API controllers is the amount you repeat yourself. Minimal APIs use route groups to reduce duplication, and Razor Pages removes a lot of the repetition by using conventions to calculate route templates based on the Razor Page’s filename. So what can we use with web API controllers?
如果您在第 6 章中设法了解了路由,那么使用 Web API 控制器进行路由应该不会给您带来任何惊喜。当您开始将属性路由与 Web API 控制器一起使用时,您可能会开始注意到的一件事是您自己重复的量。最小 API 使用路由组来减少重复,而 Razor Pages 通过使用约定根据 Razor Page 的文件名计算路由模板来消除大量重复。那么我们可以用什么来配合 Web API 控制器呢?

20.3.1 Combining route attributes to keep your route templates DRY

20.3.1 组合路由属性以保持路由模板 DRY

Adding route attributes to all of your web API controllers can get a bit tedious, especially if you’re mostly following conventions where your routes have a standard prefix, such as "api" or the controller name. Generally, you’ll want to ensure that you don’t repeat yourself (DRY) when it comes to these strings. The following listing shows two action methods with several [Route] attributes. (This is for demonstration purposes only. Stick to one per action if you can!)
向所有 Web API 控制器添加路由属性可能会有点乏味,尤其是当您主要遵循路由具有标准前缀(例如“api”或控制器名称)的约定时。通常,您需要确保在涉及这些字符串时不会重复自己 (DRY)。下面的列表显示了具有多个 [Route] 属性的两种作方法。(这仅用于演示目的。如果可以的话,每个动作坚持一个!

Listing 20.6 Duplication in RouteAttribute templates
列表 20.6 RouteAttribute 模板中的重复

public class CarController
{
    [Route("api/car/start")]             #A
    [Route("api/car/ignition")]          #A
    [Route("start-car")]
    public IActionResult Start()
    {
         /* method implementation*/
    }

    [Route("api/car/speed/{speed}")]     #A
    [Route("set-speed/{speed}")]
    public IActionResult SetCarSpeed(int speed)
    {
         /* method implementation*/
    }
}

❶ Multiple route templates use the same “api/car” prefix.
多个路由模板使用相同的 “api/car” 前缀。

There’s quite a lot of duplication here; you’re adding "api/car" to most of your routes. Presumably, if you decided to change this to "api/vehicles", you’d have to go through each attribute and update it. Code like that is asking for a typo to creep in!
这里有很多重复;您正在将 “api/car” 添加到大多数路线中。据推测,如果您决定将其更改为 “api/vehicles”,则必须检查每个属性并更新它。像这样的代码就是要求一个拼写错误悄悄溜进来!

To alleviate this pain, it’s possible to apply RouteAttributes to controllers, in addition to action methods. When a controller and an action method both have a route attribute, the overall route template for the method is calculated by combining the two templates.
为了减轻这种痛苦,除了作方法之外,还可以将 RouteAttributes 应用于控制器。当控制器和作方法都具有 route 属性时,该方法的总体路由模板是通过组合两个模板来计算的。

Listing 20.7 Combining RouteAttribute templates
示例 20.7 组合 RouteAttribute 模板

[Route("api/car")]
public class CarController
{
    [Route("start")]          #A
    [Route("ignition")]       #B
    [Route("/start-car")]          #C
    public IActionResult Start()
    {
         /* method implementation*/
    }

    [Route("speed/{speed}")]          #D
    [Route("/set-speed/{speed}")]                  #E
    public IActionResult SetCarSpeed(int speed)
    {
         /* method implementation*/
    }
}

❶ Combines to give “api/car/start”
❷ Combines to give “api/car/ignition”
❸ Does not combine because it starts with /; gives the “start-car” template
❹ Combines to give “api/car/speed/{speed}”
❺ Does not combine because it starts with /; gives the “set-speed/{speed}” template

Combining attributes in this way can reduce some of the duplication in your route templates and makes it easier to add or change the prefixes (such as switching "car" to "vehicle") for multiple action methods. To ignore the RouteAttribute on the controller and create an absolute route template, start your action method route template with a slash (/). Using a controller RouteAttribute reduces a lot of the duplication, but you can go one better by using token replacement.
以这种方式组合属性可以减少路线模板中的一些重复,并可以更轻松地为多个作方法添加或更改前缀(例如将 “car” 切换为 “vehicle”)。要忽略控制器上的 RouteAttribute 并创建绝对路由模板,请使用斜杠 (/) 启动作方法路由模板。使用控制器 RouteAttribute 可以减少很多重复,但您可以通过使用令牌替换来更好地进行一次重复。

20.3.2 Using token replacement to reduce duplication in attribute routing

20.3.2 使用令牌替换来减少属性路由中的重复

The ability to combine attribute routes is handy, but you’re still left with some duplication if you’re prefixing your routes with the name of the controller, or if your route templates always use the action name. If you wish, you can simplify even further!
组合属性路由的功能很方便,但如果您在路由前加上控制器的名称,或者如果您的路由模板始终使用作名称,则仍然会留下一些重复。如果您愿意,您可以进一步简化!

Attribute routes support the automatic replacement of [action] and [controller] tokens in your attribute routes. These will be replaced with the name of the action and the controller (without the “Controller” suffix), respectively. The tokens are replaced after all attributes have been combined, which can be useful when you have controller inheritance hierarchies. This listing shows how you can create a BaseController class that applies a consistent route template prefix to all the web API controllers in your application.
属性路由支持自动替换属性路由中的 [action] 和 [controller] 令牌。这些将分别替换为作和控制器的名称(不带 “Controller” 后缀)。在合并所有属性后,将替换令牌,这在具有控制器继承层次结构时非常有用。此清单显示了如何创建一个 BaseController 类,该类将一致的路由模板前缀应用于应用程序中的所有 Web API 控制器。

Listing 20.8 Token replacement in RouteAttributes
清单 20.8 RouteAttributes 中的 Token 替换

[Route("api/[controller]")]                      #A
public abstract class BaseController { }       #B

public class CarController : BaseController
{
    [Route("[action]")]          #C
    [Route("ignition")]             #D
    [Route("/start-car")]          #E
    public IActionResult Start()
    {
         /* method implementation*/
    }
}

❶ You can apply attributes to a base class, and derived classes will inherit them.
❷ Token replacement happens last, so [controller] is replaced with “car” not “base”.
❸ Combines and replaces tokens to give the “api/car/start” template
❹ Combines and replaces tokens to give the “api/car/ignition” template
❺ Does not combine with base attributes because it starts with /, so it remains as “start-car”

Warning If you use token replacement for [controller] or [action], remember that renaming classes and methods will change your public API. If that worries you, you can stick to using static strings like "car" instead.
警告:如果你对 [controller] 或 [action] 使用令牌替换,请记住重命名类和方法将更改你的公共 API。如果这让您感到担忧,您可以坚持使用像 “car” 这样的静态字符串。

When combined with everything you learned in chapter 6, we’ve covered pretty much everything there is to know about attribute routing. There’s just one more thing to consider: handling different HTTP request types like GET and POST.
结合您在第 6 章中学到的所有内容,我们几乎涵盖了有关属性路由的所有知识。还有一件事需要考虑:处理不同的 HTTP 请求类型,如 GET 和 POST。

20.3.3 Handling HTTP verbs with attribute routing

20.3.3 使用属性路由处理 HTTP 动词

In Razor Pages, the HTTP verb, such as GET or POST, isn’t part of the routing process. The RoutingMiddleware determines which Razor Page to execute based solely on the route template associated with the Razor Page. It’s only when a Razor Page is about to be executed that the HTTP verb is used to decide which page handler to execute: OnGet for the GET verb, or OnPost for the POST verb, for example.
在 Razor Pages 中,HTTP 谓词(如 GET 或 POST)不是路由过程的一部分。RoutingMiddleware 仅根据与 Razor Page 关联的路由模板来确定要执行的 Razor Page。仅当即将执行 Razor 页面时,才会使用 HTTP 谓词来决定要执行哪个页面处理程序:例如,OnGet 用于 GET 谓词,或 OnPost 用于 POST 谓词。

Web API controllers work like minimal API endpoints: the HTTP verb takes part in the routing process itself. So a GET request may be routed to one action, and a POST request may be routed to a different action, even if the request used the same URL.
Web API 控制器的工作方式类似于最小 API 端点:HTTP 动词参与路由过程本身。因此,GET 请求可以路由到一个作,而 POST 请求可以路由到不同的作,即使请求使用相同的 URL。

The [Route] attribute we’ve used so far responds to all HTTP verbs. Instead, an action should typically only handle a single verb. Instead of the [Route] attribute, you can use
到目前为止,我们使用的 [Route] 属性响应所有 HTTP 动词。相反,一个作通常应该只处理一个动词。您可以使用 [Route] 属性

• [HttpPost] to handle POST requests
• [HttpGet] to handle GET requests
• [HttpPut] to handle PUT requests

There are similar attributes for all the standard HTTP verbs, like DELETE and OPTIONS. You can use these attributes instead of the [Route] attribute to specify that an action method should correspond to a single verb, as shown in the following listing.
所有标准 HTTP 动词都有类似的属性,例如 DELETE 和 OPTIONS。可以使用这些属性而不是 [Route] 属性来指定作方法应对应于单个谓词,如下面的列表所示。

Listing 20.9 Using HTTP verb attributes with attribute routing
清单 20.9 在属性路由中使用 HTTP 动词属性

public class AppointmentController
{
    [HttpGet("/appointments")]                #A
    public IActionResult ListAppointments()   #A
    {                                         #A
        /* method implementation */           #A
    }                                         #A

    [HttpPost("/appointments")]                 #B
    public IActionResult CreateAppointment()    #B
    {                                           #B
        /* method implementation */             #B
    }                                           #B
}

❶ Executed only in response to GET /appointments
❷ Executed only in response to POST /appointments

If your application receives a request that matches the route template of an action method but doesn’t match the required HTTP verb, you’ll get a 405 Method not allowed error response. For example, if you send a DELETE request to the /appointments URL in the previous listing, you’ll get a 405 error response.
如果您的应用程序收到与作方法的路由模板匹配但与所需的 HTTP 动词不匹配的请求,您将收到 405 Method not allowed 错误响应。例如,如果您向上一个列表中的 /appointments URL 发送 DELETE 请求,您将收到 405 错误响应。

When you’re building web API controllers, there is some code that you’ll find yourself writing repeatedly. The [ApiController] attribute is designed to handle some of this for you and reduce the amount of boilerplate you need.
在构建 Web API 控制器时,您会发现自己需要重复编写一些代码。[ApiController] 属性旨在为您处理其中的一些问题,并减少您需要的样板数量。

20.4 Using common conventions with [ApiController]

20.4 在 [ApiController] 中使用通用约定

In this section you’ll learn about the [ApiController] attribute and how it can reduce the amount of code you need to write to create consistent web API controllers. You’ll learn about the conventions it applies, why they’re useful, and how to turn them off if you need to.
在本部分中,你将了解 [ApiController] 属性,以及它如何减少创建一致的 Web API 控制器所需的代码量。您将了解它适用的约定、它们为什么有用,以及如何在需要时关闭它们。

The [ApiController] attribute was introduced in .NET Core 2.1 to simplify the process of creating web API controllers. To understand what it does, it’s useful to look at an example of how you might write a web API controller without the [ApiController] attribute and compare that with the code required to achieve the same thing with the attribute.
[ApiController] 属性是在 .NET Core 2.1 中引入的,用于简化创建 Web API 控制器的过程。要了解它的作用,查看一个示例,了解如何编写没有 [ApiController] 属性的 Web API 控制器,并将其与使用该属性实现相同作所需的代码进行比较,这非常有用。

Listing 20.10 Creating a web API controller without the [ApiController] attribute
清单 20.10 创建不带 [ApiController] 属性的 Web API 控制器

public class FruitController : ControllerBase
{
    List<string> _fruit = new List<string>     #A
    {                                          #A
        "Pear", "Lemon", "Peach"               #A
    };                                         #A

    [HttpPost("fruit")]                              #B
    public ActionResult Update([FromBody] UpdateModel model)     #C
    {
        if (!ModelState.IsValid)                             #D
        {                                                    #D
             return BadRequest(                              #D
                new ValidationProblemDetails(ModelState));   #D
        }                                                    #D

        if (model.Id < 0 || model.Id > _fruit.Count)             
        {
            return NotFound(new ProblemDetails()              #E
            {                                                 #E
                Status = 404,                                 #E
                Title = "Not Found",                          #E
                Type = "https://tools.ietf.org/html/rfc7231"  #E
                       + "#section-6.5.4",                    #E
            });                                               #E
        }                                                     #E
        _fruit[model.Id] = model.Name;    #F
        return Ok();                      #F
    }

    public class UpdateModel                                    
    {                                                           
        public int Id { get; set; }                             

        [Required]                         #G
        public string Name { get; set; }   #G
    }
}

❶ The list of strings serves as the application model in this example.
在此示例中,字符串列表用作应用程序模型。
❷ Web APIs use attribute routing to define the route templates.
Web API 使用属性路由来定义路由模板。
❸ The [FromBody] attribute indicates that the parameter should be bound to the request body.
[FromBody] 属性指示参数应绑定到请求正文。
❹ You need to check if model validation succeeded and return a 400 response if it failed.
您需要检查模型验证是否成功,如果失败,则返回 400 响应。
❺ If the data sent does not contain a valid ID, returns a 404 ProblemDetails response
如果发送的数据不包含有效的 ID,则返回 404 ProblemDetails 响应
❻ Updates the model and returns a 200 Response
更新模型并返回 200 响应
❼ UpdateModel is valid only if the Name value is provided, as set by the [Required] attribute.
UpdateModel 仅在提供 Name 值时有效,如 [Required] 属性所设置。

This example demonstrates many common features and patterns used with web API controllers:
此示例演示了与 Web API 控制器一起使用的许多常见功能和模式:

• Web API controllers read data from the body of a request, typically sent as JSON. To ensure the body is read as JSON and not as form values, you have to apply the [FromBody] attribute to the method parameters to ensure it is model-bound correctly.
Web API 控制器从请求正文中读取数据,通常以 JSON 形式发送。若要确保将正文读取为 JSON 而不是表单值,必须将 [FromBody] 属性应用于方法参数,以确保其模型绑定正确。

• As discussed in chapter 16, after model binding, the model is validated, but it’s up to you to act on the validation results. You should return a 400 Bad Request response if the values provided failed validation. You typically want to provide details of why the request was invalid: this is done in listing 20.10 by returning a ValidationProblemDetails object in the response body, built from the ModelState.
如第 16 章所述,在模型绑定之后,将验证模型,但由您根据验证结果执行作。如果提供的值验证失败,则应返回 400 Bad Request 响应。您通常希望提供有关请求无效原因的详细信息:这是在清单 20.10 中通过在响应正文中返回一个 ValidationProblemDetails 对象来完成的,该对象是从 ModelState 构建的。

• Whenever you return an error status, such as a 404 Not Found, where possible you should return details of the problem that will allow the caller to diagnose the issue. The ProblemDetails class is the recommended way of doing that in ASP.NET Core.
每当返回错误状态(如 404 Not Found)时,应尽可能返回问题的详细信息,以便调用方诊断问题。ProblemDetails 类是在 ASP.NET Core 中执行此作的推荐方法。

The code in listing 20.10 is representative of what you might see in an ASP.NET Core API controller before .NET Core 2.1. The introduction of the [ApiController] attribute in .NET Core 2.1 (and subsequent refinement in later versions) makes this same code much simpler, as shown in the following listing.
清单 20.10 中的代码代表了您在 .NET Core 2.1 之前的 ASP.NET Core API 控制器中可能看到的内容。在 .NET Core 2.1 中引入 [ApiController] 属性(以及更高版本中的后续优化)使相同的代码变得更加简单,如下面的清单所示。

Listing 20.11 Creating a web API controller with the [ApiController] attribute
清单 20.11 创建具有 [ApiController] 属性的 Web API 控制器

[ApiController]                               #A
public class FruitController : ControllerBase
{
    List<string> _fruit = new List<string>
    {
        "Pear", "Lemon", "Peach"
    };

    [HttpPost("fruit")]
    public ActionResult Update(UpdateModel model)    #B
    {                                                    #C
        if (model.Id < 0 || model.Id > _fruit.Count)
        {
            return NotFound();   #D
        }

        _fruit[model.Id] = model.Name;

        return Ok();
    }

    public class UpdateModel
    {
        public int Id { get; set; }

        [Required]
        public string Name { get; set; }
    }
}

❶ Adding the [ApiController] attribute applies several conventions common to API controllers.
添加 [ApiController] 属性将应用 API 控制器通用的几个约定。
❷ The [FromBody] attribute is assumed for complex action method parameters.
[FromBody] 属性假定用于复杂的作方法参数。
❸ The model validation is automatically checked, and if invalid, returns a 400 response.
系统会自动检查模型验证,如果无效,则返回 400 响应。
❹ Error status codes are automatically converted to a ProblemDetails object.
错误状态代码会自动转换为 ProblemDetails 对象。

If you compare listing 20.10 with listing 20.11, you’ll see that all the bold code in listing 20.10 can be removed and replaced with the [ApiController] attribute in listing 20.11. The [ApiController] attribute automatically applies several conventions to your controllers:
如果你比较清单 20.10 和清单 20.11,你会发现清单 20.10 中的所有粗体代码都可以删除,并替换为清单 20.11 中的 [ApiController] 属性。[ApiController] 属性会自动将多个约定应用于控制器:

• Attribute routing—You must use attribute routing with your controllers; you can’t use conventional routing—not that you would, as we’ve discussed this approach only for API controllers anyway.
属性路由 - 必须对控制器使用属性路由;您不能使用传统路由 — 并不是说您会这样做,因为无论如何,我们只针对 API 控制器讨论了这种方法。
• Automatic 400 responses—I said in chapter 16 that you should always check the value of ModelState.IsValid in your Razor Page handlers and MVC actions, but the [ApiController] attribute does this for you by adding a filter, as we did with minimal APIs in chapter 7. We’ll cover MVC filters in detail in chapters 21 and 22.
自动 400 响应 - 我在第 16 章中说过,您应该始终在 Razor 页面处理程序和 MVC作中检查 ModelState.IsValid 的值,但 [ApiController] 属性通过添加过滤器来为您执行此作,就像我们在第 7 章中对最少的 API 所做的那样。我们将在第 21 章和第 22 章中详细介绍 MVC 过滤器。
• Model binding source inference—Without the [ApiController] attribute, complex types are assumed to be passed as form values in the request body. For web APIs, it’s much more common to pass data as JSON, which ordinarily requires adding the [FromBody] attribute. The [ApiController] attribute takes care of that for you.
模型绑定源推理 - 如果没有 [ApiController] 属性,则假定复杂类型在请求正文中作为表单值传递。对于 Web API,将数据作为 JSON 传递更为常见,这通常需要添加 [FromBody] 属性。[ApiController] 属性会为您处理该问题。
• ProblemDetails for error codes—You often want to return a consistent set of data when an error occurs in your API. The [ApiController] attribute intercepts any error status codes returned by your controller (for example, a 404 Not Found response), and converts them to ProblemDetails responses.
错误代码的 ProblemDetails - 当 API 中发生错误时,您通常希望返回一组一致的数据。[ApiController] 属性截获控制器返回的任何错误状态代码(例如,404 Not Found 响应),并将其转换为 ProblemDetails 响应。

When it was introduced, a key feature of the [ApiController] attribute was the Problem Details support, but as I described in chapter 5, the same automatic conversion to Problem Details is now supported by the default ExceptionHandlerMiddleware and StatusCodePagesMiddleware. Nevertheless, the [ApiController] conventions can significantly reduce the amount of boilerplate code you have to write and ensure that validation failures are handled automatically, for example.
引入 [ApiController] 属性时,它的一个关键功能是 Problem Details 支持,但正如我在第 5 章中描述的,默认的 ExceptionHandlerMiddleware 和 StatusCodePagesMiddleware 现在支持相同的自动转换为 Problem Details。尽管如此,[ApiController] 约定可以显著减少您必须编写的样板代码量,并确保自动处理验证失败。

As is common in ASP.NET Core, you will be most productive if you follow the conventions rather than trying to fight them. However, if you don’t like some of the conventions introduced by [ApiController],or want to customize them, you can easily do so.
正如 ASP.NET Core 中的常见做法一样,如果您遵循惯例而不是试图与之抗争,您将最有效率。但是,如果您不喜欢 [ApiController] 引入的某些约定,或者想要自定义它们,则可以轻松完成。

You can customize the web API controller conventions your application uses by calling ConfigureApiBehaviorOptions() on the IMvcBuilder object returned from the AddControllers() method in your Program.cs file. For example, you could disable the automatic 400 responses on validation failure, as shown in the following listing.
您可以通过对从 Program.cs 文件中的 AddControllers() 方法返回的 IMvcBuilder 对象调用 ConfigureApiBehaviorOptions() 来自定义应用程序使用的 Web API 控制器约定。例如,您可以在验证失败时禁用自动 400 响应,如下面的清单所示。

Listing 20.12 Customizing [ApiAttribute] behaviors
清单 20.12 自定义 [ApiAttribute] 行为

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
    .ConfigureApiBehaviorOptions(options =>         #A
    {
        options.SuppressModelStateInvalidFilter = true;    #B
    });

// ...

❶ Controls which conventions are applied by providing a configuration lambda
通过提供配置 lambda来控制应用哪些约定
❷ This would disable the automatic 400 responses for invalid requests.
将禁用无效请求的自动 400 响应。

Tip You can disable all the automatic features enabled by the [ApiController] attribute, but I encourage you to stick to the defaults unless you really need to change them. You can read more about disabling features in the documentation at https://docs.microsoft.com/aspnet/core/web-api.
提示:您可以禁用 [ApiController] 属性启用的所有自动功能,但我建议您坚持使用默认值,除非您确实需要更改它们。您可以在 https://docs.microsoft.com/aspnet/core/web-api 上的文档中阅读有关禁用功能的更多信息。

The ability to customize each aspect of your web API controllers is one of the key differentiators with minimal APIs. In the next section you’ll learn how to control the format of the data returned by your web API controllers—whether that’s JSON, XML, or a different, custom format.
自定义 Web API 控制器各个方面的能力是最少 API 的关键区别之一。在下一节中,您将学习如何控制 Web API 控制器返回的数据的格式,无论是 JSON、XML 还是其他自定义格式。

20.5 Generating a response from a model

20.5 从模型生成响应

This brings us to the final topic in this chapter: formatting a response. It’s common for API controllers to return JSON these days, but that’s not always the case. In this section you’ll learn about content negotiation and how to enable additional output formats such as XML.
这就引出了本章的最后一个主题:格式化响应。如今,API 控制器返回 JSON 很常见,但情况并非总是如此。在本节中,您将了解内容协商以及如何启用其他输出格式,例如 XML。

Consider this scenario: you’ve created a web API action method for returning a list of cars, as in the following listing. It invokes a method on your application model, which hands back the list of data to the controller. Now you need to format the response and return it to the caller.
请考虑以下场景:您创建了一个 Web API作方法,用于返回汽车列表,如下面的清单所示。它调用应用程序模型上的一个方法,该方法将数据列表返回给控制器。现在,您需要设置响应的格式并将其返回给调用方。

Listing 20.13 A web API controller to return a list of cars
列表 20.13 返回汽车列表的 Web API 控制器

[ApiController]
public class CarsController : Controller
{
    [HttpGet("api/cars")]              #A
    public IEnumerable<string> ListCars()      #B
    {
        return new string[]                      #C
            { "Nissan Micra", "Ford Focus" };    #C
    }
}

❶ The action is executed with a request to GET /api/cars.
通过请求 GET /api/cars 来执行作。
❷ The API model containing the data is an IEnumerable<string>.
包含数据的 API 模型是 IEnumerable<string>
❸ This data would normally be fetched from the application model.
此数据通常从应用程序模型中提取。

You saw in section 20.2 that it’s possible to return data directly from an action method, in which case the middleware formats it and returns the formatted data to the caller. But how does the middleware know which format to use? After all, you could serialize it as JSON, as XML, or even with a simple ToString() call.
你在 Section 20.2 中看到,可以直接从 action 方法返回数据,在这种情况下,中间件会对其进行格式化并将格式化的数据返回给调用者。但是中间件如何知道要使用哪种格式呢?毕竟,您可以将其序列化为 JSON、XML,甚至使用简单的 ToString() 调用。

Warning Remember that in this chapter I’m talking only about web API controller responses. Minimal APIs support only automatic serialization to JSON, nothing else.
警告:请记住,在本章中,我只讨论 Web API 控制器响应。Minimal API 仅支持自动序列化为 JSON,不支持其他任何内容。

The process of determining the format of data to send to clients is known generally as content negotiation (conneg). At a high level, the client sends a header indicating the types of content it can understand—the Accept header—and the server picks one of these, formats the response, and sends a Content-Type header in the response, indicating which type it chose.
确定要发送给客户端的数据格式的过程通常称为内容协商 (conneg)。概括地说,客户端发送一个标头(指示它可以理解的内容类型)(Accept 标头),服务器选择其中一个标头,设置响应的格式,并在响应中发送 Content-Type 标头,指示它选择了哪种类型。

The Accept and Content-Type headers
Accept 和 Content-Type 标头

The Accept header is sent by a client as part of a request to indicate the type of content that the client can handle. It consists of a number of MIME types, with optional weightings (from 0 to 1) to indicate which type would be preferred. For example, the application/json,text/xml;q=0.9,text/plain;q=0.6 header indicates that the client can accept JSON, XML, and plain text, with weightings of 1.0, 0.9, and 0.6, respectively. JSON has a weighting of 1.0, as no explicit weighting was provided. The weightings can be used during content negotiation to choose an optimal representation for both parties.
Accept 标头由客户端作为请求的一部分发送,用于指示客户端可以处理的内容类型。它由许多 MIME 类型组成,具有可选的权重(从 0 到 1)以指示首选类型。例如,application/json,text/xml;q=0.9,text/plain;q=0.6 标头表示客户端可以接受 JSON、XML 和纯文本,权重分别为 1.0、0.9 和 0.6。JSON 的权重为 1.0,因为未提供显式权重。在内容协商期间,可以使用权重来为双方选择最佳表示形式。

The Content-Type header describes the data sent in a request or response. It contains the MIME type of the data, with an optional character encoding. For example, the application/json; charset=utf-8 header would indicate that the body of the request or response is JSON, encoded using UTF-8.
Content-Type 标头描述在请求或响应中发送的数据。它包含数据的 MIME 类型,以及可选的字符编码。例如,application/json;charset=utf-8 标头表示请求或响应的正文是使用 UTF-8 编码的 JSON。

For more on MIME types, see the Mozilla documentation: http://mng.bz/gop8. You can find the RFC for content negotiation at http://mng.bz/6DXo.
有关 MIME 类型的更多信息,请参阅 Mozilla 文档:http://mng.bz/gop8。您可以在 http://mng.bz/6DXo 中找到内容协商的 RFC。

You’re not forced into sending only a Content-Type the client expects, and in some cases, you may not even be able to handle the types it requests. What if a request stipulates that it can accept only Microsoft Excel spreadsheets? It’s unlikely you’d support that, even if that’s the only Accept type the request contains.
你不会被迫只发送客户端期望的 Content-Type,在某些情况下,你甚至可能无法处理它请求的类型。如果请求规定它只能接受 Microsoft Excel 电子表格,该怎么办?您不太可能支持这一点,即使这是请求包含的唯一 Accept 类型。

When you return an API model from an action method, whether directly (as in listing 20.13) or via an OkResult or other StatusCodeResult, ASP.NET Core always returns something in the response. If it can’t honor any of the types stipulated in the Accept header, it will fall back to returning JSON by default. Figure 20.7 shows that even though XML was requested, the API controller formatted the response as JSON.
当你从作方法返回 API 模型时,无论是直接返回(如清单 20.13 所示)还是通过 OkResult 或其他 StatusCodeResult,ASP.NET Core 始终在响应中返回一些内容。如果它不能接受 Accept 标头中规定的任何类型,它将默认回退到返回 JSON。图 20.7 显示,即使请求了 XML,API 控制器也将响应格式化为 JSON。

alt text

Figure 20.7 Even though the request was made with an Accept header of text/xml, the response returned was JSON, as the server was not configured to return XML.
图 20.7 即使请求是使用 text/xml 的 Accept 标头发出的,返回的响应也是 JSON,因为服务器未配置为返回 XML。

Warning In legacy ASP.NET, objects were serialized to JSON using PascalCase, where properties start with a capital letter. In ASP.NET Core, objects are serialized using camelCase by default, where properties start with a lowercase letter.
警告:在旧版 ASP.NET 中,对象使用 PascalCase 序列化为 JSON,其中属性以大写字母开头。在 ASP.NET Core 中,默认情况下使用 camelCase 序列化对象,其中属性以小写字母开头。

However the data is sent, it’s serialized by an IOutputFormatter implementation. ASP.NET Core ships with a limited number of output formatters out of the box, but as always, it’s easy to add additional ones or change the way the defaults work.
无论数据如何发送,它都会由 IOutputFormatter 实现进行序列化。ASP.NET Core 附带了有限数量的开箱即用输出格式化程序,但与往常一样,添加其他格式化程序或更改默认值的工作方式很容易。

20.5.1 Customizing the default formatters: Adding XML support

20.5.1 自定义默认格式化程序:添加 XML 支持

As with most of ASP.NET Core, the Web API formatters are completely customizable. By default, only formatters for plain text (text/plain), HTML (text/html), and JSON (application/json) are configured. Given the common use case of single-page application (SPAs) and mobile applications, this will get you a long way. But sometimes you need to be able to return data in a different format, such as XML.
与大多数 ASP.NET Core 一样,Web API 格式化程序是完全可自定义的。默认情况下,仅配置纯文本 (text/plain)、HTML (text/html) 和 JSON (application/json) 的格式化程序。鉴于单页应用程序 (SPA) 和移动应用程序的常见用例,这将使您大有帮助。但有时您需要能够以不同的格式(如 XML)返回数据。

Newtonsoft.Json vs. System.Text.Json

Newtonsoft.Json, also known as Json.NET, has for a long time been the canonical way to work with JSON in .NET. It’s compatible with every version of .NET under the sun, and it will no doubt be familiar to virtually all .NET developers. Its reach was so great that even ASP.NET Core took a dependency on it!
Newtonsoft.Json,也称为 Json.NET,长期以来一直是在 .NET 中使用 JSON 的规范方式。它与全球所有版本的 .NET 兼容,毫无疑问,几乎所有 .NET 开发人员都熟悉它。它的覆盖范围如此之大,以至于 ASP.NET Core 都依赖于它!

That all changed with the introduction of a new library in ASP.NET Core 3.0, System .Text.Json, which focuses on performance. In .NET Core 3.0 onward, ASP.NET Core uses System.Text.Json by default instead of Newtonsoft.Json.
随着 ASP.NET Core 3.0 中引入新库 System ,这一切都发生了变化。Text.Json,它侧重于性能。在 .NET Core 3.0 及更高版本中,ASP.NET Core 默认使用 System.Text.Json,而不是 Newtonsoft.Json。

The main difference between the libraries is that System.Text.Json is picky about its JSON. It will generally only deserialize JSON that matches its expectations. For example, System.Text.Json won’t deserialize JSON that uses single quotes around strings; you have to use double quotes.
这两个库之间的主要区别在于 System.Text.Json 对其 JSON 很挑剔。它通常只会反序列化符合其预期的 JSON。例如,System.Text.Json 不会反序列化在字符串周围使用单引号的 JSON;您必须使用双引号。

If you’re creating a new application, this is generally not a problem; you quickly learn to generate the correct JSON. But if you’re converting an application to ASP.NET Core or are sending JSON to a third party you don’t control, these limitations can be real stumbling blocks.
如果要创建新应用程序,这通常不是问题;您很快就学会了生成正确的 JSON。但是,如果您要将应用程序转换为 ASP.NET Core 或将 JSON 发送给您无法控制的第三方,则这些限制可能会成为真正的绊脚石。

Luckily, you can easily switch back to the Newtonsoft.Json library instead. Install the Microsoft.AspNetCore.Mvc.NewtonsoftJson package into your project and update the AddControllers() method in Program.cs to the following:
幸运的是,您可以轻松地切换回 Newtonsoft.Json 库。将 Microsoft.AspNetCore.Mvc.NewtonsoftJson 包安装到项目中,并将 Program.cs 中的 AddControllers() 方法更新为以下内容:

builder.Services.AddControllers()
    .AddNewtonsoftJson();

This will switch ASP.NET Core’s formatters to use Newtonsoft.Json behind the scenes, instead of System.Text.Json. For more details on the differences between the libraries, see Microsoft’s article “Compare Newtonsoft.Json to System.Text.Json, and migrate to System.Text.Json”: http://mng.bz/0mRJ. For more advice on when to switch to the Newtonsoft.Json formatter, see the section “Add Newtonsoft.Json-based JSON format support” in Microsoft’s “Format response data in ASP.NET Core Web API” documentation: http://mng.bz/zx11.
这会将 ASP.NET Core 的格式化程序切换为在后台使用 Newtonsoft.Json,而不是 System.Text.Json。有关库之间差异的更多详细信息,请参阅 Microsoft 的文章“将 Newtonsoft.Json 与 System.Text.Json 进行比较,并迁移到 System.Text.Json”:http://mng.bz/0mRJ。有关何时切换到 Newtonsoft.Json 格式化程序的更多建议,请参阅 Microsoft 的“在 ASP.NET Core Web API 中格式化响应数据”文档中的“添加基于 Newtonsoft.Json 的 JSON 格式支持”部分:http://mng.bz/zx11

You can add XML output to your application by adding an output formatter. You configure your application’s formatters in Program.cs by customizing the IMvcBuilder object returned from AddControllers(). To add the XML output formatter, use the following:
您可以通过添加输出格式化程序将 XML 输出添加到您的应用程序中。通过自定义从 AddControllers() 返回的 IMvcBuilder 对象,可以在 Program.cs 中配置应用程序的格式化程序。要添加 XML 输出格式化程序,请使用以下命令:

services.AddControllers()
    .AddXmlSerializerFormatters();

NOTE Technically, this also adds an XML input formatter, which means your application can now receive XML in requests too. Previously, sending a request with XML in the body would respond with a 415 Unsupported Media Type response. For a detailed look at formatters, including creating a custom formatter, see the documentation at http://mng.bz/e5gG.
注意:从技术上讲,这还添加了一个 XML 输入格式化程序,这意味着您的应用程序现在也可以在请求中接收 XML。以前,发送正文中包含 XML 的请求将响应 415 Unsupported Media Type 响应。有关格式化程序的详细信息,包括创建自定义格式化程序,请参阅 http://mng.bz/e5gG 中的文档。

With this simple change, your API controllers can now format responses as XML as well as JSON. Running the same request as shown in figure 20.7 with XML support enabled means the app will respect the text/xml accept header. The formatter serializes the string array to XML as requested instead of defaulting to JSON, as shown in figure 20.8.
通过这个简单的更改,您的 API 控制器现在可以将响应格式化为 XML 和 JSON。在启用 XML 支持的情况下运行如图 20.7 所示的相同请求意味着应用程序将遵循 text/xml accept 标头。格式化程序根据请求将字符串数组序列化为 XML,而不是默认为 JSON,如图 20.8 所示。

alt text

Figure 20.8 With the XML output formatters added, the Accept header’ text/xml value is respected, and the response is serialized to XML.
图 20.8 添加 XML 输出格式化程序后,将遵循 Accept 标头的 text/xml 值,并将响应序列化为 XML。

This is an example of content negotiation, where the client has specified which formats it can handle and the server selects one of those, based on what it can produce. This approach is part of the HTTP protocol, but there are some quirks to be aware of when relying on it in ASP.NET Core. You won’t often run into these, but if you’re not aware of them when they hit you, they could have you scratching your head for hours!
这是内容协商的一个示例,其中客户端指定了它可以处理的格式,服务器根据它可以生成的格式选择其中一种。此方法是 HTTP 协议的一部分,但在 ASP.NET Core 中依赖它时,需要注意一些怪癖。你不会经常遇到这些,但如果它们在它们击中你时没有意识到它们,它们可能会让你挠头几个小时!

20.5.2 Choosing a response format with content negotiation

20.5.2 使用内容协商选择响应格式

Content negotiation is where a client says which types of data it can accept using the Accept header and the server picks the best one it can handle. Generally speaking, this works as you’d hope: the server formats the data using a type the client can understand.
内容协商是客户端使用 Accept 标头说明它可以接受哪些类型的数据,服务器选择它可以处理的最佳数据类型。一般来说,这就像你希望的那样工作:服务器使用客户端可以理解的类型来格式化数据。

The ASP.NET Core implementation has some special cases that are worth bearing in mind:
ASP.NET Core 实现有一些值得牢记的特殊情况:

• By default, ASP.NET Core returns only application/json, text/plain, and text/html MIME types. You can add IOutputFormatters to make other types available, as you saw in the previous section for text/xml.
默认情况下,ASP.NET Core 仅返回 application/json、text/plain 和 text/html MIME 类型。您可以添加 IOutputFormatters 以使其他类型可用,如上一节 text/xml 中所示。
• By default, if you return null as your API model, whether from an action method or by passing null in a StatusCodeResult, the middleware returns a 204 No Content response.
默认情况下,如果返回 null 作为 API 模型 (无论是从作方法还是通过在 StatusCodeResult 中传递 null),中间件都会返回 204 No Content 响应。
• When you return a string as your API model, if no Accept header is set, ASP.NET Core formats the response as text/plain.
当您将字符串作为 API 模型返回时,如果未设置 Accept 标头,则 ASP.NET Core 会将响应格式设置为 text/plain。
• When you use any other class as your API model, and there’s no Accept header or none of the supported formats was requested, the first formatter that can generate a response is used (typically JSON by default).
当您使用任何其他类作为 API 模型,并且没有 Accept 标头或未请求任何支持的格式时,将使用第一个可以生成响应的格式化程序 (通常默认为 JSON)。
• If the middleware detects that the request is probably from a browser (the accept header contains /), it will not use conneg. Instead, it formats the response as though an Accept header was not provided, using the default formatter (typically JSON).
如果中间件检测到请求可能来自浏览器 (accept 标头包含 /) ,则不会使用 conneg。相反,它使用默认格式化程序(通常为 JSON)格式化响应,就像未提供 Accept 标头一样。

These defaults are relatively sane, but they can certainly bite you if you’re not aware of them. That last point in particular, where the response to a request from a browser is virtually always formatted as JSON, has certainly caught me out when trying to test XML requests locally!
这些违约相对来说是理智的,但如果你不知道它们,它们肯定会咬你一口。特别是最后一点,对来自浏览器的请求的响应几乎总是格式化为 JSON,在尝试在本地测试 XML 请求时,这无疑让我感到困惑!

As you should expect by now, all these rules are configurable; you can easily change the default behavior in your application if it doesn’t fit your requirements. For example, the following listing, taken from Program.cs, shows how you can force the middleware to respect the browser’s Accept header and remove the text/plain formatter for strings.
正如您现在应该预料的那样,所有这些规则都是可配置的;如果应用程序的默认行为不符合您的要求,您可以轻松更改应用程序的默认行为。例如,以下清单取自 Program.cs,展示了如何强制中间件遵守浏览器的 Accept 标头并删除字符串的文本/纯格式化程序。

Listing 20.14 Customizing MVC to respect the browser’s Accept header in web APIs
清单 20.14 在 Web API 中自定义 MVC 以遵循浏览器的 Accept 标头

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>      #A
{
    options.RespectBrowserAcceptHeader = true;         #B
    options.OutputFormatters.RemoveType<StringOutputFormatter>();   #C
});

❶ AddControllers has an overload that takes a lambda function.
AddControllers 具有采用 lambda 函数的重载。
❷ False by default; several other properties are also available to be set.
默认为 False;还可以设置其他几个属性。
❸ Removes the output formatter that formats strings as text/plain
删除将字符串格式化为 text/plain 的输出格式化程序

In most cases, conneg should work well for you out of the box, whether you’re building an SPA or a mobile application. In some cases, you may find you need to bypass the usual conneg mechanisms for specific action methods, and there are various ways to achieve this, but I won’t cover them in this book as I’ve found I rarely need to use them. For details, see Microsoft’s “Format response data in ASP.NET Core Web API” documentation: http://mng.bz/zx11.
在大多数情况下,conneg 应该可以立即为您工作,无论您是构建 SPA 还是移动应用程序。在某些情况下,你可能会发现你需要绕过特定动作方法的通常连接机制,并且有多种方法可以实现这一点,但我不会在本书中介绍它们,因为我发现我很少需要使用它们。有关详细信息,请参阅 Microsoft 的“在 ASP.NET Core Web API 中设置响应数据的格式”文档:http://mng.bz/zx11

At this point we’ve covered the main points of using API controllers, but you probably still have one major question: why would I use web API controllers over minimal APIs? That’s a great question, and one we’ll look at in section 20.6.
在这一点上,我们已经介绍了使用 API 控制器的要点,但您可能仍然有一个主要问题:为什么我要使用 Web API 控制器而不是最小的 API?这是一个很好的问题,我们将在 20.6 节中讨论这个问题。

20.6 Choosing between web API controllers and minimal APIs

20.6 在 Web API 控制器和最小 API 之间进行选择

In part 1 of this book you learned all about using minimal APIs to build a JSON API. Minimal APIs are the new kid on the block, being introduced in .NET 6, but they are growing up quickly. With all the new features introduced in .NET 7 (discussed in chapter 5), minimal APIs are emerging as a great way to build HTTP APIs in modern .NET.
在本书的第 1 部分中,您了解了如何使用最小 API 构建 JSON API 的所有信息。最小 API 是 .NET 6 中引入的新成员,但它们正在迅速发展。随着 .NET 7 中引入的所有新功能(在第 5 章中讨论),最小 API 正在成为在现代 .NET 中构建 HTTP API 的好方法。

By contrast, web API controllers have been around since day one. They were introduced in their current form in ASP.NET Core 1.0 and were heavily inspired by the web API framework from legacy ASP.NET. The designs, patterns, and concepts used by web API controllers haven’t changed much since then, so if you’ve ever used web API controllers, they should look familiar in .NET 7.
相比之下,Web API 控制器从第一天起就已经存在。它们是在 ASP.NET Core 1.0 中以当前形式引入的,并受到旧版 ASP.NET 的 Web API 框架的严重启发。从那时起,Web API 控制器使用的设计、模式和概念没有太大变化,因此,如果您曾经使用过 Web API 控制器,它们在 .NET 7 中应该看起来很熟悉。

The difficult question in .NET 7 is if you need to build an API, which should you use, minimal APIs or web API controllers? Both have their pros and cons, and a large part of the decision will be personal preference, but to help your decision, you should ask yourself several questions:
.NET 7 中的难题是,您是否需要构建一个 API,您应该使用哪个 API,最小 API 还是 Web API 控制器?两者都有其优点和缺点,很大一部分决定将是个人喜好,但为了帮助你做出决定,你应该问自己几个问题:

  1. Do you need to return data in multiple formats using content negotiation?
    您是否需要使用 Content Negotiation 以多种格式返回数据?
  2. Is performance critical to your application?
    性能对应用程序至关重要吗?
  3. Do you have complex filtering requirements?
    您是否有复杂的筛选要求?
  4. Is this a new project?
    这是一个新项目吗?
  5. Do you already have experience with web API controllers?
    您是否已经有使用 Web API 控制器的经验?
  6. Do you prefer convention over configuration?
    您更喜欢约定而不是配置吗?

Questions 1-3 in this list are focused on technical differences between minimal APIs and web API controllers. Web API controllers support content negotiation (conneg), which allows clients to request data be returned in a particular format: JSON, XML, or CSV, for example, as you learned in section 20.5. Web API controllers support this feature out of the box, so if it’s crucial for your application, it may be better to choose web API controllers over minimal APIs.
此列表中的问题 1-3 侧重于最小 API 和 Web API 控制器之间的技术差异。Web API 控制器支持内容协商 (conneg),它允许 Client 端请求以特定格式返回数据:JSON、XML 或 CSV,例如,如您在第 20.5 节中学到的那样。Web API 控制器支持开箱即用的此功能,因此如果它对您的应用程序至关重要,最好选择 Web API 控制器而不是最少的 API。

Tip If you want to use content negotiation with minimal APIs, it’s possible but not built in. I show how to add conneg to minimal APIs using the open-source library Carter on my blog: http://mng.bz/o12d.
提示:如果您想通过最少的 API 使用内容协商,这是可能的,但不是内置的。我展示了如何使用开源库 Carter 将 conneg 添加到最小的 API:http://mng.bz/o12d

Question 2 is about performance. Everyone wants the most performant app, but there’s a real question of how important it is. Are you going to be regularly benchmarking your application and looking for any regressions? If so, minimal APIs are probably going to be a better choice, as they’re often more performant than web API controllers.
问题 2 是关于性能的。每个人都希望获得性能最高的应用程序,但存在一个真正的问题,即它有多重要。您是否会定期对应用程序进行基准测试并寻找任何回归?如果是这样,最小 API 可能是更好的选择,因为它们通常比 Web API 控制器性能更高。

The MVC framework that web API controllers use relies on a lot of conventions and reflection for discovering your controllers and a complex filter pipeline. These are obviously highly optimized, but if you’re writing an application where you need to squeeze out every little bit of throughput, minimal APIs will likely help get you there more easily. For most applications, the overhead of the MVC framework will be negligible when compared with any database or network access in your app, so this is worth worrying about only for performance-sensitive apps.
Web API 控制器使用的 MVC 框架依赖于许多约定和反射来发现控制器和复杂的筛选器管道。这些显然是高度优化的,但如果您正在编写一个需要挤出每一点吞吐量的应用程序,那么最少的 API 可能会帮助您更轻松地实现目标。对于大多数应用程序,与应用程序中的任何数据库或网络访问相比,MVC 框架的开销可以忽略不计,因此,仅对于性能敏感的应用程序,才值得担心。

Question 3 focuses on filtering. You learned about filtering with minimal APIs in chapter 5: filters allow you to attach a processing pipeline to your minimal API endpoints and can be used to do things like automatic validation. Web API controllers (as well as MVC controllers and Razor Pages) also have a filter pipeline, but it’s much more complex than the simple pipeline used by minimal APIs, as you’ll see in chapters 21 and 22.
问题 3 侧重于筛选。您在第 5 章中学习了如何使用最少的 API 进行过滤:过滤器允许您将处理管道附加到最小的 API 端点,并可用于执行自动验证等作。Web API 控制器(以及 MVC 控制器和 Razor Pages)也有一个筛选器管道,但它比最小 API 使用的简单管道要复杂得多,如第 21 章和第 22 章所示。

In most cases the filtering provided by minimal APIs will be perfectly adequate for your needs. The main cases where minimal API filtering will fall down will be when you already have an application that uses web API controllers and want to reuse some complex filters. In these cases, there may be no way to translate your existing web API filters to minimal API filters. If the filtering is important, then you may need to stick with web API controllers.
在大多数情况下,最小 API 提供的过滤将完全满足您的需求。最小 API 过滤失败的主要情况是,您已经有一个使用 Web API 控制器的应用程序,并且想要重用一些复杂的过滤器。在这些情况下,可能无法将现有的 Web API 筛选器转换为最小的 API 筛选器。如果筛选很重要,那么你可能需要坚持使用 Web API 控制器。

This leads to question 4: are you building a new application or working on an existing application? If this is a new application, I would be strongly in favor of using minimal APIs. Minimal APIs are conceptually simpler than web API controllers, are faster because of this, and are receiving a lot of improvements from the ASP.NET Core team. If there’s no other compelling reason to choose web API controllers in your new project, I suggest defaulting to minimal APIs.
这就引出了问题 4:您是构建新应用程序还是正在处理现有应用程序?如果这是一个新应用程序,我强烈赞成使用最少的 API。最小 API 在概念上比 Web API 控制器更简单,因此速度更快,并且从 ASP.NET Core 团队获得了很多改进。如果没有其他令人信服的理由在你的新项目中选择 Web API 控制器,我建议默认使用最小 API。

On the other hand, if you have an existing web API controller application, I would be strongly inclined to stick with web API controllers. While it’s perfectly possible to mix minimal APIs and web API controllers in the same application, I would favor consistency over using the new hotness.
另一方面,如果你有一个现有的 Web API 控制器应用程序,我强烈倾向于坚持使用 Web API 控制器。虽然完全可以在同一个应用程序中混合使用最少的 API 和 Web API 控制器,但我更喜欢一致性,而不是使用新的热度。

Question 5 considers how familiar you already are with web API controllers. If you’re coming from legacy ASP.NET or have already used web API controllers in ASP.NET Core and need to be productive quickly, you might decide to stick with web API controllers.
问题 5 考虑您对 Web API 控制器的熟悉程度。如果您来自传统 ASP.NET 或已经在 ASP.NET Core 中使用过 Web API 控制器,并且需要快速提高工作效率,则可以决定继续使用 Web API 控制器。

I consider this one of the weaker arguments, as minimal APIs are conceptually simpler than web API controllers; if you already know web API controllers, you will likely pick up minimal APIs easily. That said, the differences in the model binding approaches can be a little confusing, and you may decide it’s not worth the investment or frustration if things don’t work as you expect.
我认为这是较弱的论点之一,因为最小的 API 在概念上比 Web API 控制器简单;如果您已经了解 Web API 控制器,则可能会轻松掌握最少的 API。也就是说,模型绑定方法的差异可能有点令人困惑,如果事情没有按预期进行,您可能会认为不值得投资或感到沮丧。

The final question comes down entirely to taste and preference: do you like minimal APIs? web API controllers heavily follow the “convention over configuration” paradigm (though not to the extent of MVC controllers and Razor Pages). By contrast, you must be far more explicit with minimal APIs. Minimal APIs also don’t enforce any particular grouping, unlike web API controllers, which all follow the “action methods in a controller class” pattern.
最后一个问题完全归结为品味和偏好:您喜欢最少的 API 吗?Web API 控制器在很大程度上遵循“约定优于配置”范例(尽管没有达到 MVC 控制器和 Razor Pages 的范围)。相比之下,您必须使用最少的 API 更加明确。与 Web API 控制器不同,最小 API 也不强制执行任何特定的分组,它们都遵循“控制器类中的作方法”模式。

Different people prefer different approaches. Web API controllers mean less manual wiring up of components, but this necessarily means more magic and more rigidity around how you structure your applications.
不同的人喜欢不同的方法。Web API 控制器意味着更少的组件手动连接,但这必然意味着在构建应用程序的方式上更神奇、更严格。

By contrast, minimal API endpoints must be explicitly added to the WebApplication instance, but this also means you have more flexibility around how to group your endpoints. You can put all your endpoints in Program.cs, create natural groupings for them in separate classes, or create a file per endpoint or any pattern you choose.
相比之下,必须将最少的 API 端点显式添加到 WebApplication 实例中,但这也意味着您在如何对端点进行分组方面具有更大的灵活性。您可以将所有终端节点放在Program.cs中,在单独的类中为它们创建自然分组,或者为每个终端节点或您选择的任何模式创建一个文件。

Tip You can also more easily layer on helper frameworks to minimal APIs, such as Carter (https://github.com/CarterCommunity/Carter), which can provide some structure and support functionality if you want it.
提示:您还可以更轻松地将帮助程序框架分层到最小的 API,例如 Carter (https://github.com/CarterCommunity/Carter),如果需要,它可以提供一些结构和支持功能

Overall, the choice is up to you whether web API controllers or minimal APIs are better for your application. Table 20.1 summarizes the questions and where you should favor one approach over the other, but the final choice is up to you!
总的来说,Web API 控制器还是最小的 API 更适合您的应用程序,由您决定。表 20.1 总结了这些问题以及您应该在哪些方面更喜欢一种方法,但最终选择取决于您!

Table 20.1 Choosing between minimal APIs with web API controllers
表 20.1 在最小 API 和 Web API 控制器之间进行选择

Question Minimal APIs Web API controllers
1. Do you need conneg? Can’t use conneg out of the box Built-in and extensible
2. How critical is performance? More performant than web API controllers Less performant than minimal APIs
3. Complex filtering? Have a simple, extensible filter pipeline Have a complex, nonlinear, filter pipeline
4. Is this a new project? Minimal APIs are getting many new features and are a focus of the ASP.NET Core team The MVC framework is receiving small new features, but is less of a focus.
5. Do you have experience with web API controllers? Minimal APIs share many of the same concepts, but have subtle differences in model binding Web API controllers may be familiar to users of legacy ASP.NET or older ASP.NET Core versions
6. Do you prefer convention over configuration? Requires a lot of explicit configuration Convention- and discovery-based, which can appear more magic when you’re unfamiliar

That brings us to the end of this chapter on web APIs. In the next chapter we’ll look at one of more advanced topics of MVC and Razor Pages: the filter pipeline and how you can use it to reduce duplication in your code. The good news is that it’s similar to minimal API filters in principle. The bad news is that it’s far more complicated!
这让我们结束了本章关于 Web API 的内容。在下一章中,我们将介绍 MVC 和 Razor Pages 的更高级主题之一:筛选器管道以及如何使用它来减少代码中的重复。好消息是,它在原则上类似于最小 API 过滤器。坏消息是它要复杂得多!

20.7 Summary

20.7 总结

Web API action methods can return data directly or can use ActionResult<T> to generate an arbitrary response. If you return more than one type of result from an action method, the method signature must return ActionResult<T>.
Web API作方法可以直接返回数据,也可以用于 ActionResult<T>生成任意响应。如果从作方法返回多种类型的结果,则方法签名必须返回 ActionResult<T>

The data returned by a web API action is sometimes called an API model. It contains the data that will be serialized and send back to the client. This differs from view models and PageModels, which contain both data and metadata about how to generate the response.
Web API作返回的数据有时称为 API 模型。它包含将被序列化并发送回客户端的数据。这与视图模型和 PageModel 不同,后者包含有关如何生成响应的数据和元数据。

Web APIs are associated with route templates by applying RouteAttributes to your action methods. These give you complete control over the URLs that make up your application’s API.
通过将 RouteAttributes 应用于作方法,Web API 与路由模板相关联。这些 URL 使您可以完全控制构成应用程序 API 的 URL。

Route attributes applied to a controller combine with the attributes on action methods to form the final template. These are also combined with attributes on inherited base classes. You can use inherited attributes to reduce the amount of duplication in the attributes, such as where you’re using a common prefix on your routes.
应用于控制器的路由属性与作方法上的属性相结合,形成最终模板。这些还与继承的基类上的属性相结合。您可以使用继承的属性来减少属性中的重复数量,例如在路由上使用通用前缀的位置。

By default, the controller and action name have no bearing on the URLs or route templates when you use attribute routing. However, you can use the "[controller]" and "[action]" tokens in your route templates to reduce repetition. They’ll be replaced with the current controller and action name.
默认情况下,在使用属性路由时,控制器和作名称与 URL 或路由模板无关。但是,您可以在路由模板中使用 “[controller]” 和 “[action]” 令牌来减少重复。它们将替换为当前控制器和作名称。

The [HttpPost] and [HttpGet] attributes allow you to choose between actions based on the request’s HTTP verb when two actions correspond to the same URL. This is a common pattern in RESTful applications.
[HttpPost] 和 [HttpGet] 属性允许您在两个作对应于同一 URL 时根据请求的 HTTP 谓词在作之间进行选择。这是 RESTful 应用程序中的常见模式。

The [ApiController] attribute applies several common conventions to your controllers. Controllers decorated with the attribute automatically bind to a request’s body instead of using form values, automatically generate a 400 Bad Request response for invalid requests, and return ProblemDetails objects for status code errors. This can dramatically reduce the amount of boilerplate code you must write.
[ApiController] 属性将几个常见约定应用于控制器。使用 该属性修饰的控制器会自动绑定到请求正文,而不是使用表单值,为无效请求自动生成 400 Bad Request 响应,并为状态代码错误返回 ProblemDetails 对象。这可以显著减少您必须编写的样板代码量。

You can control which of the conventions to apply by using the ConfigureApiBehaviorOptions() method and providing a configuration lambda. This is useful if you need to fit your API to an existing specification, for example.
您可以通过使用 ConfigureApiBehaviorOptions() 方法并提供配置 lambda 来控制要应用的约定。例如,如果您需要使 API 适应现有规范,这将非常有用。

By default, ASP.NET Core formats the API model returned from a web API controller as JSON. In contrast to legacy ASP.NET, JSON data is serialized using camelCase rather than PascalCase. You should consider this change if you get errors or missing values when using data from your API.
默认情况下,ASP.NET Core 将从 Web API 控制器返回的 API 模型格式化为 JSON。与传统 ASP.NET 相比,JSON 数据使用 camelCase 而不是 PascalCase 进行序列化。如果您在使用 API 中的数据时出现错误或缺失值,则应考虑此更改。

ASP.NET Core 3.0 onwards uses System.Text.Json, which is a strict, high performance library for JSON serialization and deserialization. You can replace this serializer with the common Newtonsoft.Json formatter by calling AddNewtonsoftJson() on the return value from services.AddControllers().
ASP.NET Core 3.0 及更高版本使用 System.Text.Json,这是一个严格的高性能库,用于 JSON 序列化和反序列化。您可以通过对服务的返回值调用 AddNewtonsoftJson() 来将此序列化程序替换为通用的 Newtonsoft.Json 格式化程序。AddControllers() 的 Controller。

Content negotiation occurs when the client specifies the type of data it can handle and the server chooses a return format based on this. It allows multiple clients to call your API and receive data in a format they can understand.
当客户端指定它可以处理的数据类型,并且服务器根据此选择返回格式时,就会发生内容协商。它允许多个客户端调用您的 API 并以他们可以理解的格式接收数据。

By default, ASP.NET Core can return text/plain, text/html, and application/json, but you can add formatters if you need to support other formats.
默认情况下,ASP.NET Core 可以返回 text/plain、text/html 和 application/json,但如果您需要支持其他格式,则可以添加格式化程序。

You can add XML formatters by calling AddXmlSerializerFormatters() on the return value from services.AddControllers() in your Startup class. These can format the response as XML, as well as receive XML in a request body.
您可以通过对服务的返回值调用 AddXmlSerializerFormatters() 来添加 XML 格式化程序。AddControllers() 的 Startup。这些选项可以将响应格式化为 XML,并在请求正文中接收 XML。

Content negotiation isn’t used when the Accept header contains /, such as in most browsers. Instead, your application uses the default formatter, JSON. You can disable this option by setting the RespectBrowserAcceptHeader option to true when adding your controller services in Program.cs.
当 Accept 标头包含 / 时,不会使用内容协商,例如在大多数浏览器中。相反,您的应用程序使用默认格式化程序 JSON。在 Program.cs 中添加控制器服务时,您可以通过将 RespectBrowserAcceptHeader 选项设置为 true 来禁用此选项。

You can mix web API Controllers and minimal API endpoints in the same application, but you may find it easier to use one or the other.
您可以在同一应用程序中混合使用 Web API 控制器和最小 API 终端节点,但您可能会发现使用其中一种更容易。

Choose web API controllers when you need content negotiation, when you have complex filtering requirements, when you have experience with web controllers, or when you prefer convention over configuration for your apps.
当您需要内容协商、有复杂的筛选要求、具有 Web 控制器使用经验时,或者当您更喜欢应用程序的约定而不是配置时,请选择 Web API 控制器。

Choose minimal API endpoints when performance is critical, when you prefer explicit configuration over automatic conventions, or when you’re starting a new app.
当性能至关重要时,当您更喜欢显式配置而不是自动约定时,或者当您启动新应用程序时,请选择最少的 API 终端节点。

ASP.NET Core in Action 19 Creating a website with MVC controllers

19 Creating a website with MVC controllers
19 使用 MVC 控制器创建网站

This chapter covers
本章涵盖
• Creating a Model-View-Controller (MVC) application
创建模型-视图-控制器 (MVC) 应用程序
• Choosing between Razor Pages and MVC controllers
在 Razor Pages 和 MVC 控制器之间进行选择
• Returning Razor views from MVC controllers
从 MVC 控制器返回 Razor 视图

In this book I’ve focused on Razor Pages over MVC controllers for server-rendered HTML apps, as I consider Razor Pages to be the preferable paradigm in most cases. In this chapter we dig a bit more into exactly why I consider Razor Pages to be the right choice and take a brief look at the alternative.
在这本书中,我重点介绍了服务器渲染的 HTML 应用程序的 Razor Pages 而不是 MVC 控制器,因为我认为在大多数情况下,Razor Pages 是更可取的范例。在本章中,我们将更深入地探讨为什么我认为 Razor Pages 是正确的选择,并简要介绍一下替代方案。

In section 19.2 you’ll create a default MVC application using a template so you can familiarize yourself with the general project layout of an MVC application. We’ll look at some of the differences between an MVC application and a Razor Pages app, as well as the many similarities.
在 Section 19.2 中,您将使用模板创建默认的 MVC 应用程序,以便熟悉 MVC 应用程序的一般项目布局。我们将了解 MVC 应用程序和 Razor Pages 应用程序之间的一些差异,以及许多相似之处。

Next, I’ll dig into why I find Razor Pages to be a preferable application model compared with MVC controllers. You’ll learn about the improved developer ergonomics of Razor Pages compared with MVC controllers, as well as the cases in which MVC controllers are nevertheless the right choice.
接下来,我将深入探讨为什么我认为与 MVC 控制器相比,Razor Pages 是更可取的应用程序模型。您将了解 Razor Pages 与 MVC 控制器相比改进的开发人员人体工程学,以及 MVC 控制器仍然是正确选择的情况。

In section 19.4 you’ll learn about rendering Razor views using MVC controllers. You’ll learn how the MVC framework relies on conventions to locate view files and how to override these by selecting a specific Razor view template to render. Finally, you’ll see the full view selection algorithm in all its glory.
在第 19.4 节中,您将了解如何使用 MVC 控制器渲染 Razor 视图。您将了解 MVC 框架如何依赖约定来查找视图文件,以及如何通过选择要呈现的特定 Razor 视图模板来覆盖这些文件。最后,您将看到完整视图选择算法的所有荣耀。

19.1 Razor Pages vs. MVC in ASP.NET Core

19.1 Razor Pages 与 ASP.NET Core 中的 MVC

In this book I focus on Razor Pages, but I have also mentioned that Razor Pages use the ASP.NET Core MVC framework behind the scenes and that you can choose to use the MVC framework directly if you wish. Additionally, if you’re creating an API for working with mobile or client-side apps, and you don’t want to (or can’t) use minimal APIs, you may well use the MVC framework directly by creating web API controllers.
在本书中,我重点介绍了 Razor Pages,但我也提到了 Razor Pages 在后台使用 ASP.NET Core MVC 框架,如果您愿意,可以选择直接使用 MVC 框架。此外,如果您正在创建用于移动或客户端应用程序的 API,并且您不想(或不能)使用最少的 API,则可以通过创建 Web API 控制器来直接使用 MVC 框架。

NOTE I look at how to build web APIs with the MVC framework in chapter 20.
注意:在第 20 章中,我将介绍如何使用 MVC 框架构建 Web API。

So what are the differences between Razor Pages and the MVC framework, and when should you choose one or the other?
那么 Razor Pages 和 MVC 框架有什么区别,什么时候应该选择其中之一呢?

If you’re new to ASP.NET Core, the answer is pretty simple: use Razor Pages for server-side rendered applications, and use web API controllers (or minimal APIs) for building APIs. There are nuances to this advice, which I discuss in section 19.5, but that distinction will serve you well for now.
如果你不熟悉 ASP.NET Core,答案非常简单:将 Razor Pages 用于服务器端呈现的应用程序,并使用 Web API 控制器(或最小 API)来构建 API。这个建议有一些细微差别,我在第 19.5 节中讨论,但这种区别现在对你很有帮助。

Naming is hard, again
命名很困难,同样

Microsoft have a long history of creating a framework and naming it after a generic concept: MVC, Web Forms, Web Pages, Multi-platform App UI, and so on. it’s frankly incredible that Blazor survived! Web API is no different.
Microsoft 创建框架并以通用概念命名它的历史由来已久:MVC、Web Forms、Web Pages、Multi-platform App UI 等。坦率地说,Blazor 幸存下来真是不可思议!Web API 也不例外。

In legacy ASP.NET, Microsoft created a web API framework, which was similar in design to the existing MVC framework, but also was not interoperable. You therefore had MVC controllers, which were controller classes used with the MVC framework to generate HTML, and web API controllers, which were controller classes used with the web API framework, to generate JavaScript Object Notation (JSON) or Extensible Markup Language (XML).
在旧版 ASP.NET 中,Microsoft 创建了一个 Web API 框架,该框架在设计上与现有的 MVC 框架相似,但也不可互作。因此,您有 MVC 控制器(与 MVC 框架一起使用的控制器类,用于生成 HTML)和 Web API 控制器(与 Web API 框架一起使用的控制器类),用于生成 JavaScript 对象表示法 (JSON) 或可扩展标记语言 (XML)。

In ASP.NET Core, Microsoft merged these two parallel stacks into a single ASP.NET Core MVC framework. Controllers in ASP.NET Core can generate both HTML and JSON/XML; there is no separation. Nevertheless, it’s common for a controller to be dedicated to either HTML generation or JSON/XML. For that reason, the names MVC controller and web API controller are often used to refer to the two general types of controller: MVC for HTML and web API for JSON/XML.
在 ASP.NET Core 中,Microsoft将这两个并行堆栈合并到一个 ASP.NET Core MVC 框架中。ASP.NET Core 中的控制器可以生成 HTML 和 JSON/XML;没有分离。尽管如此,控制器通常专用于 HTML 生成或 JSON/XML。因此,MVC 控制器和 Web API 控制器这两个名称通常用于指代两种常规类型的控制器:用于 HTML 的 MVC 和用于 JSON/XML 的 Web API。

In this book when I refer to web API controllers, I’m talking about standard ASP.NET Core controllers that are generating API responses. This may be described elsewhere as a web API application using MVC controllers or as a web API application. All three cases refer to the same concept: an HTTP API built using ASP.NET Core controllers.
在本书中,当我提到 Web API 控制器时,我指的是生成 API 响应的标准 ASP.NET Core 控制器。这可能在其他地方描述为使用 MVC 控制器的 Web API 应用程序或 Web API 应用程序。这三种情况都是指同一个概念:使用 ASP.NET Core 控制器构建的 HTTP API。

Before we can get to comparisons, though, we should take a brief look at the ASP.NET Core MVC framework itself. Understanding the similarities and differences between MVC controllers and Razor Pages can be useful, as you’ll likely find a use for MVC controllers at some point, even if you use Razor Pages most of the time.
不过,在进行比较之前,我们应该简要了解一下 ASP.NET Core MVC 框架本身。了解 MVC 控制器和 Razor Pages 之间的异同可能很有用,因为即使您大部分时间都在使用 Razor Pages,您也可能会在某些时候发现 MVC 控制器的用途。

19.2 Your first MVC web application

19.2 您的第一个 MVC Web 应用程序

In this section you’ll learn how to create your first MVC web application, which server-renders HTML pages using MVC controllers and Razor views. We use a template to create the app and compare the generated code to see how it differs from a Razor Pages application.
在本部分中,你将了解如何创建第一个 MVC Web 应用程序,该应用程序使用 MVC 控制器和 Razor 视图服务器呈现 HTML 页面。我们使用模板创建应用并比较生成的代码,以了解它与 Razor Pages 应用程序有何不同。

We’ll again use a template to get an application up and running quickly. This time we’ll use the ASP.NET Core Web App (Model-View-Controller) template. To create the application in Visual Studio, follow these steps:
我们将再次使用模板来快速启动和运行应用程序。这次,我们将使用 ASP.NET Core Web 应用程序 (Model-View-Controller) 模板。要在 Visual Studio 中创建应用程序,请执行以下步骤:

  1. Choose File > New.
    选择 File > New (文件新建)。
  2. In the Create a new project dialog box, select the ASP.NET Core Web App (Model-View-Controller) template.
    在 Create a new project (创建新项目) 对话框中,选择 ASP.NET Core Web App (Model-View-Controller) 模板。
  3. In the Create a new project dialog box, enter your project name and review the Additional information box, shown in figure 19.1.
    在 Create a new project 对话框中,输入您的项目名称并查看 Additional information 框,如图 19.1 所示。
  4. Choose Create. If you’re using the command-line interface (CLI), you can create a similar template using dotnet new mvc.
    选择 Create (创建)。如果您使用的是命令行界面 (CLI),则可以使用 dotnet new mvc 创建类似的模板。

alt text

Figure 19.1 The Additional information screen for the MVC template. This screen follows on from the Configure your new project dialog box and lets you customize the template that generates your application.
图 19.1 MVC 模板的 Additional information 屏幕。此屏幕是 Configure your new project 对话框的后续屏幕,允许您自定义生成应用程序的模板。

The MVC template configures the ASP.NET Core project to use MVC controllers with Razor views. As always, you configure your app to use MVC controllers in Program.cs, as shown in listing 19.1. If you compare this template with your Razor Pages projects, you’ll see that the web API project uses AddControllersWithViews() instead of AddRazorPages(). The MVC controllers are mapped as endpoints by calling MapControllerRoute(). This method maps all the controllers in your app and configures a default conventional route for them. We discussed conventional routing in chapter 14, and I will discuss it again briefly shortly.
MVC 模板将 ASP.NET Core 项目配置为将 MVC 控制器与 Razor 视图一起使用。与往常一样,您将应用程序配置为在 Program.cs中使用 MVC 控制器,如清单 19.1 所示。如果将此模板与 Razor Pages 项目进行比较,你将看到 Web API 项目使用 AddControllersWithViews() 而不是 AddRazorPages()。通过调用 MapControllerRoute() 将 MVC 控制器映射为端点。此方法映射应用程序中的所有控制器,并为它们配置默认的常规路由。我们在第 14 章中讨论了 conventional routing,稍后我将再次简要讨论它。

Listing 19.1 Program.cs for the default MVC project
清单 19.1 默认 MVC 项目的 Program.cs

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();  #A

WebApplication app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");  #B
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();

app.MapControllerRoute(    #C
    name: "default",    #C
    pattern: "{controller=Home}/{action=Index}/{id?}");    #D

app.Run();

❶ AddControllersWithViews adds the services for MVC controllers with Razor Views.
AddControllersWithViews 为具有 Razor 视图的 MVC 控制器添加了服务。

❷ The exception handler path differs from the default Razor Pages path of /Error.
异常处理程序路径不同于默认的 Razor Pages 路径 /Error。
❸ Adds all MVC controllers in your application using conventional routing
使用常规路由在应用程序中添加所有 MVC 控制器
❹ Defines the default conventional route pattern
定义默认的常规路由模式

Much of the configuration for an MVC application is the same as for Razor Pages. The middleware configuration is essentially identical, which isn’t that surprising considering that MVC and Razor Pages are the same type of application: a server-rendered app returning HTML. The main difference, as you’ll see in section 19.3, is in the project structure.
MVC 应用程序的大部分配置与 Razor Pages 的配置相同。中间件配置本质上是相同的,考虑到 MVC 和 Razor Pages 是同一类型的应用程序:返回 HTML 的服务器渲染应用程序,这并不奇怪。正如您将在 19.3 节中看到的那样,主要区别在于项目结构。

Before we go any further, run the MVC application by pressing F5 in Visual Studio or by running dotnet run in the project folder. The application should look remarkably familiar; it’s essentially identical to the Razor Pages version of the application you created in chapter 13, as shown in figure 19.2.
在继续之前,请在 Visual Studio 中按 F5 或在项目文件夹中运行 dotnet run 来运行 MVC 应用程序。该应用程序看起来应该非常熟悉;它与您在第 13 章中创建的应用程序的 Razor Pages 版本基本相同,如图 19.2 所示。

alt text

Figure 19.2 The default MVC application. The resulting application is identical to the Razor Pages equivalent created in chapter 13.
图 19.2 默认的 MVC 应用程序。生成的应用程序与第 13 章中创建的 Razor Pages 等效项相同。

The output of the MVC app is identical to the default Razor Pages app, but the infrastructure used to generate the response differs. Instead of a Razor Page PageModel and page handler, MVC uses the concept of controllers and action methods. The following listing shows the HomeController class from the default application. Each nonabstract, public method is an action that runs in response to a request. You can ensure that a candidate method is not treated as an action method by decorating it with the [NonAction] attribute.
MVC 应用的输出与默认的 Razor Pages 应用相同,但用于生成响应的基础结构不同。MVC 使用控制器和作方法的概念,而不是 Razor Page PageModel 和页面处理程序。下面的清单显示了默认应用程序中的 HomeController 类。每个非抽象的公共方法都是为响应请求而运行的作。您可以通过使用 [NonAction] 属性修饰候选方法,确保它不会被视为作方法。

Listing 19.2 The HomeController for the default MVC app
清单 19.2 默认 MVC 应用的 HomeController

public class HomeController : Controller  #A
{
    private readonly ILogger<HomeController> _logger;
    public HomeController(Ilogger<HomeController> logger)
    {
        _logger = logger;
    }

    public IactionResult Index()  #B
    {
        return View();  #C
    }

    public IactionResult Privacy()
    {
        return View();
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None,  #D
         NoStore = true)]    #D
    public IactionResult Error()
    {
        return View(new ErrorViewModel      #E
        {       #E
            RequestId = Activity.Current?.Id     #E
                ?? HttpContext.TraceIdentifier     #E
        });    #E
    }
}

❶ MVC Controllers often inherit from the Controller base class.
MVC 控制器通常继承自 Controller 基类。
❷ Action methods are the endpoints that run in response to requests.
Action methods 是为响应请求而运行的端点。
❸ Returning View() renders a Razor view.
返回 View() 会呈现 Razor 视图。
❹ You can apply filters to actions, as you’ll learn in chapters 21 and 22.
您可以将过滤器应用于作,您将在第 21 章和第 22 章中学到。
❺ Any object returned with View is passed to the Razor view as a view model.
使用 View 返回的任何对象都将作为视图模型传递给 Razor 视图。

DEFINITION An action (or action method) is a method that runs in response to a request. An MVC controller is a class that contains one or more logically grouped action methods.
定义:一个操作 (或作方法) 是为响应请求而运行的方法。MVC 控制器是包含一个或多个逻辑分组的作方法的类。

Each of the three action methods calls View() and returns the result. This returns a ViewResult, which instructs the MVC framework to render a Razor view for the action. You’ll learn more about this process in section 19.4. The Error action method also sets an object in the call to View(). This is the view model, which is passed to the Razor view when it’s rendered.
这三个作方法中的每一个都调用 View() 并返回结果。这将返回一个 ViewResult,它指示 MVC 框架为作呈现 Razor 视图。您将在 Section 19.4 中了解有关此过程的更多信息。Error作方法还会在对 View() 的调用中设置一个对象。这是视图模型,在呈现视图时传递给 Razor 视图。

NOTE MVC controllers use explicit view models to pass data to a Razor view rather than expose the data as properties on themselves (as Razor Pages do with page models). This provides a clearer separation between the various “models” than in Razor Pages, though both Razor Pages cases use the same general MVC design pattern.
注意:MVC 控制器使用显式视图模型将数据传递到 Razor 视图,而不是将数据作为自身的属性公开(就像 Razor 页面对页面模型所做的那样)。与 Razor Pages 相比,这在各种“模型”之间提供了更清晰的分离,尽管两种 Razor Pages 情况都使用相同的通用 MVC 设计模式。

Another big difference between Razor Pages and MVC controllers is that MVC controllers typically use conventional routing, as opposed to the explicit routing used by Razor Pages. I touched on conventional routing and how it differs from explicit routing in chapter 14, but you can see it in action in this MVC application.
Razor Pages 和 MVC 控制器之间的另一个重大区别是,MVC 控制器通常使用传统路由,而不是 Razor Pages 使用的显式路由。我在第 14 章中谈到了传统路由以及它与显式路由的不同之处,但您可以在此 MVC 应用程序中看到它的实际应用。

Conventional routing defines one or more route template patterns, which are used for all the MVC controllers in your app. The default route template, shown in listing 19.1, consists of three optional segments:
传统路由定义一个或多个路由模板模式,这些模式用于应用程序中的所有 MVC 控制器。默认路由模板,如清单 19.1 所示,由三个可选段组成:

"{controller=Home}/{action=Index}/{id?}"

Conventional routes must describe which controller and action should run for any given request, so they must include controller and action route parameters at a minimum. When a request is received, ASP.NET Core matches the route template and from that calculates which MVC controller and action method to use. For example, the default route would match all the following URLs:
传统路由必须描述应该为任何给定请求运行哪个控制器和作,因此它们必须至少包含控制器和作路由参数。收到请求时,ASP.NET Core 会匹配路由模板,并从中计算要使用的 MVC 控制器和作方法。例如,默认路由将匹配以下所有 URL:

• /Home/Privacy — Executes the HomeController.Privacy() action
• /Home — Executes the HomeController.Index() action
• /customer/list — Executes the CustomerController.List() action
• /products/view/123 — Executes the ProductsController.View() action, with the route parameter id=123

With conventional routing, a single route template maps to multiple endpoints, whereas in explicit routing, one or more route templates typically map to a single endpoint. There are subtleties in both cases, but in general conventional routing is terser, and explicit routing is more expressive.
使用传统路由时,单个路由模板映射到多个终端节点,而在显式路由中,一个或多个路由模板通常映射到单个终端节点。这两种情况都有微妙之处,但通常 conventional routing 更简洁,而 explicit routing 更具表现力。

NOTE As I mentioned in chapter 14, I won’t discuss conventional routing any further in this book. It is often used only with MVC controllers, but even then, I generally prefer to use explicit routing with attributes. I describe how to use attribute routing in chapter 20 when I discuss web API controllers.
注意:正如我在第 14 章中提到的,我不会在本书中进一步讨论 conventional routing。它通常只与 MVC 控制器一起使用,但即便如此,我通常更喜欢使用带有属性的显式路由。在第 20 章中讨论 Web API 控制器时,我将介绍如何使用属性路由。

Once you’ve familiarized yourself with a basic MVC application you will likely have spotted many of the similarities and differences between the MVC framework and Razor Pages. In the next section we look at one aspect of this: MVC controllers and their Razor Page PageModel equivalent.
熟悉基本的 MVC 应用程序后,您可能已经发现了 MVC 框架和 Razor Pages 之间的许多相似之处和不同之处。在下一部分中,我们将介绍其中一个方面:MVC 控制器及其 Razor Page PageModel 等效项。

19.3 Comparing an MVC controller with a Razor Page PageModel

19.3 将 MVC 控制器与 Razor Page PageModel 进行比较

In chapter 13 we looked at the MVC design pattern, and at how it applies to Razor Pages in ASP.NET Core. Perhaps unsurprisingly, you can use MVC controllers with the MVC design pattern in almost exactly the same way.
在第 13 章中,我们了解了 MVC 设计模式,以及它如何应用于 ASP.NET Core 中的 Razor 页面。也许不足为奇的是,您可以以几乎完全相同的方式将 MVC 控制器与 MVC 设计模式一起使用。

As mentioned in section 19.2, MVC controllers and actions are analogous to their Razor Pages counterparts of PageModel and page handlers. Figure 19.3 makes this clearer; it is the MVC controller equivalent of the Razor Pages version from chapter 13.
如第 19.2 节所述,MVC 控制器和作类似于 PageModel 和页面处理程序的 Razor Pages 对应项。图 19.3 更清楚地说明了这一点;它是第 13 章中 Razor Pages 版本的 MVC 控制器等效项。

alt text

Figure 19.3 A complete MVC controller request for a category. The MVC controller pattern is almost identical to that of Razor Pages, which was shown in figure 13.12. The controller is equivalent to a Razor Page, and the action is equivalent to a page handler.
图 19.3 类别的完整 MVC 控制器请求。MVC 控制器模式与 Razor Pages 的模式几乎相同,如图 13.12 所示。控制器等效于 Razor Page,作等效于页面处理程序。

In chapter 13 I showed a simple Razor Page PageModel for displaying all the to-do items in a given category in a ToDO application. The following listing reproduces that Razor Pages code from listing 13.5 for convenience.
在第 13 章中,我展示了一个简单的 Razor Page PageModel,用于在 ToDO 应用程序中显示给定类别中的所有待办事项。为方便起见,以下清单复制了清单 13.5 中的 Razor Pages 代码。

Listing 19.3 A Razor Page for viewing all to-do items in a given category
清单 19.3 用于查看给定类别中所有待办事项的 Razor 页面

public class CategoryModel : PageModel
{
    private readonly ToDoService _service;
    public CategoryModel(ToDoService service)
    {
        _service = service;
    }

    public ActionResult OnGet(string category)
    {
        Items = _service.GetItemsForCategory(category);
        return Page();
    }

    public List<ToDoListModel> Items { get; set; }
}

The MVC equivalent of this Razor Page is shown in listing 19.4. In the MVC framework, controllers are often used to aggregate similar actions, so the controller in this case is called ToDoController, as it would typically contain additional action methods for working with to-do items, such as actions to view a specific item or to create a new one.
此 Razor Page 的 MVC 等效项显示在清单 19.4 中。在 MVC 框架中,控制器通常用于聚合类似的作,因此在这种情况下,控制器称为 ToDoController,因为它通常包含用于处理待办事项的其他作方法,例如查看特定项或创建新项的作。

Listing 19.4 An MVC controller for viewing all to-do items in a given category
清单 19.4 一个 MVC 控制器,用于查看给定类别中的所有待办事项

public class ToDoController : Controller
{
    private readonly ToDoService _service;       #A
    public ToDoController(ToDoService service)   #A
    {
        _service = service;
    }

    public ActionResult Category(string id)     #B
    {
        var items = _service.GetItemsForCategory(id);     #C
        return View(items);    #D
    }

    public ActionResult Create(ToDoListModel model)   #E
    {                                                 #E
        // ...                                        #E
    }                                                 #E
}

❶ The ToDoService is provided in the controller constructor using dependency injection.
ToDoService 使用依赖项注入在控制器构造函数中提供。
❷ The Category action method takes a parameter, id.
Category作方法采用参数 id。
❸ The action method calls out to the ToDoService to retrieve data and build a view model.
方法调用 ToDoService 以检索数据并构建视图模型。
❹ Returns a ViewResult indicating the Razor view should be rendered, passing in the view model
返回一个 ViewResult,指示应呈现 Razor 视图,传入视图模型
❺ MVC controllers often contain multiple action methods that respond to different requests.
MVC 控制器通常包含多个响应不同请求的作方法。

Aside from some naming differences, the ToDoController looks similar to the Razor Page equivalent from listing 19.3:
除了一些命名差异之外,ToDoController 看起来类似于清单 19.3 中的 Razor Page 等效项:

• They both use dependency injection to access services.
它们都使用依赖关系注入来访问服务。
• Both handlers (page handler and action method) accept parameters created using model binding in exactly the same way.
两个处理程序 (页面处理程序和作方法) 都以完全相同的方式接受使用模型绑定创建的参数。
• Both interact with the application model in the same way to handle the request.
两者以相同的方式与应用程序模型交互以处理请求。
• They both create a view model for rendering the Razor view.
它们都创建用于渲染 Razor 视图的视图模型。

One of the main differences between Razor Pages and MVC controllers is in the final step: rendering the Razor view. In the next section you’ll see how to render Razor views from your MVC controller actions, how the views differ from the Razor views you’ve seen with Razor Pages, and how the framework locates the correct Razor view to render.
Razor Pages 和 MVC 控制器之间的主要区别之一是最后一步:呈现 Razor 视图。在下一部分中,你将了解如何从 MVC 控制器作呈现 Razor 视图,这些视图与使用 Razor 页面看到的 Razor 视图有何不同,以及框架如何找到要呈现的正确 Razor 视图。

19.4 Selecting a view from an MVC controller

19.4 从 MVC 控制器中选择视图

This section covers
本节涵盖
• How MVC controllers use ViewResults to render Razor views
MVC 控制器如何使用 ViewResults 呈现 Razor 视图
• How to create a new Razor view
如何创建新的 Razor 视图
• How the framework locates a Razor view to render
框架如何查找要呈现的 Razor 视图

One of the major differences between MVC controllers and Razor Pages is how the page handler or action method chooses a Razor view to render. For Razor Pages, it’s easy; the page renders the Razor view associated with the page. For MVC controllers it’s more complicated, so it’s important to understand how you choose which view to render once an action method has executed. Figure 19.4 shows a zoomed-in view of this process, right after the action has invoked the application model and received some data back.
MVC 控制器和 Razor Pages 之间的主要区别之一是页面处理程序或作方法如何选择要呈现的 Razor 视图。对于 Razor Pages,这很容易;页面呈现与页面关联的 Razor 视图。对于 MVC 控制器,情况更复杂,因此了解在执行作方法后如何选择要呈现的视图非常重要。图 19.4 显示了此过程的放大视图,该视图是在 action 调用应用程序模型并接收一些数据之后。

alt text

Figure 19.4 The process of generating HTML from an MVC controller using a ViewResult. This is similar to the process for a Razor Page. The main difference is that for Razor Pages, the view is an integral part of the Razor Page; for MVC controllers, the view must be located at runtime.
图 19.4 使用 ViewResult 从 MVC 控制器生成 HTML 的过程。这类似于 Razor 页面的过程。主要区别在于,对于 Razor Pages,视图是 Razor Page 不可或缺的一部分;对于 MVC 控制器,视图必须位于运行时。

Some of this figure should be familiar; it’s the bottom half of figure 19.3 (with a couple of additions). It shows that the MVC controller action method uses a ViewResult object to indicate that a Razor view should be rendered. This ViewResult contains the name of the Razor view template to render and a view model, an arbitrary plain old CLR object (POCO) class containing the data to render.
这个人物中的一些人应该很熟悉;它是图 19.3 的下半部分(添加了一些内容)。它显示 MVC 控制器作方法使用 ViewResult 对象来指示应呈现 Razor 视图。此 ViewResult 包含要呈现的 Razor 视图模板的名称,以及视图模型,即包含要呈现的数据的任意普通旧 CLR 对象 (POCO) 类。

NOTE ViewResult is the MVC equivalent of a Razor Page’s PageResult. The main difference is that a ViewResult includes a view name to render and a model to pass to the view template, while a PageResult always renders the Razor Page’s associated view and always passes the PageModel to the view template.
注意:ViewResult 是 Razor 页面的 PageResult 的 MVC 等效项。主要区别在于 ViewResult 包括要呈现的视图名称和要传递给视图模板的模型,而 PageResult 始终呈现 Razor Page 的关联视图,并始终将 PageModel 传递给视图模板。

After returning a ViewResult from an action method, the control flow passes back to the MVC framework, which uses a series of heuristics to locate the view, based on the template name provided. Once it locates the Razor view template, the Razor engine passes the view model from the ViewResult to the view and executes the template to generate the final HTML. This final step, rendering the HTML, is essentially the same process as for Razor Pages.
从作方法返回 ViewResult 后,控制流将传递回 MVC 框架,该框架根据提供的模板名称使用一系列启发式方法来查找视图。找到 Razor 视图模板后,Razor 引擎会将视图模型从 ViewResult 传递到视图,并执行模板以生成最终 HTML。最后一步(呈现 HTML)与 Razor Pages 的过程基本相同。

You can add a new Razor view template to your application in Visual Studio by right-clicking the folder you wish to add the view to in Solution Explorer. Choose Add > New Item and then select Razor View - Empty from the dialog, as shown in figure 19.5. If you aren’t using Visual Studio, create a blank new file in the Views folder with the file extension .cshtml.
通过在解决方案资源管理器中右键单击要向其添加视图的文件夹,可以在 Visual Studio 中将新的 Razor 视图模板添加到应用程序中。从对话框中选择Add > New Item 并选择 Razor View - Empty,如图 19.5 所示。如果不使用 Visual Studio,请在 Views 文件夹中创建一个文件扩展名为 .cshtml 的空白新文件。

alt text

Figure 19.5 The Add New Item dialog box. Choosing Razor View - Empty adds a new Razor view template file to your application.
图 19.5 “添加新项”对话框。选择“Razor 视图 - 空”会将新的 Razor 视图模板文件添加到应用程序中。

Razor view files are almost identical to the Razor Page .cshtml files you saw in chapter 17. The only difference is that Razor view files must not specify a @page directive at the top of the file. Aside from that, they’re identical; you can use the same syntax, partial views, layouts, and view models as you can with Razor Pages. The following listing, for example, shows part of the Error.cshtml Razor view for the default MVC template. This is all recognizable as standard Razor syntax.
Razor 视图文件与你在第 17 章中看到的 Razor Page .cshtml 文件几乎相同。唯一的区别是 Razor 视图文件不得在文件顶部指定 @page 指令。除此之外,它们是相同的;您可以使用与 Razor Pages 相同的语法、分部视图、布局和视图模型。例如,以下列表显示了默认 MVC 模板的 Error.cshtml Razor 视图的一部分。这都是可识别为标准 Razor 语法的。

Listing 19.5 A Razor view
清单 19.5 Razor 视图

@model ErrorViewModel    #A
@{
    ViewData["Title"] = "Error";    #B
}

<h1 class="text-danger">Error.</h1>    #C
<h2 class="text-danger">An error occurred while 
    processing your request.</h2>

@if (Model.ShowRequestId)    #D
{
    <p>
        <strong>Request ID:</strong> <code>@Model.RequestId</code>    #E
    </p>
}

❶ Razor views may specify a view model.
Razor 视图可以指定视图模型。
❷ You can access ViewData, and execute arbitrary C# statements.
您可以访问 ViewData 并执行任意 C# 语句。
❸ Standard HTML is written directly to the output.
标准 HTML 直接写入输出。
❹ You can use standard Razor control statements and can access the view model using Model.
您可以使用标准 Razor 控制语句,并可以使用 Model 访问视图模型。
❺ You can write C# expressions using @.
您可以使用 @ 编写 C# 表达式。

With your view template created, you now need to execute it. In most cases you won’t create a ViewResult directly in your action methods. Instead, you’ll use one of the View() helper methods on the Controller base class. These helper methods simplify passing in a view model and selecting a view template, but there’s nothing magic about them; all they do is create ViewResult objects.
创建视图模板后,您现在需要执行它。在大多数情况下,您不会直接在作方法中创建 ViewResult。相反,你将在 Controller 基类上使用 View() 帮助程序方法之一。这些帮助程序方法简化了视图模型的传入和视图模板的选择,但它们并没有什么神奇之处;他们所做的只是创建 ViewResult 对象。

In the simplest case you can call the View method without any arguments, as shown in the following listing, taken from the default MVC application. The View() helper method returns a ViewResult that uses conventions to find the view template to render and does not supply a view model when executing the view.
在最简单的情况下,可以调用 View 方法,不带任何参数,如下面的清单所示,它取自默认 MVC 应用程序。帮助程序方法返回一个 ViewResult,该方法使用约定查找要呈现的视图模板,并且在执行视图时不提供视图模型。

Listing 19.6 Returning ViewResult from an action method using default conventions
示例 19.6 使用默认约定从作方法返回 ViewResult

public class HomeController : Controller     #A
{
    public IActionResult Index()
    {
        return View();     #B
    }
}

In this example, the View helper method returns a ViewResult without specifying the name of the template to run. Instead, the name of the template to use is based on the name of the controller and the name of the action method. Given that the controller is called HomeController and the method is called Index, by default the Razor template engine looks for a template at the Views/Home/Index.cshtml location, as shown in figure 19.6.
在此示例中, View 帮助程序方法返回 ViewResult,而不指定要运行的模板的名称。相反,要使用的模板的名称基于控制器的名称和作方法的名称。鉴于控制器名为 HomeController 且方法名为 Index,默认情况下,Razor 模板引擎会在 Views/Home/Index.cshtml 位置查找模板,如图 19.6 所示。

alt text

Figure 19.6 View files are located at runtime based on naming conventions. Razor view files reside in a folder based on the name of the associated MVC controller and are named with the name of the action method that requested them. Views in the Shared folder can be used by any controller.
图 19.6 视图文件在运行时根据命名约定进行定位。Razor 视图文件驻留在基于关联 MVC 控制器名称的文件夹中,并使用请求它们的作方法的名称命名。Shared 文件夹中的视图可由任何控制器使用。

This is another case of using conventions in MVC to reduce the amount of boilerplate you have to write. As always, the conventions are optional. You can also explicitly pass the name of the template to run as a string to the View method. For example, if the Index method in listing 19.6 instead returned View("ListView"), the templating engine would look for a template called ListView.cshtml instead. You can even specify the complete path to the view file, relative to your application’s root folder, such as View("Views/global.cshtml"), which would look for the template at the Views/global.chtml location.
这是在 MVC 中使用约定来减少必须编写的样板数量的另一种情况。与往常一样,约定是可选的。还可以将要作为字符串运行的模板的名称显式传递给 View 方法。例如,如果列表 19.6 中的 Index 方法返回 View(“ListView”),则模板化引擎将改为查找名为 ListView.cshtml 的模板。您甚至可以指定视图文件相对于应用程序根文件夹的完整路径,例如 View(“Views/global.cshtml”),它将在 Views/global.chtml 位置查找模板。

NOTE When specifying the absolute path to a view, you must include both the top-level Views folder and the .cshtml file extension in the path. This is similar to the rules for locating partial view templates.
注意:指定视图的绝对路径时,必须在路径中同时包含顶级 Views 文件夹和 .cshtml 文件扩展名。这类似于查找局部视图模板的规则。

The process of locating an MVC Razor view is similar to the process of locating a partial view to render, which you learned about in chapter 17. The framework searches in multiple locations to find the requested view. The difference is that for Razor Pages the search process happens only for partial view rendering, as the main Razor view to render is already known; it’s the Razor Page’s view template.
查找 MVC Razor 视图的过程类似于查找要呈现的部分视图的过程,您在第 17 章中了解了该过程。框架在多个位置搜索以查找请求的视图。区别在于,对于 Razor Pages,搜索过程仅针对部分视图呈现进行,因为要呈现的主 Razor 视图是已知的;它是 Razor 页面的视图模板。

alt text

Figure 19.7 shows the complete process used by the MVC framework to locate the correct View template to execute when a ViewResult is returned from an MVC controller. It’s possible for more than one template to be eligible, such as if an Index.chstml file exists in both the Home and Shared folders. Similar to the rules for locating partial views, the engine uses the first template it finds.
图 19.7 显示了 MVC 框架用于查找从 MVC 控制器返回 ViewResult 时要执行的正确 View 模板的完整过程。多个模板可能符合条件,例如,如果 Index .chstml 文件同时存在于 Home 和 Shared 文件夹中。与查找分部视图的规则类似,引擎使用它找到的第一个模板。

Figure 19.7 A flow chart describing how the Razor templating engine locates the correct view template to execute. Avoiding the complexity of this diagram is one of the reasons I recommend using Razor Pages wherever possible!
图 19.7 描述 Razor 模板化引擎如何查找要执行的正确视图模板的流程图。避免此图表的复杂性是我建议尽可能使用 Razor Pages 的原因之一!

Tip You can modify all these conventions, including the algorithm shown in figure 19.8, during initial configuration. In fact, you can replace the whole Razor templating engine if you really want to!
提示:在初始配置期间,您可以修改所有这些约定,包括图 19.8 中所示的算法。事实上,如果您真的愿意,您可以替换整个 Razor 模板引擎!

You may find it tempting to explicitly provide the name of the view file you want to render in your controller; if so, I’d encourage you to fight that urge. You’ll have a much simpler time if you embrace the conventions as they are and go with the flow. That extends to anyone else who looks at your code; if you stick to the standard conventions, there’ll be a comforting familiarity when they look at your app. That can only be a good thing!
你可能会发现显式提供要在控制器中渲染的视图文件的名称很诱人;如果是这样,我鼓励你克制这种冲动。如果您接受惯例并顺其自然,您将度过一段轻松得多的时光。这延伸到查看你的代码的任何其他人;如果您遵守标准约定,当他们查看您的应用程序时,会有一种令人欣慰的熟悉感。那只能是一件好事!

As well as providing a view template name, you can also pass an object to act as the view model for the Razor view. This object should match the type specified in the view’s @model directive, and it’s accessed in exactly the same way as for Razor Pages; using the Model property.
除了提供视图模板名称外,您还可以传递一个对象以充当 Razor 视图的视图模型。此对象应与视图的 @model 指令中指定的类型匹配,并且访问方式与 Razor Pages 完全相同;使用 Model 属性。

Tip All the other ways of passing data to the view I described in chapter 17 are available in MVC controllers too. You should generally favor the view model where possible, but you can also use ViewData, TempData, or @inject services, for example.
提示:我在第 17 章中描述的所有其他将数据传递给视图的方法在 MVC 控制器中也可用。通常,您应该尽可能使用视图模型,但也可以使用 ViewData、TempData 或 @inject 服务等。

The following listing shows two examples of passing a view model to a view.
下面的清单显示了将视图模型传递给视图的两个示例。

Listing 19.7 Returning ViewResult from an action method using default conventions
示例 19.7 使用默认约定从作方法返回 ViewResult

public class ToDoController : Controller
{
    public IActionResult Index()
    {
        var listViewModel = new ToDoListModel();     #A
        return View(listViewModel);      #B
    }
    public IActionResult View(int id)
    {
        var viewModel = new ViewToDoModel();
        return View("ViewToDo", viewModel);    #C
    }

}

Once the Razor view template has been located, the view is rendered using the Razor syntax you learned about in chapters 17 and 18. You can use all the features you’ve already seen—layouts, partial views, _ViewImports, and _ViewStart, for example. From the point of view of the Razor view, there’s no difference between a Razor Pages view and an MVC Razor view.
找到 Razor 视图模板后,将使用您在第 17 章和第 18 章中学到的 Razor 语法呈现视图。您可以使用您已经见过的所有功能 — 例如布局、分部视图、_ViewImports 和 _ViewStart。从 Razor 视图的角度来看,Razor 页面视图和 MVC Razor 视图之间没有区别。

Now you’ve had a brief overview of an MVC application, we can look in more depth about when to choose MVC controllers over Razor Pages.
现在您已经简要概述了 MVC 应用程序,我们可以更深入地了解何时选择 MVC 控制器而不是 Razor Pages。

19.5 Choosing between Razor Pages and MVC controllers

19.5 在 Razor Pages 和 MVC 控制器之间进行选择

Throughout this book, I have said that you should generally choose Razor Pages for server-rendered applications instead of using MVC controllers. In this section I show the difference between Razor Pages and MVC controllers from a project structure point of view and defend my reasoning. I also describe the cases where MVC controllers are a good choice.
在本书中,我一直说过,您通常应该为服务器呈现的应用程序选择 Razor Pages,而不是使用 MVC 控制器。在本节中,我将从项目结构的角度展示 Razor Pages 和 MVC 控制器之间的区别,并为我的推理辩护。我还介绍了 MVC 控制器是不错选择的情况。

If you’re familiar with legacy .NET Framework ASP.NET or earlier versions of ASP.NET Core, you may already be familiar and comfortable with MVC controllers. If you’re unsure whether to stick to what you know or switch to Razor Pages, this section should help you choose. Developers coming from those backgrounds often have misconceptions about Razor Pages initially (as I did!), incorrectly equating them with Web Forms and overlooking their underlying basis of the MVC framework. This section attempts to set the record straight.
如果您熟悉旧版 .NET Framework ASP.NET 或 ASP.NET Core 的早期版本,则您可能已经熟悉并熟悉 MVC 控制器。如果您不确定是坚持您所知道的还是切换到 Razor Pages,本节应该可以帮助您进行选择。来自这些背景的开发人员最初通常对 Razor Pages 有误解(就像我一样),错误地将它们等同于 Web 窗体,而忽略了它们作为 MVC 框架的底层基础。本节试图澄清事实。

Indeed, architecturally, Razor Pages and MVC are essentially equivalent, as they both use the MVC design pattern. The most obvious differences relate to where the files are placed in your project, as I discuss in the next section.
事实上,从架构上讲,Razor Pages 和 MVC 本质上是等效的,因为它们都使用 MVC 设计模式。最明显的区别与文件在项目中的放置位置有关,我将在下一节中讨论。

19.5.1 The benefits of Razor Pages

19.5.1 Razor Pages 的优势

In section 19.5 I showed that the code for an MVC controller looks similar to the code for a Razor Page PageModel. If that’s the case, what benefit is there to using Razor Pages? In this section I discuss some of the pain points of MVC controllers and how Razor Pages attempts to address them.
在第 19.5 节中,我展示了 MVC 控制器的代码看起来类似于 Razor Page PageModel 的代码。如果是这样的话,使用 Razor Pages 有什么好处?在本节中,我将讨论 MVC 控制器的一些痛点,以及 Razor Pages 如何尝试解决这些痛点。

Razor Pages are not Web Forms
Razor Pages 不是 Web Forms

A common argument I hear from existing ASP.NET developers against Razor Pages is “Oh, they’re just Web Forms.” That sentiment misses the mark in many ways, but it’s common enough that it’s worth addressing directly.
我从现有的 ASP.NET 开发人员那里听到的反对 Razor Pages 的常见论点是“哦,它们只是 Web Forms”。这种情绪在很多方面都错失了目标,但它足够普遍,值得直接解决。

Web Forms was a web-programming model that was released as part of .NET Framework 1.0 in 2002. It attempted to provide a highly productive experience for developers moving from desktop development to the web for the first time.
Web Forms是一种 Web 编程模型,于 2002 年作为 .NET Framework 1.0 的一部分发布。它试图为首次从桌面开发转向 Web 的开发人员提供高效的体验。

Web Forms are much maligned now, but their weaknesses only became apparent later. Web Forms attempted to hide the complexities of the web from you, to give you the impression of developing a desktop app. That often resulted in apps that were slow, with lots of interdependencies, and that were hard to maintain.
Web Forms现在受到了很多诟病,但它们的弱点后来才显现出来。Web Forms 试图向您隐藏 Web 的复杂性,让您觉得自己在开发桌面应用程序。这通常会导致应用程序运行缓慢、存在大量相互依赖关系并且难以维护。

Web Forms provided a page-based programming model, which is why Razor Pages sometimes gets associated with them. However, as you’ve seen, Razor Pages is based on the MVC design pattern, and it exposes the intrinsic features of the web without trying to hide them from you.
Web Forms 提供了基于页面的编程模型,这就是 Razor Pages 有时会与它们相关联的原因。但是,正如你所看到的,Razor Pages 基于 MVC 设计模式,它公开了 Web 的内在功能,而不会试图对你隐藏它们。

Razor Pages optimizes certain flows using conventions, but it’s not trying to build a stateful application model over the top of a stateless web application, in the way that Web Forms did.
Razor Pages 使用约定优化某些流,但它不会像 Web 窗体那样尝试在无状态 Web 应用程序之上构建有状态应用程序模型。

If you were a fan of Web Forms’ stateful application model, you should consider Blazor Server, which uses a similar paradigm but embraces the web instead of fighting against it. You can read more about the similarities at https://learn.microsoft.com/zh-cn/dotnet/architecture/blazor-for-web-forms-developers/.
如果你是 Web Forms 的有状态应用程序模型的粉丝,你应该考虑 Blazor Server,它使用类似的范例,但拥抱 Web,而不是与之对抗。您可以在 https://learn.microsoft.com/zh-cn/dotnet/architecture/blazor-for-web-forms-developers/ 上阅读有关相似之处的更多信息。

In MVC, a single controller can have multiple action methods. Each action handles a different request and generates a different response. The grouping of multiple actions in a controller is somewhat arbitrary, but it’s typically used to group actions related to a specific entity or resource: to-do list items in this case. A more complete version of the ToDoController in listing 19.4 might include action methods for listing all to-do items, for creating new items, and for deleting items, for example. Unfortunately, you can often find that your controllers become large and bloated, with many dependencies.[1]
在 MVC 中,单个控制器可以有多个方法。每个作处理不同的请求并生成不同的响应。控制器中多个作的分组在某种程度上是任意的,但它通常用于对与特定实体或资源相关的作进行分组:在本例中为待办事项列表项。例如,清单 19.4 中更完整的 ToDoController 版本可能包括用于列出所有待办事项、创建新项和删除项的作方法。不幸的是,您经常会发现您的控制器变得庞大而臃肿,并且具有许多依赖项。[1]

NOTE You don’t have to make your controllers very large like this. It’s just a common pattern. You could, for example, create a separate controller for every action instead.
注意:您不必像这样将控制器做得非常大。这只是一种常见的模式。例如,您可以为每个作创建一个单独的控制器。

Another pitfall of MVC controllers is the way they’re typically organized in your project. Most action methods in a controller need an associated Razor view, for generating the HTML, and a view model for passing data to the view. The MVC approach in .NET traditionally groups classes by type (controller, view, view model), while the Razor Page approach groups by function; everything related to a specific page is co-located.
MVC 控制器的另一个缺陷是它们在项目中的组织方式。控制器中的大多数作方法都需要一个关联的 Razor 视图(用于生成 HTML)和一个视图模型(用于将数据传递到视图)。.NET 中的 MVC 方法传统上按类型(控制器、视图、视图模型)对类进行分组,而 Razor Page 方法按函数分组;与特定页面相关的所有内容都位于同一位置。

Figure 19.8 compares the file layout for a simple Razor Pages project with the MVC equivalent. Using Razor Pages means much less scrolling up and down between the controller, views, and view model folders whenever you’re working on a particular page. Everything you need is found in two files, the .cshtml Razor view and the (nested) .cshtml.cs PageModel file.
图 19.8 将简单 Razor Pages 项目的文件布局与 MVC 等效项进行了比较。使用 Razor Pages 意味着在处理特定页面时,在控制器、视图和视图模型文件夹之间上下滚动的时间要少得多。您需要的所有内容都可以在两个文件中找到:.cshtml Razor 视图和(嵌套的).cshtml.cs PageModel 文件。

alt text

Figure 19.8 Comparing the folder structure for an MVC project with the folder structure for a Razor Pages project
图 19.8 将 MVC 项目的文件夹结构与 Razor Pages 项目的文件夹结构进行比较

There are additional differences between MVC and Razor Pages, which I have highlighted throughout the book, but this layout difference is really the biggest win. Razor Pages embraces the fact that you’re building a page-based application and optimizes your workflow by keeping everything related to a single page together.
MVC 和 Razor Pages 之间还有其他差异,我在整本书中都强调了这些差异,但这种布局差异确实是最大的胜利。Razor Pages 接受了您正在构建基于页面的应用程序这一事实,并通过将与单个页面相关的所有内容放在一起来优化您的工作流程。

Tip You can think of each Razor Page as a mini controller focused on a single page. Page handlers are functionally equivalent to MVC controller action methods.
提示:您可以将每个 Razor 页面视为一个专注于单个页面的迷你控制器。页面处理程序在功能上等同于 MVC 控制器作方法。

This layout also has the benefit of making each page a separate class. This contrasts with the MVC approach of making each page an action on a given controller. Each Razor Page is cohesive for a particular feature, such as displaying a to-do item. MVC controllers contain action methods that handle multiple different features for a more abstract concept, such as all the features related to to-do items.
此布局还具有使每个页面成为单独类的优点。这与 MVC 方法形成鲜明对比,后者将每个页面都设置为给定控制器上的作。每个 Razor 页面对于特定功能(例如显示待办事项)都是内聚的。MVC 控制器包含作方法,这些方法处理多个不同功能,以实现更抽象的概念,例如与待办事项相关的所有功能。

NOTE ASP.NET Core is eminently customizable, so you don’t have to group your MVC applications by type; it’s simply the default state and the easy path. In fact, if you do choose to use MVC controllers, I strongly suggest grouping using feature folders instead. This MSDN article provides a good introduction: http://mng.bz/mVOr.
注意: ASP.NET Core 是高度可定制的,因此您不必按类型对 MVC 应用程序进行分组;它只是默认状态和简单的路径。事实上,如果您确实选择使用 MVC 控制器,我强烈建议改用功能文件夹进行分组。这篇 MSDN 文章提供了一个很好的介绍:http://mng.bz/mVOr

Another important point is that Razor Pages doesn’t lose any of the separation of concerns that MVC has. The view part of Razor Pages is still concerned only with rendering HTML, and the handler is the coordinator that calls out to the application model. The only real difference is the lack of the explicit view model that you have in MVC, but it’s perfectly possible to emulate this in Razor Pages if that’s a deal-breaker for you.
另一个重要的一点是,Razor Pages 不会失去 MVC 所具有的任何关注点分离。Razor Pages 的视图部分仍然只关心呈现 HTML,处理程序是调用应用程序模型的协调器。唯一真正的区别是缺少 MVC 中的显式视图模型,但如果这对您来说是一个交易破坏者,那么在 Razor Pages 中完全可以模拟它。

The benefits of using Razor Pages are particularly noticeable when you have content websites, such as marketing websites, where you’re mostly displaying static data and there’s no real logic. In that case, MVC adds complexity without any real benefits, as there’s not really any logic in the controllers at all. Another great use case is when you’re creating forms for users to submit data. Razor Pages is especially optimized for this scenario, as you saw in previous chapters.
当你拥有内容网站(如营销网站)时,使用 Razor Pages 的好处尤其明显,因为你主要显示静态数据,没有真正的逻辑。在这种情况下,MVC 增加了复杂性,但没有任何实际的好处,因为控制器中根本没有任何真正的逻辑。另一个很好的用例是当您为用户创建表单以提交数据时。Razor Pages 特别针对此方案进行了优化,如前几章所示。

Clearly, I’m a fan of Razor Pages, but that’s not to say they’re perfect for every situation. In the next section I discuss some of the cases when you might choose to use MVC controllers in your application. Bear in mind it’s not an either-or choice; it’s possible to use MVC controllers, Razor Pages, and even minimal APIs in the same application, and in many cases that may be the best option.
显然,我是 Razor Pages 的粉丝,但这并不是说它们适合所有情况。在下一节中,我将讨论一些您可能会选择在应用程序中使用 MVC 控制器的情况。请记住,这不是一个非此即彼的选择;可以在同一应用程序中使用 MVC 控制器、Razor Pages 甚至最小的 API,在许多情况下,这可能是最佳选择。

19.5.2 When to choose MVC controllers over Razor Pages

19.5.2 何时选择 MVC 控制器而不是 Razor Pages

Razor Pages are great for building page-based server-side rendered applications. But not all applications fit that mold, and even some applications that do fall in that category might be best developed using MVC controllers instead of Razor Pages. These are a few such scenarios:
Razor Pages 非常适合构建基于页面的服务器端渲染应用程序。但并非所有应用程序都适合这种模式,甚至一些属于该类别的应用程序也可能最好使用 MVC 控制器而不是 Razor Pages 进行开发。以下是一些这样的场景:

• When you don’t want to render views—Razor Pages are best for page-based applications, where you’re rendering a view for the user. If you’re building an HTTP API, you should use minimal APIs or MVC (web API) controllers instead. You’ll learn about web API controllers in chapter 20.
当您不想呈现视图时 - Razor Pages 最适合基于页面的应用程序,您可以在其中为用户呈现视图。如果要构建 HTTP API,则应改用最少的 API 或 MVC (Web API) 控制器。您将在第 20 章中了解 Web API 控制器。

• When you’re converting an existing MVC application to ASP.NET Core—If you already have a legacy ASP.NET application that you’re converting to ASP.NET Core or an app using an early version of ASP.NET Core that you’re updating, you’re likely using MVC controllers. It’s probably not worth converting your existing MVC controllers to Razor Pages in this case. It makes more sense to keep your existing code and consider whether to do new development in the application with Razor Pages.
将现有 MVC 应用程序转换为 ASP.NET Core 时 - 如果您已经有一个要转换为 ASP.NET Core 的旧版 ASP.NET 应用程序,或者使用要更新的 ASP.NET Core 早期版本的应用程序,则您可能使用的是 MVC 控制器。在这种情况下,可能不值得将现有的 MVC 控制器转换为 Razor Pages。保留现有代码并考虑是否使用 Razor Pages 在应用程序中进行新开发更有意义。

• When you’re doing a lot of partial page updates—It’s possible to use JavaScript in an MVC application to avoid doing full page navigations by updating only part of the page at a time. This approach, halfway between fully server-side rendered and a client-side application, may be easier to achieve with MVC controllers than Razor Pages. On the other hand, you can easily mix Razor Pages and MVC controllers, using Razor Pages where appropriate and MVC controllers for the partial view results.
当您执行大量部分页面更新时 - 可以在 MVC 应用程序中使用 JavaScript,通过一次只更新页面的一部分来避免执行整个页面导航。这种方法介于完全服务器端渲染和客户端应用程序之间,使用 MVC 控制器可能比 Razor Pages 更容易实现。另一方面,您可以轻松地混合使用 Razor Pages 和 MVC 控制器,在适当的情况下使用 Razor Pages,并使用 MVC 控制器获得部分视图结果。

When not to use Razor Pages or MVC controllers
何时不使用 Razor Pages 或 MVC 控制器

Typically, you’ll use either Razor Pages or MVC controllers to write most of the UI logic for an app. You’ll use it to define the APIs and pages in your application and to define how they interface with your business logic. Razor Pages and MVC provide an extensive framework and include a great deal of functionality to help build your apps quickly and efficiently. But they’re not suited to every app.
通常,你将使用 Razor Pages 或 MVC 控制器来编写应用的大部分 UI 逻辑。您将使用它来定义应用程序中的 API 和页面,并定义它们如何与您的业务逻辑交互。Razor Pages 和 MVC 提供了一个广泛的框架,并包含大量功能来帮助快速有效地构建应用。但它们并不适合每个应用程序。

Providing so much functionality necessarily comes with a certain degree of performance overhead. For typical line-of-business apps, the productivity gains from using MVC or Razor Pages often outweighs any performance effect. But if you’re building a JSON API you will likely want to consider minimal APIs for the performance improvements. For server-to-server APIs or nonbrowser clients, an alternative protocol like gRPC (https://docs.microsoft.com/aspnet/core/grpc) may be a good fit. You might also consider protocols like GraphQL, as discussed in Building Web APIs in ASP.NET Core, by Valerio De Sanctis (Manning, 2023).
提供如此多的功能必然会带来一定程度的性能开销。对于典型的业务线应用,使用 MVC 或 Razor Pages 带来的工作效率提升通常超过任何性能影响。但是,如果您正在构建 JSON API,则可能需要考虑使用最少的 API 来提高性能。对于服务器到服务器 API 或非浏览器客户端,gRPC (https://docs.microsoft.com/aspnet/core/grpc) 等替代协议可能很合适。您还可以考虑像 GraphQL 这样的协议,如 Valerio De Sanctis 在 ASP.NET Core 中构建 Web API 中所述(Manning,2023 年)。

Alternatively, if you’re building an app with real-time functionality, you’ll probably want to consider using WebSockets instead of traditional HTTP requests. ASP.NET Core SignalR can be used to add real-time functionality to your app by providing an abstraction over WebSockets. SignalR also provides simple transport fallbacks and a remote procedure call (RPC) app model. For details, see the documentation at https://docs.microsoft.com/aspnet/core/signalr.
或者,如果您正在构建具有实时功能的应用程序,则可能需要考虑使用 WebSockets 而不是传统的 HTTP 请求。ASP.NET Core SignalR 可用于通过通过 WebSockets 提供抽象来向应用添加实时功能。SignalR 还提供简单的传输回退和远程过程调用 (RPC) 应用程序模型。有关详细信息,请参阅 https://docs.microsoft.com/aspnet/core/signalr 中的文档。

Another option available in ASP.NET Core 7 is Blazor. This framework allows you to build interactive client-side web applications by using the WebAssembly standard to run .NET code directly in your browser or by using a stateful model with SignalR. See Blazor in Action, by Chris Sainty (Manning, 2022), for more details.
ASP.NET Core 7 中提供的另一个选项是 Blazor。此框架允许您通过使用 WebAssembly 标准直接在浏览器中运行 .NET 代码,或者将有状态模型与 SignalR 结合使用来构建交互式客户端 Web 应用程序。有关更多详细信息,请参阅 Chris Sainty 的 Blazor in Action(Manning, 2022)。

I hope that by this point you’re sold on Razor Pages and their overall design using the MVC pattern. Nevertheless, using MVC controllers makes sense in some situations, so it’s worth bearing that in mind. Another important point to remember is that you can include both MVC controllers and Razor Pages in the same application if you need them.
我希望到此时,您已经对使用 MVC 模式的 Razor Pages 及其整体设计感到满意。尽管如此,在某些情况下使用 MVC 控制器是有意义的,因此值得牢记这一点。要记住的另一个重要点是,如果需要,您可以将 MVC 控制器和 Razor Pages 包含在同一个应用程序中。

You’ve learned about MVC controllers as an alternative to Razor Pages, and in part 1 of this book you learned about using minimal APIs to build JSON API. Web API controllers sit somewhere in between; they use MVC controllers but generate JSON and other machine-friendly format data, not HTML. In chapter 20 you’ll learn why you might choose to use web API controllers over minimal APIs and how to build a web API application.
您已经了解了 MVC 控制器作为 Razor Pages 的替代方案,在本书的第 1 部分中,您了解了如何使用最少的 API 来构建 JSON API。Web API 控制器介于两者之间;它们使用 MVC 控制器,但生成 JSON 和其他机器友好的格式数据,而不是 HTML。在第 20 章中,您将了解为什么您可能会选择使用 Web API 控制器而不是最少的 API,以及如何构建 Web API 应用程序。

19.6 Summary

19.6 总结

An action (or action method) is a method that runs in response to a request. An MVC controller is a class that contains one or more logically grouped action methods.
action (或作方法) 是为响应请求而运行的方法。MVC 控制器是包含一个或多个逻辑分组的作方法的类。

To use MVC controllers in an ASP.NET Core application, call AddControllersWithViews() on your WebApplicationBuilder. This adds all the required services for MVC controllers and Razor view rendering to the dependency injection container.
要在 ASP.NET Core 应用程序中使用 MVC 控制器,请在 WebApplicationBuilder 上调用 AddControllersWithViews()。这会将 MVC 控制器和 Razor 视图呈现所需的所有服务添加到依赖项注入容器中。

MVC controllers typically use conventional routing to select an MVC controller and action method. Instead of associating a route template with each action method in your application, conventional routing specifies one or more route template patterns that map to multiple endpoints. Conventional routes must define a controller and action route parameter to determine the action to execute.
MVC 控制器通常使用传统路由来选择 MVC 控制器和作方法。传统路由不是将路由模板与应用程序中的每个作方法相关联,而是指定一个或多个映射到多个终端节点的路由模板模式。传统路由必须定义控制器和作路由参数,以确定要执行的作。

You can return IActionResult instances from MVC controllers and they are executed in the same way as for Razor Pages. The most commonly returned type is ViewResult, using the View() helper method, which instructs the framework to render a Razor view.
可以从 MVC 控制器返回 IActionResult 实例,这些实例的执行方式与 Razor Pages 相同。最常返回的类型是 ViewResult,它使用 View() 帮助程序方法,该方法指示框架呈现 Razor 视图。

ViewResult may contain the name of the view to render and optionally a view model object to use when rendering the view. If the view name is not provided, a view is chosen using conventions.
ViewResult 可能包含要渲染的视图的名称,以及渲染视图时要使用的视图模型对象(可选)。如果未提供视图名称,则使用约定选择视图。

By convention, MVC Razor views are named the same as the action method that invokes them. They reside either in a folder with the same name as the action method’s controller or in the Shared folder.
按照约定,MVC Razor 视图的名称与调用它们的作方法相同。它们位于与作方法的控制器同名的文件夹中,或者位于 Shared 文件夹中。

MVC controllers contain multiple action methods, typically grouped around a high-level entity or resource. In contrast, Razor Pages groups all the page handlers for a single page in one place, grouping around a page/feature instead of an entity. This gives improved developer ergonomics when working on an endpoint.
MVC 控制器包含多个作方法,通常围绕高级实体或资源进行分组。相比之下,Razor Pages 将单个页面的所有页面处理程序分组到一个位置,围绕页面/功能而不是实体进行分组。这为开发人员在端点上工作时提供了改进的人体工程学。

MVC controllers may make sense over Razor Pages if you are upgrading an application that already uses MVC controllers or if your application is using a lot of partial page updates.
如果要升级已使用 MVC 控制器的应用程序,或者应用程序正在使用大量部分页面更新,则 MVC 控制器可能对 Razor Pages 有意义。

[1] Before moving to Razor Pages, the ASP.NET Core template that includes user login functionality contained two such controllers, each containing more than 20 action methods and more than 500 lines of code!
[1] 在迁移到 Razor Pages 之前,包含用户登录功能的 ASP.NET Core 模板包含两个这样的控制器,每个控制器包含 20 多个作方法和 500 多行代码!

ASP.NET Core in Action 18 Building forms with Tag Helpers

18 Building forms with Tag Helpers
18 使用标记辅助对象构建表单

This chapter covers
本章涵盖
• Building forms easily with Tag Helpers
使用标签帮助程序轻松构建表单
• Generating URLs with the Anchor Tag Helper
使用锚点标签帮助程序生成 URL
• Using Tag Helpers to add functionality to Razor
使用标签帮助程序向 Razor 添加功能

In chapter 17 you learned about Razor templates and how to use them to generate the views for your application. By mixing HTML and C#, you can create dynamic applications that can display different data based on the request, the logged-in user, or any other data you can access.
在第 17 章中,您了解了 Razor 模板以及如何使用它们为应用程序生成视图。通过混合使用 HTML 和 C#,您可以创建动态应用程序,这些应用程序可以根据请求、登录用户或您可以访问的任何其他数据显示不同的数据。

Displaying dynamic data is an important aspect of many web applications, but it’s typically only half of the story. As well as needing to displaying data to the user, you often need the user to be able to submit data back to your application. You can use data to customize the view or to update the application model by saving it to a database, for example. For traditional web applications, this data is usually submitted using an HTML form.
显示动态数据是许多 Web 应用程序的一个重要方面,但通常只是其中的一半。除了需要向用户显示数据外,您还经常需要用户能够将数据提交回您的应用程序。例如,您可以使用 data 来自定义视图或通过将应用程序模型保存到数据库来更新应用程序模型。对于传统的 Web 应用程序,此数据通常使用 HTML 表单提交。

In chapter 16 you learned about model binding, which is how you accept the data sent by a user in a request and convert it to C# objects that you can use in your Razor Pages. You also learned about validation and how important it is to validate the data sent in a request. You used DataAnnotations attributes to define the rules associated with your models, as well as associated metadata like the display name for a property.
在第 16 章中,你了解了模型绑定,即如何接受用户在请求中发送的数据,并将其转换为可在 Razor Pages 中使用的 C# 对象。您还了解了验证以及验证请求中发送的数据的重要性。您使用 DataAnnotations 属性来定义与模型关联的规则,以及关联的元数据,例如属性的显示名称。

The final aspect we haven’t yet looked at is how to build the HTML forms that users use to send this data in a request. Forms are one of the key ways users will interact with your application in the browser, so it’s important they’re both correctly defined for your application and user-friendly. ASP.NET Core provides a feature to achieve this, called Tag Helpers.
我们还没有研究的最后一个方面是如何构建用户用来在请求中发送这些数据的 HTML 表单。表单是用户在浏览器中与您的应用程序交互的关键方式之一,因此它们必须为您的应用程序正确定义并且对用户友好。ASP.NET Core 提供了一项功能来实现此目的,称为 Tag Helpers。

Tag Helpers are additions to Razor syntax that you use to customize the HTML generated in your templates. Tag Helpers can be added to an otherwise-standard HTML element, such as an <input>, to customize its attributes based on your C# model, saving you from having to write boilerplate code. Tag Helpers can also be standalone elements and can be used to generate completely customized HTML.
标记帮助程序是 Razor 语法的新增功能,用于自定义模板中生成的 HTML。可以将标记帮助程序添加到其他标准的 HTML 元素(如 <input>)以基于 C# 模型自定义其属性,从而使您不必编写样板代码。标记帮助程序也可以是独立元素,可用于生成完全自定义的 HTML。

NOTE Remember that Razor, and therefore Tag Helpers, are for server-side HTML rendering. You can’t use Tag Helpers directly in frontend frameworks like Angular and React.
注意:请记住,Razor 以及标记帮助程序用于服务器端 HTML 呈现。你不能直接在 Angular 和 React 等前端框架中使用 Tag Helpers。

If you’ve used legacy (.NET Framework) ASP.NET before, Tag Helpers may sound reminiscent of HTML Helpers, which could also be used to generate HTML based on your C# classes. Tag Helpers are the logical successor to HTML Helpers, as they provide a more streamlined syntax than the previous, C#-focused helpers. HTML Helpers are still available in ASP.NET Core, so if you’re converting some old templates to ASP.NET Core, you can still use them. But if you’re writing new Razor templates, I recommend using only Tag Helpers, as they should cover everything you need. I don’t cover HTML Helpers in this book.
如果您以前使用过旧版 (.NET Framework) ASP.NET,则标记帮助程序听起来可能会让人想起 HTML 帮助程序,后者也可用于基于 C# 类生成 HTML。标记帮助程序是 HTML 帮助程序的逻辑继承程序,因为它们提供的语法比以前以 C# 为中心的帮助程序更简化。HTML 帮助程序在 ASP.NET Core 中仍然可用,因此,如果要将一些旧模板转换为 ASP.NET Core,您仍然可以使用它们。但是,如果您正在编写新的 Razor 模板,我建议仅使用 Tag Helpers,因为它们应该涵盖您需要的所有内容。在本书中,我不涉及 HTML Helpers。

In this chapter you’ll primarily learn how to use Tag Helpers when building forms. They simplify the process of generating correct element names and IDs so that model binding can occur seamlessly when the form is sent back to your application. To put them into context, you’re going to carry on building the currency converter application that you’ve seen in previous chapters. You’ll add the ability to submit currency exchange requests to it, validate the data, and redisplay errors on the form using Tag Helpers to do the legwork for you, as shown in figure 18.1.
在本章中,您将主要学习如何在构建表单时使用 Tag Helpers。它们简化了生成正确元素名称和 ID 的过程,以便在将表单发送回应用程序时可以无缝地进行模型绑定。为了将它们放在上下文中,您将继续构建您在前几章中看到的货币转换器应用程序。您将添加向其提交货币兑换请求、验证数据以及使用 Tag Helpers 在表单上重新显示错误的功能,以为您完成跑腿工作,如图 18.1 所示。

alt text

Figure 18.1 The currency converter application forms, built using Tag Helpers. The labels, drop-down lists, input elements, and validation messages are all generated using Tag Helpers.
图 18.1 使用 Tag Helper 构建的货币转换器应用程序表单。标签、下拉列表、input 元素和验证消息都是使用 Tag Helper 生成的。

As you develop the application, you’ll meet the most common Tag Helpers you’ll encounter when working with forms. You’ll also see how you can use Tag Helpers to simplify other common tasks, such as generating links, conditionally displaying data in your application, and ensuring that users see the latest version of an image file when they refresh their browser.
在开发应用程序时,您将遇到在使用表单时遇到的最常见的标记帮助程序。您还将了解如何使用标记帮助程序来简化其他常见任务,例如生成链接、在应用程序中有条件地显示数据,以及确保用户在刷新浏览器时看到最新版本的图像文件。

To start, I’ll talk a little about why you need Tag Helpers when Razor can already generate any HTML you like by combining C# and HTML in a file.
首先,我将简要介绍一下当 Razor 已经可以通过将 C# 和 HTML 组合到一个文件中来生成您喜欢的任何 HTML 时,为什么需要标记帮助程序。

18.1 Catering to editors with Tag Helpers

18.1 使用标签助手迎合编辑者

One of the common complaints about the mixture of C# and HTML in Razor templates is that you can’t easily use standard HTML editing tools with them; all the @ and {} symbols in the C# code tend to confuse the editors. Reading the templates can be similarly difficult for people; switching paradigms between C# and HTML can be a bit jarring sometimes.
关于 Razor 模板中 C# 和 HTML 混合的常见抱怨之一是,您无法轻松地对它们使用标准的 HTML 编辑工具;C# 代码中的所有 @ 和 {} 符号往往会使编辑器感到困惑。阅读模板对人们来说同样困难;在 C# 和 HTML 之间切换范例有时可能有点不和谐。

This arguably wasn’t such a problem when Visual Studio was the only supported way to build ASP.NET websites, as it could obviously understand the templates without any problems and helpfully colorize the editor. But with ASP.NET Core going cross-platform, the desire to play nicely with other editors reared its head again.
当 Visual Studio 是构建 ASP.NET 网站的唯一受支持方式时,这可以说不是问题,因为它显然可以毫无问题地理解模板并有助于为编辑器着色。但随着 ASP.NET Core 跨平台,与其他编辑器友好合作的愿望再次抬头。

This was one of the big motivations for Tag Helpers. They integrate seamlessly into the standard HTML syntax by adding what look to be attributes, typically starting with asp-. They’re most often used to generate HTML forms, as shown in the following listing. This listing shows a view from the first iteration of the currency converter application, in which you choose the currencies and quantity to convert.
这是 Tag Helper 的主要动机之一。它们通过添加看起来像属性的内容(通常以 asp-
开头)无缝集成到标准 HTML 语法中。它们最常用于生成 HTML 表单,如下面的清单所示。此清单显示了 currency converter 应用程序第一次迭代的视图,您可以在其中选择要转换的货币和数量。

Listing 18.1 User registration form using Tag Helpers
清单 18.1 使用 Tag Helpers 的用户注册表单

@page                    #A
@model ConvertModel      #A
<form method="post">                                        
    <div class="form-group">
        <label asp-for="CurrencyFrom"></label>      #B
        <input class="form-control" asp-for="CurrencyFrom" />     #C
        <span asp-validation-for="CurrencyFrom"></span>    #D
    </div>
    <div class="form-group">
        <label asp-for="Quantity"></label>          #B
        <input class="form-control" asp-for="Quantity" />         #C
        <span asp-validation-for="Quantity"></span>        #D
    </div> 
    <div class="form-group">
        <label asp-for="CurrencyTo"></label>        #B
        <input class="form-control" asp-for="CurrencyTo" />       #C
        <span asp-validation-for="CurrencyTo"></span>      #D
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

❶ This is the view for the Razor Page Convert.cshtml. The Model type is ConvertModel.
这是 Razor Page Convert.cshtml 的视图。Model 类型为 ConvertModel。
❷ asp-for on Labels generates the caption for labels based on the view model.
Labels 上的 asp-for 根据视图模型生成标签的标题。
❸ asp-for on Inputs generates the correct type, value, name, and validation attributes for the model.
inputs 上的 asp-for 为模型生成正确的类型、值、名称和验证属性。
❹ Validation messages are written to a span using Tag Helpers.
验证消息使用 Tag Helper 写入 span。

At first glance, you might not even spot the Tag Helpers, they blend in so well with the HTML! This makes it easy to edit the files with any standard HTML text editor. But don’t be concerned that you’ve sacrificed readability in Visual Studio. As you can see in figure 18.2, elements with Tag Helpers are distinguishable from the standard HTML <div> element and the standard HTML class attribute on the <input> element. The C# properties of the view model being referenced (CurrencyFrom, in this case) are also displayed differently from “normal” HTML attributes. And of course you get IntelliSense, as you’d expect. Most other integrated development environments (IDEs) also include syntax highlighting and IntelliSense support.
乍一看,您甚至可能没有注意到 Tag Helpers,它们与 HTML 融合得非常好!这使得使用任何标准 HTML 文本编辑器编辑文件变得容易。但不要担心您牺牲了 Visual Studio 中的可读性。如图 18.2 所示,带有 Tag Helpers 的元素与标准 HTML <div>元素和元素上的标准 HTML <input>class 属性是可以区分的。所引用的视图模型的 C# 属性(在本例中为 CurrencyFrom)的显示方式也与“普通”HTML 属性不同。当然,正如您所期望的那样,您可以获得 IntelliSense。大多数其他集成开发环境 (IDE) 还包括语法突出显示和 IntelliSense 支持。

alt text

Figure 18.2 In Visual Studio, Tag Helpers are distinguishable from normal elements by being bold and a different color from standard HTML elements and attributes.
图 18.2 在 Visual Studio 中,标记帮助程序与普通元素的区别在于粗体和与标准 HTML 元素和属性不同的颜色。

Tag Helpers are extra attributes on standard HTML elements (or new elements entirely) that work by modifying the HTML element they’re attached to. They let you easily integrate your server-side values, such as those exposed on your PageModel, with the generated HTML.
标签帮助程序是标准 HTML 元素(或完全是新元素)上的额外属性,通过修改它们所附加到的 HTML 元素来工作。它们可让您轻松地将服务器端值(例如 PageModel 上公开的值)与生成的 HTML 集成。

Notice that listing 18.1 doesn’t specify the captions to display in the labels. Instead, you declaratively use asp-for="CurrencyFrom" to say “For this <label>, use the CurrencyFrom property to work out what caption to use.” Similarly, for the <input> elements, Tag Helpers are used to
请注意,清单 18.1 没有指定要在标签中显示的标题。相反,您以声明方式使用 asp-for=“CurrencyFrom” 来表示 <label>,请使用 CurrencyFrom 属性来确定要使用的标题。同样,对于 <input>元素,Tag Helpers 用于

• Automatically populate the value from the PageModel property.
自动填充 PageModel 属性中的值。
• Choose the correct id and name, so that when the form is POSTed back to the Razor Page, the property is model-bound correctly.
选择正确的 ID 和名称,以便在将表单 POST 回 Razor 页面时,该属性将正确进行模型绑定。
• Choose the correct input type to display (for example, a number input for the Quantity property).
选择要显示的正确输入类型 (例如,Quantity 属性的数字输入)。
• Display any validation errors, as shown in figure 18.3.
显示所有验证错误,如图 18.3 所示。

alt text

Figure 18.3 Tag Helpers hook into the metadata provided by DataAnnotations attributes, as well as the property types themselves. The Validation Tag Helper can even populate error messages based on the ModelState, as you saw in chapter 16.
图 18.3 标记帮助程序挂接到 DataAnnotations 属性提供的元数据以及属性类型本身。Validation Tag Helper 甚至可以根据 ModelState 填充错误消息,如第 16 章所示。

Tag Helpers can perform a variety of functions by modifying the HTML elements they’re applied to. This chapter introduces several common Tag Helpers and how to use them, but it’s not an exhaustive list. I don’t cover all the helpers that come out of the box in ASP.NET Core (there are more coming with every release!), and you can easily create your own, as you’ll see in chapter 32. Alternatively, you could use those published by others on NuGet or GitHub.
标签帮助程序可以通过修改它们所应用的 HTML 元素来执行各种功能。本章介绍了几种常见的 Tag Helper 及其使用方法,但并非详尽无遗。我没有涵盖 ASP.NET Core 中开箱即用的所有帮助程序(每个版本都会提供更多帮助程序),您可以轻松创建自己的帮助程序,如第 32 章所示。或者,您也可以使用其他人在 NuGet 或 GitHub 上发布的 Navi。

WebForms flashbacks
WebForms 闪回

For those who used ASP.NET back in the day of WebForms, before the advent of the Model-View-Controller (MVC) pattern for web development, Tag Helpers may be triggering bad memories. Although the asp- prefix is somewhat reminiscent of ASP.NET Web Server control definitions, never fear; the two are completely different beasts.
对于那些在 WebForms 时代使用 ASP.NET 的人来说,在用于 Web 开发的模型-视图-控制器 (MVC) 模式出现之前,标记帮助程序可能会触发糟糕的回忆。尽管 asp- 前缀有点让人想起 ASP.NET Web 服务器控件定义,但不要害怕;两者是完全不同的野兽。

Web Server controls were added directly to a page’s backing C# class and had a broad scope that could modify seemingly unrelated parts of the page. Coupled with that, they had a complex life cycle that was hard to understand and debug when things weren’t working. The perils of trying to work with that level of complexity haven’t been forgotten, and Tag Helpers aren’t the same.
Web 服务器控件直接添加到页面的支持 C# 类中,并且具有广泛的范围,可以修改页面中看似不相关的部分。再加上,它们的生命周期很复杂,当事情不正常时,很难理解和调试。尝试处理这种复杂程度的危险并没有被遗忘,标签帮助程序也不一样。

Tag Helpers don’t have a life cycle; they participate in the rendering of the element to which they’re attached, and that’s it. They can modify the HTML element they’re attached to, but they can’t modify anything else on your page, making them conceptually much simpler. An additional capability they bring is the ability to have multiple Tag Helpers acting on a single element—something Web Server controls couldn’t easily achieve.
标记帮助程序没有生命周期;它们参与渲染它们所附加到的元素,仅此而已。他们可以修改它们所附加到的 HTML 元素,但不能修改页面上的任何其他内容,从而在概念上使它们变得更加简单。它们带来的另一项功能是能够让多个 Tag Helpers 作用于单个元素 — 这是 Web Server 控件无法轻松实现的。

Overall, if you’re writing Razor templates, you’ll have a much more enjoyable experience if you embrace Tag Helpers as integral to its syntax. They bring a lot of benefits without obvious downsides, and your cross-platform-editor friends will thank you!
总的来说,如果你正在编写 Razor 模板,如果你将 Tag Helpers 作为其语法的组成部分,你将获得更愉快的体验。它们带来了很多好处,而且没有明显的缺点,你的跨平台编辑器朋友会感谢你!

18.2 Creating forms using Tag Helpers

18.2 使用标记帮助程序创建表单

In this section you’ll learn how to use some of the most useful Tag Helpers: Tag Helpers that work with forms. You’ll learn how to use them to generate HTML markup based on properties of your PageModel, creating the correct id and name attributes, and setting the value of the element to the model property’s value (among other things). This capability significantly reduces the amount of markup you need to write manually.
在本节中,您将学习如何使用一些最有用的 Tag Helpers:使用表单的 Tag Helpers。您将学习如何使用它们根据 PageModel 的属性生成 HTML 标记,创建正确的 id 和 name 属性,以及将元素的值设置为 model 属性的值(以及其他内容)。此功能显著减少了您需要手动编写的标记量。

Imagine you’re building the checkout page for the currency converter application, and you need to capture the user’s details on the checkout page. In chapter 16 you built a UserBindingModel model (shown in listing 18.2), added DataAnnotations attributes for validation, and saw how to model-bind it in a POST to a Razor Page. In this chapter you’ll see how to create the view for it by exposing the UserBindingModel as a property on your PageModel.
假设您正在为货币转换器应用程序构建结帐页面,并且您需要在结帐页面上捕获用户的详细信息。在第 16 章中,您构建了一个 UserBindingModel 模型(如清单 18.2 所示),添加了用于验证的 DataAnnotations 属性,并了解了如何在 POST 中将其模型绑定到 Razor 页面。在本章中,您将了解如何通过将 UserBindingModel 作为 PageModel 上的属性公开来为其创建视图。

Warning With Razor Pages, you often expose the same object in your view that you use for model binding. When you do this, you must be careful to not include sensitive values (that shouldn’t be edited) in the binding model, to prevent mass-assignment attacks on your app. You can read more about these attacks on my blog at http://mng.bz/RXw0.
警告:使用 Razor Pages,您通常会在视图中公开用于模型绑定的相同对象。执行此作时,必须注意不要在绑定模型中包含敏感值(不应编辑),以防止对应用程序进行批量赋值攻击。您可以在我的博客 http://mng.bz/RXw0 上阅读有关这些攻击的更多信息。

Listing 18.2 UserBindingModel for creating a user on a checkout page
列表 18.2 用于在结帐页面上创建用户的 UserBindingModel

public class UserBindingModel
{
    [Required]
    [StringLength(100, ErrorMessage = "Maximum length is {1}")]
    [Display(Name = "Your name")]
    public string FirstName { get; set; }

    [Required]
    [StringLength(100, ErrorMessage = "Maximum length is {1}")]
    [Display(Name = "Last name")]
    public string LastName { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Phone(ErrorMessage = "Not a valid phone number.")]
    [Display(Name = "Phone number")]
    public string PhoneNumber { get; set; }
}

The UserBindingModel is decorated with various DataAnnotations attributes. In chapter 16 you saw that these attributes are used during model validation when the model is bound to a request, before the page handler is executed. These attributes are also used by the Razor templating language to provide the metadata required to generate the correct HTML when you use Tag Helpers.
UserBindingModel 使用各种 DataAnnotations 属性进行修饰。在第 16 章中,你看到这些属性在模型验证期间、当模型绑定到请求时、在执行页面处理程序之前使用。Razor 模板语言还使用这些属性来提供在使用标记帮助程序时生成正确 HTML 所需的元数据。

You can use the pattern I described in chapter 16, exposing a UserBindindModel as an Input property of your PageModel, to use the model for both model binding and in your Razor view:
您可以使用我在第 16 章 将 UserBindindModel 公开为 PageModel 的 Input 属性中描述的模式,将该模型用于模型绑定和 Razor 视图:

public class CheckoutModel: PageModel
{
    [BindProperty]
    public UserBindingModel Input { get; set; }
}

With the help of the UserBindingModel property, Tag Helpers, and a little HTML, you can create a Razor view that lets the user enter their details, as shown in figure 18.4.
借助 UserBindingModel 属性、标记帮助程序和一些 HTML,您可以创建一个允许用户输入其详细信息的 Razor 视图,如图 18.4 所示。

alt text

Figure 18.4 The checkout page for an application. The HTML is generated based on a UserBindingModel, using Tag Helpers to render the required element values, input types, and validation messages.
图 18.4 应用程序的结帐页面。HTML 是基于 UserBindingModel 生成的,使用标记帮助程序呈现所需的元素值、输入类型和验证消息。

The Razor template to generate this page is shown in listing 18.3. This code uses a variety of tag helpers, including
用于生成此页面的 Razor 模板如清单 18.3 所示。此代码使用各种标签帮助程序,包括

• A Form Tag Helper on the <form> element
<form> 元素上的表单标记帮助程序
• Label Tag Helpers on the <label>
标签标签帮助程序
• Input Tag Helpers on the <input>
输入标记帮助程序
• Validation Message Tag Helpers on <span> validation elements for each property in the UserBindingModel
UserBindingModel 中每个属性的验证元素上的验证消息 <span>标记帮助程序

Listing 18.3 Razor template for binding to UserBindingModel on the checkout page
列表 18.3 用于在结帐页面上绑定到 UserBindingModel 的 Razor 模板

@page
@model CheckoutModel    #A
@{
    ViewData["Title"] = "Checkout";
}
<h1>@ViewData["Title"]</h1>
<form asp-page="Checkout">      #B
    <div class="form-group">
        <label asp-for="Input.FirstName"></label>               #C
        <input class="form-control" asp-for="Input.FirstName" />
        <span asp-validation-for="Input.FirstName"></span>
    </div>
    <div class="form-group">
        <label asp-for="Input.LastName"></label>             
        <input class="form-control" asp-for="Input.LastName" />
        <span asp-validation-for="Input.LastName"></span>
    </div>
    <div class="form-group">
        <label asp-for="Input.Email"></label>
        <input class="form-control" asp-for="Input.Email" />    #D
        <span asp-validation-for="Input.Email"></span>
    </div>
    <div class="form-group">
        <label asp-for="Input.PhoneNumber"></label>
        <input class="form-control" asp-for="Input.PhoneNumber" />
        <span asp-validation-for="Input.PhoneNumber"></span>    #E
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

❶ The CheckoutModel is the PageModel, which exposes a UserBindingModel on the Input property.
CheckoutModel 是 PageModel,它在 Input 属性上公开 UserBindingModel。
❷ Form Tag Helpers use routing to determine the URL the form will be posted to.
表单标签帮助程序使用路由来确定表单将发布到的 URL。
❸ The Label Tag Helper uses DataAnnotations on a property to determine the caption to display.
Label Tag Helper 在属性上使用 DataAnnotations 来确定要显示的标题。
❹ The Input Tag Helper uses DataAnnotations to determine the type of input to generate.
Input Tag Helper 使用 DataAnnotations 来确定要生成的输入类型。
❺ The Validation Tag Helper displays error messages associated with the given property.
Validation Tag Helper 显示与给定属性关联的错误消息。

You can see the HTML markup that this template produces in listing 18.4, which renders in the browser as you saw in figure 18.4. You can see that each of the HTML elements with a Tag Helper has been customized in the output: the <form> element has an action attribute, the <input> elements have an id and name based on the name of the referenced property, and both the <input> and <span> have data- attributes for validation.
您可以在清单 18.4 中看到此模板生成的 HTML 标记,该标记在浏览器中呈现,如图 18.4 所示。您可以看到,每个带有 Tag Helper 的 HTML 元素在输出中都已自定义: <form>元素具有 action 属性,<input> 元素具有基于引用属性名称的 id 和 name,并且<input><span> 都具有用于验证的 data-
属性。

Listing 18.4 HTML generated by the Razor template on the checkout page
列表 18.4 结帐页面上 Razor 模板生成的 HTML

<form action="/Checkout" method="post">
  <div class="form-group">
    <label for="Input_FirstName">Your name</label>
    <input class="form-control" type="text"
      data-val="true" data-val-length="Maximum length is 100"
      id="Input_FirstName" data-val-length-max="100"
      data-val-required="The Your name field is required."
      Maxlength="100" name="Input.FirstName" value="" />
    <span data-valmsg-for="Input.FirstName"
      class="field-validation-valid" data-valmsg-replace="true"></span>
  </div>
  <div class="form-group">
    <label for="Input_LastName">Your name</label>
    <input class="form-control" type="text"
      data-val="true" data-val-length="Maximum length is 100"
      id="Input_LastName" data-val-length-max="100"
      data-val-required="The Your name field is required."
      Maxlength="100" name="Input.LastName" value="" />
    <span data-valmsg-for="Input.LastName"
      class="field-validation-valid" data-valmsg-replace="true"></span>
  </div>
  <div class="form-group">
    <label for="Input_Email">Email</label>
    <input class="form-control" type="email" data-val="true"
      data-val-email="The Email field is not a valid e-mail address."
      Data-val-required="The Email field is required."
      Id="Input_Email" name="Input.Email" value="" />
    <span class="text-danger field-validation-valid"
      data-valmsg-for="Input.Email" data-valmsg-replace="true"></span>
    </div>
  <div class="form-group">
    <label for="Input_PhoneNumber">Phone number</label>
    <input class="form-control" type="tel" data-val="true"
      data-val-phone="Not a valid phone number." Id="Input_PhoneNumber"
      name="Input.PhoneNumber" value="" />
    <span data-valmsg-for="Input.PhoneNumber"
      class="text-danger field-validation-valid"
      data-valmsg-replace="true"></span>
  </div>
  <button type="submit" class="btn btn-primary">Submit</button>
  <input name="__RequestVerificationToken" type="hidden"
    value="CfDJ8PkYhAINFx1JmYUVIDWbpPyy_TRUNCATED" />
</form>

Wow, that’s a lot of markup! If you’re new to working with HTML, this might all seem a little overwhelming, but the important thing to notice is that you didn’t have to write most of it! The Tag Helpers took care of most of the plumbing for you. That’s basically Tag Helpers in a nutshell; they simplify the fiddly mechanics of building HTML forms, leaving you to concentrate on the overall design of your application instead of writing boilerplate markup.
哇,好多标记啊!如果您刚开始使用 HTML,这可能看起来有点让人不知所措,但需要注意的重要一点是,您不必编写大部分内容!Tag Helpers 为您处理了大部分管道工作。简而言之,这基本上就是 Tag Helpers;它们简化了构建 HTML 表单的繁琐机制,让您专注于应用程序的整体设计,而不是编写样板标记。

NOTE If you’re using Razor to build your views, Tag Helpers will make your life easier, but they’re entirely optional. You’re free to write raw HTML without them or to use the legacy HTML Helpers.
注意:如果您使用 Razor 构建视图,标记帮助程序将使您的生活更轻松,但它们完全是可选的。您可以自由编写没有它们的原始 HTML,也可以使用旧版 HTML 帮助程序。

Tag Helpers simplify and abstract the process of HTML generation, but they generally try to do so without getting in your way. If you need the final generated HTML to have a particular attribute, you can add it to your markup. You can see that in the previous listings where class attributes are defined on <input> elements, such as <input class="form-control" asp-for="Input.FirstName" />. They pass untouched from Razor to the HTML output.
标记帮助程序简化和抽象了 HTML 生成过程,但它们通常会尝试在不妨碍您的情况下这样做。如果需要最终生成的 HTML 具有特定属性,可以将其添加到标记中。你可以看到,在前面的清单中,类属性是在 <input> 元素上定义的,比如 <input class="form-control" asp-for="Input.FirstName" />.它们将原封不动的 Razor 传递到 HTML 输出。

Tip This is different from the way HTML Helpers worked in legacy ASP.NET; HTML helpers often require jumping through hoops to set attributes in the generated markup.
提示:这与 HTML 帮助程序在旧版 ASP.NET 中的工作方式不同;HTML 帮助程序通常需要跳过重重障碍才能在生成的标记中设置属性。

Even better, you can also override attributes that are normally generated by a Tag Helper, like the type attribute on an <input> element. For example, if the FavoriteColor property on your PageModel was a string, by default Tag Helpers would generate an <input> element with type="text". Updating your markup to use the HTML5 color picker type is trivial; set the type explicitly in your Razor view:
更好的是,您还可以覆盖通常由 Tag Helper 生成的属性,例如<input> 元素上的 type 属性。例如,如果 PageModel 上的 FavoriteColor 属性是一个字符串,则默认情况下,标记帮助程序将生成一个具有 type=“text” 的<input> 元素。更新标记以使用 HTML5 颜色选取器类型非常简单;在 Razor 视图中显式设置类型:

<input type="color" asp-for="FavoriteColor" />

Tip HTML5 adds a huge number of features, including lots of form elements that you may not have come across before, such as range inputs and color pickers. You can read about them on the Mozilla Developer Network website at http://mng.bz/qOc1.
提示:HTML5 添加了大量功能,包括许多您以前可能没有遇到过的表单元素,例如范围输入和颜色选择器。您可以在 Mozilla Developer Network 网站上阅读有关它们的信息,网址为 http://mng.bz/qOc1

For the remainder of section 18.2, you’ll build the currency converter Razor templates from scratch, adding Tag Helpers as you find you need them. You’ll probably find you use most of the common form Tag Helpers in every application you build, even if it’s on a simple login page.
在第 18.2 节的其余部分,您将从头开始构建货币转换器 Razor 模板,并根据需要添加 Tag Helpers。您可能会发现,在构建的每个应用程序中都使用了大多数常见形式的 Tag Helpers,即使它位于简单的登录页面上。

18.2.1 The Form Tag Helper

18.2.1 Form 标记帮助程序

The first thing you need to start building your HTML form is, unsurprisingly, the <form> element. In listing 18.3 the <form> element was augmented with an asp-page Tag Helper attribute:
毫无疑问,开始构建 HTML 表单需要做的第一件事是<form>元素。在清单 18.3 中,该<form>元素被扩充了一个 asp-page Tag Helper 属性:

<form asp-page="Checkout">

The Tag Helper adds action and method attributes to the final HTML, indicating which URL the form should be sent to when it’s submitted and the HTTP verb to use:
标记帮助程序将 action 和 method 属性添加到最终的 HTML 中,指示表单在提交时应发送到哪个 URL,以及要使用的 HTTP 动词:

<form action="/Checkout" method="post">

Setting the asp-page attribute allows you to specify a different Razor Page in your application that the form will be posted to when it’s submitted. If you omit the asp-page attribute, the form will post back to the same URL it was served from. This is common with Razor Pages. You normally handle the result of a form post in the same Razor Page that is used to display it.
设置 asp-page 属性后,可以在应用程序中指定不同的 Razor 页面,表单在提交时将发布到该页面。如果省略 asp-page 属性,表单将回发到提供它的同一 URL。这在 Razor Pages 中很常见。通常在用于显示表单帖子的同一 Razor Page 中处理表单帖子的结果。

Warning If you omit the asp-page attribute, you must add the method="post" attribute manually. It’s important to add this attribute so the form is sent using the POST verb instead of the default GET verb. Using GET for forms can be a security risk.
警告:如果省略 asp-page 属性,则必须手动添加 method=“post” 属性。添加此属性非常重要,以便使用 POST 动词而不是默认的 GET 动词发送表单。对表单使用 GET 可能会带来安全风险。

The asp-page attribute is added by a FormTagHelper. This Tag Helper uses the value provided to generate a URL for the action attribute, using the URL generation features of routing that I described in chapters 5 and 14.
asp-page 属性由 FormTagHelper 添加。此 Tag Helper 使用提供的值为 action 属性生成 URL,使用我在第 5 章和第 14 章中描述的路由的 URL 生成功能。

NOTE Tag Helpers can make multiple attributes available on an element. Think of them like properties on a Tag Helper configuration object. Adding a single asp- attribute activates the Tag Helper on the element. Adding more attributes lets you override further default values of its implementation.
注意:标记帮助程序可以在一个元素上提供多个属性。将它们视为 Tag Helper 配置对象上的属性。添加单个 asp- 属性会激活元素上的 Tag Helper。添加更多属性可让您覆盖其实现的更多默认值。

The Form Tag Helper makes several other attributes available on the <form> element that you can use to customize the generated URL. I hope you’ll remember that you can set route values when generating URLs. For example, if you have a Razor Page called Product.cshtml that uses the directive
Form Tag Helper 在 <form> 元素上提供了几个其他属性,您可以使用这些属性来自定义生成的 URL。我希望您会记住,您可以在生成 URL 时设置路由值。例如,如果你有一个名为 Product.cshtml 的 Razor 页面,该页面使用指令

@page "{id}"

the full route template for the page would be "Product/{id}". To generate the URL for this page correctly, you must provide the {id} route value. How can you set that value using the Form Tag Helper?
页面的完整路由模板将为 “Product/{id}”。要正确生成此页面的 URL,您必须提供 {id} 路由值。如何使用 Form Tag Helper 设置该值?

The Form Tag Helper defines an asp-route- wildcard attribute that you can use to set arbitrary route parameters. Set the in the attribute to the route parameter name. For example, to set the id route parameter, you’d set the asp-route-id value. If the ProductId property of your PageModel contains the id value required, you could use:
表单标记帮助程序定义可用于设置任意路由参数的 asp-route- 通配符属性。将 in 属性设置为路由参数名称。例如,要设置 id 路由参数,您需要设置 asp-route-id 值。如果 PageModel 的 ProductId 属性包含所需的 id 值,则可以使用:

<form asp-page="Product" asp-route-id="@Model.ProductId">

Based on the route template of the Product.cshtml Razor Page (and assuming ProductId=5 in this example), this would generate the following markup:
根据 Product.cshtml Razor 页面的路由模板(在此示例中假设 ProductId=5),这将生成以下标记:

<form action="/Product/5" method="post">

You can add as many asp-route-* attributes as necessary to your <form> to generate the correct action URL. You can also set the Razor Page handler to use the asp-page-handler attribute. This ensures that the form POST will be handled by the handler you specify.

您可以根据需要将任意数量的 asp-route-* 属性添加到 <form> 以生成正确的作 URL。您还可以将 Razor Page 处理程序设置为使用 asp-page-handler 属性。这可确保表单 POST 将由您指定的处理程序处理。

NOTE The Form Tag Helper has many additional attributes, such as asp-action and asp-controller, that you generally won’t use with Razor Pages. Those are useful only if you’re using MVC controllers with views. In particular, look out for the asp-route attribute—this is not the same as the asp-route- attribute. The former is used to specify a named route (such as a named minimal API endpoint), and the latter is used to specify the route values to use during URL generation.
注意:表单标记帮助程序具有许多其他属性,例如 asp-action 和 asp-controller,这些属性通常不会与 Razor Pages 一起使用。仅当您将 MVC 控制器与视图一起使用时,这些才有用。特别是,请注意 asp-route 属性 — 这与 asp-route-
属性不同。前者用于指定命名路由(例如命名的最小 API 终端节点),后者用于指定在 URL 生成期间要使用的路由值。

The main job of the Form Tag Helper is to generate the action attribute, but it performs one additional important function: generating a hidden <input> field needed to prevent cross-site request forgery (CSRF) attacks.
Form Tag Helper 的主要工作是生成 action 属性,但它执行一项额外的重要功能:生成防止跨站点请求伪造 (CSRF) 攻击所需的隐藏<input> 字段。

DEFINITION Cross-site request forgery (CSRF) attacks are a website exploit that can allow actions to be executed on your website by an unrelated malicious website. You’ll learn about them in detail in chapter 29.
定义:跨站点请求伪造 (CSRF) 攻击是一种网站漏洞,可以允许不相关的恶意网站在您的网站上执行作。您将在第 29 章中详细了解它们。

You can see the generated hidden <input> at the bottom of the <form> in listing 18.4; it’s named RequestVerificationToken and contains a seemingly random string of characters. This field won’t protect you on its own, but I’ll describe in chapter 29 how it’s used to protect your website. The Form Tag Helper generates it by default, so you generally won’t need to worry about it, but if you need to disable it, you can do so by adding asp-antiforgery="false" to your <form> element.
你可以在清单 18.4 的 <form> 底部看到生成隐藏<input> ;它被命名为
RequestVerificationToken 并包含一个看似随机的字符串。此字段本身不会保护您,但我将在第 29 章中介绍如何使用它来保护您的网站。默认情况下,Form Tag Helper 会生成它,因此您通常无需担心它,但如果您需要禁用它,可以通过将 asp-antiforgery=“false” 添加到您的<form> 元素来实现。

The Form Tag Helper is obviously useful for generating the action URL, but it’s time to move on to more interesting elements—those that you can see in your browser!
表单标记帮助程序显然可用于生成作 URL,但现在是时候转向更有趣的元素了 — 您可以在浏览器中看到的元素!

18.2.2 The Label Tag Helper

18.2.2 标签标签帮助程序

Every <input> field in your currency converter application needs to have an associated label so the user knows what the <input> is for. You could easily create those yourself, manually typing the name of the field and setting the for attribute as appropriate, but luckily there’s a Tag Helper to do that for you.
货币转换器应用程序中的每个 <input> 字段都需要有一个关联的标签,以便用户知道 for what for what.您可以轻松地自己创建这些标记,手动键入字段的名称并根据需要设置 <input> for 属性,但幸运的是,有一个 Tag Helper 可以为您执行此作。

The Label Tag Helper is used to generate the caption (the visible text) and the for attribute for a <label> element, based on the properties in the PageModel. It’s used by providing the name of the property in the asp-for attribute:
Label Tag Helper 用于根据 PageModel 中的属性为<label> 元素生成标题(可见文本)和 for 属性。通过在 asp-for 属性中提供属性的名称来使用它:

<label asp-for="FirstName"></label>

The Label Tag Helper uses the [Display] DataAnnotations attribute that you saw in chapter 16 to determine the appropriate value to display. If the property you’re generating a label for doesn’t have a [Display] attribute, the Label Tag Helper uses the name of the property instead. Consider this model in which the FirstName property has a [Display] attribute, but the Email property doesn’t:
Label Tag Helper 使用您在第 16 章中看到的 [Display] DataAnnotations 属性来确定要显示的适当值。如果要为其生成标签的属性没有 [Display] 属性,则 Label Tag Helper 会改用该属性的名称。请考虑以下模型:FirstName 属性具有 [Display] 属性,但 Email 属性没有:

public class UserModel
{
    [Display(Name = "Your name")]
    public string FirstName { get; set; }
    public string Email { get; set; }
}

The following Razor
以下 Razor

<label asp-for="FirstName"></label>
<label asp-for="Email"></label>

would generate this HTML:
将生成此 HTML:

<label for="FirstName">Your name</label>
<label for="Email">Email</label>

The inner text inside the <label> element uses the value set in the [Display] attribute, or the property name in the case of the Email property. Also note that the for attribute has been generated with the name of the property. This is a key bonus of using Tag Helpers; it hooks in with the element IDs generated by other Tag Helpers, as you’ll see shortly.
<label> 元素内部文本使用 [Display] 属性中设置的值,或者使用 Email 属性的属性名称。另请注意,已使用属性名称生成 for 属性。这是使用 Tag Helper 的一个关键好处;它与其他 Tag Helper 生成的元素 ID 挂钩,您很快就会看到。

NOTE The for attribute is important for accessibility. It specifies the ID of the element to which the label refers. This is important for users who are using a screen reader, for example, as they can tell what property a form field relates to.
注意:for 属性对于辅助功能非常重要。它指定标签所引用的元素的 ID。例如,这对于使用屏幕阅读器的用户来说非常重要,因为他们可以判断表单字段与哪个属性相关。

As well as properties on the PageModel, you can also reference sub-properties on child objects. For example, as I described in chapter 16, it’s common to create a nested class in a Razor Page, expose that as a property, and decorate it with the [BindProperty] attribute:
除了 PageModel 上的属性外,您还可以引用子对象上的子属性。例如,正如我在第 16 章中所描述的,在 Razor Page 中创建一个嵌套类,将其作为属性公开,并使用 [BindProperty] 属性对其进行修饰是很常见的:

public class CheckoutModel: PageModel
{
    [BindProperty]
    public UserBindingModel Input { get; set; }
}

You can reference the FirstName property of the UserBindingModel by “dotting” into the property as you would in any other C# code. Listing 18.3 shows more examples of this.
您可以通过在属性中“点”来引用 UserBindingModel 的 FirstName 属性,就像在任何其他 C# 代码中一样。清单 18.3 显示了更多这样的例子。

<label asp-for="Input.FirstName"></label>
<label asp-for="Input.Email"></label>

As is typical with Tag Helpers, the Label Tag Helper won’t override values that you set yourself. If, for example, you don’t want to use the caption generated by the helper, you could insert your own manually. The code
与标签帮助程序的典型情况一样,标签标签帮助程序不会覆盖您自己设置的值。例如,如果您不想使用帮助程序生成的标题,则可以手动插入自己的标题。代码:

<label asp-for="Email">Please enter your Email</label>

would generate this HTML:
将生成此 HTML:

<label for="Email">Please enter your Email</label>

As ever, you’ll generally have an easier time with maintenance if you stick to the standard conventions and don’t override values like this, but the option is there. Next up is a biggie: the Input and Textarea Tag Helpers.
与往常一样,如果您坚持标准约定并且不覆盖这样的值,您通常会更轻松地进行维护,但选项就在那里。接下来是一个大问题:Input 和 Textarea 标记帮助程序。

18.2.3 The Input and Textarea Tag Helpers

18.2.3 input 和 textarea 标记帮助程序

Now you’re getting into the meat of your form: the <input> elements that handle user input. Given that there’s such a wide array of possible input types, there’s a variety of ways they can be displayed in the browser. For example, Boolean values are typically represented by a checkbox type <input> element, whereas integer values would use a number type <input> element, and a date would use the date type, as shown in figure 18.5.
现在,你进入了表单的核心:处理用户输入的 <input>元素。鉴于可能的输入类型如此广泛,它们在浏览器中的显示方式多种多样。例如,布尔值通常由复选框类型 <input>元素表示,而整数值将使用数字类型 <input>元素,日期将使用日期类型,如图 18.5 所示。

alt text

Figure 18.5 Various input element types. The exact way in which each type is displayed varies by browser.
图 18.5 各种输入元素类型。每种类型的确切显示方式因浏览器而异。

To handle this diversity, the Input Tag Helper is one of the most powerful Tag Helpers. It uses information based on both the type of the property (bool, string, int, and so on) and any DataAnnotations attributes applied to it ([EmailAddress] and [Phone], among others) to determine the type of the input element to generate. The DataAnnotations are also used to add data-val- client-side validation attributes to the generated HTML.
为了处理这种多样性,Input Tag Helper 是最强大的 Tag Helper 之一。它使用基于属性类型(bool、string、int 等)和应用于它的任何 DataAnnotations 属性([EmailAddress] 和 [Phone] 等)的信息来确定要生成的输入元素的类型。DataAnnotations 还用于将 data-val-
客户端验证属性添加到生成的 HTML 中。

Consider the Email property from listing 18.2 that was decorated with the [EmailAddress] attribute. Adding an <input> is as simple as using the asp-for attribute:
请考虑清单 18.2 中的 Email 属性,该属性使用 [EmailAddress] 属性进行修饰。添加 <input>就像使用 asp-for 属性一样简单:

<input asp-for="Input.Email" />

The property is a string, so ordinarily the Input Tag Helper would generate an <input> with type="text". But the addition of the [EmailAddress] attribute provides additional metadata about the property. Consequently, the Tag Helper generates an HTML5 <input> with type="email":
该属性是一个字符串,因此通常 Input Tag Helper 会生成一个带有 type=“text” 的 <input> 。但是,添加 [EmailAddress] 属性会提供有关属性的其他元数据。因此,标记帮助程序会生成一个 type=“email” 的 HTML5 <input>

<input type="email" id="Input_Email" name="Input.Email"
    value="[email protected]" data-val="true"
    data-val-email="The Email Address field is not a valid e-mail address."
    Data-val-required="The Email Address field is required."
    />

You can take a whole host of things away from this example. First, the id and name attributes of the HTML element have been generated from the name of the property. The value of the id attribute matches the value generated by the Label Tag Helper in its for attribute, Input_Email. The value of the name attribute preserves the “dot” notation, Input.Email, so that model binding works correctly when the field is POSTed to the Razor Page.
您可以从这个例子中学到很多东西。首先,HTML 元素的 id 和 name 属性是从属性的名称生成的。id 属性的值与 Label Tag Helper 在其 for 属性 Input_Email 中生成的值匹配。Input.Email,name 属性的值保留“点”表示法,以便在将字段发布到 Razor 页面时,模型绑定正常工作。

Also, the initial value of the field has been set to the value currently stored in the property ("[email protected]", in this case). The type of the element has also been set to the HTML5 email type, instead of using the default text type.
此外,字段的初始值已设置为当前存储在属性中的值(在本例中为“[email protected]”)。元素的类型也已设置为 HTML5 电子邮件类型,而不是使用默认文本类型。

Perhaps the most striking addition is the swath of data-val- attributes. These can be used by client-side JavaScript libraries such as jQuery to provide client-side validation of your DataAnnotations constraints. Client-side validation provides instant feedback to users when the values they enter are invalid, providing a smoother user experience than can be achieved with server-side validation alone, as I described in chapter 16.
也许最引人注目的新增功能是大量的 data-val-
属性。客户端 JavaScript 库(如 jQuery)可以使用这些约束来提供 DataAnnotations 约束的客户端验证。客户端验证会在用户输入的值无效时向用户提供即时反馈,从而提供比单独使用服务器端验证更流畅的用户体验,如我在第 16 章中所述。

Client-side validation
客户端验证

To enable client-side validation in your application, you need to add some jQuery libraries to your HTML pages. In particular, you need to include the jQuery, jQuery-validation, and jQuery-validation-unobtrusive JavaScript libraries. You can do this in several ways, but the simplest is to include the script files at the bottom of your view using
要在应用程序中启用客户端验证,您需要向 HTML 页面添加一些 jQuery 库。特别是,您需要包括 jQuery、jQuery-validation 和 jQuery-validation-unobtrusive JavaScript 库。您可以通过多种方式执行此作,但最简单的方法是使用

<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

The default templates include these scripts for you in a handy partial template that you can add to your page in a Scripts section. If you’re using the default layout and need to add client-side validation to your view, add the following section somewhere on your view:
默认模板将这些脚本包含在一个方便的部分模板中,您可以将其添加到页面的 Scripts 部分。如果您使用的是默认布局,并且需要向视图添加客户端验证,请在视图上的某个位置添加以下部分:

@section Scripts{
    @Html.Partial("_ValidationScriptsPartial")
}

This partial view references files in your wwwroot folder. The default layout template includes jQuery itself. If you don’t need to use jQuery in your application, you may want to consider a small alternative validation library called aspnet-client-validation. I describe why you might consider this library and how to use it in this blog post: http://mng.bz/V1pX.
此分部视图引用 wwwroot 文件夹中的文件。默认布局模板包括 jQuery 本身。如果您不需要在应用程序中使用 jQuery,则可能需要考虑一个名为 aspnet-client-validation 的小型替代验证库。我在这篇博文中描述了为什么会考虑使用这个库以及如何使用它:http://mng.bz/V1pX

You can also load these files, whether you’re using jQuery or aspnet-client-validation, from a content delivery network (CDN). If you want to take this approach, you should consider scenarios where the CDN is unavailable or compromised, as I discuss in this blog post: http://mng.bz/2e6d.
您还可以从内容分发网络 (CDN) 加载这些文件,无论您使用的是 jQuery 还是 aspnet-client-validation。如果您想采用这种方法,您应该考虑 CDN 不可用或受损的情况,正如我在这篇博文中讨论的那样:http://mng.bz/2e6d

The Input Tag Helper tries to pick the most appropriate template for a given property based on DataAnnotations attributes or the type of the property. Whether this generates the exact <input> type you need may depend, to an extent, on your application. As always, you can override the generated type by adding your own type attribute to the element in your Razor template. Table 18.1 shows how some of the common data types are mapped to <input> types and how the data types themselves can be specified.
输入标记帮助程序尝试根据 DataAnnotations 属性或属性类型为给定属性选择最合适的模板。这是否生成您需要的确切类型可能在一定程度上取决于您的应用程序。与往常一样,您可以通过将自己的 type 属性添加到 Razor 模板中的 <input> 元素来替代生成的类型。Table 18.1 显示了如何将一些常见数据类型映射到类型以及如何指定数据类型本身。

Table 18.1 Common data types, how to specify them, and the input element type they map to
表 18.1 常见数据类型、如何指定它们以及它们映射到的输入元素类型

Data type How it’s specified Input element type
byte, int, short, long, uint Property type number
decimal, double, float Property type text
bool Property type checkbox
string Property type, [DataType(DataType.Text)] attribute text
HiddenInput [HiddenInput] attribute hidden
Password [Password] attribute password
Phone [Phone] attribute tel
EmailAddress [EmailAddress] attribute email
Url [Url] attribute url
Date DateTime property type, [DataType(DataType.Date)] attribute datetime-local

The Input Tag Helper has one additional attribute that can be used to customize the way data is displayed: asp-format. HTML forms are entirely string-based, so when the value of an <input> is set, the Input Tag Helper must take the value stored in the property and convert it to a string. Under the covers, this performs a string.Format() on the property’s value, passing in the format string.
Input Tag Helper 具有一个可用于自定义数据显示方式的附加属性:asp-format。HTML 表单完全基于字符串,因此在设置 <input> 的值时,Input Tag Helper 必须获取存储在属性中的值并将其转换为字符串。在后台,这将执行一个字符串。Format() 对属性的值执行,并传入格式字符串。

The Input Tag Helper uses a default format string for each different data type, but with the asp-format attribute, you can set the specific format string to use. For example, you could ensure that a decimal property, Dec, is formatted to three decimal places with the following code:
Input Tag Helper 对每种不同的数据类型使用默认格式字符串,但使用 asp-format 属性,您可以设置要使用的特定格式字符串。例如,您可以使用以下代码确保将 decimal 属性 Dec 的格式设置为三位小数:

<input asp-for="Dec" asp-format="{0:0.000}" />

If the Dec property had a value of 1.2, this would generate HTML similar to
如果 Dec 属性的值为 1.2,则生成类似于

<input type="text" id="Dec" name="Dec" value="1.200">

Alternatively, you can define the format to use by adding the [DisplayFormat] attribute to the model property:
或者,您可以通过将 [DisplayFormat] 属性添加到 model 属性来定义要使用的格式:

[DisplayFormat("{0:0.000}")]
public decimal Dec { get; set; }

NOTE You may be surprised that decimal and double types are rendered as text fields and not as number fields. This is due to several technical reasons, predominantly related to the way different cultures render decimal points and number group separators. Rendering as text avoids errors that would appear only in certain browser-culture combinations.
注意您可能会惊讶地发现,decimal 和 double 类型呈现为文本字段,而不是数字字段。这是由于几个技术原因,主要与不同区域性呈现小数点和数字组分隔符的方式有关。呈现为文本可避免仅在某些浏览器区域性组合中出现的错误。

In addition to the Input Tag Helper, ASP.NET Core provides the Textarea Tag Helper. This works in a similar way, using the asp-for attribute, but it’s attached to a <textarea> element instead:
除了 Input Tag Helper 之外,ASP.NET Core 还提供 Textarea Tag Helper。这以类似的方式工作,使用 asp-for 属性,但它被附加到一个<textarea> 元素上:

<textarea asp-for="BigtextValue"></textarea>

This generates HTML similar to the following. Note that the property value is rendered inside the element, and data-val- validation elements are attached as usual:
这将生成类似于以下内容的 HTML。请注意,property value 在元素内部呈现,并且 data-val-
验证元素像往常一样附加:

<textarea data-val="true" id="BigtextValue" name="BigtextValue"
    data-val-length="Maximum length 200." data-val-length-max="200"
    data-val-required="The Multiline field is required." >This is some text,
I'm going to display it
in a text area</textarea>

I hope that this section has hammered home how much typing Tag Helpers can cut down on, especially when using them in conjunction with DataAnnotations for generating validation attributes. But this is more than reducing the number of keystrokes required; Tag Helpers ensure that the markup generated is correct and has the correct name, id, and format to automatically bind your binding models when they’re sent to the server.
我希望本节已经阐明了 Tag Helpers 可以减少多少键入工作,尤其是在将它们与 DataAnnotations 结合使用以生成验证属性时。但这不仅仅是减少所需的击键次数;标记帮助程序确保生成的标记正确无误,并且具有正确的名称、ID 和格式,以便在将绑定模型发送到服务器时自动绑定绑定模型。

With <form>, <label>, and <input> under your belt, you’re able to build most of your currency converter forms. Before we look at displaying validation messages, there’s one more element to look at: the <select>, or drop-down, input.
使用 <form>, <label><input> ,您可以构建大多数货币转换器表单。在我们查看显示验证消息之前,还有一个元素需要查看:<select>或下拉列表,输入。

18.2.4 The Select Tag Helper

As well as <input> fields, a common element you’ll see on web forms is the <select> element, or drop-down lists and list boxes. Your currency converter application, for example, could use a <select> element to let you pick which currency to convert from a list.
除了<input> 字段之外,您将在 Web 表单上看到的一个常见元素是 <select>元素,即下拉列表和列表框。例如,您的货币转换器应用程序可以使用一个 <select>元素让您从列表中选择要转换的货币。

By default, this element shows a list of items and lets you select one, but there are several variations, as shown in figure 18.6. As well as the normal drop-down list, you could show a list box, add multiselection, or display your list items in groups.
默认情况下,此元素显示一个项目列表并允许您选择一个,但有几种变体,如图 18.6 所示。除了常规下拉列表外,您还可以显示列表框、添加多选或成组显示列表项。

alt text

Figure 18.6 Some of the many ways to display <select> elements using the Select Tag Helper.
图 18.6 使用 Select Tag Helper 显示<select>元素的多种方法中的一些。

To use <select> elements in your Razor code, you’ll need to include two properties in your PageModel: one property for the list of options to display and one to hold the value (or values) selected. For example, listing 18.5 shows the properties on the PageModel used to create the three leftmost select lists shown in figure 18.6. Displaying groups requires a slightly different setup, as you’ll see shortly.
要在 Razor 代码中使用<select> 元素,您需要在 PageModel 中包含两个属性:一个属性用于显示选项列表,另一个属性用于保存所选值。例如,清单 18.5 显示了用于创建图 18.6 中所示的三个最左侧选择列表的 PageModel 上的属性。显示组需要的设置略有不同,您很快就会看到。

Listing 18.5 View model for displaying select element drop-down lists and list boxes

public class SelectListsModel: PageModel
{
    [BindProperty]                                #A
    public class InputModel Input { get; set; }   #A

    public IEnumerable<SelectListItem> Items { get; set; }    #B
        = new List<SelectListItem>                            #B
    {                                                         #B
        new SelectListItem{Value = "csharp", Text="C#"},       #B
        new SelectListItem{Value = "python", Text= "Python"},  #B
        new SelectListItem{Value = "cpp", Text="C++"},         #B
        new SelectListItem{Value = "java", Text="Java"},       #B
        new SelectListItem{Value = "js", Text="JavaScript"},   #B
        new SelectListItem{Value = "ruby", Text="Ruby"},       #B
    };                                                        #B

    public class InputModel
    {
        public string SelectedValue1 { get; set; }                #C
        public string SelectedValue2 { get; set; }                #C
        public IEnumerable<string> MultiValues { get; set; }    #D
    }
}

❶ The InputModel for binding the user’s selections to the select boxes
用于将用户的选择绑定到选择框的 InputModel
❷ The list of items to display in the select boxes
要在选择框中显示的项目列表
❸ These properties will hold the values selected by the single-selection select boxes.
这些属性将保存由单选选择框选择的值。
❹ To create a multiselect list box, use an IEnumerable<>.
若要创建多选列表框,请使用 IEnumerable<>

This listing demonstrates several aspects of working with <select> lists:
此清单演示了使用<select>列表的几个方面:

• SelectedValue1/SelectedValue2—Used to hold the value selected by the user. They’re model-bound to the value selected from the drop-down list/list box and used to preselect the correct item when rendering the form.
SelectedValue1/SelectedValue2 - 用于保存用户选择的值。它们被模型绑定到从下拉列表/列表框中选择的值,并用于在呈现表单时预先选择正确的项目。

• MultiValues—Used to hold the selected values for a multiselect list. It’s an IEnumerable, so it can hold more than one selection per <select> element.
MultiValues - 用于保存多选列表的选定值。它是一个 IEnumerable,因此每个 <select> 元素可以保存多个选择。

• Items—Provides the list of options to display in the <select> elements. Note that the element type must be SelectListItem, which exposes the Value and Text properties, to work with the Select Tag Helper. This isn’t part of the InputModel, as we don’t want to model-bind these items to the request; they would normally be loaded directly from the application model or hardcoded. The order of the values in the Items property controls the order of items in the <select> list.
Items - 提供要在<select> 元素中显示的选项列表。请注意,元素类型必须是 SelectListItem,它公开 Value 和 Text 属性,才能使用 Select 标记帮助程序。这不是 InputModel 的一部分,因为我们不想将这些项模型绑定到请求;它们通常直接从应用程序模型加载或硬编码。Items 属性中值的顺序控制列表中项的顺序。

NOTE The Select Tag Helper works only with SelectListItem elements. That means you’ll normally have to convert from an application-specific list set of items (for example, a List<string> or List<MyClass>) to the UI-centric List<SelectListItem>.
注意:Select Tag Helper 仅适用于 SelectListItem 元素。这意味着您通常必须从特定于应用程序的列表项集(例如,a List<string>List<MyClass>)转换为以 UI 为中心的 List<SelectListItem>

The Select Tag Helper exposes the asp-for and asp-items attributes that you can add to <select> elements. As for the Input Tag Helper, the asp-for attribute specifies the property in your PageModel to bind to. The asp-items attribute provides the IEnumerable<SelectListItem> to display the available <option> elements.
Select 标记帮助程序公开可添加到<select>元素的 asp-for 和 asp-items 属性。对于 Input Tag Helper,asp-for 属性指定要绑定到的 PageModel 中的属性。asp-items 属性提供 以IEnumerable<SelectListItem> 显示可用 <option> 元素。

Tip It’s common to want to display a list of enum options in a <select> list. This is so common that ASP.NET Core ships with a helper for generating a SelectListItem for any enum. If you have an enum of the TEnum type, you can generate the available options in your view using asp-items="Html.GetEnumSelectList<TEnum>()" .
提示:希望在列表中显示枚举选项 <select>列表是很常见的。这种情况非常常见,因此 ASP.NET Core 附带了一个帮助程序,用于为任何枚举生成 SelectListItem。如果您有 TEnum 类型的枚举,则可以使用 asp-items="Html.GetEnumSelectList<TEnum>()" 在视图中生成可用选项。

The following listing shows how to display a drop-down list, a single-selection list box, and a multiselection list box. It uses the PageModel from the previous listing, binding each <select> list value to a different property but reusing the same Items list for all of them.
下面的清单显示了如何显示下拉列表、单选列表框和多选列表框。它使用上一个清单中的 PageModel,将每个<select> 列表值绑定到不同的属性,但对所有列表重用相同的 Items 列表。

Listing 18.6 Razor template to display a select element in three ways
清单 18.6 以三种方式显示 select 元素的 Razor 模板

@page
@model SelectListsModel
<select asp-for="Input.SelectedValue1"   #A
    asp-items="Model.Items"></select>    #A
<select asp-for="Input.SelectedValue2"            #B
    asp-items="Model.Items" size="4"></select>    #B
<select asp-for="Input.MultiValues"      #C
    asp-items="Model.Items"></select>    #C

❶ Creates a standard drop-down select list by binding to a standard property in asp-for
通过绑定到 asp-for中的标准属性创建标准下拉列表
❷ Creates a single-select list box of height 4 by providing the standard HTML size attribute
通过提供标准 HTML 大小属性创建高度为 4 的单选列表框
❸ Creates a multiselect list box by binding to an IEnumerable property in asp-for
通过绑定到 asp-for 中的 IEnumerable 属性创建多选列表框

I hope you can see that the Razor for generating a drop-down <select> list is almost identical to the Razor for generating a multiselect <select> list. The Select Tag Helper takes care of adding the multiple HTML attribute to the generated output if the property it’s binding to is an IEnumerable.
我希望你能看到,用于生成下拉<select>列表的 Razor 与用于生成多选<select>列表的 Razor 几乎相同。Tag Helper 负责将 multiple HTML 属性添加到生成的输出中(如果它绑定到的属性是 IEnumerable)。

Warning The asp-for attribute must not include the Model. prefix. The asp-items attribute, on the other hand, must include it if referencing a property on the PageModel. The asp-items attribute can also reference other C# items, such as objects stored in ViewData, but using a PageModel property is the best approach.
警告:asp-for 属性不得包含 Model。前缀。另一方面,如果引用 PageModel 上的属性,则 asp-items 属性必须包含它。asp-items 属性还可以引用其他 C# 项,例如存储在 ViewData 中的对象,但使用 PageModel 属性是最好的方法。

You’ve seen how to bind three types of select lists so far, but the one I haven’t yet covered from figure 18.6 is how to display groups in your list boxes using <optgroup> elements. Luckily, nothing needs to change in your Razor code; you have to update only how you define your SelectListItems.
到目前为止,您已经了解了如何绑定三种类型的选择列表,但是图 18.6 中我还没有介绍的是如何使用 <optgroup> 元素在列表框中显示组。幸运的是,您的 Razor 代码中不需要更改任何内容;您只需更新定义 SelectListItems 的方式。

The SelectListItem object defines a Group property that specifies the SelectListGroup the item belongs to. The following listing shows how you could create two groups and assign each list item to a “dynamic” or “static” group, using a PageModel similar to that shown in listing 18.5. The final list item, C#, isn’t assigned to a group, so it will be displayed as normal, without an <optgroup>.
SelectListItem 对象定义一个 Group 属性,该属性指定项目所属的 SelectListGroup。下面的清单显示了如何使用类似于清单 18.5 中所示的 PageModel 创建两个组并将每个列表项分配给“动态”或“静态”组。最后一个列表项 C# 未分配给组,因此没有它将正常显示。

Listing 18.7 Adding Groups to SelectListItems to create optgroup elements
清单 18.7 向 SelectListItems 添加组以创建 optgroup 元素

public class SelectListsModel: PageModel
{
    [BindProperty]
    public IEnumerable<string> SelectedValues { get; set; }    #A
    public IEnumerable<SelectListItem> Items { get; set; }

    public SelectListsModel()     #B
    {
        var dynamic = new SelectListGroup { Name = "Dynamic" };   #C
        var @static = new SelectListGroup { Name = "Static" };       #C
        Items = new List<SelectListItem>
        {
            new SelectListItem {
                Value= "js",
                Text="Javascript",
                Group = dynamic       #D
            },
            new SelectListItem {
                Value= "cpp",
                Text="C++",
                Group = @static        #D
            },
            new SelectListItem {
                Value= "python",
                Text="Python",
                Group = dynamic       #D
            },
            new SelectListItem {    #E
                Value= "csharp",    #E
                Text="C#",          #E
            }
        };
    }
}

With this in place, the Select Tag Helper generates <optgroup> elements as necessary when rendering the Razor to HTML. The Razor template
完成此作后,Select Tag Helper 会生成<optgroup>元素.

@page
@model SelectListsModel
<select asp-for="SelectedValues" asp-items="Model.Items"></select>

would be rendered to HTML as follows:
将呈现为 HTML,如下所示:

<select id="SelectedValues" name="SelectedValues" multiple="multiple">
    <optgroup label="Dynamic">
        <option value="js">JavaScript</option>
        <option value="python">Python</option>
    </optgroup>
    <optgroup label="Static">
        <option value="cpp">C++</option>
    </optgroup>
    <option value="csharp">C#</option>
</select>

Another common requirement when working with <select> elements is to include an option in the list that indicates that no value has been selected, as shown in figure 18.7. Without this extra option, the default <select> drop-down will always have a value, and it will default to the first item in the list.
使用<select>元素时的另一个常见要求是在列表中包含一个选项,该选项指示未选择任何值,如图 18.7 所示。如果没有这个额外的选项,<select>默认下拉列表将始终有一个值,并且它将默认为列表中的第一项。

alt text

Figure 18.7 Without a “not selected” option, the <select> element will always have a value. This may not be the behavior you desire if you don’t want an <option> to be selected by default.
图 18.7 如果没有 “not selected” 选项, <select> 元素将始终具有一个值。如果您不希望默认选择<option> ,这可能不是您想要的行为。

You can achieve this in one of two ways: you could add the “not selected” option to the available SelectListItems, or you could add the option to the Razor manually, such as by using
您可以通过以下两种方式之一来实现此目的:您可以将“未选择”选项添加到可用的 SelectListItems,也可以手动将选项添加到 Razor,例如使用

<select asp-for="SelectedValue" asp-items="Model.Items">
    <option Value="">**Not selected**</option>
</select>

This will add an extra <option> at the top of your <select> element, with a blank Value attribute, allowing you to provide a “no selection” option for the user.
这将在<select> 元素顶部添加一个额外的<option> Value,其中包含一个空白的 Value 属性,允许您为用户提供 “no selection” 选项。

Tip Adding a “no selection” option to a <select> element is so common that you might want to create a partial view to encapsulate this logic.
提示:向元<select>素添加 “no selection” 选项非常常见,以至于您可能希望创建一个 partial view 来封装此逻辑。

With the Input Tag Helper and Select Tag Helper under your belt, you should be able to create most of the forms that you’ll need. You have all the pieces you need to create the currency converter application now, with one exception.
有了 Input Tag Helper 和 Select Tag Helper,您应该能够创建所需的大多数表单。您现在拥有创建货币转换器应用程序所需的所有部分,但有一个例外。

Remember that whenever you accept input from a user, you should always validate the data. The Validation Tag Helpers provide a way for you to display model validation errors to the user on your form without having to write a lot of boilerplate markup.
请记住,无论何时接受用户的输入,都应始终验证数据。验证标记帮助程序提供了一种在表单上向用户显示模型验证错误的方法,而无需编写大量样板标记。

18.2.5 The Validation Message and Validation Summary Tag Helpers

18.2.5 验证消息和验证摘要标记帮助程序

In section 18.2.3 you saw that the Input Tag Helper generates the necessary data-val- validation attributes on form input elements themselves. But you also need somewhere to display the validation messages. This can be achieved for each property in your view model using the Validation Message Tag Helper applied to a <span> by using the asp-validation-for attribute:
在第 18.2.3 节中,您看到 Input Tag Helper 在表单 input 元素本身上生成必要的 data-val-
验证属性。但您还需要在某个位置显示验证消息。这可以通过使用 asp-validation-for 属性应用于<span> 的验证消息标记帮助程序为视图模型中的每个属性实现:

<span asp-validation-for="Email"></span>

When an error occurs during client-side validation, the appropriate error message for the referenced property is displayed in the <span>, as shown in figure 18.8. This <span> element is also used to show appropriate validation messages if server-side validation fails when the form is redisplayed.
当在客户端验证期间发生错误时,引用的属性的相应错误消息将显示在<span>中,如图 18.8 所示。此<span>元素还用于在重新显示表单时服务器端验证失败时显示相应的验证消息。

alt text

Figure 18.8 Validation messages can be shown in an associated <span> by using the Validation Message Tag Helper.
图 18.8 验证消息可以使用 Validation Message Tag Helper 显示在关联的<span>中。

Any errors associated with the Email property stored in ModelState are rendered in the element body, and the appropriate attributes to hook into jQuery validation are added:
与 ModelState 中存储的 Email 属性关联的任何错误都将呈现在元素正文中,并添加用于挂接到 jQuery 验证的相应属性:

<span class="field-validation-valid" data-valmsg-for="Email"
  data-valmsg-replace="true">The Email Address field is required.</span>

The validation error shown in the element is removed or replaced when the user updates the Email <input> field and client-side validation is performed.
当用户更新 Email <input>字段并执行客户端验证时,将删除或替换元素中显示的验证错误。

NOTE For more details on ModelState and server-side validation, see chapter 16.
注意有关 ModelState 和服务器端验证的更多详细信息,请参阅第 16 章。

As well as display validation messages for individual properties, you can display a summary of all the validation messages in a <div> with the Validation Summary Tag Helper, shown in figure 18.9. This renders a <ul> containing a list of the ModelState errors.
除了显示各个属性的验证消息外,您还可以使用 Validation Summary Tag Helper 在<div> 中显示所有验证消息的摘要,如图 18.9 所示。这将呈现一个包含 ModelState 错误列表的<ul>

alt text

Figure 18.9 Form showing validation errors. The Validation Message Tag Helper is applied to <span>, close to the associated input. The Validation Summary Tag Helper is applied to a <div>, normally at the top or bottom of the form.
图 18.9 显示验证错误的表单。验证消息标记帮助程序应用于 <span>,靠近关联的输入。验证摘要标记帮助程序应用于<div>,通常位于表单的顶部或底部。

The Validation Summary Tag Helper is applied to a <div> using the asp-validation-summary attribute and providing a ValidationSummary enum value, such as
验证摘要标记帮助程序使用 asp-validation-summary 属性并提供 ValidationSummary 枚举值(如

<div asp-validation-summary="All"></div>

The ValidationSummary enum controls which values are displayed, and it has three possible values:
ValidationSummary 枚举控制显示哪些值,它有三个可能的值:

• None—Don’t display a summary. (I don’t know why you’d use this.)
无 (None) - 不显示摘要。(我不知道你为什么会用这个。)
• ModelOnly—Display only errors that are not associated with a property.
“仅模型”(ModelOnly) - 仅显示与属性无关的错误。
• All—Display errors associated with either a property or the model.
“全部”(All) - 显示与属性或模型关联的错误。

The Validation Summary Tag Helper is particularly useful if you have errors associated with your page that aren’t specific to a single property. These can be added to the model state by using a blank key, as shown in listing 18.8. In this example, the property validation passed, but we provide additional model-level validation to check that we aren’t trying to convert a currency to itself.
如果存在与页面关联的错误,而这些错误并非特定于单个属性,则 Validation Summary Tag Helper 特别有用。这些可以通过使用空键添加到模型状态中,如清单 18.8 所示。在此示例中,属性验证通过,但我们提供了额外的模型级验证,以检查我们是否没有尝试将货币转换为自身。

Listing 18.8 Adding model-level validation errors to the ModelState
示例 18.8 向 ModelState 添加模型级验证错误

public class ConvertModel : PageModel
{
    [BindProperty]
    public InputModel Input { get; set; }

    [HttpPost]
    public IActionResult OnPost()
    {
        if(Input.CurrencyFrom == Input.CurrencyTo)    #A
        {
            ModelState.AddModelError(                  #B
                string.Empty,                          #B
                "Cannot convert currency to itself");  #B
        }
        if (!ModelState.IsValid)     #C
        {                            #C
            return Page();           #C
        }                            #C

        //store the valid values somewhere etc
        return RedirectToPage("Checkout");
    }
}

❶ Can’t convert currency to itself
无法将货币转换为自身
❷ Adds model-level error, not tied to a specific property, by using empty key
使用空键添加模型级错误,不与特定属性绑定
❸ If there are any property-level or model-level errors, displays them
如果存在任何属性级或模型级错误,则显示它们

Without the Validation Summary Tag Helper, the model-level error would still be added if the user used the same currency twice, and the form would be redisplayed. Unfortunately, there would have been no visual cue to the user indicating why the form did not submit. Obviously, that’s a problem! By adding the Validation Summary Tag Helper, the model-level errors are shown to the user so they can correct the problem, as shown in figure 18.10.
如果没有 Validation Summary Tag Helper,如果用户两次使用相同的货币,则仍会添加模型级错误,并且表单将重新显示。遗憾的是,不会向用户提供视觉提示,说明表单未提交的原因。显然,这是一个问题!通过添加 Validation Summary Tag Helper,可以向用户显示模型级错误,以便他们可以纠正问题,如图 18.10 所示。

alt text

Figure 18.10 Model-level errors are only displayed by the Validation Summary Tag Helper. Without one, users won’t have any indication that there were errors on the form and so won’t be able to correct them.
图 18.10 模型级错误仅由 Validation Summary Tag Helper 显示。如果没有 ID,用户将不会有任何迹象表明表单上存在错误,因此无法更正它们。

NOTE For simplicity, I added the validation check to the page handler. An alternative approach would be to create a custom validation attribute or use IValidatableObject (described in chapter 7). That way, your handler stays lean and sticks to the single- responsibility principle (SRP). You’ll see how to create a custom validation attribute in chapter 32.
注意:为简单起见,我将验证检查添加到页面处理程序中。另一种方法是创建自定义验证属性或使用 IValidatableObject(如第 7 章所述)。这样,您的处理人员就会保持精简并坚持单一责任原则 (SRP)。您将在第 32 章中了解如何创建自定义验证属性。

This section covered most of the common Tag Helpers available for working with forms, including all the pieces you need to build the currency converter forms. They should give you everything you need to get started building forms in your own applications. But forms aren’t the only area in which Tag Helpers are useful; they’re generally applicable any time you need to mix server-side logic with HTML generation.
本节介绍了可用于表单的大多数常见 Tag Helper,包括构建货币转换器表单所需的所有部分。它们应该为您提供开始在您自己的应用程序中构建表单所需的一切。但是,表单并不是 Tag Helpers 唯一有用的领域;它们通常适用于您需要将服务器端逻辑与 HTML 生成混合的任何时间。

One such example is generating links to other pages in your application using routing-based URL generation. Given that routing is designed to be fluid as you refactor your application, keeping track of the exact URLs the links should point to would be a bit of a maintenance nightmare if you had to do it by hand. As you might expect, there’s a Tag Helper for that: the Anchor Tag Helper.
一个这样的示例是使用基于路由的 URL 生成生成指向应用程序中其他页面的链接。鉴于路由设计为在重构应用程序时是流畅的,因此如果必须手动跟踪链接应指向的确切 URL,那将有点像维护的噩梦。如您所料,有一个 Tag Helper 可用于此:Anchor Tag Helper。

18.3 Generating links with the Anchor Tag Helper

18.3 使用 Anchor Tag Helper 生成链接

In chapters 6 and 15, I showed how you could generate URLs for links to other pages in your application using LinkGenerator and IUrlHelper. Views are another common place where you need to generate links, normally by way of an <a> element with an href attribute pointing to the appropriate URL.
在第 6 章和第 15 章中,我演示了如何使用 LinkGenerator 和 IUrlHelper 为指向应用程序中其他页面的链接生成 URL。视图是另一个需要生成链接的常见位置,通常是通过具有 href 属性的<a>元素指向相应的 URL。

In this section I show how you can use the Anchor Tag Helper to generate the URL for a given Razor Page using routing. Conceptually, this is almost identical to the way the Form Tag Helper generates the action URL, as you saw in section 18.2.1. For the most part, using the Anchor Tag Helper is identical too; you provide asp-page and asp-page-handler attributes, along with asp-route- attributes as necessary. The default Razor Page templates use the Anchor Tag Helper to generate the links shown in the navigation bar using the code in the following listing.
在本节中,我将介绍如何使用 Anchor Tag Helper 通过路由为给定的 Razor Page 生成 URL。从概念上讲,这与 Form Tag Helper 生成作 URL 的方式几乎相同,如您在第 18.2.1 节中看到的那样。在大多数情况下,使用 Anchor Tag Helper 也是相同的;根据需要提供 asp-page 和 asp-page-handler 属性以及 asp-route-
属性。默认的 Razor 页面模板使用 Anchor Tag Helper 使用以下清单中的代码生成导航栏中显示的链接。

Listing 18.9 Using the Anchor Tag Helper to generate URLs in _Layout.cshtml
列表 18.9 使用 Anchor 标记帮助程序在 _Layout.cshtml 中生成 URL

<ul class="navbar-nav flex-grow-1">
    <li class="nav-item">
        <a class="nav-link text-dark"
            asp-area="" asp-page="/Index">Home</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark"
            asp-area="" asp-page="/Privacy">Privacy</a>
    </li>
</ul>

As you can see, each <a> element has an asp-page attribute. This Tag Helper uses the routing system to generate an appropriate URL for the <a>, resulting in the following markup:
如您所见,每个<a> 元素都有一个 asp-page 属性。此 Tag Helper 使用路由系统为<a> 生成适当的 URL,从而生成以下标记:

<ul class="nav navbar-nav">
    <li class="nav-item">
        <a class="nav-link text-dark" href="/">Home</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" href="/Privacy">Privacy</a>
    </li>t
</ul>

The URLs use default values where possible, so the Index Razor Page generates the simple "/" URL instead of "/Index".
URL 会尽可能使用默认值,因此索引 Razor 页面会生成简单的“/”URL,而不是“/Index”。

If you need more control over the URL generated, the Anchor Tag Helper exposes several additional properties you can set, which are during URL generation. The attributes most often used with Razor Pages are
如果您需要对生成的 URL 进行更多控制,则 Anchor Tag Helper 会公开您可以设置的几个其他属性,这些属性在 URL 生成期间进行。Razor Pages 最常用的属性是

• asp-page—Sets the Razor Page to execute.
asp-page - 设置要执行的 Razor 页面。
• asp-page-handler—Sets the Razor Page handler to execute.
asp-page-handler - 设置要执行的 Razor Page 处理程序。
• asp-area—Sets the area route parameter to use. Areas can be used to provide an additional layer of organization to your application.[1]
asp-area - 设置要使用的区域路由参数。区域可用于为应用程序提供额外的组织层。[1]
• asp-host—If set, the generated link points to the provided host and generates an absolute URL instead of a relative URL.
asp-host - 如果设置,则生成的链接将指向提供的主机,并生成绝对 URL 而不是相对 URL。
• asp-protocol—Sets whether to generate an http or https link. If set, it generates an absolute URL instead of a relative URL.
asp-protocol - 设置是生成 http 还是 https 链接。如果设置,它将生成绝对 URL 而不是相对 URL。
• asp-route-—Sets the route parameters to use during generation. Can be added multiple times for different route parameters.
asp-route-
- 设置生成过程中要使用的路由参数。可以为不同的路由参数多次添加。

By using the Anchor Tag Helper and its attributes, you generate your URLs using the routing system, as described in chapters 5 and 14. This reduces the duplication in your code by removing the hardcoded URLs you’d otherwise need to embed in all your views.
通过使用 Anchor Tag Helper 及其属性,您可以使用路由系统生成 URL,如第 5 章和第 14 章所述。这通过删除您需要嵌入到所有视图中的硬编码 URL 来减少代码中的重复。

If you find yourself writing repetitive code in your markup, chances are someone has written a Tag Helper to help with it. The Append Version Tag Helper in the following section is a great example of using Tag Helpers to reduce the amount of fiddly code required.
如果您发现自己在标记中编写了重复的代码,则很可能有人编写了 Tag Helper 来帮助处理它。以下部分中的 Append Version Tag Helper 是使用 Tag Helpers 减少所需繁琐代码量的一个很好的示例。

18.4 Cache-busting with the Append Version Tag Helper

18.4 使用 Append Version Tag Helper 进行缓存无效化

A common problem with web development, both when developing and when an application goes into production, is ensuring that browsers are all using the latest files. For performance reasons, browsers often cache files locally and reuse them for subsequent requests rather than calling your application every time a file is requested.
Web 开发的一个常见问题,无论是在开发时还是在应用程序投入生产时,都是确保浏览器都使用最新的文件。出于性能原因,浏览器通常会在本地缓存文件,并在后续请求中重复使用它们,而不是在每次请求文件时都调用应用程序。

Normally, this is great. Most of the static assets in your site rarely change, so caching them significantly reduces the burden on your server. Think of an image of your company logo. How often does that change? If every page shows your logo, caching the image in the browser makes a lot of sense.
通常,这很好。您网站中的大多数静态资源很少更改,因此缓存它们可以显著减轻服务器的负担。想想您公司徽标的图像。这种情况多久改变一次?如果每个页面都显示您的 logo,那么在浏览器中缓存图像就很有意义。

But what happens if it does change? You want to make sure users get the updated assets as soon as they’re available. A more critical requirement might be if the JavaScript files associated with your site change. If users end up using cached versions of your JavaScript, they might see strange errors, or your application might appear broken to them.
但是,如果它真的发生了变化,会发生什么呢?您希望确保用户在更新的资产可用时立即获得更新的资产。更关键的要求可能是与您的网站关联的 JavaScript 文件是否发生更改。如果用户最终使用您的 JavaScript 的缓存版本,他们可能会看到奇怪的错误,或者您的应用程序可能会对他们造成破坏。

This conundrum is a common one in web development, and one of the most common ways for handling it is to use a cache-busting query string.
这个难题在 Web 开发中很常见,最常见的处理方法之一是使用缓存清除查询字符串。

DEFINITION A cache-busting query string adds a query parameter to a URL, such as ?v=1. Browsers will cache the response and use it for subsequent requests to the URL. When the resource changes, the query string is also changed, such as to ?v=2. Browsers will see this as a request for a new resource and make a fresh request.
定义:缓存无效化查询字符串会将查询参数添加到 URL,例如 ?v=1。浏览器将缓存响应并将其用于对 URL 的后续请求。当资源更改时,查询字符串也会更改,例如 ?v=2。浏览器会将此视为对新资源的请求,并发出新的请求。

The biggest problem with this approach is that it requires you to update a URL every time an image, CSS, or JavaScript file changes. This is a manual step that requires updating every place the resource is referenced, so it’s inevitable that mistakes are made. Tag Helpers to the rescue! When you add a <script>, <img>, or <link> element to your application, you can use Tag Helpers to automatically generate a cache-busting query string:
这种方法的最大问题是,它要求您在每次图像、CSS 或 JavaScript 文件更改时更新 URL。这是一个手动步骤,需要更新引用资源的每个位置,因此不可避免地会犯错误。标记助手来救援!当您向应用程序添加 <script>, <img><link>元素时,您可以使用 Tag Helpers 自动生成缓存无效化查询字符串:

<script src="~/js/site.js" asp-append-version="true"></script>

The asp-append-version attribute will load the file being referenced and generate a unique hash based on its contents. This is then appended as a unique query string to the resource URL:
asp-append-version 属性将加载被引用的文件,并根据其内容生成唯一的哈希值。然后,将其作为唯一查询字符串附加到资源 URL:

<script src="/js/site.js?v=EWaMeWsJBYWmL2g_KkgXZQ5nPe"></script>

As this value is a hash of the file contents, it remains unchanged as long as the file isn’t modified, so the file will be cached in users’ browsers. But if the file is modified, the hash of the contents changes and so does the query string. This ensures that browsers are always served the most up-to-date files for your application without your having to worry about updating every URL manually whenever you change a file.
由于此值是文件内容的哈希值,因此只要文件未被修改,它就会保持不变,因此该文件将缓存在用户的浏览器中。但是,如果文件被修改,内容的哈希值会发生变化,查询字符串也会发生变化。这可确保浏览器始终为您的应用程序提供最新的文件,而不必担心在更改文件时手动更新每个 URL。

So far in this chapter you’ve seen how to use Tag Helpers for forms, link generation, and cache busting. You can also use Tag Helpers to conditionally render different markup depending on the current environment. This uses a technique you haven’t seen yet, where the Tag Helper is declared as a completely separate element.
到目前为止,在本章中,您已经了解了如何使用标签帮助程序进行表单、链接生成和缓存无效化。您还可以使用 Tag Helpers 根据当前环境有条件地呈现不同的标记。这使用了一种你还没见过的技术,其中 Tag Helper 被声明为一个完全独立的元素。

18.5 Using conditional markup with the Environment Tag Helper

18.5 将条件标记与环境标记帮助程序一起使用

In many cases, you want to render different HTML in your Razor templates depending on whether your website is running in a development or production environment. For example, in development you typically want your JavaScript and CSS assets to be verbose and easy to read, but in production you’d process these files to make them as small as possible. Another example might be the desire to apply a banner to the application when it’s running in a testing environment, which is removed when you move to production, as shown in figure 18.11.
在许多情况下,您希望在 Razor 模板中呈现不同的 HTML,具体取决于您的网站是在开发环境中运行还是在生产环境中运行。例如,在开发中,您通常希望 JavaScript 和 CSS 资源冗长且易于阅读,但在生产环境中,您需要处理这些文件以使其尽可能小。另一个示例可能是希望在应用程序在测试环境中运行时向应用程序应用横幅,当您移动到生产环境时,该横幅将被删除,如图 18.11 所示。

alt text

Figure 18.11 The warning banner will be shown whenever you’re running in a testing environment, to make it easy to distinguish from production.
图 18.11 当您在测试环境中运行时,都会显示警告横幅,以便于与生产区分开来。

You’ve already seen how to use C# to add if statements to your markup, so it would be perfectly possible to use this technique to add an extra div to your markup when the current environment has a given value. If we assume that the env variable contains the current environment, you could use something like this:
您已经了解了如何使用 C# 将 if 语句添加到标记中,因此当当前环境具有给定值时,完全可以使用此技术向标记中添加额外的 div。如果我们假设 env 变量包含当前环境,则可以使用如下内容:

@if(env == "Testing" || env == "Staging")
{
    <div class="warning">You are currently on a testing environment</div>
}

There’s nothing wrong with this, but a better approach would be to use the Tag Helper paradigm to keep your markup clean and easy to read. Luckily, ASP.NET Core comes with the EnvironmentTagHelper, which can be used to achieve the same result in a slightly clearer way:
这没有错,但更好的方法是使用 Tag Helper 范例来保持标记干净且易于阅读。幸运的是,ASP.NET Core 附带了 EnvironmentTagHelper,它可用于以更清晰的方式实现相同的结果:

<environment include="Testing,Staging">
    <div class="warning">You are currently on a testing environment</div>
</environment>

This Tag Helper is a little different from the others you’ve seen before. Instead of augmenting an existing HTML element using an asp- attribute, the whole element is the Tag Helper. This Tag Helper is completely responsible for generating the markup, and it uses an attribute to configure it.
此 Tag Helper 与您以前见过的其他 Tag Helper 略有不同。整个元素不是使用 asp- 属性来扩充现有的 HTML 元素,而是 Tag Helper。此 Tag Helper 完全负责生成标记,并使用属性对其进行配置。

Functionally, this Tag Helper is identical to the C# markup (where the env variable contains the hosting environment, as described in chapter 10), but it’s more declarative in its function than the C# alternative. You’re obviously free to use either approach, but personally I like the HTML-like nature of Tag Helpers.
从功能上讲,此 Tag Helper 与 C# 标记相同(其中 env 变量包含托管环境,如第 10 章所述),但它的函数比 C# 替代方案更具声明性。显然,您可以自由使用任何一种方法,但就个人而言,我喜欢 Tag Helper 的类似 HTML 的性质。

We’ve reached the end of this chapter on Tag Helpers, and with it, we’ve finished our main look at building traditional web applications that display HTML to users. In the last part of the book, we’ll revisit Razor templates when you learn how to build custom components like custom Tag Helpers and view components. For now, you have everything you need to build complex Razor layouts; the custom components can help tidy up your code down the line.
我们已经完成了本章关于标记帮助程序的结尾,这样,我们已经完成了构建向用户显示 HTML 的传统 Web 应用程序的主要内容。在本书的最后一部分,当您学习如何构建自定义组件(如自定义标记帮助程序)和视图组件时,我们将重新访问 Razor 模板。目前,您拥有构建复杂 Razor 布局所需的一切;自定义组件可以帮助整理您的代码。

Part 3 of this book has been a whistle-stop tour of how to build Razor Page applications with ASP.NET Core. You now have the basic building blocks to start making server-rendered ASP.NET Core applications. Before we move on to discussing security in part 4 of this book, I’ll take a couple of chapters to discuss building apps with MVC controllers.
本书的第 3 部分简要介绍了如何使用 ASP.NET Core 构建 Razor Page 应用程序。现在,您拥有了开始制作服务器渲染的 ASP.NET Core 应用程序的基本构建块。在我们继续讨论本书的第 4 部分的安全性之前,我将用几章来讨论使用 MVC 控制器构建应用程序。

I’ve talked about MVC controllers a lot in passing, but in chapter 19 you’ll learn why I recommend Razor Pages over MVC controllers for server-rendered apps. Nevertheless, there are some situations for which MVC controllers make sense.
我顺便谈了很多 MVC 控制器,但在第 19 章中,您将了解为什么我建议将 Razor Pages 用于服务器渲染的应用程序,而不是 MVC 控制器。尽管如此,在某些情况下,MVC 控制器是有意义的。

18.6 Summary

18.6 总结

With Tag Helpers, you can bind your data model to HTML elements, making it easier to generate dynamic HTML while remaining editor friendly.
使用标记帮助程序,您可以将数据模型绑定到 HTML 元素,从而更轻松地生成动态 HTML,同时保持编辑器友好性。

As with Razor in general, Tag Helpers are for server-side rendering of HTML only. You can’t use them directly in frontend frameworks, such as Angular or React.
与一般的 Razor 一样,标记帮助程序仅用于 HTML 的服务器端呈现。您不能直接在前端框架(如 Angular 或 React)中使用它们。

Tag Helpers can be standalone elements or can attach to existing HTML using attributes. This lets you both customize HTML elements and add entirely new elements.
标记帮助程序可以是独立元素,也可以使用属性附加到现有 HTML。这样,您既可以自定义 HTML 元素,也可以添加全新的元素。

Tag Helpers can customize the elements they’re attached to, add additional attributes, and customize how they’re rendered to HTML. This can greatly reduce the amount of markup you need to write.
标记帮助程序可以自定义它们所附加到的元素,添加其他属性,并自定义它们呈现为 HTML 的方式。这可以大大减少您需要编写的标记量。

Tag Helpers can expose multiple attributes on a single element. This makes it easier to configure the Tag Helper, as you can set multiple, separate values.
标记帮助程序可以在单个元素上公开多个属性。这使得配置 Tag Helper 变得更加容易,因为您可以设置多个单独的值。

You can add the asp-page and asp-page-handler attributes to the <form> element to set the action URL using the URL generation feature of Razor Pages.
可以将 asp-page 和 asp-page-handler 属性添加到<form>元素,以使用 Razor Pages 的 URL 生成功能设置作 URL。

You specify route values to use during routing with the Form Tag Helper using asp-route- attributes. These values are used to build the final URL or are passed as query data.
使用 asp-route-
属性通过表单标记帮助程序指定要在路由期间使用的路由值。这些值用于构建最终 URL 或作为查询数据传递。

The Form Tag Helper also generates a hidden field that you can use to prevent CSRF attacks. This is added automatically and is an important security measure.
Form Tag Helper 还会生成一个隐藏字段,您可以使用它来防止 CSRF 攻击。这是自动添加的,是一项重要的安全措施。

You can attach the Label Tag Helper to a <label> using asp-for. It generates an appropriate for attribute and caption based on the [Display] DataAnnotation attribute and the PageModel property name.
您可以将 Label Tag Helper 附加到<label> 使用 asp-for.它根据 [Display] DataAnnotation 属性和 PageModel 属性名称生成相应的属性和标题。

The Input Tag Helper sets the type attribute of an <input> element to the appropriate value based on a bound property’s Type and any DataAnnotation attributes applied to it. It also generates the data-val- attributes required for client-side validation. This significantly reduces the amount of HTML code you need to write.
Input Tag Helper 根据绑定属性的 Type 和应用于它的任何 DataAnnotation 属性,将 <input>元素的 type 属性设置为适当的值。它还生成客户端验证所需的 data-val-
属性。这大大减少了您需要编写的 HTML 代码量。

To enable client-side validation, you must add the necessary JavaScript files to your view for jQuery validation and unobtrusive validation.
要启用客户端验证,您必须将必要的 JavaScript 文件添加到视图中,以进行 jQuery 验证和不引人注目的验证。

The Select Tag Helper can generate drop-down <select> elements as well as list boxes, using the asp-for and asp-items attributes. To generate a multiselect <select> element, bind the element to an IEnumerable property on the view model. You can use these approaches to generate several different styles of select box.
Select Tag Helper 可以使用 asp-for 和 asp-items 属性生成下拉<select>元素和列表框。要生成多选元素,请将该<select>元素绑定到视图模型上的 IEnumerable 属性。您可以使用这些方法生成多种不同样式的选择框。

You can generate an IEnumerable<SelectListItem> for an enum TEnum using the Html.GetEnumSelectList<TEnum>() helper method. This saves you having to write the mapping code yourself.
您可以使用 helper Html.GetEnumSelectList<TEnum>() 方法生成 IEnumerable<SelectListItem> 枚举 TEnum。这样就不必自己编写映射代码。

The Select Tag Helper generates <optgroup> elements if the items supplied in asp-for have an associated SelectListGroup on the Group property. Groups can be used to separate items in select lists.
如果 asp-for 中提供的项在 Group 属性上具有关联的 SelectListGroup,则 Select Tag Helper 会生成<optgroup> 元素。组可用于分隔选择列表中的项目。

Any extra additional <option> elements added to the Razor markup are passed through to the final HTML unchanged. You can use these additional elements to easily add a “no selection” option to the <select> element.
添加到 Razor 标记的任何其他额外<option>元素都将原封不动地传递到最终 HTML。您可以使用这些附加元素轻松地向 <select>元素添加“无选择”选项。

The Validation Message Tag Helper is used to render the client- and server-side validation error messages for a given property. This gives important feedback to your users when elements have errors. Use the asp-validation-for attribute to attach the Validation Message Tag Helper to a <span>.
验证消息标记帮助程序用于呈现给定属性的客户端和服务器端验证错误消息。当元素有错误时,这会向用户提供重要的反馈。使用 asp-validation-for 属性将验证消息标记帮助程序附加到<span> .

The Validation Summary Tag Helper displays validation errors for the model, as well as for individual properties. You can use model-level properties to display additional validation that doesn’t apply to just one property. Use the asp-validation-summary attribute to attach the Validation Summary Tag Helper to a <div>.
Validation Summary Tag Helper 显示模型以及各个属性的验证错误。您可以使用模型级属性来显示不仅适用于一个属性的其他验证。使用 asp-validation-summary 属性将验证摘要标记帮助程序附加到 <div>.

You can generate <a> URLs using the Anchor Tag Helper. This helper uses routing to generate the href URL using asp-page, asp-page-handler, and asp-route- attributes, giving you the full power of routing.
您可以使用 Anchor Tag Helper 生成<a> URL。此帮助程序使用路由通过 asp-page、asp-page-handler 和 asp-route-
属性生成 href URL,从而为您提供完整的路由功能。

You can add the asp-append-version attribute to <link>, <script>, and <img> elements to provide cache-busting capabilities based on the file’s contents. This ensures users cache files for performance reasons, yet still always get the latest version of files.
您可以将 asp-append-version 属性添加到 <link>, <script><img>元素中,以根据文件的内容提供缓存清除功能。这可确保用户出于性能原因缓存文件,但仍始终获得最新版本的文件。

You can use the Environment Tag Helper to conditionally render different HTML based on the app’s current execution environment. You can use this to render completely different HTML in different environments if you wish.
您可以使用 Environment Tag Helper 根据应用程序的当前执行环境有条件地呈现不同的 HTML。如果你愿意,你可以使用它来在不同的环境中呈现完全不同的 HTML。

[1] I don’t cover areas in detail in this book. They’re an optional aspect of MVC that are often only used on large projects. You can read about them here: http://mng.bz/3X64.
[1] 我在这本书中没有详细介绍各个领域。它们是 MVC 的一个可选方面,通常仅用于大型项目。您可以在此处阅读有关它们的信息:http://mng.bz/3X64

ASP.NET Core in Action 17 Rendering HTML using Razor views

17 Rendering HTML using Razor views
17 使用 Razor 视图呈现 HTML

This chapter covers
本章涵盖
• Creating Razor views to display HTML to a user
创建 Razor 视图以向用户显示 HTML
• Using C# and the Razor markup syntax to generate HTML dynamically
使用 C# 和 Razor 标记语法动态生成 HTML
• Reusing common code with layouts and partial views
将通用代码与布局和部分视图重复使用

It’s easy to get confused between the terms involved in Razor Pages—PageModel, page handlers, Razor views—especially as some of the terms describe concrete features, and others describe patterns and concepts. We’ve touched on all these terms in detail in previous chapters, but it’s important to get them straight in your mind:
Razor Pages 中涉及的术语(PageModel、页面处理程序、Razor 视图)很容易混淆,尤其是当其中一些术语描述具体功能,而另一些术语描述模式和概念时。我们在前面的章节中详细介绍了所有这些术语,但请务必将它们清晰地记在脑海中:

• Razor Pages—Razor Pages generally refers to the page-based paradigm that combines routing, model binding, and HTML generation using Razor views.
Razor Pages - Razor Pages 通常是指基于 Page 的范例,它使用 Razor 视图将路由、模型绑定和 HTML 生成相结合。

• Razor Page—A single Razor Page represents a single page or endpoint. It typically consists of two files: a .cshtml file containing the Razor view and a .cshtml.cs file containing the page’s PageModel.
Razor 页面 - 单个 Razor 页面表示单个页面或端点。它通常由两个文件组成:一个包含 Razor 视图的 .cshtml 文件和一个包含页面 PageModel 的 .cshtml.cs 文件。

• PageModel—The PageModel for a Razor Page is where most of the action happens. It’s where you define the binding models for a page, which extracts data from the incoming request. It’s also where you define the page’s page handlers.
PageModel - Razor 页面的 PageModel 是大多数作发生的位置。您可以在此处定义页面的绑定模型,该页面从传入请求中提取数据。您还可以在此处定义页面的页面处理程序。

• Page handler—Each Razor Page typically handles a single route, but it can handle multiple HTTP verbs such as GET and POST. Each page handler typically handles a single HTTP verb.
页面处理程序 - 每个 Razor 页面通常处理单个路由,但也可以处理多个 HTTP 动词,例如 GET 和 POST。每个页面处理程序通常处理一个 HTTP 动词。

• Razor view—Razor views (also called Razor templates) are used to generate HTML. They are typically used in the final stage of a Razor Page to generate the HTML response to send back to the user.
Razor 视图 - Razor 视图 (也称为 Razor 模板) 用于生成 HTML。它们通常用于 Razor 页面的最后阶段,以生成 HTML 响应以发送回用户。

In the previous four chapters, I covered a whole cross section of Razor Pages, including the Model-View-Controller (MVC) design pattern, the Razor Page PageModel, page handlers, routing, and binding models. This chapter covers the last part of the MVC pattern: using a view to generate the HTML that’s delivered to the user’s browser.
在前四章中,我介绍了 Razor Pages 的整个横截面,包括模型-视图-控制器 (MVC) 设计模式、Razor Page PageModel、页面处理程序、路由和绑定模型。本章介绍 MVC 模式的最后一部分:使用视图生成传送到用户浏览器的 HTML。

In ASP.NET Core, views are normally created using the Razor markup syntax (sometimes described as a templating language), which uses a mixture of HTML and C# to generate the final HTML. This chapter covers some of the features of Razor and how to use it to build the view templates for your application. Generally speaking, users will have two sorts of interactions with your app: they’ll read data that your app displays, and they’ll send data or commands back to it. The Razor language contains several constructs that make it simple to build both types of applications.
在 ASP.NET Core 中,视图通常使用 Razor 标记语法(有时描述为模板语言)创建,该语法使用 HTML 和 C# 的混合来生成最终的 HTML。本章介绍 Razor 的一些功能,以及如何使用它来为您的应用程序构建视图模板。一般来说,用户将与你的应用进行两种类型的交互:他们将读取你的应用显示的数据,并将数据或命令发送回它。Razor 语言包含多个构造,使构建这两种类型的应用程序变得简单。

When displaying data, you can use the Razor language to easily combine static HTML with values from your PageModel. Razor can use C# as a control mechanism, so adding conditional elements and loops is simple—something you couldn’t achieve with HTML alone.
显示数据时,您可以使用 Razor 语言轻松地将静态 HTML 与 PageModel 中的值组合在一起。Razor 可以使用 C# 作为控制机制,因此添加条件元素和循环很简单,这是单独使用 HTML 无法实现的。

The normal approach to sending data to web applications is with HTML forms. Virtually every dynamic app you build will use forms; some applications will be pretty much nothing but forms! ASP.NET Core and the Razor templating language include Tag Helpers that make generating HTML forms easy.
将数据发送到 Web 应用程序的正常方法是使用 HTML 表单。您构建的几乎每个动态应用程序都将使用表单;有些应用程序几乎只不过是表单!ASP.NET Core 和 Razor 模板语言包括标记帮助程序,使生成 HTML 表单变得容易。

NOTE You’ll get a brief glimpse of Tag Helpers in section 17.1, but I explore them in detail in chapter 18.
注意:您将在 17.1 节中简要了解标记帮助程序,但我会在第 18 章中详细探讨它们。

In this chapter we’ll be focusing primarily on displaying data and generating HTML using Razor rather than creating forms. You’ll see how to render values from your PageModel to the HTML, and how to use C# to control the generated output. Finally, you’ll learn how to extract the common elements of your views into subviews called layouts and partial views, and how to compose them to create the final HTML page.
在本章中,我们将主要关注使用 Razor 显示数据和生成 HTML,而不是创建表单。您将了解如何将 PageModel 中的值呈现到 HTML,以及如何使用 C# 控制生成的输出。最后,您将学习如何将视图的常见元素提取到称为布局和分部视图的子视图中,以及如何编写它们以创建最终的 HTML 页面。

17.1 Views: Rendering the user interface

17.1 视图:渲染用户界面

In this section I provide a quick introduction to rendering HTML using Razor views. We’ll recap the MVC design pattern used by Razor Pages and where the view fits in. Then I’ll show how Razor syntax allows you to mix C# and HTML to generate dynamic UIs.
在本节中,我将简要介绍如何使用 Razor 视图呈现 HTML。我们将回顾 Razor Pages 使用的 MVC 设计模式以及视图的适用位置。然后,我将展示 Razor 语法如何允许您混合使用 C# 和 HTML 来生成动态 UI。

As you know from earlier chapters on the MVC design pattern, it’s the job of the Razor Page’s page handler to choose what to return to the client. For example, if you’re developing a to-do list application, imagine a request to view a particular to-do item, as shown in figure 17.1.
正如您在前面有关 MVC 设计模式的章节中所知,Razor Page 的页面处理程序的工作是选择要返回给客户端的内容。例如,如果您正在开发一个待办事项列表应用程序,请想象一个查看特定待办事项的请求,如图 17.1 所示。

alt text

Figure 17.1 Handling a request for a to-do list item using ASP.NET Core Razor Pages. The page handler builds the data required by the view and exposes it as properties on the PageModel. The view generates HTML based only on the data provided; it doesn’t need to know where that data comes from.
图 17.1 使用 ASP.NET Core Razor Pages 处理待办事项列表项的请求。页面处理程序构建视图所需的数据,并将其作为 PageModel 上的属性公开。视图仅根据提供的数据生成 HTML;它不需要知道这些数据来自哪里。

A typical request follows the steps shown in figure 17.1:
典型的请求遵循图 17.1 中所示的步骤:

• The middleware pipeline receives the request, and the routing middleware determines the endpoint to invoke—in this case, the View Razor Page in the ToDo folder.
中间件管道接收请求,路由中间件确定要调用的终结点 - 在本例中为 ToDo 文件夹中的 View Razor Page。

• The model binder (part of the Razor Pages framework) uses the request to build the binding models for the page, as you saw in chapter 16. The binding models are set as properties on the Razor Page or are passed to the page handler method as arguments when the handler is executed. The page handler checks that you passed a valid id for the to-do item and marks the ModelState as valid if so.
模型绑定器 (Razor Pages 框架的一部分) 使用请求为页面构建绑定模型,如第 16 章所示。绑定模型在 Razor Page 上设置为属性,或者在执行处理程序时作为参数传递给页面处理程序方法。页面处理程序检查您是否为待办事项传递了有效的 ID,如果是,则将 ModelState 标记为有效。

• If the request is valid, the page handler calls out to the various services that make up the application model. This might load the details about the to-do from a database or from the filesystem, returning them to the handler. As part of this process, either the application model or the page handler itself generates values to pass to the view and sets them as properties on the Razor Page PageModel.
如果请求有效,则页面处理程序将调用构成应用程序模型的各种服务。这可能会从数据库或文件系统加载有关 to-do 的详细信息,并将它们返回给处理程序。在此过程中,应用程序模型或页面处理程序本身会生成要传递给视图的值,并将其设置为 Razor Page PageModel 上的属性。
Once the page handler has executed, the PageModel should contain all the data required to render a view. In this example, it contains details about the to-do itself, but it might also contain other data, such as how many to-dos you have left, whether you have any to-dos scheduled for today, your username, and so on—anything that controls how to generate the end UI for the request.
执行页面处理程序后, PageModel 应包含呈现视图所需的所有数据。在此示例中,它包含有关待办事项本身的详细信息,但它也可能包含其他数据,例如您还剩下多少个待办事项、您今天是否安排了任何待办事项、您的用户名等 — 控制如何为请求生成最终 UI 的任何内容。

• The Razor view template uses the PageModel to generate the final response and returns it to the user via the middleware pipeline.
Razor 视图模板使用 PageModel 生成最终响应,并通过中间件管道将其返回给用户。

A common thread throughout this discussion of MVC is the separation of concerns MVC brings, and it’s no different when it comes to your views. It would be easy enough to generate the HTML directly in your application model or in your controller actions, but instead you delegate that responsibility to a single component: the view.
贯穿本次 MVC 讨论的一个共同点是 MVC 带来的关注点分离,对于您的视图来说,这没有什么不同。直接在应用程序模型或控制器作中生成 HTML 很容易,但您将该责任委托给单个组件:视图。

But even more than that, you separate the data required to build the view from the process of building it by using properties on the PageModel. These properties should contain all the dynamic data the view needs to generate the final output.
但更重要的是,通过使用 PageModel 上的属性,将构建视图所需的数据与构建视图的过程分开。这些属性应包含视图生成最终输出所需的所有动态数据。

Tip Views shouldn’t call methods on the PageModel. The view should generally only be accessing data that has already been collected and exposed as properties.
提示:视图不应调用 PageModel 上的方法。视图通常应仅访问已收集并作为属性公开的数据。

Razor Page handlers indicate that the Razor view should be rendered by returning a PageResult (or by returning void), as you saw in chapter 15. The Razor Pages infrastructure executes the Razor view associated with a given Razor Page to generate the final response. The use of C# in the Razor template means you can dynamically generate the final HTML sent to the browser. This allows you to, for example, display the name of the current user in the page, hide links the current user doesn’t have access to, or render a button for every item in a list.
Razor Page 处理程序指示应通过返回 PageResult(或返回 void)来呈现 Razor 视图,如第 15 章所示。Razor Pages 基础结构执行与给定 Razor 页面关联的 Razor 视图,以生成最终响应。在 Razor 模板中使用 C# 意味着您可以动态生成发送到浏览器的最终 HTML。例如,这允许您在页面中显示当前用户的名称,隐藏当前用户无权访问的链接,或者为列表中的每个项目呈现一个按钮。

Imagine your boss asks you to add a page to your application that displays a list of the application’s users. You should also be able to view a user from the page or create a new one, as shown in figure 17.2.
假设您的老板要求您向应用程序添加一个页面,该页面显示应用程序的用户列表。您还应该能够从页面查看用户或创建新用户,如图 17.2 所示。

alt text

Figure 17.2 The use of C# in Razor lets you easily generate dynamic HTML that varies at runtime. In this example, using a foreach loop inside the Razor view dramatically reduces the duplication in the HTML that you would otherwise have to write.
图 17.2 在 Razor 中使用 C# 可让您轻松生成在运行时变化的动态 HTML。在此示例中,在 Razor 视图中使用 foreach 循环可显著减少 HTML 中必须编写的重复项。

With Razor templates, generating this sort of dynamic content is simple. Listing 17.1 shows a template that could be used to generate the interface in figure 17.2. It combines standard HTML with C# statements and uses Tag Helpers to generate the form elements.
使用 Razor 模板,生成此类动态内容非常简单。清单 17.1 显示了一个可用于生成图 17.2 中接口的模板。它将标准 HTML 与 C# 语句相结合,并使用标记帮助程序生成表单元素。

Listing 17.1 A Razor template to list users and a form for adding a new user
清单 17.1 用于列出用户的 Razor 模板和用于添加新用户的表单

@page
@model IndexViewModel
<div class="row"> ❶
<div class="col-md-6"> ❶
<form method="post">
<div class="form-group">
<label asp-for="NewUser"></label> ❷
<input class="form-control" asp-for="NewUser" /> ❷
<span asp-validation-for="NewUser"></span> ❷
</div>
<div class="form-group">
<button type="submit"
class="btn btn-success">Add</button>
</div>
</form>
</div>
</div>
<h4>Number of users: @Model.ExistingUsers.Count</h4> ❸
<div class="row">
<div class="col-md-6">
<ul class="list-group">
@foreach (var user in Model.ExistingUsers) ❹
{
<li class="list-group-item d-flex justify-content-between">
<span>@user</span>
<a class="btn btn-info"
asp-page="ViewUser" ❺
asp-route-userName="@user">View</a> ❺
</li>
}
</ul>
</div>
</div>

❶ Normal HTML is sent to the browser unchanged.
普通 HTML 原封不动地发送到浏览器。
❷ Tag Helpers attach to HTML elements to create forms.
标签助手附加到 HTML 元素以创建表单。
❸ Values can be written from C# objects to the HTML.
值可以从 C# 对象写入 HTML。
❹ C# constructs such as for loops can be used in Razor.
可以在 Razor 中使用 for 循环等 C# 构造。
❺ Tag Helpers can also be used outside forms to help in other HTML generation.
标签助手也可以在表单之外使用,以帮助生成其他 HTML。

This example demonstrates a variety of Razor features. There’s a mixture of HTML that’s written unmodified to the response output, and there are various C# constructs used to generate HTML dynamically. In addition, you can see several Tag Helpers. These look like normal HTML attributes that start with asp-, but they’re part of the Razor language. They can customize the HTML element they’re attached to, changing how it’s rendered. They make building HTML forms much simpler than they would be otherwise. Don’t worry if this template is a bit overwhelming at the moment; we’ll break it all down as you progress through this chapter and the next.
此示例演示了各种 Razor 功能。响应输出中混合了未经修改的 HTML,并且有各种 C# 构造用于动态生成 HTML。此外,您还可以看到多个 Tag Helpers。这些看起来类似于以 asp- 开头的普通 HTML 属性,但它们是 Razor 语言的一部分。他们可以自定义附加到的 HTML 元素,从而更改其呈现方式。它们使构建 HTML 表单比其他方式简单得多。如果这个模板目前有点让人不知所措,请不要担心;随着您完成本章和下一章,我们将对其进行全部分解。

Razor Pages are compiled when you build your application. Behind the scenes, they become another C# class in your application. It’s also possible to enable runtime compilation of your Razor Pages. This allows you to modify your Razor Pages while your app is running without having to explicitly stop and rebuild. This can be handy when developing locally, but it’s best avoided when you deploy to production. You can read how to enable this at http://mng.bz/jP2P.
Razor Pages 是在构建应用程序时编译的。在后台,它们成为应用程序中的另一个 C# 类。还可以启用 Razor Pages 的运行时编译。这样,您就可以在应用运行时修改 Razor Pages,而无需显式停止和重新生成。在本地开发时,这可能很方便,但在部署到生产环境时最好避免。您可以在 http://mng.bz/jP2P 阅读如何启用此功能。

NOTE As with most things in ASP.NET Core, it’s possible to swap out the Razor templating engine and replace it with your own server-side rendering engine. You can’t replace Razor with a client-side framework like Angular or React. If you want to take this approach, you’d use minimal APIs or web API controllers instead and a separate client-side framework.
注意:与 ASP.NET Core 中的大多数内容一样,可以换出 Razor 模板引擎,并将其替换为您自己的服务器端渲染引擎。您不能将 Razor 替换为 Angular 或 React 等客户端框架。如果要采用此方法,则需要使用最少的 API 或 Web API 控制器以及单独的客户端框架。

In the next section we’ll look in more detail at how Razor views fit into the Razor Pages framework and how you can pass data from your Razor Page handlers to the Razor view to help build the HTML response.
在下一部分中,我们将更详细地了解 Razor 视图如何适应 Razor Pages 框架,以及如何将数据从 Razor Page 处理程序传递到 Razor 视图以帮助构建 HTML 响应。

17.2 Creating Razor views

17.2 创建 Razor 视图

In this section we’ll look at how Razor views fit into the Razor Pages framework. You’ll learn how to pass data from your page handlers to your Razor views and how you can use that data to generate dynamic HTML.
在本部分中,我们将了解 Razor 视图如何适应 Razor Pages 框架。您将了解如何将数据从页面处理程序传递到 Razor 视图,以及如何使用该数据生成动态 HTML。

With ASP.NET Core, whenever you need to display an HTML response to the user, you should use a view to generate it. Although it’s possible to directly generate a string from your page handlers, which will be rendered as HTML in the browser, this approach doesn’t adhere to the MVC separation of concerns and will quickly leave you tearing your hair out.
使用 ASP.NET Core,每当需要向用户显示 HTML 响应时,都应该使用视图来生成它。尽管可以直接从页面处理程序生成字符串,该字符串将在浏览器中呈现为 HTML,但这种方法不符合 MVC 关注点分离,并且很快就会让您感到困惑。

NOTE Some middleware, such as the WelcomePageMiddleware you saw in chapter 4, may generate HTML responses without using a view, which can make sense in some situations. But your Razor Page and MVC controllers should always generate HTML using views.
注意:一些中间件,比如你在第 4 章中看到的 WelcomePageMiddleware,可能会在不使用视图的情况下生成 HTML 响应,这在某些情况下是有意义的。但 Razor Page 和 MVC 控制器应始终使用视图生成 HTML。

Instead, by relying on Razor views to generate the response, you get access to a wide variety of features, as well as editor tooling to help. This section serves as a gentle introduction to Razor views, the things you can do with them, and the various ways you can pass data to them.
相反,通过依靠 Razor 视图生成响应,您可以访问各种功能以及提供帮助的编辑器工具。本节简要介绍了 Razor 视图、您可以使用它们执行的作以及向它们传递数据的各种方式。

17.2.1 Razor views and code-behind

17.2.1 Razor 视图和代码隐藏

In this book you’ve already seen that Razor Pages typically consist of two files:
在本书中,您已经看到 Razor Pages 通常由两个文件组成:
• The .cshtml file, commonly called the Razor view
.cshtml 文件,通常称为 Razor 视图
• The .cshtml.cs file, commonly called the code-behind, which contains the PageModel
.cshtml.cs 文件,通常称为代码隐藏,其中包含 PageModel

The Razor view contains the @page directive, which makes it a Razor Page, as you’ve seen previously. Without this directive, the Razor Pages framework will not route requests to the page, and the file is ignored for most purposes.
Razor 视图包含 @page 指令,这使其成为 Razor 页面,如前所述。如果没有此指令,Razor Pages 框架不会将请求路由到页面,并且在大多数情况下会忽略该文件。

DEFINITION A directive is a statement in a Razor file that changes the way the template is parsed or compiled. Another common directive is the @using newNamespace directive, which makes objects in the newNamespace namespace available.
定义:指令是 Razor 文件中的一条语句,用于更改模板的分析或编译方式。另一个常见指令是 @using newNamespace 指令,它使 newNamespace 命名空间中的对象可用。

The code-behind .cshtml.cs file contains the PageModel for an associated Razor Page. It contains the page handlers that respond to requests, and it is where the Razor Page typically interacts with other parts of your application.
代码隐藏 .cshtml.cs 文件包含关联 Razor 页面的 PageModel。它包含响应请求的页面处理程序,并且是 Razor Page 通常与应用程序的其他部分交互的位置。

Even though the .cshtml and .cshtml.cs files have the same name, such as ToDoItem.cshtml and ToDoItem.cshtml.cs, it’s not the filename that’s linking them. But if it’s not by filename, how does the Razor Pages framework know which PageModel is associated with a given Razor Page view file?
即使 .cshtml 和 .cshtml.cs 文件具有相同的名称(如 ToDoItem.cshtml 和 ToDoItem.cshtml.cs),也不是链接它们的文件名。但是,如果不是按文件名,Razor Pages 框架如何知道哪个 PageModel 与给定的 Razor Page 视图文件相关联?

At the top of each Razor Page, after the @page directive, is the @model directive with a Type, indicating which PageModel is associated with the Razor view. The following directives indicate that the ToDoItemModel is the PageModel associated with the Razor Page:
在每个 Razor 页面的顶部,@page 指令后面是带有 Type 的 @model 指令,指示哪个 PageModel 与 Razor 视图相关联。以下指令指示 ToDoItemModel 是与 Razor Page 关联的 PageModel:

@page
@model ToDoItemModel

Once a request is routed to a Razor Page, as covered in chapter 14, the framework looks for the @model directive to decide which PageModel to use. Based on the PageModel selected, it then binds to any properties in the PageModel marked with the [BindProperty] attribute (as we covered in chapter 16) and executes the appropriate page handler (based on the request’s HTTP verb, as described in chapter 15).
将请求路由到 Razor Page 后(如第 14 章所述),框架会查找 @model 指令来决定使用哪个 PageModel。然后,根据所选的 PageModel,它绑定到 PageModel 中标有 [BindProperty] 属性的任何属性(如第 16 章所述),并执行相应的页面处理程序(基于请求的 HTTP 动词,如第 15 章所述)。

NOTE Technically, the PageModel and @model directive are optional. If you don’t specify a PageModel, the framework executes an implicit page handler, as you saw in chapter 15, and renders the view directly. It’s also possible to combine the .cshtml and .cshtml.cs files into a single .cshtml file. You can read more about this approach in Razor Pages in Action, by Mark Brind (Manning, 2022).
注意:从技术上讲,PageModel 和 @model 指令是可选的。如果你没有指定 PageModel,框架将执行一个隐式页面处理程序,就像你在第 15 章中看到的那样,并直接渲染视图。还可以将 .cshtml 和 .cshtml.cs 文件合并到单个 .cshtml 文件中。您可以在 Mark Brind 的 Razor Pages in Action(Manning,2022 年)中阅读有关此方法的更多信息。

In addition to the @page and @model directives, the Razor view file contains the Razor template that is executed to generate the HTML response.
除了 @page 和 @model 指令之外,Razor 视图文件还包含为生成 HTML 响应而执行的 Razor 模板。

17.2.2 Introducing Razor templates

17.2.2 Razor 模板简介

Razor view templates contain a mixture of HTML and C# code interspersed with one another. The HTML markup lets you easily describe exactly what should be sent to the browser, whereas the C# code can be used to dynamically change what is rendered. The following listing shows an example of Razor rendering a list of strings representing to-do items.
Razor 视图模板包含相互穿插的 HTML 和 C# 代码的混合。HTML 标记可让您轻松准确描述应发送到浏览器的内容,而 C# 代码可用于动态更改呈现的内容。以下清单显示了 Razor 呈现表示待办事项的字符串列表的示例。

Listing 17.2 Razor template for rendering a list of strings
清单 17.2 用于渲染字符串列表的 Razor 模板

@page
@{ ❶
var tasks = new List<string> ❶
{ "Buy milk", "Buy eggs", "Buy bread" }; ❶
} ❶
<h1>Tasks to complete</h1> ❷
<ul>
@for(var i=0; i< tasks.Count; i++) ❸
{ ❸
var task = tasks[i]; ❸
<li>@i - @task</li> ❸
} ❸
</ul>

❶ Arbitrary C# can be executed in a template. Variables remain in scope throughout the page.
可以在模板中执行任意 C#。变量在整个页面中保持范围内。
❷ Standard HTML markup will be rendered to the output unchanged.
标准 HTML 标记将原封不动地呈现到输出。
❸ Mixing C# and HTML allows you to create HTML dynamically at runtime.
混合使用 C# 和 HTML 允许您在运行时动态创建 HTML。

The pure HTML sections in this template are in the angle brackets. The Razor engine copies this HTML directly to the output, unchanged, as though you were writing a normal HTML file.
此模板中的纯 HTML 部分位于尖括号中。Razor 引擎将此 HTML 直接复制到输出中,保持不变,就像您正在编写普通的 HTML 文件一样。

NOTE The ability of Razor syntax to know when you are switching between HTML and C# can be both uncanny and infuriating at times. I discuss how to control this transition in section 17.3.
注意:Razor 语法能够知道您何时在 HTML 和 C# 之间切换,这有时既不可思议又令人恼火。我在 17.3 节中讨论了如何控制这种转换。

As well as HTML, you can see several C# statements in there. The advantage of being able to, for example, use a for loop rather than having to explicitly write out each <li> element should be self-evident. I’ll dive a little deeper into more of the C# features of Razor in the next section. When rendered, the template in listing 17.2 produces the following HTML.
除了 HTML,您还可以在其中看到几个 C# 语句。例如,能够使用 for 循环而不是显式写出每个<li> 元素应该是不言而喻的。在下一节中,我将更深入地介绍 Razor 的更多 C# 功能。呈现后,清单 17.2 中的模板将生成以下 HTML。

Listing 17.3 HTML output produced by rendering a Razor template
列表 17.3 通过呈现 Razor 模板生成的 HTML 输出

<h1>Tasks to complete</h1> ❶
<ul> ❶
<li>0 - Buy milk</li> ❷
<li>1 - Buy eggs</li> ❷
<li>2 - Buy bread</li> ❷
</ul>

❶ HTML from the Razor template is written directly to the output.
Razor 模板中的 HTML 直接写入输出。
❷ The <li> elements are generated dynamically by the for loop, based on the data provided.
<li> 元素由 for 循环根据提供的数据动态生成。
❸ HTML from the Razor template is written directly to the output.
Razor 模板中的 HTML 直接写入输出。

As you can see, the final output of a Razor template after it’s rendered is simple HTML. There’s nothing complicated left, only straight HTML markup that can be sent to the browser and rendered. Figure 17.3 shows how a browser would render it.
如你所见,Razor 模板在呈现后的最终输出是简单的 HTML。没有留下任何复杂的内容,只有可以直接发送到浏览器并呈现的 HTML 标记。图 17.3 显示了浏览器如何呈现它。

alt text

Figure 17.3 Razor templates can be used to generate the HTML dynamically at runtime from C# objects. In this case, a for loop is used to create repetitive HTML <li> elements.
图 17.3 Razor 模板可用于在运行时从 C# 对象动态生成 HTML。在这种情况下,使用 for 循环创建重复的 HTML<li> 元素。

In this example, I hardcoded the list values for simplicity; no dynamic data was provided. This is often the case on simple Razor Pages, like those you might have on your home page; you need to display an almost static page. For the rest of your application, it will be far more common to have some sort of data you need to display, typically exposed as properties on your PageModel.
在此示例中,为简单起见,我对列表值进行了硬编码;未提供动态数据。这在简单的 Razor 页面上通常就是这种情况,就像你在主页上可能拥有的那些一样;您需要显示一个几乎静态的页面。对于应用程序的其余部分,需要显示某种类型的数据(通常作为 PageModel 上的属性公开)将更加常见。

17.2.3 Passing data to views

17.2.3 将数据传递给视图

In ASP.NET Core, you have several ways of passing data from a page handler in a Razor Page to its view. Which approach is best depends on the data you’re trying to pass through, but in general you should use the mechanisms in the following order:
在 ASP.NET Core 中,可以通过多种方式将数据从 Razor 页面中的页面处理程序传递到其视图。哪种方法最好取决于您尝试传递的数据,但通常应按以下顺序使用机制:

• PageModel properties—You should generally expose any data that needs to be displayed as properties on your PageModel. Any data that is specific to the associated Razor view should be exposed this way. The PageModel object is available in the view when it’s rendered, as you’ll see shortly.
PageModel 属性 - 通常应公开需要在 PageModel 上显示为属性的任何数据。特定于关联 Razor 视图的任何数据都应以这种方式公开。PageModel 对象在呈现时在视图中可用,您很快就会看到。

• ViewData—This is a dictionary of objects with string keys that can be used to pass arbitrary data from the page handler to the view. In addition, it allows you to pass data to layout files, as you’ll see in section 17.4. Layout files are the main reason for using ViewData instead of setting properties on the PageModel.
ViewData - 这是带有字符串键的对象字典,可用于将任意数据从页面处理程序传递到视图。此外,它还允许您将数据传递给 layout 文件,如 Section 17.4 所示。布局文件是使用 ViewData 而不是在 PageModel 上设置属性的主要原因。

• TempData—TempData is a dictionary of objects with string keys, similar to ViewData, that is stored until it’s read in a different request. This is commonly used to temporarily persist data when using the POST-REDIRECT-GET pattern. By default TempData stores the data in an encrypted cookie, but other storage options are available, as described in the documentation at http://mng.bz/Wzx1.
TempData - TempData 是具有字符串键的对象字典,类似于 ViewData,在读取其他请求之前会一直存储。这通常用于在使用 POST-REDIRECT-GET 模式时临时保留数据。默认情况下,TempData 将数据存储在加密的 Cookie 中,但也提供了其他存储选项,如 http://mng.bz/Wzx1 中的文档中所述。

• HttpContext—Technically, the HttpContext object is available in both the page handler and Razor view, so you could use it to transfer data between them. But don’t—there’s no need for it with the other methods available to you.
HttpContext - 从技术上讲,HttpContext 对象在页面处理程序和 Razor 视图中均可用,因此您可以使用它来在它们之间传输数据。但不要 - 没有必要使用其他可用的方法。

• @inject services—You can use dependency injection (DI) to make services available in your views, though this should normally be used sparingly. Using the directive @inject Service myService injects a variable called myService of type Service from the DI container, which you can use in your Razor view.
@inject服务 - 您可以使用依赖关系注入 (DI) 使服务在视图中可用,但通常应谨慎使用。使用指令 @inject Service myService 会从 DI 容器中注入一个名为 myService 的 Service 类型变量,您可以在 Razor 视图中使用该变量。

Far and away the best approach for passing data from a page handler to a view is to use properties on the PageModel. There’s nothing special about the properties themselves; you can store anything there to hold the data you require.
将数据从页面处理程序传递到视图的最佳方法无疑是使用 PageModel 上的属性。属性本身并没有什么特别之处;您可以在那里存储任何内容来保存您需要的数据。

NOTE Many frameworks have the concept of a data context for binding UI components. The PageModel is a similar concept, in that it contains values to display in the UI, but the binding is one-directional; the PageModel provides values to the UI, and once the UI is built and sent as a response, the PageModel is destroyed.
注意:许多框架具有用于绑定 UI 组件的数据上下文的概念。PageModel 是一个类似的概念,因为它包含要在 UI 中显示的值,但绑定是单向的;PageModel 向 UI 提供值,一旦构建了 UI 并将其作为响应发送,PageModel 就会被销毁。

As I described in section 17.2.1, the @model directive at the top of your Razor view describes which Type of PageModel is associated with a given Razor Page. The PageModel associated with a Razor Page contains one or more page handlers and exposes data as properties for use in the Razor view, as shown in the following listing.
如第 17.2.1 节所述,Razor 视图顶部的 @model 指令描述了与给定 Razor 页面关联的 PageModel 类型。与 Razor 页面关联的 PageModel 包含一个或多个页面处理程序,并将数据公开为属性,以便在 Razor 视图中使用,如下面的清单所示。

Listing 17.4 Exposing data as properties on a PageModel
清单 17.4 将数据作为 PageModel 上的属性公开

public class ToDoItemModel : PageModel ❶
{
public List<string> Tasks { get; set; } ❷
public string Title { get; set; } ❷
public void OnGet(int id)
{
Title = "Tasks for today"; ❸
Tasks = new List<string> ❸
{ ❸
"Get fuel", ❸
"Check oil", ❸
"Check tyre pressure" ❸
}; ❸
}
}

❶ The PageModel is passed to the Razor view when it executes.
PageModel 在执行时传递到 Razor 视图。
❷ The public properties can be accessed from the Razor view.
可以从 Razor 视图访问公共属性。
❸ Building the required data: this would normally call out to a service or database to load the data.
构建所需的数据:这通常会调用服务或数据库来加载数据。

You can access the PageModel instance itself from the Razor view using the Model property. For example, to display the Title property of the ToDoItemModel in the Razor view, you’d use <h1>@Model.Title</h1>. This would render the string provided in the ToDoItemModel.Title property, producing the <h1>Tasks for today</h1> HTML.
可以使用 Model 属性从 Razor 视图访问 PageModel 实例本身。例如,要在 Razor 视图中显示 ToDoItemModel 的 Title 属性,请使用 <h1>@Model.Title</h1>.这将呈现 ToDoItemModel.Title 属性中提供的字符串,从而生成 HTML <h1>Tasks for today</h1>

Tip Note that the @model directive should be at the top of your view, immediately after the @page directive, and it has a lowercase m. The Model property can be accessed anywhere in the view and has an uppercase M.
提示:请注意,@model 指令应位于视图顶部,紧跟在 @page 指令之后,并且它有一个小写的 m。Model 属性可以在视图中的任意位置访问,并且具有大写的 M。

In most cases, using public properties on your PageModel is the way to go; it’s the standard mechanism for passing data between the page handler and the view. But in some circumstances, properties on your PageModel might not be the best fit. This is often the case when you want to pass data between view layouts. You’ll see how this works in section 17.4.
在大多数情况下,在 PageModel 上使用公共属性是可行的方法;它是在 Page 处理程序和 View 之间传递数据的标准机制。但在某些情况下,PageModel 上的属性可能不是最合适的。当您想在视图布局之间传递数据时,通常会出现这种情况。您将在 Section 17.4 中看到它是如何工作的。

A common example is the title of the page. You need to provide a title for every page in your application, so you could create a base class with a Title property and make every PageModel inherit from it. But that’s cumbersome, so a common approach for this situation is to use the ViewData collection to pass data around.
一个常见的示例是页面的标题。您需要为应用程序中的每个页面提供一个标题,以便您可以创建一个具有 Title 属性的基类,并使每个 PageModel 都继承自该基类。但这很麻烦,因此这种情况的常见方法是使用 ViewData 集合来传递数据。

In fact, the standard Razor Page templates use this approach by default, by setting values on the ViewData dictionary from within the view itself:
事实上,标准 Razor 页面模板默认使用此方法,方法是从视图本身中设置 ViewData 字典的值:

@{
    ViewData["Title"] = "Home Page";
}
<h2>@ViewData["Title"].</h2>

This template sets the value of the "Title" key in the ViewData dictionary to "Home Page" and then fetches the key to render in the template. This set and immediate fetch might seem superfluous, but as the ViewData dictionary is shared throughout the request, it makes the title of the page available in layouts, as you’ll see later. When rendered, the preceding template would produce the following output:
此模板将 ViewData 字典中 “Title” 键的值设置为 “Home Page”,然后获取要在模板中呈现的键。这种 set 和 immediate fetch 可能看起来是多余的,但是由于 ViewData 字典在整个请求中是共享的,因此它使页面的标题在布局中可用,您稍后将看到。渲染时,前面的模板将生成以下输出:

<h2>Home Page.</h2>

You can also set values in the ViewData dictionary from your page handlers in two different ways, as shown in the following listing.
您还可以通过两种不同的方式从页面处理程序中设置 ViewData 字典中的值,如下面的清单所示。

Listing 17.5 Setting ViewData values using an attribute
示例 17.5 使用属性设置 ViewData 值

public class IndexModel: PageModel
{
    [ViewData]                        #A
    public string Title { get; set; }

    public void OnGet()
    {
        Title = "Home Page";             #B
        ViewData["Subtitle"] = "Welcome";     #C
    }
}

You can display the values in the template in the same way as before:
您可以像以前一样在模板中显示值:

<h1>@ViewData["Title"]</h3>
<h2>@ViewData["Subtitle"]</h3>

Tip I don’t find the [ViewData] attribute especially useful, but it’s another feature to look out for. Instead, I create a set of global, static constants for any ViewData keys, and I reference those instead of typing "Title" repeatedly. You’ll get IntelliSense for the values, they’re refactor-safe, and you’ll avoid hard-to-spot typos.
提示:我不觉得 [ViewData] 属性特别有用,但它是另一个需要注意的功能。相反,我为任何 ViewData 键创建一组全局静态常量,并引用这些常量,而不是重复键入“Title”。您将获得值的 IntelliSense,它们是重构安全的,并且您将避免难以发现的拼写错误。

As I mentioned previously, there are mechanisms besides PageModel properties and ViewData that you can use to pass data around, but these two are the only ones I use personally, as you can do everything you need with them. As a reminder, always use PageModel properties where possible, as you benefit from strong typing and IntelliSense. Only fall back to ViewData for values that need to be accessed outside of your Razor view.
正如我前面提到的,除了 PageModel 属性和 ViewData 之外,还有一些机制可用于传递数据,但这两种机制是我个人唯一使用的机制,因为您可以使用它们执行任何需要的作。提醒一下,请尽可能使用 PageModel 属性,因为强类型化和 IntelliSense 会让您受益。对于需要在 Razor 视图之外访问的值,请仅回退到 ViewData。

You’ve had a small taste of the power available to you in Razor templates, but in the next section we’ll dive a little deeper into some of the available C# capabilities.
您已经对 Razor 模板中可用的功能有了一些了解,但在下一节中,我们将更深入地介绍一些可用的 C# 功能。

17.3 Creating dynamic web pages with Razor

17.3 使用 Razor 创建动态网页

You might be glad to know that pretty much anything you can do in C# is possible in Razor syntax. Under the covers, the .cshtml files are compiled into normal C# code (with string for the raw HTML sections), so whatever weird and wonderful behavior you need can be created!
您可能很高兴地知道,在 C# 中可以执行的几乎任何事情都可以在 Razor 语法中完成。在后台,.cshtml 文件被编译成普通的 C# 代码(原始 HTML 部分带有字符串),因此你可以创建你需要的任何奇怪而美妙的行为!

Having said that, just because you can do something doesn’t mean you should. You’ll find it much easier to work with, and maintain, your files if you keep them as simple as possible. This is true of pretty much all programming, but I find it to be especially so with Razor templates.
话虽如此,仅仅因为您可以做某事并不意味着您应该这样做。您会发现,如果您尽可能简化文件,那么处理和维护文件会容易得多。几乎所有编程都是如此,但我发现 Razor 模板尤其如此。

This section covers some of the more common C# constructs you can use. If you find you need to achieve something a bit more exotic, refer to the Razor syntax documentation at http://mng.bz/8rMw.
本部分介绍一些可以使用的更常见的 C# 构造。如果您发现需要实现一些更奇特的东西,请参阅 http://mng.bz/8rMw 上的 Razor 语法文档。

17.3.1 Using C# in Razor templates

17.3.1 在 Razor 模板中使用 C#

One of the most common requirements when working with Razor templates is to render a value you’ve calculated in C# to the HTML. For example, you might want to print the current year to use with a copyright statement in your HTML, to give this result:
使用 Razor 模板时,最常见的要求之一是将您在 C# 中计算的值呈现到 HTML。例如,您可能希望打印当前年份以与 HTML 中的版权声明一起使用,以得到以下结果:

<p>Copyright 2022 ©</p>

Or you might want to print the result of a calculation:
或者您可能希望打印计算结果:

<p>The sum of 1 and 2 is <i>3</i><p>

You can do this in two ways, depending on the exact C# code you need to execute. If the code is a single statement, you can use the @ symbol to indicate you want to write the result to the HTML output, as shown in figure 17.4. You’ve already seen this used to write out values from the PageModel or from ViewData.
您可以通过两种方式执行此作,具体取决于您需要执行的确切 C# 代码。如果代码是单个语句,则可以使用 @ 符号来指示要将结果写入 HTML 输出,如图 17.4 所示。您已经看到它用于从 PageModel 或 ViewData 中写出值。

alt text

Figure 17.4 Writing the result of a C# expression to HTML. The @ symbol indicates where the C# code begins, and the expression ends at the end of the statement, in this case at the space.
图 17.4 将 C# 表达式的结果写入 HTML。@ 符号指示 C# 代码的开始位置,表达式在语句的末尾结束,在本例中在空格处。

If the C# you want to execute is something that needs a space, you need to use parentheses to demarcate the C#, as shown in figure 17.5.
如果要执行的 C# 需要空格,则需要使用括号来分隔 C#,如图 17.5 所示。

alt text

Figure 17.5 When a C# expression contains whitespace, you must wrap it in parentheses using @() so the Razor engine knows where the C# stops and HTML begins.
图 17.5 当 C# 表达式包含空格时,必须使用 @() 将其括在括号中,以便 Razor 引擎知道 C# 停止和 HTML 开始的位置。

These two approaches, in which C# is evaluated and written directly to the HTML output, are called Razor expressions.
这两种方法(其中 C# 被计算并直接写入 HTML 输出)称为 Razor 表达式。

Tip If you want to write a literal @ character rather than a C# expression, use a second @ character: @@.
提示:如果要编写文本 @ 字符而不是 C# 表达式,请使用第二个 @ 字符:@@。

Sometimes you’ll want to execute some C#, but you don’t need to output the values. We used this technique when we were setting values in ViewData:
有时,您需要执行一些 C#,但不需要输出值。我们在 ViewData 中设置值时使用了这种技术:

@{
    ViewData["Title"] = "Home Page";
}

This example demonstrates a Razor code block, which is normal C# code, identified by the @{} structure. Nothing is written to the HTML output here; it’s all compiled as though you’d written it in any other normal C# file.
此示例演示 Razor 代码块,这是由 @{} 结构标识的普通 C# 代码。此处的 HTML 输出不会写入任何内容;它全部编译,就像您用任何其他普通的 C# 文件编写它一样。

Tip When you execute code within code blocks, it must be valid C#, so you need to add semicolons. Conversely, when you’re writing values directly to the response using Razor expressions, you don’t need them. If your output HTML breaks unexpectedly, keep an eye out for missing or rogue extra semicolons.
提示:在代码块中执行代码时,它必须是有效的 C#,因此需要添加分号。相反,当您使用 Razor 表达式将值直接写入响应时,您不需要它们。如果输出 HTML 意外中断,请留意缺失或流氓的额外分号。

Razor expressions are one of the most common ways of writing data from your PageModel to the HTML output. You’ll see the other approach, using Tag Helpers, in the next chapter. Razor’s capabilities extend far further than this, however, as you’ll see in section 17.3.2, where you’ll learn how to include traditional C# structures in your templates.
Razor 表达式是将数据从 PageModel 写入 HTML 输出的最常用方法之一。您将在下一章中看到另一种方法,即使用 Tag Helpers。但是,Razor 的功能远不止于此,如第 17.3.2 节所示,您将在其中学习如何在模板中包含传统的 C# 结构。

17.3.2 Adding loops and conditionals to Razor templates

17.3.2 向 Razor 模板添加循环和条件语句

One of the biggest advantages of using Razor templates over static HTML is the ability to generate the output dynamically. Being able to write values from your PageModel to the HTML using Razor expressions is a key part of that, but another common use is loops and conditionals. With these, you can hide sections of the UI, or produce HTML for every item in a list, for example.
与静态 HTML 相比,使用 Razor 模板的最大优势之一是能够动态生成输出。能够使用 Razor 表达式将值从 PageModel 写入 HTML 是其中的关键部分,但另一个常见用途是循环和条件。例如,您可以使用这些功能隐藏 UI 的各个部分,或者为列表中的每个项目生成 HTML。

Loops and conditionals include constructs such as if and for loops. Using them in Razor templates is almost identical to C#, but you need to prefix their usage with the @ symbol. In case you’re not getting the hang of Razor yet, when in doubt, throw in another @!
循环和条件包括诸如 if 和 for 循环之类的结构。在 Razor 模板中使用它们与 C# 几乎相同,但您需要在它们的用法前面加上 @ 符号。如果您还没有掌握 Razor 的窍门,如有疑问,请再输入一个 @!

One of the big advantages of Razor in the context of ASP.NET Core is that it uses languages you’re already familiar with: C# and HTML. There’s no need to learn a whole new set of primitives for some other templating language: it’s the same if, foreach, and while constructs you already know. And when you don’t need them, you’re writing raw HTML, so you can see exactly what the user is getting in their browser.
Razor 在 ASP.NET Core 上下文中的一大优势是它使用您已经熟悉的语言:C# 和 HTML。没有必要为其他模板语言学习一整套新的原语:它与你已经知道的 if、foreach 和 while 结构相同。当您不需要它们时,您正在编写原始 HTML,因此您可以准确地看到用户在浏览器中获得的内容。

In listing 17.6, I’ve applied a few of these techniques in a template to display a to-do item. The PageModel has a bool IsComplete property, as well as a List property called Tasks, which contains any outstanding tasks.
在列表 17.6 中,我在模板中应用了一些技术来显示待办事项。PageModel 具有一个 bool IsComplete 属性,以及一个名为 Tasks 的 List 属性,其中包含任何未完成的任务。

Listing 17.6 Razor template for rendering a ToDoItemViewModel
列表 17.6 用于呈现 ToDoItemViewModel 的 Razor 模板

@page
@model ToDoItemModel ❶
<div>
@if (Model.IsComplete)
{ ❷
<strong>Well done, you’re all done!</strong> ❷
} ❷
else
{
<strong>The following tasks remain:</strong>
<ul>
@foreach (var task in Model.Tasks) ❸
{
<li>@task</li> ❹
}
</ul>
}
</div>

❶ The @model directive indicates the type of PageModel in Model.
@model 指令指示 Model 中 PageModel 的类型。
❷ The if control structure checks the value of the PageModel’s IsComplete property at runtime.
if 控件结构在运行时检查 PageModel 的 IsComplete 属性的值。
❸ The foreach structure will generate the <li> elements once for each task in Model.Tasks.
foreach 结构体将生成<li>元素。
❹ A Razor expression is used to write the task to the HTML output.
Razor 表达式用于将任务写入 HTML 输出。

This code definitely lives up to the promise of mixing C# and HTML! There are traditional C# control structures, such as if and foreach, that you’d expect in any normal C# program, interspersed with the HTML markup that you want to send to the browser. As you can see, the @ symbol is used to indicate when you’re starting a control statement, but you generally let the Razor template infer when you’re switching back and forth between HTML and C#.
这段代码绝对兑现了混合 C# 和 HTML 的承诺!在任何普通 C# 程序中,都有您期望使用的传统 C# 控制结构,例如 if 和 foreach,其中穿插着要发送到浏览器的 HTML 标记。如你所见,@ 符号用于指示何时启动控制语句,但你通常会让 Razor 模板推断你在 HTML 和 C# 之间来回切换。

The template shows how to generate dynamic HTML at runtime, depending on the exact data provided. If the model has outstanding Tasks, the HTML generates a list item for each task, producing output something like that shown in figure 17.6.
该模板演示如何在运行时生成动态 HTML,具体取决于提供的确切数据。如果模型有未完成的任务,HTML 会为每个任务生成一个列表项,产生如图 17.6 所示的输出。

alt text

Figure 17.6 The Razor template generates a <li> item for each remaining task, depending on the data passed to the view at runtime. You can use an if block to render completely different HTML depending on the values in your model.

图 17.6 Razor 模板会生成一个<li> item 的 SET 任务,具体取决于在运行时传递给视图的数据。您可以使用 if 块根据模型中的值呈现完全不同的 HTML。

IntelliSense and tooling support
IntelliSense 和工具支持

The mixture of C# and HTML might seem hard to read in the book, and that’s a reasonable complaint. It’s also another valid argument for trying to keep your Razor templates as simple as possible.
C# 和 HTML 的混合在书中似乎很难阅读,这是一个合理的抱怨。这也是尝试使 Razor 模板尽可能简单的另一个有效论点。

Luckily, if you’re using an editor like Visual Studio or Visual Studio Code, the tooling can help somewhat. As you can see in this figure, Visual Studio highlights the transition between the C# portions of the code and the surrounding HTML, though this is less pronounced in recent versions of Visual Studio.
幸运的是,如果您使用的是 Visual Studio 或 Visual Studio Code 等编辑器,这些工具可能会有所帮助。正如您在此图中所看到的,Visual Studio 突出显示了代码的 C# 部分与周围 HTML 之间的转换,尽管这在最新版本的 Visual Studio 中不太明显。

alt text

Visual Studio highlights the @ symbols where C# transitions to HTML and uses C# syntax coloring for C# code. This makes the Razor templates somewhat easier to read that than the pure plain text.
Visual Studio 突出显示 C# 转换为 HTML 的 @ 符号,并对 C# 代码使用 C# 语法着色。这使得 Razor 模板比纯文本更容易阅读。

Although the ability to use loops and conditionals is powerful—they’re one of the advantages of Razor over static HTML—they also add to the complexity of your view. Try to limit the amount of logic in your views to make them as easy to understand and maintain as possible.
尽管使用循环和条件的功能非常强大(它们是 Razor 相对于静态 HTML 的优势之一),但它们也增加了视图的复杂性。尝试限制视图中的逻辑数量,使其尽可能易于理解和维护。

A common trope of the ASP.NET Core team is that they try to ensure you “fall into the pit of success” when building an application. This refers to the idea that by default, the easiest way to do something should be the correct way of doing it. This is a great philosophy, as it means you shouldn’t get burned by, for example, security problems if you follow the standard approaches. Occasionally, however, you may need to step beyond the safety rails; a common use case is when you need to render some HTML contained in a C# object to the output, as you’ll see in the next section.
ASP.NET Core 团队的一个常见比喻是,他们试图确保您在构建应用程序时 “掉进成功的坑”。这指的是默认情况下,执行某项作的最简单方法应该是正确的执行方式。这是一个很棒的理念,因为它意味着如果您遵循标准方法,您就不应该被安全问题等问题所困扰。但是,有时您可能需要跨出安全栏杆;一个常见的用例是当您需要将 C# 对象中包含的一些 HTML 渲染到输出时,您将在下一节中看到。

17.3.3 Rendering HTML with Raw

17.3.3 使用 Raw 渲染 HTML

In the previous example, we rendered the list of tasks to HTML by writing the string task using the @task Razor expression. But what if the task variable contains HTML you want to display, so instead of "Check oil" it contains "<strong>Check oil</strong>"? If you use a Razor expression to output this as you did previously, you might hope to get this:
在前面的示例中,我们通过使用 @task Razor 表达式编写字符串 task 将任务列表呈现为 HTML。但是,如果任务变量包含要显示的 HTML,那么它不是“Check oil”,而是"<strong>Check oil</strong>",该怎么办?如果您像以前一样使用 Razor 表达式来输出此表达式,您可能希望得到以下内容:

<li><strong>Check oil</strong></li>

But that’s not the case. The HTML generated comes out like this:
但事实并非如此。生成的 HTML 如下所示:

<li><strong>Check oil</strong></li>

Hmm, looks odd, right? What’s happened here? Why did the template not write your variable to the HTML, like it has in previous examples? If you look at how a browser displays this HTML, like in figure 17.7, I hope that it makes more sense.
嗯,看起来很奇怪,对吧?这里发生了什么?为什么模板没有像前面的示例那样将变量写入 HTML?如果您查看浏览器如何显示此 HTML,如图 17.7 所示,我希望它更有意义。

alt text

Figure 17.7 The second item, "<strong>Check oil</strong>" has been HTML-encoded, so the <strong> elements are visible to the user as part of the task. This prevents any security problems, as users can’t inject malicious scripts into your HTML.
图 17.7 第二项 "<strong>Check oil</strong>" 已经过 HTML 编码,因此 <strong> 元素作为任务的一部分对用户可见。这可以防止任何安全问题,因为用户无法将恶意脚本注入您的 HTML。

Razor templates HTML-encode C# expressions before they’re written to the output stream. This is primarily for security reasons; writing out arbitrary strings to your HTML could allow users to inject malicious data and JavaScript into your website. Consequently, the C# variables you print in your Razor template get written as HTML-encoded values.
Razor 模板在将 C# 表达式写入输出流之前对其进行 HTML 编码。这主要是出于安全原因;将任意字符串写出到 HTML 中可能会允许用户将恶意数据和 JavaScript 注入您的网站。因此,在 Razor 模板中打印的 C# 变量将写入 HTML 编码的值。

NOTE Razor also renders non-ASCII Unicode characters, such as ó and è, as HTML entities: ó and è. You can customize this behavior using WebEncoderOptions in Program.cs, as in this example: builder.Services.Configure<WebEncoderOptions>(o => o.AllowCharacter('ó')) .

注意:Razor 还将非 ASCII Unicode 字符(如 ó 和 è)呈现为 HTML 实体:ó 和 è。您可以使用 Program.cs 中的 WebEncoderOptions 自定义此行为,如以下示例所示:。 `builder.Services.Configure(o => o.AllowCharacter('ó'))

In some cases, you might need to directly write out HTML contained in a string to the response. If you find yourself in this situation, first, stop. Do you really need to do this? If the values you’re writing have been entered by a user, or were created based on values provided by users, there’s a serious risk of creating a security hole in your website.
在某些情况下,您可能需要直接将字符串中包含的 HTML 写出到响应中。如果您发现自己处于这种情况,请先停止。您真的需要这样做吗?如果您编写的值是由用户输入的,或者是根据用户提供的值创建的,则存在在您的网站中创建安全漏洞的严重风险。

If you really need to write the variable out to the HTML stream, you can do so using the Html property on the view page and calling the Raw method:
如果您确实需要将变量写出到 HTML 流中,则可以使用视图页面上的 Html 属性并调用 Raw 方法来实现:

<li>@Html.Raw(task)</li>

With this approach, the string in task is directly written to the output stream, without encoding, producing the HTML you originally wanted, <li><strong>Check oil</strong></li>, which renders as shown in figure 17.8.
使用这种方法,task 中的字符串被直接写入输出流,无需编码,生成你最初想要的 HTML <li><strong>Check oil</strong></li>,如图 17.8 所示。

alt text

Figure 17.8 The second item, "<strong>Check oil<strong>" has been output using Html.Raw(), so it hasn’t been HTML-encoded. The <strong> elements result in the second item being shown in bold instead. Using Html.Raw() in this way should be avoided where possible, as it is a security risk.
图 17.8 第二项是使用 Html.Raw() 输出的 "<strong>Check oil<strong>" ,因此尚未进行 HTML 编码。这些 <strong> 元素会导致第二个项目以粗体显示。应尽可能避免以这种方式使用 Html.Raw(),因为这会带来安全风险。

Warning Using Html.Raw on user input creates a security risk that users could use to inject malicious code into your website. Avoid using Html.Raw if possible.
警告:在用户输入上使用 Html.Raw 会带来安全风险,用户可能会利用该风险将恶意代码注入您的网站。如果可能,请避免使用 Html.Raw。

The C# constructs shown in this section can be useful, but they can make your templates harder to read. It’s generally easier to understand the intention of Razor templates that are predominantly HTML markup rather than C#.
本节中所示的 C# 构造可能很有用,但它们可能会使模板更难阅读。通常更容易理解主要是 HTML 标记而不是 C# 的 Razor 模板的意图。

In the previous version of ASP.NET, these constructs, and in particular the Html helper property, were the standard way to generate dynamic markup. You can still use this approach in ASP.NET Core by using the various HtmlHelper methods on the Html property, but these have largely been superseded by a cleaner technique: Tag Helpers.
在早期版本的 ASP.NET 中,这些构造(特别是 Html 帮助程序属性)是生成动态标记的标准方法。您仍然可以在 ASP.NET Core 中通过使用 Html 属性上的各种 HtmlHelper 方法,但这些方法在很大程度上已被一种更简洁的技术所取代:Tag Helpers。

NOTE I discuss Tag Helpers and how to use them to build HTML forms in chapter 18. HtmlHelper is essentially obsolete, though it’s still available if you prefer to use it.
注意:我在第 18 章中讨论了 Tag Helpers 以及如何使用它们来构建 HTML 表单。HtmlHelper 基本上已过时,但如果您愿意使用它,它仍然可用。

Tag Helpers are a useful feature that’s new to Razor in ASP.NET Core, but many other features have been carried through from the legacy (.NET Framework) ASP.NET. In the next section of this chapter, you’ll see how you can create nested Razor templates and use partial views to reduce the amount of duplication in your views.
标记帮助程序是 ASP.NET Core 中 Razor 新增的一项有用功能,但许多其他功能已从旧版 (.NET Framework) ASP.NET 中继承而来。在本章的下一部分中,您将了解如何创建嵌套的 Razor 模板并使用分部视图来减少视图中的重复数量。

17.4 Layouts, partial views, and _ViewStart

17.4 布局、分部视图和_ViewStart

In this section you’ll learn about layouts and partial views, which allow you to extract common code to reduce duplication. These files make it easier to make changes to your HTML that affect multiple pages at once. You’ll also learn how to run common code for every Razor Page using _ViewStart and _ViewImports, and how to include optional sections in your pages.
在本节中,您将了解布局和分部视图,它们允许您提取通用代码以减少重复。通过这些文件,可以更轻松地对 HTML 进行一次影响多个页面的更改。您还将了解如何使用 _ViewStart 和 _ViewImports 为每个 Razor 页面运行通用代码,以及如何在页面中包含可选部分。

Every HTML document has a certain number of elements that are required: <html>, <head>, and <body>. As well, there are often common sections that are repeated on every page of your application, such as the header and footer, as shown in figure 17.9. Also, each page in your application will probably reference the same CSS and JavaScript files.
每个 HTML 文档都有一定数量的必需元素:<html>, <head><body>.此外,在应用程序的每个页面上通常都有重复的常见部分,例如 header 和 footer,如图 17.9 所示。此外,应用程序中的每个页面都可能引用相同的 CSS 和 JavaScript 文件。

alt text

Figure 17.9 A typical web application has a block-based layout, where some blocks are common to every page of your application. The header block will likely be identical across your whole application, but the sidebar may be identical only for the pages in one section. The body content will differ for every page in your application.
图 17.9 典型的 Web 应用程序具有基于块的布局,其中某些块对于应用程序的每个页面都是通用的。标题块在整个应用程序中可能相同,但侧边栏可能仅对一个部分中的页面相同。应用程序中每个页面的正文内容都不同。

All these different elements add up to a maintenance nightmare. If you had to include these manually in every view, making any changes would be a laborious, error-prone process involving editing every page. Instead, Razor lets you extract these common elements into layouts.
所有这些不同的因素加起来就是一场维护噩梦。如果您必须在每个视图中手动包含这些内容,则进行任何更改都将是一个费力且容易出错的过程,涉及编辑每个页面。相反,Razor 允许您将这些常见元素提取到布局中。

DEFINITION A layout in Razor is a template that includes common code. It can’t be rendered directly, but it can be rendered in conjunction with normal Razor views.
定义:Razor 中的布局是包含通用代码的模板。它不能直接呈现,但可以与普通 Razor 视图一起呈现。

By extracting your common markup into layouts, you can reduce the duplication in your app. This makes changes easier, makes your views easier to manage and maintain, and is generally good practice!
通过将通用标记提取到布局中,您可以减少应用程序中的重复。这使得更改更容易,使您的视图更易于管理和维护,并且通常是很好的做法!

17.4.1 Using layouts for shared markup

17.4.1 将布局用于共享标记

Layout files are, for the most part, normal Razor templates that contain markup common to more than one page. An ASP.NET Core app can have multiple layouts, and layouts can reference other layouts. A common use for this is to have different layouts for different sections of your application. For example, an e-commerce website might use a three-column view for most pages but a single-column layout when you come to the checkout pages, as shown in figure 17.10.
布局文件在大多数情况下是普通的 Razor 模板,其中包含多个页面通用的标记。ASP.NET Core 应用程序可以有多个布局,并且布局可以引用其他布局。这样做的一个常见用途是为应用程序的不同部分使用不同的布局。例如,电子商务网站可能在大多数页面中使用三列视图,但在您进入结帐页面时使用单列布局,如图 17.10 所示。

alt text

Figure 17.10 The https://manning.com website uses different layouts for different parts of the web application. The product pages use a three-column layout, but the cart page uses a single-column layout.
图 17.10 https://manning.com 网站对 Web 应用程序的不同部分使用不同的布局。产品页面使用三列布局,但购物车页面使用单列布局。

You’ll often use layouts across many different Razor Pages, so they’re typically placed in the Pages/Shared folder. You can name them anything you like, but there’s a common convention to use _Layout.cshtml as the filename for the base layout in your application. This is the default name used by the Razor Page templates in Visual Studio and the .NET CLI.
您经常在许多不同的 Razor Pages 中使用布局,因此它们通常位于 Pages/Shared 文件夹中。你可以为它们命名任何你喜欢的名字,但有一个常见的约定,即使用 _Layout.cshtml 作为应用程序中基本布局的文件名。这是 Visual Studio 和 .NET CLI 中的 Razor 页面模板使用的默认名称。

Tip A common convention is to prefix your layout files with an underscore (_) to distinguish them from standard Razor templates in your Pages folder. Placing them in Pages/Shared means you can refer to them by the short name, such as "Layout", without having to specify the full path to the layout file.
提示:一个常见的约定是在布局文件前面加上下划线 (
),以将它们与 Pages 文件夹中的标准 Razor 模板区分开来。将它们放在 Pages/Shared 中意味着您可以通过短名称(如“_Layout”)来引用它们,而不必指定布局文件的完整路径。

A layout file looks similar to a normal Razor template, with one exception: every layout must call the @RenderBody() function. This tells the templating engine where to insert the content from the child views. A simple layout is shown in listing 17.7. Typically, your application references all your CSS and JavaScript files in the layout and includes all the common elements, such as headers and footers, but this example includes pretty much the bare minimum HTML.
布局文件看起来类似于普通的 Razor 模板,但有一个例外:每个布局都必须调用 @RenderBody() 函数。这会告诉模板引擎将子视图中的内容插入到何处。一个简单的布局如清单 17.7 所示。通常,您的应用程序会引用布局中的所有 CSS 和 JavaScript 文件,并包含所有常见元素,例如页眉和页脚,但此示例包含的 HTML 几乎是最低限度的。

Listing 17.7 A basic _Layout.cshtml file calling RenderBody
清单 17.7 一个调用 RenderBody 的基本 _Layout.cshtml 文件

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>@ViewData["Title"]</title> ❶
<link rel="stylesheet" href="~/css/site.css" /> ❷
</head>
<body>
@RenderBody() ❸
</body>
</html>

❶ ViewData is the standard mechanism for passing data to a layout from a view.
ViewData 是从视图向布局传递数据的标准机制。
❷ Elements common to every page, such as your CSS, are typically found in the layout.
每个页面通用的元素(例如 CSS)通常位于布局中。
❸ Tells the templating engine where to insert the child view’s content
告诉模板引擎在何处插入子视图的内容

As you can see, the layout file includes the required elements, such as <html> and <head>, as well as elements you need on every page, such as <title> and <link>. This example also shows the benefit of storing the page title in ViewData; the layout can render it in the <title> element so that it shows in the browser’s tab, as shown in figure 17.11.
如您所见,布局文件包括所需的元素,如 <html> <head>,以及每个页面上所需的元素,如 <title> <link>。此示例还显示了在 ViewData 中存储页面标题的好处;布局可以在<title> 元素中渲染它,使其显示在浏览器的选项卡中,如图 17.11 所示。

alt text

Figure 17.11 The content of the <title> element is used to name the tab in the user’s browser, in this case Home Page.
图 17.11 <title>元素的内容用于命名用户浏览器中的选项卡,在本例中为 Home Page。

NOTE Layout files are not standalone Razor Pages and do not take part in routing, so they do not start with the @page directive.
注意:布局文件不是独立的 Razor 页面,不参与路由,因此它们不以 @page 指令开头。

Views can specify a layout file to use by setting the Layout property inside a Razor code block, as shown in the following listing.
视图可以通过在 Razor 代码块中设置 Layout 属性来指定要使用的布局文件,如下面的清单所示。

Listing 17.8 Setting the Layout property from a view
示例 17.8 从视图设置 Layout 属性

@{
Layout = "_Layout"; ❶
ViewData["Title"] = "Home Page"; ❷
}
<h1>@ViewData["Title"]</h1> ❸
<p>This is the home page</p> ❸

❶ Sets the layout for the page to _Layout.cshtml
将页面的布局设置为 _Layout.cshtml
❷ ViewData is a convenient way of passing data from a Razor view to the layout.
ViewData 是将数据从 Razor 视图传递到布局的便捷方法。
❸ The content in the Razor view to render inside the layout
要在布局内呈现的 Razor 视图中的内容

Any contents in the view are be rendered inside the layout, where the call to @RenderBody() occurs. Combining the two previous listings generates the following HTML.
视图中的任何内容都将在布局中呈现,其中会调用 @RenderBody()。将前面的两个列表组合在一起将生成以下 HTML。

Listing 17.9 Rendered output from combining a view with its layout
列表 17.9 将视图与其布局组合在一起的渲染输出

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Home Page</title> ❶
<link rel="stylesheet" href="/css/site.css" />
</head>
<body>
<h1>Home Page</h1> ❷
<p>This is the home page</p> ❷
</body>
<html>

❶ ViewData set in the view is used to render the layout.
ViewData 中设置的view 用于渲染布局。
❷ The RenderBody call renders the contents of the view.
RenderBody 调用渲染视图的内容。

Judicious use of layouts can be extremely useful in reducing the duplication between pages. By default, layouts provide only a single location where you can render content from the view, at the call to @RenderBody. In cases where this is too restrictive, you can render content using sections.
明智地使用布局对于减少页面之间的重复非常有用。默认情况下,布局仅提供一个位置,您可以在调用 @RenderBody时从视图中呈现内容。如果这过于严格,您可以使用部分来呈现内容。

17.4.2 Overriding parent layouts using sections

使用部分覆盖父布局

A common requirement when you start using multiple layouts in your application is to be able to render content from child views in more than one place in your layout. Consider the case of a layout that uses two columns. The view needs a mechanism for saying “render this content in the left column” and “render this other content in the right column.” This is achieved using sections.
当您在应用程序中开始使用多个布局时,一个常见的要求是能够在布局中的多个位置呈现子视图中的内容。考虑使用两列的布局的情况。视图需要一种机制来表示 “render this content in the left column” 和 “render this other content in the right column”这是使用部分实现的。

NOTE Remember, all the features outlined in this chapter are specific to Razor, which is a server-side rendering engine. If you’re using a client-side single-page application (SPA) framework to build your application, you’ll likely handle these requirements in other ways, within the client.
注意:请记住,本章中概述的所有功能都是特定于 Razor 的,Razor 是一个服务器端渲染引擎。如果您使用客户端单页应用程序 (SPA) 框架来构建应用程序,则可能会在客户端内以其他方式处理这些要求。

Sections provide a way of organizing where view elements should be placed within a layout. They’re defined in the view using an @section definition, as shown in the following listing, which defines the HTML content for a sidebar separate from the main content, in a section called Sidebar. The @section can be placed anywhere in the file, top or bottom, wherever is convenient.
Sections 提供了一种组织视图元素在布局中应放置的位置的方法。它们是在视图中使用 @section 定义定义的,如下面的清单所示,该清单在名为 Sidebar 的部分中定义与主要内容分开的侧边栏的 HTML 内容。@section可以放置在文件中的任何位置,顶部或底部,只要方便即可。

Listing 17.10 Defining a section in a view template
清单 17.10 在视图模板中定义部分

@{
    Layout = "_TwoColumn";
}
@section Sidebar {                         #A
    <p>This is the sidebar content</p>     #A
}                                          #A
<p>This is the main content </p>     #B

❶ All content inside the braces is part of the Sidebar section, not the main body content.
大括号内的所有内容都是 Sidebar 部分的一部分,而不是主体内容。
❷ Any content not inside an @section will be rendered by the @RenderBody call.
不在 @section 中的任何内容都将由 @RenderBody 调用呈现。

The section is rendered in the parent layout with a call to @RenderSection(). This renders the content contained in the child section into the layout. Sections can be either required or optional. If they’re required, a view must declare the given @section; if they’re optional, they can be omitted, and the layout will skip them. Skipped sections won’t appear in the rendered HTML. The following listing shows a layout that has a required section called Sidebar and an optional section called Scripts.
该部分通过调用 @RenderSection() 在父布局中呈现。这会将子部分中包含的内容呈现到布局中。部分可以是必需的,也可以是可选的。如果需要,视图必须声明给定的@section;如果它们是可选的,则可以省略它们,布局将跳过它们。跳过的部分不会显示在呈现的 HTML 中。下面的清单显示了一个布局,该布局具有一个名为 Sidebar 的必需部分和一个名为 Scripts 的可选部分。

Listing 17.11 Rendering a section in a layout file, _TwoColumn.cshtml
清单 17.11 在布局文件中渲染部分 _TwoColumn.cshtml

@{
    Layout = "_Layout";     #A
}
<div class="main-content">
    @RenderBody()          #B
</div>
<div class="side-bar">
    @RenderSection("Sidebar", required: true)     #C
</div>
@RenderSection("Scripts", required: false)    #D

❶ This layout is nested inside a layout itself.
此布局嵌套在布局本身内。
❷ Renders all the content from a view that isn’t part of a section
从不属于某个部分的视图中呈现所有内容
❸ Renders the Sidebar section; if the Sidebar section isn’t defined in the view, throws an error
呈现侧边栏部分;如果视图中未定义 Sidebar 部分,则抛出错误
❹ Renders the Scripts section; if the Scripts section isn’t defined in the view, ignores it
呈现 Scripts 部分;如果 Scripts 部分未在视图中定义,则忽略它

Tip It’s common to have an optional section called Scripts in your layout pages. This can be used to render additional JavaScript that’s required by some views but isn’t needed on every view. A common example is the jQuery Unobtrusive Validation scripts for client-side validation. If a view requires the scripts, it adds the appropriate @section Scripts to the Razor markup.
提示:布局页面中通常有一个名为 Scripts 的可选部分。这可用于呈现某些视图需要但并非每个视图都需要的其他 JavaScript。一个常见的示例是用于客户端验证的 jQuery Unobtrusive Validation 脚本。如果视图需要脚本,它会将相应的 @section 脚本添加到 Razor 标记。

You may notice that the previous listing defines a Layout property, even though it’s a layout itself, not a view. This is perfectly acceptable and lets you create nested hierarchies of layouts, as shown in figure 17.12.
您可能会注意到,前面的清单定义了一个 Layout 属性,即使它本身是一个布局,而不是一个视图。这是完全可以接受的,并且允许您创建布局的嵌套层次结构,如图 17.12 所示。

alt text

Figure 17.12 Multiple layouts can be nested to create complex hierarchies. This allows you to keep the elements common to all views in your base layout and extract layout common to multiple views into sub-layouts.
图 17.12 可以嵌套多个布局以创建复杂的层次结构。这样,您就可以保持基本布局中所有视图通用的元素,并将多个视图通用的布局提取到子布局中。

Tip Most websites these days need to be responsive, so they work on a wide variety of devices. You generally shouldn’t use layouts for this. Don’t serve different layouts for a single page based on the device making the request. Instead, serve the same HTML to all devices, and use CSS on the client side to adapt the display of your web page as required.
提示:如今,大多数网站都需要响应式,因此它们可以在各种设备上运行。通常,您不应该为此使用布局。不要根据发出请求的设备为单个页面提供不同的布局。相反,应向所有设备提供相同的 HTML,并在客户端使用 CSS 来根据需要调整网页的显示。

As well as the simple optional/required flags for sections, Razor Pages have several other messages that you can use for flow control in your layout pages:
除了部分的简单可选/必需标志外,Razor Pages 还有其他几条消息可用于布局页面中的流控制:

• IsSectionDefined(string section)—Returns true if a Razor Page has defined the named section.
IsSectionDefined(string section) - 如果 Razor 页面已定义命名部分,则返回 true。

• IgnoreSection(string section)—Ignores an unrendered section. If a section is defined in a page but not rendered, the Razor Page throws an exception unless the section is ignored.
IgnoreSection(string section) - 忽略未渲染的部分。如果在页面中定义了某个部分但未呈现,则 Razor Page 会引发异常,除非忽略该部分。

• IgnoreBody()—Ignores the unrendered body of the Razor Page. Layouts must call either RenderBody() or IgnoreBody(); otherwise, they will throw an InvalidOperationException.
IgnoreBody() - 忽略 Razor Page 的未渲染主体。布局必须调用 RenderBody() 或 IgnoreBody();否则,它们将引发 InvalidOperationException。

Layout files and sections provide a lot of flexibility for building sophisticated UIs, but one of their most important uses is in reducing the duplication of code in your application. They’re perfect for avoiding duplication of content that you’d need to write for every view. But what about those times when you find you want to reuse part of a view somewhere else? For those cases, you have partial views.
布局文件和部分为构建复杂的 UI 提供了很大的灵活性,但它们最重要的用途之一是减少应用程序中的代码重复。它们非常适合避免您需要为每个视图编写的内容重复。但是,当您发现想要在其他地方重用视图的一部分时,该怎么办?对于这些情况,您有 partial views。

17.4.3 Using partial views to encapsulate markup

17.4.3 使用分部视图封装标记

Partial views are exactly what they sound like: part of a view. They provide a means of breaking up a larger view into smaller, reusable chunks. This can be useful for both reducing the complexity in a large view by splitting it into multiple partial views or for allowing you to reuse part of a view inside another.
部分视图正是它们听起来的样子:视图的一部分。它们提供了一种将较大的视图分解为更小的、可重用的块的方法。这既可以通过将大型视图拆分为多个部分视图来降低大型视图的复杂性,也可以允许您在另一个视图中重用某个视图的一部分。

Most web frameworks that use server-side rendering have this capability. Ruby on Rails has partial views, Django has inclusion tags, and Zend has partials. These all work in the same way, extracting common code into small, reusable templates. Even client-side templating engines such as Mustache and Handlebars, used by client-side frameworks like Angular and Ember, have similar “partial view” concepts.
大多数使用服务器端渲染的 Web 框架都具有此功能。Ruby on Rails 有 partial 视图,Django 有 inclusion 标签,而 Zend 有 partials。这些都以相同的方式工作,将通用代码提取到小型、可重用的模板中。即使是 Angular 和 Ember 等客户端框架使用的客户端模板引擎(如 Mustache 和 Handlebars)也具有类似的“部分视图”概念。

Consider a to-do list application again. You might find you have a Razor Page called ViewToDo.cshtml that displays a single to-do with a given id. Later, you create a new Razor Page, RecentToDos.cshtml, that displays the five most recent to-do items. Instead of copying and pasting the code from one page to the other, you could create a partial view, called _ToDo.cshtml, as in the following listing.
再次考虑一个待办事项列表应用程序。你可能会发现你有一个名为 ViewToDo.cshtml 的 Razor 页面,它显示具有给定 ID 的单个待办事项。稍后,您将创建一个新的 Razor 页面 RecentToDos.cshtml,该页面显示五个最新的待办事项。您可以创建一个名为 _ToDo.cshtml 的分部视图,而不是将代码从一个页面复制并粘贴到另一个页面,如下面的清单所示。

Listing 17.12 Partial view _ToDo.cshtml for displaying a ToDoItemViewModel
列表 17.12 用于显示 ToDoItemViewModel 的分部视图 _ToDo.cshtml

@model ToDoItemViewModel                                  #A
<h2>@Model.Title</h2>                   #B
<ul>                                    #B
    @foreach (var task in Model.Tasks)  #B
    {                                   #B
        <li>@task</li>                  #B
    }                                   #B
</ul>                                   #B

❶ Partial views can bind to data in the Model property, like a normal Razor Page uses a PageModel.
分部视图可以绑定到 Model 属性中的数据,就像普通的 Razor Page 使用 PageModel 一样。
❷ The content of the partial view, which previously existed in the ViewToDo.cshtml file
分部视图的内容,以前存在于 ViewToDo.cshtml 文件中

Partial views are a bit like Razor Pages without the PageModel and handlers. Partial views are purely about rendering small sections of HTML rather than handling requests, model binding, and validation, and calling the application model. They are great for encapsulating small usable bits of HTML that you need to generate on multiple Razor Pages.
分部视图有点像没有 PageModel 和处理程序的 Razor Pages。分部视图纯粹是关于呈现 HTML 的一小部分,而不是处理请求、模型绑定和验证以及调用应用程序模型。它们非常适合封装您需要在多个 Razor 页面上生成的少量可用 HTML。

Both the ViewToDo.cshtml and RecentToDos.cshtml Razor Pages can render the _ToDo.cshtml partial view, which handles generating the HTML for a single class. Partial views are rendered using the <partial /> Tag Helper, providing the name of the partial view to render and the data (the model) to render. For example, the RecentToDos.cshtml view could achieve this as shown in the following listing.
ViewToDo.cshtml 和 RecentToDos.cshtml Razor 页面都可以呈现 _ToDo.cshtml 分部视图,该视图处理为单个类生成 HTML。部分视图使用<partial /> Tag Helper 进行渲染,提供要渲染的分部视图的名称和要渲染的数据(模型)。例如,RecentToDos.cshtml 视图可以实现此目的,如下面的清单所示。

Listing 17.13 Rendering a partial view from a Razor Page
清单 17.13 从 Razor 页面渲染部分视图

@page                    #A
@model RecentToDoListModel                   #B

@foreach(var todo in Model.RecentItems)     #C
{
    <partial name="_ToDo" model="todo" />   #D
}

❶ This is a Razor Page, so it uses the @page directive. Partial views do not use @page.
这是一个 Razor 页面,因此它使用 @page 指令。分部视图不使用@page。
❷ The PageModel contains the list of recent items to render.
PageModel 包含要渲染的最近项目的列表。
❸ Loops through the recent items. todo is a ToDoItemViewModel, as required by the partial view.
循环浏览最近的项目。todo 是 ToDoItemViewModel,这是分部视图所需的。
❹ Uses the partial tag helper to render the _ToDo partial view, passing in the model to render
使用 partial 标签辅助函数渲染_ToDo 部分视图,传入模型以渲染

When you render a partial view without providing an absolute path or file extension, such as _ToDo in listing 17.13, the framework tries to locate the view by searching the Pages folder, starting from the Razor Page that invoked it. For example, if your Razor Page is located at Pages/Agenda/ToDos/RecentToDos.chstml, the framework would look in the following places for a file called _ToDo.chstml:
当您在不提供绝对路径或文件扩展名的情况下呈现部分视图时(如清单 17.13 中的 _ToDo),框架会尝试通过搜索 Pages 文件夹来查找视图,从调用它的 Razor Page 开始。例如,如果您的 Razor 页面位于 Pages/Agenda/ToDos/RecentToDos.chstml,则框架将在以下位置查找名为 _ToDo.chstml 的文件:

• Pages/Agenda/ToDos/ (the current Razor Page’s folder)
• Pages/Agenda/
• Pages/
• Pages/Shared/
• Views/Shared/

The first location that contains a file called _ToDo.cshtml will be selected. If you include the .cshtml file extension when you reference the partial view, the framework will look only in the current Razor Page’s folder. Also, if you provide an absolute path to the partial, such as /Pages/Agenda/ToDo.cshtml, that’s the only place the framework will look.
将选择包含名为 _ToDo.cshtml 的文件的第一个位置。如果在引用分部视图时包含 .cshtml 文件扩展名,则框架将仅在当前 Razor Page 的文件夹中查找。此外,如果提供部分的绝对路径(如 /Pages/Agenda/ToDo.cshtml),则这是框架将查看的唯一位置。

Tip As with most of Razor Pages, the search locations are conventions that you can customize. If you find the need, you can customize the paths as shown here: http://mng.bz/nM9e.
提示:与大多数 Razor Pages 一样,搜索位置是可以自定义的约定。如果找到需求,可以自定义路径,如下所示:http://mng.bz/nM9e

The Razor code contained in a partial view is almost identical to a standard view. The main difference is the fact that partial views are called only from other views. The other difference is that partial views don’t run _ViewStart.cshtml when they execute. You’ll learn about _ViewStart.cshtml shortly in section 17.4.4.
分部视图中包含的 Razor 代码与标准视图几乎相同。主要区别在于分部视图仅从其他视图调用。另一个区别是,分部视图在执行时不会运行 _ViewStart.cshtml。您很快就会在第 17.4.4 节中了解 _ViewStart.cshtml。

NOTE Like layouts, partial views are typically named with a leading underscore.
注意:与布局一样,分部视图通常使用前导下划线命名。

Child actions in ASP.NET Core

In the legacy .NET Framework version of ASP.NET, there was the concept of a child action. This was an MVC controller action method that could be invoked from inside a view. This was the main mechanism for rendering discrete sections of a complex layout that had nothing to do with the main action method. For example, a child action method might render the shopping cart in the corner of every page on an e-commerce site.
在 ASP.NET 的旧版 .NET Framework 中,存在子作的概念。这是一个可以从视图内部调用的 MVC 控制器作方法。这是渲染复杂布局的离散部分的主要机制,与主作方法无关。例如,子作方法可能会在电子商务网站上每个页面的一角呈现购物车。

This approach meant you didn’t have to pollute every page’s view model with the view model items required to render the shopping cart, but it fundamentally broke the MVC design pattern by referencing controllers from a view.
这种方法意味着你不必用渲染购物车所需的视图模型项来污染每个页面的视图模型,但它通过从视图中引用控制器,从根本上打破了 MVC 设计模式。

In ASP.NET Core, child actions are no more. View components have replaced them. These are conceptually quite similar in that they allow both the execution of arbitrary code and the rendering of HTML, but they don’t directly invoke controller actions. You can think of them as a more powerful partial view that you should use anywhere a partial view needs to contain significant code or business logic. You’ll see how to build a small view component in chapter 32.
在 ASP.NET Core 中,子作不再存在。视图组件已取代它们。它们在概念上非常相似,因为它们都允许执行任意代码和呈现 HTML,但它们不直接调用控制器作。您可以将它们视为一个功能更强大的分部视图,您应该在分部视图需要包含重要代码或业务逻辑的任何位置使用它。您将在第 32 章中看到如何构建一个小的视图组件。

Partial views aren’t the only way to reduce duplication in your view templates. Razor also allows you to put common elements such as namespace declarations and layout configuration in centralized files. In the next section you’ll see how to wield these files to clean up your templates.
分部视图并不是减少视图样板中重复的唯一方法。Razor 还允许将命名空间声明和布局配置等常见元素放在集中式文件中。在下一节中,您将看到如何使用这些文件来清理模板。

17.4.4 Running code on every view with _ViewStart and _ViewImports

17.4.4 使用 _ViewStart 和 _ViewImports 在每个视图上运行代码

Due to the nature of views, you’ll inevitably find yourself writing certain things repeatedly. If all your views use the same layout, adding the following code to the top of every page feels a little redundant:
由于视图的性质,您不可避免地会发现自己重复编写某些内容。如果所有视图都使用相同的布局,则将以下代码添加到每个页面的顶部感觉有点多余:

@{
    Layout = "_Layout";
}

Similarly, if you find you need to reference objects from a different namespace in your Razor views, then having to add @using WebApplication1.Models to the top of every page can get to be a chore. Fortunately, ASP.NET Core includes two mechanisms for handling these common tasks: _ViewImports.cshtml and _ViewStart.cshtml.
同样,如果你发现需要在 Razor 视图中引用来自不同命名空间的对象,则必须将 WebApplication1.Models 添加到每个页面的顶部@using这可能是一件苦差事。幸运的是,ASP.NET Core 包含两种用于处理这些常见任务的机制:_ViewImports.cshtml 和 _ViewStart.cshtml。

Importing common directives with _ViewImports
使用 _ViewImports 导入通用指令

The _ViewImports.cshtml file contains directives that are inserted at the top of every Razor view. This can include things like the @using and @model statements that you’ve already seen—basically any Razor directive. For example, to avoid adding a using statement to every view, you can include it in _ViewImports.cshtml instead of in your Razor Pages, as shown in the following listing.
_ViewImports.cshtml 文件包含插入到每个 Razor 视图顶部的指令。这可以包括您已经看到的 @using 和 @model 语句等内容,基本上是任何 Razor 指令。例如,若要避免将 using 语句添加到每个视图,可以将其包含在 _ViewImports.cshtml 中,而不是包含在 Razor Pages 中,如下面的清单所示。

Listing 17.14 A typical _ViewImports.cshtml file importing additional namespaces
列表 17.14 一个典型的 _ViewImports.cshtml 文件导入额外的命名空间

@using WebApplication1            #A
@using WebApplication1.Pages      #A
@using WebApplication1.Models                            #B
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers    #C

❶ The default namespace of your application and the Pages folder
应用程序的默认命名空间和 Pages 文件夹
❷ Adds this directive to avoid placing it in every view
添加此指令以避免将其放置在每个视图中
❸ Makes Tag Helpers available in your views, added by default
使标签帮助程序在您的视图中可用,默认添加

The _ViewImports.cshtml file can be placed in any folder, and it will apply to all views and subfolders in that folder. Typically, it’s placed in the root Pages folder so that it applies to every Razor Page and partial view in your app.
_ViewImports.cshtml 文件可以放置在任何文件夹中,并且它将应用于该文件夹中的所有视图和子文件夹。通常,它位于根 Pages 文件夹中,以便应用于应用中的每个 Razor 页面和部分视图。

It’s important to note that you should put Razor directives only in _ViewImports.cshtml; you can’t put any old C# in there. As you can see in the previous listing, this is limited to things like @using or the @addTagHelper directive that you’ll learn about in chapter 18. If you want to run some arbitrary C# at the start of every view in your application, such as to set the Layout property, you should use the _ViewStart.cshtml file instead.
请务必注意,应仅将 Razor 指令放在 _ViewImports.cshtml 中;你不能把任何旧的 C# 放进去。正如您在前面的清单中所看到的,这仅限于 @using 或 @addTagHelper 指令之类的内容,您将在第 18 章中学习这些内容。如果要在应用程序中的每个视图的开头运行一些任意 C#,例如设置 Layout 属性,则应改用 _ViewStart.cshtml 文件。

Running code for every view with _ViewStart
使用 _ViewStart 为每个视图运行代码

You can easily run common code at the start of every Razor Page by adding a _ViewStart.cshtml file to the Pages folder in your application. This file can contain any Razor code, but it’s typically used to set the Layout for all the pages in your application, as shown in the following listing. Then you can omit the Layout statement from all pages that use the default layout. If a view needs to use a nondefault layout, you can override it by setting the value in the Razor Page itself.
通过将 _ViewStart.cshtml 文件添加到应用程序的 Pages 文件夹,可以轻松地在每个 Razor 页面的开头运行通用代码。此文件可以包含任何 Razor 代码,但它通常用于为应用程序中的所有页面设置 Layout,如下面的清单所示。然后,您可以从使用默认布局的所有页面中省略 Layout 语句。如果视图需要使用非默认布局,您可以通过在 Razor 页面本身中设置值来覆盖它。

Listing 17.15 A typical _ViewStart.cshtml file setting the default layout
列表 17.15 设置默认布局的典型 _ViewStart.cshtml 文件

@{
    Layout = "_Layout";
}

Any code in the _ViewStart.cshtml file runs before the view executes. Note that _ViewStart.cshtml runs only for Razor Page views; it doesn’t run for layouts or partial views. Also note that the names for these special Razor files are enforced and can’t be changed by conventions.
_ViewStart.cshtml 文件中的任何代码在视图执行之前运行。请注意,_ViewStart.cshtml 仅针对 Razor 页面视图运行;它不针对布局或分部视图运行。另请注意,这些特殊 Razor 文件的名称是强制性的,不能通过约定进行更改。

Warning You must use the names _ViewStart.cshtml and _ViewImports.cshtml for the Razor engine to locate and execute them correctly. To apply them to all your app’s pages, add them to the root of the Pages folder, not to the Shared subfolder.
警告:必须使用名称 _ViewStart.cshtml 和 _ViewImports.cshtml 以便 Razor 引擎正确查找和执行它们。要将它们应用于应用程序的所有页面,请将它们添加到 Pages 文件夹的根目录,而不是 Shared 子文件夹。

You can specify additional _ViewStart.cshtml or _ViewImports.cshtml files to run for a subset of your views by including them in a subfolder in Pages. The files in the subfolders run after the files in the root Pages folder.
您可以通过将视图子集包含在 Pages 的子文件夹中来指定要为视图子集运行的其他 _ViewStart.cs_ViewImports html 文件。子文件夹中的文件在根 Pages 文件夹中的文件之后运行。

Partial views, layouts, and AJAX
分部视图、布局和 AJAX

This chapter describes using Razor to render full HTML pages server-side, which are then sent to the user’s browser in traditional web apps. A common alternative approach when building web apps is to use a JavaScript client-side framework to build an SPA, which renders the HTML client-side in the browser.
本章介绍如何使用 Razor 在服务器端呈现完整的 HTML 页面,然后将其发送到传统 Web 应用程序中的用户浏览器。构建 Web 应用程序时,一种常见的替代方法是使用 JavaScript 客户端框架构建 SPA,该 SPA 在浏览器中呈现 HTML 客户端。

One of the technologies SPAs typically use is AJAX (Asynchronous JavaScript and XML), in which the browser sends requests to your ASP.NET Core app without reloading a whole new page. It’s also possible to use AJAX requests with apps that use server-side rendering. To do so, you’d use JavaScript to request an update for part of a page.
SPA 通常使用的技术之一是 AJAX(异步 JavaScript 和 XML),在这种技术中,浏览器将请求发送到您的 ASP.NET Core 应用程序,而无需重新加载全新的页面。还可以将 AJAX 请求与使用服务器端渲染的应用程序一起使用。为此,您需要使用 JavaScript 请求更新页面的一部分。

If you want to use AJAX with an app that uses Razor, you should consider making extensive use of partial views. Then you can expose these via additional Razor Page handlers, as shown in this article: http://mng.bz/vzB1. Using AJAX can reduce the overall amount of data that needs to be sent back and forth between the browser and your app, and it can make your app feel smoother and more responsive, as it requires fewer full-page loads. But using AJAX with Razor can add complexity, especially for larger apps. If you foresee yourself making extensive use of AJAX to build a highly dynamic web app, you might want to consider using minimal APIs or web API controllers with a client-side framework, or consider using Blazor instead.
如果要将 AJAX 与使用 Razor 的应用程序一起使用,则应考虑广泛使用分部视图。然后,您可以通过其他 Razor Page 处理程序公开这些内容,如本文所示:http://mng.bz/vzB1。使用 AJAX 可以减少需要在浏览器和应用程序之间来回发送的数据总量,并且可以使您的应用程序感觉更流畅、响应更快,因为它需要的整页加载更少。但是,将 AJAX 与 Razor 结合使用可能会增加复杂性,尤其是对于较大的应用程序。如果你预见到自己会广泛使用 AJAX 来构建高度动态的 Web 应用,则可能需要考虑将最少的 API 或 Web API 控制器与客户端框架结合使用,或者考虑改用 Blazor。

That concludes our first look at rendering HTML using the Razor templating engine. In the next chapter you’ll learn about Tag Helpers and how to use them to build HTML forms, a staple of modern web applications. Tag Helpers are one of the biggest improvements to Razor in ASP.NET Core over legacy ASP.NET, so getting to grips with them will make editing your views an overall more pleasant experience!
我们第一次使用 Razor 模板引擎渲染 HTML 到此结束。在下一章中,您将了解标记帮助程序以及如何使用它们来构建 HTML 表单,这是现代 Web 应用程序的主要内容。标签帮助程序是 ASP.NET Core 中 Razor 相对于旧版 ASP.NET 的最大改进之一,因此掌握它们将使编辑视图的整体体验更加愉快!

17.5 Summary

17.5 总结

Razor is a templating language that allows you to generate dynamic HTML using a mixture of HTML and C#. This provides the power of C# without your having to build up an HTML response manually using strings.
Razor 是一种模板语言,允许您使用 HTML 和 C# 的混合生成动态 HTML。这提供了 C# 的强大功能,而无需使用字符串手动构建 HTML 响应。

Razor Pages can pass strongly typed data to a Razor view by setting public properties on the PageModel. To access the properties on the view model, the view should declare the model type using the @model directive.
Razor Pages 可以通过在 PageModel 上设置公共属性,将强类型数据传递给 Razor 视图。要访问视图模型上的属性,视图应使用 @model 指令声明模型类型。

Page handlers can pass key-value pairs to the view using the ViewData dictionary. This is useful for implicitly passing shared data to layouts and partial views.
页面处理程序可以使用 ViewData 字典将键值对传递给视图。这对于将共享数据隐式传递给布局和分部视图非常有用。

Razor expressions render C# values to the HTML output using @ or @(). You don’t need to include a semicolon after the statement when using Razor expressions.
Razor 表达式使用 @ 或 @() 将 C# 值呈现到 HTML 输出。使用 Razor 表达式时,无需在语句后包含分号。

Razor code blocks, defined using @{}, execute C# without outputting HTML. The C# in Razor code blocks must be complete statements, so it must include semicolons.
使用 @{} 定义的 Razor 代码块执行 C# 而不输出 HTML。Razor 代码块中的 C# 必须是完整语句,因此它必须包含分号。

Loops and conditionals can be used to easily generate dynamic HTML in templates, but it’s a good idea to limit the number of if statements in particular, to keep your views easy to read.
循环和条件可用于在模板中轻松生成动态 HTML,但最好特别限制 if 语句的数量,以保持视图易于阅读。

If you need to render a string as raw HTML you can use Html.Raw, but do so sparingly; rendering raw user input can create a security vulnerability in your application.
如果需要将字符串呈现为原始 HTML,则可以使用 Html.Raw,但要谨慎使用;呈现原始用户输入可能会在您的应用程序中创建安全漏洞。

Tag Helpers allow you to bind your data model to HTML elements, making it easier to generate dynamic HTML while staying editor-friendly.
标签帮助程序允许您将数据模型绑定到 HTML 元素,从而更轻松地生成动态 HTML,同时保持编辑器友好性。

You can place HTML common to multiple views in a layout to reduce duplication. The layout will render any content from the child view at the location @RenderBody is called.
您可以将多个视图通用的 HTML 放置在布局中,以减少重复。布局将在调用子视图的位置呈现子视图中的任何内容@RenderBody。

Encapsulate commonly used snippets of Razor code in a partial view. A partial view can be rendered using the tag.
在分部视图中封装 Razor 代码的常用代码片段。可以使用 tag 呈现部分视图。

_ViewImports.cshtml can be used to include common directives, such as @using statements, in every view.
_ViewImports.cshtml 可用于在每个视图中包含常见指令,例如 @using 语句。

_ViewStart.cshtml is called before the execution of each Razor Page and can be used to execute code common to all Razor Pages, such as setting a default layout page. It doesn’t execute for layouts or partial views.
_ViewStart.cshtml 在执行每个 Razor 页面之前调用,可用于执行所有 Razor 页面通用的代码,例如设置默认布局页面。它不会对布局或分部视图执行。

_ViewImports.cshtml and _ViewStart.cshtml are hierarchical. Files in the root folder execute first, followed by files in controller-specific view folders.
_ViewImports.cshtml 和 _ViewStart.cshtml 是分层的。首先执行根文件夹中的文件,然后是特定于控制器的视图文件夹中的文件。

ASP.NET Core in Action 16 Binding and validating requests with Razor Pages

16 Binding and validating requests with Razor Pages
16 使用 Razor Pages 绑定和验证请求

This chapter covers
本章涵盖

• Using request values to create binding models
使用请求值创建绑定模型
• Customizing the model-binding process
自定义模型绑定过程
• Validating user input using DataAnnotations attributes
使用 DataAnnotations 属性验证用户输入

In chapter 7 we looked at the process of model binding and validation in minimal APIs. In this chapter we look at the Razor Pages equivalent: extracting values from a request using model binding and validating user input.
在第 7 章中,我们了解了最小 API 中的模型绑定和验证过程。在本章中,我们将介绍等效的 Razor Pages:使用模型绑定从请求中提取值并验证用户输入。

In the first half of this chapter, we look at using binding models to retrieve those parameters from the request so that you can use them in your Razor Pages by creating C# objects. These objects are passed to your Razor Page handlers as method parameters or are set as properties on your Razor Page PageModel.
在本章的前半部分,我们将介绍如何使用绑定模型从请求中检索这些参数,以便你可以通过创建 C# 对象在 Razor Pages 中使用它们。这些对象作为方法参数传递给 Razor Page 处理程序,或设置为 Razor Page PageModel 上的属性。

Once your code is executing in a page handler method, you can’t simply use the binding model without any further thought. Any time you’re using data provided by a user, you need to validate it! The second half of the chapter focuses on how to validate your binding models with Razor Pages.
一旦你的代码在页面处理程序方法中执行,你就不能简单地使用绑定模型而不做任何进一步的考虑。任何时候您使用用户提供的数据时,都需要对其进行验证!本章的后半部分重点介绍如何使用 Razor Pages 验证绑定模型。

We covered model binding and validation for minimal APIs in chapter 7, and conceptually, binding and validation are the same for Razor Pages. However, the details and mechanics of both binding and validation are quite different for Razor Pages.
我们在第 7 章中介绍了最小 API 的模型绑定和验证,从概念上讲,Razor Pages 的绑定和验证是相同的。但是,Razor Pages 的绑定和验证的详细信息和机制完全不同。

The binding models populated by the Razor Pages infrastructure are passed to page handlers when they execute. Once the page handler has run, you’re all set up to use the output models in ASP.NET Core’s implementation of Model-View-Controller (MVC): the view models and API models. These are used to generate a response to the user’s request. We’ll cover them in chapters 19 and 20.
Razor Pages 基础结构填充的绑定模型在执行时传递给页面处理程序。页面处理程序运行后,您就可以使用 ASP.NET Core 的模型-视图-控制器 (MVC) 实现中的输出模型:视图模型和 API 模型。这些用于生成对用户请求的响应。我们将在第 19 章和第 20 章中介绍它们。

Before we go any further, let’s recap the MVC design pattern and how binding models fit into ASP.NET Core.
在进一步讨论之前,让我们回顾一下 MVC 设计模式以及绑定模型如何适应 ASP.NET Core。

16.1 Understanding the models in Razor Pages and MVC

16.1 了解 Razor Pages 和 MVC 中的模型

In this section I describe how binding models fit into the MVC design pattern we covered in chapter 13. I describe the difference between binding models and the other “model” concepts in the MVC pattern and how they’re each used in ASP.NET Core.
在本节中,我将介绍绑定模型如何适应我们在第 13 章中介绍的 MVC 设计模式。我将介绍绑定模型与 MVC 模式中的其他“模型”概念之间的区别,以及它们在 ASP.NET Core 中的使用方式。

MVC is all about the separation of concerns. The premise is that isolating each aspect of your application to focus on a single responsibility reduces the interdependencies in your system. This separation makes it easier to make changes without affecting other parts of your application.
MVC 就是关注点分离。前提是,将应用程序的每个方面隔离起来,专注于单一职责,可以减少系统中的相互依赖关系。这种分离可以更轻松地进行更改,而不会影响应用程序的其他部分。

The classic MVC design pattern has three independent components:
经典 MVC 设计模式具有三个独立的组件:

• Model—The data to display and the methods for updating this data
模型 - 要显示的数据和更新此数据的方法
• View—Displays a representation of data that makes up the model
视图 - 显示构成模型的数据的表示形式
• Controller—Calls methods on the model and selects a view
控制器 - 调用模型的方法并选择视图

In this representation, there’s only one model, the application model, which represents all the business logic for the application as well as how to update and modify its internal state. ASP.NET Core has multiple models, which takes the single-responsibility principle (SRP) one step further than some views of MVC.
在这种表示形式中,只有一个模型,即应用程序模型,它表示应用程序的所有业务逻辑以及如何更新和修改其内部状态。ASP.NET Core 具有多个模型,这使得单一责任原则 (SRP) 比 MVC 的某些视图更进一步。

In chapter 13 we looked at an example of a to-do list application that can show all the to-do items for a given category and username. With this application, you make a request to a URL that’s routed using todo/listcategory/{category}/{username}. This returns a response showing all the relevant to-do items, as shown in figure 16.1.
在第 13 章中,我们看了一个待办事项列表应用程序的示例,它可以显示给定类别和用户名的所有待办事项。使用此应用程序,您可以向使用 todo/listcategory/{category}/{username} 路由的 URL 发出请求。这将返回一个响应,其中显示了所有相关的待办事项,如图 16.1 所示。

alt text

Figure 16.1 A basic to-do list application that displays to-do list items. A user can filter the list of items by changing the category and username parameters in the URL.
图 16.1 显示待办事项列表项的基本待办事项列表应用程序。用户可以通过更改 URL 中的 category 和 username 参数来筛选项目列表。

The application uses the same MVC constructs you’ve already seen, such as routing to a Razor Page handler, as well as various models. Figure 16.2 shows how a request to this application maps to the MVC design pattern and how it generates the final response, including additional details around the model binding and validation of the request.
该应用程序使用您已经看到的相同 MVC 构造,例如路由到 Razor Page 处理程序以及各种模型。图 16.2 显示了对此应用程序的请求如何映射到 MVC 设计模式,以及它如何生成最终响应,包括有关请求的模型绑定和验证的其他详细信息。

alt text

Figure 16.2 The MVC pattern in ASP.NET Core handling a request to view a subset of items in a to-do list Razor Pages application
图 16.2 ASP.NET Core 中的 MVC 模式处理查看待办事项列表 Razor Pages 应用程序中项子集的请求

ASP.NET Core Razor Pages uses several models, most of which are plain old CLR objects (POCOs), and the application model, which is more of a concept around a collection of services. Each of the models in ASP.NET Core is responsible for handling a different aspect of the overall request:
ASP.NET Core Razor Pages 使用多个模型,其中大多数是普通的旧 CLR 对象 (POCO),以及应用程序模型,后者更像是围绕服务集合的概念。ASP.NET Core 中的每个模型都负责处理整个请求的不同方面:

• Binding model—The binding model is all the information that’s provided by the user when making a request, as well as additional contextual data. This includes things like route parameters parsed from the URL, the query string, and form or JavaScript Object Notation (JSON) data in the request body. The binding model itself is one or more POCO objects that you define. Binding models in Razor Pages are typically defined by creating a public property on the page’s PageModel and decorating it with the [BindProperty] attribute. They can also be passed to a page handler as parameters.
For this example, the binding model would include the name of the category, open, and the username, Andrew. The Razor Pages infrastructure inspects the binding model before the page handler executes to check whether the provided values are valid, though the page handler executes even if they’re not, as you’ll see when we discuss validation in section 16.3.
绑定模型 - 绑定模型是用户在发出请求时提供的所有信息,以及其他上下文数据。这包括从 URL 解析的路由参数、查询字符串以及请求正文中的表单或 JavaScript 对象表示法 (JSON) 数据等内容。绑定模型本身是您定义的一个或多个 POCO 对象。Razor Pages 中的绑定模型通常是通过在页面的 PageModel 上创建公共属性并使用 [BindProperty] 属性对其进行修饰来定义的。它们也可以作为参数传递给页面处理程序。
在此示例中,绑定模型将包括 category 的名称 open 和用户名 Andrew。Razor Pages 基础结构在执行页面处理程序之前检查绑定模型,以检查提供的值是否有效,但页面处理程序即使无效也会执行,正如我们在第 16.3 节中讨论验证时所看到的那样

• Application model—The application model isn’t really an ASP.NET Core model at all. It’s typically a whole group of different services and classes and is more of a concept—anything needed to perform some sort of business action in your application. It may include the domain model (which represents the thing your app is trying to describe) and database models (which represent the data stored in a database), as well as any other, additional services.
In the to-do list application, the application model would contain the complete list of to-do items, probably stored in a database, and would know how to find only those to-do items in the open category assigned to Andrew.
应用程序模型 - 应用程序模型根本不是真正的 ASP.NET Core 模型。它通常是一整套不同的服务和类,更像是一个概念 — 在应用程序中执行某种业务作所需的任何内容。它可能包括域模型(表示您的应用程序尝试描述的事物)和数据库模型(表示存储在数据库中的数据),以及任何其他附加服务。
在待办事项列表应用程序中,应用程序模型将包含待办事项的完整列表,这些项目可能存储在数据库中,并且知道如何在分配给 Andrew 的打开类别中仅查找那些待办事项。

• Page model—The PageModel of a Razor Page serves two main functions: it acts as the controller for the application by exposing page handler methods, and it acts as the view model for a Razor view. All the data required for the view to generate a response is exposed on the PageModel, such as the list of to-dos in the open category assigned to Andrew.
页面模型 - Razor 页面的 PageModel 有两个主要功能:它通过公开页面处理程序方法充当应用程序的控制器,以及充当 Razor 视图的视图模型。视图生成响应所需的所有数据都在 PageModel 上公开,例如分配给 Andrew 的打开类别中的待办事项列表。

The PageModel base class that you derive your Razor Pages from contains various helper properties and methods. One of these, the ModelState property, contains the result of the model validation as a series of key-value pairs. You’ll learn more about validation and the ModelState property in section 16.3.
从中派生 Razor Pages 的 PageModel 基类包含各种帮助程序属性和方法。其中一个属性 ModelState 包含模型验证的结果,作为一系列键值对。您将在第 16.3 节中了解有关验证和 ModelState 属性的更多信息。

These models make up the bulk of any Razor Pages application, handling the input, business logic, and output of each page handler. Imagine you have an e-commerce application that allows users to search for clothes by sending requests to the /search/{query} URL, where {query} holds their search term:
这些模型构成了任何 Razor Pages 应用程序的大部分,处理每个页面处理程序的输入、业务逻辑和输出。假设您有一个电子商务应用程序,它允许用户通过向 /search/{query} URL 发送请求来搜索衣服,其中 {query} 保存他们的搜索词:

• Binding model—This would take the {query} route parameter from the URL and any values posted in the request body (maybe a sort order, or the number of items to show), and bind them to a C# class, which typically acts as a throwaway data transport class. This would be set as a property on the PageModel when the page handler is invoked.
绑定模型 - 这将从 URL 和请求正文中发布的任何值 (可能是排序顺序或要显示的项目数) 中获取 {query} 路由参数,并将它们绑定到 C# 类,该类通常充当一次性数据传输类。调用页面处理程序时,这将设置为 PageModel 上的属性。

• Application model—This is the services and classes that perform the logic. When invoked by the page handler, this model would load all the clothes that match the query, applying the necessary sorting and filters, and return the results to the controller.
应用程序模型 - 这是执行逻辑的服务和类。当页面处理程序调用时,此模型将加载与查询匹配的所有衣服,应用必要的排序和过滤器,并将结果返回给控制器。

• Page model—The values provided by the application model would be set as properties on the Razor Page’s PageModel, along with other metadata, such as the total number of items available or whether the user can currently check out. The Razor view would use this data to render the Razor view to HTML.
页面模型 - 应用程序模型提供的值将与其他元数据 (例如可用项的总数或用户当前是否可以签出) 一起设置为 Razor 页面的 PageModel 上的属性。Razor 视图将使用此数据将 Razor 视图呈现为 HTML。

The important point about all these models is that their responsibilities are well defined and distinct. Keeping them separate and avoiding reuse helps ensure that your application stays agile and easy to update.
所有这些模型的重要一点是,它们的责任是明确和不同的。将它们分开并避免重复使用有助于确保您的应用程序保持敏捷且易于更新。

The obvious exception to this separation is the PageModel, as it is where the binding models and page handlers are defined, and it also holds the data required for rendering the view. Some people may consider the apparent lack of separation to be sacrilege, but it’s not generally a problem. The lines of demarcation are pretty apparent. So long as you don’t try to, for example, invoke a page handler from inside a Razor view, you shouldn’t run into any problems!
这种分离的明显例外是 PageModel,因为它是定义绑定模型和页面处理程序的地方,并且它还保存呈现视图所需的数据。有些人可能认为明显缺乏分离是亵渎神明,但这通常不是问题。分界线非常明显。例如,只要您不尝试从 Razor 视图内部调用页面处理程序,就不会遇到任何问题!

Now that you’ve been properly introduced to the various models in ASP.NET Core, it’s time to focus on how to use them. This chapter looks at the binding models that are built from incoming requests—how are they created, and where do the values come from?
现在,您已经正确介绍了 ASP.NET Core 中的各种模型,是时候专注于如何使用它们了。本章介绍从传入请求构建的绑定模型 — 它们是如何创建的,这些值来自何处?

16.2 From request to model: Making the request useful

16.2 从请求到模型:使请求有用

In this section you will learn
在本节中,您将学习
• How ASP.NET Core creates binding models from a request
ASP.NET Core 如何从请求创建绑定模型
• How to bind simple types, like int and string, as well as complex classes
如何绑定简单类型(如 int 和 string)以及复杂类
• How to choose which parts of a request are used in the binding model
如何选择在绑定模型中使用请求的哪些部分

By now, you should be familiar with how ASP.NET Core handles a request by executing a page handler on a Razor Page. Page handlers are normal C# methods, so the ASP.NET Core framework needs to be able to call them in the usual way. The process of extracting values from the request and creating C# objects from them is called model binding.
到目前为止,你应该熟悉 ASP.NET Core 如何通过在 Razor 页面上执行页面处理程序来处理请求。页面处理程序是普通的 C# 方法,因此 ASP.NET Core 框架需要能够以常规方式调用它们。从请求中提取值并从中创建 C# 对象的过程称为模型绑定。

Any publicly settable properties on your Razor Page’s PageModel (in the .cshtml.cs file for your Razor Page), that are decorated with the [BindProperty] attribute are created from the incoming request using model binding, as shown in listing 16.1. Similarly, if your page handler method has any parameters, these are also created using model binding.
Razor 页面的 PageModel(在 Razor 页面的 .cshtml.cs 文件中)上用 [BindProperty] 属性修饰的任何可公开设置的属性都是使用模型绑定从传入请求创建的,如清单 16.1 所示。同样,如果您的页面处理程序方法具有任何参数,则这些参数也是使用模型绑定创建的。

Warning Properties decorated with [BindProperty] must have a public setter; otherwise, binding will silently fail.
警告:使用 [BindProperty] 修饰的属性必须具有公共 setter;否则,绑定将失败。

Listing 16.1 Model binding requests to properties in a Razor Page
列表 16.1 将请求绑定到 Razor 页面中的属性

public class IndexModel: PageModel
{
[BindProperty] ❶
public string Category { get; set; } ❶
[BindProperty(SupportsGet = true)] ❷
public string Username { get; set; } ❷
public void OnGet()
{
}
public void OnPost(ProductModel model) ❸
{
}
}

❶ Properties decorated with [BindProperty] take part in model binding.
用 [BindProperty] 修饰的属性参与模型绑定。
❷ Properties are not model-bound for GET requests unless you use SupportsGet.
除非使用 SupportsGet,否则 GET 请求的属性不受模型绑定。
❸ Parameters to page handlers are also model-bound when that handler is selected.
选择该处理程序时,页面处理程序的参数也是模型绑定的。

As described in chapter 15 and shown in the preceding listing, PageModel properties are not model-bound for GET requests, even if you add the [BindProperty] attribute. For security reasons, only requests using verbs like POST and PUT are bound. If you do want to bind GET requests, you can set the SupportsGet property on the [BindProperty] attribute to opt in to model binding.
如第 15 章中所述,如前面的清单所示,PageModel 属性不是 GET 请求的模型绑定的,即使你添加了 [BindProperty] 属性也是如此。出于安全原因,仅绑定使用 POST 和 PUT 等动词的请求。如果您确实想要绑定 GET 请求,则可以在 [BindProperty] 属性上设置 SupportsGet 属性,以选择加入模型绑定。

Which part is the binding model?
哪个部分是绑定模型?
Listing 16.1 shows a Razor Page that uses multiple binding models: the Category property, the Username property, and the ProductModel property (in the OnPost handler) are all model-bound.
清单 16.1 显示了一个使用多个绑定模型的 Razor Page:Category 属性、Username 属性和 ProductModel 属性(在 OnPost 处理程序中)都是模型绑定的。
Using multiple models in this way is fine, but I prefer to use an approach that keeps all the model binding in a single, nested class, which I often call InputModel. With this approach, the Razor Page in listing 16.1 could be written as follows:
以这种方式使用多个模型很好,但我更喜欢使用一种方法,将所有模型绑定保存在一个嵌套类中,我通常将其称为 InputModel。使用这种方法,清单 16.1 中的 Razor Page 可以编写如下:

public class IndexModel: PageModel
{
    [BindProperty]
    public InputModel Input { get; set; }
    public void OnGet()
    {
    }

    public class InputModel
    {
        public string Category { get; set; }
        public string Username { get; set; }
        public ProductModel Model { get; set; }
    }
}

This approach has some organizational benefits that you’ll learn more about in section 16.4.
这种方法具有一些组织优势,您将在 Section 16.4 中了解更多信息。

ASP.NET Core automatically populates your binding models for you using properties of the request, 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 的任何数据等)自动填充您的绑定模型。

NOTE In this chapter I describe how to bind your models to an incoming request, but I don’t show how Razor Pages uses your binding models to help generate that request using HTML forms. In chapter 17 you’ll learn about Razor syntax, which renders HTML, and in chapter 18 you’ll learn about Razor Tag Helpers, which generate form fields based on your binding model.
注意:在本章中,我将介绍如何将模型绑定到传入请求,但不会展示 Razor Pages 如何使用绑定模型来帮助使用 HTML 表单生成该请求。在第 17 章中,您将了解呈现 HTML 的 Razor 语法,在第 18 章中,您将了解基于绑定模型生成表单字段的 Razor 标记帮助程序。

By default, ASP.NET Core uses three different binding sources when creating your binding models in Razor Pages. It looks through each of these in order and takes the first value it finds (if any) that matches the name of the binding model:
默认情况下,ASP.NET Core 在 Razor Pages 中创建绑定模型时使用三种不同的绑定源。它按顺序查看每个值,并获取找到的与绑定模型名称匹配的第一个值(如果有):

• Form values—Sent in the body of an HTTP request when a form is sent to the server using a POST
表单值 - 使用 POST将表单发送到服务器时,在 HTTP 请求的正文中发送
• Route values—Obtained from URL segments or through default values after matching a route, as you saw in chapter 14
路由值 - 从 URL 段获取,或在匹配路由后通过默认值获取,如第 14 章所示
• Query string values—Passed at the end of the URL, not used during routing
查询字符串值 - 在 URL 末尾传递,在路由期间不使用

Warning Even though conceptually similar, the Razor Page binding process works quite differently from the approach used by minimal APIs.
警告:尽管在概念上相似,但 Razor 页面绑定过程的工作方式与最小 API 使用的方法完全不同。

The model binding process for Razor Pages is shown in figure 16.3. The model binder checks each binding source to see whether it contains a value that could be set on the model. Alternatively, the model can choose the specific source the value should come from, as you’ll see in section 16.2.3. Once each property is bound, the model is validated and is set as a property on the PageModel or passed as a parameter to the page handler. You’ll learn about the validation process in the second half of this chapter.
Razor Pages 的模型绑定过程如图 16.3 所示。模型绑定器检查每个绑定源,以查看它是否包含可在模型上设置的值。或者,模型可以选择值应来自的特定来源,如第 16.2.3 节所示。绑定每个属性后,将验证模型并将其设置为 PageModel 上的属性或作为参数传递给页面处理程序。您将在本章的后半部分了解验证过程。

alt text

Figure 16.3 Model binding involves mapping values from binding sources, which correspond to different parts of a request.
图 16.3 模型绑定涉及映射来自绑定源的值,这些值对应于请求的不同部分。

NOTE In Razor Pages, different properties of a complex model can be model-bound to different sources. This differs from minimal APIs, where the whole object would be bound from a single source, and “partial” binding is not possible. Razor Pages also bind to form bodies by default, while minimal APIs cannot. These differences are partly for historical reasons and partly because minimal APIs opts for performance over convenience in this respect.
注意:在 Razor Pages 中,复杂模型的不同属性可以通过模型绑定到不同的源。这与最小 API 不同,在最小 API 中,整个对象将从单个源绑定,并且不可能进行“部分”绑定。默认情况下,Razor Pages 还会绑定到表单正文,而最小的 API 则不能。这些差异部分是由于历史原因,部分是因为在这方面,最小的 API 选择了性能而不是便利性。

PageModel properties or page handler parameters?
PageModel 属性还是页面处理程序参数?
There are three ways to use model binding in Razor Pages:
有三种方法可以在 Razor Pages 中使用模型绑定:

• Decorate properties on your PageModel with the [BindProperty] attribute.
使用 [BindProperty] 属性修饰 PageModel 上的属性。
• Add parameters to your page handler method.
将参数添加到页面处理程序方法。
• Decorate the whole PageModel with [BindProperties].
使用 [BindProperties] 装饰整个 PageModel。

Which of these approaches should you choose?
您应该选择哪种方法?

This answer to this question is largely a matter of taste. Setting properties on the PageModel and marking them with [BindProperty] is the approach you’ll see most often in examples. If you use this approach, you’ll be able to access the binding model when the view is rendered, as you’ll see in chapters 17 and 18.
这个问题的答案在很大程度上是一个品味问题。在 PageModel 上设置属性并使用 [BindProperty] 标记它们是示例中最常见的方法。如果使用这种方法,您将能够在呈现视图时访问绑定模型,如第 17 章和第 18 章所示。

The second approach, adding parameters to page handler methods, provides more separation between the different MVC stages, because you won’t be able to access the parameters outside the page handler. On the downside, if you do need to display those values in the Razor view, you’ll have to copy the parameters across manually to properties that can be accessed in the view.
第二种方法(向页面处理程序方法添加参数)在不同的 MVC 阶段之间提供了更多的分离,因为您将无法在页面处理程序之外访问参数。缺点是,如果您确实需要在 Razor 视图中显示这些值,则必须手动将参数复制到可在视图中访问的属性。

I avoid the final approach, decorating the PageModel itself with [BindProperties]. With this approach, every property on your PageModel takes part in model binding. I don’t like the indirection this gives and the risk of accidentally binding properties I didn’t want to be model-bound.
我避免使用最后一种方法,即使用 [BindProperties] 修饰 PageModel 本身。使用这种方法,PageModel 上的每个属性都参与模型绑定。我不喜欢这提供的间接性,以及意外绑定我不想被模型绑定的属性的风险。

The approach I choose tends to depend on the specific Razor Page I’m building. If I’m creating a form, I will favor the [BindProperty] approach, as I typically need access to the request values inside the Razor view. For simple pages, where the binding model is a product ID, for example, I tend to favor the page handler parameter approach for its simplicity, especially if the handler is for a GET request. I give some more specific advice on my approach in section 16.4.
我选择的方法往往取决于我正在构建的特定 Razor 页面。如果我要创建表单,我将倾向于使用 [BindProperty] 方法,因为我通常需要访问 Razor 视图中的请求值。例如,对于绑定模型是产品 ID 的简单页面,我倾向于使用页面处理程序参数方法,因为它简单,尤其是在处理程序用于 GET 请求时。我在 16.4 节中对我的方法给出了一些更具体的建议。

Figure 16.4 shows an example of a request creating the ProductModel method argument using model binding for the example shown at the start of this section:
图 16.4 显示了使用模型绑定创建 ProductModel 方法参数的请求示例,该示例位于本节开头所示:

public void OnPost(ProductModel product)

alt text

Figure 16.4 Using model binding to create an instance of a model that’s used to execute a Razor Page
图 16.4 使用模型绑定创建用于执行 Razor 页面的模型实例

The Id property has been bound from a URL route parameter, but the Name and SellPrice properties have been bound from the request body. The big advantage of using model binding is that you don’t have to write the code to parse requests and map the data yourself. This sort of code is typically repetitive and error-prone, so using the built-in conventional approach lets you focus on the important aspects of your application: the business requirements.
Id 属性已从 URL 路由参数绑定,但 Name 和 SellPrice 属性已从请求正文绑定。使用模型绑定的一大优点是,您不必自己编写代码来解析请求和映射数据。此类代码通常是重复的且容易出错,因此使用内置的常规方法可以让您专注于应用程序的重要方面:业务需求。

Tip Model binding is great for reducing repetitive code. Take advantage of it whenever possible, and you’ll rarely find yourself having to access the Request object directly.
提示:模型绑定非常适合减少重复代码。尽可能利用它,您很少会发现自己必须直接访问 Request 对象。

If you need to, the capabilities are there to let you completely customize the way model binding works, but it’s relatively rare that you’ll find yourself needing to dig too deep into this. For the majority of cases, it works as is, as you’ll see in the remainder of this section.
如果需要,这些功能可以让您完全自定义模型绑定的工作方式,但相对较少的情况是,您会发现自己需要对此进行深入的研究。在大多数情况下,它按原样工作,如本节的其余部分所示。

16.2.1 Binding simple types

16.2.1 绑定简单类型

We’ll start our journey into model binding by considering a simple Razor Page handler. The next listing shows a simple Razor Page that takes one number as a method parameter and squares it by multiplying the number by itself.
我们将通过考虑一个简单的 Razor Page 处理程序来开始模型绑定之旅。下一个清单显示了一个简单的 Razor Page,它采用一个数字作为方法参数,并通过将数字本身乘以来平方。

Listing 16.2 A Razor Page accepting a simple parameter
列表 16.2 接受简单参数的 Razor 页面

public class CalculateSquareModel : PageModel
{
public void OnGet(int number) ❶
{
Square = number * number; ❷
}
public int Square { get; set; } ❸
}

❶ The method parameter is the binding model.
method 参数是绑定模型。
❷ A more complex example would do this work in an external service, in the application model.
一个更复杂的示例是在应用程序模型中的外部服务中完成这项工作。
❸ The result is exposed as a property and is used by the view to generate a response.
结果作为属性公开,并由视图用于生成响应。

In chapters 6 and 14, you learned about routing and how it selects a Razor Page to execute. You can update the route template for the Razor Page to be "CalculateSquare/{number}" by adding a {number} segment to the Razor Page’s @page directive in the .cshtml file:
在第 6 章和第 14 章中,您了解了路由以及它如何选择要执行的 Razor 页面。可以通过在 .cshtml 文件中将 {number} 段添加到 Razor 页面的 @page 指令,将 Razor 页面的路由模板更新为“CalculateSquare/{number}”:

@page "{number}"

When a client requests the URL /CalculateSquare/5, the Razor Page framework uses routing to parse it for route parameters. This produces the route value pair
当客户端请求 URL /CalculateSquare/5 时,Razor Page 框架使用路由来分析路由参数。这将生成路由值对

number=5

The Razor Page’s OnGet page handler contains a single parameter—an integer called number—which is your binding model. When ASP.NET Core executes this page handler method, it will spot the expected parameter, flick through the route values associated with the request, and find the number=5 pair. Then it can bind the number parameter to this route value and execute the method. The page handler method itself doesn’t care where this value came from; it goes along its merry way, calculating the square of the value and setting it on the Square property.
Razor Page 的 OnGet 页面处理程序包含一个参数(称为 number 的整数),该参数是绑定模型。当 ASP.NET Core 执行此页面处理程序方法时,它将发现预期的参数,浏览与请求关联的路由值,并找到 number=5 对。然后它可以将 number 参数绑定到这个路由值上,并执行该方法。页面处理程序方法本身并不关心此值的来源;它沿着快乐的方式前进,计算值的平方并将其设置为 Square 属性。

The key thing to appreciate is that you didn’t have to write any extra code to try to extract the number from the URL when the method executed. All you needed to do was create a method parameter (or public property) with the right name and let model binding do its magic.
需要注意的关键是,在方法执行时,您不必编写任何额外的代码来尝试从 URL 中提取数字。您需要做的就是创建一个具有正确名称的方法参数(或公共属性),然后让模型绑定发挥它的魔力。

Route values aren’t the only values the Razor Pages model binder can use to create your binding models. As you saw previously, the framework will look through three default binding sources to find a match for your binding models:
路由值并不是 Razor Pages 模型绑定器可用于创建绑定模型的唯一值。如前所述,框架将遍历三个默认绑定源,以查找绑定模型的匹配项:

• Form values
• Route values
• Query string values

Each of these binding sources store values as name-value pairs. If none of the binding sources contains the required value, the binding model is set to a new, default instance of the type instead. The exact value the binding model will have in this case depends on the type of the variable:
这些绑定源中的每一个都将值存储为名称-值对。如果没有任何绑定源包含所需的值,则绑定模型将改为该类型的新默认实例。在这种情况下,绑定模型将具有的确切值取决于变量的类型:

• For value types, the value will be default(T). For an int parameter this would be 0, and for a bool it would be false.
对于值类型,该值将为 default(T)。对于 int 参数,此值为 0,对于 bool 参数,此值为 false。
• For reference types, the type is created using the default (parameterless) constructor. For custom types like ProductModel, that will create a new object. For nullable types like int? or bool?, the value will be null.
对于引用类型,类型是使用默认 (无参数) 构造函数创建的。对于像 ProductModel 这样的自定义类型,这将创建一个新对象。对于像 int 这样的可空类型?或 bool?,则值为 null。
• For string types, the value will be null.
对于字符串类型,该值将为 null。

Warning It’s important to consider the behavior of your page handler when model binding fails to bind your method parameters. If none of the binding sources contains the value, the value passed to the method could be null or could unexpectedly have a default value (for value types).
警告:当模型绑定无法绑定方法参数时,请务必考虑页面处理程序的行为。如果没有任何绑定源包含该值,则传递给该方法的值可能为 null,或者可能意外地具有默认值(对于值类型)。

Listing 16.2 showed how to bind a single method parameter. Let’s take the next logical step and look at how you’d bind multiple method parameters.
清单 16.2 展示了如何绑定单个方法参数。让我们进行下一个逻辑步骤,看看如何绑定多个方法参数。

Let’s say you’re building a currency converter application. As the first step you need to create a method in which the user provides a value in one currency, and you must convert it to another. You first create a Razor Page called Convert.cshtml and then customize the route template for the page using the @page directive to use an absolute path containing two route values:
假设您正在构建一个货币转换器应用程序。第一步,您需要创建一个方法,在该方法中,用户以一种货币提供值,并且必须将其转换为另一种货币。首先创建一个名为 Convert.cshtml 的 Razor 页面,然后使用 @page 指令自定义页面的路由模板,以使用包含两个路由值的绝对路径:

@page "/{currencyIn}/{currencyOut}"

Then you create a page handler that accepts the three values you need, as shown in the following listing.
然后,创建一个接受您需要的 3 个值的页面处理程序,如下面的清单所示。

Listing 16.3 A Razor Page handler accepting multiple binding parameters
列表 16.3 接受多个绑定参数的 Razor Page 处理程序

public class ConvertModel : PageModel
{
    public void OnGet(
        string currencyIn,
        string currencyOut,
        int qty
)
    {
        /* method implementation */
    }
}

As you can see, there are three different parameters to bind. The question is, where will the values come from and what will they be set to? The answer is, it depends! Table 16.1 shows a whole variety of possibilities. All these examples use the same route template and page handler, but depending on the data sent, different values will be bound. The actual values might differ from what you expect, as the available binding sources offer conflicting values!
如您所见,有三个不同的参数需要绑定。问题是,这些值从何而来,它们将设置为什么?答案是,这要看情况!表 16.1 显示了各种可能性。所有这些示例都使用相同的路由模板和页面处理程序,但根据发送的数据,将绑定不同的值。实际值可能与您的预期不同,因为可用的绑定源提供的值相互冲突!

Table 16.1 Binding request data to page handler parameters from multiple binding sources
表 16.1 将请求数据绑定到来自多个绑定源的页面处理程序参数

URL (route values) HTTP body data (form values) Parameter values bound
/GBP/USD - currencyIn=GBP
currencyOut=USD qty=0
/GBP/USD?currencyIn=CAD QTY=50 currencyIn=GBP
currencyOut=USD qty=50
/GBP/USD?qty=100 qty=50 currencyIn=GBP
currencyOut=USD qty=50
/GBP/USD?qty=100 currencyIn=CAD&
currencyOut=EUR&qty=50
currencyIn=CAD
currencyOut=EUR qty=50

For each example, be sure you understand why the bound values have the values that they do. In the first example, the qty value isn’t found in the form data, in the route values, or in the query string, so it has the default value of 0. In each of the other examples, the request contains one or more duplicated values; in these cases, it’s important to bear in mind the order in which the model binder consults the binding sources. By default, form values will take precedence over other binding sources, including route values!
对于每个示例,请确保您了解为什么绑定值具有它们所具有的值。在第一个示例中,在表单数据、路由值或查询字符串中找不到 qty 值,因此它的默认值为 0。在所有其他示例中,请求都包含一个或多个重复值;在这些情况下,请务必记住 Model Binder 查询 Binding 源的顺序。默认情况下,表单值将优先于其他绑定源,包括路由值!

NOTE The default model binder isn’t case-sensitive, so a binding value of QTY=50 will happily bind to the qty parameter.
注意:默认模型绑定器不区分大小写,因此 QTY=50 的绑定值将很高兴地绑定到 qty 参数。

Although this may seem a little overwhelming, it’s relatively unusual to be binding from all these different sources at once. It’s more common to have your values all come from the request body as form values, maybe with an ID from URL route values. This scenario serves as more of a cautionary tale about the knots you can twist yourself into if you’re not sure how things work under the hood.
虽然这看起来有点让人不知所措,但同时从所有这些不同的来源绑定是相对不寻常的。更常见的做法是,你的值都来自请求正文作为表单值,可能带有来自 URL 路由值的 ID。这个场景更像是一个警示故事,如果你不确定引擎盖下的事情是如何运作的,你可以把自己扭成一团。

In these examples, you happily bound the qty integer property to incoming values, but as I mentioned earlier, the values stored in binding sources are all strings. What types can you convert a string to?
在这些示例中,您愉快地将 qty integer 属性绑定到传入值,但正如我前面提到的,存储在绑定源中的值都是字符串。你可以将字符串转换为哪些类型?

The model binder will convert pretty much any primitive .NET type such as int, float, decimal (and string obviously), any custom type that has a TryParse method (like minimal APIs, as you saw in chapter 7) plus anything that has a TypeConverter.
模型绑定器将转换几乎任何基元 .NET 类型,如 int、float、decimal(显然还有 string)、任何具有 TryParse 方法的自定义类型(如第 7 章中所看到的最小 API)以及任何具有 TypeConverter 的类型。

NOTE TypeConverters can be found in the System.ComponentModel.TypeConverter package. You can read more about them in Microsoft’s “Type conversion in .NET” documentation: http://mng.bz/A0GK.
注意:TypeConverters 可以在 System.ComponentModel.TypeConverter 包中找到。您可以在 Microsoft 的“.NET 中的类型转换”文档中阅读有关它们的更多信息:http://mng.bz/A0GK

There are a few other special cases that can be converted from a string, such as Type, but thinking of it as built-in types only will get you a long way there!
还有一些其他特殊情况可以从字符串转换,例如 Type,但仅将其视为内置类型将使您大有帮助!

16.2.2 Binding complex types

16.2.2 绑定复杂类型

If it seems like only being able to bind simple built-in types is a bit limiting, you’re right! Luckily, that’s not the case for the model binder. Although it can only convert strings directly to those simple types, it’s also able to bind complex types by traversing any properties your binding models expose, binding each of those properties to strings instead.
如果看起来只能绑定简单的内置类型有点限制,那你是对的!幸运的是,模型 Binder 并非如此。虽然它只能将字符串直接转换为这些简单类型,但它也能够通过遍历绑定模型公开的任何属性来绑定复杂类型,而是将每个属性绑定到字符串。

If this doesn’t make you happy straight off the bat, let’s look at how you’d have to build your page handlers if simple types were your only option. Imagine a user of your currency converter application has reached a checkout page and is going to exchange some currency. Great! All you need now is to collect their name, email address, and phone number. Unfortunately, your page handler method would have to look something like this:
如果这不能让你立即满意,让我们看看如果简单类型是你唯一的选择,你将如何构建你的页面处理程序。想象一下,您的货币转换器应用程序的用户已到达结帐页面并准备兑换一些货币。伟大!您现在需要做的就是收集他们的姓名、电子邮件地址和电话号码。不幸的是,您的页面处理程序方法必须如下所示:

public IActionResult OnPost(string firstName, string lastName, string phoneNumber, string email)

Yuck! Four parameters might not seem that bad right now, but what happens when the requirements change and you need to collect other details? The method signature will keep growing. The model binder will bind the values quite happily, but it’s not exactly clean code. Using the [BindProperty] approach doesn’t really help either; you still have to clutter your PageModel with lots of properties and attributes!
呸!四个参数现在看起来可能还不错,但是当需求发生变化并且您需要收集其他详细信息时会发生什么?方法签名将不断增长。模型 Binder 会非常愉快地绑定这些值,但它并不是完全干净的代码。使用 [BindProperty] 方法也没有真正的帮助;你仍然需要用大量的属性和特性来整理你的 PageModel!

Simplifying method parameters by binding to complex objects
通过绑定到复杂对象来简化方法参数

A common pattern for any C# code when you have many method parameters is to extract a class that encapsulates the data the method requires. If extra parameters need to be added, you can add a new property to this class. This class becomes your binding model, and it might look something like the following listing.
当有许多方法参数时,任何 C# 代码的常见模式是提取一个封装方法所需数据的类。如果需要添加额外的参数,您可以向此类添加新属性。这个类将成为您的绑定模型,它可能类似于下面的清单。

Listing 16.4 A binding model for capturing a user’s details
清单 16.4 用于捕获用户详细信息的绑定模型

public class UserBindingModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string PhoneNumber { get; set; }
}

NOTE In this book I primarily use class instead of record for my binding models, but you can use record if you prefer. I find the terseness that the record positional syntax provides is lost if you want to add attributes to properties, such as to add validation attributes, as you’ll see in section 16.3. You can see the required syntax for positional property attributes in the documentation at http://mng.bz/Kex0.
注意:在本书中,我主要使用 class 而不是 record 作为我的绑定模型,但如果您愿意,也可以使用 record。我发现,如果您想向属性添加属性(例如添加验证属性),则记录位置语法提供的简洁性会丢失,如第 16.3 节所示。您可以在 http://mng.bz/Kex0 文档中查看位置属性属性所需的语法。

With this model, you can update your page handler’s method signature to
使用此模型,您可以将页面处理程序的方法签名更新为

public IActionResult OnPost(UserBindingModel user)

Alternatively, using the [BindProperty] approach, create a property on the PageModel:
或者,使用 [BindProperty] 方法,在 PageModel 上创建一个属性:

[BindProperty]
public UserBindingModel User { get; set; }

Now you can simplify the page handler signature even further:
现在,您可以进一步简化页面处理程序签名:

public IActionResult OnPost()

Functionally, the model binder treats this new complex type a little differently. Rather than look for parameters with a value that matches the parameter name (user, or User for the property), the model binder creates a new instance of the model using new UserBindingModel().
从功能上讲,模型 Binder 对这种新的复杂类型的处理方式略有不同。模型绑定器不会查找值与参数名称(或属性的 User)匹配的参数,而是使用 new UserBindingModel() 创建模型的新实例。

NOTE You don’t have to use custom classes for your methods; it depends on your requirements. If your page handler needs only a single integer, it makes more sense to bind to the simple parameter.
注意:您不必为您的方法使用自定义类;这取决于您的要求。如果您的页面处理程序只需要一个整数,则绑定到 simple 参数更有意义。

Next, the model binder loops through all the properties your binding model has, such as FirstName and LastName in listing 16.4. For each of these properties, it consults the collection of binding sources and attempts to find a name-value pair that matches. If it finds one, it sets the value on the property and moves on to the next.
接下来,模型 Binders 遍历绑定模型具有的所有属性,例如清单 16.4 中的 FirstName 和 LastName。对于其中每个属性,它都会查询绑定源的集合,并尝试查找匹配的名称/值对。如果找到一个,它将设置该属性的值,然后继续执行下一个属性。

Tip Although the name of the model isn’t necessary in this example, the model binder will also look for properties prefixed with the name of the property, such as user.FirstName and user.LastName for a property called User. You can use this approach when you have multiple complex parameters to a page handler or multiple complex [BindProperty] properties. In general, for simplicity, you should avoid this situation if possible. As for all model binding, the casing of the prefix does not matter.
提示:尽管在此示例中不需要模型名称,但模型绑定器还将查找以属性名称为前缀的属性,例如 user。FirstName 和 user。名为 User 的属性的 LastName。当页面处理程序具有多个复杂参数或多个复杂 [BindProperty] 属性时,可以使用此方法。通常,为简单起见,应尽可能避免这种情况。对于所有模型绑定,前缀的大小写无关紧要。

Once all the properties that can be bound on the binding model are set, the model is passed to the page handler (or the [BindProperty] property is set), and the handler is executed as usual. The behavior from this point on is identical to when you have lots of individual parameters—you’ll end up with the same values set on your binding model—but the code is cleaner and easier to work with.
设置了可在绑定模型上绑定的所有属性后,模型将传递给页面处理程序 (或设置了 [BindProperty] 属性) ,并照常执行处理程序。从此时开始,行为与具有大量单个参数时的行为相同 — 您最终将在绑定模型上设置相同的值 — 但代码更简洁,更易于使用。

Tip For a class to be model-bound, it must have a default public constructor. You can bind only properties that are public and settable.
提示:对于要进行模型绑定的类,它必须具有默认的 public 构造函数。您只能绑定 public 和 settable 属性。

With this technique you can bind complex hierarchical models whose properties are themselves complex models. As long as each property exposes a type that can be model-bound, the binder can traverse it with ease.
使用这种技术,您可以绑定复杂的分层模型,这些模型的属性本身就是复杂模型。只要每个属性都公开一个可以进行模型绑定的类型,Binders 就可以轻松遍历它。

Binding collections and dictionaries
绑定集合和词典
As well as binding to ordinary custom classes and primitives, you can bind to collections, lists, and dictionaries. Imagine you had a page in which a user selected all the currencies they were interested in; you’d display the rates for all those selected, as shown in figure 16.5.
除了绑定到普通自定义类和基元外,还可以绑定到集合、列表和词典。想象一下,您有一个页面,用户在其中选择了他们感兴趣的所有货币;将显示所有选定项目的 Rate,如图 16.5 所示。

alt text

Figure 16.5 The select list in the currency converter application sends a list of selected currencies to the application. Model binding binds the selected currencies and customizes the view for the user to show the equivalent cost in the selected currencies.
图 16.5 货币转换器应用程序中的选择列表将所选货币的列表发送到应用程序。模型绑定绑定所选货币并自定义视图,以便用户显示所选货币的等效成本。

To achieve this, you could create a page handler that accepts a List type, such as
为此,您可以创建一个接受 List 类型的页面处理程序,例如

public void OnPost(List<string> currencies);

You could then POST data to this method by providing values in several different formats:
然后,您可以通过提供几种不同格式的值来将数据 POST 到此方法:

• currencies[index]—Where currencies is the name of the parameter to bind and index is the index of the item to bind, such as currencies[0]= GBP&currencies[1]=USD.
currencies[index] - 其中 currencies 是要绑定的参数的名称,index 是要绑定的项目的索引,例如 currencies[0]= GBP&currencies[1]=USD。

• [index]—If you’re binding to a single list (as in this example), you can omit the name of the parameter, such as [0]=GBP&[1]=USD.
[index] - 如果要绑定到单个列表 (如本例所示),则可以省略参数的名称,例如 [0]=GBP&[1]=USD。

• currencies—Alternatively, you can omit the index and send currencies as the key for every value, such as currencies=GBP&currencies=USD.
currencies - 或者,您可以省略索引并将 currencies 作为每个值的键发送,例如 currencies=GBP&currencies=USD。

The key values can come from route values and query values, but it’s far more common to POST them in a form. Dictionaries can use similar binding, where the dictionary key replaces the index both when the parameter is named and when it’s omitted.
键值可以来自路由值和查询值,但在表单中 POST 它们更为常见。字典可以使用类似的绑定,其中字典键在命名参数和省略参数时替换索引。

Tip In the previous example I showed a collection using the built-in string type, but you can also bind collections of complex type, such as a List<UserBindingModel>.
提示:在前面的示例中,我展示了一个使用内置字符串类型的集合,但您也可以绑定复杂类型的集合,例如 List<UserBindingModel>

If this all seems a bit confusing, don’t feel too alarmed. If you’re building a traditional web application and using Razor views to generate HTML, the framework will take care of generating the correct names for you. As you’ll see in chapter 18, the Razor view ensures that any form data you POST is generated in the correct format.
如果这一切看起来有点令人困惑,请不要太惊慌。如果您正在构建传统的 Web 应用程序并使用 Razor 视图生成 HTML,框架将负责为您生成正确的名称。正如您将在第 18 章中看到的那样,Razor 视图可确保您 POST 的任何表单数据都以正确的格式生成。

Binding file uploads with IFormFile
将文件上传与 IFormFile绑定
Razor Pages supports users uploading files by exposing the IFormFile and IFormFileCollection interfaces. You can use these interfaces as your binding model, either as a method parameter to your page handler or using the [BindProperty] approach, and they will be populated with the details of the file upload:
Razor Pages 支持用户通过公开 IFormFile 和 IFormFileCollection 接口来上传文件。您可以将这些接口用作绑定模型,作为页面处理程序的方法参数或使用 [BindProperty] 方法,它们将填充文件上传的详细信息:

public void OnPost(IFormFile file);

If you need to accept multiple files, you can use IFormFileCollection, IEnumerable<IFormFile>, or List<IFormFile>:
如果需要接受多个文件,可以使用 IFormFileCollection、IEnumerable<IFormFile>List<IFormFile>

public void OnPost(IEnumerable<IFormFile> file);

You already learned how to use IFormFile in chapter 7 when you looked at minimal API binding. The process is the same for Razor Pages. I’ll reiterate one point here: if you don’t need users to upload files, great! There are so many potential threats to consider when handling files—from malicious attacks, to accidental denial-of-service vulnerabilities—that I avoid them whenever possible.
在第 7 章中,您已经学习了如何使用 IFormFile,当时您了解了最小 API 绑定。Razor Pages 的过程相同。我在这里重申一点:如果您不需要用户上传文件,那就太好了!在处理文件时,需要考虑许多潜在威胁 — 从恶意攻击到意外的拒绝服务漏洞 — 因此我尽可能避免它们。

For the vast majority of Razor Pages, the default configuration of model binding for simple and complex types works perfectly well, but you may find some situations where you need to take a bit more control. Luckily, that’s perfectly possible, and you can completely override the process if necessary by replacing the ModelBinders used in the guts of the framework.
对于绝大多数 Razor Pages,简单类型和复杂类型的模型绑定的默认配置运行良好,但你可能会发现在某些情况下需要进行更多控制。幸运的是,这是完全可能的,如有必要,您可以通过替换框架内部中使用的 ModelBinders 来完全覆盖该过程。

However, it’s rare to need that level of customization. I’ve found it’s more common to want to specify which binding source to use for a page’s binding instead.
但是,很少需要这种级别的自定义。我发现,更常见的做法是指定要用于页面绑定的绑定源。

16.2.3 Choosing a binding source

16.2.3 选择绑定源

As you’ve already seen, by default the ASP.NET Core model binder attempts to bind your binding models from three binding sources: form data, route data, and the query string.
如您所见,默认情况下,ASP.NET Core 模型 Binder 会尝试从三个绑定源绑定您的绑定模型:表单数据、路由数据和查询字符串。

Occasionally, you may find it necessary to specifically declare which binding source to bind to. In other cases, these three sources won’t be sufficient at all. The most common scenarios are when you want to bind a method parameter to a request header value or when the body of a request contains JSON-formatted data that you want to bind to a parameter. In these cases, you can decorate your binding models with attributes that say where to bind from, as shown in the following listing.
有时,您可能会发现有必要专门声明要绑定到的绑定源。在其他情况下,这三个来源根本不够。最常见的情况是,当您要将方法参数绑定到请求标头值时,或者当请求正文包含要绑定到参数的 JSON 格式数据时。在这些情况下,您可以使用说明绑定位置的属性来装饰您的绑定模型,如下面的清单所示。

Listing 16.5 Choosing a binding source for model binding
清单 16.5 为模型绑定选择绑定源

public class PhotosModel: PageModel
{
    public void OnPost(
        [FromHeader] string userId,     ❶
        [FromBody] List<Photo> photos)      ❷
    {
        /* method implementation */
    }
}

❶ The userId is bound from an HTTP header in the request.
userId 从请求中的 HTTP 标头绑定。
❷ The list of photo objects is bound to the body of the request, typically in JSON format.
照片对象列表绑定到请求的正文,通常采用 JSON 格式。

In this example, a page handler updates a collection of photos with a user ID. There are method parameters for the ID of the user to be tagged in the photos, userId, and a list of Photo objects to tag, photos.
在此示例中,页面处理程序使用用户 ID 更新照片集合。有方法参数,用于在 photos 中标记的用户 ID、userId 和要标记的 Photo 对象列表 photos。

Rather than binding these method parameters using the standard binding sources, I’ve added attributes to each parameter, indicating the binding source to use. The [FromHeader] attribute has been applied to the userId parameter. This tells the model binder to bind the value to an HTTP request header value called userId.
我没有使用标准绑定源来绑定这些方法参数,而是向每个参数添加了属性,以指示要使用的绑定源。[FromHeader] 属性已应用于 userId 参数。这会告知模型绑定器将值绑定到名为 userId 的 HTTP 请求标头值。

We’re also binding a list of photos to the body of the HTTP request by using the [FromBody] attribute. This tells the binder to read JSON from the body of the request and bind it to the List<Photo> method parameter.
我们还使用 [FromBody] 属性将照片列表绑定到 HTTP 请求的正文。这会告知 Binder 从请求正文中读取 JSON,并将其绑定到 List<Photo> 方法参数。

Warning Developers coming from .NET Framework and the legacy version of ASP.NET should take note that the [FromBody] attribute is explicitly required when binding to JSON requests in Razor Pages. This differs from the legacy ASP.NET behavior, in which no attribute was required.
警告:来自 .NET Framework 和旧版 ASP.NET 的开发人员应注意,在 Razor Pages 中绑定到 JSON 请求时,显式需要 [FromBody] 属性。这与 legacy ASP.NET 行为不同,后者不需要任何属性。

You aren’t limited to binding JSON data from the request body. You can use other formats too, depending on which InputFormatters you configure the framework to use. By default, only a JSON input formatter is configured. You’ll see how to add an XML formatter in chapter 20, when I discuss web APIs.
您不仅限于从请求正文绑定 JSON 数据。您也可以使用其他格式,具体取决于您配置框架要使用的 InputFormatters。默认情况下,仅配置 JSON 输入格式化程序。在第 20 章中,我将介绍如何添加 XML 格式化程序,届时我将讨论 Web API。

Tip Automatic binding of multiple formats from the request body is one of the features specific to Razor Pages and MVC controllers, which is missing from minimal APIs.
提示:从请求正文自动绑定多种格式是特定于 Razor Pages 和 MVC 控制器的功能之一,而最小 API 中缺少此功能。

You can use a few different attributes to override the defaults and to specify a binding source for each binding model (or each property on the binding model). These are the same attributes you used in chapter 7 with minimal APIs:
可以使用几个不同的属性来覆盖默认值,并为每个绑定模型(或绑定模型上的每个属性)指定绑定源。这些是您在第 7 章中使用的相同属性,具有最少的 API:

• [FromHeader]—Bind to a header value.
• [FromQuery]—Bind to a query string value.
• [FromRoute]—Bind to route parameters.
• [FromForm]—Bind to form data posted in the body of the request. This attribute is not available in minimal APIs.
• [FromBody]—Bind to the request’s body content.

You can apply each of these to any number of handler method parameters or properties, as you saw in listing 16.5, with the exception of the [FromBody] attribute. Only one value may be decorated with the [FromBody] attribute. Also, as form data is sent in the body of a request, the [FromBody] and [FromForm] attributes are effectively mutually exclusive.
您可以将这些参数中的每一个应用于任意数量的处理程序方法参数或属性,如清单 16.5 中所示,但 [FromBody] 属性除外。只能用 [FromBody] 属性修饰一个值。此外,由于表单数据是在请求正文中发送的,因此 [FromBody] 和 [FromForm] 属性实际上是互斥的。

Tip Only one parameter may use the [FromBody] attribute. This attribute consumes the incoming request as HTTP request bodies can be safely read only once.
提示:只有一个参数可以使用 [FromBody] 属性。此属性使用传入请求,因为 HTTP 请求正文只能安全地读取一次。

As well as these attributes for specifying binding sources, there are a few attributes for customizing the binding process even further:
除了这些用于指定绑定源的属性外,还有一些属性可用于进一步自定义绑定过程:

• [BindNever]—The model binder will skip this parameter completely. You can use this attribute to prevent mass assignment, as discussed in these two posts on my blog: http://mng.bz/QvfG and http://mng.bz/Vd90.
[BindNever] - 模型绑定器将完全跳过此参数。您可以使用此属性来防止批量分配,如我博客上的以下两篇文章所述:http://mng.bz/QvfGhttp://mng.bz/Vd90
• [BindRequired]—If the parameter was not provided or was empty, the binder will add a validation error.
[BindRequired] - 如果参数未提供或为空,则 Binder 将添加验证错误。
• [FromServices]—This is used to indicate the parameter should be provided using dependency injection (DI). This attribute isn’t required in most cases, as .NET 7 is smart enough to know that a parameter is a service registered in DI, but you can be explicit if you prefer.
[FromServices] - 这用于指示应使用依赖关系注入 (DI) 提供参数。在大多数情况下,此属性不是必需的,因为 .NET 7 足够智能,可以知道参数是在 DI 中注册的服务,但如果你愿意,可以明确表示。

In addition, you have the [ModelBinder] attribute, which puts you into “God mode” with respect to model binding. With this attribute, you can specify the exact binding source, override the name of the parameter to bind to, and specify the type of binding to perform. It’ll be rare that you need this one, but when you do, at least it’s there!
此外,您还有 [ModelBinder] 属性,该属性将您置于模型绑定的“上帝模式”。使用此属性,您可以指定确切的绑定源,覆盖要绑定到的参数的名称,并指定要执行的绑定类型。你很少需要这个,但当你需要时,至少它就在那里!

By combining all these attributes, you should find you’re able to configure the model binder to bind to pretty much any request data your page handler wants to use. In general, though, you’ll probably find you rarely need to use them; the defaults should work well for you in most cases.
通过组合所有这些属性,您应该能够配置模型 Binders 以绑定到页面处理程序想要使用的几乎所有请求数据。不过,一般来说,您可能会发现您很少需要使用它们;在大多数情况下,默认值应该对您来说效果很好。

That brings us to the end of this section on model binding. At the end of the model binding process, your page handler should have access to a populated binding model, and it’s ready to execute its logic. But before you use that user input for anything, you must always validate your data, which is the focus of the second half of this chapter. Razor Pages automatically does validation for you out-of-the-box, but you have to actually check the results.
这让我们结束了本节关于模型绑定的内容。在模型绑定过程结束时,您的页面处理程序应该可以访问填充的绑定模型,并且它已准备好执行其逻辑。但是,在将该用户输入用于任何作之前,必须始终验证数据,这是本章后半部分的重点。Razor Pages 会自动为你执行开箱即用的验证,但你必须实际检查结果。

16.3 Validating binding models

16.3 验证绑定模型

In this section I discuss how validation works in Razor Pages. You already learned how important it is to validate user input in chapter 7, as well as how you can use DataAnnotation attributes to declaratively describe your validation requirements of a model. In this section you’ll learn how to reuse this knowledge to validate your Razor Page binding models. The good news is that validation is built into the Razor Pages framework.
在本节中,我将讨论验证在 Razor Pages 中的工作原理。您已经在第 7 章中了解了验证用户输入的重要性,以及如何使用 DataAnnotation 属性以声明方式描述模型的验证要求。在本部分中,你将了解如何重复使用此知识来验证 Razor 页面绑定模型。好消息是,验证内置于 Razor Pages 框架中。

16.3.1 Validation in Razor Pages

16.3.1 Razor Pages 中的验证

In chapter 7 you learned that validation is an essential part of any web application. Nevertheless, minimal APIs don’t have any direct support for validation in the framework; you have to layer it on top using filters and additional packages.
在第 7 章中,您了解到验证是任何 Web 应用程序的重要组成部分。尽管如此,最小的 API 在框架中没有任何对验证的直接支持;您必须使用过滤器和附加包将其分层。

In Razor Pages, validation is built in. Validation occurs automatically after model binding but before the page handler executes, as you saw in figure 16.2. Figure 16.6 shows a more compact view of where model validation fits in this process, demonstrating how a request to a checkout page that requests a user’s personal details is bound and validated.
在 Razor Pages 中,验证是内置的。验证在模型绑定之后但在页面处理程序执行之前自动进行,如图 16.2 所示。图 16.6 显示了模型验证在此过程中的适用位置的更紧凑视图,演示了如何绑定和验证对请求用户个人详细信息的结帐页面的请求。

alt text

Figure 16.6 Validation occurs after model binding but before the page handler executes. The page handler executes whether or not validation is successful.
图 16.6 验证发生在模型绑定之后,但在页面处理程序执行之前。无论验证是否成功,页面处理程序都会执行。

As discussed in chapter 7, validation isn’t only about protecting against security threats, it’s also about ensuring that
如第 7 章所述,验证不仅要防止安全威胁,还要确保
• Data is formatted correctly. (Email fields have a valid email format.)
数据格式正确。(电子邮件字段具有有效的电子邮件格式。)
• Numbers are in a particular range. (You can’t buy -1 copies of a product.)
数字在特定范围内。(您不能购买 -1 份产品。)
• Required values are provided while others are optional. (Name may be required, but phone number is optional.)
提供必需值,而其他值为可选值。(姓名可能是必需的,但电话号码是可选的。)
• Values conform to your business requirements. (You can’t convert a currency to itself, it needs to be converted to a different currency.)
值符合您的业务需求。(您无法将货币转换为自身,它需要转换为其他货币。)

It might seem like some of these can be dealt with easily enough in the browser. For example, if a user is selecting a currency to convert to, don’t let them pick the same currency; and we’ve all seen the “please enter a valid email address” messages.
其中一些似乎可以在浏览器中轻松处理。例如,如果用户选择要转换为的货币,请不要让他们选择相同的货币;我们都见过 “Please enter a valid email address” 消息。

Unfortunately, although this client-side validation is useful for users, as it gives them instant feedback, you can never rely on it, as it will always be possible to bypass these browser protections. It’s always necessary to validate the data as it arrives at your web application using server-side validation.
不幸的是,尽管这种客户端验证对用户很有用,因为它为他们提供了即时反馈,但您永远不能依赖它,因为总是可以绕过这些浏览器保护。当数据到达 Web 应用程序时,始终有必要使用服务器端验证来验证数据。

Warning Always validate user input on the server side of your application.
警告:始终在应用程序的服务器端验证用户输入。

If that feels a little redundant, like you’ll be duplicating logic and code between your client and server applications, I’m afraid you’re right. It’s one of the unfortunate aspects of web development; the duplication is a necessary evil. Fortunately, ASP.NET Core provides several features to try to reduce this burden.
如果这感觉有点多余,比如您将在客户端和服务器应用程序之间复制逻辑和代码,那么恐怕您是对的。这是 Web 开发不幸的方面之一;重复是一种必要的邪恶。幸运的是,ASP.NET Core 提供了多项功能来尝试减轻这种负担。

Tip Blazor, the new C# single-page application (SPA) framework, promises to solve some of these problems. For details, see http://mng.bz/9D51 and Blazor in Action, by Chris Sainty (Manning, 2021).
提示:新的 C# 单页应用程序 (SPA) 框架 Blazor 有望解决其中的一些问题。有关详细信息,请参阅 Chris Sainty 的 http://mng.bz/9D51 和 Blazor 的实际应用(Manning,2021 年)。

If you had to write this validation code fresh for every app, it would be tedious and likely error-prone. Luckily, you can use DataAnnotations attributes to declaratively describe the validation requirements for your binding models. The following listing, first shown in chapter 7, shows how you can decorate a binding model with various validation attributes. This expands on the example you saw earlier in listing 16.4.
如果您必须为每个应用程序重新编写此验证代码,这将是乏味的,并且可能容易出错。幸运的是,您可以使用 DataAnnotations 属性以声明方式描述绑定模型的验证要求。下面的清单首先在第 7 章中显示,它显示了如何使用各种验证属性来装饰绑定模型。这扩展了您之前在 Listing 16.4 中看到的示例。

Listing 16.6 Adding DataAnnotations to a binding model to provide metadata
清单 16.6 向绑定模型添加 DataAnnotations 以提供元数据

public class UserBindingModel
{
[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; }
}

❶ 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 is a valid email address
验证 Email 的值是否为有效的电子邮件地址
❺ Validates that the value of PhoneNumber has a valid telephone format
验证 PhoneNumber 的值是否具有有效的电话格式

For validation requirements that don’t lend themselves to attributes, such as when the validity of one property depends on the value of another, you can implement IValidatableObject, as described in chapter 7. Alternatively, you can use a different validation framework, such as FluentValidation, as you’ll see in chapter 32.
对于不适合属性的验证要求,例如当一个属性的有效性取决于另一个属性的值时,您可以实现 IValidatableObject,如第 7 章所述。或者,你可以使用不同的验证框架,比如 FluentValidation,你将在第 32 章中看到。

Whichever validation approach you use, it’s important to remember that these techniques don’t protect your application by themselves. The Razor Pages framework automatically executes the validation code after model binding, but it doesn’t do anything different if validation fails! In the next section we’ll look at how to check the validation result on the server and handle the case where validation has failed.
无论您使用哪种验证方法,请务必记住,这些技术本身并不能保护您的应用程序。Razor Pages 框架会在模型绑定后自动执行验证代码,但如果验证失败,它不会执行任何不同的作!在下一节中,我们将了解如何在服务器上检查验证结果并处理验证失败的情况。

16.3.2 Validating on the server for safety

16.3.2 在服务器上验证安全性

Validation of the binding model occurs before the page handler executes, but note that the handler always executes, whether the validation failed or succeeded. It’s the responsibility of the page handler to check the result of the validation.
绑定模型的验证发生在页面处理程序执行之前,但请注意,无论验证失败还是成功,处理程序始终执行。页面处理程序负责检查验证结果。

NOTE Validation happens automatically, but handling validation failures is the responsibility of the page handler.
注意:验证会自动进行,但处理验证失败是页面处理程序的责任。

The Razor Pages framework stores the output of the validation attempt in a property on the PageModel called ModelState. This property is a ModelStateDictionary object, which contains a list of all the validation errors that occurred after model binding, as well as some utility properties for working with it.
Razor Pages 框架将验证尝试的输出存储在 PageModel 上名为 ModelState 的属性中。此属性是一个 ModelStateDictionary 对象,它包含模型绑定后发生的所有验证错误的列表,以及用于处理它的一些实用程序属性。

As an example, listing 16.7 shows the OnPost page handler for the Checkout.cshtml Razor Page. The Input property is marked for binding and uses the UserBindingModel type shown previously in listing 16.6. This page handler doesn’t do anything with the data currently, but the pattern of checking ModelState early in the method is the key takeaway here.
例如,列表 16.7 显示了 Checkout.cshtml Razor 页面的 OnPost 页面处理程序。Input 属性标记为绑定,并使用前面列表 16.6 中所示的 UserBindingModel 类型。此页面处理程序目前不对数据执行任何作,但在方法的早期检查 ModelState 的模式是这里的关键要点。

Listing 16.7 Checking model state to view the validation result
示例 16.7 检查模型状态以查看验证结果

public class CheckoutModel : PageModel ❶
{
[BindProperty] ❷
public UserBindingModel Input { get; set; } ❷
public IActionResult OnPost() ❸
{
if (!ModelState.IsValid) ❹
{
return Page(); ❺
}
/* Save to the database, update user, return success */ ❻
return RedirectToPage("Success");
}
}

❶ The ModelState property is available on the PageModel base class.
ModelState 属性在 PageModel 基类中可用。
❷ The Input property contains the model-bound data.
Input 属性包含模型绑定数据。
❸ The binding model is validated before the page handler is executed.
在执行页面处理程序之前验证绑定模型。
❹ If there were validation errors, IsValid will be false.
如果存在验证错误,IsValid 将为 false。
❺ Validation failed, so redisplay the form with errors and finish the method early.
验证失败,因此请重新显示有错误的表单并提前完成该方法。
❻ Validation passed, so it’s safe to use the data provided in the model.
验证通过,因此可以安全地使用模型中提供的数据。

If the ModelState property indicates that an error occurred, the method immediately calls the Page() helper method. This returns a PageResult that ultimately generates HTML to return to the user, as you saw in chapter 15. The view uses the (invalid) values provided in the Input property to repopulate the form when it’s displayed, as shown in figure 16.7. Also, helpful messages for the user are added automatically, using the validation errors in the ModelState property.
如果 ModelState 属性指示发生了错误,该方法会立即调用 Page() 帮助程序方法。这将返回一个 PageResult,该结果最终生成 HTML 并返回给用户,如第 15 章所示。视图使用 Input 属性中提供的(无效)值在显示表单时重新填充表单,如图 16.7 所示。此外,将使用 ModelState 属性中的验证错误自动添加对用户有用的消息。

alt text

Figure 16.7 When validation fails, you can redisplay the form to display ModelState validation errors to the user. Note that the Your Name field has no associated validation errors, unlike the other fields.
图 16.7 验证失败时,您可以重新显示表单以向用户显示 ModelState 验证错误。请注意,与其他字段不同,Your Name 字段没有关联的验证错误。

NOTE The error messages displayed on the form are the default values for each validation attribute. You can customize the message by setting the ErrorMessage property on any of the validation attributes. For example, you could customize a [Required] attribute using [Required(ErrorMessage="Required")].
注意表单上显示的错误消息是每个 validation 属性的默认值。您可以通过在任何验证属性上设置 ErrorMessage 属性来自定义消息。例如,您可以使用 [Required(ErrorMessage=“Required”)] 自定义 [Required] 属性。

If the request is successful, the page handler returns a RedirectToPageResult (using the RedirectToPage() helper method) that redirects the user to the Success.cshtml Razor Page. This pattern of returning a redirect response after a successful POST is called the POST-REDIRECT-GET pattern.
如果请求成功,页面处理程序将返回一个 RedirectToPageResult(使用 RedirectToPage() 帮助程序方法),将用户重定向到 Success.cshtml Razor 页面。这种在成功 POST 后返回重定向响应的模式称为 POST-REDIRECT-GET 模式。

POST-REDIRECT-GET
The POST-REDIRECT-GET design pattern is a web development pattern that prevents users from accidentally submitting the same form multiple times. Users typically submit a form using the standard browser POST mechanism, sending data to the server. This is the normal way by which you might take a payment, for example.
POST-REDIRECT-GET 设计模式是一种 Web 开发模式,可防止用户意外地多次提交相同的表单。用户通常使用标准浏览器 POST 机制提交表单,并将数据发送到服务器。例如,这是您可能接受付款的正常方式。

If a server takes the naive approach and responds with a 200 OK response and some HTML to display, the user will still be on the same URL. If the user refreshes their browser, they will be making an additional POST to the server, potentially making another payment! Browsers have some mechanisms to prevent this, such as in the following figure, but the user experience isn’t desirable.
如果服务器采用简单的方法,并以 200 OK 响应和一些要显示的 HTML 进行响应,则用户仍将位于同一 URL 上。如果用户刷新浏览器,他们将向服务器进行额外的 POST,可能会再次付款!浏览器有一些机制可以防止这种情况,如下图所示,但用户体验并不理想。

alt text

Refreshing a browser window after a POST causes a warning message to be shown to the user
在 POST 后刷新浏览器窗口会导致向用户显示警告消息

The POST-REDIRECT-GET pattern says that in response to a successful POST, you should return a REDIRECT response to a new URL, which will be followed by the browser making a GET to the new URL. If the user refreshes their browser now, they’ll be refreshing the final GET call to the new URL. No additional POST is made, so no additional payments or side effects should occur.
POST-REDIRECT-GET 模式表示,为了响应成功的 POST,您应该返回对新 URL 的 REDIRECT 响应,然后浏览器将对新 URL 进行 GET。如果用户现在刷新浏览器,他们将刷新对新 URL 的最终 GET 调用。不会进行额外的 POST,因此不会发生额外的付款或副作用。

This pattern is easy to achieve in ASP.NET Core applications using the pattern shown in listing 16.7. By returning a RedirectToPageResult after a successful POST, your application will be safe if the user refreshes the page in their browser.
在 ASP.NET Core 应用程序中,使用清单 16.7 中所示的模式很容易实现这种模式。通过在成功 POST 后返回 RedirectToPageResult,如果用户在浏览器中刷新页面,您的应用程序将是安全的。

You might be wondering why ASP.NET Core doesn’t handle invalid requests for you automatically; if validation has failed, and you have the result, why does the page handler get executed at all? Isn’t there a risk that you might forget to check the validation result?
您可能想知道为什么 ASP.NET Core 不自动为您处理无效请求;如果验证失败,并且您有结果,为什么还要执行页面处理程序呢?是否有忘记检查验证结果的风险?

This is true, and in some cases the best thing to do is to make the generation of the validation check and response automatic. In fact, this is exactly the approach we will use for web APIs using MVC controllers with the [ApiController] attribute when we cover them in chapter 20.
这是真的,在某些情况下,最好的办法是自动生成验证检查和响应。事实上,这正是我们在第 20 章中介绍时将用于使用带有 [ApiController] 属性的 MVC 控制器的 Web API 的方法。

For Razor Pages apps, however, you typically still want to generate an HTML response, even when validation failed. This allows the user to see the problem and potentially correct it. This is much harder to make automatic.
但是,对于 Razor Pages 应用程序,即使验证失败,您通常仍希望生成 HTML 响应。这样,用户就可以看到问题,并可能纠正问题。这要自动化要困难得多。

For example, you might find you need to load additional data before you can redisplay the Razor Page, such as loading a list of available currencies. That becomes simpler and more explicit with the ModelState.IsValid pattern. Trying to do that automatically would likely end up with you fighting against edge cases and workarounds.
例如,你可能会发现需要先加载其他数据,然后才能重新显示 Razor 页面,例如加载可用货币的列表。使用 ModelState.IsValid 模式,这将变得更简单、更明确。尝试自动执行此作可能最终会让您与边缘情况和解决方法作斗争。

Also, by including the IsValid check explicitly in your page handlers, it’s easier to control what happens when additional validation checks fail. For example, if the user tries to update a product, the DataAnnotation validation won’t know whether a product with the requested ID exists, only whether the ID has the correct format. By moving the validation to the handler method, you can treat data and business rule validation failures in the same way.
此外,通过在页面处理程序中显式包含 IsValid 检查,可以更轻松地控制其他验证检查失败时发生的情况。例如,如果用户尝试更新产品,则 DataAnnotation 验证将不知道具有请求的 ID 的产品是否存在,而只知道 ID 是否具有正确的格式。通过将验证移至处理程序方法,您可以以相同的方式处理数据和业务规则验证失败。

Tip You can also add extra validation errors to the collection, such as business rule validation errors that come from a different system. You can add errors to ModelState by calling AddModelError(), which will be displayed to users on the form alongside the DataAnnotation attribute errors.
提示:您还可以向集合中添加额外的验证错误,例如来自不同系统的业务规则验证错误。您可以通过调用 AddModelError() 向 ModelState 添加错误,该错误将与 DataAnnotation 属性错误一起显示在表单上的用户。

I hope I’ve hammered home how important it is to validate user input in ASP.NET Core, but just in case: VALIDATE! There, we’re good. Having said that, performing validation only on the server can leave users with a slightly poor experience. How many times have you filled out a form online, submitted it, gone to get a snack, and come back to find out you mistyped something and have to redo it? Wouldn’t it be nicer to have that feedback immediately?
我希望我已经清楚地认识到在 ASP.NET Core 中验证用户输入的重要性,但以防万一:验证!好了,我们很好。话虽如此,仅在服务器上执行验证可能会给用户带来略微糟糕的体验。你有多少次在网上填写了一份表格,提交了它,去买了点零食,然后回来发现你打错了东西,不得不重做一遍?立即获得这些反馈不是更好吗?

16.3.3 Validating on the client for user experience

16.3.3 在客户端上验证用户体验

You can add client-side validation to your application in a few different ways. HTML5 has several built-in validation behaviors that many browsers use. If you display an email address field on a page and use the “email” HTML input type, the browser automatically stops you from submitting an invalid format, as shown in figure 16.8. Your application doesn’t control this validation; it’s built into modern HTML5 browsers.
您可以通过几种不同的方式将客户端验证添加到您的应用程序中。HTML5 具有许多浏览器使用的几个内置验证行为。如果您在页面上显示电子邮件地址字段并使用 “email” HTML 输入类型,浏览器会自动阻止您提交无效格式,如图 16.8 所示。您的应用程序不控制此验证;它内置于现代 HTML5 浏览器中。

NOTE HTML5 constraint validation support varies by browser. For details on the available constraints, see the Mozilla documentation (http://mng.bz/daX3) and https://caniuse.com/#feat=constraint-validation.
注意:HTML5 约束验证支持因浏览器而异。有关可用约束的详细信息,请参阅 Mozilla 文档 (http://mng.bz/daX3) 和 https://caniuse.com/#feat=constraint-validation

alt text

Figure 16.8 By default, modern browsers automatically validate fields of the email type before a form is submitted.
图 16.8 默认情况下,现代浏览器会在提交表单之前自动验证电子邮件类型的字段。

The alternative approach to HTML validation is to perform client-side validation by running JavaScript on the page and checking the values the user entered before submitting the form. This is the most common approach used in Razor Pages.
HTML 验证的另一种方法是通过在页面上运行 JavaScript 并在提交表单之前检查用户输入的值来执行客户端验证。这是 Razor Pages 中最常用的方法。

I’ll go into detail on how to generate the client-side validation helpers in chapter 18, where you’ll see the DataAnnotation attributes come to the fore once again. By decorating a view model with these attributes, you provide the necessary metadata to the Razor engine for it to generate the appropriate validation HTML.
在第 18 章中,我将详细介绍如何生成客户端验证帮助程序,届时您将看到 DataAnnotation 属性再次出现。通过使用这些属性修饰视图模型,您可以向 Razor 引擎提供必要的元数据,以便它生成适当的验证 HTML。

With this approach, the user sees any errors with their form immediately, even before the request is sent to the server, as shown in figure 16.9. This gives a much shorter feedback cycle, providing a better user experience.
使用这种方法,用户会立即看到其表单中的任何错误,甚至在请求发送到服务器之前,如图 16.9 所示。这提供了更短的反馈周期,从而提供更好的用户体验。

alt text

Figure 16.9 With client-side validation, clicking Submit triggers validation to be shown in the browser before the request is sent to the server. As shown in the right pane, no request is sent.
图 16.9 使用客户端验证时,单击“提交”会触发验证,在将请求发送到服务器之前,将在浏览器中显示。如右窗格中所示,未发送任何请求。

If you’re building an SPA, the onus is on the client-side framework to validate the data on the client side before posting it to the API. The API must still validate the data when it arrives at the server, but the client-side framework is responsible for providing the smooth user experience.
如果您正在构建 SPA,则客户端框架有责任在将数据发布到 API 之前在客户端验证数据。当数据到达服务器时,API 仍必须验证数据,但客户端框架负责提供流畅的用户体验。

When you use Razor Pages to generate your HTML, you get much of this validation code for free. Razor Pages automatically configures client-side validation for most of the built-in attributes without requiring additional work, as you’ll see in chapter 18. Unfortunately, if you’ve used custom ValidationAttributes, these will run only on the server by default; you need to do some additional wiring up of the attribute to make it work on the client side too. Despite this, custom validation attributes can be useful for handling common validation scenarios in your application, as you’ll see in chapter 31.
使用 Razor Pages 生成 HTML 时,可以免费获得大部分验证代码。Razor Pages 会自动为大多数内置属性配置客户端验证,而无需执行其他工作,如第 18 章所示。遗憾的是,如果你使用了自定义 ValidationAttributes,默认情况下,这些属性将仅在服务器上运行;您需要对 attribute 进行一些额外的连接,使其也可以在 Client 端工作。尽管如此,自定义验证属性对于处理应用程序中的常见验证场景非常有用,如第 31 章所示。

The model binding framework in ASP.NET Core gives you a lot of options on how to organize your Razor Pages: page handler parameters or PageModel properties; one binding model or multiple; options for where to define your binding model classes. In the next section I give some advice on how I like to organize my Razor Pages.
ASP.NET Core 中的模型绑定框架提供了许多有关如何组织 Razor 页面的选项:页面处理程序参数或 PageModel 属性;一个或多个装订模型;用于定义绑定模型类的位置的选项。在下一节中,我将就如何组织我的 Razor 页面提供一些建议。

16.4 Organizing your binding models in Razor Pages

16.4 在 Razor Pages 中组织绑定模型

In this section I give some general advice on how I like to configure the binding models in my Razor Pages. If you follow the patterns in this section, your Razor Pages will follow a consistent layout, making it easier for others to understand how each Razor Page in your app works.
在本节中,我将就如何在 Razor Pages 中配置绑定模型提供一些一般性建议。如果遵循本部分中的模式,则 Razor 页面将遵循一致的布局,使其他人更容易了解应用中的每个 Razor 页面的工作原理。

NOTE This advice is just personal preference, so feel free to adapt it if there are aspects you don’t agree with. The important thing is to understand why I make each suggestion, and to take that on board. Where appropriate, I deviate from these guidelines too!
注意:此建议只是个人喜好,因此如果您有不同意的方面,请随时对其进行调整。重要的是理解我为什么提出每个建议,并采纳它。在适当的情况下,我也会偏离这些准则!

Model binding in ASP.NET Core has a lot of equivalent approaches to take, so there is no “correct” way to do it. Listing 16.8 shows an example of how I would design a simple Razor Page. This Razor Page displays a form for a product with a given ID and allows you to edit the details using a POST request. It’s a much longer sample than we’ve looked at so far, but I highlight the important points.
ASP.NET Core 中的模型绑定有很多等效的方法可供采用,因此没有“正确”的方法。清单 16.8 显示了如何设计一个简单的 Razor Page 的示例。此 Razor 页面显示具有给定 ID 的产品的表单,并允许您使用 POST 请求编辑详细信息。这个样本比我们目前看到的要长得多,但我强调了要点。

Listing 16.8 Designing an edit product Razor Page
清单 16.8 设计编辑产品 Razor 页面

public class EditProductModel : PageModel
{
private readonly ProductService _productService; ❶
public EditProductModel(ProductService productService) ❶
{ ❶
_productService = productService; ❶
} ❶
[BindProperty] ❷
public InputModel Input { get; set; } ❷
public IActionResult OnGet(int id) ❸
{
var product = _productService.GetProduct(id); ❹
Input = new InputModel ❺
{ ❺
Name = product.ProductName, ❺
Price = product.SellPrice, ❺
}; ❺
return Page(); ❺
}
public IActionResult OnPost(int id) ❻
{
if (!ModelState.IsValid) ❼
{ ❼
return Page(); ❼
} ❼
_productService.UpdateProduct(id, Input.Name, Input.Price); ❽
return RedirectToPage("Index"); ❾
}
public class InputModel ❿
{ ❿
[Required] ❿
public string Name { get; set; } ❿
[Range(0, int.MaxValue)] ❿
public decimal Price { get; set; } ❿
} ❿
}

❶ The ProductService is injected using DI and provides access to the application model.
ProductService 使用 DI 注入,并提供对应用程序模型的访问。
❷ A single property is marked with BindProperty.
单个属性使用 BindProperty 进行标记。
❸ The id parameter is model-bound from the route template for both OnGet and OnPost handlers.
id 参数是 OnGet 和 OnPost 处理程序的路由模板的模型绑定的。
❹ Loads the product details from the application model
从应用程序模型加载产品详细信息
❺ Builds an instance of the InputModel for editing in the form from the existing product’s details
构建 InputModel 的实例,以便根据现有产品的详细信息在表单中进行编辑
❻ The id parameter is model-bound from the route template for both OnGet and OnPost handlers.
id 参数与 OnGet 和 OnPost 处理程序的路由模板进行模型绑定。
❼ If the request was not valid, redisplays the form without saving
如果请求无效,则重新显示表单而不保存
❽ Updates the product in the application model using the ProductService
使用 ProductService更新应用程序模型中的产品
❾ Redirects to a new page using the POST-REDIRECT-GET pattern
使用 POST-REDIRECT-GET 模式重定向到新页面
❿ Defines the InputModel as a nested class in the Razor Page
将 InputModel 定义为 Razor 页面中的嵌套类

This page shows the PageModel for a typical “edit form.” These are common in many line-of-business applications, among others, and it’s a scenario that Razor Pages works well for. You’ll see how to create the HTML side of forms in chapter 18.
此页面显示了典型的 “编辑表单” 的 PageModel。这些在许多业务线应用程序中很常见,这是 Razor Pages 非常适合的方案。您将在第 18 章中了解如何创建表单的 HTML 端。

NOTE The purpose of this example is to highlight the model-binding approach. The code is overly simplistic from a logic point of view. For example, it doesn’t check that the product with the provided ID exists or include any error handling.
注意:此示例的目的是强调模型绑定方法。从逻辑的角度来看,代码过于简单。例如,它不会检查具有所提供 ID 的产品是否存在,也不包含任何错误处理。

This form shows several patterns related to model binding that I try to adhere to when building Razor Pages:
此表单显示了我在构建 Razor Pages 时尝试遵循的几种与模型绑定相关的模式:

• Bind only a single property with [BindProperty]. I favor having a single property decorated with [BindProperty] for model binding in general. When more than one value needs to be bound, I create a separate class, InputModel, to hold the values, and I decorate that single property with [BindProperty]. Decorating a single property like this makes it harder to forget to add the attribute, and it means all your Razor Pages use the same pattern.
仅将单个属性与 [BindProperty] 绑定。通常,我赞成使用 [BindProperty] 修饰单个属性以进行模型绑定。当需要绑定多个值时,我创建一个单独的类 InputModel 来保存这些值,并使用 [BindProperty] 修饰该单个属性。像这样修饰单个属性会让人更难忘记添加属性,这意味着你的所有 Razor 页面都使用相同的模式。

• Define your binding model as a nested class. I define the InputModel as a nested class inside my Razor Page. The binding model is normally highly specific to that single page, so doing this keeps everything you’re working on together. Additionally, I normally use that exact class name, InputModel, for all my pages. Again, this adds consistency to your Razor Pages.
将绑定模型定义为嵌套类。我将 InputModel 定义为 Razor Page 中的嵌套类。绑定模型通常高度特定于该单个页面,因此这样做会将您正在处理的所有内容放在一起。此外,我通常对我的所有页面使用该确切的类名 InputModel。同样,这增加了 Razor 页面的一致性。

• Don’t use [BindProperties]. In addition to the [BindProperty] attribute, there is a [BindProperties] attribute (note the different spelling) that can be applied to the Razor Page PageModel directly. This will cause all properties in your model to be model-bound, which can leave you open to overposting attacks if you’re not careful. I suggest you don’t use the [BindProperties] attribute and stick to binding a single property with [BindProperty] instead.
不要使用 [BindProperties]。除了 [BindProperty] 属性之外,还有一个 [BindProperties] 属性(请注意不同的拼写),该属性可以直接应用于 Razor Page PageModel。这将导致模型中的所有属性都受模型限制,如果您不小心,可能会使您面临过度发布攻击。我建议您不要使用 [BindProperties] 属性,而是坚持使用 [BindProperty] 绑定单个属性。

• Accept route parameters in the page handler. For simple route parameters, such as the id passed into the OnGet and OnPost handlers in listing 16.8, I add parameters to the page handler method itself. This avoids the clunky SupportsGet=true syntax for GET requests.
在页面处理程序中接受路由参数。对于简单的路由参数,例如在清单 16.8 中传递给 OnGet 和 OnPost 处理程序的 id,我将参数添加到页面处理程序方法本身。这避免了 GET 请求的笨拙 SupportsGet=true 语法。

• Always validate before using data. I said it before, so I’ll say it again: validate user input!
使用数据之前始终进行验证。我之前说过,所以我再说一遍:验证用户输入!

That concludes this look at model binding in Razor Pages. 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 most important aspect of this chapter is the focus on validation. This is a common concern for all web applications, and the use of DataAnnotations can make it easy to add validation to your models.
Razor Pages 中的模型绑定到此结束。您了解了 ASP.NET Core 框架如何使用模型绑定来简化从请求中提取值并将其转换为可快速使用的普通 .NET 对象的过程。本章最重要的方面是关注验证。这是所有 Web 应用程序的共同关注点,使用 DataAnnotations 可以轻松地向模型添加验证。

In the next chapter we’ll continue our journey through Razor Pages by looking at how to create views. In particular, you’ll learn how to generate HTML in response to a request using the Razor templating engine.
在下一章中,我们将通过了解如何创建视图来继续浏览 Razor 页面。具体而言,您将学习如何使用 Razor 模板引擎生成 HTML 以响应请求。

16.5 Summary

16.5 总结

Razor Pages uses three distinct models, each responsible for a different aspect of a request. The binding model encapsulates data sent as part of a request. The application model represents the state of the application. The PageModel is the backing class for the Razor Page, and it exposes the data used by the Razor view to generate a response.
Razor Pages 使用三种不同的模型,每种模型负责请求的不同方面。绑定模型封装作为请求的一部分发送的数据。应用程序模型表示应用程序的状态。PageModel 是 Razor Page 的支持类,它公开 Razor 视图用于生成响应的数据。

Model binding extracts values from a request and uses them to create .NET objects the page handler can use when they execute. Any properties on the PageModel marked with the [BindProperty] attribute and method parameters of the page handlers will take part in model binding.
模型绑定从请求中提取值,并使用它们创建页面处理程序在执行时可以使用的 .NET 对象。PageModel 上标有 [BindProperty] 属性和页面处理程序的方法参数的任何属性都将参与模型绑定。

By default, there are three binding sources for Razor Pages: POSTed form values, route values, and the query string. The binder will interrogate these sources in order when trying to bind your binding models.
默认情况下,Razor Pages 有三个绑定源:POST 表单值、路由值和查询字符串。Binder 将在尝试绑定 Binding Models 时按顺序询问这些源。

When binding values to models, the names of the parameters and properties aren’t case-sensitive.
将值绑定到模型时,参数和属性的名称不区分大小写。

You can bind to simple types or to the properties of complex types. Simple types must be convertible from strings to be bound automatically, such as numbers, dates, Boolean values, and custom types with a TryParse method.
可以绑定到简单类型或复杂类型的属性。简单类型必须可从字符串转换而来,以便使用 TryParse 方法自动绑定,例如数字、日期、布尔值和自定义类型。

To bind complex types, the types must have a default constructor and public, settable properties. The Razor Pages model binder binds each property of a complex type using values from the binding sources.
若要绑定复杂类型,类型必须具有默认构造函数和公共的可设置属性。Razor Pages 模型绑定器使用绑定源中的值绑定复杂类型的每个属性。

You can bind collections and dictionaries using the [index]=value and [key] =value syntax, respectively.
您可以分别使用 [index]=value 和 [key] =value 语法绑定集合和字典。

You can customize the binding source for a binding model using [From] attributes applied to the method, such as [FromHeader] and [FromBody]. These can be used to bind to nondefault binding sources, such as headers or JSON body content. The [FromBody] attribute is always required when binding to a JSON body.
您可以使用应用于方法的 [From
] 属性(如 [FromHeader] 和 [FromBody])自定义绑定模型的绑定源。这些可用于绑定到非默认绑定源,例如标头或 JSON 正文内容。绑定到 JSON 正文时,始终需要 [FromBody] 属性。

Validation is necessary to check for security threats. Check that data is formatted correctly and confirm that it conforms to expected values and that it meets your business rules.
验证对于检查安全威胁是必要的。检查数据的格式是否正确,并确认它符合预期值以及是否符合您的业务规则。

Validation in Razor Pages occurs automatically after model binding, but you must manually check the result of the validation and act accordingly in your page handler by interrogating the ModelState.IsValid property.
Razor Pages 中的验证在模型绑定后自动进行,但您必须手动检查验证结果,并通过询问 ModelState.IsValid 属性在页面处理程序中采取相应措施。

Client-side validation provides a better user experience than server-side validation alone, but you should always use server-side validation. Client-side validation typically uses JavaScript and attributes applied to your HTML elements to validate form values.
与单独的服务器端验证相比,客户端验证提供了更好的用户体验,但您应该始终使用服务器端验证。客户端验证通常使用应用于 HTML 元素的 JavaScript 和属性来验证表单值。

ASP.NET Core in Action 15 Generating responses with page handlers in Razor Pages

15 Generating responses with page handlers in Razor Pages
15 在 Razor Pages 中使用页面处理程序生成响应

This chapter covers
本章涵盖

• Selecting which page handler in a Razor Page to invoke for a request
选择要为请求调用的 Razor 页面中的页面处理程序
• Returning an IActionResult from a page handler
从页面处理程序返回 IActionResult
• Handling status code errors with StatusCodePagesMiddleware
使用 StatusCodePagesMiddleware 处理状态代码错误

In chapter 14 you learned how the routing system selects a Razor Page to execute based on its associated route template and the request URL, but each Razor Page can have multiple page handlers. In this chapter you’ll learn all about page handlers, their responsibilities, and how a single Razor Page selects which handler to execute for a request.
在第 14 章中,你了解了路由系统如何根据其关联的路由模板和请求 URL 选择要执行的 Razor 页面,但每个 Razor 页面可以有多个页面处理程序。在本章中,您将了解有关页面处理程序、其职责以及单个 Razor 页面如何为请求选择要执行的处理程序的所有信息。

In section 15.3 we look at some of the ways of retrieving values from an HTTP request in a page handler. Much like minimal APIs, page handlers can accept method arguments that are bound to values in the HTTP request, but Razor Pages can also bind the request to properties on the PageModel.
在 Section 15.3 中,我们了解了在页面处理程序中从 HTTP 请求中检索值的一些方法。与最小 API 非常相似,页面处理程序可以接受绑定到 HTTP 请求中值的方法参数,但 Razor Pages 也可以将请求绑定到 PageModel 上的属性。

In section 15.4 you’ll learn how to return IActionResult objects from page handlers. Then you look at some of the common IActionResult types that you’ll return from page handlers for generating HTML and redirect responses.
在第 15.4 节中,您将学习如何从页面处理程序返回 IActionResult 对象。然后,您查看将从页面处理程序返回的一些常见 IActionResult 类型,这些类型用于生成 HTML 和重定向响应。

Finally, in section 15.5 you’ll learn how to use the StatusCodePagesMiddleware to improve the error status code responses in your middleware pipeline. This middleware intercepts error responses such as basic 404 responses and reexecutes the middleware pipeline to generate a pretty HTML response for the error. This gives users a much nicer experience when they encounter an error browsing your Razor Pages app.
最后,在 15.5 节中,您将学习如何使用 StatusCodePagesMiddleware 来改进中间件管道中的错误状态代码响应。此中间件会拦截错误响应(例如基本的 404 响应),并重新执行中间件管道以生成错误的漂亮 HTML 响应。当用户在浏览 Razor Pages 应用程序时遇到错误时,这为他们提供了更好的体验。

We’ll start by taking a quick look at the responsibilities of a page handler before we move on to see how the Razor Page infrastructure selects which page handler to execute.
首先,我们将快速了解页面处理程序的职责,然后再继续了解 Razor Page 基础结构如何选择要执行的页面处理程序。

15.1 Razor Pages and page handlers

15.1 Razor 页面和页面处理程序

In chapter 13 I described the Model-View-Controller (MVC) design pattern and showed how it relates to ASP.NET Core. In this design pattern, the “controller” receives a request and is the entry point for UI generation. For Razor Pages, the entry point is the page handler that resides in a Razor Page’s PageModel. A page handler is a method that runs in response to a request.
在第 13 章中,我介绍了模型-视图-控制器 (MVC) 设计模式,并展示了它与 ASP.NET Core 的关系。在此设计模式中,“控制器” 接收请求,并且是生成 UI 的入口点。对于 Razor 页面,入口点是驻留在 Razor 页面的 PageModel 中的页面处理程序。页面处理程序是为响应请求而运行的方法。

The responsibility of a page handler is generally threefold:
页面处理程序的责任通常有三个方面:

• Confirm that the incoming request is valid.
确认传入请求有效。
• Invoke the appropriate business logic corresponding to the incoming request.
调用与传入请求对应的适当业务逻辑。
• Choose the appropriate kind of response to return.
选择要返回的适当响应类型。

A page handler doesn’t need to perform all these actions, but at the very least it must choose the kind of response to return. Page handlers typically return one of three things:
页面处理程序不需要执行所有这些作,但至少它必须选择要返回的响应类型。页面处理程序通常返回以下三项内容之一:

• A PageResult object—This causes the associated Razor view to generate an HTML response.
PageResult 对象 - 这会导致关联的 Razor 视图生成 HTML 响应。
• Nothing (the handler returns void or Task)—This is the same as the previous case, causing the Razor view to generate an HTML response.
Nothing (处理程序返回 void 或 Task) - 这与前一种情况相同,会导致 Razor 视图生成 HTML 响应。
• A RedirectToPageResult—This indicates that the user should be redirected to a different page in your application.
RedirectToPageResult - 这表示应将用户重定向到应用程序中的其他页面。

These are the most common results for Razor Pages, but I describe some additional options in section 15.4.
这些是 Razor Pages 最常见的结果,但我会在第 15.4 节中介绍一些其他选项。

It’s important to realize that a page handler doesn’t generate a response directly; it selects the type of response and prepares the data for it. For example, returning a PageResult doesn’t generate any HTML at that point; it merely indicates that a view should be rendered. This is in keeping with the MVC design pattern in which it’s the view that generates the response, not the controller.
请务必认识到,页面处理程序不会直接生成响应;它选择响应的类型并为其准备数据。例如,返回 PageResult 此时不会生成任何 HTML;它仅指示应呈现视图。这与 MVC 设计模式一致,在这种模式中,生成响应的是视图,而不是控制器。

Tip The page handler is responsible for choosing what sort of response to send; the view engine in the MVC framework uses the result to generate the response.
提示:页面处理程序负责选择要发送的响应类型;MVC 框架中的视图引擎使用结果生成响应。

It’s also worth bearing in mind that page handlers generally shouldn’t be performing business logic directly. Instead, they should call appropriate services in the application model to handle requests. If a page handler receives a request to add a product to a user’s cart, it shouldn’t manipulate the database or recalculate cart totals directly, for example. Instead, it should make a call to another class to handle the details. This approach of separating concerns ensures that your code stays testable and maintainable as it grows.
还值得记住的是,页面处理程序通常不应直接执行业务逻辑。相反,它们应该调用应用程序模型中的相应服务来处理请求。例如,如果页面处理程序收到将产品添加到用户购物车的请求,则它不应直接作数据库或重新计算购物车总数。相反,它应该调用另一个类来处理细节。这种分离关注点的方法可确保您的代码在增长过程中保持可测试性和可维护性。

15.2 Selecting a page handler to invoke

15.2 选择要调用的页面处理程序

In chapter 14 I said routing is about mapping URLs to an endpoint, which for Razor Pages means a page handler. But I’ve mentioned several times that Razor Pages can contain multiple page handlers. In this section you’ll learn how the EndpointMiddleware selects which page handler to invoke when it executes a Razor Page.
在第 14 章中,我说路由是将 URL 映射到端点,对于 Razor Pages 来说,端点是指页面处理程序。但我已多次提到 Razor Pages 可以包含多个页面处理程序。在本部分中,你将了解 EndpointMiddleware 如何在执行 Razor 页面时选择要调用的页面处理程序。

As you saw in chapter 14, the path of a Razor Page on disk controls the default route template for a Razor Page. For example, the Razor Page at the path Pages/Products/Search.cshtml has a default route template of Products/Search. When a request is received with the URL /products/search, the RoutingMiddleware selects this Razor Page, and the request passes through the middleware pipeline to the EndpointMiddleware. At this point, the EndpointMiddleware must choose which page handler to execute, as shown in figure 15.1.
如第 14 章所示,磁盘上 Razor 页面的路径控制 Razor 页面的默认路由模板。例如,路径 Pages/Products/Search.cshtml 处的 Razor 页面具有 Products/Search 的默认路由模板。当收到 URL 为 /products/search 的请求时,RoutingMiddleware 会选择此 Razor 页面,并且请求通过中间件管道传递到 EndpointMiddleware。此时,EndpointMiddleware 必须选择要执行的页面处理程序,如图 15.1 所示。

alt text

Figure 15.1 The routing middleware selects the Razor Page to execute based on the incoming request URL. Then the endpoint middleware selects the endpoint to execute based on the HTTP verb of the request and the presence (or lack) of a handler route value.
图 15.1 路由中间件根据传入请求 URL 选择要执行的 Razor 页面。然后,端点中间件根据请求的 HTTP 动词和处理程序路由值的存在(或缺失)选择要执行的端点。

Consider the Razor Page SearchModel shown in listing 15.1. This Razor Page has three handlers: OnGet, OnPostAsync, and OnPostCustomSearch. The bodies of the handler methods aren’t shown, as we’re interested only in how the EndpointMiddleware chooses which handler to invoke.
请考虑清单 15.1 中所示的 Razor Page SearchModel。此 Razor 页面有三个处理程序:OnGet、OnPostAsync 和 OnPostCustomSearch。处理程序方法的主体没有显示,因为我们只对 EndpointMiddleware 如何选择要调用的处理程序感兴趣。

Listing 15.1 Razor Page with multiple page handlers
列表 15.1 具有多个页面处理程序的 Razor Page

public class SearchModel : PageModel
{
public void OnGet() ❶
{
// Handler implementation
}
public Task OnPostAsync() ❷
{
// Handler implementation
}
public void OnPostCustomSearch() ❸
{
// Handler implementation
}
}

❶ Handles GET requests
处理 GET 请求
❷ Handles POST requests. The async suffix is optional and is ignored for routing purposes.
处理 POST 请求。async 后缀是可选的,出于路由目的而被忽略。
❸ Handles POST requests where the handler route value has the value CustomSearch
处理处理程序路由值值为 CustomSearch 的 POST 请求

Razor Pages can contain any number of page handlers, but only one runs in response to a given request. When the EndpointMiddleware executes a selected Razor Page, it selects a page handler to invoke based on two variables:
Razor Pages 可以包含任意数量的页面处理程序,但只有一个处理程序运行以响应给定请求。当 EndpointMiddleware 执行选定的 Razor 页面时,它会根据两个变量选择要调用的页面处理程序:

• The HTTP verb used in the request (such as GET, POST, or DELETE)
请求中使用的 HTTP 动词 (如 GET、POST 或 DELETE)
• The value of the handler route value
处理程序路由值的值

The handler route value typically comes from a query string value in the request URL, such as /Search?handler=CustomSearch. If you don’t like the look of query strings (I don’t!), you can include the {handler} route parameter in your Razor Page’s route template. For the Search page model in listing 15.2, you could update the page’s directive to
处理程序路由值通常来自请求 URL 中的查询字符串值,例如 /Search?handler=CustomSearch。如果您不喜欢查询字符串的外观(我不喜欢),则可以在 Razor Page 的路由模板中包含 {handler} 路由参数。对于清单 15.2 中的 Search 页面模型,您可以将页面的指令更新为

@page "{handler?}"

This would give a complete route template something like "Search/{handler?}", which would match URLs such as /Search and /Search/CustomSearch.
这将提供一个完整的路由模板,类似于 “Search/{handler?}”,它将匹配 /Search 和 /Search/CustomSearch 等 URL。

The EndpointMiddleware uses the handler route value and the HTTP verb together with a standard naming convention to identify which page handler to execute, as shown in figure 15.2. The handler parameter is optional and is typically provided as part of the request’s query string or as a route parameter, as described earlier. The async suffix is also optional and is often used when the handler uses asynchronous programming constructs such as Task or async/await.
EndpointMiddleware 使用处理程序路由值和 HTTP 动词以及标准命名约定来标识要执行的页面处理程序,如图 15.2 所示。handler 参数是可选的,通常作为请求的查询字符串的一部分或作为路由参数提供,如前所述。async 后缀也是可选的,当处理程序使用异步编程构造(如 Task 或 async/await)时,通常会使用该后缀。

alt text

Figure 15.2 Razor Page handlers are matched to a request based on the HTTP verb and the optional handler parameter.
图 15.2 Razor Page 处理程序根据 HTTP 谓词和可选的 handler 参数与请求匹配。

NOTE The async suffix naming convention is suggested by Microsoft, though it is unpopular with some developers. NServiceBus provides a reasoned argument against it here (along with Microsoft’s advice): http://mng.bz/e59P.
注意:async 后缀命名约定由 Microsoft 建议,尽管它在某些开发人员中并不受欢迎。NServiceBus 在这里提供了一个合理的反对它的理由(以及 Microsoft 的建议):http://mng.bz/e59P

Based on this convention, we can now identify what type of request each page handler in listing 15.1 corresponds to:
基于这个约定,我们现在可以确定清单 15.1 中的每个页面处理程序对应于什么类型的请求:

• OnGet—Invoked for GET requests that don’t specify a handler value
OnGet - 针对未指定处理程序值的GET 请求调用
• OnPostAsync—Invoked for POST requests that don’t specify a handler value; returns a Task, so it uses the Async suffix, which is ignored for routing purposes
OnPostAsync - 针对未指定处理程序值的 POST 请求调用;返回一个 Task,因此它使用 Async 后缀,该后缀在路由时被忽略
• OnPostCustomSearch—Invoked for POST requests that specify a handler value of "CustomSearch"
OnPostCustomSearch - 为指定处理程序值“CustomSearch”的 POST 请求调用

The Razor Page in listing 15.1 specifies three handlers, so it can handle only three verb-handler pairs. But what happens if you get a request that doesn’t match these, such as a request using the DELETE verb, a GET request with a nonblank handler value, or a POST request with an unrecognized handler value?
清单 15.1 中的 Razor Page 指定了三个处理程序,因此它只能处理三个谓词处理程序对。但是,如果您收到与这些不匹配的请求,例如使用 DELETE 动词的请求、具有非空处理程序值的 GET 请求或具有无法识别的处理程序值的 POST 请求,会发生什么情况?

For all these cases, the EndpointMiddleware executes an implicit page handler instead. Implicit page handlers contain no logic; they simply render the Razor view. For example, if you sent a DELETE request to the Razor Page in listing 15.1, the EndpointMiddleware would execute an implicit handler. The implicit page handler is equivalent to the following handler code:
对于所有这些情况,EndpointMiddleware 都会执行隐式页面处理程序。隐式页面处理程序不包含任何逻辑;它们只呈现 Razor 视图。例如,如果向清单 15.1 中的 Razor 页面发送了 DELETE 请求,则 EndpointMiddleware 将执行隐式处理程序。隐式页面处理程序等效于以下处理程序代码:

public void OnDelete() { }

DEFINITION If a page handler does not match a request’s HTTP verb and handler value, an implicit page handler is executed that renders the associated Razor view. Implicit page handlers take part in model binding and use page filters but execute no logic.
定义:如果页面处理程序与请求的 HTTP 谓词和处理程序值不匹配,则会执行一个隐式页面处理程序,以呈现关联的 Razor 视图。隐式页面处理程序参与模型绑定并使用页面过滤器,但不执行逻辑。

There’s one exception to the implicit page handler rule: if a request uses the HEAD verb, and there is no corresponding OnHead handler, the EndpointMiddleware executes the OnGet handler instead (if it exists).
隐式页面处理程序规则有一个例外:如果请求使用 HEAD 动词,并且没有相应的 OnHead 处理程序,则 EndpointMiddleware 将改为执行 OnGet 处理程序(如果存在)。

NOTE HEAD requests are typically sent automatically by the browser and don’t return a response body. They’re often used for security purposes, as you’ll see in chapter 28.
注意:HEAD 请求通常由浏览器自动发送,不会返回响应正文。它们通常用于安全目的,如第 28 章所示。

Now that you know how a page handler is selected, you can think about how it’s executed.
现在,您知道了如何选择页面处理程序,您可以考虑如何执行它。

15.3 Accepting parameters to page handlers

15.3 接受页面处理程序的参数

In chapter 7 you learned about the intricacies of model binding in minimal API endpoint handlers. Like minimal APIs, Razor Page page handlers can use model binding to easily extract values from the request. You’ll learn the details of Razor Page model binding in chapter 16; in this section you’ll learn about the basic mechanics of Razor Page model binding and the basic options available.
在第 7 章中,您了解了最小 API 端点处理程序中模型绑定的复杂性。与最小 API 一样,Razor Page 页面处理程序可以使用模型绑定轻松地从请求中提取值。您将在第 16 章中了解 Razor Page 模型绑定的详细信息;在本节中,您将了解 Razor Page 模型绑定的基本机制和可用的基本选项。

When working with Razor Pages, you’ll often want to extract values from an incoming request. If the request is for a search page, the request might contain the search term and the page number in the query string. If the request is POSTing a form to your application, such as a user logging in with their username and password, those values may be encoded in the request body. In other cases, there will be no values, such as when a user requests the home page for your application.
使用 Razor Pages 时,通常需要从传入请求中提取值。如果请求针对搜索页面,则请求可能包含查询字符串中的搜索词和页码。如果请求将表单 POST 到您的应用程序,例如用户使用其用户名和密码登录,则这些值可能会在请求正文中编码。在其他情况下,将没有值,例如当用户请求应用程序的主页时。

DEFINITION The process of extracting values from a request and converting them to .NET types is called model binding. I discuss model binding for Razor Pages in detail in chapter 16.
定义:从请求中提取值并将其转换为 .NET 类型的过程称为模型绑定。我在第 16 章中详细讨论了 Razor Pages 的模型绑定。

ASP.NET Core can bind two different targets in Razor Pages:
ASP.NET Core 可以在 Razor Pages 中绑定两个不同的目标:
• Method arguments—If a page handler has method parameters, the arguments are bound and created from values in the request.
方法参数 - 如果页面处理程序具有方法参数,则根据请求中的值绑定和创建参数。
• Properties marked with a [BindProperty] attribute—Any properties on the PageModel marked with this attribute are bound to the request. By default, this attribute does nothing for GET requests.
标有 [BindProperty] 属性的属性 - PageModel 上标有此属性的任何属性都将绑定到请求。默认情况下,此属性对 GET 请求不执行任何作。

Model-bound values can be simple types, such as strings and integers, or they can be complex types, as shown in the following listing. If any of the values provided in the request are not bound to a property or page handler argument, the additional values will go unused.
模型绑定值可以是简单类型,例如字符串和整数,也可以是复杂类型,如下面的清单所示。如果请求中提供的任何值未绑定到 property 或 page handler 参数,则其他值将未使用。

Listing 15.2 Example Razor Page handlers
列表 15.2 示例 Razor Page 处理程序

public class SearchModel : PageModel
{
private readonly SearchService _searchService; ❶
public SearchModel(SearchService searchService) ❶
{ ❶
_searchService = searchService; ❶
} ❶
[BindProperty] ❷
public BindingModel Input { get; set; } ❷
public List<Product> Results { get; set; } ❸
public void OnGet() ❹
{ ❹
} ❹
public IActionResult OnPost(int max) ❺
{
    if (ModelState.IsValid) ❻
{ ❻
Results = _searchService.Search(Input.SearchTerm, max); ❻
return Page(); ❻
} ❻
return RedirectToPage("./Index"); ❻
}
}

❶ The SearchService is injected from DI for use in page handlers.
SearchService 从 DI 注入,用于页面处理程序。
❷ Properties decorated with the [BindProperty] attribute are model-bound.
使用 [BindProperty] 属性修饰的属性是模型绑定的。
❸ Undecorated properties are not model-bound.
未修饰的属性不受模型限制。
❹ The page handler doesn’t need to check if the model is valid. Returning void renders the view.
页面处理程序不需要检查模型是否有效。返回 void 将呈现视图。
❺ The max parameter is model-bound using values in the request.
max 参数使用请求中的值进行模型绑定。
❻ If the request was not valid, the method indicates the user should be redirected to the Index page.
如果请求无效,该方法指示应将用户重定向到 Index 页面。

In this example, the OnGet handler doesn’t require any parameters, and the method is simple: it returns void, which means the associated Razor view will be rendered. It could also have returned a PageResult; the effect would have been the same. Note that this handler is for HTTP GET requests, so the Input property decorated with [BindProperty] is not bound.
在此示例中,OnGet 处理程序不需要任何参数,方法很简单:它返回 void,这意味着将呈现关联的 Razor 视图。它还可能返回 PageResult;效果是一样的。请注意,此处理程序用于 HTTP GET 请求,因此不会绑定用 [BindProperty] 修饰的 Input 属性。

Tip To bind properties for GET requests too, use the SupportsGet property of the attribute, as in [BindProperty(SupportsGet = true)].
提示:若要同时绑定 GET 请求的属性,请使用该属性的 SupportsGet 属性,如 [BindProperty(SupportsGet = true)] 中所示。

The OnPost handler, conversely, accepts a parameter max as an argument. In this case it’s a simple type, int, but it could also be a complex object. Additionally, as this handler corresponds to an HTTP POST request, the Input property is also model-bound to the request.
相反,OnPost 处理程序接受参数 max 作为参数。在本例中,它是一个简单类型 int,但它也可能是一个复杂对象。此外,由于此处理程序对应于 HTTP POST 请求,因此 Input 属性也与请求模型绑定。

NOTE Unlike most .NET classes, you can’t use method overloading to have multiple page handlers on a Razor Page with the same name.
注意:与大多数 .NET 类不同,不能使用方法重载在 Razor 页面上拥有多个同名页面处理程序。

When a page handler uses model-bound properties or parameters, it should always check that the provided model is valid using ModelState.IsValid. The ModelState property is exposed as a property on the base PageModel class and can be used to check that all the bound properties and parameters are valid. You’ll see how the process works in chapter 16 when you learn about validation.
当页面处理程序使用模型绑定属性或参数时,它应始终使用 ModelState.IsValid 检查提供的模型是否有效。ModelState 属性作为基 PageModel 类上的属性公开,可用于检查所有绑定的属性和参数是否有效。当您了解验证时,您将在第 16 章中看到该过程的工作原理。

Once a page handler establishes that the arguments provided to a page handler method are valid, it can execute the appropriate business logic and handle the request. In the case of the OnPost handler, this involves calling the injected SearchService and setting the result on the Results property. Finally, the handler returns a PageResult by calling the helper method on the PageModel base class:
一旦页面处理程序确定提供给页面处理程序方法的参数有效,它就可以执行相应的业务逻辑并处理请求。对于 OnPost 处理程序,这涉及调用注入的 SearchService 并在 Results 属性上设置结果。最后,处理程序通过调用 PageModel 基类上的帮助程序方法返回 PageResult:

return Page();

If the model isn’t valid, as indicated by ModelState.IsValid, you don’t have any results to display! In this example, the action returns a RedirectToPageResult using the RedirectToPage() helper method. When executed, this result sends a 302 Redirect response to the user, which will cause their browser to navigate to the Index Razor Page.
如果模型无效(如 ModelState.IsValid 所示),则没有任何结果可显示!在此示例中,该作使用 RedirectToPage() 帮助程序方法返回 RedirectToPageResult。执行时,此结果会向用户发送 302 Redirect 响应,这将导致其浏览器导航到 Index Razor 页面。

Note that the OnGet method returns void in the method signature, whereas the OnPost method returns an IActionResult. This is required in the OnPost method to allow the C# to compile (as the Page() and RedirectToPage() helper methods return different types), but it doesn’t change the final behavior of the methods. You could easily have called Page() in the OnGet method and returned an IActionResult, and the behavior would be identical.
请注意,OnGet 方法在方法签名中返回 void,而 OnPost 方法返回 IActionResult。这在 OnPost 方法中是必需的,以允许 C# 进行编译(因为 Page() 和 RedirectToPage() 帮助程序方法返回不同的类型),但它不会更改方法的最终行为。您可以轻松地在 OnGet 方法中调用 Page() 并返回 IActionResult,并且行为是相同的。

Tip If you’re returning more than one type of result from a page handler, you’ll need to ensure that your method returns an IActionResult.
提示:如果要从页面处理程序返回多种类型的结果,则需要确保方法返回 IActionResult。

In listing 15.2 I used Page() and RedirectToPage() methods to generate the return value. IActionResult instances can be created and returned using the normal new syntax of C#:
在清单 15.2 中,我使用了 Page() 和 RedirectToPage() 方法来生成返回值。可以使用 C# 的常规新语法创建和返回 IActionResult 实例:

return new PageResult()

However, the Razor Pages PageModel base class also provides several helper methods for generating responses, which are thin wrappers around the new syntax. It’s common to use the Page() method to generate an appropriate PageResult, the RedirectToPage() method to generate a RedirectToPageResult, or the NotFound() method to generate a NotFoundResult.
但是,Razor Pages PageModel 基类还提供了多个用于生成响应的帮助程序方法,这些方法是新语法的精简包装器。通常使用 Page() 方法生成相应的 PageResult,使用 RedirectToPage() 方法生成 RedirectToPageResult,或使用 NotFound() 方法生成 NotFoundResult。

Tip Most IActionResult implementations have a helper method on the base PageModel class. They’re typically named Type, and the result generated is called TypeResult. For example, the StatusCode() method returns a StatusCodeResult instance.
提示:大多数 IActionResult 实现在基 PageModel 类上都有一个帮助程序方法。它们通常命名为 Type,生成的结果称为 TypeResult。例如,StatusCode() 方法返回 StatusCodeResult 实例。

In the next section we’ll look in more depth at some of the common IActionResult types.
在下一节中,我们将更深入地了解一些常见的 IActionResult 类型。

15.4 Returning IActionResult responses

15.4 返回 IActionResult 响应

In the previous section, I emphasized that page handlers decide what type of response to return, but they don’t generate the response themselves. It’s the IActionResult returned by a page handler that, when executed by the Razor Pages infrastructure using the view engine, generates the response.
在上一节中,我强调了页面处理程序决定返回哪种类型的响应,但它们自己不会生成响应。页面处理程序返回的 IActionResult 在由使用视图引擎的 Razor Pages 基础结构执行时,会生成响应。

Warning Note that the interface type is IActionResult not IResult. IResult is used in minimal APIs and should generally be avoided in Razor Pages (and MVC controllers). In .NET 7, IResult types returned from Razor Pages or MVC controllers execute as expected, but they don’t have all the same features as IActionResult, so you should favor IActionResult in Razor Pages.
警告:请注意,接口类型是 IActionResult 而不是 IResult。IResult 用于最小的 API,通常应避免在 Razor Pages(和 MVC 控制器)中使用。在 .NET 7 中,从 Razor Pages 或 MVC 控制器返回的 IResult 类型按预期执行,但它们不具有与 IActionResult 相同的所有功能,因此你应该在 Razor Pages 中首选 IActionResult。

IActionResults are a key part of the MVC design pattern. They separate the decision of what sort of response to send from the generation of the response. This allows you to test your action method logic to confirm that the right sort of response is sent for a given input. You can then separately test that a given IActionResult generates the expected HTML, for example.
IActionResults 是 MVC 设计模式的关键部分。它们将发送哪种响应的决定与响应的生成分开。这允许您测试作方法逻辑,以确认为给定输入发送了正确类型的响应。例如,您可以单独测试给定的 IActionResult 是否生成了预期的 HTML。

ASP.NET Core has many types of IActionResult, such as
ASP.NET Core 具有多种类型的 IActionResult,例如

• PageResult—Generates an HTML view for the associated page in Razor Pages and returns a 200 HTTP response.
PageResult - 在 Razor Pages 中为关联页面生成 HTML 视图,并返回 200 HTTP 响应。
• ViewResult—Generates an HTML view for a given Razor view when using MVC controllers and returns a 200 HTTP response.
ViewResult - 使用 MVC 控制器时,为给定 Razor 视图生成 HTML 视图,并返回 200 HTTP 响应。
• PartialViewResult—Renders part of an HTML page using a given Razor view and returns a 200 HTTP result; typically used with MVC controllers and AJAX requests.
PartialViewResult - 使用给定的 Razor 视图呈现 HTML 页面的一部分,并返回 200 HTTP 结果;通常用于 MVC 控制器和 AJAX 请求。
• RedirectToPageResult—Sends a 302 HTTP redirect response to automatically send a user to another page.
RedirectToPageResult - 发送 302 HTTP 重定向响应以自动将用户发送到其他页面。
• RedirectResult—Sends a 302 HTTP redirect response to automatically send a user to a specified URL (doesn’t have to be a Razor Page).
RedirectResult - 发送 302 HTTP 重定向响应以自动将用户发送到指定的 URL (不必是 Razor 页面)。
• FileResult—Returns a file as the response. This is a base class with several derived types:
FileResult - 返回文件作为响应。这是一个具有多个派生类型的基类:
• • FileContentResult—Returns a byte[] as a file response to the browser
FileContentResult - 返回 byte[] 作为对浏览器的文件响应
• • FileStreamResult—Returns the contents of a Stream as a file response to the browser
FileStreamResult - 将 Stream 的内容作为文件响应返回给浏览器
• • PhysicalFileResult—Returns the contents of a file on disk as a file response to the browser
PhysicalFileResult - 将磁盘上文件的内容作为文件响应返回给浏览器
• ContentResult—Returns a provided string as the response.
ContentResult - 返回提供的字符串作为响应。
• StatusCodeResult—Sends a raw HTTP status code as the response, optionally with associated response body content.
StatusCodeResult - 发送原始 HTTP 状态代码作为响应,可选择发送关联的响应正文内容。
• NotFoundResult—Sends a raw 404 HTTP status code as the response.
NotFoundResult - 发送原始 404 HTTP 状态代码作为响应。

Each of these, when executed by Razor Pages, generates a response to send back through the middleware pipeline and out to the user.
当 Razor Pages 执行时,每个作都会生成一个响应,以通过中间件管道发送回给用户。

Tip When you’re using Razor Pages, you generally won’t use some of these action results, such as ContentResult and StatusCodeResult. It’s good to be aware of them, though, as you will likely use them if you are building Web APIs with MVC controllers, as you’ll see in chapter 20.
提示:使用 Razor Pages 时,通常不会使用其中一些作结果,例如 ContentResult 和 StatusCodeResult。不过,了解它们是件好事,因为如果你正在使用 MVC 控制器构建 Web API,你可能会使用它们,如第 20 章所示。

In sections 15.4.1–15.4.3 I give a brief description of the most common IActionResult types that you’ll use with Razor Pages.
在第 15.4.1–15.4.3 节中,我简要介绍了您将用于 Razor Pages 的最常见 IActionResult 类型。

15.4.1 PageResult and RedirectToPageResult

15.4.1 PageResult 和 RedirectToPageResult

When you’re building a traditional web application with Razor Pages, usually you’ll be using PageResult, which generates an HTML response from the Razor Page’s associated Razor view. We’ll look at how this happens in detail in chapter 17.
使用 Razor Pages 构建传统 Web 应用程序时,通常会使用 PageResult,它会从 Razor Page 的关联 Razor 视图生成 HTML 响应。我们将在第 17 章详细看看这是如何发生的。

You’ll also commonly use the various redirect-based results to send the user to a new web page. For example, when you place an order on an e-commerce website, you typically navigate through multiple pages, as shown in figure 15.3. The web application sends HTTP redirects whenever it needs you to move to a different page, such as when a user submits a form. Your browser automatically follows the redirect requests, creating a seamless flow through the checkout process.
您通常还会使用各种基于重定向的结果将用户发送到新网页。例如,当您在电子商务网站上下订单时,通常会浏览多个页面,如图 15.3 所示。每当 Web 应用程序需要您移动到其他页面时(例如,当用户提交表单时),它都会发送 HTTP 重定向。您的浏览器会自动遵循重定向请求,从而在结帐过程中创建无缝流程。

alt text

Figure 15.3 A typical POST, REDIRECT, GET flow through a website. A user sends their shopping basket to a checkout page, which validates its contents and redirects to a payment page without the user’s having to change the URL manually.
图 15.3 典型的 POST、REDIRECT、GET 流经网站。用户将他们的购物篮发送到结帐页面,该页面会验证其内容并重定向到支付页面,而无需用户手动更改 URL。

In this flow, whenever you return HTML you use a PageResult; when you redirect to a new page, you use a RedirectToPageResult.
在此流程中,无论何时返回 HTML,您都会使用 PageResult;当您重定向到新页面时,您将使用 RedirectToPageResult。

Tip Razor Pages are generally designed to be stateless, so if you want to persist data between multiple pages, you need to place it in a database or similar store. If you want to store data for a single request, you may be able to use TempData, which stores small amounts of data in cookies for a single request. See the documentation for details: http://mng.bz/XdXp.
提示:Razor 页面通常设计为无状态的,因此如果要在多个页面之间保存数据,则需要将其放置在数据库或类似存储中。如果要存储单个请求的数据,则可以使用 TempData,它将少量数据存储在单个请求的 Cookie 中。有关详细信息,请参阅文档:http://mng.bz/XdXp

15.4.2 NotFoundResult and StatusCodeResult

15.4.2 NotFoundResult 和 StatusCodeResult

As well as sending HTML and redirect responses, you’ll occasionally need to send specific HTTP status codes. If you request a page for viewing a product on an e-commerce application, and that product doesn’t exist, a 404 HTTP status code is returned to the browser, and you’ll typically see a “Not found” web page. Razor Pages can achieve this behavior by returning a NotFoundResult, which returns a raw 404 HTTP status code. You could achieve a similar result using StatusCodeResult and setting the status code returned explicitly to 404.
除了发送 HTML 和重定向响应外,您有时还需要发送特定的 HTTP 状态代码。如果您请求一个页面来查看电子商务应用程序上的产品,但该产品不存在,则会向浏览器返回 404 HTTP 状态代码,并且您通常会看到“未找到”网页。Razor Pages 可以通过返回 NotFoundResult 来实现此行为,该结果返回原始 404 HTTP 状态代码。您可以使用 StatusCodeResult 并将返回的状态代码显式设置为 404 来实现类似的结果。

Note that NotFoundResult doesn’t generate any HTML; it only generates a raw 404 status code and returns it through the middleware pipeline. This generally isn’t a great user experience, as the browser typically displays a default page, such as that shown in figure 15.4.
请注意,NotFoundResult 不会生成任何 HTML;它只生成一个原始的 404 状态码,并通过中间件管道返回。这通常不是很好的用户体验,因为浏览器通常会显示默认页面,如图 15.4 所示。

alt text

Figure 15.4 If you return a raw 404 status code without any HTML, the browser will render a generic default page instead. The message is of limited utility to users and may leave many of them confused or thinking that your web application is broken.
图 15.4 如果返回不带任何 HTML 的原始 404 状态代码,浏览器将呈现通用的默认页面。该消息对用户的实用性有限,可能会让许多人感到困惑或认为您的 Web 应用程序已损坏。

Returning raw status codes is fine when you’re building an API, but for a Razor Pages application, this is rarely good enough. In section 15.5 you’ll learn how you can intercept this raw 404 status code after it’s been generated and provide a user-friendly HTML response for it instead.
在构建 API 时,返回原始状态代码是可以的,但对于 Razor Pages 应用程序,这很少足够好。在 Section 15.5 中,您将学习如何在生成原始 404 状态代码后拦截它,并为其提供用户友好的 HTML 响应。

15.5 Handler status codes with StatusCodePagesMiddleware

15.5 使用 StatusCodePagesMiddleware 的处理程序状态码

In chapter 4 we discussed error handling middleware, which is designed to catch exceptions generated anywhere in your middleware pipeline, catch them, and generate a user-friendly response. In this section you’ll learn about an analogous piece of middleware that intercepts error HTTP status codes: StatusCodePagesMiddleware.
在第 4 章中,我们讨论了错误处理中间件,它旨在捕获中间件管道中任何位置生成的异常,捕获它们,并生成用户友好的响应。在本节中,您将了解一个类似的中间件,用于拦截错误 HTTP 状态代码:StatusCodePagesMiddleware。

Your Razor Pages application can return a wide range of HTTP status codes that indicate some sort of error state. You’ve seen previously that a 500 “server error” is sent when an exception occurs and isn’t handled and that a 404 “file not found” error is sent when you return a NotFoundResult from a page handler. 404 errors are particularly common, often occurring when a user enters an invalid URL.
Razor Pages 应用程序可以返回各种 HTTP 状态代码,这些代码指示某种错误状态。您之前已经看到,当发生异常且未得到处理时,会发送 500 “server error” ,当您从页面处理程序返回 NotFoundResult 时,会发送 404 “file not found” 错误。404 错误特别常见,通常在用户输入无效的 URL 时发生。

Tip 404 errors are often used to indicate that a specific requested object was not found. For example, a request for the details of a product with an ID of 23 might return a 404 if no such product exists. They’re also generated automatically if no endpoint in your application matches the request URL.
Tip: 404 错误通常用于指示未找到特定请求的对象。例如,如果不存在 ID 为 23 的产品的详细信息,则请求 ID 为 23 的产品可能会返回 404。如果应用程序中没有终端节点与请求 URL 匹配,系统也会自动生成这些 URL。

Returning “raw” status codes without additional content is generally OK if you’re building a minimal API or web API application. But as mentioned before, for apps consumed directly by users such as Razor Pages apps, this can result in a poor user experience. If you don’t handle these status codes, users will see a generic error page, as you saw in figure 15.4, which may leave many confused users thinking your application is broken. A better approach is to handle these error codes and return an error page that’s in keeping with the rest of your application or at least doesn’t make your application look broken.
如果您正在构建最小的 API 或 Web API 应用程序,则返回不带额外内容的“原始”状态代码通常是可以的。但如前所述,对于用户直接使用的应用程序(如 Razor Pages 应用程序),这可能会导致用户体验不佳。如果你不处理这些状态码,用户将看到一个通用的错误页面,如图 15.4 所示,这可能会让许多困惑的用户认为你的应用程序坏了。更好的方法是处理这些错误代码并返回一个错误页面,该页面与应用程序的其余部分保持一致,或者至少不会使您的应用程序看起来损坏。

Microsoft provides StatusCodePagesMiddleware for handling this use case. As with all error handling middleware, you should add it early in your middleware pipeline, as it will handle only errors generated by later middleware components.
Microsoft 提供了 StatusCodePagesMiddleware 来处理此用例。与所有错误处理中间件一样,您应该在中间件管道的早期添加它,因为它将仅处理后续中间件组件生成的错误。

You can use the middleware several ways in your application. The simplest approach is to add the middleware to your pipeline without any additional configuration, using
您可以在应用程序中以多种方式使用中间件。最简单的方法是将中间件添加到您的管道中,无需任何其他配置,使用

app.UseStatusCodePages();

With this method, the middleware intercepts any response that has an HTTP status code that starts with 4xx or 5xx and has no response body. For the simplest case, where you don’t provide any additional configuration, the middleware adds a plain-text response body, indicating the type and name of the response, as shown in figure 15.5. This is arguably worse than the default message at this point, but it is a starting point for providing a more consistent experience to users.
使用此方法,中间件会拦截 HTTP 状态代码以 4xx 或 5xx 开头且没有响应正文的任何响应。对于最简单的情况,如果您不提供任何其他配置,中间件会添加一个纯文本响应正文,指示响应的类型和名称,如图 15.5 所示。这可以说比此时的默认消息更糟糕,但它是为用户提供更一致体验的起点。

alt text

Figure 15.5 Status code error page for a 404 error. You generally won’t use this version of the middleware in production, as it doesn’t provide a great user experience, but it demonstrates that the error codes are being intercepted correctly.
图 15.5 404 错误的状态代码错误页面。您通常不会在生产环境中使用此版本的中间件,因为它不会提供出色的用户体验,但它表明错误代码被正确拦截。

A more typical approach to using StatusCodePagesMiddleware in production is to reexecute the pipeline when an error is captured, using a similar technique to the ExceptionHandlerMiddleware. This allows you to have dynamic error pages that fit with the rest of your application. To use this technique, replace the call to UseStatusCodePages with the following extension method:
在生产环境中使用 StatusCodePagesMiddleware 的更典型方法是在捕获错误时重新执行管道,使用与 ExceptionHandlerMiddleware 类似的技术。这允许你拥有适合应用程序其余部分的动态错误页面。若要使用此技术,请将对 UseStatusCodePages 的调用替换为以下扩展方法:

app.UseStatusCodePagesWithReExecute("/{0}");

This extension method configures StatusCodePagesMiddleware to reexecute the pipeline whenever a 4xx or 5xx response code is found, using the provided error handling path. This is similar to the way ExceptionHandlerMiddleware reexecutes the pipeline, as shown in figure 15.6.
此扩展方法将 StatusCodePagesMiddleware 配置为在找到 4xx 或 5xx 响应代码时,使用提供的错误处理路径重新执行管道。这类似于 ExceptionHandlerMiddleware 重新执行管道的方式,如图 15.6 所示。

alt text

Figure 15.6 StatusCodePagesMiddleware reexecuting the pipeline to generate an HTML body for a 404 response. A request to the / path returns a 404 response, which is handled by the status code middleware. The pipeline is reexecuted using the /404 path to generate the HTML response.
图 15.6 StatusCodePagesMiddleware 重新执行管道以生成 404 响应的 HTML 正文。对 / 路径的请求将返回 404 响应,该响应由状态代码中间件处理。使用 /404 路径重新执行管道以生成 HTML 响应。

Note that the error handling path "/{0}" contains a format string token, {0}. When the path is reexecuted, the middleware replaces this token with the status code number. For example, a 404 error would reexecute the /404 path. The handler for the path (typically a Razor Page, but it can be any endpoint) has access to the status code and can optionally tailor the response, depending on the status code. You can choose any error handling path as long as your application knows how to handle it.
请注意,错误处理路径 “/{0}” 包含格式字符串标记 {0}。重新执行路径时,中间件会将此令牌替换为状态代码编号。例如,404 错误将重新执行 /404 路径。路径的处理程序(通常是 Razor Page,但可以是任何终结点)有权访问状态代码,并且可以根据状态代码选择性地定制响应。您可以选择任何错误处理路径,只要您的应用程序知道如何处理它。

With this approach in place, you can create different error pages for different error codes, such as the 404-specific error page shown in figure 15.7. This technique ensures that your error pages are consistent with the rest of your application, including any dynamically generated content, while also allowing you to tailor the message for common errors.
使用这种方法,您可以为不同的错误代码创建不同的错误页面,例如图 15.7 中所示的特定于 404 的错误页面。此技术可确保错误页面与应用程序的其余部分(包括任何动态生成的内容)保持一致,同时还允许您针对常见错误定制消息。

alt text

Figure 15.7 An error status code page for a missing file. When an error code is detected (in this case, a 404 error), the middleware pipeline is reexecuted to generate the response. This allows dynamic portions of your web page to remain consistent on error pages.
图 15.7 缺失文件的错误状态代码页面。当检测到错误代码(在本例中为 404 错误)时,将重新执行中间件管道以生成响应。这允许网页的动态部分在错误页面上保持一致。

Warning As I mentioned in chapter 4, if your error handling path generates an error, the user will see a generic browser error. To mitigate this, it’s often better to use a static error page that will always work rather than a dynamic page that risks throwing more errors.
警告:正如我在第 4 章中提到的,如果你的错误处理路径产生了一个错误,用户将看到一个通用的浏览器错误。为了缓解这种情况,通常最好使用始终有效的静态错误页面,而不是冒着引发更多错误的动态页面。

The UseStatusCodePagesWithReExecute() method is great for returning a friendly error page when something goes wrong in a request, but there’s a second way to use the StatusCodePagesMiddleware. Instead of reexecuting the pipeline to generate the error response, you can redirect the browser to the error page instead, by calling
UseStatusCodePagesWithReExecute() 方法非常适合在请求出错时返回友好的错误页面,但还有第二种方法可以使用 StatusCodePagesMiddleware。您可以通过调用

app.UseStatusCodePagesWithRedirects("/{0}");

As for the reexecute version, this method takes a format string that defines the URL to generate the response. However, whereas the reexecute version generates the error response for the original request, the redirect version returns a 302 response initially, directing the browser to send a second request, this time for the error URL, as shown in figure 15.8. This second request generates the error page response, returning it with a 200 status code.
对于 reexecute 版本,此方法采用定义 URL 以生成响应的格式字符串。但是,reexecute 版本为原始请求生成错误响应,而重定向版本最初返回 302 响应,指示浏览器发送第二个请求,这次是针对错误 URL,如图 15.8 所示。第二个请求生成错误页面响应,并返回 200 状态代码。

alt text

Figure 15.8 StatusCodePagesMiddleware returning redirects to generate error pages. A request to the / path returns a 404 response, which is intercepted by the status code middleware and converted to a 302 response. The browser makes a second request using the /404 path to generate the HTML response.
图 15.8 StatusCodePagesMiddleware 返回重定向以生成错误页面。对 / 路径的请求返回 404 响应,该响应被状态代码中间件拦截并转换为 302 响应。浏览器使用 /404 路径发出第二个请求以生成 HTML 响应。

Whether you use the reexecute or redirect method, the browser ultimately receives essentially the same HTML. However, there are some important differences:
无论您使用 reexecute 还是 redirect 方法,浏览器最终都会收到基本相同的 HTML。但是,存在一些重要的差异:

• With the reexecute approach, the original status code (such as a 404) is preserved. The browser sees the error page HTML as the response to the original request. If the user refreshes the page, the browser makes a second request for the original path.
使用重新执行方法时,将保留原始状态代码 (如 404)。浏览器将错误页面 HTML 视为对原始请求的响应。如果用户刷新页面,浏览器将对原始路径发出第二个请求。
• With the redirect approach, the original status code is lost. The browser treats the redirect and second request as two separate requests and doesn’t “know” about the error. If the user refreshes the page, the browser makes a request for the same error path; it doesn’t resend the original request.
使用重定向方法时,原始状态代码将丢失。浏览器将重定向和第二个请求视为两个单独的请求,并且“不知道”错误。如果用户刷新页面,浏览器会请求相同的错误路径;它不会重新发送原始请求。

In most cases, I find the reexecute approach to be more useful, as it preserves the original error and typically has the behavior that users expect. There may be some cases where the redirect approach is useful, however, such as when an entirely different application generates the error page.
在大多数情况下,我发现 reexecute 方法更有用,因为它保留了原始错误,并且通常具有用户期望的行为。但是,在某些情况下,重定向方法可能很有用,例如,当完全不同的应用程序生成错误页面时。

Tip Favor using UseStatusCodePagesWithReExecute over the redirect approach when the same app is generating the error page HTML for your app.
提示:当同一应用程序为您的应用程序生成错误页面 HTML 时,优先使用 UseStatusCodePagesWithReExecute 而不是重定向方法。

You can use StatusCodePagesMiddleware in combination with other exception handling middleware by adding both to the pipeline. StatusCodePagesMiddleware modifies the response only if no response body has been written. So if another component, such as ExceptionHandlerMiddleware, returns a message body along with an error code, it won’t be modified.
您可以将 StatusCodePagesMiddleware 与其他异常处理中间件结合使用,方法是将两者添加到管道中。StatusCodePagesMiddleware 仅在未写入响应正文时修改响应。因此,如果另一个组件(比如 ExceptionHandlerMiddleware)返回消息正文和错误代码,则不会对其进行修改。

NOTE StatusCodePagesMiddleware has additional overloads that let you execute custom middleware when an error occurs instead of reexecuting the middleware pipeline. You can read about this approach at http://mng.bz/0K66.
注意:StatusCodePagesMiddleware 具有额外的重载,允许您在发生错误时执行自定义中间件,而不是重新执行中间件管道。您可以在 http://mng.bz/0K66 上阅读有关此方法的信息。

Error handling is essential when developing any web application; errors happen, and you need to handle them gracefully. The StatusCodePagesMiddleware is practically a must-have for any production Razor Pages app.
在开发任何 Web 应用程序时,错误处理都是必不可少的;错误会发生,您需要妥善处理它们。StatusCodePagesMiddleware 实际上是任何生产 Razor Pages 应用的必备工具。

In chapter 16 we’ll dive into model binding. You’ll see how the route values generated during routing are bound to your page handler parameters, and perhaps more important, how to validate the values you’re provided.
在第 16 章中,我们将深入探讨模型绑定。您将看到路由期间生成的路由值如何绑定到您的页面处理程序参数,也许更重要的是,如何验证您提供的值。

15.6 Summary

15.6 总结

A Razor Page page handler is the method in the Razor Page PageModel class that is executed when a Razor Page handles a request.
Razor Page 页面处理程序是 Razor Page PageModel 类中的方法,在 Razor Page 处理请求时执行。

Page handlers should ensure that the incoming request is valid, call in to the appropriate domain services to handle the request, and then choose the kind of response to return. They typically don’t generate the response directly; instead, they describe how to generate the response.
页面处理程序应确保传入请求有效,调用相应的域服务以处理请求,然后选择要返回的响应类型。它们通常不会直接生成响应;相反,它们描述了如何生成响应。

Page handlers should generally delegate to services to handle the business logic required by a request instead of performing the changes themselves. This ensures a clean separation of concerns that aids testing and improves application structure.
页面处理程序通常应委托给服务来处理请求所需的业务逻辑,而不是自行执行更改。这确保了关注点的清晰分离,从而有助于测试并改进应用程序结构。

When a Razor Page is executed, a single page handler is invoked based on the HTTP verb of the request and the value of the handler route value. If no page handler is found, an “implicit” handler is used instead, simply rendering the content of the Razor Page.
执行 Razor 页面时,将根据请求的 HTTP 谓词和处理程序路由值的值调用单个页面处理程序。如果未找到页面处理程序,则改用“隐式”处理程序,只呈现 Razor Page 的内容。

Page handlers can have parameters whose values are taken from properties of the incoming request in a process called model binding. Properties decorated with [BindProperty] can also be bound to the request. These are the canonical ways of reading values from the HTTP request inside your Razor Page.
页面处理程序可以具有参数,这些参数的值取自称为模型绑定的进程中传入请求的属性。使用 [BindProperty] 修饰的属性也可以绑定到请求。这些是从 Razor 页面内的 HTTP 请求中读取值的规范方法。

By default, properties decorated with [BindProperty] are not bound for GET requests. To enable binding, use [BindProperty(SupportsGet = true)].
默认情况下,使用 [BindProperty] 修饰的属性不会绑定到 GET 请求。若要启用绑定,请使用 [BindProperty(SupportsGet = true)]。

Page handlers can return a PageResult or void to generate an HTML response. The Razor Page infrastructure uses the associated Razor view to generate the HTML and returns a 200 OK response.
页面处理程序可以返回 PageResult 或 void 以生成 HTML 响应。Razor 页面基础结构使用关联的 Razor 视图生成 HTML 并返回 200 OK 响应。

You can send users to a different Razor Page using a RedirectToPageResult. It’s common to send users to a new page as part of the POST-REDIRECT-GET flow for handling user input via forms
您可以使用 RedirectToPageResult 将用户发送到不同的 Razor 页面。将用户发送到新页面通常是通过 POST-REDIRECT-GET 流程处理用户输入的一部分

The PageModel base class exposes many helper methods for creating an IActionResult, such as Page() which creates a PageResult, and RedirectToPage() which creates a RedirectToPageResult. These methods are simple wrappers around calling new on the corresponding IActionResult type.
PageModel 基类公开了许多用于创建 IActionResult 的帮助程序方法,例如创建 PageResult 的 Page() 和用于创建 RedirectToPageResult 的 RedirectToPage()。这些方法是对相应的 IActionResult 类型调用 new 的简单包装器。

StatusCodePagesMiddleware lets you provide user-friendly custom error handling messages when the pipeline returns a raw error response status code. This is important for providing a consistent user experience when status code errors are returned, such as 404 errors when a URL is not matched to an endpoint.
StatusCodePagesMiddleware 允许您在管道返回原始错误响应状态代码时提供用户友好的自定义错误处理消息。这对于在返回状态代码错误时提供一致的用户体验非常重要,例如,当 URL 与终端节点不匹配时出现 404 错误。

ASP.NET Core in Action 14 Mapping URLs to Razor Pages using routing

14 Mapping URLs to Razor Pages using routing
14 使用路由将 URL 映射到 Razor Pages

This chapter covers
本章介绍以下内容

• Routing requests to Razor Pages
将请求路由到 Razor Pages
• Customizing Razor Page route templates
自定义 Razor Page 路由模板
• Generating URLs for Razor Pages
为 Razor Pages 生成 URL

In chapter 13 you learned about the Model-View-Controller (MVC) design pattern and how ASP.NET Core uses it to generate the UI for an application using Razor Pages. Razor Pages contain page handlers that act as mini controllers for a request. The page handler calls the application model to retrieve or save data. Then the handler passes data from the application model to the Razor view, which generates an HTML response.
在第 13 章中,您了解了模型-视图-控制器 (MVC) 设计模式,以及 ASP.NET Core 如何使用它为使用 Razor Pages 的应用程序生成 UI。Razor Pages 包含充当请求的微型控制器的页面处理程序。页面处理程序调用应用程序模型来检索或保存数据。然后,处理程序将数据从应用程序模型传递到 Razor 视图,该视图会生成 HTML 响应。

Although not part of the MVC design pattern per se, one crucial part of Razor Pages is selecting which Razor Page to invoke in response to a given request. Razor Pages use the same routing system as minimal APIs (introduced in chapter 6); this chapter focuses on how routing works with Razor Pages.
虽然 Razor Pages 本身不是 MVC 设计模式的一部分,但 Razor Pages 的一个关键部分是选择要调用的 Razor Page 以响应给定请求。Razor Pages 使用与最小 API 相同的路由系统(在第 6 章中介绍);本章重点介绍路由如何与 Razor Pages 配合使用。

I start this chapter with a brief reminder about how routing works in ASP.NET Core. I’ll touch on the two pieces of middleware that are crucial to endpoint routing in .NET 7 and the approach Razor Pages uses of mixing conventions with explicit route templates.
本章开始时,我将简要介绍路由在 ASP.NET Core 中的工作原理。我将介绍对 .NET 7 中的终结点路由至关重要的两个中间件,以及 Razor Pages 使用将约定与显式路由模板混合的方法。

In section 14.3 we look at the default routing behavior of Razor Pages, and in section 14.4 you’ll learn how to customize the behavior by adding or changing route templates. Razor Pages have access to the same route template features that you learned about in chapter 6, and in section 14.4 you’ll learn how to them.
在第 14.3 节中,我们介绍了 Razor Pages 的默认路由行为,在第 14.4 节中,您将了解如何通过添加或更改路由模板来自定义行为。Razor Pages 可以访问您在第 6 章中了解的相同路由模板功能,在第 14.4 节中,您将了解如何使用它们。

In section 14.5 I describe how to use the routing system to generate URLs for Razor Pages. Razor Pages provide some helper methods to simplify URL generation compared with minimal APIs, so I compare the two approaches and discuss the benefits of each.
在第 14.5 节中,我将介绍如何使用路由系统为 Razor Pages 生成 URL。与最少的 API 相比,Razor Pages 提供了一些帮助程序方法来简化 URL 生成,因此我比较了这两种方法并讨论了每种方法的优点。

Finally, in section 14.6 I describe how to customize the conventions Razor Pages uses, giving you complete control of the URLs in your application. You’ll see how to change the built-in conventions, such as using lowercase for your URLs, as well as how to write your own convention and apply it globally to your application.
最后,在第 14.6 节中,我将介绍如何自定义 Razor Pages 使用的约定,从而让您完全控制应用程序中的 URL。您将了解如何更改内置约定,例如对 URL 使用小写,以及如何编写自己的约定并将其全局应用于应用程序。

By the end of this chapter you should have a much clearer understanding of how an ASP.NET Core application works. You can think of routing as the glue that ties the middleware pipeline to Razor Pages and the MVC framework. With middleware, Razor Pages, and routing under your belt, you’ll be writing web apps in no time!
在本章结束时,您应该对 ASP.NET Core 应用程序的工作原理有了更清晰的了解。可以将路由视为将中间件管道与 Razor Pages 和 MVC 框架绑定的粘附。借助中间件、Razor Pages 和路由,您将立即编写 Web 应用程序!

14.1 Routing in ASP.NET Core

14.1 ASP.NET Core 中的路由

In chapter 6 we looked in detail at routing and some of the benefits it brings, such as the ability to have multiple URLs pointing to the same endpoint and extracting segments from the URL. You also learned how it’s implemented in ASP.NET Core apps, using two pieces of middleware:
在第 6 章中,我们详细研究了路由及其带来的一些好处,例如让多个 URL 指向同一端点并从 URL 中提取片段的能力。您还了解了如何使用两个中间件在 ASP.NET Core 应用程序中实现它:

• EndpointMiddleware—You use this middleware to register the endpoints in the routing system when you start your application. The middleware executes one of the endpoints at runtime.
EndpointMiddleware — 启动应用程序时,您可以使用此中间件在路由系统中注册终端节点。中间件在运行时执行其中一个端点。
• RoutingMiddleware—This middleware chooses which of the endpoints registered by the EndpointMiddleware should execute for a given request at runtime.
RoutingMiddleware — 此中间件选择 EndpointMiddleware 注册的端点应在运行时为给定请求执行。

The EndpointMiddleware is where you register all the endpoints in your app, including minimal APIs, Razor Pages, and MVC controllers. It’s easy to register all the Razor Pages in your application using the MapRazorPages() extension method, as shown in the following listing.
在 EndpointMiddleware 中,你可以注册应用中的所有终结点,包括最小的 API、Razor Pages 和 MVC 控制器。使用 MapRazorPages() 扩展方法在应用程序中注册所有 Razor Pages 很容易,如下面的清单所示。

Listing 14.1 Registering Razor Pages in Startup.Configure
示例 14.1 在 Startup.Configure 中注册 Razor 页面

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages(); ❶
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting(); ❷
app.UseAuthorization();
app.MapRazorPages(); ❸
app.Run();

❶ Adds the required Razor Pages services to dependency injection
将所需的 Razor Pages 服务添加到依赖项注入
❷ Adds the RoutingMiddleware to the middleware pipeline
将 RoutingMiddleware 添加到中间件管道
❸ Registers all the Razor Pages in the application with the EndpointMiddleware
使用 EndpointMiddleware 注册应用程序中的所有 Razor Pages

Each endpoint, whether it’s a Razor Page or a minimal API, has an associated route template that defines which URLs the endpoint should match. The EndpointMiddleware stores these route templates and endpoints in a dictionary, which it shares with the RoutingMiddleware. At runtime the RoutingMiddleware compares the incoming request with the routes in the dictionary and selects the matching endpoint. When the request reaches the EndpointMiddleware, the middleware checks to see which endpoint was selected and executes it, as shown in figure 14.1.
每个终结点(无论是 Razor Page 还是最小 API)都有一个关联的路由模板,用于定义终结点应匹配的 URL。EndpointMiddleware 将这些路由模板和端点存储在一个字典中,它与 RoutingMiddleware 共享该字典。在运行时,RoutingMiddleware 将传入请求与字典中的路由进行比较,并选择匹配的端点。当请求到达 EndpointMiddleware 时,中间件会检查并选择了哪个端点并执行它,如图 14.1 所示。

alt text

Figure 14.1 Endpoint routing uses a two-step process. The RoutingMiddleware selects which endpoint to execute, and the EndpointMiddleware executes it. If the request URL doesn’t match a route template, the endpoint middleware will not generate a response.
图 14.1 终端节点路由使用两步过程。RoutingMiddleware 选择要执行的端点,EndpointMiddleware 执行它。如果请求 URL 与路由模板不匹配,则终端节点中间件不会生成响应。

As discussed in chapter 6, the advantage of having two separate pieces of middleware to handle this process is that any middleware placed after the RoutingMiddleware can see which endpoint is going to be executed before it is. You’ll see this benefit in action when we look at authorization in chapter 24.
如第 6 章所述,拥有两个独立的中间件来处理此过程的好处是,放置在 RoutingMiddleware 之后的任何中间件都可以在执行之前看到哪个端点将被执行。当我们在第 24 章中查看授权时,您将看到此好处的实际效果。

Routing in ASP.NET Core uses the same infrastructure and middleware whether you’re building minimal APIs, Razor Pages, or MVC controllers, but there are some differences in how you define the mapping between your route templates and your handlers in each case. In section 14.2 you’ll learn the different approaches each paradigm takes.
无论是构建最小的 API、Razor Pages 还是 MVC 控制器,ASP.NET Core 中的路由都使用相同的基础结构和中间件,但在每种情况下定义路由模板和处理程序之间的映射的方式存在一些差异。在 14.2 节中,您将学习每种范例采用的不同方法。

14.2 Convention-based routing vs. explicit routing

14.2 基于约定的路由与显式路由

Routing is a key part of ASP.NET Core, as it maps the incoming request’s URL to a specific endpoint to execute. You have two ways to define these URL-endpoint mappings in your application:
路由是 ASP.NET Core 的关键部分,因为它将传入请求的 URL 映射到要执行的特定终端节点。您可以通过两种方式在应用程序中定义这些 URL-终端节点映射:

• Using global, convention-based routing
使用基于约定的全局路由
• Using explicit routing, where each endpoint is mapped with a single route template
使用显式路由,其中每个端点都使用单个路由模板进行映射

Which approach you use typically depends on whether you’re using minimal APIs, Razor Pages, or MVC controllers and whether you’re building an API or a website (using HTML). These days I lean heavily toward explicit routing, as you’ll see.
使用哪种方法通常取决于你使用的是最小 API、Razor Pages 还是 MVC 控制器,以及你是构建 API 还是网站(使用 HTML)。正如您将看到的,这些天我非常倾向于显式路由。

Convention-based routing is defined globally for your application. You can use convention-based routes to map endpoints (MVC controller actions specifically) to URLs, but those MVC controllers must adhere strictly to the conventions you define. Traditionally, applications using MVC controllers to generate HTML tend to use this approach to routing. The downside of this approach is that customizing URLs for a subset of controllers and actions is tricky.
基于约定的路由是为您的应用程序全局定义的。您可以使用基于约定的路由将端点(特别是 MVC 控制器作)映射到 URL,但这些 MVC 控制器必须严格遵守您定义的约定。传统上,使用 MVC 控制器生成 HTML 的应用程序倾向于使用这种方法进行路由。这种方法的缺点是,为控制器和作的子集自定义 URL 很棘手。

Alternatively, you can use explicit routing to tie a given URL to a specific endpoint. You’ve seen this approach with minimal APIs, where each endpoint is directly associated with a route template. You can also use explicit routing with MVC controllers by placing [Route] attributes on the action methods themselves, hence explicit-routing is also often called attribute-routing.
或者,您可以使用显式路由将给定的 URL 绑定到特定的端点。您已经见过这种方法使用最少的 API,其中每个终端节点都直接与路由模板关联。您还可以通过在作方法本身上放置 [Route] 属性来将显式路由与 MVC 控制器一起使用,因此显式路由通常也称为属性路由。

Explicit routing provides more flexibility than convention-based based routing, as you can explicitly define the route template for every action method. Explicit routing is generally more verbose than the convention-based approach, as it requires applying attributes to every action method in your application. Despite this, the extra flexibility can be useful, especially when building APIs.
显式路由比基于约定的路由提供了更大的灵活性,因为您可以为每个作方法显式定义路由模板。显式路由通常比基于约定的方法更详细,因为它需要将属性应用于应用程序中的每个作方法。尽管如此,额外的灵活性还是很有用的,尤其是在构建 API 时。

Somewhat confusingly, Razor Pages use conventions to generate explicit routes! In many ways this combination gives you the best of both worlds: the predictability and terseness of convention-based routing with the easy customization of explicit routing. There are tradeoffs to each of the approaches, as shown in table 14.1.
有点令人困惑的是,Razor Pages 使用约定来生成显式路由!在许多方面,这种组合为您提供了两全其美的效果:基于约定的路由的可预测性和简洁性,以及显式路由的轻松自定义。每种方法都有权衡,如表 14.1 所示。

Table 14.1 The advantages and disadvantages of the routing styles available in ASP.NET Core
表 14.1 ASP.NET Core 中可用的路由样式的优缺点

Routing style Typical use Advantages Disadvantages
Convention-based routes
基于约定的路由
HTML-generating MVC controllers
HTML 生成 MVC 控制器
Terse definition in one location in your application. Forces a consistent layout of MVC controllers.
在应用程序中的一个位置进行简洁定义。强制 MVC 控制器布局一致。
Routes are defined in a different place from your controllers. Overriding the route conventions can be tricky and error-prone. Adds an extra layer of indirection when routing a request.
路由定义在与控制器不同的位置。覆盖路由约定可能很棘手且容易出错。在路由请求时添加额外的间接层。
Explicit routes
显式路由
Minimal API endpoints, Web API MVC controllers
最小 API 端点、Web API MVC 控制器
Gives complete control of route templates for every endpoint.Routes are defined next to the endpoint they execute.
提供对每个终端节点的路由模板的完全控制。路由在它们执行的终端节点旁边定义。
Verbose compared with convention-based routing.Can be easy to overcustomize route templates.Route templates may be scattered throughout your application rather than defined in one location.
与基于约定的路由相比,比较详细。可以很容易地过度定制路线模板。路由模板可能分散在整个应用程序中,而不是在一个位置定义。
Convention-based generation of explicit routes
基于约定的显式路由生成
Razor Pages
Razor 页面
Encourages consistent set of exposed URLs. Terse when you stick to the conventions. Easily override the route template for a single page. Customize conventions globally to change exposed URLs.
鼓励使用一组一致的公开 URL。当你坚持惯例时,简洁。轻松覆盖单个页面的路由模板。全局自定义约定以更改公开的 URL。
Possible to overcustomize route templates. You must calculate what the route template for a page is, rather than its being explicitly defined in your app.
可以过度自定义路由模板。您必须计算页面的路由模板是什么,而不是在应用程序中显式定义它。

So which approach should you use? I believe that convention-based routing is not worth the effort in 99 percent of cases and that you should stick to explicit routing. If you’re following my advice to use Razor Pages for server-rendered applications, you’re already using explicit routing under the covers. Also, if you’re creating APIs using minimal APIs or MVC controllers, explicit routing is the best option and the recommended approach.
那么您应该使用哪种方法呢?我认为,在 99% 的情况下,基于约定的路由不值得付出努力,您应该坚持使用显式路由。如果您按照我的建议将 Razor Pages 用于服务器渲染的应用程序,那么您已经在幕后使用了显式路由。此外,如果要使用最少的 API 或 MVC 控制器创建 API,则显式路由是最佳选项和推荐的方法。

The only scenario where convention-based routing is used traditionally is if you’re using MVC controllers to generate HTML. But if you’re following my advice from chapter 13, you’ll be using Razor Pages for HTML-generating applications and falling back to MVC controllers only when necessary, as I discuss in more detail in chapter 19. For consistency, I would often stick with explicit routing with attributes in that scenario too.
传统上使用基于约定的路由的唯一情况是使用 MVC 控制器生成 HTML。但是,如果您遵循我在第 13 章中的建议,您将使用 Razor Pages 生成 HTML 的应用程序,并且仅在必要时回退到 MVC 控制器,正如我在第 19 章中更详细地讨论的那样。为了保持一致性,在这种情况下,我通常也会坚持使用带有属性的显式路由。

NOTE For the reasons above, this book focuses on explicit/attribute routing. For details on convention-based routing, see Microsoft’s “Routing to controller actions in ASP.NET Core” documentation at http://mng.bz/ZP0O.
注意:由于上述原因,本书重点介绍显式/属性路由。有关基于约定的路由的详细信息,请参阅 Microsoft 的“路由到 ASP.NET Core 中的控制器作”文档,网址为 http://mng.bz/ZP0O

You learned about routing and route templates in chapter 6 in the context of minimal APIs. The good news is that exactly the same patterns and features are available with Razor Pages. The main difference with minimal APIs is that Razor Pages use conventions to generate the route template for a page, though you can easily change the template on a page-by-page basis. In section 14.3 we look at the default conventions and how routing maps a request’s URL to a Razor Page in detail.
您在第 6 章中了解了最小 API 上下文中的路由和路由模板。好消息是,Razor Pages 提供了完全相同的模式和功能。与最小 API 的主要区别在于,Razor Pages 使用约定为页面生成路由模板,但你可以轻松地逐页更改模板。在第 14.3 节中,我们将详细介绍默认约定以及路由如何将请求的 URL 映射到 Razor 页面。

14.3 Routing requests to Razor Pages

14.3 将请求路由到 Razor Pages

As I mentioned in section 14.2, Razor Pages use explicit routing by creating route templates based on conventions. ASP.NET Core creates a route template for every Razor Page in your app during app startup, when you call MapRazorPages() in Program.cs:
正如我在第 14.2 节中提到的,Razor Pages 通过基于约定创建路由模板来使用显式路由。ASP.NET Core 在应用启动期间,当您在 Program.cs 中调用 MapRazorPages() 时,会为应用中的每个 Razor 页面创建一个路由模板:

app.endpoints.MapRazorPages();

For every Razor Page in your application, the framework uses the path of the Razor Page file relative to the Razor Pages root directory (Pages/), excluding the file extension (.cshtml). If you have a Razor Page located at the path Pages/Products/View.cshtml, the framework creates a route template with the value "Products/View", as shown in figure 14.2.
对于应用程序中的每个 Razor 页面,框架使用 Razor 页面文件相对于 Razor 页面根目录 (Pages/) 的路径,不包括文件扩展名 (.cshtml)。如果你的 Razor 页面位于路径 Pages/Products/View.cshtml,则框架会创建一个值为“Products/View”的路由模板,如图 14.2 所示。

alt text

Figure 14.2 By default, route templates are generated for Razor Pages based on the path of the file relative to the root directory, Pages.
图 14.2 默认情况下,将根据文件相对于根目录 Pages 的路径为 Razor Pages 生成路由模板。

Requests to the URL /products/view match the route template "Products/View", which in turn corresponds to the View.cshtml Razor Page in the Pages/Products folder. The RoutingMiddleware selects the View.cshtml Razor Page as the endpoint for the request, and the EndpointMiddleware executes the page’s handler when the request reaches it in the middleware pipeline.
对 URL /products/view 的请求与路由模板“Products/View”匹配,而路由模板又对应于 Pages/Products 文件夹中的 View.cshtml Razor 页面。RoutingMiddleware 选择 View.cshtml Razor Page 作为请求的终结点,当请求在中间件管道中到达页面时,EndpointMiddleware 将执行页面的处理程序。

NOTE Remember that routing is not case-sensitive, so the request URL will match even if it has a different URL casing from the route template.
注意:请记住,路由不区分大小写,因此即使请求 URL 的 URL 大小写与路由模板不同,请求 URL 也会匹配。

In chapter 13 you learned that Razor Page handlers are the methods that are invoked on a Razor Page, such as OnGet. When we say “a Razor Page is executed,” we really mean “an instance of the Razor Page’s PageModel is created, and a page handler on the model is invoked.” Razor Pages can have multiple page handlers, so once the RoutingMiddleware selects a Razor Page, the EndpointMiddleware still needs to choose which handler to execute. You’ll learn how the framework selects which page handler to invoke in chapter 15.
在第 13 章中,你了解了 Razor Page 处理程序是在 Razor Page 上调用的方法,例如 OnGet。当我们说“执行 Razor 页面”时,我们实际上是指“创建 Razor 页面的 PageModel 的实例,并调用模型上的页面处理程序”。Razor Pages 可以有多个页面处理程序,因此,一旦 RoutingMiddleware 选择了 Razor Page,EndpointMiddleware 仍需要选择要执行的处理程序。在第 15 章中,您将了解框架如何选择要调用的页面处理程序。

By default, each Razor Page creates a single route template based on its file path. The exception to this rule is for Razor Pages that are called Index.cshtml. Index.cshtml pages create two route templates, one ending with "Index" and the other without this suffix. If you have a Razor Page at the path Pages/ToDo/Index.cshtml, you have two route templates that point to the same page:
默认情况下,每个 Razor 页面都会根据其文件路径创建一个路由模板。此规则的例外情况是名为 Index.cshtml 的 Razor 页面。Index.cshtml 页面创建两个路由模板,一个以“Index”结尾,另一个不带此后缀。如果在路径 Pages/ToDo/Index.cshtml 中有一个 Razor 页面,则有两个指向同一页面的路由模板:

• "ToDo"
• "ToDo/Index"

When either of these routes is matched, the same Index.cshtml Razor Page is selected. If your application is running at the URL https://example.org, you can view the page by executing https://example.org/ToDo or https://example.org/ToDo/Index.
当这些路由中的任何一个匹配时,将选择相同的 Index.cshtml Razor 页面。如果您的应用程序在 URL https://example.org 上运行,您可以通过执行 https://example.org/ToDohttps://example.org/ToDo/Index 来查看页面。

Warning You must watch out for overlapping routes when using Index.cshtml pages. For example, if you add the Pages/ToDo/Index.cshtml page in the above example you must not add a Pages/ToDo.cshtml page, as you’ll get an exception at runtime when you navigate to /todo, as you’ll see in section 14.6.
警告:使用 Index.cshtml 页面时,必须注意重叠的路由。例如,如果在上述示例中添加 Pages/ToDo/Index.cshtml 页面,则不得添加 Pages/ToDo.cshtml 页面,因为在导航到 /todo 时,将在运行时收到异常,如第 14.6 节所示。

As a final example, consider the Razor Pages created by default when you create a Razor Pages application by using Visual Studio or running dotnet new razor using the .NET command-line interface (CLI), as we did in chapter 13. The standard template includes three Razor Pages in the Pages directory:
作为最后一个示例,请考虑使用 Visual Studio 或使用 .NET 命令行界面 (CLI) 运行 dotnet new razor 创建 Razor Pages 应用程序时默认创建的 Razor Pages,就像我们在第 13 章中所做的那样。标准模板在 Pages 目录中包含三个 Razor Pages:

• Pages/Error.cshtml
• Pages/Index.cshtml
• Pages/Privacy.cshtml

That creates a collection of four routes for the application, defined by the following templates:
这将为应用程序创建四个路由的集合,由以下模板定义:

• "" maps to Index.cshtml.
• "Index" maps to Index.cshtml.
• "Error" maps to Error.cshtml.
• "Privacy" maps to Privacy.cshtml.

At this point, Razor Page routing probably feels laughably trivial, but this is the basics that you get for free with the default Razor Pages conventions, which are often sufficient for a large portion of any website. At some point, though, you’ll find you need something more dynamic, such as using route parameters to include an ID in the URL. This is where the ability to customize your Razor Page route templates becomes useful.
在这一点上,Razor Page 路由可能感觉微不足道,但这是您通过默认 Razor Pages 约定免费获得的基础知识,这些约定通常足以满足任何网站的大部分需求。但是,在某些时候,您会发现您需要更动态的东西,例如使用路由参数在 URL 中包含 ID。这是自定义 Razor Page 路由模板的功能变得有用的地方。

14.4 Customizing Razor Page route templates

14.4 自定义 Razor Page 路由模板

The route templates for a Razor Page are based on the file path by default, but you’re also able to customize the final template for each page or even replace it. In this section I show how to customize the route templates for individual pages so you can customize your application’s URLs and map multiple URLs to a single Razor Page.
默认情况下,Razor 页面的路由模板基于文件路径,但你也可以为每个页面自定义最终模板,甚至替换它。在本节中,我将介绍如何自定义各个页面的路由模板,以便您可以自定义应用程序的 URL 并将多个 URL 映射到单个 Razor 页面。

You may remember from chapter 6 that route templates consist of both literal segments and route parameters, as shown in figure 14.3. By default, Razor Pages have URLs consisting of a series of literal segments, such as "ToDo/Index".
您可能还记得第 6 章中的路由模板由文字段和路由参数组成,如图 14.3 所示。默认情况下,Razor Pages 的 URL 由一系列文本段组成,例如“ToDo/Index”。

alt text

Figure 14.3 A simple route template showing a literal segment and two required route parameters
图 14.3 一个简单的路由模板,显示了一个文字段和两个必需的路由参数

Literal segments and route parameters are the two cornerstones of ASP.NET Core route templates, but how can you customize a Razor Page to use one of these patterns? In section 14.4.1 you’ll see how to add a segment to the end of a Razor Page’s route template, and in section 14.4.2 you’ll see how to replace the route template completely.
文本段和路由参数是 ASP.NET Core 路由模板的两个基石,但如何自定义 Razor 页面以使用这些模式之一?在第 14.4.1 节中,你将了解如何将段添加到 Razor 页面路由模板的末尾,在第 14.4.2 节中,你将了解如何完全替换路由模板。

14.4.1 Adding a segment to a Razor Page route template

14.4.1 向 Razor Page 路由模板添加区段

To customize the Razor Page route template, you update the @page directive at the top of the Razor Page’s .cshtml file. This directive must be the first thing in the Razor Page file for the page to be registered correctly.
若要自定义 Razor Page 路由模板,请更新 Razor Page 的 .cshtml 文件顶部的 @page 指令。此指令必须是 Razor Page 文件中的第一项作,才能正确注册页面。

To add an extra segment to a Razor Page’s route template, add a space followed by the extra route template segment, after the @page statement. To add "Extra" to a Razor Page’s route template, for example, use
若要向 Razor Page 的路由模板添加额外段,请在 @page 语句后添加一个空格,后跟额外的路由模板段。例如,要将“Extra”添加到 Razor Page 的路由模板,请使用

@page "Extra"

This appends the provided route template to the default template generated for the Razor Page. The default route template for the Razor Page at Pages/Privacy.html, for example, is "Privacy". With the preceding directive, the new route template for the page would be "Privacy/Extra".
这会将提供的路由模板追加到为 Razor 页面生成的默认模板。例如,Pages/Privacy.html 的 Razor Page 的默认路由模板为“隐私”。使用上述指令,页面的新路由模板将为 “Privacy/Extra”。

The most common reason for customizing a Razor Page’s route template like this is to add a route parameter. You could have a single Razor Page for displaying the products in an e-commerce site at the path Pages/Products.cshtml and use a route parameter in the @page directive
像这样自定义 Razor Page 的路由模板的最常见原因是添加路由参数。可以有一个 Razor 页面,用于在路径 Pages/Products.cshtml 的电子商务站点中显示产品,并在 @page 指令中使用 route 参数

@page "{category}/{name}"

This would give a final route template of Products/{category}/{name}, which would match all the following URLs:
这将提供 Products/{category}/{name} 的最终路由模板,该模板将匹配以下所有 URL:

• /products/bags/white-rucksack
• /products/shoes/black-size9
• /Products/phones/iPhoneX

NOTE You can use the same routing features you learned about in chapter 6 with Razor Pages, including optional parameters, default parameters, and constraints.
注意:您可以将在第 6 章中学到的相同路由功能用于 Razor Pages,包括可选参数、默认参数和约束。

It’s common to add route segments to the Razor Page template like this, but what if that’s not enough? Maybe you don’t want to have the /products segment at the start of the preceding URLs, or you want to use a completely custom URL for a page. Luckily, that’s just as easy to achieve.
像这样向 Razor Page 模板添加路线段是很常见的,但如果这还不够怎么办?也许您不希望在前面的 URL 开头有 /products 段,或者您希望对页面使用完全自定义的 URL。幸运的是,这同样容易实现。

14.4.2 Replacing a Razor Page route template completely

14.4.2 完全替换 Razor Page 路由模板

You’ll be most productive working with Razor Pages if you can stick to the default routing conventions where possible, adding extra segments for route parameters where necessary. But sometimes you need more control. That’s often the case for important pages in your application, such as the checkout page for an e-commerce application or even product pages, as you saw in the previous section.
如果您可以尽可能坚持默认路由约定,并在必要时为路由参数添加额外的段,那么使用 Razor Pages 的效率将最高。但有时你需要更多的控制。应用程序中的重要页面通常就是这种情况,例如电子商务应用程序的结帐页面,甚至是产品页面,如上一节所示。

To specify a custom route for a Razor Page, prefix the route with / in the @page directive. To remove the "product/" prefix from the route templates in section 14.4.1, use this directive:
若要为 Razor Page 指定自定义路由,请在 @page 指令中为路由添加 / 前缀。要从第 14.4.1 节中的路由模板中删除 “product/” 前缀,请使用以下指令:

@page "/{category}/{name}"

Note that this directive includes the "/" at the start of the route, indicating that this is a custom route template, instead of an addition. The route template for this page will be "{category}/{name}" no matter which Razor Page it is applied to.
请注意,此指令在路由的开头包含 “/”,表示这是一个自定义路由模板,而不是附加模板。此页面的路由模板将为“{category}/{name}”,无论它应用于哪个 Razor 页面。

Similarly, you can create a static custom template for a page by starting the template with a "/" and using only literal segments:
同样,您可以通过以 “/” 开头并仅使用文字段来为页面创建静态自定义模板:

@page "/checkout"

Wherever you place your checkout Razor Page within the Pages folder, using this directive ensures that it always has the route template "checkout", so it always matches the request URL /checkout.
将 checkout Razor Page 放在 Pages 文件夹中的哪个位置,使用此指令可确保它始终具有路由模板“checkout”,因此它始终与请求 URL /checkout 匹配。

Tip You can also think of custom route templates that start with “/” as absolute route templates, whereas other route templates are relative to their location in the file hierarchy.
提示:您还可以将以 “/” 开头的自定义路由模板视为绝对路由模板,而其他路由模板则是相对于它们在文件层次结构中的位置的模板。

It’s important to note that when you customize the route template for a Razor Page, both when appending to the default and when replacing it with a custom route, the default template is no longer valid. If you use the "checkout" route template above on a Razor Page located at Pages/Payment.cshtml, you can access it only by using the URL /checkout; the URL /Payment is no longer valid and won’t execute the Razor Page.
请务必注意,当您自定义 Razor 页面的路由模板时,无论是追加到默认模板还是将其替换为自定义路由,默认模板都不再有效。如果在位于 Pages/Payment.cshtml 的 Razor 页面上使用上述“checkout”路由模板,则只能使用 URL /checkout 访问它;URL /Payment 不再有效,并且不会执行 Razor 页面。

Tip Customizing the route template for a Razor Page using the @page directive replaces the default route template for the page. In section 14.6 I show how you can add extra routes while preserving the default route template.
提示:使用 @page 指令自定义 Razor 页面的路由模板会替换页面的默认路由模板。在第 14.6 节中,我将介绍如何在保留默认路由模板的同时添加额外的路由。

In this section you learned how to customize the route template for a Razor Page. For the most part, routing to Razor Pages works like minimal APIs, the main difference being that the route templates are created using conventions. When it comes to the other half of routing—generating URLs—Razor Pages and minimal APIs are also similar, but Razor Pages gives you some nice helpers.
在本部分中,你学习了如何自定义 Razor Page 的路由模板。在大多数情况下,路由到 Razor Pages 的工作方式类似于最小的 API,主要区别在于路由模板是使用约定创建的。当涉及到路由的另一半(生成 URL)时,Razor Pages 和最小 API 也类似,但 Razor Pages 提供了一些不错的帮手。

14.5 Generating URLs for Razor Pages

14.5 为 Razor Pages 生成 URL

In this section you’ll learn how to generate URLs for your Razor Pages using the IUrlHelper that’s part of the Razor Pages PageModel type. You’ll also learn to use the LinkGenerator service you saw in chapter 6 for generating URLs with minimal APIs.
在本部分中,你将了解如何使用 IUrlHelper(属于 Razor Pages PageModel 类型的一部分)为 Razor Pages 生成 URL。您还将学习使用您在第 6 章中看到的 LinkGenerator 服务来生成具有最少 API 的 URL。

One of the benefits of using convention-based routing in Razor Pages is that your URLs can be somewhat fluid. If you rename a Razor Page, the URL associated with that page also changes. Renaming the Pages/Cart.cshtml page to Pages/Basket/View.cshtml, for example, causes the URL you use to access the page to change from /Cart to /Basket/View.
在 Razor Pages 中使用基于约定的路由的好处之一是,您的 URL 可以保持一定的流动性。如果重命名 Razor 页面,则与该页面关联的 URL 也会更改。例如,将 Pages/Cart.cshtml 页面重命名为 Pages/Basket/View.cshtml 会导致用于访问页面的 URL 从 /Cart 更改为 /Basket/View。

To track these changes (and to avoid broken links), you can use the routing infrastructure to generate the URLs that you output in your Razor Page HTML and that you include in your HTTP responses. In chapter 6 you saw how to generate URLs for your minimal API endpoints, and in this section, you’ll see how to do the same for your Razor Pages. I also describe how to generate URLs for MVC controllers, as the mechanism is virtually identical to that used by Razor Pages.
若要跟踪这些更改(并避免链接断开),可以使用路由基础结构生成在 Razor 页面 HTML 中输出并包含在 HTTP 响应中的 URL。在第 6 章中,您了解了如何为最小 API 端点生成 URL,在本部分中,您将了解如何为 Razor 页面执行相同的作。我还介绍了如何为 MVC 控制器生成 URL,因为该机制与 Razor Pages 使用的机制几乎相同。

14.5.1 Generating URLs for a Razor Page

14.5.1 为 Razor 页面生成 URL

You’ll need to generate URLs in various places in your application, and one common location is in your Razor Pages and MVC controllers. The following listing shows how you could generate a link to the Pages/Currency/View.cshtml Razor Page, using the Url helper from the PageModel base class.
您需要在应用程序的不同位置生成 URL,一个常见位置位于 Razor Pages 和 MVC 控制器中。以下列表显示了如何使用 PageModel 基类中的 Url 帮助程序生成指向 Pages/Currency/View.cshtml Razor 页面的链接。

Listing 14.2 Generating a URL using IUrlHelper and the Razor Page name
列表 14.2 使用 IUrlHelper 和 Razor 页面名称生成 URL

public class IndexModel : PageModel ❶
{
public void OnGet()
{
var url = Url.Page("Currency/View", new { code = "USD" }); ❷
}
}

❶ Deriving from PageModel gives access to the Url property.
从 PageModel 派生提供对 Url 属性的访问权限。
❷ You provide the relative path to the Razor Page, along with any additional route values.
提供 Razor 页面的相对路径,以及任何其他路由值。

The Url property is an instance of IUrlHelper that allows you to easily generate URLs for your application by referencing other Razor Pages by their file path.
Url 属性是 IUrlHelper 的一个实例,它允许你通过按文件路径引用其他 Razor 页面,轻松地为应用程序生成 URL。

NOTE IUrlHelper is a wrapper around the LinkGenerator class you learned about in chapter 6. IUrlHelper adds some shortcuts for generating URLs based on the current request.
注意:IUrlHelper 是您在第 6 章中了解的 LinkGenerator 类的包装器。IUrlHelper 添加了一些快捷方式,用于根据当前请求生成 URL。

IUrlHelper exposes a Page() method to which you pass the name of the Razor Page and any additional route data as an anonymous object. Then the helper generates a URL based on the referenced page’s route template.
IUrlHelper 公开一个 Page() 方法,将 Razor Page 的名称和任何其他路由数据作为匿名对象传递给该方法。然后,帮助程序根据引用页面的路由模板生成 URL。

Tip You can provide the relative file path to the Razor Page, as shown in listing 14.2. Alternatively, you can provide the absolute file path (relative to the Pages folder) by starting the path with a "/", such as "/Currency/View".
提示:您可以提供 Razor Page 的相对文件路径,如清单 14.2 所示。或者,您也可以通过以“/”开头来提供绝对文件路径(相对于 Pages 文件夹),例如“/Currency/View”。

IUrlHelper has several different overloads of the Page() method. Some of these methods allow you to specify a specific page handler, others let you generate an absolute URL instead of a relative URL, and some let you pass in additional route values.
IUrlHelper 具有 Page() 方法的几个不同的重载。其中一些方法允许您指定特定的页面处理程序,其他方法允许您生成绝对 URL 而不是相对 URL,而另一些方法允许您传入其他路由值。

In listing 14.2, as well as providing the file path I passed in an anonymous object, new { code = "USD" }. This object provides additional route values when generating the URL, in this case setting the code parameter to "USD", as you did when generating URLs for minimal APIs with LinkGenerator in chapter 6. As before, the code value is used in the URL directly if it corresponds to a route parameter. Otherwise, it’s appended as additional data in the query string.
在清单 14.2 中,除了提供我在匿名对象中传递的文件路径外,new { code = “USD” }.此对象在生成 URL 时提供额外的路由值,在本例中将 code 参数设置为 “USD”,就像您在第 6 章中使用 LinkGenerator 为最小 API 生成 URL 时所做的那样。与以前一样,如果 code 值对应于 route 参数,则直接在 URL 中使用 code 值。否则,它将作为附加数据附加到查询字符串中。

Generating URLs based on the page you want to execute is convenient, and it’s the usual approach taken in most cases. If you’re using MVC controllers for your APIs, the process is much the same as for Razor Pages, though the methods are slightly different.
根据您要执行的页面生成 URL 很方便,这是大多数情况下通常采用的方法。如果对 API 使用 MVC 控制器,则过程与 Razor Pages 大致相同,但方法略有不同。

14.5.2 Generating URLs for an MVC controller

14.5.2 为 MVC 控制器生成 URL

Generating URLs for MVC controllers is similar to Razor Pages. The main difference is that you use the Action method on the IUrlHelper, and you provide an MVC controller name and action name instead of a page path.
为 MVC 控制器生成 URL 类似于 Razor Pages。主要区别在于,在 IUrlHelper 上使用 Action 方法,并提供 MVC 控制器名称和作名称,而不是页面路径。

NOTE I’ve covered MVC controllers only in passing, as I generally don’t recommend them over Razor Pages or minimal APIs, so don’t worry too much about them. We’ll come back to MVC controllers in chapters 19 and 20; the main reason for mentioning them here is to point out how similar MVC controllers are to Razor Pages.
注意:我只是顺便介绍了 MVC 控制器,因为我通常不建议在 Razor Pages 或最小 API 上使用它们,因此不必太担心它们。我们将在第 19 章和第 20 章回到 MVC 控制器;在这里提到它们的主要原因是指出 MVC 控制器与 Razor Pages 的相似之处。

The following listing shows an MVC controller generating a link from one action method to another, using the Url helper from the Controller base class.
下面的清单显示了一个 MVC 控制器,它使用 Controller 基类中的 Url 帮助程序生成从一个作方法到另一个作方法的链接。

Listing 14.3 Generating a URL using IUrlHelper and the action name
示例 14.3 使用 IUrlHelper 和作名称生成 URL

public class CurrencyController : Controller ❶
{
[HttpGet("currency/index")] ❷
public IActionResult Index()
{
var url = Url.Action("View", "Currency", ❸
new { code = "USD" }); ❸
return Content($"The URL is {url}"); ❹
}
[HttpGet("currency/view/{code}")]
public IActionResult View(string code) ❺
{
    /* method implementation*/
}
}

❶ Deriving from Controller gives access to the Url property.
从 Controller 派生可访问 Url 属性。
❷ Explicit route templates using attributes
使用属性的显式路由模板
❸ You provide the action and controller name to generate, along with any additional route values.
您提供要生成的作和控制器名称,以及任何其他路由值。
❹ Returns “The URL is /Currency/View/USD”
返回“URL is /Currency/View/USD”
❺ The URL generated a route to this action method.
URL 生成了到此作方法的路由。

You can call the Action and Page methods on IUrlHelper from both Razor Pages and MVC controllers, so you can generate links back and forth between them if you need to. The important question is, what is the destination of the URL? If the URL you need refers to a Razor Page, use the Page() method. If the destination is an MVC action, use the Action() method.
可以从 Razor Pages 和 MVC 控制器调用 IUrlHelper 上的 Action 和 Page 方法,以便在需要时可以在它们之间来回生成链接。重要的问题是,URL 的目的地是什么?如果您需要的 URL 引用 Razor 页面,请使用 Page() 方法。如果目标是 MVC作,请使用 Action() 方法。

Tip Instead of using strings for the name of the action method, use the C# 6 nameof operator to make the value refactor-safe, such as nameof(View).
提示:不要使用字符串作为作方法的名称,而是使用 C# 6 nameof 运算符使值成为重构安全的,例如 nameof(View)。

If you’re routing to an action in the same controller, you can use a different overload of Action() that omits the controller name when generating the URL. The IUrlHelper uses ambient values from the current request and overrides them with any specific values you provide.
如果要路由到同一控制器中的作,则可以使用不同的 Action() 重载,在生成 URL 时省略控制器名称。IUrlHelper 使用当前请求中的环境值,并使用你提供的任何特定值替代它们。

DEFINITION Ambient values are the route values for the current request. They include Controller and Action when called from an MVC controller and Page when called from a Razor Page. Ambient values can also include additional route values that were set when the action or Razor Page was initially located using routing. See Microsoft’s “Routing in ASP.NET Core” documentation for further details: http://mng.bz/OxoE.
定义:Ambient 值是当前请求的路由值。它们包括从 MVC 控制器调用时的 Controller 和 Action,以及从 Razor 页面调用时的 Page。环境值还可以包括最初使用路由定位作或 Razor 页面时设置的其他路由值。有关更多详细信息,请参阅 Microsoft 的“ASP.NET Core 中的路由”文档:http://mng.bz/OxoE

IUrlHelper can make it simpler to generate URLs by reusing ambient values from the current request, though it also adds a layer of complexity, as the same method arguments can give a different generated URL depending on the page the method is called from.
IUrlHelper 可以通过重用当前请求中的环境值来简化生成 URL 的过程,但它也增加了一层复杂性,因为相同的方法参数可能会根据调用该方法的页面提供不同的生成 URL。

If you need to generate URLs from parts of your application outside the Razor Page or MVC infrastructure, you won’t be able to use the IUrlHelper helper. Instead, you can use the LinkGenerator class.
如果需要从 Razor Page 或 MVC 基础结构外部的应用程序部分生成 URL,则无法使用 IUrlHelper 帮助程序。相反,您可以使用 LinkGenerator 类。

14.5.3 Generating URLs with LinkGenerator

14.5.3 使用 LinkGenerator 生成 URL

In chapter 6 I described how to generate links to minimal API endpoints using the LinkGenerator class. By contrast with IUrlHelper, LinkGenerator requires that you always provide sufficient arguments to uniquely define the URL to generate. This makes it more verbose but also more consistent and has the advantage that it can be used anywhere in your application. This differs from IUrlHelper, which should be used only inside the context of a request.
在第 6 章中,我介绍了如何使用 LinkGenerator 类生成指向最小 API 端点的链接。与 IUrlHelper 相比,LinkGenerator 要求您始终提供足够的参数来唯一定义要生成的 URL。这使得它更详细,但也更一致,并且其优点是它可以在应用程序中的任何位置使用。这与 IUrlHelper 不同,后者应仅在请求的上下文中使用。

If you’re writing your Razor Pages and MVC controllers following the advice from chapter 13, you should be trying to keep your Razor Pages relatively simple. That requires you to execute your application’s business and domain logic in separate classes and services.
如果你按照第 13 章中的建议编写 Razor Pages 和 MVC 控制器,你应该尽量保持 Razor Pages 相对简单。这要求您在单独的类和服务中执行应用程序的业务和域逻辑。

For the most part, the URLs your application uses shouldn’t be part of your domain logic. That makes it easier for your application to evolve over time or even to change completely. You may want to create a mobile application that reuses the business logic from an ASP.NET Core app, for example. In that case, using URLs in the business logic wouldn’t make sense, as they wouldn’t be correct when the logic is called from the mobile app!
在大多数情况下,您的应用程序使用的 URL 不应成为域逻辑的一部分。这使您的应用程序更容易随着时间的推移而发展,甚至更容易完全改变。例如,您可能希望创建一个移动应用程序,该应用程序重用 ASP.NET Core 应用程序中的业务逻辑。在这种情况下,在业务逻辑中使用 URL 就没有意义了,因为当从移动应用程序调用逻辑时,它们将不正确!

Tip Where possible, try to keep knowledge of the frontend application design out of your business logic. This pattern is known generally as the Dependency Inversion principle.
提示:在可能的情况下,尽量将前端应用程序设计知识排除在您的业务逻辑之外。此模式通常称为 Dependency Inversion 原则。

Unfortunately, sometimes that separation is not possible, or it makes things significantly more complicated. One example might be when you’re creating emails in a background service; it’s likely you’ll need to include a link to your application in the email. The LinkGenerator class lets you generate that URL so that it updates automatically if the routes in your application change.
不幸的是,有时这种分离是不可能的,或者它使事情变得更加复杂。例如,当您在后台服务中创建电子邮件时;您可能需要在电子邮件中包含指向您的应用程序的链接。LinkGenerator 类允许您生成该 URL,以便在应用程序中的路由发生更改时自动更新。

As you saw in chapter 6, the LinkGenerator class is available everywhere in your application, so you can use it inside middleware, minimal API endpoints, or any other services. You can use it from Razor Pages and MVC too, if you want, though the IUrlHelper is often more convenient and hides some details of using the LinkGenerator.
正如您在第 6 章中所看到的,LinkGenerator 类在应用程序中随处可见,因此您可以在中间件、最小 API 端点或任何其他服务中使用它。如果需要,您也可以从 Razor Pages 和 MVC 使用它,尽管 IUrlHelper 通常更方便,并且隐藏了使用 LinkGenerator 的一些细节。

You’ve already seen how to generate links to minimal API endpoints with LinkGenerator using methods like GetPathByName() and GetUriByName(). LinkGenerator has various analogous methods for generating URLs for Razor Pages and MVC actions, such as GetPathByPage(), GetPathByAction(), and GetUriByPage(), as shown in the following listing.
您已经了解了如何使用 GetPathByName() 和 GetUriByName() 等方法通过 LinkGenerator 生成指向最小 API 端点的链接。LinkGenerator 具有各种类似的方法,用于为 Razor Pages 和 MVC作生成 URL,例如 GetPathByPage()、GetPathByAction() 和 GetUriByPage(),如下面的清单所示。

Listing 14.4 Generating URLs using the LinkGeneratorClass
清单 14.4 使用 LinkGeneratorClass 生成 URL

public class CurrencyModel : PageModel
{
private readonly LinkGenerator _link; ❶
public CurrencyModel(LinkGenerator linkGenerator) ❶
{ ❶
_link = linkGenerator; ❶
} ❶
public void OnGet ()
{
var url1 = Url.Page("Currency/View", new { id = 5 }); ❷
var url3 = _link.GetPathByPage( ❸
HttpContext, ❸
"/Currency/View", ❸
values: new { id = 5 }); ❸
var url2 = _link.GetPathByPage( ❹
"/Currency/View", ❹
values: new { id = 5 }); ❹
var url4 = _link.GetUriByPage( ❺
page: "/Currency/View", ❺
handler: null, ❺
values: new { id = 5 }, ❺
scheme: "https", ❺
host: new HostString("example.com")); ❺
}
}

❶ LinkGenerator can be accessed using dependency injection.
LinkGenerator 可以使用依赖项注入进行访问。
❷ You can generate relative paths using Url.Page. You can use relative or absolute Page paths.
您可以使用 Url.Page 生成相对路径。您可以使用相对或绝对 Page 路径。
❸ GetPathByPage is equivalent to Url.Page and generates a relative URL.
GetPathByPage 等效于 Url.Page 并生成相对 URL。
❹ Other overloads don’t require an HttpContext.
其他重载不需要 HttpContext。
❺ GetUriByPage generates an absolute URL instead of a relative URL.
GetUriByPage 生成绝对 URL 而不是相对 URL。

Warning As always, you need to be careful when generating URLs, whether you’re using IUrlHelper or LinkGenerator. If you get anything wrong—use the wrong path or don’t provide a required route parameter—the URL generated will be null.
警告:与往常一样,在生成 URL 时需要小心,无论您使用的是 IUrlHelper 还是 LinkGenerator。如果出现任何错误(使用错误的路径或未提供必需的路由参数),则生成的 URL 将为 null。

At this point we’ve covered mapping request URLs to Razor Pages and generating URLs, but most of the URLs we’ve used have been kind of ugly. If seeing capital letters in your URLs bothers you, the next section is for you. In section 14.6 we customize the conventions your application uses to calculate route templates.
在这一点上,我们已经介绍了将请求 URL 映射到 Razor Pages 和生成 URL,但我们使用的大多数 URL 都有些难看。如果在 URL 中看到大写字母让您感到困扰,那么下一部分适合您。在 Section 14.6 中,我们自定义了应用程序用于计算路由模板的约定。

14.6 Customizing conventions with Razor Pages

使用 Razor Pages 自定义约定

Razor Pages is built on a series of conventions that are designed to reduce the amount of boilerplate code you need to write. In this section you’ll see some of the ways you can customize those conventions. By customizing the conventions Razor Pages uses in your application, you get full control of your application’s URLs without having to customize every Razor Page’s route template manually.
Razor Pages 基于一系列约定构建,旨在减少需要编写的样板代码量。在本节中,您将看到一些可以自定义这些约定的方法。通过自定义 Razor Pages 在应用程序中使用的约定,可以完全控制应用程序的 URL,而无需手动自定义每个 Razor Page 的路由模板。

By default, ASP.NET Core generates URLs that match the filenames of your Razor Pages very closely. The Razor Page located at the path Pages/Products/ProductDetails.cshtml, for example, corresponds to the route template Products/ProductDetails.
默认情况下,ASP.NET Core 生成的 URL 与 Razor Pages 的文件名非常匹配。例如,位于路径 Pages/Products/ProductDetails.cshtml 的 Razor 页面对应于路由模板 Products/ProductDetails。

These days, it’s not common to see capital letters in URLs. Similarly, words in URLs are usually separated using kebab-case rather than PascalCase—product-details instead of ProductDetails. Finally, it’s also common to ensure that your URLs always end with a slash, for example—/product-details/ instead of /product-details. Razor Pages gives you complete control of the conventions your application uses to generate route templates, but these are some of the common changes I often make.
如今,在 URL 中看到大写字母并不常见。同样,URL 中的单词通常使用 kebab-case 而不是 PascalCase 分隔,即 product-details 而不是 ProductDetails。最后,确保您的 URL 始终以斜杠结尾也是很常见的,例如 /product-details/ 而不是 /product-details。Razor Pages 让您可以完全控制应用程序用于生成路由模板的约定,但这些是我经常进行的一些常见更改。

You saw how to make some of these changes in chapter 6, by customizing the RouteOptions for your application. You can make your URLs lowercase and ensure that they already have a trailing slash as shown in the following listing.
您在第 6 章中了解了如何通过自定义应用程序的 RouteOptions 来进行其中一些更改。您可以将 URL 设置为小写,并确保它们已经有一个尾部斜杠,如下面的清单所示。

Listing 14.5 Configuring routing conventions using RouteOptions in Program.cs
列表 14.5 在 Program.cs 中使用 RouteOptions 配置路由约定

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<RouteOptions>(o => ❶
{ ❶
o.LowercaseUrls = true; ❶
o.LowercaseQueryStrings = true; ❶
o.AppendTrailingSlash = true; ❶
});
WebApplication app = builder.Build();
app.MapRazorPages();
app.Run();

❶ Changes the conventions used to generate URLs. By default, these properties are false.
更改用于生成 URL 的约定。默认情况下,这些属性为 false。

To use kebab-case for your application, annoyingly you must create a custom parameter transformer. This is a somewhat advanced topic, but it’s relatively simple to implement in this case. The following listing shows how you can create a parameter transformer that uses a regular expression to replace PascalCase values in a generated URL with kebab-case.
要为您的应用程序使用 kebab-case,令人讨厌的是,您必须创建一个自定义参数 transformer。这是一个有点高级的主题,但在这种情况下实现起来相对简单。下面的清单显示了如何创建一个参数转换器,该转换器使用正则表达式将生成的 URL 中的 PascalCase 值替换为 kebab-case。

Listing 14.6 Creating a kebab-case parameter transformer
Listing 14.6 创建一个 kebab-case 参数转换器

public class KebabCaseParameterTransformer ❶
: IOutboundParameterTransformer ❶
{
public string TransformOutbound(object? value)
{
if (value is null) return null; ❷
return Regex.Replace(value.ToString(), ❸
"([a-z])([A-Z])", "$1-$2").ToLower(); ❸
}
}

❶ Creates a class that implements the parameter transformer interface
创建一个实现参数 transformer 接口的类
❷ Guards against null values to prevent runtime exceptions
防止空值以防止运行时异常
❸ The regular expression replaces PascalCase patterns with kebab-case.
正则表达式将 PascalCase 模式替换为 kebab-case。

Source generators in .NET 7
.NET 7 中的源生成器

One of the exciting features introduced in C# 9 was source generators. Source generators are a compiler feature that let you inspect code as it’s compiled and generate new C# files on the fly, which are included in the compilation. Source generators have the potential to dramatically reduce the boilerplate required for some features and to improve performance by relying on compile-time analysis instead of runtime reflection.
C# 9 中引入的一个令人兴奋的功能是源生成器。源生成器是一项编译器功能,可让您在编译代码时检查代码,并动态生成新的 C# 文件,这些文件包含在编译中。源生成器有可能显著减少某些功能所需的样板,并通过依赖编译时分析而不是运行时反射来提高性能。

.NET 6 introduced several source generator implementations, such as a high-performance logging API, which I discuss in this post: http://mng.bz/Y1GA. Even the Razor compiler used to compile .cshtml files was rewritten to use source generators!
.NET 6 引入了几个源生成器实现,例如高性能日志记录 API,我将在本文中讨论:http://mng.bz/Y1GA。甚至用于编译 .cshtml 文件的 Razor 编译器也被重写为使用源生成器!

In .NET 7, many new source generators were added. One such generator is the regular-expression generator, which can improve performance of your Regex instances, such as the one in listing 14.6. In fact, if you’re using an IDE like Visual Studio, you should see a code fix suggesting that you use the new pattern. After you apply the code fix, listing 14.6 should look like the following instead, which is functionally identical but will likely be faster:
在 .NET 7 中,添加了许多新的源生成器。一个这样的生成器是正则表达式生成器,它可以提高 Regex 实例的性能,例如清单 14.6 中的那个。事实上,如果您使用的是 Visual Studio 之类的 IDE,您应该会看到一个代码修复,建议您使用新模式。应用代码修复后,清单 14.6 应如下所示,它在功能上相同,但可能会更快:

partial class KebabCaseParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        if (value is null) return null;
        return MyRegex().Replace(value.ToString(), "$1-$2").ToLower();
    }
    [GeneratedRegex("([a-z])([A-Z])")]
    private static partial Regex MyRegex();
}

If you’d like to know more about how this source generator works and how it can improve performance, see this post at http://mng.bz/GyEO. If you’d like to learn more about source generators or even write your own, see my series on the process at http://mng.bz/zX4Q.
如果您想进一步了解此源代码生成器的工作原理以及它如何提高性能,请参阅 http://mng.bz/GyEO 上的这篇文章。如果您想了解有关源生成器的更多信息,甚至编写自己的源生成器,请参阅我在 http://mng.bz/zX4Q 上关于该过程的系列文章。

You can register the parameter transformer in your application with the AddRazorPagesOptions() extension method in Program.cs. This method is chained after the AddRazorPages() method and can be used to customize the conventions used by Razor Pages. The following listing shows how to register the kebab-case transformer. It also shows how to add an extra page route convention for a given Razor Page.
您可以在 Program.cs 中使用 AddRazorPagesOptions() 扩展方法在应用程序中注册参数转换器。此方法在 AddRazorPages() 方法之后链接,可用于自定义 Razor Pages 使用的约定。下面的清单显示了如何注册 kebab-case 转换器。它还演示如何为给定的 Razor 页面添加额外的页面路由约定。

Listing 14.7 Registering a parameter transformer using RazorPagesOptions
清单 14.7 使用 RazorPagesOptions 注册参数转换器

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages()
.AddRazorPagesOptions(opts => ❶
{
opts.Conventions.Add( ❷
new PageRouteTransformerConvention( ❷
new KebabCaseParameterTransformer())); ❷
opts.Conventions.AddPageRoute( ❸
"/Search/Products/StartSearch", "/search-products"); ❸
});
WebApplication app = builder.Build();
app.MapRazorPages();
app.Run();

❶ AddRazorPagesOptions can be used to customize the conventions used by Razor Pages
AddRazorPagesOptions 可用于自定义 Razor Pages使用的约定
❷ Registers the parameter transformer as a convention used by all Razor Pages
将参数转换器注册为所有 Razor Pages使用的约定
❸ AddPageRoute adds a route template to Pages/Search/Products/StartSearch.cshtml.
AddPageRoute 将路由模板添加到 Pages/Search/Products/StartSearch.cshtml。

The AddPageRoute() convention adds an alternative way to execute a single Razor Page. Unlike when you customize the route template for a Razor Page using the @page directive, using AddPageRoute() adds an extra route template to the page instead of replacing the default. That means there are two route templates that can access the page.
AddPageRoute() 约定添加了一种执行单个 Razor Page 的替代方法。与使用 @page 指令自定义 Razor 页面的路由模板不同,使用 AddPageRoute() 会向页面添加额外的路由模板,而不是替换默认值。这意味着有两个路由模板可以访问该页面。

Tip Even the name of the Pages root folder is a convention that you can customize! You can customize it by setting the RootDirectory property inside the AddRazorPageOptions() configuration lambda.
提示:甚至 Pages 根文件夹的名称也是您可以自定义的约定!您可以通过在 AddRazorPageOptions() 配置 lambda 中设置 RootDirectory 属性来自定义它。

If you want even more control of your Razor Pages route templates, you can implement a custom convention by implementing the IPageRouteModelConvention interface and registering it as a custom convention. IPageRouteModelConvention is one of three powerful Razor Pages interfaces which let you customize how your Razor Pages app works:
如果要对 Razor Pages 路由模板进行更多控制,可以通过实现 IPageRouteModelConvention 接口并将其注册为自定义约定来实现自定义约定。IPageRouteModelConvention 是三个功能强大的 Razor Pages 接口之一,可用于自定义 Razor Pages 应用的工作方式:

• IPageRouteModelConvention—Used to customize the route templates for all the Razor Pages in your app.
IPageRouteModelConvention - 用于自定义应用程序中所有 Razor 页面的路由模板。
• IPageApplicationModelConvention—Used to customize how the Razor Page is processed, such as to add filters to your Razor Page automatically. You’ll learn about filters in Razor Pages in chapters 21 and 22.
IPageApplicationModelConvention - 用于自定义 Razor 页面的处理方式,例如自动向 Razor 页面添加筛选器。您将在第 21 章和第 22 章中了解 Razor Pages 中的过滤器。
• IPageHandlerModelConvention—Used to customize how page handlers are discovered and selected.
IPageHandlerModelConvention - 用于自定义发现和选择页面处理程序的方式。

These interfaces are powerful, as they give you access to all the internals of your Razor Page conventions and configuration. You can use the IPageRouteModelConvention, for example, to rewrite all the route templates for your Razor Pages or to add routes automatically. This is particularly useful if you need to localize an application so that you can use URLs in multiple languages, all of which map to the same Razor Page.
这些接口功能强大,因为它们使你能够访问 Razor Page 约定和配置的所有内部结构。例如,可以使用 IPageRouteModelConvention 重写 Razor Pages 的所有路由模板或自动添加路由。如果您需要本地化应用程序,以便可以使用多种语言的 URL,所有这些 URL 都映射到同一个 Razor 页面,这将特别有用。

Listing 14.8 shows a simple example of an IPageRouteModelConvention that adds a fixed prefix, "page", to all the routes in your application. If you have a Razor Page at Pages/Privacy.cshtml, with a default route template of "Privacy", after adding the following convention it would also have the route template "page/Privacy”.
清单 14.8 显示了一个 IPageRouteModelConvention 的简单示例,该示例为应用程序中的所有路由添加了固定前缀 “page”。如果你在 Pages/Privacy.cshtml 上有一个 Razor 页面,并且默认路由模板为“Privacy”,则在添加以下约定后,它还将具有路由模板“page/Privacy”。

Listing 14.8 Creating a custom IPageRouteModelConvention
清单 14.8 创建自定义 IPageRouteModelConvention

public class PrefixingPageRouteModelConvention
: IpageRouteModelConvention ❶
{
public void Apply(PageRouteModel model) ❷
{
var selectors = model.Selectors
.Select(selector => new SelectorModel ❸
{ ❸
AttributeRouteModel = new AttributeRouteModel ❸
{ ❸
Template = AttributeRouteModel.CombineTemplates( ❸
"page", ❸
selector.AttributeRouteModel!.Template), ❸
} ❸
}) ❸
.ToList();
foreach(var newSelector in selectors) ❹
{
model.Selectors.Add(newSelector);
}
}
}

❶ The convention implements IPageRouteModelConvention.
该约定实现 IPageRouteModelConvention。
❷ ASP.NET Core calls Apply on app startup.
ASP.NET Core 在应用程序启动时调用 Apply。
❸ Creates a new SelectorModel, defining a new route template for the page
创建一个新的 SelectorModel,为页面定义一个新的路由模板
❹ Adds the new selector to the page’s route template collection
将新的选择器添加到页面的路由模板集合中

You can add the convention to your application inside the call to AddRazorPagesOptions(). The following applies the contention to all pages:
您可以在对 AddRazorPagesOptions() 的调用中将约定添加到您的应用程序。以下将争用应用于所有页面:

builder.Services.AddRazorPages().AddRazorPagesOptions(opts =>
{
    opts.Conventions.Add(new PrefixingPageRouteModelConvention());
});

There are many ways you can customize the conventions in your Razor Page applications, but a lot of the time that’s not necessary. If you do find you need to customize all the pages in your application in some way, Microsoft’s “Razor Pages route and app conventions in ASP.NET Core” documentation contains further details on everything that’s available: http://mng.bz/A0BK.
您可以通过多种方式自定义 Razor Page 应用程序中的约定,但很多时候这不是必需的。如果你确实发现需要以某种方式自定义应用程序中的所有页面,Microsoft 的“ASP.NET Core 中的 Razor Pages 路由和应用约定”文档包含有关所有可用内容的更多详细信息:http://mng.bz/A0BK

Conventions are a key feature of Razor Pages, and you should lean on them whenever you can. Although you can override the route templates for individual Razor Pages manually, as you’ve seen in previous sections, I advise against it where possible. In particular,
约定是 Razor Pages 的一项关键功能,您应该尽可能依赖它们。尽管您可以手动覆盖单个 Razor Pages 的路由模板,但正如您在前面的部分中看到的那样,我建议尽可能不要这样做。特别

• Avoid replacing the route template with an absolute path in a page’s @page directive.
避免在页面的 @page 指令中将路由模板替换为绝对路径。

• Avoid adding literal segments to the @page directive. Rely on the file hierarchy instead.
避免向 @page 指令添加文字段。请改用文件层次结构。

• Avoid adding additional route templates to a Razor Page with the AddPageRoute() convention. Having multiple URLs to access a page can often be confusing.
避免使用 AddPageRoute() 约定向 Razor 页面添加其他路由模板。使用多个 URL 来访问页面通常会令人困惑。

• Do add route parameters to the @page directive to make your routes dynamic, as in @page “{name}".
务必将路由参数添加到 @page 指令中,以使您的路由动态化,如 @page “{name}”。

• Do consider using global conventions when you want to change the route templates for all your Razor Pages, such as using kebab-case, as you saw earlier.
当您想要更改所有 Razor 页面的路由模板时,请考虑使用全局约定,例如使用 kebab-case,如前所述。

In a nutshell, these rules say “Stick to the conventions.” The danger, if you don’t, is that you may accidentally create two Razor Pages that have overlapping route templates. Unfortunately, if you end up in that situation, you won’t get an error at compile time. Instead, you’ll get an exception at runtime when your application receives a request that matches multiple route templates, as shown in figure 14.4.
简而言之,这些规则说 “Adhere the conventions”。如果不这样做,危险在于可能会意外创建两个具有重叠路由模板的 Razor 页面。不幸的是,如果你最终遇到这种情况,你不会在编译时收到错误。相反,当您的应用程序收到与多个路由模板匹配的请求时,您将在运行时收到异常,如图 14.4 所示。

alt text

Figure 14.4 If multiple Razor Pages are registered with overlapping route templates, you’ll get an exception at runtime when the router can’t work out which one to select.
图 14.4 如果多个 Razor 页面注册了重叠的路由模板,则当路由器无法确定要选择哪个页面时,您将在运行时收到异常。

We’ve covered pretty much everything about routing to Razor Pages now. For the most part, routing to Razor Pages works like minimal APIs, the main difference being that the route templates are created using conventions. When it comes to the other half of routing—generating URLs—Razor Pages and minimal APIs are also similar, but Razor Pages gives you some nice helpers.
我们现在已经介绍了几乎所有关于路由到 Razor Pages 的内容。在大多数情况下,路由到 Razor Pages 的工作方式类似于最小的 API,主要区别在于路由模板是使用约定创建的。当涉及到路由的另一半(生成 URL)时,Razor Pages 和最小 API 也类似,但 Razor Pages 提供了一些不错的帮手。

Congratulations—you’ve made it all the way through this detailed discussion on Razor Page routing! I hope you weren’t too fazed by the differences from minimal API routing. We’ll revisit routing again when I describe how to create Web APIs in chapter 20, but rest assured that we’ve already covered all the tricky details in this chapter!
恭喜 — 您已经完成了有关 Razor Page 路由的详细讨论!我希望您不会对最小 API 路由的差异感到太困扰。当我在第 20 章中描述如何创建 Web API 时,我们将再次回顾路由,但请放心,我们已经在本章中介绍了所有棘手的细节!

Routing controls how incoming requests are bound to your Razor Page, but we haven’t seen where page handlers come into it. In chapter 15 you’ll learn all about page handlers—how they’re selected, how they generate responses, and how to handle error responses gracefully.
路由控制传入请求如何绑定到 Razor 页面,但我们尚未看到页面处理程序的来源。在第 15 章中,您将了解有关页面处理程序的所有信息 — 如何选择它们,如何生成响应,以及如何优雅地处理错误响应。

14.7 Summary

14.7 总结

Routing is the process of mapping an incoming request URL to an endpoint that will execute to generate a response. Each Razor Page is an endpoint, and a single page handler executes for each request.
路由是将传入请求 URL 映射到将执行以生成响应的终端节点的过程。每个 Razor 页面都是一个端点,每个请求都会执行一个页面处理程序。

You can define the mapping between URLs and endpoint in your application using either convention-based routing or explicit routing. Minimal APIs use explicit routing, where each endpoint has a corresponding route template. MVC controllers often use conventional routing in which a single pattern matches multiple controllers but may also use explicit/attribute routing. Razor Pages lies in between; it uses conventions to generate explicit route templates for each page.
您可以使用基于约定的路由或显式路由来定义应用程序中 URL 和 endpoint 之间的映射。最小 API 使用显式路由,其中每个终端节点都有相应的路由模板。MVC 控制器通常使用传统路由,其中单个模式匹配多个控制器,但也可能使用显式/属性路由。Razor Pages 介于两者之间;它使用约定为每个页面生成显式路由模板。

By default, each Razor Page has a single route template that matches its path inside the Pages folder, so the Razor Page Pages/Products/View.cshtml has route template Products/View. These file-based defaults make it easy to visualize the URLs your application exposes.
默认情况下,每个 Razor 页面都有一个路由模板,该模板与其在 Pages 文件夹中的路径匹配,因此 Razor Page Pages/Products/View.cshtml 具有路由模板 Products/View。这些基于文件的默认值使可视化应用程序公开的 URL 变得容易。

Index.cshtml Razor Pages have two route templates, one with an /Index suffix and one without. Pages/Products/Index.cshtml, for example, has two route templates: Products/Index and Products. This is in keeping with the common behavior of index.html files in traditional HTML applications.
Index.cshtml Razor Pages 有两个路由模板,一个带有 /Index 后缀,另一个没有。例如,Pages/Products/Index.cshtml 有两个路由模板:Products/Index 和 Products。这与传统 HTML 应用程序中index.html文件的常见行为一致。

You can add segments to a Razor Page’s template by appending it to the @page directive, as in @page "{id}". Any extra segments are appended to the Razor Page’s default route template. You can include both literal and route template segments, which can be used to make your Razor Pages dynamic. You can replace the route template for a Razor Page by starting the template with a "/", as in @page "/contact".
您可以通过将 Razor 页面的模板附加到 @page 指令来向 Razor 页面的模板添加区段,就像@page “{id}” 一样。任何额外的段都将附加到 Razor Page 的默认路由模板中。您可以同时包含文本和路由模板段,这可用于使 Razor 页面动态化。可以通过使用“/”开头来替换 Razor 页面的路由模板,如@page“/contact”。

You can use IUrlHelper to generate URLs as a string based on an action name or Razor Page. IUrlHelper can be used only in the context of a request and uses ambient routing values from the current request. This makes it easier to generate links for Razor Pages in the same folder as the currently executing request but also adds inconsistency, as the same method call generates different URLs depending on where it’s called.
可以使用 IUrlHelper 根据作名称或 Razor 页面将 URL 生成为字符串。IUrlHelper 只能在请求的上下文中使用,并使用当前请求中的环境路由值。这样可以更轻松地在与当前执行的请求相同的文件夹中为 Razor Pages 生成链接,但也增加了不一致性,因为相同的方法调用会根据调用位置生成不同的 URL。

The LinkGenerator can be used to generate URLs from other services in your application, where you don’t have access to an HttpContext object. The LinkGenerator methods are more verbose than the equivalents on IUrlHelper, but they are unambiguous as they don’t use ambient values from the current request.
LinkGenerator 可用于从应用程序中的其他服务生成 URL,在这些服务中,您无权访问 HttpContext 对象。LinkGenerator 方法比 IUrlHelper 上的等效方法更详细,但它们是明确的,因为它们不使用当前请求中的环境值。

You can control the routing conventions used by ASP.NET Core by configuring the RouteOptions object, such as to force all URLs to be lowercase or to always append a trailing slash.
您可以通过配置 RouteOptions 对象来控制 ASP.NET Core 使用的路由约定,例如强制所有 URL 为小写或始终附加尾部斜杠。

You can add extra routing conventions for Razor Pages by calling AddRazorPagesOptions() after AddRazorPages() in Program.cs. These conventions can control how route parameters are displayed and can add extra route templates for specific Razor Pages.
可以通过在 Program.cs 中的 AddRazorPages() 后调用 AddRazorPagesOptions() 来为 Razor Pages 添加额外的路由约定。这些约定可以控制路由参数的显示方式,并且可以为特定 Razor 页面添加额外的路由模板。