版本
语言

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

UI
Database

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 类:

using System;
using JetBrains.Annotations;
using Volo.Abp;
using Volo.Abp.Domain.Entities.Auditing;

namespace Acme.BookStore.Authors
{
    public class Author : FullAuditedAggregateRoot<Guid>
    {
        public string Name { get; private set; }
        public DateTime BirthDate { get; set; }
        public string ShortBio { get; set; }

        private Author()
        {
            /* This constructor is for deserialization / ORM purpose */
        }

        internal Author(
            Guid id,
            [NotNull] string name,
            DateTime birthDate,
            [CanBeNull] string shortBio = null)
            : base(id)
        {
            SetName(name);
            BirthDate = birthDate;
            ShortBio = shortBio;
        }

        internal Author ChangeName([NotNull] string name)
        {
            SetName(name);
            return this;
        }

        private void SetName([NotNull] string name)
        {
            Name = Check.NotNullOrWhiteSpace(
                name,
                nameof(name),
                maxLength: AuthorConsts.MaxNameLength
            );
        }
    }
}
  • FullAuditedAggregateRoot<Guid> 继承使得实体支持软删除 (指实体被删除时, 它并没有从数据库中被删除, 而只是被标记删除), 实体也具有了 审计 属性.
  • Name 属性的 private set 限制从类的外部设置这个属性. 有两种方法设置名字 (两种都进行了验证):
    • 当新建一个作者时, 通过构造器.
    • 使用 ChangeName 方法更新名字.
  • 构造器ChangeName 方法的访问级别是 internal, 强制这些方法只能在领域层由 AuthorManager 使用. 稍后将对此进行解释.
  • Check 类是一个ABP框架工具类, 用于检查方法参数 (如果参数非法会抛出 ArgumentException).

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

namespace Acme.BookStore.Authors
{
    public static class AuthorConsts
    {
        public const int MaxNameLength = 64;
    }
}

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

AuthorManager: 领域服务

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

using System;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Volo.Abp;
using Volo.Abp.Domain.Services;

namespace Acme.BookStore.Authors
{
    public class AuthorManager : DomainService
    {
        private readonly IAuthorRepository _authorRepository;

        public AuthorManager(IAuthorRepository authorRepository)
        {
            _authorRepository = authorRepository;
        }

        public async Task<Author> CreateAsync(
            [NotNull] string name,
            DateTime birthDate,
            [CanBeNull] string shortBio = null)
        {
            Check.NotNullOrWhiteSpace(name, nameof(name));

            var existingAuthor = await _authorRepository.FindByNameAsync(name);
            if (existingAuthor != null)
            {
                throw new AuthorAlreadyExistsException(name);
            }

            return new Author(
                GuidGenerator.Create(),
                name,
                birthDate,
                shortBio
            );
        }

        public async Task ChangeNameAsync(
            [NotNull] Author author,
            [NotNull] string newName)
        {
            Check.NotNull(author, nameof(author));
            Check.NotNullOrWhiteSpace(newName, nameof(newName));

            var existingAuthor = await _authorRepository.FindByNameAsync(newName);
            if (existingAuthor != null && existingAuthor.Id != author.Id)
            {
                throw new AuthorAlreadyExistsException(newName);
            }

            author.ChangeName(newName);
        }
    }
}
  • AuthorManager 强制使用一种可控的方式创建作者和修改作者的名字. 应用层 (后面会介绍) 将会使用这些方法.

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

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

using Volo.Abp;

namespace Acme.BookStore.Authors
{
    public class AuthorAlreadyExistsException : BusinessException
    {
        public AuthorAlreadyExistsException(string name)
            : base(BookStoreDomainErrorCodes.AuthorAlreadyExists)
        {
            WithData("name", name);
        }
    }
}

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

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

namespace Acme.BookStore
{
    public static class BookStoreDomainErrorCodes
    {
        public const string AuthorAlreadyExists = "BookStore:00001";
    }
}

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

"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 文件夹 (命名空间) 中创建这个新接口:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;

namespace Acme.BookStore.Authors
{
    public interface IAuthorRepository : IRepository<Author, Guid>
    {
        Task<Author> FindByNameAsync(string name);

        Task<List<Author>> GetListAsync(
            int skipCount,
            int maxResultCount,
            string sorting,
            string filter = null
        );
    }
}
  • IAuthorRepository 扩展了标准 IRepository<Author, Guid> 接口, 所以所有的标准 repository 方法对于 IAuthorRepository 都是可用的.
  • FindByNameAsyncAuthorManager 中用来根据姓名查询用户.
  • GetListAsync 用于应用层以获得一个排序的, 经过过滤的作者列表, 显示在UI上.

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

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

结论

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

bookstore-author-domain-layer

下一章

查看本教程的下一章.

本页对您有帮助吗?
请进行选择。
感谢您的宝贵意见!

请注意,虽然我们无法回复反馈意见,但我们的团队会根据您的意见改进体验。

在本文档中
Mastering ABP Framework Book
掌握 ABP 框架

本书将帮助你全面了解框架和现代Web应用程序开发技术。