Search code examples
c#genericsinheritancereturn-type

How to return an instance of a derived generic class when a base class is expected using static factory methods?


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);
}
...

Solution

  • 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.