Search code examples
c#asynchronousexceptioncontinuewith

c# async update http then save to local db WhenAll ContinueWith timing


I've got the following code which seems to run fine apart from the continuation on the WhenAll ... await Task.WhenAll(syncTasks).ContinueWith ... is run before all four methods are completed. Would appreciate any guidance on what I'm doing wrong here. I don't really feel like I understand how to arrange complex async functionality and what seems to be happening supports that. This is in a Xamarin App BTW although I don't suppose that really matters.

private async Task SyncItems()
{
    var updateItemOnes = Task.Run(() => 
    {
        UpdateItemOnesToServer(itemOnesToUpdate).ContinueWith(async (result) => {
            if (!result.IsFaulted && !result.IsCanceled) 
            {
                await UpdateItemOnesToLocal(itemOnesToUpdate);  
            }
        });
    });

    syncTasks.Add(updateItemOnes);

    var updateItemTwos = Task.Run(() => 
    {
        UpdateItemTwosToServer(itemTwosToUpdate).ContinueWith(async (result) => {
            if (!result.IsFaulted && !result.IsCanceled) 
            {
                await UpdateItemTwosToLocal(itemTwosToUpdate);  
            }
        });
    });

    syncTasks.Add(updateItemTwos );

    //Show Loading Dialog

    await Task.WhenAll(syncTasks).ContinueWith((result) => {

        if (!result.IsFaulted && !result.IsCanceled)
        {
            //Success               
        }
        else
        {
            //Error
        }

        //Hide Loading Dialog
    });
}

private async Task UpdateItemOnesToServer(IEnumerable<Item> itemOnesToUpdate)
{
    try
    {
        var listofTasks = new List<Task>();

        foreach (var item in itemOnesToUpdate)
        {
            var convertItemOneTask = Task.Run(async () => {
               //Convert Image File in Item to Base64 here
            });

            listofTasks.Add(convertItemOneTask); 
        }

        await Task.WhenAll(listofTasks);

        var response = await _apiManager.SaveItemOnes(itemOnesToUpdate);

        if (response.IsSuccessStatusCode)
        {
            //Update ItemOnes for Local Update with Response Values
        }
    }
    catch
    {
        throw;
    }
}

private async Task UpdateItemOnesToLocal(IEnumerable<Item> itemOnesToUpdate)
{
    var listOfTasks = new List<Task<bool>>();

    foreach (var itemOne in itemOnesToUpdate)
    {
        listOfTasks.Add(_localService.UpdateItemOne(itemOne));
    }

    await Task.WhenAll<bool>(listOfTasks);    
}

private async Task UpdateItemTwosToServer(IEnumerable<ItemOne> itemTwosToUpdate)
{
    try
    {
        var listofTasks = new List<Task>();

        foreach (var item in itemTwosToUpdate)
        {
            var convertItemTwoTask = Task.Run(async () => {
               //Convert Image File in Item to Base64 here
            });

            listofTasks.Add(convertItemTwoTask); 
        }

        await Task.WhenAll(listofTasks);

        var response = await _apiManager.SaveItemTwos(itemTwosToUpdate);

        if (response.IsSuccessStatusCode)
        {
            //Update ItemTwos for Local Update with Response Values
        }
    }
    catch
    {
        throw;
    }

}

private async Task UpdateItemTwosToLocal(IEnumerable<ItemTwo> itemTwosToUpdate)
{
    var listOfTasks = new List<Task<bool>>();

    foreach (var itemTwo in itemTwosToUpdate)
    {
        listOfTasks.Add(_localService.UpdateItemTwo(itemTwo));
    }

    await Task.WhenAll<bool>(listOfTasks);    
}

Thanks in advance to anyone who can provide a little clarity. It will be much appreciated.


Solution

  • So there are a few problems with this code.

    1. someTask.ContinueWith(X) Basically this says "once the someTask is completed, do X" (there's more going on but in this case think of it like this). However if you await someTask this will not include the ContinueWith part. So like this the Task.WhenAll(syncTasks) will not wait on your ContinueWith parts.

    2. var updateItemOnes = Task.Run(() => UpdateItemOnesToServer()) wrappers. There is no awaiting here, so this will create a Task that just starts the UpdateItemOnesToServer task. That is done instantly.

    If you would like to see what is happening in practice use this test class:

    class TestAsyncClass
    {
        public async Task Run()
        {
            var tasks = new List<Task>();
            Console.WriteLine("starting tasks");
            var task1 = Task.Run(() => {
                FakeServerCall1().ContinueWith(async (result) =>
                    {
                        if (!result.IsFaulted && !result.IsCanceled)
                            await FakeLocalCall1();
                    });
            });
            tasks.Add(task1);
    
            var task2 = Task.Run(() => {
                FakeServerCall2().ContinueWith(async (result) =>
                {
                    if (result.IsCompletedSuccessfully)
                        await FakeLocalCall2();
                });
            }); 
    
            tasks.Add(task2);
    
            Console.WriteLine("starting tasks completed");
    
            await Task.WhenAll(tasks);
    
            Console.WriteLine("tasks completed");
        }
    
        public async Task<bool> FakeServerCall1()
        {
            Console.WriteLine("Server1 started");
            await Task.Delay(3000);
            Console.WriteLine("Server1 completed");
            return true;
        }
    
        public async Task<bool> FakeServerCall2()
        {
            Console.WriteLine("Server2 started");
            await Task.Delay(2000);
            Console.WriteLine("Server2 completed");
            return true;
        }
    
        public async Task<bool> FakeLocalCall1()
        {
            Console.WriteLine("Local1 started");
            await Task.Delay(1500);
            Console.WriteLine("Local1 completed");
            return true;
        }
    
        public async Task<bool> FakeLocalCall2()
        {
            Console.WriteLine("Local2 started");
            await Task.Delay(2000);
            Console.WriteLine("Local2 completed");
            return true;
        }
    }
    

    You'll see that the output is as follows:

    • starting tasks
    • starting tasks completed
    • Server1 started
    • Server2 started
    • tasks completed
    • Server2 completed
    • Local2 started
    • Server1 completed
    • Local1 started
    • Local2 completed
    • Local1 completed

    Notice here the "tasks completed" is called straight after starting the two tasks.

    Now if we change the Run method like this I think we'll get the functionality you're looking for:

    public async Task Run()
    {
        var tasks = new List<Task>();
        Console.WriteLine("starting tasks");
        var task1 = Task.Run(async () =>
        {
            await FakeServerCall1();
            await FakeLocalCall1();
        });
        tasks.Add(task1);
    
        var task2 = Task.Run(async() =>
        {
            await FakeServerCall2();
            await FakeLocalCall2();
        }); 
    
        tasks.Add(task2);
    
        Console.WriteLine("starting tasks completed");
    
        await Task.WhenAll(tasks);
    
        Console.WriteLine("tasks completed");
    }
    

    Which will output:

    • starting tasks
    • starting tasks completed
    • Server1 started
    • Server2 started
    • Server2 completed
    • Local2 started
    • Server1 completed
    • Local1 started
    • Local2 completed
    • Local1 completed
    • tasks completed

    So we see here that Local1 is always after Server1 and Local2 is always after Server2 and "tasks completed" is always after Local1 & Local2

    Hope this helps!

    Edit: From you comment you said you would like to see any exceptions that occurred in the process. This is where you could use ContinueWith (it is also fired when exceptions are throw:

    await Task.WhenAll(tasks).ContinueWith((result) =>
    {
        if (result.IsFaulted)
        {
            foreach (var e in result.Exception.InnerExceptions)
            {
                Console.WriteLine(e);
            }
        }
    });
    

    If you change the following test calls:

    public async Task<bool> FakeServerCall2()
    {
        Console.WriteLine("Server2 started");
        await Task.Delay(1000);
        Console.WriteLine("Crashing Server2");
        throw new Exception("Oops server 2 crashed");
    }
    
    public async Task<bool> FakeLocalCall1()
    {
        Console.WriteLine("Local1 started");
        await Task.Delay(1500);
        Console.WriteLine("crashing local1");
        throw new Exception("Oh ohh, local1 crashed");
    }
    

    This will be your output:

    • starting tasks
    • starting tasks completed
    • Server1 started
    • Server2 started
    • Crashing Server2
    • Server1 completed
    • Local1 started
    • crashing local1
    • System.Exception: Oh ohh, local1 crashed at TestConsoleApp.TestAsyncClass.FakeLocalCall1() in ~\TestConsoleApp\TestConsoleApp\Program.cs:line 67 at TestConsoleApp.TestAsyncClass.b__0_0() in ~\TestConsoleApp\TestConsoleApp\Program.cs:line 17
    • System.Exception: Oops server 2 crashed at TestConsoleApp.TestAsyncClass.FakeServerCall2() in ~\TestConsoleApp\TestConsoleApp\Program.cs:line 59 at TestConsoleApp.TestAsyncClass.b__0_1() in ~\TestConsoleApp\TestConsoleApp\Program.cs:line 23
    • tasks completed