Jak obsłużyć zwracanie rezultatu w WebAPI

Jakiś czasem temu pisałem o tym, że nasze metody mogą zwracać rezultat lub zgłaszać wyjątek (tutaj). Zastanówmy się, jak mogłoby wyglądać nasze API, aby status rezultatu był mapowany na odpowiedni kod Http.

Nasz rezultat może przyjmować następujące stany:

public enum OperationResultStatus
{
    Success,
    Error,
    Forbidden,
    NotFound
    // ...
}

Załóżmy, że nasz kontroler wygląda następująco:

[HttpGet("{itemId}")]
public async Task<IActionResult> RetrieveByIdAsync(Guid itemId)
{
    var result = await _itemsReadService.RetrieveByIdAsync(itemId);
    return Ok(result.Value);
}

gdzie RetrieveByIdAsync zwraca OperationResult.

Metoda

Niestety w takiej sytuacji nasze API zawsze zwraca Ok. Dzieje się to nawet wtedy, gdy nasza operacja się nie powiedzie (status jest różny od Success). Aby poprawnie obsługiwać również takie przypadki, możemy stworzyć następującą metodę:

protected IActionResult HandleOperationResult(OperationResult result)
{
    switch (result.Status)
    {
        case OperationResultStatus.Success:
            if (result.GetValue() != null)
                return Ok(result.GetValue());
            return NoContent();
        case OperationResultStatus.Forbidden:
            return new ObjectResult(result.Message)
            {
                StatusCode = StatusCodes.Status403Forbidden
            };
        case OperationResultStatus.NotFound:
            return NotFound(result.Message);
        case OperationResultStatus.Error:
            return BadRequest(result.Message);
        // ...
        default:
            throw new ArgumentOutOfRangeException();
    }
}

I zmodyfikować nasz kontroler:

[HttpGet("{itemId}")]
public async Task<IActionResult> RetrieveByIdAsync(Guid itemId)
{
    var result = await _itemsReadService.RetrieveByIdAsync(itemId);
    return HandleOperationResult(result);
}

Takie rozwiązanie jak najbardziej działa. Metoda HandleOperationResult może zostać umieszczona w klasie bazowej i każdy kontroler miałby do niej dostęp. Jedyny minus jest taki, że za każdym razem przy zwracaniu wyniku trzeba pamiętać, aby ją wywołać.

Filtr

Jeśli chcemy, aby status automatycznie mapował się na odpowiedni kod http, możemy stworzyć odpowiednie filtry. W tym celu należy zaimplementować interfejs IAlwaysRunResultFilter lub IResultFilter i problem z głowy (oba te interfejsy mają odpowiedniki Async, ale w tym konkretnym przypadku nie wydają się one potrzebne).

Przykładowa implementacja może wyglądać tak:

public class ResultHandlerFilter : IAlwaysRunResultFilter
{
    public void OnResultExecuted(ResultExecutedContext context)
    {
    }

    public void OnResultExecuting(ResultExecutingContext context)
    {
        if (context != null &&
            context.Result is ObjectResult resultObj &&
            resultObj.Value is OperationResult result)
        {
            context.Result = result.Status switch
            {
                OperationResultStatus.Success => (result.GetValue() != null
                    ? (IActionResult)new OkObjectResult(result.GetValue())
                    : (IActionResult)new NoContentResult()),
                OperationResultStatus.Forbidden => new ObjectResult(result.Message)
                {
                    StatusCode = StatusCodes.Status403Forbidden
                },
                OperationResultStatus.NotFound => new NotFoundObjectResult(result.Message),
                OperationResultStatus.Error => new BadRequestObjectResult(result.Message),
                // ...
                _ => throw new ArgumentOutOfRangeException()
            };
        }
    }
}

Musimy jednak zmienić jeszcze typ zwracany przez nasz kontroler:

[HttpGet("{itemId}")]
public async Task<OperationResult> RetrieveByIdAsync(Guid itemId)
{
    var result = await _itemsReadService.RetrieveByIdAsync(itemId);
    return result;
}

OperationResult

Dla przypomnienia jeszcze przykładowa implementacja OperationResult<T> i OperationResult:

public class OperationResult<T> : OperationResult
{
    public OperationResult(T value, string message, OperationResultStatus status)
        : base(message, status)
    {
        Value = value;
    }

    public T Value { get; }

    public override object GetValue()
    {
        return Value;
    }

    ...
}
public class OperationResult
{
    public OperationResult(string message, OperationResultStatus status)
    {
        Message = message;
        Status = status;
    }

    public string Message { get; }
    public OperationResultStatus Status { get; }

    public virtual object GetValue()
    {
        return null;
    }

    ...
}

1 myśl na “Jak obsłużyć zwracanie rezultatu w WebAPI”

  1. Pingback: dotnetomaniak.pl

Leave a Reply