4

Ok first things first. This is some exception information given by the support team. I know the line and code where it happens. It happens in a FirstOrDefault call over a dictionary obtained from cache.

1) Exception Information
*********************************************
Exception Type: System.InvalidOperationException

Message: Collection was modified; enumeration operation may not execute.

Data: System.Collections.ListDictionaryInternal

Now I wanted to simulate the problem and I could do it in a simple ASP.net application.
My page has 2 Buttons - Button_Process and Button_Add
The code behind is as follows:

 public partial class _Default : System.Web.UI.Page
 {
    protected void Page_Load(object sender, EventArgs e)
    {  
       if (!IsPostBack)
        {
            var data = Cache["key"];

            if (data == null)
            {
                var dict = new Dictionary<int, string>();
                for (int i = 0; i < 10; i++)
                {
                    dict.Add(i, "i");
                }

                Cache["key"] = dict;
            }
        }
    }

    protected void ButtonProcess_Click(object sender, EventArgs e)
    {
        var data = Cache["key"] as Dictionary<int, string>;
        if (data != null)
        {
            foreach (var d in data.Values) //In actual code there is FirstOrDefault here
            {
                Thread.Sleep(1000);

                if (d.Contains("5"))
                {
                    //some operation
                }
            }
        }
    }

    protected void Button2_Click(object sender, EventArgs e)
    {
        var data = Cache["key"] as Dictionary<int, string>;
        if (data != null)
        {
            data.Add(new Random().Next(), "101");
            Cache["key"] = data;
        }
    }
  }

Now assume there are 2 requests:

Request 1 - Someone clicks on button_Process and some operation on cache object is taking place
Request 2 - Someone clicks on button_Add and the first person gets an exception - collection modified blah blah

I understand the problem - it is happening because we are accessing same bit of memory. I have 2 solutions in my mind:
1. I use a for loop instead of for each (to replace FirstOrDefault in actual code) - I dunno how efficient this operation will be after I make the changes. - I don't ever delete any item from cache so I was thinking of this solution
2. I put some lock over cache object or something on those lines - but I dunno exactly where and how should I lock this object.

Please help me with this. I am not able to figure out an efficient solution. What is the best way to handle such situations?

  • From your description it sounds like one of the [Concurrency Collection](https://msdn.microsoft.com/en-us/library/system.collections.concurrent(v=vs.100).aspx) classes could be used to replace the Dictionary you have in the cache. Replace Dictionary with [ConcurrentDictionary](https://msdn.microsoft.com/en-us/library/dd287189(v=vs.100).aspx) and, as long as you use the Concurrency methods (which are guaranteed to be thread-safe) this should work for you, i.e. replace dict.Add() with dict.AddOrUpdate() and replace the usage of FirstOrDefault() with TryGetValue() – phil Feb 26 '15 at 00:42
  • @pip - but I am using .net 3.5 and I don't see any concurrency collection for it. I cant change it to 4.0 since many apps are dependent on it. – Gurucharan Balakuntla Maheshku Feb 26 '15 at 04:19
  • When you do your "Operation" on the specific object do you in anyway modify the object in question or do you just use it to store data? – Thomas Lindvall Mar 05 '15 at 14:35

3 Answers3

3

This happens because you're working directly with object, locating in cache. Good practise, to avoid those exceptions and other wierd behavior (when you accidentally modify cache object) is working with copy of cache data. And there are several ways of achieving it, like doing clone or some kind of deep copy. What i prefer is keeping objects in cache serialized (any kind you like - json/xml/binary or w/e else), since (de)serialization makes a deep copy of your object. Following small code snippet will clarify things:

public static class CacheManager
{
    private static readonly Cache MyCache = HttpRuntime.Cache;

    public static void Put<T>(T data, string key)
    {
        MyCache.Insert(key, Serialize(data));
    }

    public static T Get<T>(string key)
    {
        var data = MyCache.Get(key) as string;
        if (data != null)
            return Deserialize<T>(data);
        return default(T);
    }

    private static string Serialize(object data)
    {
        //this is Newtonsoft.Json serializer, but you can use the one you like  
        return JsonConvert.SerializeObject(data);
    }

    private static T Deserialize<T>(string data)
    {
        return JsonConvert.DeserializeObject<T>(data);
    }
}

And usage:

var myObj = new Dictionary<int, int>();
CacheManager.Put(myObj, "myObj");
//...
var anotherObj = CacheManager.Get<Dictionary<int, int>>("myObj");
Sergio
  • 6,900
  • 5
  • 31
  • 55
1

Check Task Parallel Library for .NET 3.5. It has Concurrent Collections such as ConcurrentStack, ConcurentQueue and ConcurrentDictionary.

http://www.nuget.org/packages/TaskParallelLibrary

Rafet
  • 858
  • 9
  • 24
1

The problem is that the cache object is global for appdomain and the data stored in are shared between all request. The only solution to this problem is to activate a lock when you want to access to the collection and then release the lock (https://msdn.microsoft.com/en-us/library/vstudio/c5kehkcz%28v=vs.100%29.aspx). (sorry form my bad english)

bdn02
  • 1,500
  • 9
  • 15