如何在C#中调试LINQ查询

原文:How to Debug LINQ queries in C#
作者:Michael Shpilt
译文:如何在C#中调试LINQ查询
译者:Lamond Lu

在C#中我最喜欢的特性就是LINQ。使用LINQ, 我们可以获得一种易于编写和理解的简洁语法,而不是单调的foreach循环,它可以让你的代码更加美观。

但是LINQ也有不好的地方,就是调试起来非常难。我们无法知道查询中到底发生了什么。我们可以看到输入值和输出值,但是仅此而已。当代码出现问题的时候,我们只能盯着代码看吗?答案是否定的,这里有几种可以使用的LINQ的调试方法。

LINQ调试

尽管很困难,但是这里还是有几种可选的方式来调试LINQ的。

这里首先,我们先创建一个测试场景。假设我们现在想要获取一个列表,这个列表中包含了3个超过平均工资的男性员工的信息,并且按照年龄排序。这是一个非常普通的查询,下面就是我针对这个场景编写的查询方法。

public IEnumerable<Employee> MyQuery(List<Employee> employees)
{
    var avgSalary = employees.Select(e=>e.Salary).Average();
 
    return employees
        .Where(e => e.Gender == "Male")
        .Take(3)
        .Where(e => e.Salary > avgSalary)
        .OrderBy(e => e.Age);
}

这里我们使用的数据集如下:

NameAgeGenderSalary
Peter Claus40“Male”61000
Jose Mond35“male”62000
Helen Gant38“Female”38000
Jo Parker42“Male”52000
Alex Mueller22“Male”39000
Abbi Black53“female”56000
Mike Mockson51“Male”82000

当运行以上查询之后, 我得到的结果是

Peter Claus, 61000, 40

这个结果看起来不太对…这里应该查出3个员工。这里我们计算出的平均工资应该是56400, 所以’Jose Mond’和’Mick Mockson’应该也是满足条件的结果。

所以呢,这里在我的LINQ查询中有BUG, 那么我们该怎么做? 当然我可以一直盯着代码来找出问题,在某些场景下这种方式可能是行的通的。或者呢我们可以来尝试调试它。

下面让我们看一下,我们有哪些可选的调试方法。

1. 使用Quickwatch

这里比较容易的方法是使用QuickWatch窗口来查看查询的不同部分的结果。你可以从第一个操作开始,一步一步的追加过滤条件。

例:

这里我们可以看到,在经过第一个查询之后,就出错了。 ‘Jose Mond’应该是一个男性,但是在结果集中缺失了。那么我们的BUG应该就是出在这里了,我们可以只盯着这一小段代码来查找问题。没错,这里的BUG原因是数据集中将男性拼写为了’male’, 而不是我们查询的’Male’。

因此,现在我可以通过忽略大小写来修复这个问题。

var res = employees
        .Where(e => e.Gender.Equals("Male", StringComparison.OrdinalIgnoreCase))
        .Take(3)
        .Where(e => e.Salary > avgSalary)
        .OrderBy(e => e.Age);
 

现在我们将得到如下结果集:

Jose Mond, 62000, 35
Peter Claus, 61000, 40

在结果集中’Jose’已经包含在内了,所以这里第一个Bug已经被修复了。但是问题是’Mike Mockson’依然没有出现在结果集里面。我们将使用后面的调试方式来解决它。

Quickwatch看似很美好,其实是有一个很大的缺点。如果你要从一个很大的数据集中找到一个指定的数据项,你可以需要花非常多的时间。

而且需要注意有些查询可能会改变应用的状态。例如,你可能在lambda表达式中,通过调用某个方法来改变一些变量的值,例如var res = source.Select(x => x.Age++)。在Quickwatch中运行这段代码,你的应用状态会被修改,调试上下文会不一致。不过在Quickwatch你可以使用添加nse这个”无副作用”标记,来避免调试上下文的变更。你可以在你的LINQ表达式后面追加, nse的后缀来启用“无副作用”标记。

例:

2. 在lambda表达式部分放置断点

另外一种非常好用的调试方式是在lambda表达式内部放置断点。这可以让你查看每个独立数据项的值。针对比较大的数据集,你可以使用条件断点。

在我们的用例中,我们发现’Mike Mockson’不在第一个Where操作结果集中。这时候我们就可以在.Where(e => e.Gender == "Male")代码部分添加一个条件断点,断点条件是e.Name=="Mike Mockson"

在我们的用例中,这个断点永远不会被触发。而且在我们将查询条件改为

.Where(e => e.Gender.Equals("Male", StringComparison.OrdinalIgnoreCase))

之后也不会触发。你知道这是为什么?

现在不要在盯着代码了,这里我们使用断点的Actions功能,这个功能允许你在断点触发时,在Output窗口中输出日志。

再次调试之后,我们会在Output窗口中得到如下结果:

只有3个人名被打印出来了。这是因为在我们的查询中使用了.Take(3), 它会让数据集只返回前3个匹配的数据项。

这里我们本来的意愿是想列出超过平均工资的前三位男性,并且按照年龄排序。所以这里我们应该把Take放到工资过滤代码的后面。

var res = employees
        .Where(e => e.Gender.Equals("Male", StringComparison.OrdinalIgnoreCase))
        .Where(e => e.Salary > avgSalary)
        .Take(3)
        .OrderBy(e => e.Age);
 

再次运行之后,结果集正确显示了Jose Mond,Peter ClausMike Mockson

注: LINQ to SQL中,这个方式不起作用。

3. 为LINQ添加日志扩展方法

现在让我们把代码还原到Bug还未修复的最初状态.

下面我们来使用扩展方法来帮助调试Query。


public static IEnumerable<T> LogLINQ<T>(this IEnumerable<T> enumerable, string logName, Func<T, string> printMethod)
{
#if DEBUG
    int count = 0;
    foreach (var item in enumerable)
    {
        if (printMethod != null)
        {
            Debug.WriteLine($"{logName}|item {count} = {printMethod(item)}");
        }
        count++;
        yield return item;
    }
    Debug.WriteLine($"{logName}|count = {count}");
#else   
    return enumerable;
#endif
}
 

你可以像这样使用你的调试方法。

var res = employees
        .LogLINQ("source", e=>e.Name)
        .Where(e => e.Gender == "Male")
        .LogLINQ("logWhere", e=>e.Name)
        .Take(3)
        .LogLINQ("logTake", e=>e.Name)
        .Where(e => e.Salary > avgSalary)
        .LogLINQ("logWhere2", e=>e.Name)
        .OrderBy(e => e.Age);
 

输出结果如下:

说明和解释:

  • LogLINQ方法需要放在你的每个查询条件后面。它会输出所有满足条件的数据项及其总数
  • logName是一个输出日志的前缀,使用它可以很容易了解到当前运行的是哪一步查询
  • Func<T, string> printMethod是一个委托,它可以帮助打印任何你指定的变量值,在上述例子中,我们打印了员工的名字
  • 为了优化代码,这个代码应该是只在调试模式使用。所以我们添加了#if DEBUG

下面我们来分析一下输出窗口的结果,你会发现这几个问题:

  • source中包含”Jose Mond”, 但是logWhere中不包含,这就是我们前面发现的大小写问题
  • “Mike Mockson”没有出现在任何结果中,原因是过早的使用Take, 过滤了许多正确的结果。

4. 使用OzCode的LINQ功能

如果你需要一个强力的工具来调试LINQ, 那么你可以使用OzCode这个Visual Studio插件。

OzCode可以提供一个可视化的LINQ查询界面来展示每一个数据项的行为。首先,它可以展示每次操作后,满足条件的所有数据项的数量。

然后呢,当你点击任何一个数字按钮的时候,你可以查看所有满足条件的数据项。

我们可以看到”Jo Parker”是源数据的第四个,经过第一个Where查询时候,变成了数据源中的第三项。这里可以看到在最后2步操作OrderByTake返回的结果集中没有这一项了,因为他已经被过滤掉了。

就调试LINQ而言,OzCode基本上已经可以满足你的所有需求了。

总结

LINQ的调试不是非常直观,但是通过一些内置和第三方组件还是可以很好调试结果。

这里我没有提到LINQ查询语法,因为它使用得并不多。只有方式#2 (lambda表达式部分放置断点)和技术#4 (OzCode)可以使用查询语法。

LINQ既适用于内存集合,也适用于数据源。直接数据源可以是SQL数据库、XML模式和web服务。但是并非所有上述技术都适用于数据源。特别是,方式#2 (lambda表达式部分放置断点)根本不起作用。方式#3(日志中间件)可以用于调试,但最好避免使用它,因为它将集合从IQueryable更改为IEnumerable。不要让LogLINQ方法用于生产数据源。方式#4 (OzCode)对于大多数LINQ提供程序都可以很好地工作,但是如果LINQ提供程序以非标准的方式工作,那么可能会有一些细微的变化。

「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!