0

Consider this non-async method called Forward which works exactly as expected when called (in C#) with Forward(forwardUrl, content);

    private ActionResult Forward(string forwardUrl, FormUrlEncodedContent content)
    {
        // NOTE - httpClient is a static HttpClient instance

        HttpResponseMessage response;
        switch (Request.RequestType.ToUpperInvariant())
        {
            case "GET":
                response = httpClient.GetAsync(forwardUrl).GetAwaiter().GetResult();
                break;
            case "POST":
                response = httpClient.PostAsync(forwardUrl, content).GetAwaiter().GetResult();
                break;
            default:
                throw new InvalidOperationException($"Unable to forward Request method {Request.RequestType}.");
        }

        if (response.IsSuccessStatusCode)
        {
            return Content(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(), response.Content.Headers.ContentType.ToString());
        }

        return new HttpStatusCodeResult(response.StatusCode, response.ReasonPhrase);
    }

That one works fine, but repeatedly calls GetAwaiter().GetResult() which essentially blocks and makes it syncronous.

Now I have the following async method called ForwardAsync and here I call it with exactly the same arguments, but with the calling pattern of ForwardAsync(forwardUrl, content).GetAwaiter().GetResult();

    private async Task<ActionResult> ForwardAsync(string forwardUrl, FormUrlEncodedContent content)
    {
        HttpResponseMessage response;
        switch (Request.RequestType.ToUpperInvariant())
        {
            case "GET":
                response = await httpClient.GetAsync(forwardUrl);
                break;
            case "POST":
                response = await httpClient.PostAsync(forwardUrl, content);
                break;
            default:
                throw new InvalidOperationException($"Unable to forward Request method {Request.RequestType}.");
        }

        if (response.IsSuccessStatusCode)
        {
            return Content(await response.Content.ReadAsStringAsync(), response.Content.Headers.ContentType.ToString());
        }

        return new HttpStatusCodeResult(response.StatusCode, response.ReasonPhrase);
    }

This one simply hangs. In my test case I am going down the POST branch (in both scenarios) but the code never returns from await httpClient.PostAsync(forwardUrl, content). I am able to confirm that the request is posted, and the code does run on the forwarded URL, and responds, but the awaited method invocation simply doesn't return.

What am I doing wrong?

Stephan G
  • 3,289
  • 4
  • 30
  • 49
  • can you show the code that calls into `ForwardAsync` – Jonesopolis Nov 19 '20 at 20:25
  • It is in the description - `return ForwardAsync(forwardUrl, content).GetAwaiter().GetResult();` – Stephan G Nov 19 '20 at 20:26
  • 1
    Don't block on `async` methods, period. However, if you really really need to do it (and there is rarely a case you ever need to do this in a normal situation), sacrifice a thread pool thread and offload it, with task run then block that. – TheGeneral Nov 19 '20 at 20:35
  • 2
    You may want to [edit] title of the post as it does not sound like you actually want to properly use async/await in controller, but rather correctly call async methods from synchronous methods (which really not a good idea, but not related to your question) – Alexei Levenkov Nov 19 '20 at 20:37
  • 1
    Please clarify do you call your controller endpoint with `ForwardAsync(forwardUrl, content).GetAwaiter().GetResult()`(within your test) or you do real HTTP call with `HttpClient`? – OlegI Nov 19 '20 at 20:57
  • Also are you sure your endpoint accepts both GET and POST verbs? – OlegI Nov 19 '20 at 21:03
  • HttpClient is for asynchronous use. If you want to synchronously download a string, use `WebClient` – Chris Danna Nov 19 '20 at 22:04

2 Answers2

2

That one works fine, but repeatedly calls GetAwaiter().GetResult() which essentially blocks and makes it syncronous.

Now I have the following async method called ForwardAsync and here I call it with exactly the same arguments, but with the calling pattern of ForwardAsync(forwardUrl, content).GetAwaiter().GetResult();

The new method also blocks and is synchronous. I'm not sure what value you're getting by making this change.

In addition, by blocking on asynchronous code, the new code is susceptible to deadlocks.

I recommend either:

  • Keeping the existing code as-is.
  • Making the new code properly asynchronous. Which means calling it with await and not using GetAwaiter().GetResult().

Don't use Task.Run on ASP.NET. In that environment, the thread pool belongs to ASP.NET, and regularly using Task.Run will throw off the thread pool heuristics.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    Thanks @StephenCleary. I've gone ahead to mark this "The Answer", and yet there remains a paradox. I am working with an old code base. I am adding new functionality. MSDN strongly suggests not using old WebRequest syncronous code when developing new functionality. Also I need async/await anyway because I am using some newer SDKs that require it. So unless I can convince the older-code project management team to upgrade throughout (painful) I am stuck, in an (older) ASP.NET context, using either `.GetAwaiter().GetResult()` or `Task.Run().Result`. – Stephan G Nov 25 '20 at 21:17
  • @StephanG: You may find my [Brownfield Async](https://learn.microsoft.com/en-us/archive/msdn-magazine/2015/july/async-programming-brownfield-async-development) article helpful. My answers (and blog posts) encourage the ideal solutions, but in the Real World sometimes things get messy. In your case, it sounds like keeping the `GetAwaiter().GetResult()` calls throughout may be the best solution. I would be sure to provide an asynchronous API as well (using the Boolean Argument Hack from the article) so that your consumers can adopt `async` when they're ready. – Stephen Cleary Nov 27 '20 at 21:31
  • Thanks @StephenCleary. Nice article. – Stephan G Nov 30 '20 at 16:04
-1

Short Answer - in this scenario use:

return Task.Run(() => ForwardAsync(forwardUrl, content)).Result;

and do NOT use the GetAwaiter().GetResult() pattern.

I found this SO question helpful, and it has the longer and correct answer: HttpClient.GetAsync(...) never returns when using await/async

Many thanks to all who commented on the initial question with clarifying insights.

Stephan G
  • 3,289
  • 4
  • 30
  • 49