Skip to main content
SEMastery
.NET Coreintermediate

Why I Switched to Primary Constructors for DI in C#

A friendly guide on why primary constructors made my C# dependency injection cleaner, with simple examples, diagrams, tables, and honest trade-offs.

12 min readUpdated March 10, 2026

Think about an autorickshaw driver getting ready for the day. Before the first ride, he checks three things: fuel in the tank, air in the tyres, and the meter working. He sets these up once in the morning. After that, for every passenger, he just drives. He does not refill fuel at each stop or refit the tyres for each trip. The setup happens one time, at the start, and then the tools are simply there whenever he needs them.

A class in C# is a lot like that driver. It also needs a few tools to do its job: maybe a logger to write messages, a repository to read from a database, an email sender. We hand those tools to the class once, when it is created. This handing-over is called dependency injection, or DI for short. And the place where we receive the tools is the constructor.

For years I wrote that constructor the long way. Then C# 12 gave us primary constructors, and after using them for a while, I switched almost all my DI classes to this style. This post is the honest story of why I switched, what got better, and the few sharp corners you should know about. We will go slowly, with small examples and pictures.

Quick note on versions: primary constructors arrived in C# 12 with .NET 8. Today .NET 10 is the current LTS and C# 14 has shipped, so this feature is now a normal, everyday tool. Everything below works in all those versions.

First, what is dependency injection?

Imagine a class called OrderService. Its job is to save an order and then send a confirmation email. To do this it needs two helpers: a database repository and an email sender.

There are two ways to get those helpers.

The bad way is to build them inside the class with new. Then OrderService is glued to one exact database and one exact email service. You cannot swap them. You cannot test the order logic without really sending an email. That is painful.

The good way is dependency injection. The class simply asks for what it needs in its constructor. Something outside the class, called the DI container, builds the helpers and hands them in. Now the class is free. In a real app you pass the real email sender. In a test you pass a fake one that does nothing.

The DI container builds the helpers and hands them to the class through its constructor.

So the constructor is the front door where dependencies walk in. The question this post answers is simple: how should we write that front door?

The old way, with hand-written fields

Before C# 12, here is how almost every DI class looked. Notice how each dependency is mentioned three times.

public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly IEmailSender _emailSender;
    private readonly ILogger<OrderService> _logger;
 
    public OrderService(
        IOrderRepository repository,
        IEmailSender emailSender,
        ILogger<OrderService> logger)
    {
        _repository = repository;
        _emailSender = emailSender;
        _logger = logger;
    }
 
    public async Task PlaceOrderAsync(Order order)
    {
        await _repository.SaveAsync(order);
        await _emailSender.SendAsync(order.CustomerEmail, "Order placed!");
        _logger.LogInformation("Order {Id} placed", order.Id);
    }
}

Count the typing. IEmailSender appears once as a field type, once as a parameter type, and the name emailSender shows up in the parameter list and again in the assignment. For a class with five dependencies, the top of the file becomes a wall of plumbing before you reach a single line of real logic.

I never liked this. It is boring to write, easy to get wrong (forget one assignment and you get a NullReferenceException later), and it hides the actual purpose of the class under a pile of repetition.

The new way, with a primary constructor

Here is the exact same class written with a primary constructor. The dependencies are listed right next to the class name. No fields. No assignments.

public class OrderService(
    IOrderRepository repository,
    IEmailSender emailSender,
    ILogger<OrderService> logger)
{
    public async Task PlaceOrderAsync(Order order)
    {
        await repository.SaveAsync(order);
        await emailSender.SendAsync(order.CustomerEmail, "Order placed!");
        logger.LogInformation("Order {Id} placed", order.Id);
    }
}

That is the whole class. The dependencies repository, emailSender, and logger are now in scope everywhere inside the class body. You use them by their plain names. No underscore, no assignment, no separate field.

When you use one of these parameters inside a method, the compiler quietly creates the hidden storage for it. You do not see that storage, but it is there. So the value lives for the whole life of the object, exactly like a field would. If a parameter is never used in any method, the compiler is smart enough to not create storage at all.

The two versions do the same thing. But the second one fits in your head at a glance. The job of the class is now the first thing you read, not the last.

Side by side: the old style repeats each dependency, the new style names it once.

Why I actually switched

It was not only about typing less. A few concrete things got better in my daily work.

My reasons to switch

Less noise
Fewer bugs
Faster reading
Easy refactor

Steps

1

Less noise

No repeated fields

2

Fewer bugs

No forgotten assignment

3

Faster reading

Logic comes first

4

Easy refactor

Add a dep in one spot

The benefits that made me change my default style for DI classes.

Less noise. A service that depends on four things used to start with about twelve lines of plumbing. Now it starts with the class name and the four parameters. The signal-to-noise ratio of my files went up a lot.

Fewer silly bugs. With hand-written constructors, a classic mistake is to add a new parameter but forget the assignment line. The code still compiles. Then at runtime you hit a null. With primary constructors there is no assignment line to forget, so that whole bug class disappears.

Logic reads first. When I open a class, I want to see what it does. The primary constructor moves the "what it needs" up to the title line and lets the methods speak for themselves right after.

Adding a dependency is one edit. Need to add an ICache? You add one parameter to the header and start using cache. Done. In the old style you touched three places.

Here is a table of the same trade-off, side by side.

ConcernOld hand-written constructorPrimary constructor
Lines to declare a dependencyAbout three per dependencyOne per dependency
Risk of forgetting an assignmentReal, fails at runtimeNone, no assignment exists
Where the name livesField, parameter, assignmentJust the parameter
Readability of small servicesBuried under plumbingClear and short
Works with the DI containerYesYes, exactly the same

How the DI container sees it

A fair worry when switching is: "Will the dependency injection container still understand my class?" The happy answer is yes, and nothing changes in your registration.

The container does not read your class line by line. It looks at the class's public constructor and its parameters, then fills each parameter with a service it knows about. A primary constructor is a public constructor. So the container treats both styles the same.

Your registration in Program.cs stays exactly as before.

var builder = WebApplication.CreateBuilder(args);
 
// Register the dependencies the container will inject.
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
 
// Register the service that uses them. Its constructor style does not matter.
builder.Services.AddScoped<OrderService>();
 
var app = builder.Build();
app.Run();

When something asks for an OrderService, the container looks at the constructor, sees it needs an IOrderRepository and an IEmailSender, builds those, and passes them in. It genuinely does not care whether you wrote the old style or the new one.

The container reads the constructor parameters and resolves each one before building the service.

This works the same way in ASP.NET Core controllers too. A controller with a primary constructor is perfectly normal, and the framework injects its dependencies just like before.

The sharp corners you should know

I switched happily, but I would be a poor teacher if I only showed the good side. There are a few things that bite people. Knowing them ahead of time means they will never bite you.

1. Primary constructor parameters are not readonly

This is the big one. The old private readonly field could never be reassigned by accident. A primary constructor parameter can be changed inside the class. The compiler does not stop you from writing repository = somethingElse; later in a method.

For a dependency, you almost never want that. If you want the old safety, assign the parameter to a real readonly field yourself.

public class PaymentService(IBankGateway gateway)
{
    // Capture into a readonly field so it can never be reassigned by accident.
    private readonly IBankGateway _gateway = gateway;
 
    public Task ChargeAsync(decimal amount) =>
        _gateway.ChargeAsync(amount);
}

Yes, this brings back one line of plumbing. But you only do it when you truly care about that extra safety, not for every class.

2. The same name can shadow

If a primary constructor parameter is named name and you also have a property called Name, things can get confusing fast. Keep parameter names clear and avoid clashing with property names. A little care here saves head-scratching later.

3. Inheritance needs a small tweak

If your class has a base class, the primary constructor passes values up to the base using a short syntax right in the header. It reads cleanly once you have seen it once.

SituationWhat to do
Plain DI serviceUse the primary constructor directly
Need a readonly guaranteeCapture into a private readonly field
Want validation on a valueValidate in a field initializer or a body block
Base class needs argumentsPass them with : base(...) in the header

Should I capture into a field?

Start
Need readonly?
Need validation?
Plain param is fine

Steps

1

Start

New DI class

2

Need readonly?

Yes: make a field

3

Need validation?

Yes: field init

4

Plain param is fine

Otherwise just use it

A quick decision guide for when to keep the plain parameter and when to add a field.

A quick word on testing

Some folks worry that hidden storage makes testing harder. It does not. You test a class with a primary constructor exactly the same way you always did: you build it directly and pass in fakes.

[Fact]
public async Task PlaceOrder_SavesAndEmails()
{
    var repo = new FakeOrderRepository();
    var email = new FakeEmailSender();
    var logger = NullLogger<OrderService>.Instance;
 
    // Build the service by hand, passing fakes into the primary constructor.
    var service = new OrderService(repo, email, logger);
 
    await service.PlaceOrderAsync(new Order { Id = 1, CustomerEmail = "[email protected]" });
 
    Assert.True(repo.WasSaved);
    Assert.True(email.WasSent);
}

Because the constructor parameters are just normal parameters, the test reads naturally. There is no magic to mock and nothing special to set up.

When I still keep the old style

I did not switch everything. There are a few cases where I happily keep a hand-written constructor.

When a class needs heavy validation on its inputs, or runs setup logic that is clearer spread across a few lines, a normal constructor body gives more room to breathe. When a dependency must be readonly and I would have to capture it into a field anyway, the saving is smaller. And in older code that the whole team knows well, I do not rewrite working classes just for style. The win is biggest for new, simple DI services, and that is most of what I write.

A note on the wider ecosystem while we are here: some popular libraries that people pair with services, like MediatR and MassTransit, have moved to a commercial license for newer versions. That has nothing to do with primary constructors, but if you are choosing tools for a new project, check the license first so there are no surprises.

Quick recap

  • Dependency injection means a class receives its tools from outside, through its constructor, instead of building them itself.
  • The old style declared a field, a parameter, and an assignment for every dependency, which was repetitive and easy to get wrong.
  • A primary constructor lists the dependencies next to the class name and lets you use them by name anywhere inside the class.
  • The compiler creates the hidden storage only when a parameter is actually used, so there is no waste.
  • The DI container works exactly the same with both styles, and your registration in Program.cs does not change.
  • The biggest gotcha: primary constructor parameters are not readonly. Capture into a private readonly field when you need that guarantee.
  • Testing is unchanged: build the class with fakes and assert as usual.
  • Keep the old style for heavy validation, strict readonly needs, or stable legacy code; reach for primary constructors for new, simple services.

References and further reading

Related Posts