Tutorial built with ASP.NET Core Blazor WebAssembly 3.2.1

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

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

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

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

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

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.

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

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

#blazor #webassembly #web-development #programming #developer

How to Build Simple User Registration and Login with Blazor WebAssembly
14.35 GEEK