Entity Framework Core Database First en aplicaciones .NET MVC

Si tuviéramos que desarrollar una nueva aplicación Web que utilice las últimas tecnologías ASP.NET de Microsoft, seguramente la gran mayoría de desarrolladores nos decantaríamos por .NET Core MVC y Entity Framework Core como ORM (Object-Relational Mapper). Como ya sabemos el ORM es el encargado de 'mapear' las clases del Modelo MVC con las entidades de la base de datos (tablas).

Pero, que ocurriría si nuestra aplicación .NET Core tuviera que trabajar con una base de datos ya existente y posiblemente con datos. Para estos casos, Entity Framework Core nos provee de un sistema de 'ingeniería inversa' (Database First) para generar de manera automática todas las clases del Modelo desde el esquema de una base de datos, así como construir el 'andamiaje' interno (Scaffold) para trabajar con ella.

EF Core

En este Post veremos cómo crear automáticamente el 'Modelo de datos' de una aplicación ASP.NET Core MVC a partir de una base de datos SQL Server, utilizando Entity Framework Core Database First. Como entorno de desarrollo utilizaremos Visual Studio Community 2017.

Instalación de Entity Framework Core

Para realizar la instalación utilizaremos  la consola de administración de paquetes Nuget: Herramientas > Administrador de paquetes NuGet > Consola del Administrador de paquetes.

En primer lugar instalaremos el paquete principal de Entity Framework. En este caso el correspondiente al proveedor de datos SQL Server.

PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer

A continuación instalamos el paquete de 'herramientas'. Entre otras cosas, este paquete será el encargado de crear el 'Modelo de Datos' en nuestra aplicación MVC.

PM> Install-Package Microsoft.EntityFrameworkCore.Tools

Por último, tenemos la opción de instalar el paquete de 'diseño'. Este paquete nos ayudará a crear los Controladores y las Vistas de nuestra aplicación (Scaffolding), en función del 'Modelo' y 'Contexto' de datos generados automáticamente por Entity Framework Database First.

PM> Install-Package Microsoft.VisualStudio.Web.CodeGeneration.Design

La ingeniería inversa: Database First

Antes de comenzar, es necesario decir que Database First en Entity Framework Core 2.0 no dispone de un 'diseñador' ni nada que nos permita generar el 'Modelo' y 'Contexto' de datos de manera interactiva. La actual herramienta solo permite hacer la generación desde una base de datos inicial, pero si luego hay cambios sobre ésta no podremos incorporarlos al Modelo. En la próxima versión Core 2.1 se supone que se incorporará esta posibilidad.

Dicho esto, el proceso de 'ingeniería inversa' debemos realizarlo entonces a través de la consola de administración de paquetes Nuget con el comando Scaffold-DbContext. Los 'parámetros' de configuración para este comando son los siguientes:

  -Connection <String>   La cadena de conexión a la base de datos.
  -Provider <String>   El proveedor de datos (P. ej. Microsoft.EntityFrameworkCore.SqlServer)
  -OutputDir <String>   El directorio donde colocar las clases del 'Modelo de datos'.
  -ContextDir <String>   El directorio donde colocar la clase de 'Contexto de datos' (DbContext).
  -Context <String>   El nombre de la clase de 'Contexto de datos' (DbContext).
  -Schemas <String[]>   La o las Bases de datos desde donde generar el 'Modelo de datos'.
  -Tables <String[]>   La o las Tablas para generar las clases del 'Modelo de datos'.
  -DataAnnotations   Usar 'atributos' (Data Annotations) para configurar las clases del 'Modelo de datos'.
  -UseDatabaseNames   Usar nombres de tablas y columnas directamente desde la base de datos.
  -Force   Sobrescribir archivos existentes.

 

Para realizar este ejemplo utilizaremos la base de datos de pruebas Northwind de Microsoft (Script disponible para descargar al final del Post). El 'Modelo de datos' lo generaremos a partir de sólo dos tablas (Territories y Region) relacionadas entre sí por una clave 'foránea' (FK_Territories_Region). También indicaremos a Entity Framework que genere los 'atributos' (Data Annotations) en las clases del Modelo mediante el parámetro -DataAnnotations.

DatabaseFirstTablas

Ejecutamos entonces el comando Scaffold-DbContext desde la Consola del Administrador de paquetes:

PM> Scaffold-DbContext "data source=*****;initial catalog=Northwind;user id=*****;password=*****" 
Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Tables Region, Territories 
-DataAnnotations -Context "AppDbContext"

Analizando el resultado

Una vez finalizado el proceso de ingeniería inversa, nuestra aplicación ya dispone de las clases del 'Modelo de datos' en la carpeta Models

Clase de Modelo Territories.cs:

    [Table("Territories", Schema = "dbo")]
    public partial class Territories
    {
        [Key]
        [Column("TerritoryID")]
        [StringLength(20)]
        public string TerritoryId { get; set; }

        [Required]
        [Column(TypeName = "nchar(50)")]
        public string TerritoryDescription { get; set; }

        [Column("RegionID")]
        public int RegionId { get; set; }

        // PROPIEDAD DE NAVEGACIÓN
        [ForeignKey("RegionId")]
        public Region Region { get; set; }
    }

Clase de Modelo Region.cs:

    [Table("Region", Schema = "dbo")]
    public partial class Region
    {
        public Region()
        {
            Territories = new HashSet<Territories>();
        }

        [Column("RegionID")]
        public int RegionId { get; set; }

        [Required]
        [Column(TypeName = "nchar(50)")]
        public string RegionDescription { get; set; }

        // PROPIEDAD DE NAVEGACIÓN
        public ICollection<Territories> Territories { get; set; }
    }

También se ha creado la clase de 'Contexto de datos' (DbContext), la cual se encargará de 'mapear' las clases del Modelo con las entidades de la base de datos (tablas).

Clase de 'Contexto de datos' AppDbContext.cs:

    public partial class AppDbContext : DbContext
    {
        // Entidades del Modelo de datos
        public virtual DbSet<Region> Region { get; set; }
        public virtual DbSet<Territories> Territories { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                // Contexto de conexión a SQL Server
                optionsBuilder.UseSqlServer(@"data source=*****;initial catalog=Northwind;
                                              user id=*****;password=*****");
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Region>(entity =>
            {
                entity.Property(e => e.RegionId).ValueGeneratedNever();
            });

            modelBuilder.Entity<Territories>(entity =>
            {
                entity.Property(e => e.TerritoryId).ValueGeneratedNever();

                // Definición de índice
                entity.HasIndex(e => e.RegionId);                

                // Definición de la clave foránea
                entity.HasOne(d => d.Region)
                    .WithMany(p => p.Territories)
                    .HasForeignKey(d => d.RegionId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_Territories_Region");
            });
        }
    }

Una vez llegados a este punto, es necesario realizar una serie de modificaciones sobre el código 'auto-generado' para que se ajuste a los patrones de diseño en .NET Core.

La cadena de conexión

Como podemos observar, la 'cadena de conexión' a la base de datos aparece directamente 'en código' en la clase AppDbContext.cs. Esto no es una buena práctica, así que la definiremos en el archivo de configuración de la aplicación appsettings.json.

{
  "ConnectionStrings": {
    "AppConnection": "data source=*****;initial catalog=Northwind;user id=*****;password=*****"
  },
}

La inyección de dependencias

El concepto de inyección de dependencias, es un aspecto fundamental en las aplicaciones ASP.NET Core ya que esta funcionalidad viene implementada de manera 'nativa' en este framework. En .NET Core, los 'servicios' se registran en el archivo Startup.cs (con inyección de dependencias) durante el inicio de la aplicación, para posteriormente ser 'inyectados' a los componentes de la aplicación que los necesiten (como los controladores MVC) a través de propiedades o parámetros de constructor. 

Siguiendo este patrón de diseño, registraremos el 'Contexto de datos' de la aplicación (AppDbContext.cs) como un 'servicio', en el método ConfigureServices(IServiceCollection services) del archivo Startup.cs

   // This method gets called by the runtime. 
   // Use this method to add services to the container.
   public void ConfigureServices(IServiceCollection services)
   {
      services.AddMvc();
      // Registro del Contexto de datos como Servicio
      services.AddDbContext<AppDbContext>(options =>
         options.UseSqlServer(Configuration.GetConnectionString("AppConnection")));
   }

Por último, solo quedaría eliminar el código para la conexión con SQL Server (UseSqlServer("cadena_de_conexión")) en la clase AppDbContext.cs, y pasar la cadena de conexión a través del constructor de la clase mediante inyección de dependencias.

El código de la clase de 'Contexto de datos' AppDbContext.cs quedaría así:

    public partial class AppDbContext : DbContext
    {
        // Entidades del Modelo de datos
        public virtual DbSet<Region> Region { get; set; }
        public virtual DbSet<Territories> Territories { get; set; }

        // CONSTRUCTOR DE LA CLASE 
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
        {
                
        }

        // ELIMINAMOS ESTE CÓDIGO
        //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        //{
        //    if (!optionsBuilder.IsConfigured)
        //    {
        //        // Contexto de conexión a SQL Server
        //        optionsBuilder.UseSqlServer(@"data source=*****;initial catalog=Northwind;
        //                                      user id=*****;password=*****");
        //    }
        //}

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Region>(entity =>
            {
                entity.Property(e => e.RegionId).ValueGeneratedNever();
            });

            modelBuilder.Entity<Territories>(entity =>
            {
                entity.Property(e => e.TerritoryId).ValueGeneratedNever();

                // Definición de índice
                entity.HasIndex(e => e.RegionId);                

                // Definición de la clave foránea
                entity.HasOne(d => d.Region)
                    .WithMany(p => p.Territories)
                    .HasForeignKey(d => d.RegionId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_Territories_Region");
            });
        }
    }

Consideraciones finales

Como ya comentamos anteriormente, podríamos decir que el proceso de 'ingenieria inversa' (Database Fisrt) en Entity Framework Core es como una 'vía de dirección única'. Si en algún momento necesitamos realizar modificaciones en la base de datos, estos cambios no podremos incorporarlos de manera 'dinámica' (utilizando Scaffold-DbContext) a las clases del Modelo ni al 'Contexto de datos'. 

Si queremos mantener un estado de 'sincronización' correcto entre nuestra aplicación y la base de datos después del proceso de 'ingenieria inversa', mi recomendación es utilizar las funcionalidades que Entity Framework Code First nos ofrece. Esto quiere decir que las futuras modificaciones que tengamos que hacer en la base de datos, las realizaremos a través de código en el 'Modelo' y 'Contexto' de datos y las actualizaremos mediante 'migraciones'. 

Para esto, realizaremos una migración inicial hacia la base de datos cuyo objetivo será crear la tabla __MigrationsHistory (historial de migraciones), y le indicaremos mediante el parámetro –IgnoreChanges que no realice ningún cambio.

PM> Add-Migration SincronizarDataBaseInicial –IgnoreChanges 
...
PM> Update-Database

A partir de este momento nuestro 'Modelo' y 'Contexto' de datos ya quedarán sincronizados con la base de datos. Las futuras modificaciones que tengamos que realizar sobre la base de datos las realizaremos primero en el código (Code First), y posteriormente las trasladaremos a la base de datos mediante el sistema habitual de 'migraciones' de Entity Framework.

Descargas

Script base de datos - northwind.sql

  Compartir


  Nuevo comentario

El campo Comentario es obligatorio.
El campo Nombre es obligatorio.

  Comentarios

JoseSepulveda JoseSepulveda

Excelente articulo. Andaba buscando algo como este ejemplo. Gracias !!
Ariel Lipschutz Ariel Lipschutz

Muy bueno Rafael, muchas gracias!
Luego de implementar tu código me encontré con un problema.
No me trae las relaciones.
Es decir si llamo a db.users.find(id); obtengo sin problema el objeto user.
Pero si el objeto user tiene una relación, por ejempo contents, la relación contents está vacia.
Alguna idea?
Gracias!
Rafael Acosta Administrador Rafael Acosta

@Francisco Araya:

Hola, 


Efectivamente, la herramienta Scaffold-DbContext de Entity Framework Core te permite realizar operaciones de Ingeniería Inversa sobre una BD existente (Database First) de una solo vez, o sea, no es una herramienta incremental con la que puedas ir creando los Modelos de tu aplicación según los vayas necesitando.


Aun así, si es posible realizar lo que tú necesitas mediante el uso de clases parciales.


Para más información: https://es.stackoverflow.com/questions/308728/agregar-tabla-existente-a-proyecto-asp-net-core-ef-core-database-first 


Francisco Araya Francisco Araya

Hola, muy buena explicación es lo que andaba buscando, solo una consulta, el ejemplo lo hiciste con dos tablas, pero si luego yo quisiera incluir una tabla de la base de datos como lo hago?.. Yo he realizado code first con tablas nuevas, pero no se como agregar una tabla ya existente que quisiera usar en mi aplicación luego fe haber hecho el database first.

Saludos.


Utilizamos cookies propias y de terceros para mejorar nuestros servicios y ofrecerle una mejor experiencia de navegación. Si continúa navegando consideramos que acepta su uso. Más información   Acepto