ASP.NET Core MVC / Razor Pages 系列教程包括三个3个部分:

    创建新项目

    创建一个名为 Acme.BookStore 的新项目,其中 Acme 是公司名 BookStore 是项目名. 你可以参阅入门 文档了解如何创建新项目. 我们将使用CLI创建新项目.

    创建项目

    使用以下命令创建一个新的ABP项目,使用 Entity Framework Core 做为数据库提供者, UI选项使用 MVC / Razor Pages. 其他CLI选项请参考ABP CLI文档.

    应用迁移

    项目创建后,需要应用初始化迁移创建数据库. 运行 Acme.BookStore.DbMigrator 应用程序. 它会应用所有迁移,完成流程后你会看到以下结果,数据库已经准备好了!

    初始化数据库表

    右键单击 Acme.BookStore.Web 项目设置为启动项. 使用 CTRL+F5F5 运行应用程序.

    更多信息,参阅入门教程的运行应用程序部分.

    默认的登录凭证:

    • Username: admin
    • Password: 1q2w3E*

    解决方案的结构

    下面的图片展示了从启动模板创建的项目是如何分层的.

    创建Book实体

    启动模板中的域层分为两个项目:

    • Acme.BookStore.Domain包含你的, 领域服务和其他核心域对象.
    • Acme.BookStore.Domain.Shared包含可与客户共享的常量,枚举或其他域相关对象.

    在解决方案的领域层(Acme.BookStore.Domain项目)中定义. 该应用程序的主要实体是Book. 在Acme.BookStore.Domain项目中创建一个名为Book的类,如下所示:

    1. using System;
    2. using Volo.Abp.Domain.Entities.Auditing;
    3. namespace Acme.BookStore
    4. {
    5. public class Book : AuditedAggregateRoot<Guid>
    6. {
    7. public string Name { get; set; }
    8. public BookType Type { get; set; }
    9. public DateTime PublishDate { get; set; }
    10. public float Price { get; set; }
    11. protected Book()
    12. {
    13. }
    14. public Book(Guid id, string name, BookType type, DateTime publishDate, float price)
    15. :base(id)
    16. {
    17. Name = name;
    18. Type = type;
    19. PublishDate = publishDate;
    20. Price = price;
    21. }
    22. }
    23. }
    • ABP为实体提供了两个基本的基类: AggregateRootEntity. Aggregate Root域驱动设计(DDD) 概念之一. 有关详细信息和最佳做法,请参阅实体文档.
    • Book实体继承了AuditedAggregateRoot,AuditedAggregateRoot类在AggregateRoot类的基础上添加了一些审计属性(CreationTime, CreatorId, LastModificationTime 等).
    • GuidBook实体的主键类型.
    • 使用 数据注解 为EF Core添加映射.或者你也可以使用 EF Core 自带的.

    BookType枚举

    上面所用到的BookType枚举定义如下:

    1. namespace Acme.BookStore
    2. {
    3. public enum BookType
    4. {
    5. Undefined,
    6. Adventure,
    7. Biography,
    8. Dystopia,
    9. Fantastic,
    10. Horror,
    11. Science,
    12. ScienceFiction,
    13. Poetry
    14. }
    15. }

    将Book实体添加到DbContext中

    EF Core需要你将实体和 DbContext 建立关联.最简单的做法是在Acme.BookStore.EntityFrameworkCore项目的BookStoreDbContext类中添加DbSet属性.如下所示:

    1. public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
    2. {
    3. public DbSet<Book> Book { get; set; }
    4. ...
    5. }

    配置你的Book实体

    Acme.BookStore.EntityFrameworkCore 项目中打开 BookStoreDbContextModelCreatingExtensions.cs 文件,并将以下代码添加到 ConfigureBookStore 方法的末尾以配置Book实体:

    1. builder.Entity<Book>(b =>
    2. {
    3. b.ToTable(BookStoreConsts.DbTablePrefix + "Book", BookStoreConsts.DbSchema);
    4. b.ConfigureByConvention(); //auto configure for the base class props
    5. b.Property(x => x.Name).IsRequired().HasMaxLength(128);
    6. });

    添加 using Volo.Abp.EntityFrameworkCore.Modeling; 以使用 ConfigureByConvention 扩展方法.

    添加新的Migration并更新数据库

    这个启动模板使用了EF Core Code First Migrations来创建并维护数据库结构.打开 程序包管理器控制台(Package Manager Console) (PMC) (工具/Nuget包管理器菜单)

    选择 Acme.BookStore.EntityFrameworkCore.DbMigrations作为默认的项目然后执行下面的命令:

    1. Add-Migration "Created_Book_Entity"

    这样就会在 Migrations 文件夹中创建一个新的migration类.然后执行 Update-Database 命令更新数据库结构:

    1. Update-Database

    添加示例数据

    Update-Database命令在数据库中创建了AppBook表. 打开数据库并输入几个示例行,以便在页面上显示它们:

    bookstore-book-table

    下一步是创建来管理(创建,列出,更新,删除)书籍. 启动模板中的应用程序层分为两个项目:

    • Acme.BookStore.Application.Contracts主要包含你的DTO和应用程序服务接口.

    BookDto

    Acme.BookStore.Application.Contracts项目中创建一个名为BookDto的DTO类:

    1. using System;
    2. using Volo.Abp.Application.Dtos;
    3. namespace Acme.BookStore
    4. {
    5. public class BookDto : AuditedEntityDto<Guid>
    6. {
    7. public string Name { get; set; }
    8. public BookType Type { get; set; }
    9. public DateTime PublishDate { get; set; }
    10. public float Price { get; set; }
    11. }
    12. }
    • DTO类被用来在 表示层应用层 传递数据.查看查看更多信息.
    • 为了在页面上展示书籍信息,BookDto被用来将书籍数据传递到表示层.
    • BookDto继承自 .跟上面定义的Book类一样具有一些审计属性.

    在将书籍返回到表示层时,需要将Book实体转换为BookDto对象. AutoMapper库可以在定义了正确的映射时自动执行此转换. 启动模板配置了AutoMapper,因此你只需在Acme.BookStore.Application项目的BookStoreApplicationAutoMapperProfile类中定义映射:

    1. using AutoMapper;
    2. namespace Acme.BookStore
    3. {
    4. public class BookStoreApplicationAutoMapperProfile : Profile
    5. {
    6. public BookStoreApplicationAutoMapperProfile()
    7. {
    8. CreateMap<Book, BookDto>();
    9. }
    10. }
    11. }

    CreateUpdateBookDto

    Acme.BookStore.Application.Contracts项目中创建一个名为CreateUpdateBookDto的DTO类:

    1. using System;
    2. using System.ComponentModel.DataAnnotations;
    3. using Volo.Abp.AutoMapper;
    4. namespace Acme.BookStore
    5. {
    6. public class CreateUpdateBookDto
    7. {
    8. [Required]
    9. [StringLength(128)]
    10. public string Name { get; set; }
    11. [Required]
    12. public BookType Type { get; set; } = BookType.Undefined;
    13. [Required]
    14. public DateTime PublishDate { get; set; }
    15. [Required]
    16. public float Price { get; set; }
    17. }
    18. }
    • 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息.
    • 它定义了数据注释属性(如[Required])来定义属性的验证. DTO由ABP框架自动验证.

    就像上面的BookDto一样,创建一个从CreateUpdateBookDto对象到Book实体的映射:

    1. using AutoMapper;
    2. namespace Acme.BookStore
    3. {
    4. public class BookStoreApplicationAutoMapperProfile : Profile
    5. {
    6. public BookStoreApplicationAutoMapperProfile()
    7. {
    8. CreateMap<Book, BookDto>();
    9. CreateMap<CreateUpdateBookDto, Book>(); //<--added this line-->
    10. }
    11. }
    12. }

    IBookAppService

    Acme.BookStore.Application.Contracts项目中定义一个名为IBookAppService的接口:

    1. using System;
    2. using Volo.Abp.Application.Dtos;
    3. using Volo.Abp.Application.Services;
    4. namespace Acme.BookStore
    5. {
    6. public interface IBookAppService :
    7. ICrudAppService< //定义了CRUD方法
    8. BookDto, //用来展示书籍
    9. Guid, //Book实体的主键
    10. PagedAndSortedResultRequestDto, //获取书籍的时候用于分页和排序
    11. CreateUpdateBookDto, //用于创建书籍
    12. CreateUpdateBookDto> //用于更新书籍
    13. {
    14. }
    15. }
    • 框架定义应用程序服务的接口不是必需的. 但是,它被建议作为最佳实践.
    • ICrudAppService定义了常见的CRUD方法:GetAsync,GetListAsync,CreateAsync,UpdateAsyncDeleteAsync. 你可以从空的IApplicationService接口继承并手动定义自己的方法.
    • ICrudAppService有一些变体, 你可以在每个方法中使用单独的DTO,也可以分别单独指定.

    BookAppService

    Acme.BookStore.Application项目中实现名为BookAppServiceIBookAppService:

    1. using System;
    2. using Volo.Abp.Application.Dtos;
    3. using Volo.Abp.Application.Services;
    4. using Volo.Abp.Domain.Repositories;
    5. namespace Acme.BookStore
    6. {
    7. public class BookAppService :
    8. CreateUpdateBookDto, CreateUpdateBookDto>,
    9. IBookAppService
    10. {
    11. public BookAppService(IRepository<Book, Guid> repository)
    12. : base(repository)
    13. {
    14. }
    15. }
    16. }
    • BookAppService继承了CrudAppService<...>.它实现了上面定义的CRUD方法.
    • BookAppService注入IRepository <Book,Guid>,这是Book实体的默认仓储. ABP自动为每个聚合根(或实体)创建默认仓储. 请参阅
    • BookAppService使用IObjectMapperBook对象转换为BookDto对象, 将CreateUpdateBookDto对象转换为Book对象. 启动模板使用AutoMapper库作为对象映射提供程序. 你之前定义了映射, 因此它将按预期工作.

    自动生成API Controllers

    你通常创建Controller以将应用程序服务公开为HTTP API端点. 因此允许浏览器或第三方客户端通过AJAX调用它们. ABP可以自动按照惯例将你的应用程序服务配置为MVC API控制器.

    Swagger UI

    启动模板配置为使用Swashbuckle.AspNetCore运行. 运行应用程序并在浏览器中输入(用你自己的端口替换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.bookStoreBookAppService的命名空间,转换成了.
    • bookBookAppService转换后的名字(去除了AppService后缀并转成了驼峰命名).
    • getList是定义在AsyncCrudAppService基类中的GetListAsync方法转换后的名字(去除了Async后缀并转成了驼峰命名).
    • {}参数用于将空对象发送到GetListAsync方法,该方法通常需要一个类型为PagedAndSortedResultRequestDto的对象,用于向服务器发送分页和排序选项(所有属性都是可选的,所以你可以发送一个空对象).
    • getList方法返回了一个promise.因此,你可以传递一个回调函数到done(或者then)方法中来获取服务返回的结果.

    运行这段代码会产生下面的输出:

    bookstore-test-js-proxy-getlist

    我们使用create方法 创建一本新书:

    1. 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); });

    你会看到控制台会显示类似这样的输出:

    1. successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246

    检查数据库中的Book表以查看新书. 你可以自己尝试get,updatedelete功能.

    现在我们来创建一些可见和可用的东西,取代经典的MVC,我们使用微软推荐的Razor Pages UI.

    Acme.BookStore.Web项目的Pages文件夹下创建一个新的文件夹叫Book并添加一个名为Index.cshtml的Razor Page.

    打开Index.cshtml并把内容修改成下面这样:

    Index.cshtml:

    1. @page
    2. @using Acme.BookStore.Web.Pages.Book
    3. @model IndexModel
    4. <h2>Book</h2>
    • 确保IndexModel(Index.cshtml.cs)具有Acme.BookStore.Web.Pages.Book命名空间,或者在Index.cshtml中更新它.

    Index.cshtml.cs:

    1. using Microsoft.AspNetCore.Mvc.RazorPages;
    2. namespace Acme.BookStore.Web.Pages.Book
    3. {
    4. public class IndexModel : PageModel
    5. {
    6. public void OnGet()
    7. {
    8. }
    9. }
    10. }

    将Book页面添加到主菜单

    打开Menus文件夹中的 BookStoreMenuContributor 类,在ConfigureMainMenuAsync方法的底部添加如下代码:

    1. //...
    2. namespace Acme.BookStore.Web.Menus
    3. {
    4. public class BookStoreMenuContributor : IMenuContributor
    5. {
    6. private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
    7. {
    8. //<-- added the below code
    9. context.Menu.AddItem(
    10. new ApplicationMenuItem("BookStore", l["Menu:BookStore"])
    11. .AddItem(
    12. new ApplicationMenuItem("BookStore.Book", l["Menu:Book"], url: "/Book")
    13. )
    14. );
    15. //-->
    16. }
    17. }
    18. }

    本地化菜单

    本地化文本位于Acme.BookStore.Domain.Shared项目的Localization/BookStore文件夹下:

    打开en.json文件,将Menu:BookStoreMenu:Book键的本地化文本添加到文件末尾:

    1. {
    2. "Culture": "en",
    3. "Texts": {
    4. "Menu:Home": "Home",
    5. "Welcome": "Welcome",
    6. "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.",
    7. "Menu:BookStore": "Book Store",
    8. "Menu:Book": "Book",
    9. "Actions": "Actions",
    10. "Edit": "Edit",
    11. "PublishDate": "Publish date",
    12. "NewBook": "New book",
    13. "Name": "Name",
    14. "Type": "Type",
    15. "Price": "Price",
    16. "CreationTime": "Creation time",
    17. "AreYouSureToDelete": "Are you sure you want to delete this item?"
    18. }
    19. }
    • ABP的本地化功能建立在)之上并增加了一些扩展.查看本地化文档.
    • 本地化key是任意的. 你可以设置任何名称. 我们更喜欢为菜单项添加Menu:前缀以区别于其他文本. 如果未在本地化文件中定义文本,则它将返回到本地化的key(ASP.NET Core的标准行为).

    运行该应用程序,看到新菜单项已添加到顶部栏:

    点击BookStore下Book子菜单项就会跳转到新增的书籍页面.

    书籍列表

    我们将使用JQuery插件来显示页面上的表格列表. Datatables可以完全通过AJAX工作,速度快,并提供良好的用户体验. Datatables插件在启动模板中配置,因此你可以直接在任何页面中使用它,而需要在页面中引用样式和脚本文件.

    Index.cshtml

    Pages/Book/Index.cshtml改成下面的样子:

    • abp-script tag helper用于将外部的 脚本 添加到页面中.它比标准的script标签多了很多额外的功能.它可以处理 最小化版本.查看获取更多信息.
    • abp-cardabp-table 是为Twitter Bootstrap的card component封装的 tag helpers.ABP中有很多tag helpers,可以很方便的使用大多数组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看tag helpers 文档.
    • 你可以像上面本地化菜单一样 本地化 列名.

    添加脚本文件

    Pages/Book/文件夹中创建 index.js文件

    index.js的内容如下:

    1. $(function () {
    2. var dataTable = $('#BookTable').DataTable(abp.libs.datatables.normalizeConfiguration({
    3. ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
    4. columnDefs: [
    5. { data: "name" },
    6. { data: "type" },
    7. { data: "publishDate" },
    8. { data: "price" },
    9. { data: "creationTime" }
    10. ]
    11. }));
    12. });
    • abp.libs.datatables.createAjax是帮助ABP的动态JavaScript API代理跟的格式相适应的辅助方法.
    • abp.libs.datatables.normalizeConfiguration是另一个辅助方法.不是必须的, 但是它通过为缺少的选项提供常规值来简化数据表配置.
    • 是获取书籍列表的方法(上面已经介绍过了)

    最终的页面如下: