Constructors — Initialising Objects Correctly

A constructor is a special method that runs when an object is created with new. Its job is to ensure the object is always in a valid, usable state from the moment it exists — no half-constructed objects. Without a constructor, callers must remember to set required properties after creation, which is error-prone. With a constructor that requires the mandatory data, an object can never exist in an invalid state. This invariant enforcement is the core principle of good OOP design.

Parameterless and Parameterised Constructors

public class Order
{
    // ── Properties ─────────────────────────────────────────────────────────
    public int      OrderId     { get; }
    public string   CustomerId  { get; }
    public DateTime OrderDate   { get; }
    public List<OrderItem> Items { get; } = new();

    // ── Parameterised constructor — required data enforced ─────────────────
    public Order(int orderId, string customerId)
    {
        if (orderId <= 0)
            throw new ArgumentException("OrderId must be positive.", nameof(orderId));
        if (string.IsNullOrWhiteSpace(customerId))
            throw new ArgumentException("CustomerId is required.", nameof(customerId));

        OrderId    = orderId;
        CustomerId = customerId.Trim();
        OrderDate  = DateTime.UtcNow;
    }

    // ── Parameterless constructor for EF Core / model binding ──────────────
    // EF Core requires a parameterless constructor to materialise objects
    // Mark private or protected to discourage external use
    private Order() { }   // EF Core uses reflection to call this

    public void AddItem(OrderItem item)
    {
        ArgumentNullException.ThrowIfNull(item);
        Items.Add(item);
    }
}

// Creating instances
var order = new Order(orderId: 42, customerId: "CUST-001");
Console.WriteLine(order.OrderDate);  // current UTC time
// var bad = new Order();   // ← cannot call private parameterless constructor
Note: Entity Framework Core needs to be able to create instances when reading data from the database. It prefers a parameterless constructor, but can also use a parameterised constructor if the parameter names match EF Core’s conventions. The cleanest approach for domain entities is: a public parameterised constructor that enforces all required fields, plus a private or protected parameterless constructor solely for EF Core. This keeps the domain logic intact while satisfying the ORM.
Tip: Use constructor chaining with this() to avoid duplicating initialisation logic across multiple constructors. The more specific constructor does the real work; simpler constructors call it with sensible defaults: public Order(string customerId) : this(GenerateId(), customerId) { }. This ensures all validation and field assignment happens in one place. If you add a new required field later, you change only the most complete constructor.
Warning: Do not perform async operations, call external services, or perform heavy I/O in constructors. Constructors are synchronous and cannot be awaited. In ASP.NET Core, database calls, file reads, and HTTP requests must happen in service methods that are called after construction — typically in dedicated InitialiseAsync() methods or inside the controller action itself. ASP.NET Core’s dependency injection creates service instances through constructors, so expensive work in constructors hurts startup time and makes testing difficult.

Constructor Chaining with this()

public class BlogPost
{
    public int      Id        { get; }
    public string   Title     { get; }
    public string   Body      { get; }
    public string   AuthorId  { get; }
    public DateTime CreatedAt { get; }

    // ── Most complete constructor — all validation here ───────────────────
    public BlogPost(int id, string title, string body, string authorId, DateTime createdAt)
    {
        if (string.IsNullOrWhiteSpace(title))
            throw new ArgumentException("Title is required.");
        if (string.IsNullOrWhiteSpace(authorId))
            throw new ArgumentException("AuthorId is required.");

        Id        = id;
        Title     = title.Trim();
        Body      = body?.Trim() ?? string.Empty;
        AuthorId  = authorId;
        CreatedAt = createdAt;
    }

    // ── Convenience constructor — chains to the full one ──────────────────
    public BlogPost(int id, string title, string body, string authorId)
        : this(id, title, body, authorId, DateTime.UtcNow)  // calls above constructor
    { }

    // ── Minimal constructor (for new posts) ───────────────────────────────
    public BlogPost(string title, string authorId)
        : this(0, title, string.Empty, authorId)  // chains again
    { }
}

// All three constructors work; validation runs in all cases
var p1 = new BlogPost(1, "Hello", "Body", "auth-1", new DateTime(2025, 1, 1));
var p2 = new BlogPost(2, "Hello", "Body", "auth-1");    // uses UtcNow
var p3 = new BlogPost("Hello", "auth-1");               // minimal — id=0, empty body

Primary Constructors (C# 12)

// Primary constructor — parameters defined on the class declaration itself
// Perfect for simple data containers and services with injected dependencies
public class EmailService(ISmtpClient smtpClient, ILogger<EmailService> logger)
{
    // smtpClient and logger are available as fields throughout the class
    public async Task SendAsync(string to, string subject, string body)
    {
        logger.LogInformation("Sending email to {To}", to);
        await smtpClient.SendAsync(to, subject, body);
    }
}

// ── This replaces the boilerplate constructor pattern: ────────────────────────
// private readonly ISmtpClient _smtpClient;
// private readonly ILogger<EmailService> _logger;
// public EmailService(ISmtpClient smtpClient, ILogger<EmailService> logger)
// {
//     _smtpClient = smtpClient;
//     _logger     = logger;
// }
// The primary constructor eliminates this entirely for simple injection scenarios

Common Mistakes

Mistake 1 — Doing async work in a constructor

❌ Wrong — constructors cannot be awaited:

public UserService(IUserRepository repo)
{
    _users = repo.GetAllAsync().Result;  // blocks the thread — deadlock risk!
}

✅ Correct — load data in a separate async method, not the constructor.

Mistake 2 — Not providing parameterless constructor for EF Core entities

EF Core cannot materialise objects if there is no accessible parameterless constructor. Either add a private parameterless constructor or use EF Core 7+ constructor injection by matching parameter names to property names.

🧠 Test Yourself

Why should the most validation-heavy constructor be the one that all others delegate to with : this(), rather than putting validation in the simpler ones?