Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Preserving State in Server-Side Blazor applications

Preserving State in Server-Side Blazor applications

Server-side Blazor is a stateful application framework. Most of the time, your users will maintain an ongoing connection to the server, and their state will be held in the server's memory in what's known as a "circuit". Examples of state held for a user's circuit include:

  • The UI being rendered (i.e., the hierarchy of component instances and their most recent render output)
  • The values of any fields and properties in component instances
  • Data held in DI service instances that are scoped to the circuit

Occasionally, users may experience a temporary network connection loss, after which Blazor will attempt to reconnect them to their original circuit so they can continue.

However, it is not always possible to reconnect users to their original circuit in the server's memory:

  • The server cannot retain disconnected circuits forever. It must release disconnected circuits after some timeout or when under memory pressure. The timeout and retention limits are configurable.
  • In multi-server (load balanced) deployment environments, the original server itself may no longer be available.
  • The user might manually close and reopen their browser, or simply reload the page. This will tear down any state held in the browser's memory, such as values set through JavaScript interop calls.

In these cases, the user will be given a new circuit that starts from an empty state. It is equivalent to closing and reopening a desktop application.

How to preserve state across circuits

Sometimes you may wish to preserve certain state across circuits. For example, if a user is building up contents in a shopping cart, you most likely want to retain the shopping cart's contents even if the web server goes down and the user's browser is forced to start a new circuit with a new web server. In general this applies to scenarios where users are actively creating data, not simply reading data that already exists.

To preserve state longer than a single circuit, don't just store it in the server's memory. You must persist it to some other storage location. This is not automatic; developers must take steps to make this happen.

It's up to you to choose which state must persist across circuits. Not all state needs this. It's typically needed only for high-value state that users have put in effort to create (such as the contents of a very complex multi-step form) or is commercially important (such as a shopping cart that represents potential revenue). It's most likely not necessary to preserve easily-recreated state, such as the username entered into a login dialog that hasn't yet been submitted.

Important: You can only persist application state. You cannot persist UIs themselves (such as actual component instances and their render trees), because components and render trees are not serializable in general. If you want to persist something that seems like UI state, such as which combination of nodes in a tree view are expanded, it's up to you to model that as serializable application state.

Where to persist state

There are three most common places to persist state in a server-side Blazor application. Each is best suited to different scenarios and has different caveats.

1. A server-side database

For any data you want to store permanently, or any data that must span multiple users or devices, you should almost certainly use some kind of server-side database. This could be a relational SQL database, a key-value store, a blob store, or something else - it's entirely independent of Blazor.

Once a user has saved some data to your database, it doesn't matter if the user starts a new circuit. The data will naturally be retained and available in the new circuit.

2. The URL

For any transient data that represents navigation state, it's best to model this as part of the URL. Examples of this state include the ID of an entity being viewed, or the current page number in a paged grid.

The contents of the browser's address bar will be retained if the user manually reloads the page, or if the web server goes down and the user is forced to reload to connect to a different server.

For more information about using the @page directive to define URL patterns, see documentation about routing.

3. Browser storage (localStorage/sessionStorage)

For any transient data that the user is actively creating, a common backing store is the browser's localStorage and sessionStorage collections. This has the advantage over server-side storage that you don't need to manage or clear it up if abandoned in any server-side database.

The two collections differ as follows:

  • localStorage is scoped to the user's browser. If they reload the page, or close and reopen the browser, the state will still be there. If they open multiple browser tabs, the same state is shared across them all.
  • sessionStorage is scoped to the user's browser tab. If they reload the tab, the state will still be there. But if the user closes the entire browser the state will be gone. If they open multiple browser tabs, each tab has its own independent version of the data.

Generally, using sessionStorage is safer, because it avoids the risk that a user opens multiple tabs and encounters bugs or confusing behavior because the tabs are overwriting each other's state. However if you want the data to be retained if the user closes the entire browser, you'll need to use localStorage.

Caveats for using browser storage:

  • Loading/saving is asynchronous (like a server-side database)
  • It's not available during prerendering (unlike a server-side database), because there no existing page in the browser during the prerendering HTTP request
  • You can easily store up to a few kilobytes of data in a single slot, but beyond this, you must consider performance implications because the data is loaded and saved across the network
  • Users may view or tamper with the data. Some aspects of this can be mitigated using Data Protection, as described below.

How to store data in localStorage/sessionStorage

Various third-party NuGet packages provide APIs for working with localStorage and sessionStorage in both server-side and client-side Blazor.

It's worth considering choosing a package that transparently uses ASP.NET Core's Data Protection features to encrypt the stored data and reduce the potential for tampering. If instead you simply store JSON-serialized data in plaintext, users can not only see that data (e.g., using the browser dev tools), but can even modify it arbitrarily. This is not always a problem, but could be depending on how your application uses the data.

An example of a NuGet package that provides Data Protection for localStorage/sessionStorage is Microsoft.AspNetCore.ProtectedBrowserStorage. Currently, this is an unsupported experimental package.

Installation

To install the Microsoft.AspNetCore.ProtectedBrowserStorage package:

  1. In your server-side Blazor application project, add a package reference to Microsoft.AspNetCore.ProtectedBrowserStorage
  2. In your top-level HTML (e.g., in the _Host.razor file in the default project template), add the following <script> tag:
<script src="_content/Microsoft.AspNetCore.ProtectedBrowserStorage/protectedBrowserStorage.js"></script>
  1. Finally, in the ConfigureServices method in Startup.cs, add the following method call:
services.AddProtectedBrowserStorage();

Saving and loading data within a component

In any component that needs to load or save data to browser storage, use @inject to inject an instance of either ProtectedLocalStorage or ProtectedSessionStorage, depending on which backing store you wish to use. For example,

@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

If you wish, you can put the @using statement into an _Imports.razor file instead.

Now, whenever the user performs an action that makes you want to store some data, you can use this service. For example, if you wanted to persist the currentCount value from the "counter" example in the project template, you could modify the IncrementCount method to use SetAsync as follows:

async Task IncrementCount()
{
    currentCount++;
    await ProtectedSessionStore.SetAsync("count", currentCount);
}

This is a very simple example. In larger and more realistic apps, you wouldn't just store individual int fields. You'd be more likely to store entire model objects that include complex state. ProtectedSessionStore will automatically JSON serialize/deserialize any data that you give to it.

The code above will cause the currentCount data to be stored as sessionStorage['count'] in the user's browser. If you evaluate that expression in the browser's developer console, you'll see that the data is not stored in plaintext but rather has been protected using ASP.NET Core's Data Protection feature.

Next, to recover this data if the user comes back to the same component later (including if they are now on an entirely new circuit), use GetAsync as follows:

protected override async Task OnInitializedAsync()
{
    currentCount = await ProtectedSessionStore.GetAsync<int>("count");
}

Note that you should instead do this from OnParametersSetAsync if your component's parameters include navigation state, since OnInitializedAsync will only be called once when the component is first instantiated, and won't be called again later if the user navigates to a different URL while remaining on the same page.

Warning: This example will only work if your server does not have prerendering enabled. If you do have prerendering enabled, you'll see an error similar to JavaScript interop calls cannot be issued at this time. This is because the component is being prerendered. You should either disable prerendering, or add further code to work with prerendering, as described in the notes below.

Handling the 'loading' state

Since browser storage is asynchronous (you're accessing it over a network connection), there will always be a period before the data is loaded. For best results, you should render a "loading" state while this is in progress instead of displaying blank or default data.

A simple way to do this is to track whether the data is null (i.e. still loading) or not. In the counter example, the count is held in an int, so you'd need to make this nullable. For example, change the definition of the currentCount field to:

int? currentCount;

Then, instead of displaying the count and "increment" button unconditionally, you can choose to display these only once the data has been loaded:

@if (currentCount.HasValue)
{
    <p>Current count: <strong>@currentCount</strong></p>

    <button @onclick="@IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

Handling prerendering

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.

One way to resolve this is to disable prerendering. That's often the best choice if your application makes heavy use of browser-based storage, since prerendering adds complexity and wouldn't benefit you anyway since you can't prerender any useful content until localStorage/sessionStorage become available. 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.

However, if you want to keep prerendering enabled, perhaps because it is useful on some other pages that don't use localStorage or sessionStorage, then you can defer the loading operation until the browser has connected to the circuit. Here's an example of doing this for storing a counter value:

@inject ProtectedLocalStorage ProtectedLocalStore
@inject IComponentContext ComponentContext

... rendering code goes here ...

@code {
    int? currentCount;
    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
            await LoadStateAsync();
        }
        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;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    async Task LoadStateAsync()
    {
        currentCount = await ProtectedLocalStore.GetAsync<int>("prerenderedCount");
    }

    async Task IncrementCount()
    {
        currentCount++;
        await ProtectedSessionStore.SetAsync("count", currentCount);
    }
}

Factoring out the state preservation into a common location

If you have many components that rely on browser-based storage, you probably don't want to reimplement the above pattern over and over, especially if you are dealing with the complexity of working with prerendering. A good option is to create a state provider component that encapsulates all this logic so that other components can simply work with the data without having to deal with it being loaded.

Here's an example of a state provider component for "counter" data:

@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

@if (hasLoaded)
{
    <CascadingValue Value="@this">
        @ChildContent
    </CascadingValue>
}
else
{
    <p>Loading...</p>
}

@functions {
    [Parameter] public RenderFragment ChildContent { get; set; }

    public int CurrentCount { get; set; }
    bool hasLoaded;

    protected override async Task OnInitAsync()
    {
        CurrentCount = await ProtectedSessionStore.GetAsync<int>("count");
        hasLoaded = true;
    }

    public async Task SaveChangesAsync()
    {
        await ProtectedSessionStore.SetAsync("count", CurrentCount);
    }
}

This component deals with the loading phase by not rendering its child content until loading is completed. To use this, wrap an instance of it around any component that will want to access this state. For example, to make the state accessible to all components in your application, wrap it around your Router in App.razor:

<CounterStateProvider>
    <Router AppAssembly="typeof(Startup).Assembly">
        ...
    </Router>
</CounterStateProvider>

Now any other component can receive and modify the persisted counter state trivially. A simple counter component could be implemented as follows:

@page "/counter"

<p>Current count: <strong>@CounterStateProvider.CurrentCount</strong></p>

<button @onclick="@IncrementCount">Increment</button>

@functions {
    [CascadingParameter] CounterStateProvider CounterStateProvider { get; set; }

    async Task IncrementCount()
    {
        CounterStateProvider.CurrentCount++;
        await CounterStateProvider.SaveChangesAsync();
    }
}

This component doesn't have to interact with ProtectedBrowserStorage, nor does it have to deal with any "loading" phase.

If you wanted, you could amend CounterStateProvider to deal with prerendering as described above, and then all components that consume this data would automatically work with prerendering with no further code changes.

In general, it's a good idea to follow this "state provider component" pattern if you will be consuming the state in many other components, and if there is just one top-level state object that you need to persist. If you need to persist many different state objects and consume different subsets of them in different places, it's better to avoid handling the loading/saving globally, so as to avoid loading or saving irrelevant data.

@GoranHalvarsson

This comment has been minimized.

Copy link

@GoranHalvarsson GoranHalvarsson commented Jul 13, 2019

Instead of using cascading, you could inject the state provider

@SteveSandersonMS

This comment has been minimized.

Copy link
Owner Author

@SteveSandersonMS SteveSandersonMS commented Jul 13, 2019

You could, but then it wouldn’t automatically trigger rendering when the data is loaded.

@GoranHalvarsson

This comment has been minimized.

Copy link

@GoranHalvarsson GoranHalvarsson commented Jul 13, 2019

Aha, I was not aware of that. Thanks for pointing that out

@jeffrey-caldwell

This comment has been minimized.

Copy link

@jeffrey-caldwell jeffrey-caldwell commented Jul 14, 2019

I needed this article a few months ago. As it is, I feel like this is the most informative article I've read on blazor. Looking forward to Monday and putting the knowledge to use. Thanks!

@texyh

This comment has been minimized.

Copy link

@texyh texyh commented Feb 17, 2020

i get this error in Production when using the ProectedBrowserStorage package.
[08:45:00 WRN] Unhandled exception rendering component: The key {64d212b3-673f-4986-a90f-adec2449bf20} was not found in the key ring. System.Security.Cryptography.CryptographicException: The key {64d212b3-673f-4986-a90f-adec2449bf20} was not found in the key ring. at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.UnprotectCore(Byte[] protectedData, Boolean allowOperationsOnRevokedKeys, UnprotectStatus& status) at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.DangerousUnprotect(Byte[] protectedData, Boolean ignoreRevocationErrors, Boolean& requiresMigration, Boolean& wasRevoked) at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.Unprotect(Byte[] protectedData) at Microsoft.AspNetCore.DataProtection.DataProtectionCommonExtensions.Unprotect(IDataProtector protector, String protectedData) at Microsoft.AspNetCore.ProtectedBrowserStorage.ProtectedBrowserStorage.GetAsync[T](String purpose, String key)

@SteveSandersonMS

This comment has been minimized.

Copy link
Owner Author

@SteveSandersonMS SteveSandersonMS commented Feb 17, 2020

@texyh That's an issue with how Data Protection is configured. For more details, please see data protection docs, or if you continue to have trouble with this, please consider posting an issue about data protection configuration at https://github.com/dotnet/aspnetcore/issues

@texyh

This comment has been minimized.

Copy link

@texyh texyh commented Feb 17, 2020

First of all, i cleared my browser localstorage and the error stopped(Any reason why?). Secondly i did not configure any DataProtection, i have read the docs in the link you sent, but it doesnt conform to what am doing. i think it might have to do with how ProtectedBrowserStorage package configure the DataProtection its using.

@LeeHollandSmith

This comment has been minimized.

Copy link

@LeeHollandSmith LeeHollandSmith commented Feb 17, 2020

@SteveSandersonMS. Two questions

  1. Is Microsoft.AspNetCore.ProtectedBrowserStorage intended to end up in production? It's still non production at the moment?
  2. Having created a production application in server side blazor using a scoped sessionState container. We have perfect user experience for page refresh, log out and log in and store the user session state on these events plus browser close. However, managing session state across multiple tabs is proving more challenging, as the only option for the session container is Scoped. Not keen to store an application wide dictionary of users to sessionState objects and not keen to have event such as 'recently visited' spamming API's for persistence on every page load (Even using an event source model) . . . Any suggestions?
@SteveSandersonMS

This comment has been minimized.

Copy link
Owner Author

@SteveSandersonMS SteveSandersonMS commented Feb 17, 2020

  1. We haven't made a commitment yet. It's under consideration for .NET 5 but may not make the cut since we have so many more highly-prioritised items.
  2. I'm not certain I follow the question. The two options that browsers offer are local storage and session storage, as summarised here, so you'll need to find a way of achieving your desired functionality using one of those two options. This library doesn't attempt to try to change the types of storage available. If you think that something in this library is restricting your options compared with what the browser provides natively, could you clarify? Thanks!
@LeeHollandSmith

This comment has been minimized.

Copy link

@LeeHollandSmith LeeHollandSmith commented Feb 17, 2020

Thanks for speedy response! Shame about point 1. Re point 2, thanks for clarifying. Will try and get creative with the options. (Not so easy given point 1 though).

@wmgdev

This comment has been minimized.

Copy link

@wmgdev wmgdev commented Feb 21, 2020

First of all, i cleared my browser localstorage and the error stopped(Any reason why?). Secondly i did not configure any DataProtection, i have read the docs in the link you sent, but it doesnt conform to what am doing. i think it might have to do with how ProtectedBrowserStorage package configure the DataProtection its using.

Maybe the same key is not getting persisted and reused after an App pool reset?
I set 'Load User Profile' to true in my IIS App pool advanced settings, then cleared localstorage in my browser, seemed to fix things

@texyh

This comment has been minimized.

Copy link

@texyh texyh commented Feb 21, 2020

First of all, i cleared my browser localstorage and the error stopped(Any reason why?). Secondly i did not configure any DataProtection, i have read the docs in the link you sent, but it doesnt conform to what am doing. i think it might have to do with how ProtectedBrowserStorage package configure the DataProtection its using.

Maybe the same key is not getting persisted and reused after an App pool reset?
I set 'Load User Profile' to true in my IIS App pool advanced settings, then cleared localstorage in my browser, seemed to fix things

i am running the app on AKS

@JasonBock

This comment has been minimized.

Copy link

@JasonBock JasonBock commented Mar 3, 2020

I tried referencing this NuGet package, but if I try to add this:

@using Microsoft.AspNetCore.ProtectedBrowserStorage

I get an error:

The type or namespace name 'ProtectedBrowserStorage' does not exist in the namespace 'Microsoft.AspNetCore' (are you missing an assembly reference?)

But I have this in my .csproj file:

<PackageReference Include="Microsoft.AspNetCore.ProtectedBrowserStorage" Version="0.1.0-alpha.19521.1" />

Any ideas why this wouldn't be working? The package reference is fine, meaning if I just reference the package in my project, it compiles successfully. But trying to use anything from that package doesn't seem to work.

@SteveSandersonMS

This comment has been minimized.

Copy link
Owner Author

@SteveSandersonMS SteveSandersonMS commented Mar 3, 2020

Are you sure the package restore completed correctly? Does “dotnet restore” in your project directory complete without errors?

@JasonBock

This comment has been minimized.

Copy link

@JasonBock JasonBock commented Mar 3, 2020

Yes, no issues with dotnet restore.

@DamianEdwards

This comment has been minimized.

Copy link

@DamianEdwards DamianEdwards commented Mar 6, 2020

Would be good to add details on how to use the querystring to this document too. Pushing state into the querystring is often preferred when dealing with free-form user input (e.g. a search form).

@ajai1109

This comment has been minimized.

Copy link

@ajai1109 ajai1109 commented Apr 2, 2020

System.Security.Cryptography.CryptographicException: 'The payload was invalid.' the error I am getting. This is happening when we save local storage with one version of the project and when we try to retrieve the data with another version of the same project.

@SteveSandersonMS

This comment has been minimized.

Copy link
Owner Author

@SteveSandersonMS SteveSandersonMS commented Apr 2, 2020

@ajai1109 Please see data protection docs, as that's responsible for the underlying encryption. In particular, docs about configuring keys are at https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-3.1. If you're unsure how to configure it, please consider posting an issue about data protection config at https://github.com/dotnet/aspnetcore/issues

@GioviQ

This comment has been minimized.

Copy link

@GioviQ GioviQ commented Apr 7, 2020

I tried referencing this NuGet package, but if I try to add this:

@using Microsoft.AspNetCore.ProtectedBrowserStorage

I get an error:

The type or namespace name 'ProtectedBrowserStorage' does not exist in the namespace 'Microsoft.AspNetCore' (are you missing an assembly reference?)

But I have this in my .csproj file:

<PackageReference Include="Microsoft.AspNetCore.ProtectedBrowserStorage" Version="0.1.0-alpha.19521.1" />

Any ideas why this wouldn't be working? The package reference is fine, meaning if I just reference the package in my project, it compiles successfully. But trying to use anything from that package doesn't seem to work.

I have the same issue only with project with TargetFramework netstandard2.1

@GioviQ

This comment has been minimized.

Copy link

@GioviQ GioviQ commented Apr 8, 2020

I retarget now Microsoft.AspNetCore.ProtectedBrowserStorage so I can use it server side, but in Blazor client I don't find example where to persist keys.
Best practices?

@spiralni

This comment has been minimized.

Copy link

@spiralni spiralni commented Apr 22, 2020

I am considering using this for a production environment? I've used it without any problem in my dev environment. It is safe to use, or you dont advice its use?

@swagfin

This comment has been minimized.

Copy link

@swagfin swagfin commented May 10, 2020

Most resourceful blazor Content i have ever read. THANK YOU

@Kattabomane

This comment has been minimized.

Copy link

@Kattabomane Kattabomane commented Jul 15, 2020

Hello, I have been experimenting this new component for storing data into session storage.
It seems it is not compatible with IE. Is there any workaround to make this possible ?
I am using server side blazor for IE compatbility.
Thanks.

image

@acpt

This comment has been minimized.

Copy link

@acpt acpt commented Oct 4, 2020

Couldn't there be something simpler like ServerVars... LoginVars ... to store and get variables ??
Why complicate ?
really ...
This is far from being good, still relaying on client to store information
Blazor should really solve this.
Dont get us technical problems, get us solutions

@ADefWebserver

This comment has been minimized.

Copy link

@ADefWebserver ADefWebserver commented Oct 4, 2020

@acpt If you don't want to store information on the client (the web browser) you would go with recommendation #1 "A server-side database".

@ajai1109

This comment has been minimized.

Copy link

@ajai1109 ajai1109 commented Oct 10, 2020

How to mock localstorage for unit testing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.