前言
由于工作原因,学习了 .NET 开发和 ABP 框架的相关知识并有相应的实践经验,故而打算将 ABP 框架相关内容总结归纳成一系列文章,帮助新接触该框架的开发者们快速上手应用。 通过阅读本篇文章,您可以学习到以下内容:
- 对 ABP 框架以及其设计理念有大致的认识。
- 如何创建一个基于 RESTFul 的 Web API 后端项目。
- 如何在 ABP 中配置并连接使用 MySQL 数据库。
- 如何快速的实现最基本的增删改查 API。
- 如何创建种子数据。
- 如何使用第三方工具快速生成代码。
注意:在阅读接下来的内容之前,我将假设您熟悉 C# 与 .NET 开发,并了解它们的基础知识。
简介
ABP 框架是一个开源的 Web 应用程序开发框架,它提供了一套完整的开发框架和一系列的最佳实践,涵盖了常见的应用程序开发需求,包括领域驱动设计(DDD)、多层架构、模块化设计、身份认证、权限管理、数据持久化、缓存、本地化、日志记录等。
ABP框架的设计理念是面向领域驱动设计,通过将应用程序划分为多个领域模型,将业务逻辑和数据访问逻辑分离,提供了一种可扩展和可维护的架构。框架提供了通用的基础设施和约定,使得开发者能够更快地搭建应用程序的基础架构,专注于业务逻辑的开发。
准备工作
在正式开始之前,确保您已经做好了以下准备:
- 支持 .NET 7.0+ 开发的环境以及 IDE(例如 Visual Studio)。
- 已安装 ABP CLI。可以使用
dotnet tool install -g Volo.Abp.Cli
命令安装,或按照官网指引进行安装。
创建解决方案
首先,在心仪的目录下新建一个项目文件夹,打开命令行终端,输入以下命令:
shell
abp new Acme.BookStore -u none -dbms MySQL -v 7.4.2
这个命令的意思是使用 ABP CLI 新建一个模板项目,参数解释如下:
Acme.BookStore
是解决方案名称,可以根据自己的需要自定义;-u none
指不创建前端 UI 项目;-dbms MySQL
指定使用 MySQL 数据库;-v 7.4.2
指定 ABP 框架版本为 7.4.2,不指定该参数则默认使用最新的 latest 版本(目前是8.0)。
更多指令可以参考官网 当命令执行成功后,在 IDE 中打开该项目,你会看到如下图所示的项目结构:
ABP 是根据DDD(domain-driven design 领域驱动设计)原则 及开发 和部署的实践来进行分层的,接下来简单介绍一下这些项目的以及其作用。
领域层
领域层分为两个项目:
Acme.BookStore.Domain
是领域层中必需的,它包含实体,值对象,领域服务,规约,仓储接口等。Acme.BookStore.Domain.Shared
是领域层中很薄的项目,它只包含领域层与其它层共享的数据类型的定义,例如枚举、常量等。
应用层
应用层也被分为了两个项目:
Acme.BookStore.Application.Contracts
包含接口 的定义及接口依赖的DTO,此项目可以被展现层或其它客户端应用程序引用。Acme.BookStore.Application
是应用层中必须 的,它实现了Acme.BookStore.Application.Contracts
项目中定义的接口。
展现层
展现层就是前端项目,一般是 Angular 或 Blazor 项目,由于我们本篇文章是创建一个纯后台 API 应用,所以没有该项目。
远程服务层
Acme.BookStore.HttpApi
包含了 HTTP API 的定义。它通常包含 MVC Controller 和 Model 。因此,你可以在此项目中提供 HTTP API。Acme.BookStore.HttpApi.Client
当 C# 客户端应用程序需要调用内部的 API 时,这个项目非常有用。客户端程序仅需引用此项目就可以通过依赖注入方式,远程调用应用服务。
在项目中的
test
文件夹下,有一个名为Acme.BookStore.HttpApi.Client.ConsoleTestApp
的控制台程序。它演示了如何使用Acme.BookStore.HttpApi.Client
项目来远程调用应用程序公开的API。
基础设施层
Acme.BookStore.EntityFrameworkCore
是必需的,因为需要集成 EF Core 。应用程序的数据库上下文DbContext
,数据库对象映射,仓储接口的实现,以及其它与_EF Core_相关的内容都位于此项目中。Acme.BookStore.EntityFrameworkCore.DbMigrations
是管理 Code First 方式的数据库迁移记录的特殊项目。此项目定义了一个独立的DbContext
来追踪迁移记录。只有当添加一个新的数据库迁移记录或添加一个新的应用模块时,才会使用此项目,否则其它情况无需修改此项目内容。
其它项目
Acme.BookStore.DbMigrator
是一个简单的控制台程序,用来执行数据库迁移,包括初始化 数据库及创建种子数据。这是一个非常实用的应用程序,可以在开发环境或生产环境中使用它。Acme.BookStore.HttpApi.Host
是启动项目,相当于入口,将其它项目关联起来。
简单理解项目结构后,我们就可以进行下一步了,注意,如果你和我一样是使用 7.4.2 版本的 ABP,在可能会报如下图所示的一个错误:
解决这个报错只需要将 Volo.Abp.AspNetCore.MultiTenancy
这个包升级到 7.4.5 即可。
配置并运行项目
在初次启动项目之前我们还需要先配置好数据库连接,打开 Acme.BookStore.HttpApi.Host
项目下的 appsettings.json
文件,修改 JSON 配置对象中的 ConnectionStrings
为自己本地的数据库连接地址:
JSON
{
//...
"ConnectionStrings": {
"Default": "Server=localhost;Port=3306;Database=BookStore;Uid=root;Pwd=myPassword;"
},
//...
}
然后我们需要将此修改同步到 Acme.BookStore.EntityFrameworkCore.DbMigrations
项目下的appsettings.json
文件,两个项目的连接字符串需要一致才能进行初次迁移。
全部修改完成后将 Acme.BookStore.DbMigrator
设为启动项目并运行,看到弹出的命令行终端出显示如下图所示的成功字样说明完成了初次的数据库迁移。
我们将命令行窗口关闭后可以打开 navicat 或其他数据库连接工具查看,会发现多了很多张表,这些表都是 ABP 框架自带的,配合一些基础的功能,例如身份验证、权限、后台任务等。一般情况下我们不需要去修改这些表。
接下来我们将 Acme.BookStore.HttpApi.Host
设为启动项目并点击运行。等待一段时间后浏览器会自动打开一个 Swagger 页面,里面含有一些 ABP 框架内置的 API,如下图所示:
如果你也成功打开了此页面,那么恭喜你成功的创建了一个最基本的 ABP 项目并成功的运行起来了!
如果你的项目在运行后有报错提示,那么可以根据报错信息去搜索引擎尝试寻找解决方案,或者重新跟着我的步骤再来一遍。
创建实体
首先我们要创建一个 Book
实体,在 Acme.BookStore.Domain
项目中新建一个名为 Books
的文件夹,然后在该文件夹中创建一个 Book.cs
类文件,定义以下代码:
csharp
using System;
using Volo.Abp.Domain.Entities.Auditing;
namespace Acme.BookStore.Books;
public class Book : AuditedAggregateRoot<Guid>
{
public string Name { get; set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
}
根据 ABP 框架的分层策略,我们的实体都定义在 Domain
项目中,ABP 提供了两种用来定义实体的基类: AggregateRoot
和 Entity
,我们使用的 AggregateRoot(聚合根)
是一个 DDD 概念,由于该概念并不是本文的重点,所以不会过多解释,感兴趣的朋友可以自行搜索。
- ABP 在
AggregateRoot
中内置了一些审计属性,例如CreationTime
,CreatorId
,LastModificationTime
等属性。 Guid
则表示Book
实体的主键数据类型。BookType Enum
我们在Book
实体中将Type
定义为BookType
类型,一般来说这是个枚举类,我们接下来会创建这个类。
由于我们在应用层也会用到这个枚举类,所以将它定义在 Acme.BookStore.Domain.Shared
项目中,我们在该项目下同样的新建一个 Books
文件夹,在其中创建 BookType.cs
枚举类文件,定义以下代码:
csharp
namespace Acme.BookStore.Books;
public enum BookType
{
Undefined,
Adventure,
Biography,
Dystopia,
Fantastic,
Horror,
Science,
ScienceFiction,
Poetry
}
将实体关联到数据库
光有实体还不行,我们需要将它关联至数据库具体的表当中,首先我们将 Book
实体添加到 DbContext
中,打开 Acme.BookStore.EntityFrameworkCore
项目中 EntityFrameworkCore
文件夹下的 BookStoreDbContext
类文件,添加以下成员:
csharp
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
{
public DbSet<Book> Books { get; set; }
//...
}
然后向下找到重写的 OnModelCreating
方法,添加以下内容:
csharp
using Acme.BookStore.Books;
...
namespace Acme.BookStore.EntityFrameworkCore;
public class BookStoreDbContext :
AbpDbContext<BookStoreDbContext>,
IIdentityDbContext,
ITenantManagementDbContext
{
...
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
/* Include modules to your migration db context */
builder.ConfigurePermissionManagement();
...
/* Configure your own tables/entities inside here */
builder.Entity<Book>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Books",
BookStoreConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
});
}
}
ABP 默认使用 EFCore(Entity Framework Core)
作为 ORM,我们现在已经将实体映射成为数据库表,接下来我们需要生成数据库迁移记录,ABP 提供了两种方式生成数据库迁移记录。
第一种是在 Acme.BookStore.EntityFrameworkCore
项目的目录下打开一个命令行终端并执行以下命令:
bash
dotnet ef migrations add Created_Book_Entity
如果你使用 Visual Studio,也可以打开程序包管理器控制台,并且将 Acme.BookStore.EntityFrameworkCore
设为启动项目和控制台上的默认项目,执行以下命令:
bash
Add-Migration Created_Book_Entity
生成迁移记录命令中的 Created_Book_Entity
这部分是迁移记录的名称,可以自己自定义名称。执行后看到如下图所示的成功字样说明就执行成功了:
生成的迁移记录可以在 Acme.BookStore.EntityFrameworkCore
项目的 Migrations
文件夹中看到对应的文件,我们也可以生成一个空的迁移记录文件,自己来写对应的数据库变更语句。
有时我们在运行应用程序之前,需要在数据库中有一些初始数据。ABP 框架提供了这样的功能,在 Acme.BookStore.Domain
项目的 Books
文件夹中再新建一个 BookStoreDataSeederContributor
类文件,编写以下代码:
csharp
using System;
using System.Threading.Tasks;
using Acme.BookStore.Books;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore;
public class BookStoreDataSeederContributor
: IDataSeedContributor, ITransientDependency
{
private readonly IRepository<Book, Guid> _bookRepository;
public BookStoreDataSeederContributor(IRepository<Book, Guid> bookRepository)
{
_bookRepository = bookRepository;
}
public async Task SeedAsync(DataSeedContext context)
{
if (await _bookRepository.GetCountAsync() <= 0)
{
await _bookRepository.InsertAsync(
new Book
{
Name = "1984",
Type = BookType.Dystopia,
PublishDate = new DateTime(1949, 6, 8),
Price = 19.84f
},
autoSave: true
);
await _bookRepository.InsertAsync(
new Book
{
Name = "The Hitchhiker's Guide to the Galaxy",
Type = BookType.ScienceFiction,
PublishDate = new DateTime(1995, 9, 27),
Price = 42.0f
},
autoSave: true
);
}
}
}
ABP 框架提供了 IDataSeedContributor
接口,我们在执行迁移时会自动执行实现该接口的类。上述代码中,在 Book
表没有数据的时候,自动插入两条图书数据。
现在我们可以执行数据库迁移了,还记得我们执行初始化迁移的步骤吗?非常简单,只要将 Acme.BookStore.DbMigrator
设为启动项目并运行就可以了。
运行成功后,我们可以查看数据库,现在应该多了一个 appbooks
表,并且表中还有两条初始数据,如下图所示:
实现应用层 API 服务
现在我们来实现应用层的 API 服务,首先我们来定义 DTO (Data Transfer Objects) 类,DTO 类用于在表示层和应用程序层之间传输数据。
在 Acme.BookStore.Application.Contracts
项目中新建一个 Books
文件夹,在该文件夹下新建 BookDto
类,并编写以下代码:
csharp
using System;
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Books;
public class BookDto : AuditedEntityDto<Guid>
{
public string Name { get; set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
}
接下来我们需要将 Book
实体映射到 BookDto
对象,ABP 框架提供了自动映射功能,只需要在 Acme.BookStore.Application
项目中的 BookStoreApplicationAutoMapperProfile
类文件中添加如下代码即可:
csharp
using Acme.BookStore.Books;
using AutoMapper;
namespace Acme.BookStore;
public class BookStoreApplicationAutoMapperProfile : Profile
{
public BookStoreApplicationAutoMapperProfile()
{
CreateMap<Book, BookDto>(); // 创建映射关系
}
}
这样在获取 Book 列表时,会自动映射为 BookDto 对象返回给前端,现在我们还需要一个书籍在创建/更新时的 Dto 类,我们继续新建一个 CreateUpdateBookDto
类,编写以下代码:
csharp
using System;
using System.ComponentModel.DataAnnotations;
namespace Acme.BookStore.Books;
public class CreateUpdateBookDto
{
[Required]
[StringLength(128)]
public string Name { get; set; }
[Required]
public BookType Type { get; set; } = BookType.Undefined;
[Required]
[DataType(DataType.Date)]
public DateTime PublishDate { get; set; } = DateTime.Now;
[Required]
public float Price { get; set; }
}
同样的,我们也需要创建映射关系,更新 BookStoreApplicationAutoMapperProfile
中的代码如下所示:
csharp
using Acme.BookStore.Books;
using AutoMapper;
namespace Acme.BookStore;
public class BookStoreApplicationAutoMapperProfile : Profile
{
public BookStoreApplicationAutoMapperProfile()
{
CreateMap<Book, BookDto>();
CreateMap<CreateUpdateBookDto, Book>();
}
}
下一步我们定义该 API 服务的接口,在 Acme.BookStore.Application.Contracts
项目的 Books
文件夹中新建 IBookAppService
接口文件,编写以下代码:
csharp
using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Acme.BookStore.Books;
public interface IBookAppService :
ICrudAppService< // 定义 crud 方法
BookDto, // 返回给前端的 BookDto
Guid, // Book 实体的主键
PagedAndSortedResultRequestDto, // 分页和排序结果返回的 Dto
CreateUpdateBookDto> // 新增/更新 Book 的 Dto
{
}
ABP 框架提供了 ICrudAppService
接口,定义常见的 CRUD 方法:GetAsync
、GetListAsync
、CreateAsync
、UpdateAsync
和 DeleteAsync
,这些方法会通过 ABP 框架的自动端点映射为相应的 API。
下一步我们继续来实现这个接口,在 Acme.BookStore.Application
项目中新建 Books
文件夹,在文件夹中新建 BookAppService
类文件,编写以下代码:
csharp
using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Books;
public class BookAppService :
CrudAppService<
Book, // Book 实体
BookDto, // 返回给前端的 BookDto
Guid, // Book 实体的主键
PagedAndSortedResultRequestDto, // 分页和排序结果返回的 Dto
CreateUpdateBookDto>, // 新增/更新 Book 的 Dto
IBookAppService // 刚才创建的 IBookAppService 接口
{
public BookAppService(IRepository<Book, Guid> repository)
: base(repository)
{
}
}
BookAppService
派生自 CrudAppService<...>
,它实现了 ICrudAppService
定义的所有 CRUD(创建、读取、更新、删除)方法,我们现在将 Acme.BookStore.HttpApi.Host
设为启动项目并运行它,等待一段时间后会自动打开 Swagger 页面,你会发现如下图所示的 Book 相关的 API 已经创建好了:
至此,我们就实现了一个最基础的增删改查 API!你可以尝试在 Swagger 中调用它们,都是可以正常工作的,这就是 ABP 框架的强大之处,我们甚至没有写任何的处理逻辑,例如调用 Repository
修改数据库等等...
AbpHelper
在 ABP 框架中创建基础的 CRUD 服务很快,但是还有进步空间,我们还可以更快!这里就要介绍到一个开源的工具了,它就是 AbpHelper,这是一个基于 AbpHelper CLI 实现的可视化代码生成工具,可以帮助使用 ABP 框架的开发者快速生成代码。
我们只需要定义好实体类,即可一键生成相关的代码文件,可谓是开发者的好帮手,具体使用方法需要各位自行去探索,这里就不进行展开介绍了。总之合理的运用好这些开源工具可以使我们的开发效率大大提升!
总结
至此,我们已经基于 ABP 框架创建了一个纯后端 Web API 项目,项目结构基于 DDD 设计理念分层,我们在项目中实现了一个最基本的 CRUD 服务。
在实际开发场景中,ABP 内置的 CRUD 类大概率不能满足我们的需求,我们还需要进行方法的重写或基于 ApplicationService
类进行实现。我们的表关系也不都是单一独立的,需要建立关系表进行业务逻辑上的连接。我们将在此系列的下一篇文章中对这部分内容进行讲解。
尽管我在本文中尽可能地详细介绍了每一个步骤和细节,但是难免会存在一些错误和不足之处。如果您在使用本文中介绍的方法时发现了任何错误或者有更好的方法,非常欢迎您指正并提出建议,以便我能够不断改进和提升文章的质量。
我是荼锦,一个兴趣使然的开发者。非常感谢您阅读本文,希望本文对您有所帮助!