Skip to content

C# Constructors — Default, Parameterized, Static, and Primary Constructors

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about C# Constructors. We cover key concepts, practical examples, and best practices to help you master this topic.

C# constructors are special methods that initialize objects when they are created, supporting default constructors, parameterized constructors with validation, static constructors for type initialization, and primary constructors introduced in C# 12.

What You'll Learn

You will master every constructor pattern in C#: default constructors that the compiler provides automatically, parameterized constructors for controlled initialization, constructor chaining with this() and base(), static constructors for one-time type setup, and primary constructors introduced in C# 12 for concise class definitions.

Why It Matters

Proper constructor design ensures objects are always created in a valid state. Understanding constructor chaining prevents code duplication. Static constructors are essential for initializing static state in .NET libraries. Primary constructors, new in C# 12, reduce boilerplate significantly for simple types and are widely adopted in modern C# code.

Real-World Use

Dependency injection containers call parameterized constructors to resolve service dependencies. Entity Framework Core entities require parameterless constructors for materialization. Configuration classes use primary constructors for concise option definitions. Singleton patterns use static constructors for thread-safe initialization.

Learning Path

graph LR
    A["09: Classes"] --> B["10: Constructors"]
    B --> C["11: Encapsulation"]
    C --> D["12: Inheritance"]
    D --> E["13: Polymorphism"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style E fill:#4a90d9,stroke:#2c5f8a,color:#fff

Default Constructor

If you do not define any constructor, the compiler generates a parameterless default constructor that initializes fields to their default values:

public class Person
{
    public string Name { get; set; }  // null
    public int Age { get; set; }       // 0
}

// Compiler-generated default constructor equivalent:
// public Person() { }

var p = new Person();
Console.WriteLine($"Name: '{p.Name}', Age: {p.Age}");
// Output: Name: '', Age: 0

Once you define any constructor, the compiler no longer generates a default constructor:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person(string name)
    {
        Name = name;
    }
}

// var p = new Person();  // Error: no parameterless constructor
var p = new Person("Alice");

Parameterized Constructors

public class BankAccount
{
    public string AccountNumber { get; }
    public decimal Balance { get; private set; }
    public DateTime CreatedAt { get; }

    public BankAccount(string accountNumber, decimal initialDeposit)
    {
        if (string.IsNullOrWhiteSpace(accountNumber))
            throw new ArgumentException("Account number is required", nameof(accountNumber));
        if (initialDeposit < 0)
            throw new ArgumentException("Initial deposit cannot be negative", nameof(initialDeposit));

        AccountNumber = accountNumber;
        Balance = initialDeposit;
        CreatedAt = DateTime.UtcNow;
    }

    // Constructor with default initial deposit
    public BankAccount(string accountNumber) : this(accountNumber, 0)
    {
    }
}

Constructor Chaining (this)

The this keyword chains to another constructor in the same class:

public class Order
{
    public int OrderId { get; }
    public List<string> Items { get; }
    public decimal Total { get; }
    public DateTime CreatedAt { get; }

    // Primary constructor with full parameters
    public Order(int orderId, List<string> items, decimal total)
    {
        OrderId = orderId;
        Items = items ?? new List<string>();
        Total = total;
        CreatedAt = DateTime.UtcNow;
    }

    // Chained: generates order ID automatically
    private static int _nextId = 1;
    public Order(List<string> items, decimal total)
        : this(_nextId++, items, total)
    {
    }

    // Chained: empty order
    public Order() : this(new List<string>(), 0)
    {
    }
}

Static Constructor

A static constructor runs once per type, before any instance is created or any static member is accessed:

public class DatabaseConfig
{
    public static string ConnectionString { get; }
    public static int MaxRetries { get; }
    public static readonly DateTime LoadedAt;

    // Static constructor
    static DatabaseConfig()
    {
        Console.WriteLine("Static constructor running...");
        ConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION")
            ?? "Server=localhost;Database=app;Trusted_Connection=true";
        MaxRetries = 3;
        LoadedAt = DateTime.UtcNow;
    }
}

Console.WriteLine(DatabaseConfig.ConnectionString);
// Static constructor runs ONCE before this line

Key characteristics:

  • No access modifiers (always implicitly private)
  • Cannot have parameters
  • Cannot be called explicitly
  • Runs exactly once per type, thread-safely
  • If a static constructor throws, the type is unusable for the lifetime of the app domain

Primary Constructors (C# 12)

Primary constructors allow defining constructor parameters directly on the class declaration:

// Traditional approach
public class Point
{
    public double X { get; }
    public double Y { get; }
    public Point(double x, double y)
    {
        X = x;
        Y = y;
    }
}

// Primary constructor (C# 12)
public class Point(double x, double y)
{
    public double X { get; } = x;
    public double Y { get; } = y;
    public double DistanceFromOrigin => Math.Sqrt(x * x + y * y);
}

Primary Constructor with Dependency Injection

// Typical ASP.NET Core pattern with primary constructors
public class UserService(ILogger<UserService> logger, IUserRepository repository)
{
    public async Task<User?> GetUser(int id)
    {
        logger.LogInformation("Getting user {Id}", id);
        return await repository.GetByIdAsync(id);
    }
}

Validation in Primary Constructors

public class Temperature(double celsius)
{
    public double Celsius { get; } =
        celsius < -273.15
            ? throw new ArgumentException("Below absolute zero")
            : celsius;

    public double Fahrenheit => Celsius * 9 / 5 + 32;
}

Private Constructors

Used in Factory patterns and singleton implementations:

public class Logger
{
    private static Logger? _instance;
    private static readonly object _lock = new();

    // Private constructor prevents external instantiation
    private Logger()
    {
    }

    public static Logger Instance
    {
        get
        {
            if (_instance == null)
            {
                lock (_lock)
                {
                    _instance ??= new Logger();
                }
            }
            return _instance;
        }
    }

    public void Log(string message) => Console.WriteLine($"[LOG] {message}");
}

Constructor with Optional Parameters

public class EmailMessage
{
    public string To { get; }
    public string Subject { get; }
    public string Body { get; }
    public bool IsHtml { get; }
    public int Priority { get; }

    public EmailMessage(
        string to,
        string subject,
        string body,
        bool isHtml = false,
        int priority = 0)
    {
        To = to;
        Subject = subject;
        Body = body;
        IsHtml = isHtml;
        Priority = priority;
    }
}

// Various ways to call:
var msg1 = new EmailMessage("a@b.com", "Hello", "Body");
var msg2 = new EmailMessage("a@b.com", "Alert", "Body", isHtml: true, priority: 1);

Constructor Initialization Order

public class InitializationDemo
{
    // 1. Static field initializers run first
    private static int _staticField = GetStaticValue();

    // 2. Instance field initializers run before constructor body
    private int _instanceField = GetInstanceValue();

    private static int GetStaticValue()
    {
        Console.WriteLine("Static field initializer");
        return 1;
    }

    private int GetInstanceValue()
    {
        Console.WriteLine("Instance field initializer");
        return 1;
    }

    public InitializationDemo()
    {
        Console.WriteLine("Constructor body");
    }
}

new InitializationDemo();
// Output:
// Static field initializer
// Instance field initializer
// Constructor body

Common Mistakes

Mistake 1: Forgetting to Chain Constructors

Duplicating initialization logic across multiple constructors leads to maintenance issues. Use : this(...) to chain to a primary constructor.

Mistake 2: Calling Virtual Methods in Constructors

Calling virtual methods from a constructor can cause bugs because the derived class constructor has not executed yet. The method may operate on uninitialized state.

Mistake 3: Static Constructor Exceptions

If a static constructor throws, the type is permanently unusable. Keep static constructors simple and avoid throwing exceptions.

Mistake 4: Not Validating Parameters

Constructors should validate parameters to ensure the object is created in a valid state. Throw ArgumentException for invalid input.

Mistake 5: Confusing Primary Constructor Parameters with Fields

Primary constructor parameters are not fields. They are available throughout the class, but they are not automatically exposed as properties. You must explicitly create properties if needed.

Mistake 6: Providing a Public Parameterless Constructor That Creates Invalid Objects

If your class requires certain data to be functional, do not expose a parameterless constructor, or ensure it provides sensible defaults.

Practice Questions

  1. When would you use a static constructor instead of a static field initializer?
  2. What is constructor chaining and why is it useful?
  3. How do primary constructors differ from traditional constructors in C# 12?
  4. Why should constructors not call virtual methods?
  5. Write a class with three constructors chained to a primary constructor.

Challenge

Create a TimeBlock class using C# 12 primary constructor syntax that represents a block of time with Start and End DateTime properties. Validate that End is after Start in the primary constructor. Include a computed Duration property.

FAQ

Can I have both a primary constructor and additional constructors?

Yes. A class can have a primary constructor and additional constructors that chain to it using : this(...). The primary constructor is always called.

What happens if I do not define any constructor?

The compiler generates a parameterless default constructor that calls the base class parameterless constructor and initializes fields to their defaults.

Can constructors be async?

No, constructors cannot be async. Use a factory method pattern like public static async Task<MyClass> CreateAsync() for async initialization.

What is the difference between a private constructor and a static class?

A private constructor prevents instantiation but still allows static members. A static class (marked static) can only have static members and cannot be instantiated or used as a type parameter.

Do primary constructor parameters become public?

No. Primary constructor parameters are scoped to the class. They do not automatically become properties. You must create properties explicitly if you need public access.

Mini Project

Create a complete Order system demonstrating all constructor types:

public class OrderItem(string product, int quantity, decimal unitPrice)
{
    public string Product { get; } = product;
    public int Quantity { get; } = quantity > 0 ? quantity :
        throw new ArgumentException("Quantity must be positive");
    public decimal UnitPrice { get; } = unitPrice >= 0 ? unitPrice :
        throw new ArgumentException("Price cannot be negative");
    public decimal Total => Quantity * UnitPrice;
}

public class Order
{
    private static int _nextId = 1;
    private static readonly HashSet<string> _promoCodes = new() { "SAVE10", "WELCOME5" };

    public int OrderId { get; }
    public List<OrderItem> Items { get; }
    public DateTime CreatedAt { get; }
    public string? PromoCode { get; }

    // Static constructor
    static Order()
    {
        Console.WriteLine("Order system initialized");
    }

    // Primary constructor
    public Order(List<OrderItem> items, string? promoCode = null)
        : this(_nextId++, items)
    {
        PromoCode = promoCode;
    }

    // Private constructor for ID generation
    private Order(int id, List<OrderItem> items)
    {
        OrderId = id;
        Items = items;
        CreatedAt = DateTime.UtcNow;
    }

    public decimal Subtotal => Items.Sum(i => i.Total);
    public decimal Discount => PromoCode switch
    {
        "SAVE10" => Subtotal * 0.10m,
        "WELCOME5" => Subtotal * 0.05m,
        _ => 0
    };
    public decimal Total => Subtotal - Discount;
}

var items = new List<OrderItem>
{
    new("Widget", 3, 10.99m),
    new("Gadget", 1, 29.99m),
    new("Doohickey", 5, 3.49m)
};

var order = new Order(items, "SAVE10");
Console.WriteLine($"Order #{order.OrderId}");
Console.WriteLine($"Created: {order.CreatedAt:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"Items: {order.Items.Count}");
Console.WriteLine($"Subtotal: {order.Subtotal:C}");
Console.WriteLine($"Discount: {order.Discount:C}");
Console.WriteLine($"Total: {order.Total:C}");

Expected output:

Order system initialized
Order #1
Created: 2026-06-28 12:00:00
Items: 3
Subtotal: $82.31
Discount: $8.23
Total: $74.08

What's Next

You have mastered all constructor patterns in C#. The next lesson covers Encapsulation: access modifiers (public, private, internal, protected) and best practices for data hiding.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro