Skip to main content

Inheritance: Person, Student, Teacher, Guardian – C# | Lesson 39

Published / updated

Introduction​

Students, teachers, and guardians are all people. Each has a first name, a last name, and a full name, and each name must not be blank. Writing that code three times means fixing every future bug three times.

Inheritance lets one class build on another. A Person class holds what all people share, and Student, Teacher, and Guardian inherit it, adding only what is special about them. This is the second OOP pillar.

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

  • Create a base class and derived classes with :.
  • Call a base constructor with base(...).
  • Use protected for members meant for derived classes.
  • Store different derived objects in one base-type variable, and check their real type.
  • Explain when inheritance fits, and when it does not.

Why this matters​

The SMS has three tables with names: Students, Teachers, and Guardians. In C#, the name rules now live once, in Person, and every kind of person follows them automatically.

Inheritance is everywhere in .NET. Every exception inherits from Exception, which is how one catch can handle a whole family of errors.

Controllers in the Web API inherit from ControllerBase. Reading such code starts with understanding this lesson.

The concept​

Example: base and derived classes​

TermMeaningSMS example
Base classThe class being inherited fromPerson
Derived classThe class that inheritsStudent, Teacher, Guardian
class Student : Person"Student inherits from Person"
"is a"The test for inheritanceA student is a person

A derived class gets every member of its base class, and adds its own. Teacher has FullName from Person, plus EmployeeCode of its own.

Think of school uniforms. Every uniform has the school's shirt and badge (the base).

The sports uniform adds track pants, and the winter uniform adds a sweater. Nobody designs the badge again for each one.

Example: the base constructor​

A derived object is built from the bottom up: first the Person part, then the Student part. : base(firstName, lastName) tells C# which arguments to pass to Person's constructor:

Example
public Student(string admissionNumber, string firstName, string lastName, DateOnly dateOfBirth)
: base(firstName, lastName)
{
AdmissionNumber = RequireText(admissionNumber, nameof(AdmissionNumber));
DateOfBirth = dateOfBirth;
}

Person checks the names before Student's own code even runs. A student with a blank name still cannot be created. Constructors are the one thing a derived class does not inherit: each class declares its own, and chains to its base with base(...).

Example: protected members​

RequireText in Person is protected. Derived classes can call it, as Student does for the admission number, but code outside the family cannot. It is a tool for the family, not part of the public face.

Example: one class, one base​

In C#, a class can inherit from only one class. class TeacherParent : Teacher, Guardian fails with error CS1721: Class 'TeacherParent' cannot have multiple base classes.

A teacher who is also a parent is two separate records, linked by data, not one class with two parents. Lesson 41 shows interfaces, which a class can implement many of.

Example: what a derived class cannot do​

A derived class inherits members, but not access to private ones. If a Librarian : Person tries to set _firstName directly, the build fails with error CS0103: The name '_firstName' does not exist in the current context.

That is encapsulation (Lesson 38) working inside the family. A derived class must use the public FirstName property, so it passes through the same check as everyone else. Even a subclass cannot sneak a blank name past Person.

A derived class also cannot remove a member. Every Student has FullName, because every Person does.

Example: composition or inheritance?​

Inheritance is not the only way to reuse code. Composition means one object holds another, instead of being one:

RelationshipTestSMS exampleC#
Inheritance"is a"A guardian is a personclass Guardian : Person
Composition"has a"A fee invoice has paymentsA property holding payments
Composition"has a"A book issue has a bookBookIssue.Book (Lesson 37 homework)

When in doubt, prefer composition. It is more flexible: an object can change what it holds at run time, but it can never change what it inherits from.

The common advice "favor composition over inheritance" does not mean "never inherit". It means use inheritance only for true, permanent "is a" relationships, like the ones in this lesson.

Apply it to the SMS​

Person takes over the Id, the names, and FullName from Student:

src/SchoolApp.Core/Entities/Person.cs
namespace SchoolApp.Core.Entities;

// Lesson 39: what students, teachers, and guardians have in common.
// Each still has its own table in SchoolDb; inheritance shares C# code, not tables.
public class Person
{
private string _firstName = "";
private string _lastName = "";

public int Id { get; set; }

public string FirstName
{
get => _firstName;
set => _firstName = RequireText(value, nameof(FirstName));
}

public string LastName
{
get => _lastName;
set => _lastName = RequireText(value, nameof(LastName));
}

public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}

public string FullName => $"{FirstName} {LastName}";

// protected: usable here and in every class that inherits from Person.
protected static string RequireText(string value, string propertyName)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException($"{propertyName} is required.", propertyName);
}
return value.Trim();
}
}

Student shrinks to what makes a student a student:

src/SchoolApp.Core/Entities/Student.cs
namespace SchoolApp.Core.Entities;

// Lesson 37: a class describes what every student has (properties) and can do (methods).
// Lesson 38: names are checked and trimmed; the admission number can never change.
// Lesson 39: names, Id, and FullName now come from the Person base class.
public class Student : Person
{
public string AdmissionNumber { get; } // set once, in the constructor
public DateOnly DateOfBirth { get; set; }

// base(...) runs Person's constructor first, which checks the names.
public Student(string admissionNumber, string firstName, string lastName, DateOnly dateOfBirth)
: base(firstName, lastName)
{
AdmissionNumber = RequireText(admissionNumber, nameof(AdmissionNumber));
DateOfBirth = dateOfBirth;
}

// A method: the age on a given date, the same rule as Lesson 29's SQL task.
public int GetAgeOn(DateOnly date)
{
int age = date.Year - DateOfBirth.Year;
if (DateOfBirth > date.AddYears(-age))
{
age--; // the birthday has not happened yet in that year
}
return age;
}
}

Teacher and Guardian follow the same pattern. Their properties match the SchoolDb columns; a guardian's Email and Occupation are string?, because those columns allow NULL.

Show src/SchoolApp.Core/Entities/Teacher.cs
src/SchoolApp.Core/Entities/Teacher.cs
namespace SchoolApp.Core.Entities;

// Lesson 39: a teacher is a Person with an employee code and contact details.
public class Teacher : Person
{
public string EmployeeCode { get; }
public string Email { get; set; }
public string Phone { get; set; }

public Teacher(string employeeCode, string firstName, string lastName, string email, string phone)
: base(firstName, lastName)
{
EmployeeCode = RequireText(employeeCode, nameof(EmployeeCode));
Email = RequireText(email, nameof(Email));
Phone = RequireText(phone, nameof(Phone));
}
}
Show src/SchoolApp.Core/Entities/Guardian.cs
src/SchoolApp.Core/Entities/Guardian.cs
namespace SchoolApp.Core.Entities;

// Lesson 39: a guardian is a Person with a phone number; email and occupation are optional.
public class Guardian : Person
{
public string Phone { get; set; }
public string? Email { get; set; }
public string? Occupation { get; set; }

public Guardian(string firstName, string lastName, string phone)
: base(firstName, lastName)
{
Phone = RequireText(phone, nameof(Phone));
}
}

Exceptions form a family too. InvalidMarkException is a more specific SchoolRuleException:

src/SchoolApp.Core/Exceptions/InvalidMarkException.cs
namespace SchoolApp.Core.Exceptions;

// Lesson 39: a more specific SchoolRuleException, for error 50001.
public class InvalidMarkException : SchoolRuleException
{
public decimal Marks { get; }
public decimal MaxMarks { get; }

public InvalidMarkException(decimal marks, decimal maxMarks)
: base(50001, $"Marks must be between 0 and {maxMarks}; got {marks}.")
{
Marks = marks;
MaxMarks = maxMarks;
}
}
Show src/SchoolApp.Console/Program.cs
src/SchoolApp.Console/Program.cs
// Lesson 39: inheritance. Student, Teacher, and Guardian inherit from Person.
using SchoolApp.Core.Entities;
using SchoolApp.Core.Exceptions;

Student aarav = new Student("ADM-2017-0001", "Aarav", "Sharma", new DateOnly(2011, 4, 12));
Teacher anitha = new Teacher("TCH-001", "Anitha", "Rao", "anitha.rao@nexcodingschool.example", "9876540001");
Guardian rajesh = new Guardian("Rajesh", "Sharma", "9876540101") { Email = "rajesh.sharma@nexcodingschool.example" };

// 1. FullName is written once, in Person, and every derived class has it.
Console.WriteLine($"{aarav.FullName} | {anitha.FullName} | {rajesh.FullName}");

// 2. A Person variable can hold any kind of person.
Person[] people = { aarav, anitha, rajesh };
foreach (Person person in people)
{
string details = person switch
{
Student student => $"student {student.AdmissionNumber}",
Teacher teacher => $"teacher {teacher.EmployeeCode}",
Guardian guardian => $"guardian, phone {guardian.Phone}",
_ => "person"
};
Console.WriteLine($"{person.FullName,-14} {person.GetType().Name,-9} {details}");
}

// 3. The name rule in Person protects every derived class.
try
{
Guardian nameless = new Guardian(" ", "Sharma", "9876540102");
}
catch (ArgumentException ex)
{
Console.WriteLine($"Guardian refused: {ex.Message}");
}

// 4. Exceptions inherit too: catching the base type also catches InvalidMarkException.
try
{
throw new InvalidMarkException(105m, 100m);
}
catch (SchoolRuleException ex)
{
Console.WriteLine($"Caught {ex.GetType().Name}: error {ex.ErrorNumber}, {ex.Message}");
}

// 5. Every class inherits from object in the end.
Console.WriteLine($"Is aarav a Person? {aarav is Person}. An object? {aarav is object}.");
Output
Aarav Sharma | Anitha Rao | Rajesh Sharma
Aarav Sharma Student student ADM-2017-0001
Anitha Rao Teacher teacher TCH-001
Rajesh Sharma Guardian guardian, phone 9876540101
Guardian refused: FirstName is required. (Parameter 'FirstName')
Caught InvalidMarkException: error 50001, Marks must be between 0 and 100; got 105.
Is aarav a Person? True. An object? True.

Example: a Person variable holds any person​

Person[] people = { aarav, anitha, rajesh }; works because every student, teacher, and guardian is a person. Through a Person variable, you can use only Person members, such as FullName.

GetType().Name still reports the real type of each object. The object did not change when it was put in the array; only the variable's point of view did.

Example: checking the real type​

The switch uses type patterns: Student student => matches only students, and gives you a student variable of the right type, so student.AdmissionNumber works.

There are three ways to go from a Person variable back to a more specific type. Suppose person holds Anitha, a Teacher:

CodeResult when the object is not a Student
if (person is Student s)false; the block is skipped
person as Studentnull, which you must then check
(Student)personThrows InvalidCastException: Unable to cast object of type 'SchoolApp.Core.Entities.Teacher' to type 'SchoolApp.Core.Entities.Student'.

Prefer the is pattern: it checks and converts in one safe step. Going the other way, from Student to Person, never needs a cast, because every student is a person.

The same check works in an if: if (person is Teacher teacher). Use type checks sparingly. In Lesson 40, polymorphism lets each class answer for itself, without a switch that must list every type.

Example: inherited rules​

new Guardian(" ", "Sharma", ...) fails with "FirstName is required". The Guardian class contains no name check at all; it inherited the rule from Person. Fix or improve the rule once, and all three classes get it.

Example: what every class inherits from object​

Every class you write inherits from object, even without saying so. That is where these members come from:

MemberWhat it does by default
ToString()Returns the class name, such as SchoolApp.Core.Entities.Student (Lesson 37)
Equals(other)Compares references for classes
GetHashCode()A number used by dictionaries (Module 8)
GetType()The object's real type, used in this lesson's program

Lesson 40 shows how a class replaces the default ToString() with its own.

Example: exception families​

The program throws an InvalidMarkException, but catches SchoolRuleException. The catch works, because an InvalidMarkException is a SchoolRuleException.

ClassInherits from
InvalidMarkExceptionSchoolRuleException
SchoolRuleExceptionException
Exceptionobject

Callers choose their level: catch InvalidMarkException to handle marks specially, or SchoolRuleException to handle every school rule the same way.

Example: inheritance in classes, not in tables​

SchoolDb has no People table. Students, Teachers, and Guardians are separate tables, each with its own name columns. Person exists only in C#, to share code.

That is a normal, deliberate choice. In Module 9, data access tools fill the inherited properties of each class from its own table. The database design and the class design can differ, as long as the mapping between them is clear.

Common mistakes​

  1. Forgetting : base(...). When the base class has no empty constructor, the build fails: error CS7036: There is no argument given that corresponds to the required parameter 'firstName' of 'Person.Person(string, string)'. Fix: call base with the values.
  2. Calling a protected member from outside. Person.RequireText(...) in Program.cs fails with error CS0122: ... is inaccessible due to its protection level.
  3. Inheriting for code reuse when "is a" is false. A FeeInvoice is not a Student, even if both have an Id. Fix: inherit only for true "is a" relationships.
  4. Deep hierarchies. Five levels of inheritance are hard to follow. Fix: keep it to one or two levels.
  5. Long is chains instead of polymorphism. Every new type means editing every chain. Fix: Lesson 40.

In real projects​

From 16 years of .NET projects

On an early project, we built Person, then Staff : Person, then Teacher : Staff, then ClassTeacher : Teacher. A class teacher who stopped being one had to become a different object, with a different type.

Being a class teacher was a role for one year, not a kind of person. We replaced the deep tree with one Teacher class plus a ClassTeachers table, which is exactly how SchoolDb models it.

My test now: if something can change over time, it is data or a role, not a subclass.

Best practices​

  • Inherit only when "is a" is true and stays true for the object's whole life.
  • Put shared data and rules in the base class, and only differences in derived classes.
  • Keep hierarchies shallow: one or two levels.
  • Use protected for helpers meant for derived classes only.
  • Build exception families so callers can catch at the level they need.

Recap​

  • class Student : Person makes Student inherit everything in Person.
  • : base(...) passes values to the base constructor, which runs first.
  • protected members are visible to the class and its derived classes only.
  • A base-type variable can hold any derived object; type patterns find the real type.
  • A class has exactly one base class; all classes end at object.
  • Person shares C# code; SchoolDb still has three separate tables.

Homework​

Library module task: For this exercise, the library also lends magazines. Write a LibraryItem base class with a title, a loan period, and GetDueDate. Derive Book (with an author, 14 days) and Magazine (with an edition, 7 days), passing the loan period to the base constructor.

A correct result looks like:

Output
The Jungle Book Book loan 14 days, due 2026-07-20
Young Scientist Magazine loan 7 days, due 2026-07-13
Show the solution
library-assignment/039-library-inheritance.cs
// Library homework, Lesson 39: books and magazines share a LibraryItem base class.
// Run with: dotnet run library-assignment/039-library-inheritance.cs
DateOnly issuedOn = new DateOnly(2026, 7, 6);

LibraryItem[] items =
{
new Book("The Jungle Book", "Rudyard Kipling"),
new Magazine("Young Scientist", "July 2026"),
};

foreach (LibraryItem item in items)
{
Console.WriteLine($"{item.Title,-16} {item.GetType().Name,-9} loan {item.LoanDays,2} days, due {item.GetDueDate(issuedOn):yyyy-MM-dd}");
}

class LibraryItem
{
public string Title { get; }
public int LoanDays { get; }

public LibraryItem(string title, int loanDays)
{
Title = title;
LoanDays = loanDays;
}

public DateOnly GetDueDate(DateOnly issuedOn) => issuedOn.AddDays(LoanDays);
}

class Book : LibraryItem
{
public string Author { get; }

public Book(string title, string author) : base(title, 14)
{
Author = author;
}
}

class Magazine : LibraryItem
{
public string Edition { get; }

public Magazine(string title, string edition) : base(title, 7)
{
Edition = edition;
}
}

GetDueDate is written once, in LibraryItem. Each derived class only decides its loan period, through base(title, 14) or base(title, 7).

Quiz​

  1. What does class Student : Person mean?

    1. Student contains a Person
    2. Student inherits from Person
    3. Person inherits from Student
    4. Student and Person are the same
    Show answer

    Answer: B. Student inherits from Person Student gets all of Person's members and adds its own.

  2. What does : base(firstName, lastName) do in Student's constructor?

    1. Creates a second object
    2. Runs Person's constructor first with these values
    3. Copies a Person
    4. Calls a static method
    Show answer

    Answer: B. Runs Person's constructor first with these values The base part of the object is built first.

  3. Who can use a protected member of Person?

    1. Everyone
    2. Only Person
    3. Person and classes that inherit from it
    4. Only Program.cs
    Show answer

    Answer: C. Person and classes that inherit from it protected is for the class and its derived classes.

  4. An InvalidMarkException is thrown. Which catch block catches it?

    1. catch (FormatException)
    2. catch (SchoolRuleException)
    3. catch (ArgumentException)
    4. None
    Show answer

    Answer: B. catch (SchoolRuleException) InvalidMarkException inherits from SchoolRuleException.

  5. How many classes can a C# class inherit from directly?

    1. One
    2. Two
    3. Any number
    4. None
    Show answer

    Answer: A. One C# allows one base class; interfaces (Lesson 41) can be many.

Interview questions​

Questions are grouped by experience level. Click a question to see a model answer. All questions: OOP interview questions.

Fresher (0-2 yrs)​

F What is inheritance? concept

A class (the derived class) building on another (the base class), getting all its members and adding its own. Student : Person gets the names and FullName from Person and adds an admission number. Use it for true "is a" relationships.

See the example · Permalink

F What does the base keyword do in a constructor? concept

: base(...) calls the base class's constructor with the given arguments, before the derived constructor's own code runs. It is required when the base class has no parameterless constructor.

See the example · Permalink

F Does C# support multiple inheritance? concept

Not for classes: a class has exactly one base class (error CS1721 otherwise). A class can implement many interfaces, which covers most needs for multiple "kinds" of behavior.

See the example · Permalink

Mid (3-5 yrs)​

M What is the difference between private and protected? concept

private members are visible only inside the class itself, even derived classes cannot use them. protected members are visible to the class and every class that inherits from it, but not to outside code.

See the example · Permalink

M What is the difference between is, as, and a cast when converting a base type to a derived type? coding

is with a pattern (if (p is Student s)) checks and converts safely. as returns null when the conversion fails. A cast (Student)p throws InvalidCastException when the object is not a Student. Prefer the is pattern.

See the example · Permalink

M Why can catch (SchoolRuleException) catch an InvalidMarkException? concept

Because InvalidMarkException inherits from SchoolRuleException, so it is one. A catch block matches the exception's type or any base type, which lets callers handle a whole family of errors at once or a specific one separately.

See the example · Permalink

Senior (6-9 yrs)​

S A hierarchy Person > Staff > Teacher > ClassTeacher breaks when a teacher stops being a class teacher. What went wrong? scenario

A role that changes over time was modeled as a subclass, but an object cannot change its type. Replace the deep tree with one Teacher class plus data that records the role per year, such as a ClassTeachers table, and keep hierarchies shallow.

How to answer: Identify the misuse (role as type), the fix (composition or data), and the general rule: if something can change, it is data or a role, not a subclass.

See the example · Permalink

S Composition or inheritance: how do you choose? design

Inherit for true, permanent "is a" relationships, such as a guardian being a person. Use composition, holding another object, for "has a", such as an invoice having payments. Composition is more flexible, so prefer it when in doubt.

How to answer: Give the tests ("is a" vs "has a", permanence), examples of each, and the costs of wrong inheritance: tight coupling and fragile base classes.

See the example · Permalink

Architect / Lead (10+ yrs)​

A The C# model has a Person base class, but the database has separate Students, Teachers, and Guardians tables. Is that a problem? design

No. The class design shares code; the table design suits storage and queries. They may differ as long as the mapping is clear: each derived class maps to its own table, including the inherited columns. ORMs support several mapping strategies for hierarchies.

How to answer: Explain the object-relational mismatch, the table-per-concrete-type approach used here, and the alternatives (one shared table, or a table per type) with their trade-offs.

See the example · Permalink

Resources​

  • Code for this lesson: git tag lesson-39-inheritance (code repository coming soon)
  • Files: src/SchoolApp.Core/Entities/Person.cs, src/SchoolApp.Core/Entities/Student.cs, src/SchoolApp.Core/Entities/Teacher.cs, src/SchoolApp.Core/Entities/Guardian.cs, src/SchoolApp.Core/Exceptions/InvalidMarkException.cs, src/SchoolApp.Console/Program.cs, library-assignment/039-library-inheritance.cs
  • Video: coming soon

What's next​

The switch in this lesson must list every type of person. Next: polymorphism, where each class answers for itself, and fee reminders go by email or phone without any switch at all. Lesson 40: Polymorphism

Want live sessions, code reviews, and interview preparation? Join the next batch at Sahasra Technologies.