Checklist de code review em C#: boas práticas para .Net, ferramentas e exemplos
Um bom code review em C# encontra problemas que compilam sem erro, mas cobram a conta depois.
Tratamento de null baseado em sorte. Código async que bloqueia sob carga. Queries do Entity Framework que funcionam com dez linhas e quebram com dez mil. Objetos IDisposable que vivem mais do que deveriam. Esse é o tipo de problema que um reviewer de C# precisa saber enxergar.
Este checklist serve para revisar pull requests reais em C# e .NET. Use antes de aprovar mudanças em APIs ASP.NET Core, workers, libraries, apps desktop ou qualquer serviço em que corretude importa.
Checklist rápido de code review em C#
Use esta primeira passada quando precisar revisar rápido.
- O código faz o que o PR diz que faz?
- Nullable reference types estão habilitados e respeitados?
- Métodos
asyncsão aguardados comawait, sem.Resultou.Wait()? - O cancelamento é repassado para operações assíncronas longas?
- Recursos
IDisposableeIAsyncDisposablesão descartados corretamente? - Queries do Entity Framework deixam explícito quando carregam dados relacionados?
- Queries LINQ são materializadas só quando necessário?
- Dependências são injetadas em vez de criadas dentro da regra de negócio?
- Exceções são específicas, úteis e seguras para logar?
- Inputs são validados antes de chegar a banco, arquivo, rede ou shell?
- Os testes cobrem o comportamento alterado?
- Questões de estilo são tratadas por analyzers em vez de comentários manuais?
Verifique corretude antes de estilo
Comece pelo comportamento.
Leia a descrição do PR, o ticket e o código alterado em conjunto. Depois, valide se a implementação realmente entrega o que foi proposto. Uma correção de autorização deve refletir no fluxo real de acesso ao endpoint. Uma mudança para evitar processamento duplicado precisa ter alguma forma de garantir idempotência. E uma otimização de performance deve atacar um gargalo conhecido ou, no mínimo, um hot path plausível.
Perguntas para fazer:
- O código corresponde ao comportamento esperado?
- Casos extremos foram cobertos, como coleções vazias, registros ausentes, IDs inválidos, timeouts e retries?
- Essa mudança pode quebrar algum caller existente?
- O nome do método ainda descreve o que ele faz?
- Algum efeito colateral escondido foi introduzido, como escritas extras no banco ou trabalho em background?
Um comentário útil é específico:
// Risco: isso atualiza a invoice antes de o provedor de pagamento confirmar a cobrança.
invoice.Status = InvoiceStatus.Paid;
await paymentProvider.ChargeAsync(invoice.Total);
Feedback melhor:
Isso marca a invoice como paga antes de a cobrança externa terminar. Podemos mover a atualização de status para depois de
ChargeAsync, ou guardar antes um estado separado dePaymentPending?
Revise o tratamento de null
C# ajuda o compilador a encontrar problemas com null, mas só se o projeto usa isso direito.
Confira o .csproj:
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
A documentação da Microsoft descreve nullable reference types como um recurso de compile-time que ajuda a encontrar possíveis NullReferenceException antes da execução. Templates mais novos de .NET já habilitam isso por padrão, mas projetos antigos podem ainda ter isso desligado. Veja o guia da Microsoft sobre nullable reference types.
Procure estes problemas:
!usado para silenciar warnings sem uma garantia real.- Métodos públicos retornando
nullsem tipo de retorno nullable. - DTOs em que campos obrigatórios podem ficar ausentes.
FirstOrDefault()seguido de acesso direto a propriedade.- Lookups em
Dictionaryusando indexer quando a chave pode não existir.
Código problemático:
var customer = customers.FirstOrDefault(c => c.Id == customerId);
return customer.Email.ToLowerInvariant();
Melhor:
var customer = customers.FirstOrDefault(c => c.Id == customerId);
if (customer is null)
{
throw new CustomerNotFoundException(customerId);
}
return customer.Email.ToLowerInvariant();
Se Email pode ser null, o tipo deve dizer isso com string?, e o caller precisa tratar.
Revise async e cancelamento
Código async em C# falha de formas bem sem graça. É por isso mesmo que merece atenção no review.
O padrão async baseado em tasks da Microsoft recomenda Task e Task<TResult> para operações assíncronas, sufixo Async previsível e parâmetros CancellationToken quando a operação aceita cancelamento. Veja o task-based asynchronous pattern do .NET.
Procure:
.Resultou.Wait()dentro de código que poderia ser async.async voidfora de event handlers.awaitausente.- Tasks fire-and-forget sem ownership ou logging.
CancellationTokenaceito em um método, mas abandonado nas chamadas internas.awaitssequenciais em operações independentes que poderiam usarTask.WhenAll.
Código problemático:
public UserProfile GetProfile(Guid userId)
{
var user = _users.GetByIdAsync(userId).Result;
var orders = _orders.GetRecentAsync(userId).Result;
return new UserProfile(user, orders);
}
Melhor:
public async Task<UserProfile> GetProfileAsync(
Guid userId,
CancellationToken cancellationToken)
{
var userTask = _users.GetByIdAsync(userId, cancellationToken);
var ordersTask = _orders.GetRecentAsync(userId, cancellationToken);
await Task.WhenAll(userTask, ordersTask);
return new UserProfile(await userTask, await ordersTask);
}
Em código de library, seja intencional sobre capture de contexto. Em código de aplicação, consistência costuma valer mais do que espalhar ConfigureAwait(false) sem uma regra do time.
Revise descarte de recursos
Tudo que implementa IDisposable precisa ter um tempo de vida claro. Arquivos, streams, timers, conexões de banco e vários objetos de framework não deveriam depender do garbage collector para liberar recursos externos.
A referência de C# da Microsoft explica que um using descarta uma instância IDisposable quando o controle sai do bloco, mesmo se uma exceção ocorrer. Ela também cobre await using para IAsyncDisposable. Veja a documentação do using statement em C#.
Código problemático:
var stream = File.OpenRead(path);
return await JsonSerializer.DeserializeAsync<Order>(stream);
Melhor:
await using var stream = File.OpenRead(path);
return await JsonSerializer.DeserializeAsync<Order>(stream, cancellationToken);
Cuidado com HttpClient. A pergunta no review não é só “ele está sendo descartado?”. Em muitas aplicações .NET, HttpClient deve ser criado via IHttpClientFactory para o pooling de conexões funcionar corretamente.
Revise Entity Framework e LINQ
Um PR em C# pode parecer limpo e ainda assim carregar um problema de query.
Procure loops que disparam chamadas ao banco, navigation properties carregadas por acidente e materialização cedo demais com ToList() ou ToArray(). A documentação do EF Core cobre eager loading com Include e ThenInclude para dados relacionados. Veja eager loading no EF Core.
Código problemático:
var orders = await _db.Orders.ToListAsync(cancellationToken);
foreach (var order in orders)
{
Console.WriteLine(order.Customer.Name);
}
Melhor:
var orders = await _db.Orders
.Include(order => order.Customer)
.Where(order => order.Status == OrderStatus.Open)
.ToListAsync(cancellationToken);
Também confira se a query retorna dados demais. Se o endpoint precisa de cinco campos, projete cinco campos.
var orders = await _db.Orders
.Where(order => order.Status == OrderStatus.Open)
.Select(order => new OrderSummary(
order.Id,
order.Number,
order.Customer.Name,
order.Total))
.ToListAsync(cancellationToken);
Comentários bons aqui são concretos: “isso pode gerar uma query por order” funciona melhor do que “problema de performance”.
Revise injeção de dependência e testabilidade
Em aplicações C#, injeção de dependência costuma ser a diferença entre código testável e código que só dá para cutucar por um endpoint HTTP.
Procure dependências criadas dentro da regra de negócio:
public class InvoiceService
{
public async Task SendInvoiceAsync(Invoice invoice)
{
var client = new SmtpClient();
await client.SendMailAsync(BuildMessage(invoice));
}
}
Melhor:
public class InvoiceService
{
private readonly IEmailSender _emailSender;
public InvoiceService(IEmailSender emailSender)
{
_emailSender = emailSender;
}
public Task SendInvoiceAsync(
Invoice invoice,
CancellationToken cancellationToken)
{
return _emailSender.SendAsync(BuildMessage(invoice), cancellationToken);
}
}
Pergunte:
- Esse código pode ser testado sem rede, banco, relógio ou filesystem reais?
- As dependências estão registradas com o lifetime correto?
- Um serviço scoped está sendo capturado por um singleton?
- A regra de negócio ficou presa dentro de controllers ou handlers?
Revise exceções e logs
Exceções devem explicar a falha sem vazar segredo.
A orientação da Microsoft diz que exceções devem ser lançadas quando um método não consegue completar sua função definida, e que informações sensíveis não devem entrar em mensagens de exceção. Veja o guia da Microsoft sobre criar e lançar exceções.
Procure:
catch (Exception)que engole o erro.- Logs com senhas, tokens, API keys ou request bodies completos.
throw new Exception()em vez de um tipo específico.- Perda do stack trace original com
throw ex;. - Retorno de
nullpara estados de erro que deveriam ser explícitos.
Código problemático:
try
{
await processor.ProcessAsync(command);
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
Melhor:
try
{
await processor.ProcessAsync(command, cancellationToken);
}
catch (PaymentProviderException ex)
{
_logger.LogError(
ex,
"Payment provider failed for order {OrderId}",
command.OrderId);
throw;
}
Se o código consegue se recuperar, recupere de forma clara. Se não consegue, registre contexto e deixe o erro seguir para o caller ou para o handler global.
Revise fronteiras de segurança
Boa parte do review de segurança em C# começa nas fronteiras de input.
Confira dados que entram por:
- Request bodies e route parameters HTTP.
- Query strings.
- Headers.
- Webhooks.
- Arquivos.
- Filas.
- Variáveis de ambiente.
- Painéis admin e ferramentas internas.
Procure:
- SQL montado com concatenação de string.
- Input de usuário passado para caminhos de arquivo.
- Checagens de autorização ausentes em endpoints alterados.
- Over-posting em model binding do ASP.NET Core.
- Segredos escritos em logs.
- Desserialização de payloads não confiáveis sem restrições.
SQL arriscado:
var sql = $"SELECT * FROM Users WHERE Email = '{email}'";
Melhor: use queries parametrizadas ou APIs de query do EF Core.
var user = await _db.Users
.SingleOrDefaultAsync(user => user.Email == email, cancellationToken);
Review de segurança também passa pelos defaults. Um endpoint novo deve deixar a permissão exigida óbvia no código, não escondida em uma convenção que ninguém lembra.
Revise testes
Testes devem provar o comportamento alterado, não só aumentar coverage.
Pergunte:
- O teste falha sem a mudança de produção?
- Ele cobre o caso extremo que motivou o PR?
- Caminhos async são aguardados nos testes?
- Testes com banco estão isolados?
- Tempo, valores aleatórios e serviços externos estão controlados?
- O nome do teste descreve o comportamento?
Exemplo ruim:
[Fact]
public async Task Test1()
É melhor descrever assim:
[Fact]
public async Task CreateOrderAsync_returns_error_when_customer_is_missing()
Para bug fix, procure teste de regressão. Para refactor, procure testes existentes que ainda descrevem o comportamento esperado.
Automatize estilo com analyzers
Tempo de review é caro demais para discutir posição de chave.
Use analyzers e .editorconfig para regras de estilo e qualidade. A documentação de Roslyn analyzers da Microsoft explica que analyzers inspecionam código C# em busca de problemas de estilo, qualidade, manutenibilidade, design e pontos relacionados. Code style analyzers são incluídos no .NET SDK a partir do .NET 5 e podem ser aplicados como warnings ou erros de build. Veja a documentação da Microsoft sobre Roslyn analyzers e configuração de regras com .editorconfig.
Um baseline útil:
[*.cs]
dotnet_diagnostic.CA1822.severity = warning
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
Para times C#, considere:
- .NET SDK analyzers.
.editorconfig.- StyleCop Analyzers.
- Roslynator.
- Sonar analyzers.
- xUnit analyzers para projetos de teste.
- GitHub Actions ou outro passo de CI rodando
dotnet buildedotnet test.
A regra é simples: se uma máquina consegue pegar algo de forma confiável, uma máquina deve pegar antes de uma pessoa abrir o PR.
Ferramentas de code review para C#
Um bom setup de code review em C# combina analyzers, testes no CI e revisão humana. A ferramenta certa depende do tipo de problema que você quer pegar: estilo, bugs, segurança, regressões ou contexto de negócio.
| Ferramenta | Melhor uso em projetos C#/.NET | O que pega bem | Onde entra no fluxo |
|---|---|---|---|
| Kodus | Revisão de PR com IA e regras do time | Problemas de lógica, padrões repetidos, testes ausentes, riscos em mudanças entre arquivos e regras específicas do projeto | Durante o pull request |
| .NET analyzers | Análise nativa do SDK .NET | Warnings de qualidade, estilo e alguns problemas comuns de C# | Build local e CI |
.editorconfig | Padronização de estilo | Naming, formatação, preferências de linguagem e severidade de regras | IDE, build e CI |
| StyleCop Analyzers | Regras mais rígidas de estilo C# | Convenções de código, documentação XML, ordenação e consistência | IDE e CI |
| Roslynator | Refactors e inspeções para C# | Simplificações, smells leves e melhorias idiomáticas | IDE e revisão local |
| SonarQube ou SonarCloud | Qualidade e segurança em nível de projeto | Bugs, code smells, duplicação, vulnerabilidades e hotspots de segurança | CI e quality gate |
| ReSharper ou Rider inspections | Feedback rápido durante desenvolvimento | Refactors, nullability, LINQ, async, dead code e sugestões específicas de C# | IDE |
| xUnit analyzers | Qualidade dos testes | Mau uso de asserts, fixtures, async tests e patterns frágeis em testes | IDE e CI |
| GitHub Actions | Execução automatizada dos checks | dotnet build, dotnet test, analyzers, coverage e security checks | Antes do merge |
A regra prática: use analyzers para o que é determinístico, CI para impedir regressão óbvia e a Kodus para revisar o que depende de contexto do PR, padrões do repositório e intenção da mudança.
O que quem revisa deve bloquear em um PR C#
Nem todo comentário precisa bloquear o merge. Em C#, eu bloquearia o PR quando a mudança introduz risco real de produção.
Bloqueie quando houver:
.Resultou.Wait()em fluxo async que pode gerar deadlock ou prender threads sob carga.async voidfora de event handlers.CancellationTokenignorado em operação longa, request HTTP, query de banco ou job.IDisposableouIAsyncDisposablesem descarte claro.- Query EF Core com risco de N+1 em endpoint ou job relevante.
- Input externo usado em SQL, path de arquivo ou chamada de shell sem validação.
catch (Exception)engolindo erro ou escondendo falha crítica.- Segredo, token ou payload sensível indo para log.
- Mudança de contrato público sem teste ou ajuste nos callers.
- Regra de negócio nova sem teste cobrindo o caminho principal e pelo menos um caso de falha.
Comentários não bloqueantes devem ser tratados como melhoria, não como pedágio. Naming, pequenas preferências de estilo e organização interna só deveriam bloquear quando violam uma regra do projeto ou tornam o código difícil de manter.
Checklist final antes de aprovar
Antes de aprovar um pull request em C#, confira:
- O comportamento bate com a descrição do PR.
- Warnings de nullable não foram ignorados.
- Código async não bloqueia.
- Cancelamento é repassado onde importa.
- Recursos descartáveis têm tempo de vida claro.
- Queries não escondem N+1.
- Dependências são injetáveis.
- Exceções preservam contexto útil.
- Logs não expõem segredos.
- Testes cobrem o comportamento alterado.
- Feedback de estilo é automatizado sempre que possível.
Um bom code review em C# é prático. Ele encontra os problemas que ficam caros depois do merge e são baratos antes dele. Esse é o ponto.