Custom validation logic on client AND server with Blazor?
July 7, 2020 · 9 minute read · Tags: blazor
Blazor has some pretty handy built-in support for validating your forms.
You can use the DataAnnotations
validator and decorate your model classes with attributes like [Required]
or go a step further and wire up more involved validation rules using tools like FluentValidation
.
For the most part this “just works”…
But…
How about those times when you need something a touch more complicated, like checking if an email address is already in use?
Turns out there is a way to create one set of validation rules which can run seamslessly on both the client and server…
Yep, you can have your cake AND eat it too :-)
Instant validation - The requirement
Let’s say you want to include an email input on your form and check, as soon as the user enters their email, if that email is already in use.
This sounds like a good way to catch any problems early and save users wasting time (perhaps they’ve already signed up and simply forgot they had an account, I mean we’ve all done it!)
The good news is, it’s entirely possible to execute instant validation rules with Blazor, using the in-built validator support and the extremely handy FluentValidation library.
The challenging part is figuring out how to write a validator which checks the database (for an existing user with that email) which will work on the client (when Blazor WASM is running in the browser) but also on the server (so even if a duplicate somehow gets past client-side validation it still gets caught by your API).
Ideally we’d want to write one validator and have it run everywhere.
So, can it be done?
Blazor WASM can share models between client and server
First up, here’s the architecture of a standard Blazor WASM application.
One of Blazor’s compelling advantages is that you can create C# classes in your application’s shared project and use them in both your Blazor WASM project (running in the browser) and the API project (running on the server).
If you add a reference to FluentValidation
in your .Shared projects, you can also create validators for your shared models. Here’s a simple example.
Account\SignUp.cs
public class SignUp
{
public string Email { get; set; }
public bool SubscribeToNewsletter { get; set; }
}
public class SignUpValidator : AbstractValidator<SignUp>
{
public SignUpValidator()
{
RuleFor(x => x.Email).NotEmpty();
}
}
We have a simple C# model to represent the SignUp form, and a validator.
The validator comprises a single rule; that the value of email cannot be null or empty (and is therefore required).
Now we have a rule, but how do we make Blazor use it?
For this we can use Chris Sainty’s exceptionally useful Blazored.FluentValidation package…
Over in the .Client project, if we add a reference to the package…
Install-Package Blazored.FluentValidation
.. we’re able to use the <FluentValidationValidator />
component in our forms.
Markup
@using SharedValidationExample.Shared.Account
@using Blazored.FluentValidation
<EditForm Model="SignUpModel" OnValidSubmit="HandleValidSubmit">
<FluentValidationValidator />
<label for="email">Your Email:</label>
<InputText id="email" @bind-Value="SignUpModel.Email"/>
<button type="submit">Register</button>
<ValidationSummary />
</EditForm>
Code
@code
{
protected SignUp SignUpModel { get; set; } = new SignUp();
protected void HandleValidSubmit()
{
Console.WriteLine("Valid submit!");
}
}
Run this in the browser now and you’ll see errors if you try to leave the email blank.
But what about that “realtime” duplicate email checking?
Now for our next trick; implementing a rule to check if the entered email address is unique.
The challenging part here is that this rule could run on either the client or the server.
Generally speaking, validation in Blazor WASM runs in the browser.
When someone launches your application, your validation rules are shipped to their browser via a .dll file.
Your Blazor application (running in the browser) then applies these rules to values entered into the form.
This works very nicely most of the time because your users get near-instant validation errors as they’re entering data into a form.
However, because the rules are applied in the browser we have no direct access to the database.
If we want to check for duplicate email addresses in the database our only realistic option is to call our API and let it do the checking for us.
This will provide fast feedback to our users but we should still check the validation rules again at the point of accepting the submitted form, as client-side validation is pretty weak and can’t be trusted!
So how can we support both “modes” of running our validation rules?
- Near-instant checking for duplicates as the user interacts with our form (client)
- Another check of the rules when the form is submitted (server)
Turns out we need an interface with two implementations (thank you Chris Sainty for suggesting this approach!)
Here’s a modified version of the SignUpValidator
which supports checking for duplicate emails.
.Shared\Account\SignUp.cs
public class SignUpValidator : AbstractValidator<SignUp>
{
private readonly IValidateEmail _validateEmail;
public SignUpValidator(IValidateEmail validateEmail)
{
_validateEmail = validateEmail;
RuleFor(x => x.Email)
.NotEmpty()
.MustAsync(BeUnique).WithMessage("Email already registered");
}
private async Task<bool> BeUnique(string email, CancellationToken token)
{
return await _validateEmail.CheckIfUnique(email, token);
}
}
Now SignUpValidator
takes an instance of IValidateEmail
and calls it (via the BeUnique
method) to check the entered email address.
The IValidateEmail
interface is straightforward enough.
IValidateEmail.cs
public interface IValidateEmail
{
Task<bool> CheckIfUnique(string email, CancellationToken cancellationToken);
}
It should return true
if the email is unique or false
if it’s a duplicate (email already exists in the database).
Check on the client
Now we need to create and wire up our two implementations of IValidateEmail
.
First, on the client we’ll need an implementation of IValidateEmail
which performs the check via an HTTP call to the API.
This implementation needs to live in the .Client project.
public class ValidateEmail : IValidateEmail
{
private readonly HttpClient _http;
public ValidateEmail(HttpClient http)
{
_http = http;
}
public async Task<bool> CheckIfUnique(string email, CancellationToken token)
{
var requestUri = $"account?email={email}";
var existingAccounts = await
_http.GetFromJsonAsync<Search.Model>(requestUri, token);
return !existingAccounts.Accounts.Any();
}
}
This makes a simple HTTP Get to our API to search for any existing accounts (by email address) then returns a bool indicating whether we found any or not.
Before we can test this we need to tell Blazor to use this implementation when asked for an instance of IValidateEmail
. We can set that up in Program.cs, in the Main
method.
builder.Services.AddTransient<IValidateEmail, ValidateEmail>();
Now (and assuming there is something running at /account to do the actual checking) we’ll get validation errors when we enter an email which already exists.
Pretty neat huh?
When the Email input loses focus:
- The validation rule is checked
IValidateEmail.CheckIfUnique()
is invoked- An HTTP GET is made to the API to see if the email is a duplicate
- Blazor shows errors against the Email input if the email already exists
Check on the server
Almost done, but now we need the same validation rules to run on the server.
At the moment we’d run into errors because the server has no corresponding implementation of IValidateEmail
.
Let’s create one of those now in the .Server project.
public class ValidateEmail : IValidateEmail
{
private readonly FakeSearch _fakeSearch;
public ValidateEmail(FakeSearch fakeSearch)
{
_fakeSearch = fakeSearch;
}
public async Task<bool> CheckIfUnique(string email, CancellationToken cancellationToken)
{
var existingAccounts = _fakeSearch.Handle(new Search.Query {Email = email});
return !existingAccounts.Accounts.Any();
}
}
This actually calls the exact same FakeSearch
class as our API, so whether the client invokes this via the API or the server invokes it the same logic is executed in both cases.
We can wire this up in the ConfigureServices
method in Startup.cs (in the .Server project).
services.AddScoped<IValidateEmail, ValidateEmail>();
And finally, we just need to run these validation rules when we’re actually attempting to create the account.
Happily FluentValidation makes this nice and simple.
So long as we reference the FluentValidation.AspNetCore
package in our .Server project, we can configure our application to use it in Startup.cs.
services.AddControllersWithViews()
.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<SignUpValidator>());
We’ve told FluentValidator to look for validators in the assembly containing our SignUpValidator
.
Now we can check ModelState
in our controller actions and be confident that FluentValidation
has our back!
[HttpPost]
public async Task<IActionResult> Create([FromBody] SignUp signUpRequest)
{
if (!ModelState.IsValid)
return BadRequest();
// save here
return Ok();
}
Note we’re using the same SignUp
model here as we were in the client.
That’s the magic, because now Fluent Validation will validate the incoming signUpRequest
model against the very same validator we used in the browser.
It will locate the (server-side) instance of ValidateEmail
and use that to check that the email is still OK.
Any issues with validation and we’ll get errors on ModelState
and can return a BadRequest
status code, otherwise all is well so we can go ahead and create the account in the database.
Finally, for reference, here’s the code which makes the HTTP call to create the new account (in our component on the client).
protected async Task HandleValidSubmit()
{
var result = await Http.PostAsJsonAsync("account", SignUpModel);
if (result.StatusCode == HttpStatusCode.BadRequest)
{
// oops, server-side validation found an issue...
}
}
In Summary
You CAN validate your models on both the client and server using Fluent Validation.
If you need to employ “dynamic” rules (such as checking whether the email has already been used) you can use an interface in the validator.
This leaves you free to implement two versions of the custom validation logic, one which operates via HTTP calls (on the client) and another which executes the query directly (on the server).
Fluent Validation can then operate in the browser to provide near-instant feedback to the user, and again on the server when the request is eventually posted to the API (just to ensure everything is still valid).
Check out the source code for this example.
Many thanks again to Chris Sainty for suggesting this approach in the first place. You should definitely check out his blog too!