Abp vNext - 应用开发系列之应用层

在系列教程中,我们会构建一个基于ABP的Web应用程序,用于管理书籍及其作者列表

使用到的技术:

  • Entity Framework Core
  • Angular

本教程分为以下部分:

IAuthorAppService

创建Author 应用层

Acme.BookStore.Application.Contracts 项目的 Authors 命名空间(文件夹)中创建 IAuthorAppService 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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服务
  • 定义标准CRUD方法
  • PagedResultDto 是 ABP 中预定义的 DTO 类,它有一个 Items 集合和一个 TotalCount 属性来返回分页结果

Dtos

AuthorDto

  • EntityDto<T> 只是具有给定泛型参数的 Id 属性。可以自己创建一个 Id 属性,而不是继承 EntityDto<T>
1
2
3
4
5
6
7
8
9
10
11
12
13
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; }
}

GetAuthorListDto

  • Filter:用于过滤搜索
  • PagedAndSortedResultRequestDto 具有标准的分页和排序属性: int MaxResultCountint SkipCount string Sorting
1
2
3
4
5
6
7
8
using Volo.Abp.Application.Dtos;

namespace Acme.BookStore.Authors;

public class GetAuthorListDto : PagedAndSortedResultRequestDto
{
public string? Filter { get; set; }
}

CreateAuthorDto

  • 数据注释属性可用于验证 DTO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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; }
}

UpdateAuthorDto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

Acme.BookStore.Application项目中创建Authors文件夹,并新增AuthorAppService类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
}
}
  • [Authorize(BookStorePermissions.Authors.Default)] 是一种声明性方式,用于检查权限(策略)以授权当前用户

  • 派生自 BookStoreAppService ,服务基类,附带了启动模板,继承自标准 ApplicationService

  • 实现定义的 IAuthorAppService

  • 注入 IAuthorRepositoryAuthorManager

GetAsync

根据Id查询单个作者信息

1
2
3
4
5
public async Task<AuthorDto> GetAsync(Guid id)
{
var author = await _authorRepository.GetAsync(id);
return ObjectMapper.Map<Author, AuthorDto>(author);
}

GetListAsync

获取分页数据

  • 默认按作者姓名排序
  • IAuthorRepository.GetListAsync 用于从数据库中获取分页、排序和过滤的作者列表
  • AuthorRepository 查询作者数,如果有过滤则添加过滤
  • 返回PagedResultDto,映射Author、AuthorDto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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)
);
}

CreateAsync

创建作者信息

  • CreateAsync需要声明BookStorePermissions.Authors.Create权限
  • 使用_authorManager创建新作者
  • 使用_authorRepository.InsertAsync将作者插入数据库
  • ObjectMapper返回创建完的AuthorDto
1
2
3
4
5
6
7
8
9
10
11
12
13
[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);
}

也可以在_authorManager.CreateAsync中插入数据

UpdateAsync

更新作者信息

  • UpdateAsync需要声明BookStorePermissions.Authors.Edit权限

  • IAuthorRepository.GetAsync 用于从数据库中获取创作实体,如何author查询为NULL则会抛出404

  • 如果客户端请求更改作者姓名,使用 AuthorManager.ChangeNameAsync (领域服务方法) 更改作者姓名

  • 直接更新了BirthDateShortBio, 因为它们没有任何业务规则,可以直接更改这些属性,接受任何值

  • 调用 IAuthorRepository.UpdateAsync 方法以更新数据库上的实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[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);
}

在ABP中,一个方法就是一个工作单元,当工作单元结束时,对实体的更改会自动调用SaveChange(),所以即使不调用await _authorRepository.UpdateAsync(author),也会更新到数据库

DeleteAsync

删除作者

  • DeleteAsync需要声明BookStorePermissions.Authors.Delete权限
1
2
3
4
5
[Authorize(BookStorePermissions.Authors.Delete)]
public async Task DeleteAsync(Guid id)
{
await _authorRepository.DeleteAsync(id);
}

权限定义

Acme.BookStore.Application.Contracts 项目内( Permissions 在文件夹中)的 BookStorePermissions 类,新增以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace Acme.BookStore.Permissions;

public static class BookStorePermissions
{
public const string GroupName = "BookStore";

public static class Books
{
public const string Default = GroupName + ".Books";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}

// *** 新增Authors CLASS ***
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定义权限

1
2
3
4
5
6
7
8
9
// Authors
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 中新增多语言键值:

1
2
3
4
"Permission:Authors": "Author Management",
"Permission:Authors.Create": "Creating new authors",
"Permission:Authors.Edit": "Editing the authors",
"Permission:Authors.Delete": "Deleting the authors"

对象映射

Acme.BookStore.Application 项目中打开 BookStoreApplicationAutoMapperProfile 类,新增以下配置:

1
CreateMap<Author, AuthorDto>();

数据种子

就像之前对书籍所做的那样,最好在数据库中有一些初始作者数据。在首次运行应用程序时会很好,对于自动化测试也非常有用。

Acme.BookStore.Domain 项目 BookStoreDataSeederContributor 中新增以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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;

/// <summary>
/// DI
/// </summary>
/// <param name="bookRepository"></param>
/// <param name="authorRepository"></param>
/// <param name="authorManager"></param>
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.ScienceFiction,
PublishDate = new DateTime(2000, 6, 8),
Price = 19.84f
},
autoSave: true
);

await _bookRepository.InsertAsync(
new Book
{
Name = "盗墓笔记",
Type = BookType.Adventure,
PublishDate = new DateTime(2005, 9, 27),
Price = 42.0f
},
autoSave: true
);
}

if (await _authorRepository.GetCountAsync() <= 0)
{
await _authorRepository.InsertAsync(
await _authorManager.CreateAsync(
"Jonty Wang",
new DateTime(1998, 03, 21),
"the best develop engineer in Singapore"
)
);

await _authorRepository.InsertAsync(
await _authorManager.CreateAsync(
"Taylor Swift",
new DateTime(1989, 03, 11),
"the best singer in the world"
)
);
}
}
}
}

运行.DbMigrator项目生成种子数据

测试

Acme.BookStore.Application.Tests项目创建Authors文件夹,并新增AuthorAppService_Tests测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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 == "Jonty Wang");
result.Items.ShouldContain(author => author.Name == "Taylor Swift");
}

[Fact]
public async Task Should_Get_Filtered_Authors()
{
var result = await _authorAppService.GetListAsync(
new GetAuthorListDto { Filter = "Jonty" });

result.TotalCount.ShouldBeGreaterThanOrEqualTo(1);
result.Items.ShouldContain(author => author.Name == "Jonty Wang");
result.Items.ShouldNotContain(author => author.Name == "Taylor Swift");
}

[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 = "Taylor Swift",
BirthDate = DateTime.Now,
ShortBio = "..."
}
);
});
}
}

Acme.BookStore.EntityFrameworkCore.Tests项目EntityFrameworkCore/Applications中创建Authors文件夹,并创建EfCoreAuthorAppService_Tests类实现AuthorAppService_Tests

1
2
3
4
5
6
7
8
9
10
using Acme.BookStore.Authors;
using Xunit;

namespace Acme.BookStore.EntityFrameworkCore.Applications.Authors;

[Collection(BookStoreTestConsts.CollectionDefinitionName)]
public class EfCoreAuthorAppService_Tests : AuthorAppService_Tests<BookStoreEntityFrameworkCoreTestModule>
{

}

run test

下一节

教程下一节