Return response with errors in MediatR pipeline behavior
Asked Answered
W

2

7

I'm new to MediatR, trying to make request validation using pipeline behavior, all the examples that I came across were throwing ValidationException if any errors happening.
below code is an example of validation pipeline:

public class ValidationPipeline<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator> _validators;

    public ValidationPipeline(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        var context = new ValidationContext<TRequest>(request);
        var validationFailures = _validators
            .Select(validator => validator.Validate(context))
            .SelectMany(validationResult => validationResult.Errors)
            .Where(validationFailure => validationFailure != null)
            .ToList();

        if (validationFailures.Any())
        {
            throw new FluentValidation.ValidationException(validationFailures);
        }
        
        return next();
    }
}

this method works fine, but I want to return the response with validation errors (without) throwing exception, so I tried this:

public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, BaseResponse<TResponse>>
    where TRequest : IRequest<BaseResponse<TResponse>>
{
    private readonly IEnumerable<IValidator> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public Task<BaseResponse<TResponse>> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<BaseResponse<TResponse>> next)
    {
        var context = new ValidationContext<TRequest>(request);
        var validationFailures = _validators
            .Select(validator => validator.Validate(context))
            .SelectMany(validationResult => validationResult.Errors)
            .Where(validationFailure => validationFailure != null)
            .ToList();

        if (validationFailures.Any())
        {
            return Task.FromResult(new BaseResponse<TResponse>
            {
                Code = 400,
                Message = "Validation error",
                Error = validationFailures.Select(err => err.ErrorMessage)
            });
        }
        else
        {
            return next();
        }
    }

but now the validation pipeline code does not execute,
and execution go to regular handlers (ex: Register User Handler).

my response (used in all handlers):

public class BaseResponse<TResponse>
{
    public int Code { get; set; }
    public string Message { get; set; }
    public TResponse Result { get; set; }
    public object Error { get; set; }
    public string TraceIdentifier { get; set; }
}

register the behaviour with DI like this:

services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

any help will be appreciated.

Wendolynwendt answered 21/6, 2021 at 17:59 Comment(6)
Do you register the MediatR request handlers in the startup.cs and how you use this MediatR in asp.net core? Details about how to use it, I suggest you could refer to this article.Crashing
When you say the validation pipeline "doesn't trigger" what exactly do you mean? Can you no longer hit breakpoints anywhere in the pipeline?Lalita
pipeline code does not execute (I put breakpoint at the first line of "Handle" method)... and execution go to regular handlers (ex: Register User Handler) .Wendolynwendt
So just replacing the code that throws the exception with the code that returns a BaseResponse<TResponse> causes the entire Handle method to never execute? That doesn't sound right, obviously. You're sure you're not swallowing/ignoring some unhandled exception that may be happening there?Lalita
Does your handler accept a message of type IRequest<BaseResponse<TResponse>>? If not, then this pipeline behaviour will not execute.Astaire
have you been able to solve? I have the same errorSuggestibility
B
5

It's doesn't trigger because this pipeline doesn't match your IRequest anymore. In your case TResponse is already BaseResponse<> and you wrapping it once more.

I assume you have the following request structure:

public record TestDto(string Result);

public class TestCommand(int Id, string Name) : IRequest<BaseResponse<TestDto>>;

public class TestCommandHandler : IRequestHandler<TestCommand, BaseResponse<TestDto>>
{
    public async Task<BaseResponse<TestDto>> Handle(TestCommand request, CancellationToken cancellationToken)
    {
        ...
    }
}

In this case TResponse is BaseResponse<TestDto>.

To solve the problem you can do the following:

Add a constructor with parameters to your BaseResponse<T> like this:

public class BaseResponse<TResponse>
{
    public int Code { get; set; }
    public string Message { get; set; }
    public TResponse Result { get; set; }
    public object Error { get; set; }
    public string TraceIdentifier { get; set; }

    public BaseResponse()
    {

    }

    public BaseResponse(int code, string message, object error)
    {
        Code = code;
        Message = message;
        Error = error;
    }
}

Then if validation fails you have to create this object. You might use Activator to achieve this.

public class ValidationPipeline<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator> _validators;

    public ValidationPipeline(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        var context = new ValidationContext<TRequest>(request);
        var validationFailures = _validators
            .Select(validator => validator.Validate(context))
            .SelectMany(validationResult => validationResult.Errors)
            .Where(validationFailure => validationFailure != null)
            .ToList();

        if (validationFailures.Any())
        {
            var code = 400;
            var message = "Validation error";
            var error = validationFailures.Select(err => err.ErrorMessage);

            return (TResponse)Activator.CreateInstance(typeof(TResponse),
                                                       code, 
                                                       message, 
                                                       error);
        }
        
        return next();
    }
}
Badinage answered 25/7, 2022 at 17:55 Comment(0)
D
1

I used next method. First of all I'm using Adrdalis.Result or via nuget Ardalist.Result i find it very usefull. Pipeline code:

public class FluentValidationPipelineBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public FluentValidationPipelineBehaviour(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }
    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        var requestType = request.GetType();
        if (requestType != null)
        {
            var attribute = requestType.GetCustomAttribute<FluentValidationAttribute>();
            if (attribute != null && attribute.IsEnabled)
            {
                var context = new ValidationContext<TRequest>(request);
                var validationResults = await Task.WhenAll(
                    _validators.Select(v =>
                        v.ValidateAsync(context, cancellationToken)));

                var failures = validationResults
                    .Where(r => r.Errors.Any())
                    .SelectMany(r => r.Errors)
                    .ToList();
            
                if (failures.Any())
                {
                    if (attribute.ThrowExceptionOnError)
                    {
                        throw new ValidationException(failures);
                    }
                    return GetValidatableResult(failures.AsErrors());
                }
            }
        }
        return await next();
    }

    private static TResponse GetValidatableResult(List<ValidationError> validationErrors)
    {
#pragma warning disable CS8603
#pragma warning disable CS8602
#pragma warning disable CS8600
        return (TResponse)(typeof(Result<>).MakeGenericType(typeof(TResponse).GetGenericArguments())
            .GetMethod("Invalid").Invoke(null, new object?[] { validationErrors }));
#pragma warning restore CS8600
#pragma warning restore CS8602
#pragma warning restore CS8603
    }
}

I'm using FluentValidationAttribute to configure fluentvalidation behaviour

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class FluentValidationAttribute : Attribute
{
    public bool IsEnabled { get; set; } = true;
    public bool ThrowExceptionOnError { get; set; } = false;
}

Use it on Command / Query etc;

Desirous answered 25/7, 2022 at 14:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.