LINQ 查询问题:从每个目标中选择第一个任务

Jon*_*ood 7 c# entity-framework entity-framework-core asp.net-core

我正在寻找有关如何编写查询的建议。对于每一个Goal,我想选择第一Task(排序Task.Sequence),除了与任何任务ShowAlways == true。(我的实际查询更复杂,但此查询演示了我遇到的限制。)

我试过这样的事情:

var tasks = (from a in DbContext.Areas
             from g in a.Goals
             from t in g.Tasks
             let nextTaskId = g.Tasks.OrderBy(tt => tt.Sequence).Select(tt => tt.Id).DefaultIfEmpty(-1).FirstOrDefault()
             where t.ShowAlways || t.Id == nextTaskId
             select new CalendarTask
             {

                 // Member assignment

             }).ToList();
Run Code Online (Sandbox Code Playgroud)

但是这个查询似乎太复杂了。

var tasks = (from a in DbContext.Areas
             from g in a.Goals
             from t in g.Tasks
             let nextTaskId = g.Tasks.OrderBy(tt => tt.Sequence).Select(tt => tt.Id).DefaultIfEmpty(-1).FirstOrDefault()
             where t.ShowAlways || t.Id == nextTaskId
             select new CalendarTask
             {

                 // Member assignment

             }).ToList();
Run Code Online (Sandbox Code Playgroud)

问题是线路let nextTaskId =...。如果我注释掉,就没有错误。(但我不明白我想要什么。)

我很乐意承认我不了解错误消息的详细信息。关于我能想到的解决此问题的唯一其他方法是返回所有Tasks,然后在客户端对它们进行排序和过滤。但我的偏好是不检索我不需要的数据。

任何人都可以看到任何其他方法来处理此查询吗?

注意:我使用的是最新版本的 Visual Studio 和 .NET。

更新:

我尝试了一种不同但效率较低的方法来处理此查询。

var tasks = (DbContext.Areas
      .Where(a => a.UserId == UserManager.GetUserId(User) && !a.OnHold)
      .SelectMany(a => a.Goals)
      .Where(g => !g.OnHold)
      .Select(g => g.Tasks.Where(tt => !tt.OnHold && !tt.Completed).OrderBy(tt => tt.Sequence).FirstOrDefault()))
    .Union(DbContext.Areas
      .Where(a => a.UserId == UserManager.GetUserId(User) && !a.OnHold)
      .SelectMany(a => a.Goals)
      .Where(g => !g.OnHold)
      .Select(g => g.Tasks.Where(tt => !tt.OnHold && !tt.Completed && (tt.DueDate.HasValue || tt.AlwaysShow)).OrderBy(tt => tt.Sequence).FirstOrDefault()))
    .Distinct()
    .Select(t => new CalendarTask
    {
        Id = t.Id,
        Title = t.Title,
        Goal = t.Goal.Title,
        CssClass = t.Goal.Area.CssClass,
        DueDate = t.DueDate,
        Completed = t.Completed
    });
Run Code Online (Sandbox Code Playgroud)

但这也产生了一个错误:

System.InvalidOperationException: 'Processing of the LINQ expression 'Where<Task>(
    source: MaterializeCollectionNavigation(Navigation: Goal.Tasks (<Tasks>k__BackingField, DbSet<Task>) Collection ToDependent Task Inverse: Goal, Where<Task>(
        source: NavigationExpansionExpression
            Source: Where<Task>(
                source: DbSet<Task>, 
                predicate: (t) => Property<Nullable<int>>((Unhandled parameter: ti).Inner, "Id") == Property<Nullable<int>>(t, "GoalId"))
            PendingSelector: (t) => NavigationTreeExpression
                Value: EntityReferenceTask
                Expression: t
        , 
        predicate: (i) => Property<Nullable<int>>(NavigationTreeExpression
            Value: EntityReferenceGoal
            Expression: (Unhandled parameter: ti).Inner, "Id") == Property<Nullable<int>>(i, "GoalId"))), 
    predicate: (tt) => !(tt.OnHold) && !(tt.Completed))' by 'NavigationExpandingExpressionVisitor' failed. This may indicate either a bug or a limitation in EF Core. See https://go.microsoft.com/fwlink/?linkid=2101433 for more detailed information.'
Run Code Online (Sandbox Code Playgroud)

Iva*_*oev 5

这是一个很好的例子,需要完全可重现的例子。当尝试使用类似的实体模型重现该问题时,我要么得到一个不同的错误DefaulIfEmpty(-1)(显然不支持,不要忘记删除它 - SQL 查询将在没有它的情况下正常工作)或在删除它时没有错误。

然后我注意到与我的错误消息相比,您的错误消息中有一个深藏不露的小差异,这使我找到了问题的原因:

MaterializeCollectionNavigation(Navigation: Goal.Tasks (<Tasks>k__BackingField, DbSet<Task>)
Run Code Online (Sandbox Code Playgroud)

特别是DbSet<Task>在最后(在我的情况下是ICollection<Task>)。我意识到,你所使用DbSet<T>类型的集合导航属性,而不是通常的ICollection<T>IEnumerable<T>List<T>等,如

public class Goal
{
    // ...
    public DbSet<Task> Tasks { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

不要那样做。DbSet<T>是一个特殊的 EF Core 类,应该仅DbContext用于表示 db 表、视图或原始 SQL 查询结果集。更重要DbSet的是,s 是唯一真正的 EF Core 查询,因此这种用法会混淆 EF Core 查询转换器也就不足为奇了。

所以把它改成一些支持的接口/类(例如, ICollection<Task>),原来的问题就解决了。

然后删除DefaultIfEmpty(-1)将允许成功翻译有问题的第一个查询。


Jam*_*lls 3

我没有启动并运行 EF Core,但是你能像这样拆分它吗?

    var allTasks = DbContext.Areas
        .SelectMany(a => a.Goals)
        .SelectMany(a => a.Tasks);

    var always = allTasks.Where(t => t.ShowAlways);

    var next = allTasks
        .OrderBy(tt => tt.Sequence)
        .Take(1);

    var result = always
        .Concat(next)
        .Select(t => new
         {
             // Member assignment
         })
        .ToList();
Run Code Online (Sandbox Code Playgroud)

编辑:抱歉,我不太擅长查询语法,也许这可以满足您的需要?

    var allGoals = DbContext.Areas
        .SelectMany(a => a.Goals);

    var allTasks = DbContext.Areas
        .SelectMany(a => a.Goals)
        .SelectMany(a => a.Tasks);

    var always = allGoals
        .SelectMany(a => a.Tasks)
        .Where(t => t.ShowAlways);

    var nextTasks = allGoals
        .SelectMany(g => g.Tasks.OrderBy(tt => tt.Sequence).Take(1));

    var result = always
        .Concat(nextTasks)
        .Select(t => new
         {
             // Member assignment
         })
        .ToList();
Run Code Online (Sandbox Code Playgroud)