Perform custom fluent validation in Blazor
In the previous article we created a custom validation attribute to validate Blazor's EditForm. If you developed ASP.NET Core MVC apps before, you might have used < a href="https://www.nuget.org/packages/fluentvalidation/" target="_blank">FlientValidation library for validation instead of using validation attributes. Can we use FluentValidation in Blazor? Unfortunately you can't do that out of the box. But using some custom technique you can integrate FluentValidation with Blazor's EditForm. In this article we will discuss one such approach.
First, open the same Blazor project that we created earlier. Since we want to use FluentValidation instead of validation attributes, the Employee model will look like this :
public class Employee
{
public int EmployeeID { get; set; }
public string FullName { get; set; }
public string Branch { get; set; }
public string Department { get; set; }
}
As you can see, we have removed all the data annotations added to the properties.
Next, install FluentValidation NuGet package using VS Code.
We need two classes to implement FluentValidation in Blazor -- EmployeeValidator and FluentValidationValidator.
The EmployeeValidator class performs fluent validations for our EditForm. This is how it looks :
public class EmployeeValidator :
AbstractValidator<Employee>
{
public EmployeeValidator()
{
RuleFor(e => e.FullName)
.NotEmpty().WithMessage("Enter Full Name");
RuleFor(e => e.Department)
.NotEmpty().WithMessage("Enter department name");
RuleFor(e => e.Department)
.Must((emp, dept) =>
IsValidBranchDepartment(emp.Branch, dept))
.WithMessage("Invalid department");
}
public bool IsValidBranchDepartment
(string branch, string dept)
{
// code here
}
}
The EmployeeValidator class inherits from AbstractValidator. The constructor contains several validation rules as indicated by RuleFor(), NotEmpty(), Must(), and WithMessage(). I won't go into the details of these methods here. You can read the FluentValidation documentation here.
Notice the code shown in bold letters. This is where our custom validation happens. The Must() call uses IsValidBranchDepartment() method to decide whether the combination of Branch and Department is correct or not. The IsValidBranchDepartment() method is shown below :
public bool IsValidBranchDepartment
(string branch, string dept)
{
if(string.IsNullOrWhiteSpace(branch)
|| string.IsNullOrWhiteSpace(dept))
{
return false;
}
string newValue = dept.ToString().ToLower();
switch (branch)
{
case "US":
if (newValue == "sales"
|| newValue == "marketing")
return true;
break;
case "UK":
if (newValue == "rd"
|| newValue == "admin")
return false;
break;
case "IN":
if (newValue == "manufacturing"
|| newValue == "packaging")
return false;
break;
}
return false;
}
The code inside this method should look familiar to you because we used it in the previous article. The code returns true if the validation is successful, else it returns false.
While using validation attributes we used <DataAnnotationsValidator /> to enable EditForm's data validation. In this case we are using FluentValidation and we need something similar to <DataAnnotationsValidator />.
The FluentValidationValidator class enables FluentValidation for our EditForm. We pass the EditContext as a [CascadingParameter] to FluentValidationValidator. And then wire the OnFieldChanged and OnValidationRequested events. The OnFieldChanged event is raised when a field value is changed. The OnValidationRequested event is raised when validation is requested.
We could have created FluentValidationValidator from scratch but to simplify our example I have taken help from Steve Sanderson's sample implementation available on GitHub here. For the sake of completeness the code inside FluentValidationValidator.cs is given below :
public class FluentValidationValidator<TValidator> :
ComponentBase where TValidator : IValidator, new()
{
private readonly static char[]
separators = new[] { '.', '[' };
private TValidator validator;
[CascadingParameter]
private EditContext EditContext { get; set; }
protected override void OnInitialized()
{
validator = new TValidator();
var messages = new ValidationMessageStore
(EditContext);
EditContext.OnFieldChanged +=
(sender, eventArgs)
=> ValidateModel
((EditContext)sender, messages);
EditContext.OnValidationRequested +=
(sender, eventArgs)
=> ValidateModel
((EditContext)sender, messages);
}
private void ValidateModel(EditContext editContext,
ValidationMessageStore messages)
{
var context = new
ValidationContext<object>(editContext.Model);
var validationResult =
validator.Validate(context);
messages.Clear();
foreach (var error in validationResult.Errors)
{
var fieldIdentifier =
ToFieldIdentifier(editContext, error.PropertyName);
messages.Add(fieldIdentifier, error.ErrorMessage);
}
editContext.NotifyValidationStateChanged();
}
private static FieldIdentifier
ToFieldIdentifier(EditContext editContext,
string propertyPath)
{
var obj = editContext.Model;
while (true)
{
var nextTokenEnd =
propertyPath.IndexOfAny(separators);
if (nextTokenEnd < 0)
{
return new FieldIdentifier
(obj, propertyPath);
}
var nextToken = propertyPath.
Substring(0, nextTokenEnd);
propertyPath = propertyPath.
Substring(nextTokenEnd + 1);
object newObj;
if (nextToken.EndsWith("]"))
{
nextToken = nextToken.Substring
(0, nextToken.Length - 1);
var prop = obj.GetType().
GetProperty("Item");
var indexerType = prop.GetIndexParameters()
[0].ParameterType;
var indexerValue = Convert.ChangeType
(nextToken, indexerType);
newObj = prop.GetValue(obj,
new object[] { indexerValue });
}
else
{
var prop = obj.GetType()
.GetProperty(nextToken);
if (prop == null)
{
throw new InvalidOperationException
($"Could not find property named
{nextToken} in object of type
{obj.GetType().FullName}.");
}
newObj = prop.GetValue(obj);
}
if (newObj == null)
{
return new FieldIdentifier
(obj, nextToken);
}
obj = newObj;
}
}
}
Now that we have EmployeeValidator and FluentValidationValidator classes ready, it's time to use them in the Blazor form.
Open Home.razor component and comment / remove the <DataAnnotationsValidator /> line. Add <FluentValidationValidator> component as shown below :
<EditForm Model="emp" OnValidSubmit="Save">
<FluentValidationValidator
TValidator="EmployeeValidator" />
@* other form fields here *@
<ValidationSummary/>
</EditForm>
As you can see, we passed EmployeeValidator to FluentValidationValidator component.
Run the app and check the validation in action :
That's it for now! Keep coding!!