Properties & the field Keyword

C# · Lesson 16 of 31 6 min read Updated

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.

C#
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; }
}
Output
Gel pen: Rs 20, stock 120
  • get is the accessor that reads the value; set writes it.
  • = ""; gives the property a starting value, so Name is never null.
  • 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.

C#
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++;
    }
}
Output
Order 5001 for Meera
Items: 2, total: Rs 570.50
Free delivery: True
DeclarationMeaning
{ 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
=> expressionComputed: recalculated every time it is read, nothing stored
requiredCallers 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:

C#
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)

C#
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.");
    }
}
Output
[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.

C#
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;
}
Output
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; } reads Age, which reads Age, forever, until the program crashes with a stack overflow. Use field or a separate backing field.
  • Public fields instead of properties. Use { get; set; } from the start.
  • Using set when init is enough. If a value should never change after creation, use init or { get; }.
  • Slow or surprising getters. Reading a property should be quick and should not change anything. Use a method for heavy work.
  • Ignoring CS8618 warnings about non-nullable properties that are never set. Add required, a default value or a constructor.

Key points

  • Properties look like fields but run get and set accessors.
  • Auto-properties { get; set; } store values in a hidden backing field.
  • private set, init, { get; } and required control who can set a value and when.
  • Computed properties => expression calculate their value each time they are read.
  • Object initializers new() { Name = ... } set properties while creating an object.
  • C# 14's field keyword 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
C#
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);
}
Output
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?

2. When can an init property be assigned?

3. In C# 14, what does the field keyword refer to inside a property accessor?

4. What is wrong with public int Age { get => Age; }?

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