Web应用开发教程 - Part 6: 作家:领域层

Web应用开发教程 - Part 6: 作家:领域层

//[doc-params]
{
    "UI": ["MVC","Blazor","BlazorServer","NG"],
    "DB": ["EF","Mongo"]
}

关于本教程

在这个教程系列中,你将创建一个基于ABP的Web应用程序,叫 Acme.BookStore。这个应用程序是用来管理书籍及其作者的列表的。它是使用以下技术开发:

  • {{DB_Value}} 作为ORM的提供者。
  • {{UI_Value}} 作为UI框架。

本教程由以下几个部分组成:

下载源代码

本教程根据你对UIDatabase的偏好有多个版本。我们准备了几个组合的源代码供大家下载:

如果你在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的基础框架轻松地构建了以下服务:

对于 "作家 "部分:

  • 我们将手动做一些事情,以显示你在需要时可以如何做。
  • 我们将完成一些领域驱动设计(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>使实体具有软删除特性等所有审计属性(这意味着当你删除它时,它不会在数据库中被真的删除,而只是被标记为删除)。
  • 属性Nameprivate set限制了从类外部设置该属性。有两种设置Name的方法(在这两种情况下,我们都要验证名称):
  • 在构造函数中,当创建一个新作家时。
  • 后续使用ChangeName方法更新Name时。
  • constructorChangeName方法是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
  • FindByNameAsyncAuthorManager中被用来按名字查询作家。
  • 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:

这一部分涵盖了书店应用程序的作家管理功能的领域层。这一部分创建/更新的主要文件在下面的图片中高亮显示:
Web应用开发教程 - Part 6: 作家:领域层

下篇

参见本教程的下一部分

上一篇:IfcDistributionFlowElementType


下一篇:IfcDistributionSystem