If you try to add/remove items from a collection while it’s being looped over in a foreach loop (enumerated), then you’ll get the following exception:
System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
at System.Collections.Generic.List`1.Enumerator.MoveNext()
This error can happen in two scenarios:
- You’re looping over the collection in a foreach loop and modifying it (add/removing) in the same loop.
- You have a race condition: You’re looping over the collection in one thread while another thread is modifying the collection.
The solution to this problem depends on which scenario you’re in. In this article, I’ll go over these scenarios and possible solutions.
Table of Contents
Scenario 1 – The collection is modified in the foreach loop
This scenario is very common. Usually devs will run into this when attempting to remove items from a collection in a loop, like this:
foreach (var movie in movieCollection)
{
if (movie.Contains(removeMovie))
{
movieCollection.Remove(removeMovie);
}
}
Code language: C# (cs)
This will throw InvalidOperationException at run-time. In my opinion, it would be better if the compiler caught this problem and showed a compile-time error instead.
The solution is to make sure you’re not modifying the collection in the foreach loop.
Solution 1 – If you’re removing items, use RemoveAll()
If you’re modifying the collection by removing items, then the simplest solution is to use RemoveAll() (Linq) instead, like this:
movieCollection.RemoveAll(movie => movie.Contains(removeMovie));
Code language: C# (cs)
This removes items that meet the conditions and doesn’t throw the run-time exception.
Solution 2 – If you’re adding items, put them in a temp and use AddRange()
Since you can’t add items while looping over it in a foreach loop, the simplest solution is:
- Put the list of items to add to a temp list.
- After looping, add the temp list to the list with AddRange().
Here’s an example:
var itemsToAdd = new List<string>();
foreach (var movie in movieCollection)
{
if (movie.Contains(duplicateMovie))
{
itemsToAdd.Add(duplicateMovie);
}
}
movieCollection.AddRange(itemsToAdd);
Code language: C# (cs)
Solution 3 – Use a regular for loop and loop in reverse
Instead of using a foreach loop, you can use a regular for loop. When you’re modifying a collection in a loop, it’s a good idea to loop in reverse. Here’s an example of looping in reverse and adding items:
for (int i = movieCollection.Count - 1; i >= 0; i--)
{
if (movieCollection[i].Contains(duplicateMovie))
{
movieCollection.Add(duplicateMovie);
}
}
Code language: C# (cs)
If you tried the same logic while looping forward, it would actually result in an infinite loop.
Scenario 2 – One thread is modifying the collection while another thread is looping over it
You can get this InvalidOperationException if you’re looping over the collection in one thread and modifying it from another thread at the same time.
The following code shows an example of this scenario:
//Resource shared between multiple threads (recipe for a race condition)
private List<string> movieCollection = new List<string>();
//Called by thread 1
void Post(string movie)
{
movieCollection.Add(movie);
}
//Called by thread 2
void GetAll()
{
//Race condition results in InvalidOperationException (can't modify collection while enumerating) here
foreach (var movie in movieCollection)
{
Console.WriteLine(movie);
}
}
Code language: C# (cs)
This code isn’t thread-safe. One thread is modifying the collection while another thread is looping over it. The thread that’s looping will run into the InvalidOperationException. Since this is a race condition, the error won’t happen every time, which means it’s possible for this bug to make it into production. Multithreading bugs are sneaky like that.
Any time you’re multithreading, you need to control access to shared resources. One way to do it is to use locks. A better way to do it in this scenario is to use a concurrent collection.
Solution – Use a concurrent collection
Switching the movieCollection field to be a ConcurrentBag<string> eliminates the race condition.
using System.Collections.Concurrent;
private ConcurrentBag<string> movieCollection = new ConcurrentBag<string>();
//Called by thread 1
void Post(string movie)
{
movieCollection.Add(movie);
}
//Called by thread 2
void GetAll()
{
foreach (var movie in movieCollection)
{
Console.WriteLine(movie);
}
}
Code language: C# (cs)
ToList() doesn’t solve the problem and results in a different exception
If you have a race condition, using ToList() won’t solve the problem. In fact, the race condition will still be there, it’ll just be a different exception.
Here’s an example of attempting to use ToList() to try to fix the original race condition:
void GetAll()
{
var snapshot = movieCollection.ToList();
foreach (var movie in snapshot)
{
Console.WriteLine(movie);
}
}
Code language: C# (cs)
Eventually this will run into the following exception:
System.ArgumentException: ‘Destination array was not long enough. Check the destination index, length, and the array’s lower bounds. (Parameter ‘destinationArray’)’
This is caused by a race condition. One thread is calling ToList() and another thread is modifying the list. Whatever ToList() is doing internally, it’s not thread-safe.
Don’t use ToList(). Use a concurrent collection instead.