Blazor hosting models

By Daniel Roth

Blazor is a web framework designed to run client-side in the browser on a WebAssembly-based .NET runtime (Blazor client-side) or server-side in ASP.NET Core (Blazor server-side). Regardless of the hosting model, the app and component models remain the same.

Client-side

The principal hosting model for Blazor is running client-side in the browser on WebAssembly. The Blazor app, its dependencies, and the .NET runtime are downloaded to the browser. The app is executed directly on the browser UI thread. UI updates and event handling occur within the same process. The app's assets are deployed as static files to a web server or service capable of serving static content to clients.

Blazor client-side: The Blazor app runs on a UI thread inside the browser.

To create a Blazor app using the client-side hosting model, use either of the following templates:

  • Blazor (dotnet new blazor) – Deployed as a set of static files.
  • Blazor (ASP.NET Core Hosted) (dotnet new blazorhosted) – Hosted by an ASP.NET Core server. The ASP.NET Core app serves the Blazor app to clients. The client-side Blazor app can interact with the server over the network using web API calls or SignalR.

The templates include the blazor.webassembly.js script that handles:

  • Downloading the .NET runtime, the app, and the app's dependencies.
  • Initialization of the runtime to run the app.

The client-side hosting model offers several benefits. Client-side Blazor:

  • Has no .NET server-side dependency.
  • Fully leverages client resources and capabilities.
  • Offloads work from the server to the client.
  • Supports offline scenarios.

There are downsides to client-side hosting. Client-side Blazor:

  • Restricts the app to the capabilities of the browser.
  • Requires capable client hardware and software (for example, WebAssembly support).
  • Has a larger download size and longer app load time.
  • Has less mature .NET runtime and tooling support (for example, limitations in .NET Standard support and debugging).

Server-side

With the server-side hosting model, the app is executed on the server from within an ASP.NET Core app. UI updates, event handling, and JavaScript calls are handled over a SignalR connection.

The browser interacts with the app (hosted inside of an ASP.NET Core app) on the server over a SignalR connection.

To create a Blazor app using the server-side hosting model, use the ASP.NET Core Blazor (server-side) template (dotnet new blazorserverside). The ASP.NET Core app hosts the server-side app and sets up the SignalR endpoint where clients connect.

The ASP.NET Core app references the app's Startup class to add:

  • Server-side services.
  • The app to the request handling pipeline.
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddServerSideBlazor<App.Startup>();

        services.AddResponseCompression(options =>
        {
            options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
            {
                MediaTypeNames.Application.Octet,
                WasmMediaTypeNames.Application.Wasm,
            });
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseResponseCompression();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // Use component registrations and static files from the app project.
        app.UseServerSideBlazor<App.Startup>();
    }
}

The blazor.server.js script† establishes the client connection. It's the app's responsibility to persist and restore app state as required (for example, in the event of a lost network connection).

The server-side hosting model offers several benefits:

  • Significantly smaller app size than a client-side app and load much faster.
  • Take full advantage of server capabilities, including using any .NET Core compatible APIs.
  • Run on .NET Core on the server, so existing .NET tooling, such as debugging, works as expected.
  • Works with thin clients (for example, browsers that don't support WebAssembly and resource constrained devices).
  • .NET/C# code base, including the app's component code, isn't served to clients.

There are downsides to server-side hosting:

  • Higher latency: Every user interaction involves a network hop.
  • No offline support: If the client connection fails, the app stops working.
  • Reduced scalability: The server must manage multiple client connections and handle client state.
  • An ASP.NET Core server is required to serve the app. Deployment without a server (for example, from a CDN) isn't possible.

†The blazor.server.js script is published to the following path: bin/{Debug|Release}/{TARGET FRAMEWORK}/publish/{APPLICATION NAME}.App/dist/_framework.

Reconnection to the same server

Blazor server-side apps require an active SignalR connection to the server. If a connection is lost, the app attempts to reconnect to the server. As long as the client's state is still in memory, the client session resumes without losing any state.

When the client detects that the connection has been lost, a default UI is displayed to the user while the client attempts to reconnect. If reconnection fails, the user is provided the option to retry. To customize the UI, define an element with components-reconnect-modal as its id. The client updates this element with one of the following CSS classes based on the state of the connection:

  • components-reconnect-show – Show the UI to indicate the connection was lost and the client is attempting to reconnect.
  • components-reconnect-hide – The client has an active connection, hide the UI.
  • components-reconnect-failed – Reconnection failed. To attempt reconnection again, call window.Blazor.reconnect().

Stateful reconnection after prerendering

Blazor server-side apps are set up by default to prerender the UI on the server before the client connection to the server is established. This is set up in the _Host.cshtml Razor page:

<body>
    <app>@(await Html.RenderComponentAsync<App>())</app>
 
    <script src="_framework/blazor.server.js"></script>
</body>

The client reconnects to the server with the same state that was used to prerender the app. If the app's state is still in memory, the component state doesn't need to be rerendered once the SignalR connection is established.

Render stateful interactive components from Razor pages and views

Stateful interactive components can be added to a Razor page or view. When the page or view renders, the component is prerendered with it. The app then reconnects to the component state once the client connection is established as long as the state is still in memory.

For example, the following Razor page renders a Counter component with an initial count that's specified using a form:

<h1>My Razor Page</h1>

<form>
    <input type="number" asp-for="InitialCount" />
    <button type="submit">Set initial count</button>
</form>
 
@(await Html.RenderComponentAsync<Counter>(new { InitialCount = InitialCount }))
 
@functions {
    [BindProperty(SupportsGet=true)]
    public int InitialCount { get; set; }
}

Detect when the app is prerendering

While a Blazor server-side app is prerendering, certain actions, such as calling into JavaScript, aren't possible because a connection with the browser hasn't been established. Components may need to render differently when prerendered.

To delay JavaScript interop calls until after the connection with the browser is established, you can use the OnAfterRenderAsync component lifecycle event. This event is only called after the app is fully rendered and the client connection is established.

To conditionally render different content based on whether the app is currently prerendering content, use the IsConnected property on the IComponentContext service. When running server-side, IsConnected only returns true if there's an active connection to the client. It always returns true when running client-side.

@page "/isconnected-example"
@using Microsoft.AspNetCore.Components.Services
@inject IComponentContext ComponentContext

<h1>IsConnected Example</h1>

<p>
    Current state:
    <strong id="connected-state">
        @(ComponentContext.IsConnected ? "connected" : "not connected")
    </strong>
</p>

<p>
    Clicks:
    <strong id="count">@count</strong>
    <button id="increment-count" onclick="@(() => count++)">Click me</button>
</p>

@functions {
    private int count;
}

Configure the SignalR client for Blazor server-side apps

Sometimes, you need to configure the SignalR client used by Blazor server-side apps. For example, you might want to configure logging on the SignalR client to diagnose a connection issue.

To configure the SignalR client in the wwwroot/index.htm file:

  • Add an autostart="false" attribute to the <script> tag for the blazor.server.js script.
  • Call Blazor.start and pass in a configuration object that specifies the SignalR builder:
<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
  Blazor.start({
    configureSignalR: function (builder) {
      builder.configureLogging(2); // LogLevel.Information
    }
  });
</script>

Improved SignalR connection lifetime handling

Automatic reconnects can be enabled by calling the withAutomaticReconnect method on HubConnectionBuilder:

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chatHub")
    .withAutomaticReconnect()
    .build();

Without specifying parameters, withAutomaticReconnect configures the client to try to reconnect, waiting 0, 2, 10, and 30 seconds between each attempt.

To configure a non-default number of reconnect attempts before failure or to change the reconnect timing, withAutomaticReconnect accepts an array of numbers representing the delay in milliseconds to wait before starting each reconnect attempt.

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chatHub")
    .withAutomaticReconnect([0, 0, 2000, 5000]) // defaults to [0, 2000, 10000, 30000]
    .build();

Improved disconnect and reconnect handling

Before starting any reconnect attempts, the HubConnection transitions to the Reconnecting state and fires its onreconnecting callback. This provides an opportunity to warn users that the connection was lost, disable UI elements, and mitigate confusing user scenarios that might occur due to the disconnected state.

connection.onreconnecting((error) => {
  console.assert(connection.state === signalR.HubConnectionState.Reconnecting);

  document.getElementById("messageInput").disabled = true;

  const li = document.createElement("li");
  li.textContent = `Connection lost due to error "${error}". Reconnecting.`;
  document.getElementById("messagesList").appendChild(li);
});

If the client successfully reconnects within its first four attempts, the HubConnectiontransitions back to the Connected state and fires onreconnected callbacks. This gives developers an opportunity to inform users that the connection is re-established.

connection.onreconnected((connectionId) => {
  console.assert(connection.state === signalR.HubConnectionState.Connected);

  document.getElementById("messageInput").disabled = false;

  const li = document.createElement("li");
  li.textContent = `Connection reestablished. Connected with connectionId "${connectionId}".`;
  document.getElementById("messagesList").appendChild(li);
});

If the client doesn't successfully reconnect within its first four attempts, the HubConnection transitions to the Disconnected state and fires its onclosed callbacks. This is a good opportunity to inform users that the connection is permanently lost and recommend refreshing the page.

connection.onclose((error) => {
  console.assert(connection.state === signalR.HubConnectionState.Disconnected);

  document.getElementById("messageInput").disabled = true;

  const li = document.createElement("li");
  li.textContent = `Connection closed due to error "${error}". Try refreshing this page to restart the connection.`;
  document.getElementById("messagesList").appendChild(li);
})

Additional resources