Blazored / FluentValidation

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

[Bug] Pass array of RuleSets to Options from component parameter #225

Open mohaaron opened 7 months ago

mohaaron commented 7 months ago

Describe the bug In my form component where I'm using FluentValidationValidator I have a parameter that I'm using to apply the RuleSets that I want to use for the for validation. This is not working for some reason.

<FluentValidationValidator Options="@(options => options.IncludeRuleSets(RuleSets.ToArray()))" />

[Parameter]
public List<string> RuleSets { get; set; } = new();

I can only get the RuleSet correctly applied by passing a static string array as follows.

<FluentValidationValidator Options="@(options => options.IncludeRuleSets("Rule1", "Rule2"))" />

Expected behavior Passing a Parameter array to the IncludeRuleSets method correctly applies the rules.

Hosting Model (is this issue happening with a certain hosting model?):

Here is the complete code that I'm trying to make work that is wrapping a TelerikForm so that I can reuse it across many forms.

@using FluentValidation
@using FluentValidation.Internal
@typeparam TModel where TModel : class
@inherits FormComponentBase<TModel>

<TelerikForm Class="@Class"
                EditContext="EditContext"
                ValidationMessageType="FormValidationMessageType.Tooltip"
                OnUpdate="FieldUpdated"
                OnValidSubmit="ValidSubmit">
    <FormValidation>
        <FluentValidationValidator Options="@(options => SetValidationStrategy(options))" />
    </FormValidation>
    <FormItems>
        @ChildContent
    </FormItems>
    <FormButtons>
        @if (IsEditMode)
        {
            <div class="account-buttons">
                <ActionButton ActionText="Save Changes" ActionType="ActionType.Primary" ActionEnabled="@IsValidated" IsBusy="@IsSaving" />
                <ActionButton ActionText="Cancel" ActionType="ActionType.Link" ActionClick="@(() => IsEditMode = false)" />
            </div>
        }
    </FormButtons>
</TelerikForm>

@code
{
    [Parameter, EditorRequired]
    public RenderFragment ChildContent { get; set; } = default!;

    [Parameter]
    public string Class { get; set; } = string.Empty;

    [Parameter]
    public List<string> RuleSets { get; set; } = new();

    private void SetValidationStrategy(ValidationStrategy<object> strategy)
    {
        if (RuleSets.Count() > 0)
        {
            strategy.IncludeRuleSets(RuleSets.ToArray());
        }
    }
}
mohaaron commented 7 months ago

Maybe I've figured out something about how the RuleSets are supposed to be setup. Does it make sense that I have to put the RuleSet in both the parent validator CustomerProfileValidator, and the child validators for the above code to work? I've found that I get very strange behavior if I don't put the rule in both the parent and child validators. On the screen the form fields when entering an incorrect value will blip a red outline around the text box for a split second and then validation returns true instead of false.

namespace Portal.Shared.Validators;
public class CustomerProfileValidator : AbstractValidator<CustomerProfile>
{
    public CustomerProfileValidator()
    {
        RuleSet("Profile", () =>
        {
            RuleFor(profile => profile.EmploymentStartDate)
                .NotNull().NotEmpty()
                .WithMessage("Start date must be selected");
        });

        RuleSet("PhoneNumber", () =>
        {
            RuleForEach(profile => profile.PhoneNumbers)
                .SetValidator(new PhoneNumberValidator());
        });

        RuleSet("Email", () =>
        {
            RuleForEach(profile => profile.EmailAddresses)
                .SetValidator(new EmailAddressValidator());
        });
    }
}

namespace Portal.Shared.Validators;
public class PhoneNumberValidator : AbstractValidator<PhoneNumber>
{
    public PhoneNumberValidator()
    {
        RuleSet("PhoneNumber", () =>
        {
            RuleFor(phone => phone.Number)
            .MaximumLength(25).WithMessage("Maximum length is 25 characters")
            .Matches(@"^[\+\d]?(?:[\d\-\s()]*)$").WithMessage("Numbers can only contain numbers, dashes, and spaces");
        });
    }
}

namespace Portal.Shared.Validators;
public class EmailAddressValidator : AbstractValidator<EmailAddress>
{
    public EmailAddressValidator()
    {
        RuleSet("Email", () =>
        {
            RuleFor(email => email.Email)
            .MaximumLength(100).WithMessage("Maximum length is 100 characters")
            .Matches(@"^[a-z0-9_\.-]+\@[\da-z\.-]+\.[a-z\.]{2,6}$", System.Text.RegularExpressions.RegexOptions.IgnoreCase)
                .WithMessage("Email is not in the correct format");
        });
    }
}
mohaaron commented 7 months ago

I've created the linked repo as an example. I am trying to recreate the issue and at this point I've not be able to repro it. I am now seeing a different issue related to RuleSets in that the RuleSets are not being enforced. In the current state of the code the Email validationshould be ignored and only the Customer validation should be running but it's not. I don't understand why RuleSets are behaving so strangely.

https://github.com/mohaaron/BlazoredCollectionValidation

  1. Navigate to Email validation menu item.
  2. Click Add Email button to add a blank text box.
  3. Click Submit button. You see that the first and last name inputs fail validation, and nothing happens with the Email input.
  4. Now enter random characters into the Email input and click submit. The red border around the input and the validation message show up for a split second and then the input returns to a green border.
  5. Add some more random characters to the Email input and now the red border and validation message stay.

I expected the Email input to be ignored completely given the RuleSet is using only "Customer".