← Back to C# series
🧩
OOP
OOP · Prerequisite: classes

07. Properties and Encapsulation

Exposing fields raw lets anything happen. C# properties look like fields on the outside but act like methods inside — perfect for natural encapsulation.

C#.NET 8OOPproperties
Duration
~1-1.5 hours
Level
📊 Intermediate
Prerequisite
🎯 Classes and objects
OUTCOME
Exposing fields raw lets anything happen. C# properties look like fields on the outside but act like methods inside — perfect for natural encapsulation.

What you'll learn

  • 1Understand auto-property (`{ get; set; }`) and how it works
  • 2Use `init` setters for "set-once" properties
  • 3Tell apart `readonly` fields and the `required` keyword (C# 11)
  • 4Add value validation in a full property with a backing field

Overview

In the previous lecture we protected data with `private` fields + `public` methods (`GetBalance` etc.). C# has a dedicated member that does this cleanly: a **property**. From the outside it looks like a field, but inside it runs like a method.

Core Concepts

1) Auto-property

csharp
class Person
{
    public string Name { get; set; } = "";   // bound to an auto-generated hidden field
    public int Age { get; set; }
}

The compiler generates a hidden field like `_name`/`_age` and writes `get`/`set` for you.

2) Read-only property (`get` only)

csharp
public string Name { get; }            // settable only from a constructor

Cannot be changed after the object is built.

3) `init` setter (C# 9+)

csharp
public string Name { get; init; } = "";

Settable only during object initialization (`new Person { Name = "A" }`) or inside a constructor; read-only afterwards. Great for **immutable objects**.

csharp
var p = new Person { Name = "Alice" };
// p.Name = "Bob";   // compile error

4) `required` keyword (C# 11)

Marks "this property must be given a value when the object is created."

csharp
class User
{
    public required string Email { get; init; }
}

var u = new User { Email = "a@b.com" };  // OK
// var u2 = new User();                  // compile error

Combines the safety of constructor + `init` with the convenience of object initializer syntax.

5) `readonly` field

Locks a **field** (not a property) so it can be assigned only once.

csharp
class Circle
{
    public readonly double Pi = 3.14159;
}

Settable only in the constructor. Use for near-constants.

6) Full property — backing field + validation

Auto-properties can't validate. If you need validation, declare a backing field and put logic in `set`.

csharp
class Temperature
{
    private double celsius;

    public double Celsius
    {
        get => celsius;
        set
        {
            if (value < -273.15)
                throw new ArgumentException("below absolute zero");
            celsius = value;
        }
    }
}

Inside `set`, `value` is the reserved parameter holding "the new incoming value."

Examples

Example 1 — `AutoProperty`: simplest form

csharp
// Program.cs
using CodingNow.Lecture.Oop07;

var p = new Person();
p.Name = "Alice";
p.Age = 30;
Console.WriteLine($"{p.Name} / {p.Age}");
csharp
// Person.cs
namespace CodingNow.Lecture.Oop07;

internal class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

**Output**

text
Alice / 30

**Note:** Looks like exposing fields, but leaves room to add logic in `set` later.

Example 2 — `GetInit`: object initializer + `init`

csharp
// Program.cs
using CodingNow.Lecture.Oop07;

var alice = new Person { Name = "Alice", Age = 30 };
Console.WriteLine($"{alice.Name} / {alice.Age}");

// alice.Name = "Bob";   // init means no later mutation (compile error)
csharp
// Person.cs
namespace CodingNow.Lecture.Oop07;

internal class Person
{
    public string Name { get; init; } = "";
    public int Age { get; init; }
}

**Output**

text
Alice / 30

**Note:** Settable only inside the object initializer `new Person { ... }`, read-only afterwards.

Example 3 — `RequiredProp`: required property

csharp
// Program.cs
using CodingNow.Lecture.Oop07;

var u = new User { Email = "alice@example.com" };
Console.WriteLine(u.Email);

// var u2 = new User();   // missing Email → compile error
csharp
// User.cs
namespace CodingNow.Lecture.Oop07;

internal class User
{
    public required string Email { get; init; }
    public string DisplayName { get; init; } = "Anonymous";
}

**Output**

text
alice@example.com

**Note:** A `required` property must be given a value when constructing. Safe even without a constructor.

Example 4 — `FullProperty`: with validation

csharp
// Program.cs
using CodingNow.Lecture.Oop07;

var t = new Temperature();
t.Celsius = 25;
Console.WriteLine($"{t.Celsius}°C");

try
{
    t.Celsius = -500;   // below absolute zero → throws
}
catch (ArgumentException ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}
csharp
// Temperature.cs
namespace CodingNow.Lecture.Oop07;

internal class Temperature
{
    private double celsius;

    public double Celsius
    {
        get => celsius;
        set
        {
            if (value < -273.15)
                throw new ArgumentException("cannot go below absolute zero (-273.15°C).");
            celsius = value;
        }
    }
}

**Output**

text
25°C
Error: cannot go below absolute zero (-273.15°C).

**Note:** From outside you just write `t.Celsius = 25;` (field-like), but a method runs inside. That's the essence of a property.

Full example code (src/)

src/AutoProperty/AutoProperty.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/AutoProperty/Person.cs

csharp
namespace CodingNow.Lecture.Oop07;

internal class Person
{
    // auto-property: compiler generates the hidden field and get/set automatically.
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

src/AutoProperty/Program.cs

csharp
using CodingNow.Lecture.Oop07;

var p = new Person();
p.Name = "Alice";
p.Age = 30;

Console.WriteLine($"{p.Name} / {p.Age}");

src/FullProperty/FullProperty.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/FullProperty/Program.cs

csharp
using CodingNow.Lecture.Oop07;

var t = new Temperature();
t.Celsius = 25;
Console.WriteLine($"{t.Celsius}°C");

try
{
    t.Celsius = -500;   // below absolute zero → exception
}
catch (ArgumentException ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}

src/FullProperty/Temperature.cs

csharp
namespace CodingNow.Lecture.Oop07;

internal class Temperature
{
    // backing field: actual data lives here
    private double celsius;

    public double Celsius
    {
        get => celsius;
        set
        {
            // 'value' inside set is the reserved parameter for "the new incoming value"
            if (value < -273.15)
                throw new ArgumentException("cannot go below absolute zero (-273.15°C).");
            celsius = value;
        }
    }
}

src/GetInit/GetInit.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/GetInit/Person.cs

csharp
namespace CodingNow.Lecture.Oop07;

internal class Person
{
    // init: settable only during object initialization or constructor; read-only after.
    public string Name { get; init; } = "";
    public int Age { get; init; }
}

src/GetInit/Program.cs

csharp
using CodingNow.Lecture.Oop07;

// Settable only in the object initializer.
var alice = new Person { Name = "Alice", Age = 30 };
Console.WriteLine($"{alice.Name} / {alice.Age}");

// alice.Name = "Bob";   // init = no later mutation (uncommenting -> compile error)

src/RequiredProp/Program.cs

csharp
using CodingNow.Lecture.Oop07;

// A required property must be given a value at object creation.
var u = new User { Email = "alice@example.com" };
Console.WriteLine($"{u.Email} / {u.DisplayName}");

// var u2 = new User();   // Email missing → compile error

src/RequiredProp/RequiredProp.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/RequiredProp/User.cs

csharp
namespace CodingNow.Lecture.Oop07;

internal class User
{
    // required: must be given a value at object creation (C# 11+)
    public required string Email { get; init; }

    // optional with a default
    public string DisplayName { get; init; } = "Anonymous";
}

Common Mistakes

  1. Exposing fields raw like `public string Name;` — prefer a property whenever possible.
  2. Trying to reassign an `init` property from outside — only allowed at object initialization or inside the constructor.
  3. Forgetting to supply a `required` property during construction → compile error.
  4. Inside `set`, writing `Celsius = value;` (calling itself) — **infinite recursion** (StackOverflow). Always assign to the backing field (`celsius`).
  5. Trying to mutate a `readonly` field outside the constructor — compile error.

Summary

  • A property is "a method that looks like a field." Keeps the external API simple while hiding internal logic.
  • Use `init` for immutable data, `required` for mandatory input, and a full property + backing field when you need validation.
  • Encapsulation is about preventing outsiders from poking at internal state arbitrarily.

Practice

**Practice - 07. Properties and Encapsulation**

Problem 1 — `Temperature` (absolute-zero validation)

  • Project folder: `Homework01/`
  • Key concepts: full property, backing field, validation in `set`

Requirements

  • Add a `Celsius` property to a `Temperature` class.
  • On set, throw `ArgumentException` if the value is below `-273.15`.
  • Add a read-only `Fahrenheit` property. Formula: `°F = °C × 9/5 + 32`.
  • In `Program.cs` set valid and invalid values and print.

Expected output

text
0°C = 32°F
100°C = 212°F
Error: cannot go below absolute zero (-273.15°C).

Hints

  • Backing field as `private double celsius;`.
  • `Fahrenheit` as `get => celsius * 9 / 5 + 32;` (computed).

Problem 2 — `Product` (non-negative price validation)

  • Project folder: `Homework02/`
  • Key concepts: `required`, `init`, full property + validation

Requirements

  • Create a `Product` class.
  • Make `Name` a `required string ... { get; init; }`.
  • Make `Price` a full property; throw `ArgumentException` on a negative value.
  • Create with object initializer `new Product { Name = "...", Price = ... }`.

Expected output

text
Apple / 1500 won
Pear / 0 won
Error: price must be >= 0.

Hints

  • Backing field for `Price` is `private int price;`.
  • No default needed for an initial 0 price (`int` defaults to 0).

Check your answer

Try it yourself, then compare against the [`answer/`](./answer/) folder.

Answer (answer/)

homework/answer/Homework01/Homework01.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

homework/answer/Homework01/Program.cs

csharp
using CodingNow.Lecture.Oop07;

var t = new Temperature();

t.Celsius = 0;
Console.WriteLine($"{t.Celsius}°C = {t.Fahrenheit}°F");

t.Celsius = 100;
Console.WriteLine($"{t.Celsius}°C = {t.Fahrenheit}°F");

try
{
    t.Celsius = -300;
}
catch (ArgumentException ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}

homework/answer/Homework01/Temperature.cs

csharp
namespace CodingNow.Lecture.Oop07;

internal class Temperature
{
    private double celsius;

    public double Celsius
    {
        get => celsius;
        set
        {
            if (value < -273.15)
                throw new ArgumentException("cannot go below absolute zero (-273.15°C).");
            celsius = value;
        }
    }

    // read-only computed property
    public double Fahrenheit => celsius * 9 / 5 + 32;
}

homework/answer/Homework02/Homework02.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

homework/answer/Homework02/Product.cs

csharp
namespace CodingNow.Lecture.Oop07;

internal class Product
{
    // required: must be supplied at construction
    public required string Name { get; init; }

    private int price;
    public int Price
    {
        get => price;
        set
        {
            if (value < 0)
                throw new ArgumentException("price must be >= 0.");
            price = value;
        }
    }
}

homework/answer/Homework02/Program.cs

csharp
using CodingNow.Lecture.Oop07;

var apple = new Product { Name = "Apple" };
apple.Price = 1500;
Console.WriteLine($"{apple.Name} / {apple.Price} won");

var pear = new Product { Name = "Pear" };
Console.WriteLine($"{pear.Name} / {pear.Price} won");

try
{
    pear.Price = -100;
}
catch (ArgumentException ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}

Try It Yourself

bash
cd src/AutoProperty
dotnet run

cd ../GetInit
dotnet run

cd ../RequiredProp
dotnet run

cd ../FullProperty
dotnet run

Next Lecture

[08_Inheritance](../08_%EC%83%81%EC%86%8D/) — Extend an existing class to build a new one.

Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗