skip to Main Content

In my WPF application (.NET 8.0) I want to consume a asynchronous operation from a 3rd party library. When testing with all code inside the MainWindow.xaml.cs it works smoothely. When I move the same code into a separate class, using a static constructor, the operation is blocking indefinitely even with WaitAsync.

Here is my code:

  public class VirtuosoCommunication
  {
    ..
    public static VirtuosoCommunication? Create(...)
    {
      VirtuosoCommunication vc = new VirtuosoCommunication();
      // setting private variables for vc

      if (!vc.TestRead(log)) return null;
      if (!vc.TestWrite(log)) return null;

      return vc;
    }

    private bool TestRead(Log? log)
    {
      string testGraph = "...";
      Task<Graph?> task = LoadGraphFromSparqlRemoteEndpoint(testGraph, log);
            
      Graph? g = task.Result;
      return g != null;
    }

    internal async Task<Graph?> LoadGraphFromSparqlRemoteEndpoint(string graphFullName, Log? log)
    {
      Uri baseUri = new Uri(_readEndpoint ?? "");

      HttpClient client = new HttpClient();
      client.BaseAddress = baseUri;

      SparqlQueryClient sparqlQueryClient = new SparqlQueryClient(client, baseUri);

      Graph result = new Graph();
      IGraph? tmp = null;

      try
      {
        while (tmp == null || tmp.Triples.Count == limit)
        {
          string query = "construct { ?s ?p ?o } FROM <" + graphFullName + "> where {?s ?p ?o.} OFFSET " + offSet.ToString() + " LIMIT " + limit.ToString();
          Task<IGraph> task = sparqlQueryClient.QueryWithResultGraphAsync(query);
          tmp = await task.WaitAsync(TimeSpan.FromSeconds(4));

          ...
        }
      }
      catch (Exception ex) { ... }

      return result;
    }

When I run the code line by line, it stops executing at tmp = await task.WaitAsync. When I then hit the "Break execution" Button in Visual Studio, it is showing me the line Graph? g = task.Result in the TestRead method to be the next to be executed when the thread returns.

As I said, the same async call in a test routine completely placed in MainWindow.xaml.cs works without problem, so I guess I am somehow using the async call wrongly.

UPDATE 25.11.2024
From the answer of this question I changed the TestRead method to:

private bool TestRead(Log? log)
{
  string testGraph = "...";
  //Task<Graph?> task = LoadGraphFromSparqlRemoteEndpoint(testGraph, log);
  Task<Graph?> task = Task.Run<Graph?>(async() => await LoadGraphFromSparqlRemoteEndpoint(testGraph, log));

        
  Graph? g = task.Result;
  return g != null;
}

It works this way (yeah!), but I am not sure if this is the correct way to do it. Regarding parallelization, I just need the call to the SPARQL endpoint to not block my UI, I don’t need to run other, parallel tasks meanwhile.

2

Answers


  1. As Fildor said in the comments, you’re not using a static cctor; you’re using a static factory method.

    And that’s a good thing. A ctor or cctor cannot be async, so you wouldn’t be able to await anything. But a factory method can be async. And in this case, it should be:

    public static async Task<VirtuosoCommunication?> Create(...)
    {
        VirtuosoCommunication vc = new VirtuosoCommunication();
        // setting private variables for vc
    
        if (!await vc.TestRead(log)) return null;
        if (!await vc.TestWrite(log)) return null;
    
        return vc;
    }
    
    private async Task<bool> TestRead(Log? log)
    {
        string testGraph = "...";
        Graph? g = await LoadGraphFromSparqlRemoteEndpoint(testGraph, log);
        return g != null;
    }
    

    Next, I’d suggest looking at your HttpClient usage, which is problematic:

    You should consider using IHttpClientFactory instead of creating a new HttpClient instance on every call.

    Use IHttpClientFactory to implement resilient HTTP requests – .NET | Microsoft Learn

    Login or Signup to reply.
  2. Your updated answer using Task.Run() is likely correct, but it is not completely clear without context:

    When you’re awaiting tasks with await DoSomething(); the current synchronization context is captured (if called from a button click in WPF, this will be the main thread). When the async operation completes, the state machine will schedule the next line of code to be executed in the same context (ie. the main thread). However, since your calling task.Result in TestRead you’re already blocking the main thread, resulting in a deadlock.

    Calling Task.Run() avoids this problem by making sure everything "inside" the Task.Run block is executed on the thread pool, which does not preserve synchronization context and will not deadlock as it will just spawn new threads if all of them are blocked.

    Please note that it would still be better to use async / await all the way through and avoid Task.Result and Task.Wait() as much as possible. If you can change the TestRead function to return a Task instead (like Richard Deeming proposed here https://stackoverflow.com/a/79214898/1074014) definitely go for this approach!

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search