C# Properties

Properties are class members that look like fields from the outside but behave like methods on the inside. They are the standard C# way to expose object data while keeping control over how that data is read, changed, validated, or calculated. Good property design is one of the main ways C# classes protect their state without making code awkward to use.

Overview: How Properties Work

A property is a named member with one or more accessors. A get accessor returns a value. A set accessor assigns a value. An init accessor assigns a value only during object initialization. Calling code uses property syntax such as person.Name, but the compiler translates that syntax into method calls behind the scenes.

This is why properties are different from public fields. A field is a storage location. A property is an API. It may store data in a private backing field, compute a value from other state, reject invalid input, expose a read-only view, or forward to another object. The outside code does not need to know which approach the class uses.

For example, public string Name { get; set; } is an auto-property. The compiler creates a hidden backing field and emits accessor methods for you. A property such as public decimal Total => UnitPrice * Quantity; has no stored field of its own; it calculates a value whenever it is read. A property with a custom setter can inspect the incoming value through the contextual keyword value and throw an exception if the assignment would break the object’s rules.

At runtime, ordinary property access is just method execution. The CLR does not treat properties as magical storage slots. Metadata marks the get and set methods as property accessors so tools, reflection, serializers, and frameworks can recognize them as properties. Because accessors are methods, they can contain logic, but they should still feel simple to callers. Expensive work, I/O, and surprising side effects usually belong in methods, not properties.

Syntax

class ClassName
{
    private string backingField = "default";

    public string AutoProperty { get; set; } = "initial";

    public string ReadOnlyProperty { get; }

    public string ValidatedProperty
    {
        get { return backingField; }
        set { backingField = value; }
    }

    public int CalculatedProperty => backingField.Length;
}
Part Meaning
get Runs when code reads the property.
set Runs when code assigns the property after construction or initialization.
init Allows assignment only during object initialization, including object initializers and constructors.
value The special name for the value being assigned inside a set or init accessor.
private set Makes a property readable from outside the class but writable only inside the class.
=> Creates an expression-bodied accessor or calculated property.

Examples

Example 1: Auto-Properties

using System;

class UserProfile
{
    public string DisplayName { get; set; } = "Guest";
    public string Email { get; set; } = "unknown@example.com";
}

class Program
{
    static void Main()
    {
        UserProfile user = new UserProfile();
        user.DisplayName = "Ada";
        user.Email = "ada@example.com";

        Console.WriteLine(user.DisplayName);
        Console.WriteLine(user.Email);
    }
}

Output:

Ada
ada@example.com

DisplayName and Email are auto-properties. The compiler creates the hidden storage, while the public API remains clean. This is the right style when a value needs no special validation and can be read and changed by callers.

Example 2: Validation With a Backing Field

using System;
using System.Globalization;

class Product
{
    private decimal price;

    public Product(string name, decimal price)
    {
        Name = name;
        Price = price;
    }

    public string Name { get; }

    public decimal Price
    {
        get { return price; }
        set
        {
            if (value < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(value), "Price cannot be negative.");
            }

            price = value;
        }
    }

    public decimal SalePrice => Price * 0.8m;
}

class Program
{
    static void Main()
    {
        Product coffee = new Product("Coffee Beans", 10.99m);
        coffee.Price = 12.50m;

        Console.WriteLine(coffee.Name);
        Console.WriteLine(coffee.Price.ToString("0.00", CultureInfo.InvariantCulture));
        Console.WriteLine(coffee.SalePrice.ToString("0.00", CultureInfo.InvariantCulture));
    }
}

Output:

Coffee Beans
12.50
10.00

Price uses a private backing field because assignment has a rule: negative prices are not allowed. The constructor assigns through the property, so the same validation is used during construction and later updates. SalePrice is calculated from Price, so it always reflects the current price without storing duplicate state.

Example 3: Private Setters and Init-Only Properties

using System;

class Order
{
    public required string CustomerName { get; init; }
    public int ItemCount { get; private set; }
    public bool IsSubmitted { get; private set; }

    public void AddItem()
    {
        if (IsSubmitted)
        {
            throw new InvalidOperationException("Submitted orders cannot be changed.");
        }

        ItemCount++;
    }

    public void Submit()
    {
        IsSubmitted = true;
    }
}

class Program
{
    static void Main()
    {
        Order order = new Order { CustomerName = "Maya" };
        order.AddItem();
        order.AddItem();
        order.Submit();

        Console.WriteLine(order.CustomerName);
        Console.WriteLine(order.ItemCount);
        Console.WriteLine(order.IsSubmitted);
    }
}

Output:

Maya
2
True

CustomerName is required and init-only, so callers must provide it while creating the object and cannot change it later. ItemCount and IsSubmitted have private setters: other code can read them, but only the Order class can change them through meaningful methods.

How Properties Work Step by Step

  1. The compiler reads the property declaration and creates accessor methods, commonly named like get_Name and set_Name in generated metadata.
  2. For an auto-property, the compiler also creates a private hidden backing field. Your source code cannot name this field directly.
  3. When code reads user.DisplayName, the compiler emits a call to the getter. The getter returns the stored or calculated value.
  4. When code assigns product.Price = 12.50m, the compiler emits a call to the setter and passes 12.50m as value.
  5. If the setter throws an exception, the assignment does not complete. This protects the object’s invariant from invalid state.
  6. An init accessor is checked by the compiler. It can be called during object creation, but later assignment in ordinary code is rejected at compile time.
  7. Reflection and many .NET libraries see the property metadata, which is why properties are commonly used by serializers, UI binding systems, and configuration tools.

Common Mistakes

Assigning a Read-Only Property From Outside

class Account
{
    public decimal Balance { get; private set; }
}

Account account = new Account();
account.Balance = 100m;

This is wrong because private set means only code inside Account can change Balance. The fix is to expose a method that represents a valid operation.

using System;

class Account
{
    public decimal Balance { get; private set; }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount));
        }

        Balance += amount;
    }
}

class Program
{
    static void Main()
    {
        Account account = new Account();
        account.Deposit(100m);
        Console.WriteLine(account.Balance);
    }
}

Output:

100

Creating a Recursive Property

class Person
{
    public string Name
    {
        get { return Name; }
        set { Name = value; }
    }
}

This compiles, but it is broken. The getter calls itself forever, and the setter assigns the property again instead of assigning storage. Use an auto-property or a separate backing field.

using System;

class Person
{
    private string name = "";

    public string Name
    {
        get { return name; }
        set { name = value; }
    }
}

class Program
{
    static void Main()
    {
        Person person = new Person();
        person.Name = "Nia";
        Console.WriteLine(person.Name);
    }
}

Output:

Nia

Best Practices

  • Use properties instead of public fields for data that is part of a type’s public API.
  • Use auto-properties when no custom logic is needed; they are clear and compile to efficient accessor methods.
  • Use a private backing field when the property must validate, normalize, cache, or coordinate with other state.
  • Keep property getters fast and predictable. Prefer methods for expensive work, I/O, random results, or actions with side effects.
  • Use private set when callers should read a value but only the object should change it.
  • Use init for values that should be provided during initialization and then remain stable.
  • Do not store calculated values unless you need caching. A calculated property avoids stale duplicate state.
  • Throw clear exceptions from setters when invalid assignment would put the object into an impossible state.

Practice Exercises

  1. Create a Movie class with Title, Year, and a read-only calculated property named DisplayText that returns both values.
  2. Create a Thermostat class with a Celsius property that rejects values below -273.15. Add a calculated Fahrenheit property.
  3. Create a ShoppingCartItem class with init-only Name and UnitPrice properties, a private-set Quantity, and a method that increases quantity.

Summary

  • Properties give classes field-like syntax with method-like control.
  • get reads, set assigns, and init assigns only during initialization.
  • Auto-properties are best for simple stored values; backing fields are best when validation or custom logic is needed.
  • Calculated properties return values derived from other state and help avoid duplicated data.
  • private set and init make invalid changes harder while keeping object usage readable.
  • Behind the scenes, the compiler emits accessor methods and, for auto-properties, hidden backing fields.