在应用开发中实现对上下文(Context)数据的统一管理

在应用开发中实现对上下文(Context)数据的统一管理

在应用开发中,我们经常需要设置一些上下文(Context)信息,这些上下文信息一般基于当前的会话 (Session),比如当前登录用户的个人信息;或者基于当前方法调用栈,比如在同一个调用中涉及的多 个层次之间数据。在这篇文章中,我创建了一个称为ApplicationContext的组件,对上下文信息进行统一 的管理

一、基于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: }

在应用开发中实现对上下文(Context)数据的统一管理

现在我们来看看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: }

在应用开发中实现对上下文(Context)数据的统一管理

运行上面的程序,你 会发现你设置的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>