wenmo8 发布的文章

在我前面介绍的WebAPI文章里面,介绍了WebAPI的架构设计方面的内容,其中提出了现在流行的WebAPI优先的路线,这种也是我们开发多应用(APP、微信、微网站、商城、以及Winform等方面的整合)的时候值得考虑的线路之一。一般情况下,由于HTTP协议的安全性,传递的参数容易被拦截,从而可能导致潜在的危险,所以一般WebAPI接口层都采用了HTTPS协议的,也就是采用SSL层来对数据进行安全性的加密的。

1、HTTPS基础知识介绍

1)
HTTPS

HTTPS(全称:
Hypertext Transfer Protocol over Secure Socket Layer),是以安全为目标的
HTTP通道,简单讲是
HTTP的安全版。即
HTTP下加入
SSL层,
HTTPS的安全基础是
SSL,因此加密的详细内容就需要
SSL。 它是一个
URI scheme(抽象标识符体系),句法类同
http:体系。用于安全的
HTTP数据传输。
https:URL表明它使用了
HTTPS,但
HTTPS存在不同于
HTTP的默认端口及一个加密
/身份验证层(在
HTTP与
TCP之间)。这个系统的最初研发由网景公司进行,提供了身份验证与加密通讯方法,现在它被广泛用于万维网上安全敏感的通讯,例如交易支付方面。

2)HTTPS和
HTTP的区别

一、
https协议需要到
ca申请证书,一般免费证书很少,需要交费。

二、
http是超文本传输协议,信息是明文传输,
https 则是具有安全性的
ssl加密传输协议。

三、
http和
https使用的是完全不同的连接方式,用的端口也不一样,前者是
80,后者是
443。

四、
http的连接很简单,是无状态的;
HTTPS协议是由
SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比
http协议安全。

3)https的实现原理

有两种基本的加解密算法类型

1)对称加密:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密算法有
DES、
AES等;

2)非对称加密:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥),加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的非对称加密算法有
RSA、
DSA等。

https的通信过程

4) https通信的优点

1)客户端产生的密钥只有客户端和服务器端能得到;

2)加密的数据只有客户端和服务器端才能得到明文;

3)客户端到服务端的通信是安全的。

2、SSL
基础知识介绍

1)SSL安全套接层协议(Secure Socket Layer)

为Netscape所研发,用以保障在Internet上数据传输之安全,利用数据加密(Encryption)技术,可确保数据在网络上之传输过程中不会被截取及窃听。目前一般通用之规格为40 bit之安全标准,美国则已推出128 bit之更高安全标准,但限制出境。只要3.0版本以上之IE或Netscape浏览器即可支持SSL。

当前版本为3.0。它已被广泛地用于Web浏览器与服务器之间的身份认证和加密数据传输。

SSL协议位于TCP/IP协议与各种应用层协议之间,是一种国际标准的加密及身份认证通信协议,为TCP提供一个可靠的端到端的安全服务,为两个通讯个体之间提供保密性和完整性(身份鉴别)。SSL协议可分为两层:SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。

2)SSL协议特点

1)SSL协议可用于保护正常运行于TCP之上的任何应用协议,如HTTP、FTP、SMTP或Telnet的通信,最常见的是用SSL来保护HTTP的通信。

2)SSL协议的优点在于它是与应用层协议无关的。高层的应用协议(如HTTP、FTP、Telnet等)能透明地建立于SSL协议之上。

3)SSL协议在应用层协议之前就已经完成加密算法、通信密钥的协商以及服务器的认证工作。在此之后应用层协议所传送的数据都会被加密,从而保证通信的安全性。

4)SSL协议使用通信双方的客户证书以及CA根证书,允许客户/服务器应用以一种不能被偷听的方式通信,在通信双方间建立起了一条安全的、可信任的通信通道。

5)该协议使用密钥对传送数据加密,许多网站都是通过这种协议从客户端接收信用卡编号等保密信息。常用于交易过程中。

3)SSL功能

1)客户对服务器的身份认证:

SSL服务器允许客户的浏览器使用标准的公钥加密技术和一些可靠的认证中心(CA)的证书,来确认服务器的合法性。

2)服务器对客户的身份认证:

也可通过公钥技术和证书进行认证,也可通过用户名,password来认证。

3)建立服务器与客户之间安全的数据通道:

SSL要求客户与服务器之间的所有发送的数据都被发送端加密、接收端解密,同时还检查数据的完整性。

3、支持SSL的CA证书购买

上面介绍一些基础知识,我们可以简单的概括一下,就是引入了HTTPS,可以很好解决接口参数的安全性问题。这些在很多大厂商的接口里面,都是使用HTTPS协议的,如腾讯微信、支付宝等接口,特别对于支付内容,使用HTTPS是必须的。

对于支持HTTPS,核心的问题就是解决证书的问题,必须是由第三方权威机构颁发的证书,这样才能达到不被伪造的可能性,一般我们采用的是CA证书,这些证书是需要通过付费购买的(天下没有免费的午餐),证书的购买的供应商网站有很多,可以选择自己合适的进行购买。

1)国外的GoDaddy平台购买证书过程

这个参考博客园站长dudu的介绍内容,这个goDaddy是支持支付宝用美元购买的,使用平台是英文,可以购买域名、证书等服务,是一个影响力较大的厂商。

1) 打开godaddy.com网站,通过菜单进入Products -> SSL&Security -> SSL Certificates,选择Protect All Subdomains("Wildcard"), 在Pick your plan type中,选择Standard(Validates domain ownership),然后完成购买。

2)进入My Account -> SSL CERTIFICATES,创建证书(Certificate),创建时将之前得到的CSR内容复制到“CSR文本框”中。

3)接入来进入GoDaddy的审批流程,在审批过程中需要验证域名的所有者(dns填加记录或上传html文件至网站目录),验证成功后很快就会生成CA证书。

4)下载CA证书文件至生成CSR的服务器上。

普通证书费用是63美元左右,可以通过支付宝进行交易。

2)国内沃通平台购买证书过程

这个是纯中文版本的平台,比较方便使用,费用也比上面的贵一些,分的级别好像也多一些,相对上面那个国外的GoDaddy的三个种类证书,这个产品线分了6个类型。最低的也要接近500块,相对GoDaddy来说,费用要多一些了。由于是初步使用,也就购买了这个使用了。

申请购买也很简单,一步步按提示操作填写内容即可,大致分为这几步:

1)输入购买的证书类型,以及年限等资料;

2)输入域名的信息,以及需要域名对应的邮件进行验证;

3)提交购买订单后,使用在线支付或者使用公司账号汇款到指定账号;

4)客服验证后,技术人员提供证书生成操作,我们在列表里面尽快使用证书密码下载证书。

最后成功后,在订单信息里面,有这样的列表,提供了两个SHA1和SHA2两种加密协议的证书,官方建议使用SHA2。

4、
Web API应用支持HTTPS

有了证书,我们在云服务器上(如阿里云)的IIS里面的证书模块里面,可以导入已有的证书。

导入证书成功后,我们可以看到列表里面有具体的证书了,同时双击可以查看证书详细信息。

创建一个网站,并指定使用这个证书,端口采用443即可。

一般情况下,我们创建这个步骤后,你可以使用类似这样地址:https://www.iqidi.com/测试下具体的网站是否已经开通了,如果可以正常访问,那说明你的443端口已经启动侦听了,如果没有,可能是没有启动,或者是防火墙禁止了,一般情况下,我们还是需要在防火墙里面增加443端口的入站规则,设置为允许,否则可能被屏蔽了。

如果都设置了,可以通过DOS命令行来检查端口是否已经开始侦听了。

查询443端口监听

netstat -ano | find "443"

如果还是有问题,可以请求CA证书的供应商或者云供应商的技术人员帮你看看,一般都是会配合你解决的,我的开始也是折腾了他们一会,最后也莫名其妙的可以了。

最后成功后,在Chrome浏览器里面打开地址就有绿色的标志了。

由于目前我的API平台还在搭建完善中,因此这个域名展示还是没有东西可以展示的。

最后的构架会如前面Web API框架图所示,做到一个统一的整合方案里面。

由于Web API层作为一个公共的接口层,我们就很好保证了各个界面应用层的数据一致性,如果考虑到响应式的集成处理,我们甚至可以把微信应用、APP应用、Web应用做层一套Web程序,即使为了利用各自应用的特殊性,也可以把这些应用做的很相似,这样就给用户提供了一个统一的界面表示方式,极大提高客户使用的界面体验效果,用户几乎不需要额外的界面学习,就可以熟悉整个应用体系的各个模块使用。

从上面的架构分析来看,我们的Web API作为核心层,可以在上面开发我们各种企业业务应用,

在目前比较热门的会员管理、客户管理等方面,结合微信的应用催化剂,就可以做的更加符合移动的潮流,从而实现我们“互联网+”的应用落地。

在前阵子,我对实体框架进行了一定的研究,然后把整个学习的过程开了一个系列,以逐步深入的方式解读实体框架的相关技术,期间每每碰到一些新的问题需要潜入研究。本文继续前面的主题介绍,着重从整体性的来总结一下实体框架的一些方面,希望针对这些实际问题,和大家进行学习交流。

我的整个实体框架的学习和研究,是以我的Winform框架顺利升级到这个实体框架基础上为一个阶段终结,这个阶段事情很多,从开始客运联网售票的WebAPI平台的开发,到微软实体框架的深入研究,以及《基于Metronic的Bootstrap开发框架经验总结》的主题学习和分享等等方面,都混到一起来了,多个主题之间穿插着写一些随笔,也是希望把自己的学习过程进行记录总结,不用等到最后全部忘记了。

1、实体框架主键的类型约束问题

在我们搭建整个实体框架的过程中,我们一般都是抽象封装处理很多基础的增删改查、分页等常见的数据处理功能,如下所示。

        /// <summary>
        ///更新对象属性到数据库中/// </summary>
        /// <param name="t">指定的对象</param>
        /// <param name="key">主键的值</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        bool Update(T t, objectkey);/// <summary>
        ///更新对象属性到数据库中(异步)/// </summary>
        /// <param name="t">指定的对象</param>
        /// <param name="key">主键的值</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        Task<bool> UpdateAsync(T t, objectkey);/// <summary>
        ///根据指定对象的ID,从数据库中删除指定对象/// </summary>
        /// <param name="id">对象的ID</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        bool Delete(objectid);/// <summary>
        ///根据指定对象的ID,从数据库中删除指定对象(异步)/// </summary>
        /// <param name="id">对象的ID</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        Task<bool> DeleteAsync(objectid);/// <summary>
        ///查询数据库,返回指定ID的对象/// </summary>
        /// <param name="id">ID主键的值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
        T FindByID(objectid);/// <summary>
        ///查询数据库,返回指定ID的对象(异步)/// </summary>
        /// <param name="id">ID主键的值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
        Task<T> FindByIDAsync(object id);

上面的外键统一定义为object类型,因为我们为了主键类型通用的考虑。

在实际上表的外键类型可能是很多种的,如可能是常见的字符类型,也可能是int类型,也可能是long类型等等。如果我们更新、查找、删除整形类型的记录的时候,那么可能机会出现错误:

The argument types 'Edm.Int32' and 'Edm.String' are incompatible for this operation.

这些错误就是主键类型不匹配导致的,我们操作这些接口的时候,一定要传入对应类型给它们,才能正常的处理。

本来想尝试在内部进行转换处理为正确的类型的,不过没有找到很好的解决方案来识别和处理,因此最好的解决方法,就是我们调用这些有object类型主键的接口时,传入正确的类型即可。

RoleInfo info = CallerFactory<IRoleService>.Instance.FindByID(currentID.ToInt32());if (info != null)
{
info
=SetRoleInfo(info);
CallerFactory
<IRoleService>.Instance.Update(info, info.ID);

RefreshTreeView();
}

又或者是下面的代码:

        /// <summary>
        ///分页控件删除操作/// </summary>
        private void winGridViewPager1_OnDeleteSelected(objectsender, EventArgs e)
{
if (MessageDxUtil.ShowYesNoAndTips("您确定删除选定的记录么?") ==DialogResult.No)
{
return;
}
int[] rowSelected = this.winGridViewPager1.GridView1.GetSelectedRows();foreach (int iRow inrowSelected)
{
string ID = this.winGridViewPager1.GridView1.GetRowCellDisplayText(iRow, "ID");
CallerFactory
<IDistrictService>.Instance.Delete(ID.ToInt64());
}

BindData();
}

2、递归函数的处理

在很多时候,我们都会用到递归函数的处理,这样能够使得我们把整个列表的内容都合理的提取出来,是我们开发常见的知识点之一。

不过一般在处理LINQ的时候,它的递归函数的处理和我们普通的做法有一些差异。

例如我们如果要获取一个树形机构列表,如果我们指定了一个开始的机构节点ID,我们需要递归获取下面的所有层次的集合的时候,常规的做法如下所示。

        /// <summary>
        ///根据指定机构节点ID,获取其下面所有机构列表/// </summary>
        /// <param name="parentId">指定机构节点ID</param>
        /// <returns></returns>
        public List<OUInfo> GetAllOUsByParent(intparentId)
{
List
<OUInfo> list = new List<OUInfo>();string sql = string.Format("Select * From {0} Where Deleted <> 1 Order By PID, Name", tableName);

DataTable dt
=SqlTable(sql);string sort = string.Format("{0} {1}", GetSafeFileName(sortField), isDescending ? "DESC" : "ASC");
DataRow[] dataRows
= dt.Select(string.Format("PID = {0}", parentId), sort);for (int i = 0; i < dataRows.Length; i++)
{
string id = dataRows[i]["ID"].ToString();
list.AddRange(GetOU(id, dt));
}
returnlist;
}
private List<OUInfo> GetOU(stringid, DataTable dt)
{
List
<OUInfo> list = new List<OUInfo>();

OUInfo ouInfo
= this.FindByID(id);
list.Add(ouInfo);
string sort = string.Format("{0} {1}", GetSafeFileName(sortField), isDescending ? "DESC" : "ASC");
DataRow[] dChildRows
= dt.Select(string.Format("PID={0}", id), sort);for (int i = 0; i < dChildRows.Length; i++)
{
string childId = dChildRows[i]["ID"].ToString();
List
<OUInfo> childList =GetOU(childId, dt);
list.AddRange(childList);
}
returnlist;
}

这里面的大概思路就是把符合条件的集合全部弄到DataTable集合里面,然后再在里面进行检索,也就是递归获取里面的内容。

上面是常规的做法,可以看出代码量还是太多了,如果使用LINQ,就不需要这样了,而且也不能这样处理。

使用实体框架后,主要就是利用LINQ进行一些集合的操作,这些LINQ的操作虽然有点难度,不过学习清楚了,处理起来也是比较方便的。

在数据访问层,处理上面同等的功能,LINQ操作代码如下所示。

        /// <summary>
        ///根据指定机构节点ID,获取其下面所有机构列表/// </summary>
        /// <param name="parentId">指定机构节点ID</param>
        /// <returns></returns>
        public IList<Ou> GetAllOUsByParent(intparentId)
{
//递归获取指定PID及下面所有所有的OU var query = this.GetQueryable().Where(s => s.PID == parentId).Where(s => !s.Deleted.HasValue || s.Deleted == 0).OrderBy(s => s.PID).OrderBy(s =>s.Name);return query.ToList().Concat(query.ToList().SelectMany(t =>GetAllOUsByParent(t.ID))).ToList();
}

基本上,可以看到就是两行代码了,是不是很神奇,它们实现的功能完全一致。

不过,也不是所有的LINQ递归函数都可以做的非常简化,有些递归函数,我们还是需要使用常规的思路进行处理。

        /// <summary>
        ///获取树形结构的机构列表/// </summary>
        public IList<OuNodeInfo>GetTree()
{
IList
<OuNodeInfo> returnList = new List<OuNodeInfo>();
IList
<Ou> list = this.GetQueryable().Where(p => p.PID == -1).OrderBy(s => s.PID).OrderBy(s =>s.Name).ToList();if (list != null)
{
foreach (Ou info in list.Where(s => s.PID == -1))
{
OuNodeInfo nodeInfo
=GetNode(info);
returnList.Add(nodeInfo);
}
}
returnreturnList;
}

不过相对来说,LINQ已经给我们带来的非常大的便利了。

3、日期字段类型转换的错误处理

我们在做一些表的时候,一般情况下都会有日期类型存在,如我们的生日,创建、编辑日期等,一般我们数据库可能用的是datetime类型,如果这个日期的类型内容在下面这个区间的话:

"0001-01-01 到 9999-12-31"(公元元年 1 月 1 日到公元 9999 年 12 月 31 日)

我们可能就会得到下面的错误:

从 datetime2 数据类型到 datetime 数据类型的转换产生一个超出范围的值

一般之所以会报错数据类型转换产生一个超出范围的值,都是因为数据的大小和范围超出要转换的目标的原因。我们先看datetime2和datetime这两个数据类型的具体区别在哪里。

官方MSDN对于datetime2的说明
:定义结合了 24 小时制时间的日期。 可将 datetime2 视作现有 datetime 类型的扩展,其数据范围更大,默认的小数精度更高,并具有可选的用户定义的精度。

这里值的注意的是datetime2的日期范围是"0001-01-01 到 9999-12-31"(公元元年 1 月 1 日到公元 9999 年 12 月 31 日)。而datetime的日期范围是:”1753 年 1 月 1 日到 9999 年 12 月 31 日“。这里的日期范围就是造成“从 datetime2 数据类型到 datetime 数据类型的转换产生一个超出范围的值”这个错误的原因!!!

在c#中,如果实体类的属性没有赋值,一般都会取默认值,比如int类型的默认值为0,string类型默认值为null, 那DateTime的默认值呢?由于DateTime的默认值为"0001-01-01",所以entity framework在进行数据库操作的时候,在传入数据的时会自动将原本是datetime类型的数据字段转换为datetime2类型(因为0001-01-01这个时间超出了数据库中datetime的最小日期范围),然后在进行数据库操作。问题来了,虽然EF已经把要保存的数据自动转为了datetime2类型,但是数据库中表的字段还是datetime类型!所以将datetime2类型的数据添加到数据库中datetime类型的字段里去,就会报错并提示转换超出范围。

解决方法如下所示:

这个问题的解决方法:

  1. C#代码中 DateTime类型的字段在作为参数传入到数据库前记得赋值,并且的日期要大于1753年1月1日。
  2. C#代码中 将原本是DateTime类型的字段修改为DateTime?类型,由于可空类型的默认值都是为null,所以传入数据库就可以不用赋值,数据库中的datetime类型也是支持null值的。
  3. 修改数据库中表的字段类型,将datetime类型修改为datetime2类型

例如,我在实体框架里面,对用户表的日期类型字段进行初始化,这样就能保证我存储数据的时候,默认值是不会有问题的。

    /// <summary>
    ///系统用户信息,数据实体对象/// </summary>
    public classUser
{
/// <summary> ///默认构造函数(需要初始化属性的在此处理)/// </summary> publicUser()
{
this.ID= 0;//从 datetime2 数据类型到 datetime 数据类型的转换产生一个超出范围的值//避免这个问题,可以初始化日期字段 DateTime defaultDate = Convert.ToDateTime("1900-1-1");this.Birthday =defaultDate;this.LastLoginTime =defaultDate;this.LastPasswordTime =defaultDate;this.CurrentLoginTime =defaultDate;this.EditTime =DateTime.Now;this.CreateTime =DateTime.Now;
}

有时候,虽然这样设置了,但是在界面可能给这个日期字段设置了不合理的值,也可能产生问题。那么我们对于这种情况,判断一下,如果小于某个值,我们给它一个默认值。

4、实体框架的界面处理

在界面调整这块,我们还是尽可能保持着的Enterprise Library的Winform界面样式,也就是混合型或者普通Winform的界面效果。不过这里我们是以混合式框架进行整合测试,因此实体框架的各个方面的调用处理基本上保持一致。

不过由于实体框架里面,实体类避免耦合的原因,我们引入了DTO的概念,并使用了AutoMapper组件进行了Entity与DTO的相互映射,具体介绍可以参考《
Entity Framework 实体框架的形成之旅--数据传输模型DTO和实体模型Entity的分离与联合

》。

因此我们在界面操作的都是DTO对象类型了,我们在定义的时候,为了避免更多的改动,依旧使用***Info这样的类名称作为DTO对象的名称,***代表表名对象。

在混合式框架的界面表现层,它们的数据对象的处理基本上保持和原来的代码差不多。

        /// <summary>
        ///新增状态下的数据保存/// </summary>
        /// <returns></returns>
        public override boolSaveAddNew()
{
UserInfoinfo
= tempInfo;//必须使用存在的局部变量,因为部分信息可能被附件使用 SetInfo(info);
info.Creator
=Portal.gc.UserInfo.FullName;
info.Creator_ID
=Portal.gc.UserInfo.ID.ToString();
info.CreateTime
=DateTime.Now;try{#region 新增数据 bool succeed = CallerFactory<IUserService>.Instance.Insert(info);if(succeed)
{
//可添加其他关联操作 return true;
}
#endregion}catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
return false;
}

但我们需要在WCF服务层说明他们之间的映射关系,方便进行内部的转换处理。

在实体框架界面层的查询中,我们也不在使用部分SQL的条件做法了,采用更加安全的基于DTO的LINQ表达式进行封装,最后传递给后台的也就是一个LINQ对象(非传统方式的实体LINQ,那样在分布式处理中会出错)。

如查询条件的封装处理如下所示:

        /// <summary>
        ///根据查询条件构造查询语句/// </summary> 
        privateExpressionNode GetConditionSql()
{
Expression
<Func<UserInfo, bool>> expression = p => true;if (!string.IsNullOrEmpty(this.txtHandNo.Text))
{
expression
= expression.And(x => x.HandNo.Equals(this.txtHandNo.Text));
}
if (!string.IsNullOrEmpty(this.txtName.Text))
{
expression
= expression.And(x => x.Name.Contains(this.txtName.Text));
}
.........................................
//如果是公司管理员,增加公司标识 if(Portal.gc.UserInRole(RoleInfo.CompanyAdminName))
{
expression
= expression.And(x => x.Company_ID ==Portal.gc.UserInfo.Company_ID);
}
//如果是单击节点得到的条件,则使用树列表的,否则使用查询条件的 if (treeCondition != null)
{
expression
=treeCondition;
}
//如非选定,只显示正常用户 if (!this.chkIncludeDelete.Checked)
{
expression
= expression.And(x => x.Deleted == 0);
}
returnexpression.ToExpressionNode();
}

而分页查询的处理,依旧和原来的风格差不多,只不过这里的Where条件为ExpressionNode 对象了,如代码所示、

ExpressionNode where =GetConditionSql();
PagerInfo PagerInfo
= this.winGridViewPager1.PagerInfo;
IList
<UserInfo> list = CallerFactory<IUserService>.Instance.FindWithPager(where, ref PagerInfo);this.winGridViewPager1.DataSource = new WHC.Pager.WinControl.SortableBindingList<UserInfo>(list);this.winGridViewPager1.PrintTitle = "系统用户信息报表";

最后我们来看看整个实体框架的结构和界面的效果介绍。

界面效果如下所示:

代码结构如下所示:

架构设计的效果图如下所示:

在《
Web API应用架构设计分析(1)
》和《
Web API应用架构设计分析(2)
》中对WebAPI的架构进行了一定的剖析,在当今移动优先的口号下,传统平台都纷纷开发了属于自己的Web API平台,方便各种终端系统的接入,很多企业的需求都是以Web API优先的理念来设计整个企业应用体系的。Web API作为整个纽带的核心,在整个核心层需要考虑到统一性、稳定性、以及安全性等方面因素。本文主要介绍,Web API应用架构,在Winform整合中的角色,以及如何实现在Winform混合架构里面的整合案例。

1、Web API介绍回顾

Web API 是一种应用接口框架,它能够构建HTTP服务以支撑更广泛的客户端(包括浏览器,手机和平板电脑等移动设备)的框架,
ASP.NET Web API 是一种用于在 .NET Framework 上构建 RESTful 应用程序的理想平台。在目前发达的应用场景下,我们往往需要接入Winform客户端、APP程序、网站程序、以及目前热火朝天的微信应用等,这些数据应该可以由同一个服务提供,这个就是我们所需要构建的Web API平台。由于Web API层作为一个公共的接口层,我们就很好保证了各个界面应用层的数据一致性。

通过上面的了解,我们可以知道,所有外部的应用,其实都可以基于一个相同的Web API核心开展的,如下图所示。

在当前大平台,大应用的背景下,可以基于一个整体的平台,构建很多应用生态链,这样就可以把Web API作为核心层,可以在上面开发我们各种企业业务应用了。

2、Web API在Winform框架中的整合

在Winform界面里面,我们除了可以利用直接访问数据库方式,以及采用访问分布式WCF服务的方式接入,还可以使得它能够访问Web API的数据服务,从而构建成一个适应性更加广泛、功能更加强大的混合式开发框架模式;对于Web API,由于它提供的是一种无状态的接口访问,而且往往Web API一般为了多种客户端接入的需要,可能需要发布在公网上进行访问,因此我们需要更加注重Web API接口层的安全性。

除了直连数据库访问的传统模式,WCF分布式访问的WCF服务访问模式,还可以接入API分布式访问的Web API接口模式,他们的关系构成了一个完整的Winform应用体系,如下图所示。

混合式框架的实现细节,就是通过一个类似开关模式的配置模块,确定是采用直接访问数据库方式,还是访问WCF服务的方式,它们两者是统一到一个Facade接口门面层上,如果考虑到Web API层,基于混合式的架构,也就是在这个Facade接口门面层上增加多一个Web API的接口的封装成即可。具体整个框架的架构图如下所示。

3、Web API访问的安全性考虑

由于Web API是基于互联网的应用,因此安全性要远比在本地访问数据库的要严格的多,基于通用的做法,一般采用几道门槛来处理这些问题,一个是基于CA证书的HTTPS进行数据传输,防止数据被窃听,具体可以参考《
Web API应用支持HTTPS的经验总结
》;二是采用参数加密签名方式传递,对传递的参数,增加一个加密签名,在服务器端验证签名内容,防止被篡改;三是对一般的接口访问,都需要使用用户身份的token进行校验,只要检查通过才允许访问数据。

Web API接口的访问方式,大概可以分为几类:

1)一个是使用用户令牌,通过Web API接口进行数据访问。这种方式,可以有效识别用户的身份,为用户接口返回用户相关的数据,如包括用户信息维护、密码修改、或者用户联系人等与用户身份相关的数据。

2)一种是使用安全签名进行数据提交。这种方式提交的数据,URL连接的签名参数是经过安全一定规则的加密的,服务器收到数据后也经过同样规则的安全加密,确认数据没有被中途篡改后,再进行数据修改处理。因此我们可以为不同接入方式,如Web/APP/Winfrom等不同接入方式指定不同的加密秘钥,但是秘钥是双方约定的,并不在网络连接上传输,连接传输的一般是这个接入的AppID,服务器通过这个AppID来进行签名参数的加密对比,这种方式,类似微信后台的回调处理机制,它们就是经过这样的处理。

3)一种方式是提供公开的接口调用,不需要传入用户令牌、或者对参数进行加密签名的,这种接口一般较少,只是提供一些很常规的数据显示而已。

基于上面的考虑,我们一般需要设计Web API对象的接口的时候,需要考虑安全性的原因,也就是需要增加更多的一些字段信息了。

如可以在增删改这些接口,除了传入token信息外(标识具体用户),也还是需要传入签名信息,如下接口所示。

        /// <summary>
        ///插入指定对象到数据库中/// </summary>
        /// <param name="info">指定的对象</param>
        /// <returns>执行操作是否成功。</returns>
        public virtual CommonResult Insert(T info, string token, string signature, string timestamp, string nonce, string appid)

上面接口,除了info对象为对象创建的参数外,其他几个参数,都是为了安全性的考虑而加入的。

在接口里面,我们就需要对用户的权限和签名信息进行校验,然后在进行下一步的数据处理,如果校验权限和参数完整性不通过,则会被拦截,不执行数据库的处理了。

            //如果用户token检查不通过,则抛出MyApiException异常。//检查用户是否有权限,否则抛出MyDenyAccessException异常
            base.CheckAuthorized(AuthorizeKey.InsertKey, token, signature, timestamp, nonce, appid);

除了这些对数据修改的特殊性接口,有时候我们还需要查找等类似的,不对数据产生变化的接口,只需要传入令牌即可,如下接口所示。

        /// <summary>
        ///查询数据库,检查是否存在指定ID的对象/// </summary>
        /// <param name="id">对象的ID值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
[HttpGet]public virtual T FindByID(string id, stringtoken)
{
//如果用户token检查不通过,则抛出MyApiException异常。//检查用户是否有权限,否则抛出MyDenyAccessException异常 base.CheckAuthorized(AuthorizeKey.ViewKey, token);

T info
=baseBLL.FindByID(id);returninfo;
}

我们可以看到,上面还是会对token进行校验,不过少了很多签名所需的日期标识、随机数,完整性校验签名,应用ID等参数。

我们会根据用户的token进行解析,如果是正常的token并可以通过解析,那么获取对应用户的权限,判断是否可以进行下一步处理即可。

如果顺利通过,那么访问数据库,把所需的数据返回给调用者即可。

上面提到了用户令牌,用户令牌是一个类似实际生活的通行证,是通过用户名、密码等信息获取到的一个安全令牌,可以在多个接口进行传递的字符串,较少密码参数的传输,提高安全性。

这个用户令牌,一般由单独的接口产生,我们一般放到AuthController里面,这个控制器负责用户令牌相关的处理调用。

        /// <summary>
        ///注册用户获取访问令牌接口/// </summary>
        /// <param name="username">用户登录名称</param>
        /// <param name="password">用户密码</param>
        /// <param name="signature">加密签名字符串</param>
        /// <param name="timestamp">时间戳</param>
        /// <param name="nonce">随机数</param>
        /// <param name="appid">应用接入ID</param>
        /// <returns></returns>
        TokenResult GetAccessToken(string username, stringpassword,string signature, string timestamp, string nonce, string appid);

如下代码是具体业务模块里面,说明如何获取用于操作各种接口的token令牌的,当然,实际环境下,一般都会使用HTTPS协议获取数据了,演示代码如下所示。

                string appid = "myapi_123456";string appsecret = "mySecret_2856FB9DBE31";//使用API方式,需要在缓存里面设置特殊的信息
                var url = "http://localhost:9001/api/Auth/GetAccessToken" + GetSignatureUrl(appid, appsecret) + "&username=admin&password=";
TokenResult result
= JsonHelper<TokenResult>.ConvertJson(url);if(result == null)
{
MessageDxUtil.ShowError(
"获取授权信息出错,请检查地址是否正确!");
}

由于Web API的调用,都是一种无状态方式的调用方式,我们通过token来传递我们的用户信息,这样我们只需要验证Token就可以了。JWT的令牌生成逻辑如下所示

令牌生成后,我们需要在Web API调用处理前,对令牌进行校验,确保令牌是正确有效的。

除了令牌的规则,还有一个是加密签名的处理,加密签名需要客户端和服务器端约定相同的秘钥,一般由Web API统一分配,然后传输的时候,客户端使用应用ID即可。

加密签名在服务端(Web API端)的验证流程参考微信的接口的处理方式,处理逻辑如下所示。

1)检查timestamp 与系统时间是否相差在合理时间内,如10分钟。
2)将appSecret、timestamp、nonce三个参数进行字典序排序
3)将三个参数字符串拼接成一个字符串进行SHA1加密
4)加密后的字符串可与signature对比,若匹配则标识该次请求来源于某应用端,请求是合法的。

4、Web API基类设计分析

上面介绍了一些Web API控制器的职能,一般情况下,我们设计一个架构,还需要考虑到基类对象之间的重用关系,尽可能把接口抽象到基类层面上去,减少子类的开发代码量,降低维护成本。

基于上面的目的,参考了我的Web开发框架对于MVC控制器的设计思路

重新整理了Web API的控制器设计对象继承关系,如下所示:

我们关键的核心就是设计好
BusinessController<B, T>
这个基类,里面设计了大量的通用接口,包括常规的增删改查、分页等处理接口,那么子类继承过来就可以直接拥有这些接口了,多方便啊。

5)Web API客户端(混合式Winform框架模块)的调用

上面介绍了Web API服务端平台的架构设计思路,通过上面的整合,我们减轻了开发重复功能的增删改查等基础功能的控制器代码,把这些接口抽象到接口里面即可实现。

但是我们具体应该如何遵循统一接口层Facade层的约定,然后统一调用WebAPI层的接口,做到悄无声息的从不同的数据源里面获取数据,展示在客户端里面呢。

上面我们分析到,整个混合式Winform框架模块里面,设计方面考虑了数据的获取方面:包含了直接从数据库获取,从WCF服务获取,以及Web API层的数据获取三部分内容,当然还可以有更多的数据接入模式(如WebService等),设计效果如下所示。

所有的数据接入,我们在Facade层都统一到接口里面,客户端的调用也统一到了CallerFactory<T>这个泛型工厂里面,我们根据配置的不同,从不同的模块里面加载,从而实现不同数据源的动态获取了。

如下逻辑就是CallerFactory<T>泛型工厂类的加载逻辑,如下所示:

为了实现简化客户端调用的封装,我们一般也把常规的通用操作封装一下,如下是我原先混合框架里面的设计思路,里面的封装都是通过***Caller的类来进行数据的访问的,这些类统一实现一定关系的集成封装。

为了简化说明调用接口的处理,这里把上面的关系进行了简化,并加入了Web API的调用封装类的处理,几种访问模式下的调用端封装继承关系,如下设计图所示。

最底层的几个DictDataCaller分别是不同访问方式下的接口调用封装类,对于Web API来说,它的访问代码就是如下所示。

        public override bool Delete(stringkey)
{
var action = "Delete";string url = GetPostUrlWithToken(action) +string.Format("&id={0}", key);

CommonResult result
= JsonHelper<CommonResult>.ConvertJson(url);returnresult.Success;
}
public List<DictDataInfo> FindByTypeID(stringdictTypeId)
{
var action = "FindByTypeID";string url = GetTokenUrl(action) + string.Format("&dictTypeId={0}", dictTypeId);

List
<DictDataInfo> result = JsonHelper<List<DictDataInfo>>.ConvertJson(url);returnresult;
}

第一个Delete函数是基类提供的,这里进行了重写,一般情况下,不需要处理就具备增删改分页等基础接口的调用封装了。

由于所有的实现类都实现继承了统一的Facade层的接口,那么统一调用也就是自然而然的事情了。所以在Winform界面里面,所有的调用都是使用CallerFactory<T>进行了统一的处理,数据访问的不同不影响接口的处理, 三种方式的数据调用,统一都是下面的代码进行处理。

DictDataInfo info = CallerFactory<IDictDataService>.Instance.FindByID(ID);if (info != null)
{
SetInfo(info);
try{bool succeed = CallerFactory<IDictDataService>.Instance.Update(info, info.ID.ToString());returnsucceed;
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}

系列文章如下所示:

Web API应用架构在Winform混合框架中的应用(1)

Web API应用架构在Winform混合框架中的应用(2)--自定义异常结果的处理

Web API接口设计经验总结

Web API应用架构在Winform混合框架中的应用(3)--Winfrom界面调用WebAPI的过程分解

Web API应用架构在Winform混合框架中的应用(4)--利用代码生成工具快速开发整套应用

Web API应用架构在Winform混合框架中的应用(5)--系统级别字典和公司级别字典并存的处理方式

在上篇随笔《
Web API应用架构在Winform混合框架中的应用(1)
》中我介绍了关于如何在Winfrom里面整合WebAPI,作为一个新型数据源的接入方式,从而形成了三种不同的数据提供来源,前文在介绍整体性框架方面,着笔较多,注重整合的统一性,但是在Web API方面还不算很具体,本系列将继续这个主题,介绍Web API开发中常见到的一些问题,对其中各个技术要点进行总结,本文主要介绍Web API自定义异常结果的处理。

1、常规的异常处理

统一的异常处理,把正确的信息返回给调用者很重要,可以让接口开发人员或者用户,了解具体的原因所在,这样可以得到有效的错误处理。

参考微信API的处理,微信API,对于调用都有一个错误信息返回,不会直接裸露未经处理的异常,因此它们都是经过了一定的拦截处理,然后把错误信息包装提供给接口调用方的。如下是微信的一些接口处理错误。

错误时微信会返回错误码等信息,JSON数据包示例如下(该示例为AppID无效错误):

{"errcode":40013,"errmsg":"invalid appid"}

我们根据自己的需要,定义了一个统一的错误信息实体类,如下所示。

    /// <summary>
    ///接口返回的错误信息/// </summary>
    public classBaseResultJson
{
/// <summary> ///错误代码/// </summary> public int errcode { get; set; }/// <summary> ///如果不成功,返回的错误信息/// </summary> public string errmsg { get; set; }/// <summary> ///是否成功/// </summary> public bool success { get; set; }
}

这样我们就可以把拦截到的错误信息,转换为这样一个方便使用的实体类信息了。

拦截Web API的调用异常,一般可以结合Try Catch的方法,以及异常拦截器进行处理,如下是主动抛出的一些异常信息处理。

            //如果没有通过,则抛出异常,由异常过滤器统一处理
            if (!result.success)
{
throw newMyApiException(result.errmsg, result.errcode);
}

其中MyApiException是自定义的一个异常信息,用来承载自定义错误信息的异常类。

异常拦截器,我们在Web API里面可以通过Attribute这种标签特性进行处理,如下是我在Web API的基类里面定义了一个异常处理器。

    /// <summary>
    ///所有接口基类/// </summary>
[ExceptionHandling]public class BaseApiController : ApiController

这个特性对象的定义,它的代码如下所示。

    /// <summary>
    ///API自定义错误过滤器属性/// </summary>
    public classExceptionHandlingAttribute : ExceptionFilterAttribute
{
/// <summary> ///统一对调用异常信息进行处理,返回自定义的异常信息/// </summary> /// <param name="context">HTTP上下文对象</param> public override voidOnException(HttpActionExecutedContext context)
{
//自定义异常的处理 MyApiException ex = context.Exception asMyApiException;if (ex != null)
{
throw new HttpResponseException(newHttpResponseMessage(HttpStatusCode.InternalServerError)
{
//封装处理异常信息,返回指定JSON对象 Content = new StringContent(new BaseResultJson(ex.Message, false, ex.errcode).ToJson()),
ReasonPhrase
= "Exception"});

}
//记录关键的异常信息 Debug.WriteLine(context.Exception);//常规异常的处理 string msg = string.IsNullOrEmpty(context.Exception.Message) ? "接口出现了错误,请重试或者联系管理员": context.Exception.Message;throw new HttpResponseException(newHttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content
= newStringContent(msg),
ReasonPhrase
= "Critical Exception"});
}
}

根据这些代码,我们就可以实现对调用异常的统一封装处理,让它给我们返回统一的对象信息了,如下是其中一个调用异常,转换为自定义异常信息后的结果输出。

{"errcode":404,"errmsg":"请求的资源不支持 http 方法“POST”。","success":false}

这样我们在处理Web API的返回结果的时候,可以先处理它的异常信息,具体的处理代码如下所示。

            HttpHelper helper = newHttpHelper();
helper.ContentType
= "application/json";string content = helper.GetHtml(url, postData, true);
VerifyErrorCode(content);

T result
= JsonConvert.DeserializeObject<T>(content);return result;

我们在上面红色部分的代码就是先处理异常定义信息,如果有这些异常,我们可以在界面中进行异常处理显示了。

例如,如果自定义异常存在,我们转换后,把对应的信息显示出来,重新抛出异常即可。

                BaseResultJson errorResult = JsonConvert.DeserializeObject<BaseResultJson>(content);//非成功操作才记录异常,因为有些操作是返回正常的结果({"errcode": 0, "errmsg": "ok"})
                if (errorResult != null && !errorResult.success)
{
string error = string.Format("请求发生错误!错误代码:{0},说明:{1}", (int)errorResult.errcode, errorResult.errmsg);
LogTextHelper.Error(errorResult.ToJson());
throw new Exception(error);//抛出错误 }

2、地址接口异常处理

对于常规的异常,我们通过上面的处理方式,就可以很好进行拦截并处理了,如果接口异常是全局性的,如访问地址簿正确,或者参数多了几个信息,那么调用的接口就不是有效的地址,这样的话,返回的信息就不会被上面的拦截器进行处理了。

如我们给一个无效的API调用路径,在浏览器中获得下面错误结果。

上面结果就无法被我们的常规异常拦截器所捕获,因此不会输出经过封装好的异常信息。

如果需要拦截,我们需要增加自己的消息代理处理,用来捕获这些特殊的异常信息。

    public static classWebApiConfig
{
public static voidRegister(HttpConfiguration config)
{

..............

config.MessageHandlers.Add(
new CustomErrorMessageDelegatingHandler());

上面红色部分就是我们自己添加的消息代理处理类,用来处理一些特殊的异常信息,如下代码所示。

    /// <summary>
    ///API自定义错误消息处理委托类。///用于处理访问不到对应API地址的情况,对错误进行自定义操作。/// </summary>
    public classCustomErrorMessageDelegatingHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage>SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return base.SendAsync(request, cancellationToken).ContinueWith<HttpResponseMessage>((responseToCompleteTask) =>{
HttpResponseMessage response
=responseToCompleteTask.Result;
HttpError error
= null;if (response.TryGetContentValue<HttpError>(outerror))
{
//添加自定义错误处理//error.Message = "Your Customized Error Message"; }if (error != null)
{
//获取抛出自定义异常,有拦截器统一解析 throw new HttpResponseException(newHttpResponseMessage(HttpStatusCode.NotFound)
{
//封装处理异常信息,返回指定JSON对象 Content = new StringContent(new BaseResultJson(error.Message, false, 404).ToJson()),
ReasonPhrase
= "Exception"});
}
else{returnresponse;
}
});
}
}

经过了上面的处理后,我们进一步测试一下不存在的地址的异常处理结果,可以看到输出的内容是经过了自定义对象的转换了。

常规的调用,如果接口不对应,那么错误也是类似下面的消息

{"errcode":404,"errmsg":"找不到与请求 URI“http://localhost:9001/api/SystemType/Delete?signature=72f8d706c79dc14d70fc3f080d4706748d754021&timestamp=1443194061&nonce=0.650359861866666663&appid=website_9A39C2A8&token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIxIiwiaWF0IjoxNDQzMTk0MDM4LCJqdGkiOiI1YmEyYmE5Ni0yZTA4LTQ1ZTgtYTAwNy01MmY3OTkzYTg2NzEiLCJuYW1lIjoiYWRtaW4iLCJjaGFubmVsIjoiMCIsInNoYXJlZGtleSI6IjEyMzRhYmNkIn0.RRXQmmSCJzDK5Or6rmBL5wjd-YIJoEQFc0pOzqhR6IU”匹配的 HTTP 资源。","success":false}

有了这些信息,我们就可以统一我们的调用规则,并进行异常记录和显示了,非常方便。

系列文章如下所示:

Web API应用架构在Winform混合框架中的应用(1)

Web API应用架构在Winform混合框架中的应用(2)--自定义异常结果的处理

Web API接口设计经验总结

Web API应用架构在Winform混合框架中的应用(3)--Winfrom界面调用WebAPI的过程分解

Web API应用架构在Winform混合框架中的应用(4)--利用代码生成工具快速开发整套应用

在Web API接口的开发过程中,我们可能会碰到各种各样的问题,我在前面两篇随笔《
Web API应用架构在Winform混合框架中的应用(1)
》、《
Web API应用架构在Winform混合框架中的应用(2)--自定义异常结果的处理
》也进行了总的介绍,在经过我的大量模块实践并成功运行后,总结了这篇随笔,希望对大家有所帮助。

1、在接口定义中确定MVC的GET或者POST方式

由于我们整个Web API平台是基于MVC的基础上进行的API开发,因此整个Web API的接口,在定义的时候,一般需要显示来声明接口是[HttpGet]或者[HttpPost],虽然有些接口也可以不用声明,但是避免出现类似下面的错误信息,显式声明还是有好处的。

请求的资源不支持 http 方法“POST

例如在基类定义的查找对象接口如下所示。

        /// <summary>
        ///查询数据库,检查是否存在指定ID的对象/// </summary>
        /// <param name="id">对象的ID值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
[HttpGet]public virtual T FindByID(string id, string token)

如果是增删改的接口,一般需要声明为POST方式提交数据,而且基于安全性的考虑,需要携带更多的参数。

        /// <summary>
        ///插入指定对象到数据库中/// </summary>
        /// <param name="info">指定的对象</param>
        /// <returns>执行操作是否成功。</returns>
[HttpPost]public virtual CommonResult Insert(T info, string token, string signature, string timestamp, string nonce, string appid)

2、动态对象的接口定义

在一般的Web API接口里面,我们可能都会碰到很多简单类型的参数,但是又想让它们以POST方式提交数据,那么我们就可以有两种方法来处理,一种是定义一个类来放置这些参数,一种是采用动态的JObject参数,前者有很多不方便的地方,因为我们不可能为每个接口参数定义多一个实体类,这样可能会有很多难以管理的类定义。如下面是微信API的调用接口案例,我们也需要设置这样的处理规则。

接口调用请求说明
http请求方式: POST(请使用https协议)
https://api.weixin.qq.com/cgi-bin/groups/update?access_token=ACCESS_TOKEN
POST数据格式:json
POST数据例子:{"group":{"id":108,"name":"test2_modify2"}}

那么我们采用JObject是这么样的呢,我们来看接口的定义和处理代码。JObject是Newtonsoft.Json.Linq命名空间下的一个对象。

        /// <summary>
        ///修改用户密码/// </summary>
        /// <param name="param">包含userName和userPassword的复合对象</param>
        /// <param name="token">用户访问令牌</param>
        /// <returns></returns>
[HttpPost]public CommonResult ModifyPassword(JObject param,  stringtoken)
{
//令牌检查,不通过则抛出异常 CheckResult checkResult =CheckToken(token);dynamic obj =param;if (obj != null)
{
string userName =obj.userName;string userPassword =obj.userPassword;bool success = BLLFactory<User>.Instance.ModifyPassword(userName, userPassword);return newCommonResult(success);
}
else{throw new MyApiException("传递参数出现错误");
}
}

其中我们把JObject对象转换为我们所需要的对象的时候,因为我们没有定义具体的实体类,因此采用了dynamic语法,声明这是一个动态对象,由运行时获取对应的属性。

dynamic obj = param;

这样我们就可以在调用的时候,动态POST对应的JSON对象给Web API接口,而不需要预先定义各种接口参数的类了。

        /// <summary>
        ///调用Web API接口,修改用户密码/// </summary>
        /// <param name="userName">用户名称</param>
        /// <param name="userPassword">修改的密码</param>
        /// <returns>如果修改成功返回true,否则返回false</returns>
        public bool ModifyPassword(string userName, stringuserPassword)
{
var action = "ModifyPassword";var postData = new{
userName
=userName,
userPassword
=userPassword
}.ToJson();
string url =GetTokenUrl(action);

CommonResult result
= JsonHelper<CommonResult>.ConvertJson(url, postData);return (result != null) ? result.Success : false;
}

其中GetTokenUrl是根据token和API的地址等参数,构建一个完整的提交地址。我们在上面代码通过

            var postData = new{
userName
=userName,
userPassword
=userPassword
}.ToJson();

就可以动态创建一个对象,并生成它的JSON字符串,把数据POST提交到对应的API接口里面即可,然后对结果进行对象的转换就算完成了。

3、集合和分页的处理

在很多接口里面,我们都需要用到分页的处理,Web API也不例外,这样可以提交数据检索效率,减少服务器数据处理的压力,同时也提交客户端的数据显示速度。

一般的集合接口定义如下所示(通用性基类接口)。

        /// <summary>
        ///返回数据库所有的对象集合/// </summary>
        /// <returns>指定对象的集合</returns>
[HttpGet]public virtual List<T> GetAll(stringtoken)
{
//检查用户是否有权限,否则抛出MyDenyAccessException异常 base.CheckAuthorized(AuthorizeKey.ListKey, token);

List
<T> list =baseBLL.GetAll();returnlist;
}

但是这样的返回记录会比较多,一般情况下需要分页,那么分页的处理接口定义如下所示。

        /// <summary>
        ///根据条件查询数据库,并返回对象集合(用于分页数据显示)/// </summary>
        /// <returns>指定对象的集合</returns>
[HttpPost]public virtual PagedList<T> FindWithPager(string condition, PagerInfo pagerInfo, string token)

分页接口,在这里返回的结果里面,用了一个PageList的泛型类,这个方便我们获取当前的记录及总数,它的定义如下所示。

    /// <summary>
    ///分页集合/// </summary>
    /// <typeparam name="T">对象</typeparam>
    public class PagedList<T>{/// <summary>
        ///返回记录的总数/// </summary>
        public int total_count { get; set; }/// <summary>
        ///列表集合/// </summary>
        public List<T> list { get; set; }
}

最后整个分页的处理Web API接口实现如下所示。

        /// <summary>
        ///根据条件查询数据库,并返回对象集合(用于分页数据显示)/// </summary>
        /// <returns>指定对象的集合</returns>
[HttpPost]public virtual PagedList<T> FindWithPager(string condition, PagerInfo pagerInfo, stringtoken)
{
//检查用户是否有权限,否则抛出MyDenyAccessException异常 base.CheckAuthorized(AuthorizeKey.ListKey, token);

List
<T> list =baseBLL.FindWithPager(condition, pagerInfo);//构造成Json的格式传递 var result = new PagedList<T>() { total_count = pagerInfo.RecordCount, list =list };returnresult;
}

最后客户端调用分页的Web API代码如下所示。

        /// <summary>
        ///根据条件查询数据库,并返回对象集合(用于分页数据显示)/// </summary>
        /// <param name="condition">查询的条件</param>
        /// <param name="pagerInfo">分页实体</param>
        /// <returns>指定对象的集合</returns>
        public virtual List<T> FindWithPager(string condition, refPagerInfo pagerInfo)
{
var action = "FindWithPager";string url = GetTokenUrl(action) + string.Format("&condition={0}", condition);var postData =pagerInfo.ToJson();

List
<T> result = new List<T>();
PagedList
<T> list = JsonHelper<PagedList<T>>.ConvertJson(url, postData);if (list != null)
{
pagerInfo.RecordCount
= list.total_count;//修改总记录数
result =
list.list;
}
returnresult;
}

4、混合框架界面整合Web API接口

在整个Web API的平台构建以及在混合框架的整合过程中,我把各个模块还是遵循相对独立的方式进行开发和整合,它们实现了从直接访问数据库、以WCF服务获取数据,以及通过WebAPI调用方式获取数据几种方式的统一,从而实现了整个混合框架的高度整合。

整个混合框架的核心是以相对独立的方式,整合各个可重用的模块,我们可以遵循一定的基础上,快速构建统一的应用平台。

搭建完毕的整个WebAPI平台,其中包括了服务端内容,以API控制器的方式,发布了对应的Web API接口。

在每个混合框架的独立模块里面,我们封装了对应的Web API客户端调用处理,从而实现了Web API的调用方式。

在Win10下,使用Web API模式运行混合框架,获得的主体界面效果如下所示。

独立模块权限管理系统界面如下所示。

系列文章如下所示:

Web API应用架构在Winform混合框架中的应用(1)

Web API应用架构在Winform混合框架中的应用(2)--自定义异常结果的处理

Web API接口设计经验总结

Web API应用架构在Winform混合框架中的应用(3)--Winfrom界面调用WebAPI的过程分解

Web API应用架构在Winform混合框架中的应用(4)--利用代码生成工具快速开发整套应用

Web API应用架构在Winform混合框架中的应用(5)--系统级别字典和公司级别字典并存的处理方式