- 关于本教程
- 下载源代码
- 介绍
- 向书实体添加关系
- 数据库和数据迁移
- 更新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
介绍我们为书店应用程序创造Book
和Author
的功能。但是,当前这些实体之间没有任何关系。
在本教程中,我们将在Book
和Author
之间建立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框架无关,因此我们不会对所有情况进行更深入的介绍。
打开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
表的外键。
由于AuthorId
是Book
实体的必需属性,因此当前的数据播种代码无法正常工作。在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
中的新方法中。
打开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
以供作者查询。 -
覆盖基本
CrudAppService
的GetAsync
方法,该方法返回具有给定id
的单个BookDto
对象。- 使用简单的LINQ表达式将书和作者连接起来,并一起查询给定的书号。
- 使用
AsyncExecuter.FirstOrDefaultAsync(...)
执行查询并获得结果。以前在AuthorAppService
中使用过AsyncExecuter
。查看存储库文档以了解我们为什么使用它。 - 如果数据库中不存在请求的书,则抛出
EntityNotFoundException
异常,结果为HTTP 404(未找到)。 - 最后,使用
ObjectMapper
创建一个BookDto
对象,然后手动分配AuthorName
。
-
覆盖基本
CrudAppService
的GetListAsync
方法,该方法返回图书列表。该方法的逻辑与前面的方法相似,因此您可以很容易地理解代码。 -
创建了一个新方法:
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
并在name
和type
列之间添加以下列定义:
...
{
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();
您可以运行该应用程序并尝试创建新书或更新现有书。您将在创建/更新表单上看到一个下拉列表,以选择该书的作者: