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
Continue lendo
Desenvolvimento Ransomware derruba a nuvem da SoftBank em 7 minutos: 495 clientes atingidos e um rescan de IA que achou 35 mil falhas novas
Ransomware na IDC Frontier, subsidiária da SoftBank, atingiu 495 organizações em 7 minutos via falha em OSS de autenticação; um rescan com IA encontrou 35 mil vulnerabilidades novas. No mesmo ciclo: zero-day no Chrome e a conta de US$ 740 mil da Railway.
Ler artigo →
Desenvolvimento Óculos do Google e Samsung a partir de US$ 395: a IA sem tela entra em pré-venda e chega às lojas em 10 de dezembro
Google e Samsung abrem pré-venda dos óculos Intelligent Eyewear com Android XR e Gemini a partir de US$ 395, com entrega em 10 de dezembro nos EUA e na Coreia do Sul. No mesmo ciclo, a Neo4j lança o Relate, banco embarcado em Rust para agentes, e a Anthropic põe o Claude dentro de chamadas de vídeo.
Ler artigo →
Desenvolvimento Expedia confirma ataque por fornecedor de dados, n0n reivindica 830 milhões de linhas e BroadWorks entra no KEV
Expedia confirma invasão por fornecedor terceirizado de plataforma de dados e o grupo n0n reivindica 830 milhões de linhas. Na mesma janela, o XXE do Cisco BroadWorks entra no KEV da CISA com prazo em 24/11, e o Maersk confirma acesso a sistema legado herdado da LF Logistics.
Ler artigo →