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/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。