Inheritance in C#: A Field Guide with Real Examples (.NET 10)

Payroll code is where I see inheritance used well and abused most. Inheritance in C# lets a derived class reuse and extend a base class: write class Employee : Person, and every Employee gets the members of Person plus its own.

In this tutorial, I’ll map out how inheritance in C# really behaves on .NET 10: what gets inherited, constructor order, override versus new, multilevel classes, sealed types, records, and when to skip it.

My running example is a small US HR app with Person, Employee and Manager classes and salaries in US dollars.

What Is Inheritance in C#?

The class you inherit from is the base (parent) class, and the new one is the derived (child) class. It’s one of the pillars in my OOP concepts in C# guide, next to encapsulation and polymorphism.

The syntax is a colon after the class name. Here, Employee calls Greet() even though only Person declares it:

var emp = new Employee { Name = "Jenna Moore", City = "Seattle", Title = "Payroll Analyst" };

Console.WriteLine(emp.Greet());     // declared in Person
Console.WriteLine(emp.Badge());     // declared in Employee
Console.WriteLine(emp is Person);   // an Employee is a Person
Console.WriteLine(emp.ToString());  // inherited from System.Object

class Person
{
    public string Name { get; set; } = "";
    public string City { get; set; } = "";
    public string Greet() => $"Hi, I'm {Name} from {City}.";
}

class Employee : Person
{
    public string Title { get; set; } = "";
    public string Badge() => $"{Name} | {Title}";
}

Output:

Hi, I'm Jenna Moore from Seattle.
Jenna Moore | Payroll Analyst
True
Employee

That last line prints Employee because every class implicitly inherits from System.Object, which gives it ToString in C#, Equals and GetHashCode. The is check returns True because an Employee really is a Person.

Inheritance Rules in C#: What a Derived Class Gets (and Doesn’t)

A derived class gets every member of its base except the ones in the first row below. Whether it can see an inherited member depends on its access modifier:

Base class memberInherited?Visible in the derived class?
Instance constructors, static constructors, finalizersNoCall base constructors with base(...)
public membersYesYes, and part of the derived class’s public surface
protected membersYesYes, but not to outside code
internal membersYesOnly from the same assembly
private membersYes (the data is there)No, unless the derived class is nested inside the base

Here’s what happens when a derived class reaches for a private field. The protected property compiles; the private field stops the build:

var emp = new Employee();
Console.WriteLine(emp.ReadBand());

class Person
{
    private decimal _bonusPool = 25_000m;
    protected string PayBand { get; } = "B2";
}

class Employee : Person
{
    public string ReadBand() => PayBand;      // protected: fine
    public decimal ReadPool() => _bonusPool; // private: CS0122
}

Build output:

C:\DotNetDemo\InheritanceDemo\PrivateMembers.cs(13,34): error CS0122: 'Person._bonusPool' is inaccessible due to its protection level

The build failed. Fix the build errors and run again.

That’s encapsulation in C# doing its job. If a derived class truly needs a value, expose it as protected, ideally as a read-only property rather than a field.

Inheritance and Constructors: base() Chaining and Call Order

Constructors aren’t inherited, so each class writes its own and passes values up with : base(...). I added a WriteLine to each constructor so you can watch the order on a three-level chain:

var boss = new Manager("Ryan Carter", 98_000m);
Console.WriteLine($"Created {boss.Name}");
class Person
{
    public string Name { get; }
    public Person(string name)
    { Name = name; Console.WriteLine("1. Person"); }
}
class Employee : Person
{
    public decimal Salary { get; }
    public Employee(string name, decimal salary) : base(name)
    { Salary = salary; Console.WriteLine("2. Employee"); }
}
class Manager : Employee
{
    public Manager(string name, decimal salary)
        : base(name, salary) => Console.WriteLine("3. Manager");
}

Output:

1. Person
2. Employee
3. Manager
Created Ryan Carter
Inheritance in C# constructor call order: Person, Employee and Manager constructors with base() in VS Code
Base constructors run first: Person, then Employee, then Manager

The base constructor body always finishes before the derived one starts, so Person prints first even though I called new Manager(...). My guide to the constructor in C# covers this(...) chaining too.

Field note: If you leave out : base(...), the compiler calls the base’s parameterless constructor. When the base doesn’t have one, you get error CS7036, “There is no argument given that corresponds to the required parameter.”

virtual, override and new: Overriding vs Method Hiding in Inheritance

This is the part of inheritance that bites people in code reviews. override replaces a virtual method for every caller, while new only hides the base method when you call through the derived type:

using System.Globalization;
CultureInfo.CurrentCulture = new CultureInfo("en-US");
Employee e = new Manager { Name = "Zoe Ng", Pay = 112_500m };
Console.WriteLine(e.Describe());              // override wins
Console.WriteLine(e.PayLabel());              // new: base runs
Console.WriteLine(((Manager)e).PayLabel());   // new version
class Employee
{
    public string Name { get; set; } = "";
    public decimal Pay { get; set; }
    public virtual string Describe() => $"{Name}, employee";
    public string PayLabel() => $"Salary {Pay:C0}";
}
class Manager : Employee
{
    public override string Describe() =>
        base.Describe() + " (manager)";
    public new string PayLabel() => $"Salary {Pay:C0} + bonus";
}

Output:

Zoe Ng, employee (manager)
Salary $112,500
Salary $112,500 + bonus
C# inheritance override vs new method hiding called through an Employee base reference, output in VS Code
Through an Employee reference, the override runs but the hidden new method doesn’t

The variable e is typed Employee but holds a Manager. Describe() is virtual, so the runtime picks the Manager version; PayLabel() isn’t, so the compile-time type wins and you lose the bonus text.

Inside an override, base.Describe() reuses the parent’s logic. I go deeper in method overriding in C#; don’t confuse it with method overloading in C#, which is same name, different parameters.

Field note: Hiding a method without new still compiles, with warning CS0108 (or CS0114 when the base method is virtual). Treat that warning as “you probably meant override.”

Types of Inheritance in C#: Single, Multilevel and Hierarchical

C# allows one direct base class per class, but you can arrange classes in several shapes:

TypeShapeAllowed for classes?
SingleEmployee : PersonYes
MultilevelManager : Employee : PersonYes
HierarchicalEmployee and Contractor both : PersonYes
MultipleManager : Employee, ContractorNo, use interfaces
HybridA mix of the aboveOnly through interfaces

Multilevel Inheritance in C#: Person -> Employee -> Manager

Inheritance is transitive, so Manager gets members from Employee and from Person. Here’s a C# program to implement multilevel inheritance:

using System.Globalization;
CultureInfo.CurrentCulture = new CultureInfo("en-US");

var chloe = new Manager
    { Name = "Chloe Brady", Salary = 104_000m, Bonus = 12_000m };
Console.WriteLine(chloe.Name);                        // Person
Console.WriteLine($"Salary: {chloe.Salary:C0}");      // Employee
Console.WriteLine($"Total:  {chloe.TotalPay():C0}");  // Manager
class Person { public string Name { get; set; } = ""; }
class Employee : Person
{
    public decimal Salary { get; set; }
    public virtual decimal TotalPay() => Salary;
}
class Manager : Employee
{
    public decimal Bonus { get; set; }
    public override decimal TotalPay() => base.TotalPay() + Bonus;
}

Output:

Chloe Brady
Salary: $104,000
Total:  $116,000
C# program to implement multilevel inheritance with Person, Employee and Manager classes and USD salary output in VS Code
A Manager uses Name from Person, Salary from Employee and its own Bonus

TotalPay() calls base.TotalPay() and adds the bonus, so a change in Employee flows down automatically. I set en-US explicitly, because :C0 follows the machine’s culture and this PC isn’t set to US English.

Keep chains shallow. Past three levels, I find a change near the top tends to break something two classes down that nobody remembers.

Why Doesn’t C# Support Multiple Inheritance of Classes?

Two base classes could both define CalculatePay() with different state behind them, and the compiler would have to guess which one you meant. C# avoids that with one base class plus any number of interfaces.

I walk through the interface workaround, default interface methods included, in multiple inheritance in C#. Structs can’t inherit at all, though a structure in C# can implement interfaces.

Abstract Classes vs Interfaces in an Inheritance Hierarchy

Both define a contract, but they answer different questions:

Abstract classInterface
Relationship“is a” (Manager is an Employee)“can do” (can be paid, can be compared)
How many per classOneMany
Instance fieldsYesNo
ConstructorsYesNo

I use an abstract base when classes share real code and data, like Person further down. The details are in abstract class in C# and interface in C#.

Sealed Classes and Sealed Methods: Stopping Inheritance in C#

Mark a class sealed when nobody should derive from it. Trying anyway is a compile error, not a runtime surprise:

var i = new Intern { Name = "Trevor Lyons" };
Console.WriteLine(i.Describe());

class Employee
{
    public string Name { get; set; } = "";
    public virtual string Describe() => $"{Name}, employee";
}

sealed class Intern : Employee
{
    public override string Describe() => $"{Name}, summer intern";
}

class SummerIntern : Intern { }   // CS0509

Build output:

C:\DotNetDemo\InheritanceDemo\SealedClass.cs(15,22): error CS0509: 'SummerIntern': cannot derive from sealed type 'Intern'

The build failed. Fix the build errors and run again.

You can also seal a single override, public sealed override string Describe(), so classes further down can’t override it again. string is sealed, which is why you can’t inherit from it.

How Do You Check Derived Types in Inheritance with is, as and Patterns?

Hierarchical inheritance shows up the moment you store mixed types in one list. Here Employee and Contractor both derive from an abstract Person record, and a switch expression sorts them out:

Person[] staff = [new Employee("Jenna Moore", 76_000m),
    new Contractor("Ethan Rodriguez", 85m),
    new Employee("Dwayne Foster", 91_500m)];
foreach (Person p in staff)
{
    string kind = p switch
    {
        Employee { Salary: > 90_000m } => "senior employee",
        Employee => "employee",
        Contractor c => $"contractor at {c.Rate}/hour",
        _ => "person"
    };
    Console.WriteLine($"{p.Name}: {kind}");
}
var asEmp = staff[1] as Employee;
Console.WriteLine($"as Employee: {asEmp?.Name ?? "null"}");
abstract record Person(string Name);
record Employee(string Name, decimal Salary) : Person(Name);
record Contractor(string Name, decimal Rate) : Person(Name);

Output:

Jenna Moore: employee
Ethan Rodriguez: contractor at 85/hour
Dwayne Foster: senior employee
as Employee: null
C# hierarchical inheritance with a Person array, a switch expression with type patterns and the as operator, output in VS Code
Type patterns pick the right derived class; as returns null for a Contractor

Order matters: the { Salary: > 90_000m } property pattern has to come before the plain Employee case, or it never matches. The switch case in C# guide covers more patterns.

as returns null instead of throwing, while a direct cast throws InvalidCastException. My notes on type casting in C# compare the two.

Record Inheritance in C#

A record can inherit from another record, but not from a class, and a class can’t inherit from a record. Positional syntax passes values to the base record:

var p = new Person("Ryan Carter", "Ohio");
var e = new Employee("Ryan Carter", "Ohio", 88_000m);

Console.WriteLine(e);
Console.WriteLine(p == (Person)e);          // run-time types differ
Console.WriteLine(e with { State = "Utah" });

record Person(string Name, string State);
record Employee(string Name, string State, decimal Salary) : Person(Name, State);

Output:

Employee { Name = Ryan Carter, State = Ohio, Salary = 88000 }
False
Employee { Name = Ryan Carter, State = Utah, Salary = 88000 }

Equality includes the run-time type, so a Person and an Employee with the same name and state aren’t equal. The generated ToString and the with expression both know about the derived Salary.

Composition over Inheritance: When Not to Use Inheritance in C#

My rule: inherit only for a true “is a” relationship. If you’re tempted to write HourlyEmployee and SalariedEmployee, the difference is a behavior, not a kind of person.

Give the employee a pay policy instead. You avoid a subclass for every pay rule:

using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-US");

var hourly = new Employee("Kelly Hernandez", new HourlyPay(42m, 160));
var salaried = new Employee("Rebecca Hughes", new SalaryPay(96_000m));
Console.WriteLine($"{hourly.Name}: {hourly.Pay.Monthly():C0}/month");
Console.WriteLine($"{salaried.Name}: {salaried.Pay.Monthly():C0}/month");

interface IPayPolicy { decimal Monthly(); }
record HourlyPay(decimal Rate, int Hours) : IPayPolicy { public decimal Monthly() => Rate * Hours; }
record SalaryPay(decimal Annual) : IPayPolicy { public decimal Monthly() => Annual / 12; }
record Employee(string Name, IPayPolicy Pay);

Output:

Kelly Hernandez: $6,720/month
Rebecca Hughes: $8,000/month

Each Employee has a pay policy rather than being one. Adding a commission plan is a new record, not a new branch in the class tree.

Inheritance in C#: Which Tool Should You Reach For?

You want to…Use
Share code and data across related typesA base class (abstract if it shouldn’t be created)
Let a derived class change behaviorvirtual + override
Stop further derivationsealed
Give unrelated types a common abilityAn interface
Vary one behavior at run timeComposition
Immutable data with a parent shapeRecord inheritance

Want to round out your OOP toolkit? These pair well with this guide:

Frequently Asked Questions

What is inheritance in C# with an example?

Inheritance lets one class reuse another’s members. With class Employee : Person, an Employee object can call methods and properties declared in Person and add its own.

Are constructors inherited in C#?

No. Instance constructors, static constructors and finalizers aren’t inherited. A derived constructor calls a base constructor with : base(...), and the base constructor runs first.

What is the difference between override and new in C#?

override replaces a virtual method for every caller, including calls through a base-class variable. new only hides the base method, so a base-class variable still runs the base version.

Does C# support multiple inheritance?

Not for classes. A class has one direct base class but can implement any number of interfaces, which covers most multiple-inheritance needs.

What is multilevel inheritance in C#?

A chain where a class derives from a class that is itself derived, such as Manager : Employee : Person. The inheritance tutorial on Microsoft Learn calls this transitive inheritance.

Can a record inherit from a class in C#?

No. A record class can inherit only from another record class, and record structs can’t inherit at all. See C# record types on Microsoft Learn.