Code Quality Help

C# Detailed Standards

This section covers language-specific rules for C# code. Adherence to these rules ensures safe, predictable, and maintainable backend services.

5.1 Declarations and Types

  • Use of var: Allowed only when the type is obvious from the right-hand side and is not a primitive. For public API return types, interface method signatures, or when the type improves clarity, always use the explicit type.

    // Good var customer = new Customer(); var orders = _orderRepository.GetAll(); int timeout = GetTimeoutSetting(); // primitive, use explicit // Bad var result = ProcessOrder(order); // What is result?
  • Explicit types for public APIs: All public methods, properties, and parameters must use explicit types; no var in signatures.

  • Pattern matching and null checks: Prefer pattern matching and is operator for type checks and null checks.

    if (obj is string str) { Console.WriteLine(str); } if (entity is not null) { ... }
  • File-scoped namespaces: Use file-scoped namespaces (C# 10+) to reduce nesting.

    namespace Company.Project.Feature;

5.2 Null and Exception Handling

  • Null guard: Always validate method arguments that are not expected to be null at the start of public methods. Use ArgumentNullException.ThrowIfNull (or if with throw in older versions).

    public void UpdateUser(User user) { ArgumentNullException.ThrowIfNull(user); // ... }
  • Exception types: Throw meaningful, specific exceptions (InvalidOperationException, ArgumentException, NotFoundException) rather than Exception or NullReferenceException. Define custom exceptions when domain meaning is needed.

  • Catch blocks: Never catch Exception without a strong reason (e.g., top-level request handler, logging wrapper). Always log the exception and rethrow or handle appropriately. Avoid empty catch blocks.

    try { // ... } catch (Exception ex) when (ex is not OutOfMemoryException) { _logger.LogError(ex, "Failed to process order {OrderId}", order.Id); throw; }
  • Async exception handling: In async methods, exceptions are captured in the returned Task. Avoid async void. If fire-and-forget is necessary, use a dedicated background job mechanism.

5.3 Asynchronous Programming

  • Async suffixes: All asynchronous methods must have the Async suffix (e.g., GetDataAsync), except when they are implementations of interface members that do not carry the suffix.

  • Return types: Use Task or Task<T> for async methods. Use ValueTask<T> only after careful benchmarking and when hot paths are proven.

  • ConfigureAwait: In library code (shared libraries), use ConfigureAwait(false) on all await calls to avoid capturing the synchronization context. In application code (ASP.NET Core controllers, etc.), it is not required but allowed.

    var data = await _service.GetAsync().ConfigureAwait(false);
  • Cancellation tokens: All I/O-bound methods (HTTP calls, database queries) that can be long-running must accept a CancellationToken and pass it through to underlying calls.

    public async Task<Order> GetOrderAsync(string id, CancellationToken ct = default) { return await _dbContext.Orders.FirstOrDefaultAsync(o => o.Id == id, ct); }
  • Avoid sync-over-async: Do not call .Result or .Wait() on tasks from synchronous code. Use async all the way up.

5.4 Dependency Injection

  • Constructor injection: Always use constructor injection for required dependencies. Mark dependencies as private readonly and order them alphabetically or by importance – choose one and be consistent.

  • Avoid Service Locator: Do not inject IServiceProvider to resolve dependencies manually. Use factories only when the service lifetime is dynamic (e.g., multi-tenant).

  • Avoid large constructors: If a class requires more than ~5 dependencies, consider splitting responsibilities.

5.5 Other Best Practices

  • LINQ: Prefer LINQ for query operations, but do not mix imperative loops and LINQ without a clear reason. Keep queries readable.

  • Immutability: Favor immutability where possible: readonly fields, init-only properties, records for DTOs.

public record OrderDto(string Id, decimal Total, string Status);
  • String concatenation: Use string.Format, string interpolation ($""), or StringBuilder for non-trivial concats. Prefer interpolation for readability.

  • Magic numbers and strings: No hardcoded numeric or string values except well-known constants (0, 1, ""). Encode them as named constants or configuration values.

  • Testibility: Design classes to be testable. Avoid static state, and use interfaces for dependencies.

04 мая 2026