4

Given, instances of CookieContainer are not thread safe.

Any public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.

So it turns out I cannot use the same container across multiple concurrent HTTP requests without synchronization. Unfortunatelly from the documentation at MSDN it's not clear how one can properly synchronize it.

A solution would be using a copy of a master container for each request and once the request is finished the cookies from the copy could be merged back to the master container. Creating a copy and merging can be done in a synchronized manner.

So the question is : how can I make a copy of an instance of the CookieContainer class?

Trident D'Gao
  • 18,973
  • 19
  • 95
  • 159
  • 2
    Have you seen the source code using Reflector or DotPeak? I believe CookieContainer is thread safe, I have used it many times with many web services requests sharing single CookieContainer and I never had any problems. – Akash Kava Aug 20 '13 at 20:50
  • I did, I still need to be able to copy it for serialization. – Trident D'Gao Aug 20 '13 at 20:59
  • http://stackoverflow.com/questions/15983166/how-can-i-get-all-cookies-of-a-cookiecontainer I think this meets your requirements. – Amer Sawan Aug 21 '13 at 00:11
  • For synchronization, what's wrong with `lock`? – John Saunders Aug 21 '13 at 00:22
  • @AmerSawan, that's a hack, isn't it? – Trident D'Gao Aug 21 '13 at 01:52
  • @JohnSaunders, you are not in control of the precise moment when the lock has to be on. Because the container is used by the internals of HttpRequest which is a black box. – Trident D'Gao Aug 21 '13 at 01:53
  • Good point. Can you say why you want to use copies of the same `CookieContainer`? It's an unusual request. – John Saunders Aug 21 '13 at 02:09
  • @JohnSaunders, if the copy is the exact replica of the original container it would work as if the original container was used. So it's like using the original container itself, with the only exception that the original container won't be affected and the copy will be affected instead. – Trident D'Gao Aug 21 '13 at 02:25
  • Yeah, but I think you should give more context. What do these HTTP requests _mean_? And do you only need a new request to start off as a copy of the original, or do you need them to be synchronized over the lifetime of the request? And can you keep track of the URL's that are being visited? – John Saunders Aug 21 '13 at 02:28
  • CookieContainer stores cookies associated with different domains. It's very handy to reuse the same container when you navigate across different pages. This way each request to a page will be given their own set cookies that are only relevant to the page (its URL) in question. So a container just maps a URL to a cookie collection. Assigning the request a cookie and storing new cookies that come from the response of that request happens behind the scenes. So this container is highly mutable which creates a problem when it gets modified in a multithreaded environment. No, I cannot track the URLs – Trident D'Gao Aug 21 '13 at 02:35
  • @bonomo what problem has occurred in multi threaded environment? Are you anticipating one without trying? – Akash Kava Aug 21 '13 at 05:52
  • @AkashKava, I am anticipating. I see what you mean, that's a good point, however a weak assumption. It's more of what you believe in. You believe it won't fail because you made a number of tries and for those tries it worked. I believe it can fail and thus eventually will. Neither point is proven or can be proven by continuing trying. It can fail on N+1 try or it can run without failing infinitely long. Practically speaking, will I bet my salary on claiming this code will run in production no problem knowing it's not guaranteed to be thread safe? No, will you? That's where the question arises. – Trident D'Gao Aug 21 '13 at 14:19
  • Anticipation is bad, try it, download Reflector or DotPeak and look at the source code, I am sure CookieContainer author knows and has made it thread safe. – Akash Kava Aug 21 '13 at 19:13
  • So the question isn't "how can I make a copy of an instance of the CookieContainer class", it's actually about how to use it across concurrent requests. – wdavo Aug 22 '13 at 01:55
  • Not when your title is "how can I make a copy of an instance of the CookieContainer class", and the last line explicitly says "So the question is : how can I make a copy of an instance of the CookieContainer class?" – wdavo Aug 22 '13 at 01:59
  • It looks like for ASP.NET if two concurrent request are made with the same session, the second request will not execute until the first request is finished. See [James's answer](http://stackoverflow.com/a/9675215/135280), and the [MSDN doc](https://msdn.microsoft.com/en-us/library/ms178581.aspx). If other http servers behave the same (see [mauris's answer](http://stackoverflow.com/a/1430921/135280)), thread safety of the CookieContainer may be a non issue, as each request would be handled sequentially anyway. – mcdon Mar 02 '16 at 19:27

5 Answers5

8

The CookieContainer class is Serializable. Since you said you need to serialize it anyway, why not just use a BinaryFormatter to serialize it to a MemorySteam and then Deserialize it to make a copy?

I know this is overly simple, so please ignore if it isn't helpful.

private CookieContainer CopyContainer(CookieContainer container)
{
    using(MemoryStream stream = new MemoryStream())
    {
        BinaryFormatter formatter = new BinaryFormatter();
        formatter.Serialize(stream, container);
        stream.Seek(0, SeekOrigin.Begin);
        return (CookieContainer)formatter.Deserialize(stream);
    }
}
Amer Sawan
  • 2,126
  • 1
  • 22
  • 40
Alan
  • 7,875
  • 1
  • 28
  • 48
  • @AmerSawan The dispose/close there isn't really necessary as the MemoryStream isn't holding unmanaged resources. Also, I assume that my code was enough to demonstrate the principle, and any user of the code would be smart enough to adapt for his own use and call Dispose/Close if he desires. – Alan Aug 21 '13 at 00:36
  • 1
    Since `MemoryStream` implements `IDisposable`, you need to call `Dispose` or FXCop will report warnings for the code. – Sam Harwell Aug 21 '13 at 01:26
  • 1
    @bonomo Yeah it looks like it works for me, but web development isn't my main thing, you may want to try it tomorrow and let me know if it is working for you too. I mostly write software for embedded systems and desktop apps – Alan Aug 21 '13 at 02:32
  • VS2019 v16.5.4, web app with .NET 4.7.2, Win 2012R2 Standard x64 - attempt to call formatter.Serialize() leads to exception "Object Graph cannot be null.\r\nParameter name: graph". What did I do wrong? – user216652 May 04 '20 at 17:10
5

Take a look at the CookieContainter class and you'll see that concurrent scenarios are suppose to occur when there are changes in the cookie collection, right?

You'll notice that the author of CookieContainer took care of using lock {} and SyncRoot all around these collection-changing parts of the code, and I don't think that such approach is not addressed to concurrent scenarios.

Also, you can notice that any added Cookie is literally cloned, so the cookies inside the container and all the operations made will not mess up with object references outside the cookie container. In the worst case of I'm missing something, the clone also gives us a tip of what exactly you have to copy and how you could do it, in case of using the reflection approach described in the other posts (I personally would not consider it a hack, since it fits the requirement and it is managed, legal and safe code :) ).

In fact, the mentions all over MSDN documentation are "Any instance members are not guaranteed to be thread safe." - its a kind of reminder, because you are right, you really need to be careful. Then with such statement you can suppose basically two things: 1) Non-static members are not safe at all. 2) Some members can be thread safe, but they aren't properly documented.

natenho
  • 5,231
  • 4
  • 27
  • 52
3

You may use Reflection to get all cookies related to all Uris and then create new CookieContainer and add them to it, maybe such as here :

public static CookieContainer DeepClone(CookieContainer src)
{
    CookieContainer cookieContainer = new CookieContainer();

    Hashtable table = (Hashtable)src.GetType().InvokeMember("m_domainTable", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance, null, src, new object[] { });

    foreach (var tableKey in table.Keys)
    {
        String str_tableKey = (string)tableKey;

        if (str_tableKey[0] == '.')
            str_tableKey = str_tableKey.Substring(1);

        SortedList list = (SortedList)table[tableKey].GetType().InvokeMember("m_list", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance, null, table[tableKey], new object[] { });

        foreach (var listKey in list.Keys)
        {
            String url = "https://" + str_tableKey + (string)listKey;

            CookieCollection collection = src.GetCookies(new Uri(url));

            foreach (Cookie c in collection)
                cookieContainer.Add(new Cookie(c.Name, c.Value, c.Path, c.Domain)
                {
                    Comment = c.Comment,
                    CommentUri = c.CommentUri,
                    Discard = c.Discard,
                    Expired = c.Expired,
                    Expires = c.Expires,
                    HttpOnly = c.HttpOnly,
                    Port = c.Port,
                    Secure = c.Secure,
                    Version = c.Version
                });
        }
    }
    return cookieContainer;
}
Amer Sawan
  • 2,126
  • 1
  • 22
  • 40
  • Thank you for reposting sombody's else answer: http://stackoverflow.com/a/15991071/139667 Using reflection is a hack. If you look at the internals there are 5 more fields you didn't bother to mention that define the state of the container. Do you have to copy them too? If there anything else outside of the CookieContainer class that has to be copied as well? – Trident D'Gao Aug 21 '13 at 01:57
  • Both reflection and serialization are hacks, but I think using reflection is more common and performance friendly IMHO! – Saw Aug 21 '13 at 08:01
  • Serialization is a legit (approved) hack. – Trident D'Gao Aug 22 '13 at 01:36
1

You can do it with reflection. This may be able to be improved and YMMV:

//Set up the source cookie container
var cookieContainerA = new CookieContainer();
cookieContainerA.Add(new Uri("http://foobar.com"), new Cookie("foo", "bar"));
cookieContainerA.Add(new Uri("http://foobar.com"), new Cookie("baz", "qux"));
cookieContainerA.Add(new Uri("http://abc123.com"), new Cookie("abc", "123"));
cookieContainerA.Add(new Uri("http://abc123.com"), new Cookie("def", "456"));

//Set up our destination cookie container
var cookieContainerB = new CookieContainer();

//Get the domain table member
var type = typeof(CookieContainer);
var domainTableField = type.GetField("m_domainTable", BindingFlags.NonPublic | BindingFlags.Instance);
var domainTable = (Hashtable)domainTableField.GetValue(cookieContainerA);

//Iterate the domain table
foreach (var obj in domainTable)
{
  var entry = (DictionaryEntry)obj;

  //The domain is the key (we only need this for our Console.WriteLine later)
  var domain = entry.Key;
  var valuesProperty = entry.Value.GetType().GetProperty("Values");
  var values = (IList)valuesProperty.GetValue(entry.Value);

  foreach (var valueObj in values)
  {
    //valueObj is a CookieCollection, cast and add to our destination container
    var cookieCollection = (CookieCollection)valueObj;
    cookieContainerB.Add(cookieCollection);

    //This is a dump of our source cookie container
    foreach (var cookieObj in cookieCollection)
    {
      var cookie = (Cookie)cookieObj;
      Console.WriteLine("Domain={0}, Name={1}, Value={2}", domain, cookie.Name, cookie.Value);
    }
  }
}


//Test the copying
//var foobarCookies = cookieContainerB.GetCookies(new Uri("http://foobar.com"));
//var abc123Cookies = cookieContainerB.GetCookies(new Uri("http://abc123.com"));
wdavo
  • 5,070
  • 1
  • 19
  • 20
  • Using reflection is a hack. If you look at the internals there are 5 more fields you didn't bother to mention that define the state of the container. Do you have to copy them too? If there anything else outside of the CookieContainer class that has to be copied as well? – Trident D'Gao Aug 21 '13 at 01:55
  • 1
    It is a hack but for whatever reason what you are trying to do wasn't made simple to do, so hacks are what you are left with. If you were against reflection maybe you could have mentioned that. 3 of the internals are mapped to properties (Capacity, MaxCookieSize and PerDomainCapacity) which you can copy through assignment. Count is N/A which leaves m_fqdnMyDomain, which I'm not really sure about TBH – wdavo Aug 21 '13 at 02:12
  • 1
    Alan's serialization solution is most likely the better option – wdavo Aug 21 '13 at 02:13
0

Just adding my own twist on Alan's answer above to convert to/from a Base64 string.

public static string ToBase64(CookieContainer container)
{
    string str = null;

    byte[] bytes = null;
    using (MemoryStream ms = new MemoryStream())
    {
        BinaryFormatter bf = new BinaryFormatter();
        bf.Serialize(ms, container);
        bytes = ms.ToArray();
    }
    str = Convert.ToBase64String(bytes);

    return str;
}

public static CookieContainer FromBase64(string container_base64)
{
    CookieContainer cc = null;

    byte[] bytes = Convert.FromBase64String(container_base64);
    using (MemoryStream ms = new MemoryStream(bytes))
    {
        BinaryFormatter bf = new BinaryFormatter();
        cc = (CookieContainer)bf.Deserialize(ms);
    }

    return cc;
}
Greg
  • 8,574
  • 21
  • 67
  • 109