Inheritance: Person, Student, Teacher, Guardian – C# | Lesson 39
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
protectedfor 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
| Term | Meaning | SMS example |
|---|---|---|
| Base class | The class being inherited from | Person |
| Derived class | The class that inherits | Student, Teacher, Guardian |
class Student : Person | "Student inherits from Person" | |
| "is a" | The test for inheritance | A 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:
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:
| Relationship | Test | SMS example | C# |
|---|---|---|---|
| Inheritance | "is a" | A guardian is a person | class Guardian : Person |
| Composition | "has a" | A fee invoice has payments | A property holding payments |
| Composition | "has a" | A book issue has a book | BookIssue.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:
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:
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
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
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:
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
// 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}.");
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:
| Code | Result when the object is not a Student |
|---|---|
if (person is Student s) | false; the block is skipped |
person as Student | null, which you must then check |
(Student)person | Throws 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:
| Member | What 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.
| Class | Inherits from |
|---|---|
InvalidMarkException | SchoolRuleException |
SchoolRuleException | Exception |
Exception | object |
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
- 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: callbasewith the values. - Calling a protected member from outside.
Person.RequireText(...)inProgram.csfails witherror CS0122: ... is inaccessible due to its protection level. - Inheriting for code reuse when "is a" is false. A
FeeInvoiceis not aStudent, even if both have an Id. Fix: inherit only for true "is a" relationships. - Deep hierarchies. Five levels of inheritance are hard to follow. Fix: keep it to one or two levels.
- Long
ischains instead of polymorphism. Every new type means editing every chain. Fix: Lesson 40.
In real 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
protectedfor helpers meant for derived classes only. - Build exception families so callers can catch at the level they need.
Recap
class Student : PersonmakesStudentinherit everything inPerson.: base(...)passes values to the base constructor, which runs first.protectedmembers 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. Personshares 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:
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 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
What does class Student : Person mean?
Show answer
Answer: B. Student inherits from Person Student gets all of Person's members and adds its own.
What does : base(firstName, lastName) do in Student's constructor?
Show answer
Answer: B. Runs Person's constructor first with these values The base part of the object is built first.
Who can use a protected member of Person?
Show answer
Answer: C. Person and classes that inherit from it protected is for the class and its derived classes.
An InvalidMarkException is thrown. Which catch block catches it?
Show answer
Answer: B. catch (SchoolRuleException) InvalidMarkException inherits from SchoolRuleException.
How many classes can a C# class inherit from directly?
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.