Author Archives: user

为公司省钱往往省不到自己身上

为公司省钱往往省不到自己身上

简单写个序:为公司省钱往往省不到自己身上,在某些方面,往往还会使自己的价值更低,省钱的好不一定会被公司记住,但因此而产生的风险和黑锅肯定是由自己来背。

花钱是为了创造更多更好的价值,所以不要替公司省钱,要为公司提供更好的产品。详细说明可见如下讨论文章:

其实老早就想写写了。但是一直太懒。又怕人说装13.但是想想还是写写吧。在企业IT管理中,总会遇到各种各样的问题。在提出新的需求的时候,新人和技术控们总喜欢用各种破解或者免费的软件去解决。

从我个人角度来看,这是比较错误的一种做法。当然这可能会与某些兄弟实际的情况有不同的地方。所以,只是一种大环境的概括,而并不是一个绝对的现象。大家如果说我这样做的,为啥老板不给加工资,为啥收入这么低。那么就需要想想为什么了。是不是需要换个老板了。

不要总说我老板根本不给钱,没用的。我认为,老板还不愿意给你加薪呢,你难道就不去争取了?说到底,员工和老板之间,无非是一个互相利用的关系,互相角力博弈的立场,互相拔河的比拼方向。老板利用你的能力,你利用老板的薪资,薪水与能力的角力博弈,老板不想加薪,而你又嫌工资低,他想少给点,你想多要点。 谈不上有什么感情。既然如此,那你还有什么理由说老板不同意不给钱进行IT投资呢?一句话:据理力争去争取属于你的东西。 没有高工资,舒适的工作环境,好用的工具都没有,那还干个屁啊?趁早换老板。 老婆不好找,但是找老板还是相对很容易的。

我入行差不多15年了吧。一般在企业中遇到一些新的问题。都是想要不要买什么东西。能用购买硬件解决的就不用软件。

有以下几种情况: 1.省钱省不到自己身上。开门办企业就是要花钱。很多技术总不想给老板要钱,总是要自己解决。 这导致以下的问题:出了问题找不到人帮忙,全靠自己。或者网上咨询。2. 想在老板前表功。觉得自己为公司省下了一大笔钱,老板应该感激,应该会给你表彰之类的。可以说这完全是一厢情愿。

能用硬件就尽量不要软件:为什么呢?因为出了问题,你会得到强力的原厂技术支持。有大部分的问题,不是靠个人能力可以解决的。如果你解决不了。黑锅就背定了,还不一定能得到老板的理解。他会认为你无能。之前你是省下了些钱,但是老板这个时候会完全忘记那些好处的。如果有硬件,有厂商。再不通情理的老板,也不会把所有的气都撒在你身上。至少有厂商垫背。你能力的问题会被老板们至少给排除一部分。 永远不要迷信技术,不要迷信你自己的能力。因为总有很多问题你搞不定。

其实能用硬件不用软件最重要的一点,前面没说。就是老板的心理问题。当然如果你坚持还是从技术角度出发,非要认为硬件不一定就比软件好,我也不反驳你。因为事实上也是这样。但是假如你买了一个深信服或者网康的上网行为管理在那里,老板能够看得见。维保过了,故障了。你说要花钱修或者续维保,老板一般都不会卡。如果设备坏了就麻烦。而且如果设备老旧,坏了,需要换新,老板一般也不会拒绝。因为公司已经习惯这个设备的功能使用了。但假如你买的是软件,这个不需要维保,你说要升级?老板一般会认为,我不是已经花过钱买了吗?怎么又要花钱?

看到有人在企业里ERP,邮件服务器居然用盗版。真的是不可思议了。你省下的钱老板给你了吗?出了问题是打个电话由厂商解决,还是心慌慌的在网上发贴求助?

只有在信息化上投入了大量的金钱,老板才会心疼这些投入,才会舍得高薪给IT,也就是给你了.搞几万几十万上百万服务器的人,永远比修电脑的待遇高。这个规则不仅仅适用于IT这个行业。试想想你新买苹果土豪金你可能会花200买个保护套,膜什么的。要是你花200块买个酷派你会这样买个200块的保护壳?假如你公司服务器全是PC代替的,路由交换都是DLINK,TPLINK。你觉得老板会给你开多少工资? 200万美金买一辆的车,3万块钱的QQ一比,人家刮花喷一下漆都能买你两辆车啊。购买的基数越大,用在维护上的基数也越大。老板开给你IT的工资,就是那个手机保护壳的钱,就是那个给车喷漆的钱,设备越贵,老板越舍得花钱。管理价值几百万上千万服务器设备的,永远比管理修电脑的收入高,就算是你管理的电脑数量价值总体超过了服务器。

只有买新的,买贵的设备,才能学到新东西。才能得到老板的重视。这是为什么呢?好的公司在买比如网络产品,什么TPLINK,DLINK根本不考虑。直接就是思科、华为、H3C等设备。服务器更不可能考虑什么组装服务器,普通PC当服务器。直接选品牌专业的服务器DELL、HP、联想、华为等等,稍微要求高点全是小型机,上百万一台的。我记得刚到苏州工作的时候原来公司有4台SUN的小型机,一台150多万块。有一个兄弟啥也不干,就维护这几台小型机,月薪比我们至少高1倍,每天还闲的没屁事可干。

贵的点东西,相对来说,稳定性就好些,你的事情就少些,但是这并不绝对,如果你非要抬杠我也没办法。自己的自由时间多些。说到这里都是泪,现在刚到一家台资企业,妈B的,居然还有08年以前的电脑,还有2004年买的笔记本台式机在用。大爷的了。。能用就凑合,工资更不用说,想加薪?还是换新IT吧。

每个老板最不舍得花钱的地方,IT投入一定名列前茅。如果你一定要赌运气。那就赌你的老板懂IT价值吧。

很多人说我们公司小,而且老板不可能批下资金的。但是,申请不申请资金是你的事。批不批准是老板的事。如果你自己不争取,老板会主动给你钱花?除非老板脑子进水了。老板还不愿意跟你加薪呢,你还不是一次又一次谈加薪的事情?但是话说回来,还是要看公司规模和实际情况。比如光办公室才几十个人,你就用思科4XXX系列?那真是找死了。但是这个又不绝对。假如你公司有MES系统,需要对每个产品进行标记,以便在任意一个产品故障时能够查到出来什么时间哪个生产线哪台设备哪个人生产出来的。这个时候就不能按公司规模来计算用什么产品了,就要从稳定角度出发,必须双核心冗余架构核心交换最好用思科4XXX系列,接入层交换机还要买冷备。所以,具体问题具体分析,不能一刀切。

不要再重复老板不批准,公司小不会投入这么多之类的屁话。说到底,老板和员工之间的关系,跟客户与厂商的关系是一样的。一个是想尽量少花钱拿到想要的,一个是想尽量高的价格卖出去手上的东西。

说下发生在我身上的事。上家公司刚成立的时候我就去了。信息化规划这边我从0开始。当时公司预算是900W。买的网络设备清一色的思科。光是两个核心4507,就花了40W。加上其他设备,光是网络硬件这块的就花去了100W不止。网络工程,服务器,存储,数据库,一年不到花了600多万。我说这种网络规模我管起来有点吃边。冗余架构的不太懂,需要去学习。老板就说了,花了这么多钱,要是管理不好不行。你去学吧。费用公司全出,报了一个CCNP,学费考试费加起来1W多,公司直接打给了培训中心,我去上课。 !!!¥¥¥¥在这家公司前面,是个马来西亚公司,那老板每次有事就问我,不花钱能搞定吗?我都说,想想办法。搞到最后,妈B的一箱网线都不给买,让去拆旧的不用的。电脑坏了就拆其他坏电脑上的硬件。最后要2个人用一台电脑了。。鼠标都不给买,总说,你自己想办法。最后因为要买一箱网线,老板就开骂说,我他妈的请你来干什么。总是要钱,自己想办法。 老子直接火了,对他说,妈的,老子不干了,你自己想办法弄网线去吧。

还有个例子。某证券公司的IT为了节省费用,使用了CENTOS装在了关键服务器上。在开市的前几个小时,发现服务器网络有问题,前端不能连接。找了N个高手来解决。眼看要开市了还没搞定,老板急的不行。有一个前来帮忙的忽然发现了问题所在。是网卡驱动的问题。虽然CENTOS95%以上的代码COPY了REDHAT,但偏偏这个网卡驱动不行。但是如果安装REDHAT就没有问题。后来临时编写了驱动才算是正常工作。老板火的不得了。就说公司就差这点钱吗?后来又重新安装购买了REDHAT.提议用CENTOS的兄弟不但没有在老板面前得好,还落得个收拾东西走人的下场。

如果你是一个技术控。那买使用最好的产品,才是提升能力最好的办法。所谓的技术,并不是你为老板省钱搞出来个ROS软路由,也不是用了几套破解的SERVER 操作系统,其实在我看来全是扯。其实只不过是考验你对主流或非主流的IT软硬件产品的了解熟悉程度而已。

如果你就想在小公司里混混就算了,要想进入好的公司,必须对技术有个全新的认识,那就是用最好的产品。你用ROS软路由用的再好,也没有会用思科路由的受到重视。比较有前途的好公司(并不一定大)是不会用你的什么ROUTEOS软路由,也不会用你的什么水之星之类的上网行为管理设备的。而是你解决问题的时间长短和出现问题的次数多少。

当然,我的意思也并不是说什么都要花钱。有时候,开源免费比收费的还要好。比如你为了方便知道什么设备宕机了好及时的修复。就可以用免费开源的网管工具。WEB服务器免费的总比收费的IIS好用。但是这种特例并不多。

总结:我仅仅是讲一种在企业中,IT面对问题的一种思路。而不是教你走极端。如果达不成我所说的目的便去跳楼上吊去。转变那种有了问题就怕花老板钱,怕老板不批的思路。转变有问题就想到使用盗版的思路。其实为什么IT业者待遇越来越差,地位越来越低,虽然不能说都是此类思维导致的,但是也占很大的原因比例。正所谓,NO ZUO NO DIE,自己都不尊重知识产权,不尊重技术,你一个出卖知识,出卖技术为生的人,凭什么老板会尊重IT人呢?会给你好的待遇呢?

https://hqyman.cn/post/3754.html

Ultimate ASP.NET Core Web API 33 BONUS 2 – INTRODUCTION TO CQRS AND MEDIATR WITH ASP.NET CORE WEB API

33 BONUS 2 - INTRODUCTION TO CQRS AND MEDIATR WITH ASP.NET CORE WEB API
33 奖励 2 - 使用 ASP.NET 核心 WEB API 的 CQRS 和 MEDIATR 简介

In this chapter, we will provide an introduction to the CQRS pattern and how the .NET library MediatR helps us build software with this architecture.‌
在本章中,我们将介绍 CQRS 模式以及 .NET 库 MediatR 如何帮助我们构建具有此体系结构的软件。

In the Source Code folder, you will find the folder for this chapter with two folders inside – start and end. In the start folder, you will find a prepared project for this section. We are going to use it to explain the implementation of CQRS and MediatR. We have used the existing project from one of the previous chapters and removed the things we don’t need or want to replace - like the service layer.
在 Source Code 文件夹中,您将找到本章的文件夹,其中包含两个文件夹 – start 和 end。在 start 文件夹中,您将找到此部分的准备工程。我们将使用它来解释 CQRS 和 MediatR 的实现。我们使用了前几章中的现有项目,并删除了我们不需要或不想替换的东西 - 比如服务层。

In the end folder, you will find a finished project for this chapter.
在 end 文件夹中,您将找到本章的已完成项目。

33.1 About CQRS and Mediator Pattern

33.1 关于 CQRS 和中介模式

The MediatR library was built to facilitate two primary software architecture patterns: CQRS and the Mediator pattern. Whilst similar, let’s spend a moment understanding the principles behind each pattern.‌
MediatR 库的构建是为了促进两种主要的软件架构模式:CQRS 和 Mediaator 模式。虽然相似,但让我们花点时间了解每种模式背后的原则。

33.1.1 CQRS

CQRS stands for “Command Query Responsibility Segregation”. As the acronym suggests, it’s all about splitting the responsibility of commands (saves) and queries (reads) into different models.
CQRS 代表 “Command Query Responsibility Segregation”。正如首字母缩略词所暗示的那样,这一切都是为了将命令 (saves) 和查询 (reads) 的责任拆分到不同的模型中。

If we think about the commonly used CRUD pattern (Create-Read- Update-Delete), we usually have the user interface interacting with a datastore responsible for all four operations. CQRS would instead have us split these operations into two models, one for the queries (aka “R”), and another for the commands (aka “CUD”).
如果我们考虑常用的 CRUD 模式(创建-读取-更新-删除),我们通常会让用户界面与负责所有四个作的数据存储进行交互。相反,CQRS 会让我们将这些作拆分为两个模型,一个用于查询(又名“R”),另一个用于命令(又名“CUD”)。

The following image illustrates how this works:
下图说明了其工作原理:

alt text

The Application simply separates the query and command models.
Application 只是将 query 和 command 模型分开。

The CQRS pattern makes no formal requirements of how this separation occurs. It could be as simple as a separate class in the same application (as we’ll see shortly with MediatR), all the way up to separate physical applications on different servers. That decision would be based on factors such as scaling requirements and infrastructure, so we won’t go into that decision path here.
CQRS 模式对这种分离的发生方式没有正式要求。它可以像同一应用程序中的单独类一样简单(我们稍后将在 MediatR 中看到),一直到不同服务器上的单独物理应用程序。该决策将基于扩展需求和基础设施等因素,因此我们不会在这里讨论该决策路径。

The key point being is that to create a CQRS system, we just need to split the reads from the writes.
关键是,要创建 CQRS 系统,我们只需要将读取与写入分开。

What problem is this trying to solve?
这试图解决什么问题?

Well, a common reason is when we design a system, we start with data storage. We perform database normalization, add primary and foreign keys to enforce referential integrity, add indexes, and generally ensure the “write system” is optimized. This is a common setup for a relational database such as SQL Server or MySQL. Other times, we think about the read use cases first, then try and add that into a database, worrying less about duplication or other relational DB concerns (often “document databases” are used for these patterns).
嗯,一个常见的原因是,当我们设计一个系统时,我们从数据存储开始。我们执行数据库规范化,添加主键和外键以强制引用完整性,添加索引,并且通常确保“写入系统”得到优化。这是关系数据库(如 SQL Server 或 MySQL)的常见设置。其他时候,我们首先考虑读取用例,然后尝试将其添加到数据库中,而不必担心重复或其他关系数据库问题(通常“文档数据库”用于这些模式)。

Neither approach is wrong. But the issue is that it’s a constant balancing act between reads and writes, and eventually one side will “win out”. All further development means both sides need to be analyzed, and often one is compromised.
这两种方法都没有错。但问题是,这是读取和写入之间的持续平衡行为,最终一方将 “胜出”。所有进一步的发展都意味着双方都需要分析,而且往往有一个会受到损害。

CQRS allows us to “break free” from these considerations and give each system the equal design and consideration it deserves without worrying about the impact of the other system. This has tremendous benefits on both performance and agility, especially if separate teams are working on these systems.
CQRS 使我们能够“摆脱”这些考虑,并为每个系统提供应有的平等设计和考虑,而无需担心其他系统的影响。这对性能和敏捷性都有巨大的好处,尤其是在不同的团队在这些系统上工作时。

33.1.2 Advantages and Disadvantages of CQRS‌

33.1.2 CQRS 的优点和缺点

The benefits of CQRS are:
CQRS 的优点是:

• Single Responsibility – Commands and Queries have only one job. It is either to change the state of the application or retrieve it. Therefore, they are very easy to reason about and understand.
单一职责 – 命令和查询只有一个作业。要么更改应用程序的状态,要么检索它。因此,它们很容易推理和理解。

• Decoupling – The Command or Query is completely decoupled from its handler, giving you a lot of flexibility on the handler side to implement it the best way you see fit.
Decoupling – Command 或 Query 与其处理程序完全解耦,在处理程序端为您提供了很大的灵活性,以便以您认为合适的最佳方式实施它。

• Scalability – The CQRS pattern is very flexible in terms of how you can organize your data storage, giving you options for great scalability. You can use one database for both Commands and Queries. You can use separate Read/Write databases, for improved performance, with messaging or replication between the databases for synchronization.
可伸缩性 – CQRS 模式在组织数据存储的方式方面非常灵活,为您提供了出色的可伸缩性选项。您可以对 Commands 和 Queries 使用一个数据库。您可以使用单独的读/写数据库来提高性能,并在数据库之间进行消息传递或复制以进行同步。

• Testability – It is very easy to test Command or Query handlers since they will be very simple by design, and perform only a single job.
可测试性 – 测试 Command 或 Query 处理程序非常容易,因为它们的设计非常简单,并且只执行一项工作。

Of course, it can’t all be good. Here are some of the disadvantages of CQRS:
当然,不可能都是好的。以下是 CQRS 的一些缺点:

• Complexity – CQRS is an advanced design pattern, and it will take you time to fully understand it. It introduces a lot of complexity that will create friction and potential problems in your project. Be sure to consider everything, before deciding to use it in your project.
复杂性 – CQRS 是一种高级设计模式,您需要花时间才能完全理解它。它引入了许多复杂性,这将在您的项目中产生摩擦和潜在问题。在决定在您的项目中使用它之前,请务必考虑所有因素。

• Learning Curve – Although it seems like a straightforward design pattern, there is still a learning curve with CQRS. Most developers are used to the procedural (imperative) style of writing code, and CQRS is a big shift away from that.
学习曲线 – 尽管它看起来是一个简单的设计模式,但 CQRS 仍然存在学习曲线。大多数开发人员都习惯了编写代码的过程(命令式)风格,而 CQRS 与此截然不同。

• Hard to Debug – Since Commands and Queries are decoupled from their handler, there isn’t a natural imperative flow of the application. This makes it harder to debug than traditional applications.
难以调试 – 由于命令和查询与其处理程序分离,因此应用程序没有自然的命令式流程。这使得它比传统应用程序更难调试。

33.1.3 Mediator Pattern‌

33.1.3 调解器模式

The Mediator pattern is simply defining an object that encapsulates how objects interact with each other. Instead of having two or more objects take a direct dependency on each other, they instead interact with a “mediator”, who is in charge of sending those interactions to the other party:
中介器模式只是定义一个对象,该对象封装了对象之间的交互方式。它们不是让两个或多个对象彼此直接依赖,而是与 “中介” 交互,该 “中介” 负责将这些交互发送给另一方:

alt text

In this image, SomeService sends a message to the Mediator, and the Mediator then invokes multiple services to handle the message. There is no direct dependency between any of the blue components.
在此图中,SomeService 向 Mediator 发送一条消息,然后 Mediator 调用多个服务来处理该消息。任何蓝色组件之间都没有直接依赖关系。

The reason the Mediator pattern is useful is the same reason patterns like Inversion of Control are useful. It enables “loose coupling”, as the dependency graph is minimized and therefore code is simpler and easier to test. In other words, the fewer considerations a component has, the easier it is to develop and evolve.
中介者模式有用的原因与像 Inversion of Control 这样的模式有用的原因相同。它支持“松散耦合”,因为依赖关系图被最小化,因此代码更简单,更容易测试。换句话说,组件的考虑因素越少,它就越容易开发和发展。

We saw in the previous image how the services have no direct dependency, and the producer of the messages doesn’t know who or how many things are going to handle it. This is very similar to how a message broker works in the “publish/subscribe” pattern. If we wanted to add another handler we could, and the producer wouldn’t have to be modified.
在上图中,我们看到服务没有直接依赖关系,消息的生成者不知道谁或多少事情将处理它。这与消息代理在 “publish/subscribe” 模式中的工作方式非常相似。如果我们想添加另一个处理程序,我们可以这样做,并且不必修改 producer。

Now that we’ve been over some theory, let’s talk about how MediatR makes all these things possible.
现在我们已经了解了一些理论,让我们谈谈 MediatR 如何使所有这些事情成为可能。

33.2 How MediatR facilitates CQRS and Mediator Patterns

33.2 MediatR 如何促进 CQRS 和中介模式

You can think of MediatR as an “in-process” Mediator implementation, that helps us build CQRS systems. All communication between the user interface and the data store happens via MediatR.‌
您可以将 MediatR 视为“进程内”中介器实现,它帮助我们构建 CQRS 系统。用户界面和数据存储之间的所有通信都通过 MediatR 进行。

The term “in process” is an important limitation here. Since it’s a .NET library that manages interactions within classes on the same process, it’s not an appropriate library to use if we want to separate the commands and queries across two systems. A better approach would be to use a message broker such as Kafka or Azure Service Bus.
术语 “in process” 在这里是一个重要的限制。由于它是一个 .NET 库,用于管理同一进程上类内的交互,因此如果我们想跨两个系统分离命令和查询,则它不是一个合适的库。更好的方法是使用消息代理,例如 Kafka 或 Azure 服务总线。

However, for this chapter, we are going to stick with a simple single- process CQRS system, so MediatR fits the bill perfectly.
但是,在本章中,我们将坚持使用一个简单的单进程 CQRS 系统,因此 MediatR 完全符合要求。

33.3 Adding Application Project and Initial Configuration

33.3 添加应用程序项目和初始配置

Let’s start by opening the starter project from the start folder. You will‌ see that we don’t have the Service nor the Service.Contracts projects. Well, we don’t need them. We are going to use CQRS with MediatR to replace that part of our solution.
让我们从 start 文件夹打开 starter 项目。您将看到我们没有 Service 和 Service.Contracts 项目。好吧,我们不需要它们。我们将使用 CQRS 和 MediatR 来替换我们解决方案的该部分。

But, we do need an additional project for our business logic so, let’s create a new class library (.NET Core) and name it Application.
但是,我们确实需要一个额外的项目来运行我们的业务逻辑,因此,让我们创建一个新的类库 (.NET Core) 并将其命名为 Application。

Additionally, we are going to add a new class named AssemblyReference. We will use it for the same purpose as we used the class with the same name in the Presentation project:
此外,我们将添加一个名为 AssemblyReference 的新类。我们将将其用于与 Presentation 项目中使用同名类相同的目的:

public static class AssemblyReference { }

Now let’s install a couple of packages.
现在让我们安装几个包。

The first package we are going to install is the MediatR in the Application project:
我们要安装的第一个包是 Application 项目中的 MediatR:

PM> install-package MediatR

Then in the main project, we are going to install another package that wires up MediatR with the ASP.NET dependency injection container:
然后在主项目中,我们将安装另一个包,该包将 MediatR 与 ASP.NET 依赖项注入容器连接起来:

PM> install-package MediatR.Extensions.Microsoft.DependencyInjection

After the installations, we are going to configure MediatR in the Program class:
安装完成后,我们将在 Program 类中配置 MediatR:

builder.Services.AddMediatR(typeof(Application.AssemblyReference).Assembly);

For this, we have to reference the Application project, and add a using directive:
为此,我们必须引用 Application 项目,并添加一个 using 指令:

using MediatR;

The AddMediatR method will scan the project assembly that contains the handlers that we are going to use to handle our business logic. Since we are going to place those handlers in the Application project, we are using the Application’s assembly as a parameter.
AddMediatR 方法将扫描包含我们将用于处理业务逻辑的处理程序的项目程序集。由于我们将这些处理程序放在 Application 项目中,因此我们将 Application 的程序集用作参数。

Before we continue, we have to reference the Application project from the Presentation project.
在继续之前,我们必须从 Presentation 项目中引用 Application 项目。

Now MediatR is configured, and we can use it inside our controller.
现在 MediatR 已经配置好了,我们可以在控制器中使用它。

In the Controllers folder of the Presentation project, we are going to find a single controller class. It contains only a base code, and we are going to modify it by adding a sender through the constructor injection:
在 Presentation 项目的 Controllers 文件夹中,我们将找到一个控制器类。它只包含一个基本代码,我们将通过构造函数注入添加一个 sender 来修改它:

[Route("api/companies")] [ApiController] public class CompaniesController : ControllerBase { private readonly ISender _sender; public CompaniesController(ISender sender) => _sender = sender; }

Here we inject the ISender interface from the MediatR namespace. We are going to use this interface to send requests to our handlers.
在这里,我们从 MediatR 命名空间注入 ISender 接口。我们将使用此接口将请求发送到我们的处理程序。

We have to mention one thing about using ISender and not the IMediator interface. From the MediatR version 9.0, the IMediator interface is split into two interfaces:
我们必须提到关于使用 ISender 而不是 IMediator 接口的一件事。从 MediatR 版本 9.0 开始,IMediator 接口分为两个接口:

public interface ISender { Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default); Task<object?> Send(object request, CancellationToken cancellationToken = default); } public interface IPublisher { Task Publish(object notification, CancellationToken cancellationToken = default); Task Publish<TNotification>(TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification; } public interface IMediator : ISender, IPublisher { }

So, by looking at the code, it is clear that you can continue using the IMediator interface to send requests and publish notifications. But it is recommended to split that by using ISender and IPublisher interfaces.
因此,通过查看代码,很明显您可以继续使用 IMediator 接口来发送请求和发布通知。但建议使用 ISender 和 IPublisher 接口来拆分该接口。

With that said, we can continue with the Application’s logic implementation.
话虽如此,我们可以继续 Application 的 logic implementation。

33.4 Requests with MediatR

33.4 使用 MediatR 的请求

MediatR Requests are simple request-response style messages where a single request is synchronously handled by a single handler (synchronous from the request point of view, not C# internal async/await). Good use cases here would be returning something from a database or updating a database.‌
MediatR 请求是简单的请求-响应样式的消息,其中单个请求由单个处理程序同步处理(从请求的角度来看是同步的,而不是 C# 内部的 async/await)。这里的好用例是从数据库返回一些东西或更新数据库。

There are two types of requests in MediatR. One that returns a value, and one that doesn’t. Often this corresponds to reads/queries (returning a value) and writes/commands (usually doesn’t return a value).
MediatR 中有两种类型的请求。一个返回值,另一个不返回值。这通常对应于 reads/queries(返回一个值)和 writes/commands(通常不返回一个值)。

So, before we start sending requests, we are going to create several folders in the Application project to separate queries, commands, and handlers:
因此,在开始发送请求之前,我们将在 Application 项目中创建多个文件夹,以分隔查询、命令和处理程序:

alt text

Since we are going to work only with the company entity, we are going to place our queries, commands, and handlers directly into these folders.
由于我们只要使用 company 实体,因此我们将查询、命令和处理程序直接放入这些文件夹中。

But in larger projects with multiple entities, we can create additional folders for each entity inside each of these folders for better organization.
但在具有多个实体的大型项目中,我们可以为每个文件夹中的每个实体创建额外的文件夹,以便更好地组织。

Also, as we already know, we are not going to send our entities as a result to the client but DTOs, so we have to reference the Shared project.
此外,正如我们已经知道的,我们不会将实体作为结果发送给客户端,而是发送给 DTO,因此我们必须引用 Shared 项目。

That said, let’s start with our first query. Let’s create it in the Queries folder:
也就是说,让我们从第一个查询开始。让我们在 Queries 文件夹中创建它:

public sealed record GetCompaniesQuery(bool TrackChanges) : IRequest<IEnumerable<CompanyDto>>;

Here, we create the GetCompaniesQuery record, which implements IRequest<IEnumerable<CompanyDto>>. This simply means our request will return a list of companies.
在这里,我们创建 GetCompaniesQuery 记录,该记录实现 IRequest<IEnumerable<CompanyDto>>。这只是意味着我们的请求将返回公司列表。

Here we need two additional namespaces:
这里我们需要两个额外的命名空间:

using MediatR;
using Shared.DataTransferObjects;

Once we send the request from our controller’s action, we are going to see the usage of this query.
一旦我们从控制器的 action 发送请求,我们将看到这个查询的用法。

After the query, we need a handler. This handler in simple words will be our replacement for the service layer method that we had in our project. In our previous project, all the service classes were using the repository to access the database – we will make no difference here. For that, we have to reference the Contracts project so we can access the IRepositoryManager interface.
查询之后,我们需要一个处理程序。简单来说,这个处理程序将成为我们项目中服务层方法的替代品。在我们之前的项目中,所有服务类都使用存储库来访问数据库 – 我们在这里不会有什么区别。为此,我们必须引用 Contracts 项目,以便我们可以访问 IRepositoryManager 接口。

After adding the reference, we can create a new GetCompaniesHandler class in the Handlers folder:
添加引用后,我们可以在 Handlers 文件夹中创建一个新的 GetCompaniesHandler 类:

internal sealed class GetCompaniesHandler : IRequestHandler<GetCompaniesQuery, IEnumerable<CompanyDto>> { private readonly IRepositoryManager _repository; public GetCompaniesHandler(IRepositoryManager repository) => _repository = repository; public Task<IEnumerable<CompanyDto>> Handle(GetCompaniesQuery request, CancellationToken cancellationToken) { throw new NotImplementedException(); } }

Our handler inherits from IRequestHandler<GetCompaniesQuery,IEnumerable<Product>>. This means this class will handle GetCompaniesQuery, in this case, returning the list of companies.
我们的处理程序继承自 IRequestHandler<GetCompaniesQuery,IEnumerable<Product>>。这意味着此类将处理 GetCompaniesQuery,在本例中,返回公司列表。

We also inject the repository through the constructor and add a default implementation of the Handle method, required by the IRequestHandler interface.
我们还通过构造函数注入存储库,并添加 IRequestHandler 接口所需的 Handle 方法的默认实现。

These are the required namespaces:
这些是必需的命名空间:

using Application.Queries; 
using Contracts;
using MediatR;
using Shared.DataTransferObjects;

Of course, we are not going to leave this method to throw an exception. But before we add business logic, we have to install AutoMapper in the Application project:
当然,我们不会让此方法抛出异常。但在添加业务逻辑之前,我们必须在 Application 项目中安装 AutoMapper:

PM> Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection

Register the package in the Program class:
在 Program 类中注册包:

builder.Services.AddAutoMapper(typeof(Program));
builder.Services.AddMediatR(typeof(Application.AssemblyReference).Assembly);

And create the MappingProfile class, also in the main project, with a single mapping rule:
并在主项目中使用 single mapping rule 创建 MappingProfile 类:

public class MappingProfile : Profile { public MappingProfile() { CreateMap<Company, CompanyDto>() .ForMember(c => c.FullAddress, opt => opt.MapFrom(x => string.Join(' ', x.Address, x.Country))); } }

Everything with these actions is familiar since we’ve already used AutoMapper in our project.
这些作的所有内容都是熟悉的,因为我们已经在项目中使用了 AutoMapper。

Now, we can modify the handler class:
现在,我们可以修改 handler 类:

internal sealed class GetCompaniesHandler : IRequestHandler<GetCompaniesQuery, IEnumerable<CompanyDto>> { private readonly IRepositoryManager _repository; private readonly IMapper _mapper; public GetCompaniesHandler(IRepositoryManager repository, IMapper mapper) {_repository = repository; _mapper = mapper; } public async Task<IEnumerable<CompanyDto>> Handle(GetCompaniesQuery request, CancellationToken cancellationToken) { var companies = await _repository.Company.GetAllCompaniesAsync(request.TrackChanges); var companiesDto = _mapper.Map<IEnumerable<CompanyDto>>(companies); return companiesDto; } }

This logic is also familiar since we had almost the same one in our GetAllCompaniesAsync service method. One difference is that we are passing the track changes parameter through the request object.
此逻辑也很熟悉,因为我们在 GetAllCompaniesAsync 服务方法中具有几乎相同的逻辑。一个区别是,我们通过 request 对象传递 track changes 参数。

Now, we can modify CompaniesController:
现在,我们可以修改 CompaniesController:

[HttpGet] public async Task<IActionResult> GetCompanies() { var companies = await _sender.Send(new GetCompaniesQuery(TrackChanges: false)); return Ok(companies); }

We use the Send method to send a request to our handler and pass the GetCompaniesQuery as a parameter. Nothing more than that. We also need an additional namespace:
我们使用 Send 方法向处理程序发送请求,并将 GetCompaniesQuery 作为参数传递。仅此而已。我们还需要一个额外的命名空间:

using Application.Queries;

Our controller is clean as it was with the service layer implemented. But this time, we don’t have a single service class to handle all the methods but a single handler to take care of only one thing.
我们的控制器与实施服务层时一样干净。但是这一次,我们没有一个服务类来处理所有方法,而只有一个处理程序来处理一件事。

Now, we can test this:
现在,我们可以测试一下:
https://localhost:5001/api/companies

alt text

Everything works great. With this in mind, we can continue and implement the logic for fetching a single company.
一切都很好。考虑到这一点,我们可以继续并实现获取单个公司的逻辑。

So, let’s start with the query in the Queries folder:
因此,让我们从 Queries 文件夹中的查询开始:

public sealed record GetCompanyQuery(Guid Id, bool TrackChanges) : IRequest<CompanyDto>;

Then, let’s implement a new handler:
然后,让我们实现一个新的处理程序:

internal sealed class GetCompanyHandler : IRequestHandler<GetCompanyQuery, CompanyDto> { private readonly IRepositoryManager _repository; private readonly IMapper _mapper; public GetCompanyHandler(IRepositoryManager repository, IMapper mapper) { _repository = repository; _mapper = mapper; } public async Task<CompanyDto> Handle(GetCompanyQuery request, CancellationToken cancellationToken) { var company = await _repository.Company.GetCompanyAsync(request.Id, request.TrackChanges); if (company is null) throw new CompanyNotFoundException(request.Id); var companyDto = _mapper.Map<CompanyDto>(company); return companyDto;} }

So again, our handler inherits from the IRequestHandler interface accepting the query as the first parameter and the result as the second. Then, we inject the required services and familiarly implement the Handle method.
因此,我们的处理程序再次继承自 IRequestHandler 接口,接受查询作为第一个参数,将结果作为第二个参数。然后,我们注入所需的服务并熟悉地实现 Handle 方法。

We need these namespaces here:
我们在此处需要这些命名空间:

using Application.Queries; 
using AutoMapper;
using Contracts;
using Entities.Exceptions; 
using MediatR;
using Shared.DataTransferObjects;

Lastly, we have to add another action in CompaniesController:
最后,我们必须在 CompaniesController 中添加另一个作:

[HttpGet("{id:guid}", Name = "CompanyById")] public async Task<IActionResult> GetCompany(Guid id) { var company = await _sender.Send(new GetCompanyQuery(id, TrackChanges: false)); return Ok(company); }

Awesome, let’s test it:
太棒了,让我们测试一下:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

Excellent, we can see the company DTO in the response body. Additionally, we can try an invalid request:
太好了,我们可以在响应正文中看到公司 DTO。此外,我们可以尝试无效的请求:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce2

alt text

And, we can see this works as well.
而且,我们可以看到这也有效。

33.5 Commands with MediatR

33.5 使用 MediatR 的命令

As with both queries, we are going to start with a command record creation inside the Commands folder:‌
与这两个查询一样,我们将从 Commands 文件夹中的命令记录创建开始:

public sealed record CreateCompanyCommand(CompanyForCreationDto Company) : IRequest<CompanyDto>;

Our command has a single parameter sent from the client, and it inherits from IRequest. Our request has to return CompanyDto because we will need it, in our action, to create a valid route in the return statement.
我们的命令有一个从客户端发送的参数,它继承自 IRequest。我们的请求必须返回 CompanyDto,因为我们在作中需要它来在 return 语句中创建有效的路由。

After the query, we are going to create another handler:
查询之后,我们将创建另一个处理程序:

internal sealed class CreateCompanyHandler : IRequestHandler<CreateCompanyCommand, CompanyDto> { private readonly IRepositoryManager _repository; private readonly IMapper _mapper; public CreateCompanyHandler(IRepositoryManager repository, IMapper mapper) { _repository = repository; _mapper = mapper; } public async Task<CompanyDto> Handle(CreateCompanyCommand request, CancellationToken cancellationToken) { var companyEntity = _mapper.Map<Company>(request.Company); _repository.Company.CreateCompany(companyEntity); await _repository.SaveAsync();var companyToReturn = _mapper.Map<CompanyDto>(companyEntity); return companyToReturn; } }

So, we inject our services and implement the Handle method as we did with the service method. We map from the creation DTO to the entity, save it to the database, and map it to the company DTO object.
因此,我们注入我们的服务并实现 Handle 方法,就像我们对 service 方法所做的那样。我们从创建 DTO 映射到实体,将其保存到数据库,并将其映射到公司 DTO 对象。

Then, before we add a new mapping rule in the MappingProfile class:
然后,在我们在 MappingProfile 类中添加新的映射规则之前:

CreateMap<CompanyForCreationDto, Company>();

Now, we can add a new action in a controller:
现在,我们可以在控制器中添加一个新的动作:

[HttpPost] public async Task<IActionResult> CreateCompany([FromBody] CompanyForCreationDto companyForCreationDto) { if (companyForCreationDto is null) return BadRequest("CompanyForCreationDto object is null"); var company = await _sender.Send(new CreateCompanyCommand(companyForCreationDto)); return CreatedAtRoute("CompanyById", new { id = company.Id }, company); }

That’s all it takes. Now we can test this:
就这样。现在我们可以测试一下:
https://localhost:5001/api/companies

alt text

A new company is created, and if we inspect the Headers tab, we are going to find the link to fetch this new company:
创建了一个新公司,如果我们检查 Headers 选项卡,我们将找到获取这家新公司的链接:

alt text

There is one important thing we have to understand here. We are communicating to a datastore via simple message constructs without having any idea on how it’s being implemented. The commands and queries could be pointing to different data stores. They don’t know how their request will be handled, and they don’t care.
在这里,我们必须了解一件重要的事情。我们通过简单的消息构造与 datastore 通信,但不知道它是如何实现的。命令和查询可能指向不同的数据存储。他们不知道他们的请求将如何处理,他们也不在乎。

33.5.1 Update Command‌

33.5.1 update 命令

Following the same principle from the previous example, we can implement the update request.
按照前面示例中的相同原则,我们可以实现 update 请求。

Let’s start with the command:
让我们从命令开始:

public sealed record UpdateCompanyCommand
(Guid Id, CompanyForUpdateDto Company, bool TrackChanges) : IRequest;

This time our command inherits from IRequest without any generic parameter. That’s because we are not going to return any value with this request.
这次我们的命令继承自 IRequest,没有任何泛型参数。那是因为我们不会在这个请求中返回任何值。

Let’s continue with the handler implementation:
让我们继续处理程序实现:

internal sealed class UpdateCompanyHandler : IRequestHandler<UpdateCompanyCommand, Unit> { private readonly IRepositoryManager _repository; private readonly IMapper _mapper; public UpdateCompanyHandler(IRepositoryManager repository, IMapper mapper) { _repository = repository; _mapper = mapper; } public async Task<Unit> Handle(UpdateCompanyCommand request, CancellationToken cancellationToken) {var companyEntity = await _repository.Company.GetCompanyAsync(request.Id, request.TrackChanges); if (companyEntity is null) throw new CompanyNotFoundException(request.Id); _mapper.Map(request.Company, companyEntity); await _repository.SaveAsync(); return Unit.Value; } }

This handler inherits from IRequestHandler<UpdateCompanyCommand, Unit>. This is new for us because the first time our command is not returning any value. But IRequestHandler always accepts two parameters (TRequest and TResponse). So, we provide the Unit structure for the TResponse parameter since it represents the void type.
此处理程序继承自 IRequestHandler<UpdateCompanyCommand, Unit> .这对我们来说是新的,因为第一次我们的命令没有返回任何值。但 IRequestHandler 始终接受两个参数(TRequest 和 TResponse)。因此,我们为 TResponse 参数提供了 Unit 结构,因为它表示 void 类型。

Then the Handle implementation is familiar to us except for the return part. We have to return something from the Handle method and we use Unit.Value.
然后,除了 return 部分之外,我们熟悉 Handle 实现。我们必须从 Handle 方法返回一些内容,并使用 Unit.Value。

Before we modify the controller, we have to add another mapping rule:
在我们修改控制器之前,我们必须添加另一个映射规则:

CreateMap<CompanyForUpdateDto, Company>();

Lastly, let’s add a new action in the controller:
最后,让我们在控制器中添加一个新作:

[HttpPut("{id:guid}")] public async Task<IActionResult> UpdateCompany(Guid id, CompanyForUpdateDto companyForUpdateDto) { if (companyForUpdateDto is null) return BadRequest("CompanyForUpdateDto object is null"); await _sender.Send(new UpdateCompanyCommand(id, companyForUpdateDto, TrackChanges: true)); return NoContent(); }

At this point, we can send a PUT request from Postman:
此时,我们可以从 Postman 发送一个 PUT 请求:

https://localhost:5001/api/companies/7aea16e2-74b9-4fd9-c22a-08d9961aa2d5

alt text

There is the 204 status code.
有 204 状态代码。

If you fetch this company, you will find the name updated for sure.
如果你找到这家公司,你肯定会发现名称更新了。

33.5.2 Delete Command‌

33.5.2 Delete 命令

After all of this implementation, this one should be pretty straightforward.
在所有这些实现之后,这个应该非常简单。

Let’s start with the command:
让我们从命令开始:

public record DeleteCompanyCommand(Guid Id, bool TrackChanges) : IRequest;

Then, let’s continue with a handler:
然后,让我们继续使用处理程序:

internal sealed class DeleteCompanyHandler : IRequestHandler<DeleteCompanyCommand, Unit> { private readonly IRepositoryManager _repository; public DeleteCompanyHandler(IRepositoryManager repository) => _repository = repository; public async Task<Unit> Handle(DeleteCompanyCommand request, CancellationToken cancellationToken) { var company = await _repository.Company.GetCompanyAsync(request.Id, request.TrackChanges); if (company is null) throw new CompanyNotFoundException(request.Id); _repository.Company.DeleteCompany(company); await _repository.SaveAsync(); return Unit.Value; } }

Finally, let’s add one more action inside the controller:
最后,让我们在控制器中再添加一个操作:

[HttpDelete("{id:guid}")]
public async Task<IActionResult> DeleteCompany(Guid id) { await _sender.Send(new DeleteCompanyCommand(id, TrackChanges: false)); return NoContent(); }

That’s it. Pretty easy.We can test this now:
就是这样。很简单。我们现在可以测试一下:
https://localhost:5001/api/companies/7aea16e2-74b9-4fd9-c22a-08d9961aa2d5

alt text

It works great.
它效果很好。

Now that we know how to work with requests using MediatR, let’s see how to use notifications.
现在,我们已经知道如何使用 MediatR 处理请求,让我们看看如何使用通知。

33.6 MediatR Notifications

So for we’ve only seen a single request being handled by a single handler. However, what if we want to handle a single request by multiple handlers?‌
因此,我们只看到单个请求由单个处理程序处理。但是,如果我们想处理多个处理程序的单个请求怎么办?

That’s where notifications come in. In these situations, we usually have multiple independent operations that need to occur after some event. Examples might be:
这就是通知的用武之地。在这些情况下,我们通常会有多个独立的作需要在某些事件之后发生。示例可能是:

• Sending an email

• Invalidating a cache

• ...

To demonstrate this, we will update the delete company flow we created previously to publish a notification and have it handled by two handlers.
为了演示这一点,我们将更新之前创建的删除公司流程,以发布通知并让两个处理程序处理该通知。

Sending an email is out of the scope of this book (you can learn more about that in our Bonus 6 Security book). But to demonstrate the behavior of notifications, we will use our logger service and log a message as if the email was sent.
发送电子邮件不在本书的讨论范围之内(您可以在我们的 Bonus 6 Security 书籍中了解更多信息)。但为了演示通知的行为,我们将使用我们的 logger 服务并记录一条消息,就像电子邮件已发送一样。

So, the flow will be - once we delete the Company, we want to inform our administrators with an email message that the delete has action occurred.
因此,流程将是 - 删除公司后,我们希望通过电子邮件通知管理员发生了删除作。

That said, let’s start by creating a new Notifications folder inside the Application project and add a new notification in that folder:
也就是说,让我们首先在 Application 项目中创建一个新的 Notifications 文件夹,然后在该文件夹中添加新的通知:

public sealed record CompanyDeletedNotification(Guid Id, bool TrackChanges) : INotification;

The notification has to inherit from the INotification interface. This is the equivalent of the IRequest we saw earlier, but for Notifications.
通知必须继承自 INotification 接口。这相当于我们之前看到的 IRequest,但用于 Notifications。

As we can conclude, notifications don’t return a value. They work on the fire and forget principle, like publishers.
我们可以得出结论,通知不返回值。他们像出版商一样,按照 Fire and Forget 的原则工作。

Next, we are going to create a new Emailhandler class:
接下来,我们将创建一个新的 Emailhandler 类:

internal sealed class EmailHandler : INotificationHandler<CompanyDeletedNotification> { private readonly ILoggerManager _logger; public EmailHandler(ILoggerManager logger) => _logger = logger; public async Task Handle(CompanyDeletedNotification notification, CancellationToken cancellationToken) { _logger.LogWarn($"Delete action for the company with id: {notification.Id} has occurred."); await Task.CompletedTask; } }

Here, we just simulate sending our email message in an async manner. Without too many complications, we use our logger service to process the message.
在这里,我们只是模拟以异步方式发送电子邮件。没有太多的复杂性,我们使用 logger 服务来处理消息。

Let’s continue by modifying the DeleteCompanyHandler class:
我们继续修改 DeleteCompanyHandler 类:

internal sealed class DeleteCompanyHandler : INotificationHandler<CompanyDeletedNotification> { private readonly IRepositoryManager _repository; public DeleteCompanyHandler(IRepositoryManager repository) => _repository = repository; public async Task Handle(CompanyDeletedNotification notification, CancellationToken cancellationToken) { var company = await _repository.Company.GetCompanyAsync(notification.Id, notification.TrackChanges); if (company is null) throw new CompanyNotFoundException(notification.Id); _repository.Company.DeleteCompany(company); await _repository.SaveAsync(); } }

This time, our handler inherits from the INotificationHandler interface, and it doesn’t return any value – we’ve modified the method signature and removed the return statement.
这一次,我们的处理程序继承自 INotificationHandler 接口,它不返回任何值 – 我们修改了方法签名并删除了 return 语句。

Finally, we have to modify the controller’s constructor:
最后,我们必须修改控制器的构造函数:

private readonly ISender _sender; private readonly IPublisher _publisher; public CompaniesController(ISender sender, IPublisher publisher) { _sender = sender; _publisher = publisher; }

We inject another interface, which we are going to use to publish notifications.
我们注入另一个接口,我们将使用它来发布通知。

And, we have to modify the DeleteCompany action:
而且,我们必须修改 DeleteCompany作:

[HttpDelete("{id:guid}")] public async Task<IActionResult> DeleteCompany(Guid id) { await _publisher.Publish(new CompanyDeletedNotification(id, TrackChanges: false)); return NoContent(); }

To test this, let’s create a new company first:
为了测试这一点,让我们先创建一个新公司:

alt text

Now, if we send the Delete request, we are going to receive the 204 NoContent response:
现在,如果我们发送 Delete 请求,我们将收到 204 NoContent 响应:

https://localhost:5001/api/companies/e06089af-baeb-44ef-1fdf-08d99630e212

alt text

And also, if we inspect the logs, we will find a new logged message stating that the delete action has occurred:
此外,如果我们检查日志,我们将找到一条新的日志记录消息,指出已发生删除操作:

alt text

33.7 MediatR Behaviors

33.7 MediatR 行为

Often when we build applications, we have many cross-cutting concerns. These include authorization, validating, and logging.‌
通常,当我们构建应用程序时,我们有许多横切关注点。这些作包括 authorization、validation 和 logging。

Instead of repeating this logic throughout our handlers, we can make use of Behaviors. Behaviors are very similar to ASP.NET Core middleware in that they accept a request, perform some action, then (optionally) pass along the request.
我们可以使用 Behaviors,而不是在整个处理程序中重复这个逻辑。行为与 ASP.NET Core 中间件非常相似,因为它们接受请求,执行一些作,然后(可选地)传递请求。

In this section, we are going to use behaviors to perform validation on the DTOs that come from the client.
在本节中,我们将使用行为对来自客户端的 DTO 执行验证。

As we have already learned in chapter 13, we can perform the validation by using data annotations attributes and the ModelState dictionary. Then we can extract the validation logic into action filters to clear our actions. Well, we can apply all of that to our current solution as well.
正如我们在第 13 章中学到的那样,我们可以使用数据注释属性和 ModelState 字典来执行验证。然后,我们可以将验证逻辑提取到作筛选器中以清除我们的作。好吧,我们也可以将所有这些应用到我们当前的解决方案中。

But, some developers have a preference for using fluent validation over data annotation attributes. In that case, behaviors are the perfect place to execute that validation logic.
但是,一些开发人员更喜欢使用 Fluent 验证而不是数据注释属性。在这种情况下,行为是执行该验证逻辑的完美位置。

So, let’s go step by step and add the fluent validation in our project first and then use behavior to extract validation errors if any, and return them to the client.
因此,让我们一步一步地开始,首先在我们的项目中添加 Fluent 验证,然后使用 behavior 提取验证错误(如果有),并将它们返回给客户端。

33.7.1 Adding Fluent Validation‌

33.7.1 添加 Fluent 验证

The FluentValidation library allows us to easily define very rich custom validation for our classes. Since we are implementing CQRS, it makes the most sense to define validation for our Commands. We should not bother ourselves with defining validators for Queries, since they don’t contain any behavior. We use Queries only for fetching data from the application.
FluentValidation 库允许我们轻松地为类定义非常丰富的自定义验证。由于我们正在实现 CQRS,因此为命令定义验证是最有意义的。我们不应该费心为 Queries 定义验证器,因为它们不包含任何行为。我们仅使用 Queries 从应用程序获取数据。

So, let’s start by installing the FluentValidation package in the Application project:
因此,让我们首先在 Application 项目中安装 FluentValidation 包:

PM> install-package FluentValidation.AspNetCore

The FluentValidation.AspNetCore package installs both FluentValidation and FluentValidation.DependencyInjectionExtensions packages.
FluentValidation.AspNetCore 包同时安装 FluentValidation 和 FluentValidation.DependencyInjectionExtensions 包。

After the installation, we are going to register all the validators inside the service collection by modifying the Program class:
安装完成后,我们将通过修改 Program 类来注册服务集合中的所有验证器:

builder.Services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assem bly);
builder.Services.AddMediatR(typeof(Application.AssemblyReference).Assembly); builder.Services.AddAutoMapper(typeof(Program));

Then, let’s create a new Validators folder inside the Application project and add a new class inside:
然后,让我们在 Application 项目中创建一个新的 Validators 文件夹,并在其中添加一个新类:

public sealed class CreateCompanyCommandValidator : AbstractValidator<CreateCompanyCommand> {public CreateCompanyCommandValidator() { RuleFor(c => c.Company.Name).NotEmpty().MaximumLength(60); RuleFor(c => c.Company.Address).NotEmpty().MaximumLength(60); } }

The following using directives are necessary for this class:
此类需要以下 using 指令:

using Application.Commands; 
using FluentValidation;

We create the CreateCompanyCommandValidator class that inherits from the AbstractValidator<T> class, specifying the type CreateCompanyCommand. This lets FluentValidation know that this validation is for the CreateCompanyCommand record. Since this record contains a parameter of type CompanyForCreationDto, which is the object that we have to validate since it comes from the client, we specify the rules for properties from that DTO.
我们创建从 AbstractValidator<T> 类继承的 CreateCompanyCommandValidator 类,并指定类型 CreateCompanyCommand。这让 FluentValidation 知道此验证是针对 CreateCompanyCommand 记录的。由于此记录包含一个 CompanyForCreationDto 类型的参数,该参数是我们必须验证的对象,因为它来自客户端,因此我们为该 DTO 中的属性指定规则。

The NotEmpty method specifies that the property can’t be null or empty, and the MaximumLength method specifies the maximum string length of the property.
NotEmpty 方法指定属性不能为 null 或为空,MaximumLength 方法指定属性的最大字符串长度。

33.7.2 Creating Decorators with MediatR PipelineBehavior

33.7.2 使用 MediatR PipelineBehavior 创建装饰器

The CQRS pattern uses Commands and Queries to convey information, and receive a response. In essence, it represents a request-response pipeline. This gives us the ability to easily introduce additional behavior around each request that is going through the pipeline, without actually modifying the original request.‌
CQRS 模式使用命令和查询来传达信息并接收响应。从本质上讲,它表示一个请求-响应管道。这使我们能够轻松地围绕通过管道的每个请求引入其他行为,而无需实际修改原始请求。

You may be familiar with this technique under the name Decorator pattern. Another example of using the Decorator pattern is the ASP.NET Core Middleware concept, which we talked about in section 1.8.
您可能熟悉这种名为 Decorator 模式的技术。使用 Decorator 模式的另一个例子是 ASP.NET Core Middleware 概念,我们在 1.8 节中讨论过。

MediatR has a similar concept to middleware, and it is called IPipelineBehavior:
MediatR 与中间件的概念类似,称为 IPipelineBehavior:

public interface IPipelineBehavior<in TRequest, TResponse> where TRequest : notnull { Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next); }

The pipeline behavior is a wrapper around a request instance and gives us a lot of flexibility with the implementation. Pipeline behaviors are a good fit for cross-cutting concerns in your application. Good examples of cross- cutting concerns are logging, caching, and of course, validation!
管道行为是请求实例的包装器,为我们的实现提供了很大的灵活性。管道行为非常适合应用程序中的横切关注点。横切关注点的好例子是日志记录、缓存,当然还有验证!

Before we use this interface, let’s create a new exception class in the Entities/Exceptions folder:
在使用此接口之前,让我们在 Entities/Exceptions 文件夹中创建一个新的异常类:

public sealed class ValidationAppException : Exception { public IReadOnlyDictionary<string, string[]> Errors { get; } public ValidationAppException(IReadOnlyDictionary<string, string[]> errors) :base("One or more validation errors occurred") => Errors = errors; }

Next, to implement the IPipelineBehavior interface, we are going to create another folder named Behaviors in the Application project, and add a single class inside it:
接下来,为了实现 IPipelineBehavior 接口,我们将在 Application 项目中创建另一个名为 Behaviors 的文件夹,并在其中添加一个类:

public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { private readonly IEnumerable<IValidator<TRequest>> _validators; public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) => _validators = validators; public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { if (!_validators.Any()) return await next(); var context = new ValidationContext<TRequest>(request); var errorsDictionary = _validators .Select(x => x.Validate(context)) .SelectMany(x => x.Errors) .Where(x => x != null) .GroupBy( x => x.PropertyName.Substring(x.PropertyName.IndexOf('.') + 1), x => x.ErrorMessage,(propertyName, errorMessages) => new { Key = propertyName, Values = errorMessages.Distinct().ToArray() }) .ToDictionary(x => x.Key, x => x.Values); if (errorsDictionary.Any()) throw new ValidationAppException(errorsDictionary); return await next(); } }

This class has to inherit from the IPipelineBehavior interface and implement the Handler method. We also inject a collection of IValidator implementations in the constructor. The FluentValidation library will scan our project for all AbstractValidator implementations for a given type and then provide us with the instance at runtime. It is how we can apply the actual validators that we implemented in our project.
此类必须继承自 IPipelineBehavior 接口并实现 Handler 方法。我们还在构造函数中注入了一组 IValidator 实现。FluentValidation 库将扫描我们的项目以查找给定类型的所有 AbstractValidator 实现,然后在运行时为我们提供实例。这就是我们如何应用我们在项目中实现的实际验证器。

Then, if there are no validation errors, we just call the next delegate to allow the execution of the next component in the middleware.
然后,如果没有验证错误,我们只调用 next 委托,以允许在中间件中执行 next 组件。

But if there are any errors, we extract them from the _validators collection and group them inside the dictionary. If there are entries in our dictionary, we throw the ValidationAppException and pass the dictionary with errors. This exception will be caught inside our global error handler, which we will modify in a minute.
但是如果有任何错误,我们会从 _validators 集合中提取它们,并在字典中对它们进行分组。如果字典中有条目,则抛出 ValidationAppException 并传递带有错误的字典。这个异常将在我们的全局错误处理程序中捕获,我们将在一分钟内对其进行修改。

But before we do that, we have to register this behavior in the Program class:
但在此之前,我们必须在 Program 类中注册此行为:

builder.Services.AddMediatR(typeof(Application.AssemblyReference).Assembly); builder.Services.AddAutoMapper(typeof(Program)); builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); builder.Services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly);

After that, we can modify the ExceptionMiddlewareExtensions class:
之后,我们可以修改 ExceptionMiddlewareExtensions 类:

public static class ExceptionMiddlewareExtensions
{ public static void ConfigureExceptionHandler(this WebApplication app, ILoggerManager logger) { app.UseExceptionHandler(appError => { appError.Run(async context => { context.Response.ContentType = "application/json"; var contextFeature = context.Features.Get<IExceptionHandlerFeature>(); if (contextFeature != null) { context.Response.StatusCode = contextFeature.Error switch { NotFoundException => StatusCodes.Status404NotFound, BadRequestException => StatusCodes.Status400BadRequest, ValidationAppException => StatusCodes.Status422UnprocessableEntity, _ => StatusCodes.Status500InternalServerError }; logger.LogError($"Something went wrong: {contextFeature.Error}"); if (contextFeature.Error is ValidationAppException exception) { await context.Response .WriteAsync(JsonSerializer.Serialize(new { exception.Errors })); } else { await context.Response.WriteAsync(new ErrorDetails() { StatusCode = context.Response.StatusCode, Message = contextFeature.Error.Message, }.ToString()); } } }); }); } }

So we modify the switch statement to check for the ValidationAppException type and to assign a proper status code 422.
因此,我们修改 switch 语句以检查 ValidationAppException 类型并分配正确的状态代码 422。

Then, we use the declaration pattern to test the type of the variable and assign it to a new variable named exception. If the type is ValidationAppException we just write our response to the client providing our errors dictionary as a parameter. Otherwise, we do the same thing we did up until now.
然后,我们使用声明模式来测试变量的类型,并将其分配给名为 exception 的新变量。如果类型是 ValidationAppException,我们只将响应写入客户端,提供我们的 errors 字典作为参数。否则,我们将做与现在相同的事情。

Now, we can test this by sending an invalid request:
现在,我们可以通过发送无效请求来测试这一点:
https://localhost:5001/api/companies

alt text

Excellent, this works great.
太好了,这效果很好。

Additionally, if the Address property has too many characters, we will see a different message:
此外,如果 Address 属性的字符太多,我们将看到一条不同的消息:

alt text

Great.
伟大。

33.7.3 Validating null Object‌

33.7.3 验证 null 对象

Now, if we send a request with an empty request body, we are going to get the result produced from our action:
现在,如果我们发送一个请求正文为空的请求,我们将得到我们的作生成的结果:

https://localhost:5001/api/companies

alt text

We can see the 400 status code and the error message. It is perfectly fine since we want to have a Bad Request response if the object sent from the client is null. But if for any reason you want to remove that validation from the action, and handle it with fluent validation rules, you can do that by modifying the CreateCompanyCommandValidator class and overriding the Validate method:
我们可以看到 400 状态代码和错误消息。这完全没问题,因为如果从客户端发送的对象为 null,我们希望得到 Bad Request 响应。但是,如果出于任何原因,您希望从作中删除该验证,并使用 Fluent 验证规则处理它,则可以通过修改 CreateCompanyCommandValidator 类并重写 Validate 方法来执行此作:

public sealed class CreateCompanyCommandValidator : AbstractValidator<CreateCompanyCommand> { public CreateCompanyCommandValidator() { RuleFor(c => c.Company.Name).NotEmpty().MaximumLength(60); RuleFor(c => c.Company.Address).NotEmpty().MaximumLength(60); } public override ValidationResult Validate(ValidationContext<CreateCompanyCommand> context) { return context.InstanceToValidate.Company is null ? new ValidationResult(new[] { new ValidationFailure("CompanyForCreationDto", "CompanyForCreationDto object is null") }) : base.Validate(context); } }

Now, you can remove the validation check inside the action and send a null body request:
现在,您可以删除作中的验证检查并发送 null 正文请求:

alt text

Pay attention that now the status code is 422 and not 400. But this validation is now part of the fluent validation.
请注意,现在状态代码是 422 而不是 400。但此验证现在是 Fluent 验证的一部分。

If this solution fits your project, feel free to use it. Our recommendation is to use 422 only for the validation errors, and 400 if the request body is null.
如果此解决方案适合您的项目,请随意使用。我们建议仅对验证错误使用 422,如果请求正文为 null,则使用 400。

Ultimate ASP.NET Core Web API 32 BONUS 1 – RESPONSE PERFORMANCE IMPROVEMENTS

32 BONUS 1 - RESPONSE PERFORMANCE IMPROVEMENTS
32 奖励 1 - 响应性能改进

As mentioned in section 6.1.1, we will show you an alternative way of handling error responses. To repeat, with custom exceptions, we have great control of returning error responses to the client due to the global error handler, which is pretty fast if we use it correctly. Also, the code is pretty clean and straightforward since we don’t have to care about the return types and additional validation in the service methods.‌
如 6.1.1 节所述,我们将向您展示一种处理错误响应的替代方法。重复一遍,对于自定义异常,由于全局错误处理程序,我们可以很好地控制将错误响应返回给客户端,如果我们正确使用它,这将非常快。此外,代码非常简洁明了,因为我们不必关心服务方法中的返回类型和其他验证。

Even though some libraries enable us to write custom responses, for example, OneOf, we still like to create our abstraction logic, which is tested by us and fast. Additionally, we want to show you the whole creation process for such a flow.
尽管一些库允许我们编写自定义响应,例如 OneOf,但我们仍然喜欢创建我们的抽象逻辑,它由我们测试并且快速。此外,我们还想向您展示此类流程的整个创建过程。

For this example, we will use an existing project from part 6 and modify it to implement our API Response flow.
在此示例中,我们将使用 第 6 部分中的现有项目,并对其进行修改以实现我们的 API 响应流。

32.1 Adding Response Classes to the Project

32.1 向项目添加响应类

Let’s start with the API response model classes.‌
让我们从 API 响应模型类开始。

The first thing we are going to do is create a new Responses folder in the Entities project. Inside that folder, we are going to add our first class:
我们要做的第一件事是在 Entities 项目中创建一个新的 Responses 文件夹。在该文件夹中,我们将添加我们的第一个类:

public abstract class ApiBaseResponse { public bool Success { get; set; } protected ApiBaseResponse(bool success) => Success = success; }

This is an abstract class, which will be the main return type for all of our methods where we have to return a successful result or an error result. It also contains a single Success property stating whether the action was successful or not.
这是一个抽象类,它将是我们所有方法的主要返回类型,我们必须返回成功结果或错误结果。它还包含一个 Success 属性,用于说明作是否成功。

Now, if our result is successful, we are going to create only one class in the same folder:
现在,如果我们的结果成功,我们将只在同一个文件夹中创建一个类:

public sealed class ApiOkResponse<TResult> : ApiBaseResponse { public TResult Result { get; set; } public ApiOkResponse(TResult result) :base(true) { Result = result; } }

We are going to use this class as a return type for a successful result. It inherits from the ApiBaseResponse and populates the Success property to true through the constructor. It also contains a single Result property of type TResult. We will store our concrete result in this property, and since we can have different result types in different methods, this property is a generic one.
我们将使用这个类作为成功结果的返回类型。它继承自 ApiBaseResponse,并通过构造函数将 Success 属性填充为 true。它还包含一个 TResult 类型的 Result 属性。我们将具体结果存储在此属性中,由于我们可以在不同的方法中具有不同的结果类型,因此此属性是通用属性。

That’s all regarding the successful responses. Let’s move one to the error classes.
这就是关于成功响应的全部内容。让我们将一个移动到 error 类。

For the error responses, we will follow the same structure as we have for the exception classes. So, we will have base abstract classes for NotFound or BadRequest or any other error responses, and then concrete implementations for these classes like CompanyNotFound or CompanyBadRequest, etc.
对于错误响应,我们将遵循与异常类相同的结构。因此,我们将为 NotFound 或 BadRequest 或任何其他错误响应提供基本抽象类,然后为这些类提供具体实现,例如 CompanyNotFound 或 CompanyBadRequest 等。

That said, let’s use the same folder to create an abstract error class:
也就是说,让我们使用相同的文件夹来创建一个抽象错误类:

public abstract class ApiNotFoundResponse : ApiBaseResponse { public string Message { get; set; } public ApiNotFoundResponse(string message) : base(false) { Message = message; } }

This class also inherits from the ApiBaseResponse, populates the Success property to false, and has a single Message property for the error message.
此类还继承自 ApiBaseResponse,将 Success 属性填充为 false,并且具有错误消息的单个 Message 属性。

In the same manner, we can create the ApiBadRequestResponse class:
以同样的方式,我们可以创建 ApiBadRequestResponse 类:

public abstract class ApiBadRequestResponse : ApiBaseResponse { public string Message { get; set; } public ApiBadRequestResponse(string message) : base(false) { Message = message; } }

This is the same implementation as the previous one. The important thing to notice is that both of these classes are abstract.
这与上一个实现相同。需要注意的重要一点是,这两个类都是抽象的。

To continue, let’s create a concrete error response:
为了继续,让我们创建一个具体的错误响应:

public sealed class CompanyNotFoundResponse : ApiNotFoundResponse { public CompanyNotFoundResponse(Guid id) : base($"Company with id: {id} is not found in db.") { } }

The class inherits from the ApiNotFoundResponse abstract class, which again inherits from the ApiBaseResponse class. It accepts an id parameter and creates a message that sends to the base class.
该类继承自 ApiNotFoundResponse 抽象类,而 ApiNotFoundResponse 抽象类又继承自 ApiBaseResponse 类。它接受 id 参数并创建发送到基类的消息。

We are not going to create the CompanyBadRequestResponse class because we are not going to need it in our example. But the principle is the same.
我们不打算创建 CompanyBadRequestResponse 类,因为在我们的示例中不需要它。但原理是一样的。

32.2 Service Layer Modification

32.2 服务层修改

Now that we have the response model classes, we can start with the service layer modification.‌
现在我们有了响应模型类,我们可以从服务层修改开始。

Let’s start with the ICompanyService interface:
让我们从 ICompanyService 接口开始:

public interface ICompanyService { ApiBaseResponse GetAllCompanies(bool trackChanges); ApiBaseResponse GetCompany(Guid companyId, bool trackChanges); }

We don’t return concrete types in our methods anymore. Instead of the IEnumerable or CompanyDto return types, we return the ApiBaseResponse type. This will enable us to return either the success result or to return any of the error response results.
我们不再在方法中返回具体类型。 我们返回 ApiBaseResponse 类型,而不是 IEnumerable 或 CompanyDto 返回类型。这将使我们能够返回成功结果或返回任何错误响应结果。

After the interface modification, we can modify the CompanyService class:
修改接口后,我们可以修改 CompanyService 类:

public ApiBaseResponse GetAllCompanies(bool trackChanges) { var companies = _repository.Company.GetAllCompanies(trackChanges); var companiesDto = _mapper.Map<IEnumerable<CompanyDto>>(companies); return new ApiOkResponse<IEnumerable<CompanyDto>>(companiesDto); } public ApiBaseResponse GetCompany(Guid id, bool trackChanges) { var company = _repository.Company.GetCompany(id, trackChanges); if (company is null) return new CompanyNotFoundResponse(id); var companyDto = _mapper.Map<CompanyDto>(company); return new ApiOkResponse<CompanyDto>(companyDto); }

Both method signatures are modified to use APIBaseResponse, and also the return types are modified accordingly. Additionally, in the GetCompany method, we are not using an exception class to return an error result but the CompanyNotFoundResponse class. With the ApiBaseResponse abstraction, we are safe to return multiple types from our method as long as they inherit from the ApiBaseResponse abstract class. Here you could also log some messages with _logger.
两个方法签名都被修改为使用 APIBaseResponse,并且返回类型也相应地被修改。此外,在 GetCompany 方法中,我们没有使用异常类来返回错误结果,而是使用 CompanyNotFoundResponse 类。使用 ApiBaseResponse 抽象,我们可以安全地从方法中返回多个类型,只要它们继承自 ApiBaseResponse 抽象类。在这里,您还可以使用 _logger 记录一些消息。

One more thing to notice here.
这里还有一点需要注意。

In the GetAllCompanies method, we don’t have an error response just a successful one. That means we didn’t have to implement our Api response flow, and we could’ve left the method unchanged (in the interface and this class). If you want that kind of implementation it is perfectly fine. We just like consistency in our projects, and due to that fact, we’ve changed both methods.
在 GetAllCompanies 方法中,我们没有错误响应,只有一个成功的响应。这意味着我们不必实现 Api 响应流,并且可以保持方法不变(在接口和这个类中)。如果你想要这种实现,那完全没问题。我们就像我们项目中的一致性一样,因此,我们改变了这两种方法。

32.3 Controller Modification

32.3 控制器修改

Before we start changing the actions in the CompaniesController, we have to create a way to handle error responses and return them to the client – similar to what we have with the global error handler middleware.‌
在我们开始更改 CompaniesController 中的作之前,我们必须创建一种方法来处理错误响应并将其返回给客户端 – 类似于我们使用全局错误处理程序中间件的方法。

We are not going to create any additional middleware but another controller base class inside the Presentation/Controllers folder:
我们不打算创建任何其他中间件,而是在 Presentation/Controllers 文件夹中创建另一个控制器基类:

public class ApiControllerBase : ControllerBase { public IActionResult ProcessError(ApiBaseResponse baseResponse) { return baseResponse switch { ApiNotFoundResponse => NotFound(new ErrorDetails { Message = ((ApiNotFoundResponse)baseResponse).Message, StatusCode = StatusCodes.Status404NotFound }), ApiBadRequestResponse => BadRequest(new ErrorDetails { Message = ((ApiBadRequestResponse)baseResponse).Message, StatusCode = StatusCodes.Status400BadRequest }), _ => throw new NotImplementedException() }; } }

This class inherits from the ControllerBase class and implements a single ProcessError action accepting an ApiBaseResponse parameter. Inside the action, we are inspecting the type of the sent parameter, and based on that type we return an appropriate message to the client. A similar thing we did in the exception middleware class.
此类继承自 ControllerBase 类,并实现接受 ApiBaseResponse 参数的单个 ProcessError作。在作中,我们将检查已发送参数的类型,并根据该类型向客户端返回适当的消息。我们在异常中间件类中做了类似的事情。

If you add additional error response classes to the Response folder, you only have to add them here to process the response for the client.
如果向 Response 文件夹添加其他错误响应类,则只需在此处添加它们即可处理客户端的响应。

Additionally, this is where we can see the advantage of our abstraction approach.
此外,这就是我们可以看到抽象方法的优势的地方。

Now, we can modify our CompaniesController:
现在,我们可以修改我们的 CompaniesController:

[Route("api/companies")] [ApiController] public class CompaniesController : ApiControllerBase { private readonly IServiceManager _service; public CompaniesController(IServiceManager service) => _service = service; [HttpGet] public IActionResult GetCompanies() { var baseResult = _service.CompanyService.GetAllCompanies(trackChanges: false); var companies = ((ApiOkResponse<IEnumerable<CompanyDto>>)baseResult).Result; return Ok(companies); } [HttpGet("{id:guid}")] public IActionResult GetCompany(Guid id) { var baseResult = _service.CompanyService.GetCompany(id, trackChanges: false); if (!baseResult.Success) return ProcessError(baseResult); var company = ((ApiOkResponse<CompanyDto>)baseResult).Result; return Ok(company); } }

Now our controller inherits from the ApiControllerBase, which inherits from the ControllerBase class. In the GetCompanies action, we extract the result from the service layer and cast the baseResult variable to the concrete ApiOkResponse type, and use the Result property to extract our required result of type IEnumerable.
现在我们的控制器继承自 ApiControllerBase,而 ApiControllerBase 继承自 ControllerBase 类。在 GetCompanies作中,我们从服务层提取结果,并将 baseResult 变量转换为具体的 ApiOkResponse 类型,并使用 Result 属性提取所需的 IEnumerable 类型结果。

We do a similar thing for the GetCompany action. Of course, here we check if our result is successful and if it’s not, we return the result of the ProcessError method.
我们对 GetCompany作执行类似的作。当然,这里我们检查结果是否成功,如果不是,我们返回 ProcessError 方法的结果。

And that’s it.
就是这样。

We can leave the solution as is, but we mind having these castings inside our actions – they can be moved somewhere else making them reusable and our actions cleaner. So, let’s do that.
我们可以保持解决方案不变,但我们介意在我们的 action 中放置这些铸件——它们可以移动到其他地方,使它们可重用,我们的 action 更干净。所以,让我们开始吧。

In the same project, we are going to create a new Extensions folder and a new ApiBaseResponseExtensions class:
在同一个项目中,我们将创建一个新的 Extensions 文件夹和一个新的 ApiBaseResponseExtensions 类:

public static class ApiBaseResponseExtensions { public static TResultType GetResult<TResultType>(this ApiBaseResponse apiBaseResponse) => ((ApiOkResponse<TResultType>)apiBaseResponse).Result; }

The GetResult method will extend the ApiBaseResponse type and return the result of the required type.
GetResult 方法将扩展 ApiBaseResponse 类型并返回所需类型的结果。

Now, we can modify actions inside the controller:
现在,我们可以修改控制器内部的 action:

[HttpGet] public IActionResult GetCompanies() { var baseResult = _service.CompanyService.GetAllCompanies(trackChanges: false); var companies = baseResult.GetResult<IEnumerable<CompanyDto>>(); return Ok(companies); } [HttpGet("{id:guid}")] public IActionResult GetCompany(Guid id) { var baseResult = _service.CompanyService.GetCompany(id, trackChanges: false); if (!baseResult.Success) return ProcessError(baseResult); var company = baseResult.GetResult<CompanyDto>(); return Ok(company); }

This is much cleaner and easier to read and understand.
这更简洁,更容易阅读和理解。

32.4 Testing the API Response Flow

32.4 测试 API 响应流

Now we can start our application, open Postman, and send some requests.‌
现在我们可以启动应用程序,打开 Postman,并发送一些请求。

Let’s try to get all the companies:
让我们尝试获取所有公司:

https://localhost:5001/api/companies

alt text

Then, we can try to get a single company:
然后,我们可以尝试获得一家公司:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

And finally, let’s try to get a company that does not exist:
最后,让我们尝试获得一家不存在的公司:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce2

alt text

And we have our response with a proper status code and response body. Excellent.
我们的响应具有适当的状态代码和响应正文。非常好。

We have a solution that is easy to implement, fast, and extendable.
我们有一个易于实施、快速且可扩展的解决方案。

Our suggestion is to go with custom exceptions since they are easier to implement and fast as well. But if you have an app flow where you have to return error responses at a much higher rate and thus maybe impact the app’s performance, the APi Response flow is the way to go.
我们建议使用自定义异常,因为它们更容易实现且速度更快。但是,如果您的应用程序流必须以更高的速率返回错误响应,从而可能会影响应用程序的性能,那么 APi 响应流就是您的不二之选。

Ultimate ASP.NET Core Web API 31 DEPLOYMENT TO IIS

31 DEPLOYMENT TO IIS
30 部署到 IIS

Before we start the deployment process, we would like to point out one important thing. We should always try to deploy an application on at least a local machine to somehow simulate the production environment as soon as we start with development. That way, we can observe how the application behaves in a production environment from the beginning of the development process.‌
在开始部署过程之前,我们想指出一件重要的事情。一旦开始开发,我们应该始终尝试至少在本地计算机上部署应用程序,以某种方式模拟生产环境。这样,我们可以从开发过程的一开始就观察应用程序在生产环境中的行为。

That leads us to the conclusion that the deployment process should not be the last step of the application’s lifecycle. We should deploy our application to the staging environment as soon as we start building it.
这使我们得出结论,部署过程不应该是应用程序生命周期的最后一步。我们应该在开始构建应用程序后立即将其部署到暂存环境。

That said, let’s start with the deployment process.
也就是说,让我们从部署过程开始。

31.1 Creating Publish Files

31.1 创建 Publish 文件

Let’s create a folder on the local machine with the name Publish. Inside that folder, we want to place all of our files for deployment. After the folder creation, let’s right-click on the main project in the Solution Explorer window and click publish option:‌
让我们在本地计算机上创建一个名为 Publish 的文件夹。在该文件夹中,我们希望放置所有用于部署的文件。创建文件夹后,让我们右键单击 Solution Explorer 窗口中的主项目,然后单击 publish 选项:

alt text

In the “Pick a publish target” window, we are going to choose the Folder option and click Next:
在“Pick a publish target”窗口中,我们将选择 Folder 选项,然后单击 Next:

alt text

And point to the location of the Publish folder we just created and click Finish:
并指向我们刚刚创建的 Publish 文件夹的位置,然后单击 Finish:

alt text
Publish windows can be different depending on the Visual Studio version.
发布窗口可能因 Visual Studio 版本而异。

After that, we have to click the Publish button:
之后,我们必须单击 Publish 按钮:

alt text

Visual Studio is going to do its job and publish the required files in the specified folder.
Visual Studio 将执行其工作并在指定文件夹中发布所需的文件。

31.2 Windows Server Hosting Bundle

31.2 Windows Server 托管捆绑包

Before any further action, let’s install the .NET Core Windows Server Hosting bundle on our system to install .NET Core Runtime. Furthermore, with this bundle, we are installing the .NET Core Library and the ASP.NET Core Module. This installation will create a reverse proxy between IIS and the Kestrel server, which is crucial for the deployment process.‌
在执行任何进一步作之前,让我们在系统上安装 .NET Core Windows Server 托管捆绑包以安装 .NET Core 运行时。此外,通过此捆绑包,我们将安装 .NET Core 库和 ASP.NET Core Module。此安装将在 IIS 和 Kestrel 服务器之间创建反向代理,这对于部署过程至关重要。

If you have a problem with missing SDK after installing the Hosting Bundle, follow this solution suggested by Microsoft:
如果您在安装 Hosting Bundle 后遇到缺少 SDK 的问题,请遵循 Microsoft 建议的以下解决方案:

Installing the .NET Core Hosting Bundle modifies the PATH when it installs the .NET Core runtime to point to the 32-bit (x86) version of .NET Core (C:\Program Files (x86)\dotnet). This can result in missing SDKs when the 32-bit (x86) .NET Core dotnet command is used (No .NET Core SDKs were detected). To resolve this problem, move C:\Program Files\dotnet\to a position before C:\Program Files (x86)\dotnet\ on the PATH environment variable.
安装 .NET Core 托管捆绑包会在安装 .NET Core 运行时时修改 PATH,使其指向 .NET Core 的 32 位 (x86) 版本 (C:\Program Files (x86)\dotnet)。这可能会导致在使用 32 位 (x86) .NET Core dotnet 命令时缺少 SDK(未检测到 .NET Core SDK)。若要解决此问题,请将 C:\Program Files\dotnet\ 移动到 PATH 环境变量上 C:\Program Files (x86)\dotnet\ 之前的位置。

After the installation, we are going to locate the Windows hosts file on C:\Windows\System32\drivers\etc and add the following record at the end of the file:
安装完成后,我们将在 C:\Windows\System32\drivers\etc 上找到 Windows 主机文件,并在文件末尾添加以下记录:

127.0.0.1 www.companyemployees.codemaze

After that, we are going to save the file.
之后,我们将保存文件。

31.3 Installing IIS
31.3 安装 IIS

If you don’t have IIS installed on your machine, you need to install it by opening ControlPanel and then Programs and Features:‌
如果您的计算机上没有安装 IIS,则需要通过打开 ControlPanel,然后打开“程序和功能”来安装它:

alt text

After the IIS installation finishes, let’s open the Run window (windows key + R) and type: inetmgr to open the IIS manager:
IIS 安装完成后,让我们打开 Run 窗口(Windows 键 + R)并键入:inetmgr 以打开 IIS 管理器:

alt text

Now, we can create a new website:
现在,我们可以创建一个新网站:

alt text

In the next window, we need to add a name to our site and a path to the published files:
在下一个窗口中,我们需要为我们的网站添加一个名称和一个已发布文件的路径:

alt text

And click the OK button.
然后点击 OK 按钮。

After this step, we are going to have our site inside the “sites” folder in the IIS Manager. Additionally, we need to set up some basic settings for our application pool:
完成此步骤后,我们将站点置于 IIS Manager 的 “sites” 文件夹中。此外,我们需要为应用程序池设置一些基本设置:

alt text

After we click on the Basic Settings link, let’s configure our application pool:
单击 Basic Settings 链接后,让我们配置应用程序池:

alt text

ASP.NET Core runs in a separate process and manages the runtime. It doesn't rely on loading the desktop CLR (.NET CLR). The Core Common Language Runtime for .NET Core is booted to host the app in the worker process. Setting the .NET CLR version to No Managed Code is optional but recommended.
ASP.NET Core 在单独的进程中运行并管理运行时。它不依赖于加载桌面 CLR (.NET CLR)。.NET Core 的核心公共语言运行时启动,以在工作进程中托管应用。将 .NET CLR 版本设置为“无托管代码”是可选的,但建议使用。

Our website and the application pool should be started automatically.
我们的网站和应用程序池应该会自动启动。

31.4 Configuring Environment File
31.4 配置环境文件

In the section where we configured JWT, we had to use a secret key that we placed in the environment file. Now, we have to provide to IIS the name of that key and the value as well.‌
在配置 JWT 的部分中,我们必须使用放置在环境文件中的密钥。现在,我们必须向 IIS 提供该键的名称和值。

The first step is to click on our site in IIS and open Configuration Editor:
第一步是在 IIS 中单击我们的站点并打开配置编辑器:

alt text

Then, in the section box, we are going to choose system.webServer/aspNetcore:
然后,在部分框中,我们将选择 system.webServer/aspNetcore:

alt text

From the “From” combo box, we are going to choose ApplicationHost.config:
从 “From” 组合框中,我们将选择 ApplicationHost.config:

alt text

After that, we are going to select environment variables:
之后,我们将选择环境变量:

alt text

Click Add and type the name and the value of our variable:
单击 Add 并键入变量的名称和值:

alt text

As soon as we click the close button, we should click apply in the next window, restart our application in IIS, and we are good to go.
单击关闭按钮后,我们应该在下一个窗口中单击应用,在 IIS 中重新启动我们的应用程序,然后就可以开始了。

31.5 Testing Deployed Application

31.5 测试已部署的应用程序

Let’s open Postman and send a request for the Root document:‌
让我们打开 Postman 并发送根文档的请求:
http://www.companyemployees.codemaze/api

alt text

We can see that our API is working as expected. If it’s not, and you have a problem related to web.config in IIS, try reinstalling the Server Hosting Bundle package.
我们可以看到我们的 API 正在按预期工作。如果不是,并且您在 IIS 中存在与 web.config 相关的问题,请尝试重新安装 Server Hosting Bundle 包。

If you get an error message that the Presentation.xml file is missing, you can copy it from the project and paste it into the Publish folder. Also, in the Properties window for that file, you can set it to always copy during the publish.
如果收到一条错误消息,指出 Presentation.xml 文件缺失,则可以从项目中复制该文件并将其粘贴到 Publish 文件夹中。此外,在该文件的 Properties (属性) 窗口中,您可以将其设置为在发布期间始终复制。

Now, let’s continue.
现在,让我们继续。

We still have one more thing to do. We have to add a login to the SQL Server for IIS APPPOOL\CodeMaze Web Api and grant permissions to the database. So, let’s open the SQL Server Management Studio and add a new login:
我们还有一件事要做。我们必须向 SQL Server for IIS APPPOOL\CodeMaze Web Api 添加登录名,并授予对数据库的权限。因此,让我们打开 SQL Server Management Studio 并添加新的登录名:

alt text

In the next window, we are going to add our user:
在下一个窗口中,我们将添加我们的用户:

alt text

After that, we are going to expand the Logins folder, right-click on our user, and choose Properties. There, under UserMappings, we have to select the CompanyEmployee database and grant the dbwriter and dbreader roles.
之后,我们将展开 Logins 文件夹,右键单击我们的用户,然后选择 Properties。在那里,在 UserMappings 下,我们必须选择 CompanyEmployee 数据库并授予 dbwriter 和 dbreader 角色。

Now, we can try to send the Authentication request:
现在,我们可以尝试发送 Authentication 请求:

http://www.companyemployees.codemaze/api/authentication/login

alt text

Excellent; we have our token. Now, we can send the request to the GetCompanies action with the generated token:
非常好;我们有我们的token。现在,我们可以使用生成的令牌将请求发送到 GetCompanies作:

http://www.companyemployees.codemaze/api/companies

alt text

And there we go. Our API is published and working as expected.
好了。我们的 API 已发布并按预期工作。

Ultimate ASP.NET Core Web API 30 DOCUMENTING API WITH SWAGGER

30 DOCUMENTING API WITH SWAGGER
30 使用 SWAGGER 编写 API 文档

Developers who consume our API might be trying to solve important business problems with it. Hence, it is very important for them to understand how to use our API effectively. This is where API documentation comes into the picture.‌
使用我们的 API 的开发人员可能正在尝试使用它来解决重要的业务问题。因此,他们了解如何有效地使用我们的 API 非常重要。这就是 API 文档的用武之地。

API documentation is the process of giving instructions on how to effectively use and integrate an API. Hence, it can be thought of as a concise reference manual containing all the information required to work with the API, with details about functions, classes, return types, arguments, and more, supported by tutorials and examples.
API 文档是提供有关如何有效使用和集成 API 的说明的过程。因此,它可以被认为是一本简明的参考手册,其中包含使用 API 所需的所有信息,以及有关函数、类、返回类型、参数等的详细信息,并附有教程和示例。

So, having the proper documentation for our API enables consumers to integrate our APIs as quickly as possible and move forward with their development. Furthermore, this also helps them understand the value and usage of our API, improves the chances for our API’s adoption, and makes our APIs easier to maintain and support.
因此,为我们的 API 提供适当的文档使消费者能够尽快集成我们的 API 并继续进行开发。此外,这还可以帮助他们了解我们 API 的价值和用法,提高我们 API 被采用的机会,并使我们的 API 更易于维护和支持。

30.1 About Swagger

30.1 关于 Swagger

Swagger is a language-agnostic specification for describing REST APIs. Swagger is also referred to as OpenAPI. It allows us to understand the capabilities of a service without looking at the actual implementation code.‌
Swagger 是用于描述 REST API 的与语言无关的规范。Swagger 也称为 OpenAPI。它允许我们了解服务的功能,而无需查看实际的实现代码。

Swagger minimizes the amount of work needed while integrating an API. Similarly, it also helps API developers document their APIs quickly and accurately.
Swagger 最大限度地减少了集成 API 时所需的工作量。同样,它还可以帮助 API 开发人员快速准确地记录他们的 API。

Swagger Specification is an important part of the Swagger flow. By default, a document named swagger.json is generated by the Swagger tool which is based on our API. It describes the capabilities of our API and how to access it via HTTP.
Swagger 规范是 Swagger 流程的重要组成部分。默认情况下,名为 swagger.json 的文档由基于我们的 API 的 Swagger 工具生成。它描述了我们的 API 的功能以及如何通过 HTTP 访问它。

30.2 Swagger Integration Into Our Project

30.2 Swagger 集成到我们的项目中

We can use the Swashbuckle package to easily integrate Swagger into our‌ .NET Core Web API project. It will generate the Swagger specification for the project as well. Additionally, the Swagger UI is also contained within Swashbuckle.
我们可以使用 Swashbuckle 包轻松地将 Swagger 集成到我们的 .NET Core Web API 项目中。它还将为项目生成 Swagger 规范。此外,Swagger UI 也包含在 Swashbuckle 中。

There are three main components in the Swashbuckle package:
Swashbuckle 包中有三个主要组件:

• Swashbuckle.AspNetCore.Swagger: This contains the Swagger object model and the middleware to expose SwaggerDocument objects as JSON.
Swashbuckle.AspNetCore.Swagger:这包含 Swagger 对象模型和用于将 SwaggerDocument 对象公开为 JSON 的中间件。

• Swashbuckle.AspNetCore.SwaggerGen: A Swagger generator that builds SwaggerDocument objects directly from our routes, controllers, and models.
Swashbuckle.AspNetCore.SwaggerGen:一个 Swagger 生成器,可直接从我们的路由、控制器和模型构建 SwaggerDocument 对象。

• Swashbuckle.AspNetCore.SwaggerUI: An embedded version of the Swagger UI tool. It interprets Swagger JSON to build a rich, customizable experience for describing web API functionality.
Swashbuckle.AspNetCore.SwaggerUI:Swagger UI 工具的嵌入式版本。它解释 Swagger JSON 以构建丰富的可自定义体验,用于描述 Web API 功能。

So, the first thing we are going to do is to install the required library in the main project. Let’s open the Package Manager Console window and type the following command:
所以,我们要做的第一件事是在主项目中安装所需的库。让我们打开 Package Manager Console 窗口并键入以下命令:

PM> Install-Package Swashbuckle.AspNetCore

After a couple of seconds, the package will be installed. Now, we have to configure the Swagger Middleware. To do that, we are going to add a new method in the ServiceExtensions class:
几秒钟后,将安装该软件包。现在,我们必须配置 Swagger 中间件。为此,我们将在 ServiceExtensions 类中添加一个新方法:

public static void ConfigureSwagger(this IServiceCollection services) { services.AddSwaggerGen(s => { s.SwaggerDoc("v1", new OpenApiInfo { Title = "Code Maze API", Version = "v1" }); s.SwaggerDoc("v2", new OpenApiInfo { Title = "Code Maze API", Version = "v2" }); }); }

We are creating two versions of SwaggerDoc because if you remember, we have two versions for the Companies controller and we want to separate them in our documentation.
我们正在创建两个版本的 SwaggerDoc,因为如果您还记得,我们有两个版本用于 Companies 控制器,我们希望在我们的文档中将它们分开。

Also, we need an additional namespace:
此外,我们还需要一个额外的命名空间:

using Microsoft.OpenApi.Models;

The next step is to call this method in the Program class:
下一步是在 Program 类中调用此方法:

builder.Services.ConfigureSwagger();

And in the middleware part of the class, we are going to add it to the application’s execution pipeline together with the UI feature:
在类的中间件部分,我们将它与 UI 功能一起添加到应用程序的执行管道中:

app.UseSwagger(); app.UseSwaggerUI(s => { s.SwaggerEndpoint("/swagger/v1/swagger.json", "Code Maze API v1"); s.SwaggerEndpoint("/swagger/v2/swagger.json", "Code Maze API v2"); });

Finally, let’s slightly modify the Companies and CompaniesV2 controllers:
最后,让我们稍微修改一下 Companies 和 CompaniesV2 控制器:

[Route("api/companies")] [ApiController] [ApiExplorerSettings(GroupName = "v1")] public class CompaniesController : ControllerBase [Route("api/companies")] [ApiController] [ApiExplorerSettings(GroupName = "v2")] public class CompaniesV2Controller : ControllerBase

With this change, we state that the CompaniesController belongs to group v1 and the CompaniesV2Controller belongs to group v2. All the other controllers will be included in both groups because they are not versioned. Which is what we want.
通过此更改,我们声明 CompaniesController 属于组 v1,而 CompaniesV2Controller 属于组 v2。所有其他控制器都将包含在这两个组中,因为它们未进行版本控制。这就是我们想要的。

And that is all. We have prepared the basic configuration.
就这样。我们已经准备好了基本配置。

Now, we can start our app, open the browser, and navigate to https://localhost:5001/swagger/v1/swagger.json. Once the page is up, you are going to see a json document containing all the controllers and actions without the v2 companies controller. Of course, if you change v1 to v2 in the URL, you are going to see all the controllers — including v2 companies, but without v1 companies.
现在,我们可以启动我们的应用程序,打开浏览器,然后导航到 https://localhost:5001/swagger/v1/swagger.json。页面打开后,您将看到一个 json 文档,其中包含所有控制器和作,但没有 v2 companies 控制器。当然,如果您在 URL 中将 v1 更改为 v2,您将看到所有控制器 — 包括 v2 公司,但没有 v1 公司。

Additionally, let’s navigate to
此外,让我们导航到
https://localhost:5001/swagger/index.html:

alt text

Also if we expand the Schemas part, we are going to find the DTOs that we used in our project.
此外,如果我们扩展 Schemas 部分,我们将找到我们在项目中使用的 DTO。

If we click on a specific controller to expand its details, we are going to see all the actions inside:
如果我们单击特定控制器以展开其详细信息,我们将看到其中的所有作:

alt text

Once we click on an action method, we can see detailed information like parameters, response, and example values. There is also an option to try out each of those action methods by clicking the Try it out button.
单击作方法后,我们可以看到参数、响应和示例值等详细信息。还有一个选项,可以通过单击 Try it out (试用) 按钮来尝试这些作方法中的每一种。

So, let’s try it with the /api/companies action:
那么,让我们用 /api/companies作来试试:

alt text

Once we click the Execute button, we are going to see that we get our response:
单击 Execute 按钮后,我们将看到我们收到了响应:

alt text

And this is an expected response. We are not authorized. To enable authorization, we have to add some modifications.
这是意料之中的回应。我们没有获得授权。要启用授权,我们必须添加一些修改。

30.3 Adding Authorization Support

30.3 添加授权支持

To add authorization support, we need to modify the ConfigureSwagger‌ method:
要添加授权支持,我们需要修改 ConfigureSwagger 方法:

public static void ConfigureSwagger(this IServiceCollection services) { services.AddSwaggerGen(s => { s.SwaggerDoc("v1", new OpenApiInfo { Title = "Code Maze API", Version = "v1" }); s.SwaggerDoc("v2", new OpenApiInfo { Title = "Code Maze API", Version = "v2" }); s.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Place to add JWT with Bearer", Name = "Authorization", Type = SecuritySchemeType.ApiKey, Scheme = "Bearer" }); s.AddSecurityRequirement(new OpenApiSecurityRequirement() { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer"}, Name = "Bearer", }, new List<string>() } }); }); }

With this modification, we are adding the security definition in our swagger configuration. Now, we can start our app again and navigate to the index.html page.
通过此修改,我们将在 swagger 配置中添加安全定义。现在,我们可以再次启动我们的应用程序并导航到 index.html 页面。

The first thing we are going to notice is the Authorize options for requests:
我们首先要注意的是请求的 Authorize 选项:

alt text

We are going to use that in a moment. But let’s get our token first. For that, let’s open the api/authentication/login action, click try it out, add credentials, and copy the received token:
我们稍后会用到它。但是,让我们先获取我们的令牌。为此,让我们打开 api/authentication/login作,单击 try it out,添加凭据,然后复制收到的令牌:

alt text

Once we have copied the token, we are going to click on the authorization button for the /api/companies request, paste it with the Bearer in front of it, and click Authorize:
复制令牌后,我们将单击 /api/companies 请求的授权按钮,将其粘贴到其前面并带有 Bearer,然后单击 Authorize:

alt text

After authorization, we are going to click on the Close button and try our request:
授权后,我们将单击 Close 按钮并尝试我们的请求:

alt text

And we get our response. Excellent job.
我们得到了我们的回应。干得好。

 30.4 Extending Swagger Configuration

30.4 扩展 Swagger 配置

Swagger provides options for extending the documentation and customizing the UI. Let’s explore some of those.‌
Swagger 提供了用于扩展文档和自定义 UI 的选项。让我们来探讨其中的一些。

First, let’s see how we can specify the API info and description. The configuration action passed to the AddSwaggerGen() method adds information such as Contact, License, and Description. Let’s provide some values for those:
首先,让我们看看如何指定 API 信息和描述。传递给 AddSwaggerGen() 方法的配置作将添加 Contact、License 和 Description 等信息。让我们为这些值提供一些值:

s.SwaggerDoc("v1", new OpenApiInfo { Title = "Code Maze API", Version = "v1", Description = "CompanyEmployees API by CodeMaze", TermsOfService = new Uri("https://example.com/terms"), Contact = new OpenApiContact { Name = "John Doe", Email = "John.Doe@gmail.com", Url = new Uri("https://twitter.com/johndoe"), }, License = new OpenApiLicense { Name = "CompanyEmployees API LICX", Url = new Uri("https://example.com/license"), } });
......

We have implemented this just for the first version, but you get the point. Now, let’s run the application once again and explore the Swagger UI:
我们只为第一个版本实现了这一点,但您明白了。现在,让我们再次运行应用程序并探索 Swagger UI:

alt text

For enabling XML comments, we need to suppress warning 1591, which will now give warnings about any method, class, or field that doesn’t have triple-slash comments. We need to do this in the Presentation project.
要启用 XML 注释,我们需要禁止显示警告 1591,它现在将针对任何没有三斜杠注释的方法、类或字段发出警告。我们需要在 Presentation 项目中执行此作。

Additionally, we have to add the documentation path for the same project, since our controllers are in the Presentation project:
此外,我们必须为同一项目添加文档路径,因为我们的控制器位于 Presentation 项目中:

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <DocumentationFile>CompanyEmployees.Presentation.xml</DocumentationFile> <OutputPath></OutputPath> <NoWarn>1701;1702;1591</NoWarn> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <NoWarn>1701;1702;1591</NoWarn> </PropertyGroup>

Now, let’s modify our configuration:
现在,让我们修改我们的配置:

s.SwaggerDoc("v2", new OpenApiInfo { Title = "Code Maze API", Version = "v2" }); var xmlFile = $"{typeof(Presentation.AssemblyReference).Assembly.GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); s.IncludeXmlComments(xmlPath);

Next, adding triple-slash comments to the action method enhances the Swagger UI by adding a description to the section header:
接下来,通过向部分标题添加说明,向作方法添加三斜杠注释可增强 Swagger UI:

/// <summary> /// Gets the list of all companies /// </summary> /// <returns>The companies list</returns> [HttpGet(Name = "GetCompanies")] [Authorize(Roles = "Manager")] public async Task<IActionResult> GetCompanies()

And this is the result:
结果如下:

alt text

The developers who consume our APIs are usually more interested in what it returns — specifically the response types and error codes. Hence, it is very important to describe our response types. These are denoted using XML comments and data annotations.
使用我们的 API 的开发人员通常对它返回的内容更感兴趣 — 特别是响应类型和错误代码。因此,描述我们的响应类型非常重要。这些使用 XML 注释和数据注释表示。

Let’s enhance the response types a little bit:
让我们稍微增强一下响应类型:

/// <summary> /// Creates a newly created company /// </summary> /// <param name="company"></param> /// <returns>A newly created company</returns> /// <response code="201">Returns the newly created item</response> /// <response code="400">If the item is null</response> /// <response code="422">If the model is invalid</response> [HttpPost(Name = "CreateCompany")] [ProducesResponseType(201)] [ProducesResponseType(400)] [ProducesResponseType(422)]

Here, we are using both XML comments and data annotation attributes. Now, we can see the result:
在这里,我们同时使用了 XML 注释和数据注释属性。现在,我们可以看到结果:

alt text

And, if we inspect the response part, we will find our mentioned responses:
而且,如果我们检查响应部分,我们会找到我们提到的响应:

alt text

Excellent.We can continue to the deployment part.
非常好。我们可以继续进行部署部分。

Ultimate ASP.NET Core Web API 29 BINDING CONFIGURATION AND OPTIONS PATTERN

29 BINDING CONFIGURATION AND OPTIONS PATTERN
29 绑定配置和选项模式

In the previous chapter, we had to use our appsettings file to store some important values for our JWT configuration and read those values from it:‌
在上一章中,我们必须使用 appsettings 文件来存储 JWT 配置的一些重要值,并从中读取这些值:

"JwtSettings": { "validIssuer": "CodeMazeAPI", "validAudience": "https://localhost:5001", "expires": 5 },

To access these values, we’ve used the GetSection method from the IConfiguration interface:
为了访问这些值,我们使用了 IConfiguration 接口中的 GetSection 方法:

var jwtSettings = configuration.GetSection("JwtSettings");

The GetSection method gets a sub-section from the appsettings file based on the provided key.
GetSection 方法根据提供的键从 appsettings 文件中获取子部分。

Once we extracted the sub-section, we’ve accessed the specific values by using the jwtSettings variable of type IConfigurationSection, with the key provided inside the square brackets:
提取子部分后,我们使用 IConfigurationSection 类型的 jwtSettings 变量访问了特定值,其中键在方括号内提供:

ValidIssuer = jwtSettings["validIssuer"],

This works great but it does have its flaws.
这效果很好,但它也有其缺陷。

Having to type sections and keys to get the values can be repetitive and error-prone. We risk introducing errors to our code, and these kinds of errors can cost us a lot of time until we discover them since someone else can introduce them, and we won’t notice them since a null result is returned when values are missing.
必须键入 sections 和 keys 才能获取值可能是重复且容易出错的。我们冒着将错误引入代码的风险,这些类型的错误可能会花费我们大量时间,直到我们发现它们,因为其他人可能会引入它们,而且我们不会注意到它们,因为当值缺失时会返回 null 结果。

To overcome this problem, we can bind the configuration data to strongly typed objects. To do that, we can use the Bind method.
为了解决这个问题,我们可以将配置数据绑定到强类型对象。为此,我们可以使用 Bind 方法。

 29.1 Binding Configuration

29.1 绑定配置

To start with the binding process, we are going to create a new ConfigurationModels folder inside the Entities project, and a new JwtConfiguration class inside that folder:‌
要开始绑定过程,我们将在 Entities 项目中创建一个新的 ConfigurationModels 文件夹,并在该文件夹中创建一个新的 JwtConfiguration 类:

public class JwtConfiguration { public string Section { get; set; } = "JwtSettings"; public string? ValidIssuer { get; set; } public string? ValidAudience { get; set; } public string? Expires { get; set; } }

Then in the ServiceExtensions class, we are going to modify the ConfigureJWT method:
然后在 ServiceExtensions 类中,我们将修改 ConfigureJWT 方法:

public static void ConfigureJWT(this IServiceCollection services, IConfiguration configuration) { var jwtConfiguration = new JwtConfiguration(); configuration.Bind(jwtConfiguration.Section, jwtConfiguration); var secretKey = Environment.GetEnvironmentVariable("SECRET"); services.AddAuthentication(opt => { opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtConfiguration.ValidIssuer, ValidAudience = jwtConfiguration.ValidAudience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)) }; }); }

We create a new instance of the JwtConfiguration class and use the Bind method that accepts the section name and the instance object as parameters, to bind to the JwtSettings section directly and map configuration values to respective properties inside the JwtConfiguration class. Then, we just use those properties instead of string keys inside square brackets, to access required values.
我们创建 JwtConfiguration 类的新实例,并使用接受节名称和实例对象作为参数的 Bind 方法,直接绑定到 JwtSettings 节,并将配置值映射到 JwtConfiguration 类中的相应属性。然后,我们只使用这些属性而不是方括号内的字符串键来访问所需的值。

There are two things to note here though. The first is that the names of the configuration data keys and class properties must match. The other is that if you extend the configuration, you need to extend the class as well, which can be a bit cumbersome, but it beats getting values by typing strings.
不过,这里有两件事需要注意。首先是配置数据键和类属性的名称必须匹配。另一个是,如果你扩展配置,你也需要扩展 class,这可能有点麻烦,但它比通过键入字符串来获取值要好。

Now, we can continue with the AuthenticationService class modification since we extract configuration values in two methods from this class:
现在,我们可以继续修改 AuthenticationService 类,因为我们从这个类中提取了两个方法的配置值:

... private readonly JwtConfiguration _jwtConfiguration; private User? _user; public AuthenticationService(ILoggerManager logger, IMapper mapper, UserManager<User> userManager, IConfiguration configuration) { _logger = logger; _mapper = mapper; _userManager = userManager; _configuration = configuration; _jwtConfiguration = new JwtConfiguration(); _configuration.Bind(_jwtConfiguration.Section, _jwtConfiguration); }

So, we add a readonly variable, and create an instance and execute binding inside the constructor.
因此,我们添加一个 readonly 变量,并在构造函数中创建一个实例并执行绑定。

And since we’re using the Bind() method we need to install the Microsoft.Extensions.Configuration.Binder NuGet package.
由于我们使用的是 Bind() 方法,因此需要安装 Microsoft.Extensions.Configuration.Binder NuGet 包。

After that, we can modify the GetPrincipalFromExpiredToken method by removing the GetSection part and modifying the TokenValidationParameters object creation:
之后,我们可以通过删除 GetSection 部分并修改 TokenValidationParameters 对象创建来修改 GetPrincipalFromExpiredToken 方法:

private ClaimsPrincipal GetPrincipalFromExpiredToken(string token) { var tokenValidationParameters = new TokenValidationParameters { ValidateAudience = true, ValidateIssuer = true,ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SECRET"))), ValidateLifetime = true, ValidIssuer = _jwtConfiguration.ValidIssuer, ValidAudience = _jwtConfiguration.ValidAudience }; ... return principal; }

And let’s do a similar thing for the GenerateTokenOptions method:
让我们对 GenerateTokenOptions 方法执行类似的作:

private JwtSecurityToken GenerateTokenOptions(SigningCredentials signingCredentials, List<Claim> claims) { var tokenOptions = new JwtSecurityToken ( issuer: _jwtConfiguration.ValidIssuer, audience: _jwtConfiguration.ValidAudience, claims: claims, expires: DateTime.Now.AddMinutes(Convert.ToDouble(_jwtConfiguration.Expires)), signingCredentials: signingCredentials ); return tokenOptions; }

Excellent.At this point, we can start our application and use both requests from Postman’s collection - 28-Refresh Token - to test our configuration.
非常好。此时,我们可以启动应用程序并使用来自 Postman 集合的两个请求 - 28-Refresh Token - 来测试我们的配置。

We should get the same responses as we did in a previous chapter, which proves that our configuration works as intended but now with a better code and less error-prone.
我们应该得到与上一章相同的响应,这证明我们的配置按预期工作,但现在代码更好,更不容易出错。

29.2 Options Pattern

29.2 选项模式

In the previous section, we’ve seen how we can bind configuration data to strongly typed objects. The options pattern gives us similar possibilities, but it offers a more structured approach and more features like validation, live reloading, and easier testing.‌
在上一节中,我们已经看到了如何将配置数据绑定到强类型对象。options 模式为我们提供了类似的可能性,但它提供了一种更结构化的方法和更多功能,如验证、实时重新加载和更轻松的测试。

Once we configure the class containing our configuration we can inject it via dependency injection with IOptions and thus injecting only part of our configuration or rather only the part that we need.
一旦我们配置了包含我们的配置的类,我们就可以通过使用 IOptions 的依赖注入来注入它,从而只注入我们配置的一部分,或者更确切地说,只注入我们需要的部分。

If we need to reload the configuration without stopping the application, we can use the IOptionsSnapshot<T> interface or the IOptionsMonitor<T> interface depending on the situation. We’ll see when these interfaces should be used and why.
如果我们需要在不停止应用程序的情况下重新加载配置,我们可以根据情况使用 IOptionsSnapshot<T> 接口或 IOptionsMonitor<T>接口。我们将了解何时应该使用这些接口以及为什么。

The options pattern also provides a good validation mechanism that uses the widely used DataAnotations attributes to check if the configuration abides by the logical rules of our application.
选项模式还提供了一种很好的验证机制,该机制使用广泛使用的 DataAnotations 属性来检查配置是否符合应用程序的逻辑规则。

The testing of options is also easy because of the helper methods and easy to mock options classes.
由于有辅助方法和易于模拟的选项类,选项的测试也很容易。

 29.2.1 Using IOptions‌

29.2.1 使用 IOptions

We have already written a lot of code in the previous section that can be used with the IOptions interface, but we still have some more actions to do.
在上一节中,我们已经编写了大量可与 IOptions 接口一起使用的代码,但我们仍有一些作要执行。

The first thing we are going to do is to register and configure the JwtConfiguration class in the ServiceExtensions class:
我们要做的第一件事是在 ServiceExtensions 类中注册和配置 JwtConfiguration 类:

public static void AddJwtConfiguration(this IServiceCollection services, IConfiguration configuration) => services.Configure<JwtConfiguration>(configuration.GetSection("JwtSettings"));

And call this method in the Program class:
并在 Program 类中调用此方法:

builder.Services.ConfigureJWT(builder.Configuration); builder.Services.AddJwtConfiguration(builder.Configuration);

Since we can use IOptions with DI, we are going to modify the ServiceManager class to support that:
由于我们可以将 IOptions 与 DI 一起使用,因此我们将修改 ServiceManager 类以支持这一点:

public ServiceManager(IRepositoryManager repositoryManager, ILoggerManager logger, IMapper mapper, IEmployeeLinks employeeLinks, UserManager<User> userManager, IOptions<JwtConfiguration> configuration)

We just replace the IConfiguration type with the IOptions type in the constructor.
我们只是在构造函数中将 IConfiguration 类型替换为 IOptions 类型。

For this, we need two additional namespaces:
为此,我们需要两个额外的命名空间:

using Entities.ConfigurationModels; 
using Microsoft.Extensions.Options;

Then, we can modify the AuthenticationService’s constructor:
然后,我们可以修改 AuthenticationService 的构造函数:

private readonly ILoggerManager _logger; private readonly IMapper _mapper; private readonly UserManager<User> _userManager; private readonly IOptions<JwtConfiguration> _configuration; private readonly JwtConfiguration _jwtConfiguration; private User? _user; public AuthenticationService(ILoggerManager logger, IMapper mapper, UserManager<User> userManager, IOptions<JwtConfiguration> configuration) { _logger = logger; _mapper = mapper; _userManager = userManager; _configuration = configuration; _jwtConfiguration = _configuration.Value; }

And that’s it.
就是这样。

We inject IOptions inside the constructor and use the Value property to extract the JwtConfiguration object with all the populated properties. Nothing else has to change in this class.
我们在构造函数中注入 IOptions,并使用 Value 属性提取包含所有填充属性的 JwtConfiguration 对象。这个类中没有其他任何东西需要改变。

If we start the application again and send the same requests, we will still get valid results meaning that we’ve successfully implemented IOptions in our project.
如果我们再次启动应用程序并发送相同的请求,我们仍然会得到有效的结果,这意味着我们已经在项目中成功实现了 IOptions。

One more thing. We didn’t modify anything inside the ServiceExtensions/ConfigureJWT method. That’s because this configuration happens during the service registration and not after services are built. This means that we can’t resolve our required service here.
还有一件事。我们没有修改 ServiceExtensions/ConfigureJWT 方法中的任何内容。这是因为此配置发生在服务注册期间,而不是在构建服务之后。这意味着我们无法在此处解决所需的服务。

Well, to be precise, we can use the BuildServiceProvider method to build a service provider containing all the services from the provided IServiceCollection, and thus being able to access the required service. But if you do that, you will create one more list of singleton services, which can be quite expensive depending on the size of your application. So, you should be careful with this method.
嗯,准确地说,我们可以使用 BuildServiceProvider 方法构建一个服务提供程序,其中包含所提供的 IServiceCollection 中的所有服务,从而能够访问所需的服务。但是,如果您这样做,您将再创建一个单例服务列表,根据应用程序的大小,这可能会非常昂贵。因此,您应该小心使用此方法。

That said, using Binding to access configuration values is perfectly safe and cheap in this stage of the application’s lifetime.
也就是说,在应用程序生命周期的这个阶段,使用 Binding 来访问配置值是完全安全且成本低廉的。

29.2.2 IOptionsSnapshot and IOptionsMonitor‌

The previous code looks great but if we want to change the value of Expires to 10 instead of 5 for example, we need to restart the application to do it. You can imagine how useful would be to have a published application and all you need to do is to modify the value in the configuration file without restarting the whole app.
前面的代码看起来很棒,但是如果我们想将 Expires 的值更改为 10 而不是 5,则需要重新启动应用程序才能执行此作。您可以想象拥有一个已发布的应用程序会有多有用,您需要做的就是修改配置文件中的值,而无需重新启动整个应用程序。

Well, there is a way to do it by using IOptionsSnapshot or IOptionsMonitor.
嗯,有一种方法可以通过使用 IOptionsSnapshot 或 IOptionsMonitor 来实现。

All we would have to do is to replace the IOptions<JwtConfiguration> type with the IOptionsSnapshot<JwtConfiguration> or IOptionsMonitor<JwtConfiguration> types inside the ServiceManager and AuthenticationService classes. Also if we use IOptionsMonitor, we can’t use the Value property but the CurrentValue.
我们所要做的就是将 IOptions<JwtConfiguration> 类型替换为 ServiceManager 和 AuthenticationService 类中的 IOptionsSnapshot<JwtConfiguration>IOptionsMonitor<JwtConfiguration> 类型。此外,如果我们使用 IOptionsMonitor,则不能使用 Value 属性,而只能使用 CurrentValue。

So the main difference between these two interfaces is that the IOptionsSnapshot service is registered as a scoped service and thus can’t be injected inside the singleton service. On the other hand, IOptionsMonitor is registered as a singleton service and can be injected into any service lifetime.
因此,这两个接口之间的主要区别在于,IOptionsSnapshot 服务注册为范围服务,因此不能注入到单一实例服务中。另一方面,IOptionsMonitor 注册为单一实例服务,可以注入到任何服务生存期中。

To make the comparison even clearer, we have prepared the following list for you:
为了使比较更加清晰,我们为您准备了以下列表:

IOptions:

• Is the original Options interface and it’s better than binding the whole Configuration
是原始的 Options 接口,比绑定整个 Configuration 要好

• Does not support configuration reloading
不支持重新加载配置

• Is registered as a singleton service and can be injected anywhere
注册为单例服务,可以在任何地方注入

• Binds the configuration values only once at the registration, and returns the same values every time
注册时仅绑定一次配置值,并且每次都返回相同的值

• Does not support named options
不支持命名选项

IOptionsSnapshot:

• Registered as a scoped service
注册为范围服务

• Supports configuration reloading
支持配置重新加载

• Cannot be injected into singleton services
不能注入到单例服务

• Values reload per request
每个请求重新加载值

• Supports named options
支持命名选项

IOptionsMonitor:

• Registered as a singleton service
注册为单例服务

• Supports configuration reloading
支持配置重新加载

• Can be injected into any service lifetime
可以注入任何使用寿命

• Values are cached and reloaded immediately
值会立即缓存并重新加载

• Supports named options
支持命名选项

Having said that, we can see that if we don’t want to enable live reloading or we don’t need named options, we can simply use IOptions<T>. If we do, we can use either IOptionsSnapshot<T> or IOptionsMonitor<T>,but IOptionsMonitor<T> can be injected into other singleton services while IOptionsSnapshot<T> cannot.
话虽如此,我们可以看到,如果我们不想启用实时重新加载或不需要命名选项,我们可以简单地使用 IOptions<T>。如果这样做,我们可以使用 IOptionsSnapshot<T>IOptionsMonitor<T>,但 IOptionsMonitor<T> 可以注入到其他单一实例服务中,而 IOptionsSnapshot<T> 则不能。

We have mentioned Named Options a couple of times so let’s explain what that is.
我们已经提到了几次 Named Options,所以让我们解释一下它是什么。

Let’s assume, just for example sake, that we have a configuration like this one:
让我们假设,只是为了说明原因,我们有一个这样的配置:

"JwtSettings": { "validIssuer": "CodeMazeAPI", "validAudience": "https://localhost:5001", "expires": 5 }, "JwtAPI2Settings": { "validIssuer": "CodeMazeAPI2", "validAudience": "https://localhost:5002", "expires": 10 },

Instead of creating a new JwtConfiguration2 class that has the same properties as our existing JwtConfiguration class, we can add another configuration:
我们可以添加另一个配置,而不是创建一个与现有 JwtConfiguration 类具有相同属性的新 JwtConfiguration2 类:

services.Configure<JwtConfiguration>("JwtSettings", configuration.GetSection("JwtSettings")); services.Configure<JwtConfiguration>("JwtAPI2Settings", configuration.GetSection("JwtAPI2Settings"));

Now both sections are mapped to the same configuration class, which makes sense. We don’t want to create multiple classes with the same properties and just name them differently. This is a much better way of doing it.
现在,这两个部分都映射到同一个 configuration class,这是有道理的。我们不想创建多个具有相同属性的类,然后只是以不同的方式命名它们。这是一种更好的方法。

Calling the specific option is now done using the Get method with a section name as a parameter instead of the Value or CurrentValue properties:
现在,使用 Get 方法将节名称作为参数,而不是 Value 或 CurrentValue 属性来调用特定选项:

_jwtConfiguration = _configuration.Get("JwtSettings");

That’s it. All the rest is the same.
就是这样。其余的都是一样的。

Ultimate ASP.NET Core Web API 28 REFRESH TOKEN

28 REFRESH TOKEN
28 刷新令牌

In this chapter, we are going to learn about refresh tokens and their use in modern web application development.‌
在本章中,我们将了解刷新令牌及其在现代 Web 应用程序开发中的使用。

In the previous chapter, we have created a flow where a user logs in, gets an access token to be able to access protected resources, and after the token expires, the user has to log in again to obtain a new valid token:
在上一章中,我们创建了一个流程,用户登录后,获取访问令牌才能访问受保护的资源,令牌过期后,用户必须再次登录才能获取新的有效令牌:

alt text

This flow is great and is used by many enterprise applications.
此流程非常棒,并被许多企业应用程序使用。

But sometimes we have a requirement not to force our users to log in every single time the token expires. For that, we can use a refresh token.
但有时我们要求不要在每次令牌过期时都强制我们的用户登录。为此,我们可以使用 refresh token。

Refresh tokens are credentials that can be used to acquire new access tokens. When an access token expires, we can use a refresh token to get a new access token from the authentication component. The lifetime of a refresh token is usually set much longer compared to the lifetime of an access token.
刷新令牌是可用于获取新访问令牌的凭证。当 access token 过期时,我们可以使用 refresh token 从身份验证组件获取新的访问 Token。与访问令牌的生命周期相比,刷新令牌的生命周期通常要长得多。

Let’s introduce the refresh token to our authentication workflow:
让我们将刷新令牌引入我们的身份验证工作流程:

alt text

  1. First, the client authenticates with the authentication component by providing the credentials.
    首先,客户端通过提供凭证来使用身份验证组件进行身份验证。

  2. Then, the authentication component issues the access token and the refresh token.
    然后,身份验证组件颁发访问令牌和刷新令牌。

  3. After that, the client requests the resource endpoints for a protected resource by providing the access token.
    之后,客户端通过提供访问令牌来请求受保护资源的资源终端节点。

  4. The resource endpoint validates the access token and provides a protected resource.
    资源终端节点验证访问令牌并提供受保护的资源。

  5. Steps 3 & 4 keep on repeating until the access token expires.
    步骤3和4不断重复,直到访问令牌过期。

  6. Once the access token expires, the client requests a new access token by providing the refresh token.
    访问令牌过期后,客户端将通过提供刷新令牌来请求新的访问令牌。

  7. The authentication component issues a new access token and refresh token.
    身份验证组件颁发新的访问令牌和刷新令牌。

  8. Steps 3 through 7 keep on repeating until the refresh token expires.
    步骤 3 到 7 会不断重复,直到刷新令牌过期。

  9. Once the refresh token expires, the client needs to authenticate with the authentication server once again and the flow repeats from step 1.
    刷新令牌过期后,客户端需要再次向身份验证服务器进行身份验证,并且从步骤 1 开始重复该流程。

28.1 Why Do We Need a Refresh Token

为什么需要刷新令牌

So, why do we need both access tokens and refresh tokens? Why don’t we just set a long expiration date, like a month or a year for the access tokens? Because, if we do that and someone manages to get hold of our access token they can use it for a long period, even if we change our password!‌
那么,为什么我们需要访问令牌和刷新令牌呢?为什么我们不直接设置一个较长的到期日期,例如访问令牌的一个月或一年呢?因为,如果我们这样做并且有人设法获得了我们的访问令牌,即使我们更改了密码,他们也可以长时间使用它!

The idea of refresh tokens is that we can make the access token short- lived so that, even if it is compromised, the attacker gets access only for a shorter period. With refresh token-based flow, the authentication server issues a one-time use refresh token along with the access token. The app stores the refresh token safely.
刷新令牌的理念是,我们可以使访问令牌的生存期较短,这样,即使它被泄露,攻击者也只能在较短的时间内获得访问权限。使用基于刷新令牌的流程,身份验证服务器会颁发一次性使用的刷新令牌以及访问令牌。应用程序安全地存储刷新令牌。

Every time the app sends a request to the server it sends the access token in the Authorization header and the server can identify the app using it. Once the access token expires, the server will send a token expired response. Once the app receives the token expired response, it sends the expired access token and the refresh token to obtain a new access token and a refresh token.
每次应用程序向服务器发送请求时,它都会在 Authorization 标头中发送访问令牌,服务器可以使用它来识别应用程序。一旦访问令牌过期,服务器将发送令牌过期响应。应用程序收到令牌过期响应后,它会发送过期的访问令牌和刷新令牌,以获取新的访问令牌和刷新令牌。

If something goes wrong, the refresh token can be revoked which means that when the app tries to use it to get a new access token, that request will be rejected and the user will have to enter credentials once again and authenticate.
如果出现问题,可以撤销刷新令牌,这意味着当应用程序尝试使用它来获取新的访问令牌时,该请求将被拒绝,用户将不得不再次输入凭据并进行身份验证。

Thus, refresh tokens help in a smooth authentication workflow without the need for users to submit their credentials frequently, and at the same time, without compromising the security of the app.
因此,刷新令牌有助于实现顺畅的身份验证工作流程,而无需用户频繁提交其凭证,同时不会影响应用程序的安全性。

28.2 Refresh Token Implementation

28.2 刷新令牌实现

So far we have learned the concept of refresh tokens. Now, let’s dig into‌ the implementation part.
到目前为止,我们已经了解了刷新令牌的概念。现在,让我们深入研究实现部分。

The first thing we have to do is to modify the User class:
我们要做的第一件事是修改 User 类:

public class User : IdentityUser { public string? FirstName { get; set; } public string? LastName { get; set; } public string? RefreshToken { get; set; } public DateTime RefreshTokenExpiryTime { get; set; } }

Here we add two additional properties, which we are going to add to the AspNetUsers table.
在这里,我们添加了两个附加属性,我们将将它们添加到 AspNetUsers 表中。

To do that, we have to create and execute another migration:
为此,我们必须创建并执行另一个迁移:

Add-Migration AdditionalUserFiledsForRefreshToken

If for some reason you get the message that you need to review your migration due to possible data loss, you should inspect the migration file and leave only the code that adds and removes our additional columns:
如果出于某种原因,您收到一条消息,指出由于可能的数据丢失而需要检查迁移,则应检查迁移文件,并仅保留添加和删除其他列的代码:

protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn<string>( name: "RefreshToken", table: "AspNetUsers", type: "nvarchar(max)", nullable: true); migrationBuilder.AddColumn<DateTime>( name: "RefreshTokenExpiryTime", table: "AspNetUsers", type: "datetime2", nullable: false, defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn( name: "RefreshToken", table: "AspNetUsers"); migrationBuilder.DropColumn( name: "RefreshTokenExpiryTime", table: "AspNetUsers"); }

Also, you should open the RepositoryContextModelSnapshot file, find the AspNetRoles part and revert the Ids of both roles to the previous values:
此外,还应打开 RepositoryContextModelSnapshot 文件,找到 AspNetRoles 部分,并将两个角色的 ID 还原为以前的值:

b.ToTable("AspNetRoles", (string)null); b.HasData( new { Id = "4ac8240a-8498-4869-bc86-60e5dc982d27", ConcurrencyStamp = "ec511bd4-4853-426a-a2fc-751886560c9a", Name = "Manager", NormalizedName = "MANAGER" }, new { Id = "562419f5-eed1-473b-bcc1-9f2dbab182b4", ConcurrencyStamp = "937e9988-9f49-4bab-a545-b422dde85016", Name = "Administrator", NormalizedName = "ADMINISTRATOR" });

After that is done, we can execute our migration with the Update- Database command. This will add two additional columns in the AspNetUsers table.
完成后,我们可以使用 Update- Database 命令执行迁移。这将在 AspNetUsers 表中添加两个附加列。

To continue, let’s create a new record in the Shared/DataTransferObjects folder:
要继续,让我们在 Shared/DataTransferObjects 文件夹中创建一个新记录:

public record TokenDto(string AccessToken, string RefreshToken);

Next, we are going to modify the IAuthenticationService interface:
接下来,我们将修改 IAuthenticationService 接口:

public interface IAuthenticationService { Task<IdentityResult> RegisterUser(UserForRegistrationDto userForRegistration); Task<bool> ValidateUser(UserForAuthenticationDto userForAuth); Task<TokenDto> CreateToken(bool populateExp); }

Then, we have to implement two new methods in the AuthenticationService class:
然后,我们必须在 AuthenticationService 类中实现两个新方法:

private string GenerateRefreshToken() { var randomNumber = new byte[32]; using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(randomNumber); return Convert.ToBase64String(randomNumber);} } private ClaimsPrincipal GetPrincipalFromExpiredToken(string token) { var jwtSettings = _configuration.GetSection("JwtSettings"); var tokenValidationParameters = new TokenValidationParameters { ValidateAudience = true, ValidateIssuer = true, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SECRET"))), ValidateLifetime = true, ValidIssuer = jwtSettings["validIssuer"], ValidAudience = jwtSettings["validAudience"] }; var tokenHandler = new JwtSecurityTokenHandler(); SecurityToken securityToken; var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken); var jwtSecurityToken = securityToken as JwtSecurityToken; if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) { throw new SecurityTokenException("Invalid token"); } return principal; }

GenerateRefreshToken contains the logic to generate the refresh token. We use the RandomNumberGenerator class to generate a cryptographic random number for this purpose.
GenerateRefreshToken 包含生成刷新令牌的逻辑。为此,我们使用 RandomNumberGenerator 类生成加密随机数。

GetPrincipalFromExpiredToken is used to get the user principal from the expired access token. We make use of the ValidateToken method from the JwtSecurityTokenHandler class for this purpose. This method validates the token and returns the ClaimsPrincipal object.
GetPrincipalFromExpiredToken 用于从过期的访问令牌中获取用户主体。为此,我们使用 JwtSecurityTokenHandler 类中的 ValidateToken 方法。此方法验证令牌并返回 ClaimsPrincipal 对象。

After that, to generate a refresh token and the expiry date for the logged- in user, and to return both the access token and refresh token to the caller, we have to modify the CreateToken method in the same class:
之后,要为已登录用户生成刷新令牌和到期日期,并将访问令牌和刷新令牌返回给调用方,我们必须修改同一类中的 CreateToken 方法:

public async Task<TokenDto> CreateToken(bool populateExp) { var signingCredentials = GetSigningCredentials();var claims = await GetClaims(); var tokenOptions = GenerateTokenOptions(signingCredentials, claims); var refreshToken = GenerateRefreshToken(); _user.RefreshToken = refreshToken; if(populateExp) _user.RefreshTokenExpiryTime = DateTime.Now.AddDays(7); await _userManager.UpdateAsync(_user); var accessToken = new JwtSecurityTokenHandler().WriteToken(tokenOptions); return new TokenDto(accessToken, refreshToken); }

Finally, we have to modify the Authenticate action:
最后,我们必须修改 Authenticate作:

[HttpPost("login")] [ServiceFilter(typeof(ValidationFilterAttribute))] public async Task<IActionResult> Authenticate([FromBody] UserForAuthenticationDto user) { if (!await _service.AuthenticationService.ValidateUser(user)) return Unauthorized(); var tokenDto = await _service.AuthenticationService .CreateToken(populateExp: true); return Ok(tokenDto); }

That’s it regarding the action modification.
这就是关于动作修改的内容。

Now, we can test this by sending the POST request from Postman:
现在,我们可以通过从 Postman 发送 POST 请求来测试这一点:

https://localhost:5001/api/authentication/login

alt text

We can see the successful authentication and both our tokens. Additionally, if we inspect the database, we are going to find populated RefreshToken and Expiry columns for JDoe:
我们可以看到成功的身份验证和我们的令牌。此外,如果我们检查数据库,我们将找到填充的 JDoe 的 RefreshToken 和 Expiry 列:

alt text

It is a good practice to have a separate endpoint for the refresh token‌ action, and that’s exactly what we are going to do now.
最好为刷新令牌作设置一个单独的终端节点,这正是我们现在要做的事情。

Let’s start by creating a new TokenController in the Presentation project:
让我们首先在 Presentation 项目中创建一个新的 TokenController:

[Route("api/token")] [ApiController] public class TokenController : ControllerBase { private readonly IServiceManager _service; public TokenController(IServiceManager service) => _service = service; }

Before we continue with the controller modification, we are going to modify the IAuthenticationService interface:
在继续修改控制器之前,我们将修改 IAuthenticationService 接口:

public interface IAuthenticationService { Task<IdentityResult> RegisterUser(UserForRegistrationDto userForRegistration); Task<bool> ValidateUser(UserForAuthenticationDto userForAuth); Task<TokenDto> CreateToken(bool populateExp); Task<TokenDto> RefreshToken(TokenDto tokenDto); }

And to implement this method:
要实现此方法:

public async Task<TokenDto> RefreshToken(TokenDto tokenDto) { var principal = GetPrincipalFromExpiredToken(tokenDto.AccessToken); var user = await _userManager.FindByNameAsync(principal.Identity.Name); if (user == null || user.RefreshToken != tokenDto.RefreshToken || user.RefreshTokenExpiryTime <= DateTime.Now) throw new RefreshTokenBadRequest(); _user = user; return await CreateToken(populateExp: false); }

We first extract the principal from the expired token and use the Identity.Name property, which is the username of the user, to fetch that user from the database. If the user doesn’t exist, or the refresh tokens are not equal, or the refresh token has expired, we stop the flow returning the BadRequest response to the user. Then we just populate the _user variable and call the CreateToken method to generate new Access and Refresh tokens. This time, we don’t want to update the expiry time of the refresh token thus sending false as a parameter.
我们首先从过期的令牌中提取主体,并使用 Identity.Name 属性(即用户的用户名)从数据库中获取该用户。如果用户不存在,或者刷新令牌不相等,或者刷新令牌已过期,我们将停止向用户返回 BadRequest 响应的流。然后,我们只需填充 _user 变量并调用 CreateToken 方法以生成新的 Access 和 Refresh 令牌。这一次,我们不想更新刷新令牌的到期时间,因此发送 false 作为参数。

Since we don’t have the RefreshTokenBadRequest class, let’s create it in the Entities\Exceptions folder:
由于我们没有 RefreshTokenBadRequest 类,因此让我们在 Entities\Exceptions 文件夹中创建它:

public sealed class RefreshTokenBadRequest : BadRequestException { public RefreshTokenBadRequest() : base("Invalid client request. The tokenDto has some invalid values.") { } }

And add a required using directive in the AuthenticationService class to remove the present error.
并在 AuthenticationService 类中添加必需的 using 指令以删除当前错误。

Finally, let’s add one more action in the TokenController:
最后,让我们在 TokenController 中再添加一个作:

[HttpPost("refresh")] [ServiceFilter(typeof(ValidationFilterAttribute))] public async Task<IActionResult> Refresh([FromBody]TokenDto tokenDto) { var tokenDtoToReturn = await _service.AuthenticationService.RefreshToken(tokenDto); return Ok(tokenDtoToReturn); }

That’s it.
就是这样。

Our refresh token logic is prepared and ready for testing.
我们的刷新令牌逻辑已准备就绪,可以进行测试。

Let’s first send the POST authentication request:
让我们首先发送 POST 身份验证请求:

https://localhost:5001/api/authentication/login

alt text

As before, we have both tokens in the response body.
和以前一样,我们在响应正文中有两个标记。

Now, let’s send the POST refresh request with these tokens as the request body:
现在,让我们发送 POST 刷新请求,并将这些令牌作为请求正文:

https://localhost:5001/api/token/refresh

alt text

And we can see new tokens in the response body. Additionally, if we inspect the database, we will find the same refresh token value:
我们可以在响应正文中看到新标记。此外,如果我们检查数据库,我们将发现相同的刷新令牌值:

alt text

Usually, in your client application, you would inspect the exp claim of the access token and if it is about to expire, your client app would send the request to the api/token endpoint and get a new set of valid tokens.
通常,在您的客户端应用程序中,您将检查访问令牌的 exp 声明,如果它即将过期,您的客户端应用程序会将请求发送到 api/token 终端节点并获取一组新的有效令牌。

Ultimate ASP.NET Core Web API 27 JWT, IDENTITY, AND REFRESH TOKEN

27 JWT, IDENTITY, AND REFRESH TOKEN
27 JWT、身份认证和刷新令牌

User authentication is an important part of any application. It refers to the process of confirming the identity of an application’s users. Implementing it properly could be a hard job if you are not familiar with the process.‌
用户身份验证是任何应用程序的重要组成部分。它是指确认应用程序用户身份的过程。如果您不熟悉该过程,正确实施它可能是一项艰巨的工作。

Also, it could take a lot of time that could be spent on different features of an application.
此外,可能需要花费大量时间,这些时间可能会花在应用程序的不同功能上。

So, in this section, we are going to learn about authentication and authorization in ASP.NET Core by using Identity and JWT (Json Web Token). We are going to explain step by step how to integrate Identity in the existing project and then how to implement JWT for the authentication and authorization actions.
因此,在本节中,我们将使用身份和 JWT(Json Web 令牌)了解 ASP.NET Core 中的身份验证和授权。我们将逐步解释如何在现有项目中集成 Identity,然后如何为身份验证和授权作实施 JWT。

ASP.NET Core provides us with both functionalities, making implementation even easier.
ASP.NET Core 为我们提供了这两种功能,使实施变得更加容易。

Finally, we are going to learn more about the refresh token flow and implement it in our Web API project.
最后,我们将了解有关刷新令牌流的更多信息,并在我们的 Web API 项目中实现它。

So, let’s start with Identity integration.
那么,让我们从 Identity integration 开始。

27.1 Implementing Identity in ASP.NET Core Project

27.1 在 ASP.NET Core Project 中实现身份认真

Asp.NET Core Identity is the membership system for web applications that includes membership, login, and user data. It provides a rich set of services that help us with creating users, hashing their passwords, creating a database model, and the authentication overall.‌That said, let’s start with the integration process.
Asp.NET Core Identity 是 Web 应用程序的成员资格系统,包括成员资格、登录名和用户数据。它提供了一组丰富的服务,可帮助我们创建用户、对他们的密码进行哈希处理、创建数据库模型以及整体身份验证。也就是说,让我们从集成过程开始。

The first thing we have to do is to install the Microsoft.AspNetCore.Identity.EntityFrameworkCore library in the Entities project:
我们要做的第一件事是在 Entities 项目中安装 Microsoft.AspNetCore.Identity.EntityFrameworkCore 库:

alt text

After the installation, we are going to create a new User class in the Entities/Models folder:
安装后,我们将在 Entities/Models 文件夹中创建一个新的 User 类:

public class User : IdentityUser { public string FirstName { get; set; } public string LastName { get; set; } }

Our class inherits from the IdentityUser class that has been provided by the ASP.NET Core Identity. It contains different properties and we can extend it with our own as well.
我们的类继承自 ASP.NET Core Identity 提供的 IdentityUser 类。它包含不同的属性,我们也可以使用自己的属性来扩展它。

After that, we have to modify the RepositoryContext class:
之后,我们必须修改 RepositoryContext 类:

public class RepositoryContext : IdentityDbContext<User> { public RepositoryContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfiguration(new CompanyConfiguration()); modelBuilder.ApplyConfiguration(new EmployeeConfiguration()); } public DbSet<Company> Companies { get; set; } public DbSet<Employee> Employees { get; set; } }

So, our class now inherits from the IdentityDbContext class and not DbContext because we want to integrate our context with Identity. For this, we have to include the Identity.EntityFrameworkCore namespace:
因此,我们的类现在继承自 IdentityDbContext 类,而不是 DbContext,因为我们希望将上下文与 Identity 集成。为此,我们必须包含 Identity.EntityFrameworkCore 命名空间:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

We don’t have to install the library in the Repository project since we already did that in the Entities project, and Repository has the reference to Entities.
我们不必在 Repository 项目中安装库,因为我们已经在 Entities 项目中安装了该库,并且 Repository 具有对 Entities 的引用。

Additionally, we call the OnModelCreating method from the base class. This is required for migration to work properly.
此外,我们还从基类调用 OnModelCreating 方法。这是迁移正常工作所必需的。

Now, we have to move on to the configuration part.
现在,我们必须进入配置部分。

To do that, let’s create a new extension method in the ServiceExtensions class:
为此,让我们在 ServiceExtensions 类中创建一个新的扩展方法:

public static void ConfigureIdentity(this IServiceCollection services) { var builder = services.AddIdentity<User, IdentityRole>(o => { o.Password.RequireDigit = true; o.Password.RequireLowercase = false; o.Password.RequireUppercase = false; o.Password.RequireNonAlphanumeric = false; o.Password.RequiredLength = 10; o.User.RequireUniqueEmail = true; }) .AddEntityFrameworkStores<RepositoryContext>() .AddDefaultTokenProviders(); }

With the AddIdentity method, we are adding and configuring Identity for the specific type; in this case, the User and the IdentityRole type. We use different configuration parameters that are pretty self-explanatory on their own. Identity provides us with even more features to configure, but these are sufficient for our example.
使用 AddIdentity 方法,我们将为特定类型添加和配置 Identity;在本例中为 User 和 IdentityRole 类型。我们使用不同的配置参数,这些参数本身就很容易理解。Identity 为我们提供了更多需要配置的功能,但这些功能对于我们的示例来说已经足够了。

Then, we add EntityFrameworkStores implementation with the default token providers.
然后,我们添加具有默认令牌提供程序的 EntityFrameworkStores 实现。

Now, let’s modify the Program class:
现在,让我们修改 Program 类:

builder.Services.AddAuthentication(); 
builder.Services.ConfigureIdentity();

And, let’s add the authentication middleware to the application’s request pipeline:
然后,让我们将身份验证中间件添加到应用程序的请求管道中:

app.UseAuthorization();
app.UseAuthentication();

That’s it. We have prepared everything we need.
就是这样。我们已经准备好了我们需要的一切。

27.2 Creating Tables and Inserting Roles

27.2 创建 table 和插入角色

Creating tables is quite an easy process. All we have to do is to create and apply migration. So, let’s create a migration:‌
创建表格是一个非常简单的过程。我们所要做的就是创建并应用迁移。那么,让我们创建一个迁移:

PM> Add-Migration CreatingIdentityTables

And then apply it:
然后应用它:

PM> Update-Database

If we check our database now, we are going to see additional tables:
如果我们现在检查我们的数据库,我们将看到额外的表:

alt text

For our project, the AspNetRoles, AspNetUserRoles, and AspNetUsers tables will be quite enough. If you open the AspNetUsers table, you will see additional FirstName and LastName columns.
对于我们的项目,AspNetRoles、AspNetUserRoles 和 AspNetUsers 表就足够了。如果打开 AspNetUsers 表,将看到其他 FirstName 和 LastName 列。

Now, let’s insert several roles in the AspNetRoles table, again by using migrations. The first thing we are going to do is to create the RoleConfiguration class in the Repository/Configuration folder:
现在,让我们再次使用迁移在 AspNetRoles 表中插入多个角色。我们要做的第一件事是在 Repository/Configuration 文件夹中创建 RoleConfiguration 类:

public class RoleConfiguration : IEntityTypeConfiguration<IdentityRole> { public void Configure(EntityTypeBuilder<IdentityRole> builder) {builder.HasData( new IdentityRole { Name = "Manager", NormalizedName = "MANAGER" }, new IdentityRole { Name = "Administrator", NormalizedName = "ADMINISTRATOR" } ); }

For this to work, we need the following namespaces included:
为此,我们需要包含以下命名空间:

using Microsoft.AspNetCore.Identity; 
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

And let’s modify the OnModelCreating method in the RepositoryContext class:
让我们修改 RepositoryContext 类中的 OnModelCreating 方法:

protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfiguration(new CompanyConfiguration()); modelBuilder.ApplyConfiguration(new EmployeeConfiguration()); modelBuilder.ApplyConfiguration(new RoleConfiguration()); }

Finally, let’s create and apply migration:
最后,让我们创建并应用迁移:

PM> Add-Migration AddedRolesToDb
PM> Update-Database

If you check the AspNetRoles table, you will find two new roles created.
如果检查 AspNetRoles 表,您将发现创建了两个新角色。

27.3 User Creation

27.3 用户创建

To create/register a new user, we have to create a new controller:‌
要创建/注册新用户,我们必须创建一个新的控制器:

[Route("api/authentication")] [ApiController] public class AuthenticationController : ControllerBase { private readonly IServiceManager _service; public AuthenticationController(IServiceManager service) => _service = service; }

So, nothing new here. We have the basic setup for our controller with IServiceManager injected.
所以,这里没什么新鲜事。我们有了注入了 IServiceManager 的控制器的基本设置。

The next thing we have to do is to create a UserForRegistrationDto record in the Shared/DataTransferObjects folder:
接下来我们要做的是在 Shared/DataTransferObjects 文件夹中创建一个 UserForRegistrationDto 记录:

public record UserForRegistrationDto { public string? FirstName { get; init; } public string? LastName { get; init; } [Required(ErrorMessage = "Username is required")] public string? UserName { get; init; } [Required(ErrorMessage = "Password is required")] public string? Password { get; init; } public string? Email { get; init; } public string? PhoneNumber { get; init; } public ICollection<string>? Roles { get; init; } }

Then, let’s create a mapping rule in the MappingProfile class:
然后,让我们在 MappingProfile 类中创建一个映射规则:

CreateMap<UserForRegistrationDto, User>();

Since we want to extract all the registration/authentication logic to the service layer, we are going to create a new IAuthenticationService interface inside the Service.Contracts project:
由于我们希望将所有注册/身份验证逻辑提取到服务层,因此我们将在 Service.Contracts 项目中创建一个新的 IAuthenticationService 接口:

public interface IAuthenticationService { Task<IdentityResult> RegisterUser(UserForRegistrationDto userForRegistration); }

This method will execute the registration logic and return the identity result to the caller.
该方法将执行注册逻辑,并将身份结果返回给调用方。

Now that we have the interface, we need to create an implementation service class inside the Service project:
现在我们有了接口,我们需要在 Service 项目中创建一个实现服务类:

internal sealed class AuthenticationService : IAuthenticationService { private readonly ILoggerManager _logger; private readonly IMapper _mapper; private readonly UserManager<User> _userManager; private readonly IConfiguration _configuration; public AuthenticationService(ILoggerManager logger, IMapper mapper, UserManager<User> userManager, IConfiguration configuration) { _logger = logger;_mapper = mapper; _userManager = userManager; _configuration = configuration; } }

This code is pretty familiar from the previous service classes except for the UserManager class. This class is used to provide the APIs for managing users in a persistence store. It is not concerned with how user information is stored. For this, it relies on a UserStore (which in our case uses Entity Framework Core).
此代码与前面的服务类非常相似,但 UserManager 类除外。此类用于提供用于管理持久性存储中的用户的 API。它不关心用户信息的存储方式。为此,它依赖于 UserStore(在我们的例子中使用 Entity Framework Core)。

Of course, we have to add some additional namespaces:
当然,我们必须添加一些额外的命名空间:

using AutoMapper; using Contracts; using Entities.Models; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Service.Contracts;

Great. Now, we can implement the RegisterUser method:
现在,我们可以实现 RegisterUser 方法:

public async Task<IdentityResult> RegisterUser(UserForRegistrationDto userForRegistration) { var user = _mapper.Map<User>(userForRegistration); var result = await _userManager.CreateAsync(user, userForRegistration.Password); if (result.Succeeded) await _userManager.AddToRolesAsync(user, userForRegistration.Roles); return result; }

So we map the DTO object to the User object and call the CreateAsync method to create that specific user in the database. The CreateAsync method will save the user to the database if the action succeeds or it will return error messages as a result.
因此,我们将 DTO 对象映射到 User 对象,并调用 CreateAsync 方法在数据库中创建该特定用户。如果作成功,CreateAsync 方法会将用户保存到数据库,否则它将返回错误消息。

After that, if a user is created, we add that user to the named roles — the ones sent from the client side — and return the result.
之后,如果创建了一个用户,我们将该用户添加到命名角色(从客户端发送的角色)并返回结果。

If you want, before calling AddToRoleAsync or AddToRolesAsync, you can check if roles exist in the database. But for that, you have to inject RoleManager and use the RoleExistsAsync method.
如果需要,在调用 AddToRoleAsync 或 AddToRolesAsync 之前,可以检查数据库中是否存在角色。但为此,您必须注入 RoleManager 并使用 RoleExistsAsync 方法。

We want to provide this service to the caller through ServiceManager and for that, we have to modify the IServiceManager interface first:
我们希望通过 ServiceManager 向调用方提供此服务,为此,我们必须先修改 IServiceManager 接口:

public interface IServiceManager { ICompanyService CompanyService { get; } IEmployeeService EmployeeService { get; } IAuthenticationService AuthenticationService { get; } }

And then the ServiceManager class:
然后是 ServiceManager 类:

public sealed class ServiceManager : IServiceManager { private readonly Lazy<ICompanyService> _companyService; private readonly Lazy<IEmployeeService> _employeeService; private readonly Lazy<IAuthenticationService> _authenticationService; public ServiceManager(IRepositoryManager repositoryManager, ILoggerManager logger, IMapper mapper, IEmployeeLinks employeeLinks, UserManager<User> userManager, IConfiguration configuration) { _companyService = new Lazy<ICompanyService>(() => new CompanyService(repositoryManager, logger, mapper)); _employeeService = new Lazy<IEmployeeService>(() => new EmployeeService(repositoryManager, logger, mapper, employeeLinks)); _authenticationService = new Lazy<IAuthenticationService>(() => new AuthenticationService(logger, mapper, userManager, configuration)); } public ICompanyService CompanyService => _companyService.Value; public IEmployeeService EmployeeService => _employeeService.Value; public IAuthenticationService AuthenticationService => _authenticationService.Value; }

Finally, it is time to create the RegisterUser action:
最后,是时候创建 RegisterUser作了:

[HttpPost] [ServiceFilter(typeof(ValidationFilterAttribute))] public async Task<IActionResult> RegisterUser([FromBody] UserForRegistrationDto userForRegistration) { var result = await _service.AuthenticationService.RegisterUser(userForRegistration); if (!result.Succeeded){ foreach (var error in result.Errors) { ModelState.TryAddModelError(error.Code, error.Description); } return BadRequest(ModelState); } return StatusCode(201); }

We are implementing our existing action filter for the entity and model validation on top of our action. Then, we call the RegisterUser method and accept the result. If the registration fails, we iterate through each error add it to the ModelState and return the BadRequest response. Otherwise, we return the 201 created status code.
我们正在为作之上的实体和模型验证实现现有的作筛选器。然后,我们调用 RegisterUser 方法并接受结果。如果注册失败,我们将遍历每个错误,将其添加到 ModelState 并返回 BadRequest 响应。否则,我们将返回 201 created 状态代码。

Before we continue with testing, we should increase a rate limit from 3 to 30 (ServiceExtensions class, ConfigureRateLimitingOptions method) just to not stand in our way while we’re testing the different features of our application.
在继续测试之前,我们应该将速率限制从 3 增加到 30(ServiceExtensions 类,ConfigureRateLimitingOptions 方法),以免在测试应用程序的不同功能时成为我们的障碍。

Now we can start with testing.Let’s send a valid request first:
现在我们可以从测试开始。让我们先发送一个有效的请求:

https://localhost:5001/api/authentication

alt text

And we get 201, which means that the user has been created and added to the role. We can send additional invalid requests to test our Action and Identity features.
我们得到 201,这意味着用户已被创建并添加到角色中。我们可以发送其他无效请求来测试我们的 Action 和 Identity 功能。

If the model is invalid:
如果模型无效:

https://localhost:5001/api/authentication

alt text

If the password is invalid:
如果密码无效:

https://localhost:5001/api/authentication

alt text

Finally, if we want to create a user with the same user name and email:

最后,如果我们想创建一个具有相同用户名和电子邮件的用户:
https://localhost:5001/api/authentication

alt text

Excellent. Everything is working as planned. We can move on to the JWT implementation.
非常好。一切都在按计划进行。我们可以继续进行 JWT 实现。

27.4 Big Picture

27.4 大局

Before we get into the implementation of authentication and authorization, let’s have a quick look at the big picture. There is an application that has a login form. A user enters their username and password and presses the login button. After pressing the login button, a client (e.g., web browser) sends the user’s data to the server’s API endpoint:‌
在我们开始实现身份验证和授权之前,让我们快速了解一下大局。有一个应用程序具有登录表单。用户输入其用户名和密码,然后按登录按钮。按下登录按钮后,客户端(例如 Web 浏览器)将用户的数据发送到服务器的 API 端点:

alt text

When the server validates the user’s credentials and confirms that the user is valid, it’s going to send an encoded JWT to the client. A JSON web token is a JavaScript object that can contain some attributes of the logged-in user. It can contain a username, user subject, user roles, or some other useful information.
当服务器验证用户的凭证并确认用户有效时,它将向客户端发送编码的 JWT。JSON Web 令牌是一个 JavaScript 对象,可以包含已登录用户的某些属性。它可以包含用户名、用户主题、用户角色或其他一些有用的信息。

27.5 About JWT

27.5 关于 JWT

JSON web tokens enable a secure way to transmit data between two parties in the form of a JSON object. It’s an open standard and it’s a popular mechanism for web authentication. In our case, we are going to use JSON web tokens to securely transfer a user’s data between the client and the server.‌
JSON Web 令牌支持以 JSON 对象的形式在两方之间传输数据的安全方式。它是一个开放标准,也是一种流行的 Web 身份验证机制。在我们的例子中,我们将使用 JSON Web 令牌在客户端和服务器之间安全地传输用户的数据。

JSON web tokens consist of three basic parts: the header, the payload, and the signature.
JSON Web 令牌由三个基本部分组成:标头、有效负载和签名。

One real example of a JSON web token:
JSON Web 令牌的一个真实示例:

alt text

Every part of all three parts is shown in a different color. The first part of JWT is the header, which is a JSON object encoded in the base64 format. The header is a standard part of JWT and we don’t have to worry about it. It contains information like the type of token and the name of the algorithm:
所有三个部分的每个部分都以不同的颜色显示。JWT 的第一部分是标头,它是以 base64 格式编码的 JSON 对象。标头是 JWT 的标准部分,我们不必担心它。它包含令牌类型和算法名称等信息:

{ "alg": "HS256", "typ": "JWT" }

After the header, we have a payload which is also a JavaScript object encoded in the base64 format. The payload contains some attributes about the logged-in user. For example, it can contain the user id, the user subject, and information about whether a user is an admin user or not.
在标头之后,我们有一个有效负载,它也是一个以 base64 格式编码的 JavaScript 对象。有效负载包含有关已登录用户的一些属性。例如,它可以包含用户 ID、用户主题以及有关用户是否为管理员用户的信息。

JSON web tokens are not encrypted and can be decoded with any base64 decoder, so please never include sensitive information in the Payload:
JSON Web 令牌未加密,可以使用任何 base64 解码器进行解码,因此请不要在有效负载中包含敏感信息:

{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }

Finally, we have the signature part. Usually, the server uses the signature part to verify whether the token contains valid information, the information which the server is issuing. It is a digital signature that gets generated by combining the header and the payload. Moreover, it’s based on a secret key that only the server knows:
最后,我们有签名部分。通常,服务器使用签名部分来验证令牌是否包含有效信息,即服务器颁发的信息。它是通过组合标头和有效负载生成的数字签名。此外,它基于只有服务器知道的密钥:

alt text

So, if malicious users try to modify the values in the payload, they have to recreate the signature; for that purpose, they need the secret key only known to the server. On the server side, we can easily verify if the values are original or not by comparing the original signature with a new signature computed from the values coming from the client.
因此,如果恶意用户尝试修改有效负载中的值,则必须重新创建签名;为此,他们需要只有服务器知道的密钥。在服务器端,我们可以通过将原始签名与根据来自客户端的值计算的新签名进行比较,轻松验证值是否为原始值。

So, we can easily verify the integrity of our data just by comparing the digital signatures. This is the reason why we use JWT.
因此,我们只需比较数字签名即可轻松验证数据的完整性。这就是我们使用 JWT 的原因。

27.6 JWT Configuration

27.6 JWT 配置

Let’s start by modifying the appsettings.json file:‌
让我们从修改 appsettings.json 文件开始:

{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", } }, "ConnectionStrings": { "sqlConnection": "server=.; database=CompanyEmployee; Integrated Security=true" }, "JwtSettings": { "validIssuer": "CodeMazeAPI", "validAudience": "https://localhost:5001" }, "AllowedHosts": "*" }

We just store the issuer and audience information in the appsettings.json file. We are going to talk more about that in a minute. As you probably remember, we require a secret key on the server-side. So, we are going to create one and store it in the environment variable because this is much safer than storing it inside the project.
我们只将发行者和受众信息存储在 appsettings.json 文件中。我们稍后将详细讨论这个问题。您可能还记得,我们需要服务器端的密钥。因此,我们将创建一个并将其存储在环境变量中,因为这比将其存储在项目中要安全得多。

To create an environment variable, we have to open the cmd window as an administrator and type the following command:
要创建环境变量,我们必须以管理员身份打开 cmd 窗口并键入以下命令:

setx SECRET "CodeMazeSecretKey" /M

This is going to create a system environment variable with the name SECRET and the value CodeMazeSecretKey. By using /M we specify that we want a system variable and not local.
这将创建一个名称为 SECRET 且值为 CodeMazeSecretKey 的系统环境变量。通过使用 /M,我们指定我们想要一个系统变量,而不是局部变量。

Great. We can now modify the ServiceExtensions class:
我们现在可以修改 ServiceExtensions 类:

public static void ConfigureJWT(this IServiceCollection services, IConfiguration configuration) { var jwtSettings = configuration.GetSection("JwtSettings"); var secretKey = Environment.GetEnvironmentVariable("SECRET"); services.AddAuthentication(opt => { opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtSettings["validIssuer"], ValidAudience = jwtSettings["validAudience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)) }; }); }

First, we extract the JwtSettings from the appsettings.json file and extract our environment variable (If you keep getting null for the secret key, try restarting the Visual Studio or even your computer).
首先,我们从 appsettings.json 文件中提取 JwtSettings 并提取环境变量(如果密钥一直为 null,请尝试重新启动 Visual Studio 甚至计算机)。

Then, we register the JWT authentication middleware by calling the method AddAuthentication on the IServiceCollection interface. Next, we specify the authentication scheme JwtBearerDefaults.AuthenticationScheme as well as ChallengeScheme. We also provide some parameters that will be used while validating JWT. For this to work, we have to install the Microsoft.AspNetCore.Authentication.JwtBearer library.
然后,我们通过在 IServiceCollection 接口上调用 AddAuthentication 方法来注册 JWT 身份验证中间件。接下来,我们指定身份验证方案 JwtBearerDefaults.AuthenticationScheme 以及 ChallengeScheme。我们还提供了一些参数,这些参数将在验证 JWT 时使用。为此,我们必须安装 Microsoft.AspNetCore.Authentication.JwtBearer 库。

For this to work, we require the following namespaces:
为此,我们需要以下命名空间:

using Microsoft.AspNetCore.Authentication.JwtBearer; 
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens; 
using System.Text;

Excellent. We’ve successfully configured the JWT authentication.
非常好。我们已成功配置 JWT 身份验证。

According to the configuration, the token is going to be valid if:
根据配置,如果满足以下条件,则令牌将有效:

• The issuer is the actual server that created the token (ValidateIssuer=true)
颁发者是创建令牌的实际服务器 (ValidateIssuer=true)

• The receiver of the token is a valid recipient (ValidateAudience=true)
令牌的接收者是有效的接收者 (ValidateAudience=true)

• The token has not expired (ValidateLifetime=true)
令牌尚未过期 (ValidateLifetime=true)

• The signing key is valid and is trusted by the server (ValidateIssuerSigningKey=true)
签名密钥有效且受服务器信任 (ValidateIssuerSigningKey=true)

Additionally, we are providing values for the issuer, the audience, and the secret key that the server uses to generate the signature for JWT.
此外,我们还为颁发者、受众和服务器用于生成 JWT 签名的密钥提供值。

All we have to do is to call this method in the Program class:
我们所要做的就是在 Program 类中调用此方法:

builder.Services.ConfigureJWT(builder.Configuration);
builder.Services.AddAuthentication(); 
builder.Services.ConfigureIdentity();

And that is it. We can now protect our endpoints.
就是这样。我们现在可以保护我们的端点。

27.7 Protecting Endpoints

27.7 保护端点

Let’s open the CompaniesController and add an additional attribute above the GetCompanies action:‌
让我们打开 CompaniesController 并在 GetCompanies作上方添加一个附加属性:

[HttpGet(Name = "GetCompanies")]
[Authorize] 
public async Task<IActionResult> GetCompanies()

The [Authorize] attribute specifies that the action or controller that it is applied to requires authorization. For it to be available we need an additional namespace:
[Authorize] 属性指定应用该属性的作或控制器需要授权。为了使其可用,我们需要一个额外的命名空间:

using Microsoft.AspNetCore.Authorization;

Now to test this, let’s send a request to get all companies:
现在为了测试这一点,让我们发送一个请求以获取 all companies:

https://localhost:5001/api/companies

alt text

We see the protection works. We get a 401 Unauthorized response, which is expected because an unauthorized user tried to access the protected endpoint. So, what we need is for our users to be authenticated and to have a valid token.
我们看到保护工作正常。我们收到 401 Unauthorized 响应,这是意料之中的,因为未经授权的用户试图访问受保护的终端节点。因此,我们需要的是让我们的用户经过身份验证并拥有有效的令牌。

27.8 Implementing Authentication

27.8 实现身份验证

Let’s begin with the UserForAuthenticationDto record:‌
让我们从 UserForAuthenticationDto 记录开始:

public record UserForAuthenticationDto { [Required(ErrorMessage = "User name is required")] public string? UserName { get; init; } [Required(ErrorMessage = "Password name is required")] public string? Password { get; init; } }

To continue, let’s modify the IAuthenticationService interface:
要继续,让我们修改 IAuthenticationService 接口:

public interface IAuthenticationService { Task<IdentityResult> RegisterUser(UserForRegistrationDto userForRegistration); Task<bool> ValidateUser(UserForAuthenticationDto userForAuth); Task<string> CreateToken(); }

Next, let’s add a private variable in the AuthenticationService class:
接下来,让我们在 AuthenticationService 类中添加一个私有变量:

private readonly UserManager<User> _userManager; private readonly IConfiguration _configuration; private User? _user;

Before we continue to the interface implementation, we have to install System.IdentityModel.Tokens.Jwt library in the Service project. Then, we can implement the required methods:
在继续接口实现之前,我们必须在 Service 项目中安装 System.IdentityModel.Tokens.Jwt 库。然后,我们可以实现所需的方法:

public async Task<bool> ValidateUser(UserForAuthenticationDto userForAuth) { _user = await _userManager.FindByNameAsync(userForAuth.UserName); var result = (_user != null && await _userManager.CheckPasswordAsync(_user, userForAuth.Password)); if (!result) _logger.LogWarn($"{nameof(ValidateUser)}: Authentication failed. Wrong user name or password."); return result; } public async Task<string> CreateToken() { var signingCredentials = GetSigningCredentials(); var claims = await GetClaims(); var tokenOptions = GenerateTokenOptions(signingCredentials, claims); return new JwtSecurityTokenHandler().WriteToken(tokenOptions); } private SigningCredentials GetSigningCredentials() { var key = Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SECRET")); var secret = new SymmetricSecurityKey(key); return new SigningCredentials(secret, SecurityAlgorithms.HmacSha256); } private async Task<List<Claim>> GetClaims() { var claims = new List<Claim> { new Claim(ClaimTypes.Name, _user.UserName) }; var roles = await _userManager.GetRolesAsync(_user); foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role)); } return claims; }private JwtSecurityToken GenerateTokenOptions(SigningCredentials signingCredentials, List<Claim> claims) { var jwtSettings = _configuration.GetSection("JwtSettings"); var tokenOptions = new JwtSecurityToken ( issuer: jwtSettings["validIssuer"], audience: jwtSettings["validAudience"], claims: claims, expires: DateTime.Now.AddMinutes(Convert.ToDouble(jwtSettings["expires"])), signingCredentials: signingCredentials ); return tokenOptions; }

For this to work, we require a few more namespaces:
为此,我们需要更多的命名空间:

using System.IdentityModel.Tokens.Jwt; 
using Microsoft.IdentityModel.Tokens; 
using System.Text;
using System.Security.Claims;

Now we can explain the code.
现在我们可以解释代码。

In the ValidateUser method, we fetch the user from the database and check whether they exist and if the password matches. The UserManager<TUser> class provides the FindByNameAsync method to find the user by user name and the CheckPasswordAsync to verify the user’s password against the hashed password from the database. If the check result is false, we log a message about failed authentication. Lastly, we return the result.
在 ValidateUser 方法中,我们从数据库中获取用户,并检查他们是否存在以及密码是否匹配。UserManager<TUser> 类提供 FindByNameAsync 方法,用于按用户名查找用户,并提供 CheckPasswordAsync 方法,用于根据数据库中的哈希密码验证用户的密码。如果检查结果为 false,我们将记录有关身份验证失败的消息。最后,我们返回结果。

The CreateToken method does exactly that — it creates a token. It does that by collecting information from the private methods and serializing token options with the WriteToken method.
CreateToken 方法正是这样做的 — 它创建一个令牌。它通过从私有方法收集信息并使用 WriteToken 方法序列化令牌选项来实现此目的。

We have three private methods as well. The GetSignInCredentials method returns our secret key as a byte array with the security algorithm. The GetClaims method creates a list of claims with the user name inside and all the roles the user belongs to. The last method, GenerateTokenOptions, creates an object of the JwtSecurityToken type with all of the required options. We can see the expires parameter as one of the token options. We would extract it from the appsettings.json file as well, but we don’t have it there. So, we have to add it:
我们还有三个私有方法。GetSignInCredentials 方法将密钥作为包含安全算法的字节数组返回。GetClaims 方法创建一个声明列表,其中包含用户名以及用户所属的所有角色。最后一个方法 GenerateTokenOptions 创建一个 JwtSecurityToken 类型的对象,其中包含所有必需的选项。我们可以将 expires 参数视为 token 选项之一。我们也会从 appsettings.json 文件中提取它,但我们没有它。所以,我们必须添加它:

"JwtSettings": { "validIssuer": "CodeMazeAPI", "validAudience": "https://localhost:5001", "expires": 5 }

Finally, we have to add a new action in the AuthenticationController:
最后,我们必须在 AuthenticationController 中添加一个新作:

[HttpPost("login")] [ServiceFilter(typeof(ValidationFilterAttribute))] public async Task<IActionResult> Authenticate([FromBody] UserForAuthenticationDto user) { if (!await _service.AuthenticationService.ValidateUser(user)) return Unauthorized(); return Ok(new { Token = await _service .AuthenticationService.CreateToken() }); }

There is nothing special in this controller. If validation fails, we return the 401 Unauthorized response; otherwise, we return our created token:
这个控制器没有什么特别之处。如果验证失败,我们将返回 401 Unauthorized 响应;否则,我们将返回创建的 token:

https://localhost:5001/api/authentication/login

alt text

Excellent. We can see our token generated. Now, let’s send invalid credentials:
非常好。我们可以看到我们的 Token 已生成。现在,让我们发送无效的凭据:

https://localhost:5001/api/authentication/login

alt text

And we get a 401 Unauthorized response.
我们收到了 401 Unauthorized 响应。

Right now if we send a request to the GetCompanies action, we are still going to get the 401 Unauthorized response even though we have successful authentication. That’s because we didn’t provide our token in a request header and our API has nothing to authorize against. To solve that, we are going to create another GET request, and in the Authorization header choose the header type and paste the token from the previous request:
现在,如果我们向 GetCompanies作发送请求,即使我们已成功进行身份验证,我们仍会收到 401 Unauthorized 响应。那是因为我们没有在请求标头中提供令牌,并且我们的 API 没有什么可以授权的。为了解决这个问题,我们将创建另一个 GET 请求,并在 Authorization 标头中选择标头类型并粘贴上一个请求的令牌:

https://localhost:5001/api/companies

alt text

Now, we can send the request again:
现在,我们可以再次发送请求:

https://localhost:5001/api/companies

alt text

Excellent. It works like a charm.
非常好。它就像一个吉祥小饰物。

27.9 Role-Based Authorization

27.9 基于角色的授权

Right now, even though authentication and authorization are working as expected, every single authenticated user can access the GetCompanies action. What if we don’t want that type of behavior? For example, we want to allow only managers to access it. To do that, we have to make one simple change:‌
现在,即使身份验证和授权按预期工作,每个经过身份验证的用户都可以访问 GetCompanies作。如果我们不希望出现这种行为怎么办?例如,我们希望仅允许管理者访问它。为此,我们必须进行一个简单的更改:

[HttpGet(Name = "GetCompanies")] 
[Authorize(Roles = "Manager")] 
public async Task<IActionResult> GetCompanies()

And that is it. To test this, let’s create another user with the Administrator role (the second role from the database):
就是这样。为了测试这一点,让我们创建另一个具有 Administrator 角色的用户(数据库中的第二个角色):

alt text

We get 201. After we send an authentication request for Jane Doe, we are going to get a new token. Let’s use that token to send the request towards the GetCompanies action:
我们得到 201。在我们发送 Jane Doe 的身份验证请求后,我们将获取新令牌。让我们使用该令牌将请求发送到 GetCompanies作:
https://localhost:5001/api/companies

alt text

We get a 403 Forbidden response because this user is not allowed to access the required endpoint. If we log in with John Doe and use his token, we are going to get a successful response for sure. Of course, we don’t have to place an Authorize attribute only on top of the action; we can place it on the controller level as well. For example, we can place just [Authorize] on the controller level to allow only authorized users to access all the actions in that controller; also, we can place the [Authorize (Role=…)] on top of any action in that controller to state that only a user with that specific role has access to that action.
我们收到 403 Forbidden 响应,因为不允许此用户访问所需的终端节点。如果我们使用 John Doe 登录并使用他的令牌,我们肯定会得到成功的响应。当然,我们不必仅在作顶部放置 Authorize 属性;我们也可以将其放在控制器级别。例如,我们可以只将 [Authorize] 放在控制器级别,以仅允许授权用户访问该控制器中的所有作;此外,我们可以将 [Authorize (Role=...)] 放在该控制器中任何作的顶部,以声明只有具有该特定角色的用户才能访问该作。

One more thing. Our token expires after five minutes after the creation point. So, if we try to send another request after that period (we probably have to wait 5 more minutes due to the time difference between servers, which is embedded inside the token – this can be overridden with the ClockSkew property in the TokenValidationParameters object ), we are going to get the 401 Unauthorized status for sure. Feel free to try.
还有一件事。我们的令牌在创建点后 5 分钟后过期。因此,如果我们尝试在该时间段之后发送另一个请求(由于服务器之间的时差,我们可能不得不再等待 5 分钟,这嵌入在令牌中 - 这可以用 TokenValidationParameters 对象中的 ClockSkew 属性覆盖),我们肯定会得到 401 Unauthorized 状态。请随意尝试。

Ultimate ASP.NET Core Web API 26 RATE LIMITING AND THROTTLING

26 RATE LIMITING AND THROTTLING
26 速率限制和限制

Rate Limiting allows us to protect our API against too many requests that can deteriorate our API’s performance. API is going to reject requests that exceed the limit. Throttling queues exceeded requests for possible later processing. The API will eventually reject the request if processing cannot occur after a certain number of attempts.‌
Rate Limiting 使我们能够保护我们的 API 免受过多的请求的影响,这些请求可能会降低 API 的性能。API 将拒绝超过限制的请求。限制队列超出了以后可能处理的请求。如果在一定次数的尝试后无法进行处理,则 API 最终将拒绝该请求。

For example, we can configure our API to create a limitation of 100 requests/hour per client. Or additionally, we can limit a client to the maximum of 1,000 requests/day per IP and 100 requests/hour. We can even limit the number of requests for a specific resource in our API; for example, 50 requests to api/companies.
例如,我们可以将 API 配置为为每个客户端创建 100 个请求/小时的限制。或者,我们可以将客户端限制为每个 IP 每天最多 1000 个请求,每小时最多 100 个请求。我们甚至可以在 API 中限制对特定资源的请求数量;例如,对 API/Companies 的 50 个请求。

To provide information about rate limiting, we use the response headers. They are separated between Allowed requests, which all start with the X- Rate-Limit and Disallowed requests.
为了提供有关速率限制的信息,我们使用响应标头。它们分为 Allowed 请求,这些请求都以 X-Rate-Limit 和 Disallowed 请求开头。

The Allowed requests header contains the following information :
Allowed requests 标头包含以下信息:

• X-Rate-Limit-Limit – rate limit period.
X-Rate-Limit-Limit – 速率限制期。

• X-Rate-Limit-Remaining – number of remaining requests.
X-Rate-Limit-Remaining – 剩余请求数。

• X-Rate-Limit-Reset – date/time information about resetting the request limit.
X-Rate-Limit-Reset – 有关重置请求限制的日期/时间信息。

For the disallowed requests, we use a 429 status code; that stands for too many requests. This header may include the Retry-After response header and should explain details in the response body.
对于不允许的请求,我们使用 429 状态代码;这代表请求太多。此标头可能包括 Retry-After 响应标头,并应在响应正文中说明详细信息。

26.1 Implementing Rate Limiting

26.1 实现速率限制

To start, we have to install the AspNetCoreRateLimit library in the main project:‌
首先,我们必须在主项目中安装 AspNetCoreRateLimit 库:

alt text

Then, we have to add it to the service collection. This library uses a memory cache to store its counters and rules. Therefore, we have to add the MemoryCache to the service collection as well.
然后,我们必须将其添加到服务集合中。此库使用内存缓存来存储其计数器和规则。因此,我们还必须将 MemoryCache 添加到服务集合中。

That said, let’s add the MemoryCache:
也就是说,让我们添加 MemoryCache:

builder.Services.AddMemoryCache();

After that, we are going to create another extension method in the ServiceExtensions class:
之后,我们将在 ServiceExtensions 类中创建另一个扩展方法:

public static void ConfigureRateLimitingOptions(this IServiceCollection services) { var rateLimitRules = new List<RateLimitRule> { new RateLimitRule { Endpoint = "*", Limit = 3, Period = "5m" } }; services.Configure<IpRateLimitOptions>(opt => { opt.GeneralRules = rateLimitRules; }); services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>(); services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>(); services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>(); services.AddSingleton<IProcessingStrategy, AsyncKeyLockProcessingStrategy>(); }

We create a rate limit rules first, for now just one, stating that three requests are allowed in a five-minute period for any endpoint in our API. Then, we configure IpRateLimitOptions to add the created rule. Finally, we have to register rate limit stores, configuration, and processing strategy as a singleton. They serve the purpose of storing rate limit counters and policies as well as adding configuration.
我们首先创建一个速率限制规则,现在只有一个,规定在 5 分钟内允许对 API 中的任何终端节点发出三个请求。然后,我们配置 IpRateLimitOptions 以添加创建的规则。最后,我们必须将 Rate limit 存储、配置和处理策略注册为单例。它们用于存储速率限制计数器和策略以及添加配置。

Now, we have to modify the Program class again:
现在,我们必须再次修改 Program 类:

builder.Services.ConfigureRateLimitingOptions(); 
builder.Services.AddHttpContextAccessor();
builder.Services.AddMemoryCache();

Finally, we have to add it to the request pipeline:
最后,我们必须将其添加到请求管道中:

app.UseIpRateLimiting();
app.UseCors("CorsPolicy");

And that is it. We can test this now:
就是这样。我们现在可以测试一下:
https://localhost:5001/api/companies

alt text

So, we can see that we have two requests remaining and the time to reset the rule. If we send an additional three requests in the five-minute period of time, we are going to get a different response:
因此,我们可以看到我们还剩下两个请求和重置规则的时间。如果我们在 5 分钟内额外发送 3 个请求,我们将得到不同的响应:
https://localhost:5001/api/companies

alt text

The status code is 429 Too Many Requests and we have the Retry-After header.
状态代码为 429 Too Many Requests,我们有 Retry-After 标头。

We can inspect the GET主体 as well:
我们也可以检查身体:

https://localhost:5001/api/companies

alt text

So, our rate limiting works.
因此,我们的速率限制有效。

There are a lot of options that can be configured with Rate Limiting and you can read more about them on the AspNetCoreRateLimit GitHub page.
有很多选项可以使用 Rate Limiting 进行配置,您可以在 AspNetCoreRateLimit GitHub 页面上阅读有关它们的更多信息。

Ultimate ASP.NET Core Web API 25 CACHING

25 CACHING
25 缓存

In this section, we are going to learn about caching resources. Caching can improve the quality and performance of our app a lot, but again, it is something first we need to look at as soon as some bug appears. To cover resource caching, we are going to work with HTTP Cache. Additionally, we are going to talk about cache expiration, validation, and cache-control headers.‌
在本节中,我们将学习缓存资源。缓存可以大大提高我们应用程序的质量和性能,但同样,一旦出现一些错误,我们首先需要查看它。为了涵盖资源缓存,我们将使用 HTTP Cache。此外,我们将讨论缓存过期、验证和缓存控制标头。

25.1 About Caching

25.1 关于缓存

We want to use cache in our app because it can significantly improve performance. Otherwise, it would be useless. The main goal of caching is to eliminate the need to send requests towards the API in many cases and also to send full responses in other cases.‌
我们希望在我们的应用程序中使用 cache,因为它可以显著提高性能。否则,它将毫无用处。缓存的主要目标是在许多情况下无需向 API 发送请求,在其他情况下也无需发送完整响应。

To reduce the number of sent requests, caching uses the expiration mechanism, which helps reduce network round trips. Furthermore, to eliminate the need to send full responses, the cache uses the validation mechanism, which reduces network bandwidth. We can now see why these two are so important when caching resources.
为了减少发送的请求数,缓存使用过期机制,这有助于减少网络往返次数。此外,为了消除发送完整响应的需要,缓存使用验证机制,这减少了网络带宽。我们现在可以看到为什么这两个在缓存资源时如此重要。

The cache is a separate component that accepts requests from the API’s consumer. It also accepts the response from the API and stores that response if they are cacheable. Once the response is stored, if a consumer requests the same response again, the response from the cache should be served.
缓存是一个单独的组件,它接受来自 API 使用者的请求。它还接受来自 API 的响应,并存储该响应(如果它们是可缓存的)。存储响应后,如果使用者再次请求相同的响应,则应提供来自缓存的响应。

But the cache behaves differently depending on what cache type is used.
但是,缓存的行为会有所不同,具体取决于所使用的缓存类型。

25.1.1 Cache Types‌

25.1.1 缓存类型

There are three types of caches: Client Cache, Gateway Cache, and Proxy Cache.
有三种类型的缓存:客户端缓存、网关缓存和代理缓存。

The client cache lives on the client (browser); thus, it is a private cache. It is private because it is related to a single client. So every client consuming our API has a private cache.
客户端缓存位于客户端(浏览器)上;因此,它是一个私有缓存。它是私有的,因为它与单个客户端相关。因此,每个使用我们 API 的客户端都有一个私有缓存。

The gateway cache lives on the server and is a shared cache. This cache is shared because the resources it caches are shared over different clients.
网关缓存位于服务器上,是共享缓存。此缓存是共享的,因为它缓存的资源在不同的客户端上共享。

The proxy cache is also a shared cache, but it doesn’t live on the server nor the client side. It lives on the network.
代理缓存也是共享缓存,但它不存在于服务器或客户端。它存在于网络上。

With the private cache, if five clients request the same response for the first time, every response will be served from the API and not from the cache. But if they request the same response again, that response should come from the cache (if it’s not expired). This is not the case with the shared cache. The response from the first client is going to be cached, and then the other four clients will receive the cached response if they request it.
使用私有缓存时,如果五个客户端首次请求相同的响应,则每个响应都将从 API 而不是缓存中提供。但是,如果他们再次请求相同的响应,则该响应应来自缓存(如果它未过期)。共享缓存不是这种情况。来自第一个客户端的响应将被缓存,然后其他四个客户端将收到缓存的响应(如果它们请求)。

25.1.2 Response Cache Attribute‌

25.1.2 响应缓存属性

So, to cache some resources, we have to know whether or not it’s cacheable. The response header helps us with that. The one that is used most often is Cache-Control: Cache-Control: max-age=180. This states that the response should be cached for 180 seconds. For that, we use the ResponseCache attribute. But of course, this is just a header. If we want to cache something, we need a cache-store. For our example, we are going to use Response caching middleware provided by ASP.NET Core.
所以,要缓存一些资源,我们必须知道它是否是可缓存的。响应标头可以帮助我们解决这个问题。最常用的是 Cache-Control: Cache-Control: max-age=180。这表示响应应缓存 180 秒。为此,我们使用 ResponseCache 属性。但当然,这只是一个标题。如果我们想缓存一些东西,我们需要一个 cache-store。对于我们的示例,我们将使用 ASP.NET Core 提供的响应缓存中间件。

25.2 Adding Cache Headers

25.2 添加缓存 Headers

Before we start, let’s open Postman and modify the settings to support caching:‌
在开始之前,让我们打开 Postman 并修改设置以支持缓存:

alt text

In the General tab under Headers, we are going to turn off the Send no- cache header:
在 Headers 下的 General 选项卡中,我们将关闭 Send no- cache 标头:

alt text

Great. We can move on.
我们可以继续前进。

Let’s assume we want to use the ResponseCache attribute to cache the result from the GetCompany action:
假设我们要使用 ResponseCache 属性来缓存 GetCompany作的结果:

alt text

It is obvious that we can work with different properties in the ResponseCache attribute — but for now, we are going to use Duration only:
很明显,我们可以在 ResponseCache 属性中使用不同的属性 — 但现在,我们只使用 Duration:

[HttpGet("{id}", Name = "CompanyById")] 
[ResponseCache(Duration = 60)] 
public async Task<IActionResult> GetCompany(Guid id)

And that is it. We can inspect our result now:
就是这样。我们现在可以检查我们的结果:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

You can see that the Cache-Control header was created with a public cache and a duration of 60 seconds. But as we said, this is just a header; we need a cache-store to cache the response. So, let’s add one.
您可以看到,Cache-Control 标头是使用公共缓存创建的,持续时间为 60 秒。但正如我们所说,这只是一个标题;我们需要一个 cache-store 来缓存响应。那么,让我们添加一个。

25.3 Adding Cache-Store

25.3 添加 Cache-Store

The first thing we are going to do is add an extension method in the‌ ServiceExtensions class:
我们要做的第一件事是在 ServiceExtensions 类中添加一个扩展方法:

public static void ConfigureResponseCaching(this IServiceCollection services) => services.AddResponseCaching();

We register response caching in the IOC container, and now we have to call this method in the Program class:
我们在 IOC 容器中注册响应缓存,现在我们必须在 Program 类中调用此方法:

builder.Services.ConfigureResponseCaching();

Additionally, we have to add caching to the application middleware right below UseCors() because Microsoft recommends having UseCors before UseResponseCaching, and as we learned in the section 1.8, order is very important for the middleware execution:
此外,我们必须将缓存添加到应用程序中间件的 UseCors() 正下方,因为 Microsoft 建议在 UseResponseCached 之前使用 UseCors,正如我们在第 1.8 节中学到的那样,顺序对于中间件的执行非常重要:

app.UseResponseCaching();
app.UseCors("CorsPolicy");

Now, we can start our application and send the same GetCompany request. It will generate the Cache-Control header. After that, before 60 seconds pass, we are going to send the same request and inspect the headers:
现在,我们可以启动应用程序并发送相同的 GetCompany 请求。它将生成 Cache-Control 标头。之后,在 60 秒过去之前,我们将发送相同的请求并检查标头:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

You can see the additional Age header that indicates the number of seconds the object has been stored in the cache. Basically, it means that we received our second response from the cache-store.
您可以看到额外的 Age 标头,该标头指示对象在缓存中存储的秒数。基本上,这意味着我们收到了来自 cache-store 的第二个响应。

Another way to confirm that is to wait 60 seconds to pass. After that, you can send the request and inspect the console. You will see the SQL query generated. But if you send a second request, you will find no new logs for the SQL query. That’s because we are receiving our response from the cache.
另一种确认方法是等待 60 秒。之后,您可以发送请求并检查控制台。您将看到生成的 SQL 查询。但是,如果您发送第二个请求,则不会找到 SQL 查询的新日志。那是因为我们正在从缓存中接收响应。

Additionally, with every subsequent request within 60 seconds, the Age property will increment. After the expiration period passes, the response will be sent from the API, cached again, and the Age header will not be generated. You will also see new logs in the console.
此外,对于 60 秒内的每个后续请求,Age 属性将递增。过期期限过后,将从 API 发送响应,再次缓存,并且不会生成 Age 标头。您还将在控制台中看到新日志。

Furthermore, we can use cache profiles to apply the same rules to different resources. If you look at the picture that shows all the properties we can use with ResponseCacheAttribute, you can see that there are a lot of properties. Configuring all of them on top of the action or controller could lead to less readable code. Therefore, we can use CacheProfiles to extract that configuration.
此外,我们可以使用缓存配置文件将相同的规则应用于不同的资源。如果你看一下图片,它显示了我们可以与 ResponseCacheAttribute 一起使用的所有属性,你可以看到有很多属性。在 action 或 controller 之上配置所有这些可能会导致代码的可读性降低。因此,我们可以使用 CacheProfiles 来提取该配置。

To do that, we are going to modify the AddControllers method:
为此,我们将修改 AddControllers 方法:

builder.Services.AddControllers(config => { config.RespectBrowserAcceptHeader = true; config.ReturnHttpNotAcceptable = true; config.InputFormatters.Insert(0, GetJsonPatchInputFormatter()); config.CacheProfiles.Add("120SecondsDuration", new CacheProfile { Duration = 120 }); })...

We only set up Duration, but you can add additional properties as well. Now, let’s implement this profile on top of the Companies controller:
我们只设置了 Duration,但您也可以添加其他属性。现在,让我们在 Companies 控制器上实现这个配置文件:

[Route("api/companies")] [ApiController] [ResponseCache(CacheProfileName = "120SecondsDuration")]

We have to mention that this cache rule will apply to all the actions inside the controller except the ones that already have the ResponseCache attribute applied.
我们必须提到,此缓存规则将应用于控制器内的所有作,但已应用 ResponseCache 属性的作除外。

That said, once we send the request to GetCompany, we will still have the maximum age of 60. But once we send the request to GetCompanies:
也就是说,一旦我们将请求发送到 GetCompany,我们的最大年龄仍然是 60 岁。但是,一旦我们将请求发送到 GetCompanies:
https://localhost:5001/api/companies

alt text

There you go. Now, let’s talk some more about the Expiration and Validation models.
现在,让我们更多地讨论 Expiration 和 Validation 模型。

25.4 Expiration Model

25.4 过期模型

The expiration model allows the server to recognize whether or not the response has expired. As long as the response is fresh, it will be served from the cache. To achieve that, the Cache-Control header is used. We have seen this in the previous example.‌
过期模型允许服务器识别响应是否已过期。只要响应是最新的,它就会从缓存中提供。为此,使用了 Cache-Control 标头。我们在前面的示例中已经看到了这一点。

Let’s look at the diagram to see how caching works:
让我们看一下图表,看看缓存是如何工作的:

alt text

So, the client sends a request to get companies. There is no cached version of that response; therefore, the request is forwarded to the API. The API returns the response with the Cache-Control header with a 10- minute expiration period; it is being stored in the cache and forwarded to the client.
因此,客户端发送请求以获取公司。该响应没有缓存版本;因此,请求将转发到 API。API 返回带有 Cache-Control 标头的响应,有效期为 10 分钟;它被存储在缓存中并转发到客户端。

If after two minutes, the same response has been requested:
如果两分钟后,请求了相同的响应:

alt text

We can see that the cached response was served with an additional Age header with a value of 120 seconds or two minutes. If this is a private cache, that is where it stops. That’s because the private cache is stored in the browser and another client will hit the API for the same response. But if this is a shared cache and another client requests the same response after an additional two minutes:
我们可以看到,缓存的响应使用值为 120 秒或 2 分钟的额外 Age 标头提供。如果这是私有缓存,则它是停止的地方。这是因为私有缓存存储在浏览器中,另一个客户端将点击 API 以获得相同的响应。但是,如果这是一个共享缓存,并且另一个客户端在额外的两分钟后请求相同的响应:

alt text

The response is served from the cache with an additional two minutes added to the Age header.
响应从缓存中提供,并向 Age 标头额外添加两分钟。

We saw how the Expiration model works, now let’s inspect the Validation model.
我们了解了 Expiration 模型的工作原理,现在让我们检查 Validation 模型。

25.5 Validation Model

25.5 验证模型

The validation model is used to validate the freshness of the response. So it checks if the response is cached and still usable. Let’s assume we have a shared cached GetCompany response for 30 minutes. If someone updates that company after five minutes, without validation the client would receive the wrong response for another 25 minutes — not the updated one.‌
验证模型用于验证响应的新鲜度。因此,它会检查响应是否已缓存且仍然可用。假设我们有一个共享缓存的 GetCompany 响应 30 分钟。如果有人在 5 分钟后更新了该公司,则客户将在 25 分钟内收到错误的响应,而不是更新后的响应。

To prevent that, we use validators. The HTTP standard advises using Last- Modified and ETag validators in combination if possible.
为了防止这种情况,我们使用验证器。HTTP 标准建议尽可能结合使用 Last- Modified 和 ETag 验证器。

Let’s see how validation works:
让我们看看验证是如何工作的:

alt text

So again, the client sends a request, it is not cached, and so it is forwarded to the API. Our API returns the response that contains the Etag and Last-Modified headers. That response is cached and forwarded to the client.
因此,客户端再次发送请求,该请求未被缓存,因此被转发到 API。我们的 API 返回包含 Etag 和 Last-Modified 标头的响应。该响应将被缓存并转发到客户端。

After two minutes, the client sends the same request:
两分钟后,客户端发送相同的请求:

alt text

So, the same request is sent, but we don’t know if the response is valid. Therefore, the cache forwards that request to the API with the additional headers If-None-Match — which is set to the Etag value — and If- Modified-Since — which is set to the Last-Modified value. If this request checks out against the validators, our API doesn’t have to recreate the same response; it just sends a 304 Not Modified status. After that, the regular response is served from the cache. Of course, if this doesn’t check out, a new response must be generated.
因此,发送了相同的请求,但我们不知道响应是否有效。因此,缓存将该请求转发到 API,其中包含额外的标头 If-None-Match(设置为 Etag 值)和 If- Modified-Since(设置为 Last-Modified 值)。如果此请求针对验证者进行检查,则我们的 API 不必重新创建相同的响应;它只是发送 304 Not Modified 状态。之后,将从缓存中提供常规响应。当然,如果这没有检查出来,则必须生成新的响应。

That brings us to the conclusion that for the shared cache if the response hasn’t been modified, that response has to be generated only once. Let’s see all of these in an example.
这使我们得出结论,对于共享缓存,如果响应尚未修改,则该响应只需生成一次。让我们通过一个示例来了解所有这些。

25.6 Supporting Validation

25.6 支持验证

To support validation, we are going to use the Marvin.Cache.Headers library. This library supports HTTP cache headers like Cache-Control, Expires, Etag, and Last-Modified and also implements validation and expiration models.‌
为了支持验证,我们将使用 Marvin.Cache.Headers 库。此库支持 Cache-Control、Expires、Etag 和 Last-Modified 等 HTTP 缓存标头,并且还实施验证和过期模型。

So, let’s install the Marvin.Cache.Headers library in the Presentation project, which will enable the reference for the main project as well. We are going to need it in both projects.
因此,让我们在 Presentation 项目中安装 Marvin.Cache.Headers 库,这也将启用主项目的引用。我们将在这两个项目中都需要它。

Now, let’s modify the ServiceExtensions class:
现在,让我们修改 ServiceExtensions 类:

public static void ConfigureHttpCacheHeaders(this IServiceCollection services) => services.AddHttpCacheHeaders();

We are going to add additional configuration later.
我们稍后将添加其他配置。

Then, let’s modify the Program class:
然后,让我们修改 Program 类:

builder.Services.ConfigureResponseCaching(); 
builder.Services.ConfigureHttpCacheHeaders();

And finally, let’s add HttpCacheHeaders to the request pipeline:
最后,让我们将 HttpCacheHeaders 添加到请求管道中:

app.UseResponseCaching(); 
app.UseHttpCacheHeaders();

To test this, we have to remove or comment out ResponseCache attributes in the CompaniesController. The installed library will provide that for us. Now, let’s send the GetCompany request:
为了测试这一点,我们必须删除或注释掉 CompaniesController 中的 ResponseCache 属性。已安装的库将为我们提供。现在,让我们发送 GetCompany 请求:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

We can see that we have all the required headers generated. The default expiration is set to 60 seconds and if we send this request one more time, we are going to get an additional Age header.
我们可以看到我们已经生成了所有必需的标头。默认过期时间设置为 60 秒,如果我们再次发送此请求,我们将获得一个额外的 Age 标头。

25.6.1 Configuration‌

25.6.1 配置

We can globally configure our expiration and validation headers. To do that, let’s modify the ConfigureHttpCacheHeaders method:
我们可以全局配置我们的 expiration 和 validation 标头。为此,让我们修改 ConfigureHttpCacheHeaders 方法:

public static void ConfigureHttpCacheHeaders(this IServiceCollection services) => services.AddHttpCacheHeaders(
(expirationOpt) => { expirationOpt.MaxAge = 65; expirationOpt.CacheLocation = CacheLocation.Private; }, (validationOpt) => { validationOpt.MustRevalidate = true; });    

After that, we are going to send the same request for a single company:
之后,我们将为单个公司发送相同的请求:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

You can see that the changes are implemented. Now, this is a private cache with an age of 65 seconds. Because it is a private cache, our API won’t cache it. You can check the console again and see the SQL logs for each request you send.
您可以看到更改已实施。现在,这是一个存在时间为 65 秒的私有缓存。因为它是私有缓存,所以我们的 API 不会缓存它。您可以再次检查控制台并查看您发送的每个请求的 SQL 日志。

Other than global configuration, we can apply it on the resource level (on action or controller). The overriding rules are the same. Configuration on the action level will override the configuration on the controller or global level. Also, the configuration on the controller level will override the global level configuration.
除了全局配置之外,我们可以在资源级别(在 action 或 controller 上)应用它。覆盖规则是相同的。作级别的配置将覆盖控制器或全局级别的配置。此外,控制器级别的配置将覆盖全局级别的配置。

To apply a resource level configuration, we have to use the HttpCacheExpiration and HttpCacheValidation attributes:
要应用资源级别配置,我们必须使用 HttpCacheExpiration 和 HttpCacheValidation 属性:

[HttpGet("{id}", Name = "CompanyById")] [HttpCacheExpiration(CacheLocation = CacheLocation.Public, MaxAge = 60)] [HttpCacheValidation(MustRevalidate = false)] public async Task<IActionResult> GetCompany(Guid id)

Once we send the GetCompanies request, we are going to see global values:
发送 GetCompanies 请求后,我们将看到全局值:

alt text

But if we send the GetCompany request:
但是,如果我们发送 GetCompany 请求:

alt text

You can see that it is public and you can send the same request again to see the Age header for the cached response.
您可以看到它是公有的,并且可以再次发送相同的请求以查看缓存响应的 Age 标头。

25.7 Using ETag and Validation

25.7 使用 ETag 和验证

First, we have to mention that the ResponseCaching library doesn’t correctly implement the validation model. Also, using the authorization header is a problem. We are going to show you alternatives later. But for now, we can simulate how validation with Etag should work.‌
首先,我们必须提到 ResponseCaching 库没有正确实现验证模型。此外,使用 authorization 标头也是一个问题。我们稍后将向您展示替代方案。但现在,我们可以模拟使用 Etag 进行验证应该如何工作。

So, let’s restart our app to have a fresh application, and send a GetCompany request one more time. In a header, we are going to get our ETag. Let’s copy the Etag’s value and use another GetCompany request:
因此,让我们重新启动应用程序以获得新的应用程序,并再次发送 GetCompany 请求。在 header 中,我们将获取我们的 ETag。让我们复制 Etag 的值并使用另一个 GetCompany 请求:
https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

We send the If-None-Match tag with the value of our Etag. And we can see as a result we get 304 Not Modified.
我们发送带有 Etag 值的 If-None-Match 标签。我们可以看到,结果是 304 Not Modified。

But this is not a valid situation. As we said, the client should send a valid request and it is up to the Cache to add an If-None-Match tag. In our example, which we sent from Postman, we simulated that. Then, it is up to the server to return a 304 message to the cache and then the cache should return the same response.
但这不是一个有效的情况。正如我们所说,客户端应该发送一个有效的请求,并且由 Cache 添加 If-None-Match 标签。在我们从 Postman 发送的示例中,我们对此进行了模拟。然后,由服务器将 304 消息返回到缓存,然后缓存应返回相同的响应。

But anyhow, we have managed to show you how validation works. If we update that company:
但无论如何,我们已经设法向您展示了验证的工作原理。如果我们更新该公司:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

And then send the same request with the same If-None-Match value:
然后发送具有相同 If-None-Match 值的相同请求:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

You can see that we get 200 OK and if we inspect Headers, we will find that ETag is different because the resource changed:
你可以看到我们得到 200 OK,如果我们检查 Headers,我们会发现 ETag 是不同的,因为资源发生了变化:

alt text

So, we saw how validation works and also concluded that the ResponseCaching library is not that good for validation — it is much better for just expiration.
因此,我们看到了验证的工作原理,并得出结论,ResponseCaching 库对于验证来说不是那么好 — 它更适合于过期。

But then, what are the alternatives? There are a lot of alternatives, such as:
但是,还有哪些选择呢?有很多选择,例如:

• Varnish - https://varnish-cache.org/

• Apache Traffic Server - https://trafficserver.apache.org/

• Squid - http://www.squid-cache.org/

They implement caching correctly. And if you want to have expiration and validation, you should combine them with the Marvin library and you are good to go. But those servers are not that trivial to implement.
它们正确地实现了缓存。如果你想拥有过期和验证,你应该将它们与 Marvin 库结合起来,一切顺利。但这些服务器并不是那么容易实现。

There is another option: CDN (Content Delivery Network). CDN uses HTTP caching and is used by various sites on the internet. The good thing with CDN is we don’t need to set up a cache server by ourselves, but unfortunately, we have to pay for it. The previous cache servers we presented are free to use. So, it’s up to you to decide what suits you best.
还有另一个选项:CDN(内容交付网络)。CDN 使用 HTTP 缓存,并被 Internet 上的各种站点使用。CDN 的好处是我们不需要自己设置缓存服务器,但不幸的是,我们必须为此付费。我们之前介绍的缓存服务器可以免费使用。因此,由您决定什么最适合您。