在实际项目开发中,我们可能会碰到各种各样的项目环境,有些项目需要一个大而全的整体框架来支撑开发,有些中小项目这需要一些简单便捷的系统框架灵活开发。目前大型一点的框架,可以采用ABP或者ABPVNext的框架,两者整体思路和基础设计类似,不过ABP侧重于一个独立完整的项目框架,开发的时候统一整合处理;而ABPVNext则是以微服务架构为基础,各个模块独立开发,既可以整合在一个项目中,也可以以微服务进行单独发布,并统一通过网关处理进行交流。
不管ABP或者ABPVNext框架,都集合了.NETCORE领域众多技术为一体,并且基础类设计上,错综复杂,关系较多,因此开发学习有一定的门槛,中小型项目应用起来有一定的费劲之处。本系列随笔介绍底层利用SqlSugar来做ORM数据访问模块,设计一个简单便捷一点的框架,本篇从基础开始介绍一些框架内容,参照一些ABP/ABPVNext中的一些类库处理,来承载类似条件分页信息,查询条件处理等处理细节。
1、基于SqlSugar开发框架的架构设计
主要的设计模块场景如下所示。
为了避免像ABPVNext框架那样分散几十个项目,我们尽可能聚合内容放在一个项目里面。
1)其中一些常用的类库,以及SqlSugar框架的基类放在框架公用模块里面。
2)Winform开发相关的基础界面以及通用组件内容,放在基础Winform界面库BaseUIDx项目中。
3)基础核心数据模块SugarProjectCo,主要就是开发业务所需的数据处理和业务逻辑的项目,为了方便,我们区分Interface、Modal、Service三个目录来放置不同的内容,其中Modal是SqlSugar的映射实体,Interface是定义访问接口,Service是提供具体的数据操作实现。其中Service里面一些框架基类和接口定义,统一也放在公用类库里面。
4)Winform应用模块,主要就是针对业务开发的WInform界面应用,而WInform开发为了方便,也会将一些基础组件和基类放在了BaseUIDx的Winform专用的界面库里面。
5)WebAPI项目采用基于.netCo6的项目开发,通过调用SugarProjectCo实现相关控制器API的发布,并整合Swagger发布接口,供其他前端界面应用进行调用。
6)纯前端通过API进行调用WebAPI的接口,纯前端模块可以包含Vue3Element项目,以及基于EelectronJS应用,发布跨平台的基于浏览器的应用界面,以及其他App或者小程序整合WebAPI进行业务数据的处理或者展示需要。
如后端开发,我们可以在VS中进行管理,管理开发Winform项目、WebAPI项目等。
Winform界面,我们可以采用基于.netFramework开发或者.netco6进行开发均可,因为我们的SugarProjectCo项目是采用.netStandard模式开发,兼容两者。这里以权限模块来进行演示整合使用。
而纯前端的项目,我们可以基于VSCode或者HBuilderX等工具进行项目的管理开发工作。
2、框架基础类的定义和处理
在开发一个易于使用的框架的时候,主要目的就是减少代码开发,并尽可能通过基类和泛型约束的方式,提高接口的通用性,并通过结合代码生成工具的方式,来提高标准项目的开发效率。
那么我们这里基于SqlSugar的ORM处理,来实现常规数据的增删改查等常规操作的时候,我们是如何进行这些接口的封装处理的呢。
例如,我们对于一个简单的客户信息表,如下所示。
那么它生成的SqlSugar实体类如下所示。
///summary///客户信息///继承自Entity,拥有Id主键属性////summary[SugarTable("T_Customer")]publicclassCustomerInfo:Entitystring{///summary///默认构造函数(需要初始化属性的在此处理)////summarypublicCustomerInfo(){this.CateTime=System.DateTime.Now;}#gionPropertyMembers///summary///姓名////summarypublicvirtualstringName{get;set;}///summary///年龄////summarypublicvirtualintAge{get;set;}///summary///创建人////summarypublicvirtualstringCator{get;set;}///summary///创建时间////summarypublicvirtualDateTimeCateTime{get;set;}#endgion}
其中Entitystring是我们根据需要定义一个基类实体对象,主要就是定义一个Id的属性来处理,毕竟对于一般表对象的处理,SqlSugar需要Id的主键定义(非中间表处理)。
[Serializable]publicabstractclassEntityTPrimaryKey:IEntityTPrimaryKey{///summary///实体类唯一主键////summary[SqlSugar.SugarColumn(IsPrimaryKey=true,ColumnDescription="主键")]publicvirtualTPrimaryKeyId{get;set;}}
而IEntityT定义了一个接口
publicinterfaceIEntityTPrimaryKey{///summary///实体类唯一主键////summaryTPrimaryKeyId{get;set;}}
以上就是实体类的处理,我们一般为了查询信息,往往通过一些条件传入进行处理,那么我们就需要定义一个通用的分页查询对象,供我们精准进行条件的处理。
生成一个以***PageDto的对象类,如下所示。
///summary///用于根据条件分页查询,DTO对象////summarypublicclassCustomerPagedDto:PagedAndSortedInputDto,IPagedAndSortedResultRequest{///summary///默认构造函数////summarypublicCustomerPagedDto():base(){}///summary///参数化构造函数////summary///paramname="skipCount"跳过的数量/param///paramname="sultCount"最大结果集数量/parampublicCustomerPagedDto(intskipCount,intsultCount):base(skipCount,sultCount){}///summary///使用分页信息进行初始化SkipCount和MaxResultCount////summary///paramname="pagerInfo"分页信息/parampublicCustomerPagedDto(PagerInfopagerInfo):base(pagerInfo){}#gionPropertyMembers///summary///不包含的对象的ID,用于在查询的时候排除对应记录////summarypublicvirtualstringExcludeId{get;set;}///summary///姓名////summarypublicvirtualstringName{get;set;}///summary///年龄-开始////summarypublicvirtualint?AgeStart{get;set;}///summary///年龄-结束////summarypublicvirtualint?AgeEnd{get;set;}///summary///创建时间-开始////summarypublicDateTime?CateTimeStart{get;set;}///summary///创建时间-结束////summarypublicDateTime?CateTimeEnd{get;set;}#endgion}
其中PagedAndSortedInputDto,IPagedAndSortedResultRequest都是参考来自于ABP/ABPVNext的处理方式,这样我们可以便于数据访问基类的查询处理操作。
接着我们定义一个基类MyCrudService,并传递如相关的泛型约束,如下所示
///summary///基于SqlSugar的数据库访问操作的基类对象////summary///typeparamname="TEntity"定义映射的实体类/typeparam///typeparamname="TKey"主键的类型,如int,string等/typeparam///typeparamname="TGetListInput"或者分页信息的条件对象/typeparampublicabstractclassMyCrudServiceTEntity,TKey,TGetListInput:IMyCrudServiceTEntity,TKey,TGetListInputwheTEntity:class,IEntityTKey,new()wheTGetListInput:IPagedAndSortedResultRequest
我们先忽略基类接口的相关实现细节,我们看看对于这个MyCrudService和IMyCrudService我们应该如何使用的。
首先我们定义一个应用层的接口ICustomerService如下所示。
///summary///客户信息服务接口////summarypublicinterfaceICustomerService:IMyCrudServiceCustomerInfo,string,CustomerPagedDto,ITransientDependency{}
然后实现在CustomerService中实现它的接口。
///summary///应用层服务接口实现////summarypublicclassCustomerService:MyCrudServiceCustomerInfo,string,CustomerPagedDto,ICustomerService
这样我们对于特定Customer的接口在ICustomer中定义,标准接口直接调用基类即可。
基类MyCrudService提供重要的两个接口,让子类进行重写,以便于进行准确的条件处理和排序处理,如下代码所示。
///summary///基于SqlSugar的数据库访问操作的基类对象////summary///typeparamname="TEntity"定义映射的实体类/typeparam///typeparamname="TKey"主键的类型,如int,string等/typeparam///typeparamname="TGetListInput"或者分页信息的条件对象/typeparampublicabstractclassMyCrudServiceTEntity,TKey,TGetListInput:IMyCrudServiceTEntity,TKey,TGetListInputwheTEntity:class,IEntityTKey,new()wheTGetListInput:IPagedAndSortedResultRequest{///summary///留给子类实现过滤条件的处理////summary///turns/turnsprotectedvirtualISugarQueryableTEntityCateFiltedQueryAsync(TGetListInputinput){turnEntityDb.AsQueryable();}///summary///默认排序,通过ID进行排序////summary///paramname="query"/param///turns/turnsprotectedvirtualISugarQueryableTEntityApplyDefaultSorting(ISugarQueryableTEntityquery){if(typeof(TEntity).IsAssignableToIEntityTKey()){turnquery.OrderBy(e=e.Id);}else{turnquery.OrderBy("Id");}}}
对于Customer特定的业务对象来说,我们需要实现具体的条件查询细节和排序条件,毕竟我们父类没有约束确定实体类有哪些属性的情况下,这些就交给子类做最合适了。
///summary///应用层服务接口实现////summarypublicclassCustomerService:MyCrudServiceCustomerInfo,string,CustomerPagedDto,ICustomerService{///summary///自定义条件处理////summary///paramname="input"查询条件Dto/param///turns/turnsprotectedoverrideISugarQueryableCustomerInfoCateFiltedQueryAsync(CustomerPagedDtoinput){varquery=base.CateFiltedQueryAsync(input);query=query.WheIF(!input.ExcludeId.IsNullOrWhiteSpace(),t=t.Id!=input.ExcludeId)//不包含排除ID.WheIF(!input.Name.IsNullOrWhiteSpace(),t=t.Name.Contains(input.Name))//如需要精确匹配则用Equals//年龄区间查询.WheIF(input.AgeStart.HasValue,s=s.Age=input.AgeStart.Value).WheIF(input.AgeEnd.HasValue,s=s.Age=input.AgeEnd.Value)//创建日期区间查询.WheIF(input.CateTimeStart.HasValue,s=s.CateTime=input.CateTimeStart.Value).WheIF(input.CateTimeEnd.HasValue,s=s.CateTime=input.CateTimeEnd.Value);turnquery;}///summary///自定义排序处理////summary///paramname="query"可查询LINQ/param///turns/turnsprotectedoverrideISugarQueryableCustomerInfoApplyDefaultSorting(ISugarQueryableCustomerInfoquery){turnquery.OrderBy(t=t.CateTime,OrderByType.Desc);//先按第一个字段排序,然后再按第二字段排序//turnbase.ApplySorting(query,input).OrderBy(s=s.Customer_ID).OrderBy(s=s.Seq);}}
通过CateFiltedQueryAsync的精确条件处理,我们就可以明确实体类的查询条件处理,因此对于CustomerPagedDto来说,就是可以有客户端传入,服务后端的基类进行处理了。
如基类的分页条件查询函数GetListAsync就是根据这个来处理的,它的实现代码如下所示。
///summary///根据条件获取列表////summary///paramname="input"分页查询条件/param///turns/turnspublicvirtualasyncTaskPagedResultDtoTEntityGetListAsync(TGetListInputinput){varquery=CateFiltedQueryAsync(input);vartotalCount=awaitquery.CountAsync();query=ApplySorting(query,input);query=ApplyPaging(query,input);varlist=awaitquery.ToListAsync();turnnewPagedResultDtoTEntity(totalCount,list);}
而其中ApplySorting就是根据条件决定是否选择子类实现的默认排序进行处理的。
///summary///记录排序处理////summary///turns/turnsprotectedvirtualISugarQueryableTEntityApplySorting(ISugarQueryableTEntityquery,TGetListInputinput){//Trytosortqueryifavailableif(inputisISortedResultRequestsortInput){if(!sortInput.Sorting.IsNullOrWhiteSpace()){turnquery.OrderBy(sortInput.Sorting);}}//IQueryable.Taskquissorting,soweshouldsortifTakewillbeused.if(inputisILimitedResultRequest){turnApplyDefaultSorting(query);}//Nosortingturnquery;}
对于获取单一对象,我们一般提供一个ID主键获取即可。
///summary///根据ID获取单一对象////summary///paramname="id"主键ID/param///turns/turnspublicvirtualasyncTaskTEntityGetAsync(TKeyid){turnawaitEntityDb.GetByIdAsync(id);}
也可以根据用户的Expss条件进行处理,在基类我们定义很多这样的Expss条件处理,便于子类进行条件处理的调用。如对于删除,可以指定ID,也可以指定条件删除。
///summary///删除指定ID的对象////summary///paramname="id"记录ID/param///turns/turnspublicvirtualasyncTaskboolDeleteAsync(TKeyid){turnawaitEntityDb.DeleteByIdAsync(id);}
///summary///根据指定条件,删除集合////summary///paramname="input"表达式条件/param///turns/turnspublicvirtualasyncTaskboolDeleteAsync(ExpssionFuncTEntity,boolinput){varsult=awaitEntityDb.DeleteAsync(input);turnsult;}
如判断是否存在也是一样处理
///summary///判断是否存在指定条件的记录////summary///paramname="id"ID主键/param///turns/turnspublicvirtualasyncTaskboolIsExistAsync(TKeyid){varinfo=awaitEntityDb.GetByIdAsync(id);varsult=(info!=null);turnsult;}///summary///判断是否存在指定条件的记录////summary///paramname="input"表达式条件/param///turns/turnspublicvirtualasyncTaskboolIsExistAsync(ExpssionFuncTEntity,boolinput){varsult=awaitEntityDb.IsAnyAsync(input);turnsult;}
关于WebAPI的处理,我在随笔《基于SqlSugar的数据库访问处理的封装,在.net6框架的WebAPI上开发应用》中也有介绍,主要就是先弄好.net6的开发环境,然后在进行相关的项目开发即可。
根据项目的需要,我们定义了一些控制器的基类,用于实现不同的功能。
其中ControllerBase是.netcoWebAPI中的标准控制器基类,我们由此派生一个LoginController用于登录授权,而BaseApiController则处理常规接口用户身份信息,而BusinessController则是对标准的增删改查等基础接口进行的封装,我们实际开发的时候,只需要开发编写类似CustomerController基类即可。
BaseApiController没有什么好介绍的,就是封装一下获取用户的身份信息。
可以通过下面代码获取接口用户的Id
///summary///当前用户身份ID////summaryprotectedvirtualstring?CurntUserId=HttpContext.User.FindFirst(JwtClaimTypes.Id)?.Value;
而BusinessController控制器则是继承这个BaseApiController即可。通过泛型约束传入相关的对象信息。
///summary///本控制器基类专门为访问数据业务对象而设的基类////summary///typeparamname="TEntity"定义映射的实体类/typeparam///typeparamname="TKey"主键的类型,如int,string等/typeparam///typeparamname="TGetListInput"或者分页信息的条件对象/typeparam[Route("[controller]")][Authorize]//需要授权登录访问publicclassBusinessControllerTEntity,TKey,TGetListInput:BaseApiControllerwheTEntity:class,IEntityTKey,new()wheTGetListInput:IPagedAndSortedResultRequest{///summary///通用基础操作接口////summaryprotectedIMyCrudServiceTEntity,TKey,TGetListInput_service{get;set;}///summary///构造函数,初始化基础接口////summary///paramname="service"通用基础操作接口/parampublicBusinessController(IMyCrudServiceTEntity,TKey,TGetListInputservice){this._service=service;}....
这个基类接收一个符合基类接口定义的对象作为基类增删删改查等处理方法的接口对象。在具体的CustomerController中的定义处理如下所示。
///summary///客户信息的控制器对象////summarypublicclassCustomerController:BusinessControllerCustomerInfo,string,CustomerPagedDto{privateICustomerService_customerService;///summary///构造函数,并注入基础接口对象////summary///paramname="customerService"/parampublicCustomerController(ICustomerServicecustomerService):base(customerService){this._customerService=customerService;}}
这样就可以实现基础的相关操作了。如果需要特殊的接口实现,那么定义方法实现即可。
类似字典项目中的控制器处理代码如下所示。定义好HTTP方法,路由信息等即可。
///summary///根据字典类型ID获取所有该类型的字典列表集合(Key为名称,Value为值)////summary///paramname="dictTypeId"字典类型ID/param///turns/turns[HttpGet][Route("by-typeid/{dictTypeId}")]publicasyncTaskDictionarystring,stringGetDictByTypeID(stringdictTypeId){turnawait_dictDataService.GetDictByTypeID(dictTypeId);}///summary///根据字典类型名称获取所有该类型的字典列表集合(Key为名称,Value为值)////summary///paramname="dictTypeName"字典类型名称/param///turns/turns[HttpGet][Route("by-typename/{dictTypeName}")]publicasyncTaskDictionarystring,stringGetDictByDictType(stringdictTypeName){turnawait_dictDataService.GetDictByDictType(dictTypeName);}
来源: