Getting Started with Primary Constructors in .NET 8 and C# 12
Learn C# 12 primary constructors in .NET 8 the easy way: cleaner classes, fewer lines, dependency injection, and simple real-life examples for beginners.
Think about joining a new cricket team. On day one, the coach hands you a kit: a bat, a ball, and a cap. From that moment, every time you walk onto the field, those items are simply with you. You do not collect them again before each match. They were given to you once, at the start, and you use them whenever you need them.
A primary constructor in C# 12 works in the very same way. You list what a class needs right at the top, next to its name, and from then on those things are available everywhere inside the class. You give them once. You use them anywhere. In this guide we will learn this feature slowly, in plain English, with small examples and pictures.
Primary constructors arrived with C# 12 and .NET 8. Today the .NET world has moved on to .NET 10 (the current LTS) and C# 14, but this feature is now a normal, everyday tool. Everything you learn here still works in the newer versions.
The problem before primary constructors
Before C# 12, even a tiny class needed a lot of repeated typing. Imagine a class that holds a student's name and roll number. You had to write the field, write the constructor parameter, and then copy the parameter into the field. The same word appeared three or four times.
Here is the old style, the one many of us wrote for years.
public class Student
{
private readonly string _name;
private readonly int _rollNumber;
public Student(string name, int rollNumber)
{
_name = name;
_rollNumber = rollNumber;
}
public string Describe() => $"{_name} has roll number {_rollNumber}";
}Look at how much of that is just plumbing. The real idea is small: a student has a name and a roll number. But the code is long. This repeated work is sometimes called boilerplate, and beginners often find it boring and easy to get wrong.
Your first primary constructor
Now look at the same idea written with a primary constructor. The parameters go right after the class name, inside round brackets.
public class Student(string name, int rollNumber)
{
public string Describe() => $"{name} has roll number {rollNumber}";
}That is the whole class. Notice three things. First, name and rollNumber are written once, next to the class name. Second, there is no separate constructor block. Third, you can use name and rollNumber directly inside the method, as if they were always there.
This is exactly like the cricket kit. The class received name and rollNumber at the start, and now it can use them anywhere inside its body.
How a primary constructor flows
Steps
Declare
List params next to class name
Capture
Compiler keeps them in scope
Use
Read them in any method
A very important rule: parameters are not properties
This part trips up many beginners, so read it slowly. In a normal class or struct, primary constructor parameters do not become public properties. They are just values that live inside the class. The outside world cannot read student.name from this example, because name is not a property.
This is different from records, which you may have seen. In a record, positional parameters automatically turn into public properties. In a plain class, they do not. If you want the outside world to see a value, you must create a property yourself.
public class Student(string name, int rollNumber)
{
// We choose to expose Name as a read-only property.
public string Name { get; } = name;
// rollNumber stays private to the class, used only inside.
public bool HasRoll(int value) => rollNumber == value;
}Here Name is a real property the outside world can read. But rollNumber is only used inside the class. You decide what to expose. This gives you control, which is a good thing.
The table below shows the difference clearly.
| Type | Parameters become properties? | Value equality for free? | Main use |
|---|---|---|---|
| Plain class | No | No | Services, helpers, small holders |
| Plain struct | No | No | Small value-like data |
| Record | Yes (positional) | Yes | Data carriers, DTOs |
| Record struct | Yes (positional) | Yes | Small immutable data |
Where primary constructors really shine: dependency injection
The single most loved use of primary constructors is dependency injection (often shortened to DI). DI is a fancy phrase for a simple idea: a class does not build the tools it needs by itself. Instead, those tools are handed to it from outside, usually through the constructor. Think again of the coach handing you the kit.
In an ASP.NET Core app, services often need a logger or a repository. Before C# 12, this meant a long constructor. Now it is one clean line.
public class OrderService(
IOrderRepository repository,
ILogger<OrderService> logger)
{
public async Task PlaceOrderAsync(Order order)
{
logger.LogInformation("Placing order {Id}", order.Id);
await repository.SaveAsync(order);
logger.LogInformation("Order {Id} saved", order.Id);
}
}See how repository and logger are listed once and then used inside the method? There are no private fields and no assignment lines. For services like this, primary constructors remove a lot of noise and let the real logic stand out.
Adding more constructors
Sometimes you want more than one way to build a class. C# allows this, but there is a rule you must follow: every other constructor must call the primary constructor using this(...). This makes sure the primary constructor parameters always receive a value.
public class Rectangle(double width, double height)
{
public double Width { get; } = width;
public double Height { get; } = height;
// A square is just a rectangle with equal sides.
public Rectangle(double side) : this(side, side)
{
}
public double Area() => Width * Height;
}The second constructor takes one number, side, and passes it to the primary constructor twice. If you forget the : this(...) part, the compiler will stop you with an error. This is the language protecting you from a half-built object.
Extra constructors must chain back
Steps
Extra ctor
Takes simpler input
this(...)
Required call
Primary ctor
Fills all params
Default values and validation
You can give primary constructor parameters default values, just like normal method parameters. You can also run a small check by using the parameter inside a property initializer.
public class Account(string owner, decimal balance = 0m)
{
public string Owner { get; } = string.IsNullOrWhiteSpace(owner)
? throw new ArgumentException("Owner is required")
: owner;
public decimal Balance { get; private set; } = balance;
public void Deposit(decimal amount) => Balance += amount;
}Here balance defaults to zero, so a caller can skip it. And Owner is checked when the object is built. If the owner name is empty, the object refuses to exist. This is a friendly way to stop bad data early.
One small caution. Validation in a property initializer runs only when that property is set. If you have many checks spread across the class, a plain old constructor body can sometimes read more clearly. Pick the style that makes your intent obvious.
Primary constructors in structs
Primary constructors are not only for classes. Structs can use them too. A struct is a small value type, good for tiny pieces of data like a point on a screen.
public struct Point(int x, int y)
{
public int X { get; } = x;
public int Y { get; } = y;
public readonly double DistanceFromOrigin()
=> Math.Sqrt(X * X + Y * Y);
}The same rules apply. The parameters x and y are in scope inside the struct, and we choose to expose them as properties. Structs do not get value equality for free here either; only records do that automatically.
How it compares to records
Beginners often ask, "If records already do this, why do I need primary constructors in classes?" Good question. The answer is about choice and control. A record decides a lot for you: it makes properties, builds value equality, and writes a nice ToString. That is wonderful for data holders. But for a service class, you usually do not want all that. You just want clean dependency injection without extra machinery.
The next table lines up the everyday decisions.
| You want... | Best pick | Why |
|---|---|---|
| A small immutable data bag | Record | Free properties and equality |
| A service with injected tools | Class with primary constructor | Clean DI, no extra machinery |
| Mutable state with identity | Class with primary constructor | Full control over members |
| A tiny value-like coordinate | Struct with primary constructor | Lightweight, value semantics |
Common mistakes to avoid
Let us look at the small traps that catch new learners. Knowing them now will save you confusion later.
The first trap is expecting properties. As we said, class Person(string name) does not give you a Name property. If you write person.name from outside, it will not compile. Declare the property yourself.
The second trap is a captured parameter changing. If you use a primary constructor parameter directly inside many methods, the compiler may store it in a hidden field. If that parameter is mutable and you change it, the change sticks for the life of the object. To avoid surprises, copy what you need into a read-only property.
The third trap is forgetting the chain rule. Any extra constructor must call : this(...). Forget it, and you get a clear compiler error.
The fourth trap is over-using the feature. Primary constructors are great for simple cases. For a class that needs heavy, multi-step setup, a normal constructor body can be easier to read. Use the right tool for the job.
Quick safety checklist
Steps
Expose?
Add a property if needed
Mutable?
Copy to read-only field
Chained?
Extra ctors call this(...)
A complete, slightly bigger example
Let us tie it all together with a small library example. A Library holds books and can lend them. It uses a primary constructor for its dependency and an extra constructor for convenience.
public class Library(string name, IClock clock)
{
private readonly List<string> _borrowed = new();
public string Name { get; } = name;
// Convenience constructor: use the real system clock.
public Library(string name) : this(name, new SystemClock())
{
}
public void Borrow(string title)
{
_borrowed.Add(title);
Console.WriteLine($"{title} borrowed at {clock.Now} from {Name}");
}
public int BorrowedCount => _borrowed.Count;
}Here name becomes a public Name property, clock stays private and is used inside Borrow, and the extra constructor chains back with : this(...). This one class shows almost everything we learned: capture, expose some, hide some, and chain extra constructors.
A note on tooling and the wider .NET world
When you build real apps, you often pull in helper libraries. A friendly reminder for the current .NET landscape: some popular packages such as MediatR and MassTransit moved to commercial licensing. That does not affect primary constructors at all, since this is a plain language feature built into the compiler. But it is good to know when you choose libraries for a new project. Primary constructors themselves cost nothing and need no package.
Quick recap
- A primary constructor lets you list a type's inputs right after its name, like
class Student(string name, int rollNumber). - Those parameters are in scope everywhere inside the type, so you write far less boilerplate.
- In a plain class or struct, the parameters are not properties. Only records turn positional parameters into properties automatically.
- If you want to expose a value, declare a property yourself, for example
public string Name { get; } = name;. - They are perfect for dependency injection, giving clean, short service classes.
- Any extra constructor must call the primary one using
: this(...). - You can use default values and do validation in property initializers.
- Use the right tool: records for data bags, classes with primary constructors for services and richer behavior.
References and further reading
- Declare C# primary constructors (Microsoft Learn)
- What's new in C# 12 (Microsoft Learn)
- .NET 8 and C# 12 — Primary Constructors, Henrique Siebert Domareski
- C# 12: Primary Constructors, Thomas Claudius Huber
With primary constructors, your classes say what they need clearly, in one line, and then get on with their real work. Just like that cricket kit handed to you on day one, you receive what you need once, and use it whenever the game calls for it.
Related Posts
Getting Started with C# Records: A Beginner's Friendly Guide
Learn C# records the easy way: value equality, with expressions, positional syntax, and record struct, explained with simple real-life examples.
C# init-only and required Properties: A Beginner's Guide
Learn C# init-only and required properties with simple analogies, diagrams, and code. Build safe, immutable objects that are filled correctly every time.
Creating Custom Attributes in C#: A Beginner's Guide
Learn to create custom attributes in C# from scratch. Use AttributeUsage, attach extra info to your code, and read it back with reflection. Simple examples.
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.
Improve Code Readability with C# Collection Expressions
Learn C# collection expressions and the spread element with simple analogies, diagrams, and code. Write cleaner arrays, lists, and spans in modern .NET.
How to Write Elegant Code with C# Pattern Matching
Learn C# pattern matching the easy way. Use is, switch expressions, property and list patterns to write clean, readable, and elegant .NET code.