I am creating a custom registration form for my ASP.NET Blazor Server Application. I've got all the validation working like I want, including validating a date using a custom validation attribute, so I am familiar with that process.
The part I am getting stuck on is the uniqueness of the email address/username. The ASP.NET identity framework returns a succeeded property that is false if there is already a user with the provided email/username. There's even a helpful description. Unfortunately this is after the form validation and Blazor has already decided the form is valid.
var result = await userManager.CreateAsync(user, _tempPassword);
result.Succeeded = false
result.Errors.Count = 1
result.Errors[0].Description = "Username 'XXX' is already taken."
It seems crazy to me that I have to create a complex, custom validation attribute that includes dependency injection (for the identity and/or main databases) when the information is readily available "on the page".
Below is my Blazor page. See the else
statement in the HandleValidSubmit
method.
@page "/Employee/Create"
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Identity
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject UserManager<ApplicationUser> userManager
@inject NavigationManager NavigationManager
@inject IDataAccess database
<AuthorizeView Roles="Admin" Context="authContext">
<Authorized>
<h3>New Employee</h3>
<EditForm Model="_employee" Context="formContext" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<p>First Name: <InputText id="firstName" @bind-Value="_employee.FirstName" /></p>
<p>Last Name: <InputText id="lastName" @bind-Value="_employee.LastName" /></p>
<p>Email: <InputText id="email" @bind-Value="_employee.Email" /></p>
<div id="newEmployeeButtons">
<button type="submit" >Save</button>
</div>
</EditForm>
</Authorized>
</AuthorizeView>
@code {
private Employee _employee = new();
protected override void OnInitialized()
{
_employee = new Employee();
}
private async Task HandleValidSubmit()
{
ApplicationUser user = new()
{
FirstName = _employee.FirstName,
LastName = _employee.LastName,
Email = _employee.Email
};
var result = await userManager.CreateAsync(user, _tempPassword);
if (result.Succeeded)
{
// Save to main DB
_employee.Id = await userManager.GetUserIdAsync(user);
await database.CreateEmployeeAsync(_employee);
_employee = new(); // Clear form
}
else
{
// Microsoft.AspNetCore.Identity.IdentityResult returns an error here,
// NOT an exception. The error has a description of "Username 'XXX'
// is already taken."
// HOW DO I TURN THIS INTO A VALIDATION MESSAGE ON THE PAGE?!
// Currently it just fails silently
}
}
}
EDIT 2:
I decided that rather than posting code segments here, I would push a minimal reproducible example to GitHub.
The repository is located here
Branch: master
As previously stated, the EditContext_OnFieldChanged
method is never raised. I set a break point at the beginning of the method, but the break point is never hit. Since I set emailExists = true
, the validation should have always failed (for testing purposes).
Branch: noModelAttribute
Removes the Model
attribute from the EditForm
component, but only throws an exception when navigating to the /Employee/Create
page.
Final Edit
The real issue seems to have been the declaration in the OnInitialized
method. Using _editContext = new(_employee);
works while EditContext _editContext = new(_employee);
resulted in an exception.
CodePudding user response:
Here's one way to do that...
<EditForm EditContext="EditContext" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<p>First Name: <InputText id="firstName" @bind-Value="_employee.FirstName" /></p>
<p>Last Name: <InputText id="lastName" @bind-Value="_employee.LastName" /></p>
<p>Email: <InputText id="email" @bind-Value="_employee.Email" /></p>
<div id="newEmployeeButtons">
<button type="submit" >Save</button>
</div>
</EditForm>
@code {
private Employee _employee = new();
private EditContext EditContext;
ValidationMessageStore messages;
protected override void OnInitialized()
{
EditContext = new EditContext(_employee);
EditContext.OnFieldChanged = EditContext_OnFieldChanged;
messages = new ValidationMessageStore(EditContext);
}
private void EditContext_OnFieldChanged(object sender, FieldChangedEventArgs e)
{
if (e.FieldIdentifier.FieldName == nameof(_employee.Email))
{
// Perform a database call to verify if the user
// name exists in the database
var _emailExists = MyService.EmailExists(
_employee.Email);
if (_emailExists)
{
messages.Clear(e.FieldIdentifier);
messages.Add(e.FieldIdentifier, "is already taken.");
}
else
{
messages.Clear(e.FieldIdentifier);
}
}
EditContext.NotifyValidationStateChanged();
// var isValid = EditContext.Validate();
// if (isValid)
// {
// this.HandleValidSubmit();
// }
InvokeAsync(() => StateHasChanged());
}
}
Note: You should define the Exists method in a service.
You must not inject the UserManager
service into your components.
I wrote this code quick, but I hope you've got the point...
Update:
@page "/Employee/Create"
@using BlazorValidation.Data
<h3>New Employee</h3>
<EditForm EditContext="_editContext" Context="formContext" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<p>First Name: <InputText id="firstName" @bind-Value="_employee.FirstName" /></p>
<p>Last Name: <InputText id="lastName" @bind-Value="_employee.LastName" /></p>
<p>Email: <InputText id="email" @bind-Value="_employee.Email" /></p>
<div id="newEmployeeButtons">
<button type="submit" >Save</button>
</div>
</EditForm>
@code {
private Employee _employee = new();
private EditContext _editContext;
private ValidationMessageStore messages;
protected override void OnInitialized()
{
_editContext = new(_employee);
_editContext.OnFieldChanged = EditContext_OnFieldChanged;
messages = new ValidationMessageStore(_editContext);
}
private void EditContext_OnFieldChanged(object sender, FieldChangedEventArgs args)
{
if (args.FieldIdentifier.FieldName == nameof(_employee.Email))
{
bool emailExists = true;
if (emailExists)
{
messages.Clear(args.FieldIdentifier);
messages.Add(args.FieldIdentifier, "A user with this email address already exists.");
}
else
{
messages.Clear(args.FieldIdentifier);
}
}
_editContext.NotifyValidationStateChanged();
InvokeAsync(() => StateHasChanged());
}
private async Task HandleValidSubmit()
{
Console.WriteLine("Submitting...");
await Task.CompletedTask;
}
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
}