ABP应用层构件分析

220 阅读6分钟

应用层构成

  • 应用服务(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 静态属性来更改此值.