3
votes

We have a legacy VB6 application that uses an ASMX webservice written in C# (.NET 4.5), which in turn uses a library (C#/.NET 4.5) to execute some business logic. One of the library methods triggers a long-running database stored procedure at the end of which we need to kick off another process that consumes the data generated by the stored procedure. Because one of the requirements is that control must immediately return to the VB6 client after calling the webservice, the library method is async, takes an Action callback as a parameter, the webservice defines the callback as an anonymous method and doesn't await the results of the library method call.

At a high level it looks like this:

using System;
using System.Data.SqlClient;
using System.Threading.Tasks;
using System.Web.Services;

namespace Sample
{
    [WebService(Namespace = "urn:Services")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    public class MyWebService
    {
        [WebMethod]
        public string Request(string request)
        {
            // Step 1: Call the library method to generate data
            var lib = new MyLibrary();
            lib.GenerateDataAsync(() =>
            {
                // Step 2: Kick off a process that consumes the data created in Step 1
            });

            return "some kind of response";
        }
    }

    public class MyLibrary
    {
        public async Task GenerateDataAsync(Action onDoneCallback)
        {
            try
            {
                using (var cmd = new SqlCommand("MyStoredProc", new SqlConnection("my DB connection string")))
                {
                    cmd.CommandType = System.Data.CommandType.StoredProcedure;
                    cmd.CommandTimeout = 0;
                    cmd.Connection.Open();

                    // Asynchronously call the stored procedure.
                    await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);

                    // Invoke the callback if it's provided.
                    if (onDoneCallback != null)
                        onDoneCallback.Invoke();
                }
            }
            catch (Exception ex)
            {
                // Handle errors...
            }
        }
    }
}

The above works in local tests, but when the code is deployed as a webservice Step 2 is never executed even though the Step 1 stored procedure completes and generates the data.

Any idea what we are doing wrong?

2
Your local machine's firewall may be blocking the incoming connection...Eser
As aside, I guess that Step 1 is calling lib.GenerateDataAsync (says GenerateData which is not shown). I think the issue is that the asmx request has finished executing and "is out the door", and there is "nowhere to execute" the callback. Have you tried awaiting that call?Mark Larter
Review your implementation of Request. If you are using async you have to use it in your whole pipeline for the most consistent solution.Jeroen Heier
@MarkLarter, thanks for noticing my typo, I've fixed it. Awaiting the call in the web service would defeat the whole "fire and forget" approach to running the stored procedure. I agree that the likely cause is that the thread on which the call to GenerateDataAsync runs gets recycled by IIS by the time the call is finished, so there's no context for the callback to execute on. I am just hoping someone who has "been there, done that" can suggest a workaround.Caspian Canuck
@CaspianCanuck Definitely BTDT, but not since .NET 2.0 (BeginAsync...). Awaiting should still free the IIS worker thread to handle other web requests, so you'd not be blocking your request pipeline. Not sure what else you need to accomplish with "fire and forget", so admittedly this may not be enough for your needs.Mark Larter

2 Answers

2
votes

it is dangerous to leave tasks running on IIS, the app domain may be shut down before the method completes, that is likely what is happening to you. If you use HostingEnvironment.QueueBackgroundWorkItem you can tell IIS that there is work happening that needs to be kept running. This will keep the app domain alive for a extra 90 seconds (by default)

using System;
using System.Data.SqlClient;
using System.Threading.Tasks;
using System.Web.Services;

namespace Sample
{
    [WebService(Namespace = "urn:Services")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    public class MyWebService
    {
        [WebMethod]
        public string Request(string request)
        {
            // Step 1: Call the library method to generate data
            var lib = new MyLibrary();
            HostingEnvironment.QueueBackgroundWorkItem((token) =>
                lib.GenerateDataAsync(() =>
                {
                    // Step 2: Kick off a process that consumes the data created in Step 1
                }));

            return "some kind of response";
        }
    }

    public class MyLibrary
    {
        public async Task GenerateDataAsync(Action onDoneCallback)
        {
            try
            {
                using (var cmd = new SqlCommand("MyStoredProc", new SqlConnection("my DB connection string")))
                {
                    cmd.CommandType = System.Data.CommandType.StoredProcedure;
                    cmd.CommandTimeout = 0;
                    cmd.Connection.Open();

                    // Asynchronously call the stored procedure.
                    await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);

                    // Invoke the callback if it's provided.
                    if (onDoneCallback != null)
                        onDoneCallback();
                }
            }
            catch (Exception ex)
            {
                // Handle errors...
            }
        }
    }
}

If you want something more reliable than 90 extra seconds see the article "Fire and Forget on ASP.NET" by Stephen Cleary for some other options.

1
votes

I have found a solution to my problem that involves the old-style (Begin/End) approach to asynchronous execution of code:

    public void GenerateData(Action onDoneCallback)
    {
        try
        {
            var cmd = new SqlCommand("MyStoredProc", new SqlConnection("my DB connection string"));
            cmd.CommandType = System.Data.CommandType.StoredProcedure;
            cmd.CommandTimeout = 0;
            cmd.Connection.Open();

            cmd.BeginExecuteNonQuery(
                (IAsyncResult result) =>
                {
                    cmd.EndExecuteNonQuery(result);
                    cmd.Dispose();

                    // Invoke the callback if it's provided, ignoring any errors it may throw.
                    var callback = result.AsyncState as Action;
                    if (callback != null)
                        callback.Invoke();
                },
                onUpdateCompleted);
        }
        catch (Exception ex)
        {
            // Handle errors...
        }
    }

The onUpdateCompleted callback action is passed to the BeginExecuteNonQuery method as the second argument and is then consumed in the AsyncCallback (the first argument). This works like a charm both when debugging inside VS and when deployed to IIS.