FluentValidation with Blazor WebAssembly: Making Validation Less Painful

Not too long ago, I wrote a blog post titled Getting Started with FluentValidation, where I focused more on how to use FluentValidation in the backend, particularly with ASP.NET Core Web APIs. That article was all about validating requests before they hit your service logic. And that’s an important part of building secure and reliable APIs.

But here’s the thing: validation doesn’t only belong in the backend.

In many frontend-heavy applications, especially those built with Blazor WebAssembly, we also need real-time, client-side validation. It improves user experience, saves round-trips to the server, and keeps your forms clean and reactive.

That’s what this post is about.

We’ll go through how to integrate FluentValidation into a Blazor WebAssembly (WASM) project so you can apply the same expressive, powerful validation rules directly in your frontend. I’ll be sharing how I did this in one of our projects, why I chose FluentValidation over DataAnnotations, and how it made my forms easier to manage and more robust.

If you’ve already seen how powerful FluentValidation is on the backend, now let’s explore how to make the most of it on the frontend.


Why FluentValidation?

In many of my past projects, I often found myself writing repetitive and messy validation code. DataAnnotations is great for simple stuff, but once you need:

  • Conditional logic
  • Custom rules
  • Reuse of validation logic
  • Separation of concerns

…it gets ugly fast.

With FluentValidation, you write expressive rules like this:

C#
RuleFor(x => x.Name)
    .NotEmpty().WithMessage("Name is required")
    .MinimumLength(3).WithMessage("Name must be at least 3 characters long");

Isn’t that cleaner?


Setting Up the Project

Let’s start with a clean Blazor WASM project (assuming .NET 9 SDK is installed).

1. Create the Project
Bash
dotnet new blazorwasm -n FluentValidationDemo
cd FluentValidationDemo
2. Install FluentValidation Packages

FluentValidation does not provide built-in integration with Blazor, so we’ll use the Blazored.FluentValidation package instead.

Run this command to install:

Bash
dotnet add package Blazored.FluentValidation

If you’re using shared components between Server and WASM (like with a shared project), you might need to install these in that shared project too.


Basic Form with Validation

Let’s say you’re building a simple registration form:

Create the Model and Validator

Models/LoginFormData.cs

C#
using System;
using FluentValidation;

public class LoginFormData
{
    public string Username { get; set; }
    public string Password { get; set; }
}

public class LoginFormDataValidator : AbstractValidator<LoginFormData>
{
    public LoginFormDataValidator()
    {
        RuleFor(user => user.Username)
            .NotEmpty().WithMessage("Username is required.");

        RuleFor(user => user.Password)
            .NotEmpty().WithMessage("Password is required.")
            .Matches(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{12,32}$")
            .WithMessage("Password must be 12 to 32 characters long and include an uppercase letter, a lowercase letter, a number, and a special character.");
    }
}

In the snippet above, we define a validator class for LoginFormData using FluentValidation. Let’s walk through each part:

C#
public class LoginFormData
{
    public string Username { get; set; }
    public string Password { get; set; }
}

This is our model class. It represents the data entered by a user on a login form, with two properties: Username and Password.

C#
public class LoginFormDataValidator : AbstractValidator<LoginFormData>
{
    public LoginFormDataValidator()
    {
        RuleFor(user => user.Username)
            .NotEmpty().WithMessage("Username is required.");

        RuleFor(user => user.Password)
            .NotEmpty().WithMessage("Password is required.")
            .Matches(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{12,32}$")
            .WithMessage("Password must be 12 to 32 characters long and include an uppercase letter, a lowercase letter, a number, and a special character.");
    }
}

This part defines the validator. We inherit from AbstractValidator<T>, where T is our model (LoginFormData).

Inside the constructor, we define two validation rules:

  1. Username Rule
    • NotEmpty(): Ensures the username field is not left blank.
    • WithMessage(…): Provides a user-friendly error message if the rule fails.
  2. Password Rule
    • NotEmpty(): The password field must also not be empty.
    • Matches(…): Uses a regular expression to enforce a strong password policy:
    • At least one lowercase letter
    • At least one uppercase letter
    • At least one digit
    • At least one special character (e.g., @, $, !, etc.)
    • Between 12 and 32 characters in length
    • WithMessage(…): Displays a clear message if the password doesn’t meet the criteria.

This kind of validator is useful for enforcing front-end validation logic in Blazor applications, helping catch invalid inputs before they reach your backend.

Displaying the Form in Blazor

Let’s integrate this into a Blazor form:

Pages/Register.razor

C#
@using Blazored.FluentValidation

@page "/signin"

<div class="card card-sign">
    <div class="card-header">
        <h3 class="card-title">Sign In</h3>
        <p class="card-text">Welcome back! Please sign in to continue.</p>
    </div>
    <div class="card-body">
        <EditForm Model="@formData" OnValidSubmit="@HandleValidSubmit">
            <FluentValidationValidator />
            <ValidationSummary />
            <div class="mb-4">
                <label class="form-label">Employee ID</label>
                <InputText @bind-value="formData.Username" class="form-control" maxlength="20" />
                <ValidationMessage For="@(() => formData.Username)" />
            </div>
            <div class="mb-4">
                <label class="form-label d-flex justify-content-between">Password <a href="#">Forgot password?</a></label>
                <InputText @bind-value="formData.Password" class="form-control" maxlength="50" type="password" />
                <ValidationMessage For="@(() => formData.Username)" />
            </div>
            <button type="submit" class="btn btn-primary btn-sign">Sign In</button>
        </EditForm>
    </div>
</div>

@code {
    private LoginFormData formData = new();

    private void HandleValidSubmit()
    {
        Console.WriteLine($"Submitted Password: {formData.Password}");
    }
}


Real-World Tip from My Experience

In one of our projects, we had a form where a user had to enter either a Tax ID or a Business Permit, but not both. With FluentValidation, this was super easy to express using .When():

C#
RuleFor(x => x.TaxId)
    .NotEmpty().When(x => string.IsNullOrWhiteSpace(x.BusinessPermit))
    .WithMessage("Either Tax ID or Business Permit is required.");

Try doing that in DataAnnotations, it gets complicated very quickly 😅


Caveats in WASM


When You Might Still Use DataAnnotations

Even though I prefer FluentValidation now, I still use DataAnnotations for:

  • Quick prototypes
  • Very simple models
  • External DTOs you don’t control

You don’t have to choose one and ignore the other, you can use both, depending on what fits best.


Final Thoughts

Switching to FluentValidation made our forms cleaner, more maintainable, and a lot easier to test. Especially on larger projects with many dynamic forms, it helps to keep validation logic out of the UI and in a separate layer.

If you’re building apps with Blazor WebAssembly and find yourself repeating validation logic or hitting the limitations of DataAnnotations, try FluentValidation. You might find yourself wondering why you didn’t use it sooner.


References

Assi Arai
Assi Arai
Articles: 31