myesn

myEsn2E9

hi
github

ABP 更详细的快速入门 - 使用分层架构(Layered Architecture)创建解决方案

简介#

本文旨在使用 ABP 框架提供对单个实体 (Entity) 的 CRUD RESTful API,也就是一个没有 UI 层的解决方案。

如果需要 Web 应用开发,请参阅这:https://docs.abp.io/en/abp/latest/Tutorials/Part-1

先决条件(Pre-Requirements)#

  • .NET 7+
  • Node.js v16+ (因为使用了 Angular 提供了默认的一些页面实现,比如登录 / 注册,租户和用户管理)

Install ABP CLI Tool#

因为我们将使用 ABP CLI 创建新的 ABP 解决方案,在此之前,需要全局安装以下工具:

dotnet tool install -g Volo.Abp.Cli

Create Your ABP Solution#

在一个空目录中打开终端执行以下命令,即创建一个名为 TodoApp 的分层解决方案:

以下命令中指定使用 app 模板,并不使用任何默认的前端 UI,ORM 采用 EF Core (默认),数据库使用 MySQL (默认为 MS SQL),并指定了数据库的连接字符串。

abp new TodoApp --template app --ui none --database-provider ef --database-management-system MySQL --connection-string "Server=localhost;Database=TodoApp;Uid=root;Pwd=123456;"

app 模板的分层架构和依赖关系如下,它采用了 Domain Driven Design 的设计模式,更多关于项目结构和职责的说明,请参阅
image

new 指令默认使用 --template app --ui mvc,也就是使用 Application Startup Template,并选择 ASP.NET Core MVC 框架提供默认的 UI 实现。ABP 一共提供了 6 种 Startup Template,分别是:app、app-nolayers、module、console、WPF、MAUI。

指定 --ui none 后有一个默认行为,就是在当前目录下创建一个 aspnet-core 子目录,如果不喜欢可以自行处理。

可以在 new 指令后追加 --output-folder--create-solution-folder 参数来指定输出的目录或是否创建解决方案文件夹。

建议了解更多关于 ABP CLI 中 new 指令的参数说明,官方也提供了一些示例以供参考。

如果创建时遇到 Find the following template in your cache directory 提示,说明你本机无法通过网络获取最新的 ABP Startup Template 版本,然后 ABP CLI 就在你本机缓存中查找,缓存不管有一个还是多个,都需要使用 --version x.x.x 指定想用版本号,这块逻辑的源码参考,本机缓存的 Startup Template 磁盘目录参考,也就是 %USERPROFILE%\.abp\templates 目录下。

Create the Database#

TodoApp.DbMigrator 项目的目录中打开终端,执行 dotnet run 命令创建数据库:
image

如果在创建项目后想要更换数据库,参阅

Run the Application#

ABP 官方建议在开发前,先运行一次 TodoApp.Web 项目,确保项目可以正常编译和运行。不过先前使用 new 创建解决方案时使用了 --ui none 参数,导致并不会有 .Web 的项目,所以这里我在 TodoApp.HttpApi.Host 目录中打开终端,然后执行 dotnet run 命令。

最后通过浏览器访问 https://localhost:44398 验证是否能够正常运行。ABP 会在创建数据库时,生成一个默认的用户到,用户名 admin,密码 1q2w3E*

关于 Swagger 集成,比如隐藏默认的 ABP 接口,或是为其启用 OAUTH 认证,参阅。如果隐藏了默认的 ABP 接口,会发现 Swagger 上的 Default Endpoints 确实没了,但下面的 Schemas 还在,这其实是预期效果,参阅

一切就绪,接下来可以开始编码了。

Domain Layer#

这是解决方案中的 **领域层(Domain Layer)**,它主要包含 entities, aggregate rootsdomain servicesvalue objectsrepository interfaces 和其他领域对象(domain objects)。

TodoApp.Domain 项目中创建一个 TodoItem class,它是一个实体(Entity),也就是映射(Mapping)到关系数据库表的对象:

using System;
using Volo.Abp.Domain.Entities;

namespace TodoApp
{
    public class TodoItem : BasicAggregateRoot<Guid>
    {
        public string Text { get; set; }
    }
}

[BasicAggregateRoot](https://docs.abp.io/en/abp/latest/Entities#basicaggregateroot-class) 是创建根实体(root entity)的最简单的基类(不需要处理并发冲突和额外的扩展属性时使用),其泛型参数中传入的 Guid 代表该实体的主键(Id)。

Database Integration#

接下来配置 Entity Framework Core

Mapping Configuration#

打开 TodoApp.EntityFrameworkCore/EntityFrameworkCore 目录中的 TodoAppDbContex class,并添加 DbSet 属性:

public DbSet<TodoItem> TodoItems { get; set; }

然后再到该 class 的 OnModelCreating 函数中添加 TodoItem 实体的映射(数据库表)代码:

using Humanizer;
...

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<TodoItem>(builder =>
    {
        builder.ToTable($"{TodoAppConsts.DbTablePrefix}TodoItem".Pluralize());
    });
}

[Pluralize](https://github.com/Humanizr/Humanizer#pluralize) 函数来自于 Humanizer.Core.zh-CN NuGet 包,将它安装到 TodoApp.Domain 项目中比较合适,因为它是最底层的项目,其他项目都依赖于它,它的作用是 “在考虑不规则词和不可数词的情况下将所提供的输入进行复数化”,这样避免写错单词的复数形式,当然这是细枝末节。

Code First Migrations#

我们创建的这个解决方案使用 EF Core 的 Code First Migrations。由于我们上面更改了数据库映射配置,因此需要创建一个新的 migration 并将更改应用到数据库。

TodoApp.EntityFrameworkCore 项目的目录中打开终端,执行以下命令:

dotnet ef migrations add Added_TodoItem

这将会在项目中添加新的 migration class:
image

随后,在同一终端中,执行以下命令将更改应用到数据库:

dotnet ef database update

查看数据库,应该可以看到这个表已经被成功创建:
image

Application Layer#

一个 Application Service 用于执行一系列相关的用例(use cases),它们用于向表示层公开领域逻辑,并使用 DTO 作为入参和出参(可选),可以理解为三层架构中的 Service 层。我们需要执行以下用例:

  • 获取待办事项列表
  • 创建新的待办事项
  • 更新现有的待办事项
  • 删除现有的待办事项

Application Service Interface#

先为 application service 定义一个 interface,在 TodoApp.Application.Contracts 项目中创建一个 ITodoAppService interface:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;

namespace TodoApp
{
    public interface ITodoAppService : IApplicationService
    {
        Task<List<TodoItemDto>> GetListAsync();
        Task<TodoItemDto> CreateAsync(string text);
        Task<TodoItemDto> UpdateAsync(Guid id, string text);
        Task DeleteAsync(Guid id);
    }
}

这里为了简便,便没有将入参封装到 DTO 中。

Data Transfer Object#

GetListAsync、CreateAsync、UpdateAsync 函数返回 TodoItemDto 对象,ApplicationService 的入参和出参通常是 DTO 而不是 Entity,一般在 TodoApp.Application.Contracts 项目中定义 DTO class。创建上面用到的 TodoItemDto

using System;

namespace TodoApp
{
    public class TodoItemDto
    {
        public Guid Id { get; set; }
        public string Text { get; set; }
    }
}

Application Service Implementation#

TodoApp.Application 项目中创建一个 TodoAppService class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;

namespace TodoApp
{
    public class TodoAppService : TodoAppAppService, ITodoAppService
    {
        private readonly IRepository<TodoItem, Guid> _todoItemRepository;

        public TodoAppService(IRepository<TodoItem, Guid> todoItemRepository)
        {
            _todoItemRepository = todoItemRepository;
        }

        public async Task<List<TodoItemDto>> GetListAsync()
        {
            var items = await _todoItemRepository.GetListAsync();
            return items
                .Select(item => new TodoItemDto
                {
                    Id = item.Id,
                    Text = item.Text
                }).ToList();
        }

        public async Task<TodoItemDto> CreateAsync(string text)
        {
            var todoItem = await _todoItemRepository.InsertAsync(
                new TodoItem { Text = text }
            );

            return new TodoItemDto
            {
                Id = todoItem.Id,
                Text = todoItem.Text
            };
        }

        public async Task<TodoItemDto> UpdateAsync(Guid id, string text)
        {
            var item = await _todoItemRepository.SingleOrDefaultAsync(x => x.Id.Equals(id));
            if (item == null) throw new EntityNotFoundException();

            item.Text = text;
            var newItem = await _todoItemRepository.UpdateAsync(item);

            return new TodoItemDto
            {
                Id = id,
                Text = newItem.Text
            };
        }

        public async Task DeleteAsync(Guid id)
        {
            await _todoItemRepository.DeleteAsync(id);
        }
    }
}

ABP 为 Entity 提供了默认的泛型 repositories

自己定义的 Application Service 实现类应该继承 ABP 提供的 ApplicationService class,这样 ABP 就可以根据约定自动为 Application Service 生成 API Controller,为我们减少冗余代码(因为一般 Controller 就简单调用 Application Service 中公开的领域逻辑),关于自动生成 API Controller 参阅

如果仅对实体进行简单的 CURD 操作,可以参考 ABP 提供 CRUD Application Service 进一步减少冗余代码。

当然对于 Entity 和 DTO 之间的类型转换,可以参阅

最后,运行 TodoApp.HttpApi.Host 项目,就可以在 Swagger 页面中看到上面实现的 CRUD Application Service 已经自动生成了 API。
image

总结#

总的来说,ABP 确实很全面,类似于 Java 的 Spring Boot 和 Node.js 的 NestJS 框架,已经超越了框架本身,是更完备和成熟的解决方案,它的确提高了开发效率,统一了代码规范,减少了交接成本,不过我建议初学者多参阅 ASP.NET Core 文档,再来使用进阶版的 ABP 框架。

参考#

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.