ASP.NET Core 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 are the same.

To create a project for the hosting models described in this article, see Get started with ASP.NET Core Blazor.

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 the Blazor WebAssembly App template (dotnet new blazorwasm).

After selecting the Blazor WebAssembly App template, you have the option of configuring the app to use an ASP.NET Core backend by selecting the ASP.NET Core hosted check box (dotnet new blazorwasm --hosted). The ASP.NET Core app serves the Blazor app to clients. The Blazor client-side 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:

  • There's no .NET server-side dependency. The app is fully functioning after downloaded to the client.
  • Client resources and capabilities are fully leveraged.
  • Work is offloaded from the server to the client.
  • An ASP.NET Core web server isn't required to host the app. Serverless deployment scenarios are possible (for example, serving the app from a CDN).

There are downsides to client-side hosting:

  • The app is restricted to the capabilities of the browser.
  • Capable client hardware and software (for example, WebAssembly support) is required.
  • Download size is larger, and apps take longer to load.
  • .NET runtime and tooling support is less mature. For example, limitations exist 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 App template (dotnet new blazorserver). The ASP.NET Core app hosts the server-side app and creates 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.

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:

  • Download size is significantly smaller than a client-side app, and the app loads much faster.
  • The app takes full advantage of server capabilities, including use of any .NET Core compatible APIs.
  • .NET Core on the server is used to run the app, so existing .NET tooling, such as debugging, works as expected.
  • Thin clients are supported. For example, server-side apps work with browsers that don't support WebAssembly and on resource-constrained devices.
  • The app's .NET/C# code base, including the app's component code, isn't served to clients.

There are downsides to server-side hosting:

  • Higher latency usually exists. Every user interaction involves a network hop.
  • There's no offline support. If the client connection fails, the app stops working.
  • Scalability is challenging for apps with many users. The server must manage multiple client connections and handle client state.
  • An ASP.NET Core server is required to serve the app. Serverless deployment scenarios aren't possible (for example, serving the app from a CDN).

†The blazor.server.js script is served from an embedded resource in the ASP.NET Core shared framework.

Reconnection to the same server

Blazor server-side apps require an active SignalR connection to the server. If the 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 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 in the _Host.cshtml Razor page. 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 isn't rerendered after 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 }))
 
@code {
    [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.

@using Microsoft.JSInterop
@inject IJSRuntime JSRuntime

<input @ref="myInput" @ref:suppressField value="Value set during render" />

@code {
    private ElementReference myInput;

    protected override void OnAfterRender()
    {
        JSRuntime.InvokeAsync<object>(
            "setElementValue", myInput, "Value set after render");
    }
}

The following component demonstrates how to use JavaScript interop as part of a component's initialization logic in a way that's compatible with prerendering. The component shows that it's possible to trigger a rendering update from inside OnAfterRenderAsync. The developer must avoid creating an infinite loop in this scenario.

Where JSRuntime.InvokeAsync is called, ElementRef is only used in OnAfterRenderAsync and not in any earlier lifecycle method because there's no JavaScript element until after the component is rendered.

StateHasChanged is called to rerender the component with the new state obtained from the JavaScript interop call. The code doesn't create an infinite loop because StateHasChanged is only called when infoFromJs is null.

@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IComponentContext ComponentContext
@inject IJSRuntime JSRuntime

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(infoFromJs ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call:
    <input id="val-set-by-interop" @ref="myElem" @ref:suppressField />
</p>

@code {
    private string infoFromJs;
    private ElementReference myElem;

    protected override async Task OnAfterRenderAsync()
    {
        // TEMPORARY: Currently we need this guard to avoid making the interop
        // call during prerendering. Soon this will be unnecessary because we
        // will change OnAfterRenderAsync so that it won't run during the
        // prerendering phase.
        if (!ComponentContext.IsConnected)
        {
            return;
        }

        if (infoFromJs == null)
        {
            infoFromJs = await JSRuntime.InvokeAsync<string>(
                "setElementValue", myElem, "Hello from interop call");

            StateHasChanged();
        }
    }
}

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>

@code {
    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 Pages/_Host.cshtml 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>

Additional resources