Skip to Content
Course content

155: Building a Personal Finance Tracker with LINQ Reports

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When I first started building data-heavy applications, I had a tendency to treat every data request like a manual scavenger hunt. If I needed a report—say, "How much did I spend on coffee in October?"—I would write a loop, initialize a running total variable, check a few conditions inside an if statement, and manually increment that total. It feels intuitive because it's exactly how you'd do it with a highlighter and a bank statement, but in C#, this "imperative" style quickly becomes a maintenance nightmare as your reporting requirements grow.

The "Manual Loop" Grind

Imagine we have a Transaction class with a Category, Amount, and Date. If you wanted to see total spending per category for a specific month, the naive approach looks something like this:

var monthlyTotals = new Dictionary<string, decimal>();
foreach (var t in transactions)
{
    if (t.Date.Month == 10 && t.Date.Year == 2023)
    {
        if (!monthlyTotals.ContainsKey(t.Category))
        {
            monthlyTotals[t.Category] = 0;
        }
        monthlyTotals[t.Category] += t.Amount;
    }
}

Now, this works. But look at what's happening here. You aren't actually describing what you want; you're describing how to move bits around in memory. You're managing the state of a dictionary, handling the "first-time" entry for every category, and mixing your filtering logic (the date check) with your aggregation logic (the sum). If I asked you to suddenly sort those totals from highest to lowest, you'd have to write yet another loop or a complex sorting delegate. It's brittle and noisy.

Declarative Reporting with LINQ

The better way is to stop thinking about the foreach loop and start thinking about the data as a pipeline. With LINQ, we can chain operations to transform a raw list of transactions into a finished report in a single, readable expression. I prefer this because it separates the "what" from the "how."

var report = transactions
    .Where(t => t.Date.Month == 10 && t.Date.Year == 2023)
    .GroupBy(t => t.Category)
    .Select(group => new 
    { 
        Category = group.Key, 
        Total = group.Sum(t => t.Amount) 
    })
    .OrderByDescending(r => r.Total);

I love this version because it reads like a sentence: "Filter by date, group by category, project into a new object with the sum, and order it by the total." If the requirements change—for example, if we only want to see categories where you spent more than $100—you just drop in another .Where(r => r.Total > 100) before the ordering. No need to nest more if statements inside a loop.

Where the Trade-off Actually Hits

You might wonder if this is "slower" because of the abstraction. Technically, LINQ involves creating delegates and iterator objects, which adds a tiny bit of overhead compared to a raw foreach loop. In a high-frequency trading system where every microsecond counts, I'd tell you to stick to the loop. But for a personal finance tracker? You're dealing with thousands of records, not billions. The performance difference is negligible, but the "developer performance" difference—the time it takes for you or a teammate to understand this code six months from now—is massive.

The only real trap to watch out for is deferred execution. Remember that the LINQ query doesn't actually run until you iterate over report (like in a foreach loop or by calling .ToList()). If you're modifying the underlying transactions list between defining the query and executing it, you'll get the updated values, which can lead to some very confusing bugs if you aren't expecting it.




📋 Practical Task

Implementing a Category Over-Budget Alert Report

You have a List<Transaction> and a Dictionary<string, decimal> called budgetLimits (where the key is the category and the value is the maximum allowed spend for that category).

Write a LINQ query that generates a list of "Budget Alerts." An alert should be created for any category where the total spending across all transactions exceeds the limit defined in the budgetLimits dictionary.

The final result should be a collection of anonymous types or a custom class containing:

  • The Category name.
  • The ActualSpend (the sum of transactions).
  • The Overage (ActualSpend minus the Limit).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.