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 Task
s 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 IResult
interface that both Result
types implement. Failure
would be a static abstract method in the interface
public interface IResult<TSelf> {
Error Error { get; }
bool IsSuccess { get; }
static abstract TSelf Failure(Error error);
}
public class Result: IResult<Result>
{
// ...
public static Result Failure(Error error)
=> ...;
}
public class Result<TValue> : IResult<Result<TValue>>
{
// ...
public static Result<TValue> Failure(Error error)
=> ...;
}
Note that Failure
should not be generic. It can use the TValue
type parameter declared in the class declaration.
Then in the validation behaviour, change the constraints for TResponse
to:
where TResponse : IResult<TResponse>
In Handle
, you can simply do
return TResponse.Failure(someError);
whenever you want to return a failure.