235 lines
5.7 KiB
Markdown
235 lines
5.7 KiB
Markdown
---
|
|
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<Document> GetByIdAsync(Guid id) { }
|
|
public string InvoiceNumber { get; init; }
|
|
|
|
// camelCase: Parameters, local variables, private fields with underscore
|
|
private readonly ILogger<DocumentService> _logger;
|
|
public void Process(string documentId, int pageCount) { }
|
|
|
|
// Interfaces: I prefix
|
|
public interface IDocumentRepository { }
|
|
|
|
// Async methods: Async suffix
|
|
public async Task<Document> 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<Document> repo, ILogger<DocumentService> logger)
|
|
{
|
|
public async Task<Document?> 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<string> 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<string> 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<Document> 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<Document> Validate(CreateRequest request) =>
|
|
string.IsNullOrEmpty(request.Name)
|
|
? Result<Document>.Fail("Name required")
|
|
: Result<Document>.Ok(new Document(request.Name));
|
|
```
|
|
|
|
## Async/Await
|
|
|
|
```csharp
|
|
// Always pass CancellationToken
|
|
public async Task<Document> 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<IRepository<Document>>();
|
|
repo.GetByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
|
|
.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<T>` 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`
|