Style
Suggestion (Info)
Enforces that method chains with 2 or more method calls should be broken down to separate lines for better readability. This rule ensures consistent and readable code by requiring each method in a chain to be on its own line when you have multiple chained calls.
- Method chains with 2+ calls from variables/properties: When starting from a variable, property, or field, method chains with 2 or more calls must have each method call on a separate line
- Single method calls: No formatting restrictions - can remain on one line
- Property access: Accessing a property after a single method call (e.g.,
str.Trim().Length) is allowed on one line - Method calls with arguments: A method call with arguments followed by a single chained call (e.g.,
MethodA(value).ToString()) is allowed on one line, as the method with arguments acts as the base expression rather than part of the chain - Method-to-method chains: A method call (instance or static) followed by a single chained method (e.g.,
GetDataAsync().ConfigureAwait(false)orTask.Run(() => 42).ContinueWith(...)) is allowed on one line - If-statement conditions: Method chains with exactly 2 chained methods in if-statement conditions are allowed on one line for improved readability (e.g.,
if (rootElement.Attributes("Sdk").Count() == 1)) - Interpolated strings: Method chains inside interpolated strings are excluded from this rule - see ATC204 for interpolation-specific analysis
- Applies to: All method invocations including LINQ, async/await, StringBuilder, Task operations, etc.
- Works with await: When using
awaitwith method chains, the same rules apply - FluentAssertions Exception: Any pattern starting with
.Should()followed by exactly one assertion method (e.g.,actual.Should().Be(...),actual.Should().NotBeNull(),actual.Should().BeEquivalentTo(...)) is allowed on one line for test readability- Chains with 3+ methods (e.g.,
.Should().Be(...).And.NotBeNull()) must still be placed on separate lines
- Chains with 3+ methods (e.g.,
Placing method chains on separate lines:
- Significantly improves code readability for complex chains
- Makes each step of the operation clearly visible
- Easier to debug by setting breakpoints on individual calls
- Reduces horizontal scrolling
- Makes code reviews easier by showing one operation per line
- Follows common C# formatting conventions
// non-compliant
var result = str.Trim().ToLower();
// compliant
var result = str
.Trim()
.ToLower();// non-compliant
var result = str.Trim().Replace("xxx", "x").Replace("yyy", "y").Trim();
// compliant
var result = str
.Trim()
.Replace("xxx", "x")
.Replace("yyy", "y")
.Trim();// non-compliant
var result = numbers.Where(x => x > 0).Select(x => x * 2).ToList();
// compliant
var result = numbers
.Where(x => x > 0)
.Select(x => x * 2)
.ToList();// non-compliant - starting from a variable with 2+ chained methods
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
// compliant
var root = await document
.GetSyntaxRootAsync(cancellationToken)
.ConfigureAwait(false);// compliant - starting from a method call with only 1 chained method
var result = await GetDataAsync().ConfigureAwait(false);
// also compliant when discard is used
_ = GetDataAsync().ConfigureAwait(false);// non-compliant
var result = new StringBuilder().Append("hello").Append(" world").ToString();
// compliant
var result = new StringBuilder()
.Append("hello")
.Append(" world")
.ToString();// compliant - single method call is allowed on one line
var result = str.Trim();// compliant - property access after single method is allowed
var length = str.Trim().Length;// compliant - method call (with or without arguments) followed by a single chained method is allowed
var v = MethodA(value).ToString(Thread.CurrentThread.CurrentCulture);
var result = CalculateSum(a, b).ToString();
var data = GetDataAsync().ConfigureAwait(false);
var task = Task.Run(() => 42).ContinueWith(t => t.Result * 2);
// non-compliant - starting from a variable with 2+ chained methods
var lower = str.Trim().ToLower(); // Should be on separate lines// compliant - 2 method chains in if-statement condition
using System.Linq;
using System.Xml.Linq;
public void ProcessElement(XElement rootElement)
{
if (rootElement.Attributes("Sdk").Count() == 1)
{
// Process element
}
if (text.Trim().ToLower() == "test")
{
// Handle test case
}
}
// non-compliant - 3+ method chains in if-statement must still be on separate lines
public void ProcessText(string text)
{
if (text.Trim().ToLower().Replace("a", "b") == "test") // Not allowed
{
}
// Should be written as:
if (text
.Trim()
.ToLower()
.Replace("a", "b") == "test")
{
}
}
// non-compliant - 2 method chains in assignment still require separate lines
public void AssignValue(string text)
{
var result = text.Trim().ToLower(); // Not allowed
// Should be written as:
var result = text
.Trim()
.ToLower();
}// compliant - .Should() followed by any single assertion method is allowed on one line
[Fact]
public void TestMethod()
{
var actual = "test";
actual.Should().Be("test");
actual.Should().NotBeNull();
actual.Should().NotBeNullOrEmpty();
var expected = new { Name = "test" };
var obj = new { Name = "test" };
obj.Should().BeEquivalentTo(expected);
var number = 42;
number.Should().BePositive();
}// non-compliant
[Fact]
public void TestMethod()
{
var actual = "test";
actual.Should().Be("test").And.NotBeNull(); // 3 methods chained
}
// compliant
[Fact]
public void TestMethod()
{
var trimmed = " Hello World ".Trim();
trimmed
.Should().Be("Hello World")
.And.NotBeNullOrWhiteSpace();
}// non-compliant
[Fact]
public void TestMethod()
{
var upper = "test".ToUpperInvariant();
upper.Should().Be("TEST").And.StartWith("TE").And.EndWith("ST");
}
// compliant
[Fact]
public void TestMethod()
{
var upper = "test".ToUpperInvariant();
upper
.Should().Be("TEST")
.And.StartWith("TE")
.And.EndWith("ST");
}// compliant for ATC203 - method chains in interpolated strings are excluded
// (See ATC204 for interpolation-specific suggestions)
var myVar = "test";
var result = $"Hello {myVar.ToString().ToLower()} world";
var message = $"{firstName.Trim()} {lastName.Trim().ToUpper()}";Configure the minimum chain length threshold in .editorconfig:
[*.cs]
# Minimum chain length before requiring separation (default: 2, range: 2-10)
dotnet_diagnostic.ATC203.min_chain_length = 3| Option | Default | Range | Description |
|---|---|---|---|
min_chain_length |
2 | 2-10 | Minimum number of chained method calls before the rule requires separation |
Example configurations:
# Default - 2+ method chains must be on separate lines
dotnet_diagnostic.ATC203.min_chain_length = 2
# Lenient - only 3+ method chains must be on separate lines
dotnet_diagnostic.ATC203.min_chain_length = 3
# Very lenient - only 4+ method chains must be on separate lines
dotnet_diagnostic.ATC203.min_chain_length = 4Note: The special case exceptions (FluentAssertions, method-to-method chains, if-statement conditions) still apply regardless of this configuration.
Exceptions:
- Method-to-method chains: Method chains starting with a method call (instance or static) followed by a single chained method are allowed on one line (e.g.,
GetDataAsync().ConfigureAwait(false),Task.Run(() => 42).ContinueWith(...)) - If-statement conditions: Method chains with exactly 2 chained methods in if-statement conditions are allowed on one line for improved readability (e.g.,
if (rootElement.Attributes("Sdk").Count() == 1)) - FluentAssertions pattern: Any pattern starting with
.Should()followed by exactly one assertion method (e.g.,actual.Should().Be(...),actual.Should().NotBeNull(),actual.Should().BeEquivalentTo(...)) is permitted on one line for test readability. This recognizes the idiomatic nature of simple FluentAssertions test assertions in xUnit and other test frameworks. - Interpolated strings: Method chains inside interpolated strings are excluded from this rule. See ATC204 for interpolation-specific analysis that suggests extracting complex expressions to variables.
This rule automatically skips analysis of generated code. Generated code is identified by:
- GeneratedCode Attribute: Classes or types marked with
[GeneratedCode]attribute fromSystem.CodeDom.Compiler - Auto-generated Headers: Files containing "auto-generated" in header comments (case-insensitive)
Example of skipped code:
using System.CodeDom.Compiler;
[GeneratedCode("MyCodeGenerator", "1.0")]
public class GeneratedClass
{
public void Method()
{
// This would normally violate ATC203, but is ignored
var result = str.Trim().ToLower();
}
}//------------------------------------------------------------------------------
// This code was auto-generated by ApiGenerator 2.0.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//------------------------------------------------------------------------------
public class AutoGeneratedClass
{
public void Method()
{
// This would normally violate ATC203, but is ignored
var result = str.Trim().ToLower();
}
}A code fix is available that automatically places method chains on separate lines with proper indentation.
How to apply:
- Position cursor on the method chain that violates the rule
- Click the lightbulb (💡) or press
Ctrl+.(Windows/Linux) orCmd+.(Mac) - Select "Place method chain on separate lines"
The code fix will:
- Place each method call in the chain on its own line
- Apply consistent indentation (4 spaces per level based on the containing statement)
- Preserve all method arguments, including complex expressions and lambda parameters
- Handle await expressions correctly
- Work with method chains in any context (assignments, return statements, arguments, etc.)
Example:
// Before code fix
var result = str.Trim().ToLower();
// After code fix (automatic)
var result = str
.Trim()
.ToLower();Complex example with nested expressions:
// Before code fix
var result = await dataTask.ContinueWith(t => t.Result).ConfigureAwait(false);
// After code fix (automatic)
var result = await dataTask
.ContinueWith(t => t.Result)
.ConfigureAwait(false);Note: The code fix correctly handles lambda expressions and does not break method chains inside lambda parameters.
- ATC201: Single parameter should be kept inline when declaration is short - Parameter formatting for single parameters
- ATC202: Multi parameters should be separated on individual lines - Parameter formatting for multiple parameters
- ATC204: Chained method calls in interpolated strings should be simplified - Interpolation-specific method chain analysis
- ATC210: Use expression body syntax when appropriate - Expression body formatting
- ATC230: Require exactly one blank line between code blocks - Blank line formatting