# .NET MAUI (Blazor Hybrid)

> Host MentorAgent in a .NET MAUI Blazor Hybrid app, pass the signed-in user for role checks, and keep the model key on a server.

- URL: https://docs.mentoragent.net/configure/maui
- Verified with: MentorAgent 1.0.0
- Last updated: 2026-10-14
- Packages: Novior.MentorAgent

This page is for a .NET MAUI Blazor Hybrid app. In such an app all of MentorAgent runs inside the app, as it does on Blazor Server. When the model key must not travel with the app, the app becomes a client of a MentorAgent server instead.

## When to choose it

### Inside the app

- **Android and Windows.** `Novior.MentorAgent` no longer needs the ASP.NET Core shared framework (`Microsoft.AspNetCore.App`), so the MAUI Blazor Hybrid template builds for Android as it is, and for Windows. The documentation has not checked iOS and Mac Catalyst, which build only on a Mac.
- A `BlazorWebView` is interactive by construction: the widget needs no render mode.
- There is no [circuit](https://docs.mentoragent.net/reference/glossary#circuit), no HTTP request and no ASP.NET Core user. Scoped services live as long as the app: one coordinator and one conversation until the app restarts.
- The model key, or the address of a local model, is part of the app's configuration. That suits an internal tool, a model running on the same machine, or development.
- The dashboard endpoint and the MCP and A2A servers need a web host, so they are not available: they are in `Novior.MentorAgent.Server`, which a MAUI app does not reference. MCP and A2A clients work normally.

### As a client of a server

For a phone or tablet, and whenever you distribute the app, keep the agent and the key on a server. Run MentorAgent on an ASP.NET Core host with `Novior.MentorAgent.Server`, configured as in [Any front end](https://docs.mentoragent.net/configure/any-front-end), and connect the app to its SignalR hub with `Microsoft.AspNetCore.SignalR.Client`. The protocol is the same for every client.

The rest of this page configures MentorAgent inside the app.

## Install the packages

**.NET CLI**

```shell
dotnet add package Novior.MentorAgent
```

**PackageReference**

```xml title="YourApp.csproj"
<ItemGroup>
  <PackageReference Include="Novior.MentorAgent" Version="1.0.0" />
</ItemGroup>
```

**Central Package Management**

```xml title="Directory.Packages.props"
<ItemGroup>
  <PackageVersion Include="Novior.MentorAgent" Version="1.0.0" />
</ItemGroup>
```

```xml title="YourApp.csproj"
<ItemGroup>
  <PackageReference Include="Novior.MentorAgent" />
</ItemGroup>
```

**File-based app**

```csharp title="app.cs"
#:package Novior.MentorAgent@1.0.0
```

`Novior.MentorAgent` includes no model provider package, and none of the packages that read the configuration. The code on this page uses OpenAI, and step 3 calls `AddJsonStream` and `AddUserSecrets`. So add these packages to the MAUI project, the last two with the same major version as your .NET:

```shell
dotnet add package Microsoft.Extensions.AI.OpenAI --version 10.6.0
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.UserSecrets
```

For another provider, see [Model providers](https://docs.mentoragent.net/configure/model-providers#install-the-packages).

## Minimal configuration

1. Reference the widget's stylesheet and script in `wwwroot/index.html`. The app serves that page itself, and the template does not know the package's assets:

   ```html
   <link rel="stylesheet" href="_content/Novior.MentorAgent.Abstractions/css/MentorAgent.css" />
   <script src="_content/Novior.MentorAgent.Abstractions/js/MentorAgent.js"></script>
   ```

2. Embed `appsettings.json` in the app: at run time there is no content root to read it from. In the MAUI project file, give it the logical name that `MauiProgram.cs` reads in step 3, here `Cases.appsettings.json`:

   ```xml
   <ItemGroup>
     <EmbeddedResource Include="appsettings.json" LogicalName="Cases.appsettings.json" />
   </ItemGroup>
   ```

   While you develop, keep the key in user secrets:

   ```shell
   dotnet user-secrets init
   dotnet user-secrets set OpenAI:Key <your-key>
   ```

3. In `MauiProgram.cs`, after `builder.Services.AddMauiBlazorWebView();`, read the configuration and register MentorAgent:

   ```csharp title="MauiProgram.cs"
   using System.Reflection;                                // Assembly
   using Novior.MentorAgent.Extensions;                           // AddMentorAgent
   using Novior.MentorAgent.Options;                              // MentorAnonymousIdentity
   using Microsoft.AspNetCore.Components.Authorization;    // AuthenticationStateProvider
   using Microsoft.Extensions.Configuration;               // AddJsonStream, AddUserSecrets
   using Microsoft.Extensions.AI;                          // AsIChatClient
   using OpenAI;                                           // OpenAIClient
   ```

   ```csharp title="MauiProgram.cs"
   // appsettings.json is embedded in the app: there is no content root to read it from
   using var stream = Assembly.GetExecutingAssembly()
       .GetManifestResourceStream("Cases.appsettings.json");
   if (stream is not null) builder.Configuration.AddJsonStream(stream);

   #if DEBUG
   builder.Configuration.AddUserSecrets<App>();   // the key stays out of the app package
   #endif
   ```

   ```csharp title="MauiProgram.cs"
   builder.Services.AddMentorAgent(options =>
   {
       options.AppName = "Case management";
       options.ChatClient = new OpenAIClient(builder.Configuration["OpenAI:Key"]
               ?? throw new InvalidOperationException("OpenAI:Key is missing from the configuration."))
           .GetChatClient("gpt-4o-mini")
           .AsIChatClient();
       options.ScanAssemblies = [typeof(MauiProgram).Assembly];

       // One installation, one user: without this every app run is a new anonymous user
       options.AnonymousIdentity = MentorAnonymousIdentity.Shared;
   });
   ```

   `AddUserSecrets<App>()` uses the app's own `App` class to find the project's user secrets. Any class of the app project works.

4. Add the widget to the layout, with no render mode:

   ```razor title="Components/Layout/MainLayout.razor"
   @using Novior.MentorAgent.Abstractions.Components
   <ChatWidget />
   ```

## Complete configuration

**The signed-in user.** Without an ASP.NET Core user, MentorAgent asks `AuthenticationStateProvider` who is using the app. Register your own provider, and call its `SignIn` method from the Razor component where the user signs in. MentorAgent then reads the same instance, because scoped services live as long as the app:

```csharp title="AppAuthenticationStateProvider.cs"
public sealed class AppAuthenticationStateProvider : AuthenticationStateProvider
{
    private ClaimsPrincipal _user = new(new ClaimsIdentity());   // signed out

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(new AuthenticationState(_user));

    // Call it from your own sign-in code, with the user's id and roles
    public void SignIn(string userId, string name, IEnumerable<string> roles)
    {
        List<Claim> claims = [new(ClaimTypes.NameIdentifier, userId), new(ClaimTypes.Name, name)];
        claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
        _user = new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType: "app"));
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    public void SignOut()
    {
        _user = new ClaimsPrincipal(new ClaimsIdentity());
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }
}
```

Register it in `MauiProgram.cs`, next to `AddMentorAgent`:

```csharp title="MauiProgram.cs"
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AppAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(
    sp => sp.GetRequiredService<AppAuthenticationStateProvider>());
```

MentorAgent checks the roles with `ClaimsPrincipal.IsInRole` on every call to an action with `RequiredRoles`. With the provider above, the roles are `ClaimTypes.Role` claims. It identifies the user for memory and the rate limit by the `ClaimTypes.NameIdentifier` claim, else by `sub`, `oid` or the name.

**Memory that survives a restart.** `AnonymousIdentity = Shared` keeps the same memory key across runs. The built-in memory store keeps everything in RAM, though: register your own `IMentorMemoryStore` before `AddMentorAgent` to keep what the agent remembers.

### Options that matter here

| Option | Why it matters on .NET MAUI |
|---|---|
| [`AnonymousIdentity`](https://docs.mentoragent.net/reference/options#anonymousidentity) | The default, `PerSession`, gives every run of the app a new key, so nothing remembered is recalled after a restart. `Shared` means one installation, one user |
| [`UseMemoryContext`](https://docs.mentoragent.net/reference/options#usememorycontext) | Turns memory on. Pair it with a persistent `IMentorMemoryStore` |
| [`ChatClient`](https://docs.mentoragent.net/reference/options#chatclient) | Any provider, including a model on the same machine: see [Model providers](https://docs.mentoragent.net/configure/model-providers) |
| [`SafetyCheckTimeout`](https://docs.mentoragent.net/reference/options#safetychecktimeout), [`ClassifierChatClient`](https://docs.mentoragent.net/reference/options#classifierchatclient) | With a slow local model, give the safety checks more time or a faster model |
| [`BotName`](https://docs.mentoragent.net/reference/options#botname), [`WelcomeMessage`](https://docs.mentoragent.net/reference/options#welcomemessage), [`Theme`](https://docs.mentoragent.net/reference/options#theme), [`PrimaryColor`](https://docs.mentoragent.net/reference/options#primarycolor), [`Position`](https://docs.mentoragent.net/reference/options#position) | The widget reads them from `MentorOptions`, as on Blazor Server |
| [`RequireConfirmation`](https://docs.mentoragent.net/reference/options#requireconfirmation) | Keep it `true`: the widget then shows the confirmation box, as on the other hosts |
| [`WarmUpAtStartup`](https://docs.mentoragent.net/reference/options#warmupatstartup) | No effect: a MAUI app runs no hosted service |

Every option is in [Configuration options](https://docs.mentoragent.net/reference/options).

## Security and roles

- **Without a signed-in user, role-gated actions are refused.** Until you register an `AuthenticationStateProvider`, every action with `RequiredRoles` is refused for everybody, rather than run unchecked. With the provider above, the checks work as on Blazor Server: see [Two checks, in this order](https://docs.mentoragent.net/concepts/governance#two-checks-in-this-order).
- **Confirmation.** The confirmation box appears in the widget, as on Blazor Server.
- **The key is on the device.** Whatever the app reads at run time, a determined user can read too. For an app you distribute, move MentorAgent to a server and connect the app as a client.
- **User secrets are for development.** They live in your user profile, and the `#if DEBUG` line reads them only in Debug builds: a Release build must get the key some other way.

## Common errors

- **The build does not find `AddJsonStream` or `AddUserSecrets`.** The two configuration packages are not in the MAUI project, because `Novior.MentorAgent` does not bring them: add them, as in [Install the packages](https://docs.mentoragent.net/configure/maui#install-the-packages).
- **The widget has no styles, or voice and the tour do not work.** The stylesheet and script are missing from `wwwroot/index.html`.
- **The app stops at the first page with "OpenAI:Key is missing from the configuration".** The embedded `appsettings.json` has a different logical name, or a Release build does not read user secrets. Put the value where the app can read it at run time. A MAUI app has no host start, so MentorAgent reports an invalid configuration the first time it uses it.
- **Every action with `RequiredRoles` is refused.** No `AuthenticationStateProvider` is registered: the log says so. Register the provider and call `SignIn`.
- **Nothing is remembered after a restart.** Set `AnonymousIdentity = Shared` and register a persistent `IMentorMemoryStore`.
- **`MapMentorAgentMcp`, `MapMentorAgentA2A` or the dashboard endpoint are not available.** They are in `Novior.MentorAgent.Server` and need a web host: run them on an ASP.NET Core server.

---

- Previous: [Blazor WebAssembly and Auto](https://docs.mentoragent.net/configure/blazor-webassembly.md)
- Next: [Any front end: React, Angular, Vue](https://docs.mentoragent.net/configure/any-front-end.md)
