Properties & the field Keyword
In the last lesson you kept fields private and wrote methods to read and change them. Writing a GetX and SetX method for every value gets tiring, so C# has properties: members that look like fields from the outside (product.Price = 20m;) but run code like methods on the inside.
Properties are everywhere in .NET: list.Count, text.Length, DateTime.Now. In this lesson you will write auto-properties, read-only and init-only properties, required properties, computed properties, and validated properties using the new C# 14 field keyword.
Auto-properties and object initializers
The simplest property is an auto-implemented property: you write { get; set; } and the compiler creates a hidden private field to store the value.
Product pen = new() { Name = "Gel pen", Price = 20m, Stock = 150 };
pen.Stock -= 30;
Console.WriteLine($"{pen.Name}: Rs {pen.Price}, stock {pen.Stock}");
class Product
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
public int Stock { get; set; }
}Gel pen: Rs 20, stock 120
getis the accessor that reads the value;setwrites it.= "";gives the property a starting value, soNameis nevernull.new() { Name = ..., Price = ... }is an object initializer: it creates the object and sets properties in one expression. It is the most common way to create simple objects in C#.
Controlling who can change a property
Often a value should be readable by everyone but changeable only in certain ways. Properties make that easy.
var order = new Order { Customer = "Meera", OrderNo = 5001 };
order.AddItem(450m);
order.AddItem(120.50m);
Console.WriteLine($"Order {order.OrderNo} for {order.Customer}");
Console.WriteLine($"Items: {order.ItemCount}, total: Rs {order.Total}");
Console.WriteLine($"Free delivery: {order.FreeDelivery}");
class Order
{
public required string Customer { get; init; }
public required int OrderNo { get; init; }
public decimal Total { get; private set; }
public int ItemCount { get; private set; }
public bool FreeDelivery => Total >= 500m;
public void AddItem(decimal price)
{
Total += price;
ItemCount++;
}
}Order 5001 for Meera Items: 2, total: Rs 570.50 Free delivery: True
| Declaration | Meaning |
|---|---|
{ get; set; } | Anyone can read and change it |
{ get; private set; } | Anyone can read it; only the class can change it |
{ get; init; } | Can be set only while the object is created (constructor or initializer) |
{ get; } | Read-only; set only in the constructor |
=> expression | Computed: recalculated every time it is read, nothing stored |
required | Callers must set it in the object initializer (C# 11) |
After creation, order.OrderNo = 5002; fails with CS8852, because an init property can only be set during initialization. Leaving out Customer in the initializer fails with CS9035: Required member 'Order.Customer' must be set in the object initializer or attribute constructor. required is the modern way to make sure an object is never created half-empty.
Total uses private set, so the only way to change it is AddItem. FreeDelivery has no storage at all; it is calculated from Total each time you read it.
Validation: the old way with a backing field
Auto-properties accept any value. To check values, you used to write a full property with your own private backing field:
class Student
{
private int _marks;
public int Marks
{
get => _marks;
set
{
if (value < 0 || value > 100)
{
throw new ArgumentOutOfRangeException(nameof(value), "Marks must be 0-100.");
}
_marks = value;
}
}
}Inside set, the keyword value holds the value being assigned. This works, and you will see it in almost every existing C# project, but notice the cost: just to add one check, you must declare a field and write both accessors in full.
The field keyword (C# 14)
var kabir = new Student { Name = " Kabir Singh ", Marks = 88 };
Console.WriteLine($"[{kabir.Name}] scored {kabir.Marks}");
kabir.Marks = 105;
Console.WriteLine("This line never runs");
class Student
{
public required string Name
{
get;
set => field = value.Trim();
}
public int Marks
{
get;
set => field = value is >= 0 and <= 100
? value
: throw new ArgumentOutOfRangeException(nameof(value), "Marks must be 0-100.");
}
}[Kabir Singh] scored 88 Unhandled exception. System.ArgumentOutOfRangeException: Marks must be 0-100. (Parameter 'value')
Name trims spaces every time it is set, and Marks refuses values outside 0 to 100 by throwing an exception (you will learn to catch exceptions in Exceptions). Compare Marks with the old version: same behaviour, half the code, and there is no _marks field that other methods could change by mistake.
field also makes lazy properties short. public List<string> Tags => field ??= []; creates the list the first time it is read and reuses it afterwards. If a class already has a member literally named field, write @field or this.field to refer to that member instead.
Static properties
Properties can be static too, belonging to the class rather than any object. .NET uses them a lot, for example DateTime.Now and Environment.ProcessorCount.
Console.WriteLine(Gst.StandardRate);
Gst.StandardRate = 0.12m;
Console.WriteLine(Gst.Calculate(1000m));
static class Gst
{
public static decimal StandardRate { get; set; } = 0.18m;
public static decimal Calculate(decimal amount) => amount * StandardRate;
}0.18 120.00
A static class cannot be instantiated; it only groups static members, like Math and Console.
Common mistakes
- A property that calls itself.
public int Age { get => Age; }readsAge, which readsAge, forever, until the program crashes with a stack overflow. Usefieldor a separate backing field. - Public fields instead of properties. Use
{ get; set; }from the start. - Using
setwheninitis enough. If a value should never change after creation, useinitor{ get; }. - Slow or surprising getters. Reading a property should be quick and should not change anything. Use a method for heavy work.
- Ignoring
CS8618warnings about non-nullable properties that are never set. Addrequired, a default value or a constructor.
Key points
- Properties look like fields but run
getandsetaccessors. - Auto-properties
{ get; set; }store values in a hidden backing field. private set,init,{ get; }andrequiredcontrol who can set a value and when.- Computed properties
=> expressioncalculate their value each time they are read. - Object initializers
new() { Name = ... }set properties while creating an object. - C# 14's
fieldkeyword lets you add validation to one accessor without declaring a backing field.
Exercise
Create a class MobileRecharge with these properties: Number (required, init-only string; trim it and reject anything that is not exactly 10 digits using the field keyword), Amount (decimal, must be between 10 and 5000), and a computed Cashback that is 2% of Amount but at most Rs 25. Create a recharge for number " 9876543210 " with amount Rs 899 and print all values. Then try setting Amount to 6000 and observe what happens.
Show solution
var r = new MobileRecharge { Number = " 9876543210 ", Amount = 899m };
Console.WriteLine($"{r.Number}: Rs {r.Amount}, cashback Rs {r.Cashback}");
r.Amount = 6000m;
class MobileRecharge
{
public required string Number
{
get;
init
{
string clean = value.Trim();
if (clean.Length != 10 || !clean.All(char.IsDigit))
{
throw new ArgumentException("Number must have 10 digits.");
}
field = clean;
}
}
public decimal Amount
{
get;
set => field = value is >= 10m and <= 5000m
? value
: throw new ArgumentOutOfRangeException(nameof(value), "Amount must be 10-5000.");
}
public decimal Cashback => Math.Min(Amount * 0.02m, 25m);
}9876543210: Rs 899, cashback Rs 17.98 Unhandled exception. System.ArgumentOutOfRangeException: Amount must be 10-5000. (Parameter 'value')
field works in an init accessor just as in set. The cashback is 899 x 0.02 = 17.98, which is below the Rs 25 cap, and setting 6000 is rejected by the Amount setter.
Quick Quiz
1. What does { get; private set; } allow?
private set restricts writing to the class itself while reading stays public.
2. When can an init property be assigned?
init accessors allow assignment during initialization only.
3. In C# 14, what does the field keyword refer to inside a property accessor?
field gives you the auto-property's hidden storage, so you can add logic without declaring your own field.
4. What is wrong with public int Age { get => Age; }?
The getter reads Age, which runs the same getter again, endlessly.
Learn C# with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
