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
🖼️ Image: Visual Studio Code Metrics Panel
A screenshot of Visual Studio’s Code Metrics results showing Maintainability Index, Cyclomatic Complexity, etc.
📊 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
🖼️ Image: Metric Definitions Heatmap Table
A visual summary showing good, warning, and bad ranges for each metric (green-yellow-red).
✅ 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);
}
}
🖼️ Image: OrdersController Before Refactor
Highlight that business logic, DB operations, and validation are all mixed in the controller.
⚠️ 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);
}
}
🖼️ Image: Refactored Architecture Diagram
Show controller → service → dbContext pattern (clean layering).
🎯 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 |
🖼️ Image: Before vs After Metrics Bar Chart
Visual comparison of metric values before and after refactoring.
🧠 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-qualitytools
🖼️ Image: CI/CD Quality Gate Flowchart
Show code → build → analyze → quality gate → deploy.
🧾 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!
✍️ Written by Rupinder Kaur, a Senior .NET Developer passionate about clean architecture, refactoring, and continuous improvement.
Top comments (0)