项目

集成模块:实现集成服务

到目前为止,您已经创建了两个模块:用于存储和管理产品的 Catalog 模块,以及用于接收订单的 Ordering 模块。然而,这些模块彼此完全独立。主应用程序将它们组合在一起在同一应用程序中运行,但这些模块之间并不相互通信。

在本部分以及接下来的两部分中,您将学习实现三种常见的集成模式来连接这些模块:

  1. Order 模块将在需要时向 Catalog 模块发起请求以获取产品信息。
  2. Catalog 模块将监听来自 Ordering 模块的事件,以便在下单时减少产品的库存数量。
  3. 最后,您将执行一个包含产品和订单数据的数据库查询。

让我们从第一种模式开始:集成服务。

集成服务的需求

回想 上一部分,订单页面显示的是产品标识符而不是产品名称:

abp-studio-browser-orders-menu-item

这是因为 Ordering 模块无法访问产品数据,因此它无法执行 JOIN 查询来从 Products 表中获取产品名称。这是模块化设计的自然结果。然而,您也不希望在产品界面上显示产品的 GUID 标识符,这并非良好的用户体验。

作为该问题的解决方案,Ordering 模块可以使用 集成服务 向 Catalog 模块请求产品名称。ABP 中的集成服务概念专为模块间(在模块化应用程序中)和微服务间(在分布式系统中)的请求/响应式通信而设计。

当您为模块间通信实现集成服务时,如果以后将您的解决方案转换为微服务系统并将您的模块转换为服务,您可以轻松地将它们转换为 REST API 调用。

创建产品集成服务

第一步是在 Catalog 模块中创建一个集成服务,以便其他模块可以使用它。

您将在 ModularCrm.Catalog.Contracts 包中定义一个接口,并在 ModularCrm.Catalog 包中实现它。

定义 IProductIntegrationService 接口

在您的 IDE 中打开 ModularCrm.Catalog .NET 解决方案,找到 ModularCrm.Catalog.Contracts 项目,在该项目内创建一个 Integration 文件夹,然后在该文件夹中创建一个名为 IProductIntegrationService 的接口。最终的文件夹结构应如下所示:

vscode-product-integration-service

创建 Integration 文件夹并非必需,但这可以作为一种良好的实践,将集成相关的代码与模块的业务逻辑隔离开来。

打开 IProductIntegrationService.cs 文件,并将其内容替换为以下代码块:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Application.Services;

namespace ModularCrm.Catalog.Integration;

[IntegrationService]
public interface IProductIntegrationService : IApplicationService
{
    Task<List<ProductDto>> GetProductsByIdsAsync(List<Guid> ids);
}

IProductIntegrationService 与典型的 应用服务 非常相似。唯一的区别在于接口定义顶部有一个 [IntegrationService] 特性。通过这种方式,ABP 可以识别它们并采取不同的行为(例如,如果您配置了 自动 API 控制器 功能,ABP 默认不会将 集成服务 公开为 HTTP API)。

IProductIntegrationService 有一个单一的方法,它接收一个产品 ID 列表,并返回这些 ID 对应的 ProductDto 对象列表。任何其他模块都可以使用此方法,在仅拥有某些产品 ID 时获取产品的详细信息。这正是我们 Ordering 模块所需要的。

设计建议

您可能会想,我们是否可以从其他模块使用现有的应用服务(如 IProductAppService),而不是创建特定的集成服务。从技术上讲,您可以这样做,ABP 没有限制。然而,从良好设计和最佳实践的角度出发,我们不建议这样做。因为应用服务是专门为表示层使用而设计的。它们将有不同的授权和验证逻辑,需要不同的 DTO 输入和输出属性,有不同的性能、优化和缓存要求等等。最重要的是,所有这些都将随着 UI 需求的变化而改变,这些变化以后可能会破坏您的集成。最好的做法是实现专为此目的设计并优化的特定集成 API。

我们复用了为 IProductAppService 创建的 ProductDto 对象,从维护的角度来看是合理的。但如果您认为将来集成服务的结果与应用服务的结果会不同,最好从一开始就分开它们,这样以后就不需要引入破坏性变更。

实现 ProductIntegrationService

我们已经定义了集成服务接口。现在,您可以在 ModularCrm.Catalog 项目中实现它。创建一个 Integration 文件夹,然后在该文件夹中创建一个 ProductIntegrationService 类。最终的文件夹结构应如下所示:

visual-studio-product-integration-service-implementation

打开 ProductIntegrationService.cs 文件,并将其内容替换为以下代码块:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;

namespace ModularCrm.Catalog.Integration;

public class ProductIntegrationService
    : CatalogAppService, IProductIntegrationService
{
    private readonly IRepository<Product, Guid> _productRepository;

    public ProductIntegrationService(IRepository<Product, Guid> productRepository)
    {
        _productRepository = productRepository;
    }

    public async Task<List<ProductDto>> GetProductsByIdsAsync(List<Guid> ids)
    {
        var products = await _productRepository.GetListAsync(
            product => ids.Contains(product.Id)
        );

        return ObjectMapper.Map<List<Product>, List<ProductDto>>(products);
    }
}

实现非常简单。只是使用一个 仓储 来查询 Product 实体

在这里,您直接使用了 List<T> 类,但您也可以将输入和输出包装到 DTO 中。这样,就有可能向这些 DTO 添加新属性,而无需更改集成服务方法的签名(也不会为客户端模块引入破坏性变更)。

使用产品集成服务

产品集成服务已准备就绪,可供其他模块使用。在本节中,您将在 Ordering 模块中使用它,将产品 ID 转换为产品名称。

添加对 ModularCrm.Catalog.Contracts 包的引用

打开 ABP Studio UI,如果应用程序正在运行,请停止它。然后在 ABP Studio 中打开 解决方案资源管理器,右键点击 ModularCrm.Ordering 包并选择 添加包引用 命令:

abp-studio-add-package-reference-4

在打开的对话框中,选择 此解决方案 选项卡,找到并勾选 ModularCrm.Catalog.Contracts 包,然后点击确定按钮:

abp-studio-add-package-reference-dialog-3

ABP Studio 将添加包引用并安排 模块 依赖关系。

除了直接添加此类包引用,还可以先导入模块(右键点击 ModularCrm.Ordering 模块,选择 导入模块 命令并导入 ModularCrm.Catalog 模块),然后再添加包引用。当您从本地模块添加包引用时,ABP 会自动导入模块,但对于其他来源,您可能需要手动执行此操作。

使用产品集成服务

现在,您可以在 Ordering 模块代码库中注入并使用 IProductIntegrationService

打开 ModularCrm.Ordering .NET 解决方案中 ModularCrm.Ordering 项目的 OrderAppService 类,并将其内容更改为以下代码块:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ModularCrm.Catalog.Integration;
using Volo.Abp.Domain.Repositories;

namespace ModularCrm.Ordering;

public class OrderAppService : OrderingAppService, IOrderAppService
{
    private readonly IRepository<Order, Guid> _orderRepository;
    private readonly IProductIntegrationService _productIntegrationService;

    public OrderAppService(
        IRepository<Order, Guid> orderRepository,
        IProductIntegrationService productIntegrationService)
    {
        _orderRepository = orderRepository;
        _productIntegrationService = productIntegrationService;
    }

    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)
    {
        var order = new Order
        {
            CustomerName = input.CustomerName,
            ProductId = input.ProductId,
            State = OrderState.Placed
        };

        await _orderRepository.InsertAsync(order);
    }
}

同时,打开 ModularCrm.Ordering .NET 解决方案中 ModularCrm.Ordering.Contracts 项目的 OrderDto 类,并向其中添加一个 ProductName 属性:

using System;

namespace ModularCrm.Ordering;

public class OrderDto
{
    public Guid Id { get; set; }
    public string CustomerName { get; set; } = null!;
    public Guid ProductId { get; set; }
    public OrderState State { get; set; }
    public string ProductName { get; set; } = null!; // 新增属性
}

最后,打开 OrderingApplicationMappers 类(位于 ModularCrm.Ordering .NET 解决方案的 ModularCrm.Ordering 项目 Services 文件夹下的 OrderingApplicationMappers.cs 文件),并添加以下映射类:

[Mapper]
public partial class OrderToOrderDtoMapper : MapperBase<Order, OrderDto>
{
    [MapperIgnoreTarget(nameof(OrderDto.ProductName))]
    public override partial OrderDto Map(Order source);

    [MapperIgnoreTarget(nameof(OrderDto.ProductName))]
    public override partial void Map(Order source, OrderDto destination);
}

让我们看看我们更改了什么:

  • 我们向 OrderDto 类添加了 ProductName 属性来存储产品名称。
  • 注入 IProductIntegrationService 接口,以便您可以使用它来请求产品。
  • GetListAsync 方法中:
    • 首先像以前一样从订单模块的数据库中获取订单。
    • 接下来,准备一个唯一的产品 ID 列表,因为 GetProductsByIdsAsync 方法需要它。
    • 然后调用 IProductIntegrationService.GetProductsByIdsAsync 方法以获取 List<ProductDto> 对象。
    • 最后一行,我们将产品列表转换为字典,其中键是 Guid Id,值是 string Name。这样,我们可以轻松地通过产品 ID 查找产品名称。
    • 最后,我们将订单映射到 OrderDto 对象,并通过在字典中查找产品 ID 来设置产品名称。

打开 Index.cshtml 文件,将 @order.ProductId 部分更改为 @Order.ProductName,以写入产品名称而不是产品 ID。最终的 Index.cshtml 内容应如下所示:

@page
@model ModularCrm.Ordering.UI.Pages.Ordering.IndexModel

<h1>Orders</h1>

<abp-card>
    <abp-card-body>
        <abp-list-group>
            @foreach (var order in Model.Orders)
            {
                <abp-list-group-item>
                    <strong>Customer:</strong> @order.CustomerName <br />
                    <strong>Product:</strong> @order.ProductName <br />
                    <strong>State:</strong> @order.State
                </abp-list-group-item>
            }
        </abp-list-group>
    </abp-card-body>
</abp-card>

就这样。现在,您可以在 ABP Studio 中对主应用程序进行图形构建并运行它来查看结果:

abp-studio-browser-list-of-orders-with-product-name

如您所见,我们现在可以看到产品名称而不是产品 ID。

设计建议

建议您尽量减少此类通信,不要将模块彼此耦合。这可能会使您的解决方案变得复杂,也可能降低系统性能。当您需要这样做时,请考虑性能并尝试进行一些优化。例如,如果 Ordering 模块频繁需要产品数据,您可以使用某种 缓存层,这样它就不会频繁地向 Catalog 模块发起请求。特别是如果您考虑将来将系统转换为微服务解决方案,过多的直接集成 API 调用可能会成为性能瓶颈。

总结

按照本教程这一部分所介绍的方式,您可以轻松地为您的模块创建集成服务,并在任何其他模块中使用这些集成服务。在 下一部分 中,我们将探讨模块之间基于事件的通信。


在本文档中