Introduction to Primary Constructors in C# 12

C# 12 brings us primary constructors, and honestly, they’re changing the way I write classes, structs, and records. With this feature, you can put constructor parameters right in the class declaration instead of creating a separate constructor method. The result? Way less code that’s much easier to read.

Let’s face it, we’ve all written the same constructor code hundreds of times. You know the drill: declare parameters, create private fields, assign values in the constructor body. It’s tedious and easy to mess up. That’s exactly the problem primary constructors solve.

How Primary Constructors Work in Classes

With primary constructors, you just put your parameters right in the class definition line. Then you can use these parameters anywhere in your class, no extra code needed.

Before and After: The Transformation

Here’s a before-and-after example that shows just how much cleaner your code gets:

// Before: Traditional constructor approach
public class Product
{
    private readonly ILogger _logger;
    private readonly int _id;

    public string Name { get; }
    public decimal Price { get; }

    public Product(int id, string name, decimal price, ILogger logger)
    {
        _id = id;
        Name = name;
        Price = price;
        _logger = logger;

        _logger.LogInformation($"Created product: {name}");
    }

    public void ApplyDiscount(decimal percentage)
    {
        // Method implementation
    }
}

// After: Using C# 12 primary constructor
public class Product(int id, string name, decimal price, ILogger logger)
{
    private readonly int _id = id;
    public string Name { get; } = name;
    public string Price { get; } = price;

    // Constructor parameters accessible throughout class body
    public void ApplyDiscount(decimal percentage)
    {
        logger.LogInformation($"Applying {percentage}% discount to {name}");
        // Implementation
    }
}

The benefits jump right out:

  • You write a lot less code
  • No need for a separate constructor method
  • You can use those parameters anywhere in the class

Primary Constructors with Inheritance

Primary constructors play nicely with inheritance too. You can easily pass values up to parent classes:

public class Entity(int id)
{
    public int Id { get; } = id;
}

public class User(int id, string username, string email) : Entity(id)
{
    public string Username { get; } = username;
    public string Email { get; } = email;
}

See how clean that is? The User class takes three parameters and passes the id right up to the Entity base class.

Dependency Injection Made Cleaner

If you work with dependency injection (and who doesn’t these days?), primary constructors are a game-changer:

public class OrderService(
    IOrderRepository repository,
    ILogger<OrderService> logger,
    IValidator<Order> validator)
{
    public async Task<Order> CreateOrderAsync(Order order)
    {
        if (!validator.Validate(order).IsValid)
        {
            logger.LogWarning("Invalid order");
            throw new ValidationException("Order is invalid");
        }

        return await repository.CreateAsync(order);
    }
}

Look at that! All your dependencies are right there at the top of the class. No more writing private fields and constructor boilerplate that just assigns parameters to fields.

Primary Constructors in Structs

Classes aren’t the only ones getting love here, structs work with primary constructors too:

public struct Point(int x, int y)
{
    public int X { get; } = x;
    public int Y { get; } = y;
}

It works pretty much the same as with classes, though remember that structs are value types so they behave a bit differently under the hood.

Primary Constructors and Records: A Perfect Match

Records and primary constructors go together like peanut butter and jelly:

// Simple record with primary constructor
public record Person(string Name, int Age);

// Record class with primary constructor
public record class Employee(string Name, int Id);

// Record struct with primary constructor
public record struct Point(int X, int Y);

The cool thing about records is they automatically create properties from those parameters, you don’t have to write any of that assignment code yourself.

Key Differences Between Classes, Structs, and Records

FeatureClassesStructsRecords
Parameter UsageCan use parameters anywhereCan use parameters anywhereParameters automatically become properties
Property AssignmentMust manually assign to properties/fieldsMust manually assign to fieldsNo need to assign manually
SyntaxVerboseVerboseShortest, cleanest
InheritanceFully supports inheritanceLimited or no inheritanceWorks with record classes and record structs
TypeReference typeValue typeCan be value (record struct) or reference type (record class)
Initialization RequirementManualMust initialize all fieldsHandled automatically

Technical Details Worth Knowing

Here are some nuts and bolts you should know about:

  • You can only use primary constructor parameters within the type they’re declared in, not in child classes
  • When a class initializes, the primary constructor runs before field initializers
  • You can still add extra constructors alongside your primary constructor
  • For records, those parameters automatically turn into properties

Best Practices for Using Primary Constructors

Want to get the most out of primary constructors? Here’s my advice:

  1. They’re perfect for data classes where parameters map directly to properties
  2. Use them for dependency injection to cut down on boilerplate
  3. For complex initialization with lots of logic, regular constructors might work better
  4. With records, just use primary constructors, it’s what they’re made for

Conclusion

Primary constructors in C# 12 are one of those features that make you wonder how you lived without them. They cut down the noisy boilerplate and let you focus on what your code actually does.

I’ve found they’re most useful in two places: simple data classes (less typing!) and dependency injection scenarios (goodbye, 10-line constructor that just assigns fields!).

Give primary constructors a try in your next project. Whether you’re working with classes, structs, or records, they’ll make your code cleaner and more straightforward. Your future self (and teammates) will thank you when they’re reading that code six months from now!