Output Expression Value constant-like

Viewed 69

I have to send expressions over http to my backend. This backend knows about enum Fun but doesn't have a reference to funs.

My job is to serialize exp2 in a way the backend can still deserialize it

Is there a way to force passing the enum value rather than a reference to the array element?

var funs = new[] { Fun.Low, Fun.High };
Expression<Func<Funky, bool>> exp1 = x => x.Status == Fun.Low;
Expression<Func<Funky, bool>> exp2 = x => x.Status == funs[0];
        
Console.WriteLine(exp1);
//Expected: x => (Convert(x.Status, Int32) == 1)
Console.WriteLine(exp2);
//Actual output: x => (Convert(x.Status, Int32) == Convert(value(Program+<>c__DisplayClass0_0).funs[0], Int32))
        
        
public enum Fun : int {
    Low = 1,
    Middle = 2,
    High = 420
}

public class Funky {
    public Fun Status {get;set;} = Fun.High;
}

Question: how can I make exp2 the same result as exp1?

_____________________________________________

Background Info:

exp1 serializes the enum value as 1 which can be correctly interpreted by the backend.

exp2 serializes funs[0] as a reference to the actual array-element, looking like this: Convert(value(Program+<>c__DisplayClass0_0).funs[0], Int32)

I also tried exp3 but this outputs the value still as a reference rather than the constant enum value.

What I've tried so far:

//another tests
var afun = new Funky();
var param = Expression.Parameter(typeof(Funky), "x");
        
var key = afun.GetType().GetProperty("Status");
var lhs = Expression.MakeMemberAccess(param, key);
var rhs = Expression.ArrayIndex(Expression.Constant(funs), Expression.Constant(0));
        
var body = Expression.Equal(lhs, rhs);
var exp3 = Expression.Lambda<Func<Funky, bool>>(body, param);
        
Console.WriteLine(exp3);
//x => (x.Status == value(Fun[])[0])

Real-life example:

The Backend holds a database that will be queried via EF-LINQ. The Frontend is supposed to send the exact LINQ Query to the backend.

Lets say a User of the Frontend has a checklist, through which he can toggle which Funky objects he can query from Backend: [x] Low [x] Middle [_] High

-> outputs var funs = new[] { Fun.Low, Fun.Middle }; Now the Frontend will have to put the Expression together like so: Expression<Func<Funky, bool>> exp2 = x => x.Status == funs[0] || x.Status == funs[1];

and serialize it before it sends it to the backend. The backend wont be able to understand funs[0] or funs[1]. But Backend knows about enum Fun and could deserialize 1 and 2 correctly.

1 Answers

Basically, you need to rewrite the Expression to remove all the indirection and use the literal value directly. This can be done with an ExpressionVisitor - a simplified example is shown below (it handles your scenario) - but if you want to handle more complex things like method invocations (evaluated locally), you'll need to add more override methods:

    public class SimplifyingVisitor : ExpressionVisitor
    {
        protected override Expression VisitBinary(BinaryExpression node)
        {
            if (node.NodeType == ExpressionType.ArrayIndex)
            {
                if (Visit(node.Left) is ConstantExpression left
                    && left.Value is Array arr && arr.Rank == 1
                    && Visit(node.Right) is ConstantExpression right)
                {
                    var type = left.Type.GetElementType();
                    switch (right.Value)
                    {
                        case int i:
                            return Expression.Constant(arr.GetValue(i), type);
                        case long l:
                            return Expression.Constant(arr.GetValue(l), type);
                    }
                }
            }
            return base.VisitBinary(node);
        }
        protected override Expression VisitUnary(UnaryExpression node)
        {
            if (node.NodeType == ExpressionType.Convert
                 && Visit(node.Operand) is ConstantExpression arg)
            {
                try
                {
                    return Expression.Constant(
                        Convert.ChangeType(arg.Value, node.Type), node.Type);
                }
                catch { } //best efforts
            }
            return base.VisitUnary(node);
        }
        protected override Expression VisitMember(MemberExpression node)
        {
            if (node.NodeType == ExpressionType.MemberAccess && Visit(node.Expression) is ConstantExpression target)
            {
                switch (node.Member)
                {
                    case PropertyInfo property:
                        return Expression.Constant(property.GetValue(target.Value), property.PropertyType);
                    case FieldInfo field:
                        return Expression.Constant(field.GetValue(target.Value), field.FieldType);
                }
            }
            return base.VisitMember(node);
        }
    }

usage:

var visitor = new SimplifyingVisitor();
exp2 = (Expression<Func<Funky, bool>>)visitor.Visit(exp2);
Related