3

I have a ASP .NET web api 2 application, and I'm trying to call asnyc method from sync method and meanwhile I encountered some issues. If I just call the method as a regular method the body after delay doesn't get executed, if I call it with Task.Run() it gets executed

public void CallingMethod(MethodExecutionArgs args)
{
  //do something with the args
            System.Diagnostics.Debug.WriteLine("BEFORE ");

            WriteFileToDiskAsync(args); // If I run only this, I never see "WriteFile() - AFTER delay" in the output
            Task.Run(async () => await WriteFileToDiskAsync(args)); // this executes the entire async method
            System.Diagnostics.Debug.WriteLine($"Finally written from sync method");

}

private async Task<bool> WriteFileToDiskAsync(dynamic file)
{
            System.Diagnostics.Debug.WriteLine("Before delay inside async");
            await Task.Delay(3000);
            System.Diagnostics.Debug.WriteLine("WriteFile() - AFTER delay");
}

Result: I always see the lines written from CallingMethod, but when I call the async method like a regular method, I only see the "Before delay inside async", If I call it with Task.Run() it see both lines from the async method.

Question1: Can someone explain this behavior, why doesn't the async method execute fully? Does it have something to do with what Stephen Cleary says: If you lose your AppDomain for any reason, that in-progress work is lost.

I read these articles but can't figure out why is this happening:

Context: What I'm trying to achieve is, on a existing API route that creates a X Resource (saves it in a database), after the X resource is created, I want to asynchronously call a method that will Create a json file with some information from that X Resource and save it in a filesystem. But even if the writing of the file fails, I want to return a successful response to the client (because his request was to actually save the X Resource - which succeeded) he doesn't care about Creating external file in XYZ filesystem.

Question2: What would you say is the best practice to achieve what I described above, considering all the existing methods chained in the CreateResource route are sync?

guxxo
  • 78
  • 7
  • 2
    The best solution is: don't call `async` methods from non- `async` methods. What's stopping you from marking `CallingMethod` as `async` and having it return a Task? – mason Oct 30 '20 at 15:49
  • Because the method that is calling the `CallingMethod` is sync and it is called by 60+ different methods which are called by N methods... And if I just make CallingMethod async and call it regularly from the sync method I would get the same behaviour as desribed above – guxxo Oct 30 '20 at 15:58
  • 1
    I would definitely run that in a separate process, maybe send it to a queue first, so you can have full control of what happens with that file creation. – insane_developer Oct 30 '20 at 15:58
  • 1
    @guxxo So? Do a one time refactoring to make those async as well. You'll find that proper use of async means that `async` grows throughout the app. It's not too hard to to refactor the apps. Just make sure all your async calls are awaited, and that they're being called from methods that are marked as async and return a Task or Task. It's a straightfoward refactor in most cases. – mason Oct 30 '20 at 16:00
  • @mason We don't have the resources to refactor the entire solution right now, we are talking about huge project with tens of thousands of files ... – guxxo Oct 30 '20 at 16:04
  • 2
    It's hard to imagine it taking a developer more than a single day to do this. It's a straight forward rinse-wash-repeat refactor. You do it once, then repeat the same process over and over. – mason Oct 30 '20 at 16:40
  • Best question, I learn many things from this – Dhrumil shah Dec 10 '20 at 11:33

1 Answers1

1

Question1: Can someone explain this behavior, why doesn't the async method execute fully? Does it have something to do with what Stephen Cleary says: If you lose your AppDomain for any reason, that in-progress work is lost.

No. The AppDomain is still there in this case. In ASP.NET (pre-Core), there is an ASP.NET SynchronizationContext that represents the request. await by default captures the current context and uses that to resume executing its async method.

So, when the await completes, it attempts to resume executing on the SynchronizationContext for that request. However, that request has been completed, and so it doesn't make sense to resume on that context. The resulting behavior is undefined.

Question2: What would you say is the best practice to achieve what I described above, considering all the existing methods chained in the CreateResource route are sync?

If you are sure you want to return early, then you should have a background service handle creating the file, not the ASP.NET service. Since you have a database already, you could use the Outbox Pattern, where you place messages in an "outbox" table as part of the same transaction as your resource creation. Then a separate background service reads the "outbox" table and writes the JSON file.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810