Respostas Unificadas de API no .NET Core

Uma estrutura de resposta padronizada é crucial para manutenção e clareza em APIs. Retornos inconsistentes dificultam a depuração e integração.

Um padrão comum é retornar JSON com uma estrutura fixa, independentemente de erros:

public class ApiResponse<T> where T : class
{
    public ResponseStatusCode StatusCode { get; set; }
    public string Message { get; set; }
    public T Payload { get; set; }
}

Para simplificar a criação, adicione métodos estáticos de fábrica:

public class ApiResponse<T> where T : class
{
    // ... propriedades ...

    private ApiResponse(ResponseStatusCode status, string message, T data)
    {
        StatusCode = status;
        Message = message;
        Payload = data;
    }

    public static ApiResponse<T> Success(T data)
    {
        return new ApiResponse<T>(ResponseStatusCode.Success, "Operação concluída", data);
    }

    public static ApiResponse<T> Failure(string message, ResponseStatusCode code = ResponseStatusCode.Error)
    {
        return new ApiResponse<T>(code, message, default);
    }
}

Exemplo de uso no controlador:

[HttpGet]
public ApiResponse<ProductDto> GetById(int id)
{
    var product = _service.Find(id);
    return ApiResponse<ProductDto>.Success(product);
}

Para capturar exceções não tratadas e formatá-las uniformemante, um middelware de tratamento global pode ser registrado.

// No Program.cs ou Startup.cs
app.UseMiddleware<UnifiedExceptionHandler>();

public class UnifiedExceptionHandler
{
    private readonly RequestDelegate _next;
    private readonly ILogger<UnifiedExceptionHandler> _logger;

    public UnifiedExceptionHandler(RequestDelegate next, ILogger<UnifiedExceptionHandler> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception exception)
        {
            var errorResponse = HandleException(exception);
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsJsonAsync(errorResponse);
        }
    }

    private ApiResponse<object> HandleException(Exception exception)
    {
        // Registrar exceções não esperadas
        if (!(exception is BusinessException))
        {
            _logger.LogError(exception, "Erro inesperado");
        }
        return ApiResponse<object>.Failure(exception.Message);
    }
}

Exceções de negócio epseradas (ex: validação) podem lançar uma exceção customizada para evitar logging desnecessário.

public class BusinessException : Exception
{
    public BusinessException(string message) : base(message) { }
}

Para validar dados de entrada, um filtro de ação pode ser criado para transformar erros de modelo na resposta padrão.

public class RequestValidationFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            var errors = context.ModelState
                .Where(e => e.Value.Errors.Count > 0)
                .SelectMany(e => e.Value.Errors
                    .Select(err => new { Field = e.Key, Error = err.ErrorMessage }))
                .ToList();

            var response = ApiResponse<object>.Failure("Dados de entrada inválidos", ResponseStatusCode.ValidationError);
            response.Payload = errors;
            context.Result = new ObjectResult(response) { StatusCode = (int)HttpStatusCode.BadRequest };
        }
    }

    public void OnActionExecuted(ActionExecutedContext context) { }
}

Registrar o filtro globalmente na configuração dos serviços:

builder.Services.AddControllers(options =>
{
    options.Filters.Add<RequestValidationFilter>();
});

Esta abordagem garante que os consumidores da API sempre recebam uma estrutura de resposta previsível, simplificando o tratamento de erros no lado do cliente.

Tags: ASP.NET Core API JSON middleware Exception Handling

Publicado em 6-11 04:11 por Thomas