C# Clean Code

2026/04/212 min read
bookmark this

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 readonly field 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.