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:
[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?
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
- Thread Safety Issues in Your Implementation
- Solution 1: Proper Locking Mechanism
- Solution 2: Using ConcurrentDictionary
- Solution 3: Creating Enumeration Snapshots
- Solution 4: Exception Handling Refactoring
- Best Practices for WCF Subscription Services
- Conclusion
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:
-
During normal execution: When
SignalData()throws an exception,UnsubscribeEvent()is called, which modifies the dictionary while the foreach loop is still active. -
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:
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:
[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:
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
Valuesproperty 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:
[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:
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:
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:
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:
- Always avoid modifying collections during enumeration - Use snapshots or separate enumeration from modification
- Implement proper thread synchronization - Use locks or ConcurrentDictionary for shared resources
- Consider the WCF threading model - InstanceContextMode.Single requires careful thread safety handling
- 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.