Entity Framework Core · Ejercicio guiado · 45 min

Ejercicio EF Code First

Te damos una aplicación con tres clases de dominio y una página Blazor sencilla, pero sin base de datos. Vas a escribir el DbContext, generar migraciones y ver qué tablas arma Entity Framework a partir de tus clases, sin escribir una línea de SQL.

code first DbContext migraciones convenciones de EF herencia TPH SQL Server en Docker

Antes de arrancar

dotnet tool install --global dotnet-ef --version 9.*   # si ya la tenés, usá "update" en vez de "install"
0

Levantar la base y abrir la solución

docker compose · LabEF.sln · conexión desde DBeaver

Descomprimí LabEF-inicial.zip y abrí LabEF.sln con Rider. En la carpeta de la solución hay un docker-compose.yml con un SQL Server. Abrí la terminal de Rider (queda parada en esa carpeta) y levantalo:

docker compose up -d   # la primera vez descarga la imagen y tarda un poco

Ahora conectate a ese servidor con DBeaver, para poder mirar las tablas por dentro durante todo el ejercicio. Creá una conexión nueva (Database → New Database Connection, o el ícono del enchufe con un +), elegí SQL Server y completá:

Tocá Test Connection. Si DBeaver pide descargar el driver, aceptá. Después Finish.

Pista Si la conexión falla con un error que menciona el certificado o SSL, es porque el SQL Server del contenedor usa un certificado propio que DBeaver no reconoce. Tildá Trust Server Certificate en la configuración de la conexión. Es lo mismo que hace TrustServerCertificate=true en el connection string de la app.

Si da timeout, el problema no es el usuario ni la contraseña: revisá que el contenedor esté corriendo con docker ps y que el puerto sea 1433.
Checkpoint La conexión da OK y en el panel de DBeaver ves el servidor. Desplegalo: no existe ninguna base que se llame LabEF.
1

Recorrer la solución

qué hay y qué falta

La solución está separada en tres proyectos:

LabEF.Web  ──▶  LabEF.DataAccess  ──▶  LabEF.Domain
(Blazor)        (EF Core)              (clases de dominio)

LabEF.Domain/Vehiculo.cs, Auto.cs, Moto.cs     las clases que vamos a guardar
LabEF.DataAccess/SqlContext.cs                 el DbContext, todavía vacío
LabEF.DataAccess/VehiculoRepositorio.cs        lista y agrega vehículos usando el contexto
LabEF.Web/Program.cs                           falta registrar el contexto
LabEF.Web/appsettings.json                     el connection string ya está
LabEF.Web/Components/Pages/Vehiculos.razor     la página para probar

Abrí las tres clases de LabEF.Domain. Vehiculo es abstracta y tiene lo común; Auto y Moto heredan de ella y agregan un dato propio cada una. Fijate que son clases comunes de C#: no saben nada de bases de datos.

Eso es code first: vos escribís las clases, y Entity Framework deduce a partir de ellas qué tablas y columnas tiene que tener la base. El DbContext es la clase que une las dos cosas: representa la base de datos dentro de tu código.

Para pensar En la base relacional no existe la herencia: hay tablas y columnas. ¿Cómo guardarías una Auto y una Moto en tablas, si comparten algunos datos y otros no?
2

El DbContext y su registro

decirle a EF qué guardar y dónde

Abrí LabEF.DataAccess/SqlContext.cs y resolvé el TODO 1:

using LabEF.Domain;
using Microsoft.EntityFrameworkCore;

namespace LabEF.DataAccess;

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

    // TODO 1: agregá acá las propiedades que le dicen a EF qué clases tiene que guardar en la base.
}
Pista Cada clase que EF tiene que guardar se declara en el contexto con una propiedad de este estilo: public DbSet<MiClase> MisClases { get; set; }. Ojo: EF no sale a buscar subclases por su cuenta, solo conoce las clases que le nombrás.
Cómo trabaja EF: por convención EF no necesita que le expliques cada detalle de la base: aplica convenciones, reglas que deduce del código. Algunas:

· El nombre del DbSet se usa como nombre de la tabla.
· Una propiedad llamada Id (o NombreDeClaseId) es la clave primaria, y si es int se autoincrementa.
· Cada propiedad pública con get y set es una columna, con un tipo de SQL según el tipo de C#.
· Si la propiedad admite null en C#, la columna admite NULL; si no, es NOT NULL.

Cuando la convención no alcanza, se puede configurar a mano. Por ahora la dejamos trabajar.

Ahora abrí LabEF.Web/Program.cs y resolvé el TODO 2. El connection string ya está en appsettings.json con el nombre DefaultConnection:

// TODO 2: registrá SqlContext para que use SQL Server con el connection string
// "DefaultConnection" de appsettings.json

builder.Services.AddScoped<VehiculoRepositorio>();
Pista Un DbContext se registra con builder.Services.AddDbContext<MiContexto>(options => ...). Adentro le decís qué motor usar (options.UseSqlServer(...)) y le pasás el connection string, que se lee con builder.Configuration.GetConnectionString("Nombre").

Corré LabEF.Web con ▶ Run y entrá a Vehículos desde el menú. Mirá qué pasa y después ■ Stop.

Checkpoint Compila, pero la página de Vehículos falla con un error de SQL Server: la base LabEF no existe. Tener las clases y el contexto no alcanza: nadie creó la base todavía.
3

La primera migración

EF traduce tus clases a un plan de cambios

Una migración es una clase de C# que EF genera comparando tu modelo (las clases y el contexto) con el estado anterior. Describe qué hay que cambiar en la base para que coincida con tu código. Generarla no toca la base: solo escribe archivos.

Regla de oro Antes de crear o aplicar una migración, la app tiene que estar detenida con ■ Stop. EF compila la solución para leer tu modelo, y con la app corriendo los archivos quedan bloqueados.

Click derecho sobre el proyecto LabEF.DataAccess → Entity Framework Core → Add Migration. En el diálogo:

Pista Si el diálogo no ofrece SqlContext o la migración falla, revisá los dos proyectos: el de migraciones tiene que ser el que contiene el contexto, y el de inicio, el que lo registra en Program.cs.

Aparece una carpeta Migrations en LabEF.DataAccess. Antes de abrirla, escriban su predicción. Después abran el archivo que termina en _Inicial.cs, lean el método Up y completen qué pasó.

PreguntaPredicción (antes de abrir)Qué dice la migración
¿Cuántas tablas crea para los vehículos? ¿Con qué nombre?
¿Qué columnas tiene? ¿Hay alguna que no esté en tus clases?
¿Qué columnas admiten null? ¿Coincide con las propiedades de tus clases?
Checkpoint En Migrations hay tres archivos: la migración _Inicial.cs, su .Designer.cs y un SqlContextModelSnapshot.cs, que es la foto del modelo con la que EF va a comparar la próxima vez.
Para pensar CantidadPuertas y Cilindrada son int, que en C# no puede ser null. ¿Por qué EF igual las creó como columnas que admiten NULL?
4

Aplicar la migración y mirar la base

la jerarquía, vista desde SQL

Click derecho sobre LabEF.DataAccess → Entity Framework Core → Update Database, con los mismos proyectos de antes. Cuando termine, en DBeaver seleccioná la conexión y apretá F5 para refrescarla.

Dónde mirar Las tablas quedan en Databases → LabEF → Schemas → dbo → Tables. Si no ves la base LabEF después de refrescar, puede que DBeaver esté mostrando solo una base: editá la conexión (click derecho → Edit Connection) y activá la opción para mostrar todas las bases. Si con eso tampoco aparece, la migración no se aplicó: revisá la salida del Update Database.
Checkpoint Ahora existe la base LabEF, con dos tablas: la de vehículos y __EFMigrationsHistory.

Corré LabEF.Web, entrá a Vehículos y agregá un auto y una moto. Después ■ Stop y abrí un editor de consultas sobre la base LabEF (en DBeaver, click derecho sobre la base → SQL Editor → New SQL script) y ejecutá:

SELECT * FROM Vehiculos;
SELECT * FROM __EFMigrationsHistory;
Checkpoint Las dos filas están en la misma tabla. La columna Discriminator dice Auto o Moto, y cada fila tiene NULL en la columna que no le corresponde. En __EFMigrationsHistory hay una fila con tu migración Inicial.
Cómo modela EF la herencia Lo que acabás de ver es TPH (Table Per Hierarchy, una tabla por jerarquía): toda la jerarquía va en una sola tabla, con una columna Discriminator que indica de qué clase es cada fila. Es lo que EF hace por convención. Hay otras dos estrategias, que se eligen configurándolas en el contexto:

· TPT (Table Per Type): una tabla por clase. Vehiculos guarda lo común, y Autos y Motos guardan lo propio, unidas por el Id. Sin columnas vacías, pero cada consulta necesita juntar tablas.
· TPC (Table Per Concrete type): una tabla por clase concreta, cada una con todas las columnas. No hay tabla Vehiculos, y los datos comunes se repiten en la estructura de cada tabla.

TPH suele ser la más rápida para consultar; a cambio, las columnas propias de cada subclase tienen que admitir NULL.
Para pensar Si mañana agregás Camion con cinco propiedades propias, ¿cómo queda la tabla con TPH? ¿En qué caso te convendría más TPT o TPC?
5

Cambiar el modelo

el código y la base se desincronizan

Agregá esta propiedad a Vehiculo, debajo de Anio:

public string Color { get; set; } = "";

No crees ninguna migración todavía. Corré LabEF.Web y entrá a Vehículos.

Checkpoint La página falla con un error de SQL Server que habla de una columna Color inválida. Tu código ya conoce el color, pero la base no.

■ Stop. Ahora probá aplicar Update Database sin crear migración y leé el mensaje de EF:

The model for context 'SqlContext' has pending changes. Add a new migration before updating the database.

TODO 3: creá una migración llamada AgregarColor, leé su método Up y aplicala con Update Database. Volvé a correr la app.

Checkpoint La migración nueva solo agrega una columna, sin recrear la tabla. La página vuelve a funcionar, el auto y la moto siguen ahí, y __EFMigrationsHistory tiene ahora dos filas.
Para pensar La columna Color no admite NULL, pero ya había filas en la tabla. ¿Qué valor les puso EF a esas filas viejas? Y si un compañero baja tu código con las dos migraciones y tiene su propia base vacía, ¿qué pasa cuando corre Update Database?

🏁 Vuelta extra (opcional)

para los que terminaron antes