Modern C# Features That Improve Code Readability — Clean Code
Introduction
Modern C# can remove boilerplate, but concise code is only useful when it remains clear and safe.
This post focuses on practical feature usage in production.
1) Records for immutable data
public record OrderDto(int Id, string CustomerName, decimal Total);
public readonly record struct Money(decimal Amount, string Currency);
Use records for DTOs and value objects where immutability and value equality are beneficial.
2) Pattern matching for clearer branching
public string GetStatusMessage(Order order) => order.Status switch
{
OrderStatus.Pending => "Your order is being processed.",
OrderStatus.Shipped => $"Shipped on {order.ShippedDate:d}.",
OrderStatus.Delivered => "Delivered!",
OrderStatus.Cancelled => "This order was cancelled.",
_ => "Unknown status."
};
Compared to nested if/else, this is easier to scan and extend.
3) Collection expressions (C# 12)
List<string> statuses = ["Active", "Inactive", "Pending"];
Prefer this syntax for concise collection initialization.
4) Raw string literals for SQL/JSON/templates
string query = """
SELECT Id, Name, Email
FROM Customers
WHERE IsActive = 1
ORDER BY Name
""";
Great for readability when strings contain quotes/newlines.
5) Primary constructors: when to use and avoid
Primary constructors reduce boilerplate, especially in DI-heavy services.
public class OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
public async Task<Order?> GetByIdAsync(int id, CancellationToken ct)
{
logger.LogInformation("Fetching order {OrderId}", id);
return await repo.GetByIdAsync(id, ct);
}
}
Important tradeoff
Primary constructor parameters are captured and can be reassigned inside the class body.
If you need strict readonly guarantees, constructor validation, or overloads, prefer a traditional constructor with private readonly fields.
public class PriceCalculator
{
private readonly decimal _taxRate;
public PriceCalculator(decimal taxRate)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(taxRate);
_taxRate = taxRate;
}
}
Decision guide
Use primary constructor when:
- class is simple
- dependency injection is straightforward
- no complex initialization required
Use traditional constructor when:
- you need guard clauses and transformations
- you require
readonlyfield safety - you need multiple constructor overloads
Summary
Modern C# features are powerful, but they are tools, not goals. Choose the feature that improves clarity and correctness for your specific class design.