vkhorikov/CSharpFunctionalExtensions

[Question] Creating an Error Result or UnitResult from a generic type

SamuelViesselman opened this issue · 6 comments

My goal is to lift some common validation logic into its own behavior to be used with Mediatr's request/response pattern. My validation logic is independent of the response's Result Value type so I would like to return a Result<T, Error> or UnitResult<Error> depending on the response type.

Here's an example of what I'd like to achieve:

public interface IUser {
   Guid UserId { get; }
}

public class LoadUserBehavior<TRequest, TResponse>
        : IPipelineBehavior<TRequest, TResponse>
        where TRequest : IUser
        where TResponse : IError<Error> {
        private readonly State _state;

        public LoadUserBehavior(State state) {
            _state = state;
        }

        public TRespons> Handle(TRequest request) {

            var user = _state.Users.Find(request.UserId);

            if (user is null) {
                /* Create and return user not found Error here */
            }
        }
    }

I'm able to make it work using some reflection magic, but I'd prefer to do more of the checks at compile time.

Is there anything I can do to make this work or anything that could be added to the library?

You should be able to do this as-is. (Note that you'll need to use Result<Unit, TError>, not UnitResult<TError> to generalize the handler signature.

The handler itself should look like this (copying code from my recent project) :

public interface IRequestHandler<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
    where TResponse : IResponse
{
    Task<Result<TResponse>> Handle(TRequest request);
}

@vkhorikov Thanks for the reply. Sorry I don't think I understand your suggestion. Here's a more descriptive example usage that I'm looking for.

I have two request handlers, one that returns a Result<UserProfile, Error> and another that returns a UnitResult<Error> or Result<Unit, Error>. I have common preprocessing code that will prevent the request handlers from running if they fail. I would also like this preprocessor code to be able to compensate with a specific Error.

I would like to be able to create a Result<UserProfile, Error> or UnitResult<Error> without explicitly knowing T's type.

public interface ILoadUser {
   Guid UserId { get; }
}

1st Request Handler:

public class GetUser : ILoadUser {
   public Guid UserId { get; init; }
}

public class GetUserHandler : IRequestHandler<Result<User, Error>, GetUser>
{
   Task<Result<User, Error>> Handle(GetUser request);
}

2nd Request Handler

public class UpdateUser : ILoadUser {
   public Guid UserId { get; init; }
   public string Name { get; init; }
}

public class UpdateUserHandler : IRequestHandler<UnitResult<Error>, UpdateUser>
{
   Task<Result<UserProfile, Error>> Handle(UpdateUser request);
}

Common Preprocessor Code

public class LoadUserBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
   where TRequest : ILoadUser
   where TResponse : IError<Error> 
{

   public TResponse Handle(TRequest request) {
      var user = _state.Users.Find(request.UserId);

       if (user?.Disabled ?? true) {
           /* 
               Return a Result.Failure<UserProfile, Error>(Error.NotFound()) for the 1st request
               Return a UnitResult.Failure<Error>(Error.NotFound()) for the 2st request
               
               Generally be able to return the correct error for any type `T` 
           */
       }
   }
}

My goal is to commonize behaviors across my RequestHandlers without needing to explicitly call the check in every RequestHandler.

Your second handler (UpdateUserHandler) should work with Result<Unit, Error> instead of UnitResult<Error>, otherwise it's hard to commonize methods with different return types.

I haven't looked into MediatR decorators for a long time, so can't say for sure if that would make it work.

Thanks, I agree that it's tricky with the different return types.

Does this point to a need for the library to have a factory method that can create a Failure without knowing the type of the value?

public class Result 
{
    IError<E> CreateErrorForResultType(E error, Type resultType)
}

Usage:

public class LoadUserBehavior<TResponse> : IPipelineBehavior<TResponse>
   where TResponse : IError<Error> 
{

   public TResponse Handle(TRequest request, RequestHandlerDelegate<TResponse> next) {
      var user = _state.Users.Find(request.UserId);

       if (user?.Disabled ?? true) {
           return Result.CreateErrorForResultType(Error.UserNotAvailable(), typeof(TResponse));
       }

      return await next();
   }
}

In my opinion, the fact that we can't do it without using reflection is more of a C# design shortcoming than something that doesn't belong in this library's functionality.

I would try to go this route instead:

1st Request Handler:

public class GetUser : ILoadUser {
   public Guid UserId { get; init; }
}

public class GetUserHandler : IRequestHandler<GetUser, Result<User, Error>>
{
   Task<Result<User, Error>> Handle(GetUser request);
}

2nd Request Handler

public class UpdateUser : ILoadUser {
   public Guid UserId { get; init; }
   public string Name { get; init; }
}

public class UpdateUserHandler : IRequestHandler<UpdateUser, Result<Unit, Error>>
{
   Task<Result<Unit, Error>> Handle(UpdateUser request);
}

Common Preprocessor Code

public class LoadUserBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
   where TRequest : ILoadUser
{

   public Task<Result<TResponse, Error>> Handle(TRequest request) {
      var user = _state.Users.Find(request.UserId);

       if (user?.Disabled ?? true) {
           return Errors.UserIsDisabled();
       }

       return _next(request);
   }
}

Thanks for the response, that makes sense and is probably a better way to go with it.