Skip to main content

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}

TemplateComportamento
{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

AbordagemExemploUso 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ônimanew { ... }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);