Object-oriented programming is at the heart of C#, and mastering its four pillars, encapsulation, inheritance, polymorphism, and abstraction, is essential for writing well-structured, maintainable code.
This collection of 40 C# OOP exercises walks you through designing classes, building inheritance hierarchies, overriding methods, and working with interfaces and abstract classes.
Each exercise includes a Practice Problem, Purpose, Hint, and a full Solution with Explanation, so you understand the design reasoning behind each pattern, not just the code.
Also, See: C# Exercises with 22 topic-wise sets and 620+ practice questions.
What You’ll Practice
- Encapsulation: Private fields, properties, and controlled access.
- Inheritance: Base and derived classes,
virtual/overridemethods. - Polymorphism: Method overriding and interface-based design.
- Abstraction: Abstract classes, interfaces, and constructors.
+ Table Of Contents (40 Exercises)
Table of contents
- Exercise 1: The Digital Watch (Class Fields & Methods)
- Exercise 2: Library Book Tracker (Multiple Constructors)
- Exercise 3: Bank Account Snapshot (Read-Only Fields)
- Exercise 4: The Singleton Configuration (Singleton Pattern)
- Exercise 5: Smart Home Thermostat (Private Fields & Public Methods)
- Exercise 6: E-Commerce Product (Internal Access Modifier)
- Exercise 7: Static Math Utility (Static Class & Methods)
- Exercise 8: Destructor Cleanup (Finalizers)
- Exercise 9: Secure User Profile (Custom Property Get/Set)
- Exercise 10: The Immutable Rectangle (Init-Only Properties)
- Exercise 11: Auto-Property Rental Car (Auto-Properties vs Backing Fields)
- Exercise 12: Smart Wardrobe Temperature (Validated Property Setter)
- Exercise 13: Calculated Circle (Read-Only Computed Property)
- Exercise 14: Flight Reservation (Private Set with Controlled Method)
- Exercise 15: Game Character Health (Property Triggering Side Effects)
- Exercise 16: Bank Yield Calculator (Custom Set Logic with Bonus)
- Exercise 17: Vehicle Hierarchy (Inheritance & Method Overriding)
- Exercise 18: The Sealed Security System (Sealed Classes)
- Exercise 19: Employee Payroll System (Virtual & Override Across Multiple Subclasses)
- Exercise 20: Base Constructor Chaining (The base Keyword)
- Exercise 21: The Sealed Method (Sealed Overrides)
- Exercise 22: Zoo Soundboard (Runtime Polymorphism)
- Exercise 23: Shadowing vs Overriding (The new Keyword)
- Exercise 24: Multi-level Appliance (Multi-level Inheritance & base Calls)
- Exercise 25: Dynamic UI Rendering (Polymorphic Collections)
- Exercise 26: Database Connector (Abstract Classes & Methods)
- Exercise 27: Abstract Shape Factory (Abstract Methods with Different Data)
- Exercise 28: Coffee Machine Blueprint (Mixing Concrete & Abstract Methods)
- Exercise 29: Document Parser (Abstract Class with a Constructor)
- Exercise 30: Online Payment Gateway (Shared Helper Method in an Abstract Class)
- Exercise 31: Game AI Behavior (Abstract Methods & Abstract Properties)
- Exercise 32: The Print & Scan Combo (Multiple Interface Implementation)
- Exercise 33: Explicit Conflict Resolution (Explicit Interface Implementation)
- Exercise 34: Default Logger Logic (Default Interface Methods)
- Exercise 35: Plugin Architecture (Interface-Based Extensibility)
- Exercise 36: Sortable Inventory (IComparable<T>)
- Exercise 37: Disposable Resources (IDisposable)
- Exercise 38: Abstract Class vs Interface (Combining Both)
- Exercise 39: Explicit Interface Hiding
Exercise 1: The Digital Watch (Class Fields & Methods)
Practice Problem: Create a DigitalWatch class with fields for hours and minutes. Implement a method to add time and print the current time.
Purpose: This exercise helps you practice combining fields and methods inside a class, where a method reads and updates the object’s own data.
Given Input: Starting time 10:45, add 30 minutes
Expected Output: 11:15
▼ Hint
- Give the class two public fields:
HoursandMinutes. - In the
AddTime()method, add the given minutes toMinutes, then use integer division and the modulus operator to roll extra minutes over intoHours. - Use the
D2format specifier so single-digit hours or minutes still display with a leading zero.
▼ Solution & Explanation
Explanation:
Hours += Minutes / 60;: Adds any whole hours produced by the extra minutes to the current hour count.Minutes = Minutes % 60;: Keeps only the leftover minutes that don’t make up a full hour.$"{Hours:D2}:{Minutes:D2}": Formats both numbers with at least two digits, so 9 displays as “09”.
Exercise 2: Library Book Tracker (Multiple Constructors)
Practice Problem: Build a Book class. Use multiple constructors: a default constructor, one taking just the title and author, and one taking title, author, and ISBN.
Purpose: This exercise helps you practice constructor overloading, giving a class several ways to be created depending on how much information is available.
Given Input: book1 (no arguments), book2 = ("1984", "George Orwell"), book3 = ("Dune", "Frank Herbert", "978-0441172719")
Expected Output:
Title: Unknown Title, Author: Unknown Author, ISBN: N/A Title: 1984, Author: George Orwell, ISBN: N/A Title: Dune, Author: Frank Herbert, ISBN: 978-0441172719
▼ Hint
- A class can have multiple constructors as long as each one has a different set of parameters.
- The default constructor can set placeholder values like “Unknown Title” instead of leaving fields empty.
- Use
: this(...)to have a simpler constructor call a more complete one, avoiding repeated code.
▼ Solution & Explanation
Explanation:
public Book(): The default constructor, called when no arguments are supplied, sets placeholder values for every property.public Book(string title, string author) : this(): Calls the default constructor first to setISBNto “N/A”, then overwritesTitleandAuthorwith the supplied values.public Book(string title, string author, string isbn): The most complete constructor, used when all three pieces of information are known upfront.
Exercise 3: Bank Account Snapshot (Read-Only Fields)
Practice Problem: Create an Account class where the account number can only be set via the constructor and is read-only afterward (using readonly fields).
Purpose: This exercise helps you practice using the readonly keyword, which locks a field’s value in place after construction, preventing it from ever being changed again.
Given Input: accountNumber = "ACC-10293"
Expected Output: Account Number: ACC-10293
▼ Hint
- Declare the field as
public readonly string AccountNumber;. - A
readonlyfield can only be assigned inside the constructor or at its declaration, never afterward. - Assign the value inside the constructor using the parameter passed in when the object is created.
▼ Solution & Explanation
Explanation:
public readonly string AccountNumber;: Declares a field that can only ever be assigned once, either here or inside the constructor.AccountNumber = accountNumber;: Sets the field’s permanent value the moment the object is created; any attempt to change it later would fail to compile.
Exercise 4: The Singleton Configuration (Singleton Pattern)
Practice Problem: Create a ConfigurationManager class that uses a private constructor and a static method to ensure only one instance ever exists.
Purpose: This exercise helps you practice the Singleton pattern, a common design pattern that guarantees a class has exactly one shared instance throughout a program.
Given Input: None
Expected Output: Same instance: True
▼ Hint
- Mark the constructor
privateso no code outside the class can callnew ConfigurationManager(). - Keep a
private staticfield that holds the single instance, starting asnull. - In a
public staticmethod, create the instance only if it doesn’t exist yet, then always return that same stored instance.
▼ Solution & Explanation
Explanation:
private ConfigurationManager() { }: Prevents any code outside the class from creating new instances directly with thenewkeyword.if (instance == null): Creates the single instance only the first timeGetInstance()is called.config1 == config2: Confirms that both variables refer to the exact same object in memory, proving only one instance was ever created.
Exercise 5: Smart Home Thermostat (Private Fields & Public Methods)
Practice Problem: Implement a Thermostat class where the internal temperature history array is private, but public methods allow adding new readings.
Purpose: This exercise helps you practice encapsulating an array inside a class, exposing only controlled methods to add and view its data instead of the array itself.
Given Input: Readings 68.5, 70.2, 69.0
Expected Output: Temperature History: 68.5, 70.2, 69
▼ Hint
- Declare the array as
privateso it cannot be accessed or modified directly from outside the class. - Track a separate
countfield to know how many readings have been added so far. - In
AddReading(), check there is still room before storing the new value at the current count, then increase the count.
▼ Solution & Explanation
Explanation:
private double[] temperatureHistory = new double[5];: Hides the array from outside code, so it can only be modified through the class’s own methods.AddReading(double temperature): The only way to add data to the array from outside the class, with a bounds check to avoid overflowing it.PrintHistory(): Provides safe, read-only access to the stored readings without ever exposing the private array itself.
Exercise 6: E-Commerce Product (Internal Access Modifier)
Practice Problem: Create a Product class utilizing internal modifiers to ensure the product’s discount logic is only accessible within its own project assembly.
Purpose: This exercise helps you practice the internal access modifier, which restricts visibility to code within the same project, striking a middle ground between public and private.
Given Input: Name = "Wireless Mouse", Price = 25.00
Expected Output: Discounted Price for Wireless Mouse: 22.5
▼ Hint
- Mark
NameandPriceaspublicso any code can read the product’s basic details. - Mark the discount calculation method as
internalinstead ofpublic. - An
internalmember can still be called fromMainhere, since both classes live in the same project, but it would not be visible to code in a different project referencing this one as a library.
▼ Solution & Explanation
Explanation:
internal double CalculateDiscountedPrice(): Restricts this method’s visibility to code within the same project, keeping the discount logic hidden from external consumers of the assembly.product.CalculateDiscountedPrice(): Works here becauseProgramandProductare both part of the same project, so theinternalrestriction does not block the call.
Exercise 7: Static Math Utility (Static Class & Methods)
Practice Problem: Design a MathWizard class that is marked static, containing static methods for calculating factorials and absolute differences, without instantiation.
Purpose: This exercise helps you practice building a static utility class, useful for grouping related helper methods that don’t need any per-object state.
Given Input: Factorial(5), AbsoluteDifference(10, 3)
Expected Output:
Factorial of 5 = 120 Absolute Difference = 7
▼ Hint
- Mark the whole class as
static, which means it can never be instantiated withnew. - Every member of a
staticclass must also bestatic. - Call the methods directly on the class name, e.g.
MathWizard.Factorial(5), without ever creating an object.
▼ Solution & Explanation
Explanation:
static class MathWizard: Declares a class that can never be instantiated, since it only exists to hold related helper methods.MathWizard.Factorial(5): Calls the method directly through the class name, without ever needing an object reference.Math.Abs(a - b): Uses the built-inMath.Abs()method to guarantee a non-negative result regardless of which number is larger.
Exercise 8: Destructor Cleanup (Finalizers)
Practice Problem: Create a ResourceWrapper class that simulates opening a file. Implement a finalizer/destructor (~ResourceWrapper()) that prints a message when the garbage collector claims the object.
Purpose: This exercise helps you practice writing a finalizer, a special method that C#’s garbage collector calls automatically before reclaiming an object’s memory.
Given Input: None
Expected Output:
File opened. File handle cleaned up by garbage collector.
▼ Hint
- Write the finalizer using a tilde followed by the class name:
~ResourceWrapper(), with no access modifier. - Set the object reference to
nullafter use so nothing is holding onto it anymore. - Call
GC.Collect()followed byGC.WaitForPendingFinalizers()to force the finalizer to run immediately for this demonstration, since it normally runs at an unpredictable time.
▼ Solution & Explanation
Explanation:
~ResourceWrapper(): Defines a finalizer, which C# calls automatically when the garbage collector is about to reclaim the object’s memory.resource = null;: Removes the only reference to the object, making it eligible for garbage collection.GC.Collect(); GC.WaitForPendingFinalizers();: Forces an immediate garbage collection cycle purely so the finalizer’s message is visible right away, which would not normally be done in real applications.
Exercise 9: Secure User Profile (Custom Property Get/Set)
Practice Problem: Create a UserProfile class where the Password property can be set, but reading it always returns "********".
Purpose: This exercise helps you practice writing a custom property accessor, where the get and set blocks don’t simply mirror a single backing field.
Given Input: Password = "MySecret123"
Expected Output: ********
▼ Hint
- Use a private backing field to actually store the real password behind the scenes.
- In the
setblock, assign the incoming value to the private field as normal. - In the
getblock, ignore the stored value entirely and always return the fixed string"********".
▼ Solution & Explanation
Explanation:
private string password;: A hidden backing field that actually holds the real password value.set { password = value; }: Stores whatever value is assigned to thePasswordproperty in the private field.get { return "********"; }: Always returns a masked placeholder instead of the real stored value, regardless of what was set.
Exercise 10: The Immutable Rectangle (Init-Only Properties)
Practice Problem: Design a Rectangle class using init accessors for Width and Height, ensuring they can only be set during object initialization and never changed again.
Purpose: This exercise helps you practice the init accessor, a modern C# feature that allows a property to be set only when the object is first created.
Given Input: Width = 10, Height = 5
Expected Output: Width: 10, Height: 5
▼ Hint
- Use
{ get; init; }instead of{ get; set; }for both properties. - Set the values using object initializer syntax when creating the object:
new Rectangle { Width = 10, Height = 5 }. - Any attempt to assign
rect.Widthafter that point would fail to compile, sinceinitonly allows assignment during initialization.
▼ Solution & Explanation
Explanation:
public double Width { get; init; }: Allows the property to be read at any time, but only written once, during object initialization.new Rectangle { Width = 10, Height = 5 }: Sets both properties using object initializer syntax at the moment the object is created, which is the only timeinitproperties can be assigned.
Exercise 11: Auto-Property Rental Car (Auto-Properties vs Backing Fields)
Practice Problem: Create a RentalCar class using auto-properties for Make and Model, but use a backing field for Odometer to ensure it can never be rolled backward.
Purpose: This exercise helps you practice mixing simple auto-properties with a manually written property, used whenever a setter needs custom validation logic.
Given Input: Set Odometer to 5000, then attempt to set it to 3000
Expected Output: Odometer: 5000
▼ Hint
- Use
{ get; set; }forMakeandModel, since they don’t need any special validation. - For
Odometer, declare a private backing field and write a full property with a customsetblock. - Inside the setter, only update the backing field if the new value is greater than or equal to the current one.
▼ Solution & Explanation
Explanation:
public string Make { get; set; }: An auto-property that lets the compiler generate a hidden backing field automatically, since no custom logic is needed.if (value >= odometer): Only allows the odometer to be updated if the new reading is equal to or higher than the current one.car.Odometer = 3000;: This assignment is silently ignored because 3000 is less than the current value of 5000.
Exercise 12: Smart Wardrobe Temperature (Validated Property Setter)
Practice Problem: Create a Clothing class with a TargetTemperature property. Throw an ArgumentException in the set accessor if the value falls outside 15°C to 30°C.
Purpose: This exercise helps you practice validating input inside a property setter, rejecting invalid values before they are ever stored.
Given Input: Attempt to set TargetTemperature to 40, then set it to 22
Expected Output:
Error: Temperature must be between 15 and 30 degrees Celsius. Target Temperature: 22
▼ Hint
- Inside the
setblock, check whethervalueis less than 15 or greater than 30. - If the value is out of range, use
throw new ArgumentException("...")instead of storing it. - Wrap the invalid assignment in a
try...catchblock inMainso the program can continue running after catching the exception.
▼ Solution & Explanation
Explanation:
throw new ArgumentException("..."): Immediately stops the assignment and signals to the caller that the supplied value was invalid.try...catch: Catches the exception thrown by the invalid assignment, letting the program handle the error gracefully instead of crashing.clothing.TargetTemperature = 22;: Succeeds without any exception since 22 falls within the allowed range.
Exercise 13: Calculated Circle (Read-Only Computed Property)
Practice Problem: Design a Circle class with a read-only calculated property Area that dynamically computes π r² whenever called.
Purpose: This exercise helps you practice a computed property, one that has no backing field at all and instead calculates its value fresh from other properties every time it is read.
Given Input: Radius = 5
Expected Output: Area = 78.54
▼ Hint
- Give
Areaonly agetblock, with noset, so it can never be assigned directly. - Inside the
getblock, calculate the result usingMath.PI * Radius * Radius. - Because the calculation happens inside
get, the returned value always reflects the currentRadius, even if it changes later.
▼ Solution & Explanation
Explanation:
public double Area { get { ... } }: A property with only agetaccessor, making it impossible to assign a value to it directly from outside the class.Math.PI * Radius * Radius: Recalculates the area from scratch every timeAreais read, always using the currentRadius.Math.Round(..., 2): Rounds the result to two decimal places for cleaner display.
Exercise 14: Flight Reservation (Private Set with Controlled Method)
Practice Problem: Implement a Flight class where the SeatNumber has a public get but a private set, modifiable only by the internal AssignSeat() method.
Purpose: This exercise helps you practice restricting how a property can be changed, allowing anyone to read it while limiting writes to controlled logic inside the class.
Given Input: AssignSeat("14A")
Expected Output: Seat Number: 14A
▼ Hint
- Declare the property as
public string SeatNumber { get; private set; }. - The
private setmeans only code inside theFlightclass itself can assign a new value. - Write an
internalmethod,AssignSeat(), that sets the property from within the class.
▼ Solution & Explanation
Explanation:
public string SeatNumber { get; private set; }: Allows any code to readSeatNumber, but only code insideFlightitself can assign it a new value.internal void AssignSeat(string seatNumber): The only method that can update the seat, since it is defined inside the same class where the private setter is accessible.
Exercise 15: Game Character Health (Property Triggering Side Effects)
Practice Problem: Create a Hero class where setting Health automatically triggers a boolean property IsAlive to flip to false if health hits 0.
Purpose: This exercise helps you practice having a property setter update related state elsewhere in the object, instead of just storing a single value.
Given Input: Set Health to 50, then set it to 0
Expected Output:
Is Alive: True Is Alive: False
▼ Hint
- Give
IsAliveaprivate setso it can only be changed from inside the class. - Inside the
Healthsetter, store the value first, then check if it dropped to0or below. - If health has run out, update
IsAlivetofalseright there in the same setter.
▼ Solution & Explanation
Explanation:
public bool IsAlive { get; private set; } = true;: Starts astrueby default and can only be changed from inside the class.if (health <= 0) { health = 0; IsAlive = false; }: As soon as health drops to zero or below, it is clamped at zero and the hero is marked as no longer alive.
Exercise 16: Bank Yield Calculator (Custom Set Logic with Bonus)
Practice Problem: Create a SavingsAccount with a Balance property. Use custom get/set blocks to automatically calculate and apply a 2% bonus if the deposit exceeds $10,000.
Purpose: This exercise helps you practice applying business logic directly inside a setter, transforming an incoming value before it is stored.
Given Input: Balance = 15000
Expected Output: Balance = 15300
▼ Hint
- Inside the
setblock, check whethervalueis greater than 10000. - If it is, multiply the value by
1.02before storing it, applying the 2% bonus. - Otherwise, store the value as-is with no bonus applied.
▼ Solution & Explanation
Explanation:
if (value > 10000): Checks whether the deposit qualifies for the bonus before storing it.balance = value * 1.02;: Applies a 2% bonus on top of the deposited amount when the condition is met.
Exercise 17: Vehicle Hierarchy (Inheritance & Method Overriding)
Practice Problem: Create a base class Vehicle and derived classes Car and Motorcycle. Override a StartEngine() method to print unique startup sounds.
Purpose: This exercise helps you practice inheritance with more than one derived class, each providing its own version of a shared base class method.
Given Input: None
Expected Output:
Vroom! The car engine roars to life. Vroooom! The motorcycle engine revs up.
▼ Hint
- Mark
StartEngine()asvirtualin theVehiclebase class. - In both
CarandMotorcycle, use: Vehicleto inherit, andoverrideto provide a unique startup message. - Store each object in a
Vehiclevariable to demonstrate that the correct overridden version still runs.
▼ Solution & Explanation
Explanation:
class Car : Vehicle/class Motorcycle : Vehicle: Both classes inherit from the same base class, sharing its structure while customizing specific behavior.Vehicle car = new Car();: Even though the variable’s type isVehicle, callingStartEngine()runs the overridden version specific toCar.
Exercise 18: The Sealed Security System (Sealed Classes)
Practice Problem: Design a SecurityCore base class. Create a StrictAuth derived class and mark it as sealed so no other class can inherit and potentially alter its authentication logic.
Purpose: This exercise helps you practice the sealed keyword, which stops a class from being used as a base class for further inheritance.
Given Input: None
Expected Output: Running strict authentication checks.
▼ Hint
- Give
SecurityCoreavirtualmethod thatStrictAuthcan override. - Add the
sealedkeyword directly beforeclass StrictAuth. - A
sealedclass can still be instantiated and used normally; it simply cannot be inherited from any further.
▼ Solution & Explanation
Explanation:
sealed class StrictAuth : SecurityCore: Inherits fromSecurityCoreas normal, but thesealedkeyword prevents any other class from inheriting fromStrictAuthitself.SecurityCore auth = new StrictAuth();: Creating and using the sealed class works exactly like any other class; only further inheritance is blocked.
Exercise 19: Employee Payroll System (Virtual & Override Across Multiple Subclasses)
Practice Problem: Write a base Employee class with a CalculatePay() method. Create HourlyEmployee and SalariedEmployee subclasses that override the calculation using virtual and override.
Purpose: This exercise helps you practice applying method overriding across multiple different subclasses, each calculating a shared concept in its own way.
Given Input: Hourly employee: rate $20, hours 80. Salaried employee: monthly salary $4000.
Expected Output:
Hourly Employee Pay = 1600 Salaried Employee Pay = 4000
▼ Hint
- Mark
CalculatePay()asvirtualin the baseEmployeeclass, with a default return value like0. - In
HourlyEmployee, override it to returnHourlyRate * HoursWorked. - In
SalariedEmployee, override it to simply returnMonthlySalary.
▼ Solution & Explanation
Explanation:
public virtual double CalculatePay(): Establishes a shared method signature that every subclass can customize with its own pay calculation.HourlyRate * HoursWorked: TheHourlyEmployeeversion calculates pay based on hours actually worked.return MonthlySalary;: TheSalariedEmployeeversion simply returns a fixed monthly amount, regardless of hours worked.
Exercise 20: Base Constructor Chaining (The base Keyword)
Practice Problem: Create a Person base class requiring a name in its constructor. Create a Student derived class that passes the name to the base constructor using the base keyword.
Purpose: This exercise helps you practice constructor chaining across an inheritance hierarchy, letting a derived class forward data to its base class instead of duplicating logic.
Given Input: name = "Emma", school = "Springfield High"
Expected Output: Name: Emma, School: Springfield High
▼ Hint
- Give
Persona constructor that takes anameparameter and assigns it to aNameproperty. - In the
Studentconstructor, add: base(name)after the parameter list to forward the name up to the base class constructor. - The
Studentconstructor only needs to handle its own additional property,School, sinceNameis already handled by the base constructor.
▼ Solution & Explanation
Explanation:
public Student(string name, string school) : base(name): Passesnamestraight up to thePersonconstructor before theStudentconstructor’s own body runs.School = school;: Handles only the property that is specific toStudent, sinceNamewas already set by the base constructor.
Exercise 21: The Sealed Method (Sealed Overrides)
Practice Problem: In an RPGCharacter hierarchy, create a virtual Move() method. Have a Knight class override Move(), but mark the overridden method as sealed to stop subclasses of Knight (like Paladin) from changing it again.
Purpose: This exercise helps you practice sealing a single overridden method rather than an entire class, locking in specific behavior partway through an inheritance chain.
Given Input: None
Expected Output: The knight marches forward in heavy armor.
▼ Hint
- Mark
Move()asvirtualin the baseRPGCharacterclass. - In
Knight, override it usingsealed override void Move(), combining both keywords together. - A class named
Paladincould still inherit fromKnightand override other methods, but it would not be allowed to overrideMove()again.
▼ Solution & Explanation
Explanation:
public sealed override void Move(): Overrides the base class method as usual, but thesealedkeyword prevents any class inheriting fromKnightfrom overridingMove()a second time.RPGCharacter knight = new Knight();: CallingMove()through the base type still runs the sealed, overridden version fromKnight.
Exercise 22: Zoo Soundboard (Runtime Polymorphism)
Practice Problem: Create an array of Animal objects containing Lion, Sparrow, and Frog instances. Loop through the array and call a virtual MakeSound() method to demonstrate runtime polymorphism.
Purpose: This exercise helps you practice runtime polymorphism, where a single loop can call the correct overridden method for each object without knowing its specific type in advance.
Given Input: animals = [Lion, Sparrow, Frog]
Expected Output:
Roar! Tweet! Ribbit!
▼ Hint
- Give
Animalavirtual MakeSound()method, then override it differently in each subclass. - Declare the array using the base type:
Animal[] animals = { new Lion(), new Sparrow(), new Frog() };. - A single
foreachloop callinganimal.MakeSound()will automatically run each object’s own overridden version.
▼ Solution & Explanation
Explanation:
Animal[] animals = { new Lion(), new Sparrow(), new Frog() };: Stores three different object types in a single array using their shared base type.animal.MakeSound();: Even though the loop variable is typed asAnimal, C# automatically calls whichever overridden version matches the object’s actual, real-world type.
Exercise 23: Shadowing vs Overriding (The new Keyword)
Practice Problem: Create a base class with a method Display(). Create a derived class that uses the new keyword to shadow it. Write a test script showing the behavior difference when cast as the base type versus the derived type.
Purpose: This exercise helps you practice recognizing the difference between shadowing (new) and overriding (override), since shadowing depends on the variable’s declared type rather than the object’s actual type.
Given Input: None
Expected Output:
Display from DerivedClass Display from BaseClass
▼ Hint
- Do not mark the base class method as
virtual; leave it as a regular method. - In the derived class, use
public new void Display()to hide the base version instead of overriding it. - Create one
DerivedClassobject, then assign it to both aDerivedClassvariable and aBaseClassvariable to see how the method call differs depending on the variable’s declared type.
▼ Solution & Explanation
Explanation:
public new void Display(): Hides the base class method rather than overriding it, meaning the two methods are treated as completely separate.derived.Display();: Sincederivedis declared asDerivedClass, this calls the shadowed version.baseRef.Display();: Even thoughbaseRefpoints to the same object, its declared type isBaseClass, so shadowing causes it to call the original base method instead.
Exercise 24: Multi-level Appliance (Multi-level Inheritance & base Calls)
Practice Problem: Build an inheritance chain: Device -> KitchenAppliance -> Microwave. Override a PowerOn() method at each level, ensuring the lower levels call base.PowerOn() first.
Purpose: This exercise helps you practice a three-level inheritance chain, where each level adds its own behavior on top of everything the levels above it already do.
Given Input: None
Expected Output:
Device powering on. Kitchen appliance initializing. Microwave ready to heat.
▼ Hint
- Mark
PowerOn()asvirtualinDevice, and asoverridein bothKitchenApplianceandMicrowave. - At the start of each overridden method, call
base.PowerOn();before printing that level’s own message. - This chains all three messages together in order, from the top of the hierarchy down to the bottom.
▼ Solution & Explanation
Explanation:
class KitchenAppliance : Device/class Microwave : KitchenAppliance: Forms a three-level chain, whereMicrowaveinherits fromKitchenAppliance, which in turn inherits fromDevice.base.PowerOn();: Calls the immediate parent class’s version of the method first, ensuring every level’s logic runs in order from the top down.
Exercise 25: Dynamic UI Rendering (Polymorphic Collections)
Practice Problem: Create a UIElement class with a virtual Draw() method. Create Button, TextBox, and Checkbox overrides. Implement a Canvas class that holds a list of UIElements and renders them all simultaneously.
Purpose: This exercise helps you practice storing a mix of different subclasses in a single collection and rendering all of them polymorphically through one shared method.
Given Input: elements = [Button, TextBox, Checkbox]
Expected Output:
Drawing a button. Drawing a text box. Drawing a checkbox.
▼ Hint
- Give
Canvasaprivate List<UIElement>field to hold whatever elements are added to it. - Write an
AddElement()method that accepts anyUIElement, including its subclasses. - In
RenderAll(), loop through the list and callDraw()on each item, letting polymorphism select the correct overridden version.
▼ Solution & Explanation
Explanation:
List<UIElement> elements: Holds any mix ofButton,TextBox, andCheckboxobjects together, since they all share theUIElementbase type.element.Draw();: Calls each object’s own overriddenDraw()method automatically, withoutCanvasneeding to know which specific subclass it is handling.
Exercise 26: Database Connector (Abstract Classes & Methods)
Practice Problem: Create an abstract DatabaseConnection class with abstract methods OpenConnection() and CloseConnection(). Implement concrete SqlConnection and MongoDbConnection classes.
Purpose: This exercise helps you practice defining an abstract class that cannot be instantiated on its own, forcing every subclass to supply its own implementation for each abstract method.
Given Input: None
Expected Output:
Opening SQL Server connection. Closing SQL Server connection.
▼ Hint
- Mark the class as
abstract class DatabaseConnection, and declare both methods aspublic abstract void ...();, with no method body. - Every concrete subclass, like
SqlConnection, must provide anoverridefor both abstract methods or it will fail to compile. - You cannot write
new DatabaseConnection()directly, since abstract classes can never be instantiated on their own.
▼ Solution & Explanation
Explanation:
abstract class DatabaseConnection: Defines a shared contract that every database connection type must follow, without providing any implementation itself.public abstract void OpenConnection();: Declares a method signature with no body, requiring every concrete subclass to supply its own version.DatabaseConnection connection = new SqlConnection();: Creates a concreteSqlConnectionobject, referenced through the abstract base type.
Exercise 27: Abstract Shape Factory (Abstract Methods with Different Data)
Practice Problem: Design an abstract Shape class with an abstract method GetPerimeter(). Implement it across Triangle and Hexagon classes.
Purpose: This exercise helps you practice implementing the same abstract method across shapes that each need completely different data and formulas to calculate their result.
Given Input: Triangle(3, 4, 5), Hexagon(4)
Expected Output:
Triangle Perimeter = 12 Hexagon Perimeter = 24
▼ Hint
- Give
Trianglethree side-length fields and a constructor that sets all of them. - Give
Hexagona single side-length field, since all six sides of a regular hexagon are equal. - Each class’s
GetPerimeter()override uses whatever fields make sense for that particular shape.
▼ Solution & Explanation
Explanation:
SideA + SideB + SideC: TheTriangleversion adds all three unique side lengths.SideLength * 6: TheHexagonversion multiplies its single side length by six, since a regular hexagon has six equal sides.
Exercise 28: Coffee Machine Blueprint (Mixing Concrete & Abstract Methods)
Practice Problem: Create an abstract WarmBeverage class with a concrete method BoilWater() and abstract methods Brew() and AddCondiments(). Implement a Latte class.
Purpose: This exercise helps you practice mixing shared, ready-to-use logic with abstract methods in the same class, so subclasses only need to fill in the steps that actually vary.
Given Input: None
Expected Output:
Boiling water to 100°C. Brewing espresso shots. Adding steamed milk.
▼ Hint
- Give
BoilWater()a normal method body in the abstract class, since boiling water works the same way for every beverage. - Leave
Brew()andAddCondiments()asabstract, since these steps differ depending on the drink. - In
Main, call all three methods on theLatteobject, one right after another.
▼ Solution & Explanation
Explanation:
public void BoilWater() { ... }: A regular, concrete method that every subclass inherits and can use directly without rewriting it.public abstract void Brew();: Has no implementation in the base class, soLattemust supply its own brewing logic.latte.BoilWater();: Calls the inherited concrete method directly on theLatteobject, since it was never overridden.
Exercise 29: Document Parser (Abstract Class with a Constructor)
Practice Problem: Design an abstract Document class with a concrete constructor that initializes the filepath, and an abstract ParseContent() method implemented by PdfParser and CsvParser.
Purpose: This exercise helps you practice giving an abstract class its own constructor, even though the class itself can never be instantiated directly, since subclasses still rely on it through base(...).
Given Input: filePath = "report.pdf"
Expected Output: Parsing PDF content from report.pdf
▼ Hint
- Give
Documentaprotectedconstructor that accepts afilePathparameter and assigns it to a property. - A
protectedconstructor can still be called by subclasses using: base(filePath), even though the abstract class itself can’t be instantiated directly. - In
ParseContent(), use the inheritedFilePathproperty to build the output message.
▼ Solution & Explanation
Explanation:
protected Document(string filePath): A constructor that can only be called from within the class itself or from a subclass, never directly from outside code.public PdfParser(string filePath) : base(filePath) { }: Forwards the file path up to the abstract base constructor, lettingDocumenthandle storing it.
Exercise 30: Online Payment Gateway (Shared Helper Method in an Abstract Class)
Practice Problem: Create an abstract PaymentProcessor class with a concrete ValidateTransaction() helper and abstract ProcessPayment() method for PayPal and Stripe integrations.
Purpose: This exercise helps you practice sharing common validation logic across every subclass, so each payment provider can reuse it instead of duplicating the same check.
Given Input: amount = 150.00
Expected Output: Processing $150 through PayPal.
▼ Hint
- Write
ValidateTransaction()as a regular, concreteprotectedmethod in the abstract class so subclasses can call it but outside code cannot. - Have it simply check whether the amount is greater than
0, returning abool. - Inside each subclass’s
ProcessPayment()override, callValidateTransaction()first before continuing.
▼ Solution & Explanation
Explanation:
protected bool ValidateTransaction(double amount): A shared helper method that every subclass can call internally, but that remains hidden from code outside the class hierarchy.if (ValidateTransaction(amount)): Reuses the same validation logic inside each subclass’sProcessPayment()override, avoiding duplicated checks.
Exercise 31: Game AI Behavior (Abstract Methods & Abstract Properties)
Practice Problem: Create an abstract EnemyAI class. Define an abstract ExecuteTurn() method alongside a protected abstract property AggressionLevel. Implement this in a Goblin and Dragon class.
Purpose: This exercise helps you practice using an abstract property alongside an abstract method, showing that properties, not just methods, can also be left for subclasses to define.
Given Input: None
Expected Output:
Goblin attacks cautiously with aggression level 3. Dragon unleashes a devastating attack with aggression level 9.
▼ Hint
- Declare
protected abstract int AggressionLevel { get; }with only agetaccessor, since it doesn’t need to be set from outside. - In each subclass, override the property using an expression body, e.g.
protected override int AggressionLevel => 3;. - Reference
AggressionLevelinside each subclass’sExecuteTurn()method to include it in the printed message.
▼ Solution & Explanation
Explanation:
protected abstract int AggressionLevel { get; }: Requires every subclass to supply its own value for this property, just like an abstract method requires its own implementation.protected override int AggressionLevel => 3;: Uses an expression-bodied property to return a fixed value forGoblin.
Exercise 32: The Print & Scan Combo (Multiple Interface Implementation)
Practice Problem: Create IPrinter and IScanner interfaces. Implement both in a single AllInOnePrinter class to show multiple interface inheritance.
Purpose: This exercise helps you practice implementing more than one interface on a single class, something C# allows even though it only permits inheriting from one base class.
Given Input: Print("Report.docx"), Scan("Receipt.jpg")
Expected Output:
Printing: Report.docx Scanning: Receipt.jpg
▼ Hint
- Declare each interface with just a method signature and no body, ending in a semicolon.
- List both interfaces after the class name, separated by a comma:
class AllInOnePrinter : IPrinter, IScanner. - Implement both methods normally inside the class, exactly as you would for a single interface.
▼ Solution & Explanation
Explanation:
class AllInOnePrinter : IPrinter, IScanner: A single class implementing two separate interfaces, gaining the responsibilities of both.public void Print(...)/public void Scan(...): Each method fulfills the contract of its respective interface, with no ambiguity since the method names don’t collide.
Exercise 33: Explicit Conflict Resolution (Explicit Interface Implementation)
Practice Problem: Create two interfaces, ILeftTurn and IRightTurn, both containing a method named Execute(). Implement both explicitly in a Driver class to avoid naming collisions.
Purpose: This exercise helps you practice explicit interface implementation, the technique C# provides for resolving a naming conflict when two interfaces share an identical method name.
Given Input: None
Expected Output:
Turning left. Turning right.
▼ Hint
- Implement each method by prefixing it with the interface name instead of an access modifier:
void ILeftTurn.Execute(). - An explicitly implemented method cannot be called directly on the class instance; it can only be called through a variable typed as that specific interface.
- Assign the same
Driverobject to both anILeftTurnvariable and anIRightTurnvariable to call each version separately.
▼ Solution & Explanation
Explanation:
void ILeftTurn.Execute(): Explicitly ties this implementation to theILeftTurninterface specifically, keeping it separate fromIRightTurn‘s version.ILeftTurn leftTurn = driver;: Since explicit implementations are hidden from the class type itself, the object must be accessed through a variable typed as the specific interface to call that version.
Exercise 34: Default Logger Logic (Default Interface Methods)
Practice Problem: Create an ILogger interface with an abstract LogError(string msg) method, and a default interface method LogInfo(string msg) that prints a timestamped message directly from the interface contract.
Purpose: This exercise helps you practice default interface methods, a modern C# feature that lets an interface provide a ready-to-use implementation instead of forcing every class to write it.
Given Input: LogError("Something went wrong."), LogInfo("Application started.")
Expected Output:
[ERROR] Something went wrong. [INFO 14:32:10] Application started.
(The timestamp will differ depending on when you run the program.)
▼ Hint
- Declare
LogError(string msg)with no body, just like a normal interface method. - Give
LogInfo(string msg)an actual method body directly inside the interface, which is only possible in modern versions of C#. - A class implementing
ILoggeronly needs to provideLogError(); it automatically inherits the workingLogInfo()unless it chooses to override it.
▼ Solution & Explanation
Explanation:
void LogInfo(string msg) { ... }: A default interface method with a real implementation, whichConsoleLoggerinherits automatically without writing any code for it.logger.LogInfo(...): Works even thoughConsoleLoggernever defined this method itself, since the interface’s default body is used instead.
Exercise 35: Plugin Architecture (Interface-Based Extensibility)
Practice Problem: Design an IPlugin interface with a Start() method. Write a host program that accepts any object implementing IPlugin to dynamically extend functionality.
Purpose: This exercise helps you practice programming against an interface rather than a specific class, allowing a host program to work with any plugin, even ones written later.
Given Input: LoggingPlugin, AnalyticsPlugin
Expected Output:
Logging plugin started. Analytics plugin started.
▼ Hint
- Give the host class a method that accepts an
IPluginparameter, not a specific plugin class. - Any class implementing
IPlugincan be passed into that method, regardless of what else the class does. - Inside the method, call
plugin.Start()without needing to know which specific plugin type was passed in.
▼ Solution & Explanation
Explanation:
public void RunPlugin(IPlugin plugin): Accepts any object implementingIPlugin, withoutPluginHostneeding to know about specific plugin classes.host.RunPlugin(new LoggingPlugin());: Passes a concrete plugin into the host, which then runs it purely through the shared interface contract.
Exercise 36: Sortable Inventory (IComparable<T>)
Practice Problem: Create a Weapon class with Name and Damage properties. Implement the native .NET IComparable<Weapon> interface to sort a list of weapons by their damage values.
Purpose: This exercise helps you practice implementing a built-in .NET interface, letting your own custom objects work directly with methods like List<T>.Sort().
Given Input: Dagger (15), Greatsword (45), Bow (25)
Expected Output:
Dagger - 15 Bow - 25 Greatsword - 45
▼ Hint
- Add
: IComparable<Weapon>after the class name to implement the interface. - Implement
CompareTo(Weapon other)by comparing theDamagevalues of the two weapons usingDamage.CompareTo(other.Damage). - Once
CompareTo()is implemented, calling.Sort()directly on aList<Weapon>will automatically use it.
▼ Solution & Explanation
Explanation:
class Weapon : IComparable<Weapon>: Signals thatWeaponobjects know how to compare themselves against one another.Damage.CompareTo(other.Damage): Defines “smaller” and “larger” weapons purely by theirDamagevalue.weapons.Sort();: Automatically uses the customCompareTo()logic to sort the entire list in ascending order of damage.
Exercise 37: Disposable Resources (IDisposable)
Practice Problem: Create a DatabaseSession class that implements IDisposable. Demonstrate its usage inside a C# using statement block to show structured resource management.
Purpose: This exercise helps you practice the IDisposable pattern, which guarantees that cleanup code runs automatically once a using block finishes, even if an error occurs inside it.
Given Input: None
Expected Output:
Database session opened. Running database query. Database session closed.
▼ Hint
- Add
: IDisposableafter the class name, then implement apublic void Dispose()method. - Wrap the object’s creation in
using (DatabaseSession session = new DatabaseSession()) { ... }. Dispose()is called automatically as soon as execution leaves theusingblock, whether it finishes normally or exits early.
▼ Solution & Explanation
Explanation:
class DatabaseSession : IDisposable: Signals that this class holds a resource that needs explicit cleanup once it is no longer needed.using (DatabaseSession session = new DatabaseSession()) { ... }: Automatically callsDispose()the moment the block ends, without requiring a manual call.
Exercise 38: Abstract Class vs Interface (Combining Both)
Practice Problem: Create a scenario where an object needs to inherit core identity data from an abstract class (Vehicle) but also needs pluggable behaviors from multiple interfaces (IFlyable, ISwimmable). Implement a FlyingBoat class.
Purpose: This exercise helps you practice combining a single abstract base class with multiple interfaces on the same class, since C# allows one base class but any number of interfaces.
Given Input: Name = "SkyCruiser"
Expected Output:
SkyCruiser is soaring through the sky. SkyCruiser is gliding across the water.
▼ Hint
- List the base class first, followed by each interface:
class FlyingBoat : Vehicle, IFlyable, ISwimmable. - Use
Vehicleto hold shared identity data likeName, set through its constructor. - Implement
Fly()andSwim()as ordinary methods that fulfill each interface’s contract.
▼ Solution & Explanation
Explanation:
class FlyingBoat : Vehicle, IFlyable, ISwimmable: Inherits shared identity data from a single abstract class while also picking up two separate, pluggable behaviors from interfaces.public FlyingBoat(string name) : base(name) { }: Forwards the name up to the abstractVehicleconstructor, which handles storing it.
Exercise 39: Explicit Interface Hiding
Practice Problem: Create an IAdmin interface with a ResetSystem() method. Implement it explicitly in a User class so that ResetSystem() is completely hidden unless the object is explicitly cast to an IAdmin.
Purpose: This exercise helps you practice using explicit interface implementation deliberately as a way to hide sensitive functionality from casual, everyday use of a class.
Given Input: None
Expected Output: System reset performed.
▼ Hint
- Implement the method as
void IAdmin.ResetSystem(), without apublicmodifier. - A regular
Uservariable will not showResetSystem()as an available method at all. - Only by assigning the object to a variable typed as
IAdmin, or casting it directly, canResetSystem()be called.
▼ Solution & Explanation
Explanation:
void IAdmin.ResetSystem(): Implements the method explicitly, which hides it from theUsertype entirely; callinguser.ResetSystem()directly would fail to compile.IAdmin admin = user;: Only through a variable typed asIAdmindoesResetSystem()become visible and callable.

Leave a Reply