如何从Linq表达式中获取字符串?

6
我有这个方法和参数。
void SomeMethod(Expression<Func<Products, bool>> where)

I call this method like this;

int i = 9;
SomeMethod(x=>x.Id==i)

我希望它能产生以下字符串:

"x=>x.Id==9"

如果我直接打印上述表达式,它将输出这个字符串:

"x => (x.Id == value(isTakibi.WepApp.Controllers.HomeController+<>c__DisplayClass4_0).i)"

但是我需要 “x.Id == 9”。我需要评估变量i的值,以便结果为“x.id==9”。


3
你的问题太具体了,让人不清楚你想要做什么。请退后一步,解释一下你的背景和情境。 - Jonathan Wilson
我想编写方法缓存。所以我将把 LINQ 查询和我的数据库表作为字符串写入缓存键。因此,我必须捕获所有的 LINQ 参数并将其转换为字符串。 - Recep Gündoğdu
4个回答

12
一般而言,简化表达式的方法是编译它并执行编译后的委托。但是,如果表达式中仍有参数表达式,因为您不知道参数的值(尚未确定),所以无法这样做。这意味着我们有两个基本步骤:首先确定在我们的树中哪些子表达式实际上包含某个参数,然后评估所有没有包含参数的子表达式。
因此,第一步是确定哪些表达式包含参数。为此,我们创建一个表达式访问器,该访问器具有指示其当前是否位于具有参数的子树中的字段,然后递归检查其子项,再检查自身,并组合结果,沿途将所有不包含参数的表达式添加到一个集合中。
private class ParameterlessExpressionSearcher : ExpressionVisitor
{
    public HashSet<Expression> ParameterlessExpressions { get; } = new HashSet<Expression>();
    private bool containsParameter = false;

    public override Expression Visit(Expression node)
    {
        bool originalContainsParameter = containsParameter;
        containsParameter = false;
        base.Visit(node);
        if (!containsParameter)
        {
            if (node?.NodeType == ExpressionType.Parameter)
                containsParameter = true;
            else
                ParameterlessExpressions.Add(node);
        }
        containsParameter |= originalContainsParameter;

        return node;
    }
}

接下来,为了评估没有参数的子表达式,我们需要另一个访问者。这个访问者只需检查表达式是否在我们使用前一个访问者找到的表达式集合中,如果是,就将该表达式编译成无参数的委托并执行,否则它会检查其子节点以查看是否可以替换任何子表达式。

private class ParameterlessExpressionEvaluator : ExpressionVisitor
{
    private HashSet<Expression> parameterlessExpressions;
    public ParameterlessExpressionEvaluator(HashSet<Expression> parameterlessExpressions)
    {
        this.parameterlessExpressions = parameterlessExpressions;
    }
    public override Expression Visit(Expression node)
    {
        if (parameterlessExpressions.Contains(node))
            return Evaluate(node);
        else
            return base.Visit(node);
    }

    private Expression Evaluate(Expression node)
    {
        if (node.NodeType == ExpressionType.Constant)
        {
            return node;
        }
        object value = Expression.Lambda(node).Compile().DynamicInvoke();
        return Expression.Constant(value, node.Type);
    }
}

现在我们只需要一个简单的方法,先执行第一个搜索器,然后执行第二个搜索器并返回结果,同时提供一个重载方法将结果转换为通用表达式:
public static class ExpressionExtensions
{
    public static Expression Simplify(this Expression expression)
    {
        var searcher = new ParameterlessExpressionSearcher();
        searcher.Visit(expression);
        return new ParameterlessExpressionEvaluator(searcher.ParameterlessExpressions).Visit(expression);
    }

    public static Expression<T> Simplify<T>(this Expression<T> expression)
    {
        return (Expression<T>)Simplify((Expression)expression);
    }

    //all previously shown code goes here

}

现在您可以写成:
Expression<Func<Products, bool>> e = x => x.Id == i;
e = e.Simplify();
Console.WriteLine(e);

它会打印:

"x => (x.Id == 9)"

或者,如果您只想评估一个特定的表达式,并且愿意更改首先写入表达式的方式以适应,这个答案展示了如何编写一种方法来指示哪些子表达式应该被评估,以及仅评估那些表达式。 如果您想要评估某些内容而不是其他内容,那将非常有用。


2
我已经尝试解决这个问题几个小时了,你的解决方案真是帮了我大忙。非常感谢。 - Silvestre
1
这正是我正在寻找的 - 谢谢 - Doc

1
"

.ToString() 对我来说可行:

"
void SomeMethod(Expression<Func<Product, bool>> where)
{
    Console.WriteLine(where.ToString());
}

当使用

调用时

SomeMethod(x=>x.Id==9);

Outputs:

x => (x.Id == 9)


是的,但我调用了这个方法:SomeMethod(x => x.Id == i); 所以输出结果为:"x => (x.Id == i)"。我需要查看i变量的值,因为它是动态变化的。 - Recep Gündoğdu

1

正如其他人所指出的那样,您可以在表达式上调用ToString()来获得一些类似原始表达式的东西,但这仅适用于非常简单的实现,并且对闭包的处理不好。C#编译器通过执行许多“魔法”来使诸如闭包之类的东西在表达式中工作,而您看到的“<>DisplayClass”就是其结果。为了回到原始表达式,您需要实现自定义访问者来遍历表达式并编写C# (本质上是反编译器)。

它可能看起来像以下存根:

public sealed class ExpressionWriterVisitor : ExpressionVisitor
{
  private TextWriter _writer;

  public ExpressionWriterVisitor(TextWriter writer)
  {
    _writer = writer;
  }

  protected override Expression VisitParameter(ParameterExpression node)
  {
    _writer.Write(node.Name);
    return node;
  }

  protected override Expression VisitLambda<T>(Expression<T> node)
  {
    _writer.Write('(');
    _writer.Write(string.Join(',', node.Parameters.Select(param => param.Name)));
    _writer.Write(')');
    _writer.Write("=>");

    Visit(node.Body);

    return node;
  }

  protected override Expression VisitConditional(ConditionalExpression node)
  {
    Visit(node.Test);

    _writer.Write('?');

    Visit(node.IfTrue);

    _writer.Write(':');

    Visit(node.IfFalse);

    return node;
  }

  protected override Expression VisitBinary(BinaryExpression node)
  {
    Visit(node.Left);

    _writer.Write(GetOperator(node.NodeType));

    Visit(node.Right);

    return node;
  }

  protected override Expression VisitMember(MemberExpression node)
  {
    // Closures are represented as a constant object with fields representing each closed over value.
    // This gets and prints the value of that closure.

    if (node.Member is FieldInfo fieldInfo && node.Expression is ConstantExpression constExpr)
    {
      WriteConstantValue(fieldInfo.GetValue(constExpr.Value));
    }
    else
    {
      Visit(node.Expression);
      _writer.Write('.');
      _writer.Write(node.Member.Name);
    }
    return node;
  }

  protected override Expression VisitConstant(ConstantExpression node)
  {
    WriteConstantValue(node.Value);

    return node;
  }

  private void WriteConstantValue(object obj)
  {
    switch (obj)
    {
      case string str:
        _writer.Write('"');
        _writer.Write(str);
        _writer.Write('"');
        break;
      default:
        _writer.Write(obj);
        break;
    }
  }

  private static string GetOperator(ExpressionType type)
  {
    switch (type)
    {
      case ExpressionType.Equal:
        return "==";
      case ExpressionType.Not:
        return "!";
      case ExpressionType.NotEqual:
        return "!==";
      case ExpressionType.GreaterThan:
        return ">";
      case ExpressionType.GreaterThanOrEqual:
        return ">=";
      case ExpressionType.LessThan:
        return "<";
      case ExpressionType.LessThanOrEqual:
        return "<=";
      case ExpressionType.Or:
        return "|";
      case ExpressionType.OrElse:
        return "||";
      case ExpressionType.And:
        return "&";
      case ExpressionType.AndAlso:
        return "&&";
      case ExpressionType.Add:
        return "+";
      case ExpressionType.AddAssign:
        return "+=";
      case ExpressionType.Subtract:
        return "-";
      case ExpressionType.SubtractAssign:
        return "-=";
      default:
        return "???";
    }
  }
}

在VisitMember中,有一些逻辑来从闭包中提取值。
这将打印出“(x)=>x.Id==9”:
static void Main(string[] args)
{
  var i = 9;
  Expression<Func<Product, bool>> where = x => x.Id == i;
  new ExpressionWriterVisitor(Console.Out).Visit(where);
}

0
在您的示例中,您的代码做得很对。问题在于变量i没有声明为const。该表达式必须假设在被调用之前i的值可能会更改。此代码将给出预期结果:const int i = 9; 不幸的是,由于同样的原因,这种方法可能无法用于方法缓存。您的代码也没有办法确保在声明表达式和评估表达式之间i不会更改。
您可以尝试编写一个ExpressionVisitor,尝试查找并计算这样的闭包,但我自己从未尝试过。

但是i的值是动态的,所以我不能写const。现在我可以使用这段代码获取值,但我需要使用动态变量名来实现它; ((dynamic)where).Body.Right.Expression.Value.i; // 这段代码给我返回了“9” - Recep Gündoğdu
也许你可以创建一个ExpressionVisitor,它能够检测这种情况,对其进行评估,并将其替换为一个常量表达式节点。 - Leo Bartkus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接