[Volo.Abp升級筆記]使用舊版Api規則替換RESTful Api以相容老程式

2023-03-29 12:00:35

@


Volo.Abp 設定應用層自動生成Controller,增刪查改服務(CrudAppService)將會以RESTful Api的方式生成對應的介面
(官方檔案),這與舊版本的Abp區別很大。RESTful固然好,雖然專案裡新的Api會逐步使用RESTful Api代替舊的,但在前後端分離的專案中已經定好的介面,往往需要相容之前的方式。

原理分析

舊版行為

應用層繼承於AsyncCrudAppService的類,在Web層呼叫CreateControllersForAppServices後,Abp框架將以預設的規則實現Controller,具體的規則如下:

  • Get: 如果方法名稱以GetList,GetAll或Get開頭.
  • Put: 如果方法名稱以Put或Update開頭.
  • Delete: 如果方法名稱以Delete或Remove開頭.
  • Post: 如果方法名稱以Create,Add,Insert或Post開頭.
  • Patch: 如果方法名稱以Patch開頭.
  • 其他情況, Post 為 預設方式.
  • 自動刪除'Async'字尾.

例子:

新版行為:
將會以RESTful Api的方式生成對應的介面,具體規則如下

服務方法名稱 HTTP Method Route
GetAsync(Guid id) GET /api/app/book/
GetListAsync() GET /api/app/book
CreateAsync(CreateBookDto input) POST /api/app/book
UpdateAsync(Guid id, UpdateBookDto input) PUT /api/app/book/
DeleteAsync(Guid id) DELETE /api/app/book/
GetEditorsAsync(Guid id) GET /api/app/book/{id}/editors
CreateEditorAsync(Guid id, BookEditorCreateDto input) POST /api/app/book/{id}/editor

例子

開始改造

更換基本類型

為了相容舊版Abp,先來還原增刪查改服務(CrudAppService)的方法簽名。
注意到

  1. Volo.Abp 中 UpdateAsync方法簽名已與舊版不同
  2. 舊版中的GetAllAsync方法,被GetListAsync所取代。

新建一個CrudAppServiceBase類繼承 CrudAppService。並重寫UpdateAsync和GetListAsync方法。

為了還原舊版的介面,將用private new關鍵字覆蓋掉 UpdateAsync,GetListAsync方法,並重新實現更改和查詢列表的功能




public abstract class CrudAppServiceBase<TEntity, TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
    : CrudAppService<TEntity, TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
    where TEntity : class, IEntity<TKey>
        where TGetOutputDto : IEntityDto<TKey>
where TGetListOutputDto : IEntityDto<TKey>
{
    protected CrudAppServiceBase(IRepository<TEntity, TKey> repository)
: base(repository)
    {

    }

    private new Task<TGetOutputDto> UpdateAsync(TKey id, TUpdateInput input)
    {
        return base.UpdateAsync(id, input);
    }
    private new Task<PagedResultDto<TGetListOutputDto>> GetListAsync(TGetListInput input)
    {
        return base.GetListAsync(input);
    }

    public virtual async Task<TGetOutputDto> UpdateAsync(TUpdateInput input)
    {
        await CheckUpdatePolicyAsync();
        var entity = await GetEntityByIdAsync((input as IEntityDto<TKey>).Id);
        MapToEntity(input, entity);
        await Repository.UpdateAsync(entity, autoSave: true);
        return await MapToGetOutputDtoAsync(entity);

    }
    public virtual Task<PagedResultDto<TGetListOutputDto>> GetAllAsync(TGetListInput input)
    {
        return this.GetListAsync(input);
    }   

}


基於擴充套件性考慮,我們可以像官方實現一樣做好型別複用

public abstract class CrudAppServiceBase<TEntity, TEntityDto, TKey>
    : CrudAppServiceBase<TEntity, TEntityDto, TKey, PagedAndSortedResultRequestDto>
    where TEntity : class, IEntity<TKey>
    where TEntityDto : IEntityDto<TKey>
{
    protected CrudAppServiceBase(IRepository<TEntity, TKey> repository)
        : base(repository)
    {

    }
}

public abstract class CrudAppServiceBase<TEntity, TEntityDto, TKey, TGetListInput>
    : CrudAppServiceBase<TEntity, TEntityDto, TKey, TGetListInput, TEntityDto>
    where TEntity : class, IEntity<TKey>
    where TEntityDto : IEntityDto<TKey>
{
    protected CrudAppServiceBase(IRepository<TEntity, TKey> repository)
        : base(repository)
    {

    }
}


public abstract class CrudAppServiceBase<TEntity, TEntityDto, TKey, TGetListInput, TCreateInput>
    : CrudAppServiceBase<TEntity, TEntityDto, TKey, TGetListInput, TCreateInput, TCreateInput>
    where TEntity : class, IEntity<TKey>
    where TEntityDto : IEntityDto<TKey>
{
    protected CrudAppServiceBase(IRepository<TEntity, TKey> repository)
        : base(repository)
    {

    }
}

public abstract class CrudAppServiceBase<TEntity, TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
: CrudAppServiceBase<TEntity, TEntityDto, TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
where TEntity : class, IEntity<TKey>
where TEntityDto : IEntityDto<TKey>
{
    protected CrudAppServiceBase(IRepository<TEntity, TKey> repository)
        : base(repository)
    {

    }

    protected override Task<TEntityDto> MapToGetListOutputDtoAsync(TEntity entity)
    {
        return MapToGetOutputDtoAsync(entity);
    }

    protected override TEntityDto MapToGetListOutputDto(TEntity entity)
    {
        return MapToGetOutputDto(entity);
    }
}

重寫介面

重寫增刪查改服務介面

public interface IBaseCrudAppService<TGetOutputDto, TGetListOutputDto, in TKey, in TGetListInput, in TCreateInput, in TUpdateInput>

    {
        Task<TGetOutputDto> GetAsync(TKey id);

        Task<PagedResultDto<TGetListOutputDto>> GetAllAsync(TGetListInput input);

        Task<TGetOutputDto> CreateAsync(TCreateInput input);

        Task<TGetOutputDto> UpdateAsync(TUpdateInput input);

        Task DeleteAsync(TKey id);

    }

基於擴充套件性考慮,我們可以像官方實現一樣做好型別複用

public interface IBaseCrudAppService<TEntityDto, in TKey>
    : IBaseCrudAppService<TEntityDto, TKey, PagedAndSortedResultRequestDto>
{

}

public interface IBaseCrudAppService<TEntityDto, in TKey, in TGetListInput>
    : IBaseCrudAppService<TEntityDto, TKey, TGetListInput, TEntityDto>
{

}

public interface IBaseCrudAppService<TEntityDto, in TKey, in TGetListInput, in TCreateInput>
    : IBaseCrudAppService<TEntityDto, TKey, TGetListInput, TCreateInput, TCreateInput>
{

}

public interface IBaseCrudAppService<TEntityDto, in TKey, in TGetListInput, in TCreateInput, in TUpdateInput>
    : IBaseCrudAppService<TEntityDto, TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
{

}

將應用服務介面IReservationAppService繼承於IBaseCrudAppService和IApplicationService

public interface IReservationAppService: IBaseCrudAppService<ReservationDto, long>, IApplicationService
{
    //除增刪查改業務的其他業務
}

建立應用服務類ReservationAppService,此時應用服務派生自CrudAppServiceBase,應用服務應該會完全實現介面

public class ReservationAppService : CrudAppServiceBase<Workflow.Reservation.Reservation, ReservationDto, long>, IReservationAppService
{
    ...
}

替換預設規則

Abp封裝了Controller自動生成規則,利用了Asp.Net MVC的約定介面IApplicationModelConvention,這一特性,所謂規則即Convention,AbpServiceConvention是此介面的實現類,在此類中約定了如何將應用層程式集增刪查改服務(CrudAppService)中的成員方法,按上述規則生成Controller。

規則的具體程式碼封裝在ConventionalRouteBuilder裡

既然是預設規則方式,我們就重寫一個自定義的Convention來代替它預設的那個。
假設有領域Workflow,在Web層中新建WorkflowServiceConvention,把原AbpServiceConvention類中的所有內容複製到這個類中

public class WorkflowServiceConvention : IAbpServiceConvention, ITransientDependency
{

}

將不需要用到的物件刪掉

// 刪除 protected IConventionalRouteBuilder ConventionalRouteBuilder { get; } 

重寫CreateAbpServiceAttributeRouteModel

protected virtual AttributeRouteModel CreateAbpServiceAttributeRouteModel(string rootPath, string controllerName, ActionModel action, string httpMethod, [CanBeNull] ConventionalControllerSetting configuration)
{
    return new AttributeRouteModel(
        new RouteAttribute(
                $"api/services/{rootPath}/{controllerName}/{action.ActionName}"
        )
    );
}

在Web層的Module檔案WorkflowHostModule中,新增WorkflowApplicationModule

Configure<AbpAspNetCoreMvcOptions>(options =>
{
    options
        .ConventionalControllers
        .Create(typeof(WorkflowApplicationModule).Assembly);
});

用WorkflowServiceConvention替換原始的AbpServiceConvention實現。

Configure<MvcOptions>(options =>
{
    options.Conventions.RemoveAt(0);
    options.Conventions.Add(convention.Value);
});

在微服務架構中的問題

Asp.Net MVC在微服務的閘道器層中無法通過僅參照應用層方法的介面,生成Controller,即便改寫 ControllerFeatureProvider, 還是需要參照實現類,這些實現類在應用層中。
但閘道器僅僅依賴定義層,若要拿到實現類,將改變微服務架構的依賴關係。

在官方的微服務範例中,也沒有用Controller的自動生成,在這個issue中作者也給出瞭解答
https://github.com/abpframework/abp/issues/1731

因此如果想達到目的,只能用重寫controller基礎類別的方式了,這個方式好處在於簡單好用,可讀性和可維護性高,缺陷就是每寫一個應用層類,需要寫一個對應的Controller類,但在專案不多用CV大法還是可以接受的。

新建WorkflowController並繼承於AbpControllerBase,並建立增刪查改(Curd)的終結點路由,通過呼叫ITAppService的方法,實現各業務功能

public abstract class WorkflowController<ITAppService, TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
    : AbpControllerBase
where ITAppService : IBaseCrudAppService<TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
        where TGetOutputDto : IEntityDto<TKey>
where TGetListOutputDto : IEntityDto<TKey>
{


    protected WorkflowController()
    {
        LocalizationResource = typeof(WorkflowResource);
    }


    private readonly ITAppService _recipeAppService;

    public WorkflowController(ITAppService recipeAppService)
    {
        _recipeAppService = recipeAppService;
    }

    [HttpPost]
    [Route("Create")]

    public async Task<TGetOutputDto> CreateAsync(TCreateInput input)
    {
        return await _recipeAppService.CreateAsync(input);
    }

    [HttpDelete]
    [Route("Delete")]
    public async Task DeleteAsync(TKey id)
    {
        await _recipeAppService.DeleteAsync(id);
    }

    [HttpGet]
    [Route("GetAll")]
    public async Task<PagedResultDto<TGetListOutputDto>> GetAllAsync(TGetListInput input)
    {
        return await _recipeAppService.GetAllAsync(input);
    }

    [HttpGet]
    [Route("Get")]
    public async Task<TGetOutputDto> GetAsync(TKey id)
    {
        return await _recipeAppService.GetAsync(id);
    }

    [HttpPut]
    [Route("Update")]
    public async Task<TGetOutputDto> UpdateAsync(TUpdateInput input)
    {
        return await _recipeAppService.UpdateAsync(input);
    }
}

基於擴充套件性考慮,我們可以做好型別複用

public abstract class WorkflowController<ITAppService, TEntityDto, TKey>
      : WorkflowController<ITAppService, TEntityDto, TKey, PagedAndSortedResultRequestDto>
      where ITAppService : IBaseCrudAppService<TEntityDto, TKey>
      where TEntityDto : IEntityDto<TKey>
{
    protected WorkflowController(ITAppService appService)
        : base(appService)
    {

    }
}

public abstract class WorkflowController<ITAppService, TEntityDto, TKey, TGetListInput>
    : WorkflowController<ITAppService, TEntityDto, TKey, TGetListInput, TEntityDto>
    where ITAppService : IBaseCrudAppService<TEntityDto, TKey, TGetListInput>
    where TEntityDto : IEntityDto<TKey>
{
    protected WorkflowController(ITAppService appService)
        : base(appService)
    {

    }
}


public abstract class WorkflowController<ITAppService, TEntityDto, TKey, TGetListInput, TCreateInput>
 : WorkflowController<ITAppService, TEntityDto, TKey, TGetListInput, TCreateInput, TCreateInput>
 where ITAppService : IBaseCrudAppService<TEntityDto, TKey, TGetListInput, TCreateInput>
 where TEntityDto : IEntityDto<TKey>
{
    protected WorkflowController(ITAppService appService)
        : base(appService)
    {

    }
}

public abstract class WorkflowController<ITAppService, TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
: WorkflowController<ITAppService, TEntityDto, TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
where ITAppService : IBaseCrudAppService<TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
where TEntityDto : IEntityDto<TKey>
{
    protected WorkflowController(ITAppService appService)
        : base(appService)
    {

    }

}


建立實際的Controller,定義Area名稱和Controller路由「api/Workflow/reservation」

此時Controller派生自WorkflowController,應用服務應該會完全實現介面

[Area(WorkflowRemoteServiceConsts.ModuleName)]
[RemoteService(Name = WorkflowRemoteServiceConsts.RemoteServiceName)]
[Route("api/Workflow/reservation")]
public class ReservationController : WorkflowController<IReservationAppService, ReservationDto,long>, IReservationAppService
{
    private readonly IReservationAppService _reservationAppService;

    public ReservationController(IReservationAppService reservationAppService):base(reservationAppService)
    {
        _reservationAppService = reservationAppService;
    }
}

執行程式,我們將得到一箇舊版的介面

每次為新的應用服務類建立Controller,只需要新建一個派生自WorkflowController類的Controller,並指定一個應用服務類物件。就完成了,不需要自己寫一大堆的控制器方法。