Skip to content

Latest commit

 

History

History
189 lines (135 loc) · 6.77 KB

File metadata and controls

189 lines (135 loc) · 6.77 KB

C# Best Practices

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.

Code Organization and Readability

1. Follow SOLID Principles

  • 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.

2. Use Meaningful Structures and Names

  • Organize your code into namespaces that reflect the functionality and layering of your application.
  • Use meaningful and descriptive names for classes, methods, and variables.

Performance

1. Avoid Unnecessary Allocations

  • 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.

2. Utilize Caching

  • Cache the results of expensive operations when the results are expected to be the same on subsequent calls.

Asynchronous Programming

Task vs ValueTask

  • Task: Use Task for 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 ValueTask for methods that may complete synchronously and do not allocate memory for the task object in these cases. Ideal for performance-critical code sections.

Returning Tasks Directly

  • Directly return Task or ValueTask from 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 async and await when 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;
    }
}

Resource Management

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);
    }
}

Error Handling

1. Use Exceptions Properly

  • Throw exceptions for exceptional conditions, not for control flow.
  • Catch only those exceptions you can handle.

2. Provide Useful Error Information

  • When throwing exceptions, provide clear and concise information that would help in diagnosing the issue.

Security

1. Validate Inputs

  • Never trust external input. Validate input from all untrusted sources to protect against injection and other attacks.

2. Minimize Exposure of Sensitive Data

  • Keep sensitive data, such as connection strings or credentials, out of the codebase. Use secure mechanisms like environment variables or secure configuration stores.

Testing

1. Write Testable Code

  • Design your code in a way that makes it easy to write unit tests. This often means following the SOLID principles mentioned above.

2. Cover Edge Cases

  • Ensure your tests cover not only the expected paths but also the edge cases and error conditions.

Documentation and Comments

1. Comment Wisely

  • Prefer self-explanatory code over comments. When comments are necessary, make sure they are meaningful and up-to-date.

2. Keep Documentation Updated

  • Keep your XML documentation, READMEs, and other documentation up-to-date with your code changes.

Project Structure

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:

Source Files

  • Place all source files within the src directory 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.

Test Files

  • 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

General Guidelines

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 test directory to reflect the structure of the src directory. This makes it easier to find the tests related to a specific piece of functionality.

Continuous Improvement

  • Stay updated with the latest C# features and .NET developments. Regularly review and refactor the code to leverage new language features and best practices.