.NET Academy

Properties and encapsulation

Expose an object's data safely with properties, validation and static members.

12 min read

You will learn

  • Write auto-properties and properties with logic
  • Restrict writes with private set and init
  • Protect an object's invariants with validation
  • Understand static members

The problem with public fields

If a class exposes a public field, anyone can put anything in it:

class Person
{
    public int Age;
}
 
var p = new Person();
p.Age = -40;   // nonsense, but nothing stops it

Making the field private protects it — but then nobody can read it either. Properties give you the best of both: they look like fields from the outside, but they run code you control.

Auto-properties

The most common property simply stores a value:

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
 
var p = new Person();
p.Name = "Ada";                 // calls the setter
Console.WriteLine(p.Name);      // calls the getter

The compiler creates a hidden backing field for you. Properties use PascalCase names.

Controlling who can write

DeclarationReadWrite
{ get; set; }everyoneeveryone
{ get; private set; }everyoneonly this class
{ get; init; }everyoneonly during construction
{ get; }everyoneonly in the constructor
class Order
{
    public int Id { get; }                       // set once, in the constructor
    public string Status { get; private set; } = "New";   // with an initial value
 
    public Order(int id) => Id = id;
 
    public void Ship() => Status = "Shipped";    // the class changes its own state
}

Properties with logic

A property can validate input or compute a value:

class Person
{
    private int age;
 
    public int Age
    {
        get => age;
        set
        {
            if (value < 0 || value > 150)
                throw new ArgumentOutOfRangeException(nameof(value), "Age must be 0–150");
            age = value;                 // 'value' is whatever the caller assigned
        }
    }
 
    public string FirstName { get; set; } = "";
    public string LastName { get; set; } = "";
 
    // Computed, read-only property: no backing field at all
    public string FullName => $"{FirstName} {LastName}";
}

Invariants

An invariant is a rule that must always be true for an object — "age is between 0 and 150", "balance is never negative". Encapsulation exists to protect invariants: if only the class can change its data, only the class needs to enforce the rules.

Object initializers

With settable or init properties, you can set values right where you create the object:

var ada = new Person { FirstName = "Ada", LastName = "Lovelace", Age = 36 };

Combined with init, this gives you objects that are easy to create and can't be changed afterwards:

class Point
{
    public int X { get; init; }
    public int Y { get; init; }
}
 
var p = new Point { X = 1, Y = 2 };
p.X = 5;   // ❌ compile error: init-only property

Static members

Sometimes data or behavior belongs to the class itself, not to any one object. Mark it static:

class Temperature
{
    public static double CelsiusToFahrenheit(double c) => c * 9 / 5 + 32;
}
 
Console.WriteLine(Temperature.CelsiusToFahrenheit(100));   // called on the class, no object

You've been using static members all along: Console.WriteLine, Math.Max, int.Parse, string.Join.

A static field has one copy shared by all objects:

class Ticket
{
    private static int nextNumber = 1;
    public int Number { get; }
 
    public Ticket() => Number = nextNumber++;   // each ticket gets the next number
}

Try it

Program.cs
Loading editor…

C# runtime not loaded

Key takeaways

  • Properties look like fields but run code: use them instead of public fields.
  • private set, init and get-only properties restrict who can change data.
  • Put validation in setters or methods to protect the object's invariants.
  • static members belong to the class, shared by all instances.

Exercise

Finish BankAccount. Owner is set once by the constructor. Balance can be read by anyone but only changed by the class itself. Deposit and Withdraw return false (and change nothing) for amounts ≤ 0; Withdraw also returns false when the balance is too low.

Program.cs
Loading editor…

C# runtime not loaded

Hint 1

The constructor should set Balance = initialBalance too. Because the setter is private, only code inside the class can do that.

Hint 2

Use guard clauses: if (amount <= 0) return false; then update Balance and return true.

Show solution
var account = new BankAccount("Ada", 100);
account.Deposit(50);
Console.WriteLine($"{account.Owner}: {account.Balance}");

class BankAccount
{
    public string Owner { get; }
    public decimal Balance { get; private set; }

    public BankAccount(string owner, decimal initialBalance)
    {
        Owner = owner;
        Balance = initialBalance;
    }

    public bool Deposit(decimal amount)
    {
        if (amount <= 0) return false;
        Balance += amount;
        return true;
    }

    public bool Withdraw(decimal amount)
    {
        if (amount <= 0 || amount > Balance) return false;
        Balance -= amount;
        return true;
    }
}

Quiz

4 questions · 3 correct to pass
  1. 1.What does `public decimal Balance { get; private set; }` allow?

  2. 2.What's special about `public string Name { get; init; }`?

  3. 3.What does `static` mean on a member?

  4. 4.Why use a property instead of a public field?

To complete this lesson

  • ○ Solve the exercise
  • ○ Pass the quiz (3/4 correct)