Agradecimento GEP

Program.cs, Logging e Tratamento Global de Exceções — o guia para colocar a casa em ordem

Objetivo: padronizar o pipeline da CoreAPI (.NET), eliminar duplicidades, centralizar o tratamento de erros no middleware, configurar logs úteis no Console/VS e disponibilizar endpoints de teste para validar cada exceção.

Resumo

  • Problemas encontrados: ordem incorreta de middlewares, filtros e handlers globais duplicados, CORS no lugar errado, inicializações antes da configuração, Serilog híbrido (manual + host).
  • Correção: um handler global de exceções (middleware), ordem “padrão-ouro” para Services + Middleware, CORS entre Routing e Auth, UseHttpsRedirection cedo, UseForwardedHeaders/UsePathBase antes do handler, Serilog apenas via builder.Host.UseSerilog lendo appsettings.
  • Benefícios: respostas ProblemDetails consistentes, logs com contexto útil (Path, Method, User, IP, RequestId), menos ruído, diagnósticos simples.

1) O que estava errado (e por quê)

  • Dois handlers globais de exceçãoExceptionMiddleware e GlobalExceptionsFilter (duplicidade e lacunas fora do MVC).
    Fique só com o ExceptionMiddleware.
  • UseHttpsRedirection tarde (depois de UseRouting).
    Coloque antes de UseRouting para redirecionar o quanto antes.
  • Serilog configurado em dois lugares (manual + UseSerilog) e “middleware de log custom” adicional.
    Centralize em builder.Host.UseSerilog((ctx, services, cfg) => cfg.ReadFrom.Configuration(ctx.Configuration)).
  • AddLocalizationAddHttpContextAccessorAddMemoryCache duplicados.
    Deixe uma chamada de cada.
  • UseCors na posição errada.
    Use entre UseRouting e UseAuthorization.
  • PathBase e inicializações (ex.: FileStorage) antes de carregar .env e appsettings*.
    Carregue config primeiro; inicialize depois.

2) Ordem “padrão-ouro”

Services (builder.Services)

  1. Config.env → appsettings.json → appsettings.{Environment}.json → Environment Variables.
  2. Loggingbuilder.Host.UseSerilog((ctx, services, cfg) => cfg.ReadFrom.Configuration(ctx.Configuration)...).
  3. BaseAddLocalization(...)AddHttpContextAccessor()AddMemoryCache().
  4. Auth/VersioningAddAuthorizationAddApiVersioningAddVersionedApiExplorer.
  5. Controllerssem GlobalExceptionsFilter global.
  6. Infra: Swagger, CORS (policy nomeada), HttpClientFactory, Options/Configure, DI (repos/services/validators).

Middleware (app)

  1. UseForwardedHeaders (se houver proxy)
  2. UsePathBase (se necessário)
  3. UseHttpsRedirection
  4. ExceptionMiddleware (global)
  5. (Opcional) RequestLoggingMiddleware (sucesso)
  6. UseRequestLocalization
  7. UseRouting
  8. UseCors("Default")
  9. UseAuthentication
  10. UseAuthorization
  11. UseSwagger / UseSwaggerUI (público: logo após Routing; protegido: após Auth)
  12. MapControllers()

Não combine UseExceptionHandler("/error") com seu ExceptionMiddleware.

3) Program.cs — esqueleto corrigido (colar/adaptar)

using Serilog;
using Microsoft.AspNetCore.HttpOverrides;

var builder = WebApplication.CreateBuilder(args);

// 1) Config
DotNetEnv.Env.Load();
builder.Configuration
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
    .AddEnvironmentVariables();

// 2) Serilog centralizado
builder.Host.UseSerilog((ctx, services, cfg) =>
{
    cfg.ReadFrom.Configuration(ctx.Configuration)
       .Enrich.FromLogContext()
       .Enrich.WithProperty("App", "CoreAPI")
       .Enrich.WithProperty("Environment", ctx.HostingEnvironment.EnvironmentName);
});

// 3) Services
builder.Services.AddLocalization(o => o.ResourcesPath = "Resources");
builder.Services.AddHttpContextAccessor();
builder.Services.AddMemoryCache();

builder.Services.AddAuthorization();
builder.Services.AddApiVersioning(o => o.AssumeDefaultVersionWhenUnspecified = true);
builder.Services.AddVersionedApiExplorer(o => { o.GroupNameFormat = "'v'VVV"; o.SubstituteApiVersionInUrl = true; });

builder.Services.AddControllers(o =>
{
    // REMOVER global filters de exceção
    // o.Filters.Add<GlobalExceptionsFilter>();
});

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddCors(o =>
{
    o.AddPolicy("Default", p => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
});

builder.Services.AddHttpClient("Default", c => { c.Timeout = TimeSpan.FromSeconds(100); });

// DI (exemplos)
// builder.Services.AddScoped<IAnalysisRepository, AnalysisRepository>();
// ...

// 4) Inits que dependem de config
FileStorageProviderFactory.Initialize(builder.Configuration);

var app = builder.Build();

// 5) Pipeline
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost,
    ForwardLimit = 2
    // KnownProxies/KnownNetworks conforme seu ambiente
});

app.UsePathBase("/coreapi");       // se necessário
app.UseHttpsRedirection();

app.UseMiddleware<ExceptionMiddleware>();       // handler global
// app.UseMiddleware<RequestLoggingMiddleware>(); // opcional: logs de sucesso

app.UseRequestLocalization();
app.UseRouting();

app.UseCors("Default");
app.UseAuthentication();
app.UseAuthorization();

// Swagger — público logo após routing, ou proteja movendo após Auth
app.UseSwagger();
app.UseSwaggerUI();

app.MapControllers();

app.Run();

4) Exceções personalizadas (Domain/Application)

namespace Core.Domain.Exceptions;

public abstract class AppException : Exception
{
    protected AppException(string message, string? code = null, Exception? inner = null)
        : base(message, inner) => Code = code;
    public string? Code { get; }
}

// 4xx
public sealed class ValidationAppException   : AppException { public ValidationAppException(string m, string? c=null) : base(m, c){} }
public sealed class NotFoundAppException     : AppException { public NotFoundAppException(string m, string? c=null) : base(m, c){} }
public sealed class ConflictAppException     : AppException { public ConflictAppException(string m, string? c=null) : base(m, c){} }
public sealed class UnauthorizedAppException : AppException { public UnauthorizedAppException(string m, string? c=null) : base(m, c){} }
public sealed class ForbiddenAppException    : AppException { public ForbiddenAppException(string m, string? c=null) : base(m, c){} }

// 5xx
public sealed class BusinessRuleAppException    : AppException { public BusinessRuleAppException(string m, string? c=null) : base(m, c){} }
public sealed class ExternalServiceAppException : AppException { public ExternalServiceAppException(string m, string? c=null, Exception? inner=null) : base(m, c, inner){} }

Mantém o domínio limpo e permite mapear claramente HTTP 4xx/5xx no middleware.

5) ExceptionMiddleware (mapa HTTP + ProblemDetails + redaction)

Se já estiver implementado, valide o mapeamento; senão, use este como base.

  • 400 → ValidationAppException
  • 401 → UnauthorizedAppException
  • 403 → ForbiddenAppException
  • 404 → NotFoundAppException
  • 409 → ConflictAppException
  • 422 → BusinessRuleAppException
  • 502 → ExternalServiceAppException
  • 500 → demais

Inclua:

  • EnableBuffering() para ler o body com segurança;
  • redaction (password, token, cpf etc.);
  • truncamento do body nos logs (ex.: 64 KB);
  • ProblemDetails (RFC 7807) como resposta (application/problem+json).

6) Endpoints de teste (somente DEV)

#if DEBUG
using Core.Domain.Exceptions;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("dev/ex")]
[ApiExplorerSettings(IgnoreApi = true)]
public sealed class DevExceptionsController : ControllerBase
{
    [HttpGet("validation")]   public IActionResult Validation()   => throw new ValidationAppException("Invalid payload", "VAL001");
    [HttpGet("notfound")]     public IActionResult NotFound_()    => throw new NotFoundAppException("Entity not found", "NF001");
    [HttpGet("conflict")]     public IActionResult Conflict_()    => throw new ConflictAppException("Version conflict", "CON001");
    [HttpGet("unauthorized")] public IActionResult Unauthorized_()=> throw new UnauthorizedAppException("No token", "AUTH001");
    [HttpGet("forbidden")]    public IActionResult Forbidden_()   => throw new ForbiddenAppException("No permission", "AUTH002");
    [HttpGet("business")]     public IActionResult Business()     => throw new BusinessRuleAppException("Rule violated", "BR001");
    [HttpGet("external")]     public IActionResult External()     => throw new ExternalServiceAppException("Upstream failed", "EXT001");
    [HttpGet("generic")]      public IActionResult Generic()      => throw new Exception("Unhandled boom");
}
#endif

Teste rápido (HTTP esperado):

/dev/ex/validation   → 400
/dev/ex/unauthorized → 401
/dev/ex/forbidden    → 403
/dev/ex/notfound     → 404
/dev/ex/conflict     → 409
/dev/ex/business     → 422
/dev/ex/external     → 502
/dev/ex/generic      → 500

7) Serilog — ver no Console/VS

Pacotes:

Serilog.AspNetCore
Serilog.Settings.Configuration
Serilog.Sinks.Console
Serilog.Sinks.Debug

appsettings.Development.json:

{
  "Serilog": {
    "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.Debug" ],
    "MinimumLevel": { "Default": "Information", "Override": { "Microsoft": "Warning", "System": "Warning" } },
    "Enrich": [ "FromLogContext" ],
    "WriteTo": [
      { "Name": "Console" },
      { "Name": "Debug" }
    ]
  }
}

No VS: View → Output → Debug (sink Debug).

8) Checklist de revisão

  •  GlobalExceptionsFilter removido do registro global.
  •  ExceptionMiddleware antes de tudo (após ForwardedHeaders/PathBase).
  •  UseHttpsRedirection antes de UseRouting.
  •  UseCors("Default") entre UseRouting e UseAuthorization.
  •  AddLocalization/AddHttpContextAccessor/AddMemoryCache — sem duplicar.
  •  UseExceptionHandler("/error") não utilizado junto com o handler custom.
  •  UseForwardedHeaders configurado (X-Forwarded-For/Proto/Host) e, se possível, KnownProxies/Networks.
  •  Endpoints /dev/ex/* habilitados só em DEV (ou protegidos).

9) Por que compartilhar isso com o time?

  • Reduz MTTR: erros padronizados + logs consistentes agilizam diagnóstico.
  • Evita regressões: guia explica o porquê da ordem correta.
  • Transferência de conhecimento: facilita continuidade do trabalho mesmo após mudanças no time.
  • Conformidade: estrutura alinhada a boas práticas (RFC 7807, princípio de responsabilidade única, logs com contexto e redaction).

Especial agradecimento a todos da GEP pelo carinho, amizade e parceria. Foi uma fase incrível e levarei comigo as boas lembranças de todos vocês!!!