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.