En esta página

Entity Framework Core 10: ORM completo para .NET

15 min lectura TextoCap. 4 — ASP.NET Core

¿Qué es Entity Framework Core?

EF Core es el ORM (Object-Relational Mapper) oficial de .NET. Permite trabajar con bases de datos relacionales usando objetos C# en lugar de SQL directo.

Proveedores disponibles:

  • SQL ServerMicrosoft.EntityFrameworkCore.SqlServer
  • PostgreSQLNpgsql.EntityFrameworkCore.PostgreSQL
  • SQLiteMicrosoft.EntityFrameworkCore.Sqlite
  • MySQLPomelo.EntityFrameworkCore.MySql
  • In-Memory — para testing: Microsoft.EntityFrameworkCore.InMemory

Instalar EF Core

# Paquetes para PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Design

# CLI de migraciones (instalar globalmente)
dotnet tool install --global dotnet-ef

# Verificar
dotnet ef --version

Code First: entidades → base de datos

Con Code First defines las clases C# primero y EF Core genera la base de datos:

// Entidad simple
public class Empleado
{
    public int    Id       { get; set; }
    public string Nombre   { get; set; } = string.Empty;
    public string Email    { get; set; } = string.Empty;
    public decimal Salario { get; set; }
    public DateTime Ingreso { get; set; }

    // Relación uno a muchos (FK)
    public int         DepartamentoId { get; set; }
    public Departamento Departamento   { get; set; } = null!;
}

public class Departamento
{
    public int    Id       { get; set; }
    public string Nombre   { get; set; } = string.Empty;

    // Propiedad de navegación (colección)
    public ICollection<Empleado> Empleados { get; set; } = new List<Empleado>();
}

Configurar el DbContext

public class EmpresaDbContext : DbContext
{
    public EmpresaDbContext(DbContextOptions<EmpresaDbContext> options)
        : base(options) { }

    public DbSet<Empleado>     Empleados     => Set<Empleado>();
    public DbSet<Departamento> Departamentos => Set<Departamento>();

    protected override void OnModelCreating(ModelBuilder model)
    {
        // Aplicar todas las configuraciones de un ensamblado
        model.ApplyConfigurationsFromAssembly(typeof(EmpresaDbContext).Assembly);
    }
}

// Configuración separada (mejor práctica)
public class EmpleadoConfiguration : IEntityTypeConfiguration<Empleado>
{
    public void Configure(EntityTypeBuilder<Empleado> builder)
    {
        builder.HasKey(e => e.Id);

        builder.Property(e => e.Nombre)
               .IsRequired()
               .HasMaxLength(150);

        builder.Property(e => e.Email)
               .IsRequired()
               .HasMaxLength(200);
               // .HasIndex omitido — se hace por HasIndex

        builder.HasIndex(e => e.Email).IsUnique();

        builder.Property(e => e.Salario)
               .HasColumnType("decimal(18,2)")
               .HasDefaultValue(0);

        builder.HasOne(e => e.Departamento)
               .WithMany(d => d.Empleados)
               .HasForeignKey(e => e.DepartamentoId)
               .OnDelete(DeleteBehavior.Cascade);
    }
}

Registrar en Program.cs

// PostgreSQL
builder.Services.AddDbContext<EmpresaDbContext>(opts =>
    opts.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

// appsettings.json
// "ConnectionStrings": {
//   "Default": "Host=localhost;Database=empresa;Username=app;Password=secret"
// }

Migraciones

# Crear migración inicial
dotnet ef migrations add InitialCreate

# Ver el SQL que se generará (sin ejecutar)
dotnet ef migrations script

# Aplicar migraciones en desarrollo
dotnet ef database update

# Revertir a una migración específica
dotnet ef database update NombreMigracion

# Eliminar la última migración (si aún no se aplicó a BD)
dotnet ef migrations remove

Aplicar programáticamente al iniciar la app:

// En Program.cs — auto-migra al arrancar
using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<EmpresaDbContext>();
    await db.Database.MigrateAsync();
}

Consultas LINQ con EF Core

// Consulta básica
var empleados = await db.Empleados.ToListAsync();

// Filtrar
var activos = await db.Empleados
    .Where(e => e.Salario > 3000)
    .ToListAsync();

// Incluir relaciones (eager loading)
var conDepartamento = await db.Empleados
    .Include(e => e.Departamento)
    .ToListAsync();

// Relaciones anidadas (ThenInclude)
var pedidosConDetalles = await db.Pedidos
    .Include(p => p.Cliente)
    .Include(p => p.Items)
        .ThenInclude(i => i.Producto)
    .ToListAsync();

// Proyección a DTO (evita cargar todo el objeto)
var resumen = await db.Empleados
    .Select(e => new EmpleadoDto(e.Id, e.Nombre, e.Departamento.Nombre))
    .ToListAsync();

// Paginación
int pagina  = 1;
int tamaño  = 10;
var pagResult = await db.Empleados
    .OrderBy(e => e.Nombre)
    .Skip((pagina - 1) * tamaño)
    .Take(tamaño)
    .AsNoTracking()
    .ToListAsync();

Operaciones CRUD

// CREATE
var nuevo = new Empleado
{
    Nombre         = "David Morales",
    Email          = "[email protected]",
    Salario        = 4500m,
    DepartamentoId = 1,
    Ingreso        = DateTime.Today
};
db.Empleados.Add(nuevo);
await db.SaveChangesAsync();
// nuevo.Id tiene ahora el valor generado por la BD

// READ
var empleado = await db.Empleados.FindAsync(id); // por PK — usa caché interna
var porEmail = await db.Empleados
    .FirstOrDefaultAsync(e => e.Email == "[email protected]");

// UPDATE
var emp = await db.Empleados.FindAsync(id);
if (emp is not null)
{
    emp.Salario = 5000m;
    await db.SaveChangesAsync(); // EF detecta el cambio automáticamente
}

// UPDATE masivo (EF Core 7+)
await db.Empleados
    .Where(e => e.DepartamentoId == 2)
    .ExecuteUpdateAsync(s =>
        s.SetProperty(e => e.Salario, e => e.Salario * 1.10m)); // +10%

// DELETE
db.Empleados.Remove(emp);
await db.SaveChangesAsync();

// DELETE masivo (EF Core 7+)
await db.Empleados
    .Where(e => !e.Activo)
    .ExecuteDeleteAsync();

Transacciones

// Transacción explícita
await using var transaction = await db.Database.BeginTransactionAsync();
try
{
    var dept = new Departamento { Nombre = "DevOps" };
    db.Departamentos.Add(dept);
    await db.SaveChangesAsync();

    var emp = new Empleado { Nombre = "Ana López", DepartamentoId = dept.Id, ... };
    db.Empleados.Add(emp);
    await db.SaveChangesAsync();

    await transaction.CommitAsync();
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

Práctica

  1. Tienda básica: Crea Categoria, Producto con relación 1-N. Configura el DbContext con SQLite (UseInMemoryDatabase("test")), crea datos seed y escribe consultas LINQ para listar productos por categoría.
  2. Migraciones: Crea un proyecto con PostgreSQL, modela Tarea y Usuario, y genera la migración InitialCreate con dotnet ef migrations add.
  3. CRUD asíncrono: Implementa un ProductoRepository con ListarAsync, CrearAsync, ActualizarAsync y EliminarAsync usando EF Core.

En la siguiente lección aprenderemos a proteger la API con autenticación JWT y autorización basada en roles y políticas.

Usa AsNoTracking para consultas de solo lectura
EF Core rastrea los objetos que consulta para detectar cambios en SaveChangesAsync. Si solo vas a leer datos (para mostrarlo en una API, por ejemplo), llama a .AsNoTracking() en la query. Reduce el uso de memoria y mejora el rendimiento hasta un 30% en consultas grandes.
ExecuteUpdateAsync y ExecuteDeleteAsync
EF Core 7+ introdujo ExecuteUpdateAsync y ExecuteDeleteAsync para operaciones masivas sin cargar los objetos en memoria. Son hasta 10x más rápidas que el enfoque de cargar → modificar → SaveChanges para actualizar o eliminar múltiples registros.
Nunca hagas dotnet ef en producción
Las migraciones (dotnet ef database update) nunca deben aplicarse manualmente en producción. Usa el método programático context.Database.MigrateAsync() al inicio de la app, o un pipeline de CI/CD dedicado. Automatizar las migraciones previene errores manuales en entornos críticos.
using Microsoft.EntityFrameworkCore;

// ── Entidades de dominio ─────────────────────────────
public class Categoria
{
    public int    Id       { get; set; }
    public string Nombre   { get; set; } = string.Empty;
    public ICollection<Producto> Productos { get; set; } = new List<Producto>();
}

public class Producto
{
    public int      Id          { get; set; }
    public string   Nombre      { get; set; } = string.Empty;
    public decimal  Precio      { get; set; }
    public int      Stock       { get; set; }
    public bool     Activo      { get; set; } = true;
    public DateTime CreadoEn    { get; set; } = DateTime.UtcNow;

    // Clave foránea + propiedad de navegación
    public int        CategoriaId { get; set; }
    public Categoria  Categoria   { get; set; } = null!;
}

// ── DbContext ────────────────────────────────────────
public class TiendaDbContext : DbContext
{
    public TiendaDbContext(DbContextOptions<TiendaDbContext> options)
        : base(options) { }

    public DbSet<Producto>  Productos  => Set<Producto>();
    public DbSet<Categoria> Categorias => Set<Categoria>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Fluent API — configuración de mapeo
        modelBuilder.Entity<Producto>(e =>
        {
            e.HasKey(p => p.Id);
            e.Property(p => p.Nombre).IsRequired().HasMaxLength(200);
            e.Property(p => p.Precio).HasColumnType("decimal(18,2)");
            e.HasOne(p => p.Categoria)
             .WithMany(c => c.Productos)
             .HasForeignKey(p => p.CategoriaId)
             .OnDelete(DeleteBehavior.Restrict);
        });

        // Datos semilla
        modelBuilder.Entity<Categoria>().HasData(
            new Categoria { Id = 1, Nombre = "Electrónica" },
            new Categoria { Id = 2, Nombre = "Periféricos" }
        );
    }
}