{"id":1164,"date":"2025-05-27T14:48:25","date_gmt":"2025-05-27T06:48:25","guid":{"rendered":"https:\/\/www.hyy.net\/?p=1164"},"modified":"2025-05-27T14:48:25","modified_gmt":"2025-05-27T06:48:25","slug":"ultimate-asp-net-core-web-api-33-bonus-2-introduction-to-cqrs-and-mediatr-with-asp-net-core-web-api","status":"publish","type":"post","link":"https:\/\/diji.net\/?p=1164","title":{"rendered":"Ultimate ASP.NET Core Web API 33 BONUS 2 &#8211; INTRODUCTION TO CQRS AND MEDIATR WITH ASP.NET CORE WEB API"},"content":{"rendered":"<p>33 BONUS 2 - INTRODUCTION TO CQRS AND MEDIATR WITH ASP.NET CORE WEB API<br \/>\n33 \u5956\u52b1 2 - \u4f7f\u7528 ASP.NET \u6838\u5fc3 WEB API \u7684 CQRS \u548c MEDIATR \u7b80\u4ecb<\/p>\n<p>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.\u200c<br \/>\n\u5728\u672c\u7ae0\u4e2d\uff0c\u6211\u4eec\u5c06\u4ecb\u7ecd CQRS \u6a21\u5f0f\u4ee5\u53ca .NET \u5e93 MediatR \u5982\u4f55\u5e2e\u52a9\u6211\u4eec\u6784\u5efa\u5177\u6709\u6b64\u4f53\u7cfb\u7ed3\u6784\u7684\u8f6f\u4ef6\u3002<\/p>\n<p>In the Source Code folder, you will find the folder for this chapter with two folders inside \u2013 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\u2019t need or want to replace - like the service layer.<br \/>\n\u5728 Source Code \u6587\u4ef6\u5939\u4e2d\uff0c\u60a8\u5c06\u627e\u5230\u672c\u7ae0\u7684\u6587\u4ef6\u5939\uff0c\u5176\u4e2d\u5305\u542b\u4e24\u4e2a\u6587\u4ef6\u5939 \u2013 start \u548c end\u3002\u5728 start \u6587\u4ef6\u5939\u4e2d\uff0c\u60a8\u5c06\u627e\u5230\u6b64\u90e8\u5206\u7684\u51c6\u5907\u5de5\u7a0b\u3002\u6211\u4eec\u5c06\u4f7f\u7528\u5b83\u6765\u89e3\u91ca CQRS \u548c MediatR \u7684\u5b9e\u73b0\u3002\u6211\u4eec\u4f7f\u7528\u4e86\u524d\u51e0\u7ae0\u4e2d\u7684\u73b0\u6709\u9879\u76ee\uff0c\u5e76\u5220\u9664\u4e86\u6211\u4eec\u4e0d\u9700\u8981\u6216\u4e0d\u60f3\u66ff\u6362\u7684\u4e1c\u897f - \u6bd4\u5982\u670d\u52a1\u5c42\u3002<\/p>\n<p>In the end folder, you will find a finished project for this chapter.<br \/>\n\u5728 end \u6587\u4ef6\u5939\u4e2d\uff0c\u60a8\u5c06\u627e\u5230\u672c\u7ae0\u7684\u5df2\u5b8c\u6210\u9879\u76ee\u3002<\/p>\n<h2>33.1 About CQRS and Mediator Pattern<\/h2>\n<p>33.1 \u5173\u4e8e CQRS \u548c\u4e2d\u4ecb\u6a21\u5f0f<\/p>\n<p>The MediatR library was built to facilitate two primary software architecture patterns: CQRS and the Mediator pattern. Whilst similar, let\u2019s spend a moment understanding the principles behind each pattern.\u200c<br \/>\nMediatR \u5e93\u7684\u6784\u5efa\u662f\u4e3a\u4e86\u4fc3\u8fdb\u4e24\u79cd\u4e3b\u8981\u7684\u8f6f\u4ef6\u67b6\u6784\u6a21\u5f0f\uff1aCQRS \u548c Mediaator \u6a21\u5f0f\u3002\u867d\u7136\u76f8\u4f3c\uff0c\u4f46\u8ba9\u6211\u4eec\u82b1\u70b9\u65f6\u95f4\u4e86\u89e3\u6bcf\u79cd\u6a21\u5f0f\u80cc\u540e\u7684\u539f\u5219\u3002<\/p>\n<h3>33.1.1 CQRS<\/h3>\n<p>CQRS stands for \u201cCommand Query Responsibility Segregation\u201d. As the acronym suggests, it\u2019s all about splitting the responsibility of commands (saves) and queries (reads) into different models.<br \/>\nCQRS \u4ee3\u8868 \u201cCommand Query Responsibility Segregation\u201d\u3002\u6b63\u5982\u9996\u5b57\u6bcd\u7f29\u7565\u8bcd\u6240\u6697\u793a\u7684\u90a3\u6837\uff0c\u8fd9\u4e00\u5207\u90fd\u662f\u4e3a\u4e86\u5c06\u547d\u4ee4 \uff08saves\uff09 \u548c\u67e5\u8be2 \uff08reads\uff09 \u7684\u8d23\u4efb\u62c6\u5206\u5230\u4e0d\u540c\u7684\u6a21\u578b\u4e2d\u3002<\/p>\n<p>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 \u201cR\u201d), and another for the commands (aka \u201cCUD\u201d).<br \/>\n\u5982\u679c\u6211\u4eec\u8003\u8651\u5e38\u7528\u7684 CRUD \u6a21\u5f0f\uff08\u521b\u5efa-\u8bfb\u53d6-\u66f4\u65b0-\u5220\u9664\uff09\uff0c\u6211\u4eec\u901a\u5e38\u4f1a\u8ba9\u7528\u6237\u754c\u9762\u4e0e\u8d1f\u8d23\u6240\u6709\u56db\u4e2a\u4f5c\u7684\u6570\u636e\u5b58\u50a8\u8fdb\u884c\u4ea4\u4e92\u3002\u76f8\u53cd\uff0cCQRS \u4f1a\u8ba9\u6211\u4eec\u5c06\u8fd9\u4e9b\u4f5c\u62c6\u5206\u4e3a\u4e24\u4e2a\u6a21\u578b\uff0c\u4e00\u4e2a\u7528\u4e8e\u67e5\u8be2\uff08\u53c8\u540d\u201cR\u201d\uff09\uff0c\u53e6\u4e00\u4e2a\u7528\u4e8e\u547d\u4ee4\uff08\u53c8\u540d\u201cCUD\u201d\uff09\u3002<\/p>\n<p>The following image illustrates how this works:<br \/>\n\u4e0b\u56fe\u8bf4\u660e\u4e86\u5176\u5de5\u4f5c\u539f\u7406\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3301.jpg\" alt=\"alt text\" \/><\/p>\n<p>The Application simply separates the query and command models.<br \/>\nApplication \u53ea\u662f\u5c06 query \u548c command \u6a21\u578b\u5206\u5f00\u3002<\/p>\n<p>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\u2019ll 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\u2019t go into that decision path here.<br \/>\nCQRS \u6a21\u5f0f\u5bf9\u8fd9\u79cd\u5206\u79bb\u7684\u53d1\u751f\u65b9\u5f0f\u6ca1\u6709\u6b63\u5f0f\u8981\u6c42\u3002\u5b83\u53ef\u4ee5\u50cf\u540c\u4e00\u5e94\u7528\u7a0b\u5e8f\u4e2d\u7684\u5355\u72ec\u7c7b\u4e00\u6837\u7b80\u5355\uff08\u6211\u4eec\u7a0d\u540e\u5c06\u5728 MediatR \u4e2d\u770b\u5230\uff09\uff0c\u4e00\u76f4\u5230\u4e0d\u540c\u670d\u52a1\u5668\u4e0a\u7684\u5355\u72ec\u7269\u7406\u5e94\u7528\u7a0b\u5e8f\u3002\u8be5\u51b3\u7b56\u5c06\u57fa\u4e8e\u6269\u5c55\u9700\u6c42\u548c\u57fa\u7840\u8bbe\u65bd\u7b49\u56e0\u7d20\uff0c\u56e0\u6b64\u6211\u4eec\u4e0d\u4f1a\u5728\u8fd9\u91cc\u8ba8\u8bba\u8be5\u51b3\u7b56\u8def\u5f84\u3002<\/p>\n<p>The key point being is that to create a CQRS system, we just need to split the reads from the writes.<br \/>\n\u5173\u952e\u662f\uff0c\u8981\u521b\u5efa CQRS \u7cfb\u7edf\uff0c\u6211\u4eec\u53ea\u9700\u8981\u5c06\u8bfb\u53d6\u4e0e\u5199\u5165\u5206\u5f00\u3002<\/p>\n<p>What problem is this trying to solve?<br \/>\n\u8fd9\u8bd5\u56fe\u89e3\u51b3\u4ec0\u4e48\u95ee\u9898\uff1f<\/p>\n<p>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 \u201cwrite system\u201d 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 \u201cdocument databases\u201d are used for these patterns).<br \/>\n\u55ef\uff0c\u4e00\u4e2a\u5e38\u89c1\u7684\u539f\u56e0\u662f\uff0c\u5f53\u6211\u4eec\u8bbe\u8ba1\u4e00\u4e2a\u7cfb\u7edf\u65f6\uff0c\u6211\u4eec\u4ece\u6570\u636e\u5b58\u50a8\u5f00\u59cb\u3002\u6211\u4eec\u6267\u884c\u6570\u636e\u5e93\u89c4\u8303\u5316\uff0c\u6dfb\u52a0\u4e3b\u952e\u548c\u5916\u952e\u4ee5\u5f3a\u5236\u5f15\u7528\u5b8c\u6574\u6027\uff0c\u6dfb\u52a0\u7d22\u5f15\uff0c\u5e76\u4e14\u901a\u5e38\u786e\u4fdd\u201c\u5199\u5165\u7cfb\u7edf\u201d\u5f97\u5230\u4f18\u5316\u3002\u8fd9\u662f\u5173\u7cfb\u6570\u636e\u5e93\uff08\u5982 SQL Server \u6216 MySQL\uff09\u7684\u5e38\u89c1\u8bbe\u7f6e\u3002\u5176\u4ed6\u65f6\u5019\uff0c\u6211\u4eec\u9996\u5148\u8003\u8651\u8bfb\u53d6\u7528\u4f8b\uff0c\u7136\u540e\u5c1d\u8bd5\u5c06\u5176\u6dfb\u52a0\u5230\u6570\u636e\u5e93\u4e2d\uff0c\u800c\u4e0d\u5fc5\u62c5\u5fc3\u91cd\u590d\u6216\u5176\u4ed6\u5173\u7cfb\u6570\u636e\u5e93\u95ee\u9898\uff08\u901a\u5e38\u201c\u6587\u6863\u6570\u636e\u5e93\u201d\u7528\u4e8e\u8fd9\u4e9b\u6a21\u5f0f\uff09\u3002<\/p>\n<p>Neither approach is wrong. But the issue is that it\u2019s a constant balancing act between reads and writes, and eventually one side will \u201cwin out\u201d. All further development means both sides need to be analyzed, and often one is compromised.<br \/>\n\u8fd9\u4e24\u79cd\u65b9\u6cd5\u90fd\u6ca1\u6709\u9519\u3002\u4f46\u95ee\u9898\u662f\uff0c\u8fd9\u662f\u8bfb\u53d6\u548c\u5199\u5165\u4e4b\u95f4\u7684\u6301\u7eed\u5e73\u8861\u884c\u4e3a\uff0c\u6700\u7ec8\u4e00\u65b9\u5c06 \u201c\u80dc\u51fa\u201d\u3002\u6240\u6709\u8fdb\u4e00\u6b65\u7684\u53d1\u5c55\u90fd\u610f\u5473\u7740\u53cc\u65b9\u90fd\u9700\u8981\u5206\u6790\uff0c\u800c\u4e14\u5f80\u5f80\u6709\u4e00\u4e2a\u4f1a\u53d7\u5230\u635f\u5bb3\u3002<\/p>\n<p>CQRS allows us to \u201cbreak free\u201d 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.<br \/>\nCQRS \u4f7f\u6211\u4eec\u80fd\u591f\u201c\u6446\u8131\u201d\u8fd9\u4e9b\u8003\u8651\uff0c\u5e76\u4e3a\u6bcf\u4e2a\u7cfb\u7edf\u63d0\u4f9b\u5e94\u6709\u7684\u5e73\u7b49\u8bbe\u8ba1\u548c\u8003\u8651\uff0c\u800c\u65e0\u9700\u62c5\u5fc3\u5176\u4ed6\u7cfb\u7edf\u7684\u5f71\u54cd\u3002\u8fd9\u5bf9\u6027\u80fd\u548c\u654f\u6377\u6027\u90fd\u6709\u5de8\u5927\u7684\u597d\u5904\uff0c\u5c24\u5176\u662f\u5728\u4e0d\u540c\u7684\u56e2\u961f\u5728\u8fd9\u4e9b\u7cfb\u7edf\u4e0a\u5de5\u4f5c\u65f6\u3002<\/p>\n<h3>33.1.2 Advantages and Disadvantages of CQRS\u200c<\/h3>\n<p>33.1.2 CQRS \u7684\u4f18\u70b9\u548c\u7f3a\u70b9<\/p>\n<p>The benefits of CQRS are:<br \/>\nCQRS \u7684\u4f18\u70b9\u662f\uff1a<\/p>\n<p>\u2022 Single Responsibility \u2013 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.<br \/>\n\u5355\u4e00\u804c\u8d23 \u2013 \u547d\u4ee4\u548c\u67e5\u8be2\u53ea\u6709\u4e00\u4e2a\u4f5c\u4e1a\u3002\u8981\u4e48\u66f4\u6539\u5e94\u7528\u7a0b\u5e8f\u7684\u72b6\u6001\uff0c\u8981\u4e48\u68c0\u7d22\u5b83\u3002\u56e0\u6b64\uff0c\u5b83\u4eec\u5f88\u5bb9\u6613\u63a8\u7406\u548c\u7406\u89e3\u3002<\/p>\n<p>\u2022 Decoupling \u2013 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.<br \/>\nDecoupling \u2013 Command \u6216 Query \u4e0e\u5176\u5904\u7406\u7a0b\u5e8f\u5b8c\u5168\u89e3\u8026\uff0c\u5728\u5904\u7406\u7a0b\u5e8f\u7aef\u4e3a\u60a8\u63d0\u4f9b\u4e86\u5f88\u5927\u7684\u7075\u6d3b\u6027\uff0c\u4ee5\u4fbf\u4ee5\u60a8\u8ba4\u4e3a\u5408\u9002\u7684\u6700\u4f73\u65b9\u5f0f\u5b9e\u65bd\u5b83\u3002<\/p>\n<p>\u2022 Scalability \u2013 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.<br \/>\n\u53ef\u4f38\u7f29\u6027 \u2013 CQRS \u6a21\u5f0f\u5728\u7ec4\u7ec7\u6570\u636e\u5b58\u50a8\u7684\u65b9\u5f0f\u65b9\u9762\u975e\u5e38\u7075\u6d3b\uff0c\u4e3a\u60a8\u63d0\u4f9b\u4e86\u51fa\u8272\u7684\u53ef\u4f38\u7f29\u6027\u9009\u9879\u3002\u60a8\u53ef\u4ee5\u5bf9 Commands \u548c Queries \u4f7f\u7528\u4e00\u4e2a\u6570\u636e\u5e93\u3002\u60a8\u53ef\u4ee5\u4f7f\u7528\u5355\u72ec\u7684\u8bfb\/\u5199\u6570\u636e\u5e93\u6765\u63d0\u9ad8\u6027\u80fd\uff0c\u5e76\u5728\u6570\u636e\u5e93\u4e4b\u95f4\u8fdb\u884c\u6d88\u606f\u4f20\u9012\u6216\u590d\u5236\u4ee5\u8fdb\u884c\u540c\u6b65\u3002<\/p>\n<p>\u2022 Testability \u2013 It is very easy to test Command or Query handlers since they will be very simple by design, and perform only a single job.<br \/>\n\u53ef\u6d4b\u8bd5\u6027 \u2013 \u6d4b\u8bd5 Command \u6216 Query \u5904\u7406\u7a0b\u5e8f\u975e\u5e38\u5bb9\u6613\uff0c\u56e0\u4e3a\u5b83\u4eec\u7684\u8bbe\u8ba1\u975e\u5e38\u7b80\u5355\uff0c\u5e76\u4e14\u53ea\u6267\u884c\u4e00\u9879\u5de5\u4f5c\u3002<\/p>\n<p>Of course, it can\u2019t all be good. Here are some of the disadvantages of CQRS:<br \/>\n\u5f53\u7136\uff0c\u4e0d\u53ef\u80fd\u90fd\u662f\u597d\u7684\u3002\u4ee5\u4e0b\u662f CQRS \u7684\u4e00\u4e9b\u7f3a\u70b9\uff1a<\/p>\n<p>\u2022 Complexity \u2013 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.<br \/>\n\u590d\u6742\u6027 \u2013 CQRS \u662f\u4e00\u79cd\u9ad8\u7ea7\u8bbe\u8ba1\u6a21\u5f0f\uff0c\u60a8\u9700\u8981\u82b1\u65f6\u95f4\u624d\u80fd\u5b8c\u5168\u7406\u89e3\u5b83\u3002\u5b83\u5f15\u5165\u4e86\u8bb8\u591a\u590d\u6742\u6027\uff0c\u8fd9\u5c06\u5728\u60a8\u7684\u9879\u76ee\u4e2d\u4ea7\u751f\u6469\u64e6\u548c\u6f5c\u5728\u95ee\u9898\u3002\u5728\u51b3\u5b9a\u5728\u60a8\u7684\u9879\u76ee\u4e2d\u4f7f\u7528\u5b83\u4e4b\u524d\uff0c\u8bf7\u52a1\u5fc5\u8003\u8651\u6240\u6709\u56e0\u7d20\u3002<\/p>\n<p>\u2022 Learning Curve \u2013 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.<br \/>\n\u5b66\u4e60\u66f2\u7ebf \u2013 \u5c3d\u7ba1\u5b83\u770b\u8d77\u6765\u662f\u4e00\u4e2a\u7b80\u5355\u7684\u8bbe\u8ba1\u6a21\u5f0f\uff0c\u4f46 CQRS \u4ecd\u7136\u5b58\u5728\u5b66\u4e60\u66f2\u7ebf\u3002\u5927\u591a\u6570\u5f00\u53d1\u4eba\u5458\u90fd\u4e60\u60ef\u4e86\u7f16\u5199\u4ee3\u7801\u7684\u8fc7\u7a0b\uff08\u547d\u4ee4\u5f0f\uff09\u98ce\u683c\uff0c\u800c CQRS \u4e0e\u6b64\u622a\u7136\u4e0d\u540c\u3002<\/p>\n<p>\u2022 Hard to Debug \u2013 Since Commands and Queries are decoupled from their handler, there isn\u2019t a natural imperative flow of the application. This makes it harder to debug than traditional applications.<br \/>\n\u96be\u4ee5\u8c03\u8bd5 \u2013 \u7531\u4e8e\u547d\u4ee4\u548c\u67e5\u8be2\u4e0e\u5176\u5904\u7406\u7a0b\u5e8f\u5206\u79bb\uff0c\u56e0\u6b64\u5e94\u7528\u7a0b\u5e8f\u6ca1\u6709\u81ea\u7136\u7684\u547d\u4ee4\u5f0f\u6d41\u7a0b\u3002\u8fd9\u4f7f\u5f97\u5b83\u6bd4\u4f20\u7edf\u5e94\u7528\u7a0b\u5e8f\u66f4\u96be\u8c03\u8bd5\u3002<\/p>\n<h3>33.1.3 Mediator Pattern\u200c<\/h3>\n<p>33.1.3 \u8c03\u89e3\u5668\u6a21\u5f0f<\/p>\n<p>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 \u201cmediator\u201d, who is in charge of sending those interactions to the other party:<br \/>\n\u4e2d\u4ecb\u5668\u6a21\u5f0f\u53ea\u662f\u5b9a\u4e49\u4e00\u4e2a\u5bf9\u8c61\uff0c\u8be5\u5bf9\u8c61\u5c01\u88c5\u4e86\u5bf9\u8c61\u4e4b\u95f4\u7684\u4ea4\u4e92\u65b9\u5f0f\u3002\u5b83\u4eec\u4e0d\u662f\u8ba9\u4e24\u4e2a\u6216\u591a\u4e2a\u5bf9\u8c61\u5f7c\u6b64\u76f4\u63a5\u4f9d\u8d56\uff0c\u800c\u662f\u4e0e \u201c\u4e2d\u4ecb\u201d \u4ea4\u4e92\uff0c\u8be5 \u201c\u4e2d\u4ecb\u201d \u8d1f\u8d23\u5c06\u8fd9\u4e9b\u4ea4\u4e92\u53d1\u9001\u7ed9\u53e6\u4e00\u65b9\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3302.jpg\" alt=\"alt text\" \/><\/p>\n<p>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.<br \/>\n\u5728\u6b64\u56fe\u4e2d\uff0cSomeService \u5411 Mediator \u53d1\u9001\u4e00\u6761\u6d88\u606f\uff0c\u7136\u540e Mediator \u8c03\u7528\u591a\u4e2a\u670d\u52a1\u6765\u5904\u7406\u8be5\u6d88\u606f\u3002\u4efb\u4f55\u84dd\u8272\u7ec4\u4ef6\u4e4b\u95f4\u90fd\u6ca1\u6709\u76f4\u63a5\u4f9d\u8d56\u5173\u7cfb\u3002<\/p>\n<p>The reason the Mediator pattern is useful is the same reason patterns like Inversion of Control are useful. It enables \u201cloose coupling\u201d, 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.<br \/>\n\u4e2d\u4ecb\u8005\u6a21\u5f0f\u6709\u7528\u7684\u539f\u56e0\u4e0e\u50cf Inversion of Control \u8fd9\u6837\u7684\u6a21\u5f0f\u6709\u7528\u7684\u539f\u56e0\u76f8\u540c\u3002\u5b83\u652f\u6301\u201c\u677e\u6563\u8026\u5408\u201d\uff0c\u56e0\u4e3a\u4f9d\u8d56\u5173\u7cfb\u56fe\u88ab\u6700\u5c0f\u5316\uff0c\u56e0\u6b64\u4ee3\u7801\u66f4\u7b80\u5355\uff0c\u66f4\u5bb9\u6613\u6d4b\u8bd5\u3002\u6362\u53e5\u8bdd\u8bf4\uff0c\u7ec4\u4ef6\u7684\u8003\u8651\u56e0\u7d20\u8d8a\u5c11\uff0c\u5b83\u5c31\u8d8a\u5bb9\u6613\u5f00\u53d1\u548c\u53d1\u5c55\u3002<\/p>\n<p>We saw in the previous image how the services have no direct dependency, and the producer of the messages doesn\u2019t know who or how many things are going to handle it. This is very similar to how a message broker works in the \u201cpublish\/subscribe\u201d pattern. If we wanted to add another handler we could, and the producer wouldn\u2019t have to be modified.<br \/>\n\u5728\u4e0a\u56fe\u4e2d\uff0c\u6211\u4eec\u770b\u5230\u670d\u52a1\u6ca1\u6709\u76f4\u63a5\u4f9d\u8d56\u5173\u7cfb\uff0c\u6d88\u606f\u7684\u751f\u6210\u8005\u4e0d\u77e5\u9053\u8c01\u6216\u591a\u5c11\u4e8b\u60c5\u5c06\u5904\u7406\u5b83\u3002\u8fd9\u4e0e\u6d88\u606f\u4ee3\u7406\u5728 \u201cpublish\/subscribe\u201d \u6a21\u5f0f\u4e2d\u7684\u5de5\u4f5c\u65b9\u5f0f\u975e\u5e38\u76f8\u4f3c\u3002\u5982\u679c\u6211\u4eec\u60f3\u6dfb\u52a0\u53e6\u4e00\u4e2a\u5904\u7406\u7a0b\u5e8f\uff0c\u6211\u4eec\u53ef\u4ee5\u8fd9\u6837\u505a\uff0c\u5e76\u4e14\u4e0d\u5fc5\u4fee\u6539 producer\u3002<\/p>\n<p>Now that we\u2019ve been over some theory, let\u2019s talk about how MediatR makes all these things possible.<br \/>\n\u73b0\u5728\u6211\u4eec\u5df2\u7ecf\u4e86\u89e3\u4e86\u4e00\u4e9b\u7406\u8bba\uff0c\u8ba9\u6211\u4eec\u8c08\u8c08 MediatR \u5982\u4f55\u4f7f\u6240\u6709\u8fd9\u4e9b\u4e8b\u60c5\u6210\u4e3a\u53ef\u80fd\u3002<\/p>\n<h2>33.2 How MediatR facilitates CQRS and Mediator Patterns<\/h2>\n<p>33.2 MediatR \u5982\u4f55\u4fc3\u8fdb CQRS \u548c\u4e2d\u4ecb\u6a21\u5f0f<\/p>\n<p>You can think of MediatR as an \u201cin-process\u201d Mediator implementation, that helps us build CQRS systems. All communication between the user interface and the data store happens via MediatR.\u200c<br \/>\n\u60a8\u53ef\u4ee5\u5c06 MediatR \u89c6\u4e3a\u201c\u8fdb\u7a0b\u5185\u201d\u4e2d\u4ecb\u5668\u5b9e\u73b0\uff0c\u5b83\u5e2e\u52a9\u6211\u4eec\u6784\u5efa CQRS \u7cfb\u7edf\u3002\u7528\u6237\u754c\u9762\u548c\u6570\u636e\u5b58\u50a8\u4e4b\u95f4\u7684\u6240\u6709\u901a\u4fe1\u90fd\u901a\u8fc7 MediatR \u8fdb\u884c\u3002<\/p>\n<p>The term \u201cin process\u201d is an important limitation here. Since it\u2019s a .NET library that manages interactions within classes on the same process, it\u2019s 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.<br \/>\n\u672f\u8bed \u201cin process\u201d \u5728\u8fd9\u91cc\u662f\u4e00\u4e2a\u91cd\u8981\u7684\u9650\u5236\u3002\u7531\u4e8e\u5b83\u662f\u4e00\u4e2a .NET \u5e93\uff0c\u7528\u4e8e\u7ba1\u7406\u540c\u4e00\u8fdb\u7a0b\u4e0a\u7c7b\u5185\u7684\u4ea4\u4e92\uff0c\u56e0\u6b64\u5982\u679c\u6211\u4eec\u60f3\u8de8\u4e24\u4e2a\u7cfb\u7edf\u5206\u79bb\u547d\u4ee4\u548c\u67e5\u8be2\uff0c\u5219\u5b83\u4e0d\u662f\u4e00\u4e2a\u5408\u9002\u7684\u5e93\u3002\u66f4\u597d\u7684\u65b9\u6cd5\u662f\u4f7f\u7528\u6d88\u606f\u4ee3\u7406\uff0c\u4f8b\u5982 Kafka \u6216 Azure \u670d\u52a1\u603b\u7ebf\u3002<\/p>\n<p>However, for this chapter, we are going to stick with a simple single- process CQRS system, so MediatR fits the bill perfectly.<br \/>\n\u4f46\u662f\uff0c\u5728\u672c\u7ae0\u4e2d\uff0c\u6211\u4eec\u5c06\u575a\u6301\u4f7f\u7528\u4e00\u4e2a\u7b80\u5355\u7684\u5355\u8fdb\u7a0b CQRS \u7cfb\u7edf\uff0c\u56e0\u6b64 MediatR \u5b8c\u5168\u7b26\u5408\u8981\u6c42\u3002<\/p>\n<h3>33.3 Adding Application Project and Initial Configuration<\/h3>\n<p>33.3 \u6dfb\u52a0\u5e94\u7528\u7a0b\u5e8f\u9879\u76ee\u548c\u521d\u59cb\u914d\u7f6e<\/p>\n<p>Let\u2019s start by opening the starter project from the start folder. You will\u200c see that we don\u2019t have the Service nor the Service.Contracts projects. Well, we don\u2019t need them. We are going to use CQRS with MediatR to replace that part of our solution.<br \/>\n\u8ba9\u6211\u4eec\u4ece start \u6587\u4ef6\u5939\u6253\u5f00 starter \u9879\u76ee\u3002\u60a8\u5c06\u770b\u5230\u6211\u4eec\u6ca1\u6709 Service \u548c Service.Contracts \u9879\u76ee\u3002\u597d\u5427\uff0c\u6211\u4eec\u4e0d\u9700\u8981\u5b83\u4eec\u3002\u6211\u4eec\u5c06\u4f7f\u7528 CQRS \u548c MediatR \u6765\u66ff\u6362\u6211\u4eec\u89e3\u51b3\u65b9\u6848\u7684\u8be5\u90e8\u5206\u3002<\/p>\n<p>But, we do need an additional project for our business logic so, let\u2019s create a new class library (.NET Core) and name it Application.<br \/>\n\u4f46\u662f\uff0c\u6211\u4eec\u786e\u5b9e\u9700\u8981\u4e00\u4e2a\u989d\u5916\u7684\u9879\u76ee\u6765\u8fd0\u884c\u6211\u4eec\u7684\u4e1a\u52a1\u903b\u8f91\uff0c\u56e0\u6b64\uff0c\u8ba9\u6211\u4eec\u521b\u5efa\u4e00\u4e2a\u65b0\u7684\u7c7b\u5e93 \uff08.NET Core\uff09 \u5e76\u5c06\u5176\u547d\u540d\u4e3a Application\u3002<\/p>\n<p>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:<br \/>\n\u6b64\u5916\uff0c\u6211\u4eec\u5c06\u6dfb\u52a0\u4e00\u4e2a\u540d\u4e3a AssemblyReference \u7684\u65b0\u7c7b\u3002\u6211\u4eec\u5c06\u5c06\u5176\u7528\u4e8e\u4e0e Presentation \u9879\u76ee\u4e2d\u4f7f\u7528\u540c\u540d\u7c7b\u76f8\u540c\u7684\u76ee\u7684\uff1a<\/p>\n<pre><code>public static class AssemblyReference { }<\/code><\/pre>\n<p>Now let\u2019s install a couple of packages.<br \/>\n\u73b0\u5728\u8ba9\u6211\u4eec\u5b89\u88c5\u51e0\u4e2a\u5305\u3002<\/p>\n<p>The first package we are going to install is the MediatR in the Application project:<br \/>\n\u6211\u4eec\u8981\u5b89\u88c5\u7684\u7b2c\u4e00\u4e2a\u5305\u662f Application \u9879\u76ee\u4e2d\u7684 MediatR\uff1a<\/p>\n<pre><code>PM&gt; install-package MediatR<\/code><\/pre>\n<p>Then in the main project, we are going to install another package that wires up MediatR with the ASP.NET dependency injection container:<br \/>\n\u7136\u540e\u5728\u4e3b\u9879\u76ee\u4e2d\uff0c\u6211\u4eec\u5c06\u5b89\u88c5\u53e6\u4e00\u4e2a\u5305\uff0c\u8be5\u5305\u5c06 MediatR \u4e0e ASP.NET \u4f9d\u8d56\u9879\u6ce8\u5165\u5bb9\u5668\u8fde\u63a5\u8d77\u6765\uff1a<\/p>\n<pre><code>PM&gt; install-package MediatR.Extensions.Microsoft.DependencyInjection<\/code><\/pre>\n<p>After the installations, we are going to configure MediatR in the Program class:<br \/>\n\u5b89\u88c5\u5b8c\u6210\u540e\uff0c\u6211\u4eec\u5c06\u5728 Program \u7c7b\u4e2d\u914d\u7f6e MediatR\uff1a<\/p>\n<pre><code>builder.Services.AddMediatR(typeof(Application.AssemblyReference).Assembly);<\/code><\/pre>\n<p>For this, we have to reference the Application project, and add a using directive:<br \/>\n\u4e3a\u6b64\uff0c\u6211\u4eec\u5fc5\u987b\u5f15\u7528 Application \u9879\u76ee\uff0c\u5e76\u6dfb\u52a0\u4e00\u4e2a using \u6307\u4ee4\uff1a<\/p>\n<pre><code>using MediatR;<\/code><\/pre>\n<p>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\u2019s assembly as a parameter.<br \/>\nAddMediatR \u65b9\u6cd5\u5c06\u626b\u63cf\u5305\u542b\u6211\u4eec\u5c06\u7528\u4e8e\u5904\u7406\u4e1a\u52a1\u903b\u8f91\u7684\u5904\u7406\u7a0b\u5e8f\u7684\u9879\u76ee\u7a0b\u5e8f\u96c6\u3002\u7531\u4e8e\u6211\u4eec\u5c06\u8fd9\u4e9b\u5904\u7406\u7a0b\u5e8f\u653e\u5728 Application \u9879\u76ee\u4e2d\uff0c\u56e0\u6b64\u6211\u4eec\u5c06 Application \u7684\u7a0b\u5e8f\u96c6\u7528\u4f5c\u53c2\u6570\u3002<\/p>\n<p>Before we continue, we have to reference the Application project from the Presentation project.<br \/>\n\u5728\u7ee7\u7eed\u4e4b\u524d\uff0c\u6211\u4eec\u5fc5\u987b\u4ece Presentation \u9879\u76ee\u4e2d\u5f15\u7528 Application \u9879\u76ee\u3002<\/p>\n<p>Now MediatR is configured, and we can use it inside our controller.<br \/>\n\u73b0\u5728 MediatR \u5df2\u7ecf\u914d\u7f6e\u597d\u4e86\uff0c\u6211\u4eec\u53ef\u4ee5\u5728\u63a7\u5236\u5668\u4e2d\u4f7f\u7528\u5b83\u3002<\/p>\n<p>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:<br \/>\n\u5728 Presentation \u9879\u76ee\u7684 Controllers \u6587\u4ef6\u5939\u4e2d\uff0c\u6211\u4eec\u5c06\u627e\u5230\u4e00\u4e2a\u63a7\u5236\u5668\u7c7b\u3002\u5b83\u53ea\u5305\u542b\u4e00\u4e2a\u57fa\u672c\u4ee3\u7801\uff0c\u6211\u4eec\u5c06\u901a\u8fc7\u6784\u9020\u51fd\u6570\u6ce8\u5165\u6dfb\u52a0\u4e00\u4e2a sender \u6765\u4fee\u6539\u5b83\uff1a<\/p>\n<pre><code>[Route(&quot;api\/companies&quot;)] [ApiController] public class CompaniesController : ControllerBase { private readonly ISender _sender; public CompaniesController(ISender sender) =&gt; _sender = sender; }<\/code><\/pre>\n<p>Here we inject the ISender interface from the MediatR namespace. We are going to use this interface to send requests to our handlers.<br \/>\n\u5728\u8fd9\u91cc\uff0c\u6211\u4eec\u4ece MediatR \u547d\u540d\u7a7a\u95f4\u6ce8\u5165 ISender \u63a5\u53e3\u3002\u6211\u4eec\u5c06\u4f7f\u7528\u6b64\u63a5\u53e3\u5c06\u8bf7\u6c42\u53d1\u9001\u5230\u6211\u4eec\u7684\u5904\u7406\u7a0b\u5e8f\u3002<\/p>\n<p>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:<br \/>\n\u6211\u4eec\u5fc5\u987b\u63d0\u5230\u5173\u4e8e\u4f7f\u7528 ISender \u800c\u4e0d\u662f IMediator \u63a5\u53e3\u7684\u4e00\u4ef6\u4e8b\u3002\u4ece MediatR \u7248\u672c 9.0 \u5f00\u59cb\uff0cIMediator \u63a5\u53e3\u5206\u4e3a\u4e24\u4e2a\u63a5\u53e3\uff1a<\/p>\n<pre><code>public interface ISender { Task&lt;TResponse&gt; Send&lt;TResponse&gt;(IRequest&lt;TResponse&gt; request, CancellationToken cancellationToken = default); Task&lt;object?&gt; Send(object request, CancellationToken cancellationToken = default); } public interface IPublisher { Task Publish(object notification, CancellationToken cancellationToken = default); Task Publish&lt;TNotification&gt;(TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification; } public interface IMediator : ISender, IPublisher { }<\/code><\/pre>\n<p>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.<br \/>\n\u56e0\u6b64\uff0c\u901a\u8fc7\u67e5\u770b\u4ee3\u7801\uff0c\u5f88\u660e\u663e\u60a8\u53ef\u4ee5\u7ee7\u7eed\u4f7f\u7528 IMediator \u63a5\u53e3\u6765\u53d1\u9001\u8bf7\u6c42\u548c\u53d1\u5e03\u901a\u77e5\u3002\u4f46\u5efa\u8bae\u4f7f\u7528 ISender \u548c IPublisher \u63a5\u53e3\u6765\u62c6\u5206\u8be5\u63a5\u53e3\u3002<\/p>\n<p>With that said, we can continue with the Application\u2019s logic implementation.<br \/>\n\u8bdd\u867d\u5982\u6b64\uff0c\u6211\u4eec\u53ef\u4ee5\u7ee7\u7eed Application \u7684 logic implementation\u3002<\/p>\n<h2>33.4 Requests with MediatR<\/h2>\n<p>33.4 \u4f7f\u7528 MediatR \u7684\u8bf7\u6c42<\/p>\n<p>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.\u200c<br \/>\nMediatR \u8bf7\u6c42\u662f\u7b80\u5355\u7684\u8bf7\u6c42-\u54cd\u5e94\u6837\u5f0f\u7684\u6d88\u606f\uff0c\u5176\u4e2d\u5355\u4e2a\u8bf7\u6c42\u7531\u5355\u4e2a\u5904\u7406\u7a0b\u5e8f\u540c\u6b65\u5904\u7406\uff08\u4ece\u8bf7\u6c42\u7684\u89d2\u5ea6\u6765\u770b\u662f\u540c\u6b65\u7684\uff0c\u800c\u4e0d\u662f C# \u5185\u90e8\u7684 async\/await\uff09\u3002\u8fd9\u91cc\u7684\u597d\u7528\u4f8b\u662f\u4ece\u6570\u636e\u5e93\u8fd4\u56de\u4e00\u4e9b\u4e1c\u897f\u6216\u66f4\u65b0\u6570\u636e\u5e93\u3002<\/p>\n<p>There are two types of requests in MediatR. One that returns a value, and one that doesn\u2019t. Often this corresponds to reads\/queries (returning a value) and writes\/commands (usually doesn\u2019t return a value).<br \/>\nMediatR \u4e2d\u6709\u4e24\u79cd\u7c7b\u578b\u7684\u8bf7\u6c42\u3002\u4e00\u4e2a\u8fd4\u56de\u503c\uff0c\u53e6\u4e00\u4e2a\u4e0d\u8fd4\u56de\u503c\u3002\u8fd9\u901a\u5e38\u5bf9\u5e94\u4e8e reads\/queries\uff08\u8fd4\u56de\u4e00\u4e2a\u503c\uff09\u548c writes\/commands\uff08\u901a\u5e38\u4e0d\u8fd4\u56de\u4e00\u4e2a\u503c\uff09\u3002<\/p>\n<p>So, before we start sending requests, we are going to create several folders in the Application project to separate queries, commands, and handlers:<br \/>\n\u56e0\u6b64\uff0c\u5728\u5f00\u59cb\u53d1\u9001\u8bf7\u6c42\u4e4b\u524d\uff0c\u6211\u4eec\u5c06\u5728 Application \u9879\u76ee\u4e2d\u521b\u5efa\u591a\u4e2a\u6587\u4ef6\u5939\uff0c\u4ee5\u5206\u9694\u67e5\u8be2\u3001\u547d\u4ee4\u548c\u5904\u7406\u7a0b\u5e8f\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3303.jpg\" alt=\"alt text\" \/><\/p>\n<p>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.<br \/>\n\u7531\u4e8e\u6211\u4eec\u53ea\u8981\u4f7f\u7528 company \u5b9e\u4f53\uff0c\u56e0\u6b64\u6211\u4eec\u5c06\u67e5\u8be2\u3001\u547d\u4ee4\u548c\u5904\u7406\u7a0b\u5e8f\u76f4\u63a5\u653e\u5165\u8fd9\u4e9b\u6587\u4ef6\u5939\u4e2d\u3002<\/p>\n<p>But in larger projects with multiple entities, we can create additional folders for each entity inside each of these folders for better organization.<br \/>\n\u4f46\u5728\u5177\u6709\u591a\u4e2a\u5b9e\u4f53\u7684\u5927\u578b\u9879\u76ee\u4e2d\uff0c\u6211\u4eec\u53ef\u4ee5\u4e3a\u6bcf\u4e2a\u6587\u4ef6\u5939\u4e2d\u7684\u6bcf\u4e2a\u5b9e\u4f53\u521b\u5efa\u989d\u5916\u7684\u6587\u4ef6\u5939\uff0c\u4ee5\u4fbf\u66f4\u597d\u5730\u7ec4\u7ec7\u3002<\/p>\n<p>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.<br \/>\n\u6b64\u5916\uff0c\u6b63\u5982\u6211\u4eec\u5df2\u7ecf\u77e5\u9053\u7684\uff0c\u6211\u4eec\u4e0d\u4f1a\u5c06\u5b9e\u4f53\u4f5c\u4e3a\u7ed3\u679c\u53d1\u9001\u7ed9\u5ba2\u6237\u7aef\uff0c\u800c\u662f\u53d1\u9001\u7ed9 DTO\uff0c\u56e0\u6b64\u6211\u4eec\u5fc5\u987b\u5f15\u7528 Shared \u9879\u76ee\u3002<\/p>\n<p>That said, let\u2019s start with our first query. Let\u2019s create it in the Queries folder:<br \/>\n\u4e5f\u5c31\u662f\u8bf4\uff0c\u8ba9\u6211\u4eec\u4ece\u7b2c\u4e00\u4e2a\u67e5\u8be2\u5f00\u59cb\u3002\u8ba9\u6211\u4eec\u5728 Queries \u6587\u4ef6\u5939\u4e2d\u521b\u5efa\u5b83\uff1a<\/p>\n<pre><code>public sealed record GetCompaniesQuery(bool TrackChanges) : IRequest&lt;IEnumerable&lt;CompanyDto&gt;&gt;;<\/code><\/pre>\n<p>Here, we create the GetCompaniesQuery record, which implements <code>IRequest&lt;IEnumerable&lt;CompanyDto&gt;&gt;<\/code>. This simply means our request will return a list of companies.<br \/>\n\u5728\u8fd9\u91cc\uff0c\u6211\u4eec\u521b\u5efa GetCompaniesQuery \u8bb0\u5f55\uff0c\u8be5\u8bb0\u5f55\u5b9e\u73b0 <code>IRequest&lt;IEnumerable&lt;CompanyDto&gt;&gt;<\/code>\u3002\u8fd9\u53ea\u662f\u610f\u5473\u7740\u6211\u4eec\u7684\u8bf7\u6c42\u5c06\u8fd4\u56de\u516c\u53f8\u5217\u8868\u3002<\/p>\n<p>Here we need two additional namespaces:<br \/>\n\u8fd9\u91cc\u6211\u4eec\u9700\u8981\u4e24\u4e2a\u989d\u5916\u7684\u547d\u540d\u7a7a\u95f4\uff1a<\/p>\n<pre><code>using MediatR;\nusing Shared.DataTransferObjects;<\/code><\/pre>\n<p>Once we send the request from our controller\u2019s action, we are going to see the usage of this query.<br \/>\n\u4e00\u65e6\u6211\u4eec\u4ece\u63a7\u5236\u5668\u7684 action \u53d1\u9001\u8bf7\u6c42\uff0c\u6211\u4eec\u5c06\u770b\u5230\u8fd9\u4e2a\u67e5\u8be2\u7684\u7528\u6cd5\u3002<\/p>\n<p>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 \u2013 we will make no difference here. For that, we have to reference the Contracts project so we can access the IRepositoryManager interface.<br \/>\n\u67e5\u8be2\u4e4b\u540e\uff0c\u6211\u4eec\u9700\u8981\u4e00\u4e2a\u5904\u7406\u7a0b\u5e8f\u3002\u7b80\u5355\u6765\u8bf4\uff0c\u8fd9\u4e2a\u5904\u7406\u7a0b\u5e8f\u5c06\u6210\u4e3a\u6211\u4eec\u9879\u76ee\u4e2d\u670d\u52a1\u5c42\u65b9\u6cd5\u7684\u66ff\u4ee3\u54c1\u3002\u5728\u6211\u4eec\u4e4b\u524d\u7684\u9879\u76ee\u4e2d\uff0c\u6240\u6709\u670d\u52a1\u7c7b\u90fd\u4f7f\u7528\u5b58\u50a8\u5e93\u6765\u8bbf\u95ee\u6570\u636e\u5e93 \u2013 \u6211\u4eec\u5728\u8fd9\u91cc\u4e0d\u4f1a\u6709\u4ec0\u4e48\u533a\u522b\u3002\u4e3a\u6b64\uff0c\u6211\u4eec\u5fc5\u987b\u5f15\u7528 Contracts \u9879\u76ee\uff0c\u4ee5\u4fbf\u6211\u4eec\u53ef\u4ee5\u8bbf\u95ee IRepositoryManager \u63a5\u53e3\u3002<\/p>\n<p>After adding the reference, we can create a new GetCompaniesHandler class in the Handlers folder:<br \/>\n\u6dfb\u52a0\u5f15\u7528\u540e\uff0c\u6211\u4eec\u53ef\u4ee5\u5728 Handlers \u6587\u4ef6\u5939\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u7684 GetCompaniesHandler \u7c7b\uff1a<\/p>\n<pre><code>internal sealed class GetCompaniesHandler : IRequestHandler&lt;GetCompaniesQuery, IEnumerable&lt;CompanyDto&gt;&gt; { private readonly IRepositoryManager _repository; public GetCompaniesHandler(IRepositoryManager repository) =&gt; _repository = repository; public Task&lt;IEnumerable&lt;CompanyDto&gt;&gt; Handle(GetCompaniesQuery request, CancellationToken cancellationToken) { throw new NotImplementedException(); } }<\/code><\/pre>\n<p>Our handler inherits from <code>IRequestHandler&lt;GetCompaniesQuery,IEnumerable&lt;Product&gt;&gt;<\/code>. This means this class will handle GetCompaniesQuery, in this case, returning the list of companies.<br \/>\n\u6211\u4eec\u7684\u5904\u7406\u7a0b\u5e8f\u7ee7\u627f\u81ea <code>IRequestHandler&lt;GetCompaniesQuery,IEnumerable&lt;Product&gt;&gt;<\/code>\u3002\u8fd9\u610f\u5473\u7740\u6b64\u7c7b\u5c06\u5904\u7406 GetCompaniesQuery\uff0c\u5728\u672c\u4f8b\u4e2d\uff0c\u8fd4\u56de\u516c\u53f8\u5217\u8868\u3002<\/p>\n<p>We also inject the repository through the constructor and add a default implementation of the Handle method, required by the IRequestHandler interface.<br \/>\n\u6211\u4eec\u8fd8\u901a\u8fc7\u6784\u9020\u51fd\u6570\u6ce8\u5165\u5b58\u50a8\u5e93\uff0c\u5e76\u6dfb\u52a0 IRequestHandler \u63a5\u53e3\u6240\u9700\u7684 Handle \u65b9\u6cd5\u7684\u9ed8\u8ba4\u5b9e\u73b0\u3002<\/p>\n<p>These are the required namespaces:<br \/>\n\u8fd9\u4e9b\u662f\u5fc5\u9700\u7684\u547d\u540d\u7a7a\u95f4\uff1a<\/p>\n<pre><code>using Application.Queries; \nusing Contracts;\nusing MediatR;\nusing Shared.DataTransferObjects;<\/code><\/pre>\n<p>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:<br \/>\n\u5f53\u7136\uff0c\u6211\u4eec\u4e0d\u4f1a\u8ba9\u6b64\u65b9\u6cd5\u629b\u51fa\u5f02\u5e38\u3002\u4f46\u5728\u6dfb\u52a0\u4e1a\u52a1\u903b\u8f91\u4e4b\u524d\uff0c\u6211\u4eec\u5fc5\u987b\u5728 Application \u9879\u76ee\u4e2d\u5b89\u88c5 AutoMapper\uff1a<\/p>\n<pre><code>PM&gt; Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection<\/code><\/pre>\n<p>Register the package in the Program class:<br \/>\n\u5728 Program \u7c7b\u4e2d\u6ce8\u518c\u5305\uff1a<\/p>\n<pre><code>builder.Services.AddAutoMapper(typeof(Program));\nbuilder.Services.AddMediatR(typeof(Application.AssemblyReference).Assembly);<\/code><\/pre>\n<p>And create the MappingProfile class, also in the main project, with a single mapping rule:<br \/>\n\u5e76\u5728\u4e3b\u9879\u76ee\u4e2d\u4f7f\u7528 single mapping rule \u521b\u5efa MappingProfile \u7c7b\uff1a<\/p>\n<pre><code>public class MappingProfile : Profile { public MappingProfile() { CreateMap&lt;Company, CompanyDto&gt;() .ForMember(c =&gt; c.FullAddress, opt =&gt; opt.MapFrom(x =&gt; string.Join(&#039; &#039;, x.Address, x.Country))); } }<\/code><\/pre>\n<p>Everything with these actions is familiar since we\u2019ve already used AutoMapper in our project.<br \/>\n\u8fd9\u4e9b\u4f5c\u7684\u6240\u6709\u5185\u5bb9\u90fd\u662f\u719f\u6089\u7684\uff0c\u56e0\u4e3a\u6211\u4eec\u5df2\u7ecf\u5728\u9879\u76ee\u4e2d\u4f7f\u7528\u4e86 AutoMapper\u3002<\/p>\n<p>Now, we can modify the handler class:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u4fee\u6539 handler \u7c7b\uff1a<\/p>\n<pre><code>internal sealed class GetCompaniesHandler : IRequestHandler&lt;GetCompaniesQuery, IEnumerable&lt;CompanyDto&gt;&gt; { private readonly IRepositoryManager _repository; private readonly IMapper _mapper; public GetCompaniesHandler(IRepositoryManager repository, IMapper mapper) {_repository = repository; _mapper = mapper; } public async Task&lt;IEnumerable&lt;CompanyDto&gt;&gt; Handle(GetCompaniesQuery request, CancellationToken cancellationToken) { var companies = await _repository.Company.GetAllCompaniesAsync(request.TrackChanges); var companiesDto = _mapper.Map&lt;IEnumerable&lt;CompanyDto&gt;&gt;(companies); return companiesDto; } }<\/code><\/pre>\n<p>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.<br \/>\n\u6b64\u903b\u8f91\u4e5f\u5f88\u719f\u6089\uff0c\u56e0\u4e3a\u6211\u4eec\u5728 GetAllCompaniesAsync \u670d\u52a1\u65b9\u6cd5\u4e2d\u5177\u6709\u51e0\u4e4e\u76f8\u540c\u7684\u903b\u8f91\u3002\u4e00\u4e2a\u533a\u522b\u662f\uff0c\u6211\u4eec\u901a\u8fc7 request \u5bf9\u8c61\u4f20\u9012 track changes \u53c2\u6570\u3002<\/p>\n<p>Now, we can modify CompaniesController:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u4fee\u6539 CompaniesController\uff1a<\/p>\n<pre><code>[HttpGet] public async Task&lt;IActionResult&gt; GetCompanies() { var companies = await _sender.Send(new GetCompaniesQuery(TrackChanges: false)); return Ok(companies); }<\/code><\/pre>\n<p>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:<br \/>\n\u6211\u4eec\u4f7f\u7528 Send \u65b9\u6cd5\u5411\u5904\u7406\u7a0b\u5e8f\u53d1\u9001\u8bf7\u6c42\uff0c\u5e76\u5c06 GetCompaniesQuery \u4f5c\u4e3a\u53c2\u6570\u4f20\u9012\u3002\u4ec5\u6b64\u800c\u5df2\u3002\u6211\u4eec\u8fd8\u9700\u8981\u4e00\u4e2a\u989d\u5916\u7684\u547d\u540d\u7a7a\u95f4\uff1a<\/p>\n<pre><code>using Application.Queries;<\/code><\/pre>\n<p>Our controller is clean as it was with the service layer implemented. But this time, we don\u2019t have a single service class to handle all the methods but a single handler to take care of only one thing.<br \/>\n\u6211\u4eec\u7684\u63a7\u5236\u5668\u4e0e\u5b9e\u65bd\u670d\u52a1\u5c42\u65f6\u4e00\u6837\u5e72\u51c0\u3002\u4f46\u662f\u8fd9\u4e00\u6b21\uff0c\u6211\u4eec\u6ca1\u6709\u4e00\u4e2a\u670d\u52a1\u7c7b\u6765\u5904\u7406\u6240\u6709\u65b9\u6cd5\uff0c\u800c\u53ea\u6709\u4e00\u4e2a\u5904\u7406\u7a0b\u5e8f\u6765\u5904\u7406\u4e00\u4ef6\u4e8b\u3002<\/p>\n<p>Now, we can test this:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u6d4b\u8bd5\u4e00\u4e0b\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\">https:\/\/localhost:5001\/api\/companies<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3304.jpg\" alt=\"alt text\" \/><\/p>\n<p>Everything works great. With this in mind, we can continue and implement the logic for fetching a single company.<br \/>\n\u4e00\u5207\u90fd\u5f88\u597d\u3002\u8003\u8651\u5230\u8fd9\u4e00\u70b9\uff0c\u6211\u4eec\u53ef\u4ee5\u7ee7\u7eed\u5e76\u5b9e\u73b0\u83b7\u53d6\u5355\u4e2a\u516c\u53f8\u7684\u903b\u8f91\u3002<\/p>\n<p>So, let\u2019s start with the query in the Queries folder:<br \/>\n\u56e0\u6b64\uff0c\u8ba9\u6211\u4eec\u4ece Queries \u6587\u4ef6\u5939\u4e2d\u7684\u67e5\u8be2\u5f00\u59cb\uff1a<\/p>\n<pre><code>public sealed record GetCompanyQuery(Guid Id, bool TrackChanges) : IRequest&lt;CompanyDto&gt;;<\/code><\/pre>\n<p>Then, let\u2019s implement a new handler:<br \/>\n\u7136\u540e\uff0c\u8ba9\u6211\u4eec\u5b9e\u73b0\u4e00\u4e2a\u65b0\u7684\u5904\u7406\u7a0b\u5e8f\uff1a<\/p>\n<pre><code>internal sealed class GetCompanyHandler : IRequestHandler&lt;GetCompanyQuery, CompanyDto&gt; { private readonly IRepositoryManager _repository; private readonly IMapper _mapper; public GetCompanyHandler(IRepositoryManager repository, IMapper mapper) { _repository = repository; _mapper = mapper; } public async Task&lt;CompanyDto&gt; 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&lt;CompanyDto&gt;(company); return companyDto;} }<\/code><\/pre>\n<p>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.<br \/>\n\u56e0\u6b64\uff0c\u6211\u4eec\u7684\u5904\u7406\u7a0b\u5e8f\u518d\u6b21\u7ee7\u627f\u81ea IRequestHandler \u63a5\u53e3\uff0c\u63a5\u53d7\u67e5\u8be2\u4f5c\u4e3a\u7b2c\u4e00\u4e2a\u53c2\u6570\uff0c\u5c06\u7ed3\u679c\u4f5c\u4e3a\u7b2c\u4e8c\u4e2a\u53c2\u6570\u3002\u7136\u540e\uff0c\u6211\u4eec\u6ce8\u5165\u6240\u9700\u7684\u670d\u52a1\u5e76\u719f\u6089\u5730\u5b9e\u73b0 Handle \u65b9\u6cd5\u3002<\/p>\n<p>We need these namespaces here:<br \/>\n\u6211\u4eec\u5728\u6b64\u5904\u9700\u8981\u8fd9\u4e9b\u547d\u540d\u7a7a\u95f4\uff1a<\/p>\n<pre><code>using Application.Queries; \nusing AutoMapper;\nusing Contracts;\nusing Entities.Exceptions; \nusing MediatR;\nusing Shared.DataTransferObjects;<\/code><\/pre>\n<p>Lastly, we have to add another action in CompaniesController:<br \/>\n\u6700\u540e\uff0c\u6211\u4eec\u5fc5\u987b\u5728 CompaniesController \u4e2d\u6dfb\u52a0\u53e6\u4e00\u4e2a\u4f5c\uff1a<\/p>\n<pre><code>[HttpGet(&quot;{id:guid}&quot;, Name = &quot;CompanyById&quot;)] public async Task&lt;IActionResult&gt; GetCompany(Guid id) { var company = await _sender.Send(new GetCompanyQuery(id, TrackChanges: false)); return Ok(company); }<\/code><\/pre>\n<p>Awesome, let\u2019s test it:<br \/>\n\u592a\u68d2\u4e86\uff0c\u8ba9\u6211\u4eec\u6d4b\u8bd5\u4e00\u4e0b\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/3d490a70-94ce-4d15-9494-5248280c2ce3\">https:\/\/localhost:5001\/api\/companies\/3d490a70-94ce-4d15-9494-5248280c2ce3<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3305.jpg\" alt=\"alt text\" \/><\/p>\n<p>Excellent, we can see the company DTO in the response body. Additionally, we can try an invalid request:<br \/>\n\u592a\u597d\u4e86\uff0c\u6211\u4eec\u53ef\u4ee5\u5728\u54cd\u5e94\u6b63\u6587\u4e2d\u770b\u5230\u516c\u53f8 DTO\u3002\u6b64\u5916\uff0c\u6211\u4eec\u53ef\u4ee5\u5c1d\u8bd5\u65e0\u6548\u7684\u8bf7\u6c42\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/3d490a70-94ce-4d15-9494-5248280c2ce2\">https:\/\/localhost:5001\/api\/companies\/3d490a70-94ce-4d15-9494-5248280c2ce2<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3306.jpg\" alt=\"alt text\" \/><\/p>\n<p>And, we can see this works as well.<br \/>\n\u800c\u4e14\uff0c\u6211\u4eec\u53ef\u4ee5\u770b\u5230\u8fd9\u4e5f\u6709\u6548\u3002<\/p>\n<h2>33.5 Commands with MediatR<\/h2>\n<p>33.5 \u4f7f\u7528 MediatR \u7684\u547d\u4ee4<\/p>\n<p>As with both queries, we are going to start with a command record creation inside the Commands folder:\u200c<br \/>\n\u4e0e\u8fd9\u4e24\u4e2a\u67e5\u8be2\u4e00\u6837\uff0c\u6211\u4eec\u5c06\u4ece Commands \u6587\u4ef6\u5939\u4e2d\u7684\u547d\u4ee4\u8bb0\u5f55\u521b\u5efa\u5f00\u59cb\uff1a<\/p>\n<pre><code>public sealed record CreateCompanyCommand(CompanyForCreationDto Company) : IRequest&lt;CompanyDto&gt;;<\/code><\/pre>\n<p>Our command has a single parameter sent from the client, and it inherits from IRequest<CompanyDto>. Our request has to return CompanyDto because we will need it, in our action, to create a valid route in the return statement.<br \/>\n\u6211\u4eec\u7684\u547d\u4ee4\u6709\u4e00\u4e2a\u4ece\u5ba2\u6237\u7aef\u53d1\u9001\u7684\u53c2\u6570\uff0c\u5b83\u7ee7\u627f\u81ea IRequest\u3002\u6211\u4eec\u7684\u8bf7\u6c42\u5fc5\u987b\u8fd4\u56de CompanyDto\uff0c\u56e0\u4e3a\u6211\u4eec\u5728\u4f5c\u4e2d\u9700\u8981\u5b83\u6765\u5728 return \u8bed\u53e5\u4e2d\u521b\u5efa\u6709\u6548\u7684\u8def\u7531\u3002<\/p>\n<p>After the query, we are going to create another handler:<br \/>\n\u67e5\u8be2\u4e4b\u540e\uff0c\u6211\u4eec\u5c06\u521b\u5efa\u53e6\u4e00\u4e2a\u5904\u7406\u7a0b\u5e8f\uff1a<\/p>\n<pre><code>internal sealed class CreateCompanyHandler : IRequestHandler&lt;CreateCompanyCommand, CompanyDto&gt; { private readonly IRepositoryManager _repository; private readonly IMapper _mapper; public CreateCompanyHandler(IRepositoryManager repository, IMapper mapper) { _repository = repository; _mapper = mapper; } public async Task&lt;CompanyDto&gt; Handle(CreateCompanyCommand request, CancellationToken cancellationToken) { var companyEntity = _mapper.Map&lt;Company&gt;(request.Company); _repository.Company.CreateCompany(companyEntity); await _repository.SaveAsync();var companyToReturn = _mapper.Map&lt;CompanyDto&gt;(companyEntity); return companyToReturn; } }<\/code><\/pre>\n<p>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.<br \/>\n\u56e0\u6b64\uff0c\u6211\u4eec\u6ce8\u5165\u6211\u4eec\u7684\u670d\u52a1\u5e76\u5b9e\u73b0 Handle \u65b9\u6cd5\uff0c\u5c31\u50cf\u6211\u4eec\u5bf9 service \u65b9\u6cd5\u6240\u505a\u7684\u90a3\u6837\u3002\u6211\u4eec\u4ece\u521b\u5efa DTO \u6620\u5c04\u5230\u5b9e\u4f53\uff0c\u5c06\u5176\u4fdd\u5b58\u5230\u6570\u636e\u5e93\uff0c\u5e76\u5c06\u5176\u6620\u5c04\u5230\u516c\u53f8 DTO \u5bf9\u8c61\u3002<\/p>\n<p>Then, before we add a new mapping rule in the MappingProfile class:<br \/>\n\u7136\u540e\uff0c\u5728\u6211\u4eec\u5728 MappingProfile \u7c7b\u4e2d\u6dfb\u52a0\u65b0\u7684\u6620\u5c04\u89c4\u5219\u4e4b\u524d\uff1a<\/p>\n<pre><code>CreateMap&lt;CompanyForCreationDto, Company&gt;();<\/code><\/pre>\n<p>Now, we can add a new action in a controller:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u5728\u63a7\u5236\u5668\u4e2d\u6dfb\u52a0\u4e00\u4e2a\u65b0\u7684\u52a8\u4f5c\uff1a<\/p>\n<pre><code>[HttpPost] public async Task&lt;IActionResult&gt; CreateCompany([FromBody] CompanyForCreationDto companyForCreationDto) { if (companyForCreationDto is null) return BadRequest(&quot;CompanyForCreationDto object is null&quot;); var company = await _sender.Send(new CreateCompanyCommand(companyForCreationDto)); return CreatedAtRoute(&quot;CompanyById&quot;, new { id = company.Id }, company); }<\/code><\/pre>\n<p>That\u2019s all it takes. Now we can test this:<br \/>\n\u5c31\u8fd9\u6837\u3002\u73b0\u5728\u6211\u4eec\u53ef\u4ee5\u6d4b\u8bd5\u4e00\u4e0b\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\">https:\/\/localhost:5001\/api\/companies<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3307.jpg\" alt=\"alt text\" \/><\/p>\n<p>A new company is created, and if we inspect the Headers tab, we are going to find the link to fetch this new company:<br \/>\n\u521b\u5efa\u4e86\u4e00\u4e2a\u65b0\u516c\u53f8\uff0c\u5982\u679c\u6211\u4eec\u68c0\u67e5 Headers \u9009\u9879\u5361\uff0c\u6211\u4eec\u5c06\u627e\u5230\u83b7\u53d6\u8fd9\u5bb6\u65b0\u516c\u53f8\u7684\u94fe\u63a5\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3308.jpg\" alt=\"alt text\" \/><\/p>\n<p>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\u2019s being implemented. The commands and queries could be pointing to different data stores. They don\u2019t know how their request will be handled, and they don\u2019t care.<br \/>\n\u5728\u8fd9\u91cc\uff0c\u6211\u4eec\u5fc5\u987b\u4e86\u89e3\u4e00\u4ef6\u91cd\u8981\u7684\u4e8b\u60c5\u3002\u6211\u4eec\u901a\u8fc7\u7b80\u5355\u7684\u6d88\u606f\u6784\u9020\u4e0e datastore \u901a\u4fe1\uff0c\u4f46\u4e0d\u77e5\u9053\u5b83\u662f\u5982\u4f55\u5b9e\u73b0\u7684\u3002\u547d\u4ee4\u548c\u67e5\u8be2\u53ef\u80fd\u6307\u5411\u4e0d\u540c\u7684\u6570\u636e\u5b58\u50a8\u3002\u4ed6\u4eec\u4e0d\u77e5\u9053\u4ed6\u4eec\u7684\u8bf7\u6c42\u5c06\u5982\u4f55\u5904\u7406\uff0c\u4ed6\u4eec\u4e5f\u4e0d\u5728\u4e4e\u3002<\/p>\n<h3>33.5.1 Update Command\u200c<\/h3>\n<p>33.5.1 update \u547d\u4ee4<\/p>\n<p>Following the same principle from the previous example, we can implement the update request.<br \/>\n\u6309\u7167\u524d\u9762\u793a\u4f8b\u4e2d\u7684\u76f8\u540c\u539f\u5219\uff0c\u6211\u4eec\u53ef\u4ee5\u5b9e\u73b0 update \u8bf7\u6c42\u3002<\/p>\n<p>Let\u2019s start with the command:<br \/>\n\u8ba9\u6211\u4eec\u4ece\u547d\u4ee4\u5f00\u59cb\uff1a<\/p>\n<pre><code>public sealed record UpdateCompanyCommand\n(Guid Id, CompanyForUpdateDto Company, bool TrackChanges) : IRequest;<\/code><\/pre>\n<p>This time our command inherits from IRequest without any generic parameter. That\u2019s because we are not going to return any value with this request.<br \/>\n\u8fd9\u6b21\u6211\u4eec\u7684\u547d\u4ee4\u7ee7\u627f\u81ea IRequest\uff0c\u6ca1\u6709\u4efb\u4f55\u6cdb\u578b\u53c2\u6570\u3002\u90a3\u662f\u56e0\u4e3a\u6211\u4eec\u4e0d\u4f1a\u5728\u8fd9\u4e2a\u8bf7\u6c42\u4e2d\u8fd4\u56de\u4efb\u4f55\u503c\u3002<\/p>\n<p>Let\u2019s continue with the handler implementation:<br \/>\n\u8ba9\u6211\u4eec\u7ee7\u7eed\u5904\u7406\u7a0b\u5e8f\u5b9e\u73b0\uff1a<\/p>\n<pre><code>internal sealed class UpdateCompanyHandler : IRequestHandler&lt;UpdateCompanyCommand, Unit&gt; { private readonly IRepositoryManager _repository; private readonly IMapper _mapper; public UpdateCompanyHandler(IRepositoryManager repository, IMapper mapper) { _repository = repository; _mapper = mapper; } public async Task&lt;Unit&gt; 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; } }<\/code><\/pre>\n<p>This handler inherits from <code>IRequestHandler&lt;UpdateCompanyCommand, Unit&gt;<\/code>. 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.<br \/>\n\u6b64\u5904\u7406\u7a0b\u5e8f\u7ee7\u627f\u81ea <code>IRequestHandler&lt;UpdateCompanyCommand, Unit&gt;<\/code> .\u8fd9\u5bf9\u6211\u4eec\u6765\u8bf4\u662f\u65b0\u7684\uff0c\u56e0\u4e3a\u7b2c\u4e00\u6b21\u6211\u4eec\u7684\u547d\u4ee4\u6ca1\u6709\u8fd4\u56de\u4efb\u4f55\u503c\u3002\u4f46 IRequestHandler \u59cb\u7ec8\u63a5\u53d7\u4e24\u4e2a\u53c2\u6570\uff08TRequest \u548c TResponse\uff09\u3002\u56e0\u6b64\uff0c\u6211\u4eec\u4e3a TResponse \u53c2\u6570\u63d0\u4f9b\u4e86 Unit \u7ed3\u6784\uff0c\u56e0\u4e3a\u5b83\u8868\u793a void \u7c7b\u578b\u3002<\/p>\n<p>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.<br \/>\n\u7136\u540e\uff0c\u9664\u4e86 return \u90e8\u5206\u4e4b\u5916\uff0c\u6211\u4eec\u719f\u6089 Handle \u5b9e\u73b0\u3002\u6211\u4eec\u5fc5\u987b\u4ece Handle \u65b9\u6cd5\u8fd4\u56de\u4e00\u4e9b\u5185\u5bb9\uff0c\u5e76\u4f7f\u7528 Unit.Value\u3002<\/p>\n<p>Before we modify the controller, we have to add another mapping rule:<br \/>\n\u5728\u6211\u4eec\u4fee\u6539\u63a7\u5236\u5668\u4e4b\u524d\uff0c\u6211\u4eec\u5fc5\u987b\u6dfb\u52a0\u53e6\u4e00\u4e2a\u6620\u5c04\u89c4\u5219\uff1a<\/p>\n<pre><code>CreateMap&lt;CompanyForUpdateDto, Company&gt;();<\/code><\/pre>\n<p>Lastly, let\u2019s add a new action in the controller:<br \/>\n\u6700\u540e\uff0c\u8ba9\u6211\u4eec\u5728\u63a7\u5236\u5668\u4e2d\u6dfb\u52a0\u4e00\u4e2a\u65b0\u4f5c\uff1a<\/p>\n<pre><code>[HttpPut(&quot;{id:guid}&quot;)] public async Task&lt;IActionResult&gt; UpdateCompany(Guid id, CompanyForUpdateDto companyForUpdateDto) { if (companyForUpdateDto is null) return BadRequest(&quot;CompanyForUpdateDto object is null&quot;); await _sender.Send(new UpdateCompanyCommand(id, companyForUpdateDto, TrackChanges: true)); return NoContent(); }<\/code><\/pre>\n<p>At this point, we can send a PUT request from Postman:<br \/>\n\u6b64\u65f6\uff0c\u6211\u4eec\u53ef\u4ee5\u4ece Postman \u53d1\u9001\u4e00\u4e2a PUT \u8bf7\u6c42\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/7aea16e2-74b9-4fd9-c22a-08d9961aa2d5\">https:\/\/localhost:5001\/api\/companies\/7aea16e2-74b9-4fd9-c22a-08d9961aa2d5<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3309.jpg\" alt=\"alt text\" \/><\/p>\n<p>There is the 204 status code.<br \/>\n\u6709 204 \u72b6\u6001\u4ee3\u7801\u3002<\/p>\n<p>If you fetch this company, you will find the name updated for sure.<br \/>\n\u5982\u679c\u4f60\u627e\u5230\u8fd9\u5bb6\u516c\u53f8\uff0c\u4f60\u80af\u5b9a\u4f1a\u53d1\u73b0\u540d\u79f0\u66f4\u65b0\u4e86\u3002<\/p>\n<h3>33.5.2 Delete Command\u200c<\/h3>\n<p>33.5.2 Delete \u547d\u4ee4<\/p>\n<p>After all of this implementation, this one should be pretty straightforward.<br \/>\n\u5728\u6240\u6709\u8fd9\u4e9b\u5b9e\u73b0\u4e4b\u540e\uff0c\u8fd9\u4e2a\u5e94\u8be5\u975e\u5e38\u7b80\u5355\u3002<\/p>\n<p>Let\u2019s start with the command:<br \/>\n\u8ba9\u6211\u4eec\u4ece\u547d\u4ee4\u5f00\u59cb\uff1a<\/p>\n<pre><code>public record DeleteCompanyCommand(Guid Id, bool TrackChanges) : IRequest;<\/code><\/pre>\n<p>Then, let\u2019s continue with a handler:<br \/>\n\u7136\u540e\uff0c\u8ba9\u6211\u4eec\u7ee7\u7eed\u4f7f\u7528\u5904\u7406\u7a0b\u5e8f\uff1a<\/p>\n<pre><code>internal sealed class DeleteCompanyHandler : IRequestHandler&lt;DeleteCompanyCommand, Unit&gt; { private readonly IRepositoryManager _repository; public DeleteCompanyHandler(IRepositoryManager repository) =&gt; _repository = repository; public async Task&lt;Unit&gt; 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; } }<\/code><\/pre>\n<p>Finally, let\u2019s add one more action inside the controller:<br \/>\n\u6700\u540e\uff0c\u8ba9\u6211\u4eec\u5728\u63a7\u5236\u5668\u4e2d\u518d\u6dfb\u52a0\u4e00\u4e2a\u64cd\u4f5c\uff1a<\/p>\n<pre><code>[HttpDelete(&quot;{id:guid}&quot;)]\npublic async Task&lt;IActionResult&gt; DeleteCompany(Guid id) { await _sender.Send(new DeleteCompanyCommand(id, TrackChanges: false)); return NoContent(); }<\/code><\/pre>\n<p>That\u2019s it. Pretty easy.We can test this now:<br \/>\n\u5c31\u662f\u8fd9\u6837\u3002\u5f88\u7b80\u5355\u3002\u6211\u4eec\u73b0\u5728\u53ef\u4ee5\u6d4b\u8bd5\u4e00\u4e0b\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\/7aea16e2-74b9-4fd9-c22a-08d9961aa2d5\">https:\/\/localhost:5001\/api\/companies\/7aea16e2-74b9-4fd9-c22a-08d9961aa2d5<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3310.jpg\" alt=\"alt text\" \/><\/p>\n<p>It works great.<br \/>\n\u5b83\u6548\u679c\u5f88\u597d\u3002<\/p>\n<p>Now that we know how to work with requests using MediatR, let\u2019s see how to use notifications.<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u5df2\u7ecf\u77e5\u9053\u5982\u4f55\u4f7f\u7528 MediatR \u5904\u7406\u8bf7\u6c42\uff0c\u8ba9\u6211\u4eec\u770b\u770b\u5982\u4f55\u4f7f\u7528\u901a\u77e5\u3002<\/p>\n<h2>33.6 MediatR Notifications<\/h2>\n<p>So for we\u2019ve only seen a single request being handled by a single handler. However, what if we want to handle a single request by multiple handlers?\u200c<br \/>\n\u56e0\u6b64\uff0c\u6211\u4eec\u53ea\u770b\u5230\u5355\u4e2a\u8bf7\u6c42\u7531\u5355\u4e2a\u5904\u7406\u7a0b\u5e8f\u5904\u7406\u3002\u4f46\u662f\uff0c\u5982\u679c\u6211\u4eec\u60f3\u5904\u7406\u591a\u4e2a\u5904\u7406\u7a0b\u5e8f\u7684\u5355\u4e2a\u8bf7\u6c42\u600e\u4e48\u529e\uff1f<\/p>\n<p>That\u2019s where notifications come in. In these situations, we usually have multiple independent operations that need to occur after some event. Examples might be:<br \/>\n\u8fd9\u5c31\u662f\u901a\u77e5\u7684\u7528\u6b66\u4e4b\u5730\u3002\u5728\u8fd9\u4e9b\u60c5\u51b5\u4e0b\uff0c\u6211\u4eec\u901a\u5e38\u4f1a\u6709\u591a\u4e2a\u72ec\u7acb\u7684\u4f5c\u9700\u8981\u5728\u67d0\u4e9b\u4e8b\u4ef6\u4e4b\u540e\u53d1\u751f\u3002\u793a\u4f8b\u53ef\u80fd\u662f\uff1a<\/p>\n<p>\u2022 Sending an email<\/p>\n<p>\u2022 Invalidating a cache<\/p>\n<p>\u2022 ...<\/p>\n<p>To demonstrate this, we will update the delete company flow we created previously to publish a notification and have it handled by two handlers.<br \/>\n\u4e3a\u4e86\u6f14\u793a\u8fd9\u4e00\u70b9\uff0c\u6211\u4eec\u5c06\u66f4\u65b0\u4e4b\u524d\u521b\u5efa\u7684\u5220\u9664\u516c\u53f8\u6d41\u7a0b\uff0c\u4ee5\u53d1\u5e03\u901a\u77e5\u5e76\u8ba9\u4e24\u4e2a\u5904\u7406\u7a0b\u5e8f\u5904\u7406\u8be5\u901a\u77e5\u3002<\/p>\n<p>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.<br \/>\n\u53d1\u9001\u7535\u5b50\u90ae\u4ef6\u4e0d\u5728\u672c\u4e66\u7684\u8ba8\u8bba\u8303\u56f4\u4e4b\u5185\uff08\u60a8\u53ef\u4ee5\u5728\u6211\u4eec\u7684 Bonus 6 Security \u4e66\u7c4d\u4e2d\u4e86\u89e3\u66f4\u591a\u4fe1\u606f\uff09\u3002\u4f46\u4e3a\u4e86\u6f14\u793a\u901a\u77e5\u7684\u884c\u4e3a\uff0c\u6211\u4eec\u5c06\u4f7f\u7528\u6211\u4eec\u7684 logger \u670d\u52a1\u5e76\u8bb0\u5f55\u4e00\u6761\u6d88\u606f\uff0c\u5c31\u50cf\u7535\u5b50\u90ae\u4ef6\u5df2\u53d1\u9001\u4e00\u6837\u3002<\/p>\n<p>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.<br \/>\n\u56e0\u6b64\uff0c\u6d41\u7a0b\u5c06\u662f - \u5220\u9664\u516c\u53f8\u540e\uff0c\u6211\u4eec\u5e0c\u671b\u901a\u8fc7\u7535\u5b50\u90ae\u4ef6\u901a\u77e5\u7ba1\u7406\u5458\u53d1\u751f\u4e86\u5220\u9664\u4f5c\u3002<\/p>\n<p>That said, let\u2019s start by creating a new Notifications folder inside the Application project and add a new notification in that folder:<br \/>\n\u4e5f\u5c31\u662f\u8bf4\uff0c\u8ba9\u6211\u4eec\u9996\u5148\u5728 Application \u9879\u76ee\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u7684 Notifications \u6587\u4ef6\u5939\uff0c\u7136\u540e\u5728\u8be5\u6587\u4ef6\u5939\u4e2d\u6dfb\u52a0\u65b0\u7684\u901a\u77e5\uff1a<\/p>\n<pre><code>public sealed record CompanyDeletedNotification(Guid Id, bool TrackChanges) : INotification;<\/code><\/pre>\n<p>The notification has to inherit from the INotification interface. This is the equivalent of the IRequest we saw earlier, but for Notifications.<br \/>\n\u901a\u77e5\u5fc5\u987b\u7ee7\u627f\u81ea INotification \u63a5\u53e3\u3002\u8fd9\u76f8\u5f53\u4e8e\u6211\u4eec\u4e4b\u524d\u770b\u5230\u7684 IRequest\uff0c\u4f46\u7528\u4e8e Notifications\u3002<\/p>\n<p>As we can conclude, notifications don\u2019t return a value. They work on the fire and forget principle, like publishers.<br \/>\n\u6211\u4eec\u53ef\u4ee5\u5f97\u51fa\u7ed3\u8bba\uff0c\u901a\u77e5\u4e0d\u8fd4\u56de\u503c\u3002\u4ed6\u4eec\u50cf\u51fa\u7248\u5546\u4e00\u6837\uff0c\u6309\u7167 Fire and Forget \u7684\u539f\u5219\u5de5\u4f5c\u3002<\/p>\n<p>Next, we are going to create a new Emailhandler class:<br \/>\n\u63a5\u4e0b\u6765\uff0c\u6211\u4eec\u5c06\u521b\u5efa\u4e00\u4e2a\u65b0\u7684 Emailhandler \u7c7b\uff1a<\/p>\n<pre><code>internal sealed class EmailHandler : INotificationHandler&lt;CompanyDeletedNotification&gt; { private readonly ILoggerManager _logger; public EmailHandler(ILoggerManager logger) =&gt; _logger = logger; public async Task Handle(CompanyDeletedNotification notification, CancellationToken cancellationToken) { _logger.LogWarn($&quot;Delete action for the company with id: {notification.Id} has occurred.&quot;); await Task.CompletedTask; } }<\/code><\/pre>\n<p>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.<br \/>\n\u5728\u8fd9\u91cc\uff0c\u6211\u4eec\u53ea\u662f\u6a21\u62df\u4ee5\u5f02\u6b65\u65b9\u5f0f\u53d1\u9001\u7535\u5b50\u90ae\u4ef6\u3002\u6ca1\u6709\u592a\u591a\u7684\u590d\u6742\u6027\uff0c\u6211\u4eec\u4f7f\u7528 logger \u670d\u52a1\u6765\u5904\u7406\u6d88\u606f\u3002<\/p>\n<p>Let\u2019s continue by modifying the DeleteCompanyHandler class:<br \/>\n\u6211\u4eec\u7ee7\u7eed\u4fee\u6539 DeleteCompanyHandler \u7c7b\uff1a<\/p>\n<pre><code>internal sealed class DeleteCompanyHandler : INotificationHandler&lt;CompanyDeletedNotification&gt; { private readonly IRepositoryManager _repository; public DeleteCompanyHandler(IRepositoryManager repository) =&gt; _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(); } }<\/code><\/pre>\n<p>This time, our handler inherits from the INotificationHandler interface, and it doesn\u2019t return any value \u2013 we\u2019ve modified the method signature and removed the return statement.<br \/>\n\u8fd9\u4e00\u6b21\uff0c\u6211\u4eec\u7684\u5904\u7406\u7a0b\u5e8f\u7ee7\u627f\u81ea INotificationHandler \u63a5\u53e3\uff0c\u5b83\u4e0d\u8fd4\u56de\u4efb\u4f55\u503c \u2013 \u6211\u4eec\u4fee\u6539\u4e86\u65b9\u6cd5\u7b7e\u540d\u5e76\u5220\u9664\u4e86 return \u8bed\u53e5\u3002<\/p>\n<p>Finally, we have to modify the controller\u2019s constructor:<br \/>\n\u6700\u540e\uff0c\u6211\u4eec\u5fc5\u987b\u4fee\u6539\u63a7\u5236\u5668\u7684\u6784\u9020\u51fd\u6570\uff1a<\/p>\n<pre><code>private readonly ISender _sender; private readonly IPublisher _publisher; public CompaniesController(ISender sender, IPublisher publisher) { _sender = sender; _publisher = publisher; }<\/code><\/pre>\n<p>We inject another interface, which we are going to use to publish notifications.<br \/>\n\u6211\u4eec\u6ce8\u5165\u53e6\u4e00\u4e2a\u63a5\u53e3\uff0c\u6211\u4eec\u5c06\u4f7f\u7528\u5b83\u6765\u53d1\u5e03\u901a\u77e5\u3002<\/p>\n<p>And, we have to modify the DeleteCompany action:<br \/>\n\u800c\u4e14\uff0c\u6211\u4eec\u5fc5\u987b\u4fee\u6539 DeleteCompany\u4f5c\uff1a<\/p>\n<pre><code>[HttpDelete(&quot;{id:guid}&quot;)] public async Task&lt;IActionResult&gt; DeleteCompany(Guid id) { await _publisher.Publish(new CompanyDeletedNotification(id, TrackChanges: false)); return NoContent(); }<\/code><\/pre>\n<p>To test this, let\u2019s create a new company first:<br \/>\n\u4e3a\u4e86\u6d4b\u8bd5\u8fd9\u4e00\u70b9\uff0c\u8ba9\u6211\u4eec\u5148\u521b\u5efa\u4e00\u4e2a\u65b0\u516c\u53f8\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3311.jpg\" alt=\"alt text\" \/><\/p>\n<p>Now, if we send the Delete request, we are going to receive the 204 NoContent response:<br \/>\n\u73b0\u5728\uff0c\u5982\u679c\u6211\u4eec\u53d1\u9001 Delete \u8bf7\u6c42\uff0c\u6211\u4eec\u5c06\u6536\u5230 204 NoContent \u54cd\u5e94\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/e06089af-baeb-44ef-1fdf-08d99630e212\">https:\/\/localhost:5001\/api\/companies\/e06089af-baeb-44ef-1fdf-08d99630e212<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3312.jpg\" alt=\"alt text\" \/><\/p>\n<p>And also, if we inspect the logs, we will find a new logged message stating that the delete action has occurred:<br \/>\n\u6b64\u5916\uff0c\u5982\u679c\u6211\u4eec\u68c0\u67e5\u65e5\u5fd7\uff0c\u6211\u4eec\u5c06\u627e\u5230\u4e00\u6761\u65b0\u7684\u65e5\u5fd7\u8bb0\u5f55\u6d88\u606f\uff0c\u6307\u51fa\u5df2\u53d1\u751f\u5220\u9664\u64cd\u4f5c\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3313.jpg\" alt=\"alt text\" \/><\/p>\n<h2>33.7 MediatR Behaviors<\/h2>\n<p>33.7 MediatR \u884c\u4e3a<\/p>\n<p>Often when we build applications, we have many cross-cutting concerns. These include authorization, validating, and logging.\u200c<br \/>\n\u901a\u5e38\uff0c\u5f53\u6211\u4eec\u6784\u5efa\u5e94\u7528\u7a0b\u5e8f\u65f6\uff0c\u6211\u4eec\u6709\u8bb8\u591a\u6a2a\u5207\u5173\u6ce8\u70b9\u3002\u8fd9\u4e9b\u4f5c\u5305\u62ec authorization\u3001validation \u548c logging\u3002<\/p>\n<p>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.<br \/>\n\u6211\u4eec\u53ef\u4ee5\u4f7f\u7528 Behaviors\uff0c\u800c\u4e0d\u662f\u5728\u6574\u4e2a\u5904\u7406\u7a0b\u5e8f\u4e2d\u91cd\u590d\u8fd9\u4e2a\u903b\u8f91\u3002\u884c\u4e3a\u4e0e ASP.NET Core \u4e2d\u95f4\u4ef6\u975e\u5e38\u76f8\u4f3c\uff0c\u56e0\u4e3a\u5b83\u4eec\u63a5\u53d7\u8bf7\u6c42\uff0c\u6267\u884c\u4e00\u4e9b\u4f5c\uff0c\u7136\u540e\uff08\u53ef\u9009\u5730\uff09\u4f20\u9012\u8bf7\u6c42\u3002<\/p>\n<p>In this section, we are going to use behaviors to perform validation on the DTOs that come from the client.<br \/>\n\u5728\u672c\u8282\u4e2d\uff0c\u6211\u4eec\u5c06\u4f7f\u7528\u884c\u4e3a\u5bf9\u6765\u81ea\u5ba2\u6237\u7aef\u7684 DTO \u6267\u884c\u9a8c\u8bc1\u3002<\/p>\n<p>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.<br \/>\n\u6b63\u5982\u6211\u4eec\u5728\u7b2c 13 \u7ae0\u4e2d\u5b66\u5230\u7684\u90a3\u6837\uff0c\u6211\u4eec\u53ef\u4ee5\u4f7f\u7528\u6570\u636e\u6ce8\u91ca\u5c5e\u6027\u548c ModelState \u5b57\u5178\u6765\u6267\u884c\u9a8c\u8bc1\u3002\u7136\u540e\uff0c\u6211\u4eec\u53ef\u4ee5\u5c06\u9a8c\u8bc1\u903b\u8f91\u63d0\u53d6\u5230\u4f5c\u7b5b\u9009\u5668\u4e2d\u4ee5\u6e05\u9664\u6211\u4eec\u7684\u4f5c\u3002\u597d\u5427\uff0c\u6211\u4eec\u4e5f\u53ef\u4ee5\u5c06\u6240\u6709\u8fd9\u4e9b\u5e94\u7528\u5230\u6211\u4eec\u5f53\u524d\u7684\u89e3\u51b3\u65b9\u6848\u4e2d\u3002<\/p>\n<p>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.<br \/>\n\u4f46\u662f\uff0c\u4e00\u4e9b\u5f00\u53d1\u4eba\u5458\u66f4\u559c\u6b22\u4f7f\u7528 Fluent \u9a8c\u8bc1\u800c\u4e0d\u662f\u6570\u636e\u6ce8\u91ca\u5c5e\u6027\u3002\u5728\u8fd9\u79cd\u60c5\u51b5\u4e0b\uff0c\u884c\u4e3a\u662f\u6267\u884c\u8be5\u9a8c\u8bc1\u903b\u8f91\u7684\u5b8c\u7f8e\u4f4d\u7f6e\u3002<\/p>\n<p>So, let\u2019s 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.<br \/>\n\u56e0\u6b64\uff0c\u8ba9\u6211\u4eec\u4e00\u6b65\u4e00\u6b65\u5730\u5f00\u59cb\uff0c\u9996\u5148\u5728\u6211\u4eec\u7684\u9879\u76ee\u4e2d\u6dfb\u52a0 Fluent \u9a8c\u8bc1\uff0c\u7136\u540e\u4f7f\u7528 behavior \u63d0\u53d6\u9a8c\u8bc1\u9519\u8bef\uff08\u5982\u679c\u6709\uff09\uff0c\u5e76\u5c06\u5b83\u4eec\u8fd4\u56de\u7ed9\u5ba2\u6237\u7aef\u3002<\/p>\n<h3>33.7.1 Adding Fluent Validation\u200c<\/h3>\n<p>33.7.1 \u6dfb\u52a0 Fluent \u9a8c\u8bc1<\/p>\n<p>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\u2019t contain any behavior. We use Queries only for fetching data from the application.<br \/>\nFluentValidation \u5e93\u5141\u8bb8\u6211\u4eec\u8f7b\u677e\u5730\u4e3a\u7c7b\u5b9a\u4e49\u975e\u5e38\u4e30\u5bcc\u7684\u81ea\u5b9a\u4e49\u9a8c\u8bc1\u3002\u7531\u4e8e\u6211\u4eec\u6b63\u5728\u5b9e\u73b0 CQRS\uff0c\u56e0\u6b64\u4e3a\u547d\u4ee4\u5b9a\u4e49\u9a8c\u8bc1\u662f\u6700\u6709\u610f\u4e49\u7684\u3002\u6211\u4eec\u4e0d\u5e94\u8be5\u8d39\u5fc3\u4e3a Queries \u5b9a\u4e49\u9a8c\u8bc1\u5668\uff0c\u56e0\u4e3a\u5b83\u4eec\u4e0d\u5305\u542b\u4efb\u4f55\u884c\u4e3a\u3002\u6211\u4eec\u4ec5\u4f7f\u7528 Queries \u4ece\u5e94\u7528\u7a0b\u5e8f\u83b7\u53d6\u6570\u636e\u3002<\/p>\n<p>So, let\u2019s start by installing the FluentValidation package in the Application project:<br \/>\n\u56e0\u6b64\uff0c\u8ba9\u6211\u4eec\u9996\u5148\u5728 Application \u9879\u76ee\u4e2d\u5b89\u88c5 FluentValidation \u5305\uff1a<\/p>\n<pre><code>PM&gt; install-package FluentValidation.AspNetCore<\/code><\/pre>\n<p>The FluentValidation.AspNetCore package installs both FluentValidation and FluentValidation.DependencyInjectionExtensions packages.<br \/>\nFluentValidation.AspNetCore \u5305\u540c\u65f6\u5b89\u88c5 FluentValidation \u548c FluentValidation.DependencyInjectionExtensions \u5305\u3002<\/p>\n<p>After the installation, we are going to register all the validators inside the service collection by modifying the Program class:<br \/>\n\u5b89\u88c5\u5b8c\u6210\u540e\uff0c\u6211\u4eec\u5c06\u901a\u8fc7\u4fee\u6539 Program \u7c7b\u6765\u6ce8\u518c\u670d\u52a1\u96c6\u5408\u4e2d\u7684\u6240\u6709\u9a8c\u8bc1\u5668\uff1a<\/p>\n<pre><code>builder.Services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assem bly);\nbuilder.Services.AddMediatR(typeof(Application.AssemblyReference).Assembly); builder.Services.AddAutoMapper(typeof(Program));<\/code><\/pre>\n<p>Then, let\u2019s create a new Validators folder inside the Application project and add a new class inside:<br \/>\n\u7136\u540e\uff0c\u8ba9\u6211\u4eec\u5728 Application \u9879\u76ee\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u7684 Validators \u6587\u4ef6\u5939\uff0c\u5e76\u5728\u5176\u4e2d\u6dfb\u52a0\u4e00\u4e2a\u65b0\u7c7b\uff1a<\/p>\n<pre><code>public sealed class CreateCompanyCommandValidator : AbstractValidator&lt;CreateCompanyCommand&gt; {public CreateCompanyCommandValidator() { RuleFor(c =&gt; c.Company.Name).NotEmpty().MaximumLength(60); RuleFor(c =&gt; c.Company.Address).NotEmpty().MaximumLength(60); } }<\/code><\/pre>\n<p>The following using directives are necessary for this class:<br \/>\n\u6b64\u7c7b\u9700\u8981\u4ee5\u4e0b using \u6307\u4ee4\uff1a<\/p>\n<pre><code>using Application.Commands; \nusing FluentValidation;<\/code><\/pre>\n<p>We create the CreateCompanyCommandValidator class that inherits from the <code>AbstractValidator&lt;T&gt;<\/code> 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.<br \/>\n\u6211\u4eec\u521b\u5efa\u4ece <code>AbstractValidator&lt;T&gt;<\/code> \u7c7b\u7ee7\u627f\u7684 CreateCompanyCommandValidator \u7c7b\uff0c\u5e76\u6307\u5b9a\u7c7b\u578b CreateCompanyCommand\u3002\u8fd9\u8ba9 FluentValidation \u77e5\u9053\u6b64\u9a8c\u8bc1\u662f\u9488\u5bf9 CreateCompanyCommand \u8bb0\u5f55\u7684\u3002\u7531\u4e8e\u6b64\u8bb0\u5f55\u5305\u542b\u4e00\u4e2a CompanyForCreationDto \u7c7b\u578b\u7684\u53c2\u6570\uff0c\u8be5\u53c2\u6570\u662f\u6211\u4eec\u5fc5\u987b\u9a8c\u8bc1\u7684\u5bf9\u8c61\uff0c\u56e0\u4e3a\u5b83\u6765\u81ea\u5ba2\u6237\u7aef\uff0c\u56e0\u6b64\u6211\u4eec\u4e3a\u8be5 DTO \u4e2d\u7684\u5c5e\u6027\u6307\u5b9a\u89c4\u5219\u3002<\/p>\n<p>The NotEmpty method specifies that the property can\u2019t be null or empty, and the MaximumLength method specifies the maximum string length of the property.<br \/>\nNotEmpty \u65b9\u6cd5\u6307\u5b9a\u5c5e\u6027\u4e0d\u80fd\u4e3a null \u6216\u4e3a\u7a7a\uff0cMaximumLength \u65b9\u6cd5\u6307\u5b9a\u5c5e\u6027\u7684\u6700\u5927\u5b57\u7b26\u4e32\u957f\u5ea6\u3002<\/p>\n<h3>33.7.2 Creating Decorators with MediatR PipelineBehavior<\/h3>\n<p>33.7.2 \u4f7f\u7528 MediatR PipelineBehavior \u521b\u5efa\u88c5\u9970\u5668<\/p>\n<p>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.\u200c<br \/>\nCQRS \u6a21\u5f0f\u4f7f\u7528\u547d\u4ee4\u548c\u67e5\u8be2\u6765\u4f20\u8fbe\u4fe1\u606f\u5e76\u63a5\u6536\u54cd\u5e94\u3002\u4ece\u672c\u8d28\u4e0a\u8bb2\uff0c\u5b83\u8868\u793a\u4e00\u4e2a\u8bf7\u6c42-\u54cd\u5e94\u7ba1\u9053\u3002\u8fd9\u4f7f\u6211\u4eec\u80fd\u591f\u8f7b\u677e\u5730\u56f4\u7ed5\u901a\u8fc7\u7ba1\u9053\u7684\u6bcf\u4e2a\u8bf7\u6c42\u5f15\u5165\u5176\u4ed6\u884c\u4e3a\uff0c\u800c\u65e0\u9700\u5b9e\u9645\u4fee\u6539\u539f\u59cb\u8bf7\u6c42\u3002<\/p>\n<p>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.<br \/>\n\u60a8\u53ef\u80fd\u719f\u6089\u8fd9\u79cd\u540d\u4e3a Decorator \u6a21\u5f0f\u7684\u6280\u672f\u3002\u4f7f\u7528 Decorator \u6a21\u5f0f\u7684\u53e6\u4e00\u4e2a\u4f8b\u5b50\u662f ASP.NET Core Middleware \u6982\u5ff5\uff0c\u6211\u4eec\u5728 1.8 \u8282\u4e2d\u8ba8\u8bba\u8fc7\u3002<\/p>\n<p>MediatR has a similar concept to middleware, and it is called IPipelineBehavior:<br \/>\nMediatR \u4e0e\u4e2d\u95f4\u4ef6\u7684\u6982\u5ff5\u7c7b\u4f3c\uff0c\u79f0\u4e3a IPipelineBehavior\uff1a<\/p>\n<pre><code>public interface IPipelineBehavior&lt;in TRequest, TResponse&gt; where TRequest : notnull { Task&lt;TResponse&gt; Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate&lt;TResponse&gt; next); }<\/code><\/pre>\n<p>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!<br \/>\n\u7ba1\u9053\u884c\u4e3a\u662f\u8bf7\u6c42\u5b9e\u4f8b\u7684\u5305\u88c5\u5668\uff0c\u4e3a\u6211\u4eec\u7684\u5b9e\u73b0\u63d0\u4f9b\u4e86\u5f88\u5927\u7684\u7075\u6d3b\u6027\u3002\u7ba1\u9053\u884c\u4e3a\u975e\u5e38\u9002\u5408\u5e94\u7528\u7a0b\u5e8f\u4e2d\u7684\u6a2a\u5207\u5173\u6ce8\u70b9\u3002\u6a2a\u5207\u5173\u6ce8\u70b9\u7684\u597d\u4f8b\u5b50\u662f\u65e5\u5fd7\u8bb0\u5f55\u3001\u7f13\u5b58\uff0c\u5f53\u7136\u8fd8\u6709\u9a8c\u8bc1\uff01<\/p>\n<p>Before we use this interface, let\u2019s create a new exception class in the Entities\/Exceptions folder:<br \/>\n\u5728\u4f7f\u7528\u6b64\u63a5\u53e3\u4e4b\u524d\uff0c\u8ba9\u6211\u4eec\u5728 Entities\/Exceptions \u6587\u4ef6\u5939\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u7684\u5f02\u5e38\u7c7b\uff1a<\/p>\n<pre><code>public sealed class ValidationAppException : Exception { public IReadOnlyDictionary&lt;string, string[]&gt; Errors { get; } public ValidationAppException(IReadOnlyDictionary&lt;string, string[]&gt; errors) :base(&quot;One or more validation errors occurred&quot;) =&gt; Errors = errors; }<\/code><\/pre>\n<p>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:<br \/>\n\u63a5\u4e0b\u6765\uff0c\u4e3a\u4e86\u5b9e\u73b0 IPipelineBehavior \u63a5\u53e3\uff0c\u6211\u4eec\u5c06\u5728 Application \u9879\u76ee\u4e2d\u521b\u5efa\u53e6\u4e00\u4e2a\u540d\u4e3a Behaviors \u7684\u6587\u4ef6\u5939\uff0c\u5e76\u5728\u5176\u4e2d\u6dfb\u52a0\u4e00\u4e2a\u7c7b\uff1a<\/p>\n<pre><code>public sealed class ValidationBehavior&lt;TRequest, TResponse&gt; : IPipelineBehavior&lt;TRequest, TResponse&gt; where TRequest : IRequest&lt;TResponse&gt; { private readonly IEnumerable&lt;IValidator&lt;TRequest&gt;&gt; _validators; public ValidationBehavior(IEnumerable&lt;IValidator&lt;TRequest&gt;&gt; validators) =&gt; _validators = validators; public async Task&lt;TResponse&gt; Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate&lt;TResponse&gt; next) { if (!_validators.Any()) return await next(); var context = new ValidationContext&lt;TRequest&gt;(request); var errorsDictionary = _validators .Select(x =&gt; x.Validate(context)) .SelectMany(x =&gt; x.Errors) .Where(x =&gt; x != null) .GroupBy( x =&gt; x.PropertyName.Substring(x.PropertyName.IndexOf(&#039;.&#039;) + 1), x =&gt; x.ErrorMessage,(propertyName, errorMessages) =&gt; new { Key = propertyName, Values = errorMessages.Distinct().ToArray() }) .ToDictionary(x =&gt; x.Key, x =&gt; x.Values); if (errorsDictionary.Any()) throw new ValidationAppException(errorsDictionary); return await next(); } }<\/code><\/pre>\n<p>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.<br \/>\n\u6b64\u7c7b\u5fc5\u987b\u7ee7\u627f\u81ea IPipelineBehavior \u63a5\u53e3\u5e76\u5b9e\u73b0 Handler \u65b9\u6cd5\u3002\u6211\u4eec\u8fd8\u5728\u6784\u9020\u51fd\u6570\u4e2d\u6ce8\u5165\u4e86\u4e00\u7ec4 IValidator \u5b9e\u73b0\u3002FluentValidation \u5e93\u5c06\u626b\u63cf\u6211\u4eec\u7684\u9879\u76ee\u4ee5\u67e5\u627e\u7ed9\u5b9a\u7c7b\u578b\u7684\u6240\u6709 AbstractValidator \u5b9e\u73b0\uff0c\u7136\u540e\u5728\u8fd0\u884c\u65f6\u4e3a\u6211\u4eec\u63d0\u4f9b\u5b9e\u4f8b\u3002\u8fd9\u5c31\u662f\u6211\u4eec\u5982\u4f55\u5e94\u7528\u6211\u4eec\u5728\u9879\u76ee\u4e2d\u5b9e\u73b0\u7684\u5b9e\u9645\u9a8c\u8bc1\u5668\u3002<\/p>\n<p>Then, if there are no validation errors, we just call the next delegate to allow the execution of the next component in the middleware.<br \/>\n\u7136\u540e\uff0c\u5982\u679c\u6ca1\u6709\u9a8c\u8bc1\u9519\u8bef\uff0c\u6211\u4eec\u53ea\u8c03\u7528 next \u59d4\u6258\uff0c\u4ee5\u5141\u8bb8\u5728\u4e2d\u95f4\u4ef6\u4e2d\u6267\u884c next \u7ec4\u4ef6\u3002<\/p>\n<p>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.<br \/>\n\u4f46\u662f\u5982\u679c\u6709\u4efb\u4f55\u9519\u8bef\uff0c\u6211\u4eec\u4f1a\u4ece _validators \u96c6\u5408\u4e2d\u63d0\u53d6\u5b83\u4eec\uff0c\u5e76\u5728\u5b57\u5178\u4e2d\u5bf9\u5b83\u4eec\u8fdb\u884c\u5206\u7ec4\u3002\u5982\u679c\u5b57\u5178\u4e2d\u6709\u6761\u76ee\uff0c\u5219\u629b\u51fa ValidationAppException \u5e76\u4f20\u9012\u5e26\u6709\u9519\u8bef\u7684\u5b57\u5178\u3002\u8fd9\u4e2a\u5f02\u5e38\u5c06\u5728\u6211\u4eec\u7684\u5168\u5c40\u9519\u8bef\u5904\u7406\u7a0b\u5e8f\u4e2d\u6355\u83b7\uff0c\u6211\u4eec\u5c06\u5728\u4e00\u5206\u949f\u5185\u5bf9\u5176\u8fdb\u884c\u4fee\u6539\u3002<\/p>\n<p>But before we do that, we have to register this behavior in the Program class:<br \/>\n\u4f46\u5728\u6b64\u4e4b\u524d\uff0c\u6211\u4eec\u5fc5\u987b\u5728 Program \u7c7b\u4e2d\u6ce8\u518c\u6b64\u884c\u4e3a\uff1a<\/p>\n<pre><code>builder.Services.AddMediatR(typeof(Application.AssemblyReference).Assembly); builder.Services.AddAutoMapper(typeof(Program)); builder.Services.AddTransient(typeof(IPipelineBehavior&lt;,&gt;), typeof(ValidationBehavior&lt;,&gt;)); builder.Services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly);<\/code><\/pre>\n<p>After that, we can modify the ExceptionMiddlewareExtensions class:<br \/>\n\u4e4b\u540e\uff0c\u6211\u4eec\u53ef\u4ee5\u4fee\u6539 ExceptionMiddlewareExtensions \u7c7b\uff1a<\/p>\n<pre><code>public static class ExceptionMiddlewareExtensions\n{ public static void ConfigureExceptionHandler(this WebApplication app, ILoggerManager logger) { app.UseExceptionHandler(appError =&gt; { appError.Run(async context =&gt; { context.Response.ContentType = &quot;application\/json&quot;; var contextFeature = context.Features.Get&lt;IExceptionHandlerFeature&gt;(); if (contextFeature != null) { context.Response.StatusCode = contextFeature.Error switch { NotFoundException =&gt; StatusCodes.Status404NotFound, BadRequestException =&gt; StatusCodes.Status400BadRequest, ValidationAppException =&gt; StatusCodes.Status422UnprocessableEntity, _ =&gt; StatusCodes.Status500InternalServerError }; logger.LogError($&quot;Something went wrong: {contextFeature.Error}&quot;); 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()); } } }); }); } }<\/code><\/pre>\n<p>So we modify the switch statement to check for the ValidationAppException type and to assign a proper status code 422.<br \/>\n\u56e0\u6b64\uff0c\u6211\u4eec\u4fee\u6539 switch \u8bed\u53e5\u4ee5\u68c0\u67e5 ValidationAppException \u7c7b\u578b\u5e76\u5206\u914d\u6b63\u786e\u7684\u72b6\u6001\u4ee3\u7801 422\u3002<\/p>\n<p>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.<br \/>\n\u7136\u540e\uff0c\u6211\u4eec\u4f7f\u7528\u58f0\u660e\u6a21\u5f0f\u6765\u6d4b\u8bd5\u53d8\u91cf\u7684\u7c7b\u578b\uff0c\u5e76\u5c06\u5176\u5206\u914d\u7ed9\u540d\u4e3a exception \u7684\u65b0\u53d8\u91cf\u3002\u5982\u679c\u7c7b\u578b\u662f ValidationAppException\uff0c\u6211\u4eec\u53ea\u5c06\u54cd\u5e94\u5199\u5165\u5ba2\u6237\u7aef\uff0c\u63d0\u4f9b\u6211\u4eec\u7684 errors \u5b57\u5178\u4f5c\u4e3a\u53c2\u6570\u3002\u5426\u5219\uff0c\u6211\u4eec\u5c06\u505a\u4e0e\u73b0\u5728\u76f8\u540c\u7684\u4e8b\u60c5\u3002<\/p>\n<p>Now, we can test this by sending an invalid request:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u901a\u8fc7\u53d1\u9001\u65e0\u6548\u8bf7\u6c42\u6765\u6d4b\u8bd5\u8fd9\u4e00\u70b9\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\">https:\/\/localhost:5001\/api\/companies<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3314.jpg\" alt=\"alt text\" \/><\/p>\n<p>Excellent, this works great.<br \/>\n\u592a\u597d\u4e86\uff0c\u8fd9\u6548\u679c\u5f88\u597d\u3002<\/p>\n<p>Additionally, if the Address property has too many characters, we will see a different message:<br \/>\n\u6b64\u5916\uff0c\u5982\u679c Address \u5c5e\u6027\u7684\u5b57\u7b26\u592a\u591a\uff0c\u6211\u4eec\u5c06\u770b\u5230\u4e00\u6761\u4e0d\u540c\u7684\u6d88\u606f\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3315.jpg\" alt=\"alt text\" \/><\/p>\n<p>Great.<br \/>\n\u4f1f\u5927\u3002<\/p>\n<h2>33.7.3 Validating null Object\u200c<\/h2>\n<p>33.7.3 \u9a8c\u8bc1 null \u5bf9\u8c61<\/p>\n<p>Now, if we send a request with an empty request body, we are going to get the result produced from our action:<br \/>\n\u73b0\u5728\uff0c\u5982\u679c\u6211\u4eec\u53d1\u9001\u4e00\u4e2a\u8bf7\u6c42\u6b63\u6587\u4e3a\u7a7a\u7684\u8bf7\u6c42\uff0c\u6211\u4eec\u5c06\u5f97\u5230\u6211\u4eec\u7684\u4f5c\u751f\u6210\u7684\u7ed3\u679c\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\">https:\/\/localhost:5001\/api\/companies<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3316.jpg\" alt=\"alt text\" \/><\/p>\n<p>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:<br \/>\n\u6211\u4eec\u53ef\u4ee5\u770b\u5230 400 \u72b6\u6001\u4ee3\u7801\u548c\u9519\u8bef\u6d88\u606f\u3002\u8fd9\u5b8c\u5168\u6ca1\u95ee\u9898\uff0c\u56e0\u4e3a\u5982\u679c\u4ece\u5ba2\u6237\u7aef\u53d1\u9001\u7684\u5bf9\u8c61\u4e3a null\uff0c\u6211\u4eec\u5e0c\u671b\u5f97\u5230 Bad Request \u54cd\u5e94\u3002\u4f46\u662f\uff0c\u5982\u679c\u51fa\u4e8e\u4efb\u4f55\u539f\u56e0\uff0c\u60a8\u5e0c\u671b\u4ece\u4f5c\u4e2d\u5220\u9664\u8be5\u9a8c\u8bc1\uff0c\u5e76\u4f7f\u7528 Fluent \u9a8c\u8bc1\u89c4\u5219\u5904\u7406\u5b83\uff0c\u5219\u53ef\u4ee5\u901a\u8fc7\u4fee\u6539 CreateCompanyCommandValidator \u7c7b\u5e76\u91cd\u5199 Validate \u65b9\u6cd5\u6765\u6267\u884c\u6b64\u4f5c\uff1a<\/p>\n<pre><code>public sealed class CreateCompanyCommandValidator : AbstractValidator&lt;CreateCompanyCommand&gt; { public CreateCompanyCommandValidator() { RuleFor(c =&gt; c.Company.Name).NotEmpty().MaximumLength(60); RuleFor(c =&gt; c.Company.Address).NotEmpty().MaximumLength(60); } public override ValidationResult Validate(ValidationContext&lt;CreateCompanyCommand&gt; context) { return context.InstanceToValidate.Company is null ? new ValidationResult(new[] { new ValidationFailure(&quot;CompanyForCreationDto&quot;, &quot;CompanyForCreationDto object is null&quot;) }) : base.Validate(context); } }<\/code><\/pre>\n<p>Now, you can remove the validation check inside the action and send a null body request:<br \/>\n\u73b0\u5728\uff0c\u60a8\u53ef\u4ee5\u5220\u9664\u4f5c\u4e2d\u7684\u9a8c\u8bc1\u68c0\u67e5\u5e76\u53d1\u9001 null \u6b63\u6587\u8bf7\u6c42\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/3317.jpg\" alt=\"alt text\" \/><\/p>\n<p>Pay attention that now the status code is 422 and not 400. But this validation is now part of the fluent validation.<br \/>\n\u8bf7\u6ce8\u610f\uff0c\u73b0\u5728\u72b6\u6001\u4ee3\u7801\u662f 422 \u800c\u4e0d\u662f 400\u3002\u4f46\u6b64\u9a8c\u8bc1\u73b0\u5728\u662f Fluent \u9a8c\u8bc1\u7684\u4e00\u90e8\u5206\u3002<\/p>\n<p>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.<br \/>\n\u5982\u679c\u6b64\u89e3\u51b3\u65b9\u6848\u9002\u5408\u60a8\u7684\u9879\u76ee\uff0c\u8bf7\u968f\u610f\u4f7f\u7528\u3002\u6211\u4eec\u5efa\u8bae\u4ec5\u5bf9\u9a8c\u8bc1\u9519\u8bef\u4f7f\u7528 422\uff0c\u5982\u679c\u8bf7\u6c42\u6b63\u6587\u4e3a null\uff0c\u5219\u4f7f\u7528 400\u3002<\/p>\n","protected":false},"excerpt":{"rendered":"<p>33 BONUS 2 &#8211; INTRODUCTION TO CQRS AND MEDIATR WITH ASP.NET CORE WEB API 33 \u5956\u52b1 2 &#8211; \u4f7f\u7528 ASP.NET \u6838\u5fc3 WEB API \u7684 CQRS \u548c MEDIATR \u7b80\u4ecb 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.\u200c \u5728\u672c\u7ae0\u4e2d\uff0c\u6211\u4eec\u5c06\u4ecb\u7ecd CQRS [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1164","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1164","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1164"}],"version-history":[{"count":0,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1164\/revisions"}],"wp:attachment":[{"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1164"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1164"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1164"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}