Abp vnext Web应用程序开发教程 8 —— 作者:应用程序层

关于本教程

本教程基于版本3.1

在本教程系列中,您将构建一个名为Acme.BookStore的基于ABPWeb应用程序。该应用程序用于管理书籍及其作者的列表。它是使用以下技术开发的:

  • 实体框架核心作为ORM提供者。
  • MVC/Razor页面作为UI框架。

本教程分为以下部分:

第1部分:创建服务器端

第2部分:书籍列表页面

第3部分:创建、更新和删除书籍

第4部分:集成测试

第5部分:授权

第6部分:作者:领域层

第7部分:作者:数据库集成

第8部分:作者:应用程序层(此部分)

第9部分:作者:用户界面

第10部分:书与作者的关系

下载源代码

MVC (Razor Pages) UI with EF Core

介绍

本部分说明为之前创建的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 MaxResultCountint SkipCountstring Sorting

ABP框架具有这些基本的DTO类,以简化和标准化您的DTO。有关所有信息,请参见[DTO文档](EntityDto simply has an Id property with the given generic argument.)。

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;

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

  • 注入IAuthorRepositoryAuthorManager以用于服务方法。

现在,我们将逐一介绍服务方法。将解释的方法复制到AuthorAppService类中。

GetAsync

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

这个方法简单地通过它的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 = await AsyncExecuter.CountAsync(
        _authorRepository.WhereIf(
            !input.Filter.IsNullOrWhiteSpace(),
            author => author.Name.Contains(input.Filter)
        )
    );

    return new PagedResultDto<AuthorDto>(
        totalCount,
        ObjectMapper.Map<List<Author>, List<AuthorDto>>(authors)
    );
}
  • 默认排序是“按作者名称”,如果客户端未发送,则在方法的开头进行排序。

  • 使用IAuthorRepository.GetListAsync从数据库中获取作者的分页,排序和筛选列表。我们已经在本教程的前一部分中实现了它。同样,实际上并不需要创建这样的方法,因为我们可以直接查询存储库,但想演示如何创建自定义存储库方法。

  • 直接从AuthorRepository查询,同时获得作者的数量。我们更喜欢使用允许我们执行异步查询而无需依赖EF Core的AsyncExecuter服务。但是,您可以依赖EF Core软件包并直接使用_authorRepository.WhereIf(...).ToListAsync()方法。请参阅存储库文档以阅读替代方法和讨论。

  • 最后,通过将Authors列表映射到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以外)。

  • 使用AuthorManeger(领域服务)创建新作者。

  • 使用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(域服务方法)更改作者姓名。

  • 直接更新BirthDateShortBio,由于没有任何业务规则可更改这些属性,因此它们接受任何值。

  • 最后,调用了更新数据库上实体的IAuthorRepository.UpdateAsync方法。

EF Core技巧:Entity Framework Core具有变更跟踪系统,并在工作单元结束时自动将所有更改保存到实体(您可以简单地认为ABP Framework 在方法结束时会自动调用SaveChanges)。因此,即使您未在方法末尾调用_authorRepository.UpdateAsync(...),它也将按预期工作。如果您不考虑以后再更改EF Core,则只需删除此行。

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正在使用ObjectMapperAuthor对象转换为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'."
                    )
                );
            }
        }
    }
}

现在,您可以运行.DbMigrator控制台应用程序迁移数据库架构播种的初始数据。

测试作者应用服务

最后,我们可以为IAuthorAppService编写一些测试。在Acme.BookStore.Application.Tests项目的Authors命名空间(文件夹)中添加一个命名为AuthorAppService_Tests新的类:

using System;
using System.Threading.Tasks;
using Shouldly;
using Xunit;

namespace Acme.BookStore.Authors
{ 
    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...
    }
}

为应用程序服务方法创建了一些测试,这些测试应该很容易理解。

下一部分

请参阅本教程的下一部分

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值