Abp vnext Web应用程序开发教程 10 —— 书与作者的关系

关于本教程

本教程基于版本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

介绍

我们为书店应用程序创造BookAuthor的功能。但是,当前这些实体之间没有任何关系。

在本教程中,我们将在BookAuthor之间建立1到N的关系。

向书实体添加关系

Acme.BookStore.Domain项目中打开Books/Book.cs,然后将以下属性添加到Book实体:

public Guid AuthorId { get; set; }

在本教程中,我们希望不向Author实体(例如public Author Author { get; set; })添加导航属性。这是由于遵循DDD最佳做法(规则:仅通过ID引用其他聚合)。但是,您可以添加这样的导航属性,并为EF Core配置它。这样,您无需在获取带有实体的书时就编写join查询(就像我们将在下面进行的操作一样),这使您的应用程序代码更简单。

数据库和数据迁移

Book实体添加了新的必需的AuthorId属性。但是,数据库中现有的书呢?它们当前没有AuthorId,这在我们尝试运行该应用程序时会出现问题。

这是一个典型的迁移问题,具体决定取决于您的情况。

  • 如果尚未将应用程序发布到产品中,则可以删除数据库中的现有书籍,甚至可以删除开发环境中的整个数据库。

  • 您可以在数据迁移或播种阶段以编程方式进行操作。

  • 您可以在数据库上手动处理它。

我们更喜欢删除数据库(在Package Manager Console中运行Drop-Database),因为这只是一个示例项目,数据丢失并不重要。由于本主题与ABP框架无关,因此我们不会对所有情况进行更深入的介绍。

更新EF核心映射

打开Acme.BookStore.EntityFrameworkCore项目EntityFrameworkCore文件夹下的BookStoreDbContextModelCreatingExtensions类,并更改builder.Entity<Book>部分,如下所示:

builder.Entity<Book>(b =>
{
    b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema);
    b.ConfigureByConvention(); //auto configure for the base class props
    b.Property(x => x.Name).IsRequired().HasMaxLength(128);
    
    // ADD THE MAPPING FOR THE RELATION
    b.HasOne<Author>().WithMany().HasForeignKey(x => x.AuthorId).IsRequired();
});

添加新的EF核心迁移

在(Visual Studio的)包管理器控制台中运行以下命令以添加新的数据库迁移:

Add-Migration "Added_AuthorId_To_Book"

这应该在其Up方法中使用以下代码创建一个新的迁移类:

migrationBuilder.AddColumn<Guid>(
    name: "AuthorId",
    table: "AppBooks",
    nullable: false,
    defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));

migrationBuilder.CreateIndex(
    name: "IX_AppBooks_AuthorId",
    table: "AppBooks",
    column: "AuthorId");

migrationBuilder.AddForeignKey(
    name: "FK_AppBooks_AppAuthors_AuthorId",
    table: "AppBooks",
    column: "AuthorId",
    principalTable: "AppAuthors",
    principalColumn: "Id",
    onDelete: ReferentialAction.Cascade);
  • AppBooks表中添加一个AuthorId字段。

  • AuthorId字段上创建索引。

  • 声明AppAuthors表的外键。

更改数据播种器

由于AuthorIdBook实体的必需属性,因此当前的数据播种代码无法正常工作。在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)
            {
                return;
            }

            var orwell = 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)."
                )
            );

            var douglas = 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'."
                )
            );

            await _bookRepository.InsertAsync(
                new Book
                {
                    AuthorId = orwell.Id, // SET THE AUTHOR
                    Name = "1984",
                    Type = BookType.Dystopia,
                    PublishDate = new DateTime(1949, 6, 8),
                    Price = 19.84f
                },
                autoSave: true
            );

            await _bookRepository.InsertAsync(
                new Book
                {
                    AuthorId = douglas.Id, // SET THE AUTHOR
                    Name = "The Hitchhiker's Guide to the Galaxy",
                    Type = BookType.ScienceFiction,
                    PublishDate = new DateTime(1995, 9, 27),
                    Price = 42.0f
                },
                autoSave: true
            );
        }
    }
}

唯一的变化是我们设置了Book实体的AuthorId属性。

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

应用层

我们将更改BookAppService以支持作者关系。

数据传输对象

让我们从DTO开始。

BookDto

Acme.BookStore.Application.Contracts项目的Books文件夹中打开BookDto类,然后添加以下属性:

public Guid AuthorId { get; set; }
public string AuthorName { get; set; }

最后BookDto类应如下:

using System;
using Volo.Abp.Application.Dtos;

namespace Acme.BookStore.Books
{
    public class BookDto : AuditedEntityDto<Guid>
    {
        public Guid AuthorId { get; set; }

        public string AuthorName { get; set; }

        public string Name { get; set; }

        public BookType Type { get; set; }

        public DateTime PublishDate { get; set; }

        public float Price { get; set; }
    }
}

CreateUpdateBookDto

打开Acme.BookStore.Application.Contracts项目Books文件夹中的CreateUpdateBookDto类,并添加一个AuthorId属性,如下所示:

public Guid AuthorId { get; set; }

AuthorLookupDto

Acme.BookStore.Application.Contracts项目的Books文件夹中创建一个新类AuthorLookupDto

using System;
using Volo.Abp.Application.Dtos;

namespace Acme.BookStore.Books
{
    public class AuthorLookupDto : EntityDto<Guid>
    {
        public string Name { get; set; }
    }
}

这将用在将添加到IBookAppService中的新方法中。

IBookAppService

打开Acme.BookStore.Application.Contracts项目Books文件夹中的IBookAppService接口,并添加一个名为GetAuthorLookupAsync的新方法,如下所示:

using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;

namespace Acme.BookStore.Books
{
    public interface IBookAppService :
        ICrudAppService< //Defines CRUD methods
            BookDto, //Used to show books
            Guid, //Primary key of the book entity
            PagedAndSortedResultRequestDto, //Used for paging/sorting
            CreateUpdateBookDto> //Used to create/update a book
    {
        // ADD the NEW METHOD
        Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync();
    }
}

UI中将使用此新方法来获取作者列表,并填充下拉列表以选择书的作者。

BookAppService

打开Acme.BookStore.Application项目Books文件夹中的BookAppService接口,并将文件内容替换为以下代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Acme.BookStore.Authors;
using Acme.BookStore.Permissions;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;

namespace Acme.BookStore.Books
{
    [Authorize(BookStorePermissions.Books.Default)]
    public class BookAppService :
        CrudAppService<
            Book, //The Book entity
            BookDto, //Used to show books
            Guid, //Primary key of the book entity
            PagedAndSortedResultRequestDto, //Used for paging/sorting
            CreateUpdateBookDto>, //Used to create/update a book
        IBookAppService //implement the IBookAppService
    {
        private readonly IAuthorRepository _authorRepository;

        public BookAppService(
            IRepository<Book, Guid> repository,
            IAuthorRepository authorRepository)
            : base(repository)
        {
            _authorRepository = authorRepository;
            GetPolicyName = BookStorePermissions.Books.Default;
            GetListPolicyName = BookStorePermissions.Books.Default;
            CreatePolicyName = BookStorePermissions.Books.Create;
            UpdatePolicyName = BookStorePermissions.Books.Edit;
            DeletePolicyName = BookStorePermissions.Books.Create;
        }

        public override async Task<BookDto> GetAsync(Guid id)
        {
            //Prepare a query to join books and authors
            var query = from book in Repository
                join author in _authorRepository on book.AuthorId equals author.Id
                where book.Id == id
                select new { book, author };

            //Execute the query and get the book with author
            var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query);
            if (queryResult == null)
            {
                throw new EntityNotFoundException(typeof(Book), id);
            }

            var bookDto = ObjectMapper.Map<Book, BookDto>(queryResult.book);
            bookDto.AuthorName = queryResult.author.Name;
            return bookDto;
        }

        public override async Task<PagedResultDto<BookDto>>
            GetListAsync(PagedAndSortedResultRequestDto input)
        {
            //Prepare a query to join books and authors
            var query = from book in Repository
                join author in _authorRepository on book.AuthorId equals author.Id
                orderby input.Sorting
                select new {book, author};

            query = query
                .Skip(input.SkipCount)
                .Take(input.MaxResultCount);

            //Execute the query and get a list
            var queryResult = await AsyncExecuter.ToListAsync(query);

            //Convert the query result to a list of BookDto objects
            var bookDtos = queryResult.Select(x =>
            {
                var bookDto = ObjectMapper.Map<Book, BookDto>(x.book);
                bookDto.AuthorName = x.author.Name;
                return bookDto;
            }).ToList();

            //Get the total count with another query
            var totalCount = await Repository.GetCountAsync();

            return new PagedResultDto<BookDto>(
                totalCount,
                bookDtos
            );
        }

        public async Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync()
        {
            var authors = await _authorRepository.GetListAsync();

            return new ListResultDto<AuthorLookupDto>(
                ObjectMapper.Map<List<Author>, List<AuthorLookupDto>>(authors)
            );
        }
    }
}

让我们看看我们所做的更改:

  • 添加[Authorize(BookStorePermissions.Books.Default)]以授权我们新添加/覆盖的方法(请记住,当为一个类声明该属性时,authorize属性对该类的所有方法均有效)。

  • 注入IAuthorRepository以供作者查询。

  • 覆盖基本CrudAppServiceGetAsync方法,该方法返回具有给定id的单个BookDto对象。

    • 使用简单的LINQ表达式将书和作者连接起来,并一起查询给定的书号。
    • 使用AsyncExecuter.FirstOrDefaultAsync(...)执行查询并获得结果。以前在AuthorAppService中使用过AsyncExecuter。查看存储库文档以了解我们为什么使用它。
    • 如果数据库中不存在请求的书,则抛出EntityNotFoundException异常,结果为HTTP 404(未找到)。
    • 最后,使用ObjectMapper创建一个BookDto对象,然后手动分配AuthorName
  • 覆盖基本CrudAppServiceGetListAsync方法,该方法返回图书列表。该方法的逻辑与前面的方法相似,因此您可以很容易地理解代码。

  • 创建了一个新方法:GetAuthorLookupAsync。这个简单的方法得到了所有的作者。UI使用这个方法在创建/编辑图书时填充下拉列表和选择和作者。

对象到对象的映射配置

介绍了AuthorLookupDto类,并在GetAuthorLookupAsync方法内部使用了对象映射。因此,我们需要在Acme.BookStore.Application项目文件BookStoreApplicationAutoMapperProfile.cs中添加一个新的映射定义:

CreateMap<Author, AuthorLookupDto>();

单元测试

由于我们对AuthorAppService进行了一些更改,因此某些单元测试将失败。在Acme.BookStore.Application.Tests项目的Books文件夹中打开BookAppService_Tests,然后将内容更改为以下内容:

using System;
using System.Linq;
using System.Threading.Tasks;
using Acme.BookStore.Authors;
using Shouldly;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Validation;
using Xunit;

namespace Acme.BookStore.Books
{ 
    public class BookAppService_Tests : BookStoreApplicationTestBase
    {
        private readonly IBookAppService _bookAppService;
        private readonly IAuthorAppService _authorAppService;

        public BookAppService_Tests()
        {
            _bookAppService = GetRequiredService<IBookAppService>();
            _authorAppService = GetRequiredService<IAuthorAppService>();
        }

        [Fact]
        public async Task Should_Get_List_Of_Books()
        {
            //Act
            var result = await _bookAppService.GetListAsync(
                new PagedAndSortedResultRequestDto()
            );

            //Assert
            result.TotalCount.ShouldBeGreaterThan(0);
            result.Items.ShouldContain(b => b.Name == "1984" &&
                                       b.AuthorName == "George Orwell");
        }
        
        [Fact]
        public async Task Should_Create_A_Valid_Book()
        {
            var authors = await _authorAppService.GetListAsync(new GetAuthorListDto());
            var firstAuthor = authors.Items.First();

            //Act
            var result = await _bookAppService.CreateAsync(
                new CreateUpdateBookDto
                {
                    AuthorId = firstAuthor.Id,
                    Name = "New test book 42",
                    Price = 10,
                    PublishDate = System.DateTime.Now,
                    Type = BookType.ScienceFiction
                }
            );

            //Assert
            result.Id.ShouldNotBe(Guid.Empty);
            result.Name.ShouldBe("New test book 42");
        }
        
        [Fact]
        public async Task Should_Not_Create_A_Book_Without_Name()
        {
            var exception = await Assert.ThrowsAsync<AbpValidationException>(async () =>
            {
                await _bookAppService.CreateAsync(
                    new CreateUpdateBookDto
                    {
                        Name = "",
                        Price = 10,
                        PublishDate = DateTime.Now,
                        Type = BookType.ScienceFiction
                    }
                );
            });

            exception.ValidationErrors
                .ShouldContain(err => err.MemberNames.Any(m => m == "Name"));
        }
    }
}
  • Should_Get_List_Of_Books中的断言条件从b => b.Name == "1984"更改为b => b.Name == "1984" && b.AuthorName == "George Orwell",以检查作者姓名是否已填写。

  • 更改Should_Create_A_Valid_Book方法,以在创建新图书时设置AuthorId,因为它已经是必需的了。

用户界面

书籍列表

图书列表页面的更改是微不足道的。打开Acme.BookStore.Web项目中的Pages/Books/Index.js并在nametype列之间添加以下列定义:

...
{
    title: l('Name'),
    data: "name"
},

// ADDED the NEW AUTHOR NAME COLUMN
{
    title: l('Author'),
    data: "authorName"
},

{
    title: l('Type'),
    data: "type",
    render: function (data) {
        return l('Enum:BookType:' + data);
    }
},
...

运行应用程序时,您可以在表上看到“作者(Author)”列:
在这里插入图片描述

创建模态

Acme.BookStore.Web项目中打开Pages/Books/CreateModal.cshtml.cs,然后更改文件内容,如下所示:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Acme.BookStore.Books;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;

namespace Acme.BookStore.Web.Pages.Books
{
    public class CreateModalModel : BookStorePageModel
    {
        [BindProperty]
        public CreateBookViewModel Book { get; set; }

        public List<SelectListItem> Authors { get; set; }

        private readonly IBookAppService _bookAppService;

        public CreateModalModel(
            IBookAppService bookAppService)
        {
            _bookAppService = bookAppService;
        }

        public async Task OnGetAsync()
        {
            Book = new CreateBookViewModel();

            var authorLookup = await _bookAppService.GetAuthorLookupAsync();
            Authors = authorLookup.Items
                .Select(x => new SelectListItem(x.Name, x.Id.ToString()))
                .ToList();
        }

        public async Task<IActionResult> OnPostAsync()
        {
            await _bookAppService.CreateAsync(
                ObjectMapper.Map<CreateBookViewModel, CreateUpdateBookDto>(Book)
                );
            return NoContent();
        }

        public class CreateBookViewModel
        {
            [SelectItems(nameof(Authors))]
            [DisplayName("Author")]
            public Guid AuthorId { get; set; }

            [Required]
            [StringLength(128)]
            public string Name { get; set; }

            [Required]
            public BookType Type { get; set; } = BookType.Undefined;

            [Required]
            [DataType(DataType.Date)]
            public DateTime PublishDate { get; set; } = DateTime.Now;

            [Required]
            public float Price { get; set; }
        }
    }
}
  • Book属性的类型从CreateUpdateBookDto更改为此文件中定义的新CreateBookViewModel类。此更改的主要动机是根据用户界面(UI)要求自定义模型类。我们不希望在CreateUpdateBookDto类内使用的用户界面相关的[SelectItems(nameof(Authors))][DisplayName("Author")]内部属性。

  • 添加的Authors属性,该属性使用前面定义的IBookAppService.GetAuthorLookupAsync方法填充在OnGetAsync方法中。

  • 更改了将CreateBookViewModel对象映射到CreateUpdateBookDto对象的OnPostAsync方法,因为IBookAppService.CreateAsync需要此类型的参数。

编辑模态

Acme.BookStore.Web项目中打开Pages/Books/EditModal.cshtml.cs,然后更改文件内容,如下所示:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Acme.BookStore.Books;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;

namespace Acme.BookStore.Web.Pages.Books
{
    public class EditModalModel : BookStorePageModel
    {
        [BindProperty]
        public EditBookViewModel Book { get; set; }

        public List<SelectListItem> Authors { get; set; }

        private readonly IBookAppService _bookAppService;

        public EditModalModel(IBookAppService bookAppService)
        {
            _bookAppService = bookAppService;
        }

        public async Task OnGetAsync(Guid id)
        {
            var bookDto = await _bookAppService.GetAsync(id);
            Book = ObjectMapper.Map<BookDto, EditBookViewModel>(bookDto);

            var authorLookup = await _bookAppService.GetAuthorLookupAsync();
            Authors = authorLookup.Items
                .Select(x => new SelectListItem(x.Name, x.Id.ToString()))
                .ToList();
        }

        public async Task<IActionResult> OnPostAsync()
        {
            await _bookAppService.UpdateAsync(
                Book.Id,
                ObjectMapper.Map<EditBookViewModel, CreateUpdateBookDto>(Book)
            );

            return NoContent();
        }

        public class EditBookViewModel
        {
            [HiddenInput]
            public Guid Id { get; set; }

            [SelectItems(nameof(Authors))]
            [DisplayName("Author")]
            public Guid AuthorId { get; set; }

            [Required]
            [StringLength(128)]
            public string Name { get; set; }

            [Required]
            public BookType Type { get; set; } = BookType.Undefined;

            [Required]
            [DataType(DataType.Date)]
            public DateTime PublishDate { get; set; } = DateTime.Now;

            [Required]
            public float Price { get; set; }
        }
    }
}
  • Book属性的类型从CreateUpdateBookDto更改为此文件中定义的新EditBookViewModel类,就像之前对上面的创建模态所做的一样。

  • Id属性移到新EditBookViewModel类中。

  • 添加的Authors属性,该属性使用IBookAppService.GetAuthorLookupAsync方法在OnGetAsync方法中填充。

  • 更改了将EditBookViewModel对象映射到CreateUpdateBookDto对象的OnPostAsync方法,因为IBookAppService.UpdateAsync需要此类型的参数。

这些更改需要在EditModal.cshtml中进行小小的更改。删除<abp-input asp-for="Id" />标签,因为我们不再需要它了(因为将其移至EditBookViewModel)。EditModal.cshtml的最终内容应为:

@page
@using Acme.BookStore.Localization
@using Acme.BookStore.Web.Pages.Books
@using Microsoft.Extensions.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model EditModalModel
@inject IStringLocalizer<BookStoreResource> L
@{
    Layout = null;
}
<abp-dynamic-form abp-model="Book" asp-page="/Books/EditModal">
    <abp-modal>
        <abp-modal-header title="@L["Update"].Value"></abp-modal-header>
        <abp-modal-body>
            <abp-form-content />
        </abp-modal-body>
        <abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
    </abp-modal>
</abp-dynamic-form>

对象到对象的映射配置

上面的更改要求定义一些对象到对象的映射。在Acme.BookStore.Web项目中打开BookStoreWebAutoMapperProfile.cs,然后在构造函数中添加以下映射定义:

CreateMap<Pages.Books.CreateModalModel.CreateBookViewModel, CreateUpdateBookDto>();
CreateMap<BookDto, Pages.Books.EditModalModel.EditBookViewModel>();
CreateMap<Pages.Books.EditModalModel.EditBookViewModel, CreateUpdateBookDto>();

您可以运行该应用程序并尝试创建新书或更新现有书。您将在创建/更新表单上看到一个下拉列表,以选择该书的作者:
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值