本文主要通过逐步构建一个CRUD示例程序来介绍 ABP 框架的基础知识。它涉及到应用开发的多个方面。在本章结束时,您将了解ABP 框架的基本开发方式。建议入门人员学习,老手不要浪费您宝贵时间。
abp new ProductManagement -t app
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属性。ProductStockState是一个简单的枚举,用来设置和跟踪产品库存。ProductStockState:namespace ProductManagement.Products{ public enum ProductStockState : byte { PreOrder, InStock, NotAvailable, Stopped }}
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类,它还增加了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属性。
DbContext类并定义实体和数据库表之间的映射;DbSet实体的属性开始。DbContext有两个主要用途:ProductManagementDbContext该类,添加以下属性:public DbSet<Product> Products { get; set; }public DbSet<Category> Categories { get; set; }
EF Core 可以使用基于属性名称和类型的约定进行大部分映射。如果要自定义默认的映射配置或额外的配置,有两种方法:数据注释(属性)和Fluent API。
[Required]和[StringLength],非常方便,也很容易理解。[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实体。
选择.EntityFrameworkCore项目作为默认项目,并右键设置.Web项目作为启动项目
Add-Migration "Added_Categories_And_Products"
此命令的输出应类似于:

dotnet tool install --global dotnet-efdotnet 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到ProductDtoProductDto类开始。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接口返回产品列表。ABP的应用服务和MVC 中的 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 框架返回分页结果的便捷方式。IProductAppService接口来执行用例。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。