Abp Framework手动实践
上一章节有很大一部分是使用默认的CRID进行操作的,本章节将手动进行各层的编写,本次以Student
为模块进行开发。
领域层
实体
在***.Domain
项目中创建Student文件夹,并在文件夹中创建Student的实体类,该实体类继承FullAuditedAggregateRoot<Guid>
类,主键为Guid类型。
Student
共有三个属性,分别为姓名,出生日期和地址。
public class Student : FullAuditedAggregateRoot<Guid>
{
// 姓名不能外部设置,当new一个实例或调用ChangeName时才能修改姓名
public string Name { get; private set; }
public DateTime Birthday { get; set; }
public string Address { get; set; }
private Student()
{}
internal Student(
Guid id,
// 不能为null
[NotNull] string name,
DateTime birthday,
// 可以为null
[CanBeNull] string address = null)
: base(id)
{
SetName(name);
Birthday = birthday;
Address = address;
}
internal Student ChangeName([NotNull] string name)
{
SetName(name);
return this;
}
// 设置姓名
private void SetName([NotNull] string name)
{
Name = Check.NotNullOrWhiteSpace(
name,
nameof(name),
maxLength: StudentConsts.MaxNameLength
);
}
}
FullAuditedAggregateRoot<Guid>
继承使得实体支持软删除 (指实体被删除时, 它并没有从数据库中被删除, 而只是被标记删除), 实体也具有了审计 属性
字段检查
为了避免某个名称过长,或使用了枚举等,可以在***.Domain.Shared
项目中添加限定条件,例如姓名最大长度不能超过5个汉字,可在此项目中添加Student
文件夹,然后创建一个StudentConsts
类
public static class StudentConsts{
public const int MaxNameLength = 5;
}
此类会在DTOs(数据传输类)中使用。
领域服务
因为实体类中的构造函数访问级别是internal的,因此只能在领域层访问他(也就是同一个命名空间中),因此在***.Doamin
项目的Student文件夹中创建StudentManager类,用来创建Student类和修改学生姓名。
StudentManager
public class StudentManager : DomainService
{
private readonly IStudentRepository _studentRepository;
// 初始化时从仓库中获得学生的操作类
public StudentManager(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
// 创建学生实体
public async Task<Student> CreateAsync(
[NotNull] string name,
DateTime brithday,
[CanBeNull] string address = null)
{
// 判断学生姓名是否为空
Check.NotNullOrWhiteSpace(name, nameof(name));
// 从数据库中查找是否存在同名的学生
var existingStudent = await _studentRepository.FindByNameAsync(name);
// 如果存在就返回异常
if (existingStudent != null)
{
throw new StudentAlreadyExistsException(name);
}
// 不存在,则进行创建并返回
return new Student(
GuidGenerator.Create(),
name,
birthDate,
Address
);
}
// 传入实体类和修改后的学生姓名
public async Task ChangeNameAsync(
[NotNull] Student student,
[NotNull] string newName)
{
// 判断两个参数都不能为空
Check.NotNull(student, nameof(student));
Check.NotNullOrWhiteSpace(newName, nameof(newName));
var existingStudent = await _studentRepository.FindByNameAsync(newName);
// 如果未存储,就直接返回;如果已存在但主键不同,则报异常
// 如果存在,且主键相同,就修改学生姓名
if (existingStudent != null && existingStudent.Id != student.Id)
{
throw new StudentAlreadyExistsException(newName);
}
student.ChangeName(newName);
}
}
StudentManager
是一中可控方式来进行业务操作的,非必要的核心业务不能放到领域方法中,这样子可以隔绝数据库与应用层的交互。
在该代码中提到了抛出异常StudentAlreadyExistsException
,需要在***.Domain
的Student文件夹中创建
public class StudentAlreadyExistsException : BusinessException
{
public StudentAlreadyExistsException(string name)
: base(DomainErrorCodes.StudentAlreadyExists)
{
WithData("name", name);
}
}
BusinessException
是一个特殊的异常类型. 在需要时抛出领域相关异常是一个好的实践. ABP框架会自动处理它, 并且它也容易本地化.WithData(...)
方法提供额外的数据给异常对象
DomainErrorCodes.StudentAlreadyExists
为唯一的错误编号,例如404,500等,该错误编号应放到Domain.Shared项目中,在**DomainErrorCodes中添加错误码
public static class ***DomainErrorCodes
{
public const string StudentAlreadyExists = "StudentError:00001";
}
StudentError:00001
是一个字符串,可以对他进行本地化处理,即在**Domain.Shared项目中的Localization/项目名称/en.json或zh-Hans.json中添加
"StudentError:00001":"学生姓名已存在"
IStudentRepository
在StudentManager中注入了IStudentRepository,因此我们需要定义它,在***.Domain
中添加Student文件夹,并创建这个接口
//
public interface IStudentRepository : IRepository<Student, Guid>
{
// 创建通过查找学生姓名找到学生的实体
Task<Student> FindByNameAsync(string name);
// 通过获得学生的列表
// 其中可以通过参数进行排序,查询,返回最大条数等
Task<List<Student>> GetListAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter = null
);
}
IStudentRepository
扩展了标准IRepository<Student, Guid>
接口, 所以所有的标准 repository 方法对于IStudentRepository
都是可用的.- IRepository集成了IQueryable,其中会有很多接口,可直接使用。
数据库集成
DB Context
在EF Core中添加Student的声明(***.EntityFrameworkCore项目中的DbContext)
public DbSet<Student> Student{get;set;}
在该类的OnModelCreating
方法中加入学生类的转换方法
builder.Entity<Student>(b =>
{
b.ToTable(***Consts.DbTablePrefix + "Students",
***Consts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Name)
.IsRequired()
.HasMaxLength(StudentConsts.MaxNameLength);
b.HasIndex(x => x.Name);
});
创建数据库迁移
按照code first模式,需要创建一个表,因此在***.EntityFrameworkCore
项目下打开Command命令行程序,输入一下命令,创建一个迁移类
dotnet ef migrations add Added_Students
如果为更新命令,则运行如下命令
dotnet ef database update
实现IStudentRepository
在***.EntityFrameworkCore
项目中创建一个Student文件夹,然后创建一个新类:EfCoreStudentRepository
public class EfCoreStudentRepository
: EfCoreRepository<***DbContext, Student, Guid>,
IStudentRepository
{
public EfCoreStudentRepository(
IDbContextProvider<***DbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task<Student> FindByNameAsync(string name)
{
var dbSet = await GetDbSetAsync();
return await dbSet.FirstOrDefaultAsync(student => student.Name == name);
}
public async Task<List<Student>> GetListAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter = null)
{
var dbSet = await GetDbSetAsync();
// WhereIf:第一个参数为true时执行查询
return await dbSet
.WhereIf(
!filter.IsNullOrWhiteSpace(),
student => student.Name.Contains(filter)
)
// 可以是 字段名,字段名+ASC/DESC
.OrderBy(sorting)
.Skip(skipCount)
.Take(maxResultCount)
.ToListAsync();
}
}
应用服务层
IStudentAppService
首先创建应用服务的接口,在***.Application.Contracts
项目中创建Student文件夹,然后再文件夹中创建IStudentAppService
public interface IStudentAppService : IApplicationService
{
Task<StudentDto> GetAsync(Guid id);
Task<PagedResultDto<StudentDto>> GetListAsync(GetStudentListDto input);
Task<StudentDto> CreateAsync(CreateStudentDto input);
Task UpdateAsync(Guid id, UpdateStudentDto input);
Task DeleteAsync(Guid id);
}
IApplicationService
是一个常规接口, 所有应用服务都继承自它, 所以 ABP 框架可以识别它们.- 在
Student
实体中定义标准方法用于CRUD操作. PagedResultDto
是一个ABP框架中预定义的 DTO 类. 它拥有一个Items
集合 和一个TotalCount
属性, 用于返回分页结果.- 优先从
CreateAsync
方法返回StudentDto
(新创建的作者), 虽然在这个程序中没有这么做 - 这里只是展示一种不同用法.
StudentDto
该类用于展示(查询时将Model转换为StudentDto)
在IStudentAppService
同级创建StudentDto
类
public class StudentDto : EntityDto<Guid>
{
public string Name { get; set; }
public DateTime Birthday { get; set; }
public string Address { get; set; }
}
EntityDto<T>
只有一个类型为指定泛型参数的Id
属性. 你可以自己创建Id
属性, 而不是继承自EntityDto<T>
.
GetStudentListDto
用于检索条件的Dto,与StudentDto
类同级,一下Dot相同
public class GetStudentListDto : PagedAndSortedResultRequestDto
{
public string? Filter { get; set; }
}
Filter
用于搜索学生姓名. 当为null
时会触发whereIf的第一个参数为false,则为查询全部.PagedAndSortedResultRequestDto
具有标准分页和排序属性:int MaxResultCount
,int SkipCount
和string Sorting
.
CreateStudentDto
新增时可以通过该类转换为Student实体类
public class CreateStudentDto
{
// 通过使用标记进行验证
[Required]
// 设置的姓名最大长度
[StringLength(StudentConsts.MaxNameLength)]
public string Name { get; set; }
[Required]
public DateTime Birthday { get; set; }
public string Address { get; set; }
}
UpdateStudentDto
更新时用的Dto
public class UpdateStudentDto
{
[Required]
[StringLength(StudentConsts.MaxNameLength)]
public string Name { get; set; }
[Required]
public DateTime Birthday { get; set; }
public string Address { get; set; }
}
StudentAppService
实现IStudentAppService
接口,在***.Application
项目的Student文件夹中创建一个新类StudentAppService
//检查权限(策略)的声明式方法, 用来给当前用户授权
[Studentize(***Permissions.Students.Default)]
public class StudentAppService : ApplicationService, IStudentAppService
{
// 注入 IStudentRepository 和 StudentManager 以使用服务方法
private readonly IStudentRepository _studentRepository;
private readonly StudentManager _studentManager;
public StudentAppService(
IStudentRepository studentRepository,
StudentManager studentManager)
{
_studentRepository = studentRepository;
_studentManager = studentManager;
}
//这个方法根据 Id 获得 Student 实体, 使用 对象到对象映射 转换为 StudentDto. 这需要配置AutoMapper
public async Task<StudentDto> GetAsync(Guid id)
{
var student = await _studentRepository.GetAsync(id);
return ObjectMapper.Map<Student, StudentDto>(student);
}
/// <summary>
/// 使用 IStudentRepository.GetListAsync 从数据库中获得分页的, 排序的和过滤的学生姓名列表. 这里只是说明如何创建自定义repository方法.
/// 直接查询 StudentRepository, 得到学生的数量. 如果客户端发送了过滤条件, 会得到过滤后的作学生数量.
/// 最后, 通过映射 Student 列表到 StudentDto 列表, 返回分页后的结果.
/// </summary>
public async Task<PagedResultDto<StudentDto>> GetListAsync(GetStudentListDto input)
{
if (input.Sorting.IsNullOrWhiteSpace())
{
input.Sorting = nameof(Student.Name);
}
var students = await _studentRepository.GetListAsync(
input.SkipCount,
input.MaxResultCount,
input.Sorting,
input.Filter
);
var totalCount = input.Filter == null
? await _studentRepository.CountAsync()
: await _studentRepository.CountAsync(
student => student.Name.Contains(input.Filter));
return new PagedResultDto<StudentDto>(
totalCount,
ObjectMapper.Map<List<Student>, List<StudentDto>>(students)
);
}
[Studentize(***Permissions.Students.Create)]
public async Task<StudentDto> CreateAsync(CreateStudentDto input)
{
var student = await _studentManager.CreateAsync(
input.Name,
input.Birthday,
input.Address
);
await _studentRepository.InsertAsync(student);
return ObjectMapper.Map<Student, StudentDto>(student);
}
[Studentize(***Permissions.Students.Edit)]
public async Task UpdateAsync(Guid id, UpdateStudentDto input)
{
var student = await _studentRepository.GetAsync(id);
if (student.Name != input.Name)
{
await _studentManager.ChangeNameAsync(student, input.Name);
}
student.Birthday = input.Birthday;
student.Address = input.Address;
await _studentRepository.UpdateAsync(student);
}
[Studentize(***Permissions.Students.Delete)]
public async Task DeleteAsync(Guid id)
{
await _studentRepository.DeleteAsync(id);
}
}
权限定义
在上述的CreateAsync,UpdateAsync,DeleteAsync方法中都Studentize
标签,用于为需要额外的权限。因此需要在***.Application.Contracts
项目中的***Permissions
类(Permissions文件夹)中添加Student的权限
public static class Student
{
public const string Default = GroupName + ".Student";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}
打开同一项目中***PermissionDefinitionProvider
,在Define
方法结尾处增加一下代码
var studentsPermission = storeGroup.AddPermission(
***Permissions.Students.Default, L("Permission:Students"));
studentsPermission.AddChild(
***Permissions.Students.Create, L("Permission:Students.Create"));
studentsPermission.AddChild(
***Permissions.Students.Edit, L("Permission:Students.Edit"));
studentsPermission.AddChild(
***Permissions.Students.Delete, L("Permission:Students.Delete"));
在***.Domain.Shared
项目中的Localization/***添加本地化。
对象映射
StudentAppService
使用 ObjectMapper
将 Student
对象 转换为 StudentDto
对象. 所以, 我们需要在 AutoMapper 配置中定义映射.
在***.Application
项目中的***ApplicationAutoMapperProfile
类中加入构造函数
CreateMap<Student, StudentDto>();
初始化时添加数据
***.Domain
项目的**DataSeederContributor
类
-
添加两个变量
private readonly IStudentRepository _studentRepository; private readonly StudentManager _studentManager;
-
构造方法修改为
public ***DataSeederContributor( IStudentRepository studentRepository, StudentManager studentManager) { _studentRepository = studentRepository; _studentManager = studentManager; }
-
SeedAsync
方法中添加if (await _studentRepository.GetCountAsync() <= 0) { await _studentRepository.InsertAsync( await _studentManager.CreateAsync( "张三", new DateTime(2020, 12, 21), "中国" ) ); }
执行初始化任务
运行**.DbMigrator控制台应用程序
界面
添加Razor组件
在***.Blazor
项目的Page文件夹中创建一个Students.razor的页面
@page "/students"
@using ***.Students
@using ***.Localization
@using Volo.Abp.AspNetCore.Components.Web
@inherits ***ComponentBase
@inject IStudentAppService StudentAppService
@inject AbpBlazorMessageLocalizerHelper<***Resource> LH
<Card>
<CardHeader>
<Row>
<Column ColumnSize="ColumnSize.Is6">
<h2>@L["Students"]</h2>
</Column>
<Column ColumnSize="ColumnSize.Is6">
<Paragraph Alignment="TextAlignment.Right">
@if (CanCreateStudent)
{
<Button Color="Color.Primary"
Clicked="OpenCreateStudentModal">
@L["NewStudent"]
</Button>
}
</Paragraph>
</Column>
</Row>
</CardHeader>
<CardBody>
<DataGrid TItem="StudentDto"
Data="StudentList"
ReadData="OnDataGridReadAsync"
TotalItems="TotalCount"
ShowPager="true"
PageSize="PageSize">
<DataGridColumns>
<DataGridColumn Width="150px"
TItem="StudentDto"
Field="@nameof(StudentDto.Id)"
Sortable="false"
Caption="@L["Actions"]">
<DisplayTemplate>
<Dropdown>
<DropdownToggle Color="Color.Primary">
@L["Actions"]
</DropdownToggle>
<DropdownMenu>
@if (CanEditStudent)
{
<DropdownItem Clicked="() => OpenEditStudentModal(context)">
@L["Edit"]
</DropdownItem>
}
@if (CanDeleteStudent)
{
<DropdownItem Clicked="() => DeleteStudentAsync(context)">
@L["Delete"]
</DropdownItem>
}
</DropdownMenu>
</Dropdown>
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="StudentDto"
Field="@nameof(StudentDto.Name)"
Caption="@L["Name"]"></DataGridColumn>
<DataGridColumn TItem="StudentDto"
Field="@nameof(StudentDto.Birthday)"
Caption="@L["Birthday"]">
<DisplayTemplate>
@context.Birthday.ToShortDateString()
</DisplayTemplate>
</DataGridColumn>
</DataGridColumns>
</DataGrid>
</CardBody>
</Card>
<Modal @ref="CreateStudentModal">
<ModalBackdrop />
<ModalContent IsCentered="true">
<Form>
<ModalHeader>
<ModalTitle>@L["NewStudent"]</ModalTitle>
<CloseButton Clicked="CloseCreateStudentModal" />
</ModalHeader>
<ModalBody>
<Validations @ref="@CreateValidationsRef" Model="@NewStudent" ValidateOnLoad="false">
<Validation MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@L["Name"]</FieldLabel>
<TextEdit @bind-Text="@NewStudent.Name">
<Feedback>
<ValidationError/>
</Feedback>
</TextEdit>
</Field>
</Validation>
<Field>
<FieldLabel>@L["Birthday"]</FieldLabel>
<DateEdit TValue="DateTime" @bind-Date="@NewStudent.Birthday"/>
</Field>
<Validation MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@L["Address"]</FieldLabel>
<MemoEdit Rows="5" @bind-Text="@NewStudent.Address">
<Feedback>
<ValidationError/>
</Feedback>
</MemoEdit>
</Field>
</Validation>
</Validations>
</ModalBody>
<ModalFooter>
<Button Color="Color.Secondary"
Clicked="CloseCreateStudentModal">
@L["Cancel"]
</Button>
<Button Color="Color.Primary"
Type="@ButtonType.Submit"
PreventDefaultOnSubmit="true"
Clicked="CreateStudentAsync">
@L["Save"]
</Button>
</ModalFooter>
</Form>
</ModalContent>
</Modal>
<Modal @ref="EditStudentModal">
<ModalBackdrop />
<ModalContent IsCentered="true">
<Form>
<ModalHeader>
<ModalTitle>@EditingStudent.Name</ModalTitle>
<CloseButton Clicked="CloseEditStudentModal" />
</ModalHeader>
<ModalBody>
<Validations @ref="@EditValidationsRef" Model="@EditingStudent" ValidateOnLoad="false">
<Validation MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@L["Name"]</FieldLabel>
<TextEdit @bind-Text="@EditingStudent.Name">
<Feedback>
<ValidationError/>
</Feedback>
</TextEdit>
</Field>
</Validation>
<Field>
<FieldLabel>@L["Birthday"]</FieldLabel>
<DateEdit TValue="DateTime" @bind-Date="@EditingStudent.Birthday"/>
</Field>
<Validation>
<Field>
<FieldLabel>@L["Address"]</FieldLabel>
<MemoEdit Rows="5" @bind-Text="@EditingStudent.Address">
<Feedback>
<ValidationError/>
</Feedback>
</MemoEdit>
</Field>
</Validation>
</Validations>
</ModalBody>
<ModalFooter>
<Button Color="Color.Secondary"
Clicked="CloseEditStudentModal">
@L["Cancel"]
</Button>
<Button Color="Color.Primary"
Type="@ButtonType.Submit"
PreventDefaultOnSubmit="true"
Clicked="UpdateStudentAsync">
@L["Save"]
</Button>
</ModalFooter>
</Form>
</ModalContent>
</Modal>
- 这些代码类似
Books.razor
, 除了不继承自AbpCrudPageBase
, 它使用自己的实现. - 注入
IStudentAppService
, 从UI使用服务器端的HTTP APIs . 我们可以直接注入应用服务接口并在 动态 C# HTTP API 客户端代理系统的帮助下像使用普通的方法一样使用它们, 动态 C# HTTP API 客户端代理系统会为我们调用REST API. 参考下面的Students
类获得使用方法. - 注入
IStudentizationService
检查 权限. - 注入
IObjectMapper
进行 对象到对象映射.
添加Razor.cs类
在Student.razor同级创建Student.razor.cs文件
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ***.Students;
using ***.Permissions;
using Blazorise;
using Blazorise.DataGrid;
using Microsoft.AspNetCore.Studentization;
using Volo.Abp.Application.Dtos;
namespace ***.Blazor.Pages
{
public partial class Students
{
private IReadOnlyList<StudentDto> StudentList { get; set; }
private int PageSize { get; } = LimitedResultRequestDto.DefaultMaxResultCount;
private int CurrentPage { get; set; }
private string CurrentSorting { get; set; }
private int TotalCount { get; set; }
private bool CanCreateStudent { get; set; }
private bool CanEditStudent { get; set; }
private bool CanDeleteStudent { get; set; }
private CreateStudentDto NewStudent { get; set; }
private Guid EditingStudentId { get; set; }
private UpdateStudentDto EditingStudent { get; set; }
private Modal CreateStudentModal { get; set; }
private Modal EditStudentModal { get; set; }
private Validations CreateValidationsRef;
private Validations EditValidationsRef;
public Students()
{
NewStudent = new CreateStudentDto();
EditingStudent = new UpdateStudentDto();
}
protected override async Task OnInitializedAsync()
{
await SetPermissionsAsync();
await GetStudentsAsync();
}
private async Task SetPermissionsAsync()
{
CanCreateStudent = await StudentizationService .IsGrantedAsync(***Permissions.Students.Create);
CanEditStudent = await StudentizationService .IsGrantedAsync(***Permissions.Students.Edit);
CanDeleteStudent = await StudentizationService .IsGrantedAsync(***Permissions.Students.Delete);
}
private async Task GetStudentsAsync()
{
var result = await StudentAppService.GetListAsync(
new GetStudentListDto
{
MaxResultCount = PageSize,
SkipCount = CurrentPage * PageSize,
Sorting = CurrentSorting
}
);
StudentList = result.Items;
TotalCount = (int)result.TotalCount;
}
private async Task OnDataGridReadAsync(DataGridReadDataEventArgs<StudentDto> e)
{
CurrentSorting = e.Columns
.Where(c => c.Direction != SortDirection.None)
.Select(c => c.Field + (c.Direction == SortDirection.Descending ? " DESC" : ""))
.JoinAsString(",");
CurrentPage = e.Page - 1;
await GetStudentsAsync();
await InvokeAsync(StateHasChanged);
}
private void OpenCreateStudentModal()
{
CreateValidationsRef.ClearAll();
NewStudent = new CreateStudentDto();
CreateStudentModal.Show();
}
private void CloseCreateStudentModal()
{
CreateStudentModal.Hide();
}
private void OpenEditStudentModal(StudentDto student)
{
EditValidationsRef.ClearAll();
EditingStudentId = student.Id;
EditingStudent = ObjectMapper.Map<StudentDto, UpdateStudentDto>(student);
EditStudentModal.Show();
}
private async Task DeleteStudentAsync(StudentDto student)
{
var confirmMessage = L["StudentDeletionConfirmationMessage", student.Name];
if (!await Message.Confirm(confirmMessage))
{
return;
}
await StudentAppService.DeleteAsync(student.Id);
await GetStudentsAsync();
}
private void CloseEditStudentModal()
{
EditStudentModal.Hide();
}
private async Task CreateStudentAsync()
{
if (CreateValidationsRef.ValidateAll())
{
await StudentAppService.CreateAsync(NewStudent);
await GetStudentsAsync();
CreateStudentModal.Hide();
}
}
private async Task UpdateStudentAsync()
{
if (EditValidationsRef.ValidateAll())
{
await StudentAppService.UpdateAsync(EditingStudentId, EditingStudent);
await GetStudentsAsync();
EditStudentModal.Hide();
}
}
}
}
添加映射
在***.Blazor
项目中的 ***AutoMapperProfile.cs
中加入
CreateMap<StudentDto, UpdateStudentDto>();
加入主菜单
***.Blozor
项目的Menus文件中的***.MenuContributor.cs
的ConfigureMainMenuAsync
方法结尾加入
if (await context.IsGrantedAsync(***Permissions.Students.Default))
{
***Menu.AddItem(new ApplicationMenuItem(
"Student",
l["Menu:Students"],
// 对应Razor组件的名称
url: "/students"
));
}