I recently added a top ten musicians of the week to my PracticeTime application.
Normally, I would have used some sort of view or stored procedure for this. However, those strategies are hard to unit test and modify. I need data in the database to test if I use either database-centric approach.
LINQ and EF6 provides an easy way to achieve the same thing but allows for maintainable code. Also, because it’s C# the unit tests against it can be included in code coverage reports.
The end result is to a list of UserData objects:
public class UserData { public string UserName { get; set; } public int TimeThisWeek { get; set; } public int RankThisWeek { get; set; } }
The business rules are this:
- Only consider practice sessions from the last 7 days
- Sum all the practice sessions from that period
- Only show the top ten
- Rank by time in descending order
var allFromLastWeek = context.Sessions.AsNoTracking() .Include(x => x.User) .Where(x => x.SessionDateTimeUtc > lastWeek) .GroupBy(x => x.User.UserName) .Select(group => new UserData() { UserName = group.Key, TimeThisWeek = group.Sum(x => x.Time) }).OrderByDescending(x => x.TimeThisWeek).Take(10).ToList();
Honestly, it took me a while to get this. The gotcha for me is that GroupBy returns a lists of lists, List<string,Session> in this instance. From that, it’s just a matter of using select to return a new UserData object populated with the UserName and summation of Time values. Lastly, sort and take the top ten.
Note that I have a foreign key between User (AspNetUser actually) and Session. This is required if you’re going to use this type of approach.
The unit tests is very simple. I add dummy data to the database and execute the method:
using (TransactionScope scope = new TransactionScope()) { UserStore<ApplicationUser> store = new UserStore<ApplicationUser>(new PracticeTimeContext()); ApplicationUser user = store.FindByNameAsync("student").Result; ApplicationUser user2 = store.FindByNameAsync("student2").Result; SessionRepository repo = new SessionRepository(); Session retval1 = repo.Add(new Session() { C_InstrumentId = 1, SessionDateTimeUtc = DateTime.UtcNow, Time = 300, TimeZoneOffset = 300, Title = "blah", UserId = user.Id }); ..... var retval = repo.GetTopUsersThisWeek(); Assert.AreEqual(2,retval.Count); Assert.AreEqual(1,retval[0].RankThisWeek); Assert.AreEqual(2, retval[1].RankThisWeek); Assert.AreEqual(400, retval[0].TimeThisWeek); Assert.AreEqual(100, retval[1].TimeThisWeek); Assert.AreEqual("student",retval[0].UserName); }
Notice, I wrap everything up in a TransactionScope? This lets me keep the database clean.
Key Points:
- Use LINQ to simplify you data access queries
- A data model with good referential integrity is important for LINQ
- LINQ is easy to test and allows for measuring coverage