FluentValidation with Blazor static server-side rendering (static SSR) on HTTP GET

Let’s see how to use FluentValidation with Blazor static server-side rendering (SSR) on HTTP GET requests.
Traditionally, we use FluentValidation (or DataAnnotations) to validate a form on HTTP POST requests. However, we can also use it to validate a form on HTTP GET requests. This is useful when we want to validate a model created by elements provided from the query string.

Prerequisites
Add the FluentValidation NuGet package to the Blazor project.
dotnet add package FluentValidation
The Model
Once we’ve set up FluentValidation, the next step is to create a simple model with three properties: Name and DoB and DoD.
public record Person
{
public string? Name { get; set; }
public string? DoB { get; set; }
public string? DoD { get; set; }
public DateOnly DoBAsDateOnly => DateOnly.TryParse(DoB, out var date) ? date : default;
public DateOnly DoDAsDateOnly => DateOnly.TryParse(DoD, out var date) ? date : default;
}
Notice that we have two additional properties: DoBAsDateOnly and DoDAsDateOnly. These properties are used to convert the DoB and DoD properties to DateOnly type. I wanted to have complete control over data validation and not get any errors from the type bindings.
The Validator
Then, let’s create a simple validator for the Person model. We will validate the Name, DoB, and DoD properties.
public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
// ===================================================================
// Name
// ===================================================================
RuleFor(x => x.Name).NotEmpty();
// ===================================================================
// Date of birth
// ===================================================================
RuleFor(x => x.DoB)
.NotEmpty().WithName("Date of birth");
RuleFor(x => x.DoB)
.Must(x => DateOnly.TryParse(x, out _))
.WithMessage("Invalid date format for 'Date of birth'.")
.When(x => !string.IsNullOrWhiteSpace(x.DoB));
RuleFor(x => x.DoB)
.Must(x =>
{
_ = DateOnly.TryParse(x, out var date);
return date.DayNumber < DateOnly.FromDateTime(DateTime.Now).DayNumber;
})
.WithMessage("The 'Date of birth' date can't be in the future.")
.When(x => !string.IsNullOrWhiteSpace(x.DoB))
.When(x => DateOnly.TryParse(x.DoB, out _));
// ===================================================================
// Date of death
// ===================================================================
RuleFor(x => x.DoD)
.NotEmpty().WithName("Date of death");
RuleFor(x => x.DoD)
.Must(x => DateOnly.TryParse(x, out _))
.WithMessage("Invalid date format for 'Date of death'.")
.When(x => !string.IsNullOrWhiteSpace(x.DoD));
RuleFor(x => x.DoD)
.Must(x =>
{
_ = DateOnly.TryParse(x, out var date);
return date.DayNumber < DateOnly.FromDateTime(DateTime.Now).DayNumber;
})
.WithMessage("The 'Date of death' can't be in the future.")
.When(x => !string.IsNullOrWhiteSpace(x.DoD))
.When(x => DateOnly.TryParse(x.DoD, out _));
RuleFor(x => x.DoD)
.Must((x, _) => x.DoDAsDateOnly.DayNumber > x.DoDAsDateOnly.DayNumber)
.WithMessage("The 'Date of death' needs to be greater than the 'Date of birth'.")
.When(x => DateOnly.TryParse(x.DoB, out _) && DateOnly.TryParse(x.DoD, out _));
}
}
The Razor Component
Now for the simple form with our three input fields: Name, DoB, and DoD.
We’ll use the Enhance attribute on the EditForm component to enable enhance navigation for our form POST requests saving us a full page reload.
We’ll also need to supply the EditContext to the EditForm component. We will use the EnableFluentValidations extension method to enable FluentValidation’s validation for our form in the OnInitialized method.
The <ValidationSummary /> component will display the validation errors for the form and the <ValidationMessage /> components will display the validation errors for each input field.
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<EditForm OnValidSubmit="Submit" FormName="PersonForm" EditContext="_editContext" Enhance>
<ValidationSummary />
<div class="mb-3">
<label for="@($"{nameof(Person)}.{nameof(Person.Name)}")" class="required form-label">Name <strong class="required">(required)</strong></label>
<ValidationMessage For="@(() => Person.Name)" />
<InputText @bind-Value="Person.Name" id="@($"{nameof(Person)}.{nameof(Person.Name)}")" class="form-control" />
</div>
<div class="mb-3">
<label for="@($"{nameof(Person)}.{nameof(Person.DoB)}")" class="required form-label">Date of birth <strong class="required">(required)</strong></label>
<ValidationMessage For="@(() => Person.DoB)" />
<InputText type="date" @bind-Value="Person.DoB" id="@($"{nameof(Person)}.{nameof(Person.DoB)}")" class="form-control" placeholder="yyyy-mm-dd" />
</div>
<div class="mb-3">
<label for="@($"{nameof(Person)}.{nameof(Person.DoD)}")" class="required form-label">Date of death <strong class="required">(required)</strong></label>
<ValidationMessage For="@(() => Person.DoD)" />
<InputText type="date" @bind-Value="Person.DoD" id="@($"{nameof(Person)}.{nameof(Person.DoD)}")" class="form-control" placeholder="yyyy-mm-dd" />
</div>
<button type="submit" class="btn btn-primary">Search</button>
</EditForm>
The Code-Behind
- In the code behind, we’ll use the
SupplyParameterFromFormattribute on thePersonproperty to supply thePersonmodel from the form. - We will use the
SupplyParameterFromQueryattribute on theN,B, andDproperties to supply the query string values to thePersonmodel in theOnParametersSetmethod. - We will use the
EnableFluentValidationsextension method to enable FluentValidation’s validation for our form in theOnInitializedmethod. - On
HTTP GETrequests, we will validate and assign the query string values to thePersonmodel in theOnParametersSetmethod and then, if any of the fields are not empty, validate the form using theEditContext.Validate();method.
public partial class Home
{
private EditContext? _editContext;
[SupplyParameterFromForm]
public Person Person { get; set; } = new();
[SupplyParameterFromQuery(Name = "n")]
public string? Name { get; set; }
[SupplyParameterFromQuery(Name = "b")]
public string? DoB { get; set; }
[SupplyParameterFromQuery(Name = "d")]
public string? DoD { get; set; }
[Inject]
public IServiceProvider ServiceProvider { get; set; } = null!;
[Inject]
public ILogger<Home> Logger { get; set; } = null!;
[Inject]
public NavigationManager NavigationManager { get; set; } = null!;
[Inject]
public IHttpContextAccessor HttpContextAccessor { get; set; } = null!;
protected override void OnInitialized()
{
_editContext = new(Person);
_editContext.EnableFluentValidations<PersonValidator>(ServiceProvider);
base.OnInitialized();
}
protected override void OnParametersSet()
{
if (HttpContextAccessor.HttpContext!.Request.Method != "GET")
{
return;
}
// Assign the query string values to the Person model
Person.Name = Name;
Person.DoB = DoB;
Person.DoD = DoD;
// If any of the fields are not empty, validate the form
if (new[] { Name, DoB, DoD }.Any(x => !string.IsNullOrWhiteSpace(x)))
{
_editContext!.Validate();
}
}
private void Submit()
{
Logger.LogInformation("Submit: {Name}, {DoB} {DoD}", Person.Name, Person.DoB, Person.DoD);
var qs = QueryString.Create(new Dictionary<string, string?>
{
["n"] = Person.Name,
["b"] = Person.DoB,
["d"] = Person.DoD
});
// Redirect to the same page with the query string (PRG pattern)
NavigationManager.NavigateTo($"/{qs}");
}
}
The FluentValidationsEventSubscriptions class
Here is the class called FluentValidationsEventSubscriptions that will handle the OnFieldChanged and OnValidationRequested events for the EditContext class.
public sealed class FluentValidationsEventSubscriptions<TValidator> : IDisposable
where TValidator : IValidator
{
private readonly EditContext _editContext;
private readonly IServiceProvider? _serviceProvider;
private readonly ValidationMessageStore _messages;
private readonly IValidator _validator;
public FluentValidationsEventSubscriptions(EditContext editContext, IServiceProvider serviceProvider)
{
_editContext = editContext ?? throw new ArgumentNullException(nameof(editContext));
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
_messages = new ValidationMessageStore(_editContext);
_editContext.OnFieldChanged += OnFieldChanged;
_editContext.OnValidationRequested += OnValidationRequested;
_validator = _serviceProvider.GetRequiredService<TValidator>();
}
private void OnFieldChanged(object? sender, FieldChangedEventArgs args)
{
var fieldIdentifier = args.FieldIdentifier;
_messages.Clear(fieldIdentifier);
var propertiesToValidate = new string[] { fieldIdentifier.FieldName };
var fluentValidationContext =
new ValidationContext<object>(
instanceToValidate: fieldIdentifier.Model,
propertyChain: new FluentValidation.Internal.PropertyChain(),
validatorSelector: new FluentValidation.Internal.MemberNameValidatorSelector(propertiesToValidate)
);
var validationResult = _validator.Validate(fluentValidationContext);
AddValidationResult(fieldIdentifier.Model, validationResult);
}
private void OnValidationRequested(object? sender, ValidationRequestedEventArgs args)
{
_messages.Clear();
var validationContext = new ValidationContext<object>(_editContext.Model);
var validationResult = _validator.Validate(validationContext);
AddValidationResult(_editContext.Model, validationResult);
}
public void Dispose()
{
_messages.Clear();
_editContext.OnFieldChanged -= OnFieldChanged;
_editContext.OnValidationRequested -= OnValidationRequested;
_editContext.NotifyValidationStateChanged();
}
private void AddValidationResult(object model, ValidationResult validationResult)
{
foreach (ValidationFailure error in validationResult.Errors)
{
var fieldIdentifier = new FieldIdentifier(model, error.PropertyName);
_messages.Add(fieldIdentifier, error.ErrorMessage);
}
_editContext.NotifyValidationStateChanged();
}
}
The EnableFluentValidations extension method
Let’s create an extension method called EnableFluentValidationsValidation that will enable FluentValidation validation on our form.
public static class EditContextFluentValidationsExtensions
{
/// <summary>
/// Enables FluentValidation validation support for the <see cref="EditContext"/>.
/// </summary>
/// <typeparam name="TValidator">The validator type that implements <see cref="IValidator"/>.</typeparam>
/// <param name="editContext">The <see cref="EditContext"/>.</param>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to be used in the <see cref="ValidationContext"/>.</param>
/// <returns>A disposable object whose disposal will remove FluentValidations validation support from the <see cref="EditContext"/>.</returns>
public static IDisposable EnableFluentValidations<TValidator>(this EditContext editContext, IServiceProvider serviceProvider)
where TValidator : IValidator
{
ArgumentNullException.ThrowIfNull(serviceProvider);
return new FluentValidationsEventSubscriptions<TValidator>(editContext, serviceProvider);
}
}
Remark that we’ll need to pass the extension method an instance of IServiceProvider so that it can resolve the IValidator instance from the generic type parameter.
_editContext.EnableFluentValidations<PersonValidator>(ServiceProvider);
DI Services Registration
Register the IHttpContextAccessor service and any of your validators in the Program class.
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<PersonValidator>();
That’s it! We can now use FluentValidation with Blazor static server-side rendering (SSR) on HTTP GET and HTTP POST requests.
Conclusion
Using FluentValidation with Blazor static server-side rendering on HTTP GET requests offers a significant shift in how we traditionally handle form validation, normally only on HTTP POST requests. It can provides a more flexible way to validate a model created from query string elements. This technique is particularly useful in scenarios where you need to validate data before it’s passed into the Blazor component render method.
As we’ve seen, setting up FluentValidation for this purpose is a straightforward process that can be easily integrated into your existing Blazor projects. It’s another tool in your .NET toolbox that can help make your applications more robust and user-friendly.
