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()orbase()is called before the constructor body executes.
Common Interview Questions
-
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.
-
What is constructor chaining?
- Calling one constructor from another using
this()for same class orbase()for parent class. Executes the chained constructor first, then the current constructor body.
- Calling one constructor from another using
-
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.
-
Can you have multiple static constructors?
- No, only one static constructor per class, and it cannot be overloaded (no parameters allowed).
-
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.
-
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.
-
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.
- Use