Using Fluent Results inside a MediatR validation pipeline to return a Result<TResponse>

Viewed 3004

First off, I'm using Fluent Results in combination with Mediatr and Fluent Validation

I initially followed this article but instead of reinventing the wheel I started using FluentResults in my Fluent Validation pipeline. Basically all responses coming from my CQRS Queries are wrapped in the Result object, this avoids having to work with exceptions as a method of error handeling.

However, I can't get my pipeline to play nice:

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

    public ValidationPipeline(IValidator<TRequest> compositeValidator)
    {
        _compositeValidator = compositeValidator;
    }

    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        var result = await _compositeValidator.ValidateAsync(request, cancellationToken);

        if (!result.IsValid)
        {
            Error error = new Error();
            var responseType = typeof(TResponse);

            foreach (var validationFailure in result.Errors)
            {
                Log.Warning($"{responseType} - {validationFailure.ErrorMessage}");
                error.Reasons.Add(new Error(validationFailure.ErrorMessage));
            }
            // This always returns null instead of a Result with errors in it. 
            var f = Result.Fail(error) as TResponse;
            return f;

        }

        return await next();
    }
}

I also have to somehow convert the Result object back to TResponse, where TResponse is always a Result

Any suggestions are greatly appreciated!

Edit:

Autofac integration

    protected override void Load(ContainerBuilder builder)
    {
        var assembly = Assembly.GetExecutingAssembly();

        // MediatR
        builder.AddMediatR(assembly);
        // Register the Command's Validators (Validators based on FluentValidation library)
        builder.RegisterAssemblyTypes(assembly)
            .Where(t => t.IsClosedTypeOf(typeof(IValidator<>)))
            .AsImplementedInterfaces();
        // Register all the Command classes (they implement IRequestHandler) in assembly holding the Commands
        builder.RegisterAssemblyTypes(assembly)
            .AsClosedTypesOf(typeof(IRequestHandler<,>));
        // Register Behavior Pipeline
        builder.RegisterGeneric(typeof(ValidationPipeline<,>)).As(typeof(IPipelineBehavior<,>));

    }
4 Answers

You should change

where TResponse : class

to

where TResponse : Result

and make sure all your requests are IRequest<Result>

where T is the actual response you want to return.

I was editing my post but it became so long I figured it's almost an answer to my problem.

I have been able to isolate the issue to the following line:

 var f = Result.Fail(error).ToResult<CustomClass>() as TResponse;
 return f;

If I hard reference the class that I pass into the Result then the conversion works as expected and everything works as it should. Now the question becomes, how can I get a class reference which I can pass into the .ToResult<T>() from just TResponse?

I have been searching around for an answer but it seems to be fundamentally impossible to retrieve a compile time class reference, during run-time, when I would need it during compile time.

I also tried to instantiate a copy of the result object and return that after adding the validation errors. Like this:

 var resultType = typeof(TResponse).GetGenericArguments()[0];
 var invalidResponseType = typeof(ValidateableResponse<>).MakeGenericType(resultType);
 var f = Activator.CreateInstance(invalidResponseType, null) as TResponse;
 return f;

This would work but the Result object has a closed constructor and thus I am left with an exception. I have left an issue on the GitHub of FluentResult and maybe it can be changed.

For now I have a workaround, I let my handlers inherit from BaseHandler

BaseHandler:

public class BaseHandler
{
    public Result Validate<TQuery, TValidator>(TQuery request) where TValidator : AbstractValidator<TQuery>
    {
        var validator = (TValidator)Activator.CreateInstance(typeof(TValidator));
        var result = validator.Validate(request);
        return CreateResult(result);
    }

    public async Task<Result> ValidateAsync<TQuery, TValidator>(TQuery request) where TValidator : AbstractValidator<TQuery>
    {
        var validator = (TValidator)Activator.CreateInstance(typeof(TValidator));
        var result = await validator.ValidateAsync(request);
        return CreateResult(result);
    }

    private Result CreateResult(ValidationResult result)
    {
        if (!result.IsValid)
        {
            if (result.Errors.Count == 1)
            {
                string msg = $"Validation Failure: {result.Errors.First().ErrorMessage}";
                return Result.Fail(msg);
            }

            Error error = new Error("Validation Failure");

            foreach (var validationFailure in result.Errors)
            {
                Log.Warning($"{validationFailure.ErrorMessage}");
                error.Reasons.Add(new Error(validationFailure.ErrorMessage));
            }
            return Result.Fail(error);
        }
        return Result.Ok();
    }
}

Usage Example:

  // At the top of my handle function
  var result = await ValidateAsync<GetItemByIdQuery, GetItemByIdQueryValidator>(request);
  if (result.IsFailed) return result;

This works as well, the only downside is that I have to add this at the top of every Handle function to enable the validation.

For now it's fine, I will wait and see if the FluentResult package can be updated so I can try the earlier stated suggestion.

Thanks everyone for the suggestions!

If I understand what you are asking, I think I've done the same thing you are trying to do. Below is my solution to this problem.

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

    public ValidationPipeline(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators ?? throw new ArgumentNullException(nameof(validators));
    }

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

        if (validationFailures.Any())
        {
            var responseType = typeof(TResponse);
            TResponse invalidResponse;
            if (responseType.IsGenericType)
            {
                var resultType = responseType.GetGenericArguments()[0];
                var invalidResponseType = typeof(Result<>).MakeGenericType(resultType);

                invalidResponse = Activator.CreateInstance(invalidResponseType, null) as TResponse;
            }
            else
            {
                invalidResponse = new TResponse();
            }

            invalidResponse.WithErrors(validationFailures.Select(s => s.ErrorMessage));
            return invalidResponse;
        }

        return await next();
    }
}

This will handle the case where you are using Result or Result.

I had a similar problem. Moreover some handlers returned Result while other Result<ABC> or Result<XYZ>. Generic behavior couldn't handle this.

Two workarounds:

  1. Instead of returning Result<> throw an exception and covert to a Result down the pipeline (catch or global error handle).

  2. Register each handler and pipeline separately like this

    services.AddTransient( typeof(IPipelineBehavior<MyCommand,Result<MyCommandResult>>), typeof(MyBehavior<MyCommand, MyCommandResult>));

    But it's a lot of headache and has to be automated.

Related