A first look at Blazor and .NET 8

Damien Bod recently took an existing .NET 7 Blazor project, secured with the backend for frontend (BFF) pattern and Azure AD, and moved it over to a .NET 8 preview. The goal was straightforward: see what breaks during the upgrade and try out a couple of features that shipped early in the preview cycle. This kind of hands on exploration is worth reading if you run Blazor in production and are already thinking about your own .NET 8 migration timeline.

The full sample is on GitHub at github.com/damienbod/Hostedblazor8Aad, built on top of the Blazor.BFF.AzureAD.Template project that Damien Bod maintains separately.

Upgrading the BFF-secured Blazor project

The starting point was a .NET 7 project that implements Azure AD authentication using the backend for frontend architecture as a best practice. In this setup, the security logic lives entirely in the backend, and the Blazor components on the client stay simple, with no tokens or sensitive session state handled in the browser. The Blazor.BFF.AzureAD.Template template took care of the initial project setup, including the security headers, since Microsoft does not ship an official template for this pattern yet.

Moving to .NET 8 at the project file level is a one line change.

<TargetFramework>net8.0</TargetFramework>

That single line is the easy part. The real work is updating every NuGet package reference alongside it, since a mismatched package and target framework combination is a common source of build errors right after an upgrade. Microsoft.Identity.Web handles the OpenID Connect confidential client here, backed by an Azure App registration configured with a web client and a user secret. For production workloads, swapping that secret for a certificate is worth doing, since certificate based authentication avoids sending a shared secret over the wire during the token request step of the OIDC code flow.

Running the upgraded application surfaced the same development friction that existed on .NET 7. The browser console throws warnings because Visual Studio’s debugging tools try to inject inline scripts into the page, and a strict Content Security Policy blocks them. This is expected, and honestly the right outcome, since a production deployment should be running with the same strict CSP and HTTPS configuration as local development. Testing against a weaker local policy just pushes CSP bugs to the day you deploy, which is a worse time to find them. A second, unexplained warning also shows up looking for a JS source map file that the project does not use. As of May 2023, Damien Bod noted that the CSP related bug had been fixed in the latest Visual Studio preview release, tracked on the Visual Studio developer community site.

Console warnings from Visual Studio's debugging tools being blocked by CSP during local development.
Console warnings from Visual Studio’s debugging tools being blocked by CSP during local development.

Generating test data with Random.Shared.GetItems

System.Random picked up a new GetItems method in .NET 8, and it turned out to be a convenient way to generate test data without hand rolling a random picker. The data itself is stored as an array and exposed as a ReadOnlySpan, which avoids an extra allocation when the caller only needs to read the values.

public static ReadOnlySpan<MyGridData> GetData()
{
	return _mydata.AsSpan();
}

One thing worth flagging if you have not used Span<T> much: it is a stack only type, so you cannot store it in a class field or pass it across an await boundary inside an async method. That is fine here because the span is created and consumed within the same synchronous call, but it is a real constraint to keep in mind if you try to reuse this pattern inside asynchronous data access code.

The API endpoint itself calls Random.Shared.GetItems to pull 24 random entries out of that span, which then get serialized and sent down to the grid on the client.

[HttpGet]
public IEnumerable<MyGridData> Get()
{
	return Random.Shared.GetItems(MyData.GetData(), 24);
}

The result is 24 randomly selected rows on every request, which is a nice trick for exercising a grid’s paging and sorting without maintaining a large seed dataset. One practical caveat: Random.Shared is not cryptographically secure, and it should never be used to generate anything like tokens, invite codes, or other values with security implications. For demo or test data, it is exactly the right tool.

Trying out QuickGrid in the WASM client

QuickGrid also shipped in .NET 8, giving Blazor a built in grid component with sorting and paging, without having to pull in a third party grid library for something this basic. The package needs to be added to the client, or WASM, project specifically, since the grid renders in the browser in this hosted setup.

Microsoft.AspNetCore.Components.QuickGrid

Once referenced, QuickGrid drops into any Razor page like any other component. The full page below fetches the random grid data from the API, wires it into a QuickGrid with sortable, typed columns, and adds a search box against the Name column using ColumnOptions.

@page "/directapi"
@using HostedBlazorAad.Shared
@using Microsoft.AspNetCore.Components.QuickGrid
@inject IAntiforgeryHttpClientFactory httpClientFactory
@inject IJSRuntime JSRuntime
 
<h3>QuickGrid display using data  Direct API</h3>
 
@if (myGridData == null)
{
    <p><em>Loading...</em></p>
}
else
{
<hr />
 
<QuickGrid Items="@FilteredItems" Pagination="@pagination">
 
    <PropertyColumn Property="@(p => p.Id)" Sortable="true" />
 
    <PropertyColumn Property="@(c => c.Name)" Sortable="true" Class="name">
        <ColumnOptions>
            <div class="search-box">
                <input type="search" autofocus @bind="nameFilter" @bind:event="oninput" placeholder="name..." />
            </div>
        </ColumnOptions>
    </PropertyColumn>
 
    <PropertyColumn Property="@(p => p.Colour)" Sortable="true" />
</QuickGrid>
 
<Paginator State="@pagination" />
}
 
@code {
 
    private IEnumerable<MyGridData>? myApiData;
    private IQueryable<MyGridData> myGridData = new List<MyGridData>().AsQueryable();
    private PaginationState pagination = new PaginationState { ItemsPerPage = 8 };
    private string nameFilter = string.Empty;
 
    GridSort<MyGridData> rankSort = GridSort<MyGridData>
        .ByDescending(x => x.Name)
        .ThenDescending(x => x.Colour)
        .ThenDescending(x => x.Id);
 
    IQueryable<MyGridData>? FilteredItems => myGridData.Where(x => x.Name.Contains(nameFilter, StringComparison.CurrentCultureIgnoreCase));
 
    protected override async Task OnInitializedAsync()
    {
        var client = await httpClientFactory.CreateClientAsync();
 
        var myApiData = await client.GetFromJsonAsync<MyGridData[]>("api/DirectApi");
 
        if (myApiData != null) myGridData = myApiData.AsQueryable();
    }
}

A few things stand out in this code. The HTTP call goes through IAntiforgeryHttpClientFactory rather than a plain HttpClient, which keeps the antiforgery token handling consistent with the rest of the BFF pattern’s protection against cross site request forgery on calls back to the backend. The grid itself pages through 8 items at a time, and the name filter runs as a LINQ Where clause directly against the in memory IQueryable.

That last point is the one production trade off worth calling out. All 24 rows are loaded into WASM memory up front, and both the search and the paging happen entirely client side. That is perfectly fine for a demo with two dozen rows, but it will not hold up once a dataset grows into the thousands. QuickGrid does support a server side data source through GridItemsProvider for exactly that scenario, so if you reuse this pattern for a real feature, plan to switch to that provider once the dataset stops being trivially small.

QuickGrid rendering the 24 random rows with client side sorting and paging set to 8 items per page.
QuickGrid rendering the 24 random rows with client side sorting and paging set to 8 items per page.

What this preview run tells you

This was an early preview, so treat the specifics as directional rather than final. Damien Bod expects Blazor’s project structure to change further before .NET 8 ships, with what was then referred to as Blazor United likely collapsing the current three project structure (server, client, and shared) down to one. Features like GetItems and QuickGrid are additive and low risk to adopt early, since they do not touch your hosting model. Structural changes to the project template are the ones worth watching closely if you have a BFF style security setup, because they can affect how and where your authentication code lives.

The other recurring point in this piece, and one Damien Bod has made consistently across his BFF related posts, is a concern that Microsoft’s default Blazor security guidance still places some responsibility in the WASM client rather than fully in the backend. Anything executing in the browser is visible and can be tampered with, so treating the backend as the actual trust boundary, the way the BFF pattern does, remains the safer default for anything beyond a low stakes internal tool.

Leave a Reply

Discover more from Behind the Stack

Subscribe now to keep reading and get access to the full archive.

Continue reading