+1

Named Query Filters trong EF 10: Hỗ trợ nhiều bộ lọc trên cùng một Entity

Entity Framework Core từ lâu đã hỗ trợ global query filters — một cách tiện lợi để áp dụng các điều kiện chung cho mọi truy vấn của một entity. Tính năng này đặc biệt hữu ích trong các trường hợp như xóa mềm (soft deletion) hoặc đa tenant (multi-tenancy), khi bạn muốn tự động thêm điều kiện WHERE vào mọi truy vấn. Tuy nhiên, trong các phiên bản EF Core trước đây tồn tại một giới hạn lớn: mỗi entity chỉ có thể khai báo một filter duy nhất. Nếu bạn cần kết hợp nhiều điều kiện (ví dụ vừa xóa mềm, vừa tách dữ liệu theo tenant), bạn buộc phải viết các biểu thức && thủ công hoặc tạm thời tắt toàn bộ filter và bật lại khi cần.Từ EF 10, điều đó đã thay đổi. Tính năng mới Named Query Filters cho phép bạn gắn nhiều filter lên cùng một entity và quản lý chúng theo tên. Giờ đây, bạn có thể vô hiệu hóa từng filter riêng biệt thay vì tắt toàn bộ cùng lúc. Hãy cùng tìm hiểu chi tiết tính năng này, lý do nó quan trọng và cách áp dụng thực tế.

Query Filter là gì?


Nếu bạn đã quen với EF Core, chắc hẳn bạn biết về global query filters. Đây là các điều kiện mà EF tự động thêm vào mọi truy vấn đối với một entity cụ thể — về bản chất là một mệnh đề WHERE tự động. Một số ví dụ phổ biến:

  • Xóa mềm (Soft Deletion) : ẩn các bản ghi có IsDeleted = true để dữ liệu bị xóa không xuất hiện trong truy vấn.
  • Đa tenant (Multi-tenancy) : flọc dữ liệu theo TenantId để mỗi tenant chỉ thấy dữ liệu của mình.

Ví dụ, filter xóa mềm có thể được cấu hình như sau:

modelBuilder.Entity<Order>()
    .HasQueryFilter(order => !order.IsDeleted);

Nếu bạn muốn truy xuất cả dữ liệu đã bị xóa (ví dụ trong báo cáo dành cho admin), bạn có thể gọi IgnoreQueryFilters(). Tuy nhiên, nhược điểm là phương thức này sẽ vô hiệu hóa toàn bộ các filter, dễ dẫn đến việc vô tình lộ dữ liệu nhạy cảm.

Sử dụng nhiều Query Filter trên cùng một Entity


Trước EF 10, nếu bạn gọi HasQueryFilter hai lần trên cùng một entity, lần gọi sau sẽ ghi đè lần trước. Vì vậy, nếu muốn kết hợp nhiều điều kiện, bạn phải gộp chúng lại trong cùng một biểu thức &&:

modelBuilder.Entity<Order>()
    .HasQueryFilter(order => !order.IsDeleted && order.TenantId == tenantId);

Cách này tuy hoạt động, nhưng lại khiến bạn không thể vô hiệu hóa từng điều kiện riêng biệt. IgnoreQueryFilters() sẽ tắt cả hai, buộc bạn phải tự xử lý lại logic lọc thủ công.

Với EF 10, bạn có thể đặt tên cho từng filter, cho phép EF nhận biết và quản lý chúng độc lập:

modelBuilder.Entity<Order>()
    .HasQueryFilter("SoftDeletionFilter", order => !order.IsDeleted)
    .HasQueryFilter("TenantFilter", order => order.TenantId == tenantId);

Giờ đây, bạn có thể tắt riêng một filter, ví dụ chỉ bỏ lọc xóa mềm:

// Truy xuất tất cả đơn hàng (bao gồm cả đã xóa mềm) của tenant hiện tại
var allOrders = await context.Orders.IgnoreQueryFilters(["SoftDeletionFilter"]).ToListAsync();

Nếu không truyền tham số, IgnoreQueryFilters() vẫn tắt toàn bộ filter như trước đây.

Mẹo: Đặt hằng số cho tên Filter


Vì các filter được xác định bằng chuỗi (string), việc hard-code tên trong nhiều nơi dễ gây lỗi đánh máy và khó bảo trì. Giải pháp tốt hơn là định nghĩa các hằng số hoặc enum để dùng lại:

public static class OrderFilters
{
    public const string SoftDelete = nameof(SoftDelete);
    public const string Tenant = nameof(Tenant);
}

modelBuilder.Entity<Order>()
    .HasQueryFilter(OrderFilters.SoftDelete, order => !order.IsDeleted)
    .HasQueryFilter(OrderFilters.Tenant, order => order.TenantId == tenantId);
    
// Sau này khi cần truy vấn
var allOrders = await context.Orders.IgnoreQueryFilters([OrderFilters.SoftDelete]).ToListAsync();

Cách này giúp tránh trùng lặp, tăng khả năng bảo trì và rõ ràng hơn.

Bạn cũng có thể bọc logic này trong một phương thức mở rộng (extension method) hoặc lớp repository để người dùng không cần thao tác trực tiếp với tên filter:

public static IQueryable<Order> IncludeSoftDeleted(this IQueryable<Order> query)
    => query.IgnoreQueryFilters([OrderFilters.SoftDelete]);

Như vậy, mục đích sử dụng được thể hiện rõ ràng và tập trung hóa toàn bộ logic lọc vào một nơi.

Kết luận


Sự ra đời của Named Query Filters trong Entity Framework 10 đã loại bỏ một trong những hạn chế lâu năm của tính năng Global Query Filters. Giờ đây, bạn có thể:

  • Gắn nhiều filter cho cùng một entity và quản lý chúng riêng biệt.
  • Tùy chọn vô hiệu hóa từng filter bằng IgnoreQueryFilters(["FilterName"]).
  • Đơn giản hóa các mô hình phổ biến như xóa mềm kết hợp đa tenant mà không cần điều kiện phức tạp.

Tính năng này mang lại sự linh hoạt mạnh mẽ, giúp mã nguồn sạch hơn và rõ ràng hơn.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí