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
SupplyParameterFromForm
attribute on thePerson
property to supply thePerson
model from the form. - We will use the
SupplyParameterFromQuery
attribute on theN
,B
, andD
properties to supply the query string values to thePerson
model in theOnParametersSet
method. - We will use the
EnableFluentValidations
extension method to enable FluentValidation’s validation for our form in theOnInitialized
method. - On
HTTP GET
requests, we will validate and assign the query string values to thePerson
model in theOnParametersSet
method 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.