项目

本文档有多个版本。请选择最适合您的选项。

UI
Database

Web 应用开发教程 - 第 10 部分:Book 到 Author 的关系

简介

我们已经为书店应用程序创建了 BookAuthor 功能。然而,目前这些实体之间没有关联。

在本教程中,我们将在 AuthorBook 实体之间建立 1 对 N 关系。

为 Book 实体添加关系

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

public Guid AuthorId { get; set; }

在本教程中,我们倾向于不向 Book 类添加指向 Author 实体的导航属性(例如 public Author Author { get; set; })。这是为了遵循 DDD 最佳实践(规则:仅通过 id 引用其他聚合)。不过,你可以添加这样的导航属性并为 EF Core 配置它。这样做的好处是,在获取书籍及其作者时(如下所示),你不需要编写连接查询,这会使你的应用代码更简单。

数据库与数据迁移

Book 实体添加了一个新的、必需的 AuthorId 属性。但是,数据库中的现有书籍怎么办?它们目前没有 AuthorId,当我们尝试运行应用程序时,这将是一个问题。

这是一个典型的迁移问题,具体决策取决于你的情况:

  • 如果你尚未将应用程序发布到生产环境,你可以直接删除数据库中的现有书籍,甚至可以在开发环境中删除整个数据库。
  • 你可以在数据迁移或种子阶段以编程方式更新现有数据。
  • 你可以在数据库上手动处理。

我们倾向于删除数据库 (你可以在 Package Manager Console 中运行 Drop-Database 命令),因为这只是一个示例项目,数据丢失并不重要。由于这个主题与 ABP 无关,我们不会深入探讨所有场景。

更新 EF Core 映射

定位到 Acme.BookStore.EntityFrameworkCore 项目的 EntityFrameworkCore 文件夹下的 BookStoreDbContext 类中的 OnModelCreating 方法,并按如下所示修改 builder.Entity<Book> 部分:

builder.Entity<Book>(b =>
{
    b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema);
    b.ConfigureByConvention(); //自动配置基类属性
    b.Property(x => x.Name).IsRequired().HasMaxLength(128);

    // 添加关系的映射
    b.HasOne<Author>().WithMany().HasForeignKey(x => x.AuthorId).IsRequired();
});

添加新的 EF Core 迁移

启动解决方案配置为使用 Entity Framework Core Code First Migrations。由于我们已经更改了数据库映射配置,我们应该创建一个新的迁移并将更改应用到数据库。

Acme.BookStore.EntityFrameworkCore 项目目录下打开命令行终端,并键入以下命令:

dotnet ef migrations add Added_AuthorId_To_Book

这应该会创建一个新的迁移类,其 Up 方法中包含以下代码:

migrationBuilder.AddColumn<Guid>(
    name: "AuthorId",
    table: "AppBooks",
    type: "uniqueidentifier",
    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 表的外键。

如果你使用 Visual Studio,你可能希望在 Package Manager Console (PMC) 中使用 Add-Migration Added_AuthorId_To_Book -c BookStoreDbContextUpdate-Database -Context BookStoreDbContext 命令。在这种情况下,请确保 Acme.BookStore.Web 是启动项目,并且 Acme.BookStore.EntityFrameworkCore 是 PMC 中的 Default Project

修改数据种子生成器

由于 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 创作了文学评论和诗歌、小说和论战新闻;最著名的作品是寓言中篇小说《动物农场》(1945)和反乌托邦小说《一九八四》(1949)。"
            )
        );

        var douglas = await _authorRepository.InsertAsync(
            await _authorManager.CreateAsync(
                "Douglas Adams",
                new DateTime(1952, 03, 11),
                "Douglas Adams 是一位英国作家、编剧、散文家、幽默作家、讽刺作家和剧作家。Adams 倡导环保和保护,热爱跑车、技术创新和苹果 Macintosh,并自称是'激进的无神论者'。"
            )
        );

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

        await _bookRepository.InsertAsync(
            new Book
            {
                AuthorId = douglas.Id, // 设置作者
                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 之前,请删除现有书籍或删除数据库。有关更多信息,请参阅上面的 数据库与数据迁移 部分。

现在,你可以运行 .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< //定义 CRUD 方法
        BookDto, //用于展示书籍
        Guid, //书籍实体的主键
        PagedAndSortedResultRequestDto, //用于分页/排序
        CreateUpdateBookDto> //用于创建/更新书籍
{
    // 添加新方法
    Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync();
}

这个新方法将被 UI 用来获取作者列表,并填充下拉列表以选择书籍的作者。

BookAppService

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
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, //Book 实体
        BookDto, //用于展示书籍
        Guid, //书籍实体的主键
        PagedAndSortedResultRequestDto, //用于分页/排序
        CreateUpdateBookDto>, //用于创建/更新书籍
    IBookAppService //实现 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.Delete;
    }

    public override async Task<BookDto> GetAsync(Guid id)
    {
        //从仓储获取 IQueryable<Book>
        var queryable = await Repository.GetQueryableAsync();

        //准备一个连接 books 和 authors 的查询
        var query = from book in queryable
            join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id
            where book.Id == id
            select new { book, 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)
    {
        //从仓储获取 IQueryable<Book>
        var queryable = await Repository.GetQueryableAsync();

        //准备一个连接 books 和 authors 的查询
        var query = from book in queryable
            join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id
            select new {book, author};

        //分页
        query = query
            .OrderBy(NormalizeSorting(input.Sorting))
            .Skip(input.SkipCount)
            .Take(input.MaxResultCount);

        //执行查询并获取列表
        var queryResult = await AsyncExecuter.ToListAsync(query);

        //将查询结果转换为 BookDto 对象列表
        var bookDtos = queryResult.Select(x =>
        {
            var bookDto = ObjectMapper.Map<Book, BookDto>(x.book);
            bookDto.AuthorName = x.author.Name;
            return bookDto;
        }).ToList();

        //用另一个查询获取总数量
        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)
        );
    }

    private static string NormalizeSorting(string sorting)
    {
        if (sorting.IsNullOrEmpty())
        {
            return $"book.{nameof(Book.Name)}";
        }

        if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase))
        {
            return sorting.Replace(
                "authorName",
                "author.Name",
                StringComparison.OrdinalIgnoreCase
            );
        }

        return $"book.{sorting}";
    }
}

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

  • 添加了 [Authorize(BookStorePermissions.Books.Default)] 以授权我们新添加/覆盖的方法(记住,当为类声明授权属性时,它对类的所有方法都有效)。
  • 注入了 IAuthorRepository 以便从作者表中查询。
  • 覆盖了基类 CrudAppServiceGetAsync 方法,该方法返回具有给定 id 的单个 BookDto 对象。
    • 使用了一个简单的 LINQ 表达式来连接 books 和 authors,并为给定的书籍 id 一起查询它们。
    • 使用 AsyncExecuter.FirstOrDefaultAsync(...) 来执行查询并获取结果。这是一种在不依赖数据库提供程序 API 的情况下使用异步 LINQ 扩展的方法。请查阅 仓储文档 以了解我们为什么使用它。
    • 如果请求的书籍不在数据库中,则抛出 EntityNotFoundException,这会导致 HTTP 404(未找到)结果。
    • 最后,使用 ObjectMapper 创建了一个 BookDto 对象,然后手动分配了 AuthorName
  • 覆盖了基类 CrudAppServiceGetListAsync 方法,该方法返回书籍列表。逻辑与上一个方法类似,因此你可以轻松理解代码。
  • 创建了一个新方法:GetAuthorLookupAsync。这个方法简单地获取所有作者。UI 使用此方法填充下拉列表,并在创建/编辑书籍时选择作者。

对象到对象映射配置

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

[Mapper]
public partial class AuthorToAuthorLookupDtoMapper : MapperBase<Author, AuthorLookupDto>
{
    public override partial AuthorLookupDto Map(Author source);

    public override partial void Map(Author source, AuthorLookupDto destination);
}

单元测试

由于我们对 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.Modularity;
using Volo.Abp.Validation;
using Xunit;

namespace Acme.BookStore.Books;

public abstract class BookAppService_Tests<TStartupModule> : BookStoreApplicationTestBase<TStartupModule>
    where TStartupModule : IAbpModule
{
    private readonly IBookAppService _bookAppService;
    private readonly IAuthorAppService _authorAppService;

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

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

        //断言
        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();

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

        //断言
        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 列之间添加一个 authorName 列:

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

// 添加了新的作者名列
{
    title: l('Author'),
    data: "authorName"
},

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

当你运行应用程序时,你可以在表格中看到 Author 列:

bookstore-added-author-to-book-list

创建模态框

打开 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; } = string.Empty;

        [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 类内部使用与 UI 相关的 [SelectItems(nameof(Authors))][DisplayName("Author")] 属性。
  • 添加了 Authors 属性,该属性在 OnGetAsync 方法内部使用之前定义的 IBookAppService.GetAuthorLookupAsync 方法进行填充。
  • 更改了 OnPostAsync 方法,将 CreateBookViewModel 对象映射到 CreateUpdateBookDto 对象,因为 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; } = string.Empty;

        [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 属性,该属性在 OnGetAsync 方法内部使用 IBookAppService.GetAuthorLookupAsync 方法进行填充。
  • 更改了 OnPostAsync 方法,将 EditBookViewModel 对象映射到 CreateUpdateBookDto 对象,因为 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 项目中的 BookStoreWebMappers.cs,并创建以下映射定义:

using Riok.Mapperly.Abstractions;
using Volo.Abp.Mapperly;

//...

[Mapper]
public partial class CreateBookViewModelToCreateUpdateBookDtoMapper : MapperBase<Pages.Books.CreateModalModel.CreateBookViewModel, CreateUpdateBookDto>
{
    public override partial CreateUpdateBookDto Map(Pages.Books.CreateModalModel.CreateBookViewModel source);

    public override partial void Map(Pages.Books.CreateModalModel.CreateBookViewModel source, CreateUpdateBookDto destination);
}

[Mapper]
public partial class BookDtoToEditBookViewModelMapper : MapperBase<BookDto, Pages.Books.EditModalModel.EditBookViewModel>
{
    public override partial Pages.Books.EditModalModel.EditBookViewModel Map(BookDto source);

    public override partial void Map(BookDto source, Pages.Books.EditModalModel.EditBookViewModel destination);
}

[Mapper]
public partial class EditBookViewModelToCreateUpdateBookDtoMapper : MapperBase<Pages.Books.EditModalModel.EditBookViewModel, CreateUpdateBookDto>
{
    public override partial CreateUpdateBookDto Map(Pages.Books.EditModalModel.EditBookViewModel source);

    public override partial void Map(Pages.Books.EditModalModel.EditBookViewModel source, CreateUpdateBookDto destination);
}

你可以运行应用程序并尝试创建新书或更新现有书籍。你将在创建/更新表单上看到一个下拉列表来选择书籍的作者:

bookstore-added-authors-to-modals


在本文档中