Technique Mapping des enums avec EF Core 9 et PostgreSQL

Julien Chomarat Photo de Julien Chomarat

Julien Chomarat

Mapping des enums avec EF Core 9 et PostgreSQL

Avec l'arrivée de la version 9 d'Entity Framework Core et du fournisseur Npgsql pour PostgreSQL, une nouvelle fonctionnalité simplifiant le mapping des enums a fait son apparition.

Dans cet article, nous allons explorer cette nouveauté à travers un exemple concret avec un projet console.

Mise en place du projet

Pour démarrer, créez un nouveau répertoire et configurez votre projet :

# Création du projet console
dotnet new console --name PgEnum --output .

# Ajout des packages NuGet
dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.0
dotnet add package Microsoft.Extensions.Hosting --version 9.0.0
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 9.0.1

# Ajout de la CLI dotnet-ef
dotnet tool install dotnet-ef --version 9.0.0 --create-manifest-if-needed

Configuration du contexte de base de données

Ajoutez le code suivant dans un fichier TestDbContext.cs

using Microsoft.EntityFrameworkCore;

public class Person
{
    public int Id { get; set; }
    public Mood Mood { get; set; }

    public override string ToString()
    {
        return $"Id = {Id}, Mood = {Mood}";
    }
}

public enum Mood
{
    Sad,
    Happy
}

public class TestDbContext(DbContextOptions<TestDbContext> options)
  : DbContext(options)
{
    public DbSet<Person> Persons => Set<Person>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Person>().HasData([
            new Person { Id = 1, Mood = Mood.Happy },
            new Person { Id = 2, Mood = Mood.Sad },
            new Person { Id = 3, Mood = Mood.Happy },
        ]);
    }
}

Configuration de l'application

Ajoutez le code suivant dans Program.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddDbContextPool<TestDbContext>(
            opt => opt.UseNpgsql("Server=localhost;Database=PgEnum;User Id=postgres;Password=Pwd12345!;",
            o => o.MapEnum<Mood>())
        );
    })
    .Build();

Le support des enums se fait avec cette méthode : o => o.MapEnum<Mood>().

L'avantage avec cette méthode est que cela simplifie la configuration des enums au niveau d'entity framework ainsi qu'au niveau de Npgsql.

Création et migration de la base de données

Générez une migration avec :

dotnet ef migrations add InitialCreate

Comme vous pouvez le voir ici, EF gère maintenant la création des types enum version PostgreSQL.

protected override void Up(MigrationBuilder migrationBuilder)
{
    // Création du type enum PostgreSQL
    migrationBuilder.AlterDatabase()
        .Annotation("Npgsql:Enum:mood", "happy,sad");

    migrationBuilder.CreateTable(
        name: "Persons",
        columns: table => new
        {
            Id = table.Column<int>(type: "integer", nullable: false)
                .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),

            // Utilisation du type enum PostgreSQL
            Mood = table.Column<Mood>(type: "mood", nullable: false)
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_Persons", x => x.Id);
        });
}

Test avec PostgreSQL 🐘

Lancez un conteneur PostgreSQL :

docker run -d --name pg-enum-test -e POSTGRES_PASSWORD=Pwd12345! -p 5432:5432 postgres:17.2

Appliquez la migration :

dotnet ef database update

Connectez-vous à PostgreSQL :

# On rentre dans notre image docker
docker exec -it pg-enum-test bash

# On se connecte au serveur PostgreSQL
psql -h localhost -U postgres

# On se connecte sur notre base de données de test
\connect PgEnum

Interrogez les données :

SELECT * FROM "Persons";

Et comme vous pouvez le voir dans la colonne Mood notre enum est sous forme de texte au lieu d'avoir un entier.

 Id | Mood
----+-------
  1 | happy
  2 | sad
  3 | happy
(3 rows)

Ce qui change ici est que le stockage sur le disk d'un enum est de 4 octets au lieu d'une chaine de caractères qui en prendrait très souvent plus.

Vous pouvez maintenant taper deux fois exit pour revenir à votre shell.

Problème d'ordre des enums entre PostgreSQL et dotnet

Vous allez ajouter ça à la fin de votre Program.cs, cela récupère la liste des Persons et les classe en fonction de leur Mood.

Sauf que le OrderBy se fait une fois par le framework dotnet et dans le second temps il se fait par PostgreSQL.

using var db = host.Services.GetRequiredService<TestDbContext>();

var persons = (await db.Persons.ToListAsync()).OrderBy(p => p.Mood).ToList();
Console.WriteLine("(await db.Persons.ToListAsync()).OrderBy(p => p.Mood).ToList();");
Console.WriteLine("---------------------------------------------------------------");
persons.ForEach(p => Console.WriteLine(p));

persons = await db.Persons.OrderBy(p => p.Mood).ToListAsync();
Console.WriteLine();
Console.WriteLine("await db.Persons.OrderBy(p => p.Mood).ToListAsync();");
Console.WriteLine("----------------------------------------------------");
persons.ForEach(p => Console.WriteLine(p));

Lancez le programme

dotnet run

Voici le résultat affiché

(await db.Persons.ToListAsync()).OrderBy(p => p.Mood).ToList();
---------------------------------------------------------------
Id = 2, Mood = Sad
Id = 1, Mood = Happy
Id = 3, Mood = Happy

await db.Persons.OrderBy(p => p.Mood).ToListAsync();
----------------------------------------------------
Id = 1, Mood = Happy
Id = 3, Mood = Happy
Id = 2, Mood = Sad

Le problème se trouve dans la migration générée. Pour le moment, l'enum est classé automatiquement par ordre alphabétique et PostgreSQL trie en fonction de l'ordre donné au moment de la création.

protected override void Up(MigrationBuilder migrationBuilder)
{
  // Création du type enum PostgreSQL
    migrationBuilder.AlterDatabase()
        .Annotation("Npgsql:Enum:mood", "happy,sad");
}

Pour aligner le tri du framework avec celui de PostgreSQL vous devez modifier l'ordre à la main pour qu'il suive celui de votre enum.

protected override void Up(MigrationBuilder migrationBuilder)
{
  // Création du type enum PostgreSQL
    migrationBuilder.AlterDatabase()
        .Annotation("Npgsql:Enum:mood", "sad,happy");
}

Maintenant on va détruire la base, la reconstruire et lancer notre programme

# On détruit la base
dotnet ef database drop

# On applique la migration
dotnet ef database update

# On lance notre programme
dotnet run

Et voila le résultat !

(await db.Persons.ToListAsync()).OrderBy(p => p.Mood).ToList();
---------------------------------------------------------------
Id = 2, Mood = Sad
Id = 1, Mood = Happy
Id = 3, Mood = Happy

await db.Persons.OrderBy(p => p.Mood).ToListAsync();
----------------------------------------------------
Id = 2, Mood = Sad
Id = 1, Mood = Happy
Id = 3, Mood = Happy

Conclusion

Le support des enums avec PostgreSQL est intéressante, mais vous devez faire attention au problème actuel de l'ordre dans la migration si vous souhaitez aligner les tris entre le framework et PostgreSQL.

Un autre point important concerne la modification des enums, PostgreSQL est assez stricte et vous serez très probablement amené à gérer ça directement en SQL via une création d'un nouvel enum et la suppression de l'ancien.

Astuce, si vous souhaitez connaitre les valeurs de votre enum et leur ordre vous pouvez le faire avec cette requête.

SELECT enum_range(null::mood);

Voici la définition de notre enum

 enum_range
-------------
 {sad,happy}
(1 row)

Vous pouvez suivre cette issue Consider ordering enum labels based on the .NET label value instead of alphabetically, cela devrait simplifier certains des points ci-dessus.

Nous sommes arrivés au bout de cet article, vous pouvez maintenant effacer votre image docker

docker rm pg-enum-test --force
Tags : C# EntityFramework PostgreSQL