## 思路
我更傾向逐個功能地推進應用開發。本節將說明如何在 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 之前,我們先介紹如何為應用層編寫自動化測試。
- 前言
- 第一部分
- 第1章 現代軟件開發和 ABP 框架
- 企業級 Web 開發的挑戰
- ABP框架的能力清單
- 第2章 ABP框架入門
- 安裝 ABP CLI
- 創建新解決方案
- 運行解決方案
- 探索預構建模塊
- 第3章 逐步開發開發ABP應用
- 創建解決方案
- 定義領域對象
- EFCore和數據庫映射
- 定義應用服務
- 測試產品
- 產品列表
- 創建產品
- 編輯產品
- 刪除產品
- 第4章 探索 EventHub解決方案
- 應用介紹
- 架構探索
- 方案運行
- 第二部分
- 第5章 探索ABP基礎架構
- 了解模塊化
- 使用依賴注入系統
- 配置應用程序
- 實現選項模式
- 日志系統
- 第6章 數據訪問基礎架構
- 定義實體
- 定義倉儲庫
- EF Core集成
- 了解 UoW
- 第7章 探索橫切關注點
- 認證授權
- 用戶驗證
- 異常處理
- 第8章 體驗 ABP 的功能和服務
- 獲取當前用戶
- 使用數據過濾
- 控制審計日志
- 緩存數據
- 本地化用戶界面
- 第三部分
- 第9章 理解領域驅動設計
- 介紹 DDD
- 構建基于 DDD 的 解決方案
- 處理多個應用程序
- 了解執行流程
- DDD的通用原則
- 第10章 領域層 Domain
- 領域事件案例分析
- 聚合和實體的設計原則和實踐
- 實現領域服務
- 落地存儲庫
- 構建規約(Specification)
- 領域事件
- 第11章 應用層 Application
- 落地應用服務
- 設計 DTO
- 理解各層的職責
- 第四部分
- 第12章 MVC/Razor 頁面
- 主題系統
- 綁定和壓縮
- 導航菜單
- Bootstrap標簽助手
- 創建表單并驗證
- 使用模態窗口
- 使用JS API
- 調用HTTP API
- 第13章 Blazor WebAssembly UI
- 什么是Blazor
- ABP Blazor UI
- 驗證用戶身份
- 理解主題系統
- 使用菜單
- 使用基本服務
- 使用UI服務
- 消費HTTP API
- 使用全局腳本和樣式
- 第14章 HTTP API 和實時服務
- 構建HTTP API
- 使用HTTP API
- 使用SignalR
- 第五部分
- 第15章 落地模塊化
- 理解模塊化
- 構建支付模塊
- 安裝模塊
- 第16章 實現多租戶
- 理解多租戶
- 多租戶基礎設施
- 使用功能系統
- 何時使用多租戶
- 第17章 構建自動化測試
- 了解ABP測試基礎設施
- 構建單元測試
- 構建集成測試