.NET 5 comes with Blazor included, so that you have everything you need to build rich, modern Web apps with.NET and C#. .NET has long supported building high-performance server applications with ASP.NET Core. Blazor in .NET 5 enables building rich, interactive, client-side Web UIs for single page apps using .NET instead of JavaScript. With ASP.NET Core and Blazor, you can build full-stack Web apps with just .NET. Blazor in .NET 5 includes many exciting updates and improvements that will make building your next Web app simple and productive. In this article, I'll show you what Blazor in .NET 5 has to offer.
A Choice of Hosting Models
Blazor apps are made up of reusable UI components. You implement Blazor components using Razor syntax, a natural mixture of HTML and C#. Blazor components handle UI events, manage their own state, and render UI updates. Blazor does the clever work of keeping track of all the rendered updates and figuring out exactly what needs to be updated in the browser DOM.
A typical Blazor Counter component that updates a displayed count each time a button is pressed looks like this:
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
The markup in a Blazor component consists of standard HTML. The @onclick
attribute specifies a C# event handler that gets called each time the user clicks the button. The IncrementCount
method updates the value of the currentCount
field, and then the component renders the updated value. The Web UI updates seamlessly without you having to write a single line of JavaScript.
How Blazor handles updating the UI depends on how your components are hosted. Blazor can execute your components either on the server or client-side in the browser via WebAssembly. .NET 5 includes support for both of these hosting models.
Blazor Server apps execute your UI components on the server from within an ASP.NET Core app. When a Blazor Server app is loaded in the browser, it sets up a real-time connection back to the server using SignalR. Blazor Server uses this connection to manage all UI interactions. Blazor sends all UI events from the browser to the server over the connection and the event is dispatched to the appropriate component to handle the event. The component renders its updates, and Blazor handles serializing the exact UI changes over the SignalR connection so they can then be applied in the browser. Blazor Server apps do all the hard work of managing the UI and app state on the server, while still giving you the rich interactivity of a single-page app.
Blazor WebAssembly apps download the .NET assemblies containing your component implementations to the browser along with a WebAssembly-based .NET runtime and then execute your components and .NET code directly in the browser. From the browser, Blazor dispatches UI events to the appropriate components and then applies the UI updates from the components. All of your .NET code is executed client-side without any required server process.
.NET 5 gives you the choice of two Blazor hosting models: Blazor Server and Blazor WebAssembly. Which model you choose depends on your app requirements. Table 1 summarizes the advantages and disadvantages of each hosting model.
Table 1: Advantages and disadvantages of the different Blazor hosting models
Blazor Server | Blazor WebAssembly | |
---|---|---|
Advantages | ||
Disadvantages |
Regardless of which Blazor hosting model you choose, the way you write your components is the same. The same components can be used with either hosting model. By using components that are hosting model agnostic, you can easily convert a Blazor app from one hosting model to the other.
.NET 5 Core Libraries
Blazor WebAssembly apps in .NET 5 have access to all the .NET 5 APIs from with the browser; you're no longer constrained to .NET Standard 2.1. The functionality of the available APIs is still subject to the limitations imposed by the browser (same origin policy, networking and file system restrictions, etc.), but .NET 5 makes many more APIs available to you, like nullability annotations and Span-based APIs. Blazor WebAssembly projects include a compatibility analyzer to help you know if your Blazor WebAssembly app tries to use .NET 5 APIs that are not supported in the browser.
Blazor WebAssembly in .NET 5 also uses the same core libraries used for server workloads in .NET 5. Unifying on a single implementation of the .NET core libraries is part of the single .NET vision for the .NET 5 and 6 wave. Having a single implementation of the core framework libraries provides greater consistency for app developers and makes the platform much easier to maintain.
New Blazor WebAssembly SDK
All the logic for building, linking, and publishing a Blazor WebAssembly is now packaged in a Blazor WebAssembly SDK. This new SDK replaces the functionality provided previously by the Microsoft.AspNetCore.Components.WebAssembly.Build
NuGet package.
Thanks to the new SDK, the project file for a Blazor WebAssembly app in .NET 5 is simpler. It looks like this:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<!-- Package references -->
</ItemGroup>
</Project>
Improved WebAssembly Runtime Performance
Blazor WebAssembly apps run .NET code directly in the browser using a WebAssembly-based .NET runtime. This runtime is a based on a .NET IL interpreter without any JIT compilation support, so it generally runs .NET code much slower than what you would see from the JIT-based .NET runtime used for native app and server scenarios. For .NET 5, we've improved Blazor WebAssembly performance significantly at multiple layers of the stack. The amount of performance improvement you'll see depends on the type of code you're running.
For arbitrary CPU-intensive code, Blazor WebAssembly in .NET 5 runs about 30% faster than Blazor WebAssembly 3.2. This performance boost is mainly due to optimizations in the core framework libraries, and improvements to the .NET IL interpreter. Things like string comparisons and dictionary lookups are generally much faster in .NET 5 on WebAssembly.
Microsoft did specific optimization work for JSON serialization and deserialization on WebAssembly to speed up all those Web API calls from the browser. JSON handling when running on WebAssembly is approximately two times faster in .NET 5.
Microsoft also made several optimizations to improve the performance of Blazor component rendering, particularly for UI involving lots of components, like when using high-density grids. Component rendering in Blazor WebAssembly is two-to-four times faster in .NET 5, depending on the specific scenario.
Component rendering in Blazor WebAssembly is two-to-four times faster in .NET 5
To test the performance of grid component rendering in .NET 5, Microsoft used three different grid component implementations, each rendering 300 rows with 20 columns:
- Fast Grid: A minimal, highly optimized implementation of a grid
- Plain Table: A minimal but not optimized implementation of a grid
- Complex Grid: A maximal, not optimized implementation of a grid, using a wide range of Blazor features at once
Table 2 shows the performance improvements for these grid rendering scenarios in .NET 5 at the time of this writing.
Table 2: Blazor WebAssembly performance improvements for different grid implementations
Fast Grid | Plain Table | Complex Grid | |
---|---|---|---|
3.2.0 | 162ms | 490ms | 1920ms |
5.0 Preview 8 | 62ms | 291ms | 1050ms |
5.0 RC1 | 52ms | 255ms | 780m |
Improvement | 3.1x | 1.9x | 2.5x |
Virtualization
You can further optimize your Blazor Web UI by taking advantage of the new built-in support for virtualization. Virtualization is a technique for limiting the number of rendered component to just the ones that are currently visible, like when you have a long list or table with many items and only a small subset is visible at any given time. Blazor in .NET 5 adds a new Virtualize component that can be used to easily add virtualization to your components.
A typical list or table-based component might use a C# foreach loop to render each item in the list or each row in the table, like this:
@foreach (var employee in employees)
{
<tr>
<td>@employee.FirstName</td>
<td>@employee.LastName</td>
<td>@employee.JobTitle</td>
</tr>
}
As the size of the list gets large (companies do grow!) rendering all the table rows this way may take a while, resulting in a noticeable UI delay.
Instead, you can replace the foreach
loop with the Virtualize
component, which only renders the rows that are currently visible.
<Virtualize Items="employees" ItemSize="40" Context="employee">
<tr>
<td>@employee.FirstName</td>
<td>@employee.LastName</td>
<td>@employee.JobTitle</td>
</tr>
</Virtualize>
The Virtualize
component calculates how many items to render based on the height of the container and the size of the rendered items in pixels. You specify how to render each item using the ItemContent
template or with child content. If the rendered items end up being slightly off from the specified size, the Virtualize
component adjusts the number of items rendered based on the previously rendered output.
If you don't want to load all items into memory, you can specify an ItemsProvider, like this:
<Virtualize ItemsProvider="LoadEmployees" ItemSize="40" Context="employee">
<tr>
<td>@employee.FirstName</td>
<td>@employee.LastName</td>
<td>@employee.JobTitle</td>
</tr>
</Virtualize>
An items provider is a delegate method that asynchronously retrieves the requested items on demand. The items provider receives an ItemsProviderRequest
, which specifies the required number of items starting at a specific start index. The items provider then retrieves the requested items from a database or other service and returns them as an ItemsProviderResult<TItem>
along with a count of the total number of items available. The items provider can choose to retrieve the items with each request, or cache them so they are readily available.
async ValueTask<ItemsProviderResult<Employee>> LoadEmployees(ItemsProviderRequest request)
{
var numEmployees = Math.Min(request.Count, totalEmployees - request.StartIndex);
var employees = await EmployeesService.GetEmployeesAsync(request.StartIndex, numEmployees, request.CancellationToken);
return new ItemsProviderResult<Employee>(employees, totalEmployees);
}
Because requesting items from a data source might take a bit, you also have the option to render a placeholder until the item is available.
<Virtualize ItemsProvider="LoadEmployees" ItemSize="40" Context="employee">
<ItemContent>
<tr>
<td>@employee.FirstName</td>
<td>@employee.LastName</td>
<td>@employee.JobTitle</td>
</tr>
</ItemContent>
<Placeholder>
<tr>
<td>Loading...</td>
</tr>
</Placeholder>
</Virtualize>
Prerendering for Blazor WebAssembly
Prerendering your Blazor app on the server can significantly speed up the perceived load time of your app. Prerendering works by rendering the UI on the server in response to the first request. Prerendering is also great for search engine optimization (SEO), as it makes your app easier to crawl and index.
Blazor Server apps already have support for prerendering through the component tag helper. The Blazor Server project template is set up by default to prerender the entire app from the Pages/_Host.cshtml
page using the component tag helper.
<component type="typeof(App)" render-mode="ServerPrerendered" />
The component tag helper renders the specified Blazor component into the page or view. Previously, the component tag helper only supported the following rendering modes:
ServerPrerendered
: Prerenders the component into static HTML and includes a marker for a Blazor Server app to later use to make the component interactive when loaded in the browser.
Server
: Renders a marker for a Blazor Server app to use to include an interactive component when loaded in the browser. The component is not prerendered.
Static
: Renders the component into static HTML. The component is not interactive.
In .NET 5, the component tag helper now supports two additional render modes for prerendering a component from a Blazor WebAssembly app:
WebAssemblyPrerendered
: Prerenders the component into static HTML and includes a marker for a Blazor WebAssembly app to later use to make the component interactive when loaded in the browser.
WebAssembly
: Renders a marker for a Blazor WebAssembly app to use to include an interactive component when loaded in the browser. The component is not prerendered.
To set up prerendering in a Blazor WebAssembly app, you first need to host the app in an ASP.NET Core app. Then, replace the default static index.html
file in the client project with a _Host.cshtml
file in the server project and update the server startup logic to fallback to the new page instead of index.html (similar to how the Blazor Server template is set up). Once that's done, you can prerender the root App component like this:
<component type="typeof(App)" render-mode="WebAssemblyPrerendered" />
In addition to dramatically improving the perceived load time of a Blazor WebAssembly app, you can also use the component tag helper with the new render modes to add multiple components on different pages and views. You don't need to configure these components as root components in the app or add your own market tags on the page - the framework handles that for you.
You can also pass parameters to the component tag helper when using the WebAssembly-based render modes if the parameters are serializable.
<component type="typeof(Counter)" render-mode="WebAssemblyPrerendered" param-IncrementAmount="10" />
The parameters must be serializable so that they can be transferred to the client and used to initialize the component in the browser. You'll also need to be sure to author your components so that they can gracefully execute server-side without access to the browser.
CSS Isolation
Blazor now supports CSS isolation, where you can define styles that are scoped to a given component. Blazor applies component-specific styles to only that component without polluting the global styles and without affecting child components. Component-specific CSS styles make it easier to reason about the styles in your app and to avoid unintentional side effects as styles are added, updated, and composed from multiple sources.
You define component-specific styles in a .razor.css
file that matches the name of the .razor
file for the component. For example, let's say you have a component MyComponent.razor
file that looks like this:
<h1>My Component</h1>
<ul class="cool-list">
<li>Item1</li>
<li>Item2</li>
</ul>
You can then define a MyComponent.razor.css
with the styles for MyComponent
:
h1 {
font-family: 'Comic Sans MS'
}
.cool-list li {
color: red;
}
The styles in MyComponent.razor.css
only get applied to the rendered output of MyComponent
; the h1
elements rendered by other components, for example, are not affected.
To write a selector in component specific styles that affects child components, use the ::deep
combinator.
.parent ::deep .child {
color: red;
}
By using the ::deep
combinator, only the .parent
class selector is scoped to the component; the .child
class selector isn't scoped, and matches content from child components.
Blazor achieves CSS isolation by rewriting the CSS selectors as part of the build so that they only match markup rendered by the component. Blazor adds component-specific attributes to the rendered output and updates the CSS selectors to require these attributes. Blazor then bundles together the rewritten CSS files and makes the bundle available to the app as a static Web asset at the path [LIBRARY NAME].styles.css
. Although Blazor doesn't natively support CSS preprocessors like Sass or Less, you can still integrate CSS preprocessors with Blazor projects to generate component specific styles before they're rewritten as part of the Blazor build system.
Lazy Loading
Lazy loading enables you to improve the load time of your Blazor WebAssembly app by deferring the download of some assemblies until they are required. For many apps, lazy loading different parts of the app isn't necessary. The .NET IL that makes up .NET assemblies is very compact, especially when its compressed. You may be surprised by how much code you have to write in your app before it significantly impacts the app size. The download size of a Blazor WebAssembly app is typically dominated by the size of the runtime and core framework libraries, which Blazor aggressively trims to remove unused code. Lazy loading may be helpful if your Blazor WebAssembly app grows exceptionally large or you have parts of your app with large dependencies that aren't used elsewhere and can't be reasonably reduced through IL trimming.
Normally, Blazor downloads and loads all dependencies of the app when it's first loaded. To delay the loading of a .NET assembly, you add it to the BlazorWebAssemblyLazyLoad
item group in your project file:
<BlazorWebAssemblyLazyLoad Include="Lib1.dll" />
Assemblies marked for lazy loading must be explicitly loaded by the app before they're used. To lazy load assemblies at runtime, use the LazyAssemblyLoader
service:
@inject LazyAssemblyLoader LazyAssemblyLoader
@code {
var assemblies = await LazyAssemblyLoader.LoadAssembliesAsync(new string[] {"Lib1.dll"});
}
Often, assembles need to be loaded when the user navigates to a particular page. The Router
component has a new OnNavigateAsync
event that's fired on every page navigation and can be used to lazy load assemblies for a particular route. You can also lazily load the entire page for a route by passing any loaded assemblies as additional assemblies to the Router.
You can see a full example of integrating lazy loading with the Router
component in Listing 1.
Listing 1: Integrating lazy loading with the Blazor Router
@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@inject LazyAssemblyLoader LazyAssemblyLoader
<Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="@lazyLoadedAssemblies" OnNavigateAsync="@OnNavigateAsync">
<Navigating>
<div>
<p>Loading the requested page...</p>
</div>
</Navigating>
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
@code {
private List<Assembly> lazyLoadedAssemblies = new List<Assembly>();
private async Task OnNavigateAsync(NavigationContext args)
{
if (args.Path.EndsWith("/page1"))
{
var assemblies = await LazyAssemblyLoader.LoadAssembliesAsync(new string[] { "Lib1.dll" });
lazyLoadedAssemblies.AddRange(assemblies);
}
}
}
Less JavaScript, More C#
Blazor's purpose is to enable building rich interactive Web UIs with .NET, but sometimes you still need some JavaScript. This might be because you want to reuse an existing library or because Blazor doesn't yet expose a native browser capability that you need. Blazor supports JavaScript interop, where you can call into any JavaScript code from your .NET code, but writing JavaScript interop code with Blazor should be rare. In .NET 5, Microsoft has added some new built-in features that reduce or eliminate the amount of JavaScript interop code required for some common scenarios.
Set UI Focus
Sometimes you need to set the focus on a UI element programmatically. Blazor in .NET 5 now has a convenient method on ElementReference
for setting the UI focus on that element.
<button @onclick="() => textInput.FocusAsync()">Set focus</button>
<input @ref="textInput"/>
File Upload
Blazor now offers an InputFile
component for handling file uploads. The InputFile component is based on an HTML input of type “file”. By default, you can upload single files, or you can add the “multiple” attribute to enable support for multiple files. When one or more files is selected for upload, the InputFile
component fires an OnChange
event and passes in an InputFileChangeEventArgs
that provides access to the selected file list and details about each file.
<InputFile OnChange="OnInputFileChange" multiple />
To read a file, you call OpenReadStream
on the file and read from the returned stream. In a Blazor WebAssembly app this reads the file into memory on the client. In a Blazor Server app, the file is transmitted to the server and read into memory on the server. Blazor also provides a ToImageFileAsync
convenience method for resizing images files before they're uploaded.
The example code in Listing 2 shows how to resize and reformat user selected images as 100x100 pixel PNG files. The example code then uploads the resized images and displays them as data URLs.
Listing 2: Using InputFile to resize and upload multiple images
<div class="image-list">
@foreach (var imageDataUrl in imageDataUrls)
{
<img src="@imageDataUrl" />
}
</div>
@code {
ElementReference textInput;
IList<string> imageDataUrls = new List<string>();
async Task OnInputFileChange(InputFileChangeEventArgs e)
{
var imageFiles = e.Files.Where(file => file.Type.StartsWith("image/"));
var format = "image/png";
foreach (var imageFile in imageFiles)
{
var resizedImageFile = await imageFile.ToImageFileAsync(format, 100, 100);
var buffer = new byte[resizedImageFile.Size];
await resizedImageFile.OpenReadStream().ReadAsync(buffer);
var imageDataUrl = $"data:{format};base64,{Convert.ToBase64String(buffer)}";
imageDataUrls.Add(imageDataUrl);
}
}
}
Influencing the HTML Head
Use the new Title, Link, and Meta components to programmatically set the title of a page and dynamically add link and meta tags to the HTML head in a Blazor app. To use these components, add a reference to the Microsoft.AspNetCore.Components.WebExtensions
NuGet packages.
The following example programmatically sets the page title to show the number of unread user notifications, and updates the page icon a as well:
@if (unreadNotificationsCount > 0)
{
var title = $"Notifications ({unreadNotificationsCount})";
<Title Value="title"></Title>
<Link rel="icon" href="icon-unread.ico" />
}
Protected Browser Storage
In Blazor Server apps, you may want to persist the app state in local or session storage so that the app can rehydrate it later if needed. When storing app state in the user's browser, you also need to ensure that it hasn't been tampered with. Blazor in .NET 5 helps solve this problem by providing two new services: ProtectedLocalStorage
and ProtectedSessionStorage
. These services help you store state in local and session storage respectively, and they take care of protecting the stored data using the ASP.NET Core data protection APIs.
To use the new services, simply inject either of them into your component implementations:
@inject ProtectedLocalStorage LocalStorage
@inject ProtectedSessionStorage SessionStorage
You can then get, set, and delete state asynchronously.
private async Task IncrementCount()
{
await LocalStorage.SetAsync("count", ++currentCount);
}
JavaScript Isolation and Object References
When you do need to write some JavaScript for your Blazor app, Blazor now enables you to isolate your JavaScript as standard JavaScript modules. This has a couple of benefits: imported JavaScript no longer pollutes the global namespace, and consumers of your library and components no longer need to manually import the related JavaScript.
For example, the following JavaScript module exports a simple JavaScript function for showing a browser prompt:
export function showPrompt(message) {
return prompt(message, 'Type anything here');
}
You can add this JavaScript module to your .NET library as a static Web asset (wwwroot/exampleJsInterop.js
) using the Razor SDK and then import the module into your .NET code using the IJSRuntime service:
var module = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "./_content/MyComponents/exampleJsInterop.js");
The import
identifier is a special identifier used specifically for importing the specified JavaScript module. You specify the module using its stable static Web asset path: _content/[LIBRARY NAME]/[PATH UNDER WWWROOT]
.
The IJSRuntime
imports the module as an IJSObjectReference
, which represents a reference to a JavaScript object from .NET code. You can then use the IJSObjectReference
to invoke exported JavaScript functions from the module:
public async ValueTask<string> Prompt(string message)
{
return await module.InvokeAsync<string>("showPrompt", message);
}
Debugging Improvements
Blazor WebAssembly debugging is significantly improved in .NET 5. Launching a Blazor WebAssembly app for debugging is now much faster and more reliable. The debugger now breaks on unhandled exceptions in your code. Microsoft has enabled support for debugging into external dependencies. The Blazor WebAssembly debug proxy has also been moved into its own process to enable future work to support debugging Blazor WebAssembly apps running in remote environments like Docker, Windows Subsystem for Linux, and Codespaces.
See UI Updates Faster with dotnet watch
When building UIs, you need to be able to see your changes as fast as possible. .NET 5 makes this easy with some nice improvements to the dotnet watch
tool. When you run dotnet watch run
on a project, it watches your files for code changes and then rebuilds and restarts the app so that you can see the results of your changes quickly. New in .NET 5, dotnet watch
launches your default browser for you once the app is started and auto refreshes the browser as you make changes. This means that you can open your Blazor project (or any ASP.NET Core project) in your favorite text editor, run dotnet watch run
once, and then focus on your code changes while the tooling handles rebuilding, restarting, and reloading your app. Microsoft expects to bring the auto refresh functionality to Visual Studio as well.
Now, dotnet watch launches your default browser for you once the app is started and auto refreshes the browser as you make changes.
More Blazor Goodies
Although this article summarizes most of the major new Blazor features in .NET 5, there are still plenty of other smaller Blazor enhancements and improvements to check out. These improvements include:
IAsyncDisposable
support- Control Blazor component instantiation
- New
InputRadio
component - Support for catch-all route parameters
- Support for toggle events
- Parameterless
InvokeAsync
overload onEventCallback
- Blazor Server reconnection improvements
- Prerendering for Blazor WebAssembly apps
Many of these features and improvements came from the enthusiastic community of open source contributors. Microsoft greatly appreciates all of the preview feedback, bug reports, feature suggestions, doc updates, and design, and code reviews from countless individuals. Thank you to everyone who helped with this release. I hope you enjoy Blazor in .NET 5! Give Blazor a try today by going to https://blazor.net.