This document outlines the best practices for writing C# code. These guidelines help ensure our codebase is maintainable, scalable, and efficient. While following these practices, always remember that every rule has exceptions.
- Single Responsibility: A class should only have one reason to change.
- Open/Closed: Classes should be open for extension but closed for modification.
- Liskov Substitution: Objects of a superclass should be replaceable with objects of a subclass without breaking the application.
- Interface Segregation: No client should be forced to depend on methods it does not use.
- Dependency Inversion: Depend on abstractions, not on concretions.
- Organize your code into namespaces that reflect the functionality and layering of your application.
- Use meaningful and descriptive names for classes, methods, and variables.
- Be mindful of memory allocation and deallocation, particularly in performance-critical parts of the code.
- Use value types (
struct) when small objects are frequently instantiated and do not survive long.
- Cache the results of expensive operations when the results are expected to be the same on subsequent calls.
-
Task: Use
Taskfor I/O-bound operations. It's suitable for operations that involve a significant amount of waiting, such as web requests or file I/O. -
ValueTask: Use
ValueTaskfor methods that may complete synchronously and do not allocate memory for the task object in these cases. Ideal for performance-critical code sections.
- Directly return
TaskorValueTaskfrom methods when there is a single asynchronous operation. This approach avoids the overhead of an async state machine.
Example:
public Task<string> GetDataAsync()
{
return _httpClient.GetStringAsync("https://api.example.com/data");
}- Use
asyncandawaitwhen you need to handle exceptions or perform multiple asynchronous operations for better readability and maintainability.
Example:
public async Task<string> GetDataAsync()
{
try
{
var response = await _httpClient.GetStringAsync("https://api.example.com/data");
// Additional processing
return response;
}
catch (HttpRequestException e)
{
// Handle exception
return default;
}
}Implement the dispose pattern properly to manage resources efficiently. Always call GC.SuppressFinalize(this) in the Dispose method to prevent the garbage collector from calling the object's finalizer.
Example:
public class Resource : IDisposable
{
private bool _disposed = false; // Flag for detecting redundant dispose calls
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
// Dispose managed resources.
}
// Free unmanaged resources and set large fields to null.
_disposed = true;
}
~Resource()
{
Dispose(false);
}
}- Throw exceptions for exceptional conditions, not for control flow.
- Catch only those exceptions you can handle.
- When throwing exceptions, provide clear and concise information that would help in diagnosing the issue.
- Never trust external input. Validate input from all untrusted sources to protect against injection and other attacks.
- Keep sensitive data, such as connection strings or credentials, out of the codebase. Use secure mechanisms like environment variables or secure configuration stores.
- Design your code in a way that makes it easy to write unit tests. This often means following the SOLID principles mentioned above.
- Ensure your tests cover not only the expected paths but also the edge cases and error conditions.
- Prefer self-explanatory code over comments. When comments are necessary, make sure they are meaningful and up-to-date.
- Keep your XML documentation, READMEs, and other documentation up-to-date with your code changes.
Maintaining a consistent and intuitive project structure is crucial for project scalability, maintainability, and team collaboration. Adhering to a well-defined directory structure ensures that team members can easily locate files, understand the project architecture, and contribute more effectively. Below are the conventions we follow for organizing project files:
- Place all source files within the
srcdirectory at the root of the project. This directory should contain all the application logic, code, and resources that are necessary for the application to run.
Example structure:
/src
|-- /ProjectName.Application
|-- /Controllers
|-- /Models
|-- /Views
|-- /Services
|-- ProjectName.Application.csproj
- Organize the `src`` directory further based on the logical components of the application, such as Client, Core, Shared, Server, etc., depending on your project's architecture and design patterns.
- Place all test projects and test files within the test directory at the root of the project. This separation emphasizes the distinction between production code and test code.
/test
|-- /ProjectName.UnitTests
|-- /Fixtures
|-- /Tests
|-- ProjectName.UnitTests.csproj
|-- /ProjectName.IntegrationTests
|-- /Fixtures
|-- /Tests
|-- ProjectName.IntegrationTests.csproj
Ensure that the solution file (.sln) is at the root of the project, encompassing both the src and test directories. This allows for easy navigation and management of the project's components.
Adopt a naming convention that clearly differentiates between production code and test code, e.g., CompanyName.ProjectName for the main project and CompanyName.ProjectName.Tests for the test project.
- Organize the
testdirectory to reflect the structure of the src directory. This makes it easier to find the tests related to a specific piece of functionality.
- Stay updated with the latest C# features and .NET developments. Regularly review and refactor the code to leverage new language features and best practices.