zoukankan      html  css  js  c++  java
  • [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="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

  • 相关阅读:
    ASP.NET中常用的优化性能的方法
    把WinDbg集成到Visual Studio中
    提高ASP.net的性能
    Msn Library
    [转帖]OutOfMemoryException问题的处理
    一完美的关于请求的目录不存在而需要url重写的解决方案!
    在 ASP.NET 中执行 URL 重写
    转 内存不断上升处理方法
    IIS 6 通配符应用映射和HttpHandler配置
    Java开源
  • 原文地址:https://www.cnblogs.com/personball/p/10138185.html
Copyright © 2011-2022 走看看