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

原文:bit.ly/3fwlKQJ
作者:Jeremy Likness
译者:精致码农-王亮

LINQ 是 Language Integrated Query(语言集成查询)的缩写,是我最喜欢的 .NET 和 C# 技术之一。使用 LINQ,开发者可以直接在强类型代码中编写查询。LINQ 提供了一种标准的语言和语法,使不同的数据源的查询编码方法一致。

1一些基础

考虑如下这个 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. 设置一个数据源,称为提供者(provider),供查询时使用。例如,到目前为止的代码使用了内置的 LINQ to Objects 提供者。你的 EF Core 项目使用的是 EF Core 提供者,它映射到你的数据库。

  2. 查询被定义并转变成一个表达式树(expression tree),我将在稍后介绍。

  3. 查询被执行,数据被返回。

第 3 步很重要,因为 LINQ 使用了所谓的延迟执行(deferred execution)。在上面的例子中,secondQuery 定义了一个表达式树,但还没有返回任何数据。事实上,在你开始迭代数据之前,实际上什么都没有发生。这很重要,因为它允许提供者通过只提供所要求的数据。例如,假设你想用 secondQuery 找到一个特定的字符串,所以你做了这样的事情:

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

一个提供者通过枚举器(enumerator)访问,这样它就可以一次输入一个元素的数据。如果你在第三次迭代时得到了想要的值,可能实际上只有三条数据从数据库中返回。另一方面,当你使用 .ToList() 扩展方法时,所有的数据都会立即被取出并填充到列表中。

2难题

我作为我们公司的 .NET 项目经理,我经常与客户交谈,了解他们的需求。最近,我与一位客户进行了讨论,他想在他们的网站上使用第三方控件来建立业务规则。更具体地说,业务规则是“谓词”(predicates,译注:也可以翻译成判断语句)或一组条件,可解析为 truefalse。该工具可以用 JSON 或 SQL 格式生成规则。SQL 很香,可以持久化到给数据库,但他们的要求是将“谓词”应用于内存对象,作为服务器上的一个过滤器。他们正在考虑使用一种工具,将 SQL 翻译成表达式(其实就是动态生成 LINQ)。我建议使用 JSON 格式,因为它可以被解析成 LINQ 表达式,针对内存中的对象运行,或者很容易应用到 Entity Framework Core 集合,相对 SQL 数据库是更好的选择。

我只要处理工具产生的 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}]
}

结构很简单:有一个 ANDOR 条件,包含一组规则,要么是比较,要么是嵌套条件。我的目标有两个:学习更多关于 LINQ 表达式的知识,以便更好地了解 EF Core 和相关技术;提供一个简单的例子,说明如何在不依赖第三方工具的情况下使用 JSON。

3动态表达式

我创建了一个简单的控制台应用程序来测试我的假设,即解析 JSON 信息直接生成 LINQ 查询。

https://github.com/JeremyLikness/ExpressionGenerator

译注:建议参照此 GitHub 源代码阅读本文,方便理解。

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

我将样本 JSON 嵌入为 rules.json。使用 System.Text.Json 来解析文件,就是这么简单:

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

然后我创建了一个 JsonExpressionParser 来解析 JSON 并创建一个表达式树。因为动态表达式是一个谓词,所以表达式树是由二元表达式 BinaryExpression 的实例构成的,这些实例计算一个左表达式和一个右表达式。这个计算可能是一个逻辑门(ANDOR),或一个比较(equalgreaterThan),或一个方法调用。对于 In 的情况,即我们想让属性 Category 出现在一个列表中,我使用 Contains。从概念上讲,引用的 JSON 看起来像这样:

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

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

4引入 Transaction

注意,这不是 System.Transaction(这里的 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; }
}

我还添加了一些额外的方法,以使其易于生成随机实例。你可以自己在 GitHub 代码中看到这些。

5参数表达式

主要方法返回一个谓词(predicate)函数。下面是该方法开始部分的代码:

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 节点来建立树。

6逻辑表达式

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

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”。规则语句得到一个节点,是相关规则的列表。我们正在跟踪表达式的左边和右边。Binder 签名是二元表达式的简写,定义如下:

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

binder 变量简单地设置了顶层表达式:Expression.AndExpression.Or。两者都使用左边和右边表达式来计算。

bind 函数更有趣一点。当我们遍历树时,我们需要建立各种节点。如果我们还没有创建一个表达式(leftnull),我们就从创建的第一个表达式开始。如果我们有一个现有的表达式,我们就用这个表达式来合并两边的内容。

现在,leftnull,然后我们开始列举属于这个条件的规则:

foreach (var rule in rules.EnumerateArray())

7属性表达式

第一条规则是一个相等规则,所以我现在跳过条件部分。大致情况是下面这样的:

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)和字段名来创建它。

8常量和调用表达式

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

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

这就动态提取了 EnumerableContains 方法,该方法需要两个参数:要使用的集合和要检查的值。接下来的逻辑看起来像这样:

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 仍然是空的,所以对 bind 的调用只是将 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 将被分配为解析新条件的结果。现在,我们的 binder 被设置为 Expression.And,所以当函数返回时,bind 的调用结果是这样的:

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

我们再来看看这里的“something”。

9比较表达式

首先,递归调用确定了一个新的条件存在,这次是一个逻辑 OR。binder 被设置为 Expression.Or,规则开始运算。第一条规则是关于 TransactionType 的。它被设置为布尔值,但根据我的推断,它意味着用户在界面中可以选择一个值或切换到另一个值。因此,我把它实现为一个简单的字符串比较。下面是建立比较的代码:

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

该值被解析为字符串或小数(后面的规则将使用小数格式)。然后,该值被转换成一个常数,然后创建比较。注意它是通过属性比较的。现在的变量看起来像这样:

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

在这个嵌套循环中,left 仍然是空的。解析器计算了下一条规则,即 PaymentModebind 函数把它变成了这个“或”语句:

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

它看起来不错......但我们还没有完成!

10Lambda 表达式和编译

接下来,我创建一个 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 个 Transaction。然后我应用过滤器并迭代结果,这样我就可以手动测试条件是否满足:

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

正如你所看到的,结果出来了(我平均每次运行约 70 次“命中”)。

11从内存到数据库

生成的委托并不只是用于对象。我们也可以用它来访问数据库。

在这篇文章的其余部分,将启动项目设置为 DatabaseTest。如果你从命令行运行它,要确保 databaseRules.json 在你的当前目录中。

首先,我重构了代码。还记得表达式是如何要求一个数据源的吗?在前面的例子中,我们编译了表达式,最后得到了一个对对象工作的委托。为了使用不同的数据源,我们需要在编译表达式之前将其传递给它。这允许数据源对其进行编译。如果我们传递已编译的数据源,数据库提供者将被迫从数据库中获取所有行,然后解析返回的列表。我们希望数据库来做这些工作。我把大部分代码移到一个名为 ParseExpressionOf<T> 的方法中,该方法返回 lambda。我把原来的方法重构成这样:

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

ExpressionGenerator 程序使用编译后的查询,DatabaseTest 使用原始的 lambda 表达式。它将其应用于一个本地的 SQLite 数据库,以演示 EF Core 是如何解析表达式的。在数据库中创建并插入 1000 条 Transaction 后,通过下面代码查询总数:

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

这会生成以下 SQL 语句:

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

谓词被解析(这次是来自 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"

该示例应用程序还打印了一个实体,以进行抽查。

12总结

LINQ 表达式是一个非常强大的工具,可以过滤和转换数据。我希望这个例子有助于理解表达式树是如何构建的。当然,解析表达式树感觉有点像魔术。Entity Framework Core 是如何在表达式树上行走以产生有意义的 SQL?我正在自己探索这个问题,并得到了 ExpressionVisitor 类的帮助。我将陆续发表更多关于这个问题的文章。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/302267.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java查找字符的方法_Java字符串查找(3种方法)

在给定的字符串中查找字符或字符串是比较常见的操作。字符串查找分为两种形式&#xff1a;一种是在字符串中获取匹配字符(串)的索引值&#xff0c;另一种是在字符串中获取指定索引位置的字符。根据字符查找String 类的 indexOf() 方法和 lastlndexOf() 方法用于在字符串中获取匹…

2018 Kaggle 报告:在技术领域,女性从业者持续减少,00后开始展露头脚

全世界只有3.14 % 的人关注了数据与算法之美就在上个月&#xff0c;Kaggle社区发布了《2018 Kaggle机器学习和数据科学调研》&#xff0c;调研结果显示&#xff1a;在技术领域&#xff0c;女性从业者持续减少&#xff1b;00后开始登上从业舞台&#xff1b;而且&#xff0c;23%受…

Nuget Package 支持打包 ReadMe 了

Nuget Package 支持打包 ReadMe 了Intro在 3月份&#xff0c;我们在NuGet生态系统状态上发布了一个博客&#xff0c;其中讨论了过去六个月以来从数百名客户那里获得的见解。客户在我们的调查中发现的最大问题之一是&#xff0c;“大多数软件包的文档不足”&#xff0c;可以从Nu…

幸运从来都只偏爱有准备的人——大龄码农的慌张日记

很多人将一件事的成功归结于能力&#xff0c;也有很多人将其归结为运气。今天要在这里跟大家分享的朋友名叫Leon&#xff0c;他在纽村政府注重本地人就业的大环境下&#xff0c;用时1个月以配偶工签的身份成功拿到大厂offer。接到我们的邀稿后&#xff0c;他花了很多心思写了这…

程序员必备表情包,速速收藏!

全世界只有3.14 % 的人关注了数据与算法之美程序猿怒产品 &#xff1a;程序猿不想和你说话&#xff0c;并… 被吐槽写BUG时怎么办 产品又来提需求 产品又要改需求&#xff0c;怎么办 产品说&#xff0c;这个功能三天后就要 日常怼产品 日常工作内心咆哮 来源&#xff1a;网络版…

深度解读服务治理 ServiceMesh、xDS

最近在同程艺龙蹲坑&#xff0c;聊一聊微服务治理的核心难点、历史演进、最新实现。☺️以上内容属自我思考&#xff0c;如理解有偏差、理解不透彻、现状梳理不清楚的请大家多指教。大纲微服务治理的核心难点方案演进的法宝&#xff1a;代理模式2.1 集中式代理2.2 客户端嵌入Sd…

struts2 kindeditor teatarea拿不到值问题。

2019独角兽企业重金招聘Python工程师标准>>> 源&#xff1a; <script type"text/javascript">var editor;KindEditor.ready(function(K) {editor K.create(textarea[name"userinfo.introduce"], {resizeType : 1,allowPreviewEmoticons …

三个字帮大家总结一下刘强东事件

全世界只有3.14 % 的人关注了数据与算法之美真干了【别和我说话】“工作战衣”的预售活动正在火热进行中&#xff0c;数量有限&#xff0c;欲购从速&#xff01;购买者还将会有机会免费获超级数学建模的第一本书&#xff08;附超模君亲笔签名&#xff0c;只限20名哦&#xff09…

【思维导图】新手该怎么学习C#/WPF

C#和WPF没有什么多大的关系&#xff0c;WPF是一个框架&#xff0c;VB都可以写WPF&#xff0c;至于如何学习C#&#xff0c;还是老样子&#xff01;基础&#xff1a;基础语法基础API基础练习所谓基础语法&#xff0c;包括if /if else &#xff0c;swicth&#xff0c;while&#x…

程序员搞笑故事:给女儿织的辫子 ​​​​,你知道是什么算法吗?

全世界只有3.14 % 的人关注了数据与算法之美1、程序员给女儿织的辫子 &#xff0c;你知道是什么算法吗&#xff1f;推荐阅读《啊哈&#xff01;算法》2、一个姑娘在我女友面前声讨她的男友&#xff0c;女友帮腔说&#xff1a;学土木工程的嘛&#xff0c;肯定又土又木。姑娘问&a…

模块XX.dll已加载,但对DllRegisterServer的调用失败

为什么80%的码农都做不了架构师&#xff1f;>>> 模块"XX.dll"已加载&#xff0c;但对DllRegisterServer的调用失败&#xff0c;错误代码为0x80004005 一句话&#xff0c;权限问题…… 转载于:https://my.oschina.net/szm/blog/76544

爱卡创誓记java刷钱_【178创誓记】快速升级:40到50级只需要两天的黄金刷

本文由178论坛会员&#xff1a;东东呛 转载&#xff0c;如果你是原作者请联系我们&#xff0c;我们会对原创作者给予奖励。(当然您也可以在评论回复表达看法&#xff0c;但是论坛会有更丰富的奖励哦。)朵朵快满级了~~~由于内测只开到50级~~~可还剩下一堆任务~所以刷怪刷到49级半…

大道至简,大数据的小窍门

在大数据时代的现今&#xff0c;数据庞大且繁杂&#xff0c;因此&#xff0c;如何有效利用它们&#xff0c;达到资源不浪费的目的成为了相关工作者思考的问题&#xff0c;于是数据分析就应运而生。在实际生活中&#xff0c;数据分析已经成为人们作出判断和采取行动的基石。比如…

服务端和客户端证书各种组合下对访问者(浏览器/中间人)的影响

今天本来想研究下nginx下如果获取SSL指纹&#xff0c;但是环境没有装成功就尝试了下如果不用nginx直接在服务端拿到SSL指纹&#xff0c;没想到从创建自签名证书到如何开启证书&#xff0c;以及服务端证书和客户端证书各种组合校验的测试就花了我很长时间。(注意自签名证书用Rsa…

如果科学家封神,会有什么称号?

全世界只有3.14 % 的人关注了数据与算法之美你听说过“天雷真君”吗&#xff1f;你知道“虐猫狂人"吗&#xff1f;其实这两个称号是网友分别送给大科学家富兰克林和薛定谔的。今天让我们看看伟大的科学家们还有一些什么有趣的称号。尺规小王子高斯如来神展傅立叶勾股圣手—…

mac php mcrypt,MacOSX 10.10安装mcrypt详细教程分享

mcrypt 是使用安全技术来交换数据文件加密方法. 这是必需的&#xff0c;例如一些 Magento 的 Web 应用程序,购物车软件或一个 PHP 框架&#xff0c;比如 Laravel. 本教程在 OS X 10.10 Yosemite 经过测试。本指南是真正为用户提供了PHP运行于OSX Yosemite 的5.5.14 版本。其他下…

WPF加载高德地图

WPF开发者QQ群&#xff1a; 340500857 前言 有小伙伴问如何加载高德地图。欢迎转发、分享、点赞&#xff0c;谢谢大家~。 接着上一篇源码中放了我的 BingMap Key 请大家不要滥用&#xff0c;谢谢。也可以自行申请 BingMap Key https://www.bingmapsportal.com/效果预览&#x…

深度学习与机器学习到底什么关系?

最近广州的天气老是变幻无常&#xff0c;前脚还冻得瑟瑟发抖&#xff0c;后脚又开始夏天模式&#xff08;如下图&#xff09;&#xff0c;让小天甚是怀念每天艳阳高照的夏天&#xff0c;虽然热了点但好歹不用担心猝不及防地收到寒风暴雨黄色预警。说到夏天&#xff0c;不得不提…

全选按钮的使用。winfrom程序中,对全选按钮的理解,欢迎拍砖!

最近在做公司项目时&#xff0c;用到了一些单选多选的处理情况。特编辑此文&#xff0c;欢迎批评指正。&#xff08;有图有真相&#xff09; winfrom程序。 首先&#xff0c;需要绑定某些用户&#xff0c;该用于由当前登陆用户获取。 private void BindUser() { …

理工男一般不浪漫,一浪漫便值很多年

今晚是平安夜接下来就是圣诞元旦小木先祝大家幸福、快乐、健康一年一度“最佳”圣诞元旦礼物奖就要发表了中了直男毒的礼物你们挚爱的女朋友可是不要的哦今天小木就大家扒一扒满满直男的礼物是怎样的&#xff01;Part 1“男票送了我一箱木瓜&#xff0c;说是丰胸疗程&#xff0…