How to store session data in server-side blazor
Asked Answered
C

9

55

In a server-side Blazor app I'd like to store some state that is retained between page navigation. How can I do it?

Regular ASP.NET Core session state does not seem to be available as most likely the following note in Session and app sate in ASP.NET Core applies:

Session isn't supported in SignalR apps because a SignalR Hub may execute independent of an HTTP context. For example, this can occur when a long polling request is held open by a hub beyond the lifetime of the request's HTTP context.

The GitHub issue Add support to SignalR for Session mentions that you can use Context.Items. But I have no idea how to use it, i.e. I don't know hot to access the HubConnectionContext instance.

What are my options for session state?

Catechetical answered 24/12, 2018 at 12:21 Comment(8)
you can register a scoped object in DI for keeping track of stateSkiplane
Are you sure it works? The page blazor.net/docs/dependency-injection.html says: Blazor doesn't currently have the concept of DI scopes. Scoped behaves like Singleton. Therefore, prefer Singleton and avoid Scoped.Catechetical
not sure - think i was getting mixed up with app stateSkiplane
I've tested DI with scoped. It does not behave like a singleton. Therefore, the description probably refers to client-side Blazor. However, it only lasts for a very short period, similar to the duration of request. It's sufficient to pass data along when navigating from one page to another. But after that, it's lost.Catechetical
@JohnB: After more testing I've found that scoped DI more or less works for session state. It lives longer than I originally thought. It is tied to the SignalR connection and stays alive as long as you don't reload the page or manually modify the URL. So it's a start but still far from what other systems offer.Catechetical
@Codo, yes, the description refers to client-side Blazor. In Razor Components you can use one of 3 options: singleton, scoped or transient. A Scoped service is scoped to a connection, which means that you'll continue to receive the same service instance for the duration of the session, which is considered the same request.Smallman
@Catechetical I think as of today your question is finally resolvedMagistery
@Catechetical I see the current docs say AddScoped is sufficient for server side Blazor. learn.microsoft.com/en-us/aspnet/core/blazor/…Stylistic
C
17

Note: This answer is from December 2018 when an early version of Server-side Blazor was available. Most likely, it is no longer relevant.

The poor man's approach to state is a hinted by @JohnB: Use a scoped service. In server-side Blazor, scoped service as tied to the SignalR connection. This is the closest thing to a session you can get. It's certainly private to a single user. But it's also easily lost. Reloading the page or modifying the URL in the browser's address list loads start a new SignalR connection, creates a new service instance and thereby loses the state.

So first create the state service:

public class SessionState
{
    public string SomeProperty { get; set; }
    public int AnotherProperty { get; set; }
}

Then configure the service in the Startup class of the App project (not server project):

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<SessionState>();
    }

    public void Configure(IBlazorApplicationBuilder app)
    {
        app.AddComponent<Main>("app");
    }
}

Now you can inject the state into any Blazor page:

@inject SessionState state

 <p>@state.SomeProperty</p>
 <p>@state.AnotherProperty</p>

Better solutions are still super welcome.

Catechetical answered 26/12, 2018 at 22:19 Comment(7)
@FranzHuber: I have since given up on Blazor. Possibly there's a better solution by now. Server-side Blazor could be very relevant for security-sensitive applications because it keeps the sensitive data on the server-side, e.g. a JWT authentication token. But if you store the state on the browser side like the Microsoft guy does with the Blazor Browser Storage package, you give up one of the main Blazor advantages.Catechetical
@Catechetical Do you think that this is still relevant for Blazor as well: learnrazorpages.com/razor-pages/session-state? Otherwise I will wait until Blazor is finally released and the docs are up-to-date.Linder
@FranzHuber23: I can't tell you as I'm no up-to-date anymore. I suspect it works for ASP.NET only, not for Blazor.Catechetical
Well, it works for Blazor, but can only be used properly on the server side (As far as I checked it yet). There's an issue on this on Github: github.com/aspnet/AspNetCore/issues/12432. Maybe they will update the documentation or provide an example.Linder
Please refer to the following repository for the server side session implementation: github.com/alihasan94/BlazorSessionAppAbc
So think about "what session real is?" I think there is no needed to more complicate it, and just store data in SessionOrWhatEverElse object. @AliHasan your example is the same as in response of Codo, but a bit extended.Corelative
with AddScoped<SessionState> SessionState will be recreated after browser refreshPlexor
C
13

Here is a relevant solution for ASP.NET Core 5.0+ (ProtectedSessionStorage, ProtectedLocalStorage): https://learn.microsoft.com/en-gb/aspnet/core/blazor/state-management?view=aspnetcore-5.0&pivots=server

An example:

@page "/"
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

User name: @UserName
<p/><input value="@UserName" @onchange="args => UserName = args.Value?.ToString()" />
<button class="btn btn-primary" @onclick="SaveUserName">Save</button>

@code {
    private string UserName;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        if (firstRender)
        {
            UserName = (await ProtectedSessionStore.GetAsync<string>("UserName")).Value ?? "";
            StateHasChanged();
        }
    }
    
    private async Task SaveUserName() {
        await ProtectedSessionStore.SetAsync("UserName", UserName);
    }
}

Note that this method stores data encrypted.

Conn answered 13/1, 2021 at 0:5 Comment(6)
ProtectedSessionStorage and ProtectedLocalStorage are great, It doesn't save data as plaintext and use encrypt/decrypt to save in browser storage. I don't know why people even thinking about use something else.Vivienviviene
This looks very similar to “Blazored.SessionStorage” by Chris Sainty except it keeps data encrypted and it is from Microsoft (no nugets needed)Seoul
@AliPoustdouzan The reason you would want to use something else is simple. Security. You see any injected JS code or browser extension can read the local and session storage. I need to store user credentials to where my blazor app gets info from another system. Storing in local or session means the data can be stolen and thus the session can be stolen from the user. This is why HTTP only cookies should be used. It can be stolen by any injected script or browser extension.Nagle
Does this works with multiple tabs? Not working in my case. Please share your feedbackGradygrae
@Gradygrae have u found your solution for multiple tabs ?Foist
ProtectedSessionStorage only uses async call so when dealing with layout that need to access session data stored in it makes it impossible. Async reading force double reload of all pages which is not a great behavior.Victualler
M
11

Steve Sanderson goes in depth how to save the state.

For server-side blazor you will need to use any storage implementation in JavaScript that could be cookies, query parameters or for example you can use local/session storage.

There currently NuGet packages implementing that via IJSRuntime like BlazorStorage or Microsoft.AspNetCore.ProtectedBrowserStorage

Now the tricky part is that server-side blazor is pre-rendering pages, so your Razor view code will be run and executed on a server before it's even displayed to the client's browser. This causes an issue where IJSRuntime and thus localStorage is not available at this time. You will need to either disable prerendering or wait for the server generated page to be sent to the client's browser and estabilish a connection back to the server

During prerendering, there is no interactive connection to the user's browser, and the browser doesn't yet have any page in which it can run JavaScript. So it's not possible to interact with localStorage or sessionStorage at that time. If you try, you'll get an error similar to JavaScript interop calls cannot be issued at this time. This is because the component is being prerendered.

To disable prerendering:

(...) open your _Host.razor file, and remove the call to Html.RenderComponentAsync. Then, open your Startup.cs file, and replace the call to endpoints.MapBlazorHub() with endpoints.MapBlazorHub<App>("app"), where App is the type of your root component and "app" is a CSS selector specifying where in the document the root component should be placed.

When you want to keep prerendering:

@inject YourJSStorageProvider storageProvider

    bool isWaitingForConnection;

    protected override async Task OnInitAsync()
    {
        if (ComponentContext.IsConnected)
        {
            // Looks like we're not prerendering, so we can immediately load
            // the data from browser storage
            string mySessionValue = storageProvider.GetKey("x-my-session-key");
        }
        else
        {
            // We are prerendering, so have to defer the load operation until later
            isWaitingForConnection = true;
        }
    }

    protected override async Task OnAfterRenderAsync()
    {
        // By this stage we know the client has connected back to the server, and
        // browser services are available. So if we didn't load the data earlier,
        // we should do so now, then trigger a new render.
        if (isWaitingForConnection)
        {
            isWaitingForConnection = false;
            //load session data now
            string mySessionValue = storageProvider.GetKey("x-my-session-key");
            StateHasChanged();
        }
    }

Now to the actual answer where you want to persist the state between pages you should use a CascadingParameter. Chris Sainty explains this as

Cascading values and parameters are a way to pass a value from a component to all of its descendants without having to use traditional component parameters.

This would be a parameter which would be a class that holds all your state data and exposes methods that can load/save via a storage provider of your choice. This is explained on Chris Sainty's blog, Steve Sanderson's note or Microsoft docs

Update: Microsoft has published new docs explaining Blazor's state management

Update2: Please note that currently BlazorStorage is not working correctly for server-side Blazor with the most recent .NET SDK preview. You can follow this issue where I posted a temporary workaround

Magistery answered 1/8, 2019 at 16:18 Comment(2)
Does ComponentContext still exist? I can't seem to find any mention of it.Affair
@JonathanAllen no, it has been removed and there is no alternative.Denyse
A
5

Here is a full code example of how you can use Blazored/LocalStorage to save session data. Used for example for storing the logged in user, etc. Confirmed working as of version 3.0.100-preview9-014004

@page "/login"
@inject Blazored.LocalStorage.ILocalStorageService localStorage

<hr class="mb-5" />
<div class="row mb-5">

    <div class="col-md-4">
        @if (UserName == null)
        {
            <div class="input-group">
                <input class="form-control" type="text" placeholder="Username" @bind="LoginName" />
                <div class="input-group-append">
                    <button class="btn btn-primary" @onclick="LoginUser">Login</button>
                </div>
            </div>
        }
        else
        {
            <div>
                <p>Logged in as: <strong>@UserName</strong></p>
                <button class="btn btn-primary" @onclick="Logout">Logout</button>
            </div>
        }
    </div>
</div>

@code {

    string UserName { get; set; }
    string UserSession { get; set; }
    string LoginName { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await GetLocalSession();

            localStorage.Changed += (sender, e) =>
            {
                Console.WriteLine($"Value for key {e.Key} changed from {e.OldValue} to {e.NewValue}");
            };

            StateHasChanged();
        }
    }

    async Task LoginUser()
    {
        await localStorage.SetItemAsync("UserName", LoginName);
        await localStorage.SetItemAsync("UserSession", "PIOQJWDPOIQJWD");
        await GetLocalSession();
    }

    async Task GetLocalSession()
    {
        UserName = await localStorage.GetItemAsync<string>("UserName");
        UserSession = await localStorage.GetItemAsync<string>("UserSession");
    }

    async Task Logout()
    {
        await localStorage.RemoveItemAsync("UserName");
        await localStorage.RemoveItemAsync("UserSession");
        await GetLocalSession();
    }
}
Afroasiatic answered 17/9, 2019 at 11:39 Comment(0)
C
5

I found a method for storing user data in a server-side session. I did this by using the CircuitHandler Id as a ‘token’ for the user to access the system. Only the Username and CircuitId gets stored in the client LocalStorage (using Blazored.LocalStorage); other user data is stored in the server. I know it's a lot of code, but this was the best way I could find to keep user data secure on the server-side.

UserModel.cs (for client-side LocalStorage)

public class UserModel
{
    public string Username { get; set; }

    public string CircuitId { get; set; }
}

SessionModel.cs (the model for my Server-side session)

public class SessionModel
{
    public string Username { get; set; }

    public string CircuitId { get; set; }

    public DateTime DateTimeAdded { get; set; }  //this could be used to timeout the session

    //My user data to be stored server side...
    public int UserRole { get; set; } 
    etc...
}

SessionData.cs (keeps a list of all active sessions on the server)

public class SessionData
{
    private List<SessionModel> sessions = new List<SessionModel>();
    private readonly ILogger _logger;
    public List<SessionModel> Sessions { get { return sessions; } }

    public SessionData(ILogger<SessionData> logger)
    {
        _logger = logger;
    }

    public void Add(SessionModel model)
    {
        model.DateTimeAdded = DateTime.Now;

        sessions.Add(model);
        _logger.LogInformation("Session created. User:{0}, CircuitId:{1}", model.Username, model.CircuitId);
    }

    //Delete the session by username
    public void Delete(string token)
    {
        //Determine if the token matches a current session in progress
        var matchingSession = sessions.FirstOrDefault(s => s.Token == token);
        if (matchingSession != null)
        {
            _logger.LogInformation("Session deleted. User:{0}, Token:{1}", matchingSession.Username, matchingSession.CircuitId);

            //remove the session
            sessions.RemoveAll(s => s.Token == token);
        }
    }

    public SessionModel Get(string circuitId)
    {
        return sessions.FirstOrDefault(s => s.CircuitId == circuitId);
    }
}

CircuitHandlerService.cs

public class CircuitHandlerService : CircuitHandler
{
    public string CircuitId { get; set; }
    public SessionData sessionData { get; set; }

    public CircuitHandlerService(SessionData sessionData)
    {
        this.sessionData = sessionData;
    }

    public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        CircuitId = circuit.Id;
        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }

    public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        //when the circuit is closing, attempt to delete the session
        //  this will happen if the current circuit represents the main window
        sessionData.Delete(circuit.Id); 

        return base.OnCircuitClosedAsync(circuit, cancellationToken);
    }

    public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        return base.OnConnectionDownAsync(circuit, cancellationToken);
    }

    public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        return base.OnConnectionUpAsync(circuit, cancellationToken);
    }
}

Login.razor

@inject ILocalStorageService localStorage
@inject SessionData sessionData
....
public SessionModel session { get; set; } = new SessionModel();
...
if (isUserAuthenticated == true)
{
    //assign the sesssion token based on the current CircuitId
    session.CircuitId = (circuitHandler as CircuitHandlerService).CircuitId;
    sessionData.Add(session);

    //Then, store the username in the browser storage
    //  this username will be used to access the session as needed
    UserModel user = new UserModel
    {
        Username = session.Username,
        CircuitId = session.CircuitId
    };

    await localStorage.SetItemAsync("userSession", user);
    NavigationManager.NavigateTo("Home");
}

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddServerSideBlazor();
    services.AddScoped<CircuitHandler>((sp) => new CircuitHandlerService(sp.GetRequiredService<SessionData>()));
    services.AddSingleton<SessionData>();
    services.AddBlazoredLocalStorage();
    ...
}
Castrate answered 11/3, 2021 at 11:17 Comment(6)
services.AddScoped<CircuitHandler> this is the best trick how to get current circuit ID, brilliant. Blazor Server scope is exactly per SignalR circuit so it could not be any better.Pallua
Attention! Be careful of using Lists in a Singleton (SessionData in this case). This, being a Blazor Server application, could lead to race conditions and also undesired behaviors. Please use ConcurrentDictionary or any other thread safe collection.Tetrapterous
@Tetrapterous Correct me if I'm wrong, but one could use a private static readonly SemaphoreSlim Lock = new SemaphoreSlim(1, 1); with a List or Dictionary. Lock.Wait(); before the try with the list or dictionary manipulation, then a finally with a Lock.Release();.Sombrero
@Sombrero yes, you could roll your own thread safe implementation,for example by using semaphores but I wouldn't since C# already has structures specially meant for that.Also consider the implications of locking the system that handles blazor's circuit life cycle... In the extreme case that you really need to customize it, please be aware of what you are really doing, specially when you are working with Blazor Server model.Tetrapterous
I'm using OAuth authentication and it seemed like the circuit id would reset to something else during a user's session. I just added a Claim with a new Guid to act as a "SessionID". So far this has been consistent for the duration of the user's session. This has helped enforce the prevention of multiple logins for a single user.Sombrero
@Tetrapterous I agree and ended up using a ConcurrentDictionary to keep the collection of active users.Sombrero
K
4

You can store data in sessions using Blazored.SessionStorage package.

Install Blazored.SessionStorage

`@inject Blazored.SessionStorage.ISessionStorageService sessionStorage` 

    @code {

    protected override async Task OnInitializedAsync()
    {
        await sessionStorage.SetItemAsync("name", "John Smith");
        var name = await sessionStorage.GetItemAsync<string>("name");
    }

}
Kandis answered 20/4, 2020 at 11:23 Comment(4)
Microsoft now has official documentation on this learn.microsoft.com/en-us/aspnet/core/blazor/…Lecce
@Lecce that's for client-side Blazor (WASM), the OP specifically says server-side.Unqualified
@Unqualified No it's not. At the top of the page it says "Choose a Blazor hosting model". Just pick the one you requireLecce
@Lecce Ha! I somehow completely missed that. Funny. Time for a vacation. Thanks.Unqualified
U
2

Don't use session state at all (I haven't tried, but I suspect AddSession doesn't even work under Blazor since the session ID is cookie-based and HTTP is mostly not in the picture). Even for non-Blazor web apps, there's no reliable mechanism for detecting the end of a session, so session cleanup is messy at best.

Instead, inject an implementation of IDistributedCache which supports persistence. One of the most popular examples is Redis cache. In one of my projects at work, I'm experimenting with the use of Microsoft Orleans for distributed caching. I'm not at liberty to share our in-house implementation but you can see an early example of this in my repo here.

Under the hood, session state is just a dictionary (keyed on the session ID) containing another dictionary of your key-value pairs. It's trivial to reproduce that approach using a long-term reliable key such as the authenticated user ID. I don't even go that far, though, since constantly serializing and deserializing an entire dictionary when I usually need just one or two keys is a lot of unnecessary overhead. Instead I prefix the individual value keys with my unique user IDs and store each value directly.

Unqualified answered 8/12, 2019 at 13:5 Comment(1)
Sadly this is the correct answer, roll your own. The other methods are Session Storage and Local Storage which is very limited storage that lives on the client web browser. That's only good for storing keys and such.Wenwenceslaus
G
2

With .net 5.0 you now have ProtectedSessionStorage which gives you encrypted browser session data.

@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; 
@inject ProtectedSessionStorage storage 

// Set   
await storage.SetAsync("myFlag", "Green");  
  
// Get  
var myFlag= await storage.GetAsync<string>("myFlag");

Uses JavaScript interops, so don't use in OnInitialize, but in OnAfterRender instead.

Grosz answered 1/4, 2021 at 5:47 Comment(1)
could you provide more info on how it is encrypted? by browser HTTPS certificate, or? I could not find any more info on thisPallua
A
1

Please refer to the following repository for the server side session implementation: https://github.com/alihasan94/BlazorSessionApp

On Login.razor page, write the following code:

@page "/"

@using Microsoft.AspNetCore.Http
@using Helpers;
@using Microsoft.JSInterop;


@inject SessionState session
@inject IJSRuntime JSRuntime
@code{

    public string Username { get; set; }
    public string Password { get; set; }
}

@functions {
    private async Task SignIn()
    {
        if (!session.Items.ContainsKey("Username") && !session.Items.ContainsKey("Password"))
        {
            //Add to the Singleton scoped Item
            session.Items.Add("Username", Username);
            session.Items.Add("Password", Password);
//Redirect to homepage
            await JSRuntime.InvokeAsync<string>(
            "clientJsMethods.RedirectTo", "/home");
        }
    }
}

<div class="col-md-12">
    <h1 class="h3 mb-3 font-weight-normal">Please Sign In</h1>
</div>

<div class="col-md-12 form-group">
    <input type="text" @bind="Username" class="form-control" id="username"
           placeholder="Enter UserName" title="Enter UserName" />
</div>

<div class="col-md-12 form-group">
        <input type="password" @bind="Password" class="form-control" id="password"
               placeholder="Enter Password" title="Enter Password" />
</div>


<button @onclick="SignIn">Login</button>

SessionState.cs

using System.Collections.Generic;

namespace BlazorSessionApp.Helpers
{
    public class SessionState
    {
        public SessionState()
        {
            Items = new Dictionary<string, object>();
        }
       public Dictionary<string, object> Items { get; set; }
    }
}

SessionBootstrapper.cs(Contains logic for setting session)

using Microsoft.AspNetCore.Http;

namespace BlazorSessionApp.Helpers
{
    public class SessionBootstrapper
    {
        private readonly IHttpContextAccessor accessor;
        private readonly SessionState session;
        public SessionBootstrapper(IHttpContextAccessor _accessor, SessionState _session)
        {
            accessor = _accessor;
            session = _session;
        }
        public void Bootstrap() 
        {
            //Singleton Item: services.AddSingleton<SessionState>(); in Startup.cs

            //Code to save data in server side session

            //If session already has data
            string Username = accessor.HttpContext.Session.GetString("Username");
            string Password = accessor.HttpContext.Session.GetString("Password");

            //If server session is null
            if (session.Items.ContainsKey("Username") && Username == null)
            {
                //get from singleton item
                Username = session.Items["Username"]?.ToString();
                // save to server side session
                accessor.HttpContext.Session.SetString("Username", Username);
                //remove from singleton Item
                session.Items.Remove("Username");
            }

            if (session.Items.ContainsKey("Password") && Password == null)
            {
                Password = session.Items["Password"].ToString();
                accessor.HttpContext.Session.SetString("Password", Password);
                session.Items.Remove("Password");
            }

            //If Session is not expired yet then  navigate to home
            if (!string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password) && accessor.HttpContext.Request.Path == "/")
            {
                accessor.HttpContext.Response.Redirect("/home");
            }
            //If Session is expired then navigate to login
            else if (string.IsNullOrEmpty(Username) && string.IsNullOrEmpty(Password) && accessor.HttpContext.Request.Path != "/")
            {
                accessor.HttpContext.Response.Redirect("/");
            }
        }
    }
}

_Host.cshtml(Initialize SessionBootstrapper class here)

@page "/"
@namespace BlazorSessionApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}
@using BlazorSessionApp.Helpers

@inject SessionBootstrapper bootstrapper

    <!DOCTYPE html>
    <html lang="en">
    <body>

        @{
            bootstrapper.Bootstrap();
        }
        <app>
            <component type="typeof(App)" render-mode="ServerPrerendered" />
        </app>

        <script src="_framework/blazor.server.js"></script>
        <script>
            // use this to redirect from "Login Page" only in order to save the state on server side session
            // because blazor's NavigateTo() won't refresh the page. The function below refresh 
            // the page and runs bootstrapper.Bootstrap(); to save data in server side session.
            window.clientJsMethods = {
              RedirectTo: function (path) {
                    window.location = path;
                }
            };
        </script>
    </body>
    </html>
Abc answered 15/12, 2019 at 13:9 Comment(1)
microsoft's docs say for security reasons, "you must not use IHttpContextAccessor within Blazor apps" here: learn.microsoft.com/en-us/aspnet/core/security/blazor/server/…Frederickson

© 2022 - 2024 — McMap. All rights reserved.