Web应用开发教程 - Part 6: 作家:领域层
//[doc-params]
{
"UI": ["MVC","Blazor","BlazorServer","NG"],
"DB": ["EF","Mongo"]
}
关于本教程
在这个教程系列中,你将创建一个基于ABP的Web应用程序,叫 Acme.BookStore
。这个应用程序是用来管理书籍及其作者的列表的。它是使用以下技术开发:
- {{DB_Value}} 作为ORM的提供者。
- {{UI_Value}} 作为UI框架。
本教程由以下几个部分组成:
- 第一部分:创建服务器端
- 第2部分:书籍列表页
- 第3部分:创建、更新和删除书籍
- 第四部分:集成测试
- 第5部分:授权
- 第六部分:作家:领域层(本部分)
- 第七部分:作家:数据库集成
- 第8部分:作家:应用层
- 第九部分:作家:用户界面
- 第10部分:关联书籍与作家
下载源代码
本教程根据你对UI和Database的偏好有多个版本。我们准备了几个组合的源代码供大家下载:
如果你在Windows上遇到 "文件名太长 "或 "解压错误",它可能与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的基础框架轻松地构建了以下服务:
- 使用基类CrudAppService,而不是为人工开发一个具有增删改查标准操作的应用服务。
- 使用generic repositories使数据库层完全自动化。
对于 "作家 "部分:
- 我们将手动做一些事情,以显示你在需要时可以如何做。
- 我们将完成一些领域驱动设计(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
限制了从类外部设置该属性。有两种设置Name
的方法(在这两种情况下,我们都要验证名称): - 在构造函数中,当创建一个新作家时。
- 后续使用
ChangeName
方法更新Name
时。 -
constructor
和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}"
每当你抛出一个AuthorAlreadyExistsException
时,终端用户会在用户界面上看到一个漂亮的错误信息。
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
。 -
FindByNameAsync
在AuthorManager
中被用来按名字查询作家。 -
GetListAsync
将用于应用层,以获得一个多行的、经过排序和过滤的作家列表并在用户界面上显示。
我们将在下一部分实现这个存储库。
这两种方法可能看起来没有必要,因为标准资源库已存在
IQueryable
,你可以直接使用它们,而不是定义这样的自定义方法。你是对的,在一个真正的应用程序中就像那样做。然而,对于这个学习教程来说,解释如何在你真正需要的时候创建自定义的资源库方法是很有用的。
总结
This part covered the domain layer of the authors functionality of the book store application. The main files created/updated in this part was highlighted in the picture below:
这一部分涵盖了书店应用程序的作家管理功能的领域层。这一部分创建/更新的主要文件在下面的图片中高亮显示:
下篇
参见本教程的下一部分。