Intermediate

Building Blazor Hybrid Apps

Blazor Hybrid with .NET MAUI, WPF and Windows Forms via BlazorWebView and shared Razor components.

Table of Contents

  1. What is Blazor Hybrid?
  2. BlazorWebView Architecture
  3. Creating a .NET MAUI Blazor Hybrid App
  4. Implementing Razor Components
  5. Razor Class Libraries — Sharing Components
  6. Authentication and Authorization
  7. Blazor Hybrid in WPF and Windows Forms
  8. Publishing Applications
  9. Best Practices and Common Pitfalls
  10. Hands-On Exercises
  11. 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

AspectBlazor WASM (Browser)Blazor Hybrid (Native)
ExecutionWASM .NET runtime in the browserApp’s native .NET runtime
WebAssemblyRequiredNot used
Native APIsLimited (sandboxed)Full access
Camera/GPSVia Web APIVia native API
File SystemBlockedFull access
PerformanceSlower (WASM overhead)Faster (native)
DistributionWeb URLApp Store / Package
OfflineService Worker requiredAlways 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 CustomAuthStateProvider serves 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:

  1. Create a WPF project: SDK=Microsoft.NET.Sdk.Razor
  2. Reference ProductCatalog.Library
  3. Add the NuGet package: Microsoft.AspNetCore.Components.WebView.Wpf
  4. Copy wwwroot/ from the MAUI project
  5. Add BlazorWebView to MainWindow.xaml
  6. 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 MAUIWPFWindows Forms
SDK ProjectMicrosoft.NET.SdkMicrosoft.NET.Sdk.RazorMicrosoft.NET.Sdk.Razor
NuGet Package(built-in)...WebView.Wpf...WebView.WindowsForms
DI SetupMauiApp.CreateBuilder()ServiceCollection in OnStartupServiceCollection in constructor
AddBlazorWebViewAddMauiBlazorWebView()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.html to 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

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

PitfallProblemSolution
HttpClient in WPF/WebUsing new HttpClient() directlyUse IHttpClientFactory
HttpClient in MAUIUsing IHttpClientFactoryCreate new HttpClient() (MAUI-specific)
CSS not loadingWrong .styles.css filenameUpdate to match the host project name
Route not foundLibrary not included in the RouterAdd AdditionalAssemblies
Missing DIService not registered on the host sideEach host project must register its own services
[Authorize] ignoredUsing RouteView instead of AuthorizeRouteViewReplace in Routes.razor

9.3 Quality Checklist

  • Shared components placed in a Razor Class Library
  • Platform-specific implementations in each host project
  • CustomAuthStateProvider registered in each host project
  • AuthorizeRouteView used in Routes.razor
  • CSS styles updated to reflect the host project name in index.html
  • Router configured with AdditionalAssemblies for 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:

  1. Create a .NET MAUI Blazor Hybrid App project
  2. Create a Product model (record)
  3. Create an IProductService interface and ProductService implementation
  4. Create a Products.razor page
  5. Add navigation in NavMenu.razor

Exercise 2 — Razor Class Library (Intermediate, ~2h)

Goal: Extract components into a shared library.

Steps:

  1. Create ProductCatalog.Library
  2. Move Products.razor, IProductService, Product.cs into it
  3. Update the Router with AdditionalAssemblies
  4. Create a Blazor Web App project that references the same library

Exercise 3 — Authentication (Advanced, ~2h)

Goal: Implement a complete CustomAuthStateProvider.

Steps:

  1. Create CustomAuthStateProvider with LogInAsync() and Logout()
  2. Add login/logout actions to NavMenu.razor
  3. Protect Products.razor with [Authorize]
  4. Display the user’s name in the layout
  5. Add a role-based restriction on a specific feature

11. Glossary

TermDefinition
App StoreApple’s application marketplace for iOS and macOS
AuthorizeRouteViewBlazor component that enforces [Authorize] during navigation
AuthorizeViewBlazor component for conditional rendering based on authentication
BlazorWebViewNative control that hosts a Blazor application inside a native app
ClickOnceWindows desktop deployment technology
ClaimsIdentityRepresentation of an identity through claims (name, role, etc.)
ClaimsPrincipalRepresentation of a user along with their identities
ContentPageMAUI page containing the main content
CustomAuthStateProviderCustom implementation of AuthenticationStateProvider
IHttpClientFactoryFactory for HttpClient instances (recommended in ASP.NET Core)
Local Interop ChannelLocal communication channel between BlazorWebView and Blazor components
.NET MAUIMulti-platform App UI — .NET framework for cross-platform apps
OIDCOpenID Connect — authentication protocol based on OAuth2
Razor Class LibraryLibrary project containing reusable Razor components
RootComponentRoot component loaded inside the BlazorWebView
SDK.Razor.NET SDK enabling the use of Razor in WPF/WinForms projects
Self-containedPublication that bundles the entire .NET runtime (no external dependency)
WindowsIdentity.NET class representing the current user’s Windows identity
WindowsPrincipalClaimsPrincipal backed by a Windows identity
WinUIWindows UI Library — UI framework used by MAUI on Windows

Additional Resources


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

Interested in this course?

Contact us to book it or get a custom training plan for your team.