2023年2月

在前面几篇随笔介绍了我对ABP框架的改造,包括对ABP总体的介绍,以及对各个业务分层的简化,Web API 客户端封装层的设计,使得我们基于ABP框架的整体方案越来越清晰化, 也越来越接近实际的项目开发需求,一旦整个模式比较成熟,并以一种比较固化的模式来指导开发,那么就可以很方便的应用在实际项目开发当中了。本篇随笔是基于前面几篇的基础上,在Winform项目上进一步改造为实际项目的场景,把我原来基于微软企业库底层的数据库访问方式的Winform框架或者混合框架的字典模块界面改造为基于ABP框架基础上的字典应用模块。

1)APICaller层接口的回顾

在上一篇随笔《
ABP开发框架前后端开发系列---(4)Web API调用类的封装和使用
》中,我介绍了Web API调用类的封装和使用,并介绍了在.net 控制台程序中,测试对ApiCaller层的调用,并能够顺利返回我们所需要的数据。测试代码如下所示。

    #region DictType

    using (var client = bootstrapper.IocManager.ResolveAsDisposable<DictTypeApiCaller>())
{
var caller =client.Object;

Console.WriteLine(
"Logging in with TOKEN based auth...");var token = caller.Authenticate("admin", "123qwe").Result;
Console.WriteLine(token.ToJson());

caller.RequestHeaders.Add(
new NameValue("Authorization", "Bearer" +token.AccessToken));

Console.WriteLine(
"Get All ...");var pagerDto = new DictTypePagedDto() { SkipCount = 0, MaxResultCount = 10};var result =caller.GetAll(pagerDto).Result;
Console.WriteLine(result.ToJson());

Console.WriteLine(
"Get All by condition ...");var pagerdictDto = new DictTypePagedDto() { Name = "民族"};
result
=caller.GetAll(pagerdictDto).Result;
Console.WriteLine(result.ToJson());

Console.WriteLine(
"Get count by condition ...");
pagerdictDto
= newDictTypePagedDto() {};var count =caller.Count(pagerdictDto).Result;
Console.WriteLine(count);
Console.WriteLine();

Console.WriteLine(
"Create DictType...");var createDto = new CreateDictTypeDto { Id = Guid.NewGuid().ToString(), Name = "Test", Code = "Test"};var dictDto =caller.Create(createDto).Result;
Console.WriteLine(dictDto.ToJson());

Console.WriteLine(
"Update DictType...");
dictDto.Code
= "testcode";var updateDto =caller.Update(dictDto).Result;
Console.WriteLine(updateDto.ToJson());
if (updateDto != null)
{
Console.WriteLine(
"Delete DictType...");
caller.Delete(
new EntityDto<string>() { Id =dictDto.Id });
}

}
#endregion

这些ApiCaller对象的接口测试代码,包括了授权登录,获取所有记录,获取条件查询记录,创建、更新、删除这些接口都成功执行,验证了我们对整体架构的设计改良,并通过对ApiCaller层基类的设计,减少我们对常规增删改查接口的编码,我们只需要编写我们的自定义业务接口代码封装类即可。

其中基类的代码如下所示。

针对Web API接口的封装,为了适应客户端快速调用的目的,这个封装作为一个独立的封装层,以方便各个模块之间进行共同调用。

也就是说,上面我们全部是基于基类接口的调用,还不需要为我们自定义接口编写任何一行代码,已经具备了常规的各种查询和数据处理功能了。

我们完整的字典类型ApiCaller类的代码如下所示。

namespaceMyProject.Caller
{
/// <summary> ///字典类型对象的Web API调用处理/// </summary> public class DictTypeApiCaller : AsyncCrudApiCaller<DictTypeDto, string, DictTypePagedDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService
{
/// <summary> ///提供单件对象使用/// </summary> public staticDictTypeApiCaller Instance
{
get{return Singleton<DictTypeApiCaller>.Instance;
}
}
/// <summary> ///默认构造函数/// </summary> publicDictTypeApiCaller()
{
this.DomainName = "DictType";//指定域对象名称,用于组装接口地址 }public async Task<Dictionary<string, string>> GetAllType(stringdictTypeId)
{
AddRequestHeaders();
//加入认证的token头信息 string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数) url += string.Format("?dictTypeId={0}", dictTypeId);var result = await apiClient.GetAsync<Dictionary<string, string>>(url);returnresult;
}
public async Task<IList<DictTypeNodeDto>> GetTree(stringpid)
{
AddRequestHeaders();
//加入认证的token头信息 string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数) url += string.Format("?pid={0}", pid);var result = await apiClient.GetAsync<IList<DictTypeNodeDto>>(url);returnresult;
}
}

这里面的函数定义才是我们需要根据实际的自定义接口封装的调用类函数代码。

前面我们介绍了,我们把ApiCaller层的项目设计为.net Standard的类库项目,因此可以在.net core或者在.net framework中进行使用,并且也在基于.net core的控制台程序中测试成功了。

下面就重点介绍一下,基于.net framework的Winfrom程序中对ABP框架的Web API接口的调用,如果以后Winform支持.net core了(据说9月份出的.net core3就包含了),那么也一样的模式进行调用。

2)Winform对ApiCaller层的调用

我们先来看看字典模块,通过封装对ABP框架的Web API调用后,实际的功能界面效果吧。

先设计一个授权登录的界面获取访问令牌信息。

字典管理界面,列出字典类型,并对字典类型下的字典数据进行分页展示,分页展示利用分页控件展示。

新增或者编辑窗体界面如下

这个界面是来自于我的框架里面的字典模块界面,不过里面对数据的处理代码确实已经更改为适应ABP框架的Web API接口的调用的了(基于ApiCaller 层的调用)。

我们下面来一一进行分析即可。

登陆界面,我们看看主要的逻辑就是调用获取授权令牌的接口,并存储起来供后续界面中的业务类进行调用即可。

由于我们自己封装的ApiCaller类,都是基于异步的方式封装的,因此我们可以看到很多地方调用都使用await的关键字,这个是异步调用的关键字,如果方法需要定义为异步,就需要增加async关键字,一般这两个关键字是配套使用的。

如果我们在事件处理代码里面使用了异步,那么事件的函数也需要标记为async,如下是字典管理模块窗体的加载函数,也是用了async声明 和await调用异步方法标记。

        private async void FrmDictionary_Load(objectsender, EventArgs e)
{
awaitInitTreeView();this.lblDictType.Text = "";awaitBindData();//分页控件事件处理代码 this.winGridViewPager1.OnPageChanged += newEventHandler(winGridViewPager1_OnPageChanged);this.winGridViewPager1.OnStartExport += newEventHandler(winGridViewPager1_OnStartExport);this.winGridViewPager1.OnEditSelected += newEventHandler(winGridViewPager1_OnEditSelected);this.winGridViewPager1.OnAddNew += newEventHandler(winGridViewPager1_OnAddNew);this.winGridViewPager1.OnDeleteSelected += newEventHandler(winGridViewPager1_OnDeleteSelected);this.winGridViewPager1.OnRefresh += newEventHandler(winGridViewPager1_OnRefresh);this.winGridViewPager1.AppendedMenu = this.contextMenuStrip2;this.winGridViewPager1.BestFitColumnWith = false;this.winGridViewPager1.gridView1.DataSourceChanged += newEventHandler(gridView1_DataSourceChanged);
}

我们的数据,主要是在BindData里面实现,这个函数是我们自己加的,由于使用了异步方法,因此也用async进行声明。

整个对于分页的数据获取和控件的数据绑定过程,代码如下所示。

        /// <summary>
        ///获取数据/// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<DictDataDto>>GetData()
{
//构建分页的条件和查询条件 var pagerDto = new DictDataPagedDto(this.winGridViewPager1.PagerInfo)
{
DictType_ID
= string.Concat(this.lblDictType.Tag)
};
var result = awaitDictDataApiCaller.Instance.GetAll(pagerDto);returnresult;
}
/// <summary> ///绑定数据/// </summary> private asyncTask BindData()
{
#region 添加别名解析 this.winGridViewPager1.DisplayColumns = "Name,Value,Seq,Remark,EditTime";this.winGridViewPager1.AddColumnAlias(Id_FieldName, "编号");this.winGridViewPager1.AddColumnAlias("DictType_ID", "字典大类");this.winGridViewPager1.AddColumnAlias("Name", "项目名称");this.winGridViewPager1.AddColumnAlias("Value", "项目值");this.winGridViewPager1.AddColumnAlias("Seq", "字典排序");this.winGridViewPager1.AddColumnAlias("Remark", "备注");this.winGridViewPager1.AddColumnAlias("Editor", "修改用户");this.winGridViewPager1.AddColumnAlias("EditTime", "更新日期");#endregion if (this.lblDictType.Tag != null)
{
var result = awaitGetData();//设置所有记录数和列表数据源 this.winGridViewPager1.DataSource =result.Items;this.winGridViewPager1.PagerInfo.RecordCount =result.TotalCount;
}
}

其中注意的是GetAll方式是传入一个条件查询的对象,这个就是DictDataPagedDto是我们定义的,放入我们DictDataDto里面的常见属性,方便我们根据属性匹配精确或者模糊查询。

    /// <summary>
    ///用于根据条件查询/// </summary>
    public classDictDataPagedDto : PagedResultRequestDto
{
/// <summary> ///字典类型ID/// </summary> public virtual string DictType_ID { get; set; }/// <summary> ///类型名称/// </summary> public virtual string Name { get; set; }/// <summary> ///指定值/// </summary> public virtual string Value { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }
}

我们在调用的时候,让它限定为一个类型的ID进行精确查询,如下代码

//构建分页的条件和查询条件
var pagerDto = new DictDataPagedDto(this.winGridViewPager1.PagerInfo)
{
DictType_ID
= string.Concat(this.lblDictType.Tag)
};

这个精确或者模糊查询,则是在应用服务层里面定义规则的,这个之前没有详细介绍了,这里稍微补充说明一下。

在应用服务层接口类里面,重写CreateFilteredQuery可以设置GetAll的查询规则,重写ApplySorting则可以指定列表的排序顺序。

再次回到Winform界面的调用上来,删除类型下面字典数据的事件的处理函数如下所示。

        private async void menu_ClearData_Click(objectsender, EventArgs e)
{
TreeNode selectedNode
= this.treeView1.SelectedNode;if (selectedNode != null && selectedNode.Tag != null)
{
string typeId =selectedNode.Tag.ToString();var dict = awaitDictDataApiCaller.Instance.GetDictByTypeID(typeId);int count =dict.Count;var format = "您确定要删除节点:{0},该节点下面有【{1}】项数据";
format
=JsonLanguage.Default.GetString(format);string message = string.Format(format, selectedNode.Text, count);if (MessageDxUtil.ShowYesNoAndWarning(message) ==DialogResult.Yes)
{
try{awaitDictDataApiCaller.Instance.DeleteByTypeID(typeId);awaitInitTreeView();awaitBindData();
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
}
}

我们看看编辑窗体界面的后台处理,编辑和更新数据的逻辑代码如下所示。

                #region 编辑大类
                var info = await DictTypeApiCaller.Instance.Get(new EntityDto<string>(ID));if (info != null)
{
SetInfo(info);
try{var updatedDto = awaitDictTypeApiCaller.Instance.Update(info);if (updatedDto != null)
{
MessageDxUtil.ShowTips(
"保存成功");this.DialogResult =DialogResult.OK;
}
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
#endregion

最后来一段gif动图,展示程序的操作功能吧。

好了,这些事件的使用规则一旦确定了,我们好利用代码生成工具对窗体界面的代码进行统一规则的生成,就好像我前面对于我Winform框架和混合框架里面的Winform窗体界面的生成一样,我们只需要稍微修改一下代码生成工具的NVelocity模板,利用上数据库表的元数据就可以快速生成整个框架所需要的代码了。

这样基于整个ABP框架,而快速应用起来的项目,其实开发项目的工作量看起来也不会很多,而且我们可以把字典、权限控制、整体框架等基础设施建设好,就会形成一整套的开发方法和思路了,这样对于我们利用ABP框架来开发业务系统,是不是有事半功倍的感觉。

一旦某个东西你很喜欢,你就会用的越来越好。

1)代码生成工具介绍

Database2Sharp是一款代码生成工具和数据库文档生成工具,该工具从2005年开始至今,一直伴随着我们的客户和粉丝们经历着过各种各样的项目开发,在实际开发中能带来效率的提高及编程的快乐。

Database2Sharp是一款主要用于C#代码生成以及数据库文档生成的工具,软件支持Oracle、SqlServer、MySql、PostgreSQL、Sqlite、Access以及国产达梦等数据库的代码生成,可以生成各种架构代码、生成Winform界面代码、Web界面代码(包括EasyUI和BootstrapWeb界面)、Entity Framework实体框架代码、导出数据库文档、浏览数据库架构、查询数据、生成Sql脚本等,还整合自定义模板和数据库信息的引擎,方便编写自定义模板调试和开发。生成的框架代码支持多种数据库一起使用,也支持不同业务的数据库切割为多个库进行使用,是一种适应性非常强、弹性很好的应用框架。

Database2Sharp推荐采用软件功能“
Enterprise Library
代码生成”
来生成项目代码,这个架构体系生成整个项目工程框架,包含实体类、数据访问类、业务类、Web页面代码、WCF相关服务层(可选)、Web API服务层(可选),以及各种服务的调用包装层代码等。该架构利用泛型及缓存机制,良好的架构极大简化代码,强大完善的基类机制使您甚至不用编写一行代码就能顺利运行。一个简单点击几次鼠标就能完成一周代码量的代码生成工具,效率惊人、友好体贴,真正的开发好伴侣。

当然,开发的过程是一个繁复、精细的过程,因此Database2Sharp也吸收了来自我们自己的实际需求,以及很多同仁朋友的宝贵意见,一直在改进,一直努力做到更好,以求达到一个更加完美、更加易用的境界。

在我们开发软件的时候,解决方案项目基于一定的分层组织,每个项目分层中,各个类的关系也是确定的,借助辅助工具(结合模板引擎)可以快速生成我们所需要的代码,并极大提高我们软件的开发效率,Database2Sharp代码生成工具就是一款专门针对我们自己框架结构配套的开发工具。

Database2Sharp代码生成工具,主要是基于数据库提取的元数据信息,根据表的信息和关系,字段信息等内容,生成我们框架所需要分层的类代码。

对于Winform开发,可以根据Winform框架或者混合框架的窗体界面类,生成标准的界面代码,列表界面默认具有分页查询、导入导出、高级查询、编辑、删除事件绑定,编辑界面则具有获取数据并显示在控件,保存后执行更新或者写入的操作。

对于Web开发,可以根据EasyUI控件界面或者Bootstrap控件界面的不同,生成对应的视图HTML代码和控制器类代码,同时这些界面默认也具有分页查询,导入导出,显示明细和保存数据的功能。

2)软件功能界面及项目结构

软件主要界面如下所示。

软件功能界面如上图所示,左侧为数据库信息展示,右侧为表信息或者代码信息展示,主要功能通过左侧树上的右键或者界面按钮实现。

代码生成工具的项目结构如下所示。

其中各个项目负责的内容不同,分为数据核心模块、配置文件模块、Winform项目模块、Web项目模块、混合框架项目模块、数据库脚本项目模块、EF实体框架项目模块、数据库文档模块,如下图所示。

根据实际生成代码的需要,我们可以进一步扩展应用模块,利用底层数据库元数据和模板引擎,从而生成更多的项目模板代码。

3)元数据对象及层级关系

其中WHC.CodeGenerator.Core项目是元素获取处理的核心模块,它主要负责从不同数据库类型的数据库中获取相关库的表信息、字段信息、存储过程和视图信息等内容。

其中各个对象的层级关系如下所示:每个数据库实例,包含多个数据库信息,一个数据库对象包括多个表、多个视图、多个存储过程对象,一个表则包含多个字段对象,而每个字段可能包含一个到多个Key对象信息,如外键、主键信息等。

代码生成工具支持几种不同的数据库元素获取,数据库元素的获取通过实现
IListData接口,从而支持多种数据库类型,包括
MS SQLServer、
Oracle、
Mysql、
PostgreSQL、
SQLite、
Access等数据库,他们从
Database基类扩展,形成自己数据库特点的子类,如
SQLServer、
Mysql类,数据库的操作也是基于
Enterprise Library数据库访问模块实现的。继承关系如下所示。

4)工具界面向导功能介绍

代码生成工具里面,我们生成代码的时候,一般需要选择哪个库,然后选择表,以及一些配置项,才能进行代码的生成,这个是一个配置向导的功能,主要是基于Microsoft.ApplicationBlocks.UIProcess 组件进行的一个动态配置(UIProcess是微软早期的一个界面管理组件),它是读取配置文件进行的界面处理。如下面是App.config配置文件的内容。

其中每个View代表一个配置一个向导界面,如SelectDatabaseView,它对应一个控制器SimpleController。

另外,配置文件的navigationGraph是一个配置型的导向,主要配置我们处理的场景,详细配置如下所示。

和这些导航图的配置信息对应,我们代码生成工具也需要提供对应名称的窗体,如下图所示。这些窗体就是我们操作某个代码生成过程的操作界面,如选择数据库界面,选择表界面,确认生成功能界面等。

另外配置文件里面会定义我们一组处理的导航窗体,用来构建我们一个业务场景,如EnterpriseLibrary架构代码生成操作。

然后我们在界面需要定义一个按钮,实现这个代码生成功能的时候,只需要调用这组视图的名称即可,如下代码所示。

这样启动后,整个处理导航逻辑就会按照配置文件的顺序一个个调用界面,最后统一生成代码即可。

5)自定义模板代码生成

代码生成工具是基于比较有名的NVelocity模板进行代码文件生成的,因此熟悉NVelocity语法可以修改模板,以适应需要,生成不同内容的文件。

除了可以利用NVelocity模板语法,实现变量定义或者赋值、控制逻辑处理等功能外,还可以利用模板和后台变量进行交互处理。

模板引擎的后台控制规则是在代码生成工具里面封装的,因此可以在NVelocity模板文件里面访问对应的数据库对象,选择的表对象、字段信息等元素内容,或者访问自定义的变量。

基于模板方式的代码生成方式,可以提高生成效率,并且减少维护成本(相对某些硬编码代码生成的方式更优),方便用户对模板进行适当的修改等特点。

关于NVelocity模板语法,详细介绍可以参考伍华聪博客《
强大的模板引擎开源软件NVelocity
》随笔。

在实际开发当中,自定义模板生成是很多代码生成工具生成代码的重要补充,例如有些项目代码片段需要(例如控件赋值语句,或者控件数据显示语句等),如果能通过自定义模板方式,利用模板引擎的灵活特点,以及已有数据库的结构信息,就很方便生成重复性强、有一定规律的代码。

用【选表进行代码生成】方式,可以很好利用后台的数据库表、字段等信息,结合模板生成高效的代码。

另外,为了方便大家对模板引擎NVelocity的了解,在软件工具安装的时候,附带了几个常见的例子,如下所示。

例子的代码大致如下所示,主要是让大家快速了解Nvelocity的模板语言VTL的使用。如果对模板引擎更加深入的了解,可以查看我之前的随笔《
强大的模板引擎开源软件NVelocity
》进一步了解。

在前面随笔,我介绍了整个ABP优化过框架的分层模型,包括尽量简化整个ABP框架的各个层的关系,以及纳入一些基类的辅助处理,使得我们对应业务分层类或者接口尽可能减少代码,并具有生产环境所需要的基类接口,通过我对整个ABP框架模型的分析,我们可以结合代码生成工具Database2Sharp来生成对应分层的代码,该工具后台具备数据库表所需要的一切字段信息和关系信息,因此我们确定好逻辑关系就可以生成对应分层的代码。本篇随笔介绍代码生成工具Database2Sharp生成基于ABP框架的分层代码过程。

1)ABP框架回顾

ABP框架主要还是基于领域驱动的理念来构建整个架构的,其中领域驱动包含的概念有 域对象Entities、仓储对象Repositories、域服务接口层Domain Services、域事件Domain Events、应用服务接口Application Services、数据传输对象DTOs等。

以下是ABP初始框架的各个分层的信息,它主要是分为下面几个项目分层。

Application应用层
:应用层提供一些应用服务(Application Services)方法供展现层调用。一个应用服务方法接收一个DTO(数据传输对象)作为输入参数,使用这个输入参数执行特定的领域层操作,并根据需要可返回另一个DTO。

Core领域核心层
,领域层就是业务层,是一个项目的核心,所有业务规则都应该在领域层实现。这个项目里面,除了定义所需的领域实体类外,其实可以定义我们自己的自定义的仓储对象(类似DAL/IDAL),以及定义自己的业务逻辑层(类似BLL/IBLL),以及基于AutoMapper映射规则等内容。

EntityFrameworkCore
实体框架核心层,这个项目不需要修改太多内容,只需要在DbContext里面加入对应领域对象的仓储对象即可。

Migrator数据迁移层
,这个是一个辅助创建的控制台程序项目,如果基于DB First,我们可以利用它来创建我们项目的初始化数据库。

Web.Core Web核心层
,基于Web或者Web API的核心层,提供了对身份登陆验证的基础处理,没有其他内容。

Web.Core.Host Web API的宿主层
,也是动态发布Web API的核心内容,另外在Web API里面整合了Swagger,使得我们可以方便对Web API的接口进行调试。

Tests 单元测试层
,这个提供了一些应用层对象的模拟测试,其中测试的数据库使用的是Entity Framework 的内存数据库,不影响实际数据库内容。

经过我进行简化和优化处理的框架项目结构如下所示。

以上是VS里面解决方案的项目结构,我根据项目之间的关系,整理了一个架构的图形,如下所示。

上图是以字典模块为介绍, 其中橘红色的部分就是我们为各个分层需要根据数据库构建对应的类或者接口文件。

例如对于01-Core模块层,需要增加文件

对于03-Application.Common模块来说,需要增加DTO和应用服务层接口文件

而对于04-Application应用层来说,需要增加对应的接口实现文件

而05、06、07模块,我们不需要加入任何文件,08-Caller层加入对WebAPI的远程调用封装类,给Winform、WPF/UWP、控制台程序等调用。

一个模块的变化,都会导致在上面各个分层之间增加对应的文件,这样的架构确定后,我们就可以根据对应的类生成规则进行生成接口。

2)利用代码生成工具生成分层代码

在前面随笔《
代码生成工具Database2Sharp的架构介绍
》中,我介绍了整个代码生成工具的架构信息,因此我们用代码生成工具生成架构代码的时候,可以利用整个数据库表的信息和关系信息来处理。

通过整合相关的生成规则,我们可以增加对应的ABP框架代码的生成,如下代码生成工具界面所示。

最终根据根据选择数据库表信息,一键生成相关ABP架构分层代码,文件结构如下所示。

对比前面项目的介绍,我们可以看到各个分层的类代码是完全一致的。如对于领域层,包含了表名称标记、字段信息和引用外键的对象。

    /// <summary>
    ///通用字典明细项目信息,领域对象/// </summary>
    [Table("TB_DictData")]public class DictData : FullAuditedEntity<string>{/// <summary>
        ///默认构造函数(需要初始化属性的在此处理)/// </summary>
        publicDictData()
{
}
#region Property Members /// <summary> ///字典大类/// </summary> //[Required] public virtual string DictType_ID { get; set; }/// <summary> ///字典名称/// </summary> //[Required] public virtual string Name { get; set; }/// <summary> ///字典值/// </summary> public virtual string Value { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }/// <summary> ///排序/// </summary> public virtual string Seq { get; set; }/// <summary> ///字典大类/// </summary> [ForeignKey("DictType_ID")]public virtual DictType DictType { get; set; }#endregion}

对于DTO文件,我们看看代码信息

    /// <summary>
    ///通用字典明细项目信息,DTO对象/// </summary>
    public classDictDataDto
{
/// <summary> ///默认构造函数(需要初始化属性的在此处理)/// </summary> publicDictDataDto()
{
}
#region Property Members /// <summary> ///字典大类/// </summary> public virtual string DictType_ID { get; set; }/// <summary> ///字典名称/// </summary> public virtual string Name { get; set; }/// <summary> ///字典值/// </summary> //[Required] public virtual string Value { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }/// <summary> ///排序/// </summary> public virtual string Seq { get; set; }#endregion}/// <summary> ///创建通用字典明细项目信息,DTO对象/// </summary> public classCreateDictDataDto : DictDataDto
{
}
/// <summary> ///用于根据条件分页查询,DTO对象/// </summary> public classDictDataPagedDto : PagedResultRequestDto
{
publicDictDataPagedDto() { }/// <summary> ///参数化构造函数/// </summary> /// <param name="skipCount">跳过的数量</param> /// <param name="resultCount">最大结果集数量</param> public DictDataPagedDto(int skipCount, intresultCount)
{
this.SkipCount =skipCount;this.MaxResultCount =resultCount;
}
/// <summary> ///使用分页信息进行初始化SkipCount 和 MaxResultCount/// </summary> /// <param name="pagerInfo">分页信息</param> publicDictDataPagedDto(PagerInfo pagerInfo)
{
if (pagerInfo != null)
{
//默认设置 var pageSize = pagerInfo.PageSize > 0 ? pagerInfo.PageSize : 50;var pageIndex = pagerInfo.CurrenetPageIndex > 0 ? pagerInfo.CurrenetPageIndex : 1;this.SkipCount = pageSize * (pageIndex - 1);this.MaxResultCount =pageSize;
}
}
#region Property Members /// <summary> ///字典大类/// </summary> public virtual string DictType_ID { get; set; }/// <summary> ///字典名称/// </summary> public virtual string Name { get; set; }/// <summary> ///字典值/// </summary> public virtual string Value { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }/// <summary> ///排序/// </summary> public virtual string Seq { get; set; }#endregion}

DTO的映射文件代码生成如下

    /// <summary>
    ///通用字典明细项目信息,映射文件/// </summary>
    public classDictDataMapProfile : Profile  
{
publicDictDataMapProfile()
{
CreateMap
<DictDataDto, DictData>();
CreateMap
<DictData, DictDataDto>();
CreateMap
<CreateDictDataDto, DictData>();
}
}

应用服务层接口实现代码如下所示。

    /// <summary>
    ///通用字典明细项目信息,应用层服务接口实现/// </summary>
[AbpAuthorize]public class DictDataAppService : MyAsyncServiceBase<DictData, DictDataDto, string, DictDataPagedDto, CreateDictDataDto, DictDataDto>, IDictDataAppService
{
private readonly IRepository<DictData, string>_repository;public DictDataAppService(IRepository<DictData, string> repository) : base(repository)
{
_repository
=repository;
}
/// <summary> ///自定义条件处理/// </summary> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override IQueryable<DictData>CreateFilteredQuery(DictDataPagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!DictType_ID.IsNullOrWhiteSpace(), t =>t.DictType_ID.Contains(input.DictType_ID))
.WhereIf(
!Name.IsNullOrWhiteSpace(), t =>t.Name.Contains(input.Name))
.WhereIf(
!Value.IsNullOrWhiteSpace(), t =>t.Value.Contains(input.Value))
.WhereIf(
!Remark.IsNullOrWhiteSpace(), t =>t.Remark.Contains(input.Remark))
.WhereIf(
!Seq.IsNullOrWhiteSpace(), t =>t.Seq.Contains(input.Seq));
}
/// <summary> ///自定义排序处理/// </summary> /// <param name="query">可查询LINQ</param> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override IQueryable<DictData> ApplySorting(IQueryable<DictData>query, DictDataPagedDto input)
{
return base.ApplySorting(query, input);//示例代码//先按字典类型排序,然后同一个字典类型下的再按Seq排序//return base.ApplySorting(query, input).OrderBy(s=>s.DictType_ID).ThenBy(s => s.Seq); }
}

ApiCaller分层的代码实现如下所示。

    /// <summary>
    ///通用字典明细项目信息的Web API调用处理/// </summary>
    public class DictDataApiCaller : AsyncCrudApiCaller<DictDataDto, string, DictDataPagedDto, CreateDictDataDto, DictDataDto>, IDictDataAppService
{
/// <summary> ///提供单件对象使用/// </summary> public staticDictDataApiCaller Instance
{
get{return Singleton<DictDataApiCaller>.Instance;
}
}
/// <summary> ///默认构造函数/// </summary> publicDictDataApiCaller()
{
this.DomainName = "DictData";//指定域对象名称,用于组装接口地址 }

}

这些信息是根据数据库对应字段信息和关系信息进行批量生成,我们可以在这基础上进行一定的调整,以及增加自己的业务接口,那么就非常方便了。

利用代码生成工具的数据库元数据,结合模板引擎NVelocity,我们可以为我们的项目框架代码快速生成提供了一个快速有效、统一标准的生成方式,大大提高了生产效率。

最近没有更新ABP框架的相关文章,一直在研究和封装相关的接口,总算告一段落,开始继续整理下开发心得。上次我在随笔《
ABP开发框架前后端开发系列---(5)Web API调用类在Winform项目中的使用
》中介绍了字典模块的管理,以及实现了常规的获取所有记录,获取条件查询记录,创建、更新、删除这些接口。本篇继续深入介绍ABP框架在实际项目中使用的情况,本篇随笔整理对ABP基础接口,以及展示完成的省份城市行政区管理模块的内容。

1、ABP常规处理接口

根据ABP框架默认提供的一些接口,我们可以在服务端封装好相关的Web API接口(由于动态API的便利,其实是完成ApplicationService层即可),前面介绍了获取条件查询记录,创建、更新、删除这些接口的实现和处理,以及可以扩展自己的自定义业务接口,如下是字典模块的接口关系。

字典管理界面,列出字典类型,并对字典类型下的字典数据进行分页展示,分页展示利用分页控件展示。

新增或者编辑窗体界面如下

或者是批量的字典数据录入

这个精确或者模糊查询,则是在应用服务层里面定义规则的,在应用服务层接口类里面,重写CreateFilteredQuery可以设置GetAll的查询规则,重写ApplySorting则可以指定列表的排序顺序。

2、ABP常规查询接口的细化

在前面介绍了的内容汇总,基本上实现了常规数据的分页查询,我们可以看到,对于字典数据来说,分页查询条件是在DictDataPagedDto里面定义,这个是我们定义的分页条件,如下代码所示。

    /// <summary>
    ///用于根据条件分页查询/// </summary>
    public classDictDataPagedDto : PagedResultRequestDto
{
/// <summary> ///字典类型ID/// </summary> public virtual string DictType_ID { get; set; }/// <summary> ///类型名称/// </summary> public virtual string Name { get; set; }/// <summary> ///指定值/// </summary> public virtual string Value { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }
}

这个类文件,我们一般把这个业务模块相关的统一放在一个文件中,例如字典数据相关的DTO放在一个DictDataDto文件里面,方便管理,如下所示。

上面是字典模块的一些基础介绍,实际上我们开发业务模块的时候,录入数据的时候,还需要一个判断的步骤,如不允许名称重复的情况。在创建新的记录和更新已有记录都需要进行必要的判断,保证数据的有效性和不重复性。

如对于省份管理界面来说,我们不能运行重复录入省份名称,那么就需要在录入数据或者更新数据的时候,进行必要的存在性判断。

那么上面的处理是如何实现的呢。

主要的界面实现代码如下所示。

if (string.IsNullOrEmpty(ID))
{
//判断存在条件 var countDto = new ProvincePagedDto() { ProvinceName = this.txtProvince.Text };bool isExist = await ProvinceApiCaller.Instance.Count(countDto) > 0;if(isExist)
{
MessageDxUtil.ShowTips(
"省份名称已存在,请选择其他名称");this.txtProvince.Focus();return;
}
else{//创建新记录 tempInfo = awaitProvinceApiCaller.Instance.Create(tempInfo);
}
}
else{//判断存在条件,排除本记录同名情况 var countDto = new ProvincePagedDto() { ProvinceName = this.txtProvince.Text, ExcludeId =ID.ToInt64() };bool isExist = await ProvinceApiCaller.Instance.Count(countDto) > 0;if(isExist)
{
MessageDxUtil.ShowTips(
"省份名称已存在,请选择其他名称");this.txtProvince.Focus();return;
}
else{//更新记录 tempInfo = awaitProvinceApiCaller.Instance.Update(tempInfo);
}
}

ProcessDataSaved(
this.btnOK, newEventArgs());this.DialogResult = System.Windows.Forms.DialogResult.OK;

我们发现,这里增加了一个Count的函数用来判断,传入的条件就是前面的分页请求条件。

bool isExist = await ProvinceApiCaller.Instance.Count(countDto) > 0;

我们看看我们的应用服务层的接口实现如下所示。

        /// <summary>
        ///获取指定条件的数量/// </summary>
        /// <param name="input">查找条件</param>
        /// <returns></returns>
        public async virtual Task<int>Count(TGetAllInput input)
{
var query =CreateFilteredQuery(input);return awaitTask.FromResult(query.Count());
}

这里最终还是跳转到 CreateFilteredQuery 函数里面实现判断逻辑了。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override IQueryable<Province>CreateFilteredQuery(ProvincePagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(input.ExcludeId.HasValue, t
=>t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!input.ProvinceName.IsNullOrWhiteSpace(), t =>t.ProvinceName.Contains(input.ProvinceName));
}

这里面包含了两个判断条件,一个是排除指定的ID记录,一个是匹配省份名称。

因为我们在更新记录的时候,需要判断非本记录是否有重复的名称。

//判断存在条件,排除本记录同名情况
var countDto = new ProvincePagedDto() { ProvinceName = this.txtProvince.Text, ExcludeId =ID.ToInt64() };bool isExist = await ProvinceApiCaller.Instance.Count(countDto) > 0;

这个ExcludeId 我们在分页条件里面增加一个固定的属性即可。

以上的分页信息,包含了实体DTO对象的一些属性,我们可以根据需要增加或者减少一部分属性。

另外我们定义的创建省份Dto对象和获取到单个实体的DTO对象,他们的定义和关系如下所示,方便我们在界面上进行操作。

    /// <summary>
    ///创建全国省份表,DTO对象/// </summary>
    public class CreateProvinceDto : EntityDto<long>{/// <summary>
        ///默认构造函数(需要初始化属性的在此处理)/// </summary>
        publicCreateProvinceDto()
{
}
#region Property Members /// <summary> ///省份名称/// </summary> [Required]public virtual string ProvinceName { get; set; }#endregion}/// <summary> ///全国省份表,DTO对象/// </summary> public classProvinceDto : CreateProvinceDto
{

}

固定这些规则后,我们也可以用代码生成工具快速生成对应的DTO文件了。

有了这些分页属性后,我们就可以在应用服务层里面定义自己的过滤规则了,如对于字典类型的应用服务层的筛选条件函数,如下所示。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        protected override IQueryable<DictType>CreateFilteredQuery(DictTypePagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!string.IsNullOrEmpty(input.ExcludeId), t => t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!string.IsNullOrEmpty(input.Name), t =>t.Name.Contains(input.Name))
.WhereIf(
!string.IsNullOrEmpty(input.Remark), t =>t.Remark.Contains(input.Remark))
.WhereIf(
!string.IsNullOrEmpty(input.Code), t => t.Code ==input.Code)
.WhereIf(
!string.IsNullOrEmpty(input.PID), t => t.PID ==input.PID);
}

上面是对于包含、相等或者不等于的三种情况的条件判断,如果我们还需要一个时间区间范围或者数值范围的判断,那么同样可以在这里进行管理规则,如下是针对产品应用服务层的过滤规则,如下代码所示。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override IQueryable<Product>CreateFilteredQuery(ProductPagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!input.ExcludeId.IsNullOrWhiteSpace(), t => t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!input.ProductNo.IsNullOrWhiteSpace(), t => t.ProductNo.Contains(input.ProductNo)) //如需要精确匹配则用Equals .WhereIf(!input.BarCode.IsNullOrWhiteSpace(), t => t.BarCode.Contains(input.BarCode)) //如需要精确匹配则用Equals .WhereIf(!input.MaterialCode.IsNullOrWhiteSpace(), t => t.MaterialCode.Contains(input.MaterialCode)) //如需要精确匹配则用Equals .WhereIf(!input.ProductType.IsNullOrWhiteSpace(), t => t.ProductType.Contains(input.ProductType)) //如需要精确匹配则用Equals .WhereIf(!input.ProductName.IsNullOrWhiteSpace(), t => t.ProductName.Contains(input.ProductName)) //如需要精确匹配则用Equals .WhereIf(!input.Unit.IsNullOrWhiteSpace(), t => t.Unit.Contains(input.Unit)) //如需要精确匹配则用Equals .WhereIf(!input.Note.IsNullOrWhiteSpace(), t => t.Note.Contains(input.Note)) //如需要精确匹配则用Equals .WhereIf(!input.Description.IsNullOrWhiteSpace(), t => t.Description.Contains(input.Description)) //如需要精确匹配则用Equals//状态 .WhereIf(input.Status.HasValue, t => t.Status==input.Status)//成本价区间查询 .WhereIf(input.PriceStart.HasValue, s => s.Price >=input.PriceStart.Value)
.WhereIf(input.PriceEnd.HasValue, s
=> s.Price <=input.PriceEnd.Value)//销售价区间查询 .WhereIf(input.SalePriceStart.HasValue, s => s.SalePrice >=input.SalePriceStart.Value)
.WhereIf(input.SalePriceEnd.HasValue, s
=> s.SalePrice <=input.SalePriceEnd.Value)//特价区间查询 .WhereIf(input.SpecialPriceStart.HasValue, s => s.SpecialPrice >=input.SpecialPriceStart.Value)
.WhereIf(input.SpecialPriceEnd.HasValue, s
=> s.SpecialPrice <=input.SpecialPriceEnd.Value)
.WhereIf(input.IsUseSpecial.HasValue, t
=> t.IsUseSpecial == input.IsUseSpecial) //如需要精确匹配则用Equals//最低折扣区间查询 .WhereIf(input.LowestDiscountStart.HasValue, s => s.LowestDiscount >=input.LowestDiscountStart.Value)
.WhereIf(input.LowestDiscountEnd.HasValue, s
=> s.LowestDiscount <=input.LowestDiscountEnd.Value)//创建日期区间查询 .WhereIf(input.CreationTimeStart.HasValue, s => s.CreationTime >=input.CreationTimeStart.Value)
.WhereIf(input.CreationTimeEnd.HasValue, s
=> s.CreationTime <=input.CreationTimeEnd.Value);
}

以上就是我们深入对分页查询和判断是否存在接口的细节处理,可以包含很多自定义的条件,如等于或不等于、包含或者不包含,区间查询(大于或者小于等)条件的处理。对于省份城市行政区管理模块的重复性判断,我们通过Count函数来判断,同时在后台应用服务层对这些参数进行规则过滤即可。

我们了解ABP框架内部自动记录审计日志和登录日志的,但是这些信息只是在相关的内部接口里面进行记录,并没有一个管理界面供我们了解,但是其系统数据库记录了这些数据信息,我们可以为它们设计一个查看和导出这些审计日志和登录日志的管理界面。本篇随笔继续ABP框架的系列介绍,一步步深入了解ABP框架的应用开发,介绍审计日志和登录日志的管理。

1、审计日志和登录日志的基础

审计日志,设置我们在访问或者调用某个应用服务层接口的时候,横切面流下的一系列操作记录,其中记录我们访问的服务接口,参数,客户端IP地址,访问时间,以及异常等信息,这些操作都是在ABP系统自动记录的,如果我们需要屏蔽某些服务类或者接口,则这些就不会记录在里面,否则默认是记录的。

登录日志,这个就是用户尝试登录的时候,留下的记录信息,其中包括用户的登录用户名,ID,IP地址、登录时间,以及登录是否成功的状态等信息。

我们查看系统数据库,可以看到对应这两个部分的日志表,如下所示。

在ABP框架内部基础项目Abp里面,我们可以看到对应的领域对象实体和Store管理类,不过并没有在应用层的对应服务和相关的DTO,我们需要实现一个审计日志和登陆日志的管理功能界面,界面效果如下所示。

我们搜索ABP项目,查找到审计日志的相关类(包含领域对象实体和Store管理类),如下界面截图。

同样对于系统登录日志对象,我们查找到对应的领域实体和对应的Manger业务逻辑类。

这些也就代表它们都有底层的实现,但是没有服务层应用和DTO对象,因此我们需要扩展这些内容才能够管理显示这些记录信息。

前面介绍过,默认的一般应用服务层和接口,都是会进行审计记录写入的,如果我们需要屏蔽某些应用服务层或者接口,不进行审计信息的记录,那么需要使用特性标记[DisableAuditing]来管理。

如我们针对审计日志应用层接口的访问,我们不想让它多余的记录,那么就设置这个标记即可。

或者屏蔽某些接口

另外,如果我们不想公布某些特殊的接口访问,那么我们可以通过标记 [RemoteService(false)]  进行屏蔽,这样在Web API层就不会公布对应的接口了。

如对于审计日志的记录,增删改我们都不允许客户端进行操作,那么我们把对应的应用服务层接口屏蔽即可。

2、系统审计日志和登录日志的完善

前面介绍了,审计日志和登陆日志的处理,Abp系统只是做了一部分底层的内容,我们如果进行这些信息的管理,我们需要完善它,增加对应的DTO类和应用服务层接口和接口实现。

首先我们根据底层的领域实体对象的属性,复制过来作为对应DTO对象的属性,并增加对应的分页条件DTO对象,由于我们不需要进行创建,因此不需要增加Create***Dto对象类。

如对于审计日志的DTO对象,我们定义如下所示(主要复制领域对象的属性)。

而分页处理的DTO对象如下所示,我们主要增加一个用户名和创建时间区间的条件。

对于登录日志的DTO对象,我们依葫芦画瓢,也是如此操作即可。

登录日志的分页对象Dto如下所示、

完善了这些DTO对象,下一步我们需要创建对应的应用服务层类,这样我们才能在客户端通过Web API获取对应的数据。

首先我们来定义审计日志应用服务类,如下所示。

    [DisableAuditing] //屏蔽这个AppService的审计功能
[AbpAuthorize]public class AuditLogAppService : AsyncCrudAppService<AuditLog, AuditLogDto, long, AuditLogPagedDto>, IAuditLogAppService<AuditLogDto, long, AuditLogPagedDto>{private readonly IRepository<AuditLog, long>_repository;private readonlyIAuditingStore _stroe;private readonly IRepository<User, long>_userRepository;public AuditLogAppService(IRepository<AuditLog, long> repository, IAuditingStore stroe, IRepository<User, long> userRepository) : base(repository)
{
_repository
=repository;
_stroe
=stroe;
_userRepository
=userRepository;
}

......

其中我们需要IRepository<User, long>用来转义用户ID为对应的用户名,这样对于我们显示有帮助。

默认来说,这个应用服务层已经具有常规的增删改查、分页等基础接口了,但是我们不需要对外公布增删改接口,我们需要重写实现把它屏蔽。

        /// <summary>
        ///屏蔽创建接口/// </summary>
        [RemoteService(false)]public override Task<AuditLogDto>Create(AuditLogDto input)
{
return base.Create(input);
}
/// <summary> ///屏蔽更新接口/// </summary> [RemoteService(false)]public override Task<AuditLogDto>Update(AuditLogDto input)
{
return base.Update(input);
}
/// <summary> ///屏蔽删除接口/// </summary> [RemoteService(false)]public override Task Delete(EntityDto<long>input)
{
return base.Delete(input);
}

那么我们就剩下GetAll和Get两个方法了,我们如果不需要转义特殊内容,我们就可以不重写它,但是我们这里需要对用户ID转义为用户名称,那么需要进行一个处理,如下所示。

[DisableAuditing]public override Task<PagedResultDto<AuditLogDto>>GetAll(AuditLogPagedDto input)
{
var result = base.GetAll(input);foreach (var item inresult.Result.Items)
{
ConvertDto(item);
//对用户名称进行解析 }returnresult;
}
[DisableAuditing]
public override Task<AuditLogDto> Get(EntityDto<long>input)
{
var result = base.Get(input);
ConvertDto(result.Result);
returnresult;
}
/// <summary> ///对记录进行转义/// </summary> /// <param name="item">dto数据对象</param> /// <returns></returns> protected virtual voidConvertDto(AuditLogDto item)
{
//用户名称转义 if(item.UserId.HasValue)
{
item.UserName
=_userRepository.Get(item.UserId.Value).UserName;
}
//IP地址转义 if (!string.IsNullOrEmpty(item.ClientIpAddress))
{
item.ClientIpAddress
= item.ClientIpAddress.Replace("::1", "127.0.0.1");
}
}

这里主要就用户ID和IP地址进行一个正常的转义处理,这个也是我们常规接口需要处理的一种常见的情况之一。

排序我们是以执行时间进行排序,倒序显示即可,因此重写排序函数。

        /// <summary>
        ///自定义排序处理/// </summary>
        /// <param name="query"></param>
        /// <param name="input"></param>
        /// <returns></returns>
        protected override IQueryable<AuditLog> ApplySorting(IQueryable<AuditLog>query, AuditLogPagedDto input)
{
return base.ApplySorting(query, input).OrderByDescending(s => s.ExecutionTime);//时间降序 }

一般情况下,我们就基本完成了这个模块的处理了,这样我们在界面上在花点功夫就可以调用这个API接口进行显示信息了,如下界面是我编写的审计日志分页列表显示界面。

明细展示界面如下所示。

上面列表界面管理中,如果我们还能够以用户进行过滤,那就更好了,因此需要添加一个用户名进行过滤(注意不是用户ID),系统表里面没有用户名称。

如果我们需要用户名称过滤,如下界面所示。

那么我们就需要在应用服务层的过滤函数里面处理相应的规则了。

我们先创建一个审计日志和用户信息的集合对象,如下所示。

    /// <summary>
    ///审计日志和用户的领域对象集合/// </summary>
    public classAuditLogAndUser
{
public AuditLog AuditLog { get;set;}public User User { get; set; }
}

然后在 CreateFilteredQuery 函数里面进行处理,如下代码所示。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">分页查询Dto对象</param>
        /// <returns></returns>
        protected override IQueryable<AuditLog>CreateFilteredQuery(AuditLogPagedDto input)
{
//构建关联查询Query var query = from auditLog inRepository.GetAll()
join user
in_userRepository.GetAll() on auditLog.UserId equals user.Id into userJoinfrom joinedUser inuserJoin.DefaultIfEmpty()whereauditLog.UserId.HasValueselect new AuditLogAndUser { AuditLog = auditLog, User =joinedUser };//过滤分页条件 returnquery
.WhereIf(
!string.IsNullOrEmpty(input.UserName), t =>t.User.UserName.Contains(input.UserName))
.WhereIf(input.ExecutionTimeStart.HasValue, s
=> s.AuditLog.ExecutionTime >=input.ExecutionTimeStart.Value)
.WhereIf(input.ExecutionTimeEnd.HasValue, s
=> s.AuditLog.ExecutionTime <=input.ExecutionTimeEnd.Value)
.Select(s
=>s.AuditLog);
}

上面其实就是先通过EF的关联表查询,返回一个集合记录,然后在判断用户名是否在集合里面,最后返回所需的实体对象列表。

这个EF的关联表查询非常关键,这个也是我们联合查询的精髓所在,通过LINQ的方式,可以很方便实现关联表的查询处理并获得对应的结果。

而对于用户登录日志,由于系统记录了用户名,那么过滤用户名,这不需要这么大费周章关联表进行处理,只需要判断数据库字段对应情况即可,这种方便很多。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        protected override IQueryable<UserLoginAttempt>CreateFilteredQuery(UserLoginAttemptPagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!string.IsNullOrEmpty(input.UserNameOrEmailAddress), t =>t.UserNameOrEmailAddress.Contains(input.UserNameOrEmailAddress))
.WhereIf(input.CreationTimeStart.HasValue, s
=> s.CreationTime >=input.CreationTimeStart.Value)
.WhereIf(input.CreationTimeEnd.HasValue, s
=> s.CreationTime <=input.CreationTimeEnd.Value);
}

同样系统用户登录日志界面如下所示。

用户登录明细界面效果如下所示。

以上就是对于审计日志和用户登录日志的扩展实现,包括了对相关DTO的增加和实现应用服务层接口,以及对Web API Caller层的实现。

    /// <summary>
    ///审计日志的Web API调用处理/// </summary>
    public class AuditLogApiCaller : AsyncCrudApiCaller<AuditLogDto, long, AuditLogPagedDto>, IAuditLogAppService<AuditLogDto, long, AuditLogPagedDto>{/// <summary>
        ///提供单件对象使用/// </summary>
        public staticAuditLogApiCaller Instance
{
get{return Singleton<AuditLogApiCaller>.Instance;
}
}
/// <summary> ///默认构造函数/// </summary> publicAuditLogApiCaller()
{
this.DomainName = "AuditLog";//指定域对象名称,用于组装接口地址 }
}

由于只是部分实现功能,我们还是可以基于前面介绍开发模式(利用代码生成工具Database2Sharp快速生成)来实现ABP优化框架类文件的生成,以及界面代码的生成,然后进行一定的调整就是本项目的代码了。

代码生成工具的ABP项目代码模板,和基于ABPWinform界面代码的模板,是我基于实际项目的反复优化和验证,并尽量减少冗余代码而完成的一种快速开发方式,基于这样开发方式可以大大减少项目开发的难度,提高开发效率,并完全匹配整个框架的需要,是一种非常惬意的快速开发方式。