您当前的位置: 首页 > 

寒冰屋

暂无认证

  • 0浏览

    0关注

    2286博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

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

寒冰屋 发布时间:2020-09-26 21:23:31 ,浏览量:0

文章目录
  • 关于本教程
    • 下载源代码
  • 介绍
  • 向书实体添加关系
  • 数据库和数据迁移
    • 更新EF核心映射
    • 添加新的EF核心迁移
  • 更改数据播种器
  • 应用层
    • 数据传输对象
    • IBookAppService
    • BookAppService
    • 对象到对象的映射配置
  • 单元测试
  • 用户界面
    • 书籍列表
    • 创建模态
    • 编辑模态
    • 对象到对象的映射配置

关于本教程

本教程基于版本3.1

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

  • 实体框架核心作为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部分,如下所示:

builder.Entity(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().WithMany().HasForeignKey(x => x.AuthorId).IsRequired();
});
添加新的EF核心迁移

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

Add-Migration "Added_AuthorId_To_Book"

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

migrationBuilder.AddColumn(
    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 _bookRepository;
        private readonly IAuthorRepository _authorRepository;
        private readonly AuthorManager _authorManager;

        public BookStoreDataSeederContributor(
            IRepository 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
    {
        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
    {
        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 //Used to create/update a book
    {
        // ADD the NEW METHOD
        Task 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, //Used to create/update a book
        IBookAppService //implement the IBookAppService
    {
        private readonly IAuthorRepository _authorRepository;

        public BookAppService(
            IRepository 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 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(queryResult.book);
            bookDto.AuthorName = queryResult.author.Name;
            return bookDto;
        }

        public override async Task
            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(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(
                totalCount,
                bookDtos
            );
        }

        public async Task GetAuthorLookupAsync()
        {
            var authors = await _authorRepository.GetListAsync();

            return new ListResultDto(
                ObjectMapper.Map(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();
单元测试

由于我们对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();
            _authorAppService = GetRequiredService();
        }

        [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(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 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 OnPostAsync()
        {
            await _bookAppService.CreateAsync(
                ObjectMapper.Map(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 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);

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

        public async Task OnPostAsync()
        {
            await _bookAppService.UpdateAsync(
                Book.Id,
                ObjectMapper.Map(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中进行小小的更改。删除标签,因为我们不再需要它了(因为将其移至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 L
@{
    Layout = null;
}

    
        
        
            
        
        
    

对象到对象的映射配置

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

CreateMap();
CreateMap();
CreateMap();

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

关注
打赏
1665926880
查看更多评论
立即登录/注册

微信扫码登录

0.0742s