Category Archives: C#

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="test@example.com" 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 ("test@example.com", in this case). The type of the element has also been set to the HTML5 email type, instead of using the default text type.
此外,字段的初始值已设置为当前存储在属性中的值(在本例中为“test@example.com”)。元素的类型也已设置为 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 页面添加额外的路由模板。

ASP.NET Core in Action 13 Creating a website with Razor Pages

Part 3 Generating HTML with Razor Pages and MVC

第 3 部分:使用 Razor Pages 和 MVC 生成 HTML

In parts 1 and 2 we looked in detail at how to create JSON API applications using minimal APIs. You learned how to configure your app from multiple sources, how to use dependency injection to reduce coupling in your app, and how to document your APIs with OpenAPI.

在第 1 部分和第 2 部分中,我们详细介绍了如何使用最少的 API 创建 JSON API 应用程序。您学习了如何从多个来源配置应用程序,如何使用依赖关系注入来减少应用程序中的耦合,以及如何使用 OpenAPI 记录 API。

API apps are everywhere these days. Mobile apps use them; clients-side Single Page Applications (SPAs) like Angular, React, or Blazor use them; even other apps use them for server-to-server communication. But in many cases, you don’t need separate server-side and client-side apps. Instead, you could create a server-rendered app.

如今,API 应用程序无处不在。移动应用程序使用它们;Angular、React 或 Blazor 等客户端单页应用程序 (SPA) 使用它们;甚至其他应用程序也使用它们进行服务器到服务器的通信。但在许多情况下,您不需要单独的服务器端和客户端应用程序。相反,您可以创建服务器呈现的应用程序。

With server-rendering, your application generates the HTML on the server and the browser displays this directly in the browser; no extra client-side framework required. You can still add dynamic client-side behavior using JavaScript, but fundamentally each page in your app is a standalone request and response, which gives a simpler developer experience.

使用服务器呈现时,应用程序会在服务器上生成 HTML,浏览器会直接在浏览器中显示 HTML;不需要额外的客户端框架。您仍然可以使用 JavaScript 添加动态客户端行为,但从根本上说,应用程序中的每个页面都是一个独立的请求和响应,这提供了更简单的开发人员体验。

In part 3, you’ll learn about the Razor Pages and MVC frameworks used by ASP.NET Core to create server-rendered apps. In chapters 13 through 16 we’ll examine the behavior of the Razor Pages framework itself, routing, and model binding. In chapters 17 and 18 we’ll look at how you can build the UI for your application using the Razor syntax and Tag Helpers, so that users can navigate and interact with your app.

在第 3 部分中,您将了解 ASP.NET Core 用于创建服务器渲染应用的 Razor Pages 和 MVC 框架。在第 13 章到第 16 章中,我们将研究 Razor Pages 框架本身的行为、路由和模型绑定。在第 17 章和第 18 章中,我们将介绍如何使用 Razor 语法和标记帮助程序为应用程序构建 UI,以便用户可以导航应用并与之交互。

In chapter 19 you’ll learn how to use the MVC framework directly, instead of Razor Pages. You’ll learn how to use MVC controllers to build server-rendered apps and when to choose MVC controllers instead of Razor Pages. In chapter 20 you’ll learn to how to use MVC controllers to build API applications, as an alternative to minimal APIs. Finally, in chapters 21 and 22 you’ll learn how to refactor your apps to extract common code out of your Razor Pages and API controllers using filters.

在第 19 章中,您将学习如何直接使用 MVC 框架,而不是 Razor Pages。您将学习如何使用 MVC 控制器构建服务器呈现的应用程序,以及何时选择 MVC 控制器而不是 Razor Pages。在第 20 章中,您将学习如何使用 MVC 控制器来构建 API 应用程序,作为最小 API 的替代方案。最后,在第 21 章和第 22 章中,您将学习如何重构应用程序,以使用筛选器从 Razor Pages 和 API 控制器中提取常见代码。

13 Creating a website with Razor Pages
13 使用 Razor Pages 创建网站

This chapter covers

本章涵盖

  • Getting started with Razor Pages
    Razor Pages 入门

  • Introducing Razor Pages and the Model-View-Controller (MVC) design pattern
    Razor Pages 和模型-视图-控制器 (MVC) 设计模式简介

  • Using Razor Pages in ASP.NET Core
    在 ASP.NET Core 中使用 Razor Pages

So far in this book you’ve built one type of ASP.NET Core application: minimal API apps that return JavaScript Object Notation (JSON). In this chapter you’ll learn how to build server-rendered, page-based applications using Razor Pages. Most ASP.NET Core apps fall into one of three categories:

到目前为止,在本书中,您已经构建了一种类型的 ASP.NET Core 应用程序:返回 JavaScript 对象表示法 (JSON) 的最小 API 应用程序。在本章中,您将学习如何使用 Razor Pages 构建服务器渲染的、基于页面的应用程序。大多数 ASP.NET Core 应用程序都属于以下三类之一:

  • An API designed for consumption by another machine or in code—Web apps often serve as an API to backend server processes, to a mobile app, or to a client framework for building single-page applications (SPAs). In this case your application serves data in machine-readable formats such as JSON or Extensible Markup Language (XML) instead of the human-focused HTML output.
    供其他计算机使用或在代码中使用的 API — Web 应用程序通常用作后端服务器进程、移动应用程序或用于构建单页应用程序 (SPA) 的客户端框架的 API。在这种情况下,您的应用程序以机器可读格式(如 JSON 或可扩展标记语言 (XML))而不是以人类为中心的 HTML 输出提供数据。

  • An HTML web application designed for direct use by users—If the application is consumed directly by users, as in a traditional web application, Razor Pages is responsible for generating the web pages that the user interacts with. It handles requests for URLs, receives data posted via forms, and generates the HTML that enables users to view and navigate your app.
    专为用户直接使用而设计的 HTML Web 应用程序 – 如果应用程序由用户直接使用,就像在传统 Web 应用程序中一样,Razor Pages 负责生成用户与之交互的网页。它处理对 URL 的请求,接收通过表单发布的数据,并生成使用户能够查看和导航您的应用程序的 HTML。

  • Both an HTML web application and an API—It’s also possible to have applications that serve both needs, which can let you cater to a wider range of clients while sharing logic in your application.
    HTML Web 应用程序和 API — 也可以拥有同时满足这两种需求的应用程序,这样您就可以在应用程序中共享逻辑的同时满足更广泛的客户端需求。

In this chapter you’ll learn how ASP.NET Core uses Razor Pages to handle the second of these options: creating server-side rendered HTML pages. We’ll get started quickly, using a template to create a simple Razor Pages application and comparing the features of a Razor Pages app with the minimal API apps you’ve seen so far. In section 13.2 we look at a more complex example of a Razor Page.

在本章中,你将了解 ASP.NET Core 如何使用 Razor Pages 来处理第二个选项:创建服务器端呈现的 HTML 页面。我们将快速入门,使用模板创建简单的 Razor Pages 应用程序,并将 Razor Pages 应用的功能与你目前看到的最小 API 应用进行比较。在 Section 13.2 中,我们看一个更复杂的 Razor Page 示例。

Next, we take a step back in section 13.3 to look at the MVC design pattern. I discuss some of the benefits of using this pattern, and you’ll learn why it’s been adopted by so many web frameworks as a model for building maintainable applications.

接下来,我们在 Section 13.3 中后退一步,看看 MVC 设计模式。我将讨论使用此模式的一些好处,您将了解为什么它被如此多的 Web 框架用作构建可维护应用程序的模型。

In section 13.4 you’ll learn how the MVC design pattern applies to ASP.NET Core. The MVC pattern is a broad concept that can be applied in a variety of situations, but the use case in ASP.NET Core is specifically as a UI abstraction. You’ll see how Razor Pages implements the MVC design pattern and builds on top of the ASP.NET Core MVC framework.

在第 13.4 节中,您将了解 MVC 设计模式如何应用于 ASP.NET Core。MVC 模式是一个广泛的概念,可以应用于各种情况,但 ASP.NET Core 中的用例专门用作 UI 抽象。您将了解 Razor Pages 如何实现 MVC 设计模式并在 ASP.NET Core MVC 框架之上构建。

In this chapter I’ll try to prepare you for each of the upcoming topics, but you may find that some of the behavior feels a bit like magic at this stage. Try not to become too concerned about exactly how all the Razor Pages pieces tie together yet; focus on the specific concepts being addressed and how they tie into concepts you’ve already met. We’ll start by creating a Razor Pages app to explore.

在本章中,我将尝试让您为即将到来的每个主题做好准备,但您可能会发现,在这个阶段,某些行为感觉有点像魔术。尽量不要太关心所有 Razor Pages 作品究竟是如何联系在一起的;专注于要解决的具体概念以及它们如何与您已经遇到的概念相关联。首先,我们将创建一个 Razor Pages 应用进行探索。

13.1 Your first Razor Pages application

13.1 您的第一个 Razor Pages 应用程序

In this section you’ll get started with Razor Pages by creating a new application from a template. After you’ve created the app and had a look around, we’ll look at some of the similarities and differences compared with a minimal API application. You’ll learn about the extra middleware added in the default template, look at how HTML is generated by Razor Pages, and take a look at the Razor Page equivalent of minimal API endpoint handlers: page handlers.

在本部分中,你将通过从模板创建新应用程序来开始使用 Razor Pages。在您创建应用程序并环顾四周后,我们将了解与最小 API 应用程序相比的一些相似之处和不同之处。您将了解默认模板中添加的额外中间件,了解 Razor Pages 如何生成 HTML,并查看 Razor Page 等价的最小 API 端点处理程序:页面处理程序。

13.1.1 Using the Web Application template

13.1.1 使用 Web 应用程序模板

Using a template is a quick way to get an application running, so we’ll take that approach using the ASP.NET Core Web App template. To create a Razor Pages application in Visual Studio, perform the following steps:

使用模板是运行应用程序的一种快速方法,因此我们将使用 ASP.NET Core 来采用这种方法Web App 模板。要在 Visual Studio 中创建 Razor Pages 应用程序,请执行以下步骤:

  1. Choose Create a New Project from the splash screen or choose File > New > Project from the main Visual Studio screen.
    从初始屏幕中选择 Create a New Project,或从 Visual Studio 主屏幕中选择 File > New > Project。

  2. From the list of templates, choose ASP.NET Core Web App, ensuring you select the C# language template.
    从模板列表中,选择 ASP.NET Core Web App,确保选择 C# 语言模板。

  3. On the next screen, enter a project name, location, and solution name, and click Next. You might use WebApplication1 as both the project and solution name, for example.
    在下一个屏幕上,输入项目名称、位置和解决方案名称,然后单击 Next。例如,您可以使用 WebApplication1 作为项目和解决方案名称。

  4. On the following screen (figure 13.1), do the following:
    在以下屏幕(图 13.1)中,执行以下作:

  • Select .NET 7.0. If this option isn’t available, ensure that you have .NET 7 installed. See appendix A for details on configuring your environment.
    选择 .NET 7.0。如果此选项不可用,请确保您已安装 .NET 7。有关配置环境的详细信息,请参阅附录 A。
  • Ensure that Configure for HTTPS is checked.
    确保选中 Configure for HTTPS (为 HTTPS 配置)。
  • Ensure that Enable Docker is unchecked.
    确保 Enable Docker (启用 Docker) 处于未选中状态。
  • Ensure that Do not use top-level statements is unchecked.
    确保 Do Not Use Top-level Statements 未选中。
  • Choose Create.
    选择 Create (创建)。

alt text

Figure 13.1 The additional information screen. This screen follows the Configure Your New Project dialog box and lets you customize the template that generates your application.
图 13.1 附加信息屏幕。此屏幕位于 Configure Your New Project 对话框之后,允许您自定义生成应用程序的模板。

If you’re not using Visual Studio, you can create a similar template using the .NET command-line interface (CLI). Create a folder to hold your new project. Open a PowerShell or cmd prompt in the folder (on Windows) or a terminal session (on Linux or macOS), and run the commands in the following listing.

如果不使用 Visual Studio,则可以使用 .NET 命令行界面 (CLI) 创建类似的模板。创建一个文件夹来保存您的新项目。在文件夹(在 Windows 上)或终端会话(在 Linux 或 macOS 上)中打开 PowerShell 或 cmd 提示符,然后运行以下列表中的命令。

Listing 13.1 Creating a new Razor Page application with the .NET CLI
清单 13.1 使用 .NET CLI创建新的 Razor Page 应用程序

dotnet new sln -n WebApplication1     ❶
dotnet new razor -o WebApplication1   ❷
dotnet sln add WebApplication1        ❸

❶ Creates a solution file called WebApplication1 in the current folder
在当前文件夹中创建名为 WebApplication1 的解决方案文件
❷ Creates an ASP.NET Core Razor Pages project in a subfolder, WebApplication1
在子文件夹 WebApplication1 中创建 ASP.NET Core Razor Pages 项目
❸ Adds the new project to the solution file
将新项目添加到解决方案文件中

Whether you use Visual Studio or the .NET CLI, now you can build and run your application. Press F5 to run your app using Visual Studio, or use dotnet run in the project folder. This command opens the appropriate URL in a web browser and displays the basic Welcome page, shown in figure 13.2.

无论您使用的是 Visual Studio 还是 .NET CLI,现在都可以构建和运行应用程序。按 F5 使用 Visual Studio 运行应用,或使用项目文件夹中的 dotnet run。此命令在 Web 浏览器中打开相应的 URL,并显示基本的 Welcome 页面,如图 13.2 所示。

alt text

Figure 13.2 The output of your new Razor Pages application. The template chooses a random port to use for your application’s URL, which is opened automatically in the browser when you run the app.
图 13.2 新 Razor Pages 应用程序的输出。该模板选择一个随机端口用于应用程序的 URL,当您运行应用程序时,该端口会自动在浏览器中打开。

By default, this page shows a simple Welcome banner and a link to the official Microsoft documentation for ASP.NET Core. At the top of the page are two links: Home and Privacy. The Home link is the page you’re currently on. Clicking Privacy takes you to a new page, shown in figure 13.3. As you’ll see in section 13.1.3, you can use Razor Pages in your application to define these two pages and build the HTML they display.

默认情况下,此页面显示一个简单的欢迎横幅和一个指向 ASP.NET Core 的官方 Microsoft 文档的链接。页面顶部有两个链接:Home 和 Privacy。Home link (主页) 链接是您当前所在的页面。点击 Privacy 将带你到一个新页面,如图 13.3 所示。如第 13.1.3 节所示,您可以在应用程序中使用 Razor Pages 来定义这两个页面并构建它们显示的 HTML。

alt text

Figure 13.3 The Privacy page of your application. You can navigate between the two pages of the application using the Home and Privacy links in the application’s header. The app generates the content of the pages using Razor Pages.
图 13.3 应用程序的 Privacy 页面。您可以使用应用程序标题中的 Home 和 Privacy 链接在应用程序的两个页面之间导航。该应用使用 Razor Pages 生成页面内容。

At this point, you should notice a couple of things:
此时,您应该注意以下几点:

  • The header containing the links and the application title, WebApplication1, is the same on both pages.
    包含链接和应用程序标题 WebApplication1 的标题在两个页面上是相同的。

  • The title of the page, as shown in the tab of the browser, changes to match the current page. You’ll see how to achieve these features in chapter 17, when we discuss the rendering of HTML using Razor templates.
    页面的标题(如浏览器选项卡中所示)会更改以匹配当前页面。您将在第 17 章中了解如何实现这些功能,届时我们将讨论使用 Razor 模板呈现 HTML。

There isn’t any more to the user experience of the application at this stage. Click around a little, and when you’re happy with the behavior of the application, return to your editor, and look at the files included in the template.
在此阶段,应用程序的用户体验不再有任何变化。单击一下,当您对应用程序的行为感到满意时,返回到编辑器,并查看模板中包含的文件。

This Razor Pages app has much the same structure as the minimal API applications you’ve created throughout this book, as shown in figure 13.4. The overall structure is identical apart from two extra folders you haven’t seen before:
此 Razor Pages 应用程序的结构与您在本书中创建的最小 API 应用程序大致相同,如图 13.4 所示。除了两个您以前从未见过的额外文件夹外,整体结构是相同的:

  • Pages folder—This folder contains the Razor Pages files that define the various pages in your web app, including the Home and Privacy pages you’ve already seen.
    Pages 文件夹 - 此文件夹包含 Razor Pages 文件,这些文件定义 Web 应用程序中的各个页面,包括您已经看到的 Home 和 Privacy 页面。

  • wwwroot folder—This folder is special in that it’s the only folder in your application that browsers are allowed to access directly when browsing your web app. You can store your Cascading Style Sheets (CSS), JavaScript, images, or static HTML files here, and the static file middleware will serve them to browsers when requested. The template creates subfolders inside wwwroot, but you don’t have to use them; you can structure your static files however you want inside wwwroot.
    wwwroot 文件夹 - 此文件夹很特殊,因为它是应用程序中唯一允许浏览器在浏览 Web 应用程序时直接访问的文件夹。您可以在此处存储级联样式表 (CSS)、JavaScript、图像或静态 HTML 文件,静态文件中间件将在需要时将它们提供给浏览器。该模板会在 wwwroot 中创建子文件夹,但您不必使用它们;您可以在 wwwroot 中根据需要构建静态文件。

alt text

Figure 13.4 Comparing the project structure of a minimal API app with a Razor Pages app. The Razor Pages app contains all the same files and folders, as well as the Pages folder for the Razor Page definitions and the wwwroot file for static files that are served directly to the browser.
图 13.4 比较最小 API 应用与 Razor Pages 应用的项目结构。Razor Pages 应用包含所有相同的文件和文件夹,以及 Razor Page 定义的 Pages 文件夹以及 wwwroot 文件,用于直接提供给浏览器的静态文件。

Aside from these extra files, the only other difference between a Razor Pages app and a minimal API app is the Program.cs file. In section 13.1.2 you’ll see that the Razor Pages app uses the same basic structure in Program.cs but adds the extra services and middleware used in a typical Razor Pages app.

除了这些额外的文件之外,Razor Pages 应用和最小 API 应用之间的唯一其他区别是 Program.cs 文件。在第 13.1.2 节中,你将看到 Razor Pages 应用在 Program.cs 中使用相同的基本结构,但添加了典型 Razor Pages 应用中使用的额外服务和中间件。

13.1.2 Adding and configuring services

13.1.2 添加和配置服务

One of the nice things about working with ASP.NET Core applications is that the setup code is quite similar even for completely different application models. No matter whether you’re creating a Razor Pages application or using minimal APIs, your Program.cs contains the same six steps:

使用 ASP.NET Core 应用程序的一个好处是,即使对于完全不同的应用程序模型,设置代码也非常相似。无论您是创建 Razor Pages 应用程序还是使用最少的 API,您的 Program.cs 都包含相同的六个步骤:

  1. Create a WebApplicationBuilder instance.
    创建 WebApplicationBuilder 实例。

  2. Register the required services with the WebApplicationBuilder.
    将所需的服务注册到WebApplicationBuilder 的 Web 应用程序构建器。

  3. Call Build on the builder instance to create a WebApplication instance.
    在构建器实例上调用 Build() 以创建一个WebApplication 实例。

  4. Add middleware to the WebApplication to create a pipeline.
    将中间件添加到 WebApplication 以创建管道。

  5. Map the endpoints in your application.
    映射应用程序中的终端节点。

  6. Call Run() on the WebApplication to start the server and handle requests.
    在 WebApplication 上调用 Run() 以启动服务器并处理请求。

The following listing shows the Program.cs file for the Razor Pages app. This file uses a lot more middleware than you’ve seen previously, but the overall structure should be familiar.

以下清单显示了 Razor Pages 应用的 Program.cs 文件。此文件使用的中间件比您之前看到的要多得多,但整体结构应该很熟悉。

Listing 13.2 The Program.cs file for a Razor Pages app
列表 13.2 Razor Pages 应用程序的 Program.cs 文件

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();    ❶

WebApplication app = builder.Build();

if (!app.Environment.IsDevelopment())   ❷
{   ❷
    app.UseExceptionHandler("/Error");  ❷
    app.UseHsts()
}   ❷

app.UseHttpsRedirection();    ❸
app.UseStaticFiles();    ❸
app.UseRouting();     ❸
app.UseAuthorization();    ❸

app.MapRazorPages();    ❹

app.Run();

❶ Registers the required services to use the Razor Pages feature
注册使用 Razor Pages 功能所需的服务
❷ Conditionally adds middleware depending on the runtime environment
根据运行时环境有条件地添加中间件
❸ Additional middleware can be added to the middleware pipeline.
以将其他中间件添加到中间件管道中。
❹ Registers each Razor Page as an endpoint in your application
将每个 Razor 页面注册为应用程序中的终结点

In chapter 4 you learned about middleware and the importance of ordering when adding middleware to the pipeline. This example adds six pieces of middleware to the pipeline, two of which are added only when not running in development:
在第 4 章中,您了解了中间件以及将中间件添加到管道时排序的重要性。此示例将 6 个中间件添加到pipeline 中,其中两个仅在未在 development 中运行时添加:

  • ExceptionHandlerMiddleware—You learned about this middleware in chapters 4 and 5. This middleware catches exceptions thrown by middleware later in the pipeline and generates a friendly error page.
    ExceptionHandlerMiddleware — 您在第 4 章和第 5 章中了解了此中间件。此中间件稍后在管道中捕获中间件引发的异常,并生成一个友好的错误页面。

  • HstsMiddleware—This middleware sets security headers in your response, in line with industry best practices. See chapter 28 for details about it and other security-related middleware.
    HstsMiddleware — 此中间件根据行业最佳实践在响应中设置安全标头。有关它和其他与安全相关的中间件的详细信息,请参见 Chapter 28。

  • HttpsRedirectionMiddleware—This middleware ensures that your application responds only to secure (HTTPS) requests and is an industry best practice. We’ll look at HTTPS in chapter 28.
    HttpsRedirectionMiddleware — 此中间件可确保您的应用程序仅响应安全 (HTTPS) 请求,是行业最佳实践。我们将在第 28 章中介绍 HTTPS。

  • StaticFileMiddleware—As you saw in chapter 4, this middleware serves requests for static files (such as .css and .js files) from the wwwroot folder in your app.
    StaticFileMiddleware — 如第 4 章所示,此中间件为应用程序中 wwwroot 文件夹中的静态文件(如 .css 和 .js 文件)的请求提供服务。

  • RoutingMiddleware—The routing middleware is responsible for selecting the endpoint for an incoming request. WebApplication adds it by default, but as discussed in chapter 4, adding it explicitly ensures that it runs after the StaticFileMiddleware.
    RoutingMiddleware — 路由中间件负责为传入请求选择终端节点。 WebApplication 默认会添加它,但正如第 4 章所讨论的,显式添加它可以确保它在 StaticFileMiddleware 之后运行。

  • AuthorizationMiddleware—This middleware controls whether an endpoint is allowed to run based on the user making the request, but requires you also to configure authentication for your application. You’ll learn more about authentication in chapter 23 and authorization in chapter 24.
    AuthorizationMiddleware — 此中间件根据发出请求的用户控制是否允许终端节点运行,但还要求您为应用程序配置身份验证。您将在第 23 章中了解有关身份验证的更多信息,在第 24 章中了解有关授权的更多信息。

In addition to the middleware added explicitly, WebApplication automatically adds some extra middleware (as discussed in chapter 4), such as the EndpointMiddleware, which is automatically added to the end of the middleware pipeline. As with minimal APIs, the RoutingMiddleware selects which endpoint handler to execute, and the EndpointMiddleware executes the handler to generate a response.
除了显式添加的中间件之外,WebApplication 还会自动添加一些额外的中间件(如第 4 章所述),例如 EndpointMiddleware,它会自动添加到中间件管道的末尾。与最小 API 一样,RoutingMiddleware 选择要执行的端点处理程序,而 EndpointMiddleware 执行处理程序以生成响应。

Together, this pair of middleware is responsible for interpreting the request to determine which Razor Page to invoke, for reading parameters from the request, and for generating the final HTML. Little configuration is required; you need only add the middleware to the pipeline and specify that you want to use Razor Page endpoints by calling MapRazorPages. For each request, the routing middleware uses the request’s URL to determine which Razor Page to invoke. Then the endpoint middleware executes the Razor Page to generate the HTML response.

这对中间件共同负责解释请求以确定要调用的 Razor Page、从请求中读取参数以及生成最终 HTML。几乎不需要配置;您只需将中间件添加到管道,并通过调用 MapRazorPages 指定要使用 Razor Page 端点。对于每个请求,路由中间件使用请求的 URL 来确定要调用的 Razor Page。然后,终结点中间件执行 Razor Page 以生成 HTML 响应。

When the application is configured, it can start handling requests. But how does it handle them? In section 13.1.3 you’ll get a glimpse at Razor Pages and how they generate HTML.

配置应用程序后,它可以开始处理请求。但是它是如何处理的呢?在第 13.1.3 节中,您将了解 Razor Pages 以及它们如何生成 HTML。

13.1.3 Generating HTML with Razor Pages

13.1.3 使用 Razor Pages 生成 HTML

When an ASP.NET Core application receives a request, it progresses through the middleware pipeline until a middleware component handles it. Normally, the routing middleware matches a request URL’s path to a configured route, which defines which Razor Page to invoke, and the endpoint middleware invokes it.

当 ASP.NET Core 应用程序收到请求时,它会在中间件管道中前进,直到中间件组件处理该请求。通常,路由中间件将请求 URL 的路径与配置的路由匹配,该路由定义要调用的 Razor Page,然后终结点中间件调用它。

Razor Pages are stored in .cshtml files (a portmanteau of .cs and .html) within the Pages folder of your project. In general, the routing middleware maps request URL paths to a single Razor Page by looking in the Pages folder of your project for a Razor Page with the same path. If you look back at figure 13.3, for example, you see that the Privacy page of your app corresponds to the path /Privacy in the browser’s address bar. If you look inside the Pages folder of your project, you’ll find the Privacy.cshtml file, shown in the following listing.

Razor Pages 存储在项目的 Pages 文件夹中的 .cshtml 文件(.cs 和 .xhtml 的组合)中。通常,路由中间件通过在项目的 Pages 文件夹中查找具有相同路径的 Razor 页面,将请求 URL 路径映射到单个 Razor 页面。例如,如果你回头看一下图 13.3,你会看到你的应用程序的 Privacy 页面对应于浏览器地址栏中的路径 /Privacy。如果查看项目的 Pages 文件夹,您将找到 Privacy.cshtml 文件,如以下清单所示。

Listing 13.3 The Privacy.cshtml Razor Page
列表 13.3 Privacy.cshtml Razor 页面

@page       ❶
@model PrivacyModel         ❷
@{
    ViewData["Title"] = "Privacy Policy";       ❸
}
<h1>@ViewData["Title"]</h1>     ❹

<p>Use this page to detail your site's privacy policy.</p>    ❺

❶ Indicates that this is a Razor Page
表示这是一个 Razor 页面
❷ Links the Razor Page to a specific PageModel
将 Razor 页面链接到特定的 PageModel
❸ C# code that doesn’t write to the response
不写入响应的 C# 代码
❹ HTML with dynamic C# values written to the response
将动态 C# 值写入响应的 HTML
❺ Standalone, static HTML
独立的静态 HTML

Razor Pages use a templating syntax called Razor that combines static HTML with dynamic C# code and HTML generation. The @page directive on the first line of the Razor Page is the most important. This directive must always be placed on the first line of the file, as it tells ASP.NET Core that the .cshtml file is a Razor Page. Without it, you won’t be able to view your page correctly.

Razor Pages 使用一种称为 Razor 的模板语法,该语法将静态 HTML 与动态 C# 代码和 HTML 生成相结合。Razor 页面第一行上的 @page 指令是最重要的。此指令必须始终放在文件的第一行,因为它会告知 ASP.NET Core .cshtml 文件是 Razor 页面。没有它,您将无法正确查看您的页面。

The next line of the Razor Page defines which PageModel in your project the Razor Page is associated with:

Razor Page 的下一行定义 Razor Page 与项目中的哪个 PageModel 相关联:

@model PrivacyModel

In this case the PageModel is called PrivacyModel, and it follows the standard convention for naming Razor Page models. You can find this class in the Privacy.cshtml.cs file in the Pages folder of your project, as shown in figure 13.5. Visual Studio nests these files underneath the Razor Page .cshtml files in Solution Explorer. We’ll look at the page model in section 13.1.4.

在这种情况下,PageModel 称为 PrivacyModel,它遵循命名 Razor Page 模型的标准约定。您可以在项目的 Pages 文件夹中的 Privacy.cshtml.cs 文件中找到此类,如图 13.5 所示。Visual Studio 将这些文件嵌套在 Razor 页面下.cshtml 文件。我们将在 13.1.4 节中查看页面模型。

alt text

Figure 13.5 By convention, page models for Razor Pages are placed in a file with the same name as the Razor Page, with a .cs suffix appended. Visual Studio nests these files below the Razor Page in Solution Explorer.
图 13.5 按照惯例,Razor 页面的页面模型放置在与 Razor 页面同名的文件中,并附加了 .cs 后缀。Visual Studio 将这些文件嵌套在“解决方案资源管理器”中的 Razor 页面下方。

In addition to the @page and @model directives, static HTML is always valid in a Razor Page and will be rendered as is in the response:

除了 @page 和 @model 指令之外,静态 HTML 在 Razor 页面中始终有效,并将按原样在响应中呈现:

<p>Use this page to detail your site’s privacy policy.</p>

You can also write ordinary C# code in Razor templates by using this construct:
您还可以使用以下构造在 Razor 模板中编写普通的 C# 代码:

@{ /* C# code here */ }

Any code between the curly braces will be executed but won’t be written to the response. In the listing, you’re setting the title of the page by writing a key to the ViewData dictionary, but you aren’t writing anything to the response at this point:

大括号之间的任何代码都将被执行,但不会写入响应。在清单中,您通过向 ViewData 字典写入一个键来设置页面的标题 ,但此时您没有向响应写入任何内容:

@{
    ViewData["Title"] = "Privacy Policy";
}

Another feature shown in this template is that you can dynamically write C# variables and expressions to the HTML stream using the @ symbol. This ability to combine dynamic and static markup is what gives Razor Pages their power. In the example, you’re fetching the "Title" value from the ViewData dictionary and writing the values to the response inside an <h1> tag:

此模板中显示的另一个功能是,您可以使用 @ 符号将 C# 变量和表达式动态写入 HTML 流 。这种结合动态和静态标记的能力赋予了 Razor Pages 强大的功能。在此示例中,您将从 ViewData 字典中提取 “Title” 值,并将该值写入 <h1> 标签内的响应:

<h1>@ViewData["Title"]</h1>

At this point, you might be a little confused by the template in listing 13.3 when it’s compared with the output shown in figure 13.3. The title and the static HTML content appear in both the listing and figure, but some parts of the final web page don’t appear in the template. How can that be?

此时,当与图 13.3 中所示的输出进行比较时,您可能会对清单 13.3 中的模板感到困惑。标题和静态 HTML 内容同时显示在列表和图中,但最终网页的某些部分不会显示在模板中。这怎么可能呢?

Razor Pages have the concept of layouts, which are base templates that define the common elements of your application, such as headers and footers. The HTML of the layout combines with the Razor Page template to produce the final HTML that’s sent to the browser. Layouts prevent you from having to duplicate code for the header and footer in every page, and mean that if you need to tweak something, you’ll need to do it in only one place.

Razor Pages 具有布局的概念,布局是定义应用程序的常见元素(如页眉和页脚)的基本模板。布局的 HTML 与 Razor Page 模板相结合,以生成发送到浏览器的最终 HTML。布局可以防止你为每个页面的页眉和页脚重复代码,这意味着如果你需要调整某些内容,只需在一个地方进行。

Note I cover Razor templates, including layouts, in detail in chapter 17. You can find layouts in the Pages/Shared folder of your project.
注意 我在第 17 章中详细介绍了 Razor 模板,包括布局。您可以在项目的 Pages/Shared 文件夹中找到布局。

As you’ve already seen, you can include C# code in your Razor Pages by using curly braces @{ }, but generally speaking, you’ll want to limit the code in your .cshtml file to presentational concerns only. Complex logic, code to access services such as a database, and data manipulation should be handled in the PageModel instead.

如你所见,你可以使用大括号 @{ } 在 Razor Pages 中包含 C# 代码,但一般来说,你需要将 .cshtml 文件中的代码限制为仅表示性问题。复杂的逻辑、访问服务(如数据库)的代码以及数据作应该在 PageModel 中处理。

13.1.4 Handling request logic with page models and handlers

13.1.4 使用页面模型和处理程序处理请求逻辑

As you’ve already seen, the @page directive in a .cshtml file marks the page as a Razor Page, but most Razor Pages also have an associated page model. By convention, this page model is placed in a file commonly known as a code-behind file that has a .cs extension, as you saw in figure 13.5. Page models should derive from the PageModel base class, and they typically contain one or more methods called page handlers that define how to handle requests to the Razor Page.

如你所见,.cshtml 文件中的 @page 指令将页面标记为 Razor 页面,但大多数 Razor 页面也有关联的页面模型。按照惯例,此页面模型被放置在一个通常称为代码隐藏文件的文件中,该文件具有 .cs 扩展名,如图 13.5 所示。页面模型应派生自 PageModel 基类,并且它们通常包含一个或多个称为 page 的方法定义如何处理对 Razor Page 的请求的处理程序。

Definition A page handler is the Razor Pages equivalent of a minimal API endpoint handler; it’s a method that runs in response to a request. Razor Page models must be derived from the PageModel class. They can contain multiple page handlers, though typically they contain only one or two.
定义 页面处理程序相当于最小 API 端点处理程序的 Razor Pages;它是为响应请求而运行的方法。Razor Page 模型必须派生自 PageModel 类。它们可以包含多个页面处理程序,但通常只包含一个或两个。

The following listing shows the page model for the Privacy.cshtml Razor Page, located in the file Privacy.cshtml.cs.

以下列表显示了位于文件 Privacy.cshtml.cs 中的 Privacy.cshtml Razor 页面的页面模型。

Listing 13.4 The PrivacyModel in Privacy.cshtml.cs: A Razor Page page model
清单 13.4 Privacy.cshtml.cs中的 PrivacyModel:一个Razor 页面模型

public class PrivacyModel: PageModel        ❶
{
    private readonly ILogger<PrivacyModel> _logger;      ❷ 
    public PrivacyModel(ILogger<PrivacyModel> logger)    ❷ 
    {                                                    ❷ 
        _logger = logger;                                ❷ 
    }                                                    ❷ 

  public void OnGet()      ❸ 
    {
    }
}

❶ Razor Pages must inherit from PageModel.
Razor Pages 必须继承自 PageModel。
❷ You can use dependency injection to provide services in the constructor.
您可以使用依赖注入在构造函数中提供服务。
❸ The default page handler is OnGet. Returning void indicates HTML should be generated.
默认页面处理程序为 OnGet。返回 void 表示应生成 HTML。

This page model is extremely simple, but it demonstrates a couple of important points:
此页面模型非常简单,但它演示了几个要点:

  • Page handlers are driven by convention.
    页面处理程序由约定驱动。

  • Page models can use dependency injection (DI) to interact with other services.
    页面模型可以使用依赖关系注入 (DI) 与其他服务进行交互。

Page handlers are typically named by convention, based on the HTTP verb they respond to. They return either void, indicating that the Razor
Page’s template should be rendered, or an IActionResult that contains other instructions for generating the response, such as redirecting the user to a different page.

页面处理程序通常根据它们响应的 HTTP 动词按约定命名。它们返回 void(指示应呈现 Razor 页面的模板)或 IActionResult(包含用于生成响应的其他说明,例如将用户重定向到其他页面)。

The PrivacyModel contains a single handler, OnGet, which indicates that it should run in response to GET requests for the page. As the method returns void, executing the handler executes the associated Razor template for the page to generate the HTML.

PrivacyModel 包含一个处理程序 OnGet,它指示它应该运行以响应页面的 GET 请求。当该方法返回 void 时,执行处理程序将执行页面的关联 Razor 模板以生成 HTML。

Note Razor Pages are focused on building page-based apps, so you typically want to return HTML rather than JSON or XML. You can also use an IActionResult to return any sort of data, to redirect users to a new page, or to send an error. You’ll learn more about IActionResults in chapter 15.
注意 Razor Pages 专注于构建基于页面的应用,因此你通常需要返回 HTML,而不是 JSON 或 XML。您还可以使用 IActionResult 返回任何类型的数据、将用户重定向到新页面或发送错误。您将在第 15 章中了解有关 IActionResult的更多信息。

DI is used to inject an ILogger<PrivacyModel> instance into the constructor of the page model the same way you would inject a service into a minimal API endpoint handler. The service is unused in this example, but you’ll learn all about ILogger in chapter 26.

DI 用于将 ILogger<PrivacyModel> 实例注入页面模型的构造函数中,就像将服务注入最小 API 终端节点处理程序中一样。在此示例中,该服务未使用,但您将在第 26 章中了解有关 ILogger 的所有信息。

Clearly, the PrivacyModel page model doesn’t do much in this case, and you may be wondering why it’s worth having. If all page models do is tell the Razor Page to generate HTML, why do we need them at all?

显然,在这种情况下,PrivacyModel 页面模型的作用不大,您可能想知道为什么值得拥有它。如果所有页面模型都只是告诉 Razor 页面生成 HTML,那么我们为什么还需要它们呢?

The key thing to remember here is that now you have a framework for performing arbitrarily complex functions in response to a request. You could easily update the handler method to load data from the database, send an email, add a product to a basket, or create an invoice—all in response to a simple HTTP request. This extensibility is where a lot of the power in Razor Pages (and the MVC pattern in general) lies.

这里要记住的关键是,现在你有一个框架来执行任意复杂的功能以响应请求。您可以轻松更新处理程序方法,以从数据库加载数据、发送电子邮件、将产品添加到购物篮或创建发票 - 所有这些都是为了响应简单的 HTTP 请求。这种可扩展性是 Razor Pages(以及一般的 MVC 模式)中许多功能所在。

The other important point is that you’ve separated the execution of these methods from the generation of the HTML. If the logic changes, and you need to add behavior to a page handler, you don’t need to touch the HTML generation code, so you’re less likely to introduce bugs. Conversely, if you need to change the UI slightly (change the color of the title, for example), your handler method logic is safe.

另一个重要的一点是,您已经将这些方法的执行与 HTML 的生成分开。如果逻辑发生变化,并且您需要向页面处理程序添加行为,则无需接触 HTML 生成代码,因此不太可能引入错误。相反,如果您需要稍微更改 UI(例如,更改标题的颜色),则处理程序方法逻辑是安全的。

And there you have it—a complete ASP.NET Core Razor Pages application! Before we move on, let’s take one last look at how your application handles a request. Figure 13.6 shows a request to the /Privacy path being handled by the sample application. You’ve seen everything here already, so the process of handling a request should be familiar. The figure shows how the request passes through the middleware pipeline before being handled by the endpoint middleware. The Privacy.cshtml Razor Page executes the OnGet handler and generates the HTML response, which passes back through the middleware to the ASP.NET Core web server before being sent to the user’s browser.

这就是一个完整的 ASP.NET Core Razor Pages 应用程序!在继续之前,让我们最后看看您的应用程序如何处理请求。图 13.6 显示了对示例应用程序正在处理的 /Privacy 路径的请求。您已经在这里看到了所有内容,因此处理请求的过程应该很熟悉。该图显示了请求在由终端节点中间件处理之前如何通过中间件管道。Privacy.cshtml Razor 页面执行 OnGet 处理程序并生成 HTML 响应,该响应传回通过中间件发送到 ASP.NET Core Web 服务器,然后再发送到用户的浏览器。

alt text

Figure 13.6 An overview of a request to the /Privacy URL for the sample ASP.NET Razor Pages application. The routing middleware routes the request to the OnGet handler of the Privacy.cshtml.cs Razor Page. The Razor Page generates an HTML response by executing the Razor template in Privacy.cshtml and passes the response back through the middleware pipeline to the browser.
图 13.6 对示例 ASP.NET Razor Pages 应用程序的 /Privacy URL 的请求概述。路由中间件将请求路由到 Privacy.cshtml.cs Razor Page 的 OnGet 处理程序。Razor 页面通过在 Privacy.cshtml 中执行 Razor 模板来生成 HTML 响应,并通过中间件管道将响应传回浏览器。

We’ve reached the end of this section working through the template, so you have a good overview of how an entire Razor Pages application is configured and how it handles a request using Razor Pages. In section 13.2 we take the basic Razor Pages in the default template a bit further, looking at a more complex example.

我们已完成本部分的末尾,逐步了解了模板的配置方式,以及它如何使用 Razor Pages 处理请求。在第 13.2 节中,我们将默认模板中的基本 Razor Pages 进一步介绍,并查看更复杂的示例。

13.2 Exploring a typical Razor Page

13.2 探索典型的 Razor 页面

The Razor Pages programming model was introduced in ASP.NET Core 2.0 as a way to build server-side rendered page-based websites. It builds on top of the ASP.NET Core infrastructure to provide a streamlined experience, using conventions where possible to reduce the amount of boilerplate code and configuration required. In this section we’ll look at a more complex page model to better understand the overall design of Razor Pages.

Razor Pages 编程模型是在 ASP.NET Core 2.0 中引入的,作为构建服务器端呈现的基于页面的网站的一种方式。它建立在 ASP.NET Core 基础设施之上,以提供简化的体验,并尽可能使用约定来减少所需的样板代码和配置的数量。本节内容我们将查看更复杂的页面模型,以更好地了解 Razor Pages 的整体设计。

In listing 13.4 you saw a simple Razor Page that didn’t contain any logic; instead, it only rendered the associated Razor view. This pattern may be common if you’re building a content-heavy marketing website, for example, but more commonly your Razor Pages will contain some logic, load data from a database, or use forms to allow users to submit information.

在清单 13.4 中,你看到了一个简单的 Razor 页面,它不包含任何逻辑;相反,它只呈现关联的 Razor 视图。例如,如果您正在构建内容繁重的营销网站,则此模式可能很常见,但更常见的是,您的 Razor 页面将包含一些逻辑、从数据库加载数据或使用表单来允许用户提交信息。

To give you more of a flavor of how typical Razor Pages work, in this section we look briefly at a slightly more complex Razor Page. This page is taken from a to-do list application and is used to display all the to-do items for a given category. We’re not focusing on the HTML generation at this point, so the following listing shows only the PageModel code-behind file for the Razor Page.

为了让您更深入地了解典型的 Razor 页面的工作原理,在本节中,我们将简要介绍一个稍微复杂一些的 Razor 页面。此页面取自待办事项列表应用程序,用于显示给定类别的所有待办事项。目前,我们不关注 HTML 生成,因此下面的清单仅显示 Razor Page 的 PageModel 代码隐藏文件。

Listing 13.5 A Razor Page for viewing all to-do items in a given category
清单 13.5 用于查看所有待办事项的 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 ToDoService is provided in the model constructor using DI.
ToDoService 是使用 DI 在模型构造函数中提供的。
❷ The OnGet handler takes a parameter, category.
OnGet 处理程序采用参数 category。
❸ The handler calls out to the ToDoService to retrieve data and sets the Items property.
处理程序调用 ToDoService 以检索数据并设置 Items 属性。
❹ Returns a PageResult indicating the Razor view should be rendered
返回一个 PageResult,指示应呈现 Razor 视图
❺ The Razor View can access the Items property when it’s rendered.
Razor 视图在呈现时可以访问 Items 属性。

This example is still relatively simple, but it demonstrates a variety of features compared with the basic example from listing 13.4:
这个例子仍然相对简单,但与 清单 13.4 中的基本例子相比,它演示了各种特性:

  • The page handler, OnGet, accepts a method parameter, category. This parameter is automatically populated using values from the incoming request via model binding, similar to the way binding works with minimal APIs. I discuss Razor Pages model binding in detail in chapter 16.
    页面处理程序 OnGet 接受方法参数 category。此参数通过模型绑定使用传入请求中的值自动填充,类似于绑定使用最小 API 的方式。我在第 16 章中详细讨论了 Razor Pages 模型绑定。

  • The handler doesn’t interact with the database directly. Instead, it uses the category value provided to interact with the ToDoService, which is injected as a constructor argument using DI.
    处理程序不直接与数据库交互。相反,它使用提供的 category 值与 ToDoService 交互,后者使用 DI 作为构造函数参数注入。

  • The handler returns Page() at the end of the method to indicate that the associated Razor view should be rendered. The return statement is optional in this case; by convention, if the page handler is a void method, the Razor view will still be rendered, behaving as though you called return Page() at the end of the method.
    处理程序在方法末尾返回 Page(),以指示应呈现关联的 Razor 视图。在这种情况下,return 语句是可选的;按照约定,如果页面处理程序是 void 方法,则仍将呈现 Razor 视图,其行为就像在方法末尾调用 return Page() 一样。

  • The Razor View has access to the CategoryModel instance, so it can access the Items property that’s set by the handler. It uses these items to build the HTML that is ultimately sent to the user.
    Razor 视图有权访问 CategoryModel 实例,因此它可以访问处理程序设置的 Items 属性。它使用这些项来构建最终发送给用户的 HTML。‌

The pattern of interactions in the Razor Page of listing 13.5 shows a common pattern. The page handler is the central controller for the Razor Page. It receives an input from the user (the category method parameter); calls out to the “brains” of the application (the ToDoService); and passes data (by exposing the Items property) to the Razor view, which generates the HTML response. If you squint, this pattern looks like the MVC design pattern.

清单 13.5 的 Razor Page 中的交互模式显示了一种常见模式。页面处理程序是 Razor Page 的中央控制器。它接收来自用户的输入(category 方法参数);调用应用程序的“大脑”(ToDoService);并将数据(通过公开 Items 属性)传递到 Razor 视图,后者会生成 HTML 响应。如果您眯着眼睛看,此模式看起来类似于 MVC 设计模式。

Depending on your background in software development, you may have come across the MVC pattern in some form. In web development, MVC is a common paradigm, used in frameworks such as Django, Rails, and Spring MVC. But as it’s such a broad concept, you can find MVC in everything from mobile apps to rich-client desktop applications. I hope that indicates the benefits of the pattern when it’s used correctly! In section 13.3 we’ll look at the MVC pattern in general and how ASP.NET Core uses it.

根据你的软件开发背景,你可能遇到过某种形式的 MVC 模式。在 Web 开发中,MVC 是一种常见的范式,用于 Django、Rails 和 Spring MVC 等框架。但是,由于 MVC 是一个如此广泛的概念,因此您可以在从移动应用程序到富客户端桌面应用程序的所有应用程序中找到 MVC。我希望这表明正确使用该模式的好处!在 Section 13.3 中,我们将大致了解 MVC 模式以及 ASP.NET Core 如何使用它。

13.3 Understanding the MVC design pattern

13.3 了解 MVC 设计模式

The MVC design pattern is a common pattern for designing apps that have UIs. The MVC pattern has many interpretations, each of which focuses on a slightly different aspect of the pattern. The original MVC design pattern was specified with rich-client graphical user interface (GUI) apps in mind, rather than web applications, so it uses terminology and paradigms associated with a GUI environment. Fundamentally, though, the pattern aims to separate the management and manipulation of data from its visual representation.

MVC 设计模式是设计具有 UI 的应用程序的常见模式。MVC 模式有多种解释,每种解释都侧重于模式的一个略有不同的方面。最初的 MVC 设计模式在指定时考虑了富客户端图形用户界面 (GUI) 应用程序,而不是 Web 应用程序,因此它使用了与 GUI 环境相关的术语和范例。不过,从根本上说,该模式旨在将数据的管理和作与其视觉表示分开。

Before I dive too far into the design pattern itself, let’s consider a typical Razor Pages request. Imagine that a user requests the Razor Page from listing 13.5 that displays a to-do list category. Figure 13.7 shows how a Razor Page handles different aspects of a request, all of which combine to generate the final response.

在深入研究设计模式本身之前,让我们考虑一个典型的 Razor Pages 请求。假设用户从显示待办事项列表类别的清单 13.5 中请求 Razor 页面。图 13.7 显示了 Razor Page 如何处理请求的不同方面,所有这些方面结合起来生成最终响应。

alt text

Figure 13.7 Requesting a to-do list page for a Razor Pages application. A different component handles each aspect of the request.
图 13.7 请求 Razor Pages 应用程序的待办事项列表页面。不同的组件处理请求的每个方面。

In general, three components make up the MVC design pattern:

通常,MVC 设计模式由三个组件组成:

  • Model—The data that needs to be displayed—the global state of the application. It’s accessed via the ToDoService in listing 13.5.
    Model (模型) - 需要显示的数据 - 应用程序的全局状态。它可以通过清单 13.5 中的 ToDoService 访问。

  • View—The template that displays the data provided by the model.
    View (视图) - 显示模型提供的数据的模板。

  • Controller—Updates the model and provides the data for display to the view. This role is taken by the page handler in Razor Pages—the OnGet method in listing 13.5.
    Controller (控制器) - 更新模型并提供数据以显示在视图中。此角色由 Razor Pages 中的页面处理程序(列表 13.5 中的 OnGet 方法)担任。

Each component of the MVC design pattern is responsible for a single aspect of the overall system, which, when combined, generates a UI. The to-do list example considers MVC in terms of a web application using Razor Pages, but a generalized request could be equivalent to the click of a button in a desktop GUI application.

MVC 设计模式的每个组件都负责整个系统的一个方面,当这些方面组合在一起时,将生成一个 UI。待办事项列表示例从使用 Razor Pages 的 Web 应用程序的角度考虑 MVC,但通用请求可能等效于单击桌面 GUI 应用程序中的按钮。

In general, the order of events when an application responds to a user interaction or request is as follows:

通常,应用程序响应用户交互或请求时的事件顺序如下:

  1. The controller (the Razor Page handler) receives the request.
    控制器 (Razor Page 处理程序) 接收请求。

  2. Depending on the request, the controller either fetches the requested data from the application model using injected services or updates the data that makes up the model.
    根据请求,控制器要么使用注入的服务从应用程序模型中获取请求的数据,要么更新构成模型的数据。

  3. The controller selects a view to display and passes a representation of the model (the view model) to it.
    控制器选择要显示的视图,并将模型的表示 (视图模型) 传递给该视图。

  4. The view uses the data contained in the model to generate the UI.
    该视图使用模型中包含的数据来生成 UI。

When we describe MVC in this format, the controller (the Razor Page handler) serves as the entry point for the interaction. The user communicates with the controller to instigate an interaction. In web applications, this interaction takes the form of an HTTP request, so when a request to a URL is received, the controller handles it.

当我们以这种格式描述 MVC 时,控制器(Razor Page 处理程序)将用作交互的入口点。用户与控制器通信以引发交互。在 Web 应用程序中,此交互采用 HTTP 请求的形式,因此当收到对 URL 的请求时,控制器会对其进行处理。

Depending on the nature of the request, the controller may take a variety of actions, but the key point is that the actions are undertaken using the application model. The model here contains all the business logic for the application, so it’s able to provide requested data or perform actions.

根据请求的性质,控制器可能会采取各种作,但关键是这些作是使用应用程序模型执行的。此处的模型包含应用程序的所有业务逻辑,因此它能够提供请求的数据或执行作。

Note In this description of MVC, the model is considered to be a complex beast, containing all the logic for how to perform an action, as well as any internal state. The Razor Page PageModel class is not the model we’re talking about! Unfortunately, as in all software development, naming things is hard.
注意 在 MVC 的这个描述中,模型被认为是一个复杂的野兽,包含如何执行作的所有逻辑,以及任何内部状态。Razor Page PageModel 类不是我们正在谈论的模型!不幸的是,就像在所有软件开发中一样,命名事物是困难的。

Consider a request to view a product page for an e-commerce application. The controller would receive the request and know how to contact some product service that’s part of the application model. This service might fetch the details of the requested product from a database and return them to the controller.

考虑查看电子商务应用程序的产品页面的请求。控制器将收到请求并知道如何联系属于应用程序模型的某些产品服务。此服务可能会从数据库中获取所请求产品的详细信息,并将其返回给控制器。

Alternatively, imagine that a controller receives a request to add a product to the user’s shopping cart. The controller would receive the request and most likely would invoke a method on the model to request that the product be added. Then the model would update its internal representation of the user’s cart, by adding (for example) a new row to a database table holding the user’s data.

或者,假设控制器收到将产品添加到用户购物车的请求。控制器将收到请求,并且很可能会调用模型上的方法以请求添加产品。然后,该模型将更新其用户购物车的内部表示,方法是向包含用户数据的数据库表添加新行(例如)。

Tip You can think of each Razor Page handler as being a mini controller focused on a single page. Every web request is another independent call to a controller that orchestrates the response. Although there are many controllers, all the handlers interact with the same application model.
提示 您可以将每个 Razor Page 处理程序视为一个专注于单个页面的迷你控制器。每个 Web 请求都是对编排响应的控制器的另一个独立调用。尽管有许多控制器,但所有处理程序都与相同的应用程序模型交互。

After the model has been updated, the controller needs to decide what response to generate. One of the advantages of using the MVC design pattern is that the model representing the application’s data is decoupled from the final representation of that data, called the view. The controller is responsible for deciding whether the response should generate an HTML view, whether it should send the user to a new page, or whether it should return an error page.

更新模型后,控制器需要决定要生成什么响应。使用 MVC 设计模式的一个优点是,表示应用程序数据的模型与该数据的最终表示形式(称为视图)分离。控制器负责决定响应是否应生成 HTML 视图,是否应将用户发送到新页面,或者是否应返回错误页面。

One of the advantages of the model’s being independent of the view is that it improves testability. UI code is classically hard to test, as it’s dependent on the environment; anyone who has written UI tests simulating a user clicking buttons and typing in forms knows that it’s typically fragile. By keeping the model independent of the view, you can ensure that the model stays easily testable, without any dependencies on UI constructs. As the model often contains your application’s business logic, this is clearly a good thing!

模型独立于视图的优点之一是它提高了可测试性。UI 代码通常很难测试,因为它依赖于环境;任何编写过模拟用户单击按钮和在表单中键入的 UI 测试的人都知道,它通常是脆弱的。通过使模型独立于视图,您可以确保模型易于测试,而不依赖于 UI 构造。由于该模型通常包含应用程序的业务逻辑,这显然是一件好事!

The view can use the data passed to it by the controller to generate the appropriate HTML response. The view is responsible only for generating the final representation of the data; it’s not involved in any of the business logic.

视图可以使用控制器传递给它的数据来生成适当的 HTML 响应。视图仅负责生成数据的最终表示形式;它不涉及任何业务逻辑。

This is all there is to the MVC design pattern in relation to web applications. Much of the confusion related to MVC seems to stem from slightly different uses of the term for slightly different frameworks and types of applications. In section 13.4 I’ll show how the ASP.NET Core framework uses the MVC pattern with Razor Pages, along with more examples of the pattern in action.

这就是 MVC 设计模式与 Web 应用程序相关的全部内容。许多与 MVC 相关的混淆似乎源于该术语对框架和应用程序类型的用法略有不同。在第 13.4 节中,我将展示 ASP.NET Core 框架如何将 MVC 模式与 Razor Pages 结合使用,以及该模式的更多实际示例。

13.4 Applying the MVC design pattern to Razor Pages

13.4 将 MVC 设计模式应用于 Razor Pages

In section 13.3 I discussed the MVC pattern as it’s typically used in web applications; Razor Pages use this pattern. But ASP.NET Core also includes a framework called ASP.NET Core MVC. This framework (unsurprisingly) closely mirrors the MVC design pattern, using controllers containing action methods in place of Razor Pages and page handlers. Razor Pages builds directly on top of the underlying ASP.NET Core MVC framework, using the MVC framework under the hood for their behavior.

在 Section 13.3 中,我讨论了 MVC 模式,因为它通常用于 Web 应用程序;Razor Pages 使用此模式。但 ASP.NET Core 还包括一个名为 ASP.NET Core MVC 的框架。此框架(不出所料)与 MVC 设计模式紧密镜像,使用包含作方法的控制器来代替 Razor Pages 和页面处理程序。Razor Pages 直接构建在基础 ASP.NET Core MVC 框架之上,并在后台使用 MVC 框架来实现其行为。

If you prefer, you can avoid Razor Pages and work with the MVC framework directly in ASP.NET Core. This option was the only one in early versions of ASP.NET Core and the previous version of ASP.NET.

如果愿意,可以避免使用 Razor Pages,直接在 ASP.NET Core 中使用 MVC 框架。此选项是 ASP.NET Core 早期版本和 ASP.NET 早期版本中的唯一选项。

Tip I look in greater depth at choosing between Razor Pages and the MVC framework in chapter 19.
提示 在第 19 章中,我将更深入地了解如何在 Razor Pages 和 MVC 框架之间进行选择。

In this section we look in greater depth at how the MVC design pattern applies to Razor Pages in ASP.NET Core. This section will also help clarify the role of various features of Razor Pages.

在本部分中,我们将更深入地了解 MVC 设计模式如何应用于 ASP.NET Core 中的 Razor 页面。这部分还将有助于阐明 Razor Pages 的各种功能的作用。

Do Razor Pages use MVC or MVVM?
Razor Pages 使用 MVC 还是 MVVM?
Occasionally, I’ve seen people describe Razor Pages as using the Model-View-View Model (MVVM) design pattern rather than the MVC design pattern. don’t agree, but it’s worth being aware of the differences.
有时,我看到人们将 Razor Pages 描述为使用模型-视图-视图模型 (MVVM) 设计模式,而不是 MVC 设计模式。我不同意,但值得意识到差异。
MVVM is a UI pattern that is often used in mobile apps, desktop apps, and some client-side frameworks. It differs from MVC in that there is a bidirectional interaction between the view and the view model. The view model tells the view what to display, but the view can also trigger changes directly on the view model. It’s often used with two-way data binding where a view model is bound to a view.
MVVM 是一种 UI 模式,通常用于移动应用程序、桌面应用程序和一些客户端框架。它与 MVC 的不同之处在于,视图和视图模型之间存在双向交互。视图模型告诉视图要显示什么,但视图也可以直接在视图模型上触发更改。它通常与双向数据绑定一起使用,其中视图模型绑定到视图。
Some people consider the Razor Pages PageModel to be filling this role, but I’m not convinced. Razor Pages definitely seems based on the MVC pattern to me (it’s based on the ASP.NET Core MVC framework after all!), and it doesn’t have the same two-way binding that I would expect with MVVM.
有些人认为 Razor Pages PageModel 正在扮演这个角色,但我不相信。在我看来,Razor Pages 绝对是基于 MVC 模式的(毕竟它基于 ASP.NET Core MVC 框架!),并且它没有我所期望的 MVVM 的双向绑定。

As you’ve seen in previous chapters, ASP.NET Core implements Razor Page endpoints using a combination of RoutingMiddleware and EndpointMiddleware, as shown in figure 13.8. When a request has been processed by earlier middleware (and assuming that none has handled the request and short-circuited the pipeline), the routing middleware selects which Razor Page handler should be executed, and the endpoint middleware executes the page handler.
如前几章所示,ASP.NET Core 使用 RoutingMiddleware 和 EndpointMiddleware 的组合实现 Razor Page 终结点,如图 13.8 所示。当请求由早期中间件处理时(并假设没有中间件处理请求并使管道短路),路由中间件将选择应执行哪个 Razor Page 处理程序,而终结点中间件将执行页面处理程序。

alt text

Figure 13.8 The middleware pipeline for a typical ASP.NET Core application. The request is processed by middleware in sequence. If the request reaches the routing middleware, the middleware selects an endpoint, such as a Razor Page, to execute. The endpoint middleware executes the selected endpoint.
图 13.8 典型 ASP.NET Core 应用程序的中间件管道。中间件会按顺序处理该请求。如果请求到达路由中间件,中间件会选择一个端点(例如 Razor Page)来执行。端点中间件执行选定的端点。

As you’ve seen in earlier chapters, middleware often handles cross-cutting concerns or narrowly defined requests, such as requests for files. For requirements that fall outside these functions or that have many external dependencies, a more robust framework is required. Razor Pages (and/or ASP.NET Core MVC) can provide this framework, allowing interaction with your application’s core business logic and the generation of a UI. It handles everything from mapping the request to an appropriate page handler (or controller action method) to generating the HTML response.

正如你在前面的章节中所看到的,中间件通常处理横切关注点或狭义定义的请求,比如文件请求。对于超出这些功能或具有许多外部依赖项的需求,需要更健壮的框架。Razor Pages(和/或 ASP.NET Core MVC)可以提供此框架,允许与应用程序的核心业务逻辑交互并生成 UI。它处理从将请求映射到适当的页面处理程序(或控制器作方法)到生成 HTML 响应的所有作。

In the traditional description of the MVC design pattern, there’s only a single type of model, which holds all the non-UI data and behavior. The controller updates this model as appropriate and then passes it to the view, which uses it to generate a UI.

在 MVC 设计模式的传统描述中,只有一种类型的模型,它包含所有非 UI 数据和行为。控制器会根据需要更新此模型,然后将其传递给视图,视图会使用它来生成 UI。

One of the problems when discussing MVC is the vague and ambiguous terms that it uses, such as controller and model. Model in particular is such an overloaded term that it’s often difficult to be sure exactly what it refers to; is it an object, a collection of objects, or an abstract concept? Even ASP.NET Core uses the word model to describe several related but different components, as you’ll see later in this chapter.

讨论 MVC 时的问题之一是它使用的术语模糊不清,例如 controller 和 model。特别是 Model 是一个超载的术语,以至于通常很难确定它到底指的是什么;它是一个对象、对象的集合还是一个抽象概念?Even ASP.NET Core 也使用单词 model 来描述几个相关但不同的组件,您将在本章后面看到。

13.4.1 Directing a request to a Razor Page and building a binding model

13.4.1 将请求定向到 Razor 页面并构建绑定模型

The first step when your app receives a request is routing the request to an appropriate Razor Page handler in the routing middleware. Let’s think again about the category to-do list page in listing 13.5. On that page, you’re displaying a list of items that have a given category label. If you’re looking at the list of items with a category of Simple, you’d make a request to the /category/Simple path.

应用收到请求时的第一步是将请求路由到路由中间件中的相应 Razor Page 处理程序。让我们再考虑一下清单 13.5 中的类别 to- do 列表页面。在该页面上,将显示具有给定类别标签的项目列表。如果您正在查看类别为 Simple 的项目列表,则需要向 /category/Simple 路径发出请求。

Routing maps a request URL, /category/Simple, against the route patterns registered with your application. You’ve seen how this process works for minimal APIs, and it’s the same for Razor Pages; each route template corresponds to a Razor Page endpoint. You’ll learn more about routing with Razor Pages in chapter 14.

路由将请求 URL /category/Simple 映射到应用程序中注册的路由模式。您已经了解了此过程如何适用于最小 API,对于 Razor Pages 也是如此;每个路由模板对应于一个 Razor Page 终结点。您将在第 14 章中了解有关使用 Razor Pages 进行路由的更多信息。

Tip I’m using the term Razor Page to refer to the combination of the Razor view and the PageModel that includes the page handler. Note that that PageModel class is not the model we’re referring to when describing the MVC pattern. It fulfills other roles, as you’ll see later in this section.
提示 我使用术语 Razor Page 来指代 Razor 视图和包含页面处理程序的 PageModel 的组合。请注意,PageModel 类不是我们在描述 MVC 模式时所指的模型。它履行其他角色,如本节后面所述。

When a page handler is selected in the routing middleware, the request continues down the middleware pipeline until it reaches the endpoint middleware, where the Razor Page executes.

在路由中间件中选择页面处理程序后,请求将继续沿中间件管道向下,直到到达执行 Razor Page 的终结点中间件。

First, the binding model (if applicable) is generated. This model is built from the incoming request, based on the properties of the PageModel marked for binding and the method parameters required by the page handler, as shown in figure 13.9. A binding model is normally one or more standard C# objects and works similarly to the way it works in minimal APIs, as you saw in chapter 6. We’ll look at Razor Page binding models in detail in chapter 16.

首先,生成绑定模型(如果适用)。此模型是根据传入请求构建的,基于标记为绑定的 PageModel 属性和页面处理程序所需的方法参数,如图 13.9 所示。绑定模型通常是一个或多个标准 C# 对象,其工作方式类似于它在最小 API 中的工作方式,如第 6 章所示。我们将在第 16 章中详细介绍 Razor Page 绑定模型。

alt text

Figure 13.9 Routing a request to a Razor Page and building a binding model. A request to the /category/Simple URL results in the execution of the CategoryModel.OnGet page handler, passing in a populated binding model, category.
图 13.9 将请求路由到 Razor 页面并构建绑定模型。对/category/Simple URL 会导致执行 CategoryModel.OnGet 页面处理程序,并传入填充的绑定模型 category。

Definition A binding model is one or more objects that act as a container for the data provided in a request—data that’s required by a page handler.
定义 绑定模型是一个或多个对象,它们充当请求中提供的数据的容器,这些数据是由页面处理程序要求。

In this case, the binding model is a simple string, category, that’s bound to the "Simple" value. This value is provided in the request URL’s path. A more complex binding model could have been used, with multiple properties populated with values from the route template, the query string, and the request body.

在本例中,绑定模型是绑定到 “Simple” 值的简单字符串 category。此值在请求 URL 的路径中提供。可以使用更复杂的绑定模型,其中多个属性填充了路由模板、查询字符串和请求正文中的值。

Note The binding model for Razor Pages is conceptually equivalent to all the parameters you pass in to a minimal API endpoint that are populated from the request.
注意 Razor Pages 的绑定模型在概念上等效于您传递给从请求填充的最小 API 端点的所有参数。

The binding model in this case corresponds to the method parameter of the OnGet page handler. An instance of the Razor Page is created using its constructor, and the binding model is passed to the page handler when it executes, so it can be used to decide how to respond. For this example, the page handler uses it to decide which to-do items to display on the page.

在这种情况下,绑定模型对应于 OnGet 页面处理程序的 method 参数。Razor Page 的实例是使用其构造函数创建的,绑定模型在执行时传递给页面处理程序,因此可用于决定如何响应。在此示例中,页面处理程序使用它来决定要在页面上显示哪些待办事项。

13.4.2 Executing a handler using the application model

13.4.2 使用应用程序模型执行处理程序

The role of the page handler as the controller in the MVC pattern is to coordinate the generation of a response to the request it’s handling. That means it should perform only a limited number of actions. In particular, it should

在 MVC 模式中,页面处理程序作为控制器的角色是协调对它正在处理的请求的响应的生成。这意味着它应该只执行有限数量的作。特别是,它应该

  • Validate that the data contained in the binding model is valid for the request.
    验证绑定模型中包含的数据是否对请求有效。

  • Invoke the appropriate actions on the application model using services.
    使用 services 对应用程序模型调用适当的作。

  • Select an appropriate response to generate based on the response from the application model.
    根据应用程序模型的响应选择要生成的适当响应。

Figure 13.10 shows the page handler invoking an appropriate method on the application model. Here, you can see that the application model is a somewhat-abstract concept that encapsulates the remaining non-UI parts of your application. It contains the domain model, several services, and the database interaction.

图 13.10 显示了在应用程序模型上调用适当方法的页面处理程序。在这里,您可以看到应用程序模型是一个有点抽象的概念,它封装了应用程序的其余非 UI 部分。它包含域模型、多个服务和数据库交互。

alt text

Figure 13.10 When executed, an action invokes the appropriate methods in the application model.
图 13.10 执行时,作会调用应用程序模型中的相应方法。

Definition The domain model encapsulates complex business logic in a series of classes that don’t depend on any infrastructure and are easy to test.
定义 域模型将复杂的业务逻辑封装在一系列不依赖于任何基础设施且易于测试的类中。

The page handler typically calls into a single point in the application model. In our example of viewing a to-do list category, the application model might use a variety of services to check whether the current user is allowed to view certain items, to search for items in the given category, to load the details from the database, or to load a picture associated with an item from a file. Assuming that the request is valid, the application model returns the required details to the page handler. Then it’s up to the page handler to choose a response to generate.

页面处理程序通常调用应用程序模型中的单个点。在查看待办事项列表类别的示例中,应用程序模型可能使用各种服务来检查是否允许当前用户查看某些项目、搜索给定类别中的项目、从数据库加载详细信息或从文件中加载与项目关联的图片。假设请求有效,应用程序模型会将所需的详细信息返回给页面处理程序。然后,由页面处理程序选择要生成的响应。

13.4.3 Building HTML using the view model

13.4.3 使用视图模型构建 HTML

When the page handler has called out to the application model that contains the application business logic, it’s time to generate a response. A view model captures the details necessary for the view to generate a response.

当页面处理程序调用了包含应用程序业务逻辑的应用程序模型时,就可以生成响应了。视图模型捕获视图生成响应所需的详细信息。

Definition A view model in the MVC pattern is all the data required by the view to render a UI. It’s typically some transformation of the data contained in the application model, plus extra information required to render the page, such as the page’s title.
定义 MVC 模式中的视图模型是视图呈现 UI 所需的所有数据。它通常是对应用程序模型中包含的数据进行一些转换,以及呈现页面所需的额外信息,例如页面的标题。

The term view model is used extensively in ASP.NET Core MVC, where it typically refers to a single object that is passed to the Razor view to render. With Razor Pages, however, the Razor view can access the Razor Page’s page model class directly. Therefore, the Razor Page PageModel typically acts as the view model in Razor Pages, with the data required by the Razor view exposed via properties, as you saw in listing 13.5.

术语“视图模型”在 ASP.NET Core MVC 中广泛使用,它通常是指传递给 Razor 视图进行渲染的单个对象。但是,使用 Razor 页面时,Razor 视图可以直接访问 Razor 页面的页面模型类。因此,Razor Page PageModel 通常充当 Razor Pages 中的视图模型,其中通过 properties 公开的 Razor 视图所需的数据,如清单 13.5 所示。

Note Razor Pages use the PageModel class itself as the view model for the Razor view by exposing the required data as properties.
注意 Razor Pages 通过将所需数据作为属性公开,使用 PageModel 类本身作为 Razor 视图的视图模型。

The Razor view uses the data exposed in the page model to generate the final HTML response. Finally, this data is sent back through the middleware pipeline and out to the user’s browser, as shown in figure 13.11.

Razor 视图使用页面模型中公开的数据来生成最终的 HTML 响应。最后,这些数据通过中间件管道发送回用户的浏览器,如图 13.11 所示。

alt text

Figure 13.11 The page handler builds a view model by setting properties on the PageModel. It’s the view that generates the response.
图 13.11 页面处理程序通过在 PageModel 上设置属性来构建视图模型。这是生成响应。

It’s important to note that although the page handler selects whether to execute the view and the data to use, it doesn’t control what HTML is generated. The view itself decides what the content of the response will be.

请务必注意,尽管页面处理程序选择是否执行视图和要使用的数据,但它并不控制生成的 HTML。视图本身决定响应的内容。

13.4.4 Putting it all together: A complete Razor Page request

13.4.4 汇总:完整的 Razor Page 请求

Now that you’ve seen the steps that go into handling a request in ASP.NET Core using Razor Pages, let’s put them together from request to response. Figure 13.12 shows how the steps combine to handle the request to display the list of to-do items for the Simple category. The traditional MVC pattern is still visible in Razor Pages, made up of the page handler (controller), the view, and the application model.

现在,你已经了解了使用 Razor Pages 在 ASP.NET Core 中处理请求的步骤,让我们将它们从请求到响应放在一起。图 13.12 显示了如何组合这些步骤来处理显示 Simple 类别的待办事项列表的请求。传统的 MVC 模式在 Razor Pages 中仍然可见,它由页面处理程序(控制器)、视图和应用程序模型组成。

alt text

Figure 13.12 A complete Razor Pages request for the list of to-dos in the Simple category
图 13.12 对“简单”类别中待办事项列表的完整 Razor Pages 请求

By now, you may be thinking this whole process seems rather convoluted. So many steps to display some HTML! Why not allow the application model to create the view directly, rather than have to go on a dance back and forth with the page handler method? The key benefit throughout this process is the separation of concerns:

到现在为止,您可能会认为整个过程似乎相当复杂。显示一些 HTML 的步骤太多了!为什么不允许应用程序模型直接创建视图,而不必使用页面处理程序方法来回跳舞呢?整个过程中的主要好处是关注点分离:

  • The view is responsible only for taking some data and generating HTML.
    该视图只负责获取一些数据并生成 HTML。

  • The application model is responsible only for executing the required business logic.
    应用程序模型仅负责执行所需的业务逻辑。

  • The page handler (controller) is responsible only for validating the incoming request and selecting which response is required, based on the output of the application model.
    页面处理程序(控制器)仅负责验证传入请求并根据应用程序模型的输出选择所需的响应。

By having clearly defined boundaries, it’s easier to update and test each of the components without depending on any of the others. If your UI logic changes, you won’t necessarily have to modify any of your business logic classes, so you’re less likely to introduce errors in unexpected places.
通过明确定义的边界,可以更轻松地更新和测试每个组件,而无需依赖任何其他组件。如果您的 UI 逻辑发生变化,您不一定必须修改任何业务逻辑类,因此您不太可能在意想不到的地方引入错误。

The dangers of tight coupling
紧密耦合的危险
It’s generally a good idea to reduce coupling between logically separate parts of your application as much as possible. This makes it easier to update your application without causing adverse effects or requiring modifications in seemingly unrelated areas. Applying the MVC pattern is one way to help with this goal.
通常,最好尽可能减少应用程序逻辑上独立的部分之间的耦合。这样可以更轻松地更新您的应用程序,而不会造成不利影响或需要在看似不相关的区域中进行修改。应用 MVC 模式是帮助实现此目标的一种方法。

As an example of when coupling rears its head, I remember a case a few years ago when I was working on a small web app. In our haste, we hadn’t decoupled our business logic from our HTML generation code properly, but initially there were no obvious problems. The code worked, so we shipped it!

举个例子,我记得几年前我正在开发一个小型 Web 应用程序。由于匆忙,我们没有将业务逻辑与 HTML 生成代码正确解耦,但最初并没有明显的问题。代码有效,所以我们发布了它!

A few months later, someone new started working on the app and immediately “helped” by renaming an innocuous spelling error in a class in the business layer. Unfortunately, the names of those classes had been used to generate our HTML code, so renaming the class caused the whole website to break in users’ browsers! Suffice it to say that we made a concerted effort to apply the MVC pattern thereafter and ensure that we had a proper separation of concerns.

几个月后,有人开始开发该应用程序,并立即“帮助”重命名了业务层中一个无害的类中的一个无害的拼写错误。不幸的是,这些类的名称已被用于生成我们的 HTML 代码,因此重命名该类会导致整个网站在用户的浏览器中中断!可以说,我们在此后齐心协力地应用 MVC 模式,并确保我们适当地分离了关注点。

The examples shown in this chapter demonstrate the bulk of the Razor Pages functionality. It has additional features, such as the filter pipeline, which I cover in chapters 21 and 22, and I discuss binding models in greater depth in chapter 16, but the overall behavior of the system is the same.

本章中显示的示例演示了 Razor Pages 的大部分功能。它还有其他功能,例如过滤器管道,我在第 21 章和第 22 章中介绍了这一点,我在第 16 章中更深入地讨论了绑定模型,但系统的整体行为是相同的。

Similarly, in chapter 19 I look at MVC controllers and explain why I don’t recommend them over Razor Pages for server-rendered applications. By contrast, in chapter 20 I discuss how you can use the MVC design pattern when you’re generating machine-readable responses using Web API controllers. The process is for all intents and purposes identical to the MVC pattern you’ve already seen.

同样,在第 19 章中,我将介绍 MVC 控制器,并解释为什么对于服务器呈现的应用程序,我不建议使用 MVC 控制器而不是 Razor Pages。相比之下,在第 20 章中,我将讨论在使用 Web API 控制器生成机器可读响应时如何使用 MVC 设计模式。该过程的所有意图和目的都与您已经看到的 MVC 模式相同。

I hope that by this point, you’re sold on Razor Pages and their overall design using the MVC pattern. The page handler methods on a Razor Page are invoked in response to a request and select the type of response to generate by returning an IActionResult.

我希望到此时,您已经对使用 MVC 模式的 Razor Pages 及其整体设计感到满意。调用 Razor 页面上的页面处理程序方法以响应请求,并通过返回 IActionResult 选择要生成的响应类型。

An aspect I’ve touched on only vaguely is how the RoutingMiddleware decides which Razor Page and handler to invoke for a given request. You don’t want to have a Razor Page for every URL in an app. It would be difficult to have, for example, a different page per product in an e-shop; every product would need its own Razor Page! In chapter 14 you’ll see how to define routes for your Razor Pages, how to add constraints to your routes, and how they deconstruct URLs to match a single handler.

我只是模糊地谈到了一个方面,即 RoutingMiddleware 如何决定为给定请求调用哪个 Razor Page 和处理程序。您不希望应用程序中的每个 URL 都有一个 Razor 页面。例如,在电子商店中为每个产品设置不同的页面是很困难的;每个产品都需要自己的 Razor Page!在第 14 章中,你将了解如何为 Razor 页面定义路由,如何向路由添加约束,以及它们如何解构 URL 以匹配单个处理程序。

13.5 Summary

13.5 总结

Razor Pages are located in the Pages folder of a project and by default are named according to the URL path they handle. Privacy.cshtml, for example, handles the path /Privacy. This convention makes it easy to quickly add new pages.
Razor Pages 位于项目的 Pages 文件夹中,默认情况下根据它们处理的 URL 路径进行命名。例如,Privacy.cshtml 处理路径 /Privacy。此约定使快速添加新页面变得容易。

Razor Pages must contain the @page directive as the first line of the .cshtml file. Without this directive, ASP.NET Core won’t recognize it as a Razor Page, and it won’t appear as an endpoint in your app.
Razor Pages 必须包含 @page 指令作为 .cshtml 文件的第一行。如果没有此指令,ASP.NET Core 将无法将其识别为 Razor 页面,并且不会在应用中显示为终结点。

Page models derive from the PageModel base class and contain page handlers. Page handlers are methods named using conventions that indicate the HTTP verb they handle. OnGet, for example, handles the GET verb. Page handlers are equivalent to minimal API endpoint handlers; they run in response to a given request.
页面模型派生自 PageModel 基类,并包含页面处理程序。页面处理程序是使用约定命名的方法,这些约定指示它们处理的 HTTP 动词。例如,OnGet 处理 GET 动词。页面处理程序等同于最小 API 端点处理程序;它们运行以响应给定的请求。

Razor templates can contain standalone C#, standalone HTML, and dynamic HTML generated from C# values. By combining all three, you can build highly dynamic applications.
Razor 模板可以包含独立 C#、独立 HTML 和从 C# 值生成的动态 HTML。通过将这三者结合起来,您可以构建高度动态的应用程序。

The MVC design pattern allows for a separation of concerns between the business logic of your application, the data that’s passed around, and the display of data in a response. This reduces coupling between the different layers of your application.
MVC 设计模式允许在应用程序的业务逻辑、传递的数据和响应中的数据显示之间分离关注点。这减少了应用程序不同层之间的耦合。

Razor Pages should inherit from the PageModel base class and contain page handlers. The routing middleware selects a page handler based on the incoming request’s URL, the HTTP verb, and the request’s query string.
Razor Pages 应继承自 PageModel 基类并包含页面处理程序。路由中间件根据传入请求的 URL、HTTP 动词和请求的查询字符串选择页面处理程序。

Page handlers generally should 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.
页面处理程序通常应委托给服务来处理请求所需的业务逻辑,而不是自行执行更改。这确保了关注点的清晰分离,从而有助于测试并改进应用程序结构。

ASP.NET Core in Action 12 Saving data with Entity Framework Core

12 Saving data with Entity Framework Core
12 使用 Entity Framework Core 保存数据

This chapter covers

本章涵盖

  • Understanding what Entity Framework Core is and why you should use it
    了解什么是 Entity Framework Core 以及为什么应该使用它

  • Adding Entity Framework Core to an ASP.NET Core application
    将 Entity Framework Core 添加到 ASP.NET Core 应用程序

  • Building a data model and using it to create a database
    构建数据模型并使用它来创建数据库

  • Querying, creating, and updating data with Entity Framework Core
    使用 Entity Framework Core 查询、创建和更新数据

Most applications that you’ll build with ASP.NET Core require storing and loading some kind of data. Even the examples so far in this book have assumed that you have some sort of data store—storing exchange rates, user shopping carts, or the locations of physical stores. I’ve glossed over this topic for the most part, but typically you’ll store this data in a database.

您将使用 ASP.NET Core 构建的大多数应用程序都需要存储和加载某种类型的数据。即使是本书中到目前为止的示例也假定您有某种数据存储 — 存储汇率、用户购物车或实体商店的位置。我大部分时间都忽略了这个主题,但通常您会将此数据存储在数据库中。

Working with databases can be a rather cumbersome process. You have to manage connections to the database, translate data from your application to a format the database can understand, and handle a plethora of other subtle problems. You can manage this complexity in a variety of ways, but I’m going to focus on using a library built for modern .NET: Entity Framework Core (EF Core). EF Core is a library that lets you quickly and easily build database access code for your ASP.NET Core applications. It’s modeled on the popular Entity Framework 6.x library, but it has significant changes that make it stand alone in its own right as more than an upgrade.

使用数据库可能是一个相当繁琐的过程。您必须管理与数据库的连接,将应用程序中的数据转换为数据库可以理解的格式,并处理大量其他细微的问题。您可以通过多种方式管理这种复杂性,但我将重点介绍如何使用为现代 .NET 构建的库:Entity Framework Core (EF Core)。EF Core 是一个库,可让您快速轻松地为 ASP.NET Core 应用程序构建数据库访问代码。它以流行的 Entity Framework 6.x 库为模型,但它具有重大变化,使其本身不仅仅是升级。

The aim of this chapter is to provide a quick overview of EF Core and show how you can use it in your applications to query and save to a database quickly. You’ll learn enough to connect your app to a database and manage schema changes to the database, but I won’t be going into great depth on any topics.

本章旨在提供 EF Core 的快速概述,并演示如何在应用程序中使用它来快速查询和保存到数据库。您将学到足够的知识来将应用程序连接到数据库并管理数据库的架构更改,但我不会深入讨论任何主题。

Note For an in-depth look at EF Core, I recommend Entity Framework Core in Action, 2nd ed., by Jon P. Smith (Manning, 2021). Alternatively, you can read about EF Core on the Microsoft documentation website at https://docs.microsoft.com/ef/core.
注意 要深入了解 EF Core,我推荐 Jon P. Smith 的 Entity Framework Core in Action,第 2 版(Manning,2021 年)。或者,您可以在 https://docs.microsoft.com/ef/core Microsoft 文档网站上阅读有关 EF Core 的信息。

Section 12.1 introduces EF Core and explains why you may want to use it in your applications. You’ll learn how the design of EF Core helps you iterate quickly on your database structure and reduce the friction of interacting with a database.

第 12.1 节介绍了 EF Core,并解释了为什么你可能想要在应用程序中使用它。你将了解 EF Core 的设计如何帮助你快速迭代数据库结构并减少与数据库交互的摩擦。

In section 12.2 you’ll learn how to add EF Core to an ASP.NET Core app and configure it by using the ASP.NET Core configuration system. You’ll see how to build a model for your app that represents the data you’ll store in the database and how to hook it into the ASP.NET Core DI container.

在第 12.2 节中,你将了解如何将 EF Core 添加到 ASP.NET Core 应用,并使用 ASP.NET Core 配置系统对其进行配置。您将了解如何为应用程序构建一个模型,该模型表示您将存储在数据库中的数据,以及如何将其挂接到 ASP.NET Core DI 容器中。

Note For this chapter I use SQLite, a small, fast, cross-platform database engine, but none of the code shown in this chapter is specific to SQLite. The code sample for the book also includes a version using SQL Server Express’s LocalDB feature. This version is installed as part of Visual Studio 2022 (when you choose the ASP.NET and Web Development workload), and it provides a lightweight SQL Server engine. You can read more about LocalDB at http://mng.bz/5jEa.
注意 在本章中,我使用了 SQLite,这是一个小型、快速、跨平台的数据库引擎,但本章中展示的代码都不是特定于 SQLite 的。该书的代码示例还包括使用 SQL Server Express 的 LocalDB 功能的版本。此版本作为 Visual Studio 2022 的一部分安装(当您选择 ASP.NET 和 Web 开发工作负载时),它提供轻量级 SQL Server 引擎。您可以在 http://mng.bz/5jEa 上阅读有关 LocalDB 的更多信息。

No matter how carefully you design your original data model, the time will come when you need to change it. In section 12.3 I show how you can easily update your model and apply these changes to the database itself, using EF Core for all the heavy lifting.

无论您多么仔细地设计原始数据模型,都需要更改它。在部分12.3 我将展示如何轻松更新模型并将这些更改应用于数据库本身,从而使用 EF Core 完成所有繁重的工作。

When you have EF Core configured and a database created, section 12.4 shows how to use it in your application code. You’ll see how to create, read, update, and delete (CRUD) records, and you’ll learn about some of the patterns to use when designing your data access.

配置 EF Core 并创建数据库后,第 12.4 节介绍了如何在应用程序代码中使用它。您将了解如何创建、读取、更新和删除 (CRUD) 记录,并了解在设计数据访问时要使用的一些模式。

In section 12.5 I highlight a few of the problems you’ll want to take into consideration when using EF Core in a production app. A single chapter on EF Core can offer only a brief introduction to all the related concepts, so if you choose to use EF Core in your own applications—especially if you’re using such a data access library for the first time—I strongly recommend reading more after you have the basics from this chapter.

在第 12.5 节中,我重点介绍了在生产应用程序中使用 EF Core 时需要考虑的一些问题。EF Core 的一章只能简要介绍所有相关概念,因此,如果您选择在自己的应用程序中使用 EF Core(尤其是您是第一次使用此类数据访问库),我强烈建议您在了解本章的基础知识后再阅读更多内容。

Before we get into any code, let’s look at what EF Core is, what problems it solves, and when you may want to use it.

在开始任何代码之前,让我们看看 EF Core 是什么,它解决了什么问题,以及何时可能需要使用它。

12.1 Introducing Entity Framework Core

12.1 Entity Framework Core 简介

Database access code is ubiquitous across web applications. Whether you’re building an e-commerce app, a blog, or the Next Big Thing™, chances are that you’ll need to interact with a database.

数据库访问代码在 Web 应用程序中无处不在。无论您是构建电子商务应用程序、博客还是 Next Big Thing™,您都有可能需要与数据库进行交互。

Unfortunately, interacting with databases from app code is often a messy affair, and you can take many approaches. A task as simple as reading data from a database, for example, requires handling network connections, writing SQL statements, and handling variable result data. The .NET ecosystem has a whole array of libraries you can use for this task, ranging from the low-level ADO.NET libraries to higher-level abstractions such as EF Core.

遗憾的是,从应用程序代码与数据库交互通常是一件麻烦的事情,您可以采用多种方法。例如,像从数据库读取数据这样简单的任务需要处理网络连接、编写 SQL 语句和处理可变结果数据。.NET 生态系统具有可用于此任务的一整套库,从低级 ADO.NET 库到高级抽象(如 EF Core)。

In this section, I describe what EF Core is and the problem it’s designed to solve. I cover the motivation for using an abstraction such as EF Core and how it helps bridge the gap between your app code and your database. As part of that discussion, I present some of the tradeoffs you’ll make by using EF Core in your apps, which should help you decide whether it’s right for your purposes. Finally, we’ll take a look at an example EF Core mapping, from app code to database, to get a feel for EF Core’s main concepts.

在本节中,我将介绍什么是 EF Core 以及它旨在解决的问题。我将介绍使用 EF Core 等抽象的动机,以及它如何帮助弥合应用程序代码和数据库之间的差距。作为该讨论的一部分,我将介绍在应用程序中使用 EF Core 将做出的一些权衡,这应该有助于您确定它是否适合您的目的。最后,我们将查看从应用代码到数据库的 EF Core 映射示例,以了解 EF Core 的主要概念。

12.1.1 What is EF Core?

12.1.1 什么是 EF Core?

EF Core is a library that provides an object-oriented way to access databases. It acts as an object-relational mapper (ORM), communicating with the database for you and mapping database responses to .NET classes and objects, as shown in figure 12.1.

EF Core 是一个库,它提供了一种面向对象的方式来访问数据库。它充当对象关系映射器 (ORM),为您与数据库通信,并将数据库响应映射到 .NET 类和对象,如图 12.1 所示。

alt text

Figure 12.1 EF Core maps .NET classes and objects to database concepts such as tables and rows.
图 12.1 EF Core 将 .NET 类和对象映射到数据库概念,例如表和行。

Definition With an object-relational mapper (ORM), you can manipulate a database with object-oriented concepts such as classes and objects by mapping them to database concepts such as tables and columns.
定义 使用对象关系映射器 (ORM),您可以使用面向对象的概念来作数据库,例如类和对象,方法是将它们映射到表和列等数据库概念。

EF Core is based on but distinct from the existing Entity Framework libraries (currently up to version 6.x). It was built as part of the .NET Core push to work cross-platform, but with additional goals in mind. In particular, the EF Core team wanted to make a highly performant library that could be used with a wide range of databases.

EF Core 基于现有的实体框架库(当前最高版本 6.x),但不同于现有实体框架库。它是作为 .NET Core 跨平台推动工作的一部分构建的,但考虑了其他目标。具体而言,EF Core 团队希望制作一个可与各种数据库一起使用的高性能库。

There are many types of databases, but probably the most commonly used family is relational databases, accessed via Structured Query Language (SQL). This is the bread and butter of EF Core; it can map Microsoft SQL Server, SQLite, MySQL, Postgres, and many other relational databases. It even has a cool in-memory feature you can use when testing to create a temporary database. EF Core uses a provider model, so support for other relational databases can be plugged in later as they become available.

数据库有很多类型,但最常用的系列可能是关系数据库,通过结构化查询语言 (SQL) 访问。这是 EF Core 的生计;它可以映射 Microsoft SQL Server、SQLite、MySQL、Postgres 和许多其他关系数据库。它甚至还有一个很酷的内存功能,您可以在测试创建临时数据库时使用。EF Core 使用提供程序模型,因此可以在其他关系数据库可用时插入对它们的支持。

Note As of .NET Core 3.0, EF Core also works with nonrelational, NoSQL, or document databases like Cosmos DB too. I’m going to consider mapping only to relational databases in this book, however, as that’s the most common requirement in my experience. Historically, most data access, especially in the .NET ecosystem, has used relational databases, so it generally remains the most popular approach.
注意 从 .NET Core 3.0 开始,EF Core 也适用于非关系数据库、NoSQL 数据库或 Cosmos DB 等文档数据库。但是,在本书中,我将只考虑映射到关系数据库,因为这是我经验中最常见的要求。从历史上看,大多数数据访问(尤其是在 .NET 生态系统中)都使用关系数据库,因此它通常仍然是最流行的方法。

That discussion covers what EF Core is but doesn’t dig into why you’d want to use it. Why not access the database directly by using the traditional ADO.NET libraries? Most of the arguments for using EF Core can be applied to ORMs in general, so what are the advantages of an ORM?

该讨论涵盖了 EF Core 是什么,但并未深入探讨你为何要使用它。为什么不使用传统的 ADO.NET 库直接访问数据库呢?大部分使用 EF Core 的论点通常可以应用于 ORM,那么 ORM 有哪些优势呢?

12.1.2 Why use an object-relational mapper?

12.1.2 为什么使用对象关系映射器?

One of the biggest advantages of an ORM is the speed with which it allows you to develop an application. You can stay in the familiar territory of object-oriented .NET, often without needing to manipulate a database directly or write custom SQL.

ORM 的最大优势之一是它允许您开发应用程序的速度。您可以停留在熟悉的面向对象的 .NET 领域,通常不需要直接作数据库或编写自定义 SQL。

Suppose that you have an e-commerce site, and you want to load the details of a product from the database. Using low-level database access code, you’d have to open a connection to the database; write the necessary SQL with the correct table and column names; read the data over the connection; create a plain old CLR object (POCO) to hold the data; and set the properties on the object, converting the data to the correct format manually as you go. Sounds painful, right?

假设您有一个电子商务网站,并且您希望从数据库中加载产品的详细信息。使用低级数据库访问代码,您必须打开与数据库的连接;使用正确的表和列名称编写必要的 SQL;通过连接读取数据;创建一个普通的旧 CLR 对象 (POCO) 来保存数据;并设置对象的属性,并随时手动将数据转换为正确的格式。听起来很痛苦,对吧?

An ORM such as EF Core takes care of most of this work for you. It handles the connection to the database, generates the SQL, and maps data back to your POCO objects. All you need to provide is a LINQ query describing the data you want to retrieve.

EF Core 等 ORM 会为你处理大部分工作。它处理与数据库的连接,生成 SQL,并将数据映射回 POCO 对象。您只需提供一个 LINQ 查询,描述您要检索的数据。

ORMs serve as high-level abstractions over databases, so they can significantly reduce the amount of plumbing code you need to write to interact with a database. At the most basic level, they take care of mapping SQL statements to objects, and vice versa, but most ORMs take this process a step further and provide additional features.

ORM 充当数据库的高级抽象,因此它们可以显著减少与数据库交互所需编写的管道代码量。在最基本的层面上,它们负责将 SQL 语句映射到对象,反之亦然,但大多数 ORM 将此过程更进一步并提供了额外的功能。

ORMs like EF Core keep track of which properties have changed on any objects they retrieve from the database, which lets you load an object from the database by mapping it from a database table, modify it in .NET code, and then ask the ORM to update the associated record in the database. The ORM works out which properties have changed and issues update statements for the appropriate columns, saving you a bunch of work.

EF Core 等 ORM 会跟踪它们从数据库中检索的任何对象上的哪些属性已更改,这允许您通过从数据库表映射对象、在 .NET 代码中修改对象,然后要求 ORM 更新数据库中的关联记录来加载数据库对象。ORM 会找出哪些属性已更改,并为相应的列发出 update 语句,从而为您节省大量工作。

As is so often the case in software development, using an ORM has its drawbacks. One of the biggest advantages of ORMs is also their Achilles’ heel: they hide the database from you. Sometimes this high level of abstraction can lead to problematic database query patterns in your apps. A classic example is the N+1 problem, in which what should be a single database request turns into separate requests for every single row in a database table.

与软件开发中经常出现的情况一样,使用 ORM 也有其缺点。ORM 的最大优势之一也是它们的致命弱点:它们对您隐藏了数据库。有时,这种高级别的抽象可能会导致您的应用程序中出现有问题的数据库查询模式。一个典型的例子是 N+1 问题,在这个问题中,本应是单个数据库请求的内容变成了对数据库表中每一行的单独请求。

Another commonly cited drawback is performance. ORMs are abstractions over several concepts, so they inherently do more work than if you were to handcraft every piece of data access in your app. Most ORMs, EF Core included, trade some degree of performance for ease of development.

另一个经常被提及的缺点是性能。ORM 是多个概念的抽象,因此它们本质上比您手动创建应用程序中的每一条数据访问要多。大多数 ORM(包括 EF Core)都以某种程度的性能为代价来简化开发。

That said, if you’re aware of the pitfalls of ORMs, you can often drastically simplify the code required to interact with a database. As with anything, if the abstraction works for you, use it; otherwise, don’t. If you have only minimal database access requirements or need the best performance you can get, an ORM such as EF Core may not be the right fit.

也就是说,如果您意识到 ORM 的陷阱,您通常可以大大简化与数据库交互所需的代码。与任何事情一样,如果抽象对您有用,请使用它;否则,不要。如果您只有最小的数据库访问要求或需要您可以获得的最佳性能,则 EF Core 等 ORM 可能不适合。

An alternative is to get the best of both worlds: use an ORM for the quick development of the bulk of your application, and fall back to lower-level APIs such as ADO.NET for those few areas that prove to be bottlenecks. That way, you can get good-enough performance with EF Core, trading performance for development time, and optimize only those areas that need it.

另一种选择是两全其美:使用 ORM 快速开发大部分应用程序,并回退到较低级别的 API,例如 ADO.NET 用于那些被证明是瓶颈的少数领域。这样,您就可以使用 EF Core 获得足够好的性能,以牺牲开发时间来换取性能,并仅优化需要它的领域。

Note These days, the performance aspect is one of the weaker arguments against ORMs. EF Core uses many database tricks and crafts clean SQL queries, so unless you’re a database expert, you may find that it outperforms even your handcrafted ADO.NET queries!
注意 如今,性能方面是反对 ORM 的较弱的论点之一。EF Core 使用许多数据库技巧并制作干净的 SQL 查询,因此,除非你是数据库专家,否则你可能会发现它甚至优于你手工制作的 ADO.NET 查询!

Even if you decide to use an ORM in your app, many ORMs are available for .NET, of which EF Core is one. Whether EF Core is right for you depends on the features you need and the tradeoffs you’re willing to make to get them. Section 12.1.3 compares EF Core with Microsoft’s other offering, Entity Framework, but you could consider many other alternatives, such as Dapper and NHibernate, each of which has its own set of tradeoffs.

即使您决定在应用程序中使用 ORM,也有许多 ORM 可用于 .NET,EF Core 就是其中之一。EF Core 是否适合你取决于你需要的功能以及你愿意为获得这些功能而做出的权衡。12.1.3部分将 EF Core 与 Microsoft 的其他产品 Entity Framework 进行比较,但您可以考虑许多其他替代方案,例如 Dapper 和 NHibernate,每个替代方案都有自己的一组权衡。

12.1.3 When should you choose EF Core?

12.1.3 何时应选择 EF Core?

Microsoft designed EF Core as a reimagining of the mature Entity Framework 6.x (EF 6.x) ORM, which it released in 2008. With many years of development behind it, EF 6.x was a stable and feature-rich ORM, but it’s no longer under active development.

Microsoft 将 EF Core 设计为对成熟的 Entity Framework 6.x (EF 6.x) ORM 的重新构想,该 ORM 在2008. 经过多年的开发,EF 6.x 是一个稳定且功能丰富的 ORM,但它不再处于积极开发阶段。

EF Core, released in 2016, is a comparatively new project. The APIs of EF Core are designed to be close to those of EF 6.x—though they aren’t identical—but the core components have been completely rewritten. You should consider EF Core to be distinct from EF 6.x; upgrading directly from EF 6.x to EF Core is nontrivial.

EF Core 于 2016 年发布,是一个相对较新的项目。EF Core 的 API 设计为接近 EF 6.x 的 API(尽管它们并不相同),但核心组件已完全重写。您应该将 EF Core 与 EF 6.x 区分开来;直接从 EF 6.x 升级到 EF Core 并非易事。

Although Microsoft supports both EF Core and EF 6.x, EF 6.x isn’t recommended for new .NET applications. There’s little reason to start a new application with EF 6.x these days, but the exact tradeoffs will depend largely on your specific app. If you decide to choose EF 6.x instead of EF Core, make sure that you understand what you’re sacrificing. Also make sure that you keep an eye on the guidance and feature comparison from the EF team at http://mng.bz/GxgA.

尽管 Microsoft 同时支持 EF Core 和 EF 6.x,但不建议将 EF 6.x 用于新的 .NET 应用程序。如今,几乎没有理由使用 EF 6.x 启动新应用程序,但确切的权衡在很大程度上取决于您的特定应用程序。如果您决定选择 EF 6.x 而不是 EF Core,请确保您了解您正在牺牲什么。此外,请务必密切关注 http://mng.bz/GxgA 的 EF 团队提供的指南和功能比较。

If you decide to use an ORM for your app, EF Core is a great contender. It’s also supported out of the box by various other subsystems of ASP.NET Core. In chapter 23 you’ll see how to use EF Core with the ASP.NET Core Identity authentication system for managing users in your apps.

如果你决定为应用使用 ORM,EF Core 是一个很好的竞争者。ASP.NET Core 的各种其他子系统也支持开箱即用。在第 23 章中,你将了解如何将 EF Core 与 ASP.NET Core Identity 身份验证系统配合使用,以管理应用中的用户。

Before I get into the nitty-gritty of using EF Core in your app, I’ll describe the application we’re going to be using as the case study for this chapter. I’ll go over the application and database details and discuss how to use EF Core to communicate between the two.

在深入探讨在您的应用程序中使用 EF Core 的细节之前,我将介绍我们将用作本章案例研究的应用程序。我将介绍应用程序和数据库的详细信息,并讨论如何使用 EF Core 在两者之间进行通信。

12.1.4 Mapping a database to your application code

12.1.4 将数据库映射到应用程序代码

EF Core focuses on the communication between an application and a database, so to show it off, you need an application. This chapter uses the example of a simple cooking app API that lists recipes and lets you retrieve a recipe’s ingredients, as shown in figure 12.2. Users can list all recipes, add new ones, edit recipes, and delete old ones.

EF Core 侧重于应用程序和数据库之间的通信,因此要展示它,您需要一个应用程序。本章使用一个简单的烹饪应用程序 API 示例,该 API 列出了食谱并允许您检索食谱的成分,如图 12.2 所示。用户可以列出所有配方、添加新配方、编辑配方和删除旧配方。

alt text

Figure 12.2 The recipe app provides an API for managing recipes. You can view, update, and delete recipes, as well as create new ones.
图 12.2 配方应用程序提供用于管理配方的 API。您可以查看、更新和删除食谱,以及创建新的食谱。

This API is obviously a simple one, but it contains all the database interactions you need with its two entities: Recipe and Ingredient.

这个 API 显然很简单,但它包含了您需要的所有数据库交互,它与两个实体:Recipe 和 Ingredient。

Definition An entity is a .NET class that’s mapped by EF Core to the database. These are classes you define, typically as POCO classes, that can be saved and loaded by mapping to database tables using EF Core.
定义 实体是由 EF Core 映射到数据库的 .NET 类。这些是你定义的类,通常为 POCO 类,可以通过使用 EF Core 映射到数据库表来保存和加载这些类。

When you interact with EF Core, you’ll be using primarily POCO entities and a database context that inherits from the DbContext EF Core class. The entity classes are the object-oriented representations of the tables in your database; they represent the data you want to store in the database. You use the DbContext in your application both to configure EF Core and access the database at runtime.

与 EF Core 交互时,将主要使用 POCO 实体和从 DbContext EF Core 类继承的数据库上下文。实体类是数据库中表的面向对象的表示形式;它们表示要存储在数据库中的数据。您可以在应用程序中使用 DbContext 来配置 EF Core 并在运行时访问数据库。

Note You can potentially have multiple DbContexts in your application and even configure them to integrate with different databases.
注意 您的应用程序中可能有多个 DbContext,甚至可以将它们配置为与不同的数据库集成。

When your application first uses EF Core, EF Core creates an internal representation of the database based on the DbSet properties on your application’s DbContext and the entity classes themselves, as shown in figure 12.3.

当应用程序首次使用 EF Core 时,EF Core 会根据应用程序的 DbContext 和实体类本身的 DbSet 属性创建数据库的内部表示形式,如图 12.3 所示。

alt text

Figure 12.3 EF Core creates an internal model of your application’s data model by exploring the types in your code. It adds all the types referenced in the DbSet<> properties on your app’s DbContext and any linked types.
图 12.3 EF Core 通过浏览代码中的类型来创建应用程序数据模型的内部模型。它会添加DbSet<> 应用的 DbContext 属性和任何链接类型。

For the recipe app, EF Core builds a model of the Recipe class because it’s exposed on the AppDbContext as a DbSet. Furthermore, EF Core loops through all the properties of Recipe, looking for types it doesn’t know about, and adds them to its internal model. In the app, the Ingredients collection on Recipe exposes the Ingredient entity as an ICollection<Ingredient>, so EF Core models the entity appropriately.

对于配方应用,EF Core 会生成 Recipe 类的模型,因为它在 AppDbContext 上作为 DbSet 公开。此外,EF Core 会循环访问 Recipe 的所有属性,查找它不知道的类型,并将它们添加到其内部模型中。在应用程序中,Recipe 上的 Ingredients 集合将 Ingredient 实体公开为 ICollection<Ingredient>,因此 EF Core 会适当地对实体进行建模。

EF Core maps each entity to a table in the database, but it also maps the relationships between the entities. Each recipe can have many ingredients, but each ingredient (which has a name, quantity, and unit) belongs to one recipe, so this is a many-to-one relationship. EF Core uses that knowledge to correctly model the equivalent many-to-one database structure.

EF Core 将每个实体映射到数据库中的表,但它也会映射实体之间的关系。每个配方可以包含许多成分,但每个成分(具有名称、数量和单位)都属于一个配方,因此这是一种多对一关系。EF Core 使用该知识对等效的多对一数据库结构进行正确建模。

Note Two different recipes, such as fish pie and lemon chicken, may use an ingredient that has both the same name and quantity, such as the juice of one lemon, but they’re fundamentally two different instances. If you update the lemon chicken recipe to use two lemons, you wouldn’t want this change to automatically update the fish pie to use two lemons too!
注意 两种不同的食谱,例如鱼馅饼和柠檬鸡,可能使用名称和数量相同的成分,例如一个柠檬的汁液,但它们本质上是两个不同的实例。如果您将柠檬鸡食谱更新为使用两个柠檬,您肯定不希望此更改自动更新鱼馅饼食谱以使用两个柠檬!

EF Core uses the internal model it builds when interacting with the database to ensure that it builds the correct SQL to create, read, update, and delete entities.

EF Core 在与数据库交互时使用它生成的内部模型,以确保它生成正确的 SQL 来创建、读取、更新和删除实体。

Right—it’s about time for some code! In section 12.2, you’ll start building the recipe app. You’ll see how to add EF Core to an ASP.NET Core application, configure a database provider, and design your application’s data model.

好了 — 是时候编写一些代码了!在第 12.2 节中,您将开始构建配方应用程序。你将了解如何将 EF Core 添加到 ASP.NET Core 应用程序、配置数据库提供程序以及设计应用程序的数据模型。

12.2 Adding EF Core to an application

12.2 将 EF Core 添加到应用程序

In this section we focus on getting EF Core installed and configured in your ASP.NET Core recipe API app. You’ll learn how to install the required NuGet packages and build the data model for your application. As we’re talking about EF Core in this chapter, I’m not going to go into how to create the application in general. I created a simple minimal API app as the basis—nothing fancy.

在本部分中,我们重点介绍如何在 ASP.NET Core 配方 API 应用程序中安装和配置 EF Core。您将了解如何安装所需的 NuGet 包并为您的应用程序构建数据模型。由于我们在本章中讨论的是 EF Core,因此我不打算一般地介绍如何创建应用程序。我创建了一个简单、最小的 API 应用程序作为基础 — 没什么花哨的。

Tip The sample code for this chapter shows the state of the application at three points in this chapter: at the end of section 12.2, at the end of section 12.3, and at the end of the chapter. It also includes examples using both LocalDB and SQLite providers.
提示 本章的示例代码显示了本章中三个点的应用程序状态:第 12.2 节的末尾、第 12.3 节的末尾和本章的末尾。它还包括使用 LocalDB 和 SQLite 提供程序的示例。

Interaction with EF Core in the example app occurs in a service layer that encapsulates all the data access outside your minimal API endpoint handlers, as shown in figure 12.4. This design keeps your concerns separated and makes your services testable.

在示例应用程序中与 EF Core 的交互发生在服务层中,该服务层封装了最小 API 端点处理程序之外的所有数据访问,如图 12.4 所示。这种设计将您的关注点分开,并使您的服务可测试。

alt text

Figure 12.4 Handling a request by loading data from a database using EF Core. Interaction with EF Core is restricted to RecipeService; the endpoint doesn’t access EF Core directly.

图 12.4 通过使用 EF Core 从数据库加载数据来处理请求。与 EF Core 的交互仅限于 RecipeService;终端节点不直接访问 EF Core。

Adding EF Core to an application is a multistep process:
将 EF Core 添加到应用程序是一个多步骤过程:

  1. Choose a database provider, such as Postgres, SQLite, or MS SQL Server.
    选择数据库提供程序,例如 Postgres、SQLite 或 MS SQL Server。

  2. Install the EF Core NuGet packages.
    安装 EF Core NuGet 包。

  3. Design your app’s DbContext and entities that make up your data model.
    设计应用程序的 DbContext 和构成数据模型的实体。

  4. Register your app’s DbContext with the ASP.NET Core DI container.
    将应用的 DbContext 注册到 ASP.NET Core DI 容器。

  5. Use EF Core to generate a migration describing your data model.
    使用 EF Core 生成描述数据模型的迁移。

  6. Apply the migration to the database to update the database’s schema.
    将迁移应用于数据库以更新数据库的架构。

This process may seem a little daunting already, but I’ll walk through steps 1–4 in sections 12.2.1–12.2.3 and steps 5–6 in section 12.3, so it won’t take long. Given the space constraints of this chapter, I stick to the default conventions of EF Core in the code I show you. EF Core is far more customizable than it may initially appear to be, but I encourage you to stick to the defaults wherever possible, which will make your life easier in the long run.

这个过程可能看起来有点令人生畏,但我将介绍第 12.2.1-12.2.3 节中的第 1-4 步和第 12.3 节中的第 5-6 步,因此不会花费很长时间。鉴于本章的篇幅限制,我在向您展示的代码中坚持 EF Core 的默认约定。EF Core 的可定制性比最初看起来要高得多,但我鼓励您尽可能坚持使用默认值,从长远来看,这将使您的生活更轻松。

The first step in setting up EF Core is deciding which database you’d like to interact with. It’s likely that a client or your company’s policy will dictate this decision, but giving some thought to it is still worthwhile.

设置 EF Core 的第一步是确定要与之交互的数据库。客户或您公司的政策可能会决定这个决定,但考虑一下仍然是值得的。

12.2.1 Choosing a database provider and installing EF Core

12.2.1 选择数据库提供程序并安装 EF Core

EF Core supports a range of databases by using a provider model. The modular nature of EF Core means that you can use the same high-level API to program against different underlying databases; EF Core knows how to generate the necessary implementation-specific code and SQL statements.

EF Core 使用提供程序模型支持一系列数据库。EF Core 的模块化特性意味着您可以使用相同的高级 API 对不同的底层数据库进行编程;EF Core 知道如何生成必要的特定于实现的代码和 SQL 语句。

You’ll probably have a database in mind when you start your application, and you’ll be pleased to know that EF Core has most of the popular ones covered. Adding support for a given database involves adding the correct NuGet package to your .csproj file, such as the following:
在启动应用程序时,您可能会想到一个数据库,并且您会很高兴地知道 EF Core 涵盖了大多数常用数据库。添加对给定数据库的支持涉及将正确的 NuGet 包添加到.csproj 文件,如下所示:

  • PostgreSQL—Npgsql.EntityFrameworkCore.PostgreSQL

  • Microsoft SQL Server—Microsoft.EntityFrameworkCore.SqlServer

  • MySQL—MySql.Data.EntityFrameworkCore

  • SQLite—Microsoft.EntityFrameworkCore.SQLite

Some of the database provider packages are maintained by Microsoft, some are maintained by the open-source community, and some (such as the Oracle provider) require a paid license, so be sure to check your requirements. You can find a list of providers at https://docs.microsoft.com/ef/core/providers.

一些数据库提供程序包由 Microsoft 维护,一些由开源社区维护,还有一些(如 Oracle 提供程序)需要付费许可证,因此请务必检查您的要求。您可以在 https://docs.microsoft.com/ef/core/providers 上找到提供商列表。

You install a database provider in your application in the same way as any other library: by adding a NuGet package to your project’s .csproj file and running dotnet restore from the command line (or letting Visual Studio automatically restore for you).

在应用程序中安装数据库提供程序的方式与任何其他库相同:将 NuGet 包添加到项目的 .csproj 文件并运行 dotnet restore从命令行(或让 Visual Studio 自动为您还原)。

EF Core is inherently modular, so you’ll need to install multiple packages. I’m using the SQLite database provider, so I’ll be using the SQLite packages:

EF Core 本质上是模块化的,因此需要安装多个包。我正在使用 SQLite 数据库提供程序,因此我将使用 SQLite 包:

  • Microsoft.EntityFrameworkCore.SQLite—This package is the main database provider package for using EF Core at runtime. It also contains a reference to the main EF Core NuGet package.
    Microsoft.EntityFrameworkCore.SQLite – 此包是在运行时使用 EF Core 的主要数据库提供程序包。它还包含对主 EF Core NuGet 包的引用。

  • Microsoft.EntityFrameworkCore.Design—This package contains shared build-time components for EF Core, required for building the EF Core data model for your app.
    Microsoft.EntityFrameworkCore.Design – 此包包含 EF Core 的共享构建时组件,这些组件是为您的应用程序构建 EF Core 数据模型所必需的。

Tip You’ll also want to install tooling to help create and update your database. I show how to install these tools in section 12.3.1.
提示 您还需要安装工具来帮助创建和更新数据库。我在 Section 12.3.1 中演示了如何安装这些工具。

Listing 12.1 shows the recipe app’s .csproj file after adding the EF Core packages. Remember, you add NuGet packages as PackageReference elements.

列表 12.1 显示了添加 EF Core 包后配方应用程序的 .csproj 文件。请记住,将 NuGet 包添加为 PackageReference 元素。

Listing 12.1 Installing EF Core in an ASP.NET Core application
列表 12.1 在 ASP.NET Core 中安装 EF Core应用

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>   ❶
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference     ❷
      Include="Microsoft.EntityFrameworkCore.SQLite"    ❷
      Version="7.0.0" />    ❷
    <PackageReference     ❸
      Include="Microsoft.EntityFrameworkCore.Design"    ❸
      Version="7.0.0" >    ❸
        <IncludeAssets>runtime; build; native; contentfiles;  ❹
          Analyzers; buildtransitive</IncludeAssets>    ❹
        <PrivateAssets>all</PrivateAssets>    ❹
    </PackageReference>
  </ItemGroup>

❶ The app targets .NET 7.0.
该应用程序面向 .NET 7.0。
❷ Installs the appropriate NuGet package for your selected DB
为所选数据库安装适当的 NuGet 包
❸ Contains shared design-time components for EF Core
包含 EF Core 的共享设计时组件
❹ Added automatically by NuGet
由 NuGet 自动添加

With these packages installed and restored, you have everything you need to start building the data model for your application. In section 12.2.2 we’ll create the entity classes and the DbContext for your recipe app.

安装并还原这些包后,您就拥有了开始为应用程序构建数据模型所需的一切。在第 12.2.2 节中,我们将为您的配方应用程序创建实体类和 DbContext。

12.2.2 Building a data model

12.2.2 构建数据模型

In section 12.1.4 I showed an overview of how EF Core builds up its internal model of your database from the DbContext and entity models. Apart from this discovery mechanism, EF Core is flexible in letting you define your entities the way you want to, as POCO classes.

在第 12.1.4 节中,我概述了 EF Core 如何从 DbContext 和实体模型构建其数据库的内部模型。除了这种发现机制之外,EF Core 还可以灵活地让你以所需的方式将实体定义为 POCO 类。

Some ORMs require your entities to inherit from a specific base class or require you to decorate your models with attributes that describe how to map them. EF Core heavily favors a convention over configuration approach, as you can see in listing 12.2, which shows the Recipe and Ingredient entity classes for your app.

某些 ORM 要求实体从特定基类继承,或者要求您使用描述如何映射模型的属性来装饰模型。EF Core 非常倾向于使用约定而不是配置方法,因为你可以这样做参见 清单 12.2 中,它显示了 Recipe 和ngredient 实体类。

Tip The required keyword, used on several properties in listing 12.2, was introduced in C# 11. It’s used here to prevent warnings about uninitialized non-nullable values. You can read more about how EF Core interacts with non-nullable types in the documentation at http://mng.bz/Keoj.

提示 在清单 12.2 中的几个属性上使用的 required 关键字是在 C# 11 中引入的。它在此处用于防止有关未初始化的不可为 null 值的警告。您可以在 http://mng.bz/Keoj 文档中阅读有关 EF Core 如何与不可为 null 的类型交互的更多信息。

Listing 12.2 Defining the EF Core entity classes
列表 12.2 定义 EF Core 实体类

public class Recipe
{
    public int RecipeId { get; set; }
    public required string Name { get; set; }
    public TimeSpan TimeToCook { get; set; }
    public bool IsDeleted { get; set; }
    public required string Method { get; set; }
    public required ICollection<Ingredient> Ingredients { get; set; }  ❶
}
public class Ingredient
{
    public int IngredientId { get; set; }
    public int RecipeId { get; set; }
    public required string Name { get; set; }
    public decimal Quantity { get; set; }
    public required string Unit { get; set; }
}

❶ A Recipe can have many Ingredients, represented by ICollection.
一个配方可以有很多成分,用 ICollection 表示。

These classes conform to certain default conventions that EF Core uses to build up a picture of the database it’s mapping. The Recipe class, for example, has a RecipeId property, and the Ingredient class has an IngredientId property. EF Core identifies this pattern of an Id suffix as indicating the primary key of the table.

这些类符合 EF Core 用于构建其映射的数据库图片的某些默认约定。例如,Recipe 类具有 RecipeId 属性,而 Ingredient 类具有 IngredientId 属性。EF Core 将 Id 后缀的这种模式标识为指示表的主键。

Definition The primary key of a table is a value that uniquely identifies the row among all the others in the table. It’s often an int or a Guid.
定义 表的主键是一个值,用于在表中的所有其他行中唯一标识该行。它通常是 int 或 Guid。

Another convention visible here is the RecipeId property on the Ingredient class. EF Core interprets this property to be a foreign key pointing to the Recipe class. When considered with ICollection on the Recipe class, this property represents a many-to-one relationship, in which each recipe has many ingredients but each ingredient belongs to a single recipe (figure 12.5).

此处显示的另一个约定是 Ingredient 类的 RecipeId 属性。EF Core 将此属性解释为指向 Recipe 类的外键。当在 Recipe 类中使用 ICollection 时,此属性表示多对一关系,其中每个配方都有许多成分,但每个成分都属于一个配方(图 12.5)。

alt text

Figure 12.5 Many-to-one relationships in code are translated to foreign key relationships between tables.
图 12.5 代码中的多对一关系转换为表之间的外键关系。

Definition A foreign key on a table points to the primary key of a different table, forming a link between the two rows.
定义 表上的外键指向不同表的主键,从而在两行之间形成链接。

Many other conventions are at play here, such as the names EF Core will assume for the database tables and columns or the database column types it will use for each property, but I’m not going to discuss them here. The EF Core documentation contains details about all these conventions, as well as how to customize them for your application; see https://docs.microsoft.com/ef/core/modeling.

这里还有许多其他约定,例如 EF Core 将为数据库表和列采用的名称,或者它将用于每个属性的数据库列类型,但我不打算在这里讨论它们。EF Core 文档包含有关所有这些约定以及如何为应用程序自定义它们的详细信息;请参阅 https://docs.microsoft.com/ef/core/modeling

Tip You can also use DataAnnotations attributes to decorate your entity classes, controlling things like column naming and string length. EF Core will use these attributes to override the default conventions.
提示 您还可以使用 DataAnnotations 属性来装饰实体类,从而控制列命名和字符串长度等内容。EF Core 将使用这些属性来替代默认约定。

As well as defining the entities, you define the DbContext for your application. The DbContext is the heart of EF Core in your application, used for all your database calls. Create a custom DbContext, in this case called AppDbContext, and derive from the DbContext base class, as shown in listing 12.3. This class exposes the DbSet so that EF Core can discover and map the Recipe entity. You can expose multiple instances of DbSet<> in this way for each of the top-level entities in your application.

除了定义实体之外,您还可以为应用程序定义 DbContext。DbContext 是应用程序中 EF Core 的核心,用于所有数据库调用。创建自定义 DbContext(在本例中称为 AppDbContext),并从 DbContext 基类派生,如清单所示12.3. 此类公开 DbSet以便 EF Core 可以发现和映射 Recipe 实体。您可以通过这种方式为应用程序中的每个顶级实体公开 DbSet<> 的多个实例

Listing 12.3 Defining the application DbContext
清单 12.3 定义应用程序 DbContext

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)      ❶
        : base(options) { }                                          ❶
    public DbSet<Recipe> Recipes { get; set; }    ❷
}

❶ The constructor options object, containing details such as the connection string
构造函数选项对象,包含连接字符串等详细信息

❷ You’ll use the Recipes property to query the database.
您将使用 Recipes 属性来查询数据库。

The AppDbContext for your app is simple, containing a list of your root entities, but you can do a lot more with it in a more complex application. If you wanted to, you could customize how EF Core maps entities to the database, but for this app you’re going to use the defaults.

应用程序的 AppDbContext 很简单,包含根实体的列表,但您可以在更复杂的应用程序中使用它执行更多作。如果需要,可以自定义 EF Core 将实体映射到数据库的方式,但对于此应用程序,你将使用默认值。

Note You didn’t list Ingredient on AppDbContext, but EF Core models it correctly as it’s exposed on the Recipe. You can still access the Ingredient objects in the database, but you must navigate via the Recipe entity’s Ingredients property to do so, as you’ll see in section 12.4.
注意 您没有在 AppDbContext 上列出 Ingredient,但 EF Core 会正确建模,因为它在 Recipe 上公开。您仍然可以访问数据库中的 Ingredient 对象,但必须通过 Recipe 实体的 Ingredients 属性进行导航,如第 12.4 节所示。

For this simple example, your data model consists of these three classes: AppDbContext, Recipe, and Ingredient. The two entities are mapped to tables and their columns to properties, and you use the AppDbContext to access them.

对于这个简单的示例,您的数据模型由以下三个类组成:AppDbContext、Recipe 和 Ingredient。这两个实体映射到表,它们的列映射到属性,您可以使用 AppDbContext 访问它们。

Note This code-first approach is typical, but if you have an existing database, you can automatically generate the EF entities and DbContext instead. (You can find more information in Microsoft’s “reverse engineering” article at http://mng.bz/mgd4.)
注意 这种代码优先方法是典型的,但如果你有现有数据库,则可以自动生成 EF 实体和 DbContext。(您可以在 Microsoft 的“逆向工程”文章中找到更多信息,网址为 http://mng.bz/mgd4

The data model is complete, but you’re not quite ready to use it: your ASP.NET Core app doesn’t know how to create your AppDbContext, and your AppDbContext needs a connection string so that it can talk to the database. In section 12.2.3 we tackle both of these problems, and we finish setting up EF Core in your ASP.NET Core app.

数据模型已完成,但您还没有完全准备好使用它:您的 ASP.NET Core 应用程序不知道如何创建 AppDbContext,并且您的 AppDbContext 需要一个连接字符串,以便它可以与数据库通信。在第 12.2.3 节中,我们解决了这两个问题,并完成了 ASP.NET Core 应用中的 EF Core 设置。

12.2.3 Registering a data context

12.2.3 注册数据上下文

As with any other service in ASP.Net Core, you should register your AppDbContext with the dependency injection (DI) container. When registering your context, you also configure the database provider and set the connection string so that EF Core knows how to talk with the database.

与 ASP.Net Core 中的任何其他服务一样,您应该向依赖项注入 (DI) 容器注册 AppDbContext。注册上下文时,还要配置数据库提供程序并设置连接字符串,以便 EF Core 知道如何与数据库通信。

You register the AppDbContext with the WebApplicationBuilder in Program.cs. EF Core provides a generic AddDbContext extension method for this purpose; the method takes a configuration function for a DbContextOptionsBuilder instance. This builder can set a host of internal properties of EF Core and lets you replace all the internal services of EF Core if you want.

您可以在 Program.cs 中向 WebApplicationBuilder 注册 AppDbContext。EF Core 为此提供了通用的 AddDbContext 扩展方法;该方法采用 DbContextOptionsBuilder 实例的配置函数。此生成器可以设置 EF Core 的大量内部属性,并允许你根据需要替换 EF Core 的所有内部服务。

The configuration for your app is, again, nice and simple, as you can see in the following listing. You set the database provider with the UseSqlite extension method, made available by the Microsoft.EntityFrameworkCore.SQLite package, and pass it a connection string.

同样,您的应用程序的配置既漂亮又简单,如下面的清单所示。您可以使用 UseSqlite 扩展方法(由 Microsoft.EntityFrameworkCore.SQLite 包提供)设置数据库提供程序,并向其传递连接字符串。

Listing 12.4 Registering a DbContext with the DI container
清单 12.4 向 DI 注册 DbContext容器

using Microsoft.EntityFrameworkCore;
WebApplicationBuillder builder = WebApplication.CreateBuilder(args);
var connString = builder.Configuration                  ❶
        .GetConnectionString("DefaultConnection");      ❶

Builder.Services.AddDbContext<AppDbContext>(            ❷
        options => options.UseSqlite(connString));   ❸

WebApplication app = builder.Build();
app.Run();

❶ The connection string is taken from configuration, from the ConnectionStrings
section.
连接字符串取自配置的 ConnectionStrings 部分。
❷ Registers your app’s DbContext by using it as the generic parameter
通过将应用的 DbContext 用作泛型参数来注册该应用的 DbContext
❸ Specifies the database provider in the customization options for the
DbContext.
在 DbContext 的自定义选项中指定数据库提供程序。

Note If you’re using a different database provider, such as a provider for SQL Server, you need to call the appropriate Use method on the options object when registering your AppDbContext.
注意 如果您使用的是其他数据库提供程序(例如 SQL Server 的提供程序),则需要在注册 AppDbContext 时对 options 对象调用相应的 Use
方法。

The connection string is a typical secret, as I discussed in chapter 10, so loading it from configuration makes sense. At runtime the correct configuration string for your current environment is used, so you can use different databases when developing locally and in production.

正如我在第 10 章中讨论的那样,连接字符串是一个典型的密钥,因此从配置中加载它是有意义的。在运行时,将使用当前环境的正确配置字符串,因此在本地开发和生产时可以使用不同的数据库。

Tip You can configure your AppDbContext’s connection string in other ways, such as with the OnConfiguring method, but I recommend the method shown here for ASP.NET Core websites.
提示 您可以通过其他方式配置 AppDbContext 的连接字符串,例如使用 OnConfiguring 方法,但我建议对 ASP.NET Core 网站使用此处显示的方法。

Now you have a DbContext, named AppDbContext, registered as a scoped service with the DI container (typical for database-related services), and a data model corresponding to your database. Codewise, you’re ready to start using EF Core, but the one thing you don’t have is a database! In section 12.3 you’ll see how you can easily use the .NET CLI to ensure that your database stays up to date with your EF Core data model.

现在,您有一个名为 AppDbContext 的 DbContext,它注册为 DI 容器的作用域服务(通常用于数据库相关服务),以及与您的数据库对应的数据模型。在代码方面,你已准备好开始使用 EF Core,但你没有的一件事是数据库!在第 12.3 节中,你将了解如何轻松使用 .NET CLI 来确保数据库与 EF Core 数据模型保持同步。

12.3 Managing changes with migrations

12.3 通过迁移管理更改

In this section you’ll learn how to generate SQL statements to keep your database’s schema in sync with your application’s data model, using migrations. You’ll learn how to create an initial migration and use it to create the database. Then you’ll update your data model, create a second migration, and use it to update the database schema.

在本节中,您将学习如何使用迁移生成 SQL 语句,以使数据库的架构与应用程序的数据模型保持同步。您将了解如何创建初始迁移并使用它来创建数据库。然后,您将更新数据模型,创建第二个迁移,并使用它来更新数据库架构。

Managing schema changes for databases, such as when you need to add a new table or a new column, is notoriously difficult. Your application code is explicitly tied to a particular version of a database, and you need to make sure that the two are always in sync.

众所周知,管理数据库的架构更改(例如,当您需要添加新表或新列时)非常困难。您的应用程序代码明确绑定到数据库的特定版本,您需要确保两者始终同步。

Definition Schema refers to how the data is organized in a database, including the tables, columns, and relationships among them.
定义 架构是指数据在数据库中的组织方式,包括表、列以及它们之间的关系。

When you deploy an app, normally you can delete the old code/executable and replace it with the new code. Job done. If you need to roll back a change, delete that new code, and deploy an old version of the app.

部署应用程序时,通常可以删除旧代码/可执行文件并将其替换为新代码。工作完成。如果您需要回滚更改,请删除该新代码,然后部署旧版本的应用程序。

The difficulty with databases is that they contain data, so blowing it away and creating a new database with every deployment isn’t possible. A common best practice is to version a database’s schema explicitly along with your application’s code. You can do this in many ways, but typically you need to store the SQL script that takes the database from the previous schema to the new schema. Then you can use a library such as DbUp (https://github.com/DbUp/DbUp) or FluentMigrator (https://github.com/fluentmigrator/fluentmigrator) to keep track of which scripts have been applied and ensure that your database schema is up to date. Alternatively, you can use external tools to manage this task.

数据库的难点在于它们包含数据,因此不可能在每次部署时都将其吹走并创建一个新数据库。一种常见的最佳实践是将数据库的架构与应用程序代码一起显式进行版本控制。您可以通过多种方式执行此作,但通常需要存储将数据库从以前的架构转移到新架构的 SQL 脚本。然后,您可以使用 DbUp (https://github.com/DbUp/DbUp) 或 FluentMigrator (https://github.com/fluentmigrator/fluentmigrator) 等库来跟踪已应用的脚本,并确保您的数据库架构是最新的。或者,您也可以使用外部工具来管理此任务。

EF Core provides its own version of schema management called migrations. Migrations provide a way to manage changes to a database schema when your EF Core data model changes.

EF Core 提供自己的架构管理版本,称为迁移。 迁移提供了一种在 EF Core 数据模型更改时管理数据库架构更改的方法。

Definition A migration is a C# code file in your application that defines how the data model changed—which columns were added, new entities, and so on. Migrations provide a record over time of how your database schema evolved as part of your application, so the schema is always in sync with your app’s data model.
定义 迁移是应用程序中的 C# 代码文件,用于定义数据模型的更改方式 - 添加了哪些列、新实体等。迁移会记录数据库 Schema 作为应用程序的一部分如何随时间演变,因此 Schema 始终与应用的数据模型保持同步。

You can use command-line tools to create a new database from the migrations or to update an existing database by applying new migrations to it. You can even roll back a migration, which updates a database to a previous schema.

您可以使用命令行工具从迁移中创建新数据库,或者通过向现有数据库应用新的迁移来更新现有数据库。您甚至可以回滚迁移,这会将数据库更新到以前的架构。

Warning Applying migrations modifies the database, so you must always be aware of data loss. If you remove a table from the database using a migration and then roll back the migration, the table will be re-created, but the data it previously contained will be gone forever!
警告 应用迁移会修改数据库,因此您必须始终注意数据丢失。如果您使用迁移从数据库中删除表,然后回滚迁移,则会重新创建该表,但之前包含的数据将永远消失!

In this section, you’ll see how to create your first migration and use it to create a database. Then you’ll update your data model, create a second migration, and use it to update the database schema.

在本节中,您将了解如何创建您的第一个迁移并使用它来创建数据库。然后,您将更新数据模型,创建第二个迁移,并使用它来更新数据库架构。

12.3.1 Creating your first migration

12.3.1 创建您的第一个迁移

Before you can create migrations, you need to install the necessary tooling. You have two primary ways to do this:
在创建迁移之前,您需要安装必要的工具。有两种主要方法可以执行此作:

  • Package manager console—You can use PowerShell cmdlets inside Visual Studio’s Package Manager Console (PMC). You can install them directly from the PMC or by adding the Microsoft.EntityFrameworkCore.Tools package to your project.
    包管理器控制台 — 您可以在 Visual Studio 的包管理器控制台 (PMC) 中使用 PowerShell cmdlet。您可以直接从 PMC 安装它们,也可以通过将 Microsoft.EntityFrameworkCore.Tools 包添加到您的项目来安装它们。

  • .NET tool—You can use cross-platform, command-line tooling that extends the .NET software development kit (SDK). You can install the EF Core .NET tool globally for your machine by running dotnet tool install --global dotnet-ef.
    .NET 工具 — 您可以使用扩展 .NET SDK 的跨平台命令行工具。可以通过运行 dotnet tool install -- global dotnet-ef 为计算机全局安装 EF Core .NET 工具。

In this book I use the cross-platform .NET tools, but if you’re familiar with EF 6.x or prefer to use the Visual Studio PMC, there are equivalent commands for the steps you’re going to take (http://mng.bz/9DK7). You can check that the .NET tool installed correctly by running dotnet ef, which should produce a help screen like the one shown in figure 12.6.

在本书中,我使用了跨平台的 .NET 工具,但如果您熟悉 EF 6.x 或更喜欢使用 Visual Studio PMC,则对于您将要执行的步骤 (http://mng.bz/9DK7),有等效的命令。您可以通过运行 dotnet ef 来检查 .NET 工具是否正确安装,这应该会产生如图 12.6 所示的帮助屏幕。

alt text

Figure 12.6 Running the dotnet ef command to check that the .NET EF Core tools are installed correctly
图 12.6 运行 dotnet ef 命令以检查 .NET EF Core 工具是否已正确安装

Tip If you get the No executable found matching command ‘dotnet-ef’ message when running the preceding command, make sure that you installed the global tool by using dotnet tool install --global dotnet-ef. In general, you need to run the dotnet ef tools from the project folder in which you registered your AppDbContext—not from the solution-folder level.
提示 如果在运行上述命令时收到 No executable found matching command 'dotnet-ef' 消息,请确保使用 dotnet tool install --global 安装了全局工具dotnet-ef 的通常,您需要从注册 AppDbContext 的项目文件夹运行 dotnet ef 工具,而不是从解决方案文件夹级别运行。

With the tools installed and your database context configured, you can create your first migration by running the following command from inside your web project folder and providing a name for the migration (in this case, InitialSchema):
安装工具并配置数据库上下文后,您可以通过从 Web 项目文件夹内运行以下命令并提供迁移名称(在本例中为 InitialSchema)来创建第一个迁移:

dotnet ef migrations add InitialSchema

This command creates three files in the Migrations folder in your project:
此命令在项目的 Migrations 文件夹中创建三个文件:

  • Migration file—This file, with the Timestamp_MigrationName.cs format, describes the actions to take on the database, such as creating a table or adding a column. Note that the commands generated here are database-provider-specific, based on the database provider configured in your project.
    迁移文件 - 此文件采用 Timestamp_MigrationName.cs 格式,描述要对数据库执行的作,例如创建表或添加列。请注意,此处生成的命令是特定于数据库提供程序的,具体取决于项目中配置的数据库提供程序。

  • Migration designer.cs file—This file describes EF Core’s internal model of your data model at the point in time when the migration was generated.
    迁移designer.cs文件 – 此文件描述在生成迁移时数据模型的 EF Core 内部模型。

  • AppDbContextModelSnapshot.cs—This file describes EF Core’s current internal model. This file is updated when you add another migration, so it should always be the same as the current (latest) migration. EF Core can use AppDbContextModelSnapshot.cs to determine a database’s previous state when creating a new migration without interacting with the database directly.
    AppDbContextModelSnapshot.cs – 此文件描述 EF Core 的当前内部模型。当您添加另一个迁移时,此文件会更新,因此它应始终与当前(最新)迁移相同。EF Core 可以在创建新迁移时使用 AppDbContextModelSnapshot.cs 来确定数据库的先前状态,而无需直接与数据库交互。

These three files encapsulate the migration process, but adding a migration doesn’t update anything in the database itself. For that task, you must run a different command to apply the migration to the database.

这三个文件封装了迁移过程,但添加迁移不会更新数据库本身的任何内容。对于该任务,您必须运行其他命令才能将迁移应用于数据库。

Tip You can, and should, look inside the migration file EF Core generates to check what it will do to your database before running the following commands. Better safe than sorry!
提示 在运行以下命令之前,您可以而且应该查看 EF Core 生成的迁移文件,以检查它将对数据库执行什么作。安全总比后悔好!

You can apply migrations in any of four ways:
您可以通过以下四种方式中的任何一种应用迁移:

  • Using the .NET tool
    使用 .NET 工具
  • Using the Visual Studio PowerShell cmdlets
    使用 Visual Studio PowerShell cmdlets
  • In code, by obtaining an instance of your AppDbContext from the DI container and calling context.Database.Migrate()
    在代码中,通过获取AppDbContext 并从 DI 容器中调用上下文。Database.Migrate() 数据库
  • By generating a migration bundle application (see http://mng.bz/jPyr)
    通过生成迁移捆绑包应用程序(请参阅 http://mng.bz/jPyr

Which method is best for you depends on how you designed your application, how you’ll update your production database, and what your personal preference is. I’ll use the .NET tool for now, but I discuss some of these considerations in section 12.5. You can apply migrations to a database by running
哪种方法最适合您取决于您如何设计应用程序、如何更新生产数据库以及您的个人偏好。我将使用.NET 工具,但我会在第 12.5 节中讨论其中的一些注意事项。您可以通过运行

dotnet ef database update

from the project folder of your application. I won’t go into the details on how this command works, but it performs four steps:
从应用程序的 project 文件夹中。我不会详细介绍此命令的工作原理,但它执行四个步骤:

  1. Builds your application
    构建您的应用程序
  2. Loads the services configured in your app’s Program.cs, including AppDbContext
    加载在应用程序的 Program.cs 中配置的服务,包括 AppDbContext
  3. Checks whether the database in the AppDbContext connection string exists and if not, creates it
    检查 AppDbContext 连接字符串中的数据库是否存在,如果不存在,则创建该数据库
  4. Updates the database by applying any unapplied migrations
    通过应用任何未应用的迁移来更新数据库

If everything is configured correctly, as in section 12.2, running this command sets you up with a shiny new database like the one shown in figure 12.7.
如果一切都配置正确,如 Section 12.2 所示,运行此命令将为您设置一个闪亮的新数据库,如图 12.7 所示。

alt text

Figure 12.7 Applying migrations to a database creates the database if it doesn’t exist and updates the database to match EF Core’s internal data model. The list of applied migrations is stored in the EFMigrationsHistory table.
图 12.7 将迁移应用于数据库会创建数据库(如果数据库不存在),并更新数据库以匹配 EF Core 的内部数据模型。应用的迁移列表存储在
EFMigrationsHistory 表。

Note If you get an error message saying No project was found when running these commands, check that you’re running them in your application’s project folder, not the top-level solution folder.
注意 如果您在运行这些命令时收到一条错误消息,指出 No project was found,请检查您是否在应用程序的项目文件夹中运行它们,而不是在顶级解决方案文件夹中运行它们。

When you apply the migrations to the database, EF Core creates the necessary tables in the database and adds the appropriate columns and keys. You may have also noticed the __EFMigrationsHistory table, which EF Core uses to store the names of migrations that it’s applied to the database. Next time you run dotnet ef database update, EF Core can compare this table with the list of migrations in your app and apply only the new ones to your database.

将迁移应用于数据库时,EF Core 会在数据库中创建必要的表,并添加相应的列和键。你可能还注意到了 EFMigrationsHistory 表,EF Core 使用该表来存储它应用于数据库的迁移的名称。下次运行 dotnet ef database update 时,EF Core 可以将此表与应用中的迁移列表进行比较,并仅将新的迁移应用于数据库。

In section 12.3.2 we’ll look at how migrations make it easy to change your data model and update the database schema without having to re-create the database from scratch.

在 Section 12.3.2 中,我们将了解迁移如何使更改数据模型和更新数据库模式变得容易,而无需从头开始重新创建数据库。

12.3.2 Adding a second migration

12.3.2 添加第二个迁移

Most applications inevitably evolve due to increased scope or simple maintenance. Adding properties to your entities, adding new entities , and removing obsolete classes are all likely.

由于范围扩大或维护简单,大多数应用不可避免地会不断发展。向实体添加属性、添加新实体和删除过时的类都是可能的。

EF Core migrations make this evolution simple. Suppose that you decide to highlight vegetarian and vegan dishes in your recipe app by exposing IsVegetarian and IsVegan properties on the Recipe entity (listing 12.5). Change your entities to your desired state, generate a migration, and apply it to the database, as shown in figure 12.8.

EF Core 迁移使这种演变变得简单。假设您决定通过在 Recipe 实体(清单 12.5)上公开 IsVegetarian 和 IsVegan 属性,在食谱应用程序中突出显示素食和纯素食菜肴。将实体更改为所需状态,生成迁移并将其应用于数据库,如图 12.8 所示。

alt text

Figure 12.8 Creating a second migration and applying it to the database using the command-line tools.
图 12.8 使用命令行工具创建第二个迁移并将其应用于数据库

Listing 12.5 Adding properties to the Recipe entity
清单 12.5 向 Recipe 实体添加属性

public class Recipe
{
    public int RecipeId { get; set; }
    public required string Name { get; set; }
    public TimeSpan TimeToCook { get; set; }
    public bool IsDeleted { get; set; }
    public required string Method { get; set; }
    public bool IsVegetarian { get; set; }
    public bool IsVegan { get; set; }
    public required ICollection<Ingredient> Ingredients { get; set; }
}

As shown in figure 12.8, after changing your entities, you need to update EF Core’s internal representation of your data model. You perform this update exactly the same way that you did for the first migration, by calling dotnet ef migrations add and providing a name for the migration:
如图 12.8 所示,更改实体后,您需要更新 EF Core 的数据模型的内部表示形式。执行此更新的方式与第一次迁移完全相同,方法是调用 dotnet ef migrations add 并提供迁移的名称:

dotnet ef migrations add ExtraRecipeFields

This command creates a second migration in your project by adding the migration file and its .designer.cs snapshot file; it also updates AppDbContextModelSnapshot.cs (figure 12.9).
此命令通过添加迁移文件及其 .designer.cs 快照文件,在项目中创建第二个迁移;它还会更新 AppDbContextModelSnapshot.cs (图 12.9)。

alt text

Figure 12.9 Adding a second migration adds a new migration file and a migration Designer.cs file. It also updates AppDbContextModelSnapshot to match the new migration’s Designer.cs file.
图 12.9 添加第二个迁移会添加新的迁移文件和迁移Designer.cs文件。它还更新 AppDbContextModelSnapshot 以匹配新迁移的 Designer.cs 文件。

As before, this command creates the migration’s files but doesn’t modify the database. You can apply the migration and update the database by running

与以前一样,此命令会创建迁移的文件,但不会修改数据库。您可以通过运行

dotnet ef database update

This command compares the migrations in your application with the __EFMigrationsHistory table in your database to see which migrations are outstanding; then it runs them. EF Core runs the 20220825201452_ExtraRecipeFields migration, adding the IsVegetarian and IsVegan fields to the database, as shown in figure 12.10.

此命令将应用程序中的迁移与数据库中的 EFMigrationsHistory 表进行比较,以查看哪些迁移未完成;然后它运行它们。EF Core 运行 20220825201452_ExtraRecipeFields 迁移,将 IsVegetarian 和 IsVegan 字段添加到数据库中,如图 12.10 所示。

alt text

Figure 12.10 Applying the ExtraRecipeFields migration to the database adds the IsVegetarian and IsVegan fields to the Recipes table.
图 12.10 将 ExtraRecipeFields 迁移应用于数据库,将 IsVegetarian 和 IsVegan 字段添加到 Recipes 表中。

Using migrations is a great way to ensure that your database is versioned along with your app code in source control. You can easily check out your app’s source code for a historical point in time and re-create the database schema your application used at that point.

使用迁移是确保数据库与源代码管理中的应用程序代码一起进行版本控制的好方法。您可以轻松查看应用程序的历史时间点源代码,并重新创建应用程序在该时间点使用的数据库架构。

Migrations are easy to use when you’re working alone or deploying to a single web server, but even in these cases, you have important things to consider when deciding how to manage your databases. For apps with multiple web servers using a shared database or for containerized applications, you have even more things to think about.

当您单独工作或部署到单个 Web 服务器时,迁移很容易使用,但即使在这些情况下,在决定如何管理数据库时,您也需要考虑重要事项。对于具有多个 Web 服务器、使用共享数据库的应用程序或容器化应用程序,您需要考虑的事项更多。

This book is about ASP.NET Core, not EF Core, so I don’t want to dwell on database management much. But section 12.5 points out some of the things you need to bear in mind when using migrations in production.

这本书是关于 ASP.NET Core 的,而不是 EF Core,因此我不想过多地讨论数据库管理。但是 Section 12.5 指出了在 生产环境 中使用 migrations 时需要记住的一些事项。

In section 12.4 we’ll get back to the meaty stuff: defining our business logic and performing CRUD operations on the database.

在 Section 12.4 中,我们将回到内容丰富的东西:定义我们的业务逻辑并在数据库上执行 CRUD作。

12.4 Querying data from and saving data to the database

12.4 从数据库查询数据并将数据保存到数据库

Let’s review where you are in creating the recipe application:
让我们回顾一下您创建配方应用程序的位置:

  • You created a simple data model consisting of recipes and ingredients.
    您创建了一个由配方和成分组成的简单数据模型。

  • You generated migrations for the data model to update EF Core’s internal model of your entities.
    您为数据模型生成了迁移,以更新 EF Core 的实体内部模型。

  • You applied the migrations to the database so that its schema matches EF Core’s model.
    您已将迁移应用于数据库,使其架构与 EF Core 的模型匹配。

In this section you’ll build the business logic for your application by creating a RecipeService. This service handles querying the database for recipes, creating new recipes, and modifying existing ones. As this app has a simple domain, I’ll be using RecipeService to handle all the requirements, but in your own apps you may have multiple services that cooperate to provide the business logic.

本节中,您将通过创建 RecipeService 来构建应用程序的业务逻辑。此服务处理在数据库中查询配方、创建新配方和修改现有配方。由于此应用程序具有一个简单的域,因此我将使用 RecipeService 来处理所有需求,但在您自己的应用程序中,您可能有多个服务相互协作以提供业务逻辑。

Note For simple apps, you may be tempted to move this logic into your endpoint handlers or Razor Pages. This approach may be fine for tiny apps, but I encourage you to resist the urge generally; extracting your business logic to other services decouples the HTTP-centric nature of your handlers from the underlying business logic, whichoften makes your business logic easier to test and more reusable.
注意 对于简单的应用程序,您可能会想将此逻辑移动到端点处理程序或 Razor Pages 中。这种方法可能适用于小型应用程序,但我鼓励您通常抵制这种冲动;将业务逻辑提取到其他服务可以将处理程序以 HTTP 为中心的性质与底层业务逻辑分离,这通常使业务逻辑更易于测试和更可重用。

Our database doesn’t have any data in it yet, so we’d better start by creating a recipe.

我们的数据库中还没有任何数据,因此我们最好先创建一个配方。

12.4.1 Creating a record

12.4.1 创建记录

In this section you’re going to build functionality to let users create a recipe by using the API. Clients send all the details of the recipe in the body of a POST request to an endpoint in your app. The endpoint uses model binding and validation attributes to confirm that the request is valid, as you learned in chapter 7.

在本节中,您将构建功能,让用户使用 API 创建配方。客户端将 POST 请求正文中配方的所有详细信息发送到 应用程序中的终端节点。终端节点使用模型绑定和验证属性来确认请求有效,如您在第 7 章中学到的那样。

If the request is valid, the endpoint handler calls RecipeService to create the new Recipe object in the database. As EF Core is the topic of this chapter, I’m going to focus on this service alone, but you can always check out the source code for this book if you want to see how everything fits together in a minimal API application.

如果请求有效,则端点处理程序会调用RecipeService 在数据库。由于 EF Core 是本章的主题,因此我将单独关注此服务,但如果您想了解所有内容如何组合到最小 API 应用程序中,您始终可以查看本书的源代码。

The business logic for creating a recipe in this application is simple: there is no logic! Copy the properties from the command binding model provided in the endpoint handler to a Recipe entity and its Ingredients, add the Recipe object to AppDbContext, and save it in the database, as shown in figure 12.11.

在此应用程序中创建配方的业务逻辑很简单:没有逻辑!将端点处理程序中提供的命令绑定模型中的属性复制到 Recipe 实体及其 Ingredients,将 Recipe 对象添加到 AppDbContext,并将其保存在数据库中,如图 12.11 所示。

alt text

Figure 12.11 Calling the POST endpoint and creating a new entity. A Recipe is created from the CreateRecipeCommand model and is added to the DbContext. EF Core generates the SQL to add a new row to the Recipes table in the database.

图 12.11 调用 POST 端点并创建新实体。配方是从 CreateRecipeCommand 模型创建的,并添加到 DbContext 中。EF Core 生成 SQL 以向数据库的 Recipes 表添加新行。

Warning Many simple, equivalent sample applications using EF or EF Core allow you to bind directly to the Recipe entity as the model in your endpoint. Unfortunately, this approach exposes a security vulnerability known as overposting, which is bad practice. If you want to avoid the boilerplate mapping code in your applications, consider using a library such as AutoMapper (http://automapper.org). For more details on overposting, see my blog post on the subject at http://mng.bz/d48O.
警告 许多使用 EF 或 EF Core 的简单等效示例应用程序允许您直接绑定到 Recipe 实体作为终端节点中的模型。不幸的是,这个方法会暴露一个称为 overpost 的安全漏洞,这是一种不好的做法。如果要避免在应用程序中使用样板映射代码,请考虑使用 AutoMapper (http://automapper.org) 等库。有关过度发布的更多详细信息,请参阅我在 http://mng.bz/d48O 上关于该主题的博客文章。

Creating an entity in EF Core involves adding a new row to the mapped table. For your application, whenever you create a new Recipe, you also add the linked Ingredient entities. EF Core takes care of linking all these entities correctly by creating the correct RecipeId for each Ingredient in the database.

在 EF Core 中创建实体涉及向映射表添加新行。对于您的应用程序,每当您创建新配方时,您都会添加链接的 Ingredient 实体。EF Core 通过为数据库中的每个成分创建正确的 RecipeId 来正确链接所有这些实体。

All interactions with EF Core and the database start with an instance of AppDbContext, which typically is DI-injected via the constructor. Creating a new entity requires three steps:

与 EF Core 和数据库的所有交互都从 AppDbContext 实例开始,该实例通常通过构造函数进行 DI 注入。创建新实体需要三个步骤:

  1. Create the and Ingredient entities.
    创建 Recipe 和 Ingredient 实体。

  2. Add the entities to EF Core’s list of tracked entities using _context.Add(entity).
    使用 _context 将实体添加到 EF Core 的跟踪实体列表中。

  3. Execute the SQL INSERT statements against the database, adding the necessary rows to the Recipe and Ingredient tables, by calling _context.SaveChangesAsync().
    对数据库执行 SQL INSERT 语句,通过调用SaveChangesAsync() 上下文。

Tip There are sync and async versions of most of the EF Core commands that involve interacting with the database, such as SaveChanges() and SaveChangesAsync(). In general, the async versions will allow your app to handle more concurrent connections, so I tend to favor them whenever I can use them.
提示 大多数涉及与数据库交互的 EF Core 命令都有同步和异步版本,例如 SaveChanges() 和 SaveChangesAsync()。通常,异步版本将允许您的应用程序处理更多的并发连接,因此只要可以使用它们,我就会倾向于使用它们。

Listing 12.6 shows these three steps in practice. The bulk of the code in this example involves copying properties from CreateRecipeCommand to the Recipe entity. The interaction with the AppDbContext consists of only two methods: Add() and SaveChangesAsync().

清单 12.6 展示了这三个步骤的实际应用。此示例中的大部分代码涉及将属性从 CreateRecipeCommand 复制到 Recipe 实体。与 AppDbContext 的交互仅包含两个方法:Add() 和 SaveChangesAsync()。

Listing 12.6 Creating a Recipe entity in the database in RecipeService
清单 12.6 在数据库中创建一个 Recipe 实体

readonly AppDbContext _context;    ❶
public async Task<int> CreateRecipe(CreateRecipeCommand cmd)   ❷
{
    var recipe = new Recipe                             ❸
    {                                                   ❸
        Name = cmd.Name,                                ❸
        TimeToCook = new TimeSpan(                      ❸
            cmd.TimeToCookHrs, cmd.TimeToCookMins, 0),  ❸
        Method = cmd.Method,                            ❸
        IsVegetarian = cmd.IsVegetarian,                ❸
        IsVegan = cmd.IsVegan,                          ❸
        Ingredients = cmd.Ingredients.Select(i =>      ❸
        new Ingredient              ❹
        {                           ❹
            Name = i.Name,          ❹
            Quantity = i.Quantity,  ❹
            Unit = i.Unit,          ❹
        }).ToList()                 ❹
    };
    _context.Add(recipe);       ❺
    await _context.SaveChangesAsync();   ❻
    return recipe.RecipeId;    ❼
}

❶ An instance of the AppDbContext is injected in the class constructor using DI.
使用 DI 将 AppDbContext 的实例注入类构造函数中。
❷ CreateRecipeCommand is passed in from the endpoint handler.
CreateRecipeCommand 从端点处理程序传入。
❸ Creates a Recipe by mapping from the command object to the Recipe entity
通过从命令对象映射到 Recipe 实体来创建 Recipe
❹ Maps each CreateIngredientCommand onto an Ingredient entity
将每个 CreateIngredientCommand 映射到一个 Ingredient 实体
❺ Tells EF Core to track the new entities
告知 EF Core 跟踪新实体
❻ Tells EF Core to write the entities to the database; uses the async version of the command
告知 EF Core 将实体写入数据库;使用命令的异步版本
❼ EF Core populates the RecipeId field on your new Recipe when it’s saved.
保存新配方时,EF Core 会填充新配方上的 RecipeId 字段。

If a problem occurs when EF Core tries to interact with your database—you haven’t run the migrations to update the database schema, for example—this code throws an exception. I haven’t shown it here, but it’s important to handle these exceptions in your application so you don’t present an ugly error message to user when things go wrong.

如果在 EF Core 尝试与数据库交互时出现问题(例如,您尚未运行迁移来更新数据库架构),则此代码将引发异常。我没有在这里展示它,但在您的应用程序中处理这些异常很重要,这样您就不会在出现问题时向用户显示难看的错误消息。

Assuming that all goes well, EF Core updates all the autogenerated IDs of your entities (RecipeId on Recipe, and both RecipeId and IngredientId on Ingredient). Return the recipe ID to the endpoint handler so the handler can use it—to return the ID in the API response, for example.

假设一切顺利,EF Core 会更新实体的所有自动生成的 ID(Recipe 上的 RecipeId,以及 Ingredient 上的 RecipeId 和 IngredientId)。将配方 ID 返回给终端节点处理程序,以便处理程序可以使用它,例如,在 API 响应中返回 ID。

Tip The DbContext type is an implementation of both the unit-of-work and repository patterns, so you generally don’t need to implement these patterns manually in your apps. You can read more about these patterns at https://martinfowler.com/eaaCatalog.
提示 DbContext 类型是工作单元模式和存储库模式的实现,因此您通常不需要在应用程序中手动实现这些模式。您可以在 https://martinfowler.com/eaaCatalog 上阅读有关这些模式的更多信息。

And there you have it. You’ve created your first entity with EF Core. In section 12.4.2 we’ll look at loading these entities from the database so you can fetch them all in a list.

你有它。你已使用 EF Core 创建了第一个实体。在 Section 12.4.2 中,我们将介绍如何从数据库中加载这些实体,以便您可以在列表中获取它们。

12.4.2 Loading a list of records

12.4.2 加载记录列表

Now that you can create recipes, you need to write the code to view them. Luckily, loading data is simple in EF Core, relying heavily on LINQ methods to control the fields you need. For your app, you’ll create a method on RecipeService that returns a summary view of a recipe, consisting of RecipeId, Name, and TimeToCook as a RecipeSummaryViewModel, as shown in figure 12.12.

现在,您可以创建配方,您需要编写代码来查看它们。幸运的是,在 EF Core 中加载数据很简单,在很大程度上依赖于 LINQ 方法来控制所需的字段。对于您的应用程序,您将在 RecipeService 上创建一个方法,该方法返回配方的摘要视图,其中包含 RecipeId、Name 和 TimeToCook 作为 RecipeSummaryViewModel,如图 12.12 所示。

alt text

Figure 12.12 Calling the GET list endpoint and querying the database to retrieve a list of RecipeSummaryViewModels. EF Core generates the SQL to retrieve the necessary fields from the database and maps them to view model objects.
图 12.12 调用 GET 列表终端节点并查询数据库以检索 RecipeSummaryViewModels 列表。EF Core 生成 SQL 以从数据库中检索必要的字段,并将它们映射到视图模型对象。

Note Creating a view model is technically a UI concern rather than a business-logic concern. I’m returning a view model directly from RecipeService here mostly to hammer home the fact that you shouldn’t be using EF Core entities directly in your endpoint’s public API. Alternatively, you might return the Recipe entity directly from the RecipeService and then build and return the RecipeSummaryViewModel inside your endpoint handler code.
注意 从技术上讲,创建视图模型是一个 UI 问题,而不是业务逻辑问题。我在这里直接从 RecipeService 返回一个视图模型,主要是为了强调您不应该直接在终端节点的公共 API 中使用 EF Core 实体的事实。或者,您可以直接从 RecipeService 返回 Recipe 实体,然后在终端节点处理程序代码中构建并返回 RecipeSummaryViewModel。

The GetRecipes method in RecipeService is conceptually simple and follows a common pattern for querying an EF Core database, as shown in figure 12.13. EF Core uses a fluent chain of LINQ commands to define the query to return on the database. The DbSet property on AppDataContext is an IQueryable, so you can use all the usual Select() and Where() clauses that you would with other IQueryable providers. EF Core converts these LINQ methods into a SQL statement to query the database when you call an execute function such as ToListAsync(), ToArrayAsync(), or SingleAsync(), or their non-async brethren.

RecipeService 中的 GetRecipes 方法在概念上很简单,并遵循查询 EF Core 数据库的常见模式,如图 12.13 所示。EF Core 使用 Fluent LINQ 命令链来定义要在数据库上返回的查询。AppDataContext 上的 DbSet 属性是 IQueryable,因此您可以使用其他 IQueryable 提供程序的所有常用 Select() 和 Where() 子句。EF Core 将这些 LINQ 方法转换为 SQL 语句,以便在调用执行函数(如 ToListAsync()、ToArrayAsync() 或 SingleAsync())或其非异步兄弟时查询数据库。

You can also use the Select() extension method to map to objects other than your entities as part of the SQL query. You can use this technique to query the database efficiently by fetching only the columns you need.

您还可以使用 Select() 扩展方法映射到实体以外的对象,作为 SQL 查询的一部分。您可以使用此技术通过仅获取所需的列来高效地查询数据库。

alt text

Figure 12.13 The three parts of an EF Core database query
图 12.13 EF Core 数据库查询的三个部分

Listing 12.7 shows the code to fetch a list of RecipeSummaryViewModels, following the same basic pattern as figure 12.12. It uses a Where LINQ expression to filter out recipes marked as deleted and a Select clause to map to the view models. The ToListAsync() command instructs EF Core to generate the SQL query, execute it on the database, and build RecipeSummaryViewModels from the data returned.

清单 12.7 显示了获取 RecipeSummaryViewModel列表的代码,遵循与图 12.12 相同的基本模式。它使用 Where LINQ 表达式筛选出标记为已删除的配方,并使用 Select 子句映射到视图模型。ToListAsync() 命令指示 EF Core 生成 SQL 查询,在数据库上执行该查询,并根据返回的数据生成 RecipeSummaryViewModels。

Listing 12.7 Loading a list of items using EF Core in RecipeService
清单 12.7 在配方服务

public async Task<ICollection<RecipeSummaryViewModel>> GetRecipes()
{
    return await _context.Recipes    ❶
        .Where(r => !r.IsDeleted)
        .Select(r => new RecipeSummaryViewModel               ❷ 
        {                                                     ❷ 
            Id = r.RecipeId,                                  ❷ 
            Name = r.Name,                                    ❷ 
            TimeToCook = $"{r.TimeToCook.TotalMinutes}mins"   ❷ 
        })
        .ToListAsync();      ❸
}

❶ A query starts from a DbSet property.
查询从 DbSet 属性开始。
❷ EF Core queries only the Recipe columns it needs to map the view model
correctly.
EF Core 仅查询正确映射视图模型所需的 Recipe 列。
❸ Executes the SQL query and creates the final view models
执行 SQL 查询并创建最终视图模型

Notice that in the Select method you convert the TimeToCook property from a TimeSpan to a string by using string interpolation:

请注意,在 Select 方法中,通过使用字符串插值将 TimeToCook 属性从 TimeSpan 转换为字符串:

TimeToCook = $"{r.TimeToCook.TotalMinutes}mins"

I said before that EF Core converts the series of LINQ expressions to SQL, but that statement is a half-truth: EF Core can’t or doesn’t know how to convert some expressions to SQL. In those cases, such as this example, EF Core finds the fields from the DB that it needs to run the expression on the client side, selects them from the database, and then runs the expression in C#. This approach lets you combine the power and performance of database-side evaluation without compromising the functionality of C#.

我之前说过 EF Core 将一系列 LINQ 表达式转换为 SQL,但该说法是半真半假:EF Core 不能或不知道如何将某些表达式转换为 SQL。在这些情况下(例如本示例),EF Core 从数据库中找到在客户端运行表达式所需的字段,从数据库中选择这些字段,然后在 C# 中运行表达式。此方法允许您将数据库端评估的强大功能和性能相结合,而不会影响 C# 的功能。

Warning Client-side evaluation is both powerful and useful but has the potential to cause problems. In general, recent versions of EF Core throw an exception if a query requires dangerous client-side evaluation, ensuring (for example) that you can’t accidentally return all records to the client before filtering. For more examples, including ways to avoid these problems, see the documentation at http://mng.bz/zxP6.
警告 客户端评估功能强大且有用,但可能会导致问题。通常,如果查询需要危险的客户端评估,最新版本的 EF Core 会引发异常,例如,确保在筛选之前不会意外地将所有记录返回给客户端。有关更多示例,包括避免这些问题的方法,请参阅 http://mng.bz/zxP6 中的文档。

At this point, you have a list of records displaying a summary of the recipe’s data, so the obvious next step is loading the detail for a single record.

此时,您有一个记录列表,其中显示了配方数据的摘要,因此显而易见的下一步是加载单个记录的详细信息。

12.4.3 Loading a single record

12.4.3 加载单个记录

For most intents and purposes, loading a single record is the same as loading a list of records. Both approaches have the same common structure you saw in figure 12.13, but when you’re loading a single record, you typically use a Where clause that restricts the data to a single entity.

对于大多数 intent 和目的,加载单个记录与加载记录列表相同。这两种方法都具有您在图 12.13 中看到的相同的通用结构,但是当您加载单个记录时,您通常会使用 Where 子句将数据限制为单个实体。

Listing 12.8 shows the code to fetch a recipe by ID, following the same basic pattern as before (figure 12.12). It uses a Where() LINQ expression to restrict the query to a single recipe, where RecipeId == id, and a Select clause to map to RecipeDetailViewModel. The SingleOrDefaultAsync() clause causes EF Core to generate the SQL query, execute it on the database, and build the view model.

清单 12.8 显示了通过 ID 获取配方的代码,遵循与之前相同的基本模式(图 12.12)。它使用 Where() LINQ 表达式将查询限制为单个配方,其中 RecipeId == id,并使用 Select 子句映射到 RecipeDetailViewModel。SingleOrDefaultAsync() 子句使 EF Core 生成 SQL 查询,在数据库上执行该查询,并生成视图模型。

Note SingleOrDefaultAsync()throws an exception if the previous Where clause returns more than one record.
注意 SingleOrDefaultAsync() 如果前面的 Where 子句返回多条记录,则引发异常。

Listing 12.8 Loading a single item using EF Core in RecipeService
清单 12.8 在配方服务

public async Task<RecipeDetailViewModel> GetRecipeDetail(int id)     ❶
{
    return await _context.Recipes         ❷
        .Where(x => x.RecipeId == id)    ❸
        .Select(x => new RecipeDetailViewModel    ❹
        {                                         ❹
            Id = x.RecipeId,                      ❹
            Name = x.Name,                        ❹
            Method = x.Method,                    ❹
            Ingredients = x.Ingredients                       ❺
            .Select(item => new RecipeDetailViewModel.Item    ❺
            {                                                 ❺
                Name = item.Name,                             ❺
                Quantity = $"{item.Quantity} {item.Unit}"     ❺
            })                                                ❺
        })
        .SingleOrDefaultAsync();     ❻
}

❶ The id of the recipe to load is passed as a parameter.
要加载的配方的 id 作为参数传递。
❷ As before, a query starts from a DbSet property.
与以前一样,查询从 DbSet 属性开始。
❸ Limits the query to the recipe with the provided id
将查询限制为具有提供的 ID 的配方
❹ Maps the Recipe to a RecipeDetailViewModel
将配方映射到 RecipeDetailViewModel
❺ Loads and maps linked Ingredients as part of the same query
加载和映射链接的 Ingredients 作为同一查询的一部分
❻ Executes the query and maps the data to the view model
执行查询并将数据映射到视图模型

Notice that as well as mapping the Recipe to a RecipeDetailViewModel, you map the related Ingredients for a Recipe, as though you’re working with the objects directly in memory. One advantage of using an ORM is that you can easily map child objects and let EF Core decide how best to build the underlying queries to fetch the data.

请注意,除了将 Recipe 映射到 RecipeDetailViewModel 之外,您还可以映射 Recipe 的相关 Ingredient,就像您直接在内存中处理对象一样。使用 ORM 的一个优点是,您可以轻松映射子对象,并让 EF Core 决定如何最好地构建基础查询来提取数据。

Note EF Core logs all the SQL statements it runs as LogLevel.Information events by default, so you can easily see what queries are running against the database.
注意 默认情况下,EF Core 将其运行的所有 SQL 语句记录为 LogLevel.Information 事件,因此您可以轻松查看针对数据库运行的查询。

Your app is definitely shaping up. You can create new recipes, view them all in a list, and drill down to view individual recipes with their ingredients and method. Soon, though, someone’s going to introduce a typo and want to change their data, so you’ll have to implement the U in CRUD: update.

您的应用肯定正在成型。您可以创建新配方,在列表中查看所有配方,并向下钻取以查看单个配方食谱及其成分和方法。不过,很快,有人会引入一个拼写错误并想要更改他们的数据,因此您必须在 CRUD: update 中实现 U。

12.4.4 Updating a model with changes

12.4.4 使用更改更新模型

Updating entities when they’ve changed generally is the hardest part of CRUD operations, as there are so many variables. Figure 12.14 shows an overview of this process as it applies to your recipe app.

在实体发生更改时更新实体通常是 CRUD作中最困难的部分,因为变量太多了。图 12.14 显示了适用于您的配方应用程序的此过程的概述。

alt text

Figure 12.14 Updating an entity involves three steps: read the entity using EF Core, update the properties of the entity, and call SaveChangesAsync() on the DbContext to generate the SQL to update the correct rows in the database.

图 12.14 更新实体涉及三个步骤:使用 EF Core 读取实体,更新实体的属性,并在 DbContext 上调用 SaveChangesAsync() 以生成 SQL 以更新数据库中的正确行。

I’m not going to handle the relationship aspect in this book because that problem generally is complex, and how you tackle it depends on the specifics of your data model. Instead, I’ll focus on updating properties on the Recipe entity itself.

我不打算在本书中处理关系方面,因为这个问题通常很复杂,如何解决它取决于数据模型的具体情况。相反,我将重点介绍更新 Recipe 上的属性实体本身。

Note For a detailed discussion of handling relationship updates in EF Core, see Entity Framework Core in Action, 2nd ed., by Jon P. Smith (Manning, 2021; http://mng.bz/w9D2).
注意 有关在 EF Core 中处理关系更新的详细讨论,请参阅 Jon P. Smith 的 Entity Framework Core in Action,第 2 版(Manning,2021 年; http://mng.bz/w9D2)。

For web applications, when you update an entity you typically follow the steps outlined in figure 12.14:
对于 Web 应用程序,当您更新实体时,通常会按照图 12.14 中概述的步骤进行作:

  1. Read the entity from the database.
    从数据库中读取实体。
  2. Modify the entity’s properties.
    修改实体的属性。
    3.Save the changes to the database.
    保存对数据库的更改。

You’ll encapsulate these three steps in a method on RecipeService called UpdateRecipe. This method takes an UpdateRecipeCommand parameter and contains the code to change the Recipe entity.

您将把这三个步骤封装在 RecipeService 上名为 UpdateRecipe 的方法中。此方法采用 UpdateRecipeCommand 参数,并包含用于更改 Recipe 实体的代码。

Note As with the Create command, you don’t modify the entities directly in the minimal API endpoint handler, ensuring that you keep the UI/API concern separate from the business logic.
注意 与 Create 命令一样,您不会直接在最小 API 终端节点处理程序中修改实体,从而确保将 UI/API 关注点与业务逻辑分开。

Listing 12.9 shows the RecipeService.UpdateRecipe method, which updates the Recipe entity. It performs the three steps we defined previously to read, modify, and save the entity. I’ve extracted the code to update the recipe with the new values to a helper method for clarity.

清单 12.9 显示了 RecipeService.UpdateRecipe 方法,该方法更新了 Recipe 实体。它执行我们之前定义的三个步骤来读取、修改和保存实体。为清楚起见,我提取了代码,以使用新值将配方更新为帮助程序方法。

Listing 12.9 Updating an existing entity with EF Core in RecipeService
清单 12.9 使用 EF Core 更新现有实体在 RecipeService 中

public async Task UpdateRecipe(UpdateRecipeCommand cmd)
{
    var recipe = await _context.Recipes.FindAsync(cmd.Id);  ❶
    if(recipe is null) {                                    ❷
        throw new Exception("Unable to find the recipe");   ❷
    }                                                       ❷
    UpdateRecipe(recipe, cmd);             ❸
    await _context.SaveChangesAsync();    ❹
}

static void UpdateRecipe(Recipe recipe, UpdateRecipeCommand cmd)   ❺
{                                                                  ❺
    recipe.Name = cmd.Name;                                        ❺
    recipe.TimeToCook =                                            ❺
        new TimeSpan(cmd.TimeToCookHrs, cmd.TimeToCookMins, 0);    ❺
    recipe.Method = cmd.Method;                                    ❺
    recipe.IsVegetarian = cmd.IsVegetarian;                        ❺
    recipe.IsVegan = cmd.IsVegan;                                  ❺
}                                                                  ❺

❶ Find is exposed directly by Recipes and simplifies reading an entity by id.
Find 由 Recipes 直接公开,并简化了按 ID 读取实体的过程。
❷ If an invalid id is provided, recipe will be null.
如果提供的 ID 无效,则 recipe 将为 null。
❸ Sets the new values on the Recipe entity
在 Recipe 实体上设置新值
❹ Executes the SQL to save the changes to the database
执行 SQL 以保存对数据库的更改
❺ A helper method for setting the new properties on the Recipe entity
用于在 Recipe 实体上设置新属性的辅助方法

In this example I read the Recipe entity using the FindAsync(id) method exposed by DbSet. This simple helper method loads an entity by its ID—in this case, RecipeId. I could have written a similar query with LINQ:

在此示例中,我使用 DbSet 公开的 FindAsync(id) 方法读取 Recipe 实体。这个简单的帮助程序方法按实体的 ID 加载实体,在本例中为 RecipeId。我本可以使用 LINQ 编写类似的查询:

_context.Recipes.Where(r=>r.RecipeId == cmd.Id).FirstOrDefault();

Using FindAsync() or Find() is a little more declarative and concise, however.
但是,使用 FindAsync() 或 Find() 的声明性和简洁性更强一些。

Tip Find is a bit more complicated. Find first checks to see whether the entity is already being tracked in EF Core’s DbContext. If so (because the entity was previously loaded in this request), the entity is returned immediately without calling the database. Using Find can obviously be faster if the entity is tracked, but it can be slower if you know that the entity isn’t being tracked yet.
提示 Find 稍微复杂一些。“查找优先”检查是否已在 EF Core 的 DbContext 中跟踪实体。如果是这样(因为之前在此请求中加载了实体),则立即返回该实体,而不调用数据库。如果跟踪实体,则使用 Find 显然会更快,但如果您知道尚未跟踪实体,则使用 Find 可能会更慢。

You may wonder how EF Core knows which columns to update when you call SaveChangesAsync(). The simplest approach would be to update every column. If the field hasn’t changed, it doesn’t matter if you write the same value again. But EF Core is cleverer than that.

你可能想知道 EF Core 在调用 SaveChangesAsync() 时如何知道要更新哪些列。最简单的方法是更新每一列。如果字段未更改,则再次写入相同的值并不重要。但 EF Core 比这更聪明。

EF Core internally tracks the state of any entities it loads from the database and creates a snapshot of all the entity’s property values so that it can track which ones have changed. When you call SaveChanges(), EF Core compares the state of any tracked entities (in this case, the Recipe entity) with the tracking snapshot. Any properties that have been changed are included in the UPDATE statement sent to the database, and unchanged properties are ignored.

EF Core 在内部跟踪它从数据库加载的任何实体的状态,并创建所有实体属性值的快照,以便它可以跟踪哪些实体已更改。调用 SaveChanges() 时,EF Core 会将任何跟踪实体(在本例中为 Recipe 实体)的状态与跟踪快照进行比较。任何已更改的属性都包含在发送到数据库的 UPDATE 语句中,而未更改的属性将被忽略。

Note EF Core provides other mechanisms to track changes, as well as options to disable change tracking. See the documentation or chapter 3 of Jon P. Smith’s Entity Framework Core in Action, 2nd ed., (Manning, 2021; http://mng.bz/q9PJ) for details. You can view which details the DbContext is tracking by accessing DbContext.ChangeTracer.DebugView, as described in the documentation at http://mng.bz/8rlz.
注意 EF Core 提供了其他机制来跟踪更改,以及用于禁用更改跟踪的选项。请参阅 Jon P. Smith 的 Entity Framework Core in Action, 2nd ed.(Manning,2021 年;http://mng.bz/q9PJ)了解详情。您可以通过访问 DbContext.ChangeTracer.DebugView 来查看 DbContext 正在跟踪的详细信息,如 http://mng.bz/8rlz 中的文档中所述。

With the ability to update recipes, you’re almost done with your recipe app. “But wait!” I hear you cry. “we haven’t handled the D in CRUD: delete!” That’s true, but in reality, I’ve found only a few occasions to delete data. Let’s consider the requirements for deleting a recipe from the application:

借助更新食谱的功能,您几乎完成了食谱应用程序。“但是等等!”我听到你哭泣。“我们还没有处理 CRUD 中的 D:删除!”这是真的,但实际上,我只发现了少数删除数据的机会。让我们考虑一下从应用程序中删除配方的要求:

  • You need to provide an API that deletes a recipe.
    您需要提供用于删除配方的 API。

  • After a recipe is deleted, it must not appear in the recipe list and can’t be retrieved.
    删除配方后,它不得显示在配方列表中,也无法检索。

You could achieve these requirements by deleting the recipe from the database, but the problem with data is that when it’s gone, it’s gone! What if a user accidentally deletes a record? Also, deleting a row from a relational database typically has implications on other entities. You can’t delete a row from the Recipe table in your application, for example, without also deleting all the Ingredient rows that reference it, thanks to the foreign-key constraint on Ingredient.RecipeId.

您可以通过从数据库中删除配方来实现这些要求,但数据的问题在于,当它消失时,它就消失了!如果用户不小心删除了记录怎么办?此外,从关系数据库中删除行通常会对其他实体产生影响。例如,由于对 Ingredient.RecipeId 的外键约束,您无法从应用程序的 Recipe 表中删除一行,而无需删除引用该行的所有 Ingredient 行。

EF Core can easily handle these true deletion scenarios for you with the DbContext .Remove(entity) command, but often what you mean when you find a need to delete data is to archive it or hide it from the UI. A common approach to handling this scenario is to include some sort of “Is this entity deleted?” flag on your entity, such as the IsDeleted flag I included on the Recipe entity:

EF Core 可以使用 DbContext 轻松处理这些真正的删除方案。Remove(entity) 命令,但当您发现需要删除数据时,您的意思是将其存档或从 UI 中隐藏数据。处理这种情况的常见方法是包括某种“Is this entity deleted?” 标志,例如IsDeleted 标志:

public bool IsDeleted { get; set; }

If you take this approach, deleting data suddenly becomes simpler, as it’s nothing more than an update to the entity—no more problems of lost data and no more referential-integrity problems.

如果您采用这种方法,删除数据会突然变得更加简单,因为它只不过是对实体的更新——不再有数据丢失的问题,也不再有引用完整性问题。

Note The main exception I’ve found to this pattern is when you’re storing your users’ personally identifying information. In these cases, you may be duty-bound (and potentially legally bound) to scrub their information from your database on request.
注意 我发现这种模式的主要例外是当您存储用户的个人身份信息时。在这些情况下,您可能有义务(并且可能受法律约束)根据要求从您的数据库中删除他们的信息。

With this approach, you can create a delete method on RecipeService that updates the IsDeleted flag, as shown in listing 12.10. In addition, make sure that you have Where() clauses in all the other methods in your RecipeService to ensure you can’t return a deleted Recipe, as you saw in listing 12.9 for the GetRecipes() method.

使用这种方法,你可以在 RecipeService 上创建一个 delete 方法来 更新 IsDeleted 标志,如清单 12.10 所示。此外,请确保在 RecipeService 的所有其他方法中都有 Where() 子句,以确保无法返回已删除的 Recipe,如清单 12.9 中的 GetRecipes() 方法所示。

Listing 12.10 Marking entities as deleted in EF Core
列表 12.10 在 EF Core 中将实体标记为已删除

public async Task DeleteRecipe(int recipeId)
{
    var recipe = await _context.Recipes.FindAsync(recipeId);     ❶
    if(recipe is null) {                                    ❷
        throw new Exception("Unable to find the recipe");   ❷
    }                                                       ❷
    recipe.IsDeleted = true;   ❸
    await _context.SaveChangesAsync();   ❹
}

❶ Fetches the Recipe entity by id
按 id 获取 Recipe 实体
❷ If an invalid id is provided, recipe will be null.
如果提供的 ID 无效,则 recipe 将为 null。
❸ Marks the Recipe as deleted
将配方标记为已删除
❹ Executes the SQL to save the changes to the database
行 SQL 以保存对数据库的更改

This approach satisfies the requirements—it removes the recipe from exposure by the API—but it simplifies several things. This soft-delete approach won’t work for all scenarios, but I’ve found it to be a common pattern in projects I’ve worked on.

这种方法满足了要求,它消除了 API 公开的配方,但它简化了几件事。这种软删除方法并不适用于所有情况,但我发现它是我参与过的项目中的常见模式。

Tip EF Core has a handy feature called global query filters. These filters allow\ you to specify a Where clause at the model level. You could ensure, for example, that EF Core never loads Recipes for which IsDeleted is true. This feature is also useful for segregating data in a multitenant environment. See the documentation for details: http://mng.bz/EQxd.
提示 EF Core 有一个称为全局查询筛选器的便捷功能。这些筛选条件允许您在模型级别指定 Where 子句。例如,您可以确保 EF Core 永远不会加载 IsDeleted 为 true 的配方。此功能对于在多租户环境中隔离数据也很有用。有关详细信息,请参阅文档:http://mng.bz/EQxd

We’re almost at the end of this chapter on EF Core. We’ve covered the basics of adding EF Core to your project and using it to simplify data access, but you’ll likely need to learn more about EF Core as your apps become more complex. In the final section of this chapter, I’d like to pinpoint a few things you need to take into consideration before using EF Core in your own applications so that you’ll be familiar with some of the problems you’ll face as your apps grow.

关于 EF Core 的本章即将结束。我们已经介绍了将 EF Core 添加到项目并使用它来简化数据访问的基础知识,但随着应用变得越来越复杂,你可能需要了解有关 EF Core 的更多信息。在本章的最后一部分中,我想指出在您自己的应用程序中使用 EF Core 之前需要考虑的一些事项,以便您熟悉随着应用程序的增长而面临的一些问题。

12.5 Using EF Core in production applications

12.5 在生产应用程序中使用 EF Core

This book is about ASP.NET Core, not EF Core, so I didn’t want to spend too much time exploring EF Core. This chapter should’ve given you enough information to get up and running, but you definitely need to learn more before you even think about putting EF Core into production. As I’ve said several times, I recommend reading Entity Framework Core in Action, 2nd ed., by Jon P. Smith (Manning, 2021), or exploring the EF Core documentation site at https://docs.microsoft.com/ef/core.

这本书是关于 ASP.NET Core 的,而不是 EF Core,因此我不想花太多时间探索 EF Core。本章应该已经为你提供了足够的信息来启动和运行,但你肯定需要在考虑将 EF Core 投入生产之前了解更多信息。正如我多次说过的,我建议阅读 Jon P. Smith 的 Entity Framework Core in Action,第 2 版(Manning,2021 年),或浏览 https://docs.microsoft.com/ef/core 的 EF Core 文档站点。

The following topics aren’t essential for getting started with EF Core, but you’ll quickly run up against them if you build a production-ready app. This section isn’t a prescriptive guide to tackling each of these items, but more a set of things to consider before you dive into production:

以下主题对于开始使用 EF Core 不是必需的,但如果您构建生产就绪型应用程序,您将很快遇到这些主题。本节不是处理这些项目的规范性指南,而是在深入研究生产之前需要考虑的一组事项:

  • Scaffolding of columns—EF Core uses conservative values for things like string columns by allowing strings of large or unlimited length. In practice, you may want to restrict these and other data types to sensible values.
    列的基架 – EF Core 通过允许较大或无限长度的字符串,对字符串列等内容使用保守值。在实践中,您可能希望将这些数据类型和其他数据类型限制为合理的值。

  • Validation—You can decorate your entities with DataAnnotations validation attributes, but EF Core won’t validate the values automatically before saving to the database. This behavior differs from EF 6.x behavior, in which validation was automatic.
    验证 – 您可以使用 DataAnnotations 验证属性修饰实体,但 EF Core 不会在保存到数据库之前自动验证值。此行为不同于 EF 6.x 行为,在 EF 6.x 中,验证是自动的。

  • Handling concurrency—EF Core provides a few ways to handle concurrency, which occurs when multiple users attempt to update an entity at the same time. One partial solution is to use Timestamp columns on your entities.
    处理并发 – EF Core 提供了几种处理并发的方法,当多个用户尝试同时更新实体时,会发生并发。一种部分解决方案是在实体上使用 Timestamp 列。

  • Handling errors—Databases and networks are inherently flaky, so you’ll always have to account for transient errors. EF Core includes various features to maintain connection resiliency by retrying on network failures.
    处理错误 - 数据库和网络本质上是不稳定的,因此您始终必须考虑暂时性错误。EF Core 包含各种功能,可通过在网络故障时重试来保持连接复原能力。

  • Synchronous vs. asynchronous—EF Core provides both synchronous and asynchronous commands for interacting with the database. Often, async is better for web apps, but this argument has nuances that make it impossible to recommend one approach over the other in all situations.
    同步与异步 – EF Core 提供用于与数据库交互的同步和异步命令。通常,async 更适合 Web 应用程序,但此参数具有细微差别,因此不可能在所有情况下都推荐一种方法而不是另一种方法。

EF Core is a great tool for being productive in writing data-access code, but some aspects of working with a database are unavoidably awkward. Database management is one of the thorniest problems to tackle. Most web applications use some sort of database, so the following problems are likely to affect ASP.NET Core developers at some point:

EF Core 是高效编写数据访问代码的绝佳工具,但使用数据库的某些方面不可避免地会遇到困难。数据库管理是需要解决的最棘手的问题之一。大多数 Web 应用程序都使用某种类型的数据库,因此以下问题可能会在某个时候影响 ASP.NET Core 开发人员:

  • Automatic migrations—If you deploy your app to production automatically as part of some sort of DevOps pipeline, you’ll inevitably need some way to apply migrations to a database automatically. You can tackle this situation in several ways, such as scripting the .NET tool, applying migrations in your app’s startup code, using EF Core bundles, or using a custom tool. Each approach has its pros and cons.
    自动迁移 — 如果您将应用程序作为某种 DevOps 管道的一部分自动部署到生产环境,那么您不可避免地需要某种方法来自动将迁移应用于数据库。您可以通过多种方式解决这种情况,例如为 .NET 工具编写脚本、在应用程序的启动代码中应用迁移、使用 EF Core 捆绑包或使用自定义工具。每种方法都有其优点和缺点。

  • Multiple web hosts—One specific consideration is whether you have multiple web servers hosting your app, all pointing to the same database. If so, applying migrations in your app’s startup code becomes harder, as you must ensure that only one app can migrate the database at a time.
    多个 Web 主机 - 一个特别的考虑因素是您是否有多个 Web 服务器托管您的应用程序,所有服务器都指向同一个数据库。如果是这样,在应用程序的启动代码中应用迁移会变得更加困难,因为您必须确保一次只有一个应用程序可以迁移数据库。

  • Making backward-compatible schema changes—A corollary of the multiple-web-host approach is that you’ll often be in a situation in which your app accesses a database that has a newer schema than the app thinks. Normally, you should endeavor to make schema changes backward-compatible wherever possible.
    进行向后兼容的架构更改 – 多 Web 主机方法的一个必然结果是,您经常会遇到这样的情况:您的应用程序访问的数据库具有比应用程序认为的架构更新的架构。通常,您应该尽可能努力使架构更改向后兼容。

  • Storing migrations in a different assembly—In this chapter I included all my logic in a single project, but in larger apps, data access is often in a different project from the web app. For apps with this structure, you must use slightly different commands when using .NET CLI or PowerShell cmdlets.
    将迁移存储在不同的程序集中 – 在本章中,我将所有逻辑包含在一个项目中,但在较大的应用程序中,数据访问通常与 Web 应用程序位于不同的项目中。对于具有此结构的应用程序,在使用 .NET CLI 或 PowerShell cmdlet 时,必须使用略有不同的命令。

  • Seeding data—When you first create a database, you often want it to have some initial seed data, such as a default user. EF 6.x had a mechanism for seeding data built in, whereas EF Core requires you to seed your database explicitly yourself.
    种子设定数据 – 首次创建数据库时,您通常希望它具有一些初始种子数据,例如默认用户。EF 6.x 内置了用于设定数据种子的机制,而 EF Core 要求您自己显式设定数据库种子。

How you choose to handle each of these problems depends on the infrastructure and the deployment approach you take with your app. None is particularly fun to tackle, but all are unfortunate necessities. Take heart, though; all these problems can be solved one way or another!

您选择如何处理这些问题取决于您对应用程序采用的基础设施和部署方法。没有一个是特别有趣的,但都是不幸的必需品。不过,请放心;所有这些问题都可以以某种方式解决!

That brings us to the end of this chapter on EF Core and part 2 of the book. In part 3 we move away from minimal APIs to look at building server-rendered page-based applications with Razor Pages.

这样,我们就结束了本章的 EF Core 和本书的第 2 部分。在第 3 部分中,我们将从最小的 API 转向使用 Razor Pages 构建服务器呈现的基于页面的应用程序。

12.6 Summary

12.6 总结

EF Core is an ORM that lets you interact with a database by manipulating standard POCO classes called entities in your application, reducing the amount of SQL and database knowledge you need to be productive.
EF Core 是一种 ORM,允许您通过作应用程序中称为实体的标准 POCO 类来与数据库交互,从而减少提高工作效率所需的 SQL 和数据库知识量。

EF Core maps entity classes to tables, properties on the entity to columns in the tables, and instances of entity objects to rows in these tables. Even if you use EF Core to avoid working with a database directly, you need to keep this mapping in mind.
EF Core 将实体类映射到表,将实体的属性映射到表中的列,并将实体对象的实例映射到这些表中的行。即使使用 EF Core 来避免直接使用数据库,也需要牢记此映射。

EF Core uses a database-provider model that lets you change the underlying database without changing any of your object manipulation code. EF Core has database providers for Microsoft SQL Server, SQLite, PostgreSQL, MySQL, and many others.
EF Core 使用数据库提供程序模型,该模型允许您在不更改任何对象作代码的情况下更改基础数据库。EF Core 具有适用于 Microsoft SQL Server、SQLite、PostgreSQL、MySQL 等的数据库提供程序。

EF Core is cross-platform and has good performance for an ORM, but it has a different feature set from EF 6.x. Nevertheless, EF Core is recommended for all new applications after EF 6.x.
EF Core 是跨平台的,对于 ORM 具有良好的性能,但它的功能集与 EF 6.x 不同。不过,建议将 EF Core 用于 EF 6.x 之后的所有新应用程序。

EF Core stores an internal representation of the entities in your application and how they map to the database, based on the DbSet properties on your application’s DbContext. EF Core builds a model based on the entity classes themselves and any other entities they reference.
EF Core 根据应用程序的 DbContext 上的 DbSet 属性存储应用程序中实体的内部表示形式以及它们如何映射到数据库。EF Core 基于实体类本身及其引用的任何其他实体构建模型。

You add EF Core to your app by adding a NuGet database provider package. You should also install the design packages for EF Core, which works in conjunction with the .NET tools to generate and apply migrations to a database.
通过添加 NuGet 数据库提供程序包,将 EF Core 添加到应用。您还应该安装 EF Core 的设计包,它与 .NET 工具结合使用,以生成迁移并将其应用于数据库。

EF Core includes many conventions for how entities are defined, such as primary keys and foreign keys. You can customize how entities are defined declaratively, by using DataAnnotations, or by using a fluent API.
EF Core 包括许多关于如何定义实体的约定,例如主键和外键。您可以使用 DataAnnotations 或使用 Fluent API 自定义以声明方式定义实体的方式。

Your application uses a DbContext to interact with EF Core and the database. You register it with a DI container using AddDbContext, defining the database provider and providing a connection string. This approach makes your DbContext available in the DI container throughout your app.
应用程序使用 DbContext 与 EF Core 和数据库交互。使用 AddDbContext 将其注册到 DI 容器,定义数据库提供程序并提供连接字符串。此方法使您的 DbContext 在整个应用程序中的 DI 容器中可用。

EF Core uses migrations to track changes to your entity definitions. They’re used to ensure that your entity definitions, EF Core’s internal model, and the database schema match.
EF Core 使用迁移来跟踪对实体定义的更改。它们用于确保实体定义、EF Core 的内部模型和数据库架构匹配。

After changing an entity, you can create a migration using either the .NET tool or Visual Studio PowerShell cmdlets. To create a new migration with the .NET command-line interface, run dotnet ef migrations add NAME in your project folder, where NAME is the name you want to give the migration. This command compares your current DbContext snapshot with the previous version and generates the necessary SQL statements to update your database.
更改实体后,您可以使用 .NET 工具或 Visual Studio PowerShell cmdlet 创建迁移。要使用 .NET 命令行界面创建新的迁移,请在项目文件夹中运行 dotnet ef migrations add NAME,其中 NAME 是要为迁移提供的名称。此命令将当前 DbContext 快照与以前的版本进行比较,并生成必要的 SQL 语句来更新数据库。

You can apply the migration to the database by using dotnet ef database update. This command creates the database if it doesn’t already exist and applies any outstanding migrations.
可以使用 dotnet ef database update 将迁移应用于数据库。此命令将创建数据库(如果尚不存在)并应用任何未完成的迁移。

EF Core doesn’t interact with the database when it creates migrations—only when you update the database explicitly—so you can still create migrations when you’re offline.
EF Core 在创建迁移时不与数据库交互,仅在显式更新数据库时交互,因此在脱机时仍可以创建迁移。

You can add entities to an EF Core database by creating a new entity, e, calling _context.Add(e) on an instance of your application’s data context, _context, and calling _context.SaveChangesAsync(). This technique generates the necessary SQL INSERT statements to add the new rows to the database.
可以通过创建新实体 e 并将实体添加到 EF Core 数据库,调用_上下文。Add(e) 在应用程序的数据上下文的实例上,_context,然后调用_SaveChangesAsync()上下文生成必要的 SQL INSERT 语句以将新行添加到数据库中。

You can load records from a database by using the DbSet properties on your app’s DbContext. These properties expose the IQueryable interface so you can use LINQ statements to filter and transform the data in the database before it’s returned.
您可以通过在应用程序的 DbContext 上使用 DbSet 属性从数据库加载记录。这些属性公开 IQueryable 接口,以便您可以在返回数据库中的数据之前使用 LINQ 语句对其进行筛选和转换。

Updating an entity consists of three steps: reading the entity from the database, modifying the entity, and saving the changes to the database. EF Core keeps track of which properties have changed so that it can optimize the SQL it generates.
更新实体包括三个步骤:从数据库中读取实体、修改实体以及保存对数据库的更改。EF Core 会跟踪哪些属性已更改,以便可以优化它生成的 SQL。

You can delete entities in EF Core by using the Remove method, but you should consider carefully whether you need this function. Often. a soft delete using an IsDeleted flag on entities is safer and easier to implement.
可以使用 Remove 方法删除 EF Core 中的实体,但应仔细考虑是否需要此函数。通常,在实体上使用 IsDeleted 标志的软删除更安全且更易于实现。

This chapter covers only a subset of the problems you must consider when using EF Core in your applications. Before using it in a production app, you should consider (among other things) the data types generated for fields, validation, handling concurrency, the seeding of initial data, handling migrations on a running application, and handling migrations in a web-farm scenario.
本章仅介绍在应用程序中使用 EF Core 时必须考虑的问题的子集。在生产应用程序中使用它之前,您应该考虑(除其他事项外)为字段生成的数据类型、验证、处理并发、初始数据的种子设定、在正在运行的应用程序上处理迁移以及在 Web 场方案中处理迁移。

ASP.NET Core in Action 11 Documenting APIs with OpenAPI

11 Documenting APIs with OpenAPI
11 使用 OpenAPI 记录 API

This chapter covers

本章涵盖

  • Understanding OpenAPI and seeing why it’s useful
    了解 OpenAPI 并了解它为何有用
  • Adding an OpenAPI description to your app
    将 OpenAPI 描述添加到您的应用程序
  • Improving your OpenAPI descriptions by adding metadata to endpoints
    通过向终端节点添加元数据来改进 OpenAPI 描述
  • Generating a C# client from your OpenAPI description
    从 OpenAPI 描述生成 C# 客户端

In this chapter I introduce the OpenAPI specification for describing RESTful APIs, demonstrate how to use OpenAPI to describe a minimal API application, and discuss some of the reasons you might want to do so.

在本章中,我将介绍用于描述 RESTful API 的 OpenAPI 规范,演示如何使用 OpenAPI 来描述最小的 API 应用程序,并讨论您可能希望这样做的一些原因。

In section 11.1 you’ll learn about the OpenAPI specification itself and where it fits in to an ASP.NET Core application. You’ll learn about the libraries you can use to enable OpenAPI documentation generation in your app and how to expose the document using middleware.

在第 11.1 节中,您将了解 OpenAPI 规范本身以及它在 ASP.NET Core 应用程序中的位置。您将了解可用于在应用程序中启用 OpenAPI 文档生成的库,以及如何使用中间件公开文档。

Once you have an OpenAPI document, you’ll see how to do something useful with it in section 11.2, where we add Swagger UI to your app. Swagger UI uses your app’s OpenAPI document to generate a UI for testing and inspecting the endpoints in your app, which can be especially useful for local testing.

拥有 OpenAPI 文档后,您将在第 11.2 节中看到如何使用它执行一些有用的作,我们将在其中将 Swagger UI 添加到您的应用程序。Swagger UI 使用应用程序的 OpenAPI 文档生成用于测试和检查应用程序中的端点的 UI,这对于本地测试特别有用。

After seeing your app described in Swagger UI, it’s time to head back to the code in section 11.3. OpenAPI and Swagger UI need rich metadata about your endpoints to provide the best functionality, so we look at some of the basic metadata you can add to your endpoints.

在 Swagger UI 中看到您的应用程序描述后,是时候返回第 11.3 节中的代码了。OpenAPI 和 Swagger UI 需要有关终端节点的丰富元数据才能提供最佳功能,因此我们来看看您可以添加到终端节点的一些基本元数据。

In section 11.4 you’ll learn about one of the best tooling features that comes from creating an OpenAPI description of your app: automatically generated clients. Using a third-party library called NSwag, you’ll learn how to automatically generate C# code and classes for interacting with your API based on the OpenAPI description you added in the previous sections. You’ll learn how to generate your client, customize the generated code, and rebuild the client when your app’s OpenAPI description changes.

在 Section 11.4 中,您将了解创建应用程序的 OpenAPI 描述所带来的最佳工具功能之一:自动生成的客户端。使用名为 NSwag 的第三方库,您将学习如何根据您在前面部分中添加的 OpenAPI 描述自动生成用于与 API 交互的 C# 代码和类。您将学习如何生成客户端、自定义生成的代码,以及在应用程序的 OpenAPI 描述发生更改时重新构建客户端。

Finally, in section 11.5, you’ll learn more ways to add metadata to your endpoints to give the best experience for your generated clients. You’ll learn how to add summaries and descriptions to your endpoints by using method calls and attributes and by extracting the XML documentation comments from your C# code.

最后,在第 11.5 节中,您将了解将元数据添加到终端节点的更多方法,以便为生成的客户端提供最佳体验。您将学习如何使用方法调用和属性以及从 C# 代码中提取 XML 文档注释,从而向终端节点添加摘要和说明。

Before we consider those advanced scenarios, we’ll look at the OpenAPI specification, what it is, and how you can add an OpenAPI document to your app.

在考虑这些高级方案之前,我们将了解 OpenAPI 规范、它是什么以及如何将 OpenAPI 文档添加到您的应用程序。

11.1 Adding an OpenAPI description to your app

11.1 将 OpenAPI 描述添加到您的应用程序

OpenAPI (previously called Swagger) is a language-agnostic specification for describing RESTful APIs. At its core, OpenAPI describes the schema of a JavaScript Object Notation (JSON) document which in turn describes the URLs available in your application, how to invoke them, and the data types they return. In this section you’ll learn how to generate an OpenAPI document for your minimal API application.

OpenAPI(以前称为 Swagger)是一种与语言无关的规范,用于描述 RESTful API。OpenAPI 的核心是描述 JavaScript 对象表示法 (JSON) 文档的架构,而 JSON 文档又描述了应用程序中可用的 URL、如何调用它们以及它们返回的数据类型。在本节中,您将学习如何为最小 API 应用程序生成 OpenAPI 文档。

Providing an OpenAPI document for your application makes it possible to add various types of automation for your app. You can do the following things, for example:

为您的应用程序提供 OpenAPI 文档可以为您的应用程序添加各种类型的自动化。例如,您可以执行以下作:

  • Explore your app using Swagger UI (section 11.2).
    使用 Swagger UI 浏览您的应用程序(第 11.2 节)。

  • Generate strongly-typed clients for interacting with your app (section 11.4).
    生成用于与您的应用程序交互的强类型客户端(第 11.4 节)。

  • Automatically integrate into third-party services such as Azure API Management
    自动集成到第三方服务中,例如 Azure API 管理。

Note If you’re familiar with SOAP from the old ASP.NET days, you can think of OpenAPI as being the HTTP/REST equivalent of Web Service Description Language (WSDL). Just as a .wsdl file described your XML SOAP services, so the OpenAPI document describes your REST API.
注意 如果您熟悉 ASP.NET 年代的 SOAP,则可以将 OpenAPI 视为 Web 服务描述语言 (WSDL) 的 HTTP/REST 等效项。正如 .wsdl 文件描述 XML SOAP 服务一样,OpenAPI 文档也描述了 REST API。

ASP.NET Core includes some support for OpenAPI documents out of the box, but to take advantage of them you’ll need to use a third-party library. The two best-known libraries to use are called NSwag and Swashbuckle. In this chapter I use Swashbuckle to add an OpenAPI document to an ASP.NET Core app. You can read how to use NSwag instead at http://mng.bz/6Dmy.

ASP.NET Core 包含一些开箱即用的 OpenAPI 文档支持,但要利用它们,您需要使用第三方库。两个最著名的库称为 NSwag 和 Swashbuckle。在本章中,我将使用 Swashbuckle 将 OpenAPI 文档添加到 ASP.NET Core 应用程序。您可以在 http://mng.bz/6Dmy 阅读如何使用 NSwag。

Note NSwag and Swashbuckle provide similar functionality for generating OpenAPI documents, though you’ll find slight differences in how to use them and in the features they support. NSwag also supports client generation, as you’ll see in section 11.4.
注意 NSwag 和 Swashbuckle 为生成 OpenAPI 文档提供了类似的功能,但您会发现它们的使用方式和它们支持的功能略有不同。NSwag 还支持客户端生成,您将在 11.4 节中看到。

Add the Swashbuckle.AspNetCore NuGet package to your project by using the NuGet Package Manager in Visual Studio, or use the .NET CLI by running
使用 Visual Studio 中的 NuGet 包管理器将 Swashbuckle.AspNetCore NuGet 包添加到项目中,或使用.NET CLI 通过运行

dotnet add package Swashbuckle.AspNetCore

from your project’s folder. Swashbuckle uses ASP.NET Core metadata services to retrieve information about all the endpoints in your application and to generate an OpenAPI document. Then this document is served by middleware provided by Swashbuckle, as shown in figure 11.1. Swashbuckle also includes middleware for visualizing your OpenAPI document, as you’ll see in section 11.2.

从项目的文件夹中。Swashbuckle 使用 ASP.NET Core 元数据服务来检索有关应用程序中所有终端节点的信息并生成 OpenAPI 文档。然后这个文档由 Swashbuckle 提供的中间件提供,如图 11.1 所示。Swashbuckle 还包括用于可视化 OpenAPI 文档的中间件,您将在 11.2 节中看到。

alt text

Figure 11.1 Swashbuckle uses ASP.NET Core metadata services to retrieve information about the endpoints in your application and builds an OpenAPI document. The OpenAPI middleware serves this document when requested. Swashbuckle also includes optional middleware for visualizing the OpenAPI document using Swagger UI.
图 11.1 Swashbuckle 使用 ASP.NET Core 元数据服务检索有关应用程序中终端节点的信息并构建 OpenAPI 文档。OpenAPI 中间件在请求时提供此文档。Swashbuckle 还包括用于使用 Swagger UI 可视化 OpenAPI 文档的可选中间件。

After installing Swashbuckle, configure your application to generate an OpenAPI document as shown in listing 11.1. This listing shows a reduced version of the fruit API from chapter 5, with only the GET and POST methods included for simplicity. The OpenAPI-related additions are in bold.

安装 Swashbuckle 后,配置您的应用程序以生成一个 OpenAPI 文档,如清单 11.1 所示。此清单显示了第 5 章中 fruit API 的简化版本,为简单起见,仅包含 GET 和 POST 方法。与 OpenAPI 相关的添加内容以粗体显示。

Note Swashbuckle uses the old Swagger nomenclature rather than OpenAPI in its method names. You should think of OpenAPI as the name of the specification and Swagger as the name of the tooling related to OpenAPI, as described in this post: http://mng.bz/o18M.
注意 Swashbuckle 在其方法名称中使用旧的 Swagger 命名法,而不是 OpenAPI。您应该将 OpenAPI 视为规范的名称,将 Swagger 视为与 OpenAPI 相关的工具的名称,如本文所述:http://mng.bz/o18M

Listing 11.1 Adding OpenAPI support to a minimal API app using Swashbuckle
清单 11.1 使用 Swashbuckle 向最小 API 应用程序添加 OpenAPI 支持

using System.Collections.Concurrent;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer(); ❶
builder.Services.AddSwaggerGen(); ❷

WebApplication app = builder.Build();

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

app.UseSwagger(); ❸
app.UseSwaggerUI(); ❹

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

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

app.Run();

record Fruit(string Name, int Stock);

❶ Adds the endpoint-discovery features of ASP.NET Core that Swashbuckle requires
添加了 Swashbuckle 所需的 ASP.NET Core 的端点发现功能
❷ Adds the Swashbuckle services required for creating OpenApi Documents
添加了创建 OpenApi 文档所需的 Swashbuckle 服务
❸ Adds middleware to expose the OpenAPI document for your app
添加中间件以公开应用程序的 OpenAPI 文档
❹ Adds middleware that serves the Swagger UI
添加为 Swagger UI 提供服务的中间件

With the changes in this listing, your application exposes an OpenAPI description of its endpoints. If you run the app and navigate to /swagger/v1/swagger.json, you’ll find a large JSON file, similar to the one shown in figure 11.2. This file is the OpenAPI Document description of your application.

通过此清单中的更改,您的应用程序将公开其终端节点的 OpenAPI 描述。如果您运行应用程序并导航到 /swagger/v1/swagger.json,您将找到一个大型 JSON 文件,类似于图 11.2 中所示的文件。此文件是应用程序的 OpenAPI 文档描述。

alt text

Figure 11.2 The OpenAPI Document for the app described in listing 11.1, generated with NSwag.
图 11.2 清单 11.1 中描述的应用程序的 OpenAPI 文档,使用 NSwag 生成

The OpenAPI document includes a general description of your app, such as a title and version, as well as specific details about each of the endpoints. In figure 11.2, for example, the /fruit/{id} endpoint describes the fact that it needs a GET verb and takes an id parameter in the path.

OpenAPI 文档包括应用程序的一般描述,例如标题和版本,以及有关每个终端节点的具体详细信息。例如,在图 11.2 中,/fruit/{id} 端点描述它需要一个 GETverb 并在 path 中采用 id 参数。

You can change some of the document values, such as the title, by adding configuration to the AddSwaggerGen() method. You can set the title of the app to "Fruitify" and add a description for the document:

您可以通过向 AddSwaggerGen() 方法添加配置来更改某些文档值,例如标题。您可以将应用程序的标题设置为 “Fruitify” 并为文档添加描述:

builder.Services.AddSwaggerGen(x =>
    x.SwaggerDoc("v1", new OpenApiInfo()
    {
        Title = "Fruitify",
        Description = "An API for interacting with fruit stock",
        Version = "1.0"
    }));

You can also change settings such as the path used to expose the document and various minutia about how Swashbuckle generates the final JSON. See the documentation for details: http://mng.bz/OxQR.

您还可以更改设置,例如用于公开文档的路径以及有关 Swashbuckle 如何生成最终 JSON 的各种细节。有关详细信息,请参阅文档:http://mng.bz/OxQR

All that is clever, but if you’re shrugging and asking “So what?”, where OpenAPI really shines is the hooks it provides for other tooling. And you’ve already added one such piece of tooling to your app: Swagger UI.

所有这些都很聪明,但如果你耸耸肩问“那又怎样”,OpenAPI 真正闪耀的地方是它为其他工具提供的钩子。您已经向应用程序添加了这样一个工具:Swagger UI。

11.2 Testing your APIs with Swagger UI

11.2 使用 Swagger UI 测试 API

In this section you’ll learn about Swagger UI (https://swagger.io/tools/swagger-ui), an open-source web UI that makes it easy to visualize and test your OpenAPI apps. In some ways you can think of Swagger UI as being a light version of Postman, which I used in previous chapters to interact with minimal API applications. Swagger UI provides an easy way to view all the endpoints in your application and send requests to them. Postman provides many extra features, such as creating collections and sharing them with your team, but if all you’re trying to do is test your application locally, Swagger UI is a great option.

在本节中,您将了解 Swagger UI (https://swagger.io/tools/swagger-ui),这是一种开源 Web UI,可让您轻松可视化和测试 OpenAPI 应用程序。在某些方面,您可以将 Swagger UI 视为 Postman 的轻量级版本,我在前面的章节中使用它来与之交互最少的 API 应用程序。Swagger UI 提供了一种简单的方法来查看应用程序中的所有终端节点并向它们发送请求。Postman 提供了许多额外的功能,例如创建集合并与您的团队共享它们,但如果您只想在本地测试您的应用程序,那么 Swagger UI 是一个不错的选择。

You can add Swagger UI to your ASP.NET Core application using Swashbuckle by calling
您可以通过调用 Swashbuckle 将 Swagger UI 添加到您的 ASP.NET Core 应用程序中

app.UseSwaggerUI()

to add the Swagger UI middleware, as you saw in listing 11.1. The Swagger UI middleware automatically integrates with the OpenAPI document middleware and exposes the Swagger UI web UI in your app at the path /swagger by default. Navigate to /swagger in your app, and you see a page like the one in figure 11.3.

添加 Swagger UI 中间件,如清单 11.1 所示。默认情况下,Swagger UI 中间件会自动与 OpenAPI 文档中间件集成,并在应用程序中的路径 /swagger 中公开 Swagger UI Web UI。导航到/swagger 添加到您的应用程序中,您会看到一个类似于图 11.3 中的页面。

alt text

Figure 11.3 The Swagger UI endpoint for the app. With this UI you can view all the endpoints in your app, the schema of objects that are sent and returned, and even test the APIs by providing parameters and sending requests.
图 11.3 应用程序的 Swagger UI 端点。使用此 UI,您可以查看应用程序中的所有端点、已发送和返回的对象的 schema,甚至通过提供参数和发送请求来测试 API。

Swagger UI lists all the endpoints described in the OpenAPI document, the schema of objects that are sent to and received from each API, and all the possible responses that each endpoint can return. You can even test an API from the UI by choosing Try it out, entering a value for the parameter, and choosing Execute. Swagger UI shows the command executed, the response headers, and the response body (figure 11.4).

Swagger UI 列出了 OpenAPI 文档中描述的所有端点、发送到每个 API 和从每个 API 接收的对象架构,以及每个端点可以返回的所有可能响应。您甚至可以从 UI 中测试 API,方法是选择 Try it out (试用),输入参数值,然后选择 Execute (执行)。Swagger UI 显示执行的命令、响应标头和响应正文(图 11.4)。

alt text

Figure 11.4 You can send requests using the Swagger UI by choosing an API, entering the required parameters, and choosing Execute. Swagger UI shows the response received.
图 11.4 您可以通过选择 API、输入所需参数并选择 Execute (执行) 来使用 Swagger UI 发送请求。Swagger UI 显示收到的响应。

Swagger UI is a useful tool for exploring your APIs and can replace a tool like Postman in some cases. But the examples we’ve shown so far reveal a problem with our API: the responses described for the GET endpoint in figure 11.3 mentioned a 200 response, but our execution in figure 11.4 reveals that it can also return a 404. To solve that documentation issue, we need to add extra metadata to our APIs.

Swagger UI 是探索 API 的有用工具,在某些情况下可以取代 Postman 等工具。但是,到目前为止我们展示的示例揭示了 API 的一个问题:图 11.3 中为 GET 端点描述的响应提到了 200 响应,但我们在图 11.4 中的执行表明它也可以返回 404。为了解决该文档问题,我们需要向 API 添加额外的元数据。

11.3 Adding metadata to your minimal APIs

11.3 将元数据添加到最小 API

Metadata is information about an API that doesn’t change the execution of the API itself. You used metadata in chapter 5 when you added names to your endpoints, using WithName(), so that you could reference them by using LinkGenerator. The name doesn’t change anything about how the endpoint executes, but it provides information for other features to hook into.

元数据是有关 API 的信息,它不会更改 API 本身的执行。在第 5 章中,当您使用 WithName() 向终端节点添加名称时,您使用了元数据,以便您可以使用 LinkGenerator 引用它们。该名称不会更改终端节点的执行方式,但它为要挂接的其他功能提供了信息。

Currently, you can add three broad categories of metadata to minimal API endpoints:

目前,您可以将三大类元数据添加到最小 API 终端节点:

  • Routing metadata—As you’ve already seen, the WithName() methods adds a globally unique name to an endpoint that’s used for URL generation.
    路由元数据 – 如您所见,WithName() 方法将全局唯一名称添加到用于 URL 生成的终端节点。

  • Metadata for other middleware—Several pieces of middleware can be customized on a per-request basis by adding metadata to an endpoint. When the middleware runs, it checks the selected endpoint’s metadata and acts accordingly. Examples include authorization, hostname filtering, and output caching.
    其他中间件的元数据 — 通过向终端节点添加元数据,可以按请求自定义多个中间件。当中间件运行时,它会检查所选终端节点的元数据并采取相应的行动。示例包括授权、主机名筛选和输出缓存。

  • OpenAPI metadata—OpenAPI document generation is driven by the metadata exposed by endpoints, which in turn controls the UI exposed by Swagger UI.
    OpenAPI 元数据 - OpenAPI 文档生成由终端节点公开的元数据驱动,而终端节点又控制 Swagger UI 公开的 UI。

We look at how to add authorization metadata to your endpoints in chapter 25, so for now we’ll focus on improving the OpenAPI description of your app using metadata. You can provide a lot of details to document your APIs, some of which Swashbuckle uses during OpenAPI generation and some of which it doesn’t. The following listing shows how to add a tag for each API and how to explicitly describe the responses that are returned, using Produces().

我们将在第 25 章中介绍如何将授权元数据添加到您的终端节点,因此现在我们将专注于使用元数据改进应用程序的 OpenAPI 描述。您可以提供大量详细信息来记录您的 API,其中一些 Swashbuckle 在 OpenAPI 生成期间使用,而另一些则不使用。下面的清单显示了如何为每个 API 添加一个标签,以及如何使用 Produces() 显式描述返回的响应。

Listing 11.2 Adding OpenAPI metadata to improve endpoint documentation
清单 11.2 添加 OpenAPI 元数据以改进端点文档

using System.Collections.Concurrent;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

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

WebApplication app = builder.Build();

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

app.UseSwagger();
app.UseSwaggerUI();

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

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

app.Run();
record Fruit(string Name, int stock);

❶ Adding a tag groups the endpoints in Swagger UI. Each endpoint can have multiple
tags.
添加标签对 Swagger UI 中的端点进行分组。每个终端节点可以有多个标签。
❷ The endpoint can return a Fruit object. When not specified, a 200 response is
assumed.
端点可以返回 Fruit 对象。如果未指定,则假定响应为 200。
❸ If the id isn’t found, the endpoint returns a 404 Problem Details response.
如果未找到 ID,则终端节点将返回 404 Problem Details 响应。
❹ Adding a tag groups the endpoints in Swagger UI. Each endpoint can have multiple
tags.
在 Swagger UI 中添加标签对端点进行分组。每个终端节点可以有多个标签。
❺ This endpoint also returns a Fruit object but uses a 201 response instead of 200.
此端点还返回一个 Fruit 对象,但使用 201 响应而不是 200。
❻ If the ID already exists, it returns a 400 Problem Details response with validation
errors.
如果 ID 已存在,则返回 400 Problem Details 响应,其中包含验证错误。

With these changes, Swagger UI shows the correct responses for each endpoint, as shown in figure 11.5. It also groups the endpoints under the tag "fruit" instead of the default tag inferred from the project name when no tags are provided.

通过这些更改,Swagger UI 会为每个端点显示正确的响应,如图 11.5 所示。它还将终端节点分组在标签 “fruit” 下,而不是在未提供标签时从项目名称推断的默认标签下。

alt text

Figure 11.5 Swagger UI groups the endpoints in your application based on the Tag metadata attached to them. The UI uses the metadata added by calling Produces() to document the expected return types and status codes for each endpoint.
图 11.5 Swagger UI 根据附加到终端节点的 Tag 元数据对应用程序中的终端节点进行分组。UI 使用通过调用 Produces() 添加的元数据来记录每个端点的预期返回类型和状态代码。

If adding all this extra metadata feels like a bit of a chore, don’t worry. Adding the extra OpenAPI metadata is optional, necessary only if you plan to expose your OpenAPI document for others to consume. If all you want is an easy way to test your minimal APIs, you can go a long way without many of these extra method calls.

如果添加所有这些额外的元数据感觉有点麻烦,请不要担心。添加额外的 OpenAPI 元数据是可选的,只有当您计划公开 OpenAPI 文档供其他人使用时,才需要添加元数据。如果你想要的只是一种简单的方法来测试你的最小API,您可以在没有许多额外方法调用的情况下走很长的路。

Tip Remember that you can also use route groups (described in chapter 5) to apply metadata to multiple APIs at the same time.
提示 请记住,您还可以使用路由组(如第 5 章所述)同时将元数据应用于多个 API。

One of the strongest arguments for making your OpenAPI descriptions as rich as possible is that it makes the tooling around your API easier to use. Swagger UI is one example. But an arguably even more useful tool lets you automatically generate C# clients for interacting with your APIs.

使 OpenAPI 描述尽可能丰富的最有力的论据之一是,它使围绕 API 的工具更易于使用。Swagger UI 就是一个例子。但是,一个可以说更有用的工具允许您自动生成用于与 API 交互的 C# 客户端。

11.4 Generating strongly typed clients with NSwag

11.4 使用 NSwag 生成强类型客户端

In this section you’ll learn how to use your OpenAPI description to automatically generate a client class that you can use to call your API from another C# project. You’ll create a console application, use a .NET tool to generate a C# client for interacting with your API, and finally customize the generated types. The generated code includes automatic serialization and deserialization of request types, and makes interacting with your API from another C# project much easier than the alternative method of crafting HTTP requests manually.

在本节中,您将学习如何使用 OpenAPI 描述自动生成可用于从另一个 C# 项目调用 API 的客户端类。您将创建一个控制台应用程序,使用 .NET 工具生成用于与 API 交互的 C# 客户端,最后自定义生成的类型。生成的代码包括请求类型的自动序列化和反序列化,并且与手动创建 HTTP 请求的替代方法相比,从另一个 C# 项目与 API 交互要容易得多。

Note Generating a strongly typed client is optional. It makes it easier to consume your APIs from C#, but if you don’t need this functionality, you can still test your APIs by using Postman or another HTTP client.
注意 生成强类型客户端是可选的。它使从 C# 使用 API 变得更加容易,但如果您不需要此功能,您仍然可以使用 Postman 或其他 HTTP 客户端来测试 API。

You could use any of several tools to automatically generate a C# client from an OpenAPI description, such as OpenAPI Generator (http://mng.bz/Y1wB), but in this chapter I use NSwag. You may remember from section 11.1 that NSwag can be used instead of Swashbuckle to generate an OpenAPI description for your API. But unlike Swashbuckle, NSwag also contains a client generator. NSwag is also the default library used by both Visual Studio and the Microsoft .NET OpenAPI global tool to generate C# client code.

您可以使用多种工具中的任何一种从 OpenAPI 描述自动生成 C# 客户端,例如 OpenAPIGenerator (http://mng.bz/Y1wB),但在本章中我使用 NSwag。你可能还记得 11.1 节 中,可以使用 NSwag 代替 Swashbuckle 来为你的 API 生成 OpenAPI 描述。但与 Swashbuckle 不同的是,NSwag 还包含一个客户端生成器。NSwag 也是 Visual Studio 和 Microsoft .NET OpenAPI 全局工具用来生成 C# 客户端代码的默认库。

Code generation based on an OpenAPI description works via the process shown in figure 11.6. First, Visual Studio or the .NET tool downloads the OpenAPI description JSON file so that it’s available locally. The code generation tool reads the OpenAPI description, identifies all the endpoints and schemas described by the document, and generates a C# client class that you can use to call the API described in the document. The code generation tool hooks into the build process so that any time the local OpenAPI description file changes, the code generator runs to regenerate the client.

基于 OpenAPI 描述的代码生成工作过程如图 11.6 所示。首先,Visual Studio 或 .NET 工具下载 OpenAPI 描述 JSON 文件,以便它在本地可用。代码生成工具读取 OpenAPI 描述,识别文档描述的所有端点和架构,并生成一个 C# 客户端类,您可以使用该类来调用文档中描述的 API。代码生成工具挂接到构建过程中,因此,每当本地 OpenAPI 描述文件发生更改时,代码生成器都会运行以重新生成客户端。

alt text

Figure 11.6 Visual Studio or a .NET tool downloads the OpenAPI description locally and installs the code-generation tool from NuGet. When your project builds, the generation tool reads the OpenAPI description and generates a C# class for interacting with the API.
图 11.6 Visual Studio 或 .NET 工具在本地下载 OpenAPI 描述,并从 NuGet 安装代码生成工具。生成项目时,生成工具会读取 OpenAPI 描述并生成用于与 API 交互的 C# 类。

You can generate clients by using Visual Studio, as shown in section 11.4.1, or a .NET tool, as shown in section 11.4.2. Both approaches produce the same result, so your choice is a matter of personal preference.

您可以使用 Visual Studio(如第 11.4.1 节所示)或 .NET 工具(如第 11.4.2 节所示)生成客户端。这两种方法都会产生相同的结果,因此您的选择取决于个人喜好。

11.4.1 Generating a client using Visual Studio

11.4.1 使用 Visual Studio 生成客户端

In this section I show how to generate a client by using Visual Studio’s built-in support. For this section I assume that you have a simple .NET 7 console app that needs to interact with your minimal API app.

在本节中,我将介绍如何使用 Visual Studio 的内置支持生成客户端。在本部分中,我假设你有一个简单的 .NET 7 控制台应用程序,它需要与最小 API 应用程序交互。

Note In the sample code for this chapter, both applications are in the same solution for simplicity, but they don’t need to be. You don’t even need the source code for the API; as long as you have the OpenAPI description of an API, you can generate a client for it.
注意 在本章的示例代码中,为简单起见,这两个应用程序位于同一解决方案中,但并非必须如此。您甚至不需要 API 的源代码;只要您具有 API 的 OpenAPI 描述,就可以为其生成客户端。

To generate the client, follow these steps:
要生成客户端,请执行以下步骤:

  1. Ensure that the API application is running and that the OpenAPI description JSON file is accessible. Note the URL at which the JSON file is exposed. If you’re following along with the source code for the book, run the OpenApiExample project.
    确保 API 应用程序正在运行,并且 OpenAPI 描述 JSON 文件可访问。记下公开 JSON 文件的 URL。如果您按照本书的源代码进行作,请运行 OpenApiExample 项目。

  2. In the client project, right-click the project file and then choose from the contextual menu, as shown in figure 11.7. This command opens the Add Service Reference dialog box.
    在客户端项目中,右键单击项目文件,然后从上下文菜单中选择 Add > Service Reference,如图 11.7 所示。此命令将打开 Add Service Reference 对话框。

alt text

Figure 11.7 Adding a service reference using Visual Studio. Right-click the project that will call the API, and choose Add > Service Reference.
图 11.7 使用 Visual Studio 添加服务引用。右键单击将调用 API 的项目,然后选择 Add > Service Reference。

  1. In the Add Service Reference dialog box, select OpenAPI and then choose Next. On the Add New OpenAPI Service Reference page, enter the URL where the OpenAPI document is located. Enter a namespace for the generated code and a name for the generated client class, as shown in figure 11.8, and then choose Finish.
    在 Add Service Reference (添加服务引用) 对话框中,选择 OpenAPI (OpenAPI),然后选择 Next (下一步)。在 Add New OpenAPI Service Reference 页面上,输入 OpenAPI 文档所在的 URL。输入生成的代码的命名空间和生成的客户端类的名称,如图 11.8 所示,然后选择 Finish。

alt text

Figure 11.8 Adding an OpenAPI service reference using Visual Studio. Add the link to the OpenAPI document, the code generation parameters, and click Finish. Visual Studio downloads the OpenAPI document and saves it to the project to use for code generation.
图 11.8 使用 Visual Studio 添加 OpenAPI 服务引用。将链接添加到 OpenAPI document,代码生成参数,然后单击 Finish。Visual Studio 下载 OpenAPI 文档并将其保存到项目中以用于代码生成。

The Service Reference Configuration Progress screen shows the changes Visual Studio makes to your application, such as installing various NuGet packages and downloading the OpenAPI document.
Service Reference Configuration Progress 屏幕显示 Visual Studio 对应用程序所做的更改,例如安装各种 NuGet 包和下载 OpenAPI 文档。

Tip If you’re running the sample code with Visual Studio, you can find the OpenAPI document at https://localhost:7186/swagger/v1/swagger.json. This location is also displayed in the Swagger UI.
提示 如果使用 Visual Studio 运行示例代码,则可以在 https://localhost:7186/swagger/v1/swagger.json 中找到 OpenAPI 文档。此位置也显示在 Swagger UI 中。

After performing these steps, look at the csproj file of your console app. You’ll see that several NuGet package references were added, as well as a new element, as shown in listing 11.3.

执行这些步骤后,请查看控制台应用的 csproj 文件。您将看到添加了多个 NuGet 包引用,以及一个新的 元素,如清单 11.3 所示。

Listing 11.3 Adding a service reference for OpenAPI client generation with Visual Studio
清单 11.3 为使用 Visual Studio 生成 OpenAPI 客户端添加服务引用

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net7.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
        <OpenApiReference ❶
            Include="OpenAPIs\swagger.json"
            CodeGenerator="NSwagCSharp"
            Namespace="Fruit"
            ClassName="FruitClient">
        <SourceUri>https://localhost:7186/swagger/v1/swagger.json</SourceUri>
        </OpenApiReference>
    </ItemGroup>

    <ItemGroup>
        <PackageReference ❷
            Include="Microsoft.Extensions.ApiDescription.Client"
            Version="3.0.0">
            <PrivateAssets>all</PrivateAssets>
            <IncludeAssets>runtime; build; native; contentfiles; analyzers;
                buildtransitive</IncludeAssets>
        </PackageReference>

        <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />

        <PackageReference Include="NSwag.ApiDescription.Client"
            Version="13.0.5">
                <PrivateAssets>all</PrivateAssets>
                <IncludeAssets>runtime; build; native; contentfiles; analyzers;
                    buildtransitive</IncludeAssets>
        </PackageReference>
    </ItemGroup>
</Project>

❶ Defines where the OpenAPI description was loaded from and code generation settings
定义 OpenAPI 描述的加载位置和代码生成设置
❷ Extra NuGet packages are required by the code generator.
代码生成器需要额外的 NuGet 包。

Theoretically, this code should be everything you need to generate the client. Unfortunately, Visual Studio adds some out-of-date packages that you’ll need to update before your project will build, as follows:
从理论上讲,此代码应该是生成客户端所需的一切。遗憾的是,Visual Studio 添加了一些过时的包,您需要在构建项目之前更新这些包,如下所示:

  1. Update NSwag.ApiDescription.Client to the latest version (currently, 13.18.2). This package does the code generation based on the OpenAPI description.
    将 NSwag.ApiDescription.Client 更新到最新版本(当前为 13.18.2)。此包根据 OpenAPI 描述执行代码生成。

  2. Update Microsoft.Extensions.ApiDescription.Client to the latest version (7.0.0 at the time of the .NET 7 release). This package is referenced transitively by NSwag.ApiDescription.Client anyway, so you don’t have to reference it directly, but doing so ensures that you have the latest version of the package.
    将 Microsoft.Extensions.ApiDescription.Client 更新到最新版本(.NET 7 版本发布时为 7.0.0)。无论如何,此包都由 NSwag.ApiDescription.Client 以传递方式引用,因此您不必直接引用它,但这样做可以确保您拥有最新版本的包。

NOTE By default, the generated client uses Newtonsoft.Json to serializes the requests and responses. In section 11.4.4 you’ll see how to replace it with the built-in System.Text.Json.
注意 默认情况下,生成的客户端使用 Newtonsoft.Json 来序列化请求和响应。在第 11.4.4 节中,你将了解如何将其替换为内置的 System.Text.Json。

After you make these changes, your project should look similar to the following listing.
进行这些更改后,您的项目应类似于下面的清单。

Listing 11.4 Updating package versions for OpenAPI generation
清单 11.4 更新包版本以生成 OpenAPI

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <OpenApiReference
        Include="OpenAPIs\swagger.json"
        CodeGenerator="NSwagCSharp"
        Namespace="Fruit"
        ClassName="FruitClient">
      <SourceUri>https://localhost:7186/swagger/v1/swagger.json</SourceUri>
    </OpenApiReference>
  </ItemGroup>

  <ItemGroup>
    <PackageReference
      Include="Microsoft.Extensions.ApiDescription.Client"
      Version="7.0.0">  ❶
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers;
          buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
    <PackageReference Include="NSwag.ApiDescription.Client"
      Version="13.18.2">  ❶
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers;
          buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

</Project>

❶ Updates to the latest version
更新到最新版本

With the packages updated, you can build your project and generate the FruitClient. In section 11.4.3 you’ll see how to use this client to call your API, but first we’ll look at how to generate the client with a .NET global tool if you’re not using Visual Studio.

更新包后,您可以构建项目并生成 FruitClient。在 11.4.3 节中,您将看到如何使用此客户端来调用您的 API,但首先我们将了解如何如果您使用 Visual Studio,请使用 .NET 全局工具生成客户端。

11.4.2 Generating a client using the .NET Global tool

11.4.2 使用.NET 全局工具

In this section you’ll learn how to generate a client from an OpenAPI definition by using a .NET global tool instead of Visual Studio. The result is essentially the same, so if you’ve followed the steps in section 11.4.1 in Visual Studio, you can skip this section.

在本部分中,你将了解如何使用 .NET 全局工具而不是 Visual Studio 从 OpenAPI 定义生成客户端。结果基本相同,因此,如果您已按照 Visual Studio 中 11.4.1 节中的步骤作,则可以跳过此部分。

Note You don’t have to use Visual Studio or a .NET tool. Ultimately ,you need a csproj file that looks like listing 11.4 and an OpenAPI definition JSON file in your project, so if you’re happy editing the project file and downloading the definition manually, you can take that approach. Visual Studio and the .NET tool simplify and automate some of these steps.
注意 您不必使用 Visual Studio 或 .NET 工具。最后,您需要一个类似于清单 11.4 的 csproj 文件和一个项目中的 OpenAPI 定义 JSON 文件,因此,如果您愿意编辑项目文件并手动下载定义,则可以采用这种方法。Visual Studio 和 .NET 工具简化并自动执行其中一些步骤。

As in section 11.4.1, the instructions in 11.4.2 assume that you have a console app that needs to call your API, that the API is accessible, and that it has an OpenAPI description. To generate a client by using NSwag, follow these steps:

与第 11.4.1 节一样,11.4.2 中的说明假定您有一个需要调用 API 的控制台应用程序,该 API 是可访问的,并且它具有 OpenAPI 描述。要使用 NSwag 生成客户端,请执行以下步骤:

  1. Ensure that the API application is running and that the OpenAPI description JSON file is accessible. Note the URL at which the JSON file is exposed. In the source code associated with the book, run the OpenApiExample project.
    确保 API 应用程序正在运行,并且 OpenAPI 描述 JSON 文件可访问。记下公开 JSON 文件的 URL。在与本书关联的源代码中,运行 OpenApiExample 项目。

  2. Install the .NET OpenAPI tool (http://mng.bz/GyOv) globally by running
    通过运行 .NET OpenAPI 工具 (http://mng.bz/GyOv) 全局安装

dotnet tool install -g Microsoft.dotnet-openapi
  1. From the project folder of your console app, add an OpenAPI reference by using the following command, substituting the path to the OpenAPI document and the location to download the JSON file to:
    在控制台应用程序的项目文件夹中,使用以下命令添加 OpenAPI 引用,将 OpenAPI 文档的路径和将 JSON 文件下载到的位置替换为:

    dotnet openapi add url http://localhost:5062/swagger/v1/swagger.json --output-file OpenAPIs\fruit.json

Tip If you’re running the sample code by using dotnet run, you can find the OpenAPI document at the preceding URL. This location is also displayed in the Swagger UI.
提示 如果使用 dotnet run 运行示例代码,则可以在前面的 URL 中找到 OpenAPI 文档。此位置也显示在 Swagger UI 中。

  1. Update the packages added to your project by running the following commands from the project folder:
    通过从项目文件夹运行以下命令来更新添加到项目中的包:
dotnet add package NSwag.ApiDescription.Client
dotnet add package Microsoft.Extensions.ApiDescription.Client
dotnet add package Newtonsoft.Json

After you run all these steps, your OpenAPI description file should have been downloaded to OpenAPIs\fruit.json, and your project file should look similar to the following listing (elements added by the tool highlighted in bold).

运行所有这些步骤后,您的 OpenAPI 描述文件应已下载到 OpenAPIs\fruit.json,并且您的项目文件应类似于以下清单(由该工具添加的元素以粗体突出显示)。

Listing 11.5 Adding an OpenAPI reference using the .NET OpenAPI tool
清单 11.5 使用 .NET OpenAPI 工具添加 OpenAPI 引用

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference
      Include="Microsoft.Extensions.ApiDescription.Client"
      Version="7.0.0">
        <IncludeAssets>runtime; build; native; contentfiles; analyzers;
          buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
    <PackageReference Include="NSwag.ApiDescription.Client"
      Version="13.18.2">
        <IncludeAssets>runtime; build; native; contentfiles; analyzers;
          buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <OpenApiReference Include="OpenAPIs\fruit.json"
      SourceUrl="http://localhost:5062/swagger/v1/swagger.json" />
  </ItemGroup>
</Project>

Other than minor ordering differences, the main difference between the Visual Studio approach and the .NET tool approach is that Visual Studio lets you specify the class name and namespace for your new client, whereas the .NET Tool uses the default values. For consistency, add the ClassName and Namespace attributes to the <OpenApiReference> element added by the tool:

除了细微的排序差异外,Visual Studio 方法和 .NET 工具方法之间的主要区别在于,Visual Studio 允许您为新客户端指定类名和命名空间,而 .NET 工具使用默认值。为了保持一致性,请将 ClassName 和 Namespace 属性添加到工具添加的 <OpenApiReference> 元素中:

<OpenApiReference Include="OpenAPIs\fruit.json"
  SourceUrl="http://localhost:5062/swagger/v1/swagger.json"
  Namespace="Fruit"
  ClassName="FruitClient" />

In section 11.4.4 you’ll learn how to customize the generated code further, but before we get to that topic, let’s look at the generated FruitClient and how to use it.

在 Section 11.4.4 中,您将学习如何进一步自定义生成的代码,但在我们进入该主题之前,让我们看看生成的 FruitClient 以及如何使用它。

11.4.3 Using a generated client to call your API

11.4.3 使用生成的客户端调用 API

So far, you’ve been taking my word for it that a client is magically generated for your application, so in this section you get to try it out. The NSwag.ApiDescription.Client package added to your project works with the Microsoft.Extensions.ApiDescription.Client package to read the OpenAPI description file in your project. From this description it can work out what APIs you have and what types you need to serialize to and from. Finally, it outputs a C# class with the class name and namespace you specified in the OpenApiReference element.

到目前为止,您一直相信我的话,即 Client 是为您的应用程序神奇生成的,因此在本节中,您将快来试试吧。添加到项目的 NSwag.ApiDescription.Client 包与 Microsoft.Extensions.ApiDescription.Client 包配合使用,以读取项目中的 OpenAPI 描述文件。从此描述中,它可以计算出您拥有哪些 API 以及需要序列化到哪些类型或从中序列化。最后,它输出一个 C# 类,其中包含您在 OpenApiReference 元素中指定的类名和命名空间。

Note The generated file is typically saved to your project’s obj folder. After building your project, you can find the fruitClient.cs file in this folder. Alternatively, use Visual Studio’s Go To Definition (F12) functionality on an instance of FruitClient to navigate to the code in your integrated development environment (IDE).
注意 生成的文件通常保存到项目的 obj 文件夹中。构建项目后,您可以在此文件夹中找到 fruitClient.cs 文件。或者,在 FruitClient 实例上使用 Visual Studio 的转到定义 (F12) 功能导航到集成开发环境 (IDE) 中的代码。

To use the FruitClient to call your API, you must create an instance of it, passing in the base address of your API and an HttpClient instance. Then you can send HTTP requests to the discovered endpoints. A client generated from the OpenAPI description of the simple minimal API in listing 11.2, for example, would have methods called FruitPOSTAsync() and FruitGETASync(), corresponding to the two exposed methods, as shown in listing 11.6.

要使用 FruitClient 调用 API,您必须创建一个 API 实例,并传入 API 的基址和 HttpClient 实例。然后,您可以向发现的终端节点发送 HTTP 请求。例如,从清单 11.2 中简单最小 API 的 OpenAPI 描述生成的客户端将具有名为 FruitPOSTAsync() 和 FruitGETASync() 的方法,对应于两个公开的方法,如下面的清单所示。

Listing 11.6 Calling the API from listing 11.2 using a generated client
清单 11.6 使用生成的客户端调用 清单 11.2 中的 API

using Fruit; ❶

var client = new FruitClient( ❷
    "https://localhost:7186", ❸
    new HttpClient()); ❹

Fruit.Fruit created = await client.FruitPOSTAsync("123", ❺
    new Fruit.Fruit { Name = "Banana", Stock = 100 }); ❻
Console.WriteLine($"Created {created.Name}");

Fruit.Fruit fetched = await client.FruitGETAsync("123"); ❼
Console.WriteLine($"Fetched {fetched.Name}");

❶ The code is generated in the Fruit namespace.
代码在 Fruit 命名空间中生成。
❷ Uses the generated FruitClient
使用生成的 FruitClient
❸ Specifies the base address of the API
指定 API 的基址
❹ The provided HttpClient is used to call the API.
提供的 HttpClient 用于调用 API。
❺ Calls the MapPost endpoint of the API
调用 API 的 MapPost 端点
❻ The Fruit type is generated automatically by NSwag.
Fruit 类型由 NSwag 自动生成。
❼ Calls the MapGet endpoint of the API
调用 API 的 MapGet 端点

This code is simultaneously impressive and somewhat horrible:

这段代码既令人印象深刻,又有些可怕:

  • It’s impressive that you’re able to generate all the boilerplate code for interacting with the API. You don’t have to do any string interpolation to calculate the path. You don’t have to serialize the request body or deserialize the response. You don’t have to check for error status codes. The generated code takes care of all those tasks.
    令人印象深刻的是,您能够生成用于与 API 交互的所有样板代码。您不必执行任何字符串插值来计算路径。您不必序列化请求正文或反序列化响应。您不必检查错误状态代码。生成的代码会处理所有这些任务。

  • Those FruitPOSTAsync and FruitGETAsync methods have really ugly names!
    那些 FruitPOSTAsync 和 FruitGETAsync 方法的名字真的很丑陋!

Luckily, you can fix the ugly method names: improve your API’s OpenAPI definition by adding WithName() to every API. The name you provide for your endpoint is used as the OperationID in the OpenAPI description; then NSwag uses it to generate the client methods. This scenario is a prime example of adding more metadata to your OpenAPI, making the tooling better for your consumers.

幸运的是,您可以修复丑陋的方法名称:通过向每个 API 添加 WithName() 来改进 API 的 OpenAPI 定义。您为终端节点提供的名称将用作 OpenAPI 描述中的 OperationID;然后 NSwag 使用它来生成客户端方法。此方案是向 OpenAPI 添加更多元数据的一个典型示例,使工具更适合您的使用者。

As well as improve your OpenAPI description, you can customize the code generation directly, as you’ll see in the next section.

除了改进 OpenAPI 描述外,您还可以直接自定义代码生成,如下一节所示。

11.4.4 Customizing the generated code

11.4.4 自定义生成的代码

In this section you’ll learn about some of the customization options available with the NSwag generator and why you might want to use them. I look at three customization options in this section:

在本节中,您将了解 NSwag 生成器提供的一些自定义选项以及您可能希望使用它们的原因。在本节中,我将查看三个自定义选项:

  • Using System.Text.Json instead of Newtonsoft.Json for JSON serialization
    使用 System.Text.Json 而不是 Newtonsoft.Json 进行 JSON 序列化

  • Generating an interface for the generated client implementation
    为生成的客户端实现生成接口

  • Not requiring an explicit BaseAddress parameter in the constructor
    在构造函数中不需要显式 BaseAddress 参数

By default, NSwag uses Newtonsoft.Json to serialize requests and deserialize responses. Newtonsoft.Json is a popular, battle-hardened JSON library, but .NET 7 has a built-in JSON library, System.Text.Json, that ASP.NET Core uses by default for JSON serialization. Instead of using two JSON libraries, you may want to replace the serialization used in your client to use System.Text.Json.

默认情况下,NSwag 使用 Newtonsoft.Json 来序列化请求和反序列化响应。Newtonsoft.Json 是一个常用的、久经考验的 JSON 库,但 .NET 7 有一个内置的 JSON 库 System.Text.Json,ASP.NET Core 默认使用该库进行 JSON 序列化。您可能希望将客户端中使用的序列化替换为 System.Text.Json,而不是使用两个 JSON 库。

When NSwag generates a client, it marks the class as partial, which means that you can define your own partial class FruitClient (for example) and add any methods that you think are useful to the client. The generated client also provides partial methods that act as hooks just before a request is sent or received.

当 NSwag 生成客户端时,它会将类标记为 partial,这意味着您可以定义自己的部分类 FruitClient(例如)并添加您认为对客户端有用的任何方法。生成的 Client 端还提供了部分方法,这些方法在发送或接收请求之前充当 hook。

Definition Partial methods in C# (http://mng.bz/zXEB) are void-returning methods that don’t have an implementation. You can define the implementation of the method in a separate partial class file. If you don’t define the implementation, the method is removed at compile time, so you use partial methods as highly performant event handlers.
定义 C# (http://mng.bz/zXEB) 中的分部方法是没有实现的返回 void 的方法。您可以在单独的Partial 类文件。如果未定义实现,则会在编译时删除该方法,因此将分部方法用作高性能事件处理程序。

Extending your generated clients is useful, but during testing it’s common to also want to substitute your generated client by using interfaces. Interfaces let you substitute fake or mock versions of a service so that your tests aren’t calling the API for real, as you learned in chapter 8. NSwag can help with this process by automatically generating an IFruitClient interface that the FruitClient implements.

扩展生成的 Client 端很有用,但在测试期间,通常还希望使用 interfaces 替换生成的 Client 端。接口允许你替换服务的 fake 或 mock 版本,这样你的测试就不会真正调用 API,正如你在第 8 章中学到的那样。NSwag 可以通过自动生成 FruitClient 实现的 IFruitClient 接口来帮助完成此过程。

Finally, providing a base address where the API is hosted makes sense on the face of it. But as we discussed in chapter 9, primitive constructor arguments such as string and int don’t play well with dependency injection. Given that HttpClient contains a BaseAddress property, you can configure NSwag to not require that the base address be passed as a constructor argument and instead set it on the HttpClient type directly. This approach helps in dependency injection (DI) scenarios, as you’ll when we discuss IHttpClientFactory in chapter 33.

最后,提供托管 API 的基址从表面上看是有意义的。但是正如我们在第 9 章中讨论的那样,像 string 和 int 这样的原始构造函数参数不能很好地与依赖注入配合。鉴于 HttpClient 包含 BaseAddress 属性,您可以将 NSwag 配置为不要求将基址作为构造函数参数传递,而是直接在 HttpClient 类型上设置它。‌这种方法在依赖关系注入 (DI) 场景中有所帮助,正如我们在第 33 章讨论 IHttpClientFactory 时所看到的那样。

These three seemingly unrelated options are all configured in NSwag in the same way: by adding an Options element to the <OpenApiReference> element in your project file. The options are provided as command-line switches and must be provided on one line, without line breaks. The switches for the three settings described are

这三个看似无关的选项在 NSwag 中都以相同的方式配置:通过在<OpenApiReference> 元素。这些选项以命令行开关的形式提供,并且必须在一行中提供,没有换行符。用于描述的三种设置的开关是

  • /UseBaseUrl:false—When false, NSwag removes the baseUrl parameter from the generated client’s constructor and instead relies on HttpClient to have the correct base address. It defaults to true.
    /UseBaseUrl:false — 当 false 时,NSwag 从生成的客户端的构造函数中删除 baseUrl 参数,而是依赖 HttpClient 具有正确的基址。它默认为 true。

  • /GenerateClientInterfaces:true—When true, NSwag generates an interface for the client, containing all the endpoints. The generated client implements this interface. It defaults to false.
    /GenerateClientInterfaces:true — 如果为 true,则 NSwag 会为客户端生成一个接口,其中包含所有端点。生成的 Client 端实现此接口。它默认为 false。

  • /JsonLibrary:SystemTextJson—This switch specifies the JSON serialization library to use. It defaults to using Newtonsoft.Json.
    /JsonLibrary:SystemTextJson - 此开关指定要使用的 JSON 序列化库。它默认使用 Newtonsoft.Json。

Tip A vast number of configuration options is available for NSwag. I find that the best documentation is available in the NSwag .NET tool. You can install the tool by using dotnet tool install -g NSwag.ConsoleCore, and you can view the available options by running nswag help openapi2csclient.
提示 NSwag 有大量的配置选项可用。我发现 NSwag .NET 工具中提供了最好的文档。可以使用 dotnet tool install -g NSwag.ConsoleCore 安装该工具,并且可以通过运行 nswag help openapi2csclient 来查看可用选项。

You can set all three of these options by adding an <Options> element to the <OpenApiReference> element, as shown in the following listing. Make sure that you open and close both elements correctly so the XML stays valid; it’s an easy mistake to make when editing by hand!

您可以通过向 <<OpenApiReference> 元素添加 <Options> 元素来设置所有这三个选项,如下面的清单所示。确保正确打开和关闭这两个元素,以便 XML 保持有效;手动编辑时很容易犯错误!‌

Listing 11.7 Customizing NSwag generator options
清单 11.7 自定义 NSwag 生成器选项

<OpenApiReference Include="OpenAPIs\fruit.json"
  SourceUrl="http://localhost:5062/swagger/v1/swagger.json"
  Namespace="Fruit"
  ClassName="FruitClient" >
    <Options>/UseBaseUrl:false /GenerateClientInterfaces:true 
[CA]/JsonLibrary:SystemTextJson</Options>  ❶
</OpenApiReference>  ❷

❶ Customizes the options NSwag uses for code generation
自定义 NSwag 用于代码生成的选项
❷ Make sure to close the outer XML element to keep the XML valid.
确保关闭外部 XML 元素以保持 XML 有效。

You’d be forgiven for thinking that after making these changes, NSwag would update the generated code next time you build. Unfortunately, it’s not necessarily that simple. NSwag watches for changes to the OpenAPI description JSON file saved in your project and will regenerate the code any time the file changes, but it won’t necessarily update when you change options in your csproj file. Even worse, doing a clean or rebuild similarly has no effect. If you find yourself in this situation, it’s best to delete the obj folder for your project to ensure that everything regenerates correctly.

如果你认为在进行这些更改后,NSwag 会在你下次构建时更新生成的代码,这是可以理解的。不幸的是,事情不一定那么简单。NSwag 会监视项目中保存的 OpenAPI 描述 JSON 文件的更改,并在文件更改时重新生成代码,但当您更改 csproj 文件中的选项时,它不一定会更新。更糟糕的是,以类似的方式进行清理或重建也没有效果。如果您发现自己处于这种情况,最好删除项目的 obj 文件夹,以确保所有内容都能正确重新生成。

Tip Another option is to make a tiny change in the OpenAPI document so that NSwag updates the generated code when you build your project. Then you can revert the OpenAPI document change.
提示 另一种选择是对 OpenAPI 文档进行微小的更改,以便 NSwag 在您构建项目时更新生成的代码。然后,您可以还原 OpenAPI 文档更改。

After you’ve persuaded NSwag to regenerate the client, you should update your code to use the new features. You can remove the Newtonsoft.Json reference from your csproj file and update your Program.cs as shown in the following listing.

在你说服 NSwag 重新生成客户端之后,你应该更新你的代码以使用新功能。您可以从 csproj 文件中删除 Newtonsoft.Json 引用并更新Program.cs,如下面的清单所示。

Listing 11.8 Using the updated NSwag client
清单 11.8 使用更新的 NSwag 客户端

using Fruit;

IFruitClient client = new FruitClient(    ❶
    new HttpClient() { BaseAddress =     ❷
        new Uri("https://localhost:7186") });    ❷

Fruit.Fruit created = await client.FruitPOSTAsync("123",
    new Fruit.Fruit { Name = "Banana", Stock = 100 });
Console.WriteLine($"Created {created.Name}");

Fruit.Fruit fetched = await client.FruitGETAsync("123");
Console.WriteLine($"Fetched {fetched.Name}");

❶ FruitClient now implements IFruitClient.
FruitClient 现在实现 IFruitClient。
❷ Sets the base address on HttpClient instead of passing as a constructor argument
在 HttpClient 上设置基址,而不是作为构造函数参数传递

If you updated the operation IDs for your API endpoints using WithName(), you may be a little surprised to see that you still have the ugly FruitPOSTAsync and FruitGETAsync methods, even though you regenerated the client. That’s because the OpenAPI description saved to your project is downloaded only once, when you initially add it. Let’s look at how to update the local OpenAPI document to reflect the changes to your remote API.

如果您使用 WithName() 更新了 API 终端节点的作 ID,您可能会有点惊讶地发现,即使您重新生成了客户端,您仍然拥有丑陋的 FruitPOSTAsync 和 FruitGETAsync 方法。这是因为保存到项目的 OpenAPI 描述在您最初添加时仅下载一次。让我们看看如何更新本地 OpenAPI 文档以反映对远程 API 的更改。

11.4.5 Refreshing the OpenAPI description

11.4.5 刷新 OpenAPI 描述

In this section you’ll learn how to update the OpenAPI description document saved to your project that’s used for generation. This document doesn’t update automatically, so the client generated by NSwag may not reflect the latest OpenAPI description for your API.

在本节中,您将了解如何更新保存到项目中用于生成的 OpenAPI 描述文档。本文档不会自动更新,因此 NSwag 生成的客户端可能无法反映您的 API 的最新 OpenAPI 描述。

Whether you used Visual Studio (as in section 11.4.1) or the .NET OpenAPI tool (as in section 11.4.2), the OpenAPI description saved as a JSON file to your project is a point-in-time snapshot of the API. If you add more metadata to your API, you need to download the OpenAPI description to your project again.

无论您使用的是 Visual Studio(如第 11.4.1 节所示)还是 .NET OpenAPI 工具(如第 11.4.2 节所示),以 JSON 文件形式保存到项目中的 OpenAPI 描述都是 API 的时间点快照。如果您向 API 添加更多元数据,则需要再次将 OpenAPI 描述下载到您的项目中。

Tip My preferred approach is low-tech: I simply navigate to the OpenAPI description in the browser, copy the JSON contents, and paste it into the JSON file in my project.
提示 我的首选方法是低技术含量的方法:我只需在浏览器中导航到 OpenAPI 描述,复制 JSON 内容,然后将其粘贴到我项目的 JSON 文件中。

If you don’t want to update the OpenAPI description manually, you can use Visual Studio or the .NET OpenAPI tool to refresh the saved document for you.

如果不想手动更新 OpenAPI 说明,可以使用 Visual Studio 或 .NET OpenAPI 工具刷新为您保存文档。

Warning If you originally used Visual Studio, you can’t refresh the document by using the OpenAPI tool and vice versa. The reason is that Visual Studio uses the SourceUri attribute on the OpenApiReference element and the .NET tool uses the SourceUrl attribute. And yes, that situation is arbitrary and annoying!
警告 如果您最初使用的是 Visual Studio,则无法使用 OpenAPI 工具刷新文档,反之亦然。原因是 Visual Studio 使用 OpenApiReference 元素上的 SourceUri 属性,而 .NET 工具使用 SourceUrl 属性。是的,这种情况是武断和烦人的!

To update your OpenAPI description by using Visual Studio, follow these steps:
要使用 Visual Studio 更新 OpenAPI 描述,请执行以下步骤:

  1. Ensure that your API is running and that the OpenAPI description document is available.
    确保您的 API 正在运行,并且 OpenAPI 描述文档可用。

  2. Navigate to the connected services page for your project by choosing Project > Connected Services > Manage Connected Services.
    通过选择 Project > Connected Services 导航到项目的 Connected Services 页面> 管理连接的服务。

3.Select the overflow button next to your OpenAPI reference and choose Refresh, as shown in figure 11.9. Then choose Yes in the dialog box to update your OpenAPI document.
选择 OpenAPI 引用旁边的溢出按钮,然后选择 Refresh,如图11.9所示. 然后在对话框中选择 Yes 以更新您的 OpenAPI 文档。

alt text

Figure 11.9 Updating the OpenAPI description for an API. Choose Refresh to download the OpenAPI description again and save it to your project. Then NSwag will generate an updated client on the next build.
图 11.9 更新 API 的 OpenAPI 描述。选择 Refresh (刷新) 以再次下载 OpenAPI 描述并将其保存到您的项目中。然后 NSwag 将在下一个构建中生成更新的客户端。

To update your OpenAPI description by using the .NET OpenAPI tool, follow these steps:
若要使用 .NET OpenAPI 工具更新 OpenAPI 说明,请执行以下步骤:

  1. Ensure that your API is running and that the OpenAPI description document is available.
    确保您的 API 正在运行,并且 OpenAPI 描述文档可用。

  2. From your project folder, run the following command, using the same URL you used to add the OpenAPI description originally:
    在项目文件夹中,使用最初用于添加 OpenAPI 描述的相同 URL 运行以下命令:

dotnet openapi refresh http://localhost:5062/swagger/v1/swagger.json

After updating your OpenAPI description by using either Visual Studio or the .NET tool, build your application to trigger NSwag to regenerate your client. Any changes you made to your OpenAPI description (such as adding operation IDs) will be reflected in the generated code.

使用 Visual Studio 或 .NET 工具更新 OpenAPI 描述后,构建应用程序以触发 NSwag 重新生成客户端。您对 OpenAPI 描述所做的任何更改(例如添加作 ID)都将反映在生成的代码中。

I think that client generation is the killer app for OpenAPI descriptions, but it works best when you use metadata to add extensive documentation to your APIs. In section 11.5 you’ll learn how to go one step further by adding summaries and descriptions to your endpoints.

我认为客户端生成是 OpenAPI 描述的杀手级应用程序,但当您使用元数据向 API 添加大量文档时,它的效果最佳。在 Section 11.5 中,您将学习如何通过向终端节点添加摘要和描述来更进一步。

11.5 Adding descriptions and summaries to your endpoints

11.5 向终端节点 添加描述和摘要

In this section you’ll learn how to add extra descriptions and summaries to your OpenAPI description document. Tools such as Swagger UI and NSwag use these extra descriptions and summaries to provide a better developer experience working with your API. You’ll also learn about alternative ways to add metadata to your minimal API endpoints.

在本节中,您将学习如何向 OpenAPI 描述文档添加额外的描述和摘要。Swagger UI 和 NSwag 等工具使用这些额外的描述和摘要来提供更好的开发人员使用 API 的体验。您还将了解将元数据添加到最小 API 终端节点的替代方法。

11.5.1 Using fluent methods to add descriptions

11.5.1 使用 Fluent 方法添加描述

Whilst working with your minimal API endpoints and calling methods such as WithName() and WithTags(), you may have noticed the methods WithSummary() and WithDescription(). These methods add metadata to your endpoint in exactly the same way as the other With* methods, but unfortunately, they don’t update your OpenAPI description without some extra changes.

在使用最小 API 端点并调用 WithName() 和 WithTags() 等方法时,您可能已经注意到了 WithSummary() 和 WithDescription() 方法。这些方法以与其他 With* 方法完全相同的方式将元数据添加到您的终端节点,但遗憾的是,如果不进行一些额外更改,它们不会更新您的 OpenAPI 描述。

To make use of the summary and description metadata, you must add an extra NuGet package, Microsoft.AspNetCore.OpenApi, and call WithOpenApi() on your endpoint. This method ensures that the summary and description metadata are added correctly to the OpenAPI description when Swashbuckle generates the document. Add this package via the NuGet package manager or the .NET CLI by calling

若要使用摘要和说明元数据,必须添加额外的 NuGet 包 Microsoft.AspNetCore.OpenApi,并在终结点上调用 WithOpenApi()。此方法可确保在 Swashbuckle 生成文档时将摘要和描述元数据正确添加到 OpenAPI 描述中。通过 NuGet 包管理器或 .NET CLI 添加此包

dotnet add package Microsoft.AspNetCore.OpenApi

from the project folder. Then update your endpoints to add summaries and/or descriptions, making sure to call WithOpenApi(), as shown in the following listing.

从项目文件夹中。然后更新您的终端节点以添加摘要和/或描述,确保调用 WithOpenApi(),如下面的清单所示。

Listing 11.9 Adding summaries and descriptions to endpoints using WithOpenApi()
列表 11.9 使用 WithOpenApi() 向端点添加摘要和描述

using System.Collections.Concurrent;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

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

WebApplication app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();

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

app.MapGet("/fruit/{id}", (string id) =>
    _fruit.TryGetValue(id, out var fruit)
        ? TypedResults.Ok(fruit)
        : Results.Problem(statusCode: 404))
    .WithName("GetFruit")
    .WithTags("fruit")
    .Produces<Fruit>()
    .ProducesProblem(404)
    .WithSummary("Fetches a fruit")    ❶
    .WithDescription("Fetches a fruit by id, or returns 404" +    ❷
      " if no fruit with the ID exists")    ❷
    .WithOpenApi();    ❸

app.Run();
record Fruit(string Name, int Stock);

❶ Adds a summary to the endpoint
向端点添加摘要
❷ Adds a description to the endpoint
向端点添加描述
❸ Exposes the metadata added by summary and description to the OpenAPI description
将 summary 和 description 添加的元数据公开到 OpenAPI 描述

With these changes, Swagger UI reflects the extra metadata, as shown in figure 11.10. NSwag also uses the summary as a documentation comment when it generates the endpoints on the client. You can see in figure 11.10, however, that one piece of documentation is missing: a description of the parameter id.
通过这些更改,Swagger UI 会反映额外的元数据,如图 11.10 所示。NSwag 在客户端上生成端点时,还会将摘要用作文档注释。但是,您可以在图 11.10 中看到缺少一条文档:参数 id 的描述。

alt text

Figure 11.10 The summary and description metadata displayed in the Swagger UI. Note that no description is displayed for the id parameter.
图 11.10 Swagger UI 中显示的摘要和描述元数据。请注意,不显示 id 参数的描述。

Unfortunately, you don’t have a particularly elegant way to add documentation for your parameters. The suggested approach is to use an overload of the WithOpenApi() method, which takes a lambda method where you can add a description for the parameter:
遗憾的是,您没有一种特别优雅的方法来为您的参数添加文档。建议的方法是使用 WithOpenApi() 方法的重载,该方法采用 lambda 方法,您可以在其中添加参数的描述:

.WithOpenApi(o =>
{
    o.Parameters[0].Description = "The id of the fruit to fetch";
    o.Summary = "Fetches a fruit";
    return o;
});

This example shows that you can use the WithOpenApi() method to set any of the OpenAPI metadata for the endpoint, so you can use this single method to set (for example) the summary and tags instead of using the dedicated WithSummary() or WithTags() method.

此示例显示,您可以使用 WithOpenApi() 方法为终端节点设置任何 OpenAPI 元数据,因此您可以使用此单一方法来设置(例如)摘要和标签,而不是使用专用的 WithSummary() 或 WithTags() 方法。

Adding all this metadata undoubtedly documents your API in more detail and makes your generated code easier to understand. But if you’re anything like me, the sheer number of methods you have to call makes it hard to see where your endpoint ends and the metadata begins! In section 11.5.2 we’ll look at an alternative approach that involves using attributes.

添加所有这些元数据无疑可以更详细地记录您的 API,并使您生成的代码更易于理解。但是,如果您和我一样,您必须调用的方法数量之多使得很难看到您的终端节点从何处结束,元数据从何处开始!在 Section 11.5.2 中,我们将介绍一种涉及使用 attributes 的替代方法。

11.5.2 Using attributes to add metadata

11.5.2 使用属性添加元数据

I’m a fan of fluent interfaces in many cases, as I feel that they make code easier to understand. But the endpoint metadata extensions, such as those shown in listing 11.9, go to extremes. It’s hard to understand what the endpoint is doing with all the noise from the metadata methods! Ever since version 1.0, C# has had a canonical way to add metadata to code—attributes—and you can replace your endpoint extension methods with dedicated attributes if you prefer.

在许多情况下,我是 Fluent 接口的粉丝,因为我觉得它们使代码更容易理解。但是端点元数据扩展(如清单 11.9 中所示的扩展)走向了极端。很难理解终端节点对元数据方法的所有干扰做了什么!从 1.0 版开始,C# 就有一种将元数据添加到代码中的规范方法 — 属性 — 如果您愿意,可以将端点扩展方法替换为专用属性。

Almost all the extension methods that you add to your endpoint have an equivalent attribute you can use instead. These attributes should be applied directly to the handler method (the lambda function, if that’s what you’re using). Listing 11.10 shows the equivalent of listing 11.9, using attributes instead of fluent methods where possible. The WithOpenApi() method is the only call that can’t be replaced; it must be included so that Swashbuckle reads the OpenAPI metadata correctly.

您添加到终端节点的几乎所有扩展方法都具有您可以改用的等效属性。这些属性应直接应用于处理程序方法(lambda 函数,如果您使用的是 lambda 函数)。清单 11.10 显示了与清单 11.9 等效的清单,尽可能使用属性而不是连贯方法。WithOpenApi() 方法是唯一无法替换的调用;必须包含它,以便 Swashbuckle 正确读取 OpenAPI 元数据。

Listing 11.10 Using attributes to describe your API
清单 11.10 使用属性来描述 API

using System.Collections.Concurrent;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

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

WebApplication app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();

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

app.MapGet("/fruit/{id}",
    [EndpointName("GetFruit")]  ❶
    [EndpointSummary("Fetches a fruit")]  ❶
    [EndpointDescription("Fetches a fruit by id, or returns 404" + ❶
        " if no fruit with the ID exists")]  ❶
    [ProducesResponseType(typeof(Fruit), 200)]  ❶
    [ProducesResponseType(typeof(HttpValidationProblemDetails), 404, ❶
        "application/problem+json")]  ❶
    [Tags("fruit")]  ❶
    (string id) =>
        _fruit.TryGetValue(id, out var fruit)
            ? TypedResults.Ok(fruit)
            : Results.Problem(statusCode: 404))
    .WithOpenApi(o =>
    {
        o.Parameters[0].Description = "The id of the fruit to fetch";
        return o;
    });

app.Run();
record Fruit(string Name, int Stock);

❶ You can use attributes instead of fluent method calls.
您可以使用属性而不是 Fluent 方法调用。

Whether you think listing 11.10 is better than listing 11.9 is largely a matter of taste, but the reality is that neither is particularly elegant. In both cases the metadata significantly obscures the intent of the API, so it’s important to consider which metadata is worth adding and which is unnecessary noise. That balance may shift depending on who your audience is (internal or external customers), how mature your API is, and how much you can extract to helper functions.

你是否认为列出 11.10 比列出 11.9 更好,这在很大程度上是一个品味问题,但现实是两者都不是特别优雅。在这两种情况下,元数据都会严重掩盖 API 的意图,因此重要的是要考虑哪些元数据值得添加,哪些是不必要的干扰。这种平衡可能会根据您的受众群体(内部或外部客户)、API 的成熟度以及可以提取到帮助程序函数的数量。

11.5.3 Using XML documentation comments to add metadata

11.5.3 使用 XML 文档注释添加元数据

One understandable complaint about both the attribute and method approaches for attaching OpenAPI metadata is that the summary and parameter descriptions are divorced from the endpoint handler to which they apply. In this section you’ll see how an alternative approach that uses Extensible Markup Language (XML) documentation comments.

关于附加 OpenAPI 元数据的属性和方法方法的一个可以理解的抱怨是,摘要和参数描述与它们所应用的端点处理程序分离。在本节中,您将了解如何使用可扩展标记语言 (XML) 文档注释的替代方法。

Every C# developer user will be used to the handy descriptions about methods and parameters you get in your IDE from IntelliSense. You can add these descriptions to your own methods by using XML documentation comments, for example:

每个 C# 开发人员用户都将习惯于从 IntelliSense 获得的有关 IDE 中的方法和参数的便捷说明。您可以使用 XML 文档注释将这些描述添加到您自己的方法中,例如:

/// <summary>
/// Adds one to the provided value and returns it
/// </summary>
/// <param name="value">The value to increment</param>
public int Increment(int value) => value + 1;

In your IDE—whether that’s Visual Studio, JetBrains Rider, or Visual Studio Code—this description appears when you try to invoke the method. Wouldn’t it be nice to use the same syntax to define the summary and parameter descriptions for our OpenAPI endpoints? Well, the good news is that we can!

在您的 IDE 中(无论是 Visual Studio、JetBrains Rider 还是 Visual Studio Code),当您尝试调用该方法时,会显示此描述。使用相同的语法来定义 OpenAPI 端点的摘要和参数描述不是很好吗?好吧,好消息是我们可以!

Warning The use of XML documentation comments is only partially supported in .NET 7. These comments work only when you have static or instance method endpoint handlers, not lambda methods or local functions. You can find the issue tracking full support for XML comments at https://github.com/dotnet/aspnetcore/issues/39927.
警告 .NET 7 仅部分支持使用 XML 文档注释。这些注释仅在您具有静态或实例方法终端节点处理程序,而不是 lambda 方法或本地函数时有效。您可以找到问题在 https://github.com/dotnet/aspnetcore/issues/39927 跟踪对 XML 注释的完全支持。

Swashbuckle can use the XML comments you add to your endpoint handlers as the descriptions for your OpenAPI description. When enabled, the .NET software development kit (SDK) generates an XML file containing all your documentation comments. Swashbuckle can read this file on startup and use it to generate the OpenAPI descriptions, as shown in figure 11.11.

Swashbuckle 可以使用您添加到终端节点处理程序的 XML 注释作为 OpenAPI 描述的描述。启用后,.NET SDK 会生成一个包含所有文档注释的 XML 文件。Swashbuckle 可以在启动时读取此文件并使用它来生成 OpenAPI 描述,如图 11.11 所示。

alt text

Figure 11.11 You can configure a .NET application to export documentation comments to a dedicated XML file when it builds. Swashbuckle reads this documentation file at runtime, combining it with the attribute and fluent method metadata for an endpoint to generate the final OpenAPI description.
图 11.11 您可以配置 .NET 应用程序以将文档注释导出到专用 XML 文件当它构建时。Swashbuckle 在运行时读取此文档文件,将其与终端节点的属性和 Fluent 方法元数据相结合,以生成最终的 OpenAPI 描述。

To enable XML documentation comment extraction for your OpenAPI description document you must do three things:
要为您的 OpenAPI 描述文档启用 XML 文档注释提取,您必须执行以下三项作:

  1. Enable documentation generation for your project. Add the <GenerateDocumentationFile> inside a <PropertyGroup> in your csproj file, and set it to true:
    为您的项目启用文档生成。在 <GenerateDocumentationFile> 中添加<PropertyGroup>,并将其设置为

true:

<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
  1. Configure Swashbuckle to read the generated XML document in SwaggerGen():
    配置 Swashbuckle 以读取 SwaggerGen() 中生成的 XML 文档:
builder.Services.AddSwaggerGen(opts =>
{
    var file = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    opts.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, file));
});

3.Use a static or instance method handler and add the XML comments, as shown in the following listing.
使用静态或实例方法处理程序并添加 XML 注释,如下面的清单所示。

Listing 11.11 Adding documentation comments to an endpoint handler
Listing 11.11 向端点处理程序添加文档注释

using Microsoft.AspNetCore.Mvc;
using System.Collections.Concurrent;
using System.Reflection;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(opts =>  ❶
{
    var file = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    opts.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, file));
});

WebApplication app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();

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

var handler = new FruitHandler(fruit);  ❷
app.MapGet("/fruit/{id}", handler.GetFruit)  ❷
    .WithName("GetFruit");  ❸

app.Run();
record Fruit(string Name, int Stock);

internal class FruitHandler
{
    private readonly ConcurrentDictionary<string, Fruit> _fruit;
    public FruitHandler(ConcurrentDictionary<string, Fruit> fruit)
    {
        _fruit = fruit;
    }

    /// <summary>    ❹
    /// Fetches a fruit by id, or returns 404 if it does not exist   ❹
    /// </summary>    ❹
    /// <param name="id" >The ID of the fruit to fetch</param>    ❹
    /// <response code="200">Returns the fruit if it exists</response>  ❹
    /// <response code="404">If the fruit doesn't exist</response>   ❹
    [ProducesResponseType(typeof(Fruit), 200)]    ❺
    [ProducesResponseType(typeof(HttpValidationProblemDetails),   ❺
        404, "application/problem+json")]    ❺
    [Tags("fruit")]    ❺
    public IResult GetFruit(string id)
        => _fruit.TryGetValue(id, out var fruit)
            ? TypedResults.Ok(fruit)
            : Results.Problem(statusCode: 404);
}

❶ Enables XML comments for your OpenAPI descriptions
为您的 OpenAPI 描述启用 XML 注释
❷ You must use static or instance handlers, not lambda methods.
您必须使用静态或实例处理程序,而不是 lambda 方法。
❸ You can add extra metadata by using methods.
您可以使用 methods 添加额外的元数据。
❹ The XML comments are used in the OpenAPI description.
XML 注释用于 OpenAPI 描述。
❺ You can also add extra metadata by using attributes on the handler method.
您还可以通过在 handler 方法上使用 attributes 来添加额外的元数据。

I like the XML comment approach, as it feels much more natural for C# and the comments are often deemphasized in IDEs, reducing visual clutter. You’ll still need to use attributes and/or fluent methods to fully describe your endpoints for OpenAPI, but every little bit helps!

我喜欢 XML 注释方法,因为它对 C# 来说感觉更自然,而且注释在 IDE 中经常被淡化,从而减少了视觉混乱。您仍然需要使用属性和/或 Fluent 方法来完整描述 OpenAPI 的端点,但每一点都有帮助!

As I’ve mentioned several times, how far you go with your OpenAPI description is up to you and how much value you get from it. If you want to use OpenAPI only for local testing with Swagger UI, it doesn’t make sense to clutter your code with lots of extra metadata. In fact, in those cases it would be best to add the swagger services and middleware conditionally only when you’re in development, as in this example:

正如我多次提到的,您对 OpenAPI 描述的了解程度取决于您,以及您从中获得多少价值。如果您只想将 OpenAPI 用于 Swagger UI 的本地测试,那么用大量额外的元数据来杂乱无章的代码是没有意义的。事实上,在这些情况下,最好仅在开发过程中有条件地添加 swagger 服务和中间件,如下例所示:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

if(builder.Environment.IsDevelopment())
{
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
}

WebApplication app = builder.Build();
if(app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.Run();

On the other hand, if you’re generating C# clients for calling your API or exposing your API for public consumption, the more metadata you add, the better! It’s also worth noting that you can add OpenAPI descriptions for all the endpoints in your application, not only your minimal API endpoints. When you create web API controllers in chapter 20, you can include them = too.

另一方面,如果要生成 C# 客户端来调用 API 或公开 API 以供公众使用,则添加的元数据越多越好!还值得注意的是,您可以为应用程序中的所有终端节点添加 OpenAPI 描述,而不仅仅是您的最小 API 终端节点。在第 20 章中创建 Web API 控制器时,也可以包含它们。

11.6 Knowing the limitations of OpenAPI

11.6 了解 OpenAPI 的局限性

In this chapter I’ve described the benefits of OpenAPI, both for simple testing with Swagger UI and for code generation. But like most things in software, it’s not all sweetness and light. OpenAPI and Swagger have limitations that you may run into, particularly as your APIs increase in complexity. In this section I describe some of the challenges to watch out for.

在本章中,我介绍了 OpenAPI 的好处,无论是使用 Swagger UI 进行简单测试还是代码生成。但就像软件中的大多数东西一样,它并不全是甜蜜和轻松。OpenAPI 和 Swagger 存在您可能会遇到的限制,尤其是在 API 复杂性增加时。在本节中,我将介绍一些需要注意的挑战。

11.6.1 Not all APIs can be described by OpenAPI

11.6.1 并非所有 API 都可以由 OpenAPI 描述

The OpenAPI specification is meant to describe your API so that any client knows how to call it. Unfortunately, OpenAPI can’t describe all APIs, which isn’t an accident. The OpenAPI specification says “Not all services can be described by OpenAPI—this specification is not intended to cover every possible style of REST APIs.” So,= the important question is which APIs can’t it describe?

OpenAPI 规范旨在描述您的 API,以便任何客户端都知道如何调用它。不幸的是,OpenAPI 无法描述所有 API,这并非偶然。OpenAPI 规范指出“并非所有服务都可以由 OpenAPI 描述— 此规范并不打算涵盖所有可能的 REST API 样式。那么,重要的问题是哪些 API 不能描述?

One classic example is an API that follows the REST design known as Hypertext As the Engine of Application State (HATEOAS). In this design, each request to an API endpoint includes a list of links describing the actions you can take and the paths to use for each action, enabling clients to discover which actions are available for a given resource. The server can add or remove links dynamically, depending on the state of the resource and which user is making the request.

一个典型的示例是遵循 REST 设计的 API,称为超文本作为应用程序状态引擎 (HATEOAS)。在此设计中,对 API 终端节点的每个请求都包含一个链接列表,这些链接描述了您可以执行的作以及用于每个作的路径,使客户端能够发现哪些作可用于给定资源。服务器可以动态添加或删除链接,具体取决于资源的状态和发出请求的用户。

Tip Martin Fowler has a great description of the REST maturity models, in which HATEOAS is the highest level of maturity, at http://mng.bz/0K1N.
提示 Martin Fowler 对 REST 成熟度模型进行了很好的描述,其中 HATEOAS 是最高级别的成熟度,达到 http://mng.bz/0K1N

HATEOAS generally introduces more complexity than is worthwhile for small projects, but it’s a great way to decouple your client-side applications from your server APIs so that they can evolve separately. This approach can be invaluable when you have large or independent teams. The problem for OpenAPI is that it wasn’t designed for these kinds of dynamic APIs. OpenAPI wants to know up front what the responses are for each of your endpoints, which isn’t information that you can give it if you’re following HATEOAS.

HATEOAS 通常会带来比小型项目所值得的更多的复杂性,但这是将客户端应用程序与服务器 API 分离的好方法,以便它们可以单独发展。当您拥有大型或独立团队时,这种方法可能非常宝贵。OpenAPI 的问题在于它不是为这些类型的动态 API 设计的。OpenAPI 希望提前知道每个终端节点的响应是什么,如果您遵循 HATEOAS,则无法提供这些信息。

In a different scenario, you may have multiple backend APIs, each with its own OpenAPI specification. You expose a single, unified API gateway app, with which all your clients interact. Unfortunately, even though each backend API has an OpenAPI specification, there’s no easy way to combine the APIs into a single unified document that you can expose in your API gateway and which clients can use for testing and code generation.

在不同的情况下,您可能有多个后端 API,每个 API 都有自己的 OpenAPI 规范。您公开一个统一的 API Gateway 应用程序,所有客户端都与该应用程序进行交互。遗憾的是,即使每个后端 API 都有一个 OpenAPI 规范,也没有简单的方法可以将这些 API 组合成一个统一的文档,您可以在 API 网关中公开该文档,客户端可以使用该文档进行测试和代码生成。

Another common problem centers on securing your APIs with authentication and authorization. The OpenAPI specification contains a section about describing your authentication requirements, and Swagger UI supports them. Where things fall down is if you’re using any extensions to the common authentication protocols or advanced features. Although some of these workflows are possible, in some cases Swagger UI simply may not support your workflow, rendering Swagger UI unusable.

另一个常见问题集中在使用身份验证和授权保护您的 API 上。OpenAPI 规范包含有关描述身份验证的部分要求,并且 Swagger UI 支持它们。出现问题的地方是如果您使用了常见身份验证协议或高级功能的任何扩展。尽管其中一些工作流程是可能的,但在某些情况下,Swagger UI 可能根本不支持您的工作流程,从而导致 Swagger UI 不可用。

11.6.2 Generated code is opinionated

11.6.2 生成的代码是固执己见的

At the end of section 11.4 I said that code generation is the killer feature for Open API documents, and in many cases it is. That statement, however, assumes that you like the generated code. If the tooling you use—whether that’s NSwag or some other code generator—doesn’t generate the code you want, you may find yourself spending a lot of effort customizing and tweaking the output. At some point and for some APIs, it may be simpler and easier to write your own client!

在 11.4 节的结尾,我说过代码生成是 Open API 文档的杀手级功能,在许多情况下确实如此。但是,该语句假定您喜欢生成的代码。如果您使用的工具(无论是 NSwag 还是其他代码生成器)没有生成您想要的代码,您可能会发现自己花费了大量精力来自定义和调整输出。在某些时候,对于某些 API,编写自己的客户端可能会更简单、更容易!

Note A classic complaint (with which I sympathize) is the use of exceptions for process flow whenever an error or unexpected status code is returned. Not all errors are exceptional, throwing exceptions is relatively expensive computationally, and it often means that every call made with a client needs custom exception handling. This design sometimes makes code generation seem more like a burden than a benefit.
注意 一个典型的抱怨(我对此表示同情)是每当返回错误或意外状态代码时,都会对流程使用异常。并非所有错误都是异常的,引发异常的计算成本相对较高,这通常意味着使用客户端进行的每个调用都需要自定义异常处理。这种设计有时使代码生成看起来更像是一种负担,而不是一种好处。

Another, subtler issue arises when you use code generation with two separate but related OpenAPI documents, such as a products API and a cart API. If you use the techniques in this chapter to generate the clients and then try to follow this simple sequence, you’ll run into trouble:

当您将代码生成与两个独立但相关的 OpenAPI 文档(例如产品 API 和购物车 API)一起使用时,会出现另一个更微妙的问题。如果使用本章中的技术生成客户端,然后尝试遵循以下简单顺序,则会遇到麻烦:

  1. Retrieve a Product instance from the products API by using ProductsClient.Get()
    使用 ProductsClient.Get() 从 products API 检索 Product 实例。

  2. Send the retrieved Product to the cart API by using CartClient.Add(Product)
    使用 将检索到的 Product 发送到购物车 API CartClient.Add(Product) 的

Unfortunately, the generated Product type retrieved from the products API is a different type from the generated Product type that the CartClient requires, so this code won’t compile. Even if the type has the same properties and is serialized to the same JSON when it’s sent to the client, C# considers the objects to be different types and won’t let them swap places. You must copy the values manually from the first Product instance to a new instance. These complaints are mostly small niggles and paper cuts, but they can add up when you run into them often.

遗憾的是,从 products API 检索到的生成的 Product 类型与 CartClient 需要的生成的 Product 类型不同,因此此代码无法编译。即使类型具有相同的属性,并且在发送到客户端时序列化为相同的 JSON,C# 也会将对象视为不同的类型,并且不允许它们交换位置。您必须手动将值从第一个 Product 实例复制到新实例。这些抱怨大多是小问题和剪纸,但当您经常遇到它们时,它们会累积起来。

11.6.3 Tooling often lags the specification

11.6.3 工具经常滞后于规格

Another factor to consider is the many groups that are involved in generating an OpenAPI document and generating a client:

另一个需要考虑的因素是生成 OpenAPI 文档和生成客户端所涉及的许多组:

  • The Open API specification is a community-driven project written by the OpenAPI Initiative group.
    Open API 规范是由 OpenAPI Initiative 小组编写的社区驱动型项目。

  • Microsoft provides the tooling built into ASP.NET Core for supplying the metadata about your API endpoints.
    Microsoft 提供了 ASP.NET Core 中内置的工具,用于提供有关 API 终端节点的元数据。

  • Swashbuckle is an open-source project that uses the ASP.NET Core metadata to generate an OpenAPI-compatible document.
    Swashbuckle 是一个开源项目,它使用 ASP.NET Core 元数据生成与 OpenAPI 兼容的文档。

  • NSwag is an open-source project that takes an OpenAPI-compatible document and generates clients (and has many other features!).
    NSwag 是一个开源项目,它采用与 OpenAPI 兼容的文档并生成客户端(并且具有许多其他功能!)

  • Swagger UI is an open-source project for interacting with APIs based on the OpenAPI document.
    Swagger UI 是一个开源项目,用于与基于 OpenAPI 文档的 API 交互。

Some of these projects have direct dependencies on others (everything depends on the OpenAPI specification, for example), but they may evolve at difference paces. If Swashbuckle doesn’t support some new feature of the OpenAPI specification, it won’t appear in your documents, and NSwag won’t be able to use it.

其中一些项目直接依赖于其他项目(例如,一切都取决于 OpenAPI 规范),但它们的发展速度可能不同。如果 Swashbuckle 不支持 OpenAPI 规范的某些新功能,它不会出现在你的文档中,NSwag 也无法使用它。

Most of the tools provide ways to override the behavior to work around these rough edges, but the reality is that if you’re using newer or less popular features, you may have more difficulty persuading all the tools in your tool chain to play together nicely.

大多数工具都提供了覆盖行为的方法,以解决这些粗糙的边缘,但现实情况是,如果您使用的是较新或不太流行的功能,则说服工具链中的所有工具很好地协同工作可能会更加困难。

Overall, the important thing to remember is that OpenAPI documents may work well if you have simple requirements or want to use Swagger UI only for testing. In these cases, there’s little investment required to add OpenAPI support, and it can improve your workflow, so you might find it worthwhile to try.

总的来说,要记住的重要一点是,如果您有简单的要求或只想使用 Swagger UI 进行测试,OpenAPI 文档可能会很好地工作。在这些情况下,添加 OpenAPI 支持只需要很少的投资,而且它可以改善您的工作流程,因此您可能会发现值得尝试。

If you have more complex requirements, are creating an API that OpenAPI can’t easily describe or aren’t a fan of the code generation, it may not be worth your time to invest heavily in OpenAPI for your documents.

如果您有更复杂的要求,正在创建 OpenAPI 无法轻松描述的 API,或者不喜欢代码生成,那么可能不值得您花时间为您的文档投入大量 OpenAPI。

Tip If you’re a fan of code generation but prefer more of a remote procedure call (RPC) style of programming, it’s worthwhile to look at gRPC. Code generation for gRPC is robust, supported across multiple languages, and has great support in .NET. You can read more in the documentation at https://learn.microsoft.com/aspnet/core/grpc.
提示 如果您喜欢代码生成,但更喜欢远程过程调用 (RPC) 风格的编程,那么值得考虑一下 gRPC。gRPC 的代码生成非常健壮,支持多种语言,并您可以在 https://learn.microsoft.com/aspnet/core/grpc 上的文档中阅读更多内容。

In chapter 12 we’ll take a brief look at the new object-relational mapper that fits well with ASP.NET Core: Entity Framework Core. You’ll get only a taste of it in this book, but you’ll learn how to load and save data, build a database from your code, and migrate the database as your code evolves.

在第 12 章中,我们将简要介绍非常适合 ASP.NET Core 的新对象关系映射器:Entity Framework Core。在本书中,您只会对它有所了解,但您将学习如何加载和保存数据、从代码构建数据库,以及随着代码的发展迁移数据库。

11.7 Summary

11.7 总结

OpenAPI is a specification for describing HTTP APIs in a machine-readable format, as a JSON document. You can use this document to drive other tooling, such as code generators or API testers.
OpenAPI 是一种规范,用于以机器可读格式(如 JSON 文档)描述 HTTP API。您可以使用本文档来驱动其他工具,例如代码生成器或 API 测试器。

You can add OpenAPI document generation to an ASP.NET Core app by using the NSwag or Swashbuckle NuGet package. These packages work with ASP.NET Core services to read metadata about all the endpoints in your application to build an OpenAPI document.
您可以使用 NSwag 或 Swashbuckle NuGet 包将 OpenAPI 文档生成添加到 ASP.NET Core 应用程序。这些软件包与 ASP.NET Core 服务配合使用,以读取有关应用程序中所有终端节点的元数据,以构建 OpenAPI 文档。

The Swashbuckle Swagger middleware exposes the OpenAPI Document for your application at the path /swagger/v1/swagger.json by default. Exposing the document in this way makes it easy for other tools to understand the endpoints in your application.
Swashbuckle Swagger 中间件在路径中公开应用程序的 OpenAPI 文档/swagger/v1/swagger.json 中。暴露该文档使其他工具能够轻松理解应用程序中的端点。

You can explore and test your API by using Swagger UI. The Swashbuckle Swagger UI middleware exposes the UI at the path /swagger by default. You can use Swagger UI to explore your API, send test requests to your endpoints, and check how well your API is documented.
您可以使用 Swagger UI 探索和测试您的 API。默认情况下,Swashbuckle Swagger UI 中间件在路径 /swagger 处公开 UI。您可以使用 Swagger UI 来浏览 API,向终端节点发送测试请求,并检查 API 的文档记录情况。

You can customize the OpenAPI description of your endpoints by adding metadata. You can provide tags, for example, by calling WithTags() on an endpoint and specify that an endpoint returns a type T with a 201 status code using Produces<T>(201). Adding metadata improves your API OpenAPI description, which in turn improves tooling such as Swagger UI.
您可以通过添加元数据来自定义终端节点的 OpenAPI 描述。例如,您可以通过在终端节点上调用 WithTags() 来提供标签,并使用 Produces<T>(201) 指定终端节点返回状态代码为 201 的类型 T。添加元数据可以改进 API 的 OpenAPI 描述,从而改进 Swagger UI 等工具。

You can use NSwag to generate a C# client from an OpenAPI description. This approach takes care of using the correct paths to call the API, substituting parameters in the path, and serializing and deserializing requests to the API, removing much of the boilerplate associated with interacting with an API.
您可以使用 NSwag 从 OpenAPI 描述生成 C# 客户端。此方法负责使用正确的路径调用 API、替换路径中的参数以及序列化和反序列化对 API 的请求,从而删除与与 API 交互相关的大部分样板。

You can add code generation to your project by using Visual Studio or the .NET API tool or by making manual changes to your project. Visual Studio and the .NET tool automate downloading the OpenAPI description to your local project and adding the necessary NuGet packages. You should update the NuGet packages to the latest versions to ensure that you have the latest bug or security fixes.
您可以使用 Visual Studio 或 .NET API 工具,或者通过手动更改项目来向项目添加代码生成。Visual Studio 和 .NET 工具会自动将 OpenAPI 描述下载到本地项目,并添加必要的 NuGet 包。应将 NuGet 包更新到最新版本,以确保您拥有最新的 bug 或安全修复。

NSwag automatically generates a C# method name on the main client class for each endpoint in the OpenAPI description. If the endpoint’s OperationID is missing, NSwag generates a name, which may not be optimal. You can specify the OperationID to use for an endpoint in your OpenAPI description by calling WithName() on the endpoint.
NSwag 在 OpenAPI 描述中的每个端点的主客户端类上自动生成一个 C# 方法名称。如果缺少终结点的 OperationID,NSwag 会生成一个名称,这可能不是最佳名称。您可以通过在终端节点上调用 WithName() 来指定 OpenAPI 描述中要用于终端节点的 OperationID。

You can customize the client NSwag generates by adding an <Options> element inside the <OpenApiReference> in your .csproj file. These options are specified as command-line switches such as /JsonLibrary:SystemTextJson. You can change many things about the generated code with these switches, such as the serialization library to use and whether to generate an interface for the client.
您可以通过在<OpenApiReference> 在 .csproj 文件中。这些选项指定为命令行开关,例如 /JsonLibrary:SystemTextJson。您可以使用这些开关更改有关生成代码的许多内容,例如要使用的序列化库以及是否为 Client 端生成接口。

If the OpenAPI description for a remote API changes, you need to download the document to your project again for the generated client to reflect these changes. If you originally added the OpenAPI reference by using Visual Studio, you should use Visual Studio to refresh the document, and the same applies to the .NET API tool. NSwag automatically updates the generated code when the downloaded OpenAPI document changes.
如果远程 API 的 OpenAPI 描述发生更改,您需要再次将文档下载到您的项目中,以便生成的客户端反映这些更改。如果最初使用 Visual Studio 添加了 OpenAPI 引用,则应使用 Visual Studio 刷新文档,这同样适用于 .NET API 工具。当下载的 OpenAPI 文档发生更改时,NSwag 会自动更新生成的代码。

You can add an OpenAPI summary and description to an endpoint by installing the Microsoft.AspNetCore.OpenApi package, calling WithOpenApi() on the endpoint, and adding calls to WithSummary() or WithDescription(). This metadata is shown in Swagger UI, and NSwag uses the summary to generate documentation comments in the C# client.
您可以通过安装 Microsoft.AspNetCore.OpenApi 包,在端点上调用 WithOpenApi(),然后添加对 WithSummary() 或 WithDescription() 的调用,将 OpenAPI 摘要和说明添加到端点。此元数据显示在 Swagger UI 中,NSwag 使用摘要在 C# 客户端中生成文档注释。

You can use attributes instead of fluent methods to add OpenAPI metadata if you prefer. This approach sometimes helps improve readability of your endpoints. You must still call WithOpenApi() on the endpoint to read the metadata attributes.
如果需要,可以使用属性而不是 Fluent 方法来添加 OpenAPI 元数据。此方法有时有助于提高终端节点的可读性。您仍必须在终端节点上调用 WithOpenApi() 才能读取元数据属性。

You can use XML documentation comments to document your OpenAPIs to reduce the clutter of extra method calls and attributes. To use this approach, you must enable documentation generation for the project, configure Swashbuckle to read the XML documentation file on startup, and use static or instance handler methods instead of lambda methods.
您可以使用 XML 文档注释来记录 OpenAPI,以减少额外方法调用和属性的混乱。要使用此方法,您必须为项目启用文档生成,将 Swashbuckle 配置为在启动时读取 XML 文档文件,并使用静态或实例处理程序方法而不是 lambda 方法。

Not all APIs can be described by the OpenAPI specification. Some styles, such as HATEOAS, are naturally dynamic and don’t lend themselves to the static design of OpenAPI. You may also have difficulty with complex authentication requirements, as well as combining OpenAPI documents. In these cases, you may find that OpenAPI brings little value to your application.
并非所有 API 都可以用 OpenAPI 规范来描述。某些样式(如 HATEOAS)自然是动态的,并不适合 OpenAPI 的静态设计。您可能还难以满足复杂的身份验证要求以及组合 OpenAPI 文档。在这些情况下,您可能会发现 OpenAPI 为您的应用程序带来什么价值。

ASP.NET Core in Action 10 Configuring an ASP.NET Core application

10 Configuring an ASP.NET Core application
10 配置 ASP.NET Core 应用程序

This chapter covers

本章涵盖

  • Loading settings from multiple configuration providers
    从多个配置提供程序加载设置

  • Storing sensitive settings safely
    安全地存储敏感设置

  • Using strongly typed settings objects
    使用强类型设置对象

  • Using different settings in different hosting environments
    在不同的托管环境中使用不同的设置

In part 1 of this book, you learned the basics of getting an ASP.NET Core app up and running, and how to use minimal API endpoints to create an HTTP API. When you start building real applications, you’ll quickly find that you want to tweak various settings at deploy time without necessarily having to recompile your application. This chapter looks at how you can achieve this task in ASP.NET Core by using configuration.

在本书的第 1 部分中,您学习了启动和运行 ASP.NET Core 应用程序的基础知识,以及如何使用最少的 API 端点来创建 HTTP API。当您开始构建真正的应用程序时,您很快就会发现您希望在部署时调整各种设置,而不必重新编译您的应用程序。本章介绍如何使用 configuration 在 ASP.NET Core 中完成此任务。

I know. Configuration sounds boring, right? But I have to confess that the configuration model is one of my favorite parts of ASP.NET Core; it’s so easy to use and so much more elegant than some approaches in old versions of .NET Framework. In section 10.2 you’ll learn how to load values from a plethora of sources—JavaScript Object Notation (JSON) files, environment variables, and command-line arguments—and combine them into a unified configuration object.

我知道。配置听起来很无聊,对吧?但我不得不承认,配置模型是 ASP.NET Core 中我最喜欢的部分之一;它非常易于使用,并且比旧版本的 .NET Framework 中的某些方法要优雅得多。在第 10.2 节中,您将学习如何从大量来源(JavaScript 对象表示法 (JSON) 文件、环境变量和命令行参数)加载值,并将它们组合到一个统一的配置对象中。

On top of that, ASP.NET Core makes it easy to bind this configuration to strongly typed options objects—simple plain old CLR object (POCO) classes that are populated from the configuration object, which you can inject into your services, as you’ll see in section 10.3. Binding to strongly typed options objects lets you nicely encapsulate settings for different features in your app.

最重要的是,ASP.NET Core 可以轻松地将此配置绑定到强类型选项对象 — 从配置对象填充的简单普通旧 CLR 对象 (POCO) 类,您可以将其注入到您的服务中,如第 10.3 节所示。绑定到强类型选项对象可以让您很好地封装应用程序中不同功能的设置。

In the final section of this chapter, you’ll learn about the ASP.NET Core hosting environments. You often want your app to run differently in different situations, such as running it on your developer machine compared with deploying it to a production server. These situations are known as environments. When the app knows the environment in which it’s running, it can load a different configuration and vary its behavior accordingly.

在本章的最后一节中,您将了解 ASP.NET Core 托管环境。您通常希望您的应用程序在不同情况下以不同的方式运行,例如在开发人员计算机上运行它与将其部署到生产服务器。这些情况称为环境。当应用程序知道它运行的环境时,它可以加载不同的配置并相应地改变其行为。

Before we get to that topic, let’s cover the basics. What is configuration, why do we need it, and how does ASP.NET Core handle these requirements?

在我们进入该主题之前,让我们先了解一下基础知识。什么是配置,我们为什么需要它,以及 ASP.NET Core 如何处理这些要求?

10.1 Introducing the ASP.NET Core configuration model

10.1 ASP.NET Core 配置模型简介

In this section I provide a brief description of configuration and what you can use it for in ASP.NET Core applications. Configuration is the set of external parameters provided to an application that controls the application’s behavior in some way. It typically consists of a mixture of settings and secrets that the application loads at runtime.

在本节中,我将简要介绍配置以及您可以在 ASP.NET Core 应用程序中使用它的用途。Configuration 是提供给应用程序的一组外部参数,它以某种方式控制应用程序的行为。它通常由应用程序在运行时加载的设置和密钥的混合组成。

Definition A setting is any value that changes the behavior of your application. A secret is a special type of setting that contains sensitive data, such as a password, an API key for a third-party service, or a connection string.
定义 设置是更改应用程序行为的任何值。密钥是一种特殊类型的设置,其中包含敏感数据,例如密码、第三方服务的 API 密钥或连接字符串。

The obvious things to consider before we get started are why we need app configuration and what sort of things we need to configure. Normally, you move anything that you can consider to be a setting or a secret out of your application code. That way, you can change these values at deploy time easily without having to recompile your application.

在开始之前,要考虑的明显事项是为什么需要应用程序配置以及需要配置哪些类型的内容。通常,您将任何可以视为设置或密钥的内容从应用程序代码中移出。这样,您就可以在部署时轻松更改这些值,而无需重新编译应用程序。

You might have an application that shows the locations of your bricks-and-mortar stores. You could have a setting for the connection string to the database in which you store the details on the stores, but also settings such as the default location to display on a map, the default zoom level to use, and the API key for accessing the Google Maps API (figure 10.1). Storing these settings and secrets outside your compiled code is good practice, as it makes it easy to tweak them without having to recompile your code.

您可能有一个显示实体店位置的应用程序。你可以设置存储商店详细信息的数据库的连接字符串,也可以设置地图上显示的默认位置、要使用的默认缩放级别以及用于访问 Google Maps API 的 API 密钥等设置(图 10.1)。将这些设置和密钥存储在已编译代码之外是一种很好的做法,因为它可以轻松调整它们,而无需重新编译代码。

alt text

Figure 10.1 You can store the default map location, zoom level, and mapping API Key in configuration and load them at runtime. It’s important to keep secrets such as API keys in configuration out of your code.
图 10.1 您可以在配置中存储默认地图位置、缩放级别和地图 API Key,并在运行时加载它们。请务必将 configuration 中的 API 密钥等机密信息保留在代码之外。

There’s also a security aspect: you don’t want to hardcode secret values such as API keys or passwords into your code, where they could be committed to source control and made publicly available. Even values embedded in your compiled application can be extracted, so it’s best to externalize them whenever possible.

还有一个安全方面:您不希望将 API 密钥或密码等秘密值硬编码到代码中,因为这些值可以提交到源代码管理中并公开可用。甚至可以提取嵌入在已编译应用程序中的值,因此最好尽可能将它们外部化。

Virtually every web framework provides a mechanism for loading configuration, and the old .NET Framework version of ASP.NET was no different. It used the element in a web.config file to store key-value configuration pairs. At runtime you’d use the static (wince) ConfigurationManager to load the value for a given key from the file. You could do more advanced things using custom configuration sections, but doing more advanced things using custom configuration sections was painful and so was rarely used, in my experience.

实际上,每个 Web 框架都提供了加载配置的机制,旧版 ASP.NET 也不例外。它使用 web.config 文件中的 元素来存储键值配置对。在运行时,您将使用静态 (wince) ConfigurationManager 从文件中加载给定键的值。您可以使用自定义配置部分执行更高级的作,但根据我的经验,这样做很痛苦,因此很少使用。

ASP.NET Core gives you a totally revamped experience. At the most basic level, you’re still specifying key-value pairs as strings, but instead of getting those values from a single file, now you can load them from multiple sources. You can load values from files, but now they can be in any format you like: JSON, XML, YAML, and so on. Further, you can load values from environment variables, from command-line arguments, from a database, or from a remote service. Or you can create your own custom configuration provider.

ASP.NET Core 为您提供全新的体验。在最基本的层面上,您仍然将键值对指定为字符串,但现在您可以从多个源加载它们,而不是从单个文件中获取这些值。您可以从文件中加载值,但现在它们可以采用您喜欢的任何格式:JSON、XML、YAML 等。此外,还可以从环境变量、命令行参数、数据库或远程服务加载值。或者,您可以创建自己的自定义配置提供程序。

Definition ASP.NET Core uses configuration providers to load key-value pairs from a variety of sources. Applications can use multiple configuration providers.
定义 ASP.NET Core 使用配置提供程序从各种来源加载键值对。应用程序可以使用多个配置提供程序。ASP.NET Core 配置模型还具有覆盖设置的概念。每个配置提供程序都可以定义自己的设置,也可以覆盖先前提供程序的设置。您将在 10.2 节中看到这个非常有用的功能。

The ASP.NET Core configuration model also has the concept of overriding settings. Each configuration provider can define its own settings, or it can overwrite settings from a previous provider. You’ll see this incredibly useful feature in action in section 10.2.

ASP.NET Core 为您提供全新的体验。在最基本的层面上,您仍然将键值对指定为字符串,但现在您可以从多个源加载它们,而不是从单个文件中获取这些值。您可以从文件中加载值,但现在它们可以采用您喜欢的任何格式:JSON、XML、YAML 等。此外,还可以从环境变量、命令行参数、数据库或远程服务加载值。或者,您可以创建自己的自定义配置提供程序。

ASP.NET Core makes it simple to bind these key-value pairs, which are defined as strings, to POCO-setting classes that you define in your code. This model of strongly typed configuration, described in section 10.3, makes it easy to group settings logically around a given feature and lends itself well to unit testing.

ASP.NET Core 使将这些定义为字符串的键值对绑定到您在代码中定义的 POCO 设置类变得简单。这种强类型配置模型(在 Section 10.3 中描述)可以很容易地围绕给定功能对设置进行逻辑分组,并且非常适合 unit testing。

Before we get to strongly typed settings, we’ll look at how you load the settings and secrets for your app, whether they’re stored in JSON files, environment variables, or command-line arguments.

在开始强类型设置之前,我们将了解如何加载应用程序的设置和 Secret,无论它们是存储在 JSON 文件、环境变量还是命令行参数中。

10.2 Building a configuration object for your app

10.2 为您的应用程序构建配置对象

In this section we’ll get into the meat of the configuration system. You’ll learn how to load settings from multiple sources, how they’re stored internally in ASP.NET Core, and how settings can override other values to produce layers of configuration. You’ll also learn how to store secrets securely while ensuring that they’re still available when you run your app.

在本节中,我们将深入介绍配置系统的核心内容。您将学习如何从多个来源加载设置,如何将它们内部存储在 ASP.NET Core 中,以及设置如何覆盖其他值以生成配置层。您还将学习如何安全地存储密钥,同时确保在运行应用程序时它们仍然可用。

ASP.NET Core’s configuration model has been essentially the same since .NET Core 1.0, but in .NET 6, ASP.NET Core introduced the ConfigurationManager class. ConfigurationManager simplifies common patterns for working with configuration by implementing both of the two main configuration-related interfaces: IConfigurationBuilder and IConfigurationRoot.

自 .NET Core 1.0 以来,ASP.NET Core 的配置模型基本相同,但在 .NET 6 中,ASP.NET Core 引入了 ConfigurationManager 类。ConfigurationManager 简化了通过实现两个主要的与配置相关的接口来处理配置:IConfigurationBuilder 和IConfigurationRoot。

Note IConfigurationBuilder describes how to construct the final configuration representation for your app, and IConfigurationRoot holds the configuration values themselves.
注意 IConfigurationBuilder 描述了如何为您的应用程序构建最终的配置表示形式,而 IConfigurationRoot 本身保存了配置值。

You describe your configuration by adding IConfigurationProviders to the ConfigurationManager. Configuration providers describe how to load the key-value pairs from a particular source, such as a JSON file or environment variables (figure 10.2). When you add a provider, the ConfigurationManager queries it and adds all the values returned to the IConfigurationRoot implementation.

您可以通过将 IConfigurationProvider添加到 ConfigurationManager 来描述您的配置。配置提供程序描述了如何从特定源(例如 JSON 文件或环境变量)加载键值对(图 10.2)。当您添加提供程序时,ConfigurationManager 会查询它,并将返回给 IConfigurationRoot 实现的所有值添加。

alt text

Figure 10.2 Using ConfigurationManager to populate IConfiguration. Configuration providers are added to the ConfigurationManager with extension methods. The manager queries the provider and adds all the returned values to the IConfigurationRoot, which implements IConfiguration.
图 10.2 使用 ConfigurationManager 填充 IConfiguration。配置提供程序使用扩展方法添加到 ConfigurationManager。经理查询提供程序,并将所有返回的值添加到实现 IConfiguration 的 IConfigurationRoot。

Note Adding a provider to the ConfigurationManager adds the configuration values to the IConfigurationRoot instance, which implements IConfiguration. You’ll generally work with the IConfiguration interface in your code.
注意 将提供程序添加到 ConfigurationManager 会将配置值添加到实现 IConfiguration 的 IConfigurationRoot 实例。您通常会在代码中使用IConfiguration 接口。

ASP.NET Core ships with configuration providers for loading data from common locations:
ASP.NET Core 附带了用于从常见位置加载数据的配置提供程序:

  • JSON files
    JSON 文件

  • Extensible Markup Language (XML) files
    可扩展标记语言 (XML) 文件

  • Environment variables
    环境变量

  • Command-line arguments
    命令行参数

  • Initialization (INI) files
    初始化 (INI) 文件

If these providers don’t fit your requirements, you can find a host of alternatives on GitHub and NuGet, and it’s not difficult to create your own custom provider. You could use the official Microsoft Azure Key Vault provider NuGet package or the YAML file provider I wrote.

如果这些提供程序不符合您的要求,您可以在 GitHub 和 NuGet 上找到许多替代方案,并且创建自己的自定义提供程序并不难。您可以使用官方的 Microsoft Azure Key Vault 提供程序 NuGet 包或我编写的 YAML 文件提供程序。

Note The Azure Key Vault provider is available on NuGet at http://mng.bz/0KrN, and you can find my YAML provider on GitHub at http://mng.bz/Yqdj.
注意: Azure Key Vault 提供程序在 NuGet 上以 http://mng.bz/ 0KrN 提供,您可以在 GitHub 上找到我的 YAML 提供程序,网址为 http://mng.bz/Yqdj

In many cases, the default providers are sufficient. In particular, most templates start with an appsettings.json file, which contains a variety of settings depending on the template you choose. The following listing shows the default file generated by the ASP.NET Core 7.0 Empty template without authentication.

在许多情况下,默认提供程序就足够了。特别是,大多数模板都以 appsettings.json 文件开头,该文件包含各种设置,具体取决于您选择的模板。以下清单显示了 ASP.NET Core 7.0 Empty 模板生成的默认文件,无需身份验证。

Listing 10.1 Default appsettings.json file created by an ASP.NET Core
清单 10.1 由 ASP.NET Core Empty 模板创建的默认 appsettings.json 文件

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

As you can see, this file contains mostly settings to control logging, but you can add extra configuration for your app here too.

如您所见,此文件主要包含用于控制日志记录的设置,但您也可以在此处为您的应用程序添加额外的配置。

Warning Don’t store sensitive values—such as passwords, API keys, and connection strings—in this file. You’ll see how to store these values securely in section 10.2.3.
警告 不要在此文件中存储敏感值,例如密码、API 密钥和连接字符串。您将在 Section 10.2.3 中看到如何安全地存储这些值。

Adding your own configuration values involves adding a key-value pair to the JSON. It’s a good idea to namespace your settings by creating a base object for related settings, as in the MapSettings object shown in the following listing.

添加您自己的配置值涉及向 JSON 添加键值对。通过为相关设置创建基本对象来命名设置是一个好主意,如下面的清单所示的 MapSettings 对象。

Listing 10.2 Adding configuration values to an appsettings.json file
示例 10.2 向 appsettings.json 文件添加配置值

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    },
    "AllowedHosts": "*",
    "MapSettings": { ❶
        "DefaultZoomLevel": 9, ❷
        "DefaultLocation": { ❸
        "latitude": 50.500, ❸
        "longitude": -4.000 ❸
        }
    }
}

❶ Nest all the configuration under the MapSettings key.
将所有配置嵌套在 MapSettings 键下。

❷ Values can be numbers in the JSON file, but they’ll be converted to strings when they’re read.
值可以是 JSON 文件中的数字,但在读取时它们会转换为字符串。

❸ You can create deeply nested structures to organize your configuration values better.
您可以创建深度嵌套的结构来更好地组织您的配置值。

I’ve nested the new configuration inside the MapSettings parent key to create a section that will be useful later for binding values to a POCO object. I also nested the latitude and longitude keys under the DefaultLocation key. You can create any structure of values you like; the configuration provider will read them fine. Also, you can store the values as any data type—numbers, in this case—but be aware that the provider will read and store them internally as strings.

我已将新配置嵌套在 MapSettings 父键中,以创建一个部分,该部分稍后将用于将值绑定到 POCO 对象。我还将纬度和经度键嵌套在 DefaultLocation 键下。您可以创建任何您喜欢的值结构;配置提供程序将读取它们。此外,您可以将值存储为任何数据类型(在本例中为数字),但请注意,提供程序将在内部将它们读取并存储为字符串。

Tip The configuration keys are not case-sensitive in your app, so bear that fact in mind when loading from providers in which the keys are case-sensitive. If you have a YAML file with keys name and NAME, for example, only one will appear in the final IConfiguration.
提示 配置键在您的应用程序中不区分大小写,因此在从键区分大小写的提供程序加载时,请记住这一事实。例如,如果您有一个具有键 name 和 NAME 的 YAML 文件,则最终的 IConfiguration 中只会显示一个。

Now that you have a configuration file, it’s time for your app to load it into the ConfigurationManager.

现在你有了一个配置文件,是时候让你的应用程序将其加载到 ConfigurationManager 中了。

10.2.1 Adding a configuration provider in Program.cs

10.2.1 在 Program.cs 中添加配置提供程序

As you’ve seen throughout this book, ASP.NET Core (from .NET 6 onward) uses the WebApplicationBuilder class to bootstrap your application. As part of the bootstrap process, WebApplicationBuilder creates a ConfigurationManager instance and exposes it as the property Configuration.

正如您在本书中所看到的,ASP.NET Core(从 .NET 6 开始)使用WebApplicationBuilder 类来引导您的应用程序。作为引导过程的一部分,WebApplicationBuilder 创建一个 ConfigurationManager 实例,并将其公开为属性 Configuration。

Tip You can access the ConfigurationManager directly on WebApplicationBuilder.Configuration and WebApplication.Configuration. Both properties reference the same ConfigurationManager instance.
提示 您可以直接在 WebApplicationBuilder 上访问 ConfigurationManager。Configuration 和 WebApplication.Configuration 的 Configuration。这两个属性引用同一个 ConfigurationManager 实例。

WebApplicationBuilder adds several default configuration providers to the ConfigurationManager, which we’ll look at in more detail throughout this chapter:

WebApplicationBuilder 向 ConfigurationManager 添加了几个默认配置提供程序,我们将在本章中更详细地介绍它们:

  • JSON file provider—Loads settings from an optional JSON file called appsettings.json. It also loads settings from an optional environment-specific JSON file called appsettings.ENVIRONMENT.json. I show how to use environment-specific files in section 10.4.
    JSON 文件提供程序 - 从名为 appsettings.json 的可选 JSON 文件加载设置。它还从名为 appsettings 的可选特定于环境的 JSON 文件加载设置。ENVIRONMENT.json 的。我在 Section 10.4 中展示了如何使用特定于环境的文件。

  • User Secrets—Loads secrets that are stored safely during development.
    User Secrets (用户密钥) - 加载在开发过程中安全存储的密钥。

  • Environment variables—Loads environment variables as configuration variables, which are great for storing secrets in production.
    环境变量 — 将环境变量加载为配置变量,这非常适合在生产中存储密钥。

  • Command-line arguments—Uses values passed as arguments when you run your app.
    命令行参数 - 使用在运行应用程序时作为参数传递的值。

The ConfigurationManager is configured with all these sources automatically, but you can easily add more providers. You can also start from scratch and clear the default providers as shown in the following listing, which completely customizes where configuration is loaded from.

ConfigurationManager 会自动配置所有这些源,但您可以轻松添加更多提供程序。您还可以从头开始并清除默认提供程序,如下面的清单所示,它完全自定义了加载配置的位置。

Listing 10.3 Loading appsettings.json by clearing the configuration sources

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Configuration.Sources.Clear(); ❶
builder.Configuration.AddJsonFile("appsettings.json", optional: true); ❷

WebApplication app = builder.Build();

app.MapGet("/", () => app.Configuration.AsEnumerable()); ❸

app.Run();

❶ Clears the providers configured by default in WebApplicationBuilder
清除 WebApplicationBuilder 中默认配置的提供程序

❷ Adds a JSON configuration provider, providing the name of the configuration file
添加 JSON 配置提供程序,提供配置文件的名称

❸ Returns all the configuration key-value pairs for display purposes
返回所有配置键值对以供显示

This example added a single JSON configuration provider by calling the AddJsonFile() extension method and providing a filename. It also set the value of optional to true, telling the configuration provider to skip files that it can’t find at runtime instead of throwing FileNotFoundException. When the provider is added, the ConfigurationManager requests all the available values from the provider and adds them to the IConfiguration implementation.

此示例通过调用 AddJsonFile() 扩展方法并提供文件名来添加单个 JSON 配置提供程序。它还将 optional 的值设置为 true,告知配置提供程序跳过在运行时找不到的文件,而不是引发 FileNotFoundException。添加提供程序后, ConfigurationManager 从提供程序请求所有可用值,并将它们添加到 IConfiguration 实现中。

ConfigurationBuilder vs. ConfigurationManager
ConfigurationBuilder 与 ConfigurationManager
Before .NET 6 and the introduction of ConfigurationManager, configuration in ASP.NET Core was implemented with ConfigurationBuilder. You’d add configuration providers to the builder type the same way you do with ConfigurationManager, but the configuration values weren’t loaded until you called Build(), which created the final IConfigurationRoot object.
在 .NET 6 和 ConfigurationManager 引入之前,ASP.NET Core 中的配置是使用 ConfigurationBuilder 实现的。您可以像使用 ConfigurationManager 一样将配置提供程序添加到生成器类型中,但在调用 Build() 之前不会加载配置值,这将创建最终的 IConfigurationRoot 对象。
By contrast, in .NET 6 and .NET 7 ConfigurationManager acts as both the builder and the final IConfigurationRoot. When you add a new configuration provider, the configuration values are added to the IConfigurationRoot immediately, without the need to call Build() first.
相比之下,在 .NET 6 和 .NET 7 中,ConfigurationManager 既充当生成器,又充当最终的 IConfigurationRoot。添加新的配置提供程序时,配置值会立即添加到 IConfigurationRoot,而无需先调用 Build()。
The ConfigurationBuilder approach using the builder pattern is cleaner in some ways, as it has a clearer separation of concerns, but the common use patterns for configuration mean that the new ConfigurationManager approach is often easier to use.
使用 builder 模式的 ConfigurationBuilder 方法在某些方面更简洁,因为它具有更清晰的关注点分离,但配置的常见使用模式意味着新的 ConfigurationManager 方法通常更易于使用。
If you prefer, you can still use the builder pattern by accessing WebApplicationBuilder.Host.ConfigureAppConfiguration. You can read about some of these patterns and the differences between the two approaches on my blog at http://mng.bz/Ke4j.
如果您愿意,您仍然可以通过访问 WebApplicationBuilder.Host.ConfigureAppConfiguration 来使用构建器模式。您可以在我的博客 http://mng.bz/Ke4j 上阅读其中一些模式以及两种方法之间的差异。

You can access the IConfiguration object directly in Program.cs, as in listing 10.3, but the ConfigurationManager is also registered as IConfiguration in the dependency injection (DI) container, so you can inject it into your classes and endpoint handlers. You could rewrite the endpoint handler in listing 10.3 as the following, and the IConfiguration object would be injected into the handler using DI:

你可以直接在 Program.cs 中访问 IConfiguration 对象,如清单 10.3 所示,但 ConfigurationManager 也在依赖注入 (DI) 容器中注册为 IConfiguration,因此你可以将其注入到你的类和端点处理程序中。您可以将清单 10.3 中的端点处理程序重写为以下内容,并且 IConfiguration 对象将使用 DI 注入到处理程序中:

app.MapGet("/", (IConfiguration config) => config.AsEnumerable());

Note The ConfigurationManager implements IConfigurationRoot, which also implements IConfiguration. The ConfigurationManager is registered in the DI container as an IConfiguration, not an IConfigurationRoot.
注意 ConfigurationManager 实现 IConfigurationRoot,后者也实现 IConfiguration。ConfigurationManager 在 DI 容器中注册为 IConfiguration,而不是 IConfigurationRoot。

You’ve seen how to add values to the ConfigurationManager by using providers such as the JSON file provider. and listing 10.3 showed an example of iterating over every configuration value, but normally you want to retrieve a specific configuration value.

您已经了解了如何使用诸如 JSON 文件提供程序之类的提供程序向 ConfigurationManager 添加值,清单 10.3 显示了一个迭代每个配置值的示例,但通常您希望检索特定的配置值。

IConfiguration stores configuration as a set of key-value string pairs. You can access any value by its key, using standard dictionary syntax. You could use

IConfiguration 将配置存储为一组键值字符串对。您可以使用标准字典语法通过其键访问任何值。您可以使用

var zoomLevel = builder.Configuration["MapSettings:DefaultZoomLevel"];

to retrieve the configured zoom level for your application (using the settings shown in listing 10.2). Note that I used a colon (:) to designate a separate section. Similarly, to retrieve the latitude key, you could use

检索为应用程序配置的缩放级别(使用清单 10.2 中所示的设置)。请注意,我使用冒号 (:) 来指定一个单独的部分。同样,要检索纬度键,您可以使用

var lat = builder.Configuration["MapSettings:DefaultLocation:Latitude"];

Note If the requested configuration key doesn’t exist, you get a null value.
注意 如果请求的配置键不存在,您将获得一个null 值。

You can also grab a whole section of the configuration by using the GetSection(section) method, which returns an IConfigurationSection, which also implements IConfiguration. This method grabs a chunk of the configuration and resets the namespace. Another way to get the latitude key is

您还可以使用 GetSection (section) 方法获取配置的整个部分,该方法返回 IConfigurationSection,该方法还实现 IConfiguration。此方法获取配置块并重置命名空间。获取纬度键的另一种方法是

var lat = builder.Configuration
    .GetSection("MapSettings")["DefaultLocation:Latitude"];

Accessing setting values this way is useful in Program.cs when you’re defining your application. When you’re setting up your application to connect to a database, for example, you’ll often load a connection string from the IConfiguration object. You’ll see a concrete example in chapter 12, which looks at Entity Framework Core.

以这种方式访问设置值在 Program.cs 定义应用程序时非常有用。例如,在设置应用程序以连接到数据库时,通常会从 IConfiguration 对象加载连接字符串。您将在第 12 章中看到一个具体示例,该示例介绍了 Entity Framework Core。

If you need to access the configuration object in places other than Program.cs, you can use DI to inject it as a dependency into your service’s constructor. But accessing configuration by using string keys this way isn’t particularly convenient; you should try to use strongly typed configuration instead, as you’ll see in section 10.3.

如果您需要在 Program.cs 以外的位置访问配置对象,则可以使用 DI 将其作为依赖项注入到服务的构造函数中。但是以这种方式使用字符串键访问配置并不是特别方便;你应该尝试使用强类型配置,正如你将在 Section 10.3 中看到的那样。

So far, this process probably feels a bit too convoluted and run-of-the-mill to load settings from a JSON file, and I’ll grant you that it is. Where the ASP.NET Core configuration system shines is when you have multiple providers.

到目前为止,从 JSON 文件加载设置,这个过程可能感觉有点太复杂和普通了,我承认确实如此。ASP.NET Core 配置系统的亮点是当您拥有多个提供商时。

10.2.2 Using multiple providers to override configuration values

10.2.2 使用多个提供程序覆盖配置值

You’ve seen how to add a configuration provider to the ConfigurationManager and retrieve the configuration values, but so far, you’ve configured only a single provider. When you add providers, it’s important to consider the order in which you add them, as that defines the order in which the configuration values will be added to the underlying dictionary. Configuration values from later providers overwrite values with the same key from earlier providers.

您已经了解了如何将配置提供程序添加到 ConfigurationManager 并检索配置值,但到目前为止,您只配置了一个提供程序。添加提供程序时,请务必考虑添加它们的顺序,因为这定义了将配置值添加到底层字典的顺序。来自更高提供程序的配置值会使用来自早期提供程序的相同键覆盖值。

Note This sentence bears repeating: the order in which you add configuration providers to ConfigurationManager is important. Later configuration providers can overwrite the values of earlier providers.
注意 这句话值得重复:将配置提供程序添加到 ConfigurationManager 的顺序很重要。更高版本的配置提供程序可以覆盖早期提供程序的值。

Think of the configuration providers as adding layers of configuration values to a stack, where each layer may overlap some or all of the layers below, as shown in figure 10.3. If the new provider contains any keys that are already known to the ConfigurationManager, they overwrite the old values to create the final set of configuration values stored in IConfiguration.

将配置提供程序视为向堆栈中添加配置值层,其中每个层可能与下面的部分或全部层重叠,如图 10.3 所示。如果新提供程序包含任何ConfigurationManager 中,它们会覆盖旧值以创建存储在 IConfiguration 中的最终配置值集。

Tip Instead of thinking in layers, you can think of the ConfigurationManager as a simple dictionary. When you add a provider, you’re setting some key-value pairs. When you add a second provider, the provider can add new keys or overwrite the value of existing keys.
提示 您可以将 ConfigurationManager 视为一个简单的字典,而不是分层思考。添加提供程序时,您将设置一些键值对。当您添加第二个提供程序时,该提供程序可以添加新键或覆盖现有键的值。

alt text

Figure 10.3 Each configuration provider adds a layer of values to ConfigurationBuilder. Calling Build() collapses that configuration. Later providers overwrite configuration values with the same keys from earlier providers.
图 10.3 每个配置提供程序都会向 ConfigurationBuilder 添加一层值。调用 Build() 会折叠该配置。后面的 provider 会用早期 providers的相同键覆盖配置值。

Update your code to load configuration from three different configuration providers—two JSON providers and an environment variable provider—by adding them to ConfigurationManager as shown in the following listing.

更新您的代码以从三个不同的配置提供程序(两个 JSON 提供程序和一个环境)加载配置变量提供程序 - 通过将它们添加到 ConfigurationManager如下面的清单所示。

Listing 10.4 Loading from multiple providers in Program.cs
清单 10.4 在 Program.cs 中从多个提供商加载

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Configuration.Sources.Clear();
builder.Configuration ❶
    .AddJsonFile("sharedSettings.json", optional: true); ❶
builder.Configuration.AddJsonFile("appsettings.json", optional: true);
builder.Configuration.AddEnvironmentVariables(); ❷

WebApplication app = builder.Build();

app.MapGet("/", () => app.Configuration.AsEnumerable());

app.Run();

❶ Loads configuration from a different JSON configuration file before the
appsettings.json file
在 appsettings.json 文件之前从不同的 JSON 配置文件加载配置

❷ Adds the machine’s environment variables as a configuration provider
将计算机的环境变量添加为配置提供程序

This layered design can be useful for several things. Fundamentally, it allows you to aggregate configuration values from multiple sources into a single, cohesive object. To cement this design in place, consider the configuration values in figure 10.4.
这种分层设计可用于多种用途。从根本上说,它允许您将来自多个源的配置值聚合到一个内聚对象中。为了巩固此设计,请考虑图 10.4 中的配置值。

alt text

Figure 10.4 The final IConfiguration includes the values from each of the providers. Both appsettings.json and the environment variables include the MyAppConnString key. As the environment variables are added later, that configuration value is used.

图 10.4 最终的 IConfiguration 包括来自每个提供程序的值。appsettings.json 和环境变量都包含 MyAppConnString 键。由于稍后添加环境变量,因此将使用该配置值。

Most of the settings in each provider are unique and are added to the final IConfiguration. But the "MyAppConnString" key appears both in appsettings.json and as an environment variable. Because the environment variable provider is added after the JSON providers, the environment variable configuration value is used in IConfiguration.

每个提供程序中的大多数设置都是唯一的,并添加到最终的 IConfiguration 中。但是 “MyAppConnString” key 既显示在 appsettings.json 中,也显示为环境变量。由于环境变量提供程序是在 JSON 提供程序之后添加的,因此在 IConfiguration 中使用环境变量配置值。

The ability to collate configuration from multiple providers is handy on its own, but this design is especially useful for handling sensitive configuration values, such as connection strings and passwords. Section 10.2.3 shows how to deal with this problem, both locally on your development machine and on production servers.

整理来自多个提供商的配置的能力本身很方便,但这种设计对于处理敏感的配置值(例如连接字符串和密码)特别有用。Section 10.2.3 展示了如何在开发计算机和 生产服务器上本地处理此问题。

10.2.3 Storing configuration secrets safely

10.2.3 安全地存储配置密钥

As soon as you build a nontrivial app, you’ll find that you need to store some sort of sensitive data as a setting somewhere. This data could be a password, a connection string, or an API key for a remote service, for example.

一旦你构建了一个重要的应用程序,你就会发现你需要在某个地方存储某种敏感数据作为设置。例如,此数据可以是密码、连接字符串或远程服务的 API 密钥。

Storing these values in appsettings.json is generally a bad idea, as you should never commit secrets to source control; the number of secret API keys people have committed to GitHub is scary! Instead, it’s much better to store these values outside your project folder, where they won’t get committed accidentally.

将这些值存储在 appsettings.json 中通常是一个坏主意,因为您永远不应该将 secret 提交到源代码管理;人们提交到 GitHub 的秘密 API 密钥的数量是可怕的!相反,最好将这些值存储在项目文件夹之外,这样它们就不会意外提交。

You can do this in a few ways, but the easiest and most common approaches are to use environment variables for secrets on your production server and User Secrets locally. Neither approach is truly secure, in that neither stores values in an encrypted format. If your machine is compromised, attackers will be able to read the stored values because they’re stored in plain text. These approaches are intended mainly to help you avoid committing secrets to source control.

您可以通过多种方式执行此作,但最简单和最常见的方法是将环境变量用于生产服务器上的密钥和本地的 User Secrets。这两种方法都不是真正安全的,因为两者都不会以加密格式存储值。如果您的计算机遭到入侵,攻击者将能够读取存储的值,因为它们以纯文本形式存储。这些方法主要是为了帮助您避免将密钥提交到源代码管理。

Tip Azure Key Vault is a secure alternative, in that it stores the values encrypted in Azure, but you still need to use User Secrets and environment variables to store the Azure Key Vault connection details. See the documentation for instructions on using Azure Key Vault in your apps http://mng.bz/BR7v. Another popular option is Vault by Hashicorp (www.vaultproject.io), which can be run on-premises or in the cloud.
提示 Azure Key Vault 是一种安全的替代方案,因为它将加密的值存储在 Azure 中,但您仍然需要使用用户密钥和环境变量来存储 Azure Key Vault 连接详细信息。有关在应用 http://mng.bz/BR7v 中使用 Azure Key Vault 的说明,请参阅文档。另一个流行的选项是 Hashicorp 的 Vault (www.vaultproject.io),它可以在本地或云中运行。

Whichever approach you use to store your application secrets, make sure that you aren’t storing them in source control. Even private repositories may not stay private forever, so it’s best to err on the side of caution.

无论您使用哪种方法来存储应用程序密钥,请确保您没有将它们存储在源代码管理中。即使是私有存储库也可能不会永远保持私有,因此最好谨慎行事。

Storing secrets in environment variables in production

在生产环境中的环境变量中执行 SECRET
You can add the environment variable configuration provider by using the AddEnvironmentVariables extension method, as you saw in listing 10.4. This method adds all the environment variables on your machine as key-value pairs to ConfigurationManager.
您可以使用 AddEnvironmentVariables 扩展方法添加环境变量配置提供程序,如清单 10.4 所示。此方法将计算机上的所有环境变量作为键值对添加到 ConfigurationManager。

Note The WebApplicationBuilder adds the environment variable provider to the ConfigurationManager by default.
注意 默认情况下,WebApplicationBuilder 将环境变量提供程序添加到 ConfigurationManager。

You can create the same hierarchical sections in environment variables that you typically see in JSON files by using a colon (:) or a double underscore () to demarcate a section, as in MapSettings:MaxNumberOfPoints or MapSettingsMaxNumberOfPoints.
您可以使用冒号 (:) 或双下划线 ( ) 来划分部分,从而在环境变量中创建通常在 JSON 文件中看到的相同分层部分,如下所示MapSettings:MaxNumberOfPoints 或MapSettings MaxNumberOfPoints 的 MapSettings MaxNumberOfPoints 的 Points。

Tip Some environments, such as Linux, don’t allow the colon in environment variables. You must use the double-underscore approach in these environments instead. A double underscore in an environment variable is converted to a colon when it’s imported into the IConfiguration object. You should always use the colon when retrieving values from an IConfiguration in your app.
提示 某些环境(如 Linux)不允许在环境变量中使用冒号。您必须在这些环境中改用双下划线方法。环境变量中的双下划线在导入 IConfiguration 对象时将转换为冒号。从应用程序中的 IConfiguration 检索值时,应始终使用冒号。

The environment-variable approach is particularly useful when you’re publishing your app to a self-contained environment, such as a dedicated server, Azure, or a Docker container. You can set environment variables on your production machine or on your Docker container, and the provider reads them at runtime, overriding the defaults specified in your appsettings.json files.

将应用发布到独立环境(如专用服务器、Azure 或 Docker 容器)时,环境变量方法特别有用。您可以在生产计算机或 Docker 容器上设置环境变量,提供程序会在运行时读取这些变量,从而覆盖 appsettings.json 文件中指定的默认值。

Tip For instructions on setting environment variables for your operating system, see Microsoft’s “Use multiple environments in ASP.NET Core” documentation at http://mng.bz/d4OD.
提示 有关为作系统设置环境变量的说明,请参阅 http://mng.bz/d4OD Microsoft的“在 ASP.NET Core 中使用多个环境”文档。

For a development machine, environment variables are less useful, as all your apps would use the same values. If you set the ConnectionStrings__ DefaultConnection environment variable, for example, that variable would be added to every app you run locally, which sounds like more of a hassle than a benefit!

对于开发计算机,环境变量不太有用,因为您的所有应用程序都将使用相同的值。例如,如果您设置 ConnectionStrings DefaultConnection 环境变量,则该变量将被添加到您在本地运行的每个应用程序中,这听起来更麻烦而不是好处!

Tip To avoid collisions, you can add only environment variables that have a given prefix, such as AddEnvironmentVariables("SomePrefix"). The prefix is removed from the key before it’s added to the ConfigurationManager, so the variable SomePrefix_MyValue is added to configuration as MyValue.
提示 为避免冲突,您可以仅添加具有给定前缀的环境变量,例如 AddEnvironmentVariables(“SomePrefix”)。在将前缀添加到ConfigurationManager 中,因此变量SomePrefix_MyValue 将作为 MyValue 添加到配置中。

For development scenarios, you can use the User Secrets Manager, which effectively adds per-app environment variables, so you can have different settings for each app but store them in a different location from the app itself.

对于开发场景,您可以使用 User Secrets Manager,它可以有效地添加每个应用程序的环境变量,因此您可以为每个应用程序设置不同的设置,但将它们存储在与应用程序本身不同的位置。

Storing secrets with the User Secrets Manager in development

STORING SECRETS 与 US系列 S ECRETS MANAGER 在开发中
The idea behind User Secrets is to simplify storing per-app secrets outside your app’s project tree. This approach is similar to environment variables, but you use a unique key for each app to keep the secrets segregated.
User Secrets 背后的理念是简化在应用程序项目树之外存储每个应用程序 Secret 的过程。此方法类似于环境变量,但您可以为每个应用程序使用唯一的键来保持密钥隔离。

Warning The secrets aren’t encrypted, so don’t consider them to be secure. Nevertheless, it’s an improvement on storing them in your project folder.
警告 密钥未加密,因此不要认为它们是安全的。不过,与将它们存储在项目文件夹中相比,这是一个改进。

Setting up User Secrets takes a bit more effort than using environment variables, as you need to configure a tool to read and write them, add the User Secrets configuration provider, and define a unique key for your application. To add User Secrets to your app, follow these steps:

设置用户密钥比使用环境变量需要更多的工作,因为您需要配置一个工具来读取和写入它们,添加用户密钥配置提供程序,并为您的应用程序定义唯一密钥。要将 User Secrets 添加到您的应用程序,请执行以下步骤:

  1. WebApplicationBuilder adds the User Secrets provider by default. The .NET SDK includes a global tool for working with secrets from the command line.
    默认情况下,WebApplicationBuilder 会添加 User Secrets 提供程序。.NET SDK 包括一个全局工具,用于从命令行处理机密。

  2. If you’re using Visual Studio, right-click your project and choose Manage User Secrets from the contextual menu. This command opens an editor for a secrets.json file in which you can store your key-value pairs as though it were an appsettings.json file, as shown in figure 10.5.
    如果您使用的是 Visual Studio,请右键单击您的项目,然后从上下文菜单中选择 Manage User Secrets (管理用户密钥)。此命令将打开一个 secrets.json 文件的编辑器,您可以在其中存储键值对,就像它是一个 appsettings.json 文件一样,如图 10.5 所示。

alt text
Figure 10.5 Choose Manage User Secrets to open an editor for the User Secrets app. You can use this file to store secrets when developing your app locally. These secrets are stored outside your project folder, so they won’t be committed to source control accidentally.
图 10.5 选择 Manage User Secrets(管理用户密钥)以打开 User Secrets 应用程序的编辑器。在本地开发应用程序时,您可以使用此文件来存储密钥。这些密钥存储在您的项目文件夹之外,因此它们不会意外提交到源代码管理。

  1. Add a unique identifier to your .csproj file. Visual Studio does this automatically when you choose Manage User Secrets, but if you’re using the command line, you’ll need to add it yourself. Typically, you’d use a unique ID, such as a globally unique identifier (GUID):
    向 .csproj 文件添加唯一标识符。当您选择 Manage User Secrets (管理用户密码) 时,Visual Studio 会自动执行此作,但如果您使用的是命令行,则需要自己添加它。通常,您会使用唯一 ID,例如全局唯一标识符 (GUID):

    <PropertyGroup>
    <UserSecretsId>96eb2a39-1ef9-4d8e-8b20-8e8bd14038aa</UserSecretsId>
    </PropertyGroup>

You can also generate the UserSecretsId property with a random value using the .NET command-line interface (CLI) by running the following command from your project folder:
您还可以使用 .NET 命令行界面 (CLI) 生成具有随机值的 UserSecretsId 属性,方法是从项目文件夹运行以下命令:

dotnet user-secrets init
  1. Add User Secrets by using the command line
    使用命令行添加用户密钥

    dotnet user-secrets set "MapSettings:GoogleMapsApiKey" F5RJT9GFHKR7

or edit the secret.json file directly by using your favorite editor. The exact location of this file depends on your operating system and may vary. Check the documentation for details at http://mng.bz/ryAg.
或者使用您最喜欢的编辑器直接编辑 secret.json 文件。此文件的确切位置取决于您的作系统,并且可能会有所不同。有关详细信息,请查看 http://mng.bz/ryAg 中的文档。

Note The Secret Manager tool is included in the .NET CLI, but you can also use the CLI to install additional .NET tools. You can find more about .NET tools in general in Microsoft’s “How to manage .NET tools” documentation: http://mng.bz/VdmX.
注意 Secret Manager 工具包含在 .NET CLI 中,但您也可以使用 CLI 安装其他 .NET 工具。您可以在 Microsoft 的“如何管理 .NET 工具”文档中找到有关 .NET 工具的更多信息:http://mng.bz/VdmX

Phew! That’s a lot of setup, and if you’re adding providers to ConfigurationManager manually, you’re not done yet! You need to update your app to load the User Secrets at runtime by using the AddUserSecrets extension method:

唷!这需要大量的设置,如果您手动将提供程序添加到 ConfigurationManager,那么您还没有完成!您需要使用 AddUserSecrets 扩展方法更新应用程序以在运行时加载用户密钥:

if (builder.Environment.IsDevelopment())
{
    builder.Configuration.AddUserSecrets<Program>();
}

Note You should use the User Secrets provider only in development, not in production, so in the preceding snippet you add the provider conditionally to ConfigurationManager. In production you should use environment variables or Azure Key Vault, as discussed earlier. All this is configured correctly by default when you use the default WebApplicationBuilder.

注意 您应该仅在开发中使用 User Secrets 提供程序,而不应在生产中使用,因此在前面的代码段中,您将有条件地将提供程序添加到 ConfigurationManager。如前所述,在生产环境中,应使用环境变量或 Azure Key Vault。默认情况下,当您使用默认的 WebApplicationBuilder 时,所有这些都是正确配置的。

The AddUserSecrets method has several overloads, but the simplest is a generic method that you can call by passing your application’s Program class as a generic argument, as shown in the preceding example. The User Secrets provider needs to read the UserSecretsId property that you (or Visual Studio) added to the .csproj file. The Program class acts as a simple marker to indicate which assembly contains this property.

AddUserSecrets 方法具有多个重载,但最简单的是泛型方法,您可以通过将应用程序的 Program 类作为泛型参数传递来调用该方法,如前面的示例所示。用户机密提供程序需要读取你(或 Visual Studio)添加到 .csproj 文件的 UserSecretsId 属性。Program 类充当一个简单的标记,用于指示哪个程序集包含此属性。

Note If you’re interested, the .NET software development kit (SDK) uses the UserSecretsId property in your .csproj file to generate an assembly-level UserSecretsIdAttribute. Then the provider reads this attribute at runtime to determine the UserSecretsId of the app and generates the path to the secrets.json file.
注意 如果您感兴趣,.NET SDK 使用 .csproj 文件中的 UserSecretsId 属性生成程序集级 UserSecretsIdAttribute。然后,提供程序在运行时读取此属性以确定应用的 UserSecretsId 并生成secrets.json文件的路径。

And there you have it—safe storage of your secrets outside your project folder during development. This cautious approach may seem like overkill, but if you have anything you consider to be remotely sensitive that you need to load into configuration, I strongly urge you to use environment variables or User Secrets.

这就是 — 在开发过程中将密钥安全地存储在项目文件夹之外。这种谨慎的方法可能会似乎有点矫枉过正,但如果您有任何您认为远程敏感的东西需要加载到配置中,我强烈建议您使用环境变量或用户密钥。

It’s almost time to leave configuration providers behind, but before we do, I’d like to show you the ASP.NET Core configuration system’s party trick: reloading files on the fly.

现在差不多该抛弃配置提供程序了,但在此之前,我想向您展示 ASP.NET Core 配置系统的派对技巧:动态重新加载文件。

10.2.4 Reloading configuration values when they change

10.2.4 在配置值更改时重新加载配置值

Besides security, not having to recompile your application every time you want to tweak a value is one of the advantages of using configuration and settings. In the previous version of ASP.NET, changing a setting by editing web.config would cause your app to restart. This feature beat having to recompile, but waiting for the app to start up before it could serve requests was a bit of a drag.
除了安全性之外,不必在每次要调整值时都重新编译应用程序是使用 configuration 和 settings 的优势之一。在早期版本的 ASP.NET 中,通过编辑 web.config 来更改设置将导致应用程序重启。此功能比必须重新编译要好,但等待应用程序启动后才能处理请求有点麻烦。

In ASP.NET Core, you finally get the ability to edit a file and have the configuration of your application update automatically, without your having to recompile or restart. An often-cited scenario in which you might find this ability useful is when you’re trying to debug an app you have in production. You typically configure logging to one of several levels:

在 ASP.NET Core 中,您最终能够编辑文件并自动更新应用程序的配置,而无需重新编译或重新启动。您可能会发现此功能非常有用的一个经常被引用的场景是,当您尝试调试生产环境中的应用程序时。通常将日志记录配置为以下几个级别之一:

  • Error
    错误
  • Warning
    警告
  • Information
    信息
  • Debug
    调试

Each of these settings is more verbose than the last, but it also provides more context. By default, you might configure your app to log only warning and error-level logs in production so that you don’t generate too many superfluous log entries. Conversely, if you’re trying to debug a problem, you want as much information as possible, so you may want to use the debug log level.

这些设置中的每一个都比上一个设置更详细,但它也提供了更多的上下文。默认情况下,您可以将应用程序配置为在生产环境中仅记录警告和错误级别的日志,这样就不会生成太多多余的日志条目。相反,如果您尝试调试问题,则需要尽可能多的信息,因此可能需要使用 debug log 级别。

Being able to change configuration at runtime means that you can easily switch on extra logs when you encounter a problem and switch them back afterward by editing your appsettings.json file.

能够在运行时更改配置意味着您可以在遇到问题时轻松打开额外的日志,并在之后通过编辑 appsettings.json 文件将它们切换回来。

Note Reloading is generally available only for file-based configuration providers, such as the JSON provider, as opposed to the environment variable provider, for example.
注意 重新加载通常仅适用于基于文件的配置提供程序,例如 JSON 提供程序,而不是环境变量提供程序。

You can enable the reloading of configuration files when you add any of the file-based providers to your ConfigurationManager. The Add*File extension methods include an overload with a reloadOnChange parameter. If this parameter is set to true, the app monitors the filesystem for changes to the file and triggers a complete rebuild of the IConfiguration, if needs be. The following listing shows how to add configuration reloading to the appsettings.json file added manually to the ConfigurationManager.

当您将任何基于文件的提供程序添加到 ConfigurationManager 时,您可以启用配置文件的重新加载。Add*File 扩展名方法包括带有 reloadOnChange 参数的重载。如果此参数设置为 true,则应用程序会监视文件系统中对文件的更改,并在需要时触发 IConfiguration 的完全重建。下面的清单显示了如何将配置重新加载添加到手动添加到 ConfigurationManager 的 appsettings.json 文件中。

Listing 10.5 Reloading appsettings.json when the file changes
示例 10.5 当文件更改时重新加载 appsettings.json

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Configuration.Sources.Clear();
builder.Configuration
    .AddJsonFile(
        "appsettings.json",
        optional: true,
        reloadOnChange: true); ❶

WebApplication app = builder.Build();

app.MapGet("/", () => app.Configuration.AsEnumerable());

app.Run();

❶ IConfiguration will be rebuilt if the appsettings.json file changes.
如果 IConfiguration 文件发生更改,appsettings.json 将重建。

Throughout section 10.2, you’ve seen how to customize the ConfigurationManager providers by clearing the default sources and adding your own, but in most cases, that won’t be necessary. As described in section 10.2.1, the default providers added by WebApplicationBuilder are normally good enough unless you want to add a new provider, such as Azure Key Vault. As a bonus, WebApplicationBuilder configures the appsettings.json with reloadOnChange:true by default. It’s worth sticking with the defaults initially and clear the sources and start again only if you really need to.

在整个 10.2 节中,您已经了解了如何通过清除默认源并添加您自己的源来自定义 ConfigurationManager 提供程序,但在大多数情况下,这不是必需的。如第 10.2.1 节所述,WebApplicationBuilder 添加的默认提供程序通常足够好,除非您要添加新的提供程序,例如 Azure Key Vault。作为奖励,WebApplicationBuilder 默认使用 reloadOnChange:true 配置appsettings.json。最初坚持使用默认值并清除源并仅在您确实需要时才重新开始是值得的。

Warning Adding a file configuration source using reloadOnChange:true isn’t entirely free, as ASP.NET Core sets up a file watcher in the background. Normally, this situation isn’t problematic, but if you set up a configuration watching thousands of files, you could run into difficulties!
警告 使用 reloadOnChange:true 添加文件配置源并非完全免费,因为 ASP.NET Core 在后台设置了文件侦听器。通常,这种情况没有问题,但如果您设置一个监视数千个文件的配置,您可能会遇到困难!

In listing 10.5, any changes you make to the file will be mirrored in the IConfiguration. But as I said at the start of this chapter, IConfiguration isn’t the preferred way to pass settings around in your application. Instead, as you’ll see in section 10.3, you should favor strongly typed objects.

在清单 10.5 中,您对文件所做的任何更改都将镜像到 IConfiguration 中。但正如我在本章开头所说,IConfiguration 并不是在应用程序中传递设置的首选方法。相反,正如您将在 10.3 节中看到的那样,您应该首选强类型对象。

10.3 Using strongly typed settings with the options pattern

10.3 将强类型设置与 options 模式一起使用

In this section you’ll learn about strongly typed configuration and the options pattern, the preferred way of accessing configuration in ASP.NET Core. By using strongly typed configuration, you can avoid problems with typos when accessing configuration. It also makes classes easier to test, as you can use simple POCO objects for configuration instead of relying on the IConfiguration abstraction.

在本节中,您将了解强类型配置和选项模式,这是在 ASP.NET Core 中访问配置的首选方式。通过使用强类型配置,您可以避免在访问配置时出现拼写错误问题。它还使类更易于测试,因为您可以使用简单的 POCO 对象进行配置,而不是依赖 IConfiguration 抽象。

Most of the examples I’ve shown so far have been about how to get values into IConfiguration, as opposed to how to use them. You’ve seen that you can access a key by using the builder.Configuration["key"] dictionary syntax, but using string keys this way feels messy and prone to typos, and the value retrieved is always a string, so you often need to convert it to another type. Instead, ASP.NET Core promotes the use of strongly typed settings—POCO objects that you define and create and that represent a small collection of settings, scoped to a single feature in your app.

到目前为止,我展示的大多数示例都是关于如何将值导入 IConfiguration,而不是如何使用它们。 您已经看到,您可以使用生成器访问密钥。Configuration[“key”] 字典语法,但以这种方式使用字符串键感觉很混乱,容易出现拼写错误,而且检索到的值总是字符串,所以你经常需要把它转换成另一种类型。相反,ASP.NET Core 促进了强类型设置的使用,即您定义和创建的 POCO 对象,这些对象表示一小部分设置,范围限定为应用程序中的单个功能。

The following listing shows both the settings for your store locator component and display settings to customize the home page of the app. They’re separated into two different objects with "MapSettings" and "AppDisplaySettings" keys, corresponding to the different areas of the app that they affect.

以下清单显示了 store locator 组件的地图设置和显示设置,以自定义应用程序的主页。它们被分成两个不同的对象,分别具有“MapSettings”和“AppDisplaySettings”键,对应于它们影响的应用的不同区域。

Listing 10.6 Separating settings into different objects in appsettings.json
清单 10.6 在 appsettings.json 中将设置分离到不同的对象中

{
    "MapSettings": { ❶
        "DefaultZoomLevel": 6, ❶
        "DefaultLocation": { ❶
            "latitude": 50.500, ❶
            "longitude": -4.000 ❶
    }
},
    "AppDisplaySettings": { ❷
        "Title": "Acme Store Locator", ❷
        "ShowCopyright": true ❷
    }
}

❶ Settings related to the store locator section of the app
与应用程序的商店定位器部分相关的设置

❷ General settings related to displaying the app
与显示应用程序相关的常规设置

The simplest approach to exposing the home-page settings in an endpoint handler is to inject IConfiguration into the endpoint handler and access the values by using the dictionary syntax:

在终结点处理程序中公开主页设置的最简单方法是将 IConfiguration 注入到终结点处理程序中,并使用字典语法访问值:

app.MapGet("/display-settings", (Iconfiguration config) =>
{
    string title = config["AppDisplaySettings:Title"];
    bool showCopyright = bool.Parse(
            config["AppDisplaySettings:ShowCopyright"]);

    return new { title, showCopyright };
});

But you don’t want to do this; there are too many strings for my liking! And that bool.Parse? Yuck! Instead, you can use custom strongly typed objects, with all the type safety and IntelliSense goodness that brings, as shown in the following listing.

但你不想这样做;琴弦太多了,我不喜欢!还有那个 bool。解析?呸!相反,您可以使用自定义的强类型对象,这些对象具有带来的所有类型安全性和 IntelliSense 优点,如下面的清单所示。

Listing 10.7 Injecting strongly typed options into a handler using IOptions
清单 10.7 使用 IOptions 将强类型选项注入处理程序

app.MapGet("/display-settings",
    (IOptions<AppDisplaySettings> options) => ❶
{
    AppDisplaySettings settings = options.Value; ❷
    string title = settings.Title; ❸

    bool showCopyright = settings.ShowCopyright; ❹

    return new { title, showCopyright };
});

❶ You can inject a strongly typed options class using the IOptions<> wrapper interface.
您可以使用 IOptions<> 包装器接口注入强类型选项类。

❷ The Value property exposes the POCO settings object.
Value 属性公开 POCO 设置对象。

❸ The settings object contains properties that are bound to configuration values at runtime.
settings 对象包含在运行时绑定到配置值的属性。

❹ The binder can also convert string values directly to built-in types.
Binder 还可以将字符串值直接转换为内置类型。

The ASP.NET Core configuration system includes a binder, which can take a collection of configuration values and bind them to a strongly typed object, called an options class. This binding is similar to the concept of JSON deserialization for creating types from chapter 6 and the model binding used by Model-View-Controller (MVC) and Razor Pages, which you’ll learn about in part 3.

ASP.NET Core 配置系统包括一个 Binder,它可以获取配置值的集合并将它们绑定到一个强类型对象,称为 options 类。此绑定类似于第 6 章中用于创建类型的 JSON 反序列化概念,以及模型-视图-控制器 (MVC) 和 Razor Pages 使用的模型绑定,您将在第 3 部分中了解。

Section 10.3.1 shows how to set up the binding of configuration values to a POCO options class, and section 10.3.2 shows how to make sure that it reloads when the underlying configuration values change. We’ll look at the different sorts of objects you can bind in section 10.3.3.

Section 10.3.1 显示了如何设置配置值到 POCO options 类的绑定,而Section 10.3.2 显示了如何确保在底层配置值更改时重新加载它。我们将在 10.3.3 节中看看你可以绑定的不同种类的对象。

10.3.1 Introducing the IOptions interface

10.3.1 IOptions 接口简介

ASP.NET Core introduced strongly typed settings as a way of letting configuration code adhere to the single-responsibility principle (SRP) and to allow the injection of configuration classes as explicit dependencies. Such settings also make testing easier; instead of having to create an instance of IConfiguration to test a service, you can create an instance of the POCO options class.

ASP.NET Core 引入了强类型设置,以此方式让配置代码遵循单一职责原则 (SRP),并允许将配置类作为显式依赖项注入。此类设置还使测试更容易;您不必创建 IConfiguration 的实例来测试服务,而是可以创建 POCO 选项类的实例。

The AppDisplaySettings class shown in the previous example could be simple, exposing only the values related to the home page:

上例所示的 AppDisplaySettings 类可能很简单,只公开与主页相关的值:

public class AppDisplaySettings
{
    public string Title { get; set; }
    public bool ShowCopyright { get; set; }
}

Your options classes need to be nonabstract and have a public parameterless constructor to be eligible for binding. The binder sets any public properties that match configuration values, as you’ll see in section 10.3.3.

你的 options 类必须是非抽象的,并且有一个公共的无参数构造函数才有资格进行绑定。Binders 设置与 configuration 值匹配的任何公共属性,如 Section 10.3.3 所示。

Tip You’re not restricted to built-in types such as string and bool; you can use nested complex types too. The options system binds sections to complex properties. See the associated source code for examples.
提示 您不仅限于 string 和 bool 等内置类型;您也可以使用嵌套的复杂类型。options 系统将 sections 绑定到 complex 属性。有关示例,请参阅关联的源代码。

To help facilitate the binding of configuration values to your custom POCO options classes, ASP.NET Core introduces the IOptions interface, a simple interface with a single property, Value, that contains your configured POCO options class at runtime. Options classes are configured as services in Program.cs , as shown in the following listing.

为了帮助将配置值绑定到自定义 POCO 选项类,ASP.NET Core 引入了 IOptions 接口,这是一个简单的接口,具有单个属性 Value,其中包含您在运行时配置的 POCO 选项类。选项类在 Program.cs 中配置为 services,如以下清单所示。

Listing 10.8 Configuring the options classes using Configure in Startup.cs
清单 10.8 在 Startup.cs 中使用Configure

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<MapSettings>(
    builder.Configuration.GetSection("MapSettings")); ❶
builder.Services.Configure<AppDisplaySettings>(
    builder.Configuration.GetSection("AppDisplaySettings")); ❷

❶ Binds the MapSettings section to the POCO options class MapSettings
将 MapSettings 部分绑定到 POCO 选项类 MapSettings

❷ Binds the AppDisplaySettings section to the POCO options class AppDisplaySettings
将 AppDisplaySettings 部分绑定到 POCO 选项类 AppDisplaySettings

Tip You don’t have to use the same name for both the section and class, as I do in listing 10.8; it’s simply a convention I like to follow. With this convention, you can also use the nameof() operator to further reduce the chance of typos, such as by calling GetSection(nameof(MapSettings)).
提示 您不必像在清单 10.8 中那样对 section 和 class 使用相同的名称;这只是我喜欢遵循的惯例。通过此约定,您还可以使用 nameof() 运算符来进一步减少拼写错误的可能性,例如通过调用 GetSection(nameof(MapSettings))。

Each call to Configure sets up the following series of actions internally:
每次调用 Configure 都会在内部设置以下一系列作:

  1. Creates an instance of ConfigureOptions, which indicates that IOptions should be configured based on configuration.
    创建 ConfigureOptions 的实例,这表示应根据配置配置 IOptions
    If Configure is called multiple times, multiple ConfigureOptions objects will be used, all of which can be applied to create the final object in much the same way that IConfiguration is built from multiple layers.
    如果多次调用 Configure,则将使用多个 ConfigureOptions 对象,所有这些对象都可以应用于创建最终对象,其方式与从多个层构建 IConfiguration 的方式大致相同。

  2. Each ConfigureOptions instance binds a section of IConfiguration to an instance of the T POCO class, setting any public properties on the options class based on the keys in the provided ConfigurationSection.
    每个 ConfigureOptions 实例将 IConfiguration 的一个部分绑定到 T POCO 类的实例,根据提供的 ConfigurationSection 中的键设置选项类上的任何公共属性。

Remember that the section name ("MapSettings" in listing 10.8) can have any value; it doesn’t have to match the name of your options class.
请记住,部分名称(清单 10.8 中的 “MapSettings”)可以有任何值;它不必与 Options 类的名称匹配。

  1. The IOptions interface is registered in the DI container as a singleton, with the final bound POCO object in the Value property.
    IOptions 接口在 DI 容器中注册为单一实例,最终绑定的 POCO 对象位于 Value 属性中。

This last step lets you inject your options classes into handlers and services by injecting IOptions, as you saw in listing 10.7, giving you encapsulated, strongly typed access to your configuration values. No more magic strings. Woo-hoo!

最后一步允许你通过注入 IOptions 将选项类注入到处理程序和服务中,就像你在清单 10.7 中看到的那样,为你提供了对配置值的封装、强类型访问。不再有神奇的字符串。呜呼!

Warning If you forget to call Configure and inject IOptions into your services, you won’t see any errors, but the T options class won’t be bound to anything and will have only default values in its properties.
警告 如果您忘记调用 Configure 并将 IOptions 注入到您的服务中,您将不会看到任何错误,但 T options 类不会绑定到任何内容,并且其属性中只有默认值。

The binding of the T options class to ConfigurationSection happens when you first request IOptions. The object is registered in the DI container as a singleton, so it’s bound only once.

T options 类与 ConfigurationSection 的绑定发生在您首次请求 IOptions 时。 该对象在 DI 容器中注册为单一实例,因此它只绑定一次。

This setup has one catch: you can’t use the reloadOnChange parameter I described in section 10.2.4 to reload your strongly typed options classes when using IOptions. IConfiguration will still be reloaded if you edit your appsettings.json files, but it won’t propagate to your options class.

此设置有一个问题:在使用 IOptions 时,您不能使用我在 Section 10.2.4 中描述的 reloadOnChange 参数来重新加载强类型选项类。如果编辑 IConfiguration 文件,appsettings.json 仍会重新加载,但它不会传播到选项类。

If that fact seems like a step backward or even a deal-breaker, don’t worry. IOptions has a cousin, IOptionsSnapshot, for such an occasion.

如果这个事实似乎是倒退,甚至是破坏交易,请不要担心。 IOptions 有一个表亲 IOptionsSnapshot 用于此类场合。

10.3.2 Reloading strongly typed options with IOptionsSnapshot

10.3.2 使用 IOptionsSnapshot 重新加载强类型选项

In section 10.3.1, you used IOptions to provide strongly typed access to configuration. Using IOptions to provide strongly typed access to configuration provided a nice encapsulation of the settings for a particular service, but with a specific drawback: the options class never changes, even if you modify the underlying configuration file from which it was loaded, such as appsettings.json.

在第 10.3.1 节中,您使用了 IOptions 提供对配置的强类型访问。使用 IOptions 提供对配置的强类型访问,为特定服务提供了很好的设置封装,但有一个特定的缺点:选项类永远不会更改,即使您修改了从中加载它的基础配置文件,例如 appsettings.json。

This situation isn’t always a problem (you generally shouldn’t be modifying files on live production servers anyway), but if you need this functionality, you can use the IOptionsSnapshot interface. Conceptually, IOptionsSnaphot is identical to IOptions in that it’s a strongly typed representation of a section of configuration. The difference is when and how often the POCO options objects are created when they’re used:

这种情况并不总是问题(通常无论如何都不应该在实时生产服务器上修改文件),但如果您需要此功能,则可以使用 IOptionsSnapshot 接口。从概念上讲,IOptionsSnaphot 与 IOptions 相同,因为它是配置部分的强类型表示形式。区别在于使用 POCO 选项对象的创建时间和频率:

  • IOptions—The instance is created once, when first needed. It always contains the configuration from when the object instance was first created.
    IOptions – 首次需要时,将创建一次实例。它始终包含首次创建对象实例时的配置。

  • IOptionsSnapshot—A new instance is created, when needed, if the underlying configuration has changed since the last instance was created.
    IOptionsSnapshot – 如果自创建上一个实例以来底层配置已更改,则在需要时创建新实例。

Warning IOptionsSnapshot is registered as a scoped service, so you can’t inject it into singleton services; if you do, you’ll have a captive dependency, as discussed in chapter 9. If you need a singleton version of IOptionsSnapshot, you can use a similar interface, IOptionsMonitor. See this blog post for details: http://mng.bz/9Da7.
警告 IOptionsSnapshot 注册为范围服务,因此您不能将其注入到单一实例服务中;如果这样做,您将拥有一个捕获依赖项,如第 9 章所述。如果需要 IOptionsSnapshot 的单一实例版本,可以使用类似的接口 IOptionsMonitor。有关详细信息,请参阅此博客文章:http://mng.bz/9Da7

IOptionsSnaphot is set up for your options classes automatically at the same time as IOptions, so you can use it in your services in exactly the same way. The following listing shows how you could update your display-settings API so that you always get the latest configuration values in your strongly typed AppDisplaySettings options class.

IOptionsSnaphot 与 IOptions 同时自动为您的选项类设置,因此您可以以完全相同的方式在服务中使用它。以下清单显示了如何更新 display-settings API,以便始终在强类型 AppDisplaySettings 选项类中获取最新的配置值。

Listing 10.9 Injecting reloadable options using IOptionsSnapshot
清单 10.9 使用IOptionsSnapshot

app.MapGet("/display-settings",
    (IOptionsSnapshot<AppDisplaySettings> options) => ❶
{
    AppDisplaySettings settings = options.Value; ❷

    return new
    {
        title = settings.Title, ❸
        showCopyright = settings.ShowCopyright, ❸
    };
});

IOptionsSnapshot<T> updates automatically if the underlying configuration values
change.
如果底层配置值发生更改,IOptionsSnapshot<T> 会自动更新。

❷ The Value property exposes the POCO settings object, the same as for IOptions<T>.
Value 属性公开 POCO 设置对象,与 IOptions<T> 相同。

❸ The settings match the configuration values at that point in time instead of at first
run.
这些设置与该时间点的配置值匹配,而不是在首次运行时匹配。

As IOptionsSnapshot<AppDisplaySettings> is registered as a scoped service, it’s re-created at every request. If you edit the settings file and cause IConfiguration to reload, IOptionsSnapshot<AppDisplaySettings> shows the new values on the next request. A new AppDisplaySettings object is created with the new configuration values and is used for all future DI—until you edit the file again, of course!

由于 IOptionsSnapshot<AppDisplaySettings> 注册为作用域服务,因此会在每次请求时重新创建它。如果编辑设置文件并导致 IConfiguration 重新加载,则 IOptionsSnapshot<AppDisplaySettings> 将在下一个请求中显示新值。使用新的配置值创建一个新的 AppDisplaySettings 对象,并将其用于所有未来的 DI — 当然,直到您再次编辑该文件为止!

Reloading your settings automatically is as simple as that: update your code to use IOptionsSnapshot<T> instead of IOptions<T> wherever you need it. But be aware that this change isn’t free. You’re rebinding and reconfiguring your options object with every request, which may have performance implications. In practice, reloading settings isn’t common in production, so you may decide that the developer convenience isn’t worth the performance impact.

自动重新加载设置非常简单:更新代码以使用 IOptionsSnapshot<T>而不是IOptions<T>,无论您需要什么地方。但请注意,此更改不是免费的。您正在使用每个请求重新绑定和重新配置 options 对象,这可能会对性能产生影响。在实践中,重新加载设置在生产中并不常见,因此您可能会认为开发人员的便利性不值得对性能造成影响。

An important consideration in using the options pattern is the design of your POCO options classes themselves. These classes typically are simple collections of properties, but there are a few things to bear in mind so that you don’t get stuck debugging why the binding seemingly hasn’t worked.

使用选项模式的一个重要考虑因素是 POCO 选项类本身的设计。这些类通常是简单的属性集合,但需要记住一些事项,这样您就不会陷入调试绑定似乎不起作用的原因。

10.3.3 Designing your options classes for automatic binding

10.3.3 为自动绑定设计 options 类

I’ve already touched on some of the requirements for POCO classes to work with the IOptions<T> binder, but there are a few rules to remember. The first key point is that the binder creates instances of your options classes by using reflection, so your POCO options classes need to

我已经谈到了 POCO 类与 IOptions<T> Binder 一起使用的一些要求,但有一些规则需要记住。第一个关键点是 Binder 使用反射创建选项类的实例,因此您的 POCO 选项类需要

  • Be nonabstract
    非抽象

  • Have a default (public parameterless) constructor
    具有默认 (public parameterless) 构造函数

If your classes satisfy these two points, the binder will loop through all the properties on your class and bind any that it can. In the broadest sense, the binder can bind any property that

如果您的类满足这两点,则 Binder 将遍历类上的所有属性,并绑定它可以绑定的任何属性。从最广泛的意义上讲,Binder 可以绑定

  • Is public
    是公开的

  • Has a getter (the binder won’t write set-only properties)
    具有 getter(Binder 不会写入仅 set-only 属性)

  • Has a setter or, for complex types, a non-null value
    具有 setter,或者对于复杂类型,具有非 null 值

  • Isn’t an indexer
    不是索引器

Listing 10.10 shows two extensive options class with a host of different types of properties. All the properties on BindableOptions are valid to bind, and all the properties on UnbindableOptions are not.

清单 10.10 展示了两个扩展的选项类,它们具有许多不同类型的属性。BindableOptions 上的所有属性都对 bind 有效,而 UnbindableOptions 上的所有属性都不是。

Listing 10.10 An options class containing binding and nonbinding properties
清单 10.10 包含 binding 和 nonbinding 属性的 options 类

public class BindableOptions
{
    public string String { get; set; } ❶
    public int Integer { get; set; } ❶
    public SubClass Object { get; set; } ❶
    public SubClass ReadOnly { get; } = new SubClass(); ❶
    public Dictionary<string, SubClass> Dictionary { get; set; } ❷
    public List<SubClass> List { get; set; } ❷
    public IDictionary<string, SubClass> IDictionary { get; set; } ❷
    public IEnumerable<SubClass> IEnumerable { get; set; } ❷
    public ICollection<SubClass> ReadOnlyCollection { get; } ❷
        = new List<SubClass>(); ❷

    public class SubClass
    {
    public string Value { get; set; }
    }
}

public class UnbindableOptions
{
    internal string NotPublic { get; set; } ❸
    public SubClass SetOnly { set => _setOnly = value; } ❸
    public SubClass NullReadOnly { get; } = null; ❸
    public SubClass NullPrivateSetter { get; private set; } = null; ❸
    public SubClass this[int i] { ❸
        get => _indexerList[i]; ❸
        set => _indexerList[i] = value; ❸
    }
    public List<SubClass> NullList { get; } ❹
    public Dictionary<int, SubClass> IntegerKeys { get; set; } ❹
    public IEnumerable<SubClass> ReadOnlyEnumerable { get; } ❹
        = new List<SubClass>(); ❹

    public SubClass _setOnly = null; ❺
    private readonly List<SubClass> _indexerList ❺
        = new List<SubClass>(); ❺

    public class SubClass
    {
        public string Value { get; set; }
    }
}

❶ The binder can bind simple and complex object types, and read-only properties with
a default.
Binder 可以将简单和复杂的对象类型以及只读属性与默认值绑定。

❷ The binder will also bind collections, including interfaces.
Binder 还将绑定集合,包括接口。

❸ The binder can’t bind nonpublic, set-only, null-read-only, or indexer properties.
Binder 无法绑定 non-public、set-only、null-read-only 或 indexer 属性。

❹ These collection properties can’t be bound.
这些集合属性无法绑定。

❺ The backing fields for implementing SetOnly and Indexer properties—not bound
directly
用于实现 SetOnly 和 Indexer 属性的支持字段 - 不直接绑定

As shown in the listing, the binder generally supports collections—both implementations and interfaces. If the collection property is already initialized, the binder uses the initialized value; otherwise, the binder may be able to create the collection instance automatically. If your property implements any of the following interfaces, the binder creates a List<> of the appropriate type as the backing object:

如清单所示,Binder 通常支持集合— 包括实现和接口。如果 collection 属性已初始化,则 Binder 将使用初始化的值;否则,Binder 可能能够自动创建 collection 实例。如果您的属性实现以下任何接口,则 Binder 会创建一个 List<> 作为后备对象:

  • IReadOnlyList<>
  • IReadOnlyCollection<>
  • ICollection<>
  • IEnumerable<>

Warning You can’t bind to an IEnumerable<> property that has already been initialized, as this interface doesn’t expose an Add function, and the binder won’t replace the backing value. You can bind to an IEnumerable<> if you leave its initial value null.
警告 您不能绑定到已初始化的 IEnumerable<> 属性,因为此接口不会公开 Add 函数,并且 Binder 不会替换后备值。如果保留 IEnumerable 的初始值<>则可以绑定到 IEnumerable null 的.

Similarly, the binder creates a Dictionary<,> as the backing field for properties with dictionary interfaces as long as they use string keys:
同样,Binder 会创建一个 Dictionary<,> 作为具有字典接口的属性的后备字段,只要它们使用 string、enum 或 integer(int、short、byte 等)键:

  • IDictionary<string,>
  • IReadOnlyDictionary<string,>

Warning You can’t bind dictionaries with non-string keys, such as int. For examples of binding collection types, see the associated source code for this book.
警告 您不能绑定使用非字符串或非整数键的字典,例如 custom classes 或 double。有关绑定集合类型的示例,请参阅本书的相关源代码。

Clearly, there are quite a few nuances here, but if you stick to the simple cases from the preceding example, you’ll be fine. Be sure to check for typos in your JSON files! You could also consider using explicit options validation, as described in this post: http://mng.bz/jPjr.

显然,这里有很多细微差别,但如果你坚持使用前面示例中的简单情况,你会没事的。请务必检查 JSON 文件中是否有拼写错误!你也可以考虑使用显式选项验证,如本文所述:http://mng.bz/jPjr

Tip The options pattern is most commonly used to bind POCO classes to configuration, but you can also configure your strongly typed settings classes in code by providing a lambda to the Configure function;, as in services.Configure(opt => opt.Value = true).
提示 选项模式最常用于将 POCO 类绑定到配置,但您也可以通过向 Configure 函数提供 lambda 来在代码中配置强类型设置类,就像在服务中一样.Configure(opt => opt.值 = true)。

The Options pattern is used throughout ASP.NET Core, but not everyone is a fan. In section 10.3.4 you’ll see how to use strongly typed settings and the configuration binder without the Options pattern.
Options 模式在整个 ASP.NET Core 中使用,但并非每个人都是粉丝。在 Section 10.3.4 中,您将看到如何使用强类型设置和没有 Options 模式的配置 Binder。

10.3.4 Binding strongly typed settings without the IOptions interface

10.3.4 在没有 IOptions 接口的情况下绑定强类型设置

The IOptions interface is canonical in ASP.NET Core; it’s used by the core ASP.NET Core libraries and has various convenience functions for binding strongly typed settings, as you’ve already seen. In many cases, however, the IOptions interface doesn’t give many benefits for consumers of the strongly typed settings objects. Services must take a dependency on the IOptions interface but then immediately extract the real object by calling IOptions<T>.Value. This situation can be especially annoying if you’re building a reusable library that isn’t inherently tied to ASP.NET Core, as you must expose the IOptions<T> interface in all your public APIs.

IOptions 接口在 ASP.NET Core 中是规范的;正如您已经看到的,它由 Core ASP.NET Core 库使用,并且具有用于绑定强类型设置的各种便捷函数。但是,在许多情况下,IOptions 接口并没有为强类型设置对象的使用者带来很多好处。服务必须依赖于 IOptions 接口,但随后通过调用 IOptions<T> 立即提取真实对象。值。如果您正在构建一个本质上不绑定到 ASP.NET Core 的可重用库,则这种情况可能尤其令人烦恼,因为您必须在所有公共 API 中公开 IOptions<T> 接口。

Luckily, the configuration binder that maps IConfiguration objects to strongly typed settings objects isn’t inherently tied to IOptions. Listing 10.11 shows how you can bind a strongly typed settings object to a configuration section manually, register it with the DI container, and inject the MapSettings object directly into a handler or service without the additional ceremony required to use IOptions.

幸运的是,将 IConfiguration 对象映射到强类型设置对象的配置绑定器本身并不与 IOptions 相关联。清单 10.11 展示了如何手动将强类型设置对象绑定到配置部分,将其注册到 DI 容器中,并将 MapSettings 对象直接注入处理程序或服务中,而无需使用 IOptions<MapSettings> 所需的额外仪式。

Listing 10.11 Configuring strongly typed settings without IOptions in Program.cs
清单 10.11 配置没有Program.cs 中的 IOptions

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

var settings = new MapSettings (); ❶
builder.Configuration.GetSection("MapSettings").Bind(settings); ❷
builder.Services.AddSingleton(settings); ❸

WebApplication app = builder.Build();

app.MapGet("/", (MapSettings mapSettings) => mapSettings); ❹

app.Run();

❶ Creates a new instance of the MapSettings object
创建 MapSettings 对象的新实例
❷ Binds the MapSettings section in IConfiguration to the settings object
将 IConfiguration 中的 MapSettings 部分绑定到设置对象
❸ Registers the settings object as a singleton
将 settings 对象注册为单例
❹ Injects the MapSettings object directly using DI
使用 DI 直接注入 MapSettings 对象

Alternatively, you can register the IOptions type in the DI container but then use a lambda to additionally register MapSettings as a singleton so it can be directly injected, as shown in listing 10.12.

或者,你可以在 DI 容器中注册 IOptions 类型,然后使用 lambda 另外将 MapSettings 注册为单例,以便可以直接注入,如清单 10.12 所示。

Listing 10.12 Configuring strongly typed settings for direct injection
示例 10.12 为直接注入配置强类型设置

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<MapSettings>( ❶
    builder.Configuration.GetSection("MapSettings")); ❶
builder.Services.AddSingleton(provider => ❷
    provider.GetRequiredService<IOptions<MapSettings>>().Value); ❷

WebApplication app = builder.Build();

app.MapGet("/", (MapSettings mapSettings) => mapSettings); ❸

app.Run();

❶ Configures the IOptions as normal
正常配置 IOptions
❷ Registers the MapSettings object in DI by delegating to the IOptions registration
通过委托 IOptions 注册,在 DI 中注册 MapSettings 对象
❸ Injects the MapSettings object directly DI
直接注入 MapSettings 对象 DI

If you use either of these approaches, you won’t benefit from the ability to reload strongly typed settings without further work or from some of the more advanced uses of IOptions, but in most cases, that’s not a big problem. I’m a fan of these approaches generally, but as always, consider what you’re losing before adopting them wholeheartedly.

如果使用这两种方法中的任何一种,则不会从无需进一步工作即可重新加载强类型设置的能力中受益,也不会受益于 IOptions 的一些更高级的用法,但在大多数情况下,这不是一个大问题。我通常是这些方法的粉丝,但与往常一样,在全心全意采用它们之前,请考虑一下你失去了什么。

Tip In chapter 31 I show one such advanced scenario in which you configure an IOptions object using services in your DI container. For other scenarios, see Microsoft’s “Options pattern in ASP.NET Core” documentation at http://mng.bz/DR7y, or see the various IOptions posts on my blog, such as this one: http://mng.bz/l1Aj.
提示 在第 31 章中,我将展示一个这样的高级场景,在该场景中,您可以使用 DI 容器中的服务配置 IOptions 对象。对于其他方案,请参阅 http://mng.bz/DR7y 上 Microsoft 的“ASP.NET Core 中的选项模式”文档,或查看我博客上的各种 IOptions 帖子,例如:http://mng.bz/l1Aj

That brings us to the end of this section on strongly typed settings. In section 10.4 we’ll look at how you can change your settings dynamically at runtime, based on the environment in which your app is running.

这将我们带到本节关于强类型设置的结尾。在 Section 10.4 中,我们将了解如何根据应用程序的运行环境在运行时动态更改设置。

10.4 Configuring an application for multiple environments

10.4 为多个环境配置应用程序

In this section you’ll learn about hosting environments in ASP.NET Core. You’ll learn how to set and determine which environment an application is running in and how to change which configuration values are used, based on the environment. Using environments lets you switch easily among different sets of configuration values in production compared with development, for example.

在本节中,您将了解如何在 ASP.NET Core 中托管环境。您将学习如何设置和确定应用程序在哪个环境中运行,以及如何根据环境更改使用的配置值。例如,与开发相比,使用环境可以让您在生产中的不同配置值集之间轻松切换。

Any application that makes it to production will likely have to run in multiple environments. If you’re building an application with database access, for example, you’ll probably have a small database running on your machine that you use for development. In production, you’ll have a completely different database running on a server somewhere else.

任何进入生产环境的应用程序都可能必须在多个环境中运行。例如,如果您正在构建具有数据库访问权限的应用程序,则您的计算机上可能会运行一个用于开发的小型数据库。在生产环境中,您将在其他位置的服务器上运行一个完全不同的数据库。

Another common requirement is to have different amounts of logging depending on where your app is running. In development, it’s great to generate lots of logs, which help with debugging, but when you get to production, too many logs can be overwhelming. You’ll want to log warnings, errors, and maybe information-level logs, but definitely not debug-level logs!

另一个常见要求是,根据应用程序的运行位置,具有不同的日志记录量。在开发中,生成大量日志非常有用,这有助于调试,但是当您进入生产环境时,过多的日志可能会让人不知所措。您需要记录警告、错误,也许还需要信息级日志,但绝对不是调试级日志!

To handle these requirements, you need to make sure that your app loads different configuration values depending on the environment it’s running in: load the production database connection string when in production, and so on. You need to consider three aspects:

要满足这些要求,您需要确保您的应用程序根据其运行环境加载不同的配置值:在生产环境中加载生产数据库连接字符串,依此类推。您需要考虑三个方面:

  • How your app identifies the environment it’s running in
    您的应用如何识别它正在运行的环境

  • How you load different configuration values based on the current environment
    如何根据当前环境加载不同的配置值

  • How to change the environment for a particular machine
    如何更改特定计算机的环境

This section tackles these aspects in turn so that you can easily tell your development machine apart from your production servers and act accordingly.

本节依次处理这些方面,以便您可以轻松地将开发计算机与生产服务器区分开来,并采取相应的措施。

10.4.1 Identifying the hosting environment

10.4.1 确定托管环境

When you create a WebApplicationBuilder instance in Program.cs, it automatically sets up the hosting environment for your application. By default, WebApplicationBuilder uses, perhaps unsurprisingly, an environment variable to identify the current environment. The WebApplicationBuilder looks for a magic environment variable called ASPNETCORE_ENVIRONMENT, uses it to create an IHostEnvironment object, and exposes it as WebApplicationBuilder.Environment.

当您在 Program.cs 中创建 WebApplicationBuilder 实例时,它会自动为您的应用程序设置托管环境。默认情况下,WebApplicationBuilder 使用环境变量来标识当前环境,这也许不足为奇。WebApplicationBuilder 查找一个名为 ASPNETCORE_ENVIRONMENT 的神奇环境变量,使用它来创建一个IHostEnvironment 对象,并将其公开为WebApplicationBuilder.Environment 的 Web 应用程序。

Note You can use either the DOTNET_ENVIRONMENT or ASPNETCOREENVIRONMENT environment variable. The ASPNETCORE value overrides the DOTNET value if both are set. I use the ASPNETCORE version throughout this book.
注意 您可以使用 DOTNET_ENVIRONMENT 或 ASPNETCOREENVIRONMENT 环境变量。如果同时设置了两者,则 ASPNETCORE 值将覆盖 DOTNET_ 值。我在本书中都使用ASPNETCORE_版本。

The IHostEnvironment interface exposes several useful properties about the running context of your app. The ContentRootPath property, for example, tells the application in which directory it can find any configuration files, such as appsettings.json. This folder is typically the one in which the application is running.

IHostEnvironment 接口公开了有关应用程序运行上下文的几个有用属性。例如,ContentRootPath 属性告诉应用程序它可以在哪个目录中找到任何配置文件,比如 appsettings.json。此文件夹通常是运行应用程序的文件夹。

TIP  ContentRootPath is not where you store static files that the browser can access directly; that’s the WebRootPath, typically wwwroot. WebRootPath is also exposed on the Environment property via the IWebHostEnvironment interface.
提示 ContentRootPath 不是存储浏览器可以直接访问的静态文件的位置;即 WebRootPath,通常为 wwwroot。WebRootPath 还通过 IWebHostEnvironment 接口在 Environment 属性上公开。

The IHostEnvironment.EnvironmentName property is what interests us in this section. It’s set to the value of the ASPNETCORE_ENVIRONMENT environment variable, so it can be any value, but you should stick to three commonly used values in most cases:

IHostEnvironment.EnvironmentName 属性是本节中我们感兴趣的内容。它设置为 ASPNETCORE_ENVIRONMENT 环境变量的值,因此它可以是任何值,但在大多数情况下,您应该坚持使用三个常用的值:

  • "Development" 开发
  • "Staging" 暂存
  • "Production" 生产

ASP.NET Core includes several helper methods for working with these three values, so you’ll have an easier time if you stick to them. In particular, whenever you’re testing whether your app is running in a particular environment, you should use one of the following extension methods:

ASP.NET Core 包含多个用于处理这三个值的帮助程序方法,因此,如果坚持使用它们,您将会更轻松。具体而言,每当测试应用程序是否在特定环境中运行时,都应使用以下扩展方法之一:

  • IHostEnvironment.IsDevelopment()
  • IHostEnvironment.IsStaging()
  • IHostEnvironment.IsProduction()
  • IHostEnvironment.IsEnvironment(string environmentName)

All these methods make sure that they do case-insensitive checks of the environment variable, so you won’t get any wonky errors at runtime if you don’t capitalize the environment variable value.

所有这些方法都确保它们对环境变量执行不区分大小写的检查,因此如果不将环境变量值大写,在运行时不会收到任何不稳定的错误。

Tip Where possible, use the IHostEnvironment extension methods instead of direct string comparison with EnvironmentName, as the methods provide case-insensitive matching.
提示 如果可能,请使用 IHostEnvironment 扩展方法,而不是直接与 EnvironmentName 进行字符串比较,因为这些方法提供不区分大小写的匹配。

IHostEnvironment doesn’t do anything other than expose the details of your current environment, but you can use it in various ways. In chapter 4 you saw that WebApplication adds the DeveloperExceptionMiddleware to your middleware pipeline only in the development environment. Now you know where WebApplication was getting its information about the environment: IHostEnvironment.

IHostEnvironment 除了公开当前环境的详细信息外,不执行任何其他作,但您可以通过多种方式使用它。在第 4 章中,您看到 WebApplication 仅在开发环境中将 DeveloperExceptionMiddleware 添加到您的中间件管道中。现在,您知道 WebApplication 从何处获取有关环境的信息:IHostEnvironment。

You can use a similar approach to customize which configuration values you load at runtime by loading different files when running in development versus production. This approach is common; it’s included out of the box in most ASP.NET Core templates and by default when you use the default ConfigurationManager included with WebApplicationBuilder.

您可以使用类似的方法来自定义在运行时加载的配置值,方法是在在开发环境中运行与生产环境。这种方法很常见;它在大多数 ASP.NET Core 模板中都是开箱即用的,并且在您使用 WebApplicationBuilder 中包含的默认 ConfigurationManager 时默认包含。

10.4.2 Loading environment-specific configuration files

10.4.2 加载特定于环境的配置文件

The EnvironmentName value is determined early in the process of bootstrapping your application, before the default ConfigurationManager is fully populated by WebApplicationBuilder. As a result, you can dynamically change which configuration providers are added to the builder and, hence, which configuration values are loaded when the IConfiguration is built.

EnvironmentName 值是在引导应用程序的早期确定的,在默认 ConfigurationManager 由 WebApplicationBuilder 完全填充之前。因此,您可以动态更改将哪些配置提供程序添加到生成器中,从而在构建 IConfiguration 时加载哪些配置值。

A common pattern is to have an optional, environment-specific appsettings .ENVIRONMENT.json file that’s loaded after the default appsettings.json file. The following listing shows how you could achieve this task if you’re customizing the ConfigurationMaanger in Program.cs, but it’s also effectively what WebApplicationBuilder does by default.

一种常见模式是具有可选的、特定于环境的 appsettings 。ENVIRONMENT.json 在默认 appsettings.json 文件之后加载的文件。下面的清单显示了如果您在 Program.cs 中自定义 ConfigurationManager,如何完成此任务,但这也是 WebApplication-Builder 默认所做的。

Listing 10.13 Adding environment-specific appsettings.json files

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

IHostEnvironment env = builder.Environment; ❶

builder.Configuration.Sources.Clear();
builder.Configuration
    .AddJsonFile(
        "appsettings.json",
        optional: false) ❷
    .AddJsonFile( ❸
        $”appsettings.{env.EnvironmentName}.json”, ❸
        Optional: true); ❸

WebApplication app = builder.Build();

app.MapGet("/", () =>"Hello world!");

app.Run();

❶ The current IHostEnvironment is available on WebApplicationBuilder.
当前的 IHostEnvironment 在 WebApplicationBuilder 上可用。
❷ It’s common to make the base appsettings.json compulsory.
将 base appsettings.json 设为必修是很常见的。
❸ Adds an optional environment-specific JSON file where the filename varies with the
environment
添加一个可选的特定于环境的 JSON 文件,其中文件名随环境而变化

With this pattern, a global appsettings.json file contains settings applicable to most environments. Additional optional JSON files called appsettings.Development.json, appsettings.Staging.json, and appsettings.Production.json are subsequently added to ConfigurationManager, depending on the current EnvironmentName.

使用此模式时,全局appsettings.json文件包含适用于大多数环境的设置。名为 appsettings 的其他可选 JSON 文件。Development.json,appsettings。Staging.json 和 appsettings 中。Production.json随后会添加到 ConfigurationManager,具体取决于当前的 EnvironmentName。

Any settings in these files will overwrite values from the global appsettings.json if they have the same key, as you’ve seen previously. Using environment-specific settings files lets you do things like set the logging to be verbose only in the development environment and switch to more selective logs in production.

这些文件中的任何设置都将覆盖全局appsettings.json的值,如果它们具有相同的键,就像您之前看到的那样。使用特定于环境的设置文件,您可以执行一些作,例如在开发环境中将日志记录设置为仅详细,并在生产环境中切换到更具选择性的日志。

Another common pattern is to add or remove configuration providers depending on the environment. You might use the User Secrets provider when developing locally, for example, but Azure Key Vault in production. Listing 10.14 shows how you can use IHostEnvironment to include the User Secrets provider conditionally only in development. Again, WebApplicationBuilder uses this pattern by default.

另一种常见模式是根据环境添加或删除配置提供程序。例如,在本地开发时使用用户机密提供程序,但在生产环境中使用 Azure Key Vault。清单 10.14 展示了如何使用 IHostEnvironment 仅在开发中有条件地包含 User Secrets 提供程序。同样,WebApplicationBuilder 默认使用此模式。

Listing 10.14 Conditionally including the User Secrets configuration provider
清单 10.14 有条件地包含 User Secrets 配置提供程序

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

IHostEnvironment env = builder.Environment;

builder.Configuration.Sources.Clear();
builder.Configuration
    .AddJsonFile(
        "appsettings.json",
        optional: false)
    .AddJsonFile(
        $"appsettings.{env}.json",
        Optional: true);

if(env.IsDevelopment()) ❶
{
    builder.Configuration.AddUserSecrets(); ❷
}

WebApplication app = builder.Build();

app.MapGet("/", () =>"Hello world!");

app.Run();

❶ Extension methods make checking the environment simple and explicit.
将问题详情服务添加到 DI 容器中,供 ExceptionHandlerMiddleware 使用
❷ In Staging and Production, the User Secrets provider won’t be used.
当不在开发中时,管道使用 ExceptionHandlerMiddleware。

As already mentioned, it’s also common to customize your application’s middleware pipeline based on the environment. In chapter 4 you learned that WebApplication adds the DeveloperExceptionPageMiddleware conditionally when developing locally. The following listing shows how you can use IHostEnvironment to control your pipeline in this way so that when you’re in staging or production, your app uses ExceptionHandlerMiddleware instead.

如前所述,根据环境自定义应用程序的中间件管道也很常见。在第 4 章中,您了解了 WebApplication 在本地开发时有条件地添加 DeveloperExceptionPageMiddleware。下面的清单显示了如何以这种方式使用 IHostEnvironment 来控制管道,以便在暂存或生产环境中,应用程序改用 ExceptionHandlerMiddleware。

Listing 10.15 Using the hosting environment to customize your middleware pipeline
清单 10.15 使用托管环境自定义中间件管道

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.AddProblemDetails(); ❶

WebApplication app = builder.Build();

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

app.MapGet("/", () =>"Hello world!");

app.Run();

❶ Adds the problem details service to the DI container for use by the ExceptionHandlerMiddleware
将问题详情服务添加到 DI 容器中,供 ExceptionHandlerMiddleware 使用
❷ When not in development, the pipeline uses ExceptionHandlerMiddleware.
当不在开发中时,管道使用 ExceptionHandlerMiddleware。

Note In listing 10.15 you added the Problem Details services to the DI container so that the ExceptionHandlerMiddleware can generate a Problem Details response automatically. As you’re adding the extra middleware only in Staging and Production, you could add the services conditionally to the DI container too instead of always adding them as we did here.
注意 在示例 10.15 中,你向 DI 容器添加了 Problem Details 服务,以便 ExceptionHandlerMiddleware 可以自动生成 Problem Details 响应。由于您仅在 Staging 和 Production 中添加额外的中间件,因此您也可以有条件地将服务添加到 DI 容器中,而不是像我们在这里所做的那样总是添加它们。

You can inject IHostEnvironment anywhere in your app, but I advise against using it in your own services outside Program.cs. It’s far better to use the configuration providers to customize strongly typed settings based on the current hosting environment and inject these settings into your application instead.

您可以在应用程序中的任何位置注入 IHostEnvironment,但我建议不要在 Program.cs 之外的您自己的服务中使用它。最好使用配置提供程序根据当前托管环境自定义强类型设置,并将这些设置注入到应用程序中。

As useful as it is, setting IHostEnvironment with an environment variable can be a little cumbersome if you want to switch back and forth among different environments during testing. Personally, I’m always forgetting how to set environment variables on the various operating systems I use. The final skill I’d like to teach you is how to set the hosting environment when you’re developing locally.

尽管它很有用,但如果您想在测试期间在不同环境之间来回切换,则使用环境变量设置 IHostEnvironment 可能会有点麻烦。就我个人而言,我总是忘记如何在我使用的各种作系统上设置环境变量。最终技能我想教你在本地开发时如何设置托管环境。

10.4.3 Setting the hosting environment

10.4.3 设置托管环境

In this section I show you a couple of ways to set the hosting environment when you’re developing. These techniques make it easy to test a specific app’s behavior in different environments without having to change the environment for all the apps on your machine.

在本节中,我将向您展示在开发时设置托管环境的几种方法。通过这些技术,可以轻松测试特定应用程序在不同环境中的行为,而无需更改计算机上所有应用程序的环境。

If your ASP.NET Core application can’t find an ASPNETCORE_ENVIRONMENT environment variable when it starts up, it defaults to a production environment, as shown in figure 10.6. So when you deploy to production, you’ll be using the correct environment by default.

如果您的 ASP.NET Core 应用程序在启动时找不到 ASPNETCORE_ENVIRONMENT 环境变量,则默认使用生产环境,如图 10.6 所示。因此,当您部署到生产环境时,默认情况下将使用正确的环境。

alt text

Figure 10.6 By default, ASP.NET Core applications run in the production hosting environment. You can override this default by setting the ASPNETCORE_ENVIRONMENT variable.

图 10.6 默认情况下,ASP.NET Core 应用程序在生产托管环境中运行。您可以覆盖此默认值通过设置 ASPNETCORE_ENVIRONMENT 变量。

Tip By default, the current hosting environment is logged to the console at startup, which can be useful for checking that the environment variable has been picked up correctly.
提示 默认情况下,当前托管环境在启动时记录到控制台中,这对于检查是否已正确选取环境变量非常有用。

Another option is to use a launchSettings.json file to control the environment. All the default ASP.NET Core applications include this file in the Properties folder. LaunchSettings.json defines profiles for running your application.

另一种选择是使用 launchSettings.json 文件来控制环境。所有默认的 ASP.NET Core 应用程序都将此文件包含在 Properties 文件夹中。LaunchSettings.json 定义用于运行应用程序的配置文件。

Tip You can use profiles to run your application with different environment variables. You can also use profiles to emulate running on Windows behind Internet Information Services (IIS) by using the IIS Express profile. I rarely use this profile, even in Windows, and always choose the http or https profile.
提示 您可以使用配置文件来运行具有不同环境变量的应用程序。您还可以使用配置文件来模拟使用 IIS Express 配置文件在 Internet Information Services (IIS) 后面的 Windows 上运行。我很少使用此配置文件,即使在 Windows 中也是如此,并且总是选择 http 或 https 配置文件。

Listing 10.16 shows a typical launchSettings.json file that defines three profiles: http, https, and IIS Express. The first two profiles are equivalent to using dotnet run to run the project. The http profile listens only for http:// requests, whereas https listens for both http:// and https://. The IIS Express profile can be used only in Windows and uses IIS Express to run your application.

清单 10.16 显示了一个典型的 launchSettings.json 文件,它定义了三个配置文件:http、https 和 IIS Express。前两个配置文件等效于使用 dotnet run 运行项目。http 配置文件仅侦听 http:// 请求,而 https 同时侦听 http:// 和 https://。IIS Express 配置文件只能在 Windows 中使用,并使用 IIS Express 运行应用程序。

Listing 10.16 A typical launchSettings.json file defining three profiles
清单 10.16 定义三个配置文件的典型 launchSettings.json 文件

{
    "iisSettings": { ❶
        "windowsAuthentication": false, ❶
        "anonymousAuthentication": true, ❶
        "iisExpress": { ❶
            "applicationUrl": "http://localhost:53846", ❶
            "sslPort": 44399 ❶
            }
            },
            "profiles": {
            "http": { ❷
                "commandName": "Project", ❸
                "dotnetRunMessages": true, ❹
                "launchBrowser": true, ❺
                "applicationUrl": "http://localhost:5063", ❻
                "environmentVariables": { ❼
                "ASPNETCORE_ENVIRONMENT": "Development" ❼
            } ❼
            },
            "https": { ❽
                "commandName": "Project",
                "dotnetRunMessages": true,
                "launchBrowser": true,
                "applicationUrl": "https://localhost:7202;http://localhost:5063", ❾
                "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            }
            },
                "IIS Express": { ❿
                "commandName": "IISExpress",
                "launchBrowser": true,
                "environmentVariables": { ⓫
                "ASPNETCORE_ENVIRONMENT": "Development" ⓫
            } ⓫
        }
    }
}

❶ Defines settings for running behind IIS or using the IIS Express profile
定义在 IIS 后面运行或使用 IIS Express 配置文件的设置
❷ The “http” profile is used by default in macOS.
macOS 中默认使用 “http” 配置文件。
❸ The “project” command is equivalent to calling dotnet run on the project.
“project” 命令等效于在项目上调用 dotnet run。
❹ If true, gives feedback when dotnet run is executing a build or restore
如果为 true,则在 dotnet run 执行生成或还原时提供反馈
❺ If true, launches the browser when you run the application
如果为 true,则在运行应用程序时启动浏览器
❻ Defines the URLs the application will listen on in this profile
定义应用程序将在此配置文件中侦听的 URL
❼ Defines custom environment variables for the profile and sets the environment to
Development
为配置文件定义自定义环境变量,并将环境设置为 Development
❽ The https profile is used by default in Visual Studio in Windows.
默认情况下,https 配置文件在 Windows 的 Visual Studio 中使用。
❾ The https profile listens on both http:// and https:// URLs.
https 配置文件同时侦听 http:// 和 https:// URL。
❿ Runs the application behind IIS Express (Windows only)
在 IIS Express 后面运行应用程序(仅限 Windows)
⓫ Each profile can have different environment variables.
每个配置文件可以具有不同的环境变量。

The advantage of using the launchSettings.json file locally is that it allows you to set local environment variables for a project. In listing 10.16 the environment is set to the development environment, for example. Setting environment variables with launchSettings.json means you can use different environment variables for each project and even for each profile, and store them in source control.

在本地使用 launchSettings.json 文件的优点是,它允许为项目设置本地环境变量。例如,在列表 10.16 中,环境被设置为开发环境。使用 launchSettings.json 设置环境变量意味着您可以为每个项目甚至每个配置文件使用不同的环境变量,并将它们存储在源代码控制中。

You can choose a profile to use in Visual Studio by choosing it from the drop-down menu next to the Debug button on the toolbar, as shown in figure 10.7. You can choose a profile to run from the command line by using `dotnet run --launch-profile <Profile Name>`. If you don’t specify a profile, the first profile listed in launchSettings.json is used. If you don’t want to use any profile, you must explicitly ignore the launchSettings.json file by using dotnet run --no-launch-profile.

您可以从工具栏上 Debug (调试) 按钮旁边的下拉菜单中选择要在 Visual Studio 中使用的配置文件,如图 10.7 所示。您可以使用 `dotnet run --launch- profile <Profile Name>`从命令行选择要运行的配置文件。如果未指定配置文件,则使用 launchSettings .json中列出的第一个配置文件。如果不想使用任何配置文件,则必须使用 dotnet run --no-launch- profile 显式忽略 launchSettings.json 文件。

alt text

Figure 10.7 You can select the profile to use from Visual Studio by choosing it from the Debug drop-down menu. Visual Studio defaults to using the https profile.
图 10.7 您可以从 Debug 下拉菜单中选择要使用的配置文件,从 Visual Studio 中选择它。Visual Studio 默认使用 https 配置文件。

If you’re using Visual Studio, you can edit the launchSettings.json file visually: double-click the Properties node, choose the Debug tab, and choose Open debug launch profiles UI. You can see in figure 10.8 that the ASPNETCORE_ENVIRONMENT is set to Development; any changes made on this tab are mirrored in launchSettings.json.

如果您使用的是 Visual Studio,则可以直观地编辑 launchSettings.json 文件:双击 Properties (属性) 节点,选择 Debug (调试) 选项卡,然后选择 Open debug launch profiles UI (打开调试启动配置文件 UI)。在图 10.8 中可以看到,ASPNETCORE_ENVIRONMENT 设置为 Development;在此选项卡上所做的任何更改都会镜像到 launchSettings.json 中。

alt text

Figure 10.8 You can use Visual Studio to edit the launchSettings.json file, if you prefer. Changes will be mirrored between the launchSettings.json file and the Properties dialog box.
图 10.8 如果您愿意,可以使用 Visual Studio 编辑 launchSettings.json 文件。更改将在 launchSettings.json 文件和 Properties (属性) 对话框之间镜像。

The launchSettings.json file is intended for local development only; by default, the file isn’t deployed to production servers. Although you can deploy and use the file in production, doing so generally isn’t worth the hassle. Environment variables are a better fit.
launchSettings.json 文件仅用于本地开发;默认情况下,文件不会部署到生产服务器。尽管您可以在生产环境中部署和使用该文件,但这样做通常不值得。环境变量更合适。

One final trick I’ve used to set the environment in production is to use command-line arguments. You could set the environment to staging like this:

我在生产环境中设置环境时使用的最后一个技巧是使用命令行参数。您可以将环境设置为 stage,如下所示:

dotnet run --no-launch-profile --environment Staging

Note that you also have to pass --no-launch-profile if there’s a launchSettings.json file; otherwise, the values in the file take precedence.
注意 如果有 launchSettings.json 文件,您还必须传递 --no-launch-profile;否则,文件中的值优先。

That brings us to the end of this chapter on configuration. Configuration isn’t glamorous, but it’s an essential part of all apps. The ASP.NET Core configuration provider model handles a wide range of scenarios, letting you store settings and secrets in a variety of locations.

这让我们结束了本章关于配置的章节。配置并不迷人,但它是所有应用程序的重要组成部分。ASP.NET Core 配置提供程序模型可处理各种方案,让您能够在各种位置存储设置和机密。

Simple settings can be stored in appsettings.json, where they’re easy to tweak and modify during development, and they can be overwritten by using environment-specific JSON files. Meanwhile, your secrets and sensitive settings can be stored outside the project file in the User Secrets manager or as environment variables. This approach gives you both flexibility and safety—as long as you don’t go writing your secrets to appsettings.json!

简单的设置可以存储在 appsettings.json 中,在开发过程中很容易调整和修改,并且可以使用特定于环境的 JSON 文件覆盖它们。同时,您的 Secret 和敏感设置可以存储在 User Secrets Manager 中的项目文件外部或作为环境变量存储。这种方法为您提供了灵活性和安全性 - 只要您不将您的秘密写给 appsettings.json!

In chapter 11 we take a look at the OpenAPI specification and how you can use it for documenting your APIs, testing your endpoints, and generating strongly typed clients.

在第 11 章中,我们将了解 OpenAPI 规范,以及如何使用它来记录 API、测试端点和生成强类型客户端。

10.5 Summary

10.5 总结

Anything that could be considered to be a setting or a secret is normally stored as a configuration value. Externalizing these values means that you can change them without recompiling your app.
任何可被视为设置或密钥的内容通常存储为配置值。外部化这些值意味着您无需重新编译应用程序即可更改它们。

ASP.NET Core uses configuration providers to load key-value pairs from a variety of sources. Applications can use many configuration providers.
ASP.NET Core 使用配置提供程序从各种来源加载键值对。应用程序可以使用许多配置提供程序。

You can add configuration providers to an instance of ConfigurationManager by using extension methods such as AddJsonFile().
您可以使用扩展方法(如 AddJsonFile())将配置提供程序添加到 ConfigurationManager 实例。

The order in which you add providers to ConfigurationManager is important; subsequent providers replace the values of the same settings defined in earlier providers while preserving unique settings.
向 ConfigurationManager 添加提供程序的顺序很重要;后续提供程序将替换早期提供程序中定义的相同设置的值,同时保留唯一设置。

ASP.NET Core includes built-in providers for JSON files, XML files, environment files, and command-line arguments, among others. NuGet packages exist for many other providers, such as YAML files and Azure Key Vault.
ASP.NET Core 包括用于 JSON 文件、XML 文件、环境文件和命令行参数等的内置提供程序。NuGet 包适用于许多其他提供程序,例如 YAML 文件和 Azure Key Vault。

ConfigurationManager implements IConfiguration as well as IConfigurationBuilder, so you can retrieve configuration values from it directly.
ConfigurationManager 实现 IConfiguration 和 IConfigurationBuilder,因此您可以直接从中检索配置值。

Configuration keys aren’t case-sensitive, so you must take care not to lose values when loading settings from case-sensitive sources such as YAML.
配置键不区分大小写,因此在从区分大小写的源(如 YAML)加载设置时,必须注意不要丢失值。

You can retrieve settings from IConfiguration directly by using the indexer syntax, such as Configuration["MySettings:Value"]. This technique is often useful for accessing configuration values in Program.cs.
可以使用索引器语法(如 Configuration[“MySettings:Value”])直接从 IConfiguration 检索设置。此方法通常用于访问 Program.cs 中的配置值。

WebApplicationBuilder automatically configures a ConfigurationManager with JSON, environment variables, command-line arguments, and User Secret providers. This combination provides in-repository storage in JSON files, secret storage in both development and production, and the ability to override settings easily at runtime.
WebApplicationBuilder 使用 JSON、环境变量、命令行参数和用户密钥提供程序自动配置 ConfigurationManager。这种组合在 JSON 文件中提供存储库内存储、开发和生产中的秘密存储,以及在运行时轻松覆盖设置的能力。

In production, store secrets in environment variables to reduce the chance of incorrectly exposing the secrets in your code repository. These secrets can be loaded after your file-based settings in the configuration builder.
在生产环境中,将密钥存储在环境变量中,以减少在代码存储库中错误地暴露密钥的可能性。这些密钥可以在配置生成器中基于文件的设置之后加载。

On development machines, the User Secrets Manager is a more convenient tool than using environment variables. It stores secrets in your operating system’s user profile, outside the project folder, reducing the risk of accidentally exposing secrets in your code repository.
在开发计算机上,User Secrets Manager 是比使用环境变量更方便的工具。它将密钥存储在作系统的用户配置文件中,位于项目文件夹之外,从而降低了在代码存储库中意外暴露密钥的风险。

Be aware that neither environment variables nor the User Secrets Manager tool encrypts secrets. They merely store them in locations that are less likely to be made public, as they’re outside your project folder.
请注意,环境变量和 User Secrets Manager 工具都不会加密密钥。它们仅将它们存储在不太可能公开的位置,因为它们位于工程文件夹之外。

File-based providers such as the JSON provider can reload configuration values automatically when the file changes, allowing you to update configuration values in real time without restarting your app.
基于文件的提供程序(如 JSON 提供程序)可以在文件更改时自动重新加载配置值,从而允许您实时更新配置值,而无需重新启动应用程序。

Use strongly typed POCO options classes to access configuration in your app. Using strongly typed options reduces coupling in your app and ensures that classes are dependent only on the configuration values they use.
使用强类型 POCO 选项类访问应用程序中的配置。使用强类型选项可以减少应用程序中的耦合,并确保类仅依赖于它们使用的配置值。

Use the Configure<T>() extension method in ConfigureServices to bind your POCO options objects to ConfigurationSection. Alternatively, you can configure IOptions<T> objects in code instead of using configuration values by passing a lambda to the Configure() method.
使用 ConfigureServices 中的 Configure<T>() 扩展方法将 POCO 选项对象绑定到 ConfigurationSection。或者,您也可以在代码中配置 IOptions<T> 对象,而不是通过将 lambda 传递给 Configure() 方法来使用配置值。

You can inject the IOptions<T> interface into your services by using DI. You can access the strongly typed options object on the Value property. IOptions<T> values are registered in DI as singletons, so they remain the same even if the underlying configuration changes.
您可以通过 DI 将 IOptions<T> 接口注入到您的服务中。您可以在 Value 属性上访问强类型 options 对象。IOptions<T> 值在 DI 中注册为单一实例,因此即使底层配置发生更改,它们也保持不变。

If you want to reload your POCO options objects when your configuration changes, use the IOptionsSnapshot<T> interface instead. These instances are registered in DI with a scoped lifetime, so they’re re-created for every request. Using the IOptionsSnapshot<T> interface has performance implications due to binding to the options object repeatedly, so use it only when that effect is acceptable.
如果要在配置更改时重新加载 POCO 选项对象,请改用 IOptionsSnapshot<T> 接口。这些实例在 DI 中注册,具有作用域生命周期,因此会为每个请求重新创建它们。由于重复绑定到 options 对象,因此使用 IOptionsSnapshot<T> 接口会影响性能,因此请仅在该成本可接受时使用它。

Applications running in different environments, such as development versus production , often require different configuration values. ASP.NET Core determines the current hosting environment by using the ASPNETCORE_ENVIRONMENT environment variable. If this variable isn’t set, the environment is assumed to be production.
在不同环境(例如开发环境与生产环境)中运行的应用程序通常需要不同的配置值。ASP.NET Core 使用 ASPNETCORE_ENVIRONMENT 环境变量确定当前托管环境。如果未设置此变量,则假定环境为 production。

You can set the hosting environment locally by using the launchSettings.json file, which allows you to scope environment variables to a specific project.
您可以使用 launchSettings.json 文件在本地设置托管环境,该文件允许您将环境变量的范围限定为特定项目。

The current hosting environment is exposed as an IHostEnvironment interface. You can check for specific environments by using IsDevelopment(), IsStaging(), and IsProduction(). Then you can use the IHostEnvironment object to load files specific to the current environment, such as appsettings.Production.json.
当前托管环境作为 IHostEnvironment 接口公开。您可以使用 IsDevelopment()、IsStaging() 和 IsProduction() 检查特定环境。然后,您可以使用 IHostEnvironment 对象加载特定于当前环境的文件,例如 appsettings。Production.json。

ASP.NET Core in Action 9 Registering services with dependency injection

9 Registering services with dependency injection
9 使用依赖注入注册服务

This chapter covers

本章涵盖

  • Configuring your services to work with dependency injection
    配置服务以使用依赖关系注入

  • Choosing the correct lifetime for your services
    为您的服务选择正确的生命周期

In chapter 8 you learned about dependency injection (DI) in general, why it’s useful as a pattern for developing loosely coupled code, and its central place in ASP.NET Core. In this chapter you’ll build on that knowledge to apply DI to your own classes.

在第 8 章中,您了解了依赖项注入 (DI) 的一般知识,为什么它作为开发松散耦合代码的模式很有用,以及它在 ASP.NET Core 中的核心位置。在本章中,您将基于这些知识将 DI 应用于您自己的类。

You’ll start by learning how to configure your app so that the ASP.NET Core framework can create your classes for you, removing the pain of having to create new objects manually in your code. We look at the various patterns you can use to register your services and some of the limitations of the built-in DI container.

首先,您将学习如何配置您的应用程序,以便 ASP.NET Core 框架可以为您创建类,从而消除必须在代码中手动创建新对象的痛苦。我们来看看你可以用来注册服务的各种模式,以及内置 DI 容器的一些限制。

Next, you’ll learn how to handle multiple implementations of a service. You’ll learn how to inject multiple versions of a service, how to override a default service registration, and how to register a service conditionally if you don’t know whether it’s already registered.

接下来,您将学习如何处理服务的多个实现。您将学习如何注入服务的多个版本,如何覆盖默认服务注册,以及如何在不知道服务是否已注册时有条件地注册服务。

In section 9.4 we look at how you can control how long your objects are used—that is, their lifetime. We explore the differences among the three lifetime options and some of the pitfalls to be aware of when you come to write your own applications. Finally, in section 9.5 you’ll learn why lifetimes are important when resolving services outside the context of an HTTP request.

在 9.4 中,我们将了解如何控制对象的使用时间 — 即它们的生命周期。我们探讨了三种生命周期选项之间的差异,以及编写自己的应用程序时需要注意的一些陷阱。最后,在 9.5 节中,您将了解为什么在 HTTP 请求上下文之外解析服务时生命周期很重要。

We’ll start by revisiting the EmailSender service from chapter 8 to see how you could register the dependency graph in your DI container.

首先,我们将重新审视第 8 章中的 EmailSender 服务,了解如何在 DI 容器中注册依赖关系图。

9.1 Registering custom services with the DI container

9.1 向 DI 容器注册自定义服务

In this section you’ll learn how to register your own services with the DI container. We’ll explore the difference between a service and an implementation, and learn how to register the EmailSender hierarchy introduced in chapter 8.

在本节中,您将学习如何向 DI 容器注册自己的服务。我们将探讨服务和实现之间的区别,并学习如何注册第 8 章中介绍的 EmailSender 层次结构。

In chapter 8 I described a system for sending emails when a new user registers in your application. Initially, the minimal API endpoint handler RegisterUser created an instance of EmailSender manually, using code similar to the following listing (which you saw in chapter 8).

在第 8 章中,我描述了一个当新用户在您的应用程序中注册时发送电子邮件的系统。最初,最小 API 端点处理程序 RegisterUser 使用类似于以下清单的代码手动创建了一个 EmailSender 实例(您在第 8 章中看到)。

Listing 9.1 Creating an EmailSender instance without dependency injection
示例 9.1 创建无依赖注入的实例EmailSender

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

app.MapGet("/register/{username}", RegisterUser); ❶

app.Run();

string RegisterUser(string username)
{
    IEmailSender emailSender = new EmailSender( ❷
        new MessageFactory(), ❸
        new NetworkClient( ❹
            new EmailServerSettings ❺
                ( ❺
                    Host: "smtp.server.com", ❺
                    Port: 25 ❺
                )) ❺
            );
    emailSender.SendEmail(username); ❻
    return $"Email sent to {username}!";
}

❶ The endpoint is called when a new user is created.
创建新用户时调用 endpoint。

❷ To create EmailSender, you must create all its dependencies.
要创建 EmailSender,您必须创建其所有依赖项。

❸ You need a new MessageFactory.

您需要一个新的 MessageFactory。

❹ The NetworkClient also has dependencies.
NetworkClient 也有依赖项。

❺ You’re already two layers deep, but there could feasibly be more.
您已经有两层了,但可能还有更多。

❻ Finally, you can send the email.
最后,您可以发送电子邮件。

We subsequently refactored this code to inject an instance of IEmailSender into the handler instead, as shown in listing 9.2. The IEmailSender interface decouples the endpoint handler from the EmailSender implementation, making it easier to change the implementation of EmailSender (or replace it) without having to rewrite RegisterUser.

我们随后重构了这段代码,以注入一个IEmailSender 添加到处理程序中,如清单所示
9.2. IEmailSender 接口将端点处理程序与 EmailSender 实现解耦,从而更容易更改 mailSender 的实现(或替换它),而无需重写 RegisterUser。

Listing 9.2 Using IEmailSender with dependency injection in an endpoint handler
清单 9.2 使用带有依赖注入的 IEmailSender在端点处理程序中

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

app.MapGet("/register/{username}", RegisterUser); ❶

app.Run();

string RegisterUser(string username, IEmailSender emailSender) ❷
{
    emailSender.SendEmail(username); ❸
    return $"Email sent to {username}!";
}

❶ The endpoint is called when a new user is created.
创建新用户时调用 endpoint。

❷ The IEmailSender is injected into the handler using DI.
IEmailSender 使用 DI 注入处理程序。

❸ The handler uses the IEmailSender instance.
处理程序使用 IEmailSender 实例。

The final step in making the refactoring work is configuring your services with the DI container. This configuration lets the DI container know what to use when it needs to fulfill the IEmailSender dependency. If you don’t register your services, you’ll get an exception at runtime, like the one in figure 9.1. This exception describes a model-binding problem; the minimal API infrastructure tries to bind the emailSender parameter to the request body because IEmailSender isn’t a known service in the DI container.

进行重构的最后一步是使用 DI 容器配置您的服务。此配置使 DI 容器知道在需要满足 IEmailSender 依赖项时要使用什么。如果您不注册您的服务,您将在运行时收到异常,如图 9.1 中所示。此异常描述了模型绑定问题;最小 API 基础结构尝试将 emailSender 参数绑定到请求正文,因为 IEmailSender 不是 DI 容器中的已知服务。

alt text

Figure 9.1 If you don’t register all your required dependencies with the DI container, you’ll get an exception at runtime, telling you which service wasn’t registered.
图 9.1 如果你没有向 DI 容器注册所有需要的依赖项,你会在运行时收到一个异常,告诉你哪个服务没有注册。

To configure the application completely, you need to register an IEmailSender implementation and all its dependencies with the DI container, as shown in figure 9.2.

要完全配置应用程序,您需要向 DI 容器注册一个 IEmailSender 实现及其所有依赖项,如图 9.2 所示。

alt text

Figure 9.2 Configuring the DI container in your application involves telling it what type to use when a given service is requested, such as “Use EmailSender when IEmailSender is required.”
图 9.2 在应用程序中配置 DI 容器包括告诉它在请求给定服务时使用什么类型,例如“当需要 IEmailSender 时使用 EmailSender”。

Configuring DI consists of making a series of statements about the services in your app, such as the following:

配置 DI 包括对应用程序中的服务进行一系列声明,例如:

  • When a service requires IEmailSender, use an instance of EmailSender.
    当服务需要 IEmailSender 时,请使用 EmailSender 的实例。

  • When a service requires NetworkClient, use an instance of NetworkClient.
    当服务需要 NetworkClient 时,请使用 NetworkClient 的实例。

  • When a service requires MessageFactory, use an instance of MessageFactory.
    当服务需要 MessageFactory 时,请使用 MessageFactory 的实例。

Note You’ll also need to register the EmailServerSettings object with the DI container. We’ll do that slightly differently in section 9.2.
注意 您还需要向 DI 容器注册 EmailServerSettings 对象。我们将在 9.2 节中略微不同地执行此作。

These statements are made by calling various Add methods on the IServiceCollection exposed as the WebApplicationBuilder.Services property. Each Add method provides three pieces of information to the DI container:

这些语句是通过对作为 WebApplicationBuilder.Services 属性公开的 IServiceCollection 调用各种 Add 方法来进行的。每个 Add 方法都向 DI 容器提供三条信息:

  • Service type—TService. This class or interface will be requested as a dependency. It’s often an interface, such as IEmailSender, but sometimes a concrete type, such as NetworkClient or MessageFactory.
    服务类型 — TService。此类或接口将作为依赖项请求。它通常是一个接口,如 IEmailSender,但有时是一个具体类型,如 NetworkClient 或 MessageFactory。

  • Implementation type—TService or TImplementation. The container should create this class to fulfill the dependency. It must be a concrete type, such as EmailSender. It may be the same as the service type, as for NetworkClient and MessageFactory.
    实现类型 - TService 或 TImplementation。容器应创建此类以满足依赖项。它必须是具体类型,例如 EmailSender。它可能与 NetworkClient 和 MessageFactory 的服务类型相同。

  • Lifetime—transient, singleton, or scoped. The lifetime defines how long an instance of the service should be used by the DI container. I discuss lifetimes in detail in section 9.4.
    Definition
    生存期 - 瞬态、单例或范围。生存期定义 DI 容器应使用服务实例的时间。我在 9.4 节中详细讨论了寿命。

DEFINITION A concrete type is a type that can be created, such as a standard class or struct. It contrasts with a type such as an interface or an abstract class, which can’t be created directly.
定义 具体类型是可以创建的类型,例如标准类或结构。它与 interface 或抽象类等类型形成对比,后者无法直接创建。

Listing 9.3 shows how you can configure EmailSender and its dependencies in your application by using three methods: AddScoped<TService>, AddSingleton<TService>, and AddScoped<TService, TImplementation>. This code tells the DI container how to create each of the TService instances when they’re required and which lifetime to use.

清单 9.3 展示了如何使用三种方法在应用程序中配置 EmailSender 及其依赖项:AddScoped<TService>AddSingleton<TService>AddScoped<TService、TImplementation>。此代码告诉 DI 容器如何在需要时创建每个 TService 实例以及要使用的生命周期。

Listing 9.3 Registering services with the DI container
清单 9.3 使用 DI 容器注册服务

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IEmailSender, EmailSender>(); ❶
builder.Services.AddScoped<NetworkClient>(); ❷
builder.Services.AddSingleton<MessageFactory>(); ❸

WebApplication app = builder.Build();

app.MapGet("/register/{username}", RegisterUser);

app.Run();

string RegisterUser(string username, IEmailSender emailSender)
{
    emailSender.SendEmail(username);
    return $"Email sent to {username}!";
}

❶ Whenever you require an IEmailSender, use EmailSender.
每当需要 IEmailSender 时,请使用 EmailSender。

❷ Whenever you require a NetworkClient, use NetworkClient.
每当您需要 NetworkClient 时,请使用 NetworkClient。

❸ Whenever you require a MessageFactory, use MessageFactory.
每当你需要 MessageFactory 时,请使用 MessageFactory。

That’s all there is to DI! It may seem a little bit like magic, but you’re simply giving the container instructions for making all the parts. You give it a recipe for cooking the chili, shred the lettuce, and grate the cheese, so when you ask for a burrito, it can put all the parts together and hand you your meal!

这就是 DI 的全部内容!这可能看起来有点像魔术,但您只是在给容器提供制作所有部件的说明。你给它一个烹饪辣椒、切碎生菜和磨碎奶酪的食谱,这样当你要墨西哥卷饼时,它可以把所有部分放在一起,然后把你的饭菜递给你!

Note Under the hood, the built-in ASP.NET Core DI container uses optimized reflection to create dependencies, but different DI containers may use other approaches. The Add APIs are the only way to register dependencies with the built-in container; there’s no support for using external configuration files to configure the container, for example.
注意 在后台,内置的 ASP.NET Core DI 容器使用优化的反射来创建依赖项,但不同的 DI 容器可能会使用其他方法。Add
API 是向内置容器注册依赖项的唯一方法;例如,不支持使用外部配置文件来配置容器。

The service type and implementation type are the same for NetworkClient and MessageFactory, so there’s no need to specify the same type twice in the AddScoped method—hence, the slightly simpler signature.

NetworkClient 和 MessageFactory 的服务类型和实现类型相同,因此无需在 AddScoped 方法中两次指定相同的类型,因此签名稍微简单一些。

Note The EmailSender instance is registered only as an IEmailSender, so you can’t resolve it by requesting the specific EmailSender implementation; you must use the IEmailSender interface.
注意 EmailSender 实例仅注册为 IEmailSender,因此您无法通过请求特定的 EmailSender 实现来解析它;您必须使用 IEmailSender 接口。

These generic methods aren’t the only ways to register services with the container. You can also provide objects directly or by using lambdas, as you’ll see in section 9.2.

这些泛型方法并不是向容器注册服务的唯一方法。您也可以直接或使用 lambda 提供对象,如第 9.2 节所示。

9.2 Registering services using objects and lambdas

9.2 使用对象和 lambda 注册服务

As I mentioned in section 9.1, I didn’t quite register all the services required by EmailSender. In the previous examples, NetworkClient depends on EmailServerSettings, which you’ll also need to register with the DI container for your project to run without exceptions.

正如我在 9.1 节中提到的,我没有完全注册 EmailSender 所需的所有服务。在前面的示例中,NetworkClient 依赖于 EmailServerSettings,您还需要向 DI 容器注册它,以便您的项目能够无异常地运行。

I avoided registering this object in the preceding example because you have to take a slightly different approach. The preceding Add* methods use generics to specify the Type of the class to register, but they don’t give any indication of how to construct an instance of that type. Instead, the container makes several assumptions that you have to adhere to:

在前面的示例中,我避免注册此对象,因为您必须采用略有不同的方法。前面的 Add* 方法使用泛型指定要注册的类的 Type,但它们没有指示如何构造该类型的实例。相反,容器会做出几个您必须遵守的假设:

  • The class must be a concrete type.
    该类必须是具体类型。

  • The class must have only a single relevant constructor that the container can use.
    该类必须只有一个容器可以使用的相关构造函数。

  • For a constructor to be valid, all constructor arguments must be registered with the container or must be arguments with a default value.
    要使构造函数相关,所有构造函数参数都必须注册到容器中,或者必须是具有默认值的参数。

Note These limitations apply to the simple built-in DI container. If you choose to use a third-party container in your app, it may have a different set of limitations.
注意 这些限制适用于简单的内置 DI 容器。如果您选择在应用程序中使用第三方容器,则它可能具有一组不同的限制。

The EmailServerSettings record doesn’t meet these requirements, as it requires you to provide a Host and Port in the constructor, which are a string and int, respectively, without default values:

EmailServerSettings 记录不满足这些要求,因为它要求您在构造函数中提供 Host 和 Port,它们分别是 string 和 int,没有默认值:

public record EmailServerSettings(string Host, int Port);

You can’t register these primitive types in the container. It would be weird to say “For every string constructor argument, in any type, use the "smtp.server.com" value.”

不能在容器中注册这些基元类型。如果说“对于任何类型的每个字符串构造函数参数,请使用 ”smtp.server.com“ 值,那就很奇怪了。

Instead, you can create an instance of the EmailServerSettings object yourself and provide that to the container, as shown in the following listing. The container uses the preconstructed object whenever an instance of the EmailServerSettings object is required.

相反,您可以自己创建 EmailServerSettings 对象的实例,并将其提供给容器,如下面的清单所示。每当需要 EmailServerSettings 对象的实例时,容器都会使用预构造的对象。

Listing 9.4 Providing an object instance when registering services
示例 9.4 在注册服务时提供对象实例

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IEmailSender, EmailSender>();
builder.Services.AddScoped<NetworkClient>();
builder.Services.AddSingleton<MessageFactory>();
builder.Services.AddSingleton(
        new EmailServerSettings ❶
        ( ❶
            Host: "smtp.server.com", ❶
            Port: 25 ❶
        )); ❶

WebApplication app = builder.Build();

app.MapGet("/register/{username}", RegisterUser);

app.Run();

❶ This instance of EmailServerSettings will be used whenever an instance is
required.
每当需要实例时,都会使用这个 EmailServerSettings 实例。

This code works fine if you want to have only a single instance of EmailServerSettings in your application; the same object will be shared everywhere. But what if you want to create a new object each time one is requested?

如果您只想在应用程序中只有一个 EmailServerSettings 实例,则此代码可以正常工作;同一对象将在任何地方共享。但是,如果您想在每次请求时都创建一个新对象,该怎么办?

Note When the same object is used whenever it’s requested, it’s known as a singleton. If you create an object and pass it to the container, it’s always registered as a singleton. You can also register any class using the AddSingleton<T>() method, and the container will use only one instance throughout your application. I discuss singletons and other lifetimes in detail in section 9.4. The lifetime is how long the DI container should use a given object to fulfill a service’s dependencies.
注意 当请求时使用相同的对象时,它称为单一实例。如果您创建一个对象并将其传递给容器,则它始终注册为单一实例。您还可以使用 AddSingleton<T>() 方法注册任何类,并且容器将在整个应用程序中仅使用一个实例。我在 9.4 节中详细讨论了 singletons 和其他生命周期。生命周期是 DI 容器应该使用给定对象来实现服务的依赖项的时间。

Instead of providing a single instance that the container will always use, you can provide a function that the container invokes when it needs an instance of the type, as shown in figure 9.3.

alt text
你可以提供一个函数,当容器需要该类型的实例时,你可以提供一个函数,而不是提供容器将始终使用的单个实例,如图 9.3 所示。

Figure 9.3 You can register a function with the DI container that will be invoked whenever a new instance of a service is required.
图 9.3 您可以在 DI 容器中注册一个函数,每当需要服务的新实例时,该函数将被调用。

Note Figure 9.3 is an example of the factory pattern, in which you define how a type is created. Note that the factory functions must be synchronous; you can’t create types asynchronously by (for example) using async.
注意 图 9.3 是工厂模式的一个示例,您可以在其中定义如何创建类型。请注意,工厂函数必须是同步的;您不能 (例如) 使用 async 异步创建类型。

The easiest way to register a service using the factory pattern is with a lambda function (an anonymous delegate), in which the container creates a new EmailServerSettings object whenever it’s needed, as shown in the following listing.

使用工厂模式注册服务的最简单方法是使用 lambda 函数(匿名委托),其中容器在需要时创建新的 EmailServerSettings 对象,如下面的清单所示。

Listing 9.5 Using a lambda factory function to register a dependency
清单 9.5 使用 lambda 工厂函数注册依赖项

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IEmailSender, EmailSender>();
builder.Services.AddScoped<NetworkClient>();
builder.Services.AddSingleton<MessageFactory>();

builder.Services.AddScoped( ❶
    provider => ❷
        new EmailServerSettings ❸
        ( ❸
            Host: "smtp.server.com", ❸
            Port: 25 ❸
        )); ❸

WebApplication app = builder.Build();

app.MapGet("/register/{username}", RegisterUser);

app.Run();

❶ Because you’re providing a function to create the object, you aren’t restricted to a singleton.
因为你提供了一个函数来创建对象,所以你不限于单一实例。

❷ The lambda is provided an instance of IServiceProvider.
lambda 提供了 IServiceProvider 的实例。

❸ The constructor is called every time an EmailServerSettings object is required instead of only once.
每次需要 EmailServerSettings 对象时都会调用构造函数,而不仅仅是一次。

In this example, I changed the lifetime of the created EmailServerSettings object to scoped instead of singleton and provided a factory lambda function that returns a new EmailServerSettings object. Every time the container requires a new EmailServerSettings, it executes the function and uses the new object it returns.
在此示例中,我将创建的 EmailServerSettings 对象的生命周期更改为 scoped 而不是 singleton,并提供了一个返回新 EmailServerSettings 对象的工厂 lambda 函数。每次容器需要新的 EmailServerSettings 时,它都会执行该函数并使用它返回的新对象。

When you use a lambda to register your services, you’re given an IServiceProvider instance at runtime, called provider in listing 9.5. This instance is the public API of the DI container itself, which exposes the GetService<T>() and GetRequiredService<T>() extension methods you saw in chapter 8. If you need to obtain dependencies to create an instance of your service, you can reach into the container at runtime in this way, but you should avoid doing so if possible.
当您使用 lambda 注册服务时,在运行时会为您提供一个 IServiceProvider 实例,在列表 9.5 中称为 provider。此实例是 DI 容器本身的公共 API,它公开了 GetService<T>()GetRequiredService<T>() 扩展方法在第 8 章中。如果您需要获取依赖项来创建服务实例,则可以在运行时以这种方式访问容器,但应尽可能避免这样做。

Tip Avoid calling GetService<T>() and GetRequiredService<T>() in your factory functions if possible. Instead, favor constructor injection; it’s more performant and simpler to reason about.
提示 如果可能,请避免在工厂函数中调用 GetService<T>()GetRequiredService<T>()。 相反,支持构造函数注入;它的性能更高,推理也更简单。

Open generics and dependency injection
开放泛型和依赖项注入
As already mentioned, you couldn’t use the generic registration methods with EmailServerSettings because it uses primitive dependencies (in this case, string and int) in its constructor. Neither can you use the generic registration methods to register open generics.
如前所述,您不能将泛型注册方法与 EmailServerSettings 一起使用,因为它在其构造函数中使用基元依赖项(在本例中为 string 和 int)。也不能使用泛型注册方法注册开放泛型。
Open generics are types that contain a generic type parameter, such as Repository <T>. You normally use this sort of type to define a base behavior that you can use with multiple generic types. In the Repository<T> example, you might inject IRepository<Customer> into your services, which should inject an instance of DbRepository<Customer>, for example.
开放泛型是包含泛型类型参数的类型,例如 Repository <T>。通常使用这种类型来定义可与多个泛型类型一起使用的基本行为。在 Repository<T> 示例中,您可以将 IRepository <Customer> 注入到您的服务中,这应该会注入 DbRepository 的实例例如,
To register these types, you must use a different overload of the Add methods, as in this example:
要注册这些类型,必须使用 Add
的不同重载方法,如以下示例所示:
builder.Services.AddScoped(typeof(IRespository<>), typeof(DbRepository<>));
This code ensures that whenever a service constructor requires IRespository<T>, the container injects an instance of DbRepository<T>.
此代码可确保每当服务构造函数需要IRespository<T> 中,容器会注入 DbRepository<T> 的实例。

At this point, all your dependencies are registered. But your Program.cs is starting to look a little messy, isn’t it? The choice is entirely down to personal preference, but I like to group my services into logical collections and create extension methods for them, as in listing 9.6. This approach creates an equivalent to the framework’s AddRazorPages() extension method—a nice, simple registration API. As you add more features to your app, I think you’ll appreciate it too.
此时,您的所有依赖项都已注册。但是你的Program.cs开始看起来有点凌乱了,不是吗?选择完全取决于个人喜好,但我喜欢将我的服务分组到逻辑集合中,并为它们创建扩展方法,如清单 9.6 所示。此方法创建与框架的 AddRazorPages() 扩展方法等效的 — 一个漂亮、简单的注册 API。随着您向应用程序添加更多功能,我想您也会喜欢它。

Listing 9.6 Creating an extension method to tidy up adding multiple services
清单 9.6 创建一个扩展方法来整理添加多个服务

public static class EmailSenderServiceCollectionExtensions
{
    public static IServiceCollection AddEmailSender(
        this IServiceCollection services) ❶
    {
        services.AddScoped<IEmailSender, EmailSender>(); ❷
        services.AddSingleton<NetworkClient>(); ❷
        services.AddScoped<MessageFactory>(); ❷
        services.AddSingleton( ❷
            new EmailServerSettings ❷
            ( ❷
                host: "smtp.server.com", ❷
                port: 25 ❷
            )); ❷
        return services; ❸
    }
}

❶ Creates an extension method on IServiceCollection by using the “this” keyword
使用“this”关键字在 IServiceCollection 上创建扩展方法

❷ Cuts and pastes your registration code from Program.cs
从 Program.cs 剪切并粘贴您的注册码

❸ By convention, returns the IServiceCollection to allow method chaining
按照约定,返回 IServiceCollection 以允许方法链接

With the preceding extension method created, the following listing shows that your startup code is much easier to grok!

创建上述扩展方法后,以下清单显示您的启动代码更容易理解!

Listing 9.7 Using an extension method to register your services
清单 9.7 使用扩展方法注册您的服务

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddEmailSender(); ❶

WebApplication app = builder.Build();

app.MapGet("/register/{username}", RegisterUser);

app.Run();

❶ The extension method registers all the services associated with the
EmailSender.
扩展方法注册与 EmailSender 关联的所有服务。

So far, you’ve seen how to register the simple DI cases in which you have a single implementation of a service. In some scenarios, you may have multiple implementations of an interface. In section 9.3 you’ll see how to register these with the container to match your requirements.

到目前为止,您已经了解了如何注册简单的 DI 案例,其中您有一个服务的单个实现。在某些情况下,您可能有多个接口实现。在 Section 9.3 中,您将看到如何将这些注册到容器中以满足您的需求。

9.3 Registering a service in the container multiple times

9.3 在容器中多次注册服务

One advantage of coding to interfaces is that you can create multiple implementations of a service. Suppose that you want to create a more generalized version of IEmailSender so that you can send messages via Short Message Service (SMS) or Facebook, as well as by email. You create the interface for it as follows,
对接口进行编码的一个优点是,您可以创建服务的多个实现。假设您要创建更通用的 IEmailSender 版本,以便可以通过短消息服务 (SMS) 或 Facebook 以及电子邮件发送消息。您可以按如下方式为其创建接口:

public interface IMessageSender
{
    public void SendMessage(string message);
}

as well as several implementations: EmailSender, SmsSender, and FacebookSender. But how do you register these implementations in the container? And how can you inject these implementations into your RegisterUser handler? The answers vary slightly, depending on whether you want to use all the implementations in your consumer or only one.

以及多种实现:EmailSender、SmsSender 和 FacebookSender。但是如何在容器中注册这些实现呢?如何将这些实现注入到 RegisterUser 处理程序中呢?答案略有不同,具体取决于您是要使用 Consumer 中的所有 implementations,还是只使用 one。

9.3.1 Injecting multiple implementations of an interface

9.3.1 注入接口的多个实现

Suppose that you want to send a message using each of the IMessageSender implementations whenever a new user registers so that they get an email, an SMS text, and a Facebook message, as shown in figure 9.4.

假设您希望在新用户注册时使用每个 IMessageSender 实现发送消息,以便他们收到电子邮件、SMS 文本和 Facebook 消息,如图 9.4 所示。

alt text

Figure 9.4 When a user registers with your application, they call the RegisterUser handler. This handler sends them an email, an SMS text, and a Facebook message using the IMessageSender classes.
图 9.4 当用户注册到您的应用程序时,他们会调用 RegisterUser 处理程序。此处理程序使用 IMessageSender 类向他们发送电子邮件、SMS 文本和 Facebook 消息。

The easiest way to achieve this goal is to register all the service implementations in your DI container and have it inject one of each type into the RegisterUser endpoint handler. Then RegisterUser can use a simple foreach loop to call SendMessage() on each implementation, as shown in figure 9.5.

实现此目标的最简单方法是在 DI 容器中注册所有服务实现,并让它将每种类型中的一个注入到 RegisterUser 端点处理程序中。然后 RegisterUser 可以使用一个简单的 foreach 循环在每个实现上调用 SendMessage(),如图 9.5 所示。

alt text

Figure 9.5 You can register multiple implementations of a service with the DI container, such as IEmailSender in this example. You can retrieve an instance of each of these implementations by requiring IEnumerable in the RegisterUser handler.
图 9.5 你可以向 DI 容器注册服务的多个实现,例如本例中的 IEmailSender。您可以通过在 RegisterUser 处理程序中要求 IEnumerable 来检索这些实现中的每一个的实例。

You register multiple implementations of the same service with a DI container in exactly the same way as for single implementations, using the Add* extension methods as in this example:

使用 Add 扩展方法,使用 Add 扩展方法,以与单个实现完全相同的方式向 DI 容器注册同一服务的多个实现,如下例所示:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IMessageSender, EmailSender>();
builder.Services.AddScoped<IMessageSender, SmsSender>();
builder.Services.AddScoped<IMessageSender, FacebookSender>();

Then you can inject IEnumerable<IMessageSender> into RegisterUser, as shown in listing 9.8. The container injects an array of IMessageSender containing one of each of the implementations you have registered, in the same order as you registered them. Then you can use a standard foreach loop and call SendMessage on each implementation.

然后你可以将 IEnumerable<IMessageSender> 注入到 RegisterUser 中,如清单 9.8 所示。该容器注入一个 IMessageSender 数组,其中包含您已注册的每个实现之一,其顺序与注册它们的顺序相同。然后,您可以使用标准 foreach 循环并在每个实现上调用 SendMessage。

Listing 9.8 Injecting multiple implementations of a service into an endpoint
清单 9.8 将服务的多个实现注入到端点中

string RegisterUser(
    string username,
    IEnumerable<IMessageSender> senders) ❶
    {
    foreach(var sender in senders) ❷
    { ❷
        Sender.SendMessage($”Hello {username}!”); ❷
    } ❷
    return $"Welcome message sent to {username}";
}

❶ Requests an IEnumerable injects an array of IMessageSender
请求 IEnumerable 注入 IMessageSender 数组

❷ Each IMessageSender in the IEnumerable is a different implementation.
IEnumerable 中的每个 IMessageSender 都是不同的实现。

Warning You must use IEnumerable<T> as the handler parameter type to inject all the registered types of a service, T. Even though this parameter will be injected as a T[] array, you can’t use T[] or ICollection<T> as your constructor argument. Doing so will cause an InvalidOperationException, similar to that in figure 9.1.
警告 您必须使用 IEnumerable<T> 作为处理程序参数类型,以注入服务的所有已注册类型。即使此参数将作为 T[]数组,则不能使用 T[] 或 ICollection<T> 作为构造函数参数。这样做会导致InvalidOperationException,类似于图 9.1 中的内容。

It’s simple enough to inject all the registered implementations of a service, but what if you need only one? How does the container know which one to use?

注入服务的所有已注册实现非常简单,但如果你只需要一个呢?容器如何知道要使用哪一个?

9.3.2 Injecting a single implementation when multiple services are registered

9.3.2 在注册多个服务时注入单个实现

Suppose that you’ve already registered all the IMessageSender implementations. What happens if you have a service that requires only one of them? Consider this example:
假设您已经注册了所有 IMessageSender 实现。如果您的服务只需要其中一个,会发生什么情况?请考虑以下示例:

public class SingleMessageSender
{
    private readonly IMessageSender _messageSender;
    public SingleMessageSender(IMessageSender messageSender)
    {
        _messageSender = messageSender;
    }
}

Of the three implementations available, the container needs to pick a single IMessageSender to inject into this service. It does this by using the last registered implementation: FacebookSender from the previous example.

在三种可用的实现中,容器需要选取一个 IMessageSender 以注入到此服务中。它通过使用上一个示例中的 FacebookSender 来实现此目的。

Note The DI container will use the last registered implementation of a service when resolving a single instance of the service.
注意 在解析服务的单个实例时,DI 容器将使用上次注册的服务实现。

This feature can be particularly useful for replacing built-in DI registrations with your own services. If you have a custom implementation of a service that you know is registered within a library’s Add* extension method, you can override that registration by registering your own implementation afterward. The DI container will use your implementation whenever a single instance of the service is requested.

此功能对于将内置 DI 注册替换为您自己的服务特别有用。如果您知道在库的 Add* 扩展方法中注册了服务的自定义实施,则可以通过在之后注册自己的实施来覆盖该注册。每当请求服务的单个实例时,DI 容器都会使用您的实现。

The main disadvantage of this approach is that you still end up with multiple implementations registered; you can inject an IEnumerable<T> as before. Sometimes you want to register a service conditionally so that you always have only a single registered implementation.

这种方法的主要缺点是你最终仍然注册了多个 implementation;您可以像以前一样注入 IEnumerable<T>。有时,您希望有条件地注册一个服务,以便始终只有一个已注册的实现。

9.3.3 Conditionally registering services using TryAdd

9.3.3 使用 TryAdd 有条件地注册服务

Sometimes you want to add an implementation of a service only if one hasn’t already been added. This approach is particularly useful for library authors; they can create a default implementation of an interface and register it only if the user hasn’t already registered their own implementation.

有时,仅当尚未添加服务时,您才希望添加服务的实现。此方法对库作者特别有用;他们可以创建 interface 的默认 implementation ,并且只有在用户尚未注册自己的 implementation 时才能注册它。

You can find several extension methods for conditional registration in the Microsoft.Extensions.DependencyInjection.Extensions namespace, such as TryAddScoped. This method checks whether a service has been registered with the container before calling AddScoped on the implementation. Listing 9.9 shows how you can add SmsSender conditionally if there are no existing IMessageSender implementations. As you initially register EmailSender, the container ignores the SmsSender registration, so it isn’t available in your app.

您可以在 Microsoft.Extensions.DependencyInjection.Exte nsions 命名空间中找到多种用于条件注册的扩展方法,例如 TryAddScoped。此方法在对实现调用 AddScoped 之前,检查是否已向容器注册服务。清单 9.9 展示了如何有条件地添加 SmsSender如果没有现有的 IMessageSender 实现。当您最初注册 EmailSender 时,容器会忽略 SmsSender 注册,因此它在您的应用程序中不可用。

Listing 9.9 Conditionally adding a service using TryAddScoped
清单 9.9 使用 TryAdd 有条件地注册服务

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMessageSender, EmailSender>(); ❶
builder.Services.TryAddScoped<IMessageSender, SmsSender>(); ❷

❶ EmailSender is registered with the container.
EmailSender 已注册到容器中。

❷ There’s already an IMessageSender implementation, so SmsSender isn’t
registered.
已经有一个 IMessageSender 实现,因此 SmsSender 未注册。

Code like this doesn’t often make a lot of sense at the application level, but it can be useful if you’re building libraries for use in multiple apps. The ASP.NET Core framework, for example, uses TryAdd* in many places, which lets you easily register alternative implementations of internal components in your own application if you want.

像这样的代码在应用程序级别通常没有多大意义,但如果您正在构建用于多个应用程序的库,它可能很有用。例如,ASP.NET Core 框架在许多地方都使用 TryAdd*,它允许您根据需要轻松地在自己的应用程序中注册内部组件的替代实施。

You can also replace a previously registered implementation by using the Replace() extension method. Unfortunately, the API for this method isn’t as friendly as the TryAdd methods. To replace a previously registered IMessageSender with SmsSender, you’d use

您还可以使用 Replace() 扩展方法替换以前注册的实现。遗憾的是,此方法的 API 不如 TryAdd 方法友好。要将以前注册的 IMessageSender 替换为 SmsSender,请使用

builder.Services.Replace(new ServiceDescriptor(
    typeof(IMessageSender), typeof(SmsSender), ServiceLifetime.Scoped
));

Tip When using Replace, you must provide the same lifetime that was used to register the service that’s being replaced.
提示 使用 Replace 时,您必须提供用于注册要替换的服务的相同生命周期。

We’ve pretty much covered registering dependencies but touched only vaguely on one important aspect: lifetimes. Understanding lifetimes is crucial in working with DI containers, so it’s important to pay close attention to them when registering your services with the container.

我们几乎已经介绍了注册依赖项,但只模糊地触及了一个重要的方面:生命周期。了解生命周期对于使用 DI 容器至关重要,因此在向容器注册服务时,请务必密切关注它们。

9.4 Understanding lifetimes: When are services created?

9.4 了解生命周期:何时创建服务?

Whenever the DI container is asked for a particular registered service, such as an instance of IMessageSender, it can do either of two things to fulfill the request:

每当向 DI 容器请求特定的已注册服务(如 IMessageSender 的实例)时,它都可以执行以下两项作之一来满足请求:

  • Create and return a new instance of the service
    创建并返回服务的新实例。

  • Return an existing instance of the service
    返回服务的现有实例。

The lifetime of a service controls the behavior of the DI container with respect to these two options. You define the lifetime of a service during DI service registration. The lifetime dictates when a DI container reuses an existing instance of the service to fulfill service dependencies and when it creates a new one.

服务的生存期控制 DI 容器相对于这两个选项的行为。您可以在 DI 服务注册期间定义服务的生命周期。生命周期规定了 DI 容器何时重用服务的现有实例来实现服务依赖项,以及何时创建新实例。

Definition The lifetime of a service is how long an instance of a service should live in a container before the container creates a new instance.
定义 服务的生命周期是指在容器创建新实例之前,服务实例应在容器中存在的时间。

It’s important to get your head around the implications for the different lifetimes used in ASP.NET Core, so this section looks at each lifetime option and when you should use it. In particular, you’ll see how the lifetime affects how often the DI container creates new objects. In section 9.4.4 I’ll show you an antipattern of lifetimes to watch out for, in which a short-lifetime dependency is captured by a long-lifetime dependency. This antipattern can cause some hard-to-debug issues, so it’s important to bear in mind when configuring your app.

了解 ASP.NET Core 中使用的不同生命周期的影响非常重要,因此本节将介绍每个生命周期选项以及何时应该使用它。特别是,您将看到生命周期如何影响 DI 容器创建新对象的频率。在 9.4.4 节中,我将向你展示一个需要注意的生命周期的反模式,其中短生命周期的依赖性被长生命周期的依赖性捕获。这种反模式可能会导致一些难以调试的问题,因此在配置应用程序时请务必记住。

In ASP.NET Core, you can specify one of three lifetimes when registering a service with the built-in container:
在 ASP.NET Core 中,您可以在使用内置容器注册服务时指定三个生命周期之一:

  • Transient—Every time a service is requested, a new instance is created. Potentially, you can have different instances of the same class within the same dependency graph.
    Transient (瞬态) – 每次请求服务时,都会创建一个新实例。您可能会在同一依赖项关系图中拥有同一类的不同实例。

  • Scoped—Within a scope, all requests for a service give you the same object. For different scopes, you get different objects. In ASP.NET Core, each web request gets its own scope.
    范围 - 在一个范围内,服务的所有请求都会为您提供相同的对象。对于不同的范围,你会得到不同的对象。在 ASP.NET Core 中,每个 Web 请求都有自己的范围。

  • Singleton—You always get the same instance of the service, regardless of scope.
    单一实例 - 无论范围如何,您始终会获得相同的服务实例。

Note These concepts align well with most other DI containers, but the terminology may differ. If you’re familiar with a third-party DI container, be sure you understand how the lifetime concepts align with the built-in ASP.NET Core DI container.
注意 这些概念与大多数其他 DI 容器非常一致,但术语可能有所不同。如果您熟悉使用第三方 DI 容器时,请确保您了解生命周期概念如何与内置的 ASP.NET Core DI 容器保持一致。

To illustrate the behavior of each lifetime, I use a simple example in this section. Suppose that you have DataContext, which has a connection to a database, as shown in listing 9.10. It has a single property, RowCount, which represents the number of rows in the Users table of a database. For the purposes of this example, we emulate calling the database by setting the number of rows randomly when the DataContext object is created, so you always get the same value every time you call RowCount on a given DataContext instance. Different instances of DataContext return different RowCount values.

为了说明每个生命周期的行为,我在本节中使用了一个简单的示例。假设你有 DataContext,它与一个数据库有连接,如清单 9.10 所示。它有一个属性 RowCount,该属性表示数据库的 Users 表中的行数。对于此示例,我们通过在创建 DataContext 对象时随机设置行数来模拟调用数据库,因此每次在给定 DataContext 实例上调用 RowCount 时,您始终会获得相同的值。DataContext 的不同实例返回不同的 RowCount 值。

Listing 9.10 DataContext generating a random RowCount on creation
清单 9.10 在创建DataContext时生成一个 random RowCount

class DataContext
{
    public int RowCount { get; } ❶
        = Random.Shared.Next(1, 1_000_000_000); ❷
}

❶ The property is read-only, so it always returns the same value.
该属性是只读的,因此它始终返回相同的值。

❷ Generates a random number between 1 and 1,000,000,000
生成一个介于 1 和 1,000,000,000 之间的随机数

You also have a Repository class that has a dependency on the DataContext, as shown in the next listing. It also exposes a RowCount property, but this property delegates the call to its instance of DataContext. Whatever value DataContext was created with, the Repository displays the same value.

您还有一个依赖于 DataContext 的 Repository 类,如下一个清单所示。它还公开了一个 RowCount 属性,但此属性将调用委托给其 DataContext 实例。无论价值如何DataContext 时,Repository 显示相同的值。

Listing 9.11 Repository service that depends on an instance of DataContext
清单 9.11 依赖于DataContext 的实例

public class Repository
{
    private readonly DataContext _dataContext; ❶
    public Repository(DataContext dataContext) ❶
    { ❶
        _dataContext = dataContext; ❶
    } ❶
    public int RowCount => _dataContext.RowCount; ❷
}

❶ An instance of DataContext is provided using DI.
DataContext 的实例是使用 DI 提供的。
❷ RowCount returns the same value as the current instance of DataContext.
RowCount 返回与 DataContext 的当前实例相同的值。

Finally, you have your endpoint handler, RowCounts, which takes a dependency on both Repository and on DataContext directly. When the minimal API infrastructure creates the arguments needed to call RowCounts, the DI container injects an instance of DataContext and an instance of Repository. To create Repository, it must create a second instance of DataContext. Over the course of two requests, four instances of DataContext will be required, as shown in figure 9.6.

最后,您有终端节点处理程序 RowCounts,它直接依赖于 Repository 和 DataContext。当最小的 API 基础设施创建调用 RowCounts 所需的参数时,DI 容器会注入一个 DataContext 实例和一个 Repository 实例。要创建 Repository,它必须创建 DataContext 的第二个实例。在两个请求的过程中,将需要 4 个 DataContext 实例,如图 9.6 所示。

alt text

Figure 9.6 The DI container uses two instances of DataContext for each request. Depending on the lifetime with which the DataContext type is registered, the container might create one, two, or four instances of DataContext.
图 9.6 DI 容器为每个请求使用两个 DataContext 实例。根据 DataContext 类型注册的生命周期,容器可能会创建一个、两个或四个 DataContext 实例。

The RowCounts handler retrieves the value of RowCount returned from both Repository and DataContext and then returns them as a string, similar to the code in listing 9.12. The sample code associated with this book also records and displays the values from previous requests so you can easily track how the values change with each request.

RowCounts 处理程序检索从 Repository 和 DataContext 返回的 RowCount 的值,然后将它们作为字符串返回,类似于清单中的代码9.12. 与本书关联的示例代码还记录并显示先前请求的值,因此您可以轻松跟踪每个请求的值如何变化。

Listing 9.12 The RowCounts handler depends on DataContext and Repository
清单 9.12 RowCounts 处理程序依赖于DataContext 和存储库

static string RowCounts( ❶
    DataContext db, ❶
    Repository repository) ❶
{
    int dbCount = db.RowCount; ❷
    int repositoryCount = repository.RowCount; ❷

    return: $"DataContext: {dbCount}, Repository: {repositoryCount}"; ❸
}

❶ DataContext and Repository are created using DI.
DataContext 和 Repository 是使用 DI 创建的。

❷ When invoked, the page handler retrieves and records RowCount from both
dependencies.
调用时,页面处理程序会从两个依赖项中检索并记录 RowCount。

❸ The counts are returned in the response.
响应中返回计数。

The purpose of this example is to explore the relationships among the four DataContext instances, depending on the lifetimes you use to register the services with the container. I’m generating a random number in DataContext as a way of uniquely identifying a DataContext instance, but you can think of this example as being a point-in-time snapshot of, say, the number of users logged on to your site or the amount of stock in a warehouse.

此示例的目的是探索 4 个 DataContext 实例之间的关系,具体取决于您用于向容器注册服务的生命周期。我在 DataContext 中生成一个随机数,作为唯一标识 DataContext 实例的一种方式,但您可以将此示例视为登录到您站点的用户数量或仓库中库存量的时间点快照。

I’ll start with the shortest-lived lifetime (transient), move on to the common scoped lifetime, and then take a look at singletons. Finally, I’ll show an important trap you should be on the lookout for when registering services in your own apps.

我将从最短生存期 (transient) 开始,然后转到常见的作用域生存期,然后看一下单例。最后,我将展示一个重要的陷阱在您自己的应用程序中注册服务时要注意。

9.4.1 Transient: Everyone is unique

9.4.1 瞬态:每个人都是独一无二的

In the ASP.NET Core DI container, transient services are always created new whenever they’re needed to fulfill a dependency. You can register your services using the AddTransient extension methods:

在 ASP.NET Core DI 容器中,每当需要临时服务来实现依赖项时,它们总是会创建新的。您可以使用 AddTransient 扩展方法注册您的服务:

builder.Services.AddTransient<DataContext>();
builder.Services.AddTransient<Repository>();

When you register services this way, every time a dependency is required, the container creates a new one. This behavior of the container for transient services applies both between requests and within requests; the DataContext injected into the Repository will be a different instance from the one injected into the RowCounts handler.

当您以这种方式注册服务时,每次需要依赖项时,容器都会创建一个新依赖项。临时服务容器的这种行为适用于请求之间和请求内;注入 Repository 的 DataContext 将与注入 RowCounts 处理程序的实例不同。

Note Transient dependencies can result in different instances of the same type within a single dependency graph.
注意:瞬态依赖关系可能会导致单个依赖关系图中出现相同类型的不同实例。

Figure 9.7 shows the results you get from calling the API repeatedly when you use the transient lifetime for both services. You can see that every value is different, both within a request and between requests. Note that figure 9.7 was generated using the source code for this chapter, which is based on the listings in this chapter, but also displays the results from previous requests to make the behavior easier to observe.

图 9.7 显示了在对这两个服务使用瞬态生命周期时重复调用 API 所获得的结果。您可以看到,每个值都不同,无论是在请求中还是在请求之间。请注意,图 9.7 是使用本章的源代码生成的,该源代码基于本章中的清单,但也显示了来自先前请求的结果,以使行为更易于观察。

alt text

Figure 9.7 When registered using the transient lifetime, all DataContext objects are different, as you see by the fact that all the values are different within and between requests.
图 9.7 当使用瞬态生命周期注册时,所有 DataContext 对象都是不同的,正如您所看到的,请求内部和请求之间的所有值都不同。

Transient lifetimes can result in the creation of a lot of objects, so they make the most sense for lightweight services with little or no state. Using the transient lifetime is equivalent to calling new every time you need a new object, so bear that in mind when using it. You probably won’t use the transient lifetime often; the majority of your services will probably be scoped instead.

瞬态生命周期可能会导致创建大量对象,因此它们对于状态很少或没有状态的轻量级服务最有意义。使用 transient 生命周期相当于每次需要新对象时调用 new,因此在使用它时请记住这一点。您可能不会使用瞬态生存期通常是;您的大多数服务可能会改为限定范围。

9.4.2 Scoped: Let’s stick together

9.4.2 范围:让我们团结一致

The scoped lifetime states that a single instance of an object will be used within a given scope, but a different instance will be used between different scopes. In ASP.NET Core, a scope maps to a request, so within a single request, the container will use the same object to fulfill all dependencies.

作用域生命周期表示将在给定范围内使用对象的单个实例,但将在不同的作用域之间使用不同的实例。在 ASP.NET Core 中,范围映射到请求,因此在单个请求中,容器将使用相同的对象来满足所有依赖项。

In the row-count example, within a single request (a single scope) the same DataContext is used throughout the dependency graph. The DataContext injected into the Repository is the same instance as the one injected into the RowCounts handler.

在行计数示例中,在单个请求(单个范围)中,在整个依赖关系图中使用相同的 DataContext。注入 Repository 的 DataContext 与注入 RowCounts 处理程序的实例相同。

In the next request, you’re in a different scope, so the container creates a new instance of DataContext, as shown in figure 9.8. A different instance means a different RowCount for each request, as you can see. As before, figure 9.8 also shows the counts for previous requests.

在下一个请求中,您处于不同的范围内,因此容器会创建一个新的 DataContext 实例,如图 9.8 所示。如您所见,不同的实例意味着每个请求的 RowCount 不同。和以前一样,图 9.8 也显示了先前请求的计数。

alt text

Figure 9.8 Scoped dependencies use the same instance of DataContext within a single request but a new instance for a separate request. Consequently, the RowCounts are identical within a request.
图 9.8 作用域依赖项在单个请求中使用相同的 DataContext 实例,但对单独的请求使用新实例。因此,请求中的 RowCount是相同的。

You can register dependencies as scoped by using the AddScoped extension methods. In this example, I registered DataContext as scoped and left Repository as transient, but you’d get the same results in this case if both were scoped:

您可以使用 AddScoped 扩展方法将依赖项注册为 scoped。在此示例中,我将 DataContext 注册为范围,并将 Repository 保留为 transient,但在这种情况下,如果两者都是范围,您将得到相同的结果:

builder.Services.AddScoped<DataContext>();

Due to the nature of web requests, you’ll often find services registered as scoped dependencies in ASP.NET Core. Database contexts and authentication services are common examples of services that should be scoped to a request—anything that you want to share across your services within a single request but that needs to change between requests.

由于 Web 请求的性质,您经常会发现在 ASP.NET Core 中注册为范围依赖项的服务。数据库上下文和身份验证服务是应将范围限定为请求的服务的常见示例,请求是您希望在单个请求中跨服务共享但需要在请求之间更改的任何内容。

NOTE If your scoped or transient services implement IDisposable, the DI container automatically disposes them
when the scope ends.
注意 如果您的范围或临时服务实现 IDisposable,则 DI 容器会在范围结束时自动释放它们。

Generally speaking, you’ll find a lot of services registered using the scoped lifetime—especially anything that uses a database, anything that’s dependent on details of the HTTP request, or anything that uses a scoped service. But some services don’t need to change between requests, such as a service that calculates the area of a circle or returns the current time in different time zones. For these services, a singleton lifetime might be more appropriate.

一般来说,您会发现许多使用作用域生命周期注册的服务,尤其是任何使用数据库的服务、任何依赖于 HTTP 请求详细信息的服务,或者任何使用作用域服务的服务。但有些服务不需要在请求之间更改,例如计算圆的面积或返回不同时区的当前时间的服务。对于这些服务,单一实例生存期可能更合适。

9.4.3 Singleton: There can be only one

9.4.3 Singleton:只能有一个

The singleton is a pattern that came before DI; the DI container provides a robust and easy-to-use implementation of it. The singleton is conceptually simple: an instance of the service is created when it’s first needed (or during registration, as in section 9.2), and that’s it. You’ll always get the same instance injected into your services.

singleton 是 DI 之前的模式;DI 容器提供了强大且易于使用的实现。singleton 在概念上很简单:在第一次需要时(或在注册期间,如 9.2 节)创建服务的实例,仅此而已。您将始终将相同的实例注入到您的服务中。

The singleton pattern is particularly useful for objects that are expensive to create, contain data that must be shared across requests, or don’t hold state. The latter two points are important: any service registered as a singleton should be thread-safe.

对于创建成本高昂、包含必须在请求之间共享的数据或不保存状态的对象,单独模式特别有用。后两点很重要:任何注册为单一实例的服务都应该是线程安全的。

Warning Singleton services must be thread-safe in a web application, as they’ll typically be used by multiple threads during concurrent requests.
警告 单例服务在 Web 应用程序中必须是线程安全的,因为它们通常在并发请求期间由多个线程使用。

Let’s consider what using singletons means for the row-count example. We can update the registration of DataContext to be a singleton:

让我们考虑一下使用单例对行计数示例意味着什么。我们可以将 DataContext 的注册更新为单例:

builder.Services.AddSingleton<DataContext>();

Then we can call the RowCounts handler and observe the results in figure 9.9. We can see that every instance has returned the same value, indicating that the same instance of DataContext is used in every request, both when injected directly into the endpoint handler and when referenced transitively by Repository.

然后我们可以调用 RowCounts 处理程序并观察图 9.9 中的结果。我们可以看到每个实例都返回了相同的值,这表明每个请求都使用了相同的 DataContext 实例,无论是直接注入到端点处理程序中时,还是被 Repository 传递引用时。

alt text

Figure 9.9 Any service registered as a singleton always returns the same instance. Consequently, all the calls to the RowCounts handler return the same value, both within a request and between requests.
图 9.9 任何注册为单例的服务总是返回相同的实例。因此,对 RowCounts 处理程序的所有调用在请求内和请求之间都返回相同的值。

Singletons are convenient for objects that need to be shared or that are immutable and expensive to create. A caching service should be a singleton, as all requests need to share the service. It must be thread-safe, though. Similarly, you might register a settings object loaded from a remote server as a singleton if you load the settings once at startup and reuse them through the lifetime of your app.

单例对于需要共享或不可变且创建成本高昂的对象来说很方便。缓存服务应该是单一实例,因为所有请求都需要共享该服务。不过,它必须是线程安全的。同样,如果您在启动时加载一次设置,并在应用程序的生命周期中重复使用它们,则可以将从远程服务器加载的设置对象注册为单一实例。

On the face of it, choosing a lifetime for a service may not seem to be too tricky. But an important gotcha can come back to bite you in subtle ways, as you’ll see in section 9.4.4.

从表面上看,为服务选择生命周期似乎并不太棘手。但是一个重要的问题可能会以微妙的方式回来咬你,正如您将在 9.4.4 节中看到的那样。

9.4.4 Keeping an eye out for captive dependencies

9.4.4 密切关注捕获依赖项

Suppose that you’re configuring the lifetime for the DataContext and Repository examples. You think about the suggestions I’ve provided and decide on the following lifetimes:

假设您正在为 DataContext 和 Repository 示例配置生命周期。您考虑我提供的建议并决定以下生命周期:

  • DataContext—Scoped, as it should be shared for a single request
    DataContext — 范围限定,因为它应该为单个请求共享

  • Repository—Singleton, as it has no state of its own and is thread-safe, so why not?
    存储库 — 单例,因为它没有自己的状态并且是线程安全的,那么为什么不呢?

Warning This lifetime configuration is to explore a bug. Don’t use it in your code; if you do, you’ll experience a similar problem!
警告 此生命周期配置用于探索 bug。不要在代码中使用它;如果你这样做,你会遇到类似的问题!

Unfortunately, you’ve created a captive dependency because you’re injecting a scoped object, DataContext, into a singleton, Repository. As it’s a singleton, the same Repository instance is used throughout the lifetime of the app, so the DataContext that was injected into it will also hang around, even though a new one should be used with every request. Figure 9.10 shows this scenario, in which a new instance of DataContext is created for each scope but the instance inside Repository hangs around for the lifetime of the app.

遗憾的是,您创建了捕获依赖项,因为您正在将范围对象 DataContext 注入到单一实例 Repository 中。由于它是一个单例,因此在应用程序的整个生命周期中使用相同的 Repository 实例,因此注入其中的 DataContext 也将挂起,即使每个请求都应该使用一个新的 DataContext 也是如此。图 9.10 显示了这种情况,其中为每个范围创建了一个新的 DataContext 实例,但Repository 中的实例在应用程序的生命周期内挂起。

alt text

Listing 9.10 DataContext is registered as a scoped dependency, but Repository is a singleton. Even though you expect a new DataContext for every request, Repository captures the injected DataContext and causes it to be reused for the lifetime of the app.

图 9.10 DataContext 注册为范围依赖项,但 Repository 是单例。即使您希望每个请求都有一个新的 DataContext,Repository 也会捕获 注入的 DataContext,并使其在应用程序的生命周期内重复使用。

Captive dependencies can cause subtle bugs that are hard to root out, so you should always keep an eye out for them. These captive dependencies are relatively easy to introduce, so always think carefully when registering a singleton service.

捕获依赖项可能会导致难以根除的细微错误,因此您应该始终留意它们。这些捕获依赖项相对容易引入,因此在注册 singleton 服务时请始终仔细考虑。

Warning A service should use only dependencies that have a lifetime longer than or equal to the service’s lifetime. A service registered as a singleton can safely use only singleton dependencies. A service registered as scoped can safely use scoped or singleton dependencies. A transient service can use dependencies with any lifetime.
警告: 服务应仅使用生命周期长于或等于服务生命周期的依赖项。注册为单一实例的服务可以安全地仅使用单一实例依赖项。注册为 scoped 的服务可以安全地使用 scoped 或单一实例依赖项。临时服务可以使用任何生命周期的依赖项。

At this point, I should mention one glimmer of hope in this cautionary tale: ASP.NET Core automatically checks for these kinds of captive dependencies and throws an exception on application startup if it detects them, or on first use of a captive dependency, as shown in figure 9.11.

在这一点上,我应该在这个警示故事中提到一丝希望:ASP.NET Core 会自动检查这些类型的捕获依赖项,并在应用程序启动时或首次使用捕获依赖项时引发异常,如图 9.11 所示。

alt text

Figure 9.11 When ValidateScopes is enabled, the DI container throws an exception when it creates a service with a captive dependency. By default, this check is enabled only for development environments.
图 9.11 启用 ValidateScopes 后,DI 容器在创建具有捕获依赖项的服务时会引发异常。默认情况下,仅对开发环境启用此检查。

This scope validation check has a performance effect, so by default it’s enabled only when your app is running in a development environment, but it should help you catch most problems of this kind. You can enable or disable this check regardless of environment by configuring the ValidateScopes option on your WebApplicationBuilder in Program.cs by using the Host property, as shown in the following listing.

此范围验证检查会产生性能成本,因此默认情况下,仅当您的应用程序在开发环境中运行时,它才会启用,但它应该可以帮助您捕获大多数此类问题。无论环境如何,您都可以通过使用 Host 属性在 Program.cs WebApplicationBuilder 上配置 ValidateScopes 选项来启用或禁用此检查,如下面的清单所示。

Listing 9.13 Setting the ValidateScopes property to always validate scopes

Listing 9.13 设置ValidateScopes属性以始终验证范围

WebApplicationBuilder builder = WebApplication.CreateBuilder(args); ❶

builder.Host.UseDefaultServiceProvider(o => ❷
{
    o.ValidateScopes = true; ❸
    o.ValidateOnBuild = true; ❹
});

❶ The default builder sets ValidateScopes to validate only in development
environments.
默认生成器将 ValidateScopes 设置为仅在开发环境中验证。

❷ You can override the validation check with the UseDefaultServiceProvider
extension.
您可以使用 UseDefaultServiceProvider 扩展覆盖验证检查。

❸ Setting this to true will validate scopes in all environments, which has
performance implications.
将此项设置为 true 将验证所有环境中的作用域,这会影响性能。

❹ ValidateOnBuild checks that every registered service has all its dependencies registered.
ValidateOnBuild 检查每个已注册的服务是否都已注册其所有依赖项。

Listing 9.13 shows another setting you can enable, ValidateOnBuild, which goes one step further. When the setting is enabled, the DI container checks on application startup that it has dependencies registered for every service it needs to build. If it doesn’t, it throws an exception and shuts down the app, as shown in figure 9.12, letting you know about the misconfiguration. This setting also has a performance effect, so it’s enabled only in development environments by default, but it’s useful for pointing out any missed service registrations.

清单 9.13 显示了另一个你可以启用的设置,ValidateOnBuild,它更进一步。启用该设置后,DI 容器会在应用程序启动时检查它是否为需要构建的每个服务注册了依赖项。如果没有,它会抛出一个异常并关闭应用程序,如图 9.12 所示,让您知道配置错误。此设置也有性能成本,因此默认情况下仅在开发环境中启用,但它对于指出任何错过的服务注册非常有用。

alt text

Figure 9.12 When ValidateOnBuild is enabled, the DI container checks on app startup that it can create all the registered services. If it finds a service it can’t create, it throws an exception. By default, this check is enabled only for development environments.
图 9.12 启用 ValidateOnBuild 后,DI 容器会在应用程序启动时检查它是否可以创建所有已注册的服务。如果它找到一个服务,它就找不到create 时,它会引发异常。默认情况下,仅对开发环境启用此检查。

Warning Unfortunately, the container can’t catch everything. For a list of caveats and exceptions, see this post from my blog: http://mng.bz/QmwG.
警告 遗憾的是,容器无法捕获所有内容。有关注意事项和例外情况的列表,请参阅我博客中的这篇文章:http://mng.bz/QmwG

We’ve almost covered everything about dependency injection now, and there’s only one more thing to consider: how to resolve scoped services on app startup in Program.cs.

我们现在几乎已经涵盖了有关依赖项注入的所有内容,现在只需要考虑一件事:如何在 Program.cs 中解析应用程序启动时的作用域服务。

9.5 Resolving scoped services outside a request

9.5 在请求之外解析分区服务

In chapter 8, I said that there are two main ways to resolve services from the DI container for minimal API applications:

在第 8 章中,我说过,对于最小的 API 应用程序,有两种主要方法可以从 DI 容器中解析服务:

  • Injecting services into an endpoint handler
    将服务注入端点处理程序

  • Accessing the DI container directly in Program.cs
    直接在 Program.cs 中访问 DI 容器

You’ve seen the first of those approaches several times now in this chapter. In chapter 8 you saw that you can access services in Program.cs by calling GetRequiredService<T>() on WebApplication.Services:

在本章中,您已经多次看到这些方法中的第一种。在第 8 章中,您看到您可以通过对 WebApplication.Services 调用 GetRequiredService<T>() 来访问 Program.cs 中的服务:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
var settings = app.Services.GetRequiredService<EmailServerSettings>();

It’s important, however, that you resolve only singleton services this way. The IServiceProvider exposed as WebApplication.Services is the root DI container for your app. Services resolved this way live for the lifetime of your app, which is fine for singleton services but typically isn’t the behavior you want for scoped or transient services.

但是,请务必以这种方式仅解析单一实例服务。作为 WebApplication.Services 公开的 IServiceProvider 是应用的根 DI 容器。以这种方式解析的服务在应用程序的生命周期内有效,这对于单一实例服务来说很好,但通常不是您想要的作用域或瞬态服务的行为。

Warning Don’t resolve scoped or transient services directly from WebApplication.Services. This approach can lead to leaking of memory, as the objects are kept alive till the app exits and aren’t garbage-collected.
警告 不要直接从 WebApplication.Services 解析范围或暂时性服务。这种方法可能会导致内存泄漏,因为对象在应用程序退出之前保持活动状态,并且不会进行垃圾回收。

Instead, you should only resolve scoped and transient services from an active scope. A new scope is created automatically for every HTTP request, but when you’re resolving services from the DI container directly in Program.cs (or anywhere else that’s outside the context of an HTTP request), you need to create (and dispose of) a scope manually.

相反,您应该只从活动范围解析范围服务和暂时性服务。系统会自动为每个 HTTP 请求创建一个新范围,但是当您在 Program.cs 中(或 HTTP 请求上下文之外的任何其他位置)中直接从 DI 容器中解析服务时,您需要手动创建(和释放)范围。

You can create a new scope by calling CreateScope() or CreateAsyncScope() on IServiceProvider, which returns a disposable IServiceScope object, as shown in figure 9.13. IServiceScope also exposes an IServiceProvider property, but any services resolved from this provider are disposed of automatically when you dispose the IServiceScope, ensuring that all the resources held by the scoped and transient services are released correctly.

您可以通过对 IServiceProvider 调用 CreateScope() 或 CreateAsyncScope() 来创建新范围,这将返回一个可释放的 IServiceScope 对象,如图 9.13 所示。IServiceScope 还公开 IServiceProvider 属性,但在释放 IServiceScope 时,将自动释放从此提供程序解析的任何服务,从而确保正确释放作用域服务和暂时性服务所持有的所有资源。

alt text

Figure 9.13 To resolve scoped or transient services manually, you must create an IServiceScope object by calling CreateScope() on WebApplication.Services. Any scoped or transient services resolved from the DI container exposed as IServiceScope.ServiceProvider are disposed of automatically when you dispose of the IServiceScope object.
图 9.13 要手动解析作用域或临时服务,必须通过在 WebApplication.Services 上调用 CreateScope() 来创建 IServiceScope 对象。在释放 IServiceScope 对象时,将自动释放从公开为 IServiceScope.ServiceProvider 的 DI 容器解析的任何作用域或暂时性服务。

The following listing shows how you can resolve a scoped service in Program.cs using the pattern in figure 9.13. This pattern ensures that the scoped DataContext object is disposed of correctly before the call to app.Run().

下面的清单显示了如何使用图 9.13 中的模式在 Program.cs 中解析范围服务。这
pattern 确保在调用 app 之前正确处理作用域内的 DataContext 对象。

Listing 9.14 Resolving a scoped service using IServiceScope in Program.cs
清单 9.14 使用Program.cs 中的 IServiceScope

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<DataContext>(); ❶

WebApplication app = builder.Build();

await using (var scope = app.Services.CreateAsyncScope()) ❷
{
    var dbContext = ❸
        scope.ServiceProvider.GetRequiredService<DataContext>(); ❸
    Console.WriteLine($"Retrieved scope: {dbContext.RowCount}");
} ❹
app.Run();

❶ DataContext is registered as scoped, so it shouldn’t be resolved directly from app.Services.
DataContext 已注册为 scoped,因此不应直接从 app 解析app.Services。

❷ Creates an IServiceScope
创建 IServiceScope

❸ Resolves the scoped service from the scoped container
从范围容器解析范围服务

❹ When the IServiceScope is disposed, all resolved services are also disposed.
释放 IServiceScope 时,也会释放所有已解析的服务。

This example uses the async form CreateAsyncScope() instead of CreateScope(), which you generally should favor whenever possible. CreateAsyncScope was introduced in .NET 6 to fix an edge case related to IAsyncDisposable (introduced in .NET Core 3.0). You can read more about this scenario on my blog at http://mng.bz/zXGB.

此示例使用异步形式 CreateAsyncScope() 而不是 CreateScope(),您通常应尽可能使用后者。CreateAsyncScope 是在 .NET 6 中引入的,用于修复与 IAsyncDisposable 相关的边缘情况(在 .NET Core 3.0 中引入)。您可以在我的博客 http://mng.bz/zXGB 上阅读有关此方案的更多信息。

With that, you’ve reached the end of this introduction to DI in ASP.NET Core. Now you know how to register your own services with the DI container, and ideally, you have a good understanding of the three service lifetimes used in .NET. DI appears everywhere in .NET, so it’s important to try to get your head around it.

至此,您已完成 ASP.NET Core 中的 DI 简介的结尾。现在您知道如何注册自己的服务,理想情况下,您对 .NET 中使用的三个服务生存期有很好的了解。DI 在 .NET 中无处不在,因此请务必尝试了解它。

In chapter 10 we’ll look at the ASP.NET Core configuration model. You’ll see how to load settings from a file at runtime, store sensitive settings safely, and make your application behave differently depending on which machine it’s running on. We’ll even use a bit of DI; it gets everywhere in ASP.NET Core!

在第 10 章中,我们将介绍 ASP.NET Core 配置模型。您将了解如何在运行时从文件加载设置,安全地存储敏感设置,以及使您的应用程序根据运行它的机器运行不同的行为。我们甚至会使用一点 DI;它在 ASP.NET Core 中无处不在!

9.6 Summary

9.6 总结

When registering your services, you describe three things: the service type, the implementation type, and the lifetime. The service type defines which class or interface will be requested as a dependency. The implementation type is the class the container should create to fulfill the dependency. The lifetime is how long an instance of the service should be used for.
在注册服务时,您需要描述三项内容:服务类型、实现类型和生命周期。服务类型定义将请求哪个类或接口作为依赖项。implementation type 是容器为实现依赖项而应创建的类。生存期是服务实例的使用时间。

You can register a service by using generic methods if the class is concrete and all its constructor arguments are registered with the container or have default values.
如果类是具体的,并且其所有构造函数参数都已注册到容器或具有默认值,则可以使用泛型方法注册服务。

You can provide an instance of a service during registration, which will register that instance as a singleton. This approach can be useful when you already have an instance of the service available.
您可以在注册期间提供服务的实例,该实例会将该实例注册为单一实例。当您已有可用的服务实例时,此方法可能很有用。

You can provide a lambda factory function that describes how to create an instance of a service with any lifetime you choose. You can take this approach when your services depend on other services that are accessible only when your application is running.
您可以提供一个 lambda 工厂函数,用于描述如何创建具有您选择的任何生命周期的服务实例。当您的服务依赖于仅在应用程序运行时才能访问的其他服务时,您可以采用此方法。

Avoid calling GetService() or GetRequiredService() in your factory functions if possible. Instead, favor constructor injection; it’s more performant and simpler to reason about.
如果可能,请避免在工厂函数中调用 GetService() 或 GetRequiredService()。 相反,支持构造函数注入;它的性能更高,推理也更简单。

You can register multiple implementations for a service. Then you can inject IEnumerable<T> to get access to all the implementations at runtime.
您可以为一个服务注册多个实现。然后,您可以注入 IEnumerable 以在运行时访问所有实现。

If you inject a single instance of a multiple-registered service, the container injects the last implementation registered.
如果您注入多个注册服务的单个实例,则容器将注入最后一个注册的实现。

You can use the TryAddextension methods to ensure that an implementation is registered only if no other implementation of the service has been registered. This approach can be useful for library authors to add default services while still allowing consumers to override the registered services.
您可以使用 TryAdd扩展方法确保仅在未注册服务的其他实施时注册实施。这种方法对于库作者来说非常有用,可以添加默认服务,同时仍允许使用者覆盖已注册的服务。

You define the lifetime of a service during DI service registration to dictate when a DI container will reuse an existing instance of the service to fulfill service dependencies and when it will create a new one.
在 DI 服务注册期间定义服务的生存期,以指示 DI 容器何时重用服务的现有实例来满足服务依赖项,以及何时创建新实例。

A transient lifetime means that every time a service is requested, a new instance is created.
瞬态生存期意味着每次请求服务时,都会创建一个新实例。

A scoped lifetime means that within a scope, all requests for a service will give you the same object. For different scopes, you’ll get different objects. In ASP.NET Core, each web request gets its own scope.
作用域生命周期意味着在一个范围内,对服务的所有请求都将为你提供相同的对象。对于不同的范围,你将获得不同的对象。在 ASP.NET Core 中,每个 Web 请求都有自己的范围。

You’ll always get the same instance of a singleton service, regardless of scope.
无论范围如何,您都将始终获得单一实例服务的相同实例。

A service should use only dependencies with a lifetime longer than or equal to the lifetime of the service. By default, ASP.NET Core performs scope validation to check for errors like this one and throws an exception when it finds them, but this feature is enabled only in development environments, as it has a performance cost.
服务应仅使用生命周期长于或等于服务生命周期的依赖项。默认情况下,ASP.NET Core 执行范围验证以检查此类错误,并在找到错误时引发异常,但此功能仅在开发环境中启用,因为它会降低性能。

To access scoped services in Program.cs, you must first create an IServiceScope object by calling CreateScope() or CreateAsyncScope() on WebApplication.Services. You can resolve services from the IServiceScope.ServiceProvider property. When you dispose IServiceScope, any scoped or transient services resolved from the scope are also disposed.
若要访问 Program.cs 中的分区服务,必须首先通过在 WebApplication 上调用 CreateScope() 或 CreateAsyncScope() 来创建 IServiceScope 对象。服务。可以从 IServiceScope.ServiceProvider 属性解析服务。释放 IServiceScope 时,还会释放从该范围解析的任何作用域或暂时性服务。