Blazored / FluentValidation

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

Fix inconsistent field-level validation (Fixes #204) #205

Closed matthetherington closed 1 month ago

matthetherington commented 11 months ago

This fixes a few issues that are causing inconsistent behaviour between field-level and whole-model validation.

See #204 for more context and a demo project

Summary of changes:

The above would just mirror the existing whole-model validation for field-level validation, so I think it's a fairly safe change. If a project is using Blazored.FluentValidation then they'll have a validator configured that works for the whole model.

Could I please get your thoughts? I'm looking at this through quite a narrow lense so there may be issues with the proposal that haven't occured to me - it does seem to work okay though.

Resolves #204 Resolves #188 Resolves #180

UniMichael commented 11 months ago

EDIT: You can ignore this comment. I messed something up when I tried the PR. It's working fine as far as I can tell.


Was asked to provide some feedback in #180, since we ended-up making our own validation handler at work to tackle something similar.

From playing around with it, it does seem to properly validate nested objects' fields, though it's worth pointing-out that it doesn't address the problem discussed in #180 (all RuleSets firing, regardless of options), but it's like I said in that discussion:

FluentValidation's API doesn't expose the necessary functions to fix that (which is why our fix at work validates the entire model on field change and only tracks the new validation errors so we can fake a field-speicific validation).


If anyone's interested in trying to fix that specific issue with this PR, here's the basic example I was using to reproduce it:

public class Person
{
    public string Name { get; set; } = string.Empty;
    public int Age { get; set; }
    public Address Address { get; set; } = new();
}

public class Address
{
    public string Street { get; set; } = string.Empty;
    public string Country { get; set; } = string.Empty;
    public string PostalCode { get; set; } = string.Empty;
}

public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator(IValidator<Address> addressValidator)
    {
        RuleFor(a => a.Address).SetValidator(addressValidator);

        RuleFor(p => p.Name).NotEmpty().WithMessage("Name required");

        RuleSet("Adult", () =>
        {
            RuleFor(p => p.Age).GreaterThan(18).WithMessage("Must be an adult");
        });
    }
}

public class AddressValidator : AbstractValidator<Address>
{
    public const string PostalCodePattern = "[a-zA-Z][0-0][a-zA-Z] [0-9][a-zA-Z][0-9]";
    public static readonly Regex PostalCodeRegex = new(PostalCodePattern);

    public AddressValidator()
    {
        RuleFor(a => a.Country).NotEmpty().WithMessage("Country required");
        RuleFor(a => a.Street).NotEmpty().WithMessage("Street required");
        RuleFor(a => a.PostalCode).Matches(PostalCodeRegex).WithMessage("Must be valid postal code");

        RuleSet("Canadian", () =>
        {
            RuleFor(a => a.Country)
                .NotEmpty().WithMessage("Country required")
                .Equal("Canada").WithMessage("Must be Canadian");
        });
    }
}
@page "/"
@using Microsoft.AspNetCore.Components.Forms
@using BlazorServerApp.Models
@using Blazored.FluentValidation

<EditForm Model="_person" OnValidSubmit="() => { }">
    @* NOTE: The options' rulesets are basically ignored, you can use whatever RuleSet you want, and it'll still run all of them *@
    <FluentValidationValidator Options="@(options => options.IncludeRuleSets("default"))"/>
    <ValidationSummary/>

    <h1>Person</h1>
    <div>
        <label>Name</label>
        <InputText @bind-Value="_person.Name"/>
    </div>
    <div>
        <label>Age</label>
        <InputNumber @bind-Value="_person.Age"/>
    </div>

    <h2>Address</h2>
    <div>
        <label>Country</label>
        <InputText @bind-Value="_person.Address.Country"/>
    </div>
    <div>
        <label>Street</label>
        <InputText @bind-Value="_person.Address.Street"/>
    </div>
    <div>
        <label>Postal Code</label>
        <InputText @bind-Value="_person.Address.PostalCode"/>
    </div>

    <button type="submit">
        Submit
    </button>
</EditForm>

@code {

    private readonly Person _person = new();

}
matthetherington commented 11 months ago

Thanks for that @UniMichael

I tried to recreate the problem you were seeing, but it seems to be working okay when using the modified Blazored.FluentValidation from this PR.

Screenshot 2023-12-07 at 15 59 59

I can enter 17 for age, and UK as the country, and both fields are valid when blurring out.

Submitting the form also does not produce a validation message for those fields, so it does seem to be using only the default rule set as specified in the <FluentValidationValidator> config and not including Adult / Canadian. Omitting the options produces the same result.

Screenshot 2023-12-07 at 16 01 10

Is that what you were expecting to happen?

matthetherington commented 11 months ago

And here's the result when I include the validator via:

<FluentValidationValidator Options="@(options => options.IncludeRuleSets("Adult"))"/>
Screenshot 2023-12-07 at 16 10 49

The postcode format check is not ran, so the default ruleset for address is not being ran, and the "must be an adult" message appears, so the Adult ruleset is being ran as per the options provided.

UniMichael commented 11 months ago

@matthetherington Ok, I think I messed something up when I tried-out the PR. I may have forgotten to actually checkout the branch after I pulled it. I'm running it now and it does seem like the rule problem was fixed.

That's what I get for testing it on a Friday morning without having coffee, I guess. 😅

I'll edit my other comment.

matthetherington commented 11 months ago

@UniMichael ah brilliant, thanks for taking another look!

Albiniivari commented 10 months ago

@matthetherington Hey, so I've tried the PR and it seems to be working fine. However, I find the partial-validation a bit odd. If you first input some non-allowed value in to the other fields, it displays a few errors. But if you then press the partial-validate, the existing errors are deleted, so that it seems that they are now OK. I would expect the partial-validate to validate only the fields included in rule-set, but leave existing errors etc to be?

etarvt commented 7 months ago

@matthetherington I've been using the Blazored.FluentValidation library and find it to be excellent, but I encountered issue #204 a week ago. I've tested the PR, and it seems to resolve the problem with the sub-objects for me.

Do you expect this PR to be merged anytime soon?

Xavia1991 commented 5 months ago

Please merge this soon, I have been waiting for this since end of last year when we switched to .net8.

psandler commented 3 months ago

Any timeline on when this will be reviewed/merged? I am also having this problem and this PR seems to resolve it.

ckirker commented 2 months ago

I am in need of this fix as well--as I'm blocked using FluentValidation without it.

DaveVW-AWE commented 1 month ago

Any updates on merging this in? Been waiting a VERY long time for this and it's holding up our project. I like to rely on and support 3rd party packages however this has taken far too long to resolve.

pwelter34 commented 1 month ago

apologies, I'll work to get this resolved and merged