如何實現對上下文(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,而從本質上講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: }
現在我們來看看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: }
運行上面的程序,你會發現你設置的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>
出處:http://artech.cnblogs.com
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。