Usando “Migrations” con EF Core 2.1

En este artículo vamos a ver como configurar correctamente la característica “Migrations”  de EF Core 2. Utilizaremos la  version 2.1.1 de Entity Framework Core y haremos uso de la herramienta “CLI” del SDK de .NET core: “dotnet ef”, en lugar de poweshell. Asumo que el el lector ya tiene ciertos conocimientos básicos de Entity Framework.

En primer lugar, vamos a repasar alguna diferencias importantes de EF Core con respecto a EF 6.x:

EF Core siempre es “Code First”.

En EF Core no existe el equivalente a un archivo de relaciones entre las clases de datos y las tablas de la base de datos por lo que siempre que hablamos de EF Core no referimos a EF en modo  “Code First”.

Cuando hacemos ingeniería inversa de la base de datos usando el comando “scaffold”, EF Core nos crea las clases de datos en lugar de un archivo edmx.

El mapeo de las clases POCO con las tablas de la base de datos es asumido por convención, mediante atributos  (“annotations”)  o usando “Fluent API”.

Puedes saber mas en:

https://docs.microsoft.com/en-us/ef/core/get-started/aspnetcore/existing-db

Herramienta CLI como alternativa a Powershell: “dotnet ef”

De la misma forma que existe una herramienta CLI para trabajar con .NET Core: “dotnet”, el SDK de .NET Core nos proporciona otra para trabajar con EF Core: “dotnet ef”. (CLI: Command Line Interface) que lógicamente tenemos que usar desde la consola de comandos.

Al ser .NET Core un framework multiplataforma y, debido a que Powershell no tiene, necesariamente, que estar instalado en un sistema operativo que no sea Windows es imprescindible disponer de una herramienta alternativa. Que además funciona bastante bien.

Básicamente, “dotnet ef” nos proporciona comandos para trabajar con: Contextos, Bases de datos y Migraciones.

El siguiente diagrama muestra los módulos y los comandos que nos ofrece “dotnet ef”:

image

Puedes saber más en:

https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dotnet

Un apunte para aquellos que estén utilizando una versión, del SDK, menor que la 2.1, para poder usar el CLI “dotnet Ef”  deben editar el archivo de proyecto y agregar una la siguiente línea:

**No es necesario a partir de la versión 2.1

<ItemGroup>
    <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.2" />
    <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.2" />
</ItemGroup>

 

Inicialización y conexión

En EF Core ya no existen los inicializadores, por tanto la única manera de sincronizar el modelo lógico con el modelo físico, es mediante el framework de migraciones, utilizando la herramienta CLI “dotnet ef” o Powershell.

En EF 6 es posible configurar los datos  y el mecanismo de conexión en el archivo de configuración “web.config”, configurando un proveedor de datos. Cuando creamos una nueva instancia del contexto de datos, si hemos utilizado el mismo nombre para la cadena de conexión que para la clase del contexto de datos, no necesitamos hacer nada mas. Por convención EF usara esta cadena de conexión para inicializar el contexto. Si no, podemos pasar la cadena de conexión al constructor de la clase del contexto, o directamente establecerlo en la llamada a la clase base del contexto como en el siguiente ejemplo:

<connectionStrings>
  <add name="BneCatalogDb" connectionString="data source=(LocalDb)\v11.0; ..." />
</connectionStrings>

...

public class CatalogCtx : DbContext
{
  static CatalogCtx()
  {
    Database.SetInitializer(new MigrateDatabaseToLatestVersion<CatalogCtx, Configuration>());
  }
  public CatalogCtx()
    : base("name=BneCatalogDb")
  {
  }
}

Pero en EF Core, ya no es posible configurar “providers” y  además no deberíamos hacer uso de un archivo web.config. Esta es una característica habitual del framework “normal”  que va desplegado sobre IIS y no de .NET Core que es cross-platform y puede ir desplegado en cualquier SO.

Por lo que la configuración de la conexión hay que hacerla de alguna otra forma. Básicamente disponemos de 2 alternativas:

  1. Sobreescribir el método OnConfiguring de la clase del contexto y configurar el objeto “DbContextOptionsBuilder”.

    public class LibraryDbContext : DbContext
    {
        public LibraryDbContext(DbContextOptions options):base(options)
        {
        }
    
        DbSet<Book> Books { get; set; }
        DbSet<BookCollection> BooksCollecions { get; set; }
    
        //
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=(localdb)\ProjectsV13;Database=BookStore;Integrated Security=True;");
            base.OnConfiguring(optionsBuilder);
        } 
    }

     

  2. Pasar al constructor un objeto “DbContextOptionsBuilder” debidamente configurado.

    public class LibraryDbContext : DbContext{
          public LibraryDbContext(DbContextOptions options) : base(options)
          {
          }
          DbSet<Book> Books { get; set; }
          DbSet<BookCollection> BooksCollecions { get; set; }
    }
    
    ...
    
    var options = new DbContextOptionsBuilder<LibraryDbContext>().UseSqlServer("Server=localhost;Port=3306;Database=bookStore;Uid=bookstore;Pwd=p@ssw0rd").Options;
    
    using (var contextDb = new LibraryDbContext(options))
    {
    
    }
    

     

En ambos casos  tendremos que hacer uso de un método para indicar con que proveedor de datos vamos a trabajar. Este método es implementado, mediante extensión de la clase “DbContextOptionsBuilder”, por lo que cada proveedor debe proporcionar el suyo propio. No tendremos acceso a este método hasta que no hayamos creado un referencia a la librería del proveedor en nuestro proyecto.

Podemos ver que es sencillo utilizar diferentes proveedores de datos, siempre dependiendo de las características del driver de cada uno:

//SqlServer
//Reference to Microsoft.EntityFrameworkCore.SqlServer
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseSqlServer(@"Server=(localdb)\ProjectsV13;Database=BookStore;Integrated Security=True;");
    base.OnConfiguring(optionsBuilder);
} 
//MySql
//Reference to Pomelo.EntityFrameworkCore.MySql
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseMySql(@"Server=localhost;Port=3306;Database=bookStore;Uid=bookstore;Pwd=p@ssw0rd;");
    base.OnConfiguring(optionsBuilder);
}
//SQLite
//Reference to Microsoft.EntityFrameworkCore.Sqlite
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseSqlite(@"data source=bioregUsers.db");
    base.OnConfiguring(optionsBuilder);
}

Estas son las alternativas comunes para configurar y crear un contexto de datos con EF Core, pero para poder usar “Migrations” necesitamos hacer algunos cambios, porque si intentamos agregar una migración recibiremos el siguiente error:

image

Como hemos visto en los ejemplos anteriores, en ambos casos tenemos un constructor con parámetros. El framework de migraciones accede al contexto mediante reflexión y no puede hacerlo a través de un constructor con parámetros, la clase de nuestro contexto debería tener un constructor público sin parámetros que pudiera ser invocado mediante reflexión. Pero entonces obtiene una instancia del contexto sin configurar. La solución pasa por crear una clase factoría que permite al CLI de migraciones, “dotnet ef”, crear un contexto a través del constructor con parámetros como en el siguiente ejemplo.

public class BookStoreDbContext : DbContext
{
    public BookStoreDbContext(DbContextOptions options)
    {
        
    }

    DbSet<Book> Books { get; set; }
    DbSet<BookCollection> BooksCollecions { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseMySql(@"Server=localhost;Port=3306;Database=bookStore;Uid=bookstore;Pwd=p@ssw0rd;");
        base.OnConfiguring(optionsBuilder);
    }

    
}
public class BookStoreDbContextFactory : IDesignTimeDbContextFactory<BookStoreDbContext>
{
    public BookStoreDbContext CreateDbContext(String[] args)
    {
        var options = new DbContextOptionsBuilder().UseMySql("").Options;
        return new BookStoreDbContext(options);
    }
}

ahora, el CLI de migraciones detectará, mediante reflexión, una clase que implementa el interface «IDesingTimeDbContextFactory» el cual incluye un método «CreateDbContext» que será invocado y podrá crear un instancia del contexto mediante un constructor con parametros.

Por tanto. ahora sí podemos usar sin problemas el CLI de “Migrations” para agregar una migración.

image

image

El conjunto de cmdlets de Powershell para “Migrations” nos proporciona prácticamente la misma funcionalidad que el CLI del SDK de .NET Core “dotnet ef”, pero yo prefiero utilizar esté último.

No vamos a entrar muy en detalle sobre la funcionalidad que nos proporciona “dotnet ef”, aunque si repasaremos los comandos más comunes:

Recomiendo visitar:

https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dotnet

para revisar las características en detalle y ampliar conocimientos.

Antes de pasar a explicar los comandos que proporciona la herramienta “dotnet ef”, debo recalcar, que, en todos comandos podemos especificar el proyecto “target” usando la opción “ – – project ” que es el proyecto al que se agregan los archivos de la migración,  y el proyecto “startup” usando la opción “ – -startupProject “ que es el proyecto utilizado para generar la migración. Normalmente son el mismo, y si no se especifican estas opciones “dotnet ef” asumirá que el proyecto “target” y “startup” es el mismo y se encuentra en en el directorio actual.

Agregar migraciones

El módulo para migraciones “dotnet ef migrations” nos ofrece comandos para agregar, eliminar y listar migraciones así como generar un script en SQL para ejecutar en la base de datos.

Es muy importante tener en cuenta que, crear una migración no implica que se apliquen los cambios en la base de datos, hay que ejecutar el comando “update” del módulo “database” para que se apliquen los cambios físicamente.

Para crear una nueva migración utilizaremos el comando “add” que sólo necesita un argumento: un nombre para la migración, mi consejo es que utilizar algo que indique la versión como: “v1.3” de esta forma siempre podemos tener una referencia orientativa de los posibles cambios que contiene. Obviamente también es necesario que existan cambios en los modelos de datos de nuestro dominio.

El comando “add” admite una opción “ -o ( – – output) ” que nos permite especificar un directorio en el que se escribirán los archivos de la migración. El valor de esta opción es la ruta de un directorio  relativa a la ruta del proyecto, y si no especificamos nada los archivos se grabarán en el directorio “/Migrations”.

Si hemos hecho uso al menos una vez de la opción  “ –o ( – – output) ” y ya se ha creado una migración en ese directorio, no es necesario volver a especificarlo. Las siguientes migraciones se crearán en ese mismo directorio.

C:\...> dotnet ef migrations add "v1.3"  (save to "Migrations")
C:\...> dotnet ef migrations add "v1.3" -o "Migrations/Users"  (save to "Migrations/Users")

C:\...> dotnet ef migrations remove
C:\...> dotnet ef migrations remove -f (revert database changes)

C:\...> dotnet ef migrations list


image

Por último el comando “script” nos permite crear el script SQL de una migración o de un conjunto de cambios entre varias migraciones. Podemos especificar la migración de origen y la migración de destino, si no especificamos nada se generará un script SQL de generación de todos los cambios incluidos en todas la migraciones desde la más antigua a la más nueva.

C:\...> dotnet ef migrations script -i -o "SQLMigrations\v1.4.sql"
C:\...> dotnet ef migrations script "v1.2" "v1.3" -i -o "SQLMigrations\v1.2To1.3.sql"

image

En este ejemplo vemos, que los cambios en la migración v1.3, consisten únicamente en el campo «Language» de una clase «User» mapaeada a la tabla «AspNetUsers».

ALTER TABLE `AspNetUsers` ADD `Language` longtext NULL;

INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
VALUES ('20180926090619_v1.2', '2.1.1-rtm-30846');

 

Sincronizar la base de datos

El módulo para trabajar con la base de datos, “dotnet ef database”, sólo nos proporciona 2 comandos: “drop” y “update”. Prácticamente, sólo vamos a usar el comando “update” para aplicar una migración sobre la base de datos. Este comando admite un único argumento: el nombre de la migración a aplicar. Si no se proporciona, se aplicará la última migración generada.

C:\...> dotnet ef database update "v1.4"  (apply migration named "v1.4")
C:\...> dotnet ef database update (apply last migration)

En el caso de haber generado el script de una migración o rango de migraciones, podemos aplicarlo sobre la base de datos y en este caso no es necesario utilizar el comando “update”.

Como hemos visto en el ejemplo de código anterior, el script incluye la entrada en la historia de migraciones por lo que podríamos revertir los cambios a pesar de haberlos aplicado mediante el script. Para esto es muy importante no quitar o modificar la inserción en la tabla con la historia de migraciones.

Mi recomendación es aplicar los cambios de las migraciones en el entorno de producción siempre mediante script, es la única manera de no tener problemas, y, también dejar la responsabilidad de la actualización al departamento de IT. Para proyectos pequeños puede valer utilizar el comando “update”, pero cuando el proyecto se complica o ya tenemos datos reales no es una buena opción.

Trabajar con los contextos

El CLI del SDK “dotnet ef” nos ofrece un módulo para trabajar con los contextos de datos y además permite trabajar con múltiples contextos en un mismo proyecto. Podemos obtener información acerca de un contexto, listar los contextos existentes y hacer un “scaffold” de un contexto. El “scaffold” creará el modelo de datos lógico equivalente al modelo físico de la base de datos.

Los comandos «info» y «list» son muy sencillos y no necesitan explicación, pero el comando «scaffold» es más complicado y tiene muchas opciones. Por lo que recomiendo revisar el contenido de la dirección que he apuntado anteriormente.

Algunos ejemplos:

C:\...> dotnet ef dbcontext info --context IdentityDbCtx
C:\...> dotnet ef dbcontext list

 

image

Espero que este artículo os haya parecido interesante y os aporte algún conocimiento nuevo y útil.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *