Skip to content

Instantly share code, notes, and snippets.

@SteveSandersonMS
Last active February 29, 2024 02:46
Show Gist options
  • Star 42 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save SteveSandersonMS/ba16f6bb6934842d78c89ab5314f4b56 to your computer and use it in GitHub Desktop.
Save SteveSandersonMS/ba16f6bb6934842d78c89ab5314f4b56 to your computer and use it in GitHub Desktop.
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.

Copy link

ghost commented Oct 29, 2021

I just ran into the error "...was not found in the key ring" in my production app, latest NET5 blazor server side.

Clearing localstorage fixed it - but it's a bit of deal breaker. Why is this happening?

@AzureGulf
Copy link

Excellent article and a step forward in my search for answers on how to handle a multi-user Blazor Server Azure hosted app with concurrent database access to an Azure SQL database and Azure Storage. Despite numerous searches, information on this remains "hidden"! Why? Are all Blazor apps single user? Doesn't anyone have users who might be reading and writing to the same database record at the same time? Entity Framework/SQL seems to blithely allow the same record (say a Customer Order) to be modified by two users at the same time, without reporting any lock issues, etc... Any pointers appreciated - thanks

@janseris
Copy link

janseris commented Dec 22, 2021

@SteveSandersonMS

ProtectedBrowserStorage has been released officially since this article was written.
However I cannot find any info or documentation on how exactly it works - how exactly is it "protected"?
I only found that it stores the data serialized as JSON and it works for Blazor Server and not for WASM.
Because it only works for Blazor Server and not WASM, I have concluded that the server encrypts and signs the data (protection against reading and detection modification) using its TLS certificate and stores it in the user's browser. Did I conclude it right? Is there any documentation on that? Does this mean that it only works via HTTPS?
Does it offer full XSS & CSRF protection -> advantage against common localStorage and sessionStorage when storing plaintext?

This could be great marketing of Blazor Server with new features against common JS web frameworks.

Thanks!

@elclon
Copy link

elclon commented Dec 29, 2021

The user experience is very bad with blazor server, now it cannot be possible to save every data that the user types in a localstorage or sessionstorage or worse still in a database on the server, it must be taken into account that if it is done that the database at some point will be congested and worse, imagine that the user closes the browser completely, how would it be done to release the records in the database? Truly the far-fetched solutions in the vast majority of times do not usually work! Couldn't it be possible that blazor server doesn't depend on SignalR connection? Now switching to blazor wasm would seem like an optimal solution, however, keep in mind that loading the first time is very slow and even worse if you have third-party components. I think that what they wanted to do with blazor was very good but I think that even this technology must change in certain aspects such as the dependence on SignalR in the case of Blazor server and in the case of wasm the download of the entire app in the browser. first time running!

@MartinThorsen
Copy link

MartinThorsen commented Mar 11, 2022

@SteveSandersonMS

Just stumbled across this and it will be very useful in our Blazor server-side app. However, I cannot for the life of me resolve IComponentContext. Note that the solution is against .NET 6.

Have not been able to find any info searching Stackoverflow etc, but I am a newbie to Blazor so that might be why.

Is IComponentContext still available? If not, what's the recommended way of doing this then?

And actually, I really need to do it our custom AuthenticationStateProvider, specifically in GetAuthenticationStateAsync.

@SteveSandersonMS
Copy link
Author

@MartinThorsen Could you post your question and more detailed goals at https://github.com/dotnet/aspnetcore/issues? Then the whole team may be able to answer.

@Robula
Copy link

Robula commented Aug 5, 2022

@ahjashish
Copy link

ahjashish commented Feb 11, 2023

Hi @SteveSandersonMS thank you for this great article. I used the knowledge here to create a state provider storing user metadata which includes the current country and some related ids in a multi country application. It worked flawlessly with .net 6 and ProtectedLocalStorage on local but as soon as I deploy it to k8s pods, I have this weird issue that the state provider is stuck at loading state and the child content is never loaded.

  • It was using server render mode earlier and had nothing being displayed on screen.
  • Switching back to pre rendering and moving the local storage code to OnAfterRenderAsync now loads only the loading state but never gets through the local storage call.
  • Has anyone faced this issue before or has some suggestions to debug. It’s a very weird issue that is only coming when deploying to k8s.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment