BobbyB
BobbyB

Reputation: 33

Fluent Validation - Check for Duplicate Value

I have a EditForm in a Blazor Server application and i want to check if an InputText value is in a list. How can i pass the list to compare from my UI to the Validator class for comparison?

I have tried comparing the @bind-Value in line and encapsulating the validation message but it skips over the validation message when the encapsulating function tests true.

 <EditForm Model="@resourceToBeCreated">
    <FluentValidationValidator ValidatorType=typeof(ResourceValidator)/>
    @if (resourcesSortedCollection.FirstOrDefault(x => x.Name == resourceToBeCreated.Name) != null)
    {
        <CustomValidationMessage For="() => resourceToBeCreated.Name" /> 
    }
                             
    <InputTextOnInput @bind-Value="@resourceToBeCreated.Name" class="form-control"  placeholder="Name..." />
 </EditForm>

I can obviously do this or something similar in the @code section but i dont get the validation popup on inupt.

So the question is, how can i pass this list to the Validator class for comparison?

EDIT 1: InputTextOnInput component:

@inherits InputText
<input @attributes="AdditionalAttributes"
       class="@CssClass"
       value="@CurrentValue"
       @oninput="EventCallback.Factory.CreateBinder<string>(this, __value => CurrentValueAsString = __value, CurrentValueAsString)" />

EDIT 2: A potential workaround while still utilising fluent validation.

1, add new property to the model :

public List<string> ResourceNames { get; set; }

2, when a new resource is created in the browser update that property in the model

resourceToBeCreated.ResourceNames = resourcesSortedCollection.Select(x => x.Name).ToList();

3, write rule in fluent validation

RuleFor(x => x.Name).Null().When(x => x.ResourceNames.Contains(x.Name)).WithMessage("Duplicate resource name");

Not sure if this is the best way to do it (code smell?) but it works for now. Either way i have to create a list of strings which contains all the resource names. If there is a more direct way to pass the resourcesSortedCollection object to the validator id like to understand.

Upvotes: 1

Views: 3404

Answers (2)

BobbyB
BobbyB

Reputation: 33

I thought id throw an answer up as i stumbled upon one while doing something else and it may help another.

You can pass the values to validate against into the validator when it is instantiated. In this case pass in a list of BaseResourceMoldes into the ResouceValidator via a constructor. As the list wont change between instantiation and validation this is suitable.

You then use the Must extension which will pass the parameter you are validating into a called function to test for bool. In this case .Must(IsUnique) passes x.Name into IsUnique(string arg1) and returns a bool.

Syntax might be slightly different to examples above as the code base will have changed between then and now but the concept is the same.

The class with the form to be validated:

[Parameter] public List<BaseResourceModel> Resources { get; set; }
ResourceValidator resourceValidator;

protected override void OnInitialized()
{
    resourceValidator = new ResourceValidator(Resources);
}

And then the ResourceValidator Class:

private List<BaseResourceModel> _resources;
private void ResourceValidator(List<BaseResourceModel> resources)
{
    _resources = resources;
    RuleFor(x => x.Name).NotEmpty().Must(IsUnique).WithMessage("Resource name must be unique");
}

private bool IsUnique(string arg1)
{
    bool isUnique = true;

    foreach (var resource in _resources)
    {
        if (resource.Name == arg1)
        {
            isUnique = false;
        }
    }
    return isUnique;
}

I'm assuming you could also do this asynchronously if the list had the potential to change. Fluent validation has async methods.

Upvotes: 1

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30185

First an inherited InputText control. This overrides TryParseValueFromString and does the validation there.

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;

namespace BlazorApp1.Pages;

public class InputTextValidated : InputText
{
    [Parameter, EditorRequired] public IEnumerable<string>? CheckList { get; set; }
 { get; set; }

    protected override bool TryParseValueFromString(string? value, out string? result, [NotNullWhen(false)] out string? validationErrorMessage)
    {
        result = null;
        validationErrorMessage = null;

        var isValid = this.CheckList is not null
            && this.CheckList.Any(item => item.Equals(value, StringComparison.InvariantCultureIgnoreCase));

        if (isValid)
            result = value;
        else
            validationErrorMessage = "You must enter a value in the validation list";

        return isValid;
    }
}

And a test page:

@page "/"

<PageTitle>Index</PageTitle>

<EditForm Model=this.model>
    <DataAnnotationsValidator />
    <InputTextValidated class="form-control" CheckList=Countries @bind-Value="@this.model.Country" />
    <ValidationSummary />
</EditForm>

<div>
    Country : @model.Country
</div>

@code {
    private DataModel model = new DataModel();

    private List<string> Countries = new List<string> { "UK", "Spain" };

    public class DataModel
    {
        public string? Country;
    }
}

As an alternative you could use/build an Input List control.

How Validation works

Validation data is held in ValidationMessageStore's associated with an EditContext. A ValidationMessageStore is a composite collection of key/value pairs:

  • the field defined as a FieldIdentifier [the model as an object and the field name as a string]
  • the validation message as string.

Each validation provider has it's own message store and can clear and add messages to it. It only has write/delete access to it's own message store. Providers get the EditContext cascaded by the EditForm, create a message store associated with the EditContext and logs messages to and clears messages from that store. FluentValidationValidator, DataAnnotationsValidator, any InputBase control or classes you write that interact with the EditContext are providers with message stores associated with the EditContext.

ValidationSummary and ValidationMessage are consumers. They interact with the message stores associated with a EditContext via the cascaded EditContext. All the messages are available as read only. ValidationMessage constructs a FieldIdentifier from the For expression to filter the messages.

Upvotes: 1

Related Questions