foreach Loop in C#: Build a Sales Report Step by Step (.NET 10)

Every Monday our Dallas sales manager asks the same question: which state sold the most last week? A foreach loop in C# answers it in a few lines, because it visits every item in an array, list or dictionary without you managing an index.

In this tutorial, I’ll build that sales-by-state report one step at a time, so you’ll see how the foreach loop in C# handles arrays, List<T>, dictionaries, indexes, break and continue on .NET 10.

Here’s what we’re building. Five sales go in, the $0 order gets dropped, and the states come out ranked by revenue:

foreach loop in C# sales report in VS Code ranking TX, NY and CA by revenue with Index() and dictionary deconstruction
The finished report: one foreach builds the totals, a second one prints the ranking

The program has three moving parts:

  • A Sale record with the rep, the state and the amount.
  • A foreach that adds each sale into a Dictionary<string, decimal> of state totals.
  • A second foreach that prints the totals with a rank number.

Before You Start the foreach Loop Build

You’ll need the .NET 10 SDK and an editor. I run every step as a single-file app with dotnet run File.cs in VS Code; if that’s new to you, start with how to create a console application in Visual Studio Code.

Every step uses this sample data, and only one thing changes per step:

RepStateAmountNote
Jenna MooreTX$1,250
Ethan RodriguezCA$0Cancelled order
Ryan CarterCA$980
Chloe BradyTX$640
Zoe NgNY$1,575

Each sale is a one-line positional record, record Sale(string Rep, string State, decimal Amount);. Amounts are decimal because it’s money, and the files that print dollars set the culture to en-US.

Step 1: Write a Basic foreach Loop Over an Array

The syntax is foreach (Type item in collection) { ... }. The loop asks the collection for each element in order and runs the body once per element:

System.Globalization.CultureInfo.CurrentCulture = new("en-US");
Sale[] sales = [
    new("Jenna Moore", "TX", 1250m),
    new("Ethan Rodriguez", "CA", 0m),
    new("Ryan Carter", "CA", 980m),
    new("Chloe Brady", "TX", 640m),
    new("Zoe Ng", "NY", 1575m)];

foreach (Sale sale in sales)
{
    Console.WriteLine(
        $"{sale.Rep,-16} {sale.State}  {sale.Amount,6:C0}");
}

record Sale(string Rep, string State, decimal Amount);

Output:

Jenna Moore      TX  $1,250
Ethan Rodriguez  CA      $0
Ryan Carter      CA    $980
Chloe Brady      TX    $640
Zoe Ng           NY  $1,575
C# foreach loop over an array of Sale records printing rep, state and amount in VS Code
One line per element of the Sale[] array, in array order

Arrays work because they implement IEnumerable, and so does every collection type in .NET. My guide to arrays in C# covers declaring and filling them.

Note: foreach doesn’t actually need an interface. Per Microsoft’s reference, any type with a public GetEnumerator() whose result has Current and MoveNext() works, which is how Span<T> supports it.

Checkpoint: five rows, from Jenna Moore TX $1,250 down to Zoe Ng NY $1,575, including Ethan’s $0.

Step 2: Loop Through a List with var, continue and break

Now the sales live in a List<Sale>, and we keep a running total until the team hits its $2,500 weekly goal. I’ve switched to var, which lets the compiler infer Sale:

System.Globalization.CultureInfo.CurrentCulture = new("en-US");
List<Sale> sales = [
    new("Jenna Moore", "TX", 1250m),
    new("Ethan Rodriguez", "CA", 0m),
    new("Ryan Carter", "CA", 980m),
    new("Chloe Brady", "TX", 640m),
    new("Zoe Ng", "NY", 1575m)];

decimal total = 0;
foreach (var sale in sales)
{
    if (sale.Amount == 0) continue;   // skip $0 orders
    total += sale.Amount;
    Console.WriteLine($"+{sale.Amount,6:C0} = {total:C0}");
    if (total >= 2500m) break;        // weekly goal met
}
Console.WriteLine($"Stopped at {total:C0}");

record Sale(string Rep, string State, decimal Amount);

Output:

+$1,250 = $1,250
+  $980 = $2,230
+  $640 = $2,870
Stopped at $2,870
C# foreach over a List with continue skipping a $0 order and break stopping at the $2,500 goal, VS Code output
continue skips Ethan’s $0 order; break stops after Chloe’s sale

continue jumps to the next element, and break leaves the loop entirely. Zoe’s $1,575 sale never gets read, because the goal was already met. The List in C# tutorial has more on adding, finding and sorting items.

Checkpoint: three + lines ($1,250, $980 and $640) and Stopped at $2,870.

Step 3: Iterate a Dictionary with foreach (KeyValuePair and Deconstruction)

Next we group sales by state. One loop fills a Dictionary<string, decimal>, and you can read it back two ways:

System.Globalization.CultureInfo.CurrentCulture = new("en-US");
List<Sale> sales = [
    new("Jenna Moore", "TX", 1250m),
    new("Ethan Rodriguez", "CA", 0m),
    new("Ryan Carter", "CA", 980m),
    new("Chloe Brady", "TX", 640m),
    new("Zoe Ng", "NY", 1575m)];

var totals = new Dictionary<string, decimal>();
foreach (var sale in sales)
    totals[sale.State] =
        totals.GetValueOrDefault(sale.State) + sale.Amount;

foreach (KeyValuePair<string, decimal> pair in totals)
    Console.WriteLine($"{pair.Key}: {pair.Value:C0}");

foreach (var (state, total) in totals)
    Console.WriteLine($"{state} -> {total:C0}");

record Sale(string Rep, string State, decimal Amount);

Output:

TX: $1,890
CA: $980
NY: $1,575
TX -> $1,890
CA -> $980
NY -> $1,575
C# foreach over a Dictionary using KeyValuePair and tuple deconstruction var (state, total), VS Code output
Same totals printed with KeyValuePair and with deconstruction

Each element of a dictionary is a KeyValuePair<TKey, TValue>. Since it has a Deconstruct method, var (state, total) splits it into two variables, which reads much better.

GetValueOrDefault returns 0 for a state we haven’t seen yet, so there’s no ContainsKey check. More tricks are in my dictionary in C# guide.

Checkpoint: TX $1,890, CA $980 and NY $1,575, printed twice in insertion order.

Step 4: Get the Index in a foreach Loop

A foreach doesn’t give you a position, but a report needs rank numbers. You’ve got two clean ways to get one:

List<Sale> sales = [
    new("Jenna Moore", "TX", 1250m),
    new("Ethan Rodriguez", "CA", 0m),
    new("Ryan Carter", "CA", 980m),
    new("Chloe Brady", "TX", 640m),
    new("Zoe Ng", "NY", 1575m)];

// any .NET version: Select with an index
foreach (var (sale, i) in sales.Select((s, i) => (s, i)))
    Console.WriteLine($"#{i + 1} {sale.Rep}");

// .NET 9 and later: Index()
foreach (var (i, sale) in sales.Index())
    Console.WriteLine($"{i}: {sale.State}");

record Sale(string Rep, string State, decimal Amount);

Output:

#1 Jenna Moore
#2 Ethan Rodriguez
#3 Ryan Carter
#4 Chloe Brady
#5 Zoe Ng
0: TX
1: CA
2: CA
3: TX
4: NY
C# foreach with index using Select((s, i) => (s, i)) and the .NET 9 Index() method, VS Code output
Select with an index works everywhere; Index() needs .NET 9 or later

Index() is a LINQ method added in .NET 9 that returns (int Index, TSource Item) tuples. On older targets, Select((s, i) => (s, i)) builds the same tuple itself.

A plain int i = 0; counter with i++ at the end of the body also works. I only reach for it when the loop has a continue that shouldn’t count.

Checkpoint: #1 Jenna Moore to #5 Zoe Ng, then 0: TX to 4: NY. Index() starts at 0.

Step 5: Remove Items in a foreach Loop Without an Error

Ethan’s $0 order shouldn’t count, so the obvious move is to remove it inside the loop. Here’s what happens:

List<Sale> sales = [
    new("Jenna Moore", "TX", 1250m),
    new("Ethan Rodriguez", "CA", 0m),
    new("Ryan Carter", "CA", 980m),
    new("Chloe Brady", "TX", 640m),
    new("Zoe Ng", "NY", 1575m)];

foreach (var sale in sales)
{
    if (sale.Amount == 0)
        sales.Remove(sale);   // changes the list mid-loop
}
Console.WriteLine($"{sales.Count} sales left");

record Sale(string Rep, string State, decimal Amount);

Output (unhandled exception):

Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
   at System.Collections.Generic.List`1.Enumerator.MoveNext()
   at Program.<Main>$(String[] args) in C:\DotNetDemo\ForeachDemo\Step5RemoveCrash.cs:line 8
C# foreach InvalidOperationException Collection was modified; enumeration operation may not execute after List.Remove, VS Code terminal
Removing from the list you’re looping over throws on the next MoveNext

A List<T> enumerator stays valid only while the list is unchanged. Microsoft’s docs say any add, change or delete invalidates it, and the next MoveNext throws InvalidOperationException.

The fix is to stop changing the list you’re enumerating. Loop over a copy, or let the list remove the items itself:

List<Sale> sales = [
    new("Jenna Moore", "TX", 1250m),
    new("Ethan Rodriguez", "CA", 0m),
    new("Ryan Carter", "CA", 980m),
    new("Chloe Brady", "TX", 640m),
    new("Zoe Ng", "NY", 1575m)];

// Fix 1: loop over a copy, change the original
foreach (var sale in sales.ToList())
    if (sale.Amount == 0) sales.Remove(sale);
Console.WriteLine($"ToList copy: {sales.Count} sales left");

// Fix 2: let List<T> do it in one call
List<Sale> again = [
    new("Jenna Moore", "TX", 1250m),
    new("Ethan Rodriguez", "CA", 0m),
    new("Ryan Carter", "CA", 980m),
    new("Chloe Brady", "TX", 640m),
    new("Zoe Ng", "NY", 1575m)];
int removed = again.RemoveAll(s => s.Amount == 0);
Console.WriteLine($"RemoveAll: {removed} removed, {again.Count} left");

record Sale(string Rep, string State, decimal Amount);

Output:

ToList copy: 4 sales left
RemoveAll: 1 removed, 4 left

I use RemoveAll in the final report. It’s one call, and it returns how many items it removed.

Checkpoint: ToList copy: 4 sales left and RemoveAll: 1 removed, 4 left.

Test the Finished foreach Sales Report

Here’s the full program from the screenshot at the top. It removes the $0 sale, builds the totals with one foreach, then ranks them with Index() and nested deconstruction:

System.Globalization.CultureInfo.CurrentCulture = new("en-US");
List<Sale> sales = [
    new("Jenna Moore", "TX", 1250m),
    new("Ethan Rodriguez", "CA", 0m),
    new("Ryan Carter", "CA", 980m),
    new("Chloe Brady", "TX", 640m),
    new("Zoe Ng", "NY", 1575m)];

sales.RemoveAll(s => s.Amount == 0);
var totals = new Dictionary<string, decimal>();
foreach (var sale in sales)
    totals[sale.State] =
        totals.GetValueOrDefault(sale.State) + sale.Amount;

var ranked = totals.OrderByDescending(t => t.Value);
foreach (var (rank, (state, total)) in ranked.Index())
    Console.WriteLine($"{rank + 1}. {state}  {total,7:C0}");
Console.WriteLine($"All states: {totals.Values.Sum():C0}");

record Sale(string Rep, string State, decimal Amount);

Output:

1. TX   $1,890
2. NY   $1,575
3. CA     $980
All states: $4,445

var (rank, (state, total)) unpacks the Index() tuple and the KeyValuePair inside it in one go. TX wins with $1,890 from two sales, and the four real sales add up to $4,445.

The ranking itself comes from LINQ’s OrderByDescending. If you’d rather sort plain values, see how to sort an array in C#.

Can You Change the Item Inside a foreach Loop?

Not by assigning to the loop variable. It’s read-only, and the compiler stops you:

int[] quotas = [1000, 1500, 2000];
foreach (int quota in quotas)
    quota += 100;

Build output:

C:\DotNetDemo\ForeachDemo\AssignLoopVariable.cs(3,5): error CS1656: Cannot assign to 'quota' because it is a 'foreach iteration variable'

The build failed. Fix the build errors and run again.

When the collection can hand out references, a ref iteration variable writes straight into it. Arrays can through AsSpan():

int[] quotas = [1000, 1500, 2000];
foreach (ref int quota in quotas.AsSpan())
    quota += 100;   // writes straight into the array

Console.WriteLine(string.Join(", ", quotas));

Output:

1100, 1600, 2100

Every quota went up by 100 in the original array. With a list of records, change a property on the object or build a new list instead; the loop variable itself stays read-only.

await foreach for Async Streams

When the data arrives over time, like pages from a web API, the method returns IAsyncEnumerable<T> and you consume it with await foreach. It’s the same pattern as the while loop in C# polling code, without the manual plumbing:

await foreach (var state in LoadStatesAsync())
    Console.WriteLine($"Loaded {state}");

static async IAsyncEnumerable<string> LoadStatesAsync()
{
    foreach (var state in new[] { "TX", "CA", "NY" })
    {
        await Task.Delay(300);   // pretend this is a web API call
        yield return state;
    }
}

Output:

Loaded TX
Loaded CA
Loaded NY

Each state printed about 300 ms after the one before it. The loop body runs as soon as each item is ready, not after the whole list loads.

foreach vs for vs List.ForEach in C#

All three can walk our five sales. Here they are side by side on the same list:

List<Sale> sales = [
    new("Jenna Moore", "TX", 1250m),
    new("Ethan Rodriguez", "CA", 0m),
    new("Ryan Carter", "CA", 980m),
    new("Chloe Brady", "TX", 640m),
    new("Zoe Ng", "NY", 1575m)];

for (int i = 0; i < sales.Count; i++)
    Console.Write($"{i}:{sales[i].State} ");
Console.WriteLine("<- for");

foreach (var sale in sales)
    Console.Write($"{sale.State} ");
Console.WriteLine("<- foreach");

sales.ForEach(sale => Console.Write($"{sale.State} "));
Console.WriteLine("<- List.ForEach");

record Sale(string Rep, string State, decimal Amount);

Output:

0:TX 1:CA 2:CA 3:TX 4:NY <- for
TX CA CA TX NY <- foreach
TX CA CA TX NY <- List.ForEach
foreachforList<T>.ForEach
Works onAny enumerableAnything with an indexerList<T> only
Index availableNo (use Index())YesNo
break / continueYesYesNo (it’s a lambda)
Remove items while loopingNoYes, loop backwardNo
await insideYesYesNot safely

I default to foreach for reading and use a for loop in C# when I need the index or must change the list by position. List.ForEach is fine for one-liners, but you can’t break out of it.

Troubleshooting foreach Loop Errors

NullReferenceException When the Collection Is null

A foreach over an empty list simply doesn’t run, but a foreach over null throws. Fall back to an empty collection with ??:

List<Sale>? imported = null;   // e.g. a failed CSV import

int count = 0;
foreach (var sale in imported ?? [])
    count++;
Console.WriteLine($"Imported {count} sales");

record Sale(string Rep, string State, decimal Amount);

Output:

Imported 0 sales

For more ways to guard against missing data, see how to check for null values in C#.

Collection Was Modified; Enumeration Operation May Not Execute

That’s the Step 5 error. It also shows up when another thread adds to the list while you loop, because enumerating a List<T> isn’t thread-safe.

Important: copying with ToList() or locking the list fixes the threading version too.

Where to Take It Next: Extend the foreach Report

Three upgrades I’d make before handing this report to the sales team:

If you want to compare the other loops next, these are the ones I’d read:

Frequently Asked Questions

What is a foreach loop in C#?

It’s a statement that runs a block of code once for each element of a collection, such as an array, List<T> or Dictionary<TKey, TValue>. You don’t manage an index; the loop gets each element from the collection’s enumerator.

How do I get the index in a C# foreach loop?

On .NET 9 and later, use foreach (var (i, item) in list.Index()). On older versions, use list.Select((item, i) => (item, i)) or keep your own counter variable.

Can I remove items from a list inside a foreach loop?

No. Changing the list invalidates its enumerator and the next step throws InvalidOperationException. Loop over list.ToList(), call RemoveAll, or use a backward for loop.

Is foreach slower than a for loop in C#?

For arrays and List<T>, the difference is tiny because the compiler and the struct enumerator avoid extra allocations. Pick the loop that reads best and measure only when a profiler points at it.

Does foreach work on a null collection?

No. Microsoft’s iteration statements reference says a foreach over null throws NullReferenceException, while an empty collection just skips the body. Use collection ?? [] when the value can be null.

What is await foreach in C#?

It consumes an IAsyncEnumerable<T>, an async stream, and waits for each element as it arrives. You use it inside an async method or a top-level program.