Grouping a list of list using linq - c#

I have these tables
public class TaskDetails
{
public string EmployeeName {get; set;}
public decimal EmployeeHours {get; set;}
}
public class Tasks
{
public string TaskName {get; set;}
public List<TaskDetails> TaskList {get; set;}
}
I have a function that returns a List<Tasks>. What I would need is to create a new List that groups the EmployeeNames and SUM the EmployeeHours irrespective of the TaskName. That is, I need to fetch TotalHours of each Employees. How to get that?
P.S: And to what have I done so far. I have stared at the code for a long time. Tried Rubber Duck Problem solving to no avail. I can do get the results using a foreach and placing it to a Dictionary<string, decimal>. That logic will be to check if key does not exist, add a new key and assign the value and if the key exists add the decimal value to the original value. But I feel its too much here. I feel there is a ForEach - GroupBy - Sum combination which I am missing.
Any pointers on how to do it will be very helpful for me.

var results = tasks.SelectMany(x => x.Tasks)
.GroupBy(x => x.EmployeeName)
.ToDictionary(g => g.Key, g => g.Sum(x => x.EmployeeHours));
Gives you Dictionary<string, decimal>.
To get a list just replace ToDictionary with Select/ToList chain:
var results = tasks.SelectMany(x => x.Tasks)
.GroupBy(x => x.EmployeeName)
.Select(g => new {
EmployeeName = g.Key,
Sum = g.Sum(x => x.EmployeeHours)
}).ToList();

a SelectMany would help, I think.
It will "flatten" the Lists of TaskDetail of all your Task elements into a single IEnumerable<TaskDetail>
var result = listOfTasks.SelectMany(x => x.Tasks)
.GroupBy(m => m.EmployeeName)
.Select(m => new {
empName = m.Key,
hours = m.Sum(x => x.EmployeeHours)
});

var emplWithHours = allTasks
.SelectMany(t => t.Tasks)
.GroupBy(empl => empl.EmployeeName)
.Select(empl => new
{
EmployeeName = empl.Key,
TotalHours = empl.Sum(hour => hour.EmployeeHours)
}).ToDictionary(i => i.EmployeeName, i => i.TotalHours);
Also, when both your class name and field name is Tasks, it gives a compile-time error:
Error 1 'Tasks': member names cannot be the same as their enclosing type
I would have named your class Task since it represents a single task.

I would do it this way:
var query =
(
from t in tasks
from td in t.TaskList
group td.EmployeeHours by td.EmployeeName into ghs
select new
{
EmployeeName = ghs.Key,
EmployeeHours = ghs.Sum(),
}
).ToDictionary(x => x.EmployeeName, x => x.EmployeeHours);
I slightly more succinct query would be this:
var query =
(
from t in tasks
from td in t.TaskList
group td.EmployeeHours by td.EmployeeName
).ToDictionary(x => x.Key, x => x.Sum());
There are pros and cons to each. I think the first is more explicit, but the second a little neater.

Related

LINQ to SQL - order by, group by and order by each group with skip and take

This is an extension of already answered question by Jon Skeet that you can find here.
The desired result is following:
A 100
A 80
B 80
B 50
B 40
C 70
C 30
considering you have following class:
public class Student
{
public string Name { get; set; }
public int Grade { get; set; }
}
to get to the result (in ideal scenario) can be done with Jon Skeet's answer:
var query = grades.GroupBy(student => student.Name)
.Select(group =>
new { Name = group.Key,
Students = group.OrderByDescending(x => x.Grade) })
.OrderBy(group => group.Students.FirstOrDefault().Grade);
However in my case I have to support paging in my query as well. This means performing SelectMany() and then do Skip() and Take(). But to do Skip() you have to apply OrderBy(). This is where my ordering breaks again as I need to preserve the order I get after SelectMany().
How to achieve this?
var query = grades.GroupBy(student => student.Name)
.Select(group =>
new { Name = group.Key,
Students = group.OrderByDescending(x => x.Grade) })
.OrderBy(group => group.Students.FirstOrDefault().Grade).SelectMany(s => s.Students).OrderBy(something magical that doesn't break ordering).Skip(s => skip).Take(t => take);
I know I could manually sort again the records when my query is materialised but I would like to avoid this and do all of it in one SQL query that is translated from LINQ.
You can take another approach using Max instead of ordering each group and taking the first value. After that you can order by max grade, name (in case two students have the same max grade) and grade:
var query = c.Customers
.GroupBy(s => s.Name, (k, g) => g
.Select(s => new { MaxGrade = g.Max(s2 => s2.Grade), Student = s }))
.SelectMany(s => s)
.OrderBy(s => s.MaxGrade)
.ThenBy(s => s.Student.Name)
.ThenByDescending(s => s.Student.Grade)
.Select(s => s.Student)
.Skip(toSkip)
.Take(toTake)
.ToList();
All these methods are supported by EF6 so you should get your desired result.
Just re-index your list results and remove the index before returning.
var query = grades.GroupBy(student => student.Name)
.Select(group =>
new { Name = group.Key,
Students = group.OrderByDescending(x => x.Grade)
})
.OrderBy(group => group.Students.FirstOrDefault().Grade)
.SelectMany(s => s.Students)
.Select((obj,index) => new {obj,index})
.OrderBy(newindex => newindex.index)
.Skip(s => skip).Take(t => take)
.Select(final=> final.obj);

Flatten Linq query of nested groupings with combined key in ASP.net

In my database there are player-objects which do have a nationality and points.
I need a possibility to nest my groupings. Because they are given by the client, grouping into an anonymous key seems no option here.
So I have to nest them like
Players.Where(p => p.Created <= /*some date*/)
.GroupBy(p => p.Nationality) // group them by their nationality
.Select(arg => new {
arg.Key,
Elements = arg.GroupBy(p => p.Points > 0) // group them by the ones with points and the ones without
})
. // here i need to flatten them by also combining the key(s) of the groupings to put them into a dictionary
.ToDictionary(/*...*/);
At the end, the Dictionary should contain the keys as string like ["USA|true"], ["USA|false"] or ["GER|true"] with their respective elements.
I guess SelectMany is the key but I don't get the point where to start from to achieve this.
What about this solution:
public class Player
{
public string Nationality {get;set;}
public int Points {get;set;}
public double otherProp {get;set;}
//new field is added
public string groupings {get;set;}
}
var groups = new List<Func<Player, string>>();
groups.Add(x => x.Nationality);
groups.Add(x => (x.Points > 0).ToString().ToLower());
Players.ForEach(x =>
groups.ForEach(y => x.groupings = x.groupings + (x.groupings == null ? "" : "|") + y(x))
);
var answer = Players.GroupBy(x => x.groupings).ToDictionary(x => x.Key, x => x.ToList());
Answering your concrete question.
As you mentioned, SelectMany is the key. The place in your query is right after the Select:
.Select(...)
.SelectMany(g1 => g1.Elements.Select(g2 => new {
Key = g1.Key + "|" + g2.Key, Elements = g2.ToList() }))
.ToDictionary(g => g.Key, g => g.Elements);
It can also replace the Select (i.e. start right after the first GroupBy):
.GroupBy(p => p.Nationality)
.SelectMany(g1 => g1.GroupBy(p => p.Points > 0).Select(g2 => new {
Key = g1.Key + "|" + g2.Key, Elements = g2.ToList() }))
.ToDictionary(g => g.Key, g => g.Elements);

Inverting a Hierarchy with Linq

Given the class
public class Article
{
public string Title { get; set; }
public List<string> Tags { get; set; }
}
and
List<Article> articles;
How can I create a "map" from individual tags (that may be associated with 1 or more articles) with Linq?
Dictionary<string, List<Article>> articlesPerTag;
I know that I can select all of the tags like this
var allTags = articlesPerTag.SelectMany(a => a.Tags);
However, I'm not sure how to associate back from each selected tag to the article it originated from.
I know I can write this conventionally along the lines of
Dictionary<string, List<Article>> map = new Dictionary<string, List<Article>>();
foreach (var a in articles)
{
foreach (var t in a.Tags)
{
List<Article> articlesForTag;
bool found = map.TryGetValue(t, out articlesForTag);
if (found)
articlesForTag.Add(a);
else
map.Add(t, new List<Article>() { a });
}
}
but I would like to understand how to accomplish this with Linq.
If you specifically need it as a dictionary from tags to articles, you could use something like this.
var map = articles.SelectMany(a => a.Tags.Select(t => new { t, a }))
.GroupBy(x => x.t, x => x.a)
.ToDictionary(g => g.Key, g => g.ToList());
Though it would be more efficient to use a lookup instead, it's precisely what you are trying to build up.
var lookup = articles.SelectMany(a => a.Tags.Select(t => new { t, a }))
.ToLookup(x => x.t, x => x.a);
One more way using GroupBy. A bit complicated though.
articles.SelectMany(article => article.Tags)
.Distinct()
.GroupBy(tag => tag, tag => articles.Where(a => a.Tags.Contains(tag)))
.ToDictionary(group => group.Key,
group => group.ToList().Aggregate((x, y) => x.Concat(y).Distinct()));

Use Linq to return first result for each category

I have a class (ApplicationHistory) with 3 properties:
ApplicantId, ProviderId, ApplicationDate
I return the data from the database into a list, however this contains duplicate ApplicantId/ProviderId keys.
I want to supress the list so that the list only contains the the earliest Application Date for each ApplicantId/ProviderId.
The example below is where I'm currently at, but I'm not sure how to ensure the earliest date is returned.
var supressed = history
.GroupBy(x => new
{
ApplicantId = x.ApplicantId,
ProviderId = x.ProviderId
})
.First();
All advice appreciated.
Recall that each group formed by the GroupBy call is an IGrouping<ApplicationHistory>, which implements IEnumerable<ApplicationHistory>. Read more about IGrouping here. You can order those and pick the first one:
var oldestPerGroup = history
.GroupBy(x => new
{
ApplicantId = x.ApplicantId,
ProviderId = x.ProviderId
})
.Select(g => g.OrderBy(x => x.ApplicationDate).FirstOrDefault());
You are selecting first group. Instead select first item from each group:
var supressed = history
.GroupBy(x => new {
ApplicantId = x.ApplicantId,
ProviderId = x.ProviderId
})
.Select(g => g.OrderBy(x => x.ApplicationDate).First());
Or query syntax (btw you don't need to specify names for anonymous object properties in this case):
var supressed = from h in history
group h by new {
h.ApplicantId,
h.ProviderId
} into g
select g.OrderBy(x => x.ApplicationDate).First();

Linq To SQL: Most Efficient Way of Looping through List of ID's

Say I have a list of ID's:
int[] ids = { 1, 2, 3, 4 };
I already have a compiled LINQ statement that returns the individual row (lets call them "Bar" here). So I could just do this:
var foo = new List<Bar>();
var len = ids.Length;
for (int i=0; i < len; i++)
{
foo.Add(db.GetBarByID(ids[i]));
}
What I'm wondering is, if there's a more efficient way of doing this? There's not that much data being returned (several nvarchar and int columns) per row and the list of ID's can be up to 50.
Update:
I'll elaborate on "GetBarByID". It's a simple LINQ statement which returns "Bar".
class Bar
{
public int ID {get; set;}
public string Name {get;set;}
public int Age {get;set;}
public string Blah{get;set;}
}
IQueryable<Bar> GetBarByID(int ID)
{
return db.Bar
.Where(w => w.Barid == ID)
.SelectMany(b => Othertable.Where(w => w.barid == b.id),
(b, x) => new Bar { ID = s.id, Name = s.name, Age = s.age, Blah = x.blah });
}
Side note: By efficient, i mean clean code and performance wise.
There's certainly a simpler way of writing the same code:
var foo = ids.Select(id => db.GetBarById(id))
.ToList();
However, it depends on what db.GetBarById really does. If you can use ids itself in your query, you might be able to do the whole thing in a single database query:
var foo = db.Bars
.Where(b => ids.Contains(b.Id))
.ToList();
Obviously that's not using your existing LINQ query though - if there's more involved in retrieving a single row, you may need to do a bit more work.
EDIT: Okay, now we've got the single method, it's fairly easy... although you should probably be using a join, to be honest... and I suspect your real code has w.Barid == ID rather than w.Barid = ID.
var foo = db.Bar
.Where(w => ids.Contains(w.Barid))
.SelectMany(b => Othertable.Where(w => w.barid == b.id),
(b, x) => new Bar { ID = s.id, Name = s.name,
Age = s.age, Blah = x.blah })
.ToList();
var myProducts = from bar in db.Bars
where ids.Contains(bar.Id)
select bar;

Categories