Search code examples
.netasp.net-coresingleton.net-8.0httpcontext

ASP.NET Core 8 : singleton pattern with HttpContextAccessor does not work together


I'm developing an ASP.NET Core 8 application and using the singleton pattern for session management, but when I want to read the values, it displays null values!

Here you can see the code:

SessionSingleton.cs:

[Serializable]
public sealed class SessionSingleton : IDisposable
{
    #region Private Members
    private const string SessionSingletonName = "SessionKey_502E69E5-668B-E011-951F-00155DF26207";
    private static readonly object LockObject = new();
    #endregion

    #region Singleton
    private SessionSingleton()
    {
    }

    public static SessionSingleton Current
    {
        get
        {
            lock (LockObject)
            {
                string? value;
                SessionSingleton sessionSingleton;

                var session = CustomHttpContext.Current.Session;

                if (session == null)
                    return null;

                if (session.GetString(SessionSingletonName) is not null)
                {
                    value = session.GetString(SessionSingletonName);
                    sessionSingleton = JsonSerializer.Deserialize<SessionSingleton>(value!)!;
                }
                else
                {
                    sessionSingleton = new SessionSingleton();
                    var sessionText = JsonSerializer.Serialize(sessionSingleton);
                    session.SetString(SessionSingletonName, sessionText);
                }

                return sessionSingleton;
            }
        }
    }
    #endregion

    #region Public properties
    public string Name { get; set; }
    #endregion

    #region Dctor
    public void Dispose()
    {
         // HttpContext.Current.Session[SessionSingletonName] = null;
         // HttpContext.Current.Session.Remove(SessionSingletonName);
    }
    #endregion
}

CustomHttpContext:

public static class CustomHttpContext
{
    private static IHttpContextAccessor _httpContextAccessor = new HttpContextAccessor();

    public static HttpContext? Current
    {
        get
        {
            if (_httpContextAccessor == null)
                _httpContextAccessor = new HttpContextAccessor();

            return _httpContextAccessor.HttpContext;
        }
    }
}

HomeController:

public partial class HomeController : BaseController
{
    public IActionResult Index()
    {
        SessionSingleton.Current.Name = "Test";

        var value = SessionSingleton.Current.Name; // value is always null

        SessionSingleton.Current.Name = "Test2";

        value = SessionSingleton.Current.Name;// value is always null

        return Ok(true);
    }
}

All code like serialization and deserialization works fine, but why is value always null in HomeController?

Please suggest a way to store the user's information and to be able to access that information simply during the execution of the program by the user.


Solution

  • Finally, after various checks, I changed my code as below and it solved my need well.

    After many investigations, I realized that for any reason, using the combination of Singleton with HttpContextAccessor will not lead to the desired result.(Maybe because of the logic of asp.net internal middlewares)

    For this reason, I tried to meet my needs in a way that is both clean and extensible code.

    Therefore, I used the interface and put my fields in it so that I can easily implement it and also use the dependency injection feature.

    ISessionHandler

    public interface ISessionHandler : IDisposable
    {
        public int UserId { get; set; }
        public string Name { get; set; }
    }
    

    SessionManager

    internal sealed class SessionManager : ISessionHandler
    {
        #region Private Members
    
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly ISession _session;
    
        #endregion
    
        #region Ctor
        public SessionManager(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
            _session = _httpContextAccessor.HttpContext?.Session!;
        }
    
        #endregion
    
        #region Dctor
    
        public void Dispose()
        {
            _session.Clear();
        }
    
        #endregion
    
    
        public int UserId
        {
            get => _session.Get<int>();
            set => _session.Set(value);
        }
        public string Name
        {
            get => _session.Get<string>();
            set => _session.Set(value);
        }
    }
    

    Then I created a static helper class to set and get information of the session.

    SessionExtension

    internal static class SessionExtension
    {
        public static void Set<T>(this ISession session, string key, T value)
        {
            session.SetString(key, JsonSerializer.Serialize(value));
        }
        public static T Get<T>(this ISession session, string key)
        {
            var value = session.GetString(key);
            return value == null ? default! : JsonSerializer.Deserialize<T>(value)!;
        }
    
        public static void Set<T>(this ISession session, T value)
        {
            var key = GetKey();
            session.SetString(key, JsonSerializer.Serialize(value));
        }
        public static T Get<T>(this ISession session)
        {
            var key = GetKey();
            var value = session.GetString(key);
            return value == null ? default! : JsonSerializer.Deserialize<T>(value)!;
        }
    
        private static string GetKey()
        {
            var frame = new StackTrace().GetFrame(2);
            var instanceName = frame?.GetMethod()?.DeclaringType?.Name;
    
            var methodName = frame?.GetMethod()?.Name;
            var key = $"{instanceName}_{methodName}";
            return key.Replace("_set", "").Replace("_get", "");
        }
    }
    
    

    program.cs

    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddHttpContextAccessor();
    builder.Services.AddSingleton<ISessionHandler, SessionManager>();
    builder.Services.AddDistributedMemoryCache();
    
    builder.Services.AddSession(options =>
    {
        options.Cookie.Name = ".Test.Session";
        options.IdleTimeout = TimeSpan.FromMinutes(30);
        options.Cookie.IsEssential = true;
    });
    
    var app = builder.Build();
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseRouting();
    app.UseAuthorization();
    
    app.UseSession();
    
    app.UseRequestLocalization();
    app.MapDefaultControllerRoute();
    
    app.Run();
    

    And finally, as shown in the code below, I used it in my controller and wherever needed.

    LoginController

    public partial class LoginController : BaseController
    {
        public LoginController(ISessionHandler sessionHandler)
        {
        }
        public IActionResult Index()
        {
            SessionHandler.Name = "Test";
    
            var name = SessionHandler.Name; // It's OK
    
    
            return Ok();
        }
    }