3

I am using Entity Framework in .NET 7.

I have 3 entities:

  1. Course that contains a ProfessorId among other things
  2. Grade that has a CourseId among other things
  3. Professor

I want to get all the courses that are assigned to a professor and have at least 1 grade associated with them and filter them in a Dictionary<string, CourseViewModel> where string is the semester.

I have written the following LINQ query:

var professorGradedCourses = _dbContext.Courses
            .Where(course => course.ProfessorId == professorId && course.Grades.Any())
            .Select(course => new CourseViewModel
            {
                Title = course.Title,
                Semester = course.Semester,
            })
            .GroupBy(course => course.Semester)
            .OrderBy(course => course.Key)
            .ToDictionary(group => group.Key, group => group.ToList());

When that executes I get an exception saying it can't be translated.

If I remove the OrderBy and keep only the GroupBy, it works and the translated SQL in Microsoft SQL Server is:

  SELECT [c].[Semester], [c].[Title]
  FROM [Courses] AS [c]
  WHERE [c].[ProfessorId] = @__professorId_0 
    AND EXISTS (SELECT 1
                FROM [Grades] AS [g]
                WHERE [c].[Id] = [g].[CourseId])
  ORDER BY [c].[Semester]

As you can see it adds ORDER BY anyway, even though I have removed it and kept only GroupBy(). Can someone explain why is that? What if I wanted to order by descending would that be possible? Also the weird thing is that if I remove GroupBy() and keep only OrderBy() and replace the ToDictionary with ToList, it works and the exact same query is produced (only now I can't really use the results without further actions).

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
StyleM
  • 138
  • 1
  • 5
  • 3
    It's always hard to answer questions on behavior of code in libraries. It's better to ask the EF team themselves. I *guess* EF needs the ordering internally for processing the SQL result sets into groups and therefore doesn't allow a custom ordering, even if it happens to be the same. – Gert Arnold Jan 31 '23 at 18:26
  • Because this is EagerLoading query. There is no sense in SQL to group and then return all items, it should be only grouping key and aggregation. Perform grouping on the client side. – Svyatoslav Danyliv Jan 31 '23 at 20:32
  • But grouping does make sense in LINQ and an ORM can choose to support it, or not. So, finally, it's supported in EFC7, as it was in EF6 (classic). EF6 even allows the OrderBy. It's all about implementation decisions. – Gert Arnold Jan 31 '23 at 20:48

2 Answers2

1

LINQ GroupBy :

Groups the elements of a sequence.

SQL GROUP BY :

A SELECT statement clause that divides the query result into groups of rows, usually by performing one or more aggregations on each group. The SELECT statement returns one row per group.

They aren't equivalent. The main difference is LINQ GroupBy return a collection by key, when SQL GROUP BY return ONE element (column) by key.

If the projection ask ONE element by key, then EF Core translate LINQ GroupBy to SQL GROUP BY :

// Get the number of course by semester
context
    .Courses
    .GroupBy(c => c.Semester)
    .Select(cs => new { Semester = cs.Key, Count = cs.Count() })
    .ToList();

Translated to :

SELECT [c].[Semester], COUNT(*) AS [Count]
FROM [Courses] AS [c]
GROUP BY [c].[Semester]

But if the projection ask several element, then EF Core translate LINQ GroupBy to SQL ORDER BY and group by itself.

context
    .Courses
    .Select(c => new { c.Id, c.Semester })
    .GroupBy(c => c.Semester)
    .ToDictionary(cs => cs.Key, cs => cs.ToList());

Translated to :

SELECT [c].[Semester], [c].[Id]
FROM [Courses] AS [c]
ORDER BY [c].[Semester]

If the result is :

Semester Id
2023 S1 1
2023 S1 4
2023 S2 2
... ...

Then EF Core read like :

  1. Read first row : Semester is "2023 S1"
    • No group
    • Then create a group and add the row in.
  2. Read second row : Semester is "2023 S1"
    • The key is the same that precedent element
    • Then Add the row in the group
  3. Read the third row : Semester is "2023 S2"
    • The key is different that precedent element
    • Then create a new group and the row in.
  4. And so on...

You understand the interest of sorting.


About the error, I don't know that EF Core can't. The query sound legit. Maybe this should not be implemented at this time.


About that you try, to convert a sorted grouping enumeration to a dictionary. This is weird because the dictionary isn't sortable. Then this sorts elements and put them in loose.

If Dictionary seem sorted, it's a coincidence, not a feature. In intern, the dictionary sort element by key's has code, that is generally the sorted order... But not every time.

If you want a sorted dictionary, you can use SortedDictyonary. But it can be tricky if you need a custom sort rule, like :

context
    .Courses
    .Select(c => new { c.Id, c.Semester })
    .GroupBy(c => c.Semester)
    .ToImmutableSortedDictionary(cs => cs.Key, cs => cs.ToList(), new ReverseComparer<string>());



public class ReverseComparer<T> : IComparer<T>
{
    private IComparer<T> _comparer = Comparer<T>.Default;

    public int Compare(T? x, T? y)
    {
        return _comparer.Compare(x, y) * -1;
    }
}
vernou
  • 6,818
  • 5
  • 30
  • 58
0

The exception you are encountering is most likely due to the fact that the OrderBy clause cannot be translated into SQL by Entity Framework. The OrderBy clause is executed in memory after the data has been retrieved from the database, which is why it works when you remove it and keep only the GroupBy clause.

However, if you want to order the dictionary by descending, you can simply call the Reverse method on the ToDictionary result:

var professorGradedCourses = _dbContext.Courses
.Where(course => course.ProfessorId == professorId && course.Grades.Any())
.Select(course => new CourseViewModel
{
    Title = course.Title,
    Semester = course.Semester,
})
.GroupBy(course => course.Semester)
.OrderByDescending(course => course.Key)
.ToDictionary(group => group.Key,
group => group.ToList())
.Reverse();

This way, the dictionary will be sorted in descending order based on the semester.

Give this a try and let me know how it works for you.

EDIT:

Converting the IEnumerable back to a Dictionary should work like this:

var professorGradedCourses = _dbContext.Courses
.Where(course => course.ProfessorId == professorId && course.Grades.Any())
.Select(course => new CourseViewModel
{
    Title = course.Title,
    Semester = course.Semester,
})
.GroupBy(course => course.Semester)
.OrderByDescending(course => course.Key)
.ToDictionary(group => group.Key,
group => group.ToList())
.Reverse()
.ToDictionary(pair => pair.Key,
pair => pair.Value);
Corey Sutton
  • 767
  • 5
  • 19
  • Thanks for your answer! Reverse works, but instead of a Dictionary it returns an IEnumerable>>. Can I get the dictionary back? For the other thing you said about OrderBy I don't think it's impossible to be translated, because if I replace GroupBy with OrderBy it works and generates the exact same query (only the result is not in the format I want). I forgot to mention that in the question, so I added it now – StyleM Jan 31 '23 at 17:54
  • 1
    you can convert the IEnumerable>> back to a Dictionary> using the ToDictionary method again. I will update my answer with more code. – Corey Sutton Jan 31 '23 at 19:55