Search code examples
c#blazorblazor-server-sidefluentvalidation

Unable to validate just a single property with Blazored Fluent Validation


In a vanilla .NET 6 Blazor Server project, I am using Blazored.FluentValidation 2.1.0. When trying to validate a single property, as documented in the FluentValidation docs, it is triggering the validation for the whole form.

I've tried to create a .NET 6 MVC project and used FluentValidation 11.5.2 directly. As expected, only the provided property is being validated when I call Validate() by specifying the property in the IncludeProperties() method. Is there something wrong that I'm doing or missing from my side on Blazor Server?

Reproduction steps:

  1. Create a FormModel.cs class
public class FormModel
{
    public string? Name { get; set; }
    public string? Email { get; set; }
    public string? Comment { get; set; }
}
  1. Create a FormModelValidator.cs class
public class FormModelValidator : AbstractValidator<FormModel>
{
    public FormModelValidator()
    {
        RuleFor(x => x.Name).NotEmpty().WithMessage("Name is mandatory");
        RuleFor(x => x.Email).NotEmpty().EmailAddress().WithMessage("Email is mandatory and must be valid");
        RuleFor(x => x.Comment).NotEmpty().WithMessage("Comment is mandatory");
    }
}
  1. Register validation class in Program.cs
builder.Services.AddValidatorsFromAssemblyContaining<FormModelValidator>();
  1. Create form in a Razor page
<EditForm Model="@_model">
    <FluentValidationValidator @ref="_fluentValidationValidator" DisableAssemblyScanning="@true" />

    <div class="form-group">
        <label for="name">Name:</label>
        <InputText id="name" class="form-control" @bind-Value="@_model.Name" />
        <ValidationMessage For="@(() => _model.Name)" />
    </div>

    <div class="form-group">
        <label for="email">Email:</label>
        <InputText id="email" class="form-control" @bind-Value="@_model.Email" />
        <ValidationMessage For="@(() => _model.Email)" />
    </div>

    <div class="form-group">
        <label for="comment">Comment:</label>
        <InputTextArea id="comment" class="form-control" @bind-Value="@_model.Comment" />
        <ValidationMessage For="@(() => _model.Comment)" />
    </div>

    <button type="submit" class="btn btn-primary" @onclick="@HandleSubmit">Submit</button>
</EditForm>

@if (_formSubmitted)
{
    <p class="text-success">Form submitted successfully!</p>
}

@if (_formError)
{
    <p class="text-danger">Form is incorrect!</p>
}

@code {
    private FormModel _model = new FormModel();
    private bool _formSubmitted, _formError;
    private FluentValidationValidator? _fluentValidationValidator;

    private async Task HandleSubmit()
    {
        var isValid = await _fluentValidationValidator!.ValidateAsync(options => options.IncludeProperties("Comment"));
        if (isValid)
        {
            _formSubmitted = true;
        }
        else
        {
            _formError = true;
        }
    }
}

Solution

  • The reason for your problem is that with the button type as submit, the form is submitted and HandleSubmitAsync is called.

    This is the EditForm code.

    private async Task HandleSubmitAsync()
    {
        Debug.Assert(_editContext != null);
    
        if (OnSubmit.HasDelegate)
            await OnSubmit.InvokeAsync(_editContext);
    
        else
        {
            var isValid = _editContext.Validate();
    
            if (isValid && OnValidSubmit.HasDelegate)
                await OnValidSubmit.InvokeAsync(_editContext);
    
            if (!isValid && OnInvalidSubmit.HasDelegate)
                await OnInvalidSubmit.InvokeAsync(_editContext);
        }
    }
    

    You don't have a registered OnSubmit delegate, so var isValid = _editContext.Validate(); is run which validates the whole object.

    There are two solutions to the problem.

    1. Change the button type in this line:
    <button type="submit" class="btn btn-primary" @onclick="@HandleSubmit">Submit</button>
    

    To:

    <button type="button" class="btn btn-primary" @onclick="@HandleSubmit">Submit</button>
    
    1. Register your HandleSubmit handler on the EditForm and remove it from the button.
    <EditForm Model="@_model" OnSubmit="HandleSubmit">
    
    //....
    
        <button type="submit" class="btn btn-primary">Submit</button>
    
    

    BTW - excellent Minimal Reproducible Example.