0

Consider the following query that must return a director along with counts of movies and actors for each movie. How would one write this in LINQ using EF CORE 2.2? I need the LINQ to generate SQL that actually uses the GROUP BY with the SQL aggregates as supported in EF CORE 2.1 and forward.

SELECT DirectorName, COUNT(m.MovieID), COUNT(a.ActorID)
FROM Directors d
LEFT OUTER JOIN Movies AS m ON m.DirectorID = d.DirectorID
LEFT OUTER JOIN Actors AS a ON a.MovieID = m.MovieID
WHERE d.DirectorID = 1
GROUP BY DirectorName

The sample LINQ query was built based on an answer and does give me results, but the query only has one of the LEFT joins and no group by. Output indicates that DefaultIfEmpty(), GroupBy, and Count could not be translated and will be evaluated locally.

var results =
(
    from d in _moviesContext.Directors
    join m in _moviesContext.Movies on d.DirectorId equals m.DirectorId 
        into grpM from movies in grpM.DefaultIfEmpty()
    join a in _moviesContext.Actors on movies.MovieId equals a.MovieId
        into grpA from actors in grpA.DefaultIfEmpty()
    where d.DirectorId == 1
    group new { d, grpM, grpA } by new
    {
        d.DirectorName
    } into grp
    select new
    {
        DirectoryName = grp.Key.DirectorName,
        MovieCount = grp.Sum(g => g.grpM.Count()),
        ActorAcount = grp.Sum(g => g.grpA.Count())
    }
).ToList();

The LINQ expression 'DefaultIfEmpty()' could not be translated and will be evaluated locally. Microsoft.EntityFrameworkCore.Query:Warning: The LINQ expression 'DefaultIfEmpty()' could not be translated and will be evaluated locally. Microsoft.EntityFrameworkCore.Query:Warning: The LINQ expression 'from Movies movies in {[grpM] => DefaultIfEmpty()}' could not be translated and will be evaluated locally. Microsoft.EntityFrameworkCore.Query:Warning: The LINQ expression 'GroupBy(new <>f__AnonymousType81(DirectorName = [d].DirectorName), new <>f__AnonymousType62(d = [d], grpM = [grpM]))' could not be translated and will be evaluated locally. Microsoft.EntityFrameworkCore.Query:Warning: The LINQ expression 'DefaultIfEmpty()' could not be translated and will be evaluated locally. Microsoft.EntityFrameworkCore.Query:Warning: The LINQ expression 'DefaultIfEmpty()' could not be translated and will be evaluated locally. Microsoft.EntityFrameworkCore.Query:Warning: The LINQ expression 'from Movies movies in {[grpM] => DefaultIfEmpty()}' could not be translated and will be evaluated locally. Microsoft.EntityFrameworkCore.Query:Warning: The LINQ expression 'GroupBy(new <>f__AnonymousType81(DirectorName = [d].DirectorName), new <>f__AnonymousType62(d = [d], grpM = [grpM]))' could not be translated and will be evaluated locally. Microsoft.EntityFrameworkCore.Query:Warning: The LINQ expression 'Count()' could not be translated and will be evaluated locally. Microsoft.EntityFrameworkCore.Query:Warning: The LINQ expression 'Sum()' could not be translated and will be evaluated locally.

Here are the models

public partial class Directors
{
    public int DirectorId { get; set; }
    public string DirectorName { get; set; }
}

public partial class Movies
{
    public int MovieId { get; set; }
    public string MovieName { get; set; }
    public int? DirectorId { get; set; }
}

public partial class Actors
{
    public int ActorId { get; set; }
    public string ActorName { get; set; }
    public int? MovieId { get; set; }
}
Geekn
  • 2,650
  • 5
  • 40
  • 80
  • Possible duplicate of [LEFT OUTER JOIN in LINQ](https://stackoverflow.com/questions/3404975/left-outer-join-in-linq) – Ryan Wilson Feb 28 '19 at 17:54
  • 1
    @Geekn Please include the entity model used in the query. Otherwise there is nothing we can do with your code. – Ivan Stoev Feb 28 '19 at 19:16
  • Sorry...I knew that original post was a bit much. I've updated it with, what I think, to be a simpler question. – Geekn Feb 28 '19 at 19:28
  • 1
    The original post at least showed your own effort, which revealed that you fell into the infamous trap of translating SQL joins into LINQ joins. One hint: use navigation properties and it'll all be much easier. – Gert Arnold Feb 28 '19 at 19:59
  • Perhaps my [SQL to LINQ Recipe](https://stackoverflow.com/questions/49245160/sql-to-linq-with-multiple-join-count-and-left-join/49245786#49245786) might help you? – NetMage Feb 28 '19 at 22:26
  • 1
    It's possible to compose LINQ query which translates close to your SQL query. But note that SQL `COUNT(MovieId)` is not what you probably think - it's basically `COUNT(*)` ignoring `NULL` values from the outer join. But the join to Actors (1 to many) will count the movie multiple times, hence the `MovieCount` is invalid. What you probably need here is SQL `COUNT(DISTINCT MovieId)`, but it has no LINQ equivalent, hence no EF Core translation. In general EF Core is not ready yet for complex grouping/aggregating queries. – Ivan Stoev Mar 01 '19 at 09:05
  • Any chance you could help with that. I can get all LEFT OUTER joins working and I can get GROUP BY working. But I can't get both working together : ( – Geekn Mar 01 '19 at 17:46

1 Answers1

0

I'm not sure why your Actors only have an int for Movie Id when an Actor usually has a list of Movies but I believe you need to include all three object types in your group and then select distinct movies and distinct actors to get the counts.

Example:

var directors = new[] { new { DirectorName = "Director A", DirectorID = 1 },
                        new { DirectorName = "Director B", DirectorID = 2 }};
var movies = new[] { new { MovieName = "Movie A", MovieID = 1, DirectorID = 1 },
                     new { MovieName = "Movie B", MovieID = 2, DirectorID = 2 }};
var actors = new[] { new { ActorName = "Actor A", ActorID = 1, MovieID = 1},
                     new { ActorName = "Actor B", ActorID = 2, MovieID = 1},
                     new { ActorName = "Actor C", ActorID = 3, MovieID = 1},
                     new { ActorName = "Actor D", ActorID = 4, MovieID = 2}};

var results = from d in directors
              from m in movies
                .Where(m => m.DirectorID == d.DirectorID)
              from a in actors
                .Where(a => a.MovieID == m.MovieID)
              where d.DirectorID == 1
              group new { d, m, a } by d.DirectorName into grp
              select new 
              { DirectorName = grp.Key,
                MovieCount = grp.Select(x => x.m).Distinct().Count(),
                ActorCount = grp.Select(x => x.a).Distinct().Count()
              };

Will yield

result = new [] { new { DirectorName = "Director A", MovieCount = 1, ActorCount = 3}};
  • That does give me the results, but, when I use that approach, the SELECT statement contains all columns from all tables which isn't really what I need. I need the SQL output to use the COUNT() based on the GROUP by clause. Here is the output from that statement. SELECT [d].[DirectorID], [d].[DirectorName], [m].[MovieID], [m].[DirectorID], … FROM [Directors] AS [d] INNER JOIN [Movies] AS [m] ON [d].[DirectorID] = [m].[DirectorID] INNER JOIN [Actors] AS [a] ON [m].[MovieID] = [a].[MovieID] WHERE [d].[DirectorID] = 1 ORDER BY [d].[DirectorName] – Geekn Feb 28 '19 at 21:11
  • What is your end goal? Are you trying to use Linq to generate SQL? I based my answer on how one could write "SELECT DirectorName, COUNT(m.MovieID), COUNT(a.ActorID)" in Linq. In the end, you get an collection of director names and counts of their movies and actors. – DividedByZer0 Feb 28 '19 at 22:35
  • You are correct that is provides the answer, but I need the LINQ to generate the SQL from the question. I really would like the generated SQL to only return the data needed for display (not all data from all tables involved in group by if that makes any sense. EF CORE 2.1 gave support for translating LINQ to real GROUP BY clause and allows for the use of SQL aggregates such as SUM, AVG, etc to be executed on SQL. That's really the goal here. – Geekn Feb 28 '19 at 23:02