Async functions in C# deadlock when called from javascript aspx page via PageMethods

1.3k views Asked by At

Problem Summary:
I'm trying to use PageMethods to call a C# function from an HTML page. The problem is that the C# function I'm calling is marked as async and will await the completion of other functions. When the nested async C# functions are called by PageMethods, the C# code seems to deadlock.
I've given an example ASP.NET page with C# coded behind it to illustrate the idiom I'm trying to use.

Example WebForm1.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs" Inherits="WebApplication3.WebForm1" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title></title></head>
<body>
    <form id="form1" runat="server">
        <asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="true"/>
        <div>
            <input type="button" value="Show Function timing" onclick="GetTiming()"/>
        </div>
    </form>
</body>

<script type="text/javascript">
    function GetTiming() {
        console.log("GetTiming function started.");
        PageMethods.GetFunctionTiming(
            function (response, userContext, methodName) { window.alert(response.Result); }
        );
        console.log("GetTiming function ended."); // This line gets hit!
    }
</script>

</html>

Example WebForm1.aspx.cs

using System;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Web.Services;
using System.Web.UI;
namespace WebApplication3
{
    public partial class WebForm1 : Page
    {
        protected void Page_Load(object sender, EventArgs e) { }
        [WebMethod]
        public static async Task<string> GetFunctionTiming()
        {
            string returnString = "Start time: " + DateTime.Now.ToString();
            Debug.WriteLine("Calling to business logic.");

            await Task.Delay(1000); // This seems to deadlock
            // Task.Delay(1000).Wait(); // This idiom would work if uncommented.

            Debug.WriteLine("Business logic completed."); // This line doesn't get hit if we await the Task!
            return returnString + "\nEnd time: "+ DateTime.Now.ToString();
        }
    }
}

Question:
I absolutely need to be able to call asynchronous code from my web page UI. I'd like to use async/await functionality to do this, but I haven't been able to figure out how to. I'm currently working around this deficit by using Task.Wait() and Task.Result instead of async/await, but that's apparently not the recommended long-term solution.
How can I await on server-side async functions in the context of a PageMethods call???
I really, really want to understand WHAT is happening under the covers here, and WHY it does NOT happen when the async method is called from a console app.

2

There are 2 answers

1
Neils Schoenfelder On BEST ANSWER

I've figured out an approach that allows PageMethods to call nested async functions on ASP.NET without deadlocking. My approach involves

  1. Using ConfigureAwait(false) so we don't force the async function to try to return to the original captured context (which the Web UI thread will have locked); and
  2. Forcing the "top level" async function onto the thread pool, instead of having it run in ASP.NET's UI context.

Each of these two approaches is commonly recommended against on forums and blogs, so I'm sure that doing both of them constitutes an antipattern. However, it does allow a useful shim to call async functions from an ASP.NET webpage by using PageMethods.
Example C# code is given below.

Example WebForm1.aspx.cs

using System;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Web.Services;
using System.Web.UI;

namespace WebApplication3
{
    public partial class WebForm1 : Page
    {

        protected void Page_Load(object sender, EventArgs e) { }

        [WebMethod]
        public static async Task<string> GetFunctionTiming()
        {
            Debug.WriteLine("Shim function called.");
            string returnString = "Start time: " + DateTime.Now.ToString();

            // Here's the idiomatic shim that allows async calls from PageMethods
            string myParameter = "\nEnd time: "; // Some parameter we're going to pass to the business logic
            Task<string> myTask = Task.Run( () => BusinessLogicAsync(myParameter) ); // Avoid a deadlock problem by forcing the task onto the threadpool
            string myResult = await myTask.ConfigureAwait(false); // Force the continuation onto the current (ASP.NET) context

            Debug.WriteLine("Shim function completed.  Returning result "+myResult+" to PageMethods call on web site...");
            return returnString + myResult;
        }

        // This takes the place of some complex business logic that may nest deeper async calls
        private static async Task<string> BusinessLogicAsync(string input)
        {
            Debug.WriteLine("Invoking business logic.");
            string returnValue = await DeeperBusinessLogicAsync();
            Debug.WriteLine("Business logic completed.");
            return input+returnValue;
        }

        // Here's a simulated deeper async call
        private static async Task<string> DeeperBusinessLogicAsync()
        {
            Debug.WriteLine("Invoking deeper business logic.");
            await Task.Delay(1000); // This simulates a long-running async process
            Debug.WriteLine("Deeper business logic completed.");
            return DateTime.Now.ToString();
        }
    }
}
3
Zer0 On

This is because await, by default, captures the current SynchronizationContext and attempts to post back to it upon completion. This gets dicey with ASP.NET threading details, but in short the deadlock is actually between that method blocking the thread while the continuation attempts to post back to it.

There are better designs but a fix, or hack, is to NOT attempt to post back to the captured SynchronizationContext. Note this is a hack because it will run the continuation (the remainder of the method) on whatever thread it happened to execute the Task on. That's generally a ThreadPool thread.

This will solve your deadlock, however. Keep in mind the dangers and I'd suggest a better design.

await Task.Delay(1000).ConfigureAwait(false);