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/.
To develop and run ASP.NET Core Blazor applications locally, download and install the following:
dotnet run
from the command line in the project root folder (where the BlazorApp.csproj file is located)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
.
Get up and running quickly just follow the below steps.
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
."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.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.
npm install
or npm i
from the command line in the project root folder (where the package.json is located).npm start
from the command line in the project root folder, you should see the message Server listening on port 4000
."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.Get up and running quickly just follow the below steps.
npm install
or npm i
from the command line in the project root folder (where the package.json is located).npm start
from the command line in the project root folder, you should see the message Server listening on port 4000
."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.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:
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).
ASP.NET Core Razor components that can be used in multiple areas of the application and are not bound to a specific route.
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.
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]
).
Anything that doesn’t fit into the above folders.
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.
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/.
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);
}
}
}
}
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];
}
}
}
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; }
}
}
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; }
}
}
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