# .NET Development Best Practices ## Project Structure ``` src/ Domain/ # Entities, value objects, domain events Application/ # Use cases, DTOs, interfaces Infrastructure/ # EF Core, external services Api/ # Controllers, middleware, filters tests/ Unit/ Integration/ ``` ## Code Style ```csharp // Use records for DTOs and value objects public sealed record CreateDocumentRequest(string Name, string Type); // Use primary constructors public class DocumentService(IRepository repo, ILogger logger) { public async Task GetAsync(Guid id, CancellationToken ct) => await repo.GetByIdAsync(id, ct); } // Prefer expression body for simple methods public Document? FindById(Guid id) => _documents.FirstOrDefault(d => d.Id == id); // Use collection expressions int[] numbers = [1, 2, 3]; List names = ["Alice", "Bob"]; ``` ## Async/Await ```csharp // Always pass CancellationToken public async Task GetAsync(Guid id, CancellationToken ct) // Use ConfigureAwait(false) in libraries await _httpClient.GetAsync(url, ct).ConfigureAwait(false); // Avoid async void (except event handlers) public async Task ProcessAsync() { } // Good public async void Process() { } // Bad // Use ValueTask for hot paths with frequent sync completion public ValueTask GetCachedCountAsync() ``` ## Dependency Injection ```csharp // Register by interface builder.Services.AddScoped(); // Use Options pattern for configuration builder.Services.Configure(builder.Configuration.GetSection("App")); public class MyService(IOptions options) { private readonly AppSettings _settings = options.Value; } // Avoid service locator pattern // Bad: var service = serviceProvider.GetService(); // Good: Constructor injection ``` ## Entity Framework Core ```csharp // Always use AsNoTracking for read-only queries await _context.Documents.AsNoTracking().ToListAsync(ct); // Use projection to select only needed fields await _context.Documents .Where(d => d.Status == "Active") .Select(d => new DocumentDto(d.Id, d.Name)) .ToListAsync(ct); // Prevent N+1 with Include or projection await _context.Documents.Include(d => d.Labels).ToListAsync(ct); // Use explicit transactions for multiple operations await using var tx = await _context.Database.BeginTransactionAsync(ct); // Configure entities with IEntityTypeConfiguration public class DocumentConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.HasKey(d => d.Id); builder.Property(d => d.Name).HasMaxLength(200).IsRequired(); builder.HasIndex(d => d.Status); } } ``` ## Error Handling ```csharp // Create domain-specific exceptions public class NotFoundException(string resource, Guid id) : Exception($"{resource} not found: {id}"); // Use global exception handler public class GlobalExceptionHandler(ILogger logger) : IExceptionHandler { public async ValueTask TryHandleAsync(HttpContext ctx, Exception ex, CancellationToken ct) { logger.LogError(ex, "Error: {Message}", ex.Message); ctx.Response.StatusCode = ex is NotFoundException ? 404 : 500; await ctx.Response.WriteAsJsonAsync(new { error = ex.Message }, ct); return true; } } // Use Result pattern for expected failures public Result Validate(CreateRequest request) => string.IsNullOrEmpty(request.Name) ? Result.Fail("Name is required") : Result.Ok(new Document(request.Name)); ``` ## Validation ```csharp // Use FluentValidation public class CreateDocumentValidator : AbstractValidator { public CreateDocumentValidator() { RuleFor(x => x.Name).NotEmpty().MaximumLength(200); RuleFor(x => x.Type).Must(BeValidType).WithMessage("Invalid document type"); } } // Or use Data Annotations for simple cases public record CreateRequest( [Required, MaxLength(200)] string Name, [Range(1, 100)] int Quantity); ``` ## Logging ```csharp // Use structured logging with templates logger.LogInformation("Processing document {DocumentId} for user {UserId}", docId, userId); // Use appropriate log levels logger.LogDebug("Cache hit for key {Key}", key); // Development details logger.LogInformation("Document {Id} created", id); // Normal operations logger.LogWarning("Retry attempt {Attempt} for {Op}", n, op); // Potential issues logger.LogError(ex, "Failed to process {DocumentId}", id); // Errors // Configure log filtering in appsettings { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", "Microsoft.EntityFrameworkCore": "Warning" } } } ``` ## API Design ```csharp [ApiController] [Route("api/v1/[controller]")] public class DocumentsController(IDocumentService service) : ControllerBase { [HttpGet("{id:guid}")] [ProducesResponseType(200)] [ProducesResponseType(404)] public async Task Get(Guid id, CancellationToken ct) { var doc = await service.GetAsync(id, ct); return doc is null ? NotFound() : Ok(doc); } [HttpPost] public async Task Create(CreateRequest request, CancellationToken ct) { var doc = await service.CreateAsync(request, ct); return CreatedAtAction(nameof(Get), new { id = doc.Id }, doc); } } ``` ## Testing ```csharp // Use descriptive test names [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"); } // Use WebApplicationFactory for integration tests public class ApiTests(WebApplicationFactory factory) : IClassFixture> { [Fact] public async Task GetDocuments_ReturnsSuccess() { var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/documents"); response.StatusCode.Should().Be(HttpStatusCode.OK); } } ``` ## Performance ```csharp // Use IMemoryCache for frequently accessed data public class CachedService(IMemoryCache cache, IRepository repo) { public async Task GetAsync(Guid id, CancellationToken ct) => await cache.GetOrCreateAsync($"doc:{id}", async entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); return await repo.GetByIdAsync(id, ct); }); } // Use pagination for large collections public async Task> GetPagedAsync(int page, int size, CancellationToken ct) => new( await _context.Documents.Skip((page - 1) * size).Take(size).ToListAsync(ct), await _context.Documents.CountAsync(ct) ); // Use IAsyncEnumerable for streaming large datasets public async IAsyncEnumerable StreamAllAsync([EnumeratorCancellation] CancellationToken ct) { await foreach (var doc in _context.Documents.AsAsyncEnumerable().WithCancellation(ct)) yield return doc; } ``` ## Security ```csharp // Never hardcode secrets var apiKey = builder.Configuration["ApiKey"]; // From environment/secrets // Use parameterized queries (EF Core does this automatically) // Bad: $"SELECT * FROM Users WHERE Id = {id}" // Good: _context.Users.Where(u => u.Id == id) // Validate and sanitize all inputs // Use HTTPS in production // Implement rate limiting builder.Services.AddRateLimiter(options => { ... }); ```