Home > front end >  Infinite await using UserManager with blazor server app
Infinite await using UserManager with blazor server app

Time:01-19

I am actually developping a blazor server project using EF Core 6 and I'm facing a problem. Actually, I would like to use a custom service class to register users in my app, but, when registering a new User, the UserManager class seems to freeze. I first thought of a deadlock, so I used the dotnet-dump command to analyse the threads data, but it seems there are no threads keeping a lock indefinitely.

The point blocking is in the RegisterService.cs on the line :

await _userManager.SetUserNameAsync(newUser, registerModel.Username);

I tried to setup the User object data like Username and Email by hand using :

newUser.UserName = registerModel.Username;

But the freeze appear in the following line then :

IdentityResult result = await _userManager.CreateAsync(newUser, registerModel.Password);

Note that the function IsEmailUsed calling the UserManager is working perfectly.

I'm running out of solutions to try to solve my problem, so I'm finally asking here. Here is my code :

Startup.cs :

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
        services.AddServerSideBlazor();
        services.AddMudServices();
        services.AddDbContext<Rpg_AgendaContext>(options => options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection")));

        services.AddDefaultIdentity<User>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<Rpg_AgendaContext>();

        services.AddScoped<SignInManager<User>>();
        services.AddScoped<UserManager<User>>();
        
        services.AddScoped<RegisterService>();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();

        app.UseMiddleware<LoginMiddleware<User>>();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapBlazorHub();
            endpoints.MapFallbackToPage("/_Host");
        });
    }
}

Register.razor :

@page "/register"
@using System.Text.RegularExpressions
@using Microsoft.AspNetCore.Identity
@using RpgAgenda.Data.Entities
@using Rpg_Agenda.Pages.Shared
@using Rpg_Agenda.Service.RegisterService.Models
@using Rpg_Agenda.Services
@using Rpg_Agenda.Services.RegisterService

@inject NavigationManager navMgr
@inject RegisterService registerService

<MudGrid Justify="Justify.Center">
    <MudItem xs="4">
        <MudPaper Class="pa-4" Elevation="3">
            <MudForm @ref="form">
                <ErrorField errorValue="@registerModel.Error"></ErrorField>

                <MudTextField T="string" Label="Username" Required="true" RequiredError="Username is required" @ref="username" />
                <MudTextField T="string" Label="Email" Required="true" Validation="@(new Func<string, string>(EmailCorrect))" RequiredError="Email is required" @ref="email" />
                <MudTextField T="string" Label="Confirm email" Required="true" Validation="@(new Func<string, string>(EmailMatch))" RequiredError="Email confirmation is required" />
                <MudTextField T="string" InputType="InputType.Password" Validation="@(new Func<string, string>(PasswordStrength))" Label="Password" Required="true" RequiredError="Password is required" @ref="password"/>
                <MudTextField T="string" InputType="InputType.Password" Validation="@(new Func<string, string>(PasswordMatch))" Label="Confirm password" Required="true" RequiredError="Password confirmation is required" />

                <div >
                    <MudButton FullWidth="true" OnClick="RegisterClicked" Variant="Variant.Filled" Color="Color.Primary">Register</MudButton>
                </div>
            </MudForm>
        </MudPaper>
    </MudItem>
</MudGrid>

@code {
    private MudForm form;
    private MudTextField<string> username;
    private MudTextField<string> email;
    private MudTextField<string> password;
    private RegisterModel registerModel = new();

    private async Task RegisterClicked()
    {
        await form.Validate();

        if(form.IsValid)
        {
            registerModel.Username = username.Value;
            registerModel.Email = email.Value;
            registerModel.Password =  password.Value;
            bool succeed = registerService.RegisterUser(registerModel).Result;
            if (succeed)
                navMgr.NavigateTo("/");
        }
    }

    private string PasswordStrength(string password)
    {
        if (password == null)
            return null;
        if (password.Length < 10)
            return "Password must be at least 10 characters";
        if (!Regex.IsMatch(password, @"[A-Z]"))
            return "Password must contain at least one capital letter";
        if (!Regex.IsMatch(password, @"[a-z]"))
            return "Password must contain at least one lowercase letter";
        if (!Regex.IsMatch(password, @"[0-9]"))
            return "Password must contain at least one digit letter";
        if (!Regex.IsMatch(password, @"[^a-zA-Z0-9]"))
            return "Password must contain at least one special character";
        return null;
    }

    private string PasswordMatch(string passwordConfirmation) => password.Value == passwordConfirmation ? null : "Password doesn't match";

    private string EmailMatch(string emailConfirmation) => email.Value == emailConfirmation  ? null : "Email doesn't match";

    private string EmailCorrect(string email)
    {
        if (email == null)
            return null;
        if (Regex.Match(email, @"^([\w\.\-] )@([\w\-] )((\.(\w){2,3}) )$", RegexOptions.IgnoreCase).Success)
        {
            if (registerService.IsEmailUsed(email).Result)
                return "Email already used !";
            return null;
        }
        else
            return "Email format is incorrect";
    }
}

RegisterService.cs :

public class RegisterService
{
    private readonly UserManager<User> _userManager;
    private readonly SignInManager<User> _signInManager;

    public RegisterService(UserManager<User> userManager, SignInManager<User> signInManager)
    {
        _userManager = userManager;
        _signInManager = signInManager;
    }

    public async Task<bool> IsEmailUsed(string email) => await _userManager.FindByEmailAsync(email) != null;

    public async Task<bool> RegisterUser(RegisterModel registerModel)
    {
        User newUser = CreateUser(registerModel).Result;
        IdentityResult result = await _userManager.CreateAsync(newUser, registerModel.Password);
        if (!result.Succeeded)
        {
            registerModel.Error = result.Errors.Select(error => error.Description).ToArray().Aggregate((current, next) => $"{current}\n{next}");
        }

        return result.Succeeded;
    }

    private async Task<User> CreateUser(RegisterModel registerModel)
    {
        User newUser = new User();

        Debug.WriteLine($"ID : {newUser.Id}, UN : {newUser.UserName}, @ : {newUser.Email}");

        await _userManager.SetUserNameAsync(newUser, registerModel.Username);
        await _userManager.SetEmailAsync(newUser, registerModel.Email);
        return newUser;
    }
}

Thank you for helping me.

CodePudding user response:

Change

bool succeed = registerService.RegisterUser(registerModel).Result;

to

bool succeed = await registerService.RegisterUser(registerModel);

and look for other uses of .Result and .Wait(). Remove them, they can deadlock.

  •  Tags:  
  • Related