Collection was modified; enumeration operation may not execute

cdonner Source

I can't get to the bottom of this error, because when the debugger is attached, it does not seem to occur. Below is the code.

This is a WCF server in a Windows service. The method NotifySubscribers is called by the service whenever there is a data event (at random intervals, but not very often - about 800 times per day).

When a Windows Forms client subscribes, the subscriber ID is added to the subscribers dictionary, and when the client unsubscribes, it is deleted from the dictionary. The error happens when (or after) a client unsubscribes. It appears that the next time the NotifySubscribers() method is called, the foreach() loop fails with the error in the subject line. The method writes the error into the application log as shown in the code below. When a debugger is attached and a client unsubscribes, the code executes fine.

Do you see a problem with this code? Do I need to make the dictionary thread-safe?

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
public class SubscriptionServer : ISubscriptionServer
{
    private static IDictionary<Guid, Subscriber> subscribers;

    public SubscriptionServer()
    {            
        subscribers = new Dictionary<Guid, Subscriber>();
    }

    public void NotifySubscribers(DataRecord sr)
    {
        foreach(Subscriber s in subscribers.Values)
        {
            try
            {
                s.Callback.SignalData(sr);
            }
            catch (Exception e)
            {
                DCS.WriteToApplicationLog(e.Message, 
                  System.Diagnostics.EventLogEntryType.Error);

                UnsubscribeEvent(s.ClientId);
            }
        }
    }


    public Guid SubscribeEvent(string clientDescription)
    {
        Subscriber subscriber = new Subscriber();
        subscriber.Callback = OperationContext.Current.
                GetCallbackChannel<IDCSCallback>();

        subscribers.Add(subscriber.ClientId, subscriber);

        return subscriber.ClientId;
    }


    public void UnsubscribeEvent(Guid clientId)
    {
        try
        {
            subscribers.Remove(clientId);
        }
        catch(Exception e)
        {
            System.Diagnostics.Debug.WriteLine("Unsubscribe Error " + 
                    e.Message);
        }
    }
}
c#wcfconcurrencydictionarythread-safety

Answers

answered 9 years ago JaredPar #1

What's likely happening is that SignalData is indirectly changing the subscribers dictionary under the hood during the loop and leading to that message. You can verify this by changing

foreach(Subscriber s in subscribers.Values)

To

foreach(Subscriber s in subscribers.Values.ToList())

If I'm right, the problem will dissapear

answered 9 years ago Mitch Wheat #2

When a subscriber unsubscribes you are changing contents of the collection of Subscribers during enumeration.

There are several ways to fix this, one being changing the for loop to use an explicit .ToList():

public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
                                              ^^^^^^^^^  
        ...

answered 9 years ago x4000 #3

A more efficient way, in my opinion, is to have another list that you declare that you put anything that is "to be removed" into. Then after you finish your main loop (without the .ToList()), you do another loop over the "to be removed" list, removing each entry as it happens. So in your class you add:

private List<Guid> toBeRemoved = new List<Guid>();

Then you change it to:

public void NotifySubscribers(DataRecord sr)
{
    toBeRemoved.Clear();

    ...your unchanged code skipped...

   foreach ( Guid clientId in toBeRemoved )
   {
        try
        {
            subscribers.Remove(clientId);
        }
        catch(Exception e)
        {
            System.Diagnostics.Debug.WriteLine("Unsubscribe Error " + 
                e.Message);
        }
   }
}

...your unchanged code skipped...

public void UnsubscribeEvent(Guid clientId)
{
    toBeRemoved.Add( clientId );
}

This will not only solve your problem, it will prevent you from having to keep creating a list from your dictionary, which is expensive if there are a lot of subscribers in there. Assuming the list of subscribers to be removed on any given iteration is lower than the total number in the list, this should be faster. But of course feel free to profile it to be sure that's the case if there's any doubt in your specific usage situation.

answered 6 years ago luc.rg.roy #4

Actually the problem seems to me that you are removing elements from the list and expecting to continue to read the list as if nothing had happened.

What you really need to do is to start from the end and back to the begining. Even if you remove elements from the list you will be able to continue reading it.

answered 6 years ago Mohammad Sepahvand #5

You can also lock your subscribers dictionary to prevent it from being modified whenever its being looped:

 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }

answered 5 years ago Rezoan #6

You can copy subscribers dictionary object to a same type of temporary dictionary object and then iterate the temporary dictionary object using foreach loop.

answered 5 years ago ford prefect #7

So a different way to solve this problem would be instead of removing the elements create a new dictionary and only add the elements you didnt want to remove then replace the original dictionary with the new one. I don't think this is too much of an efficiency problem because it does not increase the number of times you iterate over the structure.

answered 4 years ago Daniel Moreshet #8

I had the same issue, and it was solved when I used a for loop instead of foreach.

// foreach (var item in itemsToBeLast)
for (int i = 0; i < itemsToBeLast.Count; i++)
{
    var matchingItem = itemsToBeLast.FirstOrDefault(item => item.Detach);

   if (matchingItem != null)
   {
      itemsToBeLast.Remove(matchingItem);
      continue;
   }
   allItems.Add(itemsToBeLast[i]);// (attachDetachItem);
}

answered 4 years ago open and free #9

Note: In general .Net collections do not support being enumerated and modified at the same time. If you try to modifies the collection list while you are in the middle of enumerating it it will raise an exception.

So the issue behind this error is, we can not modify the list/dictionary while we are looping through. But if we iterate a dictionary using a temp list of its keys, in parallel we can modify the dictionary object, because now we are not iterating the dictionary(and iterating its key collection).

sample:

//get key collection from dictionary into a list to loop through
List<int> keys = new List<int>(Dictionary.Keys);

// iterating key collection using simple for-each loop
foreach (int key in keys)
{
  // Now we can perform any modification with values of dictionary.
  Dictionary[key] = Dictionary[key] - 1;
}

Here is a blog post about this solution.

And for a deep dive in stackoverflow: Why this error occurs?

answered 3 years ago Mike #10

I've seen many options for this but to me this one was the best.

ListItemCollection collection = new ListItemCollection();
        foreach (ListItem item in ListBox1.Items)
        {
            if (item.Selected)
                collection.Add(item);
        }

Then simply loop through the collection.

Be aware that a ListItemCollection can contain duplicates. By default there is nothing preventing duplicates being added to the collection. To avoid duplicates you can do this:

ListItemCollection collection = new ListItemCollection();
            foreach (ListItem item in ListBox1.Items)
            {
                if (item.Selected && !collection.Contains(item))
                    collection.Add(item);
            }

answered 1 year ago vivek #11

InvalidOperationException- An InvalidOperationException has occurred. It reports a "collection was modified" in a foreach-loop

Use break statement, Once the object is removed.

ex:

ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}

answered 4 weeks ago Mark Aven #12

Okay so what helped me was iterating backwards. I was trying to remove an entry from a list but iterating upwards and it screwed up the loop because the entry didn't exist anymore:

for (int x = myList.Count - 1; x > -1; x--)
                        {

                            myList.RemoveAt(x);

                        }

comments powered by Disqus