How to store session data in server-side blazor

44,242

Solution 1

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.

Solution 2

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

Solution 3

Here is a relevant solution for ASP.NET Core 5.0+ (ProtectedSessionStorage, ProtectedLocalStorage): https://docs.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.

Solution 4

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();
    }
}

Solution 5

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");
    }

}
Share:
44,242
Codo
Author by

Codo

Updated on October 14, 2021

Comments

  • Codo
    Codo over 2 years

    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?

  • Codo
    Codo almost 5 years
    @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.
  • FranzHuber23
    FranzHuber23 almost 5 years
    @Codo 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.
  • Codo
    Codo almost 5 years
    @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.
  • FranzHuber23
    FranzHuber23 almost 5 years
    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.
  • Jonathan Allen
    Jonathan Allen over 4 years
    Does ComponentContext still exist? I can't seem to find any mention of it.
  • Ali Hasan
    Ali Hasan over 4 years
    Please refer to the following repository for the server side session implementation: github.com/alihasan94/BlazorSessionApp
  • Kamil
    Kamil over 4 years
    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.
  • Bin4ry
    Bin4ry about 4 years
    @JonathanAllen no, it has been removed and there is no alternative.
  • Jesper
    Jesper almost 4 years
    Microsoft now has official documentation on this docs.microsoft.com/en-us/aspnet/core/blazor/…
  • Daniel
    Daniel almost 4 years
    microsoft's docs say for security reasons, "you must not use IHttpContextAccessor within Blazor apps" here: docs.microsoft.com/en-us/aspnet/core/security/blazor/server/‌​…
  • Jason Honingford
    Jason Honingford over 3 years
    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.
  • McGuireV10
    McGuireV10 over 3 years
    @Jesper that's for client-side Blazor (WASM), the OP specifically says server-side.
  • Jesper
    Jesper over 3 years
    @McGuireV10 No it's not. At the top of the page it says "Choose a Blazor hosting model". Just pick the one you require
  • McGuireV10
    McGuireV10 over 3 years
    @Jesper Ha! I somehow completely missed that. Funny. Time for a vacation. Thanks.
  • Ali Poustdouzan
    Ali Poustdouzan over 2 years
    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.
  • user3625699
    user3625699 over 2 years
    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.
  • user3625699
    user3625699 over 2 years
    could you provide more info on how it is encrypted? by browser HTTPS certificate, or? I could not find any more info on this
  • Theo
    Theo almost 2 years
    This looks very similar to “Blazored.SessionStorage” by Chris Sainty except it keeps data encrypted and it is from Microsoft (no nugets needed)