在Global.asax中获取Session的注意事项

几年前给朋友珠宝公司开发过一套旺财珠宝库存管理系统,用得还是web Form老技术,但是更多的走Ashx+Ajax,但前端可是HTML5+jQuery+BootStrap等新技术,所以不论功能还是用户体验,都能很完美的满足用户要求(用户才不管你用的是什么技术,先进的和古老的都必须解决他的问题,然后还需要好用)。近期特别反馈说有些页面比较慢,我觉得用了几年了,数据库就近2个G了,可能是数据库查询的问题,也可能是程序执行的问题,也可能用户网络问题。数据库可以在服务器上用Sql Server Profiler进行查询分析,但页面上还得做点跟踪。于是就用Global.asax来实现,本来很方便的,但为了获取当前登录用户,需要在Global.asax中获取Session,花了点时间才搞定,记录下来分享一下。

本来想在Application_BeginRequest或者Session_Start里面获取的,可怎么也获取不到,于是翻看MSDN了解Global.asax的事件及执行顺序,在Application_AcquireRequestState中才获取到。

    protected DateTime StartDateTime;

    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        //开始执行时间
        StartDateTime = DateTime.Now;
    }
    protected BaseUserInfo CurrentUserInfo;
    protected void Application_AcquireRequestState(object sender, EventArgs e)
    {
        if (Utilities.UserIsLogOn())
        {
            CurrentUserInfo = Utilities.GetUserInfo();
        }
    }

    protected void Application_EndRequest(object sender, EventArgs e)
    {
        DateTime endDateTime = DateTime.Now;
        TimeSpan ts = endDateTime - StartDateTime;
        //5秒以上的慢页面进行记录
        if (ts.TotalMilliseconds >= 5000)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("时间:" + endDateTime.ToString("yyyy-MM-dd hh:mm:ss fff") + ",当前请求URL:" + HttpContext.Current.Request.Url + ",请求的参数为:" + HttpContext.Current.Request.QueryString + ",页面加载的时间:" + ts.TotalMilliseconds + " 毫秒");
            if (CurrentUserInfo != null)
            {
                sb.Append(",用户:" + CurrentUserInfo.UserName);
            }
            FileUtil.WriteMessage(sb.ToString(), BaseSystemInfo.StartupPath + "//Log//Slow/" + DateTime.Now.ToString(BaseSystemInfo.DateFormat) + ".txt");
        }

    }

通过上述代码就可方便的获取哪些页面比较慢,何时、何人、何参数、何地(IP)发生的。

2018-05-11 03:33:18 947:[当前请求URL:Modules/WMS/ItemMaster/ItemMasterPlan.aspx;请求的参数为:;页面加载的时间:8151.3672 毫秒]
2018-05-11 04:09:25 181:[当前请求URL:Modules/WholesaleWMS/tools/WholesaleBPStatement.ashx?action=RefreshStatement;请求的参数为:action=RefreshStatement;页面加载的时间:19720.7031 毫秒]
2018-05-11 05:18:10 486:[当前请求URL:Modules/WMS/OutboundOrderLine/OutboundOrderLineListSummary.aspx;请求的参数为:;页面加载的时间:16742.1875 毫秒]

2018-05-12 10:33:59 305:[当前请求URL:Modules/WMS/PurchaseDemand/PurchaseDemandAdmin.aspx;请求的参数为:;页面加载的时间:9375.9765 毫秒]
2018-05-12 10:49:19 497:[当前请求URL:Modules/WMS/ItemMaster/ItemMasterPlan.aspx;请求的参数为:;页面加载的时间:5278.3203 毫秒]
2018-05-12 01:24:36 673:[当前请求URL:Modules/WMS/InboundOrderLine/InboundOrderLineListReport.aspx;请求的参数为:;页面加载的时间:11416.0156 毫秒]
2018-05-12 01:24:42 045:[当前请求URL:Modules/WMS/InboundOrderLine/InboundOrderLineListReport.aspx;请求的参数为:;页面加载的时间:6209.9609 毫秒]
2018-05-12 01:24:42 611:[当前请求URL:Modules/WMS/InboundOrderLine/InboundOrderLineListReport.aspx;请求的参数为:;页面加载的时间:10142.5781 毫秒]
2018-05-12 04:39:35 251:[当前请求URL:Modules/WMS/OutboundOrderLine/OutboundOrderLineListSummary.aspx;请求的参数为:;页面加载的时间:16623.0469 毫秒]
2018-05-12 04:51:31 401:[当前请求URL:Modules/WMS/OutboundOrderLine/OutboundOrderLineListSummary.aspx;请求的参数为:;页面加载的时间:16648.4375 毫秒]
2018-05-12 04:57:15 362:[当前请求URL:Modules/WMS/OutboundOrderLine/OutboundOrderLineListSummary.aspx;请求的参数为:;页面加载的时间:16552.7343 毫秒]

最后附上MSDN上对Global.asax的解释:

按执行顺序来解释一下Global.asax.cs中相应的事件处理方法的含义

  1. Application_BeginRequest:BeginRequest是在收到Request时第一个触发的事件,这个方法自然就是第一个执行的了。
  2. Application_AuthenticateRequest:当安全模块已经建立了当前用户的标识后执行。
  3. Application_AuthorizeRequest:当安全模块已经验证了当前用户的授权时执行。
  4. Application_ResolveRequestCache:当ASP.NET完成授权事件以使缓存模块从缓存中为请求提供服务时发生,从而跳过处理程序(页面或者是WebService)的执行。这样做可以改善网站的性能,这个事件还可以用来判断正文是不是从Cache中得到的。
  5. Application_AcquireRequestState:当ASP.NET获取当前请求所关联的当前状态(如Session)时执行(真是拗口啊,msdn上就这样写的,我自己想不出什么好句子了)。
  6. Application_PreRequestHandlerExecute:当ASP.Net即将把请求发送到处理程序对象(页面或者是WebService)之前执行。这个时候,Session就可以用了。
  7. Application_PostRequestHandlerExecute:当处理程序对象(页面或者是WebService)工作完成之后执行。
  8. Application_ReleaseRequestState:在ASP.NET执行完所有请求处理程序后执行。ReleaseRequestState事件将使当前状态数据被保存。
  9. Application_UpdateRequestCache:在ASP.NET执行完处理程序后,为了后续的请求而更新响应缓存时执行。
  10. Application_EndRequest:同上,EndRequest是在响应Request时最后一个触发的事件,这个方法自然就是最后一个执行的了。

再附上两个无顺序的,随时都可能执行的

  1. Application_PreSendRequestHeaders:向客户端发送Http标头之前执行。
  2. Application_PreSendRequestContent:向客户端发送Http正文之前执行。

Loading

还是连接池的问题,终于搞定了

上个月中旬提到过被Web.config中数据库连接池 Max Pool Size的问题折腾了,但是增加到200个最大连接池,还是会报错:

System.InvalidOperationException: Timeout expired.  The timeout period elapsed prior to obtaining a connection from the pool.   This may have occurred because all pooled connections were in use and max pool size was reached.

本打算借助微软的免费工具Debug Diagnostic Tool v2 Update 2,搞了半天不太会用。为了省事,借助RedGate的免费14天试用的ANTS Memory Profiler free trial,终于找到问题所在。原来是连接泄露了,在connection连接后未及时使用dispose()或close()进行关闭。

在升级改进吉日嘎拉DotNet数据访问层DotNet.Utilities时,本来继承了IDisposable接口,后改为IDbHelper,但并未启用手动关闭连接。原来的IDisposable接口的主要用途是释放非托管资源。当不再使用托管对象时,垃圾回收器会自动释放分配给该对象的内存。

至此,数据库连接池的报错终于完全修复。

Loading

困惑了2年多的C#问题,终于解决了

翻了一下QQ聊天记录,其实这个问题也是困扰吉日嘎拉的问题,2015年我曾经就此问题跟他交流过。

在更新语句中和条件中有相同的参数问题,造成报错:

 The variable name '%.*ls' has already been declared. Variable names must be unique within a query batch or stored procedure.  

曾经我是先判断条件语句,获取到主键ID,然后再根据主键ID为条件进行更新,多了一次数据库读取,折中处理了好几年。

这次再写类似的程序,实在觉得这么搞太费劲,索性花了几个小时,最终的思路就是即便是同名的字段,条件语句的参数自动改名:增加后缀或前缀。

这么一改,条件的参数就自动增加了后缀Where,就跟更新字段的参数不重名了。当然了你也可以自定义自己的后缀或者前缀。

最终时隔2年多,将此更改跟吉日嘎拉再次沟通,也解决了他的困惑,皆大欢喜。

有在用吉日嘎拉底层DotNet.Common数据读写层的朋友,请拿去不谢。

Loading

C#中启用托管的Oracle.ManagedDataAccess访问Oracle数据库

上个月有个项目从基于Oracle数据库的Infor ERP LN系统中多表关联查询记录,遇到一张600多万的记录表,造成本来MSSQL的Linked Server方式好用的SQL频频超时。难道是服务器与服务器之间的网络有问题?还是MSSQL 2008 R2的问题?也朝这个方向研究了一阵,后来想想算了,还是直接连Oracle吧。

原来偷懒的技术债务,不还不行了。

于是启用托管方式的Oracle.ManagedDataAccess.dll,并从老版本4.121.2.20150926的dll更新到了4.122.1.20170524,升级了原来吉日嘎拉的数据访问底层,web.config等,半天搞定老sql的替换,测试跑下来速度飞快了。

Loading

C#开发中Windows域认证登录2016(扩展吉日嘎拉GPM系统V4.2)

2013年搞公司的OA时,为了统一用户登录,将Windows AD的用户和OA的账号对接,OA用户名的规则就是使用Windows AD的用户名,格式举例:Troy.Cui,原理就是先进行域服务器的认证,认证完毕使用自定义的函数根据用户名读取出OA的用户信息,然后读取出用户名和密码信息,最后使用获取到的用户名和密码进行正常的登录。当时的文章:《C#开发中Windows域认证登录2(扩展吉日嘎拉GPM系统)》,本周打算OA中开发在线培训的功能,也想借此机会升级一下底层的应用DotNet.Business和DotNet.Utilities,第一个要升级的项目就是这个Windows域账号登录。

升级涉及到的文件如下:

DotNet.Business\DataAccess\Manager.User\BaseUserManager.LogOn.LDAP.cs (新增)

DotNet.Business\Service\LogOnService.LDAP.cs

DotNet.Business\WebUtilities\Utilities.LogOn.LDAP.cs

DotNet.IService\ILogOnService.LDAP.cs – 这个要将ILogOnService.cs接口写成partical (新增)

相关的核心代码如下:

#region public BaseUserInfo LogOnByUserName(string taskId, BaseUserInfo userInfo, string userName)
        /// <summary>
        /// 按用户名登录(LDAP专用)
        /// </summary>
        /// <param name="userInfo">用户</param>
        /// <param name="userName">用户名</param>
        /// <param name="statusCode">返回状态码</param>
        /// <param name="statusMessage">返回状消息</param>
        /// <returns>用户实体</returns>
        public UserLogOnResult LogOnByUserName(string taskId, BaseUserInfo userInfo, string userName)
        {
            UserLogOnResult result = new UserLogOnResult();
            var parameter = ServiceInfo.Create(taskId, userInfo, MethodBase.GetCurrentMethod());
            ServiceUtil.ProcessUserCenterWriteDb(userInfo,parameter, (dbHelper) =>
            {
                // 先侦测是否在线
                //userLogOnManager.CheckOnLine();
                // 然后获取用户密码
                var userManager = new BaseUserManager(userInfo);
                // 是否从角色判断管理员
                userManager.CheckIsAdministrator = true;
                //根据用户名获取用户信息
                BaseUserEntity userEntity = userManager.GetByUserName(userName);
                
                if (userEntity != null)
                {
                    var baseUserLogOnManager = new BaseUserLogOnManager(userInfo);
                    //获取密码
                    BaseUserLogOnEntity userLogOnEntity = baseUserLogOnManager.GetObject(userEntity.Id);
                    //string password = userLogOnEntity.UserPassword;
                    string openId = userLogOnEntity.OpenId;
                    //再进行登录
                    //result = userManager.LogOnByUserName(userName, password, null, false, "Base");
                    result = userManager.LogOnByOpenId(openId, string.Empty, string.Empty);
                }
                // 登录时会自动记录进行日志记录,所以不需要进行重复日志记录
                // BaseLogManager.Instance.Add(userInfo, this.serviceName, MethodBase.GetCurrentMethod());
            });
            return result;
        }
        #endregion

大家可以看出来BaseUserEntity userEntity = userManager.GetByUserName(userName);这个函数非常重要、但不能随便就调用,安全起见,仅限于集成登录时使用。

前台页面登录部分,其实您可以将自己公司的域信息写到Web.Config或者配置为BaseSystemInfo下的参数

    /// <summary>
    /// LDAP用户的登录操作
    /// </summary>
    private void LDAPUserLogOn(string userName, string password)
    {
        string lDAP = "LDAP://DC=CORP,DC=yourdomain,DC=com";
        string domain = "corpwaiglobal";
        string checkInput = string.Empty;
        try
        {
            string statusCode = string.Empty;
            string statusMessage = string.Empty;
            // 有什么权限的人才可以登录到系统
            string permissionCode = string.Empty;
            // permissionItemCode = "Project.Admin.Access";
            // 登录验证
            string openId = Utilities.GetOpenId();
            BaseUserInfo userInfo = Utilities.LogOnByLDAP(domain, lDAP, userName, password, openId, permissionCode, this.chkPersistCookie.Checked, false, out statusCode, out statusMessage);
            // txtVerifyCode.Text = string.Empty;
            // 登录结果
            if (userInfo!=null)
            {
                this.AfterLogOn(userInfo);
                // 登录成功,重新定向到跳转的页面
                // Page.Response.Redirect(this.ReturnURL);
                // 若是单点登录,还需要把OpenId传递过去,这样在其他子网站里可以获取到OpenId,而不是用户名密码了,可以进行加密登录了
                if (!string.IsNullOrEmpty(this.ReturnURL) && !string.IsNullOrEmpty(userInfo.OpenId))
                {
                    if (this.ReturnURL.IndexOf('?') > 0)
                    {
                        this.ReturnURL = this.ReturnURL + "&OpenId=" + userInfo.OpenId;
                    }
                    else
                    {
                        this.ReturnURL = this.ReturnURL + "?OpenId=" + userInfo.OpenId;
                    }
                }
                Response.Redirect(this.ReturnURL, false);
            }
            else
            {
                //checkInput = "<script>alert('提示信息:" + statusMessage + "');</script>";
                checkInput = "<script>alert('提示信息:请检查你的用户名和密码。');</script>";
                Page.ClientScript.RegisterStartupScript(this.GetType(), "message", checkInput);
                this.txtUserName.Focus();
            }
        }
        catch (System.Exception exception)
        {
            Page.Response.Write(exception.Message);
            checkInput = "<script>alert('提示信息:登录失败,请重试。');</script>";
            Page.ClientScript.RegisterStartupScript(this.GetType(), "message", checkInput);
            this.txtUserName.Focus();
        }
    }

Loading

快速开发必备:代码生成器

开发程序那么久了,真正使用代码生成器是近3年的事情,由衷的感觉到这东西真的是提高生产力!

最早的时候开发,还是用asp,那时候就是傻傻的弄好一个页面(代码和页面是混合的),复制成另一个文件,然后用DreamWeaver替换关键词(表名、描述啥的)。后来知道了动软代码生成器,那时候主要还是用asp,也捣鼓了一下生成asp代码,不过发现.net的强大后,就开始学习简单三层写东西了。说实话,动软的那个直接生成项目的功能的确没啥实际作用,最实用的就是生成三层的dal,bll,model,页面部分还得自己定制。

再后来就是学习吉日嘎拉的.Net开发平台,开始用他的代码生成器,买了源码之后用在公司OA项目上之后,就开始定制自己的代码生成器,基本上来一个需求,三下五除二就搞定了核心。

接着研究DTcms,索性写了基于动软的DTcms代码生成模板,后来淘宝上还卖出了好几份!让我看到了想偷懒、想追求效率的程序员挺多。

其实吧,VS就是一个生成器,用MVC模式或者EF的时候,很多代码都能自动生成,异常强大,但是很多时候,我们写代码还是要结合业务,还得服务客户,就得有界面!所以功能部分是后台的话,界面的部分就需要精心打造,但是界面部门的代码生成是否可行呢?我觉得肯定可行啦!

目前我正在基于吉日嘎拉的代码生成器源码,写一个自己的代码生成器,不仅生成后台代码,前台的页面,漂亮的界面,相应的ashx、webapi也一起生成了。核心的功能如下:

1、根据数据库结构生成代码,支持MSSQL,MySQL,Oracle

2、自动生成实体类

3、自动生成增删改查业务类(没有DAL了,直接用吉日嘎拉的DotNet.Business)

4、自动生成列表、查看、增加、编辑页面

5、界面基于BootStrap(暂定ZUI)

6、自动生成ashx文件,并自动生成权限控制代码

7、自动生成权限配置的SQL,便于后台管理权限

8、整站基于jQuery+Ajax(layer定制)

 

高效自动代码生成的一瞬间很美好,但要写出背后的模板(规范、可用的),可能需要几天或几十天的不停打磨,完善。感兴趣的朋友,加我微信或QQ交流。

Loading

如何使用Navicat将psc备份导入到MySQL

吉日嘎拉的DotNet.CommonV4.2程序增加了DotNet.MVC,但是目前的项目用的是MySQL数据库,而SVN上只有psc文件,而不是sql文件,所以只好Bing搜索一下如何恢复这个数据库,找了半天,不过好在又学会了用一个管理MySQL的客户端,这里记录下来过程,以备不时之需。

第一步:安装MySQL数据库到本机,我用Window 7操作系统,安装32位或64位MySQL都行。默认安装即可。

第二步:安装Navicat for MySQL,并连接到本机,创建数据库UserCenterV42

第三步:将SVN下载下来的psc后缀的备份文件复制到Navicat的临时工作目录(一般在 c:\用户目录\Documents\Navicat\MySQL\servers\下),我的是:C:\Users\troy.cui\Documents\Navicat\MySQL\servers\local\UserCenterV42。注意其中 local 是我在Navicat中创建的连接名,UserCenterV42为数据库名,一定要放在对应数据库名下)

第四步:在Navicat中打开数据库UserCenterV42,在备份列表中,就会看到刚才添加的备份,右键点击“还原备份”即可。

 

 

我的第2个psc备份恢复(如果单独恢复几个表,可以选择表名)

 

Navicat 介绍:

强大的数据库管理和设计工具,支持 Win、Mac 和 linux。直观的 GUI 让用户简单地管理 MySQL、MariaDB、SQL Server、SQLite、Oracle 和 PostgreSQL 的数据库。中文版可以14天的免费试用。中文官网:www.navicat.com.cn

 

Loading

吉日嘎拉DotNet.BusinessV4.2中的一处bug,及我的修复和扩展

bug所在位置:DotNet.Business\Utilities\BaseManager.GetDataTableByPage.cs的函数

public virtual DataTable GetDataTableByPage(out int recordCount, int pageIndex = 0, int pageSize = 20, string sortExpression = null, string sortDire = null, string tableName = null, string conditional = null, IDbDataParameter[] dbParameters = null, string selectField = null)中。当使用自己定义的查询语句作为tableName传递进来的时候,按照逻辑没有使用存储过程进行分页,但是很明显那个传递的conditional和dbParameters都被用来统计了总记录数,但是以下调用语句并没有传递conditional。

return DbLogic.GetDataTableByPage(DbHelper, recordCount, pageIndex, pageSize, tableName, dbParameters, sortExpression, sortDire);

于是我扩展了那个DotNet.Business\Utilities\Extend\DbLogic.GetDataTableByPage.Extend.cs,增加了函数

public static DataTable GetDataTableByPage(IDbHelper dbHelper, int recordCount, int pageIndex, int pageSize, string sqlQuery, string conditional, IDbDataParameter[] dbParameters, string sortExpression = null, string sortDire = null)

这里的逻辑是,多表查询构造一个viewTable,然后将where查询直接传递到viewTable里面,而不受分页的影响。上个月扩展了此函数,今天正式升级服务器程序的时候,还是出现了问题,于是有了以上函数的完善版本。上个月是将where条件放在了最外面,造成ROW_NUMBER范围内的所有记录都没有指定where条件的记录,造成用户看记录的时候明明有,显示不出来。当然,这个扩展的函数仅仅是扩展了MSSQL的数据库,有类似使用的朋友可以参考。

Loading

吉日嘎拉C#快速开发平台V4.0到V4.2升级记

目前我用的版本是4.0的,也有近2年没更新了,狠了狠心升级一下,没想到真的行动起来,也没那么难!

用了3天时间,将吉日嘎拉的代码升级到了4.2版本,并让原来的DotNet.WebApplication正常运行起来,比料想的顺利。这里简单记录一下升级中的心得。

使用到的工具:

1、BeyondCompare 试用版 – 比较程序文件

2、SQLDelta 14天试用版 – 比较数据库表结构变化(及数据变化)

3、VS2010 – 保证升级后WebApplication好用

4、MSSQL 2008 R2 – 标配数据库

最新代码的亮点:

1、分离出了DotNet.Model

2、分离出来DotNet.IService

3、DotNet.Business新增Redis缓存

4、DotNet.Utilities新增众多BaseSystemInfo参数和底层函数:数据库读写分离等

5、新增DotNet.UserCenter,用于其它程序如WebApp、安卓、苹果端调用

6、用户登录日志表完善、强大

7、数据库访问增加跟踪及底层文本日志

8、增加DotNet.MVC项目,BS端的用户及权限管理(还未研究)

相关截图:

1、数据库UserCenter更新

2、项目及解决方案截图

注意事项:

1、SqlDelta生成部署的代码后,还需要手动更新老记录中一些字段的值

UPDATE [UserCenterV40].[dbo].[BaseUser]
SET IsAdministrator=1,UserName='Administrator',NickName='Administrator'
WHERE UserName='Admin'
UPDATE BaseModule SET AuthorizedDays=0
UPDATE BaseUserLogOn SET OpenIdTimeout = GETDATE() 
UPDATE BaseUserContact SET MobileVerificationDate = GETDATE()

2、DotNet.WebApplication中有很多登录及读取权限的函数需要更新BaseSystemInfo.SystemCode

本文是升级记录的第1篇,后继会继续记录研究DotNet.MVC项目后的心得,敬请期待。

后记:请大家不要问我要源码,如需购买请直接联系吉日嘎拉,他的博客园的主页地址:http://www.cnblogs.com/jirigala/

Loading