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
Gilligan called .Result on the Professor's async radio and blocked
the entire island. Your API does the same thing — and the thread-pool exhaustion
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);
Unit Testing with NSubstitute — Tests That Survive Refactoring
The Professor could build a radio from a coconut, but his tests checked which coconut was used instead of whether the radio worked. Your test suite makes the same mistake — and 47 tests break every time you refactor.
// Test behaviour — not which functions got called. [Fact] public async Task Returns_Discount_For_VIP_Customer() { var pricing = Substitute.For<IPricingService>(); pricing.GetDiscount(Arg.Any<Customer>()) .Returns(0.15m); var service = new OrderService(pricing); var total = await service.GetTotal(vipCustomer, 100m); Assert.Equal(85m, total); }
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>();
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 } }
new
Backend
Live
12 min