Web应用程序开发教程 - 第一章: 创建服务端

关于本教程

在本系列教程中, 你将构建一个名为 Acme.BookStore 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:

  • Entity Framework Core 做为ORM提供程序.
  • MVC / Razor Pages 做为UI框架.

本教程分为以下部分:

下载源码

本教程根据你的UIDatabase偏好有多个版,我们准备了两种可供下载的源码组合:

创建解决方案

在开始开发之前,请按照入门教程创建名为 Acme.BookStore 的新解决方案.

创建Book实体

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

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

在解决方案的领域层(Acme.BookStore.Domain项目)中定义你的实体.

该应用程序的主要实体是Book. 在Acme.BookStore.Domain项目中创建一个 Books 文件夹并在其中添加一个名为 Book 的类,如下所示:

  1. using System;
  2. using Volo.Abp.Domain.Entities.Auditing;
  3. namespace Acme.BookStore.Books
  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. }
  12. }
  • ABP为实体提供了两个基本的基类: AggregateRootEntity. Aggregate Root是**领域驱动设计** 概念之一. 可以视为直接查询和处理的根实体(请参阅实体文档).
  • Book实体继承了AuditedAggregateRoot,AuditedAggregateRoot类在AggregateRoot类的基础上添加了一些审计属性(CreationTime, CreatorId, LastModificationTime 等). ABP框架自动为你管理这些属性.
  • GuidBook实体的主键类型.

为了保持简单,本教程将实体属性保留为 public get/set . 如果你想了解关于DDD最佳实践,请参阅实体文档.

BookType枚举

上面所用到了 BookType 枚举,在 Acme.BookStore.Domain.Shared 项目创建 BookType.

  1. namespace Acme.BookStore.Books
  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. }

最终的文件夹/文件结构应该如下所示:

bookstore-book-and-booktype

将Book实体添加到DbContext中

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

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

将Book实体映射到数据库表

Acme.BookStore.EntityFrameworkCore 项目中打开 BookStoreDbContextModelCreatingExtensions.cs 文件,添加 Book 实体的映射代码. 最终类应为:

  1. using Acme.BookStore.Books;
  2. using Microsoft.EntityFrameworkCore;
  3. using Volo.Abp;
  4. using Volo.Abp.EntityFrameworkCore.Modeling;
  5. namespace Acme.BookStore.EntityFrameworkCore
  6. {
  7. public static class BookStoreDbContextModelCreatingExtensions
  8. {
  9. public static void ConfigureBookStore(this ModelBuilder builder)
  10. {
  11. Check.NotNull(builder, nameof(builder));
  12. /* Configure your own tables/entities inside here */
  13. builder.Entity<Book>(b =>
  14. {
  15. b.ToTable(BookStoreConsts.DbTablePrefix + "Books",
  16. BookStoreConsts.DbSchema);
  17. b.ConfigureByConvention(); //auto configure for the base class props
  18. b.Property(x => x.Name).IsRequired().HasMaxLength(128);
  19. });
  20. }
  21. }
  22. }
  • BookStoreConsts 含有用于表的架构和表前缀的常量值. 你不必使用它,但建议在单点控制表前缀.
  • ConfigureByConvention() 方法优雅的配置/映射继承的属性,应始终对你所有的实体使用它.

添加数据迁移

启动模板使用EF Core Code First Migrations创建和维护数据库架构. 打开菜单工具 > NuGet包管理器下的程序包管理控制台 (PMC).

Open Package Manager Console

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

  1. Add-Migration "Created_Book_Entity"

bookstore-pmc-add-book-migration

它会在 Acme.BookStore.EntityFrameworkCore.DbMigrations 项目中的 Migrations 文件内创建一个新的迁移类.

在更新数据库之前,请阅读下面的部分了解如何将一些初始数据插入到数据库.

如果你使用其他IDE而不是Visual Studio, 你可以使用 [dotnet-ef](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli#create-a-migration) 工具.

添加种子数据

在运行应用程序之前最好将初始数据添加到数据库中. 本节介绍ABP框架的数据种子系统. 如果你不想创建种子数据可以跳过本节,但是建议你遵循它来学习这个有用的ABP Framework功能。

*.Domain 项目下创建派生 IDataSeedContributor 的类,并且拷贝以下代码:

  1. using System;
  2. using System.Threading.Tasks;
  3. using Acme.BookStore.Books;
  4. using Volo.Abp.Data;
  5. using Volo.Abp.DependencyInjection;
  6. using Volo.Abp.Domain.Repositories;
  7. namespace Acme.BookStore
  8. {
  9. public class BookStoreDataSeederContributor
  10. : IDataSeedContributor, ITransientDependency
  11. {
  12. private readonly IRepository<Book, Guid> _bookRepository;
  13. public BookStoreDataSeederContributor(IRepository<Book, Guid> bookRepository)
  14. {
  15. _bookRepository = bookRepository;
  16. }
  17. public async Task SeedAsync(DataSeedContext context)
  18. {
  19. if (await _bookRepository.GetCountAsync() <= 0)
  20. {
  21. await _bookRepository.InsertAsync(
  22. new Book
  23. {
  24. Name = "1984",
  25. Type = BookType.Dystopia,
  26. PublishDate = new DateTime(1949, 6, 8),
  27. Price = 19.84f
  28. },
  29. autoSave: true
  30. );
  31. await _bookRepository.InsertAsync(
  32. new Book
  33. {
  34. Name = "The Hitchhiker's Guide to the Galaxy",
  35. Type = BookType.ScienceFiction,
  36. PublishDate = new DateTime(1995, 9, 27),
  37. Price = 42.0f
  38. },
  39. autoSave: true
  40. );
  41. }
  42. }
  43. }
  44. }
  • 如果数据库中当前没有图书,则此代码使用 IRepository<Book, Guid>(默认为repository将两本书插入数据库.

更新数据库

运行 Acme.BookStore.DbMigrator 应用程序来更新数据库:

bookstore-dbmigrator-on-solution

.DbMigrator 是一个控制台使用程序,可以在开发生产环境迁移数据库架构初始化种子数据.

创建应用程序

应用程序层由两个分离的项目组成:

  • `Acme.BookStore.Application.Contracts 包含你的DTO应用服务接口.
  • Acme.BookStore.Application 包含你的应用服务实现.

在本部分中,你将创建一个应用程序服务,使用ABP Framework的 CrudAppService 基类来获取,创建,更新和删除书籍.

BookDto

CrudAppService 基类需要定义实体的基本DTO. 在 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类被用来在 表示层应用层 传递数据.查看DTO文档查看更多信息.
  • 为了在页面上展示书籍信息,BookDto被用来将书籍数据传递到表示层.
  • BookDto继承自 AuditedEntityDto<Guid>.跟上面定义的 Book 实体一样具有一些审计属性.

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

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

参阅 对象对对象映射 文档了解详情.

CreateUpdateBookDto

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

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

就像上面的BookDto一样,创建一个从CreateUpdateBookDto对象到Book实体的映射,最终映射配置类如下:

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

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.Books
  5. {
  6. public interface IBookAppService :
  7. ICrudAppService< //Defines CRUD methods
  8. BookDto, //Used to show books
  9. Guid, //Primary key of the book entity
  10. PagedAndSortedResultRequestDto, //Used for paging/sorting
  11. CreateUpdateBookDto> //Used to create/update a book
  12. {
  13. }
  14. }
  • 框架定义应用程序服务的接口不是必需的. 但是,它被建议作为最佳实践.
  • ICrudAppService定义了常见的CRUD方法:GetAsync,GetListAsync,CreateAsync,UpdateAsyncDeleteAsync. 你可以从空的IApplicationService接口继承并手动定义自己的方法(将在下一部分中完成).
  • ICrudAppService有一些变体, 你可以在每个方法中使用单独的DTO,也可以分别单独指定(例如使用不同的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.Books
  6. {
  7. public class BookAppService :
  8. CrudAppService<
  9. Book, //The Book entity
  10. BookDto, //Used to show books
  11. Guid, //Primary key of the book entity
  12. PagedAndSortedResultRequestDto, //Used for paging/sorting
  13. CreateUpdateBookDto>, //Used to create/update a book
  14. IBookAppService //implement the IBookAppService
  15. {
  16. public BookAppService(IRepository<Book, Guid> repository)
  17. : base(repository)
  18. {
  19. }
  20. }
  21. }
  • BookAppService继承了CrudAppService<...>.它实现了 ICrudAppService 定义的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运行swagger UI. 运行应用程序并在浏览器中输入https://localhost:XXXX/swagger/(用你自己的端口替换XXXX)作为URL.

你会看到一些内置的接口和Book的接口,它们都是REST风格的:

bookstore-swagger

Swagger有一个很好的UI来测试API.

你可以尝试执行[GET] /api/app/book API来获取书籍列表, 服务端会返回以下JSON结果:

  1. {
  2. "totalCount": 2,
  3. "items": [
  4. {
  5. "name": "The Hitchhiker's Guide to the Galaxy",
  6. "type": 7,
  7. "publishDate": "1995-09-27T00:00:00",
  8. "price": 42,
  9. "lastModificationTime": null,
  10. "lastModifierId": null,
  11. "creationTime": "2020-07-03T21:04:18.4607218",
  12. "creatorId": null,
  13. "id": "86100bb6-cbc1-25be-6643-39f62806969c"
  14. },
  15. {
  16. "name": "1984",
  17. "type": 3,
  18. "publishDate": "1949-06-08T00:00:00",
  19. "price": 19.84,
  20. "lastModificationTime": null,
  21. "lastModifierId": null,
  22. "creationTime": "2020-07-03T21:04:18.3174016",
  23. "creatorId": null,
  24. "id": "41055277-cce8-37d7-bb37-39f62806960b"
  25. }
  26. ]
  27. }

这很酷,因为我们没有编写任何代码来创建API控制器,但是现在我们有了一个可以正常使用的REST API!

下一章

参阅教程的下一章.