Search code examples
c#.nettask-parallel-libraryasync-awaitconvenience-methods

Converting convenience methods that use Tasks


I often write code that has convenience methods which basically wrap other methods. Here's a simple example:

public class WithoutAsync
{
    public static ReadOnlyCollection<Response> GetResponses(IEnumerable<Request> fromRequests)
    {
        var ret = new List<Response>();

        foreach (Request r in fromRequests)
        {
            ret.Add(new Response());
        }

        return ret.AsReadOnly();
    }

    //convenience method
    public static Response GetResponse(Request fromRequest)
    {
        return GetResponses(new Request[] {fromRequest})[0];
    }
}

Now I want to await long-running operations but I can't quite figure out how to retrofit this methodology for use with TPL:

public class WithAsync
{
    public static async Task<ReadOnlyCollection<Response>> GetResponses(IEnumerable<Request> fromRequests)
    {
        var awaitableResponses = new List<Task<Response>>();

        foreach (Request r in fromRequests)
        {
            awaitableResponses.Add(Task.Run<Response>(async () =>
                {
                    await Task.Delay(10000); //simulate some long running async op.
                    return new Response();
                }));
        }

        return new List<Response>(await Task.WhenAll(awaitableResponses)).AsReadOnly();
    }

    //convenience method
    public static Task<Response> GetResponse(Request fromRequest)
    {
        return GetResponse(new Request[] { fromRequest });
    }
}

The convenience method above obviously won't work because it's trying to return a Task<ReadOnlyCollection<Response>> when it really needs to return a Task<Response>.

This works:

//convenience method
public static Task<Response> GetResponse(Request fromRequest)
{
    return new Task<Response>(new Func<Response>(() => GetResponse(new Request[] { fromRequest }).Result[0]));
}

but it seems really awkward, and more importantly, it blocks on .Result[0] which is potentially on a UI thread.

Is there any good way to accomplish what I'm trying to do?


Solution

  • You're trying to avoid making that "convenience method" async, but there's no reason to do that.

    What you want is to call the other method, wait until there are responses and then get the first and only one. You can do that by making it async and using await:

    async Task<Response> GetResponseAsync(Request fromRequest)
    {
        var responses = await GetResponsesAsync(new[] { fromRequest });
        return responses.Single();
    }
    

    Although a better solution in this specific case is to switch things around and have the single GetResponse actually do that work of a single request, and have the multiple GetRsponses call it instead:

    async Task<ReadOnlyCollection<Response>> GetResponsesAsync(IEnumerable<Request> fromRequests)
    {
        return (await Task.WhenAll(fromRequests.Select(GetResponse))).ToList().AsReadOnly();
    }
    
    async Task<Response> GetResponseAsync(Request fromRequest)
    {
        await Task.Delay(10000); //simulate some long running async op.
        return new Response();
    }
    

    Notes:

    • I know it's an example, but there's probably no reason to use Task.Run instead of a simple async call.
    • The convention is to name async methods with an "Async" suffix (i.e. GetResponseAsync).
    • I've also pluralized the name of the method that returns a collection.