En este artículo vamos a desarrollar una versión de la misma aplicación de consola desarrollada anteriormente (migrada a VS 2017) pero separándola en dos capas:

  1. La capa de datos en el proyecto EFCore.Lib y
  2. La capa cliente en EFCore.App.
Puntos Importantes
  1. Separar la aplicación en una capa cliente y una capa de datos

  2. Usar EF Core CLI en aplicación multicapa para crear las migraciones

  3. Apreciar la simplicidad de los nuevos archivos .csproj

Programas fuente

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

Contexto

Paso a paso

Como en este artículo estamos separando los componentes de la aplicación, vamos a crear los archivos de programas y luego incluiremos los paquetes necesarios para poder compilar y ejecutar la solución.

En este artículo vamos a desarrollar la solución desde el principio, para apreciar los archivos .proj en su expresión más simple. No podríamos apreciar esto si hacemos la migración de un proyecto de VS 2015.

1 - Crear la solución EFCoreLib

  1. Crear una solución “blank” llamada EFCoreLib
  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

Importante

Este proyecto es la capa Cliente que usa la “capa de datos” (el proyecto EFCore.Lib)

Crear el proyecto como una “Console Application (.NET Core)”

3 - Crear proyecto src\EFCore.Lib

Importante

Este proyecto es la capa de Datos, desde luego de una forma muy rudimentaria.

Crear el proyecto como una “Class Library (.NET Core)”

4 - Crear los archivos de programa en EFCore.Lib

Vamos a crear en esencia los mismos archivos que usamos en el artículo Crear Aplicación EF Core.

4.1 - Model\Currency.cs

src\EFCore.Lib\Model\Currency.cs
[EFCoreLib-1.0]
 1 2 3 4 5 6 7 8 91011121314151617181920212223
using System.ComponentModel.DataAnnotations;

namespace EFCore.Lib.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; }
    }
}

4.2 - Base\EntityTypeConfiguration.cs

src\EFCore.Lib\Base\EntityTypeConfiguration.cs
[EFCoreLib-1.0]
 1 2 3 4 5 6 7 8 910111213141516171819202122
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCore.Lib.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);
    }
}

4.3 - Data\CurrencyConfiguration.cs

src\EFCore.Lib\Data\CurrencyConfiguration.cs
[EFCoreLib-1.0]
 1 2 3 4 5 6 7 8 91011121314151617181920212223
using EFCore.Lib.Base;
using EFCore.Lib.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCore.Lib.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();
        }
    }
}

4.4 - Config\ConnectionStrings.cs

A diferencia de lo que hicimos en el artículo Crear Aplicación EF Core, en el proyecto EFCore.Lib sólo incluimos la clase de configuración ConnectionStrings.cs porque sólo esta tiene que ver con la “capa de datos”.

src\EFCore.Lib\Config\ConnectionStrings.cs
[EFCoreLib-1.0]
1234567
namespace EFCore.Lib.Config
{
    public class ConnectionStrings
    {
        public string DefaultConnection { get; set; }
    }
}

4.5 - Data\CommonDbContext.cs

src\EFCore.Lib\Data\CommonDbContext.cs
[EFCoreLib-1.0]
 1 2 3 4 5 6 7 8 91011121314151617181920212223242526272829303132333435363738
using EFCore.Lib.Base;
using EFCore.Lib.Config;
using EFCore.Lib.Model;
using Microsoft.EntityFrameworkCore;

namespace EFCore.Lib.Data
{
    public class CommonDbContext : DbContext
    {
        // Must not be null or empty for running 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());
        }
    }
}

Para que el proyecto pueda compilar en este momento es necesario incluir los siguientes paquetes:

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Relational
  • Microsoft.EntityFrameworkCore.SqlServer

El archivo EFCore.Lib.csproj resultante es así:

src\EFCore.Lib\EFCore.Lib.csproj
[EFCoreLib-1.0]
 1 2 3 4 5 6 7 8 910111213
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp1.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="1.1.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="1.1.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="1.1.1" />
  </ItemGroup>

</Project>

5 - Crear los archivos de programa en EFCore.App

5.1 - Config\AppOptions.cs

En este caso incluimos la clase que maneja todas las configuraciones de la “aplicación” haciendo referencia a la clase de configuración de la capa de datos.

src\EFCore.App\Config\AppOptions.cs
[EFCoreLib-1.0]
123456789
using EFCore.Lib.Config;

namespace EFCore.App.Config
{
    public class AppOptions
    {
        public ConnectionStrings ConnectionStrings { get; set; }
    }
}

5.2 - Program.cs

src\EFCore.App\Program.cs
[EFCoreLib-1.0]
  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 99100101102103104105106107108109
using EFCore.App.Config;
using EFCore.Lib.Data;
using EFCore.Lib.Model;
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 Lib\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();
        }
    }
}

5.3 - Incluir referencias y paquetes adicionales

Para que el proyecto pueda compilar en este momento es necesario incluir las siguientes referencias:

  1. Referencia al proyecto EFCore.Lib de la solución y

  2. Los siguientes paquetes:

    • Microsoft.Extensions.Configuration
    • Microsoft.Extensions.Configuration.Binder
    • Microsoft.Extensions.Configuration.Json

6 - Crear migración inicial

Importante

En este caso, el proyecto EFCore.App, además de ser la “capa cliente” de la solución, va a ser el “host” para ejecutar los comandos de la interfaz de comandos .NET EF Core (.NET Core EF CLI).

Tal como se indica en la página de la interfaz de comandos .NET EF Core (.NET Core EF CLI) hay una limitación de .NET Standard que no permite ejecutar dotnet en un proyecto “Class Library”, así que hay que instalar los componentes necesarios en EFCore.App para ejecutarlo desde allí.

Si en este momento ejecutamos dotnet ef desde el proyecto EFCore.App

Crear una librería con Entity Framework Core /posts/images/cmd_2017-03-18_21-23-38.png

6.1 - Instalar “Tooling” de Entity Framework

Para esto hay que instalar el paquete Microsoft.EntityFrameworkCore.Tools en EFCore.App, pero este es un tipo de paquete “DotNetCliTool”, que no se puede instalar como un NuGet cualquiera.

Entonces, siguiendo lo indicado en la página de la interfaz de comandos .NET EF Core (.NET Core EF CLI), hay que editar el archivo .csproj del proyecto (Solution Explorer, sobre EFCore.App: Botón derecho > Edit EFCore.App.csproj) y agregar las líneas siguientes:

  <ItemGroup>
    <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" />
  </ItemGroup>

Al salvar el archivo se debe instalar el paquete automáticamente. En caso contrario utilice el comando dotnet restore desde la interfaz de comandos en el proyecto EFCore.App.

También hay que instalar el paquete: Microsoft.EntityFrameworkCore.Design.

Sin embargo, como sólo vamos a utilizar EFCore.App para ejecutar la .NET Core EF CLI, no es necesario agregar el atributo PrivateAssets=“All” manualmente en el archivo .csproj y, por lo tanto, podemos instalarlo como cualquier paquete NuGet.

6.2 - Crear la migración inicial

Después de esto ya podemos ejecutar el comando para crear la migración: dotnet ef migrations add InitialCreateMigration --project ..\EFCore.Lib.

Observe que usamos la opción --project o -p para indicar donde se va a crear la migración.

Después de ejecutar el comando obtenemos los archivos de la migración con el mismo contenido de la aplicación inicial:

src\EFCore.Lib\Migrations\20170318215905_InitialCreateMigration.cs
[EFCoreLib-1.0]
 1 2 3 4 5 6 7 8 91011121314151617181920212223242526272829303132333435363738394041424344454647
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Metadata;

namespace EFCore.Lib.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");
        }
    }
}

7 - Preparar la aplicación para ejecución

Para esto debemos:

7.1 - Crear el archivo de configuración

src\EFCore.App\appsettings.json
[EFCoreLib-1.0]
12345
{
  "connectionStrings": {
    "defaultConnection": "Data Source=localhost;Initial Catalog=EFCoreLib;Integrated Security=SSPI;"
  }
}

7.2 - Cambiar propiedades del archivo de configuración

Configurar el archivo de “appasettings.json” para que se copie a la carpeta de salida

  • Botón Derecho > Properties sobre el archivo appsettings.json
  • Propiedad “Copy to Output Directory” => “Copy if newer”

7.3 - Establecer el proyecto EFCore.App como la aplicación de arranque

Ejecutar Botón Derecho > Set as Startup Project sobre el proyecto EFCore.App

El archivo EFCore.App.csproj resultante es así:

Importante

En el formato .csproj no es necesario especificar todos los archivos que conforman la solución, porque se considera, por omisión, todos los archivos de todas las carpetas.

Una ventaja importante de este formato es que elimina los frecuentes conflictos al hacer “Merge” de dos ramas, producidos por movimiento de archivos dentro del .csproj.

src\EFCore.App\EFCore.App.csproj
[EFCoreLib-1.0]
 1 2 3 4 5 6 7 8 91011121314151617181920212223242526272829
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp1.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="1.1.1" />
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="1.1.1" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="1.1.1" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="1.1.1" />
  </ItemGroup>

  <ItemGroup>
    <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\EFCore.Lib\EFCore.Lib.csproj" />
  </ItemGroup>

  <ItemGroup>
    <None Update="appsettings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>

</Project>

8 - Ejecutar la aplicación

Ahora basta con pulsar [Ctrl]+[F5], con lo que obtenemos el resultado esperado:

Crear una librería con Entity Framework Core /posts/images/cmd_2017-03-20_16-14-12.png


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.