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# Generics Exercises: 20 Coding Problems with Solutions

C# Generics Exercises: 20 Coding Problems with Solutions

Updated on: July 15, 2026 | Leave a Comment

Generics let you write type-safe, reusable code without duplicating logic for every data type, and they power much of the .NET collections framework under the hood.

This collection of 20 C# generics exercises covers writing your own generic methods and classes, applying type constraints, and understanding how generics improve both safety and performance compared to using object.

Each exercise includes a Practice Problem, Purpose, Hint, and a full Solution with Explanation, so you build real confidence writing reusable, type-safe C# code.

Also, See: C# Exercises with  22 topic-wise sets and 620+ practice questions.

What You’ll Practice

  • Generic Methods: Type parameters and type inference.
  • Generic Classes: Building reusable containers and utility classes.
  • Constraints: where clauses like struct, class, and new().
  • Real-World Use: Comparing generics to object-based and non-generic code.
+ Table Of Contents (20 Exercises)

Table of contents

  • Exercise 1: Generic Box
  • Exercise 2: Swap Array Elements
  • Exercise 3: Generic Pair
  • Exercise 4: Print Collection
  • Exercise 5: Reverse Array
  • Exercise 6: Max Finder (Comparable Constraint)
  • Exercise 7: Entity Repository (Class Constraint)
  • Exercise 8: Factory Pattern (New() Constraint)
  • Exercise 9: Generic Stack
  • Exercise 10: ID-Based Search
  • Exercise 11: Nullable Struct Wrapper
  • Exercise 12: Double Constraint Repository
  • Exercise 13: Generic Validator
  • Exercise 14: Generic Cache
  • Exercise 15: Type Converter
  • Exercise 16: Covariant Logger (Out Keyword)
  • Exercise 17: Contravariant Consumer (In Keyword)
  • Exercise 18: Generic Event Broker
  • Exercise 19: Custom LINQ Filter
  • Exercise 20: Generic Spec Evaluation

Exercise 1: Generic Box

Practice Problem: Create a generic Box<T> class that holds a single value of type T. Include methods to Update(T newValue) and Retrieve().

Purpose: This exercise helps you practice defining a generic class, storing a value of a type parameter in a private field, and exposing methods to read and update that value safely without casting or boxing.

Given Input: Box<int> intBox = new Box<int>(42); intBox.Update(100);

Expected Output: Retrieved Value = 100

▼ Hint
  • Declare a private field of type T to store the value.
  • Add a constructor that accepts an initial value of type T.
  • Implement Update(T newValue) to overwrite the stored field.
  • Implement Retrieve() to return the current stored value.
▼ Solution & Explanation
using System;

public class Box<T>
{
    private T _value;

    public Box(T initialValue)
    {
        _value = initialValue;
    }

    public void Update(T newValue)
    {
        _value = newValue;
    }

    public T Retrieve()
    {
        return _value;
    }
}

class Program
{
    static void Main()
    {
        Box<int> intBox = new Box<int>(42);
        intBox.Update(100);
        Console.WriteLine("Retrieved Value = " + intBox.Retrieve());
    }
}Code language: C# (cs)

Explanation:

  • private T _value: Stores a value of whatever type T is bound to when the class is instantiated.
  • Box(T initialValue): The constructor assigns the starting value to the field without any casting.
  • Update(T newValue): Replaces the internal value with a new one of the same type T.
  • Retrieve(): Returns the current value, type safe because T is fixed for this instance.

Exercise 2: Swap Array Elements

Practice Problem: Write a generic method Swap<T>(T[] array, int index1, int index2) that swaps the positions of two elements in any array.

Purpose: This exercise helps you practice writing a standalone generic method (not tied to a generic class), letting the compiler infer the type parameter from the array argument, and mutating an array in place.

Given Input: int[] numbers = { 1, 2, 3, 4, 5 }; Swap(numbers, 1, 3);

Expected Output: {1, 4, 3, 2, 5}

▼ Hint
  • Declare a static generic method Swap<T>(T[] array, int index1, int index2).
  • Store array[index1] in a temporary variable before overwriting it.
  • Assign array[index2] to array[index1].
  • Assign the saved temporary value to array[index2] to complete the swap.
▼ Solution & Explanation
using System;

public static class ArrayHelper
{
    public static void Swap<T>(T[] array, int index1, int index2)
    {
        T temp = array[index1];
        array[index1] = array[index2];
        array[index2] = temp;
    }
}

class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        ArrayHelper.Swap(numbers, 1, 3);
        Console.WriteLine("{" + string.Join(", ", numbers) + "}");
    }
}Code language: C# (cs)

Explanation:

  • public static class ArrayHelper: In C#, a method cannot exist on its own; it must live inside a class, so the generic method is placed in a small static helper class.
  • static void Swap<T>(T[] array, ...): Declares a generic method whose type parameter T is inferred automatically from the array passed in.
  • T temp = array[index1]: Saves the first element so it is not lost once it gets overwritten.
  • array[index1] = array[index2]: Copies the second element into the first position.
  • array[index2] = temp: Places the saved value into the second position, finishing the swap.

Exercise 3: Generic Pair

Practice Problem: Create a Pair<TKey, TValue> class that holds two related values of different types, mimicking a basic KeyValuePair.

Purpose: This exercise helps you practice working with multiple type parameters on a single class, and shows how two independent generic types can be combined into one reusable structure.

Given Input: Pair<string, int> pair = new Pair<string, int>("Age", 25);

Expected Output: Key = Age, Value = 25

▼ Hint
  • Declare two type parameters, TKey and TValue, on the class.
  • Add two properties, one of type TKey and one of type TValue.
  • Add a constructor that accepts both values and assigns them to the properties.
  • Override ToString(), or print the properties directly, to display the pair.
▼ Solution & Explanation
using System;

public class Pair<TKey, TValue>
{
    public TKey Key { get; set; }
    public TValue Value { get; set; }

    public Pair(TKey key, TValue value)
    {
        Key = key;
        Value = value;
    }
}

class Program
{
    static void Main()
    {
        Pair<string, int> pair = new Pair<string, int>("Age", 25);
        Console.WriteLine("Key = " + pair.Key + ", Value = " + pair.Value);
    }
}Code language: C# (cs)

Explanation:

  • Pair<TKey, TValue>: Declares two independent type parameters so the key and value can be different, unrelated types.
  • public TKey Key { get; set; }: Exposes the first value as a strongly typed property.
  • public TValue Value { get; set; }: Exposes the second value as a strongly typed property.
  • Pair(TKey key, TValue value): The constructor initializes both properties in a single call.

Exercise 4: Print Collection

Practice Problem: Write a generic method PrintList<T>(List<T> list) that iterates through and prints every element of a list to the console.

Purpose: This exercise helps you practice iterating over a generic List<T>, and writing a reusable method that works for a list of any element type.

Given Input: List<string> fruits = new List<string> { "Apple", "Banana", "Cherry" };

Expected Output:

Apple
Banana
Cherry
▼ Hint
  • Declare a generic method PrintList<T>(List<T> list).
  • Use a foreach loop to go through each element in list.
  • Print each element on its own line with Console.WriteLine.
▼ Solution & Explanation
using System;
using System.Collections.Generic;

public static class ListPrinter
{
    public static void PrintList<T>(List<T> list)
    {
        foreach (T item in list)
        {
            Console.WriteLine(item);
        }
    }
}

class Program
{
    static void Main()
    {
        List<string> fruits = new List<string> { "Apple", "Banana", "Cherry" };
        ListPrinter.PrintList(fruits);
    }
}Code language: C# (cs)

Explanation:

  • public static class ListPrinter: Methods cannot exist outside a class in C#, so the generic method is placed inside a small static helper class.
  • PrintList<T>(List<T> list): Declares a generic method that accepts a list of any element type T.
  • foreach (T item in list): Iterates through every element without needing to know the concrete type in advance.
  • Console.WriteLine(item): Prints each element, relying on its default ToString() representation.

Exercise 5: Reverse Array

Practice Problem: Write a generic method Reverse<T>(T[] array) that reverses the elements of an array in place.

Purpose: This exercise helps you practice the two-pointer technique for in-place array manipulation, combined with generic type parameters so the logic works for any element type.

Given Input: int[] numbers = { 1, 2, 3, 4, 5 };

Expected Output: {5, 4, 3, 2, 1}

▼ Hint
  • Keep two index pointers, one starting at the beginning and one at the end.
  • Swap the elements at the two pointers, then move them toward each other.
  • Continue until the left pointer meets or passes the right pointer.
▼ Solution & Explanation
using System;

public static class ArrayHelper
{
    public static void Reverse<T>(T[] array)
    {
        int left = 0;
        int right = array.Length - 1;

        while (left < right)
        {
            T temp = array[left];
            array[left] = array[right];
            array[right] = temp;
            left++;
            right--;
        }
    }
}

class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        ArrayHelper.Reverse(numbers);
        Console.WriteLine("{" + string.Join(", ", numbers) + "}");
    }
}Code language: C# (cs)

Explanation:

  • public static class ArrayHelper: Since a method cannot exist outside a class in C#, the generic method is placed inside a small static helper class.
  • int left = 0; int right = array.Length - 1: Sets up two pointers at opposite ends of the array.
  • while (left < right): Keeps swapping as long as the pointers have not crossed.
  • T temp = array[left]: Temporarily holds the left value so it can be restored on the right side.
  • left++; right--: Moves both pointers one step closer to the middle after each swap.

Exercise 6: Max Finder (Comparable Constraint)

Practice Problem: Write a generic method FindMax<T>(T a, T b) that returns the larger of two values. Restrict T to types that implement IComparable<T>.

Purpose: This exercise helps you practice applying a generic type constraint so the compiler guarantees the type supports comparison, letting one method work correctly across numbers, strings, and any other comparable type.

Given Input: FindMax(10, 20) and FindMax("apple", "banana")

Expected Output:

Max = 20
Max = banana
▼ Hint
  • Constrain the method with where T : IComparable<T>.
  • Call a.CompareTo(b) to compare the two values.
  • Return a if the result is greater than zero, otherwise return b.
▼ Solution & Explanation
using System;

public static class Comparer
{
    public static T FindMax<T>(T a, T b) where T : IComparable<T>
    {
        return a.CompareTo(b) > 0 ? a : b;
    }
}

class Program
{
    static void Main()
    {
        Console.WriteLine("Max = " + Comparer.FindMax(10, 20));
        Console.WriteLine("Max = " + Comparer.FindMax("apple", "banana"));
    }
}Code language: C# (cs)

Explanation:

  • public static class Comparer: A method cannot exist outside a class in C#, so the generic method is placed inside a small static helper class.
  • where T : IComparable<T>: Restricts T to types that know how to compare themselves to another instance of the same type.
  • a.CompareTo(b): Returns a positive number when a is greater, zero when equal, and negative when smaller.
  • > 0 ? a : b: Uses the comparison result to pick and return the larger of the two values.

Exercise 7: Entity Repository (Class Constraint)

Practice Problem: Create a generic repository class Repository<T> where T must be a reference type (where T : class). Include basic Add(T entity) and GetAll() methods.

Purpose: This exercise helps you practice using a reference-type constraint to model a common data-access pattern, and shows how a single generic class can store and return any entity type.

Given Input: Repository<string> repo = new Repository<string>(); repo.Add("Item1"); repo.Add("Item2");

Expected Output:

Item1
Item2
▼ Hint
  • Add where T : class to the class declaration to restrict T to reference types.
  • Store entities internally in a List<T>.
  • Add(T entity) should append to the internal list.
  • GetAll() should return the internal list, or a read-only view of it.
▼ Solution & Explanation
using System;
using System.Collections.Generic;

public class Repository<T> where T : class
{
    private List<T> _entities = new List<T>();

    public void Add(T entity)
    {
        _entities.Add(entity);
    }

    public List<T> GetAll()
    {
        return _entities;
    }
}

class Program
{
    static void Main()
    {
        Repository<string> repo = new Repository<string>();
        repo.Add("Item1");
        repo.Add("Item2");

        foreach (string item in repo.GetAll())
        {
            Console.WriteLine(item);
        }
    }
}Code language: C# (cs)

Explanation:

  • where T : class: Restricts T to reference types, which fits the typical use case of storing entity objects rather than value types.
  • private List<T> _entities: Backs the repository with an in-memory generic list.
  • Add(T entity): Appends a new entity to the internal collection.
  • GetAll(): Returns every stored entity so the caller can iterate over them.

Exercise 8: Factory Pattern (New() Constraint)

Practice Problem: Create a generic factory class EntityFactory<T> with a method T CreateInstance() that returns a new instance of T. Use the where T : new() constraint.

Purpose: This exercise helps you practice the new() constraint, which lets a generic class create instances of its type parameter directly, a common building block for factory and dependency-injection style patterns.

Given Input: EntityFactory<Product> factory = new EntityFactory<Product>(); Product product = factory.CreateInstance();

Expected Output: New instance created: Product

▼ Hint
  • Add where T : new() to the class declaration so the compiler knows T has a parameterless constructor.
  • Inside CreateInstance(), return new T().
  • Use typeof(T).Name if you want to print the created type’s name.
▼ Solution & Explanation
using System;

public class Product
{
}

public class EntityFactory<T> where T : new()
{
    public T CreateInstance()
    {
        return new T();
    }
}

class Program
{
    static void Main()
    {
        EntityFactory<Product> factory = new EntityFactory<Product>();
        Product product = factory.CreateInstance();
        Console.WriteLine("New instance created: " + product.GetType().Name);
    }
}Code language: C# (cs)

Explanation:

  • where T : new(): Guarantees that T has an accessible parameterless constructor, so it can be created inside the generic class.
  • return new T(): Creates a brand new instance of whatever type T was bound to.
  • product.GetType().Name: Reads the runtime type name so the created type can be printed, here it resolves to Product.

Exercise 9: Generic Stack

Practice Problem: Implement a custom CustomStack<T> class, without using System.Collections.Generic.Stack, using an internal array or linked nodes, supporting Push, Pop, and Peek.

Purpose: This exercise helps you practice building a generic data structure from scratch, managing an internal collection manually, and enforcing last-in-first-out behavior for any element type.

Given Input: CustomStack<int> stack = new CustomStack<int>(); stack.Push(10); stack.Push(20); stack.Push(30);

Expected Output:

Peek = 30
Pop = 30
Peek after pop = 20
▼ Hint
  • Use an internal List<T> to hold the elements, treating the end of the list as the top of the stack.
  • Push(T item) should add to the end of the internal list.
  • Pop() should remove and return the last element, throwing an exception if the stack is empty.
  • Peek() should return the last element without removing it.
▼ Solution & Explanation
using System;
using System.Collections.Generic;

public class CustomStack<T>
{
    private List<T> _items = new List<T>();

    public void Push(T item)
    {
        _items.Add(item);
    }

    public T Pop()
    {
        if (_items.Count == 0)
            throw new InvalidOperationException("Stack is empty.");

        int lastIndex = _items.Count - 1;
        T topItem = _items[lastIndex];
        _items.RemoveAt(lastIndex);
        return topItem;
    }

    public T Peek()
    {
        if (_items.Count == 0)
            throw new InvalidOperationException("Stack is empty.");

        return _items[_items.Count - 1];
    }
}

class Program
{
    static void Main()
    {
        CustomStack<int> stack = new CustomStack<int>();
        stack.Push(10);
        stack.Push(20);
        stack.Push(30);

        Console.WriteLine("Peek = " + stack.Peek());
        Console.WriteLine("Pop = " + stack.Pop());
        Console.WriteLine("Peek after pop = " + stack.Peek());
    }
}Code language: C# (cs)

Explanation:

  • private List<T> _items: Backs the stack with a resizable internal list instead of the built-in Stack type.
  • Push(T item): Adds the new item to the end of the internal list, which represents the top of the stack.
  • Pop(): Removes and returns the last element, throwing an exception first if the stack has nothing left.
  • Peek(): Returns the last element without modifying the internal list.

Exercise 10: ID-Based Search

Practice Problem: Create an interface IIdentifiable { int Id { get; } }. Write a generic method FindById<T>(List<T> items, int id) where T : IIdentifiable that returns the matching item.

Purpose: This exercise helps you practice constraining a generic type to an interface, so the method can rely on a shared Id property regardless of the concrete type being searched.

Given Input: A list of Employee objects with Ids 1, 2, and 3, calling FindById(employees, 2).

Expected Output: Found: Id = 2, Name = Bob

▼ Hint
  • Define IIdentifiable with a single read-only int Id { get; } property.
  • Constrain the method with where T : IIdentifiable so Id is accessible on any T.
  • Loop through the list and return the first item whose Id matches.
  • Return default(T) (or null) if no match is found.
▼ Solution & Explanation
using System;
using System.Collections.Generic;

public interface IIdentifiable
{
    int Id { get; }
}

public class Employee : IIdentifiable
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public static class Finder
{
    public static T FindById<T>(List<T> items, int id) where T : IIdentifiable
    {
        foreach (T item in items)
        {
            if (item.Id == id)
                return item;
        }
        return default(T);
    }
}

class Program
{
    static void Main()
    {
        List<Employee> employees = new List<Employee>
        {
            new Employee { Id = 1, Name = "Alice" },
            new Employee { Id = 2, Name = "Bob" },
            new Employee { Id = 3, Name = "Carol" }
        };

        Employee found = Finder.FindById(employees, 2);
        Console.WriteLine("Found: Id = " + found.Id + ", Name = " + found.Name);
    }
}Code language: C# (cs)

Explanation:

  • interface IIdentifiable: Defines a contract guaranteeing that any implementing type exposes a readable Id property.
  • where T : IIdentifiable: Restricts the generic method so it can safely read item.Id on any type T.
  • if (item.Id == id): Compares each item’s Id against the target Id to find a match.
  • return default(T): Provides a safe fallback value, null for reference types, if no item matches the given id.

Exercise 11: Nullable Struct Wrapper

Practice Problem: Create a custom generic struct Optional<T> where T : struct that mimics Nullable<T>, tracking whether a value is present or not.

Purpose: This exercise helps you practice constraining a generic type to value types with where T : struct, and shows how to model presence or absence of a value without relying on the built-in Nullable<T>.

Given Input: Optional<int> some = new Optional<int>(5); Optional<int> none = default(Optional<int>);

Expected Output:

HasValue = True, Value = 5
HasValue = False
▼ Hint
  • Add where T : struct to the struct declaration.
  • Store a private T field and a private bool flag for whether a value was set.
  • Have the constructor set the value and flip the flag to true.
  • Expose HasValue as a read-only property, and throw from the Value getter when no value is present.
▼ Solution & Explanation
using System;

public struct Optional<T> where T : struct
{
    private T _value;
    private bool _hasValue;

    public Optional(T value)
    {
        _value = value;
        _hasValue = true;
    }

    public bool HasValue => _hasValue;

    public T Value
    {
        get
        {
            if (!_hasValue)
                throw new InvalidOperationException("No value present.");
            return _value;
        }
    }
}

class Program
{
    static void Main()
    {
        Optional<int> some = new Optional<int>(5);
        Optional<int> none = default(Optional<int>);

        Console.WriteLine("HasValue = " + some.HasValue + ", Value = " + some.Value);
        Console.WriteLine("HasValue = " + none.HasValue);
    }
}Code language: C# (cs)

Explanation:

  • where T : struct: Restricts T to value types, matching how Nullable<T> itself is constrained.
  • _hasValue = true: Marks the wrapper as populated as soon as a value is supplied through the constructor.
  • default(Optional<int>): Produces a struct with _hasValue defaulted to false, representing an empty optional.
  • Value getter: Throws an exception if accessed while _hasValue is false, preventing use of an unset value.

Exercise 12: Double Constraint Repository

Practice Problem: Modify your Repository<T> so that T must be a class, must have a parameterless constructor, and must implement an IStoredProcedure interface.

Purpose: This exercise helps you practice combining multiple generic constraints, class, new(), and an interface, on a single type parameter, and shows how the compiler enforces all of them together at the call site.

Given Input: Repository<UserProcedure> repo = new Repository<UserProcedure>(); repo.CreateAndAdd();

Expected Output:

Executing stored procedure logic.
Total procedures stored: 1
▼ Hint
  • Combine constraints in one clause: where T : class, IStoredProcedure, new().
  • Define IStoredProcedure with an Execute() method.
  • Add a CreateAndAdd() method that constructs a new T, calls Execute() on it, and stores it.
  • Remember that constraints must be listed in the order class/struct, interfaces, then new() last.
▼ Solution & Explanation
using System;
using System.Collections.Generic;

public interface IStoredProcedure
{
    void Execute();
}

public class UserProcedure : IStoredProcedure
{
    public void Execute()
    {
        Console.WriteLine("Executing stored procedure logic.");
    }
}

public class Repository<T> where T : class, IStoredProcedure, new()
{
    private List<T> _entities = new List<T>();

    public void Add(T entity)
    {
        _entities.Add(entity);
    }

    public T CreateAndAdd()
    {
        T entity = new T();
        entity.Execute();
        _entities.Add(entity);
        return entity;
    }

    public List<T> GetAll() => _entities;
}

class Program
{
    static void Main()
    {
        Repository<UserProcedure> repo = new Repository<UserProcedure>();
        repo.CreateAndAdd();
        Console.WriteLine("Total procedures stored: " + repo.GetAll().Count);
    }
}Code language: C# (cs)

Explanation:

  • where T : class, IStoredProcedure, new(): Requires T to be a reference type, implement the interface, and expose a parameterless constructor, all at once.
  • T entity = new T(): Valid only because of the new() constraint, this creates a fresh instance of the concrete type.
  • entity.Execute(): Valid only because of the IStoredProcedure constraint, letting the repository invoke behavior it otherwise couldn’t know about.
  • repo.GetAll().Count: Confirms the newly created entity was added to the internal list.

Exercise 13: Generic Validator

Practice Problem: Create a generic class Validator<T> that accepts a generic delegate Predicate<T> in its constructor. Add a Validate(T item) method that returns a boolean.

Purpose: This exercise helps you practice combining generics with delegates, letting the validation rule itself be supplied by the caller instead of being hardcoded inside the class.

Given Input: Validator<int> positiveValidator = new Validator<int>(n => n > 0);

Expected Output:

Is 10 valid? True
Is -5 valid? False
▼ Hint
  • Store the constructor’s Predicate<T> argument in a private field.
  • In Validate(T item), invoke the stored predicate with item and return its result.
  • At the call site, pass a lambda expression matching the rule you want to check.
▼ Solution & Explanation
using System;

public class Validator<T>
{
    private Predicate<T> _rule;

    public Validator(Predicate<T> rule)
    {
        _rule = rule;
    }

    public bool Validate(T item)
    {
        return _rule(item);
    }
}

class Program
{
    static void Main()
    {
        Validator<int> positiveValidator = new Validator<int>(n => n > 0);

        Console.WriteLine("Is 10 valid? " + positiveValidator.Validate(10));
        Console.WriteLine("Is -5 valid? " + positiveValidator.Validate(-5));
    }
}Code language: C# (cs)

Explanation:

  • Predicate<T> _rule: Holds a reference to whatever validation logic the caller passed in, decoupled from the class itself.
  • Validator(Predicate<T> rule): Accepts the rule as a constructor argument and saves it for later use.
  • Validate(T item): Runs the stored predicate against the given item and returns the boolean result.
  • n => n > 0: A lambda expression supplied at the call site, defining “positive” as the actual validation rule.

Exercise 14: Generic Cache

Practice Problem: Implement a Cache<TKey, TValue> class using a Dictionary under the hood. Include a basic time-to-live expiration check mechanism.

Purpose: This exercise helps you practice wrapping a generic Dictionary<TKey, TValue> inside your own generic class, and shows how to attach extra metadata, like an expiration time, to each cached entry.

Given Input: Cache<string, string> cache = new Cache<string, string>(); cache.Set("greeting", "Hello", TimeSpan.FromSeconds(5));

Expected Output:

Found: Hello
Not found or expired.
▼ Hint
  • Create a small private class or struct that stores the cached value along with an expiration timestamp.
  • Back the cache with a Dictionary<TKey, CacheItem>.
  • In Set, compute the expiration time as DateTime.UtcNow.Add(ttl).
  • In TryGet, return false if the key is missing or if DateTime.UtcNow has passed the stored expiration.
▼ Solution & Explanation
using System;
using System.Collections.Generic;

public class Cache<TKey, TValue>
{
    private class CacheItem
    {
        public TValue Value;
        public DateTime ExpiresAt;
    }

    private Dictionary<TKey, CacheItem> _store = new Dictionary<TKey, CacheItem>();

    public void Set(TKey key, TValue value, TimeSpan ttl)
    {
        _store[key] = new CacheItem { Value = value, ExpiresAt = DateTime.UtcNow.Add(ttl) };
    }

    public bool TryGet(TKey key, out TValue value)
    {
        if (_store.TryGetValue(key, out CacheItem item) && DateTime.UtcNow < item.ExpiresAt)
        {
            value = item.Value;
            return true;
        }

        value = default(TValue);
        return false;
    }
}

class Program
{
    static void Main()
    {
        Cache<string, string> cache = new Cache<string, string>();
        cache.Set("greeting", "Hello", TimeSpan.FromSeconds(5));

        if (cache.TryGet("greeting", out string result))
            Console.WriteLine("Found: " + result);
        else
            Console.WriteLine("Not found or expired.");

        if (cache.TryGet("unknown", out string missing))
            Console.WriteLine("Found: " + missing);
        else
            Console.WriteLine("Not found or expired.");
    }
}Code language: C# (cs)

Explanation:

  • private class CacheItem: A small internal wrapper that pairs the cached value with the time it expires.
  • DateTime.UtcNow.Add(ttl): Calculates the exact expiration timestamp when an entry is first stored.
  • DateTime.UtcNow < item.ExpiresAt: Checks whether the entry is still valid at the moment it is requested.
  • TryGet: Follows the standard try-pattern, returning false and a default value instead of throwing when the key is missing or expired.

Exercise 15: Type Converter

Practice Problem: Write a generic method ConvertType<TInput, TOutput>(TInput input) that attempts to safely cast or convert TInput into TOutput, handling errors gracefully.

Purpose: This exercise helps you practice using two independent type parameters on a method, and shows how to lean on Convert.ChangeType alongside a try/catch block to convert between types safely at runtime.

Given Input: ConvertType<string, int>("123") and ConvertType<string, int>("abc")

Expected Output:

Converted = 123
Converted = 0
▼ Hint
  • Declare the method with two type parameters, TInput and TOutput.
  • Wrap the conversion attempt in a try block using Convert.ChangeType(input, typeof(TOutput)).
  • Cast the result of Convert.ChangeType back to TOutput.
  • In the catch block, return default(TOutput) instead of letting the exception propagate.
▼ Solution & Explanation
using System;

public static class Converter
{
    public static TOutput ConvertType<TInput, TOutput>(TInput input)
    {
        try
        {
            return (TOutput)Convert.ChangeType(input, typeof(TOutput));
        }
        catch (Exception)
        {
            return default(TOutput);
        }
    }
}

class Program
{
    static void Main()
    {
        int good = Converter.ConvertType<string, int>("123");
        int bad = Converter.ConvertType<string, int>("abc");

        Console.WriteLine("Converted = " + good);
        Console.WriteLine("Converted = " + bad);
    }
}Code language: C# (cs)

Explanation:

  • ConvertType<TInput, TOutput>(TInput input): Declares a method with two separate type parameters, one for the source type and one for the target type.
  • Convert.ChangeType(input, typeof(TOutput)): Performs the actual runtime conversion based on the target type’s reflection information.
  • catch (Exception): Catches invalid conversions, such as a non-numeric string, so the method never throws.
  • return default(TOutput): Provides a safe fallback value, here 0 for int, when the conversion fails.

Exercise 16: Covariant Logger (Out Keyword)

Practice Problem: Create a covariant interface ILogger<out T> and implement it. Demonstrate that an ILogger<Dog> can be assigned to an ILogger<Animal>.

Purpose: This exercise helps you practice the out variance modifier, which allows a generic interface to be treated as more general when its type parameter only ever appears in output positions, such as return types.

Given Input: ILogger<Dog> dogLogger = new DogLogger(); ILogger<Animal> animalLogger = dogLogger;

Expected Output: Logged animal says: Woof!

▼ Hint
  • Mark the type parameter as out, meaning T may only appear in return positions, never as a method argument.
  • Give the interface a method that returns T, such as T GetLastLogged().
  • Implement the interface for a specific, more derived type like Dog.
  • Assign the ILogger<Dog> instance directly to an ILogger<Animal> variable to show covariance in action.
▼ Solution & Explanation
using System;

public class Animal
{
    public virtual string Speak() => "...";
}

public class Dog : Animal
{
    public override string Speak() => "Woof!";
}

public interface ILogger<out T>
{
    T GetLastLogged();
}

public class DogLogger : ILogger<Dog>
{
    private Dog _lastDog = new Dog();

    public Dog GetLastLogged()
    {
        return _lastDog;
    }
}

class Program
{
    static void Main()
    {
        ILogger<Dog> dogLogger = new DogLogger();
        ILogger<Animal> animalLogger = dogLogger;

        Animal animal = animalLogger.GetLastLogged();
        Console.WriteLine("Logged animal says: " + animal.Speak());
    }
}Code language: C# (cs)

Explanation:

  • ILogger<out T>: The out keyword tells the compiler T is only ever produced, never consumed, which is what makes covariance safe.
  • ILogger<Animal> animalLogger = dogLogger: Legal because a logger that only ever hands out Dog objects can safely be treated as one that hands out Animal objects.
  • animalLogger.GetLastLogged(): Returns an Animal reference even though the concrete object underneath is still a Dog.
  • animal.Speak(): Calls the overridden method, so the actual Dog behavior runs through virtual dispatch.

Exercise 17: Contravariant Consumer (In Keyword)

Practice Problem: Create a contravariant interface IConsumer<in T> with a method void Consume(T item). Demonstrate passing an IConsumer<Animal> to something expecting an IConsumer<Dog>.

Purpose: This exercise helps you practice the in variance modifier, which allows a generic interface to be treated as more specific when its type parameter only ever appears in input positions, such as method arguments.

Given Input: IConsumer<Animal> animalConsumer = new AnimalConsumer(); IConsumer<Dog> dogConsumer = animalConsumer;

Expected Output: Consumed animal: Rex

▼ Hint
  • Mark the type parameter as in, meaning T may only appear in argument positions, never as a return type.
  • Give the interface a method that accepts T, such as void Consume(T item).
  • Implement the interface for a more general type like Animal.
  • Assign the IConsumer<Animal> instance to an IConsumer<Dog> variable to show contravariance in action.
▼ Solution & Explanation
using System;

public class Animal
{
    public string Name;
}

public class Dog : Animal
{
}

public interface IConsumer<in T>
{
    void Consume(T item);
}

public class AnimalConsumer : IConsumer<Animal>
{
    public void Consume(Animal item)
    {
        Console.WriteLine("Consumed animal: " + item.Name);
    }
}

class Program
{
    static void Main()
    {
        IConsumer<Animal> animalConsumer = new AnimalConsumer();
        IConsumer<Dog> dogConsumer = animalConsumer;

        Dog dog = new Dog { Name = "Rex" };
        dogConsumer.Consume(dog);
    }
}Code language: C# (cs)

Explanation:

  • IConsumer<in T>: The in keyword tells the compiler T is only ever accepted as input, which is what makes contravariance safe.
  • IConsumer<Dog> dogConsumer = animalConsumer: Legal because a consumer that knows how to handle any Animal can also safely handle a more specific Dog.
  • dogConsumer.Consume(dog): Passes a Dog into a method that was really written to accept Animal, which works because Dog is an Animal.
  • item.Name: Reads a member defined on the base Animal class, so it works regardless of which derived type was actually passed in.

Exercise 18: Generic Event Broker

Practice Problem: Build a simple generic event publisher and subscriber pattern where components can subscribe to a specific event type, MessageEvent<T>.

Purpose: This exercise helps you practice combining generics with delegates and events, showing how a single generic type can broadcast strongly typed messages to any number of subscribers.

Given Input: MessageEvent<string> chatEvent = new MessageEvent<string>(); chatEvent.Publish("Server is going down for maintenance.");

Expected Output:

Subscriber A received: Server is going down for maintenance.
Subscriber B received: Server is going down for maintenance.
▼ Hint
  • Declare a public event Action<T> field on MessageEvent<T>.
  • Add a Publish(T message) method that invokes the event, checking for null subscribers first.
  • Subscribe multiple lambda expressions to the event using += before publishing.
▼ Solution & Explanation
using System;

public class MessageEvent<T>
{
    public event Action<T> OnMessage;

    public void Publish(T message)
    {
        OnMessage?.Invoke(message);
    }
}

class Program
{
    static void Main()
    {
        MessageEvent<string> chatEvent = new MessageEvent<string>();

        chatEvent.OnMessage += message => Console.WriteLine("Subscriber A received: " + message);
        chatEvent.OnMessage += message => Console.WriteLine("Subscriber B received: " + message);

        chatEvent.Publish("Server is going down for maintenance.");
    }
}Code language: C# (cs)

Explanation:

  • event Action<T> OnMessage: Declares a strongly typed event, so subscribers only ever receive messages of type T.
  • OnMessage?.Invoke(message): Safely raises the event, doing nothing if no subscribers have signed up yet.
  • chatEvent.OnMessage += ...: Attaches a lambda expression as a subscriber, and multiple subscribers can be attached the same way.
  • chatEvent.Publish(...): Broadcasts a single message to every currently attached subscriber in the order they were added.

Exercise 19: Custom LINQ Filter

Practice Problem: Write an extension method Filter<T>(this IEnumerable<T> source, Func<T, bool> predicate) that mimics the behavior of LINQ’s .Where() without actually using LINQ.

Purpose: This exercise helps you practice writing generic extension methods, using yield return for lazy evaluation, and understanding how a method like .Where() works under the hood.

Given Input: List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 }; numbers.Filter(n => n % 2 == 0);

Expected Output:

2
4
6
▼ Hint
  • Make the method static, inside a static class, with this IEnumerable<T> source as the first parameter.
  • Loop through source with a foreach loop.
  • Use yield return item whenever the predicate returns true for the current item.
▼ Solution & Explanation
using System;
using System.Collections.Generic;

public static class EnumerableExtensions
{
    public static IEnumerable<T> Filter<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
        foreach (T item in source)
        {
            if (predicate(item))
                yield return item;
        }
    }
}

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
        IEnumerable<int> evens = numbers.Filter(n => n % 2 == 0);

        foreach (int n in evens)
        {
            Console.WriteLine(n);
        }
    }
}Code language: C# (cs)

Explanation:

  • this IEnumerable<T> source: The this keyword on the first parameter turns the method into an extension method callable directly on any IEnumerable<T>.
  • Func<T, bool> predicate: Accepts the filtering rule as a delegate, exactly like the real .Where() does.
  • yield return item: Lazily produces matching items one at a time instead of building a full list up front.
  • numbers.Filter(n => n % 2 == 0): Calls the extension method as if it were a built-in member of the list.

Exercise 20: Generic Spec Evaluation

Practice Problem: Implement the Specification Pattern using generics. Create an ISpecification<T> interface with an IsSatisfiedBy(T entity) method, then create an AndSpecification<T> that combines two specs together.

Purpose: This exercise helps you practice composing generic behavior out of smaller generic pieces, where one specification wraps two others and reuses their logic instead of duplicating it.

Given Input: ISpecification<int> adultSpec = new AndSpecification<int>(new MinAgeSpecification(18), new MaxAgeSpecification(65));

Expected Output:

Age 30 satisfies: True
Age 15 satisfies: False
▼ Hint
  • Define ISpecification<T> with a single bool IsSatisfiedBy(T entity) method.
  • Have AndSpecification<T> implement the same interface and store two other ISpecification<T> instances internally.
  • Its IsSatisfiedBy should return true only when both stored specifications return true.
  • Write a couple of small concrete specifications, such as a minimum age and maximum age check, to combine together.
▼ Solution & Explanation
using System;

public interface ISpecification<T>
{
    bool IsSatisfiedBy(T entity);
}

public class AndSpecification<T> : ISpecification<T>
{
    private readonly ISpecification<T> _first;
    private readonly ISpecification<T> _second;

    public AndSpecification(ISpecification<T> first, ISpecification<T> second)
    {
        _first = first;
        _second = second;
    }

    public bool IsSatisfiedBy(T entity)
    {
        return _first.IsSatisfiedBy(entity) && _second.IsSatisfiedBy(entity);
    }
}

public class MinAgeSpecification : ISpecification<int>
{
    private readonly int _minAge;

    public MinAgeSpecification(int minAge)
    {
        _minAge = minAge;
    }

    public bool IsSatisfiedBy(int age)
    {
        return age >= _minAge;
    }
}

public class MaxAgeSpecification : ISpecification<int>
{
    private readonly int _maxAge;

    public MaxAgeSpecification(int maxAge)
    {
        _maxAge = maxAge;
    }

    public bool IsSatisfiedBy(int age)
    {
        return age <= _maxAge;
    }
}

class Program
{
    static void Main()
    {
        ISpecification<int> adultSpec = new AndSpecification<int>(new MinAgeSpecification(18), new MaxAgeSpecification(65));

        Console.WriteLine("Age 30 satisfies: " + adultSpec.IsSatisfiedBy(30));
        Console.WriteLine("Age 15 satisfies: " + adultSpec.IsSatisfiedBy(15));
    }
}Code language: C# (cs)

Explanation:

  • ISpecification<T>: Defines a single reusable contract for “does this entity satisfy some rule”.
  • AndSpecification<T>: Wraps two other specifications and combines their results with a logical AND, without knowing what those rules actually check.
  • _first.IsSatisfiedBy(entity) && _second.IsSatisfiedBy(entity): Only returns true when both wrapped specifications independently agree.
  • MinAgeSpecification and MaxAgeSpecification: Small, focused rules that can be composed together instead of writing one large combined check.

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