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

本文主要通過逐步構建一個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 之前,我們會先介紹如何爲應用層編寫自動化測試,敬請期待下文。
 
 
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章