2

I am new to TAP and async/await in practice in C# so I may have some bad code smells here, so be gentle. :-)

I have a service method that looks as follows:

public OzCpAddOrUpdateEmailAddressToListOutput AddOrUpdateEmailAddressToList(
    OzCpAddOrUpdateEmailAddressToListInput aParams)
{
    var result = new OzCpAddOrUpdateEmailAddressToListOutput();
    try
    {
        var mailChimManager = new MailChimpManager(aParams.MailChimpApiKey);
        Task<Member> mailChimpResult =
            mailChimManager.Members.AddOrUpdateAsync(
                aParams.Listid, 
                new Member                                                                                                     
                {
                    EmailAddress = aParams.EmailAddress
                });

        //Poll async task until it completes. 
        //Give it at most 8 seconds to do what it needs to do
        var outOfTime = DateTime.Now.AddSeconds(8);
        while (!mailChimpResult.IsCompleted)
        {
            if (DateTime.Now > outOfTime)
            {
                throw new Exception("Timed out waiting for MailChimp API.");
            }
        }

        //Should there have been a problem with the call then we raise an exception
        if (mailChimpResult.IsFaulted)
        {
            throw new Exception(
                mailChimpResult.Exception?.Message ?? 
                "Unknown mail chimp library error.", 
                mailChimpResult.Exception);
        }
        else
        {
            //Call to api returned without failing but unless we have 
            //the email address subscribed we have an issue
            if (mailChimpResult.Result.Status != Status.Subscribed)
            {
                throw new Exception(
                    $"There was a problem subscribing the email address
                    {aParams.EmailAddress} to the mailchimp list id 
                    {aParams.Listid}");
            }
        }
    }
    catch (Exception ex)
    {
        result.ResultErrors.AddFatalError(PlatformErrors.UNKNOWN, ex.Message);
    }
    return result;
}

But when I call in from MVC Controller action mailChimpResult.IsCompleted always returns false and eventually I hit the timeout.

I realise this is because I am not chaining the async calls as per HttpClient IsComplete always return false and because of different threads this behaviour is "expected".

However I want my service method to hide the complexity of the async nature of what it is doing and merely do what appears to be a synchronous call in my action method namely:

 var mailChimpResult =
    _PlatformMailChimpService.AddOrUpdateEmailAddressToList(
        new OzCpAddOrUpdateEmailAddressToListInput                                                                                                                                            
        {
            EmailAddress = aFormCollection["aEmailAddress"],                                                                                                                                           
            Listid = ApplicationSettings.Newsletter.MailChimpListId.Value,                                                                                                                                          
            MailChimpApiKey = ApplicationSettings.Newsletter.MailChimpApiKey.Value
        });

 if (mailChimpResult.Result == true)
 {
    //So something
 }
Community
  • 1
  • 1
TheEdge
  • 9,291
  • 15
  • 67
  • 135
  • 1
    You need to do `async` all the way. "Hiding" it will always lead to painful discoveries - usually [deadlock](http://stackoverflow.com/questions/13140523/await-vs-task-wait-deadlock). There is essentially no way around it. – Alexei Levenkov May 24 '16 at 03:27
  • @AlexeiLevenkov but as my service call returns no async handle etc. how am I supposed to implement this. Can you give me an example based on my code? – TheEdge May 24 '16 at 03:44
  • 1
    It is in the service call that you are hiding it. You need to `await` the call to `AddOrUpdateAsync`, rather than polling it - that is the big code smell. Then you can make the service method async and await that in the controller. – Rhumborl May 24 '16 at 11:56

1 Answers1

1

Ideally you should avoid the .Result and .IsFaulted properties of the Task and Task<T> objects, that was code smell number one. When you're using these objects you should use async and await through the entire stack. Consider your service written this way instead:

public async Task<OzCpAddOrUpdateEmailAddressToListOutput> 
    AddOrUpdateEmailAddressToList(
        OzCpAddOrUpdateEmailAddressToListInput aParams)
{
    var result = new OzCpAddOrUpdateEmailAddressToListOutput();
    try
    {
        var mailChimManager = new MailChimpManager(aParams.MailChimpApiKey);
        Member mailChimpResult =
            await mailChimManager.Members.AddOrUpdateAsync(
                aParams.Listid, 
                new Member                                                                                                     
                {
                    EmailAddress = aParams.EmailAddress
                });
    }
    catch (Exception ex)
    {
        result.ResultErrors.AddFatalError(PlatformErrors.UNKNOWN, ex.Message);
    }
    return result;
}

Notice that I was able to remove all of that unnecessary polling and examining of properties. We mark the method as Task<OzCpAddOrUpdateEmailAddressToListOutput> returning and decorate it with the async keyword. This allows us to use the await keyword in the method body. We await the .AddOrUpdateAsync which yields the Member.

The consuming call into the service looks similar, following the same paradigm of async and await keywords with Task or Task<T> return types:

var mailChimpResult =
    await _PlatformMailChimpService.AddOrUpdateEmailAddressToList(
        new OzCpAddOrUpdateEmailAddressToListInput                                                                                                                                            
        {
            EmailAddress = aFormCollection["aEmailAddress"],                                                                                                                                           
            Listid = ApplicationSettings.Newsletter.MailChimpListId.Value,                                                                                                                                          
            MailChimpApiKey = ApplicationSettings.Newsletter.MailChimpApiKey.Value
        });

 if (mailChimpResult.Result == true)
 {
    //So something
 }

It is considered best practice to postfix the "Async" word to the method, to signify that it is asynchronous, i.e.; AddOrUpdateEmailAddressToListAsync.

David Pine
  • 23,787
  • 10
  • 79
  • 107
  • And the controller method then just becomes `var mailChimpResult = await _PlatformMailChimpService.AddOrUpdateEmailAddressToList(...); if (mailChimpResult.Result == true){}` so still hiding the complexity, apart from the addition of one word – Rhumborl May 24 '16 at 12:00
  • @David thanks for that. I had refactored before I saw your reply. Only further addition was to make the controller action async as well. – TheEdge May 25 '16 at 05:01