- 关于本教程
- 下载源代码
- 介绍
- 作者实体
- AuthorManager:领域服务
- IAuthorRepository
- 结论
- 下一部分
本教程基于版本3.1
在本教程系列中,您将构建一个名为Acme.BookStore
的基于ABP的Web应用程序。该应用程序用于管理书籍及其作者的列表。它是使用以下技术开发的:
- 实体框架核心作为ORM提供者。
- MVC/Razor页面作为UI框架。
本教程分为以下部分:
第1部分:创建服务器端
第2部分:书籍列表页面
第3部分:创建、更新和删除书籍
第4部分:集成测试
第5部分:授权
第6部分:作者:领域层(此部分)
第7部分:作者:数据库集成
第8部分:作者:应用程序层
第9部分:作者:用户界面
第10部分:书与作者的关系
下载源代码MVC (Razor Pages) UI with EF Core
介绍在前面的部分中,我们使用了ABP基础结构轻松构建了一些服务。
-
使用CrudAppService基类,而不是为标准的创建,读取,更新和删除操作手动开发应用程序服务。
-
使用通用存储库来完全自动化数据库层。
对于“作者”部分;
-
我们将手动执行某些操作,以显示在需要时如何执行此操作。
-
我们将实现一些域驱动设计(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
{
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
,可以对实体进行软删除(这意味着在删除它时,它不会在数据库中删除,而只是标记为已删除),并具有所有的审计属性。 -
对于
Name
属性的private set
限制从此类之外设置此属性。有两种设置名称的方法(在两种情况下,我们都会验证名称):- 在构造函数中,同时创建一个新作者。
- 稍后使用
ChangeName
方法来更新名称。
-
constructor
和ChangeName
方法是internal
迫使仅在领域层使用这些方法,使用AuthorManager
将在后面说明。 -
Check
类是ABP Framework实用程序类,可在检查方法参数时提供帮助(在无效的情况下它会抛出ArgumentException
异常)。
AuthorConsts
是一个简单的类,位于Acme.BookStore.Domain.Shared
项目的Authors
名称空间(文件夹)下:
namespace Acme.BookStore.Authors
{
public static class AuthorConsts
{
public const int MaxNameLength = 64;
}
}
在Acme.BookStore.Domain.Shared
项目内部创建了此类,因为稍后我们将在数据传输对象(DTO)上重用该类。
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 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技巧:除非确实需要领域服务方法并执行一些核心业务规则,否则请不要引入领域服务方法。对于这种情况,我们需要此服务能够强制唯一名称约束。
两种方法都会检查是否存在具有给定名称的作者,并引发在Acme.BookStore.Domain
项目(在Authors
文件夹中)中定义的特殊业务异常AuthorAlreadyExistsException
,如下所示:
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}"
然后打开BookStoreDomainSharedModule
并在ConfigureServices
方法内添加以下代码块:
Configure(options =>
{
options.MapCodeNamespace("BookStore", typeof(BookStoreResource));
});
每当您抛出AuthorAlreadyExistsException
时,最终用户将在UI上看到一条不错的错误消息。
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
{
Task FindByNameAsync(string name);
Task GetListAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter = null
);
}
}
-
IAuthorRepository
扩展了标准IRepository
接口,因此IAuthorRepository
也可以使用所有标准存储库方法。 -
FindByNameAsync
在AuthorManager
中用于按名称查询作者。 -
GetListAsync
将在应用程序层中使用,以获取列出的,经过排序和筛选的作者列表,以显示在UI上。
我们将在下一部分中实现此存储库。
这两种方法似乎都没有必要,因为标准存储库已经具有IQueryable
并且可以直接使用它们,而不是定义这样的自定义方法。你是对的,就像在一个真实的应用程序中一样。但是,对于本学习教程,解释在真正需要时如何创建自定义存储库方法是很有用的。
这部分涵盖了书店应用程序作者功能的领域层。下图突出显示了在此部分中创建/更新的主要文件:
请参阅本教程的下一部分。