本文共 15685 字,大约阅读时间需要 52 分钟。
目录
在本文中,我将创建一个混合提供程序,该提供程序将拦截该过程,以提供一个检查和/或变异表达式树的钩子。然后,它将修改后的表达式传递给原始提供程序。然后,我们看一下自定义提供程序。
我使用一个特殊的“查询主机”,演示了如何迭代表达式树,甚至对其进行修改,从而使查询的执行方式有所不同。问题在于该方法受到限制,因为检查/修改仅在您控制端到端查询时才起作用。当查询被传递,扩展和/或再次运行时,您将失去控制和见识。像这样的产品如何让您编写所需的查询,然后成功拦截它们以运行SQL命令?秘密在提供程序中。
为了帮助解释这些概念,我构建了一个示例项目,您可以在此处浏览和下载:
“查询主机”实际上只是的实现。接口上的基本属性包括:
将查询主机视为构建查询的容器。这就是定义。是实现。接口上的主要方法应该可以证明这一点:
只要您正在定义主机的过滤器、投影、排序和查询的其他部分(作为启发,您可以查看各种)。您开始迭代查询结果的那一刻,提供程序就起作用了。对于默认的“LINQ to Objects”,这涉及对内存中的对象进行排序和过滤。对于EF Core,这涉及生成对所使用的提供程序有意义的数据库语句,例如SQL。
一般流程如下所示:
我还没有准备建立一个完整的提供程序。相反,我创建了一个混合提供程序,该提供程序拦截该过程以提供一个检查和/或变异表达式树的钩子。然后,它将修改后的表达式传递给原始提供程序。这样一来,它就可以与LINQ to Objects等内置提供程序以及EF Core等外部提供程序一样良好地工作。基本提供程序的接口如下所示:
public interface ICustomQueryProvider: IQueryProvider{ IEnumerable ExecuteEnumerable(Expression expression);}
这只是主机使用表达式调用的自定义方法。这就是拦截的地方。
抽象基类负责默认实现:
public abstract class CustomQueryProvider: ICustomQueryProvider { public CustomQueryProvider(IQueryable sourceQuery) { Source = sourceQuery; } protected IQueryable Source { get; } public abstract IQueryable CreateQuery(Expression expression); public abstract IQueryable CreateQuery (Expression expression); public virtual object Execute(Expression expression) { return Source.Provider.Execute(expression); } public virtual TResult Execute (Expression expression) { object result = (this as IQueryProvider).Execute(expression); return (TResult)result; } public virtual IEnumerable ExecuteEnumerable(Expression expression) { return Source.Provider.CreateQuery (expression); }}
定制提供程序有两个关键功能:它捕获原始查询(因此是原始提供程序),并且在调用“execute”方法时,它将表达式传递给原始提供程序。
查询主机和提供程序齐头并进。这是准备使用新提供程序的QueryHost定义(尚未定义):
public class QueryHost: IQueryHost >{ public QueryHost( IQueryable source) { Expression = source.Expression; CustomProvider = new QueryInterceptingProvider (source); } public QueryHost( Expression expression, QueryInterceptingProvider provider) { Expression = expression; CustomProvider = provider; } public virtual Type ElementType => typeof(T); public virtual Expression Expression { get; } public IQueryProvider Provider => CustomProvider; public IQueryInterceptingProvider CustomProvider { get; protected set; } public virtual IEnumerator GetEnumerator() => CustomProvider.ExecuteEnumerable(Expression).GetEnumerator();}
要注意的主要功能是在构造函数调用中捕获自定义提供程序,并将其GetEnumerator实现的请求传递给自定义提供程序。定制提供程序是什么样的?
要拦截和修改查询,需要一个函数,该函数接受原始表达式并返回变异的表达式。这是转换的定义:
public delegate Expression ExpressionTransformer(Expression source);
提供程序需要注意转换。这是定义如何注册的接口:
public interface IQueryInterceptor{ void RegisterInterceptor(ExpressionTransformer transformation);}
现在进入拦截提供程序…
这是拦截提供程序的代码:
public class QueryInterceptingProvider: CustomQueryProvider , IQueryInterceptingProvider { private ExpressionTransformer transformation = null; public QueryInterceptingProvider(IQueryable sourceQuery) : base(sourceQuery) { } public override IQueryable CreateQuery(Expression expression) { return new QueryHost (expression, this); } public override IQueryable CreateQuery (Expression expression) { if (typeof(TElement) == typeof(T)) { return CreateQuery(expression) as IQueryable ; } var childProvider = new QueryInterceptingProvider (Source); return new QueryHost ( expression, childProvider); } public void RegisterInterceptor(ExpressionTransformer transformation) { if (this.transformation != null) { throw new InvalidOperationException(); } this.transformation = transformation; } public override object Execute(Expression expression) { return Source.Provider.Execute(TransformExpression(expression)); } public override IEnumerable ExecuteEnumerable(Expression expression) { return base.ExecuteEnumerable(TransformExpression(expression)); } private Expression TransformExpression(Expression source) => transformation == null ? source : transformation(source);}
发生什么了?简而言之,定制提供程序将执行三项操作。
就是这样!查询主机使用自定义提供程序。定制提供程序可确保创建的所有新查询主机也使用定制提供程序。它还在将表达式传递给原始提供程序之前先对其进行转换。
基本的构建块已经就绪,我们已经准备好测试实现。我正在使用Thing生成随机属性和半真相来创建一个不错的价差进行查询。
public class Thing{ private static readonly Random Random = new Random(); public string Id { get; private set; } = Guid.NewGuid().ToString(); public int Value { get; private set; } = Random.Next(int.MinValue, int.MaxValue); public DateTime Created { get; private set; } = DateTime.Now; public DateTime Expires { get; private set; } = DateTime.Now.AddDays(Random.Next(1, 999)); public bool IsTrue { get; private set; } = Random.NextDouble() < 0.5;}
静态方法会生成10,000个things。接下来,我设置了拦截器。第一次拦截不会更改表达式。它只是将其写入控制台(ToString()默认覆盖提供了整个表达式树的非常清晰的表示)。
static Expression ExpressionTransformer(Expression e){ Console.WriteLine(e); return e;}
接下来,API设置要使用的查询。它将其包装在查询主机中并注册“转换”。现在可以使用查询了。
var query = new QueryHost(ThingDbQuery);query.CustomProvider.RegisterInterceptor(ExpressionTransformer);
只需向消费者显示IQueryable<Thing>即可构建查询(通过QueryHost来实现)。
这是一个立即解析为列表的查询:
var list = query.Where(t => t.IsTrue && t.Id.Contains("aa") && t.Expires < DateTime.Now.AddDays(100)) .OrderBy(t => t.Id).ToList();Console.WriteLine($"Retrieved {list.Count()} items.");
运行此命令将产生以下输出:
如果您进入调试模式,您将看到提供程序已被调用并立即“转换”表达式,从而将结果输出到控制台。就是拦截了。接下来,那么变异呢?
暴露IQueryable的挑战是事实,实际上可以应用任何类型的表达链。当滥用查询时,例如返回大型记录集或执行复杂的联接会对性能产生负面影响时,这可能会成为问题。如果您可以执行一个简单的规则,例如“此查询可能永远不会返回十个以上的项目”,该怎么办?
这是一个没有限制的查询——在上一个示例中,该查询返回了53个结果。
var results = query.Where(t => t.IsTrue && t.Id.Contains("aa") && t.Expires < DateTime.Now.AddDays(100)) .OrderBy(t => t.Id);
这是注册:
static Expression ExpressionTransformer(Expression e){ Console.WriteLine($"Before: {e}"); var newExpression = new GuardRailsExpressionVisitor().Visit(e); Console.WriteLine($"After: {newExpression}"); return newExpression;}// wrap and interceptvar query = new QueryHost(ThingDbQuery);query.CustomProvider.RegisterInterceptor(ExpressionTransformer);
请注意,该表达式已传递到GuardRailsExpressionVisitor。ExpressionVisitor类提供了一种遍历和修改表达式树的简便方法。该树由基类递归迭代,然后调用可以重写以检查树上节点的方法。被覆盖的方法将返回原始表达式或修改后的表达式,从而导致树的转换。要实现护栏,有以下三种可能性:
此实现很简单,并且假定正在使用常量。请记住,可靠的实现必须处理边缘情况,例如内部查询中的嵌套“take”语句。其他边缘情况包括使用lambda表达式引用属性并使用方法调用结果而不是常量的语句。
当您在查询上指定“take”时,实际发生的是调用Queryable类上的IQueryable。这样的查询:
var results = query.Take(10);
使用如下扩展方法:
public static IQueryableTake (this IQueryable source, int count){ if (source == null) { throw Error.ArgumentNull(nameof(source)); } return source.Provider.CreateQuery ( Expression.Call( null, IQueryable
这是拦截方法调用并强制执行“take”规则的代码:
protected override Expression VisitMethodCall(MethodCallExpression node){ if (node.Method.Name == nameof(Queryable.Take)) { TakeFound = true; if (node.Arguments[1] is ConstantExpression constant) { if (constant.Value is int valueInt) { if (valueInt > 10) { var expression = node.Update( node.Object, new[] { node.Arguments[0] } .Append(Expression.Constant(10))); return expression; } } } } return base.VisitMethodCall(node);}
仅当值超出范围时,才覆盖该表达式。该Update方法(适用于大多数表达式)无需从头开始重建原始表达式,而是使您可以轻松地创建表达式的副本并覆盖所需的参数。第一个参数指向父查询,因此第二个参数是我们覆盖到新值的参数。运行此操作将导致以下结果:
请注意,修改了较高的50,而修改较低的5则保持不变。这对于替换现有工单非常有效,但是如果没有指定任何工单怎么办?
解决方案在这里:
public override Expression Visit(Expression node){ if (first) { first = false; var expr = base.Visit(node); if (TakeFound) { return expr; } var existing = expr as MethodCallExpression; var newExpression = Expression.Call( typeof(Queryable), nameof(Queryable.Take), existing.Method.ReturnType.GetGenericArguments(), existing, Expression.Constant(10)); return newExpression; } return base.Visit(node);}
请记住,这Visit是递归调用的,因此first标志确保顶级逻辑仅运行一次。解析表达式树,如果遇到了take表达式,则仅返回修改后的表达式。否则,它将创建take表达式。顶级表达式始终是返回结果的调用。要实现take,我们只需创建自己的方法即可,将take应用于包装的方法并返回结果。故事通过Expression.Call参数讲述。
您可以在此处看到Take添加的内容,并确认结果集是否有限。
该表达式已被截取并转换!对于下一个(也是最后一个)示例,我解决了一个实际问题。
我们的一个客户问,是否有一种方法可以理解为什么在执行查询时为什么选择了某些项目而将其他项目过滤掉了。换句话说,对于每个项目,查询失败的部分是什么?解决方案很棘手。例如,如果将查询发送到数据库,则实际上没有任何拦截的决定:EF Core提供程序将创建SQL语法并将其传递给数据库。对于内存中选择,可以捕获决策。首先,我创建了一个BinaryInterceptorVisitor<T>,其用于转换给定类型的表达式树。
使用过滤器构建查询时,“where”子句将成为一组二进制表达式。例如,以下查询:
var list = query.Where( t => t.IsTrue && t.Id.Contains("aa") && t.Expires < DateTime.Now.AddDays(100)) .OrderBy(t => t.Id).ToList();
最终看起来像这样的过滤器:
AndAlso(t.IsTrue, AndAlso( t.Id.Contains("aa"), IsLessThan( t.Expires, DateTime.Now.AddDays(500))))
首先,让我们捕获t的值。
我添加了一个为捕获要评估的实体的名为instance的字段。为了简单起见,我只将其声明为一个对象:
private static object instance;
我创建了一个设置实例的方法:
private static void SetInstance(object instance){ BinaryInterceptorVisitor.instance = instance;}
为了使构建调用该方法的表达式更加容易,我添加了一些帮助程序方法,这些方法可以解决方法信息的数据结构,称为。这些是找到static方法所需的标志:
private static readonly BindingFlags GetStatic = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
这是获取MethodInfo数据的方法:
private static MethodInfo GetMethod(string methodName) => typeof(BinaryInterceptorVisitor).GetMethod(methodName, GetStatic);
这是该方法的参考:
private static readonly MethodInfo SetInstanceMethod = GetMethod(nameof(SetInstance));
查询的“where”子句使用以下签名构建为LambdaExpression:
FuncWhere;
每个实体都传递到筛选器中,最终筛选器是否通过。这是拦截lambda表达式的重载:
protected override Expression VisitLambda(Expression node)
要拦截的表达式具有特定的签名。它将只有一个类型的参数T和一个类型的返回值bool:
if (node.Parameters.Count == 1 && node.Parameters[0].Type == typeof(T) && node.ReturnType == typeof(bool))
截取正确的lambda表达式后,我们的代码会潜入以获取实体:
var returnTarget = Expression.Label(typeof(bool));var lambda = node.Update( Visit(node.Body), node.Parameters.Select(p => Visit(p)).Cast());var innerInvoke = Expression.Return( returnTarget, Expression.Invoke(lambda, lambda.Parameters));var expr = Expression.Block( Expression.Call(SetInstanceMethod, node.Parameters), innerInvoke, Expression.Label(returnTarget, Expression.Constant(false)));return Expression.Lambda >( expr, node.Parameters);
拦截类型的关键是使用BlockExpression。此表达式使您可以组合顺序运行的多个表达式。结果只使用最终表达式的结果(尽管您可以使用变量捕获其他表达式的结果)。代码中的步骤如下所示:
该代码采用以下方式:
Funclambda = t => Where(t);
并将其转换为:
Funclambda = t => { BinaryInterceptorVisitor .SetInstance(t); return Where(t);};
现在已经捕获了该值,下一步是了解哪些二进制表达式成功或失败。
理解如何捕获结果的关键是合乎逻辑的。很合逻辑。如逻辑门。现在,我们有一个Func<T,bool>可以是true或false。诀窍是注入我们自己的代码,该代码将在评估规则之前捕获该规则,然后捕获它是true还是false的结果。必须仅使用二进制表达式来完成此操作。这是一些伪代码:
逻辑提供了一种以保留原始值的方式包装表达式的方法。让我们回顾一下基本的逻辑语句:
逻辑OR left OR right=OrElse(left, right)
请注意,只有left为false时,right才运行。本质上,将其视为“当lefts是false时,穿过到right。”这是我们在调用表达式之前对其进行拦截的方式:
OrElse([our code => return false], [existing code]);
……以及另一条逻辑语句:
逻辑AND left AND right =AndAlso(left, right)
If left
基本上,您可以将其视为OR翻转,因为“当lefts是true时,穿过到right。”
这使获得成功变得容易:
AndAlso([existing code], [our code => return true]);
但是我们如何捕捉失败?我们需要的是在先前的逻辑返回false时调用代码以进行失败:
OrElse(AndAlso..., [our code => return false]);
因此,完全拦截需要四个部分:
这是替换现有表达式的实际代码:
return Expression.OrElse( orLeft, Expression.OrElse( Expression.AndAlso(binary, andRight), orRight));
那么,orLeft和其他拦截器的实现是什么样的呢?定义了两个字段来捕获嵌套级别:
private static int evalLevel = 0;private int binaryLevel = 0;
在binaryLevel解析表达式时跟踪级别。在定义阶段,随着结构的更新,将使用它。evalLevel在运行时或实现阶段跟踪嵌套的级别。
BeforeEval方法检查是否在顶层。如果是这样,它将写入由lambda表达式覆盖捕获的当前实体的值,并重置评估级别。否则,它将增加级别。然后,它编写规则。为便于阅读,会根据当前级别缩进文本。
public static void BeforeEval(int binaryLevel, string node){ if (binaryLevel == 1) { Console.WriteLine($"with {instance} => { {"); evalLevel = 0; } else { evalLevel++; } Console.WriteLine($"{Indent}[Eval {node}: ");}
AfterEval方法写出结果(成功或失败)并降低级别。如果求值在顶部表达式之后,它也会写一个右括号。
public static void AfterEval(int binaryLevel, bool success){ var result = success ? "SUCCESS" : "FAILED"; Console.WriteLine($"{Indent}{result}]"); evalLevel--; if (binaryLevel == 1) { Console.WriteLine("}"); }}
在BinaryExpression重写方法中,使用适当的参数构建方法调用:
binaryLevel++;var before = Expression.Call( BeforeEvalMethod, Expression.Constant(binaryLevel), Expression.Constant($"{node}"));var afterSuccess = Expression.Call( AfterEvalMethod, Expression.Constant(binaryLevel), Expression.Constant(true));var afterFailure = Expression.Call( AfterEvalMethod, Expression.Constant(binaryLevel), Expression.Constant(false));
接下来,创建二进制表达式输入。这些使用表达式块调用所需方法,然后返回适当的bool。
var orLeft = Expression.Block( before, Expression.Constant(false));var andRight = Expression.Block( afterSuccess, Expression.Constant(true));var orRight = Expression.Block( afterFailure, Expression.Constant(false));
现有的二进制表达式被访问left和right节点的调用替换。这确保了下游表达式也被拦截。
var binary = node.Update( Visit(node.Left), node.Conversion, Visit(node.Right));binaryLevel--;
最后,整个表达式将替换为前面显示的逻辑。
从逻辑上讲,二进制表达式被替换为该树:
这是运行的结果:
在第一个示例中,IsTrue是true,因此评估继续遵循内部规则。到期日期是2022年12月10日,它是将来的500天以上,因此失败了。这导致整个表达式失败。
在第二个示例中,IsTrue是false,因此表达式立即失败。
最后,IsTrue是true和9月3日的截止日期,2020在未来不到500天,因此整个表达式成功。
💡 提示:该方法可以比二元表达式处理更多的增强。例如,表达式AndAlso的每一侧都需要一个bool,因此您可以覆盖成员访问权限(获得IsTrue的表达式的值)并将结果显示为嵌套规则。您还可以截取表达式的求值,例如GreaterThan显示要比较的实际值。
此博客文章中涵盖的代码可在此处获得:
我希望对表达式的这些探索有助于阐明.NET工具箱中一个功能强大的工具。为了使表达式更易于访问,我一直在研究以简化表达式的使用。例如,此代码:
var query = new QueryHost(ThingDbQuery);query.CustomProvider.RegisterInterceptor(ExpressionTransformer);
简化为:
var query = ThingDbQuery.CreateInterceptedQueryable(ExpressionTranformer);
如果您对表达式感兴趣,请看一下!
转载地址:http://iiwhj.baihongyu.com/