EntityFrameworkCore Code Firsts

ryck

Anthony Ryckembusch

Posted on August 7, 2023

EntityFrameworkCore Code Firsts

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 :

Image créée à partir de MySQL, ou les tables sont créées par EF Core… vous allez voir plus tard.

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
Install-Package Microsoft.EntityFrameworkCore.Sqlite
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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; }
    }
}
Enter fullscreen mode Exit fullscreen mode
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; }
    }
}
Enter fullscreen mode Exit fullscreen mode
namespace WebApiCodeFirst.Models
{
    public class Telephone
    {
        [Key]
        public string Number { get; set; }

        public string Type { get; set; }

        [ForeignKey("ClientId")]
        public string ClientId { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
    }
}
Enter fullscreen mode Exit fullscreen mode
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; }
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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; }
    }
}
Enter fullscreen mode Exit fullscreen mode

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"));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
Install-Package Microsoft.EntityFrameworkCore.Tools
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
Add-Migration InitialCreate
Enter fullscreen mode Exit fullscreen mode

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);
                });
Enter fullscreen mode Exit fullscreen mode
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");
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode
Update-Database
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
ryck
Anthony Ryckembusch

Posted on August 7, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related