blazorblazor-server-sideblazor-webassembly.net-8.0

How to make webassembly component work in hosted Blazor web app project?


I literally did not add any of my own code, using only templates provided with VS2022, .NET 8.

There are the steps I took:

Step 1

Create Blazor web app "DemoServer", targeting .NET 8 using standard template "Blazor Web App".

Rendering mode in App.razor:

<head>
    <HeadOutlet @rendermode="InteractiveServer" /> 
</head>
<body>
    <Routes @rendermode="InteractiveServer" />
    <script src="_framework/blazor.web.js"></script>
</body>

Step 2

Create a web assembly project WasmFileEditor to serve wasm component. Add another project to the solution, based on the template "Blazor Webassembly App Empty".

Add Razor component FileEditor.razor:

<h3>File Editor Wasm Component</h3>
@code {
}

Add breakpoint in Program.cs, execute the project. Breakpoint is hit.

Step 3

Make following changes in WasmProject's Program.cs:

builder.Services.AddRazorComponents()
                .AddInteractiveServerComponents()
                .AddInteractiveWebAssemblyComponents();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(new[] { typeof(WasmFileEditor.FileEditor).Assembly });

<Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(WasmFileEditor.FileEditor).Assembly}">
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="routeData" Selector="h1" />
    </Found>
</Router>

In WasmFileEditor project:

Remaining code in Program.cs in the WasmFileEditor:

using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using WasmFileEditor;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

await builder.Build().RunAsync();

I get a run-time error when switching to Counter page where FileEditor component is added:

Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost[111] Unhandled exception in circuit bezeKQ89kvCeszvNMm1N8p1w6wo0QP_y3HZBEBM5_SU
System.NotSupportedException: Cannot create a component of type 'DemoServer.Components.Pages.Counter' because its render mode 'Microsoft.AspNetCore.Components.Web.InteractiveWebAssemblyRenderMode' is not supported by interactive server-side rendering.

Change in App.razor from this:

<body>
    <Routes @rendermode="InteractiveServer" />
    <script src="_framework/blazor.web.js"></script>
</body>

To this:

<body>
    <Routes />
    <script src="_framework/blazor.web.js"></script>
</body>

Page and component is shown, but the error is generated in console:

ManagedError: One or more errors occurred. (Root component type 'DemoServer.Components.Pages.Counter' could not be found in the assembly 'DemoServer'.)

Additionally, the breakpoint in Program.cs of the WasmFileEditor project where FileEditor component is located, is not hit.

What should I change to make it work?

Besides the error, I need to have HttpClient in the component, but Program.cs is not executed, so no DI available.


Solution

  • A minimal example:

    Project structure:

    enter image description here

    enter image description here

    Server

    Program.cs:

    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    builder.Services.AddRazorComponents()
        .AddInteractiveServerComponents()
        .AddInteractiveWebAssemblyComponents();
    builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("......") });
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Error", createScopeForErrors: true);
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    
    app.UseStaticFiles();
    app.UseAntiforgery();
    
    app.MapRazorComponents<App>()
        .AddInteractiveServerRenderMode()
        .AddInteractiveWebAssemblyRenderMode()
        .AddAdditionalAssemblies(typeof(BlazorAppWasm._Imports).Assembly)
    ;
    
    app.Run();
    

    app.razor:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <base href="/" />
        <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
        <link rel="stylesheet" href="app.css" />
        @if (server)
        {
            <link rel="stylesheet" href="BlazorAppServer.styles.css" />
        }
        else
        {
            <link rel="stylesheet" href="BlazorAppWasm.styles.css" />
        }
        
        <link rel="icon" type="image/png" href="favicon.png" />
        @if (server)
        {
            <HeadOutlet @rendermode="InteractiveServer" />
        }
        else{
            <HeadOutlet @rendermode="InteractiveWebAssembly" />
        }
    </head>
    
    <body>
        @if(server){
            <BlazorAppServer.Components.Routes @rendermode="InteractiveServer"/>
        }
        else{
            <BlazorAppWasm.Routes @rendermode="InteractiveWebAssembly" />
        }
        
        <script src="_framework/blazor.web.js"></script>
    </body>
    
    </html>
    
    @code {
        [CascadingParameter]
        private HttpContext HttpContext { get; set; } = default!;
    
        private bool server
           => HttpContext.Request.Path.ToString().Contains("wasm") ? false : true;
    
       
    }
    

    _imports.razor:

    @using System.Net.Http
    @using System.Net.Http.Json
    @using Microsoft.AspNetCore.Components.Forms
    @using Microsoft.AspNetCore.Components.Routing
    @using Microsoft.AspNetCore.Components.Web
    @using static Microsoft.AspNetCore.Components.Web.RenderMode
    @using Microsoft.AspNetCore.Components.Web.Virtualization
    @using Microsoft.JSInterop
    @using BlazorAppServer
    //notice adding codes below
    @using BlazorAppWasm
    @using BlazorAppServer.Components
    

    Routes.razor(same in server project and wasm project):

    <Router AppAssembly="typeof(Program).Assembly" >
        <Found Context="routeData">
            <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"  />
            <FocusOnNavigate RouteData="routeData" Selector="h1" />
        </Found>
    </Router>
    

    Wasm:

    Program.cs:

        var builder = WebAssemblyHostBuilder.CreateDefault(args);
    
    
    builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
    
    await builder.Build().RunAsync();
    

    modify the route of wasm components:

    @page "/home_wasm"
    @page "/counter_wasm"
    @page "/weather_wasm"
    

    Both server components and wasm components work well: enter image description here

    enter image description here

    Update :

    To enable debugging:

    add inspectUri to each profile in launchsettings.json

    For example:

    "https": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
      "applicationUrl": "https://localhost:7031;http://localhost:5054",
    
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    

    modify program.cs of your server project:

    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Error", createScopeForErrors: true);
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    else
    {
        app.UseWebAssemblyDebugging();
    }
    

    Now the breakpoint could be hit:

    enter image description here

    Update 1/3/2025: Register httpclient in wasm Project.cs:

    builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
    

    commented js invoke codes in your component for I couldn't find your js codes,it would work:

    enter image description here