GroupBy in subquery with MAX() in C# - c#

I Have this code. It works fine but when I have two same maximal values it appear 2 times. So I need to use OrderBy. But I dont know how. Thanks for any help.
IQueryable<PerformanceRealization> pr = _context.PerformanceRealization
.Where(u => u.Deadline == _context.PerformanceRealization
.Where(x => x.GroupRealizationId == u.GroupRealizationId)
.Max(x => x.Deadline)
)
.Select(u => u);
Here is the SQL code with GROUP BY
SELECT PR.GroupRealizationId
FROM Stores.PerformanceRealization PR
LEFT JOIN Stores.GroupRealization ON Stores.GroupRealization.Id = PR.GroupRealizationId
WHERE PR.Deadline = (SELECT MAX(Deadline)
FROM Stores.PerformanceRealization PR2
WHERE PR.GroupRealizationId = PR2.GroupRealizationId)
GROUP BY PR.GroupRealizationId

You can select the first object from the group
IQueryable<PerformanceRealization> pr2 = pr
.GroupBy(x => x.GroupRealizationId)
.Select(g => g.First());
If you need a specific object from the group, then you can order by another column
IQueryable<PerformanceRealization> pr2 = pr
.GroupBy(x => x.GroupRealizationId)
.Select(g => g.OrderBy(x => x.SomeColumn).First());
for SomeColumn having the smallest value. For the greatest value, use OderByDescending instead.
Of course, you can integrate this approach into the first query:
IQueryable<PerformanceRealization> pr = _context.PerformanceRealization
.Where(u => u.Deadline == _context.PerformanceRealization
.Where(x => x.GroupRealizationId == u.GroupRealizationId)
.Max(x => x.Deadline)
)
.GroupBy(x => x.GroupRealizationId)
.Select(g => g.OrderBy(x => x.SomeColumn).First());
Note, you don't need to have a Select at the end like .Select(u => u). Since it has no effect, you can just drop it.
If your EF Core version cannot handle it (as revealed in a comment), then transition to LINQ-to-Objects with AsEnumerable(), but do the filtering in EF Core to minimize the number of records sent to the front-end:
IQueryable<PerformanceRealization> pr = _context.PerformanceRealization
.Where(u => u.Deadline == _context.PerformanceRealization
.Where(x => x.GroupRealizationId == u.GroupRealizationId)
.Max(x => x.Deadline)
)
.AsEnumerable() // <===== transition from LINQ-to-EF-Core to LINQ-to-Objects
.GroupBy(x => x.GroupRealizationId)
.Select(g => g.OrderBy(x => x.SomeColumn).First());

Related

How do I get the most recent entry by condition in EF Core?

I have a table with the following structure (and sample data):
Identifier
UseDate
PartId
a123
05/01/2000
237
a123
05/01/2000
4656
a123
01/01/2000
2134
a124
04/01/2000
5234
a124
01/01/2000
2890
I need to get the most recent entry of every (non-unique) identifier, but at most one per identifier.
The SQL-Query (MariaDB) that seems to fulfill my problem is the following:
SELECT a.Identifier, a.MaxDate, b.PartId, b.UseDate
FROM
(SELECT Identifier, MAX(UseDate) AS MaxDate FROM MyTable GROUP BY Identifier) a
LEFT JOIN MyTable b ON a.Identifier = b.Identifier
WHERE a.MaxDate = b.UseDate GROUP BY a.Identifier;
However I need this to work with C# and EF Core (Pomelo.EntitiFrameworkCore.MySql 5.0.3), my attempts have been:
var q1 = db.MyTable
.GroupBy(t => t.Identifier)
.Select(t => new { Identifier = t.Key, MaxDate = t.Max(x => x.UseDate) });
return new ObjectResult(db.MyTable
.Join(
q1,
t1 => t1.Identifier,
t2 => t2.Identifier,
(t1, t2) => new { Identifier = t2.Identifier, PartId = t1.PartId, MaxDate = t1.MaxDate, UseDate = t1.UseDate })
.Where(t => t.UseDate == q1.First(x => x.Identifier == t.Identifier).MaxDate)
.GroupBy(t => t.Identifier)
.ToList()
);
and
return new ObjectResult(db.MyTable
.GroupBy(t => t.Identifier)
.Select(t => t.OrderByDescending(x => x.UseDate).FirstOrDefault())
.ToList()
);
The first one throws this error:
System.InvalidOperationException: "Unable to translate the given 'GroupBy' pattern. Call 'AsEnumerable' before 'GroupBy' to evaluate it client-side."
The second one essentially yields the same, just complaining about the LINQ expression instead of the GroupBy.
I want to avoid using raw SQL, but how do I correctly (and hopefully efficiently) implement this?
There are many ways to write such query in LINQ, with most of them being able to be translated by EF Core 5/6+.
The straightforward approach once you have defined a subquery for the necessary grouping and aggregates is to join it to the data table, but not with join operator - instead, use row limiting correlated subquery (SelectMany with Where and Take), e.g.
var query = db.MyTable
.GroupBy(t => t.Identifier)
.Select(t => new { Identifier = t.Key, MaxDate = t.Max(x => x.UseDate) })
.SelectMany(g => db.MyTable
.Where(t => t.Identifier == g.Identifier && t.UseDate == g.MaxDate)
.Take(1));
If the ordering field is unique per each other key value (i.e. in your case if UseDate is unique per each unique Identifier value), you can use directly Join operator (since lo limiting is needed), e.g.
var query = db.MyTable
.GroupBy(t => t.Identifier)
.Select(t => new { Identifier = t.Key, MaxDate = t.Max(x => x.UseDate) });
.Join(db.MyTable,
g => new { g.Identifier, UseDate = g.MaxDate },
t => new { t.Identifier, t.UseDate },
(g, t) => t);
or directly apply Max based Where condition to the data table:
var query = db.MyTable
.Where(t => t.UseDate == db.MyTable
.Where(t2 => t2.Identifier == t.Identifier)
.Max(t2 => t2.UseDate)
);
Finally, the "standard" LINQ way of getting top 1 item per group.
For EF Core 6.0+:
var query = db.MyTable
.GroupBy(t => t.Identifier)
.Select(g => g
.OrderByDescending(t => t.UseDate)
.First());
For EF Core 5.0 the grouping result set inside the query must be emulated:
var query = db.MyTable
.GroupBy(t => t.Identifier)
.Select(g => db.MyTable
.Where(t => t.Identifier == g.Key)
.OrderByDescending(t => t.UseDate)
.First());

Linq: GroupBy not show result for the .Include clause

Why in the result "Titolare" is null?
If I don't use GroupBy, "Titolare" has value.
Thank you.
var ben = context.Benefici.Include("Titolare").Include("Titolare.ComuneDomicilio")
.Where(b => !b.Titolare.SD1_DAT_DECESSO.HasValue)
.OrderByDescending(b => b.SDB_DAT_INIZIO)
.GroupBy(b => b.SDB_CODDIS)
.ToList()
.Select(b => b.First())
.ToList();
It is because of GroupBy limitation - you cannot get grouped items using LINQ to Entities. It should be fixed in EF Core 6.
To get first item of the group, you have to rewrite your query. It is mimic of what will be generated by EF Core 6:
var itemsQuery = context.Benefici
.Where(b => !b.Titolare.SD1_DAT_DECESSO.HasValue);
var benQuery =
from u in itemsQuery.Select(b => new { b.SDB_CODDIS }).Distinct()
join b in itemsQuery
.Include(x => x.Titolare.ComuneDomicilio)
.Where(x => x.SDB_CODDIS == u.SDB_CODDIS)
.OrderByDescending(x => x.SDB_DAT_INIZIO)
.Take(1)
select b;
var ben = benQuery.ToList();

EF Core LINQ GROUPBY Then Select to get more than one properties of the entity

I have 2 tables Outlet and Order with below schemas:
Outlet Order
------ -------------------
Id Id
Name Name
OrderCompletedTime
NextOrderDueTime
OutletIds
Earlier when I wanted to get the NextOrderDueTime for each outlet using entity framework core, I did:
return _dbAccessor.RequestContext.MyDbContext.Order
.Where(i => i.OutletId == _dbAccessor.RequestContext.OutletId &&
!i.IsRemoved && i.NextOrderDueTime.HasValue)
.GroupBy(i => i.OutletId)
.Select(g => new { OutletId = g.Key, NextOrderDueTime = g.Min(x => x.NextOrderDueTime) })
.ToDictionary(i => i.OutletId, i => i.NextOrderDueTime);
Now on the UI we need to make this due time as link and wants user to get navigated to that Order details page based on order id
How can I change the above query to also return OrderId along with time?
My thoughts:
Change return type of method from Dictionary<int, DateTimeOffset?> to Dictionary<int, Tuple<int,DateTimeOffset?>>
I tried changing the Linq query to :
return _dbAccessor.RequestContext.MyDbContext.Order
.Where(i => i.OutletId == _dbAccessor.RequestContext.OutletId &&
!i.IsRemoved && i.NextOrderDueTime.HasValue)
.GroupBy(i => i.OutletId)
.Select(g =>
new
{
OutletId = g.Key,
NextOrderDueTime = g.FirstOrDefault(x => x.NextOrderDueTime == g.Min(y => y.NextOrderDueTime)).NextOrderDueTime,
NextOrderId = g.FirstOrDefault(x => x.NextOrderDueTime == g.Min(y => y.NextOrderDueTime)).OrderId
})
.ToDictionary(i => i.OutletId, i => new Tuple<int, DateTimeOffset?>(i.NextOrderId, i.NextOrderDueTime));
But this throws exception at runtime?
Please help to let me know what I am doing wrong here.
You could just take the entire order along with the outletid when you return:
.Select(g => new {
OutletId = g.Key,
NextOrder = g.OrderBy(x => x.NextOrderDueTime).FirstOrDefault()
})
You could select on this to take multiple properties from the order:
.Select(g => new {
OutletId = g.Key,
NextOrder = g.OrderBy(x => x.NextOrderDueTime).FirstOrDefault()
})
.Select(s => new {
s.OutletId,
NextOrderId = NextOrder.Id,
NextOrder.NextOrderDueTime,
NextOrderName = NextOrder.Name
})
etc..
The main thing to appreciate is that grouping gives you an object that has a key, but itself is a list of all things that have that key, so if you order the list by something like the DUeDat and take the first thing then you have an entire object with the lowest duedate from which you can take various things
The .GroupBy(...).Select(...).ToDictionary(...); cannot be converted to SQL since EF Core 3.0.
Due to the breaking change in EF Core 3.0. https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes , EF Core 3.0 will throw exception to make sure you know that all records in Order will be fetched from database before grouping and map to Dictionary.
I was able to get my query working as below:
return _dbAccessor.RequestContext.MyDbContext.Order
.Where(i => i.OutletId == _dbAccessor.RequestContext.OutletId &&
!i.IsRemoved && i.NextOrderDueTime.HasValue).AsEnumerable()
.GroupBy(i => i.OutletId)
.Select(g =>
new
{
OutletId = g.Key,
NextOrderDueTime = g.FirstOrDefault(x => x.NextOrderDueTime == g.Min(y => y.NextOrderDueTime)).NextOrderDueTime,
NextOrderId = g.FirstOrDefault(x => x.NextOrderDueTime == g.Min(y => y.NextOrderDueTime)).OrderId
})
.ToDictionary(i => i.OutletId, i => new Tuple<int, DateTimeOffset?>(i.NextOrderId, i.NextOrderDueTime));
The same can be done as shown in another answer by just adding AsEnumerable() before GroupBy:
_dbAccessor.RequestContext.MyDbContext.Order
.Where(i => i.OutletId == _dbAccessor.RequestContext.OutletId &&
!i.IsRemoved && i.NextOrderDueTime.HasValue).AsEnumerable()
.GroupBy(i => i.OutletId)
.Select(g => new {
OutletId = g.Key,
NextOrder = g.OrderBy(x => x.NextOrderDueTime).FirstOrDefault()
})
.Select(s => new {
s.OutletId,
NextOrderId = NextOrder.Id,
NextOrder.NextOrderDueTime,
NextOrderName = NextOrder.Name
})`enter code here`;

LINQ: Get filtered objects with filtered subcollection items?

I have class Goal, which contains collecion of objects ProgressItems. Progress items has property Date.
I want to get only goals which has progress items with date between specified range. Goals should have included ProgressItems, but only with passed Date condition.
How should I modify my LINQ?
var goalIds = _dbContext.GoalProgressItems
.Include(p => p.Goal)
.Where(p => p.Date >= range.From && p.Date <= range.To)
.Select(p => p.Id);
var goals = _dbContext.Goals
.Include(p => p.ProgressItems)
.Where(p => goalIds.Contains(p.Id))
.ToList();
You simply need to do .Join() on the two streams. Since goalIds is filtered already, it won't have any reference to Goals that doesn't have an item within the specified range, and, thus, .Join() will filter out items in goals that don't have matching entry(-ies) in goalIds.
var goalIds = _dbContext.GoalProgressItems
.Include(p => p.Goal)
.Where(p => p.Date >= range.From && p.Date <= range.To);
var goals = _dbContext.Goals
.Include(p => p.ProgressItems)
.Join(goalIds, g => g, gpi => gpi.GoalId, (g, gpi) => g);
and so now goals should contain only those goes that have progress items that are within desired range.
How about this query?
dbContext.Goals.Include(p => p.ProgressItems)
.Where(p => p.ProgressItems.Any(pr => pr.Date >= range.From && pr.Date <= range.To))
.ToList();
Note that the using the join as mentioned by LB2 would probably generate a more efficient SQL query (provided you can change the model the perform the join).

LinQ to get the latest group of records satisfying a condition

I have below stated 2 tables:
now I want to get the set of Child Table objects for whichever their parent table entries are latest(wr.r.t lastmodified). It should be something like....
List<Child_Table> List = ChildsList.Where(x=>x.name =="pqr" && status == "done")
.Select(x=>x.Parent.lastmodified == recent record).....ToList();
You can use GroupBy on the date, then OrderByDescending on the Key then take the First followed by SelectMany to flatten the results.
var result = ChildsList.Where(x => x.name == "pqr" && x.status == "done")
.GroupBy(x => x.Parent.lastmodified)
.OrderByDescending(g => g.Key)
.First()
.SelectMany(g => g)
.ToList();
You could use a join to accomplish it:
var results = children
.Join(parents.OrderByDescending(p => p.lastmodified).Take(1),
c => c.parent_id,
p => p.id,
(c, p) => c)
.Where(x => x.name == "pqr" && x.status == "done")
.ToList();

Categories

Resources