C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
155: Building a Personal Finance Tracker with LINQ Reports
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).
There are no comments for now.