Category Archives: C#

ASP.NET Core in Action 8 An introduction to dependency injection

Part 2 Building complete applications

第 2 部分:构建完整的应用程序

We covered a lot of ground in part 1. You saw how an ASP.NET Core application is composed of middleware, and we focused heavily on minimal API endpoints. You saw how to use them to build JSON APIs, how to extract common code using filters and route groups, and how to validate your input data.

我们在第 1 部分中涵盖了很多内容。您了解了 ASP.NET Core 应用程序是如何由中间件组成的,我们主要关注最少的 API 端点。您了解了如何使用它们构建 JSON API,如何使用筛选条件和路由组提取通用代码,以及如何验证输入数据。

In part 2 we’ll dive deeper into the framework and look at a variety of components that you’ll inevitably need when you want to build more complex apps. By the end of this part, you’ll be able to build dynamic applications that can be deployed to multiple environments, each with a different configuration, saving data to a database.

在第 2 部分中,我们将更深入地研究该框架,并了解在构建更复杂的应用程序时不可避免地需要的各种组件。在本部分结束时,您将能够构建可部署到多个环境的动态应用程序,每个环境具有不同的配置,从而将数据保存到数据库。

ASP.NET Core uses dependency injection (DI) throughout its libraries, so it’s important that you understand how this design pattern works. In chapter 8 I introduce DI and discuss why it is used. In chapter 9 you’ll learn how to configure the services in your applications to use DI.

ASP.NET Core 在其整个库中使用依赖关系注入 (DI),因此了解此设计模式的工作原理非常重要。在第 8 章中,我将介绍 DI 并讨论使用它的原因。在第 9 章中,您将学习如何配置应用程序中的服务以使用 DI。

Chapter 10 looks at the ASP.NET Core configuration system, which lets you pass configuration values to your app from a range of sources—JSON files, environment variables, and many more. You’ll learn how to configure your app to use different values depending on the environment in which it is running, and how to bind strongly typed objects to your configuration to help reduce runtime errors.

第 10 章介绍了 ASP.NET Core 配置系统,它允许您将配置值从一系列来源(JSON 文件、环境变量等)传递给您的应用程序。您将学习如何将应用程序配置为根据运行环境使用不同的值,以及如何将强类型对象绑定到配置以帮助减少运行时错误。

In chapter 11 you’ll learn how to document your minimal APIs applications using the OpenAPI specification. Adding an OpenAPI document to your application makes it easier for others to interact with your app, but it has other benefits too. You’ll learn how to use Swagger UI to easily test your app from the browser, and code generation to automatically generate strongly-typed libraries for interacting with your API.

在第 11 章中,您将学习如何使用 OpenAPI 规范来记录您的最小 API 应用程序。将 OpenAPI 文档添加到您的应用程序可以使其他人更轻松地与您的应用程序交互,但它还有其他好处。您将学习如何使用 Swagger UI 从浏览器轻松测试您的应用程序,以及如何生成代码以自动生成用于与您的 API 交互的强类型库。

Most web applications require some sort of data storage, so in chapter 12 I’ll introduce Entity Framework Core (EF Core). This is a cross-platform library that makes it easier to connect your app to a database. EF Core is worthy of a book in and of itself, so I’ll only provide a brief introduction and point you to John Smith’s excellent book Entity Framework Core in Action, second edition (Manning, 2021). I’ll show you how to create a database and how to insert, update, and query simple data.

大多数 Web 应用程序都需要某种类型的数据存储,因此在第 12 章中,我将介绍 Entity Framework Core (EF Core)。这是一个跨平台库,可以更轻松地将应用程序连接到数据库。EF Core 本身就值得一本书,因此我只提供一个简短的介绍,并向您介绍 John Smith 的优秀著作 Entity Framework Core in Action,第二版(Manning,2021 年)。我将向您展示如何创建数据库以及如何插入、更新和查询简单数据。

8 An introduction to dependency injection

8 依赖注入简介

This chapter covers

本章涵盖

  • Understanding the benefits of dependency injection
    了解依赖关系注入的好处

  • Seeing how ASP.NET Core uses dependency injection
    了解 ASP.NET Core 如何使用依赖项注入

  • Retrieving services from the DI container
    从 DI 容器中检索服务

In part 1 of this book you saw the bare bones of how to build applications with ASP.NET Core. You learned how to compose middleware to create your application and how to create minimal API endpoints to handle HTTP requests. This part gave you the tools to start building simple API applications.

在本书的第 1 部分中,您了解了如何使用 ASP.NET Core 构建应用程序的基本内容。您学习了如何编写中间件来创建应用程序,以及如何创建最小的 API 端点来处理 HTTP 请求。这部分为您提供了开始构建简单 API 应用程序的工具。

In this chapter you’ll see how to use dependency injection (DI)—a design pattern that helps you develop loosely coupled code—in your ASP.NET Core applications. ASP.NET Core uses the pattern extensively, both internally in the framework and in the applications you build, so you’ll need to use it in all but the most trivial applications.

在本章中,您将了解如何在 ASP.NET Core 应用程序中使用依赖关系注入 (DI),这是一种帮助您开发松散耦合代码的设计模式。ASP.NET Core 在框架内部和您构建的应用程序中广泛使用该模式,因此您需要在除最琐碎的应用程序之外的所有应用程序中使用它。

You may have heard of DI and possibly even used it in your own applications. If so, this chapter shouldn’t hold many surprises for you. If you haven’t used DI, never fear; I’ll make sure you’re up to speed by the time the chapter is done!

您可能听说过 DI,甚至可能在自己的应用程序中使用它。如果是这样,这一章应该不会给你带来太多惊喜。如果您还没有使用过 DI,请不要担心;我会确保你在章节完成时跟上进度!

This chapter introduces DI in general, the principles it drives, and why you should care about it. You’ll see how ASP.NET Core has embraced DI throughout its implementation and why you should do the same when writing your own applications. Finally, you’ll learn how to retrieve services from DI in your app.

本章介绍了 DI 的一般情况、它驱动的原则以及您应该关注它的原因。您将了解 ASP.NET Core 如何在整个实施过程中采用 DI,以及为什么在编写自己的应用程序时也应该这样做。最后,您将学习如何从应用程序中的 DI 检索服务。

When you finish this chapter, you’ll have a solid understanding of the DI concept. In chapter 9 you’ll see how to apply DI to your own classes. You’ll learn 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 in your code manually. You’ll learn how to control how long your objects are used and some of the pitfalls to be aware of when you write your own applications. In chapter 31 we’ll look at some advanced ways to use DI, including how to wire up a third-party DI container.

完成本章后,您将对 DI 概念有深入的理解。在第 9 章中,您将看到如何将 DI 应用于您自己的类。您将学习如何配置您的应用程序,以便 ASP.NET Core 框架可以为您创建类,从而消除必须在代码中手动创建新对象的痛苦。您将学习如何控制对象的使用时间,以及在编写自己的应用程序时需要注意的一些陷阱。在第 31 章中,我们将介绍一些使用 DI 的高级方法,包括如何连接第三方 DI 容器。

For now, though, let’s get back to basics. What is DI, and why should you care about it?

不过,现在让我们回到基础。什么是 DI,为什么要关心它?

8.1 Understanding the benefits of dependency injection

8.1 了解依赖关系注入的好处

This section aims to give you a basic understanding of what DI is and why you should care about it. The topic itself extends far beyond the reach of this single chapter. If you want a deeper background, I highly recommend checking out Martin Fowler’s articles online. This article from 2004, for example, is a classic: http://mng.bz/pPJ8.

本节旨在让您对 DI 是什么以及为什么您应该关心它有一个基本的了解。这个话题本身远远超出了这一章的范围。如果您想要更深入的背景知识,我强烈建议您在线查看 Martin Fowler 的文章。例如,2004 年的这篇文章就是经典之作:http://mng.bz/pPJ8

Tip For a more directly applicable read with many examples in C#, I recommend picking up Dependency Injection Principles, Practices, and Patterns, by Steven van Deursen and Mark Seemann (Manning, 2019).
提示 要获得更直接适用的 C# 中许多示例,我建议您阅读 Steven van Deursen 和 Mark Seemann 编写的 Dependency Injection Principles, Practices, and Patterns(Manning,2019 年)。

The ASP.NET Core framework has been designed from the ground up to be modular and to adhere to good software engineering practices. As with anything in software, what is considered to be best practice varies over time, but for object-oriented programming, the SOLID principles have held up well.

ASP.NET Core 框架是从头开始设计的,采用模块化设计,并遵循良好的软件工程实践。与软件中的任何事物一样,被认为是最佳实践的内容会随着时间的推移而变化,但对于面向对象的编程,SOLID 原则一直保持得很好。

Definition SOLID is a mnemonic for “single responsibility principle, open-closed, Liskov substitution, interface segregation, and dependency inversion.” This course by Steve Smith introduces the principles using C#: http://mng.bz/Ox1R.
定义 SOLID 是“单一责任原则、开闭、Liskov 替换、接口分离和依赖关系倒置”的助记词。Steve Smith 的这门课程介绍了使用 C# 的原则:http://mng.bz/Ox1R

On that basis, ASP.NET Core has DI (sometimes called dependency inversion or inversion of control [IoC]) baked into the heart of the framework. Regardless of whether you want to use DI within your own application code, the framework libraries themselves depend on it as a concept.

在此基础上,ASP.NET Core 将 DI(有时称为依赖关系反转或控制反转 [IoC])融入到框架的核心中。无论您是否想在自己的应用程序代码中使用 DI,框架库本身都将其作为一个概念。

Note Although related, dependency injection and dependency inversion are two different things. I cover both in a general sense in this chapter, but for a good explanation of the differences, see this post by Derick Bailey, titled “Dependency Injection Is NOT the Same As the Dependency Inversion Principle”: http://mng.bz/5jvB.
注意 尽管相关,但依赖注入和依赖倒置是两个不同的东西。在本章中,我将从一般意义上介绍两者,但要很好地解释这些差异,请参阅 Derick Bailey 的这篇文章,标题为“依赖注入与依赖倒置原则不同”:http://mng.bz/5jvB

When you started programming, chances are that you didn’t use a DI framework immediately. That’s not surprising or even a bad thing; DI adds a certain amount of extra wiring that’s often not warranted in simple applications or when you’re getting started. But when things start to get more complex, DI comes into its own as a great tool to help keep that complexity under control.

当您开始编程时,您很可能没有立即使用 DI 框架。这并不奇怪,甚至不是一件坏事;DI 添加了一定数量的额外布线,这些布线在简单的应用程序中或您开始时通常是不需要的。但是当事情开始变得更加复杂时,DI 就会成为帮助控制这种复杂性的绝佳工具。

Let’s consider a simple example, written without any sort of DI. Suppose that a user has registered on your web app, and you want to send them an email. This listing shows how you might approach this task initially, using a minimal API endpoint handler.

让我们考虑一个简单的例子,它没有任何类型的 DI 编写。假设用户已在您的 Web 应用程序上注册,并且您希望向他们发送电子邮件。此清单显示了最初如何使用最小的 API 端点处理程序来处理此任务。

Listing 8.1 Sending an email without DI when there are no dependencies
清单 8.1 在没有依赖项的情况下发送没有 DI 的邮件

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

app.MapGet("/register/{username}", RegisterUser); ❶

app.Run();

string RegisterUser(string username) ❷
{
    var emailSender = new EmailSender(); ❸
    emailSender.SendEmail(username); ❹
    return $"Email sent to {username}!";
}

❶ The endpoint is called when a new user is created.
创建新用户时调用 endpoint。

❷ The RegisterUser function is the handler for the endpoint.
RegisterUser 函数是端点的处理程序。

❸ Creates a new instance of EmailSender
创建 EmailSender 的新实例

❹ Uses the new instance to send the email
使用新实例发送电子邮件

In this example, the RegisterUser handler executes when a new user registers on your app, creating a new instance of an EmailSender class and calling SendEmail() to send the email. The EmailSender class is the class that actually sends the email. For the purposes of this example, you can imagine that it looks something like this:

在此示例中,当新用户在您的应用程序上注册时,将执行处理程序,创建类的新实例并调用以发送电子邮件。类是实际发送电子邮件的类。对于此示例,您可以想象它看起来像这样:

public class EmailSender
{
    public void SendEmail(string username)
    {
        Console.WriteLine($"Email sent to {username}!");
    }
}

Console.WriteLine stands in here for the real process of sending the email.

Console.WriteLine 在此处代替发送电子邮件的真实过程。

Note Although I’m using sending email as a simple example, in practice you may want to move this code out of your handler method. This type of asynchronous task is well suited to using message queues and a background process. For more details, see http://mng.bz/Y1AB.
注意 虽然我使用发送电子邮件作为一个简单的示例,但实际上您可能希望将此代码从 handler 方法中移出。这种类型的异步任务非常适合使用消息队列和后台进程。有关更多详细信息,请参阅 http://mng.bz/Y1AB

If the EmailSender class is as simple as the previous example and has no dependencies, you may not see any need to adopt a different approach to creating objects. And to an extent, you’d be right. But what if you later update your implementation of EmailSender so that some of the email-sending logic is implemented by a different class?

如果 EmailSender 类与上一个示例一样简单,并且没有依赖项,则您可能不需要采用其他方法来创建对象。在某种程度上,你是对的。但是,如果您稍后更新 EmailSender 的实现,以便某些电子邮件发送逻辑由不同的类实现,该怎么办?

Currently, EmailSender would need to do many things to send an email. It would need to

目前,EmailSender 需要做很多事情来发送电子邮件。它需要

  • Create an email message.
    创建电子邮件。

  • Configure the settings of the email server.
    配置电子邮件服务器的设置。

  • Send the email to the email server.
    将电子邮件发送到电子邮件服务器。

Doing all that in one class would go against the single-responsibility principle (SRP), so you’d likely end up with EmailSender depending on other services. Figure 8.1 shows how this web of dependencies might look. RegisterUser wants to send an email using EmailSender, but to do so, it also needs to create the MessageFactory, NetworkClient, and EmailServerSettings objects that EmailSender depends on.
在一个类中完成所有这些作将违反单一责任原则 (SRP),因此您最终可能会使用 EmailSender,具体取决于其他服务。图 8.1 显示了这个依赖关系网络可能是什么样子。RegisterUser 希望使用 EmailSender 发送电子邮件,但为此,它还需要创建 EmailSender 所依赖的 MessageFactory、NetworkClient 和 EmailServerSettings 对象。

alt text

Figure 8.1 Dependency diagram without dependency injection. RegisterUser indirectly depends on all the other classes, so it must create them all.
图 8.1 没有依赖注入的依赖图。RegisterUser 间接依赖于所有其他类,因此它必须创建所有类。

Each class has several dependencies, so the “root” caller—in this case, the RegisterUser handler—needs to know how to create every class it depends on, as well as every class its dependencies depend on. This is sometimes called the dependency graph.

每个类都有多个依赖项,因此“根”调用方(在本例中为 RegisterUser 处理程序)需要知道如何作创建它所依赖的每个类,以及它的依赖项所依赖的每个类。这有时称为依赖关系图。

Definition The dependency graph is the set of objects that must be created to create a specific requested “root” object.
定义 依赖关系图是创建特定请求的 “root” 对象时必须创建的对象集。

EmailSender depends on the MessageFactory and NetworkClient objects, so they’re provided via the constructor, as shown in the following listing.

EmailSender 依赖于 MessageFactory 和 NetworkClient 对象,因此它们是通过构造函数提供的,如下面的清单所示。

Listing 8.2 A service with multiple dependencies
清单 8.2 具有多个依赖项的服务

public class EmailSender
{
    private readonly NetworkClient _client; ❶
    private readonly MessageFactory _factory; ❶
    public EmailSender(MessageFactory factory, NetworkClient client) ❷
    { ❷
        _factory = factory; ❷
        _client = client; ❷
    } ❷
    public void SendEmail(string username)
    {
        var email = _factory.Create(username); ❸
        _client.SendEmail(email); ❸
        Console.WriteLine($"Email sent to {username}!");
    }
}

❶ Now the EmailSender depends on two other classes.
现在 EmailSender 依赖于其他两个类。

❷ Instances of the dependencies are provided in the constructor.
构造函数中提供了依赖项的实例。

❸ The EmailSender coordinates the dependencies to create and send an email.
EmailSender 协调依赖项以创建和发送电子邮件。

On top of that, the NetworkClient class that EmailSender depends on also has a dependency on an EmailServerSettings object:
最重要的是,NetworkClient 类EmailSender 也依赖于EmailServerSettings 对象:

public class NetworkClient
{
    private readonly EmailServerSettings _settings;
    public NetworkClient(EmailServerSettings settings)
    {
        _settings = settings;
    }
}

This example might feel a little contrived, but it’s common to find this sort of chain of dependencies. In fact, if you don’t have it in your code, it’s probably a sign that your classes are too big and aren’t following the SRP.

这个例子可能感觉有点做作,但找到这种依赖关系链是很常见的。事实上,如果您的代码中没有它,则可能表明您的类太大并且没有遵循 SRP。

So how does this affect the code in RegisterUser? The following listing shows how you now have to send an email if you stick to newing up objects in the handler.

那么这对 RegisterUser 中的代码有什么影响呢?下面的清单显示了如果你坚持在处理程序中使用新的ing 对象,你现在必须如何发送电子邮件。

Listing 8.3 Sending email without DI when you create dependencies manually
清单 8.3 手动创建依赖项时发送没有 DI 的邮件

string RegisterUser(string username)
{
    var emailSender = new EmailSender( ❶
        new MessageFactory(), ❷
        new NetworkClient( ❸
            new EmailServerSettings ❹
            ( ❹
                Host: "smtp.server.com", ❹
                Port: 25 ❹
            )) ❹
        );
    emailSender.SendEmail(username); ❺
    return $"Email sent to {username}!";
}

❶ 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.
最后,您可以发送电子邮件。

This code is turning into something gnarly. Improving the design of EmailSender to separate out the responsibilities has made calling it from RegisterUser a real chore. This code has several problems:

这段代码正在变成一些粗糙的东西。改进 EmailSender 的设计以分离职责,这使得从 RegisterUser 调用它成为一件真正的苦差事。此代码有几个问题:

  • Not obeying the SRP—Now our code is responsible for both creating an EmailSender object and using it to send an email.
    不遵守 SRP – 现在,我们的代码负责创建 EmailSender 对象并使用它来发送电子邮件。

  • Considerable ceremony—Ceremony refers to code that you have to write but that isn’t adding value directly. Of the 11 lines of code in the RegisterUser method, only the last two are doing anything useful, which makes it harder to read and harder to understand the intent of the methods.
    相当大的仪式 — 仪式是指您必须编写的代码,但这不会直接增加价值。在 RegisterUser 方法的 11 行代码中,只有最后两行代码执行任何有用的作,这使得它更难阅读,也更难理解方法的意图。

  • Tied to the implementation—If you decide to refactor EmailSender and add another dependency, you’d need to update every place it’s used. Likewise, if any dependencies are refactored, you would need to update this code too.
    与实现绑定 — 如果您决定重构 EmailSender 并添加另一个依赖项,则需要更新使用它的每个位置。同样,如果重构了任何依赖项,您也需要更新此代码。

  • Hard to reuse instance—In the example code we created new instances of all the objects. But what if creating a new NetworkClient is computationally expensive and we’d like to reuse instances? We’d have to add extra code to handle that task, further increasing the amount of boilerplate code.
    难以重用实例 – 在示例代码中,我们创建了所有对象的新实例。但是,如果创建新的 NetworkClient 计算成本很高,并且我们想重用实例怎么办?我们必须添加额外的代码来处理该任务,从而进一步增加样板代码的数量。

RegisterUser has an implicit dependency on the EmailSender class, as it creates the object manually itself. The only way to know that RegisterUser uses EmailSender is to look at its source code. By contrast, EmailSender has explicit dependencies on NetworkClient and MessageFactory, which must be provided in the constructor. Similarly, NetworkClient has an explicit dependency on the EmailServerSettings class.

RegisterUser 对 EmailSender 类具有隐式依赖关系,因为它自己手动创建对象。要知道 RegisterUser 使用 EmailSender,唯一的方法是查看其源代码。相比之下,EmailSender 对 NetworkClient 和 MessageFactory 具有显式依赖项,必须在构造函数中提供。同样,NetworkClient 对 EmailServerSettings 类具有显式依赖项。

Tip Generally speaking, any dependencies in your code should be explicit, not implicit. Implicit dependencies are hard to reason about and difficult to test, so you should avoid them wherever you can. DI is useful for guiding you along this path.
提示 一般来说,代码中的任何依赖项都应该是显式的,而不是隐式的。隐式依赖关系很难推理且难以测试,因此您应该尽可能避免使用它们。DI 有助于指导您沿着这条道路前进。

DI aims to solve the problem of building a dependency graph by inverting the chain of dependencies. Instead of the RegisterUser handler creating its dependencies manually, deep inside the implementation details of the code, an already-created instance of EmailSender is passed as an argument to the RegisterUser method.

DI 旨在通过反转依赖关系链来解决构建依赖关系图的问题。RegisterUser 处理程序不是手动创建其依赖项,而是在代码的实现细节深处,将已创建的 EmailSender 实例作为参数传递给 RegisterUser 方法。

Now, obviously something needs to create the object, so the code to do that has to live somewhere. The service responsible for providing the instance is called a DI container or an IoC container, as shown in figure 8.2.

现在,显然需要创建对象,因此执行此作的代码必须位于某个位置。负责提供实例的服务称为 DI 容器或 IoC 容器,如图 8.2 所示。

Definition The DI container or IoC container is responsible for creating instances of services. It knows how to construct an instance of a service by creating all its dependencies and passing them to the constructor. I’ll refer to it as a DI container throughout this book.
定义 DI 容器或 IoC 容器负责创建服务实例。它知道如何通过创建服务的所有依赖项并将它们传递给构造函数来构造服务的实例。在本书中,我将它称为 DI 容器。

alt text

Figure 8.2 Dependency diagram using DI . RegisterUser indirectly depends on all the other classes but doesn’t need to know how to create them. The RegisterUser handler declares that it requires EmailSender, and the container provides it.
图 8.2 使用 DI 的依赖关系图 .RegisterUser 间接依赖于所有其他类,但不需要知道如何创建它们。RegisterUser 处理程序声明它需要 EmailSender,并且容器提供它。

The term DI is often used interchangeably with IoC. But DI is a specific version of the more general principle of IoC. In the context of ASP.NET Core,

术语 DI 通常与 IoC 互换使用。但 DI 是 IoC 更普遍原则的特定版本。在 ASP.NET Core 的上下文中,

  • Without IoC, you’d write the code to listen for requests, check which handler to invoke, and then invoke it. With IoC, the control flow is the other way around. You register your handlers with the framework, but it’s up to the framework to invoke your handler. Your handler is still responsible for creating its dependencies.
    如果没有 IoC,您将编写代码来侦听请求,检查要调用的处理程序,然后调用它。对于 IoC,控制流正好相反。您可以向框架注册处理程序,但由框架来调用您的处理程序。您的处理程序仍负责创建其依赖项。

  • DI takes IoC one step further. As well as invoking your handler, with DI, the framework creates all your handler’s dependencies.
    DI 使 IoC 更进一步。除了调用您的处理程序外,框架还使用 DI 创建所有处理程序的依赖项。

So when you use dependency injection, your RegisterUser handler is no longer responsible for controlling how to create an EmailSender instance. Instead, the framework provides an EmailSender to the handler directly.
因此,当您使用依赖项注入时,您的 RegisterUser 处理程序不再负责控制如何创建 EmailSender 实例。相反,框架直接向处理程序提供 EmailSender。

Note Many DI containers are available for .NET, including Autofac, Lamar, Unity, Ninject, and Simple Injector, and the list goes on! In chapter 31 you’ll see how to replace the default ASP.NET Core container with one of these alternatives.
注意 许多 DI 容器可用于 .NET,包括 Autofac、Lamar、Unity、Ninject 和 Simple Injector,不胜枚举!在第 31 章中,您将看到如何将default ASP.NET Core 容器中替换为这些替代项之一。

The advantage of adopting this pattern becomes apparent when you see how much it simplifies using dependencies. Listing 8.4 shows how the RegisterUser handler would look if you used DI to create EmailSender instead of creating it manually. All the new noise has gone, and you can focus purely on what the endpoint handler is doing: calling EmailSender and returning a string message.

当您看到采用此模式在多大程度上简化了依赖项的使用时,采用此模式的优势就变得显而易见了。清单 8.4 显示了如果使用 DI 创建 EmailSender 而不是手动创建 EmailSender,RegisterUser 处理程序会是什么样子。所有新的干扰都已消失,您可以专注于端点处理程序正在执行的作:调用 EmailSender 并返回字符串消息。

Listing 8.4 Sending an email using DI to inject dependencies
清单 8.4 使用 DI 发送邮件注入依赖

string RegisterUser(string username, EmailSender emailSender) ❶
{
    emailSender.SendEmail(username); ❷
    return $"Email sent to {username}!"; ❷
}

❶ Instead of creating the dependencies implicitly, injects them directly
不是隐式创建依赖项,而是直接注入它们

❷ The handler is easy to read and understand again.
处理程序易于阅读和理解。

One advantage of a DI container is that it has a single responsibility: creating objects or services. The minimal API infrastructure asks the DI container for an instance of a service, and the container takes care of figuring out how to create the dependency graph, based on how you configure it.

DI 容器的一个优点是它只有一个职责:创建对象或服务。最小的 API 基础设施向 DI 容器请求服务实例,容器负责根据您的配置方式确定如何创建依赖关系图。

Note It’s common to refer to services when talking about DI containers, which is slightly unfortunate, as services is one of the most overloaded terms in software engineering! In this context, a service refers to any class or interface that the DI container creates when required.
注意 在谈论 DI 容器时,通常会提到服务,这有点遗憾,因为服务是软件工程中最超载的术语之一!在这个context,服务是指 DI 容器在需要时创建的任何类或接口。

The beauty of this approach is that by using explicit dependencies, you never have to write the mess of code you saw in listing 8.3. The DI container can inspect your service’s constructor and work out how to write much of the code itself. DI containers are always configurable, so if you want to describe how to create an instance of a service manually, you can, but by default you shouldn’t need to.

这种方法的美妙之处在于,通过使用显式依赖项,您永远不必编写清单 8.3 中看到的混乱代码。DI 容器可以检查服务的构造函数,并找出如何编写大部分代码本身。DI 容器始终是可配置的,因此如果您想描述如何手动创建服务实例,您可以这样做,但默认情况下您不需要这样做。

Tip ASP.NET Core supports constructor injection and injection into endpoint handler methods out of the box. Technically, you can inject dependencies into a service in other ways, such as by using property injection, but these techniques aren’t supported by the built-in DI container.
提示 ASP.NET Core 支持构造函数注入和开箱即用的端点处理程序方法注入。从技术上讲,您可以通过其他方式将依赖项注入服务,例如使用属性注入,但内置 DI 容器不支持这些技术。

I hope that this example made the advantages of using DI in your code apparent, but in many ways these benefits are secondary to the main benefit of using DI. In particular, DI helps keep your code loosely coupled by coding to interfaces.

我希望这个例子清楚地表明了在代码中使用 DI 的优势,但在许多方面,这些好处是次要的,而不是使用 DI 的主要好处。特别是,DI 通过编码到接口来帮助保持代码松散耦合。

8.2 Creating loosely coupled code

8.2 创建松散耦合的代码

Coupling is an important concept in object-oriented programming, referring to how a given class depends on other classes to perform its function. Loosely coupled code doesn’t need to know a lot of details about a particular component to use it.

耦合是面向对象编程中的一个重要概念,指的是给定的类如何依赖其他类来执行其功能。松散耦合的代码不需要了解有关特定组件的大量详细信息即可使用它。

The initial example of RegisterUser and EmailSender was an example of tight coupling; you were creating the EmailSender object directly and needed to know exactly how to wire it up. On top of that, the code was difficult to test. Any attempts to test RegisterUser would result in an email’s being sent. If you were testing the controller with a suite of unit tests, that approach would be a surefire way to get your email server blacklisted for spam!

RegisterUser 和 EmailSender 的初始示例是紧密耦合的示例;您直接创建了 EmailSender 对象,并且需要确切地知道如何连接它。最重要的是,代码很难测试。任何测试 RegisterUser 的尝试都会导致发送电子邮件。如果你正在使用一套单元测试来测试控制器,那么这种方法将是让你的电子邮件服务器被列入垃圾邮件黑名单的可靠方法!

Taking EmailSender as a parameter and removing the responsibility of creating the object helps reduce the coupling in the system. If the EmailSender implementation changes so that it has another dependency, you no longer have to update RegisterUser at the same time.

将 EmailSender 作为参数并消除创建对象的责任有助于减少系统中的耦合。如果 EmailSender 实现发生更改,使其具有另一个依赖项,则不再需要同时更新 RegisterUser。

One problem that remains is that RegisterUser is still tied to an implementation rather than an abstraction. Coding to abstractions (often interfaces) is a common design pattern that helps further reduce the coupling of a system, as you’re not tied to a single implementation. This pattern is particularly useful for making classes testable, as you can create stub or mock implementations of your dependencies for testing purposes, as shown in figure 8.3.

仍然存在的一个问题是 RegisterUser 仍然与实现而不是抽象相关联。对抽象(通常是接口)进行编码是一种常见的设计模式,有助于进一步减少系统的耦合,因为您不受单个实现的束缚。此模式对于使类可测试特别有用,因为您可以创建依赖项的存根或模拟实现以进行测试,如图 8.3 所示。

Tip You can choose among many mocking frameworks. I’m most familiar with Moq, but NSubstitute and FakeItEasy are also popular options.
提示: 您可以在许多模拟框架中进行选择。我最熟悉 Moq,但 NSubstitute 和 FakeItEasy 也是受欢迎的选择。

alt text

Figure 8.3 By coding to interfaces instead of an explicit implementation, you can use different IEmailSender implementations in different scenarios, such as a MockEmailSender in unit tests.
图 8.3 通过对接口进行编码而不是显式实现,可以在不同的场景中使用不同的 IEmailSender 实现,例如单元测试中的 MockEmailSender。

As an example, you might create an IEmailSender interface, which EmailSender would implement:
例如,您可以创建一个 IEmailSender接口,EmailSender 将实现该接口:

public interface IEmailSender
{
    public void SendEmail(string username);
}

Then RegisterUser could depend on this interface instead of the specific EmailSender implementation, as shown in the following listing, allowing you to use a different implementation during unit tests, such as a DummyEmailSender.

然后 RegisterUser 可以依赖于此接口而不是特定的 EmailSender 实现,如下面的清单所示,从而允许您在单元测试期间使用不同的实现,例如 DummyEmailSender。

Listing 8.5 Using interfaces with dependency injection
清单 8.5 使用带有依赖注入的接口

string RegisterUser(string username, IEmailSender emailSender) ❶
{
    emailSender.SendEmail(username); ❷
    return $"Email sent to {username}!";
}

❶ Now you depend on IEmailSender instead of the specific EmailSender
implementation.
现在您依赖于 IEmailSender 而不是特定的 EmailSender 实现。

❷ You don’t care what the implementation is as long as it implements
IEmailSender.
您不关心实现是什么,只要它实现 IEmailSender 即可。

The key point here is that the consuming code, RegisterUser, doesn’t care how the dependency is implemented—only that it implements the IEmailSender interface and exposes a SendEmail method. Now the application code is independent of the implementation.

此处的关键点是,使用代码 RegisterUser 并不关心依赖项是如何实现的,只关心它实现 IEmailSender 接口并公开 SendEmail 方法。现在,应用程序代码独立于实现。

I hope that the principles behind DI seem to be sound. Having loosely coupled code makes it easy to change or swap out implementations. But this still leaves a question: how does the application know to use EmailSender in production instead of DummyEmailSender? The process of telling your DI container “When you need IEmailSender, use EmailSender” is called registration.

我希望 DI 背后的原则似乎是合理的。拥有松散耦合的代码可以很容易地更改或换出实现。但这仍然留下了一个问题:应用程序如何知道在生产环境中使用 EmailSender 而不是 DummyEmailSender?告诉 DI 容器“当您需要 IEmailSender 时,请使用 EmailSender”的过程称为注册。

Definition You register services with a DI container so that it knows which implementation to use for each requested service. This registration typically takes the form “For interface X, use implementation Y.”
定义 您可以向 DI 容器注册服务,以便它知道要为每个请求的服务使用哪个实现。此注册通常采用“对于接口 X,使用实现 Y”的形式。

Exactly how you register your interfaces and types with a DI container can vary depending on the specific DI container implementation, but the principles are generally the same. ASP.NET Core includes a simple DI container out of the box, so let’s look at how it’s used during a typical request.
向 DI 容器注册接口和类型的具体方式可能因特定的 DI 容器而异实现,但原则通常是相同的。ASP.NET Core 包含一个简单的开箱即用的 DI 容器,因此让我们看看在典型请求期间如何使用它。

8.3 Using dependency injection in ASP.NET Core

在 ASP.NET Core 中使用依赖项注入

ASP.NET Core was designed from the outset to be modular and composable, with an almost plugin-style architecture, which is generally complemented by DI. Consequently, ASP.NET Core includes a simple DI container that all the framework libraries use to register themselves and their dependencies.

ASP.NET Core 从一开始就设计为模块化和可组合的,具有几乎插件式的架构,通常由 DI 补充。因此,ASP.NET Core 包含一个简单的 DI 容器,所有框架库都使用它来注册自身及其依赖项。

This container is used, for example, to register the minimal API infrastructure—the formatters, the Kestrel web server, and so on. It’s a basic container, so it exposes only a few methods for registering services, but you have the option to replace it with a third-party DI container that gives you extra capabilities, such as autoregistration and setter injection. The DI container is built into the ASP.NET Core hosting model, as shown in figure 8.4.

例如,此容器用于注册最小 API 基础设施 — 格式化程序、Kestrel Web 服务器等。它是一个基本容器,因此它只公开了几种用于注册服务的方法,但您可以选择将其替换为第三方 DI 容器,该容器为您提供额外的功能,例如自动注册和 setter 注入。DI 容器内置于 ASP.NET Core 托管模型中,如图 8.4 所示。

alt text

Figure 8.4 The ASP.NET Core hosting model uses the DI container to fulfill dependencies when creating minimal API endpoint handlers.
图 8.4 ASP.NET Core 托管模型在创建最小 API 端点处理程序时使用 DI 容器来实现依赖项。

The hosting model pulls dependencies from the DI container when they’re needed. If the framework determines that it must invoke RegisterHandler due to the incoming URL/route, the RequestDelegateFactory responsible for creating minimal APIs asks the DI container for an IEmailSender implementation.

托管模型在需要时从 DI 容器中提取依赖项。如果框架确定它必须调用 RegisterHandler,因为传入URL/路由,负责创建最小 API 的 RequestDelegateFactory 向 DI 容器请求 IEmailSender 实现。

Note RequestDelegateFactory is part of the minimal API framework that’s responsible for invoking your minimal API handlers. You won’t use or interact with it directly, but it’s behind the scenes interacting with the DI container. I have a detailed series exploring this type on my blog at http://mng.bz/Gy6v. But be warned: this post goes into far more detail than most developers will ever need (or want)!
注意: RequestDelegateFactory 是负责调用最小 API 处理程序的最小 API 框架的一部分。您不会直接使用它或与之交互,但它在幕后与 DI 容器交互。我在 http://mng.bz/Gy6v 的博客上有一个详细的系列来探索这种类型的类型。但请注意:这篇文章比大多数开发人员需要(或想要)的要详细得多!

The DI container needs to know what to create when asked for IEmailSender, so you must have registered an implementation, such as EmailSender, with the container. When an implementation is registered, the DI container can inject it anywhere, which means that you can inject framework-related services (such as LinkGenerator from chapter 6) into your own custom services. It also means that you can register alternative versions of framework services and have the framework automatically use those versions in place of the defaults.

DI 容器需要知道在请求 IEmailSender 时要创建什么,因此您必须已向容器注册一个实现,例如 EmailSender。注册实现后,DI 容器可以将其注入到任何位置,这意味着您可以将与框架相关的服务(例如第 6 章中的 LinkGenerator)注入到您自己的自定义服务中。这也意味着您可以注册框架服务的替代版本,并让框架自动使用这些版本来代替默认版本。

Other ASP.NET Core infrastructure, such as the Model-View-Controller (MVC) and Razor Pages frameworks (which you learn about in part 3), uses dependency injection in a similar way to minimal APIs. These frameworks use the DI container to create the dependencies required by their own handlers, such as for a Razor Page (figure 8.5).

其他 ASP.NET Core 基础结构,例如模型视图控制器 (MVC) 和 Razor Pages 框架(在第 3 部分中介绍),以与最小 API 类似的方式使用依赖项注入。这些框架使用 DI 容器创建其自己的处理程序所需的依赖项,例如 Razor Page(图 8.5)。

alt text

Figure 8.5 The ASP.NET Core hosting model uses the DI container to fulfill dependencies when creating Razor Pages.
图 8.5 ASP.NET Core 托管模型在创建 Razor Pages 时使用 DI 容器来实现依赖项。

The flexibility to choose exactly how and which components you combine in your applications is one of the selling points of DI. In section 8.4 you’ll learn how to configure DI in your own ASP.NET Core application, using the default, built-in container.
DI 的卖点之一是可以灵活地选择在应用程序中组合的组件以及组合哪些组件。在第 8.4 节中,您将学习如何在拥有 ASP.NET Core 应用程序,使用默认的内置容器。

8.4 Adding ASP.NET Core framework services to the container

8.4 将 ASP.NET Core Framework 服务添加到容器中

Before ASP.NET Core, using DI was optional. By contrast, to build all but the most trivial ASP.NET Core apps, some degree of DI is required. As I’ve mentioned, the underlying framework depends on it, so features such as Razor Pages and authentication require you to configure the required services. In this section you’ll see how to register these framework services with the built-in container. In chapter 9 you’ll learn how to register your own services with the DI container.

在 ASP.NET Core 之前,使用 DI 是可选的。相比之下,要构建除最琐碎的 ASP.NET Core 应用程序之外的所有应用程序,需要一定程度的 DI。正如我所提到的,底层框架依赖于它,因此 Razor Pages 和身份验证等功能要求您配置所需的服务。在本节中,您将了解如何使用内置容器注册这些框架服务。在第 9 章中,您将学习如何使用 DI 容器注册自己的服务。

ASP.NET Core uses DI to configure both its internal components, such as the Kestrel web server, and extra features, such as Razor Pages. To use these components at runtime, the DI container needs to know about all the classes it will need. You register these services with the Services property on the WebApplicationBuilder instance in Program.cs.

ASP.NET Core 使用 DI 来配置其内部组件(如 Kestrel Web 服务器)和额外功能(如 Razor Pages)。要在运行时使用这些组件,DI 容器需要了解它需要的所有类。使用 Program.cs 中 WebApplicationBuilder 实例上的 Services 属性注册这些服务。

Note The Services property of WebApplicationBuilder is of type IServiceCollection. This is where you register the collection of services that the DI container knows about.
注意 的 Services 属性WebApplicationBuilder 的类型为IServiceCollection 的 API API 的您可以在此处注册 DI 容器知道的服务集合。

If you’re thinking “Wait—I have to configure all the internal components myself?”, don’t panic. Most of the core services are registered automatically by WebApplicationBuilder, and you don’t need to do anything else. To use other features, such as Razor Pages or authentication, you do need to register the components explicitly with the container in your app, but that’s not as hard as it sounds. All the common libraries you use expose handy extension methods to take care of the nitty-gritty details. These extension methods configure everything you need in one fell swoop instead of leaving you to wire everything up manually.

如果您在想“等等 — 我必须自己配置所有内部组件”,请不要惊慌。大多数核心服务都是由 WebApplicationBuilder 自动注册的,您无需执行任何其他作。要使用其他功能(如 Razor Pages 或身份验证),您确实需要向应用程序中的容器显式注册组件,但这并不像听起来那么难。您使用的所有公共库都公开了方便的扩展方法,以处理细节。这些扩展方法一举配置了您需要的一切,而不是让您手动连接所有内容。

The Razor Pages framework exposes the AddRazorPages() extension method, for example, which adds all the necessary framework services to your app. Invoke the extension method on the Services property of WebApplicationBuilder in Program.cs, as shown in the following listing.

例如,Razor Pages 框架公开了 AddRazorPages() 扩展方法,该方法将所有必要的框架服务添加到您的应用程序。在 Program.cs 中调用 WebApplicationBuilder 的 Services 属性上的扩展方法,如下面的清单所示。

Listing 8.6 Registering the Razor Pages services with the DI container

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

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

❶ The AddRazorPages extension method adds all necessary services to the
IServiceCollection.
AddRazorPages 扩展方法将所有必要的服务添加到 IServiceCollection。

❷ Registers all the Razor Pages in your application as endpoints
将应用程序中的所有 Razor 页面注册为终结点

It’s as simple as that. Under the hood, this call is registering multiple components with the DI container, using the same APIs you’ll see in chapter 9 for registering your own services.
就这么简单。在后台,此调用使用您将在第 9 章中看到的相同 API 向 DI 容器注册多个组件,以注册您自己的服务。

Note Don’t worry about the Razor Pages aspect of this code; you’ll learn how Razor Pages work in part 3. The important point of listing 8.6 is to show how to register and enable various features in ASP.NET Core.
注意 不必担心此代码的 Razor Pages 方面;您将在第 3 部分中了解 Razor Pages 的工作原理。列出 8.6 的重要一点是展示如何在 ASP.NET Core 中注册和启用各种功能。

Most nontrivial libraries that you add to your application will have services that you need to add to the DI container. By convention, each library that has necessary services should expose an Add*() extension method that you can call on WebApplicationBuilder.Services.

您添加到应用程序中的大多数重要库都将包含您需要添加到 DI 容器中的服务。按照约定,每个具有必要服务的库都应该公开一个 Add*() 扩展方法,您可以在 WebApplicationBuilder.Services 上调用该方法。

There’s no way of knowing exactly which libraries will require you to add services to the container; it’s generally a case of checking the documentation for any libraries you use. If you forget to add them, you may find that the functionality doesn’t work, or you might get a handy exception in your logs, like the one shown in figure 8.6. Keep an eye out for these exceptions, and be sure to register any services you need.

无法确切知道哪些库需要您向容器添加服务;通常是检查您使用的任何库的文档的情况。如果你忘记添加它们,你可能会发现该功能不起作用,或者你可能会在日志中得到一个方便的异常,如图 8.6 所示。请留意这些例外情况,并确保注册您需要的任何服务。

alt text

Figure 8.6 If you fail to call AddRazorPages(), you’ll get an exception when your app tries to start.
图 8.6 如果在使用 Razor Pages 的应用程序中调用 AddRazorPages() 失败,则当应用程序尝试启动时将出现异常。

It’s also worth noting that some of the Add*() extension methods allow you to specify additional options when you call them, often by way of a lambda expression. You can think of these options as configuring the installation of a service into your application. The AddRazorPages method, for example, provides a wealth of options for fine-tuning its behavior if you want to get your hands dirty, as shown by the IntelliSense snippet in figure 8.7.

还值得注意的是,某些 Add*() 扩展方法允许您在调用它们时指定其他选项,通常是通过 lambda 表达式。您可以将这些选项视为在应用程序中配置服务安装。例如,AddRazorPages 方法提供了大量选项,用于微调其行为(如果您想动手),如图 8.7 中的 IntelliSense 代码段所示。

alt text

Figure 8.7 Configuring services when adding them to the service collection. The AddRazorPages() function allows you to configure a wealth of the internals of the framework.
图 8.7 在将服务添加到服务集合时配置服务。AddRazorPages() 函数允许您配置框架的大量内部结构。

It’s all very well registering services with the DI container, but the important question is how to use the container to get an instance of a registered service. In section 8.5 we look at two possible ways to access these services and discuss when you should choose one over the other.

向 DI 容器注册服务一切都很好,但重要的问题是如何使用容器来获取已注册服务的实例。在 Section 8.5 中,我们将介绍访问这些服务的两种可能方法,并讨论何时应该选择其中一种。

8.5 Using services from the DI container

8.5 使用 DI 容器中的服务

In a minimal API application, you have two main ways to access services from the DI container:
在最小 API 应用程序中,有两种主要方法可以从 DI 容器访问服务:

  • Inject services into an endpoint handler.
    将服务注入到终端节点处理程序中。

  • Access the DI container directly in Program.cs.
    直接在 Program.cs 中访问 DI 容器。

The first approach—injecting services into an endpoint handler—is the most common way to access the root of a dependency graph. You should use this approach in almost all cases in your minimal API applications. You can inject a service into an endpoint handler by adding it as a parameter to your endpoint handler method, as you saw in chapters 6 and 7 when you injected a LinkGenerator instance into your handler.

第一种方法 — 将服务注入终端节点处理程序 — 是访问依赖关系图根的最常用方法。在最小 API 应用程序中,几乎在所有情况下都应该使用此方法。您可以通过将服务作为参数添加到端点处理程序方法中,将服务注入到端点处理程序中,就像您在第 6 章和第 7 章中看到的那样,当您将 LinkGenerator 实例注入到处理程序中时。

Listing 8.7 Injecting the LinkGenerator service in an endpoint handler

app.MapGet("/links", (LinkGenerator links) => ❶
{
    string link = links.GetPathByName("products");
    return $"View the product at {link}";
});

❶ The DI container creates a LinkGenerator instance and passes it as the argument to the handler.
DI 容器创建一个 LinkGenerator 实例,并将其作为参数传递给处理程序。

The minimal API infrastructure sees that you need an instance of the LinkGenerator, which is a service registered in the container, and asks the DI container to provide an instance of the service. The DI container either creates a new instance of LinkGenerator (or reuses an existing one) and returns it to the minimal API infrastructure. Then the LinkGenerator is passed as an argument to invoke the endpoint handler.

最小的 API 基础结构看到您需要一个 LinkGenerator 的实例,该实例是在容器中注册的服务,并要求 DI 容器提供服务的实例。DI 容器创建 LinkGenerator 的新实例(或重用现有实例)并将其返回到最小的 API 基础结构。然后,将 LinkGenerator 作为参数传递以调用端点处理程序。

Note Whether the DI container creates a new instance or reuses an existing instance depends on the lifetime used to register the service. You’ll learn about lifetimes in chapter 9.
注意 DI 容器是创建新实例还是重用现有实例取决于用于注册服务的生命周期。您将在第 9 章中了解一生。

As already mentioned, the DI container creates an entire dependency graph. The LinkGenerator implementation registered with the DI container declares the dependencies it requires by having parameters in its constructor, just as the EmailSender type from section 8.1 declared its dependencies. When the DI container creates the LinkGenerator, it first creates all the service’s dependencies and uses them to create the final LinkGenerator instance.

如前所述,DI 容器会创建一个完整的依赖项关系图。向 DI 容器注册的 LinkGenerator 实现通过其构造函数中的参数来声明它所需的依赖项,就像第 8.1 节中的 EmailSender 类型声明其依赖项一样。当 DI 容器创建 LinkGenerator 时,它首先创建服务的所有依赖项,并使用它们创建最终的 LinkGenerator 实例。

Injecting services into your handlers is the canonical DI approach for minimal API endpoint handlers, but sometimes you need to access a service outside the context of a request. You may have lots of reasons to do this, but some of the most common relate to working with a database or logging. You may want to run some code when your app is starting to update a database’s schema before the app starts handling requests, for example. If you need to access services in Program.cs outside the context of a request, you can retrieve services from the DI container directly by using the WebApplication.Services property, which exposes the container as an IServiceProvider.

将服务注入处理程序是最小 API 端点处理程序的规范 DI 方法,但有时您需要在请求上下文之外访问服务。您可能有很多理由这样做,但其中一些最常见的原因与使用数据库或日志记录有关。例如,当应用程序开始更新数据库的架构时,您可能希望在应用程序开始处理请求之前运行一些代码。如果需要在请求上下文之外访问 Program.cs 中的服务,则可以使用 WebApplication 直接从 DI 容器中检索服务。Services 属性,该属性将容器公开为 IServiceProvider。

Note You register services with the IServiceCollection exposed on WebApplicationBuilder.Services. You request services with the IServiceProvider exposed on WebApplication.Services.
注意 向 IServiceCollection 注册服务在 WebApplicationBuilder.Services 上公开。你请求IServiceProvider 的服务WebApplication.Services 的 Web 应用程序。

The IServiceProvider acts as a service locator, so you can request services from it directly by using GetService() and GetRequiredService():

IServiceProvider 充当服务定位器,因此您可以使用 GetService() 和 GetRequiredService() 直接向它请求服务:

  • GetService<T>()—Returns the requested service T if it is available in the DI container; otherwise, returns null
    GetService<T>() — 如果请求的服务 T 在 DI 容器中可用,则返回该服务 T;否则,返回 null

  • GetRequiredService<T>()—Returns the requested service T if it is available in the DI container; otherwise, throws an InvalidOperationException
    GetRequiredService<T>() — 如果请求的服务 T 在 DI 容器中可用,则返回该服务 T;否则,将引发 InvalidOperationException

I generally favor GetRequiredService over GetService, as it immediately tells you whether you have a configuration problem with your DI container by throwing an exception, and you don’t have to handle nulls.

我通常更喜欢 GetRequiredService 而不是 GetService,因为它通过引发异常立即告诉您 DI 容器是否存在配置问题,并且您不必处理 null。

You can use either of these methods in Program.cs to retrieve a service from DI. The following listing shows how to retrieve a LinkGenerator from the DI container, but you can access any service registered in the DI container here.

您可以在 Program.cs 中使用这两种方法中的任何一种从 DI 检索服务。下面的清单显示了如何从 DI 容器中检索 LinkGenerator,但您可以在此处访问在 DI 容器中注册的任何服务。

Listing 8.8 Retrieving a service from the DI container using WebApplication.Services
清单 8.8 从 DI 容器中检索服务使用 WebApplication.Services

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

app.MapGet("/", () => "Hello World!");

LinkGenerator links = ❶
    app.Services.GetRequiredService<LinkGenerator>(); ❶

app.Run(); ❷

❶ Retrieves a service from the DI container using the GetRequiredService<T>()
extension method
使用 GetRequiredService<T>() 扩展方法从 DI 容器中检索服务

❷ You must access services before app.Run(), as this call blocks until your app
exits.
您必须先访问服务,然后才能应用。Run(),因为此调用会一直阻止,直到您的应用退出。

This approach, in which you call the DI container directly to ask for a class, is called the service locator pattern. Generally speaking, you should try to avoid this pattern in your code; include your dependencies as constructor or endpoint handler arguments directly, and let the DI container provide them for you. This pattern is the only way to access DI services in the main loop of your application in Program.cs, however, so don’t worry about using it here. Still, you should absolutely avoid accessing WebApplication.Services from inside your endpoint handlers or other types whenever possible.

在这种方法中,您可以直接调用 DI 容器来请求一个类,这种方法称为服务定位器模式。一般来说,您应该尽量避免在代码中使用这种模式;直接将您的依赖项作为构造函数或终端节点处理程序参数包含在内,并让 DI 容器为您提供它们。但是,此模式是在 Program.cs 中访问应用程序主循环中的 DI 服务的唯一方法,因此不必担心在此处使用它。不过,您绝对应该避免访问 WebApplication 。尽可能从终端节点处理程序或其他类型内部获取服务。

Note You can read about the service locator antipattern in Dependency Injection Principles, Practices, and Patterns, by Steven van Deursen and Mark Seemann (Manning, 2019).
注意 您可以在 Steven van Deursen 和 Mark Seemann 编写的 Dependency Injection Principles, Practices, and Patterns(Manning,2019 年)中阅读有关服务定位器反模式的信息。

In this chapter we covered some of the reasons to use DI in your applications, how to enable optional ASP.NET Core features by adding services to the DI container, and how to access services from the DI container by using injection into your endpoint handlers. In chapter 9 you’ll learn about service lifetimes and how to register your own services with the DI container.

在本章中,我们介绍了在应用程序中使用 DI 的一些原因,如何通过向 DI 容器添加服务来启用可选的 ASP.NET Core 功能,以及如何通过使用注入到端点处理程序中来访问 DI 容器中的服务。在第 9 章中,您将了解服务生命周期以及如何向 DI 容器注册自己的服务。

8.6 Summary

8.6 总结

DI is baked into the ASP.NET Core framework. You need to ensure that your application adds all the framework’s dependencies for optional features in Program.cs; otherwise, you’ll get exceptions at runtime when the DI container can’t find the required services.
DI 已融入 ASP.NET Core 框架中。您需要确保您的应用程序为 Program.cs 中的可选功能添加了框架的所有依赖项;否则,当 DI 容器找不到所需的服务时,您将在运行时收到异常。

The dependency graph is the set of objects that must be created to create a specific requested root object. The DI container creates all these dependencies for you.
依赖关系图是创建特定请求的根对象时必须创建的对象集。DI 容器会为您创建所有这些依赖项。

You should aim to use explicit dependencies instead of implicit dependencies in most cases. ASP.NET Core uses constructor arguments and endpoint handler arguments to declare explicit dependencies.
在大多数情况下,您应该使用显式依赖项而不是隐式依赖项。ASP.NET Core 使用构造函数参数和终端节点处理程序参数来声明显式依赖项。

When discussing DI, the term service is used to describe any class or interface registered with the container.
在讨论 DI 时,术语 service 用于描述向容器注册的任何类或接口。

You register services with the DI container so that the container knows which implementation to use for each requested service. This registration typically takes the form “For interface X, use implementation Y.”
您可以向 DI 容器注册服务,以便容器知道要为每个请求的服务使用哪个实现。此注册通常采用“对于接口 X,使用实现 Y”的形式。

You must register services with the container by calling Addextension methods on the IServiceCollection exposed as WebApplicationBuilder.Services in Program.cs. If you forget to register a service that’s used by the framework or in your own code, you’ll get an InvalidOperationException at runtime.
必须通过在 Program.cs 中作为 WebApplicationBuilder.Services 公开的 IServiceCollection 上调用 Add扩展方法来向容器注册服务。如果您忘记注册框架使用的服务或在您自己的代码中注册的服务,您将在运行时收到 InvalidOperationException。

You can retrieve services from the DI container in your endpoint handlers by adding a parameter of the required type.
您可以通过添加所需类型的参数,从终端节点处理程序中的 DI 容器中检索服务。

You can retrieve services from the DI container in Program.cs via the service locator pattern by calling GetService<T>() or GetRequiredService<T>() on the IServiceProvider exposed as WebApplication.Services. Service location is generally considered to be an antipattern; generally, you shouldn’t use it inside your handler methods, but it’s fine to use it directly inside Program.cs.
您可以通过对公开为 WebApplication.Services 的 IServiceProvider 调用 GetService<T>()GetRequiredService<T>() Program.cs 中的服务定位器模式从 DI 容器中检索服务。服务位置通常被视为反模式;通常,您不应该在 Handler 方法中使用它,但可以直接在 Program.cs 中使用它。

GetService<T>() returns null if the requested service isn’t registered with the DI container. By contrast, GetRequiredService<T>() throws an InvalidOperationException.
GetService<T>() 如果请求的服务未注册到 DI 容器,则返回 null。相比之下,GetRequiredService<T>() 会引发 InvalidOperationException。

ASP.NET Core in Action 7 Model binding and validation in minimal APIs

7 Model binding and validation in minimal APIs
7 最小 API 中的模型绑定和验证

This chapter covers

本章涵盖

  • Using request values to create binding models
    使用请求值创建绑定模型

  • Customizing the model-binding process
    自定义模型绑定过程

  • Validating user input using DataAnnotations attributes
    使用 DataAnnotations 属性验证用户输入

In chapter 6 I showed you how to define a route with parameters—perhaps for the unique ID for a product API. But say a client sends a request to the product API. What then? How do you access the values provided in the request and read the JavaScript Object Notation (JSON) in the request body?
在第 6 章中,我向您展示了如何使用参数定义路由 — 可能是为了产品 API 的唯一 ID。但是,假设客户端向产品 API 发送请求。那又如何呢?如何访问请求中提供的值并读取请求正文中的 JavaScript 对象表示法 (JSON)?

For most of this chapter, in sections 7.1-7.9, we’ll look at model binding and how it simplifies reading data from a request in minimal APIs. You’ll see how to take the data posted in the request body or in the URL and bind it to C# objects, which are then passed to your endpoint handler methods as arguments. When your handler executes, it can use these values to do something useful—return a product’s details or change a product’s name, for example.

在本章的大部分内容中,在 7.1-7.9 节中,我们将介绍模型绑定以及它如何简化在最小 API 中从请求中读取数据的过程。您将了解如何获取请求正文或 URL 中发布的数据并将其绑定到 C# 对象,然后将这些对象作为参数传递给您的端点处理程序方法。当您的处理程序执行时,它可以使用这些值来执行一些有用的作,例如,返回产品的详细信息或更改产品的名称。

When your code is executing in an endpoint handler method, you might be forgiven for thinking that you can happily use the binding model without any further thought. Hold on, though. Where did that data come from? From a user—and you know users can’t be trusted! Section 7.10 focuses on how to make sure that the user-provided values are valid and make sense for your app.

当您的代码在终端节点处理程序方法中执行时,您可能会认为无需任何进一步考虑即可愉快地使用绑定模型,这是可以理解的。不过,请稍等。这些数据从何而来?从用户 - 以及你知道用户不可信!第 7.10 节重点介绍如何确保用户提供的值有效且对您的应用有意义。

Model binding is the process of taking the user’s raw HTTP request and making it available to your code by populating plain old CLR objects (POCOs), providing the input to your endpoint handlers. We start by looking at which values in the request are available for binding and where model binding fits in your running app.

模型绑定是通过填充普通旧 CLR 对象 (POCO) 来获取用户的原始 HTTP 请求并将其提供给您的代码的过程,从而为您的端点处理程序提供输入。我们首先查看请求中的哪些值可用于绑定,以及模型绑定在正在运行的应用程序中适合的位置。

7.1 Extracting values from a request with model binding

7.1 使用模型绑定从请求中提取值

In chapters 5 and 6 you learned that route parameters can be extracted from the request’s path and used to execute minimal API handlers. In this section we look in more detail at the process of extracting route parameters and the concept of model binding.

在第 5 章和第 6 章中,您了解了可以从请求的路径中提取路由参数,并用于执行最小的 API 处理程序。在本节中,我们将更详细地了解提取路由参数的过程和模型绑定的概念。

By now, you should be familiar with how ASP.NET Core handles a request by executing an endpoint handler. You’ve also already seen several handlers, similar to

到目前为止,您应该熟悉 ASP.NET Core 如何通过执行终端节点处理程序来处理请求。您也已经看到了几个处理程序,类似于

app.MapPost("/square/{num}", (int num) => num * num);

Endpoint handlers are normal C# methods, so the ASP.NET Core framework needs to be able to call them in the usual way. When handlers accept parameters as part of their method signature, such as num in the preceding example, the framework needs a way to generate those objects. Where do they come from, exactly, and how are they created?

端点处理程序是普通的 C# 方法,因此 ASP.NET Core 框架需要能够以通常的方式调用它们道路。当处理程序接受参数作为其方法签名的一部分时(例如前面示例中的 num),框架需要一种方法来生成这些对象。它们究竟来自哪里,又是如何产生的?

I’ve already hinted that in most cases, these values come from the request itself. But the HTTP request that the server receives is a series of strings. How does ASP.NET Core turn that into a .NET object? This is where model binding comes in.

我已经暗示过,在大多数情况下,这些值来自请求本身。但是服务器收到的 HTTP 请求是一系列字符串。ASP.NET Core 如何将其转换为 .NET 对象?这就是模型绑定的用武之地。

Definition Model binding extracts values from a request and uses them to create .NET objects. These objects are passed as method parameters to the endpoint handler being executed.
DEFINITION 模型绑定从请求中提取值,并使用它们创建 .NET 对象。这些对象作为方法参数传递给正在执行的端点处理程序。

The model binder is responsible for looking through the request that comes in and finding values to use. Then it creates objects of the appropriate type and assigns these values to your model in a process called binding.

模型绑定负责查看传入的请求并查找要使用的值。然后,它会创建适当类型的对象,并在称为 binding 的过程中将这些值分配给模型。

Note Model binding in minimal APIs (and in Razor Pages and Model-View-Controller [MVC]) is a one-way population of objects from the request, not the two-way data binding that desktop or mobile development sometimes uses.
注意 最小 API(以及 Razor Pages 和 Model-View-Controller [MVC])中的模型绑定是来自请求的对象的单向填充,而不是桌面或移动开发有时使用的双向数据绑定。

ASP.NET Core automatically creates the arguments that are passed to your handler by using the request’s properties, such as the request URL, any headers sent in the HTTP request, any data explicitly POSTed in the request body, and so on.

ASP.NET Core 使用请求的属性(例如请求 URL、HTTP 请求中发送的任何标头、请求正文中显式 POST编辑的任何数据等)自动创建传递给处理程序的参数。

Model binding happens before the filter pipeline and your endpoint handler execute, in the EndpointMiddleware, as shown in figure 7.1. The RoutingMiddleware is responsible for matching an incoming request to an endpoint and for extracting the route parameter values, but all the values at that point are strings. It’s only in the EndpointMiddleware that the string values are converted to the real argument types (such as int) needed to execute the endpoint handler.

模型绑定发生在过滤器管道和终端节点处理程序执行之前,在 EndpointMiddleware 中,如图 7.1 所示。RoutingMiddleware 负责将传入请求与终端节点匹配并提取路由参数值,但该点的所有值都是字符串s。只有在 EndpointMiddleware 中,字符串值才会被转换为执行端点处理程序所需的实际参数类型(比如 int)。

alt text

Figure 7.1 The RoutingMiddleware matches the incoming request to an endpoint and extracts the route parameters as strings. When the EndpointMiddleware executes the endpoint, the minimal API infrastructure uses model binding to create the arguments required to execute the endpoint handler, converting the string route values to real argument types such as int.
图 7.1 RoutingMiddleware 将传入的请求匹配到终端节点,并将路由参数提取为字符串s。当 EndpointMiddleware 执行端点时,最小的 API 基础设施使用模型绑定来创建执行端点处理程序所需的参数,将字符串路由值转换为实际参数类型,例如 int。

For every parameter in your minimal API endpoint handler, ASP.NET core must decide how to create the corresponding arguments. Minimal APIs can use six different binding sources to create the handler arguments:

对于最小 API 终端节点处理程序中的每个参数,ASP.NET 核心必须决定如何创建相应的参数。Minimal API 可以使用六个不同的绑定源来创建 handler 参数:

  • Route values—These values are obtained from URL segments or through default values after matching a route, as you saw in chapter 5.
    路由值 — 这些值是从 URL 分段或通过匹配路由后的默认值获取的,如第 5 章所示。

  • Query string values—These values are passed at the end of the URL, not used during routing.
    查询字符串值 – 这些值在 URL 末尾传递,在路由期间不使用。

  • Header values—Header values are provided in the HTTP request.
    标头值 — HTTP 请求中提供标头值。

  • Body JSON—A single parameter may be bound to the JSON body of a request.
    正文 JSON — 单个参数可以绑定到请求的 JSON 正文。

  • Dependency injected services—Services available through dependency injection can be used as endpoint handler arguments. We look at dependency injection in chapters 8 and 9.
    依赖项注入服务 - 通过依赖项注入提供的服务可用作终端节点处理程序参数。我们将在第 8 章和第 9 章中介绍依赖注入。

  • Custom binding—ASP.NET Core exposes methods for you to customize how a type is bound by providing access to the HttpRequest object.
    自定义绑定 — ASP.NET Core 公开了一些方法,供您通过提供对 HttpRequest 对象的访问来自定义类型的绑定方式。

Warning Unlike MVC controllers and Razor Pages, minimal APIs do not automatically bind to the body of requests sent as forms, using the application/x-www-form-urlencoded mime type. Minimal APIs will bind only to a JSON request body. If you need to work with form data in a minimal API endpoint, you can access it on HttpRequest.Form, but you won’t benefit from automatic binding.
警告 与 MVC 控制器和 Razor Pages 不同,最小的 API 不会使用 application/ x-www-form- urlencoded MIME 类型自动绑定到作为表单发送的请求正文。最小 API 将仅绑定到 JSON 请求正文。如果您需要在最小的 API 端点中处理表单数据,您可以在HttpRequest.Form 的 URL 请求,但你不会从自动绑定中受益。

We’ll look at the exact algorithm ASP.NET Core uses to choose which binding source to use in section 7.8, but we’ll start by looking at how ASP.NET Core binds simple types such as int and double.

我们将了解 ASP.NET Core 用于选择在 7.8 节中使用哪个绑定源的确切算法,但首先我们将了解 ASP.NET Core 如何绑定简单类型,例如 int 和 double。

7.2 Binding simple types to a request

7.2 将简单类型绑定到请求

When you’re building minimal API handlers, you’ll often want to extract a simple value from the request. If you’re loading a list of products in a category, for example, you’ll likely need the category’s ID, and in the calculator example at the start of section 7.1, you’ll need the number to square.

在构建最小 API 处理程序时,您通常需要从请求中提取一个简单的值。例如,如果要加载某个类别中的产品列表,则可能需要类别的 ID,在第 7.1 节开头的计算器示例中,需要将数字平方。

When you create an endpoint handler that contains simple types such as int, string, and double, ASP.NET Core automatically tries to bind the value to a route parameter, or a query string value:

当您创建包含简单类型(如 int、string 和 double)的终端节点处理程序时,ASP.NET Core 会自动尝试将值绑定到路由参数或查询字符串值:

  • If the name of the handler parameter matches the name of a route parameter in the route template, ASP.NET Core binds to the associated route value.
    如果 handler 参数的名称与路由模板中的路由参数名称匹配,则 ASP.NET Core 将绑定到关联的路由值。

  • If the name of the handler parameter doesn’t match any parameters in the route template, ASP.NET Core tries to bind to a query string value.
    如果 handler 参数的名称与路由模板中的任何参数都不匹配,则 ASP.NET Core 会尝试绑定到查询字符串值。

If you make a request to /products/123, for example, this will match the following endpoint:

例如,如果您向 /products/123 发出请求,这将匹配以下端点:

app.MapGet("/products/{id}", (int id) => $"Received {id}");

ASP.NET Core binds the id handler argument to the {id} route parameter, so the handler function is called with id=123. Conversely, if you make a request to /products?id=456, this will match the following endpoint instead:

ASP.NET Core 将 id 处理程序参数绑定到 {id} 路由参数,因此使用 id=123 调用处理程序函数。相反,如果您向 /products?id=456 发出请求,则这将匹配以下端点:

app.MapGet("/products", (int id) => $"Received {id}");

In this case, there’s no id parameter in the route template, so ASP.NET Core binds to the query string instead, and the handler function is called with id=456.

在这种情况下,路由模板中没有 id 参数,因此 ASP.NET Core 会改为绑定到查询字符串,并使用 id=456 调用处理程序函数。

In addition to this “automatic” inference, you can force ASP.NET Core to bind from a specific source by adding attributes to the parameters. [FromRoute] explicitly binds to route parameters, [FromQuery] to the query string, and [FromHeader] to header values, as shown in figure 7.2.

除了这种 “自动” 推理之外,您还可以通过向参数添加属性来强制 ASP.NET Core 从特定源进行绑定。[FromRoute] 显式绑定到路由参数,[FromQuery] 显式绑定到查询字符串,[FromHeader] 显式绑定到标头值,如图 7.2 所示。

alt text

Figure 7.2 Model binding an HTTP get request to an endpoint. The [FromRoute], [FromQuery], and [FromHeader] attributes force the endpoint parameters to bind to specific parts of the request. Only the [FromHeader] attribute is required in this case; the route parameter and query string would be inferred automatically.
图 7.2 将 HTTP get 请求绑定到终端节点的模型。[FromRoute]、[FromQuery] 和 [FromHeader] 属性强制终结点参数绑定到请求的特定部分。在这种情况下,只需要 [FromHeader] 属性;Route 参数和 Query String 将自动推断。

The [From] attributes override ASP.NET Core’s default logic and forces the parameters to load from a specific binding source. Listing 7.1 demonstrates three possible [From] attributes:

[From] 属性会覆盖 ASP.NET Core 的默认逻辑,并强制参数从特定的binding 源。清单 7.1 演示了三种可能[From]属性:

  • [FromQuery]—As you’ve already seen, this attribute forces a parameter to bind to the query string.
    [发件人查询]- 如您所见,此属性强制参数绑定到查询字符串。

  • [FromRoute]—This attribute forces the parameter to bind a route parameter value. Note that if a parameter of the required name doesn’t exist in the route template, you’ll get an exception at runtime.
    [从路线]- 此属性强制参数绑定路径参数值。请注意,如果路由模板中不存在所需名称的参数,您将在运行时收到异常。

  • [FromHeader]—This attribute binds a parameter to a header value in the request.
    [发件人标头]— 此属性将参数绑定到请求中的标头值。

Listing 7.1 Binding simple values using [From] attributes
列表 7.1 使用 [From] 绑定简单值属性

using Microsoft.AspNetCore.Mvc;                                       // ❶

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

app.MapGet("/products/{id}/paged",
    ([FromRoute] int id,                                              // ❷
    [FromQuery] int page,                                             // ❸
    [FromHeader(Name = "PageSize")] int pageSize)                     // ❹
    => $"Received id {id}, page {page}, pageSize {pageSize}");
app.Run();

❶ All the [From] attributes are in this namespace.
所有 [From
] 属性都位于此命名空间中。

❷ [FromRoute] forces the argument to bind to the route value.
[FromRoute] 强制参数绑定到路由值。

❸ [FromQuery] forces the argument to bind to the query string.
[FromQuery] 强制参数绑定到查询字符串。

❹ [FromHeader] binds the argument to the specified header.
[FromHeader] 将参数绑定到指定的标头。

Later, you’ll see other attributes, such as [FromBody] and [FromServices], but the preceding three attributes are the only [From*] attributes that operate on simple types such as int and double. prefer to avoid using [FromQuery] and [FromRoute] wherever possible and rely on the default binding conventions instead, as I find that they clutter the method signatures, and it’s generally obvious whether a simple type is going to bind to the query string or a route value.

稍后,您将看到其他属性,例如 [FromBody] 和 [FromServices],但前面的三个属性是唯一对简单类型(如 int 和 double)进行作的 [From*] 属性。我倾向于尽可能避免使用 [FromQuery] 和 [FromRoute],而是依赖默认的绑定约定,因为我发现它们会使方法签名变得混乱,并且通常很明显地将简单类型绑定到查询字符串还是路由值。

Tip ASP.NET Core binds to route parameters and query string values based on convention, but the only way to bind to a header value is with the [FromHeader] attribute.
提示 ASP.NET Core 根据约定绑定到路由参数和查询字符串值,但绑定到标头值的唯一方法是使用 [FromHeader] 属性。

You may be wondering what would happen if you try to bind a type to an incompatible value. What if you try to bind an int to the string value "two", for example? In that case ASP.NET Core throws a BadHttpRequestException and returns a 400 Bad Request response.
您可能想知道,如果尝试将类型绑定到不兼容的值,会发生什么情况。例如,如果您尝试将 int 绑定到字符串值 “two” 怎么办?在这种情况下 ASP.NET Core 会引发 BadHttpRequestException 并返回 400 Bad Request 响应。

Note When the minimal API infrastructure fails to bind a handler parameter due to an incompatible format, it throws a BadHttpRequestException and returns a 400 Bad Request response.
注意 当最小 API 基础设施由于格式不兼容而无法绑定处理程序参数时,它会引发 BadHttpRequestException 并返回 400 Bad Request 响应。

I’ve mentioned several times in this section that you can bind route values, query string values, and headers to simple types, but what is a simple type? A simple type is defined as any type that contains either of the following TryParse methods, where T is the implementing type:

我在本节中多次提到,您可以将路由值、查询字符串值和标头绑定到简单类型,但什么是 简单类型?简单类型定义为包含以下任一 TryParse 方法的任意类型,其中 T 是实现类型:

public static bool TryParse(string value, out T result);
public static bool TryParse(
    string value, IFormatProvider provider, out T result);

Types such as int and bool contain one (or both) these methods. But it’s also worth noting that you can create your own types that implement one of these methods, and they’ll be treated as simple types, capable of binding from route values, query string values, and headers.

int 和 bool 等类型包含一个(或两个)这些方法。但还值得注意的是,您可以创建自己的类型来实现这些方法之一,它们将被视为简单类型,能够从路由值、查询字符串值和标头进行绑定。

Figure 7.3 shows an example of implementing a simple strongly-typed ID[1] that’s treated as a simple type thanks to the TryParse method it exposes. When you send a request to /product/p123, ASP.NET Core sees that the ProductId type used in the endpoint handler contains a TryParse method and that the name of the id parameter has a matching route parameter name. It creates the id argument by calling ProductId.TryParse() and passes in the route value, p123.

图 7.3 显示了实现简单的强类型 ID1 的示例,由于它公开了 TryParse 方法,该 ID 1 被视为简单类型。当您向 /product/p123 发送请求时,ASP.NET Core 会发现终端节点处理程序中使用的 ProductId 类型包含 TryParse 方法,并且 id 参数的名称具有匹配的路由参数名称。它通过调用 ProductId.TryParse() 创建 id 参数,并传入路由值 p123。

alt text

Figure 7.3 The routing middleware matches the incoming URL to the endpoint. The endpoint middleware attempts to bind the route parameter id to the endpoint parameter. The endpoint parameter type ProductId implements TryParse. If parsing is successful, the parsed parameter is used to call the endpoint handler. If parsing fails, the endpoint middleware returns a 400 Bad Request response.
图 7.3 路由中间件将传入的 URL 与端点匹配。终端节点中间件尝试将路由参数 ID 绑定到 endpoint 参数。终端节点参数类型 ProductId 实现 TryParse。如果解析成功,则 parsed 参数用于调用终端节点处理程序。如果解析失败,终端节点中间件将返回 400 Bad Request 响应。

Listing 7.2 shows how you could implement the TryParse method for ProductId. This method creates a ProductId from strings that consist of an integer prefixed with 'p' (p123 or p456, for example). If the input string matches the required format, it creates a ProductId instance and returns true. If the format is invalid, it returns false, binding fails, and a 400 Bad Request is returned.

清单 7.2 展示了如何实现 ProductId 的 TryParse 方法。此方法从字符串s 创建一个 ProductId,该字符串由一个前缀为 'p' 的整数组成(例如 p123 或 p456)。如果输入字符串与所需的格式匹配,它将创建一个 ProductId 实例并返回 true。如果格式无效,则返回 false,绑定失败,并返回 400 Bad Request。

Listing 7.2 Implementing TryParse in a custom type to allow parsing from route values
清单 7.2 在自定义类型中实现 TryParse 以允许从路由值解析

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

app.MapGet("/product/{id}", (ProductId id) => $"Received {id}");             // ❶
app.Run();

readonly record struct ProductId(int Id)                                     // ❷
{
    public static bool TryParse(string? s, out ProductId result)             // ❸
    {
    if (s is not null                                                        // ❹
            && s.StartsWith('p')                                             // ❹
            && int.TryParse(                                                 // ❺
                s.AsSpan().Slice(1),                                         // ❻
                out int id))                                                 // ❼
        {
        result = new ProductId(id);                                          // ❽
            return true;                                                     // ❽
        }
    result = default;                                                        // ❾
        return false;                                                        // ❾
    }
}

❶ ProductId automatically binds to route values as it implements TryParse.
ProductId 在实现 TryParse 时会自动绑定到路由值。

❷ ProductId is a C# 10 record struct.
ProductId 是 C# 10 记录结构。

❸ It implements TryParse, so it’s treated as a simple type by minimal APIs.
它实现了 TryParse,因此它被最小的 API 视为简单类型。

❹ Checks that the string is not null and that the first character in the string is ‘p’ .
检查字符串是否不为 null,以及字符串中的第一个字符是否为 'p' 。

❺ and if it is, tries to parse the remaining characters as an integer
如果是,则尝试将剩余字符解析为整数

❻ Efficiently skips the first character by treating the string as a ReadOnlySpan
通过将字符串视为 ReadOnlySpan 来有效地跳过第一个字符

❼ If the string was parsed successfully, id contains the parsed value.
如果字符串解析成功,则 id 包含解析的值。

❽ Everything parsed successfully, so creates a new ProductId and returns true

❾ Something went wrong, so returns false and assigns a default value to the
(unused) result
所有内容都解析成功,因此创建一个新的 ProductId 并返回 true

Using modern C# and .NET features
使用现代 C# 和 .NET 功能
Listing 7.2 included some C# and .NET features that you may not have seen before, depending on your background:
清单 7.2 包含了一些您以前可能没有见过的 C# 和 .NET 功能,具体取决于您的背景:
· Pattern matching for null values—s is not null. Pattern matching features have been introduced gradually into C# since C# 7. The is not null pattern, introduced in C# 9, has some minor advantages over the common != null expression. You can read all about pattern matching at http://mng.bz/gBxl.
空值的模式匹配 - s 不为空。自 C# 7 以来,模式匹配功能已逐渐引入 C# 中。C# 9 中引入的 is not null 模式与常见的 != null 表达式相比,具有一些细微的优势。您可以在 http://mng.bz/gBxl 阅读有关模式匹配的所有信息。
· Records and struct records—readonly record struct. Records are syntactical sugar over normal class and struct declarations, which make declaring new types more succinct and provide convenience methods for working with immutable types. Record structs were introduced in C# 10. You can read more at http://mng.bz/5wWz.
记录和结构记录 - 只读记录结构。记录是普通类和结构声明的语法糖,这使得声明新类型更加简洁,并为使用不可变类型提供了便捷的方法。记录结构是在 C# 10 中引入的。您可以在 http://mng.bz/5wWz 上阅读更多内容。
· Span for performance—s.AsSpan(). Span and ReadOnlySpan were introduced in .NET Core 2.1 and are particularly useful for reducing allocations when working with string values. You can read more about them at http://mng.bz/6DNy.
Span 用于性能 - s.AsSpan()。 Span 和 ReadOnlySpan 是在 .NET Core 2.1 中引入的,对于在处理字符串值时减少分配特别有用。您可以在 http://mng.bz/6DNy 上阅读更多关于它们的信息。
· ValueTask—It’s not shown in listing 7.2, but many of the APIs in ASP.NET Core use ValueTask instead of the more common Task for APIs that normally complete asynchronously but may complete asynchronously. You can read about why they were introduced and when to use them at http://mng.bz/o1GM.
ValueTask — 它未显示在清单 7.2 中,但 ASP.NET Core 中的许多 API 都使用 ValueTask,而不是更常见的 Task,用于通常异步完成但可能异步完成的 API。您可以在 http://mng.bz/o1GM 上阅读有关引入它们的原因以及何时使用它们的信息。
Don’t worry if you’re not familiar with these constructs. C# is a fast-moving language, so keeping up can be tricky, but there’s generally no reason you need to use the new features. Nevertheless, it’s useful to be able to recognize them sot hat you can read and understand code that uses them.
如果您不熟悉这些结构,请不要担心。C# 是一种快速发展的语言,因此跟上步伐可能很棘手,但通常没有理由需要使用新功能。尽管如此,能够识别它们还是很有用的,这样您就可以阅读和理解使用它们的代码。
If you’re keen to embrace new features, you might consider implementing the IParsable interface when you implement TryParse. This interface uses the static abstract interfaces feature, which was introduced in C# 11, and requires implementing both a TryParse and Parse method. You can read more about the IParsable interface in the announcement post at http://mng.bz/nW2K.
如果您热衷于采用新功能,则可以考虑在实现 TryParse 时实现 IParsable 接口。此接口使用 C# 11 中引入的静态抽象接口功能,并且需要实现 TryParse 和 Parse 方法。‌‌您可以在 http://mng.bz/nW2K 的公告帖子中阅读有关 IParsable 接口的更多信息。

Now we’ve looked extensively at binding simple types to route values, query strings, and headers. In section 7.3 we’ll learn about binding to the body of a request by deserializing JSON to complex types.

现在,我们已经广泛研究了如何将简单类型绑定到路由值、查询字符串和标头。在第 7.3 节中,我们将了解如何通过将 JSON 反序列化为复杂类型来绑定到请求正文。

7.3 Binding complex types to the JSON body

7.3 将复杂类型绑定到 JSON 正文

Model binding in minimal APIs relies on certain conventions to simplify the code you need to write. One such convention, which you’ve already seen, is about binding to route parameters and query string values. Another important convention is that minimal API endpoints assume that requests will be sent using JSON.

最小 API 中的模型绑定依赖于某些约定来简化您需要编写的代码。其中一个约定您已经看到,它是关于绑定到路由参数和查询字符串值的。另一个重要的约定是,最小 API 端点假定将使用 JSON 发送请求。

Minimal APIs can bind the body of a request to a single complex type in your endpoint handler by deserializing the request from JSON. That means that if you have an endpoint such as the one in the following listing, ASP.NET Core will automatically deserialize the request for you from JSON, creating the Product argument.

最小 API 可以通过从 JSON 反序列化请求,将请求正文绑定到终端节点处理程序中的单个复杂类型。这意味着,如果您有一个终端节点(如下面的清单中的终端节点),ASP.NET Core 将自动从 JSON 反序列化请求,从而创建 Product 参数。

Listing 7.3 Automatically deserializing a JSON request from the body
清单 7.3 从正文中自动反序列化 JSON 请求

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

app.MapPost("/product", (Product product) => $"Received {product}");  // ❶

app.Run();

record Product(int Id, string Name, int Stock);                       // ❷

❶ Product is a complex type, so it’s bound to the JSON body of the request.
Product 是一种复杂类型,因此它绑定到请求的 JSON 正文。

❷ Product doesn’t implement TryParse, so it’s a complex type.
Product 没有实现 TryParse,所以它是一个复杂的类型。

If you send a POST request to /product for the app in listing 7.3, you need to provide valid JSON in the request body, such as

如果向清单 7.3 中的应用程序的 /product 发送 POST 请求,则需要在请求正文中提供有效的 JSON,例如

{ "id": 1, "Name": "Shoes", "Stock": 12 }

ASP.NET Core uses the built-in System.Text.Json library to deserialize the JSON into a Product instance and uses it as the product argument in the handler.

ASP.NET Core 使用内置的 System.Text.Json 库将 JSON 反序列化为 Product 实例,并将其用作处理程序中的 product 参数。

Configuring JSON binding with System.Text.Json
使用 System.Text.Json 配置 JSON 绑定
The System.Text.Json library, introduced in .NET Core 3.0, provides a high-performance, low-allocation JSON serialization library. It was designed to be something of a successor to the ubiquitous Newtonsoft.Json library, but it trades flexibility for performance.
.NET Core 3.0 中引入的 System.Text.Json 库提供高性能、低分配的 JSON 序列化库。它旨在成为无处不在的 Newtonsoft.Json 库的继任者,但它以灵活性换取了性能。
Minimal APIs use System.Text.Json for both JSON deserialization (when binding to a request’s body) and serialization (when writing results, as you saw in chapter 6). Unlike for MVC and Razor Pages, you can’t replace the JSON serialization library used by minimal APIs, so there’s no way to use Newtonsoft.Json instead. But you can customize some of the library’s serialization behavior for your minimal APIs.
最小 API 使用 System.Text.Json 进行 JSON 反序列化(绑定到请求正文时)和序列化(写入结果时,如第 6 章所示)。与 MVC 和 Razor Pages 不同,您无法替换最小 API 使用的 JSON 序列化库,因此无法改用 Newtonsoft.Json。但是,您可以为最小的 API 自定义库的某些序列化行为。
You can set System.Text.Json, for example, to relax some of its strictness to allow trailing commas in the JSON and control how property names are serialized with code like the following example:
例如,您可以设置 System.Text.Json 以放宽其一些严格性,以允许 JSON 中使用尾部逗号,并控制如何使用代码序列化属性名称,如以下示例所示:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureRouteHandlerJsonOptions(o => {
    o.SerializerOptions.AllowTrailingCommas = true;
    o.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
    o.SerializerOptions.PropertyNameCaseInsensitive = true;
});

Typically, the automatic binding for JSON requests is convenient, as most APIs these days are built around JSON requests and responses. The built-in binding uses the most performant approach and eliminates a lot of boilerplate that you’d otherwise need to write yourself. Nevertheless, bear several things in mind when you’re binding to the request body:

通常,JSON 请求的自动绑定很方便,因为现在大多数 API 都是围绕 JSON 请求和响应构建的。内置绑定使用性能最高的方法,并消除了许多您需要自己编写的样板文件。尽管如此,请熊绑定到请求正文时,请记住以下几点:

  • You can bind only a single handler parameter to the JSON body. If more than one complex parameter is eligible to bind to the body, you’ll get an exception at runtime when the app receives its first request.
    您只能将单个处理程序参数绑定到 JSON 正文。如果多个复杂参数符合绑定到正文的条件,则当应用程序收到其第一个请求时,您将在运行时收到异常。

  • If the request body isn’t JSON, the endpoint handler won’t run, and the EndpointMiddleware will return a 415 Unsupported Media Type response.
    如果请求正文不是 JSON,则终端节点处理程序不会运行,并且 EndpointMiddleware 将返回 415 Unsupported Media Type 响应。

  • If you try to bind to the body for an HTTP verb that usually doesn’t send a body (GET, HEAD, OPTIONS, DELETE, TRACE, and CONNECT), you’ll get an exception at runtime. If you change the endpoint in listing 7.3 to MapGet instead of MapPost, for example, you’ll get an exception on your first request, as shown in figure 7.4.
    如果您尝试绑定到通常不发送正文的 HTTP 动词(GET、HEAD、OPTIONS、DELETE、TRACE 和 CONNECT)的正文,则会在运行时收到异常。例如,如果将清单 7.3 中的端点更改为 MapGet 而不是 MapPost,则第一个请求将收到异常,如图 7.4 所示。

  • If you’re sure that you want to bind the body of these requests, you can override the preceding behavior by applying the [FromBody] attribute to the handler parameter. I strongly advise against this approach, though: sending a body with GET requests is unusual, could confuse the consumers of your API, and is discouraged in the HTTP specification (https://www.rfc-editor.org/rfc/rfc9110#name-get).
    如果确定要绑定这些请求的正文,可以通过将 [FromBody] 属性应用于 handler 参数来替代上述行为。不过,我强烈建议不要使用这种方法:发送带有 GET 请求的正文是不寻常的,可能会使 API 的使用者感到困惑,并且在 HTTP 规范 (https://www.rfc- editor.org/rfc/rfc9110#name-get) 中不建议这样做。

  • It’s uncommon to see, but you can also apply [FromBody] to a simple type parameter to force it to bind to the request body instead of to the route/query string. As for complex types, the body is deserialized from JSON into your parameter.
    这种情况并不常见,但你也可以将 [FromBody] 应用于简单类型参数,以强制它绑定到请求正文,而不是路由到路由/查询字符串。对于复杂类型,正文从 JSON 反序列化为参数。

alt text

Figure 7.4 If you try to bind the body to a parameter for a GET request, you’ll get an exception when your app receives its first request.
图 7.4 如果您尝试将 body 绑定到 GET 请求的参数,则当应用程序收到其第一个请求时,您将收到异常。

We’ve discussed binding of both simple types and complex types. Unfortunately, now it’s time to admit to a gray area: arrays, which can be simple types or complex types.

我们已经讨论了简单类型和复杂类型的绑定。不幸的是,现在是时候承认一个灰色地带了:数组,它可以是简单类型或复杂类型。

7.4 Arrays: Simple types or complex types?

7.4 数组:简单类型还是复杂类型?

It’s a little-known fact that entries in the query string of a URL don’t have to be unique. The following URL is valid, for example, even though it includes a duplicate id parameter:

一个鲜为人知的事实是,URL 的查询字符串中的条目不必是唯一的。例如,以下 URL 有效,即使它包含重复的 id 参数:

/products?id=123&id=456

So how do you access these query string values with minimal APIs? If you create an endpoint like

那么,如何使用最少的 API 访问这些查询字符串值呢?如果您创建类似于

app.MapGet("/products", (int id) => $"Received {id}");

a request to /products?id=123 would bind the id parameter to the query string, as you’d expect. But a request that includes two id values in the query string, such as /products?id=123&id=456, will cause a runtime error, as shown in figure 7.5. ASP.NET Core returns a 400 Bad Request response without the handler’s or filter pipeline’s running at all.

如您所料,对 /products?id=123 的请求会将 id 参数绑定到查询字符串。但是,在查询字符串中包含两个 id 值的请求(例如 /products?id=123&id=456)将导致运行时错误,如图 7.5 所示。ASP.NET Core 返回 400 Bad Request 响应,而处理程序或筛选器管道根本没有运行。

alt text

Figure 7.5 Attempting to bind a handler with a signature such as (int id) to a query string that contains ?id=123&id=456 causes an exception at runtime and a 400 Bad Request response.

图 7.5 尝试将具有 (int id) 等签名的处理程序绑定到包含 ?id=123&id=456 的查询字符串会导致运行时出现异常和 400 Bad Request 响应。

If you want to handle query strings like this one, so that users can optionally pass multiple possible values for a parameter, you need to use arrays. The following listing shows an example of an endpoint that accepts multiple id values from the query string and binds them to an array.

如果要处理像这样的查询字符串,以便用户可以选择为参数传递多个可能的值,则需要使用数组。以下清单显示了一个终端节点示例,该终端节点接受来自查询字符串的多个 id 值并将它们绑定到数组。

Listing 7.4 Binding multiple values for a parameter in a query string to an array
清单 7.4 为 中的参数绑定多个值数组的查询字符串

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

app.MapGet("/products/search",
    (int[] id) => $"Received {id.Length} ids");    // ❶

app.Run();

❶ The array will bind to multiple instances of id in the query string.
数组将绑定到查询字符串中 id 的多个实例。

If you’re anything like me, the fact that the int[] handler parameter in listing 7.4 is called id and not ids will really bug you. Unfortunately, you have to use id here so that the parameter binds correctly to a query string like ?id=123&id=456. If you renamed it ids, the query string would need to be ?ids=123&ids=456.

如果你和我一样,清单 7.4 中的 int[] handler 参数叫 id 而不是 ids 这一事实真的会让你感到困扰。遗憾的是,您必须在此处使用 id,以便参数正确绑定到查询字符串,例如 ?id=123&id=456。如果您将其重命名为 ids,则查询字符串需要为 ?ids=123&ids=456。

Luckily, you have another option. You can control the name of the target that a handler parameter binds to by using the [FromQuery] and [FromRoute] attributes, similar to the way you use [FromHeader]. For this example, you can have the best of both words by renaming the handler parameter ids and adding the [FromQuery] attribute:

幸运的是,您还有另一种选择。您可以使用 [FromQuery] 和 [FromRoute] 属性来控制处理程序参数绑定到的目标的名称,类似于使用 [FromHeader] 的方式。对于此示例,您可以通过重命名处理程序参数 ID 并添加 [FromQuery] 属性来获得两全其美的效果:

app.MapGet("/products/search",
([FromQuery(Name = "id")] int[] ids) => $"Received {ids.Length} ids");

Now you can sleep easy. The handler parameter has a better name, but it still binds to the query string ?id=123&id=456 correctly.

现在您可以安心入睡了。handler 参数具有更好的名称,但它仍正确绑定到查询字符串 ?id=123&id=456。

Tip You can bind array parameters to multiple header values in the same way that you do for as query string values, using the [FromHeader] attribute.
提示 可以使用 [FromHeader] 属性,以与查询字符串值相同的方式将数组参数绑定到多个标头值。

The example in listing 7.4 binds an int[], but you can bind an array of any simple type, including custom types with a TryParse method (listing 7.2), as well as string[] and StringValues.

清单 7.4 中的示例绑定了一个 int[],但你可以绑定任何简单类型的数组,包括带有 TryParse 方法的自定义类型(清单 7.2),以及 string[] 和 StringValues。

Note StringValues is a helper type in the Microsoft.Extensions.Primitives namespace that represents zero, one, or many strings in an efficient way.
注意 StringValues 是 Microsoft.Extensions.Primitives 命名空间中的帮助程序类型,它以高效的方式表示零个、一个或多个字符串。

So where is that gray area I mentioned? Well, arrays work as I’ve described only if

那么我提到的灰色地带在哪里呢?好吧,数组只有在

  • You’re using an HTTP verb that typically doesn’t include a request body, such as GET, HEAD, or DELETE.
    您使用的是通常不包含请求正文的 HTTP 动词,例如 GET、HEAD 或 DELETE。

  • The array is an array of simple types (or string[] or StringValues).
    该数组是简单类型(或string[] 或 StringValues)。

If either of these statements is not true, ASP.NET Core will attempt to bind the array to the JSON body of the request instead. For POST requests (or other verbs that typically have a request body), this process works without problems: the JSON body is deserialized to the parameter array. For GET requests (and other verbs without a body), it causes the same unhandled exception you saw in figure 7.4 when a body binding is detected in one of these verbs.

如果这些语句中的任何一个不 为 true,则 ASP.NET Core 将尝试将数组绑定到请求的 JSON 正文。对于 POST 请求(或其他通常具有请求正文的动词),此过程可以正常工作:JSON 正文被反序列化为参数数组。对于 GET 请求(以及其他没有正文的动词),当在其中一个动词中检测到正文绑定时,它会导致您在图 7.4 中看到的相同未处理异常。

Note As before, when binding body parameters, you can work around this situation for GET requests by adding an explicit [FromBody] to the handler parameter, but you shouldn’t!
注意 和以前一样,在绑定 body 参数时,您可以通过向 handler 参数添加显式 [FromBody] 来解决 GET 请求的这种情况,但您不应该这样做!

We’ve covered binding both simple types and complex types, from the URL and the body, and we’ve even looked at some cases in which a mismatch between what you expect and what you receive causes errors. But what if a value you expect isn’t there? In section 7.5 we look at how you can choose what happens.

我们已经介绍了从 URL 和正文绑定简单类型和复杂类型,我们甚至研究了一些情况,在这些情况下,你期望的和你收到的内容不匹配会导致错误。但是,如果您期望的值不存在怎么办?在 7.5 节中,我们将了解如何选择发生的情况。

7.5 Making parameters optional with nullables

7.5 使参数对可为 null 值

We’ve described lots of ways to bind parameters to minimal API endpoints. If you’ve been experimenting with the code samples and sending requests, you may have noticed that if the endpoint can’t bind a parameter at runtime, you get an error and a 400 Bad Request response. If you have an endpoint that binds a parameter to the query string, such as

我们已经介绍了许多将参数绑定到最小 API 端点的方法。如果您一直在试验代码示例并发送请求,您可能已经注意到,如果终端节点在运行时无法绑定参数,则会收到错误和 400 Bad Request 响应。如果您有一个将参数绑定到查询字符串的终端节点,例如

app.MapGet("/products", (int id) => $"Received {id}");

but you send a request without a query string or with the wrong name in the query string, such as a request to /products?p=3, the EndpointMiddleware throws an exception, as shown in figure 7.6. The id parameter is required, so if it can’t bind, you’ll get an error message and a 400 Bad Request response, and the endpoint handler won’t run.

但是您发送的请求没有查询字符串或查询字符串中的名称错误,例如对/products?p=3 中,EndpointMiddleware 会抛出一个exception 的 intent 示例,如图 7.6 所示。id 参数是必需的,因此如果它无法绑定,您将收到一条错误消息和一个400 Bad Request 响应,并且终端节点处理程序不会运行。

alt text

Figure 7.6 If a parameter can’t be bound because a value is missing, the EndpointMiddleware throws an exception and returns a 400 Bad Request response. The endpoint handler doesn’t run.
图 7.6 如果某个参数因为缺少值而无法绑定,则 EndpointMiddleware 会抛出一个exception 并返回 400 Bad Request 响应。终端节点处理程序不运行。

All parameters are required regardless of which binding source they use, whether that’s from a route value, a query string value, a header, or the request body. But what if you want a handler parameter to be optional? If you have an endpoint like this one,

无论它们使用哪个绑定源,无论是来自路由值、查询字符串值、标头还是请求正文,所有参数都是必需的。但是,如果您希望 handler 参数是可选的,该怎么办?如果您有像这样的终端节点,

app.MapGet("/stock/{id?}", (int id) => $"Received {id}");

given that the route parameter is marked optional, requests to both /stock/123 and /stock will invoke the handler. But in the latter case, there’ll be no id route value, and you’ll get an error like the one shown in figure 7.6.

鉴于 route 参数标记为可选,则对 /stock/123 和 /stock 的请求都将调用处理程序。但在后一种情况下,将没有 id 路由值,并且您将收到如图 7.6 所示的错误。

The way around this problem is to mark the handler parameter as optional by making it nullable. Just as ? signifies optional in route templates, it signifies optional in the handler parameters. You can update the handler to use int? instead of int, as shown in the following listing, and the endpoint will handle both /stock/123 and /stock without errors.

解决此问题的方法是通过将 handler 参数设为 null,将其标记为可选。就像 ? 表示 optional 在路由模板中,它在处理程序参数中表示 optional。您可以更新处理程序以使用 int? 而不是 int,如下面的清单所示,并且端点将同时处理 /stock/123 和 /stock,而不会出错。

Listing 7.5 Using optional parameters in endpoint handlers
列表 7.5 在 endpoint 中使用可选参数处理器

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

app.MapGet("/stock/{id?}", (int? id) => $"Received {id}");            // ❶

app.MapGet("/stock2", (int? id) => $"Received {id}");                 // ❷

app.MapPost("/stock", (Product? product) => $"Received {product}");   // ❸

app.Run();

record Product(int Id, string Name, int Stock);

❶ Uses a nullable simple type to indicate that the value is optional, so id is null when calling /stock
使用可为 null 的简单类型来指示值是可选的,因此在调用 /stock 时 id 为 null

❷ This example binds to the query string. Id will be null for the request /stock2.
此示例绑定到查询字符串。请求 /stock2 的 Id 将为 null。

❸ A nullable complex type binds to the body if it’s available; otherwise, it’s null.
如果可用,则可为 null 的复杂类型绑定到正文;否则,它是 null。

If no corresponding route value or query string contains the required value and the handler parameter is optional, the EndpointHandler uses null as the argument when invoking the endpoint handler. Similarly, for complex types that bind to the request body, if the request doesn’t contain anything in the body and the parameter is optional, the handler will have a null argument.

如果没有相应的路由值或查询字符串包含所需的值,并且 handler 参数是可选的,则 EndpointHandler 在调用终端节点处理程序时使用 null 作为参数。同样,对于绑定到请求正文的复杂类型,如果请求不包含body 中的任何内容,并且参数是可选的,则处理程序将具有 null 参数。

Warning If the request body contains the literal JSON value null and the handler parameter is marked optional, the handler argument will also be null. If the parameter isn’t marked optional, you get the same error as though the request didn’t have a body.
警告 如果请求正文包含文本 JSON 值 null,并且 handler 参数标记为可选,则 handler 参数也将为 null。如果参数未标记为 optional,则会收到与请求没有正文相同的错误。

It’s worth noting that you mark complex types binding to the request body as optional by using a nullable reference type (NRT) annotation: ?. NRTs, introduced in C# 8, are an attempt to reduce the scourge of null-reference exceptions in C#, colloquially known as “the billion-dollar mistake.” See http://mng.bz/vneM.

值得注意的是,您可以使用可为 null 的引用类型 (NRT) 注释将绑定到请求正文的复杂类型标记为可选:?。C# 8 中引入的 NRT 旨在减少 C# 中 null 引用异常的祸害,俗称“十亿美元的错误”。请参阅 http://mng.bz/vneM

ASP.NET Core in .NET 7 is built with the assumption that NRTs are enabled for your project (and they’re enabled by default in all the templates), so it’s worth using them wherever you can. If you choose to disable NRTs explicitly, you may find that some of your types are unexpectedly marked optional, which can lead to some hard-to-debug errors.

.NET 7 中的 ASP.NET Core 的构建假设是为您的项目启用了 NRT(并且它们在所有模板中都默认启用),因此值得尽可能使用它们。如果选择显式禁用 NRT,则可能会发现某些类型意外地标记为 optional,这可能会导致一些难以调试的错误。

Tip Keep NRTs enabled for your minimal API endpoints wherever possible. If you can’t use them for your whole project, consider enabling them selectively in Program.cs (or wherever you add your endpoints) by adding #nullable enable to the top of the file.
提示 尽可能为最小 API 终结点启用 NRT。如果您无法在整个项目中使用它们,请考虑通过在文件顶部添加 #nullable enable 来有选择地在 Program.cs 中启用它们(或添加终端节点的任何位置)。

The good news is that ASP.NET Core includes several analyzers built into the compiler to catch configuration problems like the ones described in this section. If you have an optional route parameter but forget to mark the corresponding handler parameter as optional, for example, integrated development environments (IDEs) such as Visual Studio will show a hint, as shown in figure 7.7, and you’ll get a build warning. You can read more about the built-in analyzers at http://mng.bz/4DMV.
好消息是 ASP.NET Core 包含编译器中内置的多个分析器来捕获配置与本节中描述的问题类似。例如,如果你有一个可选的 route 参数,但忘记将相应的 handler 参数标记为可选,则集成开发环境 (IDE)(如 Visual Studio)将显示一个提示,如图 7.7 所示,并且你将收到生成警告。您可以在 http://mng.bz/4DMV 上阅读有关内置分析器的更多信息。

alt text

Figure 7.7 Visual Studio and other IDEs use analyzers to detect potential problems with mismatched optionality.
图 7.7 Visual Studio 和其他 IDE 使用分析器来检测可选性不匹配的潜在问题。

Making your handler parameters optional is one of the approaches you can take, whether they’re bound to route parameters, headers, or the query string. Alternatively, you can provide a default value for the parameter as part of the method signature. You can’t provide default values for parameters in lambda functions in C#, so the following listing shows how to use a local function instead.

将处理程序参数设为可选是您可以采用的方法之一,无论它们是绑定到路由参数、标头还是查询字符串。或者,您可以为参数提供默认值作为方法签名的一部分。在 C# 11,2 中,您无法为 lambda 函数中的参数提供默认值,因此以下清单显示了如何改用本地函数。

Listing 7.6 Using default values for parameters in endpoint handlers
清单 7.6 对 中的参数使用默认值端点处理程序

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

app.MapGet("/stock", StockWithDefaultValue);                     // ❶

app.Run();

string StockWithDefaultValue(int id = 0) => $"Received {id}";    // ❷

❶ The local function StockWithDefaultValue is the endpoint handler.
本地函数 StockWithDefaultValue 是端点处理程序。

❷ The id parameter binds to the query string value if it’s available; otherwise, ithas the value 0.
id 参数绑定到查询字符串值(如果可用);否则,其值为 0。

We’ve thoroughly covered the differences between simple types and complex types and how they bind. In section 7.6 we look at some special types that don’t follow these rules.

我们已经彻底介绍了简单类型和复杂类型之间的区别以及它们如何绑定。在 7.6 节 中,我们看了一些不遵循这些规则的特殊类型。

7.6 Binding services and special types

7.6 绑定服务和特殊类型

In this section you’ll learn how to use some of the special types that you can bind to in your endpoint handlers. By special, I mean types that ASP.NET Core is hardcoded to understand or that aren’t created from the details of the request, by contrast with the binding you’ve seen so far. The section looks at three types of parameters:

在本节中,您将学习如何使用可在终端节点处理程序中绑定到的一些特殊类型。我所说的特殊是指 ASP.NET Core 经过硬编码以理解的类型,或者不是根据请求的详细信息创建的类型,这与你目前看到的绑定形成对比。本节介绍三种类型的参数:

  • Well-known types—that is, hard-coded types that ASP.NET Core knows about, such as HttpContext and HttpRequest
    已知类型 — 即 ASP.NET Core 知道的硬编码类型,例如 HttpContext 和 HttpRequest

  • IFormCollection and IFormFile for working with form data
    IFormFileCollection 和 IFormFile,用于处理文件上传

  • Application services registered in WebApplicationBuilder.Services
    注册的应用程序服务WebApplicationBuilder.Services

We start by looking at the well-known types you can bind to.
我们首先查看可以绑定到的已知类型。

7.6.1 Injecting well-known types

7.6.1 注入已知类型

Throughout this book you’ve seen examples of several well-known types that you can inject into your endpoint handlers, the most notable one being HttpContext. The remaining well-known types provide shortcuts for accessing various properties of the HttpContext object.

在本书中,您已经看到了几种已知类型的示例,您可以将这些类型注入到端点处理程序中,其中最著名的是 HttpContext。其余已知类型提供了用于访问 HttpContext 对象的各种属性的快捷方式。

Note As described in chapter 3, HttpContext acts as a storage box for everything related to a single a request. It contains access to all the low-level details about the request and the response, plus any application services and features you might need.
注意 如第 3 章所述,HttpContext 充当与单个 a 请求相关的所有内容的存储盒。它包含对有关请求和响应的所有低级详细信息的访问,以及您可能需要的任何应用程序服务和功能。

You can use a well-known type in your endpoint handler by including a parameter of the appropriate type. To access the HttpContext in your handler, for example, you could use

您可以通过包含相应类型的参数,在终端节点处理程序中使用已知类型。例如,要在处理程序中访问 HttpContext,您可以使用

app.MapGet("/", (HttpContext context) => "Hello world!");

You can use the following well-known types in your minimal API endpoint handlers:

您可以在最小 API 终端节点处理程序中使用以下已知类型:

  • HttpContext—This type contains all the details on both the request and the response. You can access everything you need from here, but often, an easier way to access the common properties is to use one of the other well-known types.
    HttpContext — 此类型包含有关请求和响应的所有详细信息。您可以从此处访问所需的一切,但通常,访问公共属性的一种更简单的方法是使用其他已知类型之一。
  • HttpRequest—Equivalent to the property HttpContext.Request, this type contains all the details about the request only.
    HttpRequest - 等效于属性 HttpContext.Request,此类型仅包含有关请求的所有详细信息。
  • HttpResponse—Equivalent to the property HttpContext.Response, this type contains all the details about the response only.
    HttpResponse — 等效于属性 HttpContext.Response,此类型仅包含有关响应的所有详细信息。
  • CancellationToken—Equivalent to the property HttpContext.RequestAborted, this token is canceled if the client aborts the request. It’s useful if you need to cancel a long-running task, as described in my post at http://mng.bz/QP2j.
    CancellationToken - 等效于属性 HttpContext.RequestAborted,如果客户端中止请求,则此令牌将被取消。如果您需要取消长时间运行的任务,这很有用,如我在 http://mng.bz/QP2j 上的帖子中所述。
  • ClaimsPrincipal—Equivalent to the property HttpContext.User, this type contains authentication information about the user. You’ll learn more about authentication in chapter 23.
    ClaimsPrincipal — 等效于属性 HttpContext.User,此类型包含有关用户的身份验证信息。您将在第 23 章中了解有关身份验证的更多信息。
  • Stream—Equivalent to the property HttpRequest.Body, this parameter is a reference to the Stream object of the request. This parameter can be useful for scenarios in which you need to process large amounts of data from a request efficiently, without holding it all in memory at the same time.
    Stream — 等效于属性 HttpRequest.Body,此参数是对请求的 Stream 对象的引用。此参数可用于需要高效处理请求中的大量数据,而无需同时将其全部保存在内存中的方案。
  • PipeReader—Equivalent to the property HttpContext.BodyReader, PipeReader provides a higher-level API compared with Stream, but it’s useful in similar scenarios. You can read more about PipeReader and the System.IO.Pipelines namespace at http://mng.bz/XNY6.
    PipeReader — 相当于属性 HttpContext.BodyReader,与 Stream 相比,PipeReader 提供了更高级别的 API,但它在类似情况下很有用。您可以在 http://mng.bz/XNY6 中阅读有关 PipeReader 和 System.IO.Pipelines 命名空间的更多信息。

You can access each of the latter well-known types by navigating via an injected HttpContext object if you prefer. But injecting the exact object you need generally makes for code that’s easier to read.

如果你愿意,你可以通过通过插入的 HttpContext 对象导航来访问后一种已知类型 。但是,注入所需的确切对象通常会使代码更易于阅读。

7.6.2 Injecting services

7.6.2 注入服务

I’ve mentioned several times in this book that you need to configure various core services to work with ASP.NET Core. Many services are registered automatically, but often, you must add more to use extra features, such as when you called AddHttpLogging() in chapter 3 to add request logging to your pipeline.

我在本书中多次提到,您需要配置各种核心服务才能与 ASP.NET Core 配合使用。许多服务是自动注册的,但通常,您必须添加更多服务才能使用额外的功能,例如当您在第 3 章中调用 AddHttpLogging() 以将请求日志记录添加到您的管道时。

Note Adding services to your application involves registering them with a dependency injection (DI) container. You’ll learn all about DI and registering services in chapters 8 and 9.
注意: 向应用程序添加服务涉及向依赖关系注入 (DI) 容器注册服务。您将在第 8 章和第 9 章中了解有关 DI 和注册服务的所有信息。

You can automatically use any registered service in your endpoint handlers, and ASP.NET Core will inject an instance of the service from the DI container. You saw an example in chapter 6 when you used the LinkGenerator service in an endpoint handler. LinkGenerator is one of the core services registered by WebApplicationBuilder, so it’s always available, as shown in the following listing.

您可以在终端节点处理程序中自动使用任何已注册的服务,ASP.NET Core 将从 DI 容器注入服务实例。您在第 6 章中看到了一个示例,当时您在endpoint 处理程序。LinkGenerator 是 WebApplicationBuilder 注册的核心服务之一,因此它始终可用,如下面的清单所示。

Listing 7.7 Using the LinkGenerator service in an endpoint handler
清单 7.7 在端点处理程序

app.MapGet("/links", (LinkGenerator links) =>             // ❶
{
    string link = links.GetPathByName("products");
    return $"View the product at {link}";
});

❶ The LinkGenerator can be used as a parameter because it’s available in the DI container.
LinkGenerator 可以用作参数,因为它在 DI 容器中可用。

Minimal APIs can automatically detect when a service is available in the DI container, but if you want to be explicit, you can also decorate your parameters with the [FromServices] attribute:

最小的 API 可以自动检测 DI 容器中何时有可用的服务,但如果要显式,还可以使用 [FromServices] 属性修饰参数:

app.MapGet("/links", ([FromServices] LinkGenerator links) =>

[FromServices] may be necessary in some rare cases if you’re using a custom DI container that doesn’t support the APIs used by minimal APIs. But generally, I find that I can keep endpoints readable by avoiding the [From*] attributes wherever possible and relying on minimal APIs to do the right thing automatically.

在极少数情况下,如果你使用的自定义 DI 容器不支持最小 API 使用的 API,则可能需要 [FromServices]。但总的来说,我发现我可以通过尽可能避免使用 [From*] 属性并依靠最少的 API 来自动执行正确的作,从而保持端点的可读性。

7.6.3 Binding file uploads with IFormFile and IFormFileCollection

7.6.3 使用 IFormFile 和 IFormFileCollection 绑定文件上传

A common feature of many websites is the ability to upload files. This activity could be relatively infrequent, such as a user’s uploading a profile picture to their Stack Overflow profile, or it may be integral to the application, such as uploading photos to Facebook.

许多网站的一个共同特点是能够上传文件。此活动可能相对不频繁,例如用户将个人资料图片上传到其 Stack Overflow 个人资料,也可能是应用程序不可或缺的一部分,例如将照片上传到 Facebook。

Letting users upload files to your application
允许用户将文件上传到您的应用程序
Uploading files to websites is a common activity, but you should consider carefully whether your application needs that ability. Whenever users can upload files, the situation is fraught with danger.
将文件上传到 Web 站点是一项常见的活动,但您应该仔细考虑您的应用程序是否需要该功能。只要用户可以上传文件,情况就充满了危险。
You should be careful to treat the incoming files as potentially malicious. Don’t trust the filename provided, take care of large files being uploaded, and don’t allow the files to be executed on your server.
应小心将传入文件视为潜在恶意文件。不要相信提供的文件名,注意上传的大文件,并且不允许在您的服务器上执行这些文件。
Files also raise questions about where the data should be stored: in a database, in the filesystem, or in some other storage? None of these questions has a straightforward answer, and you should think hard about the implications of choosing one over the other. Better, don’t let users upload files if you don’t have to!
文件还引发了关于数据应该存储在哪里的问题:在数据库中、在文件系统中,还是在其他存储中?这些问题都没有直接的答案,您应该认真考虑选择一个而不是另一个的影响。更好的是,如果不需要,请不要让用户上传文件!

ASP.NET Core supports uploading files by exposing the IFormFile interface. You can use this interface in your endpoint handlers, and it will be populated with the details of the file upload:

ASP.NET Core 支持通过公开 IFormFile 接口来上传文件。您可以在终端节点处理程序中使用此接口,它将填充文件上传的详细信息:

app.MapGet("/upload", (IFormFile file) => {});

You can also use an IFormFileCollection if you need to accept multiple files:
如果需要接受多个文件,也可以使用 IFormFileCollection:

app.MapGet("/upload", (IFormFileCollection files) =>
{
    foreach (IFormFile file in files)
    {
    }
});

The IFormFile object exposes several properties and utility methods for reading the contents of the uploaded file, some of which are shown here:
IFormFile 对象公开了几个用于读取上载文件内容的属性和实用程序方法,其中一些方法如下所示:

public interface IFormFile
{
    string ContentType { get; }
    long Length { get; }
    string FileName { get; }
    Stream OpenReadStream();
}

As you can see, this interface exposes a FileName property, which returns the filename that the file was uploaded with. But you know not to trust users, right? You should never use the filename directly in your code; users can use it to attack your website and access files that they shouldn’t. Always generate a new name for the file before you save it anywhere.

如您所见,此接口公开了一个 FileName 属性,该属性返回上传文件时使用的文件名。但您知道不要相信用户,对吧?切勿在代码中直接使用文件名;用户可以使用它来攻击您的网站并访问他们不应该访问的文件。在将文件保存到任何位置之前,请始终为文件生成新名称。

Warning There are lots of potential threats to consider when accepting file uploads from users. For more information, see http://mng.bz/yQ9q.
警告 在接受用户上传的文件时,需要考虑许多潜在威胁。有关更多信息,请参阅 http://mng.bz/yQ9q

The IFormFile approach is fine if users are going to be uploading only small files. When your method accepts an IFormFile instance, the whole content of the file is buffered in memory and on disk before you receive it. Then you can use the OpenReadStream method to read the data out.
如果用户只上传小文件,则 IFormFile 方法很好。当您的方法接受IFormFile 实例,则文件的全部内容在您收到之前都会缓冲在内存和磁盘上。然后,您可以使用 OpenReadStream 方法读出数据。

If users post large files to your website, you may start to run out of space in memory or on disk as ASP.NET Core buffers each of the files. In that case, you may need to stream the files directly to avoid saving all the data at the same time. Unfortunately, unlike the model-binding approach, streaming large files can be complex and error-prone, so it’s outside the scope of this book. For details, see Microsoft’s documentation at http://mng.bz/MBgn.

如果用户将大型文件发布到您的网站,您可能会开始耗尽内存或磁盘中的空间,因为 ASP.NET Core 会缓冲每个文件。在这种情况下,您可能需要直接流式传输文件,以避免同时保存所有数据。遗憾的是,与模型绑定方法不同,流式处理大文件可能很复杂且容易出错,因此不在本书的讨论范围之内。有关详细信息,请参阅 Microsoft 的文档 http://mng.bz/MBgn

Tip Don’t use the IFormFile interface to handle large file uploads, as you may see performance problem. Be aware that you can’t rely on users not to upload large files, so avoid file uploads when you can!
提示 不要使用 IFormFile 接口来处理大文件上传,因为您可能会看到性能问题。请注意,您不能指望用户不上传大文件,因此请尽可能避免上传文件!

For the vast majority of minimal API endpoints, the default configuration of model binding for simple and complex types works perfectly well. But you may find some situations in which you need to take a bit more control.
对于绝大多数最小 API 端点,简单类型和复杂类型的模型绑定的默认配置运行良好。但是您可能会发现在某些情况下,您需要采取更多的控制措施。

7.7 Custom binding with BindAsync

7.7 使用 BindAsync 的自定义绑定

The model binding you get out of the box with minimal APIs covers most of the common situations that you’ll run into when building HTTP APIs, but there are always a few edge cases in which you can’t use it.

使用最少的 API 获得的开箱即用的模型绑定涵盖了您将遇到的大多数常见情况。在构建 HTTP API 时,但总有一些边缘情况您无法使用它。

You’ve already seen that you can inject HttpContext into your endpoint handlers, so you have direct access to the request details in your handler, but often, you still want to encapsulate the logic for extracting the data you need. You can get the best of both worlds in minimal APIs by implementing BindAsync in your endpoint handler parameter types and taking advantage of completely custom model binding. To add custom binding for a parameter type, you must implement one of the following two static BindAsync methods in your type T:

您已经看到,您可以将 HttpContext 注入到终端节点处理程序中,因此您可以直接访问处理程序中的请求详细信息,但通常,您仍然希望封装用于提取所需数据的逻辑。通过在终端节点处理程序参数类型中实现 BindAsync 并利用完全自定义的模型绑定,您可以在最少的 API 中实现两全其美的效果。若要为参数类型添加自定义绑定,必须在类型 T 中实现以下两个静态 BindAsync 方法之一:

public static ValueTask<T?> BindAsync(HttpContext context);
public static ValueTask<T?> BindAsync(
HttpContext context, ParameterInfo parameter);

Both methods accept an HttpContext, so you can extract anything you need from the request. But the latter case also provides reflection details about the parameter you’re binding. In most cases the simpler signature should be sufficient, but you never know!

这两种方法都接受 HttpContext,因此您可以从请求中提取所需的任何内容。但后一种情况还提供了有关您正在绑定的参数的反射详细信息。在大多数情况下,更简单的签名应该就足够了,但您永远不知道!

Listing 7.8 shows an example of using BindAsync to bind a record to the request body by using a custom format. The implementation shown in the listing assumes that the body contains two double values, with a line break between them, and if so, it successfully parses the SizeDetails object. If there are any problems along the way, it returns null.

清单 7.8 展示了一个使用 BindAsync 通过自定义格式将记录绑定到请求正文的示例。清单中显示的实现假设主体包含两个 double 值,它们之间有一个换行符,如果是这样,它将成功解析 SizeDetails 对象。如果在此过程中出现任何问题,它将返回 null。

Listing 7.8 Using BindAsync for custom model binding
清单 7.8 使用 BindAsync 进行自定义模型绑定

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

app.MapPost("/sizes", (SizeDetails size) => $"Received {size}");                    // ❶

app.Run();

public record SizeDetails(double height, double width)                              // ❷
{                                                                                   // ❷
    public static async ValueTask<SizeDetails?> BindAsync(                          // ❷
        HttpContext context)                                                        // ❷
    {
    using var sr = new StreamReader(context.Request.Body);                          // ❸

        string? line1 = await sr.ReadLineAsync(context.RequestAborted);             // ❹
        if (line1 is null) { return null; }                                         // ❺

        string? line2 = await sr.ReadLineAsync(context.RequestAborted);             // ❹
        if (line2 is null) { return null; }                                         // ❺

        return double.TryParse(line1, out double height)                            // ❻
            && double.TryParse(line2, out double width)                             // ❻
            ? new SizeDetails(height, width)                                        // ❼
            : null;                                                                 // ❽
    }
}

❶ No extra attributes are needed for the SizeDetails parameter, as it has a BindAsync method.
SizeDetails 参数不需要额外的属性,因为它具有 BindAsync 方法。

❷ SizeDetails implements the static BindAsync method.
SizeDetails 实现静态 BindAsync 方法。

❸ Creates a StreamReader to read the request body
创建一个 StreamReader 来读取请求正文

❹ Reads a line of text from the body
从正文中读取一行文本

❺ If either line is null, indicating no content, stops processing
如果任一行为 null,则表示无内容,则停止处理

❻ Tries to parse the two lines as doubles
尝试将这两行解析为双精度

❼ If the parsing is successful, creates the SizeDetails model and returns it . . .
如果解析成功,则创建 SizeDetails 模型并将其返回 . . .

❽ . . . otherwise, returns null
. . . .否则,返回 null

In listing 7.8 we return null if parsing fails. The endpoint shown will cause the EndpointMiddleware to throw a BadHttpRequestException and return a 400 error, because the size parameter in the endpoint is required (not marked optional). You could have thrown an exception, but it wouldn’t have been caught by the EndpointMiddleware and would have resulted in a 500 response.
在列表 7.8 中,如果解析失败,我们返回 null。显示的端点将导致EndpointMiddleware 抛出一个BadHttpRequestException 并返回 400 错误,因为终端节点中的 size 参数是必需的(未标记为可选)。您可以在 BindAsync 中引发异常,但它不会被 EndpointMiddleware 捕获,并且会导致 500 响应。

7.8 Choosing a binding source

7.8 选择绑定源

Phew! We’ve finally covered all the ways you can bind a request to parameters in minimal APIs. In many cases, things should work as you expect. Simple types such as int and string bind to route values and query string values by default, and complex types bind to the request body. But it can get confusing when you add attributes, BindAsync, and TryParse to the mix!

唷!我们终于介绍了在最小 API 中将请求绑定到参数的所有方法。在许多情况下,事情应该按照您的预期进行。默认情况下,简单类型(如 int 和 string)绑定到路由值和查询字符串值,复杂类型绑定到请求正文。但是,当您将属性、BindAsync 和 TryParse 添加到组合中时,可能会感到困惑!

When the minimal API infrastructure tries to bind a parameter, it checks all the following binding sources in order. The first binding source that matches is the one it uses:

当最小 API 基础设施尝试绑定参数时,它会按顺序检查以下所有绑定源。匹配的第一个绑定源是它使用的绑定源:

  1. If the parameter defines an explicit binding source using attributes such as [FromRoute], [FromQuery], or [FromBody], the parameter binds to that part of the request.
    如果参数使用 [FromRoute]、[FromQuery] 或 [FromBody] 等属性定义显式绑定源,则参数将绑定到请求的该部分。

  2. If the parameter is a well-known type such as HttpContext, HttpRequest, Stream, or IFormFile, the parameter is bound to the corresponding value.
    如果参数是已知类型,如 HttpContext、HttpRequest、Stream 或 IFormFile,则参数将绑定到相应的值。

  3. If the parameter type has a BindAsync() method, use that method for binding.
    如果参数类型具有 BindAsync()方法,请使用该方法进行绑定。

  4. If the parameter is a string or has an appropriate TryParse() method (so is a simple type):
    如果参数是字符串或具有适当的TryParse() 方法(简单类型也是如此):
    a. If the name of the parameter matches a route parameter name, bind to the route value.
    如果参数名称与路由参数名称匹配,则绑定到路由值。
    b. Otherwise, bind to the query string.
    否则,请绑定到查询字符串。

  5. If the parameter is an array of simple types, a string[] or StringValues, the request is a GET or similar HTTP verb that normally doesn’t have a request body, bind to the query string.
    如果参数是简单类型、string[] 或 StringValues 的数组,并且请求是通常没有请求正文的 GET 或类似的 HTTP 动词,请绑定到查询字符串。

  6. If the parameter is a known service type from the dependency injection container, bind by injecting the service from the container.
    如果参数是依赖项注入容器中的已知服务类型,则通过从容器注入服务来绑定。

7.Finally, bind to the body by deserializing from JSON.
最后,通过从 JSON 反序列化来绑定到正文。

The minimal API infrastructure follows this sequence for every parameter in a handler and stops at the first matching binding source.

最小 API 基础结构对处理程序中的每个参数都遵循此序列,并在第一个匹配的绑定源处停止。

Warning If binding fails for the entry, and the parameter isn’t optional, the request fails with a 400 Bad Request response. The minimal API doesn’t try another binding source after one source fails.
警告 如果条目的绑定失败,并且参数不是可选的,则请求将失败,并显示 400 Bad Request响应。最小 API 不会在一个源失败后尝试另一个绑定源。

Remembering this sequence of binding sources is one of the hardest things about minimal APIs to get your head around. If you’re struggling to work out why a request isn’t working as you expect, be sure to come back and check this sequence. I once had a parameter that wasn’t binding to a route parameter, despite its having a TryParse method. When I checked the sequence, I realized that it also had a BindAsync method that was taking precedence!
记住这一系列绑定源是最小 API 最难理解的事情之一。如果你正在努力找出请求没有按预期工作的原因,请务必回来检查此序列。我曾经有一个参数,尽管它有一个 TryParse 方法,但它没有绑定到路由参数。当我检查序列时,我意识到它还具有BindAsync 方法优先!

7.9 Simplifying handlers with AsParameters

7.9 使用 AsParameters 简化处理程序

Before we move on, we’ll take a quick look at a .NET 7 feature for minimal APIs that can simplify some endpoint handlers: the [AsParameters] attribute. Consider the following GET endpoint, which binds to a route value, a header value, and some query values:
在继续之前,我们将快速了解一下 .NET 7 功能,这些功能适用于可简化某些终结点处理程序的最小 API:[AsParameters] 属性。请考虑以下 GET 终端节点,该终端节点绑定到路由值、标头值和一些查询值:

Before we move on, we’ll take a quick look at a .NET 7 feature for minimal APIs that can simplify some endpoint handlers: the [AsParameters] attribute. Consider the following GET endpoint, which binds to a route value, a header value, and some query values:
在继续之前,我们将快速了解一下 .NET 7 功能,这些功能适用于可简化某些终结点处理程序的最小 API:[AsParameters] 属性。请考虑以下 GET 终端节点,该终端节点绑定到路由值、标头值和一些查询值:

app.MapGet("/category/{id}", (int id, int page, [FromHeader(Name = "sort")] bool? sortAsc, [FromQuery(Name = "q")] string search) => { });

I think you’ll agree that the handler parameters for this method are somewhat hard to read. The parameters define the expected shape of the request, which isn’t ideal. The [AsParameters] attribute lets you wrap all these arguments into a single class or struct, simplifying the method signature and making everything more readable.

我想你会同意这个方法的处理程序参数有点难以阅读。参数定义请求的预期形状,这并不理想。[AsParameters] 属性允许您包装所有这些

参数转换为单个类或结构体,从而简化方法签名并使所有内容更具可读性。

Listing 7.9 shows an example of converting this endpoint to use [AsParameters] by replacing it with a record struct. You could also use a class, record, or struct, and you can use properties instead of constructor parameters if you prefer. See the documentation for all the permutations available at http://mng.bz/a1KB.

清单 7.9 展示了一个通过将这个端点替换为 record 结构体来转换它以使用 [AsParameters] 的示例。您还可以使用 class、record 或 struct,如果您愿意,可以使用 properties 而不是 constructor 参数。请参阅 http://mng.bz/a1KB 上提供的所有排列的文档。

Listing 7.9 Using [AsParameters] to simplify endpoint handler parameters
清单 7.9 使用 [AsParameters] 简化端点处理程序参数

using Microsoft.AspNetCore.Mvc;

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

app.MapGet("/category/{id}",
    ([AsParameters] SearchModel model) => $"Received {model}");    // ❶

app.Run();

record struct SearchModel(
    int id,                                                        // ❷
    int page,                                                      // ❷
    [FromHeader(Name = "sort")] bool? sortAsc,                     // ❷
    [FromQuery(Name = "q")] string search);                        // ❷

❶ [AsParameters] indicates that the constructor or properties of the type should be bound, not the type itself.
[AsParameters] 指示应绑定类型的构造函数或属性,而不是类型本身。

❷ Each parameter is bound as though it were written in the endpoint handler
每个参数都被绑定,就像它是在端点处理程序中写入的一样。

The same attributes and rules apply for binding an [AsParameters] type’s constructor parameters and binding endpoint handler parameters, so you can use [From*] attributes, inject services and well-known types, and read from the body. This approach can make your endpoints more readable if you find that they’re getting a bit unwieldy.

相同的属性和规则适用于绑定 [AsParameters] 类型的构造函数参数和绑定端点处理程序参数,因此您可以使用 [From*] 属性、注入服务和已知类型,以及从正文中读取。这种方法可以使您的如果你发现它们变得有点笨拙,它们会更具可读性。

Tip In chapter 16 you’ll learn about model binding in MVC and Razor Pages. You’ll be pleased to know that in those cases, the [AsParameters] approach works out of the box without the need for an extra attribute.
提示 在第 16 章中,您将了解 MVC 和 Razor Pages 中的模型绑定。您会很高兴地知道,在这些情况下,[AsParameters] 方法开箱即用,无需额外的属性。

That brings us to the end of this section on model binding. If all went well, your endpoint handler’s arguments are created, and the handler is ready to execute its logic. It’s time to handle the request, right? Nothing to worry about.

这让我们结束了本节关于模型绑定的内容。如果一切顺利,则 endpoint 处理程序的参数将创建,并且处理程序已准备好执行其 logic。是时候处理这个请求了,对吧?没什么好担心的。

Not so fast! How do you know that the data you received was valid? How do you know that you haven’t been sent malicious data attempting a SQL injection attack or a phone number full of letters? The binder is relatively blindly assigning values sent in a request, which you’re happily going to plug into your own methods. What stops nefarious little Jimmy from sending malicious values to your application? Except for basic safeguards, nothing is stopping him, which is why it’s important that you always validate the input coming in. ASP.NET Core provides a way to do this in a declarative manner out of the box, which is the focus of section 7.10.

没那么快!您如何知道您收到的数据有效?您如何知道您没有收到尝试 SQL 注入攻击的恶意数据或充满字母的电话号码?Binders 相对盲目地分配请求中发送的值,您很乐意将其插入到自己的方法中。如何阻止邪恶的小 Jimmy 向您的应用程序发送恶意值?除了基本的保护措施外,没有什么能阻止他,这就是为什么你总是验证输入很重要的原因。ASP.NET Core 提供了一种开箱即用的声明式方式执行此作的方法,这是第 7.10 节的重点。

7.10 Handling user input with model validation

7.10 使用模型验证处理用户输入

In this section, I discuss the following topics:

在本节中,我将讨论以下主题:

  • What validation is and why you need it
    什么是验证以及为什么需要验证

  • How to use DataAnnotations attributes to describe the data you expect
    如何使用 DataAnnotations 属性描述所需的数据

  • How to validate your endpoint handler parameters
    如何验证终端节点处理程序参数

Validation in general is a big topic, one that you’ll need to consider in every app you build. Minimal APIs don’t include validation by default, instead opting to provide nonprescriptive hooks via the filters you learned about in chapter 5. This design gives you multiple options for adding validation to your app; be sure that you do add some!

一般来说,验证是一个很大的话题,您在构建的每个应用程序中都需要考虑这个话题。默认情况下,Minimal API 不包含验证,而是选择通过您在第 5 章中学到的过滤器提供非规范性钩子。此设计为您提供了多个选项,用于向应用程序添加验证;确保你确实添加了一些!

7.10.1 The need for validation

7.10.1 验证的必要性

Data can come from many sources in your web application. You could load data from files, read it from a database, or accept values that are sent in a request. Although you may be inclined to trust that the data already on your server is valid (though this assumption is sometimes dangerous!), you definitely shouldn’t trust the data sent as part of a request.

数据可以来自 Web 应用程序中的许多来源。您可以从文件中加载数据、从数据库中读取数据或接受请求中发送的值。尽管您可能倾向于相信服务器上已有的数据是有效的(尽管这种假设有时很危险!),但您绝对不应该相信作为请求的一部分发送的数据。

Tip You can read more about the goals of validation, implementation approaches, and potential attacks at http://mng.bz/gBxE.
提示 您可以在 http://mng.bz/gBxE 上阅读有关验证目标、实施方法和潜在攻击的更多信息。

You should validate your endpoint handler parameters before you use them to do anything that touches your domain, anything that touches your infrastructure, or anything that could leak information to an attacker. Note that this warning is intentionally vague, as there’s no defined point in minimal APIs where validation should occur. I advise that you do it as soon as possible in the minimal API filter pipeline.

在使用终端节点处理程序参数执行任何涉及您的域、涉及您的基础设施或可能将信息泄露给攻击者的任何作之前,您应该先验证它们。请注意,此警告故意含糊不清,因为 minimal 中没有定义点应进行验证的 API。我建议您尽快在最小 API 过滤器管道中执行此作。

Always validate data provided by users before you use it in your methods. You have no idea what the browser may have sent you. The classic example of little Bobby Tables (https://xkcd.com/327) highlights the need to always validate data sent by a user.

在方法中使用用户提供的数据之前,请始终对其进行验证。您不知道浏览器可能向您发送了什么。小 Bobby Tables (https://xkcd.com/327) 的经典示例强调了始终验证用户发送的数据的必要性。

Validation isn’t used only to check for security threats, though. It’s also needed to check for nonmalicious errors:

不过,验证不仅仅用于检查安全威胁。还需要检查非恶意错误:

  • Data should be formatted correctly. Email fields have a valid email format, for example.
    数据的格式应正确。例如,电子邮件字段具有有效的电子邮件格式。

  • Numbers may need to be in a particular range. You can’t buy -1 copies of this book!
    数字可能需要在特定范围内。这本书你买不到 -1 本!

  • Some values may be required, but others are optional. Name may be required for a profile, but phone number is optional.
    某些值可能是必需的,但其他值是可选的。配置文件可能需要名称,但电话号码是可选的。

  • Values must conform to your business requirements. You can’t convert a currency to itself; it needs to be converted to a different currency.
    值必须符合您的业务要求。不能将货币转换为自身;它需要转换为其他货币。

As mentioned earlier, the minimal API framework doesn’t include anything specific to help you with these requirements, but you can use filters to implement validation, as you’ll see in section 7.10.3. .NET 7 also includes a set of attributes that you can use to simplify your validation code significantly.

如前所述,最小 API 框架不包含任何帮助您满足这些要求的特定内容,但您可以使用过滤器来实现验证,如第 7.10.3 节所示。.NET 7 还包括一组属性,您可以使用这些属性来显著简化验证代码。

7.10.2 Using DataAnnotations attributes for validation

7.10.2 使用 DataAnnotations 属性进行验证

Validation attributes—more precisely, DataAnnotations attributes—allow you to specify the rules that your parameters should conform to. They provide metadata about a parameter type by describing the sort of data the binding model should contain, as opposed to the data itself.

验证属性(更准确地说,DataAnnotations 属性)允许您指定参数应遵循的规则。它们通过描述绑定模型应包含的数据类型(而不是数据本身)来提供有关参数类型的元数据。

You can apply DataAnnotations attributes directly to your parameter types to indicate the type of data that’s acceptable. This approach allows you to check that required fields have been provided, that numbers are in the correct range, and that email fields are valid email addresses, for example.

您可以将 DataAnnotations 属性直接应用于参数类型,以指示可接受的数据类型。例如,此方法允许您检查是否提供了必填字段、数字是否在正确的范围内,以及电子邮件字段是否为有效的电子邮件地址。

Consider the checkout page for a currency-converter application. You need to collect details about the user—their name, email, and (optionally) phone number—so you create an API to capture these details. The following listing shows the outline of that API, which takes a UserModel parameter. The UserModel type is decorated with validation attributes that represent the validation rules for the model.

请考虑货币转换器应用程序的结帐页。您需要收集有关用户的详细信息 — 他们的姓名、电子邮件和(可选)电话号码 — 因此您创建一个 API 来捕获这些详细信息。下面的清单显示了该 API 的轮廓,它采用 UserModel 参数。UserModel 类型使用表示模型的验证规则的验证属性进行修饰。

Listing 7.10 Adding DataAnnotations to a type to provide metadata
清单 7.10 将 DataAnnotations 添加到类型中以提供元数据

using System.ComponentModel.DataAnnotations;                        // ❶

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

app.MapPost("/users", (UserModel user) => user.ToString());         // ❷

app.Run();

public record UserModel
{
    [Required]                                                      // ❸
    [StringLength(100)]                                             // ❹
    [Display(Name = "Your name")]                                   // ❺
    public string FirstName { get; set; }

    [Required]
    [StringLength(100)]
    [Display(Name = "Last name")]
    public string LastName { get; set; }

    [Required]
    [EmailAddress]                                                  // ❻
    public string Email { get; set; }

    [Phone]                                                         // ❼
    [Display(Name = "Phone number")]
    public string PhoneNumber { get; set; }
}

❶ Adds this using statement to use the validation attributes
添加此 using 语句以使用验证属性

❷ The API takes a UserModel parameter and binds it to the request body.
API 接受 UserModel 参数并将其绑定到请求正文。

❸ Values marked Required must be provided.
必须提供标记为 Required 的值。

❹ The StringLengthAttribute sets the maximum length for the property.
StringLengthAttribute 设置属性的最大长度。

❺ Customizes the name used to describe the property
自定义用于描述属性的名称

❻ Validates that the value of Email may be a valid email address
验证 Email 的值是否为有效的电子邮件地址

❼ Validates that the value of PhoneNumber has a valid telephone number format
验证 PhoneNumber 的值是否具有有效的电话号码格式

Suddenly, your parameter type, which was sparse on details, contains a wealth of information. You’ve specified that the FirstName property should always be provided; that it should have a maximum length of 100 characters; and that when it’s referred to (in error messages, for example), it should be called "Your name" instead of "FirstName".

突然之间,您的参数类型(细节稀疏)包含了大量信息。您已指定应始终提供 FirstName 属性;最大长度应为 100 个字符;当它被引用时(例如,在错误消息中),它应该被称为 “Your name” 而不是 “FirstName”。

The great thing about these attributes is that they clearly declare the expected state of an instance of the type. By looking at these attributes, you know what the properties will contain, or at least should contain. Then you can then write code after model binding to confirm that the bound parameter is valid, as you’ll see in section 7.10.3.

这些属性的伟大之处在于,它们清楚地声明了该类型实例的预期状态。通过查看这些属性,您知道属性将包含什么,或者至少应该包含什么。然后,您可以在模型绑定后编写代码以确认 bound 参数有效,如 7.10.3 节所示。

You’ve got a plethora of attributes to choose among when you apply DataAnnotations to your types. I’ve listed some of the common ones here, but you can find more in the System.ComponentModel.DataAnnotations namespace. For a more complete list, I recommend using IntelliSense in your IDE or checking the documentation at http://mng.bz/e1Mv.

当您将 DataAnnotations 应用于您的类型时,您有大量的属性可供选择。我在这里列出了一些常见的方法,但您可以在 System.ComponentModel.DataAnnotations 命名空间中找到更多方法。有关更完整的列表,我建议在 IDE 中使用 IntelliSense 或查看 http://mng.bz/e1Mv 中的文档。

  • [CreditCard]—Validates that a property has a valid credit card format
    [信用卡]- 验证属性是否具有有效的信用卡格式

  • [EmailAddress]—Validates that a property has a valid email address format
    [电子邮件地址]- 验证属性是否具有有效的电子邮件地址格式

  • [StringLength(max)]—Validates that a string has at most max number of characters
    [字符串长度(最大)]- 验证字符串是否最多具有最大字符数

  • [MinLength(min)]—Validates that a collection has at least the min number of items
    [最小长度(min)]- 验证集合是否至少具有最小项目数

  • [Phone]—Validates that a property has a valid phone number format
    [电话]- 验证属性是否具有有效的电话号码格式

  • [Range(min, max)]—Validates that a property has a value between min and max
    [范围(最小值、最大值)]- 验证属性的值是否介于 min 和 max 之间

  • [RegularExpression(regex)]—Validates that a property conforms to the regex regular expression pattern
    [正则表达式(正则表达式)]- 验证属性是否符合 regex 正则表达式模式

  • [Url]—Validates that a property has a valid URL format
    [网址]- 验证属性是否具有有效的 URL 格式

  • [Required]—Indicates that the property must not be null
    [必填]- 指示属性不能为 null

  • [Compare]—Allows you to confirm that two properties have the same value (such as Email and ConfirmEmail)
    [比较]- 允许您确认两个属性具有相同的值(例如 Email 和 ConfirmEmail)

Warning The [EmailAddress] and [Phone] attributes validate only that the format of the value is potentially correct. They don’t validate that the email address or phone number exists. For an example of how to do more rigorous phone number validation, see this post on the Twilio blog: http://mng.bz/xmZe.
警告 [EmailAddress] 和 [Phone] 属性仅验证值的格式是否可能正确。它们不会验证电子邮件地址或电话号码是否存在。有关如何执行更严格的电话号码验证的示例,请参阅 Twilio 博客上的这篇文章:http://mng.bz/xmZe

The DataAnnotations attributes aren’t new; they’ve been part of the .NET Framework since version 3.5, and their use in ASP.NET Core is almost the same as in the previous version of ASP.NET. They’re also used for purposes other than validation. Entity Framework Core (among others) uses DataAnnotations to define the types of columns and rules to use when creating database tables from C# classes. You can read more about Entity Framework Core in chapter 12 and in Entity Framework Core in Action, 2nd ed., by Jon P. Smith (Manning, 2021).

DataAnnotations 属性并不新鲜;它们自 3.5 版以来一直是 .NET Framework 的一部分,它们在 ASP.NET Core 中的使用与在以前版本的 ASP.NET 中的使用几乎相同。它们还用于验证以外的目的。Entity Framework Core(以及其他)使用 DataAnnotations 来定义从 C# 类创建数据库表时要使用的列和规则的类型。您可以在第 12 章和 Jon P. Smith 的 Entity Framework Core in Action, 2nd ed.(Manning,2021 年)中阅读有关 Entity Framework Core 的更多信息。

If the DataAnnotation attributes provided out of the box don’t cover everything you need, it’s possible to write custom attributes by deriving from the base ValidationAttribute. You’ll see how to create a custom validation attribute in chapter 32.

如果现成提供的 DataAnnotation 属性不能涵盖您需要的所有内容,则可以通过从基 ValidationAttribute 派生来编写自定义属性。您将在第 32 章中了解如何创建自定义验证属性。

One common limitation with DataAnnotation attributes is that it’s hard to validate properties that depend on the values of other properties. Maybe the UserModel type from listing 7.10 requires you to provide either an email address or a phone number but not both, which is hard to achieve with attributes. In this type of situation, you can implement IValidatableObject in your models instead of, or in addition to, using attributes. In listing 7.11, a validation rule is added to UserModel whether the email or phone number is provided. If it isn’t, Validate() returns a ValidationResult describing the problem.

DataAnnotation 属性的一个常见限制是很难验证依赖于其他属性的值的属性。也许清单 7.10 中的 UserModel 类型要求您提供电子邮件地址或电话号码,但不能同时提供两者,这很难通过属性实现。在这种情况下,您可以在模型中实现 IValidatableObject ,而不是使用属性,或者同时使用属性。在列表 7.11 中,无论提供了电子邮件还是电话号码,都会向 UserModel 添加验证规则。如果不是,则 Validate() 返回描述问题的 ValidationResult。

Listing 7.11 Implementing IValidatableObject
清单 7.11 实现 IValidatableObject

using System.ComponentModel.DataAnnotations;
public record CreateUserModel : IValidatableObject               // ❶
{
    [EmailAddress]                                               // ❷
    public string Email { get; set; }

    [Phone]                                                      // ❷
    public string PhoneNumber { get; set; }

    public IEnumerable<ValidationResult> Validate(               // ❸
        ValidationContext validationContext)                     // ❸
    {
        if(string.IsNullOrEmpty(Email)                           // ❹
        && string.IsNullOrEmpty(PhoneNumber))                    // ❹
        {
            yield return new ValidationResult(                   // ❺
            "You must provide an Email or a PhoneNumber",        // ❺
            New[] { nameof(Email), nameof(PhoneNumber) });       // ❺
        }
    }
}

❶ Implements the IValidatableObject interface
实现 IValidatableObject 接口

❷ The DataAnnotation attributes continue to validate basic format requirements.
DataAnnotation 属性继续验证基本格式要求。

❸ Validate is the only function to implement in IValidatableObject.
Validate 是在 IValidatableObject 中实现的唯一函数。

❹ Checks whether the object is valid . . .
检查对象是否有效 . . .

❺ . . . and if not, returns a result describing the error
. . . .如果不是,则返回描述错误的结果

IValidatableObject helps cover some of the cases that attributes alone can’t handle, but it’s not always the best option. The Validate function doesn’t give easy access to your app’s services, and the function executes only if all the DataAnnotation attribute conditions are met.

IValidatableObject 有助于涵盖某些仅靠属性无法处理的情况,但它并不总是最佳选择。Validate 函数无法轻松访问应用程序的服务,并且仅当满足所有 DataAnnotation 属性条件时,该函数才会执行。

Tip DataAnnotations are good for input validation of properties in isolation but not so good for validating complex business rules. You’ll most likely need to perform this validation outside the DataAnnotations framework.
提示 DataAnnotations 适用于隔离属性的输入验证,但不太适合验证复杂的业务规则。您很可能需要在 DataAnnotations 框架之外执行此验证。

Alternatively, if you’re not a fan of the DataAnnotation attribute-based-plus-IValidatableObject approach, you could use the popular FluentValidation library (https://github.com/JeremySkinner/FluentValidation) in your minimal APIs instead. Minimal APIs are completely flexible, so you can use whichever approach you prefer.

或者,如果您不喜欢 DataAnnotation 基于属性的加 IValidatableObject 方法,则可以在最小 API 中使用流行的 FluentValidation 库 (https://github.com/JeremySkinner/FluentValidation)。最小 API 是完全灵活的,因此您可以使用自己喜欢的任何方法。

DataAnnotations attributes provide the basic metadata for validation, but no part of listing 7.10 or listing 7.11 uses the validation attributes you added. You still need to add code to read the parameter type’s metadata, check whether the data is valid, and return an error response if it’s invalid. ASP.NET Core doesn’t include a dedicated validation API for that task in minimal APIs, but you can easily add it with a small NuGet package.

DataAnnotations 属性提供了用于验证的基本元数据,但清单 7.10 或清单 7.11 的任何部分都没有使用您添加的验证属性。您仍然需要添加code 读取参数类型的元数据,检查数据是否有效,如果无效,则返回错误响应。ASP.NET Core 在最少的 API 中不包含用于该任务的专用验证 API,但你可以使用小型 NuGet 包轻松添加它。

7.10.3 Adding a validation filter to your minimal APIs

7.10.3 将验证筛选器添加到最小 API

Microsoft decided not to include any dedicated validation APIs in minimal APIs. By contrast, validation is a built-in core feature of Razor Pages and MVC. Microsoft’s reasoning was that the company wanted to provide flexibility and choice for users to add validation in the way that works best for them, but didn’t want to affect performance for those who didn’t want to use their implementation.

Microsoft 决定不在最小 API 中包含任何专用的验证 API。相比之下,验证是 Razor Pages 和 MVC 的内置核心功能。Microsoft 的理由是,该公司希望为用户提供灵活性和选择,以便以最适合他们的方式添加验证,但又不想影响那些不想使用其实现的人的性能。

Consequently, validation in minimal APIs typically relies on the filter pipeline. As a classic cross-cutting concern, validation is a good fit for a filter. The only downside is that typically, you need to write your own filter rather than use an existing API. The positive side is that validation gives you complete flexibility, including the ability to use an alternative validation library (such as FluentValidation) if you prefer.

因此,最小 API 中的验证通常依赖于筛选器管道。作为一个典型的横切关注点,验证非常适合 filter。唯一的缺点是,通常您需要编写自己的过滤器,而不是使用现有的 API。积极的一面是,验证为您提供了完全的灵活性,包括如果您愿意,可以使用替代验证库(例如 FluentValidation)。

Luckily, Damian Edwards, a project manager architect on the ASP.NET Core team at Microsoft, has a NuGet package called MinimalApis.Extensions that provides the filter for you. Using a simple validation system that hooks into the DataAnnotations on your models, this NuGet package provides an extension method called WithParameterValidation() that you can add to your endpoints. To add the package, search for MinimalApis.Extensions from the NuGet Package Manager in your IDE (be sure to include prerelease versions), or run the following, using the .NET command-line interface:

幸运的是,Microsoft ASP.NET Core 团队的项目经理架构师 Damian Edwards 有一个名为 MinimalApis.Extensions 的 NuGet 包,可以为您提供筛选器。使用挂接到模型上的 DataAnnotations 的简单验证系统,此 NuGet 包提供了一个名为WithParameterValidation() 中,您可以将其添加到终端节点中。若要添加包,请从 IDE 中的 NuGet 包管理器中搜索 MinimalApis.Extensions(请务必包含预发行版本),或使用 .NET 命令行界面运行以下命令:

dotnet add package MinimalApis.Extensions

After you’ve added the package, you can add validation to any of your endpoints by adding a filter using WithParameterValidation(), as shown in listing 7.12. After the UserModel is bound to the JSON body of the request, the validation filter executes as part of the filter pipeline. If the user parameter is valid, execution passes to the endpoint handler. If the parameter is invalid, a 400 Bad Request Problem Details response is returned containing a description of the errors, as shown in figure 7.8.

添加包后,您可以通过使用 WithParameterValidation() 添加过滤器来向任何端点添加验证,如清单 7.12 所示。将 UserModel 绑定到请求的 JSON 正文后,验证筛选条件将作为筛选条件管道的一部分执行。如果 user 参数有效,则执行将传递给终端节点处理程序。如果该参数无效,则返回 400 Bad Request Problem Details 响应,其中包含错误描述,如图 7.8 所示。

alt text

Figure 7.8 If the data sent in the request body is not valid, the validation filter automatically returns a 400 Bad Request response, containing the validation errors, and the endpoint handler doesn’t execute.
图 7.8 如果请求正文中发送的数据无效,则验证筛选条件会自动返回 400 Bad Request 响应,其中包含验证错误,并且终端节点处理程序不会执行。

Listing 7.12 Adding validation to minimal APIs using MinimalApis. Extensions
清单 7.12 使用 MinimalApis.Extensions 向最小 API 添加验证

using System.ComponentModel.DataAnnotations;

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

app.MapPost("/users", (UserModel user) => user.ToString())
    .WithParameterValidation();                               // ❶
app.Run();

public record UserModel                                       // ❷
{
    [Required]
    [StringLength(100)]
    [Display(Name = "Your name")]
    public string Name { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }
}

❶ Adds the validation filter to the endpoint
将验证过滤器添加到端点

❷ The UserModel defines its validation requirements using DataAnnotations
attributes.
UserModel 使用 DataAnnotations 属性定义其验证要求。

Listing 7.12 shows how you can validate a complex type, but in some cases, you may want to validate simple types. You may want to validate that the id value in the following handler should be between 1 and 100:

清单 7.12 展示了如何验证复杂类型,但在某些情况下,你可能想要验证简单类型。您可能需要验证以下处理程序中的 id 值是否应介于 1 和 100 之间:

app.MapGet("/user/{id}", (int id) => $"Received {id}")
    .WithParameterValidation();

Unfortunately, that’s not easy to do with DataAnnotations attributes. The validation filter will check the int type, see that it’s not a type that has any DataAnnotations on its properties, and won’t validate it.

遗憾的是,使用 DataAnnotations 属性并不容易做到这一点。验证筛选器将检查 int 类型,查看它不是在其属性上具有任何 DataAnnotations 的类型,并且不会对其进行验证。

Warning Adding attributes to the handler, as in ([Range(1, 100)] int id), doesn’t work. The attributes here are added to the parameter, not to properties of the int type, so the validator won’t find them.
警告 向处理程序添加属性(如 ([Range(1, 100)] int id) )不起作用。这里的 attributes 是添加到 parameter 的,而不是 int 类型的 properties 中,所以 validator 不会找到它们。

There are several ways around this problem, but the simplest is to use the [AsParameters] attribute you saw in section 7.9 and apply annotations to the model. The following listing shows how.

有几种方法可以解决此问题,但最简单的方法是使用您在 小节 中看到的 [AsParameters] 属性7.9 并将注释应用于模型。下面的清单显示了如何作。

Listing 7.13 Adding validation to minimal APIs using MinimalApis.Extensions
清单 7.13 使用MinimalApis.扩展

using System.ComponentModel.DataAnnotations;

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

app.MapPost("/user/{id}",
    ([AsParameters] GetUserModel model) => $"Received {model.Id}")       // ❶
    .WithParameterValidation();                                          // ❷

app.Run();

struct GetUserModel
{
    [Range(1, 10)]                                                       // ❸
    Public int Id { get; set; }                                          // ❸
}

❶ Uses [AsParameters] to create a type than can be validated
使用 [AsParameters] 创建可验证的类型

❷ Adds the validation filter to the endpoint
将验证筛选器添加到端点

❸ Adds validation attributes to your simple types
将验证属性添加到简单类型

That concludes this look at model binding in minimal APIs. You saw how the ASP.NET Core framework uses model binding to simplify the process of extracting values from a request and turning them into normal .NET objects you can work with quickly. The many ways to bind may be making your head spin, but normally, you can stick to the basics and fall back to the more complex types as and when you need them.

对最小 API 中的模型绑定的介绍到此结束。您了解了 ASP.NET Core 框架如何使用模型绑定来简化从请求中提取值并将其转换为可快速使用的普通 .NET 对象的过程。许多绑定方式可能会让您头晕目眩,但通常情况下,您可以坚持使用基本方法,并在需要时回退到更复杂的类型。

Although the discussion is short, the most important aspect of this chapter is its focus on validation—a common concern for all web applications. Whether you choose to use DataAnnotations or a different validation approach, you must make sure to validate any data you receive in all your endpoints.

虽然讨论很简短,但本章最重要的方面是它对验证的关注——一个共同的关注点适用于所有 Web 应用程序。无论您选择使用 DataAnnotations 还是其他验证方法,都必须确保验证您在所有终端节点中收到的任何数据。

In chapter 8 we leave minimal APIs behind to look at dependency injection in ASP.NET Core and see how it helps create loosely coupled applications. You’ll learn how to register the ASP.NET Core framework services with a container, add your own services, and manage service lifetimes.

在第 8 章中,我们抛弃了最少的 API,看看 ASP.NET Core 中的依赖注入,看看它如何帮助创建松散耦合的应用程序。您将学习如何向容器注册 ASP.NET Core 框架服务、添加您自己的服务以及管理服务生命周期。

7.11 Summary

7.11 总结

Model binding is the process of creating the arguments for endpoint handlers from the details of an HTTP request. Model binding takes care of extracting and parsing the strings in the request so that you don’t have to.
模型绑定是根据 HTTP 请求的详细信息为终端节点处理程序创建参数的过程。模型绑定负责提取和分析请求中的字符串,因此您不必这样做。

Simple values such as int, string, and double can bind to route values, query string values, and headers. These values are common and easy to extract from the request without any manual parsing.
简单值(如 int、string 和 double)可以绑定到路由值、查询字符串值和标头。这些值很常见,并且很容易从请求中提取,而无需任何手动解析。

If a simple value fails to bind because the value in the request is incompatible with the handler parameter, a BadHttpRequestException is thrown, and a 400 Bad Request response is returned.
如果由于请求中的值与 handler 参数不兼容而导致 simple 值绑定失败,则会引发 BadHttpRequestException,并返回 400 Bad Request 响应。

You can turn a custom type into a simple type by adding a TryParse method with the signature bool TryParse(string value, out T result). If you return false from this method, minimal APIs will return a 400 Bad Request response.
您可以通过添加具有签名 bool TryParse(string value, out T result) 的 TryParse 方法,将自定义类型转换为简单类型。如果从此方法返回 false,则最少的 API 将返回 400 Bad Request 响应。

Complex types bind to the request body by default by deserializing from JSON. Minimal APIs can bind only to JSON bodies; you can’t use model binding to access form values.
默认情况下,复杂类型通过从 JSON 反序列化来绑定到请求正文。最小 API 只能绑定到 JSON 正文;您不能使用模型绑定来访问表单值。

By default, you can’t bind the body of GET requests, which goes against the expectations for GET requests. Doing so will cause an exception at runtime.
默认情况下,您无法绑定 GET 请求的正文,因为这与 GET 请求的预期背道而驰。这样做会导致运行时出现异常。

Arrays of simple types bind by default to query string values for GET requests and to the request body for POST requests. This difference can cause confusion, so always consider whether an array is the best option.
默认情况下,简单类型的数组绑定到 GET 请求的查询字符串值和 POST 请求的请求正文。这种差异可能会引起混淆,因此请始终考虑数组是否是最佳选择。

All the parameters of a handler must bind correctly. If a parameter tries to bind to a missing value, you’ll get a BadHttpRequestException and a 400 Bad Request response.
处理程序的所有参数都必须正确绑定。如果参数尝试绑定到缺失值,您将收到 BadHttpRequestException 和 400 Bad Request 响应。

You can use well-known types such as HttpContext and any services from the dependency injection container in your endpoint handlers. Minimal APIs check whether each complex type in your handler is registered as a service in the DI container; if not, they treat it as a complex type to bind to the request body instead.
您可以在终端节点处理程序中使用已知类型(如 HttpContext)和依赖项注入容器中的任何服务。最小 API 检查处理程序中的每个复杂类型是否在 DI 容器中注册为服务;否则,它们会将其视为复杂类型,以绑定到请求正文。

You can read files sent in the request by using the IFormFile and IFormFileCollection interfaces in your endpoint handlers. Take care accepting file uploads with these interfaces, as they can open your application to attacks from users.
您可以使用终端节点处理程序中的 IFormFile 和 IFormFileCollection 接口读取请求中发送的文件。请小心接受使用这些接口上传的文件,因为它们可能会使您的应用程序受到用户的攻击。

You can completely customize how a type binds by using custom binding. Create a static function with the signature ·public static ValueTask<T?> BindAsync(HttpContext context)·, and return the bound property. This approach can be useful for handling complex scenarios, such as arbitrary JSON uploads.
您可以使用自定义绑定完全自定义类型的绑定方式。创建签名为 ·public static ValueTask<T?> BindAsync(HttpContext context)· 的静态函数,并返回绑定属性。此方法可用于处理复杂场景,例如任意 JSON 上传。

You can override the default binding source for a parameter by applying [From] attributes to your handler parameters, such as [FromHeader], [FromQuery], [FromBody], and [FromServices]. These parameters take precedence over convention-based assumptions.
您可以通过将 [From
] 属性应用于处理程序参数(如 [FromHeader]、[FromQuery]、[FromBody] 和 [FromServices])来覆盖参数的默认绑定源。这些参数优先于基于约定的假设。

You can encapsulate an endpoint handler’s parameters by creating a type containing all the parameters as properties or a constructor argument and decorate the parameter with the [AsParameters] attribute. This approach can help you simplify your endpoint’s method signature.
可以通过创建包含所有参数作为属性或构造函数参数的类型来封装终结点处理程序的参数,并使用 [AsParameters] 属性修饰参数。此方法可以帮助您简化终端节点的方法签名。

Validation is necessary to check for security threats. Check that data is formatted correctly, confirm that it conforms to expected values and verify that it meets your business rules.
验证对于检查安全威胁是必要的。检查数据的格式是否正确,确认它符合预期值,并验证它是否符合您的业务规则。

Minimal APIs don’t have built-in validation APIs, so you typically apply validation via a minimal API filter. This approach provides flexibility ,as you can implement validation in the way that suits you best, though it typically means that you need to use a third-party package.
最小 API 没有内置的验证 API,因此您通常通过最小 API 过滤器应用验证。此方法提供了灵活性,因为您可以以最适合您的方式实施验证,但这通常意味着您需要使用第三方包。

The MinimalApis.Extensions NuGet package provides a validation filter that uses DataAnnotations attributes to declaratively define the expected values. You can add the filter with the extension method WithParameterValidation().
MinimalApis.Extensions NuGet 包提供了一个验证筛选器,该筛选器使用 DataAnnotations 属性以声明方式定义预期值。您可以使用扩展方法 WithParameterValidation() 添加过滤器。

To add custom validation of simple types with MinimalApis.Extensions, you must create a containing type and use the [AsParameters] attribute.
若要使用 MinimalApis.Extensions 添加简单类型的自定义验证,必须创建包含类型并使用 [AsParameters] 属性。


  1. I have a series discussing strongly-typed IDs and their benefits on my blog at http://mng.bz/a1Kz.
    我在 http://mng.bz/a1Kz 的博客上有一个系列讨论强类型 ID 及其好处。

  2. C# 12, which will be released with .NET 8, should include support for default values in lambda expressions. For more details, see http://mng.bz/AoRg.
    C# 12 将与 .NET 8 一起发布,它应该包括对 lambda 表达式中默认值的支持。有关更多详细信息,请参阅 http://mng.bz/AoRg

ASP.NET Core in Action 6 Mapping URLs to endpoints using routing

6 Mapping URLs to endpoints using routing
6 使用路由将 URL 映射到端点

This chapter covers

本章涵盖

  • Mapping URLs to endpoint handlers
    将 URL 映射到端点处理程序
  • Using constraints and default values to match URLs
    使用约束和默认值来匹配 URL
  • Generating URLs from route parameters
    从路由参数生成 URL

In chapter 5 you learned how to define minimal APIs, how to return responses, and how to work with filters and route groups. One crucial aspect of minimal APIs that we touched on only lightly is how ASP.NET Core selects a specific endpoint from all the handlers defined, based on the incoming request URL. This process, called routing, is the focus of this chapter.

在第 5 章中,您学习了如何定义最小 API、如何返回响应以及如何使用筛选条件和路由组。我们仅略微涉及的最小 API 的一个关键方面是 ASP.NET Core 如何根据传入请求 URL 从定义的所有处理程序中选择特定端点。此过程称为 routing,是本章的重点。

This chapter begins by identifying the need for routing and why it’s useful. You’ll learn about the endpoint routing system introduced in ASP.NET Core 3.0 and why it was introduced, and explore the flexibility routing can bring to the URLs you expose.

本章首先确定 routing 的需求以及它为什么有用。您将了解 ASP.NET Core 3.0 中引入的端点路由系统及其引入原因,并探索路由可以为您公开的 URL 带来的灵活性。

The bulk of this chapter focuses on the route template syntax and how it can be used with minimal APIs. You’ll learn about features such as optional parameters, default parameters, and constraints, as well as how to extract values from the URL automatically. Although we’re focusing on minimal APIs in this chapter, the same routing system is used with Razor Pages and Model-View-Controller (MVC), as you’ll see in chapter 14.

本章的大部分内容重点介绍路由模板语法以及如何将其与最少的 API 一起使用。您将了解可选参数、默认参数和约束等功能,以及如何自动从 URL 中提取值。尽管我们在本章中重点介绍了最少的 API,但 Razor Pages 和模型视图控制器 (MVC) 使用相同的路由系统,如第 14 章所示。

In section 6.4 I describe how to use the routing system to generate URLs, which you can use to create links and redirect requests for your application. One benefit of using a routing system is that it decouples your handlers from the underlying URLs they’re associated with. You can use URL generation to avoid littering your code with hardcoded URLs like /product/view/3. Instead, you can generate the URLs at runtime, based on the routing system. This approach makes changing the URL for a given endpoint easier: instead of your having to hunt down every place where you used the endpoint’s URL, the URLs are updated for you automatically, with no other changes required.

在 6.4 节中,我将介绍如何使用路由系统生成 URL,您可以使用这些 URL 为您的应用程序创建链接和重定向请求。使用路由系统的一个好处是,它将处理程序与它们关联的底层 URL 分离。您可以使用 URL 生成来避免将代码与硬编码的 URL (如 /product/view/3)混在一起。相反,您可以在运行时根据路由系统生成 URL。这种方法使更改给定终端节点的 URL 变得更加容易:您不必寻找使用终端节点 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 being the glue that ties the middleware pipeline to endpoints. With middleware, endpoints, and routing under your belt, you’ll be writing web apps in no time!

在本章结束时,您应该对 ASP.NET Core 应用程序的工作原理有了更清晰的了解。您可以将路由视为将中间件管道与终端节点联系起来的粘合剂。有了中间件、终端节点和路由,您将立即编写 Web 应用程序!

6.1 What is routing?

6.1 什么是路由?

Routing is the process of mapping an incoming request to a method that will handle it. You can use routing to control the URLs you expose in your application. You can also use routing to enable powerful features such as mapping multiple URLs to the same handler and automatically extracting data from a request’s URL.

路由是将传入请求映射到将处理该请求的方法的过程。您可以使用路由来控制在应用程序中公开的 URL。您还可以使用路由来启用强大的功能,例如映射多个 URL 指向同一处理程序,并自动从请求的 URL 中提取数据。

In chapter 4 you saw that an ASP.NET Core application contains a middleware pipeline, which defines the behavior of your application. Middleware is well suited to handling both cross-cutting concerns, such as logging and error handling, and narrowly focused requests, such as requests for images and CSS files.

在第 4 章中,您看到 ASP.NET Core 应用程序包含一个中间件管道,它定义了应用程序的行为。中间件非常适合处理横切关注点(如日志记录和错误处理)和范围狭窄的请求(如图像和 CSS 文件请求)。

To handle more complex application logic, you’ll typically use the EndpointMiddleware at the end of your middleware pipeline. This middleware can handle an appropriate request by invoking a method known as a handler and using the result to generate a response. Previous chapters described using minimal API endpoint handlers, but there are other types of handlers, such as MVC Action methods and Razor Pages, as you’ll learn in part 2 of this book.

为了处理更复杂的应用程序逻辑,您通常会在中间件管道的末尾使用 EndpointMiddleware。此中间件可以通过调用称为处理程序的方法并使用结果生成响应来处理适当的请求。前面的章节介绍了使用最少的 API 端点处理程序,但还有其他类型的处理程序,例如 MVC作方法和 Razor Pages,您将在本书的第 2 部分中学习。

One aspect that I’ve glossed over so far is how the EndpointMiddleware selects which handler executes when you receive a request. What makes a request appropriate for a given handler? The process of mapping a request to a handler is routing.

到目前为止,我所讨论的一个方面是 EndpointMiddleware 如何选择在您收到请求时执行哪个处理程序。什么使请求适合给定的处理程序?将请求映射到处理程序的过程是路由。

Definition Routing in ASP.NET Core is the process of selecting a specific handler for an incoming HTTP request. In minimal APIs, the handler is the endpoint handler associated with a route. In Razor Pages, the handler is a page handler method defined in a Razor Page. In MVC, the handler is an action method in a controller.
定义 ASP.NET Core 中的路由是为传入的 HTTP 请求选择特定处理程序的过程。在最小 API 中,处理程序是与路由关联的端点处理程序。在 Razor Pages 中,处理程序是在 Razor Page 中定义的页面处理程序方法。在 MVC 中,处理程序是控制器中的作方法。

In chapters 3 to 5, you saw several simple applications built with minimal APIs. In chapter 5, you learned the basics of routing for minimal APIs, but it’s worth exploring why routing is useful as well as how to use it. Even a simple URL path such as /person uses routing to determine which handler should be executed, as shown in figure 6.1.

在第 3 章到第 5 章中,您看到了几个使用最少 API 构建的简单应用程序。在第 5 章中,您学习了最小 API 的路由基础知识,但值得探索为什么路由很有用以及如何使用它。即使是简单的 URL 路径,如 /person,也使用路由来确定应该执行哪个处理程序,如图 6.1 所示。

Figure 6.1 The router compares the request URL with a list of configured route templates to determine which handler to execute.
图 6.1 路由器将请求 URL 与已配置的路由模板列表进行比较,以确定要执行哪个处理程序。

On the face of it, that seems pretty simple. You may wonder why I need a whole chapter to explain that obvious mapping. The simplicity of the mapping in this case belies how powerful routing can be. If this approach, using a direct comparison with static strings, were the only one available, you’d be severely limited in the applications you could feasibly build.

从表面上看,这似乎很简单。你可能会想为什么我需要一整章来解释这个明显的映射。在这种情况下,映射的简单性掩盖了路由的强大功能。如果这种方法(使用与静态字符串的直接比较)是唯一可用的方法,那么您可以构建的应用程序将受到严重限制。

Consider an e-commerce application that sells multiple products. Each product needs to have its own URL, so if you were using a purely static routing system, you’d have only two options:

考虑一个销售多种产品的电子商务应用程序。每个产品都需要有自己的 URL,因此如果您使用的是纯静态路由系统,则只有两个选项:

  • Use a different handler for every product in your product range. That approach would be unfeasible for almost any realistically sized product range.
    为您的产品系列中的每个产品使用不同的处理程序。这种方法对于几乎任何实际规模的产品系列都是不可行的。

  • Use a single handler, and use the query string to differentiate among products. This approach is much more practical, but you’d end up with somewhat-ugly URLs, such as "/product?name=big-widget" or "/product?id=12".
    使用单个处理程序,并使用查询字符串来区分产品。 这种方法要实用得多,但您最终会得到一些难看的 URL,例如“/product?name=big-widget“ 或 ”/product?id=12“ 的 API 版本。

Definition The query string is part of a URL containing additional data that doesn’t fit in the path. It isn’t used by the routing infrastructure to identify which handler to execute, but ASP.NET Core can extract values from the query string automatically in a process called model binding, as you’ll see in chapter 7. The query string in the preceding example is id=12.
定义 查询字符串是 URL 的一部分,其中包含不适合路径的其他数据。路由基础设施不使用它来标识要执行的处理程序,但 ASP.NET Core 可以在称为模型绑定的过程中自动从查询字符串中提取值,如第 7 章所示。前面示例中的查询字符串为 id=12。

With routing, you can have a single endpoint handler that can handle multiple URLs without having to resort to ugly query strings. From the point of the view of the endpoint handler, the query string and routing approaches are similar; the handler returns the results for the correct product dynamically as appropriate. The difference is that with routing, you can completely customize the URLs, as shown in figure 6.2. This feature gives you much more flexibility and can be important in real-life applications for search engine optimization (SEO).

通过路由,您可以拥有一个可以处理多个 URL 的终端节点处理程序,而不必求助于难懂的查询字符串。从端点的视角handler,则查询字符串和路由方法类似;处理程序会根据需要动态返回正确产品的结果。区别在于,使用路由,您可以完全自定义 URL,如图 6.2 所示。此功能为您提供了更大的灵活性,并且在搜索引擎优化 (SEO) 的实际应用程序中可能很重要。

Note With the flexibility of routing, you can encode the hierarchy of your site properly in your URLs, as described in Google’s SEO starter guide at http://mng.bz/EQ2J.
注意 借助路由的灵活性,您可以在 URL 中正确编码网站的层次结构,如 http://mng.bz/EQ2J 的 Google SEO 入门指南中所述。

Figure 6.2 If you use static URL-based mapping, you need a different handler for every product in your product range. With a query string, you can use a single handler, and the query string contains the data. With routing, multiple URLs map to a single handler, and a dynamic parameter captures the difference in the URL.
图 6.2 如果使用基于静态 URL 的映射,则产品范围中的每个产品都需要不同的处理程序。使用查询字符串,您可以使用单个处理程序,并且查询字符串包含数据。使用路由时,多个 URL 映射到单个处理程序,并且动态参数捕获 URL 中的差异。

As well as enabling dynamic URLs, routing fundamentally decouples the URLs in your application from the definition of your handlers.

除了启用动态 URL 之外,路由还从根本上将应用程序中的 URL 与处理程序的定义分离。

File-system based routing
基于文件系统的路由
In one alternative to routing, the location of a handler on disk dictates the URL you use to invoke it. The downside of this approach is that if you want to change an exposed URL, you also need to change the location of the handler on disk.
在路由的另一种替代方法中,处理程序在磁盘上的位置决定了用于调用它的 URL。这种方法的缺点是,如果要更改公开的 URL,还需要更改处理程序在磁盘上的位置。
This file-based approach may sound like a strange choice, but it has many advantages for some apps, primarily in terms of simplicity. As you’ll see in part 2, Razor Pages is partially file-based but also uses routing to get the best of both worlds!
这种基于文件的方法听起来可能很奇怪,但对于某些应用程序来说,它有很多优点,主要是在简单性方面。正如您将在第 2 部分中看到的那样,Razor Pages 部分基于文件,但也使用路由来获得两全其美的效果!

With routing it’s easy to modify your exposed URLs without changing any filenames or locations. You can also use routing to create friendlier URLs for users, which can improve discovery and “hackability.” All of the following routes could point to the same handler:

通过路由,可以轻松修改公开的 URL,而无需更改任何文件名或位置。您还可以使用路由为用户创建更友好的 URL,这可以提高发现和“可黑客攻击性”。以下所有路由都可以指向同一个处理程序:

  • /rates/view/1
  • /rates/view/USD
  • /rates/current-exchange-rate/USD
  • /current-exchange-rate-for-USD

This level of customization isn’t often necessary, but it’s quite useful to have the capability to customize your app’s URLs when you need it. In the next section we’ll look at how routing works in practice in ASP.NET Core.

这种级别的自定义通常不是必需的,但能够在需要时自定义应用程序的 URL 非常有用。在下一节中,我们将了解 ASP.NET Core 中的路由实际工作原理。

6.2 Endpoint routing in ASP.NET Core

6.2 ASP.NET Core 中的终端节点路由

In this section I describe how endpoint routing works in ASP.NET Core, specifically with respect to minimal APIs and the middleware pipeline. In chapter 14 you’ll learn how routing is used with Razor Pages and the ASP.NET Core MVC framework.

在本节中,我将介绍终端节点路由在 ASP.NET Core 中的工作原理,特别是关于最小 API 和中间件管道。在第 14 章中,您将了解如何将路由与 Razor Pages 和 ASP.NET Core MVC 框架一起使用。

Routing has been part of ASP.NET Core since its inception, but it has been through some big changes. In ASP.NET Core 2.0 and 2.1, routing was restricted to Razor Pages and the ASP.NET Core MVC framework. There was no dedicated routing middleware in the middleware pipeline; routing happened only within Razor Pages or MVC components.

路由自成立以来一直是 ASP.NET Core 的一部分,但它经历了一些重大变化。在 ASP.NET Core 中2.0 和 2.1 中,路由仅限于 Razor Pages 和 ASP.NET Core MVC 框架。中间件管道中没有专用的路由中间件;路由仅在 Razor Pages 或 MVC 组件中发生。

Unfortunately, restricting routing to the MVC and Razor Pages infrastructure made some things a bit messy. Some cross-cutting concerns, such as authorization, were restricted to the MVC infrastructure and were hard to use from other middleware in your application. That restriction caused inevitable duplication, which wasn’t ideal.

遗憾的是,将路由到 MVC 和 Razor Pages 基础结构会使某些事情变得有点混乱。一些横切关注点(如授权)仅限于 MVC 基础设施,并且很难从应用程序中的其他中间件中使用。这种限制导致了不可避免的重复,这并不理想。

ASP.NET Core 3.0 introduced a new routing system: endpoint routing. Endpoint routing makes the routing system a more fundamental feature of ASP.NET Core and no longer ties it to the MVC infrastructure. Now Razor Pages, MVC, and other middleware can all use the same routing system. .NET 7 continues to use the same endpoint routing system, which is integral to the minimal API functionality that was introduced in .NET 6.

ASP.NET Core 3.0 引入了一个新的路由系统:端点路由。端点路由使路由系统成为 ASP.NET Core 的更基本功能,不再将其绑定到 MVC 基础设施。现在,Razor Pages、MVC 和其他中间件都可以使用相同的路由系统。.NET 7 继续使用相同的终结点路由系统,这是 .NET 6 中引入的最小 API 功能不可或缺的一部分。

Endpoint routing is fundamental to all but the simplest ASP.NET Core apps. It’s implemented with two pieces of middleware, which you’ve already seen:

端点路由是除最简单的 ASP.NET Core 应用程序之外的所有应用程序的基础。它是通过两个中间件实现的,您已经看到了:

  • EndpointRoutingMiddleware—This middleware chooses which registered endpoints execute for a given request at runtime. To make it easier to distinguish between the two types of middleware, I’ll be referring to this middleware as the RoutingMiddleware throughout this book.
    EndpointRoutingMiddleware — 此中间件选择在运行时为给定请求执行哪些已注册的终端节点。为了更容易区分这两种类型的中间件,我在本书中将此中间件称为 RoutingMiddleware。

  • EndpointMiddleware—This middleware is typically placed at the end of your middleware pipeline. The middleware executes the endpoint selected by the RoutingMiddleware at runtime.
    EndpointMiddleware — 此中间件通常位于中间件管道的末尾。中间件在运行时执行 RoutingMiddleware 选择的端点。

You register the endpoints in your application by calling Map* functions on an IEndpointRouteBuilder instance. In .NET 7 apps, this instance typically is a WebApplication instance but doesn’t have to be, as you’ll see in chapter 30.

您可以通过在 IEndpointRouteBuilder 实例上调用 Map* 函数在应用程序中注册终端节点。在 .NET 7 应用程序中,此实例通常是 Web- Application 实例,但并非必须如此,如第 30 章所示。

Definition An endpoint in ASP.NET Core is a handler that returns a response. Each endpoint is associated with a URL pattern. Depending on the type of application you’re building, minimal API handlers, Razor Page handlers, or MVC controller action methods typically make up the bulk of the endpoints in an application. You can also use simple middleware as an endpoint or a health-check endpoint, for example.
定义 ASP.NET Core 中的终端节点是返回响应的处理程序。每个端点都与一个 URL 模式相关联。根据要生成的应用程序类型,最少的 API 处理程序、Razor Page 处理程序或 MVC 控制器作方法通常构成应用程序中的大部分终结点。例如,您还可以使用简单的中间件作为终端节点,或者您可以使用运行状况检查终端节点。

WebApplication implements IEndpointRouteBuilder, so you can register endpoints on it directly. Listing 6.1 shows how you’d register several endpoints:

WebApplication 实现 IEndpointRouteBuilder,因此您可以直接在其上注册端点。清单 6.1 展示了如何注册多个端点:

  • A minimal API handler using MapGet(), as you’ve seen in previous chapters.
    使用 MapGet() 的最小 API 处理程序,如前几章所示。

  • A health-check endpoint using MapHealthChecks(). You can read more about health checks at http://mng.bz/N2YD.
    使用 MapHealthChecks() 的运行状况检查终端节点。您可以在 http://mng.bz/N2YD 上阅读有关运行状况检查的更多信息。

  • All Razor Pages endpoints in the application using MapRazorPages(). You’ll learn more about routing with Razor Pages in chapter 14.
    使用 MapRazorPages() 的应用程序中的所有 Razor Pages 终结点。您将在第 14 章中了解有关使用 Razor Pages 进行路由的更多信息。

Listing 6.1 Registering multiple endpoints with WebApplication
清单 6.1 使用 注册多个端点Web应用程序

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();          //  ❶
builder.Services.AddRazorPages();            //  ❶

WebApplication app = builder.Build();

app.MapGet("/test", () => "Hello world!");   //  ❷
app.MapHealthChecks("/healthz");             //  ❸
app.MapRazorPages();                         //  ❹

app.Run();

❶ Adds the services required by the health-check middleware and Razor Pages
添加运行状况检查中间件和 Razor Pages 所需的服务

❷ Registers a minimal API endpoint that returns “Hello World!” at the route /test
注册一个最小的 API 端点,该端点在路由 /test 中返回 “Hello World!”

❸ Registers a health-check endpoint at the route /healthz
在路由 /healthz 中注册一个健康检查端点

❹ Registers all the Razor Pages in your application as endpoints
将应用程序中的所有 Razor Pages 注册为端点

Each endpoint is associated with a route template that defines which URLs the endpoint should match. You can see two route templates, "/healthz" and "/test", in listing 6.1.

每个终端节点都与一个路由模板相关联,该模板定义终端节点应匹配的 URL。在清单 6.1 中可以看到两个路由模板,“/healthz” 和 “/test”。

DEFINITION A route template is a URL pattern that is used to match against request URLs, which are strings of fixed values, such as "/test" in the previous listing. They can also contain placeholders for variables, as you’ll see in section 6.3.
定义: 路由模板是一种 URL 模式,用于匹配请求 URL,请求 URL 是固定值的字符串,如上一个列表中的 “/test”。 它们还可以包含变量的占位符,如 6.3 节所示。

The WebApplication stores the registered routes and endpoints in a dictionary that’s shared by the RoutingMiddleware and the EndpointMiddleware.

WebApplication 将注册的路由和终端节点存储在 RoutingMiddleware 和 EndpointMiddleware 共享的字典中。

TIP By default, WebApplication automatically adds the RoutingMiddleware to the start of the middleware and EndpointMiddleware to the end of the middleware pipeline, though you can override the location in the pipeline by calling UseRouting() or UseEndpoints(). See section 4.2.3 for more details about automatically added middleware.
提示: 默认情况下,WebApplication 会自动将 RoutingMiddleware 添加到中间件的开头,将 EndpointMiddleware 添加到中间件管道的末尾,但您可以通过调用 UseRouting() 或 UseEndpoints() 来覆盖管道中的位置。有关自动添加的中间件的更多详细信息,请参见 Section 4.2.3。

At runtime, the RoutingMiddleware compares an incoming request with the routes registered in the dictionary. If the RoutingMiddleware finds a matching endpoint, it makes a note of which endpoint was selected and attaches that to the request’s HttpContext object. Then it calls the next middleware in the pipeline. When the request reaches the EndpointMiddleware, the middleware checks to see which endpoint was selected and executes the endpoint (and any associated endpoint filters), as shown in figure 6.3.

在运行时,RoutingMiddleware 将传入请求与字典中注册的路由进行比较。如果 RoutingMiddleware 找到匹配的端点,它会记下选择了哪个端点,并将其附加到请求的 HttpContext 对象。然后,它会调用管道中的下一个中间件。当请求到达 EndpointMiddleware 时,中间件会检查选择哪个端点并执行该端点(以及任何关联的端点过滤器),如图 6.3 所示。

Figure 6.3 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 won’t generate a response.
图 6.3 终端节点路由使用两步过程。RoutingMiddleware 选择要执行的端点,EndpointMiddleware 执行它。如果请求 URL 与路由模板不匹配,则终端节点中间件不会生成响应。

If the request URL doesn’t match a route template, the RoutingMiddleware doesn’t select an endpoint, but the request still continues down the middleware pipeline. As no endpoint is selected, the EndpointMiddleware silently ignores the request and passes it to the next middleware in the pipeline. The EndpointMiddleware is typically the final middleware in the pipeline, so the “next” middleware is normally the dummy middleware that always returns a 404 Not Found response, as you saw in chapter 4.

如果请求 URL 与路由模板不匹配,则 RoutingMiddleware 不会选择终端节点,但请求仍会继续沿中间件管道向下移动。由于未选择任何终端节点,因此 EndpointMiddleware 会静默忽略该请求并将其传递给管道中的下一个中间件。EndpointMiddleware 通常是管道中的最后一个中间件,因此 “next” 中间件通常是始终返回 404 Not Found 响应的虚拟中间件,如第 4 章所示。

TIP If the request URL doesn’t match a route template, no endpoint is selected or executed. The whole middleware pipeline is still executed, but typically a 404 response is returned when the request reaches the dummy 404 middleware.
提示: 如果请求 URL 与路由模板不匹配,则不会选择或执行任何终端节点。整个中间件管道仍然会执行,但当请求到达虚拟 404 中间件时,通常会返回 404 响应。

The advantage of having two separate pieces of middleware to handle this process may not be obvious at first blush. Figure 6.3 hinted at the main benefit: all middleware placed after the RoutingMiddleware can see which endpoint is going to be executed before it is.

使用两个单独的中间件来处理此过程的优势乍一看可能并不明显。图 6.3 暗示了主要的好处:所有放在 RoutingMiddleware 后面的中间件都可以在执行之前看到哪个端点将被执行。

NOTE Only middleware placed after the RoutingMiddleware can detect which endpoint is going to be executed.
注意: 只有放在 RoutingMiddleware 之后的 middleware 才能检测到将要执行的端点。

Figure 6.4 shows a more realistic middleware pipeline in which middleware is placed both before the RoutingMiddleware and between the RoutingMiddleware and the EndpointMiddleware.

图 6.4 显示了一个更真实的中间件管道,其中中间件放置在 RoutingMiddleware 之前以及 RoutingMiddleware 和 EndpointMiddleware 之间。

Figure 6.4 Middleware placed before the routing middleware doesn’t know which endpoint the routing middleware will select. Middleware placed between the routing middleware and the endpoint middleware can see the selected endpoint.
图 6.4 放置在路由中间件前面的中间件不知道路由中间件将选择哪个端点。放置在路由中间件和终端节点中间件之间的中间件可以看到所选终端节点。

The StaticFileMiddleware in figure 6.4 is placed before the RoutingMiddleware, so it executes before an endpoint is selected. Conversely, the AuthorizationMiddleware is placed after the RoutingMiddleware, so it can tell which minimal API endpoint will be executed eventually. In addition, it can access certain metadata about the endpoint, such as its name and the permissions required to access it.

图 6.4 中的 StaticFileMiddleware 位于 RoutingMiddleware 之前,因此它在选择端点之前执行。相反,AuthorizationMiddleware 位于 RoutingMiddleware 之后,因此它可以判断最终将执行哪个最小的 API 端点。此外,它还可以访问有关终端节点的某些元数据,例如其名称和访问终端节点所需的权限。

TIP The AuthorizationMiddleware needs to know which endpoint will be executed, so it must be placed after the RoutingMiddleware and before the EndpointMiddleware in your middleware pipeline. I discuss authorization in more detail in chapter 24.
提示: AuthorizationMiddleware 需要知道将执行哪个端点,因此它必须放在中间件管道中的 RoutingMiddleware 之后和 EndpointMiddleware 之前。我在第 24 章中更详细地讨论了授权。

It’s important to remember the different roles of the two types of routing middleware when building your application. If you have a piece of middleware that needs to know which endpoint (if any) a given request will execute, you need to make sure to place it after the RoutingMiddleware and before the EndpointMiddleware.

在构建应用程序时,请务必记住这两种类型的路由中间件的不同角色。如果你有一个中间件需要知道给定请求将执行哪个端点(如果有的话),你需要确保将其放在 RoutingMiddleware 之后和 EndpointMiddleware 之前。

TIP If you want to place middleware before the RoutingMiddleware, such as the StaticFileMiddleware in figure 6.4, you need to override the automatic middleware added by WebApplication by calling UseRouting() at the appropriate point in your middleware pipeline. See listing 4.3 in chapter 4 for an example.
提示: 如果你想把中间件放在 RoutingMiddleware 之前,比如图 6.4 中的 StaticFileMiddleware,你需要在中间件管道中的适当位置调用 UseRouting() 来覆盖 WebApplication 添加的自动中间件。有关示例,请参见第 4 章中的清单 4.3。

I’ve covered how the RoutingMiddleware and EndpointMiddleware interact to provide routing capabilities in ASP.NET Core, but we’ve looked at only simple route templates so far. In the next section we’ll look at some of the many features available with route templates.

我已经介绍了 RoutingMiddleware 和 EndpointMiddleware 如何交互以在 ASP.NET Core 中提供路由功能,但到目前为止,我们只了解了简单的路由模板。在下一节中,我们将了解路由模板提供的众多功能中的一些。

6.3 Exploring the route template syntax

6.3 探索路由模板语法

So far in this book we’ve looked at simple route templates consisting of fixed values, such as /person and /test, as well as using a basic route parameter such as /fruit/{id}. In this section we explore the full range of features available in route templates, such as default values, optional segments, and constraints.

到目前为止,在本书中,我们已经了解了由固定值组成的简单路由模板,例如 /person 和 /test,以及使用基本路由参数,例如/fruit/{id} 中。在本节中,我们将探讨路由模板中提供的全部功能,例如默认值、可选分段和约束。

6.3.1 Working with parameters and literal segments

6.3.1 使用参数和文本段

Route templates have a rich, flexible syntax. Figure 6.5, however, shows a simple example, similar to ones you’ve already seen.

路由模板具有丰富、灵活的语法。然而,图 6.5 显示了一个简单的例子,类似于你已经看到的那些。

Figure 6.5 A simple route template showing a literal segment and two required route parameters
图 6.5 一个简单的路由模板,其中显示了一个文本段和两个必需的路由参数

The routing middleware parses a route template by splitting it into segments. A segment is typically separated by the / character, but it can be any valid character.

路由中间件通过将路由模板拆分为段来解析路由模板。段通常由字符分隔,但它可以是任何有效字符。/

DEFINITION Segments that use a character other than / are called complex segments. I generally recommend that you avoid them and stick to using / as a separator. Complex segments have some peculiarities that make them hard to use, so be sure to check the documentation at http://mng.bz/D4RE before you use them.
定义: 使用其他字符的线段称为复杂线段。我通常建议您避免使用它们并坚持用作分隔符。复杂区段具有一些特性,使其难以使用,因此请务必在使用 http://mng.bz/D4RE 之前查看文档。

Each segment is either
每个段是

• A literal value such as product in figure 6.5
如图 6.5 所示的 Literal 值product

• A route parameter such as {category} and {name} in figure 6.5
路由参数,如图 6.5 中的 和{category}{name}

The request URL must match literal values exactly (ignoring case). If you need to match a particular URL exactly, you can use a template consisting only of literals.

请求 URL 必须与文字值完全匹配(忽略大小写)。如果需要精确匹配特定 URL,可以使用仅包含文本的模板。

TIP Literal segments in ASP.NET Core aren’t case-sensitive.
提示: ASP.NET Core 中的文本段不区分大小写。

Imagine that you have a minimal API in your application defined using
假设您在应用程序中有一个使用

app.MapGet("/About/Contact", () => {/* */})

This route template, “/About/Contact", consists only of literal values, so it matches only the exact URL (ignoring case). None of the following URLs would match this route template:
此路由模板 “/About/Contact” 仅包含文本值,因此它仅匹配确切的 URL(忽略大小写)。以下 URL 都不匹配此路由模板:

• /about
• /about-us/contact
• /about/contact/email
• /about/contact-us

Route parameters are sections of a URL that may vary but are still a match for the template. You define them by giving them a name and placing them in braces, such as {category} or {name}. When used in this way, the parameters are required, so the request URL must have a segment that they correspond to, but the value can vary.

路由参数是 URL 的部分,这些部分可能会有所不同,但仍与模板匹配。您可以通过为它们命名并将它们放在大括号中来定义它们,例如{类别}或 {name} 的以这种方式使用时,参数是必需的,因此请求 URL 必须具有它们对应的区段,但该值可能会有所不同。

The ability to use route parameters gives you great flexibility. The simple route template "/{category}/{name}" could be used to match all the product-page URLs in an e-commerce application:
使用路由参数的能力为您提供了极大的灵活性。简单的路由模板 “/{category}/{name}” 可用于匹配电子商务应用程序中的所有产品页面 URL:

• /bags/rucksack-a—Where category=bags and name=rucksack-a
• /shoes/black-size9—Where category=shoes and name=black-size9

But note that this template would not map the following URLs:
但请注意,此模板不会映射以下 URL:

• /socks/—No name parameter specified
• /trousers/mens/formal—Extra URL segment, formal, not found in route template

When a route template defines a route parameter and the route matches a URL, the value associated with the parameter is captured and stored in a dictionary of values associated with the request. These route values typically drive other behavior in the endpoint and can be injected into the handlers (as you saw briefly in chapter 5) in a process called model binding.

当路由模板定义路由参数并且路由与 URL 匹配时,将捕获与该参数关联的值并将其存储在与请求关联的值字典中。这些路由值通常驱动端点中的其他行为,并且可以在称为模型绑定的过程中注入处理程序(如您在第 5 章中简要看到的那样)。

DEFINITION Route values are the values extracted from a URL based on a given route template. Each route parameter in a template has an associated route value, and the values are stored as a string pair in a dictionary. They can be used during model binding, as you’ll see in chapter 7.
定义: 路由值 是根据给定路由模板从 URL 中提取的值。模板中的每个路由参数都有一个关联的路由值,这些值作为字符串对存储在字典中。它们可以在模型绑定期间使用,如第 7 章所示。

Literal segments and route parameters are the two cornerstones of ASP.NET Core route templates. With these two concepts, it’s possible to build all manner of URLs for your application. In the remainder of section 6.3 we’ll look at additional features that let you have optional URL segments, provide default values when a segment isn’t specified, and place additional constraints on the values that are valid for a given route parameter.

文字段和路由参数是 ASP.NET Core 路由模板的两个基石。有了这两个概念,就可以为您的应用程序构建各种 URL。在第 6.3 节的其余部分中,我们将介绍其他功能,这些功能允许您使用可选的 URL 段,在未指定段时提供默认值,以及对给定路由参数有效的值设置其他约束。

6.3.2 Using optional and default values

6.3.2 使用可选值和默认值

In section 6.3.1 you saw a simple route template with a literal segment and two required routing parameters. Figure 6.6 shows a more complex route that uses several additional features.

在 6.3.1 节中,您看到了一个简单的路由模板,其中包含一个文字段和两个必需的路由参数。图6.6 显示了一个更复杂的路由,它使用了几个附加功能。

Figure 6.6 A more complex route template showing literal segments, named route parameters, optional parameters, and default values.
图 6.6 一个更复杂的路由模板,显示文本段、命名路由参数、可选参数和默认值。

The literal product segment and the required {category} parameter are the same as those in in figure 6.6. The {name} parameter looks similar, but it has a default value specified for it by =all. If the URL doesn’t contain a segment corresponding to the {name} parameter, the router will use the all value instead.

文本 product segment 和所需的 {category}参数与图 6.6 中的相同。这{name} 参数看起来类似,但它的默认值由 =all 指定。如果 URL 不包含与 {name} 参数对应的段,则路由器将改用 all 值。

The final segment of figure 6.6, {id?}, defines an optional route parameter called id. This segment of the URL is optional. If this segment is present, the router captures the value for the {id} parameter; if the segment isn’t there, the router doesn’t create a route value for id.

图 6.6 的最后一段 {id?} 定义了一个名为 id 的可选路由参数。URL 的此段是可选的。如果存在此段,则路由器将捕获 {id} 参数的值;如果 segment 不存在,则路由器不会为 id 创建 route 值。

You can specify any number of route parameters in your templates, and these values will be available to you for model binding. The complex route template shown in figure 6.6 allows you to match a greater variety of URLs by making {name} and {id} optional and by providing a default for {name}. Table 6.1 shows some of the URLs that this template would match and the corresponding route values that the router would set.

您可以在模板中指定任意数量的路由参数,这些值将可用于模型绑定。图中所示的复杂路由模板6.6 允许您通过以下方式匹配更多种类的 URL{姓名}和 {id} 可选,并通过为{姓名}。表 6.1 显示了此模板将匹配的一些 URL 以及路由器将设置的相应路由值。

Table 6.1 URLs that would match the template of figure 6.7 and their corresponding route values
表 6.1 与图 6.7 中的模板匹配的 URL 及其相应的路由值

URL Route values
/product/shoes/formal/3 category=shoes, name=formal, id=3
/product/shoes/formal category=shoes, name=formal
/product/shoes category=shoes, name=all
/product/bags/satchels category=bags, name=satchels
/product/phones category=phones, name=all
/product/computers/laptops/ABC-123 category=computers, name=laptops,id=ABC-123

NOTE that there’s no way to specify a value for the optional {id} parameter without also specifying the {category} and {name} parameters. You can put an optional parameter (that doesn’t have a default) only at the end of a route template.
注意: 无法为可选的{id} 参数,但未指定 {category}和 {name} 参数。您可以放置一个可选参数(没有默认值)仅在路由模板的末尾。

Using default values allows you to have multiple ways to call the same URL, which may be desirable in some cases. Given the route template in figure 6.6, the following two URLs are equivalent:

使用默认值允许您通过多种方式调用同一 URL,这在某些情况下可能是可取的。给定图 6.6 中的路由模板,以下两个 URL 是等效的:

• /product/shoes
• /product/shoes/all

Both URLs will execute the same endpoint handler, with the same route values of category=shoes and name=all. Using default values allows you to use shorter, more memorable URLs in your application for common URLs but still gives you the flexibility to match a variety of routes in a single template.

这两个 URL 将执行相同的端点处理程序,具有相同的路由值 category=shoes 和 name=all。使用默认值允许您在应用程序中为常见 URL 使用更短、更易记的 URL,但您仍然可以灵活地在单个模板中匹配各种路由。

6.3.3 Adding additional constraints to route parameters

6.3.3 向路由参数添加其他约束

By defining whether a route parameter is required or optional and whether it has a default value, you can match a broad range of URLs with terse template syntax. Unfortunately, in some cases this approach ends up being a little too broad. Routing only matches URL segments to route parameters; it doesn’t know anything about the data you’re expecting those route parameters to contain. If you consider a template similar to the one in figure 6.6, "/{category}/{name=all}/{id?}", all of the following URLs would match:

通过定义路由参数是必需的还是可选的,以及它是否具有默认值,您可以使用简洁的模板语法匹配各种 URL。不幸的是,在某些情况下,这种方法最终有点过于宽泛。路由仅将 URL 段与路由参数匹配;它不知道您期望这些路由参数包含的数据。如果您考虑类似于图 6.6 中的模板,“/{category}/{name=all}/{id?}”,则以下所有 URL 都将匹配:

• /shoes/sneakers/test
• /shoes/sneakers/123
• /Account/ChangePassword
• /ShoppingCart/Checkout/Start
• /1/2/3

These URLs are perfectly valid given the template’s syntax, but some might cause problems for your application. These URLs have two or three segments, so the router happily assigns route values and matches the template when you might not want it to! These are the route values assigned:

根据模板的语法,这些 URL 是完全有效的,但有些 URL 可能会给您的应用程序带来问题。这些 URL 有两到三个段,因此路由器会很高兴地分配路由值,并在您可能不希望的时候匹配模板!以下是分配的路由值:

• /shoes/sneakers/test has route values category=shoes, name=sneakers, and id=test.

• /shoes/sneakers/123 has route values category=shoes, name=sneakers, and id=123.

• /Account/ChangePassword has route values category=Account, and name=ChangePassword.

• /Cart/Checkout/Start has route values category=Cart, name=Checkout, and id=Start.
• /1/2/3 has route values category=1, name=2, and id=3.

Typically, the router passes route values to handlers through model binding, which you saw briefly in chapter 5 (and which chapter 7 discusses in detail). A minimal API endpoint defined as

通常,路由器通过模型绑定将 route 值传递给处理程序,您在第 5 章中简要看到过(第 7 章详细讨论了)。定义为

app.MapGet("/fruit/{id}", (int id) => "Hello world!");

would obtain the id argument from the id route value. If the id route parameter ends up assigned a noninteger value from the URL, you’ll get an exception when it’s bound to the integer id parameter.

将从 route 值中获取id参数。如果 route 参数最终从 URL 分配了一个非整数值,则当它绑定到 integer 参数时,您将收到异常。

To avoid this problem, it’s possible to add more constraints to a route template that must be satisfied for a URL to be considered a match. You can define constraints in a route template for a given route parameter by using : (colon). {id:int}, for example, would add the IntRouteConstraint to the id parameter. For a given URL to be considered a match, the value assigned to the id route value must be convertible to an integer.

为避免此问题,可以向路由模板添加更多约束,必须满足这些约束才能将 URL 视为匹配项。您可以使用 :(冒号)在路由模板中为给定路由参数定义约束。{id:int},则会添加IntRouteConstraint 设置为 id 参数。要使给定 URL 被视为匹配项,分配给 id 路由值的值必须可转换为整数。

You can apply a large number of route constraints to route templates to ensure that route values are convertible to appropriate types. You can also check more advanced constraints, such as that an integer value has a particular minimum value, that a string value has a maximum length, and that a value matches a given regular expression. Table 6.2 describes some of the available constraints. You can find a more complete list online in Microsoft’s documentation at http://mng.bz/BmRJ.

您可以将大量路由约束应用于路由模板,以确保路由值可转换为适当的类型。您还可以检查更高级的约束,例如整数值是否具有特定的最小值、字符串值是否具有最大长度或值是否与给定的正则表达式匹配。表 6.2 描述了一些可用的约束。您可以找到更完整的在线列表,请参阅 Microsoft 的文档,网址为 http://mng.bz/BmRJ

Table 6.2 A few route constraints and their behavior when applied
Constraint
表 6.2 一些路由约束及其应用时的行为

Constraint Example Description Match examples
int {qty:int} Matches any integer 123, -123, 0
Guid {id:guid} Matches any Guid d071b70c-a812-4b54-87d2-7769528e2814
decimal {cost:decimal} Matches any decimal value 29.99, 52,-1.01
min(value) {age:min(18)} Matches integer values of 18 or greater 18, 20
length(value) {name:length(6)} Matches string values with a length of 6 Andrew,123456
optional int {qty:int?} Optionally matches any integer 123, -123,0,null
optional int max(value) {qty:int:max(10)?} Optionally matches any integer of 10 or less 3, -123, 0,null

TIP As you can see from table 6.2, you can also combine multiple constraints by separating the constraints with colons.
提示: 从表 6.2 中可以看出,您还可以通过用冒号分隔约束来组合多个约束。

Using constraints allows you to narrow down the URLs that a given route template will match. When the routing middleware matches a URL to a route template, it interrogates the constraints to check that they’re all valid. If they aren’t valid, the route template isn’t considered a match, and the endpoint won’t be executed.

使用 constraints 可以缩小给定路由模板将匹配的 URL 的范围。当路由中间件将 URL 与路由模板匹配时,它会询问约束以检查它们是否都有效。如果它们无效,则路由模板不被视为匹配项,并且不会执行终端节点。

WARNING Don’t use route constraints to validate general input, such as to check that an email address is valid. Doing so will result in 404 “Page not found” errors, which will be confusing for the user. You should also be aware that all these built-in constraints assume invariant culture, which may prove to be problematic if your application uses URLs localized for other languages.
警告: 不要使用路由约束来验证常规输入,例如检查电子邮件地址是否有效。这样做将导致 404 “Page not found” 错误,这将使用户感到困惑。您还应该知道,所有这些内置约束都假定固定区域性,如果您的应用程序使用针对其他语言本地化的 URL,这可能会带来问题。

Constraints are best used sparingly, but they can be useful when you have strict requirements on the URLs used by the application, as they can allow you to work around some otherwise-tricky combinations. You can even create custom constraints, as described in the documentation at http://mng.bz/d14Q.

约束最好谨慎使用,但当您对应用程序使用的 URL 有严格要求时,它们可能很有用,因为它们可以让您解决一些其他棘手的组合。您甚至可以创建自定义约束,如 http://mng.bz/d14Q 中的文档中所述。

Constraints and overlapping routes
约束和重叠路由
If you have a well-designed set of URLs for your application, you’ll probably find that you don’t need to use route constraints. Route constraints are most useful when you have overlapping route templates.
如果您的应用程序有一组设计良好的 URL,您可能会发现不需要使用路由约束。当您有重叠的路由模板时,路由约束最有用。
Suppose that you have an endpoint with the route template "/{number}/{name}" and another with the template "/{product}/{id}". When a request with the URL /shoes/123 arrives, which template is chosen? Both match, so the routing middleware panics and throws an exception—not ideal.
假设您有一个路由模板为 “/{number}/{name}” 的终端节点,另一个终端节点的模板为 “/{product}/{id}”。当 URL 为 /shoes/123 的请求到达时,选择哪个模板?两者都匹配,因此路由中间件会 panic 并引发异常 — 这并不理想。
Using constraints can fix this problem. If you update the first template to "/{number:int}/{name}", the integer constraint means that the URL is no longer a match, and the routing middleware can choose correctly. Note, however, that the URL /123/shoes still matches both route templates, so you’re not out of the woods.
使用约束可以解决此问题。如果将第一个模板更新为 “/{number:int}/{name}”,则整数约束表示 URL 不再匹配,路由中间件可以正确选择。但请注意,URL /123/shoes 仍然与两个路由模板匹配,因此您不会陷入困境。
Generally, you should avoid overlapping route templates like these, as they’re often confusing and more trouble than they’re worth. If your route templates are well defined so that each URL maps to a single template, ASP.NET Core routing will work without any difficulties. Sticking to the built-in conventions as far as possible is the best way to stay on the happy path!
通常,您应该避免像这样的重叠路由模板,因为它们通常会令人困惑并且比它们的价值更麻烦。如果您的路由模板定义明确,以便每个 URL 都映射到单个模板,则 ASP.NET Core 路由将毫无困难地工作。尽可能坚持内在的约定俗成是保持快乐道路的最佳方式!

We’re coming to the end of our look at route templates, but before we move on, there’s one more type of parameter to think about: the catch-all parameter.

我们即将结束对路由模板的介绍,但在我们继续之前,还有一种类型的参数需要考虑:catch-all 参数。

6.3.4 Matching arbitrary URLs with the catch-all parameter

6.3.4 使用 catch-all 参数匹配任意 URL

You’ve seen how route templates take URL segments and attempt to match them to parameters or literal strings. These segments normally split around the slash character, /, so the route parameters themselves won’t contain a slash. What do you do if you need them to contain a slash or don’t know how many segments you’re going to have?

您已经了解了路由模板如何获取 URL 段并尝试将它们与参数或文本字符串匹配。这些段通常围绕斜杠字符 / 进行分割,因此,路由参数本身不会包含斜杠。如果您需要它们包含斜杠或不知道您将拥有多少个段,该怎么办?

Imagine that you’re building a currency-converter application that shows the exchange rate from one currency to one or more other currencies. You’re told that the URLs for this page should contain all the currencies as separate segments. Here are some examples:

假设您正在构建一个货币转换器应用程序,该应用程序显示从一种货币到一种或多种其他货币的汇率。您被告知此页面的 URL 应包含所有货币作为单独的段。以下是一些示例:

• /USD/convert/GBP—Show USD with exchange rate to GBP.

• /USD/convert/GBP/EUR—Show USD with exchange rates to GBP and EUR.

• /USD/convert/GBP/EUR/CAD—Show USD with exchange rates for GBP, EUR, and CAD.

If you want to support showing any number of currencies, as these URLs do, you need a way to capture everything after the convert segment. You could achieve this goal by using a catch-all parameter in the route template, as shown in figure 6.7.

如果要像这些 URL 一样支持显示任意数量的货币,则需要一种方法来捕获 convert 区段之后的所有内容。您可以通过在路由模板中使用 catch-all 参数来实现此目标,如图 6.7 所示。

Figure 6.7 You can use catch-all parameters to match the remainder of a URL. Catch-all parameters may include the / character or may be an empty string.
图 6.7 您可以使用 catch-all 参数来匹配 URL 的其余部分。Catch-all 参数可以包含 / 字符,也可以是空字符串。

You can declare catch-all parameters by using either one or two asterisks inside the parameter definition, as in {*others} and {**others}. These parameters match the remaining unmatched portion of a URL, including any slashes or other characters that aren’t part of earlier parameters. They can also match an empty string. For the USD/convert/GBP/EUR URL, the value of the route value others would be the single string "GBP/EUR".

您可以通过在参数定义中使用一个或两个星号来声明 catch-all 参数,如 和 。这些参数与 URL 的剩余不匹配部分匹配,包括任何斜杠或不属于早期参数的其他字符。它们还可以匹配空字符串。对于 URL,route 值的值将是单个字符串 。{*others}{**others}USD/convert/GBP/EURothers"GBP/EUR"

TIP Catch-all parameters are greedy and will capture the whole unmatched portion of a URL. Where possible, to avoid confusion, avoid defining route templates with catch-all parameters that overlap other route templates.
提示: Catch-all 参数是贪婪的,将捕获 URL 的整个不匹配部分。为避免混淆,请避免使用与其他路由模板重叠的 catch-all 参数定义路由模板。

The one- and two-asterisk versions of the catch-all parameter behave identically when routing an incoming request to an endpoint. The difference occurs only when you’re generating URLs (which we’ll cover in the next section): the one-asterisk version URL encodes forward slashes, and the two-asterisk version doesn’t. Typically, the round-trip behavior of the two-asterisk version is what you want.

在将传入请求路由到终端节点时,catch-all 参数的一个星号和两个星号版本的行为相同。仅在生成 URL 时(我们将在下一节中介绍)才会出现差异:一个星号版本的 URL 对正斜杠进行编码,而两个星号的 URL 则不编码。通常,两个星号版本的往返行为是您想要的。

NOTE For examples and a comparison between the one and two-asterisk catch-all versions, see the documentation at http://mng.bz/rWyX.
注意: 有关示例以及 1 个星号和 2 个星号 catch-all 版本之间的比较,请参阅 http://mng.bz/rWyX 中的文档。

You read that last paragraph correctly: mapping URLs to endpoints is only half of the responsibilities of the routing system in ASP.NET Core. It’s also used to generate URLs so that you can reference your endpoints easily from other parts of your application.

您正确地阅读了最后一段:将 URL 映射到终端节点只是 ASP.NET Core 中路由系统职责的一半。它还用于生成 URL,以便您可以轻松地从应用程序的其他部分引用终端节点。

6.4 Generating URLs from route parameters

6.4 从路由参数生成 URL

In this section we’ll look at the other half of routing: generating URLs. You’ll learn how to generate URLs as a string you can use in your code and how to send redirect URLs automatically as a response from your endpoints.

在本节中,我们将了解路由的另一半:生成 URL。您将学习如何将 URL 生成为可在代码中使用的字符串,以及如何自动发送重定向 URL 作为来自终端节点的响应。

One of the benefits and byproducts of using the routing infrastructure in ASP.NET Core is that your URLs can be somewhat fluid. You can change route templates however you like in your application—by renaming /cart to /basket, for example—and won’t get any compilation errors.

在 ASP.NET Core 中使用路由基础设施的好处和副产品之一是您的 URL 可能有些不稳定。您可以在应用程序中根据需要更改路由模板 - 通过将 /cart 重命名为/basket 的 URL,并且不会收到任何编译错误。

Endpoints aren’t isolated, of course; inevitably, you’ll want to include a link to one endpoint in another. Trying to manage these links within your app manually would be a recipe for heartache, broken links, and 404 errors. If your URLs were hardcoded, you’d have to remember to do a find-and-replace operation with every rename!

当然,终端节点不是孤立的;不可避免地,您需要在另一个终端节点中包含指向一个终端节点的链接。尝试在你的应用程序中手动管理这些链接将导致心痛、链接断开和 404 错误。如果您的 URL 是硬编码的,则必须记住在每次重命名时执行查找和替换作!

Luckily, you can use the routing infrastructure to generate appropriate URLs dynamically at runtime instead, freeing you from the burden. Conceptually, this process is almost the exact reverse of the process of mapping a URL to an endpoint, as shown in figure 6.8. In the routing case, the routing middleware takes a URL, matches it to a route template, and splits it into route values. In the URL generation case, the generator takes in the route values and combines them with a route template to build a URL.

幸运的是,您可以使用路由基础设施在运行时动态生成适当的 URL,从而减轻您的负担。从概念上讲,此过程几乎与将 URL 映射到端点的过程完全相反,如图 6.8 所示。在路由情况下,路由中间件采用 URL,将其与路由模板匹配,并将其拆分为路由值。在 URL生成的情况下,生成器会接收路由值并将它们与路由模板组合在一起以构建 URL。

Figure 6.8 A comparison between routing and URL generation. Routing takes in a URL and generates route values, but URL generation uses route values to generate a URL.
图 6.8 路由和 URL 生成之间的比较。路由接收 URL 并生成route 值,但 URL 生成使用路由值来生成 URL。

You can use the LinkGenerator class to generate URLs for your minimal APIs. You can use it in any part of your application, so you can use it in middleware and any other services too. LinkGenerator has various methods for generating URLs, such as GetPathByPage and GetPathByAction, which are used specifically for routing to Razor Pages and MVC actions, so we’ll look at those in chapter 14. We’re interested in the methods related to named routes.

您可以使用 LinkGenerator 类为您的最小 API 生成 URL。您可以在应用程序的任何部分使用它,因此您也可以在中间件和任何其他服务中使用它。LinkGenerator 具有各种生成 URL 的方法,例如 GetPathByPage 和 GetPathByAction,它们专门用于路由到 Razor Pages 和 MVC作,因此我们将在第 14 章中介绍这些方法。我们对与命名路由相关的方法感兴趣。

6.4.1 Generating URLs for a minimal API endpoint with LinkGenerator

6.4.1 使用 LinkGenerator 为最小 API 端点生成 URL

You’ll need to generate URLs in various places in your application, and one common location is your minimal API endpoints. The following listing shows how you could generate a link to one endpoint from another by annotating the target endpoint with a name and using the LinkGenerator class.

您需要在应用程序的不同位置生成 URL,一个常见位置是最小 API 终端节点。下面的清单显示了如何通过使用名称注释目标终端节点并使用 LinkGenerator 类来生成从另一个终端节点到另一个终端节点的链接 。

Listing 6.2 Generating a URL LinkGenerator and a named endpoint
清单 6.2 生成一个 URL LinkGenerator 和一个命名的端点

app.MapGet("/product/{name}", (string name) => $"The product is {name}")        // ❶
    .WithName("product");                                                       // ❷

app.MapGet("/links", (LinkGenerator links) =>                                   // ❸
{
    string link = links.GetPathByName("product",                                // ❹
        new { name = "big-widget" });                                           // ❹
    return $"View the product at {link}";                                       // ❺
});

❶ The endpoint echoes the name it receives in the route template.
端点回显它在路由模板中接收的名称。

❷ Gives the endpoint a name by adding metadata to it
通过向终端节点添加元数据来为终端节点命名

❸ References the LinkGenerator class in the endpoint handler
在端点处理程序中引用 LinkGenerator 类

❹ Creates a link using the route name “product” and provides a value for the route parameter
使用路由名称 “product” 创建链接,并为路由参数提供值

❺ Returns the value “View the product at /product/big-widget”
返回值 “View the product at /product/big-widget”

The WithName() method adds metadata to your endpoints so that they can be referenced by other parts of your application. In this case, we’re adding a name to the endpoint so we can refer to it later. You’ll learn more about metadata in chapter 11.

WithName() 方法将元数据添加到端点,以便应用程序的其他部分可以引用它们。在本例中,我们将向终端节点添加一个名称,以便稍后可以引用它。您将在第 11 章中了解有关元数据的更多信息。

Note Endpoint names are case-sensitive (unlike the route templates themselves) and must be globally unique. Duplicate names cause exceptions at runtime.
注意: 终端节点名称区分大小写(与路由模板本身不同),并且必须全局唯一。重复的名称会导致运行时出现异常

The LinkGenerator is a service available anywhere in ASP.NET Core. You can access it from your endpoints by including it as a parameter in the handler.

LinkGenerator 是一项在 ASP.NET Core 中的任何位置都可用的服务。您可以通过将其作为参数包含在处理程序中,从终端节点访问它。

NOTE You can reference the LinkGenerator in your handler because it’s registered with the dependency injection container automatically. You’ll learn about dependency injection in chapters 8 and 9.
注意: 您可以在处理程序中引用 LinkGenerator,因为它会自动注册到依赖项注入容器中。您将在第 8 章和第 9 章中了解依赖关系注入。

The GetPathByName() method takes the name of a route and, optionally, route data. The route data is packaged as key-value pairs into a single C# anonymous object. If you need to pass more than one route value, you can add more properties to the anonymous object. Then the helper will generate a path based on the referenced endpoint’s route template.

GetPathByName() 方法采用路由的名称,也可以选择获取路由数据。路由数据作为键值对打包到单个 C# 匿名对象中。如果需要传递多个路由值,可以添加更多properties 添加到匿名对象。然后,帮助程序将根据引用的端点的路由模板生成路径。

Listing 6.2 shows how to generate a path. But you can also generate a complete URL by using the GetUriByName() method and providing values for the host and scheme, as in this example:

清单 6.2 展示了如何生成路径。但是,您也可以通过使用 GetUriByName() 方法并为 host 和 scheme 提供值来生成完整的 URL,如下例所示:

links.GetUriByName("product", new { Name = "super-fancy-widget"},
    "https", new HostString("localhost"));

Also, some methods available on LinkGenerator take an HttpContext. These methods are often easier to use in an endpoint handler, as they extract ambient values such as the scheme and hostname from the incoming request and reuse them for URL generation.

此外,LinkGenerator 上可用的一些方法采用 HttpContext。这些方法通常更容易在端点处理程序中使用,因为它们从传入请求中提取环境值(如 scheme 和 hostname),并将它们重新用于 URL 生成。

WARNING Be careful when using the GetUriByName method. It’s possible to expose vulnerabilities in your app if you use unvalidated host values. For more information on host filtering and why it’s important, see this post: http://mng.bz/V1d5.
警告: 使用 GetUriByName 方法时要小心。如果您使用未经验证的 host 值,则可能会暴露应用程序中的漏洞。有关主机筛选及其重要性的更多信息,请参阅此博文:http://mng.bz/V1d5

In listing 6.2, as well as providing the route name, I passed in an anonymous object to GetPathByName:
在清单 6.2 中,除了提供路由名称外,我还向 GetPathByName 传入了一个匿名对象:

string link = links.GetPathByName("product", new { name = "big-widget"});

This object provides additional route values when generating the URL, in this case setting the name parameter to "big-widget":

此对象在生成 URL 时提供额外的路由值,在本例中,将 name 参数设置为 “big- widget”。

If a selected route explicitly includes the defined route value in its definition, such as in the "/product/{name}" route template, the route value will be used in the URL path, resulting in /product/big-widget. If a route doesn’t contain the route value explicitly, as in the "/product" template, the route value is appended to the query string as additional data. as in /product?name=big-widget.

如果所选路由在其定义中显式包含定义的路由值,例如在“/product/{name}”路由模板中,则路由值将在 URL 路径中使用,从而生成 /product/big-widget。如果路由没有显式包含路由值(如“/product”模板中所示),则路由值将作为附加数据附加到查询字符串中。如 /product?name=big-widget 中所示。

6.4.2 Generating URLs with IResults

6.4.2 使用 IResults 生成 URL

Generating URLs that link to other endpoints is common when you’re creating a REST API, for example. But you don’t always need to display URLs. Sometimes, you want to redirect a user to a URL automatically. In that situation you can use Results.RedirectToRoute() to handle the URL generation instead.

例如,在创建 REST API 时,生成链接到其他终端节点的 URL 很常见。但您并不总是需要显示 URL。有时,您希望自动将用户重定向到 URL。在这种情况下,您可以使用 Results.RedirectToRoute() 来处理 URL 生成。

NOTE Redirects are more common with server-rendered applications such as Razor Pages, but they’re perfectly valid for API applications too.
注意: 重定向在服务器呈现的应用程序(如 Razor Pages)中更为常见,但它们对 API 应用程序也完全有效。

Listing 6.3 shows how you can return a response from an endpoint that automatically redirects a user to a different named endpoint. The RedirectToRoute() method takes the name of the endpoint and any required route parameters, and generates a URL in a similar way to LinkGenerator. The minimal API framework automatically sends the generated URL as the response, so you never see the URL in your code. Then the user’s browser reads the URL from the response and automatically redirects to the new page.

清单 6.3 展示了如何从端点返回响应,该响应会自动将用户重定向到不同的命名端点。RedirectToRoute() 方法采用端点的名称和任何必需的路由参数,并以与 LinkGenerator 类似的方式生成 URL。最小 API 框架会自动将生成的 URL 作为响应发送,因此您永远不会在代码中看到 URL。然后,用户的浏览器读取 URL并自动重定向到新页面。

Listing 6.3 Generating a redirect URL using Results.RedirectToRoute()
清单 6.3 使用 Result 生成重定向 URLs.RedirectToRoute()

app.MapGet("/test", () => "Hello world!")
    .WithName("hello");                        // ❶

app.MapGet("/redirect-me",
    () => Results.RedirectToRoute("hello"))    // ❷

❶ Annotates the route with the name “hello”
使用名称 “hello” 注释路由

❷ Generates a response that sends a redirect to the “hello” endpoint
生成一个响应,将重定向发送到 “hello” 端点

By default, RedirectToRoute() generates a 302 Found response and includes the generated URL in the Location response header. You can control the status code used by setting the optional parameters preserveMethod and permanent as follows:
默认情况下,RedirectToRoute() 会生成 302 Found 响应,并将生成的 URL 包含在 Location 响应标头中。您可以通过设置可选参数 preserveMethod 和 permanent 来控制使用的状态码,如下所示:

• permanent=false, preserveMethod=false—302 Found

• permanent=true, preserveMethod=false—301 Moved Permanently

• permanent=false, preserveMethod=true—307 Temporary Redirect

• permanent=true, preserveMethod=true—308 Permanent Redirect

NOTE Each of the redirect status codes has a slightly different semantic meaning, though in practice, many sites simply use 302. Be careful with the permanent move status codes; they’ll cause browsers to never call the original URL, always favoring the redirect location. For a good explanation of these codes (and the useful 303 See Other status code), see the Mozilla documentation at http://mng.bz/x4GB.
注意: 每个重定向状态代码的语义含义略有不同,但实际上,许多站点只需使用 302。小心永久移动状态代码;它们将导致浏览器从不调用原始 URL,始终优先使用重定向位置。有关这些代码(以及有用的 303 See Other 状态代码)的良好解释,请参阅 Mozilla 文档 http://mng.bz/x4GB

As well as redirecting to a specific endpoint, you can redirect to an arbitrary URL by using the Results.Redirect() method. This method works in the same way as RedirectToRoute() but takes a URL instead of a route name and can be useful for redirecting to external URLs.

除了重定向到特定端点外,您还可以使用 Results.Redirect() 方法重定向到任意 URL。此方法的工作方式与 RedirectToRoute() 相同,但采用 URL 而不是路由名称,可用于重定向到外部 URL。

Whether you’re generating URLs by using LinkGenerator or RedirectToRoute(), you need to be careful in these route generation methods. Make sure to provide the correct endpoint name and any necessary route parameters. If you get something wrong—if you have a typo in your endpoint name or forget to include a required route parameter, for example—the URL generated will be null. Sometimes it’s worth checking the generated URL for null explicitly to make sure that there are no problems.

无论您是使用 LinkGenerator 还是 RedirectToRoute() 生成 URL,都需要小心使用这些路由生成方法。确保提供正确的终端节点名称和任何必要的路由参数。如果出现错误(例如,如果终端节点名称中有拼写错误或忘记包含必需的路由参数),生成的 URL 将为 null。有时,值得显式检查生成的 URL 是否为 null,以确保没有问题。

6.4.3 Controlling your generated URLs with RouteOptions

6.4.3 使用 RouteOptions 控制生成的 URL

Your endpoint routes are the public surface of your APIs, so you may well have opinions on how they should look. By default, LinkGenerator does its best to generate routes the same way you define them; if you define an endpoint with the route template /MyRoute, LinkGenerator generates the path /MyRoute. But what if that path isn’t what you want? What if you’d rather have LinkGenerator produce prettier paths, such as /myroute or /myroute/? In this section you’ll learn how to configure URL generation both globally and on a case-by-case basis.

您的终端节点路由是 API 的公共表面,因此您可能对它们的外观有自己的想法。默认情况下,LinkGenerator 会尽最大努力以与定义路由相同的方式生成路由;如果您定义了一个端点使用路由模板 /MyRoute,LinkGenerator 会生成路径 /MyRoute。但是,如果这条路不是您想要的呢?如果您希望 LinkGenerator 生成更漂亮的路径,例如 /myroute 或 /myroute/,该怎么办?在本节中,您将学习如何全局和逐个配置 URL 生成。

NOTE Whether to add a trailing slash to your URLs is largely a question of taste, but the choice has some implications in terms of both usability and search results. I typically choose to add trailing slashes for Razor Pages applications but not for APIs. For details, see http://mng.bz/Ao1W.
注意: 是否向 URL 添加尾部斜杠在很大程度上是一个品味问题,但这种选择在可用性和搜索结果方面都有一些影响。我通常选择为 Razor Pages 应用程序添加尾部斜杠,但不为 API 添加。有关详细信息,请参阅 http://mng.bz/Ao1W

When ASP.NET Core matches an incoming URL against your route templates by using routing, it uses a case-insensitive comparison, as you saw in chapter 5. So if you have a route template /MyRoute, requests to /myroute, /MYROUTE, and even /myROUTE match. But when generating URLs, LinkGenerator needs to choose a single version to use. By default, it uses the same casing that you defined in your route templates. So if you write

当 ASP.NET Core 使用路由将传入 URL 与路由模板匹配时,它会使用不区分大小写的比较,如第 5 章所示。因此,如果您有路由模板 /MyRoute,则对 /myroute、/MYROUTE 甚至 /myROUTE 的请求都会匹配。但是在生成 URL 时,LinkGenerator 需要选择一个版本来使用。默认情况下,它使用您在路由模板中定义的相同大小写。所以如果你写

app.MapGet("/MyRoute", () => "Hello world!").WithName("route1");

LinkGenerator.GetPathByName("route1") returns/MyRoute.
LinkGenerator.GetPathByName(“route1”) 返回/MyRoute。

Although that’s a good default, you’d probably prefer that all the links generated by your app be consistent. I like all my links to be lowercase, regardless of whether I accidentally failed to make my route template lowercase.

尽管这是一个很好的默认值,但你可能希望你的应用程序生成的所有链接都是一致的。我喜欢我的所有链接都是小写的,无论我是否意外地未能将我的路由模板设置为小写。

You can control the route generation rules by using RouteOptions. You configure the RouteOptions for your app using the Configure extension method on WebApplicationBuilder.Services, which updates the RouteOptions instance for the app using the configuration system.

您可以使用 RouteOptions 控制路由生成规则。您可以使用 WebApplicationBuilder.Services 上的 Configure 扩展方法为应用程序配置 RouteOptions,该方法使用配置系统更新应用程序的 RouteOptions 实例。

Note You’ll learn all about the configuration system and the Configure method in chapter 10.
注意 您将在第 10 章中了解有关配置系统和Configure方法的所有信息。

RouteOptions contains several configuration options, as shown in listing 6.4. These settings control whether the URLs your app generates are forced to be lowercase, whether the query string should also be lowercase, and whether a trailing slash (/) should be appended to the final URLs. In the listing, I set the URL to be lowercased, for the trailing slash to be added, and for the query string to remain unchanged.

RouteOptions 包含几个配置选项,如清单 6.4 所示。这些设置控制是否强制应用程序生成的 URL 为小写,查询字符串是否也应为小写,以及是否应将尾部斜杠 (/) 附加到最终 URL。在清单中,我将 URL 设置为小写,以便添加尾部斜杠,并使查询字符串保持不变。

Note In listing 6.4 the whole path is lowercased, including any route parameter segments such as {name}. Only the query string retains its original casing.
注意 在清单 6.4 中,整个路径都是小写的,包括任何路由参数段,比如 {name}。只有查询字符串保留其原始大小写。

Listing 6.4 Configuring link generation using RouteOptions
清单 6.4 使用路由选项

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<RouteOptions>(o =>                                        // ❶
{                                                                                    // ❶
    o.LowercaseUrls = true;                                                          // ❶
    o.AppendTrailingSlash = true;                                                    // ❶
    o.LowercaseQueryStrings = false;                                                 // ❷
});

WebApplication app = builder.Build();

app.MapGet("/HealthCheck", () => Results.Ok()).WithName("healthcheck");
app.MapGet("/{name}", (string name) => name).WithName("product");

app.MapGet("/", (LinkGenerator links) =>
new[]
{
    links.GetPathByName("healthcheck"),                                              // ❸
    links.GetPathByName("product",                                                   // ❹
        new { Name = "Big-Widget", Q = "Test"})                                      // ❹
});

app.Run();

❶ Configures the RouteOptions used for link generation
配置用于链接生成的 RouteOptions

❷ All the settings default to false.
所有设置都默认为 false。

❸ Returns /healthcheck/
返回 /healthcheck/

❹ Returns /big-widget/?Q=Test
返回 /big-widget/?Q=Test

Whatever default options you choose, you should try to use them throughout your whole app, but in some cases that may not be possible. You might have a legacy API that you need to emulate, for example, and can’t use lowercase URLs. In these cases, you can override the defaults by passing an optional LinkOptions parameter to LinkGenerator methods. The values you set in LinkOptions override the default values set in RouteOptions. Generating a link for the app in listing 6.4 by using

无论您选择什么默认选项,您都应该尝试在整个应用程序中使用它们,但在某些情况下,这可能无法实现。例如,您可能有一个需要模拟的旧 API,并且不能使用小写 URL。在这些情况下,您可以通过将可选的 LinkOptions 参数传递给 LinkGenerator 方法来覆盖默认值。在 LinkOptions 中设置的值将覆盖在 RouteOptions 中设置的默认值。

links.GetPathByName("healthcheck",
options: new LinkOptions
{
    LowercaseUrls = false,
    AppendTrailingSlash = false,
});

would return the value /HealthCheck. Without the LinkOptions parameter, GetPathByName would return /healthcheck/.

将返回值 /HealthCheck。如果没有LinkOptions 参数,GetPathByName 将返回
/healthcheck/ 中。

Congratulations—you’ve made it all the way through this detailed discussion of routing! Routing is one of those topics that people often get stuck on when they come to building an application, which can be frustrating. We’ll revisit routing when we look at Razor Pages in chapter 14 and web API controllers in chapter 20, but rest assured that this chapter has covered all the tricky details!

恭喜 — 您已经完成了这个关于路由的详细讨论!路由是人们在构建应用程序时经常陷入困境的话题之一,这可能会令人沮丧。当我们在第 14 章中查看 Razor 页面,在第 20 章中查看 Web API 控制器时,我们将重新讨论路由,但请放心,本章已经涵盖了所有棘手的细节!

In chapter 7 we’ll dive into model binding. You’ll see how the route values generated during routing are bound to your endpoint handler parameters and, perhaps more important, how to validate the values you’re provided.

在第 7 章中,我们将深入探讨模型绑定。您将看到路由期间生成的路由值如何绑定到终端节点处理程序参数,也许更重要的是,如何验证您提供的值。

6.5 Summary

6.5 总结

Routing is the process of mapping an incoming request URL to an endpoint that executes to generate a response. Routing provides flexibility to your API implementations, enabling you to map multiple URLs to a single endpoint, for example.
路由是将传入请求 URL 映射到终端节点的过程,该终端节点执行以生成响应。路由为您的 API 实施提供了灵活性,例如,使您能够将多个 URL 映射到单个终端节点。

ASP.NET Core uses two pieces of middleware for routing. The EndpointRoutingMiddleware and the EndpointMiddleware. WebApplication adds both pieces of middleware to your pipeline by default, so typically, you don’t add them to your application manually.
ASP.NET Core 使用两个中间件进行路由。EndpointRoutingMiddleware 和 EndpointMiddleware.默认情况下,WebApplication 会将这两个中间件添加到您的管道中,因此通常不会手动将它们添加到您的应用程序中。

The EndpointRoutingMiddleware selects which endpoint should be executed by using routing to match the request URL. The EndpointMiddleware executes the endpoint. Having two separate middleware components means that middleware placed between them can react based on the endpoint that will execute when it reaches the end of the pipeline.
EndpointRoutingMiddleware 通过使用路由来匹配请求 URL,从而选择应该执行的端点。EndpointMiddleware 执行 endpoint。拥有两个单独的中间件组件意味着放置在它们之间的中间件可以根据到达管道末尾时将执行的端点做出反应。

Route templates define the structure of known URLs in your application. They’re strings with placeholders for variables that can contain optional values and map to endpoint handlers. You should think about your routes carefully, as they’re the public surface of your application.
路由模板定义应用程序中已知 URL 的结构。它们是带有变量占位符的字符串,这些变量可以包含可选值并映射到终端节点处理程序。您应该仔细考虑路由,因为它们是应用程序的公共表面。

Route parameters are variable values extracted from a request’s URL. You can use route parameters to map multiple URLs to the same endpoint and to extract the variable value from the URL automatically.
路由参数是从请求的 URL 中提取的变量值。您可以使用路由参数将多个 URL 映射到同一终端节点,并自动从 URL 中提取变量值。

Route parameters can be optional and can use default values when a value is missing. You should use optional and default parameters sparingly, as they can make your APIs harder to understand, but they can be useful in some cases. Optional parameters must be the last segment of a route.
路由参数可以是可选的,并且在缺少值时可以使用默认值。您应该谨慎使用可选参数和默认参数,因为它们会使您的 API 更难理解,但在某些情况下它们可能很有用。可选参数必须是路由的最后一段。

Route parameters can have constraints that restrict the possible values allowed. If a route parameter doesn’t match its constraints, the route isn’t considered to be a match. This approach can help you disambiguate between two similar routes, but you shouldn’t use constraints for validation.
路由参数可以具有限制允许的可能值的约束。如果路由参数与其约束条件不匹配,则不会将该路由视为匹配项。此方法可以帮助您消除两个相似路由之间的歧义,但您不应使用 constraints 进行验证。

Use a catch-all parameter to capture the remainder of a URL into a route value. Unlike standard route parameters, catch-all parameters can include slashes (/) in the captured values.
使用 catch-all 参数将 URL 的其余部分捕获到路由值中。与标准路由参数不同,catch-all 参数可以在捕获的值中包含斜杠 (/)。

You can use the routing infrastructure to generate URLs for your application. This approach ensures that all your links remain correct if you change your endpoint’s route templates.
您可以使用路由基础设施为您的应用程序生成 URL。此方法可确保在更改终端节点的路由模板时所有链接都保持正确。

The LinkGenerator can be used to generate URLs from minimal API endpoints. Provide the name of the endpoint to link to and any required route values to generate an appropriate URL.
LinkGenerator 可用于从最小的 API 端点生成 URL。提供要链接到的端点的名称以及生成相应 URL 所需的任何路由值。

You can use the RedirectToRoute method to generate URLs while also generating a redirect response. This approach is useful when you don’t need to reference the URL in code.
您可以使用 RedirectToRoute 方法生成 URL,同时生成重定向响应。当您不需要在代码中引用 URL 时,此方法非常有用。

By default, URLs are generated using the same casing as the route template and any supplied route parameters. Instead, you can force lowercase URLs, lowercase query strings, and trailing slashes by customizing RouteOptions, calling builder.Services.Configure<RouteOptions>().
默认情况下,URL 是使用与路由模板相同的大小写和提供的任何路由参数生成的。相反,您可以通过自定义 RouteOptions、调用 builder.Services.Configure<RouteOptions>()来强制使用小写 URL、小写查询字符串和尾部斜杠。

You can change the settings for a single URL generation by passing a LinkOptions object to the LinkGenerator methods. These methods can be useful when you need to differ from the defaults for a single endpoint, such as when you’re trying to match an existing legacy route.
您可以通过将 LinkOptions 对象传递给 LinkGenerator 方法来更改单个 URL 生成的设置。当您需要与单个终端节点的默认值不同时,例如当您尝试匹配现有的旧路由时,这些方法可能很有用。

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

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

This chapter covers

本章涵盖

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

• Generating responses with IResult
生成响应IResult

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

alt text

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

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

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

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

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

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

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

alt text

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5.2 Defining minimal API endpoints

5.2 定义最小 API 端点

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

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

5.2.1 Extracting values from the URL with routing

5.2.1 使用路由 从 URL 中提取值

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

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

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

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

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

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

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

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

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

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

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

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

app.Run();

public record Person(string FirstName, string LastName);

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

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

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

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

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

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

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

5.2.2 Mapping verbs to endpoints

5.2.2 将动词映射到端点

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5.2.3 Defining route handlers with functions

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

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

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

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

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

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

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

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

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

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

app.Run();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

alt text

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

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

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

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

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

5.3 Generating responses with IResult

5.3 使用 IResult 生成响应

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

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

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

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

alt text

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

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

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

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

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

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

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

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

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

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

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

5.3.1 Returning status codes with Results and TypedResults

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

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

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

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

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

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

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

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

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

using System.Collections.Concurrent;

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

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

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

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

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

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

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

app.Run();

record Fruit(string Name, int stock);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

app.Run();

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

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

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

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

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

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

5.3.2 Returning useful errors with Problem Details

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

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

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

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

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

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

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

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

var _fruit = new System.Collections.Concurrent.ConcurrentDictionary<string, Fruit>();

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

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

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

record Fruit(string Name, int stock);

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

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

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

alt text

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

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

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

5.3.3 Converting all your responses to Problem Details

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

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

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

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

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

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

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

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

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

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

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

Converting exceptions to Problem Details

将异常转换为 Problem Details

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

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

app.UseExceptionHandler("/error");

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

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

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

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

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

alt text

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

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

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

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

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

WebApplication app = builder.Build();

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

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

app.Run();

❶ Adds the IProblemDetailsService implementation
添加 IProblemDetailsService 实现

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

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

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

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

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

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

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

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

Converting error status codes to Problem Details

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

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

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

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

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

alt text

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

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

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

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

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

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

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

WebApplication app = builder.Build();

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

app.UseStatusCodePages();                        // ❷

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

app.Run();

❶ Adds the IProblemDetailsService implementation
添加 IProblemDetailsService 实现

❷ Adds the StatusCodePagesMiddleware
添加了 StatusCodePagesMiddleware

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

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

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

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

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

5.3.4 Returning other data types

5.3.4 返回其他数据类型

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

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

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

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

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

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

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

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

5.4 Running common code with endpoint filters

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

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

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

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

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

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

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

var _fruit = new System.Collections.Concurrent.ConcurrentDictionary<string, Fruit>();

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

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

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

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

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

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

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

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

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

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

alt text

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

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

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

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

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

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

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

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

app.Run();

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

        return await next(context);                                        // ❻
    }
}  
record Fruit(string Name, int stock);

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

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

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

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

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

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

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

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

5.4.1 Adding multiple filters to an endpoint

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

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

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

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

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

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

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

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

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


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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

alt text

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

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

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

5.4.2 Filters or middleware: Which should you choose?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5.4.3 Generalizing your endpoint filters

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

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

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

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

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

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

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

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

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

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

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

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

alt text

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

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

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

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


using System.Collections.Concurrent;
using System.Reflection;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5.4.4 Implementing the IEndpointFilter interface

5.4.4 实现 IEndpointFilter 接口

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

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

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

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

Listing 5.12 Implementing IEndpointFilter
清单 5.12 实现IEndpointFilter


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

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

app.Run();

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

    return await next(context);
    }
}

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

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

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

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

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

5.5 Organizing your APIs with route groups

使用路由组组织 API

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

alt text

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

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

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

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

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

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

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


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

var _fruit = new System.Collections.Concurrent.ConcurrentDictionary<string, Fruit>();

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

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

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

// 还有其他代码,详细的查看源代码

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

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

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

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

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

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

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

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

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

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

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

5.6 Summary

5.6 总结

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ASP.NET Core in Action 4 Handling requests with the middleware pipeline

4 Handling requests with the middleware pipeline

使用中间件管道处理请求

This chapter covers

本章涵盖

  • Understanding middleware
    了解中间件
  • Serving static files using middleware
    使用中间件提供静态文件
  • Adding functionality using middleware
    使用中间件添加功能
  • Combining middleware to form a pipeline
    组合中间件形成管道
  • Handling exceptions and errors with middleware
    使用中间件处理异常和错误

In chapter 3 you had a whistle-stop tour of a complete ASP.NET Core application to see how the components come together to create a web application. In this chapter, we’ll focus on one small subsection: the middleware pipeline.

在第 3 章中,您简要介绍了一个完整的 ASP.NET Core 应用程序,以了解这些组件如何组合在一起以创建 Web 应用程序。在本章中,我们将重点介绍一个小部分:中间件管道。

In ASP.NET Core, middleware consists of C# classes or functions that handle an HTTP request or response. Middleware is chained together, with the output of one acting as the input to the next to form a pipeline.

在 ASP.NET Core 中,中间件由处理 HTTP 请求或响应的 C# 类或函数组成。中间件链接在一起,一个中间件的输出充当下一个中间件的输入,形成一个管道。

The middleware pipeline is one of the most important parts of configuration for defining how your application behaves and how it responds to requests. Understanding how to build and compose middleware is key to adding functionality to your applications.

中间件管道是配置中最重要的部分之一,用于定义应用程序的行为方式和响应请求的方式。了解如何构建和组合中间件是向应用程序添加功能的关键。

In this chapter you’ll learn what middleware is and how to use it to create a pipeline. You’ll see how you can chain multiple middleware components together, with each component adding a discrete piece of functionality. The examples in this chapter are limited to using existing middleware components, showing how to arrange them in the correct way for your application. In chapter 31 you’ll learn how to build your own middleware components and incorporate them into the pipeline.

在本章中,您将了解什么是中间件以及如何使用它来创建管道。您将了解如何将多个中间件组件链接在一起,每个组件添加一个独立的功能。本章中的示例仅限于使用现有的中间件组件,展示了如何为您的应用程序以正确的方式排列它们。在第 31 章中,您将学习如何构建自己的中间件组件并将其合并到管道中。

We’ll begin by looking at the concept of middleware, all the things you can achieve with it, and how a middleware component often maps to a cross-cutting concern. These functions of an application cut across multiple different layers. Logging, error handling, and security are classic cross-cutting concerns that are required by many parts of your application. Because all requests pass through the middleware pipeline, it’s the preferred location to configure and handle this functionality.

我们首先要了解中间件的概念,你可以用它实现的所有事情,以及中间件组件通常如何映射到一个横切关注点。应用程序的这些功能跨越多个不同的层。日志记录、错误处理和安全性是应用程序的许多部分都需要的典型横切关注点。由于所有请求都通过中间件管道传递,因此它是配置和处理此功能的首选位置。

In section 4.2 I’ll explain how you can compose individual middleware components into a pipeline. You’ll start out small, with a web app that displays only a holding page. From there, you’ll learn how to build a simple static-file server that returns requested files from a folder on disk.

在 Section 4.2 中,我将解释如何将单个中间件组件组合到一个管道中。您将从小规模开始,使用仅显示保留页的 Web 应用程序。从那里,您将学习如何构建一个简单的静态文件服务器,该服务器从磁盘上的文件夹返回请求的文件。

Next, you’ll move on to a more complex pipeline containing multiple middleware. In this example you’ll explore the importance of ordering in the middleware pipeline, and you’ll see how requests are handled when your pipeline contains multiple middleware.

接下来,您将转到包含多个中间件的更复杂的管道。在此示例中,您将探索中间件管道中排序的重要性,并了解当管道包含多个中间件时如何处理请求。

In section 4.3 you’ll learn how you can use middleware to deal with an important aspect of any application: error handling. Errors are a fact of life for all applications, so it’s important that you account for them when building your app.

在 Section 4.3 中,您将学习如何使用 middleware 来处理任何应用程序的一个重要方面:错误处理。错误是所有应用程序都不可避免的事实,因此在构建应用程序时考虑错误非常重要。

You can handle errors in a few ways. Errors are among the classic cross-cutting concerns, and middleware is well placed to provide the required functionality. In section 4.3 I’ll show how you can handle exceptions with middleware provided by Microsoft. In particular, you’ll learn about two different components:

您可以通过多种方式处理错误。错误是典型的横切关注点之一,中间件可以很好地提供所需的功能。在 Section 4.3 中,我将展示如何使用 Microsoft 提供的中间件处理异常。具体而言,您将了解两个不同的组件:

  • DeveloperExceptionPageMiddleware—Provides quick error feedback when building an application
    DeveloperExceptionPageMiddleware— 在构建应用程序时提供快速错误反馈

  • ExceptionHandlerMiddleware—Provides a generic error page in production so that you don’t leak any sensitive details
    ExceptionHandlerMiddleware- 在生产中提供通用错误页面,以便您不会泄露任何敏感详细信息

You won’t see how to build your own middleware in this chapter; instead, you’ll see that you can go a long way by using the components provided as part of ASP.NET Core. When you understand the middleware pipeline and its behavior, you’ll find it much easier to understand when and why custom middleware is required. With that in mind, let’s dive in!

在本章中,您不会看到如何构建自己的中间件;相反,您会发现,通过使用作为 ASP.NET Core 的一部分提供的组件,您可以走很长一段路。当您了解中间件管道及其行为时,您会发现更容易理解何时以及为什么需要自定义中间件。考虑到这一点,让我们开始吧!

4.1 Defining middleware

4.1 定义中间件

The word middleware is used in a variety of contexts in software development and IT, but it’s not a particularly descriptive word.

中间件这个词在软件开发和 IT 中的各种上下文中都有使用,但它不是一个特别具有描述性的词。

In ASP.NET Core, middleware is C# classes[1] that can handle an HTTP request or response. Middleware can
在 ASP.NET Core 中,中间件是可以处理 HTTP 请求或响应的 C# 类[1]。中间件可以

  • Handle an incoming HTTP request by generating an HTTP response
    通过生成 HTTP 响应来处理传入的 HTTP 请求

  • Process an incoming HTTP request, modify it, and pass it on to another piece of middleware
    处理传入的 HTTP 请求,对其进行修改,并将其传递给另一个中间件

  • Process an outgoing HTTP response, modify it, and pass it on to another piece of middleware or to the ASP.NET Core web server
    处理传出的 HTTP 响应,对其进行修改,并将其传递给另一个中间件或 ASP.NET Core Web 服务器

You can use middleware in a multitude of ways in your own applications. A piece of logging middleware, for example, might note when a request arrived and then pass it on to another piece of middleware. Meanwhile, a static-file middleware component might spot an incoming request for an image with a specific name, load the image from disk, and send it back to the user without passing it on.

您可以在自己的应用程序中以多种方式使用中间件。例如,一个日志记录中间件可能会记录请求何时到达,然后将其传递给另一个中间件。同时,静态文件中间件组件可能会发现对具有特定名称的图像的传入请求,从磁盘加载图像,并将其发送回给用户,而无需传递。

The most important piece of middleware in most ASP.NET Core applications is the EndpointMiddleware class. This class normally generates all your HTML and JavaScript Object Notation (JSON) responses, and is the focus of most of this book. Like image-resizing middleware, it typically receives a request, generates a response, and then sends it back to the user (figure 4.1).

在大多数 ASP.NET Core 应用程序中,最重要的中间件是类。此类通常会生成所有 HTML 和 JavaScript 对象表示法 (JSON) 响应,并且是本书大部分内容的重点。与图像大小调整中间件一样,它通常接收请求,生成响应,然后将其发送回给用户(图 4.1)。

alt text

Figure 4.1 Example of a middleware pipeline. Each middleware component handles the request and passes it on to the next middleware component in the pipeline. After a middleware component generates a response, it passes the response back through the pipeline. When it reaches the ASP.NET Core web server, the response is sent to the user’s browser.
图 4.1 中间件管道示例。每个中间件组件都处理请求并将其传递给管道中的下一个中间件组件。中间件组件生成响应后,它会通过管道将响应传回。当它到达 ASP.NET Core Web 服务器时,响应将发送到用户的浏览器。

Definition This arrangement—whereby a piece of middleware can call another piece of middleware, which in turn can call another, and so on—is referred to as a pipeline. You can think of each piece of middleware as being like a section of pipe; when you connect all the sections, a request flows through one piece and into the next.
定义 这种安排(即一个中间件可以调用另一个中间件,而另一个中间件又可以调用另一个中间件,依此类推)称为管道。您可以将每个中间件视为一段管道;当您连接所有部分时,请求将流经一个部分并进入下一个部分。

One of the most common use cases for middleware is for the cross-cutting concerns of your application. These aspects of your application need to occur for every request, regardless of the specific path in the request or the resource requested, including

中间件最常见的用例之一是应用程序的横切关注点。无论请求中的具体路径或请求的资源如何,每个请求都需要出现应用程序的这些方面,包括

  • Logging each request
    记录每个请求

  • Adding standard security headers to the response
    向响应添加标准安全标头

  • Associating a request with the relevant user
    将请求与相关用户关联

  • Setting the language for the current request
    设置当前请求的语言

In each of these examples, the middleware receives a request, modifies it, and then passes the request on to the next piece of middleware in the pipeline. Subsequent middleware could use the details added by the earlier middleware to handle the request in some way. In figure 4.2, for example, the authentication middleware associates the request with a user. Then the authorization middleware uses this detail to verify whether the user has permission to make that specific request to the application.

在上述每个示例中,中间件都会接收请求,对其进行修改,然后将请求传递到管道中的下一个中间件。后续中间件可以使用早期中间件添加的详细信息以某种方式处理请求。例如,在图 4.2 中,身份验证中间件将请求与用户相关联。然后,授权中间件使用此详细信息来验证用户是否有权向应用程序发出该特定请求。

alt text

Figure 4.2 Example of a middleware component modifying a request for use later in the pipeline. Middleware can also short-circuit the pipeline, returning a response before the request reaches later middleware.
图 4.2 中间件组件修改请求以供稍后在管道中使用的示例。中间件还可以使管道短路,在请求到达后面的中间件之前返回响应。

If the user has permission, the authorization middleware passes the request on to the endpoint middleware to allow it to generate a response. If the user doesn’t have permission, the authorization middleware can short-circuit the pipeline, generating a response directly; it returns the response to the previous middleware, and the endpoint middleware never sees the request. This scenario is an example of the chain-of-responsibility design pattern.

如果用户具有权限,则授权中间件会将请求传递给终端节点中间件,以允许其生成响应。如果用户没有权限,授权中间件可以使管道短路,直接生成响应;它将响应返回给前面的中间件,而 Endpoint 中间件永远不会看到该请求。此方案是责任链设计模式的一个示例。

Definition When a middleware component short-circuits the pipeline and returns a response, it’s called terminal middleware.
定义 当中间件组件使管道短路并返回响应时,它称为终端中间件。

A key point to glean from this example is that the pipeline is bidirectional. The request passes through the pipeline in one direction until a piece of middleware generates a response, at which point the response passes back through the pipeline, passing through each piece of middleware a second time, in reverse order, until it gets back to the first piece of middleware. Finally, the first/last piece of middleware passes the response back to the ASP.NET Core web server.

从这个例子中可以了解到的一个关键点是管道是双向的。请求沿一个方向通过管道,直到一个中间件生成响应,此时响应通过管道传回,以相反的顺序第二次通过每个中间件,直到它返回到第一个中间件。最后,第一个/最后一个中间件将响应传递回 ASP.NET Core Web 服务器。

The HttpContext object
对象HttpContext
I mentioned the HttpContext in chapter 3, and it’s sitting behind the scenes here too. The ASP.NET Core web server constructs an HttpContext for each request, which the ASP.NET Core application uses as a sort of storage box for a single request. Anything that’s specific to this particular request and the subsequent response can be associated with and stored in it. Examples are properties of the request, request-specific services, data that’s been loaded, or errors that have occurred. The web server fills the initial HttpContext with details of the original HTTP request and other configuration details, and then passes it on to the middleware pipeline and the rest of the application.
我在第 3 章中提到了HttpContext,它也位于幕后。ASP.NET Core Web 服务器为每个请求构建一个,ASP.NET Core 应用程序将其用作单个请求的存储盒。特定于此特定请求和后续响应的任何内容都可以与该请求相关联并存储在其中。示例包括请求的属性、特定于请求的服务、已加载的数据或发生的错误。Web 服务器使用原始 HTTP 请求的详细信息和其他配置详细信息填充初始请求,然后将其传递给中间件管道和应用程序的其余部分。HttpContextHttpContextHttpContext
All middleware has access to the HttpContext for a request. It can use this object to determine whether the request contains any user credentials, to identify which page the request is attempting to access, and to fetch any posted data, for example. Then it can use these details to determine how to handle the request.
所有中间件都可以访问 for a request.例如,它可以使用此对象来确定请求是否包含任何用户凭证,确定请求尝试访问的页面,以及获取任何已发布的数据。然后,它可以使用这些详细信息来确定如何处理请求。
When the application finishes processing the request, it updates the HttpContext with an appropriate response and returns it through the middleware pipeline to the web server. Then the ASP.NET Core web server converts the representation to a raw HTTP response and sends it back to the reverse proxy, which forwards it to the user’s browser.
当应用程序完成对请求的处理后,它会使用适当的响应进行更新,并通过中间件管道将其返回到 Web 服务器。然后,ASP.NET Core Web 服务器将表示形式转换为原始 HTTP 响应,并将其发送回反向代理,反向代理将其转发到用户的浏览器。

As you saw in chapter 3, you define the middleware pipeline in code as part of your initial application configuration in Program.cs. You can tailor the middleware pipeline specifically to your needs; simple apps may need only a short pipeline, whereas large apps with a variety of features may use much more middleware. Middleware is the fundamental source of behavior in your application. Ultimately, the middleware pipeline is responsible for responding to any HTTP requests it receives.

正如您在第 3 章中看到的,您在代码中定义了中间件管道,作为 Program.cs 中初始应用程序配置的一部分。您可以根据您的需求专门定制中间件管道;简单的应用程序可能只需要一个短的管道,而具有各种功能的大型应用程序可能会使用更多的中间件。中间件是应用程序中行为的基本来源。最终,中间件管道负责响应它收到的任何 HTTP 请求。

Requests are passed to the middleware pipeline as HttpContext objects. As you saw in chapter 3, the ASP.NET Core web server builds an HttpContext object from an incoming request, which passes up and down the middleware pipeline. When you’re using existing middleware to build a pipeline, this detail is one that you’ll rarely have to deal with. But as you’ll see in the final section of this chapter, its presence behind the scenes provides a route to exerting extra control over your middleware pipeline.

请求作为对象传递到中间件管道。正如您在第 3 章中所看到的,ASP.NET Core Web 服务器从传入请求构建一个对象,该请求在中间件管道中上下传递。当您使用现有中间件构建管道时,您很少需要处理这些细节。但正如您将在本章的最后一部分中看到的那样,它在幕后的存在为对中间件管道施加额外控制提供了一条途径。

You can also think of your middleware pipeline as being a series of concentric components, similar to a traditional matryoshka (Russian) doll, as shown in figure 4.3. A request progresses through the pipeline by heading deeper into the stack of middleware until a response is returned. Then the response returns through the middleware, passing through the components in reverse order from the request.

您还可以将中间件管道视为一系列同心组件,类似于传统的俄罗斯套娃,如图 4.3 所示。请求通过更深入地进入中间件堆栈在管道中前进,直到返回响应。然后,响应通过中间件返回,以与请求相反的顺序传递组件。

alt text

Figure 4.3 You can also think of middleware as being a series of nested components; a request is sent deeper into the middleware, and the response resurfaces from it. Each middleware component can execute logic before passing the response on to the next middleware component and can execute logic after the response has been created, on the way back out of the stack.
图 4.3 你也可以把 middleware 看作是一系列嵌套的组件;请求被发送到中间件的更深处,响应从中重新出现。每个中间件组件都可以在将响应传递到下一个中间件组件之前执行逻辑,并且可以在创建响应后执行逻辑,在返回堆栈的途中执行逻辑。

Middleware vs. HTTP modules and HTTP handlers
中间件与 HTTP 模块和 HTTP 处理程序
In the previous version of ASP.NET, the concept of a middleware pipeline isn’t used. Instead, you have HTTP modules and HTTP handlers.
在早期版本的 ASP.NET 中,未使用中间件管道的概念。相反,您有 HTTP 模块和 HTTP 处理程序。
An HTTP handler is a process that runs in response to a request and generates the response. The ASP.NET page handler, for example, runs in response to requests for .aspx pages. Alternatively, you could write a custom handler that returns resized images when an image is requested.
HTTP 处理程序是为响应请求而运行并生成响应的进程。例如,ASP.NET 页处理程序在响应对 .aspx 页的请求时运行。或者,您可以编写一个自定义处理程序,在请求图像时返回调整大小的图像。
HTTP modules handle the cross-cutting concerns of applications, such as security, logging, and session management. They run in response to the life-cycle events that a request progresses through when it’s received by the server. Examples of events include BeginRequest, AcquireRequestState, and PostAcquireRequestState.
HTTP 模块处理应用程序的横切关注点,例如安全性、日志记录和会话管理。它们运行以响应服务器收到请求时所经历的生命周期事件。事件的示例包括BeginRequest, AcquireRequestState, 和 PostAcquireRequestState.
This approach works, but sometimes it’s tricky to reason about which modules will run at which points. Implementing a module requires relatively detailed understanding of the state of the request at each individual life-cycle event.
这种方法有效,但有时很难推断哪些模块将在哪些点运行。实现模块需要相对详细地了解每个生命周期事件中的请求状态。
The middleware pipeline makes understanding your application far simpler. The pipeline is defined completely in code, specifying which components should run and in which order. Behind the scenes, the middleware pipeline in ASP.NET Core is simply a chain of method calls, with each middleware function calling the next in the pipeline.
中间件管道使理解您的应用程序变得更加简单。管道完全在代码中定义,指定哪些组件应运行以及运行顺序。在幕后,ASP.NET Core 中的中间件管道只是一个方法调用链,每个中间件函数都调用管道中的下一个。

That’s pretty much all there is to the concept of middleware. In the next section, I’ll discuss ways you can combine middleware components to create an application and how to use middleware to separate the concerns of your application.

这几乎就是中间件概念的全部内容。在下一节中,我将讨论组合中间件组件来创建应用程序的方法,以及如何使用中间件来分离应用程序的关注点。

4.2 Combining middleware in a pipeline

4.2 将中间件组合到一个管道中

Generally speaking, each middleware component has a single primary concern; it handles only one aspect of a request. Logging middleware deals only with logging the request, authentication middleware is concerned only with identifying the current user, and static-file middleware is concerned only with returning static files.

一般来说,每个中间件组件都有一个主要关注点;它只处理请求的一个方面。日志记录中间件只处理记录请求,身份验证中间件只关心识别当前用户,静态文件中间件只关心返回静态文件。

Each of these concerns is highly focused, which makes the components themselves small and easy to reason about. This approach also gives your app added flexibility. Adding static-file middleware, for example, doesn’t mean you’re forced to have image-resizing behavior or authentication; each of these features is an additional piece of middleware.

这些关注点中的每一个都是高度集中的,这使得组件本身很小并且易于推理。此方法还为应用提供了更大的灵活性。例如,添加静态文件中间件并不意味着您被迫具有图像大小调整行为或身份验证;这些功能中的每一个都是中间件的附加部分。

To build a complete application, you compose multiple middleware components into a pipeline, as shown in section 4.1. Each middleware component has access to the original request, as well as any changes made to the HttpContext by middleware earlier in the pipeline. When a response has been generated, each middleware component can inspect and/or modify the response as it passes back through the pipeline before it’s sent to the user. This feature allows you to build complex application behaviors from small, focused components.

要构建一个完整的应用程序,您需要将多个中间件组件组合到一个管道中,如第 4.1 节所示。每个中间件组件都可以访问原始请求,以及管道中较早时对 by 中间件所做的任何更改。生成响应后,每个中间件组件都可以在响应通过管道传回响应之前检查和/或修改响应,然后再将其发送给用户。此功能允许您从小型、专注的组件构建复杂的应用程序行为。

In the rest of this section, you’ll see how to create a middleware pipeline by combining various middleware components. Using standard middleware components, you’ll learn to create a holding page and to serve static files from a folder on disk. Finally, you’ll take a look at a more complex pipeline such as you’d get in a minimal API application with multiple middleware, routing, and endpoints.

在本节的其余部分,您将了解如何通过组合各种中间件组件来创建中间件管道。使用标准中间件组件,您将学习如何创建保留页并从磁盘上的文件夹中提供静态文件。最后,您将了解更复杂的管道,例如在具有多个中间件、路由和终端节点的最小 API 应用程序中获得的管道。

4.2.1 Simple pipeline scenario 1: A holding page

4.2.1 简单管道场景 1:一个保持页

For your first app in this chapter and your first middleware pipeline, you’ll learn how to create an app consisting of a holding page. Adding a holding page can be useful occasionally when you’re setting up your application to ensure that it’s processing requests without errors.

对于本章中的第一个应用程序和第一个中间件管道,您将学习如何创建由保持页面组成的应用程序。在设置应用程序时,添加保留页有时可能很有用,以确保它处理请求时没有错误。

Tip Remember that you can view the application code for this book in the GitHub repository at http://mng.bz/Y1qN.
提示 请记住,您可以在 http://mng.bz/Y1qN 的 GitHub 存储库中查看本书的应用程序代码。

In previous chapters, I mentioned that the ASP.NET Core framework is composed of many small individual libraries. You typically add a piece of middleware by referencing a package in your application’s .csproj project file and configuring the middleware in Program.cs. Microsoft ships many standard middleware components with ASP.NET Core for you to choose among; you can also use third-party components from NuGet and GitHub, or you can build your own custom middleware. You can find the list of built-in middleware at http://mng.bz/Gyxq.

在前面的章节中,我提到了 ASP.NET Core 框架由许多小型的单个库组成。通常,您可以通过引用应用程序的 .csproj 项目文件中的包并在 Program.cs 中配置中间件来添加中间件。Microsoft Core 附带了许多标准中间件组件 ASP.NET 供您选择;您还可以使用 NuGet 和 GitHub 中的第三方组件,也可以构建自己的自定义中间件。您可以在 http://mng.bz/Gyxq 中找到内置中间件的列表。

Note I discuss building custom middleware in chapter 31.
注意 我在第 31 章中讨论了构建自定义中间件。

In this section, you’ll see how to create one of the simplest middleware pipelines, consisting only of WelcomePageMiddleware. WelcomePageMiddleware is designed to provide a sample HTML page quickly when you’re first developing an application, as you can see in figure 4.4. You wouldn’t use it in a production app, as you can’t customize the output, but it’s a single, self-contained middleware component you can use to ensure that your application is running correctly.

在本节中,您将了解如何创建最简单的中间件管道之一,其中仅由WelcomePageMiddleware. WelcomePageMiddleware旨在在您首次开发应用程序时快速提供示例 HTML 页面,如图 4.4 所示。您不会在生产应用程序中使用它,因为您无法自定义输出,但它是一个独立的中间件组件,您可以使用它来确保您的应用程序正确运行。

alt text

Figure 4.4 The Welcome-page middleware response. Every request to the application, at any path, will return the same Welcome-page response.
图 4.4 Welcome-page 中间件响应。在任何路径上对应用程序的每个请求都将返回相同的 Welcome-page 响应。

Tip WelcomePageMiddleware is included as part of the base ASP.NET Core framework, so you don’t need to add a reference to any additional NuGet packages.
提示 WelcomePageMiddleware作为基本 ASP.NET Core 框架的一部分包含在内,因此无需添加对任何其他 NuGet 包的引用。

Even though this application is simple, the same process you’ve seen before occurs when the application receives an HTTP request, as shown in figure 4.5.

尽管此应用程序很简单,但当应用程序收到 HTTP 请求时,您之前看到的相同过程也会发生,如图 4.5 所示。
alt text

Figure 4.5 WelcomePageMiddleware handles a request. The request passes from the reverse proxy to the ASP.NET Core web server and finally to the middleware pipeline, which generates an HTML response.
图 4.5 WelcomePageMiddleware处理请求。请求从反向代理传递到 ASP.NET Core Web 服务器,最后传递到中间件管道,中间件管道会生成 HTML 响应。

The request passes to the ASP.NET Core web server, which builds a representation of the request and passes it to the middleware pipeline. As it’s the first (only!) middleware in the pipeline, WelcomePageMiddleware receives the request and must decide how to handle it. The middleware responds by generating an HTML response, no matter what request it receives. This response passes back to the ASP.NET Core web server, which forwards it to the reverse proxy and then to the user to display in their browser.

请求传递到 ASP.NET Core Web 服务器,该服务器构建请求的表示形式并将其传递给中间件管道。因为它是管道中的第一个(唯一的)中间件,所以WelcomePageMiddleware接收请求并且必须决定如何处理它。中间件通过生成 HTML 响应来响应,无论它收到什么请求。此响应将传递回 ASP.NET Core Web 服务器,该服务器将其转发到反向代理,然后转发给用户以在其浏览器中显示。

As with all ASP.NET Core applications, you define the middleware pipeline in Program.cs by calling Use* methods on the WebApplication instance. To create your first middleware pipeline, which consists of a single middleware component, you need a single method call. The application doesn’t need any extra configuration or services, so your whole application consists of the four lines in the following listing.

与所有 ASP.NET Core 应用程序一样,您可以通过调用实例上的方法在 Program.cs 中定义中间件管道。要创建您的第一个中间件管道(由单个中间件组件组成),您需要一个方法调用。该应用程序不需要任何额外的配置或服务,因此您的整个应用程序由以下清单中的 4 行组成。

Listing 4.1 Program.cs for a Welcome-page middleware pipeline
列表 4.1 Welcome-page 中间件管道的 Program.cs

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);   // ❶
WebApplication app = builder.Build();                                 // ❶
app.UseWelcomePage();                                                 // ❷
app.Run();                                                            // ❸

❶ Uses the default WebApplication configuration
使用默认的 WebApplication 配置
❷ The only custom middleware in the pipeline
管道中唯一的自定义中间件
❸ Runs the application to handle requests
运行应用程序以处理请求

You build up the middleware pipeline in ASP.NET Core by calling methods on WebApplication (which implements IApplicationBuilder). WebApplication doesn’t define methods like UseWelcomePage itself; instead, these are extension methods.

您可以通过调用 WebApplication(实现 IApplicationBuilder)上的方法,在 ASP.NET Core 中构建中间件管道。WebApplication 不定义 UseWelcomePage 本身等方法;相反,这些是扩展方法。

Using extension methods allows you to add functionality to the WebApplication class, while keeping the implementation isolated from it. Under the hood, the methods typically call another extension method to add the middleware to the pipeline. Behind the scenes, for example, the UseWelcomePage method adds the WelcomePageMiddleware to the pipeline by calling
使用扩展方法可以向 WebApplication 类添加功能,同时保持实现与该类隔离。在后台,这些方法通常会调用另一个扩展方法以将中间件添加到管道中。例如,在后台,UseWelcomePage 方法通过调用

UseMiddleware<WelcomePageMiddleware>();

This convention of creating an extension method for each piece of middleware and starting the method name with Use is designed to improve discoverability when you add middleware to your application.[2] ASP.NET Core includes a lot of middleware as part of the core framework, so you can use IntelliSense in Visual Studio and other integrated development environments (IDEs) to view all the middleware that’s available, as shown in figure 4.6.

这种为每个中间件创建一个扩展方法并以 Use 开头的方法名称的约定旨在提高向应用程序添加中间件时的可发现性。2 ASP.NET Core 包含许多中间件作为核心框架的一部分,因此您可以在 Visual Studio 和其他集成开发环境 (IDE) 中使用 IntelliSense 来查看所有可用的中间件,如图 4.6 所示。

alt text

Figure 4.6 IntelliSense makes it easy to view all the available middleware to add to your middleware pipeline.
图 4.6 使用 IntelliSense 可以轻松查看要添加到中间件管道的所有可用中间件。

Calling the UseWelcomePage method adds the WelcomePageMiddleware as the next middleware in the pipeline. Although you’re using only a single middleware component here, it’s important to remember that the order in which you make calls to IApplicationBuilder in Configure defines the order in which the middleware will run in the pipeline.

调用 UseWelcomePage 方法会将 WelcomePageMiddleware 添加为管道中的下一个中间件。尽管您在此处只使用单个中间件组件,但请务必记住,您在 Configure 中调用 IApplicationBuilder 的顺序定义了中间件在管道中运行的顺序。

Warning When you’re adding middleware to the pipeline, always take care to consider the order in which it will run. A component can access only data created by middleware that comes before it in the pipeline.
警告 当您将中间件添加到管道时,请始终注意考虑它的运行顺序。组件只能访问管道中位于其前面的中间件创建的数据。

This application is the most basic kind, returning the same response no matter which URL you navigate to, but it shows how easy it is to define your application behavior with middleware. Next, we’ll make things a little more interesting by returning different responses when you make requests to different paths.

此应用程序是最基本的类型,无论您导航到哪个 URL,它都会返回相同的响应,但它显示了使用中间件定义应用程序行为是多么容易。接下来,当您向不同的路径发出请求时,我们将返回不同的响应,从而使事情变得更有趣。

4.2.2 Simple pipeline scenario 2: Handling static files

4.2.2 简单管道场景 2:处理静态文件

In this section, I’ll show you how to create one of the simplest middleware pipelines you can use for a full application: a static-file application. Most web applications, including those with dynamic content, serve some pages by using static files. Images, JavaScript, and CSS stylesheets are normally saved to disk during development and are served up when requested from the special wwwroot folder of your project, normally as part of a full HTML page request.

在本节中,我将向您展示如何创建可用于完整应用程序的最简单的中间件管道之一:静态文件应用程序。大多数 Web 应用程序(包括具有动态内容的应用程序)都使用静态文件来提供某些页面。图像、JavaScript 和 CSS 样式表通常在开发过程中保存到磁盘中,并在从项目的特殊 wwwroot 文件夹请求时提供,通常作为完整 HTML 页面请求的一部分。

Definition By default, the wwwroot folder is the only folder in your application that ASP.NET Core will serve files from. It doesn’t serve files from other folders for security reasons. The wwwroot folder in an ASP.NET Core project is typically deployed as is to production, including all the files and folders it contains.
定义 默认情况下,wwwroot 文件夹是应用程序中 ASP.NET Core 将从中提供文件的唯一文件夹。出于安全原因,它不会提供来自其他文件夹的文件。ASP.NET Core 项目中的 wwwroot 文件夹通常按原样部署到生产环境,包括它包含的所有文件和文件夹。

You can use StaticFileMiddleware to serve static files from the wwwroot folder when requested, as shown in figure 4.7. In this example, an image called moon.jpg exists in the wwwroot folder. When you request the file using the /moon.jpg path, it’s loaded and returned as the response to the request.
当请求时,您可以使用 wwwroot 文件夹中的静态文件,如图 4.7 所示。在此示例中,名为 moon.jpg 的映像存在于 wwwroot 文件夹中。当您使用路径请求文件StaticFileMiddleware/moon.jpg时,该文件将加载并作为对请求的响应返回。

alt text

Figure 4.7 Serving a static image file using the static-file middleware
图 4.7 使用 static-file 中间件提供静态图像文件

If the user requests a file that doesn’t exist in the wwwroot folder, such as missing.jpg, the static-file middleware won’t serve a file. Instead, a 404 HTTP error code response will be sent to the user’s browser, which displays its default “File Not Found” page, as shown in figure 4.8.

如果用户请求 wwwroot 文件夹中不存在的文件(如 missing.jpg),则静态文件中间件不会提供文件。相反,404 HTTP 错误代码响应将发送到用户的浏览器,该浏览器显示其默认的 “File Not Found” 页面,如图 4.8 所示。

alt text

Figure 4.8 Returning a 404 to the browser when a file doesn’t exist. The requested file didn’t exist in the wwwroot folder, so the ASP.NET Core application returned a 404 response. Then the browser (Microsoft Edge, in this case) show the user a default “File Not Found” error pager
图 4.8 当文件不存在时向浏览器返回 404。请求的文件在 wwwroot 文件夹中不存在,因此 ASP.NET Core 应用程序返回 404 响应。然后,浏览器(在本例中为 Microsoft Edge)向用户显示默认的“找不到文件”错误分页程序

Note How this page looks depends on your browser. In some browsers, you may see a blank page.
注意 此页面的外观取决于您的浏览器。在某些浏览器中,您可能会看到一个空白页面。

Building the middleware pipeline for this simple static-file application is easy. The pipeline consists of a single piece of middleware, StaticFileMiddleware, as you can see in the following listing. You don’t need any services, so configuring the middleware pipeline with UseStaticFiles is all that’s required.

为这个简单的静态文件应用程序构建中间件管道很容易。该管道由一个中间件StaticFileMiddleware 组成,如下面的清单所示。您不需要任何服务,因此使用 UseStaticFiles 配置中间件管道就是全部需要的。

Listing 4.2 Program.cs for a static-file middleware pipeline
列表 4.2 静态文件中间件管道的 Program.cs

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.UseStaticFiles();                                                  // ❶
app.Run();

❶ Adds the StaticFileMiddleware to the pipeline
将 StaticFileMiddleware 添加到管道中

Tip Remember that you can view the application code for this book in the GitHub repository at http://mng.bz/Y1qN.
提示 记住,您可以在 http://mng.bz/Y1qN 的 GitHub 存储库中查看本书的应用程序代码。

When the application receives a request, the ASP.NET Core web server handles it and passes it to the middleware pipeline. StaticFileMiddleware receives the request and determines whether it can handle it. If the requested file exists, the middleware handles the request and returns the file as the response, as shown in figure 4.9.

当应用程序收到请求时,ASP.NET Core Web 服务器会处理该请求并将其传递给中间件管道。StaticFileMiddleware 接收请求并确定它是否可以处理它。如果请求的文件存在,中间件会处理请求并返回该文件作为响应,如图 4.9 所示。

alt text

Figure 4.9 StaticFileMiddleware handles a request for a file. The middleware checks the wwwroot folder to see if whether requested moon.jpg file exists. The file exists, so the middleware retrieves it and returns it as the response to the web server and, ultimately, to the browser.
图 4.9 StaticFileMiddleware 处理文件请求。中间件检查 wwwroot 文件夹以查看请求的 moon.jpg 文件是否存在。该文件存在,因此中间件会检索它并将其作为响应返回给 Web 服务器,并最终返回给浏览器。

If the file doesn’t exist, the request effectively passes through the static-file middleware unchanged. But wait—you added only one piece of middleware, right? Surely you can’t pass the request through to the next middleware component if there isn’t another one.

如果文件不存在,则请求将原封不动地有效地通过 static-file 中间件。但是等等 — 您只添加了一个中间件,对吧?当然,如果没有另一个中间件组件,你就不能将请求传递给下一个中间件组件。

ASP.NET Core automatically adds a dummy piece of middleware to the end of the pipeline. This middleware always returns a 404 response if it’s called.

ASP.NET Core 会自动将一个虚拟中间件添加到管道的末尾。如果调用此中间件,则始终返回 404 响应。

Tip If no middleware generates a response for a request, the pipeline automatically returns a simple 404 error response to the browser.
提示 如果没有中间件为请求生成响应,则管道会自动向浏览器返回简单的 404 错误响应。

HTTP response status codes
HTTP 响应状态代码
Every HTTP response contains a status code and, optionally, a reason phrase describing the status code. Status codes are fundamental to the HTTP protocol and are a standardized way of indicating common results. A 200 response, for example, means that the request was successfully answered, whereas a 404 response indicates that the resource requested couldn’t be found. You can see the full list of standardized status codes at https://www.rfc-editor.org/rfc/rfc9110#name-status-codes.
每个 HTTP 响应都包含一个状态代码和一个描述状态代码的原因短语(可选)。状态代码是 HTTP 协议的基础,是表示常见结果的标准化方式。例如,200 响应表示请求已成功响应,而 404 响应表示找不到请求的资源。您可以在 https://www.rfc-editor.org/rfc/rfc9110#name-status-codes 上查看标准化状态代码的完整列表。
Status codes are always three digits long and are grouped in five classes, based on the first digit:
状态代码始终为三位数,并根据第一位数字分为五类:
· 1xx—Information. This code is not often used; it provides a general acknowledgment.
1xx - 信息。此代码不经常使用;它提供了一个一般性的确认。
· 2xx—Success. The request was successfully handled and processed.
2xx - 成功。已成功处理和处理请求。
· 3xx—Redirection. The browser must follow the provided link to allow the user to log in, for example.
3xx — 重定向。例如,浏览器必须按照提供的链接允许用户登录。
· 4xx—Client error. A problem occurred with the request. The request sent invalid data, for example, or the user isn’t authorized to perform the request.
4xx — 客户端错误。请求出现问题。例如,请求发送了无效数据,或者用户无权执行请求。
· 5xx—Server error. A problem on the server caused the request to fail.
5xx — 服务器错误。服务器上的问题导致请求失败。
These status codes typically drive the behavior of a user’s browser. The browser will handle a 301 response automatically, for example, by redirecting to the provided new link and making a second request, all without the user’s interaction.
这些状态代码通常驱动用户浏览器的行为。浏览器将自动处理 301 响应,例如,重定向到提供的新链接并发出第二个请求,所有这些都无需用户交互。
Error codes are in the 4xx and 5xx classes. Common codes include a 404 response when a file couldn’t be found, a 400 error when a client sends invalid data (such as an invalid email address), and a 500 error when an error occurs on the server. HTTP responses for error codes may include a response body, which is content to display when the client receives the response.
错误代码位于 4xx 和 5xx 类中。常见代码包括找不到文件时的 404 响应、客户端发送无效数据(例如无效的电子邮件地址)时的 400 错误,以及服务器上发生错误时的 500 错误。错误代码的 HTTP 响应可能包括响应正文,该正文是客户端收到响应时要显示的内容。

This basic ASP.NET Core application makes it easy to see the behavior of the ASP.NET Core middleware pipeline and the static-file middleware in particular, but it’s unlikely that your applications will be this simple. It’s more likely that static files will form one part of your middleware pipeline. In the next section you’ll see how to combine multiple middleware components as we look at a simple minimal API application.

这个基本的 ASP.NET Core 应用程序可以轻松查看 ASP.NET Core 中间件管道的行为,尤其是静态文件中间件,但您的应用程序不太可能如此简单。静态文件更有可能构成中间件管道的一部分。在下一节中,您将看到如何组合多个中间件组件,因为我们将了解一个简单的最小 API 应用程序。

4.2.3 Simple pipeline scenario 3: A minimal API application

4.2.3 简单管道场景 3:一个最小的 API 应用程序

By this point, you should have a decent grasp of the middleware pipeline, insofar as you understand that it defines your application’s behavior. In this section you’ll see how to combine several standard middleware components to form a pipeline. As before, you do this in Program.cs by adding middleware to the WebApplication object.

此时,您应该对中间件管道有一定的了解,只要您了解它定义了应用程序的行为。在本节中,您将了解如何组合多个标准中间件组件以形成一个管道。和以前一样,您可以通过将中间件添加到 WebApplication 对象来在 Program.cs 中执行此作。

You’ll begin by creating a basic middleware pipeline that you’d find in a typical ASP.NET Core minimal APIs template and then extend it by adding middleware. Figure 4.10 shows the output you see when you navigate to the home page of the application—identical to the sample application in chapter 3.

首先,您将创建一个基本的中间件管道,该管道可以在典型的 ASP.NET Core 最小 API 模板中找到,然后通过添加中间件来扩展它。图 4.10 显示了导航到应用程序主页时看到的输出,与第 3 章中的示例应用程序相同。

alt text

Figure 4.10 A simple minimal API application. The application uses only four pieces of middleware: routing middleware to choose the endpoint to run, endpoint middleware to generate the response from a Razor Page, static-file middleware to serve image files, and exception-handler middleware to capture any errors.
图 4.10 一个简单的最小 API 应用程序。应用程序只使用四个中间件:路由中间件来选择要运行的端点,endpoint 中间件(用于从 Razor 页面生成响应)、静态文件中间件(用于提供图像文件)和异常处理程序中间件(用于捕获任何错误)。

Creating this application requires only four pieces of middleware: routing middleware to choose a minimal API endpoint to execute, endpoint middleware to generate the response, static-file middleware to serve any image files from the wwwroot folder, and exception-handler middleware to handle any errors that might occur. Even though this example is still a Hello World! example, this architecture is much closer to a realistic example. The following listing shows an example of such an application.

创建此应用程序只需要四个中间件:用于选择要执行的最小 API 终端节点的路由中间件、用于生成响应的终端节点中间件、用于提供 wwwroot 文件夹中任何图像文件的静态文件中间件,以及用于处理可能发生的任何错误的异常处理程序中间件。即使此示例仍然是 Hello World! example,此体系结构更接近于实际示例。下面的清单显示了此类应用程序的一个示例。

Listing 4.3 A basic middleware pipeline for a minimal APIs application
清单 4.3 用于最小 API 应用程序的基本中间件管道

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseDeveloperExceptionPage();         // ❶
app.UseStaticFiles();                    // ❷
app.UseRouting();                        // ❸
app.MapGet("/", () => "Hello World!");   // ❹

app.Run();

❶ This call isn’t strictly necessary, as it’s already added by WebApplication by default.
此调用并非绝对必要,因为默认情况下它已由 WebApplication 添加。

❷ Adds the StaticFileMiddleware to the pipeline
将 StaticFileMiddleware 添加到管道中

❸ Adds the RoutingMiddleware to the pipeline
将 RoutingMiddleware 添加到管道中

❹ Defines an endpoint for the application
为应用程序定义端点

The addition of middleware to WebApplication to form the pipeline should be familiar to you now, but several points are worth noting in this example:
您现在应该很熟悉向 WebApplication 添加中间件以形成管道,但在此示例中,有几点值得注意:

  • Middleware is added with Use() methods.
    中间件是使用 Use
    () 方法添加的。
  • MapGet defines an endpoint, not middleware. It defines the endpoints that the routing and endpoint middleware can use.
    MapGet 定义端点,而不是中间件。它定义路由和终端节点中间件可以使用的终端节点。
  • WebApplication automatically adds some middleware to the pipeline, such as the EndpointMiddleware.
    WebApplication 会自动向管道中添加一些中间件,例如 EndpointMiddleware。
  • The order of the Use() method calls is important and defines the order of the middleware pipeline.
    Use
    () 方法调用的顺序很重要,它定义了中间件管道的顺序。

First, all the methods for adding middleware start with Use. As I mentioned earlier, this is thanks to the convention of using extension methods to extend the functionality of WebApplication; prefixing the methods with Use should make them easier to discover.

首先,添加中间件的所有方法都以 Use 开头。正如我前面提到的,这要归功于使用扩展方法来扩展 WebApplication 功能的约定;为方法添加前缀 Use 应该会使它们更容易被发现。

Second, it’s important to understand that the MapGet method does not add middleware to the pipeline; it defines an endpoint in your application. These endpoints are used by the routing and endpoint middleware. You’ll learn more about endpoints and routing in chapter 5.

其次,请务必了解 MapGet 方法不会将中间件添加到管道中;它定义应用程序中的终端节点。这些终端节点由路由和终端节点中间件使用。您将在第 5 章中了解有关终端节点和路由的更多信息。

Tip You can define the endpoints for your app by using MapGet() anywhere in Program.cs before the call to app.Run(), but the calls are typically placed after the middleware pipeline definition.
提示 您可以使用MapGet() Program.cs在调用应用程序。Run(),但调用通常放在中间件管道定义之后。

In chapter 3, I mentioned that WebApplication automatically adds middleware to your app. You can see this process in action in listing 4.3 automatically adding the EndpointMiddleware to the end of the middleware pipeline. WebApplication also automatically adds the developer exception page middleware to the start of the middleware pipeline when you’re running in development. As a result, you can omit the call to UseDeveloperExceptionPage() from listing 4.3, and your middleware pipeline will be essentially the same.

在第 3 章中,我提到了 WebApplication 会自动将中间件添加到您的应用程序中。你可以在清单 4.3 中看到这个过程的实际效果,它自动将 EndpointMiddleware 添加到中间件管道的末尾。当您在开发中运行时,WebApplication 还会自动将开发人员异常页面中间件添加到中间件管道的开头。因此,您可以省略清单 4.3 中对 UseDeveloperExceptionPage() 的调用,您的中间件管道将基本相同。

WebApplication and autoadded middleware
WebApplication 和自动添加的中间件
WebApplication and WebApplicationBuilder were introduced in .NET 6 to try to reduce the amount of boilerplate code required for a Hello World! ASP.NET Core application. As part of this initiative, Microsoft chose to have WebApplication automatically add various middleware to the pipeline. This decision alleviates some of the common getting-started pain points of middleware ordering by ensuring that, for example, UseRouting() is always called before UseAuthorization().
WebApplication 和 WebApplicationBuilder 是在 .NET 6 中引入的,旨在尝试减少 "Hello World" ASP.NET Core 应用程序所需的样板代码量!。作为该计划的一部分,Microsoft 选择让 WebApplication 自动将各种中间件添加到管道中。此决定通过确保(例如)始终在 UseAuthorization() 之前调用 UseRouting(),缓解了中间件排序的一些常见入门痛点。
Everything has trade-offs, of course, and for WebApplication the trade-off is that it’s harder to understand exactly what’s in your middleware pipeline without having deep knowledge of the framework code itself.
当然,一切都有权衡,对于 WebApplication 来说,权衡是,如果不深入了解框架代码本身,就更难准确理解中间件管道中的内容。
Luckily, you don’t need to worry about the middleware that WebApplication adds for the most part. If you’re new to ASP.NET Core, generally you can accept that WebApplication will add the middleware only when it’s necessary and safe to do so.
幸运的是,您无需担心WebApplication 在很大程度上增加了。如果您是 ASP.NET Core 的新用户,通常,你可以接受 WebApplication 仅在必要且安全的情况下才会添加中间件。
Nevertheless, in some cases it may pay to know exactly what’s in your pipeline, especially if you’re familiar with ASP.NET Core. In .NET 7, WebApplication automatically adds some or all of the following middleware to the start of the middleware pipeline:
不过,在某些情况下,确切地了解你的管道中的内容可能是值得的,特别是如果你熟悉 ASP.NET Core。在 .NET 7 中,WebApplication 会自动将以下部分或全部中间件添加到中间件管道的开头:
· HostFilteringMiddleware—This middleware is security-related. You can read more about why it’s useful and how to configure it at http://mng.bz/zXxa.
HostFilteringMiddleware — 此中间件与安全性相关。您可以在 http://mng.bz/zXxa 中阅读有关它为何有用以及如何配置它的更多信息。
· ForwardedHeadersMiddleware—This middleware controls how forwarded headers are handled. You can read more about it in chapter 27.
ForwardedHeadersMiddleware — 此中间件控制如何处理转发的标头。您可以在第 27 章中阅读更多相关信息。
· DeveloperExceptionPageMiddleware—As already discussed, this middleware is added when you run in a development environment.
DeveloperExceptionPageMiddleware — 如前所述,当您在开发环境中运行时,会添加此中间件。
· RoutingMiddleware—If you add any endpoints to your application, UseRouting() runs before you add any custom middleware to your application.
RoutingMiddleware — 如果您向应用程序添加任何终端节点,则 UseRouting() 会在您向应用程序添加任何自定义中间件之前运行。
· AuthenticationMiddleware—If you configure authentication, this middleware authenticates a user for the request. Chapter 23 discusses authentication in detail.
AuthenticationMiddleware — 如果您配置身份验证,则此中间件将对请求的用户进行身份验证。第 23 章详细讨论了身份验证。
· AuthorizationMiddleware—The authorization middleware runs after authentication and determines whether a user is permitted to execute an endpoint. If the user doesn’t have permission, the request is short-circuited. I discuss authorization in detail in chapter 24.
AuthorizationMiddleware — 授权中间件在身份验证后运行,并确定是否允许用户执行终端节点。如果用户没有权限,则请求将短路。我在第 24 章中详细讨论了授权。
· EndpointMiddleware—This middleware pairs with the RoutingMiddleware to execute an endpoint. Unlike the other middleware described here, the EndpointMiddleware is added to the end of the middleware pipeline, after any other middleware you configure in Program.cs.
EndpointMiddleware — 此中间件与 RoutingMiddleware 配对以执行终端节点。与此处描述的其他中间件不同,EndpointMiddleware 被添加到中间件管道的末尾,位于您在 Program.cs 中配置的任何其他中间件之后。
Depending on your Program.cs configuration, WebApplication may not add all this middleware. Also, if you don’t want some of this automatic middleware to be at the start of your middleware pipeline, generally you can override the location. In listing 4.3, for example, we override the automatic RoutingMiddleware location by calling UseRouting() explicitly, ensuring that routing occurs exactly where we need it.
根据您的 Program.cs 配置,WebApplication 可能不会添加所有这些中间件。此外,如果您不希望某些自动中间件位于中间件管道的开头,通常可以覆盖该位置。例如,在清单 4.3 中,我们通过显式调用 UseRouting()来覆盖自动定位RoutingMiddleware,确保路由恰好发生在我们需要的地方。

Another important point about listing 4.3 is that the order in which you add the middleware to the WebApplication object is the order in which the middleware is added to the pipeline. The order of the calls in listing 4.3 creates a pipeline similar to that shown in figure 4.11.

清单 4.3 的另一个要点是,将中间件添加到 WebApplication 对象的顺序就是将中间件添加到管道的顺序。清单 4.3 中的调用顺序将创建一个类似于图 4.11 所示的管道。

alt text

Figure 4.11 The middleware pipeline for the example application in listing 4.3. The order in which you add the middleware to WebApplication defines the order of the middleware in the pipeline.
图 4.11 清单 4.3 中示例应用程序的中间件管道。将中间件添加到 WebApplication 的顺序定义了中间件在管道中的顺序。

The ASP.NET Core web server passes the incoming request to the developer exception page middleware first. This exception-handler middleware ignores the request initially; its purpose is to catch any exceptions thrown by later middleware in the pipeline, as you’ll see in section 4.3. It’s important for this middleware to be placed early in the pipeline so that it can catch errors produced by later middleware.

ASP.NET Core Web 服务器首先将传入请求传递给开发人员异常页面中间件。此异常处理程序中间件最初会忽略请求;它的目的是捕获管道中后续 middleware 抛出的任何异常,如 4.3 节所示。将此中间件放在管道的早期非常重要,这样它就可以捕获后续中间件产生的错误。

The developer exception page middleware passes the request on to the static-file middleware. The static-file handler generates a response if the request corresponds to a file; otherwise, it passes the request on to the routing middleware. The routing middleware selects a minimal API endpoint based on the endpoints defined and the request URL, and the endpoint middleware executes the selected minimal API endpoint. If no endpoint can handle the requested URL, the automatic dummy middleware returns a 404 response.

开发人员异常页面中间件将请求传递给静态文件中间件。如果请求对应于文件,则 static-file 处理程序会生成响应;否则,它将请求传递给路由中间件。路由中间件根据定义的端点和请求 URL 选择最小 API 端点,端点中间件执行选定的最小 API 端点。如果没有终端节点可以处理请求的 URL,则自动虚拟中间件将返回 404 响应。

In chapter 3, I mentioned that WebApplication adds the RoutingMiddleware to the start of the middleware pipeline automatically. So you may be wondering why I explicitly added it to the pipeline in listing 4.3 using UseRouting().

在第 3 章中,我提到 WebApplication 会自动将 RoutingMiddleware 添加到中间件管道的开头。因此,您可能想知道为什么我使用 UseRouting() 将它显式添加到清单 4.3 中的管道中。

The answer, again, is related to the order of the middleware. Adding an explicit call to UseRouting() tells WebApplication not to add the RoutingMiddleware automatically before the middleware defined in Program.cs. This allows us to “move” the RoutingMiddleware to be placed after the StaticFileMiddleware. Although this step isn’t strictly necessary in this case, it’s good practice. The StaticFileMiddleware doesn’t use routing, so it’s preferable to let this middleware check whether the incoming request is for a static file; if so, it can short-circuit the pipeline and avoid the unnecessary call to the RoutingMiddleware.

答案同样与中间件的顺序有关。添加对 UseRouting() 的显式调用会告诉 WebApplication 不要在 Program.cs 中定义的中间件之前自动添加 RoutingMiddleware。这允许我们将 RoutingMiddleware “移动” 为放置在 StaticFileMiddleware 之后。尽管在这种情况下,此步骤并非绝对必要,但这是一种很好的做法。StaticFileMiddleware 不使用路由,因此最好让这个中间件检查传入的请求是否是针对静态文件的;如果是这样,它可以使管道短路并避免对 RoutingMiddleware 的不必要调用。

Note In versions 1.x and 2.x of ASP.NET Core, the routing and endpoint middleware were combined in a single Model-View-Controller (MVC) middleware component. Splitting the responsibilities for routing from execution makes it possible to insert middleware between the routing and endpoint middleware. I discuss routing further in chapters 6 and 14.
注意 在 ASP.NET Core 的 1.x 和 2.x 版本中,路由和端点中间件组合在一个模型-视图-控制器 (MVC) 中间件组件中。将路由的责任与执行分开,可以在路由和终端节点中间件之间插入中间件。我在第 6 章和第 14 章中进一步讨论了路由。

The impact of ordering is most obvious when you have two pieces of middleware that are listening for the same path. The endpoint middleware in the example pipeline currently responds to a request to the home page of the application (with the / path) by returning the string "Hello World!", as shown in figure 4.10. Figure 4.12 shows what happens if you reintroduce a piece of middleware that you saw previously, WelcomePageMiddleware, and configure it to respond to the / path as well.

当你有两个 middleware 正在侦听同一路径时,排序的影响最为明显。示例管道中的终端节点中间件当前通过返回字符串 “Hello World!” 来响应对应用程序主页(使用 / 路径)的请求,如图 4.10 所示。图 4.12 显示了如果你重新引入之前看到的中间件 WelcomePageMiddleware,并将其配置为也响应 / 路径会发生什么。

alt text

Figure 4.12 The Welcome-page middleware response. The Welcome-page middleware comes before the endpoint middleware, so a request to the home page returns the Welcome-page middleware instead of the minimal API response.
图 4.12 Welcome-page 中间件响应。Welcome-page 中间件位于端点中间件之前,因此对主页的请求将返回 Welcome-page 中间件,而不是最小的 API 响应。

As you saw in section 4.2.1, WelcomePageMiddleware is designed to return a fixed HTML response, so you wouldn’t use it in a production app, but it illustrates the point nicely. In the following listing, it’s added to the start of the middleware pipeline and configured to respond only to the "/" path.

正如你在 4.2.1 节中看到的,WelcomePageMiddleware 被设计为返回一个固定的 HTML 响应,所以你不会在生产应用程序中使用它,但它很好地说明了这一点。在下面的清单中,它被添加到中间件管道的开头,并配置为仅响应 “/” 路径。

Listing 4.4 Adding WelcomePageMiddleware to the pipeline
清单 4.4 添加到管道WelcomePageMiddleware

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.UseWelcomePage("/");                                 //❶
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseRouting();                                        //❷
app.MapGet("/", () => "Hello World!");                   //❷
app.Run();

❶ WelcomePageMiddleware handles all requests to the “/” path and returns a sample HTML response.
WelcomePageMiddleware 处理对 “/” 路径的所有请求,并返回一个示例 HTML 响应。
❷ Requests to “/” will never reach the endpoint middleware, so this endpoint won’t be called.
对 “/” 的请求永远不会到达端点中间件,因此不会调用此端点。

Even though you know that the endpoint middleware can also handle the "/" path, WelcomePageMiddleware is earlier in the pipeline, so it returns a response when it receives the request to "/", short-circuiting the pipeline, as shown in figure 4.13. None of the other middleware in the pipeline runs for the request, so none has an opportunity to generate a response.
即使您知道端点中间件也可以处理 “/” 路径,但 WelcomePageMiddleware 位于管道的早期,因此当它收到对 “/” 的请求时,它会返回响应,从而使管道短路,如图 4.13 所示。管道中的其他中间件都不会为请求运行,因此没有机会生成响应。

alt text

Figure 4.13 Overview of the application handling a request to the "/" path. The Welcome-page middleware is first in the middleware pipeline, so it receives the request before any other middleware. It generates an HTML response, short-circuiting the pipeline. No other middleware runs for the request.
图 4.13 处理对 “/” 路径请求的应用程序概述。欢迎页面middleware 在中间件管道中排在第一位,因此它会在任何其他中间件之前收到请求。它生成 HTML 响应,使管道短路。没有其他中间件为请求运行。

As WebApplication automatically adds EndpointMiddleware to the end of the middleware pipeline, the WelcomePageMiddleware will always be ahead of it, so it always generates a response before the endpoint can execute in this example.

由于 WebApplication 会自动将 EndpointMiddleware 添加到中间件管道的末尾,因此 WelcomePageMiddleware 将始终位于其前面,因此在此示例中,它总是在端点可以执行之前生成响应。

Tip You should always consider the order of middleware when adding it to WebApplication. Middleware added earlier in the pipeline will run (and potentially return a response) before middleware added later.
提示 在将中间件添加到 WebApplication 时,您应该始终考虑中间件的顺序。在管道中较早添加的中间件将在稍后添加的中间件之前运行 (并可能返回响应) 。

All the examples shown so far try to handle an incoming request and generate a response, but it’s important to remember that the middleware pipeline is bidirectional. Each middleware component gets an opportunity to handle both the incoming request and the outgoing response. The order of middleware is most important for those components that create or modify the outgoing response.

到目前为止显示的所有示例都尝试处理传入请求并生成响应,但请务必记住,中间件管道是双向的。每个中间件组件都有机会同时处理传入请求和传出响应。中间件的顺序对于创建或修改传出响应的组件最为重要。

In listing 4.3, I included DeveloperExceptionPageMiddleware at the start of the application’s middleware pipeline, but it didn’t seem to do anything. Error-handling middleware characteristically ignores the incoming request as it arrives in the pipeline; instead, it inspects the outgoing response, modifying it only when an error has occurred. In the next section, I discuss the types of error-handling middleware that are available to use with your application and when to use them.

在清单 4.3 中,我在应用程序的中间件管道的开头添加了 DeveloperExceptionPageMiddleware,但它似乎没有做任何事情。错误处理中间件通常在传入请求到达管道时忽略该请求;相反,它会检查传出响应,仅在发生错误时对其进行修改。在下一节中,我将讨论可用于应用程序的错误处理中间件的类型以及何时使用它们。

4.3 Handling errors using middleware

4.3 使用中间件处理错误

Errors are a fact of life when you’re developing applications. Even if you write perfect code, as soon as you release and deploy your application, users will find a way to break it, by accident or intentionally! The important thing is that your application handles these errors gracefully, providing a suitable response to the user and not causing your whole application to fail.

在开发应用程序时,错误是不可避免的事实。即使您编写了完美的代码,一旦您发布和部署了您的应用程序,用户也会找到一种方法来破坏它,无论是有意还是有意!重要的是,您的应用程序要妥善处理这些错误,为用户提供适当的响应,并且不会导致整个应用程序失败。

The design philosophy for ASP.NET Core is that every feature is opt-in. So because error handling is a feature, you need to enable it explicitly in your application. Many types of errors could occur in your application, and you have many ways to handle them, but in this section I focus on a single type of error: exceptions.

ASP.NET Core 的设计理念是每个功能都是可选的。因此,由于错误处理是一项功能,因此您需要在应用程序中显式启用它。您的应用程序中可能会发生许多类型的错误,您有多种方法可以处理这些错误,但在本节中,我将重点介绍一种类型的错误:异常。

Exceptions typically occur whenever you find an unexpected circumstance. A typical (and highly frustrating) exception you’ll no doubt have experienced before is NullReferenceException, which is thrown when you attempt to access a variable that hasn’t been initialized.[3] If an exception occurs in a middleware component, it propagates up the pipeline, as shown in figure 4.14. If the pipeline doesn’t handle the exception, the web server returns a 500 status code to the user.

每当发现意外情况时,通常会发生异常。您以前无疑会遇到一个典型的(且非常令人沮丧的)异常是 NullReferenceException,当您尝试访问尚未初始化的变量时,会引发该异常。3 如果中间件组件中发生异常,它会沿管道向上传播,如图 4.14 所示。如果管道未处理异常,则 Web 服务器将向用户返回 500 状态代码。

alt text

Figure 4.14 An exception in the endpoint middleware propagates through the pipeline. If the exception isn’t caught by middleware earlier in the pipeline, a 500 “Server error” status code is sent to the user’s browser.
图 4.14 端点中间件中的异常通过管道传播。如果管道中较早的中间件未捕获异常,则会向用户的浏览器发送 500 的“服务器错误”状态代码。

In some situations, an error won’t cause an exception. Instead, middleware might generate an error status code. One such case occurs when a requested path isn’t handled. In that situation, the pipeline returns a 404 error.
在某些情况下,错误不会导致异常。相反,中间件可能会生成错误状态代码。当请求的路径未得到处理时,就会出现一种情况。在这种情况下,管道将返回 404 错误。

For APIs, which typically are consumed by apps (as opposed to end users), that result probably is fine. But for apps that typically generate HTML, such as Razor Pages apps, returning a 404 typically results in a generic, unfriendly page being shown to the user, as you saw in figure 4.8. Although this behavior is correct, it doesn’t provide a great experience for users of these types of applications.

对于通常由应用程序 (而不是最终用户) 使用的 API,该结果可能很好。但对于通常生成 HTML 的应用程序(如 Razor Pages 应用程序),返回 404 通常会导致向用户显示通用的、不友好的页面,如图 4.8 所示。尽管此行为是正确的,但它不会为这些类型应用程序的用户提供出色的体验。

Error-handling middleware attempts to address these problems by modifying the response before the app returns it to the user. Typically, error-handling middleware returns either details on the error that occurred or a generic but friendly HTML page to the user. You’ll learn how to handle this use case in chapter 13 when you learn about generating responses with Razor Pages.

错误处理中间件尝试通过在应用程序将响应返回给用户之前修改响应来解决这些问题。通常,错误处理中间件会返回有关所发生错误的详细信息,或者向用户返回通用但友好的 HTML 页面。在第 13 章中,您将了解如何使用 Razor Pages 生成响应,从而了解如何处理此用例。

The remainder of this section looks at the two main types of exception-handling middleware that’s available for use in your application. Both are available as part of the base ASP.NET Core framework, so you don’t need to reference any additional NuGet packages to use them.

本节的其余部分将介绍可在应用程序中使用的两种主要类型的异常处理中间件。两者都作为基本 ASP.NET Core 框架的一部分提供,因此无需引用任何其他 NuGet 包即可使用它们。

4.3.1 Viewing exceptions in development: DeveloperExceptionPage

查看开发中的异常:DeveloperExceptionPage

When you’re developing an application, you typically want access to as much information as possible when an error occurs somewhere in your app. For that reason, Microsoft provides DeveloperExceptionPageMiddleware, which you can add to your middleware pipeline by using
在开发应用程序时,您通常希望在应用程序中的某个位置发生错误时访问尽可能多的信息。因此,Microsoft 提供了 DeveloperExceptionPageMiddleware,您可以使用

app.UseDeveloperExceptionPage();

Note As shown previously, WebApplication automatically adds this middleware to your middleware pipeline when you’re running in the Development environment, so you don’t need to add it explicitly. You’ll learn more about environments in chapter 10.
注意 如前所述,当您在 Development 环境中运行时,WebApplication 会自动将此中间件添加到您的中间件管道中,因此您无需显式添加它。您将在第 10 章中了解有关环境的更多信息。

When an exception is thrown and propagates up the pipeline to this middleware, it’s captured. Then the middleware generates a friendly HTML page, which it returns with a 500 status code, as shown in figure 4.15. This page contains a variety of details about the request and the exception, including the exception stack trace; the source code at the line the exception occurred; and details on the request, such as any cookies or headers that were sent.

当引发异常并沿管道向上传播到此中间件时,会捕获该异常。然后,中间件生成一个友好的 HTML 页面,该页面返回一个 500 状态代码,如图 4.15 所示。此页面包含有关请求和异常的各种详细信息,包括异常堆栈跟踪;发生异常的行处的源代码;以及有关请求的详细信息,例如发送的任何 Cookie 或标头。

alt text

Figure 4.15 The developer exception page shows details about the exception when it occurs during the process of a request. The location in the code that caused the exception, the source code line itself, and the stack trace are all shown by default. You can also click the Query, Cookies, Headers, and Routing buttons to reveal further details about the request that caused the exception.
图 4.15 开发者异常页面展示请求过程中发生的异常详情。默认情况下,将显示代码中导致异常的位置、源代码行本身和堆栈跟踪。您还可以单击 Query、Cookie、Headers 和 Routing 按钮,以显示有关导致异常的请求的更多详细信息。

Having these details available when an error occurs is invaluable for debugging a problem, but they also represent a security risk if used incorrectly. You should never return more details about your application to users than absolutely necessary, so you should use DeveloperExceptionPage only when developing your application. The clue is in the name!

在发生错误时提供这些详细信息对于调试问题非常有价值,但如果使用不当,它们也会带来安全风险。您绝不应向用户返回有关应用程序的更多详细信息,因此您只应在开发应用程序时使用 DeveloperExceptionPage。线索就在名字里!

Warning Never use the developer exception page when running in production. Doing so is a security risk, as it could publicly reveal details about your application’s code, making you an easy target for attackers. WebApplication uses the correct behavior by default and adds the middleware only when running in development.
警告 在生产环境中运行时,切勿使用开发人员异常页面。这样做会带来安全风险,因为它可能会公开泄露有关应用程序代码的详细信息,使您很容易成为攻击者的目标。默认情况下,WebApplication 使用正确的行为,并且仅在开发中运行时添加中间件。

If the developer exception page isn’t appropriate for production use, what should you use instead? Luckily, you can use another type of general-purpose error-handling middleware in production: ExceptionHandlerMiddleware.

如果开发人员例外页面不适合生产使用,您应该使用什么?幸运的是,您可以在生产中使用另一种类型的通用错误处理中间件:ExceptionHandlerMiddleware。

4.3.2 Handling exceptions in production: ExceptionHandlerMiddleware

4.3.2 在生产环境中处理异常:ExceptionHandlerMiddleware

The developer exception page is handy when you’re developing your applications, but you shouldn’t use it in production, as it can leak information about your app to potential attackers. You still want to catch errors, though; otherwise, users will see unfriendly error pages or blank pages, depending on the browser they’re using.
在开发应用程序时,开发人员异常页面很方便,但不应在生产环境中使用它,因为它可能会将有关应用程序的信息泄露给潜在的攻击者。不过,您仍然希望捕获错误;否则,用户将看到不友好的错误页面或空白页面,具体取决于他们使用的浏览器。

You can solve this problem by using ExceptionHandlerMiddleware. If an error occurs in your application, the user will see a custom error response that’s consistent with the rest of the application but provides only necessary details about the error. For a minimal API application, that response could be JSON or plain text, as shown in figure 4.16.

您可以使用 ExceptionHandlerMiddleware 来解决此问题。如果您的应用程序中发生错误,用户将看到一个自定义错误响应,该响应与应用程序的其余部分一致,但仅提供有关错误的必要详细信息。对于最小的 API 应用程序,该响应可以是 JSON 或纯文本,如图 4.16 所示。

alt text

Figure 4.16 Using the ExceptionHandlerMiddleware, you can return a generic error message when an exception occurs, ensuring that you don’t leak any sensitive details about your application in production.
图 4.16 使用 ExceptionHandlerMiddleware,您可以在发生异常时返回通用错误消息,从而确保在生产环境中不会泄露有关应用程序的任何敏感详细信息。

For Razor Pages apps, you can create a custom error response, such as the one shown in figure 4.17. You maintain the look and feel of the application by using the same header, displaying the currently logged-in user, and displaying an appropriate message to the user instead of full details on the exception.

对于 Razor Pages 应用程序,您可以创建自定义错误响应,如图 4.17 所示。通过使用相同的标头,显示当前登录的用户,并向用户显示适当的消息,而不是有关异常的完整详细信息,可以维护应用程序的外观。

alt text

Figure 4.17 A custom error page created by ExceptionHandlerMiddleware. The custom error page can have the same look and feel as the rest of the application by reusing elements such as the header and footer. More important, you can easily control the error details displayed to users.
图 4.17 由 ExceptionHandlerMiddleware 创建的自定义错误页面。通过重用 header 和 footer 等元素,自定义错误页面可以具有与应用程序其余部分相同的外观。更重要的是,您可以轻松控制向用户显示的错误详细信息。

Given the differing requirements for error handlers in development and production, most ASP.NET Core apps add their error-handler middleware conditionally, based on the hosting environment. WebApplication automatically adds the developer exception page when running in the development hosting environment, so you typically add ExceptionHandlerMiddleware when you’re not in the development environment, as shown in the following listing.

鉴于开发和生产中对错误处理程序的要求不同,大多数 ASP.NET Core 应用程序都会添加他们的 error-handler 中间件。WebApplication 在开发托管环境中运行时会自动添加开发人员异常页面,因此您通常在不在开发环境中时添加 ExceptionHandlerMiddleware,如下面的清单所示。

Listing 4.5 Adding exception-handler middleware when in production
列表 4.5 添加异常处理程序中间件在生产环境中

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();                     // ❶
if (!app.Environment.IsDevelopment())                     // ❷
{
    app.UseExceptionHandler("/error");                    // ❸
}
// additional middleware configuration
app.MapGet("/error", () => "Sorry, an error occurred");   // ❹

❶ In development, WebApplication automatically adds the developer exception page middleware.
在开发中,WebApplication 会自动添加开发者异常页面中间件。
❷ Configures a different pipeline when not running in development
在开发中未运行时配置不同的管道
❸ The ExceptionHandlerMiddleware won’t leak sensitive details when running in production.
ExceptionHandlerMiddleware 在生产环境中运行时不会泄露敏感细节。
❹ This error endpoint will be executed when an exception is handled.
处理异常时将执行此错误端点。

As well as demonstrating how to add ExceptionHandlerMiddleware to your middleware pipeline, this listing shows that it’s perfectly acceptable to configure different middleware pipelines depending on the environment when the application starts. You could also vary your pipeline based on other values, such as settings loaded from configuration.

除了演示如何将 ExceptionHandlerMiddleware 添加到中间件管道之外,此清单还表明,在应用程序启动时,根据环境配置不同的中间件管道是完全可以接受的。您还可以根据其他值(例如从配置加载的设置)来改变管道。

Note You’ll see how to use configuration values to customize the middleware pipeline in chapter 10.
注意 您将在第 10 章中看到如何使用配置值来自定义中间件管道。

When adding ExceptionHandlerMiddleware to your application, you typically provide a path to the custom error page that will be displayed to the user. In the example in listing 4.5, you used an error handling path of "/error":
在将 ExceptionHandlerMiddleware 添加到应用程序时,通常会提供将向用户显示的自定义错误页面的路径。在清单 4.5 的示例中,您使用了 “/error” 的错误处理路径:

app.UseExceptionHandler("/error");

ExceptionHandlerMiddleware invokes this path after it captures an exception to generate the final response. The ability to generate a response dynamically is a key feature of ExceptionHandlerMiddleware; it allows you to reexecute a middleware pipeline to generate the response sent to the user.

ExceptionHandlerMiddleware 在捕获异常后调用此路径以生成最终响应。动态生成响应的能力是 ExceptionHandlerMiddleware 的一个关键功能;它允许您重新执行中间件管道以生成发送给用户的响应。

Figure 4.18 shows what happens when ExceptionHandlerMiddleware handles an exception. It shows the flow of events when the minimal API endpoint for the "/" path generates an exception. The final response returns an error status code but also provides an error string, using the "/error" endpoint.
图 4.18 显示了当 ExceptionHandlerMiddleware 处理异常时会发生什么。它显示当 “/” 路径的最小 API 端点生成异常时的事件流。最终响应返回错误状态代码,但也使用“/error”端点提供错误字符串。

alt text

Figure 4.18 ExceptionHandlerMiddleware handling an exception to generate a JSON response. A request to the / path generates an exception, which is handled by the middleware. The pipeline is reexecuted, using the /error path to generate the JSON response.
图 4.18 ExceptionHandlerMiddleware 处理异常以生成 JSON 响应。对 / 路径的请求会生成异常,该异常由中间件处理。管道将重新执行,使用 /error 路径生成 JSON 响应。

The sequence of events when an unhandled exception occurs somewhere in the middleware pipeline (or in an endpoint) after ExceptionHandlerMiddleware is as follows:

当 ExceptionHandlerMiddleware 之后的中间件管道(或端点)中的某个位置发生未经处理的异常时,事件序列如下:

  1. A piece of middleware throws an exception.
    一个 middleware 会引发一个异常。

  2. ExceptionHandlerMiddleware catches the exception.
    ExceptionHandlerMiddleware 捕获异常。

  3. Any partial response that has been defined is cleared.
    将清除已定义的任何部分响应。

  4. The ExceptionHandlerMiddleware overwrites the request path with the provided error-handling path.
    ExceptionHandlerMiddleware 使用提供的错误处理路径覆盖请求路径。

  5. The middleware sends the request back down the pipeline, as though the original request had been for the error-handling path.
    中间件将请求发送回管道,就像原始请求是针对错误处理路径的一样。

  6. The middleware pipeline generates a new response as normal.
    中间件管道照常生成新的响应。

  7. When the response gets back to ExceptionHandlerMiddleware, it modifies the status code to a 500 error and continues to pass the response up the pipeline to the web server.
    当响应返回到 ExceptionHandlerMiddleware 时,它会将状态代码修改为 500 错误,并继续将响应从管道向上传递到 Web 服务器。

One of the main advantages of reexecuting the pipeline for Razor Page apps is the ability to have your error messages integrated into your normal site layout, as shown in figure 4.17. It’s certainly possible to return a fixed response when an error occurs without reexecuting the pipeline, but you wouldn’t be able to have a menu bar with dynamically generated links or display the current user’s name in the menu, for example. By reexecuting the pipeline, you ensure that all the dynamic areas of your application are integrated correctly, as though the page were a standard page of your site.

重新执行 Razor Page 应用程序的管道的主要优点之一是能够将错误消息集成到正常的站点布局中,如图所示4.17. 当错误发生时,当然可以在不重新执行管道的情况下返回固定的响应,但你将无法拥有一个带有 dynamic 的菜单栏生成的链接或在菜单中显示当前用户的名称。通过重新执行管道,您可以确保应用程序的所有动态区域都已正确集成,就像该页面是网站的标准页面一样。

Note You don’t need to do anything other than add ExceptionHandlerMiddleware to your application and configure a valid error-handling path to enable reexecuting the pipeline, as shown in figure 4.18. The middleware will catch the exception and reexecute the pipeline for you. Subsequent middleware will treat the reexecution as a new request, but previous middleware in the pipeline won’t be aware that anything unusual happened.
注意 除了将 ExceptionHandlerMiddleware 添加到您的应用程序并配置有效的错误处理路径以启用重新执行管道之外,您不需要执行任何其他作,如图 4.18 所示。中间件将捕获异常并为您重新执行管道。后续中间件会将重新执行视为新请求,但管道中先前的中间件不会意识到发生了任何异常情况。

Reexecuting the middleware pipeline is a great way to keep consistency in your web application for error pages, but you should be aware of some gotchas. First, middleware can modify a response generated farther down the pipeline only if the response hasn’t yet been sent to the client. This situation can be a problem if, for example, an error occurs while ASP.NET Core is sending a static file to a client. In that case, ASP.NET Core may start streaming bytes to the client immediately for performance reasons. In that case, the error-handling middleware won’t be able to run, as it can’t reset the response. Generally speaking, you can’t do much about this problem, but it’s something to be aware of.

重新执行中间件管道是保持 Web 应用程序错误页面一致性的好方法,但您应该注意一些问题。首先,仅当响应尚未发送到客户端时,中间件才能修改在管道中较远处生成的响应。例如,如果在 ASP.NET Core 向客户端发送静态文件时发生错误,则这种情况可能是一个问题。在这种情况下,出于性能原因,ASP.NET Core 可能会立即开始将字节流式传输到客户端。发生这种情况时,错误处理中间件将无法运行,因为它无法重置响应。一般来说,您对此问题无能为力,但需要注意。

A more common problem occurs when the error-handling path throws an error during the reexecution of the pipeline. Imagine that there’s a bug in the code that generates the menu at the top of the page in a Razor Pages app:
当错误处理路径在重新执行管道期间引发错误时,会出现更常见的问题。假设在 Razor Pages 应用程序中生成页面顶部菜单的代码中存在一个 bug:

  1. When the user reaches your home page, the code for generating the menu bar throws an exception.
    当用户访问您的主页时,用于生成菜单栏的代码会引发异常。

  2. The exception propagates up the middleware pipeline.
    异常沿中间件管道向上传播。

  3. When reached, ExceptionHandlerMiddleware captures it, and the pipe is reexecuted, using the error-handling path.
    到达时,ExceptionHandlerMiddleware 会捕获它,并使用错误处理路径重新执行管道。

  4. When the error page executes, it attempts to generate the menu bar for your app, which again throws an exception.
    当错误页面执行时,它会尝试为您的应用程序生成菜单栏,这再次引发异常。

  5. The exception propagates up the middleware pipeline.
    异常沿中间件管道向上传播。

  6. ExceptionHandlerMiddleware has already tried to intercept a request, so it lets the error propagate all the way to the top of the middleware pipeline.
    ExceptionHandlerMiddleware 已经尝试拦截一个请求,所以它允许错误一直传播到中间件管道的顶部。

  7. The web server returns a raw 500 error, as though there were no error-handling middleware at all.
    Web 服务器返回原始 500 错误,就好像根本没有错误处理中间件一样。

Thanks to this problem, it’s often good practice to make your error-handling pages as simple as possible to reduce the possibility that errors will occur.
由于这个问题,通常最好让你的错误处理页面尽可能简单,以减少发生错误的可能性。

Warning If your error-handling path generates an error, the user will see a generic browser error. It’s often better to use a static error page that always works than a dynamic page that risks throwing more errors. You can see an alternative approach using a custom error handling function in this post: http://mng.bz/0Kmx.
警告 如果错误处理路径生成错误,则用户将看到通用浏览器错误。使用始终有效的静态错误页面通常比使用动态页面。您可以在这篇文章中看到使用自定义错误处理函数的替代方法:http://mng.bz/0Kmx

Another consideration when building minimal API applications is that you generally don’t want to return HTML. Returning an HTML page to an application that’s expecting JSON could easily break it. Instead, the HTTP 500 status code and a JSON body describing the error are more useful to a consuming application. Luckily, ASP.NET Core allows you to do exactly this when you create minimal APIs and web API controllers.

构建最小 API 应用程序时的另一个注意事项是,您通常不希望返回 HTML。 将 HTML 页面返回到需要 JSON 的应用程序很容易破坏它。相反,HTTP 500 状态代码和描述错误的 JSON 正文对于使用应用程序更有用。幸运的是,ASP.NET Core 允许您在创建最小 API 和 Web API 控制器时执行此作。

Note I discuss how to add this functionality with minimal APIs in chapter 5 and with web APIs in chapter 20.
注意:我在第 5 章中讨论了如何使用最少的 API 添加此功能,在第 20 章中讨论了如何使用 Web API 添加此功能。

That brings us to the end of middleware in ASP.NET Core for now. You’ve seen how to use and compose middleware to form a pipeline, as well as how to handle exceptions in your application. This information will get you a long way when you start building your first ASP.NET Core applications. Later, you’ll learn how to build your own custom middleware, as well as how to perform complex operations on the middleware pipeline, such as forking it in response to specific requests. In chapter 5, you’ll look in depth at minimal APIs and at how they can be used to build JSON APIs.

现在,我们来到了 ASP.NET Core 中中间件的结尾。您已经了解了如何使用和组合中间件来形成管道,以及如何处理应用程序中的异常。当您开始构建您的第一个 ASP.NET Core 应用程序时,此信息将对您有所帮助。稍后,您将学习如何构建自己的自定义中间件,以及如何在中间件管道上执行复杂的作,例如为响应特定请求而分叉。在第 5 章中,您将深入了解最小的 API 以及如何使用它们来构建 JSON API。

4.4 Summary

4.4 总结

  • Middleware has a similar role to HTTP modules and handlers in ASP.NET but is easier to reason about.
    中间件的作用与 ASP.NET 中的 HTTP 模块和处理程序类似,但更容易推理。
  • Middleware is composed in a pipeline, with the output of one middleware passing to the input of the next.
    中间件由管道组成,一个中间件的输出传递到下一个中间件的输入。
  • The middleware pipeline is two-way: requests pass through each middleware on the way in, and responses pass back through in reverse order on the way out.
    中间件管道是双向的:请求在传入时通过每个中间件,响应在传出时以相反的顺序传回。
  • Middleware can short-circuit the pipeline by handling a request and returning a response, or it can pass the request on to the next middleware in the pipeline.
    中间件可以通过处理请求并返回响应来使管道短路,也可以将请求传递给管道中的下一个中间件。
  • Middleware can modify a request by adding data to or changing the HttpContext object.
    中间件可以通过向 HttpContext 对象添加数据或更改 HttpContext 对象来修改请求。
  • If an earlier middleware short-circuits the pipeline, not all middleware will execute for all requests.
    如果较早的中间件使管道短路,则并非所有中间件都会针对所有请求执行。
  • If a request isn’t handled, the middleware pipeline returns a 404 status code.
    如果未处理请求,中间件管道将返回 404 状态代码。
  • The order in which middleware is added to WebApplication defines the order in which middleware will execute in the pipeline.
    中间件添加到 WebApplication 的顺序定义了中间件在管道中的执行顺序。
  • The middleware pipeline can be reexecuted as long as a response’s headers haven’t been sent.
    只要尚未发送响应的标头,就可以重新执行中间件管道。
  • When it’s added to a middleware pipeline, StaticFileMiddleware serves any requested files found in the wwwroot folder of your application.
    当它被添加到中间件管道时,StaticFileMiddleware 会提供在应用程序的 wwwroot 文件夹中找到的任何请求文件。
  • DeveloperExceptionPageMiddleware provides a lot of information about errors during development, but it should never be used in production.
    DeveloperExceptionPageMiddleware 在开发过程中提供了大量有关错误的信息,但绝不应该在生产中使用。
  • ExceptionHandlerMiddleware lets you provide user-friendly custom error-handling messages when an exception occurs in the pipeline. It’s safe for use in production, as it doesn’t expose sensitive details about your application.
    ExceptionHandlerMiddleware 允许您在管道中发生异常时提供用户友好的自定义错误处理消息。它可以安全地用于生产环境,因为它不会暴露有关应用程序的敏感详细信息。
  • Microsoft provides some common middleware, and many third-party options are available on NuGet and GitHub.
    Microsoft 提供了一些常见的中间件,NuGet 和 GitHub 上提供了许多第三方选项。

[1] Technically, middleware needs to be a function, as you’ll see in chapter 31, but it’s common to implement middleware as a C# class with a single method.
从技术上讲,中间件需要是一个函数,如第 31 章所示,但通常使用单个方法将中间件实现为 C# 类。

[2] The downside to this approach is that it can hide exactly which middleware is being added to the pipeline. When the answer isn’t clear, I typically search for the source code of the extension method directly in GitHub (https://github.com/aspnet/aspnetcore).
这种方法的缺点是它可以准确隐藏要添加到管道的中间件。当答案不明确时,我通常会直接在 GitHub (https://github.com/aspnet/aspnetcore) 中搜索扩展方法的源代码。

[3] C# 8.0 introduced non-nullable reference types, which provide a way to handle null values more clearly, with the promise of finally ridding .NET of NullReferenceExceptions! The ASP.NET Core framework libraries in .NET 7 have fully embraced nullable reference types. See the documentation to learn more: http://mng.bz/7V0g.
C# 8.0 引入了不可为 null 的引用类型,它提供了一种更清晰地处理 null 值的方法,并有望最终消除 .NET 的 NullReferenceExceptions!.NET 7 中的 ASP.NET Core 框架库已完全采用可为 null 的引用类型。请参阅文档以了解更多信息:http:// mng.bz/7V0g。

ASP.NET Core in Action 3 Your first application

ASP.NET Core in Action 3 Your first application

3 Your first application

3 您的第一个应用程序

This chapter covers

本章涵盖

  • Creating your first ASP.NET Core web application Running your application
    创建您的第一个 ASP.NET Core Web 应用程序运行您的应用程序

*Understanding the components of your application
了解应用程序的组件

In the previous chapters, I gave you an overview of how ASP.NET Core applications work and when you should use them. Now you should set up a development environment to use for building applications.

在前面的章节中,我概述了 ASP.NET Core 应用程序的工作原理以及何时应该使用它们。现在,您应该设置一个用于构建应用程序的开发环境。

TIP See appendix A for guidance on installing the .NET 7 software development kit (SDK) and choosing an editor/integrated development environment (IDE) for building ASP.NET Core apps.
提示:有关安装 .NET 7 软件开发工具包 (SDK) 和选择编辑器/集成开发环境 (IDE) 以构建 ASP.NET Core 应用程序的指导,请参阅附录 A。

In this chapter, you'll dive right in by creating your first web app. You'll get to kick the tires and poke around a little to get a feel for how it works. In later chapters, I’ll show you how to go about customizing and building your own applications.

在本章中,您将通过创建您的第一个 Web 应用程序来深入了解。您将可以踢轮胎并四处探查,以了解它的工作原理。在后面的章节中,我将向您展示如何自定义和构建自己的应用程序。

As you work through this chapter, you should begin to get a grasp of the various components that make up an ASP.NET Core application, as well as an understanding of the general application-building process. Most applications you create will start from a similar template, so it’s a good idea to get familiar with the setup as soon as possible.

在学习本章时,您应该开始掌握构成 ASP.NET Core 应用程序的各种组件,并了解一般的应用程序构建过程。您创建的大多数应用程序都将从类似的模板开始,因此最好尽快熟悉设置。

DEFINITION A template provides the basic code required to build an application. You can use a template as the starting point for building your own apps.
定义 模板提供构建应用程序所需的基本代码。您可以使用模板作为起点用于构建您自己的应用程序。

I’ll start by showing you how to create a basic ASP.NET Core application using one of the Visual Studio templates. If you’re using other tooling, such as the .NET command-line interface (CLI), you’ll have similar templates available. I use Visual Studio 2022 and ASP.NET Core 7 with .NET 7 in this chapter, but I also provide tips for working with the .NET CLI.

首先,我将向您展示如何使用其中一个 Visual Studio 模板创建基本的 ASP.NET Core 应用程序。如果您使用的是其他工具,例如 .NET 命令行界面 (CLI),您将有类似的模板可用。在本章中,我将 Visual Studio 2022 和 ASP.NET Core 7 与 .NET 7 结合使用,但我也提供了使用 .NET CLI 的提示。

TIP You can view the application code for this chapter in the GitHub repository for the book at http://mng.bz/5wj1.
提示 您可以在 http://mng.bz/5wj1 本书的 GitHub 存储库中查看本章的应用程序代码。

After you’ve created your application, I’ll show you how to restore all the necessary dependencies, compile your application, and run it to see the output. The application will be simple, containing the bare bones of an ASP.NET Core application that responds with "Hello World!"

创建应用程序后,我将向您展示如何恢复所有必要的依赖项、编译应用程序并运行它以查看输出。该应用程序将很简单,包含 ASP.NET Core 应用程序的基本框架,该应用程序以 “Hello World!

Having run your application, your next step is understanding what’s going on! We’ll take a journey through the ASP.NET Core application, looking at each file in the template in turn. You’ll get a feel for how an ASP.NET Core application is laid out and see what the C# code for the smallest possible app looks like.

运行应用程序后,下一步是了解发生了什么!我们将浏览 ASP.NET Core 应用程序,依次查看模板中的每个文件。您将了解 ASP.NET Core 应用程序的布局方式,并了解尽可能小的应用程序的 C# 代码是什么样子的。

As a final twist, you’ll see how to extend your application to handle requests for static files, as well as how to create a simple API that returns data in standard JavaScript Object Notation (JSON) format.

最后,您将了解如何扩展应用程序以处理对静态文件的请求,以及如何创建以标准 JavaScript 对象表示法 (JSON) 格式返回数据的简单 API。

At this stage, don’t worry if you find parts of the project confusing or complicated; you’ll be exploring each section in detail as you move through the book. By the end of the chapter, you should have a basic understanding of how ASP.NET Core applications are put together, from when your application is first run to when a response is generated. Before we begin, though, we’ll review how ASP.NET Core applications handle requests.

在这个阶段,如果您发现项目的某些部分令人困惑或复杂,请不要担心;在阅读本书时,您将详细探索每个部分。在本章结束时,您应该对 ASP.NET Core 应用程序是如何组合在一起的,从您的应用程序开始,您应该有一个基本的了解run 到生成响应时。不过,在开始之前,我们将回顾一下 ASP.NET Core 应用程序如何处理请求。

3.1 A brief overview of an ASP.NET Core application‌

3.1 ASP.NET Core 应用程序概述

In chapter 1, I described how a browser makes an HTTP request to a server and receives a response, which it uses to render HTML on the page. ASP.NET Core allows you to generate that HTML dynamically depending on the particulars of the request, so that (for example) you can display different data depending on the current logged-in user.

在第 1 章中,我介绍了浏览器如何向服务器发出 HTTP 请求并接收响应,并使用该响应在页面上呈现 HTML。ASP.NET Core 允许您根据请求的详细信息动态生成该 HTML,以便(例如)您可以根据当前登录的用户显示不同的数据。

Suppose that you want to create a web app to display information about your company. You could create a simple ASP.NET Core app to achieve this goal; later, you could add dynamic features to your app. Figure 3.1 shows how the application would handle a request for a page in your application.

假设您要创建一个 Web 应用程序来显示有关您公司的信息。您可以创建一个简单的 ASP.NET Core 应用程序来实现此目标;稍后,您可以向应用程序添加动态功能。图 3.1 显示了应用程序如何处理对应用程序中页面的请求。

alt text

Figure 3.1 An overview of an ASP.NET Core application. The ASP.NET Core application receives an incoming HTTP request from the browser. Every request passes to the middleware pipeline, which potentially modifies it and then passes it to the endpoint middleware at the end of the pipeline to generate a response. The response passes back through the middleware to the server and finally out to the browser.

图 3.1 ASP.NET Core 应用程序概述。ASP.NET Core 应用程序从浏览器接收传入的 HTTP 请求。每个请求都会传递到中间件管道,中间件管道可能会对其进行修改,然后将其传递到管道末尾的终端节点中间件以生成响应。响应通过中间件传回服务器,最后传回浏览器。

Much of this diagram should be familiar to you from figure 1.3 in chapter 1; the request and response and the ASP.NET Core web server are still there. But you’ll notice that I’ve added a reverse proxy to show a common deployment pattern for ASP.NET Core applications. I’ve also expanded the ASP.NET Core application itself to show the middleware pipeline and the endpoint middleware—the main custom part of your app that goes into generating the response from a request.

您应该从第 1 章的图 1.3 中熟悉此图表的大部分内容;请求和响应以及 ASP.NET Core Web 服务器仍然存在。但您会注意到,我添加了一个反向代理来显示 ASP.NET Core 应用程序的常见部署模式。我还扩展了 ASP.NET Core 应用程序本身,以显示中间件管道和终端节点中间件,后者是应用程序的主要自定义部分,用于从请求生成响应。

The first port of call after the reverse proxy forwards a request is the ASP.NET Core web server, which is the default cross-platform Kestrel server. Kestrel takes the raw incoming network request and uses it to generate an HttpContext object that the rest of the application can use.

反向代理转发请求后的第一个调用端口是 ASP.NET Core Web 服务器,它是默认的跨平台 Kestrel 服务器。Kestrel 获取原始传入网络请求,并使用它来生成应用程序其余部分可以使用的 HttpContext 对象。

The HttpContext object
HttpContext 对象
The HttpContext constructed by the ASP.NET Core web server is used by the application as a sort of storage box for a single request. Anything that’s specific to this particular request and the subsequent response can be associated with it and stored in it, such as properties of the request, request-specific services, data that’s been loaded, or errors that have occurred. The web server fills the initial HttpContext with details of the original HTTP request and other configuration details and then passes it on to the rest of the application.
由 ASP.NET Core Web 服务器构建的 HttpContext 被应用程序用作单个请求的一种存储盒。特定于此特定请求和后续响应的任何内容都可以与该请求相关联并存储在其中,例如请求的属性、特定于请求的服务、已加载的数据或已发生的错误。Web 服务器使用原始 HTTP 请求的详细信息和其他配置详细信息填充初始 HttpContext,然后将其传递给应用程序的其余部分。

NOTE Kestrel isn’t the only HTTP server available in ASP.NET Core, but it’s the most performant and is cross-platform. I’ll refer only to Kestrel throughout the book. A different web server, IIS HTTP Server, is used when running in-process in Internet Information Services (IIS). The main alternative, HTTP.sys, runs only in Windows and can’t be used with IIS.1

注意 Kestrel 并不是 ASP.NET Core 中唯一可用的 HTTP 服务器,但它是性能最高的,并且是跨平台的。我在整本书中只提到红隼。在 Internet Information Services (IIS) 中进行进程内运行时,会使用不同的 Web 服务器 IIS HTTP Server。主要替代方案 HTTP.sys 仅在 Windows 中运行,不能与 IIS 一起使用。1

Kestrel is responsible for receiving the request data and constructing a .NET representation of the request, but it doesn’t attempt to generate a response directly. For that task, Kestrel hands the HttpContext to the middleware pipeline in every ASP.NET Core application. This pipeline is a series of components that process the incoming request to perform common operations such as logging, handling exceptions, and serving static files.

Kestrel 负责接收请求数据并构造请求的 .NET 表示形式,但它不会尝试直接生成响应。对于该任务,Kestrel 将 HttpContext 交给每个 ASP.NET Core 应用程序中的中间件管道。此管道是一系列组件,用于处理传入请求以执行常见作,例如日志记录、处理异常和提供静态文件。

NOTE You’ll learn about the middleware pipeline in detail in chapter 4.
注意:您将在第 4 章中详细了解中间件管道。

At the end of the middleware pipeline is the endpoint middleware, which is responsible for calling the code that generates the final response. In most applications that code will be a Model-View-Controller (MVC), Razor Pages, or minimal API endpoint.

中间件管道的末端是端点中间件,它负责调用生成最终响应的代码。在大多数应用程序中,该代码将是模型-视图-控制器 (MVC)、Razor Pages 或最小 API 终结点。

Most ASP.NET Core applications follow this basic architecture, and the example in this chapter is no different. First, you’ll see how to create and run your application; then you’ll look at how the code corresponds to the outline in figure 3.1. Without further ado, let’s create an application!

大多数 ASP.NET Core 应用程序都遵循此基本体系结构,本章中的示例也不例外。首先,您将了解如何创建和运行您的应用程序;然后,您将了解代码如何与图 3.1 中的大纲相对应。事不宜迟,让我们创建一个应用程序!

3.2 Creating your first ASP.NET Core application‌

3.2 创建您的第一个 ASP.NET Core 应用程序

In this section you’re going to create a minimal API application that returns "Hello World!" when you call the HTTP API. This application is about the simplest ASP.NET Core application you can create, but it demonstrates many of the fundamental concepts of building and running applications with .NET.

在本节中,您将创建一个返回 “Hello World!” 的最小 API 应用程序。当您调用 HTTP API 时。此应用程序是您可以创建的最简单的 ASP.NET Core 应用程序,但它演示了使用 .NET 构建和运行应用程序的许多基本概念。

You can start building applications with ASP.NET Core in many ways, depending on the tools and operating system you’re using. Each set of tools has slightly different templates, but the templates have many similarities. The example used throughout this chapter is based on a Visual Studio 2022 template, but you can easily follow along with templates from the .NET CLI or Visual Studio for Mac.

您可以通过多种方式开始使用 ASP.NET Core 构建应用程序,具体取决于您使用的工具和作系统。每组工具的模板略有不同,但模板有许多相似之处。本章中使用的示例基于 Visual Studio 2022 模板,但您可以轻松地使用 .NET CLI 或 Visual Studio for Mac 中的模板进行作。

NOTE As a reminder, I use Visual Studio 2022 and ASP.NET Core with .NET 7 throughout the book.
注意:提醒一下,我在整本书中使用了 Visual Studio 2022 和 ASP.NET Core 和 .NET 7。

Getting an application up and running locally typically involves four basic steps, which we’ll work through in this chapter:

在本地启动和运行应用程序通常涉及四个基本步骤,我们将在本章中介绍这些步骤:

  1. Generate—Create the base application from a template to get started.
    生成 - 从模板创建基本应用程序以开始使用。

  2. Restore—Restore all the packages and dependencies to the local project folder using NuGet.
    还原 - 使用 NuGet 将所有包和依赖项还原到本地项目文件夹。

  3. Build—Compile the application, and generate all the necessary artifacts.
    构建 - 编译应用程序,并生成所有必要的工件。

  4. Run—Run the compiled application.
    运行 - 运行编译后的应用程序。

Visual Studio and the .NET CLI include many ASP.NET Core templates for building different types of applications, such as

Visual Studio 和 .NET CLI 包含许多 ASP.NET Core 模板,用于构建不同类型的应用程序,例如

  • Minimal API applications—HTTP API applications that return data in JSON format, which can be consumed by single-page applications (SPAs) and mobile apps.They’re typically used in conjunction with client-side applications such as Angular and React.js or mobile applications.
    最小 API 应用程序 — 以 JSON 格式返回数据的 HTTP API 应用程序,可供单页应用程序 (SPA) 和移动应用程序使用。它们通常与客户端应用程序(如 Angular 和 React.js)或移动应用程序结合使用。

  • Razor Pages web applications—Razor Pages applications generate HTML on the server and are designed to be viewed by users in a web browser directly.
    Razor Pages Web 应用程序 - Razor Pages 应用程序在服务器上生成 HTML,旨在供用户在 Web 浏览器中直接查看。

  • MVC applications—MVC applications are similar to Razor Pages apps in that they generate HTML on the server and are designed to be viewed by users directly in a web browser. They use traditional MVC controllers instead of Razor Pages.
    MVC 应用程序 - MVC 应用程序类似于 Razor Pages 应用程序,因为它们在服务器上生成 HTML,并且旨在供用户直接在 Web 浏览器中查看。它们使用传统的 MVC 控制器,而不是 Razor Pages。

  • Web API applications—Web API applications are similar to minimal API apps, in that they are typically consumed by SPAs and mobile apps. Web API apps provide additional functionality compared to minimal APIs, at the expense of some performance and convenience.
    Web API 应用程序 - Web API 应用程序类似于最小 API 应用程序,因为它们通常由 SPA 和移动应用程序使用。与最小 API 相比,Web API 应用程序提供了额外的功能,但会牺牲一些性能和便利性。

We’ll look at each of these application types in this book, but in part 1 we focus on minimal APIs, so in section 3.2.1 we start by looking at the simplest ASP.NET Core app you can create.

在本书中,我们将介绍这些应用程序类型中的每一种,但在第 1 部分中,我们将重点介绍最小的 API,因此在第 3.2.1 节中,我们首先介绍您可以创建的最简单的 ASP.NET Core 应用程序。

3.2.1 Using a template to get started‌

3.2.1 使用模板开始

In this section you’ll use a template to create your first ASP.NET Core minimal API application. Using a template can get you up and running with an application quickly, automatically configuring many of the fundamental pieces. Both Visual Studio and the .NET CLI come with standard templates for building web applications, console applications, and class libraries.

在本部分中,您将使用模板创建您的第一个 ASP.NET Core 最小 API 应用程序。使用模板可以使您快速启动并运行应用程序,并自动配置许多基本部分。Visual Studio 和 .NET CLI 都附带了用于构建 Web 应用程序、控制台应用程序和类库的标准模板。

TIP In .NET, a project is a unit of deployment, which will be compiled into a .dll file or an executable, for example. Each separate app is a separate project. Multiple projects can be built and developed at the same time in a solution.
提示 在 .NET 中,项目是一个部署单元,例如,它将被编译成 .dll 文件或可执行文件。每个单独的应用程序都是一个单独的项目。可以在解决方案中同时生成和开发多个项目。

To create your first web application, open Visual Studio, and perform the following steps:

要创建您的第一个 Web 应用程序,请打开 Visual Studio,然后执行以下步骤:

  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 Empty; select the C# language template, as shown in figure 3.2; and then choose Next.
    从模板列表中,选择 ASP.NET Core Empty;选择 C# 语言模板,如图 3.2 所示;,然后选择 Next (下一步)

alt text

Figure 3.2 The Create a New Project dialog box. Select the C# ASP.NET Core Empty template in the list on the right side. When you next create a new project, you can choose a template from the Recent Project Templates list on the left side.

图 3.2 “创建新项目”对话框。在右侧的列表中选择 C# ASP.NET Core Empty 模板。下次创建新项目时,您可以从左侧的 Recent Project Templates (最近的项目模板) 列表中选择一个模板。

On the next screen, enter a project name, location, and solution name, and choose Create, as shown in figure 3.3. You might use WebApplication1 as both the project and solution name, for example.

在下一个屏幕上,输入项目名称、位置和解决方案名称,然后选择 Create(创建),如图3.3所示. 例如,您可以使用 WebApplication1 作为项目和解决方案名称。

alt text

Figure 3.3 The Configure Your New Project dialog box. Enter a project name, location, and solution name, and choose Next.

图 3.3 “Configure Your New Project”(配置新项目)对话框。输入项目名称、位置和解决方案名称,然后选择 Next(下一步)。

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

a. 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.
a.选择 .NET 7.0。如果此选项不可用,请确保您已安装 .NET 7。有关配置环境的详细信息,请参阅附录 A。

b. Ensure that Configure for HTTPS is checked.
b.确保选中 Configure for HTTPS (为 HTTPS 配置)。

c. Ensure that Enable Docker is not checked.
c.确保未选中 Enable Docker。

d. Ensure that Do not use top-level statements is not checked. (I explain top-level statements in section 3.6.)
d.确保 Do not use top-level statements 未选中。(我在 3.6 节中解释了顶级语句)。

e. Choose Create.
e.选择 Create (创建)。

alt text

Figure 3.4 The Additional Information dialog box follows the Configure Your New Project dialog box and lets you customize the template that will generate your application. For this starter project, you’ll create an empty .NET 7 application that uses top-level statements.

图 3.4 “其他信息”对话框位于 “配置新项目” 对话框之后,并且用于自定义将生成应用程序的模板。对于此初学者项目,您将创建一个使用顶级语句的空 .NET 7 应用程序。

  1. Wait for Visual Studio to generate the application from the template. When Visual Studio finishes, an introductory page about ASP.NET Core appears; you should see that Visual Studio has created and added some files to your project, as shown in figure 3.5.
    等待 Visual Studio 从模板生成应用程序。Visual Studio 完成后,将显示有关 ASP.NET Core 的介绍性页面;您应该看到 Visual Studio 已经创建了一些文件并将其添加到您的项目中,如图 3.5 所示。

alt text

Figure 3.5 Visual Studio after creating a new ASP.NET Core application from a template. The Solution Explorer shows your newly created project. The introductory page has helpful links for learning about ASP.NET Core.

图 3.5 从模板创建新的 ASP.NET Core 应用程序后的 Visual Studio。Solution Explorer 将显示您新创建的项目。介绍性页面包含用于了解 ASP.NET Core 的有用链接。

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

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

Listing 3.1 Creating a new minimal API application with the .NET CLI
清单 3.1 使用 .NET CLI 创建一个新的最小 API 应用程序

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

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

NOTE Visual Studio uses the concept of a solution to work with multiple projects. The example solution consists of a single project, which is listed in the .sln file. If you use a CLI template to create your project, you won’t have a .sln file unless you generate it explicitly by using additional .NET CLI templates (listing 3.1).

注意:Visual Studio 使用解决方案的概念来处理多个项目。示例解决方案由一个项目组成,该项目在 .sln 文件中列出。如果使用 CLI 模板创建项目,则不会有 .sln 文件,除非使用其他 .NET CLI 模板(清单 3.1)显式生成该文件。

Whether you use Visual Studio or the .NET CLI, now you have the basic files required to build and run your first ASP.NET Core application.

无论您使用的是 Visual Studio 还是 .NET CLI,现在您都拥有构建和运行第一个 ASP.NET Core 应用程序所需的基本文件。

3.2.2 Building the application‌

3.2.2 构建应用程序

At this point, you have most of the files necessary to run your application, but you’ve got two steps left. First, you need to ensure all the dependencies used by your project are downloaded to your machine, and second, you need to compile your application so that it can be run.

此时,您拥有运行应用程序所需的大部分文件,但还剩下两个步骤。首先,您需要确保项目使用的所有依赖项都已下载到您的计算机上,其次,您需要编译应用程序,以便它可以运行。

The first step isn’t strictly necessary, as both Visual Studio and the .NET CLI automatically restore packages when they create your project, but it’s good to know what’s going on. In earlier versions of the .NET CLI, before 2.0, you needed to restore packages manually by using dotnet restore.

第一步并不是绝对必要的,因为 Visual Studio 和 .NET CLI 都会在创建项目时自动还原包,但最好了解发生了什么。在早期版本的 .NET CLI 中,在 2.0 之前,您需要使用 dotnet restore 手动还原包。

You can compile your application by choosing Build > Build Solution, pressing the shortcut Ctrl-Shift-B, or running dotnet build from the command line. If you build from Visual Studio, the output window shows the progress of the build, and assuming that everything is hunky-dory, Visual Studio compiles your application, ready for running. You can also run the dotnet build console commands from the Package Manager Console in Visual Studio.

您可以通过选择 Build > Build Solution(生成解决方案)、按快捷键 Ctrl-Shift-B 或从命令行运行 dotnet build 来编译应用程序。如果从 Visual Studio 进行构建,则输出窗口会显示构建进度,并假设一切都是 hunky-dory,则 Visual Studio 会编译您的应用程序,准备好运行。还可以从 Visual Studio 中的包管理器控制台运行 dotnet build 控制台命令。

TIP Visual Studio and the .NET CLI tools build your application automatically when you run it if they detect that a file has changed, so you generally won’t need to perform this step explicitly yourself.
提示 如果 Visual Studio 和 .NET CLI 工具检测到文件已更改,则它们在运行时会自动构建应用程序,因此您通常不需要自己显式执行此步骤。

NuGet packages and the .NET CLI
NuGet 包和 .NET CLI

One of the foundational components of .NET 7 cross-platform development is the .NET CLI, which provides several basic commands for creating, building, and running .NET 7 applications. Visual Studio effectively calls these commands automatically, but you can also invoke them directly from the command line if you’re using a different editor. The most common commands used during development are
.NET 7 跨平台开发的基础组件之一是.NET CLI,它提供了几个用于创建、构建和运行 .NET 7 应用程序的基本命令。Visual Studio 可以有效地自动调用这些命令,但如果您使用的是其他编辑器,也可以直接从命令行调用它们。开发过程中最常用的命令是

  • dotnet restore
  • dotnet build
  • dotnet run‌

Each of these commands should be run inside your project folder and will act on that project alone. Except where explicitly noted, this is the case for all .NET CLI commands.
这些命令中的每一个都应该在你的项目文件夹中运行,并且将单独作用于该项目。除非明确说明,否则所有 .NET CLI 命令都是这种情况。

Most ASP.NET Core applications have dependencies on various external libraries, which are managed through the NuGet package manager. These dependencies are listed in the project, but the files of the libraries themselves aren’t included. Before you can build and run your application, you need to ensure that there are local copies of each dependency on your machine. The first command, dotnet restore, ensures that your application’s NuGet dependencies are downloaded and the files are referenced correctly by your project.
大多数 ASP.NET Core 应用程序都依赖于各种外部库,这些库通过 NuGet 包管理器进行管理。这些依赖项在项目中列出,但不包括库本身的文件。在构建和运行应用程序之前,您需要确保计算机上有每个依赖项的本地副本。第一个命令 dotnet restore 可确保下载应用程序的 NuGet 依赖项,并且项目正确引用这些文件。

ASP.NET Core projects list their dependencies in the project’s .csproj file, an XML file that lists each dependency as a PackageReference node. When you run dotnet restore, it uses this file to establish which NuGet packages to download. Any dependencies listed are available for use in your application.
ASP.NET Core 项目在项目的 .csproj 文件中列出其依赖项,该文件是一个将每个依赖项列为 PackageReference 节点的 XML 文件。运行 dotnet restore 时,它会使用此文件来确定要下载的 NuGet 包。列出的任何依赖项都可用于您的应用程序。

The restore process typically happens implicitly when you build or run your application, as shown in the following figure, but it can be useful sometimes to run it explicitly, such as in continuous-integration build pipelines.
还原过程通常在您构建或运行应用程序时隐式发生,如下图所示,但有时显式运行它可能很有用,例如在持续集成构建管道中。

The dotnet build command runs dotnet restore implicitly. Similarly, dotnet run runs dotnet build and dotnet restore. If you don’t want to run the previous steps automatically, you can use the --no-restore and --no- build flags, as in dotnet build --no-restore.
dotnet build 命令隐式运行 dotnet restore。同样,dotnet run 运行 dotnet build 和 dotnet restore。如果不想自动运行前面的步骤,可以使用 --no-restore 和 --no- build 标志,如 dotnet build --no-restore 中所示。

alt text

You can compile your application by using dotnet build, which checks for any errors in your application and, if it finds no problems, produces output binaries that can be run with dotnet run.
可以使用 dotnet build 编译应用程序,它会检查应用程序中的任何错误,如果未发现问题,则生成可以使用 dotnet run 运行的输出二进制文件。

Each command contains switches that can modify its behavior. To see the full list of available commands, run
每个命令都包含可以修改其行为的开关。要查看可用命令的完整列表,请运行

dotnet --help

To see the options available for a particular command, such as new, run
要查看特定命令的可用选项(如 new),请运行

dotnet new --help

3.3 Running the web application‌

3.3 运行 Web 应用程序

You’re ready to run your first application, and you have several ways to go about it. In Visual Studio, you can click the green arrow on the toolbar next to WebApplication1 or press the F5 shortcut. Visual Studio will automatically open a web browser window for you with the appropriate URL, and after a second or two, you should see the basic "Hello World!" response, as shown in figure 3.6.

您已准备好运行您的第一个应用程序,并且有几种方法可以开始运行。在 Visual Studio 中,您可以单击 WebApplication1 旁边的工具栏上的绿色箭头或按 F5 快捷方式。Visual Studio 将自动为您打开一个包含相应 URL 的 Web 浏览器窗口,一两秒钟后,您应该会看到基本的“Hello World! 响应,如图 3.6 所示。

alt text

Figure 3.6 The output of your new ASP.NET Core application. The template chooses a random port to use for your application’s URL, which will be opened in the browser automatically when you run from Visual Studio.

图 3.6 新的 ASP.NET Core 应用程序的输出。该模板选择一个随机端口用于应用程序的 URL,当您从 Visual Studio 运行时,该端口将自动在浏览器中打开。

Alternatively, instead of using Visual Studio, you can run the application from the command line with the .NET CLI tools by using dotnet run. Then you can open the URL in a web browser manually, using the address provided on the command line.

或者,您可以使用 dotnet run 使用 .NET CLI 工具从命令行运行应用程序,而不是使用 Visual Studio。然后,您可以使用命令行上提供的地址在 Web 浏览器中手动打开 URL。

Depending on whether you created your application with Visual Studio, you may see an http:// or https:// URL.

根据是否使用 Visual Studio 创建应用程序,您可能会看到 http:// 或 https:// URL。

TIP The first time you run the application from Visual Studio, you may be prompted to install the development certificate. Doing so ensures that your browser doesn’t display warnings about an invalid certificate.2 See chapter 28 for more about HTTPS certificates.
提示 首次从 Visual Studio 运行应用程序时,系统可能会提示您安装开发证书。这样做可以确保您的浏览器不会显示有关无效证书的警告。 2 有关 HTTPS 证书的更多信息,请参见第 28 章。

This basic application has a single endpoint that returns the plain-text response when you request the path /, as you saw in figure 3.6. There isn’t anything more you can do with this simple app, so let’s look at some code!
此基本应用程序具有单个终端节点,当您请求路径 / 时,该终端节点将返回纯文本响应,如图 3.6.这个简单的应用程序没有更多的事情可以做,所以让我们看看一些代码!

3.4 Understanding the project layout‌

3.4 了解项目布局

When you’re new to a framework, creating an application from a template can be a mixed blessing. On one hand, you can get an application up and running quickly, with little input required on your part. Conversely, the number of files can be overwhelming, leaving you scratching your head working out where to start. The basic web application template doesn’t contain a huge number of files and folders, as shown in figure 3.7, but I’ll run through the major ones to get you oriented.

当您不熟悉框架时,从模板创建应用程序可能是一件好坏参半的事情。一方面,您可以快速启动并运行应用程序,几乎不需要您输入。相反,文件的数量可能会让人不知所措,让您摸不着头脑,不知道从哪里开始。如图 3.7 所示,基本的 Web 应用程序模板不包含大量的文件和文件夹,但我将介绍主要的文件和文件夹,以便您了解情况。

alt text

Figure 3.7 Solution Explorer and folder on disk for a new ASP.NET Core application. Solution Explorer also displays the Connected Services and Dependencies nodes, which list NuGet and other dependencies, though the folders themselves don’t exist on disk.

图 3.7 新 ASP.NET Core 应用程序的解决方案资源管理器和磁盘上的文件夹。“解决方案资源管理器”还显示“连接的服务”和“依赖项”节点,其中列出了 NuGet 和其他依赖项,但文件夹本身并不存在于磁盘上。

The first thing to notice is that the main project, WebApplication1, is nested in a top-level directory with the name of the solution, which is also WebApplication1 in this case. Within this top-level folder you’ll also find the solution (.sln) file used by Visual Studio, though this is hidden in Visual Studio’s Solution Explorer view.

首先要注意的是,主项目 WebApplication1 嵌套在具有解决方案名称的顶级目录中,在本例中也是 WebApplication1。在此顶级文件夹中,您还可以找到 Visual Studio 使用的解决方案 (.sln) 文件,尽管该文件隐藏在 Visual Studio 的“解决方案资源管理器”视图中。

Inside the solution folder you’ll find your project folder, which contains the most important file in your project: WebApplication1.csproj. This file describes how to build your project and lists any additional NuGet packages that it requires. Visual Studio doesn’t show the .csproj file explicitly, but you can edit it if you double-click the project name in Solution Explorer or right-click and choose Properties from the contextual menu. We’ll take a closer look at this project file in the next section.

在解决方案文件夹中,您将找到您的项目文件夹,其中包含项目中最重要的文件:WebApplication1.csproj。此文件描述了如何构建project 并列出它所需的任何其他 NuGet 包。Visual Studio 不会显式显示 .csproj 文件,但如果您在解决方案资源管理器中双击项目名称,或者右键单击并从上下文菜单中选择“属性”,则可以对其进行编辑。我们将在下一节中仔细研究此项目文件。

Your project folder contains a subfolder called Properties, which contains a single file: launchSettings.json. This file controls how Visual Studio will run and debug the application. Visual Studio shows the file as a special node in Solution Explorer, out of alphabetical order, near the top of your project. You’ve got two more special nodes in the project, Dependencies and Connected Services, but they don’t have corresponding folders on disk.

您的项目文件夹包含一个名为 Properties 的子文件夹,其中包含一个文件:launchSettings.json。此文件控制 Visual Studio 运行和调试应用程序的方式。Visual Studio 在“解决方案资源管理器”中将文件显示为一个特殊节点,不按字母顺序显示在项目顶部附近。项目中还有两个特殊节点,即 Dependencies 和 Connected Services,但它们在磁盘上没有相应的文件夹。

Instead, they show a collection of all the dependencies, such as NuGet packages, and remote services that the project relies on.In the root of your project folder, you’ll find two JSON files: appsettings.json and appsettings.Development.json. These files provide configuration settings that are used at runtime to control the behavior of your app.

相反,它们显示项目所依赖的所有依赖项(如 NuGet 包和远程服务)的集合。项目文件夹的根目录中,您将找到两个 JSON 文件:appsettings.json 和 appsettings。Development.json. 这些文件提供在运行时用于控制应用程序行为的配置设置。

Finally, Visual Studio shows one C# file in the project folder: Program.cs. In section 3.6 you’ll see how this file configures and runs your application.

最后,Visual Studio 在项目文件夹中显示一个 C# 文件:Program.cs。在 Section 3.6 中,您将看到此文件如何配置和运行您的应用程序。

3.5 The .csproj project file: Declaring your dependencies‌

3.5 .csproj 项目文件:声明依赖项

The .csproj file is the project file for .NET applications and contains the details required for the .NET tooling to build your project. It defines the type of project being built (web app, console app, or library), which platform the project targets (.NET Core 3.1, .NET 7 and so on), and which NuGet packages the project depends on.

.csproj 文件是 .NET 应用程序的项目文件,包含用于生成项目的 .NET 工具所需的详细信息。它定义正在生成的项目的类型 (Web 应用程序、控制台应用程序或库) ,以及项目面向的平台 (.NET Core 3.1、.NET 7 等)以及项目所依赖的 NuGet 包。

The project file has been a mainstay of .NET applications, but in ASP.NET Core it has had a facelift to make it easier to read and edit. These changes include

项目文件一直是 .NET 应用程序的支柱,但在 ASP.NET Core 中,它已经进行了改进,使其更易于阅读和编辑。这些更改包括

  • No GUIDs—Previously, globally unique identifiers (GUIDs) were used for many things, but now they’re rarely used in the project file.
    无 GUID – 以前,全局唯一标识符 (GUID) 用于许多用途,但现在它们很少在项目文件中使用。

  • Implicit file includes—Previously, every file in the project had to be listed in the .csproj file to be included in the build. Now files are compiled automatically.
    隐式文件包含 — 以前,项目中的每个文件都必须在 .csproj 文件中列出才能包含在生成中。现在文件会自动编译。

  • No paths to NuGet package .dll files—Previously, you had to include the path to the .dll files contained in NuGet packages in the .csproj, as well as list the dependencies in a packages.config file. Now you can reference the NuGet package directly in your .csproj, and you don’t need to specify the path on disk.
    没有 NuGet 包.dll文件的路径 – 以前,必须在 .csproj 中包含 NuGet 包中包含的 .dll 文件的路径,并在 packages.config 文件中列出依赖项。现在,您可以直接在 .csproj 中引用 NuGet 包,而无需指定磁盘上的路径。

All these changes combine to make the project file far more compact than you’ll be used to from previous .NET projects. The following listing shows the entire .csproj file for your sample app.
所有这些更改结合在一起,使项目文件比您以前习惯的 .NET 项目要紧凑得多。以下清单显示了示例应用程序的整个 .csproj 文件。

Listing 3.2 The .csproj project file, showing SDK, target framework, and references
清单 3.2 .csproj 项目文件,显示 SDK、目标框架和引用

<Project Sdk="Microsoft.NET.Sdk.Web">             ❶
    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework> ❷
        <Nullable>enable</Nullable>               ❸
        <ImplicitUsings>enable</ImplicitUsings>   ❹
    </PropertyGroup>
</Project>

❶ The SDK attribute specifies the type of project you’re building.
SDK 属性指定要生成的项目类型。

❷ The TargetFramework is the framework you’ll run on—in this case, .NET 7.
TargetFramework 是你将在其上运行的框架,在本例中为 .NET 7。

❸ Enables the C# 8 feature “nullable reference types”
启用 C# 8 功能“可为 null 的引用类型”

❹ Enables the C# 10 feature “implicit using statements”
启用 C# 10 功能“隐式 using 语句”

For simple applications, you probably won’t need to change the project file much. The Sdk attribute on the Project element includes default settings that describe how to build your project, whereas the TargetFramework element describes the framework your application will run on. For .NET 6.0 projects, this element will have the net6.0 value; if you’re running on .NET 7, this will be net7.0. You can also enable and disable various features of the compiler, such as the C# 8 feature nullable reference types or the C# 10 feature implicit using statements.3‌‌

对于简单的应用程序,您可能不需要对项目文件进行太多更改。Project 元素上的 Sdk 属性包括描述如何生成项目的默认设置,而 TargetFramework 元素描述应用程序将在其上运行的框架。对于 .NET 6.0 项目,此元素将具有 net6.0 值;如果您在 .NET 7 上运行,则为 Net7.0。您还可以启用和禁用编译器的各种功能,例如 C# 8 功能可为 null 的引用类型或 C# 10 隐式 using 语句功能。 3

TIP With the new csproj style, Visual Studio users can double- click a project in Solution Explorer to edit the .csproj file without having to close the project first.
提示 使用新的 csproj 样式,Visual Studio 用户可以在“解决方案资源管理器”中双击项目来编辑 .csproj 文件,而不必先关闭项目。

The most common changes you’ll make to the project file are to add more NuGet packages by using the PackageReference element. By default, your app doesn’t reference any NuGet packages at all.

对项目文件进行的最常见更改是使用 PackageReference 元素添加更多 NuGet 包。默认情况下,您的应用根本不引用任何 NuGet 包。

Using NuGet libraries in your project
在项目中使用 NuGet 库

Even though all apps are unique in some way, they also have common requirements. Most apps need to access a database, for example, or manipulate JSON- or XML-formatted data. Rather than having to reinvent that code in every project, you should use existing reusable libraries.
尽管所有应用程序在某种程度上都是唯一的,但它们也有共同的要求。例如,大多数应用程序需要访问数据库,或者作 JSON 或 XML 格式的数据。您不必在每个项目中重新创建该代码,而应该使用现有的可重用库。

NuGet is the library package manager for .NET, where libraries are packaged in NuGet packages and published to https://www.nuget.org. You can use these packages in your project by referencing the unique package name in your .csproj file, making the package’s namespace and classes available in your code files.
NuGet 是 .NET 的库包管理器,其中的库打包在 NuGet 包中并发布到 https://www.nuget.org。您可以通过在 .csproj 文件中引用唯一的包名称来在项目中使用这些包,从而使包的命名空间和类在代码文件中可用。

You can publish (and host) NuGet packages to repositories other than nuget.org; see https://learn.micro soft.com/en-us/nuget for details.
您可以将 NuGet 包发布(和托管)到 nuget.org 以外的存储库;有关详细信息 ,请参阅 https://learn.micro soft.com/en-us/nuget。

You can add a NuGet reference to your project by running dotnet add package
可以通过运行 dotnet add package 将 NuGet 引用添加到项目

from inside the project folder. This command updates your project file with a node and restores the NuGet package for your project. To install the popular Newtonsoft.Json library, for example, you would run 从项目文件夹内。此命令使用 节点更新项目文件,并还原项目的 NuGet 包。例如,要安装流行的 Newtonsoft.Json 库,您需要运行

dotnet add package Newtonsoft.Json

This command adds a reference to the latest version of the library to your project file, as shown next, and makes the Newtonsoft.Json namespace available in your source-code files:
此命令将对最新版本的库的引用添加到您的项目文件中,如下所示,并使 Newtonsoft.Json 命名空间在您的源代码文件中可用:


<Project Sdk="Microsoft.NET.Sdk.Web">
    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
            <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="NewtonSoft.Json" Version="13.0.1" />
    </ItemGroup>
</Project>

If you’re using Visual Studio, you can manage packages with the NuGet Package Manager by right-clicking the solution name or a project and choosing Manage NuGet Packages from the contextual menu.
如果您使用的是 Visual Studio,则可以使用 NuGet 包管理器管理包,方法是右键单击解决方案名称或项目,然后从上下文菜单中选择 Manage NuGet Packages。

As a point of interest, there’s no officially agreed-on pronunciation for NuGet. Feel free to use the popular “noo-get” or “nugget” style, or if you’re feeling especially posh, try “noo-jay”!
值得一提的是,NuGet 没有正式商定的发音。随意使用流行的 “no-get” 或 “nugget” 风格,或者如果你觉得特别时髦,试试 “no-jay”吧!

The simplified project file format is much easier to edit by hand than previous versions, which is great if you’re developing cross- platform. But if you’re using Visual Studio, don’t feel that you have to take this route. You can still use the GUI to add project references, exclude files, manage NuGet packages, and so on.

简化的项目文件格式比以前的版本更容易手动编辑,如果您正在跨平台开发,这非常有用。但是,如果您使用的是 Visual Studio,请不要觉得您必须走这条路。您仍然可以使用 GUI 添加项目引用、排除文件、管理 NuGet 包等。

Visual Studio will update the project file itself, as it always has.

Visual Studio 将一如既往地更新项目文件本身。

TIP For further details on the changes to the csproj format, see the documentation at http://mng.bz/vnzJ.
提示 有关对 csproj 格式的更改的更多详细信息,请参阅 http://mng.bz/vnzJ 中的文档。

The project file defines everything Visual Studio and the .NET CLI need to build your app—everything, that is, except the code! In the next section we’ll look at the file that defines your whole ASP.NET Core application: the Program.cs file.

项目文件定义了 Visual Studio 和 .NET CLI 构建应用程序所需的一切,即除代码之外的所有内容!在下一节中,我们将查看定义整个 ASP.NET Core 应用程序的文件:Program.cs 文件。

3.6 Program.cs file: Defining your application‌

3.6 Program.cs 文件:定义应用程序

All ASP.NET Core applications start life as a .NET Console application. As of .NET 6, that typically means a program written with top-level statements, in which the startup code for your application is written directly in a file instead of inside a static void Main function.

所有 ASP.NET Core 应用程序都以 .NET 控制台应用程序的形式开始运行。从 .NET 6 开始,这通常意味着使用顶级语句编写的程序,其中应用程序的启动代码直接写入文件中,而不是写入静态 void Main 函数中。

Top-level statements
顶级语句

Before C# 9, every .NET program had to include a static void Main function (it could also return int, Task, or Task), typically declared in a class called Program. This function, which must exist, defines the entry point for your program. This code runs when you start your application, as in this example:
在 C# 9 之前,每个 .NET 程序都必须包含一个静态 void Main 函数(它也可以返回 int、Task 或 Task),通常在名为 Program 的类中声明。此函数必须存在,用于定义程序的入口点。此代码在您启动应用程序时运行,如下例所示:


using System;
namespace MyApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

With top-level statements you can write the body of this method directly in the file, and the compiler generates the Main method for you.
使用顶级语句,您可以直接在文件中编写此方法的主体,编译器将为您生成 Main 方法。

When combined with C# 10 features such as implicit using statements, this dramatically simplifies the entry-point code of your app to
当与 C# 10 功能(如隐式 using 语句)结合使用时,这会极大地简化应用的入口点代码,以便


Console.WriteLine("Hello World!");

When you use the explicit Main function you can access the command-line arguments provided when the app was run using the args parameter. With top- level statements the args variable is also available as a string[], even though it’s not declared explicitly. You could echo each argument provided by using‌
您使用显式 Main 函数时,您可以使用 args 参数访问在运行应用程序时提供的命令行参数。对于顶级语句,args 变量也可以作为 string[] 使用,即使它没有显式声明。您可以使用


foreach(string arg in args)
{
    Console.WriteLine(arg);
}

In .NET 7 all the default templates use top-level statements, and I use them throughout this book. Most of the templates include an option to use the explicit Main function if you prefer (using the --use-program-main option if you’re using the CLI). For more information on top-level statements and their limitations, see http://mng.bz/4DZa. If you decide to switch approaches later, you can always add or remove the Main function manually as required.
在 .NET 7 中,所有默认模板都使用顶级语句,我在本书中一直使用它们。如果您愿意,大多数模板都包含一个选项,可以使用显式的 Main 函数(如果您使用的是 CLI,请使用 --use-program-main 选项)。有关 top-level 语句及其限制的更多信息,请参阅 http://mng.bz/4DZa。如果您决定稍后切换方法,则始终可以根据需要手动添加或删除 Main 函数。

In .NET 7 ASP.NET Core applications the top-level statements build and run a WebApplication instance, as shown in the following listing, which shows the default Program.cs file. The WebApplication is the core of your ASP.NET Core application, containing the application configuration and the Kestrel server that listens for requests and sends responses.

在 .NET 7 ASP.NET Core 应用程序中,顶级语句构建并运行 WebApplication 实例,如下面的清单所示,其中显示了默认的 Program.cs 文件。WebApplication 是 ASP.NET Core 应用程序的核心,包含应用程序配置和侦听请求并发送响应的 Kestrel 服务器。

Listing 3.3 The default Program.cs file that configures and runs a WebApplication
列表 3.3 配置和运行 WebApplication 的默认 Program.cs 文件


WebApplicationBuilder builder = WebApplication.CreateBuilder(args); ❶
WebApplication app = builder.Build();                               ❷
app.MapGet("/", () => "Hello World!");                              ❸
app.Run();                                                          ❹

❶ Creates a WebApplicationBuilder using the CreateBuilder method
使用 CreateBuilder 方法创建 WebApplicationBuilder

❷ Builds and returns an instance of WebApplication from the WebApplicationBuilder
从 WebApplicationBuilder 构建并返回 WebApplication 的实例

❸ Defines an endpoint for your application, which returns Hello World! when the path “/” is called
为您的应用程序定义一个端点,当调用路径 “/” 时,该端点将返回 Hello World!

❹ Runs the WebApplication to start listening for requests and generating responses
运行 WebApplication 以开始侦听请求并生成响应

These four lines contain all the initialization code you need to create a web server and start listening for requests. It uses a WebApplicationBuilder, created by the call to CreateBuilder, to define how the WebApplication is configured, before instantiating the WebApplication with a call to Build().

这四行包含创建 Web 服务器和开始侦听请求所需的所有初始化代码。它使用通过调用 CreateBuilder 创建的 WebApplicationBuilder 来定义 WebApplication 的配置方式,然后通过调用 Build() 实例化 WebApplication。

NOTE You’ll find this pattern of using a builder object to configure a complex object repeated throughout the ASP.NET Core framework. This technique is useful for allowing users to configure an object, delaying its creation until all configuration has finished. It’s also one of the patterns described in the “Gang of Four” book Design Patterns: Elements of Reusable Object- Oriented Software, by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley, 1994).‌‌‌‌
注意 您会发现这种使用 builder 对象配置复杂对象的模式在整个 ASP.NET Core 框架中重复出现。此技术可用于允许用户配置对象,延迟其创建,直到所有配置完成。它也是 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著的《设计模式:可重用面向对象软件的元素》(Addison-Wesley,1994 年)一书中描述的模式之一。

In this simple application we don’t make any changes to WebApplicationBuilder before calling Build(), but WebApplicationBuilder configures a lot of things by default, including

在这个简单的应用程序中,我们在调用 Build() 之前不会对 WebApplicationBuilder 进行任何更改,但 WebApplicationBuilder 默认配置了很多内容,包括

  • Configuration—Your app loads values from JSON files and environment variables that you can use to control the app’s runtime behavior, such as loading connection strings for a database. You’ll learn more about the configuration system in chapter 10.
    配置 – 您的应用程序从 JSON 文件和环境变量加载值,您可以使用这些值来控制应用程序的运行时行为,例如加载数据库的连接字符串。您将在第 10 章中了解有关配置系统的更多信息。

  • Logging—ASP.NET Core includes an extensible logging system for observability and debugging. I cover the logging system in detail in chapter 26.
    日志记录 — ASP.NET Core 包括一个可扩展的日志记录系统,用于可观察性和调试。我在第 26 章中详细介绍了日志记录系统。

  • Services—Any classes that your application depends on for providing functionality—both those used by the framework and those specific to your application— must be registered so that they can be instantiated correctly at runtime. The WebApplicationBuilder configures the minimal set of services needed for an ASP.NET Core app. Chapters 8 and 9 look at service configuration in detail.
    服务 — 您的应用程序用于提供功能所依赖的任何类(框架使用的类和特定于您的应用程序的类)都必须注册,以便可以在运行时正确实例化它们。WebApplicationBuilder 配置 ASP.NET Core 应用程序所需的最小服务集。第 8 章和第 9 章详细介绍了服务配置。

  • Hosting—ASP.NET Core uses the Kestrel web server by default to handle requests.
    托管 — 默认情况下,ASP.NET Core 使用 Kestrel Web 服务器来处理请求。

After configuring the WebApplicationBuilder you call Build() to create a WebApplication instance. The WebApplication instance is where you define how your application handles and responds to requests, using two building blocks:

配置 WebApplicationBuilder 后,调用 Build() 来创建 WebApplication 实例。WebApplication 实例是定义应用程序如何处理和响应请求的地方,它使用两个构建块:

  • Middleware—These small components execute in sequence when the application receives an HTTP request. They can perform a whole host of functions, such as logging, identifying the current user for a request, serving static files, and handling errors. We’ll look in detail at the middleware pipeline in chapter 4.
    中间件 — 当应用程序收到 HTTP 请求时,这些小组件会按顺序执行。它们可以执行一系列功能,例如日志记录、识别请求的当前用户、提供静态文件以及处理错误。我们将在第 4 章中详细介绍中间件管道。

  • Endpoints—Endpoints define how the response should be generated for a specific request to a URL in your app.
    端点 - 端点定义应如何为应用程序中 URL 的特定请求生成响应。

For the application in listing 3.3, we didn’t add any middleware, but we defined a single endpoint using a call to MapGet:

对于清单 3.3 中的应用程序,我们没有添加任何中间件,但我们使用对 MapGet 的调用定义了一个端点:

app.MapGet("/", () => "Hello World!");

You use the MapGet function to define how to handle a request that uses the GET HTTP verb. There are other Map* functions for other HTTP verbs, such as MapPost.‌‌

使用 MapGet 函数定义如何处理使用 GET HTTP 动词的请求。还有其他 Map* 函数可用于其他 HTTP 动词,例如 MapPost。

DEFINITION Every HTTP request includes a verb that indicates the type of the request. When you’re browsing a website, the default verb is GET, which fetches a resource from the server so you can view it. The second-most-common verb is POST, which is used to send data to the server, such as when you’re completing a form.
定义 每个 HTTP 请求都包含一个动词,用于指示请求的类型。当您浏览网站时,默认动词是 GET,它从服务器获取资源以便您可以查看它。第二常见的动词是 POST,用于将数据发送到服务器,例如当您填写表单时。

The first argument passed to MapGet defines which URL path to respond to, and the second argument defines how to generate the response as a delegate that returns a string. In this simple case, the arguments say “When a request is made to the path / using the GET HTTP verb, respond with the plain-text value Hello World!”.

传递给 MapGet 的第一个参数定义要响应的 URL 路径,第二个参数定义如何将响应生成为返回字符串的委托。在这个简单的例子中,参数说“当使用 GET HTTP 动词向路径发出请求时,使用纯文本值 Hello World! 进行响应”。

DEFINITION A path is the remainder of the request URL after the domain has been removed. For a request to www.example.org/accout/manage, the path is /account/manage.
定义 路径是删除域后请求 URL 的其余部分。对于 www.example.org/accout/manage 的请求,路径为/account/manage 中。

While you’re configuring the WebApplication and WebApplicationBuilder the application isn’t handling HTTP requests. Only after the call to Run() does the HTTP server start listening for requests. At this point, your application is fully operational and can respond to its first request from a remote browser.

在配置 WebApplication 和 WebApplicationBuilder 时,应用程序不处理 HTTP 请求。只有在调用 Run() 之后,HTTP 服务器才会开始侦听请求。此时,您的应用程序已完全运行,并且可以响应来自远程浏览器的第一个请求。

NOTE The WebApplication and WebApplicationBuilder classes were introduced in .NET 6. The initialization code in previous versions of ASP.NET Core was more verbose but gave you more control of your application’s behavior. Configuration was typically split between two classes—Program and Startup‌‌‌ and used different configuration types—IHostBuilder and IHost, which have fewer defaults than WebApplication. In chapter 30 I describe some of these differences in more detail and show how to configure your application by using the generic IHost instead of WebApplication.
注意 WebApplication 和 WebApplicationBuilder 类是在 .NET 6 中引入的。以前版本的 ASP.NET Core 中的初始化代码更冗长,但可以让您更好地控制应用程序的行为。配置通常分为两个类 — Program 和 Startup,并使用了不同的配置类型 - IHostBuilder 和IHost 的默认值比 WebApplication 少。在第 30 章中,我将更详细地描述其中的一些差异,并演示如何使用泛型 IHost 而不是 WebApplication 来配置应用程序。

So far in this chapter, we’ve looked at the simplest ASP.NET core application you can build: a Hello World minimal API application. For the remainder of this chapter, we’re going to build on this app to introduce some fundamental concepts of ASP.NET Core.‌

到目前为止,在本章中,我们已经了解了您可以构建的最简单的 ASP.NET 核心应用程序:Hello World 最小 API 应用程序。在本章的其余部分,我们将在此应用程序的基础上介绍 ASP.NET Core 的一些基本概念。

3.7 Adding functionality to your application‌

3.7 向应用程序添加功能

The application setup you’ve seen so far in Program.cs consists of only four lines of code but still shows the overall structure of a typical ASP.NET Core app entry point, which typically consists of six steps:

到目前为止,您在 Program.cs 中看到的应用程序设置仅包含四行代码,但仍显示了典型的 ASP.NET Core 应用程序入口点的整体结构,通常包括六个步骤:

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

  2. Register the required services and configuration with the WebApplicationBuilder.
    向 WebApplicationBuilder 注册所需的服务和配置。

  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 basic minimal API app shown previously in listing 3.3 was simple enough that it didn’t need steps 2 and 4, but otherwise it followed this sequence in its Program.cs file. The following listing extends the default application to add more functionality, and in doing so it uses all six steps.
前面清单 3.3 中所示的基本最小 API 应用程序非常简单,它不需要步骤 2 和 4,但除此之外,它在其 Program.cs 文件中遵循此顺序。下面的清单扩展了默认应用程序以添加更多功能,在此过程中,它使用了所有 6 个步骤。

Listing 3.4 The Program.cs file for a more complex example minimal API
列表 3.4 更复杂的示例最小 API 的 Program.cs 文件

using Microsoft.AspNetCore.HttpLogging;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(opts =>  opts.LoggingFields = HttpLoggingFields.RequestProperties);    ❶

builder.Logging.AddFilter( "Microsoft.AspNetCore.HttpLogging", LogLevel.Information);                  ❷

WebApplication app = builder.Build();

if (app.Environment.IsDevelopment())                                                                   ❸
{
    app.UseHttpLogging();                                                                              ❹
}

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

app.Run();

public record Person(string FirstName, string LastName);                                               ❻

❶ You can customize features by adding or customizing the services of the application.
您可以通过添加或自定义应用程序的服务来自定义功能。

❷ Ensures that logs added by the HTTP logging middleware are visible in the log output
确保 HTTP 日志记录中间件添加的日志在日志输出中可见

❸ You can add middleware conditionally, depending on the runtime environment.
您可以根据运行时环境有条件地添加中间件。

❹ The HTTP logging middleware logs each request to your application in the log output.
HTTP 日志记录中间件在日志输出中记录对应用程序的每个请求。

❺ Creates a new endpoint that returns the C# object serialized as JSON
创建一个新端点,该端点返回序列化为 JSON 的 C# 对象

❻ Creates a record type
创建记录类型

The application in listing 3.4 configures two new features:

清单 3.4 中的应用程序配置了两个新功能

  • When running in the Development environment, details about each request are logged using the HttpLoggingMiddleware.4
    在 Development 环境中运行时,将使用 HttpLoggingMiddleware 记录有关每个请求的详细信息。4

  • Creates a new endpoint at /person that creates an instance of the C# record called Person and serializes it in the response as JSON.
    在 /person 处创建一个新端点,该端点创建名为 Person 的 C# 记录实例,并在响应中将其序列化为 JSON。

When you run the application and send requests via a web browser, you see details about the request displayed in the console, as shown in figure 3.8. If you call the /person endpoint you’ll see the JSON representation of the Person record you created in the endpoint.

当您运行应用程序并通过 Web 浏览器发送请求时,您会在控制台中看到有关请求的详细信息,如图 3.8 所示。如果您调用 /person 终端节点,您将看到您在终端节点中创建的 Person 记录的 JSON 表示形式。

NOTE You can view the application only on the same computer that’s running it at the moment; your application isn’t exposed to the internet yet. You’ll learn how to publish and deploy your application in chapter 27.
注意 您只能在当前运行应用程序的同一台计算机上查看该应用程序;您的应用程序尚未暴露在 Internet 上。您将在第 27 章中学习如何发布和部署应用程序。

alt text

Figure 3.8 Calling the /person endpoint returns a JSON- serialized version of the Person record instance. Details about each request are logged to the console by the HttpLoggingMiddleware.
图 3.8 调用 /person 端点会返回 Person 记录实例的 JSON 序列化版本。有关每个请求的详细信息由 HttpLoggingMiddleware 记录到控制台中。

Configuring services, logging, middleware, and endpoints is fundamental to building ASP.NET Core applications, so the rest of section 3.7 walks you through each of these concepts to give you a taste of how they’re used. I won’t explain them in detail (we have the rest of the book for that!), but you should keep in mind how they follow on from each other and how they contribute to the application’s configuration as a whole.

配置服务、日志记录、中间件和端点是构建 ASP.NET Core 应用程序的基础,因此第 3.7 节的其余部分将引导您了解这些概念中的每一个,以便您了解它们的使用方式。我不会详细解释它们(我们还有本书的其余部分来解释),但您应该记住它们如何相互跟进,以及它们如何为整个应用程序的配置做出贡献。

3.7.1 Adding and configuring services‌

3.7.1 添加和配置服务

ASP.NET Core uses small modular components for each distinct feature. This approach allows individual features to evolve separately, with only a loose coupling to others, and it’s generally considered to be good design practice. The downside to this approach is that it places the burden on the consumer of a feature to instantiate it correctly. Within your application, these modular components are exposed as one or more services that are used by the application.

ASP.NET Core 为每个不同的功能使用小型模块化组件。这种方法允许单个功能单独发展,只与其他功能松散耦合,这通常被认为是良好的设计实践。这种方法的缺点是,它给 Feature 的使用者带来了正确实例化它的负担。在您的应用程序中,这些模块化组件将作为应用程序使用的一个或多个服务公开。

DEFINITION Within the context of ASP.Net Core, service refers to any class that provides functionality to an application.
定义 在 ASP.Net Core 的上下文中,service 是指为应用程序提供功能的任何类。

Services could be classes exposed by a library or code you’ve written for your application.

服务可以是由您为应用程序编写的库或代码公开的类。

In an e-commerce app, for example, you might have a TaxCalculator that calculates the tax due on a particular product, taking into account the user’s location in the world. Or you might have a ShippingCostService that calculates the cost of shipping to a user’s location. A third service, OrderTotalCalculator, might use both of these services to work out the total price the user must pay for an order. Each service provides a small piece of independent functionality, but you can combine them to create a complete application. This design methodology scenario is known as the single- responsibility principle.

例如,在电子商务应用程序中,您可能有一个 TaxCalculator,它计算特定产品的应缴税款,同时考虑用户在世界上的位置。或者,您可能有一个 ShippingCostService,用于计算运送到用户位置的费用。第三个服务 OrderTotalCalculator 可能会使用这两个服务来计算用户必须为订单支付的总价。每个服务都提供一小部分独立的功能,但您可以将它们组合起来创建一个完整的应用程序。这种设计方法场景称为单一责任原则。

DEFINITION The single-responsibility principle (SRP) states that every class should be responsible for only a single piece of functionality; it should need to change only if that required functionality changes. SRP is one of the five main design principles promoted by Robert C. Martin in Agile Software Development, Principles, Patterns, and Practices (Pearson, 2013).
定义 单一责任原则 (SRP) 规定,每个类只应负责一项功能;仅当所需的功能发生变化时,才需要更改。SRP 是 Robert C. Martin 在 Agile Software 中推广的五个主要设计原则之一发展、原则、模式和实践(皮尔逊,2013 年)。

OrderTotalCalculator needs access to an instance of ShippingCostService and TaxCalculator. A naive approach to this problem is to use the new keyword and create an instance of a service whenever you need it. Unfortunately, this approach tightly couples your code to the specific implementation you’re using and can undo all the good you achieved by modularizing the features in the first place. In some cases, it may break the SRP by making you perform initialization code in addition to using the service you created.

OrderTotalCalculator 需要访问 ShippingCostService 和 TaxCalculator 的实例。解决这个问题的一个天真方法是使用 new 关键字,并在需要时创建一个服务实例。不幸的是,这种方法将您的代码与您正在使用的特定实现紧密耦合,并且可能会抵消您最初通过模块化功能获得的所有好处。在某些情况下,除了使用您创建的服务之外,它还会让您执行初始化代码,从而破坏 SRP。

One solution to this problem is to make it somebody else’s problem. When writing a service, you can declare your dependencies and let another class fill those dependencies for you. Then your service can focus on the functionality for which it was designed instead of trying to work out how to build its dependencies.

这个问题的一个解决方案是让它成为别人的问题。在编写服务时,您可以声明您的依赖项,并让另一个类为您填充这些依赖项。然后,您的服务可以专注于它所设计的功能,而不是尝试弄清楚如何构建其依赖项。

This technique is called dependency injection or the Inversion of Control (IoC) principle, a well-recognized design pattern that is used extensively. Typically, you’ll register the dependencies of your application into a container, which you can use to create any service. You can use the container to create both your own custom application services and the framework services used by ASP.NET Core. You must register each service with the container before using it in your application.‌

这种技术称为依赖关系注入或控制反转 (IoC) 原则,这是一种被广泛使用且公认的设计模式。通常,您会将应用程序的依赖项注册到容器中,您可以使用该容器创建任何服务。您可以使用容器创建自己的自定义应用程序服务和 ASP.NET Core 使用的框架服务。您必须先在容器中注册每个服务,然后才能在应用程序中使用它。

NOTE I describe the dependency inversion principle and the IoC container used in ASP.NET Core in detail in chapters 8 and 9.
注意:我在第 8 章和第 9 章中详细介绍了 ASP.NET Core 中使用的依赖反转原则和 IoC 容器。

In an ASP.NET Core application, this registration is performed by using the Services property of WebApplicationBuilder.

在 ASP.NET Core 应用程序中,此注册是使用 WebApplicationBuilder 的 Services 属性执行的。

Whenever you use a new ASP.NET Core feature in your application, you need to come back to Program.cs and add the necessary services. This task isn’t always as arduous as it sounds, typically requiring only a line or two of code to configure your applications.

每当在应用程序中使用新的 ASP.NET Core 功能时,都需要返回 Program.cs 并添加必要的服务。这项任务并不总是像听起来那么艰巨,通常只需要一两行代码来配置您的应用程序。

In listing 3.4 we configured an optional service for the HTTP logging middleware by using the line
在清单 3.4 中,我们使用

builder.Services.AddHttpLogging(opts =>
    opts.LoggingFields = HttpLoggingFields.RequestProperties);

Calling AddHttpLogging() adds the necessary services for the HTTP logging middleware to the IoC container and customizes the options used by the middleware for what to display.

调用 AddHttpLogging() 会将 HTTP 日志记录中间件的必要服务添加到 IoC 容器中,并自定义中间件用于显示内容的选项。

AddHttpLogging isn’t exposed directly on the Services property; it’s an extension method that provides a convenient way to encapsulate all the code required to set up HTTP logging. This pattern of encapsulating setup behind extension methods is common in ASP.NET Core.

AddHttpLogging 不会直接在服务上公开财产;它是一种扩展方法,提供了一种便捷的方式来封装设置 HTTP 日志记录所需的所有代码。这种将设置封装在扩展方法后面的模式在 ASP.NET Core 中很常见。

As well as registering framework-related services, the Services property is where you’d register any custom services you have in your application, such as the example TaxCalculator discussed previously. The Services property is an IServiceCollection, which is a list of every known service that your application will need to use. By adding a new service to it, you ensure that whenever a class declares a dependency on your service, the IoC container will know how to provide it.

除了注册与框架相关的服务外,您还可以在 Services 属性中注册应用程序中的任何自定义服务,例如前面讨论的示例 TaxCalculator。Services 属性是一个 IServiceCollection,它是应用程序需要使用的每个已知服务的列表。通过向其添加新服务,您可以确保每当类声明对您的服务的依赖项时,IoC 容器将知道如何提供它。

As well as configuring services, WebApplicationBuilder is where you customize other cross-cutting concerns, such as logging. In listing 3.4, I showed how you can add a logging filter to ensure that the logs generated by the HttpLoggingMiddleware are written to the console:

除了配置服务之外,WebApplicationBuilder 还可以自定义其他横切关注点,例如日志记录。在列表 3.4 中,我展示了如何添加日志过滤器确保HttpLoggingMiddleware 写入控制台:

builder.Logging.AddFilter(
    "Microsoft.AspNetCore.HttpLogging", LogLevel.Information);

This line ensures that logs of severity Information or greater created in the Microsoft .AspNetCore.HttpLogging namespace will be included in the log output.

此行可确保在 Microsoft 中创建严重性 Information 或更高级别的日志。AspNetCore.HttpLogging 命名空间将包含在日志输出中。

NOTE I show configuring log filters in code here for convenience, but this isn’t the idiomatic approach for configuring filters in ASP.NET Core. Typically, you control which levels are shown by adding values to appsettings.json instead, as shown in the source code accompanying this chapter. You’ll learn more about logging and log filtering in chapter 26.
注意 为方便起见,我在此处演示了在代码中配置日志筛选器,但这不是在 ASP.NET Core 中配置筛选器的惯用方法。通常,您可以通过向 Levels 添加值来控制显示哪些级别 appsettings.json 如本章随附的源代码所示。您将在第 26 章中了解有关日志记录和日志过滤的更多信息。

After you call Build() on the WebApplicationBuilder instance, you can’t register any more services or change your logging configuration; the services defined for the WebApplication instance are set in stone. The next step is defining how your application responds to HTTP requests.‌
在 WebApplicationBuilder 实例上调用 Build() 后,您将无法再注册任何其他服务或更改日志记录配置;为 WebApplication 实例定义的服务是一成不变的。下一步是定义应用程序如何响应 HTTP 请求。‌

3.7.2 Defining how requests are handled with middleware and endpoints‌
3.7.2 定义如何使用中间件和终端节点处理请求

After registering your services with the IoC container on WebApplicationBuilder and doing any further customization, you create a WebApplication instance. You can do three main things with the WebApplication instance:

在 WebApplicationBuilder 上向 IoC 容器注册服务并执行任何进一步的自定义后,您将创建一个 WebApplication 实例。您可以使用 WebApplication 实例执行三项主要作:

  • Add middleware to the pipeline.
    将中间件添加到管道中。

  • Map endpoints that generate a response for a request.
    映射为请求生成响应的终端节点。

  • Run the application by calling Run().
    通过调用 Run() 运行应用程序。

As I described previously, middleware consists of small components that execute in sequence when the application receives an HTTP request. They can perform a host of functions, such as logging, identifying the current user for a request, serving static files, and handling errors. Middleware is typically added to WebApplication by calling Use* extension methods. In listing 3.4, I showed an example of adding the HttpLoggingMiddleware to the middleware pipeline conditionally by calling UseHttpLogging():

如前所述,中间件由一些小组件组成,当应用程序收到 HTTP 请求时,这些小组件会按顺序执行。它们可以执行许多功能,例如日志记录、识别请求的当前用户、提供静态文件以及处理错误。中间件通常通过调用 Use* 扩展方法添加到 WebApplication。在示例 3.4 中,我展示了一个通过调用 UseHttpLogging() 有条件地将 HttpLoggingMiddleware 添加到中间件管道的示例:

if (app.Environment.IsDevelopment())
{
    app.UseHttpLogging();
}

We added only a single piece of middleware to the pipeline in this example, but when you’re adding multiple pieces of middleware, the order of the Use* calls is important: the order in which they’re added to the builder is the order in which they’ll execute in the final pipeline. Middleware can use only objects created by previous middleware in the pipeline; it can’t access objects created by later middleware.

在此示例中,我们只向管道添加了一个中间件,但是当您添加多个中间件时,Use* 调用的顺序很重要:它们添加到构建器的顺序就是它们在最终管道中的执行顺序。中间件只能使用管道中先前中间件创建的对象;它无法访问由更高版本的 middleware 创建的对象。

WARNING It’s important to consider the order of middleware when adding it to the pipeline, as middleware can use only objects created earlier in the pipeline.
警告 将中间件添加到管道时,请务必考虑中间件的顺序,因为中间件只能使用在管道中较早创建的对象。

You should also note that listing 3.4 uses the WebApplication.Environment property (an instance of IWebHostEnvironment) to provide different behavior when you’re in a development environment. The HttpLoggingMiddleware is added to the pipeline only when you’re running in development; when you’re running in production (or, rather, when EnvironmentName is not set to "Development"), the HttpLoggingMiddleware will not be added.

您还应该注意,清单 3.4 使用 WebApplication.Environment 属性(IWebHostEnvironment 的一个实例)在开发环境中提供不同的行为。这HttpLoggingMiddleware 仅在你在开发中运行时才会添加到管道中;当您在生产环境中运行时(或者更确切地说,当 EnvironmentName 未设置为 “Development” 时),将不会添加 HttpLoggingMiddleware。

NOTE You’ll learn about hosting environments and how to change the current environment in chapter 10.
注意: 您将在第 10 章中了解托管环境以及如何更改当前环境。

The WebApplicationBuilder builds an IWebHostEnvironment object and sets it on the Environment property. IWebHostEnvironment exposes several environment-related properties, such as‌

WebApplicationBuilder 构建一个 IWebHostEnvironment 对象,并在 Environment 属性上设置它。IWebHostEnvironment 公开了几个与环境相关的属性,例如

  • ContentRootPath—Location of the working directory for the app, typically the folder in which the application is running
    ContentRootPath - 应用程序工作目录的位置,通常是运行应用程序的文件夹

  • WebRootPath—Location of the wwwroot folder that contains static files
    WebRootPath — 包含静态文件的 wwwroot 文件夹的位置

  • EnvironmentName—Whether the current environment is a development or production environment
    EnvironmentName - 当前环境是开发环境还是生产环境

IWebHostEnvironment is already set by the time the WebApplication instance is created. EnvironmentName is typically set externally by using an environment variable when your application starts.

IWebHostEnvironment 在创建 WebApplication 实例时已设置。 EnvironmentName 通常是在应用程序启动时使用环境变量在外部设置的。

Listing 3.4 added only a single piece of middleware to the pipeline, but WebApplication automatically adds more middleware, including two of the most important and substantial pieces of middleware in the pipeline: the routing middleware and the endpoint middleware. The routing middleware is added automatically to the start of the pipeline, before any of the additional middleware added in Program.cs (so before the HttpLoggingMiddleware). The endpoint middleware is added to the end of the pipeline, after all the other middleware added in Program.cs.

清单 3.4 只向管道添加了一个中间件,但 WebApplication 自动添加了更多中间件,包括管道中两个最重要和最重要的中间件部分:路由中间件和端点 中间件。路由中间件会自动添加到管道的开头,在 Program.cs 中添加的任何其他中间件之前(因此在 HttpLoggingMiddleware 之前)。端点中间件将添加到管道的末尾,在 Program.cs 中添加所有其他中间件之后。

NOTE WebApplication adds several more pieces of middleware to the pipeline by default. It automatically adds error-handling middleware when you’re running in the development environment, for example. I discuss some of this autoadded middleware in detail in chapter 4.
注意: 默认情况下,WebApplication 会向管道添加更多的中间件。例如,当您在开发环境中运行时,它会自动添加错误处理中间件。我在第 4 章中详细讨论了一些自动添加的中间件。

Together, this pair of middleware is responsible for interpreting the request to determine which endpoint to invoke, for reading parameters from the request, and for generating the final response. For each request, the routing middleware uses the request’s URL to determine which endpoint to invoke. Then the rest of the middleware pipeline executes until the request reaches the endpoint middleware, at which point the endpoint middleware executes the endpoint to generate the final response.

这对中间件共同负责解释请求以确定要调用的终端节点、从请求中读取参数以及生成最终响应。对于每个请求,路由中间件使用请求的 URL 来确定要调用的终端节点。然后,中间件管道的其余部分执行,直到请求到达终端节点中间件,此时终端节点中间件执行终端节点以生成最终响应。

The routing and endpoint middleware work in tandem, using the set of endpoints defined for your application. In listing 3.4 we defined two endpoints:

路由和终端节点中间件使用为您的应用程序定义的终端节点集协同工作。在清单 3.4 中,我们定义了两个端点:

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

You’ve already seen the default "Hello World!" endpoint. When you send a GET request to /, the routing middleware selects the "Hello World!" endpoint. The request continues down the middleware pipeline until it reaches the endpoint middleware, which executes the lambda and returns the string value in the response body.

您已经看到了默认的 “Hello World!” 端点。当您向 / 发送 GET 请求时,路由中间件会选择 “Hello World!” 端点。请求继续沿中间件管道向下移动,直到到达终端节点middleware,它执行 lambda 并返回字符串值。

The other endpoint defines a lambda to run for GET requests to the /person path, but it returns a C# record instead of a string. When you return a C# object from a minimal API endpoint, the object is serialized to JSON automatically and returned in the response body, as you saw in figure 3.8. In chapter 6 you’ll learn how to customize this response, as well as return other types of responses.

另一个终端节点定义一个 lambda,用于对 /person 路径的 GET 请求运行,但它返回 C# 记录而不是字符串。当您从最小 API 端点返回 C# 对象时,该对象会自动序列化为 JSON 并在响应正文中返回,如图 3.8 所示。在第 6 章中,您将学习如何自定义此响应,以及返回其他类型的响应。

And there you have it. You’ve finished the tour of your first ASP.NET Core application! Before we move on, let’s take one last look at how our application handles a request. Figure 3.9 shows a request to the /person 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 endpoint executes the lambda method and generates the JSON 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 应用程序的浏览!在继续之前,让我们最后看一下我们的应用程序如何处理请求。图 3.9 显示了对示例应用程序正在处理的 /person 路径的请求。您已经在这里看到了所有内容,因此处理请求的过程应该很熟悉。该图显示了请求在由终端节点中间件处理之前如何通过中间件管道。终端节点执行 lambda 方法并生成 JSON 响应,该响应通过中间件传回 ASP.NET Core Web 服务器,然后再发送到用户的浏览器。

alt text

Figure 3.9 An overview of a request to the /person URL for the extended ASP.NET Core minimal API application. The routing middleware routes the request to the correct lambda method. The endpoint generates a JSON response by executing the method and passes the response back through the middleware pipeline to the browser.
图 3.9 对扩展 ASP.NET Core 最小 API 应用程序的 /person URL 的请求概述。路由中间件将请求路由到正确的 lambda 方法。终端节点通过执行该方法生成 JSON 响应,并通过中间件管道将响应传递回浏览器。

The trip has been pretty intense, but now you have a good overview of how an entire application is configured and how it handles a request by using minimal APIs. In chapter 4, you’ll take a closer look at the middleware pipeline that exists in all ASP.NET Core applications. You’ll learn how it’s composed, how you can use it to add functionality to your application, and how you can use it to create simple HTTP services.

这次旅行非常紧张,但现在您已经很好地了解了整个应用程序的配置方式以及它如何使用最少的 API 处理请求。在第 4 章中,您将仔细研究所有 ASP.NET Core 应用程序中存在的中间件管道。您将了解它是如何编写的,如何使用它来向应用程序添加功能,以及如何使用它来创建简单的 HTTP 服务。

Summary

总结

The .csproj file contains the details of how to build your project, including which NuGet packages it depends on. Visual Studio and the .NET CLI use this file to build your application.
.csproj 文件包含有关如何生成项目的详细信息,包括它所依赖的 NuGet 包。Visual Studio 和 .NET CLI 使用此文件构建应用程序。

Restoring the NuGet packages for an ASP.NET Core application downloads all your project’s dependencies so that it can be built and run.
还原 ASP.NET Core 应用程序的 NuGet 包会下载项目的所有依赖项,以便可以生成和运行它。

Program.cs is where you define the code that runs when your app starts. You can create a WebApplicationBuilder by using WebApplication.CreateBuilder() and call methods on the builder to create your application.
Program.cs 是您定义应用程序启动时运行的代码的位置。您可以使用 WebApplication.CreateBuilder() 创建 WebApplicationBuilder,并在生成器上调用方法来创建应用程序。

All services, both framework and custom application services, must be registered with the WebApplicationBuilder by means of the Services property, to be accessed later in your application.
所有服务(包括框架服务和自定义应用程序服务)都必须通过 Services 属性向 WebApplicationBuilder 注册,以便稍后在应用程序中访问。

After your services are configured you call Build() on the WebApplicationBuilder instance to create a WebApplication instance. You use WebApplication to configure your app’s middleware pipeline, to register the endpoints, and to start the server listening for requests.
配置服务后,在 WebApplicationBuilder 实例上调用 Build() 以创建 WebApplication 实例。您可以使用 WebApplication 配置应用程序的中间件管道、注册终端节点以及启动服务器侦听请求。

Middleware defines how your application responds to requests. The order in which middleware is registered defines the final order of the middleware pipeline for the application.
中间件定义应用程序如何响应请求。中间件的注册顺序定义了应用程序的中间件管道的最终顺序。

The WebApplication instance automatically adds RoutingMiddleware to the start of the middleware pipeline and EndpointMiddleware as the last middleware in the pipeline.
WebApplication 实例自动将 RoutingMiddleware 添加到中间件管道的开头,并将 EndpointMiddleware 作为管道中的最后一个中间件。

Endpoints define how a response should be generated for a given request and are typically tied to a request’s path. With minimal APIs, a simple function is used to generate a response.
终端节点定义应如何为给定请求生成响应,并且通常与请求的路径相关联。使用最少的 API,使用一个简单的函数来生成响应。

You can start the web server and begin accepting HTTP requests by calling Run on the WebApplication instance.
您可以通过在 WebApplication 实例上调用 Run 来启动 Web 服务器并开始接受 HTTP 请求。


  1. If you want to learn more about Kestrel, IIS HTTP Server, and HTTP.sys, this documentation describes the differences among them: http://mng.bz/6DgD.
    如果您想了解有关 Kestrel、IIS HTTP Server 和 HTTP.sys 的更多信息,本文档介绍了它们之间的区别:http://mng.bz/6DgD

  2. You can install the development certificate in Windows and macOS. For instructions on trusting the certificate on Linux, see your distribution’s instructions. Not all browsers (Mozilla Firefox, for example) use the certificate store, so follow your browser’s guidelines for trusting the certificate. If you still have difficulties, see the troubleshooting tips at http://mng.bz/o1pr.
    您可以在 Windows 和 macOS 中安装开发证书。有关在 Linux 上信任证书的说明,请参阅您的分配的说明。并非所有浏览器(例如 Mozilla Firefox)都使用证书存储,因此请遵循浏览器的信任证书指南。如果您仍遇到困难,请参阅 http://mng.bz/o1pr 中的故障排除提示。

  3. You can read about the new C# features included in .NET 7 and C# 11 at http://mng.bz/nWMg.
    您可以在 http://mng.bz/nWMg 上阅读 .NET 7 和 C# 11 中包含的新 C# 功能。

  4. You can read in more detail about HTTP logging in the documentation at http://mng.bz/QPmw.
    您可以在 http://mng.bz/QPmw 上的文档中阅读有关 HTTP 日志记录的更多详细信息。

ASP.NET Core in Action 2 Understanding ASP.NET Core

Part 1 Getting started with minimal APIs

第 1 部分:开始使用最少的 API

Web applications are everywhere these days, from social media web apps and news sites to the apps on your phone. Behind the scenes, there's almost always a server running a web application or an HTTP API. Web applications are expected to be infinitely scalable, deployed to the cloud, and highly performant. Getting started can be overwhelming at the best of times, and doing so with such high expectations can be even more of a challenge.

如今,Web 应用程序无处不在,从社交媒体 Web 应用程序和新闻网站到手机上的应用程序。在幕后,几乎总是有服务器运行 Web 应用程序或 HTTP API。Web 应用程序应具有无限可扩展性、部署到云中和高性能。在最好的情况下,开始可能会让人不知所措,而带着如此高的期望这样做可能是一个更大的挑战。

The good news for you as a reader is that ASP.NET Core was designed to meet those requirements. Whether you need a simple website, a complex e-commerce web app, or a distributed web of microservices, you can use your knowledge of ASP.NET Core to build lean web apps that fit your needs. ASP.NET Core lets you build and run web apps in Windows, Linux, or macOS. It's highly modular, so you use only the components you need, keeping your app as compact and performant as possible.

对于读者来说,好消息是 ASP.NET Core 旨在满足这些要求。无论您需要简单的网站、复杂的电子商务 Web 应用程序还是微服务的分布式 Web,您都可以利用自己的 ASP.NET Core 知识来构建符合您需求的精益 Web 应用程序。ASP.NET Core 允许您在 Windows、Linux 或 macOS 中构建和运行 Web 应用程序。它是高度模块化的,因此您只需使用所需的组件,从而使您的应用程序尽可能紧凑和高性能。

In part 1 you'll go from a standing start all the way to building your first API applications. Chapter 2 gives you a high-level overview of ASP.NET Core, which you'll find especially useful if you're new to web development in general. You'll get your first glimpse of a full ASP.NET Core application in chapter 3; we'll look at each component of the app in turn and see how they work together to generate a response.

在第 1 部分中,您将从零开始一直到构建您的第一个 API 应用程序。第 2 章为您提供了 ASP.NET Core 的高级概述,如果您一般是 Web 开发的新手,您会发现它特别有用。在第 3 章中,您将首次了解完整的 ASP.NET Core 应用程序;我们将看看app 中,并查看它们如何协同工作以生成响应。

Chapter 4 looks in detail at the middleware pipeline, which defines how incoming web requests are processed and how a response is generated. We'll look at several standard pieces of middleware and see how they can be combined to create your application's pipeline.

第 4 章详细介绍了中间件管道,它定义了如何处理传入的 Web 请求以及如何生成响应。我们将介绍几个标准的中间件,并了解如何将它们组合起来创建应用程序的管道。

Chapters 5 through 7 focus on building ASP.NET Core apps with minimal API endpoints, which are the new simplified approach to building JSON APIs in ASP.NET Core apps. In chapter 5 you'll learn how to create endpoints that generate JSON, how to use filters to extract common behavior, and how to use route groups to organize your APIs. In chapter 6 you'll learn about routing, the process of mapping URLs to endpoints. And in chapter 7 you'll learn about model binding and validation.

第 5 章到第 7 章重点介绍如何构建具有最少 API 端点的 ASP.NET Core 应用程序,这是在 ASP.NET Core 应用程序中构建 JSON API 的新简化方法。在第 5 章中,您将学习如何创建生成 JSON 的终端节点,如何使用过滤器提取常见行为,以及如何使用路由组来组织 API。在第 6 章中,您将了解路由,即将 URL 映射到端点的过程。在第 7 章中,您将学习模型绑定和验证。

There's a lot of content in part 1, but by the end you'll be well on your way to building simple APIs with ASP.NET Core. Inevitably, I'll gloss over some of the more complex configuration aspects of the framework, but you should get a good understanding of minimal APIs and how you can use them to build simple APIs. In later parts of this book, you'll learn how to configure your application and add extra features, such as user profiles and database interaction.

第 1 部分内容丰富,但到最后,您将顺利使用 ASP.NET Core 构建简单的 API。不可避免地,我将略过框架的一些更复杂的配置方面,但您应该很好地了解最小的 API 以及如何使用它们来构建简单的 API。在本书的后面部分,您将学习如何配置应用程序并添加额外的功能,例如用户配置文件和数据库交互。

We'll also look at how to build other types of applications, such as server-rendered web apps with Razor Pages.

我们还将了解如何构建其他类型的应用程序,例如使用 Razor Pages 构建服务器渲染的 Web 应用程序。

2 Understanding ASP.NET Core

2 了解 ASP.NET Core

This chapter covers

本章涵盖

  • Why ASP.NET Core was created
    创建 ASP.NET Core 的原因

  • The many application paradigms of ASP.NET Core Approaches to migrating an existing application to ASP.NET Core
    ASP.NET Core 的许多应用程序范例将现有应用程序迁移到 ASP.NET Core 的方法

In this chapter, I provide some background on ASP.NET Core: why web frameworks are useful, why ASP.NET Core was created, and how to choose when to use ASP.NET Core. If you’re new to .NET development, this chapter will help you understand the .NET landscape. If you’re already a .NET developer, I provide guidance on whether now is the right time to consider moving your focus to .NET Core and .NET 7, as well as on the advantages ASP.NET Core can offer over previous versions of ASP.NET.

在本章中,我将提供有关 ASP.NET Core 的一些背景知识:为什么 Web 框架很有用,为什么创建 ASP.NET Core,以及如何选择何时使用 ASP.NET Core。如果您不熟悉 .NET 开发,本章将帮助您了解 .NET 的前景。如果您已经是 .NET 开发人员,我将提供指导,说明现在是否是考虑将重点转移到 .NET Core 和 .NET 7 的合适时机,以及 ASP.NET Core 相对于以前版本的 ASP.NET 可以提供的优势。

2.1 Using a web framework‌

2.1 使用 Web 框架

If you’re new to web development, it can be daunting to move into an area with so many buzzwords and a plethora of ever-changing products. You may be wondering whether all those products are necessary. How hard can it be to return a file from a server?

如果您是 Web 开发的新手,那么进入一个拥有如此多流行语和大量不断变化的产品的领域可能会令人生畏。您可能想知道所有这些产品是否都是必要的。从服务器返回文件有多难?

Well, it’s perfectly possible to build a static web application without the use of a web framework, but its capabilities will be limited. As soon as you want to provide any kind of security or dynamism, you’ll likely run into difficulties, and the original simplicity that enticed you will fade before your eyes.

嗯,完全可以在不使用 Web 框架的情况下构建静态 Web 应用程序,但它的功能将受到限制。一旦你想提供任何类型的安全或活力,你可能会遇到困难,最初吸引你的简单性会在你眼前消失。

Just as desktop or mobile development frameworks can help you build native applications, ASP.NET Core makes writing web applications faster, easier, and more secure than trying to build everything from scratch. It contains libraries for common things like

正如桌面或移动开发框架可以帮助您构建原生应用程序一样,ASP.NET Core 使编写 Web 应用程序比尝试从头开始构建所有内容更快、更轻松、更安全。它包含用于常见内容的库,例如

  • Creating dynamically changing web pages Letting users log in to your web app
    创建动态变化的网页 允许用户登录到您的 Web 应用程序

  • Letting users use their Facebook accounts to log in to your web app
    允许用户使用其 Facebook 帐户登录您的 Web 应用程序

  • Providing a common structure for building maintainable applications
    为构建可维护的应用程序提供通用结构

  • Reading configuration files Serving image files
    读取配置文件提供图像文件

  • Logging requests made to your web app
    记录对 Web 应用程序发出的请求

The key to any modern web application is the ability to generate dynamic web pages. A dynamic web page may display different data depending on the current logged-in user, or it could display content submitted by users. Without a dynamic framework, it wouldn’t be possible to log in to websites or to display any sort of personalized data on a page. In short, websites like Amazon, eBay, and Stack

任何现代 Web 应用程序的关键是生成动态网页的能力。动态网页可能会根据当前登录的用户显示不同的数据,也可以显示用户提交的内容。如果没有动态框架,就不可能登录网站或在页面上显示任何类型的个性化数据。简而言之,Amazon、eBay 和 Stack 等网站

Overflow (shown in figure 2.1) wouldn’t be possible. Web frameworks for creating dynamic web pages are almost as old as the web itself, and Microsoft has created several over the years, so why create a new one?

溢出(如图 2.1 所示)是不可能的。用于创建动态网页的 Web 框架几乎与 Web 本身一样古老,Microsoft 多年来已经创建了多个框架,那么为什么要创建一个新的框架呢?

alt text

Figure 2.1 The Stack Overflow website (https://stackoverflow.com) is built with ASP.NET and has almost entirely dynamic content.

图 2.1 Stack Overflow 网站 (https://stackoverflow.com) 是使用 ASP.NET 构建的,几乎完全具有动态内容。

2.2 Why ASP.NET Core was created‌

2.2 创建 ASP.NET Core 的原因

Microsoft’s development of ASP.NET Core was motivated by the desire to create a web framework with five main goals:

Microsoft 开发 ASP.NET Core 的动机是希望创建一个具有五个主要目标的 Web 框架:

  • To be run and developed cross-platform
    跨平台运行和开发

  • To have a modular architecture for easier maintenance
    采用模块化架构,更易于维护

  • To be developed completely as open-source software
    完全作为开源软件开发

  • To adhere to web standards
    遵守 Web 标准

  • To be applicable to current trends in web development, such as client-side applications and deployment to cloud environments
    适用于 Web 开发的当前趋势,例如客户端应用程序和部署到云环境

To achieve all these goals, Microsoft needed a platform that could provide underlying libraries for creating basic objects such as lists and dictionaries, and for performing tasks such as simple file operations. Up to this point, ASP.NET development had always been focused—and dependent—on the Windows-only .NET Framework. For ASP.NET Core, Microsoft created a lightweight platform that runs on Windows, Linux, and macOS called .NET Core (subsequently .NET), as shown in figure 2.2.

为了实现所有这些目标,Microsoft 需要一个平台,该平台可以提供基础库,用于创建基本对象(如列表和字典)以及执行任务(如简单的文件作)。在此之前,ASP.NET 开发始终以仅限 Windows 的 .NET Framework 为中心。对于 ASP.NET Core,Microsoft创建了一个在 Windows、Linux 和 macOS 上运行的轻量级平台,称为 .NET Core(随后.NET),如图 2.2 所示。

alt text

Figure 2.2 The relationships among ASP.NET Core, ASP.NET, .NET Core/.NET 5+, and .NET Framework. ASP.NET Core runs on .NET Core and .NET 5+, so it can run cross-platform. Conversely, ASP.NET runs on .NET Framework only, so it’s tied to the Windows OS.

图 2.2 ASP.NET Core、ASP.NET、.NET Core/.NET 5+ 和 .NET Framework 之间的关系。ASP.NET Core 在 .NET Core 和 .NET 5+ 上运行,因此可以跨平台运行。相反,ASP.NET 仅在 .NET Framework 上运行,因此它与 Windows作系统相关联。

DEFINITION .NET 5 was the next version of .NET Core after 3.1, followed by .NET 6 and .NET 7. It represents a unification of .NET Core and other .NET platforms in a single runtime and framework. It was considered to be the future of .NET.
定义 .NET 5 是 3.1 之后的 .NET Core 的下一个版本,其次是 .NET 6 和 .NET 7。它表示 .NET Core 和其他 .NET 平台在单个运行时和框架中的统一。它被认为是 .NET 的未来。

which is why Microsoft chose to drop the “Core” from its name. For consistency with Microsoft’s language, I use the term .NET 5+ to refer to .NET 5, .NET 6, and .NET 7, and the term .NET Core to refer to previous versions.‌

这就是 Microsoft 选择从其名称中删除“Core”的原因。为了与 Microsoft 的语言保持一致,我使用术语 .NET 5+ 来指代 .NET 5、.NET 6 和 .NET 7,使用术语 .NET Core 来指代以前的版本。

.NET Core (and its successor, .NET 5+) employs many of the same APIs as .NET Framework but is more modular. It implements a different set of features from those in .NET Framework, with the goal of providing a simpler programming model and modern APIs. It’s a separate platform rather than a fork of .NET Framework, though it uses similar code for many of its APIs.

.NET Core(及其后续产品 .NET 5+)采用许多与 .NET Framework 相同的 API,但模块化程度更高。它实现了一组与 .NET Framework 中的功能不同的功能,目的是提供更简单的编程模型和现代 API。它是一个单独的平台,而不是 .NET Framework 的分支,尽管它的许多 API 都使用类似的代码。

NOTE If you’d like to learn more about the .NET ecosystem, you can read two posts on my blog: “Understanding the .NET ecosystem: The evolution of .NET into .NET 7” (http://mng.bz/Ao0W) and “Understanding the .NET ecosystem: The introduction of .NET Standard” (http://mng.bz/ZqPZ).
注意 如果您想了解有关 .NET 生态系统的更多信息,可以阅读我博客上的两篇文章:“了解 .NET 生态系统:.NET 向 .NET 7 的演变”(http://mng.bz/Ao0W 页)和“了解 .NET 生态系统:.NET Standard 简介”(http://mng.bz/ZqPZ 页)。

The benefits and limitations of ASP.NET
ASP.NET 的好处和局限性
ASP.NET Core is the latest evolution of Microsoft’s popular ASP.NET web framework, released in June 2016. Previous versions of ASP.NET had many incremental updates, focusing on high developer productivity and prioritizing backward compatibility. ASP.NET Core bucks that trend by making significant architectural changes that rethink the way the web framework is designed and built.
ASP.NET Core 是 Microsoft 流行的 ASP.NET Web 框架的最新发展版本,于 2016 年 6 月发布。以前的 ASP.NET 版本有许多增量更新,侧重于提高开发人员的工作效率并优先考虑向后兼容性。ASP.NET Core 通过对架构进行重大更改来重新思考 Web 框架的设计和构建方式,从而逆势而上。
ASP.NET Core owes a lot to its ASP.NET heritage, and many features have been carried forward from before, but ASP.NET Core is a new framework.
ASP.NET Core 在很大程度上归功于其 ASP.NET 传统,并且许多功能都从以前继承了下来,但 ASP.NET Core 是一个新框架。
The whole technology stack has been rewritten, including both the web framework and the underlying platform.
整个技术堆栈已被重写,包括 Web 框架和底层平台。
At the heart of the changes is the philosophy that ASP.NET should be able to hold its head high when measured against other modern frameworks, but existing .NET developers should continue to have a sense of familiarity.
这些变化的核心理念是这样一种理念,即 ASP.NET 与其他现代框架相比时,应该能够昂首挺胸,但现有的 .NET 开发人员应该继续保持熟悉感。
To understand why Microsoft decided to build a new framework, it’s important to understand the benefits and limitations of the legacy ASP.NET web framework.
要了解 Microsoft 决定构建新框架的原因,了解旧版 ASP.NET Web 框架的优势和局限性非常重要。
The first version of ASP.NET was released in 2002 as part of .NET Framework 1.0. The ASP.NET Web Forms paradigm that it introduced differed significantly from the conventional scripting environments of classic ASP and PHP. ASP.NET Web Forms allowed developers to create web applications rapidly by using a graphical designer and a simple event model that mirrored desktop application-building techniques.
ASP.NET 的第一个版本于 2002 年作为 .NET Framework 1.0的一部分发布. 它引入的 ASP.NET Web Forms 范例与传统 ASP 和 PHP 的传统脚本环境有很大不同。ASP.NET Web 窗体允许开发人员使用图形设计器和反映桌面应用程序构建技术的简单事件模型快速创建 Web 应用程序。
The ASP.NET framework allowed developers to create new applications quickly, but over time the web development ecosystem changed. It became apparent that ASP.NET Web Forms suffered from many problems, especially in building larger applications. In particular, a lack of testability, a complex stateful model, and limited influence on the generated HTML (making client- side development difficult) led developers to evaluate other options.
ASP.NET 框架允许开发人员快速创建新的应用程序,但随着时间的推移,Web 开发生态系统发生了变化。很明显,ASP.NET Web Forms 存在许多问题,尤其是在构建大型应用程序时。特别是,缺乏可测试性、复杂的有状态模型以及对生成的 HTML 的有限影响(使客户端开发变得困难)导致开发人员评估其他选项。
In response, Microsoft released the first version of ASP.NET MVC in 2009, based on the Model-View-Controller (MVC) pattern, a common web pattern used in frameworks such as Ruby on Rails, Django, and Java Spring. This framework allowed developers to separate UI elements from application logic, made testing easier, and provided tighter control of the HTML- generation process.
作为回应,Microsoft 于 2009 年发布了 ASP.NET MVC 的第一个版本,该版本基于模型-视图-控制器 (MVC) 模式,这是 Ruby on Rails、Django 和 Java Spring 等框架中使用的一种常见 Web 模式。该框架允许开发人员将 UI 元素与应用程序逻辑分离,使测试更容易,并提供对 HTML 生成过程的更严格控制。
ASP.NET MVC has been through four more iterations since its first release, but all these iterations were built on the same underlying framework provided by the System .Web.dll file. This library is part of .NET Framework, so it comes preinstalled with all versions of Windows. It contains all the core code that ASP.NET uses when you build a web application.
ASP.NET MVC 自首次发布以来已经经历了四次迭代,但所有这些迭代都是建立在 System 提供的相同底层框架之上的。Web.dll 文件。此库是 .NET Framework 的一部分,因此它预装在所有版本的 Windows 中。它包含 ASP.NET 在构建 Web 应用程序时使用的所有核心代码。
This dependency brings both advantages and disadvantages. On one hand, the ASP.NET framework is a reliable, battle-tested platform that’s fine for building web applications in Windows. It provides a wide range of features that have been in production for many years, and it’s well known by virtually all Windows web developers.
这种依赖关系既有优点也有缺点。一方面,ASP.NET 框架是一个可靠的、经过实战检验的平台,非常适合在 Windows 中构建 Web 应用程序。它提供了一系列已经投入生产多年的功能,几乎所有 Windows Web 开发人员都知道它。
On the other hand, this reliance is limiting. Changes to the underlying System.Web.dll file are far-reaching and, consequently, slow to roll out, which limits the extent to which ASP.NET is free to evolve and results in release cycles happening only every few years. There’s also an explicit coupling with the Windows web host, Internet Information Services (IIS), which precludes its use on non-Windows platforms.
另一方面,这种依赖是有限的。对基础 System.Web.dll 文件的更改影响深远,因此推出速度很慢,这限制了 ASP.NET 自由发展的程度,并导致发布周期每隔几年才发生一次。此外,还与 Windows Web 主机 Internet Information Services (IIS) 显式耦合,这阻止了它在非 Windows 平台上的使用。
More recently, Microsoft declared .NET Framework to be “done.” It won’t be removed or replaced, but it also won’t receive any new features.
最近,Microsoft 宣布 .NET Framework 已“完成”。它不会被删除或替换,但它也不会获得任何新功能。
Consequently, ASP.NET based on System.Web.dll won’t receive new features or updates either.
因此,基于 System.Web.dll 的 ASP.NET 也不会收到新功能或更新。
In recent years, many web developers have started looking at cross- platform web frameworks that can run on Windows as well as Linux and macOS. Microsoft felt the time had come to create a framework that was no longer tied to its Windows legacy; thus, ASP.NET Core was born.
近年来,许多 Web 开发人员开始寻找可以在 Windows 以及 Linux 和 macOS 上运行的跨平台 Web 框架。Microsoft 认为是时候创建一个不再与其 Windows 传统挂钩的框架了;因此,ASP.NET Core 诞生了。

With .NET 7, it’s possible to build console applications that run cross-platform. Microsoft created ASP.NET Core to be an additional layer on top of console applications so that converting to a web application involves adding and composing libraries, as shown in figure 2.3.

使用 .NET 7,可以构建跨平台运行的控制台应用程序。Microsoft Core 创建了 ASP.NET Core 作为控制台应用程序之上的附加层,因此转换为 Web 应用程序涉及添加和组合库,如图 2.3 所示。

alt text

Figure 2.3 ASP.NET Core application model. The .NET 7 platform provides a base console application model for running command-line apps. Adding a web server library converts this model to an ASP.NET Core web app. You can add other features, such as configuration and logging, using various libraries.

图 2.3 ASP.NET 核心应用程序模型。.NET 7 平台提供了用于运行命令行应用程序的基本控制台应用程序模型。添加 Web 服务器库会将此模型转换为 ASP.NET Core Web 应用程序。您可以使用各种库添加其他功能,例如配置和日志记录。

When you add an ASP.NET Core web server to your .NET 7 app, your console application can run as a web application.

将 ASP.NET Core Web 服务器添加到 .NET 7 应用时,控制台应用程序可以作为 Web 应用程序运行。

ASP.NET Core contains a huge number of APIs, but you’ll rarely need all the features available to you. Some of the features are built in and will appear in virtually every application you create, such as the ones for reading configuration files or performing logging. Other features are provided by separate libraries and built on top of these base capabilities to provide application-specific functionality, such as third-party logins via Facebook or Google.

ASP.NET Core 包含大量 API,但您很少需要所有可用的功能。某些功能是内置的,几乎会出现在您创建的每个应用程序中,例如用于读取配置文件或执行日志记录的应用程序。其他功能由单独的库提供,并基于这些基本功能构建,以提供特定于应用程序的功能,例如通过 Facebook 或 Google 进行第三方登录。

Most of the libraries and APIs you’ll use in ASP.NET Core are available on GitHub, in the Microsoft .NET organization repositories at https://github.com/dotnet/aspnetcore. You can find the core APIs there, including the authentication and logging APIs, as well as many peripheral libraries, such as the third-party authentication libraries.

您将在 ASP.NET Core 中使用的大多数库和 API 都可以在 GitHub 的 Microsoft .NET 组织存储库中找到 https://github.com/dotnet/aspnetcore。您可以在其中找到核心 API,包括身份验证和日志记录 API,以及许多外围库,例如第三方身份验证库。

All ASP.NET Core applications follow a similar design for basic configuration, but in general the framework is flexible, leaving you free to create your own code conventions. These common APIs, the extension libraries that build on them, and the design conventions they promote are covered by the somewhat-nebulous term ASP.NET Core.

所有 ASP.NET Core 应用程序都遵循类似的基本配置设计,但总的来说,该框架是灵活的,让您可以自由创建自己的代码约定。这些常见的 API、基于它们的扩展库以及它们所促进的设计约定都包含在有点模糊的术语 ASP.NET Core 中。

2.3 Understanding the many paradigms of ASP.NET Core‌

2.3 了解 ASP.NET Core 的多种范式

In chapter 1 you learned that ASP.NET Core provides a generalized web framework that can be used to build a wide variety of applications. As you may recall from section 1.2, the main paradigms are

在第 1 章中,您了解了 ASP.NET Core 提供了一个通用的 Web 框架,可用于构建各种应用程序。你可能还记得 1.2 节,主要的范例是

  • Minimal APIs—Simple HTTP APIs that can be consumed by mobile applications or browser-based single-page applications (SPAs)
    最少的 API — 可由移动应用程序或基于浏览器的单页应用程序 (SPA) 使用的简单 HTTP API

  • Web APIs—An alternative approach for building HTTP APIs that adds more structure and features than minimal APIs
    Web API — 一种构建 HTTP API 的替代方法,与最少的 API 相比,它增加了更多的结构和功能

  • gRPC APIs—Used to build efficient binary APIs for server-to-server communication using the gRPC protocol
    gRPC API — 用于使用 gRPC 协议构建高效的二进制 API,以实现服务器到服务器的通信

  • Razor Pages—Used to build page-based server- rendered applications
    Razor Pages - 用于构建基于页面的服务器渲染的应用程序

  • MVC controllers—Similar to Razor Pages; used for server-based applications but without the page- based paradigm
    MVC 控制器 — 类似于 Razor Pages;用于基于服务器的应用程序,但没有基于页面的范例

  • Blazor WebAssembly—A browser-based SPA framework using the WebAssembly standard, similar to JavaScript frameworks such as Angular, React, and Vue
    Blazor WebAssembly - 使用 WebAssembly 标准的基于浏览器的 SPA 框架,类似于 Angular、React 和 Vue 等 JavaScript 框架

  • Blazor Server—Used to build stateful applications, rendered on the server, that send UI events and page updates over WebSockets to provide the feel of a client-side SPA but with the ease of development of a server-rendered application
    Blazor Server - 用于构建在服务器上呈现的有状态应用程序,这些应用程序通过 WebSockets 发送 UI 事件和页面更新,以提供客户端 SPA 的感觉,但易于开发服务器呈现的应用程序

All these paradigms use the core functionality of ASP.NET Core and layer the additional functionality on top. Each paradigm is suited to a different style of web application or API, so some may fit better than others, depending on what sort of application you’re building.

所有这些范例都使用 ASP.NET Core 的核心功能,并将附加功能分层。每种范例都适合不同风格的 Web 应用程序或 API,因此有些可能比其他范例更适合,具体取决于您正在构建的应用程序类型。

Traditional page-based, server-side-rendered web applications are the bread and butter of ASP.NET development, both in the previous version of ASP.NET and now in ASP.NET Core. The Razor Pages and MVC controller paradigms provide two slightly different styles for building these types of applications but have many of the same concepts, as you’ll see in part 2. These paradigms can be useful for building rich, dynamic websites, whether they’re e- commerce sites, content management systems (CMSes), or large n-tier applications. Both the open-source CMS Orchard Core1 (figure 2.4) and cloudscribe2 CMS project, for example, are built with ASP.NET Core.

传统的基于页面、服务器端呈现的 Web 应用程序是 ASP.NET 开发的基础,无论是在以前的 ASP.NET 版本中,还是在现在的 ASP.NET Core 中。Razor Pages 和 MVC 控制器范例为构建这些类型的应用程序提供了两种略有不同的样式,但具有许多相同的概念,您将在第 2 部分中看到。这些范例可用于构建丰富的动态网站,无论它们是电子商务网站、内容管理系统 (CMS) 还是大型 n 层应用程序。例如,开源 CMS Orchard Core1(图 2.4)和 cloudscribe2 CMS 项目都是使用 ASP.NET Core 构建的。

alt text

Figure 2.4 The California School Information Services website (https://csis.fcmat.org) is built with Orchard Core and ASP.NET Core.

图 2.4 California School Information Services 网站 (https://csis.fcmat.org) 是使用 Orchard Core 和 ASP.NET Core 构建的。

In addition to server-rendered applications, ASP.NET core is ideally suited to building a REST or HTTP API server. Whether you’re building a mobile app, a JavaScript SPA using Angular, React, Vue, or some other client-side framework, it’s easy to create an ASP.NET Core application to act as the server-side API by using both the minimal API and web API paradigms built into ASP.NET Core. You’ll learn about minimal APIs in part 1 and about web APIs in chapter 20.

除了服务器渲染的应用程序外,ASP.NET 核心还非常适合构建 REST 或 HTTP API 服务器。无论您是使用 Angular、React、Vue 还是其他客户端框架构建移动应用程序、JavaScript SPA,都可以通过使用 ASP.NET Core 中内置的最小 API 和 Web API 范例轻松创建 ASP.NET Core 应用程序以充当服务器端 API。您将在第 1 部分中了解最小 API,在第 20 章中了解 Web API。

DEFINITION REST stands for representational state transfer. RESTful applications typically use lightweight and stateless HTTP calls to read, post (create/ update), and delete data.

定义 REST 代表 representational state transfer。RESTful 应用程序通常使用轻量级和无状态的 HTTP 调用来读取、发布(创建/更新)和删除数据。

ASP.NET Core isn’t restricted to creating RESTful services. It’s easy to create a web service or remote procedure call (RPC)- style service for your application, using gRPC for example, as shown in figure 2.5. In the simplest case, your application might expose only a single endpoint! ASP.NET Core is perfectly designed for building simple services, thanks to its cross-platform support and lightweight design.

ASP.NET Core 并不局限于创建 RESTful 服务。为您的应用程序创建 Web 服务或远程过程调用 (RPC) 样式的服务很容易,例如使用 gRPC,如图 2.5 所示。在最简单的情况下,您的应用程序可能只公开一个端点!ASP.NET Core 具有跨平台支持和轻量级设计,专为构建简单服务而设计。

DEFINITION gRPC is a modern open-source, high- performance RPC framework. You can read more at https://grpc.io.

定义 gRPC 是一个现代的开源、高性能 RPC 框架。您可以在 https://grpc.io 上阅读更多内容。

alt text

Figure 2.5 ASP.NET Core can act as the server-side application for a variety of clients: it can serve HTML pages for traditional web applications, act as a REST API for client-side SPA applications, or act as an ad hoc RPC service for client applications.

图 2.5 ASP.NET Core 可以充当各种客户端的服务器端应用程序:它可以为传统 Web 应用程序提供 HTML 页面,充当客户端 SPA 应用程序的 REST API,或充当客户端应用程序的临时 RPC 服务。

As well as server-rendered web apps, APIs, and gRPC endpoints, ASP.NET Core includes the Blazor framework, which can be used to build two very different styles of application. Blazor WebAssembly (WASM) apps run directly in your browser, in the same way as traditional JavaScript SPA frameworks such as Angular and React. Your .NET code is compiled to WebAssembly (https://webassembly.org) or executes on a .NET runtime compiled for WASM, and the browser downloads and runs it as it would a JavaScript app.

除了服务器呈现的 Web 应用、API 和 gRPC 终结点外,ASP.NET Core 还包括 Blazor 框架,该框架可用于构建两种截然不同的应用程序样式。Blazor WebAssembly (WASM) 应用直接在浏览器中运行,其方式与传统的 JavaScript SPA 框架(如 Angular 和 React)相同。您的 .NET 代码被编译为 WebAssembly (https://webassembly.org) 或在为 WASM 编译的 .NET 运行时上执行,浏览器会像下载 JavaScript 应用程序一样下载和运行它。

This way you can build highly interactive client-side applications while using C# and all the .NET APIs and libraries you already know.

这样,您就可以在使用 C# 和您已经知道的所有 .NET API 和库的同时构建高度交互的客户端应用程序。

By contrast, Blazor Server applications run on the server. Each mouse click or keyboard event is sent to the server via WebSockets. Then the server calculates the changes that should be made to the UI and sends the required changes back to the client, which updates the page in the browser.

相比之下,Blazor Server 应用程序在服务器上运行。每个鼠标单击或键盘事件都通过 WebSockets 发送到服务器。然后,服务器计算应该对 UI 进行的更改,并将所需的更改发送回客户端,客户端会在浏览器中更新页面。

The result is a “stateful” application that runs server-side but can be used to build highly interactive SPAs. The main downside of Blazor Server is that it requires a constant internet connection.

结果是一个“有状态”应用程序,它在服务器端运行,但可用于构建高度交互的 SPA。Blazor Server 的主要缺点是它需要持续的 Internet 连接。

NOTE In this book I focus on building traditional page-based, server-side- rendered web applications and RESTful web APIs. I also show how to create background worker services in chapter 34. For more information on Blazor, I recommend Blazor in Action, by Chris Sainty (Manning, 2022).‌

注意 在这本书中,我重点介绍了如何构建传统的基于页面、服务器端渲染的 Web 应用程序和 RESTful Web API。我还在 Chapter 34 中展示了如何创建后台 worker 服务。有关 Blazor 的更多信息,我推荐 Chris Sainty 的 Blazor in Action(曼宁,2022 年)。

With the ability to call on all these paradigms, you can use ASP.NET Core to build a wide variety of applications, but it’s still worth considering whether ASP.NET Core is right for your specific application. That decision will likely be affected by both your experience with .NET and the application you want to build.
由于能够调用所有这些范式,您可以使用 ASP.NET Core 构建各种应用程序,但仍然值得考虑 ASP.NET Core 是否适合您特定应用。该决定可能会受到您对 .NET 的体验和要构建的应用程序的影响。

2.4 When to choose ASP.NET Core‌

2.4 何时选择 ASP.NET Core

In this section I’ll describe some of the points to consider when deciding whether to use ASP.NET Core and .NET 7 instead of legacy .NET Framework ASP.NET. In most cases the decision will be to use ASP.NET Core, but you should consider some important caveats.

在本节中,我将介绍在决定是否使用 ASP.NET Core 和 .NET 7 而不是旧版 .NET Framework ASP.NET 时要考虑的一些要点。在大多数情况下,您决定使用 ASP.NET Core,但您应该考虑一些重要的注意事项。

When choosing a platform, you should consider multiple factors, not all of which are technical. One such factor is the level of support you can expect to receive from its creators. For some organizations, limited support can be one of the main obstacles to adopting open-source software. Luckily, Microsoft has pledged to provide full support for Long Term Support (LTS) versions of .NET and ASP.NET Core for at least three years from the time of their release. And as all development takes place in the open, sometimes you can get answers to your questions from the general community as well as from Microsoft directly.

选择平台时,您应该考虑多个因素,并非所有因素都是技术因素。其中一个因素是您可以期望从其创建者那里获得的支持水平。对于某些组织来说,有限的支持可能是采用开源软件的主要障碍之一。幸运的是,Microsoft 已承诺为 .NET 和 ASP.NET Core 的长期支持 (LTS) 版本提供全面支持,从发布之日起至少三年。由于所有开发都是在公开环境中进行的,因此有时您可以从一般社区以及直接从 Microsoft 获得问题的答案。

NOTE You can view Microsoft’s official support policy at http://mng.bz/RxXP.

注意: 您可以在 http://mng.bz/RxXP 查看 Microsoft 的官方支持政策。

When deciding whether to use ASP.NET Core, you have two primary dimensions to consider: whether you’re already a .NET developer and whether you’re creating a new application or looking to convert an existing one.

在决定是否使用 ASP.NET Core 时,您需要考虑两个主要方面:您是否已经是.NET 开发人员,无论您是要创建新应用程序还是希望转换现有应用程序。

2.4.1 If you’re new to .NET development‌

2.4.1 如果您不熟悉 .NET 开发

If you’re new to .NET development, you’re joining at a great time! Many of the growing pains associated with a new framework have been worked out, and the result is a stable, high-performance, cross-platform application framework.

如果你是 .NET 开发的新手,那么加入的时机正好!与新框架相关的许多成长之痛已经解决,结果是一个稳定、高性能、跨平台的应用程序框架。

The primary language of .NET development, and of ASP.NET Core in particular, is C#. This language has a huge following, for good reason! As an object-oriented C-based language, it provides a sense of familiarity to those who are used to C, Java, and many other languages. In addition, it has many powerful features, such as Language Integrated Query (LINQ), closures, and asynchronous programming constructs. The C# language is also designed in the open on GitHub, as is Microsoft’s C# compiler, code-named Roslyn (https://github.com/dotnet/roslyn).

.NET 开发(尤其是 ASP.NET Core 的主要语言)是 C#。这种语言有大量的追随者,这是有充分理由的!作为一种面向对象的基于 C 语言,它为习惯了 C、Java 和许多其他语言的人提供了一种熟悉感。此外,它还具有许多强大的功能,例如语言集成查询 (LINQ)、闭包和异步编程结构。C# 语言也是在 GitHub 上公开设计的,Microsoft 的 C# 编译器,代号为 Roslyn (https://github.com/dotnet/roslyn)。

NOTE I use C# throughout this book and will highlight some of the newer features it provides, but I won’t be teaching the language from scratch. If you want to learn C#, I recommend C# in Depth, 4th ed., by Jon Skeet (Manning, 2019), and Code Like a Pro in C#, by Jort Rodenburg (Manning, 2021).‌‌‌

注意 我在本书中都使用了 C#,并将重点介绍它提供的一些新功能,但我不会从头开始教授这门语言。如果您想学习 C#,我推荐 Jon Skeet 的 C# in Depth,第 4 版(Manning,2019 年)和 Jort Rodenburg 的 Code Like a Pro in C#(Manning,2021 年)。

One big advantage of ASP.NET Core and .NET 7 over .NET Framework is that they enable you to develop and run on any platform. With .NET 7 you can build and run the same application on Mac, Windows, and Linux, and even deploy to the cloud using tiny container deployments.
与 .NET Framework 相比,ASP.NET Core 和 .NET 7 的一大优势是,它们使您能够在任何平台上进行开发和运行。使用 .NET 7,您可以构建和运行相同的Mac、Windows 和 Linux 上的应用程序,甚至可以使用微型容器部署部署到云中。

Built with containers in mind
以容器为构建理念
Traditionally, web applications were deployed directly to a server or, more recently, to a virtual machine. Virtual machines allow operating systems to be installed in a layer of virtual hardware, abstracting away the underlying hardware. This approach has several advantages over direct installation, such as easy maintenance, deployment, and recovery. Unfortunately, virtual machines are also heavy, in terms of both file size and resource use.
传统上,Web 应用程序直接部署到服务器,或者最近部署到虚拟机。虚拟机允许将作系统安装在虚拟硬件层中,从而抽象出底层硬件。与直接安装相比,此方法具有多个优点,例如易于维护、部署和恢复。遗憾的是,虚拟机在文件大小和资源使用方面也很繁重。
This is where containers come in. Containers are far more lightweight and don’t have the overhead of virtual machines. They’re built in a series of layers and don’t require you to boot a new operating system when starting a new one, so they’re quick to start and great for quick provisioning.Containers (Docker in particular) are quickly becoming the go-to platform for building large, scalable systems.
这就是容器的用武之地。容器的轻量级要轻得多,并且没有虚拟机的开销。它们构建在一系列层中,在启动新作系统时不需要启动新作系统,因此它们可以快速启动,非常适合快速配置。容器(尤其是 Docker)正迅速成为构建大型可扩展系统的首选平台。
Containers have never been a particularly attractive option for ASP.NET applications, but with ASP.NET Core, .NET 7, and Docker for Windows, all that is changing. A lightweight ASP.NET Core application running on the cross-platform .NET 7 framework is perfect for thin container deployments. You can learn more about your deployment options in chapter 27.
容器从来都不是 ASP.NET 应用程序特别有吸引力的选择,但随着 ASP.NET Core、.NET 7 和 Docker for Windows 的推出,这一切都发生了变化。在跨平台 .NET 7 框架上运行的轻量级 ASP.NET Core 应用程序非常适合瘦容器部署。您可以在第 27 章中了解有关部署选项的更多信息。

In addition to running on each platform, one of the selling points of .NET is your ability to write and compile only once. Your application is compiled to Intermediate Language (IL) code, which is a platform-independent format. If a target system has the .NET 7 runtime installed, you can run compiled IL from any platform. You can develop on a Mac or a Windows machine, for example, and deploy exactly the same files to your production Linux machines. This compile-once, run-anywhere promise has finally been realized with ASP.NET Core and .NET 7.
除了在每个平台上运行之外,.NET 的卖点之一是您能够只编写和编译一次。您的应用程序被编译为中间语言 (IL) 代码,这是一种与平台无关的格式。如果目标系统安装了 .NET 7 运行时,则可以从任何平台运行编译的 IL。例如,您可以在 Mac 或 Windows 计算机上进行开发,并将完全相同的文件部署到生产 Linux 计算机上。“一次编写,到处运行”的承诺终于在 ASP.NET Core 和 .NET 7 中实现了。

TIP You can go one step further and package the .NET runtime with your app in a so-called self-contained deployment (SCD). This way, you can deploy cross-platform, and the target machine doesn’t even need .NET installed.
With SCDs, the generated deployment files are customized for the target machine, so you’re no longer deploying the same files everywhere in this case.

提示 您可以更进一步,将 .NET 运行时与您的应用程序打包到所谓的自包含部署 (SCD) 中。这样,您可以跨平台部署,并且目标计算机甚至不需要安装 .NET。使用 SCD 时,生成的部署文件会针对目标计算机进行自定义,因此在这种情况下,您不再需要在任何地方部署相同的文件。

Many of the web frameworks available today use similar well-established design patterns, and ASP.NET Core is no different. Ruby on Rails, for example, is known for its use of the MVC pattern; Node.js is known for the way it processes requests using small discrete modules (called a pipeline); and dependency injection is available in a wide variety of frameworks. If these techniques are familiar to you, you should find it easy to transfer them to ASP.NET Core; if they’re new to you, you can look forward to using industry best practices!

当今可用的许多 Web 框架都使用类似的成熟设计模式,ASP.NET Core 也不例外。例如,Ruby on Rails 以其对 MVC 模式的使用而闻名;Node.js 以其使用小型离散模块(称为管道)处理请求的方式而闻名;依赖项注入可用于各种框架。如果您熟悉这些技术,您应该会发现将它们传输到 ASP.NET Core 很容易;如果您对他们不熟悉,您可以期待使用行业最佳实践!

NOTE Design patterns are solutions to common software design problems. You’ll encounter a pipeline in chapter 4, dependency injection in chapters 8 and 9, and MVC in chapter 19.

注意 设计模式是常见软件设计问题的解决方案。您将在第 4 章中遇到管道,在第 8 章和第 9 章中遇到依赖关系注入,在第 19 章中遇到 MVC。

Whether you’re new to web development generally or only with .NET, ASP.NET Core provides a rich set of features with which you can build applications but doesn’t overwhelm you with concepts, as the legacy ASP.NET framework did. On the other hand, if you’re familiar with .NET, it’s worth considering whether now is the time to take a look at ASP.NET Core.

无论你是一般的 Web 开发新手,还是只使用 .NET,ASP.NET Core 都提供了一组丰富的功能,你可以利用这些功能构建应用程序,但不会像旧版 ASP.NET 框架那样被概念所淹没。在另一方面,如果您熟悉 .NET,则值得考虑现在是否是了解 ASP.NET Core 的时候。

2.4.2 If you’re a .NET Framework developer creating a new application‌

2.4.2 如果您是创建新应用程序的 .NET Framework 开发人员

If you’re already a .NET Framework developer, you’ve likely been aware of .NET Core and ASP.NET Core, but perhaps you were wary about jumping in too soon or didn’t want to hit the inevitable “version 1” problems. The good news is that ASP.NET Core and .NET are now mature, stable platforms, and it’s absolutely time to consider using .NET 7 for your new apps.

如果您已经是 .NET Framework 开发人员,您可能已经了解 .NET Core 和 ASP.NET Core,但也许您担心过早加入,或者不想遇到不可避免的“版本 1”问题。好消息是,ASP.NET Core 和 .NET 现在是成熟、稳定的平台,绝对是时候考虑将 .NET 7 用于您的新应用程序了。

As a .NET developer, if you aren’t using any Windows-specific constructs such as the Registry, the ability to build and deploy cross-platform opens the possibility for cheaper Linux hosting in the cloud, or for developing natively in macOS without the need for a virtual machine.

作为 .NET 开发人员,如果您不使用任何特定于 Windows 的结构(如注册表),则构建和部署跨平台的能力为在云中托管 Linux 或无需虚拟机即可在 macOS 中进行本机开发提供了可能性。

.NET Core and .NET 7 are inherently cross-platform, but you can still use platform-specific features if you need to.Windows-specific features such as the Registry and Directory Services, for example, can be enabled with a Compatibility Pack that makes these APIs available in .NET 5+. They’re available only when running .NET 5+ in Windows, not Linux or macOS, so you need to take care that such applications run only in a Windows environment or account for the potential missing APIs.

.NET Core 和 .NET 7 本质上是跨平台的,但如果需要,你仍然可以使用特定于平台的功能。特定于 Windows 的功能(例如,注册表和目录服务)可以通过兼容包启用,使这些 API 在 .NET 5+ 中可用。它们仅在 Windows 中运行 .NET 5+ 时可用,而不是在 Linux 或 macOS 中运行,因此您需要注意此类应用程序仅在 Windows 环境中运行,或者考虑可能缺少的 API。

TIP The Windows Compatibility Pack is designed to help port code from .NET Framework to .NET Core/.NET 5+. See http://mng.bz/2DeX.

提示 Windows 兼容包旨在帮助将代码从 .NET Framework 移植到 .NET Core/.NET 5+。请参阅 http://mng.bz/2DeX

The hosting model for the previous ASP.NET framework was a relatively complex one, relying on Windows IIS to provide the web-server hosting. In a cross-platform environment, this kind of symbiotic relationship isn’t possible, so an alternative hosting model has been adopted—one that separates web applications from the underlying host. This opportunity has led to the development of Kestrel, a fast, cross-platform HTTP server on which ASP.NET Core can run.

以前的 ASP.NET 框架的托管模型相对复杂,依赖 Windows IIS 提供 Web 服务器托管。在跨平台环境中,这种共生关系是不可能的,因此采用了另一种托管模型,即将 Web 应用程序与底层主机分开的模型。这个机会导致了 Kestrel 的开发,这是一种快速、跨平台的 HTTP 服务器,ASP.NET Core 可以在该服务器上运行。

Instead of the previous design, whereby IIS calls into specific points of your application, ASP.NET Core applications are console applications that self-host a web server and handle requests directly, as shown in figure 2.6. This hosting model is conceptually much simpler and allows you to test and debug your applications from the command line, though it doesn’t necessarily remove the need to run IIS (or the equivalent) in production.

与以前的设计不同,IIS 调用应用程序的特定点,ASP.NET Core 应用程序是自托管 Web 服务器并直接处理请求的控制台应用程序,如图 2.6 所示。此托管模型在概念上要简单得多,并且允许您从命令行测试和调试应用程序,尽管它不一定消除在生产中运行 IIS(或等效项)的需要。

ASP.NET Core and reverse proxies
ASP.NET Core 和反向代理
You can expose ASP.NET Core applications directly to the internet so that Kestrel receives requests directly from the network. That approach is fully supported. It’s more common, however, to use a reverse proxy between the raw network and your application. In Windows, the reverse-proxy server typically is IIS; in Linux or macOS, it might be NGINX, HAProxy, or Apache.There’s even an ASP.NET Core-based reverse proxy library called YARP (https://microsoft.github.io/reverse-proxy) that you can use to build your own reverse proxy.
您可以将 ASP.NET Core 应用程序直接公开到 Internet,以便 Kestrel 直接从网络接收请求。这种方法完全受支持。但是,更常见的是在原始网络和应用程序之间使用反向代理。在 Windows 中,反向代理服务器通常是 IIS;在 Linux 或 macOS 中,它可能是 NGINX、HAProxy 或 Apache。甚至还有一个名为 YARP (https://microsoft.github.io/reverse-proxy) 的基于 ASP.NET Core 的反向代理库,您可以使用它来构建自己的反向代理。
A reverse proxy is software responsible for receiving requests and forwarding them to the appropriate web server. The reverse proxy is exposed directly to the internet, whereas the underlying web server is exposed only to the proxy. This setup has several benefits, primarily security and performance for the web servers.
反向代理是负责接收请求并将其转发到适当的 Web 服务器的软件。反向代理直接向 Internet 公开,而底层 Web 服务器仅向代理公开。此设置有几个好处,主要是 Web 服务器的安全性和性能。
You may think that having a reverse proxy and a web server is somewhat redundant. Why not have one or the other? Well, one benefit is the decoupling of your application from the underlying operating system. The same ASP.NET Core web server, Kestrel, can be cross-platform and used behind a variety of proxies without putting any constraints on a particular implementation. Alternatively, if you wrote a new ASP.NET Core web server, you could use it in place of Kestrel without needing to change anything else about your application.
您可能认为拥有反向代理和 Web 服务器有点多余。为什么不拥有一个或另一个呢?嗯,一个好处是将您的应用程序与底层作系统解耦。相同的 ASP.NET Core Web 服务器 Kestrel 可以是跨平台的,并在各种代理后面使用,而不会对特定实现施加任何限制。或者,如果您编写了一个新的 ASP.NET Core Web 服务器,则可以使用它来代替 Kestrel,而无需更改有关应用程序的任何其他内容。
Another benefit of a reverse proxy is that it can be hardened against potential threats from the public internet. Reverse proxies are often responsible for additional aspects, such as restarting a process that has crashed. Kestrel can remain a simple HTTP server, not having to worry about these extra features, when it’s used behind a reverse proxy. You can think of this approach as being a simple separation of concerns: Kestrel is concerned with generating HTTP responses, whereas the reverse proxy is concerned with handling the connection to the internet.
反向代理的另一个好处是它可以针对来自公共互联网的潜在威胁进行强化。反向代理通常负责其他方面,例如重新启动已崩溃的进程。Kestrel 可以保持一个简单的 HTTP 服务器,当它在反向代理后面使用时,不必担心这些额外的功能。您可以将此方法视为简单的关注点分离:Kestrel 关注生成 HTTP 响应,而反向代理关注处理与 Internet 的连接。

alt text

Figure 2.6 The difference between hosting models in ASP.NET (top) and ASP.NET Core (bottom). In the previous version of ASP.NET, IIS is tightly coupled with the application. The hosting model in ASP.NET Core is simpler; IIS hands off the request to a self-hosted web server in the ASP.NET Core application and receives the response but has no deeper knowledge of the application.

图 2.6 在 ASP.NET(上)和 ASP.NET Core 中托管模型(下)之间的区别。在早期版本的 ASP.NET 中,IIS 与应用程序紧密耦合。ASP.NET Core 中的托管模型更简单;IIS 将请求移交给 ASP.NET Core 应用程序中的自托管 Web 服务器并接收响应,但对应用程序没有更深入的了解。

NOTE By default, when running in Windows, ASP.NET Core runs inside IIS, as shown in figure 2.6, which can provide better performance than the reverse-proxy version. This is primarily a deployment detail and doesn’t change the way you build ASP.NET Core applications.

注意 默认情况下,在 Windows 中运行时,ASP.NET Core 在 IIS 中运行,如图 2.6 所示,它可以提供比反向代理版本更好的性能。这主要是部署详细信息,不会改变您构建 ASP.NET Core 应用程序的方式。

Changing the hosting model to use a built-in HTTP web server has created another opportunity. Performance has been something of a sore point for ASP.NET applications in the past. It’s certainly possible to build high-performing applications—Stack Overflow (https://stackoverflow.com) is a testament to that fact—but the web framework itself isn’t designed with performance as a priority, so it can end up being an obstacle.

将托管模型更改为使用内置 HTTP Web 服务器创造了另一个机会。过去,性能一直是 ASP.NET 应用程序的痛点。构建高性能应用程序当然是可能的 — Stack Overflow (https://stackoverflow.com) 证明了这一事实 — 但 Web 框架本身在设计时并未将性能作为优先事项,因此它最终可能会成为一个障碍。

To make the product competitive cross-platform, the ASP.NET team focused on making the Kestrel HTTP server as fast as possible. TechEmpower (https://www.techempower.com/benchmarks) has been running benchmarks on a wide range of web frameworks from various languages for several years now. In round 20 of the plain-text benchmarks, TechEmpower announced that ASP.NET Core with Kestrel was among the 10 fastest of more than 400 frameworks tested!3

为了使产品在跨平台方面具有竞争力,ASP.NET 团队专注于使 Kestrel HTTP 服务器尽可能快。TechEmpower (https://www.techempower.com/benchmarks) 多年来一直在各种语言的各种 Web 框架上运行基准测试。在第 20 轮纯文本基准测试中,TechEmpower 宣布 ASP.NET Core with Kestrel 是测试的 400 多个框架中最快的 10 个框架之一!3

Web servers: Naming things is hard
Web 服务器:命名很困难
One difficult aspect of programming for the web is the confusing array of often-conflicting terminology. If you’ve used IIS, for example, you may have described it as a web server or possibly a web host. Conversely, if you’ve ever built an application with Node.js, you may have also referred to that application as a web server. Or you may have called the physical machine on which your application runs a web server. Similarly, you may have built an application for the internet and called it a website or a web application, probably somewhat arbitrarily based on the level of dynamism it displayed.
Web 编程的一个困难方面是一系列经常相互冲突的令人困惑的术语。例如,如果您使用过 IIS,您可能已将其描述为 Web 服务器或可能是 Web 主机。相反,如果您曾经使用 Node.js 构建过应用程序,您可能也曾将该应用程序称为 Web 服务器。或者,您可能已将运行应用程序的物理计算机称为 Web 服务器。同样,您可能已经构建了一个用于 Internet 的应用程序,并将其称为网站或 Web 应用程序,可能根据它显示的动态水平有点武断。
In this book, when I say web server in the context of ASP.NET Core, I’m referring to the HTTP server that runs as part of your ASP.NET Core application. By default, this server is the Kestrel web server, but that’s not a requirement. It’s possible to write a replacement web server for Kestrel if you so desire.
在本书中,当我在 ASP.NET Core 的上下文中提到 Web 服务器时,我指的是作为 ASP.NET Core 应用程序的一部分运行的 HTTP 服务器。默认情况下,此服务器是 Kestrel Web 服务器,但这不是必需的。如果您愿意,可以为 Kestrel 编写替换 Web 服务器。
The web server is responsible for receiving HTTP requests and generating responses. In the previous version of ASP.NET, IIS took this role, but in ASP.NET Core, Kestrel is the web server.
Web 服务器负责接收 HTTP 请求并生成响应。在早期版本的 ASP.NET 中,IIS 承担了此角色,但在 ASP.NET Core 中,Kestrel 是 Web 服务器。
I’ll use the term web application in this book to describe ASP.NET Core applications, regardless of whether they contain only static content or are dynamic. Either way, these applications are accessed via the web, so that name seems to be the most appropriate.
在本书中,我将使用术语 Web 应用程序来描述 ASP.NET Core 应用程序,无论它们只包含静态内容还是动态的。无论哪种方式,这些应用程序都是通过 Web 访问的,因此该名称似乎是最合适的。

Many of the performance improvements made to Kestrel came not from the ASP.NET team members themselves, but from contributors to the open-source project on GitHub (https://github.com/dotnet/aspnetcore). Developing in the open means that you typically see fixes and features make their way to production faster than you would for the previous version of ASP.NET, which was dependent on .NET Framework and Windows and, as such, had long release cycles.

对 Kestrel 所做的许多性能改进并非来自 ASP.NET 团队成员本身,而是来自 GitHub (https://github.com/dotnet/aspnetcore) 上开源项目的贡献者。以开放方式进行开发意味着您通常会看到修复程序和功能比以前版本的 ASP.NET 更快地进入生产环境,该版本依赖于 .NET Framework 和 Windows,因此具有较长的发布周期。

By contrast, .NET 5+ and hence ASP.NET Core are designed to be released in small increments. Major versions will be released on a predictable cadence, with a new version every year and a new LTS version released every two years (http://mng.bz/1qrg). In addition, bug fixes and minor updates can be released as and when they’re needed.Additional functionality is provided in NuGet packages independent of the underlying .NET 5+ platform.
相比之下,.NET 5+ 和 ASP.NET Core 旨在以小增量发布。主要版本将以可预测的节奏发布,每年发布一个新版本,每两年 (http://mng.bz/1qrg) 发布一个新的 LTS 版本。此外,可以在需要时发布错误修复和次要更新。NuGet 包中提供了独立于基础 .NET 5+ 平台的其他功能。

NOTE NuGet is a package manager for .NET that enables you to import libraries into your projects. It’s equivalent to Ruby Gems, npm for JavaScript, or Maven for Java.

注意: NuGet 是 .NET 的包管理器,可用于将库导入到项目中。它等效于 Ruby Gems、用于 JavaScript 的 npm 或用于 Java 的 Maven。

To enable this approach to releases, ASP.NET Core is highly modular, with as little coupling to other features as possible. This modularity lends itself to a pay-for-play approach to dependencies, where you start with a bare-bones application and add only the libraries you require, as opposed to the kitchen-sink approach of previous ASP.NET applications. Even MVC is an optional package! But don’t worry—this approach doesn’t mean that ASP.NET Core is lacking in features, only that you need to opt into them. Some of the key infrastructure improvements include

为了实现这种发布方法,ASP.NET Core 是高度模块化的,尽可能少地与其他功能耦合。这种模块化性适用于依赖项的付费方法,在这种方法中,您从一个基本的应用程序开始,只添加您需要的库,而不是以前的 ASP.NET 应用程序的厨房接收器方法。甚至 MVC 也是一个可选包!但别担心,这种方法并不意味着 ASP.NET Core 缺少功能,只是您需要选择使用这些功能。一些关键的基础设施改进包括

  • Middleware pipeline for defining your application’s behavior
    用于定义应用程序行为的中间件管道

  • Built-in support for dependency injection
    对依赖项注入的内置支持

  • Combined UI (MVC) and API (web API) infrastructure
    组合的 UI (MVC) 和 API (Web API) 基础设施

  • Highly extensible configuration system Standardized, extensible logging system
    高度可扩展的配置系统 标准化、可扩展的日志记录系统

  • Uses asynchronous programming by default for
    默认情况下使用异步编程

  • built-in scalability on cloud platforms
    云平台上的内置可扩展性

Each of these features was possible in the previous version of ASP.NET but required a fair amount of additional work to set up. With ASP.NET Core, they’re all there, ready and waiting to be connected.
这些功能在以前的 ASP.NET 版本中都是可能的,但需要相当多的额外工作才能建立。有了 ASP.NET Core,它们都已准备就绪,等待连接。

Microsoft fully supports ASP.NET Core, so if you want to build a new system, there’s no significant reason not to use it. The largest obstacle you’re likely to come across is wanting to use programming models that are no longer supported in ASP.NET Core, such as Web Forms or WCF Server, as I’ll discuss in the next section.

Microsoft 完全支持 ASP.NET Core,因此如果您想构建一个新系统,没有明显的理由不使用它。您可能遇到的最大障碍是希望使用 ASP.NET Core 中不再支持的编程模型,例如 Web 窗体或 WCF Server,我将在下一节中讨论。

I hope that this section whetted your appetite to use ASP.NET Core for building new applications. But if you’re an existing ASP.NET developer considering whether to convert an existing ASP.NET application to ASP.NET Core, that’s another question entirely.

我希望本节能激发您使用 ASP.NET Core 构建新应用程序的兴趣。但是,如果你是现有的 ASP.NET 开发人员,正在考虑是否将现有的 ASP.NET 应用程序转换为 ASP.NET Core,那完全是另一个问题。

2.4.3 Converting an existing ASP.NET application to ASP.NET Core‌

2.4.3 将现有 ASP.NET 应用程序转换为 ASP.NET Core

By contrast with new applications, an existing application presumably already provides value, so there should always be a tangible benefit to performing what may amount to a significant rewrite in converting from ASP.NET to ASP.NET Core. The advantages of adopting ASP.NET Core are much the same as those for new applications: cross-platform deployment, modular features, and a focus on performance. Whether the benefits are sufficient will depend largely on the particulars of your application, but some characteristics make conversion more difficult:

与新应用程序相比,现有应用程序可能已经提供了价值,因此在从 ASP.NET 转换为 ASP.NET Core 的过程中执行可能相当于重大重写的作应该始终会带来切实的好处。采用 ASP.NET Core 的优势与新应用程序的优势大致相同:跨平台部署、模块化功能和对性能的关注。好处是否足够在很大程度上取决于您的应用程序的具体情况,但有些特征使转换更加困难:

  • Your application uses ASP.NET Web Forms.
    您的应用程序使用 ASP.NET Web Forms。

  • Your application is built with WCF.
    您的应用程序是使用 WCF 构建的。

  • Your application is large, with many advanced MVC features.
    您的应用程序很大,具有许多高级 MVC 功能。

If you have an ASP.NET Web Forms application, attempting to convert it directly to ASP.NET Core isn’t advisable. Web Forms is inextricably tied to System.Web.dll and IIS, so it will likely never be available in ASP.NET Core. Converting an application to ASP.NET Core effectively involves rewriting the application from scratch, not only shifting frameworks, but also potentially shifting design paradigms.

如果您有 ASP.NET Web Forms 应用程序,则不建议尝试将其直接转换为 ASP.NET Core。Web 表单与 System.Web.dll 和 IIS 有着千丝万缕的联系,因此它可能永远不会在 ASP.NET Core 中提供。将应用程序有效地转换为 ASP.NET Core 涉及从头开始重写应用程序,不仅要改变框架,还可能改变设计范式。

All is not lost, however. Blazor server provides a stateful, component-based application that’s similar to the Web Forms application model. You may be able to gradually migrate your Web Forms application page by page to an ASP.NET Core Blazor server application.4 Alternatively, you could slowly introduce web API concepts into your Web Forms application, reducing the reliance on legacy Web Forms constructs such as ViewState, with the goal of ultimately moving to an ASP.NET Core web API application.

然而,一切都没有失去。Blazor 服务器提供基于组件的有状态应用程序,它类似于 Web Forms 应用程序模型。你可以逐步逐页将 Web 窗体应用程序迁移到 ASP.NET Core Blazor 服务器应用程序。4 或者,您可以慢慢地将 Web API 概念引入 Web 窗体应用程序,从而减少对旧版 Web 窗体构造(如 ViewState)的依赖,最终迁移到 ASP.NET Core Web API 应用程序。

Windows Communication Foundation (WCF) is only partially supported in ASP.NET Core. It’s possible to build client-side WCF services using the libraries provided by ASP.NET Core (https://github.com/dotnet/wcf) and to build server-side WCF services by using the Microsoft-supported community-driven project CoreWCF.5 These libraries don’t support all the APIs available in .NET Framework WCF (distributed transactions and some message security formats, for example), so if you absolutely need those APIs, it may be best to avoid ASP.NET Core for now.

服务,也可以使用 Microsoft 支持的社区驱动项目 CoreWCF 构建服务器端 WCF 服务。5 这些库不支持 .NET Framework WCF 中提供的所有 API(例如,分布式事务和某些消息安全格式),因此,如果您绝对需要这些 API,最好暂时避免使用 ASP.NET Core。

TIP If you like WCF’s contract-based RPC-style of programming but don’t have a hard requirement for WCF itself, consider using gRPC instead. gRPC is a modern RPC framework with many concepts that are similar to WCF, and it’s supported by ASP.NET Core out of the box (http://mng.bz/wv9Q).

提示 如果您喜欢 WCF 基于协定的 RPC 样式编程,但对 WCF 本身没有硬性要求,请考虑改用 gRPC。gRPC 是一个现代 RPC 框架,具有许多与 WCF 类似的概念,并且它由开箱即用的 ASP.NET Core (http://mng.bz/wv9Q) 提供支持。

If your existing application is complex and makes extensive use of the previous MVC or web API extensibility points or message handlers, porting your application to ASP.NET Core may be more difficult. ASP.NET Core is built with many features similar to the previous version of ASP.NET MVC, but the underlying architecture is different. Several of the previous features don’t have direct replacements, so they’ll require rethinking.

如果现有应用程序很复杂,并且广泛使用以前的 MVC 或 Web API 扩展点或消息处理程序,则将应用程序移植到 ASP.NET Core 可能会更加困难。ASP.NET Core 具有许多与以前版本的 ASP.NET MVC 类似的功能,但底层架构不同。前面的几个功能没有直接替换,因此需要重新考虑。

The larger the application is, the greater the difficulty you’re likely to have converting your application to ASP.NET Core.Microsoft itself suggests that porting an application from ASP.NET MVC to ASP.NET Core is at least as big a rewrite as porting from ASP.NET Web Forms to ASP.NET MVC. If that suggestion doesn’t scare you, nothing will!

应用程序越大,将应用程序转换为 ASP.NET Core 的难度就越大。
Microsoft本身表明,将应用程序从 ASP.NET MVC 移植到 ASP.NET Core 至少与从 ASP.NET Web Forms 移植到 ASP.NET MVC 一样大。如果这个建议没有吓到你,那就什么都不会了!

If an application is rarely used, isn’t part of your core business, or won’t need significant development in the near term, I suggest that you don’t try to convert it to ASP.NET Core. Microsoft will support .NET Framework for the foreseeable future (Windows itself depends on it!), and the
payoff in converting these fringe applications is unlikely to be worth the effort.

如果某个应用程序很少使用,不是您的核心业务的一部分,或者在短期内不需要重大开发,我建议您不要尝试将其转换为 ASP.NET Core。Microsoft 将在可预见的将来支持 .NET Framework(Windows 本身依赖于它!),并且

转换这些边缘应用程序的回报不太可能值得付出努力。

So when should you port an application to ASP.NET Core? As I’ve already mentioned, the best opportunity to get started is on small new greenfield projects instead of existing applications. That said, if the existing application in question is small or will need significant future development, porting may be a good option.

那么,何时应将应用程序移植到 ASP.NET Core?正如我已经提到的,最好的开始机会是小型的新绿地项目,而不是现有的应用程序。也就是说,如果现有应用程序很小或未来需要大量开发,则移植可能是一个不错的选择。

It’s always best to work in small iterations if possible when porting an application, rather than attempt to convert the entire application at the same time. Luckily, Microsoft provides tools for that purpose. A set of System.Web adapters, a .NET-based reverse proxy called YARP (Yet Another Reverse Proxy; http://mng.bz/qr92), and tooling built into Visual Studio can help you implement the strangler fig pattern (http://mng.bz/rW6J). This tooling allows you to migrate your application one page/API at a time, reducing the risk associated with porting an ASP.NET application to ASP.NET Core.

在移植应用程序时,如果可能的话,最好在小迭代中工作,而不是尝试同时转换整个应用程序。幸运的是,Microsoft 提供了用于此目的的工具。一组 System.Web 适配器、一个 .基于 NET 的反向代理称为 YARP(Yet Another Reverse Proxy; http://mng.bz/qr92) 和 Visual Studio 中内置的工具可以帮助您实现 strangler fig 模式 (http://mng.bz/rW6J)。借助此工具,您可以一次迁移一个页面/API 的应用程序,从而降低将 ASP.NET 应用程序移植到 ASP.NET Core 的相关风险。

In this chapter, we walked through some of the historical context of ASP.NET Core, as well as some of the advantages of adopting it. In chapter 3, you’ll create your first application from a template and run it. We’ll walk through each of the main components that make up your application and see how they work together to render a web page.‌

在本章中,我们介绍了 ASP.NET Core 的一些历史背景,以及采用它的一些优势。在第 3 章中,您将从模板创建第一个应用程序并运行它。我们将介绍构成应用程序的每个主要组件,并了解它们如何协同工作以呈现网页。

Summary

总结

Web frameworks provide a way to build dynamic web applications easily.
Web 框架提供了一种轻松构建动态 Web 应用程序的方法。

ASP.NET Core is a web framework built with modern software architecture practices and modularization as its focus.
ASP.NET Core 是一个以现代软件架构实践和模块化为重点构建的 Web 框架。

ASP.NET Core runs on the cross-platform .NET 7 platform. You can access Windows-specific features such as the Windows Registry by using the Windows Compatibility Pack.
ASP.NET Core 在跨平台的 .NET 7 平台上运行。您可以使用 Windows 兼容包访问特定于 Windows 的功能,例如 Windows 注册表。

.NET 5, .NET 6, and .NET 7 are the next versions of .NET Core after .NET Core 3.1.
.NET 5、.NET 6 和 .NET 7 是.NET Core 3.1 之后的 .NET Core的下一个版本。

ASP.NET Core is best used for new greenfield projects.
ASP.NET Core 最适合用于新的绿地项目。

Legacy technologies such as WCF Server and Web Forms can’t be used directly with ASP.NET Core, but they have analogues and supporting libraries that can help with porting ASP.NET applications to ASP.NET Core.
WCF Server 和 Web Forms 等旧技术不能直接与 ASP.NET Core 一起使用,但它们具有类似物和支持库,可帮助将 ASP.NET 应用程序移植到 ASP.NET Core。

You can convert an existing ASP.NET application to ASP.NET Core gradually by using the strangler fig pattern, using tooling and libraries provided by Microsoft.
通过使用 strangler fig 模式,您可以使用 Microsoft 提供的工具和库,逐步将现有的 ASP.NET 应用程序转换为 ASP.NET Core。

ASP.NET Core apps are often protected from the internet by a reverse-proxy server, which forwards requests to the application.
ASP.NET Core 应用程序通常由反向代理服务器保护,不受 Internet 的影响,该服务器将请求转发到应用程序。


1.Orchard Core (https://orchardcore.net). Source code at https://github.com/OrchardCMS/OrchardCore.

2.The cloudscribe project (https://www.cloudscribe.com). Source code at https://github.com/cloudscribe.

3.As always in web development, technology is in a constant state of flux, so these benchmarks will evolve over time. Although ASP.NET Core may not maintain its top-10 slot, you can be sure that performance is one of the key focal points of the ASP.NET Core team.

4.There is a community-driven effort to create Blazor versions of common WebForms components (http://mng.bz/PzPP). Also see an e-book for Blazor for Web Forms developers at http://mng.bz/JgDv.

5.You can find the CoreWCF libraries at https://github.com/corewcf/corewcf and details on upgrading a WCF service to .NET 5+ at http://mng.bz/mVg2.

ASP.NET Core in Action 1 Getting started with ASP.NET Core

ASP.NET Core in Action 1 Getting started with ASP.NET Core

1 Getting started with ASP.NET Core

1 ASP.NET Core 入门

This chapter covers

本章涵盖

  • What is ASP.NET Core?
    什么是 ASP.NET Core?

  • Things you can build with ASP.NET Core
    您可以使用 ASP.NET Core 构建的内容

  • How ASP.NET Core works
    ASP.NET Core 的工作原理

Choosing to learn and develop with a new framework is a big investment, so it’s important to establish early on whether it’s right for you. In this chapter, I provide some background on ASP.NET Core: what it is, how it works, and why you should consider it for building your web applications.

选择使用新框架进行学习和开发是一项巨大的投资,因此尽早确定它是否适合您非常重要。在本章中,我将提供有关 ASP.NET Core的一些背景知识:它是什么、它是如何工作的,以及为什么您应该考虑使用它来构建Web应用程序。

By the end of this chapter, you should have a good overview of the benefits of ASP.NET Core, the role of .NET 7, and the basic mechanics of how ASP.NET Core works. So without further ado, let’s dive in!

在本章结束时,您应该对ASP.NET Core的优势、.NET 7的作用以及ASP.NET Core工作原理的基本机制有一个很好的概述。因此,事不宜迟,让我们开始吧!

1.1 What is ASP.NET Core?‌

1.1 什么是 ASP.NET Core?

ASP.NET Core is a cross-platform, open-source application framework that you can use to build dynamic web applications quickly. You can use ASP.NET Core to build server-rendered web applications, backend server applications, HTTP APIs that can be consumed by mobile applications, and much more. ASP.NET Core runs on .NET 7,which is the latest version of .NET Core—a high-performance, cross-platform, open-source runtime.

ASP.NET Core是一个跨平台的开源应用程序框架,可用于快速构建动态Web应用程序。您可以使用ASP.NET Core构建服务器渲染的Web应用程序、后端服务器应用程序、移动应用程序可以使用的HTTP API等等。ASP.NET Core在.NET 7上运行,这是.NET Core的最新版本,是一种高性能、跨平台、开源运行时。

ASP.NET Core provides structure, helper functions, and a framework for building applications, which saves you from having to write a lot of this code yourself. Then the ASP.NET Core framework code calls in to your handlers, which in turn call methods in your application’s business logic, as shown in figure 1.1. This business logic is the core of your application. You can interact with other services here, such as databases or remote APIs, but your business logic typically doesn’t depend directly on ASP.NET Core.

ASP.NET Core提供结构、帮助程序函数和用于构建应用程序的框架,这使您不必自己编写大量此类代码。然后,ASP.NET Core框架代码调用您的处理程序,这些处理程序反过来调用应用程序业务逻辑中的方法,如图1.1所示。此业务逻辑是应用程序的核心。您可以在此处与其他服务(例如数据库或远程 API)进行交互,但您的业务逻辑通常不直接依赖于ASP.NET Core。

alt text

Figure 1.1 A typical ASP.NET Core application consists of several layers. The ASP.NET Core framework code handles requests from a client, dealing with the complex networking code. Then the framework calls in to handlers (Razor Pages and Web API controllers, for example) that you write using primitives provided by the framework. Finally, these handlers call in to your application’s domain logic—typically, C# classes and objects without any dependencies that are specific to ASP.NET Core.
图 1.1 典型的 ASP.NET Core应用程序由多个层组成。ASP.NET Core框架代码处理来自客户端的请求,处理复杂的网络代码。然后,框架调用使用框架提供的基元编写的处理程序(例如 Razor Pages和Web API控制器)。最后,这些处理程序调用应用程序的域逻辑 — 通常是没有任何特定于ASP.NET Core的依赖项的C#类和对象。

1.2 What types of applications can you build?‌

1.2 您可以构建哪些类型的应用程序?

ASP.NET Core provides a generalized web framework that you can use to build a wide variety of applications. ASP.NET Core includes APIs that support many paradigms:

ASP.NET Core 提供了一个通用的 Web 框架,您可以使用它来构建各种应用程序。ASP.NET Core 包含支持许多范例的 API:

  • Minimal APIs—Simple HTTP APIs that can be consumed by mobile applications or browser-based single-page applications.
    最小 API — 可供移动应用程序或基于浏览器的单页应用程序使用的简单 HTTP API。

  • Web APIs—An alternative approach to building HTTP APIs that adds more structure and features than minimal APIs.
    Web API — 一种构建 HTTP API 的替代方法,与最小 API 相比,它增加了更多的结构和功能。

  • gRPC APIs—Used to build efficient binary APIs for server-to-server communication using the gRPC protocol.
    gRPC API — 用于构建高效的二进制 API,以使用 gRPC 协议进行服务器到服务器通信。

  • Razor Pages—Used to build page-based server- rendered applications.
    Razor Pages - 用于构建基于页面的服务器渲染的应用程序。

  • MVC controllers—Similar to Razor Pages. Model- View-Controller (MVC) controller applications are for server-based applications but without the page- based paradigm.
    MVC 控制器 - 类似于 Razor Pages。模型-视图-控制器 (MVC) 控制器应用程序适用于基于服务器的应用程序,但没有基于页面的范例。

  • Blazor WebAssembly—A browser-based single- page application framework that uses the WebAssembly standard, similar to JavaScript frameworks such as Angular, React, and Vue.
    Blazor WebAssembly - 一种基于浏览器的单页应用程序框架,它使用 WebAssembly 标准,类似于 Angular、React 和 Vue 等 JavaScript 框架。

  • Blazor Server—Used to build stateful applications, rendered on the server, that send UI events and page updates over WebSockets to provide the feel of a client-side single-page application, but with the ease of development of a server-rendered application.
    Blazor Server - 用于构建在服务器上呈现的有状态应用程序,这些应用程序通过 WebSockets 发送 UI 事件和页面更新,以提供客户端单页应用程序的感觉,但易于开发服务器呈现的应用程序。

All these paradigms are based on the same building blocks of ASP.NET Core, such as the configuration and logging libraries, and then place extra functionality on top. The best paradigm for your application depends on multiple factors, including your API requirements, the details of existing applications you need to interact with, the details of your customers’ browsers and operating environment, and scalability and uptime requirements. You don’t need to choose only one of these paradigms; ASP.NET Core can combine multiple paradigms within a single application.

所有这些范例都基于ASP.NET Core的相同构建块,例如配置和日志记录库,然后将额外的功能放在顶部。应用程序的最佳范例取决于多种因素,包括 API 要求、需要与之交互的现有应用程序的详细信息、客户的浏览器和作环境的详细信息,以及可扩展性和正常运行时间要求。您不需要只选择这些范例中的一个;ASP.NET Core可以在单个应用程序中组合多个范例。

1.3 Choosing ASP.NET Core‌

1.3 选择 ASP.NET Core

I hope that now you have a general grasp of what ASP.NET Core is and the type of applications you can build with it. But one question remains: should you use it? Microsoft recommends that all new .NET web development use ASP.NET Core, but switching to or learning a new web stack is a big ask for any developer or company.

我希望现在您已经大致了解了 ASP.NET Core是什么以及您可以使用它构建的应用程序类型。但仍然存在一个问题:您应该使用它吗?Microsoft建议所有新的.NET Web开发都使用 ASP.NET Core,但切换到或学习新的 Web 堆栈对任何开发人员或公司来说都是一个很大的要求。

If you’re new to .NET development and are considering ASP.NET Core, welcome! Microsoft is pushing ASP.NET Core as an attractive option for web development beginners, but taking .NET cross-platform means that it’s competing with many other frameworks on their own turf. ASP.NET Core has many selling points compared with other cross-platform web frameworks:

如果你不熟悉.NET开发并正在考虑使用ASP.NET Core,欢迎使用!Microsoft正在推动 ASP.NET Core成为 Web 开发初学者的一个有吸引力的选择,但采用 .NET 跨平台意味着它正在与许多其他框架在自己的地盘上竞争。与其他跨平台Web框架相比,ASP.NET Core有很多卖点:

  • It’s a modern, high-performance, open-source web framework.
    它是一个现代、高性能、开源的 Web 框架。

  • It uses familiar design patterns and paradigms.
    它使用熟悉的设计模式和范例。

  • C# is a great language (but you can use VB.NET or F# if you prefer).
    C# 是一种很好的语言(但如果您愿意,也可以使用 VB.NET 或 F#)。

  • You can build and run on any platform.
    您可以在任何平台上构建和运行。

ASP.NET Core is a reimagining of the ASP.NET framework, built with modern software design principles on top of the new .NET platform. Although it’s new in one sense, .NET (previously called .NET Core) has had widespread production use since 2016 and has drawn significantly from the mature, stable, and reliable .NET Framework, which has been used for more than two decades. You can rest easy knowing that by choosing ASP.NET Core and .NET 7, you’re getting a dependable platform as well as a full-featured web framework.

ASP.NET Core是对 ASP.NET框架的重新构想,在新的 .NET 平台之上采用现代软件设计原则构建。尽管从某种意义上说,.NET(以前称为.NET Core)是新的,但它自2016年以来已被广泛用于生产领域,并且极大地借鉴了已经使用了二十多年的成熟、稳定和可靠的.NET Framework。您可以高枕无忧,因为您知道选择ASP.NET Core和.NET 7将获得一个可靠的平台以及一个功能齐全的Web框架。

One major selling point of ASP.NET Core and .NET 7 is the ability to develop and run on any platform. Whether you’re using a Mac, Windows, or Linux computer, you can run the same ASP.NET Core apps and develop across multiple environments. A wide range of distributions are supportedfor Linux users: RHEL, Ubuntu, Debian, CentOS, Fedora, and openSUSE, to name a few. ASP.NET Core even runs on the tiny Alpine distribution, for truly compact deployments to containers, so you can be confident that your operating system of choice will be a viable option.

ASP.NET Core和.NET 7的一个主要卖点是能够在任何平台上开发和运行。无论您使用的是 Mac、Windows 还是 Linux 计算机,您都可以运行相同的 ASP.NET Core 应用程序并跨多个环境进行开发。支持多种发行版对于 Linux 用户:RHEL、Ubuntu、Debian、CentOS、Fedora 和 openSUSE等。ASP.NET Core甚至在微型Alpine 发行版上运行,以实现真正紧凑的容器部署,因此您可以确信您选择的作系统将是一个可行的选择。

If you’re already a .NET developer, the choice of whether to invest in ASP.NET Core for new applications was largely a question of timing. Early versions of .NET Core lacked some features that made it hard to adopt, but that problem no longer exists in the latest versions of .NET. Now Microsoft explicitly advises that all new .NET applications should use.NET 7 (or newer).

如果您已经是.NET 开发人员,那么选择是否为新应用程序投资 ASP.NET Core在很大程度上是一个时间问题。早期版本的.NET Core缺少一些难以采用的功能,但最新版本的.NET 中不再存在该问题。现在 Microsoft 明确建议所有新的.NET 应用程序都应该使用.NET 7(或更高版本)。

Microsoft has pledged to provide bug and security fixes for the older ASP.NET framework, but it won’t provide any more feature updates. .NET Framework isn’t being removed, so your old applications will continue to work, but you shouldn’t use it for new development.

Microsoft已承诺为较旧的ASP.NET框架提供bug和安全修复,但不会提供更多功能更新。.NET Framework未被删除,因此您的旧应用程序将继续工作,但您不应将其用于新的开发。

The main benefits of ASP.NET Core over the previous ASP.NET framework are

与以前的 ASP.NET 框架相比,ASP.NET Core 的主要优点是

  • Cross-platform development and deployment Focus on performance as a feature
    跨平台开发和部署 专注于性能作为一项功能

  • A simplified hosting model
    简化的托管模型

  • Regular releases with a shorter release cycle Open-source
    发布周期更短的常规版本开源

  • Modular features
    模块化功能

  • More application paradigm options
    更多应用程序范例选项

  • The option to package .NET with an app when publishing for standalone deployments
    在发布独立部署时将 .NET 与应用程序打包的选项

As an existing .NET developer who’s moving to ASP.NET Core, your ability to build and deploy cross-platform opens the door to a whole new avenue of applications, such as taking advantage of cheaper Linux virtual machine hosting in the cloud, using Docker containers for repeatable continuous integration, or writing .NET code on your Mac without needing to run a Windows virtual machine. ASP.NET Core, in combination with .NET 7, makes all this possible.

作为正在迁移到 ASP.NET Core的现有 .NET 开发人员,您构建和部署跨平台的能力为全新的应用程序途径打开了大门,例如利用云中更便宜的 Linux 虚拟机托管,使用 Docker 容器进行可重复的持续集成,或者在 Mac 上编写 .NET 代码而无需运行 Windows 虚拟机。ASP.NET Core 与 .NET 7 相结合,使这一切成为可能。

That’s not to say that your experience deploying ASP.NET applications to Windows and Internet Information Services (IIS) is wasted. On the contrary, ASP.NET Core uses many of the same concepts as the previous ASP.NET framework, and you can still run your ASP.NET Core applications in IIS, so moving to ASP.NET Core doesn’t mean starting from scratch.‌

这并不是说您将 ASP.NET 应用程序部署到 Windows 和 Internet Information Services (IIS) 的经验是浪费的。相反,ASP.NET Core 使用许多与以前的 ASP.NET 框架相同的概念,并且你仍然可以在 IIS 中运行 ASP.NET Core 应用程序,因此迁移到 ASP.NET Core 并不意味着从头开始。

1.4 How does ASP.NET Core work?‌

1.4 ASP.NET Core 的工作原理是什么?

I’ve covered the basics of what ASP.NET Core is, what you can use it for, and why you should consider using it. In this section, you’ll see how an application built with ASP.NET Core works, from a user request for a URL to the display of a page in the browser. To get there, first you’ll see how an HTTP request works for any web server; then you’ll see how ASP.NET Core extends the process to create dynamic web pages.

我已经介绍了 ASP.NET Core 是什么、它可以用于什么以及为什么应该考虑使用它的基础知识。在本节中,您将了解使用 ASP.NET Core 构建的应用程序的工作原理,从用户对 URL 的请求到浏览器中的页面显示。要到达那里,首先您将了解 HTTP 请求如何适用于任何 Web 服务器;然后,您将看到 ASP.NET Core 如何扩展创建动态网页的过程。

1.4.1 How does an HTTP web request work?‌

1.4.1 HTTP Web 请求如何工作?

As you know now, ASP.NET Core is a framework for building web applications that serve data from a server. One of the most common scenarios for web developers is building a web app that you can view in a web browser. Figure 1.2 shows the high-level process you can expect from any web server.

正如您现在所知,ASP.NET Core 是一个框架,用于构建从服务器提供数据的 Web 应用程序。对于 Web 开发人员来说,最常见的方案之一是构建可在 Web 浏览器中查看的 Web 应用程序。图 1.2 显示了您可以从任何 Web 服务器获得的高级过程。

alt text

Figure 1.2 Requesting a web page. The user starts by requesting a web page, which causes an HTTP request to be sent to the server. The server interprets the request, generates the necessary HTML, and sends it back in an HTTP response. Then the browser can display the web page.

图 1.2 请求网页。用户首先请求一个网页,这会导致向服务器发送 HTTP 请求。服务器解释请求,生成必要的 HTML,并在 HTTP 响应中将其发送回。然后浏览器可以显示网页。

The process begins when a user navigates to a website or types a URL in their browser. The URL or web address consists of a hostname and a path to some resource on the web app. Navigating to the address in the browser sends a request from the user’s computer to the server on which the web app is hosted, using the HTTP protocol.

当用户导航到网站或在浏览器中键入 URL 时,该过程开始。URL 或 Web 地址由主机名和 Web 应用程序上某个资源的路径组成。导航到浏览器中的地址会使用 HTTP 协议将请求从用户的计算机发送到托管 Web 应用程序的服务器。

DEFINITION The hostname of a website uniquely identifies its location on the internet by mapping via the Domain Name Service (DNS) to an IP address. Examples include microsoft.com, www.google.co.uk, and facebook.com.

定义 网站的主机名通过域名服务 (DNS) 映射到 IP 地址,从而唯一标识其在 Internet 上的位置。示例包括 microsoft.com、www.google.co.uk 和 facebook.com。

A brief primer on HTTP
HTTP 简介
Hypertext Transfer Protocol (HTTP) is the application-level protocol that powers the web. It’s a stateless request-response protocol whereby a client machine sends a request to a server, which sends a response in turn.
超文本传输协议 (HTTP) 是支持 Web 的应用程序级协议。它是一种无状态的请求-响应协议,客户端计算机通过该协议向服务器发送请求,服务器反过来发送响应。
Every HTTP request consists of a verb indicating the type of the request and a path indicating the resource to interact with. A request typically also includes headers, which are key-value pairs, and in some cases a body, such as the contents of a form, when sending data to the server.‌‌
每个 HTTP 请求都包含一个动词(指示请求的类型)和一个路径(指示要与之交互的资源)。在向服务器发送数据时,请求通常还包括标头(键值对)和在某些情况下的正文,例如表单的内容。
An HTTP response contains a status code, indicating whether the request was successful, and optionally headers and a body.
HTTP 响应包含状态代码,指示请求是否成功,以及可选的标头和正文。
For a more detailed look at the HTTP protocol itself, as well as more examples, see section 1.3 (“A quick introduction to HTTP”) of Go Web Programming, by Sau Sheong Chang (Manning, 2016), at http://mng.bz/x4mB. You can also read the raw RFC specification at https://www.rfc-editor.org/rfc/rfc9110.txt if dense text is your thing!‌
有关 HTTP 协议本身的更详细内容以及更多示例,请参阅 Sau Sheong Chang (Manning, 2016) 在 http://mng.bz/x4mB 上编写的 Go Web Programming 的第 1.3 节(“HTTP 快速介绍”)。 您还可以阅读原始 RFC 规范,https://www.rfc-editor.org/rfc/rfc9110.txt 如果您喜欢密集文本!

The request passes through the internet, potentially to the other side of the world, until it finally makes its way to the server associated with the given hostname, on which the web app is running. The request is potentially received and rebroadcast at multiple routers along the way, but only when it reaches the server associated with the hostname is the request processed.

该请求通过 Internet 传递,可能到达世界的另一端,直到最终到达与运行 Web 应用程序的给定主机名关联的服务器。请求可能会在途中的多个路由器上接收并重新广播,但只有当它到达与主机名关联的服务器时,才会处理请求。

When the server receives the request, it processes that request and generates an HTTP response. Depending on the request, this response could be a web page, an image, a JavaScript file, a simple acknowledgment, or practically any other file. For this example, I’ll assume that the user has reached the home page of a web app, so the server responds with some HTML. The HTML is added to the HTTP response, which is sent back across the internet to the browser that made the request.

当服务器收到请求时,它会处理该请求并生成 HTTP 响应。根据请求,此响应可以是网页、图像、JavaScript 文件、简单确认或几乎任何其他文件。对于此示例,我假设用户具有到达 Web 应用程序的主页,因此服务器使用一些 HTML 进行响应。HTML 将添加到 HTTP 响应中,该响应将通过 Internet 发送回发出请求的浏览器。

As soon as the user’s browser begins receiving the HTTP response, it can start displaying content on the screen, but the HTML page may also reference other pages and links on the server. To display the complete web page instead of a static, colorless, raw HTML file, the browser must repeat the request process, fetching every referenced file. HTML, images, Cascading Style Sheets (CSS) for styling, and JavaScript files for extra behavior are all fetched using exactly the same HTTP request process.

一旦用户的浏览器开始接收 HTTP 响应,它就可以开始在屏幕上显示内容,但 HTML 页面也可能引用服务器上的其他页面和链接。要显示完整的网页而不是静态的、无色的原始 HTML 文件,浏览器必须重复请求过程,获取每个引用的文件。HTML、图像、用于样式的级联样式表 (CSS) 和用于额外行为的 JavaScript 文件都使用完全相同的 HTTP 请求过程来获取。

Pretty much all interactions that take place on the internet are a facade over this basic process. A basic web page may require only a few simple requests to render fully, whereas a large modern web page may take hundreds. At this writing, the Amazon .com home page (https://www.amazon.com) makes 410 requests, including requests for 4 CSS files, 12 JavaScript files, and 299 image files!

几乎所有发生在互联网上的交互都是这个基本过程的假象。一个基本的网页可能只需要几个简单的请求就可以完全呈现,而一个大型的现代网页可能需要数百个请求。在撰写本文时,Amazon .com 主页 (https://www.amazon.com) 发出了 410 个请求,包括对 4 个 CSS 文件、12 个 JavaScript 文件和 299 个图像文件的请求!

Now that you have a feel for the process, let’s see how ASP.NET Core dynamically generates the response on the server.

现在您已经了解了该过程,让我们看看 ASP.NET Core 如何在服务器上动态生成响应。

1.4.2 How does ASP.NET Core process a request?‌

1.4.2 ASP.NET Core 如何处理请求?

When you build a web application with ASP.NET Core, browsers will still be using the same HTTP protocol as before to communicate with your application. ASP.NET Core itself encompasses everything that takes place on the server to handle a request, including verifying that the request is valid, handling login details, and generating HTML.

当您使用 ASP.NET Core 构建 Web 应用程序时,浏览器仍将使用与以前相同的 HTTP 协议与您的应用程序通信。ASP.NET Core 本身包含服务器上为处理请求而发生的所有作,包括验证请求是否有效、处理登录详细信息和生成 HTML。

As with the generic web page example, the request process starts when a user’s browser sends an HTTP request to the server, as shown in figure 1.3.

与通用网页示例一样,当用户的浏览器向服务器发送 HTTP 请求时,请求进程开始,如图 1.3 所示。

alt text

Figure 1.3 How an ASP.NET Core application processes a request. A request is received by the ASP.NET Core application, which runs a self-hosted web server. The web server processes the request and passes it to the body of the application, which generates a response and returns it to the web server. The web server sends this response to the browser.

图 1.3 ASP.NET Core 应用程序如何处理请求。运行自托管 Web 服务器的 ASP.NET Core 应用程序接收请求。Web 服务器处理请求并将其传递给应用程序正文,后者生成响应并将其返回给 Web 服务器。Web 服务器将此响应发送到浏览器。

The request is received from the network by your ASP.NET Core application. Every ASP.NET Core application has a built- in web server—Kestrel, by default—that is responsible for receiving raw requests and constructing an internal representation of the data, an HttpContext object, which the rest of the application can use.

您的 ASP.NET Core 应用程序从网络接收请求。每个 ASP.NET Core 应用程序都有一个内置的 Web 服务器(默认为 Kestrel),该服务器负责接收原始请求并构建数据的内部表示形式,即应用程序的其余部分可以使用的 HttpContext 对象。

Your application can use the details stored in HttpContext to generate an appropriate response to the request, which may be to generate some HTML, to return an “access denied” message, or to send an email, all depending on your application’s requirements.

您的应用程序可以使用存储在 HttpContext 中的详细信息来生成对请求的适当响应,这可能是生成一些 HTML、返回“拒绝访问”消息或发送电子邮件,所有这些都取决于应用程序的要求。

When the application finishes processing the request, it returns the response to the web server. The ASP.NET Core web server converts the representation to a raw HTTP response and sends it to the network, which forwards it to the user’s browser.

当应用程序完成处理请求时,它会将响应返回给 Web 服务器。ASP.NET Core Web 服务器将表示形式转换为原始 HTTP 响应,并将其发送到网络,网络将其转发到用户的浏览器。

To the user, this process appears to be the same as for the generic HTTP request shown in figure 1.2: the user sent an HTTP request and received an HTTP response. All the differences are server-side, within your application.

对于用户来说,此过程似乎与图 1.2 中所示的通用 HTTP 请求相同:用户发送了一个 HTTP 请求并收到了一个 HTTP 响应。所有差异都是在服务器端的应用程序中。

You’ve seen how requests and responses find their way to and from an ASP.NET Core application, but I haven’t yet touched on how the response is generated. Throughout this book, we’ll look at the components that make up a typical ASP.NET Core application and how they fit together. A lot goes into generating a response in ASP.NET Core, typically within a fraction of a second, but over the course of the book we’ll step through an application slowly, covering each of the components in detail.‌

您已经了解了请求和响应如何与 ASP.NET Core 应用程序之间找到进出方式,但我尚未涉及响应是如何生成的。在本书中,我们将了解构成典型 ASP.NET Core 应用程序的组件,以及它们如何组合在一起。在 ASP.NET Core 中生成响应需要做很多工作,通常在几分之一秒内,但在本书的整个过程中我们将慢慢地逐步完成一个应用程序,详细介绍每个组件。

1.5 What you’ll learn in this book‌

1.5 您将在本书中学到什么

This book takes you on an in-depth tour of the ASP.NET Core framework. To benefit from the book, you should be familiar with C# or a similar object-oriented language. Basic familiarity with web concepts such as HTML and JavaScript will also be beneficial. You’ll learn the following:

本书将带您深入浏览 ASP.NET Core 框架。要从本书中受益,您应该熟悉 C# 或类似的面向对象语言。基本熟悉 HTML 和 JavaScript 等 Web 概念也将是有益的。您将了解以下内容:

  • How to build HTTP API applications using minimal APIs
    如何使用最少的 API 构建 HTTP API 应用程序

  • How to create page-based applications with Razor Pages
    如何使用 Razor Pages 创建基于页面的应用程序

  • Key ASP.NET Core concepts such as model-binding, validation, and routing
    关键 ASP.NET 核心概念,例如模型绑定、验证和路由

  • How to generate HTML for web pages by using Razor syntax and Tag Helpers
    如何使用 Razor 语法和标记帮助程序为网页生成 HTML

  • How to use features such as dependency injection, configuration, and logging as your applications grow more complex
    如何在应用程序变得越来越复杂时使用依赖项注入、配置和日志记录等功能

  • How to protect your application by using security best practices
    如何使用安全最佳实践保护您的应用程序

Throughout the book we’ll use a variety of examples to learn and explore concepts. The examples are generally small and self-contained so that we can focus on a single feature at a time.

在整本书中,我们将使用各种示例来学习和探索概念。这些示例通常很小,并且自包含,以便我们可以一次专注于一个功能。

I’ll be using Visual Studio for most of the examples in this book, but you’ll be able to follow along using your favorite editor or integrated development environment (IDE).

对于本书中的大多数示例,我将使用 Visual Studio,但您将能够使用您最喜欢的编辑器或集成开发环境 (IDE) 来学习。

Appendix A includes details on setting up your editor or IDE and installing the .NET 7 software development kit (SDK).

附录 A 包括有关设置编辑器或 IDE 以及安装 .NET 7 软件开发工具包 (SDK) 的详细信息。

Even though the examples in this book show Windows tools, everything you see can be achieved equally well on the Linux or Mac platform.

尽管本书中的示例展示了 Windows 工具,但您所看到的一切都可以在 Linux 或 Mac 平台上同样出色地实现。

TIP You can install .NET 7 from https://dotnet.microsoft.com/download. Appendix A contains further details on configuring your development environment to work with ASP.NET Core and .NET 7.

提示 您可以从 https://dotnet.microsoft.com/download 安装 .NET 7。附录 A 包含有关配置开发环境以使用 ASP.NET Core 和 .NET 7 的更多详细信息。

In chapter 2, we’ll look in greater depth at the types of applications you can create with ASP.NET Core. We’ll also explore its advantages over the older ASP.NET and .NET Framework platforms.

在第 2 章中,我们将更深入地了解您可以使用 ASP.NET Core 创建的应用程序类型。我们还将探讨它与旧版 ASP.NET 和 .NET Framework 平台相比的优势。

Summary

总结

ASP.NET Core is a cross-platform, open-source, high-performance web framework.
ASP.NET Core 是一个跨平台、开源、高性能的 Web 框架。
ASP.NET Core runs on .NET, previously called .NET Core.
ASP.NET Core 在 .NET(以前称为 .NET Core)上运行。

You can use Razor Pages or MVC controllers to build server-rendered, page-based web applications.
您可以使用 Razor Pages 或 MVC 控制器来构建服务器呈现的、基于页面的 Web 应用程序。

You can use minimal APIs or web APIs to build RESTful or HTTP APIs.
您可以使用最少的 API 或 Web API 来构建 RESTful 或 HTTP API。

You can use gRPC to build highly efficient server-to- server RPC applications.
您可以使用 gRPC 构建高效的服务器到服务器 RPC 应用程序。

You can use Blazor WebAssembly to build client- side applications that run in the browser and Blazor Server to build stateful, server-rendered applications that send UI updates via a WebSocket connection.
可以使用 Blazor WebAssembly 构建在浏览器中运行的客户端应用程序,使用 Blazor Server 构建有状态的服务器呈现的应用程序,这些应用程序通过 WebSocket 连接发送 UI 更新。

Microsoft recommends ASP.NET Core and .NET 7 or later for all new web development over the legacy ASP.NET and .NET Framework platforms.
Microsoft 建议 ASP.NET Core 和 .NET 7 或更高版本,以便在旧版 ASP.NET 和 .NET Framework 平台上进行所有新的 Web 开发。

Fetching a web page involves sending an HTTP request and receiving an HTTP response.
获取网页涉及发送 HTTP 请求和接收 HTTP 响应。

ASP.NET Core allows you to build responses to a given request dynamically.
ASP.NET Core 允许您动态构建对给定请求的响应。

An ASP.NET Core application contains a web server, which serves as the entry point for a request.
ASP.NET Core 应用程序包含一个 Web 服务器,该服务器用作请求的入口点。

ASP.NET Core in Action Preface

ASP.NET Core in Action Preface

preface

前言

ASP.NET has a long history; Microsoft released the first version in 2002 as part of the original .NET Framework 1.0. Since then, it's been through multiple iterations, each version bringing added features and extensibility. Each iteration, however, was built on the same underlying framework provided by System.Web.dll. This library is part of the .NET Framework, so it comes preinstalled in all versions of Windows.

ASP.NET有着悠久的历史;Microsoft于2002年发布了第一个版本,作为原始.NET Framework1.0的一部分。从那时起,它经历了多次迭代,每个版本都带来了更多的功能和可扩展性。但是,每个迭代都构建在System.Web.dll提供的相同底层框架之上。此库是.NET Framework的一部分,因此它预安装在所有版本的Windows中。

This brings mixed blessings. On one hand, the ASP.NET 4.X framework today is a reliable, battle-tested platform for building modern applications on Windows. On the other hand, it is limited by this reliance; changes to the underlying System.Web.dll are far-reaching and consequently slow to roll out, and it fundamentally excludes the many developers who are building and deploying to Linux or macOS.

这带来了喜忧参半。一方面,如今的ASP.NET4.X框架是一个可靠的、久经考验的平台,用于在Windows上构建现代应用程序。另一方面,它受到这种依赖的限制;对底层System.Web.dll的更改影响深远,因此推出速度较慢,从根本上将许多正在构建和部署到Linux或macOS的开发人员排除在外。

When I began looking into ASP.NET Core, I was one of those developers. A Windows user at heart, I was issued a Mac by my employer, so I was stuck working in a virtual machine all day. ASP.NET Core promised to change all that, allowing me to develop natively on both my Windows machine and my Mac.

当我开始研究ASP.NET Core时,我就是其中之一。我本质上是Windows用户,我的雇主给我发了一台Mac,所以我整天都被困在虚拟机中工作。ASP.NET Core承诺改变这一切,让我可以在Windows机器和Mac上进行原生开发。

I was relatively late to the party in many respects, taking an active interest only just before the RC2 release of ASP.NET Core. By this point there had already been eight (!) beta releases, many of which contained significant breaking changes. By not diving in fully until RC2, I was spared the pain of dodgy tooling and changing APIs.

在许多方面,我加入派对的时间相对较晚,只是在ASP.NET Core的RC2发布之前才开始积极关注。此时已经有8个beta版本,其中许多都包含重大的破坏性变化。由于直到RC2才完全深入研究,我免于狡猾的工具和更改API的痛苦。

What I saw at that point really impressed me. ASP.NET Core let developers use their existing knowledge of the .NET Framework, and of ASP.NET MVC applications in particular, while baking in current best practices such as dependency injection, strongly typed configuration, and logging. On top of that, you could build and deploy cross-platform. I was sold.

我当时所看到的真的给我留下了深刻的印象。ASP.NET Core允许开发人员使用他们现有的.NET Framework知识,特别是ASP.NET MVC应用程序,同时采用当前的最佳实践,如依赖项注入、强类型配置和日志记录。最重要的是,您可以跨平台构建和部署。我被说服了。

This book came about largely due to my approach to learning about ASP.NET Core. Rather than simply reading documentation and blog posts, I decided to try something new and start writing about what I learned. Each week I would dedicate some time to exploring a new aspect of ASP.NET Core, and I’d write a blog post about it. When the possibility of writing a book came about, I jumped at the chance—another excuse to dive further into the framework!

这本书的诞生很大程度上是由于我学习ASP.NET Core的方法。我决定尝试一些新的东西,并开始写下我学到的东西,而不是简单地阅读文档和博客文章。每周我都会花一些时间来探索ASP.NET Core的一个新方面,并写一篇关于它的博客文章。当写书的可能性出现时,我抓住了这个机会——这是进一步深入研究框架的另一个借口!

Since I started this book, a lot has changed, both with the book and ASP.NET Core. The first major release of the framework in June 2016 still had many rough edges, in particular around the tooling experience. With the release of
.NET 7 in November 2022, ASP.NET Core has really come into its own, with the APIs and tooling reaching mature levels.

自从我开始写这本书以来,这本书和ASP.NETCore都发生了很多变化。该框架的第一个主要版本于2016年6月发布,但仍有许多粗糙的边缘,尤其是在工具体验方面。随着.NET7在2022年11月发布,ASP.NET Core已经真正崭露头角,API和工具达到了成熟的水平。

Updates to the framework in .NET 6 and .NET 7 significantly simplified the getting-started experience for newcomers with the introduction of minimal hosting and minimal APIs, which provide a terser, simpler approach to writing APIs, much closer to the experience in other languages. You can get straight into building your app's functionality without having to understand architecture first.

.NET6和.NET7中对框架的更新通过引入最小托管和最小API显著简化了新手的入门体验,这些API提供了一种更简洁、更简单的API编写方法,更接近其他语言中的体验。您可以直接开始构建应用程序的功能,而无需先了解架构。

For some experienced ASP.NET Core developers, these changes can feel regressive and unstructured, but if you're one of them, I encourage you to give them a chance and to build your own structure and patterns. For brevity and clarity of the examples in this book, I often put the whole code for your app in one file, but don't think that's how you need to write your real applications. You're free to create helper methods, classes, and any structure that helps keep your applications maintainable while taking advantage of the performance benefits of minimal APIs.

对于一些经验丰富的ASP.NET Core开发人员来说,这些变化可能会让人感觉是倒退和非结构化的,但如果你是其中之一,我鼓励你给他们一个机会,并构建你自己的结构和模式。为了简洁明了地介绍本书中的示例,我经常将应用程序的整个代码放在一个文件中,但不要认为这是编写实际应用程序的方式。您可以自由创建帮助程序方法、类和任何结构,以帮助保持应用程序的可维护性,同时利用最小API的性能优势。

This book covers everything you need to get started with ASP.NET Core, whether you’re new to web development or an existing ASP.NET developer. It focuses on the framework itself, so I don’t go into details about client-side frameworks such as Angular and React or technologies like Docker. I also don’t cover all the new features in .NET 7, such as Blazor and gRPC; instead, I provide links where you can find more information.

本书涵盖了开始使用ASP.NETCore所需的一切,无论您是Web开发新手还是现有的ASP.NET开发人员。它侧重于框架本身,因此我不会详细介绍客户端框架(如Angular和React)或Docker等技术。我也没有介绍.NET7中的所有新功能,例如Blazor和gRPC;相反,我提供了链接,您可以在其中找到更多信息。

In this edition, I have significantly expanded and rearranged many chapters compared with previous editions of the book; some chapters have been split into more manageable sizes.

与本书的前几个版本相比,在这个版本中,我显着扩展和重新排列了许多章节;一些章节被分成了更易于管理的大小。

The early chapters feature a lot of new content focusing on minimal APIs and minimal hosting introduced in .NET 6.

前几章包含许多新内容,重点介绍.NET6中引入的最小API和最小托管。

I find it a joy to work with ASP.NET Core apps compared with apps using the previous version of ASP.NET, and I hope that my passion comes through in this book!

与使用以前版本的ASP.NET的应用程序相比,我发现使用ASP.NETCore应用程序是一种乐趣,我希望我的热情在这本书中得到体现!

acknowledgments‌

致谢

Although there is only one name on the cover of this book, a plethora of people contributed to both its writing and production. In this section I’d like to thank everyone who encouraged me, contributed, and put up with me for the past year.

虽然这本书的封面上只有一个名字,但很多人都为它的写作和制作做出了贡献。在本节中,我要感谢过去一年中所有鼓励我、做出贡献和容忍我的人。

First, and most important, I’d like to thank my girlfriend, Becky. Your continual support and encouragement means the world to me and has kept me going through such a busy time. You’ve taken the brunt of my stress and pressure, and I’m eternally grateful. I love you always.

首先,也是最重要的,我要感谢我的女朋友Becky。您一直以来的支持和鼓励对我来说意味着整个世界,让我度过了如此忙碌的时光。你首当其冲地承受了我的压力和压力,我永远感激不尽。我永远爱你。

I’d also like to thank my whole family for their support, in particular my parents, Jan and Bob, for putting up with my ranting; my sister, Amanda, for your always upbeat chats; and of course, Goose, for diligently ensuring that I take regular breaks for walks and tummy tickles.

我还要感谢我全家人的支持,特别是我的父母Jan和Bob,他们忍受了我的咆哮;我的姐姐Amanda,感谢你总是乐观的聊天;当然,还有Goose,他勤奋地确保我定期休息散步和挠肚子痒痒。

On a professional level, I’d like to thank Manning for giving me this opportunity. Brian Sawyer “discovered” me for the first version of this book and encouraged me to tackle the subsequent versions. Marina Michaels served as my development editor for the third time running and again proved to be alternately meticulous, critical, encouraging, and enthusiastic. The book is undoubtedly better thanks to your involvement.

在专业层面上,我要感谢Manning给我这个机会。BrianSawyer在这本书的第一个版本中“发现”了我,并鼓励我处理后续版本。MarinaMichaels担任我的第三次担任开发编辑,并再次证明他时而细致、批判、鼓励和热情。多亏了您的参与,这本书无疑会更好。

Thank you to my review editor, Adriana Sabo, and to all the reviewers: Alen Adanić, Ben McNamara, Bela Istók, Darrin Bishop, Dennis Liabenow, Al Pezewski, Emmanouil Chardalas, Foster Haines, Onofrei George, John Guthrie, Jean-François Morin, Pedro Seromenho, Joe Cuevas, José Antonio Martinez Perez, Joe Suchy, Luis Moux, Milan Šarenac, Milorad Imbra, Nik Rimington, Nitin Ainani, Oliver Korten, Raushan Jha, Richard Young, Rick Beerendonk, Ron Lease, Ruben Vandeginste, Sumit K. Singh, Towhidul Bashar, Daniel Vásquez, and Will Lopez. Your suggestions helped make this a better book.

感谢我的评论编辑AdrianaSabo和所有评论者:AlenAdanić、BenMcNamara、BelaIstók、DarrinBishop、DennisLiabenow、AlPezewski、EmmanouilChardalas、FosterHaines、OnofreiGeorge、JohnGuthrie、Jean-FrançoisMorin、PedroSeromenho、JoeCuevas、JoséAntonioMartinezPerez、JoeSuchy、LuisMoux、MilanŠarenac、MiloradImbra、NikRimington、NitinAinani、OliverKorten、RaushanJha、RichardYoung、RickBeerendonk、RonLease、RubenVandeginste、SumitK.Singh、TowhidulBashar、DanielVásquez和WillLopez。您的建议帮助使这本书变得更好。

My thanks go to the technical editor for this book, Filip Wojcieszyn, who is a founder and maintainer of several popular open-source projects, frequent conference speaker, and a Microsoft MVP. Filip provided invaluable feedback, highlighting my incorrect assumptions and technical biases, and ensuring technical correctness in everything I wrote.

我要感谢本书的技术编辑Filip Wojcieszyn,他是几个流行的开源项目的创始人和维护者,经常在会议上发表演讲,并且是Microsoft MVP。Filip提供了宝贵的反馈,强调了我的错误假设和技术偏见,并确保我写的所有内容的技术正确性。

I also wish to thank Tanya Wilke, who served as technical proofreader. Tanya verified that the code I wrote actually ran and made sense, working through the chapters with formidable efficiency.

我还要感谢担任技术校对员的Tanya Wilke。Tanya验证了我编写的代码确实运行良好且有意义,以惊人的效率完成了各个章节。

To everyone at Manning who helped get this book published and marketed, a heartfelt thanks. I’d also like to thank all the MEAP readers for their comments, which helped improve the book in numerous ways.

衷心感谢Manning帮助出版和营销这本书的每个人。我还要感谢大家MEAP读者的评论,这在许多方面帮助改进了这本书。

I would have never been in a position to write this book if not for the excellent content produced by members of the .NET community and those I follow on social media.

如果不是.NET社区和我在社交媒体上关注的人。

Finally, thanks to all those friends who encouraged and supported me, and showed interest generally. We may not have been able to meet up as much as we’d like, but I look forward to getting together for a drink as soon as it’s possible.

最后,感谢所有鼓励和支持我的朋友,并普遍表现出兴趣。我们可能无法如愿以偿地见面,但我期待着尽快聚在一起喝一杯。

about this book‌

关于本书

This book is about the ASP.NET Core framework, what it is, and how you can use it to build web applications. Although some of this content is already available online, it’s scattered around the internet in disparate documents and blog posts. This book guides you through building your first applications, introducing additional complexity as you cement previous concepts.

这本书介绍了ASP.NET Core框架、它是什么以及如何使用它来构建Web应用程序。尽管其中一些内容已经在网上提供,但它们分散在互联网上不同的文档和博客文章中。本书将指导您构建您的第一个应用程序,并在巩固以前的概念时引入额外的复杂性。

I present each topic using relatively small examples rather than building on a single example application through the book. There are merits to both approaches, but I wanted to ensure that the focus remained on the specific topics being taught, without the mental overhead of navigating an increasingly large project.

我使用相对较小的示例来介绍每个主题,而不是在本书中构建单个示例应用程序。这两种方法都有优点,但我想确保重点仍然放在所教授的特定主题上,而不会因驾驭越来越大的项目而产生脑力开销。

By the end of the book, you should have a solid understanding of how to build apps with ASP.NET Core, its strengths and weaknesses, and how to use its features to build apps securely. I don’t spend a lot of time on application architecture, but I make sure to point out best practices, especially where I cover architecture only superficially for the sake of brevity.

在本书结束时,您应该对如何使用ASP.NETCore构建应用程序、其优缺点以及如何使用其功能安全地构建应用程序有深入的了解。我不会在应用程序架构上花费太多时间,但我会确保指出最佳实践,尤其是为了简洁起见,我只是肤浅地介绍架构。

Who should read this book

谁应该阅读这本书

This book is for C# developers who are interested in learning a cross-platform web framework. It doesn’t assume that you have any experience building web applications. You may be a mobile or desktop developer, for example, though experience with ASP.NET or another web framework is undoubtedly beneficial.

本书适用于对学习跨平台Web框架感兴趣的C#开发人员。它不假定您具有构建Web应用程序的任何经验。例如,您可能是移动或桌面开发人员,但具有ASP.NET或其他Web框架的经验无疑是有益的。

I assume that in addition to a working knowledge of C# and .NET, you have some knowledge of common object-oriented practices and a basic understanding of relational databases in general. I assume passing familiarity with HTML and CSS and of JavaScript’s place as a client-side scripting language. You don’t need to know any JavaScript or CSS frameworks for this book, though ASP.NET Core works well with both if that is your forte.

我假设除了C#和.NET中,您对常见的面向对象的做法有一定的了解,并且对关系数据库有基本的了解。我假设你对HTML和CSS以及JavaScript作为客户端脚本语言的地位有所了解。对于本书,您不需要了解任何JavaScript或CSS框架,但如果这是您的强项,ASP.NET Core可以很好地与两者配合使用。

Web frameworks naturally touch on a wide range of topics, from the database and network to visual design and client- side scripting. I provide as much context as possible, and I include links to sites and books where you can learn more.

Web框架自然涉及广泛的主题,从数据库和网络到可视化设计和客户端脚本。我提供了尽可能多的背景信息,并提供了指向您可以了解更多信息的网站和书籍的链接。

How this book is organized

本书的组织方式

This book is divided into 5 parts, 36 chapters, and 2 appendices. Ideally, you will read the book cover to cover and then use it as a reference, but I realize that this approach won’t suit everyone. Although I use small sample apps to demonstrate a topic, some chapters build on the work of previous ones, so the content will make more sense when read sequentially.

本书分为5个部分、36章和2个附录。理想情况下,你会从头到尾阅读这本书,然后将其用作参考,但我意识到这种方法并不适合所有人。虽然我使用小型示例应用程序来演示主题,但有些章节建立在前几章的基础上,因此按顺序阅读时,内容会更有意义。

I strongly suggest reading the chapters in part 1 in sequence, as each chapter builds on topics introduced in the previous chapters and provides a basis for the rest of the book. Part 2 is also best read sequentially, though most of the chapters are independent if you wish to jump around.

我强烈建议按顺序阅读第1部分中的章节,因为每一章都建立在前几章介绍的主题之上,并为本书的其余部分提供了基础。第2部分也最好按顺序阅读,但如果您想跳来跳去,大多数章节都是独立的。

Part 3, again, is best read sequentially. You’ll get the best experience by reading the chapters in parts 4 and 5 sequentially, but many of the topics are independent, so you can read them out of order if you prefer. But I recommend only doing so after you’ve covered parts 1 to 3.

同样,第3部分最好按顺序阅读。按顺序阅读第4部分和第5部分中的章节将获得最佳体验,但许多主题是独立的,因此如果您愿意,可以不按顺序阅读它们。但我建议仅在您完成第1部分到第3部分后才这样做。

Part 1 provides a general introduction to ASP.NET Core, focusing on building small JSON APIs by using the latest features introduced in .NET 7. After we cover the basics, we look at building minimal API applications that provide the simplest programming model for ASP.NET Core web applications.

第1部分提供了ASP.NET Core的一般介绍,重点介绍如何使用.NET7中引入的最新功能构建小型JSONAPI。在介绍基础知识之后,我们将了解如何构建最小的API应用程序,这些应用程序为ASP.NETCoreWeb应用程序提供最简单的编程模型。

  • Chapter 1 introduces ASP.NET Core and its place in the web development landscape. It describes the type of applications you can build, some of the reasons to choose ASP.NET Core, and the basics of web requests in an ASP.NET Core application.

  • 第1章介绍了ASP.NETCore及其在Web开发领域中的地位。它介绍了您可以构建的应用程序类型、选择ASP.NETCore的一些原因,以及ASP.NET Core应用程序中Web请求的基础知识。

  • Chapter 2 looks at why you should consider using any web framework, why ASP.NET Core was created, and the different application paradigms you can use with ASP.NET Core. Finally, it looks at the situations when you should and shouldn’t choose ASP.NET Core.

  • 第2章探讨了为什么应该考虑使用任何Web框架,创建ASP.NETCore的原因,以及可以与ASP.NET Core一起使用的不同应用程序范例。最后,它着眼于您应该和不应该选择ASP.NET Core的情况。

  • Chapter 3 walks through all the components of a basic ASP.NET Core minimal API application, discussing their role and how they combine to generate a response to a web request.

  • 第3章介绍了基本ASP.NET Core最小API应用程序的所有组件,讨论了它们的作用以及它们如何组合以生成对Web请求的响应。

  • Chapter 4 describes the middleware pipeline, the main application pipeline in ASP.NET Core, which defines how incoming requests are processed and how a response should be generated.

  • 第4章介绍了中间件管道,这是ASP.NET Core中的主要应用程序管道,它定义了如何处理传入请求以及如何生成响应。

  • Chapter 5 shows how to use minimal API endpoints to create a JavaScript Object Notation (JSON) HTTP API that can be called by client-side apps, server-side apps, or mobile devices.

  • 第5章展示了如何使用最少的API端点来创建可由客户端应用程序、服务器端应用程序或移动设备调用的JavaScript对象表示法(JSON)HTTP API

  • Chapter 6 describes the ASP.NET Core routing system. Routing is the process of mapping incoming request URLs to a specific handler method, which executes to generate a response.

  • 第6章介绍了ASP.NET Core路由系统。路由是将传入请求URL映射到特定处理程序方法的过程,该方法执行以生成响应。

  • Chapter 7 looks at model binding in minimal APIs, the process of mapping form data and URL parameters passed in a request to concrete C# objects.

  • 第7章着眼于最小API中的模型绑定,以及将请求中传递的表单数据和URL参数映射到具体C#对象的过程。

Part 2 covers important topics for building fully-featured web applications after you understand the basics:

在了解了基础知识之后,第2部分将介绍构建功能齐全的Web应用程序的重要主题:

  • Chapter 8 introduces the concept of dependency injection (DI) and describes the DI container built into ASP.NET Core.

  • 第8章介绍了依赖关系注入(DI)的概念,并介绍了ASP.NETCore中内置的DI容器。

  • Chapter 9 builds on chapter 8 by describing how to register your own services with the DI container, the patterns you can use, and how to understand the lifetime of services the DI container creates.

  • 第9章以第8章为基础,介绍了如何向DI容器注册您自己的服务、您可以使用的模式以及如何了解DI容器创建的服务的生命周期。

  • Chapter 10 discusses how to read settings and secrets in ASP.NET Core, and how to map them to strongly typed objects.

  • 第10章讨论了如何读取ASP.NET Core中的设置和密钥,以及如何将它们映射到强类型对象。

  • Chapter 11 describes how to document your APIs using the OpenAPI standard and how this helps with testing scenarios and for automatically generating clients to call your APIs.

  • 第11章描述了如何使用OpenAPI标准记录您的API,以及它如何帮助测试场景和自动生成客户端来调用API。

  • Chapter 12 introduces EntityFramework Core (EF Core) for saving data in a relational database.

  • 第12章介绍了用于在关系数据库中保存数据的EntityFramework Core(EFCore)。

Part 3 moves away from minimal APIs and looks at how to build server-rendered page-based HTML applications using Razor Pages and the Model-View-Controller (MVC) architecture:

第3部分从最少的API出发,介绍如何使用RazorPages和模型-视图-控制器(MVC)体系结构构建服务器呈现的基于页面的HTML应用程序:

  • Chapter 13 shows how to use Razor Pages to build page-based web sites. Razor Pages are the recommended way to build server-rendered applications in ASP.NET Core and are designed for page-based applications.

  • 第13章介绍如何使用RazorPages构建基于页面的网站。RazorPages是在ASP.NET Core中构建服务器呈现的应用程序的推荐方法,专为基于页面的应用程序而设计。

  • Chapter 14 describes the Razor Pages routing system and how it differs from minimal APIs.

  • 第14章介绍了RazorPages路由系统以及它与最小API的区别。

  • Chapter 15 looks at page handlers in Razor Pages, which are responsible for choosing how to respond to a request and selecting what response to generate.

  • 第15章介绍了RazorPages中的页面处理程序,这些处理程序负责选择如何响应请求并选择要生成的响应。

  • Chapter 16 looks at model binding in Razor Pages, how it differs from minimal APIs, and the importance of validating your models.

  • 第16章介绍了RazorPages中的模型绑定、它与最小API的区别,以及验证模型的重要性。

  • Chapter 17 shows how to generate HTML web pages using the Razor template language.

  • 第17章介绍如何使用Razor模板语言生成HTML网页。

  • Chapter 18 builds on chapter 17 by introducing Tag Helpers, which can greatly reduce the amount of code required to build forms and web pages.

  • 第18章在第17章的基础上引入了标记帮助程序,这可以大大减少构建表单和网页所需的代码量。

  • Chapter 19 introduces MVC controllers as an alternative approach to building both server- rendered HTML applications and API applications.

  • 第19章介绍了MVC控制器作为构建服务器渲染的HTML应用程序和API应用程序的替代方法。

  • Chapter 20 describes how to use MVC controllers to build APIs that can be called by client-side apps as an alternative to minimal APIs.

  • 第20章描述了如何使用MVC控制器构建可由客户端应用程序调用的API,作为最小API的替代方案。

  • Chapter 21 introduces the MVC and Razor Pages filter pipeline, shows how it works, and describes some of the filters built into the framework.

  • 第21章介绍了MVC和RazorPages筛选器管道,展示了它的工作原理,并介绍了框架中内置的一些筛选器。

  • Chapter 22 builds on chapter 21 by showing how to create custom filters to reduce some of the duplication in your MVC and Razor Pages applications.

  • 第22章以第21章为基础,展示了如何创建自定义筛选器以减少MVC和RazorPages应用程序中的一些重复。

The chapters that make up part 4 cover important cross- cutting aspects of ASP.NET Core development:

构成第4部分的章节涵盖了ASP.NETCore开发的重要跨领域方面:

  • Chapter 23 describes how to add user profiles and authentication to your application by using ASP.NET Core Identity.

  • 第23章描述了如何使用ASP.NETCoreIdentity向应用程序添加用户配置文件和身份验证。

  • Chapter 24 builds on the previous chapter by introducing authorization for users so you can restrict which pages a signed-in user can access.

  • 第24章在上一章的基础上引入了用户授权,以便您可以限制已登录用户可以访问的页面。

  • Chapter 25 discusses authentication and authorization for API applications, how this differs from authentication in HTML applications, and how to get started with authentication in ASP.NET Core APIs.

  • 第25章讨论了API应用程序的身份验证和授权,这与HTML应用程序中的身份验证有何不同,以及如何开始在ASP.NET Core API中进行身份验证。

  • Chapter 26 shows how to configure logging in your application and how to write log messages to multiple locations.

  • 第26章展示了如何在应用程序中配置日志记录以及如何将日志消息写入多个位置。

  • Chapter 27 looks at how to publish your app and configure it for a production environment.

  • 第27章介绍了如何发布应用程序并针对生产环境对其进行配置。

  • Chapter 28 discusses the reason for adding HTTPS to your application, how to use HTTPS when developing locally and in production, and how to force HTTPS for your whole application.

  • 第28章讨论了将HTTPS添加到应用程序的原因,在本地和生产环境中开发时如何使用HTTPS,以及如何为整个应用程序强制使用HTTPS。

  • Chapter 29 explores some other security considerations you should make when developing your application and how to stay safe with ASP.NET Core.

  • 第29章探讨了在开发应用程序时应考虑的其他一些安全注意事项,以及如何使用ASP.NETCore保持安全。

Part 5 looks at various topics that help you take your ASP.NET Core applications further, including nonweb applications, custom configuration and components, and testing:

第5部分将介绍各种主题,这些主题可帮助您进一步改进ASP.NETCore应用程序,包括非Web应用程序、自定义配置和组件以及测试:

  • Chapter 30 discusses an alternative bootstrapping approach for ASP.NET Core apps, using the generic host and a Startup class.

  • 第30章讨论了ASP.NETCore应用程序的另一种引导方法,使用通用host和Startup类。

  • Chapter 31 describes how to build and use a variety of custom components, such as custom middleware, and how to handle complex configuration requirements.

  • 第31章描述了如何构建和使用各种自定义组件,例如自定义中间件,以及如何处理复杂的配置要求。

  • Chapter 32 expands on chapter 31, showing how to build custom Razor Page components such as custom Tag Helpers and custom validation attributes.

  • 第32章对第31章进行了扩展,展示了如何构建自定义Razor页面组件,例如自定义标记帮助程序和自定义验证属性。

  • Chapter 33 discusses the IHttpClientFactory service and how to use it to create HttpClient instances for calling remote APIs.

  • 第33章讨论了IHttpClientFactory服务以及如何使用它来创建用于调用远程API的HttpClient实例。

  • Chapter 34 explores the generic IHost abstraction, which you can use to create Windows Services and Linux daemons. You’ll also learn to run tasks in the background of your applications.

  • 第34章探讨了通用的IHost抽象,您可以使用它来创建Windows服务和Linux守护程序。您还将学习如何在应用程序的后台运行任务。

  • Chapter 35 shows how to test an ASP.NET Core application with the xUnit testing framework.

  • 第35章展示了如何使用xUnit测试框架测试ASP.NET Core应用程序。

  • Chapter 36 follows on from chapter 35, showing how to test ASP.NET Core applications specifically. It covers both unit tests and integration tests using the Test Host.

  • 第36章是第35章的后续内容,专门展示了如何测试ASP.NET Core应用程序。它涵盖单元测试和使用TestHost的集成测试。

The two appendices provide supplementary information:

两个附录提供了补充信息:

  • Appendix A describes how to configure your development environment, whether you’re in Windows, Linux, or macOS.

  • 附录A介绍了如何配置开发环境,无论您使用的是Windows、Linux还是macOS。

  • Appendix B contains links that I’ve found useful in learning about ASP.NET Core.

  • 附录B包含我发现对了解ASP.NETCore有用的链接。

About the code

关于代码

Source code is provided for all chapters except chapters 1, 2, 21, and 27, which don’t have any code. You can view the source code for each chapter in my GitHub repository at https://github.com/andrewlock/asp-dot-net-core-in-action-3e. A zip file containing all the source code is also available on the publisher’s website at https://www.manning.com/books/asp-net-core-in-action-third-edition. You can get executable snippets of code from the liveBook (online) version of this book at https://livebook.manning.com/book/asp-net-core-in-action-third-edition.

除了第1、2、21和27章之外,所有章节都提供了源代码,它们没有任何代码。您可以在https://github.com/andrewlock/asp-dot-net-core-in-action-3e 的我的GitHub存储库中查看每章的源代码。出版商的网站上也提供了包含所有源代码的zip文件,网址为https://livebook.manning.com/book/asp-net-core-in-action-third-edition。您可以从本书的liveBook(在线)版本(https://livebook.manning.com/book/asp-net-core-in-action-third-edition )获取可执行的代码片段。

All the code examples in this book use .NET 7 and were built using both Visual Studio and Visual Studio Code. To build and run the examples, you need to install the .NET software development kit (SDK), as described in appendix A.

本书中的所有代码示例都使用.NET7,并且是使用VisualStudio和VisualStudio Code构建的。构建并运行示例,您需要安装.NET软件开发工具包(SDK),如附录A中所述。

This book contains many examples of source code, both in numbered listings and inline with normal text. In both cases, source code is formatted in a fixed-width font like this to separate it from ordinary text. Sometimes code is also in bold to highlight changes from previous steps in the chapter, such as when a new feature adds to an existing line of code.

本书包含许多源代码示例,包括编号列表和与普通文本内联的源代码。在这两种情况下,源代码都采用固定宽度字体的格式,以便将其与普通文本区分开来。有时,代码也以粗体显示,以突出显示与本章中前面步骤相比的更改,例如,当新功能添加到现有代码行时。

In many cases, the original source code has been reformatted; we’ve added line breaks and reworked indentation to accommodate the available page space in the book. In rare cases, even this was not enough, and some listings include line-continuation markers (➥).

在许多情况下,原始源代码已被重新格式化;我们添加了换行符并重新设计了缩进,以适应书籍中可用的页面空间。在极少数情况下,即使这样还不够,一些商品信息包含行继续标记(➥)。

Additionally, comments in the source code have been removed from the listings when the code is described in the text. Code annotations accompany many of the listings, highlighting important concepts.

此外,当文本中描述代码时,源代码中的注释已从列表中删除。许多清单都附有代码注释,突出了重要的概念。

liveBook discussion forum

liveBook论坛

Purchase of ASP.NET Core in Action, Third Edition, includes free access to liveBook, Manning’s online reading platform. Using liveBook’s exclusive discussion features, you can attach comments to the book globally or to specific sections or paragraphs. It’s a snap to make notes for yourself, ask and answer technical questions, and receive help from the author and other users. To access the forum, go to https://livebook.manning.com/book/asp-net-core-in-action-third-edition/discussion. You can also learn more about Manning’s forums and the rules of conduct at https://livebook.manning.com/discussion.

购买ASP.NET Corein Action,ThirdEdition,即可免费访问Manning的在线阅读平台liveBook。使用liveBook的独有讨论功能,您可以将评论全局附加到书籍或特定部分或段落。您可以为自己做笔记,提出和回答技术问题,并从作者和其他用户那里获得帮助。要访问论坛,请转到https://livebook.manning.com/book/asp-net-core-in-action-third-edition/discussion。您还可以在https://livebook.manning.com/discussion上了解有关Manning论坛和行为准则的更多信息。

Manning’s commitment to our readers is to provide a venue where a meaningful dialogue between individual readers and between readers and the author can take place. It is not a commitment to any specific amount of participation on the part of the author, whose contribution to the forum remains voluntary (and unpaid). We suggest that you try asking the author some challenging questions lest his interest stray!

曼宁对读者的承诺是提供一个场所,让读者个人之间以及读者与作者之间可以进行有意义的对话。这并不是作者对任何特定参与量的承诺,他对论坛的贡献仍然是自愿的(并且是无偿的)。我们建议您尝试向作者询问一些具有挑战性的问题,以免他的兴趣偏离!

The forum and the archives of previous discussions will be accessible on the publisher’s website as long as the book is in print.

只要这本书是印刷的,就可以在出版商的网站上访问论坛和以前讨论的档案。

about the author‌

关于作者

alt text

ANDREW LOCK is a .NET developer and Microsoft MVP. He graduated with an engineering degree from Cambridge University, specializing in software engineering, and went on to obtain a PhD in digital image processing. He has been developing professionally with .NET since 2010, using a wide range of technologies, including WinForms, ASP.NET WebForms, ASP.NET MVC, ASP.NET Webpages, and most recently ASP.NET Core. Andrew has put many ASP.NET Core applications into production since version 1 was released in 2016. He has an active blog at https://andrewlock.net dedicated to ASP.NET Core. This blog has frequently been featured in the community spotlight by the ASP.NET team at Microsoft, on the .NET blog, and in the weekly community standups.

ANDREW LOCK是一名.NET开发人员和MicrosoftM VP。他毕业于剑桥大学,获得工程学位,专攻软件工程,并继续获得数字图像处理博士学位。自2010年以来,他一直在使用.NET进行专业开发,使用各种技术,包括WinForms、ASP.NET WebForms、ASP.NET MVC、ASP.NET Webpages,以及最近的ASP.NET Core。自2016年发布第1版以来,Andrew已经将许多ASP.NET Core应用程序投入生产。他在 https://andrewlock.net 上有一个活跃的博客,专门用于ASP.NET Core。此博客经常出现在Microsoft的ASP.NET团队的社区聚光灯、.NET博客和每周社区站立会议中。

about the cover illustration‌

关于封面插图

alt text

The caption for the illustration on the cover of ASP.NET Core in Action, Third Edition, is “The Captain Pasha. Kapudan pasha, admiral of the Turkish navy,” taken from a collection published in 1802 by William Miller.

《ASP.NET Corein Action》第三版封面上的插图说明是“帕夏船长。Kapudanpasha,土耳其海军上将”,摘自威廉·米勒(William Miller)于1802年出版的文集。

In those days, it was easy to identify where people lived and what their trade or station in life was by their dress alone.

在那些日子里,仅凭他们的衣着很容易确定人们居住的地方以及他们的职业或生活地位。

Manning celebrates the inventiveness and initiative of the computer business with book covers based on the rich diversity of regional culture centuries ago, brought back to life by pictures from collections such as this one.

Manning以几个世纪前丰富多样的地区文化为基础的书籍封面来庆祝计算机行业的创造力和主动性,这些书籍通过像这样的收藏中的图片重新焕发了生机。

ASP.NET Core in Action Contents

ASP.NET Core in Action CONTENTS

目录

front matter

preface acknowledgments about this book about the author about the cover illustration

前言 致谢 关于本书 关于作者 关于封面插图

1 Getting started with ASP.NET Core

1 ASP.NET Core 入门

Part 1 Getting started with minimal APIs

第1部分:开始使用最少的 API

2 Understanding ASP.NET Core

2 了解 ASP.NET Core

3 Your first application

3 您的第一个应用程序

4 Handling requests with the middleware pipeline

4 使用中间件管道处理请求

5 Creating a JSON API with minimal APIs

5 使用最少的 API创建 JSON API

6 Mapping URLs to endpoints using routing

6 使用路由将 URL 映射到端点

7 Model binding and validation in minimal APIs

7 在最少的 API 中进行模型绑定和验证

Part 2 Building complete applications

第2部分:构建完整的应用程序

8 An introduction to dependency injection

8 依赖关系注入简介

9 Registering services with dependency injection

9 使用依赖关系注入注册服务

10 Configuring an ASP.NET Core application

10 配置 ASP.NET Core 应用程序

11 Documenting APIs with OpenAPI

11 使用 OpenAPI记录 API

12 Saving data with Entity Framework Core

12 使用 Entity Framework Core 保存数据

Part 3 Generating HTML with Razor Pages and MVC

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

13 Creating a website with Razor Pages

13 使用 Razor Pages创建网站

14 Mapping URLs to Razor Pages using routing

14 使用路由将 URL 映射到 Razor Pages

15 Generating responses with page handlers in Razor Pages

15 使用 Razor Pages中的页面处理程序生成响应

16 Binding and validating requests with Razor Pages

16 使用 Razor Pages绑定和验证请求

17 Rendering HTML using Razor views

17 使用 Razor 视图呈现 HTML

18 Building forms with Tag Helpers

18 使用标记帮助程序构建表单

19 Creating a website with MVC controllers

19 使用 MVC 控制器创建网站

20 Creating an HTTP API using web API controllers

20 使用 Web API 控制器创建 HTTP API

21 The MVC and Razor Pages filter pipeline

21 MVC 和 Razor Pages 筛选器管道

22 Creating custom MVC and Razor Page filters

22 创建自定义 MVC 和 Razor 页面筛选器

Part 4 Securing and deploying your applications

第4部分:保护和部署应用程序

23 Authentication: Adding users to your application with Identity

23 身份验证:使用 Identity将用户添加到您的应用程序

24 Authorization: Securing your application

24 授权:保护您的应用程序

25 Authentication and authorization for APIs

25 API的身份验证和授权

26 Monitoring and troubleshooting errors with logging

26 使用日志记录监控和排除错误

27 Publishing and deploying your application

27 发布和部署您的应用程序

28 Adding HTTPS to an application

28 将 HTTPS 添加到应用程序

29 Improving your application's security

29 提高应用程序的安全性

Part 5 Going further with ASP.NET Core

第5部分:使ASP.NET Core更进一步

30 uilding ASP.NET Core apps with the generic host and Startup

30 使用通用主机和启动ASP.NET Core 应用程序

31 Advanced configuration of ASP.NET Core

31 ASP.NET Core的高级配置

32 Building custom MVC and Razor Pages components

32 构建自定义 MVC 和 Razor Pages 组件

33 Calling remote APIs with IHttpClientFactory

33 使用 IHttpClientFactory调用远程 API

34 Building background tasks and services

34 构建后台任务和服务

35 Testing applications with xUnit

35 使用 xUnit测试应用程序

36 Testing ASP.NET Core applications

36 测试 ASP.NET Core 应用程序

appendix A Preparing your development environment

附录 A 准备开发环境

appendix B Useful references index

附录 B 有用的参考索引