Table of Contents
- What is Blazor Hybrid?
- BlazorWebView Architecture
- Creating a .NET MAUI Blazor Hybrid App
- Implementing Razor Components
- Razor Class Libraries — Sharing Components
- Authentication and Authorization
- Blazor Hybrid in WPF and Windows Forms
- Publishing Applications
- Best Practices and Common Pitfalls
- Hands-On Exercises
- Glossary
1. What is Blazor Hybrid?
1.1 Definition and Core Concepts
Blazor is Microsoft’s framework for building interactive web applications using HTML, CSS, and C#.
Blazor WebAssembly enables Blazor to run inside the browser via WebAssembly (WASM).
Blazor Hybrid goes a step further: it hosts a Blazor application directly inside a native desktop or mobile application using the BlazorWebView component.
flowchart TB
subgraph Web["Blazor WASM — In the browser"]
Browser[Web browser]
WASM_Runtime[.NET Runtime via WebAssembly]
Blazor1[Blazor Application]
Browser --> WASM_Runtime --> Blazor1
end
subgraph Hybrid["Blazor Hybrid — Native app"]
App[.NET MAUI / WPF / WinForms]
Native_Runtime[Native .NET Runtime]
BlazorWebView[BlazorWebView]
Blazor2[Blazor Application]
App --> Native_Runtime
Native_Runtime --> BlazorWebView
BlazorWebView --> Blazor2
end
subgraph Advantages["Blazor Hybrid Advantages"]
A1["Access to native APIs\n(camera, GPS, file system)"]
A2["No WebAssembly\n→ Better performance"]
A3["Share code\nacross MAUI, WPF, Web"]
A4["Same Blazor codebase\nfor all platforms"]
end
1.2 BlazorWebView vs Browser — Key Differences
| Aspect | Blazor WASM (Browser) | Blazor Hybrid (Native) |
|---|---|---|
| Execution | WASM .NET runtime in the browser | App’s native .NET runtime |
| WebAssembly | Required | Not used |
| Native APIs | Limited (sandboxed) | Full access |
| Camera/GPS | Via Web API | Via native API |
| File System | Blocked | Full access |
| Performance | Slower (WASM overhead) | Faster (native) |
| Distribution | Web URL | App Store / Package |
| Offline | Service Worker required | Always available |
1.3 Platforms Supported by BlazorWebView
flowchart LR
BlazorWebView[BlazorWebView\nMicrosoft] --> MAUI[.NET MAUI\nAndroid, iOS, Mac, Windows]
BlazorWebView --> WPF[WPF\nWindows Desktop]
BlazorWebView --> WinForms[Windows Forms\nWindows Desktop]
MAUI --> Android[Android 7+]
MAUI --> iOS[iOS 14+]
MAUI --> MacCatalyst[macOS Catalyst]
MAUI --> Windows_MAUI[Windows 10+]
WPF --> Windows_WPF[Windows 10+]
WinForms --> Windows_WF[Windows 10+]
1.4 Sample Application — ProductCatalog
The demo application for these notes: ProductCatalog for the fictional company GreenLeaf Goods. It lets users browse available products.
Final solution structure:
ProductCatalog Solution
├── ProductCatalog.Library/ ← Razor Class Library (shared components)
│ ├── Components/
│ │ ├── Layout/
│ │ │ ├── MainLayout.razor
│ │ │ └── NavMenu.razor
│ │ ├── Pages/
│ │ │ ├── Home.razor
│ │ │ └── Products.razor ← Main component
│ │ └── Routes.razor
│ └── Data/
│ ├── IProductService.cs
│ └── Product.cs ← Model record
│
├── ProductCatalog.Maui/ ← .NET MAUI App (Android, iOS, Windows)
│ ├── Data/
│ │ └── ProductService.cs ← MAUI HttpClient implementation
│ ├── Auth/
│ │ └── CustomAuthStateProvider.cs
│ └── MauiProgram.cs
│
├── ProductCatalog.Wpf/ ← WPF Windows App
│ ├── Data/
│ │ └── ProductService.cs
│ ├── Auth/
│ │ └── CustomAuthStateProvider.cs (Windows Authentication)
│ └── App.xaml.cs
│
├── ProductCatalog.WinForms/ ← Windows Forms App
│ ├── Data/
│ │ └── ProductService.cs
│ └── MainForm.cs
│
└── ProductCatalog.BlazorWeb/ ← Blazor Web App (browser)
├── Data/
│ └── ProductService.cs
└── Program.cs
2. BlazorWebView Architecture
2.1 How BlazorWebView Works
sequenceDiagram
participant App as Native App (.NET MAUI/WPF)
participant BwV as BlazorWebView
participant Channel as Local Interop Channel
participant Browser as Native WebView
participant Blazor as Blazor Components
App->>BwV: Initialize + DI services
BwV->>Channel: Configure local channel
Channel->>Browser: Initialize native WebView
Browser->>Blazor: Load index.html + Blazor
Note over Channel: No network! Local communication only
Note over Browser: Rendered inside the native WebView
Browser->>Channel: User event (click, etc.)
Channel->>Blazor: Dispatch event
Blazor->>Blazor: Execute C# code (native .NET runtime)
Blazor->>Channel: UI update
Channel->>Browser: Update the DOM
Key point: Communication between Blazor and the WebView goes through a local channel (interop), with no network requests involved. This is why performance is better than with WASM.
2.2 Structure of a MAUI Blazor Hybrid Project
MainPage.xaml — The main page that hosts the BlazorWebView:
<!-- MainPage.xaml -->
<?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:local="clr-namespace:ProductCatalog.Maui"
x:Class="ProductCatalog.Maui.MainPage">
<!-- BlazorWebView is the container for the Blazor application -->
<BlazorWebView HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<!-- Routes is the Blazor root component -->
<!-- Selector="#app" targets the div id="app" in index.html -->
<RootComponent Selector="#app"
ComponentType="{x:Type local:Components.Routes}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
</ContentPage>
wwwroot/index.html — HTML page that loads Blazor:
<!-- wwwroot/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0,
maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>ProductCatalog</title>
<base href="/" />
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="css/app.css" />
<!-- This file contains the scoped styles for each component -->
<!-- Its name matches the project that compiles the components -->
<link rel="stylesheet" href="ProductCatalog.Maui.styles.css" />
</head>
<body>
<!-- The Blazor application will be injected here -->
<div id="app">Loading...</div>
<!-- blazor.webview.js handles communication with the native WebView -->
<script src="_framework/blazor.webview.js" autostart="false"></script>
</body>
</html>
3. Creating a .NET MAUI Blazor Hybrid App
3.1 Prerequisites and Setup
Required Visual Studio Workloads:
Visual Studio Installer → Modify
☑ .NET Multi-platform App UI development ← For MAUI
☑ .NET desktop development ← For WPF + Windows Forms
Creating the project:
Visual Studio → New Project → .NET MAUI Blazor Hybrid App
→ Name: ProductCatalog.Maui
→ Solution: ProductCatalog
→ .NET: 8.0 or later
→ Create
3.2 Exploring the Generated Project
// MauiProgram.cs — MAUI application configuration
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("Roboto-Regular.ttf", "RobotoRegular");
});
// Register Blazor Hybrid services
builder.Services.AddMauiBlazorWebView();
#if DEBUG
// Developer tools (F12 in the app)
builder.Services.AddBlazorWebViewDeveloperTools();
builder.Logging.AddDebug();
#endif
// Application services
builder.Services.AddTransient<IProductService, ProductService>();
// Authentication services
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
return builder.Build();
}
}
3.3 Customizing the Look and Feel
@* Components/Layout/NavMenu.razor *@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">
@* Logo in wwwroot/logo.png *@
<img src="logo.png" class="logo" alt="GreenLeaf Goods" />
<div class="brand-text">Product Catalog</div>
</a>
</div>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="/" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill" aria-hidden="true"></span> Home
</NavLink>
</div>
<AuthorizeView>
<Authorized>
<div class="nav-item px-3">
<NavLink class="nav-link" href="/products">
<span class="bi bi-bag-fill" aria-hidden="true"></span> Products
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" @onclick="Logout" href="">
<span class="bi bi-box-arrow-right" aria-hidden="true"></span> Sign Out
</NavLink>
</div>
</Authorized>
<NotAuthorized>
<div class="nav-item px-3">
<NavLink class="nav-link" @onclick="Login" href="">
<span class="bi bi-person-fill" aria-hidden="true"></span> Sign In
</NavLink>
</div>
</NotAuthorized>
</AuthorizeView>
</nav>
</div>
/* NavMenu.razor.css */
.logo {
height: 40px;
width: auto;
margin-right: 8px;
}
.brand-text {
font-size: 1.2rem;
font-weight: bold;
color: #f8f9fa;
}
4. Implementing Razor Components
4.1 Data Model — Product Record
// Data/Product.cs — in the Library or MAUI project
// Records are immutable and ideal for DTOs
public record Product(
string Title,
string Summary,
decimal UnitPrice,
string? ThumbnailUrl = null,
string? Category = null
);
4.2 Interface and Service
// Data/IProductService.cs
public interface IProductService
{
Task<IEnumerable<Product>?> FetchProductsAsync();
Task<Product?> FindProductByTitleAsync(string title);
}
// Data/ProductService.cs — MAUI implementation
public class ProductService : IProductService
{
private readonly HttpClient _client;
public ProductService()
{
// In MAUI, create HttpClient directly
// (no IHttpClientFactory like in ASP.NET Core)
_client = new HttpClient();
}
public async Task<IEnumerable<Product>?> FetchProductsAsync()
{
try
{
// Load from a REST API
var items = await _client.GetFromJsonAsync<IEnumerable<Product>>(
"https://api.example.com/products.json");
return items;
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Network error: {ex.Message}");
// Fallback to local data
return GetLocalProducts();
}
}
public async Task<Product?> FindProductByTitleAsync(string title)
{
var items = await FetchProductsAsync();
return items?.FirstOrDefault(p => p.Title.Equals(title, StringComparison.OrdinalIgnoreCase));
}
private static IEnumerable<Product> GetLocalProducts()
{
return new[]
{
new Product("Eco Tote Bag", "Reusable canvas shopping bag", 12.99m, Category: "Accessories"),
new Product("Bamboo Tumbler", "Insulated bamboo travel cup", 24.50m, Category: "Drinkware"),
new Product("Solar Charger", "Foldable solar panel phone charger", 39.99m, Category: "Electronics"),
new Product("Seed Kit", "Organic herb seed starter kit", 18.00m, Category: "Garden"),
new Product("Cork Wallet", "Slim cork minimalist wallet", 15.75m, Category: "Accessories"),
new Product("Beeswax Wraps", "Set of 3 reusable food wraps", 22.00m, Category: "Kitchen")
};
}
}
4.3 Products Component
@* Components/Pages/Products.razor *@
@page "/products"
@attribute [Authorize]
@inject IProductService ProductService
<PageTitle>Products — GreenLeaf Goods</PageTitle>
<h1 class="mb-4">
<span class="bi bi-bag-fill me-2" aria-hidden="true"></span>
Our Products
</h1>
@if (_items is null)
{
@* Loading state *@
<div class="d-flex align-items-center gap-3">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<span class="text-muted">Fetching products, please wait...</span>
</div>
}
else if (!_items.Any())
{
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
No products are available at the moment.
</div>
}
else
{
@* Product grid *@
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
@foreach (var item in _items)
{
<div class="col">
<div class="card h-100 shadow-sm">
@if (!string.IsNullOrEmpty(item.ThumbnailUrl))
{
<img src="@item.ThumbnailUrl" class="card-img-top product-image"
alt="@item.Title" />
}
else
{
<div class="product-placeholder d-flex align-items-center justify-content-center">
<span class="bi bi-box fs-1 text-success"></span>
</div>
}
<div class="card-body">
<h5 class="card-title">@item.Title</h5>
<p class="card-text text-muted">@item.Summary</p>
@if (item.Category is not null)
{
<p class="card-text">
<small class="text-muted">
<span class="bi bi-tag me-1"></span>
Category: @item.Category
</small>
</p>
}
</div>
<div class="card-footer d-flex justify-content-between align-items-center">
<span class="fs-5 fw-bold text-success">@item.UnitPrice.ToString("C")</span>
<button class="btn btn-sm btn-success" @onclick="@(() => SelectItem(item))">
Add to Cart
</button>
</div>
</div>
</div>
}
</div>
<div class="mt-3 text-muted">
<small>@_items.Count() products available</small>
</div>
}
@if (_selectedItem is not null)
{
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div class="toast show" role="alert">
<div class="toast-header bg-success text-white">
<strong class="me-auto">Item added!</strong>
<button type="button" class="btn-close btn-close-white"
@onclick="@(() => _selectedItem = null)"></button>
</div>
<div class="toast-body">
@_selectedItem.Title has been added to your cart.
</div>
</div>
</div>
}
@code {
private IEnumerable<Product>? _items;
private Product? _selectedItem;
protected override async Task OnInitializedAsync()
{
_items = await ProductService.FetchProductsAsync();
}
private void SelectItem(Product item)
{
_selectedItem = item;
// Auto-dismiss after 3 seconds
_ = Task.Delay(3000).ContinueWith(_ =>
{
_selectedItem = null;
InvokeAsync(StateHasChanged);
});
}
}
5. Razor Class Libraries — Sharing Components
5.1 Why Razor Class Libraries?
flowchart LR
Library[ProductCatalog.Library\nRazor Class Library] -->|"Shared components"| MAUI[.NET MAUI\nAndroid/iOS/Windows]
Library -->|"Shared components"| WPF[WPF\nWindows]
Library -->|"Shared components"| WinForms[Windows Forms\nWindows]
Library -->|"Shared components"| Web[Blazor Web App\nBrowser]
style Library fill:#FFD700
style MAUI fill:#4CAF50
style WPF fill:#2196F3
style WinForms fill:#9C27B0
style Web fill:#FF5722
Benefits:
- Write a component once — use it everywhere
- Centralized maintenance
- Identical behavior across all platforms
- Eliminates code duplication
5.2 Creating a Razor Class Library
Visual Studio → New Project → Razor Class Library
→ Name: ProductCatalog.Library
→ Target Framework: .NET 8
→ Create
Clean up generated files:
Remove:
- Component1.razor
- ExampleJsInterop.cs
- wwwroot/ (entire folder)
Keep:
- _Imports.razor (add common namespaces)
5.3 Library Structure
// _Imports.razor — Namespaces available in all components within the library
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Authorization
@using ProductCatalog.Library.Data
// Data/IProductService.cs — Shared interface
namespace ProductCatalog.Library.Data;
public interface IProductService
{
Task<IEnumerable<Product>?> FetchProductsAsync();
}
// Data/Product.cs — Shared model
namespace ProductCatalog.Library.Data;
public record Product(
string Title,
string Summary,
decimal UnitPrice,
string? ThumbnailUrl = null,
string? Category = null
);
5.4 Router — Loading Components from the Library
When Razor components live in a separate library, the Router needs to know where to look for them:
@* Components/Routes.razor — In the host project (MAUI, WPF, etc.) *@
<Router AppAssembly="typeof(App).Assembly"
AdditionalAssemblies="new[] { typeof(ProductCatalog.Library._Imports).Assembly }">
@* ↑ Tells the Router to also scan the library *@
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData"
DefaultLayout="typeof(ProductCatalog.Library.Components.Layout.MainLayout)">
<NotAuthorized>
<p>Not authorized. Please sign in.</p>
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Page not found</PageTitle>
<LayoutView Layout="typeof(ProductCatalog.Library.Components.Layout.MainLayout)">
<p>Sorry, this page does not exist.</p>
</LayoutView>
</NotFound>
</Router>
5.5 Using the Library in a Blazor Web App
// ProductCatalog.BlazorWeb/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// Register the interface with a web-specific implementation
builder.Services.AddScoped<IProductService, WebProductService>();
// In a web context, use IHttpClientFactory (not a direct HttpClient)
builder.Services.AddHttpClient<WebProductService>();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAntiforgery();
// Include the library assemblies for component discovery
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddAdditionalAssemblies(typeof(ProductCatalog.Library._Imports).Assembly);
app.Run();
CoffeeService for Blazor Web:
// ProductCatalog.BlazorWeb/Data/WebProductService.cs
public class WebProductService : IProductService
{
private readonly IHttpClientFactory _factory;
public WebProductService(IHttpClientFactory factory)
{
_factory = factory;
}
public async Task<IEnumerable<Product>?> FetchProductsAsync()
{
// In ASP.NET Core, use IHttpClientFactory (not a direct HttpClient)
using var client = _factory.CreateClient();
return await client.GetFromJsonAsync<IEnumerable<Product>>(
"https://api.example.com/products.json");
}
}
6. Authentication and Authorization
6.1 Security in Blazor Hybrid — Concepts
flowchart TD
Security[Blazor Hybrid Security] --> Auth["Authentication\n'Who are you?'"]
Security --> Authz["Authorization\n'What can you do?'"]
Auth --> Native[Native Authentication\nOpenID Connect, Windows Auth, etc.]
Auth --> Custom[CustomAuthStateProvider\nBridges auth to Blazor]
Authz --> AuthorizeView[AuthorizeView\nBlazor component]
Authz --> AuthorizeAttr["[Authorize] attribute\nOn pages"]
Authz --> Roles[Roles and Claims]
Key concept: In Blazor Hybrid, authentication is handled by the platform’s native libraries. The
CustomAuthStateProviderserves as the bridge between that native authentication and the Blazor components.
6.2 CustomAuthenticationStateProvider
// Auth/CustomAuthStateProvider.cs (in each host project)
public class CustomAuthStateProvider : AuthenticationStateProvider
{
// Current user — anonymous by default
private ClaimsPrincipal _currentUser = new ClaimsPrincipal(new ClaimsIdentity());
// Returns the current auth state to Blazor components
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
return Task.FromResult(new AuthenticationState(_currentUser));
}
// Public method to sign in
public async Task LogInAsync()
{
// Perform authentication (OIDC, Windows Auth, etc.)
var authenticatedUser = await SignInWithProviderAsync();
_currentUser = authenticatedUser;
// Notify Blazor that the state changed → components re-render
NotifyAuthenticationStateChanged(
Task.FromResult(new AuthenticationState(_currentUser)));
}
// Public method to sign out
public void Logout()
{
// Reset to an anonymous user
_currentUser = new ClaimsPrincipal(new ClaimsIdentity());
NotifyAuthenticationStateChanged(
Task.FromResult(new AuthenticationState(_currentUser)));
}
// Authentication with the external provider
private async Task<ClaimsPrincipal> SignInWithProviderAsync()
{
// ─── IN PRODUCTION: use OpenID Connect ─────────────
// var result = await OidcClient.LoginAsync(...);
// return result.User;
// ─── FOR DEMO: simulate authentication ─────────────
await Task.Delay(500); // Simulate network delay
var claims = new[]
{
new Claim(ClaimTypes.Name, "Jane Smith"),
new Claim(ClaimTypes.Email, "jane@greenleafgoods.com"),
new Claim(ClaimTypes.Role, "ProductManager"),
new Claim("subscription", "premium"),
new Claim("preferred_category", "Accessories")
};
var identity = new ClaimsIdentity(claims, authenticationType: "Custom");
return new ClaimsPrincipal(identity);
}
}
6.3 Using AuthorizeView and [Authorize]
@* Conditional display with AuthorizeView *@
<AuthorizeView>
<Authorized>
<p>Welcome, @context.User.Identity!.Name!</p>
<span class="badge bg-success">
@context.User.FindFirst(ClaimTypes.Role)?.Value
</span>
</Authorized>
<NotAuthorized>
<p>Please sign in to access the catalog.</p>
</NotAuthorized>
</AuthorizeView>
@* Role-based restriction *@
<AuthorizeView Roles="Admin">
<Authorized>
<div class="admin-panel alert alert-warning">
<h4>Admin Panel</h4>
<p>This content is only visible to administrators.</p>
</div>
</Authorized>
</AuthorizeView>
@* Custom claim restriction *@
<AuthorizeView Policy="PremiumOnly">
<Authorized>
<div>Content reserved for Premium subscribers</div>
</Authorized>
<NotAuthorized>
<a href="/upgrade">Upgrade to Premium</a>
</NotAuthorized>
</AuthorizeView>
Protected page with [Authorize]:
@* Products.razor — Requires authentication *@
@page "/products"
@attribute [Authorize]
@* With role: @attribute [Authorize(Roles = "ProductManager,Admin")] *@
Configure AuthorizeRouteView in Routes.razor:
@* Routes.razor — So that [Authorize] is enforced during navigation *@
<Router AppAssembly="typeof(App).Assembly">
<Found Context="routeData">
@* AuthorizeRouteView validates the [Authorize] attribute during navigation *@
<AuthorizeRouteView RouteData="routeData"
DefaultLayout="typeof(MainLayout)">
<NotAuthorized>
<p class="text-danger">
<strong>Access denied.</strong>
Please <a @onclick="NavigateToLogin" href="">sign in</a>.
</p>
</NotAuthorized>
</AuthorizeRouteView>
</Found>
</Router>
6.4 Handling Login/Logout from NavMenu
@* NavMenu.razor *@
@inject CustomAuthStateProvider AuthStateProvider
@inject NavigationManager NavigationManager
@* In the template (see section 3.3) *@
@code {
private bool _collapseNavMenu = true;
private string NavMenuCssClass => _collapseNavMenu ? "collapse" : null!;
private void ToggleNavMenu() => _collapseNavMenu = !_collapseNavMenu;
private async Task Login()
{
await AuthStateProvider.LogInAsync();
}
private void Logout()
{
AuthStateProvider.Logout();
// Return to home after sign-out
NavigationManager.NavigateTo(string.Empty);
}
}
7. Blazor Hybrid in WPF and Windows Forms
7.1 WPF — Full Configuration
Steps:
- Create a WPF project:
SDK=Microsoft.NET.Sdk.Razor - Reference
ProductCatalog.Library - Add the NuGet package:
Microsoft.AspNetCore.Components.WebView.Wpf - Copy
wwwroot/from the MAUI project - Add
BlazorWebViewtoMainWindow.xaml - Configure dependency injection in
App.xaml.cs
MainWindow.xaml:
<Window x:Class="ProductCatalog.Wpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;
assembly=Microsoft.AspNetCore.Components.WebView.Wpf"
xmlns:local="clr-namespace:ProductCatalog.Wpf"
Title="Product Catalog" Height="600" Width="900">
<Grid>
<blazor:BlazorWebView HostPage="wwwroot/index.html"
Services="{DynamicResource services}">
<blazor:BlazorWebView.RootComponents>
<blazor:RootComponent Selector="#app"
ComponentType="{x:Type local:Components.Routes}" />
</blazor:BlazorWebView.RootComponents>
</blazor:BlazorWebView>
</Grid>
</Window>
App.xaml.cs — DI configuration for WPF:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var services = new ServiceCollection();
// Blazor Hybrid WPF services
services.AddWpfBlazorWebView(); // WPF-specific!
#if DEBUG
services.AddBlazorWebViewDeveloperTools();
#endif
// Authentication and authorization services
services.AddAuthorizationCore();
services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
// Data services
services.AddTransient<IProductService, ProductService>();
// Build the ServiceProvider and expose it via Resources
var serviceProvider = services.BuildServiceProvider();
Resources.Add("services", serviceProvider);
}
}
Windows Authentication in WPF:
// Auth/CustomAuthStateProvider.cs — WPF version with Windows Auth
private async Task<ClaimsPrincipal> SignInWithProviderAsync()
{
// Use the current Windows identity
var windowsIdentity = WindowsIdentity.GetCurrent();
var windowsPrincipal = new WindowsPrincipal(windowsIdentity);
return windowsPrincipal;
}
7.2 Windows Forms — Configuration
MainForm.cs:
// MainForm.cs — Windows Forms with BlazorWebView
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
// Configure services
var services = new ServiceCollection();
services.AddWindowsFormsBlazorWebView(); // WinForms-specific!
services.AddAuthorizationCore();
services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
services.AddTransient<IProductService, ProductService>();
blazorWebView.HostPage = "wwwroot/index.html";
blazorWebView.Services = services.BuildServiceProvider();
blazorWebView.RootComponents.Add<Routes>("#app");
}
}
7.3 Configuration Comparison
| Aspect | .NET MAUI | WPF | Windows Forms |
|---|---|---|---|
| SDK Project | Microsoft.NET.Sdk | Microsoft.NET.Sdk.Razor | Microsoft.NET.Sdk.Razor |
| NuGet Package | (built-in) | ...WebView.Wpf | ...WebView.WindowsForms |
| DI Setup | MauiApp.CreateBuilder() | ServiceCollection in OnStartup | ServiceCollection in constructor |
| AddBlazorWebView | AddMauiBlazorWebView() | AddWpfBlazorWebView() | AddWindowsFormsBlazorWebView() |
| XAML | <BlazorWebView> | <blazor:BlazorWebView> | Toolbox → BlazorWebView |
| CSS file | *.Maui.styles.css | *.Wpf.styles.css | *.WinForms.styles.css |
Important: The scoped CSS file is named after the project that compiles the components. When using a Razor Class Library, update the name in
index.htmlto match the host project.
8. Publishing Applications
8.1 Publishing a .NET MAUI App
flowchart LR
Dev[Development\nDebug] --> Release[Release\nConfiguration]
Release --> Android[Android\n.aab / .apk]
Release --> iOS[iOS\n.ipa]
Release --> Windows[Windows\n.msix]
Release --> Mac[macOS\n.pkg]
Android -->|"Google Play Store"| PlayStore[Store Publication]
iOS -->|"Apple App Store"| AppStore[Store Publication]
Windows -->|"Microsoft Store\nor direct"| WinStore[Distribution]
From Visual Studio:
1. Select Android Emulator from the platform dropdown
2. Switch configuration from Debug → Release
3. Right-click the MAUI project → Publish
4. Visual Studio starts the archiving process
From the command line:
# Publish for Android
dotnet publish -f net8.0-android -c Release
# Publish for iOS (requires a Mac)
dotnet publish -f net8.0-ios -c Release
# Publish for Windows
dotnet publish -f net8.0-windows10.0.19041.0 -c Release
8.2 Publishing a WPF App
From Visual Studio:
Right-click the WPF project → Publish
→ Target: Azure or Folder or ClickOnce
→ Configuration: Release
→ Publish
→ Output in the publish/ folder
Command-line publishing:
# Self-contained (all-inclusive, no .NET installation needed)
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
# Framework-dependent (smaller, requires .NET installed)
dotnet publish -c Release
9. Best Practices and Common Pitfalls
9.1 Recommended Architecture
flowchart TB
Library[Razor Class Library\nEverything that can be shared] --> Components[Razor Components\nPages, Layout]
Library --> Models[Models\nProduct, etc.]
Library --> Interfaces[Interfaces\nIProductService]
MAUI[MAUI Project] --> MauiImpl[Platform-specific implementations\nProductService with MAUI HttpClient\nMAUI-specific auth]
WPF[WPF Project] --> WPFImpl[Platform-specific implementations\nProductService with IHttpClientFactory\nWindows Auth]
Web[Blazor Web] --> WebImpl[Platform-specific implementations\nProductService with IHttpClientFactory\nASP.NET Core Identity/OIDC]
Library --> MAUI
Library --> WPF
Library --> Web
9.2 Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| HttpClient in WPF/Web | Using new HttpClient() directly | Use IHttpClientFactory |
| HttpClient in MAUI | Using IHttpClientFactory | Create new HttpClient() (MAUI-specific) |
| CSS not loading | Wrong .styles.css filename | Update to match the host project name |
| Route not found | Library not included in the Router | Add AdditionalAssemblies |
| Missing DI | Service not registered on the host side | Each host project must register its own services |
| [Authorize] ignored | Using RouteView instead of AuthorizeRouteView | Replace in Routes.razor |
9.3 Quality Checklist
- Shared components placed in a Razor Class Library
- Platform-specific implementations in each host project
-
CustomAuthStateProviderregistered in each host project -
AuthorizeRouteViewused inRoutes.razor - CSS styles updated to reflect the host project name in
index.html - Router configured with
AdditionalAssembliesfor the library -
AddWpfBlazorWebView()/AddWindowsFormsBlazorWebView()used per platform
10. Hands-On Exercises
Exercise 1 — Basic MAUI Application (Beginner, ~1h)
Goal: Build a MAUI Blazor Hybrid application that displays a list of products.
Steps:
- Create a
.NET MAUI Blazor Hybrid Appproject - Create a
Productmodel (record) - Create an
IProductServiceinterface andProductServiceimplementation - Create a
Products.razorpage - Add navigation in
NavMenu.razor
Exercise 2 — Razor Class Library (Intermediate, ~2h)
Goal: Extract components into a shared library.
Steps:
- Create
ProductCatalog.Library - Move
Products.razor,IProductService,Product.csinto it - Update the Router with
AdditionalAssemblies - Create a Blazor Web App project that references the same library
Exercise 3 — Authentication (Advanced, ~2h)
Goal: Implement a complete CustomAuthStateProvider.
Steps:
- Create
CustomAuthStateProviderwithLogInAsync()andLogout() - Add login/logout actions to
NavMenu.razor - Protect
Products.razorwith[Authorize] - Display the user’s name in the layout
- Add a role-based restriction on a specific feature
11. Glossary
| Term | Definition |
|---|---|
| App Store | Apple’s application marketplace for iOS and macOS |
| AuthorizeRouteView | Blazor component that enforces [Authorize] during navigation |
| AuthorizeView | Blazor component for conditional rendering based on authentication |
| BlazorWebView | Native control that hosts a Blazor application inside a native app |
| ClickOnce | Windows desktop deployment technology |
| ClaimsIdentity | Representation of an identity through claims (name, role, etc.) |
| ClaimsPrincipal | Representation of a user along with their identities |
| ContentPage | MAUI page containing the main content |
| CustomAuthStateProvider | Custom implementation of AuthenticationStateProvider |
| IHttpClientFactory | Factory for HttpClient instances (recommended in ASP.NET Core) |
| Local Interop Channel | Local communication channel between BlazorWebView and Blazor components |
| .NET MAUI | Multi-platform App UI — .NET framework for cross-platform apps |
| OIDC | OpenID Connect — authentication protocol based on OAuth2 |
| Razor Class Library | Library project containing reusable Razor components |
| RootComponent | Root component loaded inside the BlazorWebView |
| SDK.Razor | .NET SDK enabling the use of Razor in WPF/WinForms projects |
| Self-contained | Publication that bundles the entire .NET runtime (no external dependency) |
| WindowsIdentity | .NET class representing the current user’s Windows identity |
| WindowsPrincipal | ClaimsPrincipal backed by a Windows identity |
| WinUI | Windows UI Library — UI framework used by MAUI on Windows |
Additional Resources
- Official Documentation:
- .NET MAUI: learn.microsoft.com/dotnet/maui
Search Terms
blazor · hybrid · apps · web · ui · c# · .net · development · library · razor · app · blazorwebview · class · maui · components · configuration · publishing · wpf · application · architecture · authentication · concepts · forms · libraries