项目

本文档有多个版本。请选择最适合您的选项。

UI
Database

Web 应用开发教程 - 第 8 部分:作者:应用层

简介

本部分将解释如何为之前创建的 Author 实体创建应用层。

IAuthorAppService

我们将首先创建 应用服务 接口及相关的 DTO。在 Acme.BookStore.Application.Contracts 项目的 Authors 命名空间(文件夹)中创建一个名为 IAuthorAppService 的新接口:

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 属性,用于返回分页结果。
  • 首选从 CreateAsync 方法返回一个 AuthorDto(针对新创建的作者),虽然此应用程序未使用它——只是为了展示不同的用法。

此接口使用了下面定义的 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>

GetAuthorListDto

using Volo.Abp.Application.Dtos;

namespace Acme.BookStore.Authors;

public class GetAuthorListDto : PagedAndSortedResultRequestDto
{
    public string? Filter { get; set; }
}
  • Filter 用于搜索作者。它可以为 null(或空字符串)以获取所有作者。
  • PagedAndSortedResultRequestDto 包含标准的分页和排序属性:int MaxResultCountint SkipCountstring Sorting

ABP 提供了此类基础 DTO 类来简化和标准化您的 DTO。有关所有详细信息,请参阅 DTO 文档

CreateAuthorDto

using System;
using System.ComponentModel.DataAnnotations;

namespace Acme.BookStore.Authors;

public class CreateAuthorDto
{
    [Required]
    [StringLength(AuthorConsts.MaxNameLength)]
    public string Name { get; set; }  = string.Empty;

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

可以使用数据注解属性来验证 DTO。详情请参阅 验证文档

UpdateAuthorDto

using System;
using System.ComponentModel.DataAnnotations;

namespace Acme.BookStore.Authors;

public class UpdateAuthorDto
{
    [Required]
    [StringLength(AuthorConsts.MaxNameLength)]
    public string Name { get; set; } = string.Empty;

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

我们本可以在创建和更新操作之间共享(复用)相同的 DTO。虽然您可以这样做,但我们更倾向于为这些操作创建不同的 DTO,因为我们看到它们随着时间的推移通常会有所不同。因此,与紧密耦合的设计相比,这里的代码重复是合理的。

AuthorAppService

现在是实现 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

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

此方法简单地通过其 Id 获取 Author 实体,并使用 对象到对象映射器 将其转换为 AuthorDto。这需要在 Mapperly 中进行配置,稍后将进行说明。

GetListAsync

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 权限(除了为 AuthorAppService 类声明的 BookStorePermissions.Authors.Default 权限之外)。
  • 使用 AuthorManager(领域服务)创建新作者。
  • 使用 IAuthorRepository.InsertAsync 将新作者插入数据库。
  • 使用 ObjectMapper 返回一个代表新创建作者的 AuthorDto

DDD 提示:一些开发者可能认为在 _authorManager.CreateAsync 内部插入新实体很有用。我们认为将其留给应用层是更好的设计,因为它更清楚何时将其插入数据库(也许在插入之前需要对实体进行额外的工作,如果我们在领域服务中执行插入,则需要额外的更新)。然而,这完全取决于您。

UpdateAsync

[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,这在 Web 应用程序中会导致 404 HTTP 状态码。在更新操作中始终加载实体是一种良好实践。
  • 使用 AuthorManager.ChangeNameAsync(领域服务方法)来更改作者姓名(如果客户端请求更改)。
  • 直接更新 BirthDateShortBio,因为更改这些属性没有任何业务规则,它们接受任何值。
  • 最后,调用 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 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": "作者管理",
"Permission:Authors.Create": "创建新作者",
"Permission:Authors.Edit": "编辑作者",
"Permission:Authors.Delete": "删除作者"

对象到对象映射

AuthorAppService 使用 ObjectMapperAuthor 对象转换为 AuthorDto 对象。因此,我们需要在 Mapperly 配置中定义此映射。

打开 Acme.BookStore.Application 项目中的 BookStoreApplicationMappers 类,并定义以下映射类:

[Mapper]
public partial class AuthorToAuthorDtoMapper : MapperBase<Author, AuthorDto>
{
    public override partial AuthorDto Map(Author source);
    
    public override partial void Map(Author source, AuthorDto destination);
}

数据种子

就像之前对书籍所做的那样,在数据库中有一些初始作者实体是很好的。这在首次运行应用程序时很有用,同时对于自动化测试也非常有用。

打开 Acme.BookStore.Domain 项目中的 BookStoreDataSeederContributor,并将文件内容更改为以下代码:

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

        // 为作者添加种子数据

        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 编写一些测试。在 Acme.BookStore.Application.Tests 项目的 Authors 命名空间(文件夹)中添加一个名为 AuthorAppService_Tests 的新类:

using System;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Modularity;
using Xunit;

namespace Acme.BookStore.Authors;

public abstract class AuthorAppService_Tests<TStartupModule> : BookStoreApplicationTestBase<TStartupModule>
    where TStartupModule : IAbpModule
{
    private readonly IAuthorAppService _authorAppService;

    protected 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: 测试其他方法...
}

Acme.BookStore.EntityFrameworkCore.Tests 项目的 EntityFrameworkCore\Applications\Authors 命名空间(文件夹)中,为 AuthorAppService_Tests 类添加一个新的实现类,命名为 EfCoreAuthorAppService_Tests

using Acme.BookStore.Authors;
using Xunit;

namespace Acme.BookStore.EntityFrameworkCore.Applications.Authors;

[Collection(BookStoreTestConsts.CollectionDefinitionName)]
public class EfCoreAuthorAppService_Tests : AuthorAppService_Tests<BookStoreEntityFrameworkCoreTestModule>
{

}

我们创建了一些针对应用服务方法的测试,这些测试应该易于理解。


在本文档中