ABP.IO WEB應用程式框架 新手教學 No.02 開發教學 第 1 部分 創建服務端

  • 1281
  • 0
  • ABP
  • 2021-07-26

此開發教學相較上一篇 No.01 快速開始 比較複雜一些

第一次接觸還沒看過快速開始的建議先從上一篇先看

這篇理論上同樣著重在 .Net Core + EF Core 建立 API

前端框架 Angular 的實現不在本次重點會先快速帶過

[2021] ABP.IO WEB應用程式框架 新手教學 No.0 全篇索引

本篇會以官方文件 開發教學 為依據中文化並附圖加以說明的方式進行


Web應用程序開發教程 - 第一章:創建服務端

關於本教程

在本系列教程中,您將構建一個名稱Acme.BookStore的用於管理書籍及其作者列表的基於 ABP 的程序。是使用以下技術開發的:

  • Entity Framework Core 為ORM 提供程序。
  • MVC / Razor Pages做為 UI 框架。

本教程分為以下部分:

下載源碼

本教程根據你的UI數據庫首選項有多個版本,我們準備了一個模型下載的源碼組件:

創建解決方案

在開始開發之前,請按照入門教程創建命名Acme.BookStore的新解決方案。

// 這邊我從 https://abp.io/get-started 使用以下選項建立

// 這邊可以不要勾最下面的選項,這邊只是我想要分開,但分開真正要跑需要有 Redis,可以安裝 docker 後執行,docker pull redis & docker run --name some-redis -d redis -p 6379:6379

// 因為專案預設會啟用 Redis,如果沒有可以先關閉,在 appsettings.json 中的 Redis 裡面加上 "IsEnabled": "true", 請參考 Redis 快取設定 說明文件

創建圖書實體

啟動模板中的領域層分為兩個項目:

  • Acme.BookStore.Domain包括你的實體領域服務和其他核心對象(例如:倉儲介面)。
  • Acme.BookStore.Domain.Shared包括可與客戶端共享的所有對象,枚舉或其他域相關。

BookType 枚舉 (Enum)

下面的項目所產生的BookType枚舉,在Acme.BookStore.Domain.Shared創建BookType

// 如果有開發過 API 應該遇過給 client 的時候蠻常會用到實體的 enum ,因為 DDD 中領域不會開放給外部(Client),所以這類東西需要放在領域共用專案,方便到時候 DTO 可以直接使用。

namespace Acme.BookStore.Books
{
    public enum BookType
    {
        Undefined,
        Adventure,
        Biography,
        Dystopia,
        Fantastic,
        Horror,
        Science,
        ScienceFiction,
        Poetry
    }
}

Book 實體 (Entity)

在解決方案的領域層Acme.BookStore.Domain項目)中定義你的實體。

該應用程序的主要實體是Book。在Acme.BookStore.Domain項目中創建一個Books文件夾並在其中添加了一個名稱Book的類,如下所示:

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為實體提供了兩個基本的基類:AggregateRootEntityAggregate Root領域驅動設計概念一個。可以直接查詢和處理的根實體(請參閱實體文檔)。
    // 同聚合內非根的一般實體可以用 BookCover : Entity<Guid>,因為同聚合內應該只有一個根,DDD不熟暫時不想用也可以直接照你原本開發方式全部用一般實體基類Entity<T>
  • Book實體繼承了AuditedAggregateRootAuditedAggregateRoot類在AggregateRoot類的基礎上添加了一些審計屬性( CreationTime, CreatorId, LastModificationTime)。ABP框架自動為你管理這些屬性。
    // 不用聚合根也不想那麼多審計屬性,ABP 也提供其他基類,比如:CreationAuditedEntity<TKey> ,再少也可以只使用 ABP 提供的介面,例如:IHasCreationTime,優點是可以統一屬性名稱為 CreationTime
  • GuidBook實體的主鍵類型。
    // 主鍵類型也可以自己改,例如:BookPage Entity<long> ,只是 ABP 推薦使用 Guid 就是了

為了保持簡單,本教程將實體屬性保留為public get/set。如果您想了解 DDD 最佳實踐,請參閱實體文檔

// 這邊引用一下 ABP 審計基類 中關於實體中巡覽屬性的一段敘述給大家參考一下

所有這些基類也有... WithUser,像FullAuditedAggregateRootWithUser<TUser>FullAuditedAggregateRootWithUser<TKey, TUser>.

這樣就可以將導航屬性添加到你的用戶實體.但在聚合根之間添加導航屬性不是一個好做法,所以這種用法是不建議的

(除非你使用 EF Core之類的ORM可以很好地支持這種情況,並且你真的需要它.請記住這種方法不適用於NoSQL數據庫(如MongoDB),你必須真正實現聚合模式).

最終的文件夾/文件結構應該如下所示:

將Book實體添加到DbContext中

EF Core 需要你將實體和DbContext建立關聯。最簡單的做法是在Acme.BookStore.EntityFrameworkCore項目的BookStoreDbContext類中添加DbSet屬性。如下所示:

public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
{
    public DbSet<Book> Books { get; set; }
    //...
}

將書實體映射到數據庫表

Acme.BookStore.EntityFrameworkCore項目中打開BookStoreDbContextModelCreatingExtensions.cs文件,添加Book實體的映射代碼。最終類應為:

using Acme.BookStore.Books;
using Microsoft.EntityFrameworkCore;
using Volo.Abp;
using Volo.Abp.EntityFrameworkCore.Modeling;

namespace Acme.BookStore.EntityFrameworkCore
{
    public static class BookStoreDbContextModelCreatingExtensions
    {
        public static void ConfigureBookStore(this ModelBuilder builder)
        {
            Check.NotNull(builder, nameof(builder));

            /* Configure your own tables/entities inside here */

            builder.Entity<Book>(b =>
            {
                b.ToTable(BookStoreConsts.DbTablePrefix + "Books",
                          BookStoreConsts.DbSchema);
                b.ConfigureByConvention(); //auto configure for the base class props
                b.Property(x => x.Name).IsRequired().HasMaxLength(128);
            });
        }
    }
}
  • ConfigureByConvention() 方法優雅的配置/歸屬的屬性,應始終對你所有的屬性使用它。
  • BookStoreConsts包含用於表的架構和表前綴的常量值。你不一定需要使用它,但建議在單點控製表的前綴。
    // 這是定義資料表名稱前綴,用來跟 Abp 開頭的表來做區分,方便辨識哪些是框架用的資料表,哪些是我們自己應用程式用的資料表,或自己定義不同前綴來分類自己的表,
    // 這邊建議統一定義在領域層的一個統一地方,預設是 BookStoreConsts.cs ,有其他需要共用的常量 (Const) 也可以繼續統一加在這裡,方便使用與管理。

 

添加數據遷移

啟動模板使用EF Core Code First Migrations創建和維護數據庫架構。我們應該創建一個新的遷移並應用到數據庫。

Acme.BookStore.EntityFrameworkCore.DbMigrations目錄中打開命令行輸入以下命令:

dotnet ef migrations add Created_Book_Entity

它會添加新的遷移類到項目中:

書店-efcore-遷移

如果你使用Visual Studio 你可能想要在包授權管理(PMC)中Add-Migration Created_Book_Entity -c BookStoreMigrationsDbContext和使用Update-Database -c BookStoreMigrationsDbContext命令。確保Acme.BookStore.Web是啟動項目並且Acme.BookStore.EntityFrameworkCore.DbMigrations是 PMC 的默認項目

添加附加數據 (SeedData)

// 在之後的 整合測試 章節會用到這份資料來做測試,雖然不是必須的流程,但這邊建議可以試著做一遍。

在運行應用程序最好之前將詳細數據添加到數據庫中。本節介紹ABP框架的數據種子系統。如果你不想創建數據可以跳過本節,但是你自己會來學習這個建議的 ABP 框架功能。

*.Domain項目下創建派生IDataSeedContributor的類,並且拷貝以下代碼:

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
                );
            }
        }
    }
}
  • 如果中數據庫沒有當前圖書,則使用IRepository<Book, Guid>(默認為知識庫)將兩本書插入數據庫。

更新數據庫

運行Acme.BookStore.DbMigrator應用程序來更新數據庫:

bookstore-dbmigrator-on-solution

.DbMigrator 是一個開發使用程序,可以在開發生產環境遷移數據庫初始化數據

// 預設連線字串指定的 DB Server 是 LocalDb (裝 VS 預設會有的開發用 DB),沒有了話需要到 appsettings.json 改連線字串到自己的DB,

// 執行完以上可以用SSMS連到 (LocalDb)\MSSQLLocalDB 看看剛剛建立出來的資料庫與表,確認資料是否有如我們預期正確新建出來

創建應用程序

應用程序層由兩個單獨的項目組成:

  • Acme.BookStore.Application.Contracts包含你的DTO應用服務接口。
  • Acme.BookStore.Application 包含你的應用服務實現。

在本部分中,您將創建一個應用程序服務,使用 ABP 框架的CrudAppService基類來獲取、創建、更新和刪除書籍。

// 這邊教學是使用非常規(ABP內建CRUD)的應用服務基底類別,基本的可以參考上一篇 快速開始 的應用服務部分,

// 這邊主要是了解ABP如何簡化重複的CRUD程式碼,基本的可以不實作任何一行程式,只要定義DTO並繼承ABP的介面與基類就可以提供基本CRUD程作業

Book Dto

CrudAppService基類需要定義實體的基本DTO。在Acme.BookStore.Application.Contracts項目中創建一個名稱BookDto的 DTO 類:

using System;
using Volo.Abp.Application.Dtos;

namespace Acme.BookStore
{
    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; }
    }
}
  • DTO類被用來表示層應用層 傳遞數據。查看DTO 文檔查看更多信息。
  • 為了在頁面上展示書籍信息,BookDto被將書籍數據傳遞到顯示層。
  • BookDto繼承自AuditedEntityDto<Guid>。跟上面定義的Book實體一樣具有一些審計屬性。
    // 這邊為了用來做 CRUD,某些DTO可能需要繼承內鍵含有Id定義的基類,才能正常做Update與Delete,一般DTO則可以不用,可以參考 快速開始

在將書籍返回到表示層時,需要將Book實體轉換為BookDto對象。AutoMapper庫可以在定義正確的映射時自動執行此轉換。

啟動模板配置了AutoMapper,因此你很適合在Acme.BookStore.Application項目的BookStoreApplicationAutoMapperProfile類中定義映射:

using Acme.BookStore.Books;
using AutoMapper;

namespace Acme.BookStore
{
    public class BookStoreApplicationAutoMapperProfile : Profile
    {
        public BookStoreApplicationAutoMapperProfile()
        {
            CreateMap<Book, BookDto>();
        }
    }
}

參見對像對對象文檔了解詳情。

創建更新書Dto

Acme.BookStore.Application.Contracts項目中創建一個名稱CreateUpdateBookDto的 DTO 類:

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; }
    }
}
  • 這個DTO類被用於在創建或更新書籍的時候從用戶界面獲取圖書信息。
  • 它定義了數據註釋屬性(如[Required])來定義屬性的驗證。DTO由ABP框架自動驗證

就像上面BookDto一樣,創建一個從對像CreateUpdateBookDtoBook實體的映射,最後一個映射類如下:

using Acme.BookStore.Books;
using AutoMapper;

namespace Acme.BookStore
{
    public class BookStoreApplicationAutoMapperProfile : Profile
    {
        public BookStoreApplicationAutoMapperProfile()
        {
            CreateMap<Book, BookDto>();
            CreateMap<CreateUpdateBookDto, Book>();
        }
    }
}

圖書應用服務介面

下一步是應用程序定義接口,在Acme.BookStore.Application.Contracts項目中定義一個未知IBookAppService的接口:

using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;

namespace Acme.BookStore.Books
{
    public interface IBookAppService :
        ICrudAppService< //Defines CRUD methods
            BookDto, //Used to show books
            Guid, //Primary key of the book entity
            PagedAndSortedResultRequestDto, //Used for paging/sorting
            CreateUpdateBookDto> //Used to create/update a book
    {

    }
}
  • 框架定義應用程序服務的接口不是必需的。但是,它被建議為最佳實踐。
  • ICrudAppService定義了常見的CRUD方法:GetAsyncGetListAsyncCreateAsyncUpdateAsyncDeleteAsync
    你可以從空的IApplicationService接口繼承並手動定義自己的方法(將在下一個領域中完成)。
  • ICrudAppService有一些變體,你可以在每個方法中單獨使用 DTO,也可以分別單獨指定(例如使用不同的 DTO 進行創建和更新)。

圖書應用服務

Acme.BookStore.Application項目中命名BookAppServiceIBookAppService實現:

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, //The Book entity
            BookDto, //Used to show books
            Guid, //Primary key of the book entity
            PagedAndSortedResultRequestDto, //Used for paging/sorting
            CreateUpdateBookDto>, //Used to create/update a book
        IBookAppService //implement the IBookAppService
    {
        public BookAppService(IRepository<Book, Guid> repository)
            : base(repository)
        {

        }
    }
}
  • BookAppService繼承了CrudAppService<...>。它實現了ICrudAppService定義的 CRUD 方法。
  • BookAppService注入IRepository <Book,Guid>,這是Book實體的默認。ABP自動為每個化根(或實體)創建默認。請參閱文檔
  • BookAppService使用IObjectMapperBook對象轉換為BookDto對象, 將CreateUpdateBookDto對象轉換為Book對象。
    啟動模板使用AutoMapper庫作為對象映射提供程序。我們之前定義了映射,從而導致方向預期工作。

自動生成API控制器

通常你創建控制器以將應用程序服務公開為HTTP API。因此允許瀏覽器或客戶端通過 AJAX 調用他們。

ABP可以自動為你的應用程序服務配置MVC API控制器。

Swagger 的用戶界面

啟動模板配置為使用Swashbuckle.AspNetCore運行swagger UI。運行應用程序並在瀏覽器中輸入https://localhost:XXXX/swagger/(用你自己的端口替換XXXX)作為URL。

你會看到一些內置的接口和Book接口,它們都是REST風格的:

書店招搖

Swagger 有一個很好的 UI 來測試 API。

你可以嘗試執行[GET] /api/app/bookAPI來獲取書籍列表,服務端會返回以下JSON結果:

{
  "totalCount": 2,
  "items": [
    {
      "name": "The Hitchhiker's Guide to the Galaxy",
      "type": 7,
      "publishDate": "1995-09-27T00:00:00",
      "price": 42,
      "lastModificationTime": null,
      "lastModifierId": null,
      "creationTime": "2020-07-03T21:04:18.4607218",
      "creatorId": null,
      "id": "86100bb6-cbc1-25be-6643-39f62806969c"
    },
    {
      "name": "1984",
      "type": 3,
      "publishDate": "1949-06-08T00:00:00",
      "price": 19.84,
      "lastModificationTime": null,
      "lastModifierId": null,
      "creationTime": "2020-07-03T21:04:18.3174016",
      "creatorId": null,
      "id": "41055277-cce8-37d7-bb37-39f62806960b"
    }
  ]
}

這很酷,因為我們沒有寫任何代碼來創建 API 控制器,但是現在我們有了一個可以正常使用的 REST API!

下一章

請參閱教程的下一章


ABP.IO WEB應用程式框架 新手教學 No.02 開發教學 Part 2 圖書列表頁面

PS5