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 框架。

參考#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。