views:

36

answers:

2

I have two tables, Tasks and TaskMilestones. I want a query to return the most recent past milestone and the nearest future TaskMilestone for each Task. I'm working with C#/LINQ-to-SQL. How do I go about this?

Task columns: Id, TaskName TaskMilestones columns: Id, TaskId, MilestoneName, MilestoneDate

I want a return table with rows containing: TaskName, MilestoneDate, MilestoneName

My current solution causes Linq to query the database once for each Task, which is unacceptably slow.

[EDIT to address comments] The current implementation is simple and not a single statement, it just queries the list of Tasks and then queries for each TaskId twice with proper where clauses:

var x = from p in this.Database.Task
        join pm in this.Database.TaskMilestones on p.Id equals pm.TaskId
        select new
        {
            TaskId = p.Id,
            TaskName = p.Name,
            MilestoneName = m.Name,
            MilestoneDate = pm.MilestoneDate,
        };

foreach (var record in records)
{
    var y = x.Where(p => p.TaskId == record.Id && p.MilestoneDate <= dt);
    var z = x.Where(p => p.TaskId == record.Id && p.MilestoneDate > dt);

    ...
A: 

Off the top of my head:

var q = from t in Context.Tasks
        let mostRecentDate = t.Milestones.Where(m => m.MilestoneDate < DateTime.Now)
                                         .Max(m => m.MilestoneDate)
        select new 
        {
            TaskId = t.Id,
            TaskName = t.TaskName,
            RecentPastMilestone = t.Milestones.Where(m => m.MilestoneDate == mostRecentDate)
                                              .FirstOrDefault()
        };
Craig Stuntz
@Craig: I tried that just now, but it causes Linq to generate multiple SQL queries. One of these per Task:SELECT TOP (1) [t0].[Id], [t0].[NeedDate], [t0].[Duration], [t0].[IsShown], [t0].[ProgramId], [t0].[MilestonesId], [t0].[StatusId], [t0].[CreatedOn], [t0].[ModifiedOn], [t0].[CreatedBy], [t0].[ModifiedBy], [t0].[ActualCompletion]FROM [ProgramsMilestones] AS [t0]WHERE ([t0].[NeedDate] = @x2) AND ([t0].[ProgramId] = @x1)GO
Scott Stafford
Odd. It doesn't in L2E. Although it will still be a bit faster than what you have, it's not ideal. I'll think about it.
Craig Stuntz
You could try combining the `let` into the `select` part. I don't really know L2S's exact rules for SQL generation, but it's worth a shot.
Craig Stuntz
@Craig: FYI: My test was to paste what you had into LinqPad and execute. Thanks for the help!
Scott Stafford
@Craig: The multiple queries was apparently coming from LinqPad populating the Milestone with each record. When I changed that to .FirstOrDefault().Id it stopped multi-querying... I think I can just rejoin that to the main table...
Scott Stafford
+1  A: 
DateTime dt = DateTime.Today;

var records =
  from p in db.Tasks
  let pastMilestone = p.TaskMilestones
    .Where(pm => pm.MilestoneDate <= dt)
    .OrderByDescending(pm => pm.MilestoneDate)
    .FirstOrDefault()
  let nextMilestone = p.TaskMilestones
    .Where(pm => pm.MilestoneDate > dt)
    .OrderBy(pm => pm.MilestoneDate)
    .FirstOrDefault()
  select new
  {
    Task = p,
    PastMilestone = pastMilestone,
    NextMilestone = nextMilestone
  }

Another option is to load all of the milestones for each project, then filter them using LinqToObjects later:

DataLoadOptions dlo = new DataLoadOptions();
dlo.LoadWith<Task>(p => p.TaskMilestones);
db.LoadOptions = dlo;

var records = db.Tasks;

foreach(Task record in records)
{
  TaskMilestone pastMilestone = record.TaskMilestones
        .Where(pm => pm.MilestoneDate <= dt)
        .OrderByDescending(pm => pm.MilestoneDate)
        .FirstOrDefault()
  TaskMilestone nextMilestone = record.TaskMilestones
        .Where(pm => pm.MilestoneDate > dt)
        .OrderBy(pm => pm.MilestoneDate)
        .FirstOrDefault()
}
David B
@DavidB: Thanks, the first one worked well. I changed the PastMilestone field to be several individual fields, but otherwise used verbatim.
Scott Stafford