Files
accounting-system/.claude/skills/coding-standards/SKILL.md
2026-02-04 23:30:06 +01:00

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`