在系列教程中,我们会构建一个基于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 MaxResultCount
和 int 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
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
注入 IAuthorRepository
和AuthorManager
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
(领域服务方法) 更改作者姓名
直接更新了BirthDate
和 ShortBio
, 因为它们没有任何业务规则,可以直接更改这些属性,接受任何值
调用 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" ; } 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 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; 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 >{ }
下一节 教程下一节