5.7 KiB
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
CancellationTokenfor async methods - Prefer
recordsfor DTOs and immutable data - Use
IReadOnlyList<T>for return types - Never use
async void(except event handlers) - Always handle
nullwith pattern matching or null operators - Use structured logging, never
Console.WriteLine