Skip to main content
SEMastery
.NET Corebeginner

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.

12 min readUpdated November 28, 2025

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.

The old way repeats the same names many times before you can use them.

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

Declare
Capture
Use

Steps

1

Declare

List params next to class name

2

Capture

Compiler keeps them in scope

3

Use

Read them in any method

You give values once at the top, then use them anywhere inside the type.

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.

TypeParameters become properties?Value equality for free?Main use
Plain classNoNoServices, helpers, small holders
Plain structNoNoSmall value-like data
RecordYes (positional)YesData carriers, DTOs
Record structYes (positional)YesSmall 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.

Dependency injection hands tools to a class, the same way a coach hands you a kit.

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

Extra ctor
this(...)
Primary ctor

Steps

1

Extra ctor

Takes simpler input

2

this(...)

Required call

3

Primary ctor

Fills all params

Any extra constructor is forced to call the primary one, so values are never missed.

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 pickWhy
A small immutable data bagRecordFree properties and equality
A service with injected toolsClass with primary constructorClean DI, no extra machinery
Mutable state with identityClass with primary constructorFull control over members
A tiny value-like coordinateStruct with primary constructorLightweight, value semantics
A simple way to choose between a record and a class with a primary constructor.

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

Expose?
Mutable?
Chained?

Steps

1

Expose?

Add a property if needed

2

Mutable?

Copy to read-only field

3

Chained?

Extra ctors call this(...)

Run through these checks before shipping a primary constructor class.

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

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