├── Quickly-Implement-An-Email-Delivery-Gift-Card-Store-With-ABP-VNext-Framework ├── zh │ └── article.md └── en │ └── article.md ├── Install-Modules-With-One-Click-Via-AbpHelper-GUI └── en │ ├── images │ ├── Create-An-App.gif │ ├── Find-Depends-On.png │ ├── Install-Modules.gif │ └── Use-Setting-Ui.png │ └── article.md ├── Introduce-DTM-For-Multi-Tenant-Multi-Database-Scene ├── en │ ├── images │ │ ├── cover-photo.png │ │ └── cover-photo.psd │ └── article.md └── zh │ ├── images │ ├── cover-photo.png │ └── cover-photo.psd │ └── article.md ├── Reuse-ABP-VNext-Modules-To-Quickly-Implement-Application-Features ├── en │ ├── images │ │ ├── HomePage.png │ │ └── SendToSelf.png │ └── article.md └── zh │ ├── images │ ├── HomePage.png │ └── SendToSelf.png │ └── article.md ├── Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes ├── en │ ├── images │ │ ├── HomePage.png │ │ ├── ContactList.png │ │ ├── CreateContact.png │ │ └── CrudCodeGenerator.png │ └── article.md └── zh │ ├── images │ ├── HomePage.png │ ├── ContactList.png │ ├── CreateContact.png │ └── CrudCodeGenerator.png │ └── article.md ├── Use-Stepping-To-Perform-Atomic-Multi-Step-Operations ├── zh │ └── article.md └── en │ └── article.md └── Notice-And-Solve-ABP-Distributed-Events-Disordering ├── zh └── article.md └── en └── article.md /Quickly-Implement-An-Email-Delivery-Gift-Card-Store-With-ABP-VNext-Framework/zh/article.md: -------------------------------------------------------------------------------- 1 | # 用 ABP vNext 框架快速实现邮件自动发货的礼品卡商城 -------------------------------------------------------------------------------- /Install-Modules-With-One-Click-Via-AbpHelper-GUI/en/images/Create-An-App.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Install-Modules-With-One-Click-Via-AbpHelper-GUI/en/images/Create-An-App.gif -------------------------------------------------------------------------------- /Install-Modules-With-One-Click-Via-AbpHelper-GUI/en/images/Find-Depends-On.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Install-Modules-With-One-Click-Via-AbpHelper-GUI/en/images/Find-Depends-On.png -------------------------------------------------------------------------------- /Install-Modules-With-One-Click-Via-AbpHelper-GUI/en/images/Install-Modules.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Install-Modules-With-One-Click-Via-AbpHelper-GUI/en/images/Install-Modules.gif -------------------------------------------------------------------------------- /Install-Modules-With-One-Click-Via-AbpHelper-GUI/en/images/Use-Setting-Ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Install-Modules-With-One-Click-Via-AbpHelper-GUI/en/images/Use-Setting-Ui.png -------------------------------------------------------------------------------- /Introduce-DTM-For-Multi-Tenant-Multi-Database-Scene/en/images/cover-photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Introduce-DTM-For-Multi-Tenant-Multi-Database-Scene/en/images/cover-photo.png -------------------------------------------------------------------------------- /Introduce-DTM-For-Multi-Tenant-Multi-Database-Scene/en/images/cover-photo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Introduce-DTM-For-Multi-Tenant-Multi-Database-Scene/en/images/cover-photo.psd -------------------------------------------------------------------------------- /Introduce-DTM-For-Multi-Tenant-Multi-Database-Scene/zh/images/cover-photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Introduce-DTM-For-Multi-Tenant-Multi-Database-Scene/zh/images/cover-photo.png -------------------------------------------------------------------------------- /Introduce-DTM-For-Multi-Tenant-Multi-Database-Scene/zh/images/cover-photo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Introduce-DTM-For-Multi-Tenant-Multi-Database-Scene/zh/images/cover-photo.psd -------------------------------------------------------------------------------- /Reuse-ABP-VNext-Modules-To-Quickly-Implement-Application-Features/en/images/HomePage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Reuse-ABP-VNext-Modules-To-Quickly-Implement-Application-Features/en/images/HomePage.png -------------------------------------------------------------------------------- /Reuse-ABP-VNext-Modules-To-Quickly-Implement-Application-Features/zh/images/HomePage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Reuse-ABP-VNext-Modules-To-Quickly-Implement-Application-Features/zh/images/HomePage.png -------------------------------------------------------------------------------- /Reuse-ABP-VNext-Modules-To-Quickly-Implement-Application-Features/en/images/SendToSelf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Reuse-ABP-VNext-Modules-To-Quickly-Implement-Application-Features/en/images/SendToSelf.png -------------------------------------------------------------------------------- /Reuse-ABP-VNext-Modules-To-Quickly-Implement-Application-Features/zh/images/SendToSelf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Reuse-ABP-VNext-Modules-To-Quickly-Implement-Application-Features/zh/images/SendToSelf.png -------------------------------------------------------------------------------- /Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/en/images/HomePage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/en/images/HomePage.png -------------------------------------------------------------------------------- /Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/zh/images/HomePage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/zh/images/HomePage.png -------------------------------------------------------------------------------- /Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/en/images/ContactList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/en/images/ContactList.png -------------------------------------------------------------------------------- /Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/zh/images/ContactList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/zh/images/ContactList.png -------------------------------------------------------------------------------- /Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/en/images/CreateContact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/en/images/CreateContact.png -------------------------------------------------------------------------------- /Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/zh/images/CreateContact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/zh/images/CreateContact.png -------------------------------------------------------------------------------- /Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/en/images/CrudCodeGenerator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/en/images/CrudCodeGenerator.png -------------------------------------------------------------------------------- /Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/zh/images/CrudCodeGenerator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdlcf88/Articles/HEAD/Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/zh/images/CrudCodeGenerator.png -------------------------------------------------------------------------------- /Install-Modules-With-One-Click-Via-AbpHelper-GUI/en/article.md: -------------------------------------------------------------------------------- 1 | # Install Modules With One Click via AbpHelper GUI 2 | 3 | ## Introduction 4 | 5 | In this article, we will use the AbpHelper GUI to install the [EasyAbp.Abp.SettingUi](https://github.com/EasyAbp/Abp.SettingUi) module into your ABP application. 6 | 7 | ## Pre-Requirements 8 | 9 | The following tools should be installed on your development machine: 10 | 11 | * [.NET Core 5.0+](https://www.microsoft.com/net/download/dotnet-core/) 12 | * [VS Code](https://code.visualstudio.com/) or another IDE 13 | * [AbpHelper GUI 1.0+](https://github.com/EasyAbp/AbpHelper.GUI/releases) 14 | 15 | ## Creating a New Solution 16 | 17 | Open the AbpHelper GUI, switch to the "ABP CLI" tab, and create a new app: 18 | 19 | ![CreateAnApp](./images/Create-An-App.gif) 20 | 21 | ## Install the SettingUi Modules 22 | 23 | Open the new app solution in the AbpHelper GUI and install the modules: 24 | 25 | ![InstallModules](./images/Install-Modules.gif) 26 | 27 | Then we will find that the `DependsOn`s have been added: 28 | 29 | ![FindDependsOn](./images/Find-Depends-On.png) 30 | 31 | ## Run the App 32 | 33 | > See https://docs.abp.io/en/abp/latest/Getting-Started-Running-Solution to learn more about how to run an ABP solution. 34 | 35 | Run the commands: 36 | 37 | ```bash 38 | cd C:\Temp\TryAbpHelper\src\TryAbpHelper.DbMigrator 39 | dotnet run 40 | cd ..\TryAbpHelper.Web 41 | dotnet run 42 | ``` 43 | 44 | Log in and try the Setting UI: 45 | 46 | ![UseSettingUi](./images/Use-Setting-Ui.png) 47 | 48 | > Use the default username `admin` and password `1q2w3E*` to log in to the app. 49 | 50 | ## Conclusion 51 | 52 | AbpHelper is helpful for us to manage the application's modules. 53 | 54 | We can use it to install not only Volosoft's modules but also the community modules. 55 | 56 | The module management feature of the AbpHelper depends on EasyAbp's [module library](https://github.com/EasyAbp/ModuleLibrary), welcome to register your developed modules through PR. -------------------------------------------------------------------------------- /Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/zh/article.md: -------------------------------------------------------------------------------- 1 | # 五分钟完成 ABP vNext 通讯录 App 开发 2 | 3 | ABP vNext(后文简称Abp)是土耳其 Volosoft 公司艺术品级的应用开发框架,它基于领域驱动设计(DDD)的思维,创新地采用了模块化开发的设计。Abp 目前无疑是 ASP.NET Core 开发框架中最先进和最优雅的存在。 4 | 5 | 笔者认为,凭借绝妙的模块化开发的设计和丝滑的开发体验,Abp 在 ASP.NET Core 的地位有望达到 Spring 在 Java 的地位。 6 | 7 | ## 模块开发与应用开发的关系 8 | 9 | 使用 Abp 框架,你可以提前制作一些功能模块,例如微信登录、私信、博客、论坛等模块,将它们打包备用。在开发具体的应用时,你可以轻松将模块安装到你的工程中,节省了大量的重复性工作。 10 | 11 | 除了自己造轮子,你还可以在 NuGet 上安装由开源社区维护的模块,当然,社区也在等待你的贡献。 12 | 13 | ## 开始通讯录 App 的开发 14 | 15 | 今天我们不讲模块开发,而是从最简单的应用开发入手,笔者将遵循 Abp 最佳实践,带你体验如何在 5 分钟内,使用 Abp 框架开发一个通讯录 App。 16 | 17 | ### 第一步:使用 ABP CLI 生成项目 18 | 19 | 1. 命令行安装 ABP CLI:`dotnet tool install -g Volo.Abp.Cli` 20 | 21 | 2. 命令行生成通讯录 App 项目:`abp new AddressBook`(将在当前目录中生成项目) 22 | 23 | ### 第二步:创建“联系人”实体 24 | 25 | 在 Abp 中,联系人应为聚合根 AggregateRoot,详细请参考 [Abp 官方文档](https://docs.abp.io/en/abp/latest/Domain-Driven-Design)中关于领域驱动设计(DDD)的介绍。 26 | 27 | 1. 新建 `aspnet-core/src/AddressBook.Domain/Contacts` 目录 28 | 29 | 2. 在目录下手动创建 `Contact.cs` 文件 30 | 31 | ```csharp 32 | public class Contact : AggregateRoot 33 | { 34 | public virtual string Name { get; protected set; } 35 | 36 | public virtual string PhoneNumber { get; protected set; } 37 | 38 | public virtual string Address { get; protected set; } 39 | 40 | public virtual byte? Age { get; protected set; } 41 | 42 | public virtual DateTime? Birthday { get; protected set; } 43 | 44 | // 构造函数将会稍后被自动生成,不需要在此处手动添加 45 | } 46 | ``` 47 | 48 | 3. 运行 `AddressBook.DbMigrator` 项目,这是为了在数据库中为我们的应用建立基础结构和数据 49 | 50 | ### 第三步:使用代码生成器生成剩余代码 51 | 52 | 本文使用的是 EasyAbp 开源的 [AbpHelper GUI](https://easyabp.io/abphelper/AbpHelper.GUI) 生成代码,如果你是 ABP 商业版用户,你还可以选择 [ABP Suite](https://commercial.abp.io/tools/suite) 53 | 54 | 1. 下载 AbpHelper GUI:https://github.com/EasyAbp/AbpHelper.GUI/releases 55 | 56 | 2. 使用 **CRUD Code Generator** 功能,一键生成与 Contact 相关的全部代码 57 | 58 | ![CrudCodeGenerator](images/CrudCodeGenerator.png) 59 | 60 | 如果你是第一次使用 AbpHelper GUI,请通过左侧导航菜单的 `Install or update AbpHelper CLI` 安装 AbpHelper CLI。如果你更习惯命令行操作,可以直接使用 [AbpHelper CLI](https://github.com/EasyAbp/AbpHelper.CLI) 完成前面的工作。 61 | 62 | ### 第四步:启动应用 63 | 64 | 1. 启动 AddressBook.Web 项目 65 | 66 | ![HomePage](images/HomePage.png) 67 | 68 | 2. 登录并使用通讯录应用(admin 用户的默认密码是 `1q2w3E*`) 69 | 70 | ![CreateContact](images/CreateContact.png) 71 | ![ContactList](images/ContactList.png) 72 | 73 | 你一定注意到了,表单已被 [abp-dynamic-form](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Tag-Helpers/Dynamic-Forms) tag helper 自动生成。并且,你只需要简单的修改本地化 JSON 文件,就能显示出中文词汇,这里我们不做演示。 74 | 75 | 3. Contact 的 RESTful API 也已经自动生成,如果需要它们,访问路由 `/swagger` 76 | 77 | ## 后记 78 | 79 | 我们的通讯录应用天然包含:用户权限角色管理、多租户 SaaS 支持,如果你打算系统的学习 Abp 框架,请阅读[官方文档](https://docs.abp.io)。 80 | 81 | 文中使用的 AbpHelper 是由国内爱好者创建的 EasyAbp 开源组织制作的开发工具集,能明显提高你的开发效率,并且完全免费。此外,EasyAbp 还提供了很多实用的模块,你可以查看 [EasyAbp Guide](https://github.com/EasyAbp/EasyAbpGuide) 了解更多信息。 82 | 83 | ## 下一篇 84 | 85 | 下一篇,笔者将会介绍,如何给通讯录应用安装私信模块。此模块由 EasyAbp 组织开发并持续维护,你甚至可以在商业项目中免费使用它。 86 | -------------------------------------------------------------------------------- /Use-Stepping-To-Perform-Atomic-Multi-Step-Operations/zh/article.md: -------------------------------------------------------------------------------- 1 | # 使用Stepping.NET轻松执行多步原子操作 2 | 3 | Stepping 是一个基于 [BASE](https://en.wikipedia.org/wiki/Eventual_consistency) 的分布式作业实现。它可以作为工作流引擎,事件收/发件箱,用于邮箱/短信发送,用于远程接口调用等场景。 4 | 5 | 我们已为以下语言提供了文档:[English](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/README.md),[简体中文](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/README.zh-CN.md)。 6 | 7 | ## Stepping 中 `Job` 和 `Step` 是什么? 8 | 9 | `Job` 是一个分布式事务单元,而 `Step` 是 job 中一个特定的任务。 10 | 11 | 一个 job(作业)包含了一个或多个 step(步骤),事务管理器会按顺序执行步骤。如果步骤 1 失败了,它将重试直到成功,然后开始执行步骤 2。 12 | 13 | ## 什么场景需要 Stepping 14 | 15 | [![WhatCanSteppingDo](https://user-images.githubusercontent.com/30018771/190923267-38cae2ff-29de-4219-bd7f-423ff6cb98f5.png)](https://excalidraw.com/#json=5PXRUbpKnk6rBiEz5zebr,_AUzbfwUZM24qqCcBoOsUw) 16 | 17 | ### 需要执行多个步骤且确保原子性 18 | 19 | 当一个 job 开始执行,Stepping 最终会完成你布置的所有 steps。如果你的应用在执行这些步骤期间挂了,事务管理器会在应用恢复后,继续执行剩下的步骤。 20 | 21 | Stepping 会按顺序挨个完成你布置的 steps。如果一个步骤失败,它会被推迟重试,这确保了 job 的 [原子性](https://coffeecodeclimb.com/2020/07/26/atomicity-and-idempotency-for-dummies/#atomicity)。请确保你所有的 step 都能在重试后最终成功,除非它是一个 [Saga step](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/Steps.md#saga-step)。 22 | 23 | 当你的应用在执行步骤期间挂了,Stepping 有可能已经实际完成了这个步骤,而未自知,当你的应用恢复,Stepping 会冗余地执行这个步骤。因此,你所有的步骤都应该做到 [幂等](https://coffeecodeclimb.com/2020/07/26/atomicity-and-idempotency-for-dummies/#idempotence)。 24 | 25 | ### 需要确保在 DB 事务提交后,后续步骤一定执行 26 | 27 | 当一个绑定了 DB 事务的 job 开始执行,在 DB 事务提交后,Stepping 最终会完成你布置的所有 steps。 28 | 29 | 你无需担心在 DB 事务提交后、后续步骤执行之前,这期间应用挂了导致的非原子性问题。我们已经使用 DTM 的 [二阶段消息](https://en.dtm.pub/practice/msg.html) 模式处理了这种情况。 30 | 31 | Stepping 也支持“多租户且多数据库”的场景,这意味着无论你的应用有多少个不同的数据库,都不成问题。 32 | 33 | ## 用例 34 | 35 | 事务管理器会最终完成添加的步骤: 36 | 37 | ```csharp 38 | var job = await distributedJobFactory.CreateJobAsync(); 39 | 40 | job.AddStep(new RequestBank1TransferOutStep(args)); // 带参数的步骤 41 | job.AddStep(); // 不带参数的步骤 42 | 43 | await job.StartAsync(); 44 | ``` 45 | 46 | [Steps 文档](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/Steps.md) 介绍了如何定义一个步骤。 47 | 48 | 如果你希望在 DB 事务提交后开始执行一些步骤,并且确保它们最终能够执行成功: 49 | 50 | ```csharp 51 | var db = serviceProvider.GetRequiredService(); // 以 EF Core 举例 52 | await db.Database.BeginTransactionAsync(); 53 | 54 | var order = new Order(args); 55 | 56 | db.Orders.Add(order); 57 | await db.SaveChangesAsync(); 58 | 59 | var job = await distributedJobFactory.CreateJobAsync(new EfCoreSteppingDbContext(db)); 60 | 61 | job.AddStep(new SendOrderCreatedEmailStep(order)); 62 | job.AddStep(new SendOrderCreatedSmsStep(order)); 63 | 64 | await job.StartAsync(); // 这个方法也会提交 DB 事务 65 | ``` 66 | 67 | Stepping 支持 `EF Core`,`ADO.NET`(即将到来),及 `MongoDB`。 68 | 69 | 了解更多信息,请参阅 [用法文档](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/Usage.md)。 70 | 71 | ## 安装 72 | 73 | 请参阅 [安装文档](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/Installation.md)。 74 | 75 | ## 支持的事务管理器 76 | 77 | Stepping 要求使用事务管理器。你可以选择一种你喜欢的事务管理器。 78 | 79 | ### DTM Server 80 | 81 | DTM 是一个成熟的事务管理器,并且能够为 Stepping 提供能力。选择 DTM 你将可以使用更多的分布式事务模式,例如 Saga、TCC和XA。 82 | 83 | 请参阅 [DTM 文档](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/Dtm.md)。 84 | 85 | ### Local-TM 86 | 87 | Stepping 提供了一种简单的内置事务管理器实现。Local-TM 与你的应用一起运行。在这种模式下,每个应用都作为自己发布的 jobs 的事务管理器。 88 | 89 | 请参阅 [Local-TM 文档](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/LocalTm.md)。 90 | -------------------------------------------------------------------------------- /Reuse-ABP-VNext-Modules-To-Quickly-Implement-Application-Features/zh/article.md: -------------------------------------------------------------------------------- 1 | # 复用 ABP vNext 的模块以快速实现应用的功能 2 | 3 | 在[上一篇](../../Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/zh/article.md)中,我们用完成了通讯录 App 的基本功能开发。 4 | 5 | 本篇,我们会给通讯录 App 安装私信模块,使用户能够相互发送私信,并接收新私信的通知。 6 | 7 | 在私信模块的设计里,用户可以发送私信给自己,但在本篇的最后,我们还要“改装”私信模块,从而禁止用户发送私信给自己。 8 | 9 | ## 什么是“模块”? 10 | 11 | 使用 Abp 框架,你可以提前制作一些功能模块,例如微信登录、私信、博客、论坛等模块,将它们打包备用。在开发具体的应用时,你可以轻松将模块安装到你的工程中,节省了大量的重复性工作。 12 | 13 | 除了自己造轮子,你还可以在 NuGet 上安装由开源社区维护的模块,当然,社区也在等待你的贡献。 14 | 15 | ## 安装私信模块 16 | 17 | 我们用到的私信应用模块(EasyAbp.PrivateMessaging)由 [EasyAbp](https://easyabp.io) 组织开发并持续维护,你甚至可以在商业项目中免费使用它。 18 | 19 | 现在,我们将一步步给通讯录 App 安装上私信模块。 20 | 21 | ### 第一步:使用 NuGet 安装模块包 22 | 23 | 1. 将 NuGet 包 `EasyAbp.PrivateMessaging.Application`,安装到 `AddressBook.Application` 项目 24 | 2. 将 NuGet 包 `EasyAbp.PrivateMessaging.Application.Contracts`,安装到 `AddressBook.Application.Contracts` 项目 25 | 3. 将 NuGet 包 `EasyAbp.PrivateMessaging.Domain`,安装到 `AddressBook.Domain` 项目 26 | 4. 将 NuGet 包 `EasyAbp.PrivateMessaging.Domain.Shared`,安装到 `AddressBook.Domain.Shared` 项目 27 | 5. 将 NuGet 包 `EasyAbp.PrivateMessaging.EntityFrameworkCore`,安装到 `AddressBook.EntityFrameworkCore` 项目 28 | 6. 将 NuGet 包 `EasyAbp.PrivateMessaging.HttpApi`,安装到 `AddressBook.HttpApi` 项目 29 | 7. 将 NuGet 包 `EasyAbp.PrivateMessaging.HttpApi.Client`,安装到 `AddressBook.HttpApi.Client` 项目 30 | 8. 将 NuGet 包 `EasyAbp.PrivateMessaging.Web`,安装到 `AddressBook.Web` 项目(如果你不需要 UI,可以跳过这一步) 31 | 32 | ### 第二步:添加模块依赖和配置 33 | 34 | 1. 分别在以上项目的 Module 类中添加私信模块的依赖,例如:在 AddressBook.Application 项目的 AddressBookApplicationModule.cs 中给类添加特性 `[DependsOn(PrivateMessagingApplicationModule)]`,以此类推 35 | 36 | 2. 在 AddressBook.EntityFrameworkCore.DbMigrations 项目的 AddressBookMigrationsDbContext.cs 中找到 OnModelCreating 方法,在它里面添加代码 `builder.ConfigurePrivateMessaging();`,使私信模块的 EF Core 迁移能够进行 37 | 38 | ### 第三步:创建 EF Core 迁移并更新数据库 39 | 40 | 1. 在 AddressBook.EntityFrameworkCore.DbMigrations 项目目录中打开命令行,执行命令 `dotnet ef migrations add Installed_Pm_Module -s ../AddressBook.DbMigrator` 41 | 42 | 2. 运行 AddressBook.DbMigrator 项目,它将自动完成数据库的更新 43 | 44 | 如果你想对这一步骤了解更多,请阅读 [Abp 官方文档](https://docs.abp.io/en/abp/latest/Tutorials/Part-1#add-database-migration)。 45 | 46 | ### 第四步:启动应用 47 | 48 | 启动 AddressBook.Web 项目,我们可以看到私信模块已经安装成功了。 49 | 50 | ![HomePage](images/HomePage.png) 51 | 52 | ## 改进模块:禁止用户发私信给自己 53 | 54 | Abp 允许我们重写模块的代码,请在 AddressBook.Application 项目中新建文件 `MyPrivateMessageAppService.cs`: 55 | 56 | ```csharp 57 | [Dependency(ReplaceServices = true)] 58 | public class MyPrivateMessageAppService : PrivateMessageAppService 59 | { 60 | // ctor 61 | 62 | public override Task CreateAsync(CreateUpdatePrivateMessageDto input) 63 | { 64 | if (input.ToUserName == CurrentUser.UserName) 65 | { 66 | throw new UserFriendlyException("请勿给自己发消息"); 67 | } 68 | 69 | return base.CreateAsync(input); 70 | } 71 | } 72 | ``` 73 | 74 | 此时如果用户发送邮件给自己,会看到错误提示信息: 75 | 76 | ![SendToSelf](images/SendToSelf.png) 77 | 78 | 得益于 Abp 框架高可扩展性的模块化设计,JS 和 CSS 等静态文件以及页面均可被重写,请阅读官方文档以了解更多用法:[Customizing the Existing Modules](https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Guide) 79 | 80 | ## 后记 81 | 82 | 如果你希望进一步了解 PrivateMessaging 模块,请阅读它的[文档](https://easyabp.io/modules/PrivateMessaging)。 83 | 84 | 顺便一提,EasyAbp 组织还有更多强大又实用的模块,例如:EShop、PaymentService、GiftCardManagement,它们可以真正提升你的开发效率,并且加强应用程序的可靠性。如果你感兴趣,请阅读 [EasyAbp Guide](https://github.com/EasyAbp/EasyAbpGuide) 了解更多。 85 | 86 | 笔者也期待将来 Abp 官方模块商城 market.abp.io 正式上线,提供社区模块的展示、检索和自动安装功能,从而使得我们安装模块更加容易。 87 | 88 | ## 下一篇 89 | 90 | 下一篇,我们将通过一个简单的改动,让我们的通讯录 App 升级为多租户 SaaS 应用。 91 | -------------------------------------------------------------------------------- /Quickly-Implement-An-Email-Delivery-Gift-Card-Store-With-ABP-VNext-Framework/en/article.md: -------------------------------------------------------------------------------- 1 | # Quickly implement an email delivery gift card store with ABP vNext framework 2 | 3 | We will spend about 15 minutes to complete the development of a gift card store application with email delivery. 4 | 5 | Yes, you read it right! We only need a short 15 minutes, thanks to the excellent modular design of [ABP framework](https://abp.io) and the [EShop](https://easyabp.io/modules/EShop) module group provided by EasyAbp organization. 6 | 7 | ## Step 1: Create an EasyMall Application 8 | 9 | EasyMall is a startup template for the ABP framework to quickly create an application with pre-installed EShop modules. 10 | 11 | We can easily use the ABP CLI to create it, just run the following commands: 12 | 13 | ``` 14 | dotnet tool install -g Volo.Abp.Cli 15 | abp new MyStore -ts https://github.com/EasyAbp/EasyMall/releases/download/latest/latest.zip 16 | ``` 17 | 18 | If you want to install EShop to your existing application, please refer to this [document](https://easyabp.io/modules/EShop/#installation). 19 | 20 | ## Step 2: Define a Product Group 21 | 22 | 1. Create a new file `GiftCardProductGroup.cs` in the Domain layer with the following code: 23 | ```csharp 24 | [ProductGroupName("GiftCard")] 25 | public class GiftCardProductGroup 26 | { 27 | } 28 | ``` 29 | 30 | 2. Open the `MyStoreDomainModule.cs` file and add the following code in the `ConfigureServices` method: 31 | 32 | ```csharp 33 | Configure(options => 34 | { 35 | options.Groups.Configure(group => 36 | { 37 | group.DisplayName = "GiftCard"; 38 | }); 39 | }); 40 | ``` 41 | 42 | ## Step 3: Implement Automatic Email Delivery 43 | 44 | 1. Run the following command in the folder of the `MyStore.Domain.csproj` file to install the ABP Emailing module: 45 | 46 | ``` 47 | abp add-package Volo.Abp.Emailing 48 | ``` 49 | 50 | 2. Configure your email settings, please refer to the ABP official [document](https://docs.abp.io/en/abp/latest/Emailing#email-settings). 51 | 52 | 3. Create a new file `GiftCardOrderPaidEventHandler.cs` in the Domain layer with the following code: 53 | 54 | ```csharp 55 | public class GiftCardOrderPaidEventHandler: IDistributedEventHandler, ITransientDependency 56 | { 57 | private readonly IEmailSender _emailSender; 58 | private readonly IExternalUserLookupServiceProvider _externalUserLookupServiceProvider; 59 | 60 | public GiftCardOrderPaidEventHandler( 61 | IEmailSender emailSender, 62 | IExternalUserLookupServiceProvider externalUserLookupServiceProvider) 63 | { 64 | _emailSender = emailSender; 65 | _distributedEventBus = distributedEventBus; 66 | _externalUserLookupServiceProvider = externalUserLookupServiceProvider; 67 | } 68 | 69 | public async Task HandleEventAsync(OrderPaidEto eventData) 70 | { 71 | if (!eventData.Order.OrderLines.Any(x => x.ProductGroupName == "GiftCard")) 72 | { 73 | return; 74 | } 75 | 76 | var user = await _externalUserLookupServiceProvider.FindByIdAsync(eventData.Order.CustomerUserId); 77 | 78 | foreach (var orderLine in eventData.Order.OrderLines.Where(x => x.ProductGroupName == "GiftCard")) 79 | { 80 | await _emailSender.SendAsync(user.Email, "Here is your gift card", "Card number: 123456, password: 123456"); 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | ## Step 4: Create a Gift Card Product and Place an Order 87 | 88 | The EShop provides management pages to manage your products, but it has not yet provided relevant UI for users to place an order, for now, you can implement the UI yourself, read the [document](https://easyabp.io/modules/EShop/#basic-usage) to learn more. 89 | 90 | ## Postscript 91 | 92 | As you can see, using modules to implement features can make your work easier. 93 | 94 | This article only provides a simple example to show you how to use and customize the EShop module group, if you want to implement a real gift card store, you can consider using [EasyAbp.GiftCardManagement](https://easyabp.io/modules/GiftCardManagement) module, which can help you manage the gift card business and even be the provider of inventory. -------------------------------------------------------------------------------- /Use-Stepping-To-Perform-Atomic-Multi-Step-Operations/en/article.md: -------------------------------------------------------------------------------- 1 | # Use Stepping To Perform Atomic Multi-Step Operations 2 | 3 | Stepping is a distributed [BASE](https://en.wikipedia.org/wiki/Eventual_consistency) jobs implementation. You can use it as a workflow engine, event outbox/inbox, email/SMS sender, remote invoker, and more. 4 | 5 | We have provided documentation for the following languages: [English](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/README.md), [简体中文](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/README.zh-CN.md). 6 | 7 | ## What are `Job` and `Step` in Stepping? 8 | 9 | `Job` is a distributed transaction unit, and `Step` is a specific task inside a job. 10 | 11 | A job contains one or many steps, and the transaction manager will execute them in order. If step 1 fails, it will be retried until success, and then step 2 starts to execute. 12 | 13 | ## Scenarios for Using Stepping 14 | 15 | [![WhatCanSteppingDo](https://user-images.githubusercontent.com/30018771/190894723-dd4f1a17-f8f2-4d81-bea1-32f6ab7d4782.png)](https://excalidraw.com/#json=sSS0SSIWEQ3hLKuEgKQbf,g1ijMIFvKb7L8BuoiQYd0w) 16 | 17 | ### Want To Execute Steps and Ensure Atomicity 18 | 19 | When you start a job, Stepping will eventually complete the steps you require. If the app crashes during the executions, the transaction manager will continue to execute the rest steps after it recovers. 20 | 21 | Stepping will complete your steps one by one. If a step fails, it will be tried later until success, which makes the job [atomic](https://coffeecodeclimb.com/2020/07/26/atomicity-and-idempotency-for-dummies/#atomicity). Please ensure all your steps can eventually succeed after retrying unless it is a [Saga step](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/Steps.md#saga-step). 22 | 23 | Stepping may already complete the current step when your app crashes during the execution. When your app recovers, Stepping will execute it redundantly. Therefore, all your steps should be [idempotent](https://coffeecodeclimb.com/2020/07/26/atomicity-and-idempotency-for-dummies/#idempotence). 24 | 25 | ### Want To Ensure Executing Steps After a DB Transaction Commits 26 | 27 | When you start a job with a DB transaction, Stepping will eventually complete the steps you require after the DB transaction commits. 28 | 29 | You don't need to worry about the non-atomicity caused by the app crashes after the transaction commits but before the steps' execution. We have handled this case by using the DTM's [2-phase messaging](https://en.dtm.pub/practice/msg.html) pattern. 30 | 31 | Stepping also supports the "multi-tenant with multi-DB" scenario, meaning it works no matter how many different databases there are in your app. 32 | 33 | ## Examples 34 | 35 | The transaction manager will eventually complete the added steps: 36 | 37 | ```csharp 38 | var job = await distributedJobFactory.CreateJobAsync(); 39 | 40 | job.AddStep(new RequestBank1TransferOutStep(args)); // step with args 41 | job.AddStep(); // step without args 42 | 43 | await job.StartAsync(); 44 | ``` 45 | 46 | The [Steps document](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/Steps.md) shows how to define a step. 47 | 48 | If you want to execute the steps after a DB transaction commits and ensure they will eventually be done: 49 | 50 | ```csharp 51 | var db = serviceProvider.GetRequiredService(); // example for EF Core 52 | await db.Database.BeginTransactionAsync(); 53 | 54 | var order = new Order(args); 55 | 56 | db.Orders.Add(order); 57 | await db.SaveChangesAsync(); 58 | 59 | var job = await distributedJobFactory.CreateJobAsync(new EfCoreSteppingDbContext(db)); 60 | 61 | job.AddStep(new SendOrderCreatedEmailStep(order)); 62 | job.AddStep(new SendOrderCreatedSmsStep(order)); 63 | 64 | await job.StartAsync(); // it will commit the DB transaction 65 | ``` 66 | 67 | Stepping supports `EF Core`, `ADO.NET`(coming soon), and `MongoDB`. 68 | 69 | For details, please see the [Usage document](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/Usage.md). 70 | 71 | ## Installation 72 | 73 | See the [Installation document](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/Installation.md). 74 | 75 | ## Supported Transaction Managers 76 | 77 | Stepping requires transaction managers. You can choose an implementation you like. 78 | 79 | ### DTM Server 80 | 81 | DTM is a mature transaction manager you can use as the TM provider for Stepping. DTM allows you to use many other distributed transaction patterns like Saga, TCC, and XA. 82 | 83 | See the [DTM document](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/Dtm.md). 84 | 85 | ### Local-TM 86 | 87 | Stepping provides a simple built-in TM implementation. The local-TM runs with your app as a local transaction manager. Which app starts a job should be the TM of this job. 88 | 89 | See the [Local-TM document](https://github.com/TeamStepping/Stepping.NET/blob/main/docs/LocalTm.md). 90 | -------------------------------------------------------------------------------- /Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/en/article.md: -------------------------------------------------------------------------------- 1 | # Using ABP vNext to Develop an Address Book Application in 5 Minutes 2 | 3 | ABP vNext framework (hereinafter called "ABP") is an artwork-level application development framework designed by Volosoft. It is based on Domain-Driven-Design (DDD) and innovatively adopts modular development design. ABP is undoubtedly the most advanced and elegant existence in ASP.NET Core development frameworks. 4 | 5 | I believe that with the excellent modular development design and smooth development experience, ABP's position in ASP.NET Core is expected to reach the position of Spring in Java. 6 | 7 | ## Module Development and Application Development 8 | 9 | Using the ABP framework, you can make some modules in advance, such as external login implementation, private messaging, file upload, blog, forum, and other modules, and package them for later use. When developing a specific application, you can easily install modules into your application, saving a lot of repetitive work. 10 | 11 | In addition to making your own modules, you can also install modules maintained by the open-source community on NuGet. Of course, the community is also waiting for your contribution. 12 | 13 | ## Start to Develop the Address Book Application 14 | 15 | Today we will not discuss module development, but start with the simplest application development. I will follow the ABP best practices and show you how to develop an address book application using Abp vNext framework in 5 minutes. 16 | 17 | ### Step 1: Use ABP CLI to Generate the Application Solution 18 | 19 | 1. Use the command line to install ABP CLI: `dotnet tool install -g Volo.Abp.Cli`. 20 | 21 | 2. Use the command line to generate a solution: `abp new AddressBook` (the solution will be generated in the current directory). 22 | 23 | ### Step 2: Create a "Contact" Entity 24 | 25 | For ABP, the contact entity should be an AggregateRoot. For details, please refer to the introduction of Domain-Driven-Design (DDD) in the [ABP official document](https://docs.abp.io/en/abp/latest/Domain-Driven-Design). 26 | 27 | 1. Create a new directory: `aspnet-core/src/AddressBook.Domain/Contacts`. 28 | 29 | 2. Manually create the `Contact.cs` file in the above directory. 30 | 31 | ```csharp 32 | public class Contact : AggregateRoot 33 | { 34 | public virtual string Name { get; protected set; } 35 | 36 | public virtual string PhoneNumber { get; protected set; } 37 | 38 | public virtual string Address { get; protected set; } 39 | 40 | public virtual byte? Age { get; protected set; } 41 | 42 | public virtual DateTime? Birthday { get; protected set; } 43 | 44 | // The constructors will be generated later, you don’t need to add them manually here. 45 | } 46 | ``` 47 | 48 | 3. Run the `AddressBook.DbMigrator` project, which is to establish the basic structure and data in the database for our application. 49 | 50 | ### Step 3: Generate the Remaining Code 51 | 52 | This article uses EasyAbp's open-source [AbpHelper GUI](https://easyabp.io/abphelper/AbpHelper.GUI) to generate code. If you are an ABP commercial user, you can also choose [ABP Suite](https://commercial.abp.io/tools/suite). 53 | 54 | 1. Download AbpHelper GUI: https://github.com/EasyAbp/AbpHelper.GUI/releases 55 | 56 | 2. Use the **CRUD Code Generator** to generate all codes related to the Contact entity. 57 | 58 | ![CrudCodeGenerator](images/CrudCodeGenerator.png) 59 | 60 | If you are using the AbpHelper GUI for the first time, please install the AbpHelper CLI through the `Install or update AbpHelper CLI` on the left navigation menu. You can also directly use [AbpHelper CLI](https://easyabp.io/abphelper/AbpHelper.CLI) to complete the above work. 61 | 62 | ### Step 4: Run the Application 63 | 64 | 1. Run the AddressBook.Web project. 65 | 66 | ![HomePage](images/HomePage.png) 67 | 68 | 2. Log in and explore your application (the default password of the admin user is `1q2w3E*`). 69 | 70 | ![CreateContact](images/CreateContact.png) 71 | ![ContactList](images/ContactList.png) 72 | 73 | You must have noticed that the form has been automatically generated by the [abp-dynamic-form](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Tag-Helpers/Dynamic-Forms) tag helper. Moreover, you only need to modify the localization file to customize the words, but I will not demonstrate it here. 74 | 75 | 3. Contact entity's RESTful API has also been automatically generated, if you need them, visit the route `/swagger`. 76 | 77 | ## Postscript 78 | 79 | Our address book application naturally includes identity management, multi-tenant support, if you plan to learn the ABP framework systematically, please read the [official document](https://docs.abp.io). 80 | 81 | The AbpHelper used in this article is a tool produced by the EasyAbp organization, which can significantly improve your development efficiency and is completely free. In addition, EasyAbp also provides a lot of useful modules, you can read [EasyAbp Guide](https://github.com/EasyAbp/EasyAbpGuide) to learn more. 82 | 83 | ## Next 84 | 85 | In the next article, I will introduce how to install the **PrivateMessaging** module to our address book application. This module is also developed and maintained by the EasyAbp organization, and you can even use it for free in commercial applications. 86 | -------------------------------------------------------------------------------- /Reuse-ABP-VNext-Modules-To-Quickly-Implement-Application-Features/en/article.md: -------------------------------------------------------------------------------- 1 | # Reuse ABP vNext Modules to Quickly Implement Application Features 2 | 3 | In the [previous article](../../Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/en/article.md), we have completed the basic features of the address book application. 4 | 5 | In this article, we will install the **PrivateMessaging** module to our address book application, so that users can send private messages to each other and get new message notifications on the website. 6 | 7 | In the design of the private messaging module, users can send private messages to themselves, but we will override the design to prohibit users from sending private messages to themselves at the end of this article. 8 | 9 | ## What Is a Module in ABP Framework? 10 | 11 | Using the ABP framework, you can make some modules in advance, such as external login implementation, private messaging, file upload, blog, forum, and other modules, and package them for later use. When developing a specific application, you can easily install modules into your application, saving a lot of repetitive work. 12 | 13 | In addition to making your own modules, you can also install modules maintained by the open-source community on NuGet. Of course, the community is also waiting for your contribution. 14 | 15 | ## Install the Private Messaging Module 16 | 17 | The module (EasyAbp.PrivateMessaging) we use is developed and maintained by the [EasyAbp](https://easyabp.io) organization, and you can even use it for free in commercial applications. 18 | 19 | Now we will start to install the module step by step. 20 | 21 | ### Step 1: Install the Module's NuGet Packages 22 | 23 | 1. Install the NuGet package `EasyAbp.PrivateMessaging.Application` to the `AddressBook.Application` project. 24 | 2. Install the NuGet package `EasyAbp.PrivateMessaging.Application.Contracts` to the `AddressBook.Application.Contracts` project. 25 | 3. Install the NuGet package `EasyAbp.PrivateMessaging.Domain` to the `AddressBook.Domain` project. 26 | 4. Install the NuGet package `EasyAbp.PrivateMessaging.Domain.Shared` to the `AddressBook.Domain.Shared` project. 27 | 5. Install the NuGet package `EasyAbp.PrivateMessaging.EntityFrameworkCore` to the `AddressBook.EntityFrameworkCore` project. 28 | 6. Install the NuGet package `EasyAbp.PrivateMessaging.HttpApi` into the `AddressBook.HttpApi` project. 29 | 7. Install the NuGet package `EasyAbp.PrivateMessaging.HttpApi.Client` into the `AddressBook.HttpApi.Client` project. 30 | 8. Install the NuGet package `EasyAbp.PrivateMessaging.Web` into the `AddressBook.Web` project (you can skip this step if you do not need the UI). 31 | 32 | ### Step 2: Add Module Dependencies and Configurations 33 | 34 | 1. Add the dependency of the module to the module class of the above projects, for example, we add the attribute `[DependsOn(PrivateMessagingApplicationModule)]` to the class in the **AddressBookApplicationModule.cs** of the AddressBook.Application project, and so on. 35 | 36 | 2. Find the **OnModelCreating** method in **AddressBookMigrationsDbContext.cs** of the AddressBook.EntityFrameworkCore.DbMigrations project and add the code `builder.ConfigurePrivateMessaging();` to it to configure the EF Core migration of the module. 37 | 38 | ### Step 3: Create EF Core Migration and Update the Database 39 | 40 | 1. Run the command `dotnet ef migrations add Installed_Pm_Module -s ../AddressBook.DbMigrator` in the directory of AddressBook.EntityFrameworkCore.DbMigrations project. 41 | 42 | 2. Run the AddressBook.DbMigrator project, it will automatically update the database. 43 | 44 | If you want to learn more about this step, you can read the [ABP official documentation](https://docs.abp.io/en/abp/latest/Tutorials/Part-1#add-database-migration). 45 | 46 | ### Step 4: Run the Application 47 | 48 | Now run the AddressBook.Web project, we can see that the private messaging module has been installed successfully. 49 | 50 | ![HomePage](images/HomePage.png) 51 | 52 | ## Improvement: Prohibit Users From Sending Private Messages to Themselves 53 | 54 | The ABP framework allows us to override the code in the module. Please create a new file `MyPrivateMessageAppService.cs` in the AddressBook.Application project: 55 | ```csharp 56 | [Dependency(ReplaceServices = true)] 57 | public class MyPrivateMessageAppService : PrivateMessageAppService 58 | { 59 | // ctor 60 | 61 | public override Task CreateAsync(CreateUpdatePrivateMessageDto input) 62 | { 63 | if (input.ToUserName == CurrentUser.UserName) 64 | { 65 | throw new UserFriendlyException("Don't send messages to yourself"); 66 | } 67 | 68 | return base.CreateAsync(input); 69 | } 70 | } 71 | ``` 72 | 73 | If a user sends a message to himself, he will see the error message: 74 | 75 | ![SendToSelf](images/SendToSelf.png) 76 | 77 | Thanks to the extensible modular design of the ABP framework, static files (such as JS and CSS files) and pages can be overridden. Please read the ABP official document for more information: [Customizing the Existing Modules](https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Guide) 78 | 79 | ## Postscript 80 | 81 | If you want to learn more about the private messaging module, please read its [documentation](https://easyabp.io/modules/PrivateMessaging). 82 | 83 | By the way, the EasyAbp organization has many powerful and useful modules, such as EShop, PaymentService, GiftCardManagement, which can improve your development efficiency and enhance the reliability of applications. If you are interested, please read [EasyAbp Guide](https://github.com/EasyAbp/EasyAbpGuide) to learn more. 84 | 85 | I look forward to the launch of ABP's official module market (market.abp.io) in the future, providing community modules list, search, and automatic installation features, making it easier for us to install modules. 86 | 87 | ## Next 88 | 89 | In the next article, we will upgrade the address book application to a multi-tenant SaaS application with a minor change. 90 | -------------------------------------------------------------------------------- /Notice-And-Solve-ABP-Distributed-Events-Disordering/zh/article.md: -------------------------------------------------------------------------------- 1 | # 重视和解决 ABP 分布式事件乱序问题 2 | 3 | ABP Framework 5.0 实现了单体应用场景下,收件箱和发件箱的事件严格顺序性。但在微服务或多数据库场景下,由于网络时延和设施效率的限制, 4 | 分布式事件将不再是 Linearizability [[1]](#参考) 的,因此必然会存在物理时间上的收件乱序。 5 | 6 | 借用 Daniel Wu 的文章《消息可靠性和顺序(中文)》[[2]](#参考) 中的插图为您展示问题: 7 | 8 | ![image](https://user-images.githubusercontent.com/30018771/194262471-d5c7aa5f-adc6-4593-b0b2-9738ac56edab.png) 9 | 10 | 如果一个处理中的事件,与任何其他的事件之间有因果关系,则有可能因两者的乱序而产生问题。 11 | 12 | 本文在这个事实下,讨论我们在订阅方可能遇到的情况和解决方案。 13 | 14 | ## 案例 15 | 16 | 我们做以下假设。 17 | 18 | 1. 我们关注的是一个用户积分服务,它是一些分布式事件的订阅方。 19 | 2. `m1` 和 `m2` 是 **先后发生** 的两个事件。 20 | 3. `t1` 和 `t2` 分别为订阅方服务收到并处理事件 m1 和 m2 的时间。 21 | 4. `t1 < t2` 代表 t1 早于 t2,称为正序;`t1 > t2` 代表 t1 晚于 t2,称为乱序。 22 | 5. C 代表订阅方服务的状态(Configuration)。`C0` 为初始状态,`CF` 为预期的最终状态,`CW` 为错误的最终状态。 23 | 24 | ### 案例 1 25 | 26 | * 事件 m1:用户 A 创建事件 27 | * 事件 m2:用户 B 创建事件 28 | * Handler 的工作:根据 m1 和 m2,分别在本地创建 LocalUser 实体 29 | * 分析:m1 和 m2 没有因果关系,顺序不敏感 30 | * t1 < t2 (正序): 31 | 32 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 33 | 34 | * t1 > t2 (乱序): 35 | 36 | [![s1-disordered](https://user-images.githubusercontent.com/30018771/194247285-e62dc690-e691-4c38-8616-6e4f12a81975.png)](https://excalidraw.com/#json=94-kB06wg4IZdyFTBNoSe,hyQGl4eKRmgHXAU027iy0w) 37 | 38 | 无需处理。 39 | 40 | ### 案例 2 41 | 42 | * 事件 m1:用户 A 创建事件 43 | * 事件 m2:订单 1 支付事件 44 | * Handler 的工作:根据 m1,在本地创建`LocalUser`实体;根据 m2,给`LocalUser.Score`增加积分 45 | * 分析:m1 和 m2 有因果关系。m1 和 m2 顺序敏感,但“实体不存在”的异常拦截了乱序,handler 是幂等的,不存在一致性问题 46 | * t1 < t2 (正序): 47 | 48 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 49 | 50 | * t1 > t2 (乱序): 51 | 52 | [![s2-disordered](https://user-images.githubusercontent.com/30018771/201462287-6155f1b9-dd9f-4452-bb3d-921b2e1b876b.png)](https://excalidraw.com/#json=6azro2d7yq3YVGqmmFkeE,vX5ZLgF_as_otPyRgZX0Yg) 53 | 54 | 无需处理。待 m1 被处理后,m2 延迟重试处理,实质上达到正序。 55 | 56 | ### 案例 3 57 | 58 | * 事件 m1:订单 1 支付事件 59 | * 事件 m2:订单 1 取消事件 60 | * Handler 的工作:根据 m1,给`LocalUser.Score`增加积分;根据 m2,给`LocalUser.Score`扣减积分;积分最低扣到 0,不会为负数 61 | * 分析: m1 和 m2 有因果关系。m1 和 m2 顺序敏感,handler 不是幂等的,存在一致性问题 62 | * t1 < t2 (正序): 63 | 64 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 65 | 66 | * t1 > t2 (乱序): 67 | 68 | [![s3-disordered](https://user-images.githubusercontent.com/30018771/201470772-4a01a4fe-f933-4d2c-82cf-e59fd2905bec.png)](https://excalidraw.com/#json=1wnVTL1RZWpvXu3YkHpj8,uFffnJLeWEMTLk3U33Z2ZA) 69 | 70 | 积分服务在本地创建`LocalOrder`实体记录订单处理状态。 71 | 72 | ```CSharp 73 | public class LocalOrder : AggregateRoot 74 | { 75 | public bool HasPaidEventHandled { get; set; } // set to true after handling m1 76 | } 77 | ``` 78 | 79 | 当 m2 handler 发现`OrderCanceledEto.OrderPaidTime != null`而`LocalOrder.HasPaidEventHandled == false`,则抛出错误。待 m1 被处理后,m2 延迟重试处理,实质上达到正序。 80 | 81 | 我们实质上把本案例 3 转化成了案例 2 的情况,从而实现了幂等。 82 | 83 | #### 处理后 84 | 85 | * t1 < t2 (正序): 86 | 87 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 88 | 89 | * t1 > t2 (乱序): 90 | 91 | [![s3-resolved](https://user-images.githubusercontent.com/30018771/201462287-6155f1b9-dd9f-4452-bb3d-921b2e1b876b.png)](https://excalidraw.com/#json=6azro2d7yq3YVGqmmFkeE,vX5ZLgF_as_otPyRgZX0Yg) 92 | 93 | ### 案例 4 94 | 95 | * 事件 m1:用户 A 变更事件 (变更了可用区 `Region`) 96 | * 事件 m2:订单 1 支付事件 97 | * Handler 的工作:根据 m1,由于`UserEto.Region != LocalUser.Region`,清零`LocalUser.Score`。根据 m2,给`LocalUser.Score`增加积分 98 | * 分析:m1 和 m2 有因果关系。m1 和 m2 顺序敏感,handler 不是幂等的,存在一致性问题 99 | * t1 < t2 (正序): 100 | 101 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 102 | 103 | * t1 > t2 (乱序): 104 | 105 | [![s4-disordered](https://user-images.githubusercontent.com/30018771/201470772-4a01a4fe-f933-4d2c-82cf-e59fd2905bec.png)](https://excalidraw.com/#json=1wnVTL1RZWpvXu3YkHpj8,uFffnJLeWEMTLk3U33Z2ZA) 106 | 107 | 我们可以通过这些改动解决问题: 108 | 109 | 1. 给`User`实体扩展 int 类型属性`RegionVersion`,默认值为 0,每次 Region 变更时,`RegionVersion`递增 1。 110 | 2. 积分服务使用`LocalUserRegion.Score`记录用户的积分,而非使用`LocalUser.Score`。 111 | ```CSharp 112 | public class LocalUserRegion : AggregateRoot 113 | { 114 | public Guid UserId { get; set; } 115 | public string Region { get; set; } 116 | public int RegionVersion { get; set; } 117 | public int Score { get; set; } 118 | } 119 | ``` 120 | 3. 处理 m1 时,若`UserEto.RegionVersion`更新,则创建新的`LocalUserRegion`实体,初始的积分为 0,相当于变更 Region 即清零积分。 121 | 4. 在用户支付时,本地服务调用 Identity 远程服务,将查得的`UserDto.RegionVersion`写入事件 m2 的`OrderPaidEto.UserRegionVersion`。 122 | 5. 处理 m2 时,根据`OrderPaidEto.UserRegionVersion`,增加对应的`LocalUserRegion.Score`。 123 | 124 | 我们解除了 m1 和 m2 的因果关系,从而实现了幂等。 125 | 126 | #### 处理后 127 | 128 | * t1 < t2 (正序): 129 | 130 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 131 | 132 | * t1 > t2 (乱序) : 133 | 134 | [![s4-resolved](https://user-images.githubusercontent.com/30018771/201462287-6155f1b9-dd9f-4452-bb3d-921b2e1b876b.png)](https://excalidraw.com/#json=6azro2d7yq3YVGqmmFkeE,vX5ZLgF_as_otPyRgZX0Yg) 135 | 136 | ### 案例 5:ABP 实体同步器 137 | 138 | 在 ABP 的 DDD 实践中,不同模块之间会通过实体同步器冗余实体数据。一个典型的案例是 Blogging 模块的 BlogUserSynchronizer [[3]](#参考)。本案例的特别之处在于,如果不是有极严的要求,过期的事件可以被跳过处理。 139 | 140 | * 事件 m1:用户 A 变更事件 141 | * 事件 m2:用户 A 变更事件 142 | * Handler 的工作:根据 m1/m2,更新`LocalUser`实体中的用户资料 143 | * 分析:一旦 m2 早于 m1 被处理,则旧资料会覆盖新资料,存在一致性问题 144 | * t1 < t2 (正序): 145 | 146 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 147 | 148 | * t1 > t2 (乱序): 149 | 150 | [![s5-disordered](https://user-images.githubusercontent.com/30018771/201468277-40c792ce-9a9f-4b29-b46c-c4392b3b79bb.png)](https://excalidraw.com/#json=SwmSL9qcgrFZA5UV8HuPD,_LiJ20bKVHx8D7x5c-KfAw) 151 | 152 | 我们给实体增加 int 类型的 `EntityVersion` 属性,此属性的值从 0 开始,并在每次更新实体时,自动递增 1。在实体同步器处理 `EntityUpdatedEto` 事件时,若 `UserEto.EntityVersion <= LocalUser.EntityVersion`,则跳过处理。就这样,我们解决了问题。我尝试了在 ABP 框架实现以上能力,见 PR #14197 [[4]](#参考)。 153 | 154 | #### 处理后 155 | 156 | * t1 < t2 (正序): 157 | 158 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 159 | 160 | * t1 > t2 (乱序): 161 | 162 | [![s5-resolved](https://user-images.githubusercontent.com/30018771/201468757-793bc2bb-5d47-4c7d-bcff-32e705e24d1e.png)](https://excalidraw.com/#json=L0ZI13yl9EYwWtyQC6hwK,CcVzzXgnznQSGA7x8qLGng) 163 | 164 | ## 思路整理 165 | 166 | 笔者认为,解决事件乱序问题有以下思路。 167 | 168 | 1. 尽可能保持 DistributedEventHandler 的业务逻辑简单,以便发现潜在的乱序问题。 169 | 2. 某些情况下,我们可以通过在本地记录实体的状态,将 handler 转化为幂等,就如上面案例 3 演示的那样。 170 | 3. 某些情况下,我们可以通过调整业务设计,解除因果关系,就如上面案例 4 演示的那样。 171 | 4. 实体同步器应采用 EntityVersion 的设计,以避免同步到过期的数据。 172 | 173 | ## 结论 174 | 175 | 即使你的应用当前只是单体,也应关心收件乱序问题,为今后可能到来的架构变化做储备。另外,请放弃实现 Linearizability,因为在微服务或多数据库场景下这是不可能的。 176 | 177 | 本文提到的几个案例,开发者似乎不难找出一致性问题的隐患。但在实际生产中,业务往往更复杂,事件数量也会更多,我们很难顾及周全。即便我们在开发时把所有可能的因果关系都找了出来,并且处理了它们,将来业务变更时,我们还能确保万无一失吗?答案恐怕是否定的。 178 | 179 | 分布式一致性问题是没有银弹的,它永远都在那里,开发者能做的是降低复杂度,通过设计解除因果关系,或手动实现幂等。 180 | 181 | ## 参考 182 | 183 | 1. Herlihy, Maurice P.; Wing, Jeannette M. (1987). "Axioms for Concurrent Objects". Proceedings of the 14th ACM SIGACT-SIGPLAN Symposium on Principles of Programming Languages, POPL '87. p. 13 184 | 2. Daniel Wu. (2021). 《消息可靠性和顺序(中文)》. https://danielw.cn/messaging-reliability-and-order-cn 185 | 3. GitHub abpframework/abp repository. BlogUserSynchronizer.cs. https://github.com/abpframework/abp/blob/1275f2207fc39d850d23472294e456c8504f20d2/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users/BlogUserSynchronizer.cs 186 | 4. GitHub abpframework/abp repository. PR #14197. https://github.com/abpframework/abp/pull/14197 187 | -------------------------------------------------------------------------------- /Introduce-DTM-For-Multi-Tenant-Multi-Database-Scene/zh/article.md: -------------------------------------------------------------------------------- 1 | # 引入 DTM 以支持 ABP 的多租户多数据库场景 2 | 3 | 这篇文章分享了使用 DTM 二阶段消息模式解决 [issue #10036](https://github.com/abpframework/abp/issues/10036) 的方法。 4 | 5 | 今天我们要使用 EasyAbp 的 [Abp.EventBus.Boxes.Dtm](https://github.com/EasyAbp/Abp.EventBus.Boxes.Dtm) 模块。 6 | 7 | ## DTM 事件箱的介绍 8 | 9 | 这个模块使用了 DTM 的 [二阶段消息](https://dtm.pub/practice/msg.html) 使得 ABP 的事件箱得以支持 [多租户多数据库场景](https://github.com/abpframework/abp/issues/10036)。 10 | 11 | 你需要先阅读 [DTM 文档](https://en.dtm.pub/guide/start.html),它将帮助你理解这个模块。 12 | 13 | ## 与 ABP 默认事件箱的差异 14 | 15 | | | DTM 二阶段消息事件箱 | ABP 5.0+ 默认事件箱 | 16 | | :------------------------------: | :------------------: | :----------------------------------------: | 17 | | 收发速度 | :heavy_check_mark: | :x: | 18 | | 更少的数据传输 | :x: | :heavy_check_mark: | 19 | | 保证事件发出
(事务工作单元) | :heavy_check_mark: | :heavy_check_mark: | 20 | | 保证事件发出
(非事务工作单元) | :x: | :heavy_check_mark:
(要求消费端解决幂等) | 21 | | 避免重复处理(仅限纯DB操作) | :heavy_check_mark: | :heavy_check_mark: | 22 | | 支持多租户多数据库 | :heavy_check_mark: | :x: | 23 | | 没有增加外部设施 | :x: | :heavy_check_mark: | 24 | | 管理面板和报警 | :heavy_check_mark: | :x: | 25 | 26 | ## DTM 发件箱是如何工作的? 27 | 28 | 假设你正在使用发件箱发布新的事件: 29 | ```csharp 30 | await _distributedEventBus.PublishAsync(eto1, useOutbox: true); 31 | await _distributedEventBus.PublishAsync(eto2, useOutbox: true); // useOutbox 的默认值即 true 32 | ``` 33 | DTM 发件箱会临时存储这些事件。接下来我们看看,在你完成当前工作单元时它会怎么做: 34 | ```CSharp 35 | // UnitOfWork.cs 的代码片段 36 | protected override async Task CommitTransactionsAsync() 37 | { 38 | // 第 1 步:在事务内插入一条记录到 DTM 屏障表,接着发送一条“prepare”请求到 DTM 服务器 39 | await DtmMessageManager.InsertBarriersAndPrepareAsync(EventBag); 40 | 41 | // 第 2 步: 提交当前 DB 事务 42 | await base.CommitTransactionsAsync(); 43 | 44 | // 第 3 步: 发送一条"submit"请求到 DTM 服务器 45 | OnCompleted(async () => await DtmMessageManager.SubmitAsync(EventBag)); 46 | } 47 | ``` 48 | 至此,DTM 服务器已经收到了一条“submit”请求。它会调用 app 的 `PublishEvents` 服务并附上所有事件的数据,后者被调用后会立即发布这些事件到 MQ。 49 | 50 | [![](https://mermaid.ink/img/pako:eNqNVMtOwkAU_ZXJrOEHujAx4sIFC4LuuhnoKE1owXa6MMYEY1BESRUxPoKvRBOjkUBMBMvvdKbyF06dCqWUhK7mzj33nnNP7nQX5ksKhhI08baF9TxOqWjLQJqsA_4RlRQxSK2nAbXP3eGX1-mJBLJISbe0HDZE7LVeWa2fXFqiToufJUCrn6OrD3dQcQdvQDVNCwugyHMgu-yyRoc6FxJgrW9qXwsU8OpfrLIfAU8p4AX9Ia0_gjW_YKOsIIIVQJun4iJLeLxSQPoWv3WdE14kuk01iSiwm7T6wtWyuyeB69mj23Z8nR-zdoPWn-gNn9QPywYuIwNP8ON8clZ9qACw2hmtP8Tz_Fs5Dx9rpX3mOs98as4uUONkMtQyDIttGTV8Dn4Ba0wrp6lkMWdY7Zx2D2nnlHOw48qoXQEoT9SSbsbRRSwSRHEOcaDYT0lsSLCc0UEmyiaNl__oQdKnpYOD8DqNvUpnJOC9n9DG5ywonQnLFPl4hRHrAmLhRbzGSAV7PPrpdL3hBbtvuwNHsMAE1LChIVXhT3zX7yNDUsAalqHEjwreRFaRyFDW9zjU-ntLq4pKSgaUNlHRxAnoP_Xsjp6HEjEs_A8KfhMBau8Xdo4RAw)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqNVMtOwkAU_ZXJrOEHujAx4sIFC4LuuhnoKE1owXa6MMYEY1BESRUxPoKvRBOjkUBMBMvvdKbyF06dCqWUhK7mzj33nnNP7nQX5ksKhhI08baF9TxOqWjLQJqsA_4RlRQxSK2nAbXP3eGX1-mJBLJISbe0HDZE7LVeWa2fXFqiToufJUCrn6OrD3dQcQdvQDVNCwugyHMgu-yyRoc6FxJgrW9qXwsU8OpfrLIfAU8p4AX9Ia0_gjW_YKOsIIIVQJun4iJLeLxSQPoWv3WdE14kuk01iSiwm7T6wtWyuyeB69mj23Z8nR-zdoPWn-gNn9QPywYuIwNP8ON8clZ9qACw2hmtP8Tz_Fs5Dx9rpX3mOs98as4uUONkMtQyDIttGTV8Dn4Ba0wrp6lkMWdY7Zx2D2nnlHOw48qoXQEoT9SSbsbRRSwSRHEOcaDYT0lsSLCc0UEmyiaNl__oQdKnpYOD8DqNvUpnJOC9n9DG5ywonQnLFPl4hRHrAmLhRbzGSAV7PPrpdL3hBbtvuwNHsMAE1LChIVXhT3zX7yNDUsAalqHEjwreRFaRyFDW9zjU-ntLq4pKSgaUNlHRxAnoP_Xsjp6HEjEs_A8KfhMBau8Xdo4RAw) 51 | 52 |
53 | 查看更详细的时序图 54 | 55 | [![](https://mermaid.ink/img/pako:eNq1VU1PGkEY_iuTPbWJeG5IY9LWHnrwYGxvXFZ2tCSw2GX30BgTqlIRQfzAgnwoNtKQKlSDQeSj_Bc7M7uc-At9l0FcdGk1afc0H88z7_s-z7szi4LbL2HBKQTwBw3LbjzpEecV0eeSEXyqR_ViNPl2CtH4NmlU9fI5cqDJl4jUN2jkiMW3SP1Yb-6y0FfSTNNvy6S1120ecrKoqX5Z881ihc_1RJGFLx0TE7SegLET0VClkyyRWpDUviNPIKBhDuT7AGR7ZyxWpvVdJ2KJKxpPcRTSI1UW_HQHPJQlEC4bNJJHb0zCuwVJVLGE6E6UL8yoMH_1XpTnYRVqARI_beiQOxnEd2ioANmy3BHHncc76aw9z5yzbAw0ovtQqTldUPCCqOBb_GDfcT97CwGx8BaNHNrHuZFyFN5Wyp5r3EG7bO5lr5fWSf0zLbQ6e22WrHaSF-Pj4_bMgbXJYrcZJa280U7RbJFuJaEtWHibrVRG6Ph8Vpl4QlptoJOrDboeuw5mLVVdB3MsUaXhMz29iuY90tN-j3nV4RxopEhaWZa5oO0TDhklA6nVh6nWxKzemt-A57AW2QNxmlV0S8T7Yv6JZBuFG9VthvXTU_QMsVKh21znHtLjc-OiYCpHNw9pJs9SP0xtUGctRo9jYADNHLBG-jbAX4xmqU19vwVe_wouG-19thqn2xU9B_9n2oT0osGuGZCFvxjBEPmZ01erYLDRzhhHUVIrAZPl14zyGcirN3bZQRYG1jSwN4D76th79VgRRij_H71-tOwPagZ-6j_yiosCu6PPu6nshVv1-GW41uFeobUV6204JOfUNNwEJxs0VrEHTk0P6dXDPPiv6CdBy1GgPFyDXqcN2swaDcuSSxbGBB9WfKJHggdu0dxwCep77MMuwQlDCc-Jmld1CS55CaBa75V4LXlUvyI450Ro1DHBfMRmPspuwakqGr4B9R_JPmrpN52bxD0)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNq1VU1PGkEY_iuTPbWJeG5IY9LWHnrwYGxvXFZ2tCSw2GX30BgTqlIRQfzAgnwoNtKQKlSDQeSj_Bc7M7uc-At9l0FcdGk1afc0H88z7_s-z7szi4LbL2HBKQTwBw3LbjzpEecV0eeSEXyqR_ViNPl2CtH4NmlU9fI5cqDJl4jUN2jkiMW3SP1Yb-6y0FfSTNNvy6S1120ecrKoqX5Z881ihc_1RJGFLx0TE7SegLET0VClkyyRWpDUviNPIKBhDuT7AGR7ZyxWpvVdJ2KJKxpPcRTSI1UW_HQHPJQlEC4bNJJHb0zCuwVJVLGE6E6UL8yoMH_1XpTnYRVqARI_beiQOxnEd2ioANmy3BHHncc76aw9z5yzbAw0ovtQqTldUPCCqOBb_GDfcT97CwGx8BaNHNrHuZFyFN5Wyp5r3EG7bO5lr5fWSf0zLbQ6e22WrHaSF-Pj4_bMgbXJYrcZJa280U7RbJFuJaEtWHibrVRG6Ph8Vpl4QlptoJOrDboeuw5mLVVdB3MsUaXhMz29iuY90tN-j3nV4RxopEhaWZa5oO0TDhklA6nVh6nWxKzemt-A57AW2QNxmlV0S8T7Yv6JZBuFG9VthvXTU_QMsVKh21znHtLjc-OiYCpHNw9pJs9SP0xtUGctRo9jYADNHLBG-jbAX4xmqU19vwVe_wouG-19thqn2xU9B_9n2oT0osGuGZCFvxjBEPmZ01erYLDRzhhHUVIrAZPl14zyGcirN3bZQRYG1jSwN4D76th79VgRRij_H71-tOwPagZ-6j_yiosCu6PPu6nshVv1-GW41uFeobUV6204JOfUNNwEJxs0VrEHTk0P6dXDPPiv6CdBy1GgPFyDXqcN2swaDcuSSxbGBB9WfKJHggdu0dxwCep77MMuwQlDCc-Jmld1CS55CaBa75V4LXlUvyI450Ro1DHBfMRmPspuwakqGr4B9R_JPmrpN52bxD0) 56 | 57 | [![](https://mermaid.ink/img/pako:eNqFVMtuGjEU_RVrVq0U-ABUsWjpoouu0u7YTBiHIMFAh5lFFUWiKaSEQIEEyiOQgBQqVAkEIiJkgPIvqe0ZVvmF3okTCmFQvfK1z7k-91zb-4IvLGHBJUTxJw3LPuwJiH5FDHllBEMNqEGMPB_eI5rNk_HQ6PaRA3leI6Kf0FSTZXNEv6JXffO6RSZV-vOQTIv3k0tOFjU1LGuhHazw2Ci0WfLG4XZTvQBzF6KJwbzUIaMYGf1CgWhUwxzI9wHIij2W6VL9zIVY4ZZmyxyFjNSQxb48A6-oBMLNmKYa6J1F-BiRRBVLiJ6m-cK2CvGbPVH2wyrUAiSebSXJMwXZU5pogVpWb3JcPzuv1ux5VsxqGfCIVqBSK4woOCIq-B9-se9YV79EQCyZo6lL-3OerNyEt7XyoWu8gxy12HQspVxv8f0kTc8v2Li6khsovLOQ-SxDpjUOtqtzzRejc0z0I9qazoszVhrOS9dOp9Oeubg0pTboINOGOSvTWpvmSnDhWDLPvg42dOjVjuJ-QaYzoJPbE3qcuYvVlvy6i9VZYUiTPaMaR_6A9HKzc2Skr-paPnH5OtgaygGcYtOjdXc2Ef5jKit_NypT8PVP7NCcVVg8S_MDow6vrGpB-GvVjyxbWPKHGUuQ33UjPgQzzdm52UyTUQeYrPHN7PagYmN8xi5qMOG9F7aEEFZCYkCCT2PfEuQV1D0cwl7BBVMJ74paUPUKXvkAoNrDy3srBdSwIrh2xWAUbwnWx7D9WfYJLlXR8BPo8eN5RB38BaxhZok)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqFVMtuGjEU_RVrVq0U-ABUsWjpoouu0u7YTBiHIMFAh5lFFUWiKaSEQIEEyiOQgBQqVAkEIiJkgPIvqe0ZVvmF3okTCmFQvfK1z7k-91zb-4IvLGHBJUTxJw3LPuwJiH5FDHllBEMNqEGMPB_eI5rNk_HQ6PaRA3leI6Kf0FSTZXNEv6JXffO6RSZV-vOQTIv3k0tOFjU1LGuhHazw2Ci0WfLG4XZTvQBzF6KJwbzUIaMYGf1CgWhUwxzI9wHIij2W6VL9zIVY4ZZmyxyFjNSQxb48A6-oBMLNmKYa6J1F-BiRRBVLiJ6m-cK2CvGbPVH2wyrUAiSebSXJMwXZU5pogVpWb3JcPzuv1ux5VsxqGfCIVqBSK4woOCIq-B9-se9YV79EQCyZo6lL-3OerNyEt7XyoWu8gxy12HQspVxv8f0kTc8v2Li6khsovLOQ-SxDpjUOtqtzzRejc0z0I9qazoszVhrOS9dOp9Oeubg0pTboINOGOSvTWpvmSnDhWDLPvg42dOjVjuJ-QaYzoJPbE3qcuYvVlvy6i9VZYUiTPaMaR_6A9HKzc2Skr-paPnH5OtgaygGcYtOjdXc2Ef5jKit_NypT8PVP7NCcVVg8S_MDow6vrGpB-GvVjyxbWPKHGUuQ33UjPgQzzdm52UyTUQeYrPHN7PagYmN8xi5qMOG9F7aEEFZCYkCCT2PfEuQV1D0cwl7BBVMJ74paUPUKXvkAoNrDy3srBdSwIrh2xWAUbwnWx7D9WfYJLlXR8BPo8eN5RB38BaxhZok) 58 | 59 |
60 | 61 | > 如果你依然对这个模式“如何确保发送”困惑,请查看 DTM 的 [二阶段消息文档](https://en.dtm.pub/practice/msg.html) 以了解更多。 62 | 63 | ## DTM 收件箱是如何工作的? 64 | 65 | 与 ABP 的默认实现不同,DTM 收件箱从 MQ 收到一个事件后会立即处理(handle)。在所有的 handler 完成他们的工作后,收件箱会沿用当前事务,向 DTM 屏障表插入一条记录。最后它提交了事务并向 MQ 返回 ACK。 66 | 67 | 所有入箱的事件都拥有一条唯一的 MessageId。拥有相同 MessageId 的事件只会被处理一次,因为我们不能插入 gid (MessageId) 重复的记录到 DTM 屏障表。 68 | 69 | [![](https://mermaid.ink/img/pako:eNqFk8tKw0AUhl9lmLV9gaAFURciXYgus4nNtAaaVHNZSClUULEt0qgRRFJKQSUI9uKitvH2MjkTu_IVnDptMRptVknOf775zz8zBZzOywQL2CC7FtHSZFmRsrqkihpij6mYOYKWN1OIOr3gqRe2urwgWWZes9QtovPv1HoimYzoBDQsnQ1L-0G_FPTvaPWGuuX5LT0JTpv9QiliGFKWrMoovDoI_Cpr4qQIhEHpRYeetMA_F75K0K0Nr9z3pkcv22Dfwv0luN6Im1VktBDFvrc68HLx8dzg5Ckp8dsrx1L3jpZf4bgTbwV8J3S86WDfTPNK1C1cH4T2EVfBoBe81ZlZ6gyg4sX4mbC5AB4fuD6C_217TP9aiR7bUGnMDBHs0x9B1s7g8IbtCa03J0EGfT8uytk5xtr_xw2t2YF_zcaASnM2net4z9_jsr7UuoAWl9bwHFaJrkqKzA54YaQVsblNVCJigb3KJCNZOVPEolZkUmtHlkyyIitmXsdCRsoZZA6PDvrGnpbGgqlbZCIaX5KxqvgJeeqquw)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqFk8tKw0AUhl9lmLV9gaAFURciXYgus4nNtAaaVHNZSClUULEt0qgRRFJKQSUI9uKitvH2MjkTu_IVnDptMRptVknOf775zz8zBZzOywQL2CC7FtHSZFmRsrqkihpij6mYOYKWN1OIOr3gqRe2urwgWWZes9QtovPv1HoimYzoBDQsnQ1L-0G_FPTvaPWGuuX5LT0JTpv9QiliGFKWrMoovDoI_Cpr4qQIhEHpRYeetMA_F75K0K0Nr9z3pkcv22Dfwv0luN6Im1VktBDFvrc68HLx8dzg5Ckp8dsrx1L3jpZf4bgTbwV8J3S86WDfTPNK1C1cH4T2EVfBoBe81ZlZ6gyg4sX4mbC5AB4fuD6C_217TP9aiR7bUGnMDBHs0x9B1s7g8IbtCa03J0EGfT8uytk5xtr_xw2t2YF_zcaASnM2net4z9_jsr7UuoAWl9bwHFaJrkqKzA54YaQVsblNVCJigb3KJCNZOVPEolZkUmtHlkyyIitmXsdCRsoZZA6PDvrGnpbGgqlbZCIaX5KxqvgJeeqquw) 70 | 71 | 72 |
73 | 查看更详细的时序图 74 | 75 | [![](https://mermaid.ink/img/pako:eNqVVNFKG0EU_ZVhnvUHljYgtQ-lpCD1cV_W7JguZDd1M_tQREjBFhMrWXUFibE2kEgQGpNS0rjW9mf2TpKn_kJnczfGrWti52lm7rnnnjvnMps0k9cZVWiBbTjMyrBlQ8vamqlaRC5u8Bwjy6tpIrxecN0btLsY0Byetxxzjdl4Tq8splIxnEJGxYNR8X3QLwb9C7HbFLXSkzU7Bd6lvCJpVihoWfZCJ4PqduDvyiRkipFIUnHUEXtt8A-VcQi6lVG1Nqy3xPEluOfw9RhqrZA3a-jkaZx22O7AzdGfn2eR5hwniMcA3obrtsbi_S5E6RfsdLDsNCOuUmalVxSy9OwlIpil42YWMVKK2gVWSO4efG_gtW7f8s47YST-QNDYHrgfEQVXveD3qexXeFdQbiXomXAjAH58Q3yM_r7siH1cSey4UD6b6xu4-_94VzmAD005BuK0PvEu6PtJ7k2twyRodIffm4-zDk6-kJBathb5XvyUzDJL_MlncV2Fm0Mo7cneoVx_XHHEhqXHBHNG59X_zU6iZTOaEBU38Bt35c-XjjkPWzwderpATWabmqHLf2QzxKqUv2EmU6kitzpb15wcV6lqbUmo81bXOHuuGzxvU2VdyxXYAg3_k9fvrAxVuO2wCSj6iyLU1l-RFVkc)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqVVNFKG0EU_ZVhnvUHljYgtQ-lpCD1cV_W7JguZDd1M_tQREjBFhMrWXUFibE2kEgQGpNS0rjW9mf2TpKn_kJnczfGrWti52lm7rnnnjvnMps0k9cZVWiBbTjMyrBlQ8vamqlaRC5u8Bwjy6tpIrxecN0btLsY0Byetxxzjdl4Tq8splIxnEJGxYNR8X3QLwb9C7HbFLXSkzU7Bd6lvCJpVihoWfZCJ4PqduDvyiRkipFIUnHUEXtt8A-VcQi6lVG1Nqy3xPEluOfw9RhqrZA3a-jkaZx22O7AzdGfn2eR5hwniMcA3obrtsbi_S5E6RfsdLDsNCOuUmalVxSy9OwlIpil42YWMVKK2gVWSO4efG_gtW7f8s47YST-QNDYHrgfEQVXveD3qexXeFdQbiXomXAjAH58Q3yM_r7siH1cSey4UD6b6xu4-_94VzmAD005BuK0PvEu6PtJ7k2twyRodIffm4-zDk6-kJBathb5XvyUzDJL_MlncV2Fm0Mo7cneoVx_XHHEhqXHBHNG59X_zU6iZTOaEBU38Bt35c-XjjkPWzwderpATWabmqHLf2QzxKqUv2EmU6kitzpb15wcV6lqbUmo81bXOHuuGzxvU2VdyxXYAg3_k9fvrAxVuO2wCSj6iyLU1l-RFVkc) 76 | 77 |
78 | 79 | > 正如你注意到的,DTM 服务器没有参与收件箱的工作。🤭 80 | 81 | ## 安装和使用 82 | 83 | 请阅读 https://github.com/EasyAbp/Abp.EventBus.Boxes.Dtm/tree/main#installation. 84 | 85 | ## 后记 86 | 87 | 这样的一个 DTM 事件箱实现并不是完美的解决方案。这里最大的成本是,你需要额外关心 DTM 服务器的可用性。但是如果你遇到了多租户多数据库的场景,它就是现在最佳的选择。 88 | -------------------------------------------------------------------------------- /Introduce-DTM-For-Multi-Tenant-Multi-Database-Scene/en/article.md: -------------------------------------------------------------------------------- 1 | # Introduce DTM for Multi-Tenant Multi-Database Scene 2 | 3 | This article shares a new way to solve [issue #10036](https://github.com/abpframework/abp/issues/10036) using the DTM's 2-phase messages pattern. 4 | 5 | The module to be used today is EasyAbp's [Abp.EventBus.Boxes.Dtm](https://github.com/EasyAbp/Abp.EventBus.Boxes.Dtm). 6 | 7 | ## Introduction of the DTM Event Boxes Module 8 | 9 | This implementation uses DTM's [2-phase messages](https://en.dtm.pub/practice/msg.html) to support ABP event boxes in the [multi-tenant & multi-database scene](https://github.com/abpframework/abp/issues/10036). 10 | 11 | You should see the [DTM docs](https://en.dtm.pub/guide/start.html), which helps to understand this module. 12 | 13 | ## Differences From the ABP's Default Event Boxes 14 | 15 | | | DTM 2-phase Message Boxes | ABP 5.0+ Default Boxes | 16 | |:--------------------------------------------------------------------------:| :-----------------------: | :----------------------------------------------------: | 17 | | Speediness | :heavy_check_mark: | :x: | 18 | | Less data transfer | :x: | :heavy_check_mark: | 19 | | Be guaranteed to publish
(transactional UOW) | :heavy_check_mark: | :heavy_check_mark: | 20 | | Be guaranteed to publish
(non-transactional UOW) | :x: | :heavy_check_mark:
(consumers idempotency required) | 21 | | Avoid duplicate handling
(with only DB operations) | :heavy_check_mark: | :heavy_check_mark: | 22 | | Multi-tenant-database support | :heavy_check_mark: | :x: | 23 | | No additional external infrastructure | :x: | :heavy_check_mark: | 24 | | Dashboard and Alarm | :heavy_check_mark: | :x: | 25 | 26 | ## How Does the DTM Outbox Work? 27 | 28 | You are publishing events using the ABP event outbox: 29 | ```csharp 30 | await _distributedEventBus.PublishAsync(eto1, useOutbox: true); 31 | await _distributedEventBus.PublishAsync(eto2, useOutbox: true); // The useOutbox is true by default. 32 | ``` 33 | The DTM outbox collects them temporarily. Let's see what it will do when you complete the current unit of work: 34 | ```CSharp 35 | // Code snippet for UnitOfWork.cs 36 | protected override async Task CommitTransactionsAsync() 37 | { 38 | // Step 1: inserting a record to the DTM barrier table within the current DB transaction, 39 | // and then it sends a "prepare" request to the DTM server. 40 | await DtmMessageManager.InsertBarriersAndPrepareAsync(EventBag); 41 | 42 | // Step 2: committing the current DB transaction. 43 | await base.CommitTransactionsAsync(); 44 | 45 | // Step 3: sending a "submit" request to the DTM server. 46 | OnCompleted(async () => await DtmMessageManager.SubmitAsync(EventBag)); 47 | } 48 | ``` 49 | Now, the DTM server has received a "submit" request. It invokes the app's `PublishEvents` service with the events' data, and the latter will publish the events to the MQ provider immediately. 50 | 51 | [![](https://mermaid.ink/img/pako:eNqFk89uwjAMxl_Fypm-QA5IaOzAAW2IcevFbVyIlqRd_rAhxLsvbVooUG09NfHPX7449pmVtSDGmaOvQKakpcS9RZ0biJ-XXhEsP9bwFnxR_6RdDL42QRdk03rnyGbz-aJpOLyo2hGgAelcoBSPgRheoscCHUXmgGZPiQDn0d9z19M4LISAVYvtGhExEYX7jW2bloQE0JGMd0nkln535spEkx6wixdorRzc3yfExZbskSzvAo2lBi3dyBTMHnyOUDGh2lXmmXmqS6219OAPBN6icVh6WZter6eya6ET7EJZEokHyZG1ae7PS7tQxJT_7ryCb6kUVNJId-hMJ7_P7zCuQNKesh2ptpF4el8ou0aasN276TUX3ZmQwXso1GBk3A-pIusNnyBAak1Cxk5Sp0SvN1e3Az5tdVyz3oOoDU3YHJNrtJ-ALmk6VwXFZkyT1ShFnMFzm56zaFBTznj8FVRhUD5nublENHRj8Cqkry3jFSpHM9aO4_ZkSsa9DTRA_Rz31OUXaXxJzw)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqFk89uwjAMxl_Fypm-QA5IaOzAAW2IcevFbVyIlqRd_rAhxLsvbVooUG09NfHPX7449pmVtSDGmaOvQKakpcS9RZ0biJ-XXhEsP9bwFnxR_6RdDL42QRdk03rnyGbz-aJpOLyo2hGgAelcoBSPgRheoscCHUXmgGZPiQDn0d9z19M4LISAVYvtGhExEYX7jW2bloQE0JGMd0nkln535spEkx6wixdorRzc3yfExZbskSzvAo2lBi3dyBTMHnyOUDGh2lXmmXmqS6219OAPBN6icVh6WZter6eya6ET7EJZEokHyZG1ae7PS7tQxJT_7ryCb6kUVNJId-hMJ7_P7zCuQNKesh2ptpF4el8ou0aasN276TUX3ZmQwXso1GBk3A-pIusNnyBAak1Cxk5Sp0SvN1e3Az5tdVyz3oOoDU3YHJNrtJ-ALmk6VwXFZkyT1ShFnMFzm56zaFBTznj8FVRhUD5nublENHRj8Cqkry3jFSpHM9aO4_ZkSsa9DTRA_Rz31OUXaXxJzw) 52 | 53 |
54 | See the more detailed sequence diagram 55 | 56 | [![](https://mermaid.ink/img/pako:eNqtVU1v2zAM_SuETy2Q5FwYQ4ps6TYDC7YiLXrJhbaYRKgsefpoFhT976O_YidxgK5YTpH5SD4-ktJrlBlBURw5-h1IZzSXuLGYrzTwz0uvCOYPC_gZfGr-wBietuhBrsFv2fAZvEXtMPPSaMhMnkvvpd6AdOCU2d3WYTB4o0Oekq3Pj47seDqdFUUMX5RxBKjZxQWq7Wxg8xw9puiIMVvUG6oR4Dz6Y9yBXgwzISApYY-FYJjgwM2HZelWBxJAL6S9q4N07kc5E80kPWBlT9Fa2bI_duDDkuwL2bgyFJYKtNQha-P4hGcPKgaiVsqcY850qQSvWtHrw1nuY5ZPyC5rY7k0ZbhVXuY0mUwGvCoW32k_AqYAexNgS5ZuIYEdak5rQA6I9Cm106ud9NuKl8Oc4FsyB3TVeRX1VYrAlmPnPDhuyHUzLcr3eJSjtEbnW_WHdDj0qqFQldeF6Bxbl3FbXePpQpYRiVbnNseJcpfBZ3EfemtxxeQLuLlu9gO-zpIfd_NSJsHjzCqKUCiZ8XjCRooRWKMUz2iK2XMvw6V2JiwdTeChXMucOOvpONQCSg5ZpmRhnJdKcQtMRs7xsk5ggfa5bFCZuUtLiheTqxnqwD8VfEHd_9rBD4l6uce1YB-Rv6Rw2oGhiTlbtFmNHcOvkCrp6vXpX1Sdbov7eAAFMs9JSC5Z7TuPxf1BqtblfdPe8BFG0zt0aEeoiu3cOqhmiDRniUZRTjZHKfiheS0Nq4h553wBxPxX0BqD8qtopd8YGqqr-05Ib2wUr5HHcBSVT8hyr7Mo9jZQC2oeqwb19he08zVf)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqtVU1v2zAM_SuETy2Q5FwYQ4ps6TYDC7YiLXrJhbaYRKgsefpoFhT976O_YidxgK5YTpH5SD4-ktJrlBlBURw5-h1IZzSXuLGYrzTwz0uvCOYPC_gZfGr-wBietuhBrsFv2fAZvEXtMPPSaMhMnkvvpd6AdOCU2d3WYTB4o0Oekq3Pj47seDqdFUUMX5RxBKjZxQWq7Wxg8xw9puiIMVvUG6oR4Dz6Y9yBXgwzISApYY-FYJjgwM2HZelWBxJAL6S9q4N07kc5E80kPWBlT9Fa2bI_duDDkuwL2bgyFJYKtNQha-P4hGcPKgaiVsqcY850qQSvWtHrw1nuY5ZPyC5rY7k0ZbhVXuY0mUwGvCoW32k_AqYAexNgS5ZuIYEdak5rQA6I9Cm106ud9NuKl8Oc4FsyB3TVeRX1VYrAlmPnPDhuyHUzLcr3eJSjtEbnW_WHdDj0qqFQldeF6Bxbl3FbXePpQpYRiVbnNseJcpfBZ3EfemtxxeQLuLlu9gO-zpIfd_NSJsHjzCqKUCiZ8XjCRooRWKMUz2iK2XMvw6V2JiwdTeChXMucOOvpONQCSg5ZpmRhnJdKcQtMRs7xsk5ggfa5bFCZuUtLiheTqxnqwD8VfEHd_9rBD4l6uce1YB-Rv6Rw2oGhiTlbtFmNHcOvkCrp6vXpX1Sdbov7eAAFMs9JSC5Z7TuPxf1BqtblfdPe8BFG0zt0aEeoiu3cOqhmiDRniUZRTjZHKfiheS0Nq4h553wBxPxX0BqD8qtopd8YGqqr-05Ib2wUr5HHcBSVT8hyr7Mo9jZQC2oeqwb19he08zVf) 57 | 58 | [![](https://mermaid.ink/img/pako:eNp1VMtu2zAQ_JUFTy1g6wOEwoFbp62ABjk4QS66rMW1TUQkVXKV1Ajy711Sit_RSdTODmdnSL2pxmtSpYr0tyfX0MLgJqCtHcjDhluCxcMd3Pe88v9gCk9bZDBr4K0UvgMHdBEbNt5B4601zKRhjaYlfTOQYM_e9XZFYVg_RgrT2WzedSX8aH0kQAcmxp6GuhSkvEDGFUYSzBbdhgYEREY-xe3FlTDXGqoEe-w0Jh3oxg_L1DYQaaAXchwHkkP7yZ6VE5EMmOsrDMF8qD9tkMWSwguFMhe6QB0GOiCH4vRM5xFUX2HNzlxiLnzJducgjlIY-UbUdG_0AP45r_7cLiYQfCsByWTN84FcsCmbEu47CpgjHYK8mOd08icU5rUPYlfr3UZOjaWiKK50ZSm_aTcBGQt2voctBbqBCl7RySgezBXjv63C7Mur4W2eNaIl-FUtAGNe1-rYeQUhHeTIECXkr9ed26c7bpDFH4R-YuHYFfumIdLHqZz5cR34qX2VSKUCHtLFsiRBnkcqR38MIjkhUiObtpWRfUMxGrcp4A7DczIk5ZpCVRNlKVg0Wq72WxJQK2G1YlApr5rW2Ldcq9q9C7TP1-VWG_ZBlWtsI01UurbLnWtUyaGnD9D4exhR7_8BefBo_w)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNp1VMtu2zAQ_JUFTy1g6wOEwoFbp62ABjk4QS66rMW1TUQkVXKV1Ajy711Sit_RSdTODmdnSL2pxmtSpYr0tyfX0MLgJqCtHcjDhluCxcMd3Pe88v9gCk9bZDBr4K0UvgMHdBEbNt5B4601zKRhjaYlfTOQYM_e9XZFYVg_RgrT2WzedSX8aH0kQAcmxp6GuhSkvEDGFUYSzBbdhgYEREY-xe3FlTDXGqoEe-w0Jh3oxg_L1DYQaaAXchwHkkP7yZ6VE5EMmOsrDMF8qD9tkMWSwguFMhe6QB0GOiCH4vRM5xFUX2HNzlxiLnzJducgjlIY-UbUdG_0AP45r_7cLiYQfCsByWTN84FcsCmbEu47CpgjHYK8mOd08icU5rUPYlfr3UZOjaWiKK50ZSm_aTcBGQt2voctBbqBCl7RySgezBXjv63C7Mur4W2eNaIl-FUtAGNe1-rYeQUhHeTIECXkr9ed26c7bpDFH4R-YuHYFfumIdLHqZz5cR34qX2VSKUCHtLFsiRBnkcqR38MIjkhUiObtpWRfUMxGrcp4A7DczIk5ZpCVRNlKVg0Wq72WxJQK2G1YlApr5rW2Ldcq9q9C7TP1-VWG_ZBlWtsI01UurbLnWtUyaGnD9D4exhR7_8BefBo_w) 59 | 60 |
61 | 62 | > If you are still confused about how it is guaranteed to publish, see DTM's [2-phase messages doc](https://en.dtm.pub/practice/msg.html) for more information. 63 | 64 | ## How Does the DTM Inbox Work? 65 | 66 | Unlike ABP's default implementation, the DTM inbox gets an event from MQ and handles it at once. After the handlers finish their work, the inbox inserts a barrier within the current DB transaction. Finally, it commits the transaction and returns ACK to MQ. 67 | 68 | All the incoming events have a unique MessageId. Events with the same MessageId only are handled once since we cannot insert a barrier with a duplicate gid (MessageId). 69 | 70 | [![](https://mermaid.ink/img/pako:eNp9UstuwjAQ_JWVz_ADUUtFAakI5YDaYy6beAmW4jW115QK8e91HgWh0vhke2dmZ0d7VpXTpDIV6DMSV7Q0WHu0BUM6YqQhWH7ksObSnfpPjOI42pJ8_86309nsislgSY05kgdkoCOxPJV-9mVkDwiRTeoCOYWANa11L3DltjooWGKgDFYnEyRxSvTeJLlOojYanm_8l0FgIE3vjbwOTHYCOxf5Qbv54XAzLHvqHfe4VLsz9IasUxpXVBpQQ8AjQbVHrin8NdPJL_pqB9U36Xuvg3iIVUWkaTSZNQfybTRteSye0XQe2HrcbuGsNdLNLR45YCXG8bh2T_l3moTOtxnMFxs1UZa8RaPTDp5bXKFSJ0uFytJV0w5jI4Uq-JKg8aBRaKWNOK-yHTaBJqrdx_dvrlQmPtIvaNjjAXX5AUn_9To)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNp9UstuwjAQ_JWVz_ADUUtFAakI5YDaYy6beAmW4jW115QK8e91HgWh0vhke2dmZ0d7VpXTpDIV6DMSV7Q0WHu0BUM6YqQhWH7ksObSnfpPjOI42pJ8_86309nsislgSY05kgdkoCOxPJV-9mVkDwiRTeoCOYWANa11L3DltjooWGKgDFYnEyRxSvTeJLlOojYanm_8l0FgIE3vjbwOTHYCOxf5Qbv54XAzLHvqHfe4VLsz9IasUxpXVBpQQ8AjQbVHrin8NdPJL_pqB9U36Xuvg3iIVUWkaTSZNQfybTRteSye0XQe2HrcbuGsNdLNLR45YCXG8bh2T_l3moTOtxnMFxs1UZa8RaPTDp5bXKFSJ0uFytJV0w5jI4Uq-JKg8aBRaKWNOK-yHTaBJqrdx_dvrlQmPtIvaNjjAXX5AUn_9To) 71 | 72 | 73 |
74 | See the more detailed sequence diagram 75 | 76 | [![](https://mermaid.ink/img/pako:eNqNlN1uwjAMhV8lyjW8QLUxMWAampiExmVv3MSUaGnCEocxId59aVMoaPysV218_PnETrrjwkrkGff4FdAIHCsoHVS5YfEhRRrZeDFjU1PYbVqEQNaEqkCXvmfz_mBw1GRsjFpt0DEwDDdo6KFwg29FKwYsGBWrsBl6DyVOZQIcc2sOEBTgMWOTrfIUcwpwTkVcgyiVZI9d_lPrSFOS-4M6rTfwFtg_N_ncUpc2GHmiPlqJ8tk8Y8PRW4riQXYHaCydQk_3Nlyvu-7QClN7ki7Gznb_CkbG1h9VsZuSedggEyswJfq_Zhr8KEUbqezQ515buA9CIEq8OYap8ejqOdThW7PoRtGmLEFplHcn8RJlCSjDWisBhDX60kxOfTmrdfQjPpsekQPjQZCy5m7BRadtKCgbzo1D8P7vU3Ch_Zftj2xVKbrs_Ro7pVydWndieY9X6CpQMl7sXa3LeaxUYc6z-CpxCUFTznOzj9KwlrHnE6nIOp4tQXvs8fqSf_wYwTNyAQ-i9ufQqva_CSldXw)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqNlN1uwjAMhV8lyjW8QLUxMWAampiExmVv3MSUaGnCEocxId59aVMoaPysV218_PnETrrjwkrkGff4FdAIHCsoHVS5YfEhRRrZeDFjU1PYbVqEQNaEqkCXvmfz_mBw1GRsjFpt0DEwDDdo6KFwg29FKwYsGBWrsBl6DyVOZQIcc2sOEBTgMWOTrfIUcwpwTkVcgyiVZI9d_lPrSFOS-4M6rTfwFtg_N_ncUpc2GHmiPlqJ8tk8Y8PRW4riQXYHaCydQk_3Nlyvu-7QClN7ki7Gznb_CkbG1h9VsZuSedggEyswJfq_Zhr8KEUbqezQ515buA9CIEq8OYap8ejqOdThW7PoRtGmLEFplHcn8RJlCSjDWisBhDX60kxOfTmrdfQjPpsekQPjQZCy5m7BRadtKCgbzo1D8P7vU3Ch_Zftj2xVKbrs_Ro7pVydWndieY9X6CpQMl7sXa3LeaxUYc6z-CpxCUFTznOzj9KwlrHnE6nIOp4tQXvs8fqSf_wYwTNyAQ-i9ufQqva_CSldXw) 77 | 78 |
79 | 80 | > As you may have noticed, the inbox has nothing to do with the DTM Server.🤭 81 | 82 | ## Installation and Usage 83 | 84 | Please see https://github.com/EasyAbp/Abp.EventBus.Boxes.Dtm/tree/main#installation. 85 | 86 | ## Postscript 87 | 88 | This DTM event-boxes implementation is not a perfect solution. The main cost is that you need to care about the availability of the DTM Server. But if you run into the multi-tenant multi-database scene, it should be the best choice for now. 89 | -------------------------------------------------------------------------------- /Notice-And-Solve-ABP-Distributed-Events-Disordering/en/article.md: -------------------------------------------------------------------------------- 1 | # Notice and Solve ABP Distributed Events Disordering 2 | 3 | ABP Framework 5.0 implemented the strict ordering of outboxes and inboxes for monolithic apps. 4 | However, in a microservice or multi-database scenario, 5 | distributed events will no longer be linearizability [[1]](#references) due to the limitations of network latency and infrastructure efficiency. 6 | So there will inevitably be physical time disordering events. 7 | 8 | Use an illustration from Daniel Wu's article "Messaging Reliability and Ordering" [[2]](#references) to show you the problem: 9 | 10 | ![image](https://user-images.githubusercontent.com/30018771/194262471-d5c7aa5f-adc6-4593-b0b2-9738ac56edab.png) 11 | 12 | If the event being handled has a causality with any other event, there may be problems caused by the two being disordering. 13 | 14 | This article will discuss the situations and solutions we may encounter on the event subscriber apps based on the above truth. 15 | 16 | ## Cases 17 | 18 | We make the following conventions. 19 | 20 | 1. We focus on a user score service that subscribes to some distributed events. 21 | 2. `m1` and `m2` are two events that occurred **successively**. 22 | 3. `t1` and `t2` are when the service receives and handles the events m1 and m2, respectively. 23 | 4. `t1 < t2` means that t1 is earlier than t2, which is called ordered; `t1 > t2` means that t1 is later than t2, which is called disordered. 24 | 5. C (configuration) represents the state of the subscriber service. `C0` is the initial state, `CF` is the expected final state, and `CW` is the wrong final state. 25 | 26 | ### Case 1 27 | 28 | * Event m1: event of user A created 29 | * Event m2: event of user B created 30 | * Handler jobs: Create LocalUser entities locally according to m1 and m2, respectively 31 | * Analysis: m1 and m2 are non-causal and ordering insensitive 32 | * t1 < t2 (ordered): 33 | 34 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 35 | 36 | * t1 > t2 (disordered): 37 | 38 | [![s1-disordered](https://user-images.githubusercontent.com/30018771/194247285-e62dc690-e691-4c38-8616-6e4f12a81975.png)](https://excalidraw.com/#json=94-kB06wg4IZdyFTBNoSe,hyQGl4eKRmgHXAU027iy0w) 39 | 40 | We don't need to intervene in it. 41 | 42 | ### Case 2 43 | 44 | * Event m1: event of user A created 45 | * Event m2: event of order 1 paid 46 | * Handler jobs: According to m1, create a `LocalUser` entity locally; according to m2, increase `LocalUser.Score` 47 | * Analysis: m1 and m2 are causal; m1 and m2 are ordering sensitive, but the "entity not found exception" intercepts the disordering, so handlers are idempotent, which avoids the consistency problem 48 | * t1 < t2 (ordered): 49 | 50 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 51 | 52 | * t1 > t2 (disordered): 53 | 54 | [![s2-disordered](https://user-images.githubusercontent.com/30018771/201462287-6155f1b9-dd9f-4452-bb3d-921b2e1b876b.png)](https://excalidraw.com/#json=6azro2d7yq3YVGqmmFkeE,vX5ZLgF_as_otPyRgZX0Yg) 55 | 56 | We don't need to intervene in it. After m1 is handled, m2 delays retry handling, essentially reaching the order. 57 | 58 | ### Case 3 59 | 60 | * Event m1: event of order 1 paid 61 | * Event m2: event of order 1 canceled 62 | * Handler jobs: According to m1, increase `LocalUser.Score`; according to m2, deduct `LocalUser.Score`; The score is minimum deducted to 0 and will not be a negative number 63 | * Analysis: m1 and m2 are causal; m1 and m2 are ordering sensitive, and handlers are not idempotent, so there will be a consistency problem 64 | * t1 < t2 (ordered): 65 | 66 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 67 | 68 | * t1 > t2 (disordered): 69 | 70 | [![s3-disordered](https://user-images.githubusercontent.com/30018771/201470772-4a01a4fe-f933-4d2c-82cf-e59fd2905bec.png)](https://excalidraw.com/#json=1wnVTL1RZWpvXu3YkHpj8,uFffnJLeWEMTLk3U33Z2ZA) 71 | 72 | The user score service creates `LocalOrder` entities to record the order handling states. 73 | 74 | ```CSharp 75 | public class LocalOrder : AggregateRoot 76 | { 77 | public bool HasPaidEventHandled { get; set; } // set to true after handling m1 78 | } 79 | ``` 80 | 81 | If the m2 handler finds `OrderCanceledEto.OrderPaidTime != null` but `LocalOrder.HasPaidEventHandled == false`, it throws an exception. After m1 is handled, m2 delays retry handling, essentially reaching the order. 82 | 83 | We essentially transformed Case 3 into Case 2, thus achieving idempotency. 84 | 85 | #### After the Change 86 | 87 | * t1 < t2 (ordered): 88 | 89 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 90 | 91 | * t1 > t2 (disordered): 92 | 93 | [![s3-resolved](https://user-images.githubusercontent.com/30018771/201462287-6155f1b9-dd9f-4452-bb3d-921b2e1b876b.png)](https://excalidraw.com/#json=6azro2d7yq3YVGqmmFkeE,vX5ZLgF_as_otPyRgZX0Yg) 94 | 95 | ### Case 4 96 | 97 | * Event m1: event of user A updated (`Region` changed) 98 | * Event m2: event of order 1 paid 99 | * Handler jobs: According to m1, clear `LocalUser.Score` if `UserEto.Region != LocalUser.Region`. According to m2, increase `LocalUser.Score` 100 | * Analysis: m1 and m2 are causal; m1 and m2 are ordering sensitive, and handlers are not idempotent, so there will be a consistency problem 101 | * t1 < t2 (ordered): 102 | 103 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 104 | 105 | * t1 > t2 (disordered): 106 | 107 | [![s4-disordered](https://user-images.githubusercontent.com/30018771/201470772-4a01a4fe-f933-4d2c-82cf-e59fd2905bec.png)](https://excalidraw.com/#json=1wnVTL1RZWpvXu3YkHpj8,uFffnJLeWEMTLk3U33Z2ZA) 108 | 109 | We can solve the problem with these changes: 110 | 111 | 1. Add a new `RegionVersion` property in the `User` entity with the default value of 0. It increases by 1 once the user's region is changed. 112 | 2. The user score service uses `LocalUserRegion.Score` to record user scores instead of `LocalUser.Score`. 113 | ```CSharp 114 | public class LocalUserRegion : AggregateRoot 115 | { 116 | public Guid UserId { get; set; } 117 | public string Region { get; set; } 118 | public int RegionVersion { get; set; } 119 | public int Score { get; set; } 120 | } 121 | ``` 122 | 3. When handling m1, if `UserEto.RegionVersion` is new, create an new `LocalUserRegion` entity with the initial score of 0, which equals clearing the user's scores once his region is changed. 123 | 4. When the user pays, the local service invokes the Identity remote service to query and sets the found `UserDto.RegionVersion` as `OrderPaidEto.UserRegionVersion` in the event m2. 124 | 5. When handling m2, according to m1, increase the corresponding `LocalUserRegion.Score`. 125 | 126 | We made the handlers idempotent by disentangling the causality of m1 and m2. 127 | 128 | #### After the Change 129 | 130 | * t1 < t2 (ordered): 131 | 132 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 133 | 134 | * t1 > t2 (disordered): 135 | 136 | [![s4-resolved](https://user-images.githubusercontent.com/30018771/201462287-6155f1b9-dd9f-4452-bb3d-921b2e1b876b.png)](https://excalidraw.com/#json=6azro2d7yq3YVGqmmFkeE,vX5ZLgF_as_otPyRgZX0Yg) 137 | 138 | ### Case 5: ABP Entity Synchronizer 139 | 140 | In the DDD practice of the ABP framework, modules use entity synchronizers to redundant data of external entities. A typical case is the BlogUserSynchronizer [[3]](#references) of the Blogging module. What's unique about this case is that stale events can be skipped handling if not strictly required. 141 | 142 | * Event m1: event of User A updated 143 | * Event m2: event of User A updated 144 | * Handler jobs: According to m1 and m2, update the user information in the `LocalUser` entity. 145 | * Analysis: Once m2 is handled earlier than m1, the stale data will overwrite the latest data, so there will be a consistency problem 146 | * t1 < t2 (ordered): 147 | 148 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 149 | 150 | * t1 > t2 (disordered): 151 | 152 | [![s5-disordered](https://user-images.githubusercontent.com/30018771/201468277-40c792ce-9a9f-4b29-b46c-c4392b3b79bb.png)](https://excalidraw.com/#json=SwmSL9qcgrFZA5UV8HuPD,_LiJ20bKVHx8D7x5c-KfAw) 153 | 154 | Let's add a new integer property named `EntityVersion`. Its default value is 0 and increments by one every time the entity changes. When the entity synchronizer gets an `EntityUpdatedEto` event, skip handling if `UserEto.EntityVersion <= LocalUser.EntityVersion` is satisfied. That's it, and we solved the problem. I tried implementing the above feature in the ABP framework. See PR #14197 [[4]](#references). 155 | 156 | #### After the Change 157 | 158 | * t1 < t2 (ordered): 159 | 160 | [![ordered](https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png)](https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA) 161 | 162 | * t1 > t2 (disordered): 163 | 164 | [![s5-resolved](https://user-images.githubusercontent.com/30018771/201468757-793bc2bb-5d47-4c7d-bcff-32e705e24d1e.png)](https://excalidraw.com/#json=L0ZI13yl9EYwWtyQC6hwK,CcVzzXgnznQSGA7x8qLGng) 165 | 166 | ## Solution Summary 167 | 168 | We believe that there are the following ideas for solving event disordering. 169 | 170 | 1. Try to keep the business logic of the DistributedEventHandler simple to spot potential event disordering problems. 171 | 2. In some cases, we can make handlers idempotent by recording the entity's states locally, as Case 3 did above. 172 | 3. In some cases, we can disentangle causality by improving the business design, as Case 4 did above. 173 | 4. Entity synchronizers can be designed with EntityVersion to avoid synchronizing to stale data. 174 | 175 | ## Conclusion 176 | 177 | Even if your app is currently monolithic, you should be concerned about the event disordering. That is a preparation for possible architectural changes in the future. Also, please give up implementing linearizability, as it is impossible in microservices or multi-database scenarios. 178 | 179 | In several cases mentioned in this article, it seems that it is not difficult for developers to find the hidden dangers of consistency problems. However, the business is more complex in actual production, and the diverse events will be hard-considered. Even if we find out all possible causalities and deal with them during development, can we ensure that nothing will go wrong when the business changes? The answer is probably no. 180 | 181 | There is no silver bullet to the distributed consistency problem. It's always there. Developers can only reduce complexity, dissolve the causality or manually implement idempotency. 182 | 183 | ## References 184 | 185 | 1. Herlihy, Maurice P.; Wing, Jeannette M. (1987). "Axioms for Concurrent Objects". Proceedings of the 14th ACM SIGACT-SIGPLAN Symposium on Principles of Programming Languages, POPL '87. p. 13 186 | 2. Daniel Wu. (2021). "Messaging Reliability and Ordering". https://danielw.cn/messaging-reliability-and-order 187 | 3. GitHub abpframework/abp repository. BlogUserSynchronizer.cs. https://github.com/abpframework/abp/blob/1275f2207fc39d850d23472294e456c8504f20d2/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users/BlogUserSynchronizer.cs 188 | 4. GitHub abpframework/abp repository. PR #14197. https://github.com/abpframework/abp/pull/14197 189 | --------------------------------------------------------------------------------