0

I guess my code has many weak points, so please feel free to share any thoughts. My main question btw, is that when I'm trying to do the following, and at the end, when I'd like to wait all my tasks (by using Task.WaitAll) to get completed to examine if there were any exceptions, will it really make any part of the code to run synchronously, just because the lack of an 'await' operator?

public class Program
{
    static bool mock = true;
    static readonly object s_lockSource = new object();
    static readonly CancellationTokenSource s_tokenSource = new CancellationTokenSource();
    static readonly CancellationToken s_token = s_tokenSource.Token;
    public static async Task Main()
    {
        var sw = new Stopwatch();
        sw.Start();

        IConfigurationRoot config = new ConfigurationBuilder()
            .SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
            .AddJsonFile("appsettings.json").Build();

        var logger = new LiteLogger(new MemoryStream(), config);
        logger.Log(LogLevel.Debug, "Fetching data...");

        var jsonHelper = new JsonHelper();
        IAsyncIO fileService = mock ? new MockAsyncIO() : new FileService(config, s_token);
        IAsyncService webService = mock ? new MockAsyncIO() : new WebService(config, s_token);

        var remoteFetchTask = webService.FetchAsync(InOutOptions.None);
        var localFetchTask = fileService.FetchAsync(InOutOptions.ForecastPath);
        var todaysFetchTask = fileService.FetchAsync(InOutOptions.TodaysPath);

        var parseRemoteDataTask = remoteFetchTask
            .ContinueWith(task =>
            {
                lock (s_lockSource)
                {
                    logger.Log(LogLevel.Success, "Remote fetch task completed!");
                    logger.Log(LogLevel.Info, "Parsing remote data...");
                }
                return jsonHelper.FromJsonAsync<Region[]>(task.Result);
            }, TaskContinuationOptions.OnlyOnRanToCompletion);

        var filterRemoteRegionsTask = parseRemoteDataTask
            .ContinueWith(task =>
            {
                lock (s_lockSource)
                {
                    logger.Log(LogLevel.Success, "Forecasts parsing task completed!");
                    logger.Log(LogLevel.Info, "Merging data...");
                }

                Region[] remoteRegions = parseRemoteDataTask.Result.Result;
                return Task.Run(() => new DataFilter(config).FilterRegions(remoteRegions));
            }, TaskContinuationOptions.OnlyOnRanToCompletion);

        var parseLocalDataTask = localFetchTask
            .ContinueWith(task =>
            {
                lock (s_lockSource)
                {
                    logger.Log(LogLevel.Success, "Local fetch task completed!");
                    logger.Log(LogLevel.Info, "Parsing local data...");
                }
                return jsonHelper.FromJsonAsync<Region>(task.Result);
            }, TaskContinuationOptions.OnlyOnRanToCompletion);

        var parseTodaysDataTask = todaysFetchTask
            .ContinueWith(task =>
            {
                lock (s_lockSource)
                {
                    logger.Log(LogLevel.Success, "Today's fetch task completed!");
                    logger.Log(LogLevel.Info, "Parsing today's data...");
                }
                return jsonHelper.FromJsonAsync<Forecast[]>(task.Result);
            }, TaskContinuationOptions.OnlyOnRanToCompletion);


        var mergeTask =
            Task.WhenAll(filterRemoteRegionsTask, parseLocalDataTask)
            .ContinueWith(_ =>
            {
                lock (s_lockSource)
                {
                    logger.Log(LogLevel.Success, "Forecasts parsing task completed!");
                    logger.Log(LogLevel.Info, "Merging data...");
                }

                Region localInstance = parseLocalDataTask.Result.Result;
                Region remoteInstance = filterRemoteRegionsTask.Result.Result;

                var dm = new DataMerger();

                return Task.Run(() => dm.MergeRegions(localInstance, remoteInstance));
            }, TaskContinuationOptions.OnlyOnRanToCompletion);

        var forecastsSerializationTask = mergeTask
            .ContinueWith(task =>
            {
                lock (s_lockSource)
                {
                    logger.Log(LogLevel.Success, "MergeTask completed!");
                    logger.Log(LogLevel.Info, "Serializing forecasts data...");
                }

                Region newLocalInstance = task.Result.Result;
                return jsonHelper.ToJsonAsync(newLocalInstance);
            }, TaskContinuationOptions.OnlyOnRanToCompletion);

        var forecastsStoreTask = forecastsSerializationTask
            .ContinueWith(task =>
            {
                lock (s_lockSource)
                {
                    logger.Log(LogLevel.Success, "Forecasts serialization task completed!");
                    logger.Log(LogLevel.Info, "Storing forecasts data...");
                }

                var newLocalJson = task.Result.Result;

                return fileService.PersistAsync(newLocalJson, InOutOptions.ForecastPath);
            }, TaskContinuationOptions.OnlyOnRanToCompletion);


        var todaysDataBuildTask =
            Task.WhenAll(parseTodaysDataTask, filterRemoteRegionsTask)
            .ContinueWith(_ =>
            {
                lock (s_lockSource)
                {
                    logger.Log(LogLevel.Success, "Today's weather parsing task completed!");
                    logger.Log(LogLevel.Info, "Building today's data...");
                }

                Region remoteInstance = filterRemoteRegionsTask.Result.Result;
                Forecast[] todaysWeathers = parseTodaysDataTask.Result.Result;

                var tdb = new TodaysDataBuilder(remoteInstance, todaysWeathers);
                return Task.Run(() => tdb.Build());
            }, TaskContinuationOptions.OnlyOnRanToCompletion);

        var todaysDataSerializationTask = todaysDataBuildTask
            .ContinueWith(task =>
            {
                lock (s_lockSource)
                {
                    logger.Log(LogLevel.Success, "Today's weather data build task completed!");
                    logger.Log(LogLevel.Info, "Serializing today's data...");
                }

                return jsonHelper.ToJsonAsync(task.Result.Result);
            }, TaskContinuationOptions.OnlyOnRanToCompletion);

        var todaysDataStoreTask = todaysDataSerializationTask
            .ContinueWith(task =>
            {
                lock (s_lockSource)
                {
                    logger.Log(LogLevel.Success, "Today's weather data serialization task completed!");
                    logger.Log(LogLevel.Info, "Storing today's data...");
                }

                return fileService.PersistAsync(task.Result.Result, InOutOptions.TodaysPath);
            }, TaskContinuationOptions.OnlyOnRanToCompletion);

        var uiDataBuildTask = Task.WhenAll(mergeTask, todaysDataBuildTask)
            .ContinueWith(_ =>
            {
                lock (s_lockSource)
                {
                    logger.Log(LogLevel.Success, "Antecedent tasks completed!");
                    logger.Log(LogLevel.Info, "Building UI data source...");
                }

                var newLocalInstance = mergeTask.Result.Result;
                var newTodaysDatas = todaysDataBuildTask.Result.Result;

                var usb = new UiSourceBuilder(newLocalInstance, newTodaysDatas);
                return Task.Run(() => usb.Build());
            }, TaskContinuationOptions.OnlyOnRanToCompletion);


        var uiDataStoreTask = uiDataBuildTask
            .ContinueWith(task =>
            {
                lock (s_lockSource)
                {
                    logger.Log(LogLevel.Success, "Building UI data completed!");
                    logger.Log(LogLevel.Info, "Saving UI data to source file...");
                }

                return fileService.PersistAsync(task.Result.Result, InOutOptions.ResultPath);
            }, TaskContinuationOptions.OnlyOnRanToCompletion);


        try
        {
            Task.WaitAll(new Task[]
                {
            localFetchTask,
            remoteFetchTask,
            todaysFetchTask,
            parseLocalDataTask,
            parseRemoteDataTask,
            parseTodaysDataTask,
            mergeTask,
            forecastsStoreTask,
            todaysDataStoreTask,
            uiDataStoreTask
                });

            sw.Stop();
            var overall = sw.Elapsed.TotalSeconds;
            logger.Log(LogLevel.Success, "All task completed!");
            logger.Log(LogLevel.Info, $"Finished in {overall} second{(overall != 1 ? "s" : "")}");
            if (overall <= 1)
                logger.Log(LogLevel.Warn, "This application is too fast :)");
        }
        catch (AggregateException ae)
        {
            foreach (var e in ae.Flatten().InnerExceptions)
                logger.Log(LogLevel.Error,
                    $"Exception has been thrown at: {e.StackTrace}" +
                    $"{Environment.NewLine}\t\t{e.Message}");
        }
        catch (Exception ex)
        {
            logger.Log(LogLevel.Fatal, ex.ToString());
        }

        Console.WriteLine("\nPress any key to continue...");
        Console.ReadKey();
        logger.Dispose();
    }
}

Full source, if needed for further information. Maybe it's also worth mentioning that I'm using .NET 5.0.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
NeMi
  • 11
  • 3
  • 3
    A method that is marked `async` is normally expected to perform an `await` at some point, but yours doesn't. So what happens is that the function does all its work immediately and returns an already-completed task. For `Main`, this doesn't really matter. You can just remove the `async` keyword from the declaration. – Raymond Chen Mar 27 '21 at 14:40
  • The real challenge is how to refactor this `ContinueWith` galore with proper async/await code! – Theodor Zoulias Mar 27 '21 at 16:17

1 Answers1

6

will it really make any part of the code to run synchronously, just because the lack of an 'await' operator?

Yes. The Main method will run synchronously. This won't really matter because it's the Main method, but if you want to asynchronously wait for the tasks to complete, use await Task.WhenAll instead of Task.WaitAll. The asynchronous approach has an additional benefit in that it doesn't wrap exceptions in AggregateException.

On a side note, use await instead of ContinueWith.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • First of all, thank you Stephen for the answer! But, there's one thing which is a bit confusing to me. If I don't misunderstood, the 2nd answer here states that WaitAll is the correct choice regarding to handling the exceptions. https://stackoverflow.com/questions/6123406/waitall-vs-whenall – NeMi Mar 27 '21 at 15:16
  • @NeMi: Do you really need to know all the exceptions, or is one sufficient? – Stephen Cleary Mar 27 '21 at 20:17
  • It's a great question. I think the answer is "yes", because if I discover from the logs that there was a network error while fetching the API, I'd also like to know if there were any errors with e.g. a file access, so I can resolve those in a single round, rather than having yet another fail run, just to discover any other pending issues. What do you think? – NeMi Mar 28 '21 at 12:36
  • @NeMi: I have never needed to save all exceptions from a `Task.WhenAll`. It's possible to get them all by keeping the task returned from `Task.WhenAll` and examining its `Exception` when it faults; that would be better than `Task.WaitAll`. – Stephen Cleary Mar 28 '21 at 21:52
  • 1
    @NeMi you could take a look at this: [I want await to throw AggregateException, not just the first Exception](https://stackoverflow.com/questions/18314961/i-want-await-to-throw-aggregateexception-not-just-the-first-exception) – Theodor Zoulias Mar 29 '21 at 19:40