Search code examples
asp.net-coresession-variablesasp.net-core-identity

How to store an int userId in asp.net core razor website


Seriously struggling to have a decent solution to access an int userId value across multiple pages

So I am not an expert. Just when you feel competent a problem like this comes along.

I have a asp.net core razor web app project using Identity (using VS 2022). Identity is working but I really want an int userId rather than a string but totally gave up trying to modify Identity to use an int for userId rather than the default string.

Thus I have implemented a database table with all of my custom user properties that contains the Identity string userId and an int key. That works.

So there are multiple pages where I want to get the int userId.

In a razor page I can use dependency injection to inject the UserManager userManager. I could do that on every razor page where I need the int userId and use GetUserId and then lookup the int userId using a query with the string userId. However, that means replicating the code on every razor page where I need the userId so that is far from a nice solution. I thought I'd try to implement a static class to do it but that doesn't seem to be able to access httpContext.Session so that doesn't work (unless I've missed something).

To complicate matters I thought I could just store the int userId in Session but that will expire so whenever I read the Session variable I would need to check it isn't null and is a valid value and if it isn't refresh the session variable by getting the string userId, finding the int userId and saving it to the session variable. Again putting that code on every page isn't good.

However, if I create a class to do this it doesn't have the dbContext and I again run into the issue that if it is a static class it doesn't seem to have access to the Session.

This feels like it should be such a simple thing but I've got myself into one of those spiralling "what the heck do I do" conditions. I have tried done a lot googling but nothing seems to solve the problem


Solution

  • So there are multiple pages where I want to get the int userId.

    All Razor pages inherit from the class PageModel. In your scenario, set Razor pages that "get the int userid" to inherit from a custom page model, as suggested in this SO post.

    Inheriting from a base page model allows for centralizing code that would otherwise be repeated in each individual Razor page model.

    The inherited class provides access to: DI services, HttpContext, User, etc. In addition, Razor pages that inherit from the base page model can access properties defined in the custom base page model.

    The following sample shows how to set up an inherited base page model with dependency injection.

    Sample notes

    • The sample uses the IConfiguration DI service as a substitute for your scenario that requires access to your database service.

    • The property UserIdAsInt is defined in the inherited base page model. Note that the property is set in the OnGet() method of the Razor view page because the GetUserIdAsInt() method in the inherited class uses the HttpContext object. This object is not available in the constructor methods of the inherited class or the Razor code-behind class. HttpContext is available once code execution gets to the OnGet() method.

    MyBasePageModel.cs

    This C# class file can be inserted anywhere in your project. You'll just need a using statement in the code-behind .cshtml.cs if the namespace is anything other than [ProjectName].Pages.

    using Microsoft.AspNetCore.Mvc.RazorPages;
    
    namespace WebApplication1.Pages
    {
        public class MyBasePageModel : PageModel // Inherits from 'PageModel'
        {
            private readonly IConfiguration _configuration;
    
            public int UserIdAsInt { get; set; }
    
            public MyBasePageModel(IConfiguration configuration)
            {
                _configuration = configuration;
            }
    
            public int GetUserIdAsInt()
            {
                int? id = HttpContext.Session.GetInt32("UserIdAsInt");
                if (id == null)
                {
                    // Get user ID as integer from database
                    id = 100; // For sample purpose set 'id' variable to a hardcoded value
                    HttpContext.Session.SetInt32("UserIdAsInt", (int)id);
                    System.Diagnostics.Debug.WriteLine("User ID retrieved from database");
                }
                else
                {
                    System.Diagnostics.Debug.WriteLine("User ID retrieved from session");
                }
                return (int)id;
            }
    
            public string GetLoggingLevel()
            {
                return _configuration["Logging:LogLevel:Default"].ToString();
            }
        }
    }
    

    InheritFromBasePageModel.cshtml.cs

    namespace WebApplication1.Pages
    {
        public class InheritFromBasePageModelModel : MyBasePageModel // Inherits from the custom 'MyBasePageModel' class
        {
            public string? LoggingLevel { get; set; }
    
            public InheritFromBasePageModelModel(IConfiguration configuration)
                    : base(configuration: configuration)
            {
            }
    
            public void OnGet()
            {
                UserIdAsInt = GetUserIdAsInt();
                LoggingLevel = GetLoggingLevel();
            }
        }
    }
    

    InheritFromBasePageModel.cshtml

    @page
    @model WebApplication1.Pages.InheritFromBasePageModelModel
    @{
    }
    @section Styles {
        <style>
            p > span {
                font-weight: bold;
            }
        </style>
    }
    <p>Logging level property from base page model: <span>@Model.LoggingLevel</span></p>
    <p>Integer value: <span>@Model.UserIdAsInt</span></p>
    

    Program.cs

    // Configure session state
    // https://learn.microsoft.com/en-us/aspnet/core/fundamentals/app-state?view=aspnetcore-7.0#configure-session-state
    // https://stackoverflow.com/questions/71070698/session-in-asp-net-core-mvc
    // "add session middleware in your configuration file"
    builder.Services.AddSession(options =>
    {
        //options.IdleTimeout = TimeSpan.FromMinutes(1);   
    });
    
    app.UseSession();
    

    (Edit 11-Nov-2023)

    Using OnPageHandlerExecutionAsync() override

    The MyBasePageModel.cs base page model class above requires each Razor page that inherits from the base page model to add the line UserIdAsInt = GetUserIdAsInt(); in the OnGet() method.

    This SO post and article show how to execute a method for every OnGet() handler that inherits from a base page model without adding it to every Razor page.

    Here's a modified MyBasePageModel.cs class file that implements the OnPageHandlerExecutionAsync() override method.

    using Microsoft.AspNetCore.Mvc.Filters;
    using Microsoft.AspNetCore.Mvc.RazorPages;
    
    namespace WebApplication1.Pages
    {
        public class MyBasePageModel : PageModel
        {
            private readonly IConfiguration _configuration;
    
            public int UserIdAsInt { get; set; }
    
            public MyBasePageModel(IConfiguration configuration)
            {
                _configuration = configuration;
            }
    
            // https://stackoverflow.com/questions/55030251/razor-pages-call-method-from-base-class-after-all-onget-handlers
            // https://www.learnrazorpages.com/razor-pages/filters
            // "Model binding completed. Handler has not been executed."
            public async override Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
            {
                if (context.HandlerMethod?.MethodInfo.Name == "OnGet")
                {
                    // Code placed here will execute just before the OnGet()
                    // method is executed.
                    UserIdAsInt = await GetUserIdAsInt();
                }
                await base.OnPageHandlerExecutionAsync(context, next);
            }
    
            public async Task<int> GetUserIdAsInt()
            {
                int? id = HttpContext.Session.GetInt32("UserIdAsInt");
    
                // A valid user ID integer value exists in the 'Session'
                // object. Therefore, return this value.
                if (id != null)
                {
                    System.Diagnostics.Debug.WriteLine("User ID retrieved from session");
                    return (int)id;
                }
    
                // Get user ID as integer from database
    
                // 'Task.Run()' is used to simulate an async execution
                // of a database task.
                return await Task.Run(() =>
                {
                    id = 200; // For sample purpose set 'id' variable to a hardcoded value
                    HttpContext.Session.SetInt32("UserIdAsInt", (int)id);
                    System.Diagnostics.Debug.WriteLine("User ID retrieved from database");
                    return (int)id;
                });
            }
    
            public string? GetLoggingLevel()
            {
                if (_configuration == null) { return "(Configuration does not exist.)"; }
                return _configuration["Logging:LogLevel:Default"]?.ToString() ?? "Unset";
            }
        }
    }