Web应用程序开发教程 - 第一章: 创建服务端
关于本教程
在本系列教程中, 你将构建一个名为 Acme.BookStore 的用于管理书籍及其作者列表的基于ABP的应用程序.  它是使用以下技术开发的:
- Entity Framework Core 做为数据库提供程序.
- MVC / Razor Pages 做为UI框架.
本教程分为以下部分:
- Part 1: 创建服务端 (本章)
- Part 2: 图书列表页面
- Part 3: 创建,更新和删除图书
- Part 4: 集成测试
- Part 5: 授权
- Part 6: 作者: 领域层
- Part 7: 作者: 数据库集成
- Part 8: 作者: 应用服务层
- Part 9: 作者: 用户界面
- Part 10: 图书到作者的关系
下载源码
本教程根据你的UI 和 数据库偏好有多个版本,我们准备了几种可供下载的源码组合:
如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 在Windows 10中启用长路径.
如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
git config --system core.longpaths true
视频教程
本章也被录制为视频教程 发布在YouTube.
创建解决方案
在开始开发之前,请按照入门教程创建名为 Acme.BookStore 的新解决方案.
创建Book实体
启动模板中的领域层分为两个项目:
在解决方案的领域层(Acme.BookStore.Domain项目)中定义你的实体.
该应用程序的主要实体是Book. 在Acme.BookStore.Domain项目中创建一个 Books 文件夹(命名空间),并在其中添加名为 Book 的类,如下所示:
using System;
using Volo.Abp.Domain.Entities.Auditing;
namespace Acme.BookStore.Books
{
    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; }
    }
}
- ABP为实体提供了两个基本的基类: AggregateRoot和Entity. Aggregate Root是领域驱动设计 概念之一. 可以视为直接查询和处理的根实体(请参阅实体文档).
- Book实体继承了- AuditedAggregateRoot,- AuditedAggregateRoot类在- AggregateRoot类的基础上添加了一些基础审计属性(例如- CreationTime,- CreatorId,- LastModificationTime等). ABP框架自动为你管理这些属性.
- Guid是- Book实体的主键类型.
为了保持简单,本教程将实体属性保留为 public get/set . 如果你想了解关于DDD最佳实践,请参阅实体文档.
BookType枚举
Book实体使用了BookType枚举. 在Acme.BookStore.Domain.Shared项目中创建Books文件夹(命名空间),并在其中添加BookType:
namespace Acme.BookStore.Books
{
    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> Books { get; set; }
    //...
}
将Book实体映射到数据库表
打开BookStoreDbContext类的OnModelCreating方法,为Book实体添加映射代码:
using Acme.BookStore.Books;
...
namespace Acme.BookStore.EntityFrameworkCore
{
    public class BookStoreDbContext :
        AbpDbContext<BookStoreDbContext>,
        IIdentityDbContext,
        ITenantManagementDbContext
    {
        ...
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            /* Include modules to your migration db context */
            builder.ConfigurePermissionManagement();
            ...
            /* Configure your own tables/entities inside here */
            builder.Entity<Book>(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);
            });
        }
    }
}
- BookStoreConsts含有用于表的架构和表前缀的常量值. 使用它不是强制的,但建议在统一的地方控制表前缀.
- ConfigureByConvention()方法优雅的配置/映射继承的属性,应对所有的实体使用它.
添加数据迁移
本示例使用EF Core Code First Migrations.因为我们修改了数据库映射配置,我们必须创建一个新的迁移并且应用到数据库.
在 Acme.BookStore.EntityFrameworkCore 目录打开命令行终端输入以下命令:
dotnet ef migrations add Created_Book_Entity
它会添加新迁移类到项目中:
如果你使用Visual Studio, 你也许想要在包管理控制台(PMC)中使用
Add-Migration Created_Book_Entity -c BookStoreDbContext和Update-Database -Context BookStoreDbContext命令. 确保Acme.BookStore.Web是启动项目并且Acme.BookStore.EntityFrameworkCore.DbMigrations是 PMC 的默认项目.
添加种子数据
在运行应用程序之前最好将初始数据添加到数据库中. 本节介绍ABP框架的数据种子系统. 如果你不想创建种子数据可以跳过本节,但是建议你遵循它来学习这个有用的ABP Framework功能。
在 *.Domain 项目下创建 IDataSeedContributor 的派生类,并且拷贝以下代码:
using System;
using System.Threading.Tasks;
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;
        public BookStoreDataSeederContributor(IRepository<Book, Guid> bookRepository)
        {
            _bookRepository = bookRepository;
        }
        public async Task SeedAsync(DataSeedContext context)
        {
            if (await _bookRepository.GetCountAsync() <= 0)
            {
                await _bookRepository.InsertAsync(
                    new Book
                    {
                        Name = "1984",
                        Type = BookType.Dystopia,
                        PublishDate = new DateTime(1949, 6, 8),
                        Price = 19.84f
                    },
                    autoSave: true
                );
                await _bookRepository.InsertAsync(
                    new Book
                    {
                        Name = "The Hitchhiker's Guide to the Galaxy",
                        Type = BookType.ScienceFiction,
                        PublishDate = new DateTime(1995, 9, 27),
                        Price = 42.0f
                    },
                    autoSave: true
                );
            }
        }
    }
}
- 如果数据库中当前没有图书,则此代码使用 IRepository<Book, Guid>(默认repository)将两本书插入数据库.
更新数据库
运行 Acme.BookStore.DbMigrator 应用程序来更新数据库:
.DbMigrator  是一个控制台使用程序,可以在开发和生产环境迁移数据库架构和初始化种子数据.
创建应用程序
应用程序层由两个分离的项目组成:
在本部分中,你将创建一个应用程序服务,使用ABP Framework的 CrudAppService 基类来获取,创建,更新和删除书籍.
BookDto
CrudAppService 基类需要定义实体的基本DTO. 在 Acme.BookStore.Application.Contracts 项目中创建 Books 文件夹(命名空间), 并在其中添加名为 BookDto 的DTO类:
using System;
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Books
{
    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 Acme.BookStore.Books;
using AutoMapper;
namespace Acme.BookStore
{
    public class BookStoreApplicationAutoMapperProfile : Profile
    {
        public BookStoreApplicationAutoMapperProfile()
        {
            CreateMap<Book, BookDto>();
        }
    }
}
参阅 对象到对象映射 文档了解详情.
CreateUpdateBookDto
在Acme.BookStore.Application.Contracts项目中创建 Books 文件夹(命名空间),并在其中添加名为 CreateUpdateBookDto 的DTO类:
using System;
using System.ComponentModel.DataAnnotations;
namespace Acme.BookStore.Books
{
    public class CreateUpdateBookDto
    {
        [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; }
    }
}
- 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息.
- 它定义了数据注释特性(如[Required])来定义属性的验证规则. DTO由ABP框架自动验证.
就像上面的BookDto一样,创建一个从CreateUpdateBookDto对象到Book实体的映射,最终映射配置类如下:
using Acme.BookStore.Books;
using AutoMapper;
namespace Acme.BookStore
{
    public class BookStoreApplicationAutoMapperProfile : Profile
    {
        public BookStoreApplicationAutoMapperProfile()
        {
            CreateMap<Book, BookDto>();
            CreateMap<CreateUpdateBookDto, Book>();
        }
    }
}
IBookAppService
下一步是为应用程序定义接口,在Acme.BookStore.Application.Contracts项目创建 Books 文件夹(命名空间),并在其中添加名为IBookAppService的接口:
using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Acme.BookStore.Books
{
    public interface IBookAppService :
        ICrudAppService< //Defines CRUD methods
            BookDto, //Used to show books
            Guid, //Primary key of the book entity
            PagedAndSortedResultRequestDto, //Used for paging/sorting
            CreateUpdateBookDto> //Used to create/update a book
    {
    }
}
- 框架定义应用程序服务的接口不是必需的. 但是,它被建议作为最佳实践.
- ICrudAppService定义了常见的CRUD方法:- GetAsync,- GetListAsync,- CreateAsync,- UpdateAsync和- DeleteAsync. 从这个接口扩展不是必需的,你可以从空的- IApplicationService接口继承并手动定义自己的方法(将在下一部分中完成).
- ICrudAppService有一些变体, 你可以在每个方法中使用单独的DTO(例如使用不同的DTO进行创建和更新).
BookAppService
是时候实现IBookAppService接口了.在Acme.BookStore.Application项目中创建 Books 文件夹(命名空间),并在其中添加名为 BookAppService 的类:
using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Books
{
    public class BookAppService :
        CrudAppService<
            Book, //The Book entity
            BookDto, //Used to show books
            Guid, //Primary key of the book entity
            PagedAndSortedResultRequestDto, //Used for paging/sorting
            CreateUpdateBookDto>, //Used to create/update a book
        IBookAppService //implement the IBookAppService
    {
        public BookAppService(IRepository<Book, Guid> repository)
            : base(repository)
        {
        }
    }
}
- BookAppService继承了- CrudAppService<...>.它实现了- ICrudAppService定义的CRUD方法.
- BookAppService注入- IRepository <Book,Guid>,这是- Book实体的默认仓储. ABP自动为每个聚合根(或实体)创建默认仓储. 请参阅仓储文档
- BookAppService使用- IObjectMapper将- Book对象转换为- BookDto对象, 将- CreateUpdateBookDto对象转换为- Book对象. 启动模板使用AutoMapper库作为对象映射提供程序. 我们之前定义了映射, 因此它将按预期工作.
自动生成API Controllers
在典型的ASP.NET Core应用程序中,你创建API Controller以将应用程序服务公开为HTTP API端点. 这将允许浏览器或第三方客户端通过HTTP调用它们.
ABP可以自动按照约定将你的应用程序服务配置为MVC API控制器.
Swagger UI
启动模板配置为使用Swashbuckle.AspNetCore运行swagger UI. 运行应用程序并在浏览器中输入https://localhost:XXXX/swagger/(用你自己的端口替换XXXX)作为URL.
使用CTRL+F5运行应用程序 (Acme.BookStore.Web)并使用浏览器访问https://localhost:<port>/swagger/ on your browser. 使用你自己的端口号替换 <port>.
你会看到一些内置的服务端点和Book服务,它们都是REST风格的端点:
Swagger有一个很好的UI来测试API.
你可以尝试执行[GET] /api/app/book API来获取书籍列表, 服务端会返回以下JSON结果:
{
  "totalCount": 2,
  "items": [
    {
      "name": "The Hitchhiker's Guide to the Galaxy",
      "type": 7,
      "publishDate": "1995-09-27T00:00:00",
      "price": 42,
      "lastModificationTime": null,
      "lastModifierId": null,
      "creationTime": "2020-07-03T21:04:18.4607218",
      "creatorId": null,
      "id": "86100bb6-cbc1-25be-6643-39f62806969c"
    },
    {
      "name": "1984",
      "type": 3,
      "publishDate": "1949-06-08T00:00:00",
      "price": 19.84,
      "lastModificationTime": null,
      "lastModifierId": null,
      "creationTime": "2020-07-03T21:04:18.3174016",
      "creatorId": null,
      "id": "41055277-cce8-37d7-bb37-39f62806960b"
    }
  ]
}
这很酷,因为我们没有编写任何代码来创建API控制器,但是现在我们有了一个可以正常使用的REST API!
下一章
参阅教程的下一章.
 抠丁客
    抠丁客




