1

I'm refactoring code from our .NET Framework library into a microservice. The microservice has a repository that has an async method to get data from a stored procedure. However, the old code's methods don't use an async method to get this same data and the calls (from legacy code) to these methods look like this:

Task appsSubmittedTask = new Task(
    () => { Method1(otherData, dashboardData, reportData); });

Task assistancePendingTask = new Task(
    () => { Method2(otherData, dashboardData, reportData); });

Task assistanceDeniedTask = new Task(
    () => { Method3(otherData, dashboardData, reportData); });

appsSubmittedTask.Start();
assistancePendingTask.Start();
assistanceDeniedTask.Start();

Task.WaitAll(appsSubmittedTask, assistancePendingTask, assistanceDeniedTask);

Each of the three methods (Method1, Method2, Method3) in the original code returned void. Now that the new methods in the microservice are accessing an injected service for stored procedure calls which is asynchronous I updated the three methods to have async Task in the method signature, each method now has data access like this:

IList<resultType> results = await _service.AsyncStoredProcedureMethod(...params)

So now the above methods being called by the three created Tasks have squiggles underneath the method call with the "because the call is not awaited, execution of the current method continues before the call is completed"

Is there a specific way to go about this where I'm not spinning up the three Tasks, as I'm worried that ignoring the warning and running the old code (with three tasks) may cause the underlying DbContexts from the _service to overlap? Or should I create a stored procedure call in the _service that isn't asynchronous? I'm open to any ideas.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
jmath412
  • 419
  • 3
  • 13
  • In your actual code, is anything else happening between creating the tasks and starting them? Is there more code between the lines `Task assistanceDeniedTask = new Task(...` and `appsSubmittedTask.Start();`? – Theodor Zoulias Apr 02 '23 at 04:40
  • Related: [How to construct a Task without starting it?](https://stackoverflow.com/questions/16066349/how-to-construct-a-task-without-starting-it) – Theodor Zoulias Apr 02 '23 at 04:57
  • No, it's just like the code above, no more code between creation/start of the Tasks. – jmath412 Apr 02 '23 at 05:06

3 Answers3

4

Do not ever call the task constructor or Start. The old code should have been calling Task.Run instead of the task constructor with Start.

Is there a specific way to go about this where I'm not spinning up the three Tasks

The old code was doing the three calls in parallel, each one with its own connection to the database.

You're making two changes now: the code is asynchronous, and there's only one single shared connection to the database. So your solution has to take both of these aspects into account.

I'm worried that ignoring the warning and running the old code (with three tasks) may cause the underlying DbContexts from the _service to overlap?

A DbContext cannot be used concurrently, regardless of whether the code is synchronous or asynchronous. So you have a couple options.

Option 1 is to keep the new shared DbContext. In that case, the three methods cannot be run concurrently. You can execute them serially and asynchronously as such:

await Method1Async(context, dashboardData, reportData);
await Method2Async(context, dashboardData, reportData);
await Method3Async(context, dashboardData, reportData);

However, this will execute them one at a time, while the old code executed them concurrently. Maybe that's ok, maybe not.

Option 2 is to make the new code more similar to the old code: give each method its own DbContext and run them concurrently. The old code used parallel concurrency; the new code can use asynchronous concurrency:

var task1 = Method1Async(context1, dashboardData, reportData);
var task2 = Method2Async(context2, dashboardData, reportData);
var task3 = Method3Async(context3, dashboardData, reportData);
await Task.WhenAll(task1, task2, task3);

The disadvantage to this approach is that it does use three connections. However, it is the most similar to the original code in terms of behavior.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • I need to edit the question. I changed most of the previous developer's code before making this post but not the parameters. The context represents a model that is not a DbContext, it's just an ill-named parameter. – jmath412 Apr 02 '23 at 02:53
  • @jmath412: Ok. The basic solution is the same, though: create three different db contexts and pass them in (or have each method create its own). – Stephen Cleary Apr 02 '23 at 04:03
1

The simplest solution is probably to switch from the Task constructor + Start to the Task.Run method:

Task appsSubmittedTask     = Task.Run(() => Method1(otherData, dashboardData, reportData));
Task assistancePendingTask = Task.Run(() => Method2(otherData, dashboardData, reportData));
Task assistanceDeniedTask  = Task.Run(() => Method3(otherData, dashboardData, reportData));

This works because the Task.Run method includes overloads that accept asynchronous delegates, and unwrap internally the resulting Task<Task>. For a deeper explanation you can read this article: Task.Run vs Task.Factory.StartNew.

Using the Task constructor makes sense only when you have more work to do between creating and starting the task, or when the task should only be started under conditions that are unknown at the time that the task is created, or when you want to execute the task on the current thread (RunSynchronously). These scenarios are rare.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
0

You will need to chain the async and await in all methods now, otherwise the results will be unexpected and inconsistent.

One of the solution could be to await your method call in the Task objects you created like below:

Task appsSubmittedTask = new Task(async () => { await Method1(context, dashboardData, reportData); });
Task assistancePendingTask = new Task(async () => { await Method2(context, dashboardData, reportData); });
Task assistanceDeniedTask = new Task(async () => { await Method3(context, dashboardData, reportData); });

Please also note that using the Task contructor is not the good approach to be used and we used Task.Start() method for this purpose. You can refer to the following post for more details: Await new Task<T>( ... ) : Task does not run?

Ehsan Sajjad
  • 61,834
  • 16
  • 105
  • 160
  • This creates [`async void`](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void "Async/Await - Best Practices in Asynchronous Programming - Avoid Async Void") lambdas. The tasks `appsSubmittedTask`, `assistancePendingTask` and `assistanceDeniedTask` do not represent the completion of those lambdas. – Theodor Zoulias Apr 02 '23 at 04:59