Primary Constructors Everywhere: Cleaner DI in Three Seconds
Remember when you’d open an ASP.NET Core service class and the first 20 lines were just field declarations and a constructor that assigned them? We all pretended that was fine. It wasn’t fine. C# 12 primary constructors fix this for classes (not just records), and your DI code has never looked this clean.
Here’s the classic “before” that we’ve all copy-pasted a thousand times:
public class OrderService{ private readonly IOrderRepository _repo; private readonly ILogger<OrderService> _logger; private readonly IEmailSender _email;
public OrderService( IOrderRepository repo, ILogger<OrderService> logger, IEmailSender email) { _repo = repo; _logger = logger; _email = email; }
public async Task PlaceOrderAsync(Order order) { _logger.LogInformation("Placing order {Id}", order.Id); await _repo.SaveAsync(order); await _email.SendConfirmationAsync(order); }}And here’s the “after” with a primary constructor:
public class OrderService( IOrderRepository repo, ILogger<OrderService> logger, IEmailSender email){ public async Task PlaceOrderAsync(Order order) { logger.LogInformation("Placing order {Id}", order.Id); await repo.SaveAsync(order); await email.SendConfirmationAsync(order); }}That’s it. No fields, no constructor body, no ceremony. The parameters are captured and available throughout the class. You just slashed 10+ lines of boilerplate without sacrificing readability (arguably improving it).
One thing to keep in mind: primary constructor parameters are not automatically readonly fields. They’re captured variables, so technically you could reassign repo = null somewhere in the class (please don’t). If that keeps you up at night, you can still assign them to readonly fields explicitly, but in practice the convention is simple: treat them as read-only and move on.
Register your service the same way you always have (builder.Services.AddScoped<OrderService>()), and the DI container handles the rest. Less plumbing, more shipping. That’s the dream.