ASP.NET Core MVC / Razor Pages 教程 - 第一章
关于本教程
在本系列教程中, 你将构建一个名为 Acme.BookStore
的用于管理书籍及其作者列表的应用程序. Entity Framework Core将用作ORM提供者,前端使用MVC / Razor Pages 和 JavaScript.
ASP.NET Core MVC / Razor Pages 系列教程包括三个3个部分:
- Part-1: 创建项目和书籍列表页面(本章)
- Part-2: 创建,编辑,删除书籍
- Part-3: 集成测试
你也可以观看由ABP社区成员为本教程录制的视频课程.
创建新项目
创建一个名为 Acme.BookStore
的新项目,其中 Acme
是公司名 BookStore
是项目名. 你可以参阅入门 文档了解如何创建新项目. 我们将使用CLI创建新项目.
创建项目
使用以下命令创建一个新的ABP项目,使用 Entity Framework Core
做为数据库提供者, UI选项使用 MVC / Razor Pages
. 其他CLI选项请参考ABP CLI文档.
abp new Acme.BookStore --template app --database-provider ef --ui mvc --mobile none
应用迁移
项目创建后,需要应用初始化迁移创建数据库. 运行 Acme.BookStore.DbMigrator
应用程序. 它会应用所有迁移,完成流程后你会看到以下结果,数据库已经准备好了!
另外你也可以在 Visual Studio 包管理控制台运行
Update-Database
命令应用迁移.
初始化数据库表
运行应用程序
右键单击 Acme.BookStore.Web
项目设置为启动项. 使用 CTRL+F5 或 F5 运行应用程序.
更多信息,参阅入门教程的运行应用程序部分.
默认的登录凭证:
- Username: admin
- Password: 1q2w3E*
解决方案的结构
下面的图片展示了从启动模板创建的项目是如何分层的.
你可以查看应用程序模板文档以详细了解解决方案结构.
创建Book实体
启动模板中的域层分为两个项目:
在解决方案的领域层(Acme.BookStore.Domain
项目)中定义实体. 该应用程序的主要实体是Book
. 在Acme.BookStore.Domain
项目中创建一个名为Book
的类,如下所示:
using System;
using Volo.Abp.Domain.Entities.Auditing;
namespace Acme.BookStore
{
public class Book : AuditedAggregateRoot<Guid>
{
public string Name { get; set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
protected Book()
{
}
public Book(Guid id, string name, BookType type, DateTime publishDate, float price)
:base(id)
{
Name = name;
Type = type;
PublishDate = publishDate;
Price = price;
}
}
}
- ABP为实体提供了两个基本的基类:
AggregateRoot
和Entity
. Aggregate Root是域驱动设计(DDD) 概念之一. 有关详细信息和最佳做法,请参阅实体文档. Book
实体继承了AuditedAggregateRoot
,AuditedAggregateRoot
类在AggregateRoot
类的基础上添加了一些审计属性(CreationTime
,CreatorId
,LastModificationTime
等).Guid
是Book
实体的主键类型.- 使用 数据注解 为EF Core添加映射.或者你也可以使用 EF Core 自带的fluent mapping API.
BookType枚举
上面所用到的BookType
枚举定义如下:
namespace Acme.BookStore
{
public enum BookType
{
Undefined,
Adventure,
Biography,
Dystopia,
Fantastic,
Horror,
Science,
ScienceFiction,
Poetry
}
}
将Book实体添加到DbContext中
EF Core需要你将实体和 DbContext
建立关联.最简单的做法是在Acme.BookStore.EntityFrameworkCore
项目的BookStoreDbContext
类中添加DbSet
属性.如下所示:
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
{
public DbSet<Book> Book { get; set; }
...
}
配置你的Book实体
在 Acme.BookStore.EntityFrameworkCore
项目中打开 BookStoreDbContextModelCreatingExtensions.cs
文件,并将以下代码添加到 ConfigureBookStore
方法的末尾以配置Book实体:
builder.Entity<Book>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Book", BookStoreConsts.DbSchema);
b.ConfigureByConvention(); //auto configure for the base class props
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
});
添加 using Volo.Abp.EntityFrameworkCore.Modeling;
以使用 ConfigureByConvention
扩展方法.
添加新的Migration并更新数据库
这个启动模板使用了EF Core Code First Migrations来创建并维护数据库结构.打开 程序包管理器控制台(Package Manager Console) (PMC) (工具/Nuget包管理器菜单)
选择 Acme.BookStore.EntityFrameworkCore.DbMigrations
作为默认的项目然后执行下面的命令:
Add-Migration "Created_Book_Entity"
这样就会在 Migrations
文件夹中创建一个新的migration类.然后执行 Update-Database
命令更新数据库结构:
Update-Database
添加示例数据
Update-Database
命令在数据库中创建了AppBook
表. 打开数据库并输入几个示例行,以便在页面上显示它们:
INSERT INTO AppBook (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
('f3c04764-6bfd-49e2-859e-3f9bfda6183e', '2018-07-01', '1984',3,'1949-06-08','19.84')
INSERT INTO AppBook (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
('13024066-35c9-473c-997b-83cd8d3e29dc', '2018-07-01', 'The Hitchhiker`s Guide to the Galaxy',7,'1995-09-27','42')
INSERT INTO AppBook (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
('4fa024a1-95ac-49c6-a709-6af9e4d54b54', '2018-07-02', 'Pet Sematary',5,'1983-11-14','23.7')
创建应用服务
下一步是创建应用服务来管理(创建,列出,更新,删除)书籍. 启动模板中的应用程序层分为两个项目:
Acme.BookStore.Application.Contracts
主要包含你的DTO和应用程序服务接口.Acme.BookStore.Application
包含应用程序服务的实现.
BookDto
在Acme.BookStore.Application.Contracts
项目中创建一个名为BookDto
的DTO类:
using System;
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore
{
public class BookDto : AuditedEntityDto<Guid>
{
public string Name { get; set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
}
}
- DTO类被用来在 表示层 和 应用层 传递数据.查看DTO文档查看更多信息.
- 为了在页面上展示书籍信息,
BookDto
被用来将书籍数据传递到表示层. BookDto
继承自AuditedEntityDto<Guid>
.跟上面定义的Book
类一样具有一些审计属性.
在将书籍返回到表示层时,需要将Book
实体转换为BookDto
对象. AutoMapper库可以在定义了正确的映射时自动执行此转换. 启动模板配置了AutoMapper,因此你只需在Acme.BookStore.Application
项目的BookStoreApplicationAutoMapperProfile
类中定义映射:
using AutoMapper;
namespace Acme.BookStore
{
public class BookStoreApplicationAutoMapperProfile : Profile
{
public BookStoreApplicationAutoMapperProfile()
{
CreateMap<Book, BookDto>();
}
}
}
CreateUpdateBookDto
在Acme.BookStore.Application.Contracts
项目中创建一个名为CreateUpdateBookDto
的DTO类:
using System;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.AutoMapper;
namespace Acme.BookStore
{
public class CreateUpdateBookDto
{
[Required]
[StringLength(128)]
public string Name { get; set; }
[Required]
public BookType Type { get; set; } = BookType.Undefined;
[Required]
public DateTime PublishDate { get; set; }
[Required]
public float Price { get; set; }
}
}
- 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息.
- 它定义了数据注释属性(如
[Required]
)来定义属性的验证. DTO由ABP框架自动验证.
就像上面的BookDto
一样,创建一个从CreateUpdateBookDto
对象到Book
实体的映射:
using AutoMapper;
namespace Acme.BookStore
{
public class BookStoreApplicationAutoMapperProfile : Profile
{
public BookStoreApplicationAutoMapperProfile()
{
CreateMap<Book, BookDto>();
CreateMap<CreateUpdateBookDto, Book>(); //<--added this line-->
}
}
}
IBookAppService
在Acme.BookStore.Application.Contracts
项目中定义一个名为IBookAppService
的接口:
using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Acme.BookStore
{
public interface IBookAppService :
ICrudAppService< //定义了CRUD方法
BookDto, //用来展示书籍
Guid, //Book实体的主键
PagedAndSortedResultRequestDto, //获取书籍的时候用于分页和排序
CreateUpdateBookDto, //用于创建书籍
CreateUpdateBookDto> //用于更新书籍
{
}
}
- 框架定义应用程序服务的接口不是必需的. 但是,它被建议作为最佳实践.
ICrudAppService
定义了常见的CRUD方法:GetAsync
,GetListAsync
,CreateAsync
,UpdateAsync
和DeleteAsync
. 你可以从空的IApplicationService
接口继承并手动定义自己的方法.ICrudAppService
有一些变体, 你可以在每个方法中使用单独的DTO,也可以分别单独指定.
BookAppService
在Acme.BookStore.Application
项目中实现名为BookAppService
的IBookAppService
:
using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore
{
public class BookAppService :
CrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto,
CreateUpdateBookDto, CreateUpdateBookDto>,
IBookAppService
{
public BookAppService(IRepository<Book, Guid> repository)
: base(repository)
{
}
}
}
BookAppService
继承了CrudAppService<...>
.它实现了上面定义的CRUD方法.BookAppService
注入IRepository <Book,Guid>
,这是Book
实体的默认仓储. ABP自动为每个聚合根(或实体)创建默认仓储. 请参阅仓储文档BookAppService
使用IObjectMapper
将Book
对象转换为BookDto
对象, 将CreateUpdateBookDto
对象转换为Book
对象. 启动模板使用AutoMapper库作为对象映射提供程序. 你之前定义了映射, 因此它将按预期工作.
自动生成API Controllers
你通常创建Controller以将应用程序服务公开为HTTP API端点. 因此允许浏览器或第三方客户端通过AJAX调用它们. ABP可以自动按照惯例将你的应用程序服务配置为MVC API控制器.
Swagger UI
启动模板配置为使用Swashbuckle.AspNetCore运行swagger UI. 运行应用程序并在浏览器中输入https://localhost:XXXX/swagger/
(用你自己的端口替换XXXX)作为URL.
你会看到一些内置的接口和Book
的接口,它们都是REST风格的:
Swagger有一个很好的UI来测试API. 你可以尝试执行[GET] /api/app/book
API来获取书籍列表.
动态JavaScript代理
在Javascript端通过AJAX的方式调用HTTP API接口是很常见的,你可以使用$.ajax
或者其他的工具来调用接口.当然,ABP中提供了更好的方式.
ABP 自动 为所有的API接口创建了JavaScript 代理.因此,你可以像调用 JavaScript function一样调用任何接口.
在浏览器的开发者控制台中测试接口
你可以使用你钟爱的浏览器的 开发者控制台 中轻松测试JavaScript代理.运行程序,并打开浏览器的 开发者工具(快捷键:F12),切换到 Console 标签,输入下面的代码并回车:
acme.bookStore.book.getList({}).done(function (result) { console.log(result); });
acme.bookStore
是BookAppService
的命名空间,转换成了驼峰命名.book
是BookAppService
转换后的名字(去除了AppService后缀并转成了驼峰命名).getList
是定义在AsyncCrudAppService
基类中的GetListAsync
方法转换后的名字(去除了Async后缀并转成了驼峰命名).{}
参数用于将空对象发送到GetListAsync
方法,该方法通常需要一个类型为PagedAndSortedResultRequestDto
的对象,用于向服务器发送分页和排序选项(所有属性都是可选的,所以你可以发送一个空对象).getList
方法返回了一个promise
.因此,你可以传递一个回调函数到done
(或者then
)方法中来获取服务返回的结果.
运行这段代码会产生下面的输出:
你可以看到服务器返回的 book list.你还可以切换到开发者工具的 network 查看客户端到服务器端的通讯信息:
我们使用create
方法 创建一本新书:
acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05-24', price: 21.5 }).done(function (result) { console.log('successfully created the book with id: ' + result.id); });
你会看到控制台会显示类似这样的输出:
successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246
检查数据库中的Book
表以查看新书. 你可以自己尝试get
,update
和delete
功能.
创建书籍页面
现在我们来创建一些可见和可用的东西,取代经典的MVC,我们使用微软推荐的Razor Pages UI.
在 Acme.BookStore.Web
项目的Pages
文件夹下创建一个新的文件夹叫Book
并添加一个名为Index.cshtml
的Razor Page.
打开Index.cshtml
并把内容修改成下面这样:
Index.cshtml:
@page
@using Acme.BookStore.Web.Pages.Book
@model IndexModel
<h2>Book</h2>
- 确保
IndexModel
(Index.cshtml.cs)具有Acme.BookStore.Web.Pages.Book
命名空间,或者在Index.cshtml
中更新它.
Index.cshtml.cs:
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Acme.BookStore.Web.Pages.Book
{
public class IndexModel : PageModel
{
public void OnGet()
{
}
}
}
将Book页面添加到主菜单
打开Menus
文件夹中的 BookStoreMenuContributor
类,在ConfigureMainMenuAsync
方法的底部添加如下代码:
//...
namespace Acme.BookStore.Web.Menus
{
public class BookStoreMenuContributor : IMenuContributor
{
private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
{
//<-- added the below code
context.Menu.AddItem(
new ApplicationMenuItem("BookStore", l["Menu:BookStore"])
.AddItem(
new ApplicationMenuItem("BookStore.Book", l["Menu:Book"], url: "/Book")
)
);
//-->
}
}
}
本地化菜单
本地化文本位于Acme.BookStore.Domain.Shared
项目的Localization/BookStore
文件夹下:
打开en.json
文件,将Menu:BookStore
和Menu:Book
键的本地化文本添加到文件末尾:
{
"Culture": "en",
"Texts": {
"Menu:Home": "Home",
"Welcome": "Welcome",
"LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.",
"Menu:BookStore": "Book Store",
"Menu:Book": "Book",
"Actions": "Actions",
"Edit": "Edit",
"PublishDate": "Publish date",
"NewBook": "New book",
"Name": "Name",
"Type": "Type",
"Price": "Price",
"CreationTime": "Creation time",
"AreYouSureToDelete": "Are you sure you want to delete this item?"
}
}
- ABP的本地化功能建立在ASP.NET Core’s standard localization)之上并增加了一些扩展.查看本地化文档.
- 本地化key是任意的. 你可以设置任何名称. 我们更喜欢为菜单项添加
Menu:
前缀以区别于其他文本. 如果未在本地化文件中定义文本,则它将返回到本地化的key(ASP.NET Core的标准行为).
运行该应用程序,看到新菜单项已添加到顶部栏:
点击BookStore下Book子菜单项就会跳转到新增的书籍页面.
书籍列表
我们将使用Datatables.netJQuery插件来显示页面上的表格列表. Datatables可以完全通过AJAX工作,速度快,并提供良好的用户体验. Datatables插件在启动模板中配置,因此你可以直接在任何页面中使用它,而需要在页面中引用样式和脚本文件.
Index.cshtml
将Pages/Book/Index.cshtml
改成下面的样子:
@page
@model Acme.BookStore.Web.Pages.Book.IndexModel
@section scripts
{
<abp-script src="/Pages/Book/index.js" />
}
<abp-card>
<abp-card-header>
<h2>@L["Book"]</h2>
</abp-card-header>
<abp-card-body>
<abp-table striped-rows="true" id="BookTable">
<thead>
<tr>
<th>@L["Name"]</th>
<th>@L["Type"]</th>
<th>@L["PublishDate"]</th>
<th>@L["Price"]</th>
<th>@L["CreationTime"]</th>
</tr>
</thead>
</abp-table>
</abp-card-body>
</abp-card>
abp-script
tag helper用于将外部的 脚本 添加到页面中.它比标准的script
标签多了很多额外的功能.它可以处理 最小化和 版本.查看捆绑 & 压缩文档获取更多信息.abp-card
和abp-table
是为Twitter Bootstrap的card component封装的 tag helpers.ABP中有很多tag helpers,可以很方便的使用大多数bootstrap组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看tag helpers 文档.- 你可以像上面本地化菜单一样 本地化 列名.
添加脚本文件
在Pages/Book/
文件夹中创建 index.js
文件
index.js
的内容如下:
$(function () {
var dataTable = $('#BookTable').DataTable(abp.libs.datatables.normalizeConfiguration({
ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
columnDefs: [
{ data: "name" },
{ data: "type" },
{ data: "publishDate" },
{ data: "price" },
{ data: "creationTime" }
]
}));
});
abp.libs.datatables.createAjax
是帮助ABP的动态JavaScript API代理跟Datatable的格式相适应的辅助方法.abp.libs.datatables.normalizeConfiguration
是另一个辅助方法.不是必须的, 但是它通过为缺少的选项提供常规值来简化数据表配置.acme.bookStore.book.getList
是获取书籍列表的方法(上面已经介绍过了)- 查看 Datatable文档 了解更多配置项.
最终的页面如下: