Skip to main content

C# Primary Constructor: Complete Guide with Examples

Learn how to use C# primary constructor to simplify class definitions. Practical examples, best practices, and tips for cleaner C# code. Start now!

What Is a C# Primary Constructor?

A C# primary constructor lets you declare constructor parameters directly in the class or struct declaration, eliminating boilerplate code that developers have written for decades. Introduced in C# 12 (with .NET 8), primary constructors for classes and structs build on a feature that records have enjoyed since C# 9.

If you have ever written a class with a constructor that does nothing but assign parameters to fields, you already know the pain. A typical C# class constructor requires you to declare private fields, write a constructor method, and assign each parameter — often tripling the lines of code for a simple data holder. Primary constructors in C# fix this by letting you define parameters right where you define the class.

Why Primary Constructors Matter in Modern C#

Before C# 12, even a simple service class required verbose ceremony. Consider a logging service that depends on two other services:

// Traditional C# class constructor — before C# 12
public class OrderService
{
    private readonly ILogger<OrderService> _logger;
    private readonly IOrderRepository _repository;
    private readonly IEmailService _emailService;

    public OrderService(
        ILogger<OrderService> logger,
        IOrderRepository repository,
        IEmailService emailService)
    {
        _logger = logger;
        _repository = repository;
        _emailService = emailService;
    }

    public async Task PlaceOrderAsync(Order order)
    {
        _logger.LogInformation("Placing order {OrderId}", order.Id);
        await _repository.SaveAsync(order);
        await _emailService.SendConfirmationAsync(order);
    }
}

That is 20 lines before any business logic appears. Now look at the same class using a primary constructor in C#:

// C# 12 primary constructor — clean and concise
public class OrderService(
    ILogger<OrderService> logger,
    IOrderRepository repository,
    IEmailService emailService)
{
    public async Task PlaceOrderAsync(Order order)
    {
        logger.LogInformation("Placing order {OrderId}", order.Id);
        await repository.SaveAsync(order);
        await emailService.SendConfirmationAsync(order);
    }
}

The result is the same behavior in half the code. The parameters logger, repository, and emailService are captured and available throughout the class body. No private fields, no explicit assignments, no constructor method.

How C# Primary Constructor Works Under the Hood

Understanding the mechanics helps you avoid common pitfalls. When you declare a primary constructor, the compiler does the following:

  • Creates a constructor with the specified parameters on the compiled class.
  • Captures parameters that are used in the class body as compiler-generated private fields.
  • Does NOT generate properties — unlike records, primary constructor parameters in classes remain private captures, not public properties.
  • Parameters unused in the class body are not captured at all, keeping the object lean.

This is a critical distinction from records. In a record, primary constructor parameters automatically become public properties with init setters. In a regular class or struct, they are simply constructor parameters you can reference in your code.

// Record — parameters become public properties automatically
public record Product(string Name, decimal Price);

// Class — parameters are NOT public properties
public class Product(string name, decimal price)
{
    // You must explicitly create properties if you want them
    public string Name { get; } = name;
    public decimal Price { get; } = price;
}

Practical Examples of Primary Constructors in C#

1. Dependency Injection in ASP.NET Core

The most common use case for C# primary constructors is dependency injection in ASP.NET Core controllers and services. This is where the feature truly shines:

// ASP.NET Core controller with primary constructor
[ApiController]
[Route("api/[controller]")]
public class ProductsController(
    IProductService productService,
    ILogger<ProductsController> logger) : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        logger.LogInformation("Fetching all products");
        var products = await productService.GetAllAsync();
        return Ok(products);
    }

    [HttpGet("{id:int}")]
    public async Task<IActionResult> GetById(int id)
    {
        var product = await productService.GetByIdAsync(id);
        return product is null ? NotFound() : Ok(product);
    }
}

2. Initializing Properties and Fields

You can use primary constructor parameters to initialize properties, fields, and other members directly:

public class BankAccount(string accountHolder, decimal initialBalance)
{
    public string AccountHolder { get; } = accountHolder;
    public decimal Balance { get; private set; } = initialBalance;

    // The parameter 'initialBalance' is also captured because
    // it is used in the method below
    public string GetSummary()
        => $"{AccountHolder}: Balance = {Balance:C} (opened with {initialBalance:C})";

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentOutOfRangeException(nameof(amount));
        Balance += amount;
    }
}

3. Struct with Primary Constructor

Primary constructors work with structs too, which is great for lightweight value types:

public struct Coordinate(double latitude, double longitude)
{
    public double Latitude { get; } = latitude;
    public double Longitude { get; } = longitude;

    public double DistanceTo(Coordinate other)
    {
        double dLat = Math.Abs(Latitude - other.Latitude);
        double dLon = Math.Abs(Longitude - other.Longitude);
        return Math.Sqrt(dLat * dLat + dLon * dLon);
    }

    public override string ToString() => $"({Latitude}, {Longitude})";
}

4. Combining with Inheritance

Primary constructors integrate smoothly with class inheritance. The derived class can pass arguments to the base class primary constructor:

public class Animal(string name, int age)
{
    public string Name { get; } = name;
    public int Age { get; } = age;
}

// Derived class passes arguments to the base primary constructor
public class Dog(string name, int age, string breed) : Animal(name, age)
{
    public string Breed { get; } = breed;

    public string Describe()
        => $"{Name} is a {Age}-year-old {Breed}";
}

// Usage
var dog = new Dog("Rex", 5, "German Shepherd");
Console.WriteLine(dog.Describe());
// Output: Rex is a 5-year-old German Shepherd

C# Primary Constructor Best Practices

Adopting primary constructors effectively requires following a few C# constructor best practices. Here are the guidelines experienced developers follow:

Use Primary Constructors for Dependency Injection

Services, controllers, and handlers that receive dependencies through their constructor are the ideal candidates. The parameters are used throughout the class and never need to be publicly exposed:

// Ideal use case — DI services
public class NotificationService(
    IEmailSender emailSender,
    ISmsSender smsSender,
    ILogger<NotificationService> logger)
{
    public async Task NotifyAsync(User user, string message)
    {
        logger.LogInformation("Notifying user {UserId}", user.Id);

        if (user.PrefersEmail)
            await emailSender.SendAsync(user.Email, message);
        else
            await smsSender.SendAsync(user.Phone, message);
    }
}

Avoid Mutating Captured Parameters

Primary constructor parameters are not readonly. You can accidentally reassign them inside the class body. This is a common pitfall:

public class Counter(int initialCount)
{
    // BAD — mutating the captured parameter directly
    public void Increment() => initialCount++;
    public int Count => initialCount;
}

// BETTER — use an explicit field when mutation is needed
public class Counter(int initialCount)
{
    private int _count = initialCount;

    public void Increment() => _count++;
    public int Count => _count;
}

When you need to mutate state, declare an explicit field initialized from the parameter. This makes the intent clear and avoids surprising behavior.

Know When NOT to Use Primary Constructors

Primary constructors are not always the right choice. Avoid them when:

  • Constructor logic is complex — if you need validation, conditional logic, or exception handling in the constructor, a traditional constructor body is clearer.
  • You need multiple constructors — a class can have only one primary constructor. Additional constructors must use this(...) to chain to it.
  • Public API models — if the class is a data transfer object or part of a public API, records with automatic property generation are usually a better fit.

Adding Validation to a Primary Constructor

A frequent question is how to validate parameters when there is no constructor body. You have two clean approaches:

// Approach 1 — Validate via property initializer with a helper method
public class Temperature(double celsius)
{
    public double Celsius { get; } = ValidateCelsius(celsius);

    private static double ValidateCelsius(double value)
        => value < -273.15
            ? throw new ArgumentOutOfRangeException(
                nameof(value), "Temperature cannot be below absolute zero")
            : value;

    public double Fahrenheit => Celsius * 9.0 / 5.0 + 32;
}

// Approach 2 — Chain from a secondary constructor
public class Temperature
{
    public double Celsius { get; }

    public Temperature(double celsius)
    {
        ArgumentOutOfRangeException.ThrowIfLessThan(celsius, -273.15);
        Celsius = celsius;
    }

    public double Fahrenheit => Celsius * 9.0 / 5.0 + 32;
}

Approach 1 keeps the primary constructor syntax. Approach 2 is more conventional when validation is the norm. Choose whichever makes the code more readable for your team.

Primary Constructor vs Record vs Traditional Constructor

Here is a quick comparison to help you choose the right approach:

Feature Primary Constructor (class) Record Traditional Constructor
Auto-generates properties No Yes No
Value equality No Yes No
Constructor body No No Yes
Parameters are readonly No (mutable) Yes (init) Depends on field
Best for DI, service classes DTOs, immutable data Complex initialization
Minimum C# version C# 12 C# 9 All versions

Common Pitfalls to Avoid

Watch out for these mistakes when using primary constructors in C#:

  • Assuming parameters are properties — they are not. External code cannot access obj.parameterName unless you explicitly declare a property.
  • Double capture — if you initialize a property from a parameter AND use that parameter elsewhere in the class, the compiler generates both a property backing field and a separate captured field. Use the property instead of the parameter after initialization.
  • Forgetting this() chaining — any secondary constructor must call the primary constructor using : this(...). The compiler enforces this.
  • Serialization surprises — since primary constructor parameters are not properties, JSON serializers will not pick them up unless you declare explicit properties.
// Double capture pitfall — avoid this
public class UserProfile(string name)
{
    // 'name' is stored in the property backing field
    public string Name { get; } = name;

    // 'name' is ALSO captured separately for use here — wasted memory
    public string Greeting => $"Hello, {name}!";
}

// Fixed — reference the property, not the parameter
public class UserProfile(string name)
{
    public string Name { get; } = name;

    // Uses the property, no double capture
    public string Greeting => $"Hello, {Name}!";
}

Conclusion

The C# primary constructor is one of the most impactful features in C# 12 for reducing boilerplate. It is especially powerful for dependency injection scenarios in ASP.NET Core, where classes routinely accept multiple services through their constructors.

Here are the key takeaways:

  • Use primary constructors for service classes, controllers, and any class where the constructor simply captures dependencies.
  • Remember that parameters are not properties — declare properties explicitly when you need external access.
  • Avoid mutating captured parameters directly; use explicit fields for mutable state.
  • Watch for double capture — reference properties, not parameters, after initialization.
  • Choose the right tool — use records for data, primary constructors for services, and traditional constructors for complex initialization.

Start using primary constructors today in your .NET 8+ projects. They make your code cleaner, easier to read, and faster to write — all without sacrificing any functionality.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...