.NET C# Clean Code Backend Boas Práticas

Pattern Matching avançado no C#: código mais expressivo sem casting manual

Domine os padrões avançados do C# moderno: switch expressions, positional patterns, list patterns, property patterns aninhados e when guards.

N
Neryx Digital Architects
8 de março de 2026
13 min de leitura
240 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

Pattern matching não é novidade no C# — o operador is existe desde o C# 1. O que mudou radicalmente a partir do C# 8 (e evoluiu até o C# 13) é a expressividade: você substitui cascatas de if/else com casting manual por expressões declarativas que descrevem a estrutura dos dados. O resultado é código que lê como especificação, não como implementação.

O ponto de partida: switch tradicional vs switch expression

Compare as duas abordagens para calcular frete baseado no tipo de entrega:

// C# antigo — imperativo e verboso
decimal CalcularFrete(EntregaBase entrega)
{
    switch (entrega)
    {
        case EntregaExpresso e when e.Peso > 10:
            return 45.00m;
        case EntregaExpresso:
            return 25.00m;
        case EntregaEconomica eco when eco.Regiao == "Norte":
            return 18.00m;
        case EntregaEconomica:
            return 8.50m;
        default:
            throw new InvalidOperationException($"Tipo desconhecido: {entrega.GetType()}");
    }
}

// C# moderno — declarativo e expressivo
decimal CalcularFrete(EntregaBase entrega) => entrega switch
{
    EntregaExpresso { Peso: > 10 }     => 45.00m,
    EntregaExpresso                    => 25.00m,
    EntregaEconomica { Regiao: "Norte" } => 18.00m,
    EntregaEconomica                   => 8.50m,
    _                                  => throw new InvalidOperationException(
                                            $"Tipo desconhecido: {entrega.GetType()}")
};

A versão moderna não é só mais curta — ela elimina o boilerplate de variáveis temporárias, a necessidade de casting explícito, e torna a lógica de decisão visível na estrutura do código.

Property patterns: desestruturando objetos

Property patterns permitem inspecionar propriedades de um objeto diretamente na expressão de match:

record Pedido(string Status, decimal Valor, bool ClienteVip, string Pais);

string ClassificarPedido(Pedido p) => p switch
{
    // Pattern aninhado — propriedades compostas
    { Status: "Pendente", Valor: > 1000, ClienteVip: true } =>
        "Alta prioridade VIP",

    { Status: "Pendente", Valor: > 1000 } =>
        "Alta prioridade",

    // Property pattern em string com constant pattern
    { Status: "Cancelado", Pais: "BR" } =>
        "Cancelado nacional — emitir nota de cancelamento",

    { Status: "Cancelado" } =>
        "Cancelado internacional — acionar parceiro logístico",

    { Status: "Enviado" or "Entregue" } =>
        "Em trânsito ou concluído",

    _ => "Status não mapeado"
};

Positional patterns: desestruturando via Deconstruct

Se o tipo tem um método Deconstruct (incluindo records e tuples), você pode usar positional patterns:

record Coordenada(double Latitude, double Longitude)
{
    // Records geram Deconstruct automaticamente
}

string ClassificarRegiao(Coordenada c) => c switch
{
    // Desestrutura (Latitude, Longitude) diretamente
    (var lat, var lon) when lat > 0 && lon > 0   => "Nordeste",
    (var lat, var lon) when lat > 0 && lon < 0   => "Noroeste",
    (var lat, var lon) when lat < 0 && lon > 0   => "Sudeste",
    (var lat, var lon) when lat < 0 && lon < 0   => "Sudoeste",
    (0, 0)                                         => "Origem",
    _                                              => "No eixo"
};

// Também funciona com tuples inline
string ClassificarResultado(int codigo, string mensagem) => (codigo, mensagem) switch
{
    (200, _)           => "Sucesso",
    (400, var msg)     => $"Erro do cliente: {msg}",
    (401 or 403, _)    => "Não autorizado",
    (>= 500, var msg)  => $"Erro do servidor: {msg}",
    _                  => "Status desconhecido"
};

List patterns (C# 11): matching em coleções

C# 11 adicionou list patterns — você consegue fazer match na estrutura de uma lista ou array:

string AnalisarHistorico(decimal[] ultimosPrecos) => ultimosPrecos switch
{
    // Array vazio
    [] => "Sem histórico",

    // Exatamente um elemento
    [var unico] => $"Apenas um preço: {unico}",

    // Começa com dois valores específicos
    [0, 0, ..] => "Começou zerado",

    // Dois últimos elementos capturados com slice pattern
    [.., var penultimo, var ultimo] when ultimo > penultimo =>
        $"Tendência de alta: {penultimo} → {ultimo}",

    [.., var penultimo, var ultimo] when ultimo < penultimo =>
        $"Tendência de queda: {penultimo} → {ultimo}",

    _ => "Estável"
};

// Verificar protocolo HTTP a partir de bytes
bool IsHttps(byte[] cabecalho) => cabecalho switch
{
    [(byte)'H', (byte)'T', (byte)'T', (byte)'P', (byte)'S', ..] => true,
    _ => false
};

Combinando padrões: and, or, not

C# 9 introduziu os operadores lógicos de pattern — and, or, not — que funcionam como conectivos dentro de expressões de match:

// Relational patterns + logical patterns
string ClassificarIdade(int idade) => idade switch
{
    < 0                => throw new ArgumentException("Idade negativa"),
    0                  => "Recém-nascido",
    >= 1 and <= 12     => "Criança",
    >= 13 and <= 17    => "Adolescente",
    >= 18 and <= 64    => "Adulto",
    >= 65              => "Idoso",
    _                  => "Inválido" // nunca alcançado — compilador avisa
};

// not pattern — semântica de negação clara
bool PodeReceberConteudoAdulto(Usuario u) => u is
    not { Status: "Banido" } and
    not { Idade: < 18 } and
    not { TermosAceitos: false };

// or em type patterns
void ProcessarEvento(IEvento evento)
{
    if (evento is EventoPagamento or EventoEstorno { Aprovado: true })
        AtualizarSaldo(evento);
}

Pattern matching em is expressions

O operador is evoluiu para suportar todos os mesmos padrões do switch. Útil para condicionais simples sem precisar de um switch completo:

// Type pattern com captura
if (resultado is ErroValidacao { Campos: { Length: > 0 } erros })
{
    foreach (var campo in erros)
        AdicionarErroFormulario(campo);
    return;
}

// Negação com is not
if (usuário is not { Perfil: PerfilAdmin or PerfilSuperAdmin })
    throw new UnauthorizedAccessException();

// Captura + guard em uma linha
while (fila.TryDequeue(out var item) && item is { Ativo: true } tarefa)
{
    await ExecutarAsync(tarefa);
}

Caso de uso real: processamento de comandos DDD

Pattern matching brilha em aplicações com Command/Event handling, onde você precisa despachar para handlers específicos baseado no tipo e conteúdo:

public async Task<IResult> HandleAsync(IComando comando, CancellationToken ct) =>
    comando switch
    {
        CriarPedidoComando { Itens: { Length: 0 } } =>
            Results.BadRequest("Pedido deve ter pelo menos um item"),

        CriarPedidoComando { ClienteId: var cid, Itens: var itens } =>
            Results.Ok(await _pedidoService.CriarAsync(cid, itens, ct)),

        CancelarPedidoComando { PedidoId: var pid, Motivo: null or "" } =>
            Results.BadRequest("Motivo de cancelamento é obrigatório"),

        CancelarPedidoComando { PedidoId: var pid, Motivo: var motivo } =>
            Results.Ok(await _pedidoService.CancelarAsync(pid, motivo, ct)),

        AtualizarEnderecoComando { EnderecoId: <= 0 } =>
            Results.BadRequest("ID de endereço inválido"),

        AtualizarEnderecoComando atualizar =>
            Results.Ok(await _enderecoService.AtualizarAsync(atualizar, ct)),

        _ => Results.BadRequest($"Comando não reconhecido: {comando.GetType().Name}")
    };

Exhaustiveness checking: o compilador como aliado

Um dos maiores benefícios do switch expression é a análise de exaustividade em tempo de compilação. Com hierarquias de tipos seladas (sealed), o compilador verifica se todos os casos foram cobertos:

// Hierarquia selada — o compilador conhece todos os subtipos
abstract record Forma;
sealed record Circulo(double Raio) : Forma;
sealed record Retangulo(double Largura, double Altura) : Forma;
sealed record Triangulo(double Base, double Altura) : Forma;

// Se você esquecer um caso, o compilador AVISA (CS8509 — não erro, mas warning)
double CalcularArea(Forma f) => f switch
{
    Circulo { Raio: var r }             => Math.PI * r * r,
    Retangulo { Largura: var l, Altura: var a } => l * a,
    Triangulo { Base: var b, Altura: var a }    => b * a / 2
    // Sem _ => ... — compilador verifica exaustividade graças a sealed
};

Com classes abertas, o compilador não tem como garantir exaustividade — mas um _ com throw deixa explícito o caso não mapeado e lança em tempo de execução.

Quando pattern matching é a escolha errada

Pattern matching não substitui polimorfismo. Se você tem um switch em tipo que cresce frequentemente com novos casos, provavelmente deveria usar o padrão Strategy ou Virtual Methods — adicionar um novo tipo não vai exigir alterar um switch em vários lugares. Use pattern matching quando: a lógica de dispatch é localizada (um lugar, um propósito), os tipos são estáveis (não mudam frequentemente), ou você está combinando múltiplas dimensões de decisão que não fazem sentido como métodos virtuais.

Pattern matching não é uma feature de nicho — é o caminho que C# escolheu para escrever lógica condicional estruturada. Aprender os padrões disponíveis (property, positional, list, logical, relational) e saber quando combiná-los é uma das habilidades que mais reduz ruído em código .NET moderno.

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.