C# Code Smells and Refactoring Patterns That Matter — Clean Code

2026/04/092 min read
bookmark this

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.