Write .NET code that scales.
20 years of C#/.NET in production — world-class healthcare companies, national-scale real estate platforms, AI-assisted engineering workflows. The patterns here are the ones that actually held up. No toy examples. Just what works at scale.
1// ❌ The wrong way — you wrote your own dependency. 2public class OrderController : ControllerBase 3{ 4 private readonly OrderService _service = new OrderService(); 5} 6 7// ✓ The clean way — ask for what you need. 8public class OrderController(IOrderService service) : ControllerBase 9{ 10 private readonly IOrderService _service = service; 11 12 [HttpPost] 13 public IActionResult Create(OrderDto dto) => 14 Ok(_service.Create(dto)); 15} 16 17// Program.cs — register once, inject everywhere. 18builder.Services.AddScoped<IOrderService, OrderService>();
Entity Framework Core Performance — Stop Your ORM From Killing the Database
It returned in 40ms on your machine. In production it takes nine seconds and fires 2,001 queries. Nobody wrote 2,001 queries — EF Core did, because your local database has five rows in it and has been lying to you since day one.
// ❌ One round trip per claim. Six was all you saw in dev. foreach (var id in claimIds) claims.Add(await _context.Claims.FindAsync(id, ct)); // ✓ One query. Same count at five rows and two million. var claims = await _context.Claims .Where(c => claimIds.Contains(c.Id)) .ToListAsync(ct);
Unit Testing with NSubstitute — Tests That Survive Refactoring
You learned testing from a tutorial. The tutorial showed you how to mock, so you mock everything. Then you refactor a helper method and 47 tests explode. The code works — the tests can't tell the difference.
// ❌ A stunt double for the coffee cup. var claim = Substitute.For<Claim>(); var dto = Substitute.For<ClaimDto>(); // ✓ Mock the boundary. Data is just data. var repo = Substitute.For<IClaimRepository>(); var claim = new Claim { Id = "CLM-123", Amount = 150.00m };
Exception Handling That Survives Production
At 1,500 fake "errors" an hour, your error log is 97% noise — and the one
SqlException that matters is buried under exceptions that aren't
exceptional. Expected failures are results, not throws.
// ❌ Exceptions as control flow — your log becomes noise. throw new ClaimNotFoundException(claimId); // ✓ Expected outcomes are results, not exceptions. public async Task<Result<Claim>> GetClaimAsync(string id) { var claim = await _repo.FindAsync(id); return claim is null ? Result<Claim>.Failure($"Claim {id} not found") : Result<Claim>.Success(claim); }
Be the Architect, Not the Typist
Letting AI write your code one method at a time is a step sideways. Direct it at the system, hand it the constraints, and review the design — that's how twenty years of experience compounds with the tools.
// Don't type code. Direct the architect. // // Instead of: // "Write a method that adds two numbers" // // Try: // "Add an idempotent reconcile step to the // payment pipeline that handles partial refunds // without double-crediting the customer." public sealed class PaymentReconciler { public async Task<ReconcileResult> ReconcileAsync( PaymentBatch batch, CancellationToken ct) { // implementation guided by your specification } }