Passa al contenuto principale

Records e immutabilità

Cos'è un record

Un record è un tipo reference (come class) con semantica di valore: l'uguaglianza è basata sul contenuto, non sull'identità in memoria. Il compilatore genera automaticamente Equals, GetHashCode, ToString e l'operatore == confrontando proprietà per proprietà.

var a = new Punto(1, 2);
var b = new Punto(1, 2);

Console.WriteLine(a == b); // true — stesse proprietà, istanze diverse

Con una class normale lo stesso confronto restituisce false.

Sintassi

Record posizionale

Il modo più compatto. Il compilatore genera il costruttore e le proprietà init-only:

public record Punto(double X, double Y);

public record CreaOrdineRequest(string ClienteId, decimal Importo, DateTime? DataConsegna);

Record con proprietà esplicite

Quando servono attributi, validazioni o valori di default:

public record IndirizzoSpedizione
{
public required string Via { get; init; }
public required string Citta { get; init; }
public string? Cap { get; init; }
public string Paese { get; init; } = "IT";
}

with expression

I record sono immutabili: non si modificano, si copiano con le differenze. L'espressione with crea una copia del record con alcuni campi cambiati:

var originale = new IndirizzoSpedizione { Via = "Via Roma 1", Citta = "Milano" };
var aggiornato = originale with { Citta = "Torino" };

// originale è invariato
// aggiornato ha Via = "Via Roma 1", Citta = "Torino"

Questo pattern elimina intere categorie di bug da mutazione accidentale: si può passare un record a un metodo con la certezza che non verrà modificato.

record struct

Per tipi piccoli e frequentemente allocati (coordinate, range, chiavi composte) si usa record struct: stessa semantica di valore, ma allocato sullo stack anziché sull'heap.

public record struct Coordinate(double Lat, double Lon);

Quando usare i record

Caso d'usoRecord?
DTO request/response API
Value object di dominio (Money, Email, Coordinate)
Risultati di query (read model)
Configurazione immutabile
Entity di dominio con identitàNo — usare class
Oggetti con stato mutabileNo — usare class

DTO immutabili

I DTO di request e response beneficiano dell'immutabilità: una volta deserializzato, il dato non cambia lungo tutta la catena di elaborazione. Non servono setter pubblici, non esistono stati intermedi.

// ✅ DTO immutabile con record posizionale
public record CreaUtenteRequest(
string Nome,
string Email,
string Password);

// ✅ Response con record
public record UtenteResponse(
int Id,
string Nome,
string Email,
DateTime CreatoIl);

Value object di dominio

Un valore come Email o Importo ha regole di uguaglianza naturali e non ha identità propria: due istanze con lo stesso valore sono intercambiabili. Il record modella questo senza boilerplate.

public record Email
{
public string Valore { get; }

public Email(string valore)
{
if (!valore.Contains('@'))
throw new ArgumentException("Formato email non valido.", nameof(valore));
Valore = valore.ToLowerInvariant();
}
}

var a = new Email("user@example.com");
var b = new Email("USER@EXAMPLE.COM");
Console.WriteLine(a == b); // true

Entity: usare class

Le entity hanno identità: due ordini con lo stesso contenuto ma Id diverso non sono lo stesso ordine. La semantica per valore del record è sbagliata per questo caso. Si usano class normali con l'Id come discriminante di uguaglianza.