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

5.7 KiB

name, description
name description
coding-standards .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

// 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

// 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)

// 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

// 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

// 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

// 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

// 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

// 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)

[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