Home > Software engineering >  Async method - weird behavior: my app is getting stuck
Async method - weird behavior: my app is getting stuck

Time:04-12

I have a weird problem with my async method. I made second project in my solution in VS to connect my app to an API (using RestSharp for it). I made dependencies etc.

The problem is when I call this method from the UI by clicking a button (it only start backend code, there is no relation to UI etc.) app getting stuck. There is no errors, the only things I can see in the output window are "The thread ****** has exited with code 0 (0x0)." and it's going infinitely.

I took that code (only from project responsible for connecting and taking data from an api) and made a new solution, new project, but exacly copied code and it is working fine.

This is method what I am calling in the "main" WPF app using ICommand etc:

private void Api()
{
    _orderService = new OrderService();
}

And those are classes in API project:

BLContext.cs

public class BLContext
{
    private RestClient _client;
    public RestClient Client { get; set; }

    private string _token;
    public string Token { get; set; }

    public BLContext()
    {
        Client = new RestClient("https://api.baselinker.com/connector.php");
        Token = "************************";
    }
}

BaseAPIRepository.cs

public class BaseAPIRepository
    {
        private BLContext _bl = new BLContext();
        RestRequest Request = new RestRequest();

        public BaseAPIRepository() { }


        public async Task<List<Order>> GetOrders()
        {
            List<Order> orders = new List<Order>();
            List<JToken> orderList = new List<JToken>();

            StartRequest("getOrders");
            Request.AddParameter("parameters", "{ \"status_id\": 13595 }");
            Request.AddParameter("parameters", "{ \"get_unconfirmed_orders\": false }");

            RestResponse restResponse = await _bl.Client.PostAsync(Request);
            JObject response = (JObject)JsonConvert.DeserializeObject(restResponse.Content);

            orderList = response["orders"].ToList();

            foreach (JToken order in orderList)
            {
                Order newOrder = new Order();
                newOrder.Id = (int)order["order_id"];
                newOrder.ProductsInOrder = GetProductsFromOrder((JArray)order["products"]);

                orders.Add(newOrder);
            }

            return orders;

        }

        public void StartRequest(string method)
        {
            Request.AddParameter("token", _bl.Token);
            Request.AddParameter("method", method);
        }

        public List<OrderedProduct> GetProductsFromOrder(JArray productsInOrder)
        {
            List<OrderedProduct> tmpListOfProducts = new List<OrderedProduct>();
            foreach (var item in productsInOrder)
            {
                OrderedProduct tmpOrderedProduct = new OrderedProduct();
                //tmpOrderedProduct.Id = (int)item["product_id"];
                tmpOrderedProduct.Signature = (string)item["sku"];
                tmpOrderedProduct.Quantity = (int)item["quantity"];
                tmpListOfProducts.Add(tmpOrderedProduct);
            }

            return tmpListOfProducts;
        }
    }

OrderService.cs

public class OrderService
    {
        private BaseAPIRepository _repo;

        private List<Order> _ordersList;
        public List<Order> OrdersList { get; set; }


        public OrderService()
        {
            _repo = new BaseAPIRepository();
            OrdersList = new List<Order>();

            OrdersList = _repo.GetOrders().Result;
            Console.WriteLine("Test line to see if it passed 24th line.");

        }
    }

App is getting stuck on line:

RestResponse restResponse = await _bl.Client.PostAsync(Request);

CodePudding user response:

The core problem - as others have noted - is that your code is blocking on asynchronous code, which you shouldn't do (as I explain on my blog). This is particularly true for UI apps, which deliver a bad user experience when the UI thread is blocked. So, even if the code wasn't deadlocking, it wouldn't be a good idea to block on the asynchronous code anyway.

There are certain places in a UI app where the code simply cannot block if you want a good user experience. View and ViewModel construction are two of those places. When a VM is being created, the OS is asking your app to display its UI right now, and waiting for a network request before displaying data is just a bad experience.

Instead, your application should initialize and return its UI immediately (synchronously), and display that. If you have to do a network request to get some data to display, it's normal to synchronously initialize the UI into a "loading" state, start the network request time, and then at that point the construction/initialization is done. Later, when the network request completes, the UI is updated into a "loaded" state.

If you want to take this approach, there's a NotifyTask<T> type in my Nito.Mvvm.Async package which may help. Its design is described in this article and usage looks something like this (assuming OrderService is actually a ViewModel):

public class OrderService
{
  private BaseAPIRepository _repo;

  public NotifyTask<List<Order>> OrdersList { get; set; }

  public OrderService()
  {
    _repo = new BaseAPIRepository();
    OrdersList = NotifyTask.Create(() => _repo.GetOrders());
  }
}

Then, instead of data-binding to OrderService.OrdersList, you can data-bind to OrderService.OrdersList.Result, OrderService.OrdersList.IsCompleted, etc.

CodePudding user response:

You should never call Task.Result on an incomplete Task to avoid deadlocking the application. Always await a Task.
C# doesn't allow async constructors. Constructors are meant to return fast after some brief initialization. They are not a place for long-running operations or starting background threads (even if async constructors were allowed).
There are a few solutions to avoid the requirement of async constructors.

  1. A simple alternative solution using Lazy<T> or AsyncLazy<T> (requires to install the Microsoft.VisualStudio.Threading package via the NuGet Package Manager). Lazy<T> allows to defer the instantiation or allocation of expensive resources.
public class OrderService
{
  public List<object> Orders => this.OrdersInitializer.GetValue();
  private AsyncLazy<List<object>> OrdersInitializer { get; }

  public OrderService()
    => this.OrdersInitializer = new AsyncLazy<List<object>>(InitializeOrdersAsync, new JoinableTaskFactory(new JoinableTaskContext()));

  private async Task<List<object>> InitializeOrdersAsync()
  {
    await Task.Delay(TimeSpan.FromSeconds(5));
    return new List<object> { 1, 2, 3 };
  }
}

public static void Main()
{
  var orderService = new OrderService();

  // Trigger async initialization
  orderService.Orders.Add(4);
}
  1. You can expose the data using a method instead of a property
public class OrderService
{
  private List<object> Orders { get; set; }

  public async Task<List<object>> GetOrdersAsync()
  {
    if (this.Orders == null)
    {
      await Task.Delay(TimeSpan.FromSeconds(5));
      this.Orders = new List<object> { 1, 2, 3 };
    }
    return this.Orders;
  }
}

public static async Task Main()
{
  var orderService = new OrderService();

  // Trigger async initialization
  List<object> orders = await orderService.GetOrdersAsync();
}
  1. Use an InitializeAsync method that must be called before using the instance
public class OrderService
{
  private List<object> orders;
  public List<object> Orders 
  { 
    get
    {
      if (!this.IsInitialized)
      {
        throw new InvalidOperationException(); 
      }
      return this.orders;
    }
    private set
    {
      this.orders = value;
    }
  }

  public bool IsInitialized { get; private set; }

  public async Task<List<object>> InitializeAsync()
  {
    if (this.IsInitialized)
    {
      return;
    }

    await Task.Delay(TimeSpan.FromSeconds(5));
    this.Orders = new List<object> { 1, 2, 3 };
    this.IsInitialized = true;
  }
}

public static async Task Main()
{
  var orderService = new OrderService();

  // Trigger async initialization
  await orderService.InitializeAsync();
}
  1. Instantiate the instance by passing the expensive arguments to the constructor
public class OrderService
{
  public List<object> Orders { get; }

  public async Task<List<object>> OrderService(List<object> orders)
    => this.Orders = orders;
}

public static async Task Main()
{
  List<object> orders = await GetOrdersAsync();

  // Instantiate with the result of the async operation
  var orderService = new OrderService(orders);
}

private static async Task<List<object>> GetOrdersAsync()
{
  await Task.Delay(TimeSpan.FromSeconds(5));
  return new List<object> { 1, 2, 3 };
}
  1. Use a factory method and a private constructor
public class OrderService
{
  public List<object> Orders { get; set; }

  private OrderServiceBase()  
    => this.Orders = new List<object>();

  public static async Task<OrderService> CreateInstanceAsync()
  {
    var instance = new OrderService();
    await Task.Delay(TimeSpan.FromSeconds(5));
    instance.Orders = new List<object> { 1, 2, 3 };
    return instance;
  }
}

public static async Task Main()
{
  // Trigger async initialization  
  OrderService orderService = await OrderService.CreateInstanceAsync();
}
  • Related