loading experience

C#

Primary constructors in C#: when to use them and when not to

Less boilerplate in service classes, but with a few pitfalls worth knowing.

Primary constructors in C#: when to use them and when not to

Since C# 12, classes can have a primary constructor: parameters are declared next to the class name and are visible throughout the body. They are perfect for services with dependency injection.

Handy for services, to be used with some care.
Handy for services, to be used with some care.

Before and after

// Before
public class OrderService
{
    private readonly IOrderRepository _repo;
    private readonly ILogger<OrderService> _log;

    public OrderService(IOrderRepository repo, ILogger<OrderService> log)
    {
        _repo = repo;
        _log = log;
    }
}

// With the primary constructor
public class OrderService(IOrderRepository repo, ILogger<OrderService> log)
{
    public async Task<Order?> FindAsync(int id)
    {
        log.LogInformation("Looking up order {Id}", id);
        return await repo.GetAsync(id);
    }
}

The pitfall: parameters are not readonly

A primary constructor parameter can be modified inside the class. If you want the guarantee that it does not change, assign it to a readonly field:

public class Cache(TimeSpan duration)
{
    private readonly TimeSpan _duration = duration;
}

Other things to watch

  • Do not use the same parameter both directly and to initialise a field: the compiler warns because two copies of the state would be created.
  • For types that represent data, record types remain the best choice: they generate properties, equality and ToString.
  • If the constructor must validate parameters, validation in a field initialiser works, but for complex logic a classic constructor is clearer.
Comments (0)

No comments yet.

Leave a comment