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
Establish invariants in the constructor, then prevent later operations from violating them.