Understanding Cyclomatic Complexity -- NDepend (original) (raw)
May 25, 2026 9 minutes read
Cyclomatic Complexity (or CC) in C# is a code metric that counts the number of linearly independent execution paths through a method. Concretely, it is computed as 1 plus the number of branching constructs in the method body (such as if, while, for, case, &&, ||, ?: and ??). The higher the score, the harder the method is to read, test and safely change. A score of 1 means a single straight path, around 10 is the traditional upper bound recommended by Thomas McCabe, and anything above 25 is flagged as excessive by Microsoft’s CA1502 analyzer.
This guide explains, with C# examples, how Cyclomatic Complexity is calculated, what thresholds matter in practice, how to measure and visualize it in real .NET codebases, and how to go beyond the raw score by pairing it with test coverage and IL-level analysis.
- What is Cyclomatic Complexity?
- Definition of Cyclomatic Complexity in C#
- Example of Cyclomatic Complexity Impact in C#
- Cyclomatic Complexity Thresholds: What Score Is Too High?
- Measuring Cyclomatic Complexity in C#
- Ruling C# Cyclomatic Complexity
- Visualizing C# Cyclomatic Complexity
- C# Cyclomatic Complexity and Tests
- Going Beyond Cyclomatic Complexity
- How to Reduce Cyclomatic Complexity in C#
- Frequently Asked Questions
- What is a good Cyclomatic Complexity score in C#?
- Does the else keyword increase Cyclomatic Complexity?
- Does a switch statement count once or per case?
- Does Cyclomatic Complexity equal the number of unit tests I need?
- What is the difference between Cyclomatic Complexity and Cognitive Complexity?
- How do I measure Cyclomatic Complexity in Visual Studio?
- Does Cyclomatic Complexity work on async or LINQ code?
- Conclusion
Cyclomatic Complexity was introduced by Thomas J. McCabe in 1976 as a way to quantify the structural complexity of a piece of code. The idea comes from graph theory: every method can be represented as a control flow graph where nodes are blocks of statements and edges are jumps between them. On that graph, the Cyclomatic Complexity is given by the classic formula:
where E is the number of edges, N the number of nodes, and P the number of connected components. For a regular method with a single entry and a single exit, this collapses to 1 + the number of decision points, which is the form most tools actually compute.
What this number really tells you is the minimum number of test cases you need to exercise every independent path through the method. That is why Cyclomatic Complexity has stuck around for almost half a century: it is a structural metric, but it has a very concrete operational meaning for everyone who has to maintain or test the code.
Definition of Cyclomatic Complexity in C#
The Cyclomatic Complexity for a C# method is concretely 1 + {the number of following expressions found in the body of the method}:
| | if while for foreachcase default continuegoto && || catchternary operator ?: ??and or | | ------------------------------------------------------------------------------------------------- |
The following expressions are not counted for CC computation:
| | else do switch try usingthrow finally returnobject creationmethod callfield access | | ------------------------------------------------------------------------------------- |
Two details that trip people up: else does not increment the score because the alternative path was already created by its matching if; and a switch contributes one unit per case (and one for default), not one for the switch keyword itself. C# pattern-matching constructs (and, or, the modern switch expression with patterns) also add to the score, the same way their classic counterparts do.
Example of Cyclomatic Complexity Impact in C#
Exhibiting a Complex Method
Here is a complex method with entangled if and else scopes. The keyword if is used six times and && is used once. Hence its Cyclomatic Complexity score is 8:
| 1234567891011121314151617181920212223242526272829303132333435 | public static class OrderLogic { public static void ProcessOrder( int orderId, bool isPriority, bool isInternational, bool isGift, bool isCouponApplied, decimal orderTotal) { if (orderId <= 0) { Console.WriteLine("Invalid order ID."); return; } if (isPriority) { Console.WriteLine("Processing priority order."); if (isInternational) { Console.WriteLine("Processing international priority order."); if (isGift) { Console.WriteLine("This is a gift order."); } } } else { Console.WriteLine("Processing standard order."); if (isInternational) { Console.WriteLine("Processing international standard order."); } } if (isCouponApplied && orderTotal > 100) { Console.WriteLine("Applying discount for orders over $100."); } else { Console.WriteLine("No discount applicable."); } }} |
|---|
Eight independent paths means at least eight tests to fully cover this single method, plus a non-trivial amount of head-scratching every time someone has to add a new business rule. This is exactly the kind of method where a regression slips in unnoticed.
Refactoring the Complex Method in Several Simpler Methods
The method above can be refactored into several less complex methods. In the code we use CC to refer to each method Cyclomatic Complexity score:
| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748 | public static class OrderLogic { public static void ProcessOrder( int orderId, bool isPriority, bool isInternational, bool isGift, bool isCouponApplied, decimal orderTotal) { // CC 2 if (!IsValidOrder(orderId)) return; ProcessOrderType(isPriority, isInternational, isGift); ApplyDiscountIfEligible(isCouponApplied, orderTotal); } private static bool IsValidOrder(int orderId) { // CC 2 if (orderId <= 0) { Console.WriteLine("Invalid order ID."); return false; } return true; } private static void ProcessOrderType( bool isPriority, bool isInternational, bool isGift) { // CC 5 if (isPriority) { Console.WriteLine("Processing priority order."); if (isInternational) { Console.WriteLine("Processing international priority order."); } } else { Console.WriteLine("Processing standard order."); if (isInternational) { Console.WriteLine("Processing international standard order."); } } if (isGift) { Console.WriteLine("This is a gift order."); } } private static void ApplyDiscountIfEligible( // CC 3 bool isCouponApplied, decimal orderTotal) { if (isCouponApplied && orderTotal > 100) { Console.WriteLine("Applying discount for orders over $100."); } else { Console.WriteLine("No discount applicable."); } }} |
|---|
Benefits of Refactoring
- Simpler Control Flow: The main method now delegates specific tasks to smaller, more focused methods.
- Easier to Test: You can test each smaller method independently.
- Lower Cyclomatic Complexity: The complexity is spread across multiple methods, making each method easier to understand and maintain independently.
- Better Naming: Method names like
IsValidOrderorApplyDiscountIfEligibledocument intent, so a reader does not have to mentally simulate the body to understand the high-level flow.
Note that the total Cyclomatic Complexity summed across the four methods is actually slightly higher than the original 8. That is fine and even expected. What matters for maintainability is the complexity per method, because that is the unit a developer has to reason about at a time.
Cyclomatic Complexity Thresholds: What Score Is Too High?
There is no single sacred number, but the literature converges around the same ranges. The table below summarises what most teams and tools use as a guideline:
| Cyclomatic Complexity | Risk profile | Practical interpretation |
|---|---|---|
| 1 – 10 | Simple, low risk | McCabe’s original recommendation. Easy to test, easy to read. |
| 11 – 20 | Moderately complex | Still manageable, but worth a second pair of eyes during review. |
| 21 – 50 | Complex, high risk | Hard to test exhaustively. Strong refactoring candidate. |
| > 50 | Untestable | Bug magnets. Often legacy hotspots that need to be broken down. |
Two reference points are worth keeping in mind. McCabe himself recommended splitting modules that exceed a Cyclomatic Complexity of 10. Microsoft’s CA1502 analyzer defines “excessive complexity” as a score greater than 25 by default. Mark Seemann argues for an even tighter ceiling of around 7, mirroring Miller’s “magical number seven, plus or minus two” for human short-term memory.
In practice the right threshold depends on the codebase. A parser, a serializer or a state machine will routinely live in the 15-25 range without being objectively bad. A piece of business logic that scores 25 almost always is.
Measuring Cyclomatic Complexity in C#
You can’t improve what you don’t measure, so using a tool to evaluate code complexity is essential. Calculating this metric helps developers identify areas that might need refactoring to improve code quality.
NDepend is a great option for this, as it measures the cyclomatic complexity of methods in C# code. For instance, it includes the Search Methods by Complexity feature, which helps identify complex methods for further analysis.
Visual Studio itself ships a “Calculate Code Metrics” command (Analyze > Calculate Code Metrics) that reports Cyclomatic Complexity per method, type and assembly. The CA1502 analyzer can be wired into your build to actually fail on methods above a configured threshold, which is useful for new code. Roslyn-based analyzers like SonarAnalyzer.CSharp and third-party tools such as ReSharper or CodeRush also surface the same metric inside the editor.
Ruling C# Cyclomatic Complexity
NDepend offers several rules like Avoid methods too big, too complex that flag methods with excessively high Cyclomatic Complexity scores, highlighting potential issues in the code.
You are probably working with a large legacy codebase, making it impractical to refactor every complex method. This is why it’s essential to measure Cyclomatic Complexity against a baseline, allowing you to focus on new or refactored methods that are too complex. There are two rules for that:
- From now, all methods added should respect basic quality principles
- Avoid making complex methods even more complex
This baseline-driven approach matters more than any absolute threshold. When you start tracking complexity on a well-established codebase, you will inevitably find complex methods that have been stable and well-tested for years. The real risk is not the static score, it is what happens when those methods start growing. Using NDepend’s CQLinq, you can express that idea directly:
| | // Cyclomatic Complexity got worsewarnif count > 0from m in JustMyCode.Methodswhere m.CodeWasChanged() && m.OlderVersion().CyclomaticComplexity < m.CyclomaticComplexity && m.OlderVersion().CyclomaticComplexity > 10select new { m, OldComplexity = m.OlderVersion().CyclomaticComplexity, m.CyclomaticComplexity } | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
The query warns whenever an already-complex method (CC > 10) becomes even more complex between two analysis snapshots. In other words, it surfaces the modifications that are actually risky, while leaving stable legacy untouched.
Visualizing C# Cyclomatic Complexity
A colored treemap can be used to visualize the cyclomatic complexity of your C# methods. In this visualization, each rectangle represents a method:
- The size of the rectangle corresponds to the number of statements in the method.
- The color of the rectangle reflects the method’s cyclomatic complexity.
The advantage of a treemap over a flat list is that complexity hotspots literally jump out of the picture: a large, dark red rectangle inside an otherwise calm area is exactly the kind of method that deserves an architecture conversation.
C# Cyclomatic Complexity and Tests
Writing tests for your code is nowadays an essential practice for every professional C# developer. Typically, a test covers a single execution path, while the Cyclomatic Complexity score of a method represents the number of independent execution paths. Therefore, Cyclomatic Complexity provides a rough estimate of how many tests are required to fully test a method.
By running tests, you can determine the code coverage for each method. A method partially covered means that not all its independent execution paths are challenged by tests. The rule Methods should have a low C.R.A.P score spots methods that both have high Cyclomatic Complexity scores and are poorly tested (C.R.A.P stands for Change Risk Analyzer and Predictor). The matched methods clearly indicate pain points in your code and should be tested and refactored.
The CRAP score defines a specific mathematical formula to combine complexity and coverage. Expressed in CQLinq, that formula is:
| | // CRAPfrom m in JustMyCode.Methodslet CC = m.CyclomaticComplexitylet uncov = (100 - m.PercentageCoverage) / 100flet CRAP = (CC * CC * uncov * uncov * uncov) + CCselect new { m, CRAP } | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
The CRAP score scales with the square of complexity and the cube of uncovered percentage, then adds the raw complexity as a floor. The practical takeaway: a method with CC = 30 and 100% coverage is far less of a liability than a method with CC = 12 and 0% coverage. The second data point of coverage shows that not all complexity is created equal.
Going Beyond Cyclomatic Complexity
Cyclomatic Complexity is a great start for reasoning about your code’s complexity, not the end of the conversation. Two extensions are worth knowing about.
Pair Complexity with Branch Coverage
If you have two methods with the same Cyclomatic Complexity, and one is fully covered by branch tests while the other has none, the risk profile is wildly different. A CQLinq query that captures this idea:
| | // Uncovered code in complex methodswarnif count > 0from m in JustMyCode.Methodswhere m.CyclomaticComplexity > 10 && m.PercentageBranchCoverage < 100select new { m, m.PercentageBranchCoverage, m.CyclomaticComplexity } | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
It raises a warning for any complex method without total branch coverage, which is a much more nuanced view of risk than “this method has CC > 10”.
IL Cyclomatic Complexity for Third-Party Code
Any non-trivial C# project drags in third-party libraries. Most of the time you treat them as black boxes; sometimes you regret it. NDepend’s CQLinq offers a property called IL Cyclomatic Complexity that applies the same metric to the .NET Intermediate Language inside the DLLs you depend on.
This lets you measure how complex the methods of a candidate library actually are, not just how nicely their public API is documented. If you analyze a dependency and find that its internal methods are routinely above CC = 30 in IL, that is a strong signal it will be hard to debug when something goes wrong inside it.
How to Reduce Cyclomatic Complexity in C#
Refactoring for lower Cyclomatic Complexity is mostly about extracting decisions out of the hot path. The techniques below come up over and over again on real codebases.
1. Extract Method
The most boring and the most effective. Pull a contiguous block of decisions into a well-named method, exactly like in the ProcessOrder example earlier. Each call site loses one chunk of complexity; each new method is small enough to test on its own.
2. Early Return / Guard Clauses
Instead of deeply nesting validation in if blocks, return early on invalid input. The cyclomatic count is the same, but the cognitive load drops sharply because every following line can assume the input is valid.
| 12345678910111213141516171819 | // Nestedpublic decimal CalculateFee(Order order) { if (order != null) { if (order.IsValid) { if (order.Customer != null) { return order.Total * 0.05m; } } } return 0;}// Guardedpublic decimal CalculateFee(Order order) { if (order is null) return 0; if (!order.IsValid) return 0; if (order.Customer is null) return 0; return order.Total * 0.05m;} |
|---|
3. Replace Conditionals with Polymorphism
A long switch on a type discriminator is a classic smell. Each case adds 1 to the score and the method becomes the only place that needs to change whenever a new variant appears. Moving the per-variant behaviour into derived classes (Strategy, Template Method, or simple subclass overrides) typically collapses the central method to CC = 1.
4. Use Modern C# Pattern Matching and Switch Expressions
Switch expressions are still counted by analyzers, but they encourage flatter, side-effect-free code than chains of nested ifs. Combined with pattern matching, they often replace a CC of 8-10 with a single, declarative expression:
| | public static decimal DiscountFor(Customer c) => c switch { { IsVip: true, YearsActive: >= 5 } => 0.20m, { IsVip: true } => 0.10m, { YearsActive: >= 10 } => 0.08m, { YearsActive: >= 1 } => 0.03m, _ => 0m}; | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
5. Use Lookup Tables for Pure Mappings
If a method is mostly a list of “input X maps to output Y”, a Dictionary<TKey, TValue> or a static readonly array is almost always preferable to a chain of ifs. The dictionary lookup is CC = 1, regardless of how many entries it holds.
6. Boolean Parameters Are a Code Smell
A method that takes several bool flags almost always hides several methods in a trench coat. Splitting SendEmail(bool html, bool urgent, bool dryRun) into more specific methods reduces both the per-method complexity and the chance that a caller passes the wrong combination of flags.
Frequently Asked Questions
What is a good Cyclomatic Complexity score in C#?
Below 10 is considered safe, between 10 and 20 needs attention, above 25 is what Microsoft’s CA1502 rule flags as excessive. McCabe’s original recommendation, still widely quoted, is to refactor any method that exceeds 10.
Does the else keyword increase Cyclomatic Complexity?
No. The else branch is already implied by its matching if, so it does not add a new independent path. Only the if itself counts.
Does a switch statement count once or per case?
Per case (and per default). The switch keyword itself does not increment the score. A switch with 6 cases plus a default contributes 7 to the method’s Cyclomatic Complexity.
Does Cyclomatic Complexity equal the number of unit tests I need?
It is a useful lower bound, not a contract. Cyclomatic Complexity gives you the number of linearly independent paths, which is the minimum number of tests required for full path coverage. In practice you may need fewer (some paths are infeasible) or more (data combinations within a single path can still misbehave).
What is the difference between Cyclomatic Complexity and Cognitive Complexity?
Cyclomatic Complexity measures the number of paths. Cognitive Complexity, popularised by SonarSource, also weighs nesting depth and boolean operator combinations because they make code harder to read even when they do not add new paths. The two metrics are complementary: low cyclomatic, high cognitive is rare; low cognitive, high cyclomatic is also rare; methods that are bad on one are usually bad on the other.
How do I measure Cyclomatic Complexity in Visual Studio?
In Visual Studio, go to Analyze > Calculate Code Metrics > For Solution. The resulting window lists Cyclomatic Complexity per method, type and project. For continuous enforcement, enable the CA1502 analyzer and configure its threshold via a CodeMetricsConfig.txt additional file.
Does Cyclomatic Complexity work on async or LINQ code?
Yes. The C# compiler rewrites async/await into a state machine, but most tools (NDepend, the Roslyn analyzers, Visual Studio’s metrics) compute Cyclomatic Complexity on the source-level method. await itself does not count, but the if, while and catch constructs surrounding it do. LINQ query operators are method calls, which do not count either, but lambdas passed to them are analyzed as their own methods.
Conclusion
Cyclomatic Complexity is one of the oldest code metrics still in active use, and the reason is simple: it captures something that maps directly to real-world pain. A high score means more paths to reason about, more tests to write, more places where a bug can hide. Keeping it under control, especially on the methods that change the most, pays for itself within weeks.
That said, the score on its own is half the picture. A complex but heavily tested method is rarely the one that wakes you up at night; a moderately complex method with zero coverage that gets touched every sprint will. Pair Cyclomatic Complexity with coverage (or with the CRAP score), enforce a delta-based rule rather than a flat threshold on legacy code, and use IL Cyclomatic Complexity to sanity-check the libraries you depend on. Combined, these techniques turn a 1970s metric into a surprisingly modern early-warning system for technical debt.
If you want to try this on your own codebase, download a free trial of NDepend and run an analysis: the complexity hotspots usually become obvious within the first few minutes.
This article is brought to you by the team behind NDepend — a proven .NET static analysis tool for improving code maintainability, security, and overall quality. Whether you’re modernizing a legacy .NET application or starting fresh in C#, get started with your free full-featured trial today!



