.NET Academy

Extension methods and tuples

Add methods to existing types without changing them, and group values without declaring a type.

10 min read

You will learn

  • Write and call extension methods
  • Understand how LINQ relies on extension methods
  • Create, name and deconstruct tuples
  • Return several values from a method with a tuple

Extension methods: new methods for existing types

You can't edit string — it belongs to .NET. But you can add methods to it that look as if they were built in:

static class StringExtensions
{
    public static string Shout(this string text) => text.ToUpper() + "!";
 
    public static int WordCount(this string text) =>
        text.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}
 
Console.WriteLine("hello".Shout());                 // HELLO!
Console.WriteLine("the quick brown fox".WordCount()); // 4

The recipe:

  1. A static class (any name; XxxExtensions by convention).
  2. A static method whose first parameter has this before its type.
  3. Call it on any value of that type, like an instance method.

What's really happening

An extension method is just a static method with nicer syntax. The compiler rewrites

"hello".Shout()

into

StringExtensions.Shout("hello")

So extension methods can't access private members, and they only "exist" where their namespace is imported with using.

LINQ is built on extension methods

Every LINQ method you'll use in Track 4 — Where, Select, OrderBy, Sum, Max — is an extension method on IEnumerable<T>, defined in the System.Linq namespace:

int[] numbers = { 5, 3, 8 };
Console.WriteLine(numbers.Max());       // arrays don't have a Max method — LINQ adds it

That's why all collection types get them at once: they all implement IEnumerable<T>, and the extensions target the interface.

Use them judiciously

Extension methods are great for small, general helpers and for fluent APIs (builder.AddLogging().AddDatabase()). Don't use them to bolt business logic onto types you own — just add a normal method instead.

Tuples: lightweight groups of values

Sometimes you need to pass a couple of values together without defining a class or record. A tuple does that:

var point = (3, 4);
Console.WriteLine(point.Item1);           // 3
 
var person = (Name: "Ada", Age: 36);      // named elements
Console.WriteLine(person.Name);           // Ada
 
(string City, int Population) capital = ("Paris", 2_100_000);

Tuples are value types and compare by value: (1, 2) == (1, 2) is true.

Returning several values

Tuples are the cleanest way to return more than one result:

(int Min, int Max) Range(int[] values)
{
    int min = values[0], max = values[0];
    foreach (var v in values)
    {
        if (v < min) min = v;
        if (v > max) max = v;
    }
    return (min, max);
}
 
var r = Range(new[] { 4, 1, 9 });
Console.WriteLine($"{r.Min}–{r.Max}");     // 1–9

Deconstruction

Unpack a tuple into separate variables:

var (min, max) = Range(new[] { 4, 1, 9 });
var (_, highest) = Range(new[] { 4, 1, 9 });   // _ discards a value you don't need
 
// The classic swap, without a temp variable:
int a = 1, b = 2;
(a, b) = (b, a);

Records and KeyValuePair support deconstruction too: foreach (var (key, value) in dictionary).

Tuple or record?

Tuples are perfect for local, short-lived groupings — a method's private helper results, a quick pair in a LINQ query. When a group of values is passed around widely or deserves a name in your domain (Money, Address), declare a record instead.

Try it

Program.cs
Loading editor…

C# runtime not loaded

Key takeaways

  • Extension methods are static methods in static classes, with this on the first parameter.
  • They let you call helpers as if they were instance methods — LINQ is built this way.
  • Tuples group values without declaring a type: (Name: "Ada", Age: 36).
  • Deconstruct with var (a, b) = ...; use records when the grouping matters beyond one method.

Exercise

Write two string extension methods: Truncate(max) shortens text longer than max characters to its first max characters followed by "...", and IsPalindrome ignores case. Then make Stats return a named tuple with the Min, Max and Average of an array.

Program.cs
Loading editor…

C# runtime not loaded

Hint 1

Truncate: text.Length <= max ? text : text[..max] + "..."

Hint 2

IsPalindrome: lower-case the text, then compare it with its reverse — new string(lower.Reverse().ToArray()).

Hint 3

Stats: return (numbers.Min(), numbers.Max(), numbers.Average()); — LINQ's Min, Max and Average are extension methods themselves!

Show solution
Console.WriteLine("Hello world".Truncate(5));
Console.WriteLine("Racecar".IsPalindrome());

var (min, max, average) = Stats(new[] { 4, 1, 7 });
Console.WriteLine($"{min} {max} {average}");

(int Min, int Max, double Average) Stats(int[] numbers) =>
    (numbers.Min(), numbers.Max(), numbers.Average());

static class StringExtensions
{
    public static string Truncate(this string text, int max) =>
        text.Length <= max ? text : text[..max] + "...";

    public static bool IsPalindrome(this string text)
    {
        string lower = text.ToLower();
        return lower == new string(lower.Reverse().ToArray());
    }
}

Quiz

4 questions · 3 correct to pass
  1. 1.What makes a method an extension method?

  2. 2.Can an extension method access private fields of the type it extends?

  3. 3.Given `var t = (Name: "Ada", Age: 36);`, how do you read the age?

  4. 4.What does `var (name, age) = person;` do for a tuple or record?

To complete this lesson

  • ○ Solve the exercise
  • ○ Pass the quiz (3/4 correct)