aspnet-core-dtos-data-transfer-objects

DTOs en ASP.NET Core: evita exponer tus entidades

  • 4 min

Un DTO es un objeto diseñado para transportar datos entre capas o a través de una API.

Cuando empezamos a desarrollar APIs con Entity Framework, la tentación es fuerte. Tienes tu clase Producto que mapea a la base de datos, y piensas: “¿Para qué voy a crear otra clase igual? Devuelvo el Producto directamente en el controlador y acabo antes”.

Exponer tus entidades de base de datos directamente al cliente provoca riesgos de seguridad, problemas de rendimiento y acoplamiento. Es cómodo durante cinco minutos, y luego se convierte en una deuda estupenda.

La solución a todo esto tiene tres letras: DTO (Data Transfer Object).

El problema de exponer entidades

Supongamos que tenemos esta entidad en nuestra base de datos para gestionar usuarios:

public class Usuario
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string Email { get; set; }

    // Datos sensibles
    public string PasswordHash { get; set; }
    public bool EsAdmin { get; set; }
    public DateTime FechaCreacion { get; set; }
}
Copied!

Si en tu controlador haces esto:

[HttpGet("{id}")]
public IActionResult GetUsuario(int id)
{
    var usuario = _dbContext.Usuarios.Find(id);
    return Ok(usuario); // ❌ MAL: Devolviendo la entidad completa
}
Copied!

Estamos creando varios problemas:

Fuga de información

Al serializar la clase Usuario a JSON, estás enviando todos los campos públicos. Tu respuesta puede incluir PasswordHash. Aunque sea un hash, jamás debe salir del servidor.

Over-posting (mass assignment)

Este es peor aún. Pensemos en el método de actualizar:

[HttpPut]
public IActionResult Actualizar([FromBody] Usuario usuario) // ❌ Recibe la entidad
{
    _dbContext.Usuarios.Update(usuario);
    _dbContext.SaveChanges();
    return Ok();
}
Copied!

Un usuario malintencionado podría enviar este JSON:

{
  "id": 1,
  "username": "hacker",
  "esAdmin": true
}
Copied!

Como el Model Binder rellena la propiedad EsAdmin automáticamente y tú guardas el objeto tal cual, el hacker acaba de convertirse en administrador.

Referencias circulares

En Entity Framework, las entidades suelen tener relaciones de navegación (Categoria tiene Productos, y Producto tiene Categoria). Si intentas serializar este grafo sin configurarlo, System.Text.Json detectará el posible ciclo y lanzará una excepción.

La solución: Data Transfer Objects

Un DTO es una clase sencilla. No tiene lógica de negocio, no conecta a base de datos y no representa una tabla. Es solo una caja para transportar datos de un sitio a otro.

Creamos una clase específica para lo que queremos mostrar al mundo:

// Lo que el mundo ve
public class UsuarioDto
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string Email { get; set; }
    // NI RASTRO del Password ni de EsAdmin
}
Copied!

Y ahora, en nuestro controlador, hacemos el mapeo:

[HttpGet("{id}")]
public IActionResult GetUsuario(int id)
{
    // 1. Recuperamos la Entidad de la BBDD
    var entidad = _dbContext.Usuarios.Find(id);
    if (entidad == null) return NotFound();

    // 2. Mapeamos a DTO (Proyectamos)
    var dto = new UsuarioDto
    {
        Id = entidad.Id,
        Username = entidad.Username,
        Email = entidad.Email
    };

    // 3. Devolvemos el DTO
    return Ok(dto);
}
Copied!

DTOs de entrada y de salida

Una buena práctica es tener DTOs diferentes para recibir datos (Input) y para enviarlos (Output), ya que los requisitos suelen ser distintos.

Ejemplo: creación de usuario

Para crear un usuario, necesitamos recibir la contraseña (en texto plano, para hashearla nosotros), pero no necesitamos recibir el ID (se genera solo) ni la fecha de creación.

public class CrearUsuarioDto
{
    [Required]
    public string Username { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    public string Password { get; set; } // Aquí sí la pedimos
}
Copied!

Fíjate cómo el uso de DTOs nos permite tener un control granular sobre qué datos entran y salen en cada operación.

El poder de los record en C#

Desde C# 9 tenemos los record, que resultan cómodos para definir DTOs por su sintaxis concisa y su igualdad basada en valores.

Podemos definir un DTO en una sola línea:

public record ProductoDto(int Id, string Nombre, decimal Precio);
Copied!

Esto genera un tipo con constructor, propiedades init y comparadores de igualdad. Es una opción útil, aunque una clase normal también puede ser un DTO perfectamente válido.

Mapeo manual o automático

Quizás estés pensando: “¿Tengo que copiar propiedad por propiedad cada vez? Eso es muy aburrido y propenso a errores”.

Tienes razón. Existen dos enfoques:

  1. Mapeo Manual: Escribir dto.Nombre = entidad.Nombre.
  • Ventaja: Es explícito, rápido de ejecutar y fácil de depurar.
  • Desventaja: Código repetitivo.
  1. Mapeo Automático (Mapster): Librerías que copian las propiedades automáticamente basándose en que tengan el mismo nombre.

En aplicaciones pequeñas o críticas en rendimiento, el manual es mejor. En aplicaciones grandes, usaremos librerías.