How can I use async/await to call a webservice?
Asked Answered
I

5

31

I have a webservice written in Yii (php framework).

I use C# and Visual Studio 2012 to develop a WP8 application. I added a service reference to my project (Add Service Reference). So I am able to use webservice functions.

   client = new YChatWebService.WebServiceControllerPortTypeClient();

   client.loginCompleted += client_loginCompleted;   // this.token = e.Result;
   client.loginAsync(this.username, this.password); 

   client.getTestCompleted += client_getTestCompleted;
   client.getTestAsync(this.token); 

function getTestAsync and loginAsync return void and both are asynchronous. Is it possible for the functions to return Task<T>? I would like to use async/await keywords in my program.

Ironic answered 31/12, 2012 at 15:21 Comment(3)
Just change the void return type to Task, then you can call await client.loginAsync(this.username, this.password);Dickens
@NickBray And then it won't work because you won't be actually returning a task that is completed at the proper time...Lomalomas
msdn.microsoft.com/en-us/library/hh524395.aspxDickens
A
35

Assuming that loginAsync returns void, and loginCmpleted event fires when login is done, this is called the Event-based Asynchronous Pattern, or EAP.

To convert EAP to await/async, consult Tasks and the Event-based Asynchronous Pattern. In particular, you'll want to make use of the TaskCompletionSource to convert the event-based model to a Task-based model. Once you've got a Task-based model, you can use C# 5's sexy await feature.

Here's an example:

// Use LoginCompletedEventArgs, or whatever type you need out of the .loginCompleted event
// This is an extension method, and needs to be placed in a static class.
public static Task<LoginCompletedEventArgs> LoginAsyncTask(this YChatWebService.WebServiceControllerPortTypeClient client, string userName, string password) 
{ 
    var tcs = CreateSource<LoginCompletedEventArgs>(null); 
    client.loginCompleted += (sender, e) => TransferCompletion(tcs, e, () => e, null); 
    client.loginAsync(userName, password);
    return tcs.Task; 
}

private static TaskCompletionSource<T> CreateSource<T>(object state) 
{ 
    return new TaskCompletionSource<T>( 
        state, TaskCreationOptions.None); 
}

private static void TransferCompletion<T>( 
    TaskCompletionSource<T> tcs, AsyncCompletedEventArgs e, 
    Func<T> getResult, Action unregisterHandler) 
{ 
    if (e.UserState == tcs) 
    { 
        if (e.Cancelled) tcs.TrySetCanceled(); 
        else if (e.Error != null) tcs.TrySetException(e.Error); 
        else tcs.TrySetResult(getResult()); 
        if (unregisterHandler != null) unregisterHandler();
    } 
}

Now that you've converted the Event-based async programming model to a Task-based one, you can now use await:

var client = new YChatWebService.WebServiceControllerPortTypeClient();
var login = await client.LoginAsyncTask("myUserName", "myPassword");
Amabil answered 31/12, 2012 at 15:42 Comment(6)
Thank you.I had to get familiar with terms such as lambda expressions, delegates..." So everything is very new for me. The compiler has a problem with "e => e" so I changed it to "() => e" and there is no option TaskCreationOptions.DetachedFromParent. What should I use instead?Ironic
TaskCreateOptions.None should be OK here. Yes, my mistake, it should be () => e, or () => e.Foo, whatever property you want to pull off there. I'll update the code.Amabil
This code won't work since user state will be null. The check in TransferCompletion (e.UserState == tcs) will always be false. Not sure what the best approach is here, but if tcs is passed as a third argument to loginAsync, it appears to work as expected.Humorous
@user3533716: That looks reasonable to me. I believe Judah's intent was trying to ensure loginCompleted would properly synchronize calls and responses in the case that LoginAsyncTask was called more than once on the same YChatWebService.WebServiceControllerPortTypeClient. It's simpler to guarantee one call per client by instantiating the client inside LoginAsyncTask, but Judah's approach allows the caller to construct a custom instance of YChatWebService.WebServiceControllerPortTypeClient (e.g., by dynamically modifying the URL).Belk
@JudahGabrielHimango - The link you posted is dead.Vineyard
Considering this was posted 8 years ago, that link lasted fairly long! I've updated the post with the correct link.Amabil
P
8

While adding your service reference make sure you selected Generate Task based operations in Advanced section. this will create awaitable methods like LoginAsync returning Task<string>

Paedogenesis answered 31/12, 2012 at 15:28 Comment(3)
This is using an event based model, so FromAsync won't help.Lomalomas
@Lomalomas I used the url in the question and generated the awaitable functions automatically. Anything wrong?Paedogenesis
I am unable to select Generate Task based operations because it is greyed out. It seems that it is disabled for WP8 projects. See this topicIronic
X
8

I've had to do this a couple of times over the last year and I've used both @Judah's code above and the original example he has referenced but each time I've hit on the following problem with both: the async call works but doesn't complete. If I step through it I can see that it will enter the TransferCompletion method but the e.UserState == tcs will always be false.

It turns out that web service async methods like the OP's loginAsync have two signatures. The second accepts a userState parameter. The solution is to pass the TaskCompletionSource<T> object you created as this parameter. This way the e.UserState == tcs will return true.

In the OP, the e.UserState == tcs was removed to make the code work which is understandable - I was tempted too. But I believe this is there to ensure the correct event is completed.

The full code is:

public static Task<LoginCompletedEventArgs> RaiseInvoiceAsync(this Client client, string userName, string password)
{
    var tcs = CreateSource<LoginCompletedEventArgs>();
    LoginCompletedEventHandler handler = null;
    handler = (sender, e) => TransferCompletion(tcs, e, () => e, () => client.LoginCompleted -= handler);
    client.LoginCompleted += handler;

    try
    {
        client.LoginAsync(userName, password, tcs);
    }
    catch
    {
        client.LoginCompleted -= handler;
        tcs.TrySetCanceled();
        throw;
    }

    return tcs.Task;
}

Alternatively, I believe there is a tcs.Task.AsyncState property too that will provide the userState. So you could do something like:

if (e.UserState == taskCompletionSource || e.UserState == taskCompletionSource?.Task.AsyncState)
{
    if (e.Cancelled) taskCompletionSource.TrySetCanceled();
    else if (e.Error != null) taskCompletionSource.TrySetException(e.Error);
    else taskCompletionSource.TrySetResult(getResult());
    unregisterHandler();
}

This was what I tried initially as it seemed a lighter approach and I could pass a Guid rather than the full TaskCompletionSource object. Stephen Cleary has a good write-up of the AsyncState if you're interested.

Xiaoximena answered 9/12, 2016 at 17:2 Comment(1)
Your advice to pass along the tcs into the async call was missing link that got this working perfectly for me. Thank you!!Antemundane
B
0

(Copied from OP, per https://meta.stackexchange.com/a/150228/136378 )

Answer:

Following code seems to work.

internal static class Extension
{
    private static void TransferCompletion<T>(
        TaskCompletionSource<T> tcs, System.ComponentModel.AsyncCompletedEventArgs e, 
        Func<T> getResult)
    {
        if (e.Error != null)
        {
            tcs.TrySetException(e.Error);
        }
        else if (e.Cancelled)
        {
            tcs.TrySetCanceled();
        }
        else
        {
            tcs.TrySetResult(getResult());
        }
    }

    public static Task<loginCompletedEventArgs> LoginAsyncTask(
        this YChatWebService.WebServiceControllerPortTypeClient client,
        string userName, string password)
    {
        var tcs = new TaskCompletionSource<loginCompletedEventArgs>();
        client.loginCompleted += (s, e) => TransferCompletion(tcs, e, () => e);
        client.loginAsync(userName, password);
        return tcs.Task;
    }
}

I call it this way

client = new YChatWebService.WebServiceControllerPortTypeClient();
var login = await client.LoginAsyncTask(this.username, this.password);
Belk answered 31/12, 2012 at 15:21 Comment(1)
This code may function incorrectly if a single client instance is used to make multiple simultaneous calls to LoginAsyncTask. The loginCompleted event will fire all subscribed events at once on each completion. This can be resolved either by using a separate client for each call (using an extension method on client is a bad idea, in this approach). It can also be resolved by collating the callbacks with the calls via the userstate parameter (this is the approach which https://mcmap.net/q/463446/-how-can-i-use-async-await-to-call-a-webservice tries to use).Belk
D
-4

If you want to be able to await the methods, they should return Task. You cannot await a method that returns void. If you want them to return a value, like int they should return Task<int> then the method should return int.

public async Task loginAsync(string username, string password) {}

Then you can call

Task t = loginAsync(username, password);
//login executing
//do something while waiting

await t; //wait for login to complete
Dickens answered 31/12, 2012 at 15:44 Comment(3)
He's asking how to do that. He doesn't know how an event based model can return a task.Lomalomas
Take the same method you had and just put Task instead of void. Then you can call await in your program. No extra work needed. How to use it is a different story.Dickens
You can't just change the return type of the method and be done. The question is specifically asking how to change the implementation of the method so that it actually returns a task. The question is asking how you create a Task that is completed when the data is ready, and you don't answer that. See Judah's answer for how you actually implement it.Lomalomas

© 2022 - 2024 — McMap. All rights reserved.