--- name: coding-standards description: .NET/C# coding standards and best practices. --- # .NET Coding Standards ## Core Principles - **Readability First** - Clear names, self-documenting code - **KISS** - Simplest solution that works - **DRY** - Extract common logic, avoid copy-paste - **YAGNI** - Don't build features before needed ## Naming Conventions ```csharp // PascalCase: Types, methods, properties, public fields public class DocumentService { } public async Task GetByIdAsync(Guid id) { } public string InvoiceNumber { get; init; } // camelCase: Parameters, local variables, private fields with underscore private readonly ILogger _logger; public void Process(string documentId, int pageCount) { } // Interfaces: I prefix public interface IDocumentRepository { } // Async methods: Async suffix public async Task LoadAsync(CancellationToken ct) ``` ## Modern C# Features ```csharp // Records for DTOs and value objects public sealed record CreateDocumentRequest(string Name, string Type); public sealed record DocumentDto(Guid Id, string Name, DateTime CreatedAt); // Primary constructors public class DocumentService(IRepository repo, ILogger logger) { public async Task GetAsync(Guid id, CancellationToken ct) => await repo.GetByIdAsync(id, ct); } // Pattern matching var message = result switch { { IsSuccess: true, Value: var doc } => $"Found: {doc.Name}", { Error: var err } => $"Error: {err}", _ => "Unknown" }; // Collection expressions int[] numbers = [1, 2, 3]; List names = ["Alice", "Bob"]; // Null coalescing var name = user?.Name ?? "Unknown"; list ??= []; ``` ## Immutability (Critical) ```csharp // GOOD: Create new objects public record User(string Name, int Age) { public User WithName(string newName) => this with { Name = newName }; } // GOOD: Immutable collections public IReadOnlyList GetNames() => _names.AsReadOnly(); // BAD: Mutation public void UpdateUser(User user, string name) { user.Name = name; // MUTATION! } ``` ## Error Handling ```csharp // Domain exceptions public class NotFoundException(string resource, Guid id) : Exception($"{resource} not found: {id}"); // Comprehensive handling public async Task LoadAsync(Guid id, CancellationToken ct) { try { var doc = await _repo.GetByIdAsync(id, ct); return doc ?? throw new NotFoundException("Document", id); } catch (Exception ex) when (ex is not NotFoundException) { _logger.LogError(ex, "Failed to load document {Id}", id); throw; } } // Result pattern for expected failures public Result Validate(CreateRequest request) => string.IsNullOrEmpty(request.Name) ? Result.Fail("Name required") : Result.Ok(new Document(request.Name)); ``` ## Async/Await ```csharp // Always pass CancellationToken public async Task GetAsync(Guid id, CancellationToken ct) // Use ConfigureAwait(false) in libraries await _client.GetAsync(url, ct).ConfigureAwait(false); // Avoid async void public async Task ProcessAsync() { } // Good public async void Process() { } // Bad // Parallel when independent var tasks = ids.Select(id => GetAsync(id, ct)); var results = await Task.WhenAll(tasks); ``` ## LINQ Best Practices ```csharp // Prefer method syntax for complex queries var result = documents .Where(d => d.Status == "Active") .OrderByDescending(d => d.CreatedAt) .Select(d => new DocumentDto(d.Id, d.Name, d.CreatedAt)) .Take(10); // Use Any() instead of Count() > 0 if (documents.Any(d => d.IsValid)) { } // Avoid multiple enumerations var list = documents.ToList(); // Materialize once var count = list.Count; var first = list.FirstOrDefault(); ``` ## File Organization ``` src/ Domain/ # Entities, value objects Application/ # Use cases, DTOs, interfaces Infrastructure/ # EF Core, external services Api/ # Controllers, middleware tests/ Unit/ Integration/ ``` **Guidelines:** - Max 800 lines per file (typical 200-400) - Max 50 lines per method - One class per file (except nested) - Group by feature, not by type ## Code Smells ```csharp // BAD: Deep nesting if (doc != null) if (doc.IsValid) if (doc.HasFields) // ... // GOOD: Early returns if (doc is null) return null; if (!doc.IsValid) return null; if (!doc.HasFields) return null; // ... // BAD: Magic numbers if (confidence > 0.5) { } // GOOD: Named constants private const double ConfidenceThreshold = 0.5; if (confidence > ConfidenceThreshold) { } ``` ## Logging ```csharp // Structured logging with templates _logger.LogInformation("Processing document {DocumentId}", docId); _logger.LogError(ex, "Failed to process {DocumentId}", docId); // Appropriate levels LogDebug // Development details LogInformation // Normal operations LogWarning // Potential issues LogError // Errors with exceptions ``` ## Testing (AAA Pattern) ```csharp [Fact] public async Task GetById_WithValidId_ReturnsDocument() { // Arrange var repo = Substitute.For>(); repo.GetByIdAsync(Arg.Any(), Arg.Any()) .Returns(new Document("Test")); var service = new DocumentService(repo); // Act var result = await service.GetAsync(Guid.NewGuid(), CancellationToken.None); // Assert result.Should().NotBeNull(); result!.Name.Should().Be("Test"); } ``` ## Key Rules - Always use `CancellationToken` for async methods - Prefer `records` for DTOs and immutable data - Use `IReadOnlyList` for return types - Never use `async void` (except event handlers) - Always handle `null` with pattern matching or null operators - Use structured logging, never `Console.WriteLine`