NET8, Blazor and Custom User Management

blazor microsoft identity

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:

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.

Manage your account - NET8, Blazor and Custom User Management
Manage your account

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.

Your new profile page - NET8, Blazor and Custom User Management
Your new profile page

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.

NameDescriptionRender locationInteractive
StaticStatic server renderingServer❌No
Interactive ServerInteractive server rendering using Blazor ServerServer✔️Yes
Interactive WebAssemblyInteractive client rendering using Blazor WebAssemblyClient✔️Yes
Interactive AutoInteractive client rendering using Blazor Server initially and then WebAssembly on subsequent visits after the Blazor bundle is downloadedServer, 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!

2 thoughts on “NET8, Blazor and Custom User Management

  1. 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?

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.