Dans mon premier post sur EF Core, j’ai montré comment installer EF Core en ligne de commande et faire du Database First (générer du code à partir d’une base de données). Dans ce post, je vais montrer comment générer une base de données à partir du code, c’est le Code First. Le code source du projet d’exemple est sur GitHub.
Préparation
Je vais prendre un exemple simple de clients qui ont passés des commandes. Voilà à quoi la base doit ressembler :
Création du projet.
mkdir TutoEfCoreCodeFirst
cd TutoEfCoreCodeFirst
dotnet new sln # Ajout du fichier sln
mkdir WebApiCodeFirst # répertoire pour le projet
cd WebApiCodeFirst
dotnet new webapi --no-https --use-minimal-apis
cd ..
dotnet sln add .\WebApiCodeFirst\ # ajout du projet dans le sln
La commande nous crée notre projet Web API avec le minimum vital.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateTime.Now.AddDays(index),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
app.Run();
record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
J’enlève tout ce qui est inutile pour le post et il nous reste :
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.Run();
Pour que le code crée les tables dans une base de données, il faut une base. Pour l’exemple je vais prendre SQLite. Ajoutons le nécessaire au projet.
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
Install-Package Microsoft.EntityFrameworkCore.Sqlite
Création des models
Maintenant il faut créer nos modèles. Avant de les créer je vais ajouter un fichier GlobalUsings.cs
(au cas vous ne connaissez pas, un bon post pour expliquer global using)
J’ajouterai d’autres using.
global using System.ComponentModel.DataAnnotations;
global using System.ComponentModel.DataAnnotations.Schema;
Les modèles liés au « Client ».
namespace WebApiCodeFirst.Models
{
public class Client
{
[Key]
public string Id { get; set; }
[Required(ErrorMessage = "Un prénom est obligatoire")]
public string Prenom { get; set; }
[Required(ErrorMessage = "Un nom est obligatoire")]
public string Nom { get; set; }
[Required(ErrorMessage = "Un age est obligatoire")]
public int Age { get; set; }
public Adresse Adresse { get; set; }
public ICollection<Telephone> Telephones { get; set; }
public ICollection<Commande> Commandes { get; set; }
}
}
namespace WebApiCodeFirst.Models
{
public class Adresse
{
[Key]
public string Id { get; set; }
public string Numero { get; set; }
public string Rue { get; set; }
public string Ville { get; set; }
[ForeignKey("ClientId")]
public string ClientId { get; set; }
}
}
namespace WebApiCodeFirst.Models
{
public class Telephone
{
[Key]
public string Number { get; set; }
public string Type { get; set; }
[ForeignKey("ClientId")]
public string ClientId { get; set; }
}
}
Et les modèles liés à une « Commande ».
namespace WebApiCodeFirst.Models
{
public class Commande
{
[Key]
public string IdCmd { get; set; }
[ForeignKey("ClientId")]
public string ClientId { get; set; }
public ICollection<Panier> Panier { get; set; }
}
}
namespace WebApiCodeFirst.Models
{
public class Panier
{
[Key]
public Guid IdPanier { get; set; }
public int IdItem { get; set; }
public int Quantite { get; set; }
[ForeignKey("CommandeIdCmd")]
public string CommandeIdCmd { get; set; }
}
}
Comment EF Core gère les colonnes et fait les relations
Nous pouvons voir qu’il y a des attributs qui sont mis sur certaines propriétés :
Key
(doc sur MS) : Indique la clé primaire, « une entité de manière unique« .
ForeignKey
(doc sur MS) : Indique la clé étrangère.
Required
: Indique que ce sera une valeur NON NULL en base.
J’aurai pu ajouter aussi l’attribut :
[StringLength(50, ErrorMessage = "Le nom est trop long, 50 caractères max")]
pour spécifier à la base une longueur max de caractère pour cette colonne.
Pour avoir une liste des attributs possible sur entityframeworktutorial.net, entityframework.net et bien sûr la référence docs.microsoft.com.
Avec les attributs, nous pouvons indiquer comment les colonnes seront gérées (primary key, not null, …), mais comment indiquer les relations entre les tables. Prenons l’exemple entre l’entité Client et Telephone.
Telephone indique qu’il a une propriété de clé étrangère qui s’appelle ClientId
.
[ForeignKey("ClientId")]
public string ClientId { get; set; }
En mettant juste ça, il n’y aura pas de lien entre les tables, il faut aussi indiquer à l’entité Client
une propriété :
public ICollection<Telephone> Telephones { get; set; }</Telephone>
Attention : il y a une convention d’écriture entre les propriétés pour que EF puisse faire la bonne liaison entre les tables, sinon il peut soit prendre la mauvaise propriété, soit carrément créer une nouvelle colonne pour faire la FK. Le mieux est de mettre les mêmes noms de propriété dans chaque entité.
Nous avons tous les modèles, maintenant il faut indiquer à l’application comment construire la base de données.
Création du Context
Il n’y a pas besoin d’installer le package Microsoft.EntityFrameworkCore
, car Microsoft.EntityFrameworkCore.Sqlite
a comme dépendance :
Microsoft.EntityFrameworkCore.Sqlite.Core
qui lui a comme dépendance :
Microsoft.EntityFrameworkCore.Relational
qui dépend de :
Microsoft.EntityFrameworkCore
.
Ajouter un répertoire Data
et la class ApplicationDbContext
using Microsoft.EntityFrameworkCore;
namespace WebApiCodeFirst.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions options)
: base(options)
{
}
public DbSet<Telephone> Telephones { get; set; }
public DbSet<Adresse> Adresses { get; set; }
public DbSet<Client> Clients { get; set; }
public DbSet<Panier> Paniers { get; set; }
public DbSet<Commande> Commandes { get; set; }
}
}
Définition de DbContext
sur la doc MS, juste en prenant les premiers mots : « Une instance DbContext représente une session avec la base de données et peut être utilisée pour interroger et enregistrer des instances de vos entités«
Définition du type DbSet
sur la doc MS. En gros ça indique que l’entité passé dans le DbSet sera une table en base de données, et que les requêtes LinQ faites seront traduites en SQL.
Il ne reste plus qu’à ajouter dans Program.cs
, le code pour faire la connexion, ligne très importante, je reviens un peu plus bas sur cette « connexion« .
// Add Application Db Context options
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(@"Data Source=TutoCodeFirst.db"));
Génération de la base
Pour que la base de données soit généré par le code, il faut ajouter un autre package : Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.EntityFrameworkCore.Tools
Install-Package Microsoft.EntityFrameworkCore.Tools
C’est avec ce package que nous pourrons générer le code pour créer notre base de données.
Commande pour générer le code.
dotnet ef migrations add InitialCreate
Add-Migration InitialCreate
Une fois la compilation terminée, il y a dans le projet un nouveau répertoire : Migrations
.
A l’intérieur se trouve une class InitialCreate
, tient j’ai déjà vu ça quelque part, qui hérite de Migration. Elle contient 2 méthodes :
protected override void Up(MigrationBuilder migrationBuilder) {...}
voir la doc Migration.Up
protected override void Down(MigrationBuilder migrationBuilder) {...}
voir la doc Migration.Down
Il y a aussi la class ApplicationDbContextModelSnapshot, qui hérite de ModelSnapshot (par ici la doc)
Remarque
Le code qui est généré est fait en fonction de la « connexion« . Quand je parle de connexion, c’est options.UseSqlite(...)
dans Program.cs
. Si j’utilise une autre base de données, par exemple MariaDb/MySQL, j’utiliserai le package : Pomelo.EntityFrameworkCore.MySql
au lieu de Microsoft.EntityFrameworkCore.Sqlite
, et je mettrai options.UseMySql(...)
.
Voici un exemple de code généré pour SQLite et pour MySQL, juste sur la table Clients.
migrationBuilder.CreateTable(
name: "Clients",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Prenom = table.Column<string>(type: "TEXT", nullable: false),
Nom = table.Column<string>(type: "TEXT", nullable: false),
Age = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Clients", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Clients",
columns: table => new
{
Id = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Prenom = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Nom = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Age = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Clients", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
C’est ce qui est génial avec EF Core CodeFirst, sans changer code, nous ne dépendons plus d’une base.
Pour créer la base à partir du code il faut exécuter une des commandes.
dotnet ef database update InitialCreate
# en mettant le nom donné de la migration.
Update-Database
En voici les logs.
Build started...
Build succeeded.
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 6.0.1 initialized 'ApplicationDbContext' using provider 'Microsoft.EntityFrameworkCore.Sqlite:6.0.1' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (73ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
PRAGMA journal_mode = 'wal';
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (38ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE "__EFMigrationsHistory" (
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
"ProductVersion" TEXT NOT NULL
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT COUNT(*) FROM "sqlite_master" WHERE "name" = '__EFMigrationsHistory' AND "type" = 'table';
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT "MigrationId", "ProductVersion"
FROM "__EFMigrationsHistory"
ORDER BY "MigrationId";
info: Microsoft.EntityFrameworkCore.Migrations[20402]
Applying migration '20220129111929_InitialCreate'.
Applying migration '20220129111929_InitialCreate'.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE "Clients" (
"Id" TEXT NOT NULL CONSTRAINT "PK_Clients" PRIMARY KEY,
"Prenom" TEXT NOT NULL,
"Nom" TEXT NOT NULL,
"Age" INTEGER NOT NULL
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE "Commandes" (
"IdCmd" TEXT NOT NULL CONSTRAINT "PK_Commandes" PRIMARY KEY,
"ClientId" TEXT NOT NULL
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE "Adresses" (
"Id" TEXT NOT NULL CONSTRAINT "PK_Adresses" PRIMARY KEY,
"Numero" TEXT NOT NULL,
"Rue" TEXT NOT NULL,
"Ville" TEXT NOT NULL,
"ClientId" TEXT NOT NULL,
CONSTRAINT "FK_Adresses_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE "Telephones" (
"Number" TEXT NOT NULL CONSTRAINT "PK_Telephones" PRIMARY KEY,
"Type" TEXT NOT NULL,
"ClientId" TEXT NOT NULL,
CONSTRAINT "FK_Telephones_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE "Paniers" (
"IdPanier" TEXT NOT NULL CONSTRAINT "PK_Paniers" PRIMARY KEY,
"IdItem" INTEGER NOT NULL,
"Quantite" INTEGER NOT NULL,
"CommandeIdCmd" TEXT NOT NULL,
CONSTRAINT "FK_Paniers_Commandes_CommandeIdCmd" FOREIGN KEY ("CommandeIdCmd") REFERENCES "Commandes" ("IdCmd") ON DELETE CASCADE
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE UNIQUE INDEX "IX_Adresses_ClientId" ON "Adresses" ("ClientId");
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE INDEX "IX_Paniers_CommandeIdCmd" ON "Paniers" ("CommandeIdCmd");
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE INDEX "IX_Telephones_ClientId" ON "Telephones" ("ClientId");
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20220129111929_InitialCreate', '6.0.1');
Done.
La création de la base SQLite est faite à la racine du projet et se nomme : TutoCodeFirst.db
.
Voilà c’était pour démystifier Entity Framework Core : Code First.
Top comments (0)