wenmo8 发布的文章

在上篇随笔《
C#开发微信门户及应用(19)-微信企业号的消息发送(文本、图片、文件、语音、视频、图文消息等)
》介绍了有关企业号的消息发送,官方特别声明消息是不用加密发送的。但是在回调的服务器上,也就是我们网站的服务器上,微信传过来的消息是加密的,需要我们调用类库对消息和事件进行解密操作,由于官方的例子不全,因此摸索了不少时间,最终顺利解密收到的各种消息和事件。本文主要介绍 微信企业号的消息和事件的接收处理及解密操作。

1、企业号回调模式的设置

和公众号一样,微信企业号如果需要进行二次开发,也是需要在后台设置好对应的回调参数,如下界面所示。

设置好这些后,检查通过后,我们就可以在自己微信应用服务器上进行消息的收发操作了。

在回调的消息入口处,我们需要对POST数据和普通的GET数据进行分开处理,GET数据是微信自身的验证处理,POST数据是微信消息的交互操作。

    /// <summary>
    ///企业号回调信息接口。统一接收并处理信息的入口。/// </summary>
    public classcorpapi : IHttpHandler
{
/// <summary> ///处理企业号的信息/// </summary> /// <param name="context"></param> public voidProcessRequest(HttpContext context)
{

上面我们定义了一个一般应用处理程序来对消息进行处理。

然后我们分开不同的消息类型(POST、GET 方式),针对性的进行处理。

                    if (HttpContext.Current.Request.HttpMethod.ToUpper() == "POST")
{
using (Stream stream =HttpContext.Current.Request.InputStream)
{
Byte[] postBytes
= newByte[stream.Length];
stream.Read(postBytes,
0, (Int32)stream.Length);
postString
=Encoding.UTF8.GetString(postBytes);
}
if (!string.IsNullOrEmpty(postString))
{
Execute(postString, accountInfo);
}
}
else{
Auth(accountInfo);
}

2、微信回调消息的验证

下面是微信对于回调模式,验证URL的说明。

验证URL有效性

当你提交以上信息时,企业号将发送GET请求到填写的URL上,GET请求携带四个参数,
企业在获取时需要做urldecode处理
,否则会验证不成功。

参数 描述 是否必带
msg_signature 微信加密签名,msg_signature结合了企业填写的token、请求中的timestamp、nonce参数、加密的消息体
timestamp 时间戳
nonce 随机数
echostr 加密的随机字符串,以msg_encrypt格式提供。需要解密并返回echostr明文,解密后有random、msg_len、msg、$CorpID四个字段,其中msg即为echostr明文 首次校验时必带

企业通过参数msg_signature对请求进行校验,如果确认此次GET请求来自企业号,那么
企业应用对echostr参数解密并原样返回echostr明文(不能加引号)
,则接入验证生效,回调模式才能开启。

后续回调企业时都会在请求URL中带上以上参数(echostr除外),校验方式与首次验证URL一致。

根据上面的说明,我们需要获取这些参数,然后调用微信提供的消息处理函数进行加解密处理。

在验证URL的Auth(accountInfo);操作里面,我们可以看到核心的内容如下所示,就是获取到这些传递过来的参数信息,然后交给基类处理消息的签名内容。

                        #region 具体处理逻辑
                        string echoString = HttpContext.Current.Request.QueryString["echoStr"];string signature = HttpContext.Current.Request.QueryString["msg_signature"];//企业号的 msg_signature
                        string timestamp = HttpContext.Current.Request.QueryString["timestamp"];string nonce = HttpContext.Current.Request.QueryString["nonce"];string decryptEchoString = "";if (new CorpBasicApi().CheckSignature(token, signature, timestamp, nonce, corpId, encodingAESKey, echoString, refdecryptEchoString))
{
if (!string.IsNullOrEmpty(decryptEchoString))
{
HttpContext.Current.Response.Write(decryptEchoString);
HttpContext.Current.Response.End();
}
}
#endregion

验证代码部门如下所示。

        /// <summary>
        ///验证企业号签名/// </summary>
        /// <param name="token">企业号配置的Token</param>
        /// <param name="signature">签名内容</param>
        /// <param name="timestamp">时间戳</param>
        /// <param name="nonce">nonce参数</param>
        /// <param name="corpId">企业号ID标识</param>
        /// <param name="encodingAESKey">加密键</param>
        /// <param name="echostr">内容字符串</param>
        /// <param name="retEchostr">返回的字符串</param>
        /// <returns></returns>
        public bool CheckSignature(string token, string signature, string timestamp, string nonce, string corpId, string encodingAESKey, string echostr, ref stringretEchostr)
{
WXBizMsgCrypt wxcpt
= newWXBizMsgCrypt(token, encodingAESKey, corpId);int result = wxcpt.VerifyURL(signature, timestamp, nonce, echostr, refretEchostr);if (result != 0)
{
LogTextHelper.Error(
"ERR: VerifyURL fail, ret:" +result);return false;
}
return true;
}

3、企业号的消息处理

上面介绍了,微信企业号对URL的验证过程,还有另外一个消息处理过程,就是微信服务器把消息发送给我们自己的应用服务器进行处理的过程,我们应用服务器需要在收到消息后,及时进行常规回复处理。

也就是下面的代码逻辑。

                    if (HttpContext.Current.Request.HttpMethod.ToUpper() == "POST")
{
using (Stream stream =HttpContext.Current.Request.InputStream)
{
Byte[] postBytes
= newByte[stream.Length];
stream.Read(postBytes,
0, (Int32)stream.Length);
postString
=Encoding.UTF8.GetString(postBytes);
}
if (!string.IsNullOrEmpty(postString))
{
Execute(postString, accountInfo);
}
}

同样,我们给微信服务器回应消息的时候,我们也需要获得相应的参数,然后再行构造信息回答。

            string echoString = HttpContext.Current.Request.QueryString["echoStr"];string signature = HttpContext.Current.Request.QueryString["msg_signature"];//企业号的 msg_signature
            string timestamp = HttpContext.Current.Request.QueryString["timestamp"];string nonce = HttpContext.Current.Request.QueryString["nonce"];

而另外一些参数信息,则是来源于我们企业号账号的配置参数。

            //获取配置参数并对加解密函数初始化
            string CorpToken =accountInfo.Token;string AESKey =accountInfo.EncodingAESKey;string CorpId = accountInfo.CorpID;

然后使用微信提供的消息加解密类,就可以顺利对消息进行加解密的处理了。具体操作代码如下所示。

            //根据参数信息,初始化微信对应的消息加密解密类
            WXBizMsgCrypt wxcpt = newWXBizMsgCrypt(CorpToken, AESKey, CorpId);//对收到的密文进行解析处理
            string sMsg = "";  //解析之后的明文
            int flag = wxcpt.DecryptMsg(signature, timestamp, nonce, postStr, refsMsg);if (flag == 0)
{
//LogTextHelper.Info("记录解密后的数据:");//LogTextHelper.Info(sMsg);//记录解密后的数据 CorpApiDispatch dispatch= newCorpApiDispatch();string responseContent =dispatch.Execute(sMsg);//加密后并发送//LogTextHelper.Info(responseContent); string encryptResponse = "";
timestamp
=DateTime.Now.DateTimeToInt().ToString();
wxcpt.EncryptMsg(responseContent, timestamp, nonce,
ref encryptResponse, refsignature);

HttpContext.Current.Response.ContentEncoding
=Encoding.UTF8;
HttpContext.Current.Response.Write(encryptResponse);
}
else{
LogTextHelper.Info(
"解密消息失败!");
}

最终,我们把解密完成的消息交给对应的封装类进行统一处理就可以了。

                CorpApiDispatch dispatch = newCorpApiDispatch();string responseContent = dispatch.Execute(sMsg);

这样我们在企业号API的封装,就可以只需要关注消息如何应答的逻辑就可以了,其他的不用关心。


如果对这个《C#开发微信门户及应用》系列感兴趣,可以关注我的其他文章,系列随笔如下所示:

C#开发微信门户及应用(25)-微信企业号的客户端管理功能

C#开发微信门户及应用(24)-微信小店货架信息管理

C#开发微信门户及应用(23)-微信小店商品管理接口的封装和测试

C#开发微信门户及应用(22)-微信小店的开发和使用

C#开发微信门户及应用(21)-微信企业号的消息和事件的接收处理及解密

C#开发微信门户及应用(20)-微信企业号的菜单管理

C#开发微信门户及应用(19)-微信企业号的消息发送(文本、图片、文件、语音、视频、图文消息等)

C#开发微信门户及应用(18)-微信企业号的通讯录管理开发之成员管理

C#开发微信门户及应用(17)-微信企业号的通讯录管理开发之部门管理

C#开发微信门户及应用(16)-微信企业号的配置和使用

C#开发微信门户及应用(15)-微信菜单增加扫一扫、发图片、发地理位置功能

C#开发微信门户及应用(14)-在微信菜单中采用重定向获取用户数据

C#开发微信门户及应用(13)-使用地理位置扩展相关应用

C#开发微信门户及应用(12)-使用语音处理

C#开发微信门户及应用(11)--微信菜单的多种表现方式介绍

C#开发微信门户及应用(10)--在管理系统中同步微信用户分组信息

C#开发微信门户及应用(9)-微信门户菜单管理及提交到微信服务器

C#开发微信门户及应用(8)-微信门户应用管理系统功能介绍

C#开发微信门户及应用(7)-微信多客服功能及开发集成

C#开发微信门户及应用(6)--微信门户菜单的管理操作

C#开发微信门户及应用(5)--用户分组信息管理

C#开发微信门户及应用(4)--关注用户列表及详细信息管理

C#开发微信门户及应用(3)--文本消息和图文消息的应答


C#开发微信门户及应用(2)--微信消息的处理和应答


C#开发微信门户及应用(1)--开始使用微信接口

在我们很多程序的登陆过程中,如果对于一些复杂的内容,可能都有一些配置管理的对话框,本文介绍的混合式开发框架,是指什么呢,我下面给了一个定义。

《混合式开发框架》是指混合了传统《Winform开发框架》和《WCF开发框架》的特点,可以在两者之间自由切换,统一了系统界面层对业务服务的调用模式,所有组件模块均实现两种方式的调用,是一种弹性化非常好的框架应用,既可用于单机版软件或者基于局域网内的应用软件,也可以用于分布式技术的互联网环境应用。

对于这种混合式模式,上面说到了有两种方式,一种是常规的访问数据库方式,一种是访问WCF服务方式,而访问WCF服务,可能有基于局域网(内网)模式,也可能是基于互联网(外网)模式,那么我们就可能配置三种登陆方式,如果每次需要手工修改配置文件,可能比较麻烦,如果我们能够通过程序方式来实现参数的界面化配置,那么工作就轻松很多,也不容易出错。

1、配置界面和配置内容介绍

关于配置的管理,我肯定是首先推荐SettingsProvider.net这个组件,大家需要了解这个组件的使用,可以参考我的随笔《
Winform开发框架之参数配置管理功能实现-基于SettingsProvider.net的构建
》,这里可以结合FireFoxDialog界面效果实现比较友好的配置管理,界面效果如下所示。

在WCF的配置文件中,我们可以看到很多对于IP地址的配置,而一般我们把WCF的配置信息放到一个独立的配置文件里面,方便管理。

对于局域网的WCF客户端的配置信息,大概效果如下所示。

而对于外网,也只是把这些IP换成公网IP即可,一般来说,这些是独立的配置文件,我们一般还有一个主配置文件**.exe.config,用来配置我们框架常用到的一些配置信息。

如下所示,在我们混合式需要访问本地数据库的时候,就可以在主配置文件里面保存主要的数据库连接信息。

2、配置对话框的设计和实现

前面介绍了关于参数配置和WCF配置文件的一些相关信息,基于这些考虑,我们应该在混合式模式的登陆界面里面,设计好相关界面,让用户可以方便配置访问不同的数据源,以便实现真正混合式的强大功能和便利性。

首先我们来介绍一下我的混合式框架,登陆界面的设计。

单击参数设置按钮,可以进入登陆访问参数的界面对话框,在里面可以友好直观的配置相关的WCF访问参数,界面如下所示,这里面使用了类似FireFox的界面组件,后台的文件存储,主要是基于本地的XML存储,使用了SettingsProvider.net这个组件进行数据的存储管理。

如果是选择“
单机版模式
”,那么我们把对应的
网络方式
设置为不可选择,这样的效果比较好体现本地模式的排他性。

这些界面基本上介绍了如何处理混合式开发框架的参数配置内容了,具体的实现操作是如何的呢?下面我们来一步步分析介绍下。

首先我们定义一个用来保存和加载参数的一个实体类,用来存储对应的参数信息。

    /// <summary>
    ///用户登录的连接方式、访问方式/// </summary>
    public classLoginParameter
{
/// <summary> ///系统最后登录账号/// </summary> [DefaultValue("admin")]public string LoginId { get; set; }/// <summary> ///登陆密码/// </summary> [ProtectedString]public string Password { get; set; }/// <summary> ///记住密码/// </summary> [DefaultValue(false)]public bool RememberPassword { get; set; }/// <summary> ///是否为本地数据库连接方式,否则为使用WCF服务模式(内网,外网)/// </summary> [DefaultValue(false)]public bool IsLocalDatabase { get; set; }/// <summary> ///内网WCF的主机地址/// </summary> [DefaultValue("192.168.1.10")]public string InternalWcfHost { get; set; }/// <summary> ///内网WCF的端口/// </summary> [DefaultValue(8000)]public int InternalWcfPort { get; set; }/// <summary> ///外网WCF的主机地址/// </summary> [DefaultValue("183.6.161.193")]public string ExternalWcfHost { get; set; }/// <summary> ///外网WCF的端口/// </summary> [DefaultValue(8000)]public int ExternalWcfPort { get; set; }
}

这样我们就可以以实体类为载体,对数据进行获取和保存操作了。

配置信息的加载和保存操作代码如下所示,这些保存操作就是基于SettingsProvider.net的。

        /// <summary>
        ///从本地XML文件中加载参数信息/// </summary>
        private voidLoadParameter()
{
store
= new PortableStorage();//保存在本地计算机 settings = newSettingsProvider(store);
parameter
= settings.GetSettings<LoginParameter>();if (parameter != null)
{
this.cmbzhanhao.Text =parameter.LoginId;this.chkRemember.Checked =parameter.RememberPassword;if(parameter.RememberPassword)
{
this.tbPass.Text =parameter.Password;
}
else{this.tbPass.Text = "";
}
this.chkLocalVersion.Checked =parameter.IsLocalDatabase;//确保为正确的访问方式,网络版还是单机版 SetAccessType(this.chkLocalVersion.Checked);
}
}
/// <summary> ///把用户的信息保存到本地XML文件里面/// </summary> private voidSaveParameter()
{
store
= new PortableStorage();//保存在本地计算机 settings = newSettingsProvider(store);
parameter
= settings.GetSettings<LoginParameter>();if (parameter != null)
{
parameter.LoginId
= this.cmbzhanhao.Text;if(parameter.RememberPassword)
{
parameter.Password
= this.tbPass.Text;
}
else{
parameter.Password
= "";
}
parameter.IsLocalDatabase
= this.chkLocalVersion.Checked;

settings.SaveSettings
<LoginParameter>(parameter);
}
}

对于如果选择单机版模式,那么我们需要设置相应的参数,用来告诉混合式框架采用本地数据库模式,具体代码如下所示。

        /// <summary>
        ///单机版模式的复选框事件处理/// </summary>
        private void chkLocalVersion_CheckedChanged(objectsender, EventArgs e)
{
SetAccessType(
this.chkLocalVersion.Checked);
}
/// <summary> ///设置为网络方式还是单机版模式/// </summary> /// <param name="localType">是否为单机版模式</param> private void SetAccessType(boollocalType)
{
this.lblNetType.Enabled = !localType;this.radNetType.Enabled = !localType;

AppConfig config
= newAppConfig();
config.AppConfigSet(
"CallerType", localType ? "win" : "wcf");
ConfigurationManager.RefreshSection(
"appSettings");
}

如果是网络方式,还需要区分内网模式,还是外网模式,根据参数来对相关的配置文件进行修改,具体逻辑代码如下所示。

        /// <summary>
        ///网络方式(内网、外网)切换处理事件/// </summary>
        private void radNetType_SelectedIndexChanged(objectsender, EventArgs e)
{
ChangeConfig();
}
/// <summary> ///修改配置文件/// </summary> private voidChangeConfig()
{
if (parameter != null)
{
bool isIntranet = radNetType.EditValue.ToString() == "内网";if(isIntranet)
{
UpdateConfig(parameter.InternalWcfHost, parameter.InternalWcfPort);
}
else{
UpdateConfig(parameter.ExternalWcfHost, parameter.ExternalWcfPort);
}
}
else{
MessageDxUtil.ShowError(
"获取参数信息失败");
}
}
/// <summary> ///WCF模式下,修改配置文件中的主机地址信息和端口/// </summary> /// <param name="serverIPAddress">主机地址信息</param> /// <param name="serverPort">端口</param> private void UpdateConfig(string serverIPAddress, intserverPort)
{
string basePath =System.Environment.CurrentDirectory;
UpdateConfigFile(serverIPAddress, serverPort, Path.Combine(basePath,
"BaseWcfConfig.config"));
UpdateConfigFile(serverIPAddress, serverPort, Path.Combine(basePath,
"WcfConfig.config"));
}
/// <summary> ///通过正则标识方式替换其中的主机信息和端口参数/// </summary> /// <param name="serverIPAddress">主机地址信息</param> /// <param name="serverPort">端口</param> /// <param name="exeFilePath">配置文件地址</param> private void UpdateConfigFile(string serverIPAddress, int serverPort, stringexeFilePath)
{
string address =File.ReadAllText(exeFilePath, System.Text.Encoding.UTF8);string pattern = "://.*?/";string replacement = string.Format("://{0}:{1}/", serverIPAddress, serverPort);
address
=Regex.Replace(address, pattern, replacement);

File.WriteAllText(exeFilePath, address, System.Text.Encoding.UTF8);
}

以上就是我对于混合式开发框架的登陆方式和登陆参数进行的配置信息管理的一个案例,希望对大家有帮助。

很久没有写博客了,一些读者也经常问问一些问题,不过最近我确实也很忙,除了处理日常工作外,平常主要的时间也花在了继续研究微软的实体框架(
EntityFramework
)方面了。这个实体框架加入了很多特性(例如LINQ等),目前也已经应用的比较成熟了,之所以一直没有整理成一个符合自己开发模式的实体框架,是因为这个框架和原来我的基于EnterpriseLibrary的模式还是有很大的不同,不过
实体框架推出来也很久了,目前也去到了EntityFramework6了,听说7也快出来了。

随着我自己参考阅读了大量的项目源码以及对实体框架各个技术点的学习深入,对其中很多的方面都有自己的一些见解和心得,希望通过这个系列,能够和读者一步步分析,一步步深入学习这个微软目前最为流行的.NET开发框架。本篇主要从基础开始一步步介绍基于泛型的仓储模式实体框架(The Entity Framework of Generic Repository Pattern ),希望大家耐心阅读。

1、实体框架的初步印象

最简单的实体框架,你可以在Winform或者Web项目里面添加一个【ADO.NET实体数据模型】项开始,一步步创建一个基于SqlServer的实体框架项目。最开始,我们可以不考虑什么设计模式,能够使用即可,因此我们可能创建一个比较简单的项目代码,这个有助于我们了解实体框架的一些基础工作原理。

为这个项目选定数据连接以及供测试使用的一两个表的对象,然后完成创建工作,这个【ADO.NET实体数据模型】创建完成后,我们可以看到项目里面添加了一个Model1.edmx的文件,并且同时生成了几个项目文件,其中包括了数据访问对象SqlserverContext和几个实体类(默认为表名称),我们也可以打开edmx的文件进行实体类属性的修改,如下所示。

默认生成后,我们就可以使用这个数据访问上下文对象SqlserverContext, 来进行相关的数据处理操作了,简单的测试代码如下所示。

        private voidGetIntData()
{
//创建数据访问对象 var context = newSqlserverContext();//新建一个实体类并赋值 TB_Province info = newTB_Province();
info.ID
= 100001;
info.ProvinceName
= "测试省份";
context.TB_Province.Add(info);
context.SaveChanges();
//根据主键判断记录是否存在 TB_Province info2 =context.TB_Province.Find(info.ID);if (info2 != null)
{
Console.WriteLine(
"记录已存在!");//如果存在对象,先删除 context.TB_Province.Remove(info2);
context.SaveChanges();
//检查是否删除对象 info2 =context.TB_Province.Find(info.ID);if (info2 == null)
{
Console.WriteLine(
"记录已删除!");
}
}
//把记录全部获取并绑定到列表上。 var list =context.TB_Province.ToList();this.dataGridView1.DataSource =list;
}

最后获得的界面效果就是能够顺利执行各种操作后把记录显示出来到列表上了。

2、实体框架的工作原理

1)数据访问上下文对象介绍

从上面的代码我们可以看到,数据访问上下文对象SqlserverContext已经可以直接和数据库交互了,能够实现表对象基本增删改查的操作功能了,那么这个类是如何的呢?为什么具有这个功能呢?

我们先看看它的代码,
SqlserverContext的类代码如下所示(代码为自动生成的)。

    public partial classSqlserverContext : DbContext
{
publicSqlserverContext()
:
base("name=SqlserverContext")
{
}
protected override voidOnModelCreating(DbModelBuilder modelBuilder)
{
throw newUnintentionalCodeFirstException();
}
public virtual DbSet<TB_City> TB_City { get; set; }public virtual DbSet<TB_Province> TB_Province { get; set; }public virtual DbSet<TB_DictType> TB_DictType { get; set; }
}

其中代码DbSet<TB_Province> TB_Province代表一个具体的数据访问对象,对表TB_Province的数据访问,其他的类似。我们查看.NET的内置对象DbSet的已经支持了一些常规的操作了。

而EMDX文件的本身是一个XML文件,它的内容如下所示。

实体框架本身通过XML映射的方式(ORM方式),封装了从数据库到实体类,以及实体类到数据库的交互过程,具体的过程我们可以参考下面的实体数据模型 (EDM)介绍。

2)实体数据模型 (EDM)介绍

Entity Framework 实体框架的要点是实体数据模型 (EDM),一个用于描述应用程序域对象的概念模型。 Entity Framework 实体框架让开发人员可以针对实体数据模型提出查询,而不必操心数据库的具体操作。 实体数据模型的实体以及实体之间的关系以 XML 形式定义,而开发人员基于该模型的实体来处理强类型化类。

在运行时,利用特定于数据库的 ADO.NET 提供程序,Entity Framework 实体框架将针对实体数据模型而创建的查询转换为存储查询(例如 T-SQL),然后送至数据库。 Entity Framework 将查询结果转换为由强类型化实体类所定义的对象。

实体数据模型 (EDM),由三个概念组成。概念模型由概念架构定义语言文件 (.csdl)来定义,映射由映射规范语言文件 (.msl),存储模型(又称逻辑模型)由存储架构定义语言文件 (.ssdl)来定义。这三者合在一起就是EDM模式。EDM模式在项目中的表现形式就是扩展名为.edmx的文件。这个包含EDM的文件可以使用Visual Studio中的EDM设计器来设计。由于这个文件本质是一个xml文件,可以手工编辑此文件来自定义CSDL、MSL与SSDL这三部分。

CSDL定义了EDM或者说是整个程序的灵魂部分 – 概念模型。这个文件完全以程序语言的角度来定义模型的概念。即其中定义的实体、主键、属性、关联等都是对应于.NET Framework中的类型。

SSDL这个文件中描述了表、列、关系、主键及索引等数据库中存在的概念。

MSL这个文件即上面所述的CSDL与SSDL的对应,主要包括CSDL中属性与SSDL中列的对应。

通过以上三个XML文件的映射关系,在程序里面,就主要是利用强类型数据的实体类进行处理了,而对实体类的任何处理修改,最终会解析后得到相应的数据库执行语句,然后进行提交处理了。

3、基于泛型的仓储模式实体框架

如果基于第一点来构建框架,虽然很快速,但是这样的做法在中大型的项目里肯定不可取,因为生成后的代码还需要进行多个步骤的修改调整,而且也没有很好实现重用的目的,
很多地方需要自己手动编码处理,结构也不是很清晰,因此需要对框架进行一步步的优化和提炼。

在介绍基于泛型的仓储模式实体框架(The Entity Framework of Generic Repository Pattern )前,我们先来回顾一下我之前的Winform开发框架分层结构,这个基于Enterprise Library的框架,常见的分层模式,可以分为UI层、BLL层、DAL层、IDAL层、Entity层、公用类库层等等。

这种分层可以在数据库设计完成后,可以通过代码生成工具,获取到表对象的信息和关系,直接快速生成相应的分层代码,从而实现架构、分层、命名规则等方面的一致化,并且是快速开发。

而且这种分层模式也是一种比较通用的分层结构了,那么我们要介绍的实体框架是否也可以依照这种方式来构建呢?是否可以结合代码生成工具的生成模板来进行整体性框架的开发呢?

下面我们来介绍一下泛型的仓储模式框架的具体实现过程。

1)实体类的代码如下所示(先按表名生成)。

    public partial classTB_City
{
public long ID { get; set; }public string CityName { get; set; }public string ZipCode { get; set; }public Nullable<long> ProvinceID { get; set; }
}

2)数据访问基类接口层(定义了几个测试的基类接口)

    /// <summary>
    ///数据访问层基类接口/// </summary>
    /// <typeparam name="T">实体对象类型</typeparam>
    public interface IBaseDAL<T> where T : class{
T Get(
objectid);

IList
<T> GetAll(Expression<Func<T, bool>>whereCondition);

IList
<T>GetAll();
}

3)数据访问层基类实现层

    /// <summary>
    ///数据访问层基类实现层/// </summary>
    /// <typeparam name="T">实体对象类型</typeparam>
    public abstract class BaseDAL<T> : IBaseDAL<T>  where T : class{protectedDbContext baseContext;protected IDbSet<T>objectSet;publicBaseDAL(DbContext context)
{
this.baseContext =context;this.objectSet = this.baseContext.Set<T>();
}
public T Get(objectid)
{
returnobjectSet.Find(id);
}
public IList<T>GetAll()
{
return objectSet.ToList<T>();;
}
public IList<T> GetAll(Expression<Func<T, bool>>whereCondition)
{
return objectSet.Where(whereCondition).ToList<T>();
}
}

4)具体数据访问对象接口定义(城市表为例)

    /// <summary>
    ///城市数据访问层接口/// </summary>
    public interface ICityDAL : IBaseDAL<City>{
}

5)具体数据访问对象实现层(城市表为例)

    /// <summary>
    ///城市数据访问对象/// </summary>
    public class CityDAL : BaseDAL<TB_City>{protectedMyDataContext context;/// <summary>
        ///构造函数/// </summary>
        /// <param name="context"></param>
        public CityDAL(MyDataContext context) :base(context)
{
this.context =context;
}

6)数据仓储对象(上下文对象)

    public classMyDataContext : DbContext
{
public MyDataContext() : base("name=sqlserver")
{
}
protected override voidOnModelCreating(DbModelBuilder modelBuilder)
{
throw newUnintentionalCodeFirstException();
}
public virtual DbSet<TB_City> City { get; set; }
}

BLL、IBLL的分层和数据访问层的类似,主要是提高一份,方便做业务整合实现而已,在此不再赘述。

最终实现仓储模式框架的分层结构如下所示。

以上就是我对基于泛型的仓储模式的实体框架的一个初探性的开端,下面会在这个系列里面继续分析其中存在的问题,并继续优化改良这个基于泛型的仓储模式的实体框架。希望大家喜欢并继续支持。

这个系列文章索引如下:

Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)

Entity Framework 实体框架的形成之旅--利用Unity对象依赖注入优化实体框架(2)

Entity Framework 实体框架的形成之旅--基类接口的统一和异步操作的实现(3)

在本系列的第一篇随笔《
Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)
》中介绍了Entity Framework 实体框架的一些基础知识,以及构建了一个简单的基于泛型的仓储模式的框架,例子也呈现了一个实体框架应用的雏形,本篇继续介绍这个主题,继续深化介绍Entity Framework 实体框架的知识,以及持续优化这个仓储模式的实体框架,主要介绍业务逻辑层的构建,以及利用Unity和反射进行动态的对象注册。

1、EDMX文件位置的调整

我们从上篇例子,可以看到这个随笔介绍的仓储模式的实体框架结构如下所示。

但实际上上篇随笔的例子是有点理想化的了,因为我们知道,【ADO.NET实体数据模型】生成的EDMX文件实质上自动生成了数据访问的上下文SqlserverContext,以及几个表的实体类,具体的效果如下所示。

我们理想化的把它放到DAL目录,Entity目录下,实际上是不可以的,至少是有冲突的。

那么我们应该如何处理,才能比较合理的处理这些自动生成的内容呢?另外我们已经把它上升了一层到业务层,具体的BLL分层如何处理数据访问对象的呢,通过什么方式构建数据访问对象?带着这些问题,我们再来一步步分析这个框架的内容。

为了给实体类友好的名称,我们顺便把表名的前缀移除了,如EDMX的图形如下所示。

为了比较好的利用EDMX文件的代码生成,我们把这个文件整体性的移动到了Entity目录下,如下所示。

这样相当于把数据访问的上下文,以及实体类的代码全部移动到Entity命名空间里面去了,虽然可能感觉不太好,但是我们先让它跑起来,具体的细节后面在优化完善。

2、业务逻辑层的设计

我们再来关注下业务逻辑层(包括业务逻辑接口层),和数据访问层类似,我们把它构建如下所示。

1)业务逻辑接口层

    /// <summary>
    ///业务逻辑层基类接口/// </summary>
    /// <typeparam name="T">实体对象类型</typeparam>
    public interface IBaseBLL<T> where T : class{                
T Get(
objectid);

IList
<T> GetAll(Expression<Func<T, bool>>whereCondition);

IList
<T>GetAll();
}

2)业务逻辑层实现

    /// <summary>
    ///业务逻辑基类/// </summary>
    /// <typeparam name="T">实体对象类型</typeparam>
    public abstract class BaseBLL<T>: IBaseBLL<T>  where T : class{protected IBaseDAL<T> baseDAL { get; set; }public BaseBLL(IBaseDAL<T>dal)
{
this.baseDAL =dal;
}
public T Get(objectid)
{
returnbaseDAL.Get(id);
}
public IList<T> GetAll(Expression<Func<T, bool>>whereCondition)
{
returnbaseDAL.GetAll(whereCondition);
}
public IList<T>GetAll()
{
returnbaseDAL.GetAll();
}
}

3)业务对象类的逻辑接口层

    /// <summary>
    ///城市的业务对象接口/// </summary>
    public interface ICityBLL : IBaseBLL<City>{
}

4)业务对象的逻辑层实现

    /// <summary>
    ///城市的业务对象/// </summary>
    public class CityBLL : BaseBLL<City>{protectedICityDAL dal;public CityBLL(ICityDAL dal) : base(dal)
{
this.dal =dal;
}
}

上面基本上完整的阐述了业务逻辑层的实现了,不过我们看到一个问题,就是不管是逻辑层基类,还是具体业务对象的逻辑对象,都没有默认构造函数,我们
不能使用new进行对象的创建!

这是一个严重的问题,那么我们如何才能规避这个问题,能够使我们的业务对象类能够使用默认函数,使用new创建对象呢?这里我们需要引入IOC容器做法,也就是使用微软的Unity进行对象的注入及使用。

3、使用Unity实现对象的依赖注入

1)Unity的简单介绍

Unity是Unity是微软patterns& practices组用C#实现的轻量级,可扩展的依赖注入容器,它为方便开发者建立松散耦合的应用程序,

有以下优点:

1.简化了对象的创建,特别是针对分层对象结构和依赖关系;

2.需求的抽象,允许开发人员在运行时或配置文件中指定依赖关系,简化横切关注点的管理;

3.推迟为容器配置组件的时机,增加了灵活性;

4.服务定位能力,这使客户能够存储或缓存容器;

5.实例和类型拦截

Unity的依赖注入使用例子比较容易理解,具体代码如下所示。

 static void Main( string[] args )
{
//实例化一个控制器 IUnityContainer unityContainer = newUnityContainer();//实现对象注入 unityContainer.RegisterType<IBird, Swallow>();
IBird bird
= unityContainer.Resolve<IBird>();

bird.Say();
}

这个Unity的对象,我们可以通过Nuget进行添加即可,添加后,在项目里面就有对应对应的程序集引用了。

2)引入Unity实现数据访问对象注入,完善逻辑层实现

了解了Unity的使用,我们可以在BaseBLL对象基类类里面构建一个IOC的容器,并在这个容器初始化的时候,注册对应的数据访问层对象即可,如下所示。

    /// <summary>
    ///业务逻辑基类/// </summary>
    /// <typeparam name="T">实体对象类型</typeparam>
    public abstract class BaseBLL<T>: IBaseBLL<T>  where T : class{private static readonly object syncRoot = newObject();protected IBaseDAL<T> baseDAL { get; set; }protected IUnityContainer container { get; set; }/// <summary>
        ///默认构造函数。///默认获取缓存的容器,如果没有则创建容器,并注册所需的接口实现。/// </summary>
        publicBaseBLL() 
{
lock(syncRoot)
{
container
= DALFactory.Instance.Container;if (container == null)
{
throw new ArgumentNullException("container", "container没有初始化");
}
}
}

好,默认在DALFactory的类里面,我们就是在其实例化的时候,把需要的数据访问对象压进去,这样我们就可以在具体的业务对象逻辑类里面实现调用,如下代码所示。

    /// <summary>
    ///城市的业务对象/// </summary>
    public class CityBLL : BaseBLL<City>{protectedICityDAL dal;publicCityBLL()
{
dal
= container.Resolve<ICityDAL>();
baseDAL
=dal;
}
public CityBLL(ICityDAL dal) : base(dal)
{
this.dal =dal;
}
}

如果我们不关心DALFactory里面的构架细节,这个框架已经完成的对象的注入,可以正常使用了。

但是我们还是来看看它的实现细节,我们通过单例模式(饿汉模式)构架IOC容器并注入相应的DAL对象了。

    /// <summary>
    ///实体框架的数据访问层接口的构造工厂。/// </summary>
    public classDALFactory
{
//普通局部变量 private static Hashtable objCache = newHashtable();private static object syncRoot = newObject();private static DALFactory m_Instance = null;/// <summary> ///IOC的容器,可调用来获取对应接口实例。/// </summary> public IUnityContainer Container { get; set; }/// <summary> ///创建或者从缓存中获取对应业务类的实例/// </summary> public staticDALFactory Instance
{
get{if (m_Instance == null)
{
lock(syncRoot)
{
if (m_Instance == null)
{
m_Instance
= newDALFactory();//初始化相关的注册接口 m_Instance.Container = new UnityContainer();//手工加载 m_Instance.Container.RegisterType<ICityDAL, CityDAL>();
m_Instance.Container.RegisterType
<IProvinceDAL, ProvinceDAL>();
}
}
}
returnm_Instance;
}
}

OK,通过上面的Unity,我们实现了对象的注入及使用个,具体的窗体调用代码如下所示。

        private void btnCity_Click(objectsender, EventArgs e)
{
DateTime dt
=DateTime.Now;

CityBLL bll
= newCityBLL();var list =bll.GetAll();this.dataGridView1.DataSource =list;

Console.WriteLine(
"花费时间:{0}", DateTime.Now.Subtract(dt).TotalMilliseconds);
}
private void txtCityName_KeyUp(objectsender, KeyEventArgs e)
{
DateTime dt
=DateTime.Now;
CityBLL bll
= newCityBLL();if(this.txtCityName.Text.Trim().Length > 0)
{
var list = bll.GetAll(s => s.CityName.Contains(this.txtCityName.Text));this.dataGridView1.DataSource =list;
}
else{var list =bll.GetAll();this.dataGridView1.DataSource =list;
}
Console.WriteLine(
"花费时间:{0}", DateTime.Now.Subtract(dt).TotalMilliseconds);
}

我们可以得到具体的界面效果如下所示。

4、使用反射操作,在Unity容器动态注册接口对象

在上面的例子里面,不知道您是否注意到了,我们使用Unity的IOC容器的时候,注册的对象是指定的几个数据访问类。

m_Instance.Container.RegisterType<ICityDAL, CityDAL>();
m_Instance.Container.RegisterType<IProvinceDAL, ProvinceDAL>();

但这种有点类似硬编码的方式,在我们项目如果有大量的这些数据访问类,需要手工添加的话,那真不是一件雅观的事情。

如果代码能够根据接口和接口实现类,自动把我们所需要的接口对象注册进去,那该是多好的啊,可是能做到吗?能!

如果我们是在同一个程序集里面执行的话,那么我们通过反射操作,就可以从这个程序集里面获取对应的接口层(IDAL)和接口实现层(DAL)的对象,那么我们匹配它进行对象注入就可以了吧。

下面是我动态注册DAL对象的实现代码,如下所示。

        /// <summary>
        ///使用Unity自动加载对应的IDAL接口的实现(DAL层)/// </summary>
        /// <param name="container"></param>
        private static voidRegisterDAL(IUnityContainer container)
{
Dictionary
<string, Type> dictInterface = new Dictionary<string, Type>();
Dictionary
<string, Type> dictDAL = new Dictionary<string, Type>();
Assembly currentAssembly
=Assembly.GetExecutingAssembly();string dalSuffix = ".DAL";string interfaceSuffix = ".IDAL";//对比程序集里面的接口和具体的接口实现类,把它们分别放到不同的字典集合里 foreach (Type objType incurrentAssembly.GetTypes())
{
string defaultNamespace =objType.Namespace;if (objType.IsInterface &&defaultNamespace.EndsWith(interfaceSuffix))
{
if (!dictInterface.ContainsKey(objType.FullName))
{
dictInterface.Add(objType.FullName, objType);
}
}
else if(defaultNamespace.EndsWith(dalSuffix))
{
if (!dictDAL.ContainsKey(objType.FullName))
{
dictDAL.Add(objType.FullName, objType);
}
}
}
//根据注册的接口和接口实现集合,使用IOC容器进行注册 foreach (string key indictInterface.Keys)
{
Type interfaceType
=dictInterface[key];foreach (string dalKey indictDAL.Keys)
{
Type dalType
=dictDAL[dalKey];if (interfaceType.IsAssignableFrom(dalType))//判断DAL是否实现了某接口 {
container.RegisterType(interfaceType, dalType);
}
}
}
}

有了这个利用反射动态注入对象的操作,我们在基类里面的实现就避免了硬编码的不便。

    /// <summary>
    ///实体框架的数据访问层接口的构造工厂。/// </summary>
    public classDALFactory
{
//普通局部变量 private static Hashtable objCache = newHashtable();private static object syncRoot = newObject();private static DALFactory m_Instance = null;/// <summary> ///IOC的容器,可调用来获取对应接口实例。/// </summary> public IUnityContainer Container { get; set; }/// <summary> ///创建或者从缓存中获取对应业务类的实例/// </summary> public staticDALFactory Instance
{
get{if (m_Instance == null)
{
lock(syncRoot)
{
if (m_Instance == null)
{
m_Instance
= newDALFactory();//初始化相关的注册接口 m_Instance.Container = newUnityContainer();//根据约定规则自动注册DAL RegisterDAL(m_Instance.Container);//手工加载//m_Instance.Container.RegisterType<ICityDAL, CityDAL>();//m_Instance.Container.RegisterType<IProvinceDAL, ProvinceDAL>(); }
}
}
returnm_Instance;
}
}

上面整个框架的优化过程,都是围绕着业务逻辑层进行的,最后我们实现了较好的动态对象的依赖注入,并给业务逻辑层对象提供了默认构造函数,让他们可以从IOC容器里面获取对象并创建。

但是我们看到,对于EDMX文件,我们只是把它放入了Entity的模块里面,也没有真正的对它如何处理,如果每次都需要使用这个edmx的文件生成操作,我依旧觉得开发效率比较低下,而且如果对于需要支持多个数据库如何处理呢?不可能在创建一个数据操作上下文吧,它们可以已经抽象化了,本身好像不是和具体数据库相关的,和数据库相关的只是它的配置关系而已啊。

这些问题留给下一篇继续对框架的演化处理吧,谢谢大家耐心的阅读,如果觉得有用,请继续推荐支持下,毕竟为了准备这个系列,我已经花了好多天的时间,从各个方面持续优化整个仓储模式的实体框架,留下一个个版本的Demo来整理博客的。

这个系列文章索引如下:

Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)

Entity Framework 实体框架的形成之旅--利用Unity对象依赖注入优化实体框架(2)

Entity Framework 实体框架的形成之旅--基类接口的统一和异步操作的实现(3)

在本系列的第一篇随笔《
Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)
》中介绍了Entity Framework 实体框架的一些基础知识,以及构建了一个简单的基于泛型的仓储模式的框架;在随笔《
Entity Framework 实体框架的形成之旅--利用Unity对象依赖注入优化实体框架(2)
》则持续优化这个仓储模式的实体框架,主要介绍业务逻辑层的构建,以及利用Unity和反射进行动态的对象注册。本篇主要介绍基类接口的统一和异步操作的实现等方面,逐步把我框架接口命名的方式进行统一,并增加所有必要用到的增删改查、分页、lambda表达式条件处理,以及异步操作等特性,这样能够尽可能的符合基类这个特殊类的定义,实现功能接口的最大化重用和统一。

1、基类接口的统一命名和扩展

在我以前的基于Enterprise Library的框架里面,定义了一个超级的数据访问基类,是特定数据访问类基类的基类,AbstractBaseDAL的数据访问层基类定义了很多通用的接口,具有非常强大的操作功能,如下所示。

这里面的很多接口命名我都经过了一些推敲,或者我基于我或者我客户群体的使用习惯和理解考虑,也是想沿承这些命名规则,扩充我这个基于泛型的仓储模式的实体框架基类接口。

下面是各类不同接口的定义内容。

1)增加操作

        /// <summary>
        ///插入指定对象到数据库中/// </summary>
        /// <param name="t">指定的对象</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        bool Insert(T t);

2)删除操作

        /// <summary>
        ///根据指定对象的ID,从数据库中删除指定对象/// </summary>
        /// <param name="id">对象的ID</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        bool Delete(objectid);/// <summary>
        ///从数据库中删除指定对象/// </summary>
        /// <param name="id">指定对象</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        bool Delete(T t);

3)修改操作

        /// <summary>
        ///更新对象属性到数据库中/// </summary>
        /// <param name="t">指定的对象</param>
        /// <param name="key">主键的值</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        bool Update(T t, object key);

4)主键查询以及条件查询操作

        /// <summary>
        ///查询数据库,返回指定ID的对象/// </summary>
        /// <param name="id">ID主键的值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
        T FindByID(objectid);/// <summary>
        ///根据条件查询数据库,如果存在返回第一个对象/// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>存在则返回指定的第一个对象,否则返回默认值</returns>
        T FindSingle(Expression<Func<T, bool>> match);

5)集合查询(分返回IQueryable和ICollection<T>两种方式)

        /// <summary>
        ///返回可查询的记录源/// </summary>
        /// <returns></returns>
        IQueryable<T>GetQueryable();/// <summary>
        ///根据条件表达式返回可查询的记录源/// </summary>
        /// <param name="match">查询条件</param>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        IQueryable<T> GetQueryable(Expression<Func<T, bool>> match, string sortPropertyName, bool isDescending = true);/// <summary>
        ///根据条件查询数据库,并返回对象集合/// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns></returns>
        ICollection<T> Find(Expression<Func<T, bool>>match);/// <summary>
        ///根据条件查询数据库,并返回对象集合/// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        ICollection<T> Find<TKey>(Expression<Func<T, bool>> match, Expression<Func<T, TKey>> orderByProperty, bool isDescending = true);

6)分页查询操作

        /// <summary>
        ///根据条件查询数据库,并返回对象集合(用于分页数据显示)/// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="info">分页实体</param>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns>指定对象的集合</returns>
        ICollection<T> FindWithPager(Expression<Func<T, bool>>match, PagerInfo info);/// <summary>
        ///根据条件查询数据库,并返回对象集合(用于分页数据显示)/// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="info">分页实体</param>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns>指定对象的集合</returns>
        ICollection<T> FindWithPager<TKey>(Expression<Func<T, bool>> match, PagerInfo info, Expression<Func<T, TKey>> orderByProperty, bool isDescending = true);

这样我们在BaseDAL里面,把这些接口全部实现了,那么所有继承这个基类对象的数据访问对象,就具有这些标准的接口了,也给我们开发实现了整体性的统一。

首先我们来看看这个基类BaseDAL的初始化定义代码。

    /// <summary>
    ///数据访问层基类/// </summary>
    /// <typeparam name="T">实体对象类型</typeparam>
    public abstract class BaseDAL<T> : IBaseDAL<T>  where T : class{#region 变量及构造函数

        /// <summary>
        ///DbContext对象/// </summary>
        protectedDbContext baseContext;/// <summary>
        ///指定类型的实体对象集合/// </summary>
        protected DbSet<T>objectSet;/// <summary>
        ///是否为降序/// </summary>
        public bool IsDescending { get; set; }/// <summary>
        ///排序属性/// </summary>
        public string SortPropertyName { get; set; }/// <summary>
        ///参数化构造函数/// </summary>
        /// <param name="context">DbContext对象</param>
        publicBaseDAL(DbContext context)
{
this.baseContext = context;
this.objectSet = this.baseContext.Set<T>();
this.IsDescending = true;this.SortPropertyName = "ID";
}
#endregion

有了这些DbContext对象以及DbSet<T>对象,具体的接口实现就很容易了,下面我抽几个代表性的函数来介绍实现。

1)增加对象

        /// <summary>
        ///插入指定对象到数据库中/// </summary>
        /// <param name="t">指定的对象</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual boolInsert(T t)
{
ArgumentValidation.CheckForNullReference(t,
"传入的对象t为空");

objectSet.Add(t);
return baseContext.SaveChanges() > 0;
}

2)删除对象

        /// <summary>
        ///根据指定对象的ID,从数据库中删除指定对象/// </summary>
        /// <param name="id">对象的ID</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual bool Delete(objectid)
{
T obj
=objectSet.Find(id);
objectSet.Remove(obj);
return baseContext.SaveChanges() > 0;
}

3)修改对象

        /// <summary>
        ///更新对象属性到数据库中/// </summary>
        /// <param name="t">指定的对象</param>
        /// <param name="key">主键的值</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual bool Update(T t, objectkey)
{
ArgumentValidation.CheckForNullReference(t,
"传入的对象t为空");bool result = false;
T existing
=objectSet.Find(key);if (existing != null)
{
baseContext.Entry(existing).CurrentValues.SetValues(t);
result
= baseContext.SaveChanges() > 0;
}
returnresult;
}

4)根据条件查询

        /// <summary>
        ///根据条件查询数据库,如果存在返回第一个对象/// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>存在则返回指定的第一个对象,否则返回默认值</returns>
        public virtual T FindSingle(Expression<Func<T, bool>>match)
{
returnobjectSet.FirstOrDefault(match);
}
/// <summary> ///根据条件表达式返回可查询的记录源/// </summary> /// <param name="match">查询条件</param> /// <param name="orderByProperty">排序表达式</param> /// <param name="isDescending">如果为true则为降序,否则为升序</param> /// <returns></returns> public virtual IQueryable<T> GetQueryable(Expression<Func<T, bool>> match, string sortPropertyName, bool isDescending = true)
{
IQueryable
<T> query = this.objectSet;if (match != null)
{
query
=query.Where(match);
}
returnquery.OrderBy(sortPropertyName, isDescending);
}

5)分页查询

        /// <summary>
        ///根据条件查询数据库,并返回对象集合(用于分页数据显示)/// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="info">分页实体</param>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns>指定对象的集合</returns>
        public virtual ICollection<T> FindWithPager(Expression<Func<T, bool>>match, PagerInfo info)
{
int pageindex = (info.CurrenetPageIndex < 1) ? 1: info.CurrenetPageIndex;int pageSize = (info.PageSize <= 0) ? 20: info.PageSize;int excludedRows = (pageindex - 1) *pageSize;

IQueryable
<T> query =GetQueryable().Where(match);
info.RecordCount
=query.Count();returnquery.Skip(excludedRows).Take(pageSize).ToList();
}

更多的代码就不一一贴出,反正我们全部实现自己所需的各种操作就可以了,这里要提的是,我们尽可能利用Lambda表达式进行条件处理,包括查询、删除等条件处理。

对上面的这些常规接口,我们调用代码处理的例子如下所示。

       private void btnProvince_Click(objectsender, EventArgs e)
{
DateTime dt
=DateTime.Now;var list = BLLFactory<ProvinceBLL>.Instance.GetAll(s=>s.ProvinceName);this.dataGridView1.DataSource =list;

Console.WriteLine(
"花费时间:{0}", DateTime.Now.Subtract(dt).TotalMilliseconds);
}
private void btnCity_Click(objectsender, EventArgs e)
{
DateTime dt
=DateTime.Now;

CityBLL bll
= newCityBLL();var result =bll.GetAll();this.dataGridView1.DataSource =result;

Console.WriteLine(
"花费时间:{0}", DateTime.Now.Subtract(dt).TotalMilliseconds);
}

如果需要考虑分页,以上接口已经定义了分页处理的接口和实现了,我们在业务对象里面直接调用接口就可以了,具体代码如下所示。

                CityBLL bll = newCityBLL();
PagerInfo info
= newPagerInfo();
info.PageSize
= 30;
info.CurrenetPageIndex
=1;

ICollection
<City>list;if (i++ % 2 == 0)
{
sortType
= "自定义排序";//使用自定义排序 list = bll.FindWithPager(s => s.CityName.Contains("南"), info, o => o.ID, true);
}
else{
sortType
= "默认字段排序";//使用默认字段排序 list = bll.FindWithPager(s => s.CityName.Contains("南"), info);
}
this.dataGridView1.DataSource = list;

2、异步操作的定义和调用

在EF里面实现异步(并行)非常容易,在.NET 4.5里由于async/await关键字的出现,使得实现异步变得更加容易。

使用await关键字后,.NET会自动把返回结果包装在一个Task类型的对象中。使用await表达式时,控制会返回到调用此方法的线程中;在await等待的方法执行完毕后,控制会自动返回到下面的语句中。发生异常时,异常会在await表达式中抛出。

我们基本上所有的增删改查、分页等接口,都可以使用异步操作来定义这些新接口,代码如下所示。

1)增加对象异步实现

异步定义的接口如下所示

        /// <summary>
        ///插入指定对象到数据库中(异步)/// </summary>
        /// <param name="t">指定的对象</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        Task<bool> InsertAsync(T t);

接口的实现如下所示

        /// <summary>
        ///插入指定对象到数据库中(异步)/// </summary>
        /// <param name="t">指定的对象</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual async Task<bool>InsertAsync(T t)
{
ArgumentValidation.CheckForNullReference(t,
"传入的对象t为空");

objectSet.Add(t);
return await baseContext.SaveChangesAsync() > 0;
}

和普通的接口定义不一样的地方,我们看到异步的接口都是以Async结尾,并且返回值使用Task<T>进行包装,另外实现里面,增加了async的定义,方法体里面增加 await 的关键字,这些就构成了异步操作的接口定义和接口实现了。

2)条件删除异步实现

我们再来看一个复杂一点的条件删除操作,代码如下所示。

定义接口

        /// <summary>
        ///根据指定条件,从数据库中删除指定对象(异步)/// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        Task<bool> DeleteByConditionAsync(Expression<Func<T, bool>> match);

接口实现

        /// <summary>
        ///根据指定条件,从数据库中删除指定对象(异步)/// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual async Task<bool> DeleteByConditionAsync(Expression<Func<T, bool>>match)
{
objectSet.Where
<T>(match).ToList<T>().ForEach(d => baseContext.Entry<T>(d).State =EntityState.Deleted);return await baseContext.SaveChangesAsync() > 0;
}

我们定义的这些异步接口,基本上都是类似的操作,但是我们应该如何调用异步的处理呢?

好像有两个调用代码方式。

1)使用async和await关键字处理

        private asyncvoid btnCity_Click(objectsender, EventArgs e)
{
DateTime dt
=DateTime.Now;

CityBLL bll
= newCityBLL();var result = awaitbll.GetAllAsync();this.dataGridView1.DataSource =result;

Console.WriteLine(
"花费时间:{0}", DateTime.Now.Subtract(dt).TotalMilliseconds);
}

2)使用 await Task.Run的处理方式

        private async void btnCity_Click(objectsender, EventArgs e)
{
DateTime dt
=DateTime.Now;

CityBLL bll
= newCityBLL();var result = await Task.Run(() =>{var list =bll.GetAllAsync();returnlist;
});
this.dataGridView1.DataSource =result;

Console.WriteLine(
"花费时间:{0}", DateTime.Now.Subtract(dt).TotalMilliseconds);
}

两种方式都能正常运行,并得到所要的效果。

本篇主要介绍了基类接口的统一封装、并增加所有必要的增删改查、分页查询、Lambda条件等处理方式,还有就是增加了相关的异步操作接口和实现,随着我们对通用功能的进一步要求,可以为基类增加更多的接口函数。

这个系列文章索引如下:

Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)

Entity Framework 实体框架的形成之旅--利用Unity对象依赖注入优化实体框架(2)

Entity Framework 实体框架的形成之旅--基类接口的统一和异步操作的实现(3)