应用层构成
- 应用服务(Application Service) :
- 数据传输对象(DTO)
ApplicationService
在ABP中应用程序服务应该实现IApplicationService接口. 推荐每个应用程序服务创建一个接口
public interface IBookAppService : IApplicationService
{
Task CreateAsync(CreateBookDto input);
}
实现
public class BookAppService : ApplicationService, IBookAppService
{
private readonly IRepository<Book, Guid> _bookRepository;
public BookAppService(IRepository<Book, Guid> bookRepository)
{
_bookRepository = bookRepository;
}
public async Task CreateAsync(CreateBookDto input)
{
var book = new Book(
GuidGenerator.Create(),
input.Name,
input.Type,
input.Price
);
await _bookRepository.InsertAsync(book);
}
}
说明
- ApplicationService提供了应用服务常见的需求(比如本示例服务中使用的GuidGenerator).
- 应用服务的生命周期是transient的,它们会自动注册到依赖注入系统
验证、授权
可以使用标准数据注释属性或自定义验证方法来执行验证
可以对应用程序服务方法使用声明性和命令式授权.
CRUD应用服务
public interface IBookAppService :
ICrudAppService< //Defines CRUD methods
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books
CreateCreateBookDto, //Used to create a new book
CreateUpdateBookDto> //Used to update a book
{
}
public interface IIronTappingAppService :
ICrudAppService<
IronTappingPlanDto,
Guid,
GetIronTappingQueryDto,
CreateIronTrappingPlanDto,
UpdateIronTappingPlanDto>, IApplicationService{}
ICrudAppService
ICrudAppService 有泛型参数来获取实体的主键类型和CRUD操作的DTO类型(它不获取实体类型,因为实体类型未向客户端公开使用此接口)
里面定义了这些方法
Task<TEntityDto> GetAsync(TKey id);
Task<PagedResultDto<TEntityDto>> GetListAsync(TGetListInput input);
Task<TEntityDto> CreateAsync(TCreateInput input);
Task<TEntityDto> UpdateAsync(TKey id, TUpdateInput input);
Task DeleteAsync(TKey id);
CrudAppService
CrudAppService<TEntity, TEntityDto, TKey>;
CrudAppService<TEntity, TEntityDto, TKey, TGetListInput>;
CrudAppService<TEntity, TEntityDto, TKey, TGetListInput, TCreateInput>;
CrudAppService<TEntity, TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>{
override Task<TEntityDto> MapToGetListOutputDtoAsync(TEntity entity);
override TEntityDto MapToGetListOutputDto(TEntity entity);
}
CrudAppService<TEntity, TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput>{
Repository;
DeleteByIdAsync(TKey id);
GetEntityByIdAsync(TKey id);
MapToEntity(TUpdateInput updateInput, TEntity entity);
ApplyDefaultSorting(IQueryable<TEntity> query);
}
CreateFilteredQueryAsync
就是调用GetQueryableAsync
protected virtual async Task<IQueryable<TEntity>> CreateFilteredQueryAsync(TGetListInput input)
{
return await ReadOnlyRepository.GetQueryableAsync();
}
CreateAsync
public virtual async Task<TGetOutputDto> CreateAsync(TCreateInput input)
{
await CheckCreatePolicyAsync();
var entity = await MapToEntityAsync(input);
TryToSetTenantId(entity);
await Repository.InsertAsync(entity, autoSave: true);
return await MapToGetOutputDtoAsync(entity);
}
UpdateAsync
public virtual async Task<TGetOutputDto> UpdateAsync(TKey id, TUpdateInput input)
{
await CheckUpdatePolicyAsync();
var entity = await GetEntityByIdAsync(id);
//TODO: Check if input has id different than given id and normalize if it's default value, throw ex otherwise
await MapToEntityAsync(input, entity);
await Repository.UpdateAsync(entity, autoSave: true);
return await MapToGetOutputDtoAsync(entity);
}
GetListAsync
public override async Task<PagedResultDto<HeatScheduleExceptionDto>> GetListAsync(HeatScheduleExceptionGetListInput input)
{
await CheckGetListPolicyAsync();
var query = await CreateFilteredQueryAsync(input);
var totalCount = await AsyncExecuter.CountAsync(query);
query = ApplySorting(query, input);
query = ApplyPaging(query, input);
var entities = await AsyncExecuter.ToListAsync(query);
var entityDtos = await MapToGetListOutputDtosAsync(entities);
return new PagedResultDto<HeatScheduleExceptionDto>(
totalCount,
entityDtos
);
}
ApplySorting
protected virtual IQueryable<TEntity> ApplySorting(IQueryable<TEntity> query, TGetListInput input)
{
//Try to sort query if available
if (input is ISortedResultRequest sortInput)
{
if (!sortInput.Sorting.IsNullOrWhiteSpace())
{
return query.OrderBy(sortInput.Sorting);
}
}
//IQueryable.Task requires sorting, so we should sort if Take will be used.
if (input is ILimitedResultRequest)
{
return ApplyDefaultSorting(query);
}
//No sorting
return query;
}
ApplyPaging
protected virtual IQueryable<TEntity> ApplyPaging(IQueryable<TEntity> query, TGetListInput input)
{
//Try to use paging if available
if (input is IPagedResultRequest pagedInput)
{
return query.PageBy(pagedInput);
}
//Try to limit query result if available
if (input is ILimitedResultRequest limitedInput)
{
return query.Take(limitedInput.MaxResultCount);
}
//No paging
return query;
}
一个Dto更新父子表的坑
假设使用这样的Dto来更新父子表,父表更新,子表中的数据全部删了重建
public class UpdateApiDefintionDto
{
//父表字段...
//子表
public ICollection<CreateApiParameterDto> Parameters { get; set; }
}
会遇到一个问题,子表实体Map后缺少ID,需要使用ABP提供的扩展方法给ID赋值,如下
//Map完后,关键是需要SetId
var parameters = _objectMapper.Map<ICollection<CreateApiParameterDto>, List<ApiParameter>>(input.Parameters);
parameters.ForEach(parameter =>
{
EntityHelper.TrySetId(
parameter,
() => GuidGenerator.Create(),
true
);
});
复合主键
CrudAppService 要求你的实体拥有一个Id属性做为主键. 如果你使用的是复合主键,要使用AbstractKeyCrudAppService
public class DistrictAppService
//DistrictKey是创建的复合主键类
: AbstractKeyCrudAppService<District, DistrictDto, DistrictKey>
{
public DistrictAppService(IRepository<District> repository)
: base(repository)
{
}
//自己实现 DeleteByIdAsync 和 GetEntityByIdAsync 方法:
//CityId 和 Name 做为复合主键
protected async override Task DeleteByIdAsync(DistrictKey id)
{
await Repository.DeleteAsync(d => d.CityId == id.CityId && d.Name == id.Name);
}
protected async override Task<District> GetEntityByIdAsync(DistrictKey id)
{
return await AsyncQueryableExecuter.FirstOrDefaultAsync(
Repository.Where(d => d.CityId == id.CityId && d.Name == id.Name)
);
}
}
创建一个类作为复合主键
public class DistrictKey
{
public Guid CityId { get; set; }
public string Name { get; set; }
}
DTO
为什么使用DTO
- 领域层的抽象
- 数据隐藏
- 序列化和延迟加载问题:如果直接使用实体,实体之间存在相互引用,可能意外地序列化很多实体类
DTO的设计原则
- 可序列化。如果你有另一个带参数的构造函数,建议使用空(无参数)的公共构造函数.
- 如果在所有情况下填充所有属性,就可以重用输出DTO.
输入DTO的原则
- 只定义用例所需的属性. 不要包含不用于用例的属性,这样做会使开发人员感到困惑
- 不要在不同的应用程序服务方法之间重用输入DTO. 因为不同的用例将需要和使用DTO的不同属性,从而导致某些属性在某些情况下没有使用,这使得理解和使用服务更加困难,并在将来导致潜在的错误。如不要使用CreateUpdate...Dto
标准接口和基类
IEntityDto 是一个只定义 Id 属性的简单接口. 你可以实现它或从 EntityDto 继承
EntityDto、IEntityDto:只有Id
using System;
using Volo.Abp.Application.Dtos;
namespace AbpDemo
{
public class ProductDto : EntityDto<Guid>
{
public string Name { get; set; }
//...
}
}
审计DTO
- CreationAuditedEntityDto
- CreationAuditedEntityWithUserDto
- AuditedEntityDto
- AuditedEntityWithUserDto
- FullAuditedEntityDto
- FullAuditedEntityWithUserDto
可扩展的DTO
- ExtensibleObject 实现 IHasExtraProperties (其它类继承这个类).
- ExtensibleEntityDto
- ExtensibleCreationAuditedEntityDto
- ExtensibleCreationAuditedEntityWithUserDto
- ExtensibleAuditedEntityDto
- ExtensibleAuditedEntityWithUserDto
- ExtensibleFullAuditedEntityDto
- ExtensibleFullAuditedEntityWithUserDto
列表结果
IListResult 接口和 ListResultDto ,里面定义了
public interface IListResult<T>
{
IReadOnlyList<T> Items { get; set; }
}
示例
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace AbpDemo
{
public class ProductAppService : ApplicationService, IProductAppService
{
private readonly IRepository<Product, Guid> _productRepository;
public ProductAppService(IRepository<Product, Guid> productRepository)
{
_productRepository = productRepository;
}
public async Task<ListResultDto<ProductDto>> GetListAsync()
{
//Get entities from the repository
List<Product> products = await _productRepository.GetListAsync();
//Map entities to DTOs
List<ProductDto> productDtos =
ObjectMapper.Map<List<Product>, List<ProductDto>>(products);
//Return the result
//这个ListResultDto接口实现IListResult,其实就是将结果放在Items属性中,然后还可以添加别的属性
//这样的好处是可以添加更多属性
return new ListResultDto<ProductDto>(productDtos);
}
}
}
分页排序列表结果
输入类型
- ILimitedResultRequest: 定义 MaxResultCount(int) 属性从服务器请求指定数量的结果.
- IPagedResultRequest: 继承自 ILimitedResultRequest (所以它具有 MaxResultCount 属性)并且定义了 SkipCount (int)用于请求服务器的分页结果时跳过计数.
- ISortedResultRequest: 定义 Sorting (string)属性以请求服务器的排序结果. 排序值可以是“名称”,"Name", "Name DESC", "Name ASC, Age DESC"... 等.
- IPagedAndSortedResultRequest 继承自 IPagedResultRequest 和 ISortedResultRequest,所以它有上述所有属性.
建议你继承以下基类DTO类之一,而不是手动实现接口:
- LimitedResultRequestDto 实现了 ILimitedResultRequest.
- PagedResultRequestDto 实现了 IPagedResultRequest (和继承自 LimitedResultRequestDto).
- PagedAndSortedResultRequestDto 实现了 IPagedAndSortedResultRequest (和继承自 PagedResultRequestDto).
输出类型
- IHasTotalCount 定义 TotalCount(long)属性以在分页的情况下返回记录的总数.
- IPagedResult 集成自 IListResult 和 IHasTotalCount, 所以它有 Items 和 TotalCount 属性.
建议你继承以下基类DTO类之一,而不是手动实现接口:
- PagedResultDto 继承自 ListResultDto 和实现了 IPagedResult.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace AbpDemo
{
public class ProductAppService : ApplicationService, IProductAppService
{
private readonly IRepository<Product, Guid> _productRepository;
public ProductAppService(IRepository<Product, Guid> productRepository)
{
_productRepository = productRepository;
}
//传入PagedAndSortedResultRequestDto
//响应PagedResultDto
public async Task<PagedResultDto<ProductDto>> GetListAsync(
PagedAndSortedResultRequestDto input)
{
//排序,这个OrderBy不清楚是否来自EF Core
var query = _productRepository
.OrderBy(input.Sorting);
//Get total count from the repository
var totalCount = await query.CountAsync();
//Get entities from the repository
List<Product> products = await query
.Skip(input.SkipCount)
.Take(input.MaxResultCount)
.ToListAsync();
//PageBy扩展方法,与IPagedResultRequest兼容,可用于代替 Skip + Take调用
var query = _productRepository
.OrderBy(input.Sorting)
.PageBy(input);
//Map entities to DTOs
List<ProductDto> productDtos =
ObjectMapper.Map<List<Product>, List<ProductDto>>(products);
//构造函数,传入总条数,返回的序列数据
return new PagedResultDto<ProductDto>(totalCount, productDtos);
}
}
}
最大返回数量
- 如果客户端未设置 MaxResultCount,则假定为10(默认页面大小). 可以通过设置 LimitedResultRequestDto.DefaultMaxResultCoun t静态属性来更改此值.
- 如果客户端发送的 MaxResultCount 大于 1,000 ,则会产生验证错误. 保护服务器免受滥用服务很重要. 如果需要可以通过设置 LimitedResultRequestDto.MaxMaxResultCount 静态属性来更改此值.