Constructors and Object Initialization

Master C# constructors including parameterized, static, copy constructors, and modern object initialization patterns

C#
OOP
Constructors
Initialization
18 min read

Overview

Understanding constructor patterns is essential for proper object initialization, dependency injection, and creating well-designed APIs. C# offers multiple approaches for different scenarios.

Core Concepts

Constructor Overloading

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Category { get; set; }
    public bool InStock { get; set; }

    // Multiple constructors for flexibility
    public Product()
    {
        Name = "Unnamed";
        Price = 0;
        Category = "General";
        InStock = false;
    }

    public Product(string name) : this()  // Call default first
    {
        Name = name;
    }

    public Product(string name, decimal price) : this(name)
    {
        Price = price;
    }

    public Product(string name, decimal price, string category, bool inStock = true)
    {
        Name = name;
        Price = price;
        Category = category;
        InStock = inStock;
    }
}

// Usage - multiple ways to create
var p1 = new Product();
var p2 = new Product("Widget");
var p3 = new Product("Gadget", 29.99m);
var p4 = new Product("Tool", 49.99m, "Hardware");
var p5 = new Product("Part", 9.99m, "Supplies", false);

Constructor Chaining

public class Rectangle
{
    public double Width { get; }
    public double Height { get; }
    public string Color { get; }

    // Primary constructor with all parameters
    public Rectangle(double width, double height, string color)
    {
        Width = width > 0 ? width : throw new ArgumentException("Width must be positive");
        Height = height > 0 ? height : throw new ArgumentException("Height must be positive");
        Color = color ?? "Black";
    }

    // Chain to primary - creates square
    public Rectangle(double size) : this(size, size, "Black") { }

    // Chain to primary - with default color
    public Rectangle(double width, double height) : this(width, height, "Black") { }
}

Static Constructors

public class Configuration
{
    // Static readonly fields initialized by static constructor
    public static readonly string Environment;
    public static readonly string ConnectionString;
    public static readonly DateTime StartupTime;

    // Static constructor - runs once before first use
    static Configuration()
    {
        StartupTime = DateTime.UtcNow;
        Environment = System.Environment.GetEnvironmentVariable("ENVIRONMENT") ?? "Development";

        // Load configuration based on environment
        ConnectionString = Environment switch
        {
            "Production" => "Server=prod;Database=app;",
            "Staging" => "Server=staging;Database=app;",
            _ => "Server=localhost;Database=app_dev;"
        };
    }

    // Instance constructor
    public Configuration()
    {
        // Instance initialization
    }
}

// Static constructor runs automatically before first access
Console.WriteLine(Configuration.Environment);  // Triggers static constructor

Primary Constructors (C# 12)

// Traditional class
public class PersonTraditional
{
    private readonly string _name;
    private readonly int _age;

    public PersonTraditional(string name, int age)
    {
        _name = name;
        _age = age;
    }

    public string Name => _name;
    public int Age => _age;
}

// With primary constructor (C# 12)
public class Person(string name, int age)
{
    public string Name => name;
    public int Age => age;

    // Can still have methods using the parameters
    public string GetGreeting() => $"Hello, I'm {name}, {age} years old";
}

// Primary constructor with validation
public class Order(int id, string customerName)
{
    public int Id { get; } = id > 0 ? id : throw new ArgumentException("Invalid ID");
    public string CustomerName { get; } = customerName ?? throw new ArgumentNullException(nameof(customerName));
}

// Inheritance with primary constructors
public class Employee(string name, string department) : Person(name, 0)
{
    public string Department => department;
}

Object and Collection Initializers

public class Employee
{
    public string Name { get; set; }
    public string Department { get; set; }
    public List<string> Skills { get; set; } = new();
    public Address Address { get; set; }
}

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
}

// Object initializer
var employee = new Employee
{
    Name = "Alice",
    Department = "Engineering",
    Address = new Address  // Nested initializer
    {
        Street = "123 Main St",
        City = "Seattle"
    },
    Skills = { "C#", "SQL", "Azure" }  // Collection initializer
};

// Collection initializer
var employees = new List<Employee>
{
    new() { Name = "Alice", Department = "Engineering" },
    new() { Name = "Bob", Department = "Sales" },
    new() { Name = "Charlie", Department = "Engineering" }
};

// Dictionary initializer
var lookup = new Dictionary<string, int>
{
    ["one"] = 1,
    ["two"] = 2,
    ["three"] = 3
};

Required Members and init-only

public class User
{
    // Required - must be set during initialization
    public required string Username { get; init; }
    public required string Email { get; init; }

    // Optional with default
    public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
    public bool IsActive { get; init; } = true;
}

// Must provide required members
var user = new User
{
    Username = "alice",
    Email = "alice@example.com"
};

// Constructor can satisfy required members
public class Order
{
    public required string OrderId { get; init; }
    public required int CustomerId { get; init; }

    [SetsRequiredMembers]
    public Order(string orderId, int customerId)
    {
        OrderId = orderId;
        CustomerId = customerId;
    }

    public Order() { }
}

// Both work
var order1 = new Order("ORD001", 123);
var order2 = new Order { OrderId = "ORD002", CustomerId = 456 };

When to Use vs. When to Avoid

| Scenario | Use It? | Why | |----------|---------|-----| | Required dependencies | ✅ Constructor params | Enforces valid state at creation | | Optional configuration | ✅ Object initializer | Cleaner than many optional params | | Immutable object | ✅ Constructor + readonly | Set once, never changes | | Singleton/static utility | ✅ Private constructor | Prevent external instantiation | | Async initialization | ❌ Constructor | Use static async factory method instead | | Complex validation | ⚠️ Factory method | Keep constructor simple, validate in factory | | Many parameters (>5) | ⚠️ Builder pattern | Too many params is confusing |

Common Patterns

Factory Method

Use when construction requires validation or async operations.

public class Connection
{
    private Connection(string connectionString) { /* ... */ }

    public static Connection Create(string connectionString)
    {
        if (!IsValid(connectionString))
            throw new ArgumentException("Invalid connection string");
        return new Connection(connectionString);
    }

    public static async Task<Connection> CreateAsync(string connectionString)
    {
        var conn = Create(connectionString);
        await conn.OpenAsync();
        return conn;
    }
}

Constructor Chaining

Reduce duplication by chaining to a primary constructor.

public class Product
{
    public string Name { get; }
    public decimal Price { get; }
    public string Category { get; }

    public Product(string name, decimal price, string category)
    {
        Name = name; Price = price; Category = category;
    }

    public Product(string name, decimal price) : this(name, price, "General") { }
    public Product(string name) : this(name, 0, "General") { }
}

Common Mistakes

Mistake: Calling Virtual Methods in Constructor

// ❌ Bad - virtual call in constructor
public class Base
{
    public Base() { Initialize(); }  // Calls derived override!
    protected virtual void Initialize() { }
}

public class Derived : Base
{
    private string _data;
    public Derived() { _data = "initialized"; }
    protected override void Initialize()
    {
        Console.WriteLine(_data.Length);  // NullReferenceException!
    }
}

// ✅ Good - use factory or post-construction initialization
public static Derived Create()
{
    var obj = new Derived();
    obj.Initialize();  // Called after construction
    return obj;
}

Why: The derived constructor hasn't run yet when the base constructor calls the virtual method, so derived fields are still null/default.

Mistake: Throwing in Static Constructor

// ❌ Bad - exception in static constructor
public class Config
{
    static Config()
    {
        // If this throws, the type becomes unusable forever
        ConnectionString = File.ReadAllText("config.txt");
    }
}

// ✅ Good - handle errors gracefully or use lazy initialization
public class Config
{
    private static readonly Lazy<string> _connectionString =
        new(() => TryLoadConfig() ?? "default");
}

Why: A failed static constructor throws TypeInitializationException on every access attempt, making the type permanently broken.

Practical Example

Scenario: Creating a service with dependency injection and validation.

public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly ILogger<OrderService> _logger;
    private readonly OrderServiceOptions _options;

    public OrderService(
        IOrderRepository repository,
        ILogger<OrderService> logger,
        IOptions<OrderServiceOptions> options)
    {
        _repository = repository ?? throw new ArgumentNullException(nameof(repository));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _options = options?.Value ?? throw new ArgumentNullException(nameof(options));

        if (_options.MaxOrderSize <= 0)
            throw new ArgumentException("MaxOrderSize must be positive");
    }

    public async Task<Order> CreateOrderAsync(OrderRequest request)
    {
        _logger.LogInformation("Creating order for {CustomerId}", request.CustomerId);
        // Implementation...
    }
}

Related Topics

  • Dependency Injection: Constructor injection is the primary DI pattern
  • Properties: Init-only and required properties complement constructors
  • Records: Primary constructors are built into record syntax

Interview Tips

Tip 1: Know that if you define any constructor, the default parameterless constructor is no longer auto-generated - you must explicitly define it if needed.

Tip 2: Static constructors cannot have access modifiers or parameters and run exactly once per type.

Tip 3: Constructor chaining with this() or base() is called before the constructor body executes.

Common Interview Questions

  1. What happens if you don't define a constructor?

    • Compiler generates a default parameterless constructor. Once you define any constructor, the default is no longer generated automatically.
  2. What is constructor chaining?

    • Calling one constructor from another using this() for same class or base() for parent class. Executes the chained constructor first, then the current constructor body.
  3. When does a static constructor run?

    • Runs once, automatically, before the first instance is created OR any static members are accessed. Cannot be called directly. Has no parameters.
  4. Can you have multiple static constructors?

    • No, only one static constructor per class, and it cannot be overloaded (no parameters allowed).
  5. What's the difference between a constructor and an object initializer?

    • Constructor: Method that initializes object, can have logic and validation. Object initializer: Syntactic sugar that sets public properties after constructor runs. Constructor executes first, then initializer.
  6. What is a private constructor used for?

    • Prevent instantiation (singleton pattern, static utility classes), force use of factory methods, prevent inheritance. Common in Singleton pattern.
  7. How do you call a parent class constructor?

    • Use base() in the constructor declaration: public Derived(int x) : base(x) { }. Called before the derived constructor body.