1

I have a PostAsync method in an internal part of my code that doesn't seem to ever return a response. However, I use it synchronously via .GetAwaiter().GetResult(). The target framework is net45.

public async Task<TResponse> PostAsync<TResponse, TRequest>(string method, TRequest body)
{
    _logger.Log($"Method {method}, body {JsonConvert.SerializeObject(body)} on url {_configuration.ApiUrl}");
    using (var customDelegatingHandler = new HMACDelegatingHandler(_configuration, _apiId))
    {
        using (var client = new HttpClient(customDelegatingHandler))
        {
            var response = await client.PostAsync($"{_configuration.ApiUrl}/{method}",
                new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json"));

            if (response.StatusCode == HttpStatusCode.OK)
            {
                var content = await response.Content.ReadAsStringAsync();

                return JsonConvert.DeserializeObject<TResponse>(content);
            }
            else
            {
                await Log(body, response);
            }
            return default;
        }
    }
}

What I do is call the PostAsync in another method:

public async Task<decimal> GetBalance(Request request)
{
    // = new MyCustomClient...

    QueryFundsResponse response = await customClient.PostAsync<Response, Request>("testAction", request);
    if (response == default)
        return 0.0m;

    return response.Amount;
}

Then, finally at the very top of the flow, I call the GetBalance method like this:

var sw = new Stopwatch();
sw.Start();
var balance = _provider
    .GetBalance(request)
    .ConfigureAwait(false)
    .GetAwaiter()
    .GetResult();
sw.Stop();
_logger.Log($"GetBalance -> method duration: { sw.ElapsedMilliseconds }");

I don't see the log in my logs at all, and I don't seem to ever get a response or any code executed after the .GetAwaiter().GetResult(). Switching the last block of code to be asynchronous and await the GetBalance() method is not really an option for me, sadly.

I am unable to figure out why nothing is changing, even after using the .ConfigureAwait(false) method.

TheDoomDestroyer
  • 2,434
  • 4
  • 24
  • 45
  • 1
    This is a complex issue and the only thing that has any guarantees tied to it is rewriting to async code using async/await. Is this a high-traffic application? What kind of application? Web? Winforms? Console? – Lasse V. Karlsen Feb 11 '22 at 12:21
  • It is a web API in NET4.5. When someone logs in, a getbalance method is called, which internally (as per above) sends a request to a third party system here. – TheDoomDestroyer Feb 11 '22 at 12:25
  • Web API actions can be `async`; why would you think switching to `async` and `await` is not an option? – Richard Deeming Feb 11 '22 at 12:28
  • Long story short, because in order to switch to async-await, we'd have to rewrite the whole system module, as it's closely tied to the database, where everything functions synchronously, because it's legacy code where DB sessions that are opened must remain synchronous. – TheDoomDestroyer Feb 11 '22 at 12:39
  • 1
    Then rewrite the PostAsync to be Post and not use async code in there. – Lasse V. Karlsen Feb 11 '22 at 12:40
  • That would essentially mean HttpClient calls would be `HttpClient.PostAsync().GetAwaiter().GetResult()`, correct? – TheDoomDestroyer Feb 11 '22 at 12:42
  • If HttpClient does not have any non-async methods you have to switch to WebClient I guess. – Lasse V. Karlsen Feb 11 '22 at 12:49
  • The whole code is in a shared library, and the same code is used for NET Standard and NET 4.5, and if I am not mistaken, WebClient isn't supported for NET Standard, nor is it recommended. – TheDoomDestroyer Feb 11 '22 at 12:53
  • 1
    Well, the main problem is that you only have three options: 1. Hope for the best (this failed, you're already here), 2. Write everything async/await, or 3. Write nothing async/await. Option 4, mix and match is, unfortunately, option 1. – Lasse V. Karlsen Feb 11 '22 at 12:54
  • 1
    Can you separate the code so that the entire database code is run first in a synchronous manner, before you start dealing with the api's, so that you can complete everything that has to run on the main thread on the main thread, before you start with the async code that may end up switching to threadpool threads? – Lasse V. Karlsen Feb 11 '22 at 12:57
  • WebClient seems to be supported in .NET Standard 2.0 and 2.1 according to https://learn.microsoft.com/en-us/dotnet/api/system.net.webclient?view=net-6.0#applies-to – Lasse V. Karlsen Feb 11 '22 at 12:58

1 Answers1

3

You're experiencing the common deadlock that happens when you block on asynchronous code (described in detail on my blog). There are a variety of ways to get around it, but they're all hacks and none of them work in every situation.

In your case, I'd say either use the direct blocking hack or use the boolean argument hack.

The direct blocking hack requires you to use ConfigureAwait(false) everywhere. Note that your current code only uses ConfigureAwait(false) where it doesn't do anything; ConfigureAwait configures the await, so it needs to go where the awaits are. All of them.

The boolean argument hack means that your code will take a bool parameter that determines whether it executes synchronously or asynchronously. Note that HttpClient (for now) has an async-only API, so your custom delegating handler will need to support direct blocking, using ConfigureAwait(false). Similarly, Log will either need a synchronous equivalent or also support direct blocking. Your code would end up looking something like this:

public Task<TResponse> PostAsync<TResponse, TRequest>(string method, TRequest body) => PostCoreAsync(method, body, sync: false);
public TResponse Post<TResponse, TRequest>(string method, TRequest body) => PostCoreAsync(method, body, sync: true).GetAwaiter().GetResult();
private async Task<TResponse> PostCoreAsync<TResponse, TRequest>(string method, TRequest body, bool sync)
{
  _logger.Log($"Method {method}, body {JsonConvert.SerializeObject(body)} on url {_configuration.ApiUrl}");
  using (var customDelegatingHandler = new HMACDelegatingHandler(_configuration, _apiId))
  {
    using (var client = new HttpClient(customDelegatingHandler))
    {
      var responseTask = client.PostAsync($"{_configuration.ApiUrl}/{method}",
         new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json"));
      var response = sync ? responseTask.GetAwaiter().GetResult() : await responseTask;

      if (response.StatusCode == HttpStatusCode.OK)
      {
        var content = sync ? response.Content.ReadAsStringAsync().GetAwaiter().GetResult() : await response.Content.ReadAsStringAsync();

        return JsonConvert.DeserializeObject<TResponse>(content);
      }
      else
      {
        var logTask = Log(body, response);
        if (sync)
          logTask.GetAwaiter().GetResult();
        else
          await logTask;
      }
      return default;
    }
  }
}

public Task<decimal> GetBalanceAsync(Request request) => GetBalanceCoreAsync(request, sync: false);
public decimal GetBalance(Request request) => GetBalanceCoreAsync(request, sync: true).GetAwaiter().GetResult();
private async Task<decimal> GetBalanceCoreAsync(Request request, bool sync)
{
    // = new MyCustomClient...

    QueryFundsResponse response = sync ?
        customClient.Post<Response, Request>("testAction", request) :
        await customClient.PostAsync<Response, Request>("testAction", request);
    if (response == default)
        return 0.0m;

    return response.Amount;
}

var sw = new Stopwatch();
sw.Start();
var balance = _provider
    .GetBalance(request);
sw.Stop();
_logger.Log($"GetBalance -> method duration: { sw.ElapsedMilliseconds }");
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810