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,
UseHttpsRedirectioncedo,UseForwardedHeaders/UsePathBaseantes do handler, Serilog apenas viabuilder.Host.UseSeriloglendoappsettings. - Benefícios: respostas
ProblemDetailsconsistentes, 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ção:
ExceptionMiddlewareeGlobalExceptionsFilter(duplicidade e lacunas fora do MVC).
Fique só com oExceptionMiddleware. UseHttpsRedirectiontarde (depois deUseRouting).
Coloque antes deUseRoutingpara redirecionar o quanto antes.- Serilog configurado em dois lugares (manual +
UseSerilog) e “middleware de log custom” adicional.
Centralize embuilder.Host.UseSerilog((ctx, services, cfg) => cfg.ReadFrom.Configuration(ctx.Configuration)). AddLocalization,AddHttpContextAccessor,AddMemoryCacheduplicados.
Deixe uma chamada de cada.UseCorsna posição errada.
Use entreUseRoutingeUseAuthorization.PathBasee inicializações (ex.: FileStorage) antes de carregar.enveappsettings*.
Carregue config primeiro; inicialize depois.
2) Ordem “padrão-ouro”
Services (builder.Services)
- Config:
.env→appsettings.json→appsettings.{Environment}.json→ Environment Variables. - Logging:
builder.Host.UseSerilog((ctx, services, cfg) => cfg.ReadFrom.Configuration(ctx.Configuration)...). - Base:
AddLocalization(...),AddHttpContextAccessor(),AddMemoryCache(). - Auth/Versioning:
AddAuthorization,AddApiVersioning,AddVersionedApiExplorer. - Controllers: sem
GlobalExceptionsFilterglobal. - Infra: Swagger, CORS (policy nomeada), HttpClientFactory, Options/Configure, DI (repos/services/validators).
Middleware (app)
UseForwardedHeaders(se houver proxy)UsePathBase(se necessário)UseHttpsRedirection- ExceptionMiddleware (global)
- (Opcional) RequestLoggingMiddleware (sucesso)
UseRequestLocalizationUseRoutingUseCors("Default")UseAuthenticationUseAuthorizationUseSwagger/UseSwaggerUI(público: logo após Routing; protegido: após Auth)MapControllers()
Não combine
UseExceptionHandler("/error")com seuExceptionMiddleware.
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
-
GlobalExceptionsFilterremovido do registro global. -
ExceptionMiddlewareantes de tudo (apósForwardedHeaders/PathBase). -
UseHttpsRedirectionantes deUseRouting. -
UseCors("Default")entreUseRoutingeUseAuthorization. -
AddLocalization/AddHttpContextAccessor/AddMemoryCache— sem duplicar. -
UseExceptionHandler("/error")não utilizado junto com o handler custom. -
UseForwardedHeadersconfigurado (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!!!
