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!