Primary Constructors in C# 12

Every C# developer has written this pattern hundreds of times: declare a class, write a constructor, assign each parameter to a field or property. It is pure boilerplate. C# 12 introduces primary constructors for classes and structs, cutting this ceremony down to a single line.

The Old Way

A typical service class with dependency injection looks like this:

Example.cs
public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly ILogger<OrderService> _logger;

    public OrderService(IOrderRepository repository, ILogger<OrderService> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public async Task<Order> GetOrder(int id)
    {
        _logger.LogInformation("Fetching order {Id}", id);
        return await _repository.GetByIdAsync(id);
    }
}

That is eleven lines before you write any actual logic.

The New Way

With a primary constructor, the parameters are declared directly on the type:

Example.cs
public class OrderService(IOrderRepository repository, ILogger<OrderService> logger)
{
    public async Task<Order> GetOrder(int id)
    {
        logger.LogInformation("Fetching order {Id}", id);
        return await repository.GetByIdAsync(id);
    }
}

The constructor parameters are in scope throughout the entire class body. No fields, no assignments, no constructor body. The class focuses entirely on its behaviour.

How Parameters are Captured

Primary constructor parameters are not fields or properties — they are captured variables, similar to how closures capture variables in lambda expressions. The compiler generates a backing field behind the scenes, but you do not see it.

This has an important implication: the parameters are mutable by default. You can reassign them:

Example.cs
public class Counter(int initialValue)
{
    public int Current => initialValue;

    public void Increment() => initialValue++; // This compiles and works
}

If you want immutability, assign the parameter to a readonly field explicitly:

Example.cs
public class Counter(int initialValue)
{
    private readonly int _initialValue = initialValue;

    public int Current => _initialValue;
}

Initialising Properties

You can use primary constructor parameters to initialise properties:

Example.cs
public class Person(string firstName, string lastName)
{
    public string FirstName { get; } = firstName;
    public string LastName { get; } = lastName;
    public string FullName => $"{FirstName} {LastName}";
}

When a parameter is captured by a property initialiser and not used elsewhere in the class body, the compiler does not generate a separate backing field for the parameter — it only uses the property's backing field.

Calling Base Constructors

Primary constructors chain naturally to base class constructors:

Example.cs
public class Animal(string name)
{
    public string Name => name;
}

public class Dog(string name, string breed) : Animal(name)
{
    public string Breed => breed;
}

Validation

You can validate parameters by assigning them to fields in the class body with validation logic:

Example.cs
public class Temperature(double celsius)
{
    private readonly double _celsius = celsius >= -273.15
        ? celsius
        : throw new ArgumentOutOfRangeException(nameof(celsius));

    public double Celsius => _celsius;
    public double Fahrenheit => _celsius * 9.0 / 5.0 + 32.0;
}

Alternatively, use a static factory method pattern if validation is complex.

Primary Constructors vs Records

Records have had primary constructors since C# 9. The key differences:

Example.cs
// Record: generates public properties, equality, ToString, deconstruction
public record PersonRecord(string Name, int Age);

// Class: parameters are captured variables, no generated members
public class PersonClass(string name, int age)
{
    // You must explicitly expose properties if needed
    public string Name => name;
    public int Age => age;
}

Record primary constructors create public init properties automatically. Class primary constructors create nothing — the parameters are simply available in scope.

Best Practices

Use primary constructors for dependency injection. This is the single biggest win. Service classes that exist only to receive dependencies and use them become dramatically shorter.

Be careful with mutability. Since parameters are mutable, consider whether this is intentional. For services, mutability rarely matters because you never reassign the dependency. For data-carrying types, prefer records.

Do not overuse parameters as pseudo-fields. If you find yourself using a primary constructor parameter in many members, and especially if you need it to be readonly, an explicit field is clearer about intent.

Example.cs
// Good — clean DI service
public class UserService(IUserRepository repo, ILogger<UserService> logger)
{
    public async Task<User?> Find(string email) =>
        await repo.FindByEmailAsync(email);
}

// Consider a record instead — this is really a data type
public class Point(double x, double y)  // Use record Point(double X, double Y)
{
    public double X => x;
    public double Y => y;
}

Primary constructors remove one of the oldest sources of boilerplate in C#. For service classes, controllers, and handlers, they are an immediate upgrade that makes your code shorter and more focused on what matters.