From Zero to Hero? Building a tiny app from scratch using Blazor and .NET 8

September 19, 2023 · 7 minute read · Tags: blazor

.NET 8 has reached its first Release Candidate and with it arrive a number of changes for Blazor - SSR, islands of interactivity using WASM or Server, streaming rendering, and lots more besides.

All promising features, but how do they hold up in practice?

How long does it take to create a simple web app with Blazor and .NET 8 - a tiny app which could (technically at least) be deployed to production?

I sat down to run .NET 8 RC1 through its paces - Here’s how it worked out.

The requirement

Inspired by a good friend of mine who recently created a kind of “Motorsports Dictionary” I decided to hack together something similar, but for Blazor terminology.

The primary feature is a “card wall” showing all the available definitions. Each card needs to show the name of the item and a short summary.

Nice and simple, and a fairly typical example of a requirement for a modern web app.

Start with a new project

With .NET 8 RC1 comes a new Blazor Web App project template.

It defaults to using static server-side rendering for Blazor components, with support for running components interactively using Blazor Server.

dotnet new blazor -n BlazorDictionary

This gives us a single (server) project.

This new project contains a Components folder, in which we find a Pages folder - the new default location for all “routable” components.

Getting something on screen

When building out a new feature, especially in a greenfield app, I tend to start with the UI, and hardcoded HTML.

For me this is the quickest way “in” to a new feature (which I can quickly iterate) but of course you can always take the opposite approach and build the data access part first.

A little bit of style goes a long way

The default Blazor project template uses Bootstrap by default so for speed I decided to stick with that and their handy card component.

A quick look at the official Bootstrap docs reveals this example which we can adapt to our needs:

https://getbootstrap.com/docs/5.3/components/card/#grid-cards

With a few tweaks we can quickly get a prototype of our definition "card wall" up and running.

Home.razor

@page "/"

<PageTitle>Blazor Dictionary</PageTitle>

<div class="row row-cols-1 row-cols-md-3 g-4">
    <div class="col">
        <div class="card h-100">
            <div class="card-body">
                <h2>Server-side rendering</h2>
                <p>Render your Blazor components once, on the server, and return plain HTML to the browser</p>
            </div>
        </div>
    </div>
</div>

My approach here was to start with the HTML and styles needed to show a single definition.

Then, once that's looking OK, duplicate the markup to see what it looks like with multiple definitions on the page.
@page "/"

<PageTitle>Blazor Dictionary</PageTitle>

<div class="row row-cols-1 row-cols-md-3 g-4">
    <div class="col">
        <div class="card h-100">
            <div class="card-body">
                <h2>Server-side rendering</h2>
                <p>Render your Blazor components once, on the server, and return plain HTML to the browser</p>
            </div>
        </div>
    </div>
    <div class="col">
        <div class="card h-100">
            <div class="card-body">
                <h2>Streaming Rendering</h2>
                <p>Slow database call? Make your page load quickly by returning an initial payload of HTML, then stream the rest of the data when it becomes available.</p>
            </div>
        </div>
    </div>
</div>

In this case the resulting markup looks a little verbose. Some of that is bootstrap classes needed to make this work for different screen sizes.

But there’s also the fact we’re now duplicating the same exact markup for each definition.

We can fix that by introducing a shared component for these definitions:

Components/Definition.razor

<div class="col">
    <div class="card h-100">
        <div class="card-body">
            <h2>@Name</h2>
            <p>@Summary</p>
        </div>
    </div>
</div>
@code {

    [Parameter]
    public string Name { get; set; }

    [Parameter]
    public string Summary { get; set; }

}

Nice, now we can remove most of the clutter from Home.razor:

@page "/"

<PageTitle>Blazor Dictionary</PageTitle>

<div class="row row-cols-1 row-cols-md-3 g-4">
    <Definition Name="Server-side rendering" 
                Summary="Render your Blazor components once, on the server, and return plain HTML to the browser"/>
    <Definition Name="Streaming Rendering" 
                Summary="Slow database call? Make your page load quickly by returning an initial payload of HTML, then stream the rest of the data when it becomes available."/>
</div>

Now because this is .NET 8, this page is being rendered statically on the server.

That means we see this in the browser’s dev tools when we visit this page:

When someone visits this app in their browser they're getting a full HTML page response back.

Drive it from data

Now we’ve got a handle on this I’m inclined to get rid of all the repeated markup by pushing the product details into a C# collection.

Let’s start by creating a class to hold the definition data.

Data/ProductListItem.razor

namespace BlazorDictionary.Data;

public class DefinitionItem
{
    public string Name { get; set; }
    public string Summary { get; set; }
}

At this point there are a few ways we can handle fetching/storing this data.

In the interests of simplicity and staying focused on the Blazor part of this new app, I opted to create a simple Store to handle fetching definitions for now.

Data/DefinitionStore.cs

namespace BlazorDictionary.Data;

public class DefinitionStore
{
    List<DefinitionItem> definitions = new()
    {
        new()
        {
            Name = "Server-side rendering",
            Summary = "Render your Blazor components once, on the server, and return plain HTML to the browser"
        },
        new()
        {
            Name = "Streaming Rendering",
            Summary = "Slow database call? Make your page load quickly by returning an initial payload of HTML, then stream the rest of the data when it becomes available."
        }
    };
    
    public IEnumerable<DefinitionItem> Definitions()
    {
        return definitions;
    }
}

This could be replaced, eventually, by database calls, or some other mechanism for fetching definitions.

For now though we can register this in Program.cs:

builder.Services.AddScoped<DefinitionStore>();

Then inject it into Home.razor (and update that component to fetch its definitions list from the new store).

Home.razor

@page "/"
@using BlazorDictionary.Data
@inject DefinitionStore Store

<PageTitle>Blazor Dictionary</PageTitle>

<div class="row row-cols-1 row-cols-md-3 g-4">
    @foreach (var definition in definitions)
    {
        <Definition Name="@definition.Name" Summary="@definition.Summary"/>
    }
</div>
@code {
    
    private IEnumerable<DefinitionItem> definitions;

    protected override void OnInitialized()
    {
        definitions = Store.Definitions();
    }

}

Great! In a few short steps we’ve found ourselves staring at a data-driven V1 of the Blazor definitions page.

Make it searchable

At this point the “out of the box” experience with .NET 8 was holding up nicely so I decided to test it a little further by implementing a basic ability to search definitions.

Now you might be thinking that this is where we need to switch to Blazor Server or WASM to make the product list interactive, but we can actually use an older, more fundamental piece of the web for this - a form!

For this we’ll need a form (well duh!) and a way for our Home component to handle the posted form data (when the form is submitted).

Home.razor

@page "/"
@using BlazorDictionary.Data
@inject DefinitionStore Store

<PageTitle>Blazor Dictionary</PageTitle>

<form method="post" @formname="Search" class="d-flex mb-4 gap-2">
    <input type="text" name="searchTerm" value="@SearchTerm" 
           class="form-control"/>
    <button class="btn btn-primary">Search</button>
    <AntiforgeryToken/>
</form>

<div class="row row-cols-1 row-cols-md-3 g-4">
    @foreach (var definition in definitions)
    {
        <Definition Name="@definition.Name" Summary="@definition.Summary"/>
    }
</div>
@code {

    [SupplyParameterFromForm]
    public string SearchTerm { get; set; }
    
    private IEnumerable<DefinitionItem> definitions;

    protected override void OnInitialized()
    {
        definitions = Store.Definitions(SearchTerm);
    }

}

In .NET 8 we can use regular forms in a server-side rendered Blazor component, and capture the submitted data using the new [SupplyParameterFromForm] attribute.

With that, every time the form is submitted we’ll have access to the entered search term in SearchTerm.

We are also required to give each form a unique formname and use the AntiforgeryToken component to ensure the form data hasn’t been manipulated/forged since the page was originally loaded.

Now all that’s left is to update DefinitionStore.Definitions() to accept and use the string SearchTerm parameter we’re now passing in:

ProductData.cs

public IEnumerable<DefinitionItem> Definitions(string searchTerm)
{
    if (string.IsNullOrEmpty(searchTerm))
    	return definitions;
return definitions.Where(x =>
    	x.Name.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase)
    	|| x.Summary.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase));
}

With that, we have a searchable list of Blazor definitions.

Takeaway

My typical iterative flow for building web apps plays nicely with Blazor and SSR in .NET 8.

Starting with the new Blazor web app template it’s quick to get something up and running in the browser, then use Blazor’s component model to encapsulate parts of the UI into separate components.

Finally, forms make it possible to accept user input via regular form posts without defaulting to Blazor WASM/Server (and the additional overhead they bring).

Check out the code for this app here.

Jon

Next up

Finally! Improved Blazor Server reconnection UX
.NET 9 changes how your Blazor Server app behaves when server connection is lost
.NET 9 improves JavaScript module importing for Blazor
.NET 9 ensures your users always get the latest version of your JS modules
How to use .NET 9 to ensure users always get the latest version of your stylesheets
.NET 9 changes how static files are served, and it solves a long-standing problem