窥探ASP.Net MVC底层原理 实现跨越Session的分布式TempData
1、问题的引出
我相信大家在项目中都使用过TempData,TempData是一个字典集合,一般用于两个请求之间临时缓存数据或者页面之间传递消息。也都知道TempData是用Session来实现的,既然是用Session来实现的,那么模式就是线程模式,这样的Session是没法用到分布式系统中的,那么在多台机器上部署,怎么做到Session在多台机器中共存,这就涉及到分布式存储。那该如何实现TempData的分布式存储?在讲如何实现时,先给大家说说ASP.Net MVC 的管道机制,本人能力有限,说的不对的地方,还请大家能指出来,共同进步。
2、预备知识
2.1、MVC处理的流程讲解
网上有很多讲解ASP.Net 的管道机制的,都讲解的很好,大家可以找找看,今天我来点不一样的,通过Reflector,Debug进源码一步一步调试给大家看,下面开始吧:
1)俗话说的好,工欲善其事必先利其器,下面我们在VS2012上装Reflector
选择"扩展和更新",在弹出来的对话框中安装我们的利器
安装完成之后会在VS上面出现如下的菜单:
点击该菜单,选择下面的选项:
在弹出来的对话中勾选所有以system.web开头的dll,生成PDB文件,因为只有生成它,我们才能调试源码,如下图的勾选情况:
OK,装好之后我们就开始探索的旅程了~~~~~~~~
2)窥探ASP.Net MVC请求处理流程
Part 1
这里先附上一张一次请求 http://localhost:42132/Home/Index/1 处理响应的整体流程图:
看不明白的不要着急,下面会通过调试的方式详细介绍请求处理响应的流程,动动你的小手,下面开始划重点了~~
我们上网时,在浏览器地址输入网址:Http://www.cnblogs.com,按下回车,一张网页就呈现在我们眼前。这究竟发生了什么?对于一名优秀的Programmer来说,我想有必要一下熟悉浏览器--->服务器请求的过程。
1)ASP.Net
ASP.NET是运行在公共语言运行时刻时(CLR)上的应用程序框架。他用来在服务器端构建功能强大的web应用程序。当浏览器请求 ASP.NET 文件时,IIS 会把该请求传递给服务器上的 ASP.NET 引擎,ASP.NET 引擎会逐行地读取该文件,并执行文件中的脚本,最后,ASP.NET 文件会以纯 HTML 的形式返回浏览器。
客户端浏览器和服务器之间的请求响应是通过Socket进行通信,基于HTTP协议,客户端发送一次HTTP请求,服务器接收到请求,处理之后向浏览器回应响应报文。那么什么是HTTP协议呢?
2)Http协议
当浏览器寻找到Web服务器地址后,浏览器将帮助我们把对服务器的请求转换为一系列参数(消息)发给Web服务器,浏览器和Web服务器的对话中,需要使用双方都能理解语法规范进行通信,这种程序之间进行通信的语法规定,我们称之为协议。浏览器与服务器之间的协议是应用层协议,当前遵循的协议是HTTP/1.1。HTTP/1.1协议时Web开发的基础,这是一个无状态协议,客户端浏览器和服务器通过Socket通信进行请求和响应完成一次会话。每次会话中,通信双方发送的数据称为消息,分为两种:请求消息和响应消息。
对于消息而言,一般他有三部分组成,并且消息的头和消息体之间用一个空行进行分隔:
下面用Fiddler我们可以清晰看到浏览器和服务器之间的通信内容:
注意:在请求头和请求体之间是有一空行的,是Http协议规定的。
如果想更加详细的了解Http协议的内容,可以参考下面的两篇文章:
http://www.cnblogs.com/EricaMIN1987_IT/p/3837436.html
http://www.cnblogs.com/wxisme/p/6212797.html
了解了什么是HTTP协议之后,我们在回到先前提出的那个问题,浏览器的请求怎样到达服务器?
3)Http.sys和TCP.sys组件
我们知道要访问一个网站,必须要其部署在相应服务器软件上(如IIS),于IIS相关的内核驱动程序有两个:一个是TCP.sys和Http.sys,所谓的TCP,是用来定义在网络上数据传输方式的协议,它是一个位于OSI七层协议栈的传输层的协议。HTTP协议是一个定义在应用层的协议,它定义了数据交互的谓词数据的格式等,但是传输层上是使用TCP协议进行数据包传送。了解了以上内容有助于理解http.sys和TCP.sys之间的关系:TCP.sys位于Windows通信的最底层,凡是使用TCP协议传输的HTTP协议数据包都会被tcp.sys完成组包后再交给http.sys进行处理。当请求的数据包包含一个HTTP请求时,就会有tcp.sys转给http.sys进行处理,http.sys在内核态上处理完HTTP请求后,IIS就会把HTTP请求对应的HTTP上下文对象转到对应的应用程序进程中,由对应的w3wp.exe进程对请求进行处理。由于IIS本身只能处理静态页面比如html、htm等,对于动态的页面比如cshtml,IIS本身是无法处理的,那么怎样能让IIS能够支持ASP.Net动态也的处理呢?答案就是采用ISAPI。ISAPI可以理解为是IIS的一种扩展插件,当IIS发现某种服务器上的资源自给无法处理时,就会按照配置信息把请求转给对应的ISAPI的扩展来执行;IIS会等待ISAPI的执行结果,然后把结果传给客户的浏览器。
4)IIS服务器扩展
ISAPI(服务器应用编程接口),它为开发人员提供了强大的可编程能力,只要按照标准接口开发不同类型的Web应用程序的ISAPI扩展程序,就能实现对IIS功能上的扩展,从而使IIS可以处理不同类型的客户端请求。IIS管理器提供了应用程序配置功能,可以对不同的客户端请求配置不同的ISAPI扩展程序ISAPI扩展程序通常以DLL形式存在,可以被IIS加载并调用。有了基于ISAPI的扩展扩展程序,IIS服务器就可以根据客户端请求的资源扩展名,来决定应由哪个ISAPI扩展程序来处理客户端请求,然后就可以将请求转发给合适的ISAPI扩展程序。
5)IIS中处理程序映射
Part 2
1)整体把握ASP.Net MVC和ASP.Net WebForm处理流程的差异
ASP.Net是一项动态网页开发技术,在历史发展的长河中WebForm曾一时成为了ASP.Net的代名词,而ASP.Net MVC的出现让这项技术更加唤发朝气。但是,不管是ASP.Net WebForm还是ASP.Net MVC在请求处理机制上大部分都是相同的,只是在请求处理管道上的处理事件做了不同的操作。
2)ASP.Net MVC的管道机制
第一个进入ASP.Net管道的是:PipelineRuntime.ProcessRequestNotification方法
当你在浏览器中输入http://localhost:42132/Home/Index/1按回车之后,请求首先会到达PipelineRuntime.ProcessRequestNotification方法,如下图所示:
注意调用堆栈信息,我们的请求到达ASP.Net管道时,首先会经过PipelineRuntime类中的ProcessRequestNotification方法,至于该方法里面的参数暂时可以忽略,抄起你的小手,划重点了,在该方法内部,又调用了ProcessRequestNotificationHelper方法,下面转到该方法内部,如下图所示:
在该方法内部,调用了InitializeRequestContext方法,主要用来初始化请求上下文,我们接着转到该方的内部,如下图所示:
注意InitializeRequestContext方法内部的这段代码 context = new HttpContext(wr, false); 实例化HttpContext对象,接下来我们看看,在new HttpContext对象的时候都做了些神马:
在该方法内部又调用了Init方法,进行Httprequest和HttpResponse对象进行分装,如下图所示:
好了,实线收回到 ProcessRequestNotificationHelper方法中,在该方法中回执行 HttpRuntime.ProcessRequestNotification(wr, httpContext); ,如下图所示:
在该方中,第一个参数和第二个参数,就是我们上面实例化的对象,转到该方的内部,你会看到不一样的世界,如下图所示:
在该方法的内部又调用了 HttpRuntime.ProcessRequestNotificationPrivate 方法,在该方法的内部try中 EnsureFirstRequestInit 方法,确保网站第一次被访问时,调用了Global文件中了Application_Start方法,不信你看:
全局事件中例如Application_Start方法如何保证只执行一次?在_theApplicationFactory.EnsureAppStartCalled(context);方法中,判断_appOnStartCalled标志,如果是false则调用FireApplicationOnStart方法触发Application_Start方法,然后更改_appOnStartCalled标志。
注意了,重点来了,赶快抄起你的小手,划重点了,通过HttpApplicationFactory.GetApplicationInstance方法来创建APPlication对象,其实这里的application对象就是Global实例对象,有图有真相。我们来详细了解一下HttpApplicationFactory是怎么来创建application对象的,下面我们转到GetApplicationInstance方法内部,如下图所示:
在转到GetNormalApplicationInstance方法内部,窥探一下application对象是如何生成的,如下图所示:
通过查看这段代码,它首先维护着一个HttpApplication池(_freeList,本质上就是一个Stack栈),然后判断可用的HttpApplication实例的数量(_numFreeAppInstances)是否大于0?如果存在可用的,则从池中出栈,然后将可用数量减1。最后,再判断可用的数量是否小于最低限制的数量,如果小于那么则将最低限制的数量设置为目前可用的数量。那么,如果目前HttpApplication池暂时没有可用的实例呢?
代码 state = (HttpApplication) HttpRuntime.CreateNonPublicInstance(this._theApplicationType); 内部通过反射创建了application对象,注意了,重点来了,赶快抄起你的小手,划重点了,在GetNormalApplicationInstance方法,内部application对象(也就是Global对象),调用了InitInternal方法,该方法的功能整体上是这样的:创建系统配置文件和用户配置文件中的HttpModule对象,如下图所示:
在HttpApplication.InitInternal方法的内部,又调用了 this.InitModules(),在该方法中,首先通过读取Web.config配置文件中关于HttpModule的信息,然后将其传递给HttpModule的集合,如下图所示:
那在ASP.NET中已经预定了哪些HttpModule,我们通过 C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config找到web.config文件
然后,又调用了InitModulesCommon方法,遍历上面这个_moduleCollection集合,分别对其每一个HttpModule执行其对应的Init方法。
现在我们把视线收回到 HttpApplication.InitInternal()方法内部,在该方法内部又调用了this._stepManager.BuildSteps(this._resumeStepsWaitCallback); 它完成了19个请求处理管道事件的注册工作。如下图所示:
从上面的代码可知,ApplicationStepManager对象的BuildSteps方法被调用,完成HttpApplication 19个管道事件的注册。这个方法很重要,它将创建各种HttpApplication.IExecutionStep保存到一个数组列表 _execSteps 中:如上图中 steps.CopyTo(this._execSteps)。这样做的目的在于:便于在后面的BeginProcessRequest方法内部调用ResumeSteps方法依次执行这些对象的Execute()方法,完成各个事件的执行。打起精神,抄起你的小手,划重点了,在完成HttpApplication 19个管道事件的注册后,开始依次跑管道事件,在执行每个管道事件的时候,会触发HttpModule中各个事件对应的执行方法,下面列出部分方法被触发执行的情况,如下图所示:
来来来,打起精神,抄起你的小手,重点来了!!!重点来了!!!重点来了!!!重要的事情说三遍!
看见没,URLRoutingModule,它是一个实现了IHttpModule接口,重写了Init方法,在该方法内部,第七个管道事件上没注册了 OnApplicationPostResolveRequestCache方法,如下图所示:
也就是说,我们的ASP.Net MVC 网站已经进入到第七个管道事件 PostResolveRequestCache ,我们的MVC就是通过这种方法来实现的。下面 我们转到该方法内部,看看到底干了些神马,如下图所示:
在说明该方法时,我们先补充一些关于HttpModule和HttpHandler,首先附上一张管道事件的图片,如下图所示:
我们再来理解一下什么是HttpModule和HttpHandler,他们有助我们在ASP.NET页面处理过程的前后注入自定义的代码逻辑的原理。首先他们之间主要的差别在于:
(1)整体把握:
ASP.NET 请求处理过程是基于管道模型的,这个管道模型是由多个HttpModule和HttpHandler组成,ASP.NET 把http请求依次传递给管道中各个HttpModule,最终被HttpHandler处理,处理完成后,再次经过管道中的HTTP模块,把结果返回给客户端。我们可以在每个HttpModule中都可以干预请求的处理过程。
注意:在http请求的处理过程中,只能调用一个HttpHandler,但可以调用多个HttpModule。
当请求到达HttpModule的时候,系统还没有对这个请求真正处理,但是我们可以在这个请求传递到处理中心(HttpHandler)之前附加一些其它信息,或者截获的这个请求并作一些额外的工作,也或者终止请求等。在HttpHandler处理完请求之后,我们可以再在相应的HttpModule中把请求处理的结果进行再次加工返回客户端。
(2)IHttpModule
比如我们的MVC中的URLRoutingModule,就是实现了IHttpModule接口,重写了里面的Init方法。
IHttpModule定义如下:
public interface IHttpModule
{
void Dispose();
void Init(HttpApplication context);
}
Init 方法:系统初始化的时候自动调用,这个方法允许HTTP模块向HttpApplication 对象中的事件注册自己的事件处理程序。URLRoutingModule就是这样实现的
(3)IHandler
HttpHandler是HTTP请求的处理中心,真正地对客户端请求的服务器页面做出编译和执行,并将处理过后的信息附加在HTTP请求信息流中再次返回到HttpModule中。
HttpHandler与HttpModule不同,一旦定义了自己的HttpHandler类,那么它对系统的HttpHandler的关系将是“覆盖”关系。
IHttpHandler接口声明
public interface IHttpHandler
{
bool IsReusable { get; }
public void ProcessRequest(HttpContext context); //请求处理函数
}
(注:该部分参考来源:ivan.yu的.net空间)关于更详细的介绍可以参考这位前辈的文章:http://www.cnblogs.com/yuanyuan/archive/2010/11/15/1877709.html,讲解的非常详细。
后面我会,结合HttpModule和HttpHandler讲解几个实战的例子。
好了,回到URLRoutingModule中Init方法在第七个管道事件上注册的 OnApplicationPostResolveRequestCache方法,我们的MVC在第七个事件主要做的事情是创建一个MVCHandler存入到HttpContext对象的ReMapHandler属性中,但是对于静态文件是不需要经过MVC处理的。下面我们来看看在该方法内部是如何实现的, RouteData routeData = this.RouteCollection.GetRouteData(context);通过该方法获取到封装的路由信息的RouteData实例。也就是当请求到达UrlRoutingModule的时候,UrlRoutingModule会触发注册的事件方法,在该方法内部通过 GetRouteData方法 ,根据URL到路由表里面查找匹配URL规则的路由,若匹配,把请求交给IRouteHandler,即MVCRouteHandler。我们可以看下GetRouteData的源码,如下图所示:
注意了,重点来了,抄起你的小手,开始划重点,在GetRouteData方法红色框中标注的代码,会返回RouteData对象,那我们看看,RouteData对象中到底有些神马,如下图所示:
注意了这里把MVCRouteHandler对象赋值给了RouteHandler了,最终返回,把值赋值给routeData变量。接着我们把视线收回到第七个管道事件注册的方法中,
接着,会判断一下routeData是否为NUll,routeData是不为null的,所以接下来,通过routeData.RouteHandler拿到了MVCRouteHandler对象,重点来啦,赶快抄起小手!!!接下来继续执行,当执行到IHttpHandler HttpHandler=routeHandler.GetHttpHandler(requestContext)时,我们的MVCHandler就诞生了,最后把创建的MVCHandler对象,存入到了RemapHandler不信,如下图所示:
不信,如下图所示:
那我们的MVCHandler创建好了,之后该怎么执行呢?很简单,继续执行下面的管道事件呗,接着到第八个管道事件了,在第八个管道事件,先检查HttpContext里面的remapHandler,发现不为空,直接略过执行后面的是那件,在第十一和管道事件和第十二个管道事件之间调用MVCHandler的BeginProcessRequest方法,不信如下图所示:
在该方法内部,会执行ProcessRequestInit方法,进行处理请求的初始化工作,如下图所示:
看到没,我们的控制器的名字:Home,注意了,重点来啦!!!重点来啦!!!重点来啦!!!重要的事情说三遍!!!
this.ControllerBuilder.GetControllerFactory()方法拿到Controller Factory对象,然后,调用CreateController方法,拿到对应的controller对象。下面我们看看能不是如何实现了,不要忘了这篇文章讲的是如何实现跨越Session的分布式的TempData。CreateController方法中有两个参数,一个是RequestContext对象,通过他我们能拿到请求的先关信息,第二个参数是一个string类型的controller名称,它的值来源于URL,如下图所示:
首先要注意,我们的Controller Factory就是DefaultControllerFactory对象(作用:为请求提供服务的controller实例),在该方法中,通过反射去创建对应的controller对象。在该方中有两个特别重要的方法,GetControllerType和GetControllerInstance方法。GetControllerType方法方法,返回Type类型,为请求匹配对应的controller类。GetControllerInstance方法返回是IController类型,作用是根据指定的controller类型创建类型的实例。重写GetControllerInstance方法可以实现对创建controller实例的过程进行控制,最常见的就是依赖注入,这里我们暂且不讲。那么GetControllerInstance又是如何来获取实例呢? 下面我们转到GetControllerInstance方法内部,如下图所示:
看到没,它是通过ControllerActivator来拿到controller实例的,转到内部,如下图所示:
看到没,这段代码是不是很熟悉,是不是有点像我们使用autofac的影子。好了我们总结一下Controller对象的创建过程:首先当我们的DefaultControllerFactory类接收到一个controller实例的请求时,在DefaultControllerFactory类内部通过GetControllerType方法来获取controller的类型,然后把这个类型传递给GetControllerInstance方法以获取controller实例,所以在GetControllerInstance方法中就需要有某个东西来创建controller实例,这个创建的过程就是controller被激活的过程。那我们的controller对象创建完毕,接下来就是要调用Controller里面的Execute方法,执行对应的Action方法。接着我们把视线收回到MVCHandler中的BeginProcessRequest方法内部,在该方法内部又执行了 asyncController.BeginExecute(this.RequestContext, asyncCallback, asyncState);方法,为什么会执行Controller里面的ExecuteCore方法呢??首先我们补充一点关于IController的知识:
(1)我们添加的Controller都是一个继承自抽象类System.Web.MVC.Controller,该类又继承自ControllerBase,ControllerBase又实现了IController接口,在该接口中只有一个方法,就是Execute方法,当请求送到了一个实现了IController接口的Controller类时,Execute方法就会被调用。如下所示:
public interface IController { void Execute(RequestContext requestContext); }
ControllerBase实现了Execute方法,如下所示:
注意到没有,this.ExecuteCore()和 Protected abstract void ExecuteCore(),我们再看一下Controller的实现,你就会明白下面的执行流程了,如下图所示:
看到没,我们的Controller类实现了ControllerBase中的ExecuteCore这个抽象方法。注意下1和3是在执行Action方法前和后执行的,后面会讲解到底是什么,继续看我们MVC执行的流程
注意:Controller中的一切对请求的处理都是从Execute方法开始的!!!,下面我们转到BeginExecute方法的内部,如下图所示:
来来来,抄起小手,划重点了,注意到没有,return后面的AsyncRequestWrapper.Begin方法了吗?在第三个参数中有这样一句代码:this.BeginExecuteCore,这里的this值的就是Controller,F11自然会进入到该方法,如下图所示:
首先要明白,当Controller Factory创建好了一个类的实例后,MVC框架则需要一种方式来调用这个实例的Action方法,如果创建了controller是继承Controller抽象类的话,那么则是有Action Invoker来完成调用action方法的任务,MVC默认使用的是ControllerActionInvoker类。然后我们看看代码的具体实现:首先,通过路由数据获取Action名称,例如请求URL为:http://xxx.com/Home/Index,这里获取的Action名称即为Index。然后,通过IActionInvoker invoker = this.ActionInvoker;拿到Action的激活器。那么问题来了,这个ActionInvoker又是啥东东?我们先看看这个接口的定义代码如下:
public interface IActionInvoker { bool InvokeAction(ControllerContext controllerContext, string actionName); }
我们发现原来是一个叫做ControllerActionInvoker的类实现了IActionInvoker接口,ControllerActionInvoker类如下图所示:
接着执行: asyncInvoker.BeginInvokeAction(this.ControllerContext, actionName, asyncCallback, asyncState);,转到内部,如下图所示:
在该方法的内部,主要是获取Controller与Action的描述信息和过滤器信息。获取参数信息后并开始真正执行Action,在action方法执行完之后,开始View的呈现,
我们知道ActionResult是一个抽象类,那么这个InvokeActionResult应该是由其子类来实现。于是,我们找到ViewResult,但是其并未直接继承于ActionResult,再找到其父类ViewResultBase,它则继承了ActionResult。于是,我们来查看它的ExecuteResult方法,如下图所示:
在该方法内部,找到视图引擎,找到视图,执行视图生成HTML,下面我们一步一步来看看,如何执行的。先检查是否传入指定的视图名称,如果没有传入,则取Action方法的名字作为待会要读取的视图名字,代码如下:this.ViewName=context.RouteData.GetRequiredString("action");接着找到对应的视图引擎,代码如下result=this.FindView(context)。在FindView方法内部,循环视图引擎集合,看看哪个视图引擎可以找到对应的视图,就返回哪个视图引擎的结果,此结果中就包含视图接口对象,找到了RazorViewEngine对象,调用视图引擎的FindView方法,但这个方法在RazorViewEngine类中没有,而是在父类的父类中定义(继承关系:RazorViewEngine-->BuildManagerViewEngine-->VirtualPathProviderViewEngine),获取控制器名称、视图的路径,同时还获得了母版页的路径,最终返回ViewEngineResult,然后获取返回的ViewEngineResult里的View对象,然后调用它的Render方法来生成HTML代码并写入到Response中,代码如下:TextWriter writer=context。HttpContext.Response.Output;ViewContext viewContext=new ViewContext(context,view,ViewData,TempData,writer); View.Render(viewContext,writer);最后生成HTML。大家可能通过文字来不是好理解,下面我在附上一张我自己画的流程图,是根据我自己调试代码理解的,如下图所示:(想要下面流程图的可以提下,到时候发给你)
到这里我们ASP.Net MVC 的一次请求处理响应的流程就结束了,好了,不是很理解的话,不要紧,下去可以通过代码调试的方法,自己好好调试调试,慢慢理解。来来来,把思路整理一下,回到我的TempData。通过上面流程的讲解,大家知道在执行action方法之前和之后都会分别执行PossiblyLoadTempData()和PossiblySaveTempData(),如下图所示:
从中可以看到在请求开始时就去取TempData,在Action调用结束后去保存TempData。为什么要再去保存一遍呢?
2.2、TempData源码的讲解
TempData是什么
(1)可以存储一次,只能读取一次,如果第二次读取,将不会有tempdata数据,这样就起到了临时变量的作用
(2) 是一个string object的字典。
(3) action执行前后,都会对temp进行操作
(4)一般用于两个请求之间临时缓存数据或者页面之间传递消息
TempData源码分休
public TempDataDictionary TempData
{
get
{
if ((this.ControllerContext != null) && this.ControllerContext.IsChildAction)
{
return this.ControllerContext.ParentActionViewContext.TempData;
}
if (this._tempDataDictionary == null)
{
this._tempDataDictionary = new TempDataDictionary();
}
return this._tempDataDictionary;
}
set
{
this._tempDataDictionary = value;
}
}
step1:先来看看上面提到了两个方法内部是如何实现的
他们内部又调用了Load和Save方法,转到定义,如下图所示:
这两个方法内部又通过,TempDataprovider分别调用了LoadTempData和SaveTempData方法,再分别转到这两个方法内部,如下所示:
public interface ITempDataProvider
{
IDictionary<string, object> LoadTempData(ControllerContext controllerContext);
void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values);
}
注意:它是一个接口。里面是这两个方法,肯定有子类实现该接口中的两个方法,通过调试源码,你就会知道上面Load和Save方法中最后一个参数,tempDataProvider就是SessionStateTempDataProvider,不信我们来看下源码:
// Generated by .NET Reflector from C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Web.Mvc\v4.0_4.0.0.0__31bf3856ad364e35\System.Web.Mvc.dll namespace System.Web.Mvc { using System; using System.Collections.Generic; using System.Web; using System.Web.Mvc.Properties; public class SessionStateTempDataProvider : ITempDataProvider { internal const string TempDataSessionStateKey = "__ControllerTempData"; public virtual IDictionary<string, object> LoadTempData(ControllerContext controllerContext) { HttpSessionStateBase session = controllerContext.HttpContext.Session; if (session != null) { Dictionary<string, object> dictionary = session["__ControllerTempData"] as Dictionary<string, object>; if (dictionary != null) { session.Remove("__ControllerTempData"); return dictionary; } } return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); } public virtual void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values) { if (controllerContext == null) { throw new ArgumentNullException("controllerContext"); } HttpSessionStateBase session = controllerContext.HttpContext.Session; bool flag = (values != null) && (values.Count > 0); if (session == null) { if (flag) { throw new InvalidOperationException(MvcResources.SessionStateTempDataProvider_SessionStateDisabled); } } else if (flag) { session["__ControllerTempData"] = values; } else if (session["__ControllerTempData"] != null) { session.Remove("__ControllerTempData"); } } } }
看到没,我们的SessionStateTempDataProvider类实现了ITempDataProvider接口,重写了Load和Save方法。
从图中可知,SessionStatesTempDataProvider暴露了LoadTempData和SaveTempData两个方法。
其中从SaveTempData中session["__ControllerTempData"] = (object) values;可以看出,TempData是存储在Session中的。
其中LoadTempData方法中session.Remove("__ControllerTempData");就说明了从session中获取tempdata后,对应的tempdata就从session中清空了
原来每次取完TempData后都会从Session中清空,如果TempData未曾使用,那当然要重新保存到Session中啊。这就回答了为什么要再去保存一遍的问题。
那问题来了,我们要实现分布式的TempData,在MVC哪个地方注入呢?我们再来回顾一下MVC的管道和action方法执行前后发现:PossiblyLoadTempData和PossiblySaveTempData是在调用Controller中对应的action方法时执行的,并且Controller中有 TempDataProvider属性,代码如下:
public ITempDataProvider TempDataProvider
{
get
{
if (this._tempDataProvider == null)
{
this._tempDataProvider = this.CreateTempDataProvider();
}
return this._tempDataProvider;
}
set
{
this._tempDataProvider = value;
}
}
所以注入点我们就找到,在创建Controller Factory中创建Controller实例的时候,把我们自定义的DataProvider类,赋值给TempDataProvider就可以了,下面我们来实现一把分布式的tempData
3、实现分布式的TempData
准备工作:首先我们新建一个MVC项目,新建一个文件夹Infrastructure文件夹,在这个文件下添加一个类:继承自DefaultControllerFactory的MyControllerFactory类即我们自定义的Controller Factory,代码如下:
1 public class MyControllerFactory:DefaultControllerFactory 2 { 3 public override IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName) 4 { 5 var iController= base.CreateController(requestContext, controllerName); 6 7 var controller = iController as Controller; 8 controller.TempDataProvider = new CrossSessionTempData2(); 9 10 11 return iController; 12 } 13 }
3.1、把TempData的值存入到cache中
1 namespace System.Web.Mvc 2 { 3 using System; 4 using System.Collections.Generic; 5 using System.Web; 6 using System.Web.Mvc.Properties; 7 8 public class SessionStateTempDataProvider : ITempDataProvider 9 { 10 internal const string TempDataSessionStateKey = "__ControllerTempData"; 11 12 public virtual IDictionary<string, object> LoadTempData(ControllerContext controllerContext) 13 { 14 var cache = controllerContext.HttpContext.Cache; 15 if (cache != null) 16 { 17 Dictionary<string, object> dictionary =cache["__ControllerTempData"] as Dictionary<string, object>; 18 if (dictionary != null) 19 { 20 cache .Remove("__ControllerTempData"); 21 return dictionary; 22 } 23 } 24 return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); 25 } 26 27 public virtual void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values) 28 { 29 if (controllerContext == null) 30 { 31 throw new ArgumentNullException("controllerContext"); 32 } 33 var cache = controllerContext.HttpContext.Cache; 34 bool flag = (values != null) && (values.Count > 0); 35 if (cache == null) 36 { 37 if (flag) 38 { 39 throw new InvalidOperationException(MvcResources.SessionStateTempDataProvider_SessionStateDisabled); 40 } 41 } 42 else if (flag) 43 { 44 cache ["__ControllerTempData"] = values; 45 } 46 else if (cache ["__ControllerTempData"] != null) 47 { 48 cache .Remove("__ControllerTempData"); 49 } 50 } 51 } 52 }
TempData的值存入到cache中之文件依赖
接着我们需要自定义一个实现了ITempDataProvider接口的DataProvider类,代码如下:(2017年6月20日18:13:07 代码修改)
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Web; 5 using System.Web.Caching; 6 using System.Web.Mvc; 7 8 namespace CrossSessionTempData.Infrastructure 9 { 10 public class CrossSessionTempData2 : ITempDataProvider 11 { 12 13 internal const string TempDataSessionStateKey = "__ControllerTempData"; 14 15 public virtual IDictionary<string, object> LoadTempData(ControllerContext controllerContext) 16 { 17 var cache = controllerContext.HttpContext.Cache; 18 19 if (cache != null) 20 { 21 Dictionary<string, object> dictionary = cache[TempDataSessionStateKey] as Dictionary<string, object>; 22 if (dictionary != null) 23 { 24 cache.Remove(TempDataSessionStateKey); 25 return dictionary; 26 } 27 } 28 return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); 29 } 30 31 /// <summary>Saves the specified values in the temporary data dictionary by using the specified controller context.</summary> 32 /// <param name="controllerContext">The controller context.</param> 33 /// <param name="values">The values.</param> 34 /// <exception cref="T:System.InvalidOperationException">An error occurred the session context was being retrieved.</exception> 35 public virtual void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values) 36 { 37 if (controllerContext == null) 38 { 39 throw new ArgumentNullException("controllerContext"); 40 } 41 var cache = controllerContext.HttpContext.Cache; 42 bool flag = values != null && values.Count > 0; 43 if (cache == null) 44 { 45 if (flag) 46 { 47 throw new InvalidOperationException(""); 48 } 49 } 50 else 51 { 52 CacheDependency dp = new CacheDependency(controllerContext.HttpContext.Server.MapPath("/Data/123.txt")); 53 if (flag) 54 { 55 56 57 58 cache.Insert(TempDataSessionStateKey, values, dp); 59 60 return; 61 } 62 63 if (cache[TempDataSessionStateKey] != null) 64 { 65 cache.Remove(TempDataSessionStateKey); 66 } 67 } 68 } 69 } 70 }
添加一个controller,代码如下:
我们在Index中设置TempData的值,然后再List中读取。按说我们只有概念文件依赖是存在缓存中发TempData的值才会消失,下面我们运行一把,看看运行结果:
先访问:http://localhost:42913/Default/Index
在执行Index action方法之前会执行LoadTempData方法,如下图所示:
接着,设置TempData的值,如下图所示:
接着执行Save方法,如下图所示:
看到没,把TempData的值存入到Cache中了,接着我方访问以下http://localhost:42913/Default/List,TempData的值就会显示出来:
首先也会执行LoadTempData方法
再执行List里面的代码,在执行SaveTempData方法, 返回视图:
不管怎么刷新,值依然存在,但是只要我们修改依赖文件1.txt值立马就消失了。
保存,再次刷新页面,数据就丢失了。
3.2、把TempData的值存入到NoSQL Memcached中实现真正的分布式
关于Memcached的安装和操作请参考我的这篇博客:
ASP.Net MVC4+Memcached+CodeFirst实现分布式缓存
MemcacheHelper:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Web; 5 using Memcached.ClientLibrary; 6 7 namespace WebDemo.Models 8 { 9 public static class MemcacheHelper 10 { 11 private static MemcachedClient mc; 12 13 static MemcacheHelper() 14 { 15 //通过客户端来进行memcached的集群配置,在插入数据的时候,使用一致性哈希算法,将对应的value值存入Memcached 16 String[] serverlist = { "127.0.0.1:11211" }; 17 18 // 初始化Memcached的服务池 19 SockIOPool pool = SockIOPool.GetInstance("test"); 20 //设置服务器列表 21 pool.SetServers(serverlist); 22 //各服务器之间负载均衡的设置比例 23 pool.SetWeights(new int[] { 1 }); 24 pool.Initialize(); 25 //创建一个Memcached的客户端对象 26 mc = new MemcachedClient(); 27 mc.PoolName = "test"; 28 //是否启用压缩数据:如果启用了压缩,数据压缩长于门槛的数据将被储存在压缩的形式 29 mc.EnableCompression = false; 30 31 } 32 /// <summary> 33 /// 插入值 34 /// </summary> 35 /// <param name="key">建</param> 36 /// <param name="value">值</param> 37 /// <param name="expiry">过期时间</param> 38 /// <returns></returns> 39 public static bool Set(string key, object value,DateTime expiry){ 40 return mc.Set(key, value, expiry); 41 } 42 /// <summary> 43 /// 获取值 44 /// </summary> 45 /// <param name="key"></param> 46 /// <returns></returns> 47 public static object Get(string key) 48 { 49 return mc.Get(key); 50 } 51 } 52 }
引用对应的dll:
自定义的我们的DataProvider:
1 public class CrossSessionTempData2 : ITempDataProvider 2 { 3 4 internal const string TempDataSessionStateKey = "__ControllerTempData"; 5 6 public virtual IDictionary<string, object> LoadTempData(ControllerContext controllerContext) 7 { 8 9 Dictionary<string, object> dictionary = MemCaheHelper.Get(TempDataSessionStateKey) as Dictionary<string, object>; 10 if (dictionary != null) 11 { 12 MemCaheHelper.Set(TempDataSessionStateKey, dictionary, DateTime.MinValue); 13 return dictionary; 14 } 15 return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); 16 } 17 18 public virtual void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values) 19 { 20 if (controllerContext == null) 21 { 22 throw new ArgumentNullException("controllerContext"); 23 } 24 25 bool flag = values != null && values.Count > 0; 26 if (flag) 27 { 28 29 MemCaheHelper.Set(TempDataSessionStateKey, values,DateTime.Now.AddMinutes(1)); 30 return; 31 } 32 33 if (MemCaheHelper.Get(TempDataSessionStateKey) != null) 34 { 35 MemCaheHelper.Set(TempDataSessionStateKey,values,DateTime.MinValue); 36 } 37 38 39 } 40 }
运行效果完美!!!!至此,我们的分布式TempData的功能已经实现。后面我会的代码提供给大家。其实我们也可以把值存入到redis中,原理和MemCached差不多,自己可以尝试一下。
4、总结:
这篇文章花了很长时间,希望对你有帮助,如果大家觉的还可以的话,帮忙点下推荐。如果对TempData还是不太了解,可以参考这位园友的文章TempData知多少
附件下载:
参考文章:
木碗城主:http://www.cnblogs.com/OceanEyes/archive/2012/08/13/aspnetEssential-1.html
Edison Chou:http://www.cnblogs.com/edisonchou/p/3855969.html
MIN飞翔:http://www.cnblogs.com/EricaMIN1987_IT/p/3837436.html
Liam Wang:http://www.cnblogs.com/willick/p/3331521.html
一线码农:http://www.cnblogs.com/huangxincheng/p/5663725.html
作者:郭峥
出处:http://www.cnblogs.com/runningsmallguo/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。