本文主要通過逐步構建一個CRUD示例程式來介紹 ABP 框架的基礎知識。它涉及到應用開發的多個方面。在本章結束時,您將瞭解ABP 框架的基本開發方式。建議入門人員學習,老手不要浪費您寶貴時間。
建立解決方案
如果你得到一個諸如No DbContext was found in assembly... 之類的錯誤,請確保您已將*.EntityFrameworkCore*專案設定為預設專案。
第1步是為產品管理解決方案(如果您在前面已經建立過了ProductManagement解決方案,可以繼續使用它)。在這裡,我們執行以下ABP CLI 來進行建立:
abp new ProductManagement -t app
我們使用自己熟悉的 IDE 中開啟解決方案,建立資料庫,然後執行 Web 專案。如果您在執行解決方案時遇到問題,請參閱上一章,或者在知識星球裡留言。
現在我們有一個正在可執行的解決方案。下一步建立領域物件來正式啟動編碼。
定義領域物件
該應用的領域很簡單,有Product和Category兩個實體以及一個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
) 。您可以使用任何型別的主鍵(例如int
、long
或string
)。AggregateRoot
是一種特殊的實體,用於建立聚合的根實體。它是一個領域驅動設計(DDD) 概念,我們將在接下來的章節中更詳細地討論。相比
AggregateRoot
類,AuditedAggregateRoot
新增了更多屬性:CreationTime
、CreatorId
、LastModificationTime
和LastModifierId
。當您將實體插入資料庫時,ABP 會自動給這些屬性賦值,
CreationTime
會設定為當前時間,CreatorId
會自動設定為當前使用者的Id
屬性。關於充血領域模型
在本章中,我們使用公共的 getter 和 setter 來保持實體的簡單性。如果您想建立更豐富的領域模型並應用 DDD 原則和其他最佳實踐,我們將在後面的文章中討論它們。
在本章中,我們使用公共的 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
,相比Category
d的AuditedAggregateRoot
類,它還增加了IsDeleted
、DeletionTime
和DeleterId
屬性。
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
值將用於Category
的Name
屬性的約束。然後,在.Domain.Shard的 Products 資料夾中建立一個
ProductConsts
類:namespace ProductManagement.Products { public static class ProductConsts { public const int MaxNameLength = 128; } }
該
現在,領域層已經完成定義,接下來將為 EF Core 配置資料庫對映。
MaxNameLength
值將用於約束Product
的Name
屬性。現在,領域層已經完成定義,接下來將為 EF Core 配置資料庫對映。
EF Core和資料庫對映
我們在該應用中使用EF Core。EF Core 是一個由微軟提供的物件關係對映(ORM) 提供程式。ORM 提供了抽象,讓您感覺像是在使用程式碼實體物件而不是資料庫表。我們將在後面的使用資料訪問基礎架構中介紹 ABP 的 EF Core 整合。現在,我們先了解如何使用它。
-
首先,我們將實體新增到
DbContext
類並定義實體和資料庫表之間的對映; -
然後,我們將使用 EF Core 的Code First方法建立對應的資料庫表;
-
接下來,我們再看 ABP 的種子資料系統,並插入一些初始資料;
-
最後,我們會將資料庫表結構和種子資料遷移到資料庫中,以便為應用程式做好準備。
讓我們從定義
DbSet
實體的屬性開始。將實體新增到 DbContext 類
EF的
DbContext
有兩個主要用途:-
定義實體和資料庫表之間對映;
-
訪問資料庫和執行資料庫相關實體的操作。
在.EntityFrameworkCore專案中開啟
ProductManagementDbContext
該類,新增以下屬性:public DbSet<Product> Products { get; set; } public DbSet<Category> Categories { get; set; }
EF Core 可以使用基於屬性名稱和型別的約定進行大部分對映。如果要自定義預設的對映配置或額外的配置,有兩種方法:資料註釋(屬性)和Fluent API。
在資料註釋方法中,我們向實體屬性新增特性,例如
與Fluent API相比,資料註釋容易受限,比如,當你需要使用EF Core的自定義特性時,他會讓你的領域層依賴EF Core的NuGet包,比如
[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 */ }
再新增Category
和Product
實體的配置和對映關係:
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
設定表Category
的Name
欄位的最大長度。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 執行該專案,待應用程式退出後,您可以檢查Categories和Products表是否已插入資料庫中(如果您使用 Visual Studio,則可以使用SQL Server 物件資源管理器連線到LocalDB並瀏覽資料庫)。
資料庫已準備好了。接下來我們將在 UI 上顯示產品資料。
定義應用服務
思路
我更傾向逐個功能地推進應用開發。本文將說明如何在 UI 上顯示產品列表。
-
首先,我們會為
Product
實體定義一個ProductDto
; -
然後,我們將建立一個向表示層返回產品列表的應用服務方法;
-
此外,我們將學習如何自動對映
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 控制器
ABP的應用服務和MVC 中的 API 控制器有何區別?
您可以將應用服務與 ASP.NET Core MVC 中的 API 控制器進行比較。雖然它們有相似之處,但是:
-
應用服務更適合 DDD ,它們不依賴於特定的 UI 技術。
-
此外,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 之前,我們會先介紹如何為應用層編寫自動化測試,敬請期待下文。