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
100: Writing Idiomatic C#
By the time you've learned the syntax of C#, you can make the computer do almost anything. But there is a big difference between code that works and code that feels like C#. I often see developers coming from Java or C++ who write C# as if it were those languages—it's technically correct, but it's verbose and fights the framework. Writing idiomatic C# is about embracing the declarative nature of the language to reduce "noise."
The Clutter of Manual State Management
Let's look at a common task: filtering a list of orders to find high-value customers and formatting their names for a report. A naive approach usually involves creating a temporary list, looping through the data, and manually checking conditions. It looks like this:
public List<string> GetHighValueCustomers(List<Order> orders)
{
var results = new List<string>();
foreach (var order in orders)
{
if (order != null && order.Total > 1000)
{
if (order.CustomerName != null)
{
results.Add(order.CustomerName.ToUpper());
}
}
}
return results;
}
Now, this is "safe" code, but it's an eyesore. You're spending more time managing the mechanism of the loop and the null checks than you are describing the intent of the logic. When I review code like this, my first thought is that the developer is treating C# as a low-level imperative language. We have LINQ for a reason.
Declarative Intent with LINQ and Null-Conditionals
The idiomatic way to handle this is to treat your collection as a stream of data. Instead of telling the computer how to loop, you tell it what you want. Combine this with the null-conditional operator (?.) and the null-coalescing operator (??), and the noise vanishes:
public List<string> GetHighValueCustomers(IEnumerable<Order> orders)
{
return orders
.Where(o => o is { Total: > 1000 })
.Select(o => o.CustomerName?.ToUpper() ?? "UNKNOWN")
.ToList();
}
Notice a few things here. First, I changed the input to IEnumerable<Order>. Unless you specifically need to add or remove items from the list, always use the most general interface possible. It makes your method more flexible. Second, I used a property pattern { Total: > 1000 }. This not only checks that the order isn't null but also checks the property in one clean motion. It's a pattern match, and it's significantly more readable than a chain of if statements.
The Cost of Verbose Data Containers
Another place where I see "non-idiomatic" C# is in data models. Old-school C# relied heavily on classes with private fields and public getters/setters. If you're just moving data around—like a DTO (Data Transfer Object)—writing twenty lines of boilerplate for a simple object is a waste of your time.
Stop doing this:
public class Order
{
private decimal _total;
public decimal Total
{
get { return _total; }
set { _total = value; }
}
}
And start using records. Introduced in C# 9, records are the idiomatic choice for data-centric types. They give you value-based equality and conciseness out of the box:
public record Order(string CustomerName, decimal Total);
That's it. One line. The compiler generates the properties, the constructor, and the equality logic for you. I personally find that using records forces you to think more about immutability, which leads to fewer bugs in multi-threaded environments. If you don't need to change the value after the object is created, don't give it a setter.
Choosing Readability Over "Cleverness"
A word of caution: there is a tipping point. I've seen developers go too far with LINQ, creating "one-liners" that are twenty lines long and impossible to debug. If a LINQ query requires more than three or four operators and complex nested logic, break it up. Idiomatic C# isn't about the fewest lines of code; it's about the most expressive lines of code. If you have to squint to understand what a query is doing, you've traded readability for brevity, and that's a bad trade.
📋 Practical Task
Refactoring the Legacy Notification Pipeline
You've inherited a piece of code that processes user notifications. It's written in an old, imperative style that is hard to maintain. Your task is to refactor the ProcessNotifications method to be idiomatic C#.
Requirements:
- Replace the
foreachloop andifblocks with a LINQ chain. - Use a
recordfor theNotificationclass. - Use the null-conditional operator (
?.) and the null-coalescing operator (??) to handle potentially nullUserorMessageobjects. - Ensure the method returns a
List<string>containing only the messages of users who are "Active" and have a message length greater than 5 characters.
// REFACTOR THIS CODE
public class Notification
{
public User User { get; set; }
public string Message { get; set; }
}
public class User
{
public string Name { get; set; }
public bool IsActive { get; set; }
}
public class NotificationService
{
public List<string> ProcessNotifications(List<Notification> notifications)
{
var result = new List<string>();
foreach (var n in notifications)
{
if (n != null && n.User != null)
{
if (n.User.IsActive)
{
if (n.Message != null && n.Message.Length > 5)
{
result.Add(n.User.Name + ": " + n.Message);
}
}
}
}
return result;
}
}There are no comments for now.