C# Clean Code
Introduction
Code smells are warning signs, not bugs by themselves. They indicate design debt that will slow future changes.
This post covers high-impact smells and corresponding refactors.
1) Long method -> Extract method
If a method exceeds one concern or one abstraction level, split it.
public async Task PlaceOrderAsync(OrderRequest request, CancellationToken ct)
{
ValidateRequest(request);
var total = CalculateTotal(request.Items);
var order = await PersistOrderAsync(request.CustomerId, total, ct);
await SendConfirmationAsync(order, ct);
}
2) Data clumps -> Parameter object
Repeated parameter groups should become a type.
public record Address(string Line1, string City, string ZipCode);
public record CreateUserRequest(string FirstName, string LastName, string Email, Address Address);
This improves consistency and reduces call-site errors.
3) Primitive obsession -> Value objects
Domain concepts should not always be string/int.
public readonly record struct OrderId(int Value);
public readonly record struct CustomerId(int Value);
Compiler-checked types prevent ID mix-ups.
4) Magic numbers -> Named constants
private const decimal BulkOrderThreshold = 500m;
private const decimal BulkDiscountRate = 0.10m;
if (order.Total > BulkOrderThreshold)
{
order.Discount = order.Total * BulkDiscountRate;
}
Named constants explain intent and centralize change.
5) Duplicate code -> Extract shared function
public static string FormatFullName(string? firstName, string? lastName)
=> string.IsNullOrWhiteSpace(firstName) && string.IsNullOrWhiteSpace(lastName)
? "Unknown"
: $"{firstName} {lastName}".Trim();
Removing duplication lowers bug probability and maintenance cost.
6) Nested conditionals -> Guard clauses
public decimal GetDiscount(Customer customer)
{
if (customer is null) return 0m;
if (!customer.IsActive) return 0m;
return customer.OrderCount > 10 ? 0.15m : 0.05m;
}
Flat logic is easier to reason about and test.
7) Repeated type switch -> Polymorphism
If you repeatedly switch on type/enum to choose behavior, move behavior into subtype implementations.
public abstract class PriceRule
{
public abstract decimal Apply(decimal total);
}
public class StandardPriceRule : PriceRule
{
public override decimal Apply(decimal total) => total;
}
public class PremiumPriceRule : PriceRule
{
public override decimal Apply(decimal total) => total * 0.9m;
}
Refactor safely checklist
- Add or update tests first
- Refactor in small commits
- Keep behavior unchanged
- Rename with intention
- Remove dead code after migration
Summary
Refactoring is continuous, not a one-time event. Focus on smells that block change speed: long methods, duplication, primitive obsession, and nested conditionals.