Search code examples
c#async-awaittaskusing

async await execution order problems with httpclient


In our application we use async calls. These calls we need to wait for so we use await. But we notice that the application continues the application some where else on a await from the HttpClient.SendAsync. We reproduced it with the following code:-

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace AsyncExperiment
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("1");
            var adapter = new Adapter();
            Console.WriteLine("2");
            var result = Task.Factory.StartNew(() => adapter.Start()).Result;
            Console.WriteLine("21");
            Console.ReadKey();
        }
    }

    public class Adapter
    {
        public async Task<string> Start()
        {
            Console.WriteLine("3");
            return await CollectionAccessor.ExecuteWithinScope(async collection => {
                Console.WriteLine("8");
                var adapter = new AsyncSearchAdapter();
                Console.WriteLine("9");
                var result = await adapter.GetSearchAsync();
                Console.WriteLine("19");
                Console.WriteLine(result);
                Console.WriteLine("20");
                return "";
            });    
        }
    }

    public class Client
    {

        public async Task<string> Get()
        {
            Console.WriteLine("12");
            var requestMessage = new HttpRequestMessage(HttpMethod.Get, "https://22ad5e1e-688d-4ba4-9287-6bb4a351fd05.mock.pstmn.io/test");
            Console.WriteLine("13");
            HttpClient httpClient = new HttpClient();
            Console.WriteLine("14");
            HttpResponseMessage response = await httpClient.SendAsync(requestMessage);
            Console.WriteLine("15");
            if(response.IsSuccessStatusCode){
               Console.WriteLine("16a");
               return await response.Content.ReadAsStringAsync();     
            }
            Console.WriteLine("16b");
            return null;
        }
    }

    public class AsyncSearchAdapter
    {
        public async Task<string> GetSearchAsync()
        { 
            Console.WriteLine("10");
            var client = new Client();
            Console.WriteLine("11");
            var response = await client.Get();
            Console.WriteLine("17");
            if(response.Equals("{'test', 'test'}")){
                Console.WriteLine("18a");
                return response;
            }
            Console.WriteLine("18b");
            return response;
        }
    }

    public static class CollectionAccessor
    {

        public static TReturn ExecuteWithinScope<TReturn>(Func<ICatalogCollection, TReturn> func)
        {
            Console.WriteLine("4");
            if(func == null) throw new ArgumentNullException("func");
            Console.WriteLine("5");

            using(var catalogCollection = Resolver())
            {
                Console.WriteLine("7");
                return func(catalogCollection);
            }
        }

        public static ICatalogCollection Resolver()
        {
            Console.WriteLine("6");
             return new CatalogCollection();
        }
    }

    public interface ICatalogCollection: IDisposable
    {
        string notImportant { get;}
    }

    public class CatalogCollection : ICatalogCollection, IDisposable
    {
        public string notImportant { get;}

        public CatalogCollection(){ 
            notImportant = "test";
        }

        public void Dispose()
        {
            throw new NotImplementedException();
        }
    }


}

We expect the order of the logs to be

1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21

but we get the order like this:

1,2,3,4,5,6,7,8,9,10,11,12,13,14,21,15,16,17,18,19,20

Could someone explain to me why this is happening in this order. And how to get it in the expected order?

Thanks!!!


Solution

  • You are running async function (adapter.Start()) and not waiting for it. Try to change

      var result = Task.Factory.StartNew(() => adapter.Start()).Result;
    

    to

     var result = adapter.Start().Result;
    

    or

     var result = Task.Factory.StartNew(() => adapter.Start().Result).Result;
    

    and I guess you are doing same problem here

    await CollectionAccessor.ExecuteWithinScope(async collection => {...})
    

    just ensure that CollectionAccessor.ExecuteWithinScope will handle awaiting passed function into it. Like

    async Task CollectionAccessor.ExecuteWithinScope(Func <ICollection, Task> action)
    {
        ...
        await (action(collection));
        ...
    }
    

    or at least returning it

    async Task CollectionAccessor.ExecuteWithinScope(Func <ICollection, Task> action)
    {
        ...
        return  (action(collection));       
    }
    

    UPD

    Right here

    public static TReturn ExecuteWithinScope<TReturn>(Func<ICatalogCollection, TReturn> func)
    {
        Console.WriteLine("4");
        if (func == null) throw new ArgumentNullException("func");
        Console.WriteLine("5");
    
        using (var catalogCollection = Resolver())
        {
            Console.WriteLine("7");
            return func(catalogCollection); // <<<<<<<HERE
        }
    }
    

    you are creating Task which is not finished yet and you returning it and disposing collection before task fininshed. I guess you need to wait task for complete and only after it return it. Like

    public static async Task<TReturn> ExecuteWithinScope<TReturn>(Func<ICatalogCollection, Task<TReturn>> func)
    {
        Console.WriteLine("4");
        if (func == null) throw new ArgumentNullException("func");
        Console.WriteLine("5");
    
        using (var catalogCollection = Resolver())
        {
            Console.WriteLine("7");
            return await func(catalogCollection); // waiting task for completition
        }
    }
    

    OR you need to dispose collection inside task, like

    public static TReturn ExecuteWithinScope<TReturn>(Func<ICatalogCollection, TReturn> func)
    {
        Console.WriteLine("4");
        if (func == null) throw new ArgumentNullException("func");
        Console.WriteLine("5");
    
        //using (var catalogCollection = Resolver()) // not in using!
        {
            var catalogCollection = Resolver();
            Console.WriteLine("7");
            return func(catalogCollection);
        }
    }
    

    And then

        return await CollectionAccessor.ExecuteWithinScope(async collection =>
        {
            Console.WriteLine("8");
            var adapter = new AsyncSearchAdapter();
            Console.WriteLine("9");
            var result = await adapter.GetSearchAsync();
            Console.WriteLine("19");
            Console.WriteLine(result);
            Console.WriteLine("20");
    
            collection.Dispose();  //// Disposing!
            return "";
        });
    

    From my point first approach (await func(catalogCollection);) - is best one