2

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.

Csharpest
  • 1,258
  • 14
  • 32
  • I don't get it, you declared exp1 as a Func, which you pass a Funky object and it returns a boolean. But when you write to the console you do not call the function, you are just saying: Hey, what is this expression. It looks to me that you want to call the actual Func, like exp1(afun) – Bruno Canettieri Mar 15 '21 at 11:46
  • No sir, its not about calling the expression. The expressions work identical. But casting them to string outputs different results. Because of that exp2 output cant be interpreted correctly by someone who has no reference to the `funs` array – Csharpest Mar 15 '21 at 11:51
  • Can you elaborate on why you want to send an indexer expression to the backend when the backend doesn't know how to index the thing?Maybe a more realistic example? You could include the definition of `funs` in the expression itself, but that doesn't make much sense to me, because exp1 and exp2 would basically do the same thing. – Jesse de Wit Mar 15 '21 at 11:51
  • @JessedeWit I added a "real life"-example. I believe its a bad way to send a LINQ like this, but I have to do it, I can't change the backend – Csharpest Mar 15 '21 at 12:06
  • Try to send `Expression> exp2 = x => x.Status == Fun.Low || x.Status == Fun.High;` instead. – Jesse de Wit Mar 15 '21 at 16:10

1 Answers1

4

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);
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Flawless! This works exactly the way I asked for. Thanks a lot, I can also nicely extend these visitor-methods now to add unequal/higher/lower/ etc. – Csharpest Mar 15 '21 at 15:53