Search code examples
c#asp.netasynchronousasync-awaitpagemethods

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


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.


Solution

  • 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();
            }
        }
    }