ABP.IO WEB應用程式框架 新手教學 No.09 開發教學 第 8 部分:作者:應用服務層

主要說明一般常用的API該如何一步一步地完成

包含服務介面(這次使用一般應用介面而非CRUD專用介面)

還有介面實作、權限屬性、AutoMapper、種子資料、測試

Web 應用程序開發教程 - 第 8 部分:作者:應用程序層

關於本教程

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

  • Entity Framework Core作為 ORM 提供者。
  • Angular作為 UI 框架。

本教程分為以下幾個部分;

下載源代碼

本教程根據您的UI數據庫首選項有多個版本。我們準備了幾個要下載的源代碼組合:

介紹

這部分說明為之前創建的Author實體創建一個應用層。

IAuthorAppService

我們將首先創建應用服務接口和相關的DTO。在項目IAuthorAppServiceAuthors命名空間(文件夾)中創建一個名為 的新接口Acme.BookStore.Application.Contracts

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

namespace Acme.BookStore.Authors
{
    public interface IAuthorAppService : IApplicationService
    {
        Task<AuthorDto> GetAsync(Guid id);

        Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input);

        Task<AuthorDto> CreateAsync(CreateAuthorDto input);

        Task UpdateAsync(Guid id, UpdateAuthorDto input);

        Task DeleteAsync(Guid id);
    }
}
  • IApplicationService 是所有應用服務都繼承的約定俗成的接口,所以ABP框架可以識別服務。
  • 定義了對Author實體執行 CRUD 操作的標準方法。
  • PagedResultDto是 ABP 框架中預定義的 DTO 類。它有一個Items集合和一個TotalCount返回分頁結果的屬性。
  • 首選AuthorDtoCreateAsync方法返回(對於新創建的作者),而此應用程序不使用它 - 只是為了顯示不同的用法。

此接口使用下面定義的 DTO(為您的項目創建它們)。

AuthorDto

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

namespace Acme.BookStore.Authors
{
    public class AuthorDto : EntityDto<Guid>
    {
        public string Name { get; set; }

        public DateTime BirthDate { get; set; }

        public string ShortBio { get; set; }
    }
}
  • EntityDto<T>僅具有Id具有給定泛型參數的屬性。您可以Id自己創建一個屬性,而不是繼承EntityDto<T>.

獲取作者列表Dto

using Volo.Abp.Application.Dtos;

namespace Acme.BookStore.Authors
{
    public class GetAuthorListDto : PagedAndSortedResultRequestDto
    {
        public string Filter { get; set; }
    }
}
  • Filter用於搜索作者。可以null(或空字符串)獲取所有作者。
  • PagedAndSortedResultRequestDto具有標準的分頁和排序屬性:int MaxResultCount,int SkipCountstring Sorting

ABP 框架有這樣的基本 DTO 類來簡化和標準化你的 DTO。請參閱DTO 文檔了解所有信息。

創建作者Dto

using System;
using System.ComponentModel.DataAnnotations;

namespace Acme.BookStore.Authors
{
    public class CreateAuthorDto
    {
        [Required]
        [StringLength(AuthorConsts.MaxNameLength)]
        public string Name { get; set; }

        [Required]
        public DateTime BirthDate { get; set; }
        
        public string ShortBio { get; set; }
    }
}

數據註釋屬性可用於驗證 DTO。有關詳細信息,請參閱驗證文件

更新作者Dto

using System;
using System.ComponentModel.DataAnnotations;

namespace Acme.BookStore.Authors
{
    public class UpdateAuthorDto
    {
        [Required]
        [StringLength(AuthorConsts.MaxNameLength)]
        public string Name { get; set; }

        [Required]
        public DateTime BirthDate { get; set; }
        
        public string ShortBio { get; set; }
    }
}

我們可以在創建和更新操作之間共享(重用)相同的 DTO。雖然你可以做到,但我們更喜歡為這些操作創建不同的 DTO,因為我們看到它們通常隨著時間的推移而不同。因此,與緊耦合設計相比,這裡的代碼重複是合理的。

作者應用服務

是時候實現IAuthorAppService接口了。創建一個新的類別,在 Acme.BookStore.Application 項目的Authors命名空間(文件夾)中命名 AuthorAppService

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Acme.BookStore.Permissions;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Repositories;

namespace Acme.BookStore.Authors
{
    [Authorize(BookStorePermissions.Authors.Default)]
    public class AuthorAppService : BookStoreAppService, IAuthorAppService
    {
        private readonly IAuthorRepository _authorRepository;
        private readonly AuthorManager _authorManager;

        public AuthorAppService(
            IAuthorRepository authorRepository,
            AuthorManager authorManager)
        {
            _authorRepository = authorRepository;
            _authorManager = authorManager;
        }

        //...SERVICE METHODS WILL COME HERE...
    }
}
  • [Authorize(BookStorePermissions.Authors.Default)]是一種檢查權限(策略)以授權當前用戶的聲明性方式。詳見授權文件BookStorePermissions類將在下面更新,現在不要擔心編譯錯誤。
  • 派生自BookStoreAppService,它是啟動模板附帶的一個簡單基類。它派生自標準ApplicationService類。
  • 實現了IAuthorAppService上面定義的。
  • 注入IAuthorRepositoryAuthorManager以在服務方法中使用。

現在,我們將一一介紹服務方法。將解釋的方法複製到AuthorAppService類中。

GetAsync

// 這邊主要展示如何利用 ABP 進行 AutoMapper 的自動映射

public async Task<AuthorDto> GetAsync(Guid id)
{
    var author = await _authorRepository.GetAsync(id);
    return ObjectMapper.Map<Author, AuthorDto>(author);
}

這個方法簡單地通過Author實體獲取實體Id,轉換為AuthorDto使用對像到對象映射器。這需要配置AutoMapper,後面會解釋。

GetListAsync

// 這主要展示取得集合時的分頁,排序,篩選,可以如何利用 ABP 進行實作

public async Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input)
{
    if (input.Sorting.IsNullOrWhiteSpace())
    {
        input.Sorting = nameof(Author.Name);
    }

    var authors = await _authorRepository.GetListAsync(
        input.SkipCount,
        input.MaxResultCount,
        input.Sorting,
        input.Filter
    );

    var totalCount = input.Filter == null
        ? await _authorRepository.CountAsync()
        : await _authorRepository.CountAsync(
            author => author.Name.Contains(input.Filter));

    return new PagedResultDto<AuthorDto>(
        totalCount,
        ObjectMapper.Map<List<Author>, List<AuthorDto>>(authors)
    );
}
  • 默認排序是“按作者姓名”,它在方法的開頭完成,以防它不是由客戶端發送的。
  • 用於IAuthorRepository.GetListAsync從數據庫中獲取分頁、排序​​和過濾的作者列表。我們已經在本教程的前一部分中實現了它。同樣,實際上不需要創建這樣的方法,因為我們可以直接查詢存儲庫,但想演示如何創建自定義存儲庫方法。
  • 直接從AuthorRepository獲取作者計數的同時查詢。如果發送了過濾器,那麼我們將在獲取計數時使用它來過濾實體。
  • 最後,通過將Author列表映射到 AuthorDto 列表來返回分頁結果。

CreateAsync

// 這邊主要展示如何使用屬性的方式來設定權限

[Authorize(BookStorePermissions.Authors.Create)]
public async Task<AuthorDto> CreateAsync(CreateAuthorDto input)
{
    var author = await _authorManager.CreateAsync(
        input.Name,
        input.BirthDate,
        input.ShortBio
    );

    await _authorRepository.InsertAsync(author);

    return ObjectMapper.Map<Author, AuthorDto>(author);
}
  • CreateAsync需要BookStorePermissions.Authors.Create權限(除了BookStorePermissions.Authors.DefaultAuthorAppService類聲明的)。
  • 使用AuthorManager(域服務)創建新作者。
  • 用於IAuthorRepository.InsertAsync將新作者插入到數據庫中。
  • 使用ObjectMapper返回一個AuthorDto代表新創建的作者。

DDD 提示:一些開發人員可能會發現將新實體插入到_authorManager.CreateAsync. 我們認為將它留給應用層是一個更好的設計,因為它更好地知道什麼時候將它插入到數據庫中(也許它需要在插入之前對實體進行額外的工作,如果我們在域服務)。但是,這完全取決於您。

UpdateAsync

// 這邊主要展示如何在更新時透過領域服務來套用業務邏輯 (DDD)

[Authorize(BookStorePermissions.Authors.Edit)]
public async Task UpdateAsync(Guid id, UpdateAuthorDto input)
{
    var author = await _authorRepository.GetAsync(id);

    if (author.Name != input.Name)
    {
        await _authorManager.ChangeNameAsync(author, input.Name);
    }

    author.BirthDate = input.BirthDate;
    author.ShortBio = input.ShortBio;

    await _authorRepository.UpdateAsync(author);
}
  • UpdateAsync需要額外的BookStorePermissions.Authors.Edit權限。
  • 用於IAuthorRepository.GetAsync從數據庫中獲取作者實體。如果沒有給定 id 的作者,則GetAsync拋出異常EntityNotFoundException,這會導致404Web 應用程序中的HTTP 狀態代碼。始終使實體進行更新操作是一種很好的做法。
  • AuthorManager.ChangeNameAsync如果客戶端要求更改作者姓名,則使用(域服務方法)更改作者姓名。
  • 直接更新BirthDate並且ShortBio由於沒有任何業務規則來更改這些屬性,因此它們接受任何值。
  • 最後,調用IAuthorRepository.UpdateAsync方法更新數據庫上的實體。

EF Core 提示:Entity Framework Core 具有更改跟踪系統,並在工作單元結束時自動保存對實體的任何更改(您可以簡單地認為 ABP 框架會SaveChanges在方法結束時自動調用)。因此,即使您沒有_authorRepository.UpdateAsync(...)在方法的末尾調用 ,它也會按預期工作。如果您以後不考慮更改 EF Core,則可以刪除此行。

DeleteAsync

[Authorize(BookStorePermissions.Authors.Delete)]
public async Task DeleteAsync(Guid id)
{
    await _authorRepository.DeleteAsync(id);
}
  • DeleteAsync需要額外的BookStorePermissions.Authors.Delete權限。
  • 它只是使用DeleteAsync存儲庫的方法。

權限定義

您無法編譯代碼,因為它需要在BookStorePermissions類中聲明一些常量。

打開 Acme.BookStore.Application.Contracts 專案裡面的類 BookStorePermissions(在Permissions文件夾中),修改內容如下圖:

namespace Acme.BookStore.Permissions
{
    public static class BookStorePermissions
    {
        public const string GroupName = "BookStore";

        public static class Books
        {
            public const string Default = GroupName + ".Books";
            public const string Create = Default + ".Create";
            public const string Edit = Default + ".Edit";
            public const string Delete = Default + ".Delete";
        }
        
        // *** ADDED a NEW NESTED CLASS ***
        public static class Authors
        {
            public const string Default = GroupName + ".Authors";
            public const string Create = Default + ".Create";
            public const string Edit = Default + ".Edit";
            public const string Delete = Default + ".Delete";
        }
    }
}

然後BookStorePermissionDefinitionProvider在同一個項目中打開 並在Define方法的末尾添加以下幾行:

var authorsPermission = bookStoreGroup.AddPermission(
    BookStorePermissions.Authors.Default, L("Permission:Authors"));

authorsPermission.AddChild(
    BookStorePermissions.Authors.Create, L("Permission:Authors.Create"));

authorsPermission.AddChild(
    BookStorePermissions.Authors.Edit, L("Permission:Authors.Edit"));

authorsPermission.AddChild(
    BookStorePermissions.Authors.Delete, L("Permission:Authors.Delete"));

最後,將以下條目添加到 Acme.BookStore.Domain.Shared項目Localization/BookStore/en.json內部,以本地化權限名稱:

"Permission:Authors": "Author Management",
"Permission:Authors.Create": "Creating new authors",
"Permission:Authors.Edit": "Editing the authors",
"Permission:Authors.Delete": "Deleting the authors"

對像到對象映射

AuthorAppService正在使用ObjectMapperAuthor對象轉換為AuthorDto對象。所以,我們需要在 AutoMapper 配置中定義這個映射。

打開 Acme.BookStore.Application 項目中的BookStoreApplicationAutoMapperProfile類並將以下行添加到構造函數中:

CreateMap<Author, AuthorDto>();

數據播種機 (Seed Data Contributor)

正如之前對書籍所做的那樣,最好在數據庫中包含一些初始作者實體。這在第一次運行應用程序時會很好,但對於自動化測試也非常有用。

BookStoreDataSeederContributorAcme.BookStore.Domain項目中,打開並使用以下代碼更改文件內容:

using System;
using System.Threading.Tasks;
using Acme.BookStore.Authors;
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;
        private readonly IAuthorRepository _authorRepository;
        private readonly AuthorManager _authorManager;

        public BookStoreDataSeederContributor(
            IRepository<Book, Guid> bookRepository,
            IAuthorRepository authorRepository,
            AuthorManager authorManager)
        {
            _bookRepository = bookRepository;
            _authorRepository = authorRepository;
            _authorManager = authorManager;
        }

        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
                );
            }

            // ADDED SEED DATA FOR AUTHORS

            if (await _authorRepository.GetCountAsync() <= 0)
            {
                await _authorRepository.InsertAsync(
                    await _authorManager.CreateAsync(
                        "George Orwell",
                        new DateTime(1903, 06, 25),
                        "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)."
                    )
                );

                await _authorRepository.InsertAsync(
                    await _authorManager.CreateAsync(
                        "Douglas Adams",
                        new DateTime(1952, 03, 11),
                        "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'."
                    )
                );
            }
        }
    }
}

現在,您可以運行.DbMigrator控制台應用程序遷移數據庫架構種子的初始數據。

測試作者申請服務

最後,我們可以為IAuthorAppService. 添加一個新類,AuthorAppService_TestsAcme.BookStore.Application.Tests 項目的Authors命名空間(文件夾)中命名:

using System;
using System.Threading.Tasks;
using Shouldly;
using Xunit;

namespace Acme.BookStore.Authors
{ 
    public class AuthorAppService_Tests : BookStoreApplicationTestBase
    {
        private readonly IAuthorAppService _authorAppService;

        public AuthorAppService_Tests()
        {
            _authorAppService = GetRequiredService<IAuthorAppService>();
        }

        [Fact]
        public async Task Should_Get_All_Authors_Without_Any_Filter()
        {
            var result = await _authorAppService.GetListAsync(new GetAuthorListDto());

            result.TotalCount.ShouldBeGreaterThanOrEqualTo(2);
            result.Items.ShouldContain(author => author.Name == "George Orwell");
            result.Items.ShouldContain(author => author.Name == "Douglas Adams");
        }

        [Fact]
        public async Task Should_Get_Filtered_Authors()
        {
            var result = await _authorAppService.GetListAsync(
                new GetAuthorListDto {Filter = "George"});

            result.TotalCount.ShouldBeGreaterThanOrEqualTo(1);
            result.Items.ShouldContain(author => author.Name == "George Orwell");
            result.Items.ShouldNotContain(author => author.Name == "Douglas Adams");
        }

        [Fact]
        public async Task Should_Create_A_New_Author()
        {
            var authorDto = await _authorAppService.CreateAsync(
                new CreateAuthorDto
                {
                    Name = "Edward Bellamy",
                    BirthDate = new DateTime(1850, 05, 22),
                    ShortBio = "Edward Bellamy was an American author..."
                }
            );
            
            authorDto.Id.ShouldNotBe(Guid.Empty);
            authorDto.Name.ShouldBe("Edward Bellamy");
        }

        [Fact]
        public async Task Should_Not_Allow_To_Create_Duplicate_Author()
        {
            await Assert.ThrowsAsync<AuthorAlreadyExistsException>(async () =>
            {
                await _authorAppService.CreateAsync(
                    new CreateAuthorDto
                    {
                        Name = "Douglas Adams",
                        BirthDate = DateTime.Now,
                        ShortBio = "..."
                    }
                );
            });
        }

        //TODO: Test other methods...
    }
}

為應用服務方法創建了一些測試,應該很容易理解。

下一部分

請參閱本教程的下一部分

PS5