Using the Result pattern, I want to implement a ValidationBehavior using FluentValidation and MediatR. The behavior's return value should either be a Result or a Result<TValue>.
My base Result class looks like this:
public class Result
{
protected internal Result(bool isSuccess, Error error){...}
public Error Error { get; }
public bool IsFailure => !IsSuccess;
public bool IsSuccess { get; }
// some static methods to create Success and Failure results...
// e.g.
public static Result<TValue> Failure<TValue>(Error error)
=> error.ToResult<TValue>();
}
And the generic version to hold a value:
public class Result<TValue> : Result
{
private readonly TValue? _value;
protected internal Result(TValue? value, bool isSuccess, Error error)
: base(isSuccess, error)
{
_value = value;
}
public TValue Value => IsSuccess
? _value!
: throw new InvalidOperationException("The value of a failure result can't be accessed.");
}
My ValidationBehavior (using MediatR) I define as:
public class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IBaseRequest
where TResponse : Result
{ }
Depending on the request, it either expects a plain Result or a Result<TValue> (e.g., the id of the newly created entity in the db that is returned by a command (public interface ICommand<TResponse> : IRequest<Result<TResponse>> (wrapper around MediatR)). Since the generic constraint says where TResponse : Result, I can know only at runtime what type of Result is expected.
Now if the validation of a command fails (e.g., public sealed record InsertPersonCommand(string Name, int Age) : ICommand<Guid>;), I want to return a Result.Failure(error) but I don't know the type of TValue at compiletime.
Doing this, my code (simplified) compiles and I can run it:
// : Result
public async Task<TResponse> Handle(TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
// validate
var errors = CreateErrorsFromValidationFailures(validationFailures);
if (errors.Any())
{
return (TResponse)Result.Failure(errors);
}
return await next();
}
When my program hits the first return, it throws an exception because it cannot convert MyApp.CoolNamespace.Result (because I used the non-generic method) to MyApp.CoolNamespace.Result<Guid> (TResponse, expected by the command, revealed at runtime).
I have looked at (Instantiate an object with a runtime-determined type) and (How to convert a Task to a Task?) but I don't want to instantiate a Result nor am I dealing with Tasks at that stage.
How can I achieve a generic solution that returns either a Result (e.g., in case of an UpdateCommand) or a Result<TValue> (see example above)?
What I want inside if (errors.Any()) (in pseudo code):
...
if (TResponse is generic)
{
Type tValue = TResponse.GetTypeOfValue();
// this doesn't work with type variables
return Result.Failure<tValue>(errors);
}
else
{
return Result.Failure(errors);
}
...
For C# 11+, you can have static abstract interface methods.
You can declare an
IResultinterface that bothResulttypes implement.Failurewould be a static abstract method in the interfaceNote that
Failureshould not be generic. It can use theTValuetype parameter declared in the class declaration.Then in the validation behaviour, change the constraints for
TResponseto:In
Handle, you can simply dowhenever you want to return a failure.