ABP應用開發(Step by Step)-上篇
本文主要通過逐步構建一個CRUD示例程序來介紹 ABP 框架的基礎知識。它涉及到應用開發的多個方面。在本章結束時,您將了解ABP 框架的基本開發方式。建議入門人員學習,老手不要浪費您寶貴時間。
abp new ProductManagement -t app
定義領域對象

實體在解決方案的領域層中定義,它分為兩個項目:
-
.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
。CreationTime
會設置為當前時間,CreatorId
會自動設置為當前用戶的Id
屬性。在本章中,我們使用公共的 getter 和 setter 來保持實體的簡單性。如果您想創建更豐富的領域模型並應用 DDD 原則和其他最佳實踐,我們將在後面的文章中討論它們。
產品庫存狀態枚舉(ProductStockState)
ProductStockState
是一個簡單的枚舉,用來設置和跟蹤產品庫存。ProductStockState
:namespace ProductManagement.Products { public enum ProductStockState : byte { PreOrder, InStock, NotAvailable, Stopped } }
產品實體(Product)
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,則不應將導航屬性添加到其他聚合中。但是,對於關係數據庫,它可以完美運行並為我們的代碼提供靈活性。
我們已經創建了領域對象。接下來是常量值。
常量值
CategoryConsts
:namespace ProductManagement.Categories { public static class CategoryConsts { public const int MaxNameLength = 128; } }
MaxNameLength
值將用於Category
的Name
屬性的約束。ProductConsts
類:namespace ProductManagement.Products { public static class ProductConsts { public const int MaxNameLength = 128; } }
MaxNameLength
值將用於約束Product
的Name
屬性。
現在,領域層已經完成定義,接下來將為 EF Core 配置數據庫映射。
EF Core和數據庫映射
-
首先,我們將實體添加到
DbContext
類並定義實體和數據庫表之間的映射; -
然後,我們將使用 EF Core 的Code First方法創建對應的數據庫表;
-
接下來,我們再看 ABP 的種子數據系統,並插入一些初始數據;
-
最後,我們會將數據庫表結構和種子數據遷移到數據庫中,以便為應用程序做好準備。
DbSet
實體的屬性開始。將實體添加到 DbContext 類
DbContext
有兩個主要用途:-
定義實體和數據庫表之間映射;
-
訪問數據庫和執行數據庫相關實體的操作。
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]
將實體映射到數據庫表
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
實體。映射配置完成後,我們就可以創建數據庫遷移,把我們新加的實體轉換成數據庫結構。
添加遷移命令
1 使用 Visual Studio

選擇.EntityFrameworkCore項目作為默認項目,並右鍵設置.Web項目作為啟動項目
Add-Migration "Added_Categories_And_Products"
此命令的輸出應類似於:

如果你得到一個諸如No DbContext was found in assembly… 之類的錯誤,請確保您已將*.EntityFrameworkCore*項目設置為默認項目。
2 在命令行中
dotnet tool install --global dotnet-ef
dotnet ef migrations add "Added_Categories_And_Products"
一個新的遷移類會添加到.EntityFrameworkCore項目的Migrations文件夾中。
種子數據
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 的遷移有何區別?
它支持多租戶/多數據庫的場景,這是使用
Update-Database
無法實現的。為什麼要從主應用中分離出遷移項目?
定義應用服務
思路
-
首先,我們會為
Product
實體定義一個ProductDto
; -
然後,我們將創建一個向表示層返回產品列表的應用服務方法;
-
此外,我們將學習如何自動映射
Product
到ProductDto
ProductDto
類開始。ProductDto 類
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
接口返回產品列表。產品應用服務
1 應用服務與 API 控制器
ABP的應用服務和MVC 中的 API 控制器有何區別?
-
應用服務更適合 DDD ,它們不依賴於特定的 UI 技術。
-
此外,ABP 可以自動將您的應用服務公開為 HTTP API。
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); } }
-
IProductAppService
約定從IApplicationService
接口,這樣ABP 就可以識別應用服務。 -
GetListAsync
方法的入參PagedAndSortedResultRequestDto
是 ABP 框架的標準 DTO 類,它定義了MaxResultCount
、SkipCount
和Sorting
屬性。 -
GetListAsync
方法返回PagedResultDto<ProductDto>
,其中包含一個TotalCount
屬性和一個ProductDto
對象集合,這是使用 ABP 框架返回分頁結果的便捷方式。
2 異步方法
IProductAppService
接口來執行用例。3 產品應用服務
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庫進行類型轉換。它要求我們在使用之前預先定義映射關係。啟動模板包含一個配置文件類,您可以在其中創建映射。ProductManagementApplicationAutoMapperProfile
類,並將其更改為以下內容:using AutoMapper; using ProductManagement.Products; namespace ProductManagement { public class ProductManagementApplicationAutoMapperProfile : Profile { public ProductManagementApplicationAutoMapperProfile() { CreateMap<Product, ProductDto>(); } } }
CreateMap
所定義的映射。它可以自動將Product
轉換為ProductDto
對象。Product
類有一個Category
屬性,而Category
類也有一個Name
屬性。因此,如果要訪問產品的類別名稱,則應使用Product.Category.Name
表達式。但是,ProductDto
的CategoryName
可以直接使用ProductDto.CategoryName
表達式進行訪問。AutoMapper 會通過展平Category.Name
來自動映射成CategoryName
。