NeuroAgent

How to Fix WCF Dictionary Enumeration Error

Learn how to fix the 'Collection was modified; enumeration operation may not execute' error in WCF servers. Discover thread-safe solutions using locks, ConcurrentDictionary, and enumeration snapshots to prevent race conditions in your subscription service.

How to fix ‘Collection was modified; enumeration operation may not execute’ error in WCF server when modifying a dictionary during enumeration?

I’m developing a WCF server in a Windows service that uses a dictionary to track subscribers. The NotifySubscribers() method is called approximately 800 times per day to notify all subscribers of data events. When a client subscribes, their ID is added to the subscribers dictionary, and when they unsubscribe, it’s removed.

The error occurs when (or after) a client unsubscribes. The next time NotifySubscribers() is called, the foreach() loop fails with the error ‘Collection was modified; enumeration operation may not execute’. Interestingly, when a debugger is attached and a client unsubscribes, the code executes without issues.

Here’s my code:

csharp
[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);
        }
    }
}

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

NeuroAgent

The error “Collection was modified; enumeration operation may not execute” in your WCF server occurs because you’re modifying the subscribers dictionary during enumeration in the NotifySubscribers() method. When an exception is thrown during s.Callback.SignalData(), your code calls UnsubscribeEvent(), which removes items from the dictionary while the foreach loop is still active. Additionally, with InstanceContextMode.Single, multiple threads can access the dictionary simultaneously, creating race conditions that can cause this error even without exceptions.

Contents

Understanding the Root Cause

The fundamental issue is that C# collections are not designed to be modified during enumeration. As the Microsoft documentation explains, “An enumerator remains valid as long as the collection remains unchanged.” When you modify a collection while iterating over it, the internal state of the enumerator becomes corrupted, leading to the InvalidOperationException.

In your specific case, the problem manifests in two scenarios:

  1. During normal execution: When SignalData() throws an exception, UnsubscribeEvent() is called, which modifies the dictionary while the foreach loop is still active.

  2. During race conditions: With InstanceContextMode.Single, multiple client requests can access your service instance simultaneously. One thread might be enumerating the dictionary while another adds or removes subscribers, causing the error.

Why the debugger hides the problem: When you attach a debugger, execution slows down significantly, reducing the likelihood of race conditions. This is why the error doesn’t appear when debugging.

Thread Safety Issues in Your Implementation

Your current implementation has several thread safety concerns:

csharp
private static IDictionary<Guid, Subscriber> subscribers;

The use of a static field combined with InstanceContextMode.Single creates a shared resource accessed by multiple threads. The standard Dictionary<TKey, TValue> class is not thread-safe for any concurrent access, including reads and writes.

As Stack Overflow discussions highlight, “In general you are not allowed to modify a collection while an enumeration is ‘in flight’ regardless of whether it is multithreaded or single threaded.”

The race condition scenario:

  • Thread 1 calls NotifySubscribers() and starts foreach iteration
  • Thread 2 calls UnsubscribeEvent() and removes a subscriber
  • Thread 1 continues enumeration but the dictionary has been modified
  • InvalidOperationException is thrown

Solution 1: Proper Locking Mechanism

The most straightforward approach is to use a lock statement to ensure exclusive access to the dictionary during enumeration and modification:

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

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

    public void NotifySubscribers(DataRecord sr)
    {
        // Create a snapshot of values to avoid holding lock during callback
        List<Subscriber> subscribersToNotify;
        
        lock (_lockObject)
        {
            subscribersToNotify = new List<Subscriber>(subscribers.Values);
        }

        foreach(Subscriber s in subscribersToNotify)
        {
            try
            {
                s.Callback.SignalData(sr);
            }
            catch (Exception e)
            {
                DCS.WriteToApplicationLog(e.Message, 
                  System.Diagnostics.EventLogEntryType.Error);

                // Call unsubscribe outside the lock to avoid deadlocks
                UnsubscribeEvent(s.ClientId);
            }
        }
    }
    
    public Guid SubscribeEvent(string clientDescription)
    {
        lock (_lockObject)
        {
            Subscriber subscriber = new Subscriber();
            subscriber.Callback = OperationContext.Current.
                    GetCallbackChannel<IDCSCallback>();

            subscribers.Add(subscriber.ClientId, subscriber);
            
            return subscriber.ClientId;
        }
    }

    public void UnsubscribeEvent(Guid clientId)
    {
        lock (_lockObject)
        {
            try
            {
                subscribers.Remove(clientId);
            }
            catch(Exception e)
            {
                System.Diagnostics.Debug.WriteLine("Unsubscribe Error " + 
                        e.Message);
            }
        }
    }
}

Key improvements:

  • Lock during critical sections: All dictionary access is protected by a lock
  • Snapshot pattern: Create a copy of values before enumeration to avoid holding locks during callbacks
  • Deadlock prevention: Unsubscribe is called outside the lock in the exception handler

Solution 2: Using ConcurrentDictionary

For better performance and cleaner code, use ConcurrentDictionary<TKey, TValue> which is specifically designed for thread-safe operations:

csharp
using System.Collections.Concurrent;

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

    static SubscriptionServer()
    {
        subscribers = new ConcurrentDictionary<Guid, Subscriber>();
    }

    public void NotifySubscribers(DataRecord sr)
    {
        // Use ToList() to create a snapshot safe for enumeration
        List<Subscriber> subscribersToNotify = subscribers.Values.ToList();

        foreach(Subscriber s in subscribersToNotify)
        {
            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.TryAdd(subscriber.ClientId, subscriber);
        
        return subscriber.ClientId;
    }

    public void UnsubscribeEvent(Guid clientId)
    {
        subscribers.TryRemove(clientId, out _);
    }
}

Important considerations for ConcurrentDictionary:

  • Not all operations are thread-safe: As research shows, “extension methods assume that the collection isn’t being modified in another thread”
  • Use ToList() for safe enumeration: The Values property itself is thread-safe, but enumeration should be done on a snapshot
  • TryAdd/TryRemove: These atomic methods are safer than Add/Remove

Solution 3: Creating Enumeration Snapshots

Another approach is to always work with snapshots of the dictionary, ensuring you never enumerate over a potentially modified collection:

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

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

    public void NotifySubscribers(DataRecord sr)
    {
        // Create a complete snapshot before any processing
        Dictionary<Guid, Subscriber> snapshot;
        
        lock (_lockObject)
        {
            snapshot = new Dictionary<Guid, Subscriber>(subscribers);
        }

        foreach(var kvp in snapshot)
        {
            try
            {
                kvp.Value.Callback.SignalData(sr);
            }
            catch (Exception e)
            {
                DCS.WriteToApplicationLog(e.Message, 
                  System.Diagnostics.EventLogEntryType.Error);

                // Safe to modify original dictionary since we're working from snapshot
                UnsubscribeEvent(kvp.Key);
            }
        }
    }
    
    public Guid SubscribeEvent(string clientDescription)
    {
        lock (_lockObject)
        {
            Subscriber subscriber = new Subscriber();
            subscriber.Callback = OperationContext.Current.
                    GetCallbackChannel<IDCSCallback>();

            subscribers.Add(subscriber.ClientId, subscriber);
            
            return subscriber.ClientId;
        }
    }

    public void UnsubscribeEvent(Guid clientId)
    {
        lock (_lockObject)
        {
            subscribers.Remove(clientId);
        }
    }
}

Advantages of this approach:

  • Complete isolation: The snapshot ensures you’re working with a consistent state
  • Safe modification: You can safely modify the original dictionary during enumeration
  • Predictable behavior: Eliminates race conditions entirely

Solution 4: Exception Handling Refactoring

Modify your exception handling to avoid calling UnsubscribeEvent() during enumeration. Instead, collect failed subscribers and remove them afterward:

csharp
public void NotifySubscribers(DataRecord sr)
{
    List<Guid> failedSubscribers = new List<Guid>();
    
    foreach(Subscriber s in subscribers.Values.ToList()) // Use ToList() for snapshot
    {
        try
        {
            s.Callback.SignalData(sr);
        }
        catch (Exception e)
        {
            DCS.WriteToApplicationLog(e.Message, 
              System.Diagnostics.EventLogEntryType.Error);
            
            failedSubscribers.Add(s.ClientId);
        }
    }
    
    // Remove failed subscribers after enumeration completes
    foreach(Guid clientId in failedSubscribers)
    {
        UnsubscribeEvent(clientId);
    }
}

Benefits:

  • Clean separation: Enumeration and modification are separate operations
  • Reduced lock contention: Only one modification operation at the end
  • More efficient: Batch removal of failed subscribers

Best Practices for WCF Subscription Services

1. Instance Context Mode Considerations

While InstanceContextMode.Single works for your scenario, consider the alternatives:

InstanceContextMode Pros Cons Best For
Single Shared state, efficient for subscriptions Thread safety concerns, single point of failure Simple subscription services
PerSession Isolated sessions, natural subscription lifecycle More complex state management Session-based subscriptions
PerCall Stateless, no thread safety issues No shared state, subscription tracking difficult Stateless services

2. Callback Channel Reliability

Add callback channel health checking:

csharp
private bool IsCallbackValid(IDCSCallback callback)
{
    try
    {
        // Check if channel is still open
        return callback != null && 
               OperationContext.Current.Channel.State == CommunicationState.Opened;
    }
    catch
    {
        return false;
    }
}

3. Memory Management

Implement periodic cleanup of disconnected subscribers:

csharp
private static void CleanupDisconnectedSubscribers()
{
    List<Guid> toRemove = new List<Guid>();
    
    lock (_lockObject)
    {
        foreach(var kvp in subscribers)
        {
            try
            {
                if (!IsCallbackValid(kvp.Value.Callback))
                {
                    toRemove.Add(kvp.Key);
                }
            }
            catch
            {
                toRemove.Add(kvp.Key);
            }
        }
        
        foreach(Guid clientId in toRemove)
        {
            subscribers.Remove(clientId);
        }
    }
}

4. Performance Optimization

For high-frequency notifications (800 times per day), consider:

  • Batch processing: Group notifications when possible
  • Asynchronous callbacks: Use async/await to prevent blocking
  • Connection pooling: Reuse callback channels efficiently

Conclusion

The “Collection was modified; enumeration operation may not execute” error in your WCF server stems from two main issues: modifying collections during enumeration and improper thread handling in a multi-threaded WCF environment.

Key takeaways:

  1. Always avoid modifying collections during enumeration - Use snapshots or separate enumeration from modification
  2. Implement proper thread synchronization - Use locks or ConcurrentDictionary for shared resources
  3. Consider the WCF threading model - InstanceContextMode.Single requires careful thread safety handling
  4. Test under realistic load - Debuggers can hide race conditions that appear in production

Recommended solution: Use ConcurrentDictionary with the ToList() pattern for enumeration (Solution 2) as it provides the best balance of performance, safety, and code clarity for your subscription service scenario. This approach eliminates the enumeration error while maintaining thread safety for the 800 daily notifications your service handles.

Don’t forget to implement proper error handling and periodic cleanup of disconnected clients to ensure long-term reliability of your subscription service.