Skip to content

Entity Framework

Entity Framework (EF) es un ORM (Object-Relational Mapper) para .NET que sirve para trabajar con bases de datos usando clases y objetos de C# en lugar de escribir la mayor parte del SQL “a mano”.

En la práctica, te ayuda a:

  • Mapear tablas a clases (y filas a objetos): por ejemplo, una tabla Clientes se representa como una clase Cliente.
  • Hacer consultas y operaciones CRUD (crear, leer, actualizar, borrar) de forma más cómoda, normalmente con LINQ (Where, Select, Include, etc.).
  • Gestionar relaciones entre entidades (1:N, N:M) y la carga de datos relacionados.
  • Aplicar migraciones para crear y evolucionar el esquema de la base de datos desde el código (modelo “code-first”).
  • Reducir código repetitivo de acceso a datos y centralizar la configuración en un DbContext.

En resumen: EF te permite acelerar el desarrollo y mantener el acceso a datos más orientado a dominio, con menos fricción que programar todo con SQL y mapeos manuales.

Ejemplo sencillo

Modelo de datos

[Table("Recordings")]
public record Recording
{
    [Key]
    public int Id { get; init; }

    public string SourceUrl { get; init; } = string.Empty;

    public string Destination { get; init; } = string.Empty;
}

Contexto de base de datos

public class RecordingContext(DbContextOptions<RecordingContext> options) : TitaniaDbContext<BookingContext>(options)
{
    public DbSet<Recording> Recordings => Set<Recording>();

    protected oveerride void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBulder.Entity<Recording>().HasKey(r => r.Id);
    }
}

// Construir cadena de conexión en tiempo de diseño
public class BookingsContextFactory : TitaniaDbContextFactory<BookingContext> { }

Mi primera migración, chispas

Una migración en Entity Framework es un conjunto de cambios versionados que describe cómo debe evolucionar el esquema de la base de datos para mantenerse sincronizado con tu modelo (tus clases/entidades).

dotnet ef migrations add InitialScaffold --context RecordingContext --output-dir .\Migrations\Recording

Para aplicarla a la base de datos manualmente haríamos lo siguiente:

dotnet ef update --context RecordingContext

🛑 ¿Qué cadena de conexión se utiliza en este caso? En tiempo de ejecución, se indica al construir el DbContext, pero no existe en tiempo de compilación. Para esto, .NET tiene previstos los IDesignTimeDbContextFactory<T> que ayudan a construir el DbContext<T>.

Sin embargo, no vamos a hacerlo a mano, esperaremos a que se haga automáticamente la primera vez que entre el bucle de monitorización.

TitaniaDbContext

Más arriba heredamos de TitaniaDbContext<T> para evitar tener que indicar la migración automática en todos los constructores de los contextos que utilicemos. Esto es lo que hace:

public abstract class TitaniaDbContext<T> : DbContext where T : DbContext
{
    private static readonly Lock locker = new();

    protected TitaniaDbContext(DbContextOptions<T> options) : base(options)
    {
        try
        {
            lock (locker)
            {
                IEnumerable<string> pendingMigrations = this.Database.GetPendingMigrations();
                if (pendingMigrations.Any() && !isMigrationProcess())
                {
                    this.Database.Migrate();
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

    private static bool isMigrationProcess() => Environment.GetCommandLineArgs().Any(arg => arg.Contains("ef", StringComparison.OrdinalIgnoreCase));
}

Driver básico

public class DriverEntityBookingDb : DriverEntity
{
    // ...
    public override async Task<object> MonitorAsync(Parameter parameter, ICommsManager commsManager)
    {
        try
        {
            object result = Constants.DRIVER_CNXL_RESPONSE;

            if (commsManager is EntityManager entityManager)
            {
                using var context = entityManager.CreateContext<RecordingContext>();
                using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(entityManager.Timeout));

                if (context is not null)
                {
                    switch (parameter.Id)
                    {
                        case "RECORDS":
                            result = await context.Records.Take(100).ToListAsync(cts.Token);
                            break;
                        default:
                            result = await base.MonitorAsync(parameter, commsManager);
                            break;
                    }
                }
            }

            return result;
        }
        catch (Exception ex)
        {
            this.LogManager.LogException(ex);
            return $"{Constants.DRIVER_EXCEPTION_RESPONSE}: {ex.Message}";
        }
    }
}

¿Qué tenemos en DriverEntity?

Una buena cantidad de métodos genéricos que nos permiten simplificar nuestro driver si solamente se van a realiar las operaciones básicas CRUD (Create, Read, Update, Delete).

public override string Control(Parameter parameter, ICommsManager commsManager, object newValue, string user)
{
    try
    {
        string result = Constants.DRIVER_ERROR_RESPONSE;

        if (commsManager is EntityManager entityManager)
        {
            using var context = entityManager.CreateContext<RecordingContext>();
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(entityManager.Timeout));

            if (context is not null)
            {
                string jsonOrValue = newValue.ToString() ?? string.Empty;

                switch (parameter.Id)
                {
                    case "INSERT_RECORDING":
                        result = this.genericInsert<Recording>(context, jsonOrValue, cts.Token).Result;
                        break;
                    // TBC
                    default:
                        result = base.Control(parameter, commsManager, newValue, user);
                        break;
                }
            }
        }

        return result;
    }
    catch (Exception ex)
    {
        this.LogManager.LogException(ex);
        return $"{Constants.DRIVER_EXCEPTION_RESPONSE}: {ex.Message}";
    }
}