Search code examples
c#serializationexpressionfunc

Output Expression Value constant-like


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.


Solution

  • 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);