技术标签: ASP.NET CORE 动态 LINQ表达式
目录
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 someNumbers
where num > 10
orderby num descending
select 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查询都有三个阶段:
步骤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'
请注意,每个节点都是二进制的。让我们开始解析!
不,不是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>。最后,我们构建调用,并传递它:
我们的表达式树已经相当深,带有参数,属性,调用和常量。请记住,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表达式。这定义了解析表达式的形状,它将是一个谓词(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的帮助下进行了探索。
原标题:西密歇根大学排名西密歇根大学2018年USNews美国最佳大学排名207名,全球大学排名第1059名。西密歇根大学(Western Michigan University)是一所公立综合型大学,位于密歇根州,卡拉马祖市,位于芝加哥和底特律等大城市的中心位置。西密歇根大学于1903年由 Dwight B. Waldo创建,现在共有五个校区,包括位于卡拉马祖的West Campus,East ...
http://blog.sina.com.cn/s/blog_6233611f0100m4vs.html
最近在开发一个文件管理服务,实际文件存储在阿里的OSS上,服务本身只管理文件映射,需要调用阿里的API生成授权访问链接。今天做了一下压力测试:1)在服务端先开启visualvm(jdk1.7以上自带,也可单独下载),然后启动服务2) 在客户端启动JMeter,设置25个线程循环10次测试结果非常糟糕,平均响应时长30s,而且OOM了。 看了一下服务器,
LAReQA: Language-Agnostic Answer Retrieval from a Multilingual Pool trained models are available at https://tfhub.dev/s?q=lareqa. dataset and evaluation code are available at https://github.com/google-research-datasets/lareqa.提出一个Language-Agnostic的检..
最近换工作以后,结结实实的写了几个月的业务。需求完结以后,就找找自己喜欢的东西写写,换个口味。撸码最难的就是给变量取名字了。所以就写一个变量生成器吧。演示如下:实现思路:使用了 Mac 上最出名的效率工具 Alfred。利用 Alfred 调用本地的 python 脚本,利用 http 模块,请求远程的 API 接口。远程 API 获取查询的字符后,首...
Problem Description给一个N位的正整数,该数不包含前导0,先让你调整其中每个数字的位置,得到另一个n位的数,并且使得这个数越小越好,而且这个数不能包含前导0。比如543210可以变成102345,而12345保持不变才是最优结果。Input第一行一个整数T(T<=100),表示有T组数据。每组数据先输入一行一个整数N(1<=N<=100),表示位数,接下来一行输入一个N位的不包含前
C语言利用联合体和结构体位域实现位带操作代码C语言编程时经常需要操作特定的数据中特定的bit位,可以通过与、或操作实现。我试了一下用结构体位域和联合体,实现了可单独对bit操作。和正常变量赋值一样,即可对单独的bit位写1或写0。下面是代码和运行结果。小白的第一篇博客,工作时遇到的问题用博客记录一下,方便以后查看。希望能坚持下去。代码#include "stdio.h"typedef...
flink sql 读取kafka 关联mysql维表 , 写出到pg中 简单演示
$project修改输入文档的结构,如重命名、增加、删除字段、创建计算结果例1:查询学生的姓名、年龄db.stu.aggregate([ {$project:{_id:0,name:1,age:1}}])例2:查询男生、女生人数,输出人数db.stu.aggregate([ {$group:{_id:'$gender',counter:{$sum:1}}
要问我是谁? 过去,我总不愿回答, 因为我怕, 我怕机关里的公务员笑话。 我们的房间很小,放不下一个鞍马; 我们的宿舍简陋,还经常搬家; 我们的电脑很破,还经常乱码; 我们的椅子很旧,坐上去吱吱哑哑…… 但是, 我们代码工整, 我们的业绩不差! 要问我此刻最想要什么? 我要我的笔记本, 我要我的Vista! 因为,有笔记本才能扛着到处出差; 因为,有Vista客户才觉得够现代化。 IT的2008,
前面我们已经将整个openstack环境进行了部署,也通过示例镜像创建了相关的虚拟机,但是如果用户自己想创建镜像怎么才能完成呢?下面我们介绍一个比较直观的可视化界面可以做到的创建方法,利用libvirt虚拟化工具来创建。由于是界面化的安装步骤,所以我们不能再使用ubuntu的Server版本了,我们应该创建一个Ubuntu的Desktop版本,这次测试环境安装的版本为:ubuntukylin-
Mock JS最近在做一个平台报表分析系统的前端界面,就是用一堆图表来展示什么点击量啊、在线人数啊、用户构成等等数据,然而后端还没开始实现调用这些数据的功能接口,于是只能用假数据了。一开始是用的本地json文件来模拟数据,虽然图表效果出来了,但是不够灵活,看着就很假,于是心血来潮,想试着用一下以前不知道在哪听来的mock.j...