Fale conosco
Desenvolvimento

C# 15 e .NET 11: union types e classes closed acabam com o `_ =>` que esconde caso novo

32 min de leitura
C# 15 e .NET 11: union types e classes closed acabam com o `_ =>` que esconde caso novo

C# 15 e .NET 11: union types e classes closed acabam com o _ => que esconde caso novo

Em 10 de novembro, durante o .NET Conf 2026, a Microsoft liberou o .NET 11 e o C# 15 — que passa a ser a versão padrão da linguagem em projetos que miram o novo framework. O release é Standard Term Support: 24 meses de patch, até novembro de 2028. No mesmo dia, .NET 8 e .NET 9 perderam suporte, e o .NET 10 continua como LTS com a mesma data final de cobertura. Ou seja: quem migrou para o .NET 10 no mês passado não precisa correr.

O que interessa aqui não é a corrida de versão, e sim duas features que mudam a relação do compilador com o seu switch : union types (o union keyword, pedido há mais de uma década) e closed hierarchies (o modificador closed ). As duas atacam o mesmo problema: hoje, todo switch sobre hierarquia em C# precisa de um braço _ => porque o compilador nunca sabe se a lista de tipos está completa. Quando alguém adiciona um quarto meio de pagamento em outro assembly, o _ => engole o caso novo e o bug aparece em produção, não no build.

Union types: o valor é exatamente um dos casos

A sintaxe é enxuta. Em vez de uma hierarquia artificial só para servir de base, você declara o conjunto fechado:

O uso é direto, com conversão implícita para cada caso:

Se você remover o braço Pix , o compilador emite CS8509 ("not all possible values are handled"). Sem default , sem exceção em runtime: o erro — bom, o aviso — chega antes do merge. É a diferença entre "espero ter coberto tudo" e "o compilador confirma que cobri tudo".

Como o compilador representa isso

A análise de quem fuçou o IL gerado (como o vídeo do Nick Chapsas sobre o release) mostra que não há mágica: o compilador sintetiza uma struct somente leitura com um campo object e um índice do caso — o tag. Três consequências práticas:

Ordering importa. O índice é posicional. Inserir um caso no meio da declaração altera os tags; se você persiste ou serializa esse índice (cache, fila, banco), a ordem dos casos vira contrato. Adicione casos sempre no fim.

Boxing. Casos que são struct acabam alocados no heap. Em caminho quente — cálculo de frete, motor de regras, parser de payload — vale medir antes de trocar um enum + campos opcionais por uma union.

Versionamento. Para biblioteca pública, adicionar caso é mudança de contrato: quem consome recebe CS8509 no build. Isso é o objetivo, mas convém tratar como breaking change na semver.

Closed hierarchies: entre abstract e sealed

Se o seu modelo já é uma hierarquia de records — e a maioria dos domínios brasileiros de pagamento, faturamento e workflow é —, o closed resolve o mesmo problema sem trocar o desenho:

closed significa "as subclasses diretas moram nesta mesma assembly". abstract deixava qualquer projeto herdar; sealed não deixa ninguém herdar; closed fica no meio, e é exatamente o ponto onde o compilador consegue fechar a lista e dispensar o default . Em times que mantêm um núcleo de domínio compartilhado por três ou quatro soluções — situação comum em projetos sob medida de médio porte —, esse é o caminho de adoção mais barato: não muda a API pública, não muda o mapeamento do EF Core, só liga a checagem.

O que fazer no build (e onde a migração trava)

Adoção é incremental por projeto. O passo que muita gente vai esquecer é transformar o aviso em erro:

Coloque isso no Directory.Build.props da solução, não em um .csproj solto. Depois, a ordem que funciona na prática: (1) marque como closed os tipos base que já são usados em switch ; (2) veja quais CS8509 aparecem e decida caso a caso se o _ => era defesa legítima ou preguiça; (3) só então introduza unions nos pontos em que o código hoje devolve object , IActionResult genérico ou uma tupla com uma flag bool success que ninguém lê direito.

Riscos e armadilhas

O aviso é aviso. Sem WarningsAsErrors , um dotnet build verde continua saindo com caso não tratado.

O braço descartável desliga tudo. Um _ => throw new UnreachableException() parece defensivo, mas silencia o CS8509 e o caso novo volta a passar batido. Se o time usa agente de código para gerar PRs, esse é o cenário mais provável de regressão.

default(Union) não é nenhum dos casos. Por ser struct , a union tem valor padrão com o campo interno nulo. Um new PaymentMethod[10] ou um campo não inicializado produz um valor que não casa com braço algum. Valide na fronteira (construtor, binding de request) em vez de confiar na exaustividade.

JSON ambíguo. O System.Text.Json do .NET 11 lê unions nativamente, mas dois casos com o mesmo formato de objeto — dois DTOs com campos diferentes e nenhum discriminador — não são distinguíveis por introspecção de campo. Aí você precisa de tag explícita no payload. Teste com payload real, não com o objeto já materializado.

O que não veio. As dictionary expressions ficaram de fora do C# 15, e o Runtime Async — que tira das costas do compilador as máquinas de estado de async — está como opção em preview/experimental. Não planeje produção em cima disso neste ciclo.

STS vs LTS. Se a sua política interna só aceita LTS, .NET 11 não é para você; fique no .NET 10 e adote LangVersion nova se necessário.

Recomendação prática

Para quem já está no .NET 10 e mantém dezenas de switch com _ => : habilite CS8509 como erro, marque como closed uma hierarquia por sprint e veja quantos bugs latentes aparecem — na minha experiência, aparecem. Deixe union types para os pontos de borda do domínio (resultado de operação, forma de pagamento, estado de integração) e não para tudo. E meça o boxing antes de levar unions para dentro de loops de processamento.

Na Maquinaweb, times que trabalham com .NET e C# em produtos de perfis bem diferentes costumam adotar esses recursos por camada, com regra de build compartilhada, em vez de liberar a feature no repositório inteiro de uma vez — o que costuma render um lote grande de avisos e nenhuma decisão de arquitetura.

Se o seu time está avaliando migrar para o .NET 11, revisar hierarquias de domínio ou desenhar um projeto novo com stack moderna (C#, .NET, TypeScript, Angular, Node.js, Python e bancos relacionais), vale conversar com o time da Maquinaweb para discutir arquitetura, migração de stack e desenvolvimento sob medida.

Fontes

#csharp #dotnet #net-11 #csharp-15 #union-types #closed-hierarchies #pattern-matching #backend #migracao
Contato

Bora conversar?

Três campos. A resposta vem no WhatsApp.

Ou escreva para contato@maquinaweb.com.br.

Você vai para o WhatsApp com a mensagem pronta. Sem spam, sem lista.

Seus dados seguem a nossa política de privacidade.