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可用。FindByNameAsync在AuthorManager中用于按名称查询作者。GetListAsync将在应用层中使用,以获取经过分页、排序和筛选的作者列表,用于在 UI 上显示。
我们将在下一部分实现此仓储。
这两种方法可能 看起来不必要,因为标准仓储已经提供了通用的查询方法,您可以轻松使用它们,而无需定义此类自定义方法。您是对的,在真实应用中也可以这样做。然而,对于这个 “学习”教程,解释在真正需要时如何创建自定义仓储方法是有用的。
总结
本部分涵盖了书店应用程序中作者功能的领域层。本部分创建/更新的主要文件在下图中突出显示:
抠丁客



