Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async/Await in WebForms - How does the continuation run before the page lifecycle ends?

I've been testing async in WebForms and for once, my question isn't about how to do something, it's about how something that already works, DOES work. This is my test code:

protected override void OnPreRender(EventArgs e)
{
    Response.Write("OnPreRender<Br>");
}
protected override void OnPreRenderComplete(EventArgs e)
{
    Response.Write("OnPreRenderComplete<Br>");
}
protected async override void OnLoadComplete(EventArgs e)
{
    Response.Write("OnLoadComplete<br>");
    var t1 = Task.Factory.StartNew(() => {
        System.Threading.Thread.Sleep(2000);
        return 1;
    });

    //This actually does run:
    Response.Write((await t1).ToString());
}

So my task pauses for a bit and then writes out the result. My question is - I wouldn't expect this to work because control has been yielded from the OnLoadComplete method - I would expect the page to actually finish rendering and be returned to the client before my task ever returned.

The actual output is:

OnLoadComplete
OnPreRender
1OnPreRenderComplete

So it's clear that the OnLoadComplete method yielded control so that OnPreRender could run and then control returned to the OnLoadComplete. My expected result was that the "1" would never print because the subsequent events would fire and the page's thread would either be killed or the post-task write would happen after the response had been sent. I guess, given the above, it's not surprising that even if I delay for 10 seconds, the result is exactly the same.

I assume there's some wiring in the WebForm engine that ensures any awaitables are completed before the next phase of the page's life cycle proceeded. Does anyone know for sure how this happens? I'm afraid to use async/await in methods that need to complete before other events for fear that the continuation will be too late, but if it's handled internally, then I won't worry.

like image 805
powlette Avatar asked Feb 07 '13 17:02

powlette


1 Answers

For ASP.NET, you should only use async methods on .NET 4.5. I'll explain why at the end.

I have an article on SynchronizationContext that helps fill in the blanks on how this works on ASP.NET. First, note that ASP.NET supported asynchronous operations a long time ago (.NET 2.0 IIRC). You could register asynchronous operations a few different ways, but for this description we'll focus on SynchronizationContext.OperationStarted.

ASP.NET creates a SynchronizationContext for each request, and it knows the request is not complete until all registered operations have completed (by calling SynchronizationContext.OperationCompleted). Event-based asynchronous pattern components (such as BackgroundWorker) will notify the SynchronizationContext automatically when they start and complete.

Similarly, async void methods (the new task-based asynchronous pattern) will notify the SynchronizationContext automatically when they start and complete. So when you override OnLoadComplete as an async void method, the compiler inserts code for you that will call OperationStarted at the beginning and OperationCompleted when it completes.

So far so good - ASP.NET now knows to keep the request alive until all asynchronous operations for that request have completed. This is true even if there are no threads processing the request.

Now the caveat: ASP.NET before .NET 4.5 would handle this at a request level. In ASP.NET 4.5, the lifecycle pipeline was made smarter so that it would delay the page lifecycle until asynchronous operations completed. With the old ASP.NET, the "pre" handlers would start at that point in the pipeline but may not finish until later. The new ASP.NET will delay the rest of the page execution to ensure async handlers complete before moving on in the lifecycle.

Also, ASP.NET 4.5 will detect if you've used an async handler where there shouldn't be one, and will notify you of the error.

like image 178
Stephen Cleary Avatar answered Sep 27 '22 18:09

Stephen Cleary