Simplify Your .NET Codebase: A Guide to Code Metrics and Controller Refactoring



This content originally appeared on DEV Community and was authored by Rupinder Kaur

Have you ever opened an API controller and thought, “Whoa, this looks like spaghetti”? If you’re working with legacy .NET projects or scaling up a growing codebase, complexity can creep in quickly. That’s where Visual Studio’s Code Metrics come to the rescue.

In this post, I’ll walk you through:

  • What each code metric means
  • How to use them to identify messy code
  • A real-life example of refactoring a bloated API controller
  • A checklist to guide your review process

Let’s dive in!

What Are Code Metrics?

Code metrics help quantify how maintainable, complex, and testable your code is. Visual Studio provides these metrics via:

Analyze → Analyze Code Metrics

VS code metrics

Code Metrics Explained

Here’s what each metric means and why it matters:

1. Maintainability Index (MI)

  • Definition: A numeric score (0–100) indicating how easy it is to maintain a code element.
  • ✅ 80–100: Easy to maintain
  • ⚠ 50–79: Moderate
  • ❌ 0–49: Hard to maintain

2. Cyclomatic Complexity (CC)

  • Definition: The number of independent paths through the code. Increases with if, switch, for, etc.
  • ✅ <10: Simple
  • ⚠ 10–20: Moderate
  • ❌ >20: Complex

3. Depth of Inheritance

  • Definition: The number of class levels above the current class in the hierarchy.
  • ✅ 0–3: Ideal
  • ❌ >4: May be problematic

4. Class Coupling

  • Definition: Number of external classes/types referenced.
  • ✅ <10: Low coupling (preferred)
  • ❌ >20: High coupling (bad for maintainability)

5. Lines of Executable Code

  • Definition: Number of actual executable lines of code (ignores comments/braces).
  • ✅ <200 per class, <50 per method
  • ❌ >500: Too complex, consider breaking apart

Code Metrics Review Checklist

Here’s a handy checklist to evaluate your API controllers or classes:

  • MI < 50? Refactor needed!
  • CC > 10? Simplify or extract methods.
  • Inheritance depth > 3? Consider flattening it.
  • Class coupling > 10? Break dependencies, inject interfaces.
  • Too many LOC? Break into smaller classes/services.

Real-Life Example: Refactoring a Bloated API Controller

Let’s say you have this OrdersController. Everything works, but the code smells.

public class OrdersController : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> CreateOrder(OrderDto dto)
    {
        if (dto == null || dto.Items.Count == 0)
            return BadRequest("Invalid order");

        var customer = await _context.Customers.FindAsync(dto.CustomerId);
        if (customer == null)
            return NotFound("Customer not found");

        var order = new Order
        {
            CustomerId = dto.CustomerId,
            CreatedAt = DateTime.UtcNow,
            Items = dto.Items.Select(x => new OrderItem
            {
                ProductId = x.ProductId,
                Quantity = x.Quantity,
                Price = x.Price
            }).ToList()
        };

        _context.Orders.Add(order);
        await _context.SaveChangesAsync();

        _logger.LogInformation($"Order {order.Id} created");

        return Ok(order.Id);
    }
}

Code Metrics Report

Metric Value Issue
Maintainability Index 47 ❌ Too low
Cyclomatic Complexity 15 ⚠ Too many branches
Class Coupling 12 ⚠ High dependencies
Executable LOC 80 ⚠ Too much logic in one method

Refactoring Plan

We move logic to a new OrderService.

public class OrderService : IOrderService
{
    public async Task<int> CreateOrderAsync(OrderDto dto)
    {
        var customer = await _context.Customers.FindAsync(dto.CustomerId)
                      ?? throw new Exception("Customer not found");

        var order = new Order
        {
            CustomerId = dto.CustomerId,
            CreatedAt = DateTime.UtcNow,
            Items = dto.Items.Select(x => new OrderItem
            {
                ProductId = x.ProductId,
                Quantity = x.Quantity,
                Price = x.Price
            }).ToList()
        };

        _context.Orders.Add(order);
        await _context.SaveChangesAsync();
        _logger.LogInformation($"Order {order.Id} created");

        return order.Id;
    }
}

Updated controller:

[HttpPost]
public async Task<IActionResult> CreateOrder(OrderDto dto)
{
    try
    {
        var orderId = await _orderService.CreateOrderAsync(dto);
        return Ok(orderId);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error creating order");
        return BadRequest(ex.Message);
    }
}

After Refactoring Metrics

Metric Value Improvement
Maintainability Index 90 ✅ Big boost
Cyclomatic Complexity 3 ✅ Clean logic
Class Coupling 4 ✅ Reduced
Executable LOC 20 ✅ Much leaner

Lessons Learned

  • Metrics are not just numbers—they’re clues to identify code smells.
  • Refactoring based on metrics leads to better testability, scalability, and developer happiness.
  • Good architecture grows from clean, measurable decisions.

Bonus: Automate It in CI/CD

You can integrate metrics analysis into your DevOps pipeline using:

  • Roslyn Analyzers
  • NDepend
  • SonarQube
  • dotnet code-quality tools

Final Thoughts

Taking time to analyze and improve code complexity today will save you countless hours of debugging and scaling pain tomorrow. Start small—run metrics on one class per sprint.

🔁 Refactor. 🔬 Measure. ✅ Repeat.

💬 How do you manage complexity in your .NET projects? Share your tips below!


This content originally appeared on DEV Community and was authored by Rupinder Kaur