Published: August 13 2020

Blazor WebAssembly - JWT Authentication Example & Tutorial

Tutorial built with ASP.NET Core Blazor WebAssembly 3.2.1

Other versions available:

The following is a custom JWT authentication example and tutorial showing how to setup a simple login page in ASP.NET Core Blazor WebAssembly (WASM).

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).

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/.

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

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


Tools required to run the Blazor JWT 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 detailed instructions see ASP.NET Core - Setup Development Environment.


Running the Blazor JWT Auth Example Locally

  1. Download or clone the tutorial project code from https://github.com/cornflourblue/blazor-webassembly-jwt-authentication-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 - JWT Authentication Tutorial with Example API. 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-jwt-authentication-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 API

For full details about the example Node.js API see the post NodeJS - JWT Authentication Tutorial with Example API. But to get up and running quickly just follow the below steps.

  1. Install NodeJS and NPM from https://nodejs.org.
  2. Download or clone the project source code from https://github.com/cornflourblue/node-jwt-authentication-api
  3. 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.
  4. 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.js 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 "/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.

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-jwt-authentication-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 IAuthenticationService AuthenticationService { get; set; }

        protected override void Render(RenderTreeBuilder builder)
        {
            var authorize = Attribute.GetCustomAttribute(RouteData.PageType, typeof(AuthorizeAttribute)) != null;
            if (authorize && AuthenticationService.User == null)
            {
                var returnUrl = WebUtility.UrlEncode(new Uri(NavigationManager.Uri).PathAndQuery);
                NavigationManager.NavigateTo($"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 checks if the request matches one of the faked routes, at the moment this includes POST requests to the /users/authenticate route for handling authentication, and GET requests to the /users route for getting all users.

Requests to the authenticate route are handled by the authenticate() function which checks the username and password against an array of hardcoded users. If the username and password are correct then an ok response is returned with the user details and a fake jwt token, otherwise an error response is returned.

Requests to the get users route are handled by the getUsers() function which checks if the user is logged in by calling the new isLoggedIn() helper function. If the user is logged in an ok() response with the whole users array is returned, otherwise a 401 Unauthorized response is returned by calling the new unauthorized() helper function.

If the request doesn't match any of the faked routes it is passed through as a real HTTP request to the backend API.

using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace BlazorApp.Helpers
{
    public class FakeBackendHandler : HttpClientHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var users = new[] { new { Id = 1, Username = "test", Password = "test", FirstName = "Test", LastName = "User" } };
            var path = request.RequestUri.AbsolutePath;
            var method = request.Method;

            if (path == "/users/authenticate" && method == HttpMethod.Post)
            {
                return await authenticate();
            }
            else if (path == "/users" && method == HttpMethod.Get)
            {
                return await getUsers();
            }
            else
            {
                // 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<Dictionary<string, string>>(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,
                    Username = user.Username,
                    FirstName = user.FirstName,
                    LastName = user.LastName,
                    Token = "fake-jwt-token"
                });
            }

            async Task<HttpResponseMessage> getUsers()
            {
                if (!isLoggedIn()) return await unauthorized();
                return await ok(users);
            }

            // helper functions

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

            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";
            } 
        }
    }
}
 

User Model

Path: /Models/User.cs

The user model is a small class that defines the properties of a user, and is used for passing user data around the application.

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

Home Page Component

Path: /Pages/Index.razor

The home page component is displayed after signing in to the application, it is bound to the home page path with the page directive (@page "/") and contains a simple welcome message with a list of all users. The component fetches all users from the api by calling UserService.GetAll() from the OnInitializedAsync() Blazor lifecycle method.

The page is restricted to authorized users with the authorize attribute (@attribute [Authorize]) which is checked by the app route view component before rendering the page.

@page "/"
@attribute [Authorize]
@inject IUserService UserService

<div class="card mt-4">
    <h4 class="card-header">You're logged in with Blazor WebAssembly & JWT!!</h4>
    <div class="card-body">
        <h6>Users from secure api end point</h6>
        @if (loading)
        {
            <div class="spinner-border spinner-border-sm"></div>
        }
        @if (users != null)
        {
            <ul>
                @foreach (var user in users)
                {
                    <li>@user.FirstName @user.LastName</li>
                }
            </ul>
        }
    </div>
</div>

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

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

Login Page Component

Path: /Pages/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 related 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 AuthenticationService.Login(model.Username, model.Password) method is called from the HandleValidSubmit() 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).

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

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 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 "/login"
@using System.ComponentModel.DataAnnotations
@inject IAuthenticationService AuthenticationService
@inject NavigationManager NavigationManager

<div class="col-md-6 offset-md-3 mt-5">
    <div class="alert alert-info">
        Username: test<br />
        Password: test
    </div>
    <div class="card">
        <h4 class="card-header">Blazor WebAssembly JWT Login Example</h4>
        <div class="card-body">
            <EditForm Model="@model" OnValidSubmit="HandleValidSubmit">
                <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 class="btn btn-primary">
                    @if (loading) {
                        <span class="spinner-border spinner-border-sm mr-1"></span>
                    }
                    Login
                </button>
                @if (!string.IsNullOrEmpty(error)) {
                    <div class="alert alert-danger mt-3 mb-0">@error</div>
                }
            </EditForm>
        </div>
    </div>
</div>

@code {
    private Model model = new Model();
    private bool loading;
    private string error;

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

    private async void HandleValidSubmit()
    {
        loading = true;
        try
        {
            await AuthenticationService.Login(model.Username, model.Password);
            var returnUrl = NavigationManager.QueryString("returnUrl") ?? "/";
            NavigationManager.NavigateTo(returnUrl);
        }
        catch (Exception ex)
        {
            error = ex.Message;
            loading = false;
            StateHasChanged();
        }
    }

    private class Model
    {
        [Required]
        public string Username { get; set; }

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

Logout Page Component

Path: /Pages/Logout.razor

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

@page "/logout"
@inject IAuthenticationService AuthenticationService

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

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"
            }
        }
    }
}
 

Authentication Service

Path: /Services/AuthenticationService.cs

The authentication service is used to login and logout of the Blazor app, and allows access to the currently logged in user via the User property.

The Initialize() method is called from Program.cs on startup and assigns 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.

The Login() method sends the user credentials to the API via an HTTP POST request for authentication. If successful the user object (including a JWT auth token) is assigned to the User property and stored in local storage to keep the user logged in between page refreshes.

The Logout() method sets the the User property to null, removes the user object from local storage and navigates to the login page.

using BlazorApp.Models;
using Microsoft.AspNetCore.Components;
using System.Threading.Tasks;

namespace BlazorApp.Services
{
    public interface IAuthenticationService
    {
        User User { get; }
        Task Initialize();
        Task Login(string username, string password);
        Task Logout();
    }

    public class AuthenticationService : IAuthenticationService
    {
        private IHttpService _httpService;
        private NavigationManager _navigationManager;
        private ILocalStorageService _localStorageService;

        public User User { get; private set; }

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

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

        public async Task Login(string username, string password)
        {
            User = await _httpService.Post<User>("/users/authenticate", new { username, password });
            await _localStorageService.SetItem("user", User);
        }

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

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 authentication service and user service.

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<T> Post<T>(string uri, object value);
    }

    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<T> Post<T>(string uri, object value)
        {
            var request = new HttpRequestMessage(HttpMethod.Post, uri);
            request.Content = new StringContent(JsonSerializer.Serialize(value), Encoding.UTF8, "application/json");
            return await sendRequest<T>(request);
        }

        // helper methods

        private async Task<T> sendRequest<T>(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);

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

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

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

            return await response.Content.ReadFromJsonAsync<T>();
        }
    }
}
 

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 authentication service and http service.

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);
        }
    }
}
 

User Service

Path: /Services/UserService.cs

The user service contains a method for getting all users from the api, I included it to demonstrate accessing a secure api endpoint with a JWT token after logging in to the application, the JWT token is added to the HTTP authorization header by the http service, and the secure endpoint in the example is a fake one implemented in the fake backend handler.

using BlazorApp.Models;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace BlazorApp.Services
{
    public interface IUserService
    {
        Task<IEnumerable<User>> GetAll();
    }

    public class UserService : IUserService
    {
        private IHttpService _httpService;

        public UserService(IHttpService httpService)
        {
            _httpService = httpService;
        }

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

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 for authenticated users, 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 IAuthenticationService AuthenticationService
@inject NavigationManager NavigationManager

@if (AuthenticationService.User != null)
{
    <!-- 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="logout" class="nav-item nav-link">Logout</NavLink>
        </div>
    </nav>
}

<div class="container">
    @Body
</div>
 

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 login form.

#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 - JWT Authentication Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- bootstrap css -->
    <link href="//netdna.bootstrapcdn.com/bootstrap/4.5.0/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.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 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<IAuthenticationService, AuthenticationService>()
                .AddScoped<IUserService, UserService>()
                .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")
                    return new HttpClient(new FakeBackendHandler()) { BaseAddress = apiUrl };

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

            var host = builder.Build();

            var authenticationService = host.Services.GetRequiredService<IAuthenticationService>();
            await authenticationService.Initialize();

            await host.RunAsync();
        }
    }
}

 


Need Some Blazor Help?

Search fiverr for freelance Blazor developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by