[2018-12-18]ABP中的AsyncCrudAppService介紹



前言

自從寫完上次略長的《用ABP入門DDD》后,針對ABP框架的項目模板初始化,我寫了個命令行工具Abp-CLI,其中子命令abplus init可以從github拉取項目模板以初始化項目。自然而然的,又去處理了aspnetboilerplate/module-zero-core-template這個項目模板庫當中的vue項目模板,解決以前發現的,又貌似一直沒人修復的幾個問題PR362,PR366,PR367

在更新vue項目模板的示例代碼時,感覺有必要講解下ABP中的AsyncCrudAppService<>怎么用。

跟我做

先自賣自誇一下,只要你本地有裝dotnet環境,就可以跟着我一步一步來做。

安裝dotnet core全局工具:AbpTools

可以在任意目錄下,打開powershell命令窗口(按住Shift鍵的同時鼠標右鍵點擊目錄空白處,可以看到右鍵菜單在此處打開Powershell窗口),執行以下命令安裝AbpTools:

dotnet tool install -g AbpTools

如果安裝成功,會提示你已經可以使用abplus命令了。

可以通過以下命令查看已安裝了哪些dotnet core全局工具:

PS$>dotnet tool list -g
包 ID          版本         命令
-------------------------------
abptools      1.0.4      abplus

目前是1.0.4版。

初始化項目Personball.CrudDemo

在powershell命令窗口中選擇一個放代碼的目錄(用cd命令),執行以下命令初始化項目:

abplus init Personball.CrudDemo -T personball/module-zero-core-template@v4.2.2

由於撰寫本文時默認的項目模板庫aspnetboilerplate/module-zero-core-template雖然合並了上述的PR362、PR366、PR367,但還沒Release下一版本,所以暫時通過-T指定使用我自己修復過的vue項目模板。

運行起來看看

先運行 Api Host:

  1. 用VS2017打開Personball.CrudDemo/aspnet-core目錄下的Personball.CrudDemo.sln
  2. 右鍵點擊Personball.CrudDemo.Web.Mvc,移除
  3. 右鍵點擊Personball.CrudDemo.Web.Host,設為啟動項
  4. 在VS2017的視圖菜單中選擇SQL Server 對象資源管理器,展開SQL Server>(localdb)\...(localdb特定版本的實例名),右鍵數據庫,選擇添加新數據庫,數據庫名稱輸入CrudDemoDb,確認。
  5. 右鍵CrudDemoDb數據,點擊屬性,找到連接字符串,復制下來,粘貼替換Personball.CrudDemo.Web.Host項目的appsettings.json配置文件的ConnectionStrings配置下的Default值。
  6. 打開程序包管理器控制台,默認項目選Personball.CrudDemo.EntityFrameworkCore,輸入Update-Database
  7. 按F5運行

繼續運行前端vue項目(需要nodejs和npm):

  1. 用VSCode打開Personball.CrudDemo/vue目錄
  2. 在VsCode的終端窗口中運行yarn install
  3. Install完成后,運行yarn serve

先進后台,體驗下功能

  1. 打開瀏覽器,輸入剛才yarn serve提示的訪問地址,默認是http://localhost:8080
  2. 輸入默認賬號admin,密碼123qwe,租戶空着不選
  3. 登陸成功后,展開左側菜單,選擇用戶

這樣我們就到了后台的用戶列表,可以先試試輸入查詢條件,試一下列表查詢功能。

加斷點,再試一下

切換到VS2017,我們加個斷點

  1. 展開Personball.CrudDemo.Application項目,展開Users目錄,找到UserAppService
  2. 找到CreateFilteredQuery方法,在return語句的地方加個斷點,再到后台里試一下用戶列表查詢。

接下來看代碼

以后端代碼UserAppService和前端vue模板中的src/views/setting/user/user.vue為例:

后台接收列表查詢參數使用的是PagedUserResultRequestDto,繼承自PagedResultRequestDto,加上了UI界面所需的一些自定義查詢條件屬性:

public class PagedUserResultRequestDto : PagedResultRequestDto
{
    public string UserName { get; set; }
    public string Name { get; set; }
    public bool? IsActive { get; set; }
    public DateTimeOffset? From { get; set; }//javascript date within timezone
    public DateTimeOffset? To { get; set; }//javascript date within timezone
}

而前端在user.vue文件中,使用PageUserRequest和后端的DTO對應:

class  PageUserRequest extends PageRequest{
    userName:string;
    name:string;
    isActive:boolean=null;//nullable
    from:Date;
    to:Date;
}

這里可以直接用PageUserRequest類型的前端變量做UI控件綁定:

    pagerequest:PageUserRequest=new PageUserRequest();
    creationTime:Date[]=[];//時間范圍控件的值綁定另外處理
<FormItem :label="L('UserName')+':'" style="width:100%">
    <Input v-model="pagerequest.userName"></Input>
</FormItem>

對於復雜的,比如時間范圍控件(上面的creationTime),再另外處理:

 async getpage(){
    //set page parameters
    this.pagerequest.maxResultCount=this.pageSize;
    this.pagerequest.skipCount=(this.currentPage-1)*this.pageSize;
    
    //filters
    if (this.creationTime.length>0) {
        this.pagerequest.from=this.creationTime[0];
    }

    if (this.creationTime.length>1) {
        this.pagerequest.to=this.creationTime[1];
    }

    await this.$store.dispatch({
        type:'user/getAll',
        data:this.pagerequest
    })
}

前端集成了typescript的vue代碼的用法基本介紹到這,主要是前一版的vue項目模板中出現了在前端代碼里組裝where條件的情況。所以說明下,以免后端真的去處理where字符串可能引起SQL注入問題。

我們繼續回到后端代碼。

AsyncCrudAppService說明

ABP作為開發框架,非常優秀的一個地方,就是作者對DRY的追求。
對於CRUD這種通用功能,必須要有一個解決方案,這就有了泛型版的應用服務基類CrudAppService<>

我們先看下這個基類上有哪些成員:

namespace Abp.Application.Services
{
    public abstract class CrudAppServiceBase<TEntity, TEntityDto, TPrimaryKey, TGetAllInput, TCreateInput, TUpdateInput> :
    ApplicationService
        where TEntity : class, IEntity<TPrimaryKey>
        where TEntityDto : IEntityDto<TPrimaryKey>
        where TUpdateInput : IEntityDto<TPrimaryKey>
    {
        protected readonly IRepository<TEntity, TPrimaryKey> Repository;

        protected CrudAppServiceBase(IRepository<TEntity, TPrimaryKey> repository);

        protected virtual string CreatePermissionName { get; set; }
        protected virtual string GetAllPermissionName { get; set; }
        protected virtual string GetPermissionName { get; set; }
        protected virtual string UpdatePermissionName { get; set; }
        protected virtual string DeletePermissionName { get; set; }

        protected virtual IQueryable<TEntity> ApplyPaging(IQueryable<TEntity> query, TGetAllInput input);
        protected virtual IQueryable<TEntity> ApplySorting(IQueryable<TEntity> query, TGetAllInput input);
        protected virtual void CheckCreatePermission();
        protected virtual void CheckDeletePermission();
        protected virtual void CheckGetAllPermission();
        protected virtual void CheckGetPermission();
        protected virtual void CheckPermission(string permissionName);
        protected virtual void CheckUpdatePermission();
        protected virtual IQueryable<TEntity> CreateFilteredQuery(TGetAllInput input);
        protected virtual void MapToEntity(TUpdateInput updateInput, TEntity entity);
        protected virtual TEntity MapToEntity(TCreateInput createInput);
        protected virtual TEntityDto MapToEntityDto(TEntity entity);
    }
}

其中的泛型參數,依次說明如下:

  • TEntity:CRUD操作對應的實體類
  • TEntityDto:GetAll方法返回的實體DTO
  • TPrimaryKey:實體的主鍵
  • TGetAllInput:GetAll方法接收的輸入參數
  • TCreateInput:Create方法接收的輸入參數
  • TUpdateInput:Update方法接收的輸入參數

從上面我們還可以看到有關於權限(xxxPermissionName屬性和CheckxxxPermission方法),關於分頁(ApplyPaging),關於排序(ApplySorting),關於查詢條件(CreateFilteredQuery),關於對象映射(MapToxxx),所有CRUD涉及的環節都提供了擴展點(方法是virtual,可以override)。

所以對於單頁后台來說,基於CrudAppServiceBase實現CRUD功能非常簡便,而且很容易擴展定制。

以前面說的UserAppService為例,它繼承AsyncCrudAppService<>(AsyncCrudAppService繼承了上面的CrudAppServiceBase,提供了異步版本的CRUD接口實現)。除了IUserAppService中額外定義的兩個方法:

Task<ListResultDto<RoleDto>> GetRoles();

Task ChangeLanguage(ChangeUserLanguageDto input);

其他方法都是基於AsyncCrudAppService<>的可擴展點進行自定義以滿足需求。
如果只是一個非常簡單的純數據實體(User還是有不少邏輯的),這個AppService還可以更簡單:

[AbpAuthorize]
public class ArticleAppService : AsyncCrudAppService<Article, ArticleDto, int, PagedArticleResultRequestDto, CreateArticleDto, ArticleDto>, IArticleAppService
{
    public ArticleAppService(IRepository<Article, int> repository) : base(repository)
    {
        LocalizationSourceName = JsxConsts.LocalizationSourceName;
    }

    protected override IQueryable<Article> CreateFilteredQuery(PagedArticleResultRequestDto input)
    {
        return Repository.GetAll()
            .WhereIf(input.Category.HasValue, a => a.Category == input.Category)
            .WhereIf(!input.Keyword.IsNullOrWhiteSpace(), a => a.Title.Contains(input.Keyword) || a.Content.Contains(input.Keyword))
            .WhereIf(input.From.HasValue, b => b.CreationTime >= input.From.Value.LocalDateTime)
            .WhereIf(input.To.HasValue, b => b.CreationTime <= input.To.Value.LocalDateTime);
    }
}

類似這個ArticleAppService,只要定制下CreateFilteredQuery中的查詢過濾條件,其他功能代碼都免了,而CRUD的接口都是完整可以用的。

不說代碼生成器,只要自定義一個代碼片段來快速產出這個ArticleAppService,就可以節省很多的敲鍵盤時間,效率是非常高的,關鍵是省事——DRY,Don't Repeat Yourself。

再回到UserAppService中的PagedUserResultRequestDto

這個DTO就是泛型參數中的TGetAllInput。通過繼承PagedResultRequestDto,在AsyncCrudAppService基類中的各個涉及方法的簽名里以OOP多態方式傳遞該參數完全沒有問題。

TEntityDtoTUpdateInput有時候可以共用一個DTO,只要定制好映射關系,問題一般不大。例如Users/Dto/UserMapProfile中:

CreateMap<UserDto, User>()//use UserDto as TUpdateInput
    .ForMember(x => x.Roles, opt => opt.Ignore())
    .ForMember(x => x.CreationTime, opt => opt.Ignore())
    .ForMember(x => x.LastLoginTime, opt => opt.Ignore());

最后,關於vue項目模板,提兩個注意點

1.時區問題

不管什么前端框架,可能都會遇到前端提交的JavaScript中的Date類型對象是帶時區的,或者默認是UTC時間。

這個問題,建議在接口接收參數的時候用DateTimeOffset類型接收,再通過其屬性LocalDateTime轉為服務器本地時間使用,當然如果你數據庫直接存了帶時區的時間,那連轉換都免了。

2.iview框架版本問題

原先打算在PR366中降iview的主版本號到^2.13.1來修復yarn serve編譯錯誤的問題,后來發現改成~3.0.0也行得通。

但是本地demo跑起來時發現頁簽的選中樣式還是有點問題,懶得改css的話,可以直接降到^2.13.1


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM