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.
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.
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.
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.
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.
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.
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:
- In your server-side Blazor application project, add a package reference to
Microsoft.AspNetCore.ProtectedBrowserStorage
- 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>
- Finally, in the
ConfigureServices
method inStartup.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.
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.