HttpClient.GetStringAsync is not executed
Asked Answered
F

6

9

The code runs in .NET Standard 2.0. I have a constructor calling a method which will call an Azure function like this:

public ChatViewModel(IChatService chatService)
{
    Task.Run(async () =>
    {
        if (!chatService.IsConnected)
        {
            await chatService.CreateConnection();
        }
    });
}

The method is like this:

public async Task CreateConnection()
{
   await semaphoreSlim.WaitAsync();

   if (httpClient == null)
   {
        httpClient = new HttpClient();
   }

   var result = await httpClient.GetStringAsync(uri);

   var info = JsonConvert.DeserializeObject<Models.ConnectionInfo>(result);

   //... some other code ...
   semaphoreSlim.Release();
}

The code stops at

await httpClient.GetStringAsync(uri)

The URI is 100% valid, if I copy and paste it in the browser I get the JSON I wanted.

When opening up Fiddler, no call has been made to the URI.

EDIT Source code is coming from this Github repo: https://github.com/PacktPublishing/Xamarin.Forms-Projects/tree/master/Chapter06-07/Chat/Chat

And oddly enough I seem to get a call in Azure: enter image description here

EDIT 2 This code is working:

static void Main(string[] args)
{
    using (var httpClient = new HttpClient())
    {
        var a = httpClient.GetStringAsync(uri).GetAwaiter().GetResult();
    }
}

This code is not working:

static void Main(string[] args)
{
    Task.Run(async() => {
        using (var httpClient = new HttpClient())
        {
            var a = await httpClient.GetStringAsync(uri);
        }
    });
}
Friedafriedberg answered 18/9, 2019 at 19:48 Comment(8)
I think a better way of doing this is to make the constructor private, and instead have a public static async method that constructs the object for you. This way you can await the calls all the way through.Mickiemickle
or better yet use the httpclientfactory pattern in .net core 2.1Cabalism
@DanielA.White completely forgot that was a thing! My second comment was going to be on the instantiation of the HttpClient. Good callMickiemickle
When opening up Fiddler, no call has been made to the uri. And oddly enough I seem to get a call in Azure: I can't see how both of those things could be true.Quinn
Fiddler not showing but in Azure insight I do see calls. But I do see failed calls also when I'm not testing. So, this might be an error. Set up HttpClientFactory with AutoFac, gives the same result. And a static factory method is also not ok because I have registered the services in DI.Friedafriedberg
Your second one is returning the task, not the result and since a and httpClient are local variables they might be destroyed before you try to access the result later.) To make this more like the working example you would need to do var a = await httpClient.GetStringAsync(uri).ConfigureAwait(false).result;Kinsman
did you ever find a solution?Minuscule
@Minuscule I'm inclined to say this is a dupe of: #51284181Cartwell
L
12

I guess your issue raises because probably you run it in console application (or windows service) without any code snippet to keeps your app alive.

This code is not working:

static void Main(string[] args)
{
    Task.Run(async() => {
        using (var httpClient = new HttpClient())
        {
            var a = await httpClient.GetStringAsync(uri);
        }
    });
}

This code obviously doesn't work because Task.Run creates a background thread and Main method immediately being finished after that (and respectively it causes your task to be terminated). Just add Console.ReadKey() at the end of Main method to keeps it alive and you will see everything works well:

static void Main(string[] args)
{
    Task.Run(async() => {
        using (var httpClient = new HttpClient())
        {
            var a = await httpClient.GetStringAsync(uri);
        }
    });

    Console.ReadKey();
}

Another option is using new async entry points for Main method which are introduced in C# 8:

static async Task Main(string[] args)
{
    using (var httpClient = new HttpClient())
    {
        var a = await httpClient.GetStringAsync(uri);
    }
}
Loireatlantique answered 15/6, 2020 at 16:58 Comment(2)
I would add that you will probably want to await the created background Task. The most logical intent seems to be to wait until the request has finished, not until the user (if there actually is one) arbitrarily presses a key.Yablon
@Yablon That press key was just to clarify the problem and somehow acts as Press any key to exit... but thanks for your hintLoireatlantique
K
6

This might be an issue with calling async code in your constructor. You should make use of .GetAwaiter().GetResult() if you really want to run async code at that point:

public ChatViewModel(IChatService chatService)
{
    Task.Run(async () =>
    {
        if (!chatService.IsConnected)
        {
            await chatService.CreateConnection();
        }
    }).GetAwaiter().GetResult();
}

I would prefer a separated async Init method that will be called after the instance was created. Another possible solution would be to create an async GetInfo() method:

private async Task<Models.ConnectionInfo> GetInfo(){
    if(_info != null)
        return _info;

    await semaphoreSlim.WaitAsync();
    try{
        if(_info != null)
            return _info;

        if (httpClient == null)
        {
            httpClient = new HttpClient();
        }

        var result = await httpClient.GetStringAsync(uri);

        var info = JsonConvert.DeserializeObject<Models.ConnectionInfo>(result);

        //... some other code ...

        return _info = info;
    } finally {
        semaphoreSlim.Release();
    }
}

You can call that method to get your info and the code will only be run on first use.

Kiernan answered 17/6, 2020 at 5:33 Comment(1)
This seems to me to be the correct answer, you need to await the generated task of Task.RunSheet
D
2

Main() method is entry point of you program. After Main method is called program exits, if there are not any foreground threads left running. when you run you download code in new Task, tasks in c# are executed on background thread, that means when Main() method is finished that thread will be aborted. And in you case "Main" method starts download task and finishes instantly because it is not waiting for that task, so download operation is aborted even before request is sent to server. You should wait for task at the end of Main() method.

Delorasdelorenzo answered 19/6, 2020 at 12:43 Comment(0)
C
2

Do not start/initialize any async work in a constructor. Move that out into a separate function and call it.

private readonly IChatService _chatService;

public ChatViewModel(IChatService chatService)
{
   _chatService = chatService;
}

public async Task Initialize()
{
    if (!_chatService.IsConnected)
    {
        await _chatService.CreateConnection();
    }
}

Then in your ChatService, instead of newing up an HttpClient, inject an IHttpClientFactory

private readonly IHttpClientFactory _factory;

public ChatService(IHttpClientFactory factory)
{
    _factory = factory;
}

public async Task CreateConnection()
{
   var httpClient = _factory.CreateClient();

   var result = await httpClient.GetStringAsync(uri);

   var info = JsonConvert.DeserializeObject<Models.ConnectionInfo>(result);

}

In your Console app's Main function, you can either change the signature to public static async Task Main() if you are using modern C#

public static async Task Main()
{
    var serviceCollection = new ServiceCollection();
    serviceCollection.AddHttpClient();
    serviceCollection.AddSingleton<IChatService, ChatService>();
    var services = serviceCollection.BuildServiceProvider();
    var instance = ActivatorUtilities.CreateInstance<MyClass>(services);
    await instance.Run();
}

public class MyClass
{
    private readonly IChatService _chatService;

    public MyClass(IChatService chatService)
    {
        _chatService = chatService;
    }

    public async Task Run()
    {
        var viewModel = new ChatViewModel(_chatService);
        await viewModel.Initialize();
    }

}

Or if you cannot change Main's signature, you can do the following:

public static void Main()
{
    var serviceCollection = new ServiceCollection();
    serviceCollection.AddHttpClient();
    serviceCollection.AddSingleton<IChatService, ChatService>();
    var services = serviceCollection.BuildServiceProvider();
    var instance = ActivatorUtilities.CreateInstance<MyClass>(services);
    instance.Run().GetAwaiter().GetResult();
}
Corcovado answered 22/6, 2020 at 2:39 Comment(0)
G
1

If you wanna run your code in console, you can add a while loop, it pauses your code until the result changed and the default value change to something else than "wait".... So simple idea 💡

static void Main(string[] args)
{
    var result ="wait";
    Task.Run(async() => {
        using (var httpClient = new HttpClient())
        {
            result = await httpClient.GetStringAsync(uri);
        }
    });
    // display loading or kinda stuff
    while( result=="wait");

}

Happy coding 🎉

Grogshop answered 19/6, 2020 at 10:45 Comment(0)
A
1

So, I see something funny that nobody else has mentioned. It might be related to the problem, but I cannot say for sure. Lets take a look at this.

static void Main(string[] args)
{
    Task.Run(async() => {
        using (var httpClient = new HttpClient())
        {
            var a = await httpClient.GetStringAsync(uri);
        }
    });
}

Now, when you are trying to build an app that is very performant using the HttpClient object, you usually create a instance and hold it for multiple sessions, rather than create and tear down the object for each call. There are plenty of other articles online about this, if you are interested in reading more. I usually just create a private static instance and reuse it.

Now, the part that looks a little odd is your using inside of an async call. Normally, a using will auto dispose the object on exit from the block, but I don't know how this will interact with the async keyword. Under the covers there is probably a call back method being created that contains the disposal logic, so I think that it should work, but I have not tried what you are doing.

You also might try making a GetAsync call to see if that can return something where the GetStringAsync is failing. I believe that it is doing a GetAsync, and then just doing the conversion to a string of the result for you. Testing using that method might shed some light on what is happening.

Anesthetic answered 22/6, 2020 at 2:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.