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
varin signatures.Pattern matching and null checks: Prefer pattern matching and
isoperator 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(orifwiththrowin older versions).public void UpdateUser(User user) { ArgumentNullException.ThrowIfNull(user); // ... }Exception types: Throw meaningful, specific exceptions (
InvalidOperationException,ArgumentException,NotFoundException) rather thanExceptionorNullReferenceException. Define custom exceptions when domain meaning is needed.Catch blocks: Never catch
Exceptionwithout 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
asyncmethods, exceptions are captured in the returnedTask. Avoidasync 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
Asyncsuffix (e.g.,GetDataAsync), except when they are implementations of interface members that do not carry the suffix.Return types: Use
TaskorTask<T>for async methods. UseValueTask<T>only after careful benchmarking and when hot paths are proven.ConfigureAwait: In library code (shared libraries), useConfigureAwait(false)on allawaitcalls 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
CancellationTokenand 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
.Resultor.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 readonlyand order them alphabetically or by importance – choose one and be consistent.Avoid Service Locator: Do not inject
IServiceProviderto 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.
String concatenation: Use
string.Format, string interpolation ($""), orStringBuilderfor 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.