Constructor
What is a Constructor?
A constructor is a special method that gets called automatically when you create a new instance of a class. Think of it as the “initialization code” that runs every time you use the new keyword. Constructors are responsible for setting up your object in a valid, usable state.
Here’s the simplest example:
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
// Constructor
public Product(string name, decimal price)
{
Name = name;
Price = price;
}
}
// Using the constructor
var product = new Product("Laptop", 999.99m);
Notice how the constructor has the same name as the class and no return type - that’s how C# identifies it as a constructor.
Why Are Constructors Important?
Constructors solve a fundamental problem: ensuring your objects start life in a valid state. Without constructors, you’d have to remember to manually initialize every property after creating an object, which is error-prone and messy.
The Problem Without Constructors
public class User
{
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
}
// Without constructor - easy to forget initialization
var user = new User();
// Oops! Email is null, CreatedAt is default(DateTime)
// We have to remember to set these manually
user.Email = "john@example.com";
user.CreatedAt = DateTime.UtcNow;
The Solution With Constructors
public class User
{
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
public User(string email)
{
Email = email ?? throw new ArgumentNullException(nameof(email));
CreatedAt = DateTime.UtcNow;
}
}
// With constructor - guaranteed valid state
var user = new User("john@example.com");
// Email and CreatedAt are automatically set correctly
Types of Constructors
Default Constructor
If you don’t define any constructors, C# provides a parameterless default constructor:
public class SimpleClass
{
public string Name { get; set; }
// Implicit default constructor exists
}
// Can create with default constructor
var obj = new SimpleClass(); // Name will be null
But if you define any constructor, the default one disappears:
public class Product
{
public string Name { get; set; }
public Product(string name)
{
Name = name;
}
// No default constructor anymore!
}
// This won't compile:
// var product = new Product(); // Error!
// Must use the defined constructor:
var product = new Product("Laptop");
Parameterless Constructor
You can explicitly define a parameterless constructor:
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
// Explicit parameterless constructor
public Product()
{
Name = "Unknown Product";
Price = 0m;
}
public Product(string name, decimal price)
{
Name = name;
Price = price;
}
}
// Both work now
var product1 = new Product();
var product2 = new Product("Laptop", 999.99m);
Constructor Overloading
You can have multiple constructors with different parameters:
public class Customer
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
// Constructor with just email
public Customer(string email)
{
Email = email ?? throw new ArgumentNullException(nameof(email));
CreatedAt = DateTime.UtcNow;
}
// Constructor with full details
public Customer(string firstName, string lastName, string email)
{
FirstName = firstName ?? throw new ArgumentNullException(nameof(firstName));
LastName = lastName ?? throw new ArgumentNullException(nameof(lastName));
Email = email ?? throw new ArgumentNullException(nameof(email));
CreatedAt = DateTime.UtcNow;
}
}
// Can use either constructor
var customer1 = new Customer("john@example.com");
var customer2 = new Customer("John", "Doe", "john@example.com");
Constructor Chaining
You can have one constructor call another using the this keyword:
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
// Main constructor
public Product(string name, decimal price, string category)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Price = price;
Category = category ?? "General";
}
// Simplified constructor that chains to the main one
public Product(string name, decimal price) : this(name, price, "General")
{
// Additional initialization if needed
}
// Even simpler constructor
public Product(string name) : this(name, 0m, "General")
{
// Additional initialization if needed
}
}
This prevents code duplication and ensures consistent initialization logic.
Constructors in API Development
Entity Classes
In API development, constructors are crucial for entity classes:
public class Order
{
public Guid Id { get; private set; }
public string CustomerEmail { get; private set; }
public List<OrderItem> Items { get; private set; }
public DateTime CreatedAt { get; private set; }
public OrderStatus Status { get; private set; }
public Order(string customerEmail)
{
Id = Guid.NewGuid();
CustomerEmail = customerEmail ?? throw new ArgumentNullException(nameof(customerEmail));
Items = new List<OrderItem>();
CreatedAt = DateTime.UtcNow;
Status = OrderStatus.Pending;
}
// Private parameterless constructor for EF Core
private Order() { }
}
Value Objects
Constructors are essential for creating immutable value objects:
public class Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
if (amount < 0)
throw new ArgumentException("Amount cannot be negative", nameof(amount));
if (string.IsNullOrWhiteSpace(currency))
throw new ArgumentException("Currency is required", nameof(currency));
Amount = amount;
Currency = currency.ToUpperInvariant();
}
}
// Immutable - values set at construction time
var price = new Money(29.99m, "USD");
Request/Response DTOs
Constructors help create clean DTOs:
public class CreateProductRequest
{
public string Name { get; }
public decimal Price { get; }
public string Category { get; }
public CreateProductRequest(string name, decimal price, string category)
{
Name = name?.Trim() ?? throw new ArgumentNullException(nameof(name));
Price = price;
Category = category?.Trim() ?? throw new ArgumentNullException(nameof(category));
if (Price <= 0)
throw new ArgumentException("Price must be positive", nameof(price));
}
}
Dependency Injection and Constructors
Modern .NET APIs heavily use constructor injection:
public class ProductService
{
private readonly IProductRepository _repository;
private readonly ILogger<ProductService> _logger;
private readonly IMapper _mapper;
public ProductService(
IProductRepository repository,
ILogger<ProductService> logger,
IMapper mapper)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
public async Task<ProductDto> CreateProductAsync(CreateProductRequest request)
{
_logger.LogInformation("Creating product: {ProductName}", request.Name);
var product = new Product(request.Name, request.Price, request.Category);
var savedProduct = await _repository.SaveAsync(product);
return _mapper.Map<ProductDto>(savedProduct);
}
}
Static Constructors
Static constructors initialize static members and run once per application domain:
public class ApiConfiguration
{
public static string DatabaseConnectionString { get; private set; }
public static int MaxRetryAttempts { get; private set; }
// Static constructor - runs once when the class is first accessed
static ApiConfiguration()
{
DatabaseConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION")
?? "DefaultConnection";
MaxRetryAttempts = 3;
Console.WriteLine("API Configuration initialized");
}
}
Primary Constructors (C# 12+)
C# 12 introduced primary constructors for classes:
// Primary constructor syntax
public class ProductService(IProductRepository repository, ILogger<ProductService> logger)
{
// Parameters automatically become private fields
public async Task<Product> GetProductAsync(Guid id)
{
logger.LogInformation("Retrieving product {ProductId}", id);
return await repository.GetByIdAsync(id);
}
}
// Equivalent to traditional constructor:
public class ProductService
{
private readonly IProductRepository repository;
private readonly ILogger<ProductService> logger;
public ProductService(IProductRepository repository, ILogger<ProductService> logger)
{
this.repository = repository;
this.logger = logger;
}
}
Best Practices
1. Validate Parameters
Always validate constructor parameters:
public class User
{
public string Email { get; }
public string Name { get; }
public User(string email, string name)
{
Email = email?.Trim() ?? throw new ArgumentNullException(nameof(email));
Name = name?.Trim() ?? throw new ArgumentNullException(nameof(name));
if (!IsValidEmail(Email))
throw new ArgumentException("Invalid email format", nameof(email));
if (string.IsNullOrWhiteSpace(Name))
throw new ArgumentException("Name cannot be empty", nameof(name));
}
private static bool IsValidEmail(string email)
{
// Email validation logic
return email.Contains("@");
}
}
2. Keep Constructors Simple
Avoid heavy work in constructors:
// Bad - doing I/O in constructor
public class UserService
{
public UserService(string configFilePath)
{
// Don't do this - file I/O in constructor
var config = File.ReadAllText(configFilePath);
// ...
}
}
// Good - accept dependencies
public class UserService
{
public UserService(IConfiguration configuration, IUserRepository repository)
{
// Simple assignment only
Configuration = configuration;
Repository = repository;
}
}
3. Use Constructor Chaining
Avoid duplicating initialization logic:
public class Product
{
public string Name { get; }
public decimal Price { get; }
public DateTime CreatedAt { get; }
// Main constructor
public Product(string name, decimal price, DateTime? createdAt = null)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Price = price;
CreatedAt = createdAt ?? DateTime.UtcNow;
}
// Convenience constructor
public Product(string name, decimal price) : this(name, price, null)
{
}
}
4. Consider Factory Methods for Complex Construction
Sometimes static factory methods are clearer than constructors:
public class ApiKey
{
public string Value { get; private set; }
public DateTime ExpiresAt { get; private set; }
private ApiKey(string value, DateTime expiresAt)
{
Value = value;
ExpiresAt = expiresAt;
}
// Factory methods are more descriptive
public static ApiKey CreateTemporary(string value)
{
return new ApiKey(value, DateTime.UtcNow.AddDays(1));
}
public static ApiKey CreatePermanent(string value)
{
return new ApiKey(value, DateTime.MaxValue);
}
public static ApiKey CreateWithExpiry(string value, TimeSpan duration)
{
return new ApiKey(value, DateTime.UtcNow.Add(duration));
}
}
// Usage is more self-documenting
var tempKey = ApiKey.CreateTemporary("abc123");
var permKey = ApiKey.CreatePermanent("xyz789");
var customKey = ApiKey.CreateWithExpiry("def456", TimeSpan.FromHours(6));
Common Pitfalls
1. Forgetting Validation
// Bad - no validation
public class Product
{
public Product(string name, decimal price)
{
Name = name; // Could be null!
Price = price; // Could be negative!
}
}
// Good - proper validation
public class Product
{
public Product(string name, decimal price)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
if (price < 0)
throw new ArgumentException("Price cannot be negative", nameof(price));
Price = price;
}
}
2. Too Many Constructor Parameters
// Bad - too many parameters
public class User(string first, string last, string email, string phone,
string address, string city, string state, string zip,
DateTime birth, string ssn, string company)
{
// Hard to use and maintain
}
// Good - use builder pattern or break into smaller objects
public class User
{
public User(string email, PersonalInfo personal, ContactInfo contact)
{
// Much cleaner
}
}
Wrap Up
Constructors are the gatekeepers of your objects - they ensure that every instance starts life in a valid, predictable state. In API development, well-designed constructors are crucial for creating robust entities, DTOs, and services.
The key principles are:
- Validate everything - don’t accept invalid data
- Keep it simple - constructors should set up state, not do work
- Use chaining - avoid duplicating initialization logic
- Consider alternatives - sometimes factory methods are clearer
Master constructors, and you’ll write more reliable, maintainable code that’s harder to use incorrectly. Your future self (and your teammates) will thank you for it!