1. The Evolution of Pain
You've probably run into this: you need to not just loop through a collection, but also know the number of each element. Like, to print a numbered list, change elements with non-zero indexes, or do something with every third one. In a regular for loop — easy:
// All classic, C# style:
for (int i = 0; i < list.Count; i++)
{
Console.WriteLine($"{i}: {list[i]}");
}
But if you started writing with LINQ, suddenly... you lost the index! All those pretty .Where, .Select, .OrderBy — they just give you the element, not its number. Of course, you'd love to do something like:
list.SelectWithIndex((item, index) => ...);
But there was never a standard method like SelectWithIndex. Sure, you could use the overload of Select to get the index, but... if you wanted to use the index without transforming or projecting, you had to hack together extra .Select calls, which made your beautiful LINQ code less readable.
How We Survived Before .NET 9
Back in the day, C# devs had their own hacker tricks:
var result = list.Select((item, index) => new { item, index });
And yeah — combos with other LINQ methods, but it always felt kinda "bolted on," and not really as clean as you'd want for nice code.
2. The New LINQ Method: Index — What Is It and Why?
Official Description
In .NET 9, the dev team finally listened to the cries of millions of programmers (okay, tens of thousands on Twitter, but still) — and added a new LINQ extension method — Index. On the official .NET 9 docs page you can find this:
Index() adds the index to each element in a sequence, starting from zero, and returns pairs (value, index), no need to create anonymous objects or write .Select((item, idx) => new { item, idx }) by hand.
That's powerful — now Index just gives you, for each element, a pair: the element and its index.
Method Signature
public static IEnumerable<(T Element, int Index)> Index<T>(this IEnumerable<T> source);
In plain English: For every element in your collection, you get a special "tuple" — Element and its Index. That's it. No more anonymous types.
3. Examples of Using the Index Method
Super Simple Example
Let's take a list of favorite fruits.
var fruits = new List<string> { "Apple", "Banana", "Orange", "Kiwi" };
foreach (var (fruit, idx) in fruits.Index())
{
Console.WriteLine($"{idx}: {fruit}");
}
Output:
0: Apple
1: Banana
2: Orange
3: Kiwi
That's it! Now you can elegantly get the "index-value" pair and use them inside any LINQ query.
Integration with Other LINQ Methods
Index is a full member of the LINQ family! You can easily plug it into your "chains."
Example 1: Filter Odd-Indexed Elements
var numbers = Enumerable.Range(10, 10); // 10, 11, ... 19
var oddIndexes = numbers.Index()
.Where(pair => pair.Index % 2 == 1)
.Select(pair => pair.Element);
Console.WriteLine(string.Join(", ", oddIndexes));
Output:
11, 13, 15, 17, 19
Example 2: Modify Elements with Even Indexes
var users = new List<string> { "Anna", "Igor", "Katya", "Denis" };
var modified = users.Index()
.Select(pair => pair.Index % 2 == 0 ? pair.Element.ToUpper() : pair.Element.ToLower());
foreach (var name in modified)
Console.WriteLine(name);
Output:
ANNA
igor
KATYA
denis
Example 3: Merging with Other Collections by Index (zip-like)
var ids = new[] { 101, 102, 103 };
var names = new[] { "Alice", "Bob", "Charlie" };
var merged = names.Index()
.Join(ids.Index(),
namePair => namePair.Index,
idPair => idPair.Index,
(namePair, idPair) => (idPair.Element, namePair.Element));
foreach (var (id, name) in merged)
Console.WriteLine($"{id}: {name}");
Output:
101: Alice
102: Bob
103: Charlie
4. Index in a Real Application
Let's add a new feature to our "eternal" student app: print a list of all students from our collection with their numbers (for example, so the user can pick a student by number).
Example: Printing Students with Numbers
Let's say we already have a Student class from previous examples:
public class Student
{
public string Name { get; set; }
public int Grade { get; set; }
}
Let's make a small list of students:
var students = new List<Student>
{
new Student { Name = "Dasha", Grade = 5 },
new Student { Name = "Petya", Grade = 3 },
new Student { Name = "Vova", Grade = 4 },
new Student { Name = "Olya", Grade = 5 }
};
Now, using Index, let's print them all with numbers:
foreach (var (student, idx) in students.Index())
{
Console.WriteLine($"{idx + 1}. {student.Name} — Grade: {student.Grade}");
}
Here
idx + 1 — so the numbering starts from one, not zero.
Result:
1. Dasha — Grade: 5
2. Petya — Grade: 3
3. Vova — Grade: 4
4. Olya — Grade: 5
Practical benefit: Now, if the user wants to pick a student by number — it's all set! The code is simpler, no "manual" counters, max readability — minimum bugs.
5. Comparison: Why Is Index Cooler Than the Old Ways?
Before .NET 9: Old School
Before, to get both the element and its index, you had to use the .Select((item, index) => ...) overload and build anonymous types:
var withIndexes = students.Select((student, index) => new { student, index });
To get the fields you wanted, you always had to write .student, .index — and the type was anonymous, no nice named tuple.
With .NET 9: 21st Century Style
Now — no need to worry about fields or anonymous types. It's all clear:
foreach (var (student, idx) in students.Index())
{
// works out of the box, intuitive, simple, pretty
}
The code is cleaner. Less code, fewer bugs. It's perfect for big LINQ chains where you don't want to think about extra overloads.
6. Nuances and Special Features
Where Can You Use It?
Index works with any object that implements IEnumerable<T>. So — with all regular collections, arrays, results of other LINQ queries.
What Type Does Index Return?
It returns an enumeration of tuples, where the first element is the object itself (usually called Element), the second — Index (type int). Thanks to modern tuple syntax in C#, you can write foreach (var (element, index) in ...) and get both values into variables right away.
Can You Use It with Query Syntax?
Nope, Index is an extension method, query syntax (SQL-like LINQ) doesn't support it directly. So this won't work:
// Doesn't work!
var query = from s in students.Index() select ...;
If you want to combine them, just wrap the method in parentheses and work with the result like a regular collection:
var query = from pair in students.Index()
where pair.Index > 1
select pair.Element;
Indexing: Always from Zero
Index always starts counting from zero, just like most things in C#. If you want to start from one — just add 1 where you need it.
8. Mistakes and Gotchas — What to Watch Out For
A lot of students at first get confused between Index and the .Select((item, index) => ...) overload. The most common mistakes:
— Trying to use Index in query syntax: "Why doesn't it work?" — because it's only an extension method.
— Expecting the index to start from 1, but of course, it starts from 0.
— Thinking that Index changes the original collection — but, like all LINQ methods, it returns a new sequence, doesn't modify the original (immutable approach).
Another interesting thing: if your collection is "lazy" (remember: LINQ queries are lazy by default), the Index method will also calculate indexes as you access elements, not ahead of time. This is perfect for working with big or even infinite sequences — indexing will always be correct and won't "blow up" your RAM.
GO TO FULL VERSION