Raheel Khan
Raheel Khan

Reputation: 14787

Creating an expression that can be calculated at run time

I have a list of products against which I need to create expression trees that can be persisted and later retrieved and executed. This is for a client-side calculation builder.

I am new to Expressions and although I have read a reasonable amount of documentation, the learning curve is a little steep here. What I want is to be able to accumulate PropertyExpression and Operand pairs to start with. Here is what I have so far and am not sure if I have structured it correctly.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace ConsoleApplication1
{
    public enum ProductType { Perishable, Fixed, Miscellaneous }    
    public enum OperandType { Addition, Sunbtraction, Multiplication, Division }

    public class Product
    {
        public string Name { get; set; }
        public ProductType Type { get; set; }
        public float Price { get; set; }
    }

    public class Configuration
    {
        public Dictionary<string, float> Dictionary { get; set; }
    }

    public class Operand
    {
        public OperandType Type { get; set; }
    }

    public class CalculationPair<TEntityType, TProperty>
        where TEntityType: class
        where TProperty: struct
    {
        public ValueTypeProperty<TEntityType, TProperty> Left { get; set; }
        public Operand Operand { get; set; }
        public ValueTypeProperty<TEntityType, TProperty> Right { get; set; }

        // How to specify TResult as an [out] parameter?
        public TResult Calculate<TResult> ()
            where TResult: struct
        {
            TResult result = default(TResult);

            if (this.Operand.Type == OperandType.Multiplication)
            {
                // How to execute the expression?
                //result = this.Left * this.Right;
            }

            return (result);
        }
    }

    public class ValueTypeProperty<TEntityType, TProperty>
        where TEntityType: class
        where TProperty: struct
    {
        public string Name { get; set; }

        public Expression<Func<TEntityType, TProperty>> PropertyExpression { get; set; }
    }

    public class ProductPriceProperty:
        ValueTypeProperty<Product, float>
    {
    }

    public static class Program
    {
        public static void Main ()
        {
            Configuration config = new Configuration();
            List<Product> products = new List<Product>();

            config.Dictionary.Add("ExportFactor", 80);
            config.Dictionary.Add("ChannelMargin", 100);

            products.Add(new Product() { Name = "1", Type = ProductType.Fixed, Price = 10 });
            products.Add(new Product() { Name = "2", Type = ProductType.Miscellaneous, Price = 20 });
            products.Add(new Product() { Name = "3", Type = ProductType.Perishable, Price = 30 });

            foreach (var product in products)
            {
                if (product.Type == ProductType.Fixed)
                {
                    CalculationPair<Product, float> calculation = new CalculationPair<Product, float>()
                    {
                        Left = new ProductPriceProperty() { Name = "Product Price", PropertyExpression = (entity => entity.Price) },
                        Operand = new Operand() { Type = OperandType.Multiplication },
                        Right = new ProductPriceProperty() { Name = "ExportFactor", PropertyExpression = (entity => config.Dictionary ["ExportFactor"]) },
                    };

                    // Calculation needs to be persisted to be retrieved later.
                    // ???!

                    // Once calculation has been reconstruction from the persistence layer, it needs to be executed.
                    product.Price = calculation.Calculate<float>();
                }
            }
        }
    }
}

UPDATE: Here is what I am struggling with in order of priority:

Upvotes: 2

Views: 402

Answers (1)

Sergey Litvinov
Sergey Litvinov

Reputation: 7458

As Jon said, you can use usual expression tress, or you can make closure anonymous method like in this code:

public class CalculationPair<TEntityType, TProperty>
    where TEntityType : class
    where TProperty : struct
{
    // not sure that first three properties are needed here
    public ValueTypeProperty<TEntityType, TProperty> Left { get; set; }
    public Operand Operand { get; set; }
    public ValueTypeProperty<TEntityType, TProperty> Right { get; set; }

    // closure method
    public Func<TEntityType, TProperty> Calculator { get; set; }
}

And here is part of Main method that uses it:

foreach (var product in products)
{
    if (product.Type == ProductType.Fixed)
    {
        CalculationPair<Product, float> calculation = new CalculationPair<Product, float>()
        {
            Left = new ProductPriceProperty() { Name = "Product Price", PropertyExpression = (entity => entity.Price) },
            Operand = new Operand() { Type = OperandType.Multiplication },
            Right = new ProductPriceProperty() { Name = "ExportFactor", PropertyExpression = (entity => config.Dictionary["ExportFactor"]) },

            // only this property is needed, and it will handle reference to config object in the closure
            Calculator = (entity) => entity.Price * config.Dictionary["ExportFactor"]
        };

        // Once calculation has been reconstruction from the persistence layer, it needs to be executed.
        product.Price = calculation.Calculator(product);
    }
}

In that sample there is no Expression trees, just usual closure method.

UPDATE1

The problem with your expressions for Left and Right nodes, is that each this expression is linked to own entity parameter instead of ParameterExpression that we create and that will point to real entity object, so we need to rewrite old one to new one it with ExpressionVisitor . It used for parsing and rewriting needs.

Here is code of that rewriter:

public class ParameterRewriter : ExpressionVisitor
{
    private readonly ParameterExpression _expToRewrite;

    public ParameterRewriter(ParameterExpression expToRewrite)
    {
        this._expToRewrite = expToRewrite;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        // we just use type checking to understand that it's our parameter, and we replace it with new one
        if (node.Type == this._expToRewrite.Type) return this._expToRewrite;
        return base.VisitParameter(node);
    }
}

And here is CalculationPair class:

public class CalculationPair<TEntityType, TProperty>
    where TEntityType : class
    where TProperty : struct
{
    public ValueTypeProperty<TEntityType, TProperty> Left { get; set; }
    public Operand Operand { get; set; }
    public ValueTypeProperty<TEntityType, TProperty> Right { get; set; }

    public TResult Calculate<TResult>(TEntityType entity)
        where TResult : struct
    {
        TResult result = default(TResult);

        var prop = Expression.Parameter(typeof(TEntityType), "param");
        var visitor = new ParameterRewriter(prop);
        var leftExp = visitor.Visit(Left.PropertyExpression.Body);
        var rightExp = visitor.Visit(Right.PropertyExpression.Body);

        Expression body;

        switch (this.Operand.Type)
        {
            case OperandType.Multiplication:
                body = Expression.Multiply(leftExp, rightExp);
                break;
            case OperandType.Addition:
                body = Expression.Add(leftExp, rightExp);
                break;
            case OperandType.Division:
                body = Expression.Divide(leftExp, rightExp);
                break;
            case OperandType.Sunbtraction:
                body = Expression.Subtract(leftExp, rightExp);
                break;
            default:
                throw new Exception("Unknown operand type");
        }

        var lambda = Expression.Lambda<Func<TEntityType, TResult>>(body, prop);

        // compilation is long operation, so you might need to store this Func as property and don't compile it each time
        var func = lambda.Compile();
        result = func(entity);

        return (result);
    }
}

And usage is the same

product.Price = calculation.Calculate<float>(product);

Upvotes: 1

Related Questions