5

I've been struggling with async / await for a week now. Some background: the code below is part of an MVC4 website project. The website has a large number of API calls happening. The goal is to get those API calls happening in parallel instead of synchronous to improve site responsiveness. Right now all the API calls block each other. So if one page requires 4 calls...longer load time.

I've built out individual methods for both the synchronous and async versions of all the API calls. The problem I have is the await never responds. I think it's related to this question. However, I'm really not sure how to solve it. I've tried the ConfigureAwait(false), and that hasn't helped me.

Here's the code:

The initial call in the controller it looks like so:

BaseData bdata = API.GetBaseData().Result;

I'd love to use await here, but that's not an option without an AsyncController, which we can't use due to needing request / response access. The other methods are in the API class:

internal static async Task<BaseData> GetBaseData() {
    var catTask = GetParentCategoriesAsync();
    var yearTask = GetYearsAsync();

    await Task.WhenAll(new Task[] { catTask, yearTask });

    var bdata = new BaseData {
        years = await yearTask,
        cats = await catTask
    };
    return bdata;
}

internal static async Task<List<APICategory>> GetParentCategoriesAsync() {
    try {
        WebClient wc = new WebClient();
        wc.Proxy = null;

        string url = getAPIPath();
        url += "GetFullParentCategories";
        url += "?dataType=JSON";

        Uri targeturi = new Uri(url);
        List<APICategory> cats = new List<APICategory>();

        var cat_json = await wc.DownloadStringTaskAsync(targeturi);
        cats = JsonConvert.DeserializeObject<List<APICategory>>(cat_json);

        return cats;
    } catch (Exception) {
        return new List<APICategory>();
    }
}

internal static async Task<List<double>> GetYearsAsync() {
    WebClient wc = new WebClient();
    wc.Proxy = null;
    Uri targeturi = new Uri(getAPIPath() + "getyear?dataType=JSON");
    var year_json = await wc.DownloadStringTaskAsync(targeturi);
    List<double> years = JsonConvert.DeserializeObject<List<double>>(year_json);
    return years;
}

When these methods are called, I can put breakpoints in GetYearsAsync() and GetParentCategoriesAsync(). Everything fires until the await wc.DownloadStringTaskAsync(targeturi) command. That's where stops.

I've added ConfigureAwait(continueOnCapturedContext: false) to all the tasks, but that hasn't helped. I'm assuming the problem is that the threads are not in the same context. However, I'm not certain. I am certain, however, that I'm doing something wrong. I'm just not sure what. It's either that or I'm just trying to do something that can't be done with .NET MVC4. Any thoughts would be supremely appreciated.

11
  • 5
    If you can't "async all the way up" then there's not really much point of using async methods at all. If you're going to be blocking on the task at the top level you're still keeping a thread pool thread busy for the entire duration of your operation. You may as well just use non-async methods throughout. Commented Jun 7, 2013 at 17:11
  • If you're performing long-running operations in the background, you need a service, something that lives beyond the HTTP request. Commented Jun 7, 2013 at 17:11
  • 3
    "which we can't use due to needing request / response access" - not sure what you mean by this? async code can access the request and response just fine. Commented Jun 7, 2013 at 17:12
  • 3
    Task.Result when asynchrony is involved will almost always deadlock. Commented Jun 7, 2013 at 17:15
  • As Servy pointed out, there's no benefit to async if you're just going to block a thread by calling Result. That said, ConfigureAwait should work if you are awaiting the result of ConfigureAwait instead of the task directly, and if you have targetFramework or UseTaskFriendlySynchronizationContext set appropriately. Commented Jun 7, 2013 at 17:15

2 Answers 2

2

The problem is actually due to WebClient, which will always sync back to the request context (which is blocked due to the Result call).

You can use HttpClient instead, combined with ConfigureAwait(false):

internal static async Task<BaseData> GetBaseDataAsync() {
  var catTask = GetParentCategoriesAsync();
  var yearTask = GetYearsAsync();

  await Task.WhenAll(catTask, yearTask).ConfigureAwait(false);

  var bdata = new BaseData {
    years = await yearTask,
    cats = await catTask
  };
  return bdata;
}

internal static async Task<List<APICategory>> GetParentCategoriesAsync() {
  try {
    var client = new HttpClient();

    string url = getAPIPath();
    url += "GetFullParentCategories";
    url += "?dataType=JSON";

    Uri targeturi = new Uri(url);
    List<APICategory> cats = new List<APICategory>();

    var cat_json = await client.GetStringAsync(targeturi).ConfigureAwait(false);
    cats = JsonConvert.DeserializeObject<List<APICategory>>(cat_json);

    return cats;
  } catch (Exception) {
    return new List<APICategory>();
  }
}

internal static async Task<List<double>> GetYearsAsync() {
  var client = new HttpClient();
  Uri targeturi = new Uri(getAPIPath() + "getyear?dataType=JSON");
  var year_json = await client.GetStringAsync(targeturi).ConfigureAwait(false);
  List<double> years = JsonConvert.DeserializeObject<List<double>>(year_json);
  return years;
}

That should enable you to call it as such:

BaseData bdata = API.GetBaseDataAsync().Result;

However, I strongly recommend that you call it as such:

BaseData bdata = await API.GetBaseDataAsync();

You'll find that the code both before and after the await can access the request and response context just fine.

Sign up to request clarification or add additional context in comments.

Comments

0

I ended up following a combination of Servy's and Stephen Cleary's advice. Based on Stephen's response, I realized I could access the request/response as long as I did it before I made any asynchronous calls in the controller.

If I stored the HttpContext in a variable, I could pass that context around to any model/service method that needed access to it. This allowed me to just go async all the way up like Servy suggested. After that, I had no issues with anything I wanted with the async patterns.

3 Comments

Passing HttpContext in a variable to other threads that shouldn't have access to it is not recommended, to say the least. Just use async and await (without ConfigureAwait) and you will be able to access HttpContext.Current just fine both before and after the await.
The big problem that I was running into initially with just plain old async / await was that HttpContext.Current was empty in helper methods that needed to get and set cookie data. With just async and await, any method call made would throw null reference errors when trying to access HttpContext.Current. By storing HttpContext.Current in a variable, I can pass it around much like I've seen modeled in other languages. Without storing it as a variable, the cookie logic wouldn't work at all. I guess there has to be something I'm still missing then.
That is a different problem then. I recommend posting a minimal repro with the question of why HttpContext.Current is null.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.