Skip to content

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 .razor components — 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 at FileSystem.AppDataDirectory.
  • Outbox queue: write changes locally; sync when Connectivity.NetworkAccess == NetworkAccess.Internet.
  • IConnectivity from Microsoft.Maui.Networking exposes 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 OnAppLinkReceived on Application — route into Blazor's NavigationManager.
  • OnInitializedAsync runs 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., older IndexedDB quirks).
  • ⚠️ 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

builder.Services.AddHttpClient("api",
    c => c.BaseAddress = new Uri("https://api.example.com/"));
// 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

@onclick="() => Task.Run(() => HeavyCpu()).Wait()"

✅ Correct: await the work

@onclick="async () => await Task.Run(HeavyCpu)"

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, not File.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 pathswwwroot/ packs into the app, not served by Kestrel.
  • ⚠️ Skipping platform abstractions — calling File.WriteAllText directly couples components to native; abstract via DI.
  • ⚠️ Assuming HttpClient BaseAddress — there is none in Hybrid.

Further reading