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?

Unleash Blazor's Potential

Blazor promises to make it much easier (and faster) to build modern, responsive web applications, using the tools you already know and understand.

Subscribe to my Practical ASP.NET Blazor newsletter and get instant access to the vault.

In there you'll find step-by-step tutorials, source code and videos to help you get up and running using Blazor's component model.

I respect your email privacy. Unsubscribe with one click.

    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!

    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!

    Join the Practical ASP.NET Newsletter

    Ship better Blazor apps, faster. One practical tip every Tuesday.

    I respect your email privacy. Unsubscribe with one click.

      Next up

      Finally! Improved Blazor Server reconnection UX
      .NET 9 changes how your Blazor Server app behaves when server connection is lost
      .NET 9 improves JavaScript module importing for Blazor
      .NET 9 ensures your users always get the latest version of your JS modules
      How to use .NET 9 to ensure users always get the latest version of your stylesheets
      .NET 9 changes how static files are served, and it solves a long-standing problem