Menu

C# Properties: get, set, Auto-Properties, init and required

How C# properties work: get and set accessors over a backing field, auto-properties, private set and get-only properties, computed properties, validation in setters, and the init and required keywords from C# 9 and 11.

This page includes runnable editors - edit, run, and see output instantly.

A property looks like a field to the code that uses it (order.Total, order.Total = 5) but is really a pair of methods: a get accessor that returns a value and a set accessor that receives one. That indirection lets a class check values, compute them, or refuse writes, without changing how callers use it.

Fields vs properties

A public field hands out direct access to the object's storage. Anyone can put anything in it:

public class Account
{
    public decimal Balance;   // any code can write -500 here
}

A property puts code between the caller and the storage. Here is the full, hand-written form: a private backing field plus a property with both accessors. Inside set, the keyword value is the value being assigned.

Output:

250
Rejected a negative balance
250

Beyond validation, properties matter because most of the .NET ecosystem works with them and ignores fields: interfaces can declare properties but not instance fields, data binding in WPF and MAUI binds to properties, and System.Text.Json serializes public properties by default and skips fields.

Auto-properties

When the accessors would only read and write the field, let the compiler write them. { get; set; } is an auto-implemented property: the compiler generates the hidden backing field for you.

Output:

Notebook: 9.99, 10 left

It looks like a field, so why bother? Because turning a public field into a property later is a breaking change for compiled code that uses it (and for ref and out arguments), while changing an auto-property into a full property with validation is invisible to callers. Starting with { get; set; } keeps that option open for free.

private set and get-only properties

Most state should be readable by everyone and changeable only by the object itself. Give the setter a narrower access modifier:

Output:

Ines: 2 items, total 15.75

Three kinds of read-only here:

  • { get; private set; }: the class can change it at any time, outside code cannot.
  • { get; } (get-only auto-property, C# 6): can be assigned only in a constructor or an initializer. After construction nobody can change it, not even the class. This is how you make an immutable property.
  • => expression (computed property): no storage at all. The expression runs on every read, so Total is always up to date with the list.

A computed property should be cheap and have no side effects, because callers read properties casually, in loops and in the debugger. If getting the value takes real work (a database query, a big calculation), make it a method such as CalculateTotal() so the cost is visible.

Expression-bodied accessors

Full properties with one-line accessors can use => per accessor:

private string title;

public string Title
{
    get => title;
    set => title = value?.Trim() ?? "";
}

public decimal Total => SumPrices(); is shorthand for a property with only a getter; get => ... is the same idea inside a property that also has a setter.

Getter and setter logic: notifications and lazy values

Because accessors are methods, they can do more than store a value. Two common patterns: raising a change notification from the setter, and computing a value on first read and caching it.

Output:

(building summary)
Theme: light
Theme: light
Theme changed
(building summary)
Theme: dark

Changed is an event, and the setter pattern is what INotifyPropertyChanged implementations in WPF and MAUI look like. nameof(Theme) keeps the string in sync if the property is renamed.

The infinite recursion mistake

The most common property bug is a setter that assigns to the property instead of the backing field:

public string Name
{
    get { return Name; }        // calls get again, forever
    set { Name = value; }       // calls set again, forever
}

Each accessor calls itself, and the program dies with a StackOverflowException, which cannot be caught. The fix is to read and write a separate field (name, lowercase), or to use an auto-property. C# 14 adds the field keyword for exactly this case: inside an accessor, field refers to the compiler-generated backing field, so set => field = value.Trim(); works without declaring one.

init accessors (C# 9)

A get-only property forces you to pass every value through a constructor. C# 9 added init, a setter that is allowed only during object creation: in a constructor, or in an object initializer.

public class Product
{
    public string Sku { get; init; }
    public decimal Price { get; init; }
}

var p = new Product { Sku = "MUG-01", Price = 8.50m };   // fine: during creation
p.Price = 4m;   // error CS8852: Init-only property or indexer 'Product.Price' can only be assigned
                // in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor

The result is an immutable object with initializer syntax, which is exactly what record classes use for their positional properties. To "change" such an object, create a new one; records make that a one-liner with with.

On C# 7, the closest equivalent is a get-only property plus a constructor parameter:

public class Product
{
    public string Sku { get; }
    public decimal Price { get; }
    public Product(string sku, decimal price) { Sku = sku; Price = price; }
}

required members (C# 11)

An object initializer cannot force callers to set anything: new Product { } compiles even when Sku must never be empty. C# 11 added the required modifier:

public class User
{
    public required string Email { get; init; }
    public string DisplayName { get; init; } = "";
}

var ok = new User { Email = "ana@example.com" };
var bad = new User { DisplayName = "Ana" };   // error CS9035: Required member 'User.Email'
                                              // must be set in the object initializer or attribute constructor.

required works with set and init. A constructor that sets the required members itself can say so with the [SetsRequiredMembers] attribute, so callers using that constructor are not asked to set them again.

Common mistakes

  • Recursion in an accessor. set { Name = value; } calls itself. Use a backing field or an auto-property.
  • Expensive or side-effecting getters. Readers expect a property read to be fast and safe to repeat. Use a method for work.
  • Public fields "for now". Converting a field to a property later breaks binary compatibility, and serializers ignore fields by default. Start with an auto-property.
  • Public setters on everything. { get; set; } on a balance or a status invites invalid states. Use private set, init or get-only, and change state through methods.
  • Returning a mutable collection from a get-only property. public List<string> Tags { get; } stops callers replacing the list, not adding to it. Return IReadOnlyList<string> when the collection should not change from outside.

Frequently Asked Questions

What does { get; set; } mean in C#?

It declares an auto-implemented property: the compiler creates a hidden private field and a get accessor that returns it and a set accessor that assigns it. public string Name { get; set; } behaves like a public field to callers, but it is a pair of methods, so you can later add validation or change it to private set without changing the code that uses it.

What is the difference between a field and a property in C#?

A field is a variable stored in the object. A property is a pair of methods (get and set) that look like a field from outside. Properties let a class validate values, compute results, or restrict writing, and they are what data binding, serializers and interfaces work with. Public data should be exposed as properties; fields usually stay private.

What is private set in C#?

public int Stock { get; private set; } lets any code read the property but only code inside the class change it. It is how a class exposes state it owns, such as a balance or a count, while keeping all changes behind methods that enforce the rules.

What does init do in C#?

init (C# 9) is a setter that works only while the object is being created: in a constructor or an object initializer. public string Sku { get; init; } allows new Product { Sku = "A-1" } but rejects product.Sku = "B-2" afterwards with error CS8852. It gives immutable objects initializer syntax.

What is a required property in C#?

A property marked required (C# 11) must be set by every object initializer that creates the type; leaving it out is a compile error (CS9035). It combines well with init: public required string Email { get; init; } must be supplied at creation and cannot change later.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED