1

The question itself is pretty simple, but I consider it good form to give context and the current pain points. If you just want to see the question, skip to the bottom code snippet and read the code comment inside it.

Some background: We're having a backend model that consist of structured key-value pairs. We have numerous views on top these data. We want these views to be write-through and read-through. Our current implementation allows us to use expression syntax

public class Car 
{
   Car(Record car_record)
   {
       Color = new Color<ColorEnum>(()=>car_record.GetFactoryAsset("Manufacturer","Color").Value);
   }
}

The point is that the color class is agnostic as to the various ways of obtaining the correct underlying parameter. All these parameter inherit from a base class. Its constructor takes the expression and compiles an Action and a Func for getting and setting the underlying string.

public class WrappedPropBase
{
    protected Action<string> Setter;
    protected Func<string> Getter;
    public WrappedPropBase(Expression<Func<string>> expr)
    {
        var memberExpression = (MemberExpression)expr.Body;
        var instanceExpression = memberExpression.Expression;
        var parameter = Expression.Parameter(typeof(string));

        if (memberExpression.Member is PropertyInfo propertyInfo)
        {
            Setter = Expression.Lambda<Action<string>>(Expression.Call(instanceExpression, propertyInfo.GetSetMethod(), parameter), parameter).Compile();
            Getter = Expression.Lambda<Func<string>>(Expression.Call(instanceExpression, propertyInfo.GetGetMethod())).Compile();
        }
        else if (memberExpression.Member is FieldInfo fieldInfo)
        {
            Setter = Expression.Lambda<Action<string>>(Expression.Assign(memberExpression, parameter), parameter).Compile();
            Getter = Expression.Lambda<Func<string>>(Expression.Field(instanceExpression, fieldInfo)).Compile();
        }
    }
}

This is very elegant and allows the definition of the model to be terse and readable. However, the performance is not good. Since it calls the compile step twice for every property of every instance in a rather large model structure.

The performance is now such that I am basically forced to rewrite this. An obvious solution would be to accept a Action and Func when instantiating the model.

Thus the car model would look something like this.

public class Car 
{
   Car(Record car_record)
   {
       Color = new Color<ColorEnum>(()=>car_record.GetFactoryAsset("Manufacturer","Color").Value,
          s =>car_record.GetFactoryAsset("Manufacturer","Color").Value=s)
   }
}

It may not look like much of a change, but it is a duplication that could be avoided and causes a clutter.

What I would like to do is to have the runtime compile step to not execute for every car instance, but only for car model type.

Such that instead of accepting a

 ()=> car.GetTheCorrectRecord().Value

... and compiling an

Action<string> and a Func<string>

I would instead have a generic Wrapper that would accept Car as a type parameter and accept

car => car.GetTheCorrectRecord().Value

....and produce

Func<Car,string> and an Action<Car String

....giving me getter and setter that would be reusable for all instances for Car. So I am hoping to produce a base class looking like this:

public class WrappedPropBase<TModelType>
{
    protected Action<TModelType,string> Setter;
    protected Func<TModelType,string> Getter;
    public WrappedPropBase(Expression<Func<TModelType,string>> expr)
    {
        // What do I write here to produce Getter and Setter that will read/write to the string property referenced in the passed Expression instance?
    }
}
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
Tormod
  • 4,551
  • 2
  • 28
  • 50
  • Well if you have a `Expression` and want two delegates, you *must* compile twice at some point. The other alternative is to use reflection, but I suppose you'd hate that even more. What sort of black magic are you looking for here? – Sweeper Jun 02 '22 at 08:47
  • Do you want to be able to change the car color from the color object throught the manifacturer? – spzvtbg Jun 02 '22 at 09:02
  • @Sweeper Yes, but only for Car, not for every instance of car. I can for example use a static initializer and reuse the Getter / Setter by passing in "this". – Tormod Jun 02 '22 at 09:10
  • @Tormod Ah I missed that subtle difference! Thanks for pointing that out. – Sweeper Jun 02 '22 at 09:12
  • @spzvtbg Yes, the term Car may be slightly misleading. It would be more like "the make and model" of a car with a default color from the manufacturer. And changing the default color through the representation of the Car model. – Tormod Jun 02 '22 at 09:15
  • @Tormod - I think cacheing the already compiled getters and setters for each used property in this way along whit its type will help, at the end you will only look for the right delegate and will getting rid of the performance issues. – spzvtbg Jun 02 '22 at 10:15
  • The problem with caching in the old code is that the Expression already captured the car instance. The difference between ()=>car,something() and car => car.something(). The first one captures a local variable, the latter one defers the resolution of car instance to the time of invocation. – Tormod Jun 02 '22 at 10:23

1 Answers1

0

For the getter, it's just trivially compiling the expression:

Getter = expr.Compile();

For the setter, you can find the property access expression from the lambda, then create a new lambda from the property access.

Note that an additional thing you need to do is to rewrite the lambda parameters of the original lambda. I have used ExpressionSubstitute from this answer here.

var lambda = (LambdaExpression)expr;
var propertyAccess = (MemberExpression)lambda.Body;

var setterParam1 = Expression.Parameter(typeof(TModel));
var setterParam2 = Expression.Parameter(typeof(string));
Expression body = Expression.Assign(propertyAccess, setterParam2);

var swap = new ExpressionSubstitute(lambda.Parameters[0], setterParam1);
var newLambda = Expression.Lambda<Action<TModel, string>>(swap.Visit(body), setterParam1, setterParam2);

Setter = newLambda.Compile();
class ExpressionSubstitute : ExpressionVisitor
{
    public readonly Expression from, to;
    public ExpressionSubstitute(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        if (node == from) return to;
        return base.Visit(node);
    }
}
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • I will test this. I don't see the Getter being assigned...? – Tormod Jun 02 '22 at 10:26
  • @Tormod I did it in the first code snippet. Unless I'm missing something, it's trivially `expr.Compile()`, right? – Sweeper Jun 02 '22 at 10:27
  • Sorry for the delay, We had an extended weekend due to some national holidays. As you pointed out, the Getter was simple. But the setter didn't work. This is the LINQPad snippet test environment: http://share.linqpad.net/www6lb.linq – Tormod Jun 07 '22 at 10:20
  • Whoops. My bad. I forgot about accounting for lazy execution, so I was creating a new instance when I tried to verify the result. – Tormod Jun 07 '22 at 10:38