集成模块:通过消息(事件)通信
另一种常见的模块间通信方式是消息传递。通过发布和处理消息,一个模块可以在另一个模块中发生事件时执行相应的操作。
理解事件总线类型
ABP 提供了两种类型的事件总线,用于松耦合通信:
- 本地事件总线 适用于进程内消息传递。由于在模块化单体应用中,发布者和订阅者都在同一进程中,它们可以在进程内进行通信,无需外部消息代理。
- 分布式事件总线 通常用于进程间消息传递,例如微服务之间发布和订阅分布式事件。然而,ABP 的分布式事件总线默认以本地(进程内)方式工作(实际上,默认情况下它在底层使用本地事件总线),除非您配置了外部消息代理。
如果您考虑日后将模块化单体应用转换为微服务系统,最好使用默认本地/进程内实现的分布式事件总线。它已经支持数据库级别的事务性事件执行,并且没有性能损失。如果您切换到外部提供程序(RabbitMQ、Kafka 等),您无需更改应用程序代码。
另一方面,如果您希望发布事件并始终由同一模块订阅,则应使用本地事件总线。这样,如果日后切换到微服务,您不会意外地(且不必要地)分发一个本地事件。两种类型的事件总线可以在同一系统中使用;只需理解它们并正确使用即可。
由于我们将在不同模块之间使用消息传递(事件),因此我们将使用分布式事件总线。
发布事件
在示例场景中,我们希望在下新订单时发布一个事件。订单模块将发布此事件,因为它知道何时有新订单。目录模块将订阅该事件,并在新订单下单时收到通知。这将减少与新订单相关产品的库存数量。场景非常简单,我们来实现它。
定义事件类
在您的 IDE 中打开 ModularCrm.Ordering 模块,找到 ModularCrm.Ordering.Contracts 项目,创建一个 Events 文件夹,并在该文件夹内创建一个 OrderPlacedEto 类。最终的文件夹结构应如下所示:
我们将 OrderPlacedEto 类放在 ModularCrm.Ordering.Contracts 项目中,因为其他模块可以引用和使用该项目,而无需访问订单模块的内部实现。OrderPlacedEto 类的定义如下:
using System;
namespace ModularCrm.Ordering.Events;
public class OrderPlacedEto
{
public string CustomerName { get; set; } = null!;
public Guid ProductId { get; set; }
}
OrderPlacedEto 非常简单。它是一个普通的 C# 类,用于传输与事件相关的数据(ETO 是 Event Transfer Object 的缩写,是 ABP 团队建议的命名约定,但不是技术要求)。如果需要,您可以添加更多属性,但对于本教程来说,这已经足够了。
使用 IDistributedEventBus 服务
IDistributedEventBus 服务用于向事件总线发布事件。打开 ModularCrm.Ordering 模块的 .NET 解决方案,并按如下方式更新 OrderAppService:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ModularCrm.Catalog.Integration;
using ModularCrm.Ordering.Events;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EventBus.Distributed;
namespace ModularCrm.Ordering;
public class OrderAppService : OrderingAppService, IOrderAppService
{
private readonly IRepository<Order, Guid> _orderRepository;
private readonly IProductIntegrationService _productIntegrationService;
private readonly IDistributedEventBus _distributedEventBus;
public OrderAppService(
IRepository<Order, Guid> orderRepository,
IProductIntegrationService productIntegrationService,
IDistributedEventBus distributedEventBus)
{
_orderRepository = orderRepository;
_productIntegrationService = productIntegrationService;
_distributedEventBus = distributedEventBus;
}
public async Task<List<OrderDto>> GetListAsync()
{
var orders = await _orderRepository.GetListAsync();
// 准备我们需要的产品列表
var productIds = orders.Select(o => o.ProductId).Distinct().ToList();
var products = (await _productIntegrationService
.GetProductsByIdsAsync(productIds))
.ToDictionary(p => p.Id, p => p.Name);
var orderDtos = ObjectMapper.Map<List<Order>, List<OrderDto>>(orders);
orderDtos.ForEach(orderDto =>
{
orderDto.ProductName = products[orderDto.ProductId];
});
return orderDtos;
}
public async Task CreateAsync(OrderCreationDto input)
{
// 创建一个新的 Order 实体
var order = new Order
{
CustomerName = input.CustomerName,
ProductId = input.ProductId,
State = OrderState.Placed
};
// 保存到数据库
await _orderRepository.InsertAsync(order);
// 发布一个事件,以便其他模块可以获知
await _distributedEventBus.PublishAsync(
new OrderPlacedEto
{
ProductId = order.ProductId,
CustomerName = order.CustomerName
});
}
}
我们修改了 CreateAsync 方法。现在它创建一个新的 Order 实体,保存到数据库,最后发布一个 OrderPlacedEto 事件。
订阅事件
本节将在目录模块中订阅 OrderPlacedEto 事件,并在新订单下单时减少相关产品的库存数量。
添加对 ModularCrm.Ordering.Contracts 包的引用
由于 OrderPlacedEto 类在 ModularCrm.Ordering.Contracts 项目中,我们必须将该包的引用添加到目录模块。这次,我们将使用 ABP Studio 的 导入模块 功能(作为我们在上一部分 添加对 ModularCrm.Catalog.Contracts 包的引用 一节中所用方法的替代方案)。
打开 ABP Studio 界面,如果应用程序正在运行,请先停止它。然后在 ABP Studio 中打开 解决方案资源管理器,右键单击 ModularCrm.Catalog 模块,选择 导入模块 命令:
在打开的对话框中,找到并选择 ModularCrm.Ordering 模块,勾选 安装此模块 选项,然后点击确定按钮:
点击确定按钮后,订单模块将被导入到目录模块,并打开安装对话框:
在此,从左侧选择 ModularCrm.Ordering.Contracts 包(因为我们希望添加该包的引用),并在中间区域选择 ModularCrm.Catalog 包(因为我们希望将包引用添加到该项目)。同时,在右侧选择 ModularCrm.Ordering 包,并取消选中中间区域的所有包(我们不需要实现或其他包)。然后,点击确定按钮完成安装操作。
您可以检查 ABP Studio 的 解决方案资源管理器 面板,查看模块导入和项目引用(依赖项)。
处理 OrderPlacedEto 事件
现在,由于目录模块已具有 ModularCrm.Ordering.Contracts 包的引用,因此可以在目录模块内部使用 OrderPlacedEto 类。
在您的 IDE 中打开目录模块的 .NET 解决方案,找到 ModularCrm.Catalog 项目,创建一个新的 EventHandlers 文件夹,并在该文件夹内创建一个 OrderEventHandler 类。最终的文件夹结构应如下所示:
将 OrderEventHandler.cs 文件的内容替换为以下代码块:
using System;
using System.Threading.Tasks;
using ModularCrm.Ordering.Events;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EventBus.Distributed;
namespace ModularCrm.Catalog.EventHandlers;
public class OrderEventHandler :
IDistributedEventHandler<OrderPlacedEto>,
ITransientDependency
{
private readonly IRepository<Product, Guid> _productRepository;
public OrderEventHandler(IRepository<Product, Guid> productRepository)
{
_productRepository = productRepository;
}
public async Task HandleEventAsync(OrderPlacedEto eventData)
{
// 查找相关产品
var product = await _productRepository.FindAsync(eventData.ProductId);
if (product == null)
{
return;
}
// 减少库存数量
product.StockCount = product.StockCount - 1;
// 在数据库中更新实体
await _productRepository.UpdateAsync(product);
}
}
OrderEventHandler 实现了 IDistributedEventHandler<OrderPlacedEto> 接口。通过这种方式,ABP 会识别该类并自动订阅相关事件。实现 ITransientDependency 会将 OrderEventHandler 类注册到依赖注入系统中作为一个瞬态对象。
我们在事件处理方法(HandleEventAsync)中注入了产品仓储并更新了库存数量。就是这样。
测试订单创建
为了使本教程更加专注,我们不会为创建订单创建 UI。您可以轻松地在用户界面上创建一个表单来创建订单。在本节中,我们将仅使用 Swagger UI 进行测试。
在 ABP Studio 的 解决方案运行器 面板中对 ModularCrm 应用程序执行图形构建,运行它,并按照之前的演示浏览应用程序 UI。
应用程序运行并准备就绪后,手动在 URL 末尾输入 /swagger 并按回车键。您应该会看到用于发现和测试 HTTP API 的 Swagger UI:
找到 Orders API,点击 试一试 按钮,在 请求正文 中输入示例值:
{
"customerName": "David",
"productId": "e6ce1629-cfb1-1af6-e71c-3a16f10f9cc5"
}
重要提示: 此处,您应输入数据库中 CatalogProducts 表的一个有效产品 ID!
按下 执行 按钮后,将创建一个新订单。此时,您可以检查 /Orders 页面以查看新订单是否显示在 UI 上,并检查 /Products 页面以查看相关产品的库存数量是否已减少。
以下是产品和订单页面的示例截图:
我们为产品 C 下了一个新订单。结果,产品 C 的库存数量从 55 减少到 54,并且在订单页面上添加了一行新记录。
结论
在本部分中,我们使用了 ABP 的分布式事件总线在模块之间执行松耦合的消息传递。在 下一部分 ,我们将执行一个包含产品和订单数据的数据库查询,作为集成模块数据的另一种方式。
抠丁客










