Web 应用开发教程 - 第一部分:创建服务端
创建解决方案
开始开发之前,请按照 入门教程 创建一个名为 Acme.BookStore 的新解决方案并运行它。
创建解决方案之后
安装客户端包
ABP CLI 在创建应用程序时,会在后台运行 abp install-libs 命令,为你的解决方案安装所需的 NPM 包。
但是,有时可能需要手动运行此命令。例如,如果你克隆了应用程序,或者 node_modules 文件夹中的资源没有复制到 wwwroot/libs 文件夹,或者你向解决方案添加了新的客户端包依赖时,就需要运行此命令。
对于这些情况,请在解决方案的根目录运行 abp install-libs 命令以安装所有必需的 NPM 包:
abp install-libs
创建 Book 实体
启动模板中的 领域层 被分为两个项目:
Acme.BookStore.Domain包含你的 实体、领域服务 和其他核心领域对象。Acme.BookStore.Domain.Shared包含可以与客户端共享的常量、枚举或其他领域相关对象。
因此,你需要在解决方案的领域层(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。聚合根 是一个 领域驱动设计 概念,可以将其视为直接被查询和操作的根实体(详见 实体文档)。 Book实体继承自AuditedAggregateRoot,它在AggregateRoot类的基础上添加了一些基本的 审计 属性(如CreationTime、CreatorId、LastModificationTime等)。ABP 会自动为你管理这些属性。Guid是Book实体的 主键类型。
为简化起见,本教程将实体属性保留为 公共 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 属性,如下所示:
using Acme.BookStore.Books;
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);
/* 将模块包含到你的迁移数据库上下文中 */
builder.ConfigurePermissionManagement();
...
/* 在此处配置你自己的表/实体 */
builder.Entity<Book>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Books",
BookStoreConsts.DbSchema);
b.ConfigureByConvention(); // 为基类属性自动配置
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
});
}
}
BookStoreConsts包含表的架构和表前缀的常量值。你不必使用它,但建议在一个地方控制表前缀。ConfigureByConvention()方法会优雅地配置/映射继承的属性。请始终为你所有的实体使用它。
添加数据库迁移
启动解决方案配置为使用 Entity Framework Core Code First Migrations。由于我们更改了数据库映射配置,因此应该创建一个新的迁移并将更改应用到数据库。
在 Acme.BookStore.EntityFrameworkCore 项目目录中打开命令行终端,并键入以下命令:
dotnet ef migrations add Created_Book_Entity
这将向项目添加一个新的迁移类:
如果你使用 Visual Studio,可能希望在 Package Manager Console (PMC) 中使用
Add-Migration Created_Book_Entity和Update-Database命令。在这种情况下,请确保Acme.BookStore.EntityFrameworkCore是 Visual Studio 中的启动项目,并且Acme.BookStore.EntityFrameworkCore是 PMC 中的 默认项目。
添加示例种子数据
在运行应用程序之前,数据库中有一些初始数据是很好的。本节介绍 ABP 的 数据种子 系统。如果你不想创建数据种子,可以跳过本节,但建议你跟着操作以学习这个有用的 ABP 功能。
在 *.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>(默认的 仓储)向数据库插入两本书(如果其中没有任何书籍的话)。
更新数据库
运行 Acme.BookStore.DbMigrator 应用程序以更新数据库:
.DbMigrator 是一个控制台应用程序,可以在 开发 和 生产 环境中运行,以 迁移数据库架构 并 填充种子数据。
创建应用服务
应用层分为两个项目:
在本节中,你将使用 ABP 的 CrudAppService 基类创建一个应用服务,用于获取、创建、更新和删除书籍。
BookDto
CrudAppService 基类要求为实体定义基础的 DTO。在 Acme.BookStore.Application.Contracts 项目中创建一个 Books 文件夹(命名空间),并在其中添加 BookDto 类:
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 类用于在 表示层 和 应用层 之间 传输数据。有关更多详细信息,请参阅 数据传输对象文档。
BookDto用于将书籍数据传输到表示层,以便在 UI 上显示书籍信息。BookDto派生自AuditedEntityDto<Guid>,它像上面定义的Book实体一样,具有审计属性。
在向表示层返回书籍时,需要将 Book 实体映射到 BookDto 对象。当你定义了正确的映射时,Mapperly 库可以自动执行此转换。启动模板预配置了 Mapperly。因此,你只需在 Acme.BookStore.Application 项目的 BookStoreApplicationMappers 类中定义映射即可:
[Mapper]
public partial class BookToBookDtoMapper : MapperBase<Book, BookDto>
{
public override partial BookDto Map(Book source);
public override partial void Map(Book source, BookDto destination);
}
详情请参见 对象到对象映射 文档。
CreateUpdateBookDto
在 Acme.BookStore.Application.Contracts 项目的 Books 文件夹(命名空间)中创建 CreateUpdateBookDto 类:
using System;
using System.ComponentModel.DataAnnotations;
namespace Acme.BookStore.Books;
public class CreateUpdateBookDto
{
[Required]
[StringLength(128)]
public string Name { get; set; } = string.Empty;
[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])来定义属性的验证规则。ABP 会自动验证DTO。
正如上面为 BookDto 所做的那样,我们应该定义从 CreateUpdateBookDto 对象到 Book 实体的映射。最终的类将如下所示:
[Mapper]
public partial class BookToBookDtoMapper : MapperBase<Book, BookDto>
{
public override partial BookDto Map(Book source);
public override partial void Map(Book source, BookDto destination);
}
[Mapper]
public partial class CreateUpdateBookDtoToBookMapper : MapperBase<CreateUpdateBookDto, Book>
{
public override partial Book Map(CreateUpdateBookDto source);
public override partial void Map(CreateUpdateBookDto source, Book destination);
}
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< // 定义 CRUD 方法
BookDto, // 用于展示书籍
Guid, // Book 实体的主键
PagedAndSortedResultRequestDto, // 用于分页/排序
CreateUpdateBookDto> // 用于创建/更新书籍
{
}
- 框架 不要求 为应用服务定义接口。但是,作为最佳实践,建议这样做。
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, // Book 实体
BookDto, // 用于展示书籍
Guid, // Book 实体的主键
PagedAndSortedResultRequestDto, // 用于分页/排序
CreateUpdateBookDto>, // 用于创建/更新书籍
IBookAppService // 实现 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对象。启动模板使用 Mapperly 库作为对象映射提供程序。我们之前已经定义了映射,因此它将按预期工作。
自动 API 控制器
在典型的 ASP.NET Core 应用程序中,你需要创建 API 控制器 来将应用服务公开为 HTTP API 端点。这允许浏览器或第三方客户端通过 HTTP 调用它们。
ABP 可以按照约定 自动地 将你的应用服务配置为 MVC API 控制器。
Swagger UI
启动模板配置为使用 Swashbuckle.AspNetCore 库运行 Swagger UI。按 CTRL+F5 运行应用程序(Acme.BookStore.Web),并在浏览器中导航到 https://localhost:<port>/swagger/。请将 <port> 替换为你自己的端口号。
你将看到一些内置的服务端点,以及 Book 服务及其 REST 风格的端点:
Swagger 提供了一个很好的界面来测试 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!
抠丁客






