Tích hợp PostgreSQL với C# (Npgsql)
1. Tổng quan
Npgsql là .NET Data Provider chính thức cho PostgreSQL — tương đương SqlClient của SQL Server. Npgsql implement đầy đủ ADO.NET, hỗ trợ async, connection pooling, và PostgreSQL-specific types (JSONB, array, UUID, range...).
Cài đặt
dotnet add package Npgsql
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
Npgsql— driver chính để kết nối và thao tác với PostgreSQL.Microsoft.Extensions.Configuration+Configuration.Json— đọc connection string từappsettings.json.
"Npgsql — .NET Data Provider chính thức cho PostgreSQL. Tương tự
SqlClientcho SQL Server. Hỗ trợ đầy đủ ADO.NET, async/await, và tất cả PostgreSQL-specific types như JSONB, array, UUID, INET..."
2. Kết nối PostgreSQL từ C#
Connection string
File appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=elearning;User Id=ed;Password=YourPassword;"
}
}
Server=host;Port=5432;Database=dbname;User Id=user;Password=pwd;
Data Source / Initial Catalog như SQL Server.ConfigurationHelper — đọc connection string
using Microsoft.Extensions.Configuration;
public static class ConfigurationHelper
{
private static readonly IConfiguration _configuration;
static ConfigurationHelper()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
_configuration = builder.Build();
}
public static string GetConnectionString(string name)
{
var connectionString = _configuration.GetConnectionString(name);
if (string.IsNullOrEmpty(connectionString))
{
throw new InvalidOperationException(
$"The connection string '{name}' has not been initialized.");
}
return connectionString;
}
}
Kết nối cơ bản — NpgsqlConnection
using Npgsql;
string connectionString = ConfigurationHelper.GetConnectionString("DefaultConnection");
await using var conn = new NpgsqlConnection(connectionString);
await conn.OpenAsync();
Console.WriteLine($"The PostgreSQL version: {conn.PostgreSqlVersion}");
await using đảm bảo connection được dispose (trả về pool) ngay khi ra khỏi scope. Không đóng connection → connection leak → crash app.NpgsqlDataSource (Npgsql 7.0+) — khuyên dùng
string connectionString = ConfigurationHelper.GetConnectionString("DefaultConnection");
await using var dataSource = NpgsqlDataSource.Create(connectionString);
NpgsqlDataSource là factory quản lý connection tự động, thread-safe. Nên tạo một instance duy nhất cho toàn bộ application.
// Lấy connection từ data source khi cần thao tác thủ công
await using var conn = await dataSource.OpenConnectionAsync();
3. CRUD — thao tác cơ bản
Tạo bảng (CREATE TABLE)
await using var dataSource = NpgsqlDataSource.Create(connectionString);
var sql = @"CREATE TABLE IF NOT EXISTS students (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
registration_date DATE NOT NULL
)";
await using var cmd = dataSource.CreateCommand(sql);
await cmd.ExecuteNonQueryAsync();
Console.WriteLine("Table 'students' created successfully.");
Thêm dữ liệu (INSERT)
var sql = @"INSERT INTO students (first_name, last_name, email, registration_date)
VALUES (@first_name, @last_name, @email, @registration_date)
RETURNING id";
await using var cmd = dataSource.CreateCommand(sql);
cmd.Parameters.AddWithValue("@first_name", "Nguyen");
cmd.Parameters.AddWithValue("@last_name", "Van A");
cmd.Parameters.AddWithValue("@email", "nguyenvana@example.com");
cmd.Parameters.AddWithValue("@registration_date", new DateOnly(2026, 7, 8));
var newId = await cmd.ExecuteScalarAsync();
Console.WriteLine($"Inserted student with id: {newId}");
RETURNING id để lấy ID vừa insert — không cần SELECT LASTVAL() hay SCOPE_IDENTITY() như SQL Server.@param), không bao giờ nối chuỗi trực tiếp. Npgsql tự escape và chọn đúng kiểu dữ liệu PostgreSQL.Truy vấn dữ liệu (SELECT)
var sql = "SELECT id, first_name, last_name, email, registration_date FROM students";
await using var cmd = dataSource.CreateCommand(sql);
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var id = reader.GetInt32(0);
var firstName = reader.GetString(1);
var lastName = reader.GetString(2);
var email = reader.GetString(3);
var regDate = reader.GetFieldValue<DateOnly>(4);
Console.WriteLine($"{id}: {firstName} {lastName} ({email}) - {regDate}");
}
Đọc theo tên cột (an toàn hơn):
var id = reader.GetInt32("id");
var firstName = reader.GetString("first_name");
NpgsqlDataReader hỗ trợ cả GetInt32(ordinal) theo index lẫn GetString("column_name") theo tên. Đọc theo tên an toàn hơn khi thay đổi thứ tự cột SELECT.Cập nhật dữ liệu (UPDATE)
var sql = @"UPDATE students
SET email = @email
WHERE id = @id";
await using var cmd = dataSource.CreateCommand(sql);
cmd.Parameters.AddWithValue("@email", "newemail@example.com");
cmd.Parameters.AddWithValue("@id", 1);
var rowsAffected = await cmd.ExecuteNonQueryAsync();
Console.WriteLine($"Updated {rowsAffected} row(s).");
Xoá dữ liệu (DELETE)
var sql = "DELETE FROM students WHERE id = @id";
await using var cmd = dataSource.CreateCommand(sql);
cmd.Parameters.AddWithValue("@id", 1);
var rowsAffected = await cmd.ExecuteNonQueryAsync();
Console.WriteLine($"Deleted {rowsAffected} row(s).");
4. Import dữ liệu từ CSV
Dùng package CsvHelper để parse CSV, sau đó INSERT từng dòng:
dotnet add package CsvHelper
using CsvHelper;
using System.Globalization;
using Npgsql;
public record Student(string FirstName, string LastName, string Email, DateOnly RegistrationDate);
public static IEnumerable<Student> ReadStudentsFromCSV(string filePath)
{
using var reader = new StreamReader(filePath);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
csv.Read();
csv.ReadHeader();
while (csv.Read())
{
var firstName = csv.GetField<string>("Firstname");
var lastName = csv.GetField<string>("Lastname");
var email = csv.GetField<string>("Email");
var registrationDate = csv.GetField<DateOnly>("RegistrationDate");
yield return new Student(firstName, lastName, email, registrationDate);
}
}
public static async Task Main()
{
var csvFilePath = @"C:\data\students.csv";
var sql = @"INSERT INTO students(first_name, last_name, email, registration_date)
VALUES(@first_name, @last_name, @email, @registration_date)";
string connectionString = ConfigurationHelper.GetConnectionString("DefaultConnection");
await using var dataSource = NpgsqlDataSource.Create(connectionString);
foreach (var student in ReadStudentsFromCSV(csvFilePath))
{
await using var cmd = dataSource.CreateCommand(sql);
cmd.Parameters.AddWithValue("@first_name", student.FirstName);
cmd.Parameters.AddWithValue("@last_name", student.LastName);
cmd.Parameters.AddWithValue("@email", student.Email);
cmd.Parameters.AddWithValue("@registration_date", student.RegistrationDate);
await cmd.ExecuteNonQueryAsync();
}
}
yield return trong ReadStudentsFromCSV giúp stream từng dòng CSV mà không load toàn bộ file vào memory. Với file CSV lớn (hàng trăm MB), đây là pattern bắt buộc.5. Transaction trong C#
await using var conn = await dataSource.OpenConnectionAsync();
// Bắt đầu transaction
await using var tx = await conn.BeginTransactionAsync();
try
{
// Thao tác 1: Enroll student vào course
var sql1 = @"INSERT INTO enrollments (student_id, course_id, enrolled_date)
VALUES (@student_id, @course_id, @enrolled_date)";
await using var cmd1 = new NpgsqlCommand(sql1, conn, tx);
cmd1.Parameters.AddWithValue("@student_id", 2);
cmd1.Parameters.AddWithValue("@course_id", 1);
cmd1.Parameters.AddWithValue("@enrolled_date", new DateOnly(2026, 7, 8));
await cmd1.ExecuteNonQueryAsync();
// Thao tác 2: Tạo invoice
var sql2 = @"INSERT INTO invoices (student_id, course_id, amount, tax, invoice_date)
VALUES (@student_id, @course_id, @amount, @tax, @invoice_date)";
await using var cmd2 = new NpgsqlCommand(sql2, conn, tx);
cmd2.Parameters.AddWithValue("@student_id", 2);
cmd2.Parameters.AddWithValue("@course_id", 1);
cmd2.Parameters.AddWithValue("@amount", 99.5);
cmd2.Parameters.AddWithValue("@tax", 0.05);
cmd2.Parameters.AddWithValue("@invoice_date", new DateOnly(2026, 7, 8));
await cmd2.ExecuteNonQueryAsync();
// Commit — lưu tất cả thay đổi
await tx.CommitAsync();
Console.WriteLine("Transaction committed.");
}
catch (NpgsqlException ex)
{
Console.WriteLine($"Error: {ex.Message}");
// Rollback — huỷ tất cả thay đổi
await tx.RollbackAsync();
Console.WriteLine("Transaction rolled back.");
}
NpgsqlCommand phải được truyền cả connection và transaction object:new NpgsqlCommand(sql, conn, tx)
tx → command chạy ngoài transaction → không rollback được.Các bước chuẩn:
BeginTransactionAsync()— bắt đầu transaction.- Tạo
NpgsqlCommand(sql, conn, tx)— mỗi command gắn với transaction. CommitAsync()— lưu toàn bộ thay đổi.- Nếu lỗi →
RollbackAsync()— huỷ tất cả.
6. Gọi PostgreSQL Function
PostgreSQL function trả về giá trị, gọi qua SELECT function_name($1, $2):
-- Tạo function trong PostgreSQL
CREATE OR REPLACE FUNCTION get_student_count(begin_date DATE, end_date DATE)
RETURNS INT
LANGUAGE plpgsql
AS $$
DECLARE
student_count INTEGER;
BEGIN
SELECT COUNT(*) INTO student_count
FROM students
WHERE registration_date BETWEEN begin_date AND end_date;
RETURN student_count;
END;
$$;
// Gọi function từ C#
var beginDate = new DateOnly(2026, 5, 10);
var endDate = new DateOnly(2026, 5, 15);
await using var dataSource = NpgsqlDataSource.Create(connectionString);
await using var cmd = dataSource.CreateCommand("SELECT get_student_count($1, $2)");
cmd.Parameters.AddWithValue(beginDate);
cmd.Parameters.AddWithValue(endDate);
await using var reader = await cmd.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
var studentCount = reader.GetInt32(0);
Console.WriteLine($"Students registered between {beginDate} and {endDate}: {studentCount}");
}
- Function: gọi bằng
SELECT, trả về giá trị. DùngExecuteReaderAsync(). - Procedure (PG 11+): gọi bằng
CALL, không trả về giá trị, có thể COMMIT/ROLLBACK trong thân. DùngExecuteNonQueryAsync().
7. Gọi PostgreSQL Stored Procedure
-- Tạo stored procedure trong PostgreSQL
CREATE OR REPLACE PROCEDURE enroll_student(
p_student_id INTEGER,
p_course_id INTEGER,
p_amount DOUBLE PRECISION,
p_tax DOUBLE PRECISION,
p_invoice_date DATE
)
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO enrollments (student_id, course_id, enrolled_date)
VALUES (p_student_id, p_course_id, p_invoice_date);
INSERT INTO invoices (student_id, course_id, amount, tax, invoice_date)
VALUES (p_student_id, p_course_id, p_amount, p_tax, p_invoice_date);
END;
$$;
// Gọi stored procedure từ C#
var studentId = 2;
var courseId = 2;
var amount = 49.99;
var tax = 0.05;
var invoiceDate = new DateOnly(2026, 7, 8);
await using var dataSource = NpgsqlDataSource.Create(connectionString);
// Gọi procedure bằng CALL với positional parameters $1, $2...
await using var cmd = dataSource.CreateCommand("CALL enroll_student($1, $2, $3, $4, $5)");
cmd.Parameters.AddWithValue(studentId);
cmd.Parameters.AddWithValue(courseId);
cmd.Parameters.AddWithValue(amount);
cmd.Parameters.AddWithValue(tax);
cmd.Parameters.AddWithValue(invoiceDate);
await cmd.ExecuteNonQueryAsync();
Console.WriteLine("Stored procedure executed successfully.");
- Postgres dùng positional parameters (
$1,$2...) thay vì named parameters (@name) trongCALL. - Postgres dùng
CALL procedure_name(...)thay vìEXEC procedure_namenhư SQL Server. - Không dùng
CommandType.StoredProcedure— luôn là text command (CommandType.Textmặc định).
8. Pattern tổ chức code khuyên dùng
Dependency Injection — đăng ký NpgsqlDataSource
// Program.cs — .NET 8+
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<NpgsqlDataSource>(sp =>
{
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
return NpgsqlDataSource.Create(connectionString);
});
// Dùng trong controller / service
public class StudentRepository
{
private readonly NpgsqlDataSource _dataSource;
public StudentRepository(NpgsqlDataSource dataSource)
{
_dataSource = dataSource;
}
public async Task<Student?> GetByIdAsync(int id)
{
await using var cmd = _dataSource.CreateCommand(
"SELECT id, first_name, last_name, email FROM students WHERE id = $1");
cmd.Parameters.AddWithValue(id);
await using var reader = await cmd.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return new Student(
reader.GetInt32(0),
reader.GetString(1),
reader.GetString(2),
reader.GetString(3)
);
}
return null;
}
}
Dapper + Npgsql (cho team thích micro-ORM)
dotnet add package Dapper
using Dapper;
// Dapper dùng trực tiếp NpgsqlConnection
await using var conn = new NpgsqlConnection(connectionString);
// Query — tự map sang object
var students = await conn.QueryAsync<Student>(
"SELECT * FROM students WHERE registration_date >= @date",
new { date = new DateOnly(2026, 1, 1) }
);
// Execute — INSERT/UPDATE/DELETE
var rowsAffected = await conn.ExecuteAsync(
"UPDATE students SET email = @Email WHERE id = @Id",
new { Email = "new@example.com", Id = 1 }
);
Entity Framework Core + Npgsql
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
// DbContext
public class AppDbContext : DbContext
{
public DbSet<Student> Students { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseNpgsql(connectionString);
}
}
| Library | Khi nào |
|---|---|
| Npgsql thô (ADO.NET) | Performance critical, cần control hoàn toàn SQL |
| Dapper | Muốn nhanh hơn EF, vẫn map object, thích viết SQL tay |
| EF Core + Npgsql | Code-first, migration, LINQ, team quen EF |
9. Kiểu dữ liệu PostgreSQL mapping với C#
| PostgreSQL | C# |
|---|---|
INTEGER / INT | int |
BIGINT | long |
SMALLINT | short |
NUMERIC / DECIMAL | decimal |
REAL | float |
DOUBLE PRECISION | double |
BOOLEAN | bool |
VARCHAR(n) / TEXT | string |
DATE | DateOnly (.NET 6+) |
TIME | TimeOnly (.NET 6+) |
TIMESTAMP / TIMESTAMPTZ | DateTime / DateTimeOffset |
UUID | Guid |
JSON / JSONB | string (hoặc JsonDocument / custom type) |
BYTEA | byte[] |
INT[] | int[] / List<int> |
TEXT[] | string[] / List<string> |
10. Best practices
- ✅ Dùng
NpgsqlDataSource(singleton, thread-safe) — không newNpgsqlConnectionthủ công mỗi lần. - ✅ Luôn
await usingcho connection, command, reader — tránh leak resource. - ✅ Parameterized query 100% — không bao giờ nối chuỗi SQL.
- ✅ Dùng
RETURNINGclause để lấy giá trị sau INSERT/UPDATE. - ✅ Bắt
NpgsqlExceptionriêng — xử lý lỗi PostgreSQL cụ thể. - ✅ Transaction có try-catch + rollback rõ ràng.
- ✅ Pooling connection (bật mặc định trong Npgsql) — không cần tự quản lý.
- ✅ Với batch insert lớn, dùng
COPY(binary protocol) thay vì INSERT từng dòng — nhanh hơn 10-100x. - ✅ Set
CommandTimeoutcho query dài — default 30s có thể không đủ. - ✅ Dùng
DateOnly/TimeOnly(.NET 6+) thay vìDateTimecho cột DATE / TIME.
5. Q và A phỏng vấn
70+ câu hỏi phỏng vấn Postgres cho dev backend / fullstack — đáp án ngắn, có dẫn chiếu Postgres 18 & 19.
1. Giới thiệu và Cài đặt
MySQL — RDBMS open-source phổ biến nhất. Bản 9.7 LTS mới (04/2026, đến ~2034) với Hypergraph Optimizer vào Community, PBKDF2+SHA512, JavaScript in-database.