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