Cyclomatic complexity is a 1976 metric that refuses to die, for a good reason: it maps directly to real pain. McCabe defined it on the control flow graph as M = E - N + 2P, which for a normal single-entry, single-exit method collapses to 1 + the number of decision points. That number is the minimum count of test cases needed to exercise every independent path through the method.

The C# specifics are more fiddly than most people assume:

  • Counted: if, while, for, foreach, case, default, continue, goto, &&, ||, catch, ?:, ??, pattern-matching and/or.
  • Not counted: else (already implied by its if), do, the switch keyword itself, try, using, throw, finally, return, object creation, method calls, field access.
  • A switch adds one per case plus one for default — six cases and a default is +7, no credit for the switch keyword.
  • await doesn’t count, but the if/while/catch around it do. LINQ operators don’t count; the lambdas you pass them are analysed as their own methods.

Thresholds have converged without ever being sacred:

  • 1–10 is McCabe’s original recommendation; 11–20 earns a second pair of eyes in review.
  • 21–50 is a strong refactoring candidate; above 50 is effectively untestable.
  • Microsoft’s CA1502 analyzer calls anything over 25 excessive. Mark Seemann argues for roughly 7, echoing Miller’s seven-plus-or-minus-two.
  • Context decides: parsers, serializers and state machines habitually live at 15–25 and are fine. Business logic at 25 almost never is.

The part worth taking away is that the raw score is only half the picture. Pair it with coverage and you get the CRAP score — CC² × (1 - coverage)³ + CC, scaling with the square of complexity and the cube of uncovered percentage. A method at CC 30 with full coverage is less of a liability than one at CC 12 with none. The same logic applies at the enforcement layer: on a legacy codebase, don’t chase a flat threshold. Baseline it, then warn only when an already-complex method gets more complex. Stable ugly methods rarely wake you up; methods that keep growing do.

The reduction techniques are unglamorous and effective. Extract method — total complexity across the extracted methods actually rises slightly, but per-method complexity, which is the unit a person reasons about, falls. Guard clauses and early returns, which don’t lower the count at all but lower cognitive load, since every later line can assume valid input. Polymorphism instead of a type-discriminator switch, which can collapse the central method to CC 1. Switch expressions, lookup tables for pure input→output mappings, and splitting methods that take several bool flags — “several methods in a trench coat.”

The quiet conclusion is that a nearly fifty-year-old metric works best as a risk input rather than a verdict: complexity × missing coverage × rate of change. Measure all three, and it becomes a modern early-warning system for technical debt instead of a number that makes people game extract-method.