Blazor now has built-in form and validation. The default implementation uses data annotations and is a very similar experience to forms and validation in ASP.NET MVC applications. While it's great to have this included out of the box, there are other popular validation libraries available. And it would be great to be able to use them in place of data annotations if we so choose.

FluentValidation is a popular alternative to data annotations with over 12 million downloads. So I thought it would be interesting to see how much work it would take to integrate FluentValidation with Blazors forms and validation system. All the code from this post is available on GitHub.

If you just want to use able to use Fluent Validations in your Blazor app and you're not interesting in the details. I have developed the code from this post into a NuGet package called Blazored FluentValidation. You can just install it and get on with writing your code!

Getting Setup

I'm going to start with a new client-side Blazor project but you can use server-side Blazor if you prefer. The sample code contains both project types.

First, we need to install the FluentValidation library from NuGet. You can use the package manager in Visual Studio for this or if you prefer, you can use the dotnet CLI

dotnet add package FluentValidation

We're also going to need something to validate, so lets create a simple Person class. This will define the various fields that will be available on the form.

public class Person { public string Name { get; set; } public int Age { get; set; } public string EmailAddress { get; set; } }

That's it for the basics setup, next we will create the validation rules for our Person model.

Creating a model validator

FluentValidation works by creating a validator for each object you want to validate. In the validator you create validation rules for each property of the object using a fluent syntax.

Out of the box there are 20 predefined validators you can use covering most common validation checks such as not null, greater than or valid email. But if you need something that's not covered you can also write your own custom validators.

To write a model validator you must create a class that inherits from AbstractValidator<T> . You then add all the validation rules for the model in the constructor.

This is the validator code for our Person class.

public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleFor(p => p.Name).NotEmpty().WithMessage("You must enter a name"); RuleFor(p => p.Name).MaximumLength(50).WithMessage("Name cannot be longer than 50 characters"); RuleFor(p => p.Age).NotEmpty().WithMessage("Age must be greater than 0"); RuleFor(p => p.Age).LessThan(150).WithMessage("Age cannot be greater than 150"); RuleFor(p => p.EmailAddress).NotEmpty().WithMessage("You must enter a email address"); RuleFor(p => p.EmailAddress).EmailAddress().WithMessage("You must provide a valid email address"); } }

In this instance, there are no custom validators we're just using the built-in ones.

By using the NotEmpty validator, we're making all the properties required. We've set a maximum length for the Name property. We've said no age can be greater than 150. And that the email must be in a valid format.

The WithMessage method allows us to define what the error message should be if that particular rule is not met.

Building a form validator component

We've now got all the ground work done, FluentValidation is installed and we've setup a validator for our Person model. So how do we make this work with the forms and validation system in Blazor?

As it turns out we only need to build a couple of things. The first is a new validator component to use in place of the DataAnnotationsValidator which comes as default. Then we need to create an extension method for the EditContext which calls the validation logic from FluentValidation. Other than that, all the other forms components will just work without any modification. That's really cool.

We'll start by building the new validator component to replace the default data annotations one. The purpose of the validator component is to hook up the validation mechanism with the form. I really like this approach as you are able to change way you perform validation in your app by simply swapping in a new validator component.

This is what our FluentValidation validator component looks like.

public class FluentValidationValidator : ComponentBase { [CascadingParameter] EditContext CurrentEditContext { get; set; } protected override void OnInitialized() { if (CurrentEditContext == null) { throw new InvalidOperationException($"{nameof(FluentValidationValidator)} requires a cascading " + $"parameter of type {nameof(EditContext)}. For example, you can use {nameof(FluentValidationValidator)} " + $"inside an {nameof(EditForm)}."); } CurrentEditContext.AddFluentValidation(); } }

As you may have noticed, this is just a standard component inheriting from ComponentBase . It receives a CascadingParameter called CurrentEditContext which is passed down from the EditForm component. The component makes sure that this parameter is not null and then calls the AddFluentValidation method.

The AddFluentValidation method is the extension method we mentioned before and we will be looking at that in a moment. But as you can see, this is a really simple component, it's only job is to call that extension method on the EditContext .

Extending EditContext to use FluentValidation

The EditContext is the engine of forms validation in Blazor. It's what's responsible for executing validation as well as managing all the validation state.

Following the pattern used by the ASP.NET Core team for the default data annotations validation. We're going to create a new extension method for EditContext which will tell it how to use FluentValidation.

public static class EditContextFluentValidationExtensions { public static EditContext AddFluentValidation(this EditContext editContext) { if (editContext == null) { throw new ArgumentNullException(nameof(editContext)); } var messages = new ValidationMessageStore(editContext); editContext.OnValidationRequested += (sender, eventArgs) => ValidateModel((EditContext)sender, messages); editContext.OnFieldChanged += (sender, eventArgs) => ValidateField(editContext, messages, eventArgs.FieldIdentifier); return editContext; } private static void ValidateModel(EditContext editContext, ValidationMessageStore messages) { var validator = GetValidatorForModel(editContext.Model); var validationResults = validator.Validate(editContext.Model); messages.Clear(); foreach (var validationResult in validationResults.Errors) { messages.Add(editContext.Field(validationResult.PropertyName), validationResult.ErrorMessage); } editContext.NotifyValidationStateChanged(); } private static void ValidateField(EditContext editContext, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier) { var properties = new[] { fieldIdentifier.FieldName }; var context = new ValidationContext(fieldIdentifier.Model, new PropertyChain(), new MemberNameValidatorSelector(properties)); var validator = GetValidatorForModel(fieldIdentifier.Model); var validationResults = validator.Validate(context); messages.Clear(fieldIdentifier); messages.AddRange(fieldIdentifier, validationResults.Errors.Select(error => error.ErrorMessage)); editContext.NotifyValidationStateChanged(); } private static IValidator GetValidatorForModel(object model) { var abstractValidatorType = typeof(AbstractValidator<>).MakeGenericType(model.GetType()); var modelValidatorType = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(t => t.IsSubclassOf(abstractValidatorType)); var modelValidatorInstance = (IValidator)Activator.CreateInstance(modelValidatorType); return modelValidatorInstance; } }

There is a lot of code there so let's break it all down.

Hooking up events

The AddFluentValidation methods main job is to wire up the two events OnValidationRequested and OnFieldChanged .

editContext.OnValidationRequested += (sender, eventArgs) => ValidateModel((EditContext)sender, messages, validator); editContext.OnFieldChanged += (sender, eventArgs) => ValidateField(editContext, messages, eventArgs.FieldIdentifier);

OnValidationRequested is fired when validation is required for the whole model, for example, when attempting to submit the form. OnFieldChanged is fired when an individual fields value is changed.

The other important thing this method does is create a new ValidationMessageStore associated with the current EditContext .

var messages = new ValidationMessageStore(editContext);

A ValidationMessageStore is where all the validation messages for a forms fields are kept. This is used to decide if the form is valid or not based on if it contains any validation messages after validation has been run.

Validating the model

Next up we have the ValidateModel method. This method is invoked when the OnValidationRequest event is triggered. The main trigger for this event is the user attempting to submit a form so the whole model must be checked.

FluentValidation makes this really easy. All we have to do is call a method called Validate on the model validator. We get the model validator via the GetValidatorForModel method. We pass it the model we want a validator for and it uses a bit of reflection to create the correct instance. In our case, it will return an instance of the PersonValidator class we built earlier.

Once we have an instance of the validator. We can call the Validate method passing in the model we want to validate and it will give us a ValidationResult back.

var validator = GetValidatorForModel(editContext.Model); var validationResults = validator.Validate(editContext.Model);

As we're re-validating the form, we need to clear out any existing validation messages from the validation message store.

messages.Clear();

It's then just a case of looping over the errors collection on the validation result and recording any errors into the validation message store.

foreach (var validationResult in validationResults.Errors) { messages.Add(editContext.Field(validationResult.PropertyName), validationResult.ErrorMessage); }

Finally, we call NotifyValidationStateChanged on the EditContext which tells the context that there has been a change in the validation state.

Validating individual fields

The last method, ValidateField , is invoked from the OnFieldChanged event. This allows us to validate a field whenever it's been altered.

We start by creating a ValidationContext which allows us to specify the fields we want to validate.

var properties = new[] { fieldIdentifier.FieldName }; var context = new ValidationContext(fieldIdentifier.Model, new PropertyChain(), new MemberNameValidatorSelector(properties));

This is setup to only include the field which raised the event.

Just like before, we call GetValidatorForModel to get a validator instance then pass the validation context into the Validate method, except in this overload only the field we specified will be validated.

var validator = GetValidatorForModel(fieldIdentifier.Model); var validationResults = validator.Validate(context);

We clear any existing validation messages from the validation message store, except this time we only do it for the field we are validating.

messages.Clear(fieldIdentifier);

If there are any error messages in the validation result, they are added to the validation message store.

messages.AddRange(fieldIdentifier, validationResults.Errors.Select(error => error.ErrorMessage));

Before finally calling NotifyValidationStateChanged , as we did in the previous method.

And that's it! This is all we need to hook up FluentValidation to the build-in forms validation system in Blazor.

Sample Projects

If you want to see this code in action I've created a repo with a client-side Blazor and a server-side Blazor sample. The validation code in both projects is completely identical, everything work exactly the same regardless of project type.

Summary

That brings this post to a close. I was really surprised at just how simple it was to replace the default data annotations validation with FluentValidation. I think this is yet again another great example of the team providing options out of the box but not locking you in.

I do want to say that I'm by no means an expert on FluentValidation, the code above seems to work for most scenarios I've run it through. But if you find any issues please let me know in the comments.