ASP.NET 1.1 中的自定义会话状态管理






4.48/5 (9投票s)
本文讨论了一种自定义会话状态管理机制,该机制取代了 ASP.NET 1.1 的 SessionStateModule。
引言
本文提出了一种“穷举法”来解决为 ASP.NET 1.0/1.1 实现自定义会话状态管理机制的问题。ASP.NET 团队在 .NET 2.0 Beta 中已经解决了一部分这个问题,但是使用旧框架的开发者如果对任何标准的会话状态管理选项不满意,仍然需要一次又一次地重新发明自己的轮子。本文描述了一个自定义会话状态模块的实现,该模块取代了标准的 System.Web.SessionState.SessionStateModule
并使用自定义存储介质来存储会话数据。该实现与 System.Web
的内部机制紧密配合,并使用反射来获取非公开结构和动态生成 IL 代码。
背景
对于许多场景,OutOfProc 或基于 SQL Server 的状态管理引擎可能是一个不错的选择,但它们都有一些本文不在此讨论的缺点。如果自定义会话状态管理机制是客户唯一的选择呢?
ASP.NET 专家(请参阅 Patrick Y. Ng 编写的极具参考价值的“理解会话状态模式 + FAQ”)只建议了两种解决方案。第一种方法是替换会话状态模块,根据 Patrick 的说法,这项任务“可能非常繁琐”。我对此表示赞同:重写数百行经过充分测试的代码,而这些代码可能在下一个 .NET 版本中被完全废弃,似乎并不是一项有价值的工作。第二种解决方案是实现一个附加的 HTTP 模块,该模块与标准会话状态模块共存,订阅应用程序事件,并尝试处理(或者说,劫持)已经被标准模块处理过的会话状态结构。这种方法的主要缺点是:
- 与另一个正在运行的会话状态模块共享
HttpContext.Session
并不是一个直接的过程; - 会话数据在
AcquireRequestState
之后不会立即可用,这(也许不是主要问题,但仍然)违反了 ASP.NET 请求处理模型; - 我们实际上只需要一部分标准框架的功能,其余部分则白白消耗了机器资源。
一个理想的假设性解决方案是最大限度地利用现有的 SessionStateModule
代码,并完全排除 InProc/OutOfProc/SQL Server 状态管理器。让我们看看我们能让它接近到什么程度。
SessionStateModule - 如何工作
标准的 SessionStateModule
是一个实现 IHttpModule
接口的类,它执行以下操作:
- 订阅特定的 ASP.NET 应用程序事件;
- 使用 HTTP 请求信息创建或检索会话标识符;
- 将会话状态数据从存储移到当前 HTTP 上下文,再移回;
- 执行一些额外的状态维护任务(状态对象创建、删除等)。
所有与存储相关的任务都由实现 IStateClientManager
接口的对象执行。System.Web
提供了三种状态管理器对象,用于处理三种标准场景:InProc
、OutOfProc
、SqlServer
。下图展示了 InProc 情况下的状态管理。
添加自定义状态管理引擎 - 计划
从上图可以看出,最自然的解决方案显而易见 - 用你自己的类替换 InProcStateClientManager
,然后享受自定义 IStateClientManager
实现的自由和现有会话状态模块功能的强大。但这里存在一些问题。ASP.NET 的创建者不希望我们依赖他们的实现细节,也不允许我们设置指向状态客户端管理器对象的 _mgr
成员的值。此外,他们不公开 IStateClientManager
本身,并隐藏了 SessionStateModule
的所有成员,只留下 Init()
和 Dispose()
(这两个方法由 ASP.NET 框架调用)。
在传统的原生代码编译器、汇编语言、强大的调试器和反汇编器的时代,将新功能注入现有二进制文件的方法是,嗯……有点棘手:修补 EXE/DLL 文件中的可执行代码和数据,或者对运行中的进程进行一些手术。在托管世界中,我们有更好的方法来处理二进制文件:反射。我们来总结一下需要做什么:
- 创建一个标准
SessionStateModule
的包装类,允许访问其隐藏的成员。 - 创建一个
IStateClientManager
的实现,请记住 Visual Studio 无法访问System.Web
中的接口声明(它被声明为internal
)。 - 构建一个自定义
SessionStateModule
,该模块在初始化时创建一个状态客户端管理器对象的实例、一个标准会话状态模块的实例,并告知标准模块应使用我们的非标准管理器对象。自定义模块还应订阅应用程序事件并将其委托给标准模块。
以下各节将详细描述这些主要步骤。
SessionStateModule 包装类
创建包装类很容易。只需一次获取字段或成员信息,并在需要时调用 GetValue
/SetValue
/Invoke
。
public abstract class SystemWebClassWrapper
{
protected static Assembly _asmWeb = null;
protected object _o = null;
public SystemWebClassWrapper()
{
if ( _asmWeb == null )
{
_asmWeb = Assembly.GetAssembly(
typeof(System.Web.SessionState.SessionStateModule));
}
}
}
public class SessionStateModuleWrapper: SystemWebClassWrapper
{
private static Type _objType = null;
private static FieldInfo _mgrInfo = null;
private static MethodInfo _onBeginRequestInfo = null;
private static MethodInfo _beginAcquireStateInfo = null;
private static MethodInfo _endAcquireStateInfo = null;
private static MethodInfo _onReleaseStateInfo = null;
private static MethodInfo _onEndRequestInfo = null;
public SessionStateModuleWrapper()
{
if (_objType == null )
{
_objType = _asmWeb.GetType(
"System.Web.SessionState.SessionStateModule", true);
_mgrInfo = _objType.GetField(
"_mgr", BindingFlags.Instance | BindingFlags.NonPublic );
_onBeginRequestInfo = _objType.GetMethod(
"OnBeginRequest", BindingFlags.Instance | BindingFlags.NonPublic );
//
// Same for other event handlers...
//
}
}
public System.Web.SessionState.SessionStateModule InnerObject
{
get { return (System.Web.SessionState.SessionStateModule)_o; }
set { _o = value; }
}
public object _mgr
{
get { return _mgrInfo.GetValue( _o ); }
set { _mgrInfo.SetValue( _o, value ); }
}
public void OnBeginRequest(object source, EventArgs eventArgs)
{
object [] methodParams = new object [2] { source, eventArgs };
_onBeginRequestInfo.Invoke( _o, methodParams);
}
//
// Same for other event handlers...
//
}
稍后您将看到我们如何使用此包装器。在此之前,让我们弄清楚如何创建一个隐藏接口的实现。
State client manager 类 - 动态生成
让我们创建一个 IStateClientManager
的实现,它在 System.Web
中被声明为:
internal interface IStateClientManager
{
IAsyncResult BeginGet(string id, AsyncCallback cb, object state);
IAsyncResult BeginGetExclusive(string id,
AsyncCallback cb, object state);
void ConfigInit(SessionStateSectionHandler.Config config,
SessionOnEndTarget onEndTarget);
void Dispose();
SessionStateItem EndGet(IAsyncResult ar);
SessionStateItem EndGetExclusive(IAsyncResult ar);
void ReleaseExclusive(string id, int lockCookie);
void Remove(string id, int lockCookie);
void ResetTimeout(string id);
void Set(string id, SessionStateItem item, bool inStorage);
void SetStateModule(SessionStateModule module);
}
我们自定义模块程序集中定义的实现类可能如下所示。请注意,某些 StateClientManagerImp
的签名与 IStateClientManager
的签名略有不同。这是因为 System.Web
不公开相应的数据类型,因此我们必须将它们作为 object
传递。
public class StateClientManagerImp
{
public IAsyncResult BeginGetImp(string id, AsyncCallback cb, object state)
{ // Implementation...
}
public IAsyncResult BeginGetExclusiveImp(string id,
AsyncCallback cb, object state)
{ // Implementation...
}
//void ConfigInit(SessionStateSectionHandler.Config config,
// SessionOnEndTarget onEndTarget);
public void ConfigInitImp(object config, object onEndTarget)
{ // Implementation...
}
public void DisposeImp()
{ // Implementation...
}
//SessionStateItem EndGet(IAsyncResult ar);
public object EndGetImp(IAsyncResult ar)
{ // Implementation...
}
//SessionStateItem EndGetExclusive(IAsyncResult ar);
public object EndGetExclusiveImp(IAsyncResult ar)
{ // Implementation...
}
public void ReleaseExclusiveImp(string id, int lockCookie)
{ // Implementation...
}
public void RemoveImp(string id, int lockCookie)
{ // Implementation...
}
public void ResetTimeoutImp(string id)
{ // Implementation...
}
//void Set(string id, SessionStateItem item, bool inStorage);
public void SetImp(string id, object item, bool inStorage)
{ // Implementation...
}
public void SetStateModuleImp(SessionStateModule module)
{ // Implementation...
}
}
考虑一个名为 StateClientManagerFactory
的实用类,它执行以下操作:
- 从
System.Web
获取IStateClientManager
信息; - 在一个单独的动态程序集中,定义一个新的类类型
StateHijack.StateClientManager
,它继承自IStateClientManager
和上面描述的StateClientManagerImp
(该类实际上用“Imp”后缀实现了准接口方法); - 遍历所有接口方法并生成
StateHijack.StateClientManager
代码:每个StateHijack.StateClientManager
方法都必须调用父类StateClientManagerImp
的相应“Imp”方法。
public class StateClientManagerFactory
{
private static readonly OpCode[] _ldargCodes = new OpCode [4]
{
OpCodes.Ldarg_0,
OpCodes.Ldarg_1,
OpCodes.Ldarg_2,
OpCodes.Ldarg_3
};
private TypeBuilder _typeBuilder = null;
private Type _impType = null;
private Type _ifaceType = null;
public Type Create( string name,
ModuleBuilder modBld,
Type impType,
Type ifaceType )
{
_impType = impType;
_ifaceType = ifaceType;
// Define a new type in given module
_typeBuilder = modBld.DefineType( name, TypeAttributes.Public);
// Inherit given iface and implementation
_typeBuilder.AddInterfaceImplementation(ifaceType);
_typeBuilder.SetParent( impType );
// Get iface methods
MethodInfo[] ifaceMethods = ifaceType.GetMethods(
BindingFlags.Instance | BindingFlags.Public);
// Walk through iface methods and generate
// correspondent implementation methods
foreach( MethodInfo ifaceMethod in ifaceMethods)
{
ImplementIfaceMethod( ifaceMethod );
}
// Create the type
return _typeBuilder.CreateType();
}
private void ImplementIfaceMethod( MethodInfo ifaceMethodInfo )
{
// This method assumes that:
// - the number of parameters in iface and imp methods always match;
// - implementation method name = iface method name + "Imp" suffix
// - the number of parameters cannot exceed 3 (see _ldargCodes)
// Convert ParameterInfo array to Type array
ParameterInfo [] paramInfos = ifaceMethodInfo.GetParameters();
Type [] paramTypes = new Type[ paramInfos.Length ];
int paramIndex = 0;
foreach( ParameterInfo paramInfo in paramInfos)
{
paramTypes[paramIndex] = paramInfo.ParameterType;
paramIndex++;
}
// Define a new iface implementation method
MethodBuilder methodBld = _typeBuilder.DefineMethod(
ifaceMethodInfo.Name,
MethodAttributes.Public | MethodAttributes.Virtual,
ifaceMethodInfo.ReturnType,
paramTypes );
// Get "...Imp" method info
MethodInfo impMethodInfo = _impType.GetMethod(
ifaceMethodInfo.Name + "Imp",
BindingFlags.Instance | BindingFlags.Public);
// Generate code
ILGenerator methodBldIL = methodBld.GetILGenerator();
methodBldIL.Emit(OpCodes.Ldarg_0);
// Walk through parameter list and generate corresponding ldarg
for ( int index = 0; index < paramTypes.Length; index++)
{
methodBldIL.Emit( _ldargCodes[index + 1] );
}
// Generate call and graceful return
methodBldIL.EmitCall(OpCodes.Call, impMethodInfo, null);
methodBldIL.Emit(OpCodes.Ret);
// Mark this method as iface implementation
_typeBuilder.DefineMethodOverride(methodBld, ifaceMethodInfo );
}
}
结果是,我们有了 BeginGet()
方法的以下实现:
.method public virtual instance class [mscorlib]System.IAsyncResult BeginGet(
string A_1,
class [mscorlib]System.AsyncCallback A_2,
object A_3)
cil managed
{
.override [System.Web]System.Web.SessionState.IStateClientManager::BeginGet
// Code size 10 (0xa)
.maxstack 4
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: ldarg.2
IL_0003: ldarg.3
IL_0004: call instance class [mscorlib]System.IAsyncResult
[StateMirror.SessionStateModule]StateMirror.StateClientManagerImp::BeginGetImp(
string,
class [mscorlib]System.AsyncCallback,
object)
IL_0009: ret
} // end of method StateClientManager::BeginGet
自定义 SessionStateModule
我们几乎完成了。现在,我们必须实现一个自定义的 SessionStateModule
,它:
- 创建标准会话状态模块类的实例(使用上面描述的包装类);
- 创建自定义状态客户端管理器(最近生成的
StateClientManager
)的实例; - 模拟标准
SessionStateModule.Init()
的行为; - 使标准模块对象使用最近生成的
StateClientManager
; - 订阅应用程序事件并调用标准模块的处理程序。
考虑到这些任务,下面的代码基本上是自明的。可以使用 Lutz Roeder 的 .NET Reflector 工具获取初始化方法的“原始”版本。我们在这里所做的可以描述为“对标准会话状态模块进行细致的聚合以及部分重新实现”。
public class SessionStateModule: IHttpModule
{
private static Assembly _webAsm = null;
private static Type _mgrType = null;
private object _mgr = null;
private SessionStateModuleWrapper _origModuleWrapper = null;
private Type CreateStateClientManagerType()
{
AppDomain curDomain = Thread.GetDomain();
AssemblyName asmName = new AssemblyName();
asmName.Name = "StateHijack.StateClientManager";
AssemblyBuilder asmBuilder = curDomain.DefineDynamicAssembly(
asmName,
AssemblyBuilderAccess.RunAndSave);
ModuleBuilder modBuilder = asmBuilder.DefineDynamicModule(
"StateClientManager",
"StateHijack.StateClientManager.dll");
StateClientManagerFactory mgrFactory =
new StateClientManagerFactory();
Type retVal = mgrFactory.Create(
"StateClientManager",
modBuilder,
typeof(StateClientManagerImp),
_webAsm.GetType("System.Web.SessionState" +
".IStateClientManager", true) );
// You may want to save this generated assembly for
// testing (ildasm-ing and reflector-ing) purposes.
// asmBuilder.Save("StateHijack.StateClientManager.dll");
return retVal;
}
private void InitializeUnderlyingObjects()
{
if ( _webAsm == null )
{
_webAsm = Assembly.GetAssembly(
typeof(System.Web.SessionState.SessionStateModule));
// Generate our custom StateClientManager class
_mgrType = CreateStateClientManagerType();
}
if (_origModuleWrapper == null)
{
// Create an instance of the original SessionStateModule
_origModuleWrapper = new SessionStateModuleWrapper();
_origModuleWrapper.InnerObject =
new System.Web.SessionState.SessionStateModule();
// Create an instance of the newly generated StateClientManager class
_mgr = Activator.CreateInstance(_mgrType);
}
}
public void Init(HttpApplication app)
{
lock (this)
{
InitializeUnderlyingObjects();
// Mimic original SessionStateModule.Init behavior
ConfigWrapper config = new ConfigWrapper();
config.InnerObject = HttpContext.GetAppConfig("system.web/sessionState");
if (config.InnerObject == null)
{
config.Ctor();
}
InitModuleFromConfig(app, config.InnerObject, true);
// For OutOfProc and SQLServer, this call checks
// HttpRuntime.HasAspNetHostingPermission(
// AspNetHostingPermissionLevel.Medium);
if (!_origModuleWrapper.CheckTrustLevel(config.InnerObject))
{
_origModuleWrapper.s_trustLevelInsufficient = true;
}
_origModuleWrapper.s_config = config.InnerObject;
} // lock
if (_origModuleWrapper._mgr == null)
{
InitModuleFromConfig(app, _origModuleWrapper.s_config, false);
}
if (_origModuleWrapper.s_trustLevelInsufficient)
{
throw new HttpException("Session state need higher trust");
}
}
public void InitModuleFromConfig(
HttpApplication app,
object configObject,
bool configInit)
{
ConfigWrapper config = new ConfigWrapper();
config.InnerObject = configObject;
// Mimic original SessionStateModule.InitModuleFromConfig behavior
if (config._mode == SessionStateMode.Off)
{
return;
}
if (config._isCookieless)
{
// Cookieless functionality requires adding this handler
app.BeginRequest += new EventHandler( this.OnBeginRequest );
// Add session id to the path
HttpContextWrapper curContext = new HttpContextWrapper();
curContext.InnerObject = curContext.Current;
_origModuleWrapper.s_appPath =
curContext.InnerObject.Request.ApplicationPath;
if (_origModuleWrapper.s_appPath[
_origModuleWrapper.s_appPath.Length - 1] != '/')
{
_origModuleWrapper.s_appPath += "/";
}
_origModuleWrapper.s_iSessionId =
_origModuleWrapper.s_appPath.Length;
_origModuleWrapper.s_iRestOfPath =
_origModuleWrapper.s_iSessionId + 0x1a;
}
// Add event handlers
app.AddOnAcquireRequestStateAsync(
new BeginEventHandler(this.BeginAcquireState),
new EndEventHandler(this.EndAcquireState));
app.ReleaseRequestState +=(new EventHandler(this.OnReleaseState));
app.EndRequest +=(new EventHandler(this.OnEndRequest));
// Instead of analyzing config and choosing
// between InProc, OutOfProc, SQL etc,
// "patch" original SessionStateModule object, make _mgr point to
// our instance of StateClientManager. We could even provide wrappers
// for standard state client managers (InProc, OutOfProc, SqlServer)
// and create correspondent objects here
// (see that switch() statement in the
// original SessionStateModule.InitModuleFromConfig), but:
// - that would require some hacking
// on sessionState section handler, since
// "custom" sessionState managers are not supported in 1.1;
// - we do not have a goal to come up with a "better ASP.NET";
// - ASP.NET team has already done some part of the job in .NET 2.0
_origModuleWrapper._mgr = _mgr;
if (configInit)
{
// For the sake of consistency, call IStateClientManager.SetStateModule,
// but it does not do anything anyways, see comments within
MethodInfo setStateModuleInfo = _mgrType.GetMethod(
"SetStateModule",
BindingFlags.Instance | BindingFlags.Public );
object[] invokeParams = new object[1] { _origModuleWrapper.InnerObject };
setStateModuleInfo.Invoke( _mgr, invokeParams );
}
}
public void Dispose()
{
lock(this)
{
_mgr = null;
_origModuleWrapper = null;
}
_origModuleWrapper.InnerObject.Dispose();
}
private void OnBeginRequest(object source, EventArgs eventArgs)
{
_origModuleWrapper.OnBeginRequest( source, eventArgs );
}
private IAsyncResult BeginAcquireState(
object source, EventArgs e, AsyncCallback cb, object extraData)
{
return _origModuleWrapper.BeginAcquireState( source, e, cb, extraData );
}
private void EndAcquireState(IAsyncResult ar)
{
_origModuleWrapper.EndAcquireState( ar );
}
private void OnReleaseState(object source, EventArgs eventArgs)
{
_origModuleWrapper.OnReleaseState( source, eventArgs );
}
private void OnEndRequest(object source, EventArgs eventArgs)
{
_origModuleWrapper.OnEndRequest( source, eventArgs );
}
}
整合所有内容
类图现在看起来像这样:
只需将您的模块引用添加到 web.config 文件中,并确保它不干扰 machine.config 中的原始 httpModules
设置。
<httpModules>
<add name="Session" type="StateHijack.SessionStateModule, StateHijack"/>
</httpModules>
将您的自定义模块复制到应用程序的 bin 文件夹,然后享受自定义会话状态管理的好处。
源代码和示例项目
仅几点说明。
- 示例源代码中实现的状态客户端管理器使用临时目录来存储会话数据。一个会话状态对象 - 一个 *.ses 文件。这只是一个用于证明概念的示例实现。在实际部署中,这绝不是存储会话数据的正确方法。
- 项目中还有少量额外的包装类,它们允许我们访问
System.Web
没有完全公开的其他对象。这些对象包括:SessionStateSectionHandler+Config
、HttpAsyncResult
、SessionStateItem
和HttpContext
。 - 示例 ASP.NET 应用程序极其简单:它在每次回发时递增名为“
RefreshNum
”的会话变量。
进一步展望
以上所有内容均在 .NET 1.1 上进行了测试。讨论的方法也应该适用于 2.0,但我肯定会选择 ASP.NET 团队建议的会话状态存储提供程序模型。
通过 .NET 1.0 的快速反射器辅助侦察表明,StateSessionModule
的 Init()
和 InitModuleFromConfig()
实现略有不同,因此为了在 1.0 上正常工作,您可能需要模仿 1.0 的实现。不过,这似乎不是很多工作。毕竟,这就是那些走在不记录 API 的荆棘之路上的人的命运。