Lidando com parâmetros e objetos | ILogger no .NET | CSharp
- Lidando com parâmetros e objetos | ILogger no .NET | CSharp
No ILogger, o padrão recomendado é usar message templates, não interpolação de string.
Isso permite que os logs sejam estruturados, pesquisáveis e melhor integrados com ferramentas como Seq, Application Insights, OpenTelemetry, Datadog, Grafana Loki etc.
O livro Modern Distributed Tracing in .NET aborda ILogger, otimização de logs, logs estruturados e captura com OpenTelemetry no capítulo Writing Structured and Correlated Logs.
1. Forma ruim: interpolação de string
logger.LogInformation($"Pedido {pedido.Id} criado para o cliente {pedido.ClienteId}");
Problema:
Pedido 10 criado para o cliente 55
O log vira apenas texto. A ferramenta de observabilidade não entende que existem campos como:
PedidoId
ClienteId
2. Forma correta: message template
logger.LogInformation(
"Pedido {PedidoId} criado para o cliente {ClienteId}",
pedido.Id,
pedido.ClienteId);
Resultado conceitual:
{
"Message": "Pedido 10 criado para o cliente 55",
"PedidoId": 10,
"ClienteId": 55
}
Agora é possível pesquisar por:
PedidoId = 10
ClienteId = 55
3. O problema ao passar objeto inteiro
Exemplo:
logger.LogInformation("Pedido criado: {Pedido}", pedido);
Dependendo do provider, o objeto pode ser convertido usando ToString().
Se a classe não sobrescrever ToString(), o resultado pode ser ruim:
Pedido criado: MeuProjeto.Pedido
Ou seja, o log mostra apenas o nome do tipo, não os dados do objeto.
4. Exemplo com class sem ToString
public sealed class Pedido
{
public Guid Id { get; init; }
public string ClienteId { get; init; } = string.Empty;
public decimal ValorTotal { get; init; }
}
Uso:
Pedido pedido = new()
{
Id = Guid.NewGuid(),
ClienteId = "123",
ValorTotal = 250.90m
};
logger.LogInformation("Pedido criado: {Pedido}", pedido);
Saída possível:
Pedido criado: MeuProjeto.Pedido
Isso acontece porque a implementação padrão de ToString() em uma classe retorna o nome completo do tipo.
5. Resolvendo com ToString
Você pode sobrescrever o método ToString():
public sealed class Pedido
{
public Guid Id { get; init; }
public string ClienteId { get; init; } = string.Empty;
public decimal ValorTotal { get; init; }
public override string ToString()
{
return $"Id={Id}, ClienteId={ClienteId}, ValorTotal={ValorTotal}";
}
}
Agora:
logger.LogInformation("Pedido criado: {Pedido}", pedido);
Saída:
Pedido criado: Id=7b21..., ClienteId=123, ValorTotal=250.90
6. Quando usar ToString
Use ToString() quando você quer uma representação textual simples do objeto.
Bom para:
Value Objects
Objetos pequenos
Objetos sem dados sensíveis
Objetos usados em mensagens de log simples
Exemplo:
public sealed class Cnpj
{
public string Valor { get; }
public Cnpj(string valor)
{
Valor = valor;
}
public override string ToString()
{
return Valor;
}
}
Uso:
logger.LogInformation("Cliente conectado. Cnpj: {Cnpj}", cnpj);
Saída:
Cliente conectado. Cnpj: 12345678000199
7. Cuidado com ToString
Não coloque dados sensíveis no ToString().
Evite:
public override string ToString()
{
return $"Usuario={Usuario}, Senha={Senha}, Token={Token}";
}
Problema:
Vaza senha
Vaza token
Vaza segredo
Pode expor dados pessoais
Prefira:
public override string ToString()
{
return $"Usuario={Usuario}";
}
8. Usando record para melhorar o ToString
Diferente de uma class comum, um record já gera um ToString() mais útil automaticamente.
Exemplo:
public sealed record Pedido(
Guid Id,
string ClienteId,
decimal ValorTotal);
Uso:
Pedido pedido = new(
Guid.NewGuid(),
"123",
250.90m);
logger.LogInformation("Pedido criado: {Pedido}", pedido);
Saída aproximada:
Pedido criado: Pedido { Id = 7b21..., ClienteId = 123, ValorTotal = 250.90 }
Isso já resolve o problema clássico de passar o objeto inteiro e receber apenas:
MeuProjeto.Pedido
9. Record é útil para DTOs pequenos
Exemplo:
public sealed record ComandoLogDto(
Guid RequestId,
string Cnpj,
string NomeFila,
int TamanhoSql);
Uso:
ComandoLogDto log = new(
dto.RequestId,
cnpj,
dto.NomeFila,
dto.Sql?.Length ?? 0);
logger.LogInformation("Comando recebido: {Comando}", log);
Saída:
Comando recebido: ComandoLogDto { RequestId = ..., Cnpj = ..., NomeFila = ..., TamanhoSql = 1500 }
10. Record com ToString customizado
Mesmo usando record, voc ê pode sobrescrever o ToString() quando quiser controlar melhor a saída.
public sealed record ComandoLogDto(
Guid RequestId,
string Cnpj,
string NomeFila,
int TamanhoSql)
{
public override string ToString()
{
return $"RequestId={RequestId}, Cnpj={Cnpj}, NomeFila={NomeFila}, TamanhoSql={TamanhoSql}";
}
}
Uso:
logger.LogInformation("Comando recebido: {Comando}", comandoLog);
Saída:
Comando recebido: RequestId=..., Cnpj=..., NomeFila=fila-a, TamanhoSql=1500
11. {Objeto} vs {@Objeto}
| Template | Comportamento |
|---|---|
{Pedido} | Usa representação textual, geralmente ToString() |
{@Pedido} | Em providers compatíveis, quebra o objeto em propriedades |
{$Pedido} | Em alguns providers, força stringificação |
Exemplo:
logger.LogInformation("Pedido como texto: {Pedido}", pedido);
logger.LogInformation("Pedido estruturado: {@Pedido}", pedido);
Com {Pedido}, você depende da saída textual do objeto.
Com {@Pedido}, providers como Serilog tentam registrar as propriedades internas do objeto.
12. Exemplo com destructuring
logger.LogInformation("Pedido criado: {@Pedido}", pedido);
Saída estruturada esperada:
{
"Pedido": {
"Id": "7b21...",
"ClienteId": "123",
"ValorTotal": 250.90
}
}
Isso é melhor para consulta em ferramentas de observabilidade.
13. Melhor prática para objetos complexos
Mesmo com record, ToString() ou {@Objeto}, não é ideal logar entidade inteira sem critério.
Evite:
logger.LogInformation("Request recebido: {@Request}", request);
Principalmente se o objeto contém:
Senha
Token
Authorization header
CPF/CNPJ
Cartão
Payload muito grande
SQL sensível
Dados pessoais
Prefira criar uma projeção segura:
logger.LogInformation(
"Comando recebido: {@Comando}",
new
{
dto.RequestId,
Cnpj = cnpj,
dto.NomeFila,
TamanhoSql = dto.Sql?.Length ?? 0
});
14. Exemplo prático usando record para log
public sealed record ComandoRecebidoLog(
Guid RequestId,
string Cnpj,
string NomeFila,
int TamanhoSql);
Uso:
app.MapPost("/envio-comando/{cnpj}", async (
string cnpj,
RequisicaoConteudoDto dto,
ILogger<Program> logger,
CancellationToken ct) =>
{
dto.CriarIdentificadorRequest();
ComandoRecebidoLog log = new(
dto.RequestId,
cnpj,
dto.NomeFila,
dto.Sql?.Length ?? 0);
logger.LogInformation("Solicitação de comando recebida: {Comando}", log);
return Results.Ok(new
{
dto.RequestId
});
});
Saída aproximada:
Solicitação de comando recebida: ComandoRecebidoLog { RequestId = ..., Cnpj = ..., NomeFila = ..., TamanhoSql = 1500 }
15. Exemplo com log estruturado e record
Se estiver usando um provider com suporte a destructuring, como Serilog:
logger.LogInformation("Solicitação de comando recebida: {@Comando}", log);
Saída estruturada:
{
"Comando": {
"RequestId": "...",
"Cnpj": "...",
"NomeFila": "fila-a",
"TamanhoSql": 1500
}
}
Esse formato é melhor para pesquisa por campo.
16. Exemplo com erro
Forma recomendada:
try
{
await ProcessarAsync(dto, ct);
}
catch (Exception ex)
{
logger.LogError(
ex,
"Erro ao processar comando. RequestId: {RequestId}, NomeFila: {NomeFila}",
dto.RequestId,
dto.NomeFila);
throw;
}
Não faça:
logger.LogError($"Erro ao processar comando: {ex.Message}");
Problemas:
Perde stack trace
Perde estrutura
Dificulta filtro por RequestId
Dificulta correlação
17. Comparativo direto
| Abordagem | Exemplo | Uso recomendado |
|---|---|---|
| Interpolação | $"Pedido {pedido.Id}" | Evitar |
| Message template | "Pedido {PedidoId}" | Recomendado |
| Classe sem ToString | {Pedido} | Pode gerar apenas nome do tipo |
| Classe com ToString | {Pedido} | Bom para texto controlado |
| Record | {Pedido} | Bom para DTOs pequenos |
| Destructuring | {@Pedido} | Bom para log estruturado |
| Projeção anônima | new { ... } | Melhor para produção |
18. Recomendação prática
Para objeto pequeno e seguro, record resolve bem:
public sealed record PedidoLog(
Guid Id,
string ClienteId,
decimal ValorTotal);
logger.LogInformation("Pedido criado: {Pedido}", pedidoLog);
Para log estruturado:
logger.LogInformation("Pedido criado: {@Pedido}", pedidoLog);
Para classe comum, implemente ToString() se quiser uma saída textual decente:
public override string ToString()
{
return $"Id={Id}, ClienteId={ClienteId}, ValorTotal={ValorTotal}";
}
Para produção, prefira projeção segura:
logger.LogInformation(
"Pedido criado: {@Pedido}",
new
{
pedido.Id,
pedido.ClienteId,
pedido.ValorTotal
});
Resumo direto
Use message template:
logger.LogInformation(
"Pedido {PedidoId} criado para o cliente {ClienteId}",
pedido.Id,
pedido.ClienteId);
Se quiser passar objeto inteiro com classe comum, sobrescreva ToString():
public override string ToString()
{
return $"Id={Id}, ClienteId={ClienteId}, ValorTotal={ValorTotal}";
}
Se for um DTO simples, prefira record:
public sealed record PedidoLog(Guid Id, string ClienteId, decimal ValorTotal);
E use:
logger.LogInformation("Pedido criado: {Pedido}", pedidoLog);
Para log estruturado, use:
logger.LogInformation("Pedido criado: {@Pedido}", pedidoLog);