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

简介

在前面的章节中, 我们使用 ABP 框架轻松地构建了一些服务;

对于 “作者” 部分;

  • 我们将要展示在需要的情况下, 如何 手工做一些事情.
  • 我们将要实现一些 领域驱动设计 (DDD) 最佳实践.

开发将会逐层完成, 一次聚焦一层. 在真实项目中, 你会逐个功能(垂直)开发, 如同前面的教程. 通过这种方式, 你可以体验这两种方式

作者实体

Acme.BookStore.Domain 项目中创建 Authors 文件夹 (命名空间), 在其中加入 Author 类:

  1. using System;
  2. using JetBrains.Annotations;
  3. using Volo.Abp;
  4. using Volo.Abp.Domain.Entities.Auditing;
  5. namespace Acme.BookStore.Authors
  6. {
  7. public class Author : FullAuditedAggregateRoot<Guid>
  8. {
  9. public string Name { get; private set; }
  10. public DateTime BirthDate { get; set; }
  11. public string ShortBio { get; set; }
  12. private Author()
  13. {
  14. /* This constructor is for deserialization / ORM purpose */
  15. }
  16. internal Author(
  17. Guid id,
  18. [NotNull] string name,
  19. DateTime birthDate,
  20. [CanBeNull] string shortBio = null)
  21. : base(id)
  22. {
  23. SetName(name);
  24. BirthDate = birthDate;
  25. ShortBio = shortBio;
  26. }
  27. internal Author ChangeName([NotNull] string name)
  28. {
  29. SetName(name);
  30. return this;
  31. }
  32. private void SetName([NotNull] string name)
  33. {
  34. Name = Check.NotNullOrWhiteSpace(
  35. name,
  36. nameof(name),
  37. maxLength: AuthorConsts.MaxNameLength
  38. );
  39. }
  40. }
  41. }
  • FullAuditedAggregateRoot<Guid> 继承使得实体支持软删除 (指实体被删除时, 它并没有从数据库中被删除, 而只是被标记删除), 实体也具有了 审计 属性.
  • Name 属性的 private set 限制从类的外部设置这个属性. 有两种方法设置名字 (两种都进行了验证):
    • 当新建一个作者时, 通过构造器.
    • 使用 ChangeName 方法更新名字.
  • 构造器ChangeName 方法的访问级别是 internal, 强制这些方法只能在领域层由 AuthorManager 使用. 稍后将对此进行解释.
  • Check 类是一个ABP框架工具类, 用于检查方法参数 (如果参数非法会抛出 ArgumentException).

AuthorConsts 是一个简单的类, 它位于 Acme.BookStore.Domain.Shared 项目的 Authors 命名空间 (文件夹)中:

  1. namespace Acme.BookStore.Authors
  2. {
  3. public static class AuthorConsts
  4. {
  5. public const int MaxNameLength = 64;
  6. }
  7. }

Acme.BookStore.Domain.Shared 项目中创建这个类, 因为数据传输类 (DTOs) 稍后会再一次用到它.

AuthorManager: 领域服务

Author 构造器和 ChangeName 方法的访问级别是 internal, 所以它们只能在领域层使用. 在 Acme.BookStore.Domain 项目中的 Authors 文件夹 (命名空间)创建 AuthorManager 类:

  1. using System;
  2. using System.Threading.Tasks;
  3. using JetBrains.Annotations;
  4. using Volo.Abp;
  5. using Volo.Abp.Domain.Services;
  6. namespace Acme.BookStore.Authors
  7. {
  8. public class AuthorManager : DomainService
  9. {
  10. private readonly IAuthorRepository _authorRepository;
  11. public AuthorManager(IAuthorRepository authorRepository)
  12. {
  13. _authorRepository = authorRepository;
  14. }
  15. public async Task<Author> CreateAsync(
  16. [NotNull] string name,
  17. DateTime birthDate,
  18. [CanBeNull] string shortBio = null)
  19. {
  20. Check.NotNullOrWhiteSpace(name, nameof(name));
  21. var existingAuthor = await _authorRepository.FindByNameAsync(name);
  22. if (existingAuthor != null)
  23. {
  24. throw new AuthorAlreadyExistsException(name);
  25. }
  26. return new Author(
  27. GuidGenerator.Create(),
  28. name,
  29. birthDate,
  30. shortBio
  31. );
  32. }
  33. public async Task ChangeNameAsync(
  34. [NotNull] Author author,
  35. [NotNull] string newName)
  36. {
  37. Check.NotNull(author, nameof(author));
  38. Check.NotNullOrWhiteSpace(newName, nameof(newName));
  39. var existingAuthor = await _authorRepository.FindByNameAsync(newName);
  40. if (existingAuthor != null && existingAuthor.Id != author.Id)
  41. {
  42. throw new AuthorAlreadyExistsException(newName);
  43. }
  44. author.ChangeName(newName);
  45. }
  46. }
  47. }
  • AuthorManager 强制使用一种可控的方式创建作者和修改作者的名字. 应用层 (后面会介绍) 将会使用这些方法.

DDD 提示: 如非必须并且用于执行核心业务规则, 不要引入领域服务方法. 对于这个场景, 我们使用这个服务保证名字的唯一性.

两个方法都检查是否存在同名用户, 如果存在, 抛出业务异常 AuthorAlreadyExistsException, 这个异常定义在 Acme.BookStore.Domain 项目 (Authors 文件夹中):

  1. using Volo.Abp;
  2. namespace Acme.BookStore.Authors
  3. {
  4. public class AuthorAlreadyExistsException : BusinessException
  5. {
  6. public AuthorAlreadyExistsException(string name)
  7. : base(BookStoreDomainErrorCodes.AuthorAlreadyExists)
  8. {
  9. WithData("name", name);
  10. }
  11. }
  12. }

BusinessException 是一个特殊的异常类型. 在需要时抛出领域相关异常是一个好的实践. ABP框架会自动处理它, 并且它也容易本地化. WithData(...) 方法提供额外的数据给异常对象, 这些数据将会在本地化中或出于其它一些目的被使用.

打开 Acme.BookStore.Domain.Shared 项目中的 BookStoreDomainErrorCodes 并修改为:

  1. namespace Acme.BookStore
  2. {
  3. public static class BookStoreDomainErrorCodes
  4. {
  5. public const string AuthorAlreadyExists = "BookStore:00001";
  6. }
  7. }

这里定义了一个字符串, 表示应用程序抛出的错误码, 这个错误码可以被客户端应用程序处理. 为了用户, 你可能希望本地化它. 打开 Acme.BookStore.Domain.Shared 项目中的 Localization/BookStore/en.json , 加入以下项:

  1. "BookStore:00001": "There is already an author with the same name: {name}"

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

AuthorAlreadyExistsException 被抛出, 终端用户将会在UI上看到组织好的错误消息.

IAuthorRepository

AuthorManager 注入了 IAuthorRepository, 所以我们需要定义它. 在 Acme.BookStore.Domain 项目的 Authors 文件夹 (命名空间) 中创建这个新接口:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Threading.Tasks;
  4. using Volo.Abp.Domain.Repositories;
  5. namespace Acme.BookStore.Authors
  6. {
  7. public interface IAuthorRepository : IRepository<Author, Guid>
  8. {
  9. Task<Author> FindByNameAsync(string name);
  10. Task<List<Author>> GetListAsync(
  11. int skipCount,
  12. int maxResultCount,
  13. string sorting,
  14. string filter = null
  15. );
  16. }
  17. }
  • IAuthorRepository 扩展了标准 IRepository<Author, Guid> 接口, 所以所有的标准 repository 方法对于 IAuthorRepository 都是可用的.
  • FindByNameAsyncAuthorManager 中用来根据姓名查询用户.
  • GetListAsync 用于应用层以获得一个排序的, 经过过滤的作者列表, 显示在UI上.

我们会在下一章实现这个repository.

这两个方法似乎 看上去没有必要, 因为标准repositories已经是 IQueryable, 你可以直接使用它们, 而不是自定义方法. 在实际应用程序中, 这么做是没问题的. 但在这个 学习指南中, 解释如何在需要时创建自定义repository方法是有价值的.

结论

这一章覆盖了图书管理程序作者相关功能的领域层. 在这一章中创建/更新的文件在下图中被高亮:

bookstore-author-domain-layer

下一章

查看本教程的下一章.