首页 > 代码库 > 在ASP.Net MVC 中如何实现跨越Session的分布式TempData

在ASP.Net MVC 中如何实现跨越Session的分布式TempData

Hi,guys!Long time no see!

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 = http://www.mamicode.com/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对象。在该方中有两个特别重要的方法,GetControllerTypeGetControllerInstance方法。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分别调用了LoadTempDataSaveTempData方法,再分别转到这两个方法内部,如下所示:

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类,代码如下:

 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                     //cache[TempDataSessionStateKey] = values;
58                     cache.Insert(TempDataSessionStateKey, values, dp);
59 
60                     return;
61                 }
62                 cache.Insert(TempDataSessionStateKey, values, dp);
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的安装和操作请参考我的这篇博客:

http://www.cnblogs.com/runningsmallguo/p/6239215.html

 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 }
View Code

 引用对应的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代码

MemCached

Reflector注册机(最好安装8.5版本的)

流程图

参考文章:

木碗城主: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/runningsmallguo/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

在ASP.Net MVC 中如何实现跨越Session的分布式TempData