Web应用程序开发教程 - 第八章: 作者: 应用服务层

关于本教程

在本系列教程中, 你将构建一个名为 Acme.BookStore 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:

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

本教程分为以下部分:

下载源码

本教程根据你的UI数据库偏好有多个版本,我们准备了几种可供下载的源码组合:

如果你在Windows中遇到 “文件名太长” or “解压错误”, 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 在Windows 10中启用长路径.

如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path git config --system core.longpaths true

简介

这章阐述如何为前一章介绍的 作者 实体创建应用服务层.

IAuthorAppService

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

  1. using System;
  2. using System.Threading.Tasks;
  3. using Volo.Abp.Application.Dtos;
  4. using Volo.Abp.Application.Services;
  5. namespace Acme.BookStore.Authors
  6. {
  7. public interface IAuthorAppService : IApplicationService
  8. {
  9. Task<AuthorDto> GetAsync(Guid id);
  10. Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input);
  11. Task<AuthorDto> CreateAsync(CreateAuthorDto input);
  12. Task UpdateAsync(Guid id, UpdateAuthorDto input);
  13. Task DeleteAsync(Guid id);
  14. }
  15. }
  • IApplicationService 是一个常规接口, 所有应用服务都继承自它, 所以 ABP 框架可以识别它们.
  • Author 实体中定义标准方法用于CRUD操作.
  • PagedResultDto 是一个ABP框架中预定义的 DTO 类. 它拥有一个 Items 集合 和一个 TotalCount 属性, 用于返回分页结果.
  • 优先从 CreateAsync 方法返回 AuthorDto (新创建的作者), 虽然在这个程序中没有这么做 - 这里只是展示一种不同用法.

这个类使用下面定义的DTOs (为你的项目创建它们).

AuthorDto

  1. using System;
  2. using Volo.Abp.Application.Dtos;
  3. namespace Acme.BookStore.Authors
  4. {
  5. public class AuthorDto : EntityDto<Guid>
  6. {
  7. public string Name { get; set; }
  8. public DateTime BirthDate { get; set; }
  9. public string ShortBio { get; set; }
  10. }
  11. }
  • EntityDto<T> 只有一个类型为指定泛型参数的 Id 属性. 你可以自己创建 Id 属性, 而不是继承自 EntityDto<T>.

GetAuthorListDto

  1. using Volo.Abp.Application.Dtos;
  2. namespace Acme.BookStore.Authors
  3. {
  4. public class GetAuthorListDto : PagedAndSortedResultRequestDto
  5. {
  6. public string Filter { get; set; }
  7. }
  8. }
  • Filter 用于搜索作者. 它可以是 null (或空字符串) 以获得所有用户.
  • PagedAndSortedResultRequestDto 具有标准分页和排序属性: int MaxResultCount, int SkipCountstring Sorting.

ABP 框架拥有这些基本的DTO类以简化并标准化你的DTOs. 参阅 DTO 文档 获得所有DTO类的详细信息.

CreateAuthorDto

  1. using System;
  2. using System.ComponentModel.DataAnnotations;
  3. namespace Acme.BookStore.Authors
  4. {
  5. public class CreateAuthorDto
  6. {
  7. [Required]
  8. [StringLength(AuthorConsts.MaxNameLength)]
  9. public string Name { get; set; }
  10. [Required]
  11. public DateTime BirthDate { get; set; }
  12. public string ShortBio { get; set; }
  13. }
  14. }

数据标记特性可以用来验证DTO. 参阅 验证文档 获得详细信息.

UpdateAuthorDto

  1. using System;
  2. using System.ComponentModel.DataAnnotations;
  3. namespace Acme.BookStore.Authors
  4. {
  5. public class UpdateAuthorDto
  6. {
  7. [Required]
  8. [StringLength(AuthorConsts.MaxNameLength)]
  9. public string Name { get; set; }
  10. [Required]
  11. public DateTime BirthDate { get; set; }
  12. public string ShortBio { get; set; }
  13. }
  14. }

我们可以在创建和更新操作间分享 (重用) 相同的DTO. 虽然可以这么做, 但我们推荐为这些操作创建不同的DTOs, 因为我们发现随着时间的推移, 它们通常会变得有差异. 所以, 与紧耦合相比, 代码重复也是合理的.

AuthorAppService

是时候实现 IAuthorAppService 接口了. 在 Acme.BookStore.Application 项目的 Authors 命名空间 (文件夹) 中创建一个新类 AuthorAppService :

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading.Tasks;
  5. using Acme.BookStore.Permissions;
  6. using Microsoft.AspNetCore.Authorization;
  7. using Volo.Abp.Application.Dtos;
  8. using Volo.Abp.Domain.Repositories;
  9. namespace Acme.BookStore.Authors
  10. {
  11. [Authorize(BookStorePermissions.Authors.Default)]
  12. public class AuthorAppService : BookStoreAppService, IAuthorAppService
  13. {
  14. private readonly IAuthorRepository _authorRepository;
  15. private readonly AuthorManager _authorManager;
  16. public AuthorAppService(
  17. IAuthorRepository authorRepository,
  18. AuthorManager authorManager)
  19. {
  20. _authorRepository = authorRepository;
  21. _authorManager = authorManager;
  22. }
  23. //...SERVICE METHODS WILL COME HERE...
  24. }
  25. }
  • [Authorize(BookStorePermissions.Authors.Default)] 是一个检查权限(策略)的声明式方法, 用来给当前用户授权. 参阅 授权文档 获得详细信息. BookStorePermissions 类在后文会被更新, 现在不需要担心编译错误.
  • BookStoreAppService 派生, 这个类是一个简单基类, 可以做为模板. 它继承自标准的 ApplicationService 类.
  • 实现上面定义的 IAuthorAppService .
  • 注入 IAuthorRepositoryAuthorManager 以使用服务方法.

现在, 我们逐个介绍服务方法. 复制这些方法到 AuthorAppService 类.

GetAsync

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

这个方法根据 Id 获得 Author 实体, 使用 对象到对象映射 转换为 AuthorDto. 这需要配置AutoMapper, 后面会介绍.

GetListAsync

  1. public async Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input)
  2. {
  3. if (input.Sorting.IsNullOrWhiteSpace())
  4. {
  5. input.Sorting = nameof(Author.Name);
  6. }
  7. var authors = await _authorRepository.GetListAsync(
  8. input.SkipCount,
  9. input.MaxResultCount,
  10. input.Sorting,
  11. input.Filter
  12. );
  13. var totalCount = input.Filter == null
  14. ? await _authorRepository.CountAsync()
  15. : await _authorRepository.CountAsync(
  16. author => author.Name.Contains(input.Filter));
  17. return new PagedResultDto<AuthorDto>(
  18. totalCount,
  19. ObjectMapper.Map<List<Author>, List<AuthorDto>>(authors)
  20. );
  21. }
  • 为处理客户端没有设置的情况, 在方法的开头设置默认排序是 “根据作者名”.
  • 使用 IAuthorRepository.GetListAsync 从数据库中获得分页的, 排序的和过滤的作者列表. 我们已经在教程的前一章中实现了它. 再一次强调, 实际上不需要创建这个方法, 因为我们可以从数据库中直接查询, 这里只是演示如何创建自定义repository方法.
  • 直接查询 AuthorRepository , 得到作者的数量. 如果客户端发送了过滤条件, 会得到过滤后的作者数量.
  • 最后, 通过映射 Author 列表到 AuthorDto 列表, 返回分页后的结果.

CreateAsync

  1. [Authorize(BookStorePermissions.Authors.Create)]
  2. public async Task<AuthorDto> CreateAsync(CreateAuthorDto input)
  3. {
  4. var author = await _authorManager.CreateAsync(
  5. input.Name,
  6. input.BirthDate,
  7. input.ShortBio
  8. );
  9. await _authorRepository.InsertAsync(author);
  10. return ObjectMapper.Map<Author, AuthorDto>(author);
  11. }
  • CreateAsync 需要 BookStorePermissions.Authors.Create 权限 (另外包括 AuthorAppService 类声明的 BookStorePermissions.Authors.Default 权限).
  • 使用 AuthorManager (领域服务) 创建新作者.
  • 使用 IAuthorRepository.InsertAsync 插入新作者到数据库.
  • 使用 ObjectMapper 返回 AuthorDto , 代表新创建的作者.

DDD提示: 一些开发者可能会发现可以在 _authorManager.CreateAsync 插入新实体. 我们认为把它留给应用层是更好的设计, 因为应用层更了解应该何时插入实体到数据库(在插入实体前可能需要额外的工作. 如果在领域层插入, 可能需要额外的更新操作). 但是, 你拥有最终的决定权.

UpdateAsync

  1. [Authorize(BookStorePermissions.Authors.Edit)]
  2. public async Task UpdateAsync(Guid id, UpdateAuthorDto input)
  3. {
  4. var author = await _authorRepository.GetAsync(id);
  5. if (author.Name != input.Name)
  6. {
  7. await _authorManager.ChangeNameAsync(author, input.Name);
  8. }
  9. author.BirthDate = input.BirthDate;
  10. author.ShortBio = input.ShortBio;
  11. await _authorRepository.UpdateAsync(author);
  12. }
  • UpdateAsync 需要额外的 BookStorePermissions.Authors.Edit 权限.
  • 使用 IAuthorRepository.GetAsync 从数据库中获得作者实体. 如果给定的id没有找到作者, GetAsync 抛出 EntityNotFoundException, 这在web应用程序中导致一个 404 HTTP 状态码. 在更新操作中先获取实体再更新它, 是一个好的实践.
  • 如果客户端请求, 使用 AuthorManager.ChangeNameAsync (领域服务方法) 修改作者姓名.
  • 因为没有任何业务逻辑, 直接更新 BirthDateShortBio, 它们可以接受任何值.
  • 最后, 调用 IAuthorRepository.UpdateAsync 更新实体到数据库.

EF Core 提示: Entity Framework Core 拥有 change tracking 系统并在unit of work 结束时 自动保存 任何修改到实体 (你可以简单地认为APB框架在方法结束时自动调用 SaveChanges). 所以, 即使你在方法结束时没有调用 _authorRepository.UpdateAsync(...) , 它依然可以工作. 如果你不考虑以后修改EF Core, 你可以移除这一行.

DeleteAsync

  1. [Authorize(BookStorePermissions.Authors.Delete)]
  2. public async Task DeleteAsync(Guid id)
  3. {
  4. await _authorRepository.DeleteAsync(id);
  5. }
  • DeleteAsync 需要额外的 BookStorePermissions.Authors.Delete 权限.
  • 直接使用repository的 DeleteAsync 方法.

权限定义

你还不能编译代码, 因为它需要 BookStorePermissions 类定义中一些常数.

打开 Acme.BookStore.Application.Contracts 项目中的 BookStorePermissions 类 (在 Permissions 文件夹中), 修改为如下代码:

  1. namespace Acme.BookStore.Permissions
  2. {
  3. public static class BookStorePermissions
  4. {
  5. public const string GroupName = "BookStore";
  6. public static class Books
  7. {
  8. public const string Default = GroupName + ".Books";
  9. public const string Create = Default + ".Create";
  10. public const string Edit = Default + ".Edit";
  11. public const string Delete = Default + ".Delete";
  12. }
  13. // *** ADDED a NEW NESTED CLASS ***
  14. public static class Authors
  15. {
  16. public const string Default = GroupName + ".Authors";
  17. public const string Create = Default + ".Create";
  18. public const string Edit = Default + ".Edit";
  19. public const string Delete = Default + ".Delete";
  20. }
  21. }
  22. }

然后打开同一项目中的 BookStorePermissionDefinitionProvider, 在 Define 方法的结尾加入以下行:

  1. var authorsPermission = bookStoreGroup.AddPermission(
  2. BookStorePermissions.Authors.Default, L("Permission:Authors"));
  3. authorsPermission.AddChild(
  4. BookStorePermissions.Authors.Create, L("Permission:Authors.Create"));
  5. authorsPermission.AddChild(
  6. BookStorePermissions.Authors.Edit, L("Permission:Authors.Edit"));
  7. authorsPermission.AddChild(
  8. BookStorePermissions.Authors.Delete, L("Permission:Authors.Delete"));

最后, 在 Acme.BookStore.Domain.Shared 项目中的 Localization/BookStore/en.json 加入以下项, 用以本地化权限名称:

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

简体中文翻译请打开zh-Hans.json文件 ,并将”Texts”对象中对应的值替换为中文.

对象到对象映射

AuthorAppService 使用 ObjectMapperAuthor 对象 转换为 AuthorDto 对象. 所以, 我们需要在 AutoMapper 配置中定义映射.

打开 Acme.BookStore.Application 项目中的 BookStoreApplicationAutoMapperProfile 类, 加入以下行到构造函数:

  1. CreateMap<Author, AuthorDto>();

数据种子

如同图书管理部分所做的, 在数据库中生成一些初始作者实体. 不仅当第一次运行应用程序时是有用的, 对自动化测试也是很有用的.

打开 Acme.BookStore.Domain 项目中的 BookStoreDataSeederContributor, 修改文件内容如下:

  1. using System;
  2. using System.Threading.Tasks;
  3. using Acme.BookStore.Authors;
  4. using Acme.BookStore.Books;
  5. using Volo.Abp.Data;
  6. using Volo.Abp.DependencyInjection;
  7. using Volo.Abp.Domain.Repositories;
  8. namespace Acme.BookStore
  9. {
  10. public class BookStoreDataSeederContributor
  11. : IDataSeedContributor, ITransientDependency
  12. {
  13. private readonly IRepository<Book, Guid> _bookRepository;
  14. private readonly IAuthorRepository _authorRepository;
  15. private readonly AuthorManager _authorManager;
  16. public BookStoreDataSeederContributor(
  17. IRepository<Book, Guid> bookRepository,
  18. IAuthorRepository authorRepository,
  19. AuthorManager authorManager)
  20. {
  21. _bookRepository = bookRepository;
  22. _authorRepository = authorRepository;
  23. _authorManager = authorManager;
  24. }
  25. public async Task SeedAsync(DataSeedContext context)
  26. {
  27. if (await _bookRepository.GetCountAsync() <= 0)
  28. {
  29. await _bookRepository.InsertAsync(
  30. new Book
  31. {
  32. Name = "1984",
  33. Type = BookType.Dystopia,
  34. PublishDate = new DateTime(1949, 6, 8),
  35. Price = 19.84f
  36. },
  37. autoSave: true
  38. );
  39. await _bookRepository.InsertAsync(
  40. new Book
  41. {
  42. Name = "The Hitchhiker's Guide to the Galaxy",
  43. Type = BookType.ScienceFiction,
  44. PublishDate = new DateTime(1995, 9, 27),
  45. Price = 42.0f
  46. },
  47. autoSave: true
  48. );
  49. }
  50. // ADDED SEED DATA FOR AUTHORS
  51. if (await _authorRepository.GetCountAsync() <= 0)
  52. {
  53. await _authorRepository.InsertAsync(
  54. await _authorManager.CreateAsync(
  55. "George Orwell",
  56. new DateTime(1903, 06, 25),
  57. "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)."
  58. )
  59. );
  60. await _authorRepository.InsertAsync(
  61. await _authorManager.CreateAsync(
  62. "Douglas Adams",
  63. new DateTime(1952, 03, 11),
  64. "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'."
  65. )
  66. );
  67. }
  68. }
  69. }
  70. }

你现在可以运行 .DbMigrator 控制台应用程序, 迁移 数据库 schema 并生成 种子 初始数据.

测试作者应用服务

最后, 你可以为 IAuthorAppService 写一些测试. 在 Acme.BookStore.Application.Tests 项目的 Authors 命名空间(文件夹)中加入一个名为 AuthorAppService_Tests 新类:

  1. using System;
  2. using System.Threading.Tasks;
  3. using Shouldly;
  4. using Xunit;
  5. namespace Acme.BookStore.Authors
  6. {
  7. public class AuthorAppService_Tests : BookStoreApplicationTestBase
  8. {
  9. private readonly IAuthorAppService _authorAppService;
  10. public AuthorAppService_Tests()
  11. {
  12. _authorAppService = GetRequiredService<IAuthorAppService>();
  13. }
  14. [Fact]
  15. public async Task Should_Get_All_Authors_Without_Any_Filter()
  16. {
  17. var result = await _authorAppService.GetListAsync(new GetAuthorListDto());
  18. result.TotalCount.ShouldBeGreaterThanOrEqualTo(2);
  19. result.Items.ShouldContain(author => author.Name == "George Orwell");
  20. result.Items.ShouldContain(author => author.Name == "Douglas Adams");
  21. }
  22. [Fact]
  23. public async Task Should_Get_Filtered_Authors()
  24. {
  25. var result = await _authorAppService.GetListAsync(
  26. new GetAuthorListDto {Filter = "George"});
  27. result.TotalCount.ShouldBeGreaterThanOrEqualTo(1);
  28. result.Items.ShouldContain(author => author.Name == "George Orwell");
  29. result.Items.ShouldNotContain(author => author.Name == "Douglas Adams");
  30. }
  31. [Fact]
  32. public async Task Should_Create_A_New_Author()
  33. {
  34. var authorDto = await _authorAppService.CreateAsync(
  35. new CreateAuthorDto
  36. {
  37. Name = "Edward Bellamy",
  38. BirthDate = new DateTime(1850, 05, 22),
  39. ShortBio = "Edward Bellamy was an American author..."
  40. }
  41. );
  42. authorDto.Id.ShouldNotBe(Guid.Empty);
  43. authorDto.Name.ShouldBe("Edward Bellamy");
  44. }
  45. [Fact]
  46. public async Task Should_Not_Allow_To_Create_Duplicate_Author()
  47. {
  48. await Assert.ThrowsAsync<AuthorAlreadyExistsException>(async () =>
  49. {
  50. await _authorAppService.CreateAsync(
  51. new CreateAuthorDto
  52. {
  53. Name = "Douglas Adams",
  54. BirthDate = DateTime.Now,
  55. ShortBio = "..."
  56. }
  57. );
  58. });
  59. }
  60. //TODO: Test other methods...
  61. }
  62. }

完成应用服务方法的测试, 它们应该很容易理解.

下一章

查看本教程的下一章.