C# Coding Standards
Naming Conventions
General Guidelines
- Use PascalCase for class names, method names, and public members
- Use camelCase for local variables, parameters, class scoped fields
- Prefix interface names with "I" (e.g., IDisposable)
- Use meaningful and descriptive names that show core function
PascalCase when public, camelCase when private/internal as a general rule
Specific Rules
- Constants: Declare constants in PascalCase
- Example:
const int MaxValue = 100; - Class Names: Use a suffix that describes the class's purpose
- Example:
ObjectHandler,DataProcessor - Boolean Methods/Properties: Methods or properties returning booleans should usually have an "Is" or "Has" prefix
- Example:
IsValid(),HasPermission - Interfaces: Prefix interface names with "I"
- Example:
IDisposable,IComparable
Case Conventions
- Use PascalCase for:
- ✅ Class names
- ✅ Method names
- ✅ Public members
- Use camelCase for:
- ✅ Local variables
- ✅ Parameters
- ✅ Class scoped fields
Constructors
Primary Constructor Syntax
- Use primary constructor syntax when appropriate, especially for simple classes or record types.
- This syntax promotes concise and readable code.
- Example:
public class Person(string name, int age)
Multiple Constructors:
- When a class requires multiple constructors, consider using the primary constructor in combination with additional constructors.
- Use constructor chaining to avoid code duplication.
-
Example:
public class Employee(string name, int age, string department) : Person(name, age) { public Employee(string name, int age) : this(name, age, "General") { } }
Default Values
- When using primary constructors, you can specify default values for parameters.
-
Example:
public class Configuration(string environment = "Development", bool isDebug = false) { }
Readability
- Use XML comments to document constructor parameters.
Complexity & Nesting
Nesting Limit
- Avoid nesting loops and conditions generally.
- When nesting is necessary, limit it to no more than 2 levels deep.
Flatten Conditionals
- Use logical operators to combine conditions and reduce nesting.
- Consider using switch expressions or pattern matching for complex conditionals.
Lambdas
- Keep lambda expressions simple and avoid nesting within them.
- If a lambda becomes complex, consider moving logic to a method.
Be Descriptive
- To reduce cognitive load be descriptive with variable naming
Simplify Where Possible
Nesting can normally be avoided through refactoring. Example:
// Before: Deeply nested
public void ProcessData(List<Data> dataList)
{
foreach (var data in dataList)
{
if (data.IsValid)
{
if (data.NeedsProcessing)
{
// Process data
}
}
}
}
// After: Refactored to reduce nesting
public void ProcessData(List<Data> dataList)
{
var validData = dataList.Where(d => d.IsValid && d.NeedsProcessing);
foreach (var data in validData)
{
ProcessSingleDataItem(data);
}
}
private void ProcessSingleDataItem(Data data)
{
// Process data
}
Visibility & Access Modifiers
When determining the visibility of classes, interfaces, and other types, follow these guidelines:
Classes
- Classes should generally be internal by default.
- Make classes public only when they need to be accessible outside the assembly.
- Exceptions:
- Domain Models may be public when needed for cross-assembly data transfer.
- Classes that need to be visible for .NET Core or other framework functionality.
Interfaces
- Interfaces should be public by default.
- This allows for better decoupling and enables dependency injection across assembly boundaries.
Visibility Hierarchy
- Use the least permissive access modifier that still allows the code to function correctly.
- Consider the following order of preference (from most to least restrictive):
- private
- internal
- protected (for inheritance scenarios)
- public
Method Parameters
When defining and using method parameters, follow these guidelines:
Parameter Order
- When passing value types (e.g., booleans), always place these parameters first in the parameter list.
- Follow with reference type parameters.
Parameter Formatting
- For methods with multiple parameters:
- Place each parameter on a separate line if the parameter list becomes too long for a single line.
- Align parameters for readability.
-
Example:
public void SomeMethod( bool isValid, int count, string name, ComplexType complexObject) { // Method body }
Consider Using Parameter Objects
- If a method has many parameters, consider grouping related parameters into a parameter object.
- This can improve readability and make the method signature more manageable.
Asynchronous Programming
When working with asynchronous code, adhere to these guidelines:
CancellationToken Usage
- All asynchronous methods should accept a
CancellationTokenparameter. - This allows for proper cancellation of long-running operations.
- Example:
public async Task<Result> GetData(CancellationToken cancellationToken)
Cancellation Handling
- When passing a
CancellationToken, ensure the operation can be properly cancelled. - Check the token's status at appropriate intervals in long-running operations.
- Async vs Sync Methods:
- Prefer async methods where available.
- Use synchronous methods only when async alternatives are not available or when performance requirements dictate their use.
- Avoid Blocking Calls:
- Do not use
.Resultor.Wait()on tasks, as this can lead to deadlocks. - Instead, use
awaitto handle asynchronous operations.
Dependency Injection(DI)
Usage
-
Avoid Direct Instantiation: Never use the
newkeyword to create an object, except for models. -
❌ Don't:
var service = new MyService(); -
👍 Okay:
var user = new User { Name = "John Doe", Age = 30 }; -
Instead Use Constructor Injection:
- Prefer primary constructor injection for required dependencies.
- This makes dependencies explicit and ensures they are available when the object is created.
- ✅ Do:
public class ExampleController(IService service) : ControllerBase
Registration
- Interface-based Registration:
- Register objects using their interfaces, not concrete classes.
- This promotes loose coupling and makes it easier to swap implementations.
- 👍 Okay: `services.AddScoped
(); - Scoped Registrations:
- Registrations should usually be scoped.
- Use other lifetimes (Singleton, Transient) only when there's a specific reason to do so.
Testing
Follow this Class structure
- Integration test class per API Endpoint
- Unit test class for each testable class
Write Readable Tests
- Use descriptive test names that explain the scenario being tested.
- Employ the Arrange-Act-Assert pattern for clear test structure.
- Use FluentAssertions for assertions.
Generate Test Data
- Use the AutoFixture auto data pattern to generate data used inside tests. Where appropriate use the
[CustomAutoData]attribute. - Use collection fixtures where context is shared between tests
Follow the Testing Strategy for test class content.
Comments
Required XML Comments
- Use XML comments for all public interfaces, classes, methods, and properties.
- This ensures that your public APIs are well-documented and can be easily understood by other developers.
-
Example:
///
/// Represents a user in the system. /// public interface IUser { ////// Gets or sets the user's unique identifier. /// int Id { get; set; }/// <summary> /// Gets or sets the user's display name. /// </summary> string Name { get; set; }}
General Practice
Consider Refactoring
- Writing self-explanatory code using meaningful variable and method names can replace comments.
- If a piece of code is complex enough to require explanation, consider refactoring it into smaller, more descriptive methods.
When to Comment?
- Add comments when the reason for the code existing cannot be immediately clear from its structure and naming.
- ✅ Comments should explain why something is done
- ❌ Don't comment what is being done.
Maintain Comments
- Ensure that comments are kept up-to-date when the code changes.
- Outdated comments can be misleading and cause confusion.
Use TODO Comments Sparingly
- Use TODO comments to mark temporary code or reminder for future changes.
- Always include a brief explanation of what needs to be done.