项目

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

UI
Database

Web 应用开发教程 - 第 6 部分:作者:领域层

简介

在前面的部分中,我们利用 ABP 的基础设施轻松构建了一些服务;

  • 使用 CrudAppService 基类,而非手动为标准的增删改查操作开发应用服务。
  • 使用 通用仓储 来完全自动化数据库层。

对于“作者”这一部分;

  • 我们将 手动完成部分工作,以便展示在需要时您可以如何操作。
  • 我们将实现一些 领域驱动设计 (DDD) 最佳实践

开发将逐层进行,以便一次专注于单个层。在实际项目中,您将按功能(垂直)开发您的应用,正如前面部分所做的那样。通过这种方式,您将体验两种不同的开发方式。

作者实体

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

using System;
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()
    {
        /* 此构造函数用于反序列化 / ORM 目的 */
    }

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

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

    private void SetName(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 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(
        string name,
        DateTime birthDate,
        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(
        Author author,
        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": "已存在同名作者:{name}"

每当您抛出 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> 接口,因此所有标准的 仓储 方法也将对 IAuthorRepository 可用。
  • FindByNameAsyncAuthorManager 中用于按名称查询作者。
  • GetListAsync 将在应用层中使用,以获取经过分页、排序和筛选的作者列表,用于在 UI 上显示。

我们将在下一部分实现此仓储。

这两种方法可能 看起来不必要,因为标准仓储已经提供了通用的查询方法,您可以轻松使用它们,而无需定义此类自定义方法。您是对的,在真实应用中也可以这样做。然而,对于这个 “学习”教程,解释在真正需要时如何创建自定义仓储方法是有用的。

总结

本部分涵盖了书店应用程序中作者功能的领域层。本部分创建/更新的主要文件在下图中突出显示:

bookstore-author-domain-layer


在本文档中