Skip to content

Latest commit

 

History

History
2070 lines (1635 loc) · 51.9 KB

File metadata and controls

2070 lines (1635 loc) · 51.9 KB

🔝 Retour au Sommaire

20.1.5. .NET : Npgsql, Entity Framework Core

Introduction

.NET (anciennement .NET Core) est une plateforme de développement moderne créée par Microsoft, idéale pour construire des applications web, APIs, microservices et applications desktop. Pour interagir avec PostgreSQL en .NET, deux approches principales existent :

  1. Npgsql : Provider ADO.NET natif pour PostgreSQL, accès bas niveau
  2. Entity Framework Core (EF Core) : ORM moderne et puissant de Microsoft

Ce tutoriel couvre les deux approches en profondeur pour vous permettre de choisir celle qui convient le mieux à votre projet.


Vue d'Ensemble : Provider vs ORM

Qu'est-ce qu'un Provider ADO.NET ?

ADO.NET est l'API standard .NET pour l'accès aux bases de données. Npgsql est le provider ADO.NET pour PostgreSQL qui vous permet d'écrire du SQL brut et de manipuler les résultats directement.

Analogie : C'est comme parler directement à PostgreSQL dans sa langue native (SQL).

Qu'est-ce qu'un ORM ?

Un ORM (Object-Relational Mapping) est une couche d'abstraction qui traduit vos classes C# en requêtes SQL. Entity Framework Core est l'ORM phare de Microsoft, moderne et performant.

Analogie : C'est comme avoir un traducteur automatique entre votre code C# et PostgreSQL.

Comparaison Rapide

Caractéristique Npgsql (ADO.NET) EF Core
Type Provider natif ORM complet
Langage SQL brut LINQ (C# queries)
Performance Excellente (direct) Très bonne (overhead minimal)
Courbe d'apprentissage Moyenne (SQL requis) Facile (C# natif)
Contrôle Total Abstraction
Type-safety Manuelle Automatique (forte)
Migrations Manuelles Intégrées (Code-First)
Boilerplate Plus de code Moins de code
IntelliSense Limité (strings SQL) Complet (LINQ)
Tracking Manuel Automatique (ChangeTracking)
Relations Manuelles Automatiques (Navigation properties)

Recommandation :

  • Npgsql : Pour des performances maximales, SQL complexe, contrôle total, microservices légers
  • EF Core : Pour la productivité, applications CRUD standard, prototypage rapide, équipes C# expérimentées

Partie 1 : Npgsql (Provider ADO.NET)

Introduction à Npgsql

Npgsql est le provider ADO.NET open-source pour PostgreSQL. Il est :

  • Performant : Optimisé spécifiquement pour PostgreSQL
  • Complet : Support de toutes les fonctionnalités PostgreSQL
  • Mature : Plus de 15 ans de développement actif
  • Standard : Implémente l'API ADO.NET standard de .NET

Installation

Via NuGet Package Manager Console

Install-Package Npgsql

Via .NET CLI

dotnet add package Npgsql

Fichier .csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Npgsql" Version="8.0.1" />
  </ItemGroup>
</Project>

Connexion Simple

using Npgsql;  
using System;  

class Program
{
    static void Main()
    {
        // Chaîne de connexion
        string connectionString = "Host=localhost;Port=5432;Database=mydb;Username=user;Password=password";

        // Établir la connexion
        using var conn = new NpgsqlConnection(connectionString);
        conn.Open();

        Console.WriteLine("✅ Connexion établie avec succès");

        // Tester la connexion
        using var cmd = new NpgsqlCommand("SELECT version()", conn);
        string version = (string)cmd.ExecuteScalar();

        Console.WriteLine($"Version PostgreSQL : {version}");
    }
}

Format de la chaîne de connexion :

Host=[host];Port=[port];Database=[database];Username=[user];Password=[password]

Exemples de chaînes de connexion :

// Local avec port par défaut
"Host=localhost;Database=mydb;Username=user;Password=password"

// Avec SSL
"Host=localhost;Database=mydb;Username=user;Password=password;SSL Mode=Require"

// Avec timeout
"Host=localhost;Database=mydb;Username=user;Password=password;Timeout=30;Command Timeout=30"

// Avec pooling (activé par défaut)
"Host=localhost;Database=mydb;Username=user;Password=password;Pooling=true;Maximum Pool Size=100"

Pool de Connexions

Le pooling est activé par défaut dans Npgsql, mais vous pouvez le configurer :

var connectionString = "Host=localhost;Database=mydb;Username=user;Password=password;" +
                      "Pooling=true;" +                     // Activé par défaut
                      "Minimum Pool Size=5;" +              // Min 5 connexions
                      "Maximum Pool Size=100;" +            // Max 100 connexions
                      "Connection Idle Lifetime=300;" +     // 5 min avant fermeture
                      "Connection Pruning Interval=10";     // Nettoyage toutes les 10s

using var conn = new NpgsqlConnection(connectionString);

Modèles de Données (Classes)

using System;

namespace MyApp.Models
{
    public class User
    {
        public int Id { get; set; }
        public string Nom { get; set; }
        public string Email { get; set; }
        public int Age { get; set; }
        public DateTime CreatedAt { get; set; }
        public DateTime UpdatedAt { get; set; }
    }

    public class CreateUserDto
    {
        public string Nom { get; set; }
        public string Email { get; set; }
        public int Age { get; set; }
    }

    public class UpdateUserDto
    {
        public string Nom { get; set; }
        public string Email { get; set; }
        public int? Age { get; set; }
    }
}

CRUD Opérations

CREATE : Insérer des données

using Npgsql;  
using MyApp.Models;  

public class UserRepository
{
    private readonly string _connectionString;

    public UserRepository(string connectionString)
    {
        _connectionString = connectionString;
    }

    public User CreateUser(CreateUserDto dto)
    {
        using var conn = new NpgsqlConnection(_connectionString);
        conn.Open();

        string sql = @"
            INSERT INTO utilisateurs (nom, email, age)
            VALUES (@nom, @email, @age)
            RETURNING id, nom, email, age, created_at, updated_at";

        using var cmd = new NpgsqlCommand(sql, conn);

        // Paramètres (protection contre injection SQL)
        cmd.Parameters.AddWithValue("nom", dto.Nom);
        cmd.Parameters.AddWithValue("email", dto.Email);
        cmd.Parameters.AddWithValue("age", dto.Age);

        using var reader = cmd.ExecuteReader();

        if (reader.Read())
        {
            return new User
            {
                Id = reader.GetInt32(0),
                Nom = reader.GetString(1),
                Email = reader.GetString(2),
                Age = reader.GetInt32(3),
                CreatedAt = reader.GetDateTime(4),
                UpdatedAt = reader.GetDateTime(5)
            };
        }

        throw new Exception("Échec de la création");
    }
}

// Utilisation
var repo = new UserRepository(connectionString);  
var newUser = new CreateUserDto  
{
    Nom = "Alice",
    Email = "alice@example.com",
    Age = 30
};

User user = repo.CreateUser(newUser);  
Console.WriteLine($"Utilisateur créé : ID={user.Id}");  

Insertion multiple (batch) :

public void CreateUsers(List<CreateUserDto> users)
{
    using var conn = new NpgsqlConnection(_connectionString);
    conn.Open();

    using var transaction = conn.BeginTransaction();

    try
    {
        string sql = "INSERT INTO utilisateurs (nom, email, age) VALUES (@nom, @email, @age)";

        foreach (var user in users)
        {
            using var cmd = new NpgsqlCommand(sql, conn, transaction);
            cmd.Parameters.AddWithValue("nom", user.Nom);
            cmd.Parameters.AddWithValue("email", user.Email);
            cmd.Parameters.AddWithValue("age", user.Age);
            cmd.ExecuteNonQuery();
        }

        transaction.Commit();
        Console.WriteLine($"{users.Count} utilisateurs créés");
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
}

Méthode alternative avec Prepared Statement :

public void CreateUsersPrepared(List<CreateUserDto> users)
{
    using var conn = new NpgsqlConnection(_connectionString);
    conn.Open();

    string sql = "INSERT INTO utilisateurs (nom, email, age) VALUES ($1, $2, $3)";

    using var cmd = new NpgsqlCommand(sql, conn);
    cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p1" });
    cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p2" });
    cmd.Parameters.Add(new NpgsqlParameter { ParameterName = "p3" });
    cmd.Prepare();

    foreach (var user in users)
    {
        cmd.Parameters[0].Value = user.Nom;
        cmd.Parameters[1].Value = user.Email;
        cmd.Parameters[2].Value = user.Age;
        cmd.ExecuteNonQuery();
    }
}

READ : Lire des données

// Récupérer un utilisateur par ID
public User GetUserById(int id)
{
    using var conn = new NpgsqlConnection(_connectionString);
    conn.Open();

    string sql = @"
        SELECT id, nom, email, age, created_at, updated_at
        FROM utilisateurs
        WHERE id = @id";

    using var cmd = new NpgsqlCommand(sql, conn);
    cmd.Parameters.AddWithValue("id", id);

    using var reader = cmd.ExecuteReader();

    if (reader.Read())
    {
        return new User
        {
            Id = reader.GetInt32(0),
            Nom = reader.GetString(1),
            Email = reader.GetString(2),
            Age = reader.GetInt32(3),
            CreatedAt = reader.GetDateTime(4),
            UpdatedAt = reader.GetDateTime(5)
        };
    }

    return null; // Utilisateur non trouvé
}

// Récupérer tous les utilisateurs
public List<User> GetAllUsers()
{
    var users = new List<User>();

    using var conn = new NpgsqlConnection(_connectionString);
    conn.Open();

    string sql = "SELECT id, nom, email, age, created_at, updated_at FROM utilisateurs ORDER BY nom";

    using var cmd = new NpgsqlCommand(sql, conn);
    using var reader = cmd.ExecuteReader();

    while (reader.Read())
    {
        users.Add(new User
        {
            Id = reader.GetInt32(0),
            Nom = reader.GetString(1),
            Email = reader.GetString(2),
            Age = reader.GetInt32(3),
            CreatedAt = reader.GetDateTime(4),
            UpdatedAt = reader.GetDateTime(5)
        });
    }

    return users;
}

// Recherche avec filtres
public List<User> SearchUsers(string namePattern, int minAge, int maxAge)
{
    var users = new List<User>();

    using var conn = new NpgsqlConnection(_connectionString);
    conn.Open();

    string sql = @"
        SELECT id, nom, email, age, created_at, updated_at
        FROM utilisateurs
        WHERE nom ILIKE @pattern AND age BETWEEN @minAge AND @maxAge
        ORDER BY nom";

    using var cmd = new NpgsqlCommand(sql, conn);
    cmd.Parameters.AddWithValue("pattern", $"%{namePattern}%");
    cmd.Parameters.AddWithValue("minAge", minAge);
    cmd.Parameters.AddWithValue("maxAge", maxAge);

    using var reader = cmd.ExecuteReader();

    while (reader.Read())
    {
        users.Add(new User
        {
            Id = reader.GetInt32(0),
            Nom = reader.GetString(1),
            Email = reader.GetString(2),
            Age = reader.GetInt32(3),
            CreatedAt = reader.GetDateTime(4),
            UpdatedAt = reader.GetDateTime(5)
        });
    }

    return users;
}

Méthode helper pour mapper les résultats :

private User MapReaderToUser(NpgsqlDataReader reader)
{
    return new User
    {
        Id = reader.GetInt32(reader.GetOrdinal("id")),
        Nom = reader.GetString(reader.GetOrdinal("nom")),
        Email = reader.GetString(reader.GetOrdinal("email")),
        Age = reader.GetInt32(reader.GetOrdinal("age")),
        CreatedAt = reader.GetDateTime(reader.GetOrdinal("created_at")),
        UpdatedAt = reader.GetDateTime(reader.GetOrdinal("updated_at"))
    };
}

public List<User> GetAllUsersV2()
{
    var users = new List<User>();

    using var conn = new NpgsqlConnection(_connectionString);
    conn.Open();

    string sql = "SELECT * FROM utilisateurs ORDER BY nom";
    using var cmd = new NpgsqlCommand(sql, conn);
    using var reader = cmd.ExecuteReader();

    while (reader.Read())
    {
        users.Add(MapReaderToUser(reader));
    }

    return users;
}

UPDATE : Modifier des données

// Mettre à jour un utilisateur
public User UpdateUser(int id, UpdateUserDto dto)
{
    using var conn = new NpgsqlConnection(_connectionString);
    conn.Open();

    string sql = @"
        UPDATE utilisateurs
        SET nom = @nom, email = @email, age = @age, updated_at = NOW()
        WHERE id = @id
        RETURNING id, nom, email, age, created_at, updated_at";

    using var cmd = new NpgsqlCommand(sql, conn);
    cmd.Parameters.AddWithValue("nom", dto.Nom);
    cmd.Parameters.AddWithValue("email", dto.Email);
    cmd.Parameters.AddWithValue("age", dto.Age ?? 0);
    cmd.Parameters.AddWithValue("id", id);

    using var reader = cmd.ExecuteReader();

    if (reader.Read())
    {
        return MapReaderToUser(reader);
    }

    throw new Exception("Utilisateur non trouvé");
}

// Mise à jour partielle (uniquement les champs fournis)
public void UpdateUserPartial(int id, UpdateUserDto dto)
{
    using var conn = new NpgsqlConnection(_connectionString);
    conn.Open();

    var updates = new List<string>();
    var cmd = new NpgsqlCommand { Connection = conn };

    if (!string.IsNullOrEmpty(dto.Nom))
    {
        updates.Add("nom = @nom");
        cmd.Parameters.AddWithValue("nom", dto.Nom);
    }

    if (!string.IsNullOrEmpty(dto.Email))
    {
        updates.Add("email = @email");
        cmd.Parameters.AddWithValue("email", dto.Email);
    }

    if (dto.Age.HasValue)
    {
        updates.Add("age = @age");
        cmd.Parameters.AddWithValue("age", dto.Age.Value);
    }

    if (updates.Count == 0)
    {
        throw new Exception("Aucun champ à mettre à jour");
    }

    updates.Add("updated_at = NOW()");

    cmd.CommandText = $"UPDATE utilisateurs SET {string.Join(", ", updates)} WHERE id = @id";
    cmd.Parameters.AddWithValue("id", id);

    int rowsAffected = cmd.ExecuteNonQuery();

    if (rowsAffected == 0)
    {
        throw new Exception("Utilisateur non trouvé");
    }
}

DELETE : Supprimer des données

// Supprimer un utilisateur
public void DeleteUser(int id)
{
    using var conn = new NpgsqlConnection(_connectionString);
    conn.Open();

    string sql = "DELETE FROM utilisateurs WHERE id = @id";

    using var cmd = new NpgsqlCommand(sql, conn);
    cmd.Parameters.AddWithValue("id", id);

    int rowsAffected = cmd.ExecuteNonQuery();

    if (rowsAffected == 0)
    {
        throw new Exception("Utilisateur non trouvé");
    }
}

// Supprimer plusieurs utilisateurs
public int DeleteUsersByAge(int maxAge)
{
    using var conn = new NpgsqlConnection(_connectionString);
    conn.Open();

    string sql = "DELETE FROM utilisateurs WHERE age < @maxAge";

    using var cmd = new NpgsqlCommand(sql, conn);
    cmd.Parameters.AddWithValue("maxAge", maxAge);

    return cmd.ExecuteNonQuery();
}

Gestion des Transactions

public void TransferMoney(int fromAccountId, int toAccountId, decimal amount)
{
    using var conn = new NpgsqlConnection(_connectionString);
    conn.Open();

    using var transaction = conn.BeginTransaction();

    try
    {
        // Opération 1 : Débiter
        using (var cmd = new NpgsqlCommand(
            "UPDATE comptes SET solde = solde - @amount WHERE id = @id",
            conn, transaction))
        {
            cmd.Parameters.AddWithValue("amount", amount);
            cmd.Parameters.AddWithValue("id", fromAccountId);
            cmd.ExecuteNonQuery();
        }

        // Opération 2 : Créditer
        using (var cmd = new NpgsqlCommand(
            "UPDATE comptes SET solde = solde + @amount WHERE id = @id",
            conn, transaction))
        {
            cmd.Parameters.AddWithValue("amount", amount);
            cmd.Parameters.AddWithValue("id", toAccountId);
            cmd.ExecuteNonQuery();
        }

        // Valider
        transaction.Commit();
        Console.WriteLine("✅ Transfert réussi");
    }
    catch (Exception ex)
    {
        // Annuler
        transaction.Rollback();
        Console.WriteLine($"❌ Transfert annulé : {ex.Message}");
        throw;
    }
}

Transaction avec Savepoint :

public void ComplexTransaction()
{
    using var conn = new NpgsqlConnection(_connectionString);
    conn.Open();

    using var transaction = conn.BeginTransaction();

    try
    {
        // Opération 1
        using (var cmd = new NpgsqlCommand("INSERT INTO logs (message) VALUES ('Op 1')", conn, transaction))
        {
            cmd.ExecuteNonQuery();
        }

        // Créer un savepoint
        transaction.Save("savepoint1");

        try
        {
            // Opération 2 (risquée)
            using var cmd = new NpgsqlCommand("INSERT INTO risky_table (data) VALUES ('data')", conn, transaction);
            cmd.ExecuteNonQuery();
        }
        catch
        {
            // Revenir au savepoint
            transaction.Rollback("savepoint1");
            Console.WriteLine("Savepoint restauré");
        }

        // Opération 3
        using (var cmd = new NpgsqlCommand("INSERT INTO logs (message) VALUES ('Op 3')", conn, transaction))
        {
            cmd.ExecuteNonQuery();
        }

        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
}

Types de Données PostgreSQL

Types numériques

// INTEGER, BIGINT
cmd.Parameters.AddWithValue("age", 30);  
cmd.Parameters.AddWithValue("bigNumber", 1000000000L);  

// NUMERIC/DECIMAL (utiliser decimal)
cmd.Parameters.AddWithValue("price", 19.99m);

// FLOAT/DOUBLE PRECISION
cmd.Parameters.AddWithValue("pi", 3.14159);

Types texte

// VARCHAR, TEXT
cmd.Parameters.AddWithValue("nom", "Alice");  
cmd.Parameters.AddWithValue("description", "Longue description...");  

Types temporels

using NpgsqlTypes;

// DATE
cmd.Parameters.Add("birthday", NpgsqlDbType.Date).Value = new DateTime(1995, 5, 15);

// TIMESTAMP
cmd.Parameters.Add("created_at", NpgsqlDbType.Timestamp).Value = DateTime.Now;

// TIMESTAMPTZ (avec timezone)
cmd.Parameters.Add("created_at_tz", NpgsqlDbType.TimestampTz).Value = DateTime.UtcNow;

// INTERVAL
cmd.Parameters.Add("duration", NpgsqlDbType.Interval).Value = TimeSpan.FromHours(2);

JSON et JSONB

using System.Text.Json;  
using NpgsqlTypes;  

// Classe pour JSON
public class Profile
{
    public string Nom { get; set; }
    public int Age { get; set; }
    public List<string> Tags { get; set; }
}

// Insertion JSONB
var profile = new Profile
{
    Nom = "Alice",
    Age = 30,
    Tags = new List<string> { "developer", "postgresql" }
};

string profileJson = JsonSerializer.Serialize(profile);

cmd.Parameters.Add("profile", NpgsqlDbType.Jsonb).Value = profileJson;

// Lecture JSONB
string json = reader.GetString(reader.GetOrdinal("profile"));  
Profile readProfile = JsonSerializer.Deserialize<Profile>(json);  

// Requête JSONB
string sql = "SELECT * FROM users WHERE profile->>'nom' = @nom";  
cmd.Parameters.AddWithValue("nom", "Alice");  

Arrays (Tableaux PostgreSQL)

// Insertion d'array
string[] tags = { "csharp", "postgresql", "npgsql" };  
cmd.Parameters.Add("tags", NpgsqlDbType.Array | NpgsqlDbType.Text).Value = tags;  

// Lecture d'array
string[] readTags = (string[])reader["tags"];

foreach (string tag in readTags)
{
    Console.WriteLine(tag);
}

// Recherche dans array
string sql = "SELECT * FROM articles WHERE @tag = ANY(tags)";  
cmd.Parameters.AddWithValue("tag", "postgresql");  

UUID

using System;

// Génération UUID côté C#
Guid id = Guid.NewGuid();  
cmd.Parameters.Add("id", NpgsqlDbType.Uuid).Value = id;  

// Génération UUID côté PostgreSQL
string sql = "INSERT INTO events (id, data) VALUES (gen_uuid_v7(), @data) RETURNING id";  
cmd.Parameters.AddWithValue("data", "event data");  

Guid newId = (Guid)cmd.ExecuteScalar();

// Lecture UUID
Guid sessionId = reader.GetGuid(reader.GetOrdinal("id"));

Gestion des Erreurs

using Npgsql;

try
{
    // Opération de base de données
    CreateUser(newUser);
}
catch (PostgresException ex)
{
    // Erreurs PostgreSQL spécifiques
    switch (ex.SqlState)
    {
        case "23505": // Unique violation
            Console.WriteLine("Erreur : Cette valeur existe déjà");
            break;
        case "23503": // Foreign key violation
            Console.WriteLine("Erreur : Référence introuvable");
            break;
        case "23502": // NOT NULL violation
            Console.WriteLine("Erreur : Champ obligatoire manquant");
            break;
        case "42P01": // Undefined table
            Console.WriteLine("Erreur : Table inexistante");
            break;
        default:
            Console.WriteLine($"Erreur PostgreSQL : {ex.Message}");
            break;
    }
}
catch (NpgsqlException ex)
{
    // Erreurs de connexion Npgsql
    Console.WriteLine($"Erreur de connexion : {ex.Message}");
}
catch (Exception ex)
{
    // Autres erreurs
    Console.WriteLine($"Erreur : {ex.Message}");
}

Exemple Complet : API ASP.NET Core avec Npgsql

using Microsoft.AspNetCore.Mvc;  
using Npgsql;  
using MyApp.Models;  

// Program.cs (ASP.NET Core 6+)
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();  
builder.Services.AddSingleton<UserRepository>(sp =>  
    new UserRepository(builder.Configuration.GetConnectionString("PostgreSQL")));

var app = builder.Build();

app.MapControllers();  
app.Run();  

// appsettings.json
{
  "ConnectionStrings": {
    "PostgreSQL": "Host=localhost;Database=mydb;Username=user;Password=password"
  }
}

// Controllers/UsersController.cs
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly UserRepository _repository;

    public UsersController(UserRepository repository)
    {
        _repository = repository;
    }

    // GET api/users
    [HttpGet]
    public ActionResult<List<User>> GetUsers()
    {
        try
        {
            var users = _repository.GetAllUsers();
            return Ok(users);
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { error = ex.Message });
        }
    }

    // GET api/users/5
    [HttpGet("{id}")]
    public ActionResult<User> GetUser(int id)
    {
        try
        {
            var user = _repository.GetUserById(id);

            if (user == null)
                return NotFound(new { error = "Utilisateur non trouvé" });

            return Ok(user);
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { error = ex.Message });
        }
    }

    // POST api/users
    [HttpPost]
    public ActionResult<User> CreateUser([FromBody] CreateUserDto dto)
    {
        try
        {
            var user = _repository.CreateUser(dto);
            return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
        }
        catch (PostgresException ex) when (ex.SqlState == "23505")
        {
            return Conflict(new { error = "Email déjà utilisé" });
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { error = ex.Message });
        }
    }

    // PUT api/users/5
    [HttpPut("{id}")]
    public ActionResult<User> UpdateUser(int id, [FromBody] UpdateUserDto dto)
    {
        try
        {
            var user = _repository.UpdateUser(id, dto);
            return Ok(user);
        }
        catch (Exception ex) when (ex.Message.Contains("non trouvé"))
        {
            return NotFound(new { error = "Utilisateur non trouvé" });
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { error = ex.Message });
        }
    }

    // DELETE api/users/5
    [HttpDelete("{id}")]
    public IActionResult DeleteUser(int id)
    {
        try
        {
            _repository.DeleteUser(id);
            return NoContent();
        }
        catch (Exception ex) when (ex.Message.Contains("non trouvé"))
        {
            return NotFound(new { error = "Utilisateur non trouvé" });
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { error = ex.Message });
        }
    }
}

Partie 2 : Entity Framework Core (ORM)

Introduction à Entity Framework Core

Entity Framework Core (EF Core) est l'ORM moderne et cross-platform de Microsoft. Il offre :

  • Code-First : Définissez vos modèles en C#, les tables sont générées automatiquement
  • LINQ : Requêtes fortement typées en C# au lieu de SQL
  • Change Tracking : Suivi automatique des modifications
  • Migrations : Gestion du schéma de base de données
  • Relations : Navigation properties automatiques

Installation

# EF Core avec provider PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore  
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL  

# Outils pour les migrations
dotnet add package Microsoft.EntityFrameworkCore.Design

# CLI tools (global)
dotnet tool install --global dotnet-ef

Configuration et DbContext

Le DbContext est le point central d'EF Core qui représente votre session avec la base de données.

using Microsoft.EntityFrameworkCore;  
using MyApp.Models;  

namespace MyApp.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
        {
        }

        // DbSets représentent les tables
        public DbSet<User> Users { get; set; }
        public DbSet<Post> Posts { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // Configuration des entités
            modelBuilder.Entity<User>(entity =>
            {
                entity.ToTable("utilisateurs");
                entity.HasKey(e => e.Id);
                entity.Property(e => e.Nom).IsRequired().HasMaxLength(100);
                entity.Property(e => e.Email).IsRequired().HasMaxLength(255);
                entity.HasIndex(e => e.Email).IsUnique();
            });

            modelBuilder.Entity<Post>(entity =>
            {
                entity.ToTable("posts");
                entity.HasKey(e => e.Id);

                // Relation One-to-Many
                entity.HasOne(e => e.User)
                      .WithMany(e => e.Posts)
                      .HasForeignKey(e => e.UserId)
                      .OnDelete(DeleteBehavior.Cascade);
            });
        }
    }
}

Configuration dans Program.cs (ASP.NET Core 6+) :

using Microsoft.EntityFrameworkCore;  
using MyApp.Data;  

var builder = WebApplication.CreateBuilder(args);

// Enregistrer le DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("PostgreSQL")));

var app = builder.Build();  
app.Run();  

Définition des Modèles (Entities)

using System;  
using System.Collections.Generic;  
using System.ComponentModel.DataAnnotations;  
using System.ComponentModel.DataAnnotations.Schema;  

namespace MyApp.Models
{
    [Table("utilisateurs")]
    public class User
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }

        [Required]
        [MaxLength(100)]
        public string Nom { get; set; }

        [Required]
        [MaxLength(255)]
        [EmailAddress]
        public string Email { get; set; }

        public int Age { get; set; }

        [Column("created_at")]
        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

        [Column("updated_at")]
        public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;

        // Navigation property (relation)
        public ICollection<Post> Posts { get; set; }
    }

    [Table("posts")]
    public class Post
    {
        public int Id { get; set; }

        [Required]
        [MaxLength(200)]
        public string Title { get; set; }

        public string Content { get; set; }

        public bool Published { get; set; } = false;

        // Foreign Key
        public int UserId { get; set; }

        // Navigation property
        public User User { get; set; }
    }
}

Data Annotations courantes :

Annotation Description Exemple
[Key] Clé primaire [Key] public int Id { get; set; }
[Required] NOT NULL [Required] public string Nom { get; set; }
[MaxLength] Taille max [MaxLength(100)] public string Nom { get; set; }
[Column] Nom de colonne [Column("created_at")] public DateTime CreatedAt { get; set; }
[Table] Nom de table [Table("utilisateurs")] public class User
[DatabaseGenerated] Auto-généré [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[ForeignKey] Clé étrangère [ForeignKey("UserId")] public User User { get; set; }
[Index] Index [Index(nameof(Email), IsUnique = true)]

Migrations

Les migrations permettent de créer et modifier le schéma de base de données de manière versionnée.

# Créer une migration initiale
dotnet ef migrations add InitialCreate

# Appliquer les migrations à la base de données
dotnet ef database update

# Créer une nouvelle migration (après modification du modèle)
dotnet ef migrations add AddPostsTable

# Annuler la dernière migration
dotnet ef migrations remove

# Voir les migrations appliquées
dotnet ef migrations list

# Générer un script SQL
dotnet ef migrations script

Migration programmatique (au démarrage) :

// Program.cs
var app = builder.Build();

// Appliquer les migrations au démarrage
using (var scope = app.Services.CreateScope())
{
    var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    dbContext.Database.Migrate();
}

app.Run();

CRUD avec Entity Framework Core

CREATE : Créer des enregistrements

public class UserService
{
    private readonly AppDbContext _context;

    public UserService(AppDbContext context)
    {
        _context = context;
    }

    // Créer un utilisateur
    public async Task<User> CreateUserAsync(CreateUserDto dto)
    {
        var user = new User
        {
            Nom = dto.Nom,
            Email = dto.Email,
            Age = dto.Age
        };

        _context.Users.Add(user);
        await _context.SaveChangesAsync();

        return user; // Id est automatiquement rempli
    }

    // Créer plusieurs utilisateurs (batch)
    public async Task CreateUsersAsync(List<CreateUserDto> dtos)
    {
        var users = dtos.Select(dto => new User
        {
            Nom = dto.Nom,
            Email = dto.Email,
            Age = dto.Age
        }).ToList();

        _context.Users.AddRange(users);
        await _context.SaveChangesAsync();
    }
}

// Utilisation
var service = new UserService(dbContext);  
var newUser = await service.CreateUserAsync(new CreateUserDto  
{
    Nom = "Alice",
    Email = "alice@example.com",
    Age = 30
});

Console.WriteLine($"Utilisateur créé : ID={newUser.Id}");

READ : Lire des données

using Microsoft.EntityFrameworkCore;

// Récupérer un utilisateur par ID
public async Task<User> GetUserByIdAsync(int id)
{
    return await _context.Users.FindAsync(id);
}

// Récupérer par condition
public async Task<User> GetUserByEmailAsync(string email)
{
    return await _context.Users
        .FirstOrDefaultAsync(u => u.Email == email);
}

// Récupérer tous les utilisateurs
public async Task<List<User>> GetAllUsersAsync()
{
    return await _context.Users
        .OrderBy(u => u.Nom)
        .ToListAsync();
}

// Recherche avec filtres
public async Task<List<User>> SearchUsersAsync(string namePattern, int minAge, int maxAge)
{
    return await _context.Users
        .Where(u => u.Nom.Contains(namePattern) && u.Age >= minAge && u.Age <= maxAge)
        .OrderBy(u => u.Nom)
        .ToListAsync();
}

// Requêtes LINQ avancées
public async Task<List<User>> GetAdultUsersAsync()
{
    return await _context.Users
        .Where(u => u.Age >= 18)
        .OrderByDescending(u => u.CreatedAt)
        .Take(10)
        .ToListAsync();
}

// Projection (sélectionner uniquement certains champs)
public async Task<List<object>> GetUserNamesAsync()
{
    return await _context.Users
        .Select(u => new { u.Id, u.Nom, u.Email })
        .ToListAsync<object>();
}

// Compter
public async Task<int> CountUsersAsync()
{
    return await _context.Users.CountAsync();
}

public async Task<int> CountAdultsAsync()
{
    return await _context.Users.CountAsync(u => u.Age >= 18);
}

// Pagination
public async Task<List<User>> GetUsersPaginatedAsync(int page, int pageSize)
{
    return await _context.Users
        .OrderBy(u => u.Id)
        .Skip((page - 1) * pageSize)
        .Take(pageSize)
        .ToListAsync();
}

UPDATE : Modifier des données

// Mettre à jour un utilisateur
public async Task<User> UpdateUserAsync(int id, UpdateUserDto dto)
{
    var user = await _context.Users.FindAsync(id);

    if (user == null)
        throw new Exception("Utilisateur non trouvé");

    // Modifier les propriétés
    user.Nom = dto.Nom ?? user.Nom;
    user.Email = dto.Email ?? user.Email;
    user.Age = dto.Age ?? user.Age;
    user.UpdatedAt = DateTime.UtcNow;

    // EF Core track automatiquement les changements
    await _context.SaveChangesAsync();

    return user;
}

// Mise à jour sans tracking (plus performant)
public async Task UpdateUserNoTrackingAsync(int id, UpdateUserDto dto)
{
    var user = new User
    {
        Id = id,
        Nom = dto.Nom,
        Email = dto.Email,
        Age = dto.Age ?? 0,
        UpdatedAt = DateTime.UtcNow
    };

    _context.Users.Attach(user);
    _context.Entry(user).State = EntityState.Modified;

    await _context.SaveChangesAsync();
}

// Mise à jour partielle (ExecuteUpdate - EF Core 7+)
public async Task UpdateUserAgeAsync(int id, int newAge)
{
    await _context.Users
        .Where(u => u.Id == id)
        .ExecuteUpdateAsync(setters => setters
            .SetProperty(u => u.Age, newAge)
            .SetProperty(u => u.UpdatedAt, DateTime.UtcNow));
}

// Mise à jour multiple
public async Task IncrementAllAgesAsync()
{
    await _context.Users
        .ExecuteUpdateAsync(setters => setters
            .SetProperty(u => u.Age, u => u.Age + 1));
}

DELETE : Supprimer des données

// Supprimer un utilisateur
public async Task DeleteUserAsync(int id)
{
    var user = await _context.Users.FindAsync(id);

    if (user == null)
        throw new Exception("Utilisateur non trouvé");

    _context.Users.Remove(user);
    await _context.SaveChangesAsync();
}

// Supprimer sans charger l'entité (plus performant)
public async Task DeleteUserNoTrackingAsync(int id)
{
    var user = new User { Id = id };
    _context.Users.Attach(user);
    _context.Users.Remove(user);
    await _context.SaveChangesAsync();
}

// Suppression multiple (ExecuteDelete - EF Core 7+)
public async Task DeleteUsersByAgeAsync(int maxAge)
{
    await _context.Users
        .Where(u => u.Age < maxAge)
        .ExecuteDeleteAsync();
}

// Soft Delete (suppression logique)
public async Task SoftDeleteUserAsync(int id)
{
    var user = await _context.Users.FindAsync(id);

    if (user == null)
        throw new Exception("Utilisateur non trouvé");

    user.DeletedAt = DateTime.UtcNow; // Ajouter cette propriété au modèle
    await _context.SaveChangesAsync();
}

Relations dans EF Core

One-to-Many (Un à Plusieurs)

// User a plusieurs Posts
public class User
{
    public int Id { get; set; }
    public string Nom { get; set; }

    // Navigation property
    public ICollection<Post> Posts { get; set; }
}

// Post appartient à un User
public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }

    public int UserId { get; set; }
    public User User { get; set; }
}

// Utilisation
// Créer un utilisateur avec posts
var user = new User
{
    Nom = "Alice",
    Posts = new List<Post>
    {
        new Post { Title = "Post 1", Content = "Contenu 1" },
        new Post { Title = "Post 2", Content = "Contenu 2" }
    }
};

_context.Users.Add(user);
await _context.SaveChangesAsync();

// Charger avec Include (Eager Loading)
var userWithPosts = await _context.Users
    .Include(u => u.Posts)
    .FirstOrDefaultAsync(u => u.Id == 1);

// Lazy Loading (nécessite proxies)
// Install-Package Microsoft.EntityFrameworkCore.Proxies
var user = await _context.Users.FindAsync(1);  
var posts = user.Posts; // Chargé automatiquement  

// Explicit Loading
var user = await _context.Users.FindAsync(1);  
await _context.Entry(user).Collection(u => u.Posts).LoadAsync();  

Many-to-Many (Plusieurs à Plusieurs)

// EF Core 5+ : Configuration automatique
public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }

    public ICollection<Tag> Tags { get; set; }
}

public class Tag
{
    public int Id { get; set; }
    public string Name { get; set; }

    public ICollection<Post> Posts { get; set; }
}

// Utilisation
var post = new Post
{
    Title = "Mon article",
    Tags = new List<Tag>
    {
        new Tag { Name = "csharp" },
        new Tag { Name = "postgresql" }
    }
};

_context.Posts.Add(post);
await _context.SaveChangesAsync();

// Charger avec tags
var postWithTags = await _context.Posts
    .Include(p => p.Tags)
    .FirstOrDefaultAsync(p => p.Id == 1);

// Ajouter un tag à un post existant
var post = await _context.Posts.FindAsync(1);  
var tag = await _context.Tags.FindAsync(3);  

post.Tags.Add(tag);  
await _context.SaveChangesAsync();  

One-to-One (Un à Un)

public class User
{
    public int Id { get; set; }
    public string Nom { get; set; }

    public Profile Profile { get; set; }
}

public class Profile
{
    public int Id { get; set; }
    public string Bio { get; set; }

    public int UserId { get; set; }
    public User User { get; set; }
}

// Configuration dans OnModelCreating
modelBuilder.Entity<User>()
    .HasOne(u => u.Profile)
    .WithOne(p => p.User)
    .HasForeignKey<Profile>(p => p.UserId);

Transactions avec EF Core

// Transaction automatique (SaveChanges)
// Toutes les modifications dans SaveChanges sont atomiques
await _context.Users.AddAsync(user1);  
await _context.Users.AddAsync(user2);  
await _context.SaveChangesAsync(); // Transaction automatique  

// Transaction explicite
public async Task TransferMoneyAsync(int fromAccountId, int toAccountId, decimal amount)
{
    using var transaction = await _context.Database.BeginTransactionAsync();

    try
    {
        // Opération 1
        var fromAccount = await _context.Accounts.FindAsync(fromAccountId);
        fromAccount.Balance -= amount;

        // Opération 2
        var toAccount = await _context.Accounts.FindAsync(toAccountId);
        toAccount.Balance += amount;

        await _context.SaveChangesAsync();
        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}

// Transaction avec scope
public async Task ComplexOperationAsync()
{
    using var transaction = await _context.Database.BeginTransactionAsync();

    try
    {
        await _context.Users.AddAsync(new User { Nom = "Alice" });
        await _context.SaveChangesAsync();

        await _context.Posts.AddAsync(new Post { Title = "Post 1", UserId = 1 });
        await _context.SaveChangesAsync();

        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}

Requêtes Brutes (Raw SQL)

// Requête SQL brute avec FromSqlRaw
var users = await _context.Users
    .FromSqlRaw("SELECT * FROM utilisateurs WHERE age > {0}", 18)
    .ToListAsync();

// Requête SQL interpolée (recommandé)
int minAge = 18;  
var users = await _context.Users  
    .FromSqlInterpolated($"SELECT * FROM utilisateurs WHERE age > {minAge}")
    .ToListAsync();

// Exécuter SQL sans résultat
await _context.Database.ExecuteSqlRawAsync(
    "UPDATE utilisateurs SET age = age + 1 WHERE age < {0}", 50);

// Procédures stockées
var users = await _context.Users
    .FromSqlRaw("CALL get_users_by_age(@p0)", 18)
    .ToListAsync();

Exemple Complet : API ASP.NET Core avec EF Core

// Program.cs
using Microsoft.EntityFrameworkCore;  
using MyApp.Data;  
using MyApp.Services;  

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();  
builder.Services.AddDbContext<AppDbContext>(options =>  
    options.UseNpgsql(builder.Configuration.GetConnectionString("PostgreSQL")));

builder.Services.AddScoped<UserService>();

var app = builder.Build();

// Appliquer les migrations
using (var scope = app.Services.CreateScope())
{
    var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    context.Database.Migrate();
}

app.MapControllers();  
app.Run();  

// Services/UserService.cs
public class UserService
{
    private readonly AppDbContext _context;

    public UserService(AppDbContext context)
    {
        _context = context;
    }

    public async Task<List<User>> GetAllUsersAsync()
    {
        return await _context.Users.OrderBy(u => u.Nom).ToListAsync();
    }

    public async Task<User> GetUserByIdAsync(int id)
    {
        return await _context.Users.FindAsync(id);
    }

    public async Task<User> CreateUserAsync(CreateUserDto dto)
    {
        var user = new User
        {
            Nom = dto.Nom,
            Email = dto.Email,
            Age = dto.Age
        };

        _context.Users.Add(user);
        await _context.SaveChangesAsync();

        return user;
    }

    public async Task<User> UpdateUserAsync(int id, UpdateUserDto dto)
    {
        var user = await _context.Users.FindAsync(id);

        if (user == null)
            return null;

        user.Nom = dto.Nom ?? user.Nom;
        user.Email = dto.Email ?? user.Email;
        user.Age = dto.Age ?? user.Age;
        user.UpdatedAt = DateTime.UtcNow;

        await _context.SaveChangesAsync();

        return user;
    }

    public async Task<bool> DeleteUserAsync(int id)
    {
        var user = await _context.Users.FindAsync(id);

        if (user == null)
            return false;

        _context.Users.Remove(user);
        await _context.SaveChangesAsync();

        return true;
    }
}

// Controllers/UsersController.cs
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly UserService _service;

    public UsersController(UserService service)
    {
        _service = service;
    }

    [HttpGet]
    public async Task<ActionResult<List<User>>> GetUsers()
    {
        var users = await _service.GetAllUsersAsync();
        return Ok(users);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<User>> GetUser(int id)
    {
        var user = await _service.GetUserByIdAsync(id);

        if (user == null)
            return NotFound(new { error = "Utilisateur non trouvé" });

        return Ok(user);
    }

    [HttpPost]
    public async Task<ActionResult<User>> CreateUser([FromBody] CreateUserDto dto)
    {
        try
        {
            var user = await _service.CreateUserAsync(dto);
            return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
        }
        catch (DbUpdateException ex) when (ex.InnerException is PostgresException pgEx && pgEx.SqlState == "23505")
        {
            return Conflict(new { error = "Email déjà utilisé" });
        }
    }

    [HttpPut("{id}")]
    public async Task<ActionResult<User>> UpdateUser(int id, [FromBody] UpdateUserDto dto)
    {
        var user = await _service.UpdateUserAsync(id, dto);

        if (user == null)
            return NotFound(new { error = "Utilisateur non trouvé" });

        return Ok(user);
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteUser(int id)
    {
        var deleted = await _service.DeleteUserAsync(id);

        if (!deleted)
            return NotFound(new { error = "Utilisateur non trouvé" });

        return NoContent();
    }
}

Comparaison : Npgsql vs Entity Framework Core

Performance

Benchmark indicatif (10000 requêtes SELECT) :

  • Npgsql : ~900ms
  • EF Core : ~1300ms

Différence : EF Core est ~30-45% plus lent que Npgsql brut, mais reste très performant pour la plupart des applications.

Complexité du Code

Npgsql :

// Plus verbeux mais contrôle total
using var conn = new NpgsqlConnection(connectionString);  
conn.Open();  

string sql = "SELECT * FROM users WHERE age > @age";  
using var cmd = new NpgsqlCommand(sql, conn);  
cmd.Parameters.AddWithValue("age", 18);  

using var reader = cmd.ExecuteReader();  
var users = new List<User>();  

while (reader.Read())
{
    users.Add(new User
    {
        Id = reader.GetInt32(0),
        Nom = reader.GetString(1)
    });
}

EF Core :

// Concis et fortement typé
var users = await _context.Users
    .Where(u => u.Age > 18)
    .ToListAsync();

Quand Utiliser Chaque Approche

Npgsql (ADO.NET) ✅

Choisir Npgsql si :

  • Besoin de performances maximales
  • Requêtes SQL très complexes (CTEs avancés, optimisations spécifiques)
  • Microservices ultra-légers
  • Contrôle total sur le SQL généré
  • Batch processing massif
  • Pas besoin de Change Tracking

Exemples de cas d'usage :

  • API haute performance (>20k req/s)
  • ETL et data pipelines
  • Systèmes analytiques temps réel
  • Migration de données massives

Entity Framework Core ✅

Choisir EF Core si :

  • Productivité et développement rapide
  • Application CRUD standard
  • Besoin de migrations automatiques
  • Relations complexes entre entités
  • Change Tracking utile
  • Équipe .NET standard
  • Prototypage et MVP

Exemples de cas d'usage :

  • API REST ASP.NET Core
  • Applications web e-commerce
  • Systèmes de gestion interne
  • SaaS standard

Approche Hybride

Vous pouvez combiner les deux dans la même application :

// Configuration
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(connectionString));

builder.Services.AddSingleton<NpgsqlConnection>(sp =>
    new NpgsqlConnection(connectionString));

// Dans un service
public class ReportService
{
    private readonly AppDbContext _context;
    private readonly NpgsqlConnection _npgsqlConn;

    public ReportService(AppDbContext context, NpgsqlConnection npgsqlConn)
    {
        _context = context;
        _npgsqlConn = npgsqlConn;
    }

    // Utiliser EF Core pour CRUD simple
    public async Task<User> GetUserAsync(int id)
    {
        return await _context.Users.FindAsync(id);
    }

    // Utiliser Npgsql pour requêtes complexes
    public async Task<List<MonthlyStats>> GetMonthlyStatsAsync()
    {
        await _npgsqlConn.OpenAsync();

        string sql = @"
            WITH monthly_stats AS (
                SELECT DATE_TRUNC('month', created_at) as month,
                       COUNT(*) as user_count,
                       AVG(age) as avg_age
                FROM users
                GROUP BY month
            )
            SELECT * FROM monthly_stats
            ORDER BY month DESC
            LIMIT 12";

        using var cmd = new NpgsqlCommand(sql, _npgsqlConn);
        using var reader = await cmd.ExecuteReaderAsync();

        var stats = new List<MonthlyStats>();
        while (await reader.ReadAsync())
        {
            stats.Add(new MonthlyStats
            {
                Month = reader.GetDateTime(0),
                UserCount = reader.GetInt32(1),
                AvgAge = reader.GetDouble(2)
            });
        }

        return stats;
    }
}

Bonnes Pratiques

1. Toujours Utiliser le Pooling de Connexions

Bon : Le pooling est activé par défaut dans Npgsql

// Configuration recommandée
"Pooling=true;Maximum Pool Size=100;Minimum Pool Size=10"

2. Utiliser Async/Await

// ✅ Bon : Async pour éviter de bloquer les threads
var users = await _context.Users.ToListAsync();

// ❌ Éviter : Synchrone bloquant
var users = _context.Users.ToList();

3. Disposer Correctement les Ressources

// ✅ Bon : using statement
using var conn = new NpgsqlConnection(connectionString);

// ✅ Bon : using declaration (C# 8+)
using var context = new AppDbContext(options);

// ❌ Éviter : Oublier de disposer
var conn = new NpgsqlConnection(connectionString);
// ... oubli de conn.Dispose()

4. Utiliser AsNoTracking pour les Lectures Seules

// ✅ Bon : Meilleure performance pour lecture seule
var users = await _context.Users
    .AsNoTracking()
    .ToListAsync();

// ❌ Éviter : Tracking inutile pour lecture
var users = await _context.Users.ToListAsync();

5. Projections pour Sélectionner Uniquement les Données Nécessaires

// ✅ Bon : Projection
var userNames = await _context.Users
    .Select(u => new { u.Id, u.Nom })
    .ToListAsync();

// ❌ Éviter : Charger toutes les colonnes
var users = await _context.Users.ToListAsync();  
var names = users.Select(u => u.Nom);  

6. Gestion des Erreurs PostgreSQL

// ✅ Bon : Gestion spécifique
try
{
    await _context.SaveChangesAsync();
}
catch (DbUpdateException ex) when (ex.InnerException is PostgresException pgEx)
{
    switch (pgEx.SqlState)
    {
        case "23505":
            throw new DuplicateKeyException("Valeur déjà existante");
        case "23503":
            throw new ForeignKeyException("Référence invalide");
        default:
            throw;
    }
}

7. Utiliser des Indexes

// Configuration des index dans OnModelCreating
modelBuilder.Entity<User>(entity =>
{
    entity.HasIndex(u => u.Email).IsUnique();
    entity.HasIndex(u => u.Age);
    entity.HasIndex(u => new { u.Nom, u.Age }); // Index composite
});

8. Pagination Efficace

// ✅ Bon : Pagination avec Skip/Take
var users = await _context.Users
    .OrderBy(u => u.Id)
    .Skip((page - 1) * pageSize)
    .Take(pageSize)
    .ToListAsync();

Ressources et Documentation

Npgsql

Entity Framework Core

ASP.NET Core


Résumé des Points Clés

Npgsql (ADO.NET)

  • Provider natif PostgreSQL haute performance
  • ✅ Pooling de connexions activé par défaut
  • ✅ Support complet des types PostgreSQL
  • ✅ API ADO.NET standard .NET
  • Contrôle total sur le SQL
  • ✅ Idéal pour performances critiques

Entity Framework Core

  • ORM moderne de Microsoft
  • LINQ : requêtes fortement typées en C#
  • Migrations automatiques (Code-First)
  • Change Tracking automatique
  • ✅ Relations et Navigation Properties
  • Productivité élevée
  • ✅ Idéal pour développement rapide et maintenabilité

Choix Final

Pour 80% des projets .NET : Entity Framework Core (productivité, maintenabilité)
Pour performances critiques : Npgsql (contrôle, vitesse)
Approche hybride : EF Core pour CRUD + Npgsql pour requêtes complexes


Prochaine étape : Explorez les patterns de conception (Repository, Unit of Work) et les architectures modernes (Clean Architecture, CQRS) pour structurer efficacement vos applications .NET avec PostgreSQL.

⏭️ Gestion des connexions dans les applications