Khi xây dựng các chức năng lọc dữ liệu linh hoạt, việc xử lý hàng chục điều kiện tìm kiếm một cách thủ công thường dẫn đến mã nguồn rườm rà, khó bảo trì và dễ phát sinh lỗi. Thay vì viết chuỗi if lồng ghép hoặc nối chuỗi SQL động, giải pháp tối ưu là tận dụng cây biểu thức (Expression Tree) trong C# để tạo ra bộ lọc động dựa trên cấu trúc thuộc tính của đối tượng điều kiện.
Dưới đây là cách triển khai không phụ thuộc vào số lượng điều kiện — chỉ cần đánh dấu các thuộc tính bằng một thuộc tính tùy chỉnh, hệ thống sẽ tự động sinh biểu thức Where tương ứng cho IQueryable<T>.
1. Thuộc tính điều khiển truy vấn
Định nghĩa lớp FilterFieldAttribute để khai báo cách mỗi thuộc tính trong đối tượng điều kiện ánh xạ tới thuộc tính thực tế của thực thể dữ liệu:
public sealed class FilterFieldAttribute : Attribute
{
public string TargetProperty { get; }
public FilterOperation Operation { get; }
public FilterFieldAttribute(string targetProperty, FilterOperation operation = FilterOperation.Equal)
{
TargetProperty = targetProperty ?? throw new ArgumentNullException(nameof(targetProperty));
Operation = operation;
}
}
2. Hằng số thao tác so sánh
Liệt kê các phép toán hỗ trợ — từ so sánh bằng, khoảng giá trị đến tìm kiếm chuỗi:
public enum FilterOperation
{
None,
Equal,
NotEqual,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
Contains,
StartsWith,
EndsWith
}
3. Cơ chế xây dựng biểu thức động
Phương thức mở rộng ApplyFilters<TSource, TFilter> phân tích đối tượng điều kiện, đọc thuộc tính được gắn FilterFieldAttribute, và xây dựng biểu thức Expression<Func<TSource, bool>> theo từng trường hợp:
public static class QueryableExtensions
{
public static IQueryable<TSource> ApplyFilters<TSource, TFilter>(
this IQueryable<TSource> source,
TFilter filter)
{
var predicate = BuildFilterExpression<TSource, TFilter>(filter);
return predicate != null ? source.Where(predicate) : source;
}
private static Expression<Func<TSource, bool>> BuildFilterExpression<TSource, TFilter>(TFilter filter)
{
var sourceType = typeof(TSource);
var parameter = Expression.Parameter(sourceType, "x");
Expression body = Expression.Constant(true);
var filterProperties = typeof(TFilter).GetProperties()
.Where(p => p.GetCustomAttribute<FilterFieldAttribute>() != null);
foreach (var prop in filterProperties)
{
var attr = prop.GetCustomAttribute<FilterFieldAttribute>();
var value = prop.GetValue(filter);
// Bỏ qua nếu giá trị null, rỗng (với chuỗi), hoặc không phải điều kiện lọc
if (value == null || (value is string s && string.IsNullOrWhiteSpace(s)) ||
attr.Operation == FilterOperation.None)
continue;
var targetProp = sourceType.GetProperty(attr.TargetProperty);
if (targetProp == null) continue;
var propertyAccess = Expression.Property(parameter, targetProp);
var convertedValue = ConvertToTargetType(value, targetProp.PropertyType);
var constant = Expression.Constant(convertedValue, targetProp.PropertyType);
Expression comparison = attr.Operation switch
{
FilterOperation.Equal => Expression.Equal(propertyAccess, constant),
FilterOperation.NotEqual => Expression.NotEqual(propertyAccess, constant),
FilterOperation.GreaterThan => Expression.GreaterThan(propertyAccess, constant),
FilterOperation.GreaterThanOrEqual => Expression.GreaterThanOrEqual(propertyAccess, constant),
FilterOperation.LessThan => Expression.LessThan(propertyAccess, constant),
FilterOperation.LessThanOrEqual => Expression.LessThanOrEqual(propertyAccess, constant),
FilterOperation.Contains => Expression.Call(propertyAccess,
typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) }),
constant),
FilterOperation.StartsWith => Expression.Call(propertyAccess,
typeof(string).GetMethod(nameof(string.StartsWith), new[] { typeof(string) }),
constant),
FilterOperation.EndsWith => Expression.Call(propertyAccess,
typeof(string).GetMethod(nameof(string.EndsWith), new[] { typeof(string) }),
constant),
_ => Expression.Constant(true)
};
body = Expression.AndAlso(body, comparison);
}
return Expression.Lambda<Func<TSource, bool>>(body, parameter);
}
private static object ConvertToTargetType(object value, Type targetType)
{
if (value == null || targetType.IsAssignableFrom(value.GetType()))
return value;
if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
var underlying = Nullable.GetUnderlyingType(targetType);
return Convert.ChangeType(value, underlying!);
}
return Convert.ChangeType(value, targetType);
}
}
4. Sử dụng thực tế
Định nghĩa lớp điều kiện với các thuộc tính được gán nhãn rõ ràng:
public class CustomerSearchCriteria
{
[FilterField("CompanyName", FilterOperation.Contains)]
public string NameKeyword { get; set; }
[FilterField("CreatedAt", FilterOperation.GreaterThanOrEqual)]
public DateTime? FromDate { get; set; }
[FilterField("CreatedAt", FilterOperation.LessThanOrEqual)]
public DateTime? ToDate { get; set; }
[FilterField("Status", FilterOperation.Equal)]
public int? StatusId { get; set; }
}
Sau đó áp dụng trực tiếp lên truy vấn:
var criteria = new CustomerSearchCriteria
{
NameKeyword = "Tech",
FromDate = new DateTime(2023, 1, 1),
ToDate = DateTime.Today
};
var results = dbContext.Customers
.AsNoTracking()
.ApplyFilters(criteria)
.OrderByDescending(x => x.CreatedAt)
.Take(50)
.ToList();
Hệ thống tự động loại bỏ các điều kiện không có giá trị, xử lý kiểu nullable, và sinh biểu thức phù hợp cho cả EF Core lẫn LINQ to Objects — đảm bảo hiệu năng và khả năng kiểm tra cao.