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

7

我正在寻求如何编写查询的建议。对于每个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();

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

System.InvalidOperationException: 'Processing of the LINQ expression 'OrderBy<Task, int>(
    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: (t0) => Property<Nullable<int>>((Unhandled parameter: ti0).Outer.Inner, "Id") == Property<Nullable<int>>(t0, "GoalId"))
            PendingSelector: (t0) => NavigationTreeExpression
                Value: EntityReferenceTask
                Expression: t0
        ,
        predicate: (i) => Property<Nullable<int>>(NavigationTreeExpression
            Value: EntityReferenceGoal
            Expression: (Unhandled parameter: ti0).Outer.Inner, "Id") == Property<Nullable<int>>(i, "GoalId"))), 
    keySelector: (tt) => tt.Sequence)' 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.'

问题在于这行代码:let nextTaskId =...。如果我注释掉它,就不会出错。(但我得不到我想要的结果。)
我很容易承认我不理解错误信息的细节。我能想到的另一种方法就是返回所有的Task,然后在客户端对它们进行排序和过滤。但我的偏好是不检索我不需要的数据。
还有其他人看到这个查询的其他方法吗?
注意:我正在使用最新版本的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
    });

但这也产生了一个错误:

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.'

可以尝试使用 DbContext.Tasks.Where(t => t.GoalId != null).GroupBy(g =>g.GoalId)... - Matt.G
@EugenePodskal:你没有明确表达你对什么有点困惑。g(组)与任务(Tasks)之间是一对多的关系,而t(任务)具有一个“Sequence”列。我不明白你的替换查询如何获取我感兴趣的任务。使用SelectMany()应该与from a in DbContext.Areas from g in a.Goals from t in g.Tasks相同。 - Jonathan Wood
@EugenePodskal:哦,你的意思是我使用相同的符号?嗯,那是我为我的示例调整的部分。Lambda表达式中的“t”可能应该是其他内容。 - Jonathan Wood
@JonathanWood 尝试重写nextTaskId,例如:g.Tasks.OrderBy(tt => tt.Sequence).Min(tt => tt.Id)g.Tasks.OrderBy(tt => tt.Sequence).FirstOrDefault().Id - Canica
@jcruz:错误出现在我设置nextTaskId的那一行。注释掉那一行就可以消除错误了。 - Jonathan Wood
显示剩余7条评论
6个回答

5

这是一个需要完整可重现示例的好例子。当尝试使用类似的实体模型来复制问题时,我要么收到一个关于 DefaulIfEmpty(-1) 的不同错误(显然不支持,请记得将其删除——SQL查询将在无它的情况下正常工作),要么在删除它时没有错误。

然后我注意到你的错误消息与我的有一个微小但深藏不露的差异,这导致我找到了问题的原因:

MaterializeCollectionNavigation(Navigation: Goal.Tasks (<Tasks>k__BackingField, DbSet<Task>)

具体来说,特别是在结尾的DbSet<Task> (在我的情况下是 ICollection<Task>)。我意识到你使用了DbSet<T>类型作为集合导航属性,而不是通常的ICollection<T>IEnumerable<T>List<T>等,例如。

public class Goal
{
    // ...
    public DbSet<Task> Tasks { get; set; }
}

只是不要这样做。 DbSet<T>是一个特殊的EF Core类,只应该从DbContext中使用,表示数据库表、视图或原始SQL查询结果集。更重要的是,DbSet是唯一真正的EF Core查询,因此这种用法会使EF Core查询转换器混淆。
所以将其更改为某些受支持的接口/类(例如ICollection<Task>),原始问题就会得到解决。
然后删除DefaultIfEmpty(-1)将允许成功转换第一个问题中的查询。

我很感激这个有见地的答案。我已经将我的自定义模型更改为使用ICollection<>来代替DbSet<>作为外键字段。不幸的是,我仍然遇到了类似的错误。 - Jonathan Wood
类似但不同(新的信息是什么)?无论如何,遗憾的是,EF Core查询无法翻译的原因在具体细节中。如果没有具体的错误和复制(确切的实体模型、配置和查询+EF Core版本,因为翻译/错误/限制甚至在次要版本之间也有所不同),我们无法帮助解决具体问题(我很幸运地发现了DbSet<>这个问题)。 - Ivan Stoev
乍一看,错误看起来完全相同。但是你看到我在第一个错误中使用了DbSet,所以我认为至少有所改变。我指出我正在使用最新版本(尽管今天有更新)。对我来说,查询对于当前版本的Entity Framework来说太复杂了。 - Jonathan Wood

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();

编辑:抱歉,我对查询语法不是很熟悉,也许这个可以满足您的需求?

    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();

所以,这个程序可以无错误地运行并且很有趣。但是逻辑不完全相同。我需要每个目标的下一个任务。这只返回所有领域和目标的下一个任务。 - Jonathan Wood
@JonathanWood 哦,好的,我已经更新了我的答案,也许更符合你的要求?再次说明,我不确定 EF Core 是否能够处理它,我现在正在本地设置一个进行测试。 - Jamie Twells
我已经成功地让第二个版本运行起来了,看起来是正确的。谢谢。 - Jonathan Wood

0
我建议您首先将此查询分解为单独的部分。尝试通过使用foreach迭代Goals,并在其中添加Task逻辑。将每个新的CalendarTask添加到预先定义的列表中。
总体而言,将这个逻辑分解并进行一些实验可能会让您对Entity Framework Core的限制有所了解。

foreach中迭代目标首先会从数据库中检索出所有目标。然后,要获取每个相关任务将需要惰性加载,这意味着我需要为每个任务单独进行数据库访问,或在单个查询中预加载每个任务。在第一种情况下,所有这些与数据库的往返通信将严重影响性能。在第二种情况下,我会带来大量不需要的数据,正如我在问题中指出的那样,这是我不想做的事情。 - Jonathan Wood
如果 Task 是自己的 DbSet,你可以使用不同的查询选择所有具有 ShowAlways 的任务。这样,您的逻辑只会寻找序列中的第一个任务。然后只需合并结果即可。没有存储过程可能无法实现超级高效率。 - dcannistraro

0
我尝试创建 LINQ 请求,但不确定结果。
        var tasks =   ( from a in DbContext.Areas
                        from g in a.Goals
                        from t in g.Tasks 
                            join oneTask in (from  t in DbContext.Tasks
                            group t by t.Id into gt
                            select new {
                                  Id = gt.Key,
                                  Sequence = gt.Min(t => t.Sequence)
                              }) on  new { t.Id, t.Sequence } equals  new { oneTask.Id,oneTask.Sequence }
                            select new {Area = a, Goal = g, Task = t})
                        .Union(         
                        from a in DbContext.Areas
                        from g in a.Goals
                        from t in g.Tasks 
                            where t.ShowAlways 
                            select new {Area = a, Goal = g, Task = t});



0

我目前没有EF Core,但你真的需要这么多比较吗?

查询任务不足以满足需求吗?

如果定义了导航属性或外键,我可以想象使用类似以下代码:

Tasks.Where(task => task.Sequence == Tasks.Where(t => t.GoalIdentity == task.GoalIdentity).Min(t => t.Sequence) || task.ShowAlways);


不,那跟我所需要的差得很远。 - Jonathan Wood

0

我认为我们可以将查询分成两个步骤。首先,查询每个目标并获取最小序列任务并存储它们(可能使用匿名类型,如{NextTaskId,Goal})。然后,我们查询临时数据并获取结果。例如:

Areas.SelectMany(x=>x.Goals)
    .Select(g=>new {
        NextTaskId=g.Tasks.OrderBy(t=>t.Sequence).FirstOrDefault()?.Id,
        Tasks=g.Tasks.Where(t=>t.ShowAlways)
      })
    .SelectMany(a=>a.Tasks,(a,task)=>new {
            NextTaskId = a.NextTaskId,
            Task = task
      });

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