Back to Blog
5 min read

Deploying Blazor WebAssembly Applications on Azure

Introduction

Blazor WebAssembly enables building interactive web UIs using C# instead of JavaScript. With the recent .NET 6 previews bringing significant performance improvements, now is an excellent time to explore deploying Blazor WASM applications on Azure. This post covers multiple deployment strategies and best practices.

Creating a Blazor WebAssembly Project

Let’s start with a new Blazor WebAssembly project:

dotnet new blazorwasm -o BlazorAzureDemo
cd BlazorAzureDemo

Project Structure

A typical Blazor WASM project includes:

BlazorAzureDemo/
├── wwwroot/
│   ├── css/
│   ├── index.html
│   └── favicon.ico
├── Pages/
│   ├── Index.razor
│   ├── Counter.razor
│   └── FetchData.razor
├── Shared/
│   ├── MainLayout.razor
│   └── NavMenu.razor
├── Program.cs
└── BlazorAzureDemo.csproj

Deployment Option 1: Azure Static Web Apps

The most straightforward option for client-side Blazor apps:

GitHub Actions Workflow

name: Azure Static Web Apps CI/CD

on:
  push:
    branches:
      - main
  pull_request:
    types: [opened, synchronize, reopened, closed]
    branches:
      - main

jobs:
  build_and_deploy_job:
    if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
    runs-on: ubuntu-latest
    name: Build and Deploy Job
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true

      - name: Setup .NET
        uses: actions/setup-dotnet@v1
        with:
          dotnet-version: '6.0.x'
          include-prerelease: true

      - name: Build Blazor App
        run: dotnet publish -c Release -o publish

      - name: Deploy to Azure Static Web Apps
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: "upload"
          app_location: "publish/wwwroot"
          output_location: ""

Static Web App Configuration

Create staticwebapp.config.json:

{
  "navigationFallback": {
    "rewrite": "/index.html",
    "exclude": ["/_framework/*", "/css/*", "/*.{png,jpg,gif,ico,svg}"]
  },
  "mimeTypes": {
    ".dll": "application/octet-stream",
    ".wasm": "application/wasm",
    ".json": "application/json"
  }
}

Deployment Option 2: Azure App Service

For hosted Blazor apps with ASP.NET Core backend:

Creating Hosted Blazor Project

dotnet new blazorwasm -o BlazorHosted --hosted

App Service Deployment via Azure CLI

# Create App Service Plan
az appservice plan create \
  --name blazor-asp \
  --resource-group blazor-rg \
  --sku B1 \
  --is-linux

# Create Web App
az webapp create \
  --name blazor-hosted-demo \
  --resource-group blazor-rg \
  --plan blazor-asp \
  --runtime "DOTNET|6.0"

# Deploy from local
dotnet publish -c Release
cd Server/bin/Release/net6.0/publish
zip -r deploy.zip .

az webapp deployment source config-zip \
  --resource-group blazor-rg \
  --name blazor-hosted-demo \
  --src deploy.zip

Deployment Option 3: Azure Blob Storage + CDN

For cost-effective static hosting:

# Create storage account
az storage account create \
  --name blazorstoragedemo \
  --resource-group blazor-rg \
  --location australiaeast \
  --sku Standard_LRS

# Enable static website hosting
az storage blob service-properties update \
  --account-name blazorstoragedemo \
  --static-website \
  --index-document index.html \
  --404-document index.html

# Build and publish
dotnet publish -c Release -o publish

# Upload to blob storage
az storage blob upload-batch \
  --account-name blazorstoragedemo \
  --source publish/wwwroot \
  --destination '$web'

Adding Azure CDN

# Create CDN profile
az cdn profile create \
  --name blazor-cdn-profile \
  --resource-group blazor-rg \
  --sku Standard_Microsoft

# Create CDN endpoint
az cdn endpoint create \
  --name blazor-cdn-endpoint \
  --profile-name blazor-cdn-profile \
  --resource-group blazor-rg \
  --origin blazorstoragedemo.z8.web.core.windows.net \
  --origin-host-header blazorstoragedemo.z8.web.core.windows.net

Optimizing Blazor WASM Performance

Enable Compression

In Program.cs:

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
        new[] { "application/octet-stream", "application/wasm" });
});

Configure Lazy Loading

In your .csproj:

<PropertyGroup>
  <BlazorWebAssemblyLoadAllGlobalizationData>false</BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>

<ItemGroup>
  <BlazorWebAssemblyLazyLoad Include="System.Xml.dll" />
  <BlazorWebAssemblyLazyLoad Include="System.Linq.Expressions.dll" />
</ItemGroup>

Implement Assembly Lazy Loading

@page "/admin"
@inject LazyAssemblyLoader AssemblyLoader

@code {
    private bool isLoaded = false;

    protected override async Task OnInitializedAsync()
    {
        var assemblies = await AssemblyLoader.LoadAssembliesAsync(
            new[] { "AdminModule.dll" });
        isLoaded = true;
    }
}

Integrating with Azure Functions Backend

Azure Function for API

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using System.Net;

public class WeatherFunction
{
    [Function("GetWeather")]
    public async Task<HttpResponseData> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "weather")]
        HttpRequestData req)
    {
        var response = req.CreateResponse(HttpStatusCode.OK);

        var forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }).ToArray();

        await response.WriteAsJsonAsync(forecasts);
        return response;
    }

    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };
}

Blazor Component Calling Function

@page "/weather"
@inject HttpClient Http

<h3>Weather Forecast</h3>

@if (forecasts == null)
{
    <p>Loading...</p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("api/weather");
    }
}

Authentication with Azure AD B2C

// Program.cs
builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
    options.ProviderOptions.DefaultAccessTokenScopes.Add(
        "https://yourtenant.onmicrosoft.com/api/access_as_user");
});
// wwwroot/appsettings.json
{
  "AzureAdB2C": {
    "Authority": "https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/B2C_1_signupsignin",
    "ClientId": "your-client-id",
    "ValidateAuthority": false
  }
}

Conclusion

Blazor WebAssembly on Azure offers multiple deployment paths, each suited for different scenarios. Azure Static Web Apps provides the simplest deployment for pure client-side apps, while Azure App Service is ideal for hosted solutions requiring server-side processing. Understanding these options allows you to choose the best fit for your application’s requirements.

References

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.