diff --git a/docs/zh-Hans/Tutorials/Part-5.md b/docs/zh-Hans/Tutorials/Part-5.md index df1ef80ad9..be687a6cb9 100644 --- a/docs/zh-Hans/Tutorials/Part-5.md +++ b/docs/zh-Hans/Tutorials/Part-5.md @@ -6,14 +6,14 @@ "DB": ["EF","Mongo"] } ```` -## About This Tutorial +## 关于本教程 在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: * **{{DB_Text}}** 做为ORM提供程序. * **{{UI_Value}}** 做为UI框架. - This tutorial is organized as the following parts; +本教程分为以下部分: - [Part 1: 创建服务端](Part-1.md) - [Part 2: 图书列表页面](Part-2.md) @@ -34,12 +34,12 @@ * [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) - > 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). +> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - > 如果你遇到与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` +> 如果你遇到与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` - {{if UI == "MVC" && DB == "EF"}} +{{if UI == "MVC" && DB == "EF"}} ### 视频教程 diff --git a/docs/zh-Hans/Tutorials/Part-6.md b/docs/zh-Hans/Tutorials/Part-6.md index 602306c484..6030c8f3b9 100644 --- a/docs/zh-Hans/Tutorials/Part-6.md +++ b/docs/zh-Hans/Tutorials/Part-6.md @@ -1 +1,278 @@ -TODO.. \ No newline at end of file +# Web应用程序开发教程 - 第六章: 作者: 领域层 +````json +//[doc-params] +{ + "UI": ["MVC","Blazor","BlazorServer","NG"], + "DB": ["EF","Mongo"] +} +```` +## 关于本教程 + +在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: + +* **{{DB_Text}}** 做为ORM提供程序. +* **{{UI_Value}}** 做为UI框架. + +本教程分为以下部分: + +- [Part 1: 创建服务端](Part-1.md) +- [Part 2: 图书列表页面](Part-2.md) +- [Part 3: 创建,更新和删除图书](Part-2.md) +- [Part 4: 集成测试](Part-4.md) +- [Part 5: 授权](Part-5.md) +- **Part 6: 作者: 领域层**(本章) +- [Part 7: 作者: 数据库集成](Part-7.md) +- [Part 8: 作者: 应用服务层](Part-8.md) +- [Part 9: 作者: 用户页面](Part-9.md) +- [Part 10: 图书到作者的关系](Part-10.md) + +## 下载源码 + +本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合: + +* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) +* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) + +> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). + +> 如果你遇到与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](../Application-Services.md) 基类, 而不是为标准的增删改查操作手工开发应用服务. +* 使用 [generic repositories](../Repositories.md) 自动完成数据层功能. + +对于 "作者" 部分; + +* 我们将要展示在需要的情况下, 如何 **手工做一些事情**. +* 我们将要实现一些 **领域驱动设计 (DDD) 最佳实践**. + +> **开发将会逐层完成, 一次聚焦一层. 在真实项目中, 你会逐个功能(垂直)开发, 如同前面的教程. 通过这种方式, 你可以体验这两种方式** + +## 作者实体 + +在 `Acme.BookStore.Domain` 项目中创建 `Authors` 文件夹 (命名空间), 在其中加入 `Author` 类: + +````csharp +using System; +using JetBrains.Annotations; +using Volo.Abp; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Acme.BookStore.Authors +{ + public class Author : FullAuditedAggregateRoot + { + 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` 继承使得实体支持[软删除](../Data-Filtering.md) (指实体被删除时, 它并没有从数据库中被删除, 而只是被标记删除), 实体也具有了 [审计](../Entities.md) 属性. +* `Name` 属性的 `private set` 限制从类的外部设置这个属性. 有两种方法设置名字 (两种都进行了验证): + * 当新建一个作者时, 通过构造器. + * 使用 `ChangeName` 方法更新名字. +* `构造器` 和 `ChangeName` 方法的访问级别是 `internal`, 强制这些方法只能在领域层由 `AuthorManager` 使用. 稍后将对此进行解释. +* `Check` 类是一个ABP框架工具类, 用于检查方法参数 (如果参数非法会抛出 `ArgumentException`). + +`AuthorConsts` 是一个简单的类, 它位于 `Acme.BookStore.Domain.Shared` 项目的 `Authors` 命名空间 (文件夹)中: + +````csharp +namespace Acme.BookStore.Authors +{ + public static class AuthorConsts + { + public const int MaxNameLength = 64; + } +} +```` + +在 `Acme.BookStore.Domain.Shared` 项目中创建这个类, 因为[数据传输类](../Data-Transfer-Objects.md) (DTOs) 稍后会再一次用到它. + +## AuthorManager: 领域服务 + +`Author` 构造器和 `ChangeName` 方法的访问级别是 `internal`, 所以它们只能在领域层使用. 在 `Acme.BookStore.Domain` 项目中的 `Authors` 文件夹 (命名空间)创建 `AuthorManager` 类: + +````csharp +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 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` 文件夹中): + +````csharp +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` 并修改为: + +````csharp +namespace Acme.BookStore +{ + public static class BookStoreDomainErrorCodes + { + public const string AuthorAlreadyExists = "BookStore:00001"; + } +} +```` + +这里定义了一个字符串, 表示应用程序抛出的错误码, 这个错误码可以被客户端应用程序处理. 为了用户, 你可能希望本地化它. 打开 `Acme.BookStore.Domain.Shared` 项目中的 `Localization/BookStore/en.json` , 加入以下项: + +````json +"BookStore:00001": "There is already an author with the same name: {name}" +```` + +当 `AuthorAlreadyExistsException` 被抛出, 终端用户将会在UI上看到组织好的错误消息. + +## IAuthorRepository + +`AuthorManager` 注入了 `IAuthorRepository`, 所以我们需要定义它. 在 `Acme.BookStore.Domain` 项目的 `Authors` 文件夹 (命名空间) 中创建这个新接口: + +````csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Acme.BookStore.Authors +{ + public interface IAuthorRepository : IRepository + { + Task FindByNameAsync(string name); + + Task> GetListAsync( + int skipCount, + int maxResultCount, + string sorting, + string filter = null + ); + } +} +```` + +* `IAuthorRepository` 扩展了标准 `IRepository` 接口, 所以所有的标准 [repository](../Repositories.md) 方法对于 `IAuthorRepository` 都是可用的. +* `FindByNameAsync` 在 `AuthorManager` 中用来根据姓名查询用户. +* `GetListAsync` 用于应用层以获得一个排序的, 经过过滤的作者列表, 显示在UI上. + +我们会在下一章实现这个repository. + +> 这两个方法似乎 **看上去没有必要**, 因为标准repositories已经是 `IQueryable`, 你可以直接使用它们, 而不是自定义方法. 在实际应用程序中, 这么做是没问题的. 但在这个 **学习指南**中, 解释如何在需要时创建自定义repository方法是有价值的. + +## 结论 + +这一章覆盖了图书管理程序作者相关功能的领域层. 在这一章中创建/更新的文件在下图中被高亮: + +![bookstore-author-domain-layer](images/bookstore-author-domain-layer.png) + +## 下一章 + +查看本教程的[下一章](Part-7.md).