1. Introduzione ai DTO
DTO (Data Transfer Object, "oggetto per il trasferimento dati") è un termine che arriva dal mondo della programmazione professionale, soprattutto da quello delle app distribuite e dei servizi (tipo quando un servizio chiama un altro via rete). In sostanza, è solo un oggetto — nella maggior parte dei casi una semplice classe C# — il cui unico scopo è: contenere un set di dati e passare in sicurezza tra i layer del programma o tra app diverse.
Immagina una chat tra client e server. Ogni volta che l'utente manda un messaggio, il client crea un oggetto con il testo, l'orario e il nome utente, lo manda al server, e poi il server decide cosa farci. Sarebbe figo se in quell'oggetto non ci fosse nulla di superfluo: solo quello che serve per il trasferimento dati. Ecco, questa è una classe-DTO.
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; }
}
Tutto qui! Niente business logic, niente metodi complicati — solo proprietà.
Classi solo per il trasferimento dati
Immagina di lavorare nelle consegne di pizza. Il tuo corriere (DTO) non deve saper cucinare la pizza, prendere ordini o riparare la bici. Tutto quello che deve fare è portare la scatola dal punto A al punto B. Così anche la classe-DTO: il suo compito è solo contenere dati semplici. Non deve validarsi da sola, non deve sapere come mostrarsi nell'UI — solo contenere dati.
- DTO — sono "persone senza mestiere" o "oggetti senza logica", portano solo dati.
- Si usano per non sporcare la business logic con dettagli di trasferimento dati: più piccolo è il bagaglio, più facile è portarlo.
2. Problemi delle classi-DTO classiche nella pratica
Sembra la felicità: dichiari una classe con auto-property e vivi sereno! Ma non è tutto rose e fiori.
Mutabilità dei dati: cosa può andare storto
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; }
}
Ora immagina che da qualche parte nel codice qualcuno cambi per sbaglio una proprietà:
ProductDto dto = new ProductDto { Id = 1, Name = "Latte" };
SomeApiProcess(dto);
dto.Name = "Kefir"; // Ops!
Dato che tutte le proprietà hanno il set pubblico, è facile rovinare l'oggetto per sbaglio o, peggio, cambiarlo in un punto e vedere effetti strani in tutta l'app. Questo diventa un vero problema quando gli oggetti sono usati da tanti moduli e thread.
Copia e confronto — routine infinita
Supponiamo che vuoi creare una copia di ProductDto, ma cambiare solo il nome.
var otherDto = new ProductDto { Id = dto.Id, Name = "Ricotta" };
Se hai tante proprietà, diventa una noia mortale (e rischiosa) copiare ogni campo a mano.
E se devi anche confrontare gli oggetti? Devi fare override di Equals e GetHashCode, che spesso si fa male o si dimentica proprio.
Immutabilità — non per le "classi semplici"
Nella maggior parte dei casi i DTO dovrebbero essere immutabili: ricevi i dati una volta — e non li cambi più. Ma una classe con auto-property non va bene per questo: chiunque può cambiare qualsiasi campo.
3. Come appare tutto ciò in un progetto grande
Vediamo un esempio dalla vita reale.
Sei uno sviluppatore in un mega e-commerce. Devi trasferire info su un ordine via rete. Per questo crei una classe così:
public class OrderDto
{
public int Id { get; set; }
public string Customer { get; set; }
public double Total { get; set; }
}
Funziona tutto, finché non arrivano centinaia di migliaia di ordini a settimana. Un sacco di microservizi si scambiano DTO a destra e manca, qualcuno si dimentica che Total va calcolato e non impostato a mano, qualcun altro cambia per sbaglio Customer dopo l'invio... E ti ritrovi con bug che sono un incubo da trovare.
Come si può migliorare?
- Usare solo proprietà get (senza set).
- Inventarsi un costruttore speciale per impostare i valori solo alla creazione.
- Fare override di Equals e GetHashCode…
Capisci dove si va a finire? Quello che doveva essere una semplice "valigia" diventa un mostro confuso.
4. Breve riepilogo dei problemi tipici delle classi DTO
Nei progetti piccoli i bug dovuti al cambio di un DTO si possono "beccare a mano". Nei grandi — è un disastro. I DTO si usano per autorizzazioni, trasferimento soldi, gestione ordini — qualsiasi modifica illegale dei dati può diventare una perdita di soldi o (mannaggia!) un problema legale.
Qui sotto trovi una "cheat sheet" dei problemi che quasi tutti incontrano scrivendo DTO come classi:
| Problema | Descrizione |
|---|---|
| Mutabilità | I dati si cambiano facilmente ovunque — rischio bug (soprattutto con i thread) |
| Clonazione | Copiare oggetti è scomodo, spesso si fa a mano |
| Confronto | Di default si confrontano per riferimento, non per valore |
| Supporto with-copy | Vorresti copiare cambiando solo alcuni dati senza fare copia manuale |
| Nidificazione poco chiara | I DTO annidati vanno copiati tutti, ed è una seccatura |
5. Esempio: trasferimento dati tra layer nella nostra app
Supponiamo di avere una app-agenda dove possiamo salvare task. Prima avremmo avuto una classe così:
public class TaskDto
{
public int Id { get; set; }
public string Description { get; set; }
public DateTime DueDate { get; set; }
}
Leggevamo TaskDto da file, poi lo aggiungevamo a una collection, poi lo mostravamo a schermo. Tutto ok... finché qualcuno da un altro modulo non cambiava per sbaglio DueDate proprio prima della serializzazione — e l'utente si trovava confuso.
7. Si può fare meglio?
Sì! E non solo si può, ma si deve! Per risolvere questi problemi in C# è arrivata una feature speciale — record. Risolve la maggior parte dei mal di testa legati alle classi-DTO, quasi come per magia.
Cosa fa record:
- Di default è value-oriented: confronta gli oggetti per contenuto, non per riferimento.
- Immutabilità: puoi mettere solo proprietà get, i valori si danno via costruttore.
- Clonazione facile: c'è la sintassi with per copiare cambiando qualcosa.
- Genera in automatico i metodi per confronto e stampa.
public record TaskDto(int Id, string Description, DateTime DueDate);
Figo, vero? Ma di questo — nella prossima lezione.
GO TO FULL VERSION