Search code examples
c#blazorfluentvalidation

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.


Solution

  • 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.