Blazored / FluentValidation

A library for using FluentValidation with Blazor
https://blazored.github.io/FluentValidation/
MIT License
597 stars 85 forks source link

Validation on childcollection fails if using the Validator property #188

Closed jstrandelid closed 1 month ago

jstrandelid commented 1 year ago

Setup Hosted Blazor WebAssembly (default template in VS) FluentValidation 11.5.2 Blazored.FluentValidation 2.1.0

The code has three ways validate a personcollection (2persons in each collection, one collection per example) "Assembly validation" is using separate validator classes and detected by assembly scanning. "Validator property" is using separate validator classes, but instead of using scanning the Validator property is given an instance of the PersonCollectionValidator. "Inline validator" is using an instance of inline validators, where the IValidator instance is given to the Validator property of then FluentValidationValidator.

Expected For all examples. If a modified input value fails validation a validation message should appear below the input. When clicking the submit button, all fields that fails validation a validation message should appear below each input.

Actual For "Assembly validation" all works as expected. For the other two examples that are using the Validation property validation works when clicking on the submit button, but an exception is thrown when the input lost focus and an error message appears in the browser console stating "System.InvalidOperationException: Cannot validate instances of type 'PersonClass'. This validator can only validate instances of type 'PersonCollection'." image

Code (index.razor is modified)


@page "/"
@using System.ComponentModel.DataAnnotations;
@using Blazored.FluentValidation;
@using FluentValidation;
@using Microsoft.Extensions.Localization;
@using System.Diagnostics;

<PageTitle>Index</PageTitle>

<h1>Assembly validation</h1>
<EditForm Model=@pc1>
    <FluentValidationValidator  />
    @foreach(var person in pc1.Persons) {
        <div class="form-group">
            <label for="Name">Name</label>
            <InputText @bind-Value="@person.Name"/>
            <ValidationMessage For=@( () => person.Name) />
        </div>
        <div class="form-group">
            <label for="Age">Age</label>
            <InputNumber @bind-Value=person.Age class="form-control" id="Age" />
            <ValidationMessage For=@( () => person.Age ) />
        </div>
    }

    <input type="submit" class="btn btn-primary" value="Save" />
</EditForm>
<br/>
<h1>Validator property</h1>
<EditForm Model=@pc2>
    <FluentValidationValidator Validator="@validator" />
    @foreach (var person in pc2.Persons)
    {
        <div class="form-group">
            <label for="Name">Name</label>
            <InputText @bind-Value="@person.Name"/>
            <ValidationMessage For=@( () => person.Name) />
        </div>
        <div class="form-group">
            <label for="Age">Age</label>
            <InputNumber @bind-Value=person.Age class="form-control" id="Age" />
            <ValidationMessage For=@( () => person.Age ) />
        </div>
    }

    <input type="submit" class="btn btn-primary" value="Save" />
</EditForm>
<br/>
<h1>Inline validator</h1>
<EditForm Model=@pc3>
    <FluentValidationValidator Validator="@inlineValidator" />
    @foreach (var person in pc3.Persons)
    {
        <div class="form-group">
            <label for="Name">Name</label>
            <InputText @bind-Value="@person.Name"/>
            <ValidationMessage For=@( () => person.Name) />
        </div>
        <div class="form-group">
            <label for="Age">Age</label>
            <InputNumber @bind-Value=person.Age class="form-control" id="Age" />
            <ValidationMessage For=@( () => person.Age ) />
        </div>
    }

    <input type="submit" class="btn btn-primary" value="Save" />
</EditForm>

@code {
    PersonCollection pc1 = new PersonCollection();
    PersonCollection pc2 = new PersonCollection();
    PersonCollection pc3 = new PersonCollection();

    PersonClass Person = new PersonClass();
    IValidator<PersonCollection> validator = new PersonCollectionValidator();
    IValidator inlineValidator = CreateInlineValidator();

    private static IValidator CreateInlineValidator()
    {
        var collectionVal = new InlineValidator<PersonCollection>();
        var personVal = new InlineValidator<PersonClass>();
        personVal.RuleFor(x => x.Name).NotEmpty();
        collectionVal.RuleForEach(x => x.Persons).SetValidator(personVal);

        return collectionVal;
    }

    public class PersonCollection
    {
        public IEnumerable<PersonClass> Persons { get; set; } = new List<PersonClass>
        {
            new PersonClass { Age=1, Name="A"},
            new PersonClass { Age=2, Name="B"}
        };

        public IValidator GetValidator()
        {
            var inlineVal = new PersonCollectionValidator();
            inlineVal.RuleForEach(x => x.Persons).SetValidator(new PersonClassValidator());
            return inlineVal;
        }
    }

    public class PersonClass
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

    public class PersonCollectionValidator : AbstractValidator<PersonCollection>
    {
        public PersonCollectionValidator()
        {
            RuleForEach(x => x.Persons).SetValidator(new PersonClassValidator());
        }
    }

    public class PersonClassValidator : AbstractValidator<PersonClass>
    {
        public PersonClassValidator()
        {
            RuleFor(x => x.Name).NotEmpty();
        }
    }
}
jstrandelid commented 1 year ago

After some examination I notice that the ValidateField function that is triggered when a field has changed is receiving the "root" validator ("PersonCollectionValidator") and not the validator set in the "RuleForEach" for Persons, but the model given in the eventargs is of the type "PersonClass" . Hence the validator of "PersonCollection" type can not validate a PersonClass object.

The ValidateModel function, that is triggered by a submit in the form, validates the "root model"(PersonCollection) and the validator is the "root validator" (PersonCollectionValidator) and therefor it works.

jstrandelid commented 1 year ago

I think this issue regards nested validators.

matthetherington commented 11 months ago

Hi, this sounds like it would be fixed by #205 - please could you weigh in

jstrandelid commented 11 months ago

Hi Matt, From what I can understand of your description in #205 (and #204) it sounds like a good solution that would work. Unfortunately, my knowledge of FluentValidation and Blazored.FluentValidation is lacking a bit, so I can't weight in on if it's the best solution or understand the impact of the solution. If you'd like i could test some different scenarios with the commited code.

matthetherington commented 11 months ago

That'd be very helpful, thanks!

alperenbelgic commented 9 months ago

I know it looks horrible, but when I place try & empty catch around below. validations for nested object just work (don't ask me how) and I don't get the breaking errors. I got the source code & project into my solution. I'll observe for a while. I just wanted to share, and also would love to know if it works in other use cases as well.

https://github.com/Blazored/FluentValidation/blob/ef26fc1b9c34e2e4afe8d16d61dbfc11e431b204/src/Blazored.FluentValidation/EditContextFluentValidationExtensions.cs#L84C1-L89C56

jstrandelid commented 7 months ago

I "finally" had time to test this (sorry about the long delay). I my first test was with the given example above and it does not work quite as I expected. (i might have done something wrong getting the code) Could you please confirm my finding.

The errors in the console are gone, but if I: 1, enter a name (in any of the examples)

  1. leave the input
  2. enter the same input and erase the name
  3. leave the input.

I would expect that "'Name' must not be empty" should be shown, however it's not. The input has a green border as it is valid. If I press the associated save button the error message for Name is shown and the input has an "invalid" state.

Expected After an input is modified and the value is invalid, I would expect the error message to should and the input should show an invalid state (red border).