ABP應用開發(Step by Step)-上篇

張飛洪[廈門]發表於2022-04-26
本文主要通過逐步構建一個CRUD示例程式來介紹 ABP 框架的基礎知識。它涉及到應用開發的多個方面。在本章結束時,您將瞭解ABP 框架的基本開發方式。建議入門人員學習,老手不要浪費您寶貴時間。
 建立解決方案
第1步是為產品管理解決方案(如果您在前面已經建立過了ProductManagement解決方案,可以繼續使用它)。在這裡,我們執行以下ABP CLI 來進行建立:
abp new ProductManagement -t app
我們使用自己熟悉的 IDE 中開啟解決方案,建立資料庫,然後執行 ​​Web 專案。如果您在執行解決方案時遇到問題,請參閱上一章,或者在知識星球裡留言。
現在我們有一個正在可執行的解決方案。下一步建立領域物件來正式啟動編碼。

定義領域物件

該應用的領域很簡單,有ProductCategory兩個實體以及一個ProductStockState列舉,如圖所示:

實體在解決方案的領域層中定義,它分為兩個專案:
  • .Domain用於定義您的實體、值物件、領域服務、儲存庫介面和其他與領域相關的核心類。
  • .Domain.Shared用於定義一些可用於其他層的共享型別。通常,我們在這裡定義列舉和一些常量。

產品類別實體(Category)

Category實體用於對產品進行分類。在ProductManagement.Domain專案中建立一個Categories資料夾,並在其中建立一個Category類:
using System;
using Volo.Abp.Domain.Entities.Auditing;
namespace ProductManagement.Categories
{
    public class Category : AuditedAggregateRoot<Guid>
    {
        public string Name { get; set; }
    }
}
Category類派生自AuditedAggregateRoot<Guid>,這裡Guid是實體的主鍵 (Id) 。您可以使用任何型別的主鍵(例如intlongstring)。
AggregateRoot是一種特殊的實體,用於建立聚合的根實體。它是一個領域驅動設計(DDD) 概念,我們將在接下來的章節中更詳細地討論。
相比AggregateRoot類,AuditedAggregateRoot新增了更多屬性:CreationTimeCreatorIdLastModificationTimeLastModifierId
當您將實體插入資料庫時​​,ABP 會自動給這些屬性賦值,CreationTime會設定為當前時間,CreatorId會自動設定為當前使用者的Id屬性。
關於充血領域模型
在本章中,我們使用公共的 getter 和 setter 來保持實體的簡單性。如果您想建立更豐富的領域模型並應用 DDD 原則和其他最佳實踐,我們將在後面的文章中討論它們。

產品庫存狀態列舉(ProductStockState)

ProductStockState是一個簡單的列舉,用來設定和跟蹤產品庫存。
我們在*.Domain.Shared專案中建立一個Products*資料夾和一個列舉ProductStockState
namespace ProductManagement.Products
{
    public enum ProductStockState : byte
    {
        PreOrder,
        InStock,
        NotAvailable,
        Stopped
    }
}
我們將在資料傳輸物件(DTO) 和介面層複用該列舉。

產品實體(Product)

在.Domain專案中建立一個Products資料夾,並在其中建立一個類Product
using System;
using Volo.Abp.Domain.Entities.Auditing;
using ProductManagement.Categories;
namespace ProductManagement.Products
{
    public class Product : FullAuditedAggregateRoot<Guid>
    {
        public Category Category { get; set; }
        public Guid CategoryId { 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; }
    }
}

這一次,我繼承自FullAuditedAggregateRoot,相比Categoryd的AuditedAggregateRoot類,它還增加了IsDeletedDeletionTimeDeleterId屬性。

FullAuditedAggregateRoot實現了ISoftDelete介面,用於實體的軟刪除。即它永遠不會從資料庫中做物理刪除,而只是標記為已刪除。ABP 會自動處理所有的軟刪除邏輯。包括下次查詢時,已刪除的實體會被自動過濾,除非您有意請求它們,否則它不會在查詢結果中顯示。

導航屬性

在這個例子中,Product.Category是一個導航屬性為Category的實體。如果您使用 MongoDB 或想要真正實現 DDD,則不應將導航屬性新增到其他聚合中。但是,對於關聯式資料庫,它可以完美執行併為我們的程式碼提供靈活性。
解決方案中的新檔案如圖所示:

我們已經建立了領域物件。接下來是常量值。

常量值

這些常量將在輸入驗證和資料庫對映階段進行使用。
首先,在.Domain.Shared專案中建立一個 Categories 資料夾並在裡面新增一個類CategoryConsts
namespace ProductManagement.Categories
{
    public static class CategoryConsts
    {
        public const int MaxNameLength = 128;
    }
}
在這裡,MaxNameLength值將用於CategoryName屬性的約束。
然後,在.Domain.Shard的 Products 資料夾中建立一個ProductConsts類:
namespace ProductManagement.Products
{
    public static class ProductConsts
    {
        public const int MaxNameLength = 128;
    }
}
MaxNameLength值將用於約束ProductName屬性。

現在,領域層已經完成定義,接下來將為 EF Core 配置資料庫對映。

EF  Core和資料庫對映

我們在該應用中使用EF Core。EF Core 是一個由微軟提供的物件關係對映(ORM) 提供程式。ORM 提供了抽象,讓您感覺像是在使用程式碼實體物件而不是資料庫表。我們將在後面的使用資料訪問基礎架構中介紹 ABP 的 EF Core 整合。現在,我們先了解如何使用它。
  1. 首先,我們將實體新增到DbContext類並定義實體和資料庫表之間的對映;
  2. 然後,我們將使用 EF Core 的Code First方法建立對應的資料庫表;
  3. 接下來,我們再看 ABP 的種子資料系統,並插入一些初始資料;
  4. 最後,我們會將資料庫表結構和種子資料遷移到資料庫中,以便為應用程式做好準備。
讓我們從定義DbSet實體的屬性開始。

將實體新增到 DbContext 類

EF的DbContext有兩個主要用途:
  1. 定義實體和資料庫表之間對映;
  2. 訪問資料庫和執行資料庫相關實體的操作。
在.EntityFrameworkCore專案中開啟ProductManagementDbContext該類,新增以下屬性:
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }

EF Core 可以使用基於屬性名稱和型別的約定進行大部分對映。如果要自定義預設的對映配置或額外的配置,有兩種方法:資料註釋(屬性)和Fluent API

在資料註釋方法中,我們向實體屬性新增特性,例如[Required][StringLength],非常方便,也很容易理解。
與Fluent API相比,資料註釋容易受限,比如,當你需要使用EF Core的自定義特性時,他會讓你的領域層依賴EF Core的NuGet包,比如[Index][Owned]
在本章中,我更傾向 Fluent API 方法,它使實體更乾淨,並將所有 ORM 邏輯放在基礎設施層中。

將實體對映到資料庫表

ProductManagementDbContext(在*.EntityFrameworkCore*專案中)包含一個OnModelCreating方法用來配置實體到資料庫表的對映。當你首先建立您的解決方案時,此方法看起來如下所示:
protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);
    builder.ConfigurePermissionManagement();
    builder.ConfigureSettingManagement();
    builder.ConfigureIdentity();
    ...configuration of the other modules
    /* Configure your own tables/entities here */
}

再新增CategoryProduct實體的配置和對映關係:

builder.Entity<Category>(b =>
{
      b.ToTable("Categories");
      b.Property(x => x.Name)
            .HasMaxLength(CategoryConsts.MaxNameLength)
            .IsRequired();
      b.HasIndex(x => x.Name);
});
builder.Entity<Product>(b =>
{
      b.ToTable("Products");
      b.Property(x => x.Name)
            .HasMaxLength(ProductConsts.MaxNameLength)
            .IsRequired();
      b.HasOne(x => x.Category)
           .WithMany()
           .HasForeignKey(x => x.CategoryId)
           .OnDelete(DeleteBehavior.Restrict)
           .IsRequired();
b.HasIndex(x => x.Name).IsUnique();
});

我們使用CategoryConsts.MaxNameLength設定表CategoryName欄位的最大長度。Name欄位也是必填屬性。最後,我們為屬性定義了一個唯一的資料庫索引,因為它有助於按Name欄位搜尋。

Product對映類似於Category。此外,它還定義了Category實體與Product實體之間的關係;一個Product實體屬於一個Category實體,而一個Category實體可以有多個Product實體。
您可以參考 EF Core 官方文件進一步瞭解 Fluent API 的所有詳細資訊和其他選項。
對映配置完成後,我們就可以建立資料庫遷移,把我們新加的實體轉換成資料庫結構。

新增遷移命令

當你建立一個新的實體或對現有實體進行更改,還應該同步到資料庫中。EF Core 的Code First就是用來同步資料庫和實體結構的強大工具。通常,我們需要先生成遷移指令碼,然後執行遷移命令。遷移會對資料庫的架構進行增量更改。有兩種方法可以生成新遷移:

1 使用 Visual Studio

如果你正在使用Visual Studio,請開啟檢視|包管理器控制檯選單:
選擇.EntityFrameworkCore專案作為預設專案,並右鍵設定.Web專案作為啟動專案
現在,您可以在 控制檯中鍵入以下命令:
Add-Migration "Added_Categories_And_Products"

此命令的輸出應類似於:


如果你得到一個諸如No DbContext was found in assembly... 之類的錯誤,請確保您已將*.EntityFrameworkCore*專案設定為預設專案。
如果一切順利,會在.EntityFrameworkCore專案的Migrations資料夾中新增一個新的遷移類。

2 在命令列中

如果您不使用Visual Studio,你可以使用 EF Core命令列工具。如果尚未安裝,請在命令列終端中執行以下命令:
dotnet tool install --global dotnet-ef
現在,在.EntityFrameworkCore專案的根目錄中開啟一個命令列終端,然後輸入以下命令:
dotnet ef migrations add "Added_Categories_And_Products"

一個新的遷移類會新增到.EntityFrameworkCore專案的Migrations資料夾中。

種子資料

種子資料系統用於遷移資料庫時新增一些初始資料。例如,身份模組在資料庫中建立一個管理員使用者,該使用者具有登入應用程式的所有許可權。
雖然種子資料在我們的場景中不是必需的,這裡我想將一些產品類別和產品的初始化資料新增到資料庫中,以便更輕鬆地開發和測試應用程式。
關於 EF Core 種子資料
本節使用 ABP 的種子資料系統,而 EF Core 有自己的種子資料功能。ABP 種子資料系統允許您在程式碼中注入執行時服務並實現高階邏輯,適用於開發、測試和生產環境。但是,對於簡單的開發和測試,使用 EF Core 的種子資料基本夠用。請檢視官方文件
在ProductManagement.Domain專案的Data資料夾中建立一個ProductManagementDataSeedContributor類:
using ProductManagement.Categories;
using ProductManagement.Products;
using System;
using System.Threading.Tasks;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace ProductManagement.Data
{
    public class ProductManagementDataSeedContributor :
           IDataSeedContributor, ITransientDependency
    {
        private readonly IRepository<Category, Guid>_categoryRepository;
        private readonly IRepository<Product, Guid>_productRepository;
        public ProductManagementDataSeedContributor(
            IRepository<Category, Guid> categoryRepository,
            IRepository<Product, Guid> productRepository)
        {
            _categoryRepository = categoryRepository;
            _productRepository = productRepository;
        }
        public async Task SeedAsync(DataSeedContext                     context)
        {
            /***** TODO: Seed initial data here *****/
        }
    }
}

該類實現了IDataSeedContributor介面,ABP 會自動發現並呼叫其SeedAsync方法。您也可以實現建構函式注入並使用類中的任何服務(例如本示例中的儲存庫)。

然後,在SeedAsync方法內部編碼:
if (await _categoryRepository.CountAsync() > 0)
{
    return;
}
var monitors = new Category { Name = "Monitors" };
var printers = new Category { Name = "Printers" };
await _categoryRepository.InsertManyAsync(new[] { monitors, printers });
var monitor1 = new Product
{
    Category = monitors,
    Name = "XP VH240a 23.8-Inch Full HD 1080p IPS LED  Monitor",
    Price = 163,
    ReleaseDate = new DateTime(2019, 05, 24),
    StockState = ProductStockState.InStock
};
var monitor2 = new Product
{
    Category = monitors,
    Name = "Clips 328E1CA 32-Inch Curved Monitor, 4K UHD",
    Price = 349,
    IsFreeCargo = true,
    ReleaseDate = new DateTime(2022, 02, 01),
    StockState = ProductStockState.PreOrder
};
var printer1 = new Product
{
    Category = monitors,
    Name = "Acme Monochrome Laser Printer, Compact All-In One",
    Price = 199,
    ReleaseDate = new DateTime(2020, 11, 16),
    StockState = ProductStockState.NotAvailable
};
await _productRepository.InsertManyAsync(new[] { monitor1, monitor2, printer1 });

我們建立了兩個類別和三種產品並將它們插入到資料庫中。每次您執行DbMigrator應用時都會執行此類。同時,我們檢查if (await _categoryRepository.CountAsync() > 0)以防止資料重複插入。

種子資料和資料庫表結構準備就緒, 下面進入正式遷移。

遷移資料庫

EF Core 和 ABP 的遷移有何區別?
ABP 啟動模板中包含一個在開發和生產環境中非常有用的DbMigrator控制檯專案。當您執行它時,所有待處理的遷移都將應用到資料庫中,並執行資料初始化。
支援多租戶/多資料庫的場景,這是使用Update-Database無法實現的。
為什麼要從主應用中分離出遷移專案?
在生產環境中部署和執行時,通常作為持續部署(CD) 管道的一個環節。從主應用中分離出遷移功能有個好處,主應用不需要更改資料庫的許可權。此外,如果不做分離可能會遇到資料庫遷移和執行的併發問題。
將.DbMigrator專案設定為啟動專案,然後按 Ctrl+F5 執行該專案,待應用程式退出後,您可以檢查CategoriesProducts表是否已插入資料庫中(如果您使用 Visual Studio,則可以使用SQL Server 物件資源管理器連線到LocalDB並瀏覽資料庫)。
資料庫已準備好了。接下來我們將在 UI 上顯示產品資料。

定義應用服務

思路

我更傾向逐個功能地推進應用開發。本文將說明如何在 UI 上顯示產品列表。
  1. 首先,我們會為Product實體定義一個ProductDto
  2. 然後,我們將建立一個向表示層返回產品列表的應用服務方法;
  3. 此外,我們將學習如何自動對映ProductProductDto
在建立 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>,它定義了IdCreationTimeCreatorIdLastModificationTimeLastModifierId屬性(我們不需要做刪除審計DeletionTime,因為刪除的實體不是從資料庫中讀取的)。
  • 我們沒有向實體Category新增導航屬性,而是使用了一個string型別的CategoryName的屬性,用以在 UI 上顯示。
我們將使用使用ProductDto類從IProductAppService介面返回產品列表。

產品應用服務

應用服務實現了應用的業務邏輯,UI 呼叫它們用於使用者互動。通常,應用服務方法返回一個 DTO。

1 應用服務與 API 控制器

ABP的應用服務和MVC 中的 API 控制器有何區別?
您可以將應用服務與 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 類,它定義了MaxResultCountSkipCountSorting屬性。
  • 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) 擴充套件方法,比如SkipTakeOrderBy等。
AsyncExecuter服務(基類中預先注入)用於執行IQueryable物件,這使得可以使用非同步 LINQ 擴充套件方法執行資料庫查詢,而無需依賴應用程式層中的 EF Core 包。(我們將在[第 6 章 ] 中對AsyncExecuter進行更詳細的探討)
最後,我們使用ObjectMapper服務(在基類中預先注入)將Product集合對映到ProductDto集合。

物件對映

ObjectMapperIObjectMapper)會自動使用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表示式。但是,ProductDtoCategoryName可以直接使用ProductDto.CategoryName表示式進行訪問。AutoMapper 會通過展平Category.Name來自動對映成CategoryName
應用層服務已經基本完成。在開始 UI 之前,我們會先介紹如何為應用層編寫自動化測試,敬請期待下文。
 
 
 
 

相關文章