Sharing SQL Server Session State across Web Applications

点点圈 提交于 2019-11-30 07:06:24
SliverNinja - MSFT

The problem you have is sharing session state across ASP.NET applications. This is not supported out-of-the-box with SQL Server session provider. See this SO post regarding what changes to make to SQL Server session provider to support cross-application sessions.

I would not use session for managing this shared state (i.e. LastSiteUsed). This information should be persisted with the user profile store (Membership, Profile provider, custom DB, etc.). Since session can expire - using a shared session across applications isn't a reliable mechanism to track persistent user-specific state. If persistence isn't critical than utilize the AppFabric Cache for an in-memory cross-application shared state.

For anyone who wants or needs to solve this problem without modifying the database stored procedures, consider this approach (not for the faint-hearted).

The basic idea is that there is a static instance of SqlSessionStateStore.SqlPartitionInfo stored in System.Web.SessionState.SqlSessionStateStore.s_singlePartitionInfo that I initialise by calling InitSqlInfo. Once this instance is initialised, I set the value of the internal _appSuffix field. The instance needs to be initialised before setting the _appSuffix field, otherwise my custom value will be overwritten during initialisation. I calculate the hash code from the application name provided using the same hash function as in the ASP.NET state database.

Note that this is about as nasty as it gets, and is completely dependent on internal implementation details that may change at any point in time. However, I don't know of any practical alternative. Use at your own discretion and risk.

Now that I have given my disclaimer, I would be surprised if this implementation in the .NET BCL did change since this approach to authentication is being replaced and I don't see any reason for Microsoft to be tinkering in there.

/*
 * This code is a workaround for the fact that session state in ASP.NET SqlServer mode is isolated by application. The AppDomain's appId is used to
 * segregate applications, and there is no official way exposed to modify this behaviour.
 * 
 * Some workarounds tackle the problem by modifying the ASP.NET state database. This workaround approaches the problem from the application code
 * and will be appropriate for those who do not want to alter the database. We are using it during a migration process from old to new technology stacks, 
 * where we want the transition between the two sites to be seamless.
 * 
 * As always, when relying on implementation details, the reflection based approach used here may break in future / past versions of the .NET framework.
 * Test thoroughly.
 * 
 * Usage: add this to your Global.asax:
 *       protected void Application_BeginRequest()
 *       {
 *           SessionStateCrossApplicationHacker.SetSessionStateApplicationName("an application");
 *       }
 */

using System;
using System.Data.SqlClient;
using System.Globalization;
using System.Reflection;
using System.Web.SessionState;

public static class SessionStateCrossApplicationHacker
{
    static string _appName;
    static readonly object _appNameLock = new object();

    public static void SetSessionStateApplicationName(string appName)
    {
        if (_appName != appName)
        {
            lock (_appNameLock)
            {
                if (_appName != appName)
                {
                    SetSessionStateApplicationNameOnceOnly(appName);

                    _appName = appName;
                }
            }
        }
    }

    static void SetSessionStateApplicationNameOnceOnly(string appName)
    {
        //get the static instance of SqlSessionStateStore.SqlPartitionInfo from System.Web.SessionState.SqlSessionStateStore.s_singlePartitionInfo
        var sqlSessionStateStoreType = typeof (SessionStateMode).Assembly.GetType("System.Web.SessionState.SqlSessionStateStore");
        var sqlSessionStatePartitionInfoInstance = GetStaticFieldValue(sqlSessionStateStoreType, "s_singlePartitionInfo");
        if (sqlSessionStatePartitionInfoInstance == null)
            throw new InvalidOperationException("You'll need to call this method later in the pipeline - the session state mechanism (SessionStateModule, which is an IHttpModule) has not been initialised yet. Try calling this method in Global.asax's Application_BeginRequest. Also make sure that you do not specify a partitionResolverType in your web.config.");

        //ensure that the session has not been used prior to this with an incorrect app ID
        var isStaticSqlPartitionInfoInitialised = GetFieldValue<bool>(sqlSessionStatePartitionInfoInstance, "_sqlInfoInited");
        if (isStaticSqlPartitionInfoInitialised)
            throw new InvalidOperationException("You'll need to call this method earlier in the pipeline - before any sessions have been loaded.");

        //force initialisation of the static SqlSessionStateStore.SqlPartitionInfo instance - otherwise this will happen later and overwrite our change
        var connectionString = GetFieldValue<string>(sqlSessionStatePartitionInfoInstance, "_sqlConnectionString");
        using (var connection = new SqlConnection(connectionString))
        {
            connection.Open();
            CallInstanceMethod(sqlSessionStatePartitionInfoInstance, "InitSqlInfo", connection);
        }

        //calculate and set the application hash code
        string applicationNameHashCode = GetHashCode(appName).ToString("x8", CultureInfo.InvariantCulture);
        GetField(sqlSessionStatePartitionInfoInstance, "_appSuffix").SetValue(sqlSessionStatePartitionInfoInstance, applicationNameHashCode);
    }

    static int GetHashCode(string appName)
    {
        string s = appName.ToLower();
        int hash = 5381;
        int len = s.Length;

        for (int i = 0; i < len; i++)
        {
            int c = Convert.ToInt32(s[i]);
            hash = ((hash << 5) + hash) ^ c;
        }

        return hash;
    }

    static void CallInstanceMethod(object instance, string methodName, params object[] parameters)
    {
        var methodInfo = instance.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
        methodInfo.Invoke(instance, parameters);
    }

    static object GetStaticFieldValue(Type typeWithStaticField, string staticFieldName)
    {
        return typeWithStaticField.GetField(staticFieldName, BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
    }

    static FieldInfo GetField(object instance, string name)
    {
        return instance.GetType().GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
    }

    static T GetFieldValue<T>(object instance, string name)
    {
        return (T)GetField(instance, name).GetValue(instance);
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!