Programmazione

Pattern matching in C#: scenari avanzati che probabilmente non conosci

Dario Fadda Maggio 6, 2026

Il pattern matching in C# non è solo un modo più elegante di scrivere condizioni: è un cambio di paradigma nel modo in cui si ragiona sulla struttura dei dati. A partire da C# 7 e con evoluzioni significative nelle versioni successive, il pattern matching è diventato uno strumento potentissimo per scrivere codice più leggibile, manutenibile e talvolta anche più efficiente.

In questo articolo esploreremo i pattern avanzati che molti sviluppatori .NET tendono a ignorare o a sottoutilizzare, partendo da un progetto concreto che dimostra ogni scenario.

Setup del progetto di esempio

Prima di tutto, creiamo un’applicazione console di test:

dotnet new console -n PatternMatchingDemo
cd PatternMatchingDemo

Definiamo i modelli come record, ideali per il pattern matching grazie alla loro natura value-based:

namespace PatternMatchingDemo.Records;

public record Address(string City, string Country);
public record User(string Name, int Age, Address Address, List<string> Roles);
public record Request(string Source, int Priority);
public record Point(int X, int Y);

E popoliamo alcune collezioni di test:

var users = new List<User>
{
    new("Ali", 25, new Address("Milano", "Italy"), new List<string> { "Admin", "User" }),
    new("Sara", 17, new Address("Roma", "Italy"), new List<string> { "User" }),
    new("Kennedy", 65, new Address("London", "UK"), new List<string> { "Guest" })
};

var requests = new List<Request>
{
    new("System", 10),
    new("User", 3),
    new("System", 2)
};

Property Pattern: matching annidato

Uno dei pattern più utili è il property pattern, che permette di verificare le proprietà di un oggetto direttamente nell’espressione di matching, incluse proprietà annidate:

foreach (var user in users)
{
    if (user is { Address.City: "Milano" })
    {
        Console.WriteLine($"{user.Name} è di Milano");
    }
}

Il confronto tradizionale richiederebbe:

if (user != null && user.Address != null && user.Address.City == "Milano")

La versione con property pattern è non solo più compatta, ma anche null-safe per definizione: se user o user.Address sono null, il pattern semplicemente non matcha.

Pattern con not: negazione elegante

foreach (var user in users)
{
    if (user is not { Address.City: "Milano" })
    {
        Console.WriteLine($"{user.Name} non è di Milano");
    }
}

Il keyword not inverte il risultato del pattern, rendendo esplicito il significato senza bisogno di operatori logici aggiuntivi.

Matching su casi multipli con or

foreach (var user in users)
{
    if (user is { Address.City: "Milano" or "Roma" })
    {
        Console.WriteLine($"{user.Name} vive in una grande città italiana");
    }
}

Il combinatore or all’interno di un pattern è molto più leggibile di una serie di condizioni concatenate con ||, specialmente quando le condizioni riguardano la stessa proprietà.

Pattern Matching dentro LINQ

Il pattern matching si integra perfettamente con LINQ, permettendo query molto espressive:

var adultiItaliani = users
    .Where(u => u is { Age: > 18, Address.Country: "Italy" })
    .ToList();

foreach (var user in adultiItaliani)
{
    Console.WriteLine($"{user.Name} è un adulto italiano");
}

Questa combinazione è particolarmente potente per filtrare DTO complessi, validare oggetti di dominio o implementare query su collezioni in memoria.

Pattern relazionali e logici

I pattern relazionali (>, <, >=, <=) combinati con i pattern logici (and, or) permettono di esprimere range e condizioni composite in modo molto naturale:

foreach (var user in users)
{
    if (user.Age is > 18 and < 60)
    {
        Console.WriteLine($"{user.Name} è un adulto lavorativo");
    }
    else if (user.Age is < 18 or > 60)
    {
        Console.WriteLine($"{user.Name} appartiene a una categoria di età speciale");
    }
}

Switch Expression

La switch expression (introdotta in C# 8) è una versione compatta e restituisce un valore. Elimina la verbosità del tradizionale switch statement:

foreach (var user in users)
{
    var categoria = user.Age switch
    {
        < 13 => "Bambino",
        < 20 => "Adolescente",
        < 60 => "Adulto",
        _     => "Senior"
    };

    Console.WriteLine($"{user.Name} => {categoria}");
}

Rispetto allo switch classico, la versione expression è più concisa, obbliga a coprire tutti i casi (o aggiungere il wildcard _) e restituisce direttamente un valore senza variabili intermedie.

Pattern Type + Condition

Il pattern di tipo permette di verificare il tipo di un oggetto e aggiungere una condizione contemporaneamente:

object value = 150;

// Modo tradizionale
if (value is int number && number > 100)
{
    Console.WriteLine("Numero grande (vecchio stile)");
}

// Con pattern matching
if (value is int and > 100)
{
    Console.WriteLine("Numero grande (pattern matching)");
}

Questo è particolarmente utile quando si lavora con object, dynamic, o con gerarchie di tipi complesse.

List Pattern (C# 11+)

Introdotto in C# 11, il list pattern consente di matchare la struttura e il contenuto di array e liste:

int[] nums = { 1, 2, 3 };

if (nums is [1, 2, 3])
{
    Console.WriteLine("Match esatto");
}

if (nums is [1, .., 3])
{
    Console.WriteLine("Inizia con 1 e finisce con 3");
}

if (nums is [_, _, _])
{
    Console.WriteLine("Array con esattamente 3 elementi");
}

Il slice pattern .. è molto flessibile: può matchare zero o più elementi nel mezzo di una sequenza. Questo pattern è molto utile per validare payload di API che arrivano come array, controllare header HTTP, o verificare strutture di dati fisse.

Positional Pattern

var point = new Point(10, 20);

if (point is (10, 20))
{
    Console.WriteLine("Il punto è (10,20)");
}

// Con switch expression
var descrizione = point switch
{
    (0, 0) => "Origine",
    (_, 0) => "Sull'asse X",
    (0, _) => "Sull'asse Y",
    _      => $"Punto generico ({point.X},{point.Y})"
};
Console.WriteLine(descrizione);

Il positional pattern decostruisce l’oggetto usando il metodo Deconstruct (disponibile automaticamente per i record) e permette di matchare ogni componente individualmente.

Combined Pattern: la vera potenza

Combinare più pattern insieme permette di esprimere logica di business complessa in modo dichiarativo:

foreach (var user in users)
{
    if (user is
        {
            Age: > 18,
            Address.Country: "Italy",
            Roles: ["Admin", ..]
        })
    {
        Console.WriteLine($"{user.Name} è un admin adulto italiano");
    }
}

Questo esempio combina property pattern annidato, relational pattern e list pattern in un’unica espressione. In scenari reali, questa tecnica è applicabile per authorization checks, validazione di DTO, o routing di request handler.

Null Pattern

User? maybeUser = null;

if (maybeUser is not null)
{
    Console.WriteLine("L'utente esiste");
}
else
{
    Console.WriteLine("L'utente è null");
}

Il null pattern con is not null è semanticamente più preciso di != null in contesti di nullable reference types, ed è la forma raccomandata nelle linee guida di C# moderno.

Guard Clause nelle switch expression

int number = 7;

var result = number switch
{
    int n when n % 2 == 0 => "Pari",
    int n when n % 2 != 0 => "Dispari",
    _ => "Sconosciuto"
};

Console.WriteLine(result);

La clausola when aggiunge una condizione aggiuntiva a un pattern. Utile quando il solo pattern non è sufficiente a discriminare i casi.

Request Handling pattern

Un esempio pratico di uso combinato in un sistema di routing delle request:

foreach (var request in requests)
{
    var response = request switch
    {
        { Source: "System", Priority: > 5 } => "Richiesta di sistema critica",
        { Source: "User",   Priority: <= 5 } => "Richiesta utente normale",
        _ => "Fallback generico"
    };

    Console.WriteLine($"{request.Source} ({request.Priority}) => {response}");
}

Questo schema è applicabile in moltissimi contesti: event sourcing, command dispatcher, middleware pipeline, validazione di business rules.

Considerazioni sulle performance

Oltre alla leggibilità, il pattern matching in C# è progettato per essere efficiente. Il compilatore ottimizza le switch expression in jump table o sequenze di confronto ottimizzate. Per tipi primitivi, le performance sono equivalenti o superiori a catene di if-else.

Per scenari di alta performance con molti branch (es. parser, protocol handler), vale la pena misurare con BenchmarkDotNet, ma nella stragrande maggioranza dei casi applicativi il pattern matching non introduce overhead significativo.

Conclusione

Il pattern matching in C# è uno strumento che va ben oltre il semplice is o lo switch. Combinando property pattern, list pattern, relational pattern e switch expression, è possibile scrivere logica complessa in modo dichiarativo e leggibile.

La chiave per sfruttarlo al meglio è conoscere tutti i tipi di pattern disponibili e riconoscere le situazioni in cui possono sostituire costrutti più verbosi. Un codice che legge come il problema che risolve è un codice di qualità superiore.

Il codice sorgente di esempio è disponibile su GitHub: https://github.com/elmahio-blog/PatternMatchingDemo.git


Fonte originale: Pattern matching in C#: Advanced scenarios you didn’t know — elmah.io Blog

💬 Unisciti alla discussione!


Questo è un blog del Fediverso: puoi trovare quindi questo articolo ovunque con @blog@spcnet.it e ogni commento/risposta apparirà qui sotto.

Se vuoi commentare su Pattern matching in C#: scenari avanzati che probabilmente non conosci, utilizza la discussione sul Forum.
Condividi la tua esperienza, confrontati con altri professionisti e approfondisci i dettagli tecnici nel nostro 👉 forum community