In questo post spiegherò come creare un’applicazione console .NET Core 2.0 la quale legga e scriva i propri dati su MySQL e che utilizzi Entity Framework Core, e le migrazioni, per la persistenza e l’aggiornamento dello schema del DB. Inoltre mostrerò come utilizzare Docker in modo da poter sviluppare l’applicazione indipendentemente dall’ambiente utilizzato.

Al fine di evidenziare i passaggi necessari, ho suddiviso il post nel seguente modo:

  • creazione progetto console .NET Core 2.0 con supporto per Docker;
  • aggiunta container MySQL per gestire la persistenza;
  • creazione modello dati e delle configurazioni necessarie per persisterlo con Entity Framework Core;
  • inizializzazione della struttura del DB tramite script SQL;
  • implementazione di un’applicazione d’esempio per verificare il funzionamento del sistema;
  • utilizzo delle migrazioni per l’inizializzazione del DB al posto degli script SQL;

Creazione del progetto

Ho iniziato creando un nuovo progetto (File -> New -> Project…) di tipo Console App (.NET Core) con Visual Studio.

VS screenshot

Dopodiché ho abilitato il supporto per Docker cliccando con il tasto destro sul progetto, selezionando Add -> Docker Support e scegliendo Linux come sistema operativo di riferimento.

VS screenshot

Con questa operazione VS aggiunge i file Dockerfile e .dockerignore al progetto dell’applicazione e un progetto docker-compose alla soluzione.

Il file Dockefile descrive come dovrà essere creato il container dell’applicazione.

Dockerfile
1
2
3
4
5
FROM microsoft/dotnet:2.0-runtime
ARG source
WORKDIR /app
COPY ${source:-obj/Docker/publish} .
ENTRYPOINT ["dotnet", "EFCoreDockerMySQL.dll"]
Il parametro source utlizzato dal comando COPY, che verrà attualizzato in fase di costruzione del container, indica qual’è la directory del file system host da cui copiare i file che compongono l’applicazione. Questa variabile consente di specificare un comportamento diverso nel caso in cui l’applicazione venga eseguita in debug o release.

Il file .dockerignore indica a Docker quale porzione del file system host, a partire dalla cartella dove si trova il file Dockerfile, deve essere ignorato, o considerato (righe che iniziano con '!'), nel processo di build.

.dockerignore
1
2
3
*
!obj/Docker/publish/*
!obj/Docker/empty/

Il progetto docker-compose, che viene automaticamente preimpostato come StartUp Project, invece contiene il file docker-compose.yml il quale descrive i container che devono essere avviati quando si vuole eseguire l’applicazione.

docker-compose.yml
1
2
3
4
5
6
7
version: '3'
services:
  efcoredockermysql:
    image: efcoredockermysql
    build:
      context: ./EFCoreDockerMySQL
      dockerfile: Dockerfile
docker-compose.yml fa riferimento al file Dockerfile contenuto nel progetto console.

Compilando l’applicazione, e copiando l’output della compilazione in obj/Docker/publish/, è possibile, grazie ai file descritti sopra, costruire il container ed avviarlo direttamente da riga di comando digitando docker-compose up (come mostrato sotto).

PS C:\EFCoreDockerMySQL> dotnet publish .\EFCoreDockerMySQL\EFCoreDockerMySQL.csproj
Microsoft (R) Build Engine versione 15.3.409.57025 per .NET Core
Copyright (C) Microsoft Corporation. Tutti i diritti sono riservati.

  EFCoreDockerMySQL -> C:\EFCoreDockerMySQL\EFCoreDockerMySQL\bin\Debug\netcoreapp2.0\EFCoreDockerMySQL.dll
  EFCoreDockerMySQL -> C:\EFCoreDockerMySQL\EFCoreDockerMySQL\bin\Debug\netcoreapp2.0\publish\
PS C:\EFCoreDockerMySQL> Copy-Item .\EFCoreDockerMySQL\bin\Debug\netcoreapp2.0\publish\ .\EFCoreDockerMySQL\obj\Docker\publish\ -Force -Recurse
PS C:\EFCoreDockerMySQL> docker-compose.exe up
Building efcoredockermysql
Step 1/5 : FROM microsoft/dotnet:2.0-runtime
 ---> e3b41abbf6c2
Step 2/5 : ARG source
 ---> Using cache
 ---> b9164fbe106e
Step 3/5 : WORKDIR /app
 ---> Using cache
 ---> dc8b3389277a
Step 4/5 : COPY ${source:-obj/Docker/publish} .
 ---> 4ff8a44571cf
Removing intermediate container 0a696ed5881b
Step 5/5 : ENTRYPOINT dotnet EFCoreDockerMySQL.dll
 ---> Running in e75f74109286
 ---> 637957331a79
Removing intermediate container e75f74109286
Successfully built 637957331a79
Successfully tagged efcoredockermysql:latest
WARNING: Image for service efcoredockermysql was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating efcoredockermysql_efcoredockermysql_1 ...
Creating efcoredockermysql_efcoredockermysql_1 ... done
Attaching to efcoredockermysql_efcoredockermysql_1
efcoredockermysql_1  | Hello World!
efcoredockermysql_efcoredockermysql_1 exited with code 0

Ovviamente è anche possibile compilare e avviare l’applicazione direttamente da VS dimenticandosi della complessità di quanto appena descritto. Per farlo è sufficiente premere F5 o selezionare Debug -> Start Debugging. In questo caso però, oltre ai file che ho descritto sopra, VS utilizza alcuni file generati automaticamente che consentono di debuggare l’applicazione direttamente dall’ambiente di sviluppo. Per vedere dove si trovano tali file è sufficiente guardare la finestra di Output del Build e analizzare le invocazioni di docker-compose.

Aggiunta container MySQL

Dopodiché ho aggiunto un contenitore MySQL mettendolo nella stessa rete virtuale dell’applicazione.

docker-compose.yml
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
version: '3'
services:
  mysql:
    image: mysql:5.7.18
    container_name: efcoredockermysql-mysql
    environment:
      MYSQL_ROOT_PASSWORD: "p4ssw0r#"
      MYSQL_DATABASE: "efcoredockermysql"
    volumes:
      - ./mysql-data:/var/lib/mysql
    restart: always
    networks:
      - efcoredockermysql-net
  efcoredockermysql:
    image: efcoredockermysql
    build:
      context: ./EFCoreDockerMySQL
      dockerfile: Dockerfile
    depends_on:
      - mysql
    networks:
      - efcoredockermysql-net
volumes:
  mysql-data:
networks:
  efcoredockermysql-net:
    driver: bridge
La dichiarazione della rete e l’assegnazione dei due container alla stessa è essenziale per consentire la comunicazione fra i due. Senza questa operazione i due container verrebbero avviati ma, ad esempio, l’applicazione non riuscirebbe a connettersi al DB.

Creazione modello dati e aggiunta EF Core

Ho poi aggiunto Entity Framework Core al progetto utilizzando il seguente comando dalla Package Manager Console di VS:

Install-Package Microsoft.EntityFrameworkCore -Version 2.0.0

Ho quindi sviluppato un semplice modello dati da persistere e ho creato il DbContext di EF Core.

Person.cs
1
2
3
4
5
6
public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }
}
ApplicationDbContext.cs
1
2
3
4
5
6
7
8
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : 
        base(options)
    {
    }
    public DbSet<Person> People { get; set; }
}

Inizializzazione DB tramite script SQL

Prima di poter utilizzare il modello dati nell’applicazione è necessario creare la tabella People nel DB. Il database efcoredockermysql viene creato automaticamente dal container all’avvio ma, come si può verificare ispezionando la cartella mysql-data\efcoredockermysql, non viene creata alcuna tabella.

Anche se l’obiettivo finale del post è di utilizzare le migrazioni, come primo passo ho deciso di utilizzare uno script di inizializzazione in modo da creare la tabella all’avvio di MySQL. Così facendo ho la possibilità di verificare che il sistema funzioni correttamente prima di introdurre un’altra variabile all’equazione. Per fare questo ho:

  • creato il file init.sql nella cartella mysql-initdb, il quale contiene le istruzioni SQL per creare la tabella People e popolarla con alcuni dati di test;
  • ho modificato il file docker-compose.yml per mappare la cartella mysql-initdb del file system host con la cartella /docker-entrypoint-initdb.d del container MySQL.

init.sql
1
2
3
4
5
6
7
8
9
USE efcoredockermysql;
CREATE TABLE People (
    Id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    Name VARCHAR(255) NULL,
    Surname VARCHAR(255) NULL
);
INSERT INTO People (Name, Surname) VALUES ('Alice', 'Cooper');
INSERT INTO People (Name, Surname) VALUES ('Bob', 'Marley');
INSERT INTO People (Name, Surname) VALUES ('Charles', 'Xavier');
Estratto di docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
...
  mysql:
    image: mysql:5.7.18
    ...
    volumes:
      - ./mysql-data:/var/lib/mysql
      - ./mysql-initdb:/docker-entrypoint-initdb.d
...
volumes:
  mysql-data:
  mysql-initdb:
...
Per verificare l’effetto delle modifiche è sufficiente avviare docker, collegarsi al container MySQL con la bash, ed utilizzare il client mysql per controllare il contenuto del DB.

Applicazione di esempio

Avendo predisposto tutto il necessario, container, DB e modello dati, ho realizzato un programma per leggere e modificare i dati contenuti nel database. Per poter utilizzare EF Core 2.0 con MySQL ho installato il pacchetto NuGet Pomelo.EntityFrameworkCore.MySql con il seguente comando dalla Package Manager Console:

Install-Package Pomelo.EntityFrameworkCore.MySql -Version 2.0.0

Ho utilizzato la libreria Pomelo perché, al momento della scrittura del post, il pacchetto ufficiale MySql.Data.EntityFrameworkCore non è compatibile con EF Core 2.0.

L’applicazione, riportata qui sotto, fa due operazioni:

  • inserisce/aggiorna un record della tabella People;
  • mostra tutti i record presenti nella tabella stessa.
Program.cs
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
class Program
{
    static void Main(string[] args)
    {
        var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
        builder.UseMySql("server=efcoredockermysql-mysql;userid=root;pwd=p4ssw0r#;" + 
          "port=3306;database=efcoredockermysql;sslmode=none;");
        AddOrUpdateDavid(builder.Options);
        ShowAllPeople(builder.Options);
    }
    private static void AddOrUpdateDavid(
        DbContextOptions<ApplicationDbContext> options)
    {
        using (var context = new ApplicationDbContext(options))
        {
            var david = context.People
              .FirstOrDefaultAsync(x => x.Name == "David").Result;
            if (david != null)
            {
                david.Surname += "*";
            }
            else
            {
                david = new Person { Name = "David", Surname = "Gilmour" };
                context.People.Add(david);
            }
            context.SaveChanges();
        }
    }
    private static void ShowAllPeople(DbContextOptions<ApplicationDbContext> options)
    {
        using (var context = new ApplicationDbContext(options))
        {
            foreach (var person in context.People)
            {
                Console.WriteLine($"{person.Name} {person.Surname}");
            }
        }
    }
}

Da notare la stringa di connessione: il nome del container viene utilizzato come nome del server MySQL. Infatti Docker, quando si utilizza una rete di tipo bridge, mette a disposizione dei container che stanno sulla rete stessa un servizio di DNS che risolvere i nomi dati ai container tramite l’attributo container_name. La stringa di connessione viene passata al metodo di estensione UseMySql il quale è fornito dalla libreria Pomelo.

Eseguendo l’applicazione da VS, nella finestra di Output si vedrà quanto mostrato nell’immagine qui sotto.

Output screenshot

Nota sulla prima esecuzione: nonostante venga utilizzato l’attributo depends_on per indicare che il container applicativo deve essere avviato dopo quello di MySQL, può succedere che l’applicazione cerchi di aprire la connessione con il database prima che questo sia effettivamente disponibile. Questo è dovuto al fatto che Docker garantisce che i container vengano avviati nel giusto ordine, ma non può garantire che i servizi che ci girano dentro siano effettivamente pronti a rispondere alle richieste. Esistono vari metodi per avere questo tipo di garanzia ma esulano dallo scopo di questo post. Nel caso in cui alla prima esecuzione l’applicazione abbia dei problemi di connessione, è sufficiente riavviarla per far sì che tutto funzioni correttamente.

Utilizzo delle migrazioni

Dopo aver verificato che l’applicazione e MySQL interagiscono correttamente, arriviamo finalmente al cuore del post: l’uso delle migrazioni. Come succede a volte nell’informatica, per andare avanti dobbiamo prima fare un passo indietro. Ho quindi annullato le modifiche fatte nel paragrafo Inizializzazione DB tramite script SQL in modo da ripartire con un database pulito, ed in particolare:

  • ho rimosso i container precedentemente creati utilizzando il comando docker rm -f <id container> <id container>;
  • ho cancellato le cartelle mysql-data e mysql-initdb;
  • e ho rimosso il mapping di quest’ultima cartella dal file docker-compose.yml.

A dimostrazione del fatto che il DB non è più inizializzato, avviando l’applicazione vedrò il seguente errore:

MySqlException:Table 'efcoredockermysql.People' doesn't exist

Per poter utilizzare la .NET Core CLI per EF Core per prima cosa ho installato il pacchetto Microsoft.EntityFrameworkCore.Tools.DotNet. Per farlo, come descritto in questo articolo, ho modificato manualmente il file EFCoreDockerMySQL.csproj aggiungendo le seguenti righe:

Estratto di EFCoreDockerMySQL.csproj
1
2
3
4
<ItemGroup>
  <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet"
    Version="2.0.0" />
</ItemGroup>

Dopodiché ho creato la classe ApplicationDbContextFactory, la quale implementa l’interfaccia IDesignTimeDbContextFactory<ApplicationDbContext> per poter instanziare correttamente il DbContext dell’applicazione al momento dell’invocazione della CLI.

Per evitare di duplicare la stringa di connessione del DB all’interno del codice dell’applicazione, ho deciso di utilizzare il meccanismo standard di configurazione JSON di .NET Core, pacchetto NuGet Microsoft.Extensions.Configuration.Json. Ho quindi modificato il codice del main come segue e ho aggiunto un file di configurazione.

Estratto di Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
static void Main(string[] args)
{
    var builder = new DbContextOptionsBuilder<ApplicationDbContext>();

    builder.UseMySql(Configuration.GetConnectionString("DefaultConnection"));
    AddOrUpdateDavid(builder.Options);
    ShowAllPeople(builder.Options);
}

public static IConfigurationRoot Configuration
{
  get
  {
    var environmentName = Environment.GetEnvironmentVariable(
      "DOTNETCORE_ENVIRONMENT");
    var builder = new ConfigurationBuilder()
      .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
      .AddJsonFile($"appsettings.{environmentName}.json", optional: true);
    return builder.Build();
  }
}
'''
appsettings.json
1
2
3
4
5
{
  "ConnectionStrings": {
    "DefaultConnection": "server=efcoredockermysql-mysql;userid=root;pwd=p4ssw0r#;port=3306;database=efcoredockermysql;sslmode=none;"
  }
}

Infine, prima di poter generare la prima migrazione, ho fatto in modo che il DB MySQL fosse raggiungibile dalla macchina host, ovvero quella di sviluppo. Per fare questo è stato sufficiente modificare il file docker-compose.override.yml come segue e riavviare il container MySQL.

docker-compose.override.yml
1
2
3
4
5
version: '3'
services:
  mysql:
    ports:
      - "3306:3306"
Modificando questo file e non docker-compose.yml, mi sono assicurato che queste modifiche non finiscano per sbaglio nella configurazione di release.

Dato che, dopo la modifica precedente, MySQL è raggiungibile all’indirizzo localhost:3306, ho aggiunto la seguente stringa di connessione al file appsettings.json e ho modificato la classe ApplicationDbContextFactory perché la utilizzase:

"MigrationConnection": "server=localhost;userid=root;pwd=p4ssw0r#;port=3306;database=efcoredockermysql;sslmode=none;"
ApplicationDbContextFactory.cs
1
2
3
4
5
6
7
8
9
10
public class ApplicationDbContextFactory : 
  IDesignTimeDbContextFactory<ApplicationDbContext>
{
  public ApplicationDbContext CreateDbContext(string[] args)
  {
    return new ApplicationDbContext(new DbContextOptionsBuilder<ApplicationDbContext>()
      .UseMySql(Program.Configuration.GetConnectionString("MigrationConnection"))
      .Options);
  }
}

Una volta finiti i preparativi descritti sopra, ho aperto la Package Manager Console ed eseguito i seguenti comandi per creare la prima migrazione del progetto:

PM> cd .\EFCoreDockerMySQL
PM> dotnet ef migrations add InitialCreate
Done. To undo this action, use 'ef migrations remove'

Così facendo la CLI di EF Core mi ha creato i seguenti file:

  • 20171003202500_InitialCreate.cs, il quale contiene la vera e propria migrazione, sia Up che Down, che deve essere applicata al DB per allineare quest’ultimo alla struttura del modello dati;
  • 20171003202500_InitialCreate.Designer.cs, il quale descrive la struttura attesa della tabella People dopo l’applicazione della migrazione;
  • ApplicationDbContextModelSnapshot.cs, il quale descrive la struttura attesa del DB dopo l’applicazione della migrazione.

Gli ultimi due file di fatto hanno la funzione che in Entity Framework svolgevano i file di risorsa associati alle migrazioni.

Una volta creata la migrazione, l’ho applicata al DB utilizzando il seguente comando da Package Manager Console:

PM> dotnet ef database update
Applying migration '20171003202500_InitialCreate'.
Done.

Per verificare il tutto è stato sufficiente eseguire l’applicazione e controllare la finestra di Output.

Così facendo è possibile modificare il modello dati e tenere lo schema del DB sincronizzato semplicemente utilizzato la CLI di EF Core ed in particolare i comandi dotnet ef migrations add e dotnet ef database update. Tali comandi sono del tutto equivalenti, anche se con alcune opzioni in meno, ai comandi Add-Migration e Update-Database che si utilizzano normalmente con Entity Framework.

Conclusioni

L’utilizzo di .NET Core consente di sviluppare applicazioni leggere e multipiattaforma. Riuscire ad utilizzarlo correttamente insieme a Docker e MySQL consente di sfruttare a pieno le potenzialità di questa piattaforma.

Ho sempre torvato molto utile l’approccio code first per la gestione del modello dati e dello schema del DB. Con questo post, ho dimostrato come impostare un progetto, e l’ambiente di sviluppo, in modo da utilizzare le migrazioni di EF Core direttamente con MySQL. Così facendo, è possibile usare in sviluppo la stessa versione del database che sarà utilizzata in produzione.

Inoltre con questo approccio, grazie a Docker, è possibile utilizzare versioni distinte di framework e database per ogni progetto, senza nemmeno avere la necessità di installare tali versioni sul proprio computer.

Per quanto riguarda EF Core c’è ancora molto da esplorare, ad esempio il seed del database o l’applicazione delle migrazioni in produzione, etc. Tutti ottimi argomenti da approfondire in articoli futuri.

Il codice sorgente relativo a questo post è disponibile su GitHub. La configurazione dell’ambiente di sviluppo utilizzata è stata la seguente:

  • Windows 10 Pro;
  • Visual Studio Professional 2017, version 15.3.4;
  • Docker Community Edition for Windows, version 17.09.0-ce-win33 (13620);