ساختار Expressionها شبیه به ساختار یک درخت است. به عنوان مثال زمانیکه شما یک فیلتر ساده را مانند دستور زیر اجرا میکنید:
منبع : کتاب C# 8 in a Nutshell
ParameterCollection به پارامترهای استفاده شده در فیلتر اشاره دارد که در فیلتر بالا فقط s استفاده شدهاست و از نوع string است.
BinaryExpression شامل سه قسمت مهم Left , Right و NodeType میباشد. برای فیلتر بالا، مقدار پراپرتی Left برابر s.Length میباشد و پراپرتی Right شامل مقدار 5 و مقدار NodeType هم برابر LessThan میباشد. یعنی فیلتر بالا به یک درخت تبدیل شده که نود اصلی آن LessThan است و دو مقدار Left و Right را باهم مقایسه میکند. اما اگر یک شرط دیگر را به فیلتر بالا اعمال کنیم، ساختار Expression کمی تغییر میکند. برای مثال:
Expression<Func<string, bool>> filter = s => s.Length > 5 && s.Length < 45;
Expression ایجاد شده برای این فیلتر شامل همان ساختار قبلی است؛ اما با این تغییر که هر کدام از پراپرتیهای Right و Left، خود یک BinaryExpression شدهاند و مقدار NodeType اصلی از LessThan به AndAlso تغییر پیدا کردهاست. Expression ایجاد شده از فیلتر بالا ( filter.Body ) به این صورت است که پراپرتی Left آن برابر است با یک BinaryExpression که مقدار NodeType آن برابر است با GreaterThan و پراپرتی Left آن شامل s.Length میباشد و پراپرتی Right آن برابر 5 میباشد. همچنین پراپرتی Right مربوط به filter.Body برابر یک ExpressionBinary است که مقدار NodeType آن برابر است با LessThan و پراپرتی Left آن برابر s.Length است و پراپرتی Right آن برابر 45 میباشد.
filter.Body شبیه به تصویر زیر میباشد :
اگر بخواهیم خودمان یک Expression tree را ایجاد کنیم، باید از پایینترین نود آن شروع کنیم. یعنی ابتدا باید پراپرتی Left و Right را ایجاد کنیم و سپس این دو پراپرتی را با هم مقایسه کنیم (NodeType). در کد زیر Expression مربوط به فیلتر بالا را نوشتهایم:
ParameterExpression parameterExpression = Expression.Parameter(typeof(string));
MemberExpression memberExpression = Expression.Property(parameterExpression, "Length");
ConstantExpression greaterThanConstantExpression = Expression.Constant(5);
BinaryExpression greaterThanComparison = Expression.GreaterThan(memberExpression, greaterThanConstantExpression);
var greaterThan = Expression.Lambda<Func<string, bool>>(greaterThanComparison, parameterExpression);
ConstantExpression lessThanConstantExpression = Expression.Constant(45);
BinaryExpression lessThanComparsion = Expression.LessThan(memberExpression, lessThanConstantExpression);
var lessThan = Expression.Lambda<Func<string, bool>>(lessThanComparsion, parameterExpression);
var param = Expression.Parameter(typeof(string), "x");
var body = Expression.AndAlso(
Expression.Invoke(greaterThan, param),
Expression.Invoke(lessThan, param)
);
Expression<Func<string, bool>> filter = Expression.Lambda<Func<string, bool>>(body, param);
ParameterExpression : نوع پارامتری را که میخواهیم روی آن شرط را روی آن اعمال کنیم، مشخص کردهایم.
MemberExpression : پراپرتی Length را معرفی کردهایم که قرار است شرطی بر روی این پراپرتی اعمال شود.
ConstantExpression : مقدار ثابتی که پراپرتی MemeberExpression قرار است با آن مقایسه شود.
BinaryExpression : نود تایپ را مشخص کردهایم که برابر است با GreaterThan.
سپس Expression مربوط به هرکدام را در greaterThan و lessThan ایجاد کردهایم و این دو را باهم And کرده و در متغییر body قرار دادهایم و در نهایت filter را با دستور Expression.Lambda ایجاد کردهایم که برابر است با :
Expression<Func<string, bool>> filter = s => s.Length > 5 && s.Length < 45;
ساخت یک داینامیک فیلتر
در ادامه میخواهیم یک داینامیک فیلتر را ایجاد کنیم که به طور مثال برنامه نویس از سمت فرانتاند بتواند فیلترهای سادهای را اعمال کند. برای این کار یک کلاس برای فیلتر ایجاد میکنیم :
public class DynamicModel
{
public string Name { get; set; }
public string Comparison { get; set; }
public object Data { get; set; }
}
پراپرتی Data مقداری است که باید با آن مقایسه انجام شود.
Comparison نوع عملیات را مشخص میکند مانند : Equal, LessThan, GreaterThan و... .
پراپرتی Name نام پراپرتی است که باید شرط روی آن اعمال شود.
کلاس ثابت ها:
public static class ComparisonConstant
{
public const string LessThan = "LesThan";
public const string LessThanEqual = "LesThanEqual";
public const string GreaterThan = "GreaterThan";
public const string GreaterThanEqual = "GreaterThanEqual";
public const string Equal = "Equal";
public const string NotEqual = "NotEqual";
}
ساخت اکستنشن متد:
public static IQueryable<TModel> DynamicFilter<TModel>(this IQueryable<TModel> iqueryable, IEnumerable<DynamicModel> dynamicModel)
{
return iqueryable.Where(Filter<TModel>(dynamicModel));
}
public static Expression<Func<TModel, bool>> Filter<TModel>(IEnumerable<DynamicModel> dynamicModel)
{
Expression<Func<TModel, bool>> result = a => true;
foreach (var item in dynamicModel)
{
ParameterExpression parameterExpression = Expression.Parameter(typeof(TModel));
MemberExpression memberExpression = Expression.Property(parameterExpression, item.Name);
ConstantExpression constantExpression = Expression.Constant(item.Data);
BinaryExpression comparison = GetBinaryExpression(item.Comparison, memberExpression, constantExpression);
var expression = Expression.Lambda<Func<TModel, bool>>(comparison, parameterExpression);
var param = Expression.Parameter(typeof(TModel), "x");
var body = Expression.AndAlso(
Expression.Invoke(result, param),
Expression.Invoke(expression, param)
);
result = Expression.Lambda<Func<TModel, bool>>(body, param);
}
return result;
}
ورودی این مدل، لیستی از DynamicModel میباشد که به ازای هر کدام از آیتمها، یک BinaryExpression ایجاد میکند و آن را با result تعریف شده And میکند. یعنی تمامی آیتمهای ارسال شده باهم And میشوند.
متد GetBinaryExpression بر اساس مقدار فیلد Comparison که از سمت فرانت ارسال میشود، کار میکند:
private static BinaryExpression GetBinaryExpression(string comparison, MemberExpression memberExpression, ConstantExpression constantExpression)
{
switch (comparison)
{
case ComparisonConstant.Equal:
return Expression.Equal(memberExpression, constantExpression);
case ComparisonConstant.LessThan:
return Expression.LessThan(memberExpression, constantExpression);
case ComparisonConstant.GreaterThan:
return Expression.GreaterThan(memberExpression, constantExpression);
case ComparisonConstant.NotEqual:
return Expression.NotEqual(memberExpression, constantExpression);
case ComparisonConstant.GreaterThanEqual:
return Expression.GreaterThanOrEqual(memberExpression, constantExpression);
case ComparisonConstant.LessThanEqual:
return Expression.LessThanOrEqual(memberExpression, constantExpression);
default:
return null;
}
}
کلاس Category را در نظر بگیرید که شامل دو پراپرتی Title و Id میباشد و میخواهیم از این داینامیک فیلتر، برای فیلتر کردن دیتاها استفاده کنیم از سمت فرانتاند. اگر از سمت فرانتاند چنین دیتایی ارسال شود:
[
{
"Name":"Title",
"Comparison":"Equal",
"Data":"Hi"
},
{
"Name":"Id",
"Comparison":"LesThanEqual",
"Data": 100
}
]
تمامی رکوردهایی که مقدار پراپرتی Title آنها برابر Hi باشد و Id آن کوچکتر مساوی 100 باشد، از دیتابیس خوانده میشود.
var categories = _dbContext.Categories
.DynamicFilter(filter)//filter => IEnumerable<DynamicModel>
.ToList();
گیت هاب داینامیک فیلتر