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

superXX07發表於2018-12-18

前言

自從寫完上次略長的《用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

轉載於:https://www.cnblogs.com/personball/p/10138185.html

相關文章