目录

基础

挑战

解决方案:动态表达式

Transaction介绍

参数表达式

逻辑表达式

属性表达式

常量和调用表达式

比较表达式

Lambda表达式和编译

从内存到数据库

结论


LINQ意为语言集成查询。它提供了一种一致的强类型机制,用于跨各种源查询数据。LINQ基于表达式。本文通过在参考应用程序中构建自定义表达式树来探索LINQ和基础表达式。

LINQ意为语言集成查询,是我最喜欢的.NET和C#技术之一。使用LINQ,开发人员可以直接在强类型代码中编写查询。LINQ提供了跨数据源一致的标准语言和语法。

基础

考虑以下LINQ查询(您可以将其粘贴到控制台应用程序中并自己运行):

using System;
using System.Linq;
public class Program
{public static void Main(){var someNumbers = new int[]{4, 8, 15, 16, 23, 42};var query = from num in someNumberswhere num > 10orderby num descendingselect num.ToString();Console.WriteLine(string.Join('-', query.ToArray()));// 42-23-16-15}
}

因为someNumbers是一个IEnumerable<int>,所以查询由LINQ to Objects解析。相同的查询语法可以与诸如Entity Framework Core之类的工具一起使用,以生成针对关系数据库运行的T-SQL。可以使用以下两种语法之一来编写LINQ:查询语法(如上所示)或方法语法。两种语法在语义上是相同的,您使用哪种语法取决于您的偏好。可以使用如下方法语法编写上面的相同查询:

var secondQuery = someNumbers.Where(n => n > 10).OrderByDescending(n => n).Select(n => n.ToString());

每个LINQ查询都有三个阶段:

  1. 设置了一个数据源,称为提供程序,以使查询根据该数据源进行操作。例如,到目前为止显示的代码使用内置的LINQ to Objects提供程序。EF Core项目使用映射到数据库的EF Core提供程序。
  2. 定义查询并将其转换为表达式树。一会儿我将介绍更多表达式。
  3. 执行查询,并返回数据。

步骤3很重要,因为LINQ使用了所谓的延迟执行。在上面的示例中,secondQuery定义了一个表达式树,但尚未返回任何数据。实际上,在开始迭代数据之前,实际上什么也没有发生。这很重要,因为它允许提供商仅通过传递请求的数据来管理资源。例如,假设您要使用secondQuery来查找特定的字符串,那么您可以执行以下操作:

var found = false;
foreach(var item in secondQuery.AsEnumerable())
{if (item == "23"){found = true;break;}
}

提供程序可以处理枚举数,以便它一次将一个取出数据元素。如果在第三次迭代中找到该值,则可能实际上只从数据库返回了三项。另一方面,使用.ToList()扩展方法时,将立即获取所有数据以填充列表。

挑战

作为.NET Data的PM,我经常与客户交谈以了解他们的需求。最近,我与一个客户进行了讨论,该客户希望在其网站中使用第三方控件来建立业务规则。更具体地说,业务规则是“谓词”或一组可解析为true或false的条件。该工具可以生成JSON或SQL格式的规则。SQL很想传递给数据库,但是它们的要求是将谓词也作为服务器上的筛选器应用到内存中对象。他们正在考虑将SQL转换为表达式的工具(称为动态LINQ如果您有兴趣)。我建议JSON格式可能很好,因为它可以解析为LINQ表达式,该表达式针对内存中的对象运行,或者可以轻松地应用于Entity Framework Core集合以针对数据库运行。

我写的spike只处理默认JSON产生的工具:

{"condition":"and","rules":[{"label":"Category","field":"Category","operator":"in","type":"string","value":["Clothing"]},{"condition":"or","rules":[{"label":"TransactionType","field":"TransactionType","operator":"equal","type":"boolean","value":"income"},{"label":"PaymentMode","field":"PaymentMode","operator":"equal","type":"string","value":"Cash"}]},{"label":"Amount","field":"Amount","operator":"equal","type":"number","value":10}]
}

结构很简单:存在一个AND或OR 条件,其中包含一组比较或嵌套条件的规则。我的目标是双重的:了解有关LINQ表达式的更多信息,以更好地帮助我理解EF Core和相关技术,并提供一个简单的示例来说明如何在不依赖第三方工具的情况下使用JSON。

我最早的开源贡献之一是命名为Sterling的NoSQL数据库引擎,因为我将其编写为Silverlight的本地数据库。后来,当Windows Phone与Silverlight作为运行时一起发布时,它开始流行,并被用于一些流行的食谱和健身应用程序中。Sterling 遭受了一些限制,而这些限制可以通过适当的LINQ提供程序轻松缓解。我的目标是最终掌握足够的LINQ,以便在需要时编写自己的EF Core提供程序。

解决方案:动态表达式

我创建了一个简单的控制台应用程序来检验我的假设,即从JSON实现LINQ相对简单。

JeremyLikness/ExpressionGenerator

在本文的第一部分,将启动项目设置为ExpressionGenerator。如果从命令行运行它,请确保该rules.json文件位于当前目录中。

我将示例JSON嵌入为rules.json。使用System.Text.Json解析文件非常简单:

var jsonStr = File.ReadAllText("rules.json");
var jsonDocument = JsonDocument.Parse(jsonStr);

然后,我创建了一个JsonExpressionParser以解析JSON并创建表达式树。因为解决方案是谓词,所以表达式树是根据评估左表达式和右表达式的BinaryExpression实例构建的。该评估可能是逻辑门(AND或OR),或比较(equal或greaterThan)或方法调用。对于In等的情况,我们希望属性Category位于列表中的多个项目之一中,我翻转脚本并使用Contains。从概念上讲,引用的JSON如下所示:

                        /-----------AND-----------\|                         |/-AND-\                      |
Category IN ['Clothing']   Amount eq 10.0        /-OR-\TransactionType EQ 'income'  PaymentMode EQ 'Cash'

请注意,每个节点都是二进制的。让我们开始解析!

Transaction介绍

不,不是System.Transaction。这是示例项目中使用的自定义类。我没有在供应商的网站上花费太多时间,因此我根据规则猜测该实体的外观。我想出了这个:

public class Transaction
{public int Id { get; set; }public string Category { get; set; }public string TransactionType { get; set; }public string PaymentMode { get; set; }public decimal Amount { get; set; }
}

然后,我添加了一些其他方法来简化生成随机实例的过程。您可以自己在代码中看到这些内容。

参数表达式

main方法返回一个谓词函数。这是开始的代码:

public Func<T, bool> ParsePredicateOf<T>(JsonDocument doc)
{var itemExpression = Expression.Parameter(typeof(T));var conditions = ParseTree<T>(doc.RootElement, itemExpression);
}

第一步是创建谓词参数。可以将谓词传递给Where子句,如果我们自己编写它,它将看起来像这样:

var query = ListOfThings.Where(t => t.Id > 2);

t =>是所述第一参数和表示一个项目的列表中的类型。因此,我们为该类型创建一个参数。然后,我们递归地遍历JSON节点以构建树。

逻辑表达式

解析器的开始看起来像这样:

private Expression ParseTree<T>(JsonElement condition,ParameterExpression parm){Expression left = null;var gate = condition.GetProperty(nameof(condition)).GetString();JsonElement rules = condition.GetProperty(nameof(rules));Binder binder = gate == And ? (Binder)Expression.And : Expression.Or;Expression bind(Expression left, Expression right) =>left == null ? right : binder(left, right);

有一点需要消化。gate变量是状态,即,“and”或“or”。rules语句获取一个节点,该节点是相关规则的列表。我们一直在跟踪表达式的左侧和右侧。该Binder签名是一个二进制表达式的简写,并定义如下:

private delegate Expression Binder(Expression left, Expression right);

binder变量仅设置顶级表达式:Expression.And或Expression.Or。两者都采用左右表达式来求值。

bind函数更加有趣。遍历树时,我们需要构建各个节点。如果尚未创建表达式(left是null),则从创建的第一个表达式开始。如果我们有一个现有的表达式,则可以使用该表达式合并这两个方面。

现在left是null,然后我们开始枚举属于该条件的规则:

foreach (var rule in rules.EnumerateArray())

属性表达式

第一条规则是相等规则,因此我现在将跳过条件部分。这是发生了什么:

string @operator = rule.GetProperty(nameof(@operator)).GetString();
string type = rule.GetProperty(nameof(type)).GetString();
string field = rule.GetProperty(nameof(field)).GetString();
JsonElement value = rule.GetProperty(nameof(value));
var property = Expression.Property(parm, field);

首先,我们得到运算符(“in”),类型(“string”),字段(“Category”)和值(以“Clothing”为唯一元素的数组)。请注意对Expression.Property的调用。该规则的LINQ如下所示:

var filter = new List<string> { "Clothing" };
Transactions.Where(t => filter.Contains(t.Category));

该属性是t.Category组成部分,因此我们基于父属性(t)和字段名称创建它。

常量和调用表达式

接下来,我们需要构建对Contains的调用。为简化起见,我在这里创建了对该方法的引用:

private readonly MethodInfo MethodContains = typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public).Single(m => m.Name == nameof(Enumerable.Contains)&& m.GetParameters().Length == 2);

它获取了Enumerable上的方法,该方法有两个参数:要枚举的值和要检查的值。接下来的逻辑如下所示:

if (@operator == In)
{var contains = MethodContains.MakeGenericMethod(typeof(string));object val = value.EnumerateArray().Select(e => e.GetString()).ToList();var right = Expression.Call(contains,Expression.Constant(val),property);left = bind(left, right);
}

首先,我们使用Enumerable.Contains模板来创建一个Enumerable<string>,因为这是我们要查找的类型。接下来,我们获取值列表并将其转换为List<string>。最后,我们构建调用,并传递它:

  • 调用方法(contains)
  • 作为要检查的参数的值(带有“Clothing”或Expression.Constant(val)的列表)
  • 要针对(t.Category)进行检查的属性。

我们的表达式树已经相当深,带有参数,属性,调用和常量。请记住,left仍然是null,因此绑定调用仅设置left为我们刚刚创建的调用表达式。到目前为止,我们看起来像这样:

Transactions.Where(t => (new List<string> { "Clothing" }).Contains(t.Category));

反复循环,下一个规则是嵌套条件。我们点击以下代码:

if (rule.TryGetProperty(nameof(condition), out JsonElement check))
{var right = ParseTree<T>(rule, parm);left = bind(left, right);continue;
}

当前,left已分配给“in”表达式。right将被分配为解析新条件的结果。我碰巧知道这是一个OR条件。现在,我们的binder设置为Expression.And,以便当函数返回时,bind调用留给我们的是:

Transactions.Where(t => (new List<string> { "Clothing" }).Contains(t.Category) && <something>);

让我们看一下“something”。

比较表达式

首先,递归调用确定存在一个新条件,这次是一个逻辑OR。binder设置为Expression.Or,规则开始评估。第一条规则是TransactionType。设置为boolean,但据我推断,这意味着界面中的用户可以检查选择一个值或切换到另一个值。因此,我将其实现为简单的字符串比较。这是构建比较的代码:

object val = (type == StringStr || type == BooleanStr) ?(object)value.GetString() : value.GetDecimal();
var toCompare = Expression.Constant(val);
var right = Expression.Equal(property, toCompare);
left = bind(left, right);

该值被解构为字符串或十进制(以后的规则将使用十进制格式)。然后将值转换为常数,然后创建比较。注意它是传递给属性的。变量right现在看起来像这样:

Transactions.Where(t => t.TransactionType == "income");

在此嵌套循环中,left仍为空。解析器评估下一条规则,即付款方式。该bind函数将其转换为以下“or”语句:

Transactions.Where(t => t.TransactionType == "income" || t.PaymentMode == "Cash");

其余的应该是不言自明的。表达式的一个不错的功能是它们会重载ToString()以生成表示形式。这是我们的表达形式(为了方便查看,我采取了格式化的自由):

((value(System.Collections.Generic.List`1[System.String]).Contains(Param_0.Category)And ((Param_0.TransactionType == "income")Or(Param_0.PaymentMode == "Cash")))And(Param_0.Amount == 10)
)

看起来不错…但是我们还没有完成!

Lambda表达式和编译

表达式树表示一个想法。它需要变成某种物质。如果可以简化表达式,请减少它。接下来,我创建一个lambda表达式。这定义了解析表达式的形状,它将是一个谓词(Func<T,bool>)。最后,我返回编译后的委托。

var conditions = ParseTree<T>(doc.RootElement, itemExpression);
if (conditions.CanReduce)
{conditions = conditions.ReduceAndCheck();
}
var query = Expression.Lambda<Func<T, bool>>(conditions, itemExpression);
return query.Compile();

为了“检查我的数学”,我生成了1000 transactions (加权后包括应该匹配的几笔交易)。然后,我应用过滤器并迭代结果,以便可以手动测试是否满足条件。

var predicate = jsonExpressionParser.ParsePredicateOf<Transaction>(jsonDocument);
var transactionList = Transaction.GetList(1000);
var filteredTransactions = transactionList.Where(predicate).ToList();
filteredTransactions.ForEach(Console.WriteLine);

如您所见,结果全部签出(我平均每次运行约70次“匹配”。)

从内存到数据库

生成的委托不仅用于对象。我们也可以将其用于数据库访问。

在本文的其余部分,将启动项目设置为DatabaseTest。如果从命令行运行它,请确保该databaseRules.json文件位于当前目录中。

首先,我重构了代码。还记得表达式如何需要数据源吗?在前面的示例中,我们编译表达式并最终得到对对象起作用的委托。要使用其他数据源,我们需要在编译表达式之前传递它。这样就可以对数据源进行编译。如果我们传递已编译的数据源,则将强制数据库提供程序从数据库中获取所有行,然后解析返回的列表。我们希望数据库完成这项工作。我将大量代码移到了一个名为ParseExpressionOf<T>方法中,该方法返回了lambda。我将原始方法重构为:

public Func<T, bool> ParsePredicateOf<T>(JsonDocument doc)
{var query = ParseExpressionOf<T>(doc);return query.Compile();
}

该ExpressionGenerator程序使用编译后的查询。DatabaseTest使用原始λ表达式。它将其应用于本地SQLite数据库,以演示EF Core如何解析该表达式。在将1000个transactions创建并插入数据库后,代码将检索count:

var count = await context.DbTransactions.CountAsync();
Console.WriteLine($"Verified insert count: {count}.");

这将导致以下SQL:

SELECT COUNT(*)
FROM "DbTransactions" AS "d"

如果您想知道为什么有两个上下文,那是由于日志。第一个上下文插入1000条记录,如果打开了日志记录,则在将插入内容写入控制台时它将运行非常慢。第二个上下文打开日志记录,因此您可以查看评估后的语句。

对该谓词进行解析(这次是从databaseRules.json中的一组新规则),然后传递给Entity Framework Core提供程序。

var parser = new JsonExpressionParser();
var predicate = parser.ParseExpressionOf<Transaction>(JsonDocument.Parse(await File.ReadAllTextAsync("databaseRules.json")));var query = context.DbTransactions.Where(predicate).OrderBy(t => t.Id);var results = await query.ToListAsync();

启用Entity Framework Core日志记录后,我们能够检索SQL并一目了然地获取项目并在数据库引擎中进行评估。请注意,PaymentMode已选中“Credit”而不是“Cash”。

SELECT "d"."Id", "d"."Amount", "d"."Category", "d"."PaymentMode", "d"."TransactionType"
FROM "DbTransactions" AS "d"
WHERE ("d"."Category" IN ('Clothing') &((("d"."TransactionType" = 'income') AND "d"."TransactionType" IS NOT NULL) |(("d"."PaymentMode" = 'Credit') AND "d"."PaymentMode" IS NOT NULL))) &("d"."Amount" = '10.0')
ORDER BY "d"."Id"

该示例应用程序还将打印所选实体之一以进行抽查。

结论

LINQ表达式是过滤和转换数据的非常强大的工具。我希望该示例有助于揭开表达式树的构建方式。当然,解析表达式树感觉有点像魔术。Entity Framework Core如何遍历表达式树以产生有意义的SQL?我正在自己探索这个问题,并在我的朋友ExpressionVisitor的帮助下进行了探索。

动态构建LINQ表达式相关推荐

  1. 深入LINQ | 动态构建LINQ表达式

    原文:bit.ly/3fwlKQJ 作者:Jeremy Likness 译者:精致码农-王亮 LINQ 是 Language Integrated Query(语言集成查询)的缩写,是我最喜欢的 .N ...

  2. C#表达式目录树系列之5 –动态创建查询表达式

    概要 在项目开发中,根据用户的需求,一般来是,我们的查询表达式是固定的,新的查询需求都要通过代码的修改来实现.而对于不确定的查询条件,固定查询表达式的方式显然是行不通的. 针对固定查询表达式存在的问题 ...

  3. LINQ to SQL 运行时动态构建查询条件

    原文地址:http://msdn.microsoft.com/zh-cn/dd567295.aspx 在进行数据查询时,经常碰到需要动态构建查询条件.使用LINQ实现这个需求可能会比以前拼接SQL语句 ...

  4. 通过创建动态类型 动态构建Expression Select表达式来控制Property可见性

    通过创建动态类型 动态构建Expression Select表达式来控制Property可见性 项目中经常遇到的一个场景,根据当前登录用户权限,仅返回权限内可见的内容.参考了很多开源框架,更多的是在V ...

  5. 表达式树 php,Linux_LINQ学习笔记:表达式树,构建查询表达式 本节中, 我们 - phpStudy...

    构建查询表达式 本节中, 我们假设我们拥有一个这样的实体类: 1: [Table] public partial class Product 2: 3: { 4: 5: [Column(IsPrima ...

  6. python 生成器表达式(构建器表达式)

    生成器函数可以简化迭代器的创建.在迭代器的基础上,只要在函数中使用 yield 语句来替代结果的输出,就变成了一个生成器函数. 生成器函数与普通函数最显著的不同就是它没有 return 语句,取而代之 ...

  7. 【More Effective C#】LINQ表达式与方法调用的映射

    LINQ构建在两个概念之上,一种查询语言和一系列将查询语言转换成方法调用的实现.在编译时,编译器将LINQ表达式(LINQ to object)转换成方法调用. .Net基础类库提供了两种扩展方法.S ...

  8. 动态创建Lambda表达式实现高级查询

    需求简介 最近这几天做的东西总算是回归咱的老本行了,给投资管理项目做一个台账的东西,就是类似我们的报表.其 中有一个功能是一个高级查询的需求,在查询条件方面大概有7.8个查询条件.需求就是如果一个条件 ...

  9. 一个蛮复杂的LINQ表达式

    最近写了这么一个方法: //返回结果大致为:AAGP30PMDRVS56NKpublic string GetUsbDiskSnViaDriveLetter(string strUsbDiskDriv ...

最新文章

  1. PHP 通过随机数获得ASCII 值返回字符。
  2. 分布式消息通信ActiveMQ原理-持久化策略-笔记
  3. C++ Primer 5th笔记(chap 19 特殊工具与技术)两种不可移植的特性之“位域”
  4. eq linux_在线试用 200 多种 Linux 和 Unix 操作系统 | Linux 中国
  5. 我们讨论的是《战争之城》的一个简单版本。如果地图上只有空旷的空间,河流,钢墙和砖墙。你的任务是尽快得到奖金,假设没有敌人会打扰你(见下图) 你的坦克不能穿过河流和墙壁,但它可以通过射击摧毁砖墙。当你击
  6. 乔布斯亲笔签名Apple II使用手册拍出80万美元高价
  7. 怎么能把你的公司快速做大呢
  8. auto errored after 报错解决_漫谈数据倾斜解决方案(干货)
  9. ASP.NET中 Repeater 的使用前台绑定
  10. java建立英文停用词表_HanLP-停用词表的使用示例
  11. Apple Pay 详解
  12. 客户贷款逾期预测[5] - 特征工程
  13. 使用CMFCShellTreeCtrl类作为打开文件窗口一例
  14. 固定linux虚拟机ip地址,虚拟机下linux 系统网卡配置、固定IP地址
  15. GPU accelerated TensorFlow Lite / TensorRT applications - 3D Handpose project(Part A)项目简介
  16. Composer 简单介绍
  17. 一文看懂RabbitMQ
  18. Android EditText 手机号344格式化输入的最佳实现
  19. Java 208道面试题及部分答案(后期继续做)
  20. AE 制作小狗MG动画

热门文章

  1. php教程 二叉树,PHP ClassObject -- PHP 自排序二叉树的深入解析
  2. linux基础命令怎么记,linux基础命令--笔记(示例代码)
  3. 精致的App登录页设计欣赏给你灵感
  4. 专供PNG免抠设计素材好地方,做设计到搜图114
  5. 字体设计师必备灵感来源
  6. UI初学者必备知识|最好的Sketch App 素材资源都在一流设计导航
  7. 强大的导航网站,做设计必备!
  8. java 获取线程某个_Java中如何唤醒“指定的“某个线程
  9. spring+springmvc+mybatis实现图书管理系统_Spring、SpringMVC、Mybatis自学视频分享
  10. python合法的变量名有哪些_Python判断变量名是否合法的方法示例