3

I'm needing to call a third party async method from a mvc app. The name of this async method is ForceClient.QueryAsync. It is from an open source project: https://github.com/developerforce/Force.com-Toolkit-for-NET/.

Below works fine, the model.Opportunity contains expected info when the process is in the View stage of the mvc:

public async Task<ActionResult> MyController(string Id) {
    . . . .
    MyModel model = new MyModel();
    var client = new ForceClient(instanceUrl, accessToken, apiVersion);
    var qry = await client.QueryAsync<MyModel.SFOpportunity>(
            "SELECT Name, StageName FROM Opportunity where  Id='" + Id + "'");
    model.Opportunity = qry.Records.FirstOrDefault();
    . . . .
return View(viewName, myModel);
}

But below does not work. The model.Opportunity is null when the process is in the View stage. I did some debugging and see that the flow goes like this:

1) Step1

2) Step2

3) In the View stage. At this point the model.Opportunity is null, which I need it to be populated.

4) Step3.

public async Task<ActionResult> MyController(string Id) {
    . . . .
    MyModel myModel = await Task.Run(() =>
        {
            var result = new MyModel(Id);
            return result;

        });   // =====> Step 1
    . . . .
return View(viewName, myInfoView);
}    

public class MyModel
{
    public SFOpportunity Opportunity { get; set; }
    public MyModel(string id)
    {
        setOpportunityAsync(id);
    }

    private async void setOpportunityAsync(string id)
    {
        . . .
        var client = new ForceClient(instanceUrl, accessToken, apiVersion);
        var qry = await client.QueryAsync<MyModel.SFOpportunity>(
             "SELECT Name, StageName FROM Opportunity where  Id='" + id + "'");  // ======>  Step2 
        Opportunity = qry.Records.FirstOrDefault();  // =====> step3
    }

So, my question is what do I need to do to get it to execute the steps in the following sequence: 1) Step1

2) Step2

3) Step3

4) In the View stage. At this point the model.Opportunity is should be populated.

Scott Hannen
  • 27,588
  • 3
  • 45
  • 62
HockChai Lim
  • 1,675
  • 2
  • 20
  • 30

2 Answers2

8

You cannot have async constructors.

One alternative is to have async factory methods:

public class MyModel
{
  public SFOpportunity Opportunity { get; set; }
  private MyModel() { }

  public static async Task<MyModel> CreateAsync(string id)
  {
    var result = new MyModel();
    await result.setOpportunityAsync(id);
    return result;
  }

  private async Task setOpportunityAsync(string id)
  {
    ...
  }
}

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • I'm new to C#, especially async. But the way that the async work seems problematic to me. So, let's assume that I've an existing app where the database and business logic layers are buried deep down in the app and none of them are currently dealing with any async call. If a new requirement comes in and I need to make a change to a method in these layers to now make an async web api call, is that means that every method in the call chain above this method will need to be changed to deal with async/await?Would synchronous async call be the solution to this scenario? – HockChai Lim May 22 '19 at 20:07
  • @HockChaiLim: Every method in the call chain should be `async`. Any other solution is problematic. – Stephen Cleary May 23 '19 at 00:56
2

The constructor for MyModel does not (and can not) await setOpportunityAsync because the constructor itself isn't (and can't be) asynchronous. Otherwise you would be able to await the call to the constructor itself, but you can't. So the async method likely won't be finished executing right after the constructor is called. It will be finished... whenever it's finished.

Here's a smaller test class to illustrate the behavior:

public class HasConstructorWithAsyncCall
{
    public HasConstructorWithAsyncCall()
    {
        MarkConstructorFinishedAsync();
    }

    public bool ConstructorHasFinished { get; private set; }

    async void MarkConstructorFinishedAsync()
    {
        await Task.Delay(500);
        ConstructorHasFinished = true;
    }
}

What is the value of ConstructorHasFinished immediately after an instance is constructed? Here's a unit test:

[TestMethod]
public void TestWhenConstructorFinishes()
{
    var subject = new HasConstructorWithAsyncCall();
    Assert.IsFalse(subject.ConstructorHasFinished);
    Thread.Sleep(600);
    Assert.IsTrue(subject.ConstructorHasFinished);
}

The test passes. The constructor returns while MarkConstructorFinishedAsync hasn't completed, so ConstructorHasFinished is false. Half a second later it finishes, and the value is true.

You can't mark a constructor async so you can't await anything in the constructor.

In general we wouldn't put anything long-running like data retrieval in a constructor, including anything we would call asynchronously. If we do then we must either call it synchronously or know that the completion of the constructor doesn't mean that it's completely "constructed."

Scott Hannen
  • 27,588
  • 3
  • 45
  • 62
  • Yes, if I change my code to call it synchronously, it works fine. I was trying to avoid synchronous call on a async method. Is there any down side to that? I myself do not think a data retrieval should be considered as long-running process. Most transactions base web-site requires accessing data in database all the time. With could services on the rise, a lot of those sites now a day require performing web api call to apply business logic that is required to construct the page. – HockChai Lim May 20 '19 at 20:13