Web 应用开发教程 - 第 8 部分:作者:应用层
简介
本部分将解释如何为之前创建的 Author 实体创建应用层。
IAuthorAppService
我们将首先创建 应用服务 接口及相关的 DTO。在 Acme.BookStore.Application.Contracts 项目的 Authors 命名空间(文件夹)中创建一个名为 IAuthorAppService 的新接口:
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Acme.BookStore.Authors;
public interface IAuthorAppService : IApplicationService
{
Task<AuthorDto> GetAsync(Guid id);
Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input);
Task<AuthorDto> CreateAsync(CreateAuthorDto input);
Task UpdateAsync(Guid id, UpdateAuthorDto input);
Task DeleteAsync(Guid id);
}
IApplicationService是一个约定接口,所有应用服务都继承自它,因此 ABP 可以识别该服务。- 定义了在
Author实体上执行 CRUD 操作的标准方法。 PagedResultDto是 ABP 中预定义的 DTO 类。它包含一个Items集合和一个TotalCount属性,用于返回分页结果。- 首选从
CreateAsync方法返回一个AuthorDto(针对新创建的作者),虽然此应用程序未使用它——只是为了展示不同的用法。
此接口使用了下面定义的 DTO(请为您的项目创建它们)。
AuthorDto
using System;
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Authors;
public class AuthorDto : EntityDto<Guid>
{
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
}
EntityDto<T>简单地包含一个具有给定泛型参数的Id属性。您也可以自行创建一个Id属性,而不是继承EntityDto<T>。
GetAuthorListDto
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Authors;
public class GetAuthorListDto : PagedAndSortedResultRequestDto
{
public string? Filter { get; set; }
}
Filter用于搜索作者。它可以为null(或空字符串)以获取所有作者。PagedAndSortedResultRequestDto包含标准的分页和排序属性:int MaxResultCount、int SkipCount和string Sorting。
ABP 提供了此类基础 DTO 类来简化和标准化您的 DTO。有关所有详细信息,请参阅 DTO 文档。
CreateAuthorDto
using System;
using System.ComponentModel.DataAnnotations;
namespace Acme.BookStore.Authors;
public class CreateAuthorDto
{
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; } = string.Empty;
[Required]
public DateTime BirthDate { get; set; }
public string? ShortBio { get; set; }
}
可以使用数据注解属性来验证 DTO。详情请参阅 验证文档。
UpdateAuthorDto
using System;
using System.ComponentModel.DataAnnotations;
namespace Acme.BookStore.Authors;
public class UpdateAuthorDto
{
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; } = string.Empty;
[Required]
public DateTime BirthDate { get; set; }
public string? ShortBio { get; set; }
}
我们本可以在创建和更新操作之间共享(复用)相同的 DTO。虽然您可以这样做,但我们更倾向于为这些操作创建不同的 DTO,因为我们看到它们随着时间的推移通常会有所不同。因此,与紧密耦合的设计相比,这里的代码重复是合理的。
AuthorAppService
现在是实现 IAuthorAppService 接口的时候了。在 Acme.BookStore.Application 项目的 Authors 命名空间(文件夹)中创建一个名为 AuthorAppService 的新类:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Acme.BookStore.Permissions;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Authors;
[Authorize(BookStorePermissions.Authors.Default)]
public class AuthorAppService : BookStoreAppService, IAuthorAppService
{
private readonly IAuthorRepository _authorRepository;
private readonly AuthorManager _authorManager;
public AuthorAppService(
IAuthorRepository authorRepository,
AuthorManager authorManager)
{
_authorRepository = authorRepository;
_authorManager = authorManager;
}
//...SERVICE METHODS WILL COME HERE...
}
[Authorize(BookStorePermissions.Authors.Default)]是一种声明式方法来检查权限(策略)以授权当前用户。更多信息请参阅 授权文档。BookStorePermissions类将在下面更新,现在先不要担心编译错误。- 继承自
BookStoreAppService,这是一个随启动模板提供的简单基类。它派生自标准的ApplicationService类。 - 实现了上面定义的
IAuthorAppService。 - 注入了
IAuthorRepository和AuthorManager以在服务方法中使用。
现在,我们将逐一介绍服务方法。将解释的方法复制到 AuthorAppService 类中。
GetAsync
public async Task<AuthorDto> GetAsync(Guid id)
{
var author = await _authorRepository.GetAsync(id);
return ObjectMapper.Map<Author, AuthorDto>(author);
}
此方法简单地通过其 Id 获取 Author 实体,并使用 对象到对象映射器 将其转换为 AuthorDto。这需要在 Mapperly 中进行配置,稍后将进行说明。
GetListAsync
public async Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input)
{
if (input.Sorting.IsNullOrWhiteSpace())
{
input.Sorting = nameof(Author.Name);
}
var authors = await _authorRepository.GetListAsync(
input.SkipCount,
input.MaxResultCount,
input.Sorting,
input.Filter
);
var totalCount = input.Filter == null
? await _authorRepository.CountAsync()
: await _authorRepository.CountAsync(
author => author.Name.Contains(input.Filter));
return new PagedResultDto<AuthorDto>(
totalCount,
ObjectMapper.Map<List<Author>, List<AuthorDto>>(authors)
);
}
- 默认排序为“按作者姓名”,如果客户端未发送排序信息,则在方法开头执行此操作。
- 使用
IAuthorRepository.GetListAsync从数据库中获取分页、排序和筛选后的作者列表。我们在本教程的上一部分已经实现了它。重申一下,实际上并不需要创建这样一个方法,因为我们可以直接通过仓储进行查询,但希望演示如何创建自定义仓储方法。 - 在获取作者数量时直接从
AuthorRepository查询。如果发送了筛选器,则在获取数量时使用它来筛选实体。 - 最后,通过将
Author列表映射到AuthorDto列表来返回分页结果。
CreateAsync
[Authorize(BookStorePermissions.Authors.Create)]
public async Task<AuthorDto> CreateAsync(CreateAuthorDto input)
{
var author = await _authorManager.CreateAsync(
input.Name,
input.BirthDate,
input.ShortBio
);
await _authorRepository.InsertAsync(author);
return ObjectMapper.Map<Author, AuthorDto>(author);
}
CreateAsync需要BookStorePermissions.Authors.Create权限(除了为AuthorAppService类声明的BookStorePermissions.Authors.Default权限之外)。- 使用
AuthorManager(领域服务)创建新作者。 - 使用
IAuthorRepository.InsertAsync将新作者插入数据库。 - 使用
ObjectMapper返回一个代表新创建作者的AuthorDto。
DDD 提示:一些开发者可能认为在
_authorManager.CreateAsync内部插入新实体很有用。我们认为将其留给应用层是更好的设计,因为它更清楚何时将其插入数据库(也许在插入之前需要对实体进行额外的工作,如果我们在领域服务中执行插入,则需要额外的更新)。然而,这完全取决于您。
UpdateAsync
[Authorize(BookStorePermissions.Authors.Edit)]
public async Task UpdateAsync(Guid id, UpdateAuthorDto input)
{
var author = await _authorRepository.GetAsync(id);
if (author.Name != input.Name)
{
await _authorManager.ChangeNameAsync(author, input.Name);
}
author.BirthDate = input.BirthDate;
author.ShortBio = input.ShortBio;
await _authorRepository.UpdateAsync(author);
}
UpdateAsync需要额外的BookStorePermissions.Authors.Edit权限。- 使用
IAuthorRepository.GetAsync从数据库获取作者实体。如果不存在具有给定 id 的作者,GetAsync会抛出EntityNotFoundException,这在 Web 应用程序中会导致404HTTP 状态码。在更新操作中始终加载实体是一种良好实践。 - 使用
AuthorManager.ChangeNameAsync(领域服务方法)来更改作者姓名(如果客户端请求更改)。 - 直接更新
BirthDate和ShortBio,因为更改这些属性没有任何业务规则,它们接受任何值。 - 最后,调用
IAuthorRepository.UpdateAsync方法在数据库中更新实体。
DeleteAsync
[Authorize(BookStorePermissions.Authors.Delete)]
public async Task DeleteAsync(Guid id)
{
await _authorRepository.DeleteAsync(id);
}
DeleteAsync需要额外的BookStorePermissions.Authors.Delete权限。- 它简单地使用仓储的
DeleteAsync方法。
权限定义
您目前无法编译代码,因为它需要一些在 BookStorePermissions 类中声明的常量。
打开 Acme.BookStore.Application.Contracts 项目中的 BookStorePermissions 类(位于 Permissions 文件夹中)并添加新的权限名称:
namespace Acme.BookStore.Permissions;
public static class BookStorePermissions
{
public const string GroupName = "BookStore";
// 其他权限...
// 其他权限...
// *** 添加了一个新的嵌套类 ***
public static class Authors
{
public const string Default = GroupName + ".Authors";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}
}
然后在同一项目中打开 BookStorePermissionDefinitionProvider,并在 Define 方法的末尾添加以下几行:
var authorsPermission = bookStoreGroup.AddPermission(
BookStorePermissions.Authors.Default, L("Permission:Authors"));
authorsPermission.AddChild(
BookStorePermissions.Authors.Create, L("Permission:Authors.Create"));
authorsPermission.AddChild(
BookStorePermissions.Authors.Edit, L("Permission:Authors.Edit"));
authorsPermission.AddChild(
BookStorePermissions.Authors.Delete, L("Permission:Authors.Delete"));
最后,将以下条目添加到 Acme.BookStore.Domain.Shared 项目中的 Localization/BookStore/en.json 文件中,以本地化权限名称:
"Permission:Authors": "作者管理",
"Permission:Authors.Create": "创建新作者",
"Permission:Authors.Edit": "编辑作者",
"Permission:Authors.Delete": "删除作者"
对象到对象映射
AuthorAppService 使用 ObjectMapper 将 Author 对象转换为 AuthorDto 对象。因此,我们需要在 Mapperly 配置中定义此映射。
打开 Acme.BookStore.Application 项目中的 BookStoreApplicationMappers 类,并定义以下映射类:
[Mapper]
public partial class AuthorToAuthorDtoMapper : MapperBase<Author, AuthorDto>
{
public override partial AuthorDto Map(Author source);
public override partial void Map(Author source, AuthorDto destination);
}
数据种子
就像之前对书籍所做的那样,在数据库中有一些初始作者实体是很好的。这在首次运行应用程序时很有用,同时对于自动化测试也非常有用。
打开 Acme.BookStore.Domain 项目中的 BookStoreDataSeederContributor,并将文件内容更改为以下代码:
using System;
using System.Threading.Tasks;
using Acme.BookStore.Authors;
using Acme.BookStore.Books;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore;
public class BookStoreDataSeederContributor
: IDataSeedContributor, ITransientDependency
{
private readonly IRepository<Book, Guid> _bookRepository;
private readonly IAuthorRepository _authorRepository;
private readonly AuthorManager _authorManager;
public BookStoreDataSeederContributor(
IRepository<Book, Guid> bookRepository,
IAuthorRepository authorRepository,
AuthorManager authorManager)
{
_bookRepository = bookRepository;
_authorRepository = authorRepository;
_authorManager = authorManager;
}
public async Task SeedAsync(DataSeedContext context)
{
if (await _bookRepository.GetCountAsync() <= 0)
{
await _bookRepository.InsertAsync(
new Book
{
Name = "1984",
Type = BookType.Dystopia,
PublishDate = new DateTime(1949, 6, 8),
Price = 19.84f
},
autoSave: true
);
await _bookRepository.InsertAsync(
new Book
{
Name = "The Hitchhiker's Guide to the Galaxy",
Type = BookType.ScienceFiction,
PublishDate = new DateTime(1995, 9, 27),
Price = 42.0f
},
autoSave: true
);
}
// 为作者添加种子数据
if (await _authorRepository.GetCountAsync() <= 0)
{
await _authorRepository.InsertAsync(
await _authorManager.CreateAsync(
"George Orwell",
new DateTime(1903, 06, 25),
"Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)."
)
);
await _authorRepository.InsertAsync(
await _authorManager.CreateAsync(
"Douglas Adams",
new DateTime(1952, 03, 11),
"Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'."
)
);
}
}
}
现在您可以运行 .DbMigrator 控制台应用程序来 填充 初始数据。
测试作者应用服务
最后,我们可以为 IAuthorAppService 编写一些测试。在 Acme.BookStore.Application.Tests 项目的 Authors 命名空间(文件夹)中添加一个名为 AuthorAppService_Tests 的新类:
using System;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Modularity;
using Xunit;
namespace Acme.BookStore.Authors;
public abstract class AuthorAppService_Tests<TStartupModule> : BookStoreApplicationTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
private readonly IAuthorAppService _authorAppService;
protected AuthorAppService_Tests()
{
_authorAppService = GetRequiredService<IAuthorAppService>();
}
[Fact]
public async Task Should_Get_All_Authors_Without_Any_Filter()
{
var result = await _authorAppService.GetListAsync(new GetAuthorListDto());
result.TotalCount.ShouldBeGreaterThanOrEqualTo(2);
result.Items.ShouldContain(author => author.Name == "George Orwell");
result.Items.ShouldContain(author => author.Name == "Douglas Adams");
}
[Fact]
public async Task Should_Get_Filtered_Authors()
{
var result = await _authorAppService.GetListAsync(
new GetAuthorListDto {Filter = "George"});
result.TotalCount.ShouldBeGreaterThanOrEqualTo(1);
result.Items.ShouldContain(author => author.Name == "George Orwell");
result.Items.ShouldNotContain(author => author.Name == "Douglas Adams");
}
[Fact]
public async Task Should_Create_A_New_Author()
{
var authorDto = await _authorAppService.CreateAsync(
new CreateAuthorDto
{
Name = "Edward Bellamy",
BirthDate = new DateTime(1850, 05, 22),
ShortBio = "Edward Bellamy was an American author..."
}
);
authorDto.Id.ShouldNotBe(Guid.Empty);
authorDto.Name.ShouldBe("Edward Bellamy");
}
[Fact]
public async Task Should_Not_Allow_To_Create_Duplicate_Author()
{
await Assert.ThrowsAsync<AuthorAlreadyExistsException>(async () =>
{
await _authorAppService.CreateAsync(
new CreateAuthorDto
{
Name = "Douglas Adams",
BirthDate = DateTime.Now,
ShortBio = "..."
}
);
});
}
//TODO: 测试其他方法...
}
在 Acme.BookStore.MongoDB.Tests 项目的 MongoDb\Applications\Authors 命名空间(文件夹)中,为 AuthorAppService_Tests 类添加一个新的实现类,命名为 MongoDBAuthorAppService_Tests:
using Acme.BookStore.MongoDB;
using Acme.BookStore.Authors;
using Xunit;
namespace Acme.BookStore.MongoDb.Applications.Authors;
[Collection(BookStoreTestConsts.CollectionDefinitionName)]
public class MongoDBAuthorAppService_Tests : AuthorAppService_Tests<BookStoreMongoDbTestModule>
{
}
我们创建了一些针对应用服务方法的测试,这些测试应该易于理解。
抠丁客


