Calling WebServices Asynchronously in .Net: Unique state object is required for multiple asynchronous simultaneous operations

When you call a web service aynchronously, you may want to make sure that responses arrive in the same order in which they were requested.

Is this always the case? Not necessarily. At times you just don’t care if the responses to requests A and B come in order B and A. Other times, order is crucial. One would argue that in these cases synchronous methods are better than asynchronous, but – especially when you are using third party web services, or particular technologies (PCLs are an example) – you cannot always choose to “go synchronously”.

.Net helps you enforce the correct request – response order by throwing the error that is the subject of this post. If a client requests multiple async methods in a short period of time, .Net tries to prevent the inconsistencies by complaining thusly:

ERROR: System.ArgumentException: There was an error during asynchronous processing. Unique state object is required for multiple asynchronous simultaneous operations to be outstanding. ---> System.ArgumentException: Item has already been added. Key in dictionary: 'System.Object' Key being added: 'System.Object' at System.Collections.Hashtable.Insert(Object key, Object nvalue, Boolean add)

When does this happen? Is .Net mean to us?

It happens when you call an async web service method before the previous call has received its

completed

event.

Let us reproduce the issue.
Let us imagine we have a web service that we reference in a Console project with a

myWSRef

reference.
Let us imagine the service exposes an async method called

getProductDataAsync(manufactorCode,productId)

Our client repeatedly calls the async service in a while-loop, like this:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace testwcf
{
    class Program
    {
       static void Main(string[] args)
        {
            string manufactorCode="blahblahCode";
            string productId = "blahblahCode2";

            // we define the client for the WS
            myWSRef.RemoteWS wsClient = new myWSRef.RemoteWS ();

            // we attach a handler to the "Completed" event
            wsClient.getProductDataResponseCompleted += callToProductDataCompleted;
            
            int prodNumb=0;
            while (prodNumb<=100)
            {
                try
                {
                    prodNumb++;
                    string artificialSuffix=new Random().Next().ToString();
                    wsClient.getProductDataAsync(manufactorCode,productId+artificialSuffix);
                }
                catch (Exception e)
                {
                    Console.WriteLine(e);
                }
            }
            Console.ReadLine();
        }

        public static void callToProductDataCompleted(object sender, myWSRef.GetProductDataCompletedEventArgs ea)
        {
            //this is the handler to the webserver reply

            if (ea.Error != null){
                Console.WriteLine("ERROR: " + ea.Error);
                Debug.WriteLine("ERROR: " + ea.Error);
             }
            else {
                Console.WriteLine("Call OK: ");
                Console.WriteLine(ea.Result);
            }
        }
    }
}

What happens if we run this? .Net will throw the runtime error that is the subject of this post.

Let us try to distantiate the calls by asking the Thread to sleep one second.

try
                {
                    prodNumb++;
                    string artificialSuffix=new Random().Next().ToString();
                    wsClient.EditorProductsDataResponse2Async(authCode, editorId, productId + artificialSuffix, idMachine, true, year);
                    Thread.Sleep(1000);
                 }

What happens? Temporarily, the error goes away. In fact, one second is enough for the web service response to call the callToProductDataCompleted routine, and we’re safe… unless… the next call takes three seconds rather than one second. And we’re back to square one.

How to solve this issue for good? Many suggest that every call has a unique GUID.

Stackoverflow offers
this suggestion: every call passes its own Guid to the service.

What about when the web service is done by someone else and you cannot pass it any uniqueID?

One way to solve this is with a semaphore: when the response of a web service call has not been received, you cannot call another one.

This is the code, with the IsBusy semaphore implemented

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace testwcf
{
    class Program
    {

        static Boolean IsBusy = false;  // this will be our semaphore
        static void Main(string[] args)
        {
            string manufactorCode="blahblahCode";
            string productId = "blahblahCode2";

            // we define the client for the WS
            myWSRef.RemoteWS wsClient = new myWSRef.RemoteWS ();

            // we attach a handler to the "Completed" event
            wsClient.getProductDataResponseCompleted += callToProductDataCompleted;
            
            int prodNumb=0;
            while (prodNumb<=100)
            {
               // if IsBusy, it means I am in the middle of a call
                if (IsBusy)
                {
                    continue; //I wait until the web service is not being called by another client still
                }
                try
                {
                    prodNumb++;
                    string artificialSuffix=new Random().Next().ToString();
                    wsClient.getProductDataAsync(manufactorCode,productId+artificialSuffix);
                }
                catch (Exception e)
                {

                    Console.WriteLine(e);
                    // treat the exception, then
                    IsBusy=false; // free the "semaphore" for another call
            }
            Console.ReadLine();
        }

        public static void callToProductDataCompleted(object sender, myWSRef.GetProductDataCompletedEventArgs ea)
        {
            //this is the handler to the webserver reply
             IsBusy = false; // when the WS call has been dutifully served, we "free" the loop to serve another one
            if (ea.Error != null){
                Console.WriteLine("ERROR: " + ea.Error);
                Debug.WriteLine("ERROR: " + ea.Error);
             }
            else {
                Console.WriteLine("Call OK: ");
                Console.WriteLine(ea.Result);

            }
        }
    }
}

There are other methods to avoid the request overlapping, of course, like calling the following method inside the Completed callback function (iteration). Just remember that the callToProductDataCompleted method is called even when the web server throws an error (as: 400 Bad request), so the method is the place to handle those exceptions. By contrast, client errors (as a timeout or network error) will be caught by the catch block of the getProductDataAsync call.