Backend Development with .NET
Session 05
OOP I: Classes, Encapsulation
& Inheritance
Eng. Seif Mansour  ·  Andalusia Academy
Week 3  ·  2.5 hours
Session Goals

By the end of this session, you will be able to:

  • Define classes with constructors, fields, and properties
  • Apply access modifiers to enforce encapsulation
  • Use static members for shared class-level state and behaviour
  • Build class hierarchies using inheritance
  • Override methods in derived classes correctly
Agenda
TimeSegmentTypeDuration
0:00Classes & objectsTheory + Demo25 min
0:25Constructors & the this keywordTheory + Demo20 min
0:45Properties & access modifiersTheory + Demo25 min
1:10Break10 min
1:20Static membersTheory + Demo15 min
1:35Inheritance & the base keywordTheory + Demo25 min
2:00virtual & overrideTheory + Demo20 min
2:20LabLab10 min
Classes & Objects
A class is a blueprint. An object is an instance created from that blueprint — with its own independent copy of the data.
In this section
  • Blueprint vs. instance
  • Fields and methods
  • new keyword
  • Object initializer syntax
Class Anatomy
  • A class groups data (fields) with behaviour (methods)
  • Fields hold values that each instance owns — e.g., Brand, Year
  • Methods define what the object can do — e.g., StartEngine()
  • The public modifier on the class makes it accessible from other files
public class Car
{
    public string Brand;
    public int Year;

    public void StartEngine()
    {
        Console.WriteLine(
            $"{Brand} engine started.");
    }
}
Creating Objects
  • new ClassName() allocates a fresh instance on the heap
  • Each instance has its own copy of instance fields
  • Set fields one by one, or use an object initializer to set them in one expression
Tip
Object initializer syntax is equivalent to setting fields one at a time — it is just more compact and readable at the call site.
Car car1 = new Car();
car1.Brand = "Toyota";
car1.Year  = 2022;
car1.StartEngine();
// "Toyota engine started."

// Object initializer — same result
Car car2 = new Car
{
    Brand = "BMW",
    Year  = 2024
};
car2.StartEngine();
// "BMW engine started."
Constructors & the this Keyword
Constructors guarantee an object starts in a valid state. this refers to the current instance and chains constructors together.
In this section
  • Parameterized constructors
  • Constructor overloading
  • Chaining with this(...)
  • this as current instance reference
Constructors
  • A constructor runs automatically when an object is created
  • Same name as the class, no return type
  • If you define no constructor, C# provides a parameterless default
  • Use it to guarantee the object starts in a fully-initialised, valid state
public class Person
{
    public string Name;
    public int Age;

    public Person(string name, int age)
    {
        Name = name;
        Age  = age;
    }
}

// Constructor is called here
Person p = new Person("Alice", 30);
Constructor Chaining
Models/Person.cs
public class Person
{
    public string Name;
    public int Age;
    public string Email;

    // Delegates to the three-parameter constructor
    public Person(string name, int age) : this(name, age, "")
    {
    }

    public Person(string name, int age, string email)
    {
        Name  = name;
        Age   = age;
        Email = email;
    }
}

this(...) calls another constructor in the same class — avoids duplicating initialisation logic across overloads.

The this Keyword
  • this refers to the current instance of the class
  • Useful for disambiguating a parameter name from a field of the same name
  • Returning this from a method enables method chaining — calling multiple methods in one expression
  • You already saw this(...) used to chain constructors in the previous slide
public class Counter
{
    private int _count;

    public void Increment() => _count++;

    public Counter Reset()
    {
        _count = 0;
        return this;   // same object — enables chaining
    }
}

var c = new Counter();
c.Increment();
c.Increment();
c.Reset().Increment();   // chain calls
Properties & Access Modifiers
Encapsulation — hide internal data and expose only a controlled interface. Access modifiers enforce the boundary.
In this section
  • Access modifiers table
  • Auto-implemented properties
  • Full properties with backing field
  • Computed read-only properties
Access Modifiers
ModifierAccessible from
publicAnywhere
privateOnly inside the same class
protectedSame class and derived classes
internalSame assembly (project)
protected internalSame assembly or derived classes
Rule of thumb
Fields should almost always be private. Expose data through properties — that way you can add validation later without breaking any code that uses the class.
Auto-implemented Properties
  • A property exposes a field through get and set accessors
  • Auto-implemented: the compiler creates a hidden backing field automatically
  • private set restricts writes to inside the class
  • Callers stay shielded from the internal representation — you can change it later without breaking them
public class Product
{
    public string  Name  { get; set; }
    public decimal Price { get; set; }

    // Only settable from inside this class
    public int Stock { get; private set; }

    public void Restock(int units)
    {
        Stock += units;
    }
}
Full Properties with Backing Fields
Models/Product.cs
public class Product
{
    private decimal _price;

    public decimal Price
    {
        get => _price;
        set
        {
            if (value < 0)
                throw new ArgumentException(
                    "Price cannot be negative.");
            _price = value;
        }
    }
}

Use a backing field when you need validation, computed transformations, or side effects inside get or set.

Computed (Read-only) Properties
  • A property with only get is read-only from outside the class
  • Expression-bodied => expr computes the value on demand — no stored field needed
  • Derived values should be properties, not fields — they stay in sync automatically when dependencies change
Tip
If a value can always be computed from other properties, do not store it — a read-only property keeps it fresh for free.
public class Rectangle
{
    public double Width  { get; set; }
    public double Height { get; set; }

    // Computed — no backing field required
    public double Area
        => Width * Height;

    public double Perimeter
        => 2 * (Width + Height);
}

var r = new Rectangle
    { Width = 4, Height = 6 };

Console.WriteLine(r.Area);       // 24
Console.WriteLine(r.Perimeter);  // 20
Key Concept
"Hide the data; expose the behaviour. The public surface of a class is a promise — once broken, everything that depends on it breaks too."
— Encapsulation  ·  Session 05
Static Members
A static member belongs to the class itself, not to any instance — one shared copy across the entire application.
In this section
  • Static fields and methods
  • Utility / helper classes
  • Shared state across all instances
  • When to use static vs. instance
Static Members — Utility Methods
  • Accessed through the class name, not through an object reference
  • Static methods cannot access instance members — there is no this
  • Useful for stateless helpers that logically belong to a type but need no instance
You already use these
Console.WriteLine, Math.Sqrt, and string.Join are all static methods.
public class MathHelper
{
    public static double Pi = 3.14159;

    public static int Square(int n) => n * n;

    public static double CircleArea(double r)
        => Pi * r * r;
}

// Called on the class, not on an instance
double pi = MathHelper.Pi;
int    sq = MathHelper.Square(5);     // 25
double a  = MathHelper.CircleArea(3); // 28.27...
Static Shared State
Models/User.cs
public class User
{
    private static int _totalCreated = 0;   // one copy — shared by all instances

    public string Name { get; set; }

    public User(string name)
    {
        Name = name;
        _totalCreated++;                     // every constructor call increments it
    }

    public static int TotalCreated => _totalCreated;
}

var u1 = new User("Alice");
var u2 = new User("Bob");

Console.WriteLine(User.TotalCreated);  // 2

Static fields are shared — every instance reads and writes the same memory location.

Inheritance & the base Keyword
A derived class reuses and extends a base class — modelling an IS-A relationship without duplicating code.
Animal Dog

Dog IS-A Animal

Inheritance
  • The : syntax declares a derived class
  • All non-private members of the base class are inherited automatically
  • The derived class can add new fields, properties, and methods
  • Models an IS-A relationship — a Dog IS-A Animal
public class Animal
{
    public string Name { get; set; }

    public Animal(string name)
    {
        Name = name;
    }

    public void Breathe()
    {
        Console.WriteLine(
            $"{Name} is breathing.");
    }
}
Derived Classes & the base Keyword
  • base(args) in a constructor calls the parent's constructor
  • The parent constructor always runs before the derived constructor body
  • base.MethodName() inside a method calls the parent's implementation
public class Dog : Animal
{
    public string Breed { get; set; }

    public Dog(string name, string breed)
        : base(name)   // calls Animal(string name)
    {
        Breed = breed;
    }

    public void Bark()
        => Console.WriteLine($"{Name} says: Woof!");
}

Dog d = new Dog("Rex", "Labrador");
d.Breathe();   // inherited — "Rex is breathing."
d.Bark();      // "Rex says: Woof!"
virtual & override
Mark a method virtual to allow derived classes to replace it. Use override to supply the new implementation.
Shape Circle Rectangle
virtual & override
  • Methods are non-virtual by default — a derived class cannot replace them
  • virtual on a base-class method signals "derived classes may supply their own implementation"
  • override in the derived class supplies that implementation — the compiler requires the keyword explicitly, preventing accidental hiding
  • At runtime the most-derived override is always called, even when the variable type is the base class
  • This is runtime polymorphism — the same call produces different behaviour depending on the actual object type
Shape Hierarchy
Models/Shapes.cs
public class Shape
{
    public virtual double Area() => 0;
}

public class Circle : Shape
{
    public double Radius { get; set; }
    public Circle(double r) => Radius = r;
    public override double Area()
        => Math.PI * Radius * Radius;
}

public class Rectangle : Shape
{
    public double Width  { get; set; }
    public double Height { get; set; }
    public Rectangle(double w, double h)
    { Width = w; Height = h; }
    public override double Area() => Width * Height;
}
Polymorphism in Action
Program.cs
Shape[] shapes =
{
    new Circle(5),
    new Rectangle(4, 6)
};

foreach (Shape s in shapes)
{
    Console.WriteLine($"Area: {s.Area():F2}");
}

// Output:
// Area: 78.54   (Circle)
// Area: 24.00   (Rectangle)

The variable type is Shape, but the object's actual type decides which Area() runs.

sealed
  • sealed on a class prevents any class from inheriting it
  • sealed override on a method stops further overriding down the hierarchy
  • Use sparingly — over-sealing limits future extensibility
  • string and DateTime in the .NET base library are both sealed
// No class may inherit from ImmutablePoint
public sealed class ImmutablePoint
{
    public int X { get; }
    public int Y { get; }

    public ImmutablePoint(int x, int y)
    {
        X = x;
        Y = y;
    }
}

// Compile error:
// public class Special : ImmutablePoint { }
Lab — WorkItem Class Hierarchy
  1. Create a base class WorkItem with properties Id (int), Title (string), CreatedAt (DateTime), and a virtual string Summarize() method
  2. Derive TaskItem : WorkItem — add IsCompleted (bool) and DueDate (DateTime?). Override Summarize() to include the completion status
  3. Derive BugReport : WorkItem — add Severity (string). Override Summarize() to include the severity level
  4. Instantiate both types, store them in a WorkItem[], and loop over the array printing each summary
Summary
  • A class is a blueprint — each instance gets its own data; constructors guarantee a valid starting state
  • Fields should be private; expose them through properties so callers are shielded from internal changes
  • Auto-implemented properties are concise; use a full property with a backing field when you need validation or side effects
  • Static members belong to the class — one shared copy, accessed through the class name, not an instance
  • A derived class inherits all non-private members; base(args) delegates to the parent constructor
  • Mark methods virtual to allow overriding — at runtime the most-derived override always wins (polymorphism)
What's Next

Next session: Session 06 — OOP II: Polymorphism, Abstraction & Interfaces

  • Abstract classes and abstract methods
  • Interfaces and multiple-interface implementation
  • Casting with is and as
  • Polymorphism patterns used throughout the .NET standard library
Before next session
Complete the lab — the WorkItem hierarchy will be refactored into an abstract class next session. The assignment below is good preparation.
Assignment

Build a BankAccount class hierarchy that exercises encapsulation, properties, inheritance, and method overriding.

  • Create BankAccount with a private decimal _balance field, a read-only Balance property, an Owner string, and Deposit / Withdraw methods that validate their arguments
  • Add a virtual string GetAccountType() method that returns "Standard"
  • Derive SavingsAccount : BankAccount — add InterestRate (decimal) and ApplyInterest(); override GetAccountType() to return "Savings"
  • Store a BankAccount and a SavingsAccount in a BankAccount[] and print GetAccountType() and Balance for each
  • Attempting to set Balance directly from outside the class should not compile — demonstrate this in a comment
Bonus
Derive PremiumSavingsAccount : SavingsAccount that doubles the interest rate on each ApplyInterest() call and overrides GetAccountType() to return "Premium Savings".
Questions?
Session 05  ·  OOP I: Classes, Encapsulation & Inheritance