Version: 1.0
License: MIT
- Philosophy & Core Principles
- Analyzer Rules Reference
- Structure Rules (STR)
- Style Rules (STY)
- Numeric Rules (NUM)
- Encapsulation Rules (ENC)
- Universal Standards
- C# Specific Standards
- Enforcement & Tooling
Code consistency across all languages takes precedence over language-specific conventions. A developer switching between C#, C++, and Python should find familiar patterns and naming conventions, reducing cognitive load and improving maintainability.
-
Reduced Context Switching: Developers work across multiple languages daily. Using identical naming patterns eliminates mental translation.
-
Unified Debugging Experience: The single return rule provides debugging predictability—set one breakpoint to see all exit values.
-
Cross-Team Collaboration: Engineers can immediately contribute to any codebase without relearning naming conventions or patterns.
-
Error Prevention: Consistent standards eliminate entire classes of errors.
- Single Return Rule: Every function/method must have exactly one return statement at the end
- No Magic Numbers: All numeric literals must be named constants or well-documented
- Argument Validation: All public methods must validate their arguments
- Prefer Immutability: Make data structures immutable by default
- Comprehensive Documentation: All public APIs must be documented
| ID | Name | Severity | Category | Code Fix |
|---|---|---|---|---|
| STR0001 | No continue statements | Warning | Structure | No |
| STR0002 | Multiple return statements | Warning | Structure | Yes |
| STR0003 | Return in void method | Warning | Structure | Yes |
| STR0004 | If-else chains | Warning | Structure | Yes |
| STR0005 | Return in nested block | Warning | Structure | Yes |
| STR0006 | No else if | Warning | Structure | Yes |
| STR0007 | Suspicious regions | Warning | Structure | No |
| STR0008 | One type per file | Warning | Structure | No |
| STR0009 | Nesting depth exceeded | Warning | Structure | No |
| STR0010 | Missing argument validation | Warning | Structure | No |
| STY0001 | End-of-line comments | Warning | Style | Yes |
| STY0002 | Null-forgiving operator | Warning | Style | No |
| STY0003 | Avoid dynamic keyword | Warning | Style | No |
| STY0004 | Field naming convention | Warning | Style | Yes |
| STY0005 | Non-nullable string initialized to null | Warning | Style | Yes |
| STY0006 | Method naming convention | Warning | Style | Yes |
| STY0007 | Region naming convention | Warning | Style | No |
| NUM0001 | Floating-point equality | Error | Numeric | No |
| NUM0002 | Magic numbers | Warning | Numeric | No |
| ENC0001 | Avoid hiding with 'new' | Warning | Encapsulation | Yes |
| ENC0002 | Avoid direct access to inherited fields | Warning | Encapsulation | No |
| ENC0003 | Avoid public/protected fields | Warning | Encapsulation | Yes |
| ENC0004 | Missing interface I prefix | Warning | Encapsulation | Yes |
Severity: Warning
Category: Structure
Code Fix: No
Avoid using continue statements in loops. They make control flow harder to follow.
Problem:
foreach (var item in items)
{
if (item == null)
continue; // STR0001
ProcessItem(item);
}Solution:
foreach (var item in items)
{
if (item != null)
{
ProcessItem(item);
}
}Rationale:
- Continue statements create non-obvious control flow
- Makes debugging and reasoning about loops harder
- Restructuring often leads to cleaner code
Suppression:
#pragma warning disable STR0001
#pragma warning restore STR0001Severity: Warning
Category: Structure
Code Fix: Yes
Methods should have a single exit point (one return statement) at the end.
Problem:
public int Calculate(int x)
{
if (x < 0)
return -1; // STR0002
if (x == 0)
return 0; // STR0002
return x * 2;
}Solution:
public int Calculate(int x)
{
int result = x * 2;
if (x < 0)
{
result = -1;
}
else if (x == 0)
{
result = 0;
}
return result;
}Rationale:
- Single exit point makes debugging predictable
- Set one breakpoint to see all return values
- Easier to add logging or cleanup code
Suppression:
#pragma warning disable STR0002
#pragma warning restore STR0002Severity: Warning
Category: Structure
Code Fix: Yes
Void methods should not use return statements to exit early. Use structured control flow instead.
Problem:
public void ProcessData(Data data)
{
if (data == null)
return; // STR0003
// Process data
}Solution:
public void ProcessData(Data data)
{
if (data != null)
{
// Process data
}
}Rationale:
- Consistent control flow patterns
- Easier to add additional conditions
- More explicit logic structure
Suppression:
#pragma warning disable STR0003
#pragma warning restore STR0003Severity: Warning
Category: Structure
Code Fix: Yes
Excessive if-else chains should be refactored. Consider using switch expressions, pattern matching, or lookup tables.
Problem:
if (status == Status.Active)
{
result = "Active";
}
else if (status == Status.Pending)
{
result = "Pending";
}
else if (status == Status.Completed)
{
result = "Completed";
}
else
{
result = "Unknown";
}Solution:
var result = status switch
{
Status.Active => "Active",
Status.Pending => "Pending",
Status.Completed => "Completed",
_ => "Unknown"
};Suppression:
#pragma warning disable STR0004
#pragma warning restore STR0004Severity: Warning
Category: Structure
Code Fix: Yes
Return statements inside nested blocks (loops, conditions) make control flow harder to follow. Use result variables instead.
Problem:
public int FindIndex(int[] items, int target)
{
for (int i = 0; i < items.Length; i++)
{
if (items[i] == target)
return i; // STR0005
}
return -1;
}Solution:
public int FindIndex(int[] items, int target)
{
int result = -1;
bool found = false;
for (int i = 0; i < items.Length && !found; i++)
{
if (items[i] == target)
{
result = i;
found = true;
}
}
return result;
}Suppression:
#pragma warning disable STR0005
#pragma warning restore STR0005Severity: Warning
Category: Structure
Code Fix: Yes
Avoid using else-if chains. Use switch expressions or restructure the logic.
Problem:
if (x == 1)
{
result = "One";
}
else if (x == 2) // STR0006
{
result = "Two";
}
else
{
result = "Other";
}Solution:
var result = x switch
{
1 => "One",
2 => "Two",
_ => "Other"
};Suppression:
#pragma warning disable STR0006
#pragma warning restore STR0006Severity: Warning
Category: Structure
Code Fix: No
Regions that may indicate code organization issues. Large regions or regions with suspicious names may need refactoring.
Problem:
#region Hacks // STR0007
// Code that should be refactored
#endregionSolution:
Refactor the code instead of hiding it in a region. Consider extracting to separate classes or methods.
Suppression:
#pragma warning disable STR0007
#pragma warning restore STR0007Severity: Warning
Category: Structure
Code Fix: No
Each file should contain only one type (class, struct, interface, etc.). This improves code organization and makes files easier to find.
Problem:
// In a single file:
public class Customer // STR0008
{
}
public class Order // STR0008
{
}Solution:
Split into separate files:
Customer.cs:
public class Customer
{
}Order.cs:
public class Order
{
}Exceptions:
- Nested classes are allowed within their parent class file
Suppression:
#pragma warning disable STR0008
#pragma warning restore STR0008Severity: Warning
Category: Structure
Code Fix: No
Code nesting should not exceed 3 levels (soft limit) or 6 levels (hard limit). Deep nesting indicates complex logic that should be refactored.
Problem:
if (condition1)
{
if (condition2)
{
if (condition3)
{
if (condition4) // STR0009 - 4 levels
{
// Too deep
}
}
}
}Solution:
if (!condition1 || !condition2 || !condition3)
{
return;
}
if (condition4)
{
// Logic here
}Or extract to separate methods:
if (ShouldProcess(condition1, condition2, condition3))
{
ProcessCondition4(condition4);
}Rationale:
- At 6 levels (24 character indent), code is too complex to reason about
- Deep nesting indicates logic that should be extracted
Suppression:
#pragma warning disable STR0009
#pragma warning restore STR0009Severity: Warning
Category: Structure
Code Fix: No
Public methods must validate their arguments before processing.
Problem:
public void ProcessData(string input, List<int> values) // STR0010
{
// No validation - goes straight to processing
var result = input.ToUpper();
}Solution:
public void ProcessData(string input, List<int> values)
{
if (string.IsNullOrWhiteSpace(input))
throw new ArgumentNullException(nameof(input));
if (values == null)
throw new ArgumentNullException(nameof(values));
if (values.Count == 0)
throw new ArgumentException("Values cannot be empty", nameof(values));
// Now process
var result = input.ToUpper();
}Rationale:
- Fail fast with clear error messages
- Documents method contracts
- Prevents null reference exceptions deep in code
Suppression:
#pragma warning disable STR0010
#pragma warning restore STR0010Severity: Warning
Category: Style
Code Fix: Yes
End-of-line comments make code harder to read and maintain. Place comments on their own line above the code they describe.
Problem:
int timeout = 5000; // Timeout in milliseconds // STY0001Solution:
// Timeout in milliseconds
int timeout = 5000;Suppression:
#pragma warning disable STY0001
#pragma warning restore STY0001Severity: Warning
Category: Style
Code Fix: No
The null-forgiving operator (!) suppresses null warnings but can hide null reference bugs. Use proper null checks or assertions instead.
Problem:
string name = GetName()!; // STY0002Solution:
var name = GetName();
if (name != null)
{
// Use name
}
// Or with pattern matching
if (GetName() is { } name)
{
// Use name
}Suppression:
#pragma warning disable STY0002
#pragma warning restore STY0002Severity: Warning
Category: Style
Code Fix: No
The dynamic keyword bypasses compile-time type checking and should be avoided. It can lead to runtime errors that would otherwise be caught at compile time.
Problem:
dynamic value = GetValue();
var result = value.SomeMethod(); // No compile-time checking!
public dynamic Process(dynamic input) // STY0003
{
return input.Transform();
}Solution:
object value = GetValue();
if (value is ITransformable transformable)
{
var result = transformable.SomeMethod();
}
// Or use generics
public T Process<T>(T input) where T : ITransformable
{
return input.Transform();
}Why Avoid Dynamic?
- No compile-time type checking: Errors only appear at runtime
- No IntelliSense: IDE cannot help with method/property names
- Performance overhead: Dynamic dispatch is slower than static dispatch
- Harder to refactor: Renaming members won't update dynamic code
- Testing burden: Requires more runtime testing to catch type errors
When Dynamic Might Be Necessary:
- COM interop with late-bound Office automation
- Interop with dynamic languages (Python, JavaScript via scripting)
- Reflection-heavy scenarios where types are truly unknown
Suppression:
#pragma warning disable STY0003 // Required for COM interop with Excel
dynamic excelApp = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application"));
#pragma warning restore STY0003Severity: Warning
Category: Style
Code Fix: Yes
Fields must follow the naming convention with appropriate prefixes.
| Field Type | Prefix | Example |
|---|---|---|
| Private instance | m |
mCustomerName |
| Private static | ps |
psInstanceCount |
| Private static readonly | sm |
smDefaultLogger |
| Public instance | pm |
pmDisplayName |
Problem:
private string customerName; // STY0004 - missing 'm' prefix
private static int instanceCount; // STY0004 - missing 'ps' prefixSolution:
private string mCustomerName;
private static int psInstanceCount;
private static readonly ILogger smDefaultLogger;Rationale:
- Instant scope recognition in large classes
- Prevents shadowing bugs
- Thread safety awareness (e.g., static vs instance)
Suppression:
#pragma warning disable STY0004
#pragma warning restore STY0004Severity: Warning
Category: Style
Code Fix: Yes
Non-nullable strings must not be initialized to null. Use string.Empty, a meaningful default, or make the type nullable.
Problem:
string result = null; // STY0005Solution:
// Option 1: Use string.Empty
string result = string.Empty;
// Option 2: Use meaningful default
string result = "Unknown";
// Option 3: Make it nullable if null has semantic meaning
string? result = null;Suppression:
#pragma warning disable STY0005
#pragma warning restore STY0005Severity: Warning
Category: Style
Code Fix: Yes
Methods must use PascalCase naming.
Problem:
public void processData() // STY0006
{
}
public int calculate_total() // STY0006
{
}Solution:
public void ProcessData()
{
}
public int CalculateTotal()
{
}Suppression:
#pragma warning disable STY0006
#pragma warning restore STY0006Severity: Warning
Category: Style
Code Fix: No
Regions must follow specific naming patterns and encapsulate complete functional units.
Prohibited patterns:
#region Constructors // STY0007 - too generic
#region Properties // STY0007 - too generic
#region Methods // STY0007 - too generic
#region Fields // STY0007 - too genericRequired patterns:
#region SaveCommand command
// Command field, property, Execute, CanExecute all together
#endregion
#region Name property
// Backing field, property, change handlers all together
#endregion
#region CustomerChanged event
// Event field, event property, raise method all together
#endregion
#region Equality members
// Equals, GetHashCode together
#endregion
#region IDisposable implementation
// Interface implementation together
#endregionSuppression:
#pragma warning disable STY0007
#pragma warning restore STY0007Severity: Error
Category: Numeric
Code Fix: No
Direct equality comparisons between floating-point numbers (float, double) are unreliable due to precision issues. Use tolerance-based comparison methods instead.
Problem:
if (value == 0.1) // NUM0001
{
// ...
}
if (a != b) // NUM0001 when a or b is float/double
{
// ...
}Solution:
if (DoubleExtensions.AreEffectivelyEqual(value, 0.1))
{
// ...
}
if (!DoubleExtensions.AreEffectivelyEqual(a, b))
{
// ...
}Exemptions:
- Comparisons with integer types are allowed
- Comparisons with string types are allowed
- Test methods (marked with
[TestMethod],[Fact],[Theory], etc.) are exempt
Suppression:
#pragma warning disable NUM0001
#pragma warning restore NUM0001Severity: Warning
Category: Numeric
Code Fix: No
Numeric literals (magic numbers) make code harder to understand and maintain. Extract them to named constants with descriptive names.
Problem:
int timeout = 7777; // NUM0002 - What does 7777 mean?
if (retryCount > 42) // NUM0002 - Why 42?
{
// ...
}Solution:
private const int TimeoutMilliseconds = 7777;
private const int MaxRetryCount = 42;
int timeout = TimeoutMilliseconds;
if (retryCount > MaxRetryCount)
{
// ...
}Allowed Integer Values:
- Small integers: -10 to 20
- Common values: 24, 25, 30, 40, 50, 75
- Angle values: 45, 60, 90, 120, 135, 180, 225, 270, 315, 360
- Powers of 2: 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768
- Byte/word boundaries: 255, 65535
- Time-related: 1000, 2000, 3000, 5000, 60000, 120000, 3600, 86400
- Powers of 10: -10000, -1000, -100, 100, 10000, 100000, 1000000
- Buffer/collection sizes: 500, 8000
- MAX_PATH (Windows): 260
- Calendar: 365, 366
- Hash code primes: 23, 31, 37, 397
Allowed Float Values:
- Zero and one: 0.0, 1.0
- Common fractions: 0.01, 0.02, 0.05, 0.1, 0.2, 0.25, 0.33, 0.5, 0.67, 0.75 (and negatives)
- Percentage thresholds: 0.8, 0.85, 0.9, 0.95, 0.99, 0.999
- Small integers as doubles: -10.0 to 10.0
- Powers of 10 as doubles: -1000.0, -100.0, 100.0, 1000.0
- Angle values as doubles: 45.0, 60.0, 90.0, 180.0, 270.0, 360.0
- Scientific notation tolerances: 1e-10, 1e-9, 1e-8, 1e-7, 1e-6, 1e-5, 1e-4, 1e-3
- Pi-related: 3.14159, pi, 2pi
Exemptions:
constdeclarations - Named constants are the solutionstatic readonlyfields - Named constants- Enum values - Already semantic
- Attribute arguments - Often required to be compile-time constants
- Array and collection initializers - Data definitions
- Test methods - Test data is expected
GetHashCodemethods - Hash code calculations commonly use primes- Hex literals (0x prefix) - Typically intentional bit masks or flags
Suppression:
#pragma warning disable NUM0002
int specialValue = 12345; // Has a specific meaning documented elsewhere
#pragma warning restore NUM0002Severity: Warning
Category: Encapsulation
Code Fix: Yes (when base is virtual)
Using the new keyword to hide an inherited member can cause confusion and unexpected behavior.
Problem:
public class Animal
{
public int GetSpeed() => 10;
}
public class Dog : Animal
{
public new int GetSpeed() => 30; // ENC0001
}
// Confusing behavior:
Dog dog = new Dog();
Animal animal = dog;
dog.GetSpeed(); // Returns 30
animal.GetSpeed(); // Returns 10 - unexpected!Solution:
public class Animal
{
public virtual int GetSpeed() => 10;
}
public class Dog : Animal
{
public override int GetSpeed() => 30;
}
// Consistent behavior:
Dog dog = new Dog();
Animal animal = dog;
dog.GetSpeed(); // Returns 30
animal.GetSpeed(); // Returns 30 - as expected!Why Is This a Problem?
- Polymorphism breaks: Base class references don't see the derived implementation
- Confusing behavior: Same object behaves differently based on reference type
- Hard to debug: Issues may only appear in certain code paths
- Code smell: Often indicates design issues in the class hierarchy
Suppression:
#pragma warning disable ENC0001 // Intentional hiding - legacy base class cannot be modified
public new string GetData() => base.GetData().ToUpper();
#pragma warning restore ENC0001Severity: Warning
Category: Encapsulation
Code Fix: No
Derived classes should not directly access non-readonly fields inherited from base classes. Access to parent class state should be through properties or methods only.
Problem:
public class BaseClass
{
protected int mValue; // Mutable protected field
}
public class DerivedClass : BaseClass
{
public void UpdateValue()
{
mValue = 42; // ENC0002: Direct access to inherited field
}
}Solution:
public class BaseClass
{
private int mValue;
protected int Value
{
get => mValue;
set => mValue = value;
}
}
public class DerivedClass : BaseClass
{
public void UpdateValue()
{
Value = 42; // Compliant: Using property
}
}Readonly Fields Are Allowed:
public class BaseClass
{
protected readonly int SmMaxValue = 100; // Readonly is safe
}
public class DerivedClass : BaseClass
{
public int GetMax()
{
return SmMaxValue; // Compliant: Readonly field
}
}Rationale:
- Base classes can add validation logic to field modifications later
- Base classes can add notification/event logic when fields change
- The internal implementation of base classes can be changed without breaking derived classes
Suppression:
#pragma warning disable ENC0002 // Direct field access required for performance
mValue = newValue;
#pragma warning restore ENC0002Severity: Warning
Category: Encapsulation
Code Fix: Yes
Public and protected fields break encapsulation. Fields should be private; expose state through properties instead.
Problem:
public class Person
{
public string Name; // ENC0003: Should be a property
public int Age; // ENC0003: Should be a property
protected double mSalary; // ENC0003: Should be a property
}Solution:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
private double mSalary;
protected double Salary
{
get => mSalary;
set => mSalary = value;
}
}Exemptions:
- Structs - Value types commonly use public fields for performance and interop
- Records - Use compiler-generated properties by design
- Types with
[StructLayout]attribute - Interop scenarios requiring specific memory layout - WPF types - Classes deriving from
DependencyObject,FrameworkElement, etc. - Readonly/const fields - Immutable fields are safe to expose
- Static fields - Class-level state (different encapsulation concern)
Suppression:
#pragma warning disable ENC0003 // Public field required for legacy serialization compatibility
public string LegacyData;
#pragma warning restore ENC0003Severity: Warning
Category: Encapsulation
Code Fix: Yes
Interfaces must be prefixed with I.
Problem:
public interface Repository // ENC0004
{
void Save();
}
public interface Serializable // ENC0004
{
string Serialize();
}Solution:
public interface IRepository
{
void Save();
}
public interface ISerializable
{
string Serialize();
}Rationale:
- Instant recognition that you're dealing with a contract, not an implementation
- Prevents naming collisions (e.g.,
Repositoryinterface vsRepositoryclass) - IDE benefits: All interfaces sort together in alphabetical listings
Suppression:
#pragma warning disable ENC0004
#pragma warning restore ENC0004| Element | Convention | Example |
|---|---|---|
| Types | PascalCase | CustomerOrder, IDataService |
| Interfaces | I + PascalCase | IRepository, ILogger |
| Methods/Functions | PascalCase | CalculateTotal(), GetCustomerName() |
| Properties | PascalCase | FirstName, TotalAmount |
| Parameters | lowerCamelCase | customerName, orderDate |
| Local Variables | lowerCamelCase | totalAmount, isValid |
| Constants | PascalCase | MaxRetryAttempts, DefaultTimeout |
| Enums | PascalCase | OrderStatus, ColorType |
| Enum Values | PascalCase | Active, Pending, Completed |
| Field Type | Prefix | Example |
|---|---|---|
| Private instance | m |
mCustomerName |
| Private static | ps |
psInstanceCount |
| Private static readonly | sm |
smDefaultLogger |
| Public instance | pm |
pmDisplayName |
- One Type Per File: Each class, interface, struct, or enum gets its own file
- File Name Matches Type:
CustomerOrder.cscontainsclass CustomerOrder - Logical Directory Structure: Group related types by feature/module
- All public types must be documented
- All public methods and properties must be documented
- Complex algorithms must be documented regardless of visibility
Use var when type is evident:
// Good - type is obvious
var list = new List<string>();
var count = GetCount();
// Bad - type not evident
var data = GetData();
var result = Process(input);- Enable nullable reference types project-wide
- Use nullable annotations appropriately
- Never initialize non-nullable strings to null
// Correct patterns
string name = string.Empty;
string label = "Default";
string? optional = null; // Only if nullable
// Wrong - never do this
string result = null; // PROHIBITEDPrefer pattern matching when it improves readability:
// Good
if (obj is Customer { IsActive: true, Orders.Count: > 0 } customer)
{
ProcessCustomer(customer);
}
// Avoid confusing empty braces
if (obj is Customer { } customer) // What does {} mean?- Prefer auto-properties for simple cases
- Use backing fields only when logic is needed
- Expression-bodied members for simple getters
public string Name { get; set; }
public string FullName => $"{FirstName} {LastName}";All rules can be configured via .editorconfig:
# Disable a rule
dotnet_diagnostic.STR0001.severity = none
# Change to error
dotnet_diagnostic.NUM0001.severity = error
# Change to suggestion
dotnet_diagnostic.STY0004.severity = suggestion// Single line
#pragma warning disable STR0001
code here
#pragma warning restore STR0001
// With explanation (recommended)
#pragma warning disable STR0001 // Required for legacy compatibility
code here
#pragma warning restore STR0001- Single return rule followed
- No magic numbers
- Arguments validated
- Public APIs documented
- Naming conventions followed
- No continue statements
- No else-if chains
- Field prefixes correct
- Interfaces have I prefix
MIT License
Copyright (c) 2025
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.