【问题标题】:How to Translate C# Expression to Custom Nested (recursive) Class structure?如何将 C# 表达式转换为自定义嵌套类结构?
【发布时间】:2021-06-25 14:02:36
【问题描述】:

我已经自定义了查询类,它将用于在 lambda 表达式的帮助下构建查询,如下所示

var query = new Query("Person").Where<Person>(p =>
(p.Name == "Maulik" && (p.Id == 2 || p.Age == 30)) || (p.Id == 2 && p.Name == "Maulik"));

通过基于此参考link 类实现ExpressionVisitor,我能够将翻译后的查询转换为字符串格式,如下所示

(((Name EqualTo "Maulik") AND ((Id EqualTo 2) OR (Age EqualTo 30))) OR ((Id EqualTo 2) AND (Name EqualTo 'Maulik')))

但是,我想将上面的表达式翻译成我的自定义嵌套类

public class Expression
{
    public List<Expression> Filters { get; } // Nested expression
    public Operator Operator { get; set; } //e.g AND/OR
    public List<Condition> Conditions { get; } // One Expression can have many conditions
}

public class Condition
{
    public string Name { get; set; }
    public Operator Operator { get; set; } //e.g <,> ==, != etc
    public object Value { get; set; }
}

表达式表示:(A &gt; 5 &amp;&amp; A &lt; 10),条件表示最内部的条件A &gt; 5

我的意思是嵌套,一个表达式可以有子表达式,也可以有子子表达式等等,每个表达式可以有多个条件。

转换成这种结构的原因是为了处理各种 SQL/NoSQL 提供者,从这种类结构中将为所有不同的提供者创建单独的查询,因此不能完全改变这种类结构。

创建这个类结构是为了维护基于查询的AND和OR条件的括号顺序,因此相同级别的条件可以用单个表达式来俱乐部。

我正在寻找任何可以帮助将表达式转换为嵌套类结构的通用映射。

将表达式转换为自定义嵌套类结构的最佳方法是什么?如果有任何可用的实施示例,请分享。我还没有找到任何相关的例子。

【问题讨论】:

  • 如果您使用像实体框架这样的 ORM,甚至只是使用 Linq to SQL,那么这一切都会为您处理。真的有必要重新发明这个轮子吗?它可以完成,但是嵌套和子相关表达式和投影的复杂数组意味着要正确执行此操作,您将重新创建 EF 的 Db 提供程序,或者一旦 Linq 不是最基本的标准,就会出现运行时错误
  • 是的需要这样做,因为需要支持各种Sql/NoSql提供者,所以这个中间类层将用于翻译成DB特定查询
  • @Maulik 这些数据库都没有现有的 LINQ 提供程序?
  • @Servy 不是所有的,但包括 NoSql 在内的少数没有 LINQ 提供程序
  • @Maulik 那你为什么要为所有这些编写查询提供程序?为每个支持它的数据库使用现有的提供程序。

标签: c# lambda expression func expressionvisitor


【解决方案1】:

我认为您的类没有正确反映表达式的结构。让我用EBNF 写下此类表达式的可能语法(不要求完整性):

Expression = Term { ("AND" | "OR") Term }.
Term = "(" Expression ")" | Comparison.
Comparison = name ("=" | "!=" | "<" ...) Value.
Value = stringConstant | number.

我们看到的一件重要的事情是,Term 可以是 ExpressionComparison。这意味着我们必须能够将一个或另一个插入ANDOR 的两侧。示例:Name = "Maulik" AND (Id = 2 OR Id = 3)。在AND 的左侧我们有一个比较,在右侧是一个表达式。

因此,我们必须使这两个事物的赋值兼容,例如,通过从同一个基类派生它们。

public abstract class Term
{
}

public Expression : Term
{
    public Term FirstTerm { get; set; }
    public List<(BooleanOperator operator, Term term)> SucceedingTerms { get; } = new();
}

public Comparison : Term
{
    public string Name { get; set; }
    public ComparisonOperator Operator { get; set; }
    public object Value { get; set; }
}

另一个重要的事实是语法是递归的。即,一个表达式包含术语,而术语可以包含另一个表达式。这对于能够表示具有多级嵌套的括号表达式是必要的。

语法的这种递归性质有两个后果:

  1. 类结构必须允许递归结构。这是因为Expression 类有一个包含Terms 的列表,而这些列表又可以是Expressions。
  2. 后面显示的解析器必须是递归的。它包含间接递归调用(ParseExpression 调用 ParseTermParseTerm 调用 ParseExpression)。

请注意,我使用ValueTulpes 作为由运算符和术语组成的列表元素。


现在,到转换本身。你需要一个编译器。要么


由于语法简单,你可以自己编写编译器。

将任务分为词法分析(由词法分析器完成)和语法分析(由解析器完成)。除了分析语法外,解析器还创建所需的数据结构作为输出。词法分析器只返回一个符号流。例如

enum Symbol { EndOfInput, LeftPar, RightPar, AndOperator, OrOperator, Identifier, Number,
              StringLiteral, ...}
private string _input;
private int _currentPos;
private string _identifier;
private string _stringValue;
private decimal _number;
private Symbol _symbol;

/// Does the lexical analysis.
private void GetSymbol()
{
    // Get next `_symbol` (and associated variables) from `_input` at `_currentPos` and
    // update `_currentPos`.
}

使用此基本基础架构,您可以创建解析器。解析器由密切反映语法结构的方法组成(如上面的 EBNF 中给出的)。

// Expression = Term { ("AND" | "OR") Term }.
void Expression ParseExpression()
{
    var expression = new Expression();
    expression.FirstTerm = ParseTerm();
    while (_symbol == Symbol.AndOperator || _symbol == Symbol.OrOperator) {
        var op = _symbol == Symbol.AndOperator
            ? BooleanOperator.And
            : BooleanOperator.Or;
        GetSymbol();
        term = ParseTerm();
        expression.SucceedingTerms.Add((op, term));
    }
    return expression;
}

// Term = "(" Expression ")" | Comparison.
void Term ParseTerm()
{
    Term term = null;
    if (Symbol == Symbol.LeftPar) {
        GetSymbol();
        term = ParseExpression();
        if (Symbol == Symbol.RightPar) {
            GetSymbol();
        } else {
            Error("\")\" expected.");
        }
    } else {
        term = ParseComparison();
    }
    return term;
}

这并不完整,但你明白了。由于此解析器的结构取决于语法生成 (EBNF),因此在确定确切语法之前不要从它开始。

注意在下一个符号给出的语法中始终具有可确定的路径。首先,我错了。我的任期是由Term = "(" Expression | Comparison ")" | Comparison. 给出的。这里的问题是表达式以术语开头,而术语可以是比较。比较从名称开始。因此,ExpressionComparison 都可以以名称开头。当解析 Expression | Comparison 并且下一个符号是名称时,我们无法决定是否必须解析表达式或比较。

更新后的语法是Term = "(" Expression ")" | Comparison.。现在,我们知道如果下一个符号是左括号,我们必须解析表达式,否则解析比较。

【讨论】:

  • @Maulik。您可能希望有一个更简单的解决方案,但是没有简单的方法可以将递归表达式转换为递归数据结构。你觉得我的解决方案怎么样?有什么不明白的地方可以澄清吗?
  • 感谢@Olivier 的回答和帮助,您说得对,我们不能直接将表达式转换为递归数据结构,所以根据您的输入和 Servy 对这个问题的评论,现在我们有停止转换为我们的自定义类结构,并开始将 Expression 转换为单个 NoSql、Cache 等提供程序,对于其中一个,我们能够在访问者模式的帮助下保留嵌套条件,但对于其他数据库提供程序,我们仍然需要解决这个问题分开,因为每个人都希望查询不同的格式。再次感谢您的后续帮助 Olivier:)
猜你喜欢
  • 1970-01-01
  • 2013-12-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-24
相关资源
最近更新 更多