Search code examples
c#.netrest.net-coredesign-patterns

How should I return the result from a service/use-case class


In a REST API development using C# I've got a problem: If the service used by the controller returns an error, but the error depends on the business rules about something, how and where should I determine what status code to return in the HTTP response?

I mean, should that be the service responsability, or should the controller determine what status code to return based on the service result?

Nowadays at my team, we make the services return the status code and a message, as well as the payload, through a generic wrapper object from a class called Response. So when we wanna return, for example, a list of users, the service does something similar to

if (!users?.Any() ?? true)
    return new Response()
    {
        Message = "There are no users.",
        Status = HttpStatusCodes.Status204NoContent
    }

return new Response()
{
    Message = "Users retrieved successfully!",
    Status = HttpStatusCodes.Status200OK,    
    Data = users
}

However, this way doesn't look quite good to me even though that works, 'cause it's strange calling one method from ServiceA to ServiceB, given the fact an HTTP status code comes together unnecessarily sometimes.

One alternative for that is returning some Result object from the Service to the Controller, but do some data processing in a middleware converting the Result object to an effective Response object containing the status code.

So it could be something like this:

// Result object returned by services
public class Result<T>
{
    public string Message { get; }
    public ResultReason Reason { get; }
    public T Data { get; set; }
}

public enum ResultReason
{
    BusinessRuleViolation, ItemNotFound, Success, InvalidParameter
}

// Response object returned by a middleware as an HTTP response body
public class Response<T>
{
    public string Message { get; }
    public int StatusCode { get; }
    public T Data { get; set; }
}

public static class ResultMapper
{
    public static Response<T> ToResponse<T>(this Result<T> result)
    {
        return new Response()
        {
            Data = result.Data,
            Message = result.Message,
            StatusCode = result.Reason == ResultReason.BusinessRuleViolation ? 422
                : result.Reason == ResultReason.ItemNotFound ? 204
                : result.Reason == ResultReason.Success ? 200
                : result.Reason == ResultReason.InvalidParameter ? 400
        }
    }
}

Preferably, I avoid using exceptions 'cause of performance concerns.

Note: The example above doesn't necessarily work, I just wrote it for demonstration purposes


Solution

  • Use this result class in the service.

        public class Result<T>
    {
        public bool IsSuccess { get; private set; }
        public bool IsFailure { get; private set; }
        public T? Value { get; private set; }
        public string[]? Errors { get; private set; }
    
        private Result() { }
    
        public static Result<T> Success(T value) => new Result<T>() { IsSuccess = true, IsFailure = false, Value = value };
        public static Result<T> Failure(params string[] errors) => new Result<T>() { IsSuccess = false, IsFailure = true, Errors = errors };
    }
    

    The service layer could be used from some other function that is not http aware. So then in the outer layer you can decide what http code to return.