Skip to main content
ANVISoftware Solutions
Lesson 23 of 62Intermediate16 min

Constructors

By the end of this lesson

Guarantee an object is valid the moment it is created.

In the previous lesson you created an Employee like this: new Employee { Name = "Asha", Role = "Engineer" }. Nothing stopped you leaving the name out. Nothing stopped you setting a salary of minus four hundred.

A constructor closes that gap. It is a block of code that runs once, when an object is created, and it is the only moment at which you can be certain nobody else has touched the object yet. That makes it the right place to state the rules a valid object must obey.

An Employee that cannot exist in an invalid state
C#
public class Employee
{
    public string Name { get; }
    public string Role { get; }
    public decimal AnnualSalary { get; }

    public Employee(string name, string role, decimal annualSalary)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            throw new ArgumentException("An employee must have a name.", nameof(name));
        }

        if (annualSalary <= 0m)
        {
            throw new ArgumentOutOfRangeException(nameof(annualSalary), "Salary must be above zero.");
        }

        Name = name;
        Role = role;
        AnnualSalary = annualSalary;
    }
}

Employee asha = new Employee("Asha", "Engineer", 1_200_000m);   // fine
Employee broken = new Employee("", "Engineer", 0m);             // throws immediately
  • A constructor has the same name as the class and no return type. That is how the compiler recognises it.
  • Each property is declared { get; } with no set. A get-only property can be assigned inside the constructor and nowhere else, so once the object exists its values cannot change.
  • The two if blocks are the invariants, written down as code. If either fails, the constructor throws and no object comes back.
  • nameof(name) produces the text "name". It tells the caller which argument was wrong, and it keeps working if you later rename the parameter.
  • The last line never produces an Employee. There is no partially built object left behind to worry about.

Two details that catch people out. If you write no constructor at all, C# supplies an invisible one that takes no arguments — that is why new Employee { ... } worked before. The moment you declare any constructor of your own, that free one disappears. new Employee() no longer compiles, which is exactly what you want here.

And a constructor is allowed to throw. An exception from a constructor means creation failed, so the caller never receives a reference. There is no half-built object in the wild.

Several ways in, one set of rules
C#
public class Invoice
{
    public string Reference { get; }
    public decimal Amount { get; }
    public DateOnly DueDate { get; }

    // Convenience: most invoices are due in 30 days.
    public Invoice(string reference, decimal amount)
        : this(reference, amount, DateOnly.FromDateTime(DateTime.Today).AddDays(30))
    {
    }

    // The one constructor that actually does the work and the checking.
    public Invoice(string reference, decimal amount, DateOnly dueDate)
    {
        if (string.IsNullOrWhiteSpace(reference))
        {
            throw new ArgumentException("An invoice needs a reference.", nameof(reference));
        }

        if (amount <= 0m)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "An invoice must be for a positive amount.");
        }

        Reference = reference;
        Amount = amount;
        DueDate = dueDate;
    }
}
  • Two constructors with different parameter lists is called overloading. The compiler picks the one that matches the arguments you passed.
  • : this(...) after the parameter list means "run that other constructor first, then my body". Here the short constructor fills in a default due date and hands over.
  • The important consequence: the validation exists once. Add a third convenience constructor next year and it still cannot skip the checks.
  • DateOnly holds a date with no time component, which is the right type for a due date.

Summary

  • A constructor runs once at creation and is the only moment when no other code has touched the object
  • An invariant is a rule that holds for an object's whole life; checking it in the constructor means callers do not have to
  • Declaring any constructor removes the implicit parameterless one
  • Get-only or private-set properties are what keep a constructor's guarantee true afterwards
  • Chain overloads with : this(...) so validation is written once

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Try it yourself

Write a Product class with a name, a unit price and a quantity in stock. It must be impossible to create a product with a blank name, a negative price, or negative stock.

Then try to create an invalid one and read the exception message. Does it tell you which value was wrong?

Show solution

Each rule becomes one check at the top of the constructor. Making the properties get-only is the half people skip, and without it the guarantee lasts exactly until the next line of calling code.

Note that quantity of zero is allowed while price of zero is not. That is a judgement about the domain, not about C#: a product can be out of stock, but a price of nothing is almost certainly a data error. Writing the constructor forces you to make that decision on purpose.

C#
public class Product
{
    public string Name { get; }
    public decimal UnitPrice { get; }
    public int QuantityInStock { get; }

    public Product(string name, decimal unitPrice, int quantityInStock)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            throw new ArgumentException("A product must have a name.", nameof(name));
        }

        if (unitPrice <= 0m)
        {
            throw new ArgumentOutOfRangeException(nameof(unitPrice), "Price must be above zero.");
        }

        if (quantityInStock < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(quantityInStock), "Stock cannot be negative.");
        }

        Name = name;
        UnitPrice = unitPrice;
        QuantityInStock = quantityInStock;
    }
}

Think about it

Think about it

An alternative design lets any Employee be created, then offers a bool IsValid property that callers check before using it.

What does every piece of code that touches an Employee now have to do, and what happens the first time someone forgets?

Show solution

Every caller has to check IsValid, which means the check is repeated everywhere and can be omitted anywhere. The compiler cannot help, because an invalid Employee is a perfectly ordinary Employee as far as the type system is concerned.

When someone forgets, the invalid object travels onward and fails somewhere distant — in a payroll report, or in a database write. The stack trace points at the symptom, not at the code that created the bad object.

A constructor that refuses to build an invalid object moves the failure to the moment and the line where the mistake was made. That is the real argument: not tidiness, but shorter distance between cause and symptom.

Knowledge check

Nothing is recorded and there is no score. The explanation appears either way.

You add a constructor Employee(string name, string role) to a class that previously had none. What happens to existing code that calls new Employee()?

Saved in this browser only.