C# - Protegendo código com novos recursos
![]() |
Hoje veremos alguns recursos do C# moderno que te ajudam a proteger suas invariantes de forma simples. |
O C# tem evoluído constantemente (especialmente após o C# 8 e 10), onde o sistema de tipos e o compilador assumem a responsabilidade de garantir que o estado do objeto seja válido, em vez de depender apenas de múltiplos if (x == null) espalhados pelo código
Hoje vou apresentar alguns destes recursos mostrando como eles te ajudam a proteger seu código de forma simpls.
1- Membros obrigatórios (Required Members)
Por muito tempo, o C# não tinha uma boa maneira de dizer “esta propriedade deve ser definida”. Construtores ajudavam, mas uma vez que os inicializadores de objeto se tornaram comuns, era fácil criar objetos inválidos acidentalmente.
var usuario = new Usuario { Nome = "Maria" // Email esquecido }; |
O compilador ficava perfeitamente satisfeito. Seu código em tempo de execução,
não. O modificador
required corrige isso no nível da
linguagem.
public class Usuario { public required string Nome { get; init; } public required string Email { get; init; } } |
Agora este código vai falhar em tempo de compilação:
var usuario = new Usuario { Nome = "Maria" }; |
Isso não é apenas "açúcar sintático". Ele muda fundamentalmente
como você modela invariantes. Em vez de esperar que quem chama o código lembre
do que é importante, você codifica isso diretamente no sistema de tipos.
Se você já adicionou verificações de nulo no fundo do seu código porque
“alguém pode esquecer de definir isso”, os membros required
são a correção que você realmente queria.
2- Setters apenas de inicialização (Init-Only Setters)
Os objetos mutáveis são convenientes e são também a forma como as regras de negócio silenciosamente se degradam. O init fica no meio termo: ele permite que valores sejam definidos durante a construção, mas não depois.
public class Pedido { public Guid Id { get; init; } public DateTime CriadoEm { get; init; } } |
Este código funciona:
var pedido = new Pedido { Id = Guid.NewGuid(), CriadoEm = DateTime.UtcNow }; |
Já este código não funciona
| pedido.CriadoEm = DateTime.UtcNow.AddDays(-1); |
A propriedade só pode receber um valor dentro do construtor da classe ou através de um inicializador de objeto. Uma vez que o objeto termina de ser criado e a execução sai das chaves ({...}) a propriedade torna-se somente-leitura.
Agora, qando combinado com required, isso se torna extremamente poderoso. Você pode definir objetos que devem ser totalmente inicializados, de forma correta, exatamente uma vez. Isso simplifica drasticamente o raciocínio sobre o estado, especialmente em sistemas maiores onde os objetos transitam entre camadas.
Exemplo:
public class Pedido { // O compilador obriga a atribuição e o 'init' impede a alteração posterior public required Guid Id { get; init; } public required DateTime CriadoEm { get; init; }
// Propriedade opcional: pode ser definida na criação, mas nunca alterada depois public string? Observacao { get; init; } } |
A combinação de required + init resolve o problema dos
construtores gigantes.
Antigamente, para garantir que um objeto fosse
válido e imutável, você precisava de um construtor com 10 parâmetros. Com essa
combinação:
Legibilidade: Você usa a sintaxe de inicializador de objeto
({ Prop = Valor }), que é muito mais clara que uma lista longa de
argumentos no construtor.
Segurança (Fail-fast): O erro acontece
em tempo de compilação, não em tempo de execução. Você elimina o risco de
NullReferenceException por esquecimento de preenchimento.
Contrato Claro: Quem consome sua classe sabe exatamente o que é
indispensável para que o objeto exista de forma íntegra.
Por que init não é o mesmo que
private set. ?
À primeira vista, o init pode
parecer uma sintaxe mais bonita para o private set. Ambos
parecem impedir a mutação externa, mas resolvem problemas diferentes em pontos
distintos da vida de um objeto.
Com private set, a
propriedade ainda é mutável dentro da classe, a qualquer momento.
public class Pedido { public DateTime CriadoEm { get; private set; } public void Recalcular() { CriadoEm = DateTime.UtcNow; // perfeitamente legal } } |
Isso significa que o valor pode mudar muito depois da construção, possivelmente
em resposta a uma lógica não relacionada. O compilador não te ajuda aqui. Se
essa mutação é válida ou não, é puramente uma questão de disciplina.
O
init muda as regras inteiramente. Ele garante que o objeto seja
mutável apenas enquanto está sendo "montado". Depois de pronto, ele se comporta
como um objeto imutável para essas propriedades.
|
public class Pedido { public DateTime CriadoEm { get; init; } } |
3- ConfigureAwaitOptions
O ConfigureAwaitOptions é uma funcionalidade introduzida no .NET 8 para dar mais flexibilidade e clareza ao controle de contextos assíncronos.
Ele existe há anos, mas sempre foi um instrumento bruto. Ou você capturava o
contexto ou não. No .NET 8, o
ConfigureAwaitOptions torna a intenção explícita.
Antigamente, você só tinha o método ConfigureAwait(bool continueOnCapturedContext). Se passasse false, você evitava voltar para o contexto original (útil para evitar deadlocks em aplicações UI ou melhorar performance em bibliotecas). No entanto, o .NET 8 transformou isso em um Enum com sinalizadores (Flags), permitindo combinar diferentes comportamentos.
| await AlgumTrabalhoAssincrono().ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); |
Esta opção altera como as exceções fluem através do await,
permitindo um tratamento mais seguro em alguns cenários avançados, especialmente
em códigos de infraestrutura ou frameworks.
Mais importante ainda, a
existência de opções força você a pensar sobre o porquê de estar configurando os
awaits. Você está evitando deadlocks? Escrevendo código de
biblioteca? Suprimindo intencionalmente o comportamento do contexto?
Imagine que você está escrevendo um código de infraestrutura onde deseja evitar o contexto original e também quer que a execução seja forçadamente assíncrona para não bloquear a thread principal:
using System.Threading.Tasks; public async Task ProcessarDadosAssincrono() { // Combinando opções: não captura contexto E força execução assíncrona await Task.Delay(100).ConfigureAwait(ConfigureAwaitOptions.ContinueOnCapturedContext | ConfigureAwaitOptions.ForceAsync); // Código que executa após o await
}
|
Por que usar este recurso no lugar do ConfigureAwait(false) ?
O antigo ConfigureAwait(false) era o que chamamos de "instrumento
cego". Ele apenas dizia: "não me importo com o contexto".
Com o
ConfigureAwaitOptions, o código fica autoexplicativo. Se você
usa ConfigureAwaitOptions.SuppressThrowing, qualquer
desenvolvedor que ler seu código entenderá imediatamente que você está tratando
exceções de uma forma especial naquele ponto, algo que um simples false não
comunicava.
4- CallerArgumentExpression
O CallerArgumentExpression é um atributo introduzido no C# 10
que permite que o compilador capture o texto literal (o código-fonte)
de um argumento passado para um método.
Em termos simples: ele permite
que o seu código saiba exatamente o que o desenvolvedor escreveu entre os
parênteses na hora de chamar uma função.
O uso principal é para
validação e diagnósticos. Antes desse recurso, se você quisesse lançar uma
exceção detalhada, precisava repetir manualmente o nome da variável ou usar
reflexão/nameof, o
que era limitado.
Assim, por anos, a validação de argumentos significava repetir nomes de parâmetros ou escrever mensagens de erro vagas.
| if (usuario == null)
throw new ArgumentNullException(nameof(usuario)); |
Isso funciona, mas falha rapidamente com expressões mais complexas.
| Validar(usuario.Email); |
O que falhou? Qual valor era inválido? O
CallerArgumentExpression resolve isso deixando o compilador te
dizer qual expressão foi passada.
do de
void Validar( string valor, [CallerArgumentExpression(nameof(valor))] string? expressao = null) { if (string.IsNullOrWhiteSpace(valor)) throw new ArgumentException($"Valor inválido: {expressao}"); } |
Agora este código:
| Validar(usuario.Email); |
Produz uma mensagem significativa ("Valor inválido: usuario.Email") sem esforço extra. Este recurso é pequeno, mas seu impacto é real. Ele torna as cláusulas de guarda (guard clauses), ajudantes de validação e asserções de depuração dramaticamente mais úteis sem sacrificar a legibilidade.
5- Atributos de nulabilidade e Análise de fluxo
O C# moderno tenta nos ajudar a evitar o famoso erro
NullReferenceException. No entanto, o compilador não é "vidente";
ele analisa o fluxo de dados.
Quando você tem um método que retorna um
booleano para indicar
sucesso e usa um parâmetro
out para devolver o
objeto, o compilador fica confuso sem ajuda extra
Os tipos de referência anuláveis são tão bons quanto a informação que você fornece ao compilador. Por padrão, o compilador é conservador. Atributos de nulabilidade são como você ensina a ele sobre sua intenção.
Imagine o método sem o atributo:
|
// Sem o atributo NotNullWhen bool TentarObterUsuario(int id, out Usuario? usuario) { // se encontrar, preenche usuario e retorna true // se não encontrar, usuario é null e retorna false } |
Quando você usa esse método, o compilador pensa assim: "A variável usuario pode
ser nula, pois o tipo é Usuario?. Mesmo que o método retorne
true, eu não tenho certeza absoluta de que o desenvolvedor preencheu a
variável."
Por isso, ele exibe um Aviso (Warning) se
você tentar usar o usuário logo após o if, forçando você a fazer um if
(usuario != null) desnecessário.
O atributo
[NotNullWhen(true)] é
uma instrução explícita para o motor de análise do C#. Você está dizendo ao
compilador:
"Ei, compilador, eu garanto que se este método retornar
true, o
parâmetro
usuario
não será nulo,
mesmo que o tipo dele permita nulos."
Por que isso é importante?
Elimina Avisos Falsos: O
compilador para de reclamar de algo que você sabe que está seguro.
Inteligência no Fluxo: Se o método retornar false, o compilador
continuará te avisando que o usuário pode ser nulo, o que é correto!
Contrato Claro: Quem consome seu código (ou sua biblioteca)
sabe exatamente como lidar com o retorno sem precisar ler todo o código interno
da função.
Exemplo de Comparação:
Sem o Atributo:
| if
(TentarObterUsuario(1, out var usuario)) { // O compilador exibe um Warning aqui: // "Possível desreferência de uma referência nula" usuario.FazerAlgo(); } |
Com o atributo:
if (TentarObterUsuario(1, out var usuario)) { // O compilador exibe um Warning aqui: // "Possível desreferência de uma referência nula" usuario.FazerAlgo(); } |
Sem o atributo, ou você recebe avisos ou adiciona verificações defensivas que não agregam valor real. Eles são especialmente valiosos em APIs públicas e bibliotecas compartilhadas, onde suposições se espalham rápida e silenciosamente.
Conclusão
O que une esses recursos não é a sintaxe moderna ou a evolução da linguagem pela
evolução em si. É uma mudança clara de filosofia. Nenhum desses recursos torna
seu código "esperto". Eles o tornam mais difícil de ser usado
incorretamente, mais difícil de ser mal interpretado e mais fácil de manter sob
pressão.
E essa é geralmente a diferença entre um código que parece limpo
hoje e um código que ainda parece sólido daqui a um ano.
E estamos conversados...
"Mas o Senhor Deus é a verdade; ele mesmo é o Deus vivo e o Rei eterno; ao seu
furor treme a terra, e as nações não podem suportar a sua indignação."
Jeremias 10:10
Referências:
NET - Unit of Work - Padrão Unidade de ...