Form validation is straightforward when all rules can be evaluated locally. A required field can be checked immediately, a string can be validated for length, and a number can be compared with a known range.
Real applications often need more than that.
A registration form may need to check whether an email address already exists. An order form may need to verify product availability. A username field may need to check a remote service. A customer form may need to validate a code against a database.
These checks are asynchronous because they involve I/O.
That creates an important distinction in Blazor:
Synchronous validation
|
v
Validate immediately
Asynchronous validation
|
v
Call API/database
|
v
Wait for result
|
v
Update validation state
The validation operation should not turn into a blocking operation that makes the interface unresponsive or creates unnecessary requests.
This article explains a practical pattern for asynchronous form validation in Blazor, including EditForm, EditContext, custom validation messages, cancellation, race conditions, database checks, API validation, submit handling, and production considerations.
Why Asynchronous Validation Is Different
Consider a normal validation rule:
private bool IsValidAge(int age)
{
return age >= 18;
}
The method completes immediately.
A database validation is different:
var exists = await db.Users
.AnyAsync(x => x.Email == email);
The application has to wait for an external operation.
The same applies to an API:
var response = await Http.GetAsync(
$"api/users/check-email?email={email}");
The important part is await.
The asynchronous operation allows the application to yield while the I/O operation is in progress rather than synchronously blocking the thread.
However, simply putting await inside a validation method doesn't automatically make the validation architecture correct.
The application also needs to handle:
validation timing,
cancellation,
stale responses,
validation message updates,
duplicate requests,
submit behavior,
error handling,
and server-side validation.
Built-In Validation and Remote Validation Solve Different Problems
Blazor's standard validation components are useful for local rules.
For example:
<EditForm Model="Model"
OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<InputText @bind-Value="Model.Email" />
<ValidationMessage For="@(() => Model.Email)" />
<button type="submit">
Register
</button>
</EditForm>
A model might contain:
public class RegistrationModel
{
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
[MinLength(8)]
public string Password { get; set; } = string.Empty;
}
These rules are local.
The application doesn't need a database or API to determine whether the email has a valid format.
Remote validation is different:
Is the email syntactically valid?
|
v
Local validation
Does the email already exist?
|
v
Database/API validation
Keeping these responsibilities separate makes the form easier to reason about.
Using EditContext for Custom Validation
For asynchronous validation, EditContext is useful because it provides access to the form's validation lifecycle.
Create an EditContext:
<EditForm EditContext="_editContext"
OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<InputText @bind-Value="Model.Email" />
<ValidationMessage For="@(() => Model.Email)" />
<button type="submit">
Register
</button>
</EditForm>
Then:
@code {
private RegistrationModel Model = new();
private EditContext _editContext = default!;
protected override void OnInitialized()
{
_editContext = new EditContext(Model);
}
private async Task HandleValidSubmit()
{
// Submit the validated model.
}
}
The EditContext can also be used to trigger validation and manage custom validation state.
ValidationMessageStore
ValidationMessageStore is useful when validation messages don't come from standard data annotations.
Create one for the form:
private ValidationMessageStore _messageStore = default!;
Initialize it:
protected override void OnInitialized()
{
_editContext = new EditContext(Model);
_messageStore = new ValidationMessageStore(_editContext);
}
A custom message can then be added:
_messageStore.Add(
new FieldIdentifier(Model, nameof(Model.Email)),
"This email address is already registered.");
After changing the validation messages, notify the EditContext:
_editContext.NotifyValidationStateChanged();
This tells the form that its validation state has changed and the UI needs to update.
A Basic Async Email Validation Pattern
A simple implementation can validate an email after the user leaves the field.
<EditForm EditContext="_editContext">
<DataAnnotationsValidator />
<div>
<label>Email</label>
<InputText @bind-Value="Model.Email"
@onblur="ValidateEmailAsync" />
<ValidationMessage For="@(() => Model.Email)" />
</div>
<button type="submit">
Register
</button>
</EditForm>
The validation method can be:
private async Task ValidateEmailAsync(FocusEventArgs _)
{
_messageStore.Clear(
new FieldIdentifier(Model, nameof(Model.Email)));
if (string.IsNullOrWhiteSpace(Model.Email))
{
_editContext.NotifyValidationStateChanged();
return;
}
var exists = await UserService.EmailExistsAsync(
Model.Email);
if (exists)
{
_messageStore.Add(
new FieldIdentifier(
Model,
nameof(Model.Email)),
"This email address is already registered.");
}
_editContext.NotifyValidationStateChanged();
}
The important pattern is:
Clear previous message
|
v
Check local value
|
v
Perform async operation
|
v
Add message if necessary
|
v
Notify validation state changed
Don't Query the Database on Every Keystroke
One of the easiest mistakes is performing a database or API request every time the user changes the field.
For example:
<InputText @bind-Value="Model.Email"
@oninput="ValidateEmailAsync" />
If a user types:
b
ba
bai
baib
baibh
baibha
baibhav@
...
the application can generate a large number of requests.
This is inefficient and can create unnecessary load.
Instead, validate at a meaningful point such as:
blur,form submission,
an explicit "Check" action,
or after a debounce period.
For email uniqueness, validating after the user leaves the field is often sufficient.
Debouncing Async Validation
For search-like validation, a debounce can be useful.
The basic idea is:
User types
|
v
Wait briefly
|
+-- User types again → cancel previous wait
|
v
Call API
A cancellation token can help prevent obsolete requests from continuing unnecessarily.
For example:
private CancellationTokenSource? _validationCts;
private async Task ValidateEmailAsync(FocusEventArgs _)
{
_validationCts?.Cancel();
_validationCts?.Dispose();
_validationCts = new CancellationTokenSource();
var token = _validationCts.Token;
await ValidateEmailCoreAsync(token);
}
The validation operation can then accept the token:
private async Task ValidateEmailCoreAsync(
CancellationToken cancellationToken)
{
_messageStore.Clear(
new FieldIdentifier(Model, nameof(Model.Email)));
_editContext.NotifyValidationStateChanged();
if (string.IsNullOrWhiteSpace(Model.Email))
return;
try
{
var exists = await UserService.EmailExistsAsync(
Model.Email,
cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
if (exists)
{
_messageStore.Add(
new FieldIdentifier(
Model,
nameof(Model.Email)),
"This email address is already registered.");
}
_editContext.NotifyValidationStateChanged();
}
catch (OperationCanceledException)
{
// A newer validation request replaced this one.
}
}
Cancellation becomes especially useful when multiple asynchronous validations can overlap.
The Race Condition Problem
Suppose the user enters:
[email protected]
and validation request A starts.
Before it finishes, the user changes the value to:
[email protected]
and request B starts.
Now imagine:
Request A → slow
Request B → fast
Request B finishes first and says:
[email protected] is available
Then request A finishes and says:
[email protected] is already registered
If the application doesn't associate the result with the value that was actually checked, it could display the wrong validation message.
Cancellation helps, but the server-side operation should also be designed so that the latest state is authoritative.
A simple defensive check is:
var email = Model.Email;
var exists = await UserService.EmailExistsAsync(
email,
cancellationToken);
if (!string.Equals(
email,
Model.Email,
StringComparison.OrdinalIgnoreCase))
{
return;
}
The result is ignored if the user has already changed the field.
Async Validation with an API
A validation service should hide HTTP details from the component.
For example:
public interface IUserValidationService
{
Task<bool> EmailExistsAsync(
string email,
CancellationToken cancellationToken);
}
The component then calls:
var exists = await ValidationService.EmailExistsAsync(
Model.Email,
cancellationToken);
This keeps the UI component focused on UI state.
A service implementation might use:
public async Task<bool> EmailExistsAsync(
string email,
CancellationToken cancellationToken)
{
return await httpClient.GetFromJsonAsync<bool>(
$"api/users/email-exists?email={Uri.EscapeDataString(email)}",
cancellationToken);
}
The important production practice is to pass the cancellation token all the way to the HTTP operation.
Otherwise, cancelling the component's validation task may not actually cancel the underlying network operation.
Async Validation with Entity Framework Core
A server-side service can perform an efficient existence query:
public async Task<bool> EmailExistsAsync(
string email,
CancellationToken cancellationToken)
{
return await dbContext.Users
.AsNoTracking()
.AnyAsync(
user => user.Email == email,
cancellationToken);
}
AnyAsync is appropriate when the application only needs to know whether a matching record exists.
There is no reason to retrieve an entire user entity just to answer a yes/no question.
AsNoTracking is also appropriate for a read-only existence check because the returned entity isn't needed.
The database should also enforce uniqueness.
Application-level validation improves the user experience, but it should not be the only protection against duplicate data.
Validation Should Not Replace Server-Side Rules
This is a critical production rule.
Suppose the client checks:
Email available
Then another request registers the same email before the user's submission reaches the server.
Two requests can therefore pass the client-side availability check.
The database must still enforce the actual uniqueness constraint.
The architecture should be:
Client validation
|
| Better user experience
v
Server validation
|
| Authoritative business rules
v
Database constraints
|
| Final data integrity
v
Stored data
Client-side asynchronous validation is a convenience and early warning mechanism. It is not a replacement for authoritative server-side validation.
Handling Validation During Submit
A common design is to perform lightweight remote checks when the user leaves a field and then perform authoritative validation again during submission.
For example:
private async Task HandleValidSubmit()
{
IsSubmitting = true;
try
{
var result = await RegistrationService.RegisterAsync(
Model);
if (result.Success)
{
Navigation.NavigateTo("registration-complete");
return;
}
AddServerValidationErrors(result);
}
finally
{
IsSubmitting = false;
}
}
The server can return field-specific errors.
For example:
private void AddServerValidationErrors(
RegistrationResult result)
{
foreach (var error in result.Errors)
{
var field = new FieldIdentifier(
Model,
error.FieldName);
_messageStore.Add(field, error.Message);
}
_editContext.NotifyValidationStateChanged();
}
This gives the user a useful error message while preserving server-side authority.
Preventing Duplicate Submissions
Asynchronous validation and submission can overlap.
A simple guard prevents multiple submissions:
private bool IsSubmitting;
private async Task HandleValidSubmit()
{
if (IsSubmitting)
return;
IsSubmitting = true;
try
{
await RegistrationService.RegisterAsync(Model);
}
finally
{
IsSubmitting = false;
}
}
The button can reflect the state:
<button type="submit"
disabled="@IsSubmitting">
@(IsSubmitting ? "Creating Account..." : "Create Account")
</button>
This is not just a UI improvement. It reduces accidental duplicate requests.
Displaying an Async Validation State
It can be useful to show the user that a remote check is running.
For example:
@if (IsCheckingEmail)
{
<span>Checking email...</span>
}
The validation method can manage the state:
private bool IsCheckingEmail;
private async Task ValidateEmailAsync(FocusEventArgs _)
{
IsCheckingEmail = true;
try
{
await ValidateEmailCoreAsync(
CancellationToken.None);
}
finally
{
IsCheckingEmail = false;
}
}
For a good user experience, don't display a loading indicator for every tiny synchronous validation. Reserve it for operations that can actually take noticeable time.
Avoiding Excessive Validation Requests
A production application should decide when remote validation is worth performing.
Trigger | Advantages | Disadvantages |
|---|---|---|
Every keystroke | Immediate feedback | High request volume |
Debounced input | Responsive and controlled | More implementation complexity |
On blur | Simple and efficient | Feedback arrives later |
On submit | Lowest request volume | Less immediate feedback |
Explicit check | User controls request | Additional UI interaction |
For fields such as email uniqueness, onblur or submit-time validation is often more appropriate than querying on every keystroke.
For username availability, debounced validation can make sense because availability is often part of the interactive input experience.
Handling API Failures
Remote validation can fail.
The API may be unavailable, the request may time out, or the user may temporarily lose connectivity.
Don't automatically interpret an API failure as:
Email is invalid
Those are different conditions.
A better model is:
Valid
Invalid
Unable to validate
For example:
try
{
var exists = await ValidationService.EmailExistsAsync(
Model.Email,
cancellationToken);
if (exists)
{
AddEmailError("This email address is already registered.");
}
}
catch (HttpRequestException)
{
ValidationStatus =
"We couldn't check the email right now. Please try again.";
}
The form can then allow the final server-side submission to make the authoritative decision.
Security Considerations
Remote validation endpoints can expose information if designed carelessly.
For example, an unrestricted endpoint that answers:
Does this email exist?
can potentially be abused to enumerate registered accounts.
Consider whether the validation result itself is sensitive.
For authentication-related forms, avoid returning more information than the UI genuinely needs.
Rate limiting, authorization, generic error responses, and server-side controls may be appropriate depending on the application.
Never trust a client-side validation result for security decisions.
Common Mistakes
Making Synchronous Calls to Async APIs
Avoid patterns that block while waiting for asynchronous work.
Bad:
var exists = ValidationService
.EmailExistsAsync(Model.Email)
.Result;
Use:
var exists = await ValidationService
.EmailExistsAsync(Model.Email);
Blocking asynchronous operations can create responsiveness and scalability problems.
Validating on Every Keystroke
Don't send a database query for every character unless there is a deliberate debouncing strategy.
Ignoring Cancellation
If multiple validations can overlap, old requests may continue running after their results are no longer useful.
Ignoring Race Conditions
Always consider what happens when the user changes the field before the previous validation completes.
Relying Only on Client Validation
A client-side "available" result is not a guarantee that the value will still be available when the request is processed.
Clearing All Validation Messages
Avoid clearing unrelated validation messages when updating one field.
Prefer:
_messageStore.Clear(
new FieldIdentifier(Model, nameof(Model.Email)));
instead of clearing the entire store when only the email validation changed.
Treating Network Failure as Invalid Input
An API outage isn't the same thing as a validation failure.
Troubleshooting
Validation Message Doesn't Appear
Make sure the field has a corresponding ValidationMessage:
<ValidationMessage For="@(() => Model.Email)" />
Also make sure the application calls:
_editContext.NotifyValidationStateChanged();
after changing the custom validation state.
Old Error Remains After the Value Changes
Clear the field's previous messages before performing the new check:
_messageStore.Clear(
new FieldIdentifier(Model, nameof(Model.Email)));
Wrong Validation Result Appears
Check for overlapping asynchronous operations.
Use cancellation and verify that the result still corresponds to the current field value.
Validation Runs Too Often
Look for handlers attached to oninput or other high-frequency events.
Move the validation to blur, submit, or a debounced workflow where appropriate.
Submit Doesn't Trigger Expected Validation
Check whether the form is using the correct EditContext, model, and validation components.
For example:
<EditForm EditContext="_editContext"
OnValidSubmit="HandleValidSubmit">
API Failure Blocks the Entire Form
Distinguish validation failure from service availability.
If a non-critical availability check fails, the final server-side submission should still remain the authoritative decision.
Best Practices
Keep synchronous validation rules local whenever possible.
Use asynchronous validation only when external state is required.
Avoid database or API calls on every keystroke.
Use debouncing for high-frequency remote validation.
Use cancellation tokens for operations that can become obsolete.
Protect against stale asynchronous responses.
Update
ValidationMessageStoreonly for the affected field.Call
NotifyValidationStateChangedafter changing custom messages.Keep API and database logic inside services rather than UI components.
Enforce important business rules on the server.
Use database constraints for data integrity.
Prevent duplicate form submissions.
Treat network failures separately from validation failures.
Avoid exposing sensitive information through validation endpoints.
Test slow, failed, cancelled, and concurrent validation requests.
Advantages
Better User Experience
Users can receive feedback about server-side conditions without waiting until the final submission.
Reduced Invalid Submissions
Availability and business-rule checks can identify problems earlier.
Reusable Validation Services
Keeping API and database checks in services makes them easier to reuse and test.
Better Separation of Responsibilities
Local validation, remote validation, server-side business rules, and database constraints can each have a clear role.
Cancellation Can Reduce Wasted Work
Obsolete validation requests can be cancelled when a newer value replaces them.
Disadvantages and Trade-Offs
More Complex Than Local Validation
Asynchronous validation introduces concurrency, cancellation, and error-handling concerns.
Additional Network or Database Traffic
Every remote validation request consumes resources.
Potential Race Conditions
A value can change while a validation request is still running.
Remote Services Can Fail
Validation depends on infrastructure that may be temporarily unavailable.
Client Validation Is Not Authoritative
The server must still validate important business rules.
Production-Ready Pattern
A practical Blazor form can combine local validation, custom asynchronous validation, cancellation, and authoritative server submission.
<EditForm EditContext="_editContext"
OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<div>
<label>Email</label>
<InputText @bind-Value="Model.Email"
@onblur="ValidateEmailAsync" />
<ValidationMessage For="@(() => Model.Email)" />
@if (IsCheckingEmail)
{
<span>Checking email...</span>
}
</div>
<div>
<label>Password</label>
<InputText type="password"
@bind-Value="Model.Password" />
<ValidationMessage For="@(() => Model.Password)" />
</div>
<button type="submit"
disabled="@IsSubmitting">
@(IsSubmitting ? "Creating..." : "Create Account")
</button>
</EditForm>
@code {
private RegistrationModel Model = new();
private EditContext _editContext = default!;
private ValidationMessageStore _messageStore = default!;
private CancellationTokenSource? _validationCts;
private bool IsCheckingEmail;
private bool IsSubmitting;
protected override void OnInitialized()
{
_editContext = new EditContext(Model);
_messageStore = new ValidationMessageStore(_editContext);
}
private async Task ValidateEmailAsync(FocusEventArgs _)
{
_validationCts?.Cancel();
_validationCts?.Dispose();
_validationCts = new CancellationTokenSource();
var token = _validationCts.Token;
var email = Model.Email;
var field = new FieldIdentifier(
Model,
nameof(Model.Email));
_messageStore.Clear(field);
_editContext.NotifyValidationStateChanged();
if (string.IsNullOrWhiteSpace(email))
return;
IsCheckingEmail = true;
try
{
var exists =
await UserService.EmailExistsAsync(
email,
token);
token.ThrowIfCancellationRequested();
if (!string.Equals(
email,
Model.Email,
StringComparison.OrdinalIgnoreCase))
{
return;
}
if (exists)
{
_messageStore.Add(
field,
"This email address is already registered.");
}
_editContext.NotifyValidationStateChanged();
}
catch (OperationCanceledException)
{
}
finally
{
IsCheckingEmail = false;
}
}
private async Task HandleValidSubmit()
{
if (IsSubmitting)
return;
IsSubmitting = true;
try
{
await RegistrationService.RegisterAsync(Model);
}
finally
{
IsSubmitting = false;
}
}
}
This pattern isn't intended to be copied blindly into every form. The exact validation trigger and service behavior should depend on the field and the business requirement.
The important architecture is:
Blazor form
|
+--> Local validation
|
+--> Async availability/business check
| |
| +--> Cancellation
| +--> Stale-result protection
|
v
Server submission
|
+--> Authoritative validation
|
v
Database constraints
Final Takeaway
Asynchronous form validation is useful when a validation rule depends on information that isn't available inside the browser.
Blazor's EditForm, EditContext, and ValidationMessageStore provide the foundation for adding those checks without replacing the standard validation system.
The most important design principle is to separate responsibilities.
Use local validation for simple rules:
Required
Email format
Length
Range
Use asynchronous validation for external state:
Email already exists
Username available
Product currently available
Remote business rule
Then perform authoritative validation again on the server when the form is submitted.
For production applications, cancellation and stale-result protection are especially important. A slow request shouldn't overwrite the result of a newer request, and an unavailable validation service shouldn't automatically be treated as invalid user input.
A well-designed Blazor form therefore doesn't just ask whether a value is valid. It also considers where the validation comes from, how long it takes, what happens when the value changes, and which layer ultimately owns the business rule.

Join the conversation! Your thoughts help the community grow.