前言
自從寫完上次略長的《用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:
- 用VS2017打開
Personball.CrudDemo/aspnet-core目錄下的Personball.CrudDemo.sln - 右鍵點擊
Personball.CrudDemo.Web.Mvc,移除 - 右鍵點擊
Personball.CrudDemo.Web.Host,設為啟動項 - 在VS2017的視圖菜單中選擇SQL Server 對象資源管理器,展開
SQL Server>(localdb)\...(localdb特定版本的實例名),右鍵數據庫,選擇添加新數據庫,數據庫名稱輸入CrudDemoDb,確認。 - 右鍵
CrudDemoDb數據,點擊屬性,找到連接字符串,復制下來,粘貼替換Personball.CrudDemo.Web.Host項目的appsettings.json配置文件的ConnectionStrings配置下的Default值。 - 打開程序包管理器控制台,默認項目選
Personball.CrudDemo.EntityFrameworkCore,輸入Update-Database - 按F5運行
繼續運行前端vue項目(需要nodejs和npm):
- 用VSCode打開
Personball.CrudDemo/vue目錄 - 在VsCode的終端窗口中運行
yarn install - Install完成后,運行
yarn serve
先進后台,體驗下功能
- 打開瀏覽器,輸入剛才
yarn serve提示的訪問地址,默認是http://localhost:8080 - 輸入默認賬號admin,密碼123qwe,租戶空着不選
- 登陸成功后,展開左側菜單,選擇用戶
這樣我們就到了后台的用戶列表,可以先試試輸入查詢條件,試一下列表查詢功能。
加斷點,再試一下
切換到VS2017,我們加個斷點
- 展開Personball.CrudDemo.Application項目,展開Users目錄,找到
UserAppService - 找到
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多態方式傳遞該參數完全沒有問題。
而TEntityDto和TUpdateInput有時候可以共用一個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。
