Search code examples
linqgroup-bylinq-to-entitiesdatediffef-core-2.2

EFCore 2.2 GroupBy Sum and DateDiff


I'm trying to translate the following SQL in to an EF Core query and I'm getting warnings that GroupBy and Sum will be evaluated locally. Is there anyway currently to write this that it will fully translate to SQL?

SELECT UserId, ST.StatusId, SUM(DATEDIFF(MINUTE, StartDate, ISNULL(EndDate,GETDATE()))) AS Time
FROM StatusTransaction ST
WHERE
    TeamManagerId = 1
    AND StartDate >= N'01-01-2019'
    AND ISNULL(EndDate,GETDATE()) <= N'01-02-2019'
GROUP BY UserId, ST.StatusId
ORDER BY UserId

And these are the EF queries I've used:

var efFunction = await context
    .Where(st => st.TeamManagerId == tmId && st.StartDate >= dateFrom && (st.EndDate ?? DateTime.Now) <= dateTo)
    .GroupBy(st => new { st.UserId, st.StatusId })
    .Select(g => new
        {
            g.Key.UserId,
            g.Key.StatusId,
            Time = g.Sum(st => Microsoft.EntityFrameworkCore.EF.Functions.DateDiffMinute(st.StartDate, st.EndDate)) // null check not done on end date - (st.EndDate ?? DateTime.Now) causes an error here
        }).ToListAsync(cancellationToken).ConfigureAwait(false);

var simpleDateSubtraction = await context
    .Where(st => st.TeamManagerId == tmId && st.StartDate >= dateFrom && (st.EndDate ?? DateTime.Now) <= dateTo)
    .GroupBy(st => new { st.UserId, st.StatusId })
    .Select(g => new
    {
        g.Key.UserId,
        g.Key.StatusId,
        Time = g.Sum(st => st.EndDate.Value.Subtract(st.StartDate).Minutes)// null check not done on end date - (st.EndDate ?? DateTime.Now) causes an error here
    }).ToListAsync(cancellationToken).ConfigureAwait(false);

var groupBySimpleSum = await context
    .Where(st => st.TeamManagerId == tmId)
    .GroupBy(st => new { st.TeamManagerId, st.OperationsManagerId })
    .Select(g => new
    {
        g.Key.OperationsManagerId,
        g.Key.TeamManagerId,
        Foo = g.Sum(st => st.UserId) // nonsense but a simple column to sum, this translates fully to SQL
    }).ToListAsync(cancellationToken).ConfigureAwait(false);

Solution

  • First, EF Core still doesn't support translating TimeSpan operations, and DateTime difference produces TimeSpan, hence EF.Functions.DateDiff methods are the right way to go.

    Second, it still can translate GroupBy aggregates only on simple member accessor expressions. So you have to either pre Select the GroupBy expressions:

    var query = context
        .Where(st => st.TeamManagerId == tmId
            && st.StartDate >= dateFrom
            && (st.EndDate ?? DateTime.Now) <= dateTo
        )
        .Select(st => new
        {
            st.UserId,
            st.StatusId,
            Time = EF.Functions.DateDiffMinute(st.StartDate, st.EndDate ?? DateTime.Now)
        })
        .GroupBy(st => new { st.UserId, st.StatusId })
        .Select(g => new
        {
            g.Key.UserId,
            g.Key.StatusId,
            Time = g.Sum(st => st.Time)
        });
    

    or use the GroupBy overload which allows pre selecting the source for the aggregates:

    var query = context
        .Where(st => st.TeamManagerId == tmId
            && st.StartDate >= dateFrom
            && (st.EndDate ?? DateTime.Now) <= dateTo
        )
        .GroupBy(st => new { st.UserId, st.StatusId }, st => new
        {
            Time = EF.Functions.DateDiffMinute(st.StartDate, st.EndDate ?? DateTime.Now)
        })
        .Select(g => new
        {
            g.Key.UserId,
            g.Key.StatusId,
            Time = g.Sum(st => st.Time)
        });