Declan Key Declan Key - 17 days ago 5
C# Question

C# Linq query help removing foreach loops creating cleaner code

Is there a way to remove the for loops using linq to solve the problem I have

I want to get the subject and sum of points for each student and each subject in this list:

IEnumerable<Student> students = new List<Student> {
new Student() {Id = 1, Name = "John", Age = 13},
new Student() {Id = 2, Name = "Mary", Age = 12},
new Student() {Id = 3, Name = "Anne", Age = 14}
};


I have a second list containing all the scores and subject information:

IEnumerable<StudentScore> studentScores = new List<StudentScore> {
new StudentScore() {StudentId = 1, Subject = "Maths", Points = 54},
new StudentScore() {StudentId = 1, Subject = "Maths", Points = 32},
new StudentScore() {StudentId = 1, Subject = "English", Points = 55},
new StudentScore() {StudentId = 1, Subject = "English", Points = 54},

new StudentScore() {StudentId = 2, Subject = "Maths", Points = 44},
new StudentScore() {StudentId = 2, Subject = "Maths", Points = 37},
new StudentScore() {StudentId = 2, Subject = "English", Points = 59},
new StudentScore() {StudentId = 2, Subject = "English", Points = 64},

new StudentScore() {StudentId = 3, Subject = "Maths", Points = 53},
new StudentScore() {StudentId = 3, Subject = "Maths", Points = 72},
new StudentScore() {StudentId = 3, Subject = "English", Points = 54},
new StudentScore() {StudentId = 3, Subject = "English", Points = 59},
};


this is the solution I came up with:

foreach (var student in students)
{
foreach (var studentScore in studentScores.Select(ss=>ss.Subject).Distinct())
{
Console.WriteLine("Name: " + student.Name + " Subject:" + studentScore + "Score: " + studentScores.Where(ss => ss.StudentId == student.Id)
.Where(ss => ss.Subject == studentScore)
.Sum(ss => ss.Points));
}
}

Answer

Your solution has both a belt and suspenders - the foreach loops are combined with LINQ, where LINQ part is dependent on the values through which you go in your foreach loop.

The trick to understanding how this could be done entirely with LINQ is realization that LINQ deals with expressions, not statements. Therefore, you need to produce the whole list at once, and then either print it in a single foreach loop, or to use string.Format method to avoid loops altogether.

Here is how you prepare your data:

var rows = students
    .Join(
        studentScores
    ,   st => st.Id
    ,   sc => sc.StudentId
    ,   (st, sc) => new { Student = st, Score = sc }
    )
    .GroupBy(p => new { p.Student.Id, p.Score.Subject })
    .Select(g => new {
        Name = g.First().Student.Name
    ,   Subj = g.Key.Subject
    ,   Points = g.Sum(p => p.Score.Points)
    })
    .ToList();

Now you can go through this in a foreach loop, and print prepared results:

foreach (var r in rows) {
    Console.WriteLine($"Name: {r.Name} Subject: {r.Subject} Score: {r.Score}");
}
Comments