Gaz83
Gaz83

Reputation: 2865

Cast exception when validation failed in Fluent Validation

I have a .net 6 WebApi which I am using Fluent Validation with MediatR. I have everything working when there is no validation errors.

When I force an error I get the following Exception.

Unable to cast object of type 'System.Collections.Generic.Dictionary`2[System.String,System.String[]]' to type 'System.Collections.Generic.IEnumerable`1[FluentValidation.Results.ValidationFailure]'.
at TestMediatR.Behaviours.ValidationBehaviour`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
at MediatR.Pipeline.RequestExceptionProcessorBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
at MediatR.Pipeline.RequestExceptionProcessorBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
at MediatR.Pipeline.RequestExceptionActionProcessorBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
at MediatR.Pipeline.RequestExceptionActionProcessorBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
at MediatR.Pipeline.RequestPostProcessorBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
at MediatR.Pipeline.RequestPreProcessorBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
at WebApi.Controllers.v1.OrdersController.AddOrder(OrderTicketDto model) in D:\Git Repositories\Current\Web-Sites\RestWebApi\src\WebApi\Controllers\v1\OrdersController.cs:line 36

Code executing the mediatR send is this.

 [HttpPost("AddOrder")]
    public async Task<IActionResult> AddOrder([FromBody] OrderTicketDto model)
    {
        _logger.LogInformation("Adding Order: {@model}", model);

        try
        {
            var response = await Mediator.Send(new AddOrderCommand()
            {
                OrderData = model.OrderTicket,
                Url = model.SiteUrl,
                Token = model.Token
            });

            return Ok(response);

        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Add Order Error"); //<------ FluentValidation exception caught here
            return BadRequest(ex.Message);
        }
    }

and validation for the command executed above is done like this

public class AddOrderCommandValidator : AbstractValidator<AddOrderCommand>
{
    public AddOrderCommandValidator()
    {
        RuleFor(x => x.Url)
            .NotEmpty()
            .NotNull();

        RuleFor(x => x.Token)
            .NotEmpty()
            .NotNull();

        RuleFor(x => x.OrderData)
            .NotNull();
    }
}

Register of the validators is done here in startup

public static IServiceCollection AddPiKSRestValidators(this IServiceCollection services)
    {
        var domainAssembly = typeof(GetTablesCommandValidator).GetTypeInfo().Assembly;

        //Add FluentValidation
        services.AddValidatorsFromAssembly(domainAssembly);

        return services;
    }

As I say, everything works when I pass valid properties, but force it to be invalid by setting say Token property to null and I get the exception.

Feel like I am missing something.

Upvotes: 1

Views: 3310

Answers (1)

Gaz83
Gaz83

Reputation: 2865

So the issue was with the ValidationBehaviour

here is the code to report the errors

public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull, IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;
    private readonly ILogger<TRequest> _logger;

    public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators, ILogger<TRequest> logger)
    {
        _validators = validators;
        _logger = logger;
    }

    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        if (_validators.Any())
        {
            var context = new ValidationContext<TRequest>(request);

            var errorsDictionary = _validators
            .Select(x => x.Validate(context))
            .SelectMany(x => x.Errors)
            .Where(x => x != null)
            .GroupBy(
                x => x.PropertyName,
                x => x.ErrorMessage,
                (propertyName, errorMessages) => new
                {
                    Key = propertyName,
                    Values = errorMessages.Distinct().ToArray()
                })
            .ToDictionary(x => x.Key, x => x.Values);

            if (errorsDictionary.Any())
            {
                throw new ValidationException((IEnumerable<FluentValidation.Results.ValidationFailure>)errorsDictionary);
            }
        }
        else
            _logger.LogDebug("No Validators found");

        return await next();
    }
}

As you can see a dictionary is trying to be cast to (IEnumerable<FluentValidation.Results.ValidationFailure>)

fixed with this.

var errorsDictionary = _validators
        .Select(x => x.Validate(context))
        .SelectMany(x => x.Errors)
        .Where(x => x != null)
        .GroupBy(x => new {x.PropertyName, x.ErrorMessage })
        .Select(x => x.FirstOrDefault())
        .ToList();            

        if (errorsDictionary.Any())
        {
            throw new ValidationException(errorsDictionary);
        }

Upvotes: 1

Related Questions