一起谈.NET技术,如何实现对上下文(Context)数据的统一管理 [提供源代码下载]
2011-08-29 19:31
1086 查看
在应用开发中,我们经常需要设置一些上下文(Context)信息,这些上下文信息一般基于当前的会话(Session),比如当前登录用户的个人信息;或者基于当前方法调用栈,比如在同一个调用中涉及的多个层次之间数据。在这篇文章中,我创建了一个称为ApplicationContext的组件,对上下文信息进行统一的管理。[Source Code从这里下载]
为了使ApplicationContext定义得尽可能地简单,我直接让它继承自Dictionary,而从本质上讲ApplicationContext就是一个基于字典的上下文数据的容器。静态属性Current表示当前的ApplicationConext,如何当前存在HttpContext,则使用HttpConext的Session,否则使用CallConext。Session和CallConext的采用相同的Key:Artech.ApplicationContexts.ApplicationContext。你可以采用如下的方式对上下文数据进行设置和读取。
现在我们来看看ApplicationContext在一个简单的Windows Form应用中的使用情况。在如右图(点击看大图)所示的一个Form中,我们可以进行Profile的设置和获取。其中“Get [Sync]”和“Get [Async]”按钮分别模拟对存贮于当前ApplicationContext中的Profile信息进行同步和异步方式的获取,通过点击Save按钮将设置的Profile信息保存到当前的ApplicationContext之中。
“Save”、“Clear”、“Get [Sync]”和“Get [Async]”响应的事件处理程序如下面的代码所示:
运行上面的程序,你会发现你设置的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说起。
IllogicalCallContext:IllogicalCallContext和LogicalCallContext 相反,仅仅是存储与当前线程的TLS中,并不能随着跨线程的操作执行实现跨线程传播。
HttpContext本质上也通过CallContext存储的,不过HttpContext本身是作为IllogicalCallContext的形式保存在CallContext,这也正是为何基于HttpSessionState的ApplicationContext也不能解决多线程的问题的真正原因。
也就说,在ApplicationContext的Current方法中,我们只需要将CallContext.SetData(ContextKey, new ApplicationContext());替换成CallContext.LogicalSetData(ContextKey, new ApplicationContext());即可:
那么如果我们们能够将存储与当前HttpContext的Session中的ApplicationContext作为LogicalCallContext拷贝到CallContext中,那么在进行异步调用的时候,就能自动传递到另外一个线程之中了。此外,由于ASP.NET采用线程池的机制处理HTTP请求,我们需要将当前CallContext的数据进行及时清理,以免被另外一个请求复用。我们可以有很多方式实现这样的功能,比如在Global.asax中定义响应的事件处理方法,自定义HttpApplication或者自定义HttpModule。
如果自定义HttpModule,我们可以注册HttpApplication的两个事件:PostAcquireRequestState和PreSendRequestContent,分别实现对当前ApplicationContext的拷贝和清理。具体定义如下:
我们只需要通过如下的配置将其应用到我们的程序之中即可:
一、基于CallContext和HttpSessionState的ApplicationContext
如何实现对上下文信息的存储,对于Web应用来说,我们可以借助于HttpSessionState;对于GUI应用来讲,我们则可以使用CallConext。ApplicationContext完全是借助于这两者建立起来的,首先来看看其定义:using System;
using System.Collections.Generic;
using System.Runtime.Remoting.Messaging;
using System.Web;
namespace Artech.ApplicationContexts
{
[Serializable]
public class ApplicationContext:Dictionary<string, object>
{
public const string ContextKey = "Artech.ApplicationContexts.ApplicationContext";
public static ApplicationContext Current
{
get
{
if (null != HttpContext.Current)
{
if (null == HttpContext.Current.Session[ContextKey])
{
HttpContext.Current.Session[ContextKey] = new ApplicationContext();
}
return HttpContext.Current.Session[ContextKey] as ApplicationContext;
}
if (null == CallContext.GetData(ContextKey))
{
CallContext.SetData(ContextKey, new ApplicationContext());
}
return CallContext.GetData(ContextKey) as ApplicationContext;
}
}
}
}
为了使ApplicationContext定义得尽可能地简单,我直接让它继承自Dictionary,而从本质上讲ApplicationContext就是一个基于字典的上下文数据的容器。静态属性Current表示当前的ApplicationConext,如何当前存在HttpContext,则使用HttpConext的Session,否则使用CallConext。Session和CallConext的采用相同的Key:Artech.ApplicationContexts.ApplicationContext。你可以采用如下的方式对上下文数据进行设置和读取。
//设置
ApplicationContext.Current["UserName"] = "Foo";
//读取
var userName = ApplicationContext.Current["UserName"];
二、ApplicationContext在异步调用中的局限
在同步调用的情况下,ApplicationContext可以正常工作。但是对于异步调用,当前的上下文信息并不能被传播到另一个线程中去。接下来,我们将给出一个简单的例子,模拟通过ApplicationContext存贮用户的Profile信息,为此,我定义了如下一个Profile类,属性FirstName、LastName和Age代表三个Profile属性。using System;
namespace Artech.ApplicationContexts
{
[Serializable]
public class Profile
{
public string FirstName
{ get; set;}
public string LastName
{ get; set;}
public int Age
{ get; set;}
public Profile()
{
this.FirstName = "N/A";
this.LastName = "N/A";
this.Age = 0;
}
}
}
为了便于操作,我直接在ApplicationContext定义了一个Profile属性,返回值类型为Profile,定义如下:
[Serializable]
public class ApplicationContext : Dictionary<string, object>
{
public const string ProfileKey = "Artech.ApplicationContexts.ApplicationContext.Profile";
public Profile Profile
{
get
{
if (!this.ContainsKey(ProfileKey))
{
this[ProfileKey] = new Profile();
}
return this[ProfileKey] as Profile;
}
}
}
现在我们来看看ApplicationContext在一个简单的Windows Form应用中的使用情况。在如右图(点击看大图)所示的一个Form中,我们可以进行Profile的设置和获取。其中“Get [Sync]”和“Get [Async]”按钮分别模拟对存贮于当前ApplicationContext中的Profile信息进行同步和异步方式的获取,通过点击Save按钮将设置的Profile信息保存到当前的ApplicationContext之中。
“Save”、“Clear”、“Get [Sync]”和“Get [Async]”响应的事件处理程序如下面的代码所示:
using System;
using Artech.ApplicationContexts;
namespace WindowsApp
{
public partial class ProfileForm : System.Windows.Forms.Form
{
public ProfileForm()
{
InitializeComponent();
}
private void buttonSave_Click(object sender, EventArgs e)
{
ApplicationContext.Current.Profile.FirstName = this.textBoxFirstName.Text.Trim();
ApplicationContext.Current.Profile.LastName = this.textBoxLastName.Text.Trim();
ApplicationContext.Current.Profile.Age = (int)this.numericUpDownAge.Value;
}
private void buttonClear_Click(object sender, EventArgs e)
{
this.textBoxFirstName.Text = string.Empty;
this.textBoxLastName.Text = string.Empty;
this.numericUpDownAge.Value = 0;
}
private void buttonSyncGet_Click(object sender, EventArgs e)
{
this.textBoxFirstName.Text = ApplicationContext.Current.Profile.FirstName;
this.textBoxLastName.Text = ApplicationContext.Current.Profile.LastName;
this.numericUpDownAge.Value = ApplicationContext.Current.Profile.Age;
}
private void buttonAsyncGet_Click(object sender, EventArgs e)
{
GetProfile getProfileDel = () =>
{
return ApplicationContext.Current.Profile;
};
IAsyncResult asynResult = getProfileDel.BeginInvoke(null, null);
Profile profile = getProfileDel.EndInvoke(asynResult);
this.textBoxFirstName.Text = profile.FirstName;
this.textBoxLastName.Text = profile.LastName;
this.numericUpDownAge.Value = profile.Age;
}
delegate Profile GetProfile();
}
}
运行上面的程序,你会发现你设置的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定义如下:[Serializable, ComVisible(true), SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)]
public sealed class CallContext
{
public static void FreeNamedDataSlot(string name);
public static object GetData(string name);
public static Header[] GetHeaders();
public static object LogicalGetData(string name);
public static void LogicalSetData(string name, object data);
public static void SetData(string name, object data);
public static void SetHeaders(Header[] headers);
public static object HostContext{ get; [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)] set;}
}
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());即可:
[Serializable]
public class ApplicationContext : Dictionary<string, object>
{
//其他成员
public static ApplicationContext Current
{
get
{
//...
if (null == CallContext.GetData(ContextKey))
{
CallContext.LogicalSetData(ContextKey, new ApplicationContext());
}
return CallContext.GetData(ContextKey) as ApplicationContext;
}
}
}
或者说,我们直接让ApplicationContext实现ILogicalThreadAffinative接口。由于该ILogicalThreadAffinative没有定义任何成员,所有我们不需要添加任何多余的代码:
[Serializable]
public class ApplicationContext : Dictionary<string, object>, ILogicalThreadAffinative
{
//...
}
现在再次运行我们上面的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的拷贝和清理。具体定义如下:
using System.Runtime.Remoting.Messaging;
using System.Web;
namespace Artech.ApplicationContexts
{
public class ContextHttpModule:IHttpModule
{
public void Dispose(){}
public void Init(HttpApplication context)
{
context.PostAcquireRequestState += (sender, args) =>
{
CallContext.SetData(ApplicationContext.ContextKey, ApplicationContext.Current);
};
context.PreSendRequestContent += (sender, args) =>
{
CallContext.SetData(ApplicationContext.ContextKey, null);
};
}
}
}
我们只需要通过如下的配置将其应用到我们的程序之中即可:
xml version="1.0"?>
<configuration>
<system.web>
<httpModules>
<add name="ContextHttpModule" type="Artech.ApplicationContexts.ContextHttpModule,Artech.ApplicationContexts.Lib"/>
httpModules>
system.web>
configuration>
相关文章推荐
- 如何实现对上下文(Context)数据的统一管理 [提供源代码下载]
- 如何实现对上下文(Context)数据的统一管理
- 一起谈.NET技术,从数据到代码—通过代码生成机制实现强类型编程[上篇]
- 一起谈.NET技术,如何实现ASP.NET网站个性化?
- 一起谈.NET技术,一句代码实现批量数据绑定[上篇]
- 一起谈.NET技术,WF4.0中如何实现XAML工作流的动态加载
- 一起谈.NET技术,一句代码实现批量数据绑定[下篇]
- 一起谈.NET技术,Silverlight 4中把DataGrid数据导出Excel—附源码下载
- 一起谈.NET技术,ASP.NET Eval如何进行数据绑定
- 一起谈.NET技术,浅谈如何使用.NET存储XML数据
- 一起谈.NET技术,Dojo Data Store——统一数据访问接口
- 一起谈.NET技术,从数据到代码—通过代码生成机制实现强类型编程[下篇]
- 一起谈.NET技术,在ASP.NET 2.0中数据绑定的实现方法
- 一起谈.NET技术,使用VS2010的Database项目模板统一管理数据库对象
- 一步步教你如何用疯狂.NET架构中的通用权限系统 -- 分布式管理(每个公司管理每个公司自己的数据)
- 数据管理的智能趋势(2):如何实现高效的数据管理
- 一个通用的单元测试框架的思考和设计07-实现篇-自动管理测试数据-如何为自增长主键id赋值
- 一起谈.NET技术,Flex 数据访问 WebService 使用参数(下)
- 一起谈.NET技术,如何通过ildasm/ilasm修改assembly的IL代码
- 从技术到管理——如何实现跨越