Expression in .Net

 

یکی از مهمترین بخش های در  .Net ایجاد درخت Expression است. Expression Tree یک ساختمان داده که در آن هر Node یک Expression می یاشد که این اجازه با شما می دهد آنالیز، ویرایش و  در زمان Runtime کامپایل را مدیریت کنند. این قابلیت بنیادی پایه یکی از فریمورک، از جمله  Entity Framework, AutoMapper و برخی Testing Libraries است.

Using Lambda Expression

Expression<Func<int, bool>> isPositive = num => num > 0;

 

Building Expression Trees Manually

ParameterExpression numParam = Expression.Parameter(typeof(int), "num");
ConstantExpression zero = Expression.Constant(0, typeof(int));
BinaryExpression greaterThan = Expression.GreaterThan(numParam, zero);
Expression<Func<int, bool>> isPositive = Expression.Lambda<Func<int, bool>>(
    greaterThan,
    new ParameterExpression[] { numParam }
);

ساختار Epression Tree را بررسی کنیم :

  • Root: LambdaEpression
  • Parameters: ParameterExpression for x and y
  • Body: BinaryExpression representing addition
  • Left: ParameterExpression for x
  • Right: ParameterExpression for y
  • NodeType: ExpressionType.Add

حال که با ساختار آنها شده ای میتوانیم ساختار درخت رو با Expression Tree بشکافیم :

Expression<Func<int, int, int>> add = (x,y) => x+y;

Console.WriteLine($"NodeType: {add.NodeType}");
Console.WriteLine($"Return Type: {add.ReturnType}");
Console.WriteLine($"Parameters: {string.Join(", ", add.Parameters.Select(p => p.Name))}");
Console.WriteLine($"Body Type: {add.Body.NodeType}");

از ExpressionVisitor مشتق بگیرید و انواع گره‌هایی را که برایتان مهم است، بازنویسی کنید:

public class ExpressionPrinter : ExpressionVisitor
{
    protected override Expression VisitBinary(BinaryExpression node)
    {
        Console.Write("(");
        Visit(node.Left);
        Console.Write($" {GetOperatorSymbol(node.NodeType)} ");
        Visit(node.Right);
        Console.Write(")");
        return node;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        Console.Write(node.Name);
        return node;
    }
    private string GetOperatorSymbol(ExpressionType type)
    {
        switch (type)
        {
            case ExpressionType.Add: return "+";
            case ExpressionType.Subtract: return "-";
            case ExpressionType.Multiply: return "*";
            case ExpressionType.Divide: return "/";
            // Add more cases as needed
            default: return type.ToString();
        }
    }
}

کوئری LINQ داینامیک :

public IQueryable<Product> FilterProducts(IQueryable<Product> products,
                                          string propertyName,
                                          string operation,
                                          object value)
{
    ParameterExpression parameter = Expression.Parameter(typeof(Product), "p");
    MemberExpression property = Expression.Property(parameter, propertyName);
    ConstantExpression constant = Expression.Constant(value);

    Expression condition;
    switch (operation.ToLower())
    {
        case "equals":
            condition = Expression.Equal(property, constant);
            break;
        case "greaterthan":
            condition = Expression.GreaterThan(property, constant);
            break;
        // Add more operations as needed
        default:
            throw new NotSupportedException($"Operation {operation} is not supported.");
    }
    var lambda = Expression.Lambda<Func<Product, bool>>(condition, parameter);
    return products.Where(lambda);
}

یک نکته: تابع Expression.Constant(value) نوع ثابت را از نوع زمان اجرای مقدار کادربندی شده (یا شیء وقتی مقدار null است) استنباط می‌کند. اگر آن نوع با نوع ویژگی هم‌تراز نباشد، Expression.Equal در زمان ساخت خطا می‌دهد یا ارائه‌دهنده نمی‌تواند مقایسه را ترجمه کند، بنابراین در محیط عملیاتی معمولاً ابتدا مقدار را به property.Type تبدیل می‌کنید

(Expression.Constant(Convert.ChangeType(value, property.Type), property.Type)).

Property Mapping

درخت‌ها برای تولید نگاشت‌کننده‌های شیء به شیء مناسب هستند:

public static Action<TSource, TTarget> CreateMapper<TSource, TTarget>()
{
    ParameterExpression sourceParam = Expression.Parameter(typeof(TSource), "source");
    ParameterExpression targetParam = Expression.Parameter(typeof(TTarget), "target");

    List<Expression> assignments = new List<Expression>();
    foreach (var targetProp in typeof(TTarget).GetProperties())
    {
        if (!targetProp.CanWrite)
            continue;
        var sourceProp = typeof(TSource).GetProperty(targetProp.Name);
        if (sourceProp != null && sourceProp.CanRead &&
            sourceProp.PropertyType == targetProp.PropertyType)
        {
            MemberExpression sourceProperty = Expression.Property(sourceParam, sourceProp);
            MemberExpression targetProperty = Expression.Property(targetParam, targetProp);
            BinaryExpression assign = Expression.Assign(targetProperty, sourceProperty);
            assignments.Add(assign);
        }
    }
    Expression body = assignments.Count > 0 ? Expression.Block(assignments) : Expression.Empty();
    var lambda = Expression.Lambda<Action<TSource, TTarget>>(body, sourceParam, targetParam);
    return lambda.Compile();
}

فراخوانی متد پویا

یک درخت عبارت کامپایل شده، متد را سریع‌تر از MethodInfo.Invoke فراخوانی می‌کند، زیرا هزینه بازتاب یک بار در زمان کامپایل پرداخت می‌شود:

public static Func<object, object[], object> CreateDynamicMethod(MethodInfo methodInfo)
{
    ParameterExpression instanceParam = Expression.Parameter(typeof(object), "instance");
    ParameterExpression argumentsParam = Expression.Parameter(typeof(object[]), "arguments");

    List<Expression> argumentExpressions = new List<Expression>();
    ParameterInfo[] parameters = methodInfo.GetParameters();
    for (int i = 0; i < parameters.Length; i++)
    {
        Expression argument = Expression.ArrayIndex(argumentsParam, Expression.Constant(i));
        argumentExpressions.Add(Expression.Convert(argument, parameters[i].ParameterType));
    }
    Expression instanceCast = methodInfo.IsStatic
        ? null
        : Expression.Convert(instanceParam, methodInfo.DeclaringType);
    MethodCallExpression methodCall = Expression.Call(
        instanceCast,
        methodInfo,
        argumentExpressions);
    UnaryExpression resultCast = Expression.Convert(
        methodCall,
        typeof(object));
    var lambda = Expression.Lambda<Func<object, object[], object>>(
        resultCast,
        instanceParam,
        argumentsParam);
    return lambda.Compile();
}

این فرض می‌کند که متد یک مقدار را برمی‌گرداند. برای یک متد void، تابع Expression.Convert(methodCall, typeof(object)) در زمان ساخت با شکست مواجه می‌شود، بنابراین شما باید آن مورد را جداگانه مدیریت کنید (فراخوانی را در بلوکی که عبارت نهایی آن Expression.Constant(null) است، برگردانید).

ایجاد سازندگان گزاره

یک سازنده‌ی گزاره، عبارات را به صورت پویا ترکیب می‌کند:

public static class PredicateBuilder
{
    public static Expression<Func<T, bool>> True<T>() => f => true;
    public static Expression<Func<T, bool>> False<T>() => f => false;

    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expressionFirst,
        Expression<Func<T, bool>> expressionSecond)
    {
        var invokedExpr = Expression.Invoke(expressionSecond, expressionFirst.Parameters);
        return Expression.Lambda<Func<T, bool>>(Expression.OrElse(expressionFirst.Body, invokedExpr),
            expressionFirst.Parameters);
    }
    
    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expressionFirst,
        Expression<Func<T, bool>> expressionSecond)
    {
        var invokedExpr = Expression.Invoke(expressionSecond, expressionFirst.Parameters);
        return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expressionFirst.Body, invokedExpr),
            expressionFirst.Parameters);
    }
}

کاربرد:

var predicate = PredicateBuilder.True<Customer>();

if (!string.IsNullOrEmpty(firstName))
    predicate = predicate.And(c => c.FirstName.Contains(firstName));
if (!string.IsNullOrEmpty(lastName))
    predicate = predicate.And(c => c.LastName.Contains(lastName));
var customers = dbContext.Customers.Where(predicate).ToList();

بازنویسی درخت عبارات

بازنویسی به شما امکان می‌دهد یک درخت را تغییر دهید، که اغلب برای ایجاد دو لامبدا با یک پارامتر مشترک است:

public class ParameterRebinder : ExpressionVisitor
{
    private readonly Dictionary<ParameterExpression, ParameterExpression> _map;

    public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
    {
        _map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
    }
    public static Expression ReplaceParameters(
        Dictionary<ParameterExpression, ParameterExpression> map,
        Expression exp)
    {
        return new ParameterRebinder(map).Visit(exp);
    }
    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (_map.TryGetValue(node, out ParameterExpression replacement))
            node = replacement;
        return base.VisitParameter(node);
    }
}

ایجاد زبان های خاص دامنه

public class QueryBuilder<T>
{
    private Expression<Func<T, bool>> _filter = x => true;

    public QueryBuilder<T> Where(Expression<Func<T, bool>> predicate)
    {
        _filter = CombineExpressions(_filter, predicate, Expression.AndAlso);
        return this;
    }
    
    public QueryBuilder<T> OrWhere(Expression<Func<T, bool>> predicate)
    {
        _filter = CombineExpressions(_filter, predicate, Expression.OrElse);
        return this;
    }
    
    public IQueryable<T> Apply(IQueryable<T> source)
    {
        return source.Where(_filter);
    }
    
    private Expression<Func<T, bool>> CombineExpressions(
        Expression<Func<T, bool>> expr1,
        Expression<Func<T, bool>> expr2,
        Func<Expression, Expression, BinaryExpression> combiner)
    {
        var parameter = Expression.Parameter(typeof(T));
        var leftVisitor = new ExpressionReplacer(expr1.Parameters[0], parameter);
        var left = leftVisitor.Visit(expr1.Body);
        var rightVisitor = new ExpressionReplacer(expr2.Parameters[0], parameter);
        var right = rightVisitor.Visit(expr2.Body);
        return Expression.Lambda<Func<T, bool>>(combiner(left, right), parameter);
    }
    
    private class ExpressionReplacer : ExpressionVisitor
    {
        private readonly ParameterExpression _oldParameter;
        private readonly ParameterExpression _newParameter;
        public ExpressionReplacer(ParameterExpression oldParameter, ParameterExpression newParameter)
        {
            _oldParameter = oldParameter;
            _newParameter = newParameter;
        }
        protected override Expression VisitParameter(ParameterExpression node)
        {
            return node == _oldParameter ? _newParameter : base.VisitParameter(node);
        }
    }
}

کاربرد:

var query = new QueryBuilder<Customer>()
    .Where(c => c.Age > 18)
    .OrWhere(c => c.IsVIP)
    .Apply(dbContext.Customers);

ملاحظات عملکرد


درختان انعطاف پذیری را می خرند، و شما هزینه آن را در چند جا می پردازید:

  1. هزینه کامپایل: Compile() نسبت به فراخوانی متد معمولی گران است. یک بار کامپایل کنید و از نماینده برای هر چیز مهمی را دوباره استفاده کنید.
  2. پیچیدگی: ساخت و جمع آوری درختان بزرگتر زمان بیشتری می برد.
  3. حافظه: ساخت درختان بزرگ تعداد زیادی اشیاء گره را به خود اختصاص می دهد.
  4. بهینه سازی: کامپایلر مقداری بهینه سازی را انجام می دهد، اما یک نماینده دست نویس همچنان می تواند سریعتر باشد.

Specification Pattern بسیار مناسب است :

public interface ISpecification<T>
{
    Expression<Func<T, bool>> ToExpression();
    bool IsSatisfiedBy(T entity);
}

public abstract class Specification<T> : ISpecification<T>
{
    public abstract Expression<Func<T, bool>> ToExpression();
    public bool IsSatisfiedBy(T entity)
    {
        Func<T, bool> predicate = ToExpression().Compile();
        return predicate(entity);
    }
    public static Specification<T> operator &(Specification<T> left, Specification<T> right)
    {
        return new AndSpecification<T>(left, right);
    }
    public static Specification<T> operator |(Specification<T> left, Specification<T> right)
    {
        return new OrSpecification<T>(left, right);
    }
}

public class AndSpecification<T>(ISpecification<T> left, ISpecification<T> right) : Specification<T>
{
    private readonly ISpecification<T> _left = left;
    private readonly ISpecification<T> _right = right;

    public override Expression<Func<T, bool>> ToExpression()
    {
        Expression<Func<T, bool>> leftExpression = _left.ToExpression();
        Expression<Func<T, bool>> rightExpression = _right.ToExpression();
        var parameter = Expression.Parameter(typeof(T));
        var leftVisitor = new ExpressionReplacer(leftExpression.Parameters[0], parameter);
        var left = leftVisitor.Visit(leftExpression.Body);
        var rightVisitor = new ExpressionReplacer(rightExpression.Parameters[0], parameter);
        var right = rightVisitor.Visit(rightExpression.Body);
        return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left, right), parameter);
    }
}

public class OrSpecification<T>(ISpecification<T> left, ISpecification<T> right) : Specification<T>
{
    private readonly ISpecification<T> _left = left;
    private readonly ISpecification<T> _right = right;

    public override Expression<Func<T, bool>> ToExpression()
    {
        Expression<Func<T, bool>> leftExpression = _left.ToExpression();
        Expression<Func<T, bool>> rightExpression = _right.ToExpression();
        var parameter = Expression.Parameter(typeof(T));
        var leftVisitor = new ExpressionReplacer(leftExpression.Parameters[0], parameter);
        var left = leftVisitor.Visit(leftExpression.Body);
        var rightVisitor = new ExpressionReplacer(rightExpression.Parameters[0], parameter);
        var right = rightVisitor.Visit(rightExpression.Body);
        return Expression.Lambda<Func<T, bool>>(Expression.OrElse(left, right), parameter);
    }
}

public class ExpressionReplacer : ExpressionVisitor
{
    private readonly ParameterExpression _oldParameter;
    private readonly ParameterExpression _newParameter;
    public ExpressionReplacer(ParameterExpression oldParameter, ParameterExpression newParameter)
    {
        _oldParameter = oldParameter;
        _newParameter = newParameter;
    }
    
    protected override Expression VisitParameter(ParameterExpression node)
    {
        return node == _oldParameter ? _newParameter : base.VisitParameter(node);
    }
}

دیدگاه شما

نشانی ایمیل شما منتشر نخواهد شد.