Web应用开发教程 - Part 8: 作家:应用层
Web应用开发教程 - Part 8: 作家:应用层
//[doc-params]
{
"UI": ["MVC","Blazor","BlazorServer","NG"],
"DB": ["EF","Mongo"]
}
关于本教程
在这个教程系列中,你将创建一个基于ABP的Web应用程序,叫 Acme.BookStore
。这个应用程序是用来管理书籍及其作者的列表的。它是使用以下技术开发:
- {{DB_Value}} 作为ORM的提供者。
- {{UI_Value}} 作为UI框架。
本教程由以下几个部分组成:
- 第一部分:创建服务器端
- 第2部分:书籍列表页
- 第3部分:创建、更新和删除书籍
- 第四部分:集成测试
- 第5部分:授权
- 第六部分:作家:领域层
- 第七部分:作家:数据库集成
- 第8部分:作家:应用层 (本部分)
- 第九部分:作家:用户界面
- 第10部分:关联书籍与作家
下载源代码
本教程根据你对UI和Database的偏好有多个版本。我们准备了几个组合的源代码供大家下载:
如果你在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
简介
This part explains to create an application layer for the Author
entity created before.
这一部分讲解如何为之前创建的Author
实体创建应用层。
IAuthorAppService
We will first create the application service interface and the related DTOs. Create a new interface, named IAuthorAppService
, in the Authors
namespace (folder) of the Acme.BookStore.Application.Contracts
project:
我们将首先创建应用服务接口和相关的DTOs。在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`。
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; }
[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; }
[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);
}
This method simply gets the Author
entity by its Id
, converts to the AuthorDto
using the object to object mapper. This requires to configure the AutoMapper, which will be explained later.
这个方法只是通过Id
获得Author
实体,使用对象间映射转换为AuthorDto
。这需要配置AutoMapper,这将在后面解释。
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应用程序中会产生404
HTTP状态代码。在更新实体操作中总是先获取这个实体是一个好的做法。 - 如果在客户端请求改变作家的名字时,使用
AuthorManager.ChangeNameAsync
(领域服务方法)进行修改。 - 直接更新
BirthDate
和ShortBio
,因为改变这些属性时没有任何业务规则,它们接受任何值。 - 最后,调用
IAuthorRepository.UpdateAsync
方法来更新数据库中的实体。
EF Core提示:Entity Framework Core有一个变化跟踪系统,并在工作单元结束时自动保存实体的任何变化(你可以简单地认为ABP框架在方法结束时自动调用
SaveChanges
)。所以,即使你不在方法结束时调用_authorRepository.UpdateAsync(...)
,它也会按预期工作。如果你不考虑以后替换EF Core,你可以直接删除这一行。
{{end}}
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 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";
}
// *** ADDED a NEW NESTED 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",在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": "Author Management",
"Permission:Authors.Create": "Creating new authors",
"Permission:Authors.Edit": "Editing the authors",
"Permission:Authors.Delete": "Deleting the authors"
对象间映射
AuthorAppService
正在使用ObjectMapper
将Author
对象转换成AuthorDto
对象。因此,我们需要在AutoMapper配置中定义这个映射。
打开Acme.BookStore.Application
项目中的BookStoreApplicationAutoMapperProfile
类,并在构造函数中添加下面一行代码:
CreateMap<Author, AuthorDto>();
数据初始化
正如之前为书籍所做的那样,在数据库中拥有一些初始的作者实体将是很好的。这在第一次运行应用程序时是很好的,而且对自动测试也很有用。
打开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
);
}
// ADDED SEED DATA FOR AUTHORS
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'."
)
);
}
}
}
}
{{if DB=="EF"}}
现在你可以运行.DbMigrator
控制台应用程序来迁移数据库结构和植入初始化数据。
{{else if DB=="Mongo"}}
You can now run the .DbMigrator
console application to seed the initial data.
现在你可以运行.DbMigrator
控制台应用程序来初始化数据。
{{end}}
作家应用服务测试
最后,我们可以为IAuthorAppService
写一些测试。在Acme.BookStore.Application.Tests
项目的Authors
命名空间(文件夹)中添加一个新的类,命名为 AuthorAppService_Tests
。
using System;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
namespace Acme.BookStore.Authors
{ {{if DB=="Mongo"}}
[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}}
public class AuthorAppService_Tests : BookStoreApplicationTestBase
{
private readonly IAuthorAppService _authorAppService;
public 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: Test other methods...
}
}
为应用服务方法创建了一些简单容易理解的测试。
下一篇
参见本教程的下一篇。
本文来自博客园,作者:草叶睡蜢,转载请注明原文链接:https://www.cnblogs.com/tjubuntu/p/15722257.html