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
36: Grouping and Joining with LINQ
I see this all the time when developers first hit LINQ: they treat GroupBy as if it's a shortcut for creating a Dictionary. They'll write a grouping expression and then immediately try to access a specific group using a key, like myGroups["Electronics"], only to be greeted by a compiler error that makes no sense to them. They assume the result is a map, but in reality, LINQ is giving them a sequence of sequences.
Grouping is a sequence, not a lookup table
Let's look at why that assumption fails. Imagine we're building a simple inventory system for a bookstore. We have a list of books, each with a category.
var books = new List<Book>
{
new Book { Title = "The Hobbit", Category = "Fantasy" },
new Book { Title = "Mistborn", Category = "Fantasy" },
new Book { Title = "Clean Code", Category = "Technical" },
new Book { Title = "Refactoring", Category = "Technical" },
new Book { Title = "The Great Gatsby", Category = "Classic" }
};
// The common mistake: thinking this returns a Dictionary<string, List<Book>>
var groupedBooks = books.GroupBy(b => b.Category);
// This will NOT compile:
// var fantasyBooks = groupedBooks["Fantasy"];
The reason this fails is that GroupBy returns an IEnumerable<IGrouping<TKey, TElement>>. An IGrouping is essentially a list that also happens to know what its own key is. It's a collection of groups, not a map to them. If you want to actually get a dictionary, you have to explicitly call .ToDictionary() after the group. But usually, you don't need to. You just iterate over the groups.
foreach (var group in groupedBooks)
{
Console.WriteLine($"Category: {group.Key}"); // The key is built into the group itself
foreach (var book in group)
{
Console.WriteLine($" - {book.Title}");
}
}
Stop using Where() to link two collections
Now, let's talk about joining. When you need to combine data from two different lists—say, Customers and Orders—a lot of people fall into the trap of nesting a Where clause inside a Select. It looks "clean" and intuitive, but it's a performance nightmare. I call it the "N+1" habit; for every customer, you're scanning the entire orders list from scratch.
Here is the "wrong" way (the slow way):
var customerOrders = customers.Select(c => new
{
CustomerName = c.Name,
Orders = orders.Where(o => o.CustomerId == c.Id).ToList()
});
If you have 1,000 customers and 10,000 orders, that's potentially 10 million comparisons. Instead, use the Join method. It uses a hash table internally, making the operation drastically faster because it only passes through each list once.
While you can use method syntax, I'll admit that Join is one of the few places where the "Query Syntax" (which looks like SQL) is actually more readable. I use it even in professional production code because it clarifies the intent.
var orderDetails = from customer in customers
join order in orders on customer.Id equals order.CustomerId
select new
{
CustomerName = customer.Name,
OrderDate = order.Date,
Total = order.Amount
};
Note the equals keyword. You can't use == here. It's a quirk of the LINQ query language, and it'll trip you up if you forget it. If you prefer the method syntax, it looks like this: customers.Join(orders, c => c.Id, o => o.CustomerId, (c, o) => new { ... }). Personally, I find the method syntax for joins to be a bit of a cognitive mess, so stick to the query syntax for your joins and you'll be much happier.
📋 Practical Task
Exercise: Generating a Sales Performance Report
You are tasked with creating a report that shows the total revenue generated by each product category. You have two data sources: a list of Product objects and a list of Sale objects.
Requirements:
- Create a
Productclass withId,Name, andCategory. - Create a
Saleclass withProductIdandPrice. - Populate both lists with at least 5 products (across 3 different categories) and 10 sales.
- Use a
Jointo link the sales to their respective products. - Use
GroupByto group the joined results by the product's category. - Calculate the sum of the prices for each category and print the result in the format:
"Category: [Name], Total Revenue: [Amount]".
Constraint: Do not use nested Where clauses to link the products and sales; use the join keyword or the .Join() method.
There are no comments for now.