0
0
Lập trình
Hưng Nguyễn Xuân 1
Hưng Nguyễn Xuân 1xuanhungptithcm

Làm chủ Interceptor cơ sở dữ liệu trong .NET Core Web API

Đăng vào 5 ngày trước

• 8 phút đọc

1. Giới thiệu

Khi xây dựng các ứng dụng Web API hiện đại với .NET, các thao tác với cơ sở dữ liệu là trung tâm của mọi thứ. Nhưng bạn đã bao giờ tự hỏi:

  • Các truy vấn SQL nào đang được thực thi?
  • Làm cách nào để tôi có thể ghi lại hoặc đo lường hiệu suất truy vấn?
  • Tôi có thể ngăn chặn các lệnh nguy hiểm trước khi chúng tác động đến cơ sở dữ liệu không?

Câu trả lời nằm ở việc sử dụng interceptor cơ sở dữ liệu.

Trong bài viết này, chúng ta sẽ khám phá việc chặn truy vấn cơ sở dữ liệu trong Entity Framework Core (EF Core) trên các phiên bản .NET 6, 7, 8 và 9.
Chúng ta sẽ bắt đầu từ cấp độ cơ bản (chỉ ghi lại các truy vấn) và tiến tới các tình huống nâng cao như giám sát hiệu suất, kiểm toán và chặn các câu lệnh SQL nguy hiểm.


2. Điều kiện tiên quyết

  • Một dự án Web API hoạt động với .NET 6, 7, 8 hoặc 9
  • Kiến thức cơ bản về Entity Framework Core

Các gói NuGet EF Core đã cài đặt:

Copy
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer

3. Cấp độ cơ bản: Thiết lập Web API với EF Core

Hãy bắt đầu với một thực thể User đơn giản và DbContext.

csharp Copy
public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}
csharp Copy
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<User> Users { get; set; }
}

Trong Program.cs, đăng ký context:

csharp Copy
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

var app = builder.Build();

app.MapGet("/users", async (AppDbContext db) => await db.Users.ToListAsync());

app.Run();

4. Tạo một interceptor ghi log đơn giản

Một interceptor cho phép chúng ta can thiệp vào các lệnh SQL trước khi chúng được gửi đến cơ sở dữ liệu.

csharp Copy
using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Data.Common;

public class LoggingInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<DbDataReader> ReaderExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        Console.WriteLine($"[SQL LOG] ReaderExecuting: {command.CommandText}");
        return base.ReaderExecuting(command, eventData, result);
    }

    public override InterceptionResult<int> NonQueryExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult<int> result)
    {
        Console.WriteLine($"[SQL LOG] NonQueryExecuting: {command.CommandText}");
        return base.NonQueryExecuting(command, eventData, result);
    }
}

5. Cấp độ trung cấp: Đo lường hiệu suất

Chúng ta có thể vượt ra ngoài việc ghi lại và đo lường thời gian thực thi truy vấn.

csharp Copy
using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Data.Common;

public class PerformanceInterceptor: DbCommandInterceptor
{
    public override DbDataReader ReaderExecuted(
        DbCommand command, CommandExecutedEventData eventData, DbDataReader result)
    {
        var elapsed = eventData?.Duration.TotalMilliseconds ?? 0;
        Console.WriteLine($"[PERF] Query took {elapsed} ms. SQL: {Truncate(command.CommandText)}");
        return base.ReaderExecuted(command, eventData, result);
    }

    public override int NonQueryExecuted(
        DbCommand command, CommandExecutedEventData eventData, int result)
    {
        var elapsed = eventData?.Duration.TotalMilliseconds ?? 0;
        Console.WriteLine($"[PERF] NonQuery took {elapsed} ms. SQL: {Truncate(command.CommandText)}");
        return base.NonQueryExecuted(command, eventData, result);
    }

    private string Truncate(string s, int len = 200) =>
        string.IsNullOrEmpty(s) ? s : (s.Length <= len ? s : s.Substring(0, len) + "...");
}

6. Cấp độ nâng cao: Chặn SQL nguy hiểm

Điều gì sẽ xảy ra nếu ai đó vô tình viết một câu lệnh DELETE mà không có điều kiện WHERE? Chúng ta có thể chặn nó!

csharp Copy
using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Data.Common;

public class SecurityInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<int> NonQueryExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult<int> result)
    {
        var sql = (command.CommandText ?? string.Empty).TrimStart();

        if (sql.StartsWith("DELETE", StringComparison.OrdinalIgnoreCase)
            && !sql.IndexOf("WHERE", StringComparison.OrdinalIgnoreCase).Equals(-1) == false) // không có WHERE
        {
            Console.WriteLine("[SECURITY] Blocked dangerous DELETE without WHERE.");
            throw new InvalidOperationException("Blocked dangerous DELETE without WHERE clause.");
        }

        return base.NonQueryExecuting(command, eventData, result);
    }
}

7. Cấp độ anh hùng: Kiểm toán hành động của người dùng

Chúng ta có thể sử dụng các interceptor để kiểm toán ai đã thực hiện truy vấn nào.

csharp Copy
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.AspNetCore.Http;

public class AuditSaveChangesInterceptor : SaveChangesInterceptor
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public AuditSaveChangesInterceptor(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
    {
        var userName = _httpContextAccessor?.HttpContext?.User?.Identity?.Name ?? "anonymous";
        Console.WriteLine($"[AUDIT] User '{userName}' is calling SaveChanges at {DateTime.UtcNow:O}");
        return base.SavingChanges(eventData, result);
    }
}

8. Đăng ký Interceptor trong Program.cs

csharp Copy
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();

builder.Services.AddScoped<IInterceptor, LoggingInterceptor>();
builder.Services.AddScoped<IInterceptor, PerformanceInterceptor>();
builder.Services.AddScoped<IInterceptor, SecurityInterceptor>();
builder.Services.AddScoped<IInterceptor, AuditSaveChangesInterceptor>();

builder.Services.AddDbContext<AppDbContext>((serviceProvider, options) =>
{
    var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ??
                           "Server=(localdb)\\mssqllocaldb;Database=EfInterceptorsDemo;Trusted_Connection=True;";

    options.UseSqlServer(connectionString);

    var interceptors = serviceProvider.GetServices<IInterceptor>().ToArray();
    if (interceptors.Any())
    {
        options.AddInterceptors(interceptors);
    }

    options.EnableSensitiveDataLogging();
    options.LogTo(Console.WriteLine, LogLevel.Information);
});

builder.Services.AddControllers();
var app = builder.Build();

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

9. UsersController (controller + logic kinh doanh đơn giản)

csharp Copy
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly AppDbContext _db;

    public UsersController(AppDbContext db) => _db = db;

    [HttpGet]
    public async Task<IActionResult> GetUsers()
    {
        var users = await _db.Users.ToListAsync();
        return Ok(users);
    }

    [HttpPost]
    public async Task<IActionResult> AddUser([FromQuery] string name)
    {
        if (string.IsNullOrWhiteSpace(name)) return BadRequest("Name required");
        _db.Users.Add(new User { Name = name });
        await _db.SaveChangesAsync();
        return Ok();
    }

    [HttpDelete("dangerous")]
    public IActionResult AttemptDangerousDelete()
    {
        _db.Database.ExecuteSqlRaw("DELETE FROM Users");
        return Ok("Attempted dangerous delete");
    }
}

10. Cách kiểm tra (bước từng bước)

i. Chạy ứng dụng (F5 hoặc dotnet run). Theo dõi console.

ii. Tạo CSDL / Áp dụng migrations. Ví dụ tạo nhanh:

  • Sử dụng EF migrations hoặc đảm bảo bảng Users tồn tại.

iii. Kiểm tra Ghi log + Hiệu suất

  • GET http://localhost:5000/api/users
  • Dự kiến đầu ra console:
Copy
[SQL LOG] ReaderExecuting: SELECT [u].[Id], [u].[Name] FROM [Users] AS [u]
[PERF] Query took 12 ms. SQL: SELECT [u].[Id], [u].[Name] FROM [Users] AS [u]

iv. Kiểm tra Kiểm toán

POST http://localhost:5000/api/users?name=Alice

Dự kiến console:

Copy
[AUDIT] User 'anonymous' is calling SaveChanges at 2025-09-01T...
[SQL LOG] NonQueryExecuting: INSERT INTO ...
[PERF] NonQuery took 15 ms. SQL: INSERT INTO ...

v. Kiểm tra An ninh (bị chặn DELETE)

DELETE http://localhost:5000/api/users/dangerous

Dự kiến phản hồi API: 500 Internal Server Error

Console:

Copy
[SECURITY] Blocked dangerous DELETE without WHERE.

11. Thực tiễn tốt nhất

  • Giữ cho các interceptor nhẹ (tránh logic chạy lâu).
  • Sử dụng ghi log có cấu trúc (Serilog, Seq, Application Insights) thay vì Console.WriteLine.
  • Tách biệt các mối quan tâm vào nhiều interceptor (ghi log, kiểm toán, an ninh).
  • Kiểm tra tác động hiệu suất trước khi triển khai lên sản xuất.

12. Kết luận

Interceptors cơ sở dữ liệu trong Entity Framework Core (EF Core) là một tính năng mạnh mẽ nhưng ít được sử dụng, có thể biến đổi cách bạn tương tác với lớp cơ sở dữ liệu. Chúng cho phép bạn quan sát, thao tác và nâng cao các thao tác cơ sở dữ liệu một cách liền mạch — tất cả mà không cần thay đổi logic ứng dụng của bạn.

Dưới đây là lý do tại sao interceptors lại có giá trị:

Ghi lại truy vấn SQL

  • Bắt mọi câu lệnh SQL đã thực thi.
  • Giúp việc gỡ lỗi, xử lý sự cố và hiểu các mẫu truy vấn.
  • Đảm bảo tầm nhìn vào cách EF Core chuyển đổi LINQ thành SQL.

Giám sát hiệu suất

  • Đo lường thời gian thực thi truy vấn theo thời gian thực.
  • Phát hiện các truy vấn chậm hoặc không hiệu quả.
  • Tối ưu hóa hiệu suất cơ sở dữ liệu với mức tải tối thiểu.

Chặn các lệnh rủi ro

  • Can thiệp và ngăn chặn các thao tác hủy diệt như TRUNCATE hoặc DROP.
  • Thực thi các quy tắc kinh doanh và chính sách an ninh ở cấp độ cơ sở dữ liệu.
  • Giảm rủi ro về hư hỏng hoặc mất dữ liệu.

Thêm Kiểm toán

  • Theo dõi hoạt động của người dùng, bao gồm chèn, cập nhật và xóa.
  • Tự động lưu trữ siêu dữ liệu như dấu thời gian, ID người dùng và địa chỉ IP.
  • Đảm bảo tuân thủ các yêu cầu kiểm toán và quy định.

Tóm lại, các interceptor cơ sở dữ liệu nâng bạn từ việc ghi log cấp độ cơ bản lên cấp độ anh hùng trong kiểm toán, giám sát và an ninh. Chúng cung cấp cho các nhà phát triển cái nhìn sâu sắc, kiểm soát chính xác và tương tác an toàn với cơ sở dữ liệu — trong khi vẫn giữ cho mã ứng dụng sạch sẽ và dễ bảo trì.


13. Tải mã

Mã được phát triển trong bài viết này có thể được tìm thấy tại đây.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào