Bijgewerkt: 17 september 2026
.NET MAUI Blazor Hybrid laat je Razor componenten uit een web-app hergebruiken in een native mobiele of desktop shell door ze te renderen in een BlazorWebView, waarbij de C# code lokaal draait (niet in een browser) en volledige toegang heeft tot native platform-API's via dependency injection of JavaScript interop. In de praktijk betekent dit één UI-codebase voor je marketing site, portal en mobiele app, mits je bereid bent te leven met een WebView-render-engine in plaats van native controls. In deze gids laat ik zien hoe we dit patroon op onze cross-platform teams inrichten voor .NET 9 en .NET 10 preview builds.
Wat is .NET MAUI Blazor Hybrid?
Kort door de bocht: .NET MAUI Blazor Hybrid is een hosting-model waarbij Razor componenten (dezelfde .razor bestanden die je in een Blazor Server- of WASM-app gebruikt) worden weergegeven in een BlazorWebView-control binnen een .NET MAUI-applicatie. Anders dan bij Blazor Server draait er geen SignalR-verbinding, en anders dan bij Blazor WebAssembly wordt er ook geen .NET runtime naar de client gedownload. De componenten worden direct in-process op het device gerenderd, met volledige toegang tot native code via je MauiProgram.cs service registratie.
Wat dit uniek maakt binnen cross-platform ontwikkeling is dat teams die vertrouwd zijn met Blazor de mobiele wereld kunnen betreden zonder dat ze XAML hoeven te leren. Ik heb dit meerdere keren als bruggenbouwer ingezet. Toen een backend-team met veel ASP.NET-ervaring bij een van onze klanten een companion mobiele app moest leveren, gaf Blazor Hybrid ze een productieve weg naar voren zonder dat ze eerst een half jaar MVVM en XAML data binding moesten absorberen. De C#-code die OnInitializedAsync aanroept en state beheert is namelijk exact hetzelfde als in hun web-app.
Onder de motorkap gebruikt BlazorWebView per platform een andere WebView-implementatie. De Microsoft Blazor Hybrid documentatie beschrijft WKWebView op iOS en macOS, WebView2 op Windows, en de Android WebView-component. De rendering gebeurt dus wél in een browser-engine, maar de codepad naar je database, je bestanden en de camera loopt via native .NET APIs. Dat onderscheid is belangrijk, want het is precies waarom je Essentials-services direct kunt injecteren.
Wanneer kies je Blazor Hybrid boven XAML MAUI?
Dit is de vraag die ik het vaakst krijg tijdens architectuurreviews. Het simpele antwoord: kies Blazor Hybrid wanneer UI-hergebruik met een bestaande web-property meer waarde oplevert dan native pixel-perfect gedrag. Kies puur XAML MAUI wanneer je een greenfield mobiele app bouwt zonder web-tegenhanger, of wanneer je high-performance native rendering en platform-specifieke gestures echt nodig hebt.
| Aspect | Blazor Hybrid (BlazorWebView) | Pure XAML MAUI |
|---|---|---|
| UI-taal | Razor + HTML + CSS | XAML + code-behind |
| Deelt UI met web | Ja, via RCL | Nee (los platform) |
| Rendering | WebView engine per platform | Native controls |
| Startup-tijd (cold, mid-range Android) | ≈ 1,4–1,9 s | ≈ 0,7–1,1 s |
| Scroll-performance (10k items) | Vergt Virtualize-component | Native CollectionView virtualisatie |
| Toegang tot native API | Via DI, geen JS-brug nodig | Direct via Microsoft.Maui.* |
| Leercurve voor web-teams | Laag | Middel-hoog |
| Hot Reload op device | Ja (Razor + C#) | Ja (XAML + C#) |
In gemengde teams gebruiken we vaak een hybride aanpak. De meeste schermen gaan in Blazor Hybrid (formulieren, dashboards, lijsten met paginering), en een handvol prestatie-kritieke schermen (denk aan een camera-preview, een custom drawing surface of een real-time chart) doen we alsnog in native XAML. Omdat een BlazorWebView gewoon een View is die je in een ContentPage plaatst, is die menging op één navigatiestack triviaal.
Blazor Hybrid project opzetten met BlazorWebView
De snelste weg is de kant-en-klare template: dotnet new maui-blazor -o SharedShop.App. Deze genereert een MAUI-project met een Components-map (in plaats van Pages), een wwwroot-map voor statische assets en een MainPage.xaml die één BlazorWebView host. Onder de motorkap wordt AddMauiBlazorWebView() aangeroepen in MauiProgram.cs, wat de benodigde services registreert. Wil je een bestaand MAUI-project uitbreiden? Voeg dan het NuGet-pakket Microsoft.AspNetCore.Components.WebView.Maui toe en registreer je de services handmatig.
// MauiProgram.cs, Blazor Hybrid setup op een bestaand MAUI-project
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Components.WebView.Maui;
namespace SharedShop.App;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Blazor Hybrid activeren
builder.Services.AddMauiBlazorWebView();
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
builder.Logging.AddDebug();
#endif
// Domain services, gedeeld tussen Razor componenten en native code
builder.Services.AddSingleton<IProductCatalog, ProductCatalog>();
builder.Services.AddScoped<ICartState, CartState>();
return builder.Build();
}
}
Vervolgens plaats je een BlazorWebView in je root page. Merk op dat je RootComponent hier verwijst naar het eerste Razor-component dat gerenderd wordt, niet naar een pagina met routing. Routing gebeurt binnenin de Blazor-tree via <Router> in Routes.razor.
<!-- MainPage.xaml: de BlazorWebView hosting-container -->
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:b="clr-namespace:Microsoft.AspNetCore.Components.WebView.Maui;assembly=Microsoft.AspNetCore.Components.WebView.Maui"
xmlns:local="clr-namespace:SharedShop.App"
x:Class="SharedShop.App.MainPage"
BackgroundColor="{DynamicResource PageBackgroundColor}">
<b:BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html">
<b:BlazorWebView.RootComponents>
<b:RootComponent Selector="#app" ComponentType="{x:Type local:Components.Routes}" />
</b:BlazorWebView.RootComponents>
</b:BlazorWebView>
</ContentPage>
De HostPage is een minimale HTML file in wwwroot/ met een <div id="app"></div> en een script-referentie naar _framework/blazor.webview.js. Vanaf hier is je Razor-tree exact wat je gewend bent uit web-Blazor. Eerlijk gezegd voelt het bijna een beetje anti-klimatisch als je voor het eerst F5 drukt: dezelfde componenten, alleen in een native shell.
Razor Class Library voor gedeelde componenten
Hier zit de echte hefboom van Blazor Hybrid: gedeelde componenten in een Razor Class Library die zowel je Blazor Server-portal als je MAUI-app kunnen consumeren. We structureren onze solutions doorgaans zo:
SharedShop.UI, een Razor Class Library met alle presentational componenten (formulieren, cards, layouts, feature-schermen).SharedShop.Web, een Blazor Server- of WASM-app met alleen de web-shell (auth, hosting-configuratie).SharedShop.App, de .NET MAUI Blazor Hybrid app met de native shell, MAUI-services en platform-specifieke code.SharedShop.Domain, een klassieke class library met entiteiten, DTO's en service-contracten.
Het geheim? Gebruik interfaces voor alles wat platform-specifiek is (bestanden, notificaties, secure storage), zodat de RCL geen directe afhankelijkheid heeft op Microsoft.Maui. Elke shell registreert vervolgens zijn eigen implementatie. Dit patroon is verwant aan wat we in onze .NET MAUI dependency injection gids uitwerken, en het is echt de moeite om deze abstractie vanaf dag één in te bouwen.
// SharedShop.UI/Services/ISecureTokenStore.cs: contract in de RCL
public interface ISecureTokenStore
{
Task<string?> GetAsync(string key);
Task SetAsync(string key, string value);
Task RemoveAsync(string key);
}
// SharedShop.App/Platform/MauiSecureTokenStore.cs: MAUI-implementatie
using Microsoft.Maui.Storage;
public sealed class MauiSecureTokenStore : ISecureTokenStore
{
public Task<string?> GetAsync(string key) => SecureStorage.GetAsync(key);
public Task SetAsync(string key, string value) => SecureStorage.SetAsync(key, value);
public Task RemoveAsync(string key)
{
SecureStorage.Remove(key);
return Task.CompletedTask;
}
}
// SharedShop.Web/Services/CookieTokenStore.cs: web-implementatie (server-side)
public sealed class CookieTokenStore : ISecureTokenStore { /* HttpContext-based */ }
Je Razor component blijft platform-agnostisch: @inject ISecureTokenStore Tokens, en de juiste implementatie wordt bij runtime uitgekozen. Deze abstractie-laag houdt je RCL portable en voorkomt het klassieke anti-pattern waarbij "gedeelde" code stiekem NuGet-referenties naar Microsoft.Maui heeft en je web-build ineens breekt. Die val heb ik zelf ooit vol in gelopen op een migratieproject, en de refactor die volgde was pijnlijker dan het probleem in eerste instantie had opgelost.
Native API's aanroepen vanuit Razor componenten
Om native functionaliteit zoals camera, geolocatie of vibratie te gebruiken hoef je geen JavaScript interop te schrijven. Je injecteert simpelweg de MAUI Essentials-service in je Razor component. Omdat de componenten in-process draaien, is er geen browser-sandbox die de aanroep blokkeert. Dit is een van de grootste voordelen ten opzichte van pure Blazor WASM in de browser, waar je afhankelijk bent van Web APIs met beperkte device-toegang.
@* Components/Pages/CheckIn.razor *@
@page "/checkin"
@using Microsoft.Maui.Devices.Sensors
@inject IGeolocation Geolocation
@inject IJSRuntime JS
<h1>Locatie inchecken</h1>
<button class="btn btn-primary" @onclick="CaptureLocationAsync">
Huidige locatie ophalen
</button>
@if (_location is not null)
{
<p>Lat: @_location.Latitude, Lon: @_location.Longitude</p>
}
else if (_error is not null)
{
<div class="alert alert-danger">@_error</div>
}
@code {
private Location? _location;
private string? _error;
private async Task CaptureLocationAsync()
{
try
{
var request = new GeolocationRequest(GeolocationAccuracy.Medium,
TimeSpan.FromSeconds(10));
_location = await Geolocation.GetLocationAsync(request);
}
catch (FeatureNotSupportedException)
{
_error = "Geolocatie wordt niet ondersteund op dit toestel.";
}
catch (PermissionException)
{
_error = "Geen toestemming voor locatie. Ga naar instellingen.";
}
}
}
De IGeolocation-service (en de rest van de Essentials) is standaard geregistreerd door UseMauiApp. Voor Android moet je overigens nog wel de ACCESS_FINE_LOCATION permission declareren in AndroidManifest.xml en de runtime-toestemming aanvragen. Dat verandert niet doordat je Blazor gebruikt. Voor operaties die alléén in de webView kunnen bestaan (bijvoorbeeld navigator.share) blijft IJSRuntime beschikbaar via een minimale JS-module.
Styling, CSS isolation en statische assets
CSS isolation werkt hetzelfde als in web-Blazor. Een bestand Card.razor.css naast Card.razor genereert scoped selectors met een b-xxx-attribuut per component. Globale styling zet je in wwwroot/css/app.css, en deze wordt door de HostPage ingeladen. Fonts, iconen en beelden komen in de wwwroot-map van de MAUI-app, en zijn direct benaderbaar met root-relatieve paden zoals /images/logo.svg.
Een valkuil die we vaak zien: teams kopiëren de wwwroot uit hun web-project één-op-één naar de MAUI-app. Dat werkt zolang je niet afhankelijk bent van server-endpoints, maar het breekt zodra je bijvoorbeeld naar /api/products verwijst, want die endpoint bestaat niet in de MAUI-shell. In gedeelde componenten roep je in plaats daarvan een geïnjecteerde IProductApi aan, die per shell een andere implementatie krijgt (HTTP naar de backend in beide gevallen, maar met verschillende base URLs en auth).
@* SharedShop.UI/Components/Card.razor + Card.razor.css *@
@code {
[Parameter] public string Title { get; set; } = "";
[Parameter] public RenderFragment? ChildContent { get; set; }
}
<article class="card">
<header><h3>@Title</h3></header>
<div class="body">@ChildContent</div>
</article>
/* Card.razor.css, scoped to this component only */
.card { border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,.08); }
.card > header { background: var(--brand-surface); padding: 12px 16px; }
Hot Reload en debugging in productie
Hot Reload voor Razor-markup én C# code-behind werkt in Blazor Hybrid vanaf Visual Studio 2022 17.10+, ook op fysieke Android- en iOS-devices die je via USB debugt. De workflow is exact zoals bij web-Blazor: sla je .razor-bestand op, en de wijziging verschijnt binnen ~800 ms in de draaiende WebView. Wat níet werkt: veranderingen aan je MauiProgram.cs-DI-registratie, wijzigingen in MainPage.xaml, en toevoegingen aan de wwwroot. Die vereisen allemaal een volledige rebuild.
Voor productie-debugging is AddBlazorWebViewDeveloperTools() (alleen in DEBUG) je vriend. Op Windows en macOS krijg je dan DevTools via rechter-muisklik → "Inspect", op Android via chrome://inspect in Chrome (na WebView.SetWebContentsDebuggingEnabled(true)), en op iOS via Safari's Web Inspector. In productiebuilds moet je deze tools écht uitzetten, anders logt je app potentieel gevoelige data naar de webinspector.
Performance en beperkingen van BlazorWebView
Ik ga niet doen alsof Blazor Hybrid gratis is. De WebView-render-engine is per definitie een laag boven native, en dat kost wat. In onze benchmarks op een Pixel 7 (Android 15) en iPhone 13 zien we consistent 30 tot 60% langere cold-start dan puur XAML (1,4 tot 1,9 seconden versus 0,7 tot 1,1 seconden), en grote scrollende lijsten vragen om de <Virtualize>-component om acceptabel te blijven. Voor extreme lijst-workloads is een native CollectionView nog altijd sneller. Zie onze .NET MAUI CollectionView performance gids voor de tuning-details.
Andere praktische beperkingen om te wegen voordat je voor Blazor Hybrid kiest:
- Complex gestures (pinch-to-zoom in beeld, swipe-to-dismiss met haptics) zijn moeilijker native aan te voelen in een WebView dan in XAML.
- Deep linking vereist coördinatie tussen MAUI Shell-routes en de interne
<Router>. We routeren doorgaans alles via Shell en gebruiken één Blazor-scherm per Shell-route (zie ook onze MAUI Shell navigatie gids). - Bundle size: een MAUI Blazor Hybrid IPA is doorgaans 8 tot 14 MB groter dan een pure MAUI-app door de extra runtime-assets. Op Android is dit vaak 4 tot 8 MB extra na R8-shrinking.
- Accessibility: TalkBack en VoiceOver werken via de WebView, wat betekent dat je HTML-semantiek (juiste heading-hiërarchie, ARIA-attributen) belangrijker is dan bij native XAML met automatische accessibility trees.
Deployment naar Play Store en App Store
Publishing verschilt niet fundamenteel van een gewone .NET MAUI-app: dotnet publish -f net9.0-android -c Release voor een AAB, en dotnet publish -f net9.0-ios -c Release voor een signed IPA. De twee dingen om extra op te letten zijn: (1) je moet zeker weten dat AddBlazorWebViewDeveloperTools alleen in DEBUG-builds actief is, en (2) je moet je wwwroot-assets minifyen, anders neem je onnodige megabytes mee in je bundle. Ik gebruik meestal esbuild of PostCSS in een MSBuild-target om dit tijdens de release-build te doen.
<!-- SharedShop.App.csproj: wwwroot assets bundelen tijdens Release build -->
<Target Name="BundleWwwroot" BeforeTargets="Build"
Condition="'$(Configuration)' == 'Release'">
<Exec Command="npm --prefix wwwroot run build:prod" />
</Target>
<!-- Ontwikkelaars-tools alleen in DEBUG -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DefineConstants>$(DefineConstants);BLAZOR_NO_DEVTOOLS</DefineConstants>
</PropertyGroup>
App Store review: Apple's review team stelt in 2026 nog altijd vragen over apps die "in principe een website in een WebView" zijn (guideline 4.2). Blazor Hybrid-apps passeren doorgaans zonder issue omdat je in-process C# hebt, native platform-APIs aanroept, en offline werkt zonder externe URL. Zorg wel dat je review notes expliciet vermelden dat de app géén remote URL laadt en dat alle bedrijfslogica lokaal draait. Dat scheelt vaak een reject-loop. Voor de Play Store is er geen equivalente drempel, mits je géén trackers of ongewenste permissies bundelt in je AndroidManifest.xml. Voor extra zekerheid rond het onderscheid tussen "hybrid app" en "wrapped webview" verwijst Apple zelf naar de App Review Guidelines (Minimum Functionality), die op dit punt vaak wordt aangehaald.
Veelgestelde vragen
Wat is het verschil tussen .NET MAUI en Blazor Hybrid?
.NET MAUI is het cross-platform native UI framework waarin je typisch XAML gebruikt voor je views. Blazor Hybrid is een hosting-model binnen MAUI: je vervangt (delen van) je XAML-views door een BlazorWebView-control die Razor componenten rendert. Het is dus geen alternatief voor MAUI, maar een manier om Razor UI in een MAUI-shell te draaien.
Is Blazor Hybrid production-ready in 2026?
Ja. Vanaf .NET 8 (LTS) is Blazor Hybrid GA en breed ingezet in productie. In .NET 9 zijn iOS Hot Reload en WebView-initialisatie verder verbeterd. De belangrijkste caveat blijft performance voor rendering-intensieve schermen. Voor formulier-, dashboard- en CRUD-apps is het uitstekend, voor grafisch-zware apps blijft native XAML sneller.
Kun je Razor componenten delen tussen een Blazor web-app en een MAUI-app?
Ja, via een Razor Class Library (RCL) die beide projecten als referentie opnemen. De sleutel is om platform-specifieke afhankelijkheden (bestanden, secure storage, notificaties) achter interfaces te abstraheren, zodat de RCL zelf geen Microsoft.Maui-referentie nodig heeft. Elke shell registreert dan zijn eigen implementaties in DI.
Heeft Blazor Hybrid een internetverbinding nodig?
Nee. Anders dan Blazor Server draait er geen SignalR-verbinding, en anders dan Blazor WebAssembly wordt er geen runtime van een server gedownload. Alle Razor componenten, C# code en assets zitten in je app-bundle en draaien lokaal in-process op het device. Internet is alleen nodig voor je eigen backend-calls.
Hoe roep ik JavaScript aan vanuit een Razor component in Blazor Hybrid?
Via de standaard IJSRuntime-service, precies zoals in web-Blazor. Injecteer IJSRuntime in je component en gebruik InvokeAsync<T> om functies aan te roepen. Voor de meeste native functionaliteit is dit echter niet nodig: injecteer in plaats daarvan de MAUI Essentials-service (zoals IGeolocation of IFileSystem), want die geven directere en type-safe toegang zonder JS-brug.




