Skip to Content
Course content

100: Writing Idiomatic C#

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

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 foreach loop and if blocks with a LINQ chain.
  • Use a record for the Notification class.
  • Use the null-conditional operator (?.) and the null-coalescing operator (??) to handle potentially null User or Message objects.
  • 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;
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.