如何實現對上下文(Context)數據的統一管理

如何實現對上下文(Context)數據的統一管理 [提供源代碼下載]

在應用開發中,我們經常需要設置一些上下文(Context)信息,這些上下文信息一般基於當前的會話(Session),比如當前登錄用戶的個人信息;或者基於當前方法調用棧,比如在同一個調用中涉及的多個層次之間數據。在這篇文章中,我創建了一個稱爲ApplicationContext的組件,對上下文信息進行統一的管理。[Source Code從這裏下載]

一、基於CallContext和HttpSessionState的ApplicationContext

如何實現對上下文信息的存儲,對於Web應用來說,我們可以藉助於HttpSessionState;對於GUI應用來講,我們則可以使用CallConext。ApplicationContext完全是藉助於這兩者建立起來的,首先來看看其定義:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Runtime.Remoting.Messaging;
   4: using System.Web;
   5: namespace Artech.ApplicationContexts
   6: {
   7:     [Serializable]
   8:     public class ApplicationContext:Dictionary<string, object>
   9:     {
  10:         public const string ContextKey = "Artech.ApplicationContexts.ApplicationContext";
  11:         
  12:         public static ApplicationContext Current
  13:         {
  14:             get
  15:             {
  16:                 if (null != HttpContext.Current)
  17:                 {
  18:                     if (null == HttpContext.Current.Session[ContextKey])
  19:                     {
  20:                         HttpContext.Current.Session[ContextKey] = new ApplicationContext();
  21:                     }
  22:  
  23:                     return HttpContext.Current.Session[ContextKey] as ApplicationContext;
  24:                 }
  25:  
  26:                 if (null == CallContext.GetData(ContextKey))
  27:                 {
  28:                     CallContext.SetData(ContextKey, new ApplicationContext());
  29:                 }
  30:                 return CallContext.GetData(ContextKey) as ApplicationContext;                
  31:             }
  32:         }        
  33:     }
  34: }

爲了使ApplicationContext定義得儘可能地簡單,我直接讓它繼承自Dictionary<string,object>,而從本質上講ApplicationContext就是一個基於字典的上下文數據的容器。靜態屬性Current表示當前的ApplicationConext,如何當前存在HttpContext,則使用HttpConext的Session,否則使用CallConext。Session和CallConext的採用相同的Key:Artech.ApplicationContexts.ApplicationContext。你可以採用如下的方式對上下文數據進行設置和讀取。

   1: //設置
   2: ApplicationContext.Current["UserName"] = "Foo";
   3: //讀取
   4: var userName = ApplicationContext.Current["UserName"];

二、ApplicationContext在異步調用中的侷限

在同步調用的情況下,ApplicationContext可以正常工作。但是對於異步調用,當前的上下文信息並不能被傳播到另一個線程中去。接下來,我們將給出一個簡單的例子,模擬通過ApplicationContext存貯用戶的Profile信息,爲此,我定義瞭如下一個Profile類,屬性FirstName、LastName和Age代表三個Profile屬性。

   1: using System;
   2: namespace Artech.ApplicationContexts
   3: {
   4:     [Serializable]
   5:     public class Profile
   6:     {
   7:         public string FirstName
   8:         { get; set; }
   9:         public string LastName
  10:         { get; set; }
  11:         public int Age
  12:         { get; set; }
  13:         public Profile()
  14:         {
  15:             this.FirstName = "N/A";
  16:             this.LastName = "N/A";
  17:             this.Age = 0;
  18:         }
  19:     }
  20: }

爲了便於操作,我直接在ApplicationContext定義了一個Profile屬性,返回值類型爲Profile,定義如下:

   1: [Serializable]
   2: public class ApplicationContext : Dictionary<string, object>
   3: {
   4:     public const string ProfileKey = "Artech.ApplicationContexts.ApplicationContext.Profile";   
   5:  
   6:     public Profile Profile
   7:     {
   8:         get
   9:         {
  10:             if (!this.ContainsKey(ProfileKey))
  11:             {
  12:                 this[ProfileKey] = new Profile();
  13:             }
  14:             return this[ProfileKey] as Profile;
  15:         }
  16:     }
  17: }

image現在我們來看看ApplicationContext在一個簡單的Windows Form應用中的使用情況。在如右圖(點擊看大圖)所示的一個Form中,我們可以進行Profile的設置和獲取。其中“Get [Sync]”和“Get [Async]”按鈕分別模擬對存貯於當前ApplicationContext中的Profile信息進行同步異步方式的獲取,通過點擊Save按鈕將設置的Profile信息保存到當前的ApplicationContext之中。

“Save”、“Clear”、“Get [Sync]”和“Get [Async]”響應的事件處理程序如下面的代碼所示:

   1: using System;
   2: using Artech.ApplicationContexts;
   3: namespace WindowsApp
   4: {
   5:     public partial class ProfileForm : System.Windows.Forms.Form
   6:     {
   7:         public ProfileForm()
   8:         {
   9:             InitializeComponent();
  10:         }
  11:  
  12:         private void buttonSave_Click(object sender, EventArgs e)
  13:         {
  14:             ApplicationContext.Current.Profile.FirstName = this.textBoxFirstName.Text.Trim();
  15:             ApplicationContext.Current.Profile.LastName = this.textBoxLastName.Text.Trim();
  16:             ApplicationContext.Current.Profile.Age = (int)this.numericUpDownAge.Value;
  17:         }
  18:  
  19:         private void buttonClear_Click(object sender, EventArgs e)
  20:         {
  21:             this.textBoxFirstName.Text = string.Empty;
  22:             this.textBoxLastName.Text = string.Empty;
  23:             this.numericUpDownAge.Value = 0;
  24:         }
  25:  
  26:         private void buttonSyncGet_Click(object sender, EventArgs e)
  27:         {
  28:             this.textBoxFirstName.Text = ApplicationContext.Current.Profile.FirstName;
  29:             this.textBoxLastName.Text = ApplicationContext.Current.Profile.LastName;
  30:             this.numericUpDownAge.Value = ApplicationContext.Current.Profile.Age;
  31:         }
  32:  
  33:         private void buttonAsyncGet_Click(object sender, EventArgs e)
  34:         {
  35:             GetProfile getProfileDel = () =>
  36:                 {
  37:                     return ApplicationContext.Current.Profile;
  38:                 };
  39:             IAsyncResult asynResult = getProfileDel.BeginInvoke(null, null);
  40:             Profile profile = getProfileDel.EndInvoke(asynResult);
  41:             this.textBoxFirstName.Text = profile.FirstName;
  42:             this.textBoxLastName.Text = profile.LastName;
  43:             this.numericUpDownAge.Value = profile.Age;
  44:         }
  45:  
  46:         delegate Profile GetProfile();       
  47:     }
  48: }

image 運行上面的程序,你會發現你設置的Profile信息,可以通過點擊“Get [Sync]”按鈕顯示出來,。而你點擊“Get [Async]”按鈕的時候,卻不能顯示正確的值。具體的結果如下圖(點擊看大圖)所示。三張截圖分別模擬的點擊“Save”、Get [Sync]”和“Get [Async]”按鈕之後的顯示。

上面演示的是ApplicationContext在Windows Form應用中的使用,實際上在ASP.NET應用中,你依然會得到相同的結果。通過ApplicaticationContext的定義我們可以知道,ApplicationContext對象最終保存在CallContext或者HttpSessionState中。Windows Form應用採用的是前者,而Web應用則採用後者。

也就是說,無論是CallContext還是HttpContext(HttpSessionState最終依附於當前的HttpContext),都不能自動實現數據的跨線程傳遞。至於原因,需要從兩種不同的CallContext說起。

三、LogicalCallContext V.S. IllogicalCallContext

CallContext定義在System.Runtime.Remoting.Messaging.CallContext命名空間下,是類似於方法調用的線程本地存儲區的專用集合對象,並提供對每個邏輯執行線程都唯一的數據槽。數據槽不在其他邏輯線程上的調用上下文之間共享。當 CallContext 沿執行代碼路徑往返傳播並且由該路徑中的各個對象檢查時,可將對象添加到其中。CallContext定義如下:

   1: [Serializable, ComVisible(true), SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)]
   2: public sealed class CallContext
   3: {
   4:     
   5:     public static void FreeNamedDataSlot(string name);
   6:     public static object GetData(string name);
   7:     public static Header[] GetHeaders();    
   8:     public static object LogicalGetData(string name);
   9:     public static void LogicalSetData(string name, object data);
  10:     public static void SetData(string name, object data);
  11:     public static void SetHeaders(Header[] headers);
  12:    
  13:     public static object HostContext { get; [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)] set; }   
  14: }

CallContext具有如下兩種不同的類型:

  • LogicalCallContext:LogicalCallContext 類是在對遠程應用程序域進行方法調用時使用的 CallContext 類的一個版本。CallContext 是類似於方法調用的線程本地存儲的專用集合對象,並提供對每個邏輯執行線程都唯一的數據槽。數據槽不在其他邏輯線程上的調用上下文之間共享。當 CallContext 沿執行代碼路徑往返傳播並且由該路徑中的各個對象檢查時,可將對象添加到其中。當對另一個 AppDomain 中的對象進行遠程方法調用時,CallContext 類將生成一個與該遠程調用一起傳播的 LogicalCallContext。只有公開 ILogicalThreadAffinative 接口並存儲在 CallContext 中的對象被在 LogicalCallContext 中傳播到 AppDomain 外部。不支持此接口的對象不在 LogicalCallContext 實例中與遠程方法調用一起傳輸。
  • IllogicalCallContext:IllogicalCallContext和LogicalCallContext 相反,僅僅是存儲與當前線程的TLS中,並不能隨着跨線程的操作執行實現跨線程傳播。

HttpContext本質上也通過CallContext存儲的,不過HttpContext本身是作爲IllogicalCallContext的形式保存在CallContext,這也正是爲何基於HttpSessionState的ApplicationContext也不能解決多線程的問題的真正原因。

四、讓CallContext實現跨線程傳播

也就是說,如果想讓CallContext的數據被自動傳遞當目標線程,只能將其作爲LogicalCallContext。我們有兩種當時將相應的數據存儲爲LogicalCallContext:調用CallContext的靜態方法LogicalSetData,或者放上下文類型實現ILogicalThreadAffinative接口。

也就說,在ApplicationContext的Current方法中,我們只需要將CallContext.SetData(ContextKey, new ApplicationContext());替換成CallContext.LogicalSetData(ContextKey, new ApplicationContext());即可:

   1: [Serializable]
   2: public class ApplicationContext : Dictionary<string, object>
   3: {
   4:     //其他成員
   5:     public static ApplicationContext Current
   6:     {
   7:         get
   8:         {
   9:             //...
  10:             if (null == CallContext.GetData(ContextKey))
  11:             {
  12:                 CallContext.LogicalSetData(ContextKey, new ApplicationContext());
  13:             }
  14:             return CallContext.GetData(ContextKey) as ApplicationContext;
  15:         }
  16:     }
  17: }

或者說,我們直接讓ApplicationContext實現ILogicalThreadAffinative接口。由於該ILogicalThreadAffinative沒有定義任何成員,所有我們不需要添加任何多餘的代碼:

   1: [Serializable]
   2: public class ApplicationContext : Dictionary<string, object>, ILogicalThreadAffinative
   3: {
   4:     //...
   5: }

現在再次運行我們上面的Windows Form應用,點擊“Get [Async]”按鈕後將會得到正確的Profile顯示,有興趣的讀者不妨下載實例代碼試試。但是當運行Web應用的時候,依然有問題,爲此我們需要進行一些額外工作。

五、通過ASP.NET擴展解決Web應用的異步調用問題

在上面我們已經提過,ASP.NET管道將當前的HttpContext的存儲與基於當前線程的CallContext中,而存貯的形式是IllogicalCallContext而非LogicalCallContext,說在非請求處理線程是獲取不到當前HttpContext的。針對我們ApplicationContext就意味着:在Web應用中,主線程實際上操作的是當前HttpContext的Session,而另外一個線程中則是直接使用CallConext。

那麼如果我們們能夠將存儲與當前HttpContext的Session中的ApplicationContext作爲LogicalCallContext拷貝到CallContext中,那麼在進行異步調用的時候,就能自動傳遞到另外一個線程之中了。此外,由於ASP.NET採用線程池的機制處理HTTP請求,我們需要將當前CallContext的數據進行及時清理,以免被另外一個請求複用。我們可以有很多方式實現這樣的功能,比如在Global.asax中定義響應的事件處理方法,自定義HttpApplication或者自定義HttpModule。

如果自定義HttpModule,我們可以註冊HttpApplication的兩個事件:PostAcquireRequestState和PreSendRequestContent,分別實現對當前ApplicationContext的拷貝和清理。具體定義如下:

   1: using System.Runtime.Remoting.Messaging;
   2: using System.Web;
   3: namespace Artech.ApplicationContexts
   4: {
   5:     public class ContextHttpModule:IHttpModule
   6:     {
   7:  
   8:         public void Dispose(){}
   9:         public void Init(HttpApplication context)
  10:         {
  11:             context.PostAcquireRequestState += (sender, args) =>
  12:                 {
  13:                     CallContext.SetData(ApplicationContext.ContextKey, ApplicationContext.Current);
  14:                 };
  15:             context.PreSendRequestContent += (sender, args) =>
  16:             {
  17:                 CallContext.SetData(ApplicationContext.ContextKey, null);
  18:             };
  19:         }
  20:     }
  21: }

我們只需要通過如下的配置將其應用到我們的程序之中即可:

   1: <?xml version="1.0"?>
   2: <configuration>
   3:   <system.web>
   4:     <httpModules>
   5:       <add name="ContextHttpModule" type="Artech.ApplicationContexts.ContextHttpModule,Artech.ApplicationContexts.Lib"/>
   6:     </httpModules>
   7:   </system.web> 
   8: </configuration>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章