在使用查询语法时,是否有一种简洁的方法在LINQ查询中执行ToList操作?

12

考虑下面的代码:

StockcheckJobs = 
     (from job in (from stockcheckItem in MDC.StockcheckItems
                   where distinctJobs.Contains(stockcheckItem.JobId)
                   group stockcheckItem by new { stockcheckItem.JobId, stockcheckItem.JobData.EngineerId } into jobs
                   select jobs).ToList()
      let date = MJM.GetOrCreateJobData(job.Key.JobId).CompletedJob.Value
      orderby date descending 
      select new StockcheckJobsModel.StockcheckJob()
      {
          JobId = job.Key.JobId,
          Date = date,
          Engineer = (EngineerModel)job.Key.EngineerId,
          MatchingLines = job.Count(sti => sti.Quantity == sti.ExpectedQuantity),
          DifferingLines = job.Count(sti => sti.Quantity != sti.ExpectedQuantity)
      }).ToList()

由于GetOrCreateJobData方法无法转换成SQL,因此在中间出现了ToList()

因此,我不得不将查询的第一部分用括号括起来,然后使用外部查询来完成剩余部分。

我知道我可以将其拆分为两个变量,但我不想这样做(这是在对象初始化器中完成的)。

当我需要在linq查询中间执行ToList(或以其他方式转到linq-to-objects)时,是否有其他语法可以增加可读性,最好消除内部和外部查询的需求?


在理想的情况下,我希望像这样(尽可能接近):

StockcheckJobs =
     from stockcheckItem in MDC.StockcheckItems
     where distinctJobs.Contains(stockcheckItem.JobId)
     group stockcheckItem by new { stockcheckItem.JobId, stockcheckItem.JobData.EngineerId } into jobs
     MAGIC_DO_BELOW_AS_LINQ-TO-OBJECTS_KEYWORD_OR_SYNTAX
     let date = MJM.GetOrCreateJobData(jobs.Key.JobId).CompletedJob.Value
     orderby date descending 
     select new StockcheckJobsModel.StockcheckJob()
     {
         JobId = jobs.Key.JobId,
         Date = date,
         Engineer = new ThreeSixtyScheduling.Models.EngineerModel() { Number = jobs.Key.EngineerId },
         MatchingLines = jobs.Count(sti => sti.Quantity == sti.ExpectedQuantity),
         DifferingLines = jobs.Count(sti => sti.Quantity != sti.ExpectedQuantity)
     };

2
我知道我可以将其拆分为两个变量,但我不想这样做。不过,这可能是一个好主意。在一行上尝试做太多的事情会导致问题。 - Servy
MDC.StockcheckItems.AsEnumerable()会有帮助吗?可以强制将“source”查询为Linq-To-Objects吗? - Jeppe Stig Nielsen
@GeorgeDuckett 我不是指一个文本行,而是一个可执行行。无论如何,既然你对可读性感兴趣,我认为如果你要执行两个不同的查询,将每次访问数据库分开会更有益,而不是在单个赋值上进行外部/内部查询,从而导致两个不同的数据库查询。 - Servy
在一个可执行行上做太多的事情会使堆栈跟踪的行号不是很有用。祝你在那段代码中调试NullReferenceException好运。 - Jesse Webb
不,没有办法。所有的LINQ查询语法都会产生IQueryable<T>IEnumerable<T>的结果。你必须调用扩展方法才能得到类似List<T>的东西。这是本地代码和数据库代码并存的副作用...你必须在某个地方承受代价。 - Peter Ritchie
显示剩余7条评论
4个回答

5
您可以通过实现指定方法调用表达式的自定义查询翻译器来解决无法将GetOrCreateJobData翻译成SQL的问题。这样,您就可以控制LINQ-to-SQL如何解释该方法。有一篇好的文章解释了这个过程,并提供了相关资源链接,请参考:http://www.codeproject.com/Articles/32968/QueryMap-Custom-translation-of-LINQ-expressions 另外,您可以将GetOrCreateJobData方法重构为扩展方法,使用表达式构建相同的逻辑,以便LINQ-to-SQL可以自然地解释它。根据方法的复杂性,这可能比我的第一个建议更可行或不可行。

只要 GetOrCreateJobData 真的涉及数据库,这是最好的想法。 - Jodrell
非常好的链接,谢谢提供这个信息。我一直使用两个属性,一个返回表达式,另一个返回实际值。很高兴知道有一种方法可以将两者结合起来。 - George Duckett

3

我发现使用方法语法可以使事情更清晰,但这只是个人偏好。它确实使查询的上半部分更好,但在方法语法中使用let虽然也可行,但需要更多的工作。

var result = stockcheckItem in MDC.StockcheckItems
    .Where(item => distinctJobs.Contains(item.JobId))
    .GroupBy(item => new { item.JobId, item.JobData.EngineerId })
    .AsEnumerable() //switch from Linq-to-sql to Linq-to-objects
    .Select(job => new StockcheckJobsModel.StockcheckJob()
    {
        JobId = job.Key.JobId,
        Date = MJM.GetOrCreateJobData(job.Key.JobId).CompletedJob.Value,
        Engineer = (EngineerModel)job.Key.EngineerId,
        MatchingLines = job.Count(sti => sti.Quantity == sti.ExpectedQuantity),
        DifferingLines = job.Count(sti => sti.Quantity != sti.ExpectedQuantity)
    })
    .Orderby(item => item.Date)
    .ToList()

我必须说,我有点更喜欢查询语法(仅因为少了一些“item =>”和“let”),但这看起来相当易读。与查询语法相比,唯一的劣势是会有一个很长的断点。不过,还是加一分。 - George Duckett
@GeorgeDuckett,你明确表示希望它在一行上。我仍然支持将其分成多行。此外,请注意,虽然有许多item =>,但查询语法中也存在很多冗余。例如,有很多身份选择,并且group by所需的语法比这个更多。 - Servy
在这个阶段,您可以通过将“OrderBy”移动到最终的“Select”之后并在该“Select”中初始化日期来剪切匿名对象和所有“item.”。 (还有您的“item”和“stockcheckItem”不一致。)(但+1因为我喜欢方法语法。) - Rawling
只是一个概念性的问题。我只是指出这种解决方案相对于(可能不存在的)理想解决方案/语法的劣势。 - George Duckett

3
我有两点要提出来回答这个问题:
  1. 我真的不认为在这里引入一个额外的变量会有任何可读性问题。事实上,我认为它使代码更易读,因为它将“本地执行”的代码与在数据库上执行的代码分开。
  2. 要简单地切换到LINQ-To-Objects,则首选使用AsEnumerable而不是ToList
话虽如此,以下是您如何始终保持查询状态而无需在整个查询表达式上使用中间的AsEnumerable() / ToList()的方法:通过欺骗C#编译器使用您的自定义扩展方法而不是BCL。这是可能的,因为C#使用“基于模式”的方法(而不是与BCL耦合)将查询表达式转换为方法调用和Lambda表达式。
声明这样的恶意类:
public static class To
{
    public sealed class ToList { }

    public static readonly ToList List;

    // C# should target this method when you use "select To.List"
    // inside a query expression.
    public static List<T> Select<T>
        (this IEnumerable<T> source, Func<T, ToList> projector)
    {
        return source.ToList();
    }
}

public static class As
{
    public sealed class AsEnumerable { }

    public static readonly AsEnumerable Enumerable;

    // C# should target this method when you use "select As.Enumerable"
    // inside a query expression.
    public static IEnumerable<T> Select<T>
        (this IEnumerable<T> source, Func<T, AsEnumerable> projector)
    {
        return source;
    }
}

然后你可以编写如下的查询:

List<int> list = from num in new[] { 41 }.AsQueryable()
                 select num + 1 into result
                 select To.List;

IEnumerable<int> seq = from num in new[] { 41 }.AsQueryable()
                       select num + 1 into result
                       select As.Enumerable into seqItem
                       select seqItem + 1; // Subsequent processing

在您的情况下,您的查询将变为:
StockcheckJobs =
     from stockcheckItem in MDC.StockcheckItems
     where distinctJobs.Contains(stockcheckItem.JobId)
     group stockcheckItem by new { stockcheckItem.JobId, stockcheckItem.JobData.EngineerId } into jobs
     select As.Enumerable into localJobs // MAGIC!
     let date = MJM.GetOrCreateJobData(localJobs.Key.JobId).CompletedJob.Value
     orderby date descending 
     select new StockcheckJobsModel.StockcheckJob()
     {
         JobId = localJobs.Key.JobId,
         Date = date,
         Engineer = new ThreeSixtyScheduling.Models.EngineerModel() { Number = localJobs.Key.EngineerId },
         MatchingLines = localJobs.Count(sti => sti.Quantity == sti.ExpectedQuantity),
         DifferingLines = localJobs.Count(sti => sti.Quantity != sti.ExpectedQuantity)
     };

我并不认为这是任何改进,相反,它是对语言特性的滥用。


0

一种选择是将所有与SQL兼容的工作提前转换为匿名类型。

var jobs = 
     (from job in (from stockcheckItem in MDC.StockcheckItems
        where distinctJobs.Contains(stockcheckItem.JobId)
        group stockcheckItem by new 
             { stockcheckItem.JobId, stockcheckItem.JobData.EngineerId } 
         into jobs
        select new 
             {
                JobId = job.Key.JobId,
                Engineer = (EngineerModel)job.Key.EngineerId,
                MatchingLines = 
                    job.Count(sti => sti.Quantity == sti.ExpectedQuantity),
                DifferingLines = 
                    job.Count(sti => sti.Quantity != sti.ExpectedQuantity)
             }
      ).AsEnumerable()

StockcheckJobs = jobs.Select(j => new StockcheckJobsModel.StockcheckJob
    {
         JobId = j.JobId,
         Date = MJM.GetOrCreateJobData(j.JobId).CompletedJob.Value,
         Engineer = j.EngineerId,
         MatchingLines = j.MatchingLines,
         DifferingLines = j.DifferingLines
    }).OrderBy(j => j.Date).ToList();

显然没有经过测试,但你能理解这个想法。


@GeorgeDuckett,修复了排序方式。 - Jodrell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接