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
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