Published:

Blazor WebAssembly - User Registration and Login Example & Tutorial

Tutorial built with ASP.NET Core Blazor WebAssembly 3.2.1

Other versions available:

In this tutorial we'll go through an example of how to build a simple user registration, login and user management (CRUD) application with Blazor WebAssembly.

The blazor app runs with a fake backend by default to enable it to run completely in the browser without a real backend api (backend-less), to switch to a real api you just have to change the "fakeBackend" setting to "false" in the app settings file (/wwwroot/appsettings.json). You can build your own api or hook it up with the ASP.NET Core api or Node.js api available (instructions below).

The project is available on GitHub at https://github.com/cornflourblue/blazor-webassembly-registration-login-example.

Styling of the example app is all done with Bootstrap 4.5 CSS, for more info about Bootstrap see https://getbootstrap.com/docs/4.5/getting-started/introduction/.

Here it is in action:(Hosted on GitHub Pages at https://cornflourblue.github.io/blazor-webassembly-registration-login-example/)


Tools required to run the Blazor Login Example Locally

To develop and run ASP.NET Core Blazor applications locally, download and install the following:

  • .NET Core SDK - includes the .NET Core runtime and command line tools
  • Visual Studio Code - code editor that runs on Windows, Mac and Linux
  • C# extension for Visual Studio Code - adds support to VS Code for developing .NET Core applications

For more detailed instructions on setting up your local dev environment see ASP.NET Core - Setup Development Environment.


Running the Blazor Login Tutorial Example Locally

  1. Download or clone the tutorial project code from https://github.com/cornflourblue/blazor-webassembly-registration-login-example
  2. Start the app by running dotnet run from the command line in the project root folder (where the BlazorApp.csproj file is located)
  3. Open a new browser tab and navigate to the URL http://localhost:5000

NOTE: To enable hot reloading during development so the app automatically restarts when a file is changed, start the app with the command dotnet watch run.


Running the Blazor WebAssembly App with an ASP.NET Core 3.1 API

For full details about the example ASP.NET Core API see the post ASP.NET Core 3.1 - Simple API for Authentication, Registration and User Management. But to get up and running quickly just follow the below steps.

  1. Download or clone the project source code from https://github.com/cornflourblue/aspnet-core-3-registration-login-api
  2. Start the api by running dotnet run from the command line in the project root folder (where the WebApi.csproj file is located), you should see the message Now listening on: http://localhost:4000.
  3. Back in the Blazor WebAssembly app, change the "fakeBackend" setting to "false" in the app settings file (/wwwroot/appsettings.json), then start the Blazor app and it should now be hooked up with the ASP.NET Core API.


Running the Blazor App with a Node.js + MySQL API

For full details about the example Node.js + MySQL API see the post NodeJS + MySQL - Simple API for Authentication, Registration and User Management. But to get up and running quickly just follow the below steps.

  1. Install MySQL Community Server from https://dev.mysql.com/downloads/mysql/ and ensure it is started. Installation instructions are available at https://dev.mysql.com/doc/refman/8.0/en/installing.html.
  2. Install NodeJS and NPM from https://nodejs.org.
  3. Download or clone the project source code from https://github.com/cornflourblue/node-mysql-registration-login-api
  4. Install all required npm packages by running npm install or npm i from the command line in the project root folder (where the package.json is located).
  5. Start the api by running npm start from the command line in the project root folder, you should see the message Server listening on port 4000.
  6. Back in the Blazor WebAssembly app, change the "fakeBackend" setting to "false" in the app settings file (/wwwroot/appsettings.json), then start the Blazor app and it should now be hooked up with the Node + MySQL API.


Running the Blazor App with a Node.js + MongoDB API

For full details about the example Node.js + MongoDB API see the post NodeJS + MongoDB - Simple API for Authentication, Registration and User Management. But to get up and running quickly just follow the below steps.

  1. Install MongoDB Community Server from  https://www.mongodb.com/download-center.
  2. Run MongoDB, instructions are available on the install page for each OS at https://docs.mongodb.com/manual/administration/install-community/
  3. Install NodeJS and NPM from https://nodejs.org.
  4. Download or clone the project source code from https://github.com/cornflourblue/node-mongo-registration-login-api
  5. Install all required npm packages by running npm install or npm i from the command line in the project root folder (where the package.json is located).
  6. Start the api by running npm start from the command line in the project root folder, you should see the message Server listening on port 4000.
  7. Back in the Blazor WebAssembly app, change the "fakeBackend" setting to "false" in the app settings file (/wwwroot/appsettings.json), then start the Blazor app and it should now be hooked up with the Node + Mongo API.


Blazor WebAssembly Project Structure

The .NET Core CLI (dotnet) was used to generate the base project structure with the command dotnet new blazorwasm -o BlazorApp, the CLI is also used to build and serve the application. For more info about the .NET Core CLI see https://docs.microsoft.com/en-us/dotnet/core/tools/.

The tutorial project is organised into the following folders:

Pages

ASP.NET Core Razor components that contain the pages for the Blazor application. Each component specifies which route it is bound to with a @page directive at the top of the file (e.g. @page "/account/login" in the login component).

Shared

ASP.NET Core Razor components that can be used in multiple areas of the application and are not bound to a specific route.

Services

Contain the core logic for the application and handles most of the heavy lifting so page components can be kept as lean and simple as possible. The services layer encapsulates all http communication with backend apis and interaction with local storage, and exposes a simple set of interfaces for the rest of the app to use.

Models

Represent the model data handled by the Blazor application and transferred between components and services, including data received in api responses and sent in requests. Form validation is implemented in models with data annotation attributes (e.g. [Required]).

Helpers

Anything that doesn't fit into the above folders.

wwwroot

The Blazor project "web root" folder that contains static files including the root index.html file or host page (/wwwroot/index.html), css stylesheets, images and app settings (/wwwroot/appsettings.json). Everything in the wwwroot folder is publicly accessible via a web request so make sure you only include static files that should be public.

docs

You can ignore this folder, it just contains a compiled demo of the code hosted on GitHub Pages at https://cornflourblue.github.io/blazor-webassembly-registration-login-example/.

Click the below links to jump to a description of each file along with its code:

 

App Route View Component

Path: /Helpers/AppRouteView.cs

The app route view component is used inside the app component and renders the page component for the current route along with its layout.

If the page component for the route contains an authorize attribute (@attribute [Authorize]) then the user must be logged in, otherwise they will be redirected to the login page.

The app route view extends the built in ASP.NET Core RouteView component and uses the base class to render the page by calling base.Render(builder).

using BlazorApp.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using System;
using System.Net;

namespace BlazorApp.Helpers
{
    public class AppRouteView : RouteView
    {
        [Inject]
        public NavigationManager NavigationManager { get; set; }

        [Inject]
        public IAccountService AccountService { get; set; }

        protected override void Render(RenderTreeBuilder builder)
        {
            var authorize = Attribute.GetCustomAttribute(RouteData.PageType, typeof(AuthorizeAttribute)) != null;
            if (authorize && AccountService.User == null)
            {
                var returnUrl = WebUtility.UrlEncode(new Uri(NavigationManager.Uri).PathAndQuery);
                NavigationManager.NavigateTo($"account/login?returnUrl={returnUrl}");
            }
            else
            {
                base.Render(builder);
            }
        }
    }
}
 

Extension Methods

Path: /Helpers/ExtensionMethods.cs

The extension methods class adds a couple of simple extension methods to the NavigationManager for accessing query string parameters in the URL.

For more info see Blazor WebAssembly - Get Query String Parameters with Navigation Manager.

using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Specialized;
using System.Web;

namespace BlazorApp.Helpers
{
    public static class ExtensionMethods
    {
        public static NameValueCollection QueryString(this NavigationManager navigationManager)
        {
            return HttpUtility.ParseQueryString(new Uri(navigationManager.Uri).Query);
        }

        public static string QueryString(this NavigationManager navigationManager, string key)
        {
            return navigationManager.QueryString()[key];
        }
    }
}
 

Fake Backend Handler

Path: /Helpers/FakeBackendHandler.cs

In order to run and test the Blazor application without a real backend API, the example uses a fake backend handler that intercepts the HTTP requests from the Blazor app and sends back "fake" responses. The fake backend handler inherits from the ASP.NET Core HttpClientHandler class and is configured with the http client in Program.cs.

The fake backend contains a handleRoute() local function that checks if the request matches one of the faked routes, at the moment these include requests for handling registration, authentication and user CRUD operations. Matching requests are intercepted and handled by one of the below // route functions, non-matching requests are sent through to the real backend by calling base.SendAsync(request, cancellationToken);. Below the route functions there are // helper functions for returning different response types and performing small tasks.

For more info see the tutorial Blazor WebAssembly - Fake Backend Example for Backendless Development.

using BlazorApp.Models.Account;
using BlazorApp.Services;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

namespace BlazorApp.Helpers
{
    public class FakeBackendHandler : HttpClientHandler
    {
        private ILocalStorageService _localStorageService;

        public FakeBackendHandler(ILocalStorageService localStorageService)
        {
            _localStorageService = localStorageService;
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            // array in local storage for registered users
            var usersKey = "blazor-registration-login-example-users";
            var users = await _localStorageService.GetItem<List<UserRecord>>(usersKey) ?? new List<UserRecord>();
            var method = request.Method;
            var path = request.RequestUri.AbsolutePath;            

            return await handleRoute();

            async Task<HttpResponseMessage> handleRoute()
            {
                if (path == "/users/authenticate" && method == HttpMethod.Post)
                    return await authenticate();
                if (path == "/users/register" && method == HttpMethod.Post)
                    return await register();
                if (path == "/users" && method == HttpMethod.Get)
                    return await getUsers();
                if (Regex.Match(path, @"\/users\/\d+$").Success && method == HttpMethod.Get)
                    return await getUserById();
                if (Regex.Match(path, @"\/users\/\d+$").Success && method == HttpMethod.Put)
                    return await updateUser();
                if (Regex.Match(path, @"\/users\/\d+$").Success && method == HttpMethod.Delete)
                    return await deleteUser();
                
                // pass through any requests not handled above
                return await base.SendAsync(request, cancellationToken);
            }

            // route functions
            
            async Task<HttpResponseMessage> authenticate()
            {
                var bodyJson = await request.Content.ReadAsStringAsync();
                var body = JsonSerializer.Deserialize<Login>(bodyJson);
                var user = users.FirstOrDefault(x => x.Username == body.Username && x.Password == body.Password);

                if (user == null)
                    return await error("Username or password is incorrect");

                return await ok(new {
                    Id = user.Id.ToString(),
                    Username = user.Username,
                    FirstName = user.FirstName,
                    LastName = user.LastName,
                    Token = "fake-jwt-token"
                });
            }

            async Task<HttpResponseMessage> register()
            {
                var bodyJson = await request.Content.ReadAsStringAsync();
                var body = JsonSerializer.Deserialize<AddUser>(bodyJson);

                if (users.Any(x => x.Username == body.Username))
                    return await error($"Username '{body.Username}' is already taken");

                var user = new UserRecord {
                    Id = users.Count > 0 ? users.Max(x => x.Id) + 1 : 1,
                    Username = body.Username,
                    Password = body.Password,
                    FirstName = body.FirstName,
                    LastName = body.LastName
                };

                users.Add(user);

                await _localStorageService.SetItem(usersKey, users);
                
                return await ok();
            }

            async Task<HttpResponseMessage> getUsers()
            {
                if (!isLoggedIn()) return await unauthorized();
                return await ok(users.Select(x => basicDetails(x)));
            }

            async Task<HttpResponseMessage> getUserById()
            {
                if (!isLoggedIn()) return await unauthorized();

                var user = users.FirstOrDefault(x => x.Id == idFromPath());
                return await ok(basicDetails(user));
            }

            async Task<HttpResponseMessage> updateUser() 
            {
                if (!isLoggedIn()) return await unauthorized();

                var bodyJson = await request.Content.ReadAsStringAsync();
                var body = JsonSerializer.Deserialize<EditUser>(bodyJson);
                var user = users.FirstOrDefault(x => x.Id == idFromPath());

                // if username changed check it isn't already taken
                if (user.Username != body.Username && users.Any(x => x.Username == body.Username))
                    return await error($"Username '{body.Username}' is already taken");

                // only update password if entered
                if (!string.IsNullOrWhiteSpace(body.Password))
                    user.Password = body.Password;

                // update and save user
                user.Username = body.Username;
                user.FirstName = body.FirstName;
                user.LastName = body.LastName;
                await _localStorageService.SetItem(usersKey, users);

                return await ok();
            }

            async Task<HttpResponseMessage> deleteUser()
            {
                if (!isLoggedIn()) return await unauthorized();

                users.RemoveAll(x => x.Id == idFromPath());
                await _localStorageService.SetItem(usersKey, users);

                return await ok();
            }

            // helper functions

            async Task<HttpResponseMessage> ok(object body = null)
            {
                return await jsonResponse(HttpStatusCode.OK, body ?? new {});
            }

            async Task<HttpResponseMessage> error(string message)
            {
                return await jsonResponse(HttpStatusCode.BadRequest, new { message });
            }

            async Task<HttpResponseMessage> unauthorized()
            {
                return await jsonResponse(HttpStatusCode.Unauthorized, new { message = "Unauthorized" });
            }

            async Task<HttpResponseMessage> jsonResponse(HttpStatusCode statusCode, object content)
            {
                var response = new HttpResponseMessage
                {
                    StatusCode = statusCode,
                    Content = new StringContent(JsonSerializer.Serialize(content), Encoding.UTF8, "application/json")
                };
                
                // delay to simulate real api call
                await Task.Delay(500);

                return response;
            }

            bool isLoggedIn()
            {
                return request.Headers.Authorization?.Parameter == "fake-jwt-token";
            } 

            int idFromPath()
            {
                return int.Parse(path.Split('/').Last());
            }

            dynamic basicDetails(UserRecord user)
            {
                return new {
                    Id = user.Id.ToString(),
                    Username = user.Username,
                    FirstName = user.FirstName,
                    LastName = user.LastName
                };
            }
        }
    }

    // class for user records stored by fake backend

    public class UserRecord {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Username { get; set; }
        public string Password { get; set; }
    }
}
 

String Converter

Path: /Helpers/StringConverter.cs

The string converter is a custom JSON converter used in the sendRequest<T>() method of the http service to deserialize numbers as strings from API responses.

This makes the Blazor app compatible with APIs that return either integer id or string id properties in JSON responses. Without the string converter, an API that returns an int id results in the error: The JSON value could not be converted to System.String. Path: $.id.

using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace BlazorApp.Helpers
{
    public class StringConverter : JsonConverter<string>
    {
        public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            // deserialize numbers as strings.
            if (reader.TokenType == JsonTokenType.Number)
            {
                return reader.GetInt32().ToString();
            }
            else if (reader.TokenType == JsonTokenType.String)
            {
                return reader.GetString();
            }
 
            throw new System.Text.Json.JsonException();
        }
 
        public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value);
        }
    }
}
 

Add User Model

Path: /Models/Account/AddUser.cs

The add user model represents the data and validation rules for registering or adding a new user. The model is bound to the register form and add user form, which use it to pass form data to the AccountService.Register() method to create new user accounts.

using System.ComponentModel.DataAnnotations;

namespace BlazorApp.Models.Account
{
    public class AddUser
    {
        [Required]
        public string FirstName { get; set; }

        [Required]
        public string LastName { get; set; }

        [Required]
        public string Username { get; set; }

        [Required]
        [MinLength(6, ErrorMessage = "The Password field must be a minimum of 6 characters")]
        public string Password { get; set; }
    }
}
 

Edit User Model

Path: /Models/Account/EditUser.cs

The edit user model represents the data and validation rules for updating an existing user account. The model is bound to the edit user form which uses it to pass form data to the AccountService.Update() method.

using System.ComponentModel.DataAnnotations;

namespace BlazorApp.Models.Account
{
    public class EditUser
    {
        [Required]
        public string FirstName { get; set; }

        [Required]
        public string LastName { get; set; }

        [Required]
        public string Username { get; set; }

        [MinLength(6, ErrorMessage = "The Password field must be a minimum of 6 characters")]
        public string Password { get; set; }

        public EditUser() { }

        public EditUser(User user)
        {
            FirstName = user.FirstName;
            LastName = user.LastName;
            Username = user.Username;
        }
    }
}
 

Login Model

Path: /Models/Account/Login.cs

The login model represents the data and validation rules for logging into the Blazor app. The model is bound to the login form which uses it to pass form data to the AccountService.Login() method.

using System.ComponentModel.DataAnnotations;

namespace BlazorApp.Models.Account
{
    public class Login
    {
        [Required]
        public string Username { get; set; }

        [Required]
        public string Password { get; set; }
    }
}
 

Alert Model

Path: /Models/Alert.cs

The alert model defines the properties of an alert, and the AlertType enum defines the different types of alerts supported by the application.

The model and enum are used by the alert service and alert component for sending, receiving and displaying alerts in the blazor app.

namespace BlazorApp.Models
{
    public class Alert
    {
        public string Id { get; set; }
        public AlertType Type { get; set; }
        public string Message { get; set; }
        public bool AutoClose { get; set; }
        public bool KeepAfterRouteChange { get; set; }
        public bool Fade { get; set; }
    }

    public enum AlertType
    {
        Success,
        Error,
        Info,
        Warning
    }
}
 

User Model

Path: /Models/User.cs

The user model defines the properties of a user account, it's used by the account service for handling user data returned from the api, and other parts of the application for passing user data around.

The IsDeleting property is used by the users home page to show a spinner while a user account is deleting.

namespace BlazorApp.Models
{
    public class User
    {
        public string Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Username { get; set; }
        public string Token { get; set; }
        public bool IsDeleting { get; set; }
    }
}
 

Account Imports File

Path: /Pages/Account/_Imports.razor

Razor imports files include directives that are automatically applied to all other razor components in the same folder and subfolders. An _Imports.razor file can be added to any folder and can include any razor directive.

By adding the below @layout directive to this imports file, all blazor components in the /Pages/Account folder will use the account layout.

@layout Layout
 

Account Layout Component

Path: /Pages/Account/Layout.razor

The account layout component is a nested layout for all pages in the /Pages/Account folder, it simply wraps the @Body in a div with some bootstrap classes to set the width and alignment of all of the account pages. It is set as the layout for all account pages in the account imports file.

The OnInitialized() Blazor lifecycle method is used to automatically redirect the user to the home page if they are already logged in.

@inherits LayoutComponentBase
@layout MainLayout
@inject IAccountService AccountService
@inject NavigationManager NavigationManager

<div class="col-md-6 offset-md-3 mt-5">
    @Body
</div>

@code {
    protected override void OnInitialized()
    {
        // redirect to home if already logged in
        if (AccountService.User != null)
        {
            NavigationManager.NavigateTo("");
        }
    }
}
 

Login Page Component

Path: /Pages/Account/Login.razor

The login page component contains a login form with username and password fields. It is built with the ASP.NET Core <EditForm> and <InputText> components, and displays validation messages for invalid fields when the user attempts to submit the form or when a field is changed.

On valid submit the AccountService.Login(model) method is called from the OnValidSubmit() method, if login is successful the user is redirected back to the original page they were trying to access (or the home page by default), if there is an error the error message is displayed with the alert service.

The <DataAnnotationsValidator /> component enables support for validating the form using the data annotations attributes on the Model class that is bound to the form (e.g. [Required]), and the <ValidationMessage For="..." /> components display the validation message below each field. The login model class contains properties for each of the fields in the form along with validation rules defined using data annotations attributes.

For more info on ASP.NET Core Blazor forms and validation Blazor WebAssembly - Form Validation Example.

@page "/account/login"
@inject IAccountService AccountService
@inject IAlertService AlertService
@inject NavigationManager NavigationManager

<div class="card">
    <h4 class="card-header">Login</h4>
    <div class="card-body">
        <EditForm Model="@model" OnValidSubmit="OnValidSubmit">
            <DataAnnotationsValidator />
            <div class="form-group">
                <label>Username</label>
                <InputText @bind-Value="model.Username" class="form-control" />
                <ValidationMessage For="@(() => model.Username)" />
            </div>
            <div class="form-group">
                <label>Password</label>
                <InputText @bind-Value="model.Password" type="password" class="form-control" />
                <ValidationMessage For="@(() => model.Password)" />
            </div>
            <button disabled="@loading" class="btn btn-primary">
                @if (loading) 
                {
                    <span class="spinner-border spinner-border-sm mr-1"></span>
                }
                Login
            </button>
            <NavLink href="account/register" class="btn btn-link">Register</NavLink>
        </EditForm>
    </div>
</div>

@code {
    private Models.Account.Login model = new Models.Account.Login();
    private bool loading;

    private async void OnValidSubmit()
    {
        // reset alerts on submit
        AlertService.Clear();

        loading = true;
        try
        {
            await AccountService.Login(model);
            var returnUrl = NavigationManager.QueryString("returnUrl") ?? "";
            NavigationManager.NavigateTo(returnUrl);
        }
        catch (Exception ex)
        {
            AlertService.Error(ex.Message);
            loading = false;
            StateHasChanged();
        }
    }
}
 

Logout Page Component

Path: /Pages/Account/Logout.razor

The logout page component logs out of the example app by calling AccountService.Logout() from the Blazor OnInitialized() lifecycle method. The logout method of the account service redirects to the user to the login page after logout.

The @layout MainLayout directive is used to override the layout in the account imports file to disable the automatic redirect to the home page for logged in users.

@page "/account/logout"
@layout MainLayout
@inject IAccountService AccountService

@code {
    protected override async void OnInitialized()
    {
        await AccountService.Logout();
    }
}
 

Register Page Component

Path: /Pages/Account/Register.razor

The register page component contains a simple registration form with fields for first name, last name, username and password. It is built with the .NET Core <EditForm> and <InputText> components, and displays validation messages for invalid fields when the user attempts to submit the form or when a field is changed.

On valid submit the AccountService.Register(model) method is called from the OnValidSubmit() method, if registration is successful the user is redirected to the login page with a success alert message, if there is an error the error message is displayed with the alert service.

The <DataAnnotationsValidator /> component enables support for validating the form using the data annotations attributes on the Model class that is bound to the form (e.g. [Required]), and the <ValidationMessage For="..." /> components display the validation message below each field. The add user model class contains properties for each of the fields in the form along with validation rules defined using data annotations attributes.

For more info on ASP.NET Core Blazor forms and validation Blazor WebAssembly - Form Validation Example.

@page "/account/register"
@inject IAccountService AccountService
@inject IAlertService AlertService
@inject NavigationManager NavigationManager

<div class="card">
    <h4 class="card-header">Register</h4>
    <div class="card-body">
        <EditForm Model="@model" OnValidSubmit="OnValidSubmit">
            <DataAnnotationsValidator />
            <div class="form-group">
                <label>First Name</label>
                <InputText @bind-Value="model.FirstName" class="form-control" />
                <ValidationMessage For="@(() => model.FirstName)" />
            </div>
            <div class="form-group">
                <label>Last Name</label>
                <InputText @bind-Value="model.LastName" class="form-control" />
                <ValidationMessage For="@(() => model.LastName)" />
            </div>
            <div class="form-group">
                <label>Username</label>
                <InputText @bind-Value="model.Username" class="form-control" />
                <ValidationMessage For="@(() => model.Username)" />
            </div>
            <div class="form-group">
                <label>Password</label>
                <InputText @bind-Value="model.Password" type="password" class="form-control" />
                <ValidationMessage For="@(() => model.Password)" />
            </div>
            <button disabled="@loading" class="btn btn-primary">
                @if (loading) 
                {
                    <span class="spinner-border spinner-border-sm mr-1"></span>
                }
                Register
            </button>
            <NavLink href="account/login" class="btn btn-link">Cancel</NavLink>
        </EditForm>
    </div>
</div>

@code {
    private AddUser model = new AddUser();
    private bool loading;

    private async void OnValidSubmit()
    {
        // reset alerts on submit
        AlertService.Clear();
        
        loading = true;
        try
        {
            await AccountService.Register(model);
            AlertService.Success("Registration successful", keepAfterRouteChange: true);
            NavigationManager.NavigateTo("account/login");
        }
        catch (Exception ex)
        {
            AlertService.Error(ex.Message);
            loading = false;
            StateHasChanged();
        }
    }
}
 

Users Imports File

Path: /Pages/Users/_Imports.razor

Razor imports files include directives that are automatically applied to all other razor components in the same folder and subfolders. An _Imports.razor file can be added to any folder and can include any razor directive.

By adding the below @layout directive to this imports file, all blazor components in the /Pages/Users folder will use the users layout.

@layout Layout
 

Add User Page Component

Path: /Pages/Users/Add.razor

The add user page component contains a form for creating a new user with fields for first name, last name, username and password. It is built with the .NET Core <EditForm> and <InputText> components, and displays validation messages for invalid fields when the user attempts to submit the form or when a field is changed.

On valid submit the AccountService.Register(model) method is called from the OnValidSubmit() method, if registration is successful the user is redirected to the users list page with a success alert message, if there is an error the error message is displayed with the alert service.

The <DataAnnotationsValidator /> component enables support for validating the form using the data annotations attributes on the Model class that is bound to the form (e.g. [Required]), and the <ValidationMessage For="..." /> components display the validation message below each field. The add user model class contains properties for each of the fields in the form along with validation rules defined using data annotations attributes.

For more info on ASP.NET Core Blazor forms and validation Blazor WebAssembly - Form Validation Example.

@page "/users/add"
@attribute [Authorize]
@inject IAlertService AlertService
@inject IAccountService AccountService
@inject NavigationManager NavigationManager

<h1>Add User</h1>
<EditForm Model="@model" OnValidSubmit="OnValidSubmit">
    <DataAnnotationsValidator />
    <div class="form-row">
        <div class="form-group col">
            <label>First Name</label>
            <InputText @bind-Value="model.FirstName" class="form-control" />
            <ValidationMessage For="@(() => model.FirstName)" />
        </div>
        <div class="form-group col">
            <label>Last Name</label>
            <InputText @bind-Value="model.LastName" class="form-control" />
            <ValidationMessage For="@(() => model.LastName)" />
        </div>
    </div>
    <div class="form-row">
        <div class="form-group col">
            <label>Username</label>
            <InputText @bind-Value="model.Username" class="form-control" />
            <ValidationMessage For="@(() => model.Username)" />
        </div>
        <div class="form-group col">
            <label>Password</label>
            <InputText @bind-Value="model.Password" type="password" class="form-control" />
            <ValidationMessage For="@(() => model.Password)" />
        </div>
    </div>
    <div class="form-group">
        <button disabled="@loading" class="btn btn-primary">
            @if (loading) 
            {
                <span class="spinner-border spinner-border-sm mr-1"></span>
            }
            Save
        </button>
        <NavLink href="users" class="btn btn-link">Cancel</NavLink>
    </div>
</EditForm>

@code {
    private AddUser model = new AddUser();
    private bool loading;

    private async void OnValidSubmit()
    {
        loading = true;
        try
        {
            await AccountService.Register(model);
            AlertService.Success("User added successfully", keepAfterRouteChange: true);
            NavigationManager.NavigateTo("users");
        }
        catch (Exception ex)
        {
            AlertService.Error(ex.Message);
            loading = false;
            StateHasChanged();
        }
    }
}
 

Edit User Page Component

Path: /Pages/Users/Edit.razor

The edit user page component contains a form for updating an existing user with fields for first name, last name, username and password. It is built with the .NET Core <EditForm> and <InputText> components, and displays validation messages for invalid fields when the user attempts to submit the form or when a field is changed.

The OnInitializedAsync() Blazor lifecycle method is used to fetch the user account details from the account service and populate the model bound to the form. A loading spinner is displayed while the account details are loading.

On valid submit the AccountService.Update(model) method is called from the OnValidSubmit() method, if the update is successful the user is redirected to the users list page with a success alert message, if there is an error the error message is displayed with the alert service.

The <DataAnnotationsValidator /> component enables support for validating the form using the data annotations attributes on the Model class that is bound to the form (e.g. [Required]), and the <ValidationMessage For="..." /> components display the validation message below each field. The edit user model class contains properties for each of the fields in the form along with validation rules defined using data annotations attributes.

For more info on ASP.NET Core Blazor forms and validation Blazor WebAssembly - Form Validation Example.

@page "/users/edit/{Id}"
@attribute [Authorize]
@inject IAlertService AlertService
@inject IAccountService AccountService
@inject NavigationManager NavigationManager

<h1>Edit User</h1>
@if (model != null)
{
    <EditForm Model="@model" OnValidSubmit="OnValidSubmit">
        <DataAnnotationsValidator />
        <div class="form-row">
            <div class="form-group col">
                <label>First Name</label>
                <InputText @bind-Value="model.FirstName" class="form-control" />
                <ValidationMessage For="@(() => model.FirstName)" />
            </div>
            <div class="form-group col">
                <label>Last Name</label>
                <InputText @bind-Value="model.LastName" class="form-control" />
                <ValidationMessage For="@(() => model.LastName)" />
            </div>
        </div>
        <div class="form-row">
            <div class="form-group col">
                <label>Username</label>
                <InputText @bind-Value="model.Username" class="form-control" />
                <ValidationMessage For="@(() => model.Username)" />
            </div>
            <div class="form-group col">
                <label>
                    Password
                    <em>(Leave blank to keep the same password)</em>
                </label>
                <InputText @bind-Value="model.Password" type="password" class="form-control" />
                <ValidationMessage For="@(() => model.Password)" />
            </div>
        </div>
        <div class="form-group">
            <button disabled="@loading" class="btn btn-primary">
                @if (loading) 
                {
                    <span class="spinner-border spinner-border-sm mr-1"></span>
                }
                Save
            </button>
            <NavLink href="users" class="btn btn-link">Cancel</NavLink>
        </div>
    </EditForm>
}
else
{
    <div class="text-center p-3">
        <span class="spinner-border spinner-border-lg align-center"></span>
    </div>
}

@code {
    private EditUser model;
    private bool loading;

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

    protected override async Task OnInitializedAsync()
    {
        var user = await AccountService.GetById(Id);
        model = new EditUser(user);
    }

    private async void OnValidSubmit()
    {
        loading = true;
        try
        {
            await AccountService.Update(Id, model);
            AlertService.Success("Update successful", keepAfterRouteChange: true);
            NavigationManager.NavigateTo("users");
        }
        catch (Exception ex)
        {
            AlertService.Error(ex.Message);
            loading = false;
            StateHasChanged();
        }
    }
}
 

Users Home Page Component

Path: /Pages/Users/Index.razor

The users home page component displays a list of all users and contains buttons for adding, editing and deleting users.

The OnInitializedAsync() Blazor lifecycle method is used to fetch all user accounts from the account service and make them available to the template via the users property.

The DeleteUser() method first sets the property user.IsDeleting = true so the template displays a spinner on the delete button, it then calls AccountService.Delete() to delete the user and removes the deleted user from the users list so it is removed from the UI.

@page "/users"
@attribute [Authorize]
@inject IAccountService AccountService

<h1>Users</h1>
<NavLink href="users/add" class="btn btn-sm btn-success mb-2">Add User</NavLink>
<table class="table table-striped">
    <thead>
        <tr>
            <th style="width: 30%">First Name</th>
            <th style="width: 30%">Last Name</th>
            <th style="width: 30%">Username</th>
            <th style="width: 10%"></th>
        </tr>
    </thead>
    <tbody>
        @if (users != null)
        {
            foreach (var user in users)
            {
                <tr>
                    <td>@user.FirstName</td>
                    <td>@user.LastName</td>
                    <td>@user.Username</td>
                    <td style="white-space: nowrap">
                        <NavLink href="@($"users/edit/{user.Id}")" class="btn btn-sm btn-primary mr-1">Edit</NavLink>
                        <button @onclick="@(() => DeleteUser(user.Id))" disabled="@user.IsDeleting" class="btn btn-sm btn-danger btn-delete-user">
                            @if (user.IsDeleting) 
                            {
                                <span class="spinner-border spinner-border-sm"></span>
                            }
                            else
                            {
                                <span>Delete</span>
                            }
                        </button>
                    </td>
                </tr>
            }
        }
        @if (loading)
        {
            <tr>
                <td colspan="4" class="text-center">
                    <span class="spinner-border spinner-border-lg align-center"></span>
                </td>
            </tr>
        }
    </tbody>
</table>

@code {
    private bool loading;
    private IList<User> users;

    protected override async Task OnInitializedAsync()
    {
        loading = true;
        users = await AccountService.GetAll();
        loading = false;
    }

    private async void DeleteUser(string id) 
    {
        var user = users.First(x => x.Id == id);
        user.IsDeleting = true;
        await AccountService.Delete(id);
        users.Remove(user);
        StateHasChanged();
    }
}
 

Users Layout Component

Path: /Pages/Users/Layout.razor

The users layout component is a nested layout for all pages in the /Pages/Users folder, it simply wraps the @Body in a couple of div tags with some bootstrap classes to set the width, padding and alignment of all of the users pages. It is set as the layout for all users pages in the users imports file.

@inherits LayoutComponentBase
@layout MainLayout

<div class="p-4">
    <div class="container">
        @Body
    </div>
</div>
 

Home Page Component

Path: /Pages/Index.razor

The home page component displays a simple welcome message with the current user's first name and a link to the users section.

The [Authorize] attribute restricts this page to authenticated users.

@page "/"
@attribute [Authorize]
@inject IAccountService AccountService

<div class="p-4">
    <div class="container">
        <h1>Hi @AccountService.User.FirstName!</h1>
        <p>You're logged in with Blazor WebAssembly!!</p>
        <p><NavLink href="users">Manage Users</NavLink></p>
    </div>
</div>
 

Launch Settings

Path: /Properties/launchSettings.json

The launch settings file contains settings that are used when you run the example Blazor application on your local development machine.

The "BlazorApp" profile is used when you run the Blazor app using the .NET Core CLI (dotnet run), and the "IIS Express" profile is used when you run the Blazor app from Visual Studio.

{
    "iisSettings": {
        "windowsAuthentication": false,
        "anonymousAuthentication": true,
        "iisExpress": {
            "applicationUrl": "http://localhost:25181",
            "sslPort": 44330
        }
    },
    "profiles": {
        "IIS Express": {
            "commandName": "IISExpress",
            "launchBrowser": true,
            "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
            "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            }
        },
        "BlazorApp": {
            "commandName": "Project",
            "launchBrowser": true,
            "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
            "applicationUrl": "https://localhost:5001;http://localhost:5000",
            "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            }
        }
    }
}
 

Account Service

Path: /Services/AccountService.cs

The account service handles communication between the Blazor app and the backend api for everything related to user accounts. It contains methods for the login, logout and registration, as well as and standard CRUD methods for retrieving and modifying user data.

On successful login the User property is set to the value returned from the api, and the user object is stored in browser local storage to keep the user logged in between page refreshes and browser sessions. If you prefer not to use local storage you can simply remove references to the local storage service from the account service and the application will continue to work correctly, except for the "stay logged in" feature.

The User property provides access to the currently logged in user to any other component, for example the main layout component uses it to show/hide the main navigation bar and set a css class on the app container div.

The Initialize() method is called from Program.cs on startup to assign the "user" object from local storage to the User property, which enables the user to stay logged in between page refreshes and browser sessions. This couldn't be put into the constructor because getting data from local storage is an async action.

using BlazorApp.Models;
using BlazorApp.Models.Account;
using Microsoft.AspNetCore.Components;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace BlazorApp.Services
{
    public interface IAccountService
    {
        User User { get; }
        Task Initialize();
        Task Login(Login model);
        Task Logout();
        Task Register(AddUser model);
        Task<IList<User>> GetAll();
        Task<User> GetById(string id);
        Task Update(string id, EditUser model);
        Task Delete(string id);
    }

    public class AccountService : IAccountService
    {
        private IHttpService _httpService;
        private NavigationManager _navigationManager;
        private ILocalStorageService _localStorageService;
        private string _userKey = "user";

        public User User { get; private set; }

        public AccountService(
            IHttpService httpService,
            NavigationManager navigationManager,
            ILocalStorageService localStorageService
        ) {
            _httpService = httpService;
            _navigationManager = navigationManager;
            _localStorageService = localStorageService;
        }

        public async Task Initialize()
        {
            User = await _localStorageService.GetItem<User>(_userKey);
        }

        public async Task Login(Login model)
        {
            User = await _httpService.Post<User>("/users/authenticate", model);
            await _localStorageService.SetItem(_userKey, User);
        }

        public async Task Logout()
        {
            User = null;
            await _localStorageService.RemoveItem(_userKey);
            _navigationManager.NavigateTo("account/login");
        }

        public async Task Register(AddUser model)
        {
            await _httpService.Post("/users/register", model);
        }

        public async Task<IList<User>> GetAll()
        {
            return await _httpService.Get<IList<User>>("/users");
        }

        public async Task<User> GetById(string id)
        {
            return await _httpService.Get<User>($"/users/{id}");
        }

        public async Task Update(string id, EditUser model)
        {
            await _httpService.Put($"/users/{id}", model);

            // update stored user if the logged in user updated their own record
            if (id == User.Id) 
            {
                // update local storage
                User.FirstName = model.FirstName;
                User.LastName = model.LastName;
                User.Username = model.Username;
                await _localStorageService.SetItem(_userKey, User);
            }
        }

        public async Task Delete(string id)
        {
            await _httpService.Delete($"/users/{id}");

            // auto logout if the logged in user deleted their own record
            if (id == User.Id)
                await Logout();
        }
    }
}
 

Alert Service

Path: /Services/AlertService.cs

The alert service acts as the bridge between any component or class in the Blazor application and the alert component that actually displays the alert messages. It contains methods for sending, clearing and subscribing to alert messages.

The service uses C# events and delegates to enable communication with other components, for more information on how this works see the tutorial Blazor WebAssembly - Communication Between Components.

using BlazorApp.Models;
using System;

namespace BlazorApp.Services
{
    public interface IAlertService
    {
        event Action<Alert> OnAlert;
        void Success(string message, bool keepAfterRouteChange = false, bool autoClose = true);
        void Error(string message, bool keepAfterRouteChange = false, bool autoClose = true);
        void Info(string message, bool keepAfterRouteChange = false, bool autoClose = true);
        void Warn(string message, bool keepAfterRouteChange = false, bool autoClose = true);
        void Alert(Alert alert);
        void Clear(string id = null);
    }

    public class AlertService : IAlertService
    {
        private const string _defaultId = "default-alert";
        public event Action<Alert> OnAlert;

        public void Success(string message, bool keepAfterRouteChange = false, bool autoClose = true)
        {
            this.Alert(new Alert
            {
                Type = AlertType.Success,
                Message = message,
                KeepAfterRouteChange = keepAfterRouteChange,
                AutoClose = autoClose
            });
        }        

        public void Error(string message, bool keepAfterRouteChange = false, bool autoClose = true)
        {
            this.Alert(new Alert
            {
                Type = AlertType.Error,
                Message = message,
                KeepAfterRouteChange = keepAfterRouteChange,
                AutoClose = autoClose
            });
        }        

        public void Info(string message, bool keepAfterRouteChange = false, bool autoClose = true)
        {
            this.Alert(new Alert
            {
                Type = AlertType.Info,
                Message = message,
                KeepAfterRouteChange = keepAfterRouteChange,
                AutoClose = autoClose
            });
        }        

        public void Warn(string message, bool keepAfterRouteChange = false, bool autoClose = true)
        {
            this.Alert(new Alert
            {
                Type = AlertType.Warning,
                Message = message,
                KeepAfterRouteChange = keepAfterRouteChange,
                AutoClose = autoClose
            });
        }        

        public void Alert(Alert alert)
        {
            alert.Id = alert.Id ?? _defaultId;
            this.OnAlert?.Invoke(alert);
        }

        public void Clear(string id = _defaultId)
        {
            this.OnAlert?.Invoke(new Alert { Id = id });
        }
    }
}
 

HTTP Service

Path: /Services/HttpService.cs

The HTTP service is a lightweight wrapper for the .NET Core HttpClient to simplify the code for making HTTP requests from other services, and to implement the following:

  • add JWT token to HTTP Authorization header for API requests when the user is logged in.
  • automatically logout of the Blazor app when a 401 Unauthorized response is received from the API.
  • on error response throw an exception with the message from the response body.

The HTTP service is used by the account service.

using BlazorApp.Helpers;
using BlazorApp.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace BlazorApp.Services
{
    public interface IHttpService
    {
        Task<T> Get<T>(string uri);
        Task Post(string uri, object value);
        Task<T> Post<T>(string uri, object value);
        Task Put(string uri, object value);
        Task<T> Put<T>(string uri, object value);
        Task Delete(string uri);
        Task<T> Delete<T>(string uri);
    }

    public class HttpService : IHttpService
    {
        private HttpClient _httpClient;
        private NavigationManager _navigationManager;
        private ILocalStorageService _localStorageService;
        private IConfiguration _configuration;

        public HttpService(
            HttpClient httpClient,
            NavigationManager navigationManager,
            ILocalStorageService localStorageService,
            IConfiguration configuration
        ) {
            _httpClient = httpClient;
            _navigationManager = navigationManager;
            _localStorageService = localStorageService;
            _configuration = configuration;
        }

        public async Task<T> Get<T>(string uri)
        {
            var request = new HttpRequestMessage(HttpMethod.Get, uri);
            return await sendRequest<T>(request);
        }

        public async Task Post(string uri, object value)
        {
            var request = createRequest(HttpMethod.Post, uri, value);
            await sendRequest(request);
        }

        public async Task<T> Post<T>(string uri, object value)
        {
            var request = createRequest(HttpMethod.Post, uri, value);
            return await sendRequest<T>(request);
        }

        public async Task Put(string uri, object value)
        {
            var request = createRequest(HttpMethod.Put, uri, value);
            await sendRequest(request);
        }

        public async Task<T> Put<T>(string uri, object value)
        {
            var request = createRequest(HttpMethod.Put, uri, value);
            return await sendRequest<T>(request);
        }

        public async Task Delete(string uri)
        {
            var request = createRequest(HttpMethod.Delete, uri);
            await sendRequest(request);
        }

        public async Task<T> Delete<T>(string uri)
        {
            var request = createRequest(HttpMethod.Delete, uri);
            return await sendRequest<T>(request);
        }

        // helper methods

        private HttpRequestMessage createRequest(HttpMethod method, string uri, object value = null)
        {
            var request = new HttpRequestMessage(method, uri);
            if (value != null)
                request.Content = new StringContent(JsonSerializer.Serialize(value), Encoding.UTF8, "application/json");
            return request;
        }

        private async Task sendRequest(HttpRequestMessage request)
        {
            await addJwtHeader(request);

            // send request
            using var response = await _httpClient.SendAsync(request);

            // auto logout on 401 response
            if (response.StatusCode == HttpStatusCode.Unauthorized)
            {
                _navigationManager.NavigateTo("account/logout");
                return;
            }

            await handleErrors(response);
        }

        private async Task<T> sendRequest<T>(HttpRequestMessage request)
        {
            await addJwtHeader(request);
            
            // send request
            using var response = await _httpClient.SendAsync(request);

            // auto logout on 401 response
            if (response.StatusCode == HttpStatusCode.Unauthorized)
            {
                _navigationManager.NavigateTo("account/logout");
                return default;
            }

            await handleErrors(response);

            var options = new JsonSerializerOptions();
            options.PropertyNameCaseInsensitive = true;
            options.Converters.Add(new StringConverter());
            return await response.Content.ReadFromJsonAsync<T>(options);
        }

        private async Task addJwtHeader(HttpRequestMessage request)
        {
            // add jwt auth header if user is logged in and request is to the api url
            var user = await _localStorageService.GetItem<User>("user");
            var isApiUrl = !request.RequestUri.IsAbsoluteUri;
            if (user != null && isApiUrl)
                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", user.Token);
        }

        private async Task handleErrors(HttpResponseMessage response)
        {
            // throw exception on error response
            if (!response.IsSuccessStatusCode)
            {
                var error = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
                throw new Exception(error["message"]);
            }
        }
    }
}
 

Local Storage Service

Path: /Services/LocalStorageService.cs

The local storage service is a lightweight wrapper for the .NET Core IJSRuntime service to simplify getting, setting and removing items from browser local storage. It is used by the account service for getting and setting the currently logged in user.

using Microsoft.JSInterop;
using System.Text.Json;
using System.Threading.Tasks;

namespace BlazorApp.Services
{
    public interface ILocalStorageService
    {
        Task<T> GetItem<T>(string key);
        Task SetItem<T>(string key, T value);
        Task RemoveItem(string key);
    }

    public class LocalStorageService : ILocalStorageService
    {
        private IJSRuntime _jsRuntime;

        public LocalStorageService(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
        }

        public async Task<T> GetItem<T>(string key)
        {
            var json = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", key);

            if (json == null)
                return default;

            return JsonSerializer.Deserialize<T>(json);
        }

        public async Task SetItem<T>(string key, T value)
        {
            await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, JsonSerializer.Serialize(value));
        }

        public async Task RemoveItem(string key)
        {
            await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", key);
        }
    }
}
 

Alert Component

Path: /Shared/Alert.razor

The alert component is responsible for rendering alert messages sent to the alert service. It accepts an optional Id parameter that defaults to "default-alert" that specifies which alerts are rendered by the component, e.g. an alert component with id "default-alert" will render alert (Models.Alert) objects from the alert service that have the same id ("default-alert").

The OnInitialized() method subscribes to the AlertService.OnAlert event, this enables the alert component to be notified whenever an alert message is sent to the alert service and add it to the alerts list for display. Alerts are cleared when an alert with a null message is received from the alert service. The OnInitialized() method also subscribes to the NavigationManager.LocationChanged event so it can automatically clear alerts on route changes.

The Dispose() method unsubscribes from the alert service and navigation manager when the component is destroyed to prevent memory leaks from orphaned subscriptions. The alert component implements the IDisposable interface to add support for the Dispose() method.

The RemoveAlert() method removes the specified alert object from the alerts list which removes it from the UI.

The CssClass() method returns bootstrap classes for each alert type to style the alert, if you're using something other than bootstrap you can change the CSS classes returned to suit your application.

@implements IDisposable
@inject IAlertService AlertService
@inject NavigationManager NavigationManager

@foreach (var alert in alerts)
{
    <div class="@CssClass(alert)">
        <a class="close" @onclick="@(() => RemoveAlert(alert))">&times;</a>
        <span>@alert.Message</span>
    </div>
}

@code {
    [Parameter]
    public string Id { get; set; } = "default-alert";

    [Parameter]
    public bool Fade { get; set; } = true;

    private List<Models.Alert> alerts = new List<Models.Alert>();

    protected override void OnInitialized()
    {
        // subscribe to new alerts and location change events
        AlertService.OnAlert += OnAlert;
        NavigationManager.LocationChanged += OnLocationChange;
    }

    public void Dispose()
    {
        // unsubscribe from alerts and location change events
        AlertService.OnAlert -= OnAlert;
        NavigationManager.LocationChanged -= OnLocationChange;
    }

    private async void OnAlert(Models.Alert alert)
    {
        // ignore alerts sent to other alert components
        if (alert.Id != Id) 
            return;

        // clear alerts when an empty alert is received
        if (alert.Message == null) 
        {
            // remove alerts without the 'KeepAfterRouteChange' flag set to true
            alerts.RemoveAll(x => !x.KeepAfterRouteChange);

            // set the 'KeepAfterRouteChange' flag to false for the 
            // remaining alerts so they are removed on the next clear
            alerts.ForEach(x => x.KeepAfterRouteChange = false);
        }
        else
        {
            // add alert to array
            alerts.Add(alert);
            StateHasChanged();

            // auto close alert if required
            if (alert.AutoClose)
            {
                await Task.Delay(3000);
                RemoveAlert(alert);
            }
        }

        StateHasChanged();
    }

    private void OnLocationChange(object sender, LocationChangedEventArgs e)
    {
        AlertService.Clear(Id);
    }

    private async void RemoveAlert(Models.Alert alert)
    {
        // check if already removed to prevent error on auto close
        if (!alerts.Contains(alert)) return;

        if (Fade) 
        {
            // fade out alert
            alert.Fade = true;

            // remove alert after faded out
            await Task.Delay(250);
            alerts.Remove(alert);
        } 
        else 
        {
            // remove alert
            alerts.Remove(alert);
        }

        StateHasChanged();
    }

    private string CssClass(Models.Alert alert) 
    {
        if (alert == null) return null;

        var classes = new List<string> { "alert", "alert-dismissable", "mt-4", "container" };

        var alertTypeClass = new Dictionary<AlertType, string>();
        alertTypeClass[AlertType.Success] = "alert-success";
        alertTypeClass[AlertType.Error] = "alert-danger";
        alertTypeClass[AlertType.Info] = "alert-info";
        alertTypeClass[AlertType.Warning] = "alert-warning";

        classes.Add(alertTypeClass[alert.Type]);

        if (alert.Fade)
            classes.Add("fade");

        return string.Join(' ', classes);
    }   
}
 

Main Layout Component

Path: /Shared/MainLayout.razor

The main layout component is the default layout for the Blazor application, it contains the main nav bar for the app which is only displayed when the user is logged in, the global <Alert /> component, and the @Body keyword to specify the location to render each page component based on the current route / path. The main layout is set as the default layout in the app component.

@inherits LayoutComponentBase
@inject IAccountService AccountService
@inject NavigationManager NavigationManager

@if (LoggedIn)
{
    <!-- nav -->
    <nav class="navbar navbar-expand navbar-dark bg-dark">
        <div class="navbar-nav">
            <NavLink href="" Match="NavLinkMatch.All" class="nav-item nav-link">Home</NavLink>
            <NavLink href="users" class="nav-item nav-link">Users</NavLink>
            <NavLink href="account/logout" class="nav-item nav-link">Logout</NavLink>
        </div>
    </nav>
}

<div class="app-container @(LoggedIn ? "bg-light" : "")">
    <Alert />
    @Body
</div>

@code {
    public bool LoggedIn 
    {
        get { return AccountService.User != null; }
    }
}
 

App CSS

Path: /wwwroot/css/app.css

The app css contains custom styles for the Blazor application.

The blazor-error-ui styles were part of the initial generated project and are for styling uncaught exceptions in a yellow bar at the bottom of the window.

The validation classes were copied from Bootstrap 4.5.0 and renamed to their equivalent Blazor classes to make validation messages appear correctly with Bootstrap styling on the forms in the tutorial example.

a { cursor: pointer }

.app-container {
    min-height: 320px;
    overflow: hidden;
}

.btn-delete-user {
    width: 40px;
    text-align: center;
    box-sizing: content-box;
}

#blazor-error-ui {
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;
}

/* 
    below styles were copied from bootstrap 4.5.0 and renamed to be compatible with blazor
    - '.validation-message' == bootstrap '.invalid-feedback'
    - '.invalid' == bootstrap '.is-invalid'
*/

.validation-message {
    display: none;
    width: 100%;
    margin-top: 0.25rem;
    font-size: 80%;
    color: #dc3545;
}

.invalid ~ .validation-message {
    display: block;
}

.form-control.invalid {
    border-color: #dc3545;
    padding-right: calc(1.5em + 0.75rem);
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
    background-repeat: no-repeat;
    background-position: right calc(0.375em + 0.1875rem) center;
    background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}

.form-control.invalid:focus {
    border-color: #dc3545;
    box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}

textarea.form-control.invalid {
    padding-right: calc(1.5em + 0.75rem);
    background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);
}

.form-check-input.invalid ~ .form-check-label {
    color: #dc3545;
}

.form-check-input.invalid ~ .validation-message {
    display: block;
}
 

App Settings

Path: /wwwroot/appsettings.json

The app settings file contains config variables used by the Blazor application.

To disable the fake backend and send HTTP requests through to the "apiUrl" change the "fakeBackend" setting to "false".

{
    "apiUrl": "http://localhost:4000",
    "fakeBackend": "true"
}
 

Root index.html file (host page)

Path: /wwwroot/index.html

The Blazor host page is the initial file loaded by the browser that kicks everything off, it loads the blazor.webassembly.js script that downloads and initializes the .NET runtime and our compiled Blazor application.

<!DOCTYPE html>
<html>
<head>
    <base href="/" />
    <title>ASP.NET Core Blazor WebAssembly - User Registration and Login Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- bootstrap css -->
    <link href="//netdna.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />

    <!-- custom css -->
    <link href="css/app.css" rel="stylesheet" />
</head>
<body>
    <app>Loading...</app>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">X</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
</body>
</html>
 

Razor Imports File

Path: /_Imports.razor

Razor imports files include directives that are automatically applied to all other razor components in the same folder and subfolders. An _Imports.razor file can be added to any folder and can include any razor directive.

By adding the below @using statements to this imports file in the root folder, all blazor components in the application have access to the namespaces without needing to add any @using statements themselves.

@using BlazorApp
@using BlazorApp.Helpers
@using BlazorApp.Models
@using BlazorApp.Models.Account
@using BlazorApp.Services
@using BlazorApp.Shared
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using System.Web
 

App Component

Path: /App.razor

The app component is the root component in the Blazor tutorial application and contains a Router component with Found and NotFound templates.

If the route is found (i.e. bound to a page component with the @page directive) then the route data is passed to the AppRouteView to render the page.

If the route is not found the user is redirected to the home page.

@inject NavigationManager NavigationManager

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <AppRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        @{ 
            // redirect to home page if not found
            NavigationManager.NavigateTo(""); 
        }
    </NotFound>
</Router>
 

Blazor App csproj

Path: /BlazorApp.csproj

The csproj (C# project) is an MSBuild based file that contains target framework and NuGet package dependency information for the Blazor application.

<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>netstandard2.1</TargetFramework>
        <RazorLangVersion>3.0</RazorLangVersion>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.1" />
        <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.1" PrivateAssets="all" />
        <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="3.2.1" PrivateAssets="all" />
        <PackageReference Include="System.Net.Http.Json" Version="3.2.1" />
    </ItemGroup>

</Project>
 

Blazor App Program

Path: /Program.cs

The program class the main entry point to start the application, it sets the root component, configures dependency injection, performs service initialization and starts the Blazor app.

using BlazorApp.Helpers;
using BlazorApp.Services;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace BlazorApp
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("app");

            builder.Services
                .AddScoped<IAccountService, AccountService>()
                .AddScoped<IAlertService, AlertService>()
                .AddScoped<IHttpService, HttpService>()
                .AddScoped<ILocalStorageService, LocalStorageService>();

            // configure http client
            builder.Services.AddScoped(x => {
                var apiUrl = new Uri(builder.Configuration["apiUrl"]);

                // use fake backend if "fakeBackend" is "true" in appsettings.json
                if (builder.Configuration["fakeBackend"] == "true")
                {
                    var fakeBackendHandler = new FakeBackendHandler(x.GetService<ILocalStorageService>());
                    return new HttpClient(fakeBackendHandler) { BaseAddress = apiUrl };
                }

                return new HttpClient() { BaseAddress = apiUrl };
            });

            var host = builder.Build();

            var accountService = host.Services.GetRequiredService<IAccountService>();
            await accountService.Initialize();

            await host.RunAsync();
        }
    }
}

 

Subscribe or Follow Me For Updates

Subscribe to my YouTube channel or follow me on Twitter or GitHub to be notified when I post new content.