Blazor Hybrid & .NET MAUI
Key Points
- Blazor Hybrid = Razor components hosted inside a native shell (BlazorWebView) — not over the network, not in WASM. Full .NET on the device.
- Targets: .NET MAUI (iOS, Android, macOS, Windows), WPF, WinForms. One control:
BlazorWebView. - Not Blazor WASM: no browser sandbox, no .NET-in-WASM runtime; native .NET 9 with full BCL, full P/Invoke, full file system.
- Not MAUI XAML: instead of XAML controls you author UI as
.razorcomponents — same skills as web Blazor. - Performance: app code runs at native speed; UI paint goes through WebView2 / WkWebView (slower than native UI but fine for line-of-business).
- Sharing: put components in a Razor Class Library (RCL); reference from web Blazor and MAUI. Branch with
OperatingSystem.IsAndroid()etc. - Auth: MSAL.NET broker flows (not browser cookies). Identity != web identity.
- Limits: WebView availability per OS, app size (~15-25 MB minimum), platform lifecycle (background, deep links).
Concepts (deep dive)
What Blazor Hybrid actually is
┌──────────────────────────────────────────────────────┐
│ Native app process (MAUI / WPF / WinForms) │
│ ┌────────────────────────────────────────────────┐ │
│ │ BlazorWebView control │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ System WebView (WebView2 / WkWebView) │ │ │
│ │ │ - HTML/CSS/JS render surface │ │ │
│ │ │ - DOM interop bridge │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ │ ▲ in-process bridge (no HTTP, no SignalR) │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ Blazor render tree (.NET 9, full BCL) │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
The Razor render tree runs in the native .NET process. Diffs are pushed to the WebView via an in-process channel — no HTTP, no WebSocket, no WASM runtime. Click latency is microseconds.
Distinguishing the three Blazor flavors
| Flavor | Where .NET runs | UI surface | Network needed |
|---|---|---|---|
| Blazor Server | Server process | Browser DOM via SignalR | Always |
| Blazor WASM | Browser (WASM) | Browser DOM | Initial download |
| Blazor Hybrid | Native device process | Embedded WebView | No (for UI) |
Hybrid is the only flavor that has access to the full .NET runtime — System.IO, Process, sockets, P/Invoke, native DLLs.
MAUI shell
<!-- MainPage.xaml -->
<ContentPage xmlns="..." xmlns:b="clr-namespace:Microsoft.AspNetCore.Components.WebView.Maui;assembly=Microsoft.AspNetCore.Components.WebView.Maui">
<b:BlazorWebView HostPage="wwwroot/index.html">
<b:BlazorWebView.RootComponents>
<b:RootComponent Selector="#app" ComponentType="{x:Type local:Routes}" />
</b:BlazorWebView.RootComponents>
</b:BlazorWebView>
</ContentPage>
<!-- wwwroot/index.html -->
<!DOCTYPE html>
<html><head><base href="/" /><link href="app.css" rel="stylesheet" /></head>
<body>
<div id="app">Loading…</div>
<script src="_framework/blazor.webview.js" autostart="false"></script>
</body></html>
Routes.razor is the same router you'd use in web Blazor.
WPF / WinForms shell
<!-- WPF -->
<blazor:BlazorWebView HostPage="wwwroot/index.html"
Services="{StaticResource services}">
<blazor:BlazorWebView.RootComponents>
<blazor:RootComponent Selector="#app" ComponentType="{x:Type local:Routes}" />
</blazor:BlazorWebView.RootComponents>
</blazor:BlazorWebView>
Useful for modernizing legacy desktop apps page-by-page.
Native interop (DI shared services)
// MauiProgram.cs
builder.Services.AddMauiBlazorWebView();
builder.Services.AddSingleton<IDeviceLocation>(sp => new DeviceLocationImpl());
@inject IDeviceInfo Device
@inject IDeviceLocation Location
<p>Device: @Device.Manufacturer @Device.Model</p>
<button @onclick="GetGps">GPS</button>
@code {
async Task GetGps()
{
var p = await Location.GetCurrentAsync();
// Razor component just called native sensor API
}
}
Components inject native services exactly like web Blazor injects scoped services. The DI container is the bridge.
Sharing components between Web and Hybrid
src/
├── MyApp.Components/ ← RCL: pages, components, layouts
├── MyApp.Web/ ← Blazor Web App (Server + WASM)
└── MyApp.Maui/ ← MAUI host with BlazorWebView
@* Inside RCL — works in both shells *@
@inject IFileStore Store
<button @onclick="Save">Save</button>
Each host registers its own IFileStore impl: web stores via API, MAUI stores via FileSystem.AppDataDirectory.
Conditional code
if (OperatingSystem.IsAndroid())
{
// Android-specific
}
else if (OperatingSystem.IsBrowser())
{
// Running under WASM, not Hybrid
}
Use OperatingSystem.IsBrowser() to detect WASM specifically — Hybrid returns false (it's the native OS).
Authentication
Browser cookies don't make sense in a native app. Use MSAL.NET with the platform broker:
builder.Services.AddSingleton(sp =>
PublicClientApplicationBuilder.Create(clientId)
.WithRedirectUri("msauth.com.example.app://auth")
.WithIosKeychainSecurityGroup("com.example")
.Build());
Tokens cached in OS keychain (iOS Keychain, Android Keystore, Windows Credential Locker). The Blazor component injects IPublicClientApplication and calls AcquireTokenInteractive. Outbound API calls attach the bearer token.
Offline-first patterns
- SQLite via
Microsoft.EntityFrameworkCore.Sqlite— local DB atFileSystem.AppDataDirectory. - Outbox queue: write changes locally; sync when
Connectivity.NetworkAccess == NetworkAccess.Internet. IConnectivityfromMicrosoft.Maui.Networkingexposes connection state to components.
@inject IConnectivity Conn
@implements IDisposable
@code {
protected override void OnInitialized() => Conn.ConnectivityChanged += OnNet;
void OnNet(object? s, ConnectivityChangedEventArgs e) => InvokeAsync(StateHasChanged);
public void Dispose() => Conn.ConnectivityChanged -= OnNet;
}
Performance characteristics
| Layer | Speed |
|---|---|
| C# / business logic | Native AOT or JIT — full speed |
| DI / Razor diffing | Native — microseconds |
| DOM paint | WebView — slower than native UIKit/Compose |
| Animations | CSS — fine for LOB; not 120fps games |
Rule: anything CPU-bound in C# is fast. Anything that pushes pixels through HTML is WebView-bound.
App size
A trimmed MAUI Blazor app typically ships at 15-25 MB (Android APK, iOS IPA). Most of that is the .NET runtime. Web Blazor downloads ~5-10 MB; Hybrid ships it once via the store.
Lifecycle differences
- App can be killed by the OS in background (mobile) — components disposed, then recreated on resume.
- Deep links arrive as
OnAppLinkReceivedonApplication— route into Blazor'sNavigationManager. OnInitializedAsyncruns on every cold start; cache aggressively.
Limits and caveats
- ⚠️ WebView2 Evergreen on Windows must be installed (it usually is on Win11; older Win10 may need bootstrap).
- ⚠️ iOS WebView (
WkWebView) cannot debug as easily as Chromium DevTools. - ⚠️ Some browser APIs aren't in
WkWebView(e.g., olderIndexedDBquirks). - ⚠️ Hot reload works but is finicky across the WebView boundary.
Code: correct vs wrong
❌ Wrong: HttpClient assumes a server origin
// In Hybrid there's no host — BaseAddress is "app://0.0.0.0/"
var http = new HttpClient { BaseAddress = new Uri("/") };
await http.GetAsync("api/users"); // 404
✅ Correct: explicit API base
❌ Wrong: cookie-based auth
// Browser cookies don't work the way you think inside WebView
services.AddAuthentication().AddCookie();
✅ Correct: MSAL token in keychain
var token = await pca.AcquireTokenInteractive(scopes).ExecuteAsync();
http.DefaultRequestHeaders.Authorization = new("Bearer", token.AccessToken);
❌ Wrong: blocking the WebView thread
✅ Correct: await the work
Design patterns for this topic
Pattern 1 — "Razor Class Library as the shared core"
- Intent: one component set, two hosts (web + native).
Pattern 2 — "Platform service via DI abstraction"
- Intent: components depend on
IFileStore, notFile.WriteAllText. Each host wires its impl.
Pattern 3 — "Outbox + SQLite for offline-first"
- Intent: writes work without network; sync resumes when back online.
Pattern 4 — "MSAL broker flow for auth"
- Intent: native identity, OS keychain, no cookie hackery.
Pattern 5 — "Static SSR for content pages, BlazorWebView for the app"
- Intent: marketing site is server-rendered web; the installed app reuses the same components.
Pros & cons / trade-offs
| Aspect | Pro | Con |
|---|---|---|
| Code reuse | Same Razor across web/desktop/mobile | UI conventions differ per platform |
| Performance | Full .NET speed | WebView paint cost |
| Distribution | One codebase, App Stores | Native install vs URL |
| Native APIs | All of MAUI Essentials | Each platform's quirks |
| Hot reload | Yes | Less smooth than web |
| App size | Acceptable | 15-25 MB minimum |
When to use / when to avoid
- ✅ Use when you have a web Blazor app and need the same UX as an installed app.
- ✅ Use for line-of-business desktop apps modernizing WinForms / WPF.
- ✅ Use when offline + native sensors matter and you want C# everywhere.
- ❌ Avoid for graphics-heavy apps (games, photo editors) — native UI wins.
- ❌ Avoid if you have zero web devs — MAUI XAML may be a better fit.
- ❌ Avoid for tiny utilities where 20 MB install is ridiculous.
Interview Q&A
Q1. What is Blazor Hybrid? Razor components hosted inside a native shell (MAUI/WPF/WinForms) via BlazorWebView. Runs full .NET on device.
Q2. Hybrid vs WASM? WASM runs the .NET runtime in the browser sandbox; Hybrid runs native .NET in the OS process and only paints HTML in an embedded WebView.
Q3. Hybrid vs MAUI XAML? Same MAUI shell, different UI authoring: XAML controls vs Razor components. Razor wins for web teams; XAML wins for true native fidelity.
Q4. Where does the render tree run? In the native .NET process. Diffs cross an in-process bridge to the WebView — no HTTP.
Q5. How do components reuse between web and hybrid? Put them in a Razor Class Library; both hosts reference it; DI injects host-specific services.
Q6. Authentication? MSAL.NET with platform broker; tokens cached in OS keychain. Not cookies.
Q7. How to detect platform from a component? OperatingSystem.IsAndroid() / IsIOS() / IsBrowser(). Hybrid returns false for IsBrowser().
Q8. App size? 15-25 MB typical (most is .NET runtime).
Q9. Offline storage? SQLite via EF Core Sqlite at FileSystem.AppDataDirectory; outbox for sync.
Q10. Performance bottleneck? WebView paint, not C#. CPU-bound code is native speed.
Q11. Can I share an entire Blazor Web App with MAUI? Yes — same RCL. Routing, layouts, components reuse; only the host project differs.
Q12. WebView per platform? WebView2 on Windows, WkWebView on iOS/macOS, Android System WebView.
Gotchas / common mistakes
- ⚠️ Treating it like Blazor Server — there's no server; your DI is local.
- ⚠️ Cookie auth — works oddly inside WebView; use MSAL.
- ⚠️ Forgetting WebView2 bootstrap on older Windows.
- ⚠️ Heavy CSS animations — WebView paint is the limit.
- ⚠️ Static file paths —
wwwroot/packs into the app, not served by Kestrel. - ⚠️ Skipping platform abstractions — calling
File.WriteAllTextdirectly couples components to native; abstract via DI. - ⚠️ Assuming
HttpClientBaseAddress — there is none in Hybrid.