Properties

Properties are how C# classes expose data. To the caller they look like fields — person.Age = 30 — but behind the scenes they run code, so the class stays in control: it can validate new values, compute values on the fly, or make data read-only. Public data in C# is almost always a property, never a public field.

Auto-implemented properties

The simplest property has an empty get and set. The compiler creates a hidden field to store the value:

C#
var p = new Product { Name = "Kettle", Price = 29.99m };
p.Price = 24.99m;
Console.WriteLine($"{p.Name}: {p.Price}");

class Product
{
    public string Name { get; set; } = "";
    public decimal Price { get; set; }
}
Output
Kettle: 24.99

= "" gives the property an initial value. Why not just use a public field? Because a property can later gain validation or logic without changing how callers use it, and much of .NET (data binding, JSON serialisation, ORMs) works with properties, not fields.

Read-only and init-only properties

Leave out the setter, make it private, or use init to control who can change a value:

C#
var order = new Order("A-1001") { Customer = "Lin" };
order.AddItem(15m);
order.AddItem(7.5m);

Console.WriteLine($"{order.Id} for {order.Customer}: {order.Total} ({order.ItemCount} items)");

class Order(string id)
{
    public string Id { get; } = id;                // read-only: set only in the constructor
    public string Customer { get; init; } = "";    // settable only during initialisation
    public decimal Total { get; private set; }     // read publicly, written only inside the class
    public int ItemCount { get; private set; }

    public void AddItem(decimal price)
    {
        Total += price;
        ItemCount++;
    }
}
Output
A-1001 for Lin: 22.5 (2 items)
C#
var order = new Order { Customer = "Lin" };
order.Customer = "Someone else";
order.Total = 0;

class Order
{
    public string Customer { get; init; } = "";
    public decimal Total { get; private set; }
}
Build error
Program.cs(2,1): error CS8852: Init-only property or indexer 'Order.Customer' 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 'Order.Total' cannot be used in this context because the set accessor is inaccessible

Computed properties

A property with only a getter can calculate its value each time it's read. Use => for a one-line getter:

C#
var r = new Rectangle { Width = 4, Height = 3 };
Console.WriteLine($"{r.Area} {r.Perimeter} {r.IsSquare}");
r.Width = 3;
Console.WriteLine($"{r.Area} {r.Perimeter} {r.IsSquare}");

class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }

    public double Area => Width * Height;
    public double Perimeter => 2 * (Width + Height);
    public bool IsSquare => Width == Height;
}
Output
12 14 False
9 12 True

Computed properties are always up to date — there's no stored copy that could get out of sync. Keep them cheap; if a calculation is expensive, a method (CalculateTax()) signals that better.

Validation with the field keyword

To check values in a setter you need somewhere to store them. C# 14 adds the field keyword, which refers to the property's compiler-generated storage, so you can add logic without declaring a separate field:

C#
var player = new Player { Name = "  Zed " };
player.Health = 80;
player.Health += 50;              // clamped to 100
Console.WriteLine($"{player.Name}: {player.Health}");

player.Name = "";

class Player
{
    public string Name
    {
        get;
        set => field = string.IsNullOrWhiteSpace(value)
            ? throw new ArgumentException("Name cannot be empty.")
            : value.Trim();
    } = "Player";

    public int Health
    {
        get;
        set => field = Math.Clamp(value, 0, 100);
    } = 100;
}
Output
Zed: 100
Unhandled exception. System.ArgumentException: Name cannot be empty.

Inside a setter, value is the incoming value. Before C# 14 you'd write the same thing with an explicit backing field, a pattern you'll see in a lot of existing code:

C#
var t = new Thermostat();
t.Target = 35;
Console.WriteLine(t.Target);

class Thermostat
{
    private double _target = 20;

    public double Target
    {
        get => _target;
        set => _target = Math.Clamp(value, 5, 30);
    }
}
Output
30

Properties that notify or record

Because setters are code, they can do more than store a value — for example keep a history:

C#
var doc = new Document();
doc.Title = "Draft";
doc.Title = "Final";
doc.Title = "Final v2";
Console.WriteLine(doc.Title);
Console.WriteLine(string.Join(" -> ", doc.TitleHistory));

class Document
{
    private readonly List<string> _history = [];

    public IReadOnlyList<string> TitleHistory => _history;

    public string Title
    {
        get;
        set
        {
            if (field != value)
            {
                _history.Add(value);
                field = value;
            }
        }
    } = "";
}
Output
Final v2
Draft -> Final -> Final v2

IReadOnlyList<string> lets callers read the history without being able to add to it — the list itself stays private.

Indexers

An indexer is a property that takes an index, letting objects be used with square brackets like arrays and dictionaries:

C#
var week = new WeekPlan();
week["Mon"] = "Gym";
week["Wed"] = "Swim";
Console.WriteLine(week["Mon"]);
Console.WriteLine(week["Tue"]);

class WeekPlan
{
    private readonly Dictionary<string, string> _plans = [];

    public string this[string day]
    {
        get => _plans.GetValueOrDefault(day, "(rest day)");
        set => _plans[day] = value;
    }
}
Output
Gym
(rest day)

Property cheat sheet

DeclarationMeaning
{ get; set; }Read and write from anywhere
{ get; private set; }Read anywhere, write only inside the class
{ get; init; }Write only during construction or an object initialiser
{ get; }Read-only; set in the constructor or initialiser
=> expressionComputed every time it's read
requiredCallers must set it when creating the object
set => field = …Custom logic with compiler-generated storage (C# 14)

Try it yourself

Add a Percentage property to ProgressBar that's computed from Current and Max, and make Current stay between 0 and Max.

Try it yourself · Program.cs
Edit the code, copy it into Program.cs and run it with dotnet run Program.cs (.NET 10 or later). How to set up .NET
Output
[#####...............] 25%
[####################] 100%