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