PYnative

Python Programming

  • Learn Python ▼
    • Python Tutorials
    • Python Basics
    • Python Interview Q&As
  • Exercises ▼
    • Python Exercises
    • C++ Exercises
    • C Programming Exercises
    • Java Exercises
    • C# Exercises
  • Quizzes
  • Code Editor ▼
    • Online Python Code Editor
    • Online C Compiler
    • Online C++ Compiler
    • Online Java Compiler
    • Online C# Compiler
Home » C# Exercises » C# OOP Exercises: 40 Coding Problems with Solutions

C# OOP Exercises: 40 Coding Problems with Solutions

Updated on: July 15, 2026 | Leave a Comment

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/override methods.
  • 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: Hours and Minutes.
  • In the AddTime() method, add the given minutes to Minutes, then use integer division and the modulus operator to roll extra minutes over into Hours.
  • Use the D2 format specifier so single-digit hours or minutes still display with a leading zero.
▼ Solution & Explanation
using System;
namespace DigitalWatchApplication
{
    class DigitalWatch
    {
        public int Hours;
        public int Minutes;

        public void AddTime(int minutesToAdd)
        {
            Minutes += minutesToAdd;
            Hours += Minutes / 60;
            Minutes = Minutes % 60;
        }

        public void PrintTime()
        {
            Console.WriteLine($"{Hours:D2}:{Minutes:D2}");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            DigitalWatch watch = new DigitalWatch();
            watch.Hours = 10;
            watch.Minutes = 45;

            watch.AddTime(30);
            watch.PrintTime();
        }
    }
}Code language: C# (cs)

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
using System;
namespace LibraryBookApplication
{
    class Book
    {
        public string Title { get; set; }
        public string Author { get; set; }
        public string ISBN { get; set; }

        public Book()
        {
            Title = "Unknown Title";
            Author = "Unknown Author";
            ISBN = "N/A";
        }

        public Book(string title, string author) : this()
        {
            Title = title;
            Author = author;
        }

        public Book(string title, string author, string isbn)
        {
            Title = title;
            Author = author;
            ISBN = isbn;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Book book1 = new Book();
            Book book2 = new Book("1984", "George Orwell");
            Book book3 = new Book("Dune", "Frank Herbert", "978-0441172719");

            Console.WriteLine($"Title: {book1.Title}, Author: {book1.Author}, ISBN: {book1.ISBN}");
            Console.WriteLine($"Title: {book2.Title}, Author: {book2.Author}, ISBN: {book2.ISBN}");
            Console.WriteLine($"Title: {book3.Title}, Author: {book3.Author}, ISBN: {book3.ISBN}");
        }
    }
}Code language: C# (cs)

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 set ISBN to “N/A”, then overwrites Title and Author with 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 readonly field 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
using System;
namespace BankAccountSnapshotApplication
{
    class Account
    {
        public readonly string AccountNumber;

        public Account(string accountNumber)
        {
            AccountNumber = accountNumber;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Account account = new Account("ACC-10293");

            Console.WriteLine($"Account Number: {account.AccountNumber}");
        }
    }
}Code language: C# (cs)

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 private so no code outside the class can call new ConfigurationManager().
  • Keep a private static field that holds the single instance, starting as null.
  • In a public static method, create the instance only if it doesn’t exist yet, then always return that same stored instance.
▼ Solution & Explanation
using System;
namespace SingletonConfigApplication
{
    class ConfigurationManager
    {
        private static ConfigurationManager instance;
        public string AppName = "MyApp";

        private ConfigurationManager() { }

        public static ConfigurationManager GetInstance()
        {
            if (instance == null)
            {
                instance = new ConfigurationManager();
            }
            return instance;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ConfigurationManager config1 = ConfigurationManager.GetInstance();
            ConfigurationManager config2 = ConfigurationManager.GetInstance();

            Console.WriteLine($"Same instance: {config1 == config2}");
        }
    }
}Code language: C# (cs)

Explanation:

  • private ConfigurationManager() { }: Prevents any code outside the class from creating new instances directly with the new keyword.
  • if (instance == null): Creates the single instance only the first time GetInstance() 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 private so it cannot be accessed or modified directly from outside the class.
  • Track a separate count field 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
using System;
namespace ThermostatApplication
{
    class Thermostat
    {
        private double[] temperatureHistory = new double[5];
        private int count = 0;

        public void AddReading(double temperature)
        {
            if (count < temperatureHistory.Length)
            {
                temperatureHistory[count] = temperature;
                count++;
            }
        }

        public void PrintHistory()
        {
            string[] readings = new string[count];
            for (int i = 0; i < count; i++)
            {
                readings[i] = temperatureHistory[i].ToString();
            }
            Console.WriteLine("Temperature History: " + string.Join(", ", readings));
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Thermostat thermostat = new Thermostat();
            thermostat.AddReading(68.5);
            thermostat.AddReading(70.2);
            thermostat.AddReading(69.0);

            thermostat.PrintHistory();
        }
    }
}Code language: C# (cs)

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 Name and Price as public so any code can read the product’s basic details.
  • Mark the discount calculation method as internal instead of public.
  • An internal member can still be called from Main here, 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
using System;
namespace ECommerceApplication
{
    class Product
    {
        public string Name { get; set; }
        public double Price { get; set; }

        internal double CalculateDiscountedPrice()
        {
            return Price * 0.9;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Product product = new Product();
            product.Name = "Wireless Mouse";
            product.Price = 25.00;

            double discountedPrice = product.CalculateDiscountedPrice();

            Console.WriteLine($"Discounted Price for {product.Name}: {discountedPrice}");
        }
    }
}Code language: C# (cs)

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 because Program and Product are both part of the same project, so the internal restriction 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 with new.
  • Every member of a static class must also be static.
  • Call the methods directly on the class name, e.g. MathWizard.Factorial(5), without ever creating an object.
▼ Solution & Explanation
using System;
namespace MathWizardApplication
{
    static class MathWizard
    {
        public static long Factorial(int n)
        {
            long result = 1;
            for (int i = 1; i <= n; i++)
            {
                result *= i;
            }
            return result;
        }

        public static int AbsoluteDifference(int a, int b)
        {
            return Math.Abs(a - b);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"Factorial of 5 = {MathWizard.Factorial(5)}");
            Console.WriteLine($"Absolute Difference = {MathWizard.AbsoluteDifference(10, 3)}");
        }
    }
}Code language: C# (cs)

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-in Math.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 null after use so nothing is holding onto it anymore.
  • Call GC.Collect() followed by GC.WaitForPendingFinalizers() to force the finalizer to run immediately for this demonstration, since it normally runs at an unpredictable time.
▼ Solution & Explanation
using System;
namespace ResourceWrapperApplication
{
    class ResourceWrapper
    {
        public ResourceWrapper()
        {
            Console.WriteLine("File opened.");
        }

        ~ResourceWrapper()
        {
            Console.WriteLine("File handle cleaned up by garbage collector.");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ResourceWrapper resource = new ResourceWrapper();
            resource = null;

            GC.Collect();
            GC.WaitForPendingFinalizers();
        }
    }
}Code language: C# (cs)

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 set block, assign the incoming value to the private field as normal.
  • In the get block, ignore the stored value entirely and always return the fixed string "********".
▼ Solution & Explanation
using System;
namespace UserProfileApplication
{
    class UserProfile
    {
        private string password;

        public string Password
        {
            get { return "********"; }
            set { password = value; }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            UserProfile profile = new UserProfile();
            profile.Password = "MySecret123";

            Console.WriteLine(profile.Password);
        }
    }
}Code language: C# (cs)

Explanation:

  • private string password;: A hidden backing field that actually holds the real password value.
  • set { password = value; }: Stores whatever value is assigned to the Password property 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.Width after that point would fail to compile, since init only allows assignment during initialization.
▼ Solution & Explanation
using System;
namespace ImmutableRectangleApplication
{
    class Rectangle
    {
        public double Width { get; init; }
        public double Height { get; init; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Rectangle rectangle = new Rectangle { Width = 10, Height = 5 };

            Console.WriteLine($"Width: {rectangle.Width}, Height: {rectangle.Height}");
        }
    }
}Code language: C# (cs)

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 time init properties 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; } for Make and Model, since they don’t need any special validation.
  • For Odometer, declare a private backing field and write a full property with a custom set block.
  • Inside the setter, only update the backing field if the new value is greater than or equal to the current one.
▼ Solution & Explanation
using System;
namespace RentalCarApplication
{
    class RentalCar
    {
        public string Make { get; set; }
        public string Model { get; set; }

        private int odometer;

        public int Odometer
        {
            get { return odometer; }
            set
            {
                if (value >= odometer)
                {
                    odometer = value;
                }
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            RentalCar car = new RentalCar();
            car.Make = "Toyota";
            car.Model = "Camry";

            car.Odometer = 5000;
            car.Odometer = 3000;

            Console.WriteLine($"Odometer: {car.Odometer}");
        }
    }
}Code language: C# (cs)

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 set block, check whether value is 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...catch block in Main so the program can continue running after catching the exception.
▼ Solution & Explanation
using System;
namespace SmartWardrobeApplication
{
    class Clothing
    {
        private int targetTemperature;

        public int TargetTemperature
        {
            get { return targetTemperature; }
            set
            {
                if (value < 15 || value > 30)
                {
                    throw new ArgumentException("Temperature must be between 15 and 30 degrees Celsius.");
                }
                targetTemperature = value;
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Clothing clothing = new Clothing();

            try
            {
                clothing.TargetTemperature = 40;
            }
            catch (ArgumentException ex)
            {
                Console.WriteLine("Error: " + ex.Message);
            }

            clothing.TargetTemperature = 22;
            Console.WriteLine($"Target Temperature: {clothing.TargetTemperature}");
        }
    }
}Code language: C# (cs)

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 Area only a get block, with no set, so it can never be assigned directly.
  • Inside the get block, calculate the result using Math.PI * Radius * Radius.
  • Because the calculation happens inside get, the returned value always reflects the current Radius, even if it changes later.
▼ Solution & Explanation
using System;
namespace CalculatedCircleApplication
{
    class Circle
    {
        public double Radius { get; set; }

        public double Area
        {
            get { return Math.Round(Math.PI * Radius * Radius, 2); }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Circle circle = new Circle();
            circle.Radius = 5;

            Console.WriteLine($"Area = {circle.Area}");
        }
    }
}Code language: C# (cs)

Explanation:

  • public double Area { get { ... } }: A property with only a get accessor, making it impossible to assign a value to it directly from outside the class.
  • Math.PI * Radius * Radius: Recalculates the area from scratch every time Area is read, always using the current Radius.
  • 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 set means only code inside the Flight class itself can assign a new value.
  • Write an internal method, AssignSeat(), that sets the property from within the class.
▼ Solution & Explanation
using System;
namespace FlightReservationApplication
{
    class Flight
    {
        public string SeatNumber { get; private set; }

        internal void AssignSeat(string seatNumber)
        {
            SeatNumber = seatNumber;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Flight flight = new Flight();
            flight.AssignSeat("14A");

            Console.WriteLine($"Seat Number: {flight.SeatNumber}");
        }
    }
}Code language: C# (cs)

Explanation:

  • public string SeatNumber { get; private set; }: Allows any code to read SeatNumber, but only code inside Flight itself 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 IsAlive a private set so it can only be changed from inside the class.
  • Inside the Health setter, store the value first, then check if it dropped to 0 or below.
  • If health has run out, update IsAlive to false right there in the same setter.
▼ Solution & Explanation
using System;
namespace GameCharacterApplication
{
    class Hero
    {
        private int health;

        public int Health
        {
            get { return health; }
            set
            {
                health = value;
                if (health <= 0)
                {
                    health = 0;
                    IsAlive = false;
                }
            }
        }

        public bool IsAlive { get; private set; } = true;
    }

    class Program
    {
        static void Main(string[] args)
        {
            Hero hero = new Hero();

            hero.Health = 50;
            Console.WriteLine($"Is Alive: {hero.IsAlive}");

            hero.Health = 0;
            Console.WriteLine($"Is Alive: {hero.IsAlive}");
        }
    }
}Code language: C# (cs)

Explanation:

  • public bool IsAlive { get; private set; } = true;: Starts as true by 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 set block, check whether value is greater than 10000.
  • If it is, multiply the value by 1.02 before storing it, applying the 2% bonus.
  • Otherwise, store the value as-is with no bonus applied.
▼ Solution & Explanation
using System;
namespace SavingsAccountApplication
{
    class SavingsAccount
    {
        private double balance;

        public double Balance
        {
            get { return balance; }
            set
            {
                if (value > 10000)
                {
                    balance = value * 1.02;
                }
                else
                {
                    balance = value;
                }
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            SavingsAccount account = new SavingsAccount();
            account.Balance = 15000;

            Console.WriteLine($"Balance = {account.Balance}");
        }
    }
}Code language: C# (cs)

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() as virtual in the Vehicle base class.
  • In both Car and Motorcycle, use : Vehicle to inherit, and override to provide a unique startup message.
  • Store each object in a Vehicle variable to demonstrate that the correct overridden version still runs.
▼ Solution & Explanation
using System;
namespace VehicleHierarchyApplication
{
    class Vehicle
    {
        public virtual void StartEngine()
        {
            Console.WriteLine("The vehicle starts.");
        }
    }

    class Car : Vehicle
    {
        public override void StartEngine()
        {
            Console.WriteLine("Vroom! The car engine roars to life.");
        }
    }

    class Motorcycle : Vehicle
    {
        public override void StartEngine()
        {
            Console.WriteLine("Vroooom! The motorcycle engine revs up.");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Vehicle car = new Car();
            Vehicle motorcycle = new Motorcycle();

            car.StartEngine();
            motorcycle.StartEngine();
        }
    }
}Code language: C# (cs)

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 is Vehicle, calling StartEngine() runs the overridden version specific to Car.

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 SecurityCore a virtual method that StrictAuth can override.
  • Add the sealed keyword directly before class StrictAuth.
  • A sealed class can still be instantiated and used normally; it simply cannot be inherited from any further.
▼ Solution & Explanation
using System;
namespace SealedSecurityApplication
{
    class SecurityCore
    {
        public virtual void Authenticate()
        {
            Console.WriteLine("Running base authentication.");
        }
    }

    sealed class StrictAuth : SecurityCore
    {
        public override void Authenticate()
        {
            Console.WriteLine("Running strict authentication checks.");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            SecurityCore auth = new StrictAuth();
            auth.Authenticate();
        }
    }
}Code language: C# (cs)

Explanation:

  • sealed class StrictAuth : SecurityCore: Inherits from SecurityCore as normal, but the sealed keyword prevents any other class from inheriting from StrictAuth itself.
  • 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() as virtual in the base Employee class, with a default return value like 0.
  • In HourlyEmployee, override it to return HourlyRate * HoursWorked.
  • In SalariedEmployee, override it to simply return MonthlySalary.
▼ Solution & Explanation
using System;
namespace PayrollApplication
{
    class Employee
    {
        public virtual double CalculatePay()
        {
            return 0;
        }
    }

    class HourlyEmployee : Employee
    {
        public double HourlyRate { get; set; }
        public double HoursWorked { get; set; }

        public override double CalculatePay()
        {
            return HourlyRate * HoursWorked;
        }
    }

    class SalariedEmployee : Employee
    {
        public double MonthlySalary { get; set; }

        public override double CalculatePay()
        {
            return MonthlySalary;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            HourlyEmployee hourly = new HourlyEmployee { HourlyRate = 20, HoursWorked = 80 };
            SalariedEmployee salaried = new SalariedEmployee { MonthlySalary = 4000 };

            Console.WriteLine($"Hourly Employee Pay = {hourly.CalculatePay()}");
            Console.WriteLine($"Salaried Employee Pay = {salaried.CalculatePay()}");
        }
    }
}Code language: C# (cs)

Explanation:

  • public virtual double CalculatePay(): Establishes a shared method signature that every subclass can customize with its own pay calculation.
  • HourlyRate * HoursWorked: The HourlyEmployee version calculates pay based on hours actually worked.
  • return MonthlySalary;: The SalariedEmployee version 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 Person a constructor that takes a name parameter and assigns it to a Name property.
  • In the Student constructor, add : base(name) after the parameter list to forward the name up to the base class constructor.
  • The Student constructor only needs to handle its own additional property, School, since Name is already handled by the base constructor.
▼ Solution & Explanation
using System;
namespace ConstructorChainingApplication
{
    class Person
    {
        public string Name { get; set; }

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

    class Student : Person
    {
        public string School { get; set; }

        public Student(string name, string school) : base(name)
        {
            School = school;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Student student = new Student("Emma", "Springfield High");

            Console.WriteLine($"Name: {student.Name}, School: {student.School}");
        }
    }
}Code language: C# (cs)

Explanation:

  • public Student(string name, string school) : base(name): Passes name straight up to the Person constructor before the Student constructor’s own body runs.
  • School = school;: Handles only the property that is specific to Student, since Name was 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() as virtual in the base RPGCharacter class.
  • In Knight, override it using sealed override void Move(), combining both keywords together.
  • A class named Paladin could still inherit from Knight and override other methods, but it would not be allowed to override Move() again.
▼ Solution & Explanation
using System;
namespace RPGCharacterApplication
{
    class RPGCharacter
    {
        public virtual void Move()
        {
            Console.WriteLine("The character moves.");
        }
    }

    class Knight : RPGCharacter
    {
        public sealed override void Move()
        {
            Console.WriteLine("The knight marches forward in heavy armor.");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            RPGCharacter knight = new Knight();
            knight.Move();
        }
    }
}Code language: C# (cs)

Explanation:

  • public sealed override void Move(): Overrides the base class method as usual, but the sealed keyword prevents any class inheriting from Knight from overriding Move() a second time.
  • RPGCharacter knight = new Knight();: Calling Move() through the base type still runs the sealed, overridden version from Knight.

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 Animal a virtual 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 foreach loop calling animal.MakeSound() will automatically run each object’s own overridden version.
▼ Solution & Explanation
using System;
namespace ZooSoundboardApplication
{
    class Animal
    {
        public virtual void MakeSound()
        {
            Console.WriteLine("The animal makes a sound.");
        }
    }

    class Lion : Animal
    {
        public override void MakeSound()
        {
            Console.WriteLine("Roar!");
        }
    }

    class Sparrow : Animal
    {
        public override void MakeSound()
        {
            Console.WriteLine("Tweet!");
        }
    }

    class Frog : Animal
    {
        public override void MakeSound()
        {
            Console.WriteLine("Ribbit!");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Animal[] animals = { new Lion(), new Sparrow(), new Frog() };

            foreach (Animal animal in animals)
            {
                animal.MakeSound();
            }
        }
    }
}Code language: C# (cs)

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 as Animal, 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 DerivedClass object, then assign it to both a DerivedClass variable and a BaseClass variable to see how the method call differs depending on the variable’s declared type.
▼ Solution & Explanation
using System;
namespace ShadowingApplication
{
    class BaseClass
    {
        public void Display()
        {
            Console.WriteLine("Display from BaseClass");
        }
    }

    class DerivedClass : BaseClass
    {
        public new void Display()
        {
            Console.WriteLine("Display from DerivedClass");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            DerivedClass derived = new DerivedClass();
            BaseClass baseRef = derived;

            derived.Display();
            baseRef.Display();
        }
    }
}Code language: C# (cs)

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();: Since derived is declared as DerivedClass, this calls the shadowed version.
  • baseRef.Display();: Even though baseRef points to the same object, its declared type is BaseClass, 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() as virtual in Device, and as override in both KitchenAppliance and Microwave.
  • 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
using System;
namespace MultiLevelApplianceApplication
{
    class Device
    {
        public virtual void PowerOn()
        {
            Console.WriteLine("Device powering on.");
        }
    }

    class KitchenAppliance : Device
    {
        public override void PowerOn()
        {
            base.PowerOn();
            Console.WriteLine("Kitchen appliance initializing.");
        }
    }

    class Microwave : KitchenAppliance
    {
        public override void PowerOn()
        {
            base.PowerOn();
            Console.WriteLine("Microwave ready to heat.");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Microwave microwave = new Microwave();
            microwave.PowerOn();
        }
    }
}Code language: C# (cs)

Explanation:

  • class KitchenAppliance : Device / class Microwave : KitchenAppliance: Forms a three-level chain, where Microwave inherits from KitchenAppliance, which in turn inherits from Device.
  • 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 Canvas a private List<UIElement> field to hold whatever elements are added to it.
  • Write an AddElement() method that accepts any UIElement, including its subclasses.
  • In RenderAll(), loop through the list and call Draw() on each item, letting polymorphism select the correct overridden version.
▼ Solution & Explanation
using System;
using System.Collections.Generic;

namespace UIRenderingApplication
{
    class UIElement
    {
        public virtual void Draw()
        {
            Console.WriteLine("Drawing a generic UI element.");
        }
    }

    class Button : UIElement
    {
        public override void Draw()
        {
            Console.WriteLine("Drawing a button.");
        }
    }

    class TextBox : UIElement
    {
        public override void Draw()
        {
            Console.WriteLine("Drawing a text box.");
        }
    }

    class Checkbox : UIElement
    {
        public override void Draw()
        {
            Console.WriteLine("Drawing a checkbox.");
        }
    }

    class Canvas
    {
        private List<UIElement> elements = new List<UIElement>();

        public void AddElement(UIElement element)
        {
            elements.Add(element);
        }

        public void RenderAll()
        {
            foreach (UIElement element in elements)
            {
                element.Draw();
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Canvas canvas = new Canvas();
            canvas.AddElement(new Button());
            canvas.AddElement(new TextBox());
            canvas.AddElement(new Checkbox());

            canvas.RenderAll();
        }
    }
}Code language: C# (cs)

Explanation:

  • List<UIElement> elements: Holds any mix of Button, TextBox, and Checkbox objects together, since they all share the UIElement base type.
  • element.Draw();: Calls each object’s own overridden Draw() method automatically, without Canvas needing 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 as public abstract void ...();, with no method body.
  • Every concrete subclass, like SqlConnection, must provide an override for 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
using System;
namespace DatabaseConnectorApplication
{
    abstract class DatabaseConnection
    {
        public abstract void OpenConnection();
        public abstract void CloseConnection();
    }

    class SqlConnection : DatabaseConnection
    {
        public override void OpenConnection()
        {
            Console.WriteLine("Opening SQL Server connection.");
        }

        public override void CloseConnection()
        {
            Console.WriteLine("Closing SQL Server connection.");
        }
    }

    class MongoDbConnection : DatabaseConnection
    {
        public override void OpenConnection()
        {
            Console.WriteLine("Opening MongoDB connection.");
        }

        public override void CloseConnection()
        {
            Console.WriteLine("Closing MongoDB connection.");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            DatabaseConnection connection = new SqlConnection();
            connection.OpenConnection();
            connection.CloseConnection();
        }
    }
}Code language: C# (cs)

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 concrete SqlConnection object, 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 Triangle three side-length fields and a constructor that sets all of them.
  • Give Hexagon a 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
using System;
namespace AbstractShapeApplication
{
    abstract class Shape
    {
        public abstract double GetPerimeter();
    }

    class Triangle : Shape
    {
        public double SideA { get; set; }
        public double SideB { get; set; }
        public double SideC { get; set; }

        public Triangle(double sideA, double sideB, double sideC)
        {
            SideA = sideA;
            SideB = sideB;
            SideC = sideC;
        }

        public override double GetPerimeter()
        {
            return SideA + SideB + SideC;
        }
    }

    class Hexagon : Shape
    {
        public double SideLength { get; set; }

        public Hexagon(double sideLength)
        {
            SideLength = sideLength;
        }

        public override double GetPerimeter()
        {
            return SideLength * 6;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Triangle triangle = new Triangle(3, 4, 5);
            Hexagon hexagon = new Hexagon(4);

            Console.WriteLine($"Triangle Perimeter = {triangle.GetPerimeter()}");
            Console.WriteLine($"Hexagon Perimeter = {hexagon.GetPerimeter()}");
        }
    }
}Code language: C# (cs)

Explanation:

  • SideA + SideB + SideC: The Triangle version adds all three unique side lengths.
  • SideLength * 6: The Hexagon version 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() and AddCondiments() as abstract, since these steps differ depending on the drink.
  • In Main, call all three methods on the Latte object, one right after another.
▼ Solution & Explanation
using System;
namespace CoffeeMachineApplication
{
    abstract class WarmBeverage
    {
        public void BoilWater()
        {
            Console.WriteLine("Boiling water to 100°C.");
        }

        public abstract void Brew();
        public abstract void AddCondiments();
    }

    class Latte : WarmBeverage
    {
        public override void Brew()
        {
            Console.WriteLine("Brewing espresso shots.");
        }

        public override void AddCondiments()
        {
            Console.WriteLine("Adding steamed milk.");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Latte latte = new Latte();
            latte.BoilWater();
            latte.Brew();
            latte.AddCondiments();
        }
    }
}Code language: C# (cs)

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, so Latte must supply its own brewing logic.
  • latte.BoilWater();: Calls the inherited concrete method directly on the Latte object, 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 Document a protected constructor that accepts a filePath parameter and assigns it to a property.
  • A protected constructor can still be called by subclasses using : base(filePath), even though the abstract class itself can’t be instantiated directly.
  • In ParseContent(), use the inherited FilePath property to build the output message.
▼ Solution & Explanation
using System;
namespace DocumentParserApplication
{
    abstract class Document
    {
        public string FilePath { get; private set; }

        protected Document(string filePath)
        {
            FilePath = filePath;
        }

        public abstract void ParseContent();
    }

    class PdfParser : Document
    {
        public PdfParser(string filePath) : base(filePath) { }

        public override void ParseContent()
        {
            Console.WriteLine($"Parsing PDF content from {FilePath}");
        }
    }

    class CsvParser : Document
    {
        public CsvParser(string filePath) : base(filePath) { }

        public override void ParseContent()
        {
            Console.WriteLine($"Parsing CSV content from {FilePath}");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Document pdfDocument = new PdfParser("report.pdf");
            pdfDocument.ParseContent();
        }
    }
}Code language: C# (cs)

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, letting Document handle 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, concrete protected method 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 a bool.
  • Inside each subclass’s ProcessPayment() override, call ValidateTransaction() first before continuing.
▼ Solution & Explanation
using System;
namespace PaymentGatewayApplication
{
    abstract class PaymentProcessor
    {
        protected bool ValidateTransaction(double amount)
        {
            return amount > 0;
        }

        public abstract void ProcessPayment(double amount);
    }

    class PayPal : PaymentProcessor
    {
        public override void ProcessPayment(double amount)
        {
            if (ValidateTransaction(amount))
            {
                Console.WriteLine($"Processing ${amount} through PayPal.");
            }
            else
            {
                Console.WriteLine("Invalid transaction amount.");
            }
        }
    }

    class Stripe : PaymentProcessor
    {
        public override void ProcessPayment(double amount)
        {
            if (ValidateTransaction(amount))
            {
                Console.WriteLine($"Processing ${amount} through Stripe.");
            }
            else
            {
                Console.WriteLine("Invalid transaction amount.");
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            PaymentProcessor paypal = new PayPal();
            paypal.ProcessPayment(150.00);
        }
    }
}Code language: C# (cs)

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’s ProcessPayment() 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 a get accessor, 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 AggressionLevel inside each subclass’s ExecuteTurn() method to include it in the printed message.
▼ Solution & Explanation
using System;
namespace GameAIApplication
{
    abstract class EnemyAI
    {
        public abstract void ExecuteTurn();
        protected abstract int AggressionLevel { get; }
    }

    class Goblin : EnemyAI
    {
        protected override int AggressionLevel => 3;

        public override void ExecuteTurn()
        {
            Console.WriteLine($"Goblin attacks cautiously with aggression level {AggressionLevel}.");
        }
    }

    class Dragon : EnemyAI
    {
        protected override int AggressionLevel => 9;

        public override void ExecuteTurn()
        {
            Console.WriteLine($"Dragon unleashes a devastating attack with aggression level {AggressionLevel}.");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            EnemyAI goblin = new Goblin();
            EnemyAI dragon = new Dragon();

            goblin.ExecuteTurn();
            dragon.ExecuteTurn();
        }
    }
}Code language: C# (cs)

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 for Goblin.

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
using System;
namespace PrintScanApplication
{
    interface IPrinter
    {
        void Print(string document);
    }

    interface IScanner
    {
        void Scan(string document);
    }

    class AllInOnePrinter : IPrinter, IScanner
    {
        public void Print(string document)
        {
            Console.WriteLine($"Printing: {document}");
        }

        public void Scan(string document)
        {
            Console.WriteLine($"Scanning: {document}");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            AllInOnePrinter device = new AllInOnePrinter();
            device.Print("Report.docx");
            device.Scan("Receipt.jpg");
        }
    }
}Code language: C# (cs)

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 Driver object to both an ILeftTurn variable and an IRightTurn variable to call each version separately.
▼ Solution & Explanation
using System;
namespace ExplicitConflictApplication
{
    interface ILeftTurn
    {
        void Execute();
    }

    interface IRightTurn
    {
        void Execute();
    }

    class Driver : ILeftTurn, IRightTurn
    {
        void ILeftTurn.Execute()
        {
            Console.WriteLine("Turning left.");
        }

        void IRightTurn.Execute()
        {
            Console.WriteLine("Turning right.");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Driver driver = new Driver();

            ILeftTurn leftTurn = driver;
            IRightTurn rightTurn = driver;

            leftTurn.Execute();
            rightTurn.Execute();
        }
    }
}Code language: C# (cs)

Explanation:

  • void ILeftTurn.Execute(): Explicitly ties this implementation to the ILeftTurn interface specifically, keeping it separate from IRightTurn‘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 ILogger only needs to provide LogError(); it automatically inherits the working LogInfo() unless it chooses to override it.
▼ Solution & Explanation
using System;
namespace DefaultLoggerApplication
{
    interface ILogger
    {
        void LogError(string msg);

        void LogInfo(string msg)
        {
            Console.WriteLine($"[INFO {DateTime.Now:HH:mm:ss}] {msg}");
        }
    }

    class ConsoleLogger : ILogger
    {
        public void LogError(string msg)
        {
            Console.WriteLine($"[ERROR] {msg}");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ILogger logger = new ConsoleLogger();
            logger.LogError("Something went wrong.");
            logger.LogInfo("Application started.");
        }
    }
}Code language: C# (cs)

Explanation:

  • void LogInfo(string msg) { ... }: A default interface method with a real implementation, which ConsoleLogger inherits automatically without writing any code for it.
  • logger.LogInfo(...): Works even though ConsoleLogger never 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 IPlugin parameter, not a specific plugin class.
  • Any class implementing IPlugin can 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
using System;
namespace PluginArchitectureApplication
{
    interface IPlugin
    {
        void Start();
    }

    class LoggingPlugin : IPlugin
    {
        public void Start()
        {
            Console.WriteLine("Logging plugin started.");
        }
    }

    class AnalyticsPlugin : IPlugin
    {
        public void Start()
        {
            Console.WriteLine("Analytics plugin started.");
        }
    }

    class PluginHost
    {
        public void RunPlugin(IPlugin plugin)
        {
            plugin.Start();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            PluginHost host = new PluginHost();
            host.RunPlugin(new LoggingPlugin());
            host.RunPlugin(new AnalyticsPlugin());
        }
    }
}Code language: C# (cs)

Explanation:

  • public void RunPlugin(IPlugin plugin): Accepts any object implementing IPlugin, without PluginHost needing 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 the Damage values of the two weapons using Damage.CompareTo(other.Damage).
  • Once CompareTo() is implemented, calling .Sort() directly on a List<Weapon> will automatically use it.
▼ Solution & Explanation
using System;
using System.Collections.Generic;

namespace SortableInventoryApplication
{
    class Weapon : IComparable<Weapon>
    {
        public string Name { get; set; }
        public int Damage { get; set; }

        public int CompareTo(Weapon other)
        {
            return Damage.CompareTo(other.Damage);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<Weapon> weapons = new List<Weapon>
            {
                new Weapon { Name = "Dagger", Damage = 15 },
                new Weapon { Name = "Greatsword", Damage = 45 },
                new Weapon { Name = "Bow", Damage = 25 }
            };

            weapons.Sort();

            foreach (Weapon weapon in weapons)
            {
                Console.WriteLine($"{weapon.Name} - {weapon.Damage}");
            }
        }
    }
}Code language: C# (cs)

Explanation:

  • class Weapon : IComparable<Weapon>: Signals that Weapon objects know how to compare themselves against one another.
  • Damage.CompareTo(other.Damage): Defines “smaller” and “larger” weapons purely by their Damage value.
  • weapons.Sort();: Automatically uses the custom CompareTo() 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 : IDisposable after the class name, then implement a public void Dispose() method.
  • Wrap the object’s creation in using (DatabaseSession session = new DatabaseSession()) { ... }.
  • Dispose() is called automatically as soon as execution leaves the using block, whether it finishes normally or exits early.
▼ Solution & Explanation
using System;
namespace DisposableResourcesApplication
{
    class DatabaseSession : IDisposable
    {
        public DatabaseSession()
        {
            Console.WriteLine("Database session opened.");
        }

        public void Dispose()
        {
            Console.WriteLine("Database session closed.");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            using (DatabaseSession session = new DatabaseSession())
            {
                Console.WriteLine("Running database query.");
            }
        }
    }
}Code language: C# (cs)

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 calls Dispose() 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 Vehicle to hold shared identity data like Name, set through its constructor.
  • Implement Fly() and Swim() as ordinary methods that fulfill each interface’s contract.
▼ Solution & Explanation
using System;
namespace AbstractInterfaceApplication
{
    abstract class Vehicle
    {
        public string Name { get; set; }

        protected Vehicle(string name)
        {
            Name = name;
        }
    }

    interface IFlyable
    {
        void Fly();
    }

    interface ISwimmable
    {
        void Swim();
    }

    class FlyingBoat : Vehicle, IFlyable, ISwimmable
    {
        public FlyingBoat(string name) : base(name) { }

        public void Fly()
        {
            Console.WriteLine($"{Name} is soaring through the sky.");
        }

        public void Swim()
        {
            Console.WriteLine($"{Name} is gliding across the water.");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            FlyingBoat boat = new FlyingBoat("SkyCruiser");
            boat.Fly();
            boat.Swim();
        }
    }
}Code language: C# (cs)

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 abstract Vehicle constructor, 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 a public modifier.
  • A regular User variable will not show ResetSystem() as an available method at all.
  • Only by assigning the object to a variable typed as IAdmin, or casting it directly, can ResetSystem() be called.
▼ Solution & Explanation
using System;
namespace ExplicitHidingApplication
{
    interface IAdmin
    {
        void ResetSystem();
    }

    class User : IAdmin
    {
        void IAdmin.ResetSystem()
        {
            Console.WriteLine("System reset performed.");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            User user = new User();

            IAdmin admin = user;
            admin.ResetSystem();
        }
    }
}Code language: C# (cs)

Explanation:

  • void IAdmin.ResetSystem(): Implements the method explicitly, which hides it from the User type entirely; calling user.ResetSystem() directly would fail to compile.
  • IAdmin admin = user;: Only through a variable typed as IAdmin does ResetSystem() become visible and callable.

Filed Under: C# Exercises

Did you find this page helpful? Let others know about it. Sharing helps me continue to create free Python resources.

TweetF  sharein  shareP  Pin

About Vishal

I’m Vishal Hule, the Founder of PYnative.com. As a Python developer, I enjoy assisting students, developers, and learners. Follow me on Twitter.

Related Tutorial Topics:

C# Exercises

All Coding Exercises:

C Exercises
C++ Exercises
Python Exercises
Java Exercises
C# Exercises

Python Exercises and Quizzes

Free coding exercises and quizzes cover Python basics, data structure, data analytics, and more.

  • 15+ Topic-specific Exercises and Quizzes
  • Each Exercise contains 25+ questions
  • Each Quiz contains 25 MCQ
Exercises
Quizzes

Leave a Reply Cancel reply

your email address will NOT be published. all comments are moderated according to our comment policy.

Use <pre> tag for posting code. E.g. <pre> Your entire code </pre>

In: C# Exercises
TweetF  sharein  shareP  Pin

  C# Exercises

  • C# Exercise for Beginners
  • C# Loops Exercise
  • C# Array Exercise
  • C# String Exercise
  • C# OOP Exercise
  • C# Structs, Records and Enums Exercise
  • C# Collections Exercise
  • C# List Exercise
  • C# Dictionary Exercise
  • C# LINQ Exercise
  • C# Exception Handling Exercise
  • C# File Handling Exercise
  • C# Date and Time Exercise
  • C# Generics Exercise
  • C# Lambda Expressions Exercise
  • C# Delegates and Events Exercise
  • C# Extension Methods Exercise
  • C# Regex Exercise
  • C# Pattern Matching Exercise
  • C# Iterators and Yield Exercise
  • C# Indexers and Operator Overloading Exercise
  • C# Random Data Generation Exercise

All Coding Exercises

Python Exercises C Exercises C++ Exercises Java Exercises C# Exercises

About PYnative

PYnative.com is for Python lovers. Here, You can get Tutorials, Exercises, and Quizzes to practice and improve your Python skills.

Follow Us

To get New Python Tutorials, Exercises, and Quizzes

  • Twitter
  • Facebook
  • Sitemap

Explore Python

  • Learn Python
  • Python Basics
  • Python Databases
  • Python Exercises
  • Python Quizzes
  • Online Python Code Editor
  • Python Tricks

Coding Exercises

  • C Exercises
  • C++ Exercises
  • Python Exercises
  • Java Exercises
  • C# Exercises

Legal Stuff

  • About Us
  • Contact Us

We use cookies to improve your experience. While using PYnative, you agree to have read and accepted our:

  • Terms Of Use
  • Privacy Policy
  • Cookie Policy

Copyright © 2018–2026 pynative.com