Matthew
Matthew

Reputation: 195

How to execute multiple parallel tasks on completion of a prior task

I have a situation where I need to call a web service and, on successful completion, do multiple things with the results returned from the web service. I have developed code that "works" -- just not as I intended. Specifically, I want to take the results from the call to the web service and pass those results onto multiple successive tasks that are to execute in parallel, but what I have at the moment executes the first successive task before starting the second.

I've put together a much simplified example of what I'm currently doing that'll hopefully help illustrate this situation. First, the implementation:

public interface IConfigurationSettings
{
    int? ConfigurationSetting { get; set; }
}

public interface IPrintCommandHandler
{
    System.Threading.Tasks.Task<bool> ExecuteAsync(byte[] reportContent);
}

public interface ISaveCommandHandler
{
    System.Threading.Tasks.Task<bool> ExecuteAsync(byte[] reportContent);
}

public interface IWebService
{
    System.Threading.Tasks.Task<object> RetrieveReportAsync(string searchToken, string reportFormat);
}

public class ReportCommandHandler
{
    private readonly IConfigurationSettings _configurationSettings;
    private readonly IPrintCommandHandler _printCommandHandler;
    private readonly ISaveCommandHandler _saveCommandHandler;
    private readonly IWebService _webService;

    public ReportCommandHandler(IWebService webService, IPrintCommandHandler printCommandHandler, ISaveCommandHandler saveCommandHandler, IConfigurationSettings configurationSettings)
    {
        _webService = webService;
        _printCommandHandler = printCommandHandler;
        _saveCommandHandler = saveCommandHandler;
        _configurationSettings = configurationSettings;
    }

    public async Task<bool> ExecuteAsync(string searchToken)
    {
        var reportTask = _webService.RetrieveReportAsync(searchToken, "PDF");
        var nextStepTasks = new List<Task<bool>>();

        // Run "print" task after report task.
        var printTask = await reportTask.ContinueWith(task => _printCommandHandler.ExecuteAsync((byte[]) task.Result));
        nextStepTasks.Add(printTask);

        // Run "save" task after report task.
        if (_configurationSettings.ConfigurationSetting.HasValue)
        {
            var saveTask = await reportTask.ContinueWith(task => _saveCommandHandler.ExecuteAsync((byte[]) task.Result));
            nextStepTasks.Add(saveTask);
        }

        var reportTaskResult = await Task.WhenAll(nextStepTasks);
        return reportTaskResult.Aggregate(true, (current, result) => current & result);
    }
}

So, the web service (third party, nothing to do with me) has an endpoint for doing a search/lookup that, if successful, returns a reference number (I've called it a search token in my example). This reference number is then used to retrieve the results of the lookup (using a different endpoint) in any of several different formats.

The IWebService interface in this example is representative of an application service I created to manage interaction with the web service. The actual implementation has other methods on it for doing a lookup, ping, etc.

Just to make things more interesting, one of the successive tasks is required (will always execute after the primary task) but the other successive task is optional, execution subject to a configuration setting set elsewhere in the application.

To more easily demonstrate the issue, I created a unit test:

public class RhinoMockRepository : IDisposable
{
    private readonly ArrayList _mockObjectRepository;

    public RhinoMockRepository()
    {
        _mockObjectRepository = new ArrayList();
    }

    public T CreateMock<T>() where T : class
    {
        var mock = MockRepository.GenerateMock<T>();
        _mockObjectRepository.Add(mock);
        return mock;
    }

    public T CreateStub<T>() where T : class
    {
        return MockRepository.GenerateStub<T>();
    }

    public void Dispose()
    {
        foreach (var obj in _mockObjectRepository) obj.VerifyAllExpectations();
        _mockObjectRepository.Clear();
    }
}

[TestFixture]
public class TapTest
{
    private const string SearchToken = "F71C8B50-ECD1-4C02-AD3F-6C24F1AF3D9A";

    [Test]
    public void ReportCommandExecutesPrintAndSave()
    {
        using (var repository = new RhinoMockRepository())
        {
            // Arrange
            const string reportContent = "This is a PDF file.";
            var reportContentBytes = System.Text.Encoding.Default.GetBytes(reportContent);

            var retrieveReportResult = System.Threading.Tasks.Task.FromResult<object>(reportContentBytes);
            var webServiceMock = repository.CreateMock<IWebService>();
            webServiceMock.Stub(x => x.RetrieveReportAsync(SearchToken, "PDF")).Return(retrieveReportResult);

            var printCommandHandlerMock = repository.CreateMock<IPrintCommandHandler>();
            var printResult = System.Threading.Tasks.Task.FromResult(true);
            printCommandHandlerMock
                .Expect(x => x.ExecuteAsync(reportContentBytes))
                //.WhenCalled(method => System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2)))
                .Return(printResult);

            var configurationSettingsStub = repository.CreateStub<IConfigurationSettings>();
            configurationSettingsStub.ConfigurationSetting = 10;

            var saveCommandHandlerMock = repository.CreateMock<ISaveCommandHandler>();
            var saveResult = System.Threading.Tasks.Task.FromResult(true);
            saveCommandHandlerMock.Expect(x => x.ExecuteAsync(reportContentBytes)).Return(saveResult);

            // Act
            var reportCommandHandler = new ReportCommandHandler(webServiceMock, printCommandHandlerMock, saveCommandHandlerMock, configurationSettingsStub);
            var result = System.Threading.Tasks.Task
                .Run(async () => await reportCommandHandler.ExecuteAsync(SearchToken))
                .Result;

            // Assert
            Assert.That(result, Is.True);
        }
    }
}

Ideally, on completion of the call to RetrieveReportAsync() on IWebService both the "print" and "save" command handlers should be executed simultaneously, having received a copy of the results from RetrieveReportAsync(). However, if the call to WhenCalled... in the unit test is uncommented, and on stepping through the implementation of ReportCommandHandler.ExecuteAsync(), you can see that the "print" command executes and completes before it gets to the "save" command. Now, I am aware that the whole point of await is to suspend execution of the calling async method until the awaited code completes, but it isn't clear to me how to instantiate both the "print" and "save" commands (tasks) as continuations of the "report" task such that they both execute in parallel when the "report" task completes, and the "report" command is then able to return a result that is based on the results from both the "print" and "save" commands (tasks).

Upvotes: 1

Views: 508

Answers (1)

Peter Duniho
Peter Duniho

Reputation: 70652

Your question really involves addressing two different goals:

  1. How to wait for a task?
  2. How to execute two other tasks concurrently?

I find the mixing of await and ContinueWith() in your code confusing. It's not clear to me why you did that. One of the key things await does for you is to automatically set up a continuation, so you don't have to call ContinueWith() explicitly. Yet, you do anyway.

On the assumption that's simply a mistake, out of lack of full understanding of how to accomplish your goal, here's how I'd have written your method:

public async Task<bool> ExecuteAsync(string searchToken)
{
    var reportTaskResult = await _webService.RetrieveReportAsync(searchToken, "PDF");
    var nextStepTasks = new List<Task<bool>>();

    // Run "print" task after report task.
    var printTask = _printCommandHandler.ExecuteAsync((byte[]) reportTaskResult);
    nextStepTasks.Add(printTask);

    // Run "save" task after report task.
    if (_configurationSettings.ConfigurationSetting.HasValue)
    {
        var saveTask = _saveCommandHandler.ExecuteAsync((byte[]) reportTaskResult);
        nextStepTasks.Add(saveTask);
    }

    var reportTaskResult = await Task.WhenAll(nextStepTasks);
    return reportTaskResult.Aggregate(false, (current, result) => current | result);
}

In other words, do await the original task first. Then you know it's done and have its result. At that time, go ahead and start the other tasks, adding their Task object to your list, but not awaiting each one individually. Finally, await the entire list of tasks.

Upvotes: 2

Related Questions