Looking for a more elegant LINQ solution - c#

I am trying to figure out an efficient way to retrieve the data I am after. I need to get a list of all of the most recent children by ParentId coupled with all parent entries that do NOT have children. I have created a visual guide to illustrate what the response should be.
The query needs to remain as IQueryable until ALL sorting and paging is completed.
Last and LastOrDefault are not supported by LINQ to Entities (as stated by the error messages I have received while using them).
Using First or FirstOrDefault will return the error "This method or operation is not implemented"
Original Data:
-------------------------------
- Id - ParentId - CreatedDate -
-------------------------------
- 1 - - 07/01/2013 -
- 2 - - 07/01/2013 -
- 3 - - 07/01/2013 -
- 4 - 1 - 07/02/2013 -
- 5 - 2 - 07/03/2013 -
- 6 - 2 - 07/04/2013 -
- 7 - 1 - 07/05/2013 -
-------------------------------
Data returned by query
-------------------------------
- Id - ParentId - CreatedDate -
-------------------------------
- 3 - - 07/01/2013 -
- 6 - 2 - 07/04/2013 -
- 7 - 1 - 07/05/2013 -
-------------------------------
Currently, my LINQ query looks like this:
// Retrieves parent records with NO children.
var q1 = myTable
.Where(x => x.ParentId == null)
.Except(myTable
.Where(x => myTable
.Any(c => (c.ParentId == x.Id))));
// Retrieves most recent child records for each parentId
var q2 =
(from a in myTable
join b in
(myTable.Where(a => a.ParentId != null)
.GroupBy(a => a.ParentId)
.Select(b => new { ParentId = b.Key, CreatedDate = b.Max(t => t.CreatedDate) }))
on a.ParentId equals b.ParentId
where a.CreatedDate == b.CreatedDate
select a);
q1 = q1.Union(q2);
The back-end is using Npgsql2 with PostgreSQL. I am looking for a more elegant solution for this query. I am very new to LINQ and would like to optimize this.
Sorting code (sloppy, but jTable returns these strings):
if (string.IsNullOrEmpty(sorting) || sorting.Equals("Name ASC")) {
q1 = q1.OrderBy(p => p.Customer.Name);
} else if (sorting.Equals("Name DESC")) {
q1 = q1.OrderByDescending(p => p.Customer.Name);
} else if (sorting.Equals("Date ASC")) {
q1 = q1.OrderBy(p => p.CreatedDate);
} else if (sorting.Equals("Date DESC")) {
q1 = q1.OrderByDescending(p => p.CreatedDate);
}
Paging code:
var result = pageSize > 0
? q1.Skip(startIndex).Take(pageSize).ToList()
: q1.ToList();

Use grouping:
Mock data:
public class Entry {
public int Id { get; set; }
public int? ParentId { get; set; }
public DateTime Date { get; set; }
};
var list = new List<Entry> {
new Entry{ Id = 1, ParentId = null, Date = new DateTime(2013, 7, 1) },
new Entry{ Id = 2, ParentId = null, Date = new DateTime(2013, 7, 1) },
new Entry{ Id = 3, ParentId = null, Date = new DateTime(2013, 7, 1) },
new Entry{ Id = 4, ParentId = 1, Date = new DateTime(2013, 7, 2) },
new Entry{ Id = 5, ParentId = 2, Date = new DateTime(2013, 7, 3) },
new Entry{ Id = 6, ParentId = 2, Date = new DateTime(2013, 7, 4) },
new Entry{ Id = 7, ParentId = 1, Date = new DateTime(2013, 7, 5) }
};
Query:
var query = from l in list
group l by l.ParentId into g
select new {
Items = g.OrderBy(x => x.Date).Last()
};
var res = query.OrderBy(x => x.Items.Id).Select(x => x.Items).ToList();
LinqPad result:
Id ParentId Date
3 null 01.07.2013 0:00:00
6 2 04.07.2013 0:00:00
7 1 05.07.2013 0:00:00

I can propose a different query, still in two phases
var firstQuery = myTable.Select(p => new { p.ID, ParentID = p.ParentID ?? p.ID, p.CreatedDate })
.GroupBy( p => p.ParentID).Select( q => new
{
el = q.OrderByDescending( k => k.CreatedDate).Take(1)
}).SelectMany(t => t.el);
var result = dc.TabellaId_ParentId.Where(p => test.Select(q => q.ID).Contains(p.ID));

Related

Linq not grouping

Given the data below, I am trying to write a LINQ statement that will group by ParentProductId, and select the maximum EndDate if there is more than one item in the group.
Since the data listed below has two items with the same ParentProductId, I would expect three records to be returned (and for the "2020" date to be used for the ParentProductId = 1 group, not "2019"). However, the LINQ statement that I have is still returning all four records. What am I doing wrong?
Data:
Subscription.Add(new Subscription() { CustomerId = 555, ParentProductId = 37 , EndDate= null});
Subscription.Add(new Subscription() { CustomerId = 555, ParentProductId = 38 , EndDate = null });
Subscription.Add(new Subscription() { CustomerId = 555, ParentProductId = 1 , EndDate = new DateTime(2019, 11, 28) });
Subscription.Add(new Subscription() { CustomerId = 555, ParentProductId = 1, EndDate = new DateTime(2020, 1, 28) });
LINQ Statement:
var IndividualSubscription = (from s in db.Subscriptions
join ptp in db.ProductToProducts on s.ProductToProductId equals ptp.ProductToProductId
join p in db.Products on ptp.ParentProductId equals p.ProductId
where SubscriptionIds.Contains(s.OriginalSubscriptionId)
&& s.CustomerId == CustomerId
group new {ptp.ParentProductId, s.EndDate }
by new
{
s.CustomerId,
ptp.ParentProductId,
p.Name,
s.EndDate,
} into grp
select new NCCN.Model.IndividualGroupSubscription
{
CustomerId = grp.Key.CustomerId,
ParentProductId = grp.Key.ParentProductId,
ParentProductName = grp.Key.Name,
EndDate = grp.Max(p => p.EndDate),
}).ToList();
You group for {ptp.ParentProductId, s.EndDate }.
Group only for ParentProductId, if you need 3 rows result.
var subscription = new List<Subscription>(){new Subscription() { CustomerId = 555, ParentProductId = 37 , EndDate= null}
,new Subscription() { CustomerId = 555, ParentProductId = 38 , EndDate = null }
,new Subscription() { CustomerId = 555, ParentProductId = 1 , EndDate = new DateTime(2019, 11, 28) }
,new Subscription() { CustomerId = 555, ParentProductId = 1, EndDate = new DateTime(2020, 1, 28) }
};
var query = subscription
.GroupBy(x=> x.ParentProductId)
.Select(x=> new {x.Key, EndDate = x.Max(s=>s.EndDate)});
Note sure why your linq is that overkill but following your written requirement and not your code what you need is a million times shorter than what you have wrote
// get grouped by product id
var result = Subscription.GroupBy(s => s.ParentProductId)
// select to output something for each group. What you want is
// order descending all elements and pick the first which is the highest
.Select(grp => grp.OrderByDescending(o => o.EndDate).FirstOrDefault())
// returned as a list
.ToList();

Select TOP 1 for each FK in list using Entity Framework

I have a large table where I'm trying to select the top 1 row for each FK in a list.
My table is laid out as:
ChangeId | AssetId | Timestamp
1 1 123
2 2 999
3 1 3478
4 3 344
5 2 1092
Where ChangeId is my PK, AssetId is my FK and Timestamp is the value I'm trying to select.
If I try the following:
var results =
from Asset in _context.Asset
join change in _context.Change on Asset.AssetId equals change.AssetId into potentialChange
from actualChange in potentialChange.OrderByDescending(y => y.ChangeId).Take(1)
select
{
AssetId,
Timestamp
}
Where my expected result would be:
[
{
AssetId: 1,
Timestamp: 3478
},
{
AssetId: 2,
Timestamp: 1092
},
{
AssetId: 3,
Timestamp: 344
}
]
This query flags up the The LINQ expression could not be translated and will be evaluated locally. which is not suitable for a production rollout.
Running a foreach loop and selecting each item out 1 by 1 works, not it's not a performant solution.
Is there a suitable way to achieve the above?
Try to group it by AssetId and take max from each group
var results =
from Asset in _context.Asset
join change in _context.Change on Asset.AssetId equals change.AssetId into potentialChange
group potentialChange by potentialCharge.AssetId into g
select
{
g.Key,
g.Max().Timestamp
}
Use Group By as follows:
List<MyTable> data = new List<MyTable>()
{
new MyTable(){ChangeId = 1, AssetId = 1, Timestamp = 123},
new MyTable(){ChangeId = 2, AssetId = 2, Timestamp = 999},
new MyTable(){ChangeId = 3, AssetId = 1, Timestamp = 123},
new MyTable(){ChangeId = 5, AssetId = 3, Timestamp = 123},
new MyTable(){ChangeId = 5, AssetId = 2, Timestamp = 123},
};
var expectedData = data.OrderByDescending(d => d.Timestamp).GroupBy(d => d.AssetId).Select(g => new
{
AssetId = g.Key,
TimeStamp = g.First().Timestamp
}).ToList();
This will give your expected result.
Try using .First() instead of .Take(1)
LINQ How to take one record and skip rest c#

Linq to select objects with unique Id and greatest date?

I have a list of objects that look like this:
public class A
{
public int Id {get; set;}
public DateTime Date {get;set;}
public int TypeId {get; set;}
public int Version {get; set;}
}
My data looks like this:
Id Date TypeId Version
1 10/3/18 1 1
2 10/3/18 1 2
3 10/4/18 1 1
4 10/4/18 2 1
How can I make a linq query to return these 2 in a list where it gets the item with the greatest date where the version # is 1 and also uses the TypeId to return more items?
Id Date TypeId Version
3 10/4/18 1 1
4 10/4/18 2 1
This is what I have tried but my code only returns one item because of my FirstOrDefault function.
var q = from n in A.All.Where(x => x.Version == 1)
group n by n.TypeId into g
select g.OrderByDescending(t => t.Date).FirstOrDefault();
You need to group by typeId and then in each group order elements by date. Then you can pick first element in each group. Try this code:
var input = new[]
{
new A {Id = 1, Date = new DateTime(2018, 10, 3), TypeId = 1, Version = 1},
new A {Id = 2, Date = new DateTime(2018, 10, 3), TypeId = 1, Version = 2},
new A {Id = 3, Date = new DateTime(2018, 10, 4), TypeId = 1, Version = 1},
new A {Id = 4, Date = new DateTime(2018, 10, 4), TypeId = 2, Version = 1},
};
var result = input.Where(a => a.Version == 1)
.GroupBy(a => a.TypeId)
.Select(g => g.OrderByDescending(x => x.Date).First())
.ToArray();

Select values with max date for each ID [duplicate]

This question already has answers here:
How do I get the MAX row with a GROUP BY in LINQ query?
(7 answers)
Closed 5 years ago.
This is the table "history"
id value date
1 1 01/01/2017 20:20:20
1 2 02/01/2017 20:20:20
1 3 03/01/2017 20:20:20
2 5 01/01/2017 20:20:20
2 6 02/01/2017 20:20:20
How with linq select max values for each id
context.History
.GroupBy(x => x.id) ??
.SelectOnlyWithMax(z => z.date) ??
In result only two objects
id value date
1 3 03/01/2017 20:20:20
2 6 02/01/2017 20:20:20
If you want the entire row with the highest date for each Id, you can use the following code (written with LinqPad). If you just want the Id, you can use #BurnsBA's answer, as it will be slightly more efficient.
void Main()
{
var data = new List<Record>
{
new Record(){Id=1, Value=1, Date=new DateTime(2017,1,1)},
new Record(){Id=1, Value=2, Date=new DateTime(2017,2,1)},
new Record(){Id=1, Value=3, Date=new DateTime(2017,3,1)},
new Record(){Id=2, Value=5, Date=new DateTime(2017,1,1)},
new Record(){Id=2, Value=6, Date=new DateTime(2017,2,1)},
};
var query = data.GroupBy(d => d.Id)
.SelectMany(g => g.OrderByDescending(d => d.Date)
.Take(1));
query.Dump();
}
public class Record
{
public int Id { get; set; }
public int Value { get; set; }
public DateTime Date { get; set; }
}
Results:
First it groups by Id, then sorts the items within the group by Date in descending order, and returns the first one, SelectMany then flattens the list.
public class History
{
public int id { get; set; }
public int value { get; set; }
public DateTime date { get; set; }
}
// setup:
var values = new List<History>();
values.Add(new History() { id = 1, value = 1, date = DateTime.Parse("01/01/2017 20:20:20") });
values.Add(new History() { id = 1, value = 2, date = DateTime.Parse("02/01/2017 20:20:20") });
values.Add(new History() { id = 1, value = 3, date = DateTime.Parse("03/01/2017 20:20:20") });
values.Add(new History() { id = 2, value = 5, date = DateTime.Parse("01/01/2017 20:20:20") });
values.Add(new History() { id = 2, value = 6, date = DateTime.Parse("02/01/2017 20:20:20") });
// result :
values.GroupBy(
x => x.id,
y => y.date,
// Below, dates will be enumerable
(id, dates) => new { id = id, date = dates.Max() }
)
// returns enumerable collection of anonymous type:
{
{ id = 1, date = [3/1/2017 8:20:20 PM] },
{ id = 2, date = [2/1/2017 8:20:20 PM] }
}
I suggest MoreLINQ's MaxBy function, that is:
context.History.GroupBy( x => x.id ).Select( x => x.MaxBy( y => y.date) )

How to re-write this inner join subquery from SQL to Lambda

SELECT ulcch.ID, ulcch.UserLoginHistoryID, ulcch.StatusID,
ulcch.ClientModuleID, ulcch.DeviceState, ulcch.UpdatedAt, ulcch.CreatedAt
FROM UserLoginClientConnectionHistory AS ulcch INNER JOIN
(SELECT MAX(CreatedAt) AS maxCreatedAt
FROM UserLoginClientConnectionHistory AS ulcch1
GROUP BY UserLoginHistoryID) AS m ON m.maxCreatedAt = ulcch.CreatedAt
There can be many updates of 'device state' per day audited into this login table. This query returns the last unique one for each day.
I would like this re-written as a Lambda statement. This is how far I got, I don't know if i'm on the right track, and my Max() is throwing a type error, probably because the group by is making another list or something...
Hope you can work it out from my object examples.... :S
userLogin.UserLoginClientConnectionHistories.Where(x => x.CreatedAt ==
userLoginClientConnectionHistoryRepository.GetAll(
GenericStatus.Active).GroupBy(y => y.UserLoginHistoryID).Max(y => y.CreatedAt));
I think this does what you want:
var result = userLogin.UserLoginClientConnectionHistories
.GroupBy(y => new { Id = y.UserLoginHistoryID, Day = y.CreatedAt.Date })
.Select(x => new
{
Id = x.Key.Id,
Day = x.Key.Day,
MostRecent = x.Max(y => y.CreatedAt)
});
Here is a testbed for it:
public class Program
{
class LoginEntry
{
public int UserLoginHistoryID { get; set; }
public DateTime CreatedAt { get; set; }
}
class UserLogin
{
public List<LoginEntry> UserLoginClientConnectionHistories = new List<LoginEntry>();
}
public static void Main(string[] args)
{
UserLogin userLogin = new UserLogin();
userLogin.UserLoginClientConnectionHistories = new List<LoginEntry> {
new LoginEntry {UserLoginHistoryID = 1, CreatedAt = new DateTime(2009, 1, 1, 3, 0 ,0)},
new LoginEntry {UserLoginHistoryID = 1, CreatedAt = new DateTime(2009, 1, 1, 15, 0 ,0)},
new LoginEntry {UserLoginHistoryID = 1, CreatedAt = new DateTime(2009, 1, 3, 11, 0 ,0)},
new LoginEntry {UserLoginHistoryID = 1, CreatedAt = new DateTime(2009, 1, 1, 10, 0 ,0)},
new LoginEntry {UserLoginHistoryID = 2, CreatedAt = new DateTime(2009, 1, 3, 4, 0 ,0)},
new LoginEntry {UserLoginHistoryID = 2, CreatedAt = new DateTime(2009, 1, 3, 5, 0 ,0)},
};
var result = userLogin.UserLoginClientConnectionHistories
.GroupBy(y => new { Id = y.UserLoginHistoryID, Day = y.CreatedAt.Date })
.Select(x => new
{
Id = x.Key.Id,
Day = x.Key.Day,
MostRecent = x.Max(y => y.CreatedAt)
});
foreach (var item in result)
{
Console.WriteLine("User {0}, day {1}, most recent {2}",
item.Id,
item.Day,
item.MostRecent);
}
}
}
Output:
User 1, day 01-01-2009 00:00:00, most recent 01-01-2009 15:00:00
User 1, day 03-01-2009 00:00:00, most recent 03-01-2009 11:00:00
User 2, day 03-01-2009 00:00:00, most recent 03-01-2009 05:00:00
Here is the inner join portion as a lambda. I assumed CreatedAt was a dateTime.
UserLoginClientConnectionHistory
.GroupBy (ulcch1 =>
new
{
Name = ulcch1.Name
})
.Select (g =>
new
{
maxCreatedAt = (DateTime?)(g.Max (p => p.CreatedAt))
})
I think you want to group by CreatedAt rather than UserLoginHistoryID:
var q = userLogin.UserLoginClientConnectionHistories
.GroupBy(h => h.CreatedAt)
.OrderByDescending(g => g.Key) // Sort by CreatedAt
.First()
.Select(h => new { h.Id, h.UserLoginHistoryID, ... });
This will return the set of UserLoginClientConnectionHistory entries that share the most recent CreatedAt value.
Thanks for all your help guys, i've voted you all up, but you wouldn't believe it but a few hours later I searched for a program to convert SQL to LINQ, and to my surprise found one called "Linqer". Sounds crazy and didn't expect to get far, but it worked perfectly.. definitely worth checking out that app if anyone else gets stuck in the same boat...
Check the mammoth query it returned! After analysing it, don't think it's got extra bloat? Anyone have any optimisation tips or spot any unnecessary code?
moduleDeviceStates = from ulh in user.UserLoginHistories
join ulcch in userLogin.UserLoginClientConnectionHistories on new { ID = ulh.ID } equals new { ID = ulcch.UserLoginHistoryID }
join cm in clientModuleRepository.GetAll(GenericStatus.Active) on new { ClientModuleID = ulcch.ClientModuleID } equals new { ClientModuleID = cm.ID }
join mo in moduleRepository.GetAll(GenericStatus.Active) on new { ModuleID = cm.ModuleID } equals new { ModuleID = mo.ID }
join m in
(
(from ulcch1 in userLogin.UserLoginClientConnectionHistories
group ulcch1 by new
{
ulcch1.UserLoginHistoryID
} into g
select new
{
maxCreatedAt = g.Max(p => p.CreatedAt)
})) on new { maxCreatedAt = ulcch.CreatedAt } equals new { maxCreatedAt = m.maxCreatedAt }
select new ModuleDeviceState()
{
ModuleID = mo.ID,
Name = mo.Name,
DeviceState = (State.DeviceState)ulcch.DeviceState,
CreatedAt = ulcch.CreatedAt
};
Cheers for your help dahlbyk, but I did want to group on UserLoginHistoryID, I had my query confirmed in SQL before delving into a lambda equivalent :) thanks.
#Mark Thanks for taking the time to reply, yes I do what the [last] entries per user (userloginhistory.. which in turn contains a userID) for each day, and exporting my sql into the linq query did produce what I wanted (which can be seen in the query result below; this is what I want. The reason you see double entries for each day is because there are also attached ClientModule's.. so I really want all client module, per login entry per day - so hard to get a programming requirement across over a discussion forum argh!) Perhaps yours does exactly the same thing (it appears to if I am reading your output correctly) just a lot more streamlined.
See I didn't know too much about the anon casting you've done there with GroupBy and Select, but now I see it, it makes sense. I might give yours a go. Hopefully I can give it a tweak to include distinct ClientModule's per day too. So anyway.. here is the query result from my SQL, and effectively what I got through my own lambda:
ID UserLoginHistoryID StatusID ClientModuleID DeviceState UpdatedAt CreatedAt
277 62 1 1 4 NULL 2009-10-31 13:28:59.003
278 62 1 16 4 NULL 2009-10-31 13:28:59.003
331 65 1 1 4 NULL 2009-10-31 17:13:28.333
332 65 1 16 4 NULL 2009-10-31 17:13:28.333
Update Mark: Hi again, well after a couple of tweaks on your query, I could produce the same object graph in .NET between both lambda statements. This is the one I will use now, derived from yours as it's more streamlined and easier to understand than the auto-gen'd one and I will award you the points :)
I added a few more entries to the Group By as I need that for my new ModuleDeviceState class.
moduleDeviceStates = userLogin.UserLoginClientConnectionHistories
.GroupBy(y => new { Id = y.UserLoginHistoryID,
CreatedAt = y.CreatedAt.Date,
ModuleID = y.ClientModule.ModuleID,
ModuleName = y.ClientModule.Module.Name,
DeviceState = y.DeviceState })
.Select(x => new ModuleDeviceState()
{
ModuleID = x.Key.ModuleID,
Name = x.Key.ModuleName,
DeviceState = (State.DeviceState)x.Key.DeviceState,
CreatedAt = x.Max(y => y.CreatedAt)
});

Categories