Maintainability Guidelines
Methods should not exceed 15 statements (AV1500)
A method that requires more than 15 statements is simply doing too much or has too many responsibilities. It also requires the human mind to analyze the exact statements to understand what the code is doing. Break it down into multiple small and focused methods with self-explaining names, but make sure the high-level algorithm is still clear.
Also ensure that each method operates at a single level of abstraction. Mixing high-level functional calls (the “what”) with low-level implementation details (the “how”) in the same method makes it harder to understand the intent of the code.
// Avoid: mixing abstraction levels in the same method
void ProcessOrder(Order order)
{
if (IsPremiumCustomer(order.CustomerId)) // functional; the "what"
{
ApplyDiscount(order);
}
if (Regex.IsMatch(order.Email.ToUpper().Trim(), @"^[^@]+@[^@]+\.[^@]+$")) // technical; the "how"
{
SendConfirmation(order.Email);
}
}
// Better: extract the technical detail behind an abstraction
void ProcessOrder(Order order)
{
if (IsPremiumCustomer(order.CustomerId))
{
ApplyDiscount(order);
}
if (IsValidEmail(order.Email))
{
SendConfirmation(order.Email);
}
}
Make all members private and types internal sealed by default
(AV1501)
To make a more conscious decision on which members to make available to other classes, first restrict the scope as much as possible. Then carefully decide what to expose as a public member or type.
Avoid conditions with double negatives (AV1502)
Although a property like customer.HasNoOrders makes sense, avoid using it in a negative condition like this:
bool hasOrders = !customer.HasNoOrders;
Double negatives are more difficult to grasp than simple expressions, and people tend to read over the double negative easily.
Name assemblies after their contained namespace (AV1505)
All DLLs should be named according to the pattern Company.Component.dll where Company refers to your company’s name and Component contains one or more dot-separated clauses. For example AvivaSolutions.Web.Controls.dll.
As an example, consider a group of classes organized under the namespace AvivaSolutions.Web.Binding exposed by a certain assembly. According to this guideline, that assembly should be called AvivaSolutions.Web.Binding.dll.
[!IMPORTANT] Exception: If you decide to combine classes from multiple unrelated namespaces into one assembly, consider suffixing the assembly name with
Core, but do not use that suffix in the namespaces. For instance,AvivaSolutions.Consulting.Core.dll.
Name a source file to the type it contains (AV1506)
Use Pascal casing to name the file and don’t use underscores. Don’t include (the number of) generic type parameters in the file name.
Limit the contents of a source code file to one type (AV1507)
[!IMPORTANT] Exception: Nested types should be part of the same file.
[!IMPORTANT] Exception: Types that only differ by their number of generic type parameters should be part of the same file.
Name a source file to the logical function of the partial type (AV1508)
When using partial types and allocating a part per file, name each file after the logical part that part plays. For example:
// In MyClass.cs
public partial class MyClass
{...}
// In MyClass.Designer.cs
public partial class MyClass
{...}
Don’t use “magic” numbers (AV1515)
Don’t use literal values, either numeric or strings, in your code, other than to define symbolic constants. For example:
public class Whatever
{
public static readonly Color PapayaWhip = new Color(0xFFEFD5);
public const int MaxNumberOfWheels = 18;
public const byte ReadCreateOverwriteMask = 0b0010_1100;
}
Strings intended for logging or tracing are exempt from this rule. Literals are allowed when their meaning is clear from the context, and not subject to future changes, For example:
mean = (a + b) / 2; // okay
WaitMilliseconds(waitTimeInSeconds * 1000); // clear enough
If the value of one constant depends on the value of another, attempt to make this explicit in the code.
public class SomeSpecialContainer
{
public const int MaxItems = 32;
public const int HighWaterMark = 3 * MaxItems / 4; // at 75%
}
[!NOTE] An enumeration can often be used for certain types of symbolic constants.
Only use var when the type is evident
(AV1520)
Use var for anonymous types (typically resulting from a LINQ query), or if the type is evident.
Never use var for built-in types.
// Projection into anonymous type.
var largeOrders =
from order in dbContext.Orders
where order.Items.Count > 10 && order.TotalAmount > 1000
select new { order.Id, order.TotalAmount };
// Built-in types.
bool isValid = true;
string phoneNumber = "(unavailable)";
uint pageSize = Math.Max(itemCount, MaxPageSize);
// Types are evident.
var customer = new Customer();
var invoice = Invoice.Create(customer.Id);
var user = sessionCache.Resolve<User>("[email protected]");
var subscribers = new List<Subscriber>();
var summary = shoppingBasket.ToOrderSummary();
// All other cases.
IQueryable<Order> recentOrders = ApplyFilter(order => order.CreatedAt > DateTime.Now.AddDays(-30));
LoggerMessage message = Compose(context);
ReadOnlySpan<char> key = ExtractKeyFromPair("[email protected]");
IDictionary<Category, Product> productsPerCategory =
shoppingBasket.Products.ToDictionary(product => product.Category);
Declare and initialize variables as late as possible (AV1521)
Define and initialize each variable at the point where it is needed, rather than declaring all variables at the top of a block.
Assign each variable in a separate statement (AV1522)
Don’t use confusing constructs like the one below:
var result = someField = GetSomeMethod();
[!IMPORTANT] Exception: Multiple assignments per statement are allowed by using out variables, is-patterns or deconstruction into tuples. Examples:
bool success = int.TryParse(text, out int result); if ((items[0] is string text) || (items[1] is Action action)) { } (string name, string value) = SplitNameValuePair(text);
Favor object and collection initializers over separate statements (AV1523)
Use Object and Collection Initializers:
var startInfo = new ProcessStartInfo("myapp.exe")
{
StandardOutput = Console.Output,
UseShellExecute = true
};
new List<string> countries = ["Netherlands", "United States"];
var countryLookupTable = new Dictionary<string, string>
{
["NL"] = "Netherlands",
["US"] = "United States"
};
This also applies to arrays and the spread operator (C# 12+):
string[] moreCountries = ["Belgium", "Germany"];
string[] allCountries = [..countries, ..moreCountries];
Avoid nested loops (AV1532)
A method that nests loops is more difficult to understand than one with only a single loop.
Always add a block after the keywords if, else, do, while, for, foreach and case
(AV1535)
Please note that this also avoids possible confusion in statements of the form:
if (isActive) if (isVisible) Foo(); else Bar(); // which 'if' goes with the 'else'?
// The right way:
if (isActive)
{
if (isVisible)
{
Foo();
}
else
{
Bar();
}
}
Always add a default block after the last case in a switch statement
(AV1536)
Add a descriptive comment if the default block is supposed to be empty. Moreover, if that block is not supposed to be reached throw an InvalidOperationException to detect future changes that may fall through the existing cases. This ensures better code, because all paths the code can travel have been thought about.
void Foo(string answer)
{
switch (answer)
{
case "no":
{
Console.WriteLine("You answered with No");
break;
}
case "yes":
{
Console.WriteLine("You answered with Yes");
break;
}
default:
{
// Not supposed to end up here.
throw new InvalidOperationException("Unexpected answer " + answer);
}
}
}
Finish every if-else-if statement with an else clause
(AV1537)
For example:
void Foo(string answer)
{
if (answer == "no")
{
Console.WriteLine("You answered with No");
}
else if (answer == "yes")
{
Console.WriteLine("You answered with Yes");
}
else
{
// What should happen when this point is reached? Ignored? If not,
// throw an InvalidOperationException.
}
}
Be reluctant with multiple return statements
(AV1540)
One entry, one exit is a sound principle and keeps control flow readable. However, if the method body is very small and complies with guideline AV1500 then multiple return statements may actually improve readability over some central boolean flag that is updated at various points.
Use concise conditional expressions instead of if-else blocks
(AV1545)
Write conditional expressions using modern, concise syntax. For example, rather than:
bool isPositive;
if (value > 0)
{
isPositive = true;
}
else
{
isPositive = false;
}
write:
bool isPositive = value > 0;
Or use the ternary operator:
return value > 0 ? "positive" : "negative";
Or the null-coalescing operator:
return offset ?? -1;
Or the null-coalescing assignment operator:
firstJobStartedAt ??= DateTime.UtcNow;
Or the conditional member access operator:
return employee.Manager?.Name;
Prefer interpolated strings over concatenation or string.Format.
(AV1546)
Since .NET 6, interpolated strings are optimized at compile-time, which inlines constants and reduces memory allocations due to boxing and string copying. In .NET 8 and later, interpolated strings are further optimized using DefaultInterpolatedStringHandler, making them the fastest option in most scenarios.
// GOOD
string result = $"Welcome, {firstName} {lastName}!";
// BAD
string result = string.Format("Welcome, {0} {1}!", firstName, lastName);
// BAD
string result = "Welcome, " + firstName + " " + lastName + "!";
// BAD
string result = string.Concat("Welcome, ", firstName, " ", lastName, "!");
For building strings in loops or with many parts, consider using StringBuilder or raw string literals (C# 11+) when appropriate.
Encapsulate complex expressions in a property, method or local function (AV1547)
Consider the following example:
if (member.HidesBaseClassMember && member.NodeType != NodeType.InstanceInitializer)
{
// do something
}
In order to understand what this expression is about, you need to analyze its exact details and all of its possible outcomes. Obviously, you can add an explanatory comment on top of it, but it is much better to replace this complex expression with a clearly named method:
if (NonConstructorMemberUsesNewKeyword(member))
{
// do something
}
private bool NonConstructorMemberUsesNewKeyword(Member member)
{
return member.HidesBaseClassMember &&
member.NodeType != NodeType.InstanceInitializer;
}
You still need to understand the expression if you are modifying it, but the calling code is now much easier to grasp.
Call the more overloaded method from other overloads (AV1551)
This guideline only applies to overloads that are intended to provide optional arguments. Consider, for example, the following code snippet:
public class MyString
{
private string someText;
public int IndexOf(string phrase)
{
return IndexOf(phrase, 0);
}
public int IndexOf(string phrase, int startIndex)
{
return IndexOf(phrase, startIndex, someText.Length - startIndex);
}
public virtual int IndexOf(string phrase, int startIndex, int count)
{
return someText.IndexOf(phrase, startIndex, count);
}
}
The class MyString provides three overloads for the IndexOf method, but two of them simply call the one with one more parameter. Notice that the same rule applies to class constructors; implement the most complete overload and call that one from the other overloads using the this() operator. Also notice that the parameters with the same name should appear in the same position in all overloads.
[!IMPORTANT] If you also want to allow derived classes to override these methods, define the most complete overload as a non-private
virtualmethod that is called by all overloads.
Only use optional parameters to replace overloads (AV1553)
The only valid reason for using optional parameters is to replace the example from rule AV1551 with a single method like:
public virtual int IndexOf(string phrase, int startIndex = 0, int count = -1)
{
int length = count == -1 ? someText.Length - startIndex : count;
return someText.IndexOf(phrase, startIndex, length);
}
Since strings, collections and tasks should never be null according to rule AV1135, if you have an optional parameter of these types with default value null then you must use overloaded methods instead.
Be aware that the default values of optional parameters are stored at the caller side. Changing the default argument without recompiling the calling code will not apply the new default value.
Do not use optional parameters with defaults in interface methods or their concrete implementations (AV1554)
When an interface method defines an optional parameter with a default value, its default value is discarded during overload resolution unless you call the concrete class through the interface reference.
When a concrete implementation of an interface method sets a default argument for a parameter, the default value is discarded during overload resolution if you call the concrete class through the interface reference.
See the series on optional argument corner cases by Eric Lippert (part one, two, three, four) for more details.
Avoid using named arguments (AV1555)
C# 4.0’s named arguments have been introduced to make it easier to call COM components that are known for offering many optional parameters. If you need named arguments to improve the readability of the call to a method, that method is probably doing too much and should be refactored.
[!IMPORTANT] Exception: The only exception where named arguments improve readability is when calling a method of some code base you don’t control that has a
boolparameter, like this:object[] myAttributes = type.GetCustomAttributes(typeof(MyAttribute), inherit: false);
Don’t declare signatures with more than 3 parameters (AV1561)
To keep constructors, methods, delegates and local functions small and focused, do not use more than three parameters. Do not use tuple parameters. Do not return tuples with more than two elements.
If you want to use more parameters, use a structure or class to pass multiple arguments, as explained in the Specification design pattern. In general, the fewer the parameters, the easier it is to understand the method. Additionally, unit testing a method with many parameters requires many scenarios to test.
[!IMPORTANT] Exception: A parameter that is a collection of tuples is allowed.
Don’t use ref or out parameters
(AV1562)
They make code less understandable and might cause people to introduce bugs. Instead, return compound objects or tuples.
[!IMPORTANT] Exception: Calling and declaring members that implement the TryParse pattern is allowed. For example:
bool success = int.TryParse(text, out int number);
Avoid signatures that take a bool parameter
(AV1564)
Consider the following method signature:
public Customer CreateCustomer(bool platinumLevel)
{
}
On first sight this signature seems perfectly fine, but when calling this method you will lose this purpose completely:
Customer customer = CreateCustomer(true);
Often, a method taking such a bool is doing more than one thing and needs to be refactored into two or more methods. An alternative solution is to replace the bool with an enumeration.
Prefer is patterns over as operations
(AV1570)
If you use ‘as’ to safely upcast an interface reference to a certain type, always verify that the operation does not return null. Failure to do so may cause a NullReferenceException at a later stage if the object did not implement that interface.
Pattern matching syntax prevents this and improves readability. For example, instead of:
var remoteUser = user as RemoteUser;
if (remoteUser != null)
{
}
write:
if (user is RemoteUser remoteUser)
{
}
Don’t comment out code (AV1575)
Never check in code that is commented out. Instead, use a work item tracking system to keep track of some work to be done. Nobody knows what to do when they encounter a block of commented-out code. Was it temporarily disabled for testing purposes? Was it copied as an example? Should I delete it?
Align projects and folders with deployment units, not architectural layers (AV1578)
A common but problematic project structure organizes code into layer-based projects such as MyApp.Domain, MyApp.Application, MyApp.Infrastructure and MyApp.Web. While this separation has academic appeal, it tends to create artificial coupling, makes it harder to split off bounded contexts later, and forces cross-cutting concerns into awkward locations.
Instead, organize projects or folders around deployment units and bounded contexts:
// Avoid (layer-based)
MyApp.Domain
MyApp.Application
MyApp.Infrastructure
MyApp.Web
// Prefer (deployment-unit / bounded-context-based)
MyApp.Orders // all layers for the Orders context
MyApp.Catalog // all layers for the Catalog context
MyApp.Notifications // a deployable notification service
MyApp.Web // host project, composes the above
MyApp.Common // Code that is complicated enough to avoid duplicating (see also [AV1580](/maintainability-guidelines#AV1580))
This approach:
- Makes each project independently deployable or extractable.
- Keeps cohesive code together: the model, business logic and persistence for a given feature are in the same project.
- Reduces the need for cross-project dependencies within a single bounded context.
- Makes room for keeping complicated generic code
- Allows using projects or folders to represent functional boundaries
[!NOTE] A thin host project (
MyApp.Web,MyApp.Api) that composes all contexts is still appropriate, and shared infrastructure utilities can live in aMyApp.Infrastructure.Sharedproject when genuinely reused.
Use raw string literals for multi-line or escape-heavy strings (AV1582)
C# 11 introduced raw string literals, which make it much easier to write strings that contain quotes, backslashes, or multiple lines without escape sequences.
// Avoid escape-heavy strings
string json = "{\n \"name\": \"Alice\",\n \"age\": 30\n}";
string pattern = "^(https?:\\/\\/)(www\\.)?[a-zA-Z0-9]+\\.[a-z]+$";
// Prefer raw string literals (C# 11+)
string json = """
{
"name": "Alice",
"age": 30
}
""";
string pattern = """^(https?:\/\/)(www\.)?[a-zA-Z0-9]+\.[a-z]+$""";
Raw string literals:
- Start and end with three or more double-quote characters (
"""). - Can contain any characters, including quotes and backslashes, without escaping.
- Support interpolation:
$"""..."""(use$$"""..."""for JSON to avoid conflicting with{}). - Trim leading indentation automatically based on the position of the closing
""".
Use raw string literals for JSON/XML/HTML snippets, regex patterns, SQL queries and any other string that is hard to read when escaped.
Make properties required when they must be set during initialization (AV1585)
C# 11 introduced the required modifier for properties and fields. When a property is marked required, the compiler enforces that any object initializer sets it, preventing accidentally uninitialized state without needing a constructor parameter.
// Without required: easy to forget a property
var order = new Order { TotalAmount = 99.95m }; // OrderId is silently left as default
// With required: the compiler will error if OrderId is not set
public class Order
{
public required Guid OrderId { get; set; }
public required decimal TotalAmount { get; set; }
public string? Notes { get; set; }
}
var order = new Order
{
OrderId = Guid.NewGuid(),
TotalAmount = 99.95m
// Notes is optional, so it's fine to omit
};
Use required when:
- A property must always be set to a meaningful value for the object to be valid.
- The type is a DTO or a record-like class used with object initializers.
[!NOTE]
requiredandinit-only properties are complementary. Combine them (public required string Name { get; init; }) to enforce both initialization and immutability.