Object-Oriented C# · Lesson 11

Constructor invariants that hold from the start

Create domain objects only when their required state is valid.

How it works

A constructor establishes the initial state of an object. If a type promises that a quantity is nonnegative or an identifier is nonempty, accepting an invalid initial value breaks that promise before any method can help preserve it. Validating at construction gives later behavior a smaller set of states to reason about.

An invariant is a rule that should hold whenever the object is available for normal use. Constructor validation handles only the first moment; later mutation must preserve the same rule. A get-only property is one way to prevent arbitrary replacement after creation. Choose a focused domain rule rather than adding speculative checks that the application has never required.

Walk through the example. StockLevel accepts a nonnegative quantity and exposes it through a get-only property. Constructing a level of five succeeds and prints five. Constructing a level of negative one throws ArgumentOutOfRangeException, which the sample catches as an invalid-construction demonstration. There is no partially valid StockLevel returned to the caller, and no public setter can later replace the quantity with a negative value.

Watch the boundary. A constructor should not call overridable behavior that depends on a derived object's not-yet-initialized state. It should also avoid unrelated external work when a simple value construction is expected. Keep validation failures distinguishable from environmental failures. Throwing for a violated constructor contract is different from using exceptions as the routine path for every user-input parse failure.

Practice independently. Design a ProductCode value object that rejects empty or whitespace-only codes and retains the accepted spelling. Test a valid code and each invalid category. Then decide whether trimming is part of the type's contract or an ingestion step before construction. Add a quantity-changing operation to StockLevel and prove that the same nonnegative invariant is preserved after every successful operation.

A working example

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

var level = new StockLevel(5);
Console.WriteLine(level.Quantity);
try { _ = new StockLevel(-1); }
catch (ArgumentOutOfRangeException) { Console.WriteLine("invalid"); }
sealed class StockLevel
{
    public int Quantity { get; }
    public StockLevel(int quantity)
    {
        if (quantity < 0) throw new ArgumentOutOfRangeException(nameof(quantity));
        Quantity = quantity;
    }
}

Expected output

5
invalid
Try this
Establish invariants in the constructor, then prevent later operations from violating them.