Extension methods let you add new functionality to existing types, including ones you don’t own, without modifying their source code or creating a subclass. This collection of 20 C# extension method exercises walks you through writing your own extensions for built-in types like string, int, and List<T>, a technique widely used throughout LINQ and the .NET framework itself.
Each exercise includes a Practice Problem, Purpose, Hint, and a full Solution with Explanation, so you understand exactly how the this keyword turns a static method into an extension.
Also, See: C# Exercises with 22 topic-wise sets and 620+ practice questions.
What You’ll Practice
- Fundamentals: Static classes, static methods, and the
thisparameter modifier. - String Extensions: Adding custom string manipulation methods.
- Collection Extensions: Extending
List<T>and other collection types. - Real-World Use: Chaining extension methods for readable, fluent code.
+ Table Of Contents (20 Exercises)
Table of contents
- Exercise 1: Convert a String to PascalCase
- Exercise 2: Check if a Number Is Even or Odd
- Exercise 3: Count Words in a Sentence
- Exercise 4: Format a Decimal as a Percentage
- Exercise 5: Check if a Date Falls on a Weekend
- Exercise 6: Truncate a Long String
- Exercise 7: Check if a Collection Is Empty
- Exercise 8: Shuffle a Collection Randomly
- Exercise 9: Remove Duplicate Items from a List
- Exercise 10: Take Random Elements from a Collection
- Exercise 11: Calculate Age from a Birthdate
- Exercise 12: Validate an Email Address
- Exercise 13: Join a Collection of Strings
- Exercise 14: Serialize and Deserialize an Object to JSON
- Exercise 15: Check if a Value Is in a List of Values
- Exercise 16: Get or Add a Value in a Dictionary
- Exercise 17: Find the Root Cause of an Exception
- Exercise 18: Paginate a Queryable Collection
- Exercise 19: Read a Description Attribute from an Enum
- Exercise 20: Chain Function Calls with Pipe
Exercise 1: Convert a String to PascalCase
Practice Problem: Write an extension method for string that converts a space-separated or snake_case string into PascalCase.
Purpose: This exercise helps you practice writing a basic extension method using the this keyword on the first parameter, letting you call custom logic as if it were a built-in method on string.
Given Input: text = "hello_world"
Expected Output:
Original = hello_world Pascal Case = HelloWorld
▼ Hint
- Declare the method as
public static string ToPascalCase(this string input)inside astatic class. - Replace underscores with spaces first, then split the string on spaces to get individual words.
- For each word, capitalize the first letter and lowercase the rest, then join the words together with no separator.
▼ Solution & Explanation
Explanation:
this string input: Thethismodifier on the first parameter is what turns a regular static method into an extension method callable on anystring.input.Replace('_', ' ').Split(' ', ...): Normalizes snake_case input into space separated words so both formats can be handled by the same logic.char.ToUpper(word[0]) + word.Substring(1).ToLower(): Capitalizes only the first character of each word and lowercases the remainder before concatenating.text.ToPascalCase(): Once defined, the extension method is called just like any regular instance method on the string.
Exercise 2: Check if a Number Is Even or Odd
Practice Problem: Create two simple extension methods for int that return a boolean indicating if the number is even or odd.
Purpose: This exercise helps you practice writing extension methods on a value type like int, and shows how one extension method can call another to avoid duplicating logic.
Given Input: number = 4
Expected Output:
Number = 4 Is Even = True Is Odd = False
▼ Hint
- Declare both methods as
public static bool IsEven(this int number)andpublic static bool IsOdd(this int number). - Implement
IsEven()using the modulus operator, then implementIsOdd()by simply negating a call toIsEven().
▼ Solution & Explanation
Explanation:
number % 2 == 0: Checks whether dividing the number by 2 leaves no remainder, which is true only for even numbers.!number.IsEven(): Reuses theIsEven()extension method and simply inverts the result instead of repeating the modulus logic.number.IsEven()andnumber.IsOdd(): Both extension methods are called directly on theintvariable as if they were built-in members.
Exercise 3: Count Words in a Sentence
Practice Problem: Extend the string class to count the number of words in a sentence, ignoring extra spaces.
Purpose: This exercise helps you practice using string.Split() with an option that removes empty entries, so consecutive or leading and trailing spaces do not get counted as extra words.
Given Input: sentence = " Coding is fun "
Expected Output:
Sentence = " Coding is fun " Word Count = 3
▼ Hint
Call sentence.Split(' ', StringSplitOptions.RemoveEmptyEntries) and return the length of the resulting array.
▼ Solution & Explanation
Explanation:
sentence.Split(' ', StringSplitOptions.RemoveEmptyEntries): Splits the sentence on spaces while discarding empty strings caused by extra or leading and trailing spaces.words.Length: Returns the count of actual words remaining after the empty entries have been removed.
Exercise 4: Format a Decimal as a Percentage
Practice Problem: Create a method for double that formats a decimal fraction as a percentage string with a specified number of decimal places.
Purpose: This exercise helps you practice writing an extension method that accepts an additional parameter beyond the instance it extends, similar to how many built-in formatting methods work.
Given Input: fraction = 0.856, decimalPlaces = 1
Expected Output:
Fraction = 0.856 Percentage = 85.6%
▼ Hint
- Declare the method as
public static string ToPercentage(this double value, int decimalPlaces). - Multiply the value by 100 first, then format it using a fixed point format string built from
decimalPlaces, such as"F" + decimalPlaces. - Append a literal
%character to the formatted number before returning it.
▼ Solution & Explanation
Explanation:
this double value, int decimalPlaces: The extension method extendsdoublewhile still accepting a normal second parameter for how many decimal places to show.value * 100: Converts the decimal fraction into a whole percentage value before formatting.percentage.ToString("F" + decimalPlaces): Builds a dynamic fixed point format string so the number of decimal places can vary per call.
Exercise 5: Check if a Date Falls on a Weekend
Practice Problem: Extend DateTime to check if a given date falls on a Saturday or Sunday.
Purpose: This exercise helps you practice writing an extension method on a built-in struct type like DateTime, and reading its DayOfWeek property to make a decision.
Given Input: date = January 1, 2000
Expected Output:
Date = 2000-01-01 (Saturday) Is Weekend = True
▼ Hint
Compare date.DayOfWeek against DayOfWeek.Saturday and DayOfWeek.Sunday using the logical OR operator ||.
▼ Solution & Explanation
Explanation:
date.DayOfWeek: A built-inDateTimeproperty that returns an enum value representing the day of the week.date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday: Returns true only when the date lands on one of the two weekend days.{date:yyyy-MM-dd}: Uses a format specifier inside the string interpolation to display the date in a readable format.
Exercise 6: Truncate a Long String
Practice Problem: Write a method for string that cuts off text at a maximum length and appends "..." if it exceeds that length.
Purpose: This exercise helps you practice combining a length check with string.Substring() inside an extension method, a pattern often used for display text in UIs and previews.
Given Input: text = "Long text here", maxLength = 7
Expected Output:
Original = Long text here Truncated = Long te...
▼ Hint
- First check if
text.Lengthis already less than or equal tomaxLength, and return the text unchanged if so. - Otherwise use
text.Substring(0, maxLength)and append"..."to the result.
▼ Solution & Explanation
Explanation:
if (text.Length <= maxLength): Skips truncation entirely and returns the original text when it already fits within the limit.text.Substring(0, maxLength): Extracts only the firstmaxLengthcharacters from the string.+ "...": Appends the ellipsis to signal that the text was cut off.
Exercise 7: Check if a Collection Is Empty
Practice Problem: Write a method to cleanly check if an enumerable is empty, serving as a more readable alternative to !collection.Any().
Purpose: This exercise helps you practice writing a generic extension method on IEnumerable<T>, so the same method works for lists, arrays, and any other sequence type.
Given Input: An empty List<int>.
Expected Output: Is Empty = True
▼ Hint
- Declare the method as
public static bool IsEmpty<T>(this IEnumerable<T> source). - Return
!source.Any()so the extension method is just a more expressive wrapper around the existing LINQ check.
▼ Solution & Explanation
Explanation:
IsEmpty<T>(this IEnumerable<T> source): A generic extension method that works on any sequence type, not justList<T>.!source.Any(): Reuses the existing LINQAny()method and simply inverts it, so callers get a more readable name for the same check.numbers.IsEmpty(): Reads more naturally at the call site than the equivalent!numbers.Any().
Exercise 8: Shuffle a Collection Randomly
Practice Problem: Create an extension method that randomly shuffles the elements of any IEnumerable<T> and returns a new sequence.
Purpose: This exercise helps you practice combining a generic extension method with Random and LINQ’s OrderBy(), a simple way to produce a randomized ordering without writing a manual shuffle algorithm.
Given Input: numbers = { 1, 2, 3, 4, 5 }
Expected Output:
Original = 1, 2, 3, 4, 5 Shuffled = 4, 1, 5, 2, 3
Note: since Shuffle() relies on randomness, the exact order of the shuffled output will differ each time the program runs.
▼ Hint
- Create a single
Randominstance and useOrderBy(item => random.Next())to reorder the sequence unpredictably. - Return the result directly from
OrderBy()since it already produces a newIEnumerable<T>without modifying the original sequence.
▼ Solution & Explanation
Explanation:
source.OrderBy(item => random.Next()): Assigns each element a random sort key, so the resulting order is effectively randomized.new Random(): Creates a single random number generator that is reused for every element’s sort key within the same call.numbers.Shuffle().ToList(): Calls the extension method and materializes the shuffled sequence into a new list, leaving the originalnumberslist untouched.
Exercise 9: Remove Duplicate Items from a List
Practice Problem: Extend IList<T> to remove duplicate elements from the collection in-place.
Purpose: This exercise helps you practice writing an extension method that mutates the original collection directly, instead of returning a new sequence like most LINQ style methods do.
Given Input: numbers = { 1, 2, 2, 3, 4, 4, 5 }
Expected Output: After Removing Duplicates = 1, 2, 3, 4, 5
▼ Hint
- Declare the method as
public static void RemoveDuplicates<T>(this IList<T> list)since it modifies the list and does not need to return anything. - Use a
HashSet<T>to track values already seen, and loop through the list backwards withRemoveAt()so removing an item does not skip the next one.
▼ Solution & Explanation
Explanation:
for (int i = list.Count - 1; i >= 0; i--): Iterates from the end of the list toward the start, so removing an item never shifts the indices still left to check.seen.Add(list[i]):HashSet<T>.Add()returnsfalsewhen the value already exists in the set, which signals a duplicate.list.RemoveAt(i): Removes the duplicate element directly from the original list, since the method modifieslistin-place rather than returning a copy.
Exercise 10: Take Random Elements from a Collection
Practice Problem: Write a method that returns a specified number of random elements from a collection.
Purpose: This exercise helps you practice combining a randomized ordering with LINQ’s Take() method to sample a subset of a collection, useful for things like picking random items to display or test.
Given Input: items = { "Apple", "Banana", "Cherry", "Date", "Elderberry" }, count = 3
Expected Output: Random Items = Cherry, Apple, Elderberry
Note: since TakeRandom() relies on randomness, the exact items and their order will differ each time the program runs.
▼ Hint
- Reuse the same shuffling idea from
Shuffle(), ordering the sequence by a random key withOrderBy(item => random.Next()). - Chain
.Take(count)after the random ordering to keep only the requested number of elements.
▼ Solution & Explanation
Explanation:
source.OrderBy(item => random.Next()).Take(count): Randomizes the order of the entire sequence first, then keeps only the firstcountelements from that shuffled order.this IEnumerable<T> source, int count: The extension method extends any sequence type while accepting a normal parameter for how many random elements to return.items.TakeRandom(3): Calls the extension method to pull three random items out of the five available strings.
Exercise 11: Calculate Age from a Birthdate
Practice Problem: Extend DateTime (representing a birthdate) to accurately calculate a person’s current age in years, accounting for leap years and months.
Purpose: This exercise helps you practice date arithmetic that goes beyond a simple year subtraction, correctly handling cases where the birthday has not yet occurred in the current year.
Given Input: birthDate = July 20, 2000
Expected Output:
Birth Date = 2000-07-20 Age = 25
Note: since Age() is based on DateTime.Today, the output above reflects the current date and will change as time passes.
▼ Hint
- Start with a simple
today.Year - birthDate.Yearsubtraction to get a rough age. - Then check whether the birthday has already occurred this year by comparing
birthDate.Datetotoday.AddYears(-age), and subtract one from the age if it has not.
▼ Solution & Explanation
Explanation:
today.Year - birthDate.Year: Provides a starting estimate of the age based purely on the difference in calendar years.birthDate.Date > today.AddYears(-age): Checks whether this year’s birthday date has already passed by shifting today back by the estimated age and comparing the two dates directly.age--: Corrects the estimate downward by one year if the birthday later in the year has not happened yet.
Exercise 12: Validate an Email Address
Practice Problem: Create a method that uses Regex to validate whether a string is a properly formatted email address.
Purpose: This exercise helps you practice combining an extension method with System.Text.RegularExpressions to perform pattern based validation on a string.
Given Input: email = "test@test.com"
Expected Output:
Email = test@test.com Is Valid Email = True
▼ Hint
- Use a regular expression that requires characters before and after an
@symbol, followed by a dot and more characters. - Call
Regex.IsMatch(email, pattern)to test the string against the pattern and return a boolean.
▼ Solution & Explanation
Explanation:
@"^[^@\s]+@[^@\s]+\.[^@\s]+$": A regular expression requiring one or more non-space, non-@characters, an@symbol, more such characters, a literal dot, and a final group of characters.Regex.IsMatch(email, pattern): Tests the entire email string against the pattern and returnstrueonly if it matches from start to end.
Exercise 13: Join a Collection of Strings
Practice Problem: Extend IEnumerable<string> to join elements into a single string with a custom separator (shortcut for string.Join).
Purpose: This exercise helps you practice writing a thin extension method wrapper around an existing static method, so the separator focused call reads more naturally at the call site.
Given Input: letters = { "A", "B" }, separator = ", "
Expected Output: Joined = A, B
▼ Hint
Inside the method, simply call and return string.Join(separator, source), passing the extended sequence as the values to join.
▼ Solution & Explanation
Explanation:
this IEnumerable<string> source, string separator: Extends any string sequence while accepting the separator as a normal second parameter.string.Join(separator, source): Delegates the actual joining work to the existing built-in method, keeping the extension method a thin, readable wrapper.letters.JoinStrings(", "): Reads more like a fluent instance call compared to the equivalentstring.Join(", ", letters).
Exercise 14: Serialize and Deserialize an Object to JSON
Practice Problem: Create generic extension methods to easily serialize any object to a JSON string and deserialize a JSON string back into an object using System.Text.Json.
Purpose: This exercise helps you practice writing generic extension methods on both a general type T and on string, wrapping System.Text.Json calls behind more convenient names.
Given Input: person = new Person { Name = "Alice", Age = 30 }
Expected Output:
JSON = {"Name":"Alice","Age":30}
Deserialized Name = Alice
Deserialized Age = 30
▼ Hint
- Declare
ToJson<T>(this T obj)and have it returnJsonSerializer.Serialize(obj). - Declare
FromJson<T>(this string json)and have it returnJsonSerializer.Deserialize<T>(json). - Since
FromJson<T>cannot inferTfrom a plain string, the caller must specify it explicitly, for examplejson.FromJson<Person>().
▼ Solution & Explanation
Explanation:
JsonSerializer.Serialize(obj): Converts the object’s public properties into a compact JSON formatted string.JsonSerializer.Deserialize<T>(json): Parses the JSON string back into an instance of the specified typeT.json.FromJson<Person>(): Since the string itself carries no type information, the target type must be supplied explicitly as a generic argument.
Exercise 15: Check if a Value Is in a List of Values
Practice Problem: Write a generic extension method In<T> that checks if an item exists within a provided array or list of arguments.
Purpose: This exercise helps you practice writing a generic extension method that uses the params keyword, allowing callers to pass any number of comparison values directly as arguments.
Given Input: myStatus = Status.Pending, checked against Status.Active, Status.Pending
Expected Output:
Status = Pending Is In List = True
▼ Hint
- Declare the method as
public static bool In<T>(this T item, params T[] values)so it accepts a variable number of comparison values. - Use
Array.IndexOf(values, item)and check whether the result is0or greater to confirm a match.
▼ Solution & Explanation
Explanation:
params T[] values: Lets the caller pass any number of comparison values as plain arguments instead of building an array manually.Array.IndexOf(values, item): Searches the values array for the item and returns its position, or-1if it is not found.myStatus.In(Status.Active, Status.Pending): Reads naturally as checking whether the status falls within an inline set of allowed values.
Exercise 16: Get or Add a Value in a Dictionary
Practice Problem: Extend IDictionary so that if a key doesn’t exist, it evaluates a factory function, adds the result to the dictionary, and returns it.
Purpose: This exercise helps you practice combining TryGetValue() with a Func<V> factory parameter, a pattern often used for simple in-memory caching.
Given Input: An empty Dictionary<string, int>, key "count", factory returning 42
Expected Output:
Result = 42 Dictionary Now Contains Key = True
▼ Hint
- Declare the method as
public static V GetOrAdd<K, V>(this IDictionary<K, V> dictionary, K key, Func<V> factory). - Use
TryGetValue()to check for the key first, only calling the factory and inserting into the dictionary when the key is missing.
▼ Solution & Explanation
Explanation:
dictionary.TryGetValue(key, out V value): Checks for the key without throwing an exception, and captures the existing value if one is found.value = factory(): Only runs the factory function when the key was missing, avoiding unnecessary work when the value already exists.cache.GetOrAdd("count", () => 42): Passes a lambda as the factory, which is only invoked the first time the key is requested.
Exercise 17: Find the Root Cause of an Exception
Practice Problem: Write a method that recursively traverses an Exception‘s InnerException property to find and return the root cause exception.
Purpose: This exercise helps you practice writing an extension method that walks a chain of linked objects, a pattern that shows up whenever exceptions are wrapped by multiple layers of application code.
Given Input: A chain of three exceptions, where the outer exception wraps a middle exception that wraps a root cause exception.
Expected Output: Innermost Message = Root cause failure
▼ Hint
- Use a
whileloop that keeps moving tocurrent.InnerExceptionas long as it is notnull. - Return
currentonce the loop ends, since at that point it holds the exception with no further inner exception.
▼ Solution & Explanation
Explanation:
while (current.InnerException != null): Keeps descending through the chain of wrapped exceptions until reaching one with no inner exception.current = current.InnerException: Moves the traversal one level deeper on each iteration of the loop.topException.GetInnermostException(): Called on the outermost exception, but returns the original root cause buried at the bottom of the chain.
Exercise 18: Paginate a Queryable Collection
Practice Problem: Create a pagination extension method for IQueryable<T> that takes a pageNumber and pageSize and applies the correct .Skip() and .Take() logic.
Purpose: This exercise helps you practice writing an extension method on IQueryable<T>, so the same pagination logic can later be pushed down to a database query instead of only working on in-memory collections.
Given Input: Numbers 1 through 20, pageNumber = 2, pageSize = 5
Expected Output: Page 2 (Page Size 5) = 6, 7, 8, 9, 10
▼ Hint
- Calculate how many items to skip using
(pageNumber - 1) * pageSize, then chain.Take(pageSize)after the skip. - Both
.Skip()and.Take()work directly onIQueryable<T>, so no conversion to a list is needed inside the extension method itself.
▼ Solution & Explanation
Explanation:
(pageNumber - 1) * pageSize: Converts a 1 based page number into the number of items that need to be skipped to reach that page.source.Skip(...).Take(pageSize): Skips past all earlier pages, then takes only enough items to fill the current page.numbers.Page(2, 5): Requests the second page of results using a page size of five items each.
Exercise 19: Read a Description Attribute from an Enum
Practice Problem: Write an extension method for enums that reads and returns the string value from a [Description] attribute decorated on the enum member.
Purpose: This exercise helps you practice using reflection inside an extension method to read a custom attribute applied to a specific enum member, a common technique for showing friendly labels in a UI.
Given Input: status = OrderStatus.Shipped
Expected Output:
Status = Shipped Description = Order Shipped
▼ Hint
- Decorate each enum member with
[Description("...")]from theSystem.ComponentModelnamespace. - Use
value.GetType().GetField(value.ToString())to get the reflection info for the specific enum member. - Call
GetCustomAttribute<DescriptionAttribute>()on that field, and fall back tovalue.ToString()if no attribute is present.
▼ Solution & Explanation
Explanation:
value.GetType().GetField(value.ToString()): Uses reflection to locate the specific field on the enum type that matches the current member’s name.field.GetCustomAttribute<DescriptionAttribute>(): Reads theDescriptionattribute applied to that field, returningnullif it was never added.attribute != null ? attribute.Description : value.ToString(): Falls back to the raw enum name whenever a member has no description attribute defined.
Exercise 20: Chain Function Calls with Pipe
Practice Problem: Create a functional programming-style Pipe extension method that allows you to pass the object as an argument into a sequence of functions smoothly.
Purpose: This exercise helps you practice writing a generic extension method with two type parameters, enabling a fluent left to right chain of transformations instead of nested function calls.
Given Input: 5.Pipe(x => x * 2).Pipe(x => x.ToString())
Expected Output: Result = 10
▼ Hint
- Declare the method as
public static TResult Pipe<T, TResult>(this T input, Func<T, TResult> function). - Inside the body, simply call and return
function(input), letting the caller supply whatever transformation is needed. - Since
Pipe()itself returns the transformed result, calls can be chained one after another, each feeding into the next.
▼ Solution & Explanation
Explanation:
Pipe<T, TResult>(this T input, Func<T, TResult> function): Extends any typeTand applies a function that transforms it into a possibly different typeTResult.5.Pipe(x => x * 2): Passes the integer 5 into the first lambda, producing the integer 10..Pipe(x => x.ToString()): Chains a second call directly onto the result of the first, converting the integer 10 into the string “10”.

Leave a Reply