AlgoMaster Logo

Properties (get/set/init)

Last Updated: May 17, 2026

13 min read

A property is C#'s way of exposing data on an object without giving callers raw access to the underlying field. From the outside, a property looks and feels like a field: you read it, you assign to it, you use it in expressions. From the inside, it's a pair of accessor methods that the compiler generates for you, which means you can add validation, change storage, or make the value read-only later without breaking a single caller. Most modern C# code treats properties as the default way to expose state, and public fields are rare enough to be a code smell.

Why Properties Exist

The simplest way to give an object some data is a public field:

That works. The compiler is happy, the program runs, and the syntax at the call site is exactly what you want. So why doesn't C# code look like this in practice?

The trouble shows up the moment you need anything more than raw storage. Suppose six months later a bug report comes in: a customer set the price of a product to -5.99m. You'd like to reject negative prices. With a public field, you can't. There's no method to add a check to. Every assignment goes straight to the field, and the only way to enforce a rule is to hunt down every caller and add a check at each site, which is exactly the kind of work that doesn't scale.

A property fixes this. From the outside, the syntax is identical to a field. From the inside, the assignment runs through a setter method where you can do whatever you want: validate, log, normalize, fire an event, lazily initialize a backing object.

Same call site, completely different guarantees. The class now owns the rule that prices can't be negative, and no caller can route around it. This is the core reason properties exist: they keep field-like syntax for callers while giving the class a place to put logic.

There's a second reason that matters as much: binary compatibility. In .NET, a property and a field look different to the compiler. If you ship version 1 of a library with public string Name; and version 2 with public string Name { get; set; }, every program that linked against version 1 has to be recompiled. The two are interchangeable in source but not in IL. Properties stay shaped like properties forever, which means you can change the body without breaking downstream code.

Full Property Syntax With a Backing Field

The longhand form of a property is what every other form is sugar for. It has three parts: a private field that actually stores the value, a get accessor that returns it, and a set accessor that writes it.

The private field _price is the actual storage. The convention is an underscore prefix and camelCase. The property Price is what callers see. The get block runs whenever someone reads keyboard.Price, and the set block runs whenever someone writes to it.

Inside the setter, value is a magic parameter the compiler hands you. It's the right-hand side of the assignment. When a caller writes keyboard.Price = 89.99m, the compiler turns that into a call equivalent to keyboard.set_Price(89.99m), with 89.99m arriving as value inside the setter body. You don't declare value; it's always there inside any setter.

The shape of the setter is where the leverage lives. You can do anything you'd do in a regular method: validate, normalize, log, fire events.

Two things to notice. First, the setter rounded 19.995m to two decimal places before storing it, so the read returns 20.00m, not the original input. Second, the negative assignment threw before _price was touched, so the previous value is still safe inside the object.

The getter usually returns the field as-is, but it doesn't have to. You can compute, format, or transform on read. The point is that callers see one consistent shape, and the class chooses how to satisfy it.

A deep look at validation philosophy (when to throw, when to clamp, when to coerce) belongs in the Encapsulation section later in the course. This lesson uses validation as an example of what properties unlock, not as the final word on how to design it.

Auto-Implemented Properties

The pattern of "private field plus trivial get/set" was so common in early C# code that C# 3 introduced a shorthand for it. An auto-implemented property declares just the accessors, and the compiler generates the backing field behind the scenes.

The compiler synthesizes a hidden backing field for each auto-property, with a name like <Name>k__BackingField. You can't reference that field by name from C# source, but it exists in the compiled IL and behaves exactly like a manually written one. The get returns it; the set writes to it. There's no validation, no logging, no computation.

Auto-properties are the default in modern C# code. Use them whenever the body would just be a trivial get/set, which covers most data-shaped fields on a typical class. The = "" and the implicit default on LoyaltyPoints are initializers; the next part of this section covers them.

Initializers on Auto-Properties

An auto-property can carry an initializer just like a field can:

The initializer runs once during object construction, before any constructor body. If a constructor or object initializer assigns the property, that assignment wins because it runs after.

Initializers matter most for reference types (string, lists, dictionaries). Without them, the property starts as null, and the first read on something like cart.Owner.Length throws NullReferenceException. With the initializer = "", the property starts as the empty string and the nullable reference type analysis is happy.

Mixing Auto-Properties With Full Properties in the Same Class

You don't have to pick one form for the whole class. Auto-properties cover the boring fields, and full properties handle the ones that need logic.

This is the typical shape of a real class. Most properties are auto-properties because most fields don't have rules attached. The few that do get the full form, and the rest of the class isn't cluttered by ceremony it doesn't need.

Read-Only Properties (Get-Only)

A property doesn't have to have a setter. Drop the set accessor and the property becomes read-only from the outside.

A get;-only auto-property can still be assigned in two places: the constructor of the same class, and the property's own initializer. Once construction finishes, the property is frozen for the rest of the object's life. Try to assign it from outside and the compiler reports CS0200 ("Property or indexer cannot be assigned to, it is read only").

Behind the scenes, a get-only auto-property compiles to a readonly backing field. The field can only be assigned from a constructor or its initializer, which is what enforces the rule. The IL is the same shape as a manually written private readonly decimal _price; with a public decimal Price => _price;.

Get-only properties are how you build immutable objects: once an Order is constructed, its OrderId, Customer, and PlacedAt are fixed. Any code that holds a reference to that order can read those values and know they won't change underneath. That's a useful guarantee in concurrent code, in collections that hash on those values, and in code that wants to reason about state.

Get-Only With a Backing Field

For cases where the setter is private logic, not nothing, you can still use the full form with a read-only backing field:

Total has no setter, so callers can read it but not write it. The class still updates _total internally through methods. This is one of two patterns for "read from outside, write from inside"; the next section covers the cleaner version.

Init-Only Setters (C# 9+)

A read-only property is great once construction is done, but it forces all values into the constructor. For a class with ten fields, that's a constructor with ten parameters and no easy way for callers to set just a few. Object initializers (the new Product { Name = "...", Price = ... } syntax) work much better for that shape, but they require a setter, which means the property isn't truly read-only afterward.

C# 9 introduced the init accessor to solve this. An init accessor behaves like a setter during object construction (constructor body, object initializer, with expressions on records), and like a missing setter afterward.

The object initializer assigns Name, Price, Category, and StockCount as part of construction. After the closing brace of the initializer, those properties are frozen. Try to assign one later and the compiler reports CS8852 ("Init-only property... can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor").

The diagram below shows the two windows a property can be written in:

The orange phase is where init accessors will accept writes. The teal phase is the rest of the object's life, where only reads succeed. Once the object hops from orange to teal, the door closes.

The result is the best of both worlds: clear object initializer syntax at the call site and full immutability afterward. This is the default shape for a modern C# data class.

init vs set vs get-only

The three forms can be confusing at first because the call site looks similar.

Property formAssign in initializer?Assign in constructor?Assign after construction?
{ get; set; }YesYesYes
{ get; init; }YesYesNo
{ get; }NoYesNo

get; set; is mutable forever. get; init; is mutable only until the object initializer or constructor finishes. get; is the most restrictive, you can't even use an object initializer; the value has to come through the constructor.

Most modern data classes default to init. It supports the object initializer pattern that C# developers expect, and it prevents the object from changing afterward, which avoids a whole category of bugs.

The required modifier on init-only properties (C# 11) forces the caller to set them in the initializer.

Expression-Bodied Properties

When the getter is a single expression and there's no setter, you can drop the braces and write the property with =>. This is the same expression-bodied syntax that methods use, and it works on both the property itself and on individual accessors.

A read-only property with a single-expression getter:

Three forms, same meaning. ItemCount uses the block-bodied getter. Subtotal uses an expression-bodied accessor (get =>). IsEmpty uses the shortest form: a property body that's a single => with no get keyword at all. That last form only works for get-only properties; if you need a setter or init, you have to write the accessors out.

You can also use expression-bodied syntax on both accessors of a writable property:

The setter clamps negative values to zero. Both accessors are one expression, so both use =>. This style is common for simple validation that fits on one line. When the setter grows beyond a single expression (multiple statements, complex conditions), switch back to a block body.

C# 13 added a field keyword that lets you reference the compiler-generated backing field inside an accessor without declaring one yourself, blurring the line between auto-properties and full properties. That's outside the scope of this lesson; it lands deeper in the language and is worth its own treatment in the Modern C# Features section.

Different Access on get vs set

The get and set accessors don't have to share the same visibility. You can give one of them a stricter access modifier so that callers can read freely but only the class itself can write. This is one of the most useful tricks properties offer.

The shape:

The property as a whole is public (anyone can read). The setter is private (only code inside the class can write). From the outside, this looks exactly like a read-only property. From inside the class, methods can update it.

A cart that tracks its own total:

Outside code can read cart.Total and cart.ItemCount, but can't change them directly. The only path to modify them is through the AddItem and Clear methods, which means the class controls the rules: the total can never go negative, item count and total stay in sync, and so on. Try to assign cart.Total = 0m from outside and the compiler reports CS0272.

The asymmetry can go the other way too, though it's much rarer. You could write private get; public set; for a write-only property, but the cases for that are unusual enough that you probably won't see it in real code.

A side-by-side comparison of the common access patterns:

FormRead from outside?Write from outside?Write from inside?
public T Prop { get; set; }YesYesYes
public T Prop { get; private set; }YesNoYes
public T Prop { get; init; }YesOnly in initializerOnly in constructor
public T Prop { get; }YesNoOnly in constructor
private T Prop { get; set; }NoNoYes

The two most useful entries are get; private set; (mutable, but the class owns when) and get; init; (immutable after construction). They cover the bulk of well-designed data classes.

The cyan node is anyone calling the class from outside. The dashed arrow shows what the compiler refuses to allow: writes from external code. The green node is internal methods, which still have full access through the private setter. The class is the gatekeeper.

Computed Properties (No Backing Field)

A property doesn't have to store anything. If the value can be calculated from other state, the getter can return the result of a computation and skip the backing field entirely. These are usually called computed properties or calculated properties.

Prices and DiscountPercent are stored. ItemCount, Subtotal, DiscountAmount, and Total are all computed from those two. Every read recalculates. The class doesn't need to remember the subtotal because it can always compute it from the prices, and that means the subtotal can never get out of sync with the prices.

This is a useful pattern for any value that's a function of other state. The class becomes easier to reason about (fewer fields to keep in sync) and the API stays clean (cart.Total reads the same whether it's stored or computed).

The cost trade-off is worth understanding. A field read is roughly one memory load. A computed property that loops over 10,000 items is 10,000 times more work, every read. For a typical class with a handful of items, the difference is invisible. For a class that's read in a tight loop with a large collection, it can dominate the runtime.

A simple rule of thumb: if the body is O(1) and cheap, compute. If the body is O(n) and gets read often, store. If the work is expensive enough that callers might want to know it's happening, expose it as a method instead. A computed property carries the implicit promise that reading it is cheap; a method named RecalculateTotal() does not.

Computed properties can also reference other properties. Total reads Subtotal and DiscountAmount, which in turn read Prices and DiscountPercent. The chain compiles to nested method calls under the hood, and the JIT often inlines the whole thing into a single arithmetic expression. The readability gain is enormous and the performance cost is usually nothing.

Summary

  • Properties give callers field-like syntax while letting the class own validation, computation, and storage decisions. Modern C# uses properties as the default way to expose data and treats public fields as a code smell.
  • The full form has a private backing field plus get and set accessors. The setter receives the assigned value through the implicit value parameter.
  • Auto-implemented properties (public T Prop { get; set; }) skip the backing field and let the compiler generate one. They cover most plain-data fields.
  • A get-only auto-property ({ get; }) compiles to a readonly backing field. It can only be assigned in the constructor or its own initializer, and it's the simplest way to build an immutable object.
  • init accessors (C# 9+) accept writes during construction, including in object initializers, then freeze. They give you object-initializer syntax and immutability at the same time, which is the modern default for data classes.
  • Expression-bodied syntax (=>) works on whole properties (public int Count => _items.Length;) and on individual accessors (get => _field;). Use it when the body is a single expression, switch to a block otherwise.
  • Different access modifiers on get and set (public T { get; private set; }) let external code read freely while keeping writes inside the class. This is the standard pattern for objects that update their own state through methods.
  • Computed properties have no backing field and recalculate on every read. They keep derived values in sync automatically but pay the body's cost on each read. Compute for cheap O(1) expressions, store for expensive O(n) ones.