2023年2月

本篇主要对常规数据操作的处理和实体框架的处理代码进行对比,以便更容易学习理解实体框架里面,对各种数据库处理技巧,本篇介绍几种数据库操作的代码,包括写入中间表操作、联合中间表获取对象集合、递归操作、设置单一字段的修改等几种方式。

1、写入中间表操作

一般情况下,我们可以通过执行数据库脚本方式写入。

        /// <summary>
        ///增加用户IP信息/// </summary>
        /// <param name="userID"></param>
        /// <param name="blackID"></param>
        public void AddUser(int userID, stringblackID)
{
string commandText = string.Format("INSERT INTO T_ACL_BLACKIP_USER(User_ID, BLACKIP_ID) VALUES({0}, '{1}')", userID, blackID);base.SqlExecute(commandText);
}
/// <summary> ///移除用户IP 信息/// </summary> /// <param name="userID"></param> /// <param name="blackID"></param> public void RemoveUser(int userID, stringblackID)
{
string commandText = string.Format("DELETE FROM T_ACL_BLACKIP_USER WHERE User_ID={0} AND BLACKIP_ID='{1}'", userID, blackID);base.SqlExecute(commandText);
}
        /// <summary>
        ///执行一些特殊的语句/// </summary>
        /// <param name="sql">SQL语句</param>
        /// <param name="parameters">要应用于命令字符串的参数</param>
        public virtual int SqlExecute(string sql, params object[] parameters)
{
returnbaseContext.Database.ExecuteSqlCommand(sql, parameters);
}

上面间接用来执行脚本方式写入中间表数据;但是,如果我们采用Entity Framework的方式来处理,也是可以的,例如我们为角色添加和移除用户的代码如下所示。

        public void AddUser(int userID, introleID)
{
var role = new Role() { ID = roleID, Functions = new List<Function>() };
context.Roles.Attach(role);

User user
= new User() { ID =userID };
context.Users.Attach(user);
int count = this.GetQueryable().Where(s => s.ID == roleID).Count(s => s.Users.Any(p => p.ID ==userID));if (count == 0)
{
role.Users.Add(user);
}

context.SaveChanges();
}
public void RemoveUser(int userID, introleID)
{
var role = this.GetQueryable().FirstOrDefault(s => s.ID ==roleID);var deleted = role.Users.FirstOrDefault(s => s.ID ==userID);if (deleted != null)
{
role.Users.Remove(deleted);
}

context.SaveChanges();
}

如果我们需要重新写入中间列表的数据,那么处理方式如下所示。

        public bool EditOuUsers(int ouID, List<int>newUserList)
{
//先清空 Ou ou = this.GetQueryable().FirstOrDefault(s => s.ID ==ouID);
ou.Users.Clear();
context.SaveChanges();
//设置新的列表 var users = context.Users.Where(s =>newUserList.Contains(s.ID)).ToList();
ou.Users
=users;return context.SaveChanges() > 0;
}

2、联合中间表获取对象集合

一般情况下,我们在数据访问层需要结合中间表进行查询的时候,常用的处理方式是在SQL语句里面联合两个表进行处理。如联合机构角色中间表、机构用户中间表来获取对应机构的列表操作如下所示。

        public List<OUInfo> GetOUsByRole(introleID)
{
string sql = "SELECT * FROM T_ACL_OU INNER JOIN T_ACL_OU_Role On [T_ACL_OU].ID=T_ACL_OU_Role.OU_ID WHERE Role_ID =" +roleID;return this.GetList(sql, null);
}
public List<OUInfo> GetOUsByUser(intuserID)
{
string sql = "SELECT * FROM T_ACL_OU INNER JOIN T_ACL_OU_User On [T_ACL_OU].ID=T_ACL_OU_User.OU_ID WHERE User_ID =" +userID;return this.GetList(sql, null);
}

如果是在实体框架里面,我们不能那么直接操作SQL语句,尽可能用内置的LINQ查询操作,使用的时候,你也发现很简单的。

        public IList<Ou> GetOUsByRole(introleID)
{
return this.GetQueryable().Where(s => s.Roles.Any(c => c.ID ==roleID)).ToList();
}
public IList<Ou> GetOUsByUser(intuserID)
{
return this.GetQueryable().Where(s => s.Users.Any(c => c.ID ==userID)).ToList();
}

3、递归操作

很多时候,我们表的数据是有层次结构的,有时候,需要根据父ID获取下面的子集列表的时候,我们就需要用到递归。常规的SQL处理方式,可以如下实现。

       /// <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;
}

如果这样的实现,在实体框架实现,那么应该如何处理呢?

        public IList<Ou> GetAllOUsByParent(intparentId)
{
List
<Ou> list = this.GetQueryable().Where(s => s.PID == parentId).Where(s => !s.IsDeleted).OrderBy(s => s.PID).OrderBy(s =>s.Name).ToList();foreach(Ou info inlist)
{
list.AddRange(GetAllOUsByParent(info.ID));
}
returnlist;
}

是不是觉得简单很多了呢?

4、设置单一字段的修改

有时候,我们可能需要更新列表里面的某些字段,那么如果采用SQL操作处理,那么代码如下所示。

        /// <summary>
        ///设置删除标志/// </summary>
        /// <param name="id">记录ID</param>
        /// <param name="deleted">是否删除</param>
        /// <param name="trans">事务对象</param>
        /// <returns></returns>
        public bool SetDeletedFlag(object id, bool deleted = true, DbTransaction trans = null)
{
int intDeleted = deleted ? 1 : 0;string sql = string.Format("Update {0} Set Deleted={1} Where ID = {2}", tableName, intDeleted, id);return SqlExecute(sql, trans) > 0;
}

如果是采用实体框架,那么处理代码如下所示。

        public bool SetDeletedFlag(object id, bool deleted = true)
{
var ou = new Ou() { ID = id.ToString().ToInt32(), IsDeleted =deleted };
context.Ous.Attach(ou);

context.Entry(ou).Property(x
=> x.Deleted).IsModified = true;return context.SaveChanges() > 0;
}

如果更新多个字段,那么常规的SQL方式代码如下所示。

        /// <summary>
        ///更新用户登录的时间和IP地址/// </summary>
        /// <param name="id">用户ID</param>
        /// <param name="ip">IP地址</param>
        /// <param name="macAddr">MAC地址</param>
        /// <returns></returns>
        public bool UpdateUserLoginData(int id, string ip, stringmacAddr)
{
//先复制最后的登录时间和IP地址 string sql = string.Format("Update {0} set LastLoginIP=CurrentLoginIP,LastLoginTime=CurrentLoginTime,LastMacAddress=CurrentMacAddress Where ID={1}", tableName, id);
Database db
=CreateDatabase();
DbCommand dbCommand
=db.GetSqlStringCommand(sql);
db.ExecuteNonQuery(dbCommand);

sql
= string.Format("Update {0} Set CurrentLoginIP='{1}',CurrentMacAddress='{2}', CurrentLoginTime=@CurrentLoginTime Where ID = {3}", tableName, ip, macAddr, id);
dbCommand
=db.GetSqlStringCommand(sql);
db.AddInParameter(dbCommand,
"CurrentLoginTime", DbType.DateTime, DateTime.Now);return db.ExecuteNonQuery(dbCommand) > 0;
}

多个字段的更新,使用实体框架则是如下所示。

        public bool UpdateUserLoginData(int id, string ip, stringmacAddr)
{
//先复制最后的登录时间和IP地址 string tableName = context.GetTableName<User>();string sql = string.Format("Update {0} set LastLoginIP=CurrentLoginIP,LastLoginTime=CurrentLoginTime,LastMacAddress=CurrentMacAddress Where ID={1}", tableName, id);
SqlExecute(sql);
var obj = new User() { ID = id.ToString().ToInt32(), CurrentLoginIP = ip, CurrentMacAddress = macAddr, CurrentLoginTime =DateTime.Now };
context.Users.Attach(obj);
context.Entry(obj).Property(x
=> x.CurrentLoginIP).IsModified = true;
context.Entry(obj).Property(x => x.CurrentMacAddress).IsModified = true;
context.Entry(obj).Property(x => x.CurrentLoginTime).IsModified = true;
return context.SaveChanges() > 0;
}
 

这个系列文章如下所示:

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

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

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

Entity Framework 实体框架的形成之旅--实体数据模型 (EDM)的处理(4)

Entity Framework 实体框架的形成之旅--Code First的框架设计(5)

Entity Framework 实体框架的形成之旅--Code First模式中使用 Fluent API 配置(6)

Entity Framework 实体框架的形成之旅--数据传输模型DTO和实体模型Entity的分离与联合

Entity Framework 实体框架的形成之旅--界面操作的几个典型的处理(8)

Entity Framework 实体框架的形成之旅--几种数据库操作的代码介绍(9)

本篇介绍Entity Framework 实体框架的文章已经到了第十篇了,对实体框架的各个分层以及基类的封装管理,已经臻于完善,为了方便对基类接口的正确性校验,以及方便对以后完善或扩展接口进行回归测试,那么建立单元测试就有很大的必要,本篇主要介绍如何利用VS创建内置的单元测试项目进行实体框架的基类接口测试。

在采用单元测试这个事情上,很多人可能想到了NUnit单元测试工具和NMock工具进行处理,其实微软VS里面也已经为我们提供了类似的单元测试工具了,可以不需要使用这个第三方的单元测试工具,经试用VS的单元测试工具还是整合性很好,使用非常方便的。

1、实体框架架构及基础类库接口

在上次的随笔《
Entity Framework 实体框架的形成之旅--数据传输模型DTO和实体模型Entity的分离与联合
》里面,我根据实体框架中混合模式的框架结构,所涉及的架构图形如下所示。

我们从上图可以看到,整个框架从下往上分为了几个明显的层次,一个数据访问层DAL层,一个是业务逻辑层BLL层,一个是Facade门面层,各个层的功能不同,这几个层中以DAL层最为复杂一些,涉及到底层多种数据库的抽象实现,由于Entity Framework 实体框架本身就是对多种数据库的实现抽象,因此本文重点针对这个
DAL
层进行单元测试。

其中的实体框架的公用类库(WHC.Framework.EF),里面涉及到的IBaseDAL就是数据访问层的基类接口,具体数据访问的抽象实现就在BaseDAL的基类上。

在IBaseDAL接口里面,定义了很多我们数据访问类需要使用的增删改查、分页、统计、辅助方法等接口,以及各个方法的异步方法接口,如下所示。

namespaceWHC.Framework.EF
{
/// <summary> ///数据访问层基类接口/// </summary> /// <typeparam name="T">实体对象类型</typeparam> public interface IBaseDAL<T> where T : class{#region 对象添加、修改、删除 /// <summary> ///插入指定对象到数据库中/// </summary> /// <param name="t">指定的对象</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> boolInsert(T t);/// <summary> ///插入指定对象到数据库中(异步)/// </summary> /// <param name="t">指定的对象</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> Task<bool>InsertAsync(T t);/// <summary> ///插入指定对象集合到数据库中/// </summary> /// <param name="list">指定的对象集合</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> bool InsertRange(IEnumerable<T>list);/// <summary> ///插入指定对象集合到数据库中(异步)/// </summary> /// <param name="list">指定的对象集合</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> Task<bool> InsertRangeAsync(IEnumerable<T>list);/// <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> ///从数据库中删除指定对象/// </summary> /// <param name="t">指定对象</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> boolDelete(T t);/// <summary> ///从数据库中删除指定对象(异步)/// </summary> /// <param name="t">指定对象</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> Task<bool>DeleteAsync(T t);/// <summary> ///根据指定条件,从数据库中删除指定对象/// </summary> /// <param name="match">条件表达式</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> bool DeleteByExpression(Expression<Func<T, bool>>match);/// <summary> ///根据指定条件,从数据库中删除指定对象(异步)/// </summary> /// <param name="match">条件表达式</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> Task<bool> DeleteByExpressionAsync(Expression<Func<T, bool>>match);/// <summary> ///根据指定条件,从数据库中删除指定对象/// </summary> /// <param name="condition">删除记录的条件语句</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> bool DeleteByCondition(stringcondition);/// <summary> ///根据指定条件,从数据库中删除指定对象(异步)/// </summary> /// <param name="condition">删除记录的条件语句</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> Task<bool> DeleteByConditionAsync(stringcondition);#endregion

或者一些其他的分页等复杂的实现接口。

        #region 返回集合的接口

        /// <summary>
        ///返回可查询的记录源/// </summary>
        /// <returns></returns>
        IQueryable<T>GetQueryable();/// <summary>
        ///根据条件表达式返回可查询的记录源/// </summary>
        /// <param name="match">查询条件</param>
        /// <param name="sortPropertyName">排序表达式</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>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        IQueryable<T> GetQueryable<TKey>(Expression<Func<T, bool>> match, Expression<Func<T, TKey>> orderByProperty, bool isDescending = true);/// <summary>
        ///返回数据库所有的对象集合/// </summary>
        /// <returns></returns>
        IList<T>GetAll();/// <summary>
        ///返回数据库所有的对象集合(异步)/// </summary>
        /// <returns></returns>
        Task<IList<T>>GetAllAsync();/// <summary>
        ///返回数据库所有的对象集合/// </summary>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        IList<T> GetAll<TKey>(Expression<Func<T, TKey>> orderByProperty, bool isDescending = true);/// <summary>
        ///返回数据库所有的对象集合(异步)/// </summary>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        Task<IList<T>> GetAllAsync<TKey>(Expression<Func<T, TKey>> orderByProperty, bool isDescending = true);/// <summary>
        ///返回数据库所有的对象集合(用于分页数据显示)/// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="info">分页实体</param>
        /// <returns>指定对象的集合</returns>
        IList<T>GetAllWithPager(PagerInfo info);/// <summary>
        ///返回数据库所有的对象集合(用于分页数据显示,异步)/// </summary>
        /// <param name="info">分页实体</param>
        /// <returns>指定对象的集合</returns>
        Task<IList<T>>GetAllWithPagerAsync(PagerInfo info);/// <summary>
        ///根据条件查询数据库,并返回对象集合/// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns></returns>
        IList<T> Find(Expression<Func<T, bool>>match);/// <summary>
        ///根据条件查询数据库,并返回对象集合(异步)/// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns></returns>
        Task<IList<T>> FindAsync(Expression<Func<T, bool>>match);/// <summary>
        ///根据条件查询数据库,并返回对象集合/// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        IList<T> Find<TKey>(Expression<Func<T, bool>> match, Expression<Func<T, TKey>> orderByProperty, bool isDescending = true);/// <summary>
        ///根据条件查询数据库,并返回对象集合(异步)/// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        Task<IList<T>> FindAsync<TKey>(Expression<Func<T, bool>> match, Expression<Func<T, TKey>> orderByProperty, bool isDescending = true);/// <summary>
        ///根据条件查询数据库,并返回对象集合(用于分页数据显示)/// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="info">分页实体</param>
        /// <returns>指定对象的集合</returns>
        IList<T> FindWithPager(Expression<Func<T, bool>>match, PagerInfo info);/// <summary>
        ///根据条件查询数据库,并返回对象集合(用于分页数据显示,异步)/// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="info">分页实体</param>
        /// <returns>指定对象的集合</returns>
        Task<IList<T>> FindWithPagerAsync(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>
        IList<T> FindWithPager<TKey>(Expression<Func<T, bool>> match, PagerInfo info, Expression<Func<T, TKey>> orderByProperty, bool isDescending = true);#endregion

以及更多的方法接口,我们为了校验没有接口都能够正常工作,就需要对它们进行单元测试。

2、在VS里面创建单元测试项目及编写单元测试代码

在VS里面创建内置的单元测试项目如下所示,在添加新项目里面选择测试->单元测试项目即可,如下图所示。

为了方便对基类测试,我们还是需要创建一个简单的代表性数据库用来检查基础的接口操作。

由于前面的系列,已经介绍过了,我们在构建数据访问层的时候,使用的是基于IOC的方式构建一个对象的接口对象,如
这样
代码
IFactory.Instance<IUserDAL>()
所示。

而单元测试,基本原理就是我们调用接口,并获取对应的输出结果,和我们预期的值进行对比,如果吻合就是正常通过的测试用例。

为了进行基础类库的单元测试,我们需要根据实体框架的结构搭建一个具体表的对象项目工程,这个采用代码生成工具Database2Sharp进行生成就可以了,生成的处理操作如下所示。

这样根据表快速生成的整个实体框架,就是我们所需要的实体框架项目,具体效果如下所示。

例如我们创建一个查找记录的单元测试方法代码如下所示。

namespaceTestFrameworkEF
{
[TestClass]
public classTestBaseDAL
{
private string userId =Guid.NewGuid().ToString();

[TestInitialize]
public voidInit()
{
User user
= new User() { ID = userId, Account = "Nunit", Password = "Nunit"};bool result = IFactory.Instance<IUserDAL>().Insert(user);
Assert.AreEqual(result,
true);
}

[TestCleanup]
public voidCleanup()
{
bool result = IFactory.Instance<IUserDAL>().Delete(userId);
Assert.AreEqual(result,
true);
}

[TestMethod]
public voidFindByID()
{
User user
= IFactory.Instance<IUserDAL>().FindByID(userId);

Assert.IsNotNull(user);
Assert.AreEqual(user.ID, userId);
}

其中上面红色代码部分就是单元测试的各种标识,包括单元测试类标识,以及初始化、退出清除、测试用例的标识。

上面案例,我们是在单元测试前,在数据库写入一条记录,然后在进行各种单元测试用例的运行及校验,最后退出的时候,清除我们写入的记录。

而记录的更新和删除接口,我们具体的单元测试代码如下所示。

[TestMethod]public voidUpdate()
{
string newAccount = "Test";
User user
= IFactory.Instance<IUserDAL>().FindByID(userId);
user.Account
=newAccount;bool result = IFactory.Instance<IUserDAL>().Update(user, user.ID);
Assert.AreEqual(result,
true);

user
= IFactory.Instance<IUserDAL>().FindByID(userId);
Assert.IsNotNull(user);
Assert.AreEqual(user.Account, newAccount);
}

[TestMethod]
public voidDelete()
{
var id =Guid.NewGuid().ToString();
User user
= new User() { ID = id, Account = "Nunit", Password = "Nunit"};bool result = IFactory.Instance<IUserDAL>().Insert(user);
Assert.AreEqual(result,
true);

result
= IFactory.Instance<IUserDAL>().Delete(id);
Assert.AreEqual(result,
true);
}

最后我们整个单元测试的测试代码如下所示。

usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Text;usingSystem.Threading.Tasks;usingWHC.Framework.EF;usingEFCore.IDAL;usingEFCore.Entity;usingMicrosoft.VisualStudio.TestTools.UnitTesting;usingSystem.Linq.Expressions;usingSystem.Linq;usingWHC.Pager.Entity;namespaceTestFrameworkEF
{
[TestClass]
public classTestBaseDAL
{
private string userId =Guid.NewGuid().ToString();

[TestInitialize]
public voidInit()
{
User user
= new User() { ID = userId, Account = "Nunit", Password = "Nunit"};bool result = IFactory.Instance<IUserDAL>().Insert(user);
Assert.AreEqual(result,
true);
}

[TestCleanup]
public voidCleanup()
{
bool result = IFactory.Instance<IUserDAL>().Delete(userId);
Assert.AreEqual(result,
true);
}

[TestMethod]
public voidFindByID()
{
User user
= IFactory.Instance<IUserDAL>().FindByID(userId);

Assert.IsNotNull(user);
Assert.AreEqual(user.ID, userId);
}

[TestMethod]
public voidInsert()
{
var id =Guid.NewGuid().ToString();
User user
= new User() { ID = id , Account = "Nunit", Password = "Nunit"};bool result = IFactory.Instance<IUserDAL>().Insert(user);
Assert.AreEqual(result,
true);

user
= IFactory.Instance<IUserDAL>().FindByID(id);
Assert.IsNotNull(user);
Assert.AreEqual(user.ID, id);

result
= IFactory.Instance<IUserDAL>().Delete(id);
Assert.AreEqual(result,
true);
}

[TestMethod]
public voidInsertRang()
{
List
<User> list = new List<User>();for(int i = 0; i<3; i++)
{
var id =Guid.NewGuid().ToString();
User user
= new User() { ID = id, Account = "Nunit" + i.ToString(), Password = "Nunit"};
list.Add(user);
}
bool result = IFactory.Instance<IUserDAL>().InsertRange(list);
Assert.AreEqual(result,
true);foreach(User user inlist)
{
result
= IFactory.Instance<IUserDAL>().Delete(user.ID);
Assert.AreEqual(result,
true);
}
}

[TestMethod]
public voidUpdate()
{
string newAccount = "Test";
User user
= IFactory.Instance<IUserDAL>().FindByID(userId);
user.Account
=newAccount;bool result = IFactory.Instance<IUserDAL>().Update(user, user.ID);
Assert.AreEqual(result,
true);

user
= IFactory.Instance<IUserDAL>().FindByID(userId);
Assert.IsNotNull(user);
Assert.AreEqual(user.Account, newAccount);
}

[TestMethod]
public voidDelete()
{
var id =Guid.NewGuid().ToString();
User user
= new User() { ID = id, Account = "Nunit", Password = "Nunit"};bool result = IFactory.Instance<IUserDAL>().Insert(user);
Assert.AreEqual(result,
true);

result
= IFactory.Instance<IUserDAL>().Delete(id);
Assert.AreEqual(result,
true);
}

[TestMethod]
public voidDeleteByExpression()
{
var id =Guid.NewGuid().ToString();
User user
= new User() { ID = id, Account = "Nunit", Password = "Nunit"};bool result = IFactory.Instance<IUserDAL>().Insert(user);
Assert.AreEqual(result,
true);

Expression
<Func<User, bool>> expression = p => p.ID == user.ID && p.Account ==user.Account;
result
= IFactory.Instance<IUserDAL>().DeleteByExpression(expression);
Assert.AreEqual(result,
true);
}

[TestMethod]
public voidDeleteByCondition()
{
var id =Guid.NewGuid().ToString();
User user
= new User() { ID = id, Account = "Nunit", Password = "Nunit"};bool result = IFactory.Instance<IUserDAL>().Insert(user);
Assert.AreEqual(result,
true);string condition = string.Format("ID ='{0}'", id);
result
= IFactory.Instance<IUserDAL>().DeleteByCondition(condition);
Assert.AreEqual(result,
true);
}

[TestMethod]
public voidFindSingle()
{
Expression
<Func<User, bool>> expression = p => p.ID ==userId;
User dbUser
= IFactory.Instance<IUserDAL>().FindSingle(expression);
Assert.IsNotNull(dbUser);
Assert.AreEqual(dbUser.ID, userId);
}

[TestMethod]
public voidGetQueryable()
{
User user
= IFactory.Instance<IUserDAL>().GetQueryable().Take(1).ToList()[0];
Assert.IsNotNull(user);
//Assert.AreEqual(user.ID, userId); }

[TestMethod]
public voidGetQueryableExpression()
{
Expression
<Func<User, bool>> expression = p => p.ID ==userId;
User user
= IFactory.Instance<IUserDAL>().GetQueryable(expression, "Account").Take(1).ToList()[0];
Assert.IsNotNull(user);
Assert.AreEqual(user.ID, userId);
}


[TestMethod]
public voidGetQueryableExpression2()
{
Expression
<Func<User, bool>> expression = p => p.ID ==userId;
User user
= IFactory.Instance<IUserDAL>().GetQueryable(expression, s=>s.Account).Take(1).ToList()[0];
Assert.IsNotNull(user);
Assert.AreEqual(user.ID, userId);
}

[TestMethod]
public voidGetAll()
{
User user
= IFactory.Instance<IUserDAL>().GetAll().Take(1).ToList()[0];
Assert.IsNotNull(user);
}

[TestMethod]
public voidGetAllOrderBy()
{
User user
= IFactory.Instance<IUserDAL>().GetAll(s=>s.Account).Take(1).ToList()[0];
Assert.IsNotNull(user);
}

[TestMethod]
public voidGetAllWithPager()
{
PagerInfo pagerInfo
= newPagerInfo();
pagerInfo.PageSize
= 30;

User user
= IFactory.Instance<IUserDAL>().GetAllWithPager(pagerInfo).Take(1).ToList()[0];
Assert.IsNotNull(user);
}

[TestMethod]
public voidFind()
{
Expression
<Func<User, bool>> expression = p => p.ID ==userId;
User user
= IFactory.Instance<IUserDAL>().Find(expression).Take(1).ToList()[0];
Assert.IsNotNull(user);

Assert.AreEqual(user.ID, userId);
}

[TestMethod]
public voidFind2()
{
Expression
<Func<User, bool>> expression = p => p.ID ==userId;
User user
= IFactory.Instance<IUserDAL>().Find(expression, s=>s.Account).Take(1).ToList()[0];
Assert.IsNotNull(user);

Assert.AreEqual(user.ID, userId);
}

[TestMethod]
public voidFindWithPager()
{
PagerInfo pagerInfo
= newPagerInfo();
pagerInfo.PageSize
= 30;

Expression
<Func<User, bool>> expression = p => p.ID ==userId;

User user
= IFactory.Instance<IUserDAL>().FindWithPager(expression, pagerInfo).Take(1).ToList()[0];
Assert.IsNotNull(user);

Assert.AreEqual(user.ID, userId);
}

[TestMethod]
public voidFindWithPager2()
{
PagerInfo pagerInfo
= newPagerInfo();
pagerInfo.PageSize
= 30;

Expression
<Func<User, bool>> expression = p => p.ID ==userId;

User user
= IFactory.Instance<IUserDAL>().FindWithPager(expression, pagerInfo, s=>s.Account).Take(1).ToList()[0];
Assert.IsNotNull(user);

Assert.AreEqual(user.ID, userId);
}

[TestMethod]
public voidGetRecordCount()
{
int count = IFactory.Instance<IUserDAL>().GetRecordCount();
Assert.AreNotEqual(count,
0);
}

[TestMethod]
public voidGetRecordCount2()
{
Expression
<Func<User, bool>> expression = p => p.ID ==userId;int count = IFactory.Instance<IUserDAL>().GetRecordCount(expression);
Assert.AreNotEqual(count,
0);
}

[TestMethod]
public voidIsExistRecord()
{
bool result = IFactory.Instance<IUserDAL>().IsExistRecord(userId);
Assert.AreEqual(result,
true);
}

[TestMethod]
public voidIsExistRecord2()
{
Expression
<Func<User, bool>> expression = p => p.ID ==userId;bool result = IFactory.Instance<IUserDAL>().IsExistRecord(expression);
Assert.AreEqual(result,
true);
}

[TestMethod]
public voidSqlExecute()
{
string newAccount = "newAccount";string sql = string.Format("update [User] set Account='{0}' Where ID='{1}'", newAccount, userId);int count = IFactory.Instance<IUserDAL>().SqlExecute(sql);
Assert.AreEqual(count,
1);
}

[TestMethod]
public voidSqlValueList()
{
string sql = string.Format("Select ID From [User]");
ICollection
<string> valueString = IFactory.Instance<IUserDAL>().SqlValueList(sql);
Assert.IsNotNull(valueString);
Assert.IsTrue(valueString.Count
> 0);
}
}
}

3、运行单元测试

代码编译没有问题后,我们需要检验我们的单元测试代码的正确性,那么只需要在VS的测试菜单里面,执行下面的操作即可。

最后得到的运行结果如下所示,验证了我们基类代码的正确性。

Web API 是一种应用接口框架,它能够构建HTTP服务以支撑更广泛的客户端(包括浏览器,手机和平板电脑等移动设备)的框架,
ASP.NET Web API 是一种用于在 .NET Framework 上构建 RESTful 应用程序的理想平台。本文主要以ASP.NET Web API 的框架实现来介绍整个Web API应用架构设计,但不局限于.NET的技术。

1、Web API的核心层设计

在目前发达的应用场景下,我们往往需要接入Winform客户端、APP程序、网站程序、以及目前热火朝天的微信应用等,这些数据应该可以由同一个服务提供,这个就是我们所需要构建的Web API平台,基于上述的需求,很多企业的需求都是以Web API优先的理念来设计整个企业应用体系的。Web API作为整个纽带的核心,在整个核心层需要考虑到统一性、稳定性、以及安全性等方面因素。

从上图我们可以看到,整个外围的应用场景围绕着Web API核心层构建,如果我们把它换一种方式表达,那么也就是下面的设计图示,我们把微信应用、APP应用、Web应用、Winform应用,作为Web API接口层上面的一个界面应用层来处理,这样就是基于一个API接口层,接入多个或多种方式的界面应用层,从而构建企业丰富的数据服务应用。

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

在整个WebAPI下面可以通过业务逻辑层整合数据存储和外部接口访问两部分工作,也就是数据访问层、外部接口层这样的分层概念,如果扩展开来,我们还可以提供给客户一些文件、图片、视频等资料的文件存储,类似微信的多媒体API接口一样,这样整个Web API的接口层就能为所有接入的客户端提供丰富的数据接口,从而实现强大的、灵活的接入。

上面提到了如果考虑到响应式的集成处理,我们甚至可以把微信应用、APP应用、Web应用做层一套Web程序,其实APP应用层,可以分为两种类型,一种是原生的APP类型,采用原生语言如Object C来开发IOS应用,采用java来开发安卓的原生应用一样;还有一种是封装一个入口的原生框架+后台响应式Web页面,如下图所示。

对比原生应用,采用入口框架+响应式页面的方式,开发效率非常快、升级维护成本也可以降低很多,比较Web开发总比使用原始APP开发快捷得多。

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

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

2、Web API层在Winform混合框架中的应用

同样,在Winform界面里面,我们除了可以利用直接访问数据库方式,以及采用访问
分布式
WC
F
服务的方式接入,还可以使得它能够访问Web API的数据服务,从而构建成一个适应性更加广泛、功能更加强大的混合式开发框架模式。

安全性方便,直接访问数据库方式,没有在网络上公开接口,它们只是在单机或者局域网安全的环境运行,因此只需要确保数据库的安全即可,一般可以通过加密连接字符串方式实现一定的限制即可。

WCF服务的安全性,可以通过X509证书方式实现校验,也还可以利用自定义的用户名、密码验证方式进行检查等等。

对于Web API,由于它提供的是一种无状态的接口访问,而且往往Web API一般为了多种客户端接入的需要,可能需要发布在公网上进行访问,因此我们需要更加注重Web API接口层的安全性,这方面我们后面详细介绍。

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

下面图示是我的基于传统访问数据库方式和分布式WCF
数据
服务访问方式的一个模块分析图,围绕着混合型框架的核心,我们可以构建很多松散耦合的模块,从而能够为我们Winform应用的开发集成提供更高的开发效率。

混合型框架可以看成是Winform框架高级版本,除了它本身是一个完整的业务系统外,它外围的所有辅助性模块均(如通用权限、通用字典、通用附件管理、通用人员管理。。。。)都实现了这种混合型的框架,因此使用非常方便,整个框架如果简化来看,就是在原有的Winform界面层,用接口调用方式,避免和业务逻辑类的紧耦合关系。由于它是通过接口方式的调用方式,它本身又可以通过配置指定指向WCF的实现,因此也囊括了WCF框架的一切特点。在完成Web API层的开发后,基于Web API层的整合就是我下一个阶段的工作了。

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

在上篇随笔《
Web API应用架构设计分析(1)
》,我对Web API的各种应用架构进行了概括性的分析和设计,Web API 是一种应用接口框架,它能够构建HTTP服务以支撑更广泛的客户端(包括浏览器,手机和平板电脑等移动设备)的框架,本篇继续这个主题,介绍如何利用ASP.NET Web API 来设计Web API层以及相关的调用处理。

1、Web API的接口访问分类

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

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

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

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

下面图示就是这几种接入方式的说明和大概应用场景。

2、Web API使用安全签名的实现

首先我们为用户注册的时候,需要由我们认可的终端发起,也就是它们需要进行安全签名,后台确认签名有效性,才能正常实现用户注册,否则遭到伪造数据,系统就失去原有的意义了。

    /// <summary>
    ///注册用户信息接口/// </summary>
    public interfaceIUserApi
{
/// <summary> ///注册用户处理,包括用户名,密码,身份证号,手机等信息/// </summary> /// <param name="json">注册用户信息</param> /// <param name="signature">加密签名字符串</param> /// <param name="timestamp">时间戳</param> /// <param name="nonce">随机数</param> /// <param name="appid">应用接入ID</param> /// <returns></returns> ResultData Add(UserJson json,string signature, string timestamp, string nonce, stringappid);
}

其实我们获得用户的令牌,也是需要进行用户安全签名认证的,这样我们才有效保证用户身份令牌获取的合法性。

    /// <summary>
    ///系统认证等基础接口/// </summary>
    public interfaceIAuthApi
{
/// <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, stringappid);
}

上面介绍到的参数,我们提及了几个参数,一个是加密签名字符串,一个是时间戳,一个是随机数,一个是应用接入ID,我们一般的处理规则如下所示。

1)Web API 为各种应用接入,如APP、Web、Winform等接入端分配应用AppID以及通信密钥AppSecret,双方各自存储。
2)接入端在请求Web API接口时需携带以下参数:signature、 timestamp、nonce、appid,签名是根据几个参数和加密秘钥生成。
3) Web API 收到接口调用请求时需先检查传递的签名是否合法,验证后才调用相关接口。

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

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

C#端代码校验如下所示。

        /// <summary>
        ///检查应用接入的数据完整性/// </summary>
        /// <param name="signature">加密签名内容</param>
        /// <param name="timestamp">时间戳</param>
        /// <param name="nonce">随机字符串</param>
        /// <param name="appid">应用接入Id</param>
        /// <returns></returns>
        public CheckResult ValidateSignature(string signature, string timestamp, string nonce, stringappid)
{
CheckResult result
= newCheckResult();
result.errmsg
= "数据完整性检查不通过";//根据Appid获取接入渠道的详细信息 AppInfo channelInfo = BLLFactory<App>.Instance.FindByAppId(appid);if (channelInfo != null)
{
#region 校验签名参数的来源是否正确 string[] ArrTmp ={ channelInfo.AppSecret, timestamp, nonce };

Array.Sort(ArrTmp);
string tmpStr = string.Join("", ArrTmp);

tmpStr
= FormsAuthentication.HashPasswordForStoringInConfigFile(tmpStr, "SHA1");
tmpStr
=tmpStr.ToLower();if (tmpStr == signature &&ValidateUtil.IsNumber(timestamp))
{
DateTime dtTime
=timestamp.ToInt32().IntToDateTime();double minutes =DateTime.Now.Subtract(dtTime).TotalMinutes;if (minutes >timspanExpiredMinutes)
{
result.errmsg
= "签名时间戳失效";
}
else{
result.errmsg
= "";
result.success
= true;
result.channel
=channelInfo.Channel;
}
}
#endregion}returnresult;
}

一旦我们完成对安全签名进行成功认证,也就是我们对数据提交的来源和完整性进行了确认,就可以进行更多和安全性相关的操作了,如获取用户的访问令牌信息的操作如下所示。

第一步是验证用户的签名是否符合要求,符合要求后进行用户信息的比对,并生成用户访问令牌数据JSON,返回给调用端即可。

3、Web API使用安全令牌的实现

通过上面的接口,我们获取到的用户访问令牌,以后和用户相关的信息调用,我们就可以通过这个令牌参数进行传递就可以了,这个令牌带有用户的一些基础信息,如用户ID,过期时间等等,这个Token的设计思路来源于JSON Web Token (JWT),具体可以参考
http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html
,以及GitHub上的项目
https://github.com/jwt-dotnet/jwt

由于Web API的调用,都是一种无状态方式的调用方式,我们通过token来传递我们的用户信息,这样我们只需要验证Token就可以了。

JWT的令牌生成逻辑如下所示

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

检查的代码,就是把令牌生成的过程逆反过来,获取相应的信息,并且对令牌签发的时间进行有效性判断,一般可以约定一个失效时间,如1天或者7天,也不用设置太短。

        /// <summary>
        ///检查用户的Token有效性/// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        public CheckResult ValidateToken(stringtoken)
{
//返回的结果对象 CheckResult result = newCheckResult();
result.errmsg
= "令牌检查不通过";if (!string.IsNullOrEmpty(token))
{
try{string decodedJwt =JsonWebToken.Decode(token, sharedKey);if (!string.IsNullOrEmpty(decodedJwt))
{
#region 检查令牌对象内容dynamic root=JObject.Parse(decodedJwt);string username =root.name;string userid =root.iss;int jwtcreated = (int)root.iat;//检查令牌的有效期,7天内有效 TimeSpan t = (DateTime.UtcNow - new DateTime(1970, 1, 1));int timestamp = (int)t.TotalDays;if (timestamp - jwtcreated >expiredDays)
{
throw new ArgumentException("用户令牌失效.");
}
//成功校验 result.success = true;
result.errmsg
= "";
result.userid
=userid;#endregion}
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
}
}
returnresult;
}

一般来说,访问令牌不能永久有效,对于访问令牌的重新更新问题,可以设置一个规则,只允许最新的令牌使用,并把它存储在接口缓存里面进行对比,应用系统退出的时候,就把内存里面的Token移除就可以了。

4、ASP.NET Web API的开发

上面我们定义了一般的Web API接口,以及实现相应的业务实现,如果我们需要创建Web API层,还需要构建一个Web API项目的。

创建好相应的项目后,可以为项目添加一个Web API
基类
,方便控制共同的接口。

然后我们就可以在Controller目录上创建更多的应用API控制器了。

最后我们为了统一所有的API接口都是返回JSON方式,我们需要对WebApiConfig里面的代码进行设置下。

    public static classWebApiConfig
{
public static voidRegister(HttpConfiguration config)
{
//Web API 配置和服务 config.SetCorsPolicyProviderFactory(newCorsPolicyFactory());
config.EnableCors();
//Web API 路由 config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name:
"DefaultApi",
routeTemplate:
"api/{controller}/{action}/{id}",
defaults:
new { action = "post", id =RouteParameter.Optional }
);
//Remove the JSON formatter//config.Formatters.Remove(config.Formatters.JsonFormatter);//Remove the XML formatter config.Formatters.Remove(config.Formatters.XmlFormatter);
}
}

5、Web API 接口的测试

接下来我们要做的就是需要增加业务接口,以便进行具体的测试了,建议使用Winform项目,对每个接口进行一个测试,或者也可以考虑使用单元测试的方式,看个人喜好吧。

例如我们如果要测试用户登陆的接口的话,我们的测试代码如下所示。

        /// <summary>
        ///生成签名字符串/// </summary>
        /// <param name="appSecret">接入秘钥</param>
        /// <param name="timestamp">时间戳</param>
        /// <param name="nonce">随机数</param>
        private string SignatureString(string appSecret, string timestamp, stringnonce)
{
string[] ArrTmp ={ appSecret, timestamp, nonce };

Array.Sort(ArrTmp);
string tmpStr = string.Join("", ArrTmp);

tmpStr
= FormsAuthentication.HashPasswordForStoringInConfigFile(tmpStr, "SHA1");returntmpStr.ToLower();
}
privateTokenResult GetTokenResult()
{
string timestamp =DateTime.Now.DateTimeToInt().ToString();string nonce = newRandom().NextDouble().ToString();string signature =SignatureString(appSecret, timestamp, nonce);string appended = string.Format("&signature={0}&timestamp={1}&nonce={2}&appid={3}", signature, timestamp, nonce, appId);string queryUrl = url + "Auth/GetAccessToken?username=test&password=123456" +appended;

HttpHelper helper
= newHttpHelper();string token =helper.GetHtml(queryUrl);
Console.WriteLine(token);
TokenResult tokenResult
= JsonConvert.DeserializeObject<TokenResult>(token);returntokenResult;
}

如果我们已经获得了令牌,我们根据令牌传递参数
给连接,并获取其他数据的测试处理代码如下所示。

            //获取访问令牌
            TokenResult tokenResult =GetTokenResult();string queryUrl = url + "/Contact/get?token=" +tokenResult.access_token;
HttpHelper helper
= newHttpHelper();string result =helper.GetHtml(queryUrl);
Console.WriteLine(result);

如果需要POST数据的话,那么调用代码如下所示。

            //使用POST方式
            var data = new{
name
= "张三",
certno
= "123456789",
};
var postData =data.ToJson();

queryUrl
= url + "/Contact/Add?token=" +tokenResult.access_token;
helper
= newHttpHelper();
helper.ContentType
= "application/json";
result
= helper.GetHtml(queryUrl, postData, true);
Console.WriteLine(result);

Web API后台,会自动把POST的JSON数据转换为对应的对象的。

如果是GET方式,我们可能可以直接通过浏览器进行调试,如果是POST方式,我们需要使用一些协助工具,如Fiddler等处理工具,但是最好的方式是自己根据需要弄一个测试工具,方便测试。

以下就是我为了自己Web API 接口开发的需要,专门弄的一个调试工具,可以自动组装相关的参数,包括使用安全签名的参数,还可以把所有参数数据进行存储。

最近一直很多事情,博客停下来好久没写了,整理下思路,把最近研究的基于Metronic的Bootstrap开发框架进行经验的总结出来和大家分享下,同时也记录自己对Bootstrap开发的学习研究的点点滴滴,希望在开一个《
基于MVC4+EasyUI的Web开发框架经验总结
》的系列文章,逐步介绍这个响应式框架的点点滴滴。

Bootstrap是一个前端的技术框架,很多平台都可以采用,JAVA/PHP/.NET都可以用来做前端界面,整合JQuery可以实现非常丰富的界面效果,目前也有很多Bootstrap的插件能够提供给大家使用,但是在国内很多基于Bootstrap的介绍很多还是停留在教学的基础上,介绍Bootstrap的各种基础知识和简单的使用;本文希望以基于C#的MVC实际项目的基础上,对Bootstrap开发框架进行全面的案例介绍,以实际项目的代码和效果截图进行讲解,力求详尽、直观为大家介绍这方面的经验和心得。

1、基于Metronic的Bootstrap开发框架总览

Metronic是一个国外的基于HTML、JS等技术的Bootstrap开发框架整合,整合了很多Bootstrap的前端技术和插件的使用,是一个非常不错的技术框架。本文以这个为基础,结合我对MVC的Web框架的研究,整合了基于MVC的Bootstrap开发框架,使之能够符合实际项目的结构需要的实际项目。

以下是我整体性项目的总的效果图。

启动菜单区的内容,动态从数据库里面获取,系统顶栏放置一些信息展示,以及提供用户对个人数据快速处理,如查看个人信息、注销、锁屏等操作,内容区域则主要是可视化展示的数据,可以通过树列表控件、表格控件进行展示,一般数据还有增删改查、以及分页的需要,因此需要整合各种功能的处理。另外,用户的数据,除了查询展示外,还需要有导入、导出等相关操作,这些是常规性的数据处理功能。确定好这些规则和界面效果后,我们可以通过代码生成工具进行生成,快速生成这些Web项目的界面效果了。

2、Bootstrap开发框架菜单展示

整个框架涉及了很多内容,包括常规Bootstrap的各种CSS特性的使用,以及菜单栏、Bootstrap图标管理、系统顶栏、树形控件JSTree、Portlet容器、
Modal对话框、Tab控件、、
下拉列表Select2、复选框iCheck、多文本编辑控件summernote、文件及图片上传展示fileinput、提示控件bootstrap-toastr和sweetalert、数值调整控件touchspin、视频播放展示控件video-player等等,这些特性在整体性的解决方案里面都有设计,集合这些优秀的插件,能够为我们的框架提供更强大的功能和丰富的界面体验。

本节继续回到框架的开始部分,菜单

处理

展示。一般为了管理方便,菜单分为三级,选中的菜单和别的菜单样式有所区分,菜单可以折叠最小化,效果如下所示。


在Bootstrap,构建菜单是一个相对比较容易的事情,主要是利用了UL和LI,通过样式的处理,就可以实现了菜单的布局设置了,代码如下所示。

                <ulclass="page-sidebar-menu page-sidebar-menu-hover-submenu "data-keep-expanded="false"data-auto-scroll="true"data-slide-speed="200">
                    <liclass="start"id="1">
                        <ahref="/Home/index">
                            <iclass="icon-home"></i>
                            <spanclass="title">首页</span>
                            <spanclass="selected"></span>
                            <spanclass="arrow open"></span>
                        </a>
                    </li>
                    <liid="2">
                        <ahref="javascript:;">
                            <iclass="icon-basket"></i>
                            <spanclass="title">行业动态</span>
                            <spanclass="selected"></span>
                            <spanclass="arrow open"></span>
                        </a>
                        <ulclass="sub-menu">
                            <liclass="heading"style="font-size:14px;color:yellow">
                                <iclass="icon-home"></i>行业动态</li>
                            <li>
                                <ahref="#">
                                    <iclass="icon-home"></i>
                                    <spanclass="badge badge-danger">4</span>政策法规</a>
                            </li>
                            <li>
                                <ahref="#">
                                    <iclass="icon-basket"></i>
                                    <spanclass="badge badge-warning">4</span>通知公告</a>
                            </li>
                            <li>
                                <ahref="#">
                                    <iclass="icon-tag"></i>
                                    <spanclass="badge badge-success">4</span>动态信息</a>
                            </li>
                        </ul>
                    </li>
                </ul>

但是,我们一般的菜单是动态变化的,也就是需要从数据库里面获取,并设置到前端显示,这样我们就需要在MVC的控制器里面,输出菜单的内容,然后绑定到前端的界面上,实现菜单数据的动态化,同时这也是权限控制的基础处理。

在基类里面,我们可以在用户登陆后,获取菜单的数据放到ViewBag对象里面。

具体代码如下所示,先判断用户是否登陆,如果登陆,则获取用户的菜单数据,存在ViewBag里面待用。

        /// <summary>
        ///重写基类在Action执行之前的处理/// </summary>
        /// <param name="filterContext">重写方法的参数</param>
        protected override voidOnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);//得到用户登录的信息 CurrentUser = Session["UserInfo"] asUserInfo;if (CurrentUser == null)
{
Response.Redirect(
"/Login/Index");//如果用户为空跳转到登录界面 }else{//设置授权属性,然后赋值给ViewBag保存 ConvertAuthorizedInfo();
ViewBag.AuthorizeKey
=AuthorizeKey;//登录信息统一设置 ViewBag.FullName =CurrentUser.FullName;
ViewBag.Name
=CurrentUser.Name;

ViewBag.MenuString
=GetMenuString();//ViewBag.MenuString = GetMenuStringCache();//使用缓存,隔一段时间更新 }
}

其中,GetMenuString函数就是对菜单的组装处理。数据库中菜单的信息是一个树形结构,如下所示。

我们可以根据数据库的菜单信息,构建一部分界面用到的HTML代码。

            #region 定义的格式模板
            //javascript:;//{0}?tid={1}
            var firstTemplate = @"<li id='{3}'>
<a href='{0}'>
<i class='{1}'></i>
<span class='title'>{2}</span>
<span class='selected'></span>
<span class='arrow open'></span>
</a>
";var secondTemplate = @"<li class='heading' style='font-size:14px;color:yellow'>
<i class='{0}'></i>
{1}
</li>
";var thirdTemplate = @"<li id='{3}'>
<a href='{0}'>
<i class='{1}'></i>
{2}
</a>
</li>
";var firstTemplateEnd = "</li>";var secondTemplateStart = "<ul class='sub-menu'>";var secondTemplateEnd = "</ul>";#endregion

例如三级菜单就可以通过代码进行生成。

                        //三级
                        icon =subNodeInfo.WebIcon;//tid 为顶级分类id,sid 为第三级菜单id
                        tmpUrl = string.Format("{0}{1}tid={2}&sid={3}", subNodeInfo.Url, GetUrlJoiner(subNodeInfo.Url), info.ID, subNodeInfo.ID);
url
= (!string.IsNullOrEmpty(subNodeInfo.Url) && subNodeInfo.Url.Trim() != "#") ? tmpUrl : "javascript:;";
sb
= sb.AppendFormat(thirdTemplate, url, icon, subNodeInfo.Name, subNodeInfo.ID);

当然,如果想提高并发量,可以减少菜单的频繁检索,把这部分数据放到MemeryCache里面,如下处理即可。

        public stringGetMenuStringCache()
{
string itemValue = MemoryCacheHelper.GetCacheItem<string>("GetMenuStringCache", delegate()
{
returnGetMenuString();
},
null, DateTime.Now.AddMinutes(5) //5分钟以后过期,重新获取 );returnitemValue;
}

3、布局页面的使用

同时,我们为了提高页面的重用,一般情况下,是把每个页面相同部分的内容抽离出来,放到总的布局页面上,这样处理内容区域外,其他部分全部是继承自布局视图页面的内容了,我们的动态菜单部分,也是在布局视图里面的一部分内容。

上图的_Layout.cshtml就是基于C#的MVC总的布局视图页面。这样,我们在这个页面里面,设定了菜单的展示内容,以及留出主页面内容的部分,以及脚本的部分展示,就可以了。

菜单的展示代码如下所示:

布局页面留出的页面展示Section如下所示。

由于Bootstrap一般把JS文件放到最后加载,因此我们在布局页面保留部分必备的Jquery等脚本外,还需要把脚本部分内容放到页面底部进行加载,并且我们脚本加载可以利用MVC的Bundles技术进行压缩整合处理。关于这方面技术,可以参考我之前的文章介绍《
基于MVC4+EasyUI的Web开发框架经验总结(11)--使用Bundles处理简化页面代码
》。

这样,我们在各个子页面的视图里面,引入布局视图页面后,只需要编写个性化展示内容的部分即可,具体代码如下所示。

然后在页面底部,包含所需部分的脚本代码即可,这样在页面生成后,就会依据布局页面设置好的顺序块,进行合理的展示,并且是把所有部分内容进行整合了。

4、页面编辑工具Sublime Text的使用

我前面截图,很多是VS环境里面的,不过一般我们编辑视图页面的时候,都是采用Sublime Text这个强大的编辑工具的,丰富的插件、智能语法提示等,会让你用了之后爱不释手,是编辑视图页面非常快速的利器,强烈推荐使用。

VS一般我们用来做文件管理,以及编译等处理就可以了。

如果有兴趣,可以继续参考系列文章:

基于Metronic的Bootstrap开发框架经验总结(1)-框架总览及菜单模块的处理

基于Metronic的Bootstrap开发框架经验总结(2)--列表分页处理和插件JSTree的使用


基于Metronic的Bootstrap开发框架经验总结(3)--下拉列表Select2插件的使用

基于Metronic的Bootstrap开发框架经验总结(4)--Bootstrap图标的提取和利用

基于Metronic的Bootstrap开发框架经验总结(5)--Bootstrap文件上传插件File Input的使用


基于Metronic的Bootstrap开发框架经验总结(6)--对话框及提示框的处理和优化

基于Metronic的Bootstrap开发框架经验总结(7)--数据的导入、导出及附件的查看处理


基于Metronic的Bootstrap开发框架经验总结(8)--框架功能总体界面介绍


基于Metronic的Bootstrap开发框架经验总结(9)--实现Web页面内容的打印预览和保存操作