项目

集成模块:通过消息(事件)通信

另一种常见的模块间通信方式是消息传递。通过发布和处理消息,一个模块可以在另一个模块中发生事件时执行相应的操作。

理解事件总线类型

ABP 提供了两种类型的事件总线,用于松耦合通信:

  • 本地事件总线 适用于进程内消息传递。由于在模块化单体应用中,发布者和订阅者都在同一进程中,它们可以在进程内进行通信,无需外部消息代理。
  • 分布式事件总线 通常用于进程间消息传递,例如微服务之间发布和订阅分布式事件。然而,ABP 的分布式事件总线默认以本地(进程内)方式工作(实际上,默认情况下它在底层使用本地事件总线),除非您配置了外部消息代理。

如果您考虑日后将模块化单体应用转换为微服务系统,最好使用默认本地/进程内实现的分布式事件总线。它已经支持数据库级别的事务性事件执行,并且没有性能损失。如果您切换到外部提供程序(RabbitMQKafka 等),您无需更改应用程序代码。

另一方面,如果您希望发布事件并始终由同一模块订阅,则应使用本地事件总线。这样,如果日后切换到微服务,您不会意外地(且不必要地)分发一个本地事件。两种类型的事件总线可以在同一系统中使用;只需理解它们并正确使用即可。

由于我们将在不同模块之间使用消息传递(事件),因此我们将使用分布式事件总线。

发布事件

在示例场景中,我们希望在下新订单时发布一个事件。订单模块将发布此事件,因为它知道何时有新订单。目录模块将订阅该事件,并在新订单下单时收到通知。这将减少与新订单相关产品的库存数量。场景非常简单,我们来实现它。

定义事件类

在您的 IDE 中打开 ModularCrm.Ordering 模块,找到 ModularCrm.Ordering.Contracts 项目,创建一个 Events 文件夹,并在该文件夹内创建一个 OrderPlacedEto 类。最终的文件夹结构应如下所示:

visual-studio-order-event

我们将 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# 类,用于传输与事件相关的数据(ETOEvent 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 模块,选择 导入模块 命令:

abp-studio-import-module-ordering

在打开的对话框中,找到并选择 ModularCrm.Ordering 模块,勾选 安装此模块 选项,然后点击确定按钮:

abp-studio-import-module-dialog-for-ordering

点击确定按钮后,订单模块将被导入到目录模块,并打开安装对话框:

abp-studio-install-module-dialog-for-ordering

在此,从左侧选择 ModularCrm.Ordering.Contracts 包(因为我们希望添加该包的引用),并在中间区域选择 ModularCrm.Catalog 包(因为我们希望将包引用添加到该项目)。同时,在右侧选择 ModularCrm.Ordering 包,并取消选中中间区域的所有包(我们不需要实现或其他包)。然后,点击确定按钮完成安装操作。

您可以检查 ABP Studio 的 解决方案资源管理器 面板,查看模块导入和项目引用(依赖项)。

abp-studio-imports-and-dependencies

处理 OrderPlacedEto 事件

现在,由于目录模块已具有 ModularCrm.Ordering.Contracts 包的引用,因此可以在目录模块内部使用 OrderPlacedEto 类。

在您的 IDE 中打开目录模块的 .NET 解决方案,找到 ModularCrm.Catalog 项目,创建一个新的 EventHandlers 文件夹,并在该文件夹内创建一个 OrderEventHandler 类。最终的文件夹结构应如下所示:

visual-studio-order-event-handler

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:

abp-studio-swagger-create-order

找到 Orders API,点击 试一试 按钮,在 请求正文 中输入示例值:

{
  "customerName": "David",
  "productId": "e6ce1629-cfb1-1af6-e71c-3a16f10f9cc5"
}

重要提示: 此处,您应输入数据库中 CatalogProducts 表的一个有效产品 ID!

按下 执行 按钮后,将创建一个新订单。此时,您可以检查 /Orders 页面以查看新订单是否显示在 UI 上,并检查 /Products 页面以查看相关产品的库存数量是否已减少。

以下是产品和订单页面的示例截图:

products-orders-pages-crop

我们为产品 C 下了一个新订单。结果,产品 C 的库存数量从 55 减少到 54,并且在订单页面上添加了一行新记录。

结论

在本部分中,我们使用了 ABP 的分布式事件总线在模块之间执行松耦合的消息传递。在 下一部分 ,我们将执行一个包含产品和订单数据的数据库查询,作为集成模块数据的另一种方式。


在本文档中