Nicolas
Nicolas

Reputation: 2376

C#/Xamarin HttpClientSendAsync not returning when waiting for to long

Recently I had several issues when calling my API endpoint on mobile. I never had the issue on my emulator (cause of the better network), but I finally could reproduce it.

So what happens is the following: A user starts their app and several API endpoints are being called to retrieve the data. Some of these API endpoints however take (to) long.

So the API calling setup is setup as following:

public async Task<bool> GetData()
{
         
    Task.Run(async () =>
    {
        if (App.elementsSynced == false)
            await _oDataService.GetElements(_userId, _token);
            
        // .... more code
    }
}

All the (top level) methods calling GetData() are async as well and await the call.

GetElements():

public async Task GetElements(string userId, string token)
{
    if (App.elementsSynced == true)
    {
        return;
    }

    try
    {

        var apiEndpointUri = new Uri(_apiUri, $"GetElements(UserId='{userId}')");
        var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, apiEndpointUri);
        httpRequestMessage.Headers.Add("Authorization", $"Bearer {token}");
        var response = await _httpClient.SendAsync(httpRequestMessage);
        HttpStatusCode statusCode = response.StatusCode;

        var responseResult = await response.Content.ReadAsStringAsync();

        if (statusCode != HttpStatusCode.OK)
        {
            // ... code omitted

            return;
        }


        // .. saving data locally

    
        App.elementsSynced = true;
        return;

    }
    catch (Exception ex)
    {
        
    }
}

The above code works fine when there is a stable internet connection, however when it takes to long, and I don't know what the threshold exactly is, then the result of the call which is being made at

var response = await _httpClient.SendAsync(httpRequestMessage);

is never being returned, also not when the data IS being returned after ~60s or so (tested it with a 60s threshold).

I reproduced this problem by setting a breakpoint in my API endpoint and waiting for about 60 seconds or so. When I resume the API after 60 seconds, and the data is being send back, it never hits the caller (Xamarin APP).

Is there a 'hidden' timeout at which the http request does not return a result anymore? I know .NET 5 introduced the TimeoutException which I can use when setting a timeout in my HttpClient, but I have no idea what should be the (max) timeout. The default value is 100 seconds, so I would expect that after 100 seconds it should fall in exception anyway, but it doesn't..

Upvotes: 0

Views: 193

Answers (1)

Michael
Michael

Reputation: 1276

Since it is a common problem, I am posting my comment as an answer. Problem: A page needs to fetch some async data or perform some async checks before it can be displayed. But the constructor of the page can't be async (for a good reason, constructors have to be minimal). Sometimes this problem is bypassed with bad solutions (blocking calls with Wait() or Result, fire & forget with Task.Run, ...).

Solution: An async create pattern for your ViewModel You can define a static CreateAsync() Method in your ViewModel:

public class ViewModelB
{
    public string DataFromApi { get; }

    // private constructor to prevent direct instantiation
    private ViewModelB(string dataFromApi)
    {
        DataFromApi = dataFromApi;
    }

    public static async Task<ViewModelB> CreateAsync()
    {
        // Load data from HttpClient, ...
        var loadedData = await client.GetStringAsync();
        // Now we have all data to instantiate the view model.
        return new ViewModelB(loadedData);
    }
}

Now you can add a constructor to your page class which uses an initialized view model to set the binding context:

public partial class PageB : ContentPage
{
    // private constructor to prevent direct instantiation
    private PageB()
    {
        InitializeComponent();
    }

    // Our page needs an initialized vm.
    public PageB(ViewModelB vm) : this()
    {
        BindingContext = vm;
    }
}

In an event handler of Page A (login button, ...), which can be async, you can create your VM for page B and pass it to your page constructor:

try
{
    var vm = await ViewModelB.CreateAsync();
    await myNavigation.PushAsync(new PageB(vm));
}
catch 
{
    // Give feedback if there was a problem during initialization (server error, authorization problems,  ...)            
}

Upvotes: 1

Related Questions