是否有可能知道在 IEnumerable 上调用了哪些操作?

Is it possible to know what actions have been called on an IEnumerable?

我有几个条件可能会影响在列表中使用的过滤器 (.Where(...))。并且在某个时候抛出异常,我想知道到目前为止对列表调用了哪些操作。

这样的事情可能吗?

var myList = new List<SomeClass>();

myList = myList.Where(item => item.property == value);
.
.
.
myList = myList.Where(item => item.otherProperty < otherValue);

Console.WriteLine(myList.ToActionsString());

它可能打印出如下内容:

list.Where(i => i.property == <the actual value>)
    .Where(i => i.otherProperty < <the actual otherValue>)

只是在列表中调用 toString() 并不能准确地给出任何相关信息,只是列出列表中的项目也不是我们感兴趣的。

不,这是不可能的,至少不能像你的问题中描述的那样直接。

List 不是一步一步过滤的(即,对所有元素应用 Where(expr1),然后对所有剩余元素应用 Where(expr2),...),而是在延迟方式:

  1. 如果您请求结果 IEnumerable 的第一个项目,LINQ 会计算每个项目的 expr1 子句,直到有一个项目匹配。
  2. 然后检查这个列表项是否也匹配 expr2.
    • 如果是,return 它(或传递到进一步的 Where 阶段)。
    • 如果不匹配,返回步骤 1 并继续查找匹配 expr1 的项目。

所以在这里通过简单地调用一些 ToActionsString() 来记录是很困难的。正如评论中已经指出的那样,添加 Where 子句时只记录可能更容易,因为无论如何你都处于已知状态:

if(condition1)
{
    myList = myList.Where(item => item.Property == value);
    Log($"Adding expression 1 with value '{value}'");
}

如果您担心 value 可能会在 IEnumerable 实际 求值 (捕获的变量)之前发生变化,并且您无法充分重组控制流,解决方法可能是创建 Func<T> 输出捕获变量的对象,并在迭代列表之前立即评估这些变量:

List<Func<int>> values = new List<Func<int>>();
if(condition1)
{
    myList = myList.Where(item => item.Property == value);
    values.Add(() => value);
}
...
foreach(var v in values)
    Log($"List will be filtered by {v()}");
var filteredList = myList.ToList();

最后,您可以在表达式中调用一些日志记录函数,记录条件 and/or 在评估这些条件时捕获异常:

myList.Where(item => 
{
    Log(item, value);
    return item.Property == value;
});

警告:这会产生开销,因为编译器必须创建所有 Expression 对象(然后必须在运行时分配和编译)。 谨慎使用

您可以使用 AsQueryable 并依靠内置 EnumerableQueryExpression 类 的 ToString 逻辑来执行此操作。以下扩展方法会将您的查询转换为其文本表示形式:

public static string GetText<T>(this IQueryable<T> query) {
   retury query.Expression.ToString();
}

可以这样使用:

var list = new List<int>();
var query = list.AsQueryable()
                .Select((c, i) => c * (i + 1))
                .Where(c => c > 5)
                .Where(c => c < 10 && c != 7)
                .Take(2)
                .OrderBy(x => 1);   
var text = query.GetText();

结果如下:

System.Collections.Generic.List`1[System.Int32].Select((c, i) => (c * (i + 1))).Where(c => (c > 5)).Where(c => ((c < 10) AndAlso (c != 7))).Take(2).OrderBy(x => 1)

我们可以在混合中加入匿名类型,看看它看起来如何:

var query = list.AsQueryable()
                .Select((c, i) => c * (i + 1))
                .Select(x => new { Value = x, ValueSquared = x * x });
var result = query.GetText();

将打印:

System.Collections.Generic.List`1[System.Int32].Select((c, i) => (c * (i + 1))).Select(x => new <>f__AnonymousType0`2(Value = x, ValueSquared = (x * x)))

通过使用Expression操作,我们可以使这个方法更健壮一点。我们可以在方法调用之间添加换行符,并可选择删除列表类型的名称。

public static string GetText<T>(this IQueryable<T> query, bool lineBreaks, bool noClassName)
{
    var text = query.Expression.ToString();

    if (!lineBreaks && !noClassName) 
        return text;

    var expression = StripQuotes(query.Expression);
    if (!(expression is MethodCallExpression mce)) 
        return text;

    if (lineBreaks)
    {
        var strings = new Stack<string>();
        strings.Push(mce.ToString());

        while (mce.Arguments.Count > 0 && mce.Arguments[0] is MethodCallExpression me)
        {
            strings.Push(me.ToString());
            mce = me;
        }

        var sb = new StringBuilder(strings.Pop());

        var len = sb.Length;
        while (strings.TryPop(out var item))
        {
            sb.AppendLine().Append(item.Substring(len));
            len = item.Length;
        }    
        text = sb.ToString();
    }

    if (mce.Arguments.Count > 0 && mce.Arguments[0] is ConstantExpression ce)
    {
        var root = ce.Value.ToString();
        if (root != null && text.StartsWith(root))
        {
            text = noClassName
                    ? text.Substring(root.Length + 1)
                    : text.Insert(root.Length, Environment.NewLine);
        }        
    }

    return text;
}

// helper in case we get an actual Queryable in there
private static Expression StripQuotes(Expression e) 
{
    while (e.NodeType == ExpressionType.Quote)
        e = ((UnaryExpression)e).Operand;
    return e;
}

我们可以这样调用这个方法:

var list = new List<int>();
var query = list.AsQueryable()
                .Select((c, i) => c * (i + 1))
                .Where(c => c > 5)
                .Where(c => c < 10 && c != 7)
                .Take(2)
                .OrderBy(x => 1);   
var text = query.GetText(true, true);

这将产生以下内容:

Select((c, i) => (c * (i + 1)))
.Where(c => (c > 5))
.Where(c => ((c < 10) AndAlso (c != 7)))
.Take(2)
.OrderBy(x => 1)

请注意,这是非常基本的。它不会涵盖闭包(传入变量)的情况,您将获得 <>DisplayClass 对象写入您的查询。我们可以用 ExpressionVisitor 来解决这个问题,它遍历表达式并计算代表闭包的 ConstantExpressions。

(遗憾的是,我目前没有时间提供 ExpressionVisitor 解决方案,但请继续关注更新)