Leveraging Blazor for Building Interactive Web UIs in ASP.NET Core
Bringing C# to the Browser: The Future of Web Development
Blazor has revolutionized how .NET developers approach web UI development by enabling them to build rich, interactive web applications using C# instead of JavaScript. This powerful framework brings the reliability and productivity of the .NET ecosystem directly to front-end development, allowing developers to share code and libraries between client and server.
Whether you're a seasoned ASP.NET developer or new to the Microsoft stack, Blazor offers compelling advantages for creating modern web experiences with the language and tools you already know and love.
Introduction
Remember when building web applications meant juggling multiple languages and frameworks? Server-side code in C# or another backend language, client-side in JavaScript, plus HTML and CSS—each with its own syntax, quirks, and development patterns. This context-switching has long been one of the most challenging aspects of web development.
Enter Blazor, Microsoft's game-changing web framework that allows developers to build interactive web UIs using C# instead of JavaScript. Released as part of the ASP.NET Core ecosystem, Blazor represents a fundamental shift in how .NET developers can approach web development.
In this article, we'll explore what makes Blazor special, how it works under the hood, and practical approaches to building robust, interactive web applications. I'll share real-world examples, best practices, and tips to help you leverage the full power of Blazor in your ASP.NET Core projects.
Understanding Blazor: What It Is and How It Works
At its core, Blazor is a framework for building interactive web UIs using .NET. Instead of relying on JavaScript, Blazor allows developers to use C# and Razor syntax to create dynamic web applications. This approach brings several advantages:
You can write both client and server code in C#
You can share code and libraries between client and server
You can leverage the extensive .NET ecosystem
You benefit from strong typing and compile-time checking
You get access to modern C# language features
Blazor comes in two primary hosting models: Blazor WebAssembly (client-side) and Blazor Server (server-side). Let's explore these options in more detail.
Blazor WebAssembly: Client-Side Execution
Blazor WebAssembly runs entirely in the browser using WebAssembly (often abbreviated as Wasm). WebAssembly is a binary instruction format that provides near-native performance in the browser. When a user visits a Blazor WebAssembly application, the browser downloads the .NET runtime, your application, and its dependencies. Once downloaded, the application runs entirely on the client.
The key advantages of Blazor WebAssembly include:
Offline support, as the application runs entirely in the browser
Reduced server load since processing occurs on the client
Ability to deploy as static files
No server round-trips for UI interactions
The current version of Blazor WebAssembly uses the .NET runtime compiled to WebAssembly, allowing C# code to run directly in the browser. Microsoft continues to optimize this model with each release, improving download sizes and startup performance.
Blazor Server: Server-Side Execution
Blazor Server, on the other hand, runs your components on the server. When a user interacts with a Blazor Server application, those interactions are sent to the server over a SignalR connection (a real-time communication library). The server processes the interactions, updates its DOM representation, and sends back UI updates to the client.
The primary benefits of Blazor Server include:
Smaller download size and faster initial load times
Immediate access to server resources and APIs
Works with browsers that don't support WebAssembly
Full debugging support in Visual Studio
The tradeoff is that Blazor Server requires an active connection to the server and may face scalability challenges with large numbers of concurrent users.
How Blazor Components Work
Regardless of the hosting model, Blazor applications are built from components. A component in Blazor is a self-contained chunk of user interface (UI) that includes HTML markup and the processing logic needed to inject data or respond to UI events.
Components are defined in .razor
files, which combine HTML markup with C# code. Here's a simple example:
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
This component defines a counter page with a button that, when clicked, increments a counter. The @code
block contains the C# code that defines the component's state and behavior, while the HTML markup defines its appearance. The @
symbol is used to transition between HTML and C#.
Components can be nested and reused, allowing you to build complex UIs from simpler building blocks. This component-based architecture aligns well with modern web development practices and makes it easier to maintain large applications.
Getting Started with Blazor in ASP.NET Core
Now that we understand what Blazor is and how it works, let's look at how to get started with it.
Prerequisites
To begin developing with Blazor, you'll need:
.NET 8 SDK or later (download from Microsoft's .NET download page)
A code editor like Visual Studio 2022, Visual Studio Code with the C# extension, or JetBrains Rider
Creating Your First Blazor Application
The easiest way to create a new Blazor application is to use the .NET CLI. Open a terminal or command prompt and run:
dotnet new blazorserver -o MyFirstBlazorApp
cd MyFirstBlazorApp
dotnet run
This creates a new Blazor Server application and runs it. Navigate to the URL shown in the console (typically
https://localhost:5001
) to see your application.
If you prefer Blazor WebAssembly, you can use:
dotnet new blazorwasm -o MyFirstBlazorWasmApp
cd MyFirstBlazorWasmApp
dotnet run
Project Structure
When you create a new Blazor application, you'll see a structure similar to this:
Program.cs
: The entry point for the application, where services are configuredApp.razor
: The root component of the application_Imports.razor
: Contains common using directives for Razor fileswwwroot/
: Contains static files like CSS, JavaScript, and imagesPages/
: Contains the routable components (pages) of your applicationShared/
: Contains common components like layout and navigationData/
orServices/
: Contains classes for business logic and data access
This structure follows ASP.NET Core conventions and makes it easy to organize your application as it grows.
Building Interactive UIs with Blazor Components
The heart of Blazor development is working with components. Let's explore how to create and use components effectively.
Component Basics
As mentioned earlier, Blazor components are defined in .razor
files. Each component consists of HTML markup and C# code. The markup describes the component's UI, while the code defines its behavior.
Components can:
Accept parameters from parent components
Maintain their own state
Handle UI events
Render other components
Here's an example of a simple component that accepts a parameter:
@* TodoItem.razor *@
<div class="todo-item @(IsDone ? "done" : "")">
<input type="checkbox" @bind="IsDone" />
<span>@Title</span>
</div>
@code {
[Parameter]
public string Title { get; set; }
[Parameter]
public bool IsDone { get; set; }
}
This component represents a to-do item with a checkbox. The [Parameter]
attribute indicates that Title
and IsDone
should be provided by the parent component.
To use this component in another component or page:
<TodoItem Title="Learn Blazor" IsDone="false" />
<TodoItem Title="Build a Blazor app" IsDone="true" />
Data Binding
One of Blazor's strengths is its data binding system. Blazor supports both one-way and two-way data binding:
One-way binding:
@VariableName
or@Expression
Two-way binding:
@bind="Property"
or@bind-Value="Property"
For example, to create a simple form with two-way binding:
<input @bind="userName" />
<p>Hello, @userName!</p>
@code {
private string userName = "Guest";
}
In this example, when the user types in the input field, the userName
variable is automatically updated, and the greeting updates to reflect the new value.
For more complex scenarios, you can use @bind-Value
with @bind-ValueChanged
to specify custom binding behavior:
<input @bind-value="searchTerm" @bind-value:event="oninput" />
@code {
private string searchTerm;
}
This example updates the searchTerm
variable as the user types, rather than waiting for the input element to lose focus.
Handling Events
Blazor provides a simple way to handle DOM events using the @on{Event}
syntax. For example, to handle a button click:
<button @onclick="HandleClick">Click me</button>
@code {
private void HandleClick()
{
// Handle the click
}
}
You can also pass parameters to event handlers:
<button @onclick="() => DeleteItem(item.Id)">Delete</button>
@code {
private void DeleteItem(int id)
{
// Delete the item with the given ID
}
}
For more complex event handling, you can use EventCallback<T>
to create callbacks that can be passed to child components:
@* ParentComponent.razor *@
<ChildComponent OnSave="@HandleSave" />
@code {
private void HandleSave(SaveEventArgs args)
{
// Handle the save
}
}
@* ChildComponent.razor *@
<button @onclick="TriggerSave">Save</button>
@code {
[Parameter]
public EventCallback<SaveEventArgs> OnSave { get; set; }
private async Task TriggerSave()
{
await OnSave.InvokeAsync(new SaveEventArgs { /* ... */ });
}
}
Component Lifecycle
Blazor components have a lifecycle that you can hook into to perform actions at specific times. The most commonly used lifecycle methods are:
OnInitialized
/OnInitializedAsync
: Called when the component is initializedOnParametersSet
/OnParametersSetAsync
: Called when parameters have been setOnAfterRender
/OnAfterRenderAsync
: Called after the component has been rendered
For example, to load data when a component initializes:
@inject DataService DataService
<h1>Products</h1>
@if (products == null)
{
<p>Loading...</p>
}
else
{
<ul>
@foreach (var product in products)
{
<li>@product.Name</li>
}
</ul>
}
@code {
private List<Product> products;
protected override async Task OnInitializedAsync()
{
products = await DataService.GetProductsAsync();
}
}
This component injects a DataService
, then uses OnInitializedAsync
to load products when the component initializes.
Advanced Blazor Techniques and Patterns
Once you're comfortable with the basics, there are several advanced techniques that can help you build more sophisticated Blazor applications.
State Management
In simple applications, component state can be managed directly in the components. However, as your application grows, you'll likely need more sophisticated state management solutions.
Using Services for State Management
One approach is to use services to hold shared state:
// CounterState.cs
public class CounterState
{
private int _count = 0;
public int Count => _count;
public event Action OnChange;
public void IncrementCount()
{
_count++;
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
}
Register this service in Program.cs
:
builder.Services.AddSingleton<CounterState>();
Then, inject and use it in your components:
@page "/counter"
@inject CounterState CounterState
@implements IDisposable
<h1>Counter</h1>
<p>Current count: @CounterState.Count</p>
<button class="btn btn-primary" @onclick="CounterState.IncrementCount">Click me</button>
@code {
protected override void OnInitialized()
{
CounterState.OnChange += StateHasChanged;
}
public void Dispose()
{
CounterState.OnChange -= StateHasChanged;
}
}
This pattern allows multiple components to share and update the same state.
Flux/Redux Pattern
For more complex state management, you might consider implementing a Flux or Redux-like pattern. There are several libraries available for this purpose, such as Fluxor and Blazor-State.
These libraries provide a centralized store for your application state and a structured way to update that state through actions and reducers.
JavaScript Interoperability
While one of Blazor's main selling points is reducing the need for JavaScript, there are times when you need to interact with JavaScript libraries or APIs. Blazor provides several ways to do this.
Calling JavaScript from .NET
To call JavaScript functions from your C# code, you can use the IJSRuntime
service:
@inject IJSRuntime JSRuntime
<button @onclick="ShowAlert">Show Alert</button>
@code {
private async Task ShowAlert()
{
await JSRuntime.InvokeVoidAsync("alert", "Hello from Blazor!");
}
}
For more complex interactions, you can create a JavaScript file in the wwwroot
folder:
// wwwroot/js/app.js
window.myApp = {
showToast: function(message) {
// Show a toast notification
},
getLocation: function() {
return {
latitude: 37.7749,
longitude: -122.4194
};
}
};
Then call these functions from C#:
await JSRuntime.InvokeVoidAsync("myApp.showToast", "Operation completed successfully");
var location = await JSRuntime.InvokeAsync<Location>("myApp.getLocation");
Console.WriteLine($"Latitude: {location.Latitude}, Longitude: {location.Longitude}");
Calling .NET from JavaScript
You can also call .NET methods from JavaScript using JavaScript interop. First, define a method with the [JSInvokable]
attribute:
public class ExampleJsInterop
{
[JSInvokable]
public static string GetHelloMessage(string name)
{
return $"Hello, {name}!";
}
}
Then, create a reference to the .NET instance in JavaScript:
// In a component
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JSRuntime.InvokeVoidAsync("exampleJsFunctions.setDotNetReference", DotNetObjectReference.Create(this));
}
}
[JSInvokable]
public string GetMessage()
{
return "Hello from .NET!";
}
In your JavaScript file:
window.exampleJsFunctions = {
setDotNetReference: function(dotNetReference) {
this.dotNetReference = dotNetReference;
},
callDotNetMethod: function() {
return this.dotNetReference.invokeMethodAsync('GetMessage');
}
};
Now you can call exampleJsFunctions.callDotNetMethod()
from JavaScript to invoke the .NET method.
Forms and Validation
Blazor provides built-in support for forms and validation using data annotations.
Basic Forms
For simple forms, you can use the EditForm
component:
<EditForm Model="@person" OnValidSubmit="@HandleValidSubmit">
<div>
<label for="name">Name:</label>
<InputText id="name" @bind-Value="person.Name" />
</div>
<div>
<label for="email">Email:</label>
<InputText id="email" @bind-Value="person.Email" />
</div>
<button type="submit">Submit</button>
</EditForm>
@code {
private Person person = new Person();
private void HandleValidSubmit()
{
// Process the form
}
}
public class Person
{
public string Name { get; set; }
public string Email { get; set; }
}
Validation
To add validation, you can use data annotations and the DataAnnotationsValidator
component:
public class Person
{
[Required]
[StringLength(50, ErrorMessage = "Name is too long.")]
public string Name { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
}
Then, update your form:
<EditForm Model="@person" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div>
<label for="name">Name:</label>
<InputText id="name" @bind-Value="person.Name" />
<ValidationMessage For="@(() => person.Name)" />
</div>
<div>
<label for="email">Email:</label>
<InputText id="email" @bind-Value="person.Email" />
<ValidationMessage For="@(() => person.Email)" />
</div>
<button type="submit">Submit</button>
</EditForm>
This form will display validation errors and will only call HandleValidSubmit
if all validations pass.
Authentication and Authorization
Blazor works seamlessly with ASP.NET Core's authentication and authorization systems. Let's look at how to implement authentication in a Blazor application.
Setting Up Authentication
For a Blazor Server application, you can use the built-in authentication templates:
dotnet new blazorserver -au Individual -o MyAuthenticatedApp
This creates a Blazor Server application with individual user accounts stored in a SQL Server database.
For Blazor WebAssembly, you can use:
dotnet new blazorwasm -au Individual -o MyWasmAuthApp
This creates a Blazor WebAssembly application with authentication handled by an ASP.NET Core backend.
Using Authentication in Components
Once authentication is set up, you can use the AuthorizeView
component to display different content based on the user's authentication state:
<AuthorizeView>
<Authorized>
<h1>Hello, @context.User.Identity.Name!</h1>
<p>You are authorized.</p>
</Authorized>
<NotAuthorized>
<h1>Authentication Failure!</h1>
<p>You are not authorized to access this resource.</p>
</NotAuthorized>
</AuthorizeView>
You can also require authentication for entire pages or components using the [Authorize]
attribute:
@page "/secured-page"
@attribute [Authorize]
<h1>Secured Page</h1>
<p>This page can only be accessed by authenticated users.</p>
To require specific roles or policies:
@attribute [Authorize(Roles = "Admin")]
Or:
@attribute [Authorize(Policy = "RequireAdminRole")]
Real-World Blazor Scenarios and Best Practices
Now that we've covered the major features of Blazor, let's look at some real-world scenarios and best practices.
Performance Optimization
Blazor applications, especially WebAssembly ones, can face performance challenges. Here are some tips for optimizing performance:
Minimize Component Rendering: Use the
@key
directive to help Blazor optimize rendering:
@foreach (var item in items)
{
<TodoItem @key="item.Id" Title="@item.Title" />
}
Lazy Loading: For Blazor WebAssembly, use lazy loading to reduce the initial download size:
// In Program.cs
builder.Services.AddScoped<LazyAssemblyLoader>();
@page "/lazy"
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@inject LazyAssemblyLoader LazyLoader
@if (!isLoaded)
{
<p>Loading...</p>
}
else
{
<DynamicComponent Type="@componentType" />
}
@code {
private bool isLoaded;
private Type componentType;
protected override async Task OnInitializedAsync()
{
var assemblies = await LazyLoader.LoadAssembliesAsync(
new[] { "MyApp.LargeComponents.dll" });
componentType = assemblies
.SelectMany(a => a.GetTypes())
.First(t => t.Name == "HeavyComponent");
isLoaded = true;
}
}
Virtual Lists: For long lists, consider using a virtualization component:
<Virtualize Items="@largeList" Context="item">
<p>@item.Text</p>
</Virtualize>
Prerendering: For Blazor WebAssembly, use prerendering to improve perceived performance:
// In the server-side Program.cs
app.MapFallbackToPage("/_Host");
Error Handling
Proper error handling is crucial for production applications. Here are some approaches:
Global Error Handling: Create an error boundary component:
@inherits ErrorBoundary
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
protected override void OnInitialized()
{
ErrorContent = builder =>
{
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", "error-ui");
builder.AddContent(2, "An error has occurred.");
builder.CloseElement();
};
}
}
Use this component to wrap other components:
<ErrorBoundaryComponent>
<YourComponent />
</ErrorBoundaryComponent>
Try-Catch Blocks: Use try-catch blocks in event handlers and lifecycle methods:
csharp
private async Task HandleClick()
{
try
{
await DoSomethingThatMightFailAsync();
}
catch (Exception ex)
{
// Log the error
logger.LogError(ex, "An error occurred during button click");
// Show user-friendly message
errorMessage = "Something went wrong. Please try again.";
}
}
Testing Blazor Applications
Testing is an essential part of building reliable applications. Here's how to approach testing Blazor components:
Unit Testing: Use bUnit to test individual components:
[Fact]
public void CounterShouldIncrementWhenButtonIsClicked()
{
// Arrange
using var ctx = new TestContext();
var cut = ctx.RenderComponent<Counter>();
var paraElm = cut.Find("p");
// Act - click the button
cut.Find("button").Click();
// Assert
paraElm.MarkupMatches("<p>Current count: 1</p>");
}
Integration Testing: Use Playwright or Selenium to test the full application:
[Fact]
public async Task NavigationWorks()
{
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
await page.GotoAsync("https://localhost:5001");
await page.ClickAsync("a[href='counter']");
var heading = await page.TextContentAsync("h1");
Assert.Equal("Counter", heading);
}
Deployment Strategies
Deploying Blazor applications differs based on the hosting model:
Blazor Server: Deploy as a standard ASP.NET Core application to:
Azure App Service
IIS
Docker containers
Linux with Nginx/Apache and Kestrel
Blazor WebAssembly: Deploy the static files to:
Azure Static Web Apps
GitHub Pages
Netlify or Vercel
Any static file hosting service
For Blazor WebAssembly applications with an API backend, you might deploy the static files separately from the API or use a solution like Azure Static Web Apps that can host both.
Security Considerations
When building Blazor applications, keep these security considerations in mind:
For Blazor WebAssembly:
Assume all client-side code and data can be viewed by users
Never store secrets in WebAssembly applications
Always validate data on the server
Use HTTPS to secure data in transit
For Blazor Server:
Protect the SignalR connection with proper authentication
Be aware of increased server memory usage for maintaining client state
Consider the implications of server-side session state in a scaled-out environment
For both:
Implement proper authentication and authorization
Use Content Security Policy (CSP) headers
Be cautious with JavaScript interop, as it can expose security vulnerabilities
Conclusion
Blazor represents a significant advancement in web development for .NET developers. By bringing C# to the browser, it eliminates the context-switching between languages and provides a more cohesive development experience. Whether you choose Blazor Server for its smaller download size and server resources access, or Blazor WebAssembly for its client-side execution and offline capabilities, Blazor offers a compelling option for building modern web applications.
As we've explored in this article, Blazor provides a rich set of features for building interactive UIs, from basic components and data binding to advanced patterns like state management and JavaScript interoperability. With proper attention to performance, error handling, testing, and security, you can build robust and maintainable applications that provide excellent user experiences.
The future of Blazor looks bright, with Microsoft continuing to invest in improvements like ahead-of-time (AOT) compilation for WebAssembly, better debugging experiences, and tighter integration with the .NET ecosystem. As WebAssembly itself continues to evolve with features like garbage collection and direct DOM access, Blazor will become an even more powerful tool for web development.
Whether you're building a simple internal tool or a complex customer-facing application, Blazor provides the tools you need to create rich, interactive web UIs with the stability and productivity of the .NET platform.
Join the ASP Today Community
Enjoyed this deep dive into Blazor? There's much more to learn and discover! Subscribe to ASP Today on Substack to receive regular articles, tutorials, and updates about ASP.NET Core, Blazor, and related technologies. Don't miss out on future content that will help you become a better .NET developer.
Join our Substack Chat community to connect with other developers, ask questions, share your experiences, and learn from peers. Together, we can build the next generation of web applications with ASP.NET Core and Blazor!