Mert Özen Further With Every Line
Backend Object-Oriented Programming in C# — #02

Fields, Properties and Constructors in C#: Building Objects Right

Mert Özen Nov 7, 2026 10 min 2 views
Fields, Properties and Constructors in C#: Building Objects Right

The parameterless constructor that disappears once you write your own, private and readonly fields, a validating property, auto-properties, get-only, private set, init and computed properties, constructor chaining and the order in which initializers run.

Version note: The code in this post uses C# 10 and the .NET 6 APIs; it was built and run with the .NET 10 SDK.

Guess First

I matured the Book from the previous post a little: the fields became properties, and a constructor that takes the title and the price arrived. Then I create the book with a familiar syntax, an object initializer. Does this code compile?

Book dune = new Book { Title = "Dune", Price = 45.90m };
Console.WriteLine(dune.Title);

public class Book
{
    public string Title { get; set; } = "";
    public decimal Price { get; set; }

    public Book(string title, decimal price)
    {
        Title = title;
        Price = price;
    }
}

Both properties are public and both have a set; at first glance there is nothing to object to. The answer is in the next section.

The Constructor That Disappears When You Write One

It does not compile:

Program.cs(1,17): error CS7036: There is no argument given that corresponds to the required parameter 'title' of 'Book.Book(string, decimal)'

Writing new Book { ... } really means new Book() { ... }: first the parameterless constructor is called, then the assignments in the braces run. For a class where you wrote no constructor at all, the compiler adds an empty parameterless one; that is why new Book() worked in the previous post. The moment you write your own constructor, that gift is taken back. The compiler's reasoning is: "You decided how this type is built; if a book without a title is also allowed, say so explicitly."

You have two ways out. Either you make the calling side fit the constructor: new Book("Dune", 45.90m). Or you add a parameterless constructor on purpose. Which one is right is a design question: does a book without a title make sense in your application? Usually it does not, and the constructor exists exactly to enforce that.

Fields: Where the Data Lives

A field is a variable inside an object. In the previous post we made the fields public; in real code a field is almost always private, and by C# convention it has a camelCase name starting with an underscore: _price, _number. Fields are the object's internal state; you decide what to show to the outside with properties.

If you make a field readonly, you can only assign it where it is declared or in a constructor:

Order order = new Order("ORD-1001");
Console.WriteLine(order.Number);

public class Order
{
    private readonly string _number;

    public Order(string number)
    {
        _number = number;
    }

    public string Number => _number;

    public void Renumber(string number)
    {
        _number = number;
    }
}
Program.cs(17,9): error CS0191: A readonly field cannot be assigned to (except in a constructor or init-only setter of the type in which the field is defined or a variable initializer)

An order number is given once and never changes; readonly turns that from a comment or good intentions into a rule the compiler checks. The line Number => _number is also our first property example: it opens only a read door to the outside.

Properties: The Doorman of a Field

From the outside a property looks like a field, but inside it is two methods: get, which runs when you read, and set, which runs when you write. Think of a building's doorman; the way to the apartment goes through him, and he decides who gets in. Here is a Book that stops the price from going negative:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Book dune = new Book("Dune", 45.90m);
dune.Price = 39.90m;
Console.WriteLine($"{dune.Title}: {dune.Price}");

try
{
    dune.Price = -10m;
}
catch (ArgumentOutOfRangeException ex)
{
    Console.WriteLine(ex.Message);
}

Console.WriteLine($"{dune.Title}: {dune.Price}");

public class Book
{
    private decimal _price;

    public string Title { get; }

    public decimal Price
    {
        get { return _price; }
        set
        {
            if (value < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(value), value, "Price cannot be negative.");
            }

            _price = value;
        }
    }

    public Book(string title, decimal price)
    {
        Title = title;
        Price = price;
    }
}
Dune: 39.90
Price cannot be negative. (Parameter 'value')
Actual value was -10.
Dune: 39.90

The value inside set is the value someone wants to assign; you do not declare it, the language provides it. When a negative price arrives we throw an exception, and the price stays at 39.90. Notice that the constructor assigns to Price, not to _price: the rule is written in one place, and the constructor walks through that same door. The culture line keeps the decimal separator from changing from machine to machine.

Why not just write a public field and move on? Because you can change the code behind a property without touching any line that uses it. Today Price holds a plain value; tomorrow you can add validation or logging to it. Turning a public field into a property later breaks other assemblies that use the compiled code. On top of that, tools like JSON serializers, data binding and EF Core work with properties by default.

Auto-Properties, Get-Only and Computed Properties

You do not have to write a field by hand behind every property. When you write { get; set; } the compiler generates an invisible field; this is called an auto-property. Most properties without rules look like this. Here are a few variations that narrow access, in a single class:

Book dune = new Book("Dune", "Frank Herbert")
{
    Isbn = "978-0441172719"
};
dune.Receive(3);

Console.WriteLine($"{dune.Title} / {dune.Author} / {dune.Isbn} / stock {dune.Stock} / in stock: {dune.IsInStock}");

Book draft = new Book("Untitled");
Console.WriteLine($"{draft.Title} / {draft.Author} / '{draft.Isbn}' / in stock: {draft.IsInStock}");

public class Book
{
    public string Title { get; }
    public string Author { get; }
    public string Isbn { get; init; } = "";
    public int Stock { get; private set; }
    public bool IsInStock => Stock > 0;

    public Book(string title, string author)
    {
        if (string.IsNullOrWhiteSpace(title))
        {
            throw new ArgumentException("Title is required.", nameof(title));
        }

        Title = title;
        Author = author;
    }

    public Book(string title) : this(title, "Unknown")
    {
    }

    public void Receive(int quantity)
    {
        Stock += quantity;
    }
}
Dune / Frank Herbert / 978-0441172719 / stock 3 / in stock: True
Untitled / Unknown / '' / in stock: False

Title { get; } is a get-only auto-property: it gets its value only in the constructor or next to its declaration, and nobody can change it afterwards. Isbn { get; init; } uses the init accessor that arrived in C# 9; it can be assigned while the object is being created, in the initializer block, and is locked after that. Stock { get; private set; } lets everyone read but leaves writing to the class itself; stock changes only through methods like Receive. IsInStock => Stock > 0 is a property with no field behind it, recalculated every time it is read. As Stock changes, IsInStock stays correct by itself; there is no second value you have to keep in sync.

Constructors: Let the Object Be Born Valid

A constructor is a special method that runs once when an object is created; its name matches the class name and it has no return type. Its real job is to hand over the object in a usable state. The two-parameter constructor above rejects an empty title. As a result, nowhere in the program do you need to wonder "can there be a book with an empty title?"; such a book can never be created.

Constructors can be overloaded too. Instead of rewriting its own validation, the one-parameter constructor delegates to the other one with : this(title, "Unknown"). Thanks to this chaining the validation stays in one place; when you add a new rule for the title tomorrow, there is no risk of forgetting to update both constructors. As you can see, the author of draft became "Unknown" and the ISBN stayed at the property's default, an empty string.

A note: C# 12 brought primary constructors to classes, which made simple assignments like these much shorter to write; we will see them in the C# Versions series. In C# 10 you write the constructor body yourself.

init and Object Initializers

An object initializer is a short way to assign properties right after the constructor has run. Instead of adding a constructor parameter for every optional value, you take the required ones through the constructor and the optional ones through the initializer. So what happens if you try to touch these properties after the book has been created?

Book dune = new Book("Dune", "Frank Herbert") { Isbn = "978-0441172719" };
dune.Isbn = "000";
dune.Stock = 10;
dune.Title = "Dune Messiah";

public class Book
{
    public string Title { get; }
    public string Author { get; }
    public string Isbn { get; init; } = "";
    public int Stock { get; private set; }

    public Book(string title, string author)
    {
        Title = title;
        Author = author;
    }
}
Program.cs(2,1): error CS8852: Init-only property or indexer 'Book.Isbn' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.
Program.cs(3,1): error CS0272: The property or indexer 'Book.Stock' cannot be used in this context because the set accessor is inaccessible
Program.cs(4,1): error CS0200: Property or indexer 'Book.Title' cannot be assigned to -- it is read only

Three lines, three different locks. init says "only during construction", private set says "only from inside", and get-only says "only in the constructor". Had you tried to assign Stock inside the initializer, you would have got the same CS0272; private set is closed to initializers as well.

init has one weak spot: it does not make assignment mandatory. The ISBN of draft was never given, and nobody complained. In C# 10 the place for a mandatory value is the constructor. required in C# 11 closes this gap; that is a topic for the C# Versions series too.

In Which Order Does It Run

When field initializers, the constructor body and an object initializer come together in the same new expression, the order is this:

Book dune = new Book("Dune") { Format = Log("3. object initializer", "Paperback") };
Console.WriteLine($"{dune.Title}, {dune.Format}, {dune.Currency}");

static string Log(string step, string value)
{
    Console.WriteLine(step);
    return value;
}

public class Book
{
    public string Currency { get; } = Show("1. field/property initializer", "EUR");
    public string Title { get; }
    public string Format { get; init; } = "Hardcover";

    public Book(string title)
    {
        Console.WriteLine("2. constructor body");
        Title = title;
    }

    private static string Show(string step, string value)
    {
        Console.WriteLine(step);
        return value;
    }
}
1. field/property initializer
2. constructor body
3. object initializer
Dune, Paperback, EUR

First the initial values next to the declarations are assigned, then the constructor body runs, and the initializer block comes last. Format first became "Hardcover", then the initializer overwrote it with "Paperback". The practical consequence of this order: inside the constructor you cannot rely on values that will come from the initializer, because at that moment they have not been assigned yet. Everything you want to validate in the constructor should be a constructor parameter.

Common Pitfalls

Calling a property from inside itself. When you write a property with a field behind it, typing the property's name instead of the field's is an easy mistake, and the compiler does not warn you:

Book dune = new Book();
dune.Title = "Dune";

public class Book
{
    public string Title
    {
        get => Title;
        set => Title = value;
    }
}
Stack overflow.
Repeat 261297 times:
--------------------------------
   at Book.set_Title(System.String)
--------------------------------

set calls itself, which calls itself, until the stack is full. You cannot catch a stack overflow with try/catch; the process simply goes down. The name set_Title in the output is proof that, behind the scenes, a property really is a method.

Writing { get; set; } on every property. Every set opened out of habit means anyone can change the object's state. Start with get-only; if it needs to change, decide whether init, private set or a method that states the intent fits better.

Validating in the property but not in the constructor, or the other way round. If a rule is written in two places, sooner or later one of them gets updated and the other is forgotten. Keep the rule in one place by assigning from the constructor to the property, or by chaining constructors with this(...).

Loading work onto the constructor. A constructor builds the object; connecting to a database, reading a file or making a network call is not its job. Error handling gets harder, and to test the object you have to bring that whole world up.

A Small Experiment

★☆☆ Add a PageCount property to Book: get-only, coming from the constructor, throwing an ArgumentOutOfRangeException when 0 or a negative number arrives. Try it with a bad value and read the exception message.

★★☆ Next to Receive, write a Sell(int quantity) method. It should throw an InvalidOperationException when someone wants to sell more than the stock. See how Stock being private set protects this rule by trying to break it with a public set.

★★★ Add a Discount property to the ordering example above; its default is 0, and the initializer sets it to 10. Print Discount inside the constructor, guess which value you will see, then run it. What does the result tell you about which values you can rely on in a constructor?

In this post we took control of how an object is born and through which doors it can be changed. But we used the word private mostly by instinct. In the next post we look at encapsulation and the full set of access modifiers: public, private, protected, internal and their two-word combinations, plus why a get-only list can still be changed from outside.