Write .NET code that scales.
20 years of C#/.NET in production — world-class healthcare companies, national-scale real estate platforms, high-volume microservices. 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>();
Async/Await — Don't Block Your Threads
A waiter who stands at the kitchen window waiting for one dish starves the whole dining room. Your API does the same thing every time async code turns back into blocking code — and it only shows up when production traffic hits.
// ❌ Blocks the thread — exhausts the pool under load. public IActionResult GetOrder(int id) => Ok(_repo.GetAsync(id).Result); // ✓ Awaits properly — frees the thread for other work. public async Task<IActionResult> GetOrder(int id) => Ok(await _repo.GetAsync(id)); // And the call site: await using var db = await _factory.CreateAsync(); var customer = await db.Customers .Where(c => c.Id == id) .SingleAsync(ct);
Dependency Injection in ASP.NET Core — Stop Using new
If your controller creates its own dependencies, your tests must too — including real database connections. A test that should take 50 ms now takes 2 seconds and fails if the database is offline.
// Constructor injection — clean & testable. public class OrderController(IOrderService service) : ControllerBase { [HttpPost] public IActionResult Create(OrderDto dto) => Ok(service.Create(dto)); } // Program.cs — register once, inject everywhere. builder.Services.AddScoped<IOrderService, OrderService>();
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 } }