C# Primary Constructor: Syntax and Use Cases
c# primary constructor: Learn how C# primary constructors work, when they simplify code, and where they cause maintenance problems.
If you have written a C# class that only stores constructor parameters as fields, you have felt the boilerplate. The c# primary constructor, introduced in C# 12, aims to remove that ceremony for the common case where a class exists to hold data. It allows you to declare constructor parameters directly in the type declaration, and the compiler generates the backing fields and assigns them, saving you from writing explicit field declarations and assignment code. This article explains the syntax, the generated behavior, and the practical cases where primary constructors help or hurt your codebase.
How the Syntax Works
A primary constructor is declared by adding a parameter list right after the type name. For example, instead of declaring a field, a constructor, and an assignment, you can write:
public class Product(string name, decimal price) { public string Name { get; } = name; public decimal Price { get; } = price; }
The parameters name and price are in scope throughout the class body. They can be used to initialize properties, passed to base constructors, or captured in methods. The compiler creates a constructor with those exact parameters and wires up the assignments you write. If you do not use a parameter outside of initialization, the compiler elides the backing field entirely, which keeps the type lean.
This differs from a record, which gives you positional properties automatically. A primary constructor on a regular class only provides parameter capture; you must decide how to expose the values. You could expose them as read-only properties, as in the example above, or keep them private and use them in methods.
Capturing and Using Parameters in Methods
When a primary constructor parameter is used within a method body, the compiler creates a hidden backing field to hold the value across calls. This enables patterns like the following:
public class OrderService(IOrderRepository repository, ILogger<OrderService> logger) { public async Task<Order> GetAsync(int id) { logger.LogInformation("Fetching order {OrderId}", id); return await repository.GetByIdAsync(id); } }
Here repository and logger are captured by the generated backing fields, and the methods use them. This is a common pattern for dependency injection, where the primary constructor succinctly expresses the service's dependencies. The generated constructor is equivalent to a conventional constructor that assigns these fields, so this code is functionally identical to the older form:
public class OrderService { private readonly IOrderRepository _repository; private readonly ILogger<OrderService> _logger; public OrderService(IOrderRepository repository, ILogger<OrderService> logger) { _repository = repository; _logger = logger; } // same methods }
From a consumer's perspective, there is no difference: you still use new OrderService(repo, logger). The primary constructor simply collapses the boilerplate into the type declaration, which can improve readability when there are many dependencies.
Where Primary Constructors Excel
Primary constructors fit best with classes that primarily exist to hold data or that have a clear set of dependencies injected once at construction. Typical examples include DTOs, configuration records, and service classes that take a set of dependencies and expose methods using them.
For a data holder, the syntax is compact and the intent is obvious. Compare the explicit version:
public class Point { public double X { get; } public double Y { get; } public Point(double x, double y) { X = x; Y = y; } }
with the primary constructor version:
public class Point(double x, double y) { public double X { get; } = x; public double Y { get; } = y; }
Both produce a class with read-only X and Y properties. The primary constructor version reduces the risk of forgeting to assign a field and makes it easier to spot the data shape at a glance. This benefit scales with the number of fields: a class with six fields and six assignments becomes six lines of parameters plus six property initializers, instead of a constructor body with repetitive assignments.
For dependency injection, primary constructors also reduce the noise of repeated field declarations and assignments. If you have a service with five injected dependencies, you save about ten lines of code. More importantly, the class signature clearly lists its dependencies, which can help with unit testing and composition root configuration.
Limitations and Pitfalls
Primary constructors are not a universal replacement for conventional constructors. They have a few notable limitations that can trip you up.
First, you cannot add validation logic inside the primary constructor itself because the parameter list is not a method body. If a price must be non-negative, you must either use a conventional constructor or perform validation in property initializers or property setters. For example, you could write:
public class Product(string name, decimal price) { public string Name { get; } = name; public decimal Price { get; } = price; // no constructor body to run validation }
There is no place to throw an ArgumentException before the object is fully constructed, unless you introduce a static factory or a traditional constructor. If validation is a core requirement, a conventional constructor is often clearer.
Second, you cannot have a primary constructor and another constructor that calls its own logic. The generated constructor is the only one that executes the parameter assignments. If you add an additional constructor, it must call the primary constructor using this(...). This is possible, but it can become awkward if the primary constructor has many parameters.
Third, using primary constructor parameters in property initializers only works for properties that are set directly. You cannot, for example, use them in an expression-bodied property that requires processing at construction time, such as:
public class Product(string name, decimal price) { public decimal TaxedPrice => price * 1.2m; // works, uses captured field }
That is fine because it is a computed property, not an initialization. But if you need to calculate a value once and store it, you have to do it in a conventional constructor or in a method called from the primary constructor.
Interaction with Records and Inheritance
Records already have a positional syntax that resembles primary constructors, but they are different in purpose. A record with record Product(string Name, decimal Price) automatically generates properties, equality members, and a deconstructor. A primary constructor on a class does none of that. Choosing between them depends on whether you need value semantics. If you want structural equality and built-in ToString, use a record. If you only want to eliminate boilerplate, use a primary constructor on a class.
With inheritance, primary constructors must call a base constructor. The base class's primary constructor parameters are passed via the : base(...) clause. For example:
public class DiscountedProduct(string name, decimal price, decimal discountPercent) : Product(name, price) { public decimal Discount => price * discountPercent / 100; }
The derived class must pass the base's required arguments. This works cleanly when the base class also uses a primary constructor. If the base has a conventional constructor, the same rules apply: you call it in the initializer list.
One subtlety is that a derived class's primary constructor parameter that is not passed to the base is still captured in the derived type, so it remains in scope for the derived class's members.
Compatibility and Build Considerations
Primary constructors are a C# 12 feature, so they require .NET 8 or a newer SDK that supports C# 12. If you are targeting an older runtime, you can still use the syntax if you compile with a newer compiler and set the language version, but the generated code may rely on runtime support only if you use certain features. In practice, using C# 12 features with older target frameworks can cause issues; it is safer to match the language version with the project's target framework.
If you need to support older compilers, you cannot use this syntax at all. You have to stick with conventional constructors. That is a practical consideration if you maintain a library that must compile against older language versions.
Another point is that primary constructors can affect source-level compatibility when you later change the parameter list. Changing a primary constructor signature changes the generated public constructor, which is a breaking change for callers. With a conventional constructor, you have more control over overloads and can often add an optional parameter without breaking callers. Primary constructors do not support optional parameters directly, though you can define multiple constructors that call the primary one.
How to Decide Between Primary and Conventional Constructors
Given the tradeoffs, the choice is not about which is newer but about what the class needs. Use a primary constructor when:
- The class is a simple data holder whose constructor only assigns fields.
- You have a service class with a fixed set of injected dependencies.
- You do not need to run validation or other logic during construction.
- You are compiling with C# 12 or later.
Prefer a conventional constructor when:
- You must validate parameters and reject invalid input before the object is created.
- The constructor needs to perform complex initialization, such as loading data or preparing collections.
- You need to provide multiple overloads with different parameter sets.
- The class is part of a library that must support older language versions.
The decision also depends on team preference and codebase conventions. If your team values minimal boilerplate and the code is a modern .NET project, primary constructors can improve readability. If you need explicit control over construction-time behavior, the conventional form remains the safer choice. The important thing is to understand that a primary constructor is not a hidden performance advantage; the generated IL is essentially identical to a manual constructor. It is a source-level convenience, not a runtime optimization.
A good approach is to start with primary constructors only in classes that are clearly data carriers, such as configuration models or simple DTOs. For services, consider whether the dependency list is stable and whether you might need to add an overload later. If you foresee changes to the constructor signature, a conventional constructor gives you more flexibility.
Ultimately, C# primary constructors are a useful tool that reduces repetitive code, but they come with constraints that matter when a class needs real construction logic. Weigh those constraints against the readability benefits before adopting them broadly.