patterncsharpMinor
Nested LINQ Nightmare, calculating %completed
Viewed 0 times
linqnestedcalculatingcompletednightmare
Problem
This code horrifies and scares me. I wrote this.
Let me explain how this fits in to the rest of the application. On the top level, a Kendo Grid control calls an MVC controller for its data. That controller calls another controller, which calls this. So this method returns the data which populates a grid in the UI. That grid lists all of the
So the problem starts with the
public IQueryable StudentTracks(int employeeId)
{
IQueryable EducationPortalStudentTracks =
from studentTrack in _dbContext.EducationPortalStudentTracks
join track in _dbContext.EducationPortalTracks on studentTrack.TrackId equals track.Id
orderby track.Id
where studentTrack.EmployeeId == employeeId
select new StudentTrack
{
Id = studentTrack.Id,
TrackId = studentTrack.TrackId,
Name = track.Name,
Deadline = studentTrack.Deadline,
// this is where it gets bad
PercentComplete = (from doesntMatter in _dbContext.EducationPortalStudentCourses
let courses = (from studentCourse in _dbContext.EducationPortalStudentCourses
join studentTrack_ in _dbContext.EducationPortalStudentTracks on studentCourse.TrackId equals studentTrack_.TrackId
where studentTrack.Id == studentTrack.Id && studentTrack.EmployeeId == studentCourse.EmployeeId
select studentCourse)
select courses.Where(x => x.Status == true).Count() * 100 / courses.Count()).FirstOrDefault()
};
return EducationPortalStudentTracks;
}Let me explain how this fits in to the rest of the application. On the top level, a Kendo Grid control calls an MVC controller for its data. That controller calls another controller, which calls this. So this method returns the data which populates a grid in the UI. That grid lists all of the
StudentTracks in the database. In order to return the StudentTracks, this method does a little bit of Linq and returns a series of StudentTrack objects. So the problem starts with the
PercentComplete field. To calculate the value for that field, we first find all of the children StudentCourses. We then Solution
Break it down into more methods.
Then
...Which is still horrible.
Why are you using the query syntax? If you have navigation properties on your entities (assuming EF), I find this would be much easier to parse:
In
Why the
Your code iterates
And the intent is not exactly clear because of the
The
I think exposing
I would leave the last
```
public IEnumerable GetStudentTracks(int employeeId)
{
return _dbContext.EducationPortalStudentTracks
.Where(studentTrack => studentTrack.EmployeeId == employeeId)
.OrderBy(studentTrack => studentTrack.Track.Id)
.ToList()
.Select(studentTrack =>
new StudentTrack
{
Id = studentTrack.Id,
TrackId = studentTrack.TrackId,
Name = student
StudentTracks has multiple responsibilities, you could extract a private method whose job would be to CalculatePercentCompletion(studentTrack), making your method look like this:public IQueryable StudentTracks(int employeeId)
{
IQueryable EducationPortalStudentTracks =
from studentTrack in _dbContext.EducationPortalStudentTracks
join track in _dbContext.EducationPortalTracks on studentTrack.TrackId equals track.Id
orderby track.Id
where studentTrack.EmployeeId == employeeId
select new StudentTrack
{
Id = studentTrack.Id,
TrackId = studentTrack.TrackId,
Name = track.Name,
Deadline = studentTrack.Deadline,
PercentComplete = CalculatePercentCompletion(studentTrack)
};
return EducationPortalStudentTracks;
}Then
CalculatePercentCompletion could look like this:private int CalculatePercentCompletion(StudentTrack studentTrack)
{
return (from doesntMatter in _dbContext.EducationPortalStudentCourses
let courses = (from studentCourse in _dbContext.EducationPortalStudentCourses
join studentTrack_ in _dbContext.EducationPortalStudentTracks on studentCourse.TrackId equals studentTrack_.TrackId
where studentTrack.Id == studentTrack.Id && studentTrack.EmployeeId == studentCourse.EmployeeId
select studentCourse)
select courses.Where(x => x.Status == true).Count() * 100 / courses.Count()).FirstOrDefault();
}...Which is still horrible.
Why are you using the query syntax? If you have navigation properties on your entities (assuming EF), I find this would be much easier to parse:
public IQueryable StudentTracks(int employeeId)
{
return _dbContext.EducationPortalStudentTracks
.Where(studentTrack => studentTrack.EmployeeId == employeeId)
.OrderBy(studentTrack => studentTrack.Track.Id)
.Select(studentTrack =>
new StudentTrack
{
Id = studentTrack.Id,
TrackId = studentTrack.TrackId,
Name = studentTrack.Track.Name,
Deadline = studentTrack.Deadline,
PercentComplete = CalculatePercentCompletion(studentTrack)
});
}In
CalculatePercentCompletion the doesntMatter part makes it pretty confusing, I'm not sure but I think this could be equivalent:private int CalculatePercentCompletion(StudentTrack studentTrack)
{
var courses = _dbContext.EducationPortalStudentCourses
.Where(course => course.TrackId == studentTrack.Id
&& course.EmployeeId == studentTrack.EmployeeId)
.ToList();
return courses.Count(course => course.Status == true) * 100 / courses.Count
}Why the
.ToList() call? You want to materialize your query only when you need to - this method needs to know what the courses are for a given studentTrack, in order to count the number of courses with Status == true, multiply that by 100 and divide by the total number of courses.Your code iterates
courses twice, in the two Count() calls.And the intent is not exactly clear because of the
doesntMatter in the from clause, and the .FirstOrDefault() call makes it look like we're looking at an IEnumerable when we're really all materialized at that point and we either have zero or one values selected (if I get this correctly).The
status == true part is confusing. Normally I would have written it as .Where(course => course.Status) to avoid comparing a Boolean with a Boolean value, but Status is really a poor name and doing that would have made it harder to understand what's going on. I suggest you rename Status to whatever IsCompleted, so the where condition can read as .Where(course => course.IsCompleted), which is crystal-clear.I think exposing
IQueryable to your UI is a bad idea. StudentTracks should return an IEnumerable instead, your grid should be able to work off that just as well.I would leave the last
Select to Linq-to-Objects, and materialize the query just after the OrderBy call:```
public IEnumerable GetStudentTracks(int employeeId)
{
return _dbContext.EducationPortalStudentTracks
.Where(studentTrack => studentTrack.EmployeeId == employeeId)
.OrderBy(studentTrack => studentTrack.Track.Id)
.ToList()
.Select(studentTrack =>
new StudentTrack
{
Id = studentTrack.Id,
TrackId = studentTrack.TrackId,
Name = student
Code Snippets
public IQueryable<StudentTrack> StudentTracks(int employeeId)
{
IQueryable<StudentTrack> EducationPortalStudentTracks =
from studentTrack in _dbContext.EducationPortalStudentTracks
join track in _dbContext.EducationPortalTracks on studentTrack.TrackId equals track.Id
orderby track.Id
where studentTrack.EmployeeId == employeeId
select new StudentTrack
{
Id = studentTrack.Id,
TrackId = studentTrack.TrackId,
Name = track.Name,
Deadline = studentTrack.Deadline,
PercentComplete = CalculatePercentCompletion(studentTrack)
};
return EducationPortalStudentTracks;
}private int CalculatePercentCompletion(StudentTrack studentTrack)
{
return (from doesntMatter in _dbContext.EducationPortalStudentCourses
let courses = (from studentCourse in _dbContext.EducationPortalStudentCourses
join studentTrack_ in _dbContext.EducationPortalStudentTracks on studentCourse.TrackId equals studentTrack_.TrackId
where studentTrack.Id == studentTrack.Id && studentTrack.EmployeeId == studentCourse.EmployeeId
select studentCourse)
select courses.Where(x => x.Status == true).Count() * 100 / courses.Count()).FirstOrDefault();
}public IQueryable<StudentTrack> StudentTracks(int employeeId)
{
return _dbContext.EducationPortalStudentTracks
.Where(studentTrack => studentTrack.EmployeeId == employeeId)
.OrderBy(studentTrack => studentTrack.Track.Id)
.Select(studentTrack =>
new StudentTrack
{
Id = studentTrack.Id,
TrackId = studentTrack.TrackId,
Name = studentTrack.Track.Name,
Deadline = studentTrack.Deadline,
PercentComplete = CalculatePercentCompletion(studentTrack)
});
}private int CalculatePercentCompletion(StudentTrack studentTrack)
{
var courses = _dbContext.EducationPortalStudentCourses
.Where(course => course.TrackId == studentTrack.Id
&& course.EmployeeId == studentTrack.EmployeeId)
.ToList();
return courses.Count(course => course.Status == true) * 100 / courses.Count
}public IEnumerable<StudentTrack> GetStudentTracks(int employeeId)
{
return _dbContext.EducationPortalStudentTracks
.Where(studentTrack => studentTrack.EmployeeId == employeeId)
.OrderBy(studentTrack => studentTrack.Track.Id)
.ToList()
.Select(studentTrack =>
new StudentTrack
{
Id = studentTrack.Id,
TrackId = studentTrack.TrackId,
Name = studentTrack.Track.Name,
Deadline = studentTrack.Deadline,
PercentComplete = CalculatePercentCompletion(studentTrack)
});
}Context
StackExchange Code Review Q#54004, answer score: 6
Revisions (0)
No revisions yet.