Jon Egerton Jon Egerton - 26 days ago 8
C# Question

Changing the value of a ConstantExpression in a pre-existing BinaryExpression

I have some code generating expressions to pass as the "where" statement in a database read, and I'm trying to speed things up a bit.

This example below makes a where statement to match the PK of a table with a passed in value:

private Expression MakeWhereForPK(int id)
{
var paramExp = Expression.Parameter(typeof(Brand),"b");

//Expression to get value from the entity
var leftExp = Expression.Property(paramExp,"ID");

//Expression to state the value to match (from the passed in variable)
var rightExp = Expression.Constant(id,typeof(int));

//Expression to compare the two
var whereExp = Expression.Equal(leftExp,rightExp);

return Expression.Lambda<Func<Brand,bool>>(whereExp,paramExp);
}


The above is a simplification for the question - the real thing includes code to take the table to query on and find its PK etc. It is effectively doing the same thing as you might do in code usually:

ctx.Brands.Where(b => b.ID = id);


This works ok, but, while doing to testing to optimize things a bit I've found its rather slow - doing the above 1000000 times takes around 25 secs. Its better if I omit the last line above (but obviously then it's useless!), so it appears to be Expression.Lamba that taking around 2/3rds of the time, but the rest isn't great either.

If all the queries were going to happen at once I could turn it into an
IN
style expression and generate once, but unfortunately that's not possible, so what I'm hoping for is to save on most of the generation above, and just reuse the generated expression, but passing in a different value of
id
.

Note that, as this is going to be passed to Linq, I can't compile the expression to have an integer parameter that I can pass in on invocation - it must remain as the expression tree.

So the following might be an simple version for the purposes of doing the timing exercise:

Expression<Func<Brand,bool>> savedExp;

private Expression MakeWhereForPKWithCache(int id)
{
if (savedExp == null)
{
savedExp = MakeWhereForPK(id);
}
else
{
var body = (BinaryExpression)savedExp.Body;
var rightExp = (ConstantExpression)body.Right;

//At this point, value is readonly, so is there some otherway to "inject" id,
//and save on compilation?
rightExp.Value = id;
}

return savedExp;
}


How might I re-use the expression, just with a different value of id?

Answer

You can't change expression trees - they are immutable. But you can replace constant expression via making your own visitor:

class MyVisitor : ExpressionVisitor
{
    private readonly ConstantExpression newIdExpression;

    public MyVisitor(int newId)
    {
        this.newIdExpression = Expression.Constant(newId);
    }

    public Expression ReplaceId(Expression sourceExpression)
    {
        return Visit(sourceExpression);
    }

    protected override Expression VisitConstant(ConstantExpression node)
    {
        return newIdExpression;
    }
}

Usage:

            var expr = MakeWhereForPK(0); // p => p.ID == 0
            var visitor = new MyVisitor(1);
            var newExpr = visitor.ReplaceId(expr); p => p.ID == 1

Note, that this makes a copy of existing tree., and I haven't test this for performance. I'm not sure, will this be faster or not.

This code:

            // warming up
            var visitor = new MyVisitor(1);
            var expr = MakeWhereForPK(0);
            visitor.ReplaceId(MakeWhereForPK(0));

            var sw = new System.Diagnostics.Stopwatch();
            sw.Start();

            for (var i = 0; i < 1000000; i++)
            {
                MakeWhereForPK(i);
            }

            sw.Stop();
            Console.WriteLine("Make expression: {0}", sw.Elapsed);

            sw.Restart();

            for (var i = 0; i < 1000000; i++)
            {
                visitor.Visit(expr);
            }

            sw.Stop();
            Console.WriteLine("Replace constant expression: {0}", sw.Elapsed);

            Console.WriteLine("Done.");    

produces these results on my machine:

Make expression: 00:00:04.1714254
Replace constant expression: 00:00:02.3644953
Done.

Looks like visitor is faster, than creating a new expression.

Comments