در بیشتر پروژههای ASP.NET Core، جستجو و مرتبسازی معمولاً داخل کنترلر نوشته میشود؛ همراه با ifهای تکراری، switch-caseهای طولانی و کوئریهای تکراری. این کار باعث پیچیدگی کد و نقض اصول Clean Architecture میشود.
در این مقاله، فقط روی دو قابلیت بسیار مهم تمرکز میکنیم:
- ApplySearch: جستجوی داینامیک با Expression Tree
- OrderByField: مرتبسازی داینامیک بر اساس نام فیلد
این دو قابلیت، پایهٔ طراحی یک Query Pipeline حرفهای در محیطهای Enterprise هستند.
1. مشکل رایج در پروژهها
معمولاً جستجو و مرتبسازی به شکل زیر نوشته میشود:
if (!string.IsNullOrWhiteSpace(search))
{
query = query.Where(x => x.Name.Contains(search));
}
if (sort == "Name")
query = query.OrderBy(x => x.Name);
else
query = query.OrderByDescending(x => x.Id);
مشکلات این روش:
- تکرار کد در تمام کنترلرها
- وابستگی شدید به موجودیتها
- غیرقابل توسعه برای فیلدهای بیشتر
- ناخوانا شدن کنترلر
- عدم رعایت اصول SOLID
2. طراحی جستجوی داینامیک (ApplySearch)
هدف این است که بتوانیم روی هر تعداد فیلد دلخواه، بدون تکرار کد، جستجو انجام دهیم.
public static class SearchExtensions
{
public static IQueryable<T> ApplySearch<T>(
this IQueryable<T> query,
string searchTerm,
params string[] propertyNames)
{
if (string.IsNullOrWhiteSpace(searchTerm) || propertyNames == null || propertyNames.Length == 0)
return query;
searchTerm = searchTerm.Trim();
var parameter = Expression.Parameter(typeof(T), "x");
Expression finalExp = null;
var constant = Expression.Constant(searchTerm);
foreach (var propertyName in propertyNames)
{
var property = Expression.Property(parameter, propertyName);
var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
var contains = Expression.Call(property, containsMethod!, constant);
finalExp = finalExp == null ? contains : Expression.OrElse(finalExp, contains);
}
var lambda = Expression.Lambda<Func<T, bool>>(finalExp, parameter);
return query.Where(lambda);
}
}
ویژگیهای این نسخه:
- بدون وابستگی به Entity خاص
- پشتیبانی از تعداد نامحدود فیلد جستجو
- قابل ترجمه کامل به SQL توسط EF Core
- Zero-Duplication
- افزایش خوانایی و تمیزی کد کنترلر
نمونه استفاده:
query = query.ApplySearch(searchString, "Name");
یا چند فیلد:
query = query.ApplySearch(searchString, "Name", "MetaTitle", "MetaDescription");
3. طراحی مرتبسازی داینامیک (OrderByField)
برای حذف switch-case های طولانی مرتبسازی، از Expression Tree استفاده میکنیم:
public static class OrderByExtensions
{
public static IQueryable<T> OrderByField<T>(
this IQueryable<T> q,
string sortField,
bool ascending)
{
if (string.IsNullOrWhiteSpace(sortField)) return q;
var param = Expression.Parameter(typeof(T), "x");
var body = Expression.PropertyOrField(param, sortField);
var keySelector = Expression.Lambda(body, param);
var methodName = ascending ? "OrderBy" : "OrderByDescending";
var method = typeof(Queryable).GetMethods().First(m => m.Name == methodName && m.GetParameters().Length == 2);
var generic = method.MakeGenericMethod(typeof(T), body.Type);
return (IQueryable<T>)generic.Invoke(null, new object[] { q, keySelector })!;
}
}
مزایای این روش:
- مرتبسازی داینامیک فقط با نام فیلد
- بدون کد اضافه در کنترلر
- کاملاً قابل استفاده مجدد در همه موجودیتها
- ترجمه عالی در SQL
نمونه استفاده:
query = query.OrderByField(sort, true); // صعودی
query = query.OrderByField(sort, false); // نزولی
4. اجرای Pipeline نهایی
کنترلر از حالت شلوغ به 3 خط تمیز تبدیل میشود:
query = query.ApplySearch(searchString, "Name");
query = query.OrderByField(sort, true);
var items = await query.ToListAsync();
این ساختار موجب خوانایی بیشتر، قابلیت تست بهتر، توسعهپذیری بالا و یکپارچگی کامل معماری میشود.