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.
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.
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.
1
2
3
4
5
FROM microsoft/dotnet:2.0-runtime
ARG source
WORKDIR /app
COPY ${source:-obj/Docker/publish} .
ENTRYPOINT ["dotnet", "EFCoreDockerMySQL.dll"]
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.
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.
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.
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
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.
1
2
3
4
5
6
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
}
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 cartellamysql-initdb
, il quale contiene le istruzioni SQL per creare la tabellaPeople
e popolarla con alcuni dati di test; - ho modificato il file
docker-compose.yml
per mappare la cartellamysql-initdb
del file system host con la cartella/docker-entrypoint-initdb.d
del container MySQL.
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');
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:
...
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.
1 |
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.
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
emysql-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:
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.
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();
}
}
'''
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.
1
2
3
4
5
version: '3'
services:
mysql:
ports:
- "3306:3306"
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;"
1 |
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, siaUp
cheDown
, 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 tabellaPeople
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);