As a modern, general-purpose language with strong static typing and an extensive ecosystem, C# encourages maintainable, expressive code. However, good code doesn’t happen by accident; it requires adherence to conventions and best practices. Here are key guidelines to keep your C# code clean, maintainable and robust.
Naming and coding conventions
Follow C# naming conventions to make code self-describing. Use PascalCase for class names, methods and properties, camelCase for local variables and method parameters, and ALL_CAPS for constants. Avoid abbreviations and choose meaningful names that clearly communicate intent.
// Good
public class OrderProcessor {
private readonly IEmailSender emailSender;
public void ProcessOrder(int orderId) { /* ... */ }
}
// Bad
public class ordProc {
public void prc(int id) { /* ... */ }
}
Organize your code into logical namespaces and folders that mirror your domain model. Group related classes together to improve discoverability.
Write clean, DRY and modular code
Adhere to the Don’t Repeat Yourself (DRY) principle: factor out common logic into methods or classes instead of copying and pasting. Keep methods small and focused on a single responsibility. Break complex logic into smaller private methods. This improves readability and testability.
Use documentation comments (///
) to explain the purpose and usage of public APIs. Summarize what a method does, describe parameters and return values, and note exceptions thrown.
Handle errors properly
Use exceptions to signal errors and don’t ignore them. Catch only exceptions you can handle; otherwise let them bubble up. Create custom exception types for domain-specific errors. Always log exceptions for diagnostic purposes. Avoid returning null or special codes to indicate failure.
Use using
statements to ensure disposable resources (files, streams, database connections) are closed even when exceptions occur.
Embrace dependency injection and interfaces
C# has strong support for interfaces and dependency injection (DI). Depend on abstractions instead of concrete implementations to make your code more modular and testable. Pass dependencies into classes via constructor injection rather than creating them inside the class. Most modern .NET frameworks include built-in DI containers.
public class OrderProcessor {
private readonly IEmailSender emailSender;
public OrderProcessor(IEmailSender emailSender) {
this.emailSender = emailSender;
}
public void Process(Order order) {
// use emailSender...
}
}
Write unit tests and use source control
Comprehensive unit tests give you confidence to refactor and extend code. Use testing frameworks like xUnit or NUnit to write tests covering happy paths and edge cases. Follow Test-Driven Development (TDD) when possible.
Version your code in a source control system like Git. Commit early and often with descriptive messages. Use branching strategies to manage features and releases.
Focus on performance and security
Be mindful of allocations and unnecessary work inside loops or frequently called methods. Use StringBuilder
for repeated string concatenation and prefer spans and ReadOnlySpan<T>
to avoid copying large arrays. Profile your code and use asynchronous APIs to avoid blocking threads.
Validate all input from untrusted sources and use parameterized SQL queries or ORMs to prevent injection attacks. Keep sensitive data encrypted and follow the least privilege principle.
Follow SOLID and design principles
Apply the SOLID principles—Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation and Dependency Inversion—to build flexible, maintainable systems. Encapsulate related behavior in classes, keep classes small and cohesive, and use interfaces to decouple components. Perform regular code reviews with peers to catch issues early and share knowledge.
Asynchronous programming best practices
C#’s async
/await
makes it easier to write asynchronous code, but there are pitfalls. Follow these guidelines:
- Return
Task
orValueTask
from async methods; avoidasync void
except for event handlers. - Use
ConfigureAwait(false)
in library code that doesn’t need to return to the original context. - Combine multiple tasks with
Task.WhenAll
instead of awaiting them sequentially. - Support cancellation tokens so callers can cancel long-running operations.
- Avoid blocking calls (
.Result
or.Wait
) in async code, which can lead to deadlocks. - Dispose of asynchronous resources with
await using
and implementIAsyncDisposable
where appropriate.
By applying these practices consistently, you’ll write C# code that is easier to understand, modify and extend. Investing in clean code today pays dividends as your projects grow in size and complexity.
References
- [1] dev.to – C# Best Practices
- [2] dev.to – 24 Async/Await Best Practices