Search code examples
c#localizationblazorblazor-webassemblyfluentvalidation

Blazor Wasm and FluentValidation with Localization


In my razor file:

<EditForm id="@EditFromId" Model="@Entity" OnValidSubmit="Save">
    <FluentValidator TValidator="PersonalInformationValidator" />

In my PersonalInformationValidator class:

public PersonalInformationValidator(IStringLocalizer<ErrorResource> Loc)
{
    ClassLevelCascadeMode = CascadeMode.Continue;

    RuleFor(entity => entity.FatherName)
        .NotEmpty().WithMessage(x=>Loc["Father-Name-Required"])
        .Length(3, 50).WithMessage(x => Loc["Father-Name-Length"]);
}

I get a compilation time error:

Error CS1662 Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type

and

Error CS0310 'PersonalInformationValidator' must be a non-abstract type with a public parameterless constructor in order to use it as parameter 'TValidator' in the generic type or method 'FluentValidator'

Tried to add this line to the Program.cs

builder.Services.AddTransient<IValidator<PersonalInformation>, PersonalInformationValidator>();

Also tried to create a parameterless contractor PersonalInformationValidator. It resolves the compile time error but the validator does not work.

I am using FluentValidation (11.4.0)

and this class on the client side:

public class FluentValidator<TValidator> : ComponentBase where TValidator : IValidator, new()
{
    private readonly static char[] separators = new[] { '.', '[' };
    private TValidator? validator;

    [CascadingParameter] private EditContext? EditContext { get; set; }

    protected override void OnInitialized()
    {
        if (EditContext != null)
        {
            validator = new TValidator();
            var messages = new ValidationMessageStore(EditContext);
            EditContext.OnFieldChanged += (sender, eventArgs)
                => ValidateModel((EditContext?)sender, messages);

            EditContext.OnValidationRequested += (sender, eventArgs)
                => ValidateModel((EditContext?)sender, messages);
        }
    }

    private void ValidateModel(EditContext? editContext, ValidationMessageStore messages)
    {
        if (editContext != null)
        {

            var context = new ValidationContext<object>(editContext.Model);
            var validationResult = validator?.Validate(context);
            messages.Clear();
            if (validationResult != null)
            {
                foreach (var error in validationResult.Errors)
                {
                    var fieldIdentifier = ToFieldIdentifier(editContext, error.PropertyName);
                    messages.Add(fieldIdentifier, error.ErrorMessage);
                }
                editContext.NotifyValidationStateChanged();
            }
        }
    }

    private static FieldIdentifier ToFieldIdentifier(EditContext editContext, string propertyPath)
    {
        var obj = editContext.Model;

        while (true)
        {
            var nextTokenEnd = propertyPath.IndexOfAny(separators);
            if (nextTokenEnd < 0)
            {
                return new FieldIdentifier(obj, propertyPath);
            }

            var nextToken = propertyPath.Substring(0, nextTokenEnd);
            propertyPath = propertyPath.Substring(nextTokenEnd + 1);

            object newObj;
            if (nextToken.EndsWith("]"))
            {

                nextToken = nextToken.Substring(0, nextToken.Length - 1);
                var prop = obj.GetType().GetProperty("Item");
                var indexerType = prop?.GetIndexParameters()[0].ParameterType;
                var indexerValue = Convert.ChangeType(nextToken, indexerType);
                newObj = prop?.GetValue(obj, new object[] { indexerValue });
            }
            else
            {
                var prop = obj.GetType().GetProperty(nextToken);
                if (prop == null)
                {
                    throw new InvalidOperationException($"Could not find property named {nextToken} on object of type {obj.GetType().FullName}.");
                }
                newObj = prop?.GetValue(obj);
            }

            if (newObj == null)
            {
                return new FieldIdentifier(obj, nextToken);
            }

            obj = newObj;
        }
    }
}

Solution

    1. Remove the constraint with parameterless constructor new() from the TValidator constraint.

    2. Get the IStringLocalizer<ErrorResource> service from the DI.

    3. With Activator.CreateInstance() create TValidator instance with provide the Loc as parameter.

    public class FluentValidator<TValidator> : ComponentBase
        where TValidator : IValidator
    {
        private readonly static char[] separators = new[] { '.', '[' };
        private TValidator? validator;
    
        [CascadingParameter] private EditContext? EditContext { get; set; }
    
        [Inject]
        IStringLocalizer<ErrorResource> Loc { get; set; }
    
        protected override void OnInitialized()
        {
            if (EditContext != null)
            {
                validator = (TValidator)Activator.CreateInstance(typeof(TValidator), new object[] { Loc });
    
                ...
            }
        }
    
        ...
    }
    

    Demo

    enter image description here