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.