🔝 Retour au Sommaire
.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 :
- Npgsql : Provider ADO.NET natif pour PostgreSQL, accès bas niveau
- 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.
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).
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.
| 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
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
Install-Package Npgsqldotnet add package Npgsql<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Npgsql" Version="8.0.1" />
</ItemGroup>
</Project>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"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);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; }
}
}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();
}
}// 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;
}// 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é");
}
}// 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();
}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;
}
}// 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);// VARCHAR, TEXT
cmd.Parameters.AddWithValue("nom", "Alice");
cmd.Parameters.AddWithValue("description", "Longue description..."); 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);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"); // 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"); 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"));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}");
}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 });
}
}
}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
# 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-efLe 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(); 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)] |
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 scriptMigration 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();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}");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();
}// 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));
}// 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();
}// 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(); // 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(); 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);// 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ê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();// 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();
}
}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.
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();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
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
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;
}
}✅ Bon : Le pooling est activé par défaut dans Npgsql
// Configuration recommandée
"Pooling=true;Maximum Pool Size=100;Minimum Pool Size=10"// ✅ Bon : Async pour éviter de bloquer les threads
var users = await _context.Users.ToListAsync();
// ❌ Éviter : Synchrone bloquant
var users = _context.Users.ToList();// ✅ 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()// ✅ Bon : Meilleure performance pour lecture seule
var users = await _context.Users
.AsNoTracking()
.ToListAsync();
// ❌ Éviter : Tracking inutile pour lecture
var users = await _context.Users.ToListAsync();// ✅ 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); // ✅ 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;
}
}// 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
});// ✅ Bon : Pagination avec Skip/Take
var users = await _context.Users
.OrderBy(u => u.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();- Documentation officielle : https://www.npgsql.org/doc/
- GitHub : https://github.com/npgsql/npgsql
- NuGet : https://www.nuget.org/packages/Npgsql
- Documentation officielle : https://docs.microsoft.com/en-us/ef/core/
- EF Core avec PostgreSQL : https://www.npgsql.org/efcore/
- GitHub : https://github.com/dotnet/efcore
- Documentation : https://docs.microsoft.com/en-us/aspnet/core/
- Tutorials : https://docs.microsoft.com/en-us/aspnet/core/tutorials/
- ✅ 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
- ✅ 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é
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.