Blazored / FluentValidation

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

How can I validate a collection of objects? #199

Open kingua opened 1 year ago

kingua commented 1 year ago

I'm trying to loop through and validate a collection of objects, but I'm getting the following error. It seems that the validator doesn't have the context of the object to validate. What am I doing wrong / how can I accomplish this?

System.InvalidOperationException: No pending ValidationResult found

Here is the component markup:

<div id="@HtmlIdBase">
    <Alert Id="@($"{HtmlIdBase}-error-alert")" AlertType="@AlertType" Message="@AlertMessage" />
    @if (IsLoading)
    {
        <LoadingIndicator HtmlIdBase="@HtmlIdBase" />
    }
    else
    {
        <div class="container-fluid">
            <TelerikForm @attributes="@Helpers.SetBlazorComponentHtmlIdAttribute($"{HtmlIdBase}")" Model="@Model" Columns="@FormLayoutColumnsCount" ColumnSpacing="25px" OnValidSubmit="@UploadEquipmentAttachments">
                <FormValidation>
                    <FluentValidationValidator @ref="FluentValidationValidator"></FluentValidationValidator>
                </FormValidation>
                <FormItems>
                    <FormItem>
                        <Template>
                            <label class="k-label k-form-label" for="@($"{HtmlIdBase}-description-textbox")">@Constants.FieldLabels.Description</label>
                            <TelerikTextBox @bind-Value="_description"/>
                        </Template>
                    </FormItem>
                    <TelerikFileSelect Multiple="true" OnSelect="@HandleAttachment" Accept="@AcceptedTypes()" AllowedExtensions="@_allowedMimeTypes.Keys.ToList()" />
                    <ValidationMessage For="@(() => FirstFileContent)" class="k-form-error k-invalid-msg"></ValidationMessage>
                </FormItems>
                <FormButtons>
                    <TelerikButton id="@($"{HtmlIdBase}-cancel-button")" ButtonType="@ButtonType.Button" OnClick="@NavigateToEquipmentSummary" Class="button-cancel">@Constants.ButtonLabels.Cancel</TelerikButton>
                    <TelerikButton id="@($"{HtmlIdBase}-upload-button")" ButtonType="@ButtonType.Submit" ThemeColor="@ThemeConstants.Button.ThemeColor.Primary" Class="button-save">@Constants.ButtonLabels.Upload</TelerikButton>
                </FormButtons>
            </TelerikForm>
        </div>
    }
</div>

Below is the codebehind in its entirety for context, but the relevant method is UploadEquipmentAttachments():

public partial class EquipmentAttachmentImageOrVideoUploadForm : ProjectFormComponent<EquipmentAttachmentImageOrVideoUploadForm>
{
    public const string HtmlIdBase = "equipment-attachment-image-or-video-upload";

    [Inject] private IEquipmentAttachmentService EquipmentAttachmentService { get; set; } = null!;

    protected override ILogger TaskRunnerLogger => Logger;

    [Parameter] public int EquipmentId { get; set; }

    public List<AttachmentUpload> Model = new();

    private readonly Dictionary<string, string> _allowedMimeTypes = MimeType.ImageAndVideo;
    private List<string> _validationMessages = new();
    private string _description = string.Empty;
    private byte[]? FirstFileContent => Model.Count > 0 ? Model[0].FileContent : null;

    public async Task HandleAttachment(FileSelectEventArgs args)
    {
        foreach (var file in args.Files)
        {
            var attachment = new AttachmentUpload();

            var byteArray = new byte[file.Size];
            await using var ms = new MemoryStream(byteArray);
            await file.Stream.CopyToAsync(ms);

            // NOTE: The Telerik Control limits file extensions to our allowed types, so this should never be null
            if (_allowedMimeTypes.TryGetValue(file.Extension, out var type))
            {
                attachment.ContentType = type;
            }

            attachment.FileContent = byteArray;
            attachment.FileName = file.Name;
            attachment.Description = _description?.Trim();

            Model.Add(attachment);
        }
    }

    private string AcceptedTypes()
    {
        var sb = new StringBuilder();

        foreach (var pair in _allowedMimeTypes)
        {
            if (sb.Length > 0) sb.Append(',');

            sb.Append(pair.Key);
            sb.Append(',');
            sb.Append(pair.Value);
        }

        var asString = sb.ToString();
        return asString;
    }

    public async Task UploadEquipmentAttachments()
    {
        if (ProjectId is null)
            return;

        await TryExecuteAsync(async _ =>
        {
            IsLoading = true;

            var numberOfValidAttachments = 0;

            foreach (var attachment in Model)
            {
                var validationResult = await FluentValidationValidator!.ValidateAsync(strategy =>
                    strategy.IncludeRuleSets(ValidatorRuleSets.ClientRules));

                if (validationResult)
                {
                    numberOfValidAttachments++;
                }
                else
                {
                    _validationMessages.Add($"Attachment: {attachment.FileName} is invalid.  Please remove.{Environment.NewLine}");
                }
            }

            if (numberOfValidAttachments != Model.Count)
            {
                AlertType = Alert.AlertTypes.Warning;
                AlertMessage = _validationMessages.ToString();
            }
            else
            {
                var serviceResults =
                        await EquipmentAttachmentService.UploadEquipmentAttachments(ProjectId.Value, EquipmentId, Model);

                var results = serviceResults as ServiceResult<bool>[] ?? serviceResults.ToArray();
                var allResultsSuccessful = results.All(sr => sr.Status == OperationResult.Success);

                if (allResultsSuccessful)
                {
                    NavigateToEquipmentSummary();
                }
                else
                {
                    foreach (var result in results)
                    {
                        HandleServiceResult(result,
                            onError: message =>
                            {
                                AlertType = Alert.AlertTypes.Warning;
                                AlertMessage += message + Environment.NewLine;
                            });
                    }
                }
            }

            IsLoading = false;
        }, (ex, _) => throw ex);
    }

    public void NavigateToEquipmentSummary()
    {
        NavigationManager.NavigateTo(
            Constants.Routes.Projects.EquipmentDetail(JobNumber!, EquipmentId));
    }
}
kingua commented 1 year ago

I think I figured this out:

public class AttachmentUploadValidator : AbstractValidator<AttachmentUpload>
{
    public AttachmentUploadValidator()
    {
        RuleLevelCascadeMode = CascadeMode.Stop;

        RuleSet(ValidatorRuleSets.ClientRules, () =>
        {
            RuleFor(attachment => attachment.AttachmentType)
                .NotEmpty()
                .When(attachment => MimeType.EquipmentInformation.ContainsValue(attachment.ContentType));

            RuleFor(attachment => attachment.FileContent).NotEmpty()
                .WithMessage("'File' must be selected");
        });
    }
}

**public class AttachmentUploadListValidator : AbstractValidator<List<AttachmentUpload>>
{
    public AttachmentUploadListValidator()
    {
        RuleForEach(x => x).SetValidator(new AttachmentUploadValidator());
    }
}**
kingua commented 1 year ago

The above seems to properly validate, but now I don't get the actual validation error message returned as validationResult is just a bool. Is there a way to get the error messages so I can do something like the following?

            foreach (var attachment in Model)
            {
                var validationResult = await FluentValidationValidator!.ValidateAsync(strategy =>
                    strategy.IncludeRuleSets(ValidatorRuleSets.ClientRules));

                if (validationResult)
                {
                    numberOfValidAttachments++;
                }
                else
                {
                    var errorMessages = string.Join(", ", validationResult.Errors.Select(e => e.ErrorMessage));
                    validationMessages.Add($"Attachment: {attachment.FileName} is invalid for the following reasons: {errorMessages}.");
                }
            }