I have a ASP.NET Core 2.2 project with EF Core 2.2 Code-First DB. I have the following entities:
What if you didn't use EF Navigations properties but used manual joins with LINQ to EF?
var ans2 = (from b in dbContext.Buildings
join f in dbContext.Floors on b.Id equals f.BuildingId into fj
from f in fj.DefaultIfEmpty()
join r in dbContext.Rooms on f.Id equals r.FloorId into rj
from r in rj.DefaultIfEmpty()
join ro in dbContext.RoomOccupancies on r.Id equals ro.RoomId
join w in dbContext.WorkGroups on ro.WorkGroupId equals w.Id into wj
from w in wj.DefaultIfEmpty()
where !w.IsFinished && w.StartDate < DateTime.Now
select new BuildingDatableElementDTO() {
BuildingId = b.Id,
Name = b.Name,
FloorCount = fj.Count(),
RoomCount = rj.Count(),
CurrentWorkerCount = wj.Sum(w => w.NumberOfEmployees)
})
.ToList();
I read that there are problems with the aggregates with EF Core 2.1, but I think it shouldn't be a hard task for the ORM to translate this Projection into one query.
You are right that EF Core had (and still have - the latest at this time v2.2) problems translating GroupBy
and aggregates (and not only). But not for "shouldn't be a hard task" - try converting arbitrary expression tree to pseudo SQL yourself and you'll quickly find that it is quite complicated task.
Anyway, EF Core query translation improves over the time, but as mentioned, is far from perfect. The showstopper in this case are nested aggregates - sum of sum/count etc. The solution is to flatten the target set and apply single aggregate. For instance, rewriting your LINQ query as follows:
dbContext.Buildings.Select(b => new //BuildingDatableElementDTO()
{
BuildingId = b.Id,
Name = b.Name,
FloorCount = b.Floors.Count(),
// (1)
RoomCount = b.Floors.SelectMany(f => f.Rooms).Count(),
// (2)
CurrentWorkerCount = b.Floors
.SelectMany(f => f.Rooms)
.SelectMany(r => r.RoomOccupancies)
.Select(o => o.WorkGroup)
.Where(w => !w.IsFinished && w.StartDate < DateTime.Now)
.Sum(w => w.NumberOfEmployees),
})
.ToList();
is translated to a single SQL (as expected):
SELECT [e].[Id] AS [BuildingId], [e].[Name], (
SELECT COUNT(*)
FROM [Floors] AS [e0]
WHERE ([e0].[IsDeleted] = 0) AND ([e].[Id] = [e0].[BuildingId])
) AS [FloorCount], (
SELECT COUNT(*)
FROM [Floors] AS [e1]
INNER JOIN (
SELECT [e2].[Id], [e2].[FloorId], [e2].[IsDeleted], [e2].[Name]
FROM [Rooms] AS [e2]
WHERE [e2].[IsDeleted] = 0
) AS [t] ON [e1].[Id] = [t].[FloorId]
WHERE ([e1].[IsDeleted] = 0) AND ([e].[Id] = [e1].[BuildingId])
) AS [RoomCount], (
SELECT SUM([f.Rooms.RoomOccupancies.WorkGroup].[NumberOfEmployees])
FROM [Floors] AS [e3]
INNER JOIN (
SELECT [e4].*
FROM [Rooms] AS [e4]
WHERE [e4].[IsDeleted] = 0
) AS [t0] ON [e3].[Id] = [t0].[FloorId]
INNER JOIN (
SELECT [e5].*
FROM [RoomOccupancies] AS [e5]
WHERE [e5].[IsDeleted] = 0
) AS [t1] ON [t0].[Id] = [t1].[RoomId]
INNER JOIN [WorkGroups] AS [f.Rooms.RoomOccupancies.WorkGroup] ON [t1].[WorkgroupId] = [f.Rooms.RoomOccupancies.WorkGroup].[Id]
WHERE (([e3].[IsDeleted] = 0) AND (([f.Rooms.RoomOccupancies.WorkGroup].[IsFinished] = 0) AND ([f.Rooms.RoomOccupancies.WorkGroup].[StartDate] < GETDATE()))) AND ([e].[Id] = [e3].[BuildingId])
) AS [CurrentWorkerCount]
FROM [Building] AS [e]
WHERE [e].[IsDeleted] = 0