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!)

Security is complicated…

One thing to watch here is that anything which indicates whether an account already exists can be exploited.

If someone’s trying to hack your site and has lists of emails/passwords, they can hammer your signup form with all the email addresses they have, until they find one which warns them that user already exists.

From here they’re free to try your login form with all the accounts they know definitely exist, focusing just on the password part of the puzzle!

It’s beyond the scope of this article, but limiting how many times someone can run this check in a given timeframe could help mitigate this and, as always, it’s a question of judgement how much of a risk this actually is for your site…

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.

Blazor client and server project can share models

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.

Validation error because email is empty

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.

Shared validator used in client and server projects

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!

Better to be safe…

Client-side validation should catch any issues but it’s generally bad practice to rely on it, for a few reasons including:

  • The request may have been tampered with
  • Something may have changed whilst the screen was open on the user’s computer (before they eventually submitted the form)
  • Someone may be attempting to hack your API, in which case client-side validation is almost certainly useless!

So how can we support both “modes” of running our validation rules?

  1. Near-instant checking for duplicates as the user interacts with our form (client)
  2. 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.

Entering existing email shows near-instant validation error

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!

Next up

Render Blazor WASM components in your existing MVC/Razor Pages applications
You can render individual Blazor WASM components in your existing Razor Pages (or MVC) Core app.
Prerendering your Blazor WASM application with .NET 5 (part 2 - solving the missing HttpClient problem)
If you refresh your prerendered Blazor WASM site today you’re in for a big surprise…
Prerendering your Blazor WASM application with .NET 5 (part 1)
With a small tweak you can have your site appear near instantly so your users aren’t left hanging while the rest of your application downloads