Skip to main content

Primary Constructors (C# 12)

Overview

Primary constructors allow you to declare constructor parameters directly in the class or struct declaration. This feature was originally available only for records but is now extended to all types in C# 12.

Syntax

// Basic primary constructor
public class Person(string name, int age)
{
// Parameters are automatically available in the class scope
public void Introduce() => Console.WriteLine($"Hi, I'm {name}, {age} years old");
}

// With struct
public struct Point(double x, double y)
{
public double Distance => Math.Sqrt(x * x + y * y);
}

Key Concepts

1. Parameter Scope

Primary constructor parameters are not automatically properties. They're captured variables available throughout the class:

public class Logger(string logPath)
{
// logPath is available here but NOT a property
private readonly FileStream _stream = File.OpenWrite(logPath);

public void Log(string message) =>
Console.WriteLine($"Logging to {logPath}: {message}");
}

// Usage
var logger = new Logger("app.log");
// logger.logPath // ❌ This doesn't exist!

2. Creating Properties from Parameters

If you need properties, you must explicitly declare them:

public class Person(string name, int age)
{
// Create properties explicitly
public string Name { get; } = name;
public int Age { get; } = age;
}

3. Additional Constructors Must Chain

Any additional constructors must call the primary constructor using this():

public class Product(string name, decimal price)
{
// Additional constructor - MUST chain to primary
public Product(string name) : this(name, 0m)
{
}

public void Display() => Console.WriteLine($"{name}: ${price}");
}

4. With Inheritance

Base class primary constructors must be called:

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

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

Common Patterns

Pattern 1: Dependency Injection

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

Pattern 2: Immutable Types

public class Configuration(string connectionString, int timeout, bool enableRetry)
{
public string ConnectionString { get; } = connectionString;
public int TimeoutSeconds { get; } = timeout;
public bool EnableRetry { get; } = enableRetry;
}

Pattern 3: Validation in Constructor

public class Email(string address)
{
public string Address { get; } = Validate(address);

private static string Validate(string email)
{
if (string.IsNullOrWhiteSpace(email) || !email.Contains('@'))
throw new ArgumentException("Invalid email", nameof(email));
return email;
}
}

Primary Constructors vs Traditional Constructors

AspectPrimary ConstructorTraditional Constructor
SyntaxConcise, parameters in class declarationVerbose, separate method
Parameter accessAvailable throughout classOnly in constructor body
PropertiesMust declare explicitlyCan set in constructor
Additional constructorsMust chain with this()Can chain with this()
Best forDI, immutable types, simple initializationComplex initialization logic

When to Use Primary Constructors

Use when:

  • Injecting dependencies (DI pattern)
  • Creating immutable value objects
  • Parameters are used in initializers or throughout the class
  • You want cleaner, more concise code

Avoid when:

  • You need complex validation logic
  • Parameters should be public properties (use records instead)
  • You need to store parameters differently than received

Interview Questions

Q: What's the difference between primary constructors in classes vs records?

A: In records, primary constructor parameters automatically become public properties. In classes, they're just captured parameters available in the class scope - you must explicitly create properties if needed.

Q: Can you have multiple primary constructors?

A: No, you can only have one primary constructor. However, you can have multiple additional constructors that chain to the primary constructor using this().

Q: Are primary constructor parameters fields or properties?

A: Neither - they're captured parameters similar to closure variables. They exist for the lifetime of the instance and can be accessed anywhere in the class, but they're not properties or fields.

Practice Exercise

Create a BankAccount class using primary constructor that:

  1. Takes accountNumber and initialBalance as parameters
  2. Exposes AccountNumber as a public readonly property
  3. Keeps _balance as a private field initialized from initialBalance
  4. Has an additional constructor that takes only accountNumber (defaults balance to 0)
  5. Implements Deposit and Withdraw methods