Julien Chomarat
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.
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
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 },
]);
}
}
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.
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);
});
}
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.
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
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