## 思路 我更倾向逐个功能地推进应用开发。本节将说明如何在 UI 上显示产品列表。 1. 首先,我们会为`Product`实体定义一个`ProductDto`; 2. 然后,我们将创建一个向表示层返回产品列表的应用服务方法; 3. 此外,我们将学习如何自动映射`Product`到`ProductDto` 在创建 UI 之前,我将向您展示如何为应用服务编写**自动化测试**。这样,在开始 UI 开发之前,我们就可以确定应用服务是否正常工作。 在整个在开发过程中,我们将探索 ABP 框架的一些能力,例如自动 API 控制器和动态 JavaScript 代理系统。 最后,我们将创建一个新页面,并在其中添加一个数据表,然后从服务端获取产品列表,并将其显示在 UI 上。 梳理完思路,我们从创建一个`ProductDto`类开始。 ## ProductDto 类 DTO 用于在应用层和表示层之间传输数据。最佳实践是将 DTO 返回到表示层而不是实体,因为将实体直接暴露给表示层可能导致序列化和安全问题,有了DTO,我们不但可以抽象实体,对接口展示内容也更加可控。 为了在 UI 层中可复用,DTO 规定在Application.Contracts项目中进行定义。我们首先在\*.Application.Contracts项目的Products文件夹中创建一个`ProductDto`类: ``` using System; using Volo.Abp.Application.Dtos; namespace ProductManagement.Products {     public class ProductDto : AuditedEntityDto<Guid>     {         public Guid CategoryId { get; set; }         public string CategoryName { get; set; }         public string Name { get; set; }         public float Price { get; set; }         public bool IsFreeCargo { get; set; }         public DateTime ReleaseDate { get; set; }         public ProductStockState StockState { get; set; }     } } ``` `ProductDto`与实体类基本相似,但又有以下区别: * 它派生自`AuditedEntityDto<Guid>`,它定义了`Id`、`CreationTime`、`CreatorId`、`LastModificationTime`和`LastModifierId`属性(我们不需要做删除审计`DeletionTime`,因为删除的实体不是从数据库中读取的)。 * 我们没有向实体`Category`添加导航属性,而是使用了一个`string`类型的`CategoryName`的属性,用以在 UI 上显示。 我们将使用使用`ProductDto`类从`IProductAppService`接口返回产品列表。 ## 产品应用服务 **应用服务**实现了应用的业务逻辑,UI 调用它们用于用户交互。通常,应用服务方法返回一个 DTO。 #### 1 应用服务与 API 控制器 >[warning] ABP的应用服务和MVC 中的 API 控制器有何区别? 您可以将应用服务与 [ASP.NET](http://ASP.NET) Core MVC 中的 API 控制器进行比较。虽然它们有相似之处,但是: 1. 应用服务更适合 DDD ,它们不依赖于特定的 UI 技术。 2. 此外,ABP 可以自动将您的应用服务公开为 HTTP API。 我们在\*.Application.Contracts项目的Products文件夹中创建一个`IProductAppService`接口: ``` using System.Threading.Tasks; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; namespace ProductManagement.Products {     public interface IProductAppService : IApplicationService     {         Task<PagedResultDto<ProductDto>> GetListAsync(PagedAndSortedResultRequestDto input);     } } ``` 我们可以看到一些预定义的 ABP 类型: * `IProductAppService`约定从`IApplicationService`接口,这样ABP 就可以识别应用服务。 * `GetListAsync`方法的入参`PagedAndSortedResultRequestDto`是 ABP 框架的标准 DTO 类,它定义了`MaxResultCount`、`SkipCount`和`Sorting`属性。 * `GetListAsync`方法返回`PagedResultDto<ProductDto>`,其中包含一个`TotalCount`属性和一个`ProductDto`对象集合,这是使用 ABP 框架返回分页结果的便捷方式。 当然,您可以使用自己的 DTO 代替这些预定义的 DTO。但是,当您想要标准化一些常见问题,避免到处都使用相同的命名时,它们非常有用。 #### 2 异步方法 将所有应用服务方法定义为异步方法是最佳实践。如果您定义为同步方法,在某些情况下,某些 ABP 功能(例如工作单元)可能无法按预期工作。 现在,我们可以实现`IProductAppService`接口来执行用例。 #### 3 产品应用服务 我们在ProductManagement.Application项目中创建一个`ProductAppService`类: ``` using System.Linq.Dynamic.Core; using System.Threading.Tasks; using Volo.Abp.Application.Dtos; using Volo.Abp.Domain.Repositories; namespace ProductManagement.Products {     public class ProductAppService : ProductManagementAppService, IProductAppService     {         private readonly IRepository<Product, Guid>  _productRepository;         public ProductAppService(IRepository<Product, Guid> productRepository)         {             _productRepository = productRepository;         }         public async Task<PagedResultDto<ProductDto>> GetListAsync(PagedAndSortedResultRequestDto input)         {             /* TODO: Implementation */         }     } } ``` `ProductAppService`派生自`ProductManagementAppService`,它在启动模板中定义,可用作应用服务的基类。它实现了之前定义的`IProductAppService`接口,并注入`IRepository<Product, Guid>`服务。这就是通用**默认存储**库,方面我们对数据库执行操作(ABP 自动为所有聚合根实体提供默认存储库实现)。 我们实现`GetListAsync`方法,如下代码块所示: ``` public async Task<PagedResultDto<ProductDto>> GetListAsync(PagedAndSortedResultRequestDto input) {     var queryable = await _productRepository.WithDetailsAsync(x => x.Category);     queryable = queryable         .Skip(input.SkipCount)         .Take(input.MaxResultCount)         .OrderBy(input.Sorting ?? nameof(Product.Name));     var products = await AsyncExecuter.ToListAsync(queryable);     var count = await _productRepository.GetCountAsync();     return new PagedResultDto<ProductDto>(         count,         ObjectMapper.Map<List<Product>, List<ProductDto>>(products)     ); } ``` 这里,`_productRepository.WithDetailsAsync`返回一个包含产品类别的`IQueryable<Product>`对象,(`WithDetailsAsync`方法类似于 EF Core 的`Include`扩展方法,用于将相关数据加载到查询中)。于是,我们就可以方便地使用标准的(**LINQ**) 扩展方法,比如`Skip`、`Take`和`OrderBy`等。 `AsyncExecuter`服务(基类中预先注入)用于执行`IQueryable`对象,这使得可以使用异步 LINQ 扩展方法执行数据库查询,而无需依赖应用程序层中的 EF Core 包。(我们将在\[*第 6 章* \] 中对`AsyncExecuter`进行更详细的探讨) 最后,我们使用`ObjectMapper`服务(在基类中预先注入)将`Product`集合映射到`ProductDto`集合。 ## 对象映射 `ObjectMapper`(`IObjectMapper`)会自动使用**AutoMapper**库进行类型转换。它要求我们在使用之前预先定义映射关系。启动模板包含一个配置文件类,您可以在其中创建映射。 在ProductManage.Application项目中打开`ProductManagementApplicationAutoMapperProfile`类,并将其更改为以下内容: ``` using AutoMapper; using ProductManagement.Products; namespace ProductManagement {     public class ProductManagementApplicationAutoMapperProfile : Profile     {         public ProductManagementApplicationAutoMapperProfile()         {             CreateMap<Product, ProductDto>();         }     } } ``` 如`CreateMap`所定义的映射。它可以自动将`Product`转换为`ProductDto`对象。 AutoMapper中有一个有趣的功能:**Flattening**,它默认会将复杂的对象模型展平为更简单的模型。在这个例子中,`Product`类有一个`Category`属性,而`Category`类也有一个`Name`属性。因此,如果要访问产品的类别名称,则应使用`Product.Category.Name`表达式。但是,`ProductDto`的`CategoryName`可以直接使用`ProductDto.CategoryName`表达式进行访问。AutoMapper 会通过展平`Category.Name`来自动映射成`CategoryName`。 应用层服务已经基本完成。在开始 UI 之前,我们先介绍如何为应用层编写自动化测试。