I will show how to create custom user management with NET8 and Blazor based on Microsoft Identity. Here is how to add custom fields for users
In every application I wrote, there is always the requirement to authenticate the user: in this new series of posts, I will show how to create custom user management with NET8 and Blazor based on Microsoft Identity.
The full source code of this post is available on GitHub. If you find this post useful, please consider making a donation on GitHub.
For any comment, suggestion or help, please see the Forum.
Other posts:
- Custom User Management with NET8 and Blazor (1st part)
- NET8, Blazor and Custom User Management (2nd part)
Allow Login with both Username and Email
In the first post, I showed how to add custom fields to the registration page. Ideally, I may want to allow my users to log in with both the username and the email ID. Under Components \ Pages \ Account
open the Razor page Login.razor
. Almost at the beginning of the code
section you have the InputModel
.
Now, replace the InputModel
with this one:
public class InputModel
{
[Required]
[Display(Name = "Email / Username")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
At the model level, we made the Email Property accept both email IDs and plain text. Now, I add a new function to check if the entered data is a valid email ID or not.
First, at the top of the page add
@using System.Net.Mail
@inject UserManager<ApplicationUser> UserManager
Then, at the bottom of the code
section add this function to check is the email is a valid one:
public bool IsValidEmail(string emailaddress)
{
try
{
MailAddress m = new MailAddress(emailaddress);
return true;
}
catch (FormatException)
{
return false;
}
}
Now, I have to check if the email is valid in order to find the username of the user. For this reason, I have to change the LoginUser()
function adding a few lines at the beginning (full code for clarity):
public async Task LoginUser()
{
var userName = Input.Email;
if (IsValidEmail(Input.Email))
{
var user = await UserManager.FindByEmailAsync(Input.Email);
if (user != null)
{
userName = user.UserName;
}
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
Logger.LogInformation("User logged in.");
RedirectManager.RedirectTo(ReturnUrl);
}
if (result.RequiresTwoFactor)
{
RedirectManager.RedirectTo(
"/Account/LoginWith2fa",
new() { ["ReturnUrl"] = ReturnUrl, ["RememberMe"] = Input.RememberMe });
}
if (result.IsLockedOut)
{
Logger.LogWarning("User account locked out.");
RedirectManager.RedirectTo("/Account/Lockout");
}
else
{
errorMessage = "Error: Invalid login attempt.";
}
}
Build the application and run it. You would be able to log in with both the username and the email id now.
Adding the Custom User Fields To Profile Settings
If you click on the left side on your name, you are redirected to the Manage your account section.
There are quite a lot of basic options here, like changing your phone number, updating the email id, changing the password, and so on. Let’s try to extend these pages in the coming sections.
Change the InputModel
As the first step, let’s try to add the first name and last name fields to this form. Navigate to Components/Pages/Account/Manage/Index.razor
.
Replace the InputModel
. Here we added the new fields (including Profile Picture, although we will implement it in the next section)
private sealed class InputModel
{
[Display(Name = "First Name")]
public string? FirstName { get; set; }
[Display(Name = "Last Name")]
public string? LastName { get; set; }
[Display(Name = "Username")]
public string? Username { get; set; }
[Phone]
[Display(Name = "Phone number")]
public string? PhoneNumber { get; set; }
[Display(Name = "Profile Picture")]
public byte[]? ProfilePicture { get; set; }
}
Next, while loading the form we need to load these data to the memory as well. So, change the OnInitializedAsync
with the following code:
@code {
private ApplicationUser _user = default!;
private string? _firstname;
private string? _lastname;
private string? _username;
private string? _phoneNumber;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
Input ??= new();
_user = await UserAccessor.GetRequiredUserAsync();
_username = await UserManager.GetUserNameAsync(_user);
_phoneNumber = await UserManager.GetPhoneNumberAsync(_user);
_firstname = _user.FirstName;
_lastname = _user.LastName;
Input.FirstName ??= _firstname;
Input.LastName ??= _lastname;
Input.PhoneNumber ??= _phoneNumber;
Input.ProfilePicture ??= _user.ProfilePicture;
}
Change the UI
The next step is to add the fields in the form. For that, in the EditForm
tag add the following HTML code:
<div class="form-floating mb-3">
<InputText id="firstname" @bind-Value="Input.FirstName" class="form-control" placeholder="Please enter your first name." />
<label for="firstname" class="form-label">First name</label>
<ValidationMessage For="() => Input.FirstName" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText id="lastname" @bind-Value="Input.LastName" class="form-control" placeholder="Please enter your last name." />
<label for="lastname" class="form-label">Last name</label>
<ValidationMessage For="() => Input.LastName" class="text-danger" />
</div>
That’s it. Build and Run your application. Go to the “Manage your account” page. You will see the changes here.
Adding a Profile Picture
Remember the part where we added the Update code for FirstName and Lastname? Now, I want to do something similar for the picture profile of the user. There is a BUT. With NET8, Microsoft changes the lifecycle of a Blazor application. I recommend reading the article from Microsoft Learn to have a glance at the new changes. One of the exciting new entry is the QuickGrid
component and you have more info here.
Before implementing the profile picture, there is one thing I want to highlight and it is the new RenderMode.InteractiveServer
.
Render modes
So, every component in a Blazor Web App adopts a render mode to determine the hosting model that it uses, where it’s rendered, and whether or not it’s interactive.
The following table shows the available render modes for rendering Razor components in a Blazor Web App. To apply a render mode to a component use the @rendermode
directive on the component instance or on the component definition. Later in this article, examples are shown for each render mode scenario.
Name | Description | Render location | Interactive |
---|---|---|---|
Static | Static server rendering | Server | ❌No |
Interactive Server | Interactive server rendering using Blazor Server | Server | ✔️Yes |
Interactive WebAssembly | Interactive client rendering using Blazor WebAssembly | Client | ✔️Yes |
Interactive Auto | Interactive client rendering using Blazor Server initially and then WebAssembly on subsequent visits after the Blazor bundle is downloaded | Server, then client | ✔️Yes |
Prerendering is enabled by default for interactive components. Guidance on controlling prerendering is provided later in this article.
The following examples demonstrate setting the component’s render mode with a few basic Razor component features.
To test the render mode behaviours locally, you can place the following components in an app created from the Blazor Web App project template. When you create the app, select the checkboxes (Visual Studio) or apply the CLI options (.NET CLI) to enable both server-side and client-side interactivity. For guidance on how to create a Blazor Web App, see Tooling for ASP.NET Core Blazor.
Prepare the profile
Now, in the InputModel
I already added a field for the ProfilePicture
to be saved in the database. In the Index.razor
file, I have to accommodate the picture to be uploaded. So, I wrap the EditForm
in a Bootstrap col
and define 2 columns like that
<div class="row">
<div class="col-md-6">
<EditForm id="profile-form" Model="Input" FormName="profile"
OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" />
// ... code omitted ...
</EditForm>
</div>
<div class="col-md-6">
<div class="form-floating">
<UploadProfilePicture />
</div>
</div>
</div>
As you can see, I added the UploadProfilePicture
, a Razor component that has not been created yet.
Implement the UploadProfilePicture component
Now, the implementation of this Razor page should be quite straightforward. First, I have to add the code to display the image or ask the user to upload the image.
<div class="form-floating">
<label asp-for="User.ProfilePicture" style="width: 100%;"></label>
@if (User?.ProfilePicture?.Length > 0)
{
<img id="profilePicture" style="width:350px;height:350px; object-fit:cover"
src="data:image/*;base64,@(Convert.ToBase64String(User.ProfilePicture))">
}
else
{
<InputFile OnChange="LoadFiles" />
}
<span asp-validation-for="User.ProfilePicture" class="text-danger"></span>
</div>
As you can see, the image is coming from the database and then I convert the string from the database into a Base64
string and then display the image. The code behind that is the following
@code {
[Parameter]
public ApplicationUser User { get; set; } = default!;
[CascadingParameter]
private Task<AuthenticationState> AuthenticationState { get; set; } = default!;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
MemoryStream ms = new MemoryStream();
await e.File.OpenReadStream().CopyToAsync(ms);
var bytes = ms.ToArray();
await using var scope = ServiceProvider.CreateAsyncScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
// Reload the ApplicationUser so we can make modifications to it in the new scope.
var principal = (await AuthenticationState).User;
User = await userManager.GetUserAsync(principal) ??
throw new Exception("Could not reload user!");
User.ProfilePicture = bytes;
await userManager.UpdateAsync(User);
}
}
In order to have this code working, I have to add the following using
@using BlazorIdentity.Data
@using Microsoft.AspNetCore.Identity
@inject IServiceProvider ServiceProvider
Because this component has to interact with the server, I also have to add this new line at the top of the file
@rendermode RenderMode.InteractiveServer
This line informs Blazor that this component has to communicate with the server something.
Wrap up
So far, I explained how to improve the basic Identity section with a few changes to the original files. In the next section, I’m going to create a bunch of completely new files for managing users and roles. Then, I’m going to update the project to remove the concurrent access to the UserManager
. Stay tuned!
In the UploadProfilePicture Component is correct to update the User parameter? Best practice not indicate to update only private vars to avoid external update of the parameter?
I shared my repository with Microsoft and they gave me this solution/workaround. So, I think this implementation is correct.