En este artículo desarrollamos una aplicación de consola sencilla, usando Code First con EF Core 1.1, con el fin explorar algunos aspectos básicos del trabajo con EF Core.

Puntos Importantes
  1. Paquetes mínimos necesarios para trabajar con EF Core.

  2. Clases de configuración con Fluent API.

  3. Leer string de conexión de archivos configuración.

  4. Usar de EF Core CLI para crear la migración inicial.

  5. Crear automáticamente la base de datos.

Programas fuente

Artículo: EFCoreApp-1.0.zip (release 1.0 del repositorio)
Repositorio: https://github.com/mvelosop/EFCoreApp

Contexto

Para seguir este artículo es necesario tener instalado lo siguiente:

Herramientas

.NET Core 1.1

Paso a paso

1 - Crear la solución EFCoreApp

  1. Crear una solución “blank” llamada EFCoreApp
  2. Crear el solution folder “src”
  3. Crear la carpeta “src” dentro de la carpeta de la solución

2 - Crear proyecto src/EFCore.App

  1. Crear el proyecto como una “Console Application (.NET Core)”
  2. Actualizar project.json a lo siguiente:

    project.json
    [EFCoreApp-1.0] src\EFCore.App\
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    {
        "buildOptions": {
            "copyToOutput": {
                "include": [ "appsettings.json" ]
            },
            "emitEntryPoint": true
        },
        "dependencies": {
            "Microsoft.EntityFrameworkCore": "1.1.0",
            "Microsoft.EntityFrameworkCore.Design": {
                "type": "build",
                "version": "1.1.0"
            },
            "Microsoft.EntityFrameworkCore.SqlServer": "1.1.0",
            "Microsoft.Extensions.Configuration": "1.1.0",
            "Microsoft.Extensions.Configuration.Binder": "1.1.0",
            "Microsoft.Extensions.Configuration.Json": "1.1.0",
            "Microsoft.NETCore.App": {
                "type": "platform",
                "version": "1.1.0"
            },
            "System.ComponentModel.Annotations": "4.3.0"
        },
        "frameworks": {
            "netcoreapp1.0": {
                "imports": "dnxcore50"
            }
        },
        "tools": {
            "Microsoft.EntityFrameworkCore.Tools.DotNet": "1.1.0-preview4-final"
        },
        "version": "1.0.0-*"
    }
  3. Salvar el archivo desde VS para actualizar todos los paquetes o, si prefiere usar la interfaz de comandos de desarrollo ([Shift]+[Alt]+[,]), ejecute dotnet restore

Detalles de project.json

Importante

Aquí vemos los paquetes mínimos necesarios para trabajar con EF Core

  • Paquetes de Entity Framework Core

    • Microsoft.EntityFrameworkCore: Paquete base
    • Microsoft.EntityFrameworkCore.Design: Componentes para EF Core CLI, sólo para desarrollo, por eso "type": "build"
    • Microsoft.EntityFrameworkCore.Tools.DotNet: EF Core CLI
    • Microsoft.EntityFrameworkCore.SqlServer: SQL Server provider
  • Paquetes para manejar archivos de configuración

    • Microsoft.Extensions.Configuration: Paquete base
    • Microsoft.Extensions.Configuration.Binder: Para manejar configuraciones “strongly typed”.
    • Microsoft.Extensions.Configuration.Json: Manejo de archivos json
  • Otros

    • System.ComponentModel.Annotations: Annotations para los modelos

3 - Agregar archivos del proyecto

Model\Currency.cs

La clase del modelo, Divisas en este caso.

Currency.cs
[EFCoreApp-1.0] src\EFCore.App\Model\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.ComponentModel.DataAnnotations;

namespace EFCore.App.Model
{
    public class Currency
    {
        public int Id { get; set; }

        [Required]
        [MaxLength(3)]
        public string IsoCode { get; set; }

        [Required]
        [MaxLength(100)] // Default string length
        public string Name { get; set; }

        public byte[] RowVersion { get; set; }

        [Required]
        [MaxLength(10)]
        public string Symbol { get; set; }
    }
}

Base\EntityTypeConfiguration.cs

Importante

Estas clases permiten manejar una clase de configuración por cada clase del modelo, para mantener el DbContext lo más sencillo posible, de forma similar a como se puede hacer con EF 6, según lo sugerido en https://github.com/aspnet/EntityFramework/issues/2805

EntityTypeConfiguration.cs
[EFCoreApp-1.0] src\EFCore.App\Base\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCore.App.Base
{
    // As suggested in: https://github.com/aspnet/EntityFramework/issues/2805

    public static class ModelBuilderExtensions
    {
        public static void AddConfiguration<TEntity>(this ModelBuilder modelBuilder, EntityTypeConfiguration<TEntity> configuration)
            where TEntity : class
        {
            configuration.Map(modelBuilder.Entity<TEntity>());
        }
    }

    public abstract class EntityTypeConfiguration<TEntity> 
        where TEntity : class
    {
        public abstract void Map(EntityTypeBuilder<TEntity> builder);
    }
}

Data\CurrencyConfiguration.cs

Importante

La clase de configuración del modelo permite mantener fuera del DbContext los detalles de cada clase. Esto simplifica mucho el DbContext.

CurrencyConfiguration.cs
[EFCoreApp-1.0] src\EFCore.App\Data\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using EFCore.App.Base;
using EFCore.App.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCore.App.Data
{
    public class CurrencyConfiguration : EntityTypeConfiguration<Currency>
    {
        public override void Map(EntityTypeBuilder<Currency> builder)
        {
            builder.ToTable("Currencies", schema: "Common");

            builder.HasKey(e => e.Id);

            builder.Property(e => e.RowVersion)
                .IsRowVersion();

            builder.HasIndex(e => e.IsoCode)
                .IsUnique();
        }
    }
}

Config\ConfigClasses.cs

Importante

Aquí vemos como podemos manejar las opciones de configuración a través de clases auxiliares, para hacerlo de forma “strongly typed”.

Clases de configuración de la aplicación, permiten manejar la configuraciones que se carguen del archivo appsettings.json de una forma “strongly typed”.

ConfigClasses.cs
[EFCoreApp-1.0] src\EFCore.App\Config\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
namespace EFCore.App.Config
{
    public class AppOptions
    {
        public ConnectionStrings ConnectionStrings { get; set; }
    }

    public class ConnectionStrings
    {
        public string DefaultConnection { get; set; }
    }

}

Data\CommonDbContext.cs

El DbContext para la aplicación, define la vista de la base de datos a la que tiene acceso la aplicación.

CommonDbContext.cs
[EFCoreApp-1.0] src\EFCore.App\Data\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using EFCore.App.Base;
using EFCore.App.Config;
using EFCore.App.Model;
using Microsoft.EntityFrameworkCore;

namespace EFCore.App.Data
{
    public class CommonDbContext : DbContext
    {
        // Must not be null or empty for initial create migration
        private string _connectionString = "ConnectionString";

        // Default constructor for initial create migration
        public CommonDbContext()
        {
        }

        // Normal use constructor
        public CommonDbContext(ConnectionStrings connectionStrings)
        {
            _connectionString = connectionStrings.DefaultConnection;
        }

        public DbSet<Currency> Currencies { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder options)
        {
            options.UseSqlServer(_connectionString);
        }

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

            modelBuilder.AddConfiguration(new CurrencyConfiguration());
        }
    }
}

Program.cs

Importante

El método InitDb crea la base de datos o aplica las migraciones necesarias al arrancar la aplicación, en vez de hacerlo con el primer request. Esto mejora la experiencia del usuario.

El programa principal de la aplicación. Aquí están los métodos que crean/actualizan la base de datos y realizar la carga de datos iniciales.

Program.cs
[EFCoreApp-1.0] src\EFCore.App\
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
using EFCore.App.Config;
using EFCore.App.Data;
using EFCore.App.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Linq;
using System.Text;

namespace EFCore.App
{
    public class Program
    {
        private static Currency[] _currencyData = new[]
        {
            new Currency { IsoCode = "USD", Name = "US Dolar", Symbol = "US$" },
            new Currency { IsoCode = "EUR", Name = "Euro", Symbol = "€" },
            new Currency { IsoCode = "CHF", Name = "Swiss Franc", Symbol = "Fr." },
        };

        public static AppOptions AppOptions { get; set; }

        public static IConfigurationRoot Configuration { get; set; }

        public static void Main(string[] args)
        {
            Console.OutputEncoding = Encoding.UTF8;

            Console.WriteLine("EF Core App\n");

            ReadConfiguration();

            InitDb();

            PrintDb();
        }

        private static void InitDb()
        {
            using (var db = new CommonDbContext(AppOptions.ConnectionStrings))
            {
                Console.WriteLine("Creating database...\n");

                db.Database.EnsureCreated();

                Console.WriteLine("Seeding database...\n");

                LoadInitalData(db);
            }
        }

        private static void LoadInitalData(CommonDbContext db)
        {
            foreach (var item in _currencyData)
            {
                Currency currency = db.Currencies.FirstOrDefault(c => c.Symbol == item.Symbol);

                if (currency == null)
                {
                    db.Currencies.Add(item);
                }
                else
                {
                    currency.Name = item.Name;
                    currency.Symbol = item.Symbol;
                }
            }

            db.SaveChanges();
        }

        private static void PrintDb()
        {
            using (var db = new CommonDbContext(AppOptions.ConnectionStrings))
            {
                Console.WriteLine("Reading database...\n");

                Console.WriteLine("Currencies");
                Console.WriteLine("----------");

                int symbolLength = _currencyData.Select(c => c.Symbol.Length).Max();
                int nameLength = _currencyData.Select(c => c.Name.Length).Max();

                foreach (var item in db.Currencies)
                {
                    Console.WriteLine($"| {item.IsoCode} | {item.Symbol.PadRight(symbolLength)} | {item.Name.PadRight(nameLength)} |");
                }

                Console.WriteLine();
            }
        }

        private static void ReadConfiguration()
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json");

            Configuration = builder.Build();

            // Reads appsettings.json into a (strongly typed) class
            AppOptions = Configuration.Get<AppOptions>();

            Console.WriteLine("Configuration\n");
            Console.WriteLine($@"connectionString (defaultConnection) = ""{AppOptions.ConnectionStrings.DefaultConnection}""");
            Console.WriteLine();
        }
    }
}

4 - Generar la migración inicial

Importante

Aquí vemos la forma general de trabajar con el EF Core CLI.

Siempre podemos usar dotnet ef <comando> -h para consultar la ayuda del comando.

Ahora es necesario generar la migración inicial que utilizará EF para crear la base de datos cuando se ejecute la aplicación por primera vez.

  1. Abrir la interfaz de comandos de desarrollo

    • Hacer click sobre el nodo del proyecto EFCore.App en el explorador de la solución.
    • Pulsar [Shift]+[Alt]+[,] o Botón derecho > Open Command Line > Developer Command Prompt
    • Ejecutar dotnet ef
    • Si todo marchó bien, debe observar una pantalla similar a la siguiente:
    Crear una aplicación con Entity Framework Core /posts/images/cmd_2017-02-27_23-41-30.png
  2. Crear la migración inicial

    • Ejecutar dotnet ef migrations add InitialCreateMigration en la interfaz de comandos.
    • Se puede utilizar cualquier nombre para la clase de la migración, pero recomiendo utilizar el sufijo “Migration” para evitar conflictos de nombres con otras clases de la aplicación.
    • También podemos utilizar dotnet ef [comando] --help para consultar la ayuda de cualquier comando de la interfaz.
  3. Verificar que se hayan creado los archivos de la migración inicial, en la carpeta Migrations, similar a los siguientes:

Migrations\CommonDbContextModelSnapshot.cs

Este archivo es la configuración de la última versión del modelo, se utiliza al ejecutar el método DbContext.Database.EnsureCreated().

Observe que en esta clase está consolidada toda la definición de los objetos de base de datos, usando Fluent API, incluyendo los atributos utilizados en las propiedades del modelo de dominio.

CommonDbContextModelSnapshot.cs
[EFCoreApp-1.0] src\EFCore.App\Migrations\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using EFCore.App.Data;

namespace EFCore.App.Migrations
{
    [DbContext(typeof(CommonDbContext))]
    partial class CommonDbContextModelSnapshot : ModelSnapshot
    {
        protected override void BuildModel(ModelBuilder modelBuilder)
        {
            modelBuilder
                .HasAnnotation("ProductVersion", "1.1.0-rtm-22752")
                .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

            modelBuilder.Entity("EFCore.App.Model.Currency", b =>
                {
                    b.Property<int>("Id")
                        .ValueGeneratedOnAdd();

                    b.Property<string>("IsoCode")
                        .IsRequired()
                        .HasMaxLength(3);

                    b.Property<string>("Name")
                        .IsRequired()
                        .HasMaxLength(100);

                    b.Property<byte[]>("RowVersion")
                        .IsConcurrencyToken()
                        .ValueGeneratedOnAddOrUpdate();

                    b.Property<string>("Symbol")
                        .IsRequired()
                        .HasMaxLength(10);

                    b.HasKey("Id");

                    b.HasIndex("IsoCode")
                        .IsUnique();

                    b.ToTable("Currencies","Common");
                });
        }
    }
}

Migrations\20170227231210_InitialCreateMigration

Este archivo es el encargado de generar la migración desde la versión anterior de CommonDbContextModelSnapshot.cs, se utiliza al ejecutar el método DbContext.Database.Migrate().

Los números iniciales del nombre indican el año-mes-día-hora-minuto-segundo (yyyyMMddHHmmss) de generación de la migración.

20170227231210_InitialCreateMigration.cs
[EFCoreApp-1.0] src\EFCore.App\Migrations\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Metadata;

namespace EFCore.App.Migrations
{
    public partial class InitialCreateMigration : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.EnsureSchema(
                name: "Common");

            migrationBuilder.CreateTable(
                name: "Currencies",
                schema: "Common",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                    IsoCode = table.Column<string>(maxLength: 3, nullable: false),
                    Name = table.Column<string>(maxLength: 100, nullable: false),
                    RowVersion = table.Column<byte[]>(rowVersion: true, nullable: true),
                    Symbol = table.Column<string>(maxLength: 10, nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Currencies", x => x.Id);
                });

            migrationBuilder.CreateIndex(
                name: "IX_Currencies_IsoCode",
                schema: "Common",
                table: "Currencies",
                column: "IsoCode",
                unique: true);
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Currencies",
                schema: "Common");
        }
    }
}

5 - Crear archivo de configuración

appsettings.json

Este string de conexión es adecuado para SQL Server Developer Edition con la instancia por default (MSSQLSERVER), puede ser necesario ajustarlo si la situación es distinta.

appsettings.json
[EFCoreApp-1.0] src\EFCore.App\
1
2
3
4
5
{
    "connectionStrings": {
        "defaultConnection": "Data Source=localhost;Initial Catalog=EFCore.App;Integrated Security=SSPI;"
    }
}

Verificar que project.json incluya la opción para copiar este archivo a la carpeta de salida:

    "buildOptions": {
        "copyToOutput": {
            "include": [ "appsettings.json" ]
        },
        "emitEntryPoint": true
    },

6 - Ejecutar la aplicación

Suponiendo que ya se instaló el SQL Server 2016 Developer Edition, al ejecutar la aplicación con [Ctrl]+[F5] se debe obtener una salida similar a esta:

Crear una aplicación con Entity Framework Core /posts/images/cmd_2017-02-28_00-47-31.png

Y si en algún momento necesitamos empezar con una base de datos nueva, basta con eliminarla usando el SQL Server Management Studio y ejecutar la aplicación de nuevo.


Espero que este artículo le haya resultado útil y le invito a darme su opinión en la sección de comentarios.

Gracias,

Miguel.

Enlaces relacionados

.NET Core current downloads
https://www.microsoft.com/net/download/core#/current

.NET Core EF CLI
https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dotnet