Hello Blazor - Compare the different Blazor projects

There two types of Blazor apps the Blazor Server App and Blazor WebAssembly (Wasm) App, yet there are several different ways to launch a Blazor Wasm app. In this post I want to look into the following project setups and see how each works. The source code of this post on GitHub.

  • Blazor Server App
  • Blazor Wasm App
  • Blazor Wasm App with “ASP.NET Core hosted”
  • Blazor Wasm App referenced by a Blazor Server App with _Host.cshtml loading the Wasm App

Server Model vs. Wasm Model

The two models (Server vs. Wasm) each has their pros and cons and good for their scenarios. I like the Wasm model better for it feels more like a true SPA, meaning you have an initial page load after which the rest of the communication between client and server is done by client hitting server API endpoints and any UI updates are done on the client. With this model your visitors could see an initial "Loading..." indicator on the SPA's first launch, although it'd be really nice if eventually the Wasm model could provide server-side rendering to make this initial "Loading..." indicator go away. The Server model is interesting in its own right because it presents a different approach by communicating through SignalR of any UI changes, the components instead of having a HttpClient they call the app's service layer directly for data.

The Blazor Hosting models doc has guidelines on when to use which model, I pick out the gist here.

 Blazor Server AppBlazor Wasm App
Pros
  • Download size is smaller, no initial "Loading..."
  • Full server capabilities
  • Existing tooling, e.g. debugging, works as expected
  • Works on browsers that don't support WASM yet
  • No server-side dependency
  • Client resources and capabilities are fully leveraged
  • Work is offloaded from the server to the client
  • Serverless deployment scenarios are possible
Cons
  • Higher network latency
  • No offline support
  • Scalability is challenging for apps with many users
  • Download size is larger
  • Browser must support WASM
  • Tooling support is less mature

Blazor Server App

File new project a Blazor Server App. This app runs server-side and handles user interactions over a SignalR connection. The project is an ASP.NET Core app, its .csproj shows

<TargetFramework>netcoreapp3.1</TargetFramework>

The Startup.cs looks like this

public class Startup
{
    ...
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
        services.AddServerSideBlazor();
        services.AddSingleton<WeatherForecastService>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        ...
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapBlazorHub();
            endpoints.MapFallbackToPage("/_Host");
        });
    }
}

In ConfigureServices it adds Server-Side Blazor services to the service collection, see implementation.

services.AddServerSideBlazor();

In Configure it maps the Blazor SignalR Hub to the default path. And it adds a specialized RouteEndpoint that will match requests for non-file-names with the lowest possible priority, which means it handles cases where URL path of the request does not contain a file name, and no other endpoint has matched. This is convenient for routing requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to result in an HTTP 404.

app.UseEndpoints(endpoints =>
{
    endpoints.MapBlazorHub();
    endpoints.MapFallbackToPage("/_Host");
});

The app's template file is _Host.cshtml with a script to establish SignalR connection.

<script src="_framework/blazor.server.js"></script>

When the app launches you don't see "Loading..." before you see the app.

Blazor Wasm App

File new project a Blazor Wasm App. This app runs on WebAssembly and handles user interactions via HttpClient calling to a server's API endpoints. Unlike the Server app, this is a .NET Standard app. Its .csproj shows

<TargetFramework>netstandard2.1</TargetFramework>

There is no Startup.cs only Program.cs in which it acquires a WebAssemblyHostBuilder and then adds the App component to root.

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.RootComponents.Add<App>("app");

        await builder.Build().RunAsync();
    }
}

The app's template file is index.html with a script to host the runtime to run the app.

<script src="_framework/blazor.webassembly.js"></script>

When the app launches you see an initial "Loading..." before you see the app.

Blazor Wasm App with “ASP.NET Core hosted”

File new project a Blazor Wasm App with the "ASP.NET Core hosted" checkbox checked. This will give you 3 projects

  • A client app
  • A server app to serve the client app
  • A shared class library between the server and client app

If you take a look at this server app's Startup.cs

In Configure it adds blazor debugging, client side blazor files, finally it adds endpoint that will match requests for non-file-names to index.html the host file in the client app.

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

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

    app.UseStaticFiles();
    app.UseClientSideBlazorFiles<Client.Program>();

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
    	endpoints.MapDefaultControllerRoute();
        endpoints.MapFallbackToClientSideBlazor<Client.Program>("index.html");
    });
}

This server app project is minimal with an API controller of a weather data endpoint for the client FetchData page to call. When the app launches you still see "Loading..." first.

Blazor Wasm App loads by a Blazor Server App

Finally, in this Steve Anderson's presentation he uses a Blazor Server App to reference and load a Blazor Wasm App. This is different than the above hosted wasm approach, his demo is meant to show how to keep one set of code to run it either as a Server App or Wasm App. I guess this technique is for for people who can't decide or want to support both models at the same time.

Essentially, in the Server app's _Host.cshtml make it load the Wasm app's App component. Then launch the app, you do not see "Loading..." because it's a Server app.

<component type="typeof(BlazorWasmApp.App)" render-mode="ServerPrerendered" />

First check out the Counter page and indeed when running from the Server app it uses SignalR. You can tell this by opening up the Chrome dev tool, go to Network tab and refresh the page, click on the row with _blazor?id= you will see the binary messages pass in and out.

Blazor Server App network messages
Blazor Server App network messages

Now, if you run the Wasm app, the Counter page runs off of .NET DLLs loaded in the browser and thus you do not see network communications happen.

However, check out the FetchData page and it does not work, it complains about HttpClient that's injected on the page cannot be found. This is because in the Wasm app's Program.cs, it calls WebAssemblyHostBuilder Build method which in turn registers HttpClient as a singleton

services.AddSingleton<HttpClient>(s =>
{
    // Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
    var navigationManager = s.GetRequiredService<NavigationManager>();
    return new HttpClient
    {
        BaseAddress = new Uri(navigationManager.BaseUri)
    };
});

but the Server app has not registered HttpClient yet. So, open up Server app's Startup.cs in ConfigureServices add this line at the end

services.AddSingleton<System.Net.Http.HttpClient>();

Then you find FetchData page still does not work, because of URL base is not set. Notice the Wasm version has its BaseUri set when it registers the HttpClient. The Server app does not have the base URI set, to fix it add the Server app's full URL to sample-data/weather.json

await Http.GetJsonAsync<WeatherForecast[]>("https://localhost:44303/sample-data/weather.json");

Note the weather.json file does not exist on Server app yet, so copy the sample-data folder from Wasm app to Server app. Now running from the Server app works. Unfortunately, when I go back and run the Wasm app as the startup project, it cannot find that endpoint because it points to the Server app's port.

So be aware when you want to support both Server and Wasm models, first you need all static assets to be on both Server app and Wasm app, secondly the URL needs adjustment when you switch. 

Final Thoughts

This has been my initial experiment with Blazor and I do like the Wasm app better. My goal is to have a Wasm app with server-rendering of its initial page, right now I don't see an elegant way to do this yet. Running Wasm from a Server app kinda gives met this effect but there are trade offs as well. Therefore I feel it's better to decide which model (server or wasm) to use and stick to it. Out of these 4 setups my current favorite is the Blazor Wasm App with “ASP.NET Core hosted”.