PostgreSQL

Tích hợp PostgreSQL với C# (Npgsql)

Hướng dẫn toàn diện kết nối PostgreSQL từ C# dùng Npgsql — CRUD, transaction, gọi function/stored procedure, import CSV. Dành cho dev .NET.

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.
Câu PV:"Trong .NET, dùng thư viện gì để kết nối PostgreSQL?"

"Npgsql — .NET Data Provider chính thức cho PostgreSQL. Tương tự SqlClient cho 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;"
  }
}
Format connection string PostgreSQL:
Server=host;Port=5432;Database=dbname;User Id=user;Password=pwd;
Không dùng 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}");
Pattern quan trọng: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}");
Dùng RETURNING id để lấy ID vừa insert — không cần SELECT LASTVAL() hay SCOPE_IDENTITY() như SQL Server.
Tránh SQL injection: luôn dùng parameterized query (@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();
    }
}
Pattern quan trọng: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.");
}
Lưu ý quan trọng: Khi dùng transaction, NpgsqlCommand phải được truyền cả connectiontransaction object:
new NpgsqlCommand(sql, conn, tx)
Thiếu tx → command chạy ngoài transaction → không rollback được.

Các bước chuẩn:

  1. BeginTransactionAsync() — bắt đầu transaction.
  2. Tạo NpgsqlCommand(sql, conn, tx) — mỗi command gắn với transaction.
  3. CommitAsync() — lưu toàn bộ thay đổi.
  4. 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 vs Stored Procedure trong PostgreSQL:
  • Function: gọi bằng SELECT, trả về giá trị. Dùng ExecuteReaderAsync().
  • Procedure (PG 11+): gọi bằng CALL, không trả về giá trị, có thể COMMIT/ROLLBACK trong thân. Dùng ExecuteNonQueryAsync().

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.");
Khác biệt PostgreSQL vs SQL Server:
  • Postgres dùng positional parameters ($1, $2...) thay vì named parameters (@name) trong CALL.
  • Postgres dùng CALL procedure_name(...) thay vì EXEC procedure_name như SQL Server.
  • Không dùng CommandType.StoredProcedure — luôn là text command (CommandType.Text mặ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);
    }
}
Chọn library nào?
LibraryKhi nào
Npgsql thô (ADO.NET)Performance critical, cần control hoàn toàn SQL
DapperMuốn nhanh hơn EF, vẫn map object, thích viết SQL tay
EF Core + NpgsqlCode-first, migration, LINQ, team quen EF

9. Kiểu dữ liệu PostgreSQL mapping với C#

PostgreSQLC#
INTEGER / INTint
BIGINTlong
SMALLINTshort
NUMERIC / DECIMALdecimal
REALfloat
DOUBLE PRECISIONdouble
BOOLEANbool
VARCHAR(n) / TEXTstring
DATEDateOnly (.NET 6+)
TIMETimeOnly (.NET 6+)
TIMESTAMP / TIMESTAMPTZDateTime / DateTimeOffset
UUIDGuid
JSON / JSONBstring (hoặc JsonDocument / custom type)
BYTEAbyte[]
INT[]int[] / List<int>
TEXT[]string[] / List<string>

10. Best practices

  1. ✅ Dùng NpgsqlDataSource (singleton, thread-safe) — không new NpgsqlConnection thủ công mỗi lần.
  2. ✅ Luôn await using cho connection, command, reader — tránh leak resource.
  3. ✅ Parameterized query 100% — không bao giờ nối chuỗi SQL.
  4. ✅ Dùng RETURNING clause để lấy giá trị sau INSERT/UPDATE.
  5. ✅ Bắt NpgsqlException riêng — xử lý lỗi PostgreSQL cụ thể.
  6. ✅ Transaction có try-catch + rollback rõ ràng.
  7. ✅ Pooling connection (bật mặc định trong Npgsql) — không cần tự quản lý.
  8. ✅ Với batch insert lớn, dùng COPY (binary protocol) thay vì INSERT từng dòng — nhanh hơn 10-100x.
  9. ✅ Set CommandTimeout cho query dài — default 30s có thể không đủ.
  10. ✅ Dùng DateOnly / TimeOnly (.NET 6+) thay vì DateTime cho cột DATE / TIME.

© 2026 .NET Fresher Guide. All rights reserved.

Về Trang Web

Hướng dẫn toàn diện để chuẩn bị phỏng vấn vị trí .NET Fresher với nội dung từ lý thuyết đến thực hành.