1. Conociendo los DTO
DTO (Data Transfer Object, "objeto de transferencia de datos") es un término que viene del mundo pro de la programación, sobre todo de apps distribuidas y servicios (por ejemplo, cuando un servicio llama a otro por la red). Básicamente, es solo un objeto — en la mayoría de los casos, una clase normal de C# — cuya única tarea es: contener un conjunto de datos y transferirse de forma segura entre capas del programa o entre aplicaciones.
Imagina un chat entre cliente y servidor. Cada vez que el usuario manda un mensaje, el cliente crea un objeto con el texto, la hora y el nombre del usuario, lo manda al servidor, y este ya decide qué hacer con eso. Sería genial que ese objeto no tuviera nada de más: solo lo necesario para transferir los datos. Eso es justo una clase DTO.
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; }
}
¡Eso es todo! Nada de lógica de negocio, ni métodos complicados — solo propiedades.
Clases solo para transferir datos
Imagina que te dedicas a repartir pizza. Tu repartidor (DTO) no tiene que saber cocinar la pizza, ni tomar pedidos, ni arreglar la bici. Todo lo que tiene que hacer es llevar la caja del punto A al punto B. Así es una clase DTO: su tarea es guardar datos simples. No tiene que validarse a sí misma, ni saber cómo mostrarse en la UI — solo guardar.
- DTO — son "personas sin oficio" o "objetos sin lógica", solo llevan datos.
- Se usan para no ensuciar la lógica de negocio con detalles de transferencia de datos: cuanto más pequeño el maletín, más fácil de llevar.
2. Problemas de las clases DTO normales en la práctica
Parece la felicidad: declaras una clase con auto-propiedades y a vivir tranquilo. ¡Pero no es tan fácil!
Mutabilidad de los datos: ¿qué puede salir mal?
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; }
}
Ahora imagina que en algún sitio del código cambias una propiedad sin querer:
ProductDto dto = new ProductDto { Id = 1, Name = "Leche" };
SomeApiProcess(dto);
dto.Name = "Kéfir"; // ¡Ups!
Como todas las propiedades tienen set público, es fácil romper el objeto por accidente o, peor aún, cambiarlo en un sitio y que eso "contamine" todo el sistema. Esto se vuelve un problema real cuando los objetos los usan muchos módulos e hilos.
Copiar y comparar — la rutina interminable
Supón que quieres crear una copia de ProductDto, pero solo cambiar el nombre.
var otherDto = new ProductDto { Id = dto.Id, Name = "Requesón" };
Si tienes muchas propiedades, esto se vuelve una tarea aburrida y propensa a bugs, copiando cada campo a mano.
¿Y si además tienes que comparar objetos? Toca sobreescribir Equals y GetHashCode, que muchas veces se hace mal o se olvida.
Inmutabilidad — no para las "clases simples"
En la mayoría de los casos, los DTO deberían ser inmutables: recibimos los datos una vez — y no los cambiamos más. Pero una clase con auto-propiedades no sirve para eso: cualquiera puede cambiar cualquier campo.
3. ¿Cómo se ve esto en un proyecto grande?
Vamos a ver un ejemplo real.
Eres dev en una mega tienda online. Tienes que transferir info de un pedido por la red. Para eso creas una clase así:
public class OrderDto
{
public int Id { get; set; }
public string Customer { get; set; }
public double Total { get; set; }
}
Todo va bien hasta que tienes cientos de miles de pedidos a la semana. Un montón de microservicios empiezan a pasar DTOs de un lado a otro, alguien se olvida de que Total se debe calcular y no poner a mano, otro cambia Customer después de enviar... Y aparecen bugs que son súper difíciles de pillar.
¿Cómo se puede mejorar?
- Usar solo propiedades get (sin set).
- Crear un constructor especial para poner los valores solo al crear el objeto.
- Sobreescribir Equals y GetHashCode…
¿Ves a dónde va esto? Lo que debía ser una "maleta" simple, de repente se convierte en un monstruo raro.
4. Resumen rápido de los problemas típicos de las clases DTO
En proyectos pequeños, los bugs por cambiar un DTO los puedes "pillar a mano". En los grandes — es un desastre. Los DTO se usan para autorización, transferir dinero, procesar pedidos — cualquier cambio ilegal de datos puede acabar en pérdidas de pasta o (¡madre mía!) problemas legales.
Aquí tienes una "chuleta" de problemas que casi todos los que escriben DTO con clases han sufrido:
| Problema | Descripción |
|---|---|
| Mutabilidad | Los datos se pueden cambiar en cualquier parte — riesgo de bugs (sobre todo con multihilo) |
| Clonado | Copiar objetos es incómodo, a menudo se hace a mano |
| Comparación | Por defecto se comparan por referencia, no por valor |
| Soporte para with-copias | Estaría bien copiar cambiando solo parte de los datos sin copiar a mano |
| Anidamiento poco claro | Los DTO anidados hay que copiarlos enteros, y eso es un rollo |
5. Ejemplo: transferencia de datos entre capas en nuestra app
Supón que tenemos una app de agenda, donde guardamos tareas. Antes teníamos una clase así:
public class TaskDto
{
public int Id { get; set; }
public string Description { get; set; }
public DateTime DueDate { get; set; }
}
Leíamos TaskDto de un archivo, luego lo metíamos en una colección, luego lo mostrábamos en pantalla. Todo iba bien... hasta que alguien de otro módulo cambió sin querer DueDate justo antes de serializar — y el usuario se lió.
7. ¿Se puede hacer mejor?
¡Sí! ¡Y no solo se puede, sino que se debe! Para solucionar estos problemas en C# apareció una construcción especial — record. Soluciona casi toda la pesadilla de las clases DTO, casi como por arte de magia.
¿Qué hace record?
- Por defecto es orientado a valores: compara objetos por contenido, no por referencia.
- Inmutabilidad: puedes poner solo propiedades get, los valores se ponen en el constructor.
- Clonado fácil: existe la construcción with para copiar cambiando datos.
- Genera automáticamente métodos para comparar y mostrar.
public record TaskDto(int Id, string Description, DateTime DueDate);
¿Mola? ¡Mucho! Pero de eso hablamos en la próxima lección.
GO TO FULL VERSION