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
。ABP应用开发(Step by Step)-上篇的更多相关文章
- Step by Step: 基于MFC下的COM组件开发-Helloworld
http://blog.csdn.net/sybifei/article/details/45008745 [这篇文章有问题, 仅供参考] http://blog.csdn.net/define_us ...
- Step by step Dynamics CRM 2011升级到Dynamics CRM 2013
原创地址:http://www.cnblogs.com/jfzhu/p/4018153.html 转载请注明出处 (一)检查Customizations 从2011升级到2013有一些legacy f ...
- Step by step Install a Local Report Server and Remote Report Server Database
原创地址:http://www.cnblogs.com/jfzhu/p/4012097.html 转载请注明出处 前面的文章<Step by step SQL Server 2012的安装 &g ...
- WPF Step By Step 系列-Prism框架在项目中使用
WPF Step By Step 系列-Prism框架在项目中使用 回顾 上一篇,我们介绍了关于控件模板的用法,本节我们将继续说明WPF更加实用的内容,在大型的项目中如何使用Prism框架,并给予Pr ...
- WPF Step By Step 完整布局介绍
WPF Step By Step 完整布局介绍 回顾 上一篇,我们介绍了基本控件及控件的重要属性和用法,我们本篇详细介绍WPF中的几种布局容器及每种布局容器的使用场景,当 然这些都是本人在实际项目中的 ...
- WPF Step By Step 控件介绍
WPF Step By Step 控件介绍 回顾 上一篇,我们主要讨论了WPF的几个重点的基本知识的介绍,本篇,我们将会简单的介绍几个基本控件的简单用法,本文会举几个项目中的具体的例子,结合这些 例子 ...
- WPF Step By Step 系列 - 开篇 ·
WPF Step By Step 系列 - 开篇 公司最近要去我去整理出一个完整的WPF培训的教程,我刚好将自己学习WPF的过程和经验总结整理成笔记的方式来讲述,这里就不按照书上面的东西来说了,书本上 ...
- WinForm RDLC SubReport Step by step
最近在做的一个PO管理系统,因为要用到订单打印,没有用水晶报表,直接使用VS2010的Reporting.参考了网上的一些文章,但因为找到的数据是用于WebForm的,适配到WinForm有点区别,竟 ...
- Struts2+Spring+Hibernate step by step 11 ssh拦截验证用户登录到集成
注意:该系列文章从教师王健写了一部分ssh集成开发指南 引言: 之前没有引入拦截器之前,我们使用Filter过滤器验证用户是否登录,在使用struts2之后,全然能够使用拦截器,验证用户是否已经登录, ...
- 持续交付工具ThoughtWorks Go部署step by step
持续交付工具ThoughtWorks Go部署step by step http://blogs.360.cn/360cloud/2014/05/13/%E6%8C%81%E7%BB%AD%E4%BA ...
随机推荐
- 【flareon6】 overlong-通过动调改内存修改程序
程序分析 无壳,32位程序 运行后结果 程序比较简单一共三个函数 根据题目和运行结果可以看出来是a3太小了,没法完全解密密钥 解决该问题可以通过写脚本或动调解决 方法一:动调改内存 定位到a3入栈的位 ...
- Java代码查错部分?
1. abstract class Name { private String name; public abstract boolean isStupidName(String name) {} } ...
- 什么是 Spring Profiles?
Spring Profiles 允许用户根据配置文件(dev,test,prod 等)来注册 bean.因此,当应用程序在开发中运行时,只有某些 bean 可以加载,而在 PRODUCTION中,某些 ...
- VMware ESXi安装NVIDIA GPU显卡硬件驱动和配置vGPU
一.驱动软件准备:从nvidia网站下载驱动,注意,和普通显卡下载驱动地址不同. 按照ESXi对应版本不同下载不同的安装包.安装包内含ESXi主机驱动和虚拟机驱动. GPU显卡和物理服务器兼容查询:( ...
- ros系统21讲—前六讲
课程介绍(第一讲) linux介绍安装(第二讲) linux的基础操作(第三讲) ROS中语言c++与python介绍(第四讲) 安装ROS系统(第五讲) 第一个: sudo sh -c echo d ...
- Pandas怎样新增数据列
Pandas怎样新增数据列? 在进行数据分析时,经常需要按照一定条件创建新的数据列,然后进行进一步分析. 直接赋值 df.apply方法 df.assign方法 按条件选择分组分别赋值 0.读取csv ...
- 使用Vue2+webpack+Es6快速开发一个移动端项目,封装属于自己的jsonpAPI和手势响应式组件
导语 最近看到不少使用vue制作的音乐播放器,挺好玩的,本来工作中也经常使用Vue,一起交流学习,好的话点个star哦 本项目特点如下 : 1. 原生js封装自己的跨域请求函数,支持promise调用 ...
- ES6-11学习笔记--const
新声明方式:const 1.不属于顶层对象 window 2.不允许重复声明 3.不存在变量提升 4.暂时性死区 5.块级作用域 以上特性跟let声明一样,特性可看 let 的学习笔记:链接跳转 ...
- springboot插件
目前spring官网(http://spring.io/tools/sts/all)上可下载的spring插件只有:springsource-tool-suite-3.8.4(sts-3.8.4).但 ...
- MySQL 中 SQL语句大全(详细)
sql语句总结 总结内容 1. 基本概念 2. SQL列的常用类型 3. DDL简单操作 3.1 数据库操作 3.2 表操作 4. DML操作 4.1 修改操作(UPDATE SET) 4.2 插入操 ...