Blazor WebAssembly and Auto
Keep the widget on the server, or run it in a WebAssembly or Auto client with Novior.MentorAgent.Blazor, backed by Novior.MentorAgent.Server.
On this page
This page is for a standalone Blazor WebAssembly app, and for a Blazor Web App with Auto or WebAssembly interactivity. In such an app the agent always runs on the server. The widget can stay on the server too (option A), or run in the browser and reach the agent over SignalR (option B).
When to choose it
Section titled “When to choose it”Option A: keep the widget on the server
Section titled “Option A: keep the widget on the server”Choose it for a Blazor Web App with Auto or WebAssembly interactivity whose layout is rendered statically: Interactivity location: Per page/component in the template. Only the widget is pinned to Server interactivity, that is, it gets its own InteractiveServer render mode. The rest of the app keeps its render mode. You need neither Novior.MentorAgent.Server nor Novior.MentorAgent.Blazor. An app created with WebAssembly interactivity also needs Server interactivity turned on.
Option B: run the widget in WebAssembly
Section titled “Option B: run the widget in WebAssembly”Choose it for a standalone Blazor WebAssembly app, for a Blazor Web App with Interactivity location: Global, or when the widget must run in the browser. The server project gets Novior.MentorAgent.Server, which adds the SignalR hub; the client project gets Novior.MentorAgent.Blazor, which gives the same <ChatWidget /> a hub connection. The model key never reaches the browser, and the agent's features (specialists, RAG, MCP, A2A, memory, confirmation) all run on the server.
For a desktop app see .NET MAUI; for a JavaScript or mobile client, Any front end.
Install the packages
Section titled “Install the packages”Option A. The server project needs Novior.MentorAgent and the package of your model provider, as on Blazor Server.
Option B. In the server project, Novior.MentorAgent.Server brings Novior.MentorAgent with it:
.NET CLI
dotnet add package Novior.MentorAgent.ServerPackageReference
<ItemGroup>
<PackageReference Include="Novior.MentorAgent.Server" Version="1.0.0" />
</ItemGroup>Central Package Management
<ItemGroup>
<PackageVersion Include="Novior.MentorAgent.Server" Version="1.0.0" />
</ItemGroup><ItemGroup>
<PackageReference Include="Novior.MentorAgent.Server" />
</ItemGroup>File-based app
#:package Novior.MentorAgent.Server@1.0.0With either option, MentorAgent does not include a model provider package. The server code on this page uses OpenAI, so add its package to the server project:
dotnet add package Microsoft.Extensions.AI.OpenAI --version 10.6.0For another provider, see Model providers.
In the client project:
.NET CLI
dotnet add package Novior.MentorAgent.BlazorPackageReference
<ItemGroup>
<PackageReference Include="Novior.MentorAgent.Blazor" Version="1.0.0" />
</ItemGroup>Central Package Management
<ItemGroup>
<PackageVersion Include="Novior.MentorAgent.Blazor" Version="1.0.0" />
</ItemGroup><ItemGroup>
<PackageReference Include="Novior.MentorAgent.Blazor" />
</ItemGroup>File-based app
#:package Novior.MentorAgent.Blazor@1.0.0Minimal configuration
Section titled “Minimal configuration”Option A. Register MentorAgent in the server project with the same Program.cs lines as on Blazor Server:
using Novior.MentorAgent.Abstractions.Models; // MentorLanguage, MentorTheme, ChatPosition, MentorshipLevel
using Novior.MentorAgent.Extensions; // AddMentorAgent
using Microsoft.Extensions.AI; // AsIChatClient
using OpenAI; // OpenAIClientbuilder.Services.AddMentorAgent(options =>
{
options.AppName = "Case management";
options.ChatClient = new OpenAIClient(builder.Configuration["OpenAI:Key"]
?? throw new InvalidOperationException("Set OpenAI:Key with dotnet user-secrets."))
.GetChatClient("gpt-4o-mini")
.AsIChatClient();
options.ScanAssemblies = [typeof(Program).Assembly];
});The Auto template already turns on Server interactivity. The WebAssembly template does not: in the same Program.cs, chain .AddInteractiveServerComponents() to AddRazorComponents(), and .AddInteractiveServerRenderMode() to MapRazorComponents<App>(). Without them every page answers HTTP 500.
Then pin the widget to Server interactivity in the server project's layout:
@using Novior.MentorAgent.Abstractions.Components
<ChatWidget @rendermode="InteractiveServer" />This works only because the layout is static. With Interactivity location: Global (--all-interactive), option A fails:
- the layout is in the client project, inside the interactive router, and a child component cannot switch to another interactive render mode;
- the client project does not know
ChatWidgetwithout a MentorAgent package, and the build stops. With the package added, the build, the startup and the prerendered page all succeed: the error appears only in the browser, when WebAssembly renders the layout; - with Auto, the widget works while the app still runs on the server, and fails once it runs in WebAssembly.
With global interactivity, use option B.
Option B.
In the server project, register the services and map the hub:
using Novior.MentorAgent.Extensions; // AddMentorAgent using Novior.MentorAgent.Server.Extensions; // AddMentorAgentServer, MapMentorAgentServer using Microsoft.Extensions.AI; // AsIChatClient using OpenAI; // OpenAIClientbuilder.Services.AddMentorAgent(options => { options.AppName = "Case management"; options.ChatClient = new OpenAIClient(builder.Configuration["OpenAI:Key"] ?? throw new InvalidOperationException("Set OpenAI:Key with dotnet user-secrets.")) .GetChatClient("gpt-4o-mini") .AsIChatClient(); options.ScanAssemblies = [typeof(Program).Assembly]; }); builder.Services.AddMentorAgentServer(); // after var app = builder.Build(); app.MapMentorAgentServer(); // /mentor-hub, /mentor/chat, /mentor/approve, /mentor/cancel ...In the client project, register the widget's services:
using Novior.MentorAgent.Abstractions.Models; // MentorLanguage, MentorTheme using Novior.MentorAgent.Blazor.Extensions; // AddMentorAgentBlazor// The widget posts Stop and confirmations through an HttpClient: the standalone template registers this line, a Blazor Web App client does not builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddMentorAgentBlazor(options => { // Same origin as the server: the default "/mentor-hub" is resolved against the base address of the page options.HubUrl = "/mentor-hub"; });HubUrldefaults to/mentor-hub. A relative value is resolved against the base address of the page, as a browser does:/mentor-hubpoints to the root of the origin, andmentor-hubto the base path of the app. It works when the server also serves the client, as in a Blazor Web App or a hosted WebAssembly app. For a server on another origin, set an absoluteHubUrl: see Complete configuration.Add the widget to a layout or page of the client project, as on Blazor Server. In a Blazor Web App the client project has a layout only with Interactivity location: Global:
@using Novior.MentorAgent.Abstractions.Components <ChatWidget />In a standalone app, reference the widget's stylesheet and script in
wwwroot/index.html. That page is served before .NET starts in the browser, so the widget cannot add them in time:<link href="_content/Novior.MentorAgent.Abstractions/css/MentorAgent.css?v=1.0.0" rel="stylesheet" /> <script src="_content/Novior.MentorAgent.Abstractions/js/MentorAgent.js?v=1.0.0"></script>Keep the
?v=query string and change it on every upgrade: nothing else tells the browser that its cached copy is old.A Blazor Web App has no
index.html. Its server prerenders the widget, and the widget adds both tags to the<head>of the page itself, as on Blazor Server. Add the two lines toComponents/App.razoronly if you turn prerendering off.
Complete configuration
Section titled “Complete configuration”A standalone client on another origin, with signed-in users. On the server, allow the client's origin and require authentication on the hub:
builder.Services.AddCors(options => options.AddPolicy("MentorClients", policy => policy
.WithOrigins("https://localhost:7001") // the WebAssembly app's origin
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials())); // SignalR needs credentials
// Plus your authentication scheme (JWT bearer, cookies): builder.Services.AddAuthentication()...
app.UseCors("MentorClients"); // before authentication and authorization
app.UseAuthentication();
app.UseAuthorization();
app.MapMentorAgentServer(configureTransports: e => e.RequireAuthorization());On the client, point to the server and send the user's token:
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddMentorAgentBlazor(options =>
{
// Server on another origin: absolute URL, and CORS on the server
options.HubUrl = "https://api.example.com/mentor-hub";
// The signed-in user's token, for the hub, Stop and confirmations
options.AccessTokenProvider = () => Task.FromResult(tokenStore.Token);
// The widget: on WebAssembly these settings live here, not in MentorOptions
options.BotName = "Case assistant";
options.Language = MentorLanguage.English;
options.Theme = MentorTheme.Minimal;
options.EnableSuggestions = true;
});TokenStore stands for whatever your application uses to keep the signed-in user's token.
Options that matter here
Section titled “Options that matter here”| Option | Project | Why it matters |
|---|---|---|
HubUrl | client | The default /mentor-hub is resolved against the base address of the page. Set an absolute URL for a server on another origin. The origin of an absolute URL is also where the widget posts Stop and confirmations |
AccessTokenProvider | client | The user's token, read on connect and on every reconnect, and sent with Stop and confirmations. Without it every user is anonymous to the server |
ConfigureConnection | client | Anything else the hub connection needs: a header, a transport, cookies |
BotName, Theme, PrimaryColor, Position, WelcomeMessage, Language | client | On WebAssembly the widget reads its look and its own texts from the client options, not from MentorOptions. In a Blazor Web App the server prerenders it with MentorOptions first: give both the same values |
EnableSuggestions | client | Off by default on the client, while it is on by default in MentorOptions |
EnableOnboardingTour | client | Turn it on in the client options. The server answers GET /mentor/tour whatever its own value. In a Blazor Web App, give MentorOptions the same value, because the server prerenders the widget |
EnableImageInput | both | Turn it on in the client options and in MentorOptions |
| Everything about the agent | server | Model, agents, RAG, memory, MCP, A2A, costs and limits stay in MentorOptions, on the server. Language there sets the language of the answers |
Every server option is in Configuration options, and every client option under MentorAgentBlazorOptions.
Security and roles
Section titled “Security and roles”- Who the user is.
AddMentorAgentServer()passes the user of the hub connection to MentorAgent.RequiredRoles, per-user memory and the rate limit then work as on Blazor Server. The client must send the token throughAccessTokenProvider. - In a Blazor Web App the hub still knows the user.
AddRazorComponents()andAddMentorAgentServer()can come in either order. MentorAgent reads the user of the hub connection from the request, before it asks Blazor's own provider, which works only inside a circuit. So role-gated actions, per-user memory and the rate limit work with option B, if the client sends the token throughAccessTokenProvider. To add claims or roles for the hub, see Secure the endpoints. - With Auto, the user comes from two places. On the first visit the page runs on the server, and the widget runs in the circuit: the user is the one of the circuit. After the switch to WebAssembly the widget uses the hub: the user is the one of the token that
AccessTokenProvidersends. Without a token, the widget is anonymous after the switch and role-gated actions are refused. - When the token is read. The widget opens the connection with the first message and keeps it for the life of the page. A user who signs in after that stays anonymous until the page reloads.
- JWT bearer. In the browser, WebSocket and Server-Sent Events connections cannot carry an
Authorizationheader, so SignalR sends the token in theaccess_tokenquery string. The server must read it from there for/mentor-hub: see Choose the authentication scheme. - Confirmation. The widget shows the confirmation box. The answer goes to
POST /mentor/approveand Stop toPOST /mentor/cancel, on the hub's origin and with the same token. While a signed-in user's connection is open, only that user can send them.POST /mentor/approveanswers404when nothing waits for the answer any more: it was answered already, the turn was stopped, or the connection closed. The widget treats404as "nothing left to answer". When the connection closes, the turn waiting for a confirmation ends with it. See Where nobody can answer for the other channels. - The hub is open until you protect it.
configureTransports: e => e.RequireAuthorization()covers the hub and both/mentor/chatendpoints.
Common errors
Section titled “Common errors”- The widget never connects from another origin. The browser blocked the SignalR handshake. List the client's origin with
AllowCredentials(), and use an absoluteHubUrl. CallUseCorsbeforeUseAuthentication()andUseAuthorization(), and write those two calls inProgram.csyourself. Otherwise authorization can refuse the browser's preflight request. - The widget appears without styles for a moment. The stylesheet and script are missing from the host page.
- After an upgrade, the tour does not start and voice output waits for the end of the answer. The browser still has an old
MentorAgent.js: change the?v=value. - Every page answers HTTP 500 after you pin the widget: "the required endpoints are not mapped on the server". The app was created with WebAssembly interactivity and has no Server interactivity: add
AddInteractiveServerComponents()andAddInteractiveServerRenderMode(), as in Minimal configuration. - "Cannot create a component of type 'Novior.MentorAgent.Abstractions.Components.ChatWidget'… is not supported by WebAssembly rendering" in the browser console. The widget is pinned to the server inside an interactive router (Interactivity location: Global). Option A needs a static layout: with global interactivity, use option B.
- The WebAssembly runtime does not start, and the console says
CannotResolveServiceforHttpClient. The client project of a Blazor Web App registers noHttpClient, and the widget needs one for Stop and confirmations. Add theAddScoped(sp => new HttpClient { … })line of the client snippet in Minimal configuration. - Every action with
RequiredRolesis refused, and memory starts over at every reconnect. The server sees an anonymous caller: setAccessTokenProvider, and with JWT bearer read the token from the query string. - Stop or a confirmation seems ignored. The client has no
HttpClientregistered, or the server's CORS policy does not allow thePOSTor theAuthorizationheader. The widget only logs the failure, in the browser console. SerializeSessionAsyncthrowsNotSupportedException. On WebAssembly the conversation lives on the server. Save and restore it over HTTP withGETandPOST /mentor/session, as described in Any front end. The snapshot is opaque: store it as text and send it back unchanged.
See also
Section titled “See also”- Channels: how clients reach the agent — Which channel fits your client: the Blazor or MAUI widget, SignalR or SSE for JavaScript and mobile apps, and MCP or A2A for other agents and AI tools.
- Blazor Server app — Configure MentorAgent in a Blazor Web App with Server interactivity: services, the options that matter, the widget and authentication.
- Secure the endpoints — Protect the SignalR hub, Server-Sent Events, /mcp and /a2a endpoints with authentication and CORS before you expose them.
- Configuration options — Every MentorOptions property with its type, default and effect, grouped by area, plus the options of the WebAssembly client and the YAML loader.
